@mzzsfy/dsh-rs-workflow 1.0.1 → 1.1.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/lib/storage.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  // storage — rs-workflow 自有文件存储(不经宿主 settings 服务,settings.yaml 不承载本插件配置)
2
2
  // 布局:<dataDir>/v5/{templates,config}.json;dataDir(DSH_RS_WORKFLOW_DATA_DIR 覆写)
3
3
  import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
4
- import { homedir, tmpdir } from 'node:os'
4
+ import { homedir } from 'node:os'
5
5
  import { dirname, join } from 'node:path'
6
6
 
7
7
  export function dataDir() {
@@ -14,18 +14,27 @@ function v5File(name) {
14
14
  }
15
15
 
16
16
  export function loadJson(name, fallback) {
17
+ let raw
17
18
  try {
18
- return JSON.parse(readFileSync(v5File(name), 'utf8'))
19
+ raw = readFileSync(v5File(name), 'utf8')
19
20
  } catch {
21
+ // 缺失即首启:回默认;存在但读失败同路,下次 save 重建
22
+ return fallback
23
+ }
24
+ try {
25
+ return JSON.parse(raw)
26
+ } catch (e) {
27
+ // 损坏不可静默吞成默认值:留痕后回退,否则损坏文件被下一次 save 无声覆盖
28
+ console.error(`[rsww] ${name} 解析失败,回退默认值:${e?.message ?? e}`)
20
29
  return fallback
21
30
  }
22
31
  }
23
32
 
24
- // 写临时文件后原子改名,断电不产生半截 JSON
33
+ // 写同目录临时文件后原子改名:跨卷 tmpdir 会触发 EXDEV;同盘保证 rename 原子性
25
34
  export function saveJson(name, data) {
26
35
  const target = v5File(name)
27
36
  mkdirSync(dirname(target), { recursive: true })
28
- const tmp = `${join(tmpdir(), 'rsww')}-${Date.now()}-${Math.random().toString(36).slice(2)}.json`
37
+ const tmp = `${target}.${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`
29
38
  try {
30
39
  writeFileSync(tmp, JSON.stringify(data, null, 2))
31
40
  renameSync(tmp, target)
package/lib/store.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  // run-store(v5):每 run 全量 JSON + index.json 索引;LRU 容量收敛;零截断
2
2
  // namespace 子目录 v5/(旧数据物理隔离);record 增 plan/warnings/controls[].by(见 data-design.md)
3
3
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs'
4
- import { homedir, tmpdir } from 'node:os'
4
+ import { homedir } from 'node:os'
5
5
  import { join } from 'node:path'
6
6
 
7
7
  export const KEEP_RUNS = 200
@@ -31,9 +31,14 @@ export function createStore({ dir = defaultDataDir(), logger = console, keepRuns
31
31
  const warn = (message) => logger.warn?.(`[rsww-store] ${message}`)
32
32
 
33
33
  const writeAtomic = (path, text) => {
34
- const tmp = join(tmpdir(), `rsww-${Date.now()}-${Math.random().toString(36).slice(2)}`)
35
- writeFileSync(tmp, text, 'utf8')
36
- renameSync(tmp, path)
34
+ // 临时文件落目标同目录:跨卷 tmpdir rename 触发 EXDEV;同盘保证原子性
35
+ const tmp = `${path}.${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`
36
+ try {
37
+ writeFileSync(tmp, text, 'utf8')
38
+ renameSync(tmp, path)
39
+ } finally {
40
+ rmSync(tmp, { force: true })
41
+ }
37
42
  }
38
43
 
39
44
  const persistRun = (record) => writeAtomic(join(runsDir, `${record.runId}.json`), JSON.stringify(record, null, 2))
@@ -54,7 +59,7 @@ export function createStore({ dir = defaultDataDir(), logger = console, keepRuns
54
59
  const at = Date.now()
55
60
  record.status = 'cancelled'
56
61
  record.finishedAt = at
57
- if (!record.summary) record.summary = '进程重启,运行中断,可在会话页签断点续跑'
62
+ if (!record.summary) record.summary = '进程重启,运行已自动取消,可在会话页签断点续跑'
58
63
  record.controls = [...(record.controls ?? []), { at, kind: 'cancel', text: '进程重启,自动收敛' }]
59
64
  persistRun(record)
60
65
  const entry = index.find((e) => e.runId === record.runId)
@@ -101,13 +106,17 @@ export function createStore({ dir = defaultDataDir(), logger = console, keepRuns
101
106
  warn(`运行记录 ${name} 读取失败,跳过:${e.message}`)
102
107
  }
103
108
  }
104
- // 对账:runs/ 文件集合为权威修齐 index(缺行补、幽灵行删、收敛改写行落盘),仅内存修正
109
+ // 对账:runs/ 文件集合为权威修齐 index(缺行补、幽灵行删、已存行内容刷新),仅内存修正
105
110
  let repaired = settledCount > 0
106
111
  const fileIds = new Set(records.keys())
107
112
  for (const record of records.values()) {
108
- if (!index.some((e) => e.runId === record.runId)) {
113
+ const entry = index.find((e) => e.runId === record.runId)
114
+ if (entry === undefined) {
109
115
  index.push(indexEntryOf(record))
110
116
  repaired = true
117
+ } else {
118
+ // finish 先写 run 文件后写 index,中断会留旧行;以文件记录刷新,消除失真窗口
119
+ Object.assign(entry, indexEntryOf(record))
111
120
  }
112
121
  }
113
122
  for (let i = index.length - 1; i >= 0; i--) {
@@ -154,9 +163,18 @@ export function createStore({ dir = defaultDataDir(), logger = console, keepRuns
154
163
  state: state ?? emptyState(), controls: [], stepsTrace: {}, queued: [],
155
164
  }
156
165
  records.set(id, record)
157
- index.unshift(indexEntryOf(record))
158
- persistRun(record)
159
- persistIndex()
166
+ const entry = indexEntryOf(record)
167
+ index.unshift(entry)
168
+ try {
169
+ persistRun(record)
170
+ persistIndex()
171
+ } catch (e) {
172
+ // 落盘失败须回滚内存,否则同 id 重试恒撞"runId 已存在"且列表含幽灵行
173
+ records.delete(id)
174
+ const at = index.indexOf(entry)
175
+ if (at >= 0) index.splice(at, 1)
176
+ throw e
177
+ }
160
178
  evictOverCapacity()
161
179
  return record
162
180
  },
@@ -1,161 +1,172 @@
1
- // template-tool — rs_workflow_template 模型工具:AI 按用户口述逻辑生成/修改流程模板
2
- // 激活于已创建组合行(template-tool 行);落盘走自有文件存储与 release 模块,不直接触碰预设目录(v5)
3
- import { defineTool } from '@deepseek-ai/dsh-tools'
4
- import { parseTemplate, validateTemplate, validateTemplateSet } from './template.mjs'
5
- import { SPEC_TEXT } from './spec.mjs'
6
- import { loadJson, saveJson } from './storage.mjs'
7
- import { releaseFlowTemplate, unreleaseFlowTemplate } from './release.mjs'
8
-
9
- const ACTIONS = ['spec', 'list', 'save', 'remove']
10
-
11
- function normalizeTemplates(value) {
12
- if (!Array.isArray(value)) return []
13
- return value
14
- .filter((t) => t && typeof t === 'object')
15
- .map((t) => ({
16
- id: typeof t.id === 'string' ? t.id : '',
17
- label: typeof t.label === 'string' ? t.label : '',
18
- description: typeof t.description === 'string' ? t.description : '',
19
- enabled: t.enabled !== false,
20
- json: typeof t.json === 'string' ? t.json : '',
21
- }))
22
- .filter((t) => t.id !== '')
23
- }
24
-
25
- export function createTemplateTool({ getTemplates, setTemplates, removeTemplate, releaseTemplate, unreleaseTemplate, logger }) {
26
- return defineTool({
27
- name: 'rs_workflow_template',
28
- description: [
29
- '若水工作流流程模板的 AI 编辑入口:用户口述流程逻辑,你据此生成/修改流程模板(严格 JSON)。',
30
- '先调 {action:"spec"} 获取 DSL v5 规范与示例,再 {action:"save", template:{...}, release:true} 保存并创建为可选模式。',
31
- 'list 列出现有模板;remove 按 id 移除并撤下其模式。',
32
- '每步以 outputs 声明结构化产出契约(子代理以结构化工具提交,引擎强制校验);人工审校用 type:"approve" 原语;',
33
- '流程入参用 inputs;禁止把「希望模型怎么做」写成口头约定。',
34
- ].join(''),
35
- parameters: {
36
- action: { type: 'string', required: true, enum: ACTIONS, description: 'spec=获取 DSL 规范;list=列模板;save=保存模板(可同时创建);remove=移除模板' },
37
- template: {
38
- type: 'object',
39
- additionalProperties: true,
40
- description: 'save:模板对象 {id,label,description,enabled,json};json 为流程定义严格 JSON 文本(先调 spec 按规范写)',
41
- },
42
- id: { type: 'string', description: 'remove:要移除的模板 id' },
43
- release: { type: 'boolean', description: 'save:true=保存后立即创建为可选模式(rs-<id>)' },
44
- dryRun: { type: 'boolean', description: 'save:true=仅校验不落盘(编辑器「校验」同源通道)' },
45
- },
46
- output: {
47
- schema: {
48
- type: 'object',
49
- additionalProperties: false,
50
- properties: {
51
- ok: { type: 'boolean', required: true },
52
- action: { type: 'string' },
53
- spec: { type: 'string' },
54
- templates: { type: 'array', items: { type: 'object', additionalProperties: true } },
55
- released: { type: 'boolean' },
56
- presetId: { type: 'string' },
57
- errors: { type: 'array', items: { type: 'string' } },
58
- error: { type: 'string' },
59
- },
60
- },
61
- render: (_args, value) => [{
62
- type: 'text',
63
- text: value.spec !== undefined ? value.spec : JSON.stringify(value, null, 2),
64
- }],
65
- },
66
- async execute(args) {
67
- const action = args.action
68
- if (action === 'spec') {
69
- return { ok: true, action, spec: SPEC_TEXT }
70
- }
71
- if (action === 'list') {
72
- const templates = normalizeTemplates(await getTemplates())
73
- return { ok: true, action, templates }
74
- }
75
- if (action === 'save') {
76
- const t = args.template && typeof args.template === 'object' ? args.template : null
77
- if (!t) return { ok: false, action, error: '缺少 template 对象 {id,label,description,json}' }
78
- const id = typeof t.id === 'string' ? t.id.trim() : ''
79
- const json = typeof t.json === 'string' ? t.json : ''
80
- if (id === '' || json.trim() === '') return { ok: false, action, error: 'template.id 与 template.json 均为必填' }
81
- let parsed
82
- try {
83
- parsed = parseTemplate(json)
84
- } catch (error) {
85
- return { ok: false, action, errors: ['JSON 解析失败: ' + String(error?.message ?? error)] }
86
- }
87
- if (parsed && typeof parsed.id === 'string' && parsed.id !== id) {
88
- return { ok: false, action, errors: [`template.id("${id}") 与流程定义内 id("${parsed.id}") 不一致`] }
89
- }
90
- // 单模板校验 + 与既有模板并集跨流程校验(动态路由目标存在性;损坏既有模板跳过)
91
- const single = validateTemplate(parsed)
92
- if (single.length > 0) return { ok: false, action, errors: single.map((e) => `${e.target}: ${e.message}`) }
93
- const others = normalizeTemplates(await getTemplates()).filter((item) => item.id !== id)
94
- const otherFlows = []
95
- for (const item of others) {
96
- try {
97
- otherFlows.push(parseTemplate(item.json))
98
- } catch { /* 既有模板损坏不阻塞新模板保存 */ }
99
- }
100
- const cross = validateTemplateSet([parsed, ...otherFlows].filter(Boolean))
101
- if (cross.length > 0) return { ok: false, action, errors: cross.map((e) => `${e.target}: ${e.message}`) }
102
- const entry = {
103
- id,
104
- label: typeof t.label === 'string' && t.label.trim() !== '' ? t.label.trim() : (parsed && parsed.label) || id,
105
- description: typeof t.description === 'string' && t.description.trim() !== '' ? t.description.trim() : (parsed && parsed.description) || '',
106
- enabled: t.enabled !== false,
107
- json,
108
- }
109
- if (args.dryRun === true) {
110
- return { ok: true, action, templates: normalizeTemplates([...others, entry]) }
111
- }
112
- await setTemplates([...others, entry])
113
- let released = false
114
- let presetId = ''
115
- if (args.release === true) {
116
- released = (await releaseTemplate(entry)) === true
117
- presetId = 'rs-' + id
118
- }
119
- logger?.info?.(`rs-workflow 模板已保存: ${id}${released ? `(已创建为模式 ${presetId})` : ''}`)
120
- return { ok: true, action, released, presetId, templates: normalizeTemplates([...others, entry]) }
121
- }
122
- if (action === 'remove') {
123
- const id = typeof args.id === 'string' ? args.id.trim() : ''
124
- if (id === '') return { ok: false, action, error: '缺少 id' }
125
- const outcome = await removeTemplate(id)
126
- if (!outcome.ok) return { ok: false, action, error: outcome.error }
127
- return { ok: true, action, presetId: 'rs-' + id, templates: outcome.templates }
128
- }
129
- return { ok: false, action, error: '未知 action: ' + action }
130
- },
131
- presentCall: () => ({ card: 'generic', title: '编辑若水工作流模板', kind: 'other', rawInput: {} }),
132
- })
133
- }
134
-
135
- // 模式行激活入口:自有文件存储读写 + release 落盘(ctx = agent realm 组合行上下文)
136
- export function registerTemplateTool(ctx) {
137
- ctx.inject(['tools'], (tctx) => {
138
- tctx.effect(() => tctx.tools.register(createTemplateTool({
139
- getTemplates: () => loadJson('templates.json', []),
140
- setTemplates: (templates) => saveJson('templates.json', templates),
141
- removeTemplate: (id) => {
142
- const raw = loadJson('templates.json', [])
143
- const next = raw.filter((t) => t.id !== id)
144
- if (next.length === raw.length) return { ok: false, error: '模板不存在: ' + id }
145
- saveJson('templates.json', next)
146
- const outcome = unreleaseFlowTemplate(id)
147
- return { ok: true, templates: next, outcome }
148
- },
149
- releaseTemplate: (entry) => {
150
- try {
151
- return releaseFlowTemplate(entry) !== 'foreign'
152
- } catch (error) {
153
- ctx.logger?.warn?.(`rs-workflow 模板释放失败: ${error?.message ?? error}`)
154
- return false
155
- }
156
- },
157
- unreleaseTemplate: (id) => unreleaseFlowTemplate(id),
158
- logger: ctx.logger,
159
- })), 'rs-workflow template tool')
160
- })
161
- }
1
+ // template-tool — rs_workflow_template 模型工具:AI 按用户口述逻辑生成/修改流程模板
2
+ // 激活于已创建组合行(template-tool 行);落盘走自有文件存储与释放分派,不直接触碰预设目录(v5)
3
+ import { defineTool } from '@deepseek-ai/dsh-tools'
4
+ import { parseTemplate, validateTemplate, validateTemplateSet } from './template.mjs'
5
+ import { SPEC_TEXT } from './spec.mjs'
6
+ import { loadJson, saveJson } from './storage.mjs'
7
+ import { currentAgentPresets, markReleased, releaseTemplateVia, unreleaseTemplateVia } from './release-registry.mjs'
8
+
9
+ const ACTIONS = ['spec', 'list', 'save', 'remove']
10
+
11
+ function normalizeTemplates(value) {
12
+ if (!Array.isArray(value)) return []
13
+ return value
14
+ .filter((t) => t && typeof t === 'object')
15
+ .map((t) => ({
16
+ id: typeof t.id === 'string' ? t.id : '',
17
+ label: typeof t.label === 'string' ? t.label : '',
18
+ description: typeof t.description === 'string' ? t.description : '',
19
+ enabled: t.enabled !== false,
20
+ json: typeof t.json === 'string' ? t.json : '',
21
+ // released 是注册形态的释放事实源(templates.json),归一化不得剥离
22
+ ...(t.released === true ? { released: true } : {}),
23
+ }))
24
+ .filter((t) => t.id !== '')
25
+ }
26
+
27
+ export function createTemplateTool({ getTemplates, setTemplates, removeTemplate, releaseTemplate, logger }) {
28
+ return defineTool({
29
+ name: 'rs_workflow_template',
30
+ description: [
31
+ '若水工作流流程模板的 AI 编辑入口:用户口述流程逻辑,你据此生成/修改流程模板(严格 JSON)。',
32
+ '先调 {action:"spec"} 获取 DSL v5 规范与示例,再 {action:"save", template:{...}, release:true} 保存并创建为可选模式。',
33
+ 'list 列出现有模板;remove 按 id 移除并撤下其模式。',
34
+ '每步以 outputs 声明结构化产出契约(子代理以结构化工具提交,引擎强制校验);人工审校用 type:"approve" 原语;',
35
+ '流程入参用 inputs;禁止把「希望模型怎么做」写成口头约定。',
36
+ ].join(''),
37
+ parameters: {
38
+ action: { type: 'string', required: true, enum: ACTIONS, description: 'spec=获取 DSL 规范;list=列模板;save=保存模板(可同时创建);remove=移除模板' },
39
+ template: {
40
+ type: 'object',
41
+ additionalProperties: true,
42
+ description: 'save:模板对象 {id,label,description,enabled,json};json 为流程定义严格 JSON 文本(先调 spec 按规范写)',
43
+ },
44
+ id: { type: 'string', description: 'remove:要移除的模板 id' },
45
+ release: { type: 'boolean', description: 'save:true=保存后立即创建为可选模式(rs-<id>)' },
46
+ dryRun: { type: 'boolean', description: 'save:true=仅校验不落盘(编辑器「校验」同源通道)' },
47
+ },
48
+ output: {
49
+ schema: {
50
+ type: 'object',
51
+ additionalProperties: false,
52
+ properties: {
53
+ ok: { type: 'boolean', required: true },
54
+ action: { type: 'string' },
55
+ spec: { type: 'string' },
56
+ templates: { type: 'array', items: { type: 'object', additionalProperties: true } },
57
+ released: { type: 'boolean' },
58
+ presetId: { type: 'string' },
59
+ errors: { type: 'array', items: { type: 'string' } },
60
+ error: { type: 'string' },
61
+ },
62
+ },
63
+ render: (_args, value) => [{
64
+ type: 'text',
65
+ text: value.spec !== undefined ? value.spec : JSON.stringify(value, null, 2),
66
+ }],
67
+ },
68
+ async execute(args) {
69
+ const action = args.action
70
+ if (action === 'spec') {
71
+ return { ok: true, action, spec: SPEC_TEXT }
72
+ }
73
+ if (action === 'list') {
74
+ const templates = normalizeTemplates(await getTemplates())
75
+ return { ok: true, action, templates }
76
+ }
77
+ if (action === 'save') {
78
+ const t = args.template && typeof args.template === 'object' ? args.template : null
79
+ if (!t) return { ok: false, action, error: '缺少 template 对象 {id,label,description,json}' }
80
+ const id = typeof t.id === 'string' ? t.id.trim() : ''
81
+ const json = typeof t.json === 'string' ? t.json : ''
82
+ if (id === '' || json.trim() === '') return { ok: false, action, error: 'template.id 与 template.json 均为必填' }
83
+ let parsed
84
+ try {
85
+ parsed = parseTemplate(json)
86
+ } catch (error) {
87
+ return { ok: false, action, errors: ['JSON 解析失败: ' + String(error?.message ?? error)] }
88
+ }
89
+ if (parsed && typeof parsed.id === 'string' && parsed.id !== id) {
90
+ return { ok: false, action, errors: [`template.id("${id}") 与流程定义内 id("${parsed.id}") 不一致`] }
91
+ }
92
+ // 单模板校验 + 与既有模板并集跨流程校验(动态路由目标存在性;损坏既有模板跳过)
93
+ const single = validateTemplate(parsed)
94
+ if (single.length > 0) return { ok: false, action, errors: single.map((e) => `${e.target}: ${e.message}`) }
95
+ const all = normalizeTemplates(await getTemplates())
96
+ const others = all.filter((item) => item.id !== id)
97
+ const otherFlows = []
98
+ for (const item of others) {
99
+ try {
100
+ otherFlows.push(parseTemplate(item.json))
101
+ } catch { /* 既有模板损坏不阻塞新模板保存 */ }
102
+ }
103
+ const cross = validateTemplateSet([parsed, ...otherFlows].filter(Boolean))
104
+ if (cross.length > 0) return { ok: false, action, errors: cross.map((e) => `${e.target}: ${e.message}`) }
105
+ const entry = {
106
+ id,
107
+ label: typeof t.label === 'string' && t.label.trim() !== '' ? t.label.trim() : (parsed && parsed.label) || id,
108
+ description: typeof t.description === 'string' && t.description.trim() !== '' ? t.description.trim() : (parsed && parsed.description) || '',
109
+ enabled: t.enabled !== false,
110
+ json,
111
+ // 编辑性重存不改变释放态(注销走 remove/unrelease 显式动作)
112
+ ...(all.find((item) => item.id === id)?.released === true ? { released: true } : {}),
113
+ }
114
+ if (args.dryRun === true) {
115
+ return { ok: true, action, templates: normalizeTemplates([...others, entry]) }
116
+ }
117
+ await setTemplates([...others, entry])
118
+ let released = false
119
+ let presetId = ''
120
+ if (args.release === true) {
121
+ released = (await releaseTemplate(entry)) === true
122
+ presetId = 'rs-' + id
123
+ }
124
+ logger?.info?.(`rs-workflow 模板已保存: ${id}${released ? `(已创建为模式 ${presetId})` : ''}`)
125
+ return { ok: true, action, released, presetId, templates: normalizeTemplates([...others, entry]) }
126
+ }
127
+ if (action === 'remove') {
128
+ const id = typeof args.id === 'string' ? args.id.trim() : ''
129
+ if (id === '') return { ok: false, action, error: '缺少 id' }
130
+ const outcome = await removeTemplate(id)
131
+ if (!outcome.ok) return { ok: false, action, error: outcome.error }
132
+ return { ok: true, action, presetId: 'rs-' + id, templates: outcome.templates }
133
+ }
134
+ return { ok: false, action, error: '未知 action: ' + action }
135
+ },
136
+ presentCall: () => ({ card: 'generic', title: '编辑若水工作流模板', kind: 'other', rawInput: {} }),
137
+ })
138
+ }
139
+
140
+ // 模式行激活入口:自有文件存储读写 + 释放分派(注册/目录形态,与 board 同源
141
+ // 服务当值;ctx = agent realm 组合行上下文)
142
+ export function registerTemplateTool(ctx) {
143
+ ctx.inject(['tools'], (tctx) => {
144
+ tctx.effect(() => tctx.tools.register(createTemplateTool({
145
+ getTemplates: () => loadJson('templates.json', []),
146
+ setTemplates: (templates) => saveJson('templates.json', templates),
147
+ removeTemplate: async (id) => {
148
+ const raw = loadJson('templates.json', [])
149
+ const next = raw.filter((t) => t.id !== id)
150
+ if (next.length === raw.length) return { ok: false, error: '模板不存在: ' + id }
151
+ saveJson('templates.json', next)
152
+ const result = await unreleaseTemplateVia(currentAgentPresets(), id)
153
+ return { ok: true, templates: next, outcome: result.outcome }
154
+ },
155
+ releaseTemplate: async (entry) => {
156
+ try {
157
+ const result = await releaseTemplateVia(currentAgentPresets(), entry)
158
+ if (result.outcome === 'failed') {
159
+ ctx.logger?.warn?.(`rs-workflow 模板释放失败: ${result.broken ?? '未知原因'}`)
160
+ return false
161
+ }
162
+ markReleased(entry.id, true)
163
+ return true
164
+ } catch (error) {
165
+ ctx.logger?.warn?.(`rs-workflow 模板释放失败: ${error?.message ?? error}`)
166
+ return false
167
+ }
168
+ },
169
+ logger: ctx.logger,
170
+ })), 'rs-workflow template tool')
171
+ })
172
+ }
package/lib/template.mjs CHANGED
@@ -1,4 +1,4 @@
1
- // 模板静态校验器:DSL v5 唯一权威(board 保存校验唯一入口);v5 新增顶层 autoApprove 键
1
+ // 模板静态校验器:DSL v5 唯一权威(board 保存校验唯一入口);v5 新增顶层 autoApprove 键
2
2
 
3
3
  export const SLOT_KEYS = ['planner', 'executor', 'reviewer', 'executor-loop', 'reviewer-approve', 'executor-escalate']
4
4
  const TOP_FIELDS = new Set(['id', 'label', 'description', 'inputs', 'steps', 'autoApprove'])
@@ -313,12 +313,13 @@ export function validateTemplateSet(list) {
313
313
  }
314
314
  if (outs.size > 0) tmplEdges.set(t.id, outs)
315
315
  }
316
- const depthOf = (id, seen) => {
317
- // 环即拒绝:深度无穷,不限于此路径是否重复经过
318
- if (seen.has(id)) return Infinity
319
- seen.add(id)
316
+ const depthOf = (id, path) => {
317
+ // 仅同路径重访即环(深度无穷);菱形(不同路径汇合同一子流程)是合法 DAG,回溯时摘除路径标记
318
+ if (path.has(id)) return Infinity
319
+ path.add(id)
320
320
  let d = 1
321
- for (const n of tmplEdges.get(id) ?? []) d = Math.max(d, 1 + depthOf(n, seen))
321
+ for (const n of tmplEdges.get(id) ?? []) d = Math.max(d, 1 + depthOf(n, path))
322
+ path.delete(id)
322
323
  return d
323
324
  }
324
325
  for (const id of tmplEdges.keys()) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mzzsfy/dsh-rs-workflow",
3
3
  "description": "若水工作流设置面:流程模板管理与工作位/预算配置(模板编辑/校验/配置读写)。编排运行时按 v5 设计(docs/rsww-v5/)另行实现。",
4
- "version": "1.0.1",
4
+ "version": "1.1.0",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {
@@ -34,6 +34,8 @@
34
34
  "README.md"
35
35
  ],
36
36
  "peerDependencies": {
37
- "@deepseek-ai/schemastery": "^3.18.1"
37
+ "@deepseek-ai/schemastery": "^3.18.1",
38
+ "@deepseek-ai/dsh-llm": ">=0.1.2-rc.1",
39
+ "@deepseek-ai/dsh-tools": ">=0.1.5-rc.2"
38
40
  }
39
41
  }