Skip to content

fix: 父群图片 lazy loading + history-fork 实现合并 - #233

Merged
lishuceo merged 10 commits into
mainfrom
fix/mention-gate-and-queue-context
May 7, 2026
Merged

fix: 父群图片 lazy loading + history-fork 实现合并#233
lishuceo merged 10 commits into
mainfrom
fix/mention-gate-and-queue-context

Conversation

@lishuceo

@lishuceo lishuceo commented May 7, 2026

Copy link
Copy Markdown
Owner

变更概述

  • fix: 父群聊图片改为 lazy loading(与父群文件对齐),避免话题外简历干扰土豆儿的简历分析
  • test: 新增 lazy-history-images.test.ts 8 个回归用例覆盖 parentMsgCount 边界、下载失败、多图等场景
  • refactor: 合并 buildChatHistoryContextbuildDirectTaskHistory 95% 重复实现,抽出统一 buildHistoryContext,净减少 44 行

背景

土豆儿在话题中分析简历时,会把群聊历史里其他人发的简历图片一起下载分析,导致候选人信息混淆。原因是 downloadHistoryImages 没有 parentMsgCount 保护,而 downloadHistoryFiles 已经有了。

双层防御

  • 代码层:父群图片转为元数据提示(包含 message_id / image_key),由 LLM 按需调用 feishu_download_message_image 加载
  • Prompt 层:config/knowledge/recruitment.md 追加"简历分析作用域规则",明确默认只分析话题内简历(不在本 PR,配置文件 deployment-specific)

测试计划

  • 5 个相关 test file / 63 个测试全部通过
  • typecheck + build 通过
  • 已在部署目录重启 pm2 验证
  • 待真实场景回归:群聊里多份简历 + 话题中追问单份的场景

🤖 Generated with Claude Code

lishuceo and others added 10 commits May 6, 2026 20:43
将 @mention 过滤从"没被拦住就放行"改为"必须有明确 passReason 才放行"。
原来 shouldRespond 返回 false 时静默 return 不留日志,导致无法诊断
群聊话题内 bot 未被 @mention 却主动响应的问题。

- 新增 resolveMentionGate() 统一所有 @mention 决策逻辑
- 新增 getRespondReason() 替代 shouldRespond 返回放行原因
- 每条消息的过滤决策都留 info 日志 (passReason 或 blocked)
- 新增 29 个 mention-gate 测试 + 12 个 getRespondReason 测试

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
根因:task queue 的 .finally() 回调继承前一个任务的 AsyncLocalStorage
上下文。当 pm bot 的消息在 dev bot 任务完成后出队执行时,feishuClient
会用 dev 的凭证发送响应,导致"@土豆儿但张全栈回复"。

修复:在 QueueTask 中保存 accountId,processQueue 执行时用
runWithAccountId 恢复正确的上下文。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- queue.test.ts: 新增 3 个 accountId 存储/传递测试
- queue-context.test.ts: 新增 6 个 AsyncLocalStorage 上下文测试
  - 复现 bug:.finally() 继承外层上下文导致 pm 任务用 dev 凭证
  - 验证 fix:als.run() 包裹后每个任务获得正确上下文

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
审批/管道/记忆卡片按钮的回调 handleCardAction 此前未包裹
runWithAccountId,导致 feishuClient 走 defaultClient。
与 message handler 和 bot-added handler 对齐。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- 话题内只有创建者和 bot 两人时直接放行,不走 Qwen
- 多人话题时,拉 fetchRecentMessages 构建对话上下文喂给 Qwen,
  让 Qwen 看清发言者身份判断消息在跟谁说话(之前只看一条文本)
- 新增 formatThreadContext 将话题消息格式化为 [人名]: 内容
- 结果卡片底部加上 hostname:pid 标识实例来源
- 更新 mention-gate 测试覆盖双人/多人场景

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
多 bot 场景下,pm-bot 的回复不再被误标为当前 bot 名字。
用 accountManager.getBotOpenId 区分 self vs 其他 bot。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
从 accountManager 和 chatBotRegistry 查找 bot 名字,
Qwen 看到的对话记录从 [其他bot] 变为 [土豆儿(bot)]。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
话题中分析简历时,buildChatHistoryContext / buildDirectTaskHistory
会从父群聊补充消息以填满 historyMaxCount,导致 downloadHistoryImages
把父群里其他人发的简历图片一并 base64 嵌入 prompt。

修复:downloadHistoryImages 接受 parentMsgCount,与 downloadHistoryFiles
行为对齐:父群图片只注入元数据提示(feishu_download_message_image
工具调用 hint),需要时由 LLM 主动加载。

之前文件已有 lazy loading(lines 1500-1507),但图片没有同等防护。
8 个用例覆盖:
- parentMsgCount=0 时全部下载(向后兼容)
- 父群图片仅生成 lazyHints 元数据,不下载
- 元数据格式包含 message_id / image_key / 工具名
- 父群-only / 话题-only / 无图片场景
- 话题图片下载失败时父群元数据仍正常输出
- 同一消息多张图片按来源分别处理

