Skip to content

feat(dingtalk): stream AI trace cards and unify trace streaming - #226

Open
skywclouds wants to merge 14 commits into
OpenBMB:mainfrom
skywclouds:feat/dingtalk-stream-trace-cards
Open

feat(dingtalk): stream AI trace cards and unify trace streaming#226
skywclouds wants to merge 14 commits into
OpenBMB:mainfrom
skywclouds:feat/dingtalk-stream-trace-cards

Conversation

@skywclouds

Copy link
Copy Markdown
Contributor

Summary

Introduce a shared trace_streamer core that drives AI trace cards for both Feishu and DingTalk, replacing the previous Feishu-only inline implementation.

  • DingTalk: add interactive card create-and-deliver plus streaming output with flowStatus state transitions (processing / input / complete / failed), threaded through the DingTalk adapter and service intake.
  • Feishu: refactor the streamer onto the shared core, removing the duplicated inline implementation.
  • Config: add the DingTalk card streaming config keys.

Intent & risk

Enables real-time AI trace cards in DingTalk alongside the existing Feishu support. The refactor consolidates shared streaming logic, so the main risk is regression of existing Feishu trace cards; covered by the test suite below.

Tests run

  • backend/.venv/bin/python -m pytest backend/tests/test_channel_dingtalk.py backend/tests/test_dingtalk_trace_streamer.py backend/tests/test_feishu_trace_streamer.py backend/tests/test_chat_trace.py — 153 passed.
  • backend/.venv/bin/ruff check on changed files — no new issues introduced (pre-existing SIM/UP041/DTZ warnings unchanged).

UI validation

DingTalk interactive card rendering requires live channel credentials; visual verification performed locally via the DingTalk test robot.

Introduce a shared trace_streamer that drives AI trace cards for both
Feishu and DingTalk, replacing the Feishu-only inline implementation.
Add DingTalk interactive card create-and-deliver plus streaming output
with flowStatus states, and thread the trace into service intake. Refactor
the Feishu streamer onto the shared core and add focused regression tests
for the DingTalk streamer and channel trace cards.
@skywclouds

Copy link
Copy Markdown
Contributor Author

新增提交:542f9a4 — SOP 转人工节点下线「处理人」下拉框,转人工优先路由到渠道默认处理人

背景:数字员工 SOP 编辑器(源码 / 流程图视图)的转人工节点带有一个「处理人」下拉框,实际使用中用户经常不配置或错误配置,导致转人工落到错误的回退对象。

改动

  • 前端(DistillPage.tsx):源码视图与流程图视图的处理人下拉框均通过新开关 HANDOFF_ASSIGNEE_SELECTOR_ENABLED = false 下线,原 JSX 代码完整保留,改回 true 即可恢复 UI。
  • 后端(human_handoff_service.py):新增 HANDOFF_STEP_ASSIGNEE_ENABLED = False,运行时忽略 SOP 节点上的 assignee_user_id/assignee_notify_channel(历史数据保留、校验不破坏)。处理人优先级变为:渠道默认处理人 → 数字员工负责人 → 租户管理员(仅移除「SOP 节点指定」一级,其余顺序不变)。
  • handoff metadata 新增 assignee_source(step / binding_default / fallback)记录命中来源;命中渠道默认处理人时不再追加「由于没有配置处理人…」提示,仅回退到 owner/admin/队列时提示。

回滚:把前后端两个开关改回 true 即可,现有代码零删除。

测试:后端 2052 passed / 前端 244 passed / ruff 无新增问题 / vite build 通过。依赖旧优先级的测试改为 monkeypatch 打开回滚开关验证回滚路径,另新增 4 个现行方案测试。

