Install
openclaw skills install @noaheleven/dingtalk-auto-reply监控钉钉未读会话,单聊用 AI 以本人身份自动回复(普通员工口吻、平级回同事),群聊/指定名单只发微信提醒(不代发,防社死)。覆盖 dws 未读接口、CodeBuddy Agent SDK 生成回复的 SERVER__PORT 端口冲突坑、自报家门坑、回复自己坑。当用户要求「钉钉自动回复/钉钉代回/监听钉钉未读+AI回复」时使用。
openclaw skills install @noaheleven/dingtalk-auto-reply监控钉钉未读会话,单聊用 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 |
%USERPROFILE%\.workbuddy\binaries\python\envs\default\Scripts\python.exe -m pip install -r requirements.txt
.env
%USERPROFILE%\.workbuddy\binaries\python\envs\default\Scripts\python.exe _setup_env.py
# 仅预览不写: _setup_env.py --check
# 强制覆盖 SDK 项: _setup_env.py --force
codebuddy-agent-sdk 的那个,连同主模型 / 网络环境 / CLI 路径写入 .env 的 CODEBUDDY_SDK_PYTHON 等项。之后 monitor 被任意 python 拉起都能按该声明自拉回到正确环境(见 runtime sdk_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]:
dingtalk-helper-backup.md 兜底)。每次跑其它模式(集成 / --inject / --test-*)也会先自动跑一遍预检,环境异常第一时间暴露。
⚠️ 视觉与文本共用同一 SDK 后端:
_SDK_AVAILABLE=True时两者都可用;SDK 缺失时文本也生成不了,不是视觉单独降级——所谓「仅通知不识别」是 SDK 缺失时的整体回退,与以前行为一致。
dws chat message list-unread-conversations --format json → 只返回有未读的会话(天然过滤)。
字段:openConversationId、title、singleChat(true=单聊)、unreadPoint、lastMsgCreateAt。openConversationId 拉最新一条消息:
dws chat message list --group <openConversationId> --time "<now>" --direction older --limit 5 --format json
⚠️ 必须带 --direction older:含义"从给定时间往更早拉",配 --time now 即返回最新消息(newest-first)。
不带 direction 时默认 older 且起始时间若早于消息 → 消息被过滤成空。
单聊也能用 --group <openConversationId>(无需 --user/--open-dingtalk-id)。detect_table_intent 判断查哪张表
(feedback=问题反馈表 / project=ROS项目表),用 fetch_table_context 拉「本人(userId=<BOSS_UID>)名下未解决项」
(带缓存)注入 system_prompt;再用 CodeBuddy Agent SDK(_gen_reply_sdk_async,system_prompt=人设+查表数据,query=干净单行消息)
生成回复,session_id=dt_<cid>(首轮 session_id、后续 resume 续上下文)实现「一人一会话、记忆连续」。
SDK 不可用/失败 → 返回空(不代发,推微信转人工),无 CLI 兜底。详见下方「生成回复的关键坑」。dws chat message reply --conversation-id <cid> --ref-msg-id <openMessageId> --ref-sender <senderOpenDingTalkId> --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 后自行填写。.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(如把 <系统用户名> 当称呼),也禁止主动透露老板真实姓名/城市/公司等个人信息。真源 = 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 也开)已补上,勿回退。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"直接冲突,留作兜底反而有害)。dingtalk-helper-backup.md → 硬编码中性话术(不会因缺文件而崩),全程不会双灵魂注入。旧方案:把产品资料/项目文件/记忆直接铺进 agent 工作空间,靠 agent 用 Grep/Glob/Read 自行读文件取知识——慢、依赖模型主动检索、易漏。
新方案(更快更准,且带保底):gbrain 知识库 MCP 服务为知识主入口,工作空间本地文档作保底回退。
gbrain serve --http,默认 localhost:3131/mcp),持 PGLite 写锁;WorkBuddy 连接器与本 skill 的回复 agent 都作为 HTTP 客户端连它,不各起进程、不抢锁。mcp_servers 挂上 gbrain(reply.py 内 CodeBuddyAgentOptions(mcp_servers={"gbrain": {"type":"http","url":...,"headers":...}})),按需调用 mcp__gbrain__search / mcp__gbrain__query 做语义检索。_MODE_LOCK 的"直接写出回复正文/只输出回复本身/标签外不写任何字"等强约束压制——agent 把工具调用当成"违反约束",跳过 gbrain 直接凭记忆答"印象里 N200 Pro 出厂默认波特率是 115200"。现 _MODE_LOCK 顶部新增「⚠️ 先查再答 · 任务前置步骤」段,明确"调工具是隐式步骤不展示给同事、不违反最终输出格式"+"禁止凭印象答印象里/我记得/大概是";build_knowledge_instruction() 同步改为"事实性问题必须先查 · 不是按需是必须";_looks_like_reply() 加安全网拦"含糊词+技术参数"回复转人工。纯闲聊 / 问候 / 安排 / 表态等无需事实依据时,仍直接基于对话上下文作答,不为查而查。_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 核实。两路都查不到才如实说「这个我得查下资料确认,稍回你」。本地文件是保底、并非禁用(之前"禁止读本地"的过激限制已撤销)。产品资料/ 项目文件/ 文档树保留作保底回退,仍随工作空间维护、不要删。解决什么:同事在钉钉问「源码 / 接口 / 实现细节」类问题(话题名、节点名、参数名、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 实时值,绝不悬空。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 记忆上下文;但工作空间目录需你手动 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 启动,首次运行自己拉 dws 沉淀出 .workbuddy/memory/(长期 MEMORY.md / KNOWLEDGE.md + 每日 YYYY-MM-DD.md)。记忆内容是 agent 跟老板协作中自己学出来的私有上下文(含老板隐私),绝不可随包外发或迁到别的机器;全新机留空,agent 会自行重建。dws patch chmod 授权 chat.message:list / chat.message:send / contact:search;dws 须已在 PATH(本 skill 的 gen_launcher.py 自动追加)。未授权 / 不在 PATH → 监控"活着"却读不到未读。PATH / 会话 / 授权三条的排错详见下文「dws 依赖」段。dingtalk-helper.md 人设里内置的两表结构 + 「老板钉钉身份」是老板专属配置(ROS 软件项目表 <ROS_BASE>/<ROS_TAB>、问题反馈表 <FB_BASE>/<FB_TAB>、老板 userId <BOSS_UID>)。这些真值只在本机 .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,把 <BOSS_UID>/<ROS_BASE> 等占位填成你自己的钉钉身份与表结构
# 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)。
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=<managed codebuddy.cmd>, cwd=<钉钉自动回复工作空间,自动加载该空间记忆>, session_id/resume=dt_<cid>);用户消息走 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 多模态「看图代复一次调用」)。messages 列表必须按时间倒序(newest 在前,主循环已按 _msg_ts 排序+兜底 sorted 二次保护)。
[T0 - REPLY_DELAY_SEC, T0] 内(默认 120s)的对方连发消息物理合并成一条虚拟主消息(拼接格式 [1/N] sender:content1\n[2/N] sender:content2\n...,按时间正序呈现),作为 AI 的【主回复对象】。背景历史 = 窗口外对方消息 + 老板本人历史发言。AI 看到 main_content 直接就是"对方在延时窗口内连发的全部消息",prompt 明确告知"请综合理解后给一条统一回复,覆盖核心诉求,不要漏答、不要逐条复述"。比 prompt 软约束强:物理合并让 AI 没法只挑最新一条答。msgs[0] = 对方最新一条 = AI 唯一要回的内容;msgs[1:] = 背景历史,仅供了解上下文。强约束"只回主消息、不要复述历史话题"。gen_reply 内 _burst_mode 由"窗口内对方消息 ≥ 2 条"判定,日志区分 mode=burst-merged / mode=single-main。fetch_table_context(意图, session_id) 拉「本人(userId=<BOSS_UID>)名下未解决项」拼进 system_prompt(带 300s 缓存,同会话不重复拉)。表 baseId/tableId/字段 ID 全部从 .env 经环境变量注入(<FB_BASE>/<FB_TAB>/<ROS_BASE>/<ROS_TAB> 等),源码零硬编码;不配置则自动退化为纯人设代复。两表在 system_prompt 与回复里明确区分,不混淆。
detect_table_intent(main_content) 校验一次:主消息(含 burst 合并后的全文)无相关意图 → 丢弃 table_context(避免"背景历史含'问题反馈'、主消息是'好的谢谢'"时被背景查表结果污染)。注意:burst 合并模式下,对方延时期内只要有一条含"问题反馈"就会判为 feedback 触发查表——这是合理的,对方确实在问。session_id = "dt_" + <cid>;同一对话首轮传 session_id,后续传 resume=dt_<cid> 续上下文(记忆连续,已实测跨进程 resume 有效)。session 历史里若已回复过某话题,话题红线会禁止 AI 再次主动提及(详见 _MODE_LOCK 末尾)。关键坑(必读):
env["SERVER__PORT"],否则 query() 0 字节超时。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 精确匹配。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」先量长度、别怀疑环境。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-13):常量与文档副本里的代码检索示例已改为中性占位(<某功能包>/<话题A>),不泄露项目细节;本机真实项目案例放私有文件 dws-reply-examples.private.md(已 gitignore、不随包分发),运行时经 runtime.FEW_SHOT_PRIVATE_FILE 由 _load_private_few_shot() 追加注入——真实示例的驱动效果不退化,通用 skill 又不带项目隐私。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() 抽 <reply>、_looks_like_reply() 反拒废话/反问/角色扮演;质量不达标/超时/异常 → 空 → 不代发转人工。
<reply> 标签,输出「完整正文多行 + 结尾一句助理式收尾」("先这样定方向,有问题随时找我。" / "需要我直接通过钉钉发出去吗?")。旧 extract_reply 兜底规则取【最后一行短句】恰好抓到收尾 → 空洞废话代发(实锤:盘锦丽 2 图案例)。修复:_TAIL_SKIP_RE(extract 挑正文行跳过收尾/问句/装饰行,--- 等装饰行剔除)+ _EMPTY_REPLY_RE(质检只拦"整条=空洞收尾",含信息的正常回复如"具体我下午发你。有问题随时找我。"不误伤)。 改这两处正则时用 _validate.py 或本地 10 用例单测回归(空洞收尾拒 / 正文+收尾抽正文 / 正常回复不误伤 / 正常)。_looks_like_reply 规则「回复含'老板'即拒」(假设真人回同事不会写"老板")把同事间正常提及老板全误杀——08-21 蔡达轩「跟老板定的时候好对着看」、朱文杰「今晚来得及给老板汇报」两条正常草稿因此被拦(老板反馈「生成没问题为什么没正常回复」)。实锤:"老板" in text 一刀切是错的,同事聊到"老板"时老板本人回应提"老板"完全正常。修复:只拦「老板+称呼标点」开头(^\s*老板[,,::!!??])——那才是模型对老板说话的角色扮演;指代/转述形态(跟老板定/给老板汇报/老板说的/老板说)全部放行。教训:质检规则的"假设"要拿真实业务场景验证,一刀切关键词拦截必误伤。||DSML||(全角竖线 U+FF5C)泄漏进回复输出,codepoint 级实锤形态:<reply>正文</||DSML||reply>、正文。</||DSML||parameter>、</||DSML||tool_calls>(历史 6 条全为此形态)。⚠️ 前两轮修复(08-14 reply 变体 / 08-19 结尾标签剥除)全部无效——正则要求 < 后紧跟 / 或字母,被 ||DSML|| 中缀骗过;且当时测试用例是手打的 </parameter>,从未测过真实字符(显示层把全角竖线看丢了)。根治(08-20):extract_reply 入口 re.sub(r"[||]{2}\s*DSML\s*[||]{2}", "", t) 直接删协议标记(正文不可能合法含它),使 </||DSML||parameter> 规范化为 </parameter>,前两轮的剥除/拦截防线(_strip_reply_tags 结尾剥 + 质检三道)随即全部生效;_looks_like_reply 再加 DSML 标记兜底拦截(含孤立标记)。回归用例见 _validate.py --test-regex(真实字符用例含全角竖线,自检 U+FF5C,勿改成半角)。血泪教训:①定位乱码必须做 codepoint 级逐字符验证,肉眼/repr 会看丢全角字符;②测试用例必须从真实日志原样复制,手打简化字符=测了个寂寞;③「修复后仍复发」优先怀疑「测的不是真实字符」。send_reply_self + 微信 ClawBot push_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 抢话"。群聊/媒体无文本/生成失败仍走原"不代发转人工"逻辑,不受影响。
问题:早期实现把所有群消息都推到微信——普通群刷屏、@别人的消息、特别关注的人发言全都会打扰老板。老板只关心"有人@我"这类真正重要的群消息。
接口事实(已实测):
list-unread-conversations 和 chat message list 的单条消息对象里没有 @我 结构化字段(atUsers/atDingTalkIds 都不返回);群消息里 @ 某人只表现为内容前缀纯文本 @昵称(如 @同事A 已经试过了)。chat message list-mentions --group <cid>(只返回 @我 的消息),但空群返回无 messages 字段的空结果,结构不稳定,不适合做热路径每轮轮询。方案(内容启发式,零额外 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 生成回复」的效果,但红线是不在群聊替老板发言——所以做成草稿预览:生成后绝不发钉钉群,只推微信给老板看,验证期后再决定是否开放真代发。
机制(复用单聊 PENDING 状态机,零新状态):
GROUP_REPLY_PREVIEW=1 且 群聊 @我(at_me)且有文本(纯图片 @我 保持现有通知,不预览)。REPLY_DELAY_SEC 2min,与单聊一致)→ 窗口内活跃检测(老板在群里则取消)→ 窗口内消息累积为背景历史(_fetch_recent,含老板历史发言标记 is_self)→ 到期 gen_reply 生成(session dt_<cid> 记忆连续,支持查表/知识库/代码检索全链路)→ 只 push_weixin 推草稿(标签「🔔 钉钉群聊·AI 草稿(未发送)」,附群名/@我的人/原文/草稿),绝不调用 send_reply。preview_only=True;分发处 if job.get("preview_only") 走预览分支(微信推送失败复用 _pending_after_send 重试语义)。.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_images 1 次调用)。AUTO_REPLY_IMAGE 开启(默认开) → 把图以 image 协议直接内联进 gen_reply 的 deepseek-v4-flash 调用(识别+代复 1 次调用完成,不再先 describe 再代复的二次调用);失败/质量不达标仍走安全网不代发。单聊不代复(开关关,AUTO_REPLY_IMAGE=0)时则只走 describe_images 1 次拿描述补通知。
gen_reply 的带图提示由 _build_img_hint(n) 生成——动态图片数量 + 顺序说明("附了 N 张图片,按发送先后依次为 图1、图2…;逐张看图 → 对照文字里"这个/那个/如图"等指代判断对应哪张图 → 对不上如实说"这个我得看下图确认下",不硬答")。改提示语改 reply.py::_build_img_hint 一处,勿再写死"一张图片"。降级: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 可关闭)。
[heartbeat] alive, unread_now=N(带 dws 健康标记:empty=真无未读、dws_unhealthy!=dws 挂了),无未读也打——一眼区分「真无消息」vs「dws 坏了伪装健康」;连续 6 次失败(约 12 分钟)自动推一次微信异常提醒(节流 30 分钟)。详见下方「读取日志」。lastMsgCreateAt(媒体消息 openMessageId 可能为空,不能做去重依据)。DRY_RUN=1 环境变量:只生成+打印,不真发回复/不推微信,用于验证。SKIP_SENDERS 可配置不代发的名单(家人/上级)。_looks_like_reply 拒绝)、或媒体消息无文本时,绝不发兜底话术(避免对方以为老板看到了其实没看到),只推微信「需手动处理」转人工。单聊即使质检拦下也会把 AI 生成的草稿原文一并带进微信通知(不代发到钉钉,但让老板看到草稿、便于手动补发)。~/.workbuddy/dingtalk_auto_audit.jsonl(每行一个 JSON:时间、会话、发件人、内容、回复、发送结果)。以本人身份发消息是高风险对外操作,必须可追溯。_dws_ok() 解析 dws 返回的 JSON 看 success/errcode,空输出/含 error/errcode≠0 都判失败;不再用"不含 error 即成功"的弱判据。~/.workbuddy/dingtalk_auto.lock 存 PID(原子 O_CREAT|O_EXCL 创建,避免竞态),启动检测到锁里 PID 还活着则立刻退出(杜绝同条消息双发);锁里是陈旧 PID(进程已死)则接管。dingtalk_auto_debug.log 超 1MB、dingtalk_auto_audit.jsonl 超 512KB 自动重命名为 .1(保留一份回溯),常驻进程下不再无限撑爆磁盘。_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 亦见该段。
脚本启动即做认证健康检查,未通过会打印醒目提示并退出(exit 2),不会悄悄空跑:
.env 填 CODEBUDDY_API_KEY=你的key(申请:https://copilot.tencent.com 控制台)→ 用 Key 直连,不依赖交互登录。codebuddy 登录过的凭据。codebuddy 完成登录;②在 .env 填 CODEBUDDY_API_KEY。~/.codebuddy/local_storage 是否标记 internal,自动注入 CODEBUDDY_INTERNET_ENVIRONMENT=internal,无需手动配。脚本绝不代填/存储你的登录凭据,只检测"是否已登录"并给提醒——认证动作由你本人在终端完成。首次启动前请先确认已登录(终端跑一次
codebuddy或填 Key),未登录会打印提示并以退出码 2 退出,绝不代发。
WorkBuddy 桌面客户端不能作为后台常驻进程;但本脚本是独立 Python 进程,可自行后台常驻轮询。生成回复走 Python SDK(即起即退的子进程),不依赖 WorkBuddy 桌面客户端运行。
启动器 .vbs 不随技能分发(见上方移植说明,已被 .gitignore 排除),由本机 gen_launcher.py 生成到 %APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\。两种落地方式,任选其一(或都做):
# 方式 A:手动生成一次(推荐,部署时执行)
"$PY" ~/.workbuddy/skills/dingtalk-auto-reply/gen_launcher.py
# 方式 B:什么都不做 —— 监控脚本首次运行会自动自检并生成(日志可见 [launcher] 行)
dingtalk_unread_monitor.py 的 python——进程不在立刻拉起;进程在但调试日志超 180s 未更新(卡死/静默空转)则杀掉重拉,~30s 内自愈,无需重登。.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。
nohup python ~/.workbuddy/skills/dingtalk-auto-reply/dingtalk_unread_monitor.py \
> ~/.workbuddy/dingtalk_auto_daemon.log 2>&1 & disown
python dingtalk_unread_monitor.py(前台 python 调试;正式运行由 VBS 启动器以 python.exe 隐藏窗口拉起)。stop_monitor.ps1(精确结束本脚本 python,不动其它 python)。pkill -f dingtalk_unread_monitor.py 按 PID 杀。跨单聊+群聊查找历史消息,可靠手段 = 按人 / 按会话枚举;+search-msg 关键词全量搜索当前不可用(见下证据)。
| 需求 | 命令 | 状态 |
|---|---|---|
| 按人拉消息(含单聊+群聊) | dws chat message list-by-sender --sender-user-id <userId> --start/--end | ✅ 可靠,首选(agent 查同事记录就用它) |
| 枚举全部会话(含单聊+群聊) | dws chat list-all-conversations --limit N | ✅ 可靠(实测 15 个会话) |
| 按会话拉消息 | dws chat message list --group <cid> --time/--direction older | ✅ 可靠 |
| 按关键词全量搜索 | dws chat +search-msg --query "词" --start/--end | ❌ 接口在但当前不命中 |
+search-msg 不可用的验证证据(2026-08-03,已排除授权/数据缺失/时间窗):
list 已拉到)→ count=0count=0;对照 list-by-sender 同条件 → 命中 2 条--verbose 全程无报错;dws chat data-auth cross-org --all --ttl 24h 授权后重测仍 0chat.message:search / chat.message:list scope 授权尝试 → unknown scope(INVALID_SCOPE)+search-msg 做热路径,按人查走 list-by-sender 即可。监控只通过 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 |
SDK 是流式输出——agent 每一步(思考 → 工具调用 → 工具结果 → 最终回复)都能完整看到。 排查「agent 到底调没调 dws / 查到了什么 / 为什么回复不理想」时,不用猜,直接看轨迹:
# 自测时前缀环境变量(监控常驻期间开轨迹需重启监控生效)
DEBUG_AGENT_TRACE=1 <venv_python> _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] <reply>跟进了,7月8号跟同事丙聊过…基本快好了。</reply>
[thinking]=思考过程、[tool_use]=工具名+入参、[tool_result]=工具返回、[result]=最终 <reply>。[tool_use] dws contact… → agent 偷懒没查;有 tool_use 但没 tool_result → dws 命令失败(多半 PATH 问题,见 runtime.build_sdk_env 的 PATH 注入);有 tool_result 但回复没引用 → 口吻/约束问题。DEBUG_AGENT_TRACE=1 开启,用完整绝对路径 venv python 跑才生效。reply.py 的 _FEW_SHOT_EXAMPLES 常量(内联进代码 prompt,2026-08-04 起;dws-reply-examples.md 为随包分发文档副本)每次调用注入 system_prompt,教 agent「提到人名+对接/进展→先调 dws 查记录再答;源码/接口问题→先调 search.py 查代码再答;回复像老板真人、不暴露查询动作」。代码检索示例已去项目化(中性占位),真实项目案例走私有文件 dws-reply-examples.private.md(FEW_SHOT_PRIVATE_FILE,运行时追加)。_validate.py7 种模式(参数互斥,按需选一):
--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 命令形态。--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
⚠️ 会真实以本人身份发消息,仅在你确认"确有漏发"时使用;群聊不代发(只通知),符合全局策略。
chat.message:list、chat.message:send 与 contact:search(后者用于获取本人 openid 以精确识别「回复自己」;用 dws patch chmod 授权)。
codebuddy_code_path 指向其 codebuddy.cmd,SDK 内部 spawn 它来跑 agent;模型 deepseek-v4-flash。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 直接返回空(不代发),不会崩溃。dingtalk-helper.md(全局 ~/.codebuddy/agents/,不随 skill 打包);skill 内 dingtalk-helper-backup.md 是其干净部署模板(占位,无私人数据;随 skill 走、换机兜底)。改人设只改 dingtalk-helper.md 一处。.env,可选但推荐)技能目录自带 .env.example,移植到新机:cp .env.example .env 后按需填写。所有项均可选,留空=自动探测/默认。已存在的系统环境变量不被 .env 覆盖。
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 侧视觉模型)、各二进制路径。全在 .env.example 有注释。