@mzzsfy/dsh-rs-workflow 0.2.3 → 1.0.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/README.md +14 -49
- package/cordis.patch.yml +8 -17
- 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 +8 -6
- package/src/client.js +1269 -0
- package/lib/preset-sync.mjs +0 -227
- package/preset/rs-workflow/agent.cordis.yml +0 -196
- package/preset/rs-workflow/preset.yml +0 -5
- 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.0",
|
|
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,16 @@
|
|
|
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",
|
|
26
|
-
"
|
|
30
|
+
"src",
|
|
27
31
|
"cordis.patch.yml",
|
|
28
32
|
"README.md"
|
|
29
33
|
],
|
|
30
34
|
"peerDependencies": {
|
|
31
|
-
"@deepseek-ai/dsh-settings": ">=0.1.2-alpha.2",
|
|
32
|
-
"@deepseek-ai/dsh-tools": "^0.1.1-rc.2",
|
|
33
35
|
"@deepseek-ai/schemastery": "^3.18.1"
|
|
34
36
|
}
|
|
35
37
|
}
|