# AGENTS.md 给在本仓库开发的 agent。先读 [README.md](README.md),再改代码。 ## 这是什么 DeepSeek Harness 的 Cordis 插件:**除了子代理以外,任何会话停下来就把消息推到配置好的消息平台**(QQ / Telegram / 飞书 / 微信)。范围仅限推送;不做审批决策、不在聊天里批复、不把入站注入 agent 会话。 Host API 以 DSH 源码为准:`/home/alec/deepseek-harness/`。 ## 硬约束 - **改名必须四同步**:`package.json` 的 `name`、`cordis.patch.yml` 的 `name`、`src/util.mjs` 的 `NAME`(→ `src/index.mjs` 的 `export const name`)、`client.js` 的 `__ModuleLoader__.load({ id })` + `exports.name`。漏一处会出现 `loaded without registering "..."`。 - RPC 路径 `/api/dsh-message-push` 与 `client.js` 的 `rpc.call('/api', 'dsh-message-push', …)` 必须成对;`tests/exports.test.mjs` 守着这两条。 - 命名导出 `name` / `inject` / `apply`,**禁止 default export**。 - **服务留在 host plane**:`ctx.provide('messagePush', …)` 被跨插件消费,因此只能走 profile bundle 行,不能搬进 agent preset(第二个会话会因同名服务注册直接抛错)。 - 纯 JS:无 TS / JSX / import 变换;Client React 用 `createElement`。**不 import 任何 `@deepseek-ai/*` 包**(只用注入名 `timer` 与运行时的 `fetch` / `WebSocket`),`tests/exports.test.mjs` 会扫源码。 - 只摘叶子字段:禁止 `JSON.stringify` / 深拷贝 live 的 session / agent / event。会话侧只存 `id`、`header.cwd`、`header.origin`、`header.delegationDepth`、`sessionTitle.get()` 的 title、`turn/end` 的 reason 叶子字段、assistant 文本块。 - 事件回调里**不 await 网络发送**:`approval/request`、`user-questions/request`、`agent/status` 的回调必须同步 `return next()` 或立刻返回,推送走 fire-and-forget。 - 所有副作用进 Fiber disposer:渠道连接、`ctx.timer.interval` 兜底、`ctx.timer.timeout`、RPC 注册(`c.effect(() => connection.fetch.register(...))`)、扫码 provisioning 的 abort。 - 凭据只存在用户 `~/.dsh/message-push/`(`0600`),禁止提交;快照只回掩码。 - 配置/凭据**损坏时用内存默认且绝不写盘**,写操作返回明确错误;只有显式 `overwriteCorrupt` 才覆盖。 - 触发面只用实证过的 DSH 事件:`agent/status`(`idle`)、`session/event`(`turn/end` / `session/title` / `assistant/message`)、`approval/request`、`user-questions/request`、`session/disposed`。 - 文案:推送正文与审计日志为中文(`src/format.mjs` 内按 `lang` 支持 en);Client 设置页走 `locales.mjs` 的 zh/en,经 `ctx.locale.register`。 ## 结构 ``` src/index.mjs 宿主:事件接线、渠道装配、扫码、RPC、provide('messagePush') src/watcher.mjs 停下观察器:去重键、窗口合并、原因兜底(tick) src/format.mjs 正文渲染与叶子字段提取(纯函数) src/service.mjs 对外服务:broadcast / send / notify / onInbound / setWatcher src/inbound.mjs 入站:绑定目标 + 回执(BIND_HINT_INTERVAL_MS 限频) src/replies.mjs 入站文本判定(纯函数) src/store.mjs normalizeConfig / mergeConfig / createStore src/audit.mjs 审计 + 限频 WARN src/channels/ hub / meta / qqbot / telegram / feishu / wechat src/provisioning.mjs QQ 官方扫码创建机器人 client.js 设置页(settings.section,鉴权 RPC) locales.mjs Client zh/en 字典(键集以 zh 为准) tests/ node:test,离线;不连平台 ``` `inject: ['timer']`;`connection` / `tools` 用 `ctx.inject([...], cb)` + `ctx.get` 可选消费,缺了只降级不阻塞。 ## 判定语义(改动前先读) - 去重键:`turn-end::` 与 `pending::approval|question`、`external::`。 - `turn/end` 可能晚于 `agent/status(idle)`:先 hold,`reasonUnknownDelayMs` 内等原因;原因落地为「已配置不推」则丢弃。 - 已发出的「待审批/待回答」会折叠同段停顿的一次 `idle`(`stopFolded`),之后重复 `idle` 静默。 - 窗口内重复只在计数变化时补发一条(`emittedCoalesce`),换去重键才重置计数。 - 子代理默认跳过;`notify.events.subagents = true` 才推。 ## 开发命令 ```sh npm test npm run check ```