编程

dingtalk-auto-reply

试用

监控钉钉未读会话,单聊用 AI 以本人身份自动回复(普通员工口吻、平级回同事),群聊/指定名单只发微信提醒(不代发,防社死)。覆盖 dws 未读接口、CodeBuddy Agent SDK 生成回复的 SERVER__PORT 端口冲突坑、自报家门坑、回复自己坑。当用户要求「钉钉自动回复/钉钉代回/监听钉钉未读+AI回复」时使用。

它能做什么

监控钉钉未读会话,单聊用 AI 以本人身份自动回复(普通员工口吻、平级回同事),群聊/指定名单只发微信提醒(不代发,防社死)。覆盖 dws 未读接口、CodeBuddy Agent SDK 生成回复的 SERVER__PORT 端口冲突坑、自报家门坑、回复自己坑。当用户要求「钉钉自动回复/钉钉代回/监听钉钉未读+AI回复」时使用。

技能文档

钉钉自动回复(未读监控 → AI 代复 → 微信通知)

监控钉钉未读会话,单聊用 AI 以本人身份自动回复(普通员工口吻、平级回同事),群聊/指定名单只发微信提醒(不代发,防社死)。

内部 skill,未公开仓库;要迁移直接整目录拷贝(见下方「迁移 / 重装」段)。

技能包含的文件(自包含,可直接移植)

dingtalk-auto-reply/
├── SKILL.md                      # 本说明
├── README.md                     # 精简版介绍(特性 / 架构 / 安装 / 配置,随包分发)
├── .env.example                  # 配置样例(cp .env.example .env 后填写真实身份/Key)
├── .gitignore                   # 隐私黑名单(.env / _media_cache / .vbs 等不随包分发)
├── dingtalk_unread_monitor.py    # 入口(仅 docstring + main() + 统一 re-export;旧 import 兼容)
├── runtime.py                    # 基础/配置层:.env 加载、SDK 可用探测、路径探测、全部共享常量、日志/锁/缓存/审计/鉴权
├── dingtalk_api.py               # 钉钉交互层:dws 调用、未读/消息拉取、单聊·群@判定、图片下载、发送
├── vision.py                     # 多模态层:图片识别(统一走 CodeBuddy Agent SDK,与文本同后端)
├── reply.py                      # 回复生成层:persona / 查表 grounding / SDK 生成 / 微信推送
├── gen_launcher.py              # 启动器生成器(本机自动生成 Startup .vbs,.vbs 不随包分发)
├── dingtalk-helper-backup.md    # 【方案B·人设干净部署模板】dingtalk-helper.md 的占位版(无私人数据),随 skill 移植;新机器未注册 agent 时自动兜底注入
├── _validate.py                  # 自测脚本(7 模式:集成 / --inject / --test-guard / --test-construct / --test-statemachine / --test-mcp / --env,不真发回复;--env 做环境预检)
├── _setup_env.py                 # 安装脚本:探测并写入 SDK 运行环境(CODEBUDDY_SDK_PYTHON 等)到 .env
├── recover_missed.py            # 补发漏掉的消息(监控曾宕机/DRY_RUN 时,手动拉未读并代复)
└── stop_monitor.ps1             # 精确结束本脚本 python(仅停进程,Windows 用)

运行时自动生成(不随包分发,已在 .gitignore 排除):.env_media_cache/__pycache__/dingtalk_auto_reply_launcher.vbs

代码已按职责拆分为 4 模块 + 入口(2026-07-17,原单文件 1642 行)。改哪块改哪块:

  • 调 dws / 未读判定 / 发送 → dingtalk_api.py
  • 图片识别 → vision.py
  • 人设 / 查表 grounding / SDK 生成 / 微信推送 → reply.py
  • 路径 / 常量 / 日志 / 锁 / 鉴权 → runtime.py
  • 调度主循环 → dingtalk_unread_monitor.py 入口 dingtalk_unread_monitor.py 把各模块名字 re-export,故 _validate.py / recover_missed.pyimport dingtalk_unread_monitor as M 无需改动。 注意:可变配置 DRY_RUN / SELF_OPEN_IDruntime.DRY_RUN / runtime.SELF_OPEN_ID 访问(模块属性而非 import 副本),recover_missed.py 改的是 runtime.DRY_RUN

移植:把整个目录复制到目标机的 ~/.workbuddy/skills/ 即可,无需改代码。

⚙️ 移植后必做一步:把 dws 加入系统 PATH(根因与自动追加机制见下方「dws 依赖」段)。dws 不在 PATH → get_unread 恒空;换机部署先跑一次 gen_launcher.py 自动幂等追加(无需重登),新终端敲 dws 有输出即就位。

⚠️ 启动器 .vbs 不随包分发.vbs 是机器专属胶水文件(路径用 %USERPROFILE% 环境变量展开动态推导、无硬编码用户名,并备 SpecialFolders("Profile") / %HOMEDRIVE%%HOMEPATH% 兜底),被列入 .gitignore不会随仓库/打包上传。换机或重新克隆后,启动器由 gen_launcher.py 在本机生成(见下方「Windows · Startup 启动器」),监控脚本首次运行也会自动自检并生成,无需手动复制 .vbs

环境要求与安装(一键预检)

本技能运行依赖以下外部组件,一个都不能少;缺任何一项都会导致「监控活着却读不到未读 / 生成失败」。换机部署或异常时,先跑下面的「一键预检」,缺啥它会直接给出修复命令,不用盲猜。

依赖作用安装 / 修复
Python(default venv)跑脚本的解释器;codebuddy-agent-sdk 必须装在这个 venv 里用 venv python 跑本脚本:%USERPROFILE%\.workbuddy\binaries\python\envs\default\Scripts\python.exe(macOS/Linux 用 .../bin/python3)。裸 managed python / anaconda 不含 SDK,会 _SDK_AVAILABLE=False
codebuddy-agent-sdk生成回复 + 图片识别的唯一后端(deepseek-v4-flash 经此 SDK)venv_python -m pip install codebuddy-agent-sdk(或 pip install -r requirements.txt
dws CLI拉未读 / 发消息 / 通讯录gen_launcher.py 自动加入 PATH;dws patch chmod 授权 chat.message:list / chat.message:send / contact:search
codebuddy CLISDK 底层起 prewarm server 用随 WorkBuddy 安装;CODEBUDDY_CMD 路径见 print_paths()
nodedws NODE-direct 路由需要装 Node 并确保在可用路径
gbrain MCP(可选)查表 / 知识库;不配则纯人设回复GBRAIN_MCP_URL / GBRAIN_MCP_TOKEN,缺省自动读 ~/.workbuddy/mcp.jsongbrain 条目
代码库检索(可选)让回复 agent 检索本地源码回答「源码/接口/实现细节」问题.envCODE_SEARCH_ROOTS(分号分隔路径列表,不填=不启用);CODE_SEARCH_TOOL 指向 search.py(默认自动探测 ~/.workbuddy/tools/code-search/search.py,未装则回退 Grep/Glob/Read)
视觉识别(可选)图片识别;与文本同后端、零额外 key默认跟随文本模型 CODEBUDDY_MODEL;可用 VISION_MODEL 单独指定 CodeBuddy 侧视觉模型。SDK 缺失时视觉与文本一并降级为「仅通知不识别」
SDK 运行环境声明(.env让 skill / agent 识别「用哪个 python 跑」安装时跑 _setup_env.py 自动把 CODEBUDDY_SDK_PYTHON / CODEBUDDY_MODEL / CODEBUDDY_INTERNET_ENVIRONMENT / CODEBUDDY_CMD 写入 .env

安装步骤(agent 安装时写入 SDK 环境)

  1. 装依赖(如尚未装):用目标 venv 的 python 装 SDK
    %USERPROFILE%\.workbuddy\binaries\python\envs\default\Scripts\python.exe -m pip install -r requirements.txt
    
  2. 探测并写入 SDK 环境(关键):跑安装脚本,让 agent 把 SDK 运行环境写进 .env
    %USERPROFILE%\.workbuddy\binaries\python\envs\default\Scripts\python.exe _setup_env.py
    # 仅预览不写: _setup_env.py --check
    # 强制覆盖 SDK 项: _setup_env.py --force
    
    它会遍历候选 python 找到装了 codebuddy-agent-sdk 的那个,连同主模型 / 网络环境 / CLI 路径写入 .envCODEBUDDY_SDK_PYTHON 等项。之后 monitor 被任意 python 拉起都能按该声明自拉回到正确环境(见 runtime sdk_reexec_target)。
  3. 预检:确认环境完整
    %USERPROFILE%\.workbuddy\binaries\python\envs\default\Scripts\python.exe _validate.py --env
    
  4. 填私密身份cp .env.example .env(若还没建)→ 在 .envBOSS_UID / SELF_OPENDINGTALK_ID / SELF_SENDERS / MENTION_NAMES 等(详见下方「隐私与身份配置」)。

⚠️ CODEBUDDY_SDK_PYTHON 不是隐私,可随技能分发;但 .env 整体仍被 .gitignore 忽略(含 BOSS_UID 等隐私)。换机时重新跑 _setup_env.py 即可重新写入 SDK 环境。

一键预检(推荐每次部署/异常先跑)

# 必须用装了 SDK 的 venv python 跑(关键)
%USERPROFILE%\.workbuddy\binaries\python\envs\default\Scripts\python.exe _validate.py --env

--env 逐项打印 [OK] / [MISSING] / [WARN]

  • 阻断级缺失(SDK / dws)→ 给精确修复命令、退出码非 0;必须先修。
  • 非阻断项(gbrain / 人设 agent)→ 只告警,功能降级仍可运行(纯人设回复、用 skill 内 dingtalk-helper-backup.md 兜底)。

每次跑其它模式(集成 / --inject / --test-*)也会先自动跑一遍预检,环境异常第一时间暴露。

⚠️ 视觉与文本共用同一 SDK 后端_SDK_AVAILABLE=True 时两者都可用;SDK 缺失时文本也生成不了,不是视觉单独降级——所谓「仅通知不识别」是 SDK 缺失时的整体回退,与以前行为一致。

完整 pipeline(顺序不可乱)

  1. dws chat message list-unread-conversations --format json → 只返回有未读的会话(天然过滤)。 字段:openConversationIdtitlesingleChat(true=单聊)、unreadPointlastMsgCreateAt
  2. 单聊 → 用 openConversationId 拉最新一条消息: dws chat message list --group --time "" --direction older --limit 5 --format json ⚠️ 必须带 --direction older:含义"从给定时间往更早拉",配 --time now 即返回最新消息(newest-first)。 不带 direction 时默认 older 且起始时间若早于消息 → 消息被过滤成空。 单聊也能用 --group (无需 --user/--open-dingtalk-id)。
  3. 回复生成(CodeBuddy Agent SDK 主后端 + 查表事实 grounding + 会话记忆):先按消息意图 detect_table_intent 判断查哪张表 (feedback=问题反馈表 / project=ROS项目表),用 fetch_table_context 拉「本人(userId=)名下未解决项」 (带缓存)注入 system_prompt;再用 CodeBuddy Agent SDK_gen_reply_sdk_asyncsystem_prompt=人设+查表数据,query=干净单行消息) 生成回复,session_id=dt_(首轮 session_id、后续 resume 续上下文)实现「一人一会话、记忆连续」。 SDK 不可用/失败 → 返回空(不代发,推微信转人工),无 CLI 兜底。详见下方「生成回复的关键坑」。
  4. dws chat message reply --conversation-id --ref-msg-id --ref-sender --text <回复> --yes 带引用回复。
  5. 微信推送:默认用 ~/.workbuddy/skills/weixinclaw-proactive-push/send.js(仅文本,不转发媒体)。 若该 skill 未安装,脚本自动降级为仅日志,不报错、不阻断主流程。

路径解析(可移植核心,勿硬编码用户名)

脚本不再写死任何用户名。外部二进制按以下优先级解析,全部支持环境变量覆盖

用途环境变量覆盖自动探测位置
dws CLI(直接路径·主路由)DWS_EXE~/.workbuddy/binaries/node/cli-connector-packages/node_modules/dingtalk-workspace-cli/vendor/dws.exe
dws CLI(cmd 兜底)DWS_CMD~/.workbuddy/binaries/node/cli-connector-packages/dws.cmd
codebuddy CLICODEBUDDY_CMD~/.workbuddy/binaries/node/versions/<最新版本>/codebuddy.cmd
nodeNODE~/.workbuddy/binaries/node/versions/<最新版本>/node.exe
微信推送脚本WEIXIN_SEND_JS~/.workbuddy/skills/weixinclaw-proactive-push/send.js

人设真源 = codebuddy 全局注册 agent dingtalk-helper~/.codebuddy/agents/dingtalk-helper.md),由 reply.pyextra_args={"agent": "dingtalk-helper"} 透传 CLI --agent 按名加载; skill 内 dingtalk-helper-backup.md 是其干净模板(占位,无私人数据),仅在新机器尚未注册 agent 时兜底注入,随技能一起走、可移植。