渠道 handoff_notice 与网页收件箱卡片共用 core.handoff_notice 构建:
标题(SOP·节点)、提问人、未配置处理人说明、SOP 入口到转人工的对话
窗口。渠道通知头部补上未配置处理人时的实际转接对象(与用户侧回复
同源),网页 /api/chat/handoffs 暴露统一 notice 内容,收件箱卡片
改为渲染该内容,旧字段回退保留。
purge_chat_session_records 是所有会话删除路径(用户删会话/删员工/
删团队/启动孤儿清扫)的汇聚点,在此级联取消该会话的 pending 转人工
请求,避免原会话删除后残留记录一直挂在收件箱里。仅取消 pending,
历史状态保留审计,取消原因写入 metadata_json.cancelled_reason。
@skywclouds

Copy link
Copy Markdown
Contributor Author

新增提交:1b25dce + 64dbadc — 统一转人工通知的渠道端/网页端生成,并补上会话删除的级联取消

背景:客户反馈转人工时渠道端(飞书/企微)与网页端"待回答"收件箱的内容不一致:网页端有「由于没有配置处理人,已经转接给Administrator」提示,渠道端没有;且渠道端通知内容此前问题较多——slot 英文键原样输出(与用户原话三重复)、上下文仅回看 2 条、多处截断不一致(240/300/600/800)。

