Skip to content

feat(handoff): route assignee notifications by channel - #200

Open
skywclouds wants to merge 15 commits into
OpenBMB:mainfrom
skywclouds:feat/handoff-assignee-notify-channel
Open

feat(handoff): route assignee notifications by channel#200
skywclouds wants to merge 15 commits into
OpenBMB:mainfrom
skywclouds:feat/handoff-assignee-notify-channel

Conversation

@skywclouds

Copy link
Copy Markdown
Contributor

Intent

人工转交的处理人现在可以指定通知渠道:SOP handoff 节点与渠道绑定默认处理人均支持 assignee_notify_channelNone=默认投递、web=仅网页收件箱、绑定渠道如 feishu=按该渠道转接)。同时收紧 SOP 编辑器:处理人下拉框与转人工动作仅出现在 handoff 类型节点上,回复/收集等节点不再提供转人工能力。

Changes

后端

  • SkillGraphNode / ChannelBindingAgentsUpdate / ChannelBindingRead 新增 assignee_notify_channel 字段
  • 保存渠道默认处理人时校验:处理人必须是租户内部成员;指定绑定渠道时须已在当前绑定作用域绑定非群聊渠道身份
  • Skills API 校验 SOP 节点渠道可达性,不可达时返回 400
  • HumanHandoffService 按命中配置携带通知渠道(SOP 节点 → 渠道默认 → 负责人/管理员),agent_loop 按渠道偏好决定是否经当前 binding 投递
  • 修复:恢复人工转交时以会话属主作为 user,渠道身份重绑后不再触发 harness 的 session-user 围栏失败

前端

  • 新增 lib/handoff-assignee(格式化/解析 user::channel 值),渠道页与 SOP 编辑器共用
  • 渠道页默认处理人下拉支持"姓名(渠道)"变体;SOP 编辑器(源码/流程视图)同步支持
  • SOP 编辑器:非 handoff 节点隐藏处理人下拉框并从动作选项中移除转人工;节点类型切离 handoff 时自动清理动作与处理人

Risk

  • 存量 SOP 中非 handoff 节点上的 handoff_human 动作仍被运行时识别(数据不迁移、不失效),仅 UI 不再提供新增入口
  • 通知渠道校验放宽了原"飞书必须可达"的限制为"仅指定渠道时校验",未指定渠道的存量行为不变

Tests

  • backend/.venv/bin/python -m pytest backend/tests — 1838 passed(本地环境另有 35 个与本次改动无关的既有失败,已在干净工作树复现确认)
  • npm --prefix frontend-enterprise test — 212 passed
  • npm --prefix frontend-enterprise run build — 通过
  • npm --prefix frontend-enterprise run i18n:check — 通过
  • 新增回归:test_channel_api.pytest_feishu_handoff.pytest_human_handoff_policy.pytest_graph_rules.pytest_skill_editing_and_stats.pylib/handoff-assignee.test.tsDistillPage.handoffRestrictions.test.tsx

UI Validation

  • 路由:/enterprise/channels(管理员)验证默认处理人渠道变体保存与回显;/enterprise/skills SOP 编辑器源码/流程视图验证 handoff 节点处理人、非 handoff 节点无处理人与转人工动作、类型切换自动清理

Wang He added 3 commits August 20, 2026 11:56
- SOP handoff nodes and channel bindings gain assignee_notify_channel:
  None keeps default delivery, "web" limits to the web inbox, and a bound
  channel (e.g. feishu) forwards the handoff via that channel
- validate the assignee is an internal member and, when a channel is
  requested, that a non-group channel identity is bound in the binding
  scope; skills API rejects unreachable channel specs
- surface channel-labelled assignee options in the channels page and the
  SOP editor (source and flow views), sharing the new handoff-assignee
  helpers
- restrict the SOP editor: the assignee picker and handoff action now
  appear only on handoff-type nodes, and switching a node away from
  handoff strips its handoff action and assignee