可调环境变量:DRY_RUN=1(只验证不真发)、POLL_INTERVAL=秒(轮询间隔)、TEST_MODE=1(生成的回复只发给老板自己,绝不发给原发送人,自测用)、ONCE=1(单次轮询后退出,自测用)、TABLE_GROUNDING=0(关闭查表 grounding,纯人设回复)。

隐私与身份配置(重要)

  • 源码不含任何真实身份:真实姓名、昵称、英文名、城市、openDingTalkId一律不硬编码.py / 人设 / .env.example 里,全部通过私密 .env 注入(SELF_SENDERS/MENTION_NAMES/BOSS_DISPLAY_NAME/SELF_OPENDINGTALK_ID)。源码默认值只保留中性词"老板"。
  • .env 不分发:已被 .gitignore 忽略,切勿提交或随技能打包外发。换机时 cp .env.example .env 后自行填写。
  • 代码内禁止出现任何真实第三方姓名(含注释/测试/demo):老板本人身份走 .env 变量(BOSS_NAME/BOSS_COMPANY/BOSS_TITLE/BOSS_UID/SELF_OPENDINGTALK_ID/SELF_SENDERS/MENTION_NAMES),同事/名单用中性占位(如「同事甲」)或 .env 变量,禁止把真实姓名写进 .py 注释、_validate.py 测试样例、SKILL.md 正文等任何位置。改代码后自查:凡 温*/蔡*/真实拼音全名等一律清零。
  • 人设红线:真源 ~/.codebuddy/agents/dingtalk-helper.md(skill 内 dingtalk-helper-backup.md 为其干净模板)明确禁止在回复里输出任何系统标识/用户名/英文 ID(如把 <系统用户名> 当称呼),也禁止主动透露老板真实姓名/城市/公司等个人信息。

人设真源(方案 B:codebuddy 注册 agent 为唯一真源)

真源 = codebuddy 全局注册 agent dingtalk-helper~/.codebuddy/agents/dingtalk-helper.md), 由 reply.pyCodeBuddyAgentOptions(extra_args={"agent": "dingtalk-helper"}) 透传 CLI --agent 按名加载该全局注册灵魂(已实测 18.6s 成功), 含双模式人设(探索 + 直接回复同事)+ 身份红线 + 两表结构;自动代复只走其中「直接回复同事」那一刀。 agent 已注册时 _resolve_persona() 返回 None(不重复注入,避免双灵魂);未注册(如新机器)时退回读 dingtalk-helper-backup.md 兜底注入。

⚠️ 冷启动注意--agent 首次调用 codebuddy 会做一次一次性注册/编译(解析 ~/.codebuddy/agents/dingtalk-helper.md 并缓存), 那次耗时可达数分钟(实测初调 7~26 分钟无输出,易被误判为卡死/机制损坏)。注册沉淀后,后续调用即 18.6s 秒回,机制本身正常可用。 切勿因单次冷启动延迟就判定 --agent 损坏、擅自改成其它注入方式——复测须在冷启动后二次调用验证。

⚠️ 冷启动与首条消息(已修复,仍需了解)gen_reply 对「进程内首次成功生成前的调用」自动使用 AGENT_FIRST_CALL_TIMEOUT(默认 1800s),覆盖 codebuddy 一次性注册延迟(实测 7~26 分钟); 首次成功生成后自动切回常规 AGENT_CALL_TIMEOUT(默认 240s)。因此注册后首条真实消息不再被 240s 静默跳过,会在放宽窗口内等到注册完成并正常代复。 可选加速(非必须):部署/重装后若想立即热好,可先跑一次 _validate.py --inject --sender 测试同事 --message 你好 预热注册(耗时几分钟但不会真发给人), 看到「returned in 18.6s」「REPLY >>>」即注册完成;不预热也行,监控首条会在放宽超时内自动等到注册完成。两个超时均可经环境变量 AGENT_CALL_TIMEOUT / AGENT_FIRST_CALL_TIMEOUT 调整。

  • _MODE_LOCK(直接回复同事模式锁)始终注入:无论 agent 是否注册,gen_reply 都把这段强制锁拼进 system_prompt, 把双模式灵魂锁死在「直接回复同事」模式(你是本公司普通员工、对方是平级同事、不是 boss 本人也不是代笔助理;不许反问/自调 dws/摆领导架子),根治「模型把同事消息当指令、输出『我帮你拟』或替你拍板」的抖动。
  • 工具权限统一(黑名单模式,可配置):危险工具清单集中在 runtime.DISALLOWED_TOOLS(默认 ["Write","Edit"],即仅禁用写入;Bash 已开放,允许跑 dws 等只读/查询命令、访问本地系统文件),不再硬编码在 reply.py / vision.py;两处 SDK 调用统一引用该变量,改一处全局生效。可用环境变量 DINGTALK_AGENT_DISALLOWED_TOOLS(逗号分隔)覆盖,如 "Write,Edit"。agent 文件已移除 tools: 白名单 → 继承 codebuddy 默认工具集(含 WebSearch/WebFetch 联网搜索、Read/Grep/Glob 只读检索、Bash 只读查询),代复时如需查实时信息可直接联网。与 _MODE_LOCK 双重锁。reply.py 与 vision.py 两处 SDK 调用同源锁定;vision.py 原本漏设 disallowed_tools(等于 Bash 也开)已补上,勿回退。
  • skill 内 dingtalk-helper-backup.md = 干净部署模板:是 dingtalk-helper.md 的占位版(无私人数据),随 skill 走; 移植到新机器、且尚未把 agent 注册进 ~/.codebuddy/agents/ 时,_resolve_persona() 自动兜底注入它,换机即开箱即用。 正式注册:把 dingtalk-helper-backup.md 复制为 ~/.codebuddy/agents/dingtalk-helper.md 即可(_AGENT_REGISTERED 检测命中后自动改走真源)。
  • 设计红线:人设只此一份真源(dingtalk-helper.md),skill 内只放备份/兜底;改人设只改 ~/.codebuddy/agents/dingtalk-helper.md 一处。 旧 secretary_system_prompt.txt / reply_persona_grounded.md 已删除(内容被 dingtalk-helper.md 完全覆盖; 且前者教模型"自行调 dws"与 _MODE_LOCK 的"严禁自己调 dws"直接冲突,留作兜底反而有害)。
  • 兜底顺序(agent 未注册时)dingtalk-helper-backup.md → 硬编码中性话术(不会因缺文件而崩),全程不会双灵魂注入。
  • 改动生效需重启监控(常驻进程不会因 .py 改动热重载)。

