dsh-card-dispatch 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +249 -2
- package/cordis.patch.yml +25 -0
- package/icon.svg +21 -0
- package/index.js +88 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +56 -4
- package/src/catalog-source.js +122 -0
- package/src/client/index.js +155 -0
- package/src/config-store.js +49 -0
- package/src/config.js +229 -0
- package/src/console.html +964 -0
- package/src/criteria-llm.js +79 -0
- package/src/criteria.js +103 -0
- package/src/gates.js +123 -0
- package/src/human-channel.js +794 -0
- package/src/import.js +193 -0
- package/src/model-call.js +89 -0
- package/src/models.js +249 -0
- package/src/parse-llm.js +96 -0
- package/src/prompts-llm.js +104 -0
- package/src/selfcheck.js +117 -0
- package/src/split.js +397 -0
- package/src/verdict.js +133 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 往事似风如雾
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,250 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-card-dispatch
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/dsh-card-dispatch)
|
|
4
|
+
|
|
5
|
+
DSH Host 插件:**把需求拆成任务看板卡片**。就这一件事。
|
|
6
|
+
|
|
7
|
+
```powershell
|
|
8
|
+
npm i dsh-card-dispatch # 装进 DSH profile 的 node_modules
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
- 拆分:`src/split.js`(纯函数,判据自动补 `C-01`,按判据拆父卡 + 并行子卡,prompt 自包含)
|
|
12
|
+
- 填写:控制台页面直接调看板的 `POST /api/task-board/action`
|
|
13
|
+
- 执行:**不归本插件**。开会话、钉工作区、预设/权限、跑、结算,全部是看板的事
|
|
14
|
+
|
|
15
|
+
人类入口:`/api/card-dispatch/`(面板里的「派卡台」)。
|
|
16
|
+
|
|
17
|
+
## 1. 为什么只做这两件事
|
|
18
|
+
|
|
19
|
+
这个插件曾经有 6000 行:事件台账、二十行迁移授权表、24 个模型工具、看门狗、
|
|
20
|
+
自动开会话、预算核算、拓扑审计……结果是「什么都能干,但哪一步都不确定」。
|
|
21
|
+
|
|
22
|
+
现在按一条线切:
|
|
23
|
+
|
|
24
|
+
| 谁 | 负责 |
|
|
25
|
+
|---|---|
|
|
26
|
+
| 平台(DSH) | 会话、权限、计费、工作区、模型通道 |
|
|
27
|
+
| 看板插件 | 卡片的真源、运行、子卡关系、运行记录 |
|
|
28
|
+
| 本插件 | 闸门(能不能开跑)、投影(需求 → 卡片)、人入口(一块能看懂的表单) |
|
|
29
|
+
|
|
30
|
+
**平台已经保证的事不重复实现**:不自己起会话、不自己记台账、不自己算并发——
|
|
31
|
+
看板的 `maxParallelRuns`、`sessionDefaultPermission`、`ExecutionRecord` 就是答案。
|
|
32
|
+
重复一份只会得到两份不一致的真相。
|
|
33
|
+
|
|
34
|
+
## 2. 安装与用法
|
|
35
|
+
|
|
36
|
+
**安装**(两步都要做,然后重启一次 DSH):
|
|
37
|
+
|
|
38
|
+
1. 装进 profile 的依赖里:
|
|
39
|
+
|
|
40
|
+
```powershell
|
|
41
|
+
cd $env:USERPROFILE\.dsh\profiles\desktop
|
|
42
|
+
npm i dsh-card-dispatch
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
2. 在 profile 的 `cordis.patch.yml` 里加一个 insert 块:
|
|
46
|
+
|
|
47
|
+
```yaml
|
|
48
|
+
- insert:
|
|
49
|
+
- id: card-dispatch
|
|
50
|
+
name: 'dsh-card-dispatch'
|
|
51
|
+
config:
|
|
52
|
+
enabled: false # 总开关:先关着,进面板自己开
|
|
53
|
+
permission: workspace-write
|
|
54
|
+
dataDir: .card-dispatch
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**必须是 insert 块**:只写 `- id: card-dispatch` + config 不生效,宿主会报
|
|
58
|
+
`patch: entry "card-dispatch" not found` 并跳过。
|
|
59
|
+
|
|
60
|
+
本地改代码时用链接安装(不进 npm 仓库,改完刷新页面即可):
|
|
61
|
+
|
|
62
|
+
```powershell
|
|
63
|
+
cd $env:USERPROFILE\.dsh\profiles\desktop
|
|
64
|
+
npm i "dsh-card-dispatch@link:<仓库路径>/plugins/card-dispatch"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**用法**:
|
|
68
|
+
1. 左侧边栏「派卡台」→ 面板(左栏填需求;右栏一列装「工作区 / 模型 / 怎么跑 / 开关 / 体检」;下面整条是预览);
|
|
69
|
+
**模型在右栏**:它是全局设置(解析并拆分、生成判据草稿都用它)。换一个就写回 `models.draftRoute`,
|
|
70
|
+
刷新或重启后仍是它;下拉候选来自宿主设置里已添加的模型。
|
|
71
|
+
2. 填 **需求标题**、描述(可选)、**验收判据**(一行一条);
|
|
72
|
+
需求写在文档里的话,用「从文档导入」选 `.md` / `.txt` / `.json`,标题、描述、判据会自动填好;
|
|
73
|
+
手头只有一段话(聊天记录、验收邮件、会议纪要)就直接贴进描述,点 **「AI 解析并拆分」**:
|
|
74
|
+
- 第一步(模型)把原文整理成 标题 + 描述(给谁用 / 要什么 / 明确不要什么)+ 3~8 条可判定的判据;
|
|
75
|
+
- 第二步(规则)按判据拆成「父卡 + 子卡 + 验收卡」,拆几张是算出来的,不由模型决定;
|
|
76
|
+
- 第三步(模型)**按需求给每张卡写提示词开头**(目标 / 范围 / 不要做什么),
|
|
77
|
+
判据清单和「完成要交证据」那些硬要求仍由系统确定性地拼在后面;
|
|
78
|
+
- 预览里能逐张看到提示词,点「看解析用的提示词」能看到两段提示词原文;
|
|
79
|
+
- 标题只在空着时填、判据只追加不覆盖;改过表单后这一轮写的提示词会被丢掉(回落到模板);
|
|
80
|
+
- 模型不可用时按格式规则解析(认 `# 标题`、`- [ ] 勾选项`、`## 验收判据` 列表),并明说走了哪条路;
|
|
81
|
+
3. 判据不想手写就点「生成草稿」:**用右栏选的模型写**,模型不可用或关掉 `models.useModel` 时退回规则提炼。
|
|
82
|
+
两条路都是草稿,改完再用。
|
|
83
|
+
4. 点「看拆分」先看预览:几张卡、每张卡会收到什么 prompt、这次会不会开跑。
|
|
84
|
+
5. 点「写入看板」(右下)建卡;勾了「写入后立即运行」才会真的开会话(**这一步才计费**)。
|
|
85
|
+
6. 验收驳回后回来点「按结论处理」:读 `out/<根卡 id>/verdict.json`,按打回上限给出 pass / rework / escalate。
|
|
86
|
+
|
|
87
|
+
## 3. 路由(十一条)
|
|
88
|
+
|
|
89
|
+
都以 `/api/card-dispatch` 为前缀,`kind: prefix`,页面每次请求从磁盘读(改 HTML 不用重启,改路由要)。
|
|
90
|
+
|
|
91
|
+
| 方法 + 路径 | 作用 |
|
|
92
|
+
|---|---|
|
|
93
|
+
| `GET /` | 控制台页面(`src/console.html`) |
|
|
94
|
+
| `GET /view` | 页面要的状态快照:能力清单、看板基址、当前配置 |
|
|
95
|
+
| `GET /models` | 宿主设置里已添加的模型(只读扫描 `cordis.patch.yml`),给下拉用 |
|
|
96
|
+
| `POST /split` | 需求 → 卡片计划(`cards[]` + `envelopes[]` + `warnings[]`),**不写任何东西** |
|
|
97
|
+
| `POST /plan` | 解析 + 算拆几张卡 + 给每张卡写提示词(两次模型调用);回 `intros` 给页面,写卡时原样带回 `/split` |
|
|
98
|
+
| `POST /parse` | 整段需求 → 标题 / 描述 / 判据(只整理字段,不拆分) |
|
|
99
|
+
| `POST /criteria` | 判据草稿:模型优先,失败退回规则提炼 |
|
|
100
|
+
| `POST /import` | 文档正文 → 标题 / 描述 / 判据(正文由浏览器读成文本发来,插件不碰磁盘路径) |
|
|
101
|
+
| `POST /rework` | 读 `verdict.json`,按 `gates.maxReworkRounds` 给 pass / rework / escalate |
|
|
102
|
+
| `GET /config` | 声明表 + 当前值(页面据此回填控件) |
|
|
103
|
+
| `POST /config` | 归一化后落盘成完整配置;不认识的键回 `rejected`,不会被悄悄丢掉 |
|
|
104
|
+
| `GET /status` | 配置、开关、体检结论 |
|
|
105
|
+
|
|
106
|
+
`POST` 一律要求人标记:请求头 `x-card-dispatch-human: true` 或 body 里 `human: true`。
|
|
107
|
+
没有这个标记就是 400——**这条路只能人走**,模型和脚本不该替人按「开跑」。
|
|
108
|
+
|
|
109
|
+
## 4. 配置
|
|
110
|
+
|
|
111
|
+
配置只有一份真源:`src/config.js` 的声明表。面板、`/config` 路由、`cordis.patch.yml` 都按它收敛;
|
|
112
|
+
旧版键(`budgetPerDay`、`autoProvision.permission`)仍然认得,非法值退回缺省并记一条 note。
|
|
113
|
+
|
|
114
|
+
| 键 | 缺省 | 作用 | 插件能真拦吗 |
|
|
115
|
+
|---|---|---|---|
|
|
116
|
+
| `enabled` | `false` | 总开关;关掉只拆卡、不返回运行信封 | 能 |
|
|
117
|
+
| `permission` | `workspace-write` | 卡片权限预设(写进每个 create 信封) | 能 |
|
|
118
|
+
| `dataDir` | `.card-dispatch` | 插件的数据目录(配置、结论文件) | — |
|
|
119
|
+
| `gates.requirePlanApproval` | `true` | 没批准的拆分不给运行信封 | 能(「批准」= 人点「写入并运行」) |
|
|
120
|
+
| `gates.requireEvidence` | `true` | 没有 commit / 报告不算完成(团队开着时验收卡也在同一条会话里) | 能 |
|
|
121
|
+
| `gates.budgetPerDay` | `200` | 每日运行次数上限 | 能,需调用方给 `runsToday` |
|
|
122
|
+
| `gates.maxParallelRuns` | `3` | 同时在跑的会话上限 | 能,需调用方给 `runningSessions` |
|
|
123
|
+
| `gates.maxReworkRounds` | `2` | 打回上限,第 3 次升级给人(基线 R3) | 能 |
|
|
124
|
+
| `models.useModel` | `true` | 解析与判据草稿用不用模型;关掉只用规则(点一次才计费) | 是 |
|
|
125
|
+
| `models.draftRoute` | 空 | 全局模型:解析并拆分、判据草稿、写卡提示词都用它(右栏下拉写的就是这一项),留空 = 宿主默认 | 是 |
|
|
126
|
+
| `org.departments` | `dev/verify/integrate` | 部门名(写进卡片描述,供人读) | 只记录 |
|
|
127
|
+
| `execution.driver` | `task-board` | 执行层驱动(只有看板一条路) | 枚举校验 |
|
|
128
|
+
| `execution.teamRun` | `true` | 智能体团队:开了才按判据拆子卡(父卡带 `teamRun: true`,看板只起一个 Lead 会话,子卡由它派 teammate);关掉不拆子卡、每张卡各一条会话 | 能(决定拆不拆 + 写进 create 信封) |
|
|
129
|
+
| `audit.orgChart` | `true` | 拓扑稽查:事后核对有没有人绕开主管 | 只检查,不拦 |
|
|
130
|
+
| `org.depthRequired` | `2` | 主管带人所需的委派深度 | **不能**,只体检提示 |
|
|
131
|
+
|
|
132
|
+
**优先级:`<dataDir>/config.json` 优先于部署配置。** 面板保存的是归一化后的**完整**配置,
|
|
133
|
+
所以不会出现半份配置的中间态;想回到部署值就删掉这个文件(`/status` 会给出路径)。
|
|
134
|
+
|
|
135
|
+
三个闸都是纯函数(`src/gates.js`):同样的输入永远同样答案,因此能逐条单测,也能在页面上先算给人看。
|
|
136
|
+
`/split` 用 `evaluateStart` 决定给不给运行信封——**写卡不花钱,运行才花钱**,闸门只卡后者。
|
|
137
|
+
|
|
138
|
+
## 5. 看板信封的字段约束(踩过就回不来)
|
|
139
|
+
|
|
140
|
+
看板对 action 做 `exactKeys` 校验,**多一个字段就是 400**:
|
|
141
|
+
|
|
142
|
+
- `create` 只认 `{kind, title, description, prompt, workspaceId?, mode?, permission?, parentId?, teamRun?}`;
|
|
143
|
+
`null` 也要整个省掉,不能写 `workspaceId: null`。
|
|
144
|
+
- `teamRun` 只写在**父卡**上。子卡再带一次,看板会给子卡也起一个 Lead,等于开了两套班子。
|
|
145
|
+
- `run` 是 `{kind:'run', taskId}`;`delete` 拒删**有子卡**的卡(`task has subtasks; detach or delete them first`),
|
|
146
|
+
也拒删正在跑的卡;要整棵子树消失用 `archive`(可恢复)。
|
|
147
|
+
- 插件进程调 `/state` 是 403:看板有信任栅栏(`sec-fetch-site` / `Origin` / `dsh-auth-` cookie)。
|
|
148
|
+
所以插件只**生成信封**,真正的请求由**页面**同源发出(浏览器的标记天然通过)。
|
|
149
|
+
|
|
150
|
+
## 6. 拆分策略
|
|
151
|
+
|
|
152
|
+
| 情况 | 产出 | 会话数 |
|
|
153
|
+
|---|---|---|
|
|
154
|
+
| 判据 0 条 | 1 张父卡 + 警告「执行者不知道做到什么算完」 | 1 |
|
|
155
|
+
| 判据 1 条 | 1 张父卡(不拆——拆多了全是计费) | 1 |
|
|
156
|
+
| **没开智能体团队**(`execution.teamRun: false`) | 不拆子卡,判据全写进一张卡 | 每张卡各 1 条 |
|
|
157
|
+
| 判据 ≥2 条 + 开了团队 + 勾了并行 | 父卡 + 每判据组 1 张子卡(子卡由父卡的 Lead 在**同一个**会话里派成 teammate) | **1** |
|
|
158
|
+
| 判据 ≥2 条但没勾并行 | 1 张父卡,全部判据写进去 | 1 |
|
|
159
|
+
| 交付门槛开着(`gates.requireEvidence`) | 上面任一种 + **1 张验收卡** | 团队开:仍是 1;团队关:+1 |
|
|
160
|
+
|
|
161
|
+
**卡数 ≠ 会话数**。团队开着时 5 张卡(父卡 + 3 子卡 + 验收卡)都在同一个 Lead 会话里跑,
|
|
162
|
+
只算 1 条会话;关掉团队时每张卡各开一条,所以「1 张开发卡 + 1 张验收卡」是 2 条。
|
|
163
|
+
预览里的警告会把这次到底是 1 条还是 N 条直接写出来。
|
|
164
|
+
|
|
165
|
+
拆不拆子卡只看**跑法**:团队开着才拆。页面跟着联动——团队没勾时
|
|
166
|
+
「按判据拆成并行子卡」直接禁用;勾一下就存盘并重算预览,不用先点「保存」才发现会话条数变了。
|
|
167
|
+
|
|
168
|
+
**子卡是并行的**:看板没有「卡片间依赖」字段。所以每张子卡的 prompt 自带它那组判据
|
|
169
|
+
和「怎么算完」,不靠顺序;要严格串行就别勾并行。
|
|
170
|
+
|
|
171
|
+
**验收卡为什么是另一张卡**:基线 §3.3 要求验证独立于开发——同一张卡自己说「我做完了」
|
|
172
|
+
不算验收。所以门槛开着时,插件会把交付要求写进每张开发卡(报告路径写死成
|
|
173
|
+
`out/<该卡 id>/report.md`,附 commit sha),再生成一张只读证据的验收卡:
|
|
174
|
+
结论只能是 PASS 或 REJECT,REJECT 必须带 `{criteriaId, expected, actual, evidencePath}`。
|
|
175
|
+
顺序(先开发、后验收、打回最多两次)写在父卡 prompt 里——teamRun 模式下父卡就是 Lead,
|
|
176
|
+
由它守;平台排不出这个顺序,因为看板没有依赖字段。
|
|
177
|
+
验收卡和团队开关无关:门槛开着就有;代价只是没开团队时它多占一条会话,预览会写明。
|
|
178
|
+
|
|
179
|
+
## 7. 已验证 / 未验证
|
|
180
|
+
|
|
181
|
+
已验证(真机,2026-10-22,在运行中的看板页里直接发请求):
|
|
182
|
+
|
|
183
|
+
| 动作 | 结果 |
|
|
184
|
+
|---|---|
|
|
185
|
+
| `GET /api/task-board/state` | 200,`sessionDefaultPermission: workspace-write` |
|
|
186
|
+
| `create` 单卡 | 200,卡片出现 `status: todo` |
|
|
187
|
+
| `create` 父卡 + 子卡(`parentId`) | 200,子卡正确挂到父卡下 |
|
|
188
|
+
| `delete` | 200,卡片移除,看板回到干净状态 |
|
|
189
|
+
| `delete` 有子卡的父卡 | 400 `task has subtasks; detach or delete them first`(所以要按子树删/归档) |
|
|
190
|
+
|
|
191
|
+
**AI 解析并拆分**(2026-10-24,真机点过一次,用的是宿主里配的 `deepseek-v4.1-flash`):
|
|
192
|
+
一段「做一个可视化计算器…」的原文,返回 5 条判据,页面自动拆成 6 张卡(父卡 + 4 子卡 + 验收卡),
|
|
193
|
+
预览里逐张列出提示词。`/plan` 的两段提示词(解析、写卡)在同一轮里各调一次模型。
|
|
194
|
+
模型不可用或关掉 `models.useModel` 时退回「格式规则解析 + 模板提示词」,并在提示里说明。
|
|
195
|
+
|
|
196
|
+
**未验证**:`run` 的会话侧效果(真的开出一个会话并跑完)——那会真开会话计费,
|
|
197
|
+
没有替你跑。第一次点运行时请留意看板是否解析出工作区与预设。
|
|
198
|
+
`teamRun` 写进 create 信封后看板是否真的只起一个 Lead、把子卡派成 teammate,同样没跑过;
|
|
199
|
+
看板自己的 `teamRunAvailable` 与「Agent Teams 服务缺失就拒绝运行」两道检查仍在它那边。
|
|
200
|
+
|
|
201
|
+
**交付门槛现在到哪一步**:证据要求已写进每张开发卡的 prompt,验收卡独立存在、只给
|
|
202
|
+
PASS/REJECT 并把结论落到 `out/<根卡 id>/verdict.json`——「证据从哪来、谁来验、结论放哪」都解决了。
|
|
203
|
+
|
|
204
|
+
「打回之后谁重跑」现在有一个**人触发**的答案:`POST /rework` 读那个文件,按
|
|
205
|
+
`gates.maxReworkRounds` 给出三种结局——`pass` 收尾、`rework` 返回重跑被拒卡片的 run 信封、
|
|
206
|
+
`escalate` 停下来交给人(基线 R3)。没有轮询、没有定时器:人点一下才跑一次判定。
|
|
207
|
+
|
|
208
|
+
为什么判定靠文件而不是看板状态:`GET /api/task-board/state` 走看板自己的信任栅栏,
|
|
209
|
+
插件进程没有那张 cookie,**实测 403**;而且 `ExecutionRecord.result` 只记「执行成没成」,
|
|
210
|
+
不记「验收判 PASS 还是 REJECT」。落盘的文件三方都能读,也才符合基线 P3。
|
|
211
|
+
|
|
212
|
+
**还没做**:预算真正计数(需要调用方给 `runsToday` / `runningSessions`)、worktree 隔离、
|
|
213
|
+
逐手台账、`skills/card-dispatch-org/SKILL.md` 的安装。
|
|
214
|
+
|
|
215
|
+
## 8. 测试
|
|
216
|
+
|
|
217
|
+
```powershell
|
|
218
|
+
node test\run-all.mjs # 155 个用例,全绿
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
十五个文件:`config`(收敛/兼容/合并)、`gates`(三个闸的判定顺序与理由)、
|
|
222
|
+
`selfcheck`(体检判据)、`split`(拆分、信封、智能体团队、提示词覆盖)、`status`(所有路由)、
|
|
223
|
+
`verdict`(结论文件契约)、`import`(文档解析)、`criteria`(规则提炼)、`models`(模型清单扫描)、
|
|
224
|
+
`criteria-llm`(判据草稿的模型通道)、`parse-llm`(整段需求解析)、`prompts-llm`(按需求写每张卡的提示词)、
|
|
225
|
+
`human-channel`(路由 + 两道门 + 依赖透传 + 闸门降级)、`console`(页面 id / 看板基址 / 旧机制痕迹清零)、
|
|
226
|
+
`index`(导出形状 / 旧配置兼容 / apply 只挂路由 / 端到端打一次 /models)。
|
|
227
|
+
|
|
228
|
+
模型相关的用例全部用假的 `llm` 服务:**不在测试里花真钱**。真模型的验证靠人点一次、
|
|
229
|
+
看页面回的 `source: 'model'` 和实际文案。
|
|
230
|
+
|
|
231
|
+
## 9. 文件
|
|
232
|
+
|
|
233
|
+
| 文件 | 行数 | 作用 |
|
|
234
|
+
|---|---|---|
|
|
235
|
+
| `index.js` | ~90 | cordis 入口:`apply` 只注册路由,注入 `models` 依赖 |
|
|
236
|
+
| `src/human-channel.js` | ~800 | 十一条路由、两道门(人标记 + 配置闸)、依赖透传 |
|
|
237
|
+
| `src/split.js` | ~390 | 需求 → 卡片计划(唯一有「业务判断」的地方,纯函数) |
|
|
238
|
+
| `src/gates.js` | ~120 | 三个闸:计划、预算、并发、打回次数 |
|
|
239
|
+
| `src/config.js` | ~230 | 声明表 + 归一化 + 兼容旧键 |
|
|
240
|
+
| `src/config-store.js` | ~90 | 配置读写(`<dataDir>/config.json` 优先) |
|
|
241
|
+
| `src/selfcheck.js` | ~200 | 体检:看板在不在、模型清单、权限、委派深度 |
|
|
242
|
+
| `src/catalog-source.js` | ~70 | 从宿主的 `cordis.patch.yml` 扫出可选模型 |
|
|
243
|
+
| `src/model-call.js` | ~120 | 模型调用与 JSON 抽取(一次点击一次调用,45 秒超时) |
|
|
244
|
+
| `src/criteria.js` | ~90 | 规则提炼判据(无模型时的退路) |
|
|
245
|
+
| `src/criteria-llm.js` | ~125 | 判据草稿的模型通道 |
|
|
246
|
+
| `src/parse-llm.js` | ~120 | 整段需求 → 标题 / 描述 / 判据:提示词 + 回复解析 |
|
|
247
|
+
| `src/prompts-llm.js` | ~140 | 按需求给每张卡写提示词开头(结构不归它管) |
|
|
248
|
+
| `src/import.js` | ~150 | `.md` / `.txt` / `.json` → 标题 / 描述 / 判据 |
|
|
249
|
+
| `src/verdict.js` | ~120 | `verdict.json` 的读写与形状校验 |
|
|
250
|
+
| `src/console.html` | ~950 | 控制台页面(单文件,无构建) |
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
- insert:
|
|
2
|
+
- id: card-dispatch
|
|
3
|
+
name: 'dsh-card-dispatch'
|
|
4
|
+
config:
|
|
5
|
+
# 总开关:false 时只做「拆分 + 填卡」,不开任何会话、不产生费用。
|
|
6
|
+
enabled: false
|
|
7
|
+
# 新卡片钉住的权限预设;null 表示不改动(沿用看板默认)。
|
|
8
|
+
permission: workspace-write
|
|
9
|
+
# 数据目录(相对宿主 cwd;桌面端 cwd 是 profile 目录)。
|
|
10
|
+
# 面板改的开关会落到 <dataDir>/config.json,且**文件优先于本文件**。
|
|
11
|
+
dataDir: .card-dispatch
|
|
12
|
+
gates:
|
|
13
|
+
requirePlanApproval: true # DAG 未经人确认不开跑(计划闸)
|
|
14
|
+
budgetPerDay: 200 # 每日运行次数预算(D10)
|
|
15
|
+
maxParallelRuns: 3 # 同时在跑的会话上限
|
|
16
|
+
maxReworkRounds: 2 # 打回上限,第 3 次升级给人(基线 R3)
|
|
17
|
+
requireEvidence: true # 没有 commit / 报告不算完成(基线 P4)
|
|
18
|
+
org:
|
|
19
|
+
departments: [dev, verify, integrate] # 基线 §12:第一版三个职能
|
|
20
|
+
depthRequired: 2 # 主管要带人所需的委派深度(插件改不了,只体检)
|
|
21
|
+
execution:
|
|
22
|
+
driver: task-board # 执行层驱动:任务看板(D-15)
|
|
23
|
+
teamRun: true # 父卡起 Lead、子卡起 teammate
|
|
24
|
+
audit:
|
|
25
|
+
orgChart: true # 拓扑事后稽查(人员越权在平台上已不可能)
|
package/icon.svg
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" role="img" aria-label="Unattended Dev Ledger">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="ud-bg" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
+
<stop offset="0" stop-color="#3b6fd4"/>
|
|
5
|
+
<stop offset="1" stop-color="#1e3a7a"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
<rect x="4" y="4" width="56" height="56" rx="14" fill="url(#ud-bg)"/>
|
|
9
|
+
<g fill="none" stroke="#ffffff" stroke-width="2.4" stroke-linecap="round">
|
|
10
|
+
<path d="M18 20h28"/>
|
|
11
|
+
<path d="M18 32h28"/>
|
|
12
|
+
<path d="M18 44h18"/>
|
|
13
|
+
</g>
|
|
14
|
+
<g fill="#8ec5ff">
|
|
15
|
+
<circle cx="24" cy="20" r="3.2"/>
|
|
16
|
+
<circle cx="33" cy="32" r="3.2"/>
|
|
17
|
+
<circle cx="21" cy="44" r="3.2"/>
|
|
18
|
+
</g>
|
|
19
|
+
<circle cx="47" cy="43" r="7.5" fill="none" stroke="#ffffff" stroke-width="2.6"/>
|
|
20
|
+
<path d="M52.4 48.4 57 53" stroke="#ffffff" stroke-width="2.6" stroke-linecap="round"/>
|
|
21
|
+
</svg>
|
package/index.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 插件入口:**控制面**(配置 / 闸门 / 体检),执行面交给任务看板。
|
|
3
|
+
*
|
|
4
|
+
* 历史(偏差 D-14 / D-15):这里曾经装着台账、20 行迁移规则、24 个模型工具、看门狗、
|
|
5
|
+
* 自动调度和自动开会话。真机验证后确认两件事:
|
|
6
|
+
*
|
|
7
|
+
* 1. 宿主不授权插件直调会话控制器(`sessionController` 是网关客户端,缺 peer 抛
|
|
8
|
+
* `throwIfAborted` / `truncatedAt`),自己开会话这条路走不通;
|
|
9
|
+
* 2. 任务看板(`@linxin666/dsh-client-ui-task-board`)已经把「建卡 → 钉工作区/预设/
|
|
10
|
+
* 权限 → 开会话 → 跑 → 结算」做通了,且暴露 `POST /api/task-board/action`。
|
|
11
|
+
*
|
|
12
|
+
* 所以本插件的定位是:**平台没有的那三件事**——闸门、体检、人入口;执行一律外移。
|
|
13
|
+
* 反过来,平台已经保证的事(谁能给谁发消息、谁指挥谁、委派深度、计费)**不写**,
|
|
14
|
+
* 写进插件只会造成「以为拦住了」的错觉。
|
|
15
|
+
*
|
|
16
|
+
* 关于 inject:一个都不声明。`webServer` 由 human-channel 用 `ctx.inject(['webServer'], …)`
|
|
17
|
+
* 惰性获取——纯 CLI profile 里没有 webServer,硬依赖会让插件装不上(偏差 D-9)。
|
|
18
|
+
* 同样不导出 `export const Config`(有意避开 schemastery 依赖,偏差 D-7)。
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { registerHumanChannel, HUMAN_ROUTE_PREFIX } from './src/human-channel.js'
|
|
22
|
+
import { normalizeConfig, normalizeConfigDetailed } from './src/config.js'
|
|
23
|
+
import { createConfigStore } from './src/config-store.js'
|
|
24
|
+
import { loadCatalog } from './src/catalog-source.js'
|
|
25
|
+
|
|
26
|
+
export const name = 'card-dispatch'
|
|
27
|
+
|
|
28
|
+
// 测试与面板共用同一份收敛逻辑,避免「插件里一套、面板里一套」
|
|
29
|
+
export { normalizeConfig, normalizeConfigDetailed }
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 取配置:**面板保存的文件优先于部署配置**。
|
|
33
|
+
*
|
|
34
|
+
* 理由:面板上的开关是「人此刻的意图」,重启后不该悄悄回退;而部署配置是安装时的
|
|
35
|
+
* 意图。文件里存的永远是归一化后的**完整**配置,所以不会出现半份配置的中间态。
|
|
36
|
+
* 想让部署配置重新生效,删掉 `dataDir/config.json` 即可(面板会把路径给出来)。
|
|
37
|
+
*
|
|
38
|
+
* @returns {{config: object, notes: string[], source: 'file'|'deploy', store: object, error: string|null}}
|
|
39
|
+
*/
|
|
40
|
+
function resolveConfig(rawConfig) {
|
|
41
|
+
const deployed = normalizeConfigDetailed(rawConfig)
|
|
42
|
+
const store = createConfigStore({ dir: deployed.config.dataDir })
|
|
43
|
+
const loaded = store.load()
|
|
44
|
+
const hasFile = loaded.error === null && Object.keys(loaded.config).length > 0
|
|
45
|
+
if (!hasFile) {
|
|
46
|
+
return { config: deployed.config, notes: deployed.notes, source: 'deploy', store, error: loaded.error }
|
|
47
|
+
}
|
|
48
|
+
const fromFile = normalizeConfigDetailed(loaded.config)
|
|
49
|
+
return { config: fromFile.config, notes: fromFile.notes, source: 'file', store, error: null }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function apply(ctx, rawConfig = {}) {
|
|
53
|
+
const { config, notes, source, store, error } = resolveConfig(rawConfig)
|
|
54
|
+
const logger = typeof ctx.get === 'function' ? ctx.get('logger') : null
|
|
55
|
+
|
|
56
|
+
logger?.info?.(
|
|
57
|
+
`[card-dispatch] 已装载:控制台 ${HUMAN_ROUTE_PREFIX}(需求拆分 → 任务看板);` +
|
|
58
|
+
`enabled=${config.enabled};配置来源=${source === 'file' ? store.file : '部署配置'}`,
|
|
59
|
+
)
|
|
60
|
+
if (error !== null) logger?.warn?.(`[card-dispatch] ${error}(已退回部署配置)`)
|
|
61
|
+
for (const note of notes) logger?.warn?.(`[card-dispatch] ${note}`)
|
|
62
|
+
|
|
63
|
+
const dispose = registerHumanChannel(ctx, {
|
|
64
|
+
config,
|
|
65
|
+
store,
|
|
66
|
+
// 插件读不到部署的子智能体深度,也探测不了看板;两者都交给 /status 的体检去问人。
|
|
67
|
+
facts: () => ({ subagentDepthMax: null, boardReachable: null, boardTeamRun: null }),
|
|
68
|
+
/* 模型接口用宿主已经配好的那份:`llm` 是官方服务,模型清单读 profile 的
|
|
69
|
+
cordis.patch.yml。拿不到就返回 null,页面自动退回规则提炼。 */
|
|
70
|
+
models: {
|
|
71
|
+
llm: () => {
|
|
72
|
+
try {
|
|
73
|
+
return typeof ctx.get === 'function' ? ctx.get('llm') ?? null : null
|
|
74
|
+
} catch {
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
catalog: () => loadCatalog(),
|
|
79
|
+
},
|
|
80
|
+
})
|
|
81
|
+
return () => {
|
|
82
|
+
try {
|
|
83
|
+
dispose?.()
|
|
84
|
+
} catch {
|
|
85
|
+
// 卸载失败不影响宿主回收 fiber
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,58 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-card-dispatch",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "派卡台开发的上游:把需求拆成任务看板卡片(执行交给 dsh-client-ui-task-board)",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"dsh",
|
|
8
|
+
"deepseek-harness",
|
|
9
|
+
"dsh-plugin",
|
|
10
|
+
"task-board",
|
|
11
|
+
"card-dispatch",
|
|
12
|
+
"requirement-split",
|
|
13
|
+
"prompt"
|
|
14
|
+
],
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"author": "yangchaoran",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://gitee.com/past-events-sifenruwu/dsh-card-dispatch.git"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://gitee.com/past-events-sifenruwu/dsh-card-dispatch#readme",
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://gitee.com/past-events-sifenruwu/dsh-card-dispatch/issues"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18"
|
|
27
|
+
},
|
|
28
|
+
"exports": {
|
|
29
|
+
".": "./index.js",
|
|
30
|
+
"./client": "./src/client/index.js",
|
|
31
|
+
"./locale/*.json": "./locale/*.json",
|
|
32
|
+
"./icon": "./icon.svg"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"index.js",
|
|
36
|
+
"src",
|
|
37
|
+
"cordis.patch.yml",
|
|
38
|
+
"locale/*.json",
|
|
39
|
+
"icon.svg",
|
|
40
|
+
"README.md"
|
|
41
|
+
],
|
|
42
|
+
"scripts": {
|
|
43
|
+
"test": "node test/run-all.mjs",
|
|
44
|
+
"test:node": "node --test test/"
|
|
45
|
+
},
|
|
46
|
+
"dsh": {
|
|
47
|
+
"bundle": {
|
|
48
|
+
"patch": "./cordis.patch.yml"
|
|
49
|
+
},
|
|
50
|
+
"client": {
|
|
51
|
+
"platform": "web",
|
|
52
|
+
"immediately": true,
|
|
53
|
+
"inject": [
|
|
54
|
+
"@deepseek-ai/dsh-client-ui-slots"
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* catalog-source.js:把「宿主已添加的模型」从磁盘读出来。
|
|
3
|
+
*
|
|
4
|
+
* 真相在 profile 的 `cordis.patch.yml`(provider 插件把 models 写在那里)。
|
|
5
|
+
* 本文件负责找文件、读文件;解析优先用 `yaml` 包,解析不到就退回 models.js
|
|
6
|
+
* 里的纯文本扫描(插件是 junction 进来的,yaml 不一定解析得到)。
|
|
7
|
+
*
|
|
8
|
+
* 缓存按 mtime:设置里加了模型,刷新页面就能看到,不用重启。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs'
|
|
12
|
+
import { homedir } from 'node:os'
|
|
13
|
+
import { join } from 'node:path'
|
|
14
|
+
|
|
15
|
+
import { buildCatalog, scanProviders } from './models.js'
|
|
16
|
+
|
|
17
|
+
/** 进程内缓存:同一次运行里反复点「生成」不该反复读盘。 */
|
|
18
|
+
let cache = { file: null, mtimeMs: 0, catalog: null, error: null }
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 找出该读哪个 profile 的配置。
|
|
22
|
+
*
|
|
23
|
+
* 机器上可能有好几个 profile(desktop / personwork…),插件不知道宿主跑的是哪个。
|
|
24
|
+
* 规则:**先只考虑声明了本插件的**(那才是宿主真在用的),再取最近改过的;
|
|
25
|
+
* 时间一样就优先叫 desktop 的(桌面端默认 profile),否则按名字排——保证确定性。
|
|
26
|
+
*
|
|
27
|
+
* @param {object} [options]
|
|
28
|
+
* @param {string} [options.home] 用户目录(测试里可替换)
|
|
29
|
+
* @returns {string|null}
|
|
30
|
+
*/
|
|
31
|
+
export function findProfileConfig({ home = homedir() } = {}) {
|
|
32
|
+
const root = join(home, '.dsh', 'profiles')
|
|
33
|
+
let names = []
|
|
34
|
+
try {
|
|
35
|
+
names = readdirSync(root, { withFileTypes: true })
|
|
36
|
+
.filter((entry) => entry.isDirectory())
|
|
37
|
+
.map((entry) => entry.name)
|
|
38
|
+
.sort()
|
|
39
|
+
} catch {
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const candidates = []
|
|
44
|
+
for (const name of names) {
|
|
45
|
+
const file = join(root, name, 'cordis.patch.yml')
|
|
46
|
+
try {
|
|
47
|
+
const stat = statSync(file)
|
|
48
|
+
let mentions = false
|
|
49
|
+
try {
|
|
50
|
+
mentions = readFileSync(file, 'utf8').includes('card-dispatch')
|
|
51
|
+
} catch {
|
|
52
|
+
mentions = false
|
|
53
|
+
}
|
|
54
|
+
candidates.push({ file, name, mtimeMs: stat.mtimeMs, mentions })
|
|
55
|
+
} catch {
|
|
56
|
+
/* 这个 profile 没有配置文件,跳过 */
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (candidates.length === 0) return null
|
|
60
|
+
|
|
61
|
+
const preferred = candidates.filter((item) => item.mentions)
|
|
62
|
+
const pool = preferred.length > 0 ? preferred : candidates
|
|
63
|
+
pool.sort((a, b) => {
|
|
64
|
+
if (b.mtimeMs !== a.mtimeMs) return b.mtimeMs - a.mtimeMs
|
|
65
|
+
if (a.name === 'desktop') return -1
|
|
66
|
+
if (b.name === 'desktop') return 1
|
|
67
|
+
return a.name < b.name ? -1 : 1
|
|
68
|
+
})
|
|
69
|
+
return pool[0].file
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 读模型目录(带 mtime 缓存)。
|
|
74
|
+
*
|
|
75
|
+
* @param {object} [options]
|
|
76
|
+
* @param {string} [options.home]
|
|
77
|
+
* @param {string} [options.file] 直接指定配置文件(测试用)
|
|
78
|
+
* @returns {Promise<{file:string|null, catalog:{default:string|null, groups:Array}, error:string|null}>}
|
|
79
|
+
*/
|
|
80
|
+
export async function loadCatalog({ home, file } = {}) {
|
|
81
|
+
const target = file ?? findProfileConfig(home === undefined ? {} : { home })
|
|
82
|
+
if (target === null) {
|
|
83
|
+
return { file: null, catalog: { default: null, groups: [] }, error: '没找到 profile 的 cordis.patch.yml' }
|
|
84
|
+
}
|
|
85
|
+
let mtimeMs = 0
|
|
86
|
+
try {
|
|
87
|
+
mtimeMs = statSync(target).mtimeMs
|
|
88
|
+
} catch (error) {
|
|
89
|
+
return { file: target, catalog: { default: null, groups: [] }, error: `读不到配置:${error.message}` }
|
|
90
|
+
}
|
|
91
|
+
if (cache.file === target && cache.mtimeMs === mtimeMs && cache.catalog !== null) {
|
|
92
|
+
return { file: target, catalog: cache.catalog, error: cache.error }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let text = ''
|
|
96
|
+
try {
|
|
97
|
+
text = readFileSync(target, 'utf8')
|
|
98
|
+
} catch (error) {
|
|
99
|
+
const catalog = { default: null, groups: [] }
|
|
100
|
+
cache = { file: target, mtimeMs, catalog, error: `读不到配置:${error.message}` }
|
|
101
|
+
return { file: target, catalog, error: cache.error }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 先试 YAML 解析(结构最准);包不在或有语法错,都退回文本扫描
|
|
105
|
+
let catalog = null
|
|
106
|
+
let error = null
|
|
107
|
+
try {
|
|
108
|
+
const { parse } = await import('yaml')
|
|
109
|
+
catalog = buildCatalog(parse(text))
|
|
110
|
+
} catch (yamlError) {
|
|
111
|
+
try {
|
|
112
|
+
catalog = scanProviders(text)
|
|
113
|
+
} catch (scanError) {
|
|
114
|
+
catalog = { default: null, groups: [] }
|
|
115
|
+
error = `解析配置失败:${String(scanError?.message ?? scanError)}`
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (catalog.groups.length === 0 && error === null) error = '配置里没读到 providers(设置里还没添加模型?)'
|
|
119
|
+
|
|
120
|
+
cache = { file: target, mtimeMs, catalog, error }
|
|
121
|
+
return { file: target, catalog, error }
|
|
122
|
+
}
|