同时新增 _testDownloadHistoryImages export,与 _testDownloadHistoryFiles 风格对齐。
两个函数 95% 完全相同(fork 语义 + 增量去重 + 父群懒加载附件),
唯一差异是 direct 路径多打印一份诊断日志、错误日志的 label 不同。

抽出统一的 buildHistoryContext,通过 BuildHistoryOptions 控制
verboseLogging / errorLabel;buildChatHistoryContext 与
buildDirectTaskHistory 退化为薄包装,保留对外签名不变。

净减少 44 行,消除后续修这条 history-fork 路径时双地维护的负担。
@github-actions

github-actions Bot commented May 7, 2026

Copy link
Copy Markdown
Contributor

Claude finished @lishuceo's task in 3m 58s —— View job


✅ Approved — No critical issues found

Reviewed the full diff against origin/main (10 commits, 11 files, +1218/-231).

Verdict

  • 0 previous claude[bot] comments to reconcile.
  • 0 new inline issues at the ≥75 confidence threshold.
  • Three logical changes are well-scoped, well-tested, and the refactor is faithful.

What was checked

1. downloadHistoryImages lazy-loading (src/feishu/event-handler.ts:1367-1429)

  • parentMsgCount boundary indexing is correct: i < parentMsgCount flags messages 0..N-1 as parent.
  • Fallback path (downloadable.length === 0return { images: [], lazyHints }) returns the hints; they get merged into fileTexts upstream so they reach the prompt.
  • Lazy-hint string includes the exact MCP tool name + both required args — LLM has everything to call feishu_download_message_image.
  • Test coverage in lazy-history-images.test.ts covers parent-only, thread-only, mixed, multi-imageRefs per message, download failure, and metadata-format scenarios.

2. buildHistoryContext merge (event-handler.ts:1561-1683)

  • Verified the merged function preserves both behaviors:
    • parentMsgCount recompute after afterMsgId slice: Math.max(0, parentMsgCount - (idx + 1)) matches the old direct-task version exactly.
    • verboseLogging gate keeps the pipeline diagnostic logs only on the direct path (was always-on for buildDirectTaskHistory, now flag-gated — same effective behavior).
    • errorLabel parameter preserves the differentiated error log message.
  • The two public entry points (buildChatHistoryContext, buildDirectTaskHistory) are now thin wrappers — call sites unchanged.

3. resolveMentionGate extraction (event-handler.ts:464-543)

  • Logic equivalence with the old inline gate verified branch-by-branch:
    • p2p → 'p2p' (was: pass-through)
    • multi-bot + thread bypass + relevant → 'thread_bypass' (was: threadBypass = true)
    • multi-bot + no bypass → getRespondReason (was: shouldRespond)
    • single-bot + group + no @ + thread session creator → semantic check (preserved)
  • The new humanSenders.size <= 1 "exclusive bypass" is genuinely new behavior (skips Qwen call when only creator+bot in thread) — covered by mention-gate.test.ts.
  • mention-gate.test.ts is thorough (438 lines, covering p2p, single-bot, multi-bot, thread bypass, edge cases, and the exact bug-reproduction scenario from the PR).

4. Queue accountId context isolation (queue.ts, processQueue in event-handler.ts:629-660)

  • The fix is correct: wrapping execute() in runWithAccountId(taskAccountId, ...) overrides the inherited AsyncLocalStorage context that .finally() would otherwise propagate from the previous task.
  • queue-context.test.ts includes a clean reproduction of the bug AND the fix using a synthetic AsyncLocalStorage, which is excellent regression coverage.

5. getRespondReason refactor (router.ts)

  • Backward-compat wrapper shouldRespond correctly returns reason !== undefined.
  • Discriminated reason return is good ergonomics for the gate's logging.

6. checkThreadRelevance context-aware variant (thread-relevance.ts)

  • Two-prompt design (with/without context) is sound. The 2s → 3s timeout bump is reasonable given the larger payload.
  • Minor downstream observation (NOT blocking): when resolveUserNames fails for multiple senders in formatThreadContext, they all collapse to literal "用户", making the conversation log ambiguous. However Qwen's prompt defaults to false on uncertainty, so this is fail-safe and not a regression vs. the previous no-context call.

7. message-builder.ts hostname tag

  • Adds ${hostname()}:${process.pid} to every result card. Useful for debugging multi-instance PM2 deployments. os.hostname() is sync and effectively constant per process — no perf concern.

Tests

  • 3 new test files (lazy-history-images, mention-gate, queue-context) plus extensions to agent-router and queue — comprehensive and aligned with the project testing policy in CLAUDE.md.
    · Branch: fix/mention-gate-and-queue-context

@lishuceo
lishuceo merged commit ff2b6e3 into main May 7, 2026
4 checks passed
@lishuceo
lishuceo deleted the fix/mention-gate-and-queue-context branch May 7, 2026 08:12
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