- resume handoff turns as the session owner so harness user fencing
  passes after channel-identity rebinding migrates session.user_id
Deleting an agent left orphan rows in channel_binding_agents, so the
Feishu/DingTalk /员工 command listed a bare agent_id for a deleted
employee and /切换 could route a conversation to it. delete_agent now
removes the agent's channel mounts, promotes the first remaining mount
to default (syncing binding.agent_id) when the default mount is deleted.
mounted_agents additionally filters mounts whose agent no longer exists,
so legacy orphan rows self-heal across listing, switching, autorouting,
and the channels page; conversation pointers keep resetting through the
existing resolve_current_agent fallback.
@skywclouds

Copy link
Copy Markdown
Contributor Author

追加修复:删除数字员工后渠道挂载未清理(debff16)

问题

删除员工(如 agent_508e762658ef49cd)后,channel_binding_agents 里残留孤儿挂载行,导致飞书/钉钉 /员工 指令列出已删除员工的裸 agent_id,/切换 还能把会话路由到已删除的员工。

修复(两层)

  1. 根因delete_agent 删除员工时同步删除其所有渠道挂载行;若删除的是默认挂载,将首个剩余挂载提升为默认并同步 binding.agent_id
  2. 兜底mounted_agents 读取挂载集时过滤指向已不存在员工的孤儿行——/员工/切换、智能分发、渠道页挂载展示均经由该函数,历史脏数据一次性自愈;会话指针由 resolve_current_agent 既有的"指针不在挂载集 → 重置默认"逻辑自动复位

测试

  • test_channel_routing.py 新增 4 个回归用例:孤儿挂载过滤(含 /员工 不再列出已删员工)、挂载全部失效时回退绑定默认、删除清理+默认挂载提升、删除唯一挂载的边界
  • test_channel_routing.py + test_channel_api.py + test_channel_autoroute.py + test_slash_commands.py 共 109 个测试全部通过

The Feishu setup page now lists all twelve permissions the integration
needs: three contact-reading scopes (contact:contact.base:readonly,
contact:user.base:readonly, contact:user.id:readonly), the group
message scopes (group_at_msg, group_at_msg.include_bot, group_msg,
group_msg.include_bot:read, im:message:readonly) alongside the existing
p2p/send/reaction scopes. Directory and sensitive group-read entries
move out of the "usually removable" list since they are now required,
and the tooltip copy plus English translations follow suit.
@skywclouds

Copy link
Copy Markdown
Contributor Author

追加:飞书接入页权限名单补全(d7ac5b1)

变更

接入飞书向导的"必需权限"从 5 条扩充到 12 条,覆盖实际运行所需的完整权限集:

  • 通讯录(新增 3 条)contact:contact.base:readonly(获取通讯录基本信息)、contact:user.base:readonly(获取用户基本信息)、contact:user.id:readonly(通过手机号或邮箱获取用户 ID)
  • 群消息(新增 4 条)im:message.group_at_msg.include_bot:readonlyim:message.group_at_msg:readonlyim:message.group_msg(敏感权限)、im:message.group_msg.include_bot:read
  • 补充 1 条im:message:readonly(获取单聊、群组消息)
  • 原有 5 条保留:p2p_msg、send_as_bot、reactions:read、reactions:write_only 等

同时把"通讯录权限范围 / 获取用户 user ID / 读取群内全部消息的敏感权限"从"通常可删除"列表移除(与新增必需权限冲突),tooltip 提示文案与 en.json 翻译同步更新。

验证

  • npm --prefix frontend-enterprise run i18n:check 通过(3181 条翻译)
  • npm --prefix frontend-enterprise run build 通过
  • UI:/enterprise/channels → 接入飞书向导确认权限清单展示

The SOP call count in the admin console was stuck at zero: skill stats
count skill_started/skill_resumed agent events, which only the removed
legacy agent loop used to emit. Harness v2 now records the events when
an SOP task frame is activated — start_new_task emits skill_started,
switch_to_pending emits skill_resumed, continuing the active SOP stays
uncounted — with the legacy payload shape so the stats reader, trace
consumers, and Feishu traces keep working.