🧠 知识库调用链路(gbrain MCP,2026-07-29 优化)

旧方案:把产品资料/项目文件/记忆直接铺进 agent 工作空间,靠 agent 用 Grep/Glob/Read 自行读文件取知识——慢、依赖模型主动检索、易漏。

新方案(更快更准,且带保底):gbrain 知识库 MCP 服务为知识主入口,工作空间本地文档作保底回退

  • gbrain 以 HTTP 单实例常驻(gbrain serve --http,默认 localhost:3131/mcp),持 PGLite 写锁;WorkBuddy 连接器与本 skill 的回复 agent 都作为 HTTP 客户端连它,不各起进程、不抢锁。
  • 回复 agent 经 SDK 的 mcp_servers 挂上 gbrain(reply.pyCodeBuddyAgentOptions(mcp_servers={"gbrain": {"type":"http","url":...,"headers":...}})),按需调用 mcp__gbrain__search / mcp__gbrain__query 做语义检索。
  • ⚠️ 事实性问题必须先查 gbrain(不是按需,是必须 · 2026-07-29 修复):早版 prompt 写"按需但默认查",被 _MODE_LOCK 的"直接写出回复正文/只输出回复本身/标签外不写任何字"等强约束压制——agent 把工具调用当成"违反约束",跳过 gbrain 直接凭记忆答"印象里 N200 Pro 出厂默认波特率是 115200"。现 _MODE_LOCK 顶部新增「⚠️ 先查再答 · 任务前置步骤」段,明确"调工具是隐式步骤不展示给同事、不违反最终输出格式"+"禁止凭印象答印象里/我记得/大概是";build_knowledge_instruction() 同步改为"事实性问题必须先查 · 不是按需是必须";_looks_like_reply() 加安全网拦"含糊词+技术参数"回复转人工。纯闲聊 / 问候 / 安排 / 表态等无需事实依据时,仍直接基于对话上下文作答,不为查而查。
  • ⚠️ prompt 指令与工具挂载状态严格一致(防悬空指令):拼进 _MODE_LOCK 的知识库指令由 build_knowledge_instruction() 动态生成,与 mcp_servers 挂载判断都读 runtime.GBRAIN_GROUNDING / runtime.GBRAIN_MCP_URL 同一实时来源——gbrain 已挂载则写「必须先调 gbrain、失败回退本地」;gbrain 未挂载(=0 或无端点)则明确写「gbrain 未启用、不要调它,直接走本地文件」。绝不会出现「让 agent 去调一个不存在的工具」的悬空指令。
  • 控制开关(均在 runtime.py,风格对齐 TABLE_GROUNDING):
    • GBRAIN_GROUNDING(默认开,=0 关):关闭则 prompt 切本地文件模式 + 不挂 gbrain 工具,退回纯人设代复(仍保留本地 产品资料/ 项目文件/ 保底)。
    • GBRAIN_MCP_URL / GBRAIN_MCP_TOKEN:缺省时自动从 ~/.workbuddy/mcp.jsongbrain 条目读取(url + Bearer token),单一真源、无需重复填 token;也可用环境变量覆盖。
  • _MODE_LOCKdingtalk-helper.md 均写明「gbrain 优先(事实性问题必须)、本地文件保底」:事实性且需要的内容才调 mcp__gbrain__search/query若 gbrain 未接入 / 调用失败 / 超时 / 检索结果为空或明显不对题,回退到工作空间本地 产品资料/ 项目文件/ 文档用 Grep/Glob/Read 核实。两路都查不到才如实说「这个我得查下资料确认,稍回你」。本地文件是保底、并非禁用(之前"禁止读本地"的过激限制已撤销)。
  • 知识入库:优先把产品手册/项目文档/记忆整理进 gbrain(见 gbrain 自身文档)做主检索源;工作空间 产品资料/ 项目文件/ 文档树保留作保底回退,仍随工作空间维护、不要删。

🧩 本地代码库检索(CODE_SEARCH_ROOTS / CODE_SEARCH_TOOL,2026-08-04 新增,可选能力)

解决什么:同事在钉钉问「源码 / 接口 / 实现细节」类问题(话题名、节点名、参数名、launch/配置文件、具体 .py/.cpp 实现逻辑),gbrain 文档往往没收录或过时 → agent 必须直接检索本地源码库拿真实实现作答(同属"先查再答"必查项,禁止凭印象)。

通用设计(不绑定任何特定项目/型号)

  • 配置全走环境变量(runtime.py 常量 + .env 注入,源码零硬编码):
    • CODE_SEARCH_ROOTS:分号(;)分隔的本地源码库绝对路径列表;不填 = 不启用代码检索(agent 走 gbrain + 本地文档),通用 skill 默认留空,新用户填自己的源码目录即启用。
    • CODE_SEARCH_TOOL:代码检索工具 search.py 路径(rg 全文 + ctags 符号定位的封装脚本),默认自动探测 ~/.workbuddy/tools/code-search/search.py,装到别处可覆盖。
  • 工具优先、悬空防护build_code_search_instruction() 动态生成检索指令(拼进回复 agent 的 system_prompt)——search.py 存在 → 教 agent 用 Bash 调它(search.py <关键词> --pkg <包名> 全文 / --symbol <符号名> 定位定义,输出「路径:行号:内容」,命中后 Read 精读);search.py 不存在 → 回退 Grep/Glob/Read 直接搜;两者都没有 → 整段不注入。与 gbrain 指令同读 runtime 实时值,绝不悬空。
  • 检索优先级:gbrain(文档/语义)→ 本地代码库(真实实现)→ 工作空间文档(保底)。

search.py 工具安装(可选增强,由部署者自行安装):装 ripgrep + Universal Ctags + 一个薄封装脚本 search.py(自带 REPOS 仓库映射 + --index 建 ctags 符号索引 + --list-repos),支持 python search.py 关键词 [--pkg 包名] [--context N] [--max N] [--regex] / python search.py --symbol 符号名。装好后 skill 自动探测并启用;不装也能跑(回退 Grep/Glob),只是大仓检索慢些。

运行时初始化:工作空间 / cwd 记忆 / dws 权限 / 项目说明(新用户须知)

首次运行(或新机部署后首次启动监控)会自行重建 cwd 记忆上下文;但工作空间目录需你手动 mkdir(默认 ~/WorkBuddy/dingtalk_auto_reply,可配 DINGTALK_WORKSPACE),记忆不随包分发

  • ① 工作空间 DINGTALK_WORKSPACE(agent 的 cwd:默认 ~/WorkBuddy/dingtalk_auto_reply;换路径用 DINGTALK_WORKSPACE 环境变量覆盖,改后需你手动 mkdir 建好该目录(agent 不会自动建)。它只是 agent 的 cwd,里面只放 .workbuddy/memory/不随 skill 分发、不迁移
  • ② cwd 记忆自动生成(含隐私,禁止迁移):agent 以该工作空间为 cwd 启动,首次运行自己拉 dws 沉淀.workbuddy/memory/(长期 MEMORY.md / KNOWLEDGE.md + 每日 YYYY-MM-DD.md)。记忆内容是 agent 跟老板协作中自己学出来的私有上下文(含老板隐私)绝不可随包外发或迁到别的机器;全新机留空,agent 会自行重建。
  • ③ dws 权限:需 dws patch chmod 授权 chat.message:list / chat.message:send / contact:search;dws 须已在 PATH(本 skill 的 gen_launcher.py 自动追加)。未授权 / 不在 PATH → 监控"活着"却读不到未读。PATH / 会话 / 授权三条的排错详见下文「dws 依赖」段。

⚠️ 项目说明 · 新用户不一定用 dws 管项目

dingtalk-helper.md 人设里内置的两表结构 + 「老板钉钉身份」是老板专属配置(ROS 软件项目表 /、问题反馈表 /、老板 userId ``)。这些真值只在本机 .env~/.codebuddy/agents/dingtalk-helper.md,随包分发的 dingtalk-helper-backup.md 已替换为 <...> 占位符。换一个新用户,这些 baseId/tableId/身份大概率不存在——他未必用 dingtalk 多维表管项目,甚至未必接 dws。

新用户跑起来有两种姿势(skill 不假设项目表存在):

  1. 纯人设代复(推荐起步):设 TABLE_GROUNDING=0 关闭查表 grounding,agent 只按【身份红线】以本人口吻回复,不查任何表。最稳,零 dws 项目依赖。
  2. 接自己的项目结构:把 dingtalk-helper.md 里「钉钉工作空间 · 已知结构」段改成自己的 dingtalk Base / 表 / 身份,再注册进 ~/.codebuddy/agents/;agent 的【自主探索原则】会自己 dws aitable list 探明结构后拉数据,不强依赖写死的 baseId。

关键:自动代复的核心契约是「以本人口吻直接回复同事」(身份红线),查表 grounding 是增强项不是必需项。新用户没项目表也能正常代复,只是少了"结合项目进度回"的能力。

📦 迁移 / 重装到其他电脑

skill 目录自包含、整目录拷走即可。需要明确的四类外部构件:

构件位置是否迁移说明
skill 本体~/.workbuddy/skills/dingtalk-auto-reply/✅ 整目录拷.py/.md/.env.example 全在里面
人设灵魂 dingtalk-helper.mdskill 内 dingtalk-helper-backup.md模板,占位符 <...>⚠️ 模板≠真身dingtalk-helper-backup.md干净模板(userId/baseId/字段全为 <...> 占位,随包分发不含私人数据)。不要盲目 cp 它去覆盖你本机已配置好的 dingtalk-helper.md——那会把真身冲成模板。正确做法:要么直接编辑你本机的 dingtalk-helper.md 填自己的钉钉身份/表结构;要么全新机才 cp dingtalk-helper-backup.md ~/.codebuddy/agents/dingtalk-helper.md再填真实数据。不注册也会自动用模板兜底(纯人设代复)。
工作空间 + cwd 记忆DINGTALK_WORKSPACE/.workbuddy/(agent 运行时自动加载)禁止迁移工作空间由你手动 mkdir(默认路径可配 DINGTALK_WORKSPACE);cwd 记忆由 agent 自己拉 dws 生成/你移植内含老板隐私。全新机留空,agent 首次运行自动建空记忆并自行重建上下文,绝不随包分发。
私密配置 .envskill 内 .env.example.env 已被 .gitignore 排除)⚠️ 不随分发新机 cp .env.example .env 后填 BOSS_UID/SELF_OPENDINGTALK_ID 等。

