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,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* criteria-llm.js:让宿主已经配置好的模型写判据草稿。
|
|
3
|
+
*
|
|
4
|
+
* 调用的是官方 `llm` 服务(`ctx.get('llm')`),和任务看板「粘贴文本 → 解析成任务」
|
|
5
|
+
* 走同一条路;通道细节在 model-call.js,这里只负责提示词与回复解析。
|
|
6
|
+
* 不自己拼 HTTP、不碰 API Key——模型与密钥都由宿主管。
|
|
7
|
+
*
|
|
8
|
+
* 一次点击 = 一次很小的调用,会计费,所以页面必须把「用哪个模型」写出来。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { callModel, extractJsonObject, MODEL_TIMEOUT_MS } from './model-call.js'
|
|
12
|
+
|
|
13
|
+
/** 一次草稿最多等多久。超过就当没答上来,退回规则提炼。 */
|
|
14
|
+
export const DRAFT_TIMEOUT_MS = MODEL_TIMEOUT_MS
|
|
15
|
+
|
|
16
|
+
/** 一条判据最长多少字(模型爱写长句,页面里读起来很累)。 */
|
|
17
|
+
const ITEM_LIMIT = 120
|
|
18
|
+
|
|
19
|
+
/** 最多要几条。 */
|
|
20
|
+
const MAX_ITEMS = 8
|
|
21
|
+
|
|
22
|
+
/** 系统提示词。导出给测试与 README 引用,免得文档和代码说两套。 */
|
|
23
|
+
export const CRITERIA_SYSTEM = [
|
|
24
|
+
'你把一条需求改写成可验收的判据。',
|
|
25
|
+
'只输出一个 JSON 对象,不要任何解释:{"criteria":["...","..."]}',
|
|
26
|
+
'- criteria:3 到 8 条,每条一句话,是可判定真假的验收条件,不是任务步骤。',
|
|
27
|
+
'- 每条必须写清「什么情况下、看到什么」,能对上具体现象。',
|
|
28
|
+
'- 不要写「代码质量好」「测试通过」这种无法核对的话。',
|
|
29
|
+
'- 用需求本来的语言写。',
|
|
30
|
+
].join('\n')
|
|
31
|
+
|
|
32
|
+
/** 从模型回复里抠出判据(容忍代码围栏与前后废话)。 */
|
|
33
|
+
export function parseCriteriaReply(reply) {
|
|
34
|
+
const payload = extractJsonObject(reply)
|
|
35
|
+
if (payload === undefined || payload === null) return []
|
|
36
|
+
const raw = Array.isArray(payload.criteria) ? payload.criteria : []
|
|
37
|
+
const seen = new Set()
|
|
38
|
+
const out = []
|
|
39
|
+
for (const item of raw) {
|
|
40
|
+
const line = String(typeof item === 'string' ? item : item?.text ?? '')
|
|
41
|
+
.replace(/\s+/g, ' ')
|
|
42
|
+
.replace(/^[-*+]\s+/, '')
|
|
43
|
+
.trim()
|
|
44
|
+
.slice(0, ITEM_LIMIT)
|
|
45
|
+
if (line === '') continue
|
|
46
|
+
const key = line.replace(/[\s。;;,,::]/g, '')
|
|
47
|
+
if (seen.has(key)) continue
|
|
48
|
+
seen.add(key)
|
|
49
|
+
out.push(line)
|
|
50
|
+
if (out.length >= MAX_ITEMS) break
|
|
51
|
+
}
|
|
52
|
+
return out
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 让模型写判据草稿。
|
|
57
|
+
*
|
|
58
|
+
* @param {object} input
|
|
59
|
+
* @param {any} input.llm 宿主的 llm 服务(拿不到就传 null)
|
|
60
|
+
* @param {{provider:string, model:string}} input.route
|
|
61
|
+
* @param {string} input.title
|
|
62
|
+
* @param {string} input.description
|
|
63
|
+
* @param {number} [input.timeoutMs]
|
|
64
|
+
* @returns {Promise<{ok:boolean, criteria:Array<string>, error:string|null}>}
|
|
65
|
+
*/
|
|
66
|
+
export async function draftWithModel({ llm, route, title = '', description = '', timeoutMs = DRAFT_TIMEOUT_MS } = {}) {
|
|
67
|
+
const userText = [
|
|
68
|
+
title.trim() === '' ? '' : `需求标题:${title.trim()}`,
|
|
69
|
+
description.trim() === '' ? '' : `需求描述:\n${description.trim()}`,
|
|
70
|
+
].filter((line) => line !== '').join('\n\n')
|
|
71
|
+
if (userText === '') return { ok: false, criteria: [], error: '没有可拆的内容' }
|
|
72
|
+
|
|
73
|
+
const reply = await callModel({ llm, route, system: CRITERIA_SYSTEM, text: userText, timeoutMs })
|
|
74
|
+
if (!reply.ok) return { ok: false, criteria: [], error: reply.error }
|
|
75
|
+
|
|
76
|
+
const criteria = parseCriteriaReply(reply.reply)
|
|
77
|
+
if (criteria.length === 0) return { ok: false, criteria: [], error: '模型没给出可用的判据(回复不是约定的 JSON)' }
|
|
78
|
+
return { ok: true, criteria, error: null }
|
|
79
|
+
}
|
package/src/criteria.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* criteria.js:从标题与描述里提炼**判据草稿**(无模型时的退路)。
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么这里不调模型
|
|
5
|
+
*
|
|
6
|
+
* 模型通道在 `criteria-llm.js`(`ctx.get('llm')`)。这一层刻意只用规则:
|
|
7
|
+
* 模型不可用、开关关着、或者一次点击不值得多花一次调用时,仍然要能给出可编辑的草稿。
|
|
8
|
+
*
|
|
9
|
+
* 所以这里只做**提炼**:把描述里本来就写成要求的话捞出来,变成一行一条。
|
|
10
|
+
* 提炼不出来就说提炼不出来,并告诉人怎么写才好提炼——不编造判据。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** 像「要求」的句子:出现这些词才值得当判据。 */
|
|
14
|
+
const BEHAVIOR = /(要|需要|必须|支持|能|可以|不要|不能|不得|应该|允许|禁止|自动|提示|校验|保存|导出|显示|登录|上传|下载|计算|校验|报错)/
|
|
15
|
+
|
|
16
|
+
/** 列表项:`- x`、`* x`、`1. x`、`1) x`、`- [ ] x`。 */
|
|
17
|
+
const LIST_ITEM = /^\s*(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s*)?(.+?)\s*$/
|
|
18
|
+
|
|
19
|
+
const MAX_ITEMS = 8
|
|
20
|
+
/** 中文判据常常只有三四个字(「能登录」),门槛设 3 而不是 4。 */
|
|
21
|
+
const MIN_LENGTH = 3
|
|
22
|
+
const MAX_LENGTH = 80
|
|
23
|
+
|
|
24
|
+
function clean(text) {
|
|
25
|
+
return String(text ?? '')
|
|
26
|
+
.replace(/^#+\s*/, '')
|
|
27
|
+
.replace(/\*\*/g, '')
|
|
28
|
+
.replace(/`/g, '')
|
|
29
|
+
.replace(/^[-*+]\s+/, '')
|
|
30
|
+
.replace(/\s+/g, ' ')
|
|
31
|
+
.replace(/[。;;,,::]+$/, '')
|
|
32
|
+
.trim()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 归一化后去重(同一句话写两遍只留一条)。 */
|
|
36
|
+
function unique(items) {
|
|
37
|
+
const seen = new Set()
|
|
38
|
+
const out = []
|
|
39
|
+
for (const item of items) {
|
|
40
|
+
const key = item.replace(/[\s。;;,,::]/g, '')
|
|
41
|
+
if (key === '' || seen.has(key)) continue
|
|
42
|
+
seen.add(key)
|
|
43
|
+
out.push(item)
|
|
44
|
+
}
|
|
45
|
+
return out
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 提炼判据草稿。
|
|
50
|
+
*
|
|
51
|
+
* @param {object} input
|
|
52
|
+
* @param {string} [input.title] 需求标题(描述为空时给一句兜底说明用,不进判据)
|
|
53
|
+
* @param {string} [input.description] 需求描述
|
|
54
|
+
* @returns {{criteria:Array<string>, notes:Array<string>}}
|
|
55
|
+
*/
|
|
56
|
+
export function suggestCriteria({ title = '', description = '' } = {}) {
|
|
57
|
+
const notes = []
|
|
58
|
+
const text = String(description ?? '').replace(/\r\n?/g, '\n')
|
|
59
|
+
if (text.trim() === '') {
|
|
60
|
+
return {
|
|
61
|
+
criteria: [],
|
|
62
|
+
notes: [`描述是空的,没法提炼判据。写两三句「要什么、不要什么」,或用文档导入。`],
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 1) 描述里的列表项:本来就是一条条要求,直接当判据
|
|
67
|
+
const listed = text
|
|
68
|
+
.split('\n')
|
|
69
|
+
.map((line) => LIST_ITEM.exec(line))
|
|
70
|
+
.filter((match) => match !== null)
|
|
71
|
+
.map((match) => clean(match[1]))
|
|
72
|
+
.filter((item) => item.length >= MIN_LENGTH)
|
|
73
|
+
|
|
74
|
+
if (listed.length > 0) {
|
|
75
|
+
const criteria = unique(listed).slice(0, MAX_ITEMS)
|
|
76
|
+
notes.push(`从描述里的列表取了 ${criteria.length} 条;这是草稿,按需要改。`)
|
|
77
|
+
if (listed.length > criteria.length) notes.push(`另有 ${listed.length - criteria.length} 条没放进来(一次最多 ${MAX_ITEMS} 条)`)
|
|
78
|
+
return { criteria, notes }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 2) 没有列表:按句子切,只留「像要求」的
|
|
82
|
+
const sentences = text
|
|
83
|
+
.split(/[。!?!?;;\n]+/)
|
|
84
|
+
.map((line) => clean(line))
|
|
85
|
+
.filter((line) => line.length >= MIN_LENGTH && line.length <= MAX_LENGTH)
|
|
86
|
+
.filter((line) => BEHAVIOR.test(line))
|
|
87
|
+
|
|
88
|
+
if (sentences.length > 0) {
|
|
89
|
+
const criteria = unique(sentences).slice(0, MAX_ITEMS)
|
|
90
|
+
notes.push(`从描述里提炼了 ${criteria.length} 条;这是草稿,按需要改。`)
|
|
91
|
+
if (sentences.length > criteria.length) notes.push(`另有 ${sentences.length - criteria.length} 句没放进来(一次最多 ${MAX_ITEMS} 条)`)
|
|
92
|
+
return { criteria, notes }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 3) 什么都没有:说清楚怎么写
|
|
96
|
+
notes.push(
|
|
97
|
+
'描述里没有能提炼成判据的句子(要含「要 / 支持 / 不能 / 提示」这类词)。' +
|
|
98
|
+
'换成一行一条的写法最稳,例如「- 除数为 0 要提示」。',
|
|
99
|
+
)
|
|
100
|
+
if (String(title ?? '').trim() !== '') notes.push('标题只说明做什么,判据得写清「怎样算做完」。')
|
|
101
|
+
return { criteria: [], notes }
|
|
102
|
+
}
|
package/src/gates.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gates.js:插件**唯一的拦截点**,全部是纯函数。
|
|
3
|
+
*
|
|
4
|
+
* 为什么是纯函数:闸门的价值在于「同样的输入永远给同样的答案」,这样它可测、可回放、
|
|
5
|
+
* 也能在面板上先算一遍给人看(dry-run)。这里不读盘、不看时间、不调宿主。
|
|
6
|
+
*
|
|
7
|
+
* 只拦平台拦不住的三种东西:
|
|
8
|
+
* 1. **钱**——每日运行数(每次运行 = 一条会话 = 一次计费);
|
|
9
|
+
* 2. **并发**——同时在跑的会话数(写冲突、限流、人也看不过来);
|
|
10
|
+
* 3. **交付质量**——没有证据的「完成」不算完成。
|
|
11
|
+
*
|
|
12
|
+
* 明确不管的(平台已保证,写在这里只会误导):
|
|
13
|
+
* - 谁能给谁发消息(人员没有团队地址,发不出去);
|
|
14
|
+
* - 谁能指挥谁(lead 点名不到成员的下属);
|
|
15
|
+
* - 委派深度、权限预设、模型计费(部署设置与宿主行为)。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** 拒绝时的错误码:面板按它给中文提示,测试按它断言。 */
|
|
19
|
+
export const GATE_CODES = Object.freeze({
|
|
20
|
+
DISABLED: 'E_DISABLED',
|
|
21
|
+
PLAN_NOT_APPROVED: 'E_PLAN_NOT_APPROVED',
|
|
22
|
+
STATE_UNKNOWN: 'E_STATE_UNKNOWN',
|
|
23
|
+
BUDGET: 'E_BUDGET_EXCEEDED',
|
|
24
|
+
PARALLEL: 'E_PARALLEL_LIMIT',
|
|
25
|
+
EVIDENCE: 'E_EVIDENCE_MISSING',
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 能不能开一轮(= 能不能花钱)。
|
|
30
|
+
*
|
|
31
|
+
* 顺序是刻意的:先看总开关,再看计划闸,然后运行态、预算和并发。
|
|
32
|
+
* 因为「没打开」和「没批准」是配置/流程问题,人应该先看到它们;
|
|
33
|
+
* 运行态与预算是运行态问题,排在后面;运行态拿不到就直接拒绝,不拿 0 冒充。
|
|
34
|
+
*
|
|
35
|
+
* @param {object} input
|
|
36
|
+
* @param {object} input.config 归一化后的配置
|
|
37
|
+
* @param {object} [input.state] 运行态快照 `{runsToday, runningSessions}`
|
|
38
|
+
* @param {object} [input.request] 本次请求 `{planApproved}`
|
|
39
|
+
* @param {boolean} [input.stateKnown] 调用方是否真的拿到了运行态;false = 不许拿 0 冒充
|
|
40
|
+
* @returns {{allow:boolean, code:string|null, message:string, checks:Array}}
|
|
41
|
+
*/
|
|
42
|
+
export function evaluateStart({ config, state = {}, request = {}, stateKnown } = {}) {
|
|
43
|
+
const checks = []
|
|
44
|
+
const push = (id, ok, detail) => {
|
|
45
|
+
checks.push({ id, ok, detail })
|
|
46
|
+
return ok
|
|
47
|
+
}
|
|
48
|
+
const deny = (code, message) => ({ allow: false, code, message, checks })
|
|
49
|
+
const allow = () => ({ allow: true, code: null, message: '可以开工', checks })
|
|
50
|
+
|
|
51
|
+
if (!push('enabled', config.enabled === true, config.enabled ? '总开关已打开' : '总开关关闭')) {
|
|
52
|
+
return deny(GATE_CODES.DISABLED, '插件总开关没打开:现在只会拆需求、填卡片,不会开任何会话')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const planOk = request.planApproved === true || config.gates.requirePlanApproval !== true
|
|
56
|
+
if (!push('plan', planOk, planOk ? '计划已批准(或计划闸关闭)' : '计划未批准')) {
|
|
57
|
+
return deny(GATE_CODES.PLAN_NOT_APPROVED, '计划闸:DAG 还没有经过人确认,先让人看一遍拆得对不对再开跑')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const runsToday = Number(state.runsToday ?? 0)
|
|
61
|
+
const running = Number(state.runningSessions ?? 0)
|
|
62
|
+
// 调用方没明说时:给了非空 state 就当知道,空对象/没给就是「不知道」。
|
|
63
|
+
const known = stateKnown ?? (state !== null && typeof state === 'object' && Object.keys(state).length > 0)
|
|
64
|
+
|
|
65
|
+
// 运行态缺失时**不许按 0 放行**:预算/并发闸的全部价值就是「拿真实数字做决定」。
|
|
66
|
+
// 以前调用方传 state:{},这里永远是 0,两个闸形同虚设;现在缺数据就明说。
|
|
67
|
+
if (!push('state', known, known ? `运行态:今天 ${runsToday} 次 / 在跑 ${running} 个` : '没有运行态快照')) {
|
|
68
|
+
return deny(GATE_CODES.STATE_UNKNOWN, '预算/并发闸拿不到运行态(今天跑了几次、几个会话在跑):先刷新看板状态再开跑,不要拿 0 冒充')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!push('budget', runsToday < config.gates.budgetPerDay, `今天已跑 ${runsToday} / ${config.gates.budgetPerDay}`)) {
|
|
72
|
+
return deny(GATE_CODES.BUDGET, `预算闸:今天已经跑了 ${runsToday} 次,达到上限 ${config.gates.budgetPerDay};清零后才能继续`)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!push('parallel', running < config.gates.maxParallelRuns, `当前在跑 ${running} / ${config.gates.maxParallelRuns}`)) {
|
|
76
|
+
return deny(GATE_CODES.PARALLEL, `并发闸:已有 ${running} 个会话在跑,上限 ${config.gates.maxParallelRuns}`)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return allow()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 交付能不能进「已提交」。
|
|
84
|
+
*
|
|
85
|
+
* 证据的定义故意很窄:**能复核的东西**。一张嘴说「我完成了」不算,因为基线 P4 要求
|
|
86
|
+
* 「完成 = 完成 + 证据」,而自动跑卡最贵的错误就是把没证据的完成当完成。
|
|
87
|
+
*
|
|
88
|
+
* @param {object} input
|
|
89
|
+
* @param {object} input.config
|
|
90
|
+
* @param {object} [input.evidence] `{commitSha, reportPath, testOutput}`
|
|
91
|
+
* @returns {{allow:boolean, code:string|null, missing:Array<string>, message:string}}
|
|
92
|
+
*/
|
|
93
|
+
export function evaluateEvidence({ config, evidence = {} } = {}) {
|
|
94
|
+
if (config.gates.requireEvidence !== true) {
|
|
95
|
+
return { allow: true, code: null, missing: [], message: '交付门槛已关闭(不建议:没有证据的完成无法复核)' }
|
|
96
|
+
}
|
|
97
|
+
const missing = []
|
|
98
|
+
if (!String(evidence.commitSha ?? '').trim()) missing.push('commit')
|
|
99
|
+
const hasReport = String(evidence.reportPath ?? '').trim() || String(evidence.testOutput ?? '').trim()
|
|
100
|
+
if (!hasReport) missing.push('report')
|
|
101
|
+
if (missing.length === 0) return { allow: true, code: null, missing, message: '证据齐全' }
|
|
102
|
+
return {
|
|
103
|
+
allow: false,
|
|
104
|
+
code: GATE_CODES.EVIDENCE,
|
|
105
|
+
missing,
|
|
106
|
+
message: `交付门槛:缺少 ${missing.join(' 和 ')},不能算完成`,
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 第 N 次打回该不该继续返工。
|
|
112
|
+
*
|
|
113
|
+
* 语义对齐基线 R3:`rejectedCount` 是**这次判决之前**已经打回的次数。
|
|
114
|
+
* 上限 2 表示「允许打回两次」,第 3 次必须升级给人(派生澄清任务),否则就是死循环。
|
|
115
|
+
*
|
|
116
|
+
* @param {object} input
|
|
117
|
+
* @param {object} input.config
|
|
118
|
+
* @param {number} [input.rejectedCount]
|
|
119
|
+
* @returns {{allow:boolean, escalate:boolean, nextCount:number, message:string}}
|
|
120
|
+
*/
|
|
121
|
+
export function evaluateRework({ config, rejectedCount = 0 } = {}) {
|
|
122
|
+
const previous = Math.max(0, Math.trunc(Number(rejectedCount) || 0))
|
|
123
|
+
const limit = config.gates.maxReworkRounds
|
|
124
|
+
const escalate = previous >= limit
|
|
125
|
+
const nextCount = previous + 1
|
|
126
|
+
return {
|
|
127
|
+
allow: !escalate,
|
|
128
|
+
escalate,
|
|
129
|
+
nextCount,
|
|
130
|
+
message: escalate
|
|
131
|
+
? `这将是第 ${nextCount} 次打回,超过上限 ${limit}:停止让开发继续改,升级给人(对应基线 R3)`
|
|
132
|
+
: `第 ${nextCount} 次打回,仍在上限 ${limit} 内,可以返工`,
|
|
133
|
+
}
|
|
134
|
+
}
|