@aalis/plugin-workflow 0.15.0 → 0.17.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/dist/engine.d.ts +18 -7
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +97 -79
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +9 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +283 -160
- package/dist/index.js.map +1 -1
- package/dist/loader.d.ts +36 -11
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +97 -27
- package/dist/loader.js.map +1 -1
- package/dist/locales/en.d.ts +96 -0
- package/dist/locales/en.d.ts.map +1 -0
- package/dist/locales/en.js +96 -0
- package/dist/locales/en.js.map +1 -0
- package/dist/persistence.d.ts +23 -13
- package/dist/persistence.d.ts.map +1 -1
- package/dist/persistence.js +79 -32
- package/dist/persistence.js.map +1 -1
- package/dist/texts.d.ts +191 -0
- package/dist/texts.d.ts.map +1 -0
- package/dist/texts.js +103 -0
- package/dist/texts.js.map +1 -0
- package/dist/triggers.d.ts +11 -4
- package/dist/triggers.d.ts.map +1 -1
- package/dist/triggers.js +57 -12
- package/dist/triggers.js.map +1 -1
- package/package.json +20 -14
package/dist/index.js
CHANGED
|
@@ -5,179 +5,207 @@
|
|
|
5
5
|
// 按 DAG 执行节点,结果存 data:/workflow-runs.json,
|
|
6
6
|
// 定义存 workspace:/workflows/*.yaml。
|
|
7
7
|
// ============================================================
|
|
8
|
+
import { authority } from '@aalis/api-authority';
|
|
8
9
|
import { cronEngine } from '@aalis/api-cron-engine';
|
|
10
|
+
import { gateway } from '@aalis/api-gateway';
|
|
9
11
|
import { hooks } from '@aalis/api-hooks';
|
|
10
12
|
import { isStorageUri, storage } from '@aalis/api-storage';
|
|
11
|
-
import { tools } from '@aalis/api-tools';
|
|
13
|
+
import { actingIdentity, tools } from '@aalis/api-tools';
|
|
12
14
|
import { markdownCode, webuiServer } from '@aalis/api-webui';
|
|
13
15
|
import { workflow } from '@aalis/api-workflow';
|
|
14
16
|
import { config, definePlugin, events, lifecycle, logger, optional, provide } from '@aalis/core';
|
|
15
17
|
import { configError, defineConfig, parseConfig } from '@aalis/schema-config';
|
|
18
|
+
import { TextError, textClauses, textOf, toSource } from '@aalis/schema-i18n';
|
|
19
|
+
import { validateIntervalSeconds } from '@aalis/util-cron';
|
|
16
20
|
import { parse, stringify } from 'yaml';
|
|
17
|
-
import { runDag, validateGraph } from './engine.js';
|
|
18
|
-
import { normalizeDef, WorkflowLoader } from './loader.js';
|
|
21
|
+
import { errorReason, runDag, validateGraph } from './engine.js';
|
|
22
|
+
import { normalizeDef, validateId, WorkflowLoader } from './loader.js';
|
|
19
23
|
import { RunStore } from './persistence.js';
|
|
20
|
-
import {
|
|
24
|
+
import { t } from './texts.js';
|
|
25
|
+
import { BLOCKED_EVENT_TRIGGERS, TriggerManager } from './triggers.js';
|
|
21
26
|
// ─── 配置 ───
|
|
22
27
|
const configSchema = defineConfig({
|
|
23
28
|
defsDir: {
|
|
24
29
|
type: 'string',
|
|
25
|
-
label: '
|
|
30
|
+
label: t('defsDirLabel'),
|
|
26
31
|
default: 'workspace:/workflows',
|
|
27
32
|
onInvalid: 'error',
|
|
28
|
-
description: '
|
|
33
|
+
description: t('defsDirDescription'),
|
|
29
34
|
},
|
|
30
35
|
runsFile: {
|
|
31
36
|
type: 'string',
|
|
32
|
-
label: '
|
|
37
|
+
label: t('runsFileLabel'),
|
|
33
38
|
default: 'data:/workflow-runs.json',
|
|
34
39
|
onInvalid: 'error',
|
|
35
|
-
description: '
|
|
40
|
+
description: t('runsFileDescription'),
|
|
36
41
|
},
|
|
37
42
|
maxRuns: {
|
|
38
43
|
type: 'number',
|
|
39
|
-
label: '
|
|
44
|
+
label: t('maxRunsLabel'),
|
|
40
45
|
default: 200,
|
|
41
|
-
description: '
|
|
46
|
+
description: t('maxRunsDescription'),
|
|
42
47
|
},
|
|
43
48
|
enableTools: {
|
|
44
49
|
type: 'boolean',
|
|
45
|
-
label: '
|
|
50
|
+
label: t('enableToolsLabel'),
|
|
46
51
|
default: true,
|
|
47
52
|
onInvalid: 'error',
|
|
48
|
-
description: '
|
|
53
|
+
description: t('enableToolsDescription'),
|
|
49
54
|
},
|
|
50
55
|
});
|
|
51
56
|
// ─── 工具函数(与工作流页共享) ───
|
|
52
|
-
function triggerSummary(
|
|
53
|
-
switch (
|
|
57
|
+
function triggerSummary(trigger) {
|
|
58
|
+
switch (trigger.type) {
|
|
54
59
|
case 'cron':
|
|
55
|
-
return `cron ${
|
|
60
|
+
return `cron ${trigger.expr ?? ''}`.trim();
|
|
56
61
|
case 'interval':
|
|
57
|
-
return
|
|
62
|
+
return t('everySeconds', { seconds: trigger.seconds ?? '?' });
|
|
58
63
|
case 'once':
|
|
59
|
-
return
|
|
64
|
+
return t('onceAt', { at: trigger.runAt ?? '?' });
|
|
60
65
|
case 'event':
|
|
61
|
-
return `event:${
|
|
66
|
+
return `event:${trigger.event ?? '?'}`;
|
|
62
67
|
case 'manual':
|
|
63
|
-
return '
|
|
68
|
+
return t('manual');
|
|
64
69
|
default:
|
|
65
|
-
return
|
|
70
|
+
return trigger.type;
|
|
66
71
|
}
|
|
67
72
|
}
|
|
73
|
+
/** 有平台与用户 ID 的才是一个身份,缺一个即匿名 */
|
|
74
|
+
function identityOf(who) {
|
|
75
|
+
return who?.platform && who.userId ? { platform: who.platform, userId: who.userId } : undefined;
|
|
76
|
+
}
|
|
77
|
+
/** 定义表里的定义者:平台:用户 ID,没有的写「匿名」 */
|
|
78
|
+
const identityText = (who) => (who ? `${who.platform}:${who.userId}` : t('anonymous'));
|
|
79
|
+
/** 用时的写法:字典的 {ms, duration}(到秒,按读者的语言);没有用时写「—」 */
|
|
68
80
|
function fmtDuration(ms) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const m = Math.floor(ms / 60_000);
|
|
76
|
-
const s = Math.floor((ms % 60_000) / 1000);
|
|
77
|
-
return `${m}m${s}s`;
|
|
81
|
+
return ms === undefined || !Number.isFinite(ms) || ms < 0 ? '—' : t('elapsed', { ms });
|
|
82
|
+
}
|
|
83
|
+
/** 运行的用时(ms):在跑的算到现在;进程退出时中断的不知道停在何时,没有用时 */
|
|
84
|
+
function elapsed(run) {
|
|
85
|
+
const end = run.status === 'running' ? Date.now() : run.finishedAt;
|
|
86
|
+
return end === undefined ? undefined : end - run.startedAt;
|
|
78
87
|
}
|
|
79
88
|
/**
|
|
80
|
-
* once
|
|
89
|
+
* once 记录读不出时本次运行不安排 once:定义或启用一个 once 的回执据此如实说明(AI 工具与工作流页同一口径,工具取原文)。
|
|
81
90
|
* manualRun 是各自那一面手动执行的说法;不持久化的定义重启后就没了,「重启恢复」只对写了文件的定义成立。
|
|
82
91
|
*/
|
|
83
92
|
function onceSkippedNote(def, persist, onceLedgerReadable, manualRun) {
|
|
84
93
|
if (def.trigger.type !== 'once' || def.enabled === false || onceLedgerReadable)
|
|
85
94
|
return undefined;
|
|
86
|
-
return
|
|
95
|
+
return t(persist ? 'onceSkippedPersisted' : 'onceSkipped', { manualRun });
|
|
87
96
|
}
|
|
97
|
+
/** 工作流页的回执:once 记录读不出时接上附注 */
|
|
98
|
+
const withNote = (receipt, note) => (note ? textClauses([receipt, note]) : receipt);
|
|
88
99
|
const STATUS = {
|
|
89
|
-
pending: { text: '
|
|
90
|
-
running: { text: '
|
|
91
|
-
success: { text: '
|
|
92
|
-
failed: { text: '
|
|
93
|
-
cancelled: { text: '
|
|
100
|
+
pending: { text: t('statusPending'), state: 'inactive' },
|
|
101
|
+
running: { text: t('statusRunning'), state: 'running' },
|
|
102
|
+
success: { text: t('statusSuccess') },
|
|
103
|
+
failed: { text: t('statusFailed'), state: 'error' },
|
|
104
|
+
cancelled: { text: t('statusCancelled'), state: 'inactive' },
|
|
94
105
|
};
|
|
95
106
|
/**
|
|
96
107
|
* 工作流页:处理函数是闭包,直接用本次激活构造的 WorkflowService,不按名回查容器(查到的可能是别人提供的同名服务)。
|
|
97
108
|
* 返回页面的句柄:定义增删与运行起止时让它重取
|
|
98
109
|
*/
|
|
99
|
-
function defineWorkflowPage(webui, service, onceLedgerReadable) {
|
|
110
|
+
function defineWorkflowPage(webui, service, definitions, onceLedgerReadable) {
|
|
100
111
|
const definitionOf = (id) => {
|
|
101
112
|
const def = service.getWorkflow(id);
|
|
102
113
|
if (!def)
|
|
103
|
-
throw new
|
|
114
|
+
throw new TextError(t('workflowMissing', { id }));
|
|
104
115
|
return def;
|
|
105
116
|
};
|
|
106
|
-
/**
|
|
117
|
+
/** 启用或停用:改的是写在磁盘上的定义,定义者沿用原来的 */
|
|
107
118
|
const setEnabled = async (id, enabled) => {
|
|
108
119
|
const next = { ...definitionOf(id), enabled };
|
|
109
|
-
await
|
|
110
|
-
const note = onceSkippedNote(next, true, onceLedgerReadable(), '
|
|
111
|
-
return
|
|
120
|
+
await definitions.define(next, true, definitions.definedBy(id));
|
|
121
|
+
const note = onceSkippedNote(next, true, onceLedgerReadable(), t('runNow'));
|
|
122
|
+
return withNote(t(enabled ? 'enabled' : 'disabled', { id }), note);
|
|
112
123
|
};
|
|
113
124
|
return webui.definePage({
|
|
114
125
|
key: 'workflows',
|
|
115
|
-
title: '
|
|
126
|
+
title: t('displayName'),
|
|
116
127
|
icon: 'workflow',
|
|
117
128
|
actions: {
|
|
118
129
|
runWorkflow: {
|
|
119
|
-
label: '
|
|
130
|
+
label: t('runWorkflow'),
|
|
120
131
|
effect: 'write',
|
|
121
132
|
row: true,
|
|
122
|
-
async handler({ rowKey }) {
|
|
123
|
-
|
|
133
|
+
async handler({ rowKey, ctx }) {
|
|
134
|
+
// 调用者是登录的 owner:按定义者执行(见 execute 的运行身份)
|
|
135
|
+
const run = await service.runWorkflow(rowKey, {}, 'manual:webui', ctx.caller);
|
|
124
136
|
if (run.status !== 'success') {
|
|
125
|
-
|
|
137
|
+
const status = STATUS[run.status].text;
|
|
138
|
+
throw new TextError(run.error ? t('runNotSucceededBecause', { status, error: run.error }) : t('runNotSucceeded', { status }));
|
|
126
139
|
}
|
|
127
|
-
return
|
|
140
|
+
return t('ran', { id: rowKey, duration: fmtDuration(elapsed(run)) });
|
|
128
141
|
},
|
|
129
142
|
},
|
|
130
|
-
enableWorkflow: {
|
|
143
|
+
enableWorkflow: {
|
|
144
|
+
label: t('enableAction'),
|
|
145
|
+
effect: 'write',
|
|
146
|
+
row: true,
|
|
147
|
+
handler: ({ rowKey }) => setEnabled(rowKey, true),
|
|
148
|
+
},
|
|
131
149
|
disableWorkflow: {
|
|
132
|
-
label: '
|
|
150
|
+
label: t('disableAction'),
|
|
133
151
|
effect: 'write',
|
|
134
152
|
row: true,
|
|
135
153
|
handler: ({ rowKey }) => setEnabled(rowKey, false),
|
|
136
154
|
},
|
|
137
155
|
removeWorkflow: {
|
|
138
|
-
label: '
|
|
156
|
+
label: t('delete'),
|
|
139
157
|
effect: 'destructive',
|
|
140
|
-
confirm: '
|
|
158
|
+
confirm: t('deleteConfirm'),
|
|
141
159
|
row: true,
|
|
142
160
|
async handler({ rowKey }) {
|
|
143
161
|
if (!(await service.removeWorkflow(rowKey)))
|
|
144
|
-
throw new
|
|
145
|
-
return
|
|
162
|
+
throw new TextError(t('workflowMissing', { id: rowKey }));
|
|
163
|
+
return t('deleted', { id: rowKey });
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
cancelRun: {
|
|
167
|
+
label: t('cancelRun'),
|
|
168
|
+
effect: 'destructive',
|
|
169
|
+
confirm: t('cancelRunConfirm'),
|
|
170
|
+
row: true,
|
|
171
|
+
// 只发出取消:运行照常收尾,记录留在运行历史里、状态为已取消,收尾时页面重取
|
|
172
|
+
handler({ rowKey }) {
|
|
173
|
+
if (!service.cancelRun(rowKey))
|
|
174
|
+
throw new TextError(t('runEnded'));
|
|
175
|
+
return t('runCancelled', { id: rowKey });
|
|
146
176
|
},
|
|
147
177
|
},
|
|
148
178
|
defineWorkflow: {
|
|
149
|
-
label: '
|
|
179
|
+
label: t('defineWorkflow'),
|
|
150
180
|
effect: 'write',
|
|
151
181
|
input: {
|
|
152
182
|
yaml: {
|
|
153
183
|
type: 'textarea',
|
|
154
|
-
label: '
|
|
184
|
+
label: t('yamlLabel'),
|
|
155
185
|
required: true,
|
|
156
|
-
description: '
|
|
157
|
-
'wait、agent(派发指令给 agent 并等回复,配合 deps 与 {{outputs.X}} 做多智能体编排)。同 id 的定义被覆盖,' +
|
|
158
|
-
'保存后立即注册触发器。现有定义的 YAML 在定义表格的详情里,可以照着改。',
|
|
186
|
+
description: t('yamlDescription'),
|
|
159
187
|
},
|
|
160
188
|
persist: {
|
|
161
189
|
type: 'boolean',
|
|
162
|
-
label: '
|
|
190
|
+
label: t('persistLabel'),
|
|
163
191
|
default: true,
|
|
164
|
-
description: '
|
|
192
|
+
description: t('persistDescription'),
|
|
165
193
|
},
|
|
166
194
|
},
|
|
167
|
-
async handler({ input }) {
|
|
195
|
+
async handler({ input, ctx }) {
|
|
168
196
|
let raw;
|
|
169
197
|
try {
|
|
170
198
|
raw = parse(input.yaml);
|
|
171
199
|
}
|
|
172
200
|
catch (err) {
|
|
173
|
-
throw new
|
|
201
|
+
throw new TextError(t('yamlFailed', { error: textOf(err) }));
|
|
174
202
|
}
|
|
175
203
|
const def = normalizeDef(raw, `wf-${Date.now()}`);
|
|
176
204
|
if (!def)
|
|
177
|
-
throw new
|
|
178
|
-
await
|
|
179
|
-
const note = onceSkippedNote(def, input.persist, onceLedgerReadable(), '
|
|
180
|
-
return
|
|
205
|
+
throw new TextError(t('badDefinition'));
|
|
206
|
+
await definitions.define(def, input.persist, ctx.caller);
|
|
207
|
+
const note = onceSkippedNote(def, input.persist, onceLedgerReadable(), t('runNow'));
|
|
208
|
+
return withNote(t('saved', { id: def.id }), note);
|
|
181
209
|
},
|
|
182
210
|
},
|
|
183
211
|
},
|
|
@@ -189,12 +217,12 @@ function defineWorkflowPage(webui, service, onceLedgerReadable) {
|
|
|
189
217
|
const defs = service.listWorkflows();
|
|
190
218
|
const recent = service.listRuns(200).filter(r => Date.now() - r.startedAt < 86_400_000);
|
|
191
219
|
return [
|
|
192
|
-
{ label: '
|
|
193
|
-
{ label: '
|
|
194
|
-
{ label: '
|
|
220
|
+
{ label: t('definitions'), value: defs.length },
|
|
221
|
+
{ label: t('stateEnabled'), value: defs.filter(d => d.enabled !== false).length },
|
|
222
|
+
{ label: t('runs24h'), value: recent.length },
|
|
195
223
|
{
|
|
196
|
-
label: '
|
|
197
|
-
value: recent.filter(r => r.status === 'failed'
|
|
224
|
+
label: t('failures24h'),
|
|
225
|
+
value: recent.filter(r => r.status === 'failed').length,
|
|
198
226
|
},
|
|
199
227
|
];
|
|
200
228
|
},
|
|
@@ -202,16 +230,17 @@ function defineWorkflowPage(webui, service, onceLedgerReadable) {
|
|
|
202
230
|
{
|
|
203
231
|
type: 'table',
|
|
204
232
|
id: 'workflows',
|
|
205
|
-
label: '
|
|
233
|
+
label: t('definitions'),
|
|
206
234
|
rowKey: 'id',
|
|
207
|
-
empty: '
|
|
235
|
+
empty: t('noWorkflows'),
|
|
208
236
|
columns: [
|
|
209
237
|
{ key: 'id', label: 'ID', kind: 'code' },
|
|
210
|
-
{ key: 'name', label: '
|
|
211
|
-
{ key: 'trigger', label: '
|
|
212
|
-
{ key: 'nodes', label: '
|
|
213
|
-
{ key: 'status', label: '
|
|
214
|
-
{ key: '
|
|
238
|
+
{ key: 'name', label: t('name'), primary: true },
|
|
239
|
+
{ key: 'trigger', label: t('trigger') },
|
|
240
|
+
{ key: 'nodes', label: t('nodeCount') },
|
|
241
|
+
{ key: 'status', label: t('status'), kind: 'status', primary: true },
|
|
242
|
+
{ key: 'definedBy', label: t('definedBy') },
|
|
243
|
+
{ key: 'description', label: t('description'), kind: 'long-text' },
|
|
215
244
|
],
|
|
216
245
|
rowActions: [
|
|
217
246
|
{ action: 'runWorkflow' },
|
|
@@ -224,10 +253,13 @@ function defineWorkflowPage(webui, service, onceLedgerReadable) {
|
|
|
224
253
|
name: d.name ?? d.id,
|
|
225
254
|
trigger: triggerSummary(d.trigger),
|
|
226
255
|
nodes: d.nodes.length,
|
|
227
|
-
status: d.enabled === false
|
|
256
|
+
status: d.enabled === false
|
|
257
|
+
? { text: t('stateDisabled'), state: 'inactive' }
|
|
258
|
+
: { text: t('stateEnabled') },
|
|
228
259
|
enabled: d.enabled !== false,
|
|
229
260
|
disabled: d.enabled === false,
|
|
230
261
|
description: d.description ?? '',
|
|
262
|
+
definedBy: identityText(definitions.definedBy(d.id)),
|
|
231
263
|
})),
|
|
232
264
|
detail: [
|
|
233
265
|
{
|
|
@@ -241,26 +273,32 @@ function defineWorkflowPage(webui, service, onceLedgerReadable) {
|
|
|
241
273
|
{
|
|
242
274
|
type: 'table',
|
|
243
275
|
id: 'runs',
|
|
244
|
-
label: '
|
|
276
|
+
label: t('recentRuns'),
|
|
245
277
|
rowKey: 'runId',
|
|
246
|
-
empty: '
|
|
278
|
+
empty: t('noRuns'),
|
|
247
279
|
columns: [
|
|
248
|
-
{ key: 'workflowId', label: '
|
|
249
|
-
{ key: 'status', label: '
|
|
250
|
-
{ key: 'triggerSource', label: '
|
|
251
|
-
{ key: 'startedAt', label: '
|
|
252
|
-
{ key: 'duration', label: '
|
|
253
|
-
{ key: 'error', label: '
|
|
254
|
-
{ key: 'runId', label: '
|
|
280
|
+
{ key: 'workflowId', label: t('workflowColumn'), primary: true },
|
|
281
|
+
{ key: 'status', label: t('status'), kind: 'status', primary: true },
|
|
282
|
+
{ key: 'triggerSource', label: t('triggerSource') },
|
|
283
|
+
{ key: 'startedAt', label: t('startedAt'), kind: 'time' },
|
|
284
|
+
{ key: 'duration', label: t('duration') },
|
|
285
|
+
{ key: 'error', label: t('error'), kind: 'long-text' },
|
|
286
|
+
{ key: 'runId', label: t('runId'), kind: 'code' },
|
|
255
287
|
],
|
|
256
|
-
|
|
288
|
+
rowActions: [{ action: 'cancelRun', visibleWhen: 'running' }],
|
|
289
|
+
// 最近 50 次,更早而还在跑的接在后面(按开始时间倒序,它们本就排在后面):取消的入口不随新的运行挤掉
|
|
290
|
+
data: () => service
|
|
291
|
+
.listRuns()
|
|
292
|
+
.filter((r, i) => i < 50 || r.status === 'running')
|
|
293
|
+
.map(r => ({
|
|
257
294
|
runId: r.runId,
|
|
258
295
|
workflowId: r.workflowId,
|
|
259
296
|
status: STATUS[r.status],
|
|
260
297
|
triggerSource: r.triggerSource,
|
|
261
298
|
startedAt: r.startedAt,
|
|
262
|
-
duration: fmtDuration((r
|
|
299
|
+
duration: fmtDuration(elapsed(r)),
|
|
263
300
|
error: r.error ?? '',
|
|
301
|
+
running: r.status === 'running',
|
|
264
302
|
})),
|
|
265
303
|
},
|
|
266
304
|
{ type: 'actions', actions: ['defineWorkflow'] },
|
|
@@ -273,7 +311,7 @@ function resolveUri(key, input, fallback) {
|
|
|
273
311
|
if (!s)
|
|
274
312
|
return fallback;
|
|
275
313
|
if (!isStorageUri(s)) {
|
|
276
|
-
throw configError(
|
|
314
|
+
throw configError(t('badUri', { key, value: s, fallback }));
|
|
277
315
|
}
|
|
278
316
|
return s;
|
|
279
317
|
}
|
|
@@ -281,15 +319,22 @@ function resolveUri(key, input, fallback) {
|
|
|
281
319
|
const uses = {
|
|
282
320
|
/** cron / interval 触发器共享的整分钟 tick 与表达式解析 */
|
|
283
321
|
cronEngine,
|
|
284
|
-
/**
|
|
285
|
-
|
|
322
|
+
/**
|
|
323
|
+
* 定义(*.yaml)与运行历史的持久化出口:激活时读回。required:读回要在它上线之后(optional 不参与激活先后),
|
|
324
|
+
* 它下线时随之停下、重新上线后重新读回
|
|
325
|
+
*/
|
|
326
|
+
storage,
|
|
286
327
|
/** tool 节点的执行入口,兼 AI 工具面的登记口 */
|
|
287
328
|
tools: optional(tools),
|
|
288
329
|
/** 工作流页的登记口;无 WebUI 时引擎照常运行 */
|
|
289
330
|
webui: optional(webuiServer),
|
|
331
|
+
/** 判调用者是不是 owner,定运行身份;缺席时判不出谁是 owner,调用者与定义者不是同一身份的按匿名 */
|
|
332
|
+
authority: optional(authority),
|
|
290
333
|
events,
|
|
291
334
|
/** agent 节点经 'agent:turn:after' 捕获本轮回复 */
|
|
292
335
|
hooks,
|
|
336
|
+
/** agent 节点经它送达指令;缺席时 agent 节点失败 */
|
|
337
|
+
gateway: optional(gateway),
|
|
293
338
|
lifecycle,
|
|
294
339
|
logger,
|
|
295
340
|
config,
|
|
@@ -297,7 +342,7 @@ const uses = {
|
|
|
297
342
|
};
|
|
298
343
|
export default definePlugin({
|
|
299
344
|
name: '@aalis/plugin-workflow',
|
|
300
|
-
displayName: '
|
|
345
|
+
displayName: t('displayName'),
|
|
301
346
|
subsystem: 'workflow',
|
|
302
347
|
extends: {
|
|
303
348
|
events: ['workflow:run:start', 'workflow:run:done', 'workflow:run:error', 'workflow:node:done', 'trigger:fired'],
|
|
@@ -317,36 +362,39 @@ async function run(caps) {
|
|
|
317
362
|
maxRuns: cfg.maxRuns > 0 ? cfg.maxRuns : configSchema.maxRuns.default,
|
|
318
363
|
};
|
|
319
364
|
const logger = caps.logger.use.child('workflow');
|
|
320
|
-
|
|
321
|
-
const scanned = await loader.loadAll();
|
|
365
|
+
// 先读运行历史:载入定义时按里面的定义者记录认定义者
|
|
322
366
|
const runStore = new RunStore(caps.storage.use, config.runsFile, config.maxRuns, logger);
|
|
323
367
|
await runStore.init();
|
|
368
|
+
const loader = new WorkflowLoader(caps.storage.use, config.defsDir, logger);
|
|
369
|
+
const scanned = await loader.loadAll((id, sha256) => runStore.definerOf(id, sha256));
|
|
324
370
|
/** 在飞 run 的取消开关,按 runId */
|
|
325
371
|
const aborts = new Map();
|
|
326
372
|
/** 在飞的 run:拆卸先中止,再等它们收尾,被取消的 run 也要把终态写进运行历史 */
|
|
327
373
|
const running = new Set();
|
|
374
|
+
/** 定时与事件触发:以定义者作调用者 */
|
|
375
|
+
const runAsDefiner = (workflowId, vars, source) => runById(workflowId, vars, source, loader.definedBy(workflowId));
|
|
328
376
|
// ── 触发管理器:内部 cron/interval/event 监听 ──
|
|
329
|
-
const triggers = new TriggerManager({ cronEngine: caps.cronEngine, events: events.use, logger }, (workflowId, source, payload) => {
|
|
377
|
+
const triggers = new TriggerManager({ cronEngine: caps.cronEngine, events: events.use, logger, signal: lifecycle.use.signal }, (workflowId, source, payload) => {
|
|
330
378
|
// 异步触发,不阻塞触发源;event 触发的 payload({args})作为 vars 注入,{{vars.X}} 才读得到
|
|
331
|
-
|
|
379
|
+
runAsDefiner(workflowId, payload ?? {}, source).catch(err => {
|
|
332
380
|
logger.error(`触发执行 workflow=${workflowId} source=${source} 失败: ${err instanceof Error ? err.message : err}`);
|
|
333
381
|
});
|
|
334
382
|
}, runStore);
|
|
335
|
-
// 定义已从磁盘删除的,once
|
|
336
|
-
//
|
|
383
|
+
// 定义已从磁盘删除的,once 记录与定义者不留孤儿(手删 yaml 不经 removeWorkflow);注册前清。
|
|
384
|
+
// 列目录失败那次启动看到的是空集,不能据此清掉记录——否则下次启动所有过期 once 全部重放
|
|
337
385
|
if (scanned)
|
|
338
|
-
runStore.
|
|
386
|
+
runStore.prune(new Set(loader.list().map(d => d.id)));
|
|
339
387
|
for (const def of loader.list())
|
|
340
388
|
triggers.register(def);
|
|
341
389
|
// ── 订阅外部 trigger:fired(来自 scheduler / 其他触发源)──
|
|
342
390
|
events.use.on('trigger:fired', info => {
|
|
343
391
|
if (!info?.workflowId)
|
|
344
392
|
return;
|
|
345
|
-
|
|
393
|
+
runAsDefiner(info.workflowId, info.payload ?? {}, info.source ?? `trigger:${info.type}`).catch(err => {
|
|
346
394
|
logger.error(`trigger:fired workflow=${info.workflowId} 执行失败: ${err instanceof Error ? err.message : err}`);
|
|
347
395
|
});
|
|
348
396
|
});
|
|
349
|
-
// ──
|
|
397
|
+
// ── 核心:执行一次 workflow(登记进 running,拆卸据此等它收尾)──
|
|
350
398
|
function runById(...args) {
|
|
351
399
|
const pending = execute(...args);
|
|
352
400
|
running.add(pending);
|
|
@@ -354,12 +402,24 @@ async function run(caps) {
|
|
|
354
402
|
pending.then(settle, settle);
|
|
355
403
|
return pending;
|
|
356
404
|
}
|
|
405
|
+
/**
|
|
406
|
+
* 运行身份:调用者是 owner 时取定义者(没有即匿名),否则取调用者。两者都是 owner 才按 owner 跑,有一方不是 owner 就由
|
|
407
|
+
* 那一方定:owner 调用别人定义或覆盖过的编排按那个人的等级执行,免确认也只对 owner 生效,借别人的编排提不了权
|
|
408
|
+
*/
|
|
409
|
+
function runIdentity(invoker, definer) {
|
|
410
|
+
if (caps.authority.present) {
|
|
411
|
+
return invoker && caps.authority.use.isOwner(invoker.platform, invoker.userId) ? definer : invoker;
|
|
412
|
+
}
|
|
413
|
+
// 权限服务不在(停用或重载的窗口)时判不出谁是 owner:两者是同一身份才按它,否则匿名
|
|
414
|
+
return invoker?.platform === definer?.platform && invoker?.userId === definer?.userId ? invoker : undefined;
|
|
415
|
+
}
|
|
357
416
|
async function execute(workflowId, extraVars, triggerSource, caller) {
|
|
358
417
|
const def = loader.get(workflowId);
|
|
359
418
|
if (!def)
|
|
360
|
-
throw new
|
|
419
|
+
throw new TextError(t('runMissing', { id: workflowId }));
|
|
361
420
|
if (def.enabled === false)
|
|
362
|
-
throw new
|
|
421
|
+
throw new TextError(t('runDisabled', { id: workflowId }));
|
|
422
|
+
const actor = runIdentity(identityOf(caller), loader.definedBy(workflowId));
|
|
363
423
|
const runId = `${workflowId}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
364
424
|
const vars = { ...(def.vars ?? {}), ...extraVars };
|
|
365
425
|
const abort = new AbortController();
|
|
@@ -378,24 +438,25 @@ async function run(caps) {
|
|
|
378
438
|
await events.use.emit('workflow:run:start', run);
|
|
379
439
|
logger.info(`run=${runId} 启动 (trigger=${triggerSource})`);
|
|
380
440
|
try {
|
|
441
|
+
// 执行完毕就摘掉中止句柄,再记终态、发收尾事件:之后的 cancelRun 回 false,不对已经落定的运行回「已取消」
|
|
381
442
|
const result = await runDag({
|
|
382
443
|
tools,
|
|
383
444
|
events: events.use,
|
|
384
445
|
hooks: hooks.use,
|
|
446
|
+
gateway: caps.gateway,
|
|
385
447
|
logger,
|
|
386
448
|
def,
|
|
387
449
|
runId,
|
|
388
450
|
vars,
|
|
389
|
-
//
|
|
390
|
-
//
|
|
391
|
-
//
|
|
392
|
-
// 'workflow' 会话语义(不再借调用者平台——那会污染定时任务归属/审批的发起类别等分流)。
|
|
393
|
-
// cron/event/webui 触发无调用者 → 保持匿名(仅能跑 public 工具)。
|
|
451
|
+
// 运行身份是工具调用上下文的唯一参与者,交给工作流内部工具与 agent/send_message 节点,按它的等级过 authority 闸;
|
|
452
|
+
// 匿名的是无主体,只能跑 public 工具。platform 保持 'workflow' 会话语义(不借调用者平台——那会污染定时任务归属/审批的
|
|
453
|
+
// 发起类别等分流)。
|
|
394
454
|
toolCallContext: {
|
|
395
455
|
sessionId: `workflow::${workflowId}`,
|
|
396
456
|
platform: 'workflow',
|
|
397
|
-
|
|
457
|
+
participants: [actor ?? { platform: 'workflow', userId: '' }],
|
|
398
458
|
},
|
|
459
|
+
callerSessionId: caller?.sessionId,
|
|
399
460
|
signal: abort.signal,
|
|
400
461
|
onNodeDone: (info) => {
|
|
401
462
|
const idx = run.nodes.findIndex(n => n.id === info.id);
|
|
@@ -403,7 +464,7 @@ async function run(caps) {
|
|
|
403
464
|
run.nodes[idx] = info;
|
|
404
465
|
events.use.emit('workflow:node:done', { runId, node: info }).catch(() => { });
|
|
405
466
|
},
|
|
406
|
-
});
|
|
467
|
+
}).finally(() => aborts.delete(runId));
|
|
407
468
|
run.status = result.status;
|
|
408
469
|
run.nodes = result.nodes;
|
|
409
470
|
run.error = result.error;
|
|
@@ -415,23 +476,64 @@ async function run(caps) {
|
|
|
415
476
|
}
|
|
416
477
|
else {
|
|
417
478
|
await events.use.emit('workflow:run:error', run);
|
|
418
|
-
logger.warn(`run=${runId} ${result.status}: ${result.error ?? ''}`);
|
|
479
|
+
logger.warn(`run=${runId} ${result.status}: ${toSource(result.error) ?? ''}`);
|
|
419
480
|
}
|
|
420
481
|
}
|
|
421
482
|
catch (err) {
|
|
422
483
|
run.status = 'failed';
|
|
423
|
-
run.error =
|
|
484
|
+
run.error = errorReason(err);
|
|
424
485
|
run.finishedAt = Date.now();
|
|
425
486
|
runStore.update(run);
|
|
426
487
|
await events.use.emit('workflow:run:error', run);
|
|
427
|
-
logger.error(`run=${runId} 异常: ${run.error}`);
|
|
488
|
+
logger.error(`run=${runId} 异常: ${toSource(run.error)}`);
|
|
428
489
|
}
|
|
429
490
|
finally {
|
|
430
|
-
aborts.delete(runId);
|
|
431
491
|
page.invalidate();
|
|
432
492
|
}
|
|
433
493
|
return run;
|
|
434
494
|
}
|
|
495
|
+
/**
|
|
496
|
+
* 触发器写错的原因:收下了也订阅不上、永远不会触发。cron 交 cron-engine 校验;interval 与定时任务的固定间隔同一条(大于 0、
|
|
497
|
+
* 不超过定时器能排的上限,超过的溢出成每毫秒触发);once 的 runAt 要能解析;event 不能订阅不可作触发源的事件
|
|
498
|
+
*/
|
|
499
|
+
function triggerError(trigger) {
|
|
500
|
+
switch (trigger.type) {
|
|
501
|
+
case 'cron': {
|
|
502
|
+
const checked = caps.cronEngine.use.validate(String(trigger.expr ?? ''));
|
|
503
|
+
return checked.ok ? null : checked.text;
|
|
504
|
+
}
|
|
505
|
+
case 'interval':
|
|
506
|
+
return validateIntervalSeconds(trigger.seconds);
|
|
507
|
+
case 'once':
|
|
508
|
+
return Number.isFinite(Date.parse(String(trigger.runAt)))
|
|
509
|
+
? null
|
|
510
|
+
: t('badRunAt', { runAt: String(trigger.runAt) });
|
|
511
|
+
case 'event':
|
|
512
|
+
return BLOCKED_EVENT_TRIGGERS.has(trigger.event) ? t('blockedEventTrigger', { event: trigger.event }) : null;
|
|
513
|
+
default:
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* 定义或覆盖:定义者写进记录,连同写进文件的文本的哈希;只在内存里的定义,定义者也只在内存。新定义的 id 不合规(它是新定义
|
|
519
|
+
* 文件的文件名;已载入的照原 id 改写它自己的文件)、图不合法、启用的定义触发器写错的,报错、不保存。停用的定义不注册触发器,
|
|
520
|
+
* 不校验它(定义目录里载入的、触发器写错的要能停用),启用时再校验
|
|
521
|
+
*/
|
|
522
|
+
async function define(def, persist, definer) {
|
|
523
|
+
const err = (loader.get(def.id) ? null : validateId(def.id)) ??
|
|
524
|
+
validateGraph(def) ??
|
|
525
|
+
(def.enabled === false ? null : triggerError(def.trigger));
|
|
526
|
+
if (err)
|
|
527
|
+
throw typeof err === 'string' ? new Error(err) : new TextError(err);
|
|
528
|
+
if (persist)
|
|
529
|
+
runStore.recordDefiner(def.id, definer, await loader.saveDef(def));
|
|
530
|
+
loader.putMemory(def, definer);
|
|
531
|
+
// 重新触发器
|
|
532
|
+
triggers.register(def);
|
|
533
|
+
page.invalidate();
|
|
534
|
+
logger.info(`workflow 已定义: ${def.id} (持久化=${persist})`);
|
|
535
|
+
}
|
|
536
|
+
const definitions = { define, definedBy: id => loader.definedBy(id) };
|
|
435
537
|
// ── WorkflowService ──
|
|
436
538
|
const service = {
|
|
437
539
|
listWorkflows() {
|
|
@@ -440,28 +542,20 @@ async function run(caps) {
|
|
|
440
542
|
getWorkflow(id) {
|
|
441
543
|
return loader.get(id);
|
|
442
544
|
},
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
throw new Error(err);
|
|
447
|
-
// 重新触发器
|
|
448
|
-
if (opts?.persist !== false) {
|
|
449
|
-
await loader.saveDef(def);
|
|
450
|
-
}
|
|
451
|
-
else {
|
|
452
|
-
loader.putMemory(def);
|
|
453
|
-
}
|
|
454
|
-
triggers.register(def);
|
|
455
|
-
page.invalidate();
|
|
456
|
-
logger.info(`workflow 已定义: ${def.id} (持久化=${opts?.persist !== false})`);
|
|
545
|
+
defineWorkflow(def, opts) {
|
|
546
|
+
// 认不出调用方是谁:没有定义者
|
|
547
|
+
return define(def, opts?.persist !== false, undefined);
|
|
457
548
|
},
|
|
458
549
|
async removeWorkflow(id) {
|
|
459
|
-
//
|
|
460
|
-
|
|
550
|
+
// 只动已载入的定义,删的是它载入时的文件:别的 id(借 ../ 指到定义目录之外的、载入失败的)不碰存储
|
|
551
|
+
if (!loader.get(id))
|
|
552
|
+
return false;
|
|
553
|
+
// 先删文件,删不掉就抛错、触发器与 once 记录原样保留:先清掉记录的话,重启后定义被重新载入,过期 once 会重放
|
|
554
|
+
await loader.removeDef(id);
|
|
461
555
|
triggers.unregister(id);
|
|
462
|
-
runStore.
|
|
556
|
+
runStore.forget(id); // 定义没了,once 记录与定义者不留孤儿:同 id 重建算新工作流
|
|
463
557
|
page.invalidate();
|
|
464
|
-
return
|
|
558
|
+
return true;
|
|
465
559
|
},
|
|
466
560
|
async runWorkflow(id, vars, source, caller) {
|
|
467
561
|
return await runById(id, vars ?? {}, source ?? 'manual', caller);
|
|
@@ -481,19 +575,19 @@ async function run(caps) {
|
|
|
481
575
|
},
|
|
482
576
|
};
|
|
483
577
|
provide.use(workflow, service);
|
|
484
|
-
const page = defineWorkflowPage(webui.use, service, () => runStore.onceLedgerReadable());
|
|
578
|
+
const page = defineWorkflowPage(webui.use, service, definitions, () => runStore.onceLedgerReadable());
|
|
485
579
|
// ── AI 工具 ──
|
|
486
|
-
// tools
|
|
580
|
+
// tools 晚上线也无妨:登记口在提供者缺席时先记下,上线后自动补挂
|
|
487
581
|
if (config.enableTools) {
|
|
488
|
-
registerTools(tools.use, service, () => runStore.onceLedgerReadable());
|
|
582
|
+
registerTools(tools.use, service, definitions, () => runStore.onceLedgerReadable());
|
|
489
583
|
}
|
|
490
584
|
// ── 清理 ──
|
|
491
585
|
lifecycle.use.onDispose(async () => {
|
|
492
586
|
triggers.dispose();
|
|
493
587
|
// 中止全部在飞 run:不再调度新节点,阻塞中的 wait / agent 节点立即结束。再等它们收尾、排队中的运行历史 /
|
|
494
|
-
// once
|
|
588
|
+
// once 记录写完才结束拆卸:否则 app.stop() 返回后仍有落盘在飞,bounce 时迟到的快照会盖掉新实例的记录。
|
|
495
589
|
// 不响应中止信号的工具节点至多让这里等到 disposeTimeoutMs;放弃等待后旧实例记为 lingering,同 id 的新实例要等那次
|
|
496
|
-
// run
|
|
590
|
+
// run 真正落定、这里返回之后才启动
|
|
497
591
|
for (const abort of aborts.values())
|
|
498
592
|
abort.abort();
|
|
499
593
|
await Promise.allSettled(running);
|
|
@@ -502,13 +596,19 @@ async function run(caps) {
|
|
|
502
596
|
logger.info(`工作流插件已启动 (${loader.list().length} 个定义已加载)`);
|
|
503
597
|
}
|
|
504
598
|
// ─── AI 工具注册 ───
|
|
505
|
-
/** onceLedgerReadable:once
|
|
506
|
-
function registerTools(tools, service, onceLedgerReadable) {
|
|
507
|
-
tools.registerGroup({
|
|
599
|
+
/** onceLedgerReadable:once 记录读不出时本次运行不安排 once,workflow_define 的回执据此如实说明 */
|
|
600
|
+
function registerTools(tools, service, definitions, onceLedgerReadable) {
|
|
601
|
+
tools.registerGroup({
|
|
602
|
+
name: 'workflow',
|
|
603
|
+
label: t('displayName'),
|
|
604
|
+
summary: t('groupSummary'),
|
|
605
|
+
description: '定义、运行、查询自主工作流(DAG)',
|
|
606
|
+
});
|
|
508
607
|
tools.register({
|
|
509
|
-
//
|
|
510
|
-
//
|
|
511
|
-
|
|
608
|
+
// 定义、跑、删编排:编排存下来以后按定义者的身份自己运行,节点能往任意会话发消息、开一轮。与子任务、定时任务、停后台任务
|
|
609
|
+
// 同一规则,2 级加确认,审批卡上是原文;别人单独发起的没人能批即拒,参与者都是 owner 且在免确认白名单里(或确认覆盖设为
|
|
610
|
+
// off)时免,批过的调用按批的人执行、定义者随之是他。列与看运行记录只看不改:2 级、不确认
|
|
611
|
+
risk: 'dangerous',
|
|
512
612
|
groups: ['workflow'],
|
|
513
613
|
definition: {
|
|
514
614
|
type: 'function',
|
|
@@ -518,7 +618,7 @@ function registerTools(tools, service, onceLedgerReadable) {
|
|
|
518
618
|
' 节点支持类型 tool / send-message / wait / agent;deps 形成 DAG;字符串值支持 {{vars.X}} 与 {{outputs.Y}} 插值。' +
|
|
519
619
|
' agent 节点(instruction 必填,可选 sessionId/platform/timeoutSeconds)会把指令派发给 agent 并等待其回复,' +
|
|
520
620
|
'回复经 out 存入 outputs 供下游插值——用 deps + agent 节点即可表达"分解→依赖→串/并行→管道→聚合"的确定性多智能体编排。' +
|
|
521
|
-
' once 触发器一生只触发一次:同 id
|
|
621
|
+
' once 触发器一生只触发一次:同 id 覆盖不会重新触发,要再执行一次请先 workflow_remove 再定义,或直接 workflow_run。',
|
|
522
622
|
parameters: {
|
|
523
623
|
type: 'object',
|
|
524
624
|
properties: {
|
|
@@ -529,7 +629,7 @@ function registerTools(tools, service, onceLedgerReadable) {
|
|
|
529
629
|
},
|
|
530
630
|
},
|
|
531
631
|
},
|
|
532
|
-
handler: async (args) => {
|
|
632
|
+
handler: async (args, callCtx) => {
|
|
533
633
|
const yamlText = String(args.yaml ?? '');
|
|
534
634
|
let raw;
|
|
535
635
|
try {
|
|
@@ -543,7 +643,9 @@ function registerTools(tools, service, onceLedgerReadable) {
|
|
|
543
643
|
return JSON.stringify({ error: '定义不合法:缺少 trigger 或 nodes' });
|
|
544
644
|
const persist = args.persist !== false;
|
|
545
645
|
try {
|
|
546
|
-
|
|
646
|
+
// 定义者取调用者的授权身份:经审批的是批的人,匿名的不记
|
|
647
|
+
const definedBy = identityOf(actingIdentity(callCtx));
|
|
648
|
+
await definitions.define(def, persist, definedBy);
|
|
547
649
|
}
|
|
548
650
|
catch (e) {
|
|
549
651
|
return JSON.stringify({ error: e instanceof Error ? e.message : String(e) });
|
|
@@ -554,11 +656,12 @@ function registerTools(tools, service, onceLedgerReadable) {
|
|
|
554
656
|
id: def.id,
|
|
555
657
|
nodes: def.nodes.length,
|
|
556
658
|
trigger: def.trigger.type,
|
|
557
|
-
...(note ? { note } : {}),
|
|
659
|
+
...(note ? { note: toSource(note) } : {}),
|
|
558
660
|
});
|
|
559
661
|
},
|
|
560
662
|
});
|
|
561
663
|
tools.register({
|
|
664
|
+
visibility: 'restricted',
|
|
562
665
|
groups: ['workflow'],
|
|
563
666
|
definition: {
|
|
564
667
|
type: 'function',
|
|
@@ -581,7 +684,7 @@ function registerTools(tools, service, onceLedgerReadable) {
|
|
|
581
684
|
},
|
|
582
685
|
});
|
|
583
686
|
tools.register({
|
|
584
|
-
risk: '
|
|
687
|
+
risk: 'dangerous',
|
|
585
688
|
groups: ['workflow'],
|
|
586
689
|
definition: {
|
|
587
690
|
type: 'function',
|
|
@@ -599,17 +702,35 @@ function registerTools(tools, service, onceLedgerReadable) {
|
|
|
599
702
|
},
|
|
600
703
|
},
|
|
601
704
|
handler: async (args, callCtx) => {
|
|
705
|
+
const { signal } = callCtx;
|
|
706
|
+
const aborted = JSON.stringify({ error: '调用已中止,不再等' });
|
|
707
|
+
if (signal?.aborted)
|
|
708
|
+
return aborted;
|
|
602
709
|
try {
|
|
603
|
-
//
|
|
604
|
-
//
|
|
605
|
-
|
|
710
|
+
// 透传调用者的授权身份(这一项要 owner 确认:批过的是批的人,免了的是那位 owner);运行身份按它与定义者定(见 execute)。
|
|
711
|
+
// 发起这次运行的会话也带上:这一轮在等这次运行,指向它的 agent 节点立即失败。调用被中止(这一轮被停止键、禁言、/clear
|
|
712
|
+
// 等停下,或本插件停用、重启)时不再等这次运行,这一轮才落定得了。回合停下时运行照常进行;本插件停用、重启时运行随拆卸
|
|
713
|
+
// 取消
|
|
714
|
+
const run = await new Promise((resolve, reject) => {
|
|
715
|
+
const stop = () => resolve(undefined);
|
|
716
|
+
signal?.addEventListener('abort', stop, { once: true });
|
|
717
|
+
service
|
|
718
|
+
.runWorkflow(String(args.id), args.vars ?? {}, 'manual:tool', {
|
|
719
|
+
...actingIdentity(callCtx),
|
|
720
|
+
sessionId: callCtx.sessionId,
|
|
721
|
+
})
|
|
722
|
+
.then(resolve, reject)
|
|
723
|
+
.finally(() => signal?.removeEventListener('abort', stop));
|
|
724
|
+
});
|
|
725
|
+
if (!run)
|
|
726
|
+
return aborted;
|
|
606
727
|
return JSON.stringify({
|
|
607
728
|
ok: true,
|
|
608
729
|
runId: run.runId,
|
|
609
730
|
status: run.status,
|
|
610
|
-
durationMs: (run
|
|
611
|
-
nodes: run.nodes.map(n => ({ id: n.id, status: n.status, error: n.error })),
|
|
612
|
-
error: run.error,
|
|
731
|
+
durationMs: elapsed(run),
|
|
732
|
+
nodes: run.nodes.map(n => ({ id: n.id, status: n.status, error: toSource(n.error) })),
|
|
733
|
+
error: toSource(run.error),
|
|
613
734
|
});
|
|
614
735
|
}
|
|
615
736
|
catch (e) {
|
|
@@ -618,6 +739,8 @@ function registerTools(tools, service, onceLedgerReadable) {
|
|
|
618
739
|
},
|
|
619
740
|
});
|
|
620
741
|
tools.register({
|
|
742
|
+
// 只看不改:运行的出错原因带会话 id(agent 节点的几句都写目标会话,私聊会话的 id 带对方账号),与 scheduler_list_jobs 同档
|
|
743
|
+
visibility: 'restricted',
|
|
621
744
|
groups: ['workflow'],
|
|
622
745
|
definition: {
|
|
623
746
|
type: 'function',
|
|
@@ -643,14 +766,14 @@ function registerTools(tools, service, onceLedgerReadable) {
|
|
|
643
766
|
status: r.status,
|
|
644
767
|
triggerSource: r.triggerSource,
|
|
645
768
|
startedAt: new Date(r.startedAt).toISOString(),
|
|
646
|
-
durationMs: (r
|
|
647
|
-
error: r.error,
|
|
769
|
+
durationMs: elapsed(r),
|
|
770
|
+
error: toSource(r.error),
|
|
648
771
|
})),
|
|
649
772
|
});
|
|
650
773
|
},
|
|
651
774
|
});
|
|
652
775
|
tools.register({
|
|
653
|
-
risk: '
|
|
776
|
+
risk: 'dangerous',
|
|
654
777
|
groups: ['workflow'],
|
|
655
778
|
definition: {
|
|
656
779
|
type: 'function',
|