路径可移植:DINGTALK_WORKSPACEBOSS_UID 均支持环境变量覆盖(runtime.pyos.environ.get + os.path.expanduser("~")),换机用户名不同也不会硬编码失效;不设则用默认 ~/WorkBuddy/dingtalk_auto_reply

一步到位脚本(新机)

# 1) skill 本体
cp -r dingtalk-auto-reply ~/.workbuddy/skills/

# 2) 注册人设灵魂(仅全新机;dingtalk-helper-backup.md 是干净模板,cp 后务必填真实数据)
#    ⚠️ 若本机已有配置好的 dingtalk-helper.md,切勿盲目 cp 覆盖——直接编辑它即可。
mkdir -p ~/.codebuddy/agents
cp ~/.workbuddy/skills/dingtalk-auto-reply/dingtalk-helper-backup.md ~/.codebuddy/agents/dingtalk-helper.md
#    # 然后编辑 ~/.codebuddy/agents/dingtalk-helper.md,把 / 等占位填成你自己的钉钉身份与表结构

# 3) 私密配置(不随包分发,新机自建)
cp ~/.workbuddy/skills/dingtalk-auto-reply/.env.example ~/.workbuddy/skills/dingtalk-auto-reply/.env
#    # 编辑 .env 填 BOSS_UID / SELF_OPENDINGTALK_ID / CODEBUDDY_API_KEY(如需)

# 4) dws 入 PATH + 生成本机启动器(幂等,无需重登)
cd ~/.workbuddy/skills/dingtalk-auto-reply && python gen_launcher.py
#    工作空间需你手动 mkdir(默认路径见 DINGTALK_WORKSPACE);cwd 记忆由 agent 首次运行自行重建

⚠️ 生效需重启常驻监控(旧进程不会热重载 .py / 新注册的 agent)。

生成回复(CodeBuddy Agent SDK 唯一后端 · 融合查表 grounding + 会话记忆)

