dsh-card-dispatch
Verifieddsh-card-dispatch · v0.1.1 · MIT · Web UI
派卡台开发的上游:把需求拆成任务看板卡片(执行交给 dsh-client-ui-task-board)
Install
dsh plugin add dsh-card-dispatch Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Tags
Readme
dsh-card-dispatch
DSH Host 插件:把需求拆成任务看板卡片。就这一件事。
npm i dsh-card-dispatch # 装进 DSH profile 的 node_modules
更新日志:CHANGELOG.md;当前发布版本见上面的 npm 徽章。
- 拆分:
src/split.js(纯函数,判据自动补C-01,按判据拆父卡 + 并行子卡,prompt 自包含) - 填写:控制台页面直接调看板的
POST /api/task-board/action - 执行:不归本插件。开会话、钉工作区、预设/权限、跑、结算,全部是看板的事
人类入口:/api/card-dispatch/(面板里的「派卡台」)。
1. 为什么只做这两件事
这个插件曾经有 6000 行:事件台账、二十行迁移授权表、24 个模型工具、看门狗、 自动开会话、预算核算、拓扑审计……结果是「什么都能干,但哪一步都不确定」。
现在按一条线切:
| 谁 | 负责 |
|---|---|
| 平台(DSH) | 会话、权限、计费、工作区、模型通道 |
| 看板插件 | 卡片的真源、运行、子卡关系、运行记录 |
| 本插件 | 闸门(能不能开跑)、投影(需求 → 卡片)、人入口(一块能看懂的表单) |
平台已经保证的事不重复实现:不自己起会话、不自己记台账、不自己算并发——
看板的 maxParallelRuns、sessionDefaultPermission、ExecutionRecord 就是答案。
重复一份只会得到两份不一致的真相。
2. 安装与用法
安装(两步都要做,然后重启一次 DSH):
装进 profile 的依赖里:
cd $env:USERPROFILE\.dsh\profiles\desktop npm i dsh-card-dispatch在 profile 的
cordis.patch.yml里加一个 insert 块:- insert: - id: card-dispatch name: 'dsh-card-dispatch' config: enabled: false # 总开关:先关着,进面板自己开 permission: workspace-write dataDir: .card-dispatch必须是 insert 块:只写
- id: card-dispatch+ config 不生效,宿主会报patch: entry "card-dispatch" not found并跳过。
本地改代码时用链接安装(不进 npm 仓库,改完刷新页面即可):
cd $env:USERPROFILE\.dsh\profiles\desktop
npm i "dsh-card-dispatch@link:<仓库路径>/plugins/card-dispatch"
用法:
- 左侧边栏「派卡台」→ 面板(左栏填需求;右栏一列装「工作区 / 模型 / 怎么跑 / 开关 / 体检」;下面整条是预览);
模型在右栏:它是全局设置(解析并拆分、生成判据草稿都用它)。换一个就写回
models.draftRoute, 刷新或重启后仍是它;下拉候选来自宿主设置里已添加的模型。 - 填 需求标题、描述(可选)、验收判据(一行一条);
需求写在文档里的话,用「从文档导入」选
.md/.txt/.json,标题、描述、判据会自动填好; 手头只有一段话(聊天记录、验收邮件、会议纪要)就直接贴进描述,点 「AI 解析并拆分」:- 第一步(模型)把原文整理成 标题 + 描述(给谁用 / 要什么 / 明确不要什么)+ 3~8 条可判定的判据;
- 第二步(规则)按判据拆成「父卡 + 子卡 + 验收卡」,拆几张是算出来的,不由模型决定;
- 第三步(模型)按需求给每张卡写提示词开头(目标 / 范围 / 不要做什么), 判据清单和「完成要交证据」那些硬要求仍由系统确定性地拼在后面;
- 预览里能逐张看到提示词,点「看解析用的提示词」能看到两段提示词原文;
- 标题只在空着时填、判据只追加不覆盖;改过表单后这一轮写的提示词会被丢掉(回落到模板);
- 模型不可用时按格式规则解析(认
# 标题、- [ ] 勾选项、## 验收判据列表),并明说走了哪条路;
- 判据不想手写就点「生成草稿」:用右栏选的模型写,模型不可用或关掉
models.useModel时退回规则提炼。 两条路都是草稿,改完再用。 - 点「看拆分」先看预览:几张卡、每张卡会收到什么 prompt、这次会不会开跑。
- 点「写入看板」(右下)建卡;勾了「写入后立即运行」才会真的开会话(这一步才计费)。
- 验收驳回后回来点「按结论处理」:读
out/<根卡 id>/verdict.json,按打回上限给出 pass / rework / escalate。
3. 路由(十二条)
都以 /api/card-dispatch 为前缀,kind: prefix,页面每次请求从磁盘读(改 HTML 不用重启,改路由要)。
| 方法 + 路径 | 作用 |
|---|---|
GET / |
控制台页面(src/console.html) |
GET /view |
页面要的状态快照:能力清单、看板基址、当前配置 |
GET /models |
宿主设置里已添加的模型(只读扫描 cordis.patch.yml),给下拉用 |
POST /split |
需求 → 卡片计划(cards[] + envelopes[] + warnings[]),不写任何东西;同一份输入 10 分钟内幂等 |
POST /plan |
解析 + 算拆几张卡 + 给每张卡写提示词(两次模型调用);回 intros 给页面,写卡时原样带回 /split |
POST /parse |
整段需求 → 标题 / 描述 / 判据(只整理字段,不拆分) |
POST /criteria |
判据草稿:模型优先,失败退回规则提炼 |
POST /import |
文档正文 → 标题 / 描述 / 判据(正文由浏览器读成文本发来,插件不碰磁盘路径) |
POST /rework |
读 verdict.json,按 gates.maxReworkRounds 给 pass / rework / escalate;打回次数由服务端落盘 |
GET /config |
声明表 + 当前值(页面据此回填控件) |
POST /config |
归一化后落盘成完整配置;不认识的键回 rejected,不会被悄悄丢掉 |
GET /status |
配置、开关、体检结论 |
POST 一律要求人标记:请求头 x-card-dispatch-human: true 或 body 里 human: true。
没有这个标记就是 400——这条路只能人走,模型和脚本不该替人按「开跑」。
除人标记外还有三道同源栅栏(人标记不是鉴权,别的页面照样能带):
来源必须是回环地址;sec-fetch-site 不能是 cross-site / same-site;带了 Origin 就必须与 Host 一致;
POST 必须声明 content-type: application/json(text/plain 的简单请求不触发预检,正是跨站伪造最常用的形状)。
命令行调用要同时带标记与 JSON 头。
/split 的幂等窗口是 10 分钟:同一份输入(标题、描述、判据、跑法、提示词)重复提交会被当成重试,
卡 id 与 requestId 都不变,看板不会建出第二批;要故意新建一批,改一下标题/判据,或等窗口过去。
4. 配置
配置只有一份真源:src/config.js 的声明表。面板、/config 路由、cordis.patch.yml 都按它收敛;
旧版键(budgetPerDay、autoProvision.permission)仍然认得,非法值退回缺省并记一条 note。
| 键 | 缺省 | 作用 | 插件能真拦吗 |
|---|---|---|---|
enabled |
false |
总开关;关掉只拆卡、不返回运行信封 | 能 |
permission |
workspace-write |
卡片权限预设(写进每个 create 信封;null = 不指定,沿用看板默认;请求体不能覆盖它) |
能 |
dataDir |
.card-dispatch |
插件的数据目录(配置、结论文件) | — |
gates.requirePlanApproval |
true |
没批准的拆分不给运行信封 | 能(「批准」= 人点「写入并运行」) |
gates.requireEvidence |
true |
没有 commit / 报告不算完成;验收卡随父卡自动运行(团队开着时在同一条会话里) | 能 |
gates.budgetPerDay |
200 |
每日运行次数上限 | 能,需调用方给 runsToday |
gates.maxParallelRuns |
3 |
同时在跑的会话上限 | 能,需调用方给 runningSessions |
gates.maxReworkRounds |
2 |
打回上限,第 3 次升级给人(基线 R3) | 能 |
models.useModel |
true |
解析与判据草稿用不用模型;关掉只用规则(点一次才计费) | 是 |
models.draftRoute |
空 | 全局模型:解析并拆分、判据草稿、写卡提示词都用它(右栏下拉写的就是这一项),留空 = 宿主默认 | 是 |
org.departments |
dev/verify/integrate |
部门名(写进卡片描述,供人读) | 只记录 |
execution.driver |
task-board |
执行层驱动(只有看板一条路) | 枚举校验 |
execution.teamRun |
true |
智能体团队:开了才按判据拆子卡(父卡带 teamRun: true,看板只起一个 Lead 会话,子卡由它派 teammate);关掉不拆子卡;交付门槛开着时父卡仍以 Lead 会话运行(只带验收卡) |
能(决定拆不拆 + 写进 create 信封) |
audit.orgChart |
true |
拓扑稽查:事后核对有没有人绕开主管 | 只检查,不拦 |
org.depthRequired |
2 |
主管带人所需的委派深度 | 不能,只体检提示 |
优先级:<dataDir>/config.json 优先于部署配置。 面板保存的是归一化后的完整配置,
所以不会出现半份配置的中间态;想回到部署值就删掉这个文件(/status 会给出路径)。
三个闸都是纯函数(src/gates.js):同样的输入永远同样答案,因此能逐条单测,也能在页面上先算给人看。
/split 用 evaluateStart 决定给不给运行信封——写卡不花钱,运行才花钱,闸门只卡后者。
5. 看板信封的字段约束(踩过就回不来)
看板对 action 做 exactKeys 校验,多一个字段就是 400:
create只认{kind, title, description, prompt, workspaceId?, mode?, permission?, parentId?, teamRun?};null也要整个省掉,不能写workspaceId: null。teamRun只写在父卡上。子卡再带一次,看板会给子卡也起一个 Lead,等于开了两套班子。run是{kind:'run', taskId};delete拒删有子卡的卡(task has subtasks; detach or delete them first), 也拒删正在跑的卡;要整棵子树消失用archive(可恢复)。- 插件进程调
/state是 403:看板有信任栅栏(sec-fetch-site/Origin/dsh-auth-cookie)。 所以插件只生成信封,真正的请求由页面同源发出(浏览器的标记天然通过)。
6. 拆分策略
| 情况 | 产出 | 会话数 |
|---|---|---|
| 判据 0 条 | 1 张父卡 + 警告「执行者不知道做到什么算完」 | 1 |
| 判据 1 条 | 1 张父卡(不拆——拆多了全是计费) | 1 |
| 没开智能体团队、也没开交付门槛 | 不拆子卡,判据全写进一张卡 | 1 |
| 判据 ≥2 条 + 开了团队 + 勾了并行 | 父卡 + 每判据组 1 张子卡(子卡由父卡的 Lead 在同一个会话里派成 teammate) | 1 |
| 判据 ≥2 条但没勾并行 | 1 张父卡,全部判据写进去 | 1 |
交付门槛开着(gates.requireEvidence) |
上面任一种 + 1 张验收卡 | 1(父卡以 Lead 会话运行,验收卡是同一会话里的 teammate;团队没勾也不额外开会话) |
卡数 ≠ 会话数。只要走 Lead 模式(开了团队,或团队没开但开了交付门槛),父卡 + 子卡 + 验收卡 都在同一个 Lead 会话里跑,只算 1 条会话;只有团队和门槛都关、只有一张父卡时,才是单卡单会话。 验收卡可能比开发卡先启动,它会在会话里等开发报告,再给出 PASS/REJECT。 预览里的警告会把这次到底开几条会话直接写出来。
拆不拆子卡只看跑法:团队开着才拆。页面跟着联动——团队没勾时 「按判据拆成并行子卡」直接禁用;勾一下就存盘并重算预览,不用先点「保存」才发现会话条数变了。 其余开关(总开关、计划闸、交付门槛、拓扑稽查、预算 / 并发 / 打回)也一样:改完立刻存盘; 已经出过预览的会就地重算,不必再点一次「保存」。
子卡是并行的:看板没有「卡片间依赖」字段。所以每张子卡的 prompt 自带它那组判据 和「怎么算完」,不靠顺序;要严格串行就别勾并行。
验收卡为什么是另一张卡:基线 §3.3 要求验证独立于开发——同一张卡自己说「我做完了」
不算验收。所以门槛开着时,插件会把交付要求写进每张开发卡(报告路径写死成
out/<该卡 id>/report.md,附 commit sha),再生成一张只读证据的验收卡:
结论只能是 PASS 或 REJECT,REJECT 必须带 {criteriaId, expected, actual, evidencePath}。
打回上限按 gates.maxReworkRounds 写进验收 prompt;走 Lead 模式时父卡就是 Lead,负责收尾。
平台排不出「先开发、后验收」的顺序(看板没有依赖字段),所以顺序靠验收卡自己等报告:
父卡自己就是干活的那张时,prompt 不会再写「父卡不产出报告」,也不会提一张并不存在的验收卡。
验收卡和团队开关无关:门槛开着就有,并且随父卡一起运行——它是父卡的子卡,运行父卡会把子卡
一起带起来,所以不再需要手动点「运行验收卡」。代价是它可能比开发卡先启动,因此验收 prompt 里
写死了等待规则:开始先等 30 秒;报告缺失或比该卡最近一次执行开始时间还旧时,每 60 秒再看一次;
开发卡已结束但仍缺本轮报告 → 立即 REJECT;最多等 120 分钟。开发卡的 prompt 同时要求开工前
删掉上一轮的 report.md,所以复验不会把旧报告当新证据。返工只重跑单张开发卡、不会带起验收卡,
页面会在重跑信封之后再单独排一次验收卡。
走 Lead 模式时它只是 Lead 会话里的一个 teammate,不额外开一条会话——所以团队没勾、门槛开着时,
父卡 + 验收卡也只算 1 条会话。
7. 已验证 / 未验证
已验证(真机,2026-10-22,在运行中的看板页里直接发请求):
| 动作 | 结果 |
|---|---|
GET /api/task-board/state |
200,sessionDefaultPermission: workspace-write |
create 单卡 |
200,卡片出现 status: todo |
create 父卡 + 子卡(parentId) |
200,子卡正确挂到父卡下 |
delete |
200,卡片移除,看板回到干净状态 |
delete 有子卡的父卡 |
400 task has subtasks; detach or delete them first(所以要按子树删/归档) |
AI 解析并拆分(2026-10-24,真机点过一次,用的是宿主里配的 deepseek-v4.1-flash):
一段「做一个可视化计算器…」的原文,返回 5 条判据,页面自动拆成 6 张卡(父卡 + 4 子卡 + 验收卡),
预览里逐张列出提示词。/plan 的两段提示词(解析、写卡)在同一轮里各调一次模型。
模型不可用或关掉 models.useModel 时退回「格式规则解析 + 模板提示词」,并在提示里说明。
未验证:run 的会话侧效果(真的开出一个会话并跑完)——那会真开会话计费,
没有替你跑。第一次点运行时请留意看板是否解析出工作区与预设。
teamRun 写进 create 信封后看板是否真的只起一个 Lead、把子卡派成 teammate,同样没跑过;
交付门槛开着、团队没勾时「父卡当 Lead + 验收卡做 teammate」这条路径也没跑过;
看板自己的 teamRunAvailable 与「Agent Teams 服务缺失就拒绝运行」两道检查仍在它那边。
交付门槛现在到哪一步:证据要求已写进每张开发卡的 prompt,验收卡独立存在、只给
PASS/REJECT 并把结论落到 out/<根卡 id>/verdict.json——「证据从哪来、谁来验、结论放哪」都解决了。
验收卡现在随父卡自动启动,不再需要人点「运行验收卡」;「等报告」是 prompt 级约束,未真机跑过。
「打回之后谁重跑」现在有一个人触发的答案:POST /rework 读那个文件,按
gates.maxReworkRounds 给出三种结局——pass 收尾、rework 返回重跑被拒卡片的 run 信封、
escalate 停下来交给人(基线 R3)。没有轮询、没有定时器:人点一下才跑一次判定。
为什么判定靠文件而不是看板状态:GET /api/task-board/state 走看板自己的信任栅栏,
插件进程没有那张 cookie,实测 403;而且 ExecutionRecord.result 只记「执行成没成」,
不记「验收判 PASS 还是 REJECT」。落盘的文件三方都能读,也才符合基线 P3。
还没做:worktree 隔离、模型调用缓存、skills/card-dispatch-org/SKILL.md 的安装。
模型调用已加一次重试(只重试非超时失败)。预算与并发闸已经接上真实运行态:页面从看板 /state 的 executions 算「今天跑了几次、几个会话在跑」,
随 /split 传给服务端;拿不到快照就回 E_STATE_UNKNOWN 拒绝放行,不再拿 0 冒充。
注意:看板每张卡只保留最近 20 条 executions,单卡一天跑超过 20 次时预算数会少算。
8. 测试
node test\run-all.mjs # 196 个用例,全绿
十七个测试文件:config(收敛/兼容/合并)、gates(四个闸的判定顺序与理由)、
selfcheck(体检判据)、split(拆分、信封、智能体团队、提示词覆盖、幂等)、status(所有路由 + 打回次数落盘)、
verdict(结论文件契约)、import(文档解析)、criteria(规则提炼)、models(模型清单扫描)、
model-call(调用超时 / 重试边界)、criteria-llm(判据草稿的模型通道)、parse-llm(整段需求解析)、
prompts-llm(按需求写每张卡的提示词)、human-channel(路由 + 两道门 + 依赖透传 + 闸门降级)、
console(页面 id / 看板基址 / 旧机制痕迹清零)、http(同源 / JSON / nosniff 栅栏)、
index(导出形状 / 旧配置兼容 / apply 只挂路由 / 端到端打一次 /models)。
模型相关的用例全部用假的 llm 服务:不在测试里花真钱。真模型的验证靠人点一次、
看页面回的 source: 'model' 和实际文案。
9. 文件
| 文件 | 行数 | 作用 |
|---|---|---|
index.js |
~90 | cordis 入口:apply 只注册路由,注入 models 依赖 |
src/human-channel.js |
~830 | 十二条路由、两道门(人标记 + 同源/JSON)、闸门与配置 |
src/split.js |
~470 | 需求 → 卡片计划(唯一有「业务判断」的地方,纯函数;同输入幂等) |
src/gates.js |
~110 | 四个闸:计划、运行态、预算、并发(+ 打回次数判定) |
src/rework-store.js |
~70 | 打回次数按 rootId 落盘(<dataDir>/rework.json) |
src/config.js |
~230 | 声明表 + 归一化 + 兼容旧键 |
src/config-store.js |
~90 | 配置读写(<dataDir>/config.json 优先) |
src/selfcheck.js |
~200 | 体检:看板在不在、模型清单、权限、委派深度 |
src/catalog-source.js |
~70 | 从宿主的 cordis.patch.yml 扫出可选模型 |
src/model-call.js |
~120 | 模型调用与 JSON 抽取(一次点击一次调用,45 秒超时) |
src/criteria.js |
~90 | 规则提炼判据(无模型时的退路) |
src/criteria-llm.js |
~125 | 判据草稿的模型通道 |
src/parse-llm.js |
~120 | 整段需求 → 标题 / 描述 / 判据:提示词 + 回复解析 |
src/prompts-llm.js |
~140 | 按需求给每张卡写提示词开头(结构不归它管) |
src/import.js |
~150 | .md / .txt / .json → 标题 / 描述 / 判据 |
src/verdict.js |
~120 | verdict.json 的读写与形状校验 |
src/console.html |
~1010 | 控制台页面(单文件,无构建) |