@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.
@@ -0,0 +1,173 @@
1
+ // 批次执行:slots 解析/候选轮换/engine.start 派发/收敛记账/失败账
2
+ import { readFileSync } from 'node:fs'
3
+ import { dirname, join } from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { schemaOf, buildPrompt } from './prompts.mjs'
6
+ import { stepTypeOf } from './scheduler.mjs'
7
+
8
+ const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..')
9
+ export const FLOW_EXEC_SOURCE = readFileSync(join(PKG_ROOT, 'engine', 'flow-exec.js'), 'utf8')
10
+
11
+
12
+ const DEFAULT_SLOT = {
13
+ normal: 'executor',
14
+ loop: 'executor-loop',
15
+ redo: 'executor-loop',
16
+ escalate: 'executor-escalate',
17
+ approve: 'reviewer-approve',
18
+ }
19
+
20
+ // 失败账缺省上限:step.maxFail 与 budgets.maxStepFail 均缺省时兜底,防无限重试
21
+ const DEFAULT_MAX_STEP_FAIL = 2
22
+
23
+ // 步骤槽位类别:常规/循环/重做/升级/审批(显式 slot 优先,审批不可覆盖)
24
+ export function slotCategoryOf(step, { isRedo = false, isEscalate = false } = {}) {
25
+ if (stepTypeOf(step) === 'approve') return 'approve'
26
+ if (isEscalate) return 'escalate'
27
+ if (isRedo) return 'redo'
28
+ if (step.for_each) return 'loop'
29
+ return 'normal'
30
+ }
31
+
32
+ // 槽位解析:候选数组按 run 级游标轮换;失败换下一候选(游标+1)
33
+ // 显式 slot 不限类别(settings-schema:缺省绑定仅在未声明 slot 时生效)
34
+ export function resolveSlot(step, category, slots, state) {
35
+ const key = step.slot ?? DEFAULT_SLOT[category]
36
+ const binding = slots?.[key]
37
+ if (binding === undefined || binding === '' || binding === null) return { slotKey: key, provider: undefined, model: undefined }
38
+ if (!Array.isArray(binding)) return { slotKey: key, provider: binding, model: undefined }
39
+ state.slotCursor ??= {}
40
+ const cursor = state.slotCursor[key] ?? 0
41
+ const candidate = binding[cursor % binding.length]
42
+ return { slotKey: key, provider: candidate, model: undefined, cursorKey: key }
43
+ }
44
+
45
+ function rotateCursor(state, slotKey) {
46
+ if (!slotKey) return
47
+ state.slotCursor ??= {}
48
+ state.slotCursor[slotKey] = (state.slotCursor[slotKey] ?? 0) + 1
49
+ }
50
+
51
+ // 单批次:组 Batch → engine.start → 收敛逐 call 记账。返回 {results, cancelled}
52
+ export async function runBatch({ state, script, template, batch, ctx }) {
53
+ const { store, runId, engine, parent, signal } = ctx
54
+ const calls = []
55
+ const callMeta = new Map()
56
+ state.batchSeq++
57
+ const batchNo = state.batchSeq
58
+ for (const [i, item] of batch.entries()) {
59
+ const step = (script ?? template).steps.find((s) => s.id === item.stepId) ?? template.steps.find((s) => s.id === item.stepId)
60
+ const inst = item.instance
61
+ const redo = state.redoInfo?.[item.stepId]
62
+ const isRedo = !!redo
63
+ const category = slotCategoryOf(step, { isRedo, isEscalate: !!item.escalate })
64
+ const { slotKey, provider, model, cursorKey } = resolveSlot(step, category, ctx.slots, state)
65
+ const prompt = buildPrompt({
66
+ state, request: ctx.request, inputs: ctx.inputs, step, inst,
67
+ planStep: ctx.planStepOf?.get(item.stepId),
68
+ injectMessages: ctx.injectMessages, queuedMessages: ctx.queuedMessages,
69
+ redo, readDoc: ctx.readDoc,
70
+ })
71
+ const callId = `${item.stepId}#${inst?.key ?? '-'}@${batchNo}:${i}`
72
+ const label = `${step.label ?? step.id}${inst ? ` ${inst.key}` : ''}`
73
+ calls.push({ callId, prompt, label, schema: schemaOf(step), provider, model })
74
+ callMeta.set(callId, { item, step, slotKey, cursorKey, prompt, label })
75
+ }
76
+
77
+ for (const call of calls) {
78
+ const { item, prompt } = callMeta.get(call.callId)
79
+ const s = state.steps[item.stepId]
80
+ if (item.instance) {
81
+ const inst = s.instances.find((x) => x.key === item.instance.key)
82
+ if (inst) inst.status = 'running'
83
+ } else {
84
+ s.status = 'running'
85
+ }
86
+ store.step({ runId, stepId: item.stepId, instance: item.instance?.key, event: 'dispatch', body: { prompt, callLabel: call.label } })
87
+ }
88
+
89
+ let outcome
90
+ try {
91
+ const run = engine.start({
92
+ script: FLOW_EXEC_SOURCE,
93
+ args: { calls },
94
+ // 宿主 meta 契约:description 必填非空;phases 必须为 {title} 对象数组(dsh-workflow-worker-thread meta 校验)
95
+ meta: {
96
+ name: `rsww-batch-${runId}-${batchNo}`,
97
+ description: calls.map((c) => c.label).join(' / '),
98
+ phases: calls.map((c) => ({ title: c.label })),
99
+ },
100
+ parent,
101
+ signal,
102
+ })
103
+ outcome = await (run.result ?? run)
104
+ } catch (e) {
105
+ if (signal?.aborted) return { cancelled: true }
106
+ outcome = { results: calls.map((c) => ({ callId: c.callId, ok: false, error: String(e?.message ?? e) })) }
107
+ }
108
+ if (signal?.aborted) return { cancelled: true }
109
+ // 引擎 run.result 契约:{ value(脚本返回值), stopReason, error?, agentsStarted }
110
+ // stopReason 非 completed(如 cancelled/error)时 value 不可信,全部调用按失败记账
111
+ if (outcome?.stopReason !== undefined && outcome?.stopReason !== 'completed') {
112
+ const reason = String(outcome?.error ?? outcome?.stopReason)
113
+ return { results: calls.map((c) => ({ callId: c.callId, ok: false, error: reason })) }
114
+ }
115
+ const results = outcome?.value?.results ?? outcome?.results ?? []
116
+ const settled = []
117
+ for (const result of results) {
118
+ const meta = callMeta.get(result.callId)
119
+ if (!meta) continue
120
+ const s = state.steps[meta.item.stepId]
121
+ if (result.ok) {
122
+ if (meta.item.instance) {
123
+ const inst = s.instances.find((x) => x.key === meta.item.instance.key)
124
+ if (inst) {
125
+ inst.status = 'done'
126
+ inst.outputs = result.outputs
127
+ }
128
+ } else {
129
+ s.status = 'done'
130
+ s.outputs = result.outputs
131
+ }
132
+ store.step({ runId, stepId: meta.item.stepId, instance: meta.item.instance?.key, event: 'submit', body: { outputs: result.outputs } })
133
+ } else {
134
+ const failTarget = meta.item.instance ? s.instances.find((x) => x.key === meta.item.instance.key) : s
135
+ if (failTarget) {
136
+ failTarget.failCount++
137
+ const limit = meta.step.maxFail ?? ctx.budgets.maxStepFail ?? DEFAULT_MAX_STEP_FAIL
138
+ if (failTarget.failCount >= limit) {
139
+ failTarget.status = 'failed'
140
+ failTarget.error = result.error
141
+ } else {
142
+ failTarget.status = 'pending'
143
+ rotateCursor(state, meta.cursorKey)
144
+ }
145
+ }
146
+ store.step({ runId, stepId: meta.item.stepId, instance: meta.item.instance?.key, event: 'fail', body: { error: result.error, candidate: meta.slotKey } })
147
+ }
148
+ settled.push({ item: meta.item, result })
149
+ }
150
+ // 看门狗:引擎契约要求结果逐 callId 返回;缺失即引擎侧异常,按标准失败路径记账(failCount+上限)
151
+ const seenIds = new Set(results.map((r) => r.callId))
152
+ for (const call of calls) {
153
+ if (seenIds.has(call.callId)) continue
154
+ const meta = callMeta.get(call.callId)
155
+ const s = state.steps[meta.item.stepId]
156
+ const failTarget = meta.item.instance ? s.instances.find((x) => x.key === meta.item.instance.key) : s
157
+ const error = '引擎结果缺失该调用'
158
+ if (failTarget) {
159
+ failTarget.failCount++
160
+ const limit = meta.step.maxFail ?? ctx.budgets.maxStepFail ?? DEFAULT_MAX_STEP_FAIL
161
+ if (failTarget.failCount >= limit) {
162
+ failTarget.status = 'failed'
163
+ failTarget.error = error
164
+ } else {
165
+ failTarget.status = 'pending'
166
+ rotateCursor(state, meta.cursorKey)
167
+ }
168
+ }
169
+ store.step({ runId, stepId: meta.item.stepId, instance: meta.item.instance?.key, event: 'fail', body: { error, candidate: meta.slotKey } })
170
+ settled.push({ item: meta.item, result: { callId: call.callId, ok: false, error } })
171
+ }
172
+ return { settled }
173
+ }
@@ -0,0 +1,159 @@
1
+ // 就绪批次计算(调度域=剧本):依赖(plan.deps,gate 预计算)/for_each 展开/并发上限/审批就绪侦测/死锁防护
2
+ export const DEFAULT_CONCURRENCY = 4
3
+ const SELF_KEY = '-'
4
+
5
+ export function stepTypeOf(step) {
6
+ return step.type ?? 'ai'
7
+ }
8
+
9
+ // 升级步集(模板级语义:onExhausted 目标);是否在剧本由 planner-gate 裁决
10
+ export function escalateOnlyIds(template) {
11
+ const ids = new Set()
12
+ for (const step of template.steps) {
13
+ if (stepTypeOf(step) === 'approve' && typeof step.onExhausted === 'string' && step.onExhausted !== 'blocked') {
14
+ ids.add(step.onExhausted)
15
+ }
16
+ }
17
+ return ids
18
+ }
19
+
20
+ // 剧本视图:plan.steps(ref 子序列)映射为模板步骤对象;依赖表 = gate 重算的 plan.deps
21
+ export function scriptViewOf(template, plan) {
22
+ const byId = new Map(template.steps.map((s) => [s.id, s]))
23
+ const steps = (plan?.steps ?? []).map((p) => byId.get(p.ref)).filter(Boolean)
24
+ const depsById = new Map(Object.entries(plan?.deps ?? {}))
25
+ const planRefs = new Set((plan?.steps ?? []).map((p) => p.ref))
26
+ const escalateOnly = new Set([...escalateOnlyIds(template)].filter((id) => planRefs.has(id)))
27
+ return { steps, depsById, escalateOnly, byId }
28
+ }
29
+
30
+ const stepDone = (s) => s.status === 'done'
31
+ const stepBlockedInput = (s) => s.status === 'failed' || s.status === 'skipped'
32
+
33
+ // 数据源列表解析;源未完成返回 null
34
+ function sourceList(state, step) {
35
+ const [srcId, srcOut] = step.for_each.split('.')
36
+ const src = state.steps[srcId]
37
+ if (!src || !stepDone(src)) return null
38
+ const list = src.outputs?.[srcOut]
39
+ return Array.isArray(list) ? list : []
40
+ }
41
+
42
+ function expandForEach(state, step) {
43
+ const s = state.steps[step.id]
44
+ if (s.instances.length > 0) return
45
+ const list = sourceList(state, step)
46
+ if (list === null) return
47
+ s.instances = list.map((item, index) => ({
48
+ key: `#${index + 1}`, index, item, status: 'pending', outputs: null, failCount: 0, carry: null,
49
+ }))
50
+ }
51
+
52
+ // 宏观状态归一:实例级失败传播为步骤失败;实例全终态推导步骤态
53
+ function normalizeForEach(state, step) {
54
+ const s = state.steps[step.id]
55
+ if (!s || s.instances.length === 0 || s.status === 'done' || s.status === 'failed' || s.status === 'skipped') return
56
+ if (s.instances.some((i) => i.status === 'failed')) {
57
+ s.status = 'failed'
58
+ s.error = s.instances.find((i) => i.status === 'failed')?.error
59
+ return
60
+ }
61
+ if (s.instances.every((i) => i.status === 'done')) s.status = 'done'
62
+ }
63
+
64
+ // 就绪批次。kind:normal(并行 calls)/approve(外部裁决侦测)/flow(嵌套)/terminal/blocked/idle
65
+ export function nextBatch(state, script, { concurrency = DEFAULT_CONCURRENCY } = {}) {
66
+ const { steps, depsById, escalateOnly } = script
67
+
68
+ // 实例失败传播与步骤态归一
69
+ for (const step of steps) if (step.for_each) normalizeForEach(state, step)
70
+
71
+ // 依赖失败传播(宏观级)
72
+ for (const step of steps) {
73
+ if (escalateOnly.has(step.id)) continue
74
+ const s = state.steps[step.id]
75
+ if (!s || s.status !== 'pending') continue
76
+ const bad = (depsById.get(step.id) ?? []).some((d) => state.steps[d] && stepBlockedInput(state.steps[d]))
77
+ if (bad) {
78
+ s.status = 'skipped'
79
+ s.skipReason = '依赖失败传播'
80
+ s.instances = []
81
+ }
82
+ }
83
+
84
+ // 审批就绪侦测:target done 且依赖 done → 进入外部裁决(waiting_approval),不派发
85
+ for (const step of steps) {
86
+ if (stepTypeOf(step) !== 'approve') continue
87
+ const s = state.steps[step.id]
88
+ if (!s || s.status !== 'pending') continue
89
+ const depsOk = (depsById.get(step.id) ?? []).every((d) => stepDone(state.steps[d] ?? {}))
90
+ const target = state.steps[step.target]
91
+ if (depsOk && target && stepDone(target)) {
92
+ return { kind: 'approve', step }
93
+ }
94
+ }
95
+
96
+ const calls = []
97
+ for (const step of steps) {
98
+ if (escalateOnly.has(step.id)) continue
99
+ const type = stepTypeOf(step)
100
+ const s = state.steps[step.id]
101
+ if (!s || s.status !== 'pending') continue
102
+ const depsOk = (depsById.get(step.id) ?? []).every((d) => stepDone(state.steps[d] ?? {}))
103
+ if (!depsOk) continue
104
+ if (type === 'flow') return { kind: 'flow', step }
105
+ if (type !== 'ai') continue
106
+ if (step.for_each) {
107
+ expandForEach(state, step)
108
+ const list = sourceList(state, step)
109
+ if (list !== null && list.length === 0) {
110
+ s.status = 'skipped'
111
+ s.skipReason = 'for_each 数据源为空'
112
+ continue
113
+ }
114
+ let prev = null
115
+ // sequential 缺省(spec §8):仅显式 parallel 才并行,防缺省 mode 翻转语义
116
+ const sequential = step.mode !== 'parallel'
117
+ for (const inst of s.instances) {
118
+ const ready = sequential
119
+ ? inst.status === 'pending' && (prev === null || prev.status === 'done')
120
+ : inst.status === 'pending'
121
+ if (ready && sequential && prev) inst.carry = prev.outputs
122
+ prev = inst
123
+ if (!ready) continue
124
+ calls.push({ stepId: step.id, instance: { key: inst.key, index: inst.index, item: inst.item, carry: inst.carry } })
125
+ if (calls.length >= concurrency) return { kind: 'normal', calls }
126
+ }
127
+ } else {
128
+ calls.push({ stepId: step.id })
129
+ if (calls.length >= concurrency) return { kind: 'normal', calls }
130
+ }
131
+ }
132
+ if (calls.length > 0) return { kind: 'normal', calls }
133
+
134
+ // 升级步:唯一触发边 = 审批耗尽(escalateReady 标记)
135
+ for (const id of escalateOnly) {
136
+ const s = state.steps[id]
137
+ if (s && s.status === 'pending' && s.escalateReady) return { kind: 'normal', calls: [{ stepId: id, escalate: true }] }
138
+ }
139
+
140
+ const anyRunning = steps.some((step) => {
141
+ const s = state.steps[step.id]
142
+ if (!s) return false
143
+ return s.status === 'running' || (s.instances ?? []).some((i) => i.status === 'running')
144
+ })
145
+ const anyPending = steps.some((step) => {
146
+ const s = state.steps[step.id]
147
+ if (!s || s.status !== 'pending') return false
148
+ if (escalateOnly.has(step.id)) return !!s.escalateReady
149
+ return true
150
+ })
151
+
152
+ if (!anyRunning) {
153
+ if (anyPending) return { kind: 'blocked', reason: '无就绪且无在飞但存在未完成步骤(依赖链缺陷)' }
154
+ return { kind: 'terminal' }
155
+ }
156
+ return { kind: 'idle' }
157
+ }
158
+
159
+ export { SELF_KEY }
package/lib/index.js CHANGED
@@ -1,214 +1,31 @@
1
- /**
2
- * dsh-rs-workflow — 若水工作流 (rs-workflow) 一体化插件。
3
- *
4
- * 一个包,三种行角色(由组合行的 config.role 决定,加载前经 Config 校验):
5
- * - "settings":注册 settings 命名空间 "rs-workflow",GUI 设置页出现配置表单
6
- * (3 基础工作位 + 13 细分工作位 + 工作流默认项 + 预算,语义对齐 rs-tui 原始配置)。
7
- * 放在 profile 的 cordis.patch.yml(host 平面,常驻;裸包名从 profile
8
- * node_modules 解析)。
9
- * - "preset-sync":把包内 preset/rs-workflow(preset.yml + agent.cordis.yml +
10
- * skills 协议技能与编排引擎)幂等同步到 <dsh-home>/.agent-presets/rs-workflow,
11
- * 模式选择器即出现"若水工作流"。同为 host 平面行;升级包后重启即更新 preset。
12
- * 卸载时 pnpm 不执行依赖的 preuninstall(实验证实),残留 preset 因 tool 行
13
- * import 失败在选择器显示 broken,手动清理命令见 README。
14
- * - "tool":注册模型工具 rs_workflow_config,主代理启动工作流编排前读取当前
15
- * 配置。放在释放出的 preset 组合(agent 平面,仅该模式可见;预设行的
16
- * 裸包名经 PresetTree.import 以组合 baseUrl 锚定 profile 目录、上溯
17
- * node_modules 解析,直接命中 dsh plugin add 安装的本包)。
18
- *
19
- * 三个角色读写同一 settings 命名空间:settings 行负责注册与默认值(组合 config 即
20
- * base 层),tool 行只在执行时经 ctx.get("settings") 读取宿主进程里的同一
21
- * 实例,不注册、不产生第二个实例。
22
- */
23
- import z from "@deepseek-ai/schemastery";
24
- import { defineTool } from "@deepseek-ai/dsh-tools";
25
- import { presetDest, removePreset, syncPreset } from "./preset-sync.mjs";
26
-
27
- const name = "rs-workflow";
28
-
29
- const NAMESPACE = "rs-workflow";
30
-
31
- const TEMPLATES = ["auto", "lite", "plan-final", "step-review", "multi-plan"];
32
-
33
- // tool 输出 schema 的工作位键集,与 buildSlots 互为镜像(非派生):
34
- // tests/workflow-parity.test.mjs 对拍钉住,增删工作位须双侧同步
35
- const SLOT_KEYS = [
36
- "planner", "executor", "reviewer",
37
- "planner-triage", "planner-command", "planner-subplan", "planner-escalate",
38
- "reviewer-plan", "reviewer-task", "reviewer-subplan", "reviewer-final", "reviewer-cross",
39
- "executor-task", "executor-enhance", "executor-retry", "executor-escalate",
40
- ];
41
-
42
- // 预算字段与默认值对齐 rs-tui budgets(config.ts:290-299),clamp 边界同 node-tree BUDGET_MIN/MAX
43
- const BUDGET_DEFAULTS = {
44
- reviewRejectBeforeEscalate: 2,
45
- planRejectBeforeBlocked: 2,
46
- emptyOutputRetryLimit: 3,
47
- reportNudgeLimit: 3,
48
- };
49
- const BUDGET_MIN = 1;
50
- const BUDGET_MAX = 10;
51
-
52
- /** 预算数值子 schema;clamp 在引擎侧执行,schema 只声明合法域。 */
53
- function buildBudgets() {
54
- const budget = (def, description) => z.number().default(def).min(BUDGET_MIN).max(BUDGET_MAX).description(description);
55
- return z.object({
56
- reviewRejectBeforeEscalate: budget(BUDGET_DEFAULTS.reviewRejectBeforeEscalate, "任务连续被拒/失败达此值触发升级重规划(含自报失败与调用失败)"),
57
- planRejectBeforeBlocked: budget(BUDGET_DEFAULTS.planRejectBeforeBlocked, "计划/子计划连续被拒达此值触发升级重规划"),
58
- emptyOutputRetryLimit: budget(BUDGET_DEFAULTS.emptyOutputRetryLimit, "审批缺验证证据时重问上限,超限视为拒绝"),
59
- reportNudgeLimit: budget(BUDGET_DEFAULTS.reportNudgeLimit, "执行完成但未给出交接摘要时追问上限"),
60
- });
61
- }
62
-
63
- /** 工作位(slot)子 schema;细分位场景文案对齐 rs-tui SLOT_SCENES。string/array/{rotation} 三态:array 与 rotation 数组等价(候选依次轮换,被拒重做/重问换模型)。工厂函数保证 Config 与 SETTINGS_SCHEMA 各持独立实例。 */
64
- function buildSlots() {
65
- const slot = (description) => z.union([z.string(), z.array(z.string()), z.object({ rotation: z.array(z.string()) })]).default("").description(description);
66
- return z.object({
67
- planner: slot("planner 基础位(规划域兜底)。格式 provider/model、候选数组或 {rotation:[...]},留空 = 会话默认模型"),
68
- executor: slot("executor 基础位(执行域兜底);候选依次轮换,被拒重做/重试换模型"),
69
- reviewer: slot("reviewer 基础位(审批域兜底);候选依次轮换"),
70
- "planner-triage": slot("细分位:首次分诊,分析需求选模板拆任务;缺省降级 planner"),
71
- "planner-command": slot("细分位:总规划,计划重写与大纲修订;缺省降级 planner"),
72
- "planner-subplan": slot("细分位:子计划细化,子计划内任务拆解;缺省降级 planner"),
73
- "planner-escalate": slot("细分位:升级重规划,连续拒绝超阈后的尾段重拆;缺省降级 planner"),
74
- "reviewer-plan": slot("细分位:计划审批,审批计划文本与大纲;缺省降级 reviewer"),
75
- "reviewer-task": slot("细分位:任务审批,审批单个任务执行结果;缺省降级 reviewer"),
76
- "reviewer-subplan": slot("细分位:子计划交付审批,审批整个子计划交付;缺省降级 reviewer"),
77
- "reviewer-final": slot("细分位:单终审,末尾终审全部交付;缺省降级 reviewer,建议配更强的模型"),
78
- "reviewer-cross": slot("细分位:交叉终审,多视角交叉终审链;缺省降级 reviewer,建议配更强的模型"),
79
- "executor-task": slot("细分位:任务首次执行;缺省降级 executor"),
80
- "executor-enhance": slot("细分位:被拒重做,携带 REJECTED 理由修改重交;缺省降级 executor"),
81
- "executor-retry": slot("细分位:失败重试,自报失败/无产出后的重试;缺省降级 executor"),
82
- "executor-escalate": slot("细分位:升级后执行,升级重规划产出的新任务;缺省降级 executor"),
83
- });
1
+ // dsh-rs-workflow — 若水工作流 v5:人机协作的强规则编排。
2
+ // 主循环(人机接口+规划者)+ 分段 continuable job 编排;配置/模板存自有文件(~/.dsh/dsh-rs-workflow/v5/),
3
+ // 不经宿主 settings 服务,不写 settings.yaml。本文件只做行角色分发,业务在 lib/ 各模块。
4
+ import z from '@deepseek-ai/schemastery'
5
+ import { registerBoardRoutes } from './board.mjs'
6
+ import { registerOrchestrator } from './orchestrator.mjs'
7
+ import { registerTemplateTool } from './template-tool.mjs'
8
+ import { SPEC_TEXT } from './spec.mjs'
9
+
10
+ export const name = 'rs-workflow'
11
+ export { SPEC_TEXT }
12
+
13
+ // 组合行 config schema(行分发字段 role 必填)
14
+ export const Config = z.object({
15
+ role: z.union(['board', 'orchestrator', 'template-tool']).required(),
16
+ templateId: z.string().description('orchestrator:本组合锚定的模板 id(释放生成器按组合名写入);orchestrator 角色必填'),
17
+ })
18
+
19
+ export function apply(ctx, config) {
20
+ const cfg = Config(config || {})
21
+ if (cfg.role === 'board') {
22
+ registerBoardRoutes(ctx)
23
+ return
24
+ }
25
+ if (cfg.role === 'orchestrator') {
26
+ if (!cfg.templateId) throw new Error('orchestrator 行必须配置 templateId(组合锚定)')
27
+ registerOrchestrator(ctx, { templateId: cfg.templateId })
28
+ return
29
+ }
30
+ registerTemplateTool(ctx)
84
31
  }
85
-
86
- /** 工作流默认项子 schema。maxTasks 为 DSH 原生任务预算(rs-tui 无数量上限),约束全部任务实例化;
87
- * 引擎侧对 <=0/非数值另有自保回落(见 engine.js),此处 schema 声明合法域挡住非法值。 */
88
- const MAX_TASKS_DEFAULT = 8;
89
- const MAX_TASKS_MIN = 1;
90
- const MAX_TASKS_MAX = 64;
91
- function buildWorkflow() {
92
- return z.object({
93
- defaultTemplate: z.union(TEMPLATES).default("auto").description("默认模板:auto = planner 分诊自动选型(无信号时兜底 multi-plan);其余 = 无信号时兜底该模板,planner 声明与分诊矩阵仍优先生效"),
94
- maxTasks: z.number().default(MAX_TASKS_DEFAULT).min(MAX_TASKS_MIN).max(MAX_TASKS_MAX).description("单轮任务拆解数上限(含子计划运行时任务的全局预算)"),
95
- });
96
- }
97
-
98
- /** settings 命名空间 schema(GUI 表单渲染的就是它,不含行角色字段)。 */
99
- const SETTINGS_SCHEMA = z.object({
100
- slots: buildSlots(),
101
- workflow: buildWorkflow(),
102
- budgets: buildBudgets(),
103
- });
104
-
105
- /** 组合行 config schema(在 settings 之上多一个行角色字段)。 */
106
- const Config = z.object({
107
- role: z.union(["settings", "preset-sync", "tool"]).required().description("行角色:settings = 注册 GUI 设置命名空间(host 层常驻);preset-sync = 同步释放 agent preset 到用户预设根(host 层常驻);tool = 注册 rs_workflow_config 模型工具(预设层)"),
108
- slots: buildSlots(),
109
- workflow: buildWorkflow(),
110
- budgets: buildBudgets(),
111
- });
112
-
113
- /** 从(已解析的)行 config 里摘出 settings base 层。 */
114
- function baseOf(config) {
115
- return { slots: config.slots, workflow: config.workflow, budgets: config.budgets };
116
- }
117
-
118
- function apply(ctx, config) {
119
- const cfg = Config(config || {});
120
- if (cfg.role === "preset-sync") {
121
- try {
122
- const outcome = syncPreset();
123
- if (outcome === "skipped-foreign") {
124
- ctx.logger?.warn?.("rs-workflow preset 目录无有效来源标记或归属他人,已保留不覆盖;确认后手动删除,下次启动即由本包接管");
125
- } else if (outcome === "created") {
126
- ctx.logger?.info?.(`rs-workflow preset 已首次释放到 ${presetDest()}`);
127
- } else if (outcome === "updated") {
128
- ctx.logger?.info?.(`rs-workflow preset 已同步更新: ${presetDest()}`);
129
- }
130
- } catch (error) {
131
- ctx.logger?.warn?.(`rs-workflow preset 同步失败(不影响本插件其余角色): ${error?.code ? `[${error.code}] ` : ""}${error?.message ?? error}; 目标: ${presetDest()}`);
132
- }
133
- return;
134
- }
135
- if (cfg.role === "settings") {
136
- ctx.inject(["settings"], (sctx) => {
137
- sctx.settings.register(NAMESPACE, SETTINGS_SCHEMA, { base: baseOf(cfg) });
138
- });
139
- return;
140
- }
141
- // tools 以嵌套 inject 声明:服务缺失时仅 tool 角色保持未激活(干净禁用),
142
- // settings/preset-sync 角色不再被模块级 inject 连坐(非 dsh-base 组合下旧形态
143
- // 会因启动审计整树 fatal)
144
- ctx.inject(["tools"], (tctx) => {
145
- tctx.tools.register(defineTool({
146
- name: "rs_workflow_config",
147
- description: "读取若水工作流 (rs-workflow) 的当前配置:各角色的模型工作位 (slots)、工作流默认项 (workflow) 与预算 (budgets)。启动 workflow 编排前必须先调用本工具:slots 原样作为 workflow 调用 args.slots;workflow.defaultTemplate 作为 args.defaultTemplate(auto = 无信号时兜底 multi-plan,其余值 = 无信号时兜底该值;planner 声明与分诊矩阵始终优先);workflow.maxTasks 作为 args.limits.maxTasks;budgets 原样作为 args.budgets。",
148
- parameters: {},
149
- output: {
150
- schema: {
151
- type: "object",
152
- additionalProperties: false,
153
- properties: {
154
- slots: {
155
- type: "object",
156
- required: true,
157
- additionalProperties: true,
158
- properties: Object.fromEntries(SLOT_KEYS.map((k) => [k, {
159
- oneOf: [
160
- { type: "string" },
161
- { type: "array", items: { type: "string" } },
162
- { type: "object", additionalProperties: false, properties: { rotation: { type: "array", items: { type: "string" } } } },
163
- ],
164
- }])),
165
- },
166
- workflow: {
167
- type: "object",
168
- required: true,
169
- additionalProperties: false,
170
- properties: {
171
- defaultTemplate: { type: "string", enum: TEMPLATES },
172
- maxTasks: { type: "number" },
173
- },
174
- },
175
- budgets: {
176
- type: "object",
177
- required: true,
178
- additionalProperties: false,
179
- properties: {
180
- reviewRejectBeforeEscalate: { type: "number" },
181
- planRejectBeforeBlocked: { type: "number" },
182
- emptyOutputRetryLimit: { type: "number" },
183
- reportNudgeLimit: { type: "number" },
184
- },
185
- },
186
- source: { type: "string", required: true, enum: ["settings", "fallback"] },
187
- },
188
- },
189
- render: (_args, value) => [{
190
- type: "text",
191
- text: JSON.stringify({ slots: value.slots, workflow: value.workflow, budgets: value.budgets }, null, 2)
192
- + (value.source === "fallback" ? "\n(设置服务不可用:返回的是组合默认值,GUI 修改不在此生效)" : ""),
193
- }],
194
- },
195
- execute() {
196
- const settings = tctx.get("settings");
197
- let value;
198
- try {
199
- value = settings ? settings.get(NAMESPACE) : undefined;
200
- } catch (error) {
201
- // settings 服务在但读取失败(命名空间被移除/配置损坏)时走组合默认值,工具永不硬失败
202
- ctx.logger?.warn?.(`rs-workflow settings 读取失败,回退组合默认值: ${error?.message ?? error}`);
203
- }
204
- if (value) {
205
- return Promise.resolve({ slots: value.slots, workflow: value.workflow, budgets: value.budgets, source: "settings" });
206
- }
207
- return Promise.resolve({ slots: cfg.slots, workflow: cfg.workflow, budgets: cfg.budgets, source: "fallback" });
208
- },
209
- presentCall: () => ({ card: "generic", title: "读取若水工作流配置", kind: "other", rawInput: {} }),
210
- }));
211
- });
212
- }
213
-
214
- export { BUDGET_DEFAULTS, BUDGET_MAX, BUDGET_MIN, Config, MAX_TASKS_DEFAULT, MAX_TASKS_MAX, MAX_TASKS_MIN, NAMESPACE, SETTINGS_SCHEMA, TEMPLATES, apply, name, presetDest, removePreset, syncPreset };