gen_reply(sender, content, …) 调度逻辑(唯一后端 = CodeBuddy Agent SDK,无 CLI 兜底):

  • _gen_reply_sdk_async唯一):CodeBuddyAgentOptions(extra_args={"agent":"dingtalk-helper"} # 按名加载 codebuddy 注册灵魂 dingtalk-helper.md(人设真源), system_prompt=AppendSystemPrompt(append=<查表数据,人设由 --agent 加载>), model=deepseek-v4-flash, permission_mode=bypassPermissions, codebuddy_code_path=, cwd=<钉钉自动回复工作空间,自动加载该空间记忆>, session_id/resume=dt_);用户消息走 query 干净单行(f"{sender}:{content}"),system_prompt 用追加模式保留工作空间记忆、用户消息天然分离(无命令行多行坑)。agent 未注册时 extra_args={} 且改为把 dingtalk-helper-backup.md 兜底注入 system_prompt。
    • 空闲 SERVER__PORT 注入 env 防 prewarm 端口冲突挂死(0 字节超时根因)。
    • env 注入 CODEBUDDY_INTERNET_ENVIRONMENT=internal(中国版 deepseek-v4-flash 路由);bypassPermissions 放行后 agent 可自行调 dws 补查(事实 grounding 双保险)。
    • 图片消息:image_paths 非空时以 Anthropic image 协议内联进 query(deepseek-v4-flash 多模态「看图代复一次调用」)。
  • 【主回复对象 vs 背景历史 · 2026-07-20 老板纠偏 + 2026-07-30 burst 合并修复】:消息窗口内累积的 messages 列表必须按时间倒序(newest 在前,主循环已按 _msg_ts 排序+兜底 sorted 二次保护)。
    • 【2026-07-30 burst 合并 · 当前主路径】:取最新对方消息 ts T0,窗口 [T0 - REPLY_DELAY_SEC, T0] 内(默认 120s)的对方连发消息物理合并成一条虚拟主消息(拼接格式 [1/N] sender:content1\n[2/N] sender:content2\n...,按时间正序呈现),作为 AI 的【主回复对象】。背景历史 = 窗口外对方消息 + 老板本人历史发言。AI 看到 main_content 直接就是"对方在延时窗口内连发的全部消息",prompt 明确告知"请综合理解后给一条统一回复,覆盖核心诉求,不要漏答、不要逐条复述"。比 prompt 软约束强:物理合并让 AI 没法只挑最新一条答。
    • 【2026-07-20 旧设计 · single-main 模式 · 仍在使用】:当窗口内对方消息仅 1 条时走此模式——msgs[0] = 对方最新一条 = AI 唯一要回的内容;msgs[1:] = 背景历史,仅供了解上下文。强约束"只回主消息、不要复述历史话题"。
    • 两种模式自动切换gen_reply_burst_mode 由"窗口内对方消息 ≥ 2 条"判定,日志区分 mode=burst-merged / mode=single-main
    • 跨窗口防话题死循环(仍生效):跨窗口(时间差 > REPLY_DELAY_SEC)的对方更早消息归入背景历史、不进 burst;prompt 强约束"不要复述或扩展背景历史里已说过的话题、不要提已被处理过的问题反馈表/项目进度等"。这是 2026-07-20 老板纠偏的核心防波堤,burst 合并只在"延时期内对方连发"这个有限范围内松绑,不动摇跨窗口的硬约束。
    • 设计权衡:burst 合并后"对方延时期内只要有一条提到'问题反馈'就会触发查表"是合理的(对方确实在问,不是历史回顾);真正要防的"延时期外更早'问题反馈'被 AI 主动接茬"由"跨窗口进背景 + prompt 不复述历史"双重保险。
    • 历史背景:2026-07-20 老板截图实锤某同事发 5 条消息、最后一条"已经忙到头昏了"AI 还在接茬"问题反馈表我跟进下内容",根因是老版本把所有 msgs 拼成一个 content 让 AI"综合理解" + SDK resume 加载旧 reply 形成话题死循环。先修成"主/背景分段+只回主消息";2026-07-30 老板再纠偏"延时期内连发应综合回复",遂加 burst 合并路径——既保留跨窗口防波堤,又松绑延时期内连发的合并回复。
  • 查表事实 grounding(收紧):用主消息(burst 合并后是合并全文,single-main 模式下是最新一条)判意图 → fetch_table_context(意图, session_id) 拉「本人(userId=)名下未解决项」拼进 system_prompt(带 300s 缓存,同会话不重复拉)。表 baseId/tableId/字段 ID 全部从 .env 经环境变量注入(/// 等),源码零硬编码;不配置则自动退化为纯人设代复。两表在 system_prompt 与回复里明确区分,不混淆。
    • 【关键收紧】主消息二次校验:table_context 注入 system_prompt 之前会再用 detect_table_intent(main_content) 校验一次:主消息(含 burst 合并后的全文)无相关意图 → 丢弃 table_context(避免"背景历史含'问题反馈'、主消息是'好的谢谢'"时被背景查表结果污染)。注意:burst 合并模式下,对方延时期内只要有一条含"问题反馈"就会判为 feedback 触发查表——这是合理的,对方确实在问。
  • 会话记忆(一人一会话连续)session_id = "dt_" + ;同一对话首轮传 session_id,后续传 resume=dt_ 续上下文(记忆连续,已实测跨进程 resume 有效)。session 历史里若已回复过某话题,话题红线会禁止 AI 再次主动提及(详见 _MODE_LOCK 末尾)。
  • SDK 不可用/失败 → 返回空(不代发,推微信转人工),无 CLI 兜底。

关键坑(必读)

  1. SERVER__PORT 端口冲突(最致命):每次生成分配空闲端口注入 env["SERVER__PORT"],否则 query() 0 字节超时。
  2. 运行环境必须固定为托管 venv pythonbinaries/python/envs/default/Scripts/python.exe):本机 codebuddy-agent-sdk 装在 venv 里,用基础托管 python 跑会因 import 失败导致 _SDK_AVAILABLE=False。生产 launcher(gen_launcher.py)已固定 venv python,自测也要用 venv python。
  3. 别"回复自己":同前,_is_self + SELF_OPEN_ID 精确匹配。
  4. ⚠️ cmd.exe 8191 字符命令行限制(2026-08-04 实锤,最隐蔽):SDK 把 AppendSystemPrompt 的内容经 --append-system-prompt 拼进 codebuddy.cmd(批处理) 的命令行;Windows 执行 .cmd 必须经 cmd.exe,整条命令行超 8191 字符即报「命令行太长」→ CLI 子进程秒退 → SDK 报 CLIConnectionError: Connection closed(0.1s)。症状极像环境故障,但 CLI -p 直连一切正常。修复=控制注入 system_prompt 的总长_MODE_LOCK + few-shot + build_knowledge_instruction + build_code_search_instruction + 查表数据 合计须 < ~7900 字符(含基础参数余量,安全值 7000);2026-08-04 曾因 few-shot 扩容触发,精简后 5699 恢复。改动任何注入段后必须跑 python -c 量总长(见 reply.py 注释),超限即回退精简;排查「突然全部 Connection closed」先量长度、别怀疑环境。
  5. ⚠️ few-shot 只能放 append、不能搬进 agent 灵魂(2026-08-04 实测):曾把 few-shot 完整版写进 dingtalk-helper.md 灵魂(灵魂经 --agent 加载不走命令行、不受 8191 限制),结果 agent 行为退化——连续 5 次测试不调 search.py/gbrain,凭记忆「回忆模式」直接答;回滚灵魂 few-shot 段后立即恢复检索(3 次 tool_use + 精确源码细节回答)。结论:few-shot 放 append(system_prompt)是行为最强驱动力,灵魂里加重复内容会干扰(疑似模型对「上下文已有示例」产生惰性/缓存效应)。灵魂保持干净。真源 = reply.py 的 _FEW_SHOT_EXAMPLES 常量(2026-08-04 老板要求内联进代码 prompt,与 _MODE_LOCK 同级,不依赖 .md 读取);dws-reply-examples.md 保留为随包分发文档副本(改动同步常量)。
  6. ⚠️ 毫秒/秒时间戳混用(2026-08-05 实锤,隐蔽排序 bug):dws 的 lastMsgCreateAt毫秒(13 位,如 1584669332376),消息对象 createTime 常是秒级字符串——两者混用排序差 1000 倍。症状:图片消息(PENDING 初始 msgs[0].ts 用 lastMsgCreateAt 毫秒)被误判为"最新",burst 窗口以它为锚,图片之后连发的文字消息(秒级)全落窗外 → 进背景历史 → AI 只回图片不回文字(老板 08-05 反馈);还导致 NOTIFIED 去重失效(job.ts 秒 vs lastMsgCreateAt 毫秒永不相等 → 重复处理/重复推送隐患)。修复dingtalk_api._norm_ts(v)(数字 >1e11 判定毫秒 ÷1000 归一化为秒),_msg_ts 数字分支与 monitor 的 last_ts 统一走它——全链路(PENDING.msgs[].ts / job.ts / NOTIFIED / burst 排序)秒级一致。凡新增时间字段解析,一律经 _norm_ts 归一化
  • 安全网统一(防御纵深,别删):extract_reply() 抽 ``、_looks_like_reply() 反拒废话/反问/角色扮演;质量不达标/超时/异常 → 空 → 不代发转人工。
  • TEST_MODE=1:生成的回复只发给老板自己(钉钉「自己」会话 send_reply_self + 微信 ClawBot push_weixin),绝不发给原发送人。自测用,验证效果不冒犯同事。详见 _validate.py --inject

抢答防护(延迟窗口 + 老板活跃检测)

问题:发现单聊未读后立即代发,会抢老板自己的回答(老板正拿着手机准备回,AI 已代回)。

方案:发现未读不立即发,走「延迟 + 活跃检测」两步:

  1. 活跃检测(发现即查)owner_recently_active(cid) 拉该会话最近消息,看老板最近 ACTIVE_WINDOW_SEC(默认 300s=5分钟)内是否在该会话发过消息(SELF_OPEN_ID 精确匹配)。活跃 → 跳过不代发(老板在跟,会自己回)。
  2. 延迟窗口:不活跃 → 进 PENDINGREPLY_DELAY_SEC(默认 120s=2分钟)。
    • 窗口内每轮(轮询间隔)持续检测老板是否活跃,活跃则取消代发。
    • 窗口到期再次确认仍不活跃 → 真正代发(gen_reply+send_reply+审计+微信通知)。
  3. 老板在窗口内自己回了该会话 → 活跃检测命中 → 代发取消,绝不抢答

可调环境变量(默认已配好):

  • REPLY_DELAY_SEC=120:延迟窗口时长(秒),给老板自己回的时间。
  • ACTIVE_WINDOW_SEC=300:老板活跃判定窗口(秒),最近 N 秒在该会话发过消息算"正在跟"。
  • TEST_MODE=1:生成的回复只发老板自己(钉钉自己会话 + 微信),不发原发送人;自测上线前验证用。
  • ONCE=1:单次轮询后退出,自测用(不常驻)。
  • TABLE_GROUNDING=0:关闭查表 grounding(纯人设回复,排查用)。

效果:既给老板充足回复时间,又从根上避免"老板正在聊却被 AI 抢话"。群聊/媒体无文本/生成失败仍走原"不代发转人工"逻辑,不受影响。

群聊推送过滤(默认只推 @我/@all)

问题:早期实现把所有群消息都推到微信——普通群刷屏、@别人的消息、特别关注的人发言全都会打扰老板。老板只关心"有人@我"这类真正重要的群消息。

接口事实(已实测)

  • list-unread-conversationschat message list单条消息对象里没有 @我 结构化字段atUsers/atDingTalkIds 都不返回);群消息里 @ 某人只表现为内容前缀纯文本 @昵称(如 @同事A 已经试过了)。
  • dws 另有服务端权威接口 chat message list-mentions --group (只返回 @我 的消息),但空群返回无 messages 字段的空结果,结构不稳定,不适合做热路径每轮轮询。
  • 钉钉"特别关注"联系人列表 dws 拿不到,无法按"是否特别关注"精确过滤;因此用"是否 @我/@all"这个可识别维度来收敛群推送。

方案(内容启发式,零额外 API 调用 + GROUP_PUSH 策略开关)

  • group_msg_is_at_me(content) 判据(⚠️ 2026-08-05 修复:由「仅开头单@」放宽为任意位置子串匹配):
    1. 内容含 @所有人 / @all(词边界)/ <@all>(dws 发送占位)→ 必然含老板 → True
    2. 内容【任意位置】含 @昵称(命中 MENTION_NAMES)→ True——兼容句首/句中/句末、多个 @ 连排、@@昵称 双 @ 格式;
    3. 其它(普通群消息、@别人、特别关注发言、媒体无文本)→ False
  • 群聊微信推送按 GROUP_PUSH 策略(仅作用于群消息;单聊代复+提醒不受影响):
    • atme默认):仅 at_me=True(@我/@all)才推微信;其余群消息静默跳过,不推
    • all:所有群消息都推(旧行为)。
    • off:群聊完全不推微信(连@我都不推)。
    • 命中推送的消息标签为 🔔 钉钉新消息(群聊·有人@你),一眼区分。

配置

  • 老板群昵称候选 MENTION_NAMES(源码默认仅 老板;真实群昵称在私密 .env 配置),群昵称特殊时覆盖:MENTION_NAMES="你的昵称,你的简称"
  • 群推送策略:GROUP_PUSH=atme(默认)| alloff

局限:纯展示名匹配,若群里恰好有人昵称与你相同会误判(极罕见);如需 100% 服务端权威,可改用 list-mentions 二次确认(见上"接口事实"),当前为成本/可靠性权衡选择启发式。

🧪 群聊 AI 草稿预览(GROUP_REPLY_PREVIEW,2026-08-05 新增,可选能力,默认关)

解决什么:老板想验证「群聊 @我 也能 AI 生成回复」的效果,但红线是不在群聊替老板发言——所以做成草稿预览:生成后绝不发钉钉群,只推微信给老板看,验证期后再决定是否开放真代发。

机制(复用单聊 PENDING 状态机,零新状态):

  • 触发:GROUP_REPLY_PREVIEW=1 且 群聊 @我(at_me)且有文本(纯图片 @我 保持现有通知,不预览)。
  • 流程:进延迟窗口(REPLY_DELAY_SEC 2min,与单聊一致)→ 窗口内活跃检测(老板在群里则取消)→ 窗口内消息累积为背景历史(_fetch_recent,含老板历史发言标记 is_self)→ 到期 gen_reply 生成(session dt_ 记忆连续,支持查表/知识库/代码检索全链路)→ push_weixin 推草稿(标签「🔔 钉钉群聊·AI 草稿(未发送)」,附群名/@我的人/原文/草稿),绝不调用 send_reply
  • job 标记 preview_only=True;分发处 if job.get("preview_only") 走预览分支(微信推送失败复用 _pending_after_send 重试语义)。
  • 生成失败 → 推微信「群聊 @你 · AI 草稿生成失败」转人工。
  • 配置:.envGROUP_REPLY_PREVIEW=1(默认 0=关,通用安全)。验证期后若要真代发,再评估放开(需额外确认群聊口吻/权限)。

