@mzzsfy/dsh-rs-workflow 0.2.4 → 1.0.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/README.md +14 -49
- package/cordis.patch.yml +8 -17
- package/engine/flow-exec.js +23 -0
- package/lib/board.mjs +355 -0
- package/lib/driver/approve.mjs +126 -0
- package/lib/driver/control.mjs +46 -0
- package/lib/driver/index.mjs +454 -0
- package/lib/driver/prompts.mjs +102 -0
- package/lib/driver/runner.mjs +173 -0
- package/lib/driver/scheduler.mjs +159 -0
- package/lib/index.js +30 -213
- package/lib/orchestrator.mjs +428 -0
- package/lib/planner-gate.mjs +195 -0
- package/lib/release.mjs +183 -0
- package/lib/settings-schema.mjs +73 -0
- package/lib/spec.mjs +161 -0
- package/lib/storage.mjs +35 -0
- package/lib/store.mjs +255 -0
- package/lib/template-tool.mjs +161 -0
- package/lib/template.mjs +341 -0
- package/package.json +9 -5
- package/preset/rs-workflow/agent.cordis.yml +54 -73
- package/preset/rs-workflow/preset.yml +4 -4
- package/src/client.js +1269 -0
- package/lib/preset-sync.mjs +0 -227
- package/preset/rs-workflow/skills/rs-workflow/SKILL.md +0 -110
- package/preset/rs-workflow/skills/rs-workflow/references/engine.js +0 -1446
- package/preset/rs-workflow/skills/rs-workflow/references/templates.md +0 -104
- package/preset/rs-workflow/skills/rs-workflow/slots.json5 +0 -53
package/lib/template.mjs
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
// 模板静态校验器:DSL v5 唯一权威(board 保存校验唯一入口);v5 新增顶层 autoApprove 键
|
|
2
|
+
|
|
3
|
+
export const SLOT_KEYS = ['planner', 'executor', 'reviewer', 'executor-loop', 'reviewer-approve', 'executor-escalate']
|
|
4
|
+
const TOP_FIELDS = new Set(['id', 'label', 'description', 'inputs', 'steps', 'autoApprove'])
|
|
5
|
+
const STEP_FIELDS = new Set(['id', 'label', 'slot', 'prompt', 'load', 'outputs', 'listOutputs', 'after', 'maxFail', 'for_each', 'mode', 'type', 'flow', 'input', 'target', 'rounds', 'onExhausted'])
|
|
6
|
+
const ID_RE = /^[a-z][a-z0-9-]*$/
|
|
7
|
+
const INPUT_NAME_RE = /^[a-z][a-zA-Z0-9]*$/
|
|
8
|
+
// 保留步骤 id:{input.x} 占位符以入参命名空间优先解析,同 id 步骤产出会被遮蔽
|
|
9
|
+
const RESERVED_STEP_IDS = new Set(['input'])
|
|
10
|
+
const PLACEHOLDER_RE = /\{([^{}]+)\}/g
|
|
11
|
+
const LOAD_PREFIX_RE = /^(skill|doc):/
|
|
12
|
+
const MAX_ROUND = 10
|
|
13
|
+
const MAX_NEST_DEPTH = 3
|
|
14
|
+
|
|
15
|
+
const VERDICT_SCHEMA = {
|
|
16
|
+
type: 'object',
|
|
17
|
+
required: ['verdict', 'comments'],
|
|
18
|
+
additionalProperties: false,
|
|
19
|
+
properties: { verdict: { type: 'string', enum: ['APPROVED', 'REJECTED'] }, comments: { type: 'string' } },
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const err = (target, message) => ({ target, message })
|
|
23
|
+
const isObj = (v) => typeof v === 'object' && v !== null && !Array.isArray(v)
|
|
24
|
+
const placeholdersOf = (text) => [...String(text).matchAll(PLACEHOLDER_RE)].map((m) => m[1])
|
|
25
|
+
|
|
26
|
+
// JSON 解析;失败抛错,调用方经 parseErrorLine 提取行号
|
|
27
|
+
export function parseTemplate(text) {
|
|
28
|
+
return JSON.parse(text)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function parseErrorLine(message) {
|
|
32
|
+
const text = String(message)
|
|
33
|
+
// V8 JSON.parse 形态 "(line 1 column 3)" 与 "3:5" 位置形态双兼容
|
|
34
|
+
const v8 = text.match(/\bline (\d+)\b/)
|
|
35
|
+
if (v8) return Number(v8[1])
|
|
36
|
+
const m = text.match(/(?:^|\s)(\d+):(\d+)(?:\s|$|\))/)
|
|
37
|
+
return m ? Number(m[1]) : null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 单模板校验:结构/字段/引用/环/审批与升级规则;返回 errors[](空=通过)
|
|
41
|
+
export function validateTemplate(t) {
|
|
42
|
+
const errors = []
|
|
43
|
+
if (!isObj(t)) return [err('json', '模板必须是对象')]
|
|
44
|
+
for (const k of Object.keys(t)) if (!TOP_FIELDS.has(k)) errors.push(err(`top:${k}`, `未知顶层字段 ${k}`))
|
|
45
|
+
if (typeof t.id !== 'string' || !ID_RE.test(t.id)) errors.push(err('top:id', 'id 必填且须匹配 ^[a-z][a-z0-9-]*$'))
|
|
46
|
+
if (typeof t.label !== 'string' || t.label.trim() === '') errors.push(err('top:label', 'label 必填且为非空字符串'))
|
|
47
|
+
if (typeof t.description !== 'string' || t.description.trim() === '') errors.push(err('top:description', 'description 必填且为非空字符串(分诊目录/保底 brief 依据)'))
|
|
48
|
+
if (t.autoApprove !== undefined && typeof t.autoApprove !== 'boolean') errors.push(err('top:autoApprove', 'autoApprove 须为布尔(缺省 false;true = 审批由主循环代审)'))
|
|
49
|
+
|
|
50
|
+
const declaredInputs = new Set()
|
|
51
|
+
if (t.inputs !== undefined) {
|
|
52
|
+
if (!isObj(t.inputs)) errors.push(err('top:inputs', 'inputs 须为对象(name: 说明)'))
|
|
53
|
+
else {
|
|
54
|
+
for (const [k, v] of Object.entries(t.inputs)) {
|
|
55
|
+
if (!INPUT_NAME_RE.test(k)) errors.push(err('top:inputs', `入参名 ${k} 须匹配 ${INPUT_NAME_RE.source}`))
|
|
56
|
+
if (typeof v !== 'string') errors.push(err('top:inputs', `入参 ${k} 的说明须为字符串`))
|
|
57
|
+
declaredInputs.add(k)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!Array.isArray(t.steps) || t.steps.length === 0) {
|
|
63
|
+
errors.push(err('top:steps', 'steps 必填且为非空数组'))
|
|
64
|
+
return errors
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const byId = new Map()
|
|
68
|
+
for (const s of t.steps) {
|
|
69
|
+
if (isObj(s) && typeof s.id === 'string') {
|
|
70
|
+
if (byId.has(s.id)) errors.push(err('top:steps', `步骤 id 重复:${s.id}`))
|
|
71
|
+
byId.set(s.id, s)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// escalate-only 集:被任一 approve 步 onExhausted 指向的步骤;记其审批对象(target)用于产出引用禁令
|
|
76
|
+
const escalateTargetOf = new Map()
|
|
77
|
+
for (const s of t.steps) {
|
|
78
|
+
if (isObj(s) && s.type === 'approve' && typeof s.target === 'string' && typeof s.onExhausted === 'string' && s.onExhausted !== 'blocked' && byId.has(s.onExhausted)) {
|
|
79
|
+
escalateTargetOf.set(s.onExhausted, s.target)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const order = []
|
|
84
|
+
for (const s of t.steps) {
|
|
85
|
+
const sid = isObj(s) && typeof s.id === 'string' ? s.id : `#${order.length}`
|
|
86
|
+
order.push(sid)
|
|
87
|
+
if (!isObj(s)) {
|
|
88
|
+
errors.push(err('top:steps', `步骤 ${sid} 必须是对象`))
|
|
89
|
+
continue
|
|
90
|
+
}
|
|
91
|
+
if (typeof s.id !== 'string' || !ID_RE.test(s.id)) {
|
|
92
|
+
errors.push(err('top:steps', `步骤 id 必填且须匹配 ${ID_RE.source}`))
|
|
93
|
+
continue
|
|
94
|
+
}
|
|
95
|
+
const target = `step:${s.id}`
|
|
96
|
+
if (RESERVED_STEP_IDS.has(s.id)) errors.push(err(target, `步骤 id ${s.id} 为保留字,与子流程入参命名空间冲突`))
|
|
97
|
+
for (const k of Object.keys(s)) if (!STEP_FIELDS.has(k)) errors.push(err(target, `未知步骤字段 ${k}`))
|
|
98
|
+
const type = s.type === undefined ? 'ai' : s.type
|
|
99
|
+
if (!['ai', 'flow', 'approve'].includes(type)) errors.push(err(target, `type 仅支持 ai/flow/approve,得到 ${type}`))
|
|
100
|
+
|
|
101
|
+
if (s.slot !== undefined) {
|
|
102
|
+
if (!SLOT_KEYS.includes(s.slot)) errors.push(err(target, `slot 仅六键:${SLOT_KEYS.join('/')}`))
|
|
103
|
+
if (type === 'approve' && s.slot !== 'reviewer-approve') errors.push(err(target, 'approve 步骤 slot 固定 reviewer-approve,不可声明其他值'))
|
|
104
|
+
}
|
|
105
|
+
if (s.label !== undefined && typeof s.label !== 'string') errors.push(err(target, 'label 须为字符串'))
|
|
106
|
+
if (s.maxFail !== undefined && (!Number.isInteger(s.maxFail) || s.maxFail < 1 || s.maxFail > MAX_ROUND)) errors.push(err(target, `maxFail 须为 [1,${MAX_ROUND}] 整数`))
|
|
107
|
+
|
|
108
|
+
if (s.outputs !== undefined) {
|
|
109
|
+
if (!isObj(s.outputs)) errors.push(err(target, 'outputs 须为对象(字段名: 说明)'))
|
|
110
|
+
else for (const [k, v] of Object.entries(s.outputs)) {
|
|
111
|
+
if (k.trim() === '') errors.push(err(target, 'outputs 字段名不可为空'))
|
|
112
|
+
if (typeof v !== 'string') errors.push(err(target, `outputs.${k} 的说明须为字符串`))
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const outputNames = new Set(isObj(s.outputs) ? Object.keys(s.outputs) : [])
|
|
116
|
+
if (s.listOutputs !== undefined) {
|
|
117
|
+
if (!Array.isArray(s.listOutputs)) errors.push(err(target, 'listOutputs 须为字符串数组'))
|
|
118
|
+
else for (const n of s.listOutputs) {
|
|
119
|
+
if (!outputNames.has(n)) errors.push(err(target, `listOutputs 项 ${n} 未在本步 outputs 中声明`))
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (s.load !== undefined) {
|
|
123
|
+
if (!Array.isArray(s.load) || s.load.some((v) => typeof v !== 'string' || !LOAD_PREFIX_RE.test(v))) {
|
|
124
|
+
errors.push(err(target, 'load 须为字符串数组且每项带 skill:/doc: 前缀'))
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (s.after !== undefined) {
|
|
128
|
+
if (!Array.isArray(s.after) || s.after.some((v) => typeof v !== 'string')) errors.push(err(target, 'after 须为步骤 id 数组'))
|
|
129
|
+
else for (const dep of s.after) {
|
|
130
|
+
if (dep === s.id) errors.push(err(target, 'after 不可引用自身'))
|
|
131
|
+
else if (!byId.has(dep)) errors.push(err(target, `after 引用不存在的步骤 ${dep}`))
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (s.for_each !== undefined) {
|
|
135
|
+
if (typeof s.for_each !== 'string' || !/^[a-z][a-z0-9-]*\.[a-zA-Z0-9]+$/.test(s.for_each)) {
|
|
136
|
+
errors.push(err(target, 'for_each 须为 "<步骤id>.<产出名>" 形态'))
|
|
137
|
+
} else {
|
|
138
|
+
const [srcId, srcOut] = s.for_each.split('.')
|
|
139
|
+
const src = byId.get(srcId)
|
|
140
|
+
if (!src) errors.push(err(target, `for_each 数据源步骤 ${srcId} 不存在`))
|
|
141
|
+
else if (!(Array.isArray(src.listOutputs) && src.listOutputs.includes(srcOut))) {
|
|
142
|
+
errors.push(err(target, `for_each 数据源 ${s.for_each} 未在该步骤 listOutputs 中声明`))
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (s.mode !== undefined) {
|
|
147
|
+
if (!['sequential', 'parallel'].includes(s.mode)) errors.push(err(target, 'mode 仅支持 sequential/parallel'))
|
|
148
|
+
if (s.for_each === undefined) errors.push(err(target, 'mode 仅在声明 for_each 时有效'))
|
|
149
|
+
}
|
|
150
|
+
// sequential 为缺省(spec §8),不必显式声明;校验器无副作用,仅接受缺省
|
|
151
|
+
|
|
152
|
+
if (type === 'ai') {
|
|
153
|
+
if (typeof s.prompt !== 'string' || s.prompt.trim() === '') errors.push(err(target, '普通步骤 prompt 必填'))
|
|
154
|
+
if (s.flow !== undefined || s.input !== undefined || s.target !== undefined || s.rounds !== undefined || s.onExhausted !== undefined) {
|
|
155
|
+
errors.push(err(target, '普通步骤不可声明 flow/input/target/rounds/onExhausted'))
|
|
156
|
+
}
|
|
157
|
+
if (!isObj(s.outputs) || outputNames.size === 0) errors.push(err(target, '普通步骤必须以 outputs 声明产出契约'))
|
|
158
|
+
}
|
|
159
|
+
if (type === 'approve') {
|
|
160
|
+
if (typeof s.prompt !== 'string' || s.prompt.trim() === '') errors.push(err(target, 'approve 步骤 prompt(审批口径)必填'))
|
|
161
|
+
if (s.outputs !== undefined || s.listOutputs !== undefined) errors.push(err(target, 'approve 步骤裁决契约由引擎生成,不可声明 outputs/listOutputs'))
|
|
162
|
+
if (s.for_each !== undefined || s.mode !== undefined) errors.push(err(target, 'approve 步骤不支持 for_each/mode'))
|
|
163
|
+
if (typeof s.target !== 'string' || !byId.has(s.target)) errors.push(err(target, 'approve 步骤 target 必填且须指向存在步骤'))
|
|
164
|
+
else if (s.target === s.id) errors.push(err(target, 'approve 步骤 target 不可为自身'))
|
|
165
|
+
if (s.rounds !== undefined && (!Number.isInteger(s.rounds) || s.rounds < 1 || s.rounds > MAX_ROUND)) errors.push(err(target, `rounds 须为 [1,${MAX_ROUND}] 整数`))
|
|
166
|
+
if (s.onExhausted !== undefined && s.onExhausted !== 'blocked' && !byId.has(s.onExhausted)) {
|
|
167
|
+
errors.push(err(target, `onExhausted 须为 "blocked" 或存在步骤 id,得到 ${s.onExhausted}`))
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (type === 'flow') {
|
|
171
|
+
if (s.prompt !== undefined) errors.push(err(target, 'flow 步骤不派发子代理,不可声明 prompt'))
|
|
172
|
+
if (s.outputs !== undefined || s.listOutputs !== undefined) errors.push(err(target, 'flow 步骤产出由子流程扁平挂载,不可声明 outputs/listOutputs'))
|
|
173
|
+
if (s.load !== undefined || s.for_each !== undefined || s.mode !== undefined) errors.push(err(target, 'flow 步骤不支持 load/for_each/mode'))
|
|
174
|
+
if (typeof s.flow !== 'string' || s.flow.trim() === '') errors.push(err(target, 'flow 步骤 flow 必填(模板 id 或 "{步骤id.产出名}" 动态路由)'))
|
|
175
|
+
else if (s.flow.trim() === t.id) errors.push(err(target, '子流程路由不可指向自身'))
|
|
176
|
+
if (s.input !== undefined) {
|
|
177
|
+
if (!isObj(s.input)) errors.push(err(target, 'input 须为对象'))
|
|
178
|
+
else for (const [k, v] of Object.entries(s.input)) {
|
|
179
|
+
if (typeof v !== 'string' || !/^\{[^{}]+\}$/.test(v)) errors.push(err(target, `input.${k} 须为单占位符引用 "{来源}"`))
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 占位符来源校验(prompt/flow/input 中 {引用} 的存在性与合法性)
|
|
186
|
+
const refOk = (s, ph, target) => {
|
|
187
|
+
if (ph === 'request') return true
|
|
188
|
+
if (ph === 'item' || ph === 'item.index') {
|
|
189
|
+
if (s.for_each === undefined) errors.push(err(target, `占位符 {${ph}} 仅在 for_each 步骤内可用`))
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
if (ph.startsWith('input.')) {
|
|
193
|
+
if (!declaredInputs.has(ph.slice(6))) errors.push(err(target, `占位符 {${ph}} 引用未声明的入参`))
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
const dot = ph.indexOf('.')
|
|
197
|
+
if (dot <= 0) {
|
|
198
|
+
errors.push(err(target, `占位符 {${ph}} 无合法来源(request/input.<name>/<步骤>.<产出>/item)`))
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
const refId = ph.slice(0, dot)
|
|
202
|
+
const refOut = ph.slice(dot + 1)
|
|
203
|
+
const ref = byId.get(refId)
|
|
204
|
+
if (!ref) {
|
|
205
|
+
errors.push(err(target, `占位符 {${ph}} 引用不存在的步骤`))
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
if (ref.type === 'approve') {
|
|
209
|
+
errors.push(err(target, `占位符 {${ph}} 不可引用审批步骤产出(裁决不进产出账)`))
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
const refOuts = isObj(ref.outputs) ? Object.keys(ref.outputs) : []
|
|
213
|
+
if (!refOuts.includes(refOut)) {
|
|
214
|
+
errors.push(err(target, `占位符 {${ph}} 的产出未在步骤 ${refId} outputs 中声明`))
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
if (refId === s.id && !(s.for_each !== undefined && s.mode === 'sequential')) {
|
|
218
|
+
errors.push(err(target, '自引用产出仅 sequential 循环实例可用(首实例解析空串)'))
|
|
219
|
+
}
|
|
220
|
+
const forbid = escalateTargetOf.get(s.id)
|
|
221
|
+
if (forbid !== undefined && refId === forbid) {
|
|
222
|
+
errors.push(err(target, `升级步占位符 {${ph}} 禁止引用审批对象产出(耗尽时已清空),只可引用其上游产出`))
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
for (const s of t.steps) {
|
|
226
|
+
if (!isObj(s) || typeof s.id !== 'string') continue
|
|
227
|
+
const target = `step:${s.id}`
|
|
228
|
+
if (typeof s.prompt === 'string') for (const ph of placeholdersOf(s.prompt)) refOk(s, ph, target)
|
|
229
|
+
if (type_flow(s)) {
|
|
230
|
+
const m = /^\{([^{}]+)\}$/.exec(String(s.flow).trim())
|
|
231
|
+
if (m) {
|
|
232
|
+
const ph = m[1]
|
|
233
|
+
if (!ph.includes('.') || ph.startsWith('input.') || ph === 'request' || ph === 'item') {
|
|
234
|
+
errors.push(err(target, `flow 动态路由 {${ph}} 须引用 "<步骤id>.<产出名>"`))
|
|
235
|
+
} else refOk(s, ph, target)
|
|
236
|
+
}
|
|
237
|
+
if (isObj(s.input)) for (const v of Object.values(s.input)) {
|
|
238
|
+
for (const ph of placeholdersOf(v)) refOk(s, ph, target)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// escalate-only 规则:不参与依赖图(不可声明 after),且不可被其他步骤 after 引用
|
|
244
|
+
for (const s of t.steps) {
|
|
245
|
+
if (!isObj(s) || typeof s.id !== 'string') continue
|
|
246
|
+
if (escalateTargetOf.has(s.id) && s.after !== undefined) {
|
|
247
|
+
errors.push(err(`step:${s.id}`, '升级步由引擎脱离常规调度图,不可声明 after'))
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
for (const s of t.steps) {
|
|
251
|
+
if (!isObj(s) || !Array.isArray(s.after)) continue
|
|
252
|
+
for (const dep of s.after) {
|
|
253
|
+
if (escalateTargetOf.has(dep)) errors.push(err(`step:${s.id}`, `after 不可引用升级步 ${dep}(其唯一触发边为审批耗尽)`))
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// 环检测:边 = 显式 after,缺省 = 文档序前一步(escalate-only 无缺省边)
|
|
258
|
+
const depsOf = new Map()
|
|
259
|
+
t.steps.forEach((s, i) => {
|
|
260
|
+
if (!isObj(s) || typeof s.id !== 'string') return
|
|
261
|
+
let deps
|
|
262
|
+
if (Array.isArray(s.after)) deps = s.after
|
|
263
|
+
else if (escalateTargetOf.has(s.id) || i === 0) deps = []
|
|
264
|
+
else deps = [order[i - 1]]
|
|
265
|
+
depsOf.set(s.id, deps.filter((d) => byId.has(d) && d !== s.id))
|
|
266
|
+
})
|
|
267
|
+
const state = new Map()
|
|
268
|
+
const visit = (id, stack) => {
|
|
269
|
+
const st = state.get(id)
|
|
270
|
+
if (st === 'done') return
|
|
271
|
+
if (st === 'visiting') {
|
|
272
|
+
const cycle = [...stack.slice(stack.indexOf(id)), id].join(' -> ')
|
|
273
|
+
errors.push(err('top:steps', `依赖环:${cycle}`))
|
|
274
|
+
return
|
|
275
|
+
}
|
|
276
|
+
state.set(id, 'visiting')
|
|
277
|
+
for (const d of depsOf.get(id) ?? []) visit(d, [...stack, id])
|
|
278
|
+
state.set(id, 'done')
|
|
279
|
+
}
|
|
280
|
+
for (const id of depsOf.keys()) visit(id, [])
|
|
281
|
+
|
|
282
|
+
return errors
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const type_flow = (s) => (s.type === undefined ? false : s.type === 'flow')
|
|
286
|
+
|
|
287
|
+
// 集合校验:各模板单独校验 + 字面子流程引用存在性 + 嵌套深度 ≤3;损坏成员跳过不阻塞其余
|
|
288
|
+
export function validateTemplateSet(list) {
|
|
289
|
+
const errors = []
|
|
290
|
+
const ok = []
|
|
291
|
+
for (const t of Array.isArray(list) ? list : []) {
|
|
292
|
+
const es = validateTemplate(t)
|
|
293
|
+
errors.push(...es)
|
|
294
|
+
if (es.length === 0) ok.push(t)
|
|
295
|
+
}
|
|
296
|
+
const ids = new Set(ok.map((t) => t.id))
|
|
297
|
+
const parsedById = new Map(ok.map((t) => [t.id, t]))
|
|
298
|
+
const tmplEdges = new Map()
|
|
299
|
+
for (const t of ok) {
|
|
300
|
+
const outs = new Set()
|
|
301
|
+
for (const s of t.steps) {
|
|
302
|
+
if (s.type !== 'flow' || typeof s.flow !== 'string') continue
|
|
303
|
+
const literal = s.flow.trim()
|
|
304
|
+
if (/^\{[^{}]+\}$/.test(literal)) continue
|
|
305
|
+
// 归属本模板(top:id 前缀):释放权威点按 id 过滤集合校验错误
|
|
306
|
+
if (!ids.has(literal)) errors.push(err('top:id', `${t.id} 步骤 ${s.id} 的子流程 ${literal} 不存在于模板集合`))
|
|
307
|
+
else {
|
|
308
|
+
if (literal !== t.id) outs.add(literal)
|
|
309
|
+
// 审批只在顶层可达:子模板含审批步则任何 flow 引用它的模板无法完成该子流程
|
|
310
|
+
const sub = parsedById.get(literal)
|
|
311
|
+
if (sub?.steps?.some((x) => x.type === 'approve')) errors.push(err('top:id', `${t.id} 引用的子流程 ${literal} 含审批步:审批仅支持顶层剧本`))
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (outs.size > 0) tmplEdges.set(t.id, outs)
|
|
315
|
+
}
|
|
316
|
+
const depthOf = (id, seen) => {
|
|
317
|
+
// 环即拒绝:深度无穷,不限于此路径是否重复经过
|
|
318
|
+
if (seen.has(id)) return Infinity
|
|
319
|
+
seen.add(id)
|
|
320
|
+
let d = 1
|
|
321
|
+
for (const n of tmplEdges.get(id) ?? []) d = Math.max(d, 1 + depthOf(n, seen))
|
|
322
|
+
return d
|
|
323
|
+
}
|
|
324
|
+
for (const id of tmplEdges.keys()) {
|
|
325
|
+
if (depthOf(id, new Set()) > MAX_NEST_DEPTH) errors.push(err(`top:id`, `${id} 子流程嵌套深度超过 ${MAX_NEST_DEPTH}`))
|
|
326
|
+
}
|
|
327
|
+
return errors
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// 产出契约 → structured_output schema(宿主 schema 子集内);approve 步为固定裁决契约
|
|
331
|
+
export function buildSchema(step) {
|
|
332
|
+
if (step.type === 'approve') return VERDICT_SCHEMA
|
|
333
|
+
const properties = {}
|
|
334
|
+
const required = []
|
|
335
|
+
const listSet = new Set(Array.isArray(step.listOutputs) ? step.listOutputs : [])
|
|
336
|
+
for (const name of Object.keys(isObj(step.outputs) ? step.outputs : {})) {
|
|
337
|
+
required.push(name)
|
|
338
|
+
properties[name] = listSet.has(name) ? { type: 'array', items: { type: 'string' } } : { type: 'string' }
|
|
339
|
+
}
|
|
340
|
+
return { type: 'object', required, additionalProperties: false, properties }
|
|
341
|
+
}
|
package/package.json
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mzzsfy/dsh-rs-workflow",
|
|
3
|
-
"description": "
|
|
4
|
-
"version": "0.
|
|
3
|
+
"description": "若水工作流设置面:流程模板管理与工作位/预算配置(模板编辑/校验/配置读写)。编排运行时按 v5 设计(docs/rsww-v5/)另行实现。",
|
|
4
|
+
"version": "1.0.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./lib/index.js",
|
|
9
|
+
"./client": "./src/client.js",
|
|
9
10
|
"./package.json": "./package.json"
|
|
10
11
|
},
|
|
11
12
|
"dsh": {
|
|
12
13
|
"bundle": {
|
|
13
14
|
"patch": "./cordis.patch.yml"
|
|
15
|
+
},
|
|
16
|
+
"client": {
|
|
17
|
+
"platform": "web"
|
|
14
18
|
}
|
|
15
19
|
},
|
|
16
20
|
"scripts": {
|
|
@@ -18,18 +22,18 @@
|
|
|
18
22
|
},
|
|
19
23
|
"license": "MIT",
|
|
20
24
|
"repository": {
|
|
21
|
-
"type": "
|
|
25
|
+
"type": "object",
|
|
22
26
|
"url": "git+https://github.com/mzzsfy/dsh-plugin.git"
|
|
23
27
|
},
|
|
24
28
|
"files": [
|
|
25
29
|
"lib",
|
|
30
|
+
"src",
|
|
31
|
+
"engine",
|
|
26
32
|
"preset",
|
|
27
33
|
"cordis.patch.yml",
|
|
28
34
|
"README.md"
|
|
29
35
|
],
|
|
30
36
|
"peerDependencies": {
|
|
31
|
-
"@deepseek-ai/dsh-settings": ">=0.1.2-alpha.2",
|
|
32
|
-
"@deepseek-ai/dsh-tools": "^0.1.1-rc.2",
|
|
33
37
|
"@deepseek-ai/schemastery": "^3.18.1"
|
|
34
38
|
}
|
|
35
39
|
}
|
|
@@ -1,32 +1,38 @@
|
|
|
1
|
-
# The `rs-workflow` agent preset:
|
|
1
|
+
# The `rs-workflow` agent preset: 若水工作流(v5,主循环协作编排)。
|
|
2
2
|
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
# 差异有四处:
|
|
6
|
-
# 1. persona 换成若水工作流纪律(非平凡需求走 workflow 编排);
|
|
7
|
-
# 2. skill-filesystem 追加预设自带技能目录 skills/(rs-workflow 协议技能随预设分发);
|
|
8
|
-
# 3. 追加 GUI 配置读取工具行 rs-workflow-config(@mzzsfy/dsh-rs-workflow
|
|
9
|
-
# 的 tool 角色,裸包名从组合 baseUrl 上溯解析到 profile node_modules;
|
|
10
|
-
# GUI 表单注册在同包 settings 角色、bundle patch 行上;本文件整体由该包
|
|
11
|
-
# 的 preset-sync 角色行释放到用户预设根,随插件安装/升级自动同步);
|
|
12
|
-
# 4. 其余行保持 standard 原样 —— 工具面不变,模式差异只由 persona + 技能承载。
|
|
3
|
+
# 本文件是释放骨架源:release 生成器把它作为唯一源生成 rs-<id> 模式组合
|
|
4
|
+
# (运行时仅改写 orchestrator 行的 templateId 锚定),不在包内被宿主装载。
|
|
13
5
|
#
|
|
14
|
-
#
|
|
6
|
+
# 基底是 `standard` 全功能编码代理行集,差异:
|
|
7
|
+
# 1. persona 换成流程模式纪律 v5(人机接口+受约束规划者,任务交付由编排子代理承担);
|
|
8
|
+
# 2. delegation 组内 rs-workflow-orchestrator 工具行与 template-tool 行;
|
|
9
|
+
# 3. 无 report 行(trace 事件 + run-store 取代);plan-mode 组移除(规划在编排内)。
|
|
15
10
|
|
|
16
11
|
# ── identity ────────────────────────────────────────────────────────────────
|
|
17
12
|
|
|
18
13
|
- id: persona
|
|
19
14
|
name: '@deepseek-ai/dsh-persona'
|
|
20
15
|
config:
|
|
21
|
-
|
|
22
|
-
你是"若水工作流"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
16
|
+
prefix: |-
|
|
17
|
+
你是"若水工作流"会话代理,由 {{model}} 模型驱动。你负责人机协作与编排调度;
|
|
18
|
+
任务交付由流程引擎接管下的子代理完成,你不直接产出交付物。
|
|
19
|
+
职责判据:
|
|
20
|
+
1. 纯会话内请求(问答、解释、讨论,不产生文件)→ 直接答复,不起编排;
|
|
21
|
+
2. 凡需新建或修改交付物文件(代码、页面、脚本、文档、配置,无论大小与步骤数,例如"写个贪吃蛇html")
|
|
22
|
+
→ 必须调用 rs_workflow_start,提交结构化规划(steps 各步要点与验收口径);禁止以"一句话能答、
|
|
23
|
+
改动小"为由绕开编排自行产出文件;此通道无豁免:规划被拒按 errors 修正重交,不许改道自行完成;
|
|
24
|
+
3. 编排运行中你只被段结束通知或用户消息唤醒:任一唤醒轮先 rs_workflow_status 检查现役 run——
|
|
25
|
+
status=running 且无活跃段 job(待拉起)或 status=paused 且 awaitingResume=true(页签已发恢复)
|
|
26
|
+
时,即 rs_workflow_resume 补拉;waiting_approval 一律不补拉,轮到裁决:按模板 autoApprove 决定
|
|
27
|
+
代审或 ask_user 转呈真人,裁决经 rs_workflow_verdict 回写(autoApprove 代审 by 缺省,转呈真人
|
|
28
|
+
by=user,意见必填),裁决受理后 rs_workflow_resume 拉起下一段;
|
|
29
|
+
终态则汇报(完成经 deliverables 交付,汇报必须引用 runId 供页签核验;失败/阻塞说明卡点与建议);
|
|
30
|
+
任何交付说明若无对应 run 记录即为违规——禁止把自行产出的文件当交付物汇报;
|
|
31
|
+
用户中途纠偏经 rs_workflow_message 注入,下一步骤边界进入编排;
|
|
32
|
+
4. 你不代替子代理执行编排内步骤,不伪造产出,不跳过引擎直接交付;
|
|
33
|
+
5. 引擎不可用或编排启动失败时,不得自行完成文件类请求——向用户说明故障与已尝试的调用,
|
|
34
|
+
等待用户处置;文件交付只有 rs_workflow_ 通道,没有自行兜底。
|
|
35
|
+
suffix: Your working directory is {{cwd}}.
|
|
30
36
|
|
|
31
37
|
- id: agent-instructions
|
|
32
38
|
name: '@deepseek-ai/dsh-agent-instructions'
|
|
@@ -35,7 +41,10 @@
|
|
|
35
41
|
|
|
36
42
|
# ── shell ───────────────────────────────────────────────────────────────────
|
|
37
43
|
|
|
38
|
-
#
|
|
44
|
+
# 注:不可禁本组合 shell/fs 工具行——宿主 workflow worker 的段子代理继承组合行集,
|
|
45
|
+
# 禁行使段子代理无法落盘交付物(实测证实)。主循环不写文件由 persona 判据约束,
|
|
46
|
+
# 物理隔离需宿主提供 worker 行集覆写能力(外部依赖)。
|
|
47
|
+
|
|
39
48
|
- id: tool-bash
|
|
40
49
|
name: '@deepseek-ai/dsh-tool-bash'
|
|
41
50
|
disabled: !!js process.platform === 'win32'
|
|
@@ -61,60 +70,23 @@
|
|
|
61
70
|
|
|
62
71
|
# ── skills ──────────────────────────────────────────────────────────────────
|
|
63
72
|
|
|
64
|
-
# 与 cordis 出厂预设相同的手法:customSkillDirs 指向预设自己的 skills/ 目录,
|
|
65
|
-
# baseUrl 即预设目录本身,因此 rs-workflow 协议技能随预设安装、复制、迁移。
|
|
66
73
|
- id: skill-filesystem
|
|
67
74
|
name: '@deepseek-ai/dsh-skill-filesystem'
|
|
68
|
-
config:
|
|
69
|
-
customSkillDirs:
|
|
70
|
-
- !!js "process.getBuiltinModule('node:url').fileURLToPath(new URL('skills/', baseUrl))"
|
|
71
75
|
|
|
72
76
|
- id: tool-skill
|
|
73
77
|
name: '@deepseek-ai/dsh-tool-skill'
|
|
74
78
|
|
|
75
|
-
#
|
|
76
|
-
# 包本体由 `dsh plugin add @mzzsfy/dsh-rs-workflow` 安装进 profile node_modules;
|
|
77
|
-
# 本 preset 文件由同包的 preset-sync 角色行在宿主启动时释放/同步到用户预设根。
|
|
78
|
-
# 预设行的裸包名从组合 baseUrl(profile 目录)上溯解析;settings 表单的注册
|
|
79
|
-
# 在同包 bundle patch 行上(见包内 cordis.patch.yml)。
|
|
80
|
-
- id: rs-workflow-config
|
|
81
|
-
name: '@mzzsfy/dsh-rs-workflow'
|
|
82
|
-
config:
|
|
83
|
-
role: tool
|
|
84
|
-
|
|
85
|
-
# ── goals ───────────────────────────────────────────────────────────────────
|
|
86
|
-
|
|
87
|
-
- id: tool-goal
|
|
88
|
-
name: '@deepseek-ai/dsh-tool-goal'
|
|
79
|
+
# ── web ─────────────────────────────────────────────────────────────────────
|
|
89
80
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
# 保留 standard 的计划模式:用户仍可把会话切到 plan 模式做前置规划,
|
|
93
|
-
# 与工作流不冲突(工作流自身的规划发生在编排内部)。
|
|
94
|
-
- id: planning
|
|
95
|
-
name: cordis:group
|
|
96
|
-
group: true
|
|
97
|
-
isolate:
|
|
98
|
-
planMode: true
|
|
81
|
+
- id: tool-web
|
|
82
|
+
name: '@deepseek-ai/dsh-tool-web'
|
|
99
83
|
config:
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
config:
|
|
103
|
-
section: |
|
|
104
|
-
You are in plan mode. Stay in plan mode until exit_plan_mode succeeds or the user switches the session mode. Imperative language to implement changes means plan the implementation, not execute it. A user's conversational agreement — including an answer confirming something you asked — approves nothing and does not end plan mode; fold the confirmed decision into the plan and submit it through exit_plan_mode.
|
|
105
|
-
|
|
106
|
-
Explore first. Use non-mutating reads, searches, static analysis, and checks to ground the plan in the actual repository. Do not edit or write files, change configuration, run formatters or code generation that rewrites tracked files, commit, or otherwise carry out the plan. Prefer existing functions and patterns over new machinery.
|
|
107
|
-
|
|
108
|
-
The tool catalog stays the same across modes for request-cache stability. These plan-mode rules override any later tool description or guidance that suggests using mutation tools; those tools remain listed to keep the tool catalog unchanged. Do not use todo_write to track this planning phase: it tracks implementation after an approved plan, while the plan itself belongs in exit_plan_mode.
|
|
109
|
-
|
|
110
|
-
Resolve discoverable facts by inspection. Use ask_user_question only for user-owned choices or material ambiguity that inspection cannot answer. Do not ask the user where code lives or how current behavior works when you can find out.
|
|
111
|
-
|
|
112
|
-
Make the plan decision-complete: state the goal and success criteria; group implementation changes by subsystem; identify public API, schema, and data-flow changes; cover edge cases, failure modes, tests, acceptance criteria, and explicit assumptions. Keep it concise enough to review but detailed enough that another engineer can implement it without making design decisions.
|
|
113
|
-
|
|
114
|
-
When ready, call exit_plan_mode with the complete plan markdown, starting with a # title. Make exit_plan_mode the only and final tool call in that assistant response: it presents the plan for approval, and implementation begins only in a later step after approval. Do not paste the final plan as a plain reply or ask "should I proceed?" through prose or ask_user_question. If review rejects it, incorporate the feedback and present again. If the review channel is unavailable or aborted, stay in plan mode and ask the user to switch modes manually; do not proceed with implementation.
|
|
84
|
+
fetch: false
|
|
85
|
+
searchTimeoutMs: 60000
|
|
115
86
|
|
|
116
87
|
# ── compaction ──────────────────────────────────────────────────────────────
|
|
117
88
|
|
|
89
|
+
# 编排子代理上下文较长,保留 standard 的压缩组。
|
|
118
90
|
- id: compaction
|
|
119
91
|
name: cordis:group
|
|
120
92
|
group: true
|
|
@@ -137,8 +109,10 @@
|
|
|
137
109
|
|
|
138
110
|
# ── delegation and workflows ────────────────────────────────────────────────
|
|
139
111
|
|
|
140
|
-
#
|
|
141
|
-
# 所有触达它的行共享一个 entry-local realm。
|
|
112
|
+
# workflowEngine 是预设私有服务(preset 组合发布全局服务会被宿主拒绝 mount),
|
|
113
|
+
# 所有触达它的行共享一个 entry-local isolate realm。
|
|
114
|
+
# orchestrator 行注册 rs_workflow_* 六工具(含裁决回写 rs_workflow_verdict)并经 jobs 服务拉起分段 continuable job;
|
|
115
|
+
# 控制队列与 store 为普通模块单例,host/agent 行经同一 realpath 解析共享实例。
|
|
142
116
|
- id: delegation
|
|
143
117
|
name: cordis:group
|
|
144
118
|
group: true
|
|
@@ -179,6 +153,19 @@
|
|
|
179
153
|
subagentProvider: spawn
|
|
180
154
|
maxRounds: 64
|
|
181
155
|
|
|
156
|
+
# orchestrator 工具行:模板 id 由组合名承载(rs-<id> → <id>),释放生成器按组合名写入
|
|
157
|
+
- id: rs-workflow-orchestrator
|
|
158
|
+
name: '@mzzsfy/dsh-rs-workflow'
|
|
159
|
+
config:
|
|
160
|
+
role: orchestrator
|
|
161
|
+
templateId: TPL_ANCHOR
|
|
162
|
+
|
|
163
|
+
# 模板编辑工具行:rs_workflow_template(AI 按口述逻辑生成/修改流程模板)
|
|
164
|
+
- id: rs-workflow-template-tool
|
|
165
|
+
name: '@mzzsfy/dsh-rs-workflow'
|
|
166
|
+
config:
|
|
167
|
+
role: template-tool
|
|
168
|
+
|
|
182
169
|
# ── remaining model-facing rows ─────────────────────────────────────────────
|
|
183
170
|
|
|
184
171
|
- id: tool-ask-user
|
|
@@ -188,9 +175,3 @@
|
|
|
188
175
|
name: '@deepseek-ai/dsh-tool-todo'
|
|
189
176
|
config:
|
|
190
177
|
allowParallelInProgress: true
|
|
191
|
-
|
|
192
|
-
- id: tool-web
|
|
193
|
-
name: '@deepseek-ai/dsh-tool-web'
|
|
194
|
-
config:
|
|
195
|
-
fetch: false
|
|
196
|
-
searchTimeoutMs: 60000
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
name:
|
|
1
|
+
name: 若水·流程编排 v5
|
|
2
2
|
description: >-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
若水工作流 v5:简单请求直接答复,复杂请求经结构化规划进入流程编排;
|
|
4
|
+
主模型作为人机接口与受约束规划者,任务交付由引擎子代理分段完成;
|
|
5
|
+
审批可代审可转呈,运行中消息注入纠偏,取消即时生效,产出与重跑/续跑在会话页签全量可查。
|