feat(channels): 飞书人工介入转接与 /回复反馈 指令 - #187
Conversation
Implement end-to-end feishu human handoff flow: - Knowledge-driven assignee: Contact concept type in okf.py with frontmatter fields (feishu_open_id, feishu_mobile, feishu_email, staffdeck_user_id). resolve_contact_assignee searches all concept types to find the right human staffer. - Feishu notification: notify_handoff_assignee sends a handoff notice to the assignee's feishu open_id (resolved via ChannelIdentity or batch_get_id API fallback). notify_message_id stored for reply matching. - /回复反馈 command: parse_command recognizes /回复反馈 and /handoff_reply prefixes. _run_handoff_reply_command matches sender open_id to pending handoff via ChannelIdentity or contact_target fallback, calls _apply_handoff_reply, and stages confirmation delivery. Intercepted before AgentLoop to avoid triggering the digital employee. - parent_id fallback: _try_handle_feishu_handoff_reply remains as a fallback for direct message replies to the notification. - Delivery fix: _immutable_delivery_target now falls back to chat_session.channel_target_json when client_turn_id is missing or no matching ChannelInboundEvent is found, fixing the delivery_target_missing error on handoff resume replies. - Handoff reply injection: _inject_handoff_context injects handoff_info (with human_reply) on the first turn after resume, controlled by resume_finished_at marker. response_generator_prompt rule 26 requires the LLM to faithfully relay human_reply content. - Tests: 32 regression tests in test_feishu_handoff.py covering Contact concepts, assignee resolution, open_id lookup, handoff notice delivery, /回复反馈 command parsing, handoff reply matching, and delivery target fallback. Fixed test_channel_intake.py, test_channel_team_binding.py (event_sink kwarg), and test_graph_rules.py (sub_sop_id field).
|
这个方向需要调整后再合并。目前实现把人工处理人建模为知识库 建议调整为:
当前数据模型只有一个 |
…el binding priority chain - Remove Contact concept from okf.py (CONCEPT_TYPES, frontmatter keys, extract/search functions) - human_handoff_service.py: remove resolve_contact_assignee, add step_assignee_user_id + binding_default_assignee_user_id params - graph_rules.py: node_as_step passes assignee_user_id from handoff node - agent_loop.py: _create_human_handoff_request reads assignee from SOP node + channel binding; _maybe_notify_handoff_assignee_on_feishu uses session binding for ChannelIdentity lookup - service_outbox.py: _resolve_assignee_feishu_open_id queries ChannelIdentity with binding scope only; notify_handoff_assignee drops contact_target; _build_handoff_problem_description reads User - service_intake.py: _try_handle_feishu_handoff_reply strictly validates receive_id == from_user_id; _run_handoff_reply_command uses ChannelIdentity only; both use source='feishu' kind='handoff_ack' - chat.py: _apply_handoff_reply takes explicit source param; resume worker no longer writes resume_finished_at - harness_v2_engine.py: _inject_handoff_context uses request.channel == 'human_handoff_resume' for resume detection; remove contact_target/assignee_name/assignee_role from handoff_info - harness_agent.py: fix handoff_to_human -> handoff_human - turn_planner.py: when plan.decision == 'handoff_human' and active SOP has a handoff node, route to it (SOP frame) instead of creating a conversation task - turn_finalizer.py: when step_result.handoff=True but current step doesn't declare handoff, route to SOP handoff node before creating handoff request - response_generator_prompt.md: rules 25-26 updated to remove assignee_name/assignee_role references - Tests: 33 feishu handoff tests rewritten; 2 new turn_planner routing tests; 1 new turn_finalizer routing test; full suite 1674 passed
|
感谢详细的 review 意见,已按照全部 7 点要求完成重构,代码已推送到 main 分支(commit 1. 移除知识库 Contact 作为默认指派来源 ✅
2. SOP 人工节点配置处理人 ✅
3. 渠道配置默认人工处理人 ✅
4. 前端配置前端配置 UI(SOP 节点处理人选择器、飞书渠道默认处理人选择器、身份映射状态展示、通知测试)将在后续 PR 单独补充,避免本 PR 范围过大。 5. 正确处理飞书身份映射 ✅
6. 修复现有流程问题 ✅
7. 补充测试 ✅
新增测试:
额外修复:router handoff_human 路由 bug在验证过程中发现一个运行时 bug:当 LLM 返回 修复方案:
端到端验证已在生产环境验证飞书 p2p 会话完整链路:用户发送"我的电脑开不开机了" → SOP 收集故障信息 → LLM 决定转人工 → 路由到 全量测试 1674 passed,0 failed(排除 |
|
当前 PR 还没有形成可用闭环,建议先 Request changes: [P1] SOP 人工节点的负责人无法保存,配置实际不会生效 |
…ing config, precise reply matching, reachable handoff node, dedup notice - Add assignee_user_id to SkillGraphNode schema (round-trip preserved) - Add default_handoff_assignee_user_id to channel binding API + frontend selector - Rewrite /回复反馈 to match by parent_id→notify_message_id; reject multiple pending - GraphRules.find_handoff_node_id: BFS from active node → start node → array fallback - Fix duplicate confirmation message via _HANDOFF_REPLY_HANDLED sentinel in process_inbound - Frontend: tenant user fetch + assignee selectors in DistillPage and ChannelsPage - Tests: 1705 backend passed; 3 new channel API tests; 4 new graph_rules tests; new turn_planner reachable handoff test; feishu handoff reply matching tests
|
感谢第二轮 review,已逐条修复并推送 ( 1. SOP 节点缺少
|
…or in source view
- feishu adapter: add get_user_name() via GET /contact/v3/users/{open_id}
- service_identity: resolve_or_provision_user accepts name_resolver callback,
updates User.display_name and ChannelIdentity.display_name when placeholder
- service_intake: _build_name_resolver wires feishu name resolution into intake
- auth API: UserRead includes channel_identities; list_users?include_channel=true
- DistillPage: SkillSource (source view) now shows 处理人 selector for handoff
nodes, matching the flow inspector behavior
- ChannelsPage/DistillPage: user dropdowns show 真实姓名 (飞书) format
notify_handoff_assignee now resolves the inquirer's display name via ChannelIdentity (binding scope) → User fallback, and prepends a 提问人: line to the problem description sent to the assignee.
前端继续请求 但接口仍调用
群聊也会生成 一旦选中群聊虚拟用户,通知逻辑还会把 处理人候选应是明确的内部 StaffDeck 成员,而不是全部渠道会话身份。
后端用字典按 前端又只展示 建议渠道处理人候选接口接收
需要在 SOP 创建、更新、发布时校验,并在运行时对已删除用户回退渠道默认处理人。
最新提交没有改动
两个 SOP 视图都只传入用户选项,没有“未指定/使用渠道默认”选项:DistillPage.tsx。一旦选人,UI 无法恢复到渠道默认负责人。
占位名称存在时,每次正常入站都会同步请求飞书 Contact API:service_identity.py。如果应用没有通讯录权限,查询永远返回 |
…dge-aware BFS, negative name cache - auth.py: include_channel=true returns all users with channel identities (including external_account_scope); frontend filters source='web' for assignee selectors; non-admin users can list via ensure_current_user_tenant - skills.py: validate assignee_user_id on SOP create/update — must exist, belong to same tenant, and be source='web' (internal member) - graph_rules.py: find_handoff_node_id two-phase BFS — first follows only unconditional edges (condition empty/default/else), then falls back to all edges; prevents picking handoff node on unmatched conditional branch - service_intake.py: negative name resolution cache (5min TTL) for feishu get_user_name — avoids sync API call on every message when contact permission is missing or name unavailable - DistillPage/ChannelsPage: '未指定(使用渠道默认)' option in assignee selectors; scope-aware reachability badge in ChannelsPage - 2 new graph_rules tests for conditional/unconditional BFS
第三轮 Review 修复1. [P1] 普通创建者的处理人选择器为空
2. [P1] 处理人候选混入客户/群聊虚拟账号
3. [P1] 身份状态不能证明当前飞书渠道可达
4. [P1] SOP 节点处理人未校验新增
5. [P1] 条件分支路由问题
新增 2 个测试: 6. [P2] SOP 处理人无法清空DistillPage 两个视图(source view + flow inspector)的 7. [P2] 飞书姓名查询同步重试
测试: backend 1707 passed (excluding |
功能概述
实现飞书人工介入转接的完整端到端流程:知识库驱动匹配处理人 → 飞书通知 → 处理人在飞书内回复 → SOP 恢复并将结果投递回原用户。
实现细节
1. 知识库驱动转接(Phase 1-2)
okf.py新增Contact概念类型,支持 frontmatter 字段:feishu_open_id、feishu_mobile、feishu_email、staffdeck_user_id、name、role、keywords等search_concepts对 Contact 类型加权检索,extract_contact_target从知识概念中提取联系人信息human_handoff_service.py的resolve_contact_assignee搜索所有概念类型找到合适的处理人2. 飞书通知(Phase 3)
feishu.py的send()返回message_id,新增resolve_open_id_by_mobile_or_email通过手机号/邮箱反查 open_idservice_outbox.py新增notify_handoff_assignee,向处理人飞书发送人工转接通知,通知中提示使用/回复反馈回复models.py新增HumanHandoffRequest.notify_message_id字段,database.py增加迁移守卫3. /回复反馈 指令(Phase 4)
service_routing.py的parse_command识别/回复反馈和/handoff_reply前缀service_intake.py新增_run_handoff_reply_command:匹配发送者 open_id → ChannelIdentity → staffdeck_user_id → pending handoff;回退匹配contact_target.feishu_open_id。命中后调用_apply_handoff_reply并给处理人回确认process_inbound中早于 AgentLoop 拦截,不会触发数字员工_try_handle_feishu_handoff_reply(parent_id 匹配)作为 fallback4. 投递修复(Phase 5)
delivery_target_missing),因为 resume 的ChatTurnRequest没有client_turn_id,_immutable_delivery_target查不到对应的ChannelInboundEvent_immutable_delivery_target在client_turn_id为空或查不到时,fallback 到chat_session.channel_target_json5. 人工回复原文注入
_inject_handoff_context:移除session.status != "handoff"的早期 return;当 handoff 已 answered 且resume_finished_at标记存在时(即 resume 后第一个 turn),对所有 task 注入handoff_info(含human_reply),注入后立即清除标记,确保只注入一次response_generator_prompt.md新增第 26 条规则:要求 LLM 完整转述human_reply原文,不得省略或只回复寒暄测试
test_feishu_handoff.py:32 个回归测试,覆盖 Contact 概念、assignee 解析、open_id 查找、通知投递、/回复反馈指令解析、handoff 回复匹配、投递目标 fallbacktest_channel_intake.py、test_channel_team_binding.py(event_sinkkwarg)test_graph_rules.py(sub_sop_id字段)test_llm_client.py失败为预存问题,与本次改动无关)已验证的 UI 路由与角色