图片识别(群聊 @我 / 单聊图片补全内容)

问题:钉钉图片消息不是"无文本"——mediaId 内嵌在 content 里([图片消息](mediaId=@lQLP...)),常带文字说明+@人。原逻辑把图片当"(图片/媒体消息)"丢弃,老板既看不到图里是什么,也修不掉"图片被误当纯文本代复"的隐患(content 含 mediaId 噪音,喂给 AI 会乱回)。

实现

  • extract_media_ids(content):正则提取所有图片 mediaId(一条消息可能多张,最多取前 4 张)。
  • clean_text_for_ai(content):去掉 [图片消息](mediaId=...) 噪音,保留纯文字说明,作为 AI 上下文(修掉误代复 bug)。
  • download_images(...):调 dws chat message download-media 把图下到 _media_cache/(路径必须用 Windows 原生反斜杠绝对路径,否则底层 Go 组件误解析 /c/ 导致落盘失败)。
  • describe_image(path) + describe_images(paths)复用 CodeBuddy Agent SDK 多模态(与 gen_reply 同一后端、同一视觉模型;把图 base64 内联进 SDK query,实测可准确读出文字/颜色/图形,文本落在 AssistantMessage/TextBlock,抽取方式与 gen_reply 完全一致)。视觉模型默认跟随文本主模型(CODEBUDDY_MODEL),可用 VISION_MODEL 单独指定 CodeBuddy 侧视觉模型。返回中文描述。主要用于群聊 @我 通知单聊不代复时让老板知道图里是什么
  • 群聊:仅 @我 时识别(避免群刷图烧视觉额度),结果补进微信通知 🖼️ 图片内容:XXXdescribe_images 1 次调用)。
  • 单聊代复:有图且 AUTO_REPLY_IMAGE 开启(默认开) → 把图以 image 协议直接内联进 gen_reply 的 deepseek-v4-flash 调用(识别+代复 1 次调用完成,不再先 describe 再代复的二次调用);失败/质量不达标仍走安全网不代发。单聊不代复(开关关,AUTO_REPLY_IMAGE=0)时则只走 describe_images 1 次拿描述补通知。

降级:CodeBuddy Agent SDK 不可用(_SDK_AVAILABLE=False)→ VISION_ENABLED=False → 不识别、不下载图片,微信通知里图片只显示 (图片/媒体消息)不报错、不阻断。只要 SDK 可用即开箱即用识别,无需任何 key。

配置:视觉后端默认与文本回复同一套 CodeBuddy SDK(零配置、无需任何 key);视觉模型默认跟随 CODEBUDDY_MODEL,可用 VISION_MODEL 单独指定 CodeBuddy 侧视觉模型。单聊图片自动代复开关 AUTO_REPLY_IMAGE(默认开,设 AUTO_REPLY_IMAGE=0 可关闭)。

健壮性要点

  • 日志心跳:每 ~120s 打一行 [heartbeat] alive, unread_now=N(带 dws 健康标记:empty=真无未读、dws_unhealthy!=dws 挂了),无未读也打——一眼区分「真无消息」vs「dws 坏了伪装健康」;连续 6 次失败(约 12 分钟)自动推一次微信异常提醒(节流 30 分钟)。详见下方「读取日志」。
  • 启动静默 seed:首轮把当前未读时间戳记进去重表,只对启动后新到的消息回复,不 retro 回复历史未读。
  • 去重键用 lastMsgCreateAt(媒体消息 openMessageId 可能为空,不能做去重依据)。
  • DRY_RUN=1 环境变量:只生成+打印,不真发回复/不推微信,用于验证。
  • 仅单聊自动回复;SKIP_SENDERS 可配置不代发的名单(家人/上级)。
  • 生成失败/媒体消息不代发:AI 生成失败、质量不达标(_looks_like_reply 拒绝)、或媒体消息无文本时,绝不发兜底话术(避免对方以为老板看到了其实没看到),只推微信「需手动处理」转人工。单聊即使质检拦下也会把 AI 生成的草稿原文一并带进微信通知(不代发到钉钉,但让老板看到草稿、便于手动补发)。
  • 审计日志:每次代发/跳过都写 ~/.workbuddy/dingtalk_auto_audit.jsonl(每行一个 JSON:时间、会话、发件人、内容、回复、发送结果)。以本人身份发消息是高风险对外操作,必须可追溯。
  • 发送成功判断_dws_ok() 解析 dws 返回的 JSON 看 success/errcode,空输出/含 error/errcode≠0 都判失败;不再用"不含 error 即成功"的弱判据。
  • 启动 seed 不吞消息(防"毫无反应"):首轮把未读时间戳记进去重表(不 retro 回复历史),但对「24h 内到达」的单聊未读发一次被动微信提醒(仅提醒、不代复)——downtime 期间到达的消息重启时不再被静默吞掉。
  • 单实例锁~/.workbuddy/dingtalk_auto.lock 存 PID(原子 O_CREAT|O_EXCL 创建,避免竞态),启动检测到锁里 PID 还活着则立刻退出(杜绝同条消息双发);锁里是陈旧 PID(进程已死)则接管。
  • 日志轮转dingtalk_auto_debug.log 超 1MB、dingtalk_auto_audit.jsonl 超 512KB 自动重命名为 .1(保留一份回溯),常驻进程下不再无限撑爆磁盘。
  • 图片缓存清理:每 200 轮自动清 _media_cache/ 下超过 7 天的图片,防常驻下缓存无限增长。
  • 主循环韧性:外层 try/except 包裹整轮;get_unread() 等持续失败时按次数退避(每次 +10s、上限 120s)避免热循环狂刷日志;异常被捕获后下一轮继续,监控不会"死掉"。

运行

Python 环境(固定):必须用装了 codebuddy-agent-sdkdefault venv python(SDK 主后端依赖它)。WorkBuddy 上即该 venv: Windows %USERPROFILE%\.workbuddy\binaries\python\envs\default\Scripts\python.exe;macOS/Linux $HOME/.workbuddy/binaries/python/envs/default/bin/python3(下文统称 $PY) 生产 launcher(gen_launcher.py)已固定此 venv python。若 SDK 不可用,脚本直接走「不代发」(无 CLI 兜底)。

# Windows
PY="$USERPROFILE/.workbuddy/binaries/python/envs/default/Scripts/python.exe"
# macOS / Linux
PY="$HOME/.workbuddy/binaries/python/envs/default/bin/python3"

# 0) 自测(集成验证,不真发回复;会真实调一次 SDK 生成)
"$PY" ~/.workbuddy/skills/dingtalk-auto-reply/_validate.py

# 1) 先验证(不发真实回复、不推微信)
DRY_RUN=1 "$PY" ~/.workbuddy/skills/dingtalk-auto-reply/dingtalk_unread_monitor.py

# 2) 真实运行(会以本人身份发钉钉 + 推微信)
"$PY" ~/.workbuddy/skills/dingtalk-auto-reply/dingtalk_unread_monitor.py

dws 环境变量:监控本体走绝对路径,但为让你手动用 dws 及兜底 DWS_CMD,本技能 gen_launcher.py 会自动把 dws/node 目录追加进用户 PATH(详见下方「dws 依赖」段);手动补 PATH 亦见该段。

⚠️ CodeBuddy 认证(启动前必读)

