@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/README.md +1 -1
- package/lib/board.mjs +79 -24
- package/lib/driver/approve.mjs +3 -1
- package/lib/driver/index.mjs +47 -15
- package/lib/driver/runner.mjs +11 -5
- package/lib/driver/scheduler.mjs +2 -1
- package/lib/guard.mjs +67 -0
- package/lib/orchestrator.mjs +513 -428
- package/lib/release-registry.mjs +134 -0
- package/lib/release.mjs +190 -183
- package/lib/skeleton.mjs +136 -0
- package/lib/storage.mjs +13 -4
- package/lib/store.mjs +28 -10
- package/lib/template-tool.mjs +172 -161
- package/lib/template.mjs +7 -6
- package/package.json +4 -2
- package/preset/rs-workflow/agent.cordis.yml +5 -64
- package/src/client.js +0 -1
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// release-registry — 释放的注册形态(rc.1+:agentPresets.register 运行时注册,见 .compat/计划-rs-workflow-preset-rc1.md)
|
|
2
|
+
// 与 release.mjs 的目录形态(0.1.2-0.1.5)并立;分派判据 = register 方法在场
|
|
3
|
+
// (0.1.5 及以下的 agentPresets 是目录读取服务,同名但无 register)。
|
|
4
|
+
// 运行态:flowId → 注销 disposer 的模块级单例;board 行是唯一注册方,行卸载时
|
|
5
|
+
// disposeAllRegistered 统一注销(热重载 = 清空后重放,规避宿主 Duplicate 抛错)。
|
|
6
|
+
import { FLOW_ID_RE, presetMeta, releaseFlowTemplate, unreleaseFlowTemplate } from './release.mjs'
|
|
7
|
+
import { validateTemplate } from './template.mjs'
|
|
8
|
+
import { skeletonRows } from './skeleton.mjs'
|
|
9
|
+
import { loadJson, saveJson } from './storage.mjs'
|
|
10
|
+
|
|
11
|
+
const PRESET_ID_PREFIX = 'rs-'
|
|
12
|
+
// 注册预设排在官方 standard(order 1)之后
|
|
13
|
+
const PRESET_ORDER = 100
|
|
14
|
+
|
|
15
|
+
// 目录形态兜底用的家目录透传(测试注入临时目录;生产省略走 env 解析)
|
|
16
|
+
export function registryFormAvailable(agentPresets) {
|
|
17
|
+
return typeof agentPresets?.register === 'function'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// 注册形态服务运行态:board 行注入时设置,行卸载清理时清除;template-tool 等
|
|
21
|
+
// 非 board 上下文的释放动作经 currentAgentPresets 取当值分派
|
|
22
|
+
let currentService
|
|
23
|
+
export function setAgentPresets(service) {
|
|
24
|
+
currentService = service
|
|
25
|
+
}
|
|
26
|
+
export function currentAgentPresets() {
|
|
27
|
+
return currentService
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// released 标志落盘(注册形态的释放事实源 = templates.json;目录形态事实源 =
|
|
31
|
+
// 目录本身,标志仅冗余)。持久化形状与 normalizeTemplates 同约:仅 true 存字段,
|
|
32
|
+
// 撤销即删字段。模板不存在时静默跳过
|
|
33
|
+
export function markReleased(id, released) {
|
|
34
|
+
const raw = loadJson('templates.json', [])
|
|
35
|
+
const entry = raw.find((t) => t.id === id)
|
|
36
|
+
if (entry === undefined) return
|
|
37
|
+
if (released === true) entry.released = true
|
|
38
|
+
else delete entry.released
|
|
39
|
+
saveJson('templates.json', raw)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 流程 id 校验收口(目录/注册两形态共用同一规则)
|
|
43
|
+
function normalizeFlowId(entry) {
|
|
44
|
+
const flowId = typeof entry?.id === 'string' ? entry.id.trim() : ''
|
|
45
|
+
if (!FLOW_ID_RE.test(flowId)) throw new Error(`流程 id 非法: ${JSON.stringify(entry?.id)}(须匹配 ${FLOW_ID_RE.source})`)
|
|
46
|
+
return flowId
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 注册定义:骨架行(rows,已按 flowId 改写 templateId)+ 展示元数据
|
|
50
|
+
// 注册前完成全部校验,失败不触达宿主注册表
|
|
51
|
+
function buildDefinition(flowId, entry, rows) {
|
|
52
|
+
let parsed
|
|
53
|
+
try {
|
|
54
|
+
parsed = JSON.parse(entry.json)
|
|
55
|
+
} catch (error) {
|
|
56
|
+
throw new Error(`模板 JSON 解析失败: ${error.message}`)
|
|
57
|
+
}
|
|
58
|
+
const errors = validateTemplate(parsed)
|
|
59
|
+
if (errors.length > 0) throw new Error('模板校验失败:\n' + errors.map((e) => `${e.target}: ${e.message}`).join('\n'))
|
|
60
|
+
const { name, description } = presetMeta(entry, flowId)
|
|
61
|
+
return { id: PRESET_ID_PREFIX + flowId, name, description, order: PRESET_ORDER, plugins: rows }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const disposers = new Map()
|
|
65
|
+
|
|
66
|
+
async function registerDefinition(agentPresets, definition) {
|
|
67
|
+
const previous = disposers.get(definition.id)
|
|
68
|
+
const replaced = previous !== undefined
|
|
69
|
+
if (replaced) {
|
|
70
|
+
disposers.delete(definition.id)
|
|
71
|
+
await previous()
|
|
72
|
+
}
|
|
73
|
+
const dispose = await agentPresets.register(definition)
|
|
74
|
+
disposers.set(definition.id, dispose)
|
|
75
|
+
// register 对挂载失败不抛(record.broken);回读名单把 broken 透出,防假成功。
|
|
76
|
+
// broken 是诊断不是失败:定义已注册在案,可正常注销
|
|
77
|
+
const roster = await agentPresets.list()
|
|
78
|
+
const row = roster.find((item) => item.id === definition.id)
|
|
79
|
+
return { replaced, broken: row?.broken === undefined ? undefined : String(row.broken) }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 释放:注册形态优先,无 register 的宿主回落目录形态。
|
|
83
|
+
// 返回 {form, outcome, presetId, broken?};outcome ∈ created|updated|removed|missing|foreign|failed
|
|
84
|
+
export async function releaseTemplateVia(agentPresets, entry, rowsOf = (flowId) => skeletonRows(flowId), options = {}) {
|
|
85
|
+
if (!registryFormAvailable(agentPresets)) {
|
|
86
|
+
return { form: 'dir', outcome: releaseFlowTemplate(entry, options.dshHome) }
|
|
87
|
+
}
|
|
88
|
+
const flowId = normalizeFlowId(entry)
|
|
89
|
+
const definition = buildDefinition(flowId, entry, rowsOf(flowId))
|
|
90
|
+
try {
|
|
91
|
+
const { replaced, broken } = await registerDefinition(agentPresets, definition)
|
|
92
|
+
return { form: 'registry', outcome: replaced ? 'updated' : 'created', presetId: definition.id, ...(broken === undefined ? {} : { broken }) }
|
|
93
|
+
} catch (error) {
|
|
94
|
+
return { form: 'registry', outcome: 'failed', presetId: definition.id, broken: error.message }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function unreleaseTemplateVia(agentPresets, flowId, options = {}) {
|
|
99
|
+
if (!registryFormAvailable(agentPresets)) {
|
|
100
|
+
return { form: 'dir', outcome: unreleaseFlowTemplate(flowId, options.dshHome) }
|
|
101
|
+
}
|
|
102
|
+
const id = PRESET_ID_PREFIX + normalizeFlowId({ id: flowId })
|
|
103
|
+
const dispose = disposers.get(id)
|
|
104
|
+
if (dispose === undefined) return { form: 'registry', outcome: 'missing', presetId: id }
|
|
105
|
+
disposers.delete(id)
|
|
106
|
+
await dispose()
|
|
107
|
+
return { form: 'registry', outcome: 'removed', presetId: id }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 启动重放:board 行激活时对已释放模板逐一注册。校验类失败(抛错)收敛为
|
|
111
|
+
// failed 条目,不阻断其余;单条注册服务错误同理
|
|
112
|
+
export async function replayReleased(agentPresets, entries, rowsOf = (flowId) => skeletonRows(flowId)) {
|
|
113
|
+
const results = []
|
|
114
|
+
for (const entry of entries) {
|
|
115
|
+
try {
|
|
116
|
+
results.push(await releaseTemplateVia(agentPresets, entry, rowsOf))
|
|
117
|
+
} catch (error) {
|
|
118
|
+
results.push({ form: 'registry', outcome: 'failed', broken: error.message })
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return results
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 行卸载清理:注销全部注册定义(热重载后由重放重建)
|
|
125
|
+
export async function disposeAllRegistered() {
|
|
126
|
+
const pending = [...disposers.values()]
|
|
127
|
+
disposers.clear()
|
|
128
|
+
for (const dispose of pending) {
|
|
129
|
+
try {
|
|
130
|
+
await dispose()
|
|
131
|
+
} catch { /* 单条注销失败不阻断其余 */ }
|
|
132
|
+
}
|
|
133
|
+
return pending.length
|
|
134
|
+
}
|
package/lib/release.mjs
CHANGED
|
@@ -1,183 +1,190 @@
|
|
|
1
|
-
// release — 流程模板创建/移除/自清理(v5:模板 id 由组合名承载,orchestrator 行 templateId 锚定改写)
|
|
2
|
-
import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
3
|
-
import { homedir } from 'node:os'
|
|
4
|
-
import { dirname, join, resolve } from 'node:path'
|
|
5
|
-
import { fileURLToPath } from 'node:url'
|
|
6
|
-
import { validateTemplate } from './template.mjs'
|
|
7
|
-
|
|
8
|
-
const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
9
|
-
const AGENT_YAML = 'agent.cordis.yml'
|
|
10
|
-
const PRESET_YML = 'preset.yml'
|
|
11
|
-
const FLOW_FILE = 'flow.json'
|
|
12
|
-
const MARKER_NAME = '.dsh-rs-workflow-source.json'
|
|
13
|
-
const PACKAGE_NAME = '@mzzsfy/dsh-rs-workflow'
|
|
14
|
-
const MARKER_KIND_FLOW = 'flow'
|
|
15
|
-
const PRESET_SKELETON = join(PKG_ROOT, 'preset', 'rs-workflow', AGENT_YAML)
|
|
16
|
-
const USER_PRESET_DIR = '.agent-presets'
|
|
17
|
-
const FLOW_PRESET_PREFIX = 'rs-'
|
|
18
|
-
const STAGING_PREFIX = '.rs-workflow-staging-'
|
|
19
|
-
const OLD_PREFIX = '.rs-workflow-old-'
|
|
20
|
-
const ORPHAN_PREFIX = '.rs-workflow-orphan-'
|
|
21
|
-
// 骨架唯一源中的 orchestrator 行 templateId 锚定;创建产物一律改写为组合承载的模板 id(恰一处)
|
|
22
|
-
const TEMPLATE_ANCHOR_MAIN = 'TPL_ANCHOR'
|
|
23
|
-
const FLOW_ID_RE = /^[a-z][a-z0-9-]*$/
|
|
24
|
-
const DESC_MAX_CHARS = 200
|
|
25
|
-
const DESC_FALLBACK = '流程工作流模板'
|
|
26
|
-
const PRESET_NAME_PREFIX = '若水·'
|
|
27
|
-
let PKG_VERSION = 'unknown'
|
|
28
|
-
try {
|
|
29
|
-
const parsed = JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf8')).version
|
|
30
|
-
if (typeof parsed === 'string' && parsed.length > 0) PKG_VERSION = parsed
|
|
31
|
-
} catch { /* 读包失败归 unknown */ }
|
|
32
|
-
|
|
33
|
-
const nonEmpty = (v) => (typeof v === 'string' && v.trim().length > 0 ? v.trim() : null)
|
|
34
|
-
|
|
35
|
-
// 预设根解析:参数 > env 根 > env home > 默认家目录,统一拼 .agent-presets
|
|
36
|
-
function resolveHome(explicit) {
|
|
37
|
-
const home = nonEmpty(explicit) ?? nonEmpty(process.env.DSH_RS_WORKFLOW_PRESET_ROOT) ?? nonEmpty(process.env.DSH_HOME) ?? join(homedir(), '.dsh')
|
|
38
|
-
return resolve(home)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function presetRoot(dshHome) {
|
|
42
|
-
return join(resolveHome(dshHome), USER_PRESET_DIR)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function flowPresetDest(flowId, dshHome) {
|
|
46
|
-
return join(presetRoot(dshHome), FLOW_PRESET_PREFIX + flowId)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// 读 marker;缺失/损坏同义返回 null(无法证明归属)
|
|
50
|
-
function readMarker(dir) {
|
|
51
|
-
try {
|
|
52
|
-
return JSON.parse(readFileSync(join(dir, MARKER_NAME), 'utf8'))
|
|
53
|
-
} catch {
|
|
54
|
-
return null
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const ownedByUs = (marker) => marker !== null && marker.package === PACKAGE_NAME
|
|
59
|
-
|
|
60
|
-
const markerJson = () => JSON.stringify({ package: PACKAGE_NAME, kind: MARKER_KIND_FLOW, version: PKG_VERSION }, null, 2) + '\n'
|
|
61
|
-
|
|
62
|
-
// 创建骨架:包内唯一源逐字保留,仅改写 orchestrator 行 templateId 锚定(必须恰一处)
|
|
63
|
-
function releaseAgentYaml(flowId) {
|
|
64
|
-
const parts = readFileSync(PRESET_SKELETON, 'utf8').split(TEMPLATE_ANCHOR_MAIN)
|
|
65
|
-
if (parts.length !== 2) throw new Error(`创建骨架 templateId 锚定串异常(期望恰一处): ${PRESET_SKELETON}`)
|
|
66
|
-
return parts.join(flowId)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// preset.yml 由骨架元数据 + 模板条目生成(组合名已含模板语义,描述取模板条目)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const name = `${PRESET_NAME_PREFIX}${entry.label || flowId}
|
|
74
|
-
return
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
1
|
+
// release — 流程模板创建/移除/自清理(v5:模板 id 由组合名承载,orchestrator 行 templateId 锚定改写)
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { homedir } from 'node:os'
|
|
4
|
+
import { dirname, join, resolve } from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
import { validateTemplate } from './template.mjs'
|
|
7
|
+
|
|
8
|
+
const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
9
|
+
const AGENT_YAML = 'agent.cordis.yml'
|
|
10
|
+
const PRESET_YML = 'preset.yml'
|
|
11
|
+
const FLOW_FILE = 'flow.json'
|
|
12
|
+
const MARKER_NAME = '.dsh-rs-workflow-source.json'
|
|
13
|
+
const PACKAGE_NAME = '@mzzsfy/dsh-rs-workflow'
|
|
14
|
+
const MARKER_KIND_FLOW = 'flow'
|
|
15
|
+
const PRESET_SKELETON = join(PKG_ROOT, 'preset', 'rs-workflow', AGENT_YAML)
|
|
16
|
+
const USER_PRESET_DIR = '.agent-presets'
|
|
17
|
+
const FLOW_PRESET_PREFIX = 'rs-'
|
|
18
|
+
const STAGING_PREFIX = '.rs-workflow-staging-'
|
|
19
|
+
const OLD_PREFIX = '.rs-workflow-old-'
|
|
20
|
+
const ORPHAN_PREFIX = '.rs-workflow-orphan-'
|
|
21
|
+
// 骨架唯一源中的 orchestrator 行 templateId 锚定;创建产物一律改写为组合承载的模板 id(恰一处)
|
|
22
|
+
const TEMPLATE_ANCHOR_MAIN = 'TPL_ANCHOR'
|
|
23
|
+
export const FLOW_ID_RE = /^[a-z][a-z0-9-]*$/
|
|
24
|
+
const DESC_MAX_CHARS = 200
|
|
25
|
+
const DESC_FALLBACK = '流程工作流模板'
|
|
26
|
+
const PRESET_NAME_PREFIX = '若水·'
|
|
27
|
+
let PKG_VERSION = 'unknown'
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf8')).version
|
|
30
|
+
if (typeof parsed === 'string' && parsed.length > 0) PKG_VERSION = parsed
|
|
31
|
+
} catch { /* 读包失败归 unknown */ }
|
|
32
|
+
|
|
33
|
+
const nonEmpty = (v) => (typeof v === 'string' && v.trim().length > 0 ? v.trim() : null)
|
|
34
|
+
|
|
35
|
+
// 预设根解析:参数 > env 根 > env home > 默认家目录,统一拼 .agent-presets
|
|
36
|
+
function resolveHome(explicit) {
|
|
37
|
+
const home = nonEmpty(explicit) ?? nonEmpty(process.env.DSH_RS_WORKFLOW_PRESET_ROOT) ?? nonEmpty(process.env.DSH_HOME) ?? join(homedir(), '.dsh')
|
|
38
|
+
return resolve(home)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function presetRoot(dshHome) {
|
|
42
|
+
return join(resolveHome(dshHome), USER_PRESET_DIR)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function flowPresetDest(flowId, dshHome) {
|
|
46
|
+
return join(presetRoot(dshHome), FLOW_PRESET_PREFIX + flowId)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 读 marker;缺失/损坏同义返回 null(无法证明归属)
|
|
50
|
+
function readMarker(dir) {
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(readFileSync(join(dir, MARKER_NAME), 'utf8'))
|
|
53
|
+
} catch {
|
|
54
|
+
return null
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const ownedByUs = (marker) => marker !== null && marker.package === PACKAGE_NAME
|
|
59
|
+
|
|
60
|
+
const markerJson = () => JSON.stringify({ package: PACKAGE_NAME, kind: MARKER_KIND_FLOW, version: PKG_VERSION }, null, 2) + '\n'
|
|
61
|
+
|
|
62
|
+
// 创建骨架:包内唯一源逐字保留,仅改写 orchestrator 行 templateId 锚定(必须恰一处)
|
|
63
|
+
function releaseAgentYaml(flowId) {
|
|
64
|
+
const parts = readFileSync(PRESET_SKELETON, 'utf8').split(TEMPLATE_ANCHOR_MAIN)
|
|
65
|
+
if (parts.length !== 2) throw new Error(`创建骨架 templateId 锚定串异常(期望恰一处): ${PRESET_SKELETON}`)
|
|
66
|
+
return parts.join(flowId)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// preset.yml 由骨架元数据 + 模板条目生成(组合名已含模板语义,描述取模板条目)
|
|
70
|
+
// presetMeta:展示元数据的唯一权威(原始值,不转义);序列化方自行按载体转义
|
|
71
|
+
export function presetMeta(entry, flowId) {
|
|
72
|
+
const desc = String(entry.description ?? '').replace(/\s*\n\s*/g, ' ').slice(0, DESC_MAX_CHARS) || DESC_FALLBACK
|
|
73
|
+
const name = `${PRESET_NAME_PREFIX}${entry.label || flowId}`
|
|
74
|
+
return { name, description: desc }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function presetYaml(entry, flowId) {
|
|
78
|
+
const { name, description: desc } = presetMeta(entry, flowId)
|
|
79
|
+
// name 用双引号纯量防 YAML 结构字符(: 与换行)破坏生成物
|
|
80
|
+
const escaped = name.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\s*\n\s*/g, ' ')
|
|
81
|
+
return `name: "${escaped}"\ndescription: >-\n ${desc}\n`
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 硬崩溃残留的 staging/备份目录清理(前缀本包独占,直接删安全;orphan 前缀永不自动删)
|
|
85
|
+
function cleanStaleStaging(parentDir) {
|
|
86
|
+
let entries
|
|
87
|
+
try {
|
|
88
|
+
entries = readdirSync(parentDir)
|
|
89
|
+
} catch {
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
for (const name of entries) {
|
|
93
|
+
if (name.startsWith(STAGING_PREFIX) || name.startsWith(OLD_PREFIX)) {
|
|
94
|
+
rmSync(join(parentDir, name), { recursive: true, force: true })
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 原子换入:staging 组装 → 旧目录 rename 备份 → 新目录入位 → 删备份;
|
|
100
|
+
// 入位失败还原,还原也失败改 orphan 前缀保留待人工处置,绝不静默销毁
|
|
101
|
+
function atomicReplace(dest, existed, build) {
|
|
102
|
+
const parent = dirname(dest)
|
|
103
|
+
mkdirSync(parent, { recursive: true })
|
|
104
|
+
const stagingRoot = mkdtempSync(join(parent, STAGING_PREFIX))
|
|
105
|
+
const out = join(stagingRoot, 'out')
|
|
106
|
+
const backup = join(parent, OLD_PREFIX + Date.now())
|
|
107
|
+
let orphan = null
|
|
108
|
+
try {
|
|
109
|
+
mkdirSync(out, { recursive: true })
|
|
110
|
+
build(out)
|
|
111
|
+
const hasDest = existsSync(dest)
|
|
112
|
+
if (hasDest) renameSync(dest, backup)
|
|
113
|
+
try {
|
|
114
|
+
renameSync(out, dest)
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (hasDest) {
|
|
117
|
+
try {
|
|
118
|
+
renameSync(backup, dest)
|
|
119
|
+
} catch {
|
|
120
|
+
orphan = join(parent, ORPHAN_PREFIX + Date.now())
|
|
121
|
+
try {
|
|
122
|
+
renameSync(backup, orphan)
|
|
123
|
+
} catch {
|
|
124
|
+
orphan = backup
|
|
125
|
+
}
|
|
126
|
+
console.warn(`[rs-workflow] 创建换入失败且还原失败,旧目录保留在 ${orphan}(不会被自动清理,需人工处置)`)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
throw error
|
|
130
|
+
}
|
|
131
|
+
} finally {
|
|
132
|
+
rmSync(stagingRoot, { recursive: true, force: true })
|
|
133
|
+
if (orphan === null) rmSync(backup, { recursive: true, force: true })
|
|
134
|
+
}
|
|
135
|
+
return existed ? 'updated' : 'created'
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function releaseFlowTemplate(entry, dshHome) {
|
|
139
|
+
const flowId = typeof entry?.id === 'string' ? entry.id.trim() : ''
|
|
140
|
+
if (!FLOW_ID_RE.test(flowId)) throw new Error(`流程 id 非法: ${JSON.stringify(entry?.id)}(须匹配 ${FLOW_ID_RE.source})`)
|
|
141
|
+
// 创建前重校验(设置里可能被外部改坏);v5 模板文本为严格 JSON
|
|
142
|
+
let parsed
|
|
143
|
+
try {
|
|
144
|
+
parsed = JSON.parse(entry.json)
|
|
145
|
+
} catch (error) {
|
|
146
|
+
throw new Error(`模板 JSON 解析失败: ${error.message}`)
|
|
147
|
+
}
|
|
148
|
+
const errors = validateTemplate(parsed)
|
|
149
|
+
if (errors.length > 0) throw new Error('模板校验失败:\n' + errors.map((e) => `${e.target}: ${e.message}`).join('\n'))
|
|
150
|
+
const dest = flowPresetDest(flowId, dshHome)
|
|
151
|
+
cleanStaleStaging(dirname(dest))
|
|
152
|
+
const existed = existsSync(dest)
|
|
153
|
+
if (existed && !ownedByUs(readMarker(dest))) throw new Error(`目标目录归属他人,拒绝覆盖: ${dest}`)
|
|
154
|
+
return atomicReplace(dest, existed, (out) => {
|
|
155
|
+
writeFileSync(join(out, FLOW_FILE), entry.json, 'utf8')
|
|
156
|
+
writeFileSync(join(out, PRESET_YML), presetYaml(entry, flowId), 'utf8')
|
|
157
|
+
writeFileSync(join(out, AGENT_YAML), releaseAgentYaml(flowId), 'utf8')
|
|
158
|
+
writeFileSync(join(out, MARKER_NAME), markerJson(), 'utf8')
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function unreleaseFlowTemplate(flowId, dshHome) {
|
|
163
|
+
const dest = flowPresetDest(String(flowId ?? '').trim(), dshHome)
|
|
164
|
+
if (!existsSync(dest)) return 'missing'
|
|
165
|
+
if (!ownedByUs(readMarker(dest))) return 'foreign'
|
|
166
|
+
rmSync(dest, { recursive: true, force: true })
|
|
167
|
+
cleanStaleStaging(dirname(dest))
|
|
168
|
+
return 'removed'
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function isReleaseDir(root, name) {
|
|
172
|
+
return name.startsWith(FLOW_PRESET_PREFIX) && statSync(join(root, name), { throwIfNoEntry: false })?.isDirectory() === true
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function releasedTemplateIds(dshHome) {
|
|
176
|
+
const root = presetRoot(dshHome)
|
|
177
|
+
let entries
|
|
178
|
+
try {
|
|
179
|
+
entries = readdirSync(root)
|
|
180
|
+
} catch {
|
|
181
|
+
return []
|
|
182
|
+
}
|
|
183
|
+
const ids = []
|
|
184
|
+
for (const name of entries) {
|
|
185
|
+
if (!isReleaseDir(root, name)) continue
|
|
186
|
+
const marker = readMarker(join(root, name))
|
|
187
|
+
if (ownedByUs(marker) && marker.kind === MARKER_KIND_FLOW) ids.push(name.slice(FLOW_PRESET_PREFIX.length))
|
|
188
|
+
}
|
|
189
|
+
return ids
|
|
190
|
+
}
|
package/lib/skeleton.mjs
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// skeleton — rs-workflow 预设组合骨架单一源(JS 行形态)+ 规范化 yml 序列化器
|
|
2
|
+
// 消费方:注册形态(buildDefinition 的 plugins)直接用 skeletonRows;目录形态
|
|
3
|
+
// 仍读 preset/rs-workflow/agent.cordis.yml——该文件是 emitRows 的规范化产物,
|
|
4
|
+
// 由 test/skeleton.test.mjs 对拍防漂移,两形态共享同一骨架事实源。
|
|
5
|
+
// 基底是 standard 全功能编码代理行集,差异:persona 换流程模式纪律 v5;
|
|
6
|
+
// delegation 组含 rs-workflow-orchestrator/template-tool 行;无 report 行
|
|
7
|
+
// (trace 事件 + run-store 取代);plan-mode 组移除(规划在编排内)。
|
|
8
|
+
// 行形状闭集:id/name/disabled{__jsExpr}/group/isolate/config(对象或子行数组)。
|
|
9
|
+
// `!!js` 表达式在 JS 形态统一为 {__jsExpr: '表达式'},与 loader 解析语义一致。
|
|
10
|
+
export const TEMPLATE_ANCHOR = 'TPL_ANCHOR'
|
|
11
|
+
|
|
12
|
+
const JS_EXPR = (expr) => ({ __jsExpr: expr })
|
|
13
|
+
|
|
14
|
+
const ORCHESTRATOR_CONFIG = (templateId) => ({
|
|
15
|
+
role: 'orchestrator',
|
|
16
|
+
templateId,
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
export function skeletonRows(templateId) {
|
|
20
|
+
if (typeof templateId !== 'string' || templateId.trim() === '') {
|
|
21
|
+
throw new Error(`骨架 templateId 非法: ${JSON.stringify(templateId)}`)
|
|
22
|
+
}
|
|
23
|
+
return [
|
|
24
|
+
{ id: 'persona', name: '@deepseek-ai/dsh-persona', config: { prefix: PERSONA_PREFIX, suffix: 'Your working directory is {{cwd}}.' } },
|
|
25
|
+
{ id: 'agent-instructions', name: '@deepseek-ai/dsh-agent-instructions', config: { maxBytes: 65536 } },
|
|
26
|
+
// 不可禁本组合 shell/fs 工具行:workflow worker 的段子代理继承组合行集,禁行使
|
|
27
|
+
// 段子代理无法落盘交付物(实测证实)。主循环不写文件由 persona 判据约束
|
|
28
|
+
{ id: 'tool-bash', name: '@deepseek-ai/dsh-tool-bash', disabled: JS_EXPR('process.platform === \'win32\'') },
|
|
29
|
+
{ id: 'tool-pwsh', name: '@deepseek-ai/dsh-tool-pwsh', disabled: JS_EXPR('process.platform !== \'win32\'') },
|
|
30
|
+
{ id: 'tool-fs', name: '@deepseek-ai/dsh-tool-fs' },
|
|
31
|
+
{ id: 'tool-fs-search', name: '@deepseek-ai/dsh-tool-fs-search', config: { sampleOverCapGlobResults: false } },
|
|
32
|
+
// 缺省连续唤醒预算 3 次且仅用户消息重置:纯自主编排每段 settle 耗 1 次,放宽到
|
|
33
|
+
// 64 段覆盖 spec 建议的 3~12 步多实例流程上限
|
|
34
|
+
{ id: 'tool-jobs', name: '@deepseek-ai/dsh-tool-jobs', config: { maxConsecutiveWakes: 64 } },
|
|
35
|
+
{ id: 'skill-filesystem', name: '@deepseek-ai/dsh-skill-filesystem' },
|
|
36
|
+
{ id: 'tool-skill', name: '@deepseek-ai/dsh-tool-skill' },
|
|
37
|
+
{ id: 'tool-web', name: '@deepseek-ai/dsh-tool-web', config: { fetch: false, searchTimeoutMs: 60000 } },
|
|
38
|
+
// 编排子代理上下文较长,保留压缩组
|
|
39
|
+
{
|
|
40
|
+
id: 'compaction', name: 'cordis:group', group: true,
|
|
41
|
+
isolate: { compaction: true, toolResultPruner: true },
|
|
42
|
+
config: [
|
|
43
|
+
{ id: 'compaction-basic', name: '@deepseek-ai/dsh-compaction-basic' },
|
|
44
|
+
{ id: 'command-compact', name: '@deepseek-ai/dsh-command-compact' },
|
|
45
|
+
{ id: 'tool-result-pruner', name: '@deepseek-ai/dsh-compaction-tool-result-pruner', config: { thresholdChars: 8192, headChars: 4096, tailChars: 1024 } },
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
// workflowEngine 是预设私有服务(preset 组合发布全局服务会被宿主拒绝 mount),
|
|
49
|
+
// 所有触达它的行共享一个 entry-local isolate realm;orchestrator 行模板 id 由
|
|
50
|
+
// 组合名承载(rs-<id> → <id>),目录形态经 TPL_ANCHOR 锚定改写
|
|
51
|
+
{
|
|
52
|
+
id: 'delegation', name: 'cordis:group', group: true,
|
|
53
|
+
isolate: { workflowEngine: true },
|
|
54
|
+
config: [
|
|
55
|
+
{ id: 'tool-subagent-control', name: '@deepseek-ai/dsh-tool-subagent-control' },
|
|
56
|
+
{ id: 'tool-subagent-list-agents', name: '@deepseek-ai/dsh-tool-subagent-control/list-agents' },
|
|
57
|
+
{ id: 'tool-subagent', name: '@deepseek-ai/dsh-tool-subagent', config: { provider: 'spawn', toolName: 'subagent', backgroundMode: 'continuable' } },
|
|
58
|
+
{ id: 'tool-subagent-fork', name: '@deepseek-ai/dsh-tool-subagent', config: { provider: 'fork', toolName: 'subagent_fork', backgroundMode: 'continuable' } },
|
|
59
|
+
{ id: 'workflow-ptc', name: '@deepseek-ai/dsh-workflow-ptc', config: { provider: 'spawn' } },
|
|
60
|
+
{ id: 'tool-workflow', name: '@deepseek-ai/dsh-tool-workflow' },
|
|
61
|
+
{ id: 'tool-ralph', name: '@deepseek-ai/dsh-tool-ralph', config: { subagentProvider: 'spawn', maxRounds: 64 } },
|
|
62
|
+
{ id: 'rs-workflow-orchestrator', name: '@mzzsfy/dsh-rs-workflow', config: ORCHESTRATOR_CONFIG(templateId) },
|
|
63
|
+
{ id: 'rs-workflow-template-tool', name: '@mzzsfy/dsh-rs-workflow', config: { role: 'template-tool' } },
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
{ id: 'tool-ask-user', name: '@deepseek-ai/dsh-tool-ask-user' },
|
|
67
|
+
{ id: 'tool-todo', name: '@deepseek-ai/dsh-tool-todo', config: { allowParallelInProgress: true } },
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const PERSONA_PREFIX = `你是"若水工作流"会话代理,由 {{model}} 模型驱动。你负责人机协作与编排调度;
|
|
72
|
+
任务交付由流程引擎接管下的子代理完成,你不直接产出交付物。
|
|
73
|
+
职责判据:
|
|
74
|
+
1. 纯会话内请求(问答、解释、讨论,不产生文件)→ 直接答复,不起编排;
|
|
75
|
+
2. 凡需新建或修改交付物文件(代码、页面、脚本、文档、配置,无论大小与步骤数,例如"写个贪吃蛇html")
|
|
76
|
+
→ 必须调用 rs_workflow_start,提交结构化规划(steps 各步要点与验收口径);禁止以"一句话能答、
|
|
77
|
+
改动小"为由绕开编排自行产出文件;此通道无豁免:规划被拒按 errors 修正重交,不许改道自行完成;
|
|
78
|
+
3. 编排运行中你只被段结束通知或用户消息唤醒:任一唤醒轮先 rs_workflow_status 检查现役 run——
|
|
79
|
+
status=running 且无活跃段 job(待拉起)或 status=paused 且 awaitingResume=true(页签已发恢复)
|
|
80
|
+
时,即 rs_workflow_resume 补拉;waiting_approval 一律不补拉,轮到裁决:按模板 autoApprove 决定
|
|
81
|
+
代审或 ask_user 转呈真人,裁决经 rs_workflow_verdict 回写(autoApprove 代审 by 缺省,转呈真人
|
|
82
|
+
by=user,意见必填),裁决受理后 rs_workflow_resume 拉起下一段;
|
|
83
|
+
终态则汇报(完成经 deliverables 交付,汇报必须引用 runId 供页签核验;失败/阻塞说明卡点与建议);
|
|
84
|
+
任何交付说明若无对应 run 记录即为违规——禁止把自行产出的文件当交付物汇报;
|
|
85
|
+
用户中途纠偏经 rs_workflow_message 注入,下一步骤边界进入编排;
|
|
86
|
+
4. 你不代替子代理执行编排内步骤,不伪造产出,不跳过引擎直接交付;
|
|
87
|
+
5. 引擎不可用或编排启动失败时,不得自行完成文件类请求——向用户说明故障与已尝试的调用,
|
|
88
|
+
等待用户处置;文件交付只有 rs_workflow_ 通道,没有自行兜底。`
|
|
89
|
+
|
|
90
|
+
const ROW_KEYS = ['id', 'name', 'disabled', 'group', 'isolate', 'config']
|
|
91
|
+
// 裸标量白名单:内部冒号(: 后非空格,如 cordis:group)合法且为宿主 preset 惯例;
|
|
92
|
+
// '@' 起(保留字符)与含 ': '/尾冒号形态拒绝,BARE_RE 字符类不含 @ 且前置检查排除
|
|
93
|
+
const BARE_RE = /^[A-Za-z0-9_][A-Za-z0-9_: ./+(){}|=<>~^-]*$/
|
|
94
|
+
|
|
95
|
+
// 标量:数字/布尔直书;字符串裸安全(无 ': '、不以 ':' 收尾、字符类受控)则裸,
|
|
96
|
+
// 否则单引号(内部 ' 翻倍)
|
|
97
|
+
function scalarOf(value) {
|
|
98
|
+
if (typeof value === 'number' || typeof value === 'boolean') return String(value)
|
|
99
|
+
const text = String(value)
|
|
100
|
+
if (!text.includes(': ') && !text.endsWith(':') && BARE_RE.test(text)) return text
|
|
101
|
+
return `'${text.replace(/'/g, '\'\'')}'`
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 键值行:!!js 表达式 / 块标量(多行串)|- / 映射 / 子行序列 / 标量,五形态收口
|
|
105
|
+
function entryLines(key, value, indent) {
|
|
106
|
+
const pad = ' '.repeat(indent)
|
|
107
|
+
if (value !== null && typeof value === 'object' && value.__jsExpr !== undefined) {
|
|
108
|
+
return [`${pad}${key}: !!js ${value.__jsExpr}`]
|
|
109
|
+
}
|
|
110
|
+
if (typeof value === 'string' && value.includes('\n')) {
|
|
111
|
+
return [`${pad}${key}: |-`].concat(value.split('\n').map((line) => ' '.repeat(indent + 2) + line))
|
|
112
|
+
}
|
|
113
|
+
if (value !== null && typeof value === 'object') {
|
|
114
|
+
if (Array.isArray(value)) {
|
|
115
|
+
return [`${pad}${key}:`].concat(value.flatMap((row) => renderRow(row, indent + 2)))
|
|
116
|
+
}
|
|
117
|
+
return [`${pad}${key}:`].concat(
|
|
118
|
+
Object.entries(value).flatMap(([childKey, childValue]) => entryLines(childKey, childValue, indent + 2)),
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
return [`${pad}${key}: ${scalarOf(value)}`]
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function renderRow(row, indent) {
|
|
125
|
+
const pad = ' '.repeat(indent)
|
|
126
|
+
const rest = ROW_KEYS.slice(1)
|
|
127
|
+
.filter((key) => row[key] !== undefined)
|
|
128
|
+
.flatMap((key) => entryLines(key, row[key], indent + 2))
|
|
129
|
+
return [`${pad}- id: ${scalarOf(row.id)}`].concat(rest)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 规范化 yml 序列化:行集 → 确定性文本(尾随换行);与 preset/rs-workflow/
|
|
133
|
+
// agent.cordis.yml 正文逐字节对拍(test/skeleton.test.mjs)
|
|
134
|
+
export function emitRows(rows) {
|
|
135
|
+
return rows.flatMap((row) => renderRow(row, 0)).join('\n') + '\n'
|
|
136
|
+
}
|