@mzzsfy/dsh-rs-workflow 1.0.2 → 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 CHANGED
@@ -25,4 +25,4 @@ node --test "test/*.test.mjs" # 包内测试(在 packages/dsh-rs-workflow
25
25
 
26
26
  ## dsh 版本兼容
27
27
 
28
- 三版本全部通过:preset 释放、Agent 预设页若水工作流卡、rs_workflow_ 工具可列出、激活 live。
28
+ 三版本(0.1.2-rc.1 / 0.1.5-rc.3 / 0.1.7-rc.1)全部通过:preset 释放、Agent 预设页若水工作流卡(0.1.7-rc.1 预设页换形「模式」卡,属宿主演进)、rs_workflow_ 工具可列出、配置页六工作位、移除删除联动、激活 live。
package/lib/board.mjs CHANGED
@@ -6,7 +6,17 @@
6
6
  import { reportStore, ACTIVE_STATES } from './store.mjs'
7
7
  import { registry, post, initiatorOf, resumerOf } from './driver/control.mjs'
8
8
  import { validateTemplate, validateTemplateSet } from './template.mjs'
9
- import { releaseFlowTemplate, unreleaseFlowTemplate, releasedTemplateIds } from './release.mjs'
9
+ import { releasedTemplateIds } from './release.mjs'
10
+ import {
11
+ currentAgentPresets,
12
+ disposeAllRegistered,
13
+ markReleased,
14
+ registryFormAvailable,
15
+ replayReleased,
16
+ setAgentPresets,
17
+ unreleaseTemplateVia,
18
+ releaseTemplateVia,
19
+ } from './release-registry.mjs'
10
20
  import { SPEC_TEXT } from './spec.mjs'
11
21
  import { normalizeConfig, BUDGET_KEYS, SLOT_KEYS } from './settings-schema.mjs'
12
22
  import { loadJson, saveJson } from './storage.mjs'
@@ -105,13 +115,25 @@ function writeTemplates(templates) {
105
115
  saveJson('templates.json', templates)
106
116
  }
107
117
 