脚本启动即做认证健康检查,未通过会打印醒目提示并退出(exit 2),不会悄悄空跑:

  • API Key(推荐无人值守).envCODEBUDDY_API_KEY=你的key(申请:https://copilot.tencent.com 控制台)→ 用 Key 直连,不依赖交互登录。
  • CLI 已登录凭据(零配置):留空 Key,脚本自动复用你终端 codebuddy 登录过的凭据。
  • 都没 → 退出码 2,提示二选一:①终端运行 codebuddy 完成登录;②在 .envCODEBUDDY_API_KEY
  • 中国版自动适配:脚本检测 ~/.codebuddy/local_storage 是否标记 internal,自动注入 CODEBUDDY_INTERNET_ENVIRONMENT=internal,无需手动配。

脚本绝不代填/存储你的登录凭据,只检测"是否已登录"并给提醒——认证动作由你本人在终端完成。首次启动前请先确认已登录(终端跑一次 codebuddy 或填 Key),未登录会打印提示并以退出码 2 退出,绝不代发。

部署方式

WorkBuddy 桌面客户端不能作为后台常驻进程;但本脚本是独立 Python 进程,可自行后台常驻轮询。生成回复走 Python SDK(即起即退的子进程),不依赖 WorkBuddy 桌面客户端运行。

Windows · Startup 启动器(推荐)

启动器 .vbs 不随技能分发(见上方移植说明,已被 .gitignore 排除),由本机 gen_launcher.py 生成到 %APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\。两种落地方式,任选其一(或都做):

# 方式 A:手动生成一次(推荐,部署时执行)
"$PY" ~/.workbuddy/skills/dingtalk-auto-reply/gen_launcher.py
# 方式 B:什么都不做 —— 监控脚本首次运行会自动自检并生成(日志可见 [launcher] 行)
  • 生成后双击即可手动拉起;放进「启动」文件夹则每次登录自动运行(生成器已写到该位置)。
  • 自带崩溃自愈看门狗:常驻循环每 30s 探活 dingtalk_unread_monitor.py 的 python——进程不在立刻拉起;进程在但调试日志超 180s 未更新(卡死/静默空转)则杀掉重拉,~30s 内自愈,无需重登。
  • 单实例锁(见上方「健壮性要点」)双重保险,重复启动自动退出,杜绝双发。
  • 生成器以 纯 ASCII.vbs(全英文注释),Windows Script Host 读取零风险。
  • 看门狗逻辑唯一可信源即生成出的 dingtalk_auto_reply_launcher.vbs(由 gen_launcher.pyVBS_CONTENT 常量生成,改动请以改 gen_launcher.py 为准,勿手改运行中的 .vbs 以免漂移)。

⚠️ 为什么不用 Windows 计划任务:计划任务把脚本跑在隔离会话里,该会话无网络出口、也拿不到 WorkBuddy 宿主本地服务dws 连不上后端、每次 get_unread 超时返回空 → 日志只剩启动 banner、看不到 raw unread、不会代复(进程活着 CPU≈0 却不工作)。Startup 启动器在登录后的交互式会话(与 WorkBuddy 宿主同会话、有完整网络)拉起,从根上避开。故计划任务方案已弃用,不要再注册。

🔴 窗口样式与 dws 访问无关(2026-07-15 老板纠偏):VBS 拉起监控的窗口样式 N(0=隐藏 / 1=普通 / 2=最小化)不影响 dws 能否拿到未读。能否拿到未读只取决于「dws 在系统 PATH」+「跑在登录后的交互式会话」(见上方「为什么不用 Windows 计划任务」与下方「dws 依赖」两段),与隐藏/最小化无关。之前"隐藏窗口会摘出会话导致 dws 返回空"是错误猜想,真实根因是 dws 当时没进 PATH。

macOS / Linux

nohup python ~/.workbuddy/skills/dingtalk-auto-reply/dingtalk_unread_monitor.py \
  > ~/.workbuddy/dingtalk_auto_daemon.log 2>&1 & disown

手动 start / stop

  • 启动:直接 python dingtalk_unread_monitor.py(前台 python 调试;正式运行由 VBS 启动器以 python.exe 隐藏窗口拉起)。
  • 停止(Windows):stop_monitor.ps1(精确结束本脚本 python,不动其它 python)。
  • 停止(macOS/Linux):pkill -f dingtalk_unread_monitor.py 按 PID 杀。

消息查找能力现状(2026-08-03 严谨验证,改查找逻辑前必读)

跨单聊+群聊查找历史消息,可靠手段 = 按人 / 按会话枚举+search-msg 关键词全量搜索当前不可用(见下证据)。

需求命令状态
按人拉消息(含单聊+群聊)dws chat message list-by-sender --sender-user-id --start/--end可靠,首选(agent 查同事记录就用它)
枚举全部会话(含单聊+群聊)dws chat list-all-conversations --limit N✅ 可靠(实测 15 个会话)
按会话拉消息dws chat message list --group --time/--direction older✅ 可靠
按关键词全量搜索dws chat +search-msg --query "词" --start/--end接口在但当前不命中

+search-msg 不可用的验证证据(2026-08-03,已排除授权/数据缺失/时间窗):

  • 定向群搜「MS42DDC」「步进电机」(词确认 7/31 真实出现在该群,list 已拉到)→ count=0
  • 按发送者搜同事乙(确认 7/31 发过 2 条)→ count=0;对照 list-by-sender 同条件 → 命中 2 条
  • --verbose 全程无报错;dws chat data-auth cross-org --all --ttl 24h 授权后重测仍 0
  • chat.message:search / chat.message:list scope 授权尝试 → unknown scope(INVALID_SCOPE)
  • 结论:疑似服务端全文索引未对当前账号开放/未覆盖单聊与历史消息。勿再尝试用 +search-msg 做热路径,按人查走 list-by-sender 即可。

dws 依赖(必须满足,不依赖钉钉客户端)

监控只通过 dws(DingTalk Workspace CLI,WorkBuddy 连接器) 读取未读、发送回复, 不依赖本机钉钉桌面客户端(dws 走 WorkBuddy 宿主本地服务,与是否在跑钉钉客户端无关)。 今天实测已石锤:dws 独立工作,之前「unread_now 恒为 0」的真实根因是 dws 不在系统 PATH 上, 而非客户端。需满足:

  • dws 已授权dws patch chmod 授权 chat.message:list / chat.message:send / contact:search);

  • dws 在系统 PATH 上(User 环境变量):终端能敲出 dws 即说明已就位;本技能 gen_launcher.py自动把 dws/node 目录追加进用户 PATH(HKCU\Environment,幂等、写后广播刷新),无需手动配;

  • 监控进程跑在登录后的交互式会话(与 WorkBuddy 宿主同会话、有完整网络,而非计划任务/服务那种隔离会话);

  • dws 取不到未读(返回空/超时)的典型原因:PATH 缺失(dws 找不到)、会话隔离(无网)、未授权 → 监控"看着活着"却什么都不做。

    ⚙️ 关于"读取空"的另一层根因(已实现方案,无需用户操作):dws 是 Node 打包二进制,对匿名管道(PIPE)的 stdout 是异步写,进程退出前未 flush → Python capture_output 读到 0 字节(与父进程是否控制台无关,python.exe 下同样复现)。监控统一用文件重定向(dws 输出写临时文件再读)绕过,每次稳定拿到数据;此实现细节已封装,用户只需保证上方 PATH / 会话 / 授权三条即可。

读取日志(诊断)

日志位置(监控以 python.exe 隐藏窗口运行,全部写文件,不落 stdout):

  • ~/.workbuddy/dingtalk_auto_debug.log —— 运行日志(超 1MB 自动轮转为 .1
  • ~/.workbuddy/dingtalk_auto_audit.jsonl —— 代发/跳过审计(超 512KB 轮转)

如何判断监控健康tail 最后几行):

日志现象含义处理
每 ~120s 一行 [heartbeat] alive, unread_now=N, ts=HH:MM:SS正常轮询中(dws 健康)无需处理
[heartbeat] alive, unread_now=0 (empty: 真无未读), ts=...dws 健康但当前真无未读无需处理
[heartbeat] alive, unread_now=0 (dws_unhealthy! fail×N), ts=...dws 挂了(PATH/授权/会话异常),unread_now=0 是假的,消息全漏检查 dws 是否在 PATH / 授权过期 / 钉钉客户端是否在跑;连续 6 次失败(约 12 分钟)会自动推一次微信提醒
raw unread: [...](仅未读集合变化时打印)拉到新未读会话了正常,走代复/提醒流程
只有启动 banner、长时间无 heartbeat/raw unread卡死或掉进无网会话看门狗应在 180s 内自动杀掉重拉;若仍无,检查钉钉客户端是否在跑、网络是否通
已有另一个监控实例在运行,本进程退出单实例锁生效,重复启动被拒正常,无需处理
auto-reply monitor started新实例刚拉起正常
审计日志出现 skip_reply / notify_only主动跳过(老板活跃 / 群非@我 / 生成失败)设计内,非 bug

🧠 观察 agent 完整思考/执行过程(DEBUG_AGENT_TRACE=1,2026-08-03 新增)

SDK 是流式输出——agent 每一步(思考 → 工具调用 → 工具结果 → 最终回复)都能完整看到。 排查「agent 到底调没调 dws / 查到了什么 / 为什么回复不理想」时,不用猜,直接看轨迹:

# 自测时前缀环境变量(监控常驻期间开轨迹需重启监控生效)
DEBUG_AGENT_TRACE=1  _validate.py --inject --sender 同事甲 --message "第二期更新你对接同事丙了吗"

开启后,每次 SDK 生成会在 ~/.workbuddy/dingtalk_auto_debug.log 打一段 [agent-trace] 多行块:

[18:17:28] [agent-trace]
[thinking] 用户问的是…提到了具体同事人名"同事丙",并涉及"对接/进展"…
[tool_use] Bash input={"command": "dws contact user search --query \"同事丙\" --format json", ...}
[tool_use] Bash input={"command": "dws chat message list-by-sender --sender-user-id …", ...}
[thinking] 查到了真实记录:7月8号同事丙说"好的 这个我弄一下"…
[result] 跟进了,7月8号跟同事丙聊过…基本快好了。
  • block 类型[thinking]=思考过程、[tool_use]=工具名+入参、[tool_result]=工具返回、[result]=最终 ``。
  • 典型排查:回复说"还没对接"但轨迹里没有 [tool_use] dws contact… → agent 偷懒没查;有 tool_use 但没 tool_result → dws 命令失败(多半 PATH 问题,见 runtime.build_sdk_env 的 PATH 注入);有 tool_result 但回复没引用 → 口吻/约束问题。
  • 默认关(常驻防刷屏):环境变量 DEBUG_AGENT_TRACE=1 开启,用完整绝对路径 venv python 跑才生效。
  • 配合 few-shotreply.py_FEW_SHOT_EXAMPLES 常量(内联进代码 prompt,2026-08-04 起;dws-reply-examples.md 为随包分发文档副本)每次调用注入 system_prompt,教 agent「提到人名+对接/进展→先调 dws 查记录再答;源码/接口问题→先调 search.py 查代码再答;回复像老板真人、不暴露查询动作」。