改动一:统一通知生成(1b25dce

  • 新增 backend/app/core/handoff_notice.py:渠道通知文本与网页收件箱卡片共用同一份内容构建——标题(SOP 名·触发节点名)、提问人、未配置处理人说明(复用 HumanHandoffService.unconfigured_assignee_notice,与用户侧回复同源)、自进入 SOP 起到转人工的完整对话窗口(以 skill_started/skill_resumed 事件定位入口,起点回退到事件前最近一条用户消息)。
  • 渠道通知头部新增「由于没有配置处理人,已经转接给XXX。」(命中渠道默认处理人时不显示);删除整个 slot 展示区块;统一字数预算(整条 ≤1800 字低于适配器 2000 拆条阈值,保护引用回复关联;超预算从最旧丢弃并标注省略条数)。
  • 网页端 /api/chat/handoffs 返回新增 notice 结构化字段(仅 pending 计算),前端收件箱卡片(ChatDialogs.tsx)渲染统一内容,旧字段回退保留;提问人身份改为按会话所属 binding 解析。

改动二:会话删除级联取消(64dbadc

  • 实测数据中存在孤儿 handoff:原会话被删除后 pending 记录残留,一直挂在"待回答"收件箱里无法清除。在 purge_chat_session_records(用户删会话/删员工/删团队/启动孤儿清扫的公共汇聚点)级联取消该会话的 pending 转人工请求,取消原因写入 metadata_json.cancelled_reason 供审计;answered 等历史状态保留。

测试:后端全量 2059 passed(新增渠道含/不含未配置说明、网页 notice 一致性、SOP 入口窗口、超预算降级、无入口事件回退、会话/团队删除级联取消等回归);前端 246 passed(新增 ChatDialogs 卡片测试),build / i18n:check / config:check 通过。

UI 验证:admin 角色在浏览器打开"待回答"收件箱确认统一卡片渲染;飞书侧触发转人工确认通知头部含转接对象说明。

@skywclouds

Copy link
Copy Markdown
Contributor Author

补充说明:转人工通知的消息格式也已重构(1b25dce

此前渠道端给处理人的通知是平铺式结构,信息挤压在一起靠冒号区分,实际效果(真实客户案例节选):

【人工介入转接】已转接给真人员工 Administrator
问题:[转交真人法务]提问人:飞书用户679cOf6c 1.时间期限:9月初 2.合作伙伴名称:AA ...(用户7点答复原文)
已收集信息:topic:开源模型使用合规 eventdescriptionand_purpose:合作伙伴AA在PR中提及...(slot 全量展开,英文键名)
上下文:assistant:感谢您提供的信息,我已记录以下背景...(240字硬截断)user:1.时间期限:9月初...(用户答复第2次重复)
如需答复,请直接回复本条消息(引用后输入答复内容);也可发送/回复反馈<答复内容>。

主要问题:同一信息重复 3 次(用户原话 / slot 展开 / 聚合 slot 重复序列化)、英文 slot 键对处理人不可读、上下文仅回看 2 条且在句中硬截断、SOP 名称不展示。

更改后的格式

【转人工】法律咨询·转交真人法务
提问人:张三
由于没有配置处理人,已经转接给Administrator。
────────────────
对话记录(自进入该SOP起):
  用户:合作伙伴在PR里用了我们的开源模型,想咨询合规问题
  助手:请补充:时间期限、合作伙伴名称、金额与地区等信息。
  用户:1.时间期限:9月初 2.合作伙伴名称:AA 3.金额与地区:无金额、大陆
  助手:好的,正在为您转接人工。
────────────────
如需答复,请直接回复本条消息(引用后输入答复内容);也可发送 /回复反馈 <答复内容>。

结构说明:

  • 标题行【转人工】{SOP名}·{触发节点名},SOP 名由 trigger_skill_id 查 Skill 表获得,解决了此前不知道是哪个 SOP 转人工的问题
  • 未配置处理人说明:回退链命中的处理人(owner/admin/队列)时展示实际转接对象,与网页端用户可见回复同源;命中渠道默认处理人时不展示
  • 对话窗口:自触发 SOP 的用户消息起、到转人工回复止的完整往返,替代原"上下文摘要"(仅回看 2 条);不再展示 slot 展开区块——信息已在用户原话里,英文键名没有人类可读价值
  • 超长控制:整条 ≤1800 字(低于适配器 2000 字拆条阈值,避免拆多条破坏处理人引用回复的匹配);单条消息超 400 字句末边界截断;窗口超预算时从最旧轮次丢弃并标注 (较早的 N 条对话已省略)
  • 兜底:查不到 SOP 入口事件(历史数据)时回退完整会话记录并不带窗口标注;无任何会话消息时回退 pending_question

- service_intake: keep DingTalk trace streamer import alongside upstream
  WeCom stream reply sink initialization
- service_outbox: keep unified handoff notice builder from
  app.core.handoff_notice, adopt upstream channel-specific reply footer
  (feishu quote-reply vs /回复反馈 <handoff_id> for other channels)
- human_handoff_service: drop duplicated resolver calls after upstream
  moved them before the existing-handoff check, keep toggle comment
Router reason/user_intent 可能夹带 conversation 等内部枚举值并原样
渲染到飞书/钉钉执行卡片。在 TraceStreamer 渲染层净化无 SOP 轮次的
展示文案:已知枚举映射中文、其余英文删除,保留「SOP」与用户原文
复述的英文;入库事件数据不变。同时约束 Router prompt 输出中文,
并将本轮用户消息传入流式器用于白名单判断。
@skywclouds

skywclouds commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

新增提交:dc25f8e — 飞书 scope 未回填时按 provider_tenant_key 推导生效作用域

背景:客户在 SOP 编辑器修改数字员工 SOP的转人工节点的节点说明时被拒绝,报错「人工节点处理人在租户内可用的飞书绑定作用域下未绑定渠道身份,无法按该渠道转接」。实际该处理人(admin)的飞书身份与绑定是同企业同应用的,属于误判。

根因:启动迁移 _migrate_channel_account_key_schema 每次重启都会把非企微绑定的 identity_scope_key 重置为空串,直到下一条飞书入站事件才回填(stage_feishu_inbox)。而权威解析函数 external_account_scope() 对飞书直接返回空串,与身份实际所在的 app:<app_id>:tenant:<tenant_key> 作用域匹配不上。绑定回显(channel_binding_read)此前已有按 app_id + provider_tenant_key 推导的逻辑,但校验/投递链路用的权威解析器没有。同一问题还会导致重启后(下一条入站事件前)转人工通知投递查不到处理人身份,静默降级到网页收件箱。

改动

  • external_account_scope()service_identity.py):飞书绑定 identity_scope_key 为空时按 app_id + provider_tenant_key 推导,与入站回填、绑定回显同一格式;学得信息不全(无 provider_tenant_key)时保持空串,不做猜测。
  • 测试:test_channel_scope.py 新增 scope 推导测试(已回填/重置后/未学得三态);test_skill_editing_and_stats.py 新增回归测试复现本次事故(scope 未回填 + 身份挂在推导 scope 下,校验应通过);test_feishu_handoff.py 夹具修正——身份默认 scope 对齐绑定的生效 scope(生产数据中身份总是在入站回填后按该 scope 落库),并移除 5 处对 external_account_scope 返回空串的手动 mock,改走真实解析链路。

验证:用客户真实数据库副本复现报错并确认修复后校验通过;后端全量 2120 passed / 11 failed(失败项与改动前完全一致,均为存量问题);ruff 无新增告警。

@skywclouds

Copy link
Copy Markdown
Contributor Author

本 PR 新增一条修复:网页控制台发起的回合不再向外部渠道(飞书等)推送回复(d35b082)。

问题:之前投递决策只看会话的 channel 锚定字段,不看每条消息的回合来源。因此在网页端打开飞书绑定的会话发消息时,数字员工的回复也会被推送到飞书侧。

修复方案(按回合来源决定是否投递)

  • 用户消息 metadata 记录回合来源 channelconversation_projection.py / api/chat.py
  • stage_channel_delivery 新增来源检查:web 来源的回合直接跳过投递登记(service_outbox.py
  • _finish_with_error 补传 user_message_id,web 回合的报错回复也不推送渠道

修复后的行为

  • 渠道端发消息 → 网页端可见消息与回复,回复同时推送到渠道 ✓
  • 网页端发消息 → 回复只在网页端可见,不推送渠道 ✓
  • 既有能力保留:human_handoff_resume(人工接管回复)、定时任务、团队唤醒仍正常投递到渠道;取消/中断通知跟随被取消回合的来源
  • 兼容性:存量历史回合无来源标记,维持原有投递行为

测试:新增 3 个回归测试(web 来源不投递 / 渠道来源正常投递 / legacy 数据保持投递),更新 2 个 metadata 断言。全套件 2103 passed,失败项与改动前完全一致(均为遗留问题,已逐一比对确认);ruff 无新增问题。

@skywclouds

skywclouds commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

新增一条飞书修复:reaction 登记静默跳过 + 入站 target 污染(b784e74,均为 upstream 回归,经 merge upstream/main 带入)。

问题 1:飞书"处理中"✅ reaction 不再登记test_feishu_durable_inbox 回归,e7b6620 引入)

  • 根因:_stage_received_reaction 的能力门禁依赖 adapter 注册表,而飞书 adapter 靠模块导入副作用注册。inbox 守护进程独立于 start_channel_services 被调用时(单测/组件化部署),channel_reaction_token('feishu') 返回 None → reaction 被静默跳过
  • 修复:_stage_received_reaction 在门禁前幂等调用 _ensure_adapters_registered() 兜底;_ensure_adapters_registered 改为跳过已注册渠道,不覆盖测试注入的 fake adapter(新增 channel_adapter_registered() 谓词)

问题 2:飞书群聊会话 target 混入 wecom 风格键test_feishu_durable_inbox 回归,PR #203 引入)

  • 根因:process_inbound 把暂存事件 target 与通用基础形状 {**target, **event.target_json} 合并,导致 wecom 群聊键(to_user_id/reply_quote/is_group/reply_to_user_id)污染飞书会话的 channel_target_json
  • 修复:暂存事件的 target 由各渠道 inbox 按该渠道投递形状构建,即为权威目标,直接采用不再合并。已验证对其他渠道无影响:wecom 暂存 target 自足;WeChatKfAdapter.send 只读 to_user_id+open_kfid(暂存已含);dingtalk 暂存 target 自含所需键

测试test_feishu_durable_inbox 16/16 通过(原 2 失败已修复);9 个渠道套件 399 通过;全量 2125 通过,剩余 9 个失败为既有的 team_id 迁移顺序问题(_migrate_wechat_kf_accounts 先查后建列,与本修复无关,upstream 同样存在)。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant