dsh-card-dispatch 0.0.0-stage → 0.1.1
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/CHANGELOG.md +41 -0
- package/LICENSE +21 -0
- package/README.md +276 -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 +57 -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 +1056 -0
- package/src/criteria-llm.js +79 -0
- package/src/criteria.js +102 -0
- package/src/gates.js +134 -0
- package/src/human-channel.js +900 -0
- package/src/import.js +193 -0
- package/src/model-call.js +106 -0
- package/src/models.js +249 -0
- package/src/parse-llm.js +96 -0
- package/src/prompts-llm.js +104 -0
- package/src/rework-store.js +69 -0
- package/src/selfcheck.js +117 -0
- package/src/split.js +549 -0
- package/src/verdict.js +133 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prompts-llm.js:让模型按需求给每张卡写「提示词开头」。
|
|
3
|
+
*
|
|
4
|
+
* 结构不由模型决定(拆几张、哪几条判据一组仍由 split.js 的规则算),模型只写
|
|
5
|
+
* 每张卡开头那段「目标 / 范围 / 明确不要做什么」。这样账单可预期,
|
|
6
|
+
* 而且「完成要交证据」这类硬要求仍由我们确定性地拼在后面,不会被模型写丢。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { callModel, extractJsonObject, MODEL_TIMEOUT_MS } from './model-call.js'
|
|
10
|
+
|
|
11
|
+
/** 一段开头最少多少字(太短就是没写)。 */
|
|
12
|
+
const MIN_INTRO = 12
|
|
13
|
+
|
|
14
|
+
/** 一段开头最多多少字(长了会挤掉判据与交付要求)。 */
|
|
15
|
+
const MAX_INTRO = 900
|
|
16
|
+
|
|
17
|
+
export const PROMPTS_SYSTEM = [
|
|
18
|
+
'你给任务卡写「提示词」的开头段:执行者会在一张新会话里读这段话干活,它看不到任何其它上下文。',
|
|
19
|
+
'只输出一个 JSON 对象,不要解释:{"cards":[{"key":"root","intro":"..."}]}',
|
|
20
|
+
'',
|
|
21
|
+
'每段 intro 写 3 到 6 行,依次说清:',
|
|
22
|
+
'- 目标:这一步要交付什么;',
|
|
23
|
+
'- 范围:只碰哪些文件 / 模块 / 场景;',
|
|
24
|
+
'- 不要做什么:最容易顺手做错的那一两件事。',
|
|
25
|
+
'',
|
|
26
|
+
'规则:',
|
|
27
|
+
'- key 必须原样使用给你的那一个,不要自己新增卡、不要合并卡。',
|
|
28
|
+
'- 每段都要自包含:不写「同上」「参见前面」,不引用别张卡才能看懂的话。',
|
|
29
|
+
'- 保留需求里出现的具体数字、型号、接口名;不要补需求没提的功能。',
|
|
30
|
+
'- 只写开头段,不要重复判据列表、不要写交付证据的要求(那些会由系统拼在后面)。',
|
|
31
|
+
'- 用需求本来使用的语言写。',
|
|
32
|
+
].join('\n')
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 把「需求原文 + 这张卡负责什么」整理成给模型的一次提问。
|
|
36
|
+
*
|
|
37
|
+
* @param {object} input
|
|
38
|
+
* @param {string} input.requirement 需求原文(标题 + 描述)
|
|
39
|
+
* @param {Array<{key:string,title:string,criteriaIds?:Array<string>,scope?:string}>} input.cards
|
|
40
|
+
* @returns {string}
|
|
41
|
+
*/
|
|
42
|
+
export function buildBriefing({ requirement = '', cards = [] }) {
|
|
43
|
+
const cardLines = cards.map((card) => {
|
|
44
|
+
const ids = (card.criteriaIds ?? []).join('、')
|
|
45
|
+
return [
|
|
46
|
+
`- key: ${card.key}`,
|
|
47
|
+
` 卡片标题:${card.title}`,
|
|
48
|
+
ids === '' ? ' 负责判据:无(这是协调 / 验卡)' : ` 负责判据:${ids}`,
|
|
49
|
+
card.scope === undefined ? '' : ` 说的范围:${card.scope}`,
|
|
50
|
+
].filter((line) => line !== '').join('\n')
|
|
51
|
+
})
|
|
52
|
+
return [`需求原文:\n${requirement.trim()}`, '要写开头段的卡:', cardLines.join('\n')].join('\n\n')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 解析模型回复:只要 key 认得、字数够的段落。
|
|
57
|
+
*
|
|
58
|
+
* @param {string} reply
|
|
59
|
+
* @param {Array<string>} [allowedKeys]
|
|
60
|
+
* @returns {Record<string,string>}
|
|
61
|
+
*/
|
|
62
|
+
export function parsePromptsReply(reply, allowedKeys) {
|
|
63
|
+
const payload = extractJsonObject(reply)
|
|
64
|
+
const raw = Array.isArray(payload?.cards) ? payload.cards : []
|
|
65
|
+
const allowed = Array.isArray(allowedKeys) && allowedKeys.length > 0 ? new Set(allowedKeys) : undefined
|
|
66
|
+
const out = {}
|
|
67
|
+
for (const item of raw) {
|
|
68
|
+
const key = String(item?.key ?? '').trim()
|
|
69
|
+
if (key === '') continue
|
|
70
|
+
if (allowed !== undefined && !allowed.has(key)) continue
|
|
71
|
+
if (Object.prototype.hasOwnProperty.call(out, key)) continue
|
|
72
|
+
const intro = String(item?.intro ?? '').replace(/\r/g, '').trim().slice(0, MAX_INTRO)
|
|
73
|
+
if (intro.length < MIN_INTRO) continue
|
|
74
|
+
out[key] = intro
|
|
75
|
+
}
|
|
76
|
+
return out
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 给这些卡写开头段。
|
|
81
|
+
*
|
|
82
|
+
* @param {object} input
|
|
83
|
+
* @param {any} input.llm
|
|
84
|
+
* @param {{provider:string, model:string}} input.route
|
|
85
|
+
* @param {string} input.requirement
|
|
86
|
+
* @param {Array<{key:string,title:string,criteriaIds?:Array<string>,scope?:string}>} input.cards
|
|
87
|
+
* @param {number} [input.timeoutMs]
|
|
88
|
+
* @returns {Promise<{ok:boolean, intros:Record<string,string>, error:string|null}>}
|
|
89
|
+
*/
|
|
90
|
+
export async function writeCardIntros({ llm, route, requirement = '', cards = [], timeoutMs = MODEL_TIMEOUT_MS } = {}) {
|
|
91
|
+
if (cards.length === 0) return { ok: false, intros: {}, error: '没有要写提示词的卡' }
|
|
92
|
+
const reply = await callModel({
|
|
93
|
+
llm,
|
|
94
|
+
route,
|
|
95
|
+
system: PROMPTS_SYSTEM,
|
|
96
|
+
text: buildBriefing({ requirement, cards }),
|
|
97
|
+
timeoutMs,
|
|
98
|
+
})
|
|
99
|
+
if (!reply.ok) return { ok: false, intros: {}, error: reply.error }
|
|
100
|
+
const intros = parsePromptsReply(reply.reply, cards.map((card) => card.key))
|
|
101
|
+
const written = Object.keys(intros).length
|
|
102
|
+
if (written === 0) return { ok: false, intros: {}, error: '模型没写出可用的提示词(回复不是约定的 JSON 或太短)' }
|
|
103
|
+
return { ok: true, intros, error: written < cards.length ? `有 ${cards.length - written} 张卡的提示词没写成,按模板补` : null }
|
|
104
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rework-store.js:把「这一批已经打回了几次」**落到磁盘**。
|
|
3
|
+
*
|
|
4
|
+
* 为什么不能只放页面内存:R3 要求「第三次打回必须升级给人」。以前计数只活在一个
|
|
5
|
+
* JS 变量里,刷新页面/重开面板就归零,等于这条上限可以被无限绕过。
|
|
6
|
+
*
|
|
7
|
+
* 存哪儿:`<dataDir>/rework.json`,按 `rootId` 记一个很小的对象。
|
|
8
|
+
* 和 config.json 一样:读失败当空、写失败要报错(写不进去就不能假装限流生效)。
|
|
9
|
+
*
|
|
10
|
+
* @module dsh-card-dispatch/rework-store
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
14
|
+
import { join, resolve } from 'node:path'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {object} options
|
|
18
|
+
* @param {string} options.dir dataDir(相对宿主 cwd,和 config.json 同一个目录)
|
|
19
|
+
*/
|
|
20
|
+
export function createReworkStore({ dir }) {
|
|
21
|
+
const root = resolve(dir)
|
|
22
|
+
const file = join(root, 'rework.json')
|
|
23
|
+
|
|
24
|
+
/** @returns {Record<string, {count:number, updatedAt:string}>} */
|
|
25
|
+
function loadAll() {
|
|
26
|
+
try {
|
|
27
|
+
const parsed = JSON.parse(readFileSync(file, 'utf8'))
|
|
28
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) return {}
|
|
29
|
+
return parsed
|
|
30
|
+
} catch {
|
|
31
|
+
// 文件不存在 / 被手改成半截 JSON:都当「没有记录」,不能因此让面板挂掉
|
|
32
|
+
return {}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 这一批已经打回了几次。
|
|
38
|
+
*
|
|
39
|
+
* @param {string} rootId
|
|
40
|
+
* @returns {number}
|
|
41
|
+
*/
|
|
42
|
+
function get(rootId) {
|
|
43
|
+
const entry = loadAll()[rootId]
|
|
44
|
+
const count = Number(entry?.count)
|
|
45
|
+
return Number.isFinite(count) && count > 0 ? Math.trunc(count) : 0
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 写回计数;0 表示清掉记录(验收通过)。
|
|
50
|
+
*
|
|
51
|
+
* @param {string} rootId
|
|
52
|
+
* @param {number} count
|
|
53
|
+
* @returns {number}
|
|
54
|
+
*/
|
|
55
|
+
function set(rootId, count) {
|
|
56
|
+
const value = Math.max(0, Math.trunc(Number(count) || 0))
|
|
57
|
+
const all = loadAll()
|
|
58
|
+
if (value === 0) {
|
|
59
|
+
delete all[rootId]
|
|
60
|
+
} else {
|
|
61
|
+
all[rootId] = { count: value, updatedAt: new Date().toISOString() }
|
|
62
|
+
}
|
|
63
|
+
mkdirSync(root, { recursive: true })
|
|
64
|
+
writeFileSync(file, `${JSON.stringify(all, null, 2)}\n`, 'utf8')
|
|
65
|
+
return value
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { file, get, set }
|
|
69
|
+
}
|
package/src/selfcheck.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* selfcheck.js:开机体检,纯函数。
|
|
3
|
+
*
|
|
4
|
+
* 为什么要体检而不是「自动纠正」:插件改不了部署设置(委派深度、权限预设、计费),
|
|
5
|
+
* 偷偷改配置只会让人以为组织图能跑、实际跑到一半才炸。所以这里的产出是**结论 + 一句
|
|
6
|
+
* 该怎么办**,由面板和启动日志展示。
|
|
7
|
+
*
|
|
8
|
+
* 判据只有一条:**fatal 必须为零**。warn 是「能跑但会难受」,info 是「知道了就行」。
|
|
9
|
+
*
|
|
10
|
+
* facts 全部由调用方注入(不在这里读盘/读宿主),这样每条分支都能被单测锁住。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {object} Facts
|
|
15
|
+
* @property {number|null} [subagentDepthMax] 部署允许的最大子智能体递归深度;未知传 null
|
|
16
|
+
* @property {boolean|null} [boardReachable] 任务看板 HTTP 是否可达;未知传 null
|
|
17
|
+
* @property {boolean|null} [boardTeamRun] 看板是否支持 teamRun 卡片(父卡起 Lead)
|
|
18
|
+
* @property {number|null} [workspaceCount] 已知工作区数量
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const FINDINGS = Object.freeze({
|
|
22
|
+
'ORG-DEPTH': {
|
|
23
|
+
severity: 'fatal',
|
|
24
|
+
title: '委派深度不足,主管带不了人',
|
|
25
|
+
hint: '设置 → 子智能体 → 最大递归深度 改成 2(0 = 禁用、1 = 只有主 Agent 能派生)',
|
|
26
|
+
},
|
|
27
|
+
'ORG-DEPTH-UNKNOWN': {
|
|
28
|
+
severity: 'warn',
|
|
29
|
+
title: '委派深度未知',
|
|
30
|
+
hint: '到 设置 → 子智能体 确认「最大递归深度」是否为 2;插件读不到这个部署设置,只能提示',
|
|
31
|
+
},
|
|
32
|
+
'BOARD-UNREACHABLE': {
|
|
33
|
+
severity: 'warn',
|
|
34
|
+
title: '执行层(任务看板)不可达',
|
|
35
|
+
hint: '插件只产出计划与卡片;看板不可达时卡片发不出去,确认 @linxin666/dsh-client-ui-task-board 已启用',
|
|
36
|
+
},
|
|
37
|
+
'BOARD-UNKNOWN': {
|
|
38
|
+
severity: 'info',
|
|
39
|
+
title: '执行层可达性未检测',
|
|
40
|
+
hint: '在面板点一次「体检」即可拿到结果(会发一个只读请求,不花钱)',
|
|
41
|
+
},
|
|
42
|
+
'TEAMRUN-UNSUPPORTED': {
|
|
43
|
+
severity: 'warn',
|
|
44
|
+
title: '看板不支持 teamRun',
|
|
45
|
+
hint: '关闭 execution.teamRun,退化为普通级联;否则父卡不会起 Lead,主管层不存在',
|
|
46
|
+
},
|
|
47
|
+
'NO-WORKSPACE': {
|
|
48
|
+
severity: 'warn',
|
|
49
|
+
title: '没有指定工作区',
|
|
50
|
+
hint: '卡片不钉工作区时会回退到「最近改动的那个」,对自动跑卡是隐患;在面板里显式选一个',
|
|
51
|
+
},
|
|
52
|
+
'PLAN-GATE-OFF': {
|
|
53
|
+
severity: 'warn',
|
|
54
|
+
title: '计划闸关着但总开关开着',
|
|
55
|
+
hint: '还没批准的计划也会直接开跑 = 直接花钱;建议保持 gates.requirePlanApproval = true',
|
|
56
|
+
},
|
|
57
|
+
'EVIDENCE-OFF': {
|
|
58
|
+
severity: 'warn',
|
|
59
|
+
title: '交付门槛关着',
|
|
60
|
+
hint: '没有 commit / 报告也算完成,自动跑卡时无法复核;建议保持 gates.requireEvidence = true',
|
|
61
|
+
},
|
|
62
|
+
DISABLED: {
|
|
63
|
+
severity: 'info',
|
|
64
|
+
title: '总开关关闭',
|
|
65
|
+
hint: '当前只做拆分与填卡,不会开任何会话、不产生费用',
|
|
66
|
+
},
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 跑一遍体检。
|
|
71
|
+
*
|
|
72
|
+
* @param {object} input
|
|
73
|
+
* @param {object} input.config 归一化配置(决定「应该是什么样」)
|
|
74
|
+
* @param {Facts} [input.facts] 外部注入的事实(决定「实际是什么样」)
|
|
75
|
+
* @returns {{ok:boolean, findings:Array<object>}}
|
|
76
|
+
*/
|
|
77
|
+
export function runSelfCheck({ config, facts = {} } = {}) {
|
|
78
|
+
const findings = []
|
|
79
|
+
const add = (id, detail) => {
|
|
80
|
+
const base = FINDINGS[id]
|
|
81
|
+
if (!base) return
|
|
82
|
+
findings.push({ id, severity: base.severity, title: base.title, detail, hint: base.hint })
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const requiredDepth = config.org?.depthRequired ?? 2
|
|
86
|
+
const depth = facts.subagentDepthMax
|
|
87
|
+
if (depth === null || depth === undefined) {
|
|
88
|
+
add('ORG-DEPTH-UNKNOWN', `插件读不到部署的子智能体深度,按需要 ${requiredDepth} 层处理`)
|
|
89
|
+
} else if (Number(depth) < requiredDepth) {
|
|
90
|
+
add('ORG-DEPTH', `当前最大递归深度 ${depth},低于所需的 ${requiredDepth}:成员(主管)无法再派生部门人员`)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const board = facts.boardReachable
|
|
94
|
+
if (board === false) add('BOARD-UNREACHABLE', '调用 /api/task-board 失败')
|
|
95
|
+
else if (board === null || board === undefined) add('BOARD-UNKNOWN', '尚未探测')
|
|
96
|
+
|
|
97
|
+
if (facts.boardTeamRun === false && config.execution?.teamRun === true) {
|
|
98
|
+
add('TEAMRUN-UNSUPPORTED', '已要求 teamRun 卡片,但看板侧没有这个能力')
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (facts.workspaceCount === 0) {
|
|
102
|
+
add('NO-WORKSPACE', '工作区列表为空')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (config.enabled === true && config.gates?.requirePlanApproval !== true) {
|
|
106
|
+
add('PLAN-GATE-OFF', 'enabled=true 且 requirePlanApproval=false')
|
|
107
|
+
}
|
|
108
|
+
if (config.enabled === true && config.gates?.requireEvidence !== true) {
|
|
109
|
+
add('EVIDENCE-OFF', 'enabled=true 且 requireEvidence=false')
|
|
110
|
+
}
|
|
111
|
+
if (config.enabled !== true) {
|
|
112
|
+
add('DISABLED', 'enabled=false')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const ok = findings.every((item) => item.severity !== 'fatal')
|
|
116
|
+
return { ok, findings }
|
|
117
|
+
}
|