自测脚本 _validate.py

7 种模式(参数互斥,按需选一):

  • 默认集成验证:打印所有外部二进制/人设的解析路径(DWS_EXE/DWS_ENTRY/DWS_CMD/NODE/CODEBUDDY_CMD/SEND_JS/SOUL_AGENT 等,确认可移植探测正确)。
  • --inject:手动注入一条假消息跑 gen_reply,把回复「发给自己」(dws chat message send --open-dingtalk-id <自己>),绝不发给别人;想去掉 DRY_RUN 后真发,方便验证生成效果。
  • --test-guard:验证抢答防护逻辑(活跃检测 owner_recently_active + 延迟窗口参数),不真发、不触碰任何人会话。自动探测真实单聊 cid,打印活跃检测结果并演示"进窗口→到期代发"(DRY_RUN 下只生成不真发)。
  • --test-construct:回归验证 gen_reply 的 prompt 构造(2026-07-20 老板纠偏 + 2026-07-30 burst 合并的关键修复点)。monkey-patch 拦截 SDK 调用,不真发、不耗积分。五个场景:①延时期内对方连发 5 条 → burst-merged 物理合并成 1 条主消息;②单条对方消息问进度 → single-main 模式 + 查表激活;③延时期内对方连发"问题反馈"+"测试" → burst 合并后 table_context 保留(旧"测试丢弃"已被 burst 替代);④含老板历史发言 → burst 合并对方窗口消息 + 老板历史进背景;⑤跨窗口的"问题反馈"(时间差 > REPLY_DELAY_SEC)→ 不进 burst、当背景历史、不触发查表(防 07-20 旧 bug 复发)。
  • --test-statemachine:验证延迟代发状态机纯函数(_pending_next_state / _pending_after_send / _gnotify_next_state / _gnotify_after_push,定义在 dingtalk_unread_monitor.py)。不依赖 dws/SDK,纯逻辑断言。覆盖:PENDING/GNOTIFY 的「未到期+活跃→取消」「未到期+重试中不取消」「到期+dws未确认→defer」「defer超限→单聊转人工/群直推」「到期→代发/推送」「发送/推送失败<5次→重试」「失败达5次→放弃」,共 19 条断言。回归「监控主循环状态机偏重」的重构——抽纯函数后行为不变。
  • --test-mcp:验证 gbrain MCP 工具链路(initialize 握手 + tools/list 含 search/query + tools/call search 返回真实内容 + gen_reply system_prompt 注入 gbrain 指引),确认知识库通路就绪。
  • --env(即 --check-env:仅做环境预检:核查 SDK / dws / codebuddy CLI / node / 视觉 / 人设 是否就位,逐项打印 [OK]/[MISSING]/[WARN],阻断级缺失给精确修复命令并以退出码非 0 退出;不跑任何生成。

每次自测必跑 dws 实际调用比对(默认开 DEBUG_DWS_CALL=1):把「配置的路径(DWS_EXE/DWS_ENTRY/DWS_CMD/NODE 是否存在)」与「run_dws 实际选用的路由(DIRECT-exe / NODE-direct / DWS_CMD-fallback / NONE)」和「接口真实返回的字节长度 / 解析出的会话数」三者对齐打印,给 PASS / WARN / FAIL 结论;同时把每次 dws 调用的真实 argv + 返回码 + stdout 长度写进调试日志([dws-call] / [dws-route] 行,数据抓取表现为 [dws-file] OK out_len=N)。三条路由输出统一走文件重定向(dws 对 PIPE 异步 flush 丢失、Python 读 PIPE 恒空,文件重定向是当前主力方案,与控制台/解释器无关)。排障"dws 找不到/调不通"时,先看这段比对 + 日志里的 argv,一眼定位是 PATH 缺失还是入口解析错。

  • 默认模式会调用 get_unread 看未读接口是否通;若有单聊未读,继续验证 get_latest_msg 字段、gen_reply 生成、reply --dry-run 命令形态。
  • 不真发任何回复(reply 用 --dry-run),但默认模式若有真实单聊未读时 gen_reply 会真实调一次 codebuddy 生成文本(仅一次、不发送);--test-construct 用 monkey-patch 拦截 SDK,不真发、不耗积分。
  • 当前环境若无单聊未读(只有群聊),默认模式会优雅跳过单聊集成段——符合「群聊不代发」预期,不算失败。

漏发补发脚本 recover_missed.py

监控曾宕机、或历史某次以 DRY_RUN=1 启动过(只生成不真发)导致漏掉的单聊消息,用本脚本手动补发:拉取当前未读单聊 → 用 deepseek-v4-flash 生成本人口吻回复 → 真正发送并推微信通知。

# 补发所有漏掉的单聊未读(实时真发)
"$PY" ~/.workbuddy/skills/dingtalk-auto-reply/recover_missed.py

⚠️ 会真实以本人身份发消息,仅在你确认"确有漏发"时使用;群聊不代发(只通知),符合全局策略。

依赖(移植目标机需具备)

  1. dws CLI(WorkBuddy 自带):需授权 chat.message:listchat.message:sendcontact:search(后者用于获取本人 openid 以精确识别「回复自己」;用 dws patch chmod 授权)。
    • ⚙️ 必须在系统 PATH 上(关键根因,勿漏):机制与自动追加见上方「dws 依赖」段。
  2. codebuddy CLI(WorkBuddy managed node 目录自带):SDK 的 codebuddy_code_path 指向其 codebuddy.cmd,SDK 内部 spawn 它来跑 agent;模型 deepseek-v4-flash
  3. codebuddy-agent-sdk(Python SDK,唯一后端):pip install codebuddy-agent-sdk必须装到运行脚本的 python 环境——WorkBuddy managed python 的 default venv(Windows:%USERPROFILE%\.workbuddy\binaries\python\envs\default\Scripts\python.exe -m pip install codebuddy-agent-sdk;macOS/Linux:$HOME/.workbuddy/binaries/python/envs/default/bin/python -m pip install codebuddy-agent-sdk)。未装则 gen_reply 直接返回空(不代发),不会崩溃。
  4. weixinclaw-proactive-push skill(可选):用于微信主动推送;未装则自动降级为仅日志。
  5. 人设真源 = codebuddy 注册 agent dingtalk-helper.md(全局 ~/.codebuddy/agents/,不随 skill 打包);skill 内 dingtalk-helper-backup.md 是其干净部署模板(占位,无私人数据;随 skill 走、换机兜底)。改人设只改 dingtalk-helper.md 一处。

配置(.env,可选但推荐)

技能目录自带 .env.example,移植到新机:cp .env.example .env 后按需填写。所有项均可选,留空=自动探测/默认。已存在的系统环境变量不被 .env 覆盖。

  • CodeBuddy 认证:见上方「运行」段的 ⚠️ CodeBuddy 认证(API Key 或 CLI 已登录二选一)。
  • 其他可配项POLL_INTERVAL(轮询秒)、DRY_RUN=1(只验证不真发)、REPLY_DELAY_SEC(延迟窗口秒,默认120)、ACTIVE_WINDOW_SEC(活跃窗口秒,默认300)、GROUP_PUSH=atme|all|off(群消息推微信策略,默认 atme)、MENTION_NAMES(群 @我 昵称候选,在私密 .env 配你的昵称)、SKIP_SENDERS(不代发名单)、SELF_SENDERS(本人昵称)、AUTO_REPLY_IMAGE(单聊图片代复开关,默认开)、VISION_MODEL(视觉模型,默认跟随文本主模型 CODEBUDDY_MODEL 即 deepseek-v4-flash、可单独指定 CodeBuddy 侧视觉模型)

相关技能

钉钉群机器人工具与开放平台开发指南。Use for: (1) 用附带的零依赖 CLI 向钉钉群发通知——文本/markdown/链接卡片,支持加签安全模式和@人, (2) CI/监控/日报等自动化通知场景, (3) 钉钉开放平台 API(token/审批/通讯录)开发指导。DingTalk group-robot...

33 次安装2 星标

Cross-platform IM messaging CLI for DingTalk, Feishu (Lark) enterprise apps and WeCom (WeChat Work) group robot webhooks. Send P2P / group messages with text...

管理钉钉产品能力(AI表格/AI搜问/日历/通讯录/群聊与机器人/待办/审批/考勤/日志/DING消息/开放平台文档/钉钉文档/钉钉云盘/AI听记/邮箱/在线电子表格/知识库等)。当用户需要操作表格数据、管理日程会议、模糊找人/查谁负责某事项、查询通讯录、管理群聊、机器人发消息、创建待办、提交审批、查看考勤、提交日报周报(钉钉日志模版)、读写钉钉文档、上传下载云盘文件、查询听记纪要、收发邮件、读写在线电子表格(axls)、管理钉钉知识库时使用。

火一五钉钉智能体隔离 - 为每个钉钉用户创建独立 Agent 工作空间、记忆和会话历史。支持手动绑定和动态创建两种模式。触发词:钉钉Agent隔离、钉钉用户隔离、动态Agent创建、dingtalk agent isolation。

微信AI助手 — 主动发消息 + 自动回复 / WeChat AI assistant — send messages & auto-reply

1 次安装1 星标