# AGENT.md — dsh-plugin-todo-scanner > 面向后续开发/维护本仓库的 AI Agent 与人类开发者:仓库是什么、怎么组织、核心不可违背的约束、常见改动改哪里。先读本文件,再读 [PRD.md](./PRD.md)(功能需求与验收口径的权威来源),两者冲突时以**代码现状**为准并回改文档。 --- ## 1. 仓库是什么 `dsh-plugin-todo-scanner` 是运行在 **DeepSeek-Harness(Cordis 组合)** 里的 TODO 代码扫描插件(版本 1.0.0,PRD V1.0 功能已按代码现状全部实现): - **扫描侧(Host,Node.js)**:递归扫描本地项目目录,提取 `TODO / FIXME / HACK / NOTE / XXX / BUG / OPTIMIZE / REVIEW` 标记(`//`、`#`、`/* */`、``、JSDoc `*`、SQL `--`),输出结构化条目(内容、文件、行号、代码行、前后 2 行上下文、语言、出现次数)。 - **使用侧**:给大模型注册 8 个工具(`todo_scan` / `todo_list` / `todo_get` / `todo_update_status` / `todo_batch_update` / `todo_export` / `todo_stats` / `todo_clear`),支持状态管理、统计报告、Markdown 导出。 - **展示侧(Client,浏览器)**:侧边栏「TODO 雷达」面板(统计卡片 + 类型分布 + 可筛选/排序清单 + 操作按钮),通过**本地 HTTP 数据服务**(默认 `127.0.0.1:18766`)轮询取数。 一句话定位(PRD §1.3):**插件负责本地只读扫描与标记提取;语义分类、优先级评估等交给大模型**。插件不做语义分析、不自动修复、不集成 git(PRD §2.3)。 **关键交付边界**:状态只存内存、按会话隔离、卸载即清空(无任何持久化与残留)。 --- ## 2. 当前状态与文档差异(务必先知道) - 按仓库代码,PRD V1.0 的功能(8 个工具、HTTP 服务、侧边面板、忽略规则、去重合并、会话隔离、卸载清理)**均已实现**。PRD 头部「状态:开发中」与 §7 手工用例未勾选属于文档滞后,不代表代码缺失。 - PRD §9 交付物清单中的 `docs/api.md`、`README.md`、`CHANGELOG.md`、`LICENSE` **已补齐**(2026-09 评审批次);`tests/` 已有端到端冒烟(`tests/smoke.test.mjs`)与 matcher/ignore-rules/safety 单测(见 §3 / §11)。若任务涉及缺失文档,先确认是否需要补齐而非假设存在。 - PRD V1.1(§2.2)项——优先级评估、自动分类、负责人识别、git blame 过期检测、增量扫描、文件监听、飞书/GitHub 导出、趋势图、修复建议——均未实现,属后续迭代。 - 实现相对 PRD 的补充:面板数据走**本地 HTTP 服务 + 轮询**(新增 `server` 配置段,PRD §3.3 无此项);Host 额外监听 `session/disposed` 做会话级清理;面板展示会话选择器/审计(各会话最近 20 条)等增强。客户端侧另有**可选 remotes 推送**通路(`todo-scanner/state` 事件),默认不启用,面板靠轮询即零集成可用(见 `src/client/index.tsx` 头注释)。 --- ## 3. 技术栈与命令 - Node >= 20,ESM(`"type": "module"`),TypeScript `strict` 模式(另开 `noUncheckedIndexedAccess` 等)。 - 依赖:`@deepseek-ai/cordis`(插件框架)、`@deepseek-ai/schemastery`(配置 schema)、`@deepseek-ai/dsh-tools`(工具注册 `defineTool`)。 - 源码内相对导入**带 `.ts` 扩展名**(`allowImportingTsExtensions` + `rewriteRelativeImportExtensions`,tsc 产物改写为 `.js`);新增文件请沿用此风格。 ```bash npm install npm run typecheck # tsc --noEmit(tsconfig 排除了 src/client,客户端不在 tsc 范围内) npm run build # tsc 产物输出到 lib/(Host 侧入口 lib/index.js) npm test # build + node --test tests/{smoke,matcher,ignore-rules,safety}.test.mjs(冒烟 + 单测,见 §11;该 Node 版本把目录参数当模块入口,故逐文件列出) ``` - **包管理器固定为 npm,禁止 pnpm/yarn**:改依赖统一 `npm install `。混用 pnpm 会再生成第二个锁文件,并靠「自动装 peer」悄悄补上 npm 不会装的宿主包——一旦删掉重装,`ctx.sessions`、`session/disposed` 等类型增强与运行时依赖就会丢失(本轮迁移已踩过)。`.npmrc` 已固定 `legacy-peer-deps=true`(npm 10 解析 `tsdown` 的通配 optional peer 会崩,见 .npmrc 注释);因此类型/运行时用到的 dsh 宿主包(`@deepseek-ai/dsh-session` / `dsh-llm` / `dsh-scope` / `dsh-invariants` 等)必须显式列入 `devDependencies`,不要指望 npm 自动装 peer。 - 客户端(`src/client/*.tsx`)是 React/TSX,需经 Harness 客户端构建链(tsdown 打包)而非 tsc,`package.json` 的 `./client` 导出与 `dsh.client` 元数据负责衔接;改面板后需在 Harness 客户端重新构建/刷新验证。 - 发布形态:`cordis.patch.yml` 作为插件行(`dsh.bundle.patch` 引用),可通过 `name: dsh-plugin-todo-scanner`(已安装模块)或本地路径挂载,也可 `- import:` 进宿主 cordis.yml(见 `cordis.patch.yml` 头注释)。 --- ## 4. 目录结构 ``` AGENT.md / PRD.md / package.json / tsconfig.json / cordis.patch.yml(config 段默认值) src/ index.ts 插件入口(装配层):apply(store → 面板 HTTP 服务 → 工具注册 → runScan 仲裁与 hooks → 会话销毁/卸载清理、emit 'todo-scanner/state') config.ts 配置 schema:Config(schemastery)+ validateMarkerTypes(随 cordis.patch.yml config 段维护) tools.ts 8 个工具注册(defineTool + JSON Schema / render;依赖经 registerTodoTools 的 TodoToolDeps 注入) types.ts 共享类型与常量(TodoItem/ScanStats/SessionState/PluginConfig/TodoScanOutput…) scanner.ts 扫描引擎:第一遍收集候选文件 → 第二遍逐文件扫描(大小/二进制/编码/逐行匹配/块注释续读) matcher.ts 标记匹配器(纯函数):注释符号 sigil 规则 + matchLine/completeBlockContent/normalizeContent ignore-rules.ts 忽略规则:默认目录/扩展名 + .gitignore 解析(git 语义子集)+ buildIgnoreMatcher language-map.ts 文件名/扩展名 → 语言推断(未知回退 "text") todo-store.ts 会话级内存存储:身份键合并保留状态、筛选排序、审计日志、批量/全部/清空 stats.ts 统计:computeScanStats / refreshStatusStats / summarizeScan / STATUS_LABELS exporter.ts Markdown 导出(by_type 默认 / by_file) safety.ts 安全边界:系统目录黑名单 + resolveScanRoot(白名单/cwd 校验) server.ts 面板数据服务:127.0.0.1 本地 HTTP + REST 路由 + 端口冲突自增重试 client/ index.tsx 客户端插件入口:注入 settings.section slot(TODO 雷达分区)+ zh/en 词典 Panel.tsx 「TODO 雷达」面板组件(轮询本地 HTTP) locales.ts 面板文案词典(zh/en,key 类型 TodoScannerKey) lib/ 构建产物(.gitignore 忽略) tests/ package.json 脚本引用但当前未提交 ``` **模块依赖方向**:`index.ts` 装配全部;`tools.ts` → `config/exporter/stats/todo-store/types`(依赖注入,不反向 import index);`config.ts` → `ignore-rules/types`;`scanner.ts` → `matcher/ignore-rules/language-map/stats/types`;`todo-store.ts` → `stats/types`;`exporter.ts` → `stats/types`;`server.ts` 只依赖 `types` + 宿主 hooks 回调面;`client/*` 与 Host 侧**无共享代码**(各自独立类型),仅通过 HTTP JSON 契约通信。 --- ## 5. 数据流(两条并行通路) ``` 通路 A(模型工具): 大模型 → todo_scan/... → tools.ts execute()(deps.runScan) → sessionId = exec.agent.id(会话隔离键;cwd 取 exec.agent.session.cwd) → runScan(): resolveScanRoot(安全校验) → loadGitignore → scanDirectory() → TodoStore.setScanResult(身份键合并保留旧状态) → 返回 items/stats/summary 通路 B(侧边面板): 浏览器 Panel.tsx(每 2s 轮询) → http://127.0.0.1:18766/todo-scanner/api/*(REST,见 server.ts 头注释) → TodoServerHooks 回调(index.ts 内实现)→ TodoStore (可选增强:宿主把 'todo-scanner/state' 事件加入 remotes 转发后可改推送,默认不启用) ``` - 两个入口共享同一 `TodoStore` 与 `runScan`,HTTP 路径的 `session` 参数缺省时按 `lastScanTime` 选最近活跃会话。 - `TodoStore` 是 Host 内存单例(`Map`),按 `exec.agent.id` / HTTP session 参数隔离。 - **无跨进程共享、无数据库**:Harness 重启即全部清空——这是设计而非缺陷(PRD §4.1「仅内存保存」)。 --- ## 6. 核心不可违背的约束(改代码前必读) 1. **扫描只读**:不改任何磁盘文件、不执行 shell、不跟 git 交互(PRD §6.1 / §2.3)。扫描入口新增能力时不得破坏这一点。 2. **安全边界三层**(`src/safety.ts` + `scanner.ts`):① 系统目录黑名单;② 根目录必须在 `allowedRoots` 白名单内(为空时仅允许会话 cwd 及子目录);③ `maxFileSize`(默认 1MB)与 `maxFiles`(默认 5000)上限 + `AbortSignal` 可取消。任何放宽都属安全回归。 3. **会话隔离**:工具侧 sessionId 一律来自 `requireSessionId(exec)`(无 `exec.agent` 直接报错),不要改走全局/单例;HTTP 侧按 `resolveSession` 解析。 4. **状态保留语义**(`todo-store.ts` 身份键):重扫时按「同文件+同类型+同内容」合并,旧条目保留 status/时间戳、只刷新行号/代码/上下文/occurrences;空内容占位(`(无描述)`)则退化为「同文件+同类型+同行号」逐行独立。改 `identityKeyOf` 必须保持该语义。 5. **清理无残留**:卸载清理(`ctx.effect(..., 'todo-scanner.cleanup')`)要 abort 所有扫描控制器、关闭 HTTP 服务、`store.clearAll()`;`session/disposed` 时 abort + `resetSession`。新增的定时器/监听/资源必须挂到 ctx 生命周期(`ctx.effect`/`ctx.on`)内。 6. **并发仲裁**:同一会话新扫描会 abort 上一次(`scanControllers`);结果落地前用 `isCurrent()` 校验「最新扫描」归属,过期结果丢弃(返回 `superseded`)。 7. **数据契约同步**:Host 侧 JSON Schema(tools.ts 内 `TODO_ITEM_SCHEMA` / `SCAN_STATS_SCHEMA`)、`server.ts` REST 路由、`Panel.tsx` 的类型与 `DEFAULT_PORT`、`cordis.patch.yml`/`config.ts Config` 四处需保持一致;尤其面板端口须与配置 `server.port` 同步(默认 18766)。 8. **纯函数优先**:`matcher.ts` / `ignore-rules.ts` / `stats.ts` / `exporter.ts` / `safety.ts` 均为纯逻辑、无 IO(便于单测),新逻辑尽量落在这里。 --- ## 7. 关键实现机制速查 ### 7.1 扫描(scanner.ts) - 两遍式:第一遍 `collectFiles` 递归遍历(忽略规则过滤、symlink 一律跳过、目录读失败静默跳过、超 `maxFiles` 截断);第二遍逐文件:`stat` 大小上限 → `readFile` → 首 8KB 含 NUL 判二进制 → UTF-8 按行匹配 → 进度回调。 - 每行每类型只记第一次命中;非占位内容按「file+type+content」跨行合并计 `occurrences`。 - 文件读取失败/超大/二进制 → 计入 `skippedFiles` 与 `meta.warnings`,不中断整体扫描(PRD §6.2)。 ### 7.2 匹配(matcher.ts) - sigil 规则:`//`(避开 `://`)、`#`(避开 shebang/`#field`)、`/* */`、``、`*` JSDoc 续行、`--` SQL;标记类型正则大小写不敏感(默认)、锚定在注释符后。 - 未闭合块注释经 `completeBlockContent` 向后最多续读 3 行直至闭合串;空内容归一为 `(无描述)`。 ### 7.3 忽略规则(ignore-rules.ts) - 三层:目录名 → 扩展名/后缀(如 `.min.js`、`.map`)→ 根 `.gitignore`(默认开)。gitignore 实现子集:注释、`!` 取反、尾 `/` 目录限定、首 `/` 锚定、`*`/`**`/`?`、无斜杠模式匹配任意层级、最后命中生效。 ### 7.4 统计(stats.ts) - `computeScanStats`(扫描时刻全为 pending)与 `refreshStatusStats`(状态变更后按当前清单重算 `byStatus`)分工;`summarizeScan` 产出聊天流摘要文本。 ### 7.5 导出(exporter.ts) - `by_type`(默认,按类型分组 + 末尾附「按文件分组」)与 `by_file`;`done` → `[x]`,其余 `[ ]`;`ignored` 一律不导出,`includeDone=false` 时滤掉 done。 ### 7.6 HTTP 服务(server.ts) - 仅绑定 `127.0.0.1`,端口被占自增 +1(最多 5 次);请求体上限 1MB;CORS 放开(Electron/file:// 客户端可访问)。路由清单见文件头注释:`/health`、`/api/sessions`、`/api/state`、`/api/scan`、`/api/update-status`、`/api/batch-update`、`/api/mark-all-done`、`/api/export`、`/api/clear`、`/api/audit` 及根状态页。 - 面板 `getState` 返回条目上限 1000(截断标记 `itemsTruncated`)、审计最近 20 条。 ### 7.7 事件 - 主动 emit:`todo-scanner/state`(扫描/状态/清空变更通知,Host 内声明于 `index.ts` module augmentation)。 - 监听:`session/disposed`(会话清理);卸载走 `ctx.effect` cleanup(等价 PRD §5.2 的 `plugin:unload` 语义)。 - 不监听 `user:message` / `turn:*` / `tool:*` 等会话事件(纯工具型)。 --- ## 8. 配置(cordis.patch.yml config 段 ↔ src/config.ts `Config`,PRD §3.3) | key | 默认 | 说明 | |---|---|---| | `enable` | `true` | 总开关(false 时 apply 直接返回) | | `scan.allowedRoots` | `[]` | 根目录白名单;空 = 仅会话 cwd | | `scan.ignoreDirs` | node_modules/dist/build/.git/… | 忽略目录名 | | `scan.ignoreExtensions` | .min.js/.map/.png/… | 忽略扩展名/后缀 | | `scan.useGitignore` | `true` | 应用根 .gitignore | | `scan.maxFileSize` | `1048576` | 单文件上限,超出跳过 | | `scan.maxFiles` | `5000` | 单次扫描文件上限,超出截断 | | `markers.types` | 8 种内置类型 | 可扩展(正则锚定于注释符后) | | `markers.caseSensitive` | `false` | 大小写不敏感匹配 | | `export.defaultFormat` | `by_type` | by_type / by_file | | `ui.showIgnored` | `false` | 面板是否显示 ignored | | `server.enable` / `server.port` | `true` / `18766` | 面板数据服务(PRD 补充项) | 改配置默认值需同步:`cordis.patch.yml` config 段、`src/config.ts` Config schema、必要时 `DEFAULT_IGNORE_DIRS/EXTENSIONS`(ignore-rules.ts)、`DEFAULT_MARKER_TYPES`(types.ts)。 --- ## 9. 工具一览(src/tools.ts,PRD §5.1) | 工具 | 入参(要点) | 行为 | |---|---|---| | `todo_scan` | `rootPath?`, `types?` | 触发扫描,返回 items/stats/summary(含 truncated/cancelled/warnings/added/preserved) | | `todo_list` | `filter{type,status,filePath,keyword}?`, `sortBy?`, `includeIgnored?` | 查询清单(默认含 ignored;keyword 匹配内容/代码行/路径) | | `todo_get` | `id` | 单条详情(含上下文) | | `todo_update_status` | `id`, `status`(pending/in_progress/done/ignored) | 单条改状态,写审计 | | `todo_batch_update` | `ids[]`, `status` | 批量改状态,返回 updatedCount/missing | | `todo_export` | `format?`, `includeDone?` | Markdown 导出(内容直出到聊天流) | | `todo_stats` | — | 统计(scanned 标记是否已扫描) | | `todo_clear` | — | 清空当前会话清单(不影响磁盘) | 新增工具:在 `src/tools.ts` 的 `registerTodoTools` 内用 `ctx.tools.register(defineTool({...}))` 追加(运行时依赖从 `TodoToolDeps` 取,勿反向 import index.ts),schema 务必 `additionalProperties:false` + 明确 required/enum;如需面板可见,同步在 `server.ts` 路由 + `TodoServerHooks` + 面板侧实现。 --- ## 10. 常见改动指南(对应改哪里) | 想做什么 | 改哪里 | |---|---| | 新增标记类型 | `cordis.patch.yml markers.types`(无需改代码);默认集在 `types.ts DEFAULT_MARKER_TYPES` | | 新增注释格式/sigil | `matcher.ts SIGIL_RULES`(注意与现有规则避让,如 `//` 与 `/*`) | | 调整默认忽略目录/扩展名 | `ignore-rules.ts DEFAULT_IGNORE_DIRS/EXTENSIONS` | | 完善 .gitignore 语义 | `ignore-rules.ts parseGitignore/globToRegex` | | 新增语言映射 | `language-map.ts`(先名后扩展名,回退 "text") | | 改扫描流程/上限/进度 | `scanner.ts` + 对应 Config | | 改状态保留/查询语义 | `todo-store.ts`(勿破身份键语义) | | 改导出格式 | `exporter.ts` | | 加统计维度/摘要文案 | `stats.ts` + `ScanStats` 类型 + 相关 JSON Schema | | 改安全边界 | `safety.ts`(务必谨慎,见 §6.2) | | 新增面板接口 | `server.ts` 路由 + `index.ts TodoServerHooks` 实现 + `Panel.tsx` 调用 | | 改面板 UI/文案 | `Panel.tsx` + `locales.ts`(zh/en 成对加 key) | | 行为/契约对齐 | 牢记 §6.7 四处契约同步 | --- ## 11. 测试与验证提示 - `tests/smoke.test.mjs`:端到端冒烟回归(node:test)。mock 最小 cordis ctx 加载 `lib/index.js`,真实扫描临时 fixture(多语言注释 / 跨行块注释 / 无描述占位 / 大小写 / https 不误判 / node_modules、.gitignore、二进制忽略 / 系统目录拒绝),覆盖 8 个工具、状态与导出、本地 HTTP 面板服务与卸载清理。`npm test` 触发;在受管道限制的沙箱里可用 `node tests/smoke.test.mjs` 直接运行。 - `tests/matcher.test.mjs` / `tests/ignore-rules.test.mjs` / `tests/safety.test.mjs`:纯逻辑单测(import `lib/*.js` 构建产物,先 build)。改这些模块时同步补/改用例——单测曾抓到 `isSystemPath` 对 `C:\Windows\System32` 深层路径漏网的真实缺陷。 - 为 stats/exporter/todo-store 补单测仍值得(当前仅冒烟层 + 上述三模块)。 - 手动验证参照 PRD §7(17 个用例)与 §6.2 异常场景。 - 面板验证需在 Harness 客户端运行插件并刷新页面;可用浏览器打开 `http://127.0.0.1:18766/todo-scanner/` 状态页调试服务端。 - Windows 注意:相对路径一律正斜杠存储/比较(`toRelativePath` 等已处理);黑名单含盘符根与 `C:\Windows` 等。 --- ## 12. 快速定位索引(PRD ↔ 代码) | 主题 | PRD | 代码 | |---|---|---| | 多语言扫描/上下文/去重 | §3.2 功能1、3 | scanner.ts + matcher.ts | | 忽略规则/.gitignore | §3.2 功能2 | ignore-rules.ts | | 数据结构 | §3.2 功能4 / §4.1 | types.ts | | 状态管理/审计 | §3.2 功能5 | todo-store.ts | | 侧边面板 | §3.2 功能6 / §3.4 | server.ts + client/* | | Markdown 导出 | §3.2 功能7 | exporter.ts | | 统计报告 | §3.2 功能8 | stats.ts | | 配置 | §3.3 | cordis.patch.yml + config.ts `Config` | | 生命周期 | §4.2 | index.ts(apply/effect/disposed) | | 工具清单 | §5.1 | tools.ts `registerTodoTools`(index.ts 装配) | | 安全约束/异常 | §6.1 / §6.2 | safety.ts + scanner.ts | 遇到「PRD 写了但没有对应代码」的条目:先按 §2 判断是否为 V1.1 迭代项或文档滞后项,再决定补实现还是改文档,不要盲写功能。