108
- function removeTemplateById(id) {
118
+ // ── 释放分派(注册形态 rc.1+ / 目录形态 0.1.2-0.1.5,见 计划-rs-workflow-preset-rc1.md)──
119
+ // released 事实源随形态:注册形态 = templates.json released 标志(markReleased 落盘);
120
+ // 目录形态 = 目录本身。服务当值经 release-registry 模块单例,template-tool 行同源消费
121
+ const releaseDispatch = (entry) => releaseTemplateVia(currentAgentPresets(), entry)
122
+
123
+ const unreleaseDispatch = (id) => unreleaseTemplateVia(currentAgentPresets(), id)
124
+
125
+ function releasedIdsVia() {
126
+ if (!registryFormAvailable(currentAgentPresets())) return releasedTemplateIds()
127
+ return rawTemplates().filter((t) => t.released === true).map((t) => t.id)
128
+ }
129
+
130
+ async function removeTemplateById(id) {
109
131
  const raw = rawTemplates()
110
132
  const next = raw.filter((t) => t.id !== id)
111
133
  if (next.length === raw.length) return { ok: false, error: '模板不存在: ' + id }
112
134
  writeTemplates(next)
113
- // 撤下释放物:删除模板即移除对应释放目录(外来目录返回 foreign 不动)
114
- const outcome = unreleaseFlowTemplate(id)
135
+ // 撤下释放物:注册形态注销预设,目录形态移除释放目录(外来目录返回 foreign 不动)
136
+ const outcome = await unreleaseDispatch(id)
115
137
  return { ok: true, outcome }
116
138
  }
117
139
 
@@ -232,6 +254,19 @@ export function handleControl(body) {
232
254
 
233
255
  export function registerBoardRoutes(ctx) {
234
256
  const store = reportStore()
257
+ // 注册形态运行态:服务注入即设当值 + 重放已释放模板;行卸载清当值并注销全部
258
+ // 注册(热重载后随重放重建)。目录形态宿主(无 register)保持当值空缺
259
+ ctx.inject(['agentPresets'], (actx) => {
260
+ const service = actx.agentPresets
261
+ if (!registryFormAvailable(service)) return
262
+ setAgentPresets(service)
263
+ const released = rawTemplates().filter((t) => t.released === true && t.enabled !== false)
264
+ replayReleased(service, released).catch(() => {})
265
+ actx.effect(() => async () => {
266
+ setAgentPresets(undefined)
267
+ await disposeAllRegistered()
268
+ }, 'rs-workflow preset registrations')
269
+ })
235
270
  ctx.inject(['webServer'], (wctx) => {
236
271
  const route = (path, handler, name) => wctx.effect(() => wctx.webServer.register({ kind: 'exact', path, handler }), name)
237
272
  route('/api/rsww/runs', guardedRoute(async (req, res) => {
@@ -300,7 +335,7 @@ export function registerBoardRoutes(ctx) {
300
335
  sendJson(res, 200, { entry, parsed })
301
336
  }), 'rsww template detail route')
302
337
  route('/api/rsww/released', guardedRoute(async (req, res) => {
303
- sendJson(res, 200, { ids: releasedTemplateIds() })
338
+ sendJson(res, 200, { ids: releasedIdsVia() })
304
339
  }), 'rsww released route')
305
340
  route('/api/rsww/spec', guardedRoute(async (req, res) => {
306
341
  sendJson(res, 200, { spec: SPEC_TEXT })
@@ -313,6 +348,7 @@ export function registerBoardRoutes(ctx) {
313
348
  sendJson(res, 200, { ok: true, dryRun: true })
314
349
  return
315
350
  }
351
+ const previous = rawTemplates().find((t) => t.id === id)
316
352
  const templates = rawTemplates().filter((t) => t.id !== id)
317
353
  templates.push({
318
354
  id,
@@ -320,6 +356,8 @@ export function registerBoardRoutes(ctx) {
320
356
  description: typeof body.description === 'string' && body.description.trim() !== '' ? body.description.trim() : parsed.description || '',
321
357
  enabled: body.enabled !== false,
322
358
  json,
359
+ // 已释放模板的编辑性重存不改变释放态(注销走 unrelease 显式动作)
360
+ ...(previous?.released === true ? { released: true } : {}),
323
361
  })
324
362
  writeTemplates(templates)
325
363
  sendJson(res, 200, { ok: true })
@@ -327,7 +365,7 @@ export function registerBoardRoutes(ctx) {
327
365
  route('/api/rsww/template-remove', guardedRoute.post(async (req, res) => {
328
366
  const body = JSON.parse(await readJsonBody(req, res))
329
367
  const id = typeof body.id === 'string' ? body.id.trim() : ''
330
- const outcome = removeTemplateById(id)
368
+ const outcome = await removeTemplateById(id)
331
369
  if (!outcome.ok) throw new Error(outcome.error)
332
370
  sendJson(res, 200, { ok: true })
333
371
  }), 'rsww template-remove route')
@@ -344,14 +382,17 @@ export function registerBoardRoutes(ctx) {
344
382
  }).filter((t) => t !== null)
345
383
  const setErrors = validateTemplateSet(parsedSet).filter((e) => e.target === 'top:id' && e.message.startsWith(id + ' '))
346
384
  if (setErrors.length > 0) throw new Error('模板集合校验失败:' + setErrors.map((e) => e.message).join(';'))
347
- const outcome = releaseFlowTemplate(entry)
348
- sendJson(res, 200, { ok: true, outcome })
385
+ const result = await releaseDispatch(entry)
386
+ if (result.outcome === 'failed') throw new Error(result.broken ?? '释放失败')
387
+ markReleased(id, true)
388
+ sendJson(res, 200, { ok: true, outcome: result.outcome, ...(result.broken === undefined ? {} : { broken: result.broken }) })
349
389
  }), 'rsww release route')
350
390
  route('/api/rsww/unrelease', guardedRoute.post(async (req, res) => {
351
391
  const body = JSON.parse(await readJsonBody(req, res))
352
392
  const id = typeof body.id === 'string' ? body.id.trim() : ''
353
- const outcome = unreleaseFlowTemplate(id)
354
- sendJson(res, 200, { ok: true, outcome })
393
+ const result = await unreleaseDispatch(id)
394
+ markReleased(id, false)
395
+ sendJson(res, 200, { ok: true, outcome: result.outcome })
355
396
  }), 'rsww unrelease route')
356
397
  route('/api/rsww/config', guardedRoute(async (req, res) => {
357
398
  sendJson(res, 200, { config: normalizeConfig(loadJson('config.json', undefined)) })
@@ -464,17 +464,19 @@ export function registerOrchestrator(ctx, config) {
464
464
  // 现行为返回注册 disposer,行销毁即注销;挂靠表(initiator/resumer)以 agentId 为键,行销毁后的
465
465
  // stale 表项由下次 status/start 受理覆盖;run 终态经 finishRun 清 activeRuns。
466
466
  })
467
- registerTurnGuard(ctx, { currentRunId })
467
+ registerTurnGuard(ctx, { currentRunId, createUserMessage: config.createUserMessage })
468
468
  return { rejectCounts, activeRuns }
469
469
  }
470
470
 
471
471
  // 会话守门注册:轮次将停时判定欠动作并 steer 拉回(宿主机器重读 inbox,有 steering 即续跑一步)。
472
- // 消息构造依赖宿主 llm 包,缺失即降级不守门(规约条款 1:编排能力不得被增强功能拖垮)。
473
- function registerTurnGuard(ctx, { currentRunId }) {
472
+ // 消息构造依赖宿主 llm 包(peerDependency,仅宿主运行时可解析):config.createUserMessage
473
+ // 供单元测试注入桩;生产缺省动态 import,解析失败即降级不守门(规约条款 1:编排能力不得被
474
+ // 增强功能拖垮)。
475
+ function registerTurnGuard(ctx, { currentRunId, createUserMessage: injectedCreateUserMessage }) {
474
476
  // 事件面缺失的上下文(旧宿主 / 精简组合)不注册守门——编排本身照常可用
475
477
  if (typeof ctx.on !== 'function') return
476
478
  const nudgeGate = createNudgeGate()
477
- let createUserMessage
479
+ let createUserMessage = injectedCreateUserMessage
478
480
  ctx.on('agent/turn-stopping', async ({ agent }) => {
479
481
  const runId = currentRunId(agent)
480
482
  if (runId === undefined) { return }
@@ -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
+ }