@aalis/plugin-workflow 0.14.0 → 0.15.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/dist/engine.d.ts +8 -7
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +48 -18
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +247 -253
- package/dist/index.js.map +1 -1
- package/dist/triggers.d.ts +2 -2
- package/dist/triggers.d.ts.map +1 -1
- package/dist/triggers.js +20 -26
- package/dist/triggers.js.map +1 -1
- package/package.json +12 -12
package/dist/index.js
CHANGED
|
@@ -7,27 +7,31 @@
|
|
|
7
7
|
// ============================================================
|
|
8
8
|
import { cronEngine } from '@aalis/api-cron-engine';
|
|
9
9
|
import { hooks } from '@aalis/api-hooks';
|
|
10
|
-
import {
|
|
10
|
+
import { isStorageUri, storage } from '@aalis/api-storage';
|
|
11
11
|
import { tools } from '@aalis/api-tools';
|
|
12
|
-
import { webuiServer } from '@aalis/api-webui';
|
|
12
|
+
import { markdownCode, webuiServer } from '@aalis/api-webui';
|
|
13
13
|
import { workflow } from '@aalis/api-workflow';
|
|
14
14
|
import { config, definePlugin, events, lifecycle, logger, optional, provide } from '@aalis/core';
|
|
15
|
+
import { configError, defineConfig, parseConfig } from '@aalis/schema-config';
|
|
15
16
|
import { parse, stringify } from 'yaml';
|
|
16
17
|
import { runDag, validateGraph } from './engine.js';
|
|
17
18
|
import { normalizeDef, WorkflowLoader } from './loader.js';
|
|
18
19
|
import { RunStore } from './persistence.js';
|
|
19
20
|
import { TriggerManager } from './triggers.js';
|
|
20
|
-
|
|
21
|
+
// ─── 配置 ───
|
|
22
|
+
const configSchema = defineConfig({
|
|
21
23
|
defsDir: {
|
|
22
24
|
type: 'string',
|
|
23
25
|
label: '工作流定义目录',
|
|
24
26
|
default: 'workspace:/workflows',
|
|
27
|
+
onInvalid: 'error',
|
|
25
28
|
description: '加载存储下的 *.yaml 定义(storage URI);AI 通过 workflow_define 创建的定义也写入此处。',
|
|
26
29
|
},
|
|
27
30
|
runsFile: {
|
|
28
31
|
type: 'string',
|
|
29
32
|
label: '运行历史文件',
|
|
30
33
|
default: 'data:/workflow-runs.json',
|
|
34
|
+
onInvalid: 'error',
|
|
31
35
|
description: '保存最近 N 条运行实例(storage URI)。',
|
|
32
36
|
},
|
|
33
37
|
maxRuns: {
|
|
@@ -40,83 +44,11 @@ const configSchema = {
|
|
|
40
44
|
type: 'boolean',
|
|
41
45
|
label: '注册 AI 工具',
|
|
42
46
|
default: true,
|
|
47
|
+
onInvalid: 'error',
|
|
43
48
|
description: '开启后向 LLM 暴露 workflow_define / workflow_run 等工具。',
|
|
44
49
|
},
|
|
45
|
-
};
|
|
46
|
-
// ───
|
|
47
|
-
const webuiPages = [
|
|
48
|
-
{
|
|
49
|
-
key: 'workflows',
|
|
50
|
-
label: '工作流',
|
|
51
|
-
icon: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="6" r="3"/><circle cx="18" cy="6" r="3"/><circle cx="12" cy="18" r="3"/><path d="M9 6h6"/><path d="M7.5 8.5L11 15.5"/><path d="M16.5 8.5L13 15.5"/></svg>',
|
|
52
|
-
order: 57,
|
|
53
|
-
content: [
|
|
54
|
-
{
|
|
55
|
-
type: 'stat',
|
|
56
|
-
label: '工作流总览',
|
|
57
|
-
source: 'workflowStats',
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
type: 'table',
|
|
61
|
-
label: '工作流定义',
|
|
62
|
-
source: 'listWorkflowsTable',
|
|
63
|
-
columns: [
|
|
64
|
-
{ key: 'id', label: 'ID', nowrap: true },
|
|
65
|
-
{ key: 'name', label: '名称' },
|
|
66
|
-
{ key: 'trigger', label: '触发器', nowrap: true },
|
|
67
|
-
{ key: 'nodeCount', label: '节点' },
|
|
68
|
-
{ key: 'enabled', label: '状态', nowrap: true },
|
|
69
|
-
{ key: 'description', label: '描述', maxWidth: 360, render: 'expandable-text' },
|
|
70
|
-
],
|
|
71
|
-
actions: [
|
|
72
|
-
{ label: '立即运行', method: 'triggerWorkflow' },
|
|
73
|
-
{ label: '禁用/启用', method: 'toggleWorkflow' },
|
|
74
|
-
{ label: '查看 YAML', method: 'getWorkflowYaml' },
|
|
75
|
-
{ label: '删除', method: 'removeWorkflow', confirm: '确定删除该工作流?(同时删除磁盘文件)', danger: true },
|
|
76
|
-
],
|
|
77
|
-
refresh: 60,
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
type: 'table',
|
|
81
|
-
label: '最近运行历史',
|
|
82
|
-
source: 'listRunsTable',
|
|
83
|
-
columns: [
|
|
84
|
-
{ key: 'runId', label: 'Run ID', nowrap: true },
|
|
85
|
-
{ key: 'workflowId', label: '工作流', nowrap: true },
|
|
86
|
-
{ key: 'status', label: '状态', nowrap: true },
|
|
87
|
-
{ key: 'triggerSource', label: '触发源', nowrap: true },
|
|
88
|
-
{ key: 'startedAtText', label: '开始时间', nowrap: true },
|
|
89
|
-
{ key: 'durationText', label: '耗时', nowrap: true },
|
|
90
|
-
{ key: 'error', label: '错误', maxWidth: 320, render: 'expandable-text' },
|
|
91
|
-
],
|
|
92
|
-
refresh: 30,
|
|
93
|
-
},
|
|
94
|
-
{
|
|
95
|
-
type: 'form',
|
|
96
|
-
label: '新建 / 覆盖工作流(粘贴完整 YAML)',
|
|
97
|
-
source: 'newWorkflowDraft',
|
|
98
|
-
save: 'upsertWorkflowYaml',
|
|
99
|
-
schema: {
|
|
100
|
-
yaml: {
|
|
101
|
-
type: 'textarea',
|
|
102
|
-
label: 'YAML 定义',
|
|
103
|
-
required: true,
|
|
104
|
-
description: '完整 WorkflowDef YAML:id / name / trigger / nodes。trigger 类型支持 cron / interval / once / event / manual。' +
|
|
105
|
-
'节点类型 tool / send-message / wait / agent(agent 节点派发指令给 agent 并等回复,配合 deps + {{outputs.X}} 做多智能体编排)。' +
|
|
106
|
-
'保存后将立即注册触发器并持久化到 defsDir。',
|
|
107
|
-
},
|
|
108
|
-
persist: {
|
|
109
|
-
type: 'boolean',
|
|
110
|
-
label: '写入磁盘',
|
|
111
|
-
default: true,
|
|
112
|
-
description: '关闭后仅在内存中注册(进程重启后丢失)',
|
|
113
|
-
},
|
|
114
|
-
},
|
|
115
|
-
},
|
|
116
|
-
],
|
|
117
|
-
},
|
|
118
|
-
];
|
|
119
|
-
// ─── 工具函数(与页面动作共享) ───
|
|
50
|
+
});
|
|
51
|
+
// ─── 工具函数(与工作流页共享) ───
|
|
120
52
|
function triggerSummary(t) {
|
|
121
53
|
switch (t.type) {
|
|
122
54
|
case 'cron':
|
|
@@ -133,16 +65,6 @@ function triggerSummary(t) {
|
|
|
133
65
|
return t.type;
|
|
134
66
|
}
|
|
135
67
|
}
|
|
136
|
-
function fmtTs(ts) {
|
|
137
|
-
if (!ts || !Number.isFinite(ts))
|
|
138
|
-
return '—';
|
|
139
|
-
try {
|
|
140
|
-
return new Date(ts).toLocaleString('zh-CN', { hour12: false });
|
|
141
|
-
}
|
|
142
|
-
catch {
|
|
143
|
-
return String(ts);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
68
|
function fmtDuration(ms) {
|
|
147
69
|
if (!Number.isFinite(ms) || ms < 0)
|
|
148
70
|
return '—';
|
|
@@ -155,7 +77,7 @@ function fmtDuration(ms) {
|
|
|
155
77
|
return `${m}m${s}s`;
|
|
156
78
|
}
|
|
157
79
|
/**
|
|
158
|
-
* once 记账读不出时本次运行不安排 once:定义或启用一个 once 的回执据此如实说明(AI
|
|
80
|
+
* once 记账读不出时本次运行不安排 once:定义或启用一个 once 的回执据此如实说明(AI 工具与工作流页同一口径)。
|
|
159
81
|
* manualRun 是各自那一面手动执行的说法;不持久化的定义重启后就没了,「重启恢复」只对写了文件的定义成立。
|
|
160
82
|
*/
|
|
161
83
|
function onceSkippedNote(def, persist, onceLedgerReadable, manualRun) {
|
|
@@ -163,143 +85,198 @@ function onceSkippedNote(def, persist, onceLedgerReadable, manualRun) {
|
|
|
163
85
|
return undefined;
|
|
164
86
|
return `once 记账读取失败(见日志),本次运行不会触发;可用${manualRun}手动执行${persist ? ',修复运行历史文件后重启恢复' : ''}`;
|
|
165
87
|
}
|
|
88
|
+
const STATUS = {
|
|
89
|
+
pending: { text: '等待', state: 'inactive' },
|
|
90
|
+
running: { text: '运行中', state: 'running' },
|
|
91
|
+
success: { text: '成功' },
|
|
92
|
+
failed: { text: '失败', state: 'error' },
|
|
93
|
+
cancelled: { text: '已取消', state: 'inactive' },
|
|
94
|
+
};
|
|
166
95
|
/**
|
|
167
|
-
*
|
|
168
|
-
*
|
|
96
|
+
* 工作流页:处理函数是闭包,直接用本次激活构造的 WorkflowService,不按名回查容器(查到的可能是别人提供的同名服务)。
|
|
97
|
+
* 返回页面的句柄:定义增删与运行起止时让它重取
|
|
169
98
|
*/
|
|
170
|
-
function
|
|
171
|
-
|
|
172
|
-
const
|
|
173
|
-
const enabled = defs.filter(d => d.enabled !== false).length;
|
|
174
|
-
const runs = service.listRuns(200);
|
|
175
|
-
const last24h = runs.filter(r => Date.now() - r.startedAt < 86_400_000);
|
|
176
|
-
const failed24h = last24h.filter(r => r.status === 'failed' || r.status === 'cancelled').length;
|
|
177
|
-
return {
|
|
178
|
-
value: defs.length,
|
|
179
|
-
hint: `启用 ${enabled} · 24h 运行 ${last24h.length} · 失败 ${failed24h}`,
|
|
180
|
-
};
|
|
181
|
-
});
|
|
182
|
-
webui.registerAction('listWorkflowsTable', async () => {
|
|
183
|
-
return service.listWorkflows().map(d => ({
|
|
184
|
-
id: d.id,
|
|
185
|
-
name: d.name ?? d.id,
|
|
186
|
-
trigger: triggerSummary(d.trigger),
|
|
187
|
-
nodeCount: d.nodes.length,
|
|
188
|
-
enabled: d.enabled === false ? '⏸ 禁用' : '✅ 启用',
|
|
189
|
-
description: d.description ?? '',
|
|
190
|
-
}));
|
|
191
|
-
});
|
|
192
|
-
webui.registerAction('listRunsTable', async () => {
|
|
193
|
-
return service.listRuns(50).map(r => {
|
|
194
|
-
const dur = (r.finishedAt ?? Date.now()) - r.startedAt;
|
|
195
|
-
const statusIcon = r.status === 'success' ? '✅' : r.status === 'failed' ? '❌' : r.status === 'cancelled' ? '⏹' : '⏳';
|
|
196
|
-
return {
|
|
197
|
-
runId: r.runId,
|
|
198
|
-
workflowId: r.workflowId,
|
|
199
|
-
status: `${statusIcon} ${r.status}`,
|
|
200
|
-
triggerSource: r.triggerSource,
|
|
201
|
-
startedAtText: fmtTs(r.startedAt),
|
|
202
|
-
durationText: fmtDuration(dur),
|
|
203
|
-
error: r.error ?? '',
|
|
204
|
-
};
|
|
205
|
-
});
|
|
206
|
-
});
|
|
207
|
-
webui.registerAction('triggerWorkflow', async (args) => {
|
|
208
|
-
try {
|
|
209
|
-
const run = await service.runWorkflow(String(args.id), {}, 'manual:webui');
|
|
210
|
-
return {
|
|
211
|
-
ok: true,
|
|
212
|
-
runId: run.runId,
|
|
213
|
-
status: run.status,
|
|
214
|
-
durationMs: (run.finishedAt ?? Date.now()) - run.startedAt,
|
|
215
|
-
error: run.error,
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
catch (err) {
|
|
219
|
-
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
220
|
-
}
|
|
221
|
-
});
|
|
222
|
-
webui.registerAction('toggleWorkflow', async (args) => {
|
|
223
|
-
const def = service.getWorkflow(String(args.id));
|
|
224
|
-
if (!def)
|
|
225
|
-
return { ok: false, error: '工作流不存在' };
|
|
226
|
-
const next = { ...def, enabled: def.enabled === false };
|
|
227
|
-
try {
|
|
228
|
-
await service.defineWorkflow(next, { persist: true });
|
|
229
|
-
const note = onceSkippedNote(next, true, onceLedgerReadable(), '「立即运行」');
|
|
230
|
-
return { ok: true, enabled: next.enabled, ...(note ? { message: `已启用;${note}` } : {}) };
|
|
231
|
-
}
|
|
232
|
-
catch (err) {
|
|
233
|
-
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
234
|
-
}
|
|
235
|
-
});
|
|
236
|
-
webui.registerAction('getWorkflowYaml', async (args) => {
|
|
237
|
-
const def = service.getWorkflow(String(args.id));
|
|
99
|
+
function defineWorkflowPage(webui, service, onceLedgerReadable) {
|
|
100
|
+
const definitionOf = (id) => {
|
|
101
|
+
const def = service.getWorkflow(id);
|
|
238
102
|
if (!def)
|
|
239
|
-
|
|
240
|
-
return
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
103
|
+
throw new Error(`工作流 ${id} 不存在`);
|
|
104
|
+
return def;
|
|
105
|
+
};
|
|
106
|
+
/** 启用或停用:改的是写在磁盘上的定义 */
|
|
107
|
+
const setEnabled = async (id, enabled) => {
|
|
108
|
+
const next = { ...definitionOf(id), enabled };
|
|
109
|
+
await service.defineWorkflow(next, { persist: true });
|
|
110
|
+
const note = onceSkippedNote(next, true, onceLedgerReadable(), '「立即运行」');
|
|
111
|
+
return `已${enabled ? '启用' : '停用'} ${id}${note ? `;${note}` : ''}`;
|
|
112
|
+
};
|
|
113
|
+
return webui.definePage({
|
|
114
|
+
key: 'workflows',
|
|
115
|
+
title: '工作流',
|
|
116
|
+
icon: 'workflow',
|
|
117
|
+
actions: {
|
|
118
|
+
runWorkflow: {
|
|
119
|
+
label: '立即运行',
|
|
120
|
+
effect: 'write',
|
|
121
|
+
row: true,
|
|
122
|
+
async handler({ rowKey }) {
|
|
123
|
+
const run = await service.runWorkflow(rowKey, {}, 'manual:webui');
|
|
124
|
+
if (run.status !== 'success') {
|
|
125
|
+
throw new Error(`运行没有成功(${STATUS[run.status].text})${run.error ? `:${run.error}` : ''}`);
|
|
126
|
+
}
|
|
127
|
+
return `已运行 ${rowKey},用时 ${fmtDuration((run.finishedAt ?? Date.now()) - run.startedAt)}`;
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
enableWorkflow: { label: '启用', effect: 'write', row: true, handler: ({ rowKey }) => setEnabled(rowKey, true) },
|
|
131
|
+
disableWorkflow: {
|
|
132
|
+
label: '停用',
|
|
133
|
+
effect: 'write',
|
|
134
|
+
row: true,
|
|
135
|
+
handler: ({ rowKey }) => setEnabled(rowKey, false),
|
|
136
|
+
},
|
|
137
|
+
removeWorkflow: {
|
|
138
|
+
label: '删除',
|
|
139
|
+
effect: 'destructive',
|
|
140
|
+
confirm: '删除这个工作流,连同磁盘上的定义文件?',
|
|
141
|
+
row: true,
|
|
142
|
+
async handler({ rowKey }) {
|
|
143
|
+
if (!(await service.removeWorkflow(rowKey)))
|
|
144
|
+
throw new Error(`工作流 ${rowKey} 不存在`);
|
|
145
|
+
return `已删除 ${rowKey}`;
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
defineWorkflow: {
|
|
149
|
+
label: '新建或覆盖',
|
|
150
|
+
effect: 'write',
|
|
151
|
+
input: {
|
|
152
|
+
yaml: {
|
|
153
|
+
type: 'textarea',
|
|
154
|
+
label: 'YAML 定义',
|
|
155
|
+
required: true,
|
|
156
|
+
description: '完整的定义:id、name、trigger、nodes。trigger 支持 cron、interval、once、event、manual;节点支持 tool、send-message、' +
|
|
157
|
+
'wait、agent(派发指令给 agent 并等回复,配合 deps 与 {{outputs.X}} 做多智能体编排)。同 id 的定义被覆盖,' +
|
|
158
|
+
'保存后立即注册触发器。现有定义的 YAML 在定义表格的详情里,可以照着改。',
|
|
159
|
+
},
|
|
160
|
+
persist: {
|
|
161
|
+
type: 'boolean',
|
|
162
|
+
label: '写入磁盘',
|
|
163
|
+
default: true,
|
|
164
|
+
description: '关闭后只在内存里注册,重启后丢失',
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
async handler({ input }) {
|
|
168
|
+
let raw;
|
|
169
|
+
try {
|
|
170
|
+
raw = parse(input.yaml);
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
throw new Error(`YAML 解析失败:${err instanceof Error ? err.message : String(err)}`);
|
|
174
|
+
}
|
|
175
|
+
const def = normalizeDef(raw, `wf-${Date.now()}`);
|
|
176
|
+
if (!def)
|
|
177
|
+
throw new Error('定义不合法:缺少 trigger 或 nodes');
|
|
178
|
+
await service.defineWorkflow(def, { persist: input.persist });
|
|
179
|
+
const note = onceSkippedNote(def, input.persist, onceLedgerReadable(), '「立即运行」');
|
|
180
|
+
return `已保存工作流 ${def.id}${note ? `;${note}` : ''}`;
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
content: [
|
|
185
|
+
{
|
|
186
|
+
type: 'info',
|
|
187
|
+
id: 'summary',
|
|
188
|
+
data() {
|
|
189
|
+
const defs = service.listWorkflows();
|
|
190
|
+
const recent = service.listRuns(200).filter(r => Date.now() - r.startedAt < 86_400_000);
|
|
191
|
+
return [
|
|
192
|
+
{ label: '定义', value: defs.length },
|
|
193
|
+
{ label: '启用', value: defs.filter(d => d.enabled !== false).length },
|
|
194
|
+
{ label: '24 小时运行', value: recent.length },
|
|
195
|
+
{
|
|
196
|
+
label: '24 小时失败',
|
|
197
|
+
value: recent.filter(r => r.status === 'failed' || r.status === 'cancelled').length,
|
|
198
|
+
},
|
|
199
|
+
];
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
type: 'table',
|
|
204
|
+
id: 'workflows',
|
|
205
|
+
label: '定义',
|
|
206
|
+
rowKey: 'id',
|
|
207
|
+
empty: '还没有工作流',
|
|
208
|
+
columns: [
|
|
209
|
+
{ key: 'id', label: 'ID', kind: 'code' },
|
|
210
|
+
{ key: 'name', label: '名称', primary: true },
|
|
211
|
+
{ key: 'trigger', label: '触发' },
|
|
212
|
+
{ key: 'nodes', label: '节点数' },
|
|
213
|
+
{ key: 'status', label: '状态', kind: 'status', primary: true },
|
|
214
|
+
{ key: 'description', label: '描述', kind: 'long-text' },
|
|
215
|
+
],
|
|
216
|
+
rowActions: [
|
|
217
|
+
{ action: 'runWorkflow' },
|
|
218
|
+
{ action: 'enableWorkflow', visibleWhen: 'disabled' },
|
|
219
|
+
{ action: 'disableWorkflow', visibleWhen: 'enabled' },
|
|
220
|
+
{ action: 'removeWorkflow' },
|
|
221
|
+
],
|
|
222
|
+
data: () => service.listWorkflows().map(d => ({
|
|
223
|
+
id: d.id,
|
|
224
|
+
name: d.name ?? d.id,
|
|
225
|
+
trigger: triggerSummary(d.trigger),
|
|
226
|
+
nodes: d.nodes.length,
|
|
227
|
+
status: d.enabled === false ? { text: '停用', state: 'inactive' } : { text: '启用' },
|
|
228
|
+
enabled: d.enabled !== false,
|
|
229
|
+
disabled: d.enabled === false,
|
|
230
|
+
description: d.description ?? '',
|
|
231
|
+
})),
|
|
232
|
+
detail: [
|
|
233
|
+
{
|
|
234
|
+
type: 'markdown',
|
|
235
|
+
id: 'yaml',
|
|
236
|
+
label: 'YAML',
|
|
237
|
+
data: ({ rowKey }) => markdownCode(stringify(definitionOf(rowKey)), 'yaml'),
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
type: 'table',
|
|
243
|
+
id: 'runs',
|
|
244
|
+
label: '最近运行',
|
|
245
|
+
rowKey: 'runId',
|
|
246
|
+
empty: '还没有运行过',
|
|
247
|
+
columns: [
|
|
248
|
+
{ key: 'workflowId', label: '工作流', primary: true },
|
|
249
|
+
{ key: 'status', label: '状态', kind: 'status', primary: true },
|
|
250
|
+
{ key: 'triggerSource', label: '触发来源' },
|
|
251
|
+
{ key: 'startedAt', label: '开始时间', kind: 'time' },
|
|
252
|
+
{ key: 'duration', label: '用时' },
|
|
253
|
+
{ key: 'error', label: '错误', kind: 'long-text' },
|
|
254
|
+
{ key: 'runId', label: '运行 ID', kind: 'code' },
|
|
255
|
+
],
|
|
256
|
+
data: () => service.listRuns(50).map(r => ({
|
|
257
|
+
runId: r.runId,
|
|
258
|
+
workflowId: r.workflowId,
|
|
259
|
+
status: STATUS[r.status],
|
|
260
|
+
triggerSource: r.triggerSource,
|
|
261
|
+
startedAt: r.startedAt,
|
|
262
|
+
duration: fmtDuration((r.finishedAt ?? Date.now()) - r.startedAt),
|
|
263
|
+
error: r.error ?? '',
|
|
264
|
+
})),
|
|
265
|
+
},
|
|
266
|
+
{ type: 'actions', actions: ['defineWorkflow'] },
|
|
267
|
+
],
|
|
283
268
|
});
|
|
284
269
|
}
|
|
285
|
-
/** 路径配置只接受 storage URI
|
|
270
|
+
/** 路径配置只接受 storage URI;留空用默认值,其它写法(如相对路径 workspace/workflows)拒绝激活。 */
|
|
286
271
|
function resolveUri(key, input, fallback) {
|
|
287
|
-
const s =
|
|
272
|
+
const s = input.trim();
|
|
288
273
|
if (!s)
|
|
289
274
|
return fallback;
|
|
290
275
|
if (!isStorageUri(s)) {
|
|
291
|
-
throw
|
|
276
|
+
throw configError(`${key}="${s}" 不是 storage URI,请写成 <根名>:/<路径>(如 ${fallback}),或删掉该键使用默认值`);
|
|
292
277
|
}
|
|
293
278
|
return s;
|
|
294
279
|
}
|
|
295
|
-
function resolveConfig(raw) {
|
|
296
|
-
return {
|
|
297
|
-
defsDir: resolveUri('defsDir', raw.defsDir, 'workspace:/workflows'),
|
|
298
|
-
runsFile: resolveUri('runsFile', raw.runsFile, 'data:/workflow-runs.json'),
|
|
299
|
-
maxRuns: typeof raw.maxRuns === 'number' && raw.maxRuns > 0 ? raw.maxRuns : 200,
|
|
300
|
-
enableTools: raw.enableTools !== false,
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
280
|
// ─── 插件入口 ───
|
|
304
281
|
const uses = {
|
|
305
282
|
/** cron / interval 触发器共享的整分钟 tick 与表达式解析 */
|
|
@@ -308,7 +285,7 @@ const uses = {
|
|
|
308
285
|
storage: optional(storage),
|
|
309
286
|
/** tool 节点的执行入口,兼 AI 工具面的登记口 */
|
|
310
287
|
tools: optional(tools),
|
|
311
|
-
/**
|
|
288
|
+
/** 工作流页的登记口;无 WebUI 时引擎照常运行 */
|
|
312
289
|
webui: optional(webuiServer),
|
|
313
290
|
events,
|
|
314
291
|
/** agent 节点经 'agent:turn:after' 捕获本轮回复 */
|
|
@@ -332,16 +309,24 @@ export default definePlugin({
|
|
|
332
309
|
});
|
|
333
310
|
async function run(caps) {
|
|
334
311
|
const { events, hooks, lifecycle, provide, tools, webui } = caps;
|
|
335
|
-
const
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
312
|
+
const cfg = parseConfig(configSchema, caps.config.use, caps.logger.use);
|
|
313
|
+
const config = {
|
|
314
|
+
...cfg,
|
|
315
|
+
defsDir: resolveUri('defsDir', cfg.defsDir, configSchema.defsDir.default),
|
|
316
|
+
runsFile: resolveUri('runsFile', cfg.runsFile, configSchema.runsFile.default),
|
|
317
|
+
maxRuns: cfg.maxRuns > 0 ? cfg.maxRuns : configSchema.maxRuns.default,
|
|
318
|
+
};
|
|
319
|
+
const logger = caps.logger.use.child('workflow');
|
|
320
|
+
const loader = new WorkflowLoader(caps.storage.use, config.defsDir, logger);
|
|
339
321
|
const scanned = await loader.loadAll();
|
|
340
|
-
const runStore = new RunStore(
|
|
322
|
+
const runStore = new RunStore(caps.storage.use, config.runsFile, config.maxRuns, logger);
|
|
341
323
|
await runStore.init();
|
|
342
|
-
|
|
324
|
+
/** 在飞 run 的取消开关,按 runId */
|
|
325
|
+
const aborts = new Map();
|
|
326
|
+
/** 在飞的 run:拆卸先中止,再等它们收尾,被取消的 run 也要把终态写进运行历史 */
|
|
327
|
+
const running = new Set();
|
|
343
328
|
// ── 触发管理器:内部 cron/interval/event 监听 ──
|
|
344
|
-
const triggers = new TriggerManager({ cronEngine: caps.cronEngine, events, logger }, (workflowId, source, payload) => {
|
|
329
|
+
const triggers = new TriggerManager({ cronEngine: caps.cronEngine, events: events.use, logger }, (workflowId, source, payload) => {
|
|
345
330
|
// 异步触发,不阻塞触发源;event 触发的 payload({args})作为 vars 注入,{{vars.X}} 才读得到
|
|
346
331
|
runById(workflowId, payload ?? {}, source).catch(err => {
|
|
347
332
|
logger.error(`触发执行 workflow=${workflowId} source=${source} 失败: ${err instanceof Error ? err.message : err}`);
|
|
@@ -354,15 +339,22 @@ async function run(caps) {
|
|
|
354
339
|
for (const def of loader.list())
|
|
355
340
|
triggers.register(def);
|
|
356
341
|
// ── 订阅外部 trigger:fired(来自 scheduler / 其他触发源)──
|
|
357
|
-
events.on('trigger:fired', info => {
|
|
342
|
+
events.use.on('trigger:fired', info => {
|
|
358
343
|
if (!info?.workflowId)
|
|
359
344
|
return;
|
|
360
345
|
runById(info.workflowId, info.payload ?? {}, info.source ?? `trigger:${info.type}`).catch(err => {
|
|
361
346
|
logger.error(`trigger:fired workflow=${info.workflowId} 执行失败: ${err instanceof Error ? err.message : err}`);
|
|
362
347
|
});
|
|
363
348
|
});
|
|
364
|
-
// ── 核心:跑一次 workflow
|
|
365
|
-
|
|
349
|
+
// ── 核心:跑一次 workflow(登记进 running,拆卸据此等它收尾)──
|
|
350
|
+
function runById(...args) {
|
|
351
|
+
const pending = execute(...args);
|
|
352
|
+
running.add(pending);
|
|
353
|
+
const settle = () => running.delete(pending);
|
|
354
|
+
pending.then(settle, settle);
|
|
355
|
+
return pending;
|
|
356
|
+
}
|
|
357
|
+
async function execute(workflowId, extraVars, triggerSource, caller) {
|
|
366
358
|
const def = loader.get(workflowId);
|
|
367
359
|
if (!def)
|
|
368
360
|
throw new Error(`workflow "${workflowId}" 不存在`);
|
|
@@ -370,8 +362,8 @@ async function run(caps) {
|
|
|
370
362
|
throw new Error(`workflow "${workflowId}" 已禁用`);
|
|
371
363
|
const runId = `${workflowId}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
372
364
|
const vars = { ...(def.vars ?? {}), ...extraVars };
|
|
373
|
-
const
|
|
374
|
-
|
|
365
|
+
const abort = new AbortController();
|
|
366
|
+
aborts.set(runId, abort);
|
|
375
367
|
const run = {
|
|
376
368
|
runId,
|
|
377
369
|
workflowId,
|
|
@@ -382,13 +374,14 @@ async function run(caps) {
|
|
|
382
374
|
nodes: def.nodes.map(n => ({ id: n.id, type: n.type, status: 'pending' })),
|
|
383
375
|
};
|
|
384
376
|
runStore.add(run);
|
|
385
|
-
|
|
377
|
+
page.invalidate();
|
|
378
|
+
await events.use.emit('workflow:run:start', run);
|
|
386
379
|
logger.info(`run=${runId} 启动 (trigger=${triggerSource})`);
|
|
387
380
|
try {
|
|
388
381
|
const result = await runDag({
|
|
389
382
|
tools,
|
|
390
|
-
events,
|
|
391
|
-
hooks,
|
|
383
|
+
events: events.use,
|
|
384
|
+
hooks: hooks.use,
|
|
392
385
|
logger,
|
|
393
386
|
def,
|
|
394
387
|
runId,
|
|
@@ -396,19 +389,19 @@ async function run(caps) {
|
|
|
396
389
|
// 运行时身份:经 workflow_run 工具触发时以 actor 透传【调用者】授权身份,使工作流
|
|
397
390
|
// 内部工具与 agent/send_message 节点按【调用者】的权限等级过 authority 闸(而非匿名
|
|
398
391
|
// level-0);owner 定义、谁调按谁裁决,杜绝借他人 workflow 提权。platform 保持
|
|
399
|
-
// 'workflow'
|
|
392
|
+
// 'workflow' 会话语义(不再借调用者平台——那会污染定时任务归属/审批的发起类别等分流)。
|
|
400
393
|
// cron/event/webui 触发无调用者 → 保持匿名(仅能跑 public 工具)。
|
|
401
394
|
toolCallContext: {
|
|
402
395
|
sessionId: `workflow::${workflowId}`,
|
|
403
396
|
platform: 'workflow',
|
|
404
397
|
actor: caller?.platform && caller?.userId ? { platform: caller.platform, userId: caller.userId } : undefined,
|
|
405
398
|
},
|
|
406
|
-
|
|
399
|
+
signal: abort.signal,
|
|
407
400
|
onNodeDone: (info) => {
|
|
408
401
|
const idx = run.nodes.findIndex(n => n.id === info.id);
|
|
409
402
|
if (idx >= 0)
|
|
410
403
|
run.nodes[idx] = info;
|
|
411
|
-
events.emit('workflow:node:done', { runId, node: info }).catch(() => { });
|
|
404
|
+
events.use.emit('workflow:node:done', { runId, node: info }).catch(() => { });
|
|
412
405
|
},
|
|
413
406
|
});
|
|
414
407
|
run.status = result.status;
|
|
@@ -417,11 +410,11 @@ async function run(caps) {
|
|
|
417
410
|
run.finishedAt = Date.now();
|
|
418
411
|
runStore.update(run);
|
|
419
412
|
if (result.status === 'success') {
|
|
420
|
-
await events.emit('workflow:run:done', run);
|
|
413
|
+
await events.use.emit('workflow:run:done', run);
|
|
421
414
|
logger.info(`run=${runId} 完成 (${run.finishedAt - run.startedAt}ms)`);
|
|
422
415
|
}
|
|
423
416
|
else {
|
|
424
|
-
await events.emit('workflow:run:error', run);
|
|
417
|
+
await events.use.emit('workflow:run:error', run);
|
|
425
418
|
logger.warn(`run=${runId} ${result.status}: ${result.error ?? ''}`);
|
|
426
419
|
}
|
|
427
420
|
}
|
|
@@ -430,11 +423,12 @@ async function run(caps) {
|
|
|
430
423
|
run.error = err instanceof Error ? err.message : String(err);
|
|
431
424
|
run.finishedAt = Date.now();
|
|
432
425
|
runStore.update(run);
|
|
433
|
-
await events.emit('workflow:run:error', run);
|
|
426
|
+
await events.use.emit('workflow:run:error', run);
|
|
434
427
|
logger.error(`run=${runId} 异常: ${run.error}`);
|
|
435
428
|
}
|
|
436
429
|
finally {
|
|
437
|
-
|
|
430
|
+
aborts.delete(runId);
|
|
431
|
+
page.invalidate();
|
|
438
432
|
}
|
|
439
433
|
return run;
|
|
440
434
|
}
|
|
@@ -458,6 +452,7 @@ async function run(caps) {
|
|
|
458
452
|
loader.putMemory(def);
|
|
459
453
|
}
|
|
460
454
|
triggers.register(def);
|
|
455
|
+
page.invalidate();
|
|
461
456
|
logger.info(`workflow 已定义: ${def.id} (持久化=${opts?.persist !== false})`);
|
|
462
457
|
},
|
|
463
458
|
async removeWorkflow(id) {
|
|
@@ -465,16 +460,17 @@ async function run(caps) {
|
|
|
465
460
|
const had = await loader.removeDef(id);
|
|
466
461
|
triggers.unregister(id);
|
|
467
462
|
runStore.clearOnceFired(id); // 定义没了,once 记账不留孤儿:同 id 重建算新工作流
|
|
463
|
+
page.invalidate();
|
|
468
464
|
return had;
|
|
469
465
|
},
|
|
470
466
|
async runWorkflow(id, vars, source, caller) {
|
|
471
467
|
return await runById(id, vars ?? {}, source ?? 'manual', caller);
|
|
472
468
|
},
|
|
473
469
|
cancelRun(runId) {
|
|
474
|
-
const
|
|
475
|
-
if (!
|
|
470
|
+
const abort = aborts.get(runId);
|
|
471
|
+
if (!abort)
|
|
476
472
|
return false;
|
|
477
|
-
|
|
473
|
+
abort.abort();
|
|
478
474
|
return true;
|
|
479
475
|
},
|
|
480
476
|
getRun(runId) {
|
|
@@ -484,25 +480,23 @@ async function run(caps) {
|
|
|
484
480
|
return runStore.list(limit, workflowId);
|
|
485
481
|
},
|
|
486
482
|
};
|
|
487
|
-
provide(workflow, service);
|
|
483
|
+
provide.use(workflow, service);
|
|
484
|
+
const page = defineWorkflowPage(webui.use, service, () => runStore.onceLedgerReadable());
|
|
488
485
|
// ── AI 工具 ──
|
|
489
486
|
// tools 晚上线也无妨:登记口在提供者缺席时先挂账,上线后自动补挂
|
|
490
487
|
if (config.enableTools) {
|
|
491
|
-
registerTools(tools, service, () => runStore.onceLedgerReadable());
|
|
488
|
+
registerTools(tools.use, service, () => runStore.onceLedgerReadable());
|
|
492
489
|
}
|
|
493
|
-
// ── WebUI ──
|
|
494
|
-
for (const page of webuiPages)
|
|
495
|
-
webui.registerPage(page);
|
|
496
|
-
registerWebuiActions(webui, service, () => runStore.onceLedgerReadable());
|
|
497
490
|
// ── 清理 ──
|
|
498
|
-
lifecycle.onDispose(async () => {
|
|
491
|
+
lifecycle.use.onDispose(async () => {
|
|
499
492
|
triggers.dispose();
|
|
500
|
-
//
|
|
501
|
-
//
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
493
|
+
// 中止全部在飞 run:不再调度新节点,阻塞中的 wait / agent 节点立即结束。再等它们收尾、排队中的运行历史 /
|
|
494
|
+
// once 记账写完才结束拆卸:否则 app.stop() 返回后仍有落盘在飞,bounce 时迟到的快照会盖掉新实例的记账。
|
|
495
|
+
// 不响应中止信号的工具节点至多让这里等到 disposeTimeoutMs;放弃等待后旧实例记为 lingering,同 id 的新实例要等那次
|
|
496
|
+
// run 真正落定、这里返回之后才起
|
|
497
|
+
for (const abort of aborts.values())
|
|
498
|
+
abort.abort();
|
|
499
|
+
await Promise.allSettled(running);
|
|
506
500
|
await runStore.flushed();
|
|
507
501
|
});
|
|
508
502
|
logger.info(`工作流插件已启动 (${loader.list().length} 个定义已加载)`);
|