@mzzsfy/dsh-rs-workflow 0.2.4 → 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/lib/store.mjs ADDED
@@ -0,0 +1,255 @@
1
+ // run-store(v5):每 run 全量 JSON + index.json 索引;LRU 容量收敛;零截断
2
+ // namespace 子目录 v5/(旧数据物理隔离);record 增 plan/warnings/controls[].by(见 data-design.md)
3
+ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs'
4
+ import { homedir, tmpdir } from 'node:os'
5
+ import { join } from 'node:path'
6
+
7
+ export const KEEP_RUNS = 200
8
+ export const ACTIVE_STATES = new Set(['running', 'paused', 'waiting_approval'])
9
+ const RUN_ID_RE = /^[a-z0-9-]+$/
10
+ const REQUEST_PREVIEW = 120
11
+ const CONTROL_EVENTS = new Set(['control'])
12
+
13
+ let seq = 0
14
+ const genRunId = () => `r-${Date.now().toString(36)}-${++seq}`
15
+
16
+ // v5 namespace:dataDir(dsh-rs-workflow 根)下的 v5/ 子目录;DSH_RS_WORKFLOW_DATA_DIR 覆写根
17
+ export function defaultDataDir() {
18
+ if (process.env.DSH_RS_WORKFLOW_DATA_DIR) return join(process.env.DSH_RS_WORKFLOW_DATA_DIR, 'v5')
19
+ return join(process.env.DSH_HOME || join(homedir(), '.dsh'), 'dsh-rs-workflow', 'v5')
20
+ }
21
+
22
+ const emptyState = () => ({ status: 'running', steps: {}, approvals: {}, escalations: 0, queued: [], batchSeq: 0, slotCursor: {}, controlSeq: 0, redoInfo: {} })
23
+
24
+ export function createStore({ dir = defaultDataDir(), logger = console, keepRuns = KEEP_RUNS } = {}) {
25
+ const runsDir = join(dir, 'runs')
26
+ const indexPath = join(runsDir, 'index.json')
27
+ const records = new Map()
28
+ const index = []
29
+ let loaded = false
30
+
31
+ const warn = (message) => logger.warn?.(`[rsww-store] ${message}`)
32
+
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)
37
+ }
38
+
39
+ const persistRun = (record) => writeAtomic(join(runsDir, `${record.runId}.json`), JSON.stringify(record, null, 2))
40
+ const persistIndex = () => writeAtomic(indexPath, JSON.stringify({ runs: index }, null, 2))
41
+
42
+ const indexEntryOf = (record) => ({
43
+ runId: record.runId, sessionId: record.sessionId, workspace: record.workspace,
44
+ templateId: record.templateId, status: record.status, createdAt: record.createdAt,
45
+ finishedAt: record.finishedAt, summary: record.summary,
46
+ request: typeof record.request === 'string' ? record.request.slice(0, REQUEST_PREVIEW) : record.request,
47
+ })
48
+
49
+ const loadFileRecord = (path, collectIndex) => {
50
+ const record = JSON.parse(readFileSync(path, 'utf8'))
51
+ records.set(record.runId, record)
52
+ if (collectIndex) index.push(indexEntryOf(record))
53
+ if (ACTIVE_STATES.has(record.status)) {
54
+ const at = Date.now()
55
+ record.status = 'cancelled'
56
+ record.finishedAt = at
57
+ if (!record.summary) record.summary = '进程重启,运行中断,可在会话页签断点续跑'
58
+ record.controls = [...(record.controls ?? []), { at, kind: 'cancel', text: '进程重启,自动收敛' }]
59
+ persistRun(record)
60
+ const entry = index.find((e) => e.runId === record.runId)
61
+ if (entry) Object.assign(entry, indexEntryOf(record))
62
+ return true
63
+ }
64
+ return false
65
+ }
66
+
67
+ const removeInternal = (runId) => {
68
+ records.delete(runId)
69
+ const at = index.findIndex((e) => e.runId === runId)
70
+ if (at >= 0) index.splice(at, 1)
71
+ rmSync(join(runsDir, `${runId}.json`), { force: true })
72
+ }
73
+
74
+ const ensureLoaded = () => {
75
+ if (loaded) return
76
+ loaded = true
77
+ mkdirSync(runsDir, { recursive: true })
78
+ let indexValid = false
79
+ if (existsSync(indexPath)) {
80
+ try {
81
+ const parsed = JSON.parse(readFileSync(indexPath, 'utf8'))
82
+ if (Array.isArray(parsed.runs)) {
83
+ for (const entry of parsed.runs) index.push(entry)
84
+ indexValid = true
85
+ }
86
+ } catch {
87
+ index.length = 0
88
+ }
89
+ }
90
+ const files = []
91
+ try {
92
+ for (const name of readdirSync(runsDir)) if (name.endsWith('.json') && name !== 'index.json') files.push(name)
93
+ } catch (e) {
94
+ warn(`runs 目录扫描失败:${e.message}`)
95
+ }
96
+ let settledCount = 0
97
+ for (const name of files) {
98
+ try {
99
+ if (loadFileRecord(join(runsDir, name), !indexValid)) settledCount++
100
+ } catch (e) {
101
+ warn(`运行记录 ${name} 读取失败,跳过:${e.message}`)
102
+ }
103
+ }
104
+ // 对账:runs/ 文件集合为权威修齐 index(缺行补、幽灵行删、收敛改写行落盘),仅内存修正
105
+ let repaired = settledCount > 0
106
+ const fileIds = new Set(records.keys())
107
+ for (const record of records.values()) {
108
+ if (!index.some((e) => e.runId === record.runId)) {
109
+ index.push(indexEntryOf(record))
110
+ repaired = true
111
+ }
112
+ }
113
+ for (let i = index.length - 1; i >= 0; i--) {
114
+ if (!fileIds.has(index[i].runId)) {
115
+ index.splice(i, 1)
116
+ repaired = true
117
+ }
118
+ }
119
+ if (!indexValid || repaired) {
120
+ index.sort((a, b) => (b.createdAt ?? 0) - (a.createdAt ?? 0))
121
+ persistIndex()
122
+ }
123
+ }
124
+
125
+ const assertRunId = (runId) => {
126
+ if (typeof runId !== 'string' || !RUN_ID_RE.test(runId)) throw new Error(`runId 非法:${runId}`)
127
+ }
128
+
129
+ const evictOverCapacity = () => {
130
+ // index 恒新在前;同毫秒 finishedAt 并列时稳定排序保持原序,先反转使并列按旧在前
131
+ const finished = index.filter((e) => !ACTIVE_STATES.has(e.status)).reverse().sort((a, b) => (a.finishedAt ?? a.createdAt ?? 0) - (b.finishedAt ?? b.createdAt ?? 0))
132
+ let excess = finished.length - keepRuns
133
+ let evicted = false
134
+ for (const entry of finished) {
135
+ if (excess <= 0) break
136
+ excess--
137
+ removeInternal(entry.runId)
138
+ evicted = true
139
+ }
140
+ if (evicted) persistIndex()
141
+ }
142
+
143
+ const store = {
144
+ start({ runId, sessionId, workspace, request, templateId, inputs, plan, warnings, state }) {
145
+ ensureLoaded()
146
+ const id = runId === undefined || runId === null || runId === '' ? genRunId() : runId
147
+ assertRunId(id)
148
+ if (records.has(id)) throw new Error(`runId 已存在:${id}`)
149
+ const at = Date.now()
150
+ const record = {
151
+ runId: id, templateId: templateId ?? '', sessionId: sessionId ?? '', workspace: workspace ?? '',
152
+ request: request ?? '', inputs: inputs ?? {}, status: 'running', createdAt: at,
153
+ summary: '', plan: plan ?? null, warnings: warnings ?? [],
154
+ state: state ?? emptyState(), controls: [], stepsTrace: {}, queued: [],
155
+ }
156
+ records.set(id, record)
157
+ index.unshift(indexEntryOf(record))
158
+ persistRun(record)
159
+ persistIndex()
160
+ evictOverCapacity()
161
+ return record
162
+ },
163
+
164
+ step({ runId, stepId, instance, event, body }) {
165
+ ensureLoaded()
166
+ assertRunId(runId)
167
+ const record = records.get(runId)
168
+ if (!record) throw new Error(`运行记录不存在:${runId}`)
169
+ const at = Date.now()
170
+ if (CONTROL_EVENTS.has(event)) {
171
+ // controls 裁决/控制记账:v5 增 by(裁决来源)与 reason(代审必填)
172
+ record.controls.push({ seq: record.controls.length + 1, at, kind: body?.kind, by: body?.by, reason: body?.reason, text: body?.text, inject: body?.inject })
173
+ } else {
174
+ const key = instance ?? '-'
175
+ const stepEvents = (record.stepsTrace[stepId] ??= {})
176
+ ;(stepEvents[key] ??= []).push({ event, at, ...body })
177
+ }
178
+ persistRun(record)
179
+ return record
180
+ },
181
+
182
+ update({ runId, state, status, summary, finishedAt, queued, waiting }) {
183
+ ensureLoaded()
184
+ assertRunId(runId)
185
+ const record = records.get(runId)
186
+ // 记录已被移除(run-remove)时丢弃 dangling 写,保 driver 收尾链走完注销
187
+ if (!record) return undefined
188
+ if (state !== undefined) record.state = state
189
+ if (status !== undefined) record.status = status
190
+ if (summary !== undefined) record.summary = summary
191
+ if (finishedAt !== undefined) record.finishedAt = finishedAt
192
+ if (queued !== undefined) record.queued = queued
193
+ if (waiting !== undefined) record.waiting = waiting ?? undefined
194
+ if (status !== undefined) {
195
+ // 状态迁移同步 index 行:waiting_approval/paused 期间列表分组/轮询不基于失真状态
196
+ const entry = index.find((e) => e.runId === runId)
197
+ if (entry) Object.assign(entry, indexEntryOf(record))
198
+ persistIndex()
199
+ }
200
+ persistRun(record)
201
+ return record
202
+ },
203
+
204
+ finish({ runId, status, summary }) {
205
+ ensureLoaded()
206
+ assertRunId(runId)
207
+ const record = records.get(runId)
208
+ // 同 update:记录缺失即收尾目标已达成,幂等无害丢弃
209
+ if (!record) return undefined
210
+ record.status = status
211
+ record.finishedAt = Date.now()
212
+ record.summary = summary ?? ''
213
+ persistRun(record)
214
+ const entry = index.find((e) => e.runId === runId)
215
+ if (entry) Object.assign(entry, indexEntryOf(record))
216
+ persistIndex()
217
+ evictOverCapacity()
218
+ return record
219
+ },
220
+
221
+ get(runId) {
222
+ ensureLoaded()
223
+ assertRunId(runId)
224
+ const record = records.get(runId)
225
+ if (!record) throw new Error(`运行记录不存在:${runId}`)
226
+ return record
227
+ },
228
+
229
+ has(runId) {
230
+ ensureLoaded()
231
+ return records.has(runId)
232
+ },
233
+
234
+ list({ sessionId, workspace } = {}) {
235
+ ensureLoaded()
236
+ return index
237
+ .filter((e) => (sessionId === undefined || e.sessionId === sessionId) && (workspace === undefined || e.workspace === workspace))
238
+ .map((e) => ({ ...e }))
239
+ },
240
+
241
+ remove(runId) {
242
+ ensureLoaded()
243
+ assertRunId(runId)
244
+ removeInternal(runId)
245
+ persistIndex()
246
+ },
247
+ }
248
+ return store
249
+ }
250
+
251
+ let singleton = null
252
+ export function reportStore() {
253
+ singleton ??= createStore()
254
+ return singleton
255
+ }
@@ -0,0 +1,161 @@
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
+ }