Also fix a fallout from the mount cleanup: adopt_orphan_channel_sessions
must claim sessions using raw mount rows instead of the user-facing
filtered mount set, since historical sessions can belong to deleted
employees and still need to migrate to the new binding.
@skywclouds

Copy link
Copy Markdown
Contributor Author

追加修复:数字员工管理端 SOP 调用次数永远是 0(8424a5a)

根因

SOP 调用次数统计(api/skills._skill_stats)只认 skill_started / skill_resumed 两种 AgentEvent。这两个事件原本由 legacy agent loop 的 _record_runtime_event 产生;4bdab76(refactor: remove legacy agent loop runtime)删除该运行时后,Harness v2 一直没有补发事件,导致统计自那以后始终为 0。

修复

  • harness_v2_engine_activate_frame 调度 SOP 任务帧时新增 _record_skill_activation_event——start_new_taskskill_startedswitch_to_pendingskill_resumedcontinue_active(继续当前 SOP)不重复计数,非 SOP 帧不落事件
  • payload 保持旧结构to_skill_id / to_skill_version / from_skill_id / from_step_id 等),统计读取方、会话 trace、飞书 trace 均零改动兼容
  • 顺带修复:全量测试暴露出上一提交挂载过滤的回归——adopt_orphan_channel_sessions(孤儿会话认领)改用挂载表原始行取 agent 集(历史会话的 agent 可能已删除,但归属仍需迁移到新绑定),无挂载行时回退 binding.agent_id

验证

  • 新增回归测试 test_activate_frame_records_skill_call_events_for_stats(覆盖 4 种 decision + 事件结构 + 恢复次数,连跑 5 次稳定)
  • 端到端验证:事件 → _skill_statscall_count 正确计数
  • test_harness_v2.py + test_skill_editing_and_stats.py + test_channel_binding_lifecycle.py 共 124 个测试通过;全量后端测试与基线一致(仅既有 LLM client 环境失败)

注意

历史调用因事件从未落库无法追溯统计;修复部署后新产生的对话调用会开始正常累计。

@fadeoreo

Copy link
Copy Markdown
Collaborator

整体方案和 Feishu 场景没问题,相关测试也覆盖到了。

但目前“按渠道转接”的实现还不完整:前端和 SOP 配置允许选择 dingtalk/wecom 等渠道,后端校验也允许保存这些渠道;运行时实际通知仍只走 Feishu:

AgentLoop._maybe_notify_handoff_assignee_on_feishu
notify_handoff_assignee

因此选择 dingtalk/wecom 后,人工转接会创建成功,但处理人不会收到对应渠道通知,只能在网页端看到。

请二选一:

  1. 补齐各渠道的人工转接通知;
  2. 如果当前只支持 Feishu,就限制前后端只能选择 Feishu,并在后端拒绝其他渠道配置。

@skywclouds

Copy link
Copy Markdown
Contributor Author

采用方案 2:限制前后端只能选择飞书(11bf079)

选择钉钉/企微后配置保存成功但处理人收不到渠道通知,根因是钉钉适配器只能通过 sessionWebhook 回会话内消息、企微/微信出站同样依赖会话上下文(context_token / chatid),均无法对处理人发起主动私聊;补齐需要各渠道全新的服务端 API 集成,超出本 PR 范围,因此按评论建议收窄为仅支持飞书:

