🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

双智能体协作 wuuuuu123-32/dsh-tandem

DeepSeek Harness
wuuuuu123-3211 天前有提交已提供中文文档扫描:含敏感能力

把独立子任务并行派给本地 Codex 执行并机械验收

30.7/100
综合分
1
GitHub 星标
—
npm 周下载
—/5
用户评分

数据截至 2026/10/21 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add wuuuuu123-32/dsh-tandem
该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:已验证
是什么
dsh 原生插件 · dev
装得上吗
本站已在 dsh 0.2.1-rc.2 下真实安装成功
非静态推断
L4 · 真实安装等级说明 →
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 11 天前
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →
社区实装还没有人打卡 · 装过的第一个就是你
安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。

可执行外部命令读写/删除本地文件
查看扫描证据(共 54 处,展示前 52 条)
  • 写入本地文件
    index.js:92
    mkdirSync(join(homedir(), '.dsh', 'tandem-state'), { recursive: true });
  • 写入本地文件
    index.js:93
    appendFileSync(PROBE_FILE, new Date().toISOString() + ' ' + msg + '\n');
  • 读取本地文件
    index.js:132
    if (!existsSync(path)) return {};
  • 读取本地文件
    index.js:133
    return JSON.parse(readFileSync(path, 'utf8'));
  • 读取本地文件
    index.js:217
    if (!existsSync(cfg.configFile)) {
  • 读取本地文件
    index.js:680
    if (!existsSync(targetDir)) {
  • 读取本地文件
    lib/delegate.js:73
    const raw = readFileSync(logPath, 'utf8');
  • 执行外部命令固定命令
    lib/delegate.js:143
    const k = spawn('taskkill', ['/T', '/F', '/PID', String(pid)], { stdio: 'ignore' });
  • 写入本地文件
    lib/delegate.js:177
    mkdirSync(dir, { recursive: true });
  • 执行外部命令来自配置/变量
    lib/delegate.js:184
    child = spawn(config.codexCommand, args, {
  • 读取本地文件
    lib/delegate.js:201
    try { tail = readFileSync(logPath, 'utf8').slice(-MAX_LOG_TAIL); } catch {}
  • 写入本地文件
    lib/consult.js:183
    try { appendFileSync(file, JSON.stringify(record) + '\n', 'utf8'); } catch { }
  • 读取本地文件
    lib/consult.js:189
    if (!existsSync(file)) return null;
  • 读取本地文件
    lib/consult.js:191
    try { text = readFileSync(file, 'utf8'); } catch { return null; }
  • 写入本地文件
    lib/consult.js:205
    mkdirSync(dir, { recursive: true });
  • 写入本地文件
    lib/consult.js:207
    writeFileSync(schemaPath, JSON.stringify(SCHEMA, null, 2), 'utf8');
  • 写入本地文件
    lib/consult.js:210
    writeFileSync(promptPath, prompt, 'utf8');
  • 读取本地文件
    lib/consult.js:219
    if (existsSync(want)) targetCwd = want;
  • 执行外部命令来自配置/变量
    lib/consult.js:254
    child = spawn(config.codexCommand, args, {
  • 读取本地文件
    lib/consult.js:268
    try { text = readFileSync(logPath, 'utf8'); } catch {}
  • 执行外部命令固定命令
    lib/acceptance.js:86
    spawn('taskkill', ['/PID', String(pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore' });
  • 执行外部命令来自配置/变量
    lib/acceptance.js:113
    child = spawn(check.cmd, { cwd: dir, shell: true, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] });
  • 读取本地文件
    lib/tickets.js:21
    if (!existsSync(file)) return {};
  • 读取本地文件
    lib/tickets.js:22
    const data = JSON.parse(readFileSync(file, 'utf8'));
  • 写入本地文件
    lib/tickets.js:31
    mkdirSync(stateDir, { recursive: true });
  • 写入本地文件
    lib/tickets.js:33
    writeFileSync(tmp, JSON.stringify(tickets, null, 2));
  • 写入本地文件
    lib/tickets.js:34
    renameSync(tmp, file);
  • 读取本地文件
    lib/reviews.js:64
    names = readdirSync(dir, { withFileTypes: true });
  • 读取本地文件
    lib/reviews.js:80
    const st = statSync(full);
  • 读取本地文件
    lib/reviews.js:85
    entries.push(`${rel}|h:${createHash('sha1').update(readFileSync(full)).digest('hex').slice(0, 12)}`);
  • 读取本地文件
    lib/reviews.js:95
    const st = statSync(p);
  • 写入本地文件
    lib/reviews.js:106
    mkdirSync(stateDir, { recursive: true });
  • 写入本地文件
    lib/reviews.js:107
    appendFileSync(reviewsFile(stateDir), JSON.stringify(review) + '\n');
  • 读取本地文件
    lib/reviews.js:115
    if (!existsSync(file)) return [];
  • 读取本地文件
    lib/reviews.js:117
    for (const line of readFileSync(file, 'utf8').split('\n')) {
  • 写入本地文件
    lib/reviews.js:171
    writeFileSync(tmp, next.map(r => JSON.stringify(r)).join('\n') + '\n');
  • 写入本地文件
    lib/reviews.js:172
    renameSync(tmp, file);
  • 读取本地文件
    lib/ledger.js:152
    const st = statSync(reviewsFile(stateDir));
  • 写入本地文件
    lib/ledger.js:185
    writeFileSync(tmp, rows.map((r) => JSON.stringify({ path: r.path, t: r.t })).join('\n') + '\n');
  • 写入本地文件
    lib/ledger.js:186
    renameSync(tmp, file);
  • 读取本地文件
    lib/ledger.js:199
    if (!existsSync(file)) return;
  • 读取本地文件
    lib/ledger.js:200
    text = readFileSync(file, 'utf8');
  • 读取本地文件
    lib/ledger.js:224
    existed = statSync(abs).isFile();
  • 读取本地文件
    lib/ledger.js:239
    existed = statSync(abs).isFile();
  • 写入本地文件
    lib/ledger.js:249
    mkdirSync(stateDir, { recursive: true });
  • 写入本地文件
    lib/ledger.js:250
    appendFileSync(ledgerFile(stateDir), JSON.stringify({ path: rec.path, t: rec.t }) + '\n');
  • 读取本地文件
    lib/ledger.js:266
    const st = statSync(info.path);
  • 写入本地文件
    lib/ledger.js:321
    mkdirSync(stateDir, { recursive: true });
  • 写入本地文件
    lib/ledger.js:322
    appendFileSync(
  • 读取本地文件
    lib/env.js:20
    if (!existsSync(p)) return null;
  • 读取本地文件
    lib/env.js:22
    try { text = readFileSync(p, 'utf8'); } catch { return null; }
  • 读取本地文件
    lib/codex-path.js:24
    if (existsSync(c)) return c;
扫描于 2026/10/31(1 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

双 agent 协作 DSH 插件:主 agent 负责跟你沟通与决策,本地 Codex(GPT) 负责并行执行与方案会审。任务缺验收标准直接拒绝派发。

README

由 DeepSeek 最新模型翻译生成
dsh-tandem 让 DSH 主智能体与本地 Codex(GPT)形成双智能体协作:主智能体负责跟你沟通、拆任务、做最终决定;Codex 负责并行执行、方案会审,以及按机械判据返工。 机制由插件保证,不靠智能体「记得」——参数不合法的派活会被当场拒绝。 五个工具 | 工具 | 作用 | 关键约束 | | tandem_status | 查看当前是否可用:Codex 路径、模型、并发上限、允许派活的目录根、凭据是否就绪 | 第一次派活前、或派活报错后先看它 | | tandem_delegate | 把 N 个互不依赖的子任务并行交给 Codex;成功的任务返回 taskId | 每项必须给 timeoutMs 和 acceptance(怎么算干完了),缺失直接拒绝;dir 必须在白名单根内且互不重复 | | tandem_consult | 让 Codex 作为「第二个有边界的判断者」回答一个问题;成功时签发 reviewId(会审标记) | 只判断、不写文件;必须回「结论 / 证据 / 反例 / 验证方式」四段,缺段即判无效 |
展开完整文档(README 剩余 1 节)
dsh-tandem

让 DSH 主智能体与本地 Codex(GPT)形成双智能体协作:主智能体负责跟你沟通、拆任务、做最终决定;Codex 负责并行执行、方案会审,以及按机械判据返工。

机制由插件保证,不靠智能体「记得」——参数不合法的派活会被当场拒绝。

五个工具

| 工具 | 作用 | 关键约束 |
| tandem_status | 查看当前是否可用:Codex 路径、模型、并发上限、允许派活的目录根、凭据是否就绪 | 第一次派活前、或派活报错后先看它 |
| tandem_delegate | 把 N 个互不依赖的子任务并行交给 Codex;成功的任务返回 taskId | 每项必须给 timeoutMs 和 acceptance(怎么算干完了),缺失直接拒绝;dir 必须在白名单根内且互不重复 |
| tandem_consult | 让 Codex 作为「第二个有边界的判断者」回答一个问题;成功时签发 reviewId(会审标记) | 只判断、不写文件;必须回「结论 / 证据 / 反例 / 验证方式」四段,缺段即判无效 |
| tandem_check | 对派活的产物跑机械验收:在任务目录执行命令,按退出码 + 输出包含判定;返回 reviewed / stale 与 review 标记 | 判据在首次检查时冻结(同一 taskId 改判据会被拒);被审内容在会审后变了则 stale: true;只做门禁,不替代语义审阅 |
| tandem_reject | 把验收缺口原样回灌给 Codex 重做(同一 taskId) | 最多 2 轮;要求最近一次检查确实失败;到上限把人拉回来决定 |

什么时候该用

delegate(并行执行)——同时满足才派:
- 有 ≥2 个产物,各自能独立验收;
- 写入路径不重叠、彼此不依赖;
- 都不要本对话的上下文。

check / reject(验收闭环)——delegate 成功后:
- tandem_check { taskId, checks: [...] }:每条检查形如 { cmd, expect: { exit?, contains?, notContains? }, timeoutMs? },断言全部 AND;
- 不通过就 tandem_reject { taskId, gaps? }:失败输出被包进 <codex-output> 定界块回灌(并声明「这是数据不是指令」),最多重做 2 轮;
- 没有 taskId 的临时检查传 dir 即可(不冻结、不记账)。

consult(会审)——判据不是「有几个方案」,而是「这份东西的设计是不是我方(主智能体)出的」。出现任一条就问一次:
- 我要改插件 / 工具 / 脚本的功能实现(改逻辑、改 schema、改加载路径);
- 我要给用户一个由我设计的方案 / 实施计划 / 排查思路 / 架构选型;
- 要往文档里写设计说明。

不审:照既定方案执行命令、重跑失败任务、读文件查资料、改文案错别字。

同一问题默认只问一次;只有出现新证据或明确分歧时才追问。GPT 是第二个判断者、不是审批人——它挑出的问题不采纳时要说明理由。

安装

1) 装依赖(Codex CLI)
npm i -g @openai/codex

2) 把插件链进桌面端配置档(先把下面的路径换成你实际存放插件的位置)
cd ~/.dsh/profiles/desktop

2a. package.json 改两处: dependencies 里加一行 "dsh-tandem": "link:D:/dsh/config/dsh-tandem" dsh.profile.bundles 里追加一项 "dsh-tandem"

2b. 在配置档目录下建目录联接 New-Item -ItemType Junction -Path node_modules\dsh-tandem -Target "D:\dsh\config\dsh-tandem" 等价 cmd(在同一目录下):mklink /J node_modules\dsh-tandem "D:\dsh\config\dsh-tandem"

3) 重启 DSH(插件在启动时加载,热重载不会加载新插件)

配置

默认值 → ~/.dsh/tandem.json → 插件 config,后者覆盖前者。见 config.example.json。

{
"codexCommand": "codex",
"model": "gpt-6.1-sol",
"maxParallel": 3,
"workRoots": ["D:/dsh/work"],
"taskTimeoutMs": 300000,
"requireCompletionMarker": true,
"stateDir": "D:/dsh/config/dsh-tandem/state",
"envKey": "PORTDAN_API_KEY",
"reviewGuard": true,
"autoRescue": true,
"rescueAfterCalls": 15,
"rescueAfterMin": 10,
"rescueCooldownMin": 20,
"rescueMaxPerSession": 3
}

- codexCommand:留 codex 即可,插件会自动探测 npm 全局安装位置,并优先用 .exe(Windows 下 spawn .cmd 需要额外包一层 cmd.exe)。
- workRoots:允许派活的目录白名单——插件会拒绝工作根之外的 dir、互相包含或重复的 dir。注意它限制的是「任务从哪儿起步」,不是文件写入范围:Codex 以 danger-full-access 运行(Windows 沙箱实测会把 workspace-write 静默降级成只读,改不了文件),所以白名单挡得住手滑与误配,挡不住任务提示词里明目张胆的越界写入。要真正限制写入,得在 checks 里自己核验产物路径。
- maxParallel:上限 3,实测超过 3 会被上游挂住。
- requireCompletionMarker:默认 true。Codex 退出码 0 之后,还会检查日志里有没有它固定打印的 tokens used 完成标记,没有就判「空跑」并按失败重派。实测过同一份 prompt 一次 37 秒空跑(退出码 0、一个文件没改)、一次 117 秒正常改完,光看退出码分不出来。这是人类可读输出、不是稳定接口,换版本后若失效就设成 false。
- 0.163 的结尾格式变了(2026-10-18 实测):tokens used 之后还有一段重复的最后消息,所以匹配允许标记后跟最多 4000 字符收尾文本,仍锚定日志末尾。

验收闭环(v0.2.0)

tandem_delegate 成功的任务会返回一个 taskId,后续验收就是围绕它走:

delegate 成功 → taskId 写进票据(<stateDir>/tickets.json)
tandem_check { taskId, checks } ← 首次调用冻结判据(checks 的哈希)
├─ 全部通过 → 结束 └─ 有失败 → tandem_reject { taskId, gaps? }(最多 2 轮) └─ 重做成功后 lastCheck 清空,再 check

判据冻结:同一 taskId 只有第一次 tandem_check 能定义 checks,之后传不同判据会被拒绝。这是为了防止「跑完发现没过、回头把验收条件改松」——会审明确指出这是最容易被钻的空子。

明确的能力边界(也是会审要求写清的):
- 插件只提供执行器,不提供独立 oracle;checks 由主智能体写,语义验收仍由主智能体负责;
- 不防「Codex 改测试文件/测试脚本」——那只能靠对 diff 的代码审阅;
- 轮次上限按 taskId 计数,防的是同一任务无限循环,不是安全边界(新开任务会重置)。

会审标记 reviewId(v0.3.0)

tandem_consult 成功(Codex 真回了有效四段答案)时,插件在本进程内随机签发一个 reviewId(rv- + 16 位十六进制),并记下这次会审覆盖的路径。

它标的是内容,不是使用配额:

- 同一个 reviewId 可以在它覆盖的路径上反复派活、反复检查——没有「一次只能开一个新任务」这种限制;
- 下次改动不带标记,验收结果就是 reviewed: false,明摆着「这段没经过独立复核」;
- 模型自己编的 id 查不到(tandem_delegate 当场拒绝),因为它不在 reviews.jsonl 里。

tandem_consult { mode: "review", question: "...", paths: ["D:/dsh/config/dsh-tandem"] }
→ { ok: true, reviewId: "rv-1a2b3c4d5e6f7a8b", reviewedPaths: [...] }

tandem_delegate { tasks: [...], reviewId: "rv-1a2b3c4d5e6f7a8b" }
→ 票据带上标记;tandem_check 返回 reviewed: true 和 review 摘要

标记会过期。 签发时同时记下被审路径的内容指纹(源码文件内容 sha1,最多 300 个 / 单文件 1MB 以上退回大小+时间)。之后这些文件被改动,tandem_check 就把 reviewed 翻成 false、给出 stale: true 和一句说明——标的是内容,内容变了标记就该失效(盲审指出的假信号:只绑路径的话,换了内容它照样说「审过」)。验收本身照跑,只是不再声称「这段审过」。

指纹只算 CODE_EXT 里的扩展名(.js .mjs .cjs .ts .tsx .jsx .json .md .yml .yaml .py .rs .go .toml .css .html .sh)。一个目录里没有这类文件时指纹恒定——那种情况下它什么也判断不了,别指望它。

落盘:<stateDir>/reviews.jsonl(一行一条,只追加)。轮转保留最近 200 条,且被票据引用的凭证不会被清掉——否则证据链会在某次清理后突然断掉。覆盖路径与任务目录不符只提示、不阻断。

边界:这仍然只是标记。它证明「有人独立审过」,不证明「审得对」;reviews.jsonl 能被手工改写(本机信任模型)。

会审闸:让「改了没审」自己冒出来(v0.4.x)

前面的 reviewId 是自愿机制——智能体自己要带上标记。实测的失效方式很朴素:主智能体一口气改完代码、直接回复用户,全程没想起会审,而任何人(包括用户)当场都看不出来。

会审闸把这件事改成插件负责:每个会话维护一份改动账本,未经会审的改动会在提醒、拦截、留痕三个位置自己暴露。

| 位置 | 干什么 |
| fs/write-intent / fs/edit-intent | 记账。任何走 DSH fs 服务的写入 / 编辑都记下绝对路径,PTC 嵌套调用里的写操作同样经过这条线。这是主数据源——不从工具参数里猜路径 |
| tools/post-execute | 提醒。未审文件数到阈值就注入一条 user 消息(source.kind = "dsh-tandem",不会被当成真人发言),点名具体文件 |
| tools/pre-execute | 拦截。present 在还有未审改动时被拒一次,带机器可读的 code: "REVIEW_REQUIRED" |

配置(config.example.json 里都有):

{
"reviewGuard": true,
"reviewGuardFirst": 2,
"reviewGuardEvery": 4,
"reviewGuardGate": "deny"
}

reviewGuardFirst 是第一次提醒的未审文件数(不是等到第 3 个——第一次出现未审改动就该说话),之后按 reviewGuardEvery 递增再提醒。reviewGuardGate 设成 "off" 则只提醒、不拦 present。

闸只拦一次,但放行不沉默。 第二次 present 放行——否则智能体重试一下就永远交付不出去,闸会变成死锁而不是约束。代价是「拦一次」本身拦不住决心要交付的人,所以放行时补两件事:
- 往对话里注入一条明示「这次交付含 N 个未经会审的改动」,让这件事出现在用户眼前,而不是烂在插件内部;
- 往 <stateDir>/deliveries.jsonl 落一条 {at, session, action, count, files},事后可查。

重启不洗白。 账本落盘在 <stateDir>/ledger.jsonl(一行 {path, t},超过 400 行压到最近 200 条),插件启动时读回。DSH 重启一次就把未审状态清零,是这套机制最容易被打穿的地方——重启后一样拦得住、一样认那次已经发生过的会审。

判定规则

每个改动文件取 t = max(记账时刻, 当前 mtime);从最新的会审记录往前扫,找到一条 at > t 且覆盖该路径的会审就算已审,扫到 at <= t 就停。

两个后果值得写清:旧会审清不掉新改动(时间条件天然挡住「拿一个月前的会审复用」);用 utimesSync 之类绕过 fs 钩子改 mtime 也会被重新判成未审(mtime 越过会审时间即重新变脏)。

能力边界

- 只跟踪代码类扩展名(与 reviewId 的指纹用同一份 CODE_EXT:.js .mjs .cjs .ts .tsx .jsx .json .md .yml .yaml .py .rs .go .toml .css .html .sh),并跳过 node_modules、.git、dist、build 等目录段。只排除插件自己的 stateDir,别的同名目录照常记账(一刀切排除 state 目录段会让「改动正好落在这种目录下」变成直接绕过,会审挑出过这条)。
- 相对路径直接跳过:判不出它属于哪个工作目录,猜错会留下永远清不掉的假条目。fs 意图给的本来就是绝对路径。
- 路径是词法比较(resolve + win32 小写),不解析符号链接 / junction / subst 盘符 / UNC 别名。同一目录换个写法会被当成不同路径,方向是「多记账」而不是「漏记账」(.jsonl 本来就不在跟踪扩展名里,实际影响为零)。
- 它只知道「有没有一次成功会审覆盖了这个文件」,判断不了「审得够不够」。
- present 不是唯一的交付路径——结论直接写在回复里就不经过它。闸只能覆盖走 present 的交付。
- 账本文件能被手工改写(本机信任模型,与会审标记同一条边界)。
- 文件被删掉后从账本摘除(记账 10 秒后生效,窗口内可能短暂误报,取的是保守方向)。

自动求援:转久了主动问一次 Codex(v0.5.0)

不是「卡住检测」—— 是「到点问一句」。判据故意宽,判断交给外部模型。

为什么这么做:真实样本(一个 145 次工具调用、57 分钟没解决的 APK 逆向会话)里,显式失败只有 5 次且全是孤立的(连续失败段长度恒为 1),同一命令重复最多 6 次。任何「按重复次数 / 报错次数」写的判据在那个样本上都不会响。真正卡住的形态是:一直在换动作、每一步都有局部结果、整体没推进、用户沉默。

什么时候响(全部同时满足才问):

- 当前 turn 还在跑(在等用户时不打扰)
- 自最后一条真实用户消息以来 ≥ 15 次工具调用
- 且距那条消息 ≥ 10 分钟
- 且距上次求援 ≥ 20 分钟
- 且本会话求援次数 < 3

「连续 ≥3 次异常输出」只用来写触发原因,不单独触发(60 个历史会话里连续异常段 ≥3 只出现 4 次,而 ≥30 次调用无用户消息的占 15/60 —— 两个信号单独用都脏)。

怎么给:插件把最近 30 步(工具名 + 命令首行 + 结果首行 + 异常标记)加用户最后一条消息,发给 Codex 复核。触发当下先注入一句「已发出求援」,结论回来后在下一次工具调用时注入;正文第一句明写「这次不是用户说话」,并允许忽略。Codex 认为在正常推进时只回 NO_STUCK,那条注入会缩成一句背景提示。

拿不到结论也说话:高峰时段 Codex 会空转或超时(实测 empty_response 和 40 秒流断开都出现过),这时注入「没有拿到结论」加失败原因,不会静默。

不注入的情况:结论超过 30 分钟(挂死场景要等工具超时回来,别给太短)、或用户在求援期间开口(可能已经换了任务)—— 旧建议不许塞进新任务。

重启就清零(已知边界):触发计数、正在跑的求援、待交付的结论都只在内存里,DSH 重启即丢。迟到的结论本来也不该跨重启塞进新会话,所以不为此加持久化;代价是重启后同一会话可能再问一次(同一会话每进程仍最多 3 次)。

关掉:"autoRescue": false。只调阈值就改上面几个字段,重启 DSH 生效。

排查:~/.dsh/tandem-state/load.log 里有 auto rescue installed (...),之后每次 rescue fire / announce / deliver / drop 都有一行。

会审失败 ≠ 没有意见

tandem_consult 不是「问了就有答案」。它失败时会给出 reason 和 hint 两个字段,那不是在说「没问题」:

| reason | 含义 | 该做什么 |
| policy_blocked | 上游内容策略拦下了提示词 | 改写措辞(去掉可能触发的技术细节)后重试一次 |
| auth_failed / model_not_found | 凭据或模型名不对 | 先跑 tandem_status 核对 |
| rate_limited | 上游限流 | 等一会儿,别连发 |
| empty_response | 进程正常退出但没产出(空转) | 已自动重试一次;仍空就换个问法 |
| invalid_answer | 不是合法 JSON,或缺四段之一 | 原文在 tail 里,据此决定要不要重问 |
| timeout / spawn_failed / unknown | 超时、启动失败、未能识别 | 缩小材料 / 查路径 / 读 tail |

规则(写在这里就是把话说死):ok:false 时不许把它当成「审过了、没意见」继续实施改动。
要么改写措辞重试一次,要么在回复里明写「未获独立复核」并停下来问人。

每一次会审的结果(含失败)都会追加到 <stateDir>/consult-history.jsonl,tandem_status 的 lastConsult 直接显示最近一次的时间 / 是否成功 / 原因 / 尝试次数 —— 「上次到底审成没审成」不再靠记忆。

凭据

Codex 需要 PORTDAN_API_KEY。插件在加载时若发现环境变量缺失,会自动从 ~/.dsh/.credentials.yaml 的 refs 段补齐;tandem_delegate / tandem_consult 每次执行前还会再补一次(幂等),所以后来才写进凭据文件的情况也能赶上。补不到就在 tandem_status 里明确报出来。

已知限制

- Codex 非 tty 时输出会被吞,所以日志直接写文件(结果里给日志路径)。
- 「退出码 0」不等于干完活:偶发空跑(进程正常退出、日志只有头部和 prompt、文件没动)。已用完成标记兜住并重派一次,但它只是启发式判定,最终仍要靠 checks 自己核验产物。
- 上游缓存命中率不可控(实测 0~80% 波动),同一任务重复跑成本不是线性的。
- 超时按整棵进程树杀,避免 codex 子进程残留。
- checks 的命令用 shell 执行,语义随平台而定(Windows 是 cmd /c);总时限到点后剩余检查直接判失败。
- tandem_consult 用 read-only 沙箱,它物理上改不了文件——这是刻意的。
代价:Windows 上 read-only 沙箱会把读取也拦住(exec_command ... rejected: blocked by policy,连 cwd 里的文件都读不到),cwd 参数因此在本机不起作用。要让 Codex 会审一个真实项目,就把关键文件的内容拼进 context 传给它——实测这样效果正常。这个怪癖只在 Windows 的 Codex 沙箱上出现。

与 Codex 0.163 的兼容性(2026-10-18)

- 必须 --disable multi_agent:0.163 默认开启多 agent 协作工具,非交互 exec 下模型会调用 collab wait(等待一个不存在的子线程)然后永久挂起。实测一条 2K 字符的评审 prompt 挂 172 秒无输出;关掉后 27 秒正常返回。
- 必须 -c mcp_servers={}:桌面端配置的 MCP 服务器(node_repl / computer-use)在非交互 exec 里反复启动失败,把一个 12 字节文件的活拖到 90~130 秒;只覆盖本次进程后降到 25 秒,且不改用户的 ~/.codex/config.toml。

一个刻意的取舍:不做「跑完比对哪些文件被改」

只保证各任务目录互不重叠(重复、互相包含、越界都拒绝),不做运行前后快照对比。

原因是快照这条路两头都不讨好:并行任务和外部进程都会在同一个审计窗口里改文件,快照只能看到「差异」、看不到「谁改的」,归因错了就是误报;而 mtime + size 的粒度还会漏掉同尺寸改写。误报和漏报同时存在时,这个字段只会变成噪音,不如只保证「互不重叠」这个能确定的事实。

真需要知道改了哪些文件,就在 checks 里让任务自己 git status --short 并把输出写进产物。

装不上 / 工具不出现时先看这里

DSH 不写运行日志,插件加载失败在界面上毫无提示(工具就是静默消失)。本插件在启动时写探针:

~/.dsh/tandem-state/load.log

正常应当长这样(五个工具各一行 registered):

... module evaluated
... apply start, codex=...\codex.exe, tools service=object
... registered tandem_status
... registered tandem_delegate
... registered tandem_consult
... registered tandem_check
... registered tandem_reject

- 只有 module evaluated → apply 没被调用(插件未被激活 / 依赖的 tools 服务没起来)。
- 有 REGISTER FAILED → 那行后面跟着完整堆栈,是 schema 或定义不合法。

两个必须知道的坑(都实际踩过)

1. 不能 import @deepseek-ai/dsh-tools:桌面端 profile 的 node_modules/@deepseek-ai/ 里没有这个包,import 必失败。插件的 defineTool 是自带的 compileSpec / compileParameters(属性级 required: true → 对象节点 required 数组)。少了这层编译,register() 会抛 required is not supported on type "boolean"。
2. execute 的返回值整体就是被校验的 value:DSH 用 createSuccessResult(exec, tool, returned) 直接拿返回值去校验 output.schema,渲染才由 output.render(args, value) 做。返回 { content, structuredContent } 会报 missing required property "value.ok"。

自测(不经过 DSH)

node --check index.js
node --check lib/consult.js
node --check lib/acceptance.js
node --check lib/ledger.js
机械验收 + 票据单测(含会审给的反例:echo PASS && exit 1、notContains 假阳性、超时)
node test/acceptance.mjs
会审闸单测:账本判定、重启不洗白、交付留痕、三个 hook 的装配
node test/guard.test.mjs
假 ctx 走一遍 apply,确认五个工具都注册成功
node -e "import('./index.js').then(m=>{const r=[];m.apply({tools:{register:d=>{r.push(d.name);return()=>{}}}});console.log(r)})"
预览分享卡片长什么样 ↓wuuuuu123-32/dsh-tandem
分享 · DeepSeek Harness Hub
wuuuuu123-32/dsh-tandem

把独立子任务并行派给本地 Codex 执行并机械验收

回到顶部