Skip to content

dsh-card-dispatch

Verified

dsh-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

npm

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):

  1. 装进 profile 的依赖里:

    cd $env:USERPROFILE\.dsh\profiles\desktop
    npm i dsh-card-dispatch
    
  2. 在 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"

用法:

  1. 左侧边栏「派卡台」→ 面板(左栏填需求;右栏一列装「工作区 / 模型 / 怎么跑 / 开关 / 体检」;下面整条是预览); 模型在右栏:它是全局设置(解析并拆分、生成判据草稿都用它)。换一个就写回 models.draftRoute, 刷新或重启后仍是它;下拉候选来自宿主设置里已添加的模型。
  2. 填 需求标题、描述(可选)、验收判据(一行一条); 需求写在文档里的话,用「从文档导入」选 .md / .txt / .json,标题、描述、判据会自动填好; 手头只有一段话(聊天记录、验收邮件、会议纪要)就直接贴进描述,点 「AI 解析并拆分」:
    • 第一步(模型)把原文整理成 标题 + 描述(给谁用 / 要什么 / 明确不要什么)+ 3~8 条可判定的判据;
    • 第二步(规则)按判据拆成「父卡 + 子卡 + 验收卡」,拆几张是算出来的,不由模型决定;
    • 第三步(模型)按需求给每张卡写提示词开头(目标 / 范围 / 不要做什么), 判据清单和「完成要交证据」那些硬要求仍由系统确定性地拼在后面;
    • 预览里能逐张看到提示词,点「看解析用的提示词」能看到两段提示词原文;
    • 标题只在空着时填、判据只追加不覆盖;改过表单后这一轮写的提示词会被丢掉(回落到模板);
    • 模型不可用时按格式规则解析(认 # 标题、- [ ] 勾选项、## 验收判据 列表),并明说走了哪条路;
  3. 判据不想手写就点「生成草稿」:用右栏选的模型写,模型不可用或关掉 models.useModel 时退回规则提炼。 两条路都是草稿,改完再用。
  4. 点「看拆分」先看预览:几张卡、每张卡会收到什么 prompt、这次会不会开跑。
  5. 点「写入看板」(右下)建卡;勾了「写入后立即运行」才会真的开会话(这一步才计费)。
  6. 验收驳回后回来点「按结论处理」:读 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 控制台页面(单文件,无构建)