后端拒绝其他渠道配置

  • PUT /channels/{id}:指定具体转接渠道且绑定非飞书 → 400「人工转接通知当前仅支持飞书渠道」(app/api/channels.py
  • SOP 保存(create/update):assignee_notify_channel 为非 feishu 渠道 → 400(app/api/skills.py _validate_handoff_assignees),并保留既有"处理人须已绑定该渠道身份"校验

前端只提供飞书选项

  • SOP 人工节点处理人选择器:只为飞书身份生成「姓名(飞书)」标注,dingtalk/wecom 身份不再出现在选项中(DistillPage.tsx handoffAssigneeUserOptions
  • 渠道绑定默认人工处理人:渠道标注选项仅对飞书绑定渲染,提示文案注明「当前仅支持飞书」(ChannelsPage.tsx

测试

  • test_channel_api.py 新增:钉钉绑定即使处理人已绑定钉钉身份仍被 400 拒绝,web 选项不受影响
  • test_skill_editing_and_stats.py 新增:SOP 节点钉钉渠道被 400 拒绝
  • 前端新增 handoffAssigneeUserOptions 选项过滤用例(只出飞书标注,过滤其他渠道及渠道懒建账号)
  • 相关后端 4 个套件 229 个测试、前端 213 个测试全部通过

后续如需支持钉钉/企微转接(钉钉企业内部机器人单聊 API / 企微应用消息),再单独开 PR 补齐运行时通知链路并放开此限制。

@skywclouds

Copy link
Copy Markdown
Contributor Author

追加修复 1:限制转接渠道只能选飞书(11bf079)

采用方案 2:选择钉钉/企微后配置保存成功但处理人收不到渠道通知,根因是钉钉适配器只能通过 sessionWebhook 回会话内消息、企微/微信出站同样依赖会话上下文(context_token / chatid),均无法对处理人发起主动私聊;补齐需要各渠道全新的服务端 API 集成,超出本 PR 范围,因此按评论建议收窄为仅支持飞书:

后端拒绝其他渠道配置

  • PUT /channels/{id}:指定具体转接渠道且绑定非飞书 → 400「人工转接通知当前仅支持飞书渠道」(app/api/channels.py
  • SOP 保存(create/update):assignee_notify_channel 为非 feishu 渠道 → 400(app/api/skills.py _validate_handoff_assignees),并保留既有「处理人须已绑定该渠道身份」校验

前端只提供飞书选项

  • SOP 人工节点处理人选择器:只为飞书身份生成「姓名(飞书)」标注,dingtalk/wecom 身份不再出现在选项中(DistillPage.tsx handoffAssigneeUserOptions
  • 渠道绑定默认人工处理人:渠道标注选项仅对飞书绑定渲染,提示文案注明「当前仅支持飞书」(ChannelsPage.tsx

测试:新增钉钉绑定渠道选项被 400、SOP 节点钉钉渠道被 400、前端选项过滤等回归用例;相关后端 4 个套件 229 个测试、前端 213 个测试全部通过。后续如需支持钉钉/企微转接再单独开 PR 补齐运行时通知链路并放开限制。


追加修复 2:飞书身份绑定列表显示异常(08571c0)

修复三个显示问题:

  1. 「已绑定:用户609115」与「Administrator 已绑定」实为同一身份重复——根因是 _bind_external_identity 重绑身份时只改指针不更新 display_name,残留懒建期占位名(如「飞书用户 xxx」)。修复:绑定/解绑时同步 identity.display_name 为账号名(service_intake.py / service_identity.py);my-identity-bindings 读取接口对存量脏数据自愈回写,已有数据刷新即修复
  2. 外部用户(渠道懒建账号)出现在「已绑定成员」——identityBoundUsers 未过滤 source,懒建账号自带的身份被误算入。修复:只统计内部成员;同时「已绑定成员」名单与「邀请绑定」下拉均排除当前用户(自己的绑定在上方「身份绑定」区有专门入口),消除 Administrator 出现两次的重复
  3. scope 徽章显示内部技术键 Bot: app:20:cli_...:tenant:16:...——新增 lib/identity-scope.ts 按长度前缀解析飞书 scope,展示为「飞书应用: cli_xxx」

测试test_channel_bind.py 补绑定/解绑显示名同步断言 + 接口自愈新用例(35 个通过);新增前端 scope 解析用例 4 个(通过);前端全量 217 个测试通过。

Wang He added 2 commits August 20, 2026 16:21
删除团队此前只清理成员/任务等表,带 team_id 的会话残留并在对话端
全局列表可见。抽出共享清理助手 session/cleanup.py(消息/事件/反馈/
Harness 记录同事务删除,commit 后清 Harness 工作区),delete_team 与
delete_chat_session 端点统一复用。
删除员工此前会话全部残留,且发消息会由租户默认 persona 静默接管
回复。现在删除员工时级联清空其全部会话(含 Harness 工作区),同步
清理团队成员关系,active 定时任务立即 paused,pending 人工转接
标记 cancelled。
@skywclouds

Copy link
Copy Markdown
Contributor Author

新增修复:删除团队/数字员工后对话记录残留

本分支新增两个级联清理修复,与原 handoff 通知改动互不影响:

根因

  • delete_team 只清理成员/任务/竞标/黑板等表,带 team_idChatSession 成为孤儿会话,仍匹配对话端全局列表查询(title LIKE '%TL 对话%' + user_id),已删团队的聊天记录继续可见。
  • delete_agent 只清理资源绑定和渠道挂载,其会话全部残留且仍可继续对话:新消息由租户默认 persona 静默生成回复,前端还会错误显示默认员工名片。

改动

  • 新增 backend/app/session/cleanup.py:共享清理助手 purge_chat_session_records(同事务删除 Message/AgentEvent/MessageFeedback/SkillFeedback + Harness v2 记录 + 会话行)与 remove_chat_session_workspace(commit 后清理 Harness 磁盘工作区)。
  • delete_team(teams/service.py)与 delete_chat_session 端点统一复用该助手。
  • delete_agent(api/agents.py)在原有渠道挂载清理基础上,级联清空其全部会话(含 Harness 工作区),同步清理 TeamMember 成员关系,active 定时任务立即 paused(原需等下次触发才失败),pending 人工转接标记 cancelled

风险

  • 删除团队/员工会连带删除相关聊天记录与 Harness 工作区,属破坏性操作,但与删除会话端点的既有语义一致;渠道绑定对已删团队已有兜底提示,无需处理。

测试

  • backend/.venv/bin/python -m pytest backend/tests:1855 passed / 34 failed(34 个失败为基线存量问题,与本次改动无关,已用 stash 基线对比确认)。
  • 新增回归测试:test_delete_team_purges_team_chat_sessionstest_delete_team_cleans_team_session_harness_state_and_workspacetest_delete_agent_purges_sessions_membership_tasks_and_handoffs
  • ruff:改动文件无新增诊断(agents.py 与基线逐条 diff 确认仅行号平移)。
  • UI 验证:未涉及前端改动,无路由/角色变更。

级联清理上线前删除团队/员工残留的孤儿会话,init_db 启动时幂等清理,
复用与删除时相同的 purge 逻辑。
@skywclouds

Copy link
Copy Markdown
Contributor Author

新增启动时一次性清理:自动删除团队或数字员工已被删除但残留在库中的历史孤儿会话。

@skywclouds

Copy link
Copy Markdown
Contributor Author

飞书流式卡片文案用户可读化(新增提交 08fef6e

问题

飞书渠道流式卡片直接展示技术细节,用户看不懂:

  • step handoff_to_repair_specialiststep reply_final_result(SOP 节点 id)
  • 状态 handoff(task frame 状态码)
  • 能力调用完成 hr.balance_querycapability_describe(工具调用名)
  • step_completed(skill_completed 的 reason)

方案

飞卡片复用 _event_trace_lines 渲染,通过新增 step_names / tool_names 参数开启用户可读模式,仅飞书流式器传入;Web 控制台不传,保持原有技术视图不变。

  1. 步骤 id → 中文名(chat.py _resolve_step_label):优先查 Skill.content_json.nodes 节点名,查不到按常见模式兜底(handoff→转人工处理、collect→收集需要的信息、end→流程结束 等)
  2. 状态码 → 中文_FRAME_STATUS_TEXTS):handoff→已转人工处理、awaiting_user→等待用户补充信息、action_budget→本轮处理已达上限 等
  3. 工具名 → 中文_resolve_tool_label):查 toolsdisplay_name/description;内置保留能力映射(capability_describe→查看能力详情 等);通用技能映射中文名
  4. reason → 中文_SKILL_COMPLETED_REASON_LABELS):step_completed→全部步骤已完成
  5. 周边术语:选择SOP/推进SOP/完成SOP → 进入流程/推进流程/完成流程;SOP TaskFrame/单步上限/Harness 最多 N 轮 等细节不再下发到卡片
  6. 数据加载(feishu_trace.py _load_skill_trace_names):一次查询加载技能名/节点名/工具名,懒加载+失败隔离,不影响主流程

测试

  • 新增 12 个回归测试:卡片渲染(步骤/状态/工具名/reason)、DB 加载、兜底模式、Web 控制台视图不变、原始行为保持
  • python -m pytest backend/tests:失败集与改动前完全一致(34 个为环境相关存量失败,已逐一比对确认无新增)
  • ruff check 无新增问题(存量 75 个与基线一致)

UI 验证

  • 渠道/角色:飞书渠道绑定用户 + 飞书客户端流式卡片
  • 实测对话卡片输出:✅ 进入流程 假期余额查询 / 当前步骤 收集员工工号与假期类型 / ✅ 能力调用完成 假期考勤查询 / ✅ 完成流程 假期余额查询 · 全部步骤已完成

@fadeoreo

Copy link
Copy Markdown
Collaborator
  • 当前 PR 只支持 Feishu,并限制 schema/UI;
  • 要么根据 notify_channel 找到对应 binding、identity 和 adapter,真正走通用 outbox。
    另外,SOP 节点的渠道校验目前是按“用户绑定过某渠道”判断,不是按当前具体 binding scope 判断。用户可能绑定过另一个 Feishu 企业,但当前会话所属 binding 不可达,最终仍会静默跳过通知,建议增加 scope 级测试。

@fadeoreo

Copy link
Copy Markdown
Collaborator

增加 一些 流程可读性后的截图

@skywclouds

skywclouds commented Aug 21, 2026

Copy link
Copy Markdown
Contributor Author

@fadeoreo 两条评论均已处理,见提交 174db2c

1. 通用 outbox 路由(去掉 Feishu 硬编码)

投递链路通用化

  • agent_loop._maybe_notify_handoff_assignee(原 _..._on_feishu):按 notify_channel 解析投递 binding——指定渠道时优先会话所属 binding,渠道不匹配则回退租户内该渠道任一 active 员工绑定(resolve_handoff_notify_binding);web 偏好仅网页收件箱;无偏好用会话所属 binding。租户内无可用绑定时记 warning 日志(不再完全静默)。
  • service_outbox.notify_handoff_assignee:不再查飞书 open_id 专用函数,改为 resolve_assignee_channel_identity(binding 渠道 + scope 下的非群聊身份),按渠道构造 target(feishu: receive_id_type=open_id;wecom: to_user_id=chatid),经 outbox worker 用对应渠道 adapter 投递。
  • 新增 HANDOFF_NOTIFY_CHANNELS = {feishu, wecom}:以"适配器能否主动私聊"划分,钉钉/微信只能回会话内消息(webhook/context_token 过期)故不支持。schema/UI 的渠道限制同步改为引用该集合(不再写死 feishu)。

2. SOP 节点校验改为 scope 级可达

skills.py::_validate_handoff_assignees 不再按"用户绑定过某渠道"判断:

  • 计算租户内该渠道全部 active 员工绑定(排除团队绑定/停用绑定)的 external_account_scope 集合;
  • 要求处理人的非群聊身份挂在这些 scope 之一下,多绑定任一可达即通过;
  • 用户绑定的是另一个飞书企业(scope 不在集合内)时返回 400,不再出现"校验通过但投递静默跳过"。

channels.py 默认处理人校验同样改为复用 resolve_assignee_channel_identity(与运行时同一身份解析逻辑,消除两套判断漂移)。

新增测试(13 个)

  • scope 级test_validate_handoff_assignees_scope_level_reachability(跨企业身份不可达→400,补 active 绑定后通过)、test_validate_handoff_assignees_ignores_team_and_inactive_bindings
  • 通用投递:wecom 通知 target 构造、scope 不匹配跳过、钉钉不支持私聊跳过、resolve_handoff_notify_binding 绑定筛选
  • agent_loop 路由:偏好渠道≠会话渠道时回退租户内匹配 binding、偏好与会话一致时优先会话 binding、web/无可用 binding 跳过、默认偏好走会话 binding(企微)
  • channels API:wecom 渠道选项 scope 级校验(跨企业 400 → 同 scope 200)
  • 前端:handoffAssigneeUserOptions 更新为 feishu+wecom

测试结果

  • pytest backend/tests:1876 passed,34 个失败均为 test_llm_client 等环境相关存量失败(与改动前基线一致,无新增)
  • npm test:217 passed;npm run buildi18n:check 通过(剩余 1 条 飞书应用:{1} 缺翻译为存量)

@skywclouds

Copy link
Copy Markdown
Contributor Author

飞书 Trace Card 紧凑模式

  • compact_sop=True 时,SOP 中间步骤折叠为翻书动画(📚/📖)+ "正在推进SOP" 单行
  • 流程状态定格:
    • 暂停(awaiting_user):📖 流程已暂停
    • 完成(skill_completed):✅ 流程已结束
    • 异常(error):❌ 流程未完成
  • 回滚开关:binding.config_json.compact_trace=False 可恢复原逐行样式

Assignees can now answer human handoff notices by replying (quoting)
the notification message directly in feishu, without the /回复反馈
prefix. Replies quoting already-answered notices or ack messages are
consumed with a hint instead of starting a new agent conversation.

- match quote replies against delivered handoff_notice/handoff_ack
  message ids with strict receive_id sender validation
- persist message_id for handoff_ack deliveries so follow-up quote
  replies are consumed too
- bound problem description to keep notices single-message for
  reliable quote matching
- update notice and usage text to advertise direct quote reply
@skywclouds

Copy link
Copy Markdown
Contributor Author

支持处理人直接在飞书里引用(回复)转人工通知消息进行答复,无需 /回复反馈 前缀,且不会触发处理人与数字员工的新对话。

行为变更

  • 引用待处理通知:直接作为人工答复,置 answered 并恢复 SOP,回复确认消息
  • 引用已处理的通知或确认消息:消费并提示「该人工转接请求已处理」,不进入数字员工会话(避免误触发新对话)
  • 引用通知但为纯图片等无文本:提示用文字答复,同样不进入数字员工会话
  • /回复反馈 前缀指令保留为备选,通知文案改为优先引导直接回复本条消息

实现要点

  • 匹配逻辑改为按 parent_id/root_id 命中本 binding 已投递的 handoff_notice/handoff_ack message_id,严格校验发送者 == 通知目标 receive_id(防转发/截图越权)不变
  • handoff_ack 投递成功后回写飞书 message_id,使「引用确认消息再回复」同样被消费
  • 问题描述截断到 600 字保证通知单条投递(拆分会破坏引用匹配,只有末条 message_id 可关联)

测试

  • 新增 7 个回归测试,含 2 个端到端用例断言「无前缀引用回复 → handoff answered、ack 投递、不新建 ChatSession」
  • test_feishu_handoff.py 51 个全过;渠道相关 263 个测试全过;ruff 无新增告警

…ee-notify-channel

# Conflicts:
#	backend/app/api/chat.py
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.

2 participants