钉钉群机器人工具与开放平台开发指南。Use for: (1) 用附带的零依赖 CLI 向钉钉群发通知——文本/markdown/链接卡片,支持加签安全模式和@人, (2) CI/监控/日报等自动化通知场景, (3) 钉钉开放平台 API(token/审批/通讯录)开发指导。DingTalk group-robot...
Coding
dingtalk-auto-reply
Try it监控钉钉未读会话,单聊用 AI 以本人身份自动回复(普通员工口吻、平级回同事),群聊/指定名单只发微信提醒(不代发,防社死)。覆盖 dws 未读接口、CodeBuddy Agent SDK 生成回复的 SERVER__PORT 端口冲突坑、自报家门坑、回复自己坑。当用户要求「钉钉自动回复/钉钉代回/监听钉钉未读+AI回复」时使用。
What it does
监控钉钉未读会话,单聊用 AI 以本人身份自动回复(普通员工口吻、平级回同事),群聊/指定名单只发微信提醒(不代发,防社死)。覆盖 dws 未读接口、CodeBuddy Agent SDK 生成回复的 SERVER__PORT 端口冲突坑、自报家门坑、回复自己坑。当用户要求「钉钉自动回复/钉钉代回/监听钉钉未读+AI回复」时使用。
The skill document
钉钉自动回复(未读监控 → 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.py仍import dingtalk_unread_monitor as M无需改动。 注意:可变配置DRY_RUN/SELF_OPEN_ID经runtime.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 CLI | SDK 底层起 prewarm server 用 | 随 WorkBuddy 安装;CODEBUDDY_CMD 路径见 print_paths() |
| node | dws NODE-direct 路由需要 | 装 Node 并确保在可用路径 |
| gbrain MCP(可选) | 查表 / 知识库;不配则纯人设回复 | GBRAIN_MCP_URL / GBRAIN_MCP_TOKEN,缺省自动读 ~/.workbuddy/mcp.json 的 gbrain 条目 |
| 代码库检索(可选) | 让回复 agent 检索本地源码回答「源码/接口/实现细节」问题 | .env 填 CODE_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 环境)
- 装依赖(如尚未装):用目标 venv 的 python 装 SDK
%USERPROFILE%\.workbuddy\binaries\python\envs\default\Scripts\python.exe -m pip install -r requirements.txt - 探测并写入 SDK 环境(关键):跑安装脚本,让 agent 把 SDK 运行环境写进
.env
它会遍历候选 python 找到装了%USERPROFILE%\.workbuddy\binaries\python\envs\default\Scripts\python.exe _setup_env.py # 仅预览不写: _setup_env.py --check # 强制覆盖 SDK 项: _setup_env.py --forcecodebuddy-agent-sdk的那个,连同主模型 / 网络环境 / CLI 路径写入.env的CODEBUDDY_SDK_PYTHON等项。之后 monitor 被任意 python 拉起都能按该声明自拉回到正确环境(见 runtimesdk_reexec_target)。 - 预检:确认环境完整
%USERPROFILE%\.workbuddy\binaries\python\envs\default\Scripts\python.exe _validate.py --env - 填私密身份:
cp .env.example .env(若还没建)→ 在.env填BOSS_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(顺序不可乱)
dws chat message list-unread-conversations --format json→ 只返回有未读的会话(天然过滤)。 字段:openConversationId、title、singleChat(true=单聊)、unreadPoint、lastMsgCreateAt。- 单聊 → 用
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)。 - 回复生成(CodeBuddy Agent SDK 主后端 + 查表事实 grounding + 会话记忆):先按消息意图
detect_table_intent判断查哪张表 (feedback=问题反馈表 /project=ROS项目表),用fetch_table_context拉「本人(userId=)名下未解决项」 (带缓存)注入 system_prompt;再用 CodeBuddy Agent SDK(_gen_reply_sdk_async,system_prompt=人设+查表数据,query=干净单行消息) 生成回复,session_id=dt_(首轮session_id、后续resume续上下文)实现「一人一会话、记忆连续」。 SDK 不可用/失败 → 返回空(不代发,推微信转人工),无 CLI 兜底。详见下方「生成回复的关键坑」。 dws chat message reply --conversation-id --ref-msg-id --ref-sender --text <回复> --yes带引用回复。- 微信推送:默认用
~/.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 CLI | CODEBUDDY_CMD | ~/.workbuddy/binaries/node/versions/<最新版本>/codebuddy.cmd |
| node | NODE | ~/.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.py 经 extra_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.py 经 CodeBuddyAgentOptions(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.py内CodeBuddyAgentOptions(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.json的gbrain条目读取(url + Bearer token),单一真源、无需重复填 token;也可用环境变量覆盖。
_MODE_LOCK与dingtalk-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 不假设项目表存在):
- 纯人设代复(推荐起步):设
TABLE_GROUNDING=0关闭查表 grounding,agent 只按【身份红线】以本人口吻回复,不查任何表。最稳,零 dws 项目依赖。 - 接自己的项目结构:把
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.md | skill 内 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 首次运行自动建空记忆并自行重建上下文,绝不随包分发。 |
私密配置 .env | skill 内 .env.example(.env 已被 .gitignore 排除) | ⚠️ 不随分发 | 新机 cp .env.example .env 后填 BOSS_UID/SELF_OPENDINGTALK_ID 等。 |
路径可移植:
DINGTALK_WORKSPACE、BOSS_UID均支持环境变量覆盖(runtime.py用os.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 合并路径——既保留跨窗口防波堤,又松绑延时期内连发的合并回复。
- 【2026-07-30 burst 合并 · 当前主路径】:取最新对方消息 ts T0,窗口
- 查表事实 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 触发查表——这是合理的,对方确实在问。
- 【关键收紧】主消息二次校验:table_context 注入 system_prompt 之前会再用
- 会话记忆(一人一会话连续):
session_id = "dt_" +;同一对话首轮传session_id,后续传resume=dt_续上下文(记忆连续,已实测跨进程 resume 有效)。session 历史里若已回复过某话题,话题红线会禁止 AI 再次主动提及(详见_MODE_LOCK末尾)。 - SDK 不可用/失败 → 返回空(不代发,推微信转人工),无 CLI 兜底。
关键坑(必读):
- SERVER__PORT 端口冲突(最致命):每次生成分配空闲端口注入
env["SERVER__PORT"],否则query()0 字节超时。 - 运行环境必须固定为托管 venv python(
binaries/python/envs/default/Scripts/python.exe):本机codebuddy-agent-sdk装在 venv 里,用基础托管 python 跑会因 import 失败导致_SDK_AVAILABLE=False。生产 launcher(gen_launcher.py)已固定 venv python,自测也要用 venv python。 - 别"回复自己":同前,
_is_self+SELF_OPEN_ID精确匹配。 - ⚠️ 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」先量长度、别怀疑环境。 - ⚠️ 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保留为随包分发文档副本(改动同步常量)。 - ⚠️ 毫秒/秒时间戳混用(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+ 微信 ClawBotpush_weixin),绝不发给原发送人。自测用,验证效果不冒犯同事。详见_validate.py --inject。
抢答防护(延迟窗口 + 老板活跃检测)
问题:发现单聊未读后立即代发,会抢老板自己的回答(老板正拿着手机准备回,AI 已代回)。
方案:发现未读不立即发,走「延迟 + 活跃检测」两步:
- 活跃检测(发现即查):
owner_recently_active(cid)拉该会话最近消息,看老板最近ACTIVE_WINDOW_SEC(默认 300s=5分钟)内是否在该会话发过消息(SELF_OPEN_ID精确匹配)。活跃 → 跳过不代发(老板在跟,会自己回)。 - 延迟窗口:不活跃 → 进
PENDING等REPLY_DELAY_SEC(默认 120s=2分钟)。- 窗口内每轮(轮询间隔)持续检测老板是否活跃,活跃则取消代发。
- 窗口到期再次确认仍不活跃 → 真正代发(
gen_reply+send_reply+审计+微信通知)。
- 老板在窗口内自己回了该会话 → 活跃检测命中 → 代发取消,绝不抢答。
可调环境变量(默认已配好):
REPLY_DELAY_SEC=120:延迟窗口时长(秒),给老板自己回的时间。ACTIVE_WINDOW_SEC=300:老板活跃判定窗口(秒),最近 N 秒在该会话发过消息算"正在跟"。TEST_MODE=1:生成的回复只发老板自己(钉钉自己会话 + 微信),不发原发送人;自测上线前验证用。ONCE=1:单次轮询后退出,自测用(不常驻)。TABLE_GROUNDING=0:关闭查表 grounding(纯人设回复,排查用)。
效果:既给老板充足回复时间,又从根上避免"老板正在聊却被 AI 抢话"。群聊/媒体无文本/生成失败仍走原"不代发转人工"逻辑,不受影响。
群聊推送过滤(默认只推 @我/@all)
问题:早期实现把所有群消息都推到微信——普通群刷屏、@别人的消息、特别关注的人发言全都会打扰老板。老板只关心"有人@我"这类真正重要的群消息。
接口事实(已实测):
list-unread-conversations和chat 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 修复:由「仅开头单@」放宽为任意位置子串匹配):- 内容含
@所有人/@all(词边界)/<@all>(dws 发送占位)→ 必然含老板 →True; - 内容【任意位置】含
@昵称(命中MENTION_NAMES)→True——兼容句首/句中/句末、多个 @ 连排、@@昵称双 @ 格式; - 其它(普通群消息、@别人、特别关注发言、媒体无文本)→
False。
- 内容含
- 群聊微信推送按
GROUP_PUSH策略(仅作用于群消息;单聊代复+提醒不受影响):atme(默认):仅at_me=True(@我/@all)才推微信;其余群消息静默跳过,不推。all:所有群消息都推(旧行为)。off:群聊完全不推微信(连@我都不推)。- 命中推送的消息标签为
🔔 钉钉新消息(群聊·有人@你),一眼区分。
配置:
- 老板群昵称候选
MENTION_NAMES(源码默认仅老板;真实群昵称在私密.env配置),群昵称特殊时覆盖:MENTION_NAMES="你的昵称,你的简称"。 - 群推送策略:
GROUP_PUSH=atme(默认)|all|off。
局限:纯展示名匹配,若群里恰好有人昵称与你相同会误判(极罕见);如需 100% 服务端权威,可改用 list-mentions 二次确认(见上"接口事实"),当前为成本/可靠性权衡选择启发式。
🧪 群聊 AI 草稿预览(GROUP_REPLY_PREVIEW,2026-08-05 新增,可选能力,默认关)
解决什么:老板想验证「群聊 @我 也能 AI 生成回复」的效果,但红线是不在群聊替老板发言——所以做成草稿预览:生成后绝不发钉钉群,只推微信给老板看,验证期后再决定是否开放真代发。
机制(复用单聊 PENDING 状态机,零新状态):
- 触发:
GROUP_REPLY_PREVIEW=1且 群聊 @我(at_me)且有文本(纯图片 @我 保持现有通知,不预览)。 - 流程:进延迟窗口(
REPLY_DELAY_SEC2min,与单聊一致)→ 窗口内活跃检测(老板在群里则取消)→ 窗口内消息累积为背景历史(_fetch_recent,含老板历史发言标记 is_self)→ 到期gen_reply生成(sessiondt_记忆连续,支持查表/知识库/代码检索全链路)→ 只push_weixin推草稿(标签「🔔 钉钉群聊·AI 草稿(未发送)」,附群名/@我的人/原文/草稿),绝不调用send_reply。 - job 标记
preview_only=True;分发处if job.get("preview_only")走预览分支(微信推送失败复用_pending_after_send重试语义)。 - 生成失败 → 推微信「群聊 @你 · AI 草稿生成失败」转人工。
- 配置:
.env填GROUP_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 侧视觉模型。返回中文描述。主要用于群聊 @我 通知和单聊不代复时让老板知道图里是什么。- 群聊:仅 @我 时识别(避免群刷图烧视觉额度),结果补进微信通知
🖼️ 图片内容:XXX(describe_images1 次调用)。 - 单聊代复:有图且
AUTO_REPLY_IMAGE开启(默认开) → 把图以 image 协议直接内联进gen_reply的 deepseek-v4-flash 调用(识别+代复 1 次调用完成,不再先describe再代复的二次调用);失败/质量不达标仍走安全网不代发。单聊不代复(开关关,AUTO_REPLY_IMAGE=0)时则只走describe_images1 次拿描述补通知。
降级: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-sdk的 default 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(推荐无人值守):
.env填CODEBUDDY_API_KEY=你的key(申请:https://copilot.tencent.com 控制台)→ 用 Key 直连,不依赖交互登录。 - CLI 已登录凭据(零配置):留空 Key,脚本自动复用你终端
codebuddy登录过的凭据。 - 都没 → 退出码 2,提示二选一:①终端运行
codebuddy完成登录;②在.env填CODEBUDDY_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.py的VBS_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授权后重测仍 0chat.message:search/chat.message:listscope 授权尝试 →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-shot:
reply.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_replysystem_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
⚠️ 会真实以本人身份发消息,仅在你确认"确有漏发"时使用;群聊不代发(只通知),符合全局策略。
依赖(移植目标机需具备)
- dws CLI(WorkBuddy 自带):需授权
chat.message:list、chat.message:send与contact:search(后者用于获取本人 openid 以精确识别「回复自己」;用dws patch chmod授权)。- ⚙️ 必须在系统 PATH 上(关键根因,勿漏):机制与自动追加见上方「dws 依赖」段。
- codebuddy CLI(WorkBuddy managed node 目录自带):SDK 的
codebuddy_code_path指向其codebuddy.cmd,SDK 内部 spawn 它来跑 agent;模型deepseek-v4-flash。 - 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直接返回空(不代发),不会崩溃。 - weixinclaw-proactive-push skill(可选):用于微信主动推送;未装则自动降级为仅日志。
- 人设真源 = 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 侧视觉模型)
Related skills
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)、管理钉钉知识库时使用。
Operate DingTalk v1.0 messaging APIs via UXC + curated OpenAPI schema
火一五钉钉智能体隔离 - 为每个钉钉用户创建独立 Agent 工作空间、记忆和会话历史。支持手动绑定和动态创建两种模式。触发词:钉钉Agent隔离、钉钉用户隔离、动态Agent创建、dingtalk agent isolation。
微信AI助手 — 主动发消息 + 自动回复 / WeChat AI assistant — send messages & auto-reply