dsh-auto-flow 0.1.1 → 0.1.2

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/index.js CHANGED
@@ -1,79 +1,2170 @@
1
+ import "./types.js";
2
+ import * as path from "node:path";
3
+ import { join } from "node:path";
1
4
  import { Service } from "@deepseek-ai/cordis";
2
- import { HarnessError } from "@deepseek-ai/dsh-llm";
3
- import { defineTool } from "@deepseek-ai/dsh-tools";
4
5
  import { Remote, RemoteError, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
6
+ import { HarnessError, createUserMessage } from "@deepseek-ai/dsh-llm";
5
7
  import z from "@deepseek-ai/schemastery";
6
8
  import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
7
9
  import { z as z$1 } from "zod";
8
- //#region src/domain.ts
10
+ import { appendFile, mkdir, open, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
11
+ import mime from "mime";
12
+ import { deadline, timeoutOf } from "@deepseek-ai/dsh-timeout";
13
+ import { deepEqualJson } from "@deepseek-ai/dsh-util-values";
14
+ import { promises } from "node:fs";
15
+ import { defineTool } from "@deepseek-ai/dsh-tools";
16
+ import { Buffer as Buffer$1 } from "node:buffer";
17
+ //#region src/host/config.ts
9
18
  /**
10
- * dsh-auto-flow 的持久化领域(dsh-storage domain)声明。
11
- *
12
- * 插件的持久化状态一律进 **domain storage**(这里),与 session 日志完全隔离
13
- * (`dsh-storage-domain` 只存非会话数据、不追加会话事件)。不要把机器执行信息 /
14
- * 运行态写成会话里的 durable 节点——往会话日志里塞非会话数据,会导致该会话在
15
- * dsh 重启后无法正常打开。
16
- *
17
- * 领域用 `defineDomain` 声明一次(表 schema 用 zod,`z.infer` 派生类型),
18
- * 由 `ctx.storageDomain.open(spec)` 打开,返回的 `Domain` 提供同步读 + 串行化落盘写。
19
- *
20
- * 高级选项(DomainSpec)与适用场景:
21
- * - layout: 'single'(默认)整单元一文档;'per-record' 按记录分文档,适合大/稀疏/可
22
- * 独立丢弃的记录(如投影缓存),并逐记录做版本隔离(过期记录文档被丢弃而非迁移)。
23
- * - compatibleVersions: 列旧版本号,声明当前 record schema 仍能接受旧记录(把旧字段
24
- * 声明为可选)。升级 version 后写这里。
25
- * - invalidRecords: 缺省(打开遇坏记录直接拒绝)——权威数据用这个;'backup-and-skip'
26
- * 把坏记录移到备份、打开继续——可丢弃的派生数据用这个。全局槽始终拒绝。
27
- * - global: 全局单例槽(schema + 未写前 initial),见下方示例。
19
+ * dsh-auto-flow 的插件配置:Config 接口、schema 与校验归一化。
20
+ * ConfigSchema 既是 Loader 读的类静态 Config,也是 settings namespace 的 schema。
21
+ */
22
+ /** 工作流运行产物根目录(每次运行在其下建 run-* 子目录)。 */
23
+ const DEFAULT_WORKSPACE_DIR = "workflow-runs";
24
+ /** 领域错误:extends HarnessError 以便失败类别可机器路由。 */
25
+ var AutoFlowError = class extends HarnessError {
26
+ constructor(message, code) {
27
+ super(message, code);
28
+ }
29
+ };
30
+ /** 在边界处校验并归一化一份配置。 */
31
+ function resolveConfig(config = {}) {
32
+ const workspaceDir = (config.workspaceDir ?? "workflow-runs").trim();
33
+ if (workspaceDir === "") throw new AutoFlowError("auto-flow: workspaceDir 不能是空字符串", "AUTO_FLOW_INVALID_WORKSPACE_DIR");
34
+ const maxAgentDepth = config.maxAgentDepth ?? 3;
35
+ if (!Number.isSafeInteger(maxAgentDepth) || maxAgentDepth < 0) throw new AutoFlowError("auto-flow: maxAgentDepth 必须是非负安全整数", "AUTO_FLOW_INVALID_MAX_AGENT_DEPTH");
36
+ const runLogRetentionDays = config.runLogRetentionDays ?? 14;
37
+ if (!Number.isSafeInteger(runLogRetentionDays) || runLogRetentionDays < 1) throw new AutoFlowError("auto-flow: runLogRetentionDays 必须是正整数", "AUTO_FLOW_INVALID_RUN_LOG_RETENTION");
38
+ return {
39
+ workspaceDir,
40
+ maxAgentDepth,
41
+ runLogRetentionDays
42
+ };
43
+ }
44
+ /** 配置 schema:Loader 校验的类静态 Config,也是 settings namespace 的 schema。 */
45
+ const ConfigSchema = z.object({
46
+ workspaceDir: z.string().default(DEFAULT_WORKSPACE_DIR),
47
+ maxAgentDepth: z.number().default(3),
48
+ runLogRetentionDays: z.number().default(14)
49
+ });
50
+ //#endregion
51
+ //#region src/host/domain.ts
52
+ /**
53
+ * dsh-auto-flow 的持久化领域声明。
54
+ * 注意:插件持久化只进 domain storage,别往会话日志里塞非会话数据(会导致会话打不开)。
55
+ */
56
+ /** 工作流数据值 schema(与 types.ts 的 FlowDataValue 同形,递归 JSON)。 */
57
+ const FlowDataValueSchema = z$1.lazy(() => z$1.union([
58
+ z$1.string(),
59
+ z$1.number(),
60
+ z$1.boolean(),
61
+ z$1.null(),
62
+ z$1.array(FlowDataValueSchema),
63
+ z$1.record(z$1.string(), FlowDataValueSchema)
64
+ ]));
65
+ /** 工作流节点/连线/画布的持久化 schema(与 types.ts 的 JSON 安全载荷同形)。 */
66
+ const FlowNodeSchema = z$1.object({
67
+ id: z$1.string(),
68
+ type: z$1.string(),
69
+ position: z$1.object({
70
+ x: z$1.number(),
71
+ y: z$1.number()
72
+ }),
73
+ data: z$1.record(z$1.string(), FlowDataValueSchema),
74
+ style: z$1.record(z$1.string(), FlowDataValueSchema).optional()
75
+ });
76
+ const FlowEdgeSchema = z$1.object({
77
+ id: z$1.string(),
78
+ source: z$1.string(),
79
+ target: z$1.string(),
80
+ type: z$1.string().optional(),
81
+ animated: z$1.boolean().optional(),
82
+ data: z$1.record(z$1.string(), FlowDataValueSchema).optional()
83
+ });
84
+ /**
85
+ * flows 表的持久化 schema(向后兼容):旧版记录只有 `{ nodes, edges }`,
86
+ * 元数据字段(formatVersion / name / description)可缺省——读取时由
87
+ * {@link decodeWorkflowDefinition} 用 id 兜底归一化,旧存档无需迁移即可继续工作。
28
88
  */
29
- /** 领域名:UNIT_NAME_RE /^[a-z][a-z0-9_]*$/,由脚手架按包名推导(dsh_auto_flow)。 */
30
- const GreetingDomain = defineDomain({
89
+ const StoredWorkflowSchema = z$1.object({
90
+ formatVersion: z$1.number().int().positive().optional(),
91
+ name: z$1.string().optional(),
92
+ description: z$1.string().optional(),
93
+ nodes: z$1.array(FlowNodeSchema),
94
+ edges: z$1.array(FlowEdgeSchema)
95
+ });
96
+ /** 把 flows 表的一条原始记录归一化为当前文档形态(旧版缺省元数据时以 id 兜底)。 */
97
+ function decodeWorkflowDefinition(id, stored) {
98
+ return {
99
+ formatVersion: stored.formatVersion ?? 1,
100
+ name: stored.name ?? id,
101
+ description: stored.description ?? "",
102
+ nodes: stored.nodes,
103
+ edges: stored.edges
104
+ };
105
+ }
106
+ /** 领域名(UNIT_NAME_RE /^[a-z][a-z0-9_]*$/),由脚手架按包名推导。
107
+ * 只持有长期资产(工作流定义);运行记录是高频即弃的审计数据,走插件自持的
108
+ * 追加式按天 JSONL 运行日志(host/run-store.ts),不进 domain storage。 */
109
+ const AutoFlowDomain = defineDomain({
31
110
  name: "dsh_auto_flow",
32
111
  version: 1,
33
112
  layout: "single",
34
- global: {
35
- schema: z$1.object({ lastGreetedAt: z$1.number().nullable() }),
36
- initial: { lastGreetedAt: null }
37
- },
38
- tables: { greetings: domainTable(z$1.object({
39
- id: z$1.number(),
40
- text: z$1.string(),
41
- createdAt: z$1.number()
42
- })) }
113
+ tables: { flows: domainTable(StoredWorkflowSchema) }
43
114
  });
44
115
  //#endregion
45
- //#region src/invariant.ts
46
- /**
47
- * 断言助手:失败抛 HarnessError(稳定机器可路由码 INVARIANT)。
48
- * 官方参考:@deepseek-ai/dsh-goal 的 ./invariant 出口。
49
- */
50
- /**
51
- * 断言 condition 为真,否则抛 INVARIANT 错误(route on code,勿解析 message)。
52
- * @param condition - 期望为真的条件。
53
- * @param message - 失败时的人类可读消息。
54
- */
55
- function invariant(condition, message) {
56
- if (!condition) throw new HarnessError(message, "INVARIANT");
116
+ //#region src/host/command-line.ts
117
+ /** 按空白分割命令行,但双引号包裹的段整体保留(支持 `task="hello world"` 这类含空格值,
118
+ * 也支持 `key="a b"` 形态;引号本身不进入标记)。
119
+ * 用状态机代替正则:`\S+` 会把 `task="hello` 里的引号一并吞掉,`=` 前缀后的引号段
120
+ * 无法用正则交替干净捕获。
121
+ * @param line - 命令行文本。
122
+ * @returns 分割后的标记;未闭合引号按剩余内容宽容处理。
123
+ */
124
+ function splitWorkflowLine(line) {
125
+ const tokens = [];
126
+ let current = "";
127
+ let inQuote = false;
128
+ for (const ch of line) {
129
+ if (ch === "\"") {
130
+ inQuote = !inQuote;
131
+ continue;
132
+ }
133
+ if (ch === " " || ch === " ") {
134
+ if (inQuote) current += ch;
135
+ else if (current !== "") {
136
+ tokens.push(current);
137
+ current = "";
138
+ }
139
+ continue;
140
+ }
141
+ current += ch;
142
+ }
143
+ if (current !== "") tokens.push(current);
144
+ return tokens;
145
+ }
146
+ /** 解析 `/workflow` 命令名之后的文本。
147
+ * @param rawInput - 命令名之后的完整文本(含分隔空白)。
148
+ * @returns 已解析的命令行;运行输入保持字符串,允许空值。
149
+ */
150
+ function parseWorkflowCommandLine(rawInput) {
151
+ const [target, ...rest] = splitWorkflowLine(rawInput);
152
+ if (target === void 0) return { kind: "list" };
153
+ const args = {};
154
+ for (const token of rest) {
155
+ const eq = token.indexOf("=");
156
+ if (eq <= 0) return {
157
+ kind: "error",
158
+ error: `无法解析参数「${token}」:应为 key=value 形式`
159
+ };
160
+ args[token.slice(0, eq)] = token.slice(eq + 1);
161
+ }
162
+ return {
163
+ kind: "run",
164
+ target,
165
+ args
166
+ };
167
+ }
168
+ /** 把 `/workflow` 的目标词解析为工作流 id:先精确 id,再唯一精确名称匹配。 */
169
+ function resolveWorkflowTarget(flows, target) {
170
+ if (flows.some((f) => f.id === target)) return {
171
+ ok: true,
172
+ id: target
173
+ };
174
+ const byName = flows.filter((f) => f.name === target);
175
+ if (byName.length === 1) return {
176
+ ok: true,
177
+ id: byName[0].id
178
+ };
179
+ if (byName.length > 1) return {
180
+ ok: false,
181
+ error: `名称「${target}」匹配多个工作流(${byName.map((f) => f.id).join("、")}),请改用 id`
182
+ };
183
+ return {
184
+ ok: false,
185
+ error: `没有名称或 id 为「${target}」的工作流`
186
+ };
187
+ }
188
+ /** 把一次已保存工作流的摘要渲染成列表文本(人类 /workflow 命令用:名称/描述/输入键/节点数,不含 uuid)。 */
189
+ function renderFlowList(flows) {
190
+ return flows.map((f) => {
191
+ const desc = f.description === "" ? "" : `:${f.description}`;
192
+ const inputs = f.inputs.length > 0 ? `(输入:${f.inputs.join("、")})` : "";
193
+ return `- ${f.name}(${f.nodeCount} 个节点)${desc}${inputs}`;
194
+ }).join("\n");
195
+ }
196
+ //#endregion
197
+ //#region src/host/run-store.ts
198
+ /**
199
+ * 运行日志表:插件自持的追加式、按天轮转的运行记录日志(对齐参考项目 dsh-auto-flow)。
200
+ *
201
+ * 运行记录是高频、即弃的审计数据,因此与「定义进 domain storage」不同——运行日志放在
202
+ * `workspaceDir/runs/` 下每天一个 `YYYY-MM-DD.jsonl` 文件,终态时追加一行 JSON(settled
203
+ * 记录只写一次,不做逐节点重写):永不重写历史,写入 O(1);保留期按文件年龄
204
+ * (`retentionDays`)整体删除文件,用完即弃就是字面意义的删文件。读取只读最近文件的
205
+ * 尾部(最多 1MB),单行损坏只丢单行。
206
+ *
207
+ * 运行中的实时状态归宿主服务内存(runRuntimes)权威,本模块只承担终态记录的落盘与
208
+ * 重启后的读取兜底。写操作经一条串行链排队,运行并发结算也不会撕裂行。
209
+ * @module dsh-auto-flow/run-store
210
+ */
211
+ /** 日志文件名:`YYYY-MM-DD.jsonl`。 */
212
+ const DAY_FILE_PATTERN = /^\d{4}-\d{2}-\d{2}\.jsonl$/u;
213
+ /** 尾部读取上限:最近文件只读末尾这么多字节就足以覆盖近期记录。 */
214
+ const MAX_TAIL_BYTES = 1048576;
215
+ /** 一毫秒的日换算系数(保留期计算用)。 */
216
+ const MS_PER_DAY = 864e5;
217
+ /** 把一个时间戳格式化为日志文件名中的日期戳。 */
218
+ function dayStamp(time) {
219
+ const date = new Date(time);
220
+ const pad = (value) => String(value).padStart(2, "0");
221
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
222
+ }
223
+ /** 插件自持的运行日志:追加写入、尾部读取、按天保留。 */
224
+ var WorkflowRunStore = class {
225
+ dir;
226
+ retentionDays;
227
+ logger;
228
+ /** 写链尾:所有追加串行化;链自身的拒绝被吞掉,一次写失败不毒化后续写入。 */
229
+ writeChain = Promise.resolve();
230
+ /** 上次执行过期清理的日期戳;跨天时清理一次过期文件。 */
231
+ lastPruneDay;
232
+ constructor(dir, retentionDays, logger) {
233
+ this.dir = dir;
234
+ this.retentionDays = retentionDays;
235
+ this.logger = logger;
236
+ }
237
+ /** 追加一条运行记录(当日文件、单行 JSON、串行写)。 */
238
+ append(record) {
239
+ const next = this.writeChain.then(() => this.appendNow(record));
240
+ this.writeChain = next.then(() => void 0, () => void 0);
241
+ return next;
242
+ }
243
+ /** 读取单条运行记录;找不到时返回 undefined(按天文件从新到旧扫描尾部)。 */
244
+ async read(runId) {
245
+ for (const name of await this.dayFiles()) for (const record of this.parseLines(await this.tailRead(name))) if (record.runId === runId) return record;
246
+ }
247
+ /** 按条件列出运行记录,整体按 updatedAt 倒序(新文件优先 + 文件内最新在前),
248
+ * 使「最近 N 条」语义(limit 截断)取到真正的新记录而非最旧记录。 */
249
+ async list(filter = {}) {
250
+ const runs = [];
251
+ const limit = filter.limit ?? 200;
252
+ for (const name of await this.dayFiles()) {
253
+ const lines = [...this.parseLines(await this.tailRead(name))].reverse();
254
+ for (const record of lines) {
255
+ if (filter.sessionId !== void 0 && record.sessionId !== filter.sessionId) continue;
256
+ if (filter.workflowId !== void 0 && record.workflowId !== filter.workflowId) continue;
257
+ runs.push(record);
258
+ if (runs.length >= limit) return runs;
259
+ }
260
+ }
261
+ return runs;
262
+ }
263
+ /** 串行链上的一次追加:确保目录、追加当日文件、跨天时清理过期文件。 */
264
+ async appendNow(record) {
265
+ await mkdir(this.dir, { recursive: true });
266
+ const day = dayStamp(Date.now());
267
+ await appendFile(this.dayPath(day), `${JSON.stringify(record)}\n`, "utf8");
268
+ if (this.lastPruneDay !== day) {
269
+ this.lastPruneDay = day;
270
+ await this.pruneExpired(day);
271
+ }
272
+ }
273
+ /** 删除早于保留期的日志文件(按文件名日期比较;当天文件永不清除)。 */
274
+ async pruneExpired(today) {
275
+ const cutoff = dayStamp(Date.now() - this.retentionDays * MS_PER_DAY);
276
+ for (const name of await this.dayFiles()) {
277
+ if (name >= cutoff || name === today) continue;
278
+ await rm(this.dayPath(name), { force: true });
279
+ }
280
+ }
281
+ /** 列出全部日志文件的日期戳(`YYYY-MM-DD`),新到旧。目录不存在时为空。 */
282
+ async dayFiles() {
283
+ let entries;
284
+ try {
285
+ entries = await readdir(this.dir);
286
+ } catch {
287
+ return [];
288
+ }
289
+ return entries.filter((name) => DAY_FILE_PATTERN.test(name)).map((name) => name.slice(0, -6)).sort().reverse();
290
+ }
291
+ dayPath(day) {
292
+ return join(this.dir, `${day}.jsonl`);
293
+ }
294
+ /** 读取一个日志文件的末尾(至多 1MB);文件不存在时返回空行。 */
295
+ async tailRead(day) {
296
+ const path = this.dayPath(day);
297
+ let size;
298
+ try {
299
+ size = (await stat(path)).size;
300
+ } catch {
301
+ return [];
302
+ }
303
+ const handle = await open(path, "r");
304
+ try {
305
+ const start = Math.max(0, size - MAX_TAIL_BYTES);
306
+ const buffer = Buffer.alloc(size - start);
307
+ await handle.read(buffer, 0, buffer.length, start);
308
+ const lines = buffer.toString("utf8").split("\n");
309
+ return start > 0 ? lines.slice(1) : lines;
310
+ } finally {
311
+ await handle.close();
312
+ }
313
+ }
314
+ /** 逐行解析运行记录;空行跳过,损坏行只丢单行并留痕(不再完全静默)。 */
315
+ *parseLines(lines) {
316
+ for (const line of lines) {
317
+ if (line.length === 0) continue;
318
+ try {
319
+ const record = JSON.parse(line);
320
+ if (typeof record.runId !== "string" || typeof record.status !== "string") continue;
321
+ yield record;
322
+ } catch (error) {
323
+ this.logger?.warn(`auto-flow: dropped damaged run-log line: ${error instanceof Error ? error.message : String(error)}`);
324
+ }
325
+ }
326
+ }
327
+ };
328
+ //#endregion
329
+ //#region src/workflow/pipeline.ts
330
+ /** 脚本节点的兜底硬超时(毫秒):节点未配 timeout 或填 0 时启用,
331
+ * 防止脚本内部长等待(如元宝图生文动辄数分钟的点击等待)一旦永不满足就把运行永久挂起。 */
332
+ const SCRIPT_NODE_DEFAULT_TIMEOUT_MS = 6e5;
333
+ /** 把任意 JSON 字符串弱化为对象(非法/非对象 → 空对象)。 */
334
+ function parseJsonObject(raw) {
335
+ if (typeof raw !== "string" || raw.trim() === "") return {};
336
+ try {
337
+ const parsed = JSON.parse(raw);
338
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
339
+ } catch {}
340
+ return {};
341
+ }
342
+ /** 把聚合得到的产物文件路径按脚本规则整形。 */
343
+ function shapeFiles(files, spec, payload) {
344
+ if (spec.filesShape === "vidu-imgs") {
345
+ const raw = payload[spec.filesPromptKey ?? "prompt"];
346
+ const prompts = typeof raw === "string" && raw !== "" ? [raw] : Array.isArray(raw) ? raw.filter((v) => typeof v === "string") : [];
347
+ return files.map((imgPath) => ({
348
+ imgPath,
349
+ prompts
350
+ }));
351
+ }
352
+ return files;
353
+ }
354
+ /** 按「内联负载 → 直连表单字段 → 直连脚本输出 → 直连脚本文件」装配 payload(与脚本节点 UI 解耦)。 */
355
+ function buildScriptPayload(script, input, artifacts) {
356
+ const payload = { ...parseJsonObject(input.inline.payload) };
357
+ Object.assign(payload, input.params);
358
+ for (const out of Object.values(input.outputs)) Object.assign(payload, out);
359
+ if (artifacts?.filesKey !== void 0) {
360
+ const files = Object.values(input.files).flat();
361
+ if (files.length > 0) payload[artifacts.filesKey] = shapeFiles(files, artifacts, payload);
362
+ }
363
+ return payload;
364
+ }
365
+ /** 由脚本节点数据派生本次运行选项(browser 覆写 + 硬超时)。 */
366
+ function buildScriptRunOptions(inline) {
367
+ const options = {};
368
+ const headless = inline.headless;
369
+ const channel = inline.browser;
370
+ const browser = {};
371
+ if (typeof headless === "boolean") browser.headless = headless;
372
+ if (typeof channel === "string" && channel !== "") browser.channel = channel;
373
+ if (browser.headless !== void 0 || browser.channel !== void 0) options.browser = browser;
374
+ const timeout = inline.timeout;
375
+ if (typeof timeout === "number" && Number.isFinite(timeout) && timeout > 0) options.timeoutMs = timeout;
376
+ else options.timeoutMs = SCRIPT_NODE_DEFAULT_TIMEOUT_MS;
377
+ return options;
378
+ }
379
+ const FORM_NODE_TYPE = "form";
380
+ /** 值传递节点(确定性内容流水线,来自参考项目 my-dsh-plugin 的节点族)。 */
381
+ const PROMPT_NODE_TYPE = "prompt";
382
+ const VISION_NODE_TYPE = "vision";
383
+ const COMMAND_NODE_TYPE = "command";
384
+ const HTTP_NODE_TYPE = "http";
385
+ const CONDITION_NODE_TYPE = "condition";
386
+ const OUTPUT_NODE_TYPE = "output";
387
+ /** AI 智能体节点(硬→软):把子任务委托给一个子代理(LLM + 工具循环),结果回灌值链。 */
388
+ const AGENT_NODE_TYPE = "agent";
389
+ /** 调用工作流节点(硬→硬复用):运行另一个已保存工作流并回灌其 Output 值。 */
390
+ const WORKFLOW_NODE_TYPE = "workflow";
391
+ /** 条件节点支持的运算符(安全白名单,不做任意 JS 求值)。 */
392
+ const CONDITION_OPERATORS = [
393
+ "equals",
394
+ "notEquals",
395
+ "contains",
396
+ "exists",
397
+ "truthy"
398
+ ];
399
+ /** 读取节点的实际类型:优先 data.nodeType,回退 React Flow 的 type。 */
400
+ function nodeTypeOf(node) {
401
+ return typeof node.data.nodeType === "string" ? node.data.nodeType : node.type;
402
+ }
403
+ /** 表单节点 data 去掉 nodeType / fields 元键后的字段值对象。 */
404
+ function formFieldsOf(data) {
405
+ const out = {};
406
+ for (const [key, value] of Object.entries(data)) {
407
+ if (key === "nodeType" || key === "fields") continue;
408
+ out[key] = value;
409
+ }
410
+ return out;
411
+ }
412
+ /**
413
+ * 沿入边向上游遍历(穿越审批节点——审批是数据通道),对遇到的表单节点 / 脚本节点各回调一次。
414
+ * 静态校验(missingRequiredParams)与运行时聚合(engine.collectInputs)共用,避免两处各写一遍遍历。
415
+ */
416
+ function walkUpstream(flow, nodeId, nodesById, onForm, onScript) {
417
+ const visited = /* @__PURE__ */ new Set([nodeId]);
418
+ const stack = [nodeId];
419
+ while (stack.length > 0) {
420
+ const current = stack.pop();
421
+ for (const edge of flow.edges) {
422
+ if (edge.target !== current) continue;
423
+ const source = nodesById.get(edge.source);
424
+ if (source === void 0 || visited.has(source.id)) continue;
425
+ const type = nodeTypeOf(source);
426
+ if (type === "form") {
427
+ visited.add(source.id);
428
+ onForm(source);
429
+ } else if (type === "pw-script") {
430
+ visited.add(source.id);
431
+ onScript(source);
432
+ } else if (type === "approval") {
433
+ visited.add(source.id);
434
+ stack.push(source.id);
435
+ }
436
+ }
437
+ }
438
+ }
439
+ /** 节点展示名(脚本节点按 data.script + 脚本展示名定位)。 */
440
+ function nodeLabel(nodeType, script, label) {
441
+ if (nodeType === "form") return "表单参数";
442
+ if (nodeType === "approval") return "人工审批";
443
+ if (nodeType === "pw-script") {
444
+ if (script !== void 0 && script !== "") return label ?? script;
445
+ return "运行脚本";
446
+ }
447
+ if (nodeType === "prompt") return "文本模板";
448
+ if (nodeType === "llm") return "AI 模型";
449
+ if (nodeType === "agent") return "AI 智能体";
450
+ if (nodeType === "workflow") return "调用工作流";
451
+ if (nodeType === "vision") return "图片识别";
452
+ if (nodeType === "command") return "执行命令";
453
+ if (nodeType === "http") return "HTTP 请求";
454
+ if (nodeType === "condition") return "条件分支";
455
+ if (nodeType === "output") return "输出";
456
+ return nodeType;
457
+ }
458
+ const TEMPLATE_REFERENCE_PATTERN = /{{\s*([^{}]+?)\s*}}/g;
459
+ /** 从模板字符串提取全部变量引用(按出现顺序,去首尾空白)。 */
460
+ function templateReferences(template) {
461
+ return [...template.matchAll(TEMPLATE_REFERENCE_PATTERN)].map((match) => match[1].trim());
462
+ }
463
+ /** 把任意 JSON 值弱化为字符串(字符串原样,其余 JSON 序列化)。 */
464
+ function stringify(value) {
465
+ return typeof value === "string" ? value : JSON.stringify(value) ?? "";
466
+ }
467
+ /**
468
+ * 读取一条点分变量路径:`current`、`input`、`input.*` 或 `steps.<nodeId>[.*]`。
469
+ * 任一段缺失返回 undefined(模板插值时降级为空串)。
470
+ */
471
+ function readPath(path, input, steps, current) {
472
+ if (path === "current") return current;
473
+ if (path === "input") return input;
474
+ const parts = path.split(".");
475
+ let value;
476
+ if (parts[0] === "input") value = input;
477
+ else if (parts[0] === "steps") value = steps[parts[1] ?? ""];
478
+ else return void 0;
479
+ for (const part of parts.slice(parts[0] === "steps" ? 2 : 1)) {
480
+ if (value === null || value === void 0 || typeof value !== "object" || Array.isArray(value)) return void 0;
481
+ value = Object.prototype.hasOwnProperty.call(value, part) ? value[part] : void 0;
482
+ }
483
+ return value;
484
+ }
485
+ /** 解析一个模板:精确引用(整串是 {{x}})保留 JSON 类型,插值处一律转字符串。 */
486
+ function resolveValue(source, input, steps, current) {
487
+ const exact = source.match(/^{{\s*([^{}]+?)\s*}}$/);
488
+ if (exact !== null) return readPath(exact[1].trim(), input, steps, current);
489
+ return renderText(source, input, steps, current);
490
+ }
491
+ /** 把模板渲染为文本:所有 `{{...}}` 引用以字符串展开。 */
492
+ function renderText(source, input, steps, current) {
493
+ return source.replace(TEMPLATE_REFERENCE_PATTERN, (_match, path) => stringify(readPath(path.trim(), input, steps, current) ?? ""));
494
+ }
495
+ /** 构造直接 LLM 节点的提示词:含模板则原样渲染,否则在末尾追加当前输入。 */
496
+ function promptFor(source, input, steps, current) {
497
+ return TEMPLATE_REFERENCE_PATTERN.test(source) ? renderText(source, input, steps, current) : `${source}\n\nInput:\n${stringify(current)}`;
498
+ }
499
+ /** 按边做拓扑排序(Kahn),返回节点 id 顺序;无依赖/孤立节点按出现顺序追加。 */
500
+ function topoSortNodeIds(flow) {
501
+ const nodes = flow.nodes;
502
+ const indegree = new Map(nodes.map((n) => [n.id, 0]));
503
+ const out = new Map(nodes.map((n) => [n.id, []]));
504
+ for (const edge of flow.edges) {
505
+ if (!indegree.has(edge.source) || !indegree.has(edge.target)) continue;
506
+ out.get(edge.source).push(edge.target);
507
+ indegree.set(edge.target, (indegree.get(edge.target) ?? 0) + 1);
508
+ }
509
+ const queue = [];
510
+ for (const [id, degree] of indegree) if (degree === 0) queue.push(id);
511
+ queue.sort((a, b) => nodes.findIndex((n) => n.id === a) - nodes.findIndex((n) => n.id === b));
512
+ const result = [];
513
+ while (queue.length > 0) {
514
+ const id = queue.shift();
515
+ result.push(id);
516
+ for (const next of out.get(id) ?? []) {
517
+ const degree = (indegree.get(next) ?? 0) - 1;
518
+ indegree.set(next, degree);
519
+ if (degree === 0) queue.push(next);
520
+ }
521
+ queue.sort((a, b) => nodes.findIndex((n) => n.id === a) - nodes.findIndex((n) => n.id === b));
522
+ }
523
+ const seen = new Set(result);
524
+ for (const node of nodes) if (!seen.has(node.id)) result.push(node.id);
525
+ return result;
526
+ }
527
+ /**
528
+ * 检测工作流画布中是否存在环(DFS 三色 + 回溯路径)。
529
+ * 返回构成环的节点 id 列表(首尾同 id,便于展示);无环返回 null。
530
+ */
531
+ function detectCycle(flow) {
532
+ const adj = new Map(flow.nodes.map((n) => [n.id, []]));
533
+ const nodeIds = new Set(flow.nodes.map((n) => n.id));
534
+ for (const edge of flow.edges) if (nodeIds.has(edge.source) && nodeIds.has(edge.target)) adj.get(edge.source).push(edge.target);
535
+ const WHITE = 0;
536
+ const GRAY = 1;
537
+ const BLACK = 2;
538
+ const color = new Map(flow.nodes.map((n) => [n.id, WHITE]));
539
+ const stack = [];
540
+ const dfs = (u) => {
541
+ color.set(u, GRAY);
542
+ stack.push(u);
543
+ for (const v of adj.get(u) ?? []) if (color.get(v) === WHITE) {
544
+ const cycle = dfs(v);
545
+ if (cycle !== null) return cycle;
546
+ } else if (color.get(v) === GRAY) {
547
+ const start = stack.indexOf(v);
548
+ return [...stack.slice(start), v];
549
+ }
550
+ stack.pop();
551
+ color.set(u, BLACK);
552
+ return null;
553
+ };
554
+ for (const node of flow.nodes) if (color.get(node.id) === WHITE) {
555
+ const cycle = dfs(node.id);
556
+ if (cycle !== null) return cycle;
557
+ }
558
+ return null;
559
+ }
560
+ /** 由画布构建运行步骤列表(拓扑序;每个节点一步);resolveScriptLabel 提供脚本展示名。 */
561
+ function buildRunSteps(flow, resolveScriptLabel) {
562
+ const nodesById = new Map(flow.nodes.map((n) => [n.id, n]));
563
+ return topoSortNodeIds(flow).map((nodeId) => {
564
+ const node = nodesById.get(nodeId);
565
+ const nodeType = node !== void 0 ? nodeTypeOf(node) : "__unknown__";
566
+ const script = nodeType === "pw-script" && node !== void 0 && typeof node.data.script === "string" ? node.data.script : void 0;
567
+ const label = script !== void 0 ? resolveScriptLabel?.(script) : void 0;
568
+ return {
569
+ nodeId,
570
+ nodeType,
571
+ ...script !== void 0 ? { script } : {},
572
+ label: nodeLabel(nodeType, script, label),
573
+ status: "pending"
574
+ };
575
+ });
576
+ }
577
+ /** 值是否「空」(视为未填)。 */
578
+ function isEmptyValue(value) {
579
+ return value === void 0 || value === null || value === "";
580
+ }
581
+ /**
582
+ * 运行前静态校验:找出「必填参数在当前画布里确定无法满足」的脚本节点。
583
+ * 只做确定性的静态判断,绝不误报:
584
+ * - 内联 payload JSON 提供该 key 且非空 → 满足;
585
+ * - 直连(穿越审批)表单提供该 key 且非空 → 满足;
586
+ * - 存在直连(穿越审批)上游脚本 → 该节点的必填参数整体视为「运行时再校验」,跳过;
587
+ * - 其余视为缺失。
588
+ */
589
+ function missingRequiredParams(flow, paramsByScript) {
590
+ const nodesById = new Map(flow.nodes.map((n) => [n.id, n]));
591
+ const missing = [];
592
+ for (const node of flow.nodes) {
593
+ if (nodeTypeOf(node) !== "pw-script") continue;
594
+ const script = typeof node.data.script === "string" ? node.data.script : "";
595
+ if (script === "") continue;
596
+ const params = paramsByScript.get(script);
597
+ if (params === void 0) continue;
598
+ const upstream = staticUpstream(flow, node.id, nodesById);
599
+ if (upstream.hasScript) continue;
600
+ const inline = parseJsonObject(node.data.payload);
601
+ for (const param of params) {
602
+ if (param.required !== true) continue;
603
+ if (!isEmptyValue(inline[param.key]) || !isEmptyValue(upstream.formValues[param.key])) continue;
604
+ missing.push({
605
+ nodeId: node.id,
606
+ script,
607
+ param: param.key
608
+ });
609
+ }
610
+ }
611
+ return missing;
612
+ }
613
+ /** 收集某脚本节点的直连上游(穿越审批节点):表单字段值 + 是否存在上游脚本。 */
614
+ function staticUpstream(flow, nodeId, nodesById) {
615
+ const formValues = {};
616
+ let hasScript = false;
617
+ walkUpstream(flow, nodeId, nodesById, (source) => Object.assign(formValues, formFieldsOf(source.data)), () => {
618
+ hasScript = true;
619
+ });
620
+ return {
621
+ formValues,
622
+ hasScript
623
+ };
624
+ }
625
+ //#endregion
626
+ //#region src/workflow/validation.ts
627
+ /** 值节点里参与 {{...}} 模板的字段(用于引用校验)。 */
628
+ const VALUE_TEMPLATE_KEYS = {
629
+ [PROMPT_NODE_TYPE]: ["template"],
630
+ ["llm"]: ["prompt"],
631
+ [AGENT_NODE_TYPE]: ["task"],
632
+ [VISION_NODE_TYPE]: ["image", "instruction"],
633
+ [COMMAND_NODE_TYPE]: ["command"],
634
+ [HTTP_NODE_TYPE]: [
635
+ "url",
636
+ "headers",
637
+ "body"
638
+ ],
639
+ [CONDITION_NODE_TYPE]: ["left", "right"],
640
+ [OUTPUT_NODE_TYPE]: ["value"],
641
+ [WORKFLOW_NODE_TYPE]: ["args"]
642
+ };
643
+ /** 运行前结构化校验:返回结构化诊断(不抛错)。 */
644
+ function validateFlow(flow, paramsByScript = /* @__PURE__ */ new Map()) {
645
+ const diagnostics = [];
646
+ const nodesById = new Map(flow.nodes.map((n) => [n.id, n]));
647
+ const cycle = detectCycle(flow);
648
+ if (cycle !== null) diagnostics.push({
649
+ level: "error",
650
+ code: "cycle",
651
+ message: `工作流存在环:${cycle.join(" → ")}`
652
+ });
653
+ const nodeIds = /* @__PURE__ */ new Set();
654
+ for (const node of flow.nodes) if (nodeIds.has(node.id)) diagnostics.push({
655
+ level: "error",
656
+ code: "duplicate-node-id",
657
+ message: `节点 ID 重复:${node.id}`,
658
+ nodeId: node.id
659
+ });
660
+ else nodeIds.add(node.id);
661
+ const edgeIds = /* @__PURE__ */ new Set();
662
+ for (const edge of flow.edges) {
663
+ if (edgeIds.has(edge.id)) diagnostics.push({
664
+ level: "error",
665
+ code: "duplicate-edge-id",
666
+ message: `连线 ID 重复:${edge.id}`,
667
+ edgeId: edge.id
668
+ });
669
+ else edgeIds.add(edge.id);
670
+ if (edge.source === edge.target) diagnostics.push({
671
+ level: "error",
672
+ code: "self-edge",
673
+ message: "节点不能连接到自身",
674
+ edgeId: edge.id
675
+ });
676
+ }
677
+ for (const edge of flow.edges) {
678
+ const source = nodesById.get(edge.source);
679
+ if (source === void 0) continue;
680
+ if ((typeof source.data.nodeType === "string" ? source.data.nodeType : source.type) !== "condition") continue;
681
+ if (edge.sourceHandle !== "true" && edge.sourceHandle !== "false") diagnostics.push({
682
+ level: "error",
683
+ code: "condition-edge-handle",
684
+ message: "条件分支连线必须从 true 或 false 端口发出",
685
+ edgeId: edge.id
686
+ });
687
+ }
688
+ for (const node of flow.nodes) {
689
+ if ((typeof node.data.nodeType === "string" ? node.data.nodeType : node.type) !== "pw-script") continue;
690
+ if (typeof node.data.script !== "string" || node.data.script === "") diagnostics.push({
691
+ level: "error",
692
+ code: "blank-script",
693
+ message: "运行脚本节点未选择脚本",
694
+ nodeId: node.id
695
+ });
696
+ }
697
+ for (const node of flow.nodes) {
698
+ if ((typeof node.data.nodeType === "string" ? node.data.nodeType : node.type) !== "agent") continue;
699
+ if (typeof node.data.task !== "string" || node.data.task.trim() === "") diagnostics.push({
700
+ level: "error",
701
+ code: "blank-task",
702
+ message: "AI 智能体节点未填写任务",
703
+ nodeId: node.id
704
+ });
705
+ }
706
+ for (const missing of missingRequiredParams(flow, paramsByScript)) diagnostics.push({
707
+ level: "error",
708
+ code: "missing-required-param",
709
+ message: `${missing.script} 缺少必填参数 ${missing.param}`,
710
+ nodeId: missing.nodeId
711
+ });
712
+ for (const node of flow.nodes) {
713
+ const type = typeof node.data.nodeType === "string" ? node.data.nodeType : node.type;
714
+ const blank = (key) => typeof node.data[key] !== "string" || node.data[key].trim() === "";
715
+ if (type === "prompt" && blank("template")) diagnostics.push({
716
+ level: "error",
717
+ code: "blank-prompt-template",
718
+ message: "文本模板节点的模板不能为空",
719
+ nodeId: node.id
720
+ });
721
+ else if (type === "llm" && blank("prompt")) diagnostics.push({
722
+ level: "error",
723
+ code: "blank-llm-prompt",
724
+ message: "AI 模型节点的提示词不能为空",
725
+ nodeId: node.id
726
+ });
727
+ else if (type === "vision") {
728
+ if (node.data.skipWhenNoImage !== true && blank("image")) diagnostics.push({
729
+ level: "error",
730
+ code: "blank-vision-image",
731
+ message: "图片识别节点的图片路径不能为空",
732
+ nodeId: node.id
733
+ });
734
+ if (blank("instruction")) diagnostics.push({
735
+ level: "error",
736
+ code: "blank-vision-instruction",
737
+ message: "图片识别节点的识别指令不能为空",
738
+ nodeId: node.id
739
+ });
740
+ if (blank("provider")) diagnostics.push({
741
+ level: "error",
742
+ code: "blank-vision-provider",
743
+ message: "图片识别节点的模型路由不能为空",
744
+ nodeId: node.id
745
+ });
746
+ if (blank("model")) diagnostics.push({
747
+ level: "error",
748
+ code: "blank-vision-model",
749
+ message: "图片识别节点的模型不能为空",
750
+ nodeId: node.id
751
+ });
752
+ } else if (type === "command" && blank("command")) diagnostics.push({
753
+ level: "error",
754
+ code: "blank-command-line",
755
+ message: "执行命令节点的命令不能为空",
756
+ nodeId: node.id
757
+ });
758
+ else if (type === "http" && blank("url")) diagnostics.push({
759
+ level: "error",
760
+ code: "blank-http-url",
761
+ message: "HTTP 请求节点的地址不能为空",
762
+ nodeId: node.id
763
+ });
764
+ else if (type === "condition" && blank("left")) diagnostics.push({
765
+ level: "error",
766
+ code: "blank-condition-left",
767
+ message: "条件分支节点的左值不能为空",
768
+ nodeId: node.id
769
+ });
770
+ else if (type === "output" && blank("value")) diagnostics.push({
771
+ level: "error",
772
+ code: "blank-output-value",
773
+ message: "输出节点的返回值不能为空",
774
+ nodeId: node.id
775
+ });
776
+ else if (type === "workflow" && blank("workflowId")) diagnostics.push({
777
+ level: "error",
778
+ code: "blank-workflow-id",
779
+ message: "调用工作流节点未选择目标工作流",
780
+ nodeId: node.id
781
+ });
782
+ }
783
+ for (const node of flow.nodes) {
784
+ const type = typeof node.data.nodeType === "string" ? node.data.nodeType : node.type;
785
+ const keys = VALUE_TEMPLATE_KEYS[type];
786
+ if (keys === void 0) continue;
787
+ for (const key of keys) {
788
+ const raw = node.data[key];
789
+ if (typeof raw !== "string") continue;
790
+ for (const ref of templateReferences(raw)) {
791
+ if (ref === "current" || ref === "input") continue;
792
+ if (ref.startsWith("input.")) continue;
793
+ if (ref.startsWith("steps.")) {
794
+ const target = ref.slice(6).split(".")[0];
795
+ if (!nodesById.has(target)) diagnostics.push({
796
+ level: "error",
797
+ code: "unknown-step-reference",
798
+ message: `变量引用不存在的节点:${target}`,
799
+ nodeId: node.id
800
+ });
801
+ else if (target === node.id) diagnostics.push({
802
+ level: "error",
803
+ code: "self-step-reference",
804
+ message: "节点不能引用自身尚未生成的输出",
805
+ nodeId: node.id
806
+ });
807
+ continue;
808
+ }
809
+ diagnostics.push({
810
+ level: "error",
811
+ code: "unknown-variable-reference",
812
+ message: `不支持的变量引用:${ref}`,
813
+ nodeId: node.id
814
+ });
815
+ }
816
+ }
817
+ }
818
+ return {
819
+ valid: diagnostics.every((d) => d.level !== "error"),
820
+ diagnostics
821
+ };
822
+ }
823
+ //#endregion
824
+ //#region src/host/workspace.ts
825
+ /**
826
+ * 工作区文件操作(宿主):引擎在运行中重扫脚本产物文件用。
827
+ * 注意:人工增删改查不再走本插件(用户直接在 dsh 工作区目录里用系统资源管理器 /
828
+ * dsh 文件工具操作),本模块只保留引擎聚合下游脚本产物需要的「列出文件」。
829
+ */
830
+ /** 不参与产物聚合的元数据文件。 */
831
+ const SKIP_NAMES = /* @__PURE__ */ new Set(["task.json"]);
832
+ /** 递归列出根目录下所有产物文件(相对路径相对 rootDir;跳过 task.json)。 */
833
+ async function listWorkspaceFiles(rootDir) {
834
+ const files = [];
835
+ async function walk(dir) {
836
+ let entries;
837
+ try {
838
+ entries = await promises.readdir(dir, { withFileTypes: true });
839
+ } catch {
840
+ return;
841
+ }
842
+ for (const entry of entries) {
843
+ const full = path.join(dir, entry.name);
844
+ if (entry.isDirectory()) {
845
+ await walk(full);
846
+ continue;
847
+ }
848
+ if (!entry.isFile() || SKIP_NAMES.has(entry.name)) continue;
849
+ const stat = await promises.stat(full).catch(() => null);
850
+ files.push({
851
+ path: full,
852
+ relativePath: path.relative(rootDir, full),
853
+ name: entry.name,
854
+ size: stat?.size ?? 0
855
+ });
856
+ }
857
+ }
858
+ await walk(rootDir);
859
+ return files.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
860
+ }
861
+ /** HTTP 节点响应体保留上限(字符)。 */
862
+ const HTTP_NODE_BODY_CAP_CHARS = 1e5;
863
+ /** HTTP 节点超时在 dsh-timeout 里携带的稳定错误码。 */
864
+ const HTTP_TIMEOUT_CODE = "AUTO_FLOW_HTTP_TIMEOUT";
865
+ /** 计算一次条件比较(安全白名单运算符);JSON 值用 dsh 生态的 deepEqualJson 做结构比较。 */
866
+ function compare(left, operator, right) {
867
+ if (operator === "equals") return deepEqualJson(left, right);
868
+ if (operator === "notEquals") return !deepEqualJson(left, right);
869
+ if (operator === "exists") return left !== void 0 && left !== null;
870
+ if (operator === "truthy") return Boolean(left);
871
+ if (operator === "contains") {
872
+ if (typeof left === "string") return left.includes(String(right ?? ""));
873
+ if (Array.isArray(left)) return left.some((item) => deepEqualJson(item, right));
874
+ return false;
875
+ }
876
+ return false;
877
+ }
878
+ /** 确定性错误码:重试必失败,不做无谓重试(其余/无码一律视为瞬态、可重试)。 */
879
+ const NON_RETRYABLE_CODES = /* @__PURE__ */ new Set(["SCRIPT_NOT_FOUND", "SCRIPT_PARAM_ERROR"]);
880
+ /** 判断运行是否被外部取消(cancelRun 在服务层设置,引擎内部不做此赋值,故经独立函数读取避免 TS 收窄)。 */
881
+ function isCancelled(state) {
882
+ return state.status === "cancelled";
883
+ }
884
+ var WorkflowEngine = class {
885
+ deps;
886
+ constructor(deps) {
887
+ this.deps = deps;
888
+ }
889
+ /** 运行入口:发出 run-start,执行图,最后按终态发出 run-end(观察者失败不影响运行)。 */
890
+ async run(state, flow) {
891
+ this.emit("auto-flow/run-start", {
892
+ runId: state.runId,
893
+ nodeCount: state.steps.length
894
+ });
895
+ await this.runInner(state, flow);
896
+ const result = runResultOf(state);
897
+ this.emit("auto-flow/run-end", {
898
+ runId: state.runId,
899
+ stopReason: state.status,
900
+ ...result !== void 0 ? { result } : {}
901
+ });
902
+ }
903
+ /** 发出一次生命周期事件;观察者抛错一律吞掉,绝不影响本次运行。 */
904
+ emit(type, payload) {
905
+ try {
906
+ this.deps.emit?.(type, payload);
907
+ } catch {}
908
+ }
909
+ /** 运行是否被取消:显式状态(cancelRun)或运行级信号已中止。 */
910
+ cancelled(state) {
911
+ return isCancelled(state) || this.deps.signal?.aborted === true;
912
+ }
913
+ /** 把运行标记为已取消并持久化(幂等;cancelRun 已置位时跳过)。 */
914
+ markCancelled(state) {
915
+ if (state.status === "cancelled") return;
916
+ state.status = "cancelled";
917
+ this.update(state);
918
+ }
919
+ async runInner(state, flow) {
920
+ const nodesById = new Map(flow.nodes.map((n) => [n.id, n]));
921
+ const ctx = new RunContext(state.workspaceDir);
922
+ const steps = state.steps;
923
+ const n = steps.length;
924
+ const indexOf = new Map(steps.map((s, i) => [s.nodeId, i]));
925
+ const upstream = Array.from({ length: n }, () => []);
926
+ for (const edge of flow.edges) {
927
+ const s = indexOf.get(edge.source);
928
+ const t = indexOf.get(edge.target);
929
+ if (s !== void 0 && t !== void 0 && s !== t) upstream[t].push(s);
930
+ }
931
+ const level = Array.from({ length: n }, () => 0);
932
+ for (let i = 0; i < n; i++) {
933
+ let maxUp = -1;
934
+ for (const s of upstream[i]) maxUp = Math.max(maxUp, level[s]);
935
+ level[i] = maxUp + 1;
936
+ }
937
+ const byLevel = /* @__PURE__ */ new Map();
938
+ for (let i = 0; i < n; i++) {
939
+ const lv = level[i];
940
+ const list = byLevel.get(lv);
941
+ if (list === void 0) byLevel.set(lv, [i]);
942
+ else list.push(i);
943
+ }
944
+ const levels = [...byLevel.keys()].sort((a, b) => a - b);
945
+ const values = {};
946
+ const started = /* @__PURE__ */ new Set();
947
+ const completed = /* @__PURE__ */ new Set();
948
+ const input = collectWorkflowInput(flow);
949
+ /** 一条边是否仍活跃:源节点已开始执行;条件节点只看所选分支(true/false 源句柄)。 */
950
+ const edgeActive = (edge) => {
951
+ if (!started.has(edge.source)) return false;
952
+ const source = nodesById.get(edge.source);
953
+ if (source === void 0) return false;
954
+ if (nodeTypeOf(source) !== "condition") return true;
955
+ return values[edge.source] === (edge.sourceHandle === "true");
956
+ };
957
+ /** 节点的「当前值」= 拓扑序最大的活跃前驱输出;无前驱(入口)时取全局输入。 */
958
+ const primaryValue = (nodeId) => {
959
+ const incoming = flow.edges.filter((e) => e.target === nodeId && edgeActive(e));
960
+ let primary;
961
+ let best = -1;
962
+ for (const e of incoming) {
963
+ const idx = indexOf.get(e.source) ?? -1;
964
+ if (idx > best) {
965
+ best = idx;
966
+ primary = e.source;
967
+ }
968
+ }
969
+ if (primary === void 0) return input;
970
+ return values[primary] ?? null;
971
+ };
972
+ for (const lv of levels) {
973
+ if (this.cancelled(state)) {
974
+ this.markCancelled(state);
975
+ return;
976
+ }
977
+ const batch = byLevel.get(lv);
978
+ const runnable = [];
979
+ for (const i of batch) {
980
+ const step = steps[i];
981
+ if (nodesById.get(step.nodeId) === void 0) {
982
+ step.status = "skipped";
983
+ step.error = "节点不存在";
984
+ continue;
985
+ }
986
+ if (!(upstream[i].length === 0 || flow.edges.some((e) => e.target === step.nodeId && edgeActive(e)))) {
987
+ step.status = "skipped";
988
+ continue;
989
+ }
990
+ runnable.push(i);
991
+ }
992
+ const valueIdx = [];
993
+ const approvalIdx = [];
994
+ const scriptIdx = [];
995
+ for (const i of runnable) {
996
+ const t = nodeTypeOf(nodesById.get(steps[i].nodeId));
997
+ if (t === "approval") approvalIdx.push(i);
998
+ else if (t === "pw-script") scriptIdx.push(i);
999
+ else valueIdx.push(i);
1000
+ }
1001
+ if (valueIdx.length > 0) {
1002
+ for (const i of valueIdx) {
1003
+ const step = steps[i];
1004
+ started.add(step.nodeId);
1005
+ step.status = "running";
1006
+ this.emit("auto-flow/node-start", {
1007
+ runId: state.runId,
1008
+ nodeId: step.nodeId,
1009
+ nodeType: step.nodeType,
1010
+ label: step.label
1011
+ });
1012
+ }
1013
+ this.update(state);
1014
+ const results = await Promise.allSettled(valueIdx.map((i) => {
1015
+ const step = steps[i];
1016
+ const node = nodesById.get(step.nodeId);
1017
+ return this.executeValueNode(nodeTypeOf(node), node, input, values, primaryValue(step.nodeId));
1018
+ }));
1019
+ if (this.cancelled(state)) {
1020
+ this.markCancelled(state);
1021
+ return;
1022
+ }
1023
+ for (let k = 0; k < valueIdx.length; k++) {
1024
+ const step = steps[valueIdx[k]];
1025
+ const r = results[k];
1026
+ if (r.status === "fulfilled") {
1027
+ values[step.nodeId] = r.value;
1028
+ step.result = r.value;
1029
+ step.status = "success";
1030
+ completed.add(step.nodeId);
1031
+ this.emit("auto-flow/node-result", {
1032
+ runId: state.runId,
1033
+ nodeId: step.nodeId,
1034
+ nodeType: step.nodeType,
1035
+ label: step.label,
1036
+ ok: true,
1037
+ output: r.value
1038
+ });
1039
+ } else {
1040
+ const error = r.reason instanceof Error ? r.reason.message : String(r.reason);
1041
+ step.status = "failed";
1042
+ step.error = error;
1043
+ this.emit("auto-flow/node-result", {
1044
+ runId: state.runId,
1045
+ nodeId: step.nodeId,
1046
+ nodeType: step.nodeType,
1047
+ label: step.label,
1048
+ ok: false,
1049
+ error
1050
+ });
1051
+ state.status = "failed";
1052
+ this.update(state);
1053
+ return;
1054
+ }
1055
+ }
1056
+ }
1057
+ for (const i of approvalIdx) {
1058
+ const step = steps[i];
1059
+ started.add(step.nodeId);
1060
+ this.emit("auto-flow/node-start", {
1061
+ runId: state.runId,
1062
+ nodeId: step.nodeId,
1063
+ nodeType: step.nodeType,
1064
+ label: step.label
1065
+ });
1066
+ await this.runApproval(state, step, nodesById.get(step.nodeId));
1067
+ if (state.status === "failed" || state.status === "cancelled") {
1068
+ this.emit("auto-flow/node-result", {
1069
+ runId: state.runId,
1070
+ nodeId: step.nodeId,
1071
+ nodeType: step.nodeType,
1072
+ label: step.label,
1073
+ ok: false,
1074
+ ...step.error !== void 0 ? { error: step.error } : {}
1075
+ });
1076
+ return;
1077
+ }
1078
+ const value = primaryValue(step.nodeId);
1079
+ values[step.nodeId] = value;
1080
+ step.result = value;
1081
+ completed.add(step.nodeId);
1082
+ this.emit("auto-flow/node-result", {
1083
+ runId: state.runId,
1084
+ nodeId: step.nodeId,
1085
+ nodeType: step.nodeType,
1086
+ label: step.label,
1087
+ ok: true,
1088
+ output: value
1089
+ });
1090
+ }
1091
+ if (scriptIdx.length > 0) {
1092
+ for (const i of scriptIdx) started.add(steps[i].nodeId);
1093
+ const limit = this.deps.maxConcurrency ?? 4;
1094
+ const results = [];
1095
+ for (let start = 0; start < scriptIdx.length; start += limit) {
1096
+ const chunk = scriptIdx.slice(start, start + limit);
1097
+ const chunkResults = await Promise.allSettled(chunk.map((i) => this.runScriptStep(state, steps[i], nodesById.get(steps[i].nodeId), flow, nodesById, ctx, i, input, values, primaryValue(steps[i].nodeId))));
1098
+ results.push(...chunkResults);
1099
+ }
1100
+ if (this.cancelled(state)) {
1101
+ this.markCancelled(state);
1102
+ return;
1103
+ }
1104
+ let failed = false;
1105
+ for (let k = 0; k < scriptIdx.length; k++) {
1106
+ const step = steps[scriptIdx[k]];
1107
+ if (step.status === "failed") {
1108
+ failed = true;
1109
+ continue;
1110
+ }
1111
+ const r = results[k];
1112
+ if (r.status === "rejected") {
1113
+ step.status = "failed";
1114
+ step.error = r.reason instanceof Error ? r.reason.message : String(r.reason);
1115
+ failed = true;
1116
+ } else if (r.value.ok === false) {
1117
+ step.status = "failed";
1118
+ step.error = r.value.error;
1119
+ failed = true;
1120
+ } else {
1121
+ values[step.nodeId] = step.result ?? {};
1122
+ completed.add(step.nodeId);
1123
+ }
1124
+ }
1125
+ if (failed) {
1126
+ state.status = "failed";
1127
+ this.update(state);
1128
+ return;
1129
+ }
1130
+ }
1131
+ this.update(state);
1132
+ }
1133
+ if (this.cancelled(state)) {
1134
+ this.markCancelled(state);
1135
+ return;
1136
+ }
1137
+ state.status = "completed";
1138
+ this.update(state);
1139
+ }
1140
+ /** 执行一个值节点(form/prompt/llm/vision/command/http/condition/output),返回其输出值。 */
1141
+ async executeValueNode(nodeType, node, input, values, current) {
1142
+ const d = node.data;
1143
+ switch (nodeType) {
1144
+ case FORM_NODE_TYPE: return formFieldsOf(d);
1145
+ case PROMPT_NODE_TYPE: return renderText(asString(d.template), input, values, current);
1146
+ case OUTPUT_NODE_TYPE: return resolveValue(asString(d.value), input, values, current) ?? null;
1147
+ case CONDITION_NODE_TYPE: {
1148
+ const left = resolveValue(asString(d.left), input, values, current);
1149
+ const right = resolveValue(asString(d.right), input, values, current);
1150
+ return compare(left, asConditionOperator(d.operator), right);
1151
+ }
1152
+ case "llm": return this.runLlmNode(node, input, values, current);
1153
+ case AGENT_NODE_TYPE: return this.runAgentNode(node, input, values, current);
1154
+ case WORKFLOW_NODE_TYPE: return this.runWorkflowNode(node, input, values, current);
1155
+ case VISION_NODE_TYPE: return this.runVisionNode(node, input, values, current);
1156
+ case COMMAND_NODE_TYPE: return this.runCommandNode(node, input, values, current);
1157
+ case HTTP_NODE_TYPE: return this.runHttpNode(node, input, values, current);
1158
+ default: throw new Error(`未知节点类型: ${nodeType}`);
1159
+ }
1160
+ }
1161
+ /** 直接 LLM 调用(单次,无 agent 循环)。 */
1162
+ async runLlmNode(node, input, values, current) {
1163
+ const llm = this.deps.llm;
1164
+ if (llm === void 0) throw new Error("AI 模型节点需要 LLM 服务,但当前部署未提供");
1165
+ const d = node.data;
1166
+ const promptText = promptFor(asString(d.prompt), input, values, current);
1167
+ const provider = asString(d.provider);
1168
+ const model = asString(d.model);
1169
+ let resolvedProvider = provider;
1170
+ let resolvedModel = model;
1171
+ if (resolvedProvider === "" || resolvedModel === "") {
1172
+ const fallback = this.deps.agentDefaultModel?.currentSelection();
1173
+ resolvedProvider = resolvedProvider === "" ? fallback?.provider.trim() ?? "" : resolvedProvider;
1174
+ resolvedModel = resolvedModel === "" ? fallback?.model.trim() ?? "" : resolvedModel;
1175
+ }
1176
+ if (resolvedProvider === "" || resolvedModel === "") throw new Error("AI 模型节点未配置 provider / model,且部署无默认模型");
1177
+ const maxTokens = asNumber(d.maxTokens);
1178
+ const stream = llm.stream({
1179
+ provider: resolvedProvider,
1180
+ model: resolvedModel,
1181
+ messages: [createUserMessage({
1182
+ content: [{
1183
+ type: "text",
1184
+ text: promptText
1185
+ }],
1186
+ source: {
1187
+ kind: "plugin",
1188
+ plugin: "dsh-auto-flow"
1189
+ }
1190
+ })],
1191
+ ...maxTokens !== void 0 ? { maxTokens } : {},
1192
+ ...this.deps.signal !== void 0 ? { signal: this.deps.signal } : {}
1193
+ });
1194
+ const collected = await this.collectLlmText(stream);
1195
+ if (!collected.ok) throw new Error(`AI 模型调用失败:${collected.error}`);
1196
+ return collected.text;
1197
+ }
1198
+ /** AI 智能体节点:把子任务委托给子代理(LLM + 工具循环),结果回灌值链。 */
1199
+ async runAgentNode(node, input, values, current) {
1200
+ const agent = this.deps.agent;
1201
+ if (agent === void 0) throw new Error("AI 智能体节点需要子代理能力:请由 agent 触发(workflow_run),且部署已组合 subagents 服务");
1202
+ const d = node.data;
1203
+ const task = promptFor(asString(d.task), input, values, current).trim();
1204
+ if (task === "") throw new Error("AI 智能体节点的任务为空");
1205
+ const provider = asString(d.provider);
1206
+ const model = asString(d.model);
1207
+ const outcome = await agent.run(task, {
1208
+ label: node.id,
1209
+ ...provider !== "" ? { provider } : {},
1210
+ ...model !== "" ? { model } : {}
1211
+ });
1212
+ if (!outcome.ok) throw new Error(`AI 智能体执行失败:${outcome.error ?? "未知错误"}`);
1213
+ return outcome.text ?? "";
1214
+ }
1215
+ /** 调用另一个已保存工作流:渲染 args(JSON 对象)→ 嵌套运行 → 回灌其 Output 值。 */
1216
+ async runWorkflowNode(node, input, values, current) {
1217
+ const runWorkflow = this.deps.runWorkflow;
1218
+ if (runWorkflow === void 0) throw new Error("调用工作流节点需要运行能力,但当前部署未提供");
1219
+ const d = node.data;
1220
+ const workflowId = asString(d.workflowId).trim();
1221
+ if (workflowId === "") throw new Error("调用工作流节点的目标工作流为空");
1222
+ const argsRaw = asString(d.args);
1223
+ let args = {};
1224
+ if (argsRaw !== "") {
1225
+ const rendered = renderText(argsRaw, input, values, current).trim();
1226
+ if (rendered !== "") try {
1227
+ const parsed = JSON.parse(rendered);
1228
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) args = parsed;
1229
+ } catch {
1230
+ throw new Error("调用工作流节点的参数不是合法 JSON 对象");
1231
+ }
1232
+ }
1233
+ const outcome = await runWorkflow(workflowId, args);
1234
+ if (!outcome.ok) throw new Error(outcome.error ?? "调用工作流失败");
1235
+ return outcome.result ?? null;
1236
+ }
1237
+ /** 图片识别:宿主读文件 → 附件存储 → 一次视觉模型调用。 */
1238
+ async runVisionNode(node, input, values, current) {
1239
+ const llm = this.deps.llm;
1240
+ if (llm === void 0) throw new Error("图片识别节点需要 LLM 服务,但当前部署未提供");
1241
+ const d = node.data;
1242
+ const imagePath = renderText(asString(d.image), input, values, current).trim();
1243
+ if (imagePath === "" && d.skipWhenNoImage === true) return current;
1244
+ const instruction = renderText(asString(d.instruction), input, values, current);
1245
+ if (imagePath === "") throw new Error("图片识别节点的图片路径为空");
1246
+ const attachments = this.deps.attachments;
1247
+ if (attachments === void 0) throw new Error("图片识别节点需要附件存储,但当前部署未提供");
1248
+ const provider = asString(d.provider);
1249
+ const model = asString(d.model);
1250
+ if (provider === "" || model === "") throw new Error("图片识别节点未配置 provider / model");
1251
+ let bytes;
1252
+ try {
1253
+ bytes = await readFile(imagePath);
1254
+ } catch {
1255
+ throw new Error(`图片识别节点无法读取图片:${imagePath}`);
1256
+ }
1257
+ const mediaType = mediaTypeOfPath(imagePath);
1258
+ if (mediaType === void 0) throw new Error(`图片识别节点的图片不是受支持类型:${imagePath}`);
1259
+ const saved = await attachments.saveImage({
1260
+ data: bytes,
1261
+ mediaType,
1262
+ name: node.id
1263
+ });
1264
+ const stream = llm.stream({
1265
+ provider,
1266
+ model,
1267
+ messages: [createUserMessage({
1268
+ content: [{
1269
+ type: "image",
1270
+ attachment: { attachmentId: saved.attachmentId }
1271
+ }, {
1272
+ type: "text",
1273
+ text: instruction
1274
+ }],
1275
+ source: {
1276
+ kind: "plugin",
1277
+ plugin: "dsh-auto-flow"
1278
+ }
1279
+ })]
1280
+ });
1281
+ const collected = await this.collectLlmText(stream);
1282
+ if (!collected.ok) throw new Error(`图片识别调用失败:${collected.error}`);
1283
+ return collected.text;
1284
+ }
1285
+ /** 确定性 shell 命令执行。 */
1286
+ async runCommandNode(node, input, values, current) {
1287
+ const shell = this.deps.shell;
1288
+ if (shell === void 0) throw new Error("执行命令节点需要 shell 服务,但当前部署未提供");
1289
+ const d = node.data;
1290
+ const command = renderText(asString(d.command), input, values, current).trim();
1291
+ if (command === "") throw new Error("执行命令节点的命令为空");
1292
+ const workdir = asString(d.workdir);
1293
+ const timeoutMs = asNumber(d.timeoutMs);
1294
+ const sandboxMode = asString(d.sandboxMode);
1295
+ const spec = shell.resolve({
1296
+ command,
1297
+ ...workdir === "" ? {} : { workdir },
1298
+ ...timeoutMs !== void 0 ? { timeoutMs } : {},
1299
+ ...sandboxMode === "" ? {} : { sandboxPolicy: {
1300
+ mode: sandboxMode,
1301
+ workspaceRoot: process.cwd()
1302
+ } },
1303
+ ...this.deps.signal !== void 0 ? { signal: this.deps.signal } : {}
1304
+ });
1305
+ const result = await shell.run(spec);
1306
+ if (result.sandbox?.denied === true) throw new Error("执行命令被沙箱拒绝");
1307
+ if (result.timedOut) throw new Error(`执行命令超时(${result.timeoutMs ?? timeoutMs ?? "默认"} ms)`);
1308
+ if (result.aborted) throw new Error("执行命令已中止");
1309
+ if (result.exitCode !== 0) {
1310
+ const detail = [result.stderr.text.trim(), result.stdout.text.trim()].filter((line) => line !== "").join("\n");
1311
+ throw new Error(`命令退出码 ${String(result.exitCode)}${detail === "" ? "" : `:\n${detail}`}`);
1312
+ }
1313
+ return result.stdout.text.trim();
1314
+ }
1315
+ /** 确定性 HTTP 请求(超时 + 响应截断)。 */
1316
+ async runHttpNode(node, input, values, current) {
1317
+ const d = node.data;
1318
+ const url = renderText(asString(d.url), input, values, current).trim();
1319
+ if (url === "") throw new Error("HTTP 请求节点的地址为空");
1320
+ const method = asString(d.method) || "GET";
1321
+ let headers;
1322
+ const headersRaw = asString(d.headers);
1323
+ if (headersRaw !== "") {
1324
+ const rendered = renderText(headersRaw, input, values, current).trim();
1325
+ try {
1326
+ const parsed = JSON.parse(rendered);
1327
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error();
1328
+ headers = parsed;
1329
+ } catch {
1330
+ throw new Error("HTTP 请求节点的请求头不是合法 JSON 对象");
1331
+ }
1332
+ }
1333
+ const bodyRaw = asString(d.body);
1334
+ const body = bodyRaw === "" ? void 0 : renderText(bodyRaw, input, values, current);
1335
+ const timeoutMs = asNumber(d.timeoutMs) ?? 3e4;
1336
+ const fused = deadline(this.deps.signal, timeoutMs, HTTP_TIMEOUT_CODE);
1337
+ const controller = new AbortController();
1338
+ const onAbort = () => controller.abort();
1339
+ if (fused.signal.aborted) onAbort();
1340
+ else fused.signal.addEventListener("abort", onAbort, { once: true });
1341
+ try {
1342
+ const response = await fetch(url, {
1343
+ method,
1344
+ ...headers === void 0 ? {} : { headers },
1345
+ ...body === void 0 ? {} : { body },
1346
+ signal: controller.signal
1347
+ });
1348
+ const text = (await response.text()).slice(0, HTTP_NODE_BODY_CAP_CHARS);
1349
+ const bodyText = text.length === 1e5 ? `${text}…[已截断]` : text;
1350
+ return {
1351
+ status: response.status,
1352
+ ok: response.ok,
1353
+ body: bodyText
1354
+ };
1355
+ } catch (error) {
1356
+ if (timeoutOf(fused.signal, HTTP_TIMEOUT_CODE) !== void 0) throw new Error(`HTTP 请求超时(${timeoutMs} ms)`);
1357
+ if (fused.signal.aborted) throw new Error("HTTP 请求已取消");
1358
+ throw new Error(`HTTP 请求失败:${error instanceof Error ? error.message : String(error)}`);
1359
+ } finally {
1360
+ fused.signal.removeEventListener("abort", onAbort);
1361
+ fused[Symbol.dispose]();
1362
+ }
1363
+ }
1364
+ /** 把一次直接 LLM 流收敛为文本。 */
1365
+ async collectLlmText(stream) {
1366
+ const parts = [];
1367
+ let finishKind;
1368
+ let finishMessage;
1369
+ for await (const chunk of stream) {
1370
+ if (chunk.type === "text-delta" && typeof chunk.text === "string") parts.push(chunk.text);
1371
+ if (chunk.type !== "finish") continue;
1372
+ finishKind = chunk.reason?.kind;
1373
+ finishMessage = chunk.reason?.failure?.message;
1374
+ }
1375
+ if (finishKind === "error" || finishKind === "aborted") return {
1376
+ ok: false,
1377
+ error: finishMessage ?? "模型请求失败"
1378
+ };
1379
+ return {
1380
+ ok: true,
1381
+ text: parts.join("").trim()
1382
+ };
1383
+ }
1384
+ async runApproval(state, step, node) {
1385
+ step.status = "waiting";
1386
+ state.status = "waiting";
1387
+ this.update(state);
1388
+ this.emit("auto-flow/run-need-approval", {
1389
+ runId: state.runId,
1390
+ nodeId: step.nodeId,
1391
+ label: step.label
1392
+ });
1393
+ const options = approvalOptionsOf(node.data);
1394
+ const chosen = await this.deps.waitApproval(step, options);
1395
+ if (isCancelled(state)) return;
1396
+ const matched = options.find((o) => o.label === chosen) ?? {
1397
+ label: chosen,
1398
+ action: "abort"
1399
+ };
1400
+ step.result = matched.label;
1401
+ if (matched.action === "abort") {
1402
+ step.status = "failed";
1403
+ step.error = `审批中止:${matched.label}`;
1404
+ state.status = "failed";
1405
+ this.update(state);
1406
+ return;
1407
+ }
1408
+ step.status = "success";
1409
+ state.status = "running";
1410
+ }
1411
+ async runScriptStep(state, step, node, flow, nodesById, ctx, topoIndex, input, values, current) {
1412
+ step.status = "running";
1413
+ this.update(state);
1414
+ this.emit("auto-flow/node-start", {
1415
+ runId: state.runId,
1416
+ nodeId: step.nodeId,
1417
+ nodeType: step.nodeType,
1418
+ label: step.label
1419
+ });
1420
+ const script = typeof node.data.script === "string" ? node.data.script : "";
1421
+ if (script === "") {
1422
+ step.status = "failed";
1423
+ step.error = "未选择脚本";
1424
+ this.emit("auto-flow/node-result", {
1425
+ runId: state.runId,
1426
+ nodeId: step.nodeId,
1427
+ nodeType: step.nodeType,
1428
+ label: step.label,
1429
+ ok: false,
1430
+ error: "未选择脚本"
1431
+ });
1432
+ return {
1433
+ ok: false,
1434
+ error: "未选择脚本"
1435
+ };
1436
+ }
1437
+ step.script = script;
1438
+ step.stepDir = `${topoIndex}-${script.replaceAll("/", "-")}`;
1439
+ try {
1440
+ const payload = interpolatePayload(buildScriptPayload(script, await this.collectInputs(node, flow, nodesById, ctx), this.deps.resolveArtifacts?.(script)), input, values, current);
1441
+ const outcome = await this.runWithRetry(script, payload, ctx, state, step.stepDir, buildScriptRunOptions(node.data), this.deps.maxRetries ?? 1);
1442
+ step.result = outcome.data;
1443
+ step.durationMs = outcome.durationMs;
1444
+ step.status = outcome.ok ? "success" : "failed";
1445
+ if (!outcome.ok) {
1446
+ step.error = outcome.error;
1447
+ step.errorCode = outcome.code;
1448
+ }
1449
+ this.emit("auto-flow/node-result", {
1450
+ runId: state.runId,
1451
+ nodeId: step.nodeId,
1452
+ nodeType: step.nodeType,
1453
+ label: step.label,
1454
+ ok: outcome.ok,
1455
+ ...step.result !== void 0 ? { output: step.result } : {},
1456
+ ...!outcome.ok && step.error !== void 0 ? { error: step.error } : {}
1457
+ });
1458
+ return outcome;
1459
+ } catch (err) {
1460
+ step.status = "failed";
1461
+ step.error = err instanceof Error ? err.message : String(err);
1462
+ this.emit("auto-flow/node-result", {
1463
+ runId: state.runId,
1464
+ nodeId: step.nodeId,
1465
+ nodeType: step.nodeType,
1466
+ label: step.label,
1467
+ ok: false,
1468
+ error: step.error
1469
+ });
1470
+ return {
1471
+ ok: false,
1472
+ error: step.error
1473
+ };
1474
+ }
1475
+ }
1476
+ /** 执行脚本并按需重试:仅在失败/抛错时重试,成功立即返回。 */
1477
+ async runWithRetry(script, payload, ctx, state, stepDir, runOptions, maxRetries) {
1478
+ let last = {
1479
+ ok: false,
1480
+ error: "未执行"
1481
+ };
1482
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
1483
+ try {
1484
+ last = await this.runStep(script, payload, ctx, state, stepDir, runOptions);
1485
+ } catch (err) {
1486
+ last = {
1487
+ ok: false,
1488
+ error: err instanceof Error ? err.message : String(err)
1489
+ };
1490
+ }
1491
+ if (last.ok) return last;
1492
+ if (last.code !== void 0 && NON_RETRYABLE_CODES.has(last.code)) return last;
1493
+ }
1494
+ if (!last.ok && maxRetries > 0) return {
1495
+ ok: false,
1496
+ error: `${last.error}(已重试 ${maxRetries} 次)`
1497
+ };
1498
+ return last;
1499
+ }
1500
+ /**
1501
+ * 聚合「直连上游」(穿越审批节点,审批是数据通道不改变数据流):
1502
+ * - 表单 → 字段值(参数数据源);
1503
+ * - 脚本 → 产物文件 + 标量输出(脚本是终止来源,不继续穿越)。
1504
+ */
1505
+ async collectInputs(node, flow, nodesById, ctx) {
1506
+ const params = {};
1507
+ const scriptNodes = [];
1508
+ walkUpstream(flow, node.id, nodesById, (source) => Object.assign(params, formFieldsOf(source.data)), (source) => {
1509
+ scriptNodes.push(source);
1510
+ });
1511
+ const files = {};
1512
+ const outputs = {};
1513
+ for (const source of scriptNodes) {
1514
+ const sourceScript = typeof source.data.script === "string" ? source.data.script : "";
1515
+ if (sourceScript === "") continue;
1516
+ files[sourceScript] = (await ctx.filesOf(sourceScript)).map((f) => f.path);
1517
+ const out = ctx.outputs[sourceScript];
1518
+ if (out !== void 0 && out.ok) outputs[sourceScript] = out.data;
1519
+ }
1520
+ return {
1521
+ params,
1522
+ files,
1523
+ outputs,
1524
+ inline: node.data
1525
+ };
1526
+ }
1527
+ async runStep(script, payload, ctx, state, stepDir, runOptions) {
1528
+ const outcome = await this.deps.runScript(script, payload, {
1529
+ baseDir: state.workspaceDir,
1530
+ runId: stepDir,
1531
+ ...runOptions,
1532
+ ...this.deps.signal !== void 0 ? { signal: this.deps.signal } : {}
1533
+ });
1534
+ if (outcome.ok) {
1535
+ const data = asRecord(outcome.data);
1536
+ const dir = path.join(state.workspaceDir, stepDir);
1537
+ ctx.outputs[script] = {
1538
+ script,
1539
+ ok: true,
1540
+ data,
1541
+ stepDir: dir
1542
+ };
1543
+ ctx.stepDirs.set(script, dir);
1544
+ return {
1545
+ ok: true,
1546
+ data,
1547
+ durationMs: outcome.durationMs
1548
+ };
1549
+ }
1550
+ return {
1551
+ ok: false,
1552
+ error: outcome.error,
1553
+ code: outcome.code,
1554
+ durationMs: outcome.durationMs
1555
+ };
1556
+ }
1557
+ update(state) {
1558
+ state.updatedAt = Date.now();
1559
+ this.deps.persist(state);
1560
+ }
1561
+ };
1562
+ /** 引擎在一次运行中维护的「产物」上下文(脚本返回 data + 落盘目录,供下游聚合取用)。 */
1563
+ var RunContext = class {
1564
+ workspaceDir;
1565
+ outputs = {};
1566
+ stepDirs = /* @__PURE__ */ new Map();
1567
+ constructor(workspaceDir) {
1568
+ this.workspaceDir = workspaceDir;
1569
+ }
1570
+ /** 取某脚本「当前」产物文件(人工删改后实时重扫)。 */
1571
+ async filesOf(script) {
1572
+ const dir = this.stepDirs.get(script);
1573
+ if (dir === void 0) return [];
1574
+ return listWorkspaceFiles(dir);
1575
+ }
1576
+ };
1577
+ /** 把任意 JSON 值弱化为对象(脚本 data 应为对象)。 */
1578
+ function asRecord(value) {
1579
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) return value;
1580
+ return {};
1581
+ }
1582
+ /** 审批节点的默认选项:通过(继续)/ 驳回(中止)。 */
1583
+ const DEFAULT_APPROVAL_OPTIONS = [{
1584
+ label: "通过",
1585
+ action: "continue"
1586
+ }, {
1587
+ label: "驳回",
1588
+ action: "abort"
1589
+ }];
1590
+ /** 从审批节点 data.options 解析选项(非法/空则回退默认 通过/驳回)。 */
1591
+ function approvalOptionsOf(data) {
1592
+ const raw = data.options;
1593
+ if (Array.isArray(raw)) {
1594
+ const options = [];
1595
+ for (const item of raw) {
1596
+ if (item === null || typeof item !== "object" || Array.isArray(item)) continue;
1597
+ const o = item;
1598
+ if (typeof o.label !== "string" || o.label === "") continue;
1599
+ options.push({
1600
+ label: o.label,
1601
+ action: o.action === "abort" ? "abort" : "continue"
1602
+ });
1603
+ }
1604
+ if (options.length > 0) return options;
1605
+ }
1606
+ return [...DEFAULT_APPROVAL_OPTIONS];
1607
+ }
1608
+ /** 一次运行的「最终结果」= 最后一个成功执行的 output 节点值(无 output 节点则 undefined)。 */
1609
+ function runResultOf(state) {
1610
+ let result;
1611
+ for (const step of state.steps) if (step.nodeType === "output" && step.status === "success") result = step.result;
1612
+ return result;
1613
+ }
1614
+ /** 弱化为字符串(非字符串 → 空串)。 */
1615
+ function asString(value) {
1616
+ return typeof value === "string" ? value : "";
1617
+ }
1618
+ /** 弱化为有限数值(非数字/非有限 → undefined)。 */
1619
+ function asNumber(value) {
1620
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
1621
+ }
1622
+ /** 弱化为条件运算符(白名单;非法/缺省回退 equals)。 */
1623
+ function asConditionOperator(value) {
1624
+ const s = asString(value);
1625
+ return CONDITION_OPERATORS.includes(s) ? s : "equals";
1626
+ }
1627
+ /** 全局输入 = 全部表单节点字段值合并({{input.*}} 的取值来源;画布顺序靠后者覆盖)。 */
1628
+ function collectWorkflowInput(flow) {
1629
+ const out = {};
1630
+ for (const node of flow.nodes) {
1631
+ if (nodeTypeOf(node) !== "form") continue;
1632
+ Object.assign(out, formFieldsOf(node.data));
1633
+ }
1634
+ return out;
1635
+ }
1636
+ /** 按扩展名推断图片媒体类型(vision 节点白名单;交给 mime 库识别扩展名)。 */
1637
+ function mediaTypeOfPath(p) {
1638
+ const type = mime.getType(p);
1639
+ return type === "image/png" || type === "image/jpeg" || type === "image/webp" || type === "image/gif" ? type : void 0;
1640
+ }
1641
+ /** 递归插值脚本 payload 里的字符串值(消费上游值节点输出 / 全局输入 / current)。 */
1642
+ function interpolatePayload(payload, input, values, current) {
1643
+ const out = {};
1644
+ for (const [key, value] of Object.entries(payload)) out[key] = interpolateDeep(value, input, values, current);
1645
+ return out;
1646
+ }
1647
+ function interpolateDeep(value, input, values, current) {
1648
+ if (typeof value === "string") return resolveValue(value, input, values, current) ?? value;
1649
+ if (Array.isArray(value)) return value.map((item) => interpolateDeep(item, input, values, current));
1650
+ if (value !== null && typeof value === "object") {
1651
+ const out = {};
1652
+ for (const [key, item] of Object.entries(value)) out[key] = interpolateDeep(item, input, values, current);
1653
+ return out;
1654
+ }
1655
+ return value;
1656
+ }
1657
+ //#endregion
1658
+ //#region src/host/agent-tools.ts
1659
+ /** 渲染运行工具结果文本(截断)。 */
1660
+ function renderRun(id, nodesStarted, value, maxChars) {
1661
+ const rendered = JSON.stringify(value, null, 2);
1662
+ return `工作流 "${id}" 运行完成(${nodesStarted} 个节点)。\n结果:\n${rendered.length > maxChars ? `${rendered.slice(0, maxChars)}\n…[已截断]` : rendered}`;
1663
+ }
1664
+ /** 渲染运行历史列表文本(一列一行,倒序已由 listRuns 保证)。 */
1665
+ function renderRuns(runs) {
1666
+ if (runs.length === 0) return "(没有运行记录)";
1667
+ return runs.map((r) => `- ${r.runId} · ${r.name}(${r.workflowId})· ${r.status} · ${new Date(r.createdAt).toISOString()}`).join("\n");
1668
+ }
1669
+ /** 把任意业务对象编码为线协议 JSON 值(JSON 往返丢弃 undefined,并满足工具 output 的 JsonValue 约束)。 */
1670
+ function toJsonValue(value) {
1671
+ return JSON.parse(JSON.stringify(value) ?? "null");
1672
+ }
1673
+ /** 把模型提供的工作流对象(含 id)弱化为 { id, flow };形状非法时抛错说明缺什么。 */
1674
+ function coerceWorkflow(raw) {
1675
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) throw new Error("workflow 必须是对象");
1676
+ const o = raw;
1677
+ const id = typeof o.id === "string" ? o.id.trim() : "";
1678
+ if (id === "") throw new Error("workflow.id 必须是非空字符串");
1679
+ if (!Array.isArray(o.nodes)) throw new Error("workflow.nodes 必须是数组");
1680
+ if (!Array.isArray(o.edges)) throw new Error("workflow.edges 必须是数组");
1681
+ return {
1682
+ id,
1683
+ flow: {
1684
+ formatVersion: typeof o.formatVersion === "number" ? o.formatVersion : 1,
1685
+ name: typeof o.name === "string" ? o.name : id,
1686
+ description: typeof o.description === "string" ? o.description : "",
1687
+ nodes: o.nodes,
1688
+ edges: o.edges
1689
+ }
1690
+ };
1691
+ }
1692
+ /**
1693
+ * 注册 workflow_run / workflow_list / workflow_runs / workflow_read / workflow_save / workflow_delete 模型工具(tools 服务未组合时跳过)。
1694
+ *
1695
+ * 注册契约:在插件根上下文经 `ctx.tools.register` 注册即落在 dsh-tools 的「全局层」,
1696
+ * 所有子代理(soft `workflow` 工具的 fan-out 子代理、硬工作流 agent 节点的子代理)默认
1697
+ * 继承全局层工具——除非部署的子代理 preset 用 `ctx.tools.restrict()` 显式过滤。因此软→硬
1698
+ * 调用桥(子代理里调 workflow_run / workflow_read 等)在注册层天然已通,唯一的外部前提是
1699
+ * preset 未把这些工具 restrict 掉。
1700
+ */
1701
+ function registerAutoFlowTools(ctx, service) {
1702
+ const tools = ctx.get("tools");
1703
+ if (tools === void 0) return;
1704
+ tools.register(defineTool({
1705
+ name: "workflow_list",
1706
+ description: "列出工作流编辑器里已保存的工作流(id / 名称 / 描述 / 节点数 / 输入键)。运行某个工作流前先用它发现 id 与可传的输入键。",
1707
+ parameters: {},
1708
+ output: {
1709
+ schema: {
1710
+ type: "object",
1711
+ additionalProperties: false,
1712
+ properties: { workflows: {
1713
+ type: "array",
1714
+ required: true,
1715
+ items: {
1716
+ type: "object",
1717
+ additionalProperties: false,
1718
+ properties: {
1719
+ id: {
1720
+ type: "string",
1721
+ required: true
1722
+ },
1723
+ name: {
1724
+ type: "string",
1725
+ required: true
1726
+ },
1727
+ description: {
1728
+ type: "string",
1729
+ required: true
1730
+ },
1731
+ nodeCount: {
1732
+ type: "integer",
1733
+ required: true
1734
+ },
1735
+ inputs: {
1736
+ type: "array",
1737
+ required: true,
1738
+ items: { type: "string" }
1739
+ }
1740
+ }
1741
+ }
1742
+ } }
1743
+ },
1744
+ render: (_args, value) => {
1745
+ const workflows = value.workflows;
1746
+ return [{
1747
+ type: "text",
1748
+ text: workflows.length === 0 ? "(没有已保存的工作流)" : workflows.map((w) => {
1749
+ const inputs = w.inputs.length > 0 ? `(输入:${w.inputs.join("、")})` : "";
1750
+ return `- ${w.name}(id: ${w.id},${w.nodeCount} 个节点):${w.description}${inputs}`;
1751
+ }).join("\n")
1752
+ }];
1753
+ }
1754
+ },
1755
+ async execute() {
1756
+ return { workflows: (await service.listFlows()).map((f) => ({
1757
+ id: f.id,
1758
+ name: f.name,
1759
+ description: f.description,
1760
+ nodeCount: f.nodeCount,
1761
+ inputs: f.inputs
1762
+ })) };
1763
+ },
1764
+ presentCall: () => ({
1765
+ card: "generic",
1766
+ title: "workflow_list"
1767
+ }),
1768
+ presentResult: () => ({ card: "generic" })
1769
+ }));
1770
+ tools.register(defineTool({
1771
+ name: "workflow_run",
1772
+ description: "按 id 运行工作流编辑器里已保存的工作流(浏览器脚本 / shell / HTTP / LLM / AI 智能体节点)。先用 workflow_list 发现 id 与可传的输入键;args 按输入键覆盖工作流表单的默认值。若工作流含人工审批节点,会在本会话里弹卡片请你通过/驳回。",
1773
+ parameters: {
1774
+ id: {
1775
+ type: "string",
1776
+ required: true,
1777
+ description: "要运行的工作流 id(用 workflow_list 发现)。"
1778
+ },
1779
+ args: {
1780
+ type: "object",
1781
+ additionalProperties: true,
1782
+ description: "可选运行输入;覆盖工作流表单默认值。"
1783
+ }
1784
+ },
1785
+ output: {
1786
+ schema: {
1787
+ type: "object",
1788
+ additionalProperties: false,
1789
+ properties: {
1790
+ runId: {
1791
+ type: "string",
1792
+ required: true
1793
+ },
1794
+ nodesStarted: {
1795
+ type: "integer",
1796
+ required: true
1797
+ },
1798
+ result: {
1799
+ type: "json",
1800
+ required: true
1801
+ }
1802
+ }
1803
+ },
1804
+ render: (args, value) => {
1805
+ const v = value;
1806
+ return [{
1807
+ type: "text",
1808
+ text: renderRun(String(args.id ?? ""), v.nodesStarted, v.result, 5e4)
1809
+ }];
1810
+ }
1811
+ },
1812
+ async execute(args, exec) {
1813
+ const id = String(args.id ?? "");
1814
+ const runArgs = args.args ?? {};
1815
+ const outcome = await service.runForAgent(id, runArgs, exec.agent, exec.signal);
1816
+ if (!outcome.ok) throw new Error(outcome.error ?? "运行失败");
1817
+ const nodesStarted = outcome.steps.filter((s) => s.status !== "skipped" && s.status !== "pending").length;
1818
+ return {
1819
+ runId: outcome.runId,
1820
+ nodesStarted,
1821
+ result: outcome.result ?? null
1822
+ };
1823
+ },
1824
+ presentCall: (args) => ({
1825
+ card: "generic",
1826
+ title: `workflow_run: ${String(args.id ?? "")}`,
1827
+ rawInput: JSON.stringify(args.args ?? {})
1828
+ }),
1829
+ presentResult: () => ({ card: "generic" })
1830
+ }));
1831
+ tools.register(defineTool({
1832
+ name: "workflow_runs",
1833
+ description: "读取工作流运行历史(状态 / 节点数 / 时间),可按 workflowId 或 sessionId 过滤。用于排查某次运行是否失败、查看某个工作流或会话的历史运行记录。",
1834
+ parameters: {
1835
+ workflowId: {
1836
+ type: "string",
1837
+ description: "只返回该工作流的运行记录。"
1838
+ },
1839
+ sessionId: {
1840
+ type: "string",
1841
+ description: "只返回该会话发起的运行记录。"
1842
+ }
1843
+ },
1844
+ output: {
1845
+ schema: {
1846
+ type: "object",
1847
+ additionalProperties: false,
1848
+ properties: { runs: {
1849
+ type: "array",
1850
+ required: true,
1851
+ items: {
1852
+ type: "object",
1853
+ additionalProperties: false,
1854
+ properties: {
1855
+ runId: {
1856
+ type: "string",
1857
+ required: true
1858
+ },
1859
+ workflowId: {
1860
+ type: "string",
1861
+ required: true
1862
+ },
1863
+ name: {
1864
+ type: "string",
1865
+ required: true
1866
+ },
1867
+ status: {
1868
+ type: "string",
1869
+ required: true
1870
+ },
1871
+ nodeCount: {
1872
+ type: "integer",
1873
+ required: true
1874
+ },
1875
+ createdAt: {
1876
+ type: "integer",
1877
+ required: true
1878
+ }
1879
+ }
1880
+ }
1881
+ } }
1882
+ },
1883
+ render: (_args, value) => {
1884
+ const runs = value.runs;
1885
+ return [{
1886
+ type: "text",
1887
+ text: renderRuns(runs)
1888
+ }];
1889
+ }
1890
+ },
1891
+ async execute(args) {
1892
+ const a = args;
1893
+ return { runs: (await service.listRuns({
1894
+ ...typeof a.workflowId === "string" && a.workflowId !== "" ? { workflowId: a.workflowId } : {},
1895
+ ...typeof a.sessionId === "string" && a.sessionId !== "" ? { sessionId: a.sessionId } : {}
1896
+ })).map((r) => ({
1897
+ runId: r.runId,
1898
+ workflowId: r.workflowId,
1899
+ name: r.name,
1900
+ status: r.status,
1901
+ nodeCount: r.nodeCount,
1902
+ createdAt: r.createdAt
1903
+ })) };
1904
+ },
1905
+ presentCall: (args) => ({
1906
+ card: "generic",
1907
+ title: "workflow_runs",
1908
+ rawInput: JSON.stringify(args ?? {})
1909
+ }),
1910
+ presentResult: () => ({ card: "generic" })
1911
+ }));
1912
+ tools.register(defineTool({
1913
+ name: "workflow_read",
1914
+ description: "按 id 读取工作流定义(formatVersion / name / description / nodes / edges),用于查看某个工作流做了什么,或作为用 workflow_save 调整它的起点。",
1915
+ parameters: { id: {
1916
+ type: "string",
1917
+ required: true,
1918
+ description: "要读取的工作流 id(用 workflow_list 发现)。"
1919
+ } },
1920
+ output: {
1921
+ schema: {
1922
+ type: "object",
1923
+ additionalProperties: false,
1924
+ properties: { workflow: { type: "json" } }
1925
+ },
1926
+ render: (_args, value) => {
1927
+ const w = value.workflow;
1928
+ return [{
1929
+ type: "text",
1930
+ text: w === null || w === void 0 ? "工作流不存在。" : `工作流定义:\n${JSON.stringify(w, null, 2)}`
1931
+ }];
1932
+ }
1933
+ },
1934
+ async execute(args) {
1935
+ const id = String(args.id ?? "").trim();
1936
+ if (id === "") return {};
1937
+ const loaded = await service.loadFlow(id);
1938
+ return loaded === null ? {} : { workflow: toJsonValue({
1939
+ id,
1940
+ ...loaded
1941
+ }) };
1942
+ },
1943
+ presentCall: (args) => ({
1944
+ card: "generic",
1945
+ title: `workflow_read: ${String(args.id ?? "")}`
1946
+ }),
1947
+ presentResult: () => ({ card: "generic" })
1948
+ }));
1949
+ tools.register(defineTool({
1950
+ name: "workflow_save",
1951
+ description: "创建或更新一个工作流定义。workflow 必须包含:id(kebab-case)、name、description、nodes、edges。保存前会做结构化校验(环 / 空白字段 / 变量引用等),失败时错误说明要改什么,可迭代修正。调整前先用 workflow_read 读取当前定义。",
1952
+ parameters: { workflow: {
1953
+ type: "object",
1954
+ additionalProperties: true,
1955
+ required: true,
1956
+ description: "完整工作流定义(含 id)。"
1957
+ } },
1958
+ output: {
1959
+ schema: {
1960
+ type: "object",
1961
+ additionalProperties: false,
1962
+ properties: {
1963
+ saved: {
1964
+ type: "boolean",
1965
+ required: true
1966
+ },
1967
+ id: {
1968
+ type: "string",
1969
+ required: true
1970
+ }
1971
+ }
1972
+ },
1973
+ render: (_args, value) => {
1974
+ const v = value;
1975
+ return [{
1976
+ type: "text",
1977
+ text: `工作流「${v.id}」${v.saved ? "已保存" : "未保存"}。`
1978
+ }];
1979
+ }
1980
+ },
1981
+ async execute(args) {
1982
+ const parsed = coerceWorkflow(args.workflow);
1983
+ const validation = validateFlow({
1984
+ nodes: parsed.flow.nodes,
1985
+ edges: parsed.flow.edges
1986
+ });
1987
+ if (!validation.valid) throw new Error(`工作流校验失败,未保存:${validation.diagnostics.map((d) => d.message).join(";")}`);
1988
+ await service.saveFlow(parsed.id, parsed.flow);
1989
+ return {
1990
+ saved: true,
1991
+ id: parsed.id
1992
+ };
1993
+ },
1994
+ presentCall: (args) => ({
1995
+ card: "generic",
1996
+ title: "workflow_save",
1997
+ rawInput: JSON.stringify(args.workflow ?? {})
1998
+ }),
1999
+ presentResult: () => ({ card: "generic" })
2000
+ }));
2001
+ tools.register(defineTool({
2002
+ name: "workflow_delete",
2003
+ description: "按 id 删除一个工作流,返回是否实际删除。仅在用户明确要求删除时使用;删除前先用 workflow_list 确认目标。删除定义不会删除其历史运行记录。",
2004
+ parameters: { id: {
2005
+ type: "string",
2006
+ required: true,
2007
+ description: "要删除的工作流 id。"
2008
+ } },
2009
+ output: {
2010
+ schema: {
2011
+ type: "object",
2012
+ additionalProperties: false,
2013
+ properties: { deleted: {
2014
+ type: "boolean",
2015
+ required: true
2016
+ } }
2017
+ },
2018
+ render: (args, value) => {
2019
+ const v = value;
2020
+ const id = String(args.id ?? "");
2021
+ return [{
2022
+ type: "text",
2023
+ text: v.deleted ? `工作流「${id}」已删除。` : `工作流「${id}」不存在(已删除)。`
2024
+ }];
2025
+ }
2026
+ },
2027
+ async execute(args) {
2028
+ const id = String(args.id ?? "").trim();
2029
+ if (id === "") return { deleted: false };
2030
+ return service.deleteFlow(id);
2031
+ },
2032
+ presentCall: (args) => ({
2033
+ card: "generic",
2034
+ title: `workflow_delete: ${String(args.id ?? "")}`
2035
+ }),
2036
+ presentResult: () => ({ card: "generic" })
2037
+ }));
2038
+ }
2039
+ //#endregion
2040
+ //#region src/host/experience.ts
2041
+ /** 系统提示指引:让模型知道何时/如何用 workflow_* 全套工具(运行 + 增删改查 + 历史)。 */
2042
+ const SYSTEM_PROMPT_SECTION = "当用户明确要求运行工作流编辑器(auto-flow)里保存的确定性工作流时,先用 workflow_list 发现 id 与可传输入键,再用 workflow_run 按 id 运行并传入 args(按输入键覆盖工作流表单默认值)。工作流在本机确定性执行(浏览器脚本 / shell / HTTP / 直接模型调用 / AI 智能体节点);若含人工审批节点,会在会话里弹卡片请你通过/驳回。管理定义:workflow_read 读定义、workflow_save 建改定义(保存前校验,失败会报错说明要改什么)、workflow_delete 仅在用户明确要求删除时使用;workflow_runs 读运行历史(可按 workflowId / sessionId 过滤)排查失败。不要臆造 id、参数键或文件路径。";
2043
+ /** 注册系统提示指引与 /workflow 斜杠命令(对应宿主服务未组合时静默跳过)。 */
2044
+ function registerWorkflowExperience(ctx, service) {
2045
+ const systemPrompt = ctx.get("systemPrompt");
2046
+ if (systemPrompt !== void 0) ctx.effect(() => systemPrompt.section({
2047
+ name: "auto-flow:tools",
2048
+ order: 2600,
2049
+ text: SYSTEM_PROMPT_SECTION
2050
+ }), "auto-flow: 系统提示指引");
2051
+ const commands = ctx.get("commands");
2052
+ if (commands !== void 0) ctx.effect(() => commands.register({
2053
+ name: "workflow",
2054
+ description: "列出或运行工作流编辑器里保存的工作流(不带参数列出;/workflow <id> [key=value ...] 运行)",
2055
+ input: { hint: "<id> [key=value ...]" },
2056
+ recordInput: false,
2057
+ handler: (invocation) => service.runWorkflowCommand(invocation)
2058
+ }), "auto-flow: /workflow 命令");
2059
+ }
2060
+ function isImageMediaType(value) {
2061
+ return value === "image/png" || value === "image/jpeg" || value === "image/webp" || value === "image/gif";
2062
+ }
2063
+ /** 解析一条 `user/message` 事件为文本与图片引用;非普通用户消息返回 undefined。 */
2064
+ function parseUserTurn(event) {
2065
+ if (event.type !== "user/message") return void 0;
2066
+ const data = event.data;
2067
+ if (data === null || typeof data !== "object") return void 0;
2068
+ const record = data;
2069
+ if (record.source?.kind !== "user") return void 0;
2070
+ if (!Array.isArray(record.content)) return void 0;
2071
+ const images = [];
2072
+ const texts = [];
2073
+ for (const block of record.content) {
2074
+ if (block === null || typeof block !== "object") continue;
2075
+ const item = block;
2076
+ if (item.type === "text" && typeof item.text === "string" && item.text.length > 0) texts.push(item.text);
2077
+ if (item.type === "image") {
2078
+ const attachment = item.attachment;
2079
+ if (attachment === null || typeof attachment !== "object") continue;
2080
+ const ref = attachment;
2081
+ if (typeof ref.attachmentId !== "string" || !isImageMediaType(ref.mediaType)) continue;
2082
+ images.push({
2083
+ attachmentId: ref.attachmentId,
2084
+ mediaType: ref.mediaType
2085
+ });
2086
+ }
2087
+ }
2088
+ return {
2089
+ text: texts.join("\n"),
2090
+ images
2091
+ };
2092
+ }
2093
+ /** 读取最近一条普通用户回合,跳过不带图片的 `/workflow` 命令行;图片取自最新一条带图消息。 */
2094
+ function latestUserTurn(events) {
2095
+ let images;
2096
+ let text;
2097
+ for (let index = events.length - 1; index >= 0; index -= 1) {
2098
+ const turn = parseUserTurn(events[index]);
2099
+ if (turn === void 0) continue;
2100
+ const trimmed = turn.text.trim();
2101
+ if (turn.images.length === 0 && /^\/workflow(?:\s|$)/u.test(trimmed)) continue;
2102
+ if (images === void 0 && turn.images.length > 0) images = turn.images;
2103
+ if (text === void 0) {
2104
+ if (trimmed.length === 0) {} else if (usableTaskText(trimmed) !== void 0) text = trimmed;
2105
+ else text = "";
2106
+ }
2107
+ if (images !== void 0 && text !== void 0) break;
2108
+ }
2109
+ if (images === void 0 && text === void 0) return void 0;
2110
+ return {
2111
+ text: text ?? "",
2112
+ images: images ?? []
2113
+ };
2114
+ }
2115
+ /** 可安全注入 args.task 的用户文本:简短提问,非命令、非超长父会话长文。 */
2116
+ function usableTaskText(text) {
2117
+ const trimmed = text.trim();
2118
+ if (trimmed.length === 0) return void 0;
2119
+ if (/^\/workflow(?:\s|$)/u.test(trimmed)) return void 0;
2120
+ if (trimmed.length > 200) return void 0;
2121
+ return trimmed;
2122
+ }
2123
+ /** 该运行图是否读取 input.image(声明了 image 表单输入,或含图片识别节点)。 */
2124
+ function workflowAcceptsImageInput(inputs, nodes) {
2125
+ if (inputs.includes("image")) return true;
2126
+ return nodes.some((node) => (typeof node.data.nodeType === "string" ? node.data.nodeType : node.type) === VISION_NODE_TYPE);
2127
+ }
2128
+ /** 该运行图是否读取 input.task。 */
2129
+ function workflowAcceptsTaskInput(inputs) {
2130
+ return inputs.includes("task");
2131
+ }
2132
+ function isBlankArg(value) {
2133
+ return value === void 0 || value === null || typeof value === "string" && value.trim() === "";
2134
+ }
2135
+ /** 把最近一条用户回合补齐进缺失的 image/task;显式参数优先,无补填时原样返回 args。 */
2136
+ function mergeSessionRunArgs(inputs, nodes, args, turn, imagePath) {
2137
+ if (turn === void 0 && imagePath === void 0) return args;
2138
+ const next = { ...args };
2139
+ let changed = false;
2140
+ if (imagePath !== void 0 && imagePath.trim() !== "" && workflowAcceptsImageInput(inputs, nodes) && isBlankArg(next.image)) {
2141
+ next.image = imagePath;
2142
+ changed = true;
2143
+ }
2144
+ const task = turn === void 0 ? void 0 : usableTaskText(turn.text);
2145
+ if (task !== void 0 && workflowAcceptsTaskInput(inputs) && isBlankArg(next.task)) {
2146
+ next.task = task;
2147
+ changed = true;
2148
+ }
2149
+ if (!changed) return args;
2150
+ return next;
2151
+ }
2152
+ /** 把会话图片落盘为文件,返回供 vision 节点图片模板使用的绝对路径。 */
2153
+ async function materializeSessionImage(attachments, image, uploadsDir, signal) {
2154
+ const stored = await attachments.readImage(image, signal);
2155
+ await mkdir(uploadsDir, { recursive: true });
2156
+ const extension = stored.mediaType === "image/jpeg" ? "jpg" : stored.mediaType.slice(6);
2157
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace(/[:T]/g, "-");
2158
+ const file = path.resolve(uploadsDir, `${stamp}-${crypto.randomUUID().slice(0, 8)}.${extension}`);
2159
+ await writeFile(file, Buffer$1.from(stored.data));
2160
+ return file;
57
2161
  }
58
2162
  //#endregion
59
- //#region src/index.ts
2163
+ //#region src/host/service.ts
60
2164
  /**
61
- * dsh-auto-flow — Host 半(dsh 的 Node 进程内运行)。
62
- *
63
- * 通信与平台集成(业务开发按此分工):
64
- * - 重通道:AutoFlowService(Cordis 服务,@Remote 严格模式暴露给浏览器,
65
- * 契约由 `pnpm typert:gen` 生成——双向 zod 校验 + 完整类型)
66
- * - 轻执行也走 @Remote:`exec` 在宿主内经 ctx.shell(与 bash 工具同一执行器、不经过
67
- * 模型)执行;`runInBackground` 走 shell.start 自管后台进程 + `execRead`/`execKill` 消费。
68
- * - 模型工具:plugin_auto_flow(复用上面的服务;presentCall + presentResult)
69
- * - 斜杠命令:/auto-flow(ctx.commands 注册,用户键入后直接进 handler,不经过模型)
70
- * - 持久化:greetings 写入 domain storage(dsh-storage-domain,与 session 日志隔离;
71
- * 不追加自定义会话事件——harness 持久化读路径会拒绝未知事件类型,见 send 的说明)
72
-
73
- * - 事件:声明 + 发出 + 监听 `autoFlow/recorded`(跨插件实时通知 seam)
74
- * - 系统提示词:ctx.systemPrompt.section 贡献一条模型可见指引
75
- * - 设置:同一份 Config schema 注册为 settings namespace,UI 内可编辑、可 watch
76
- * - 生命周期:持久层异步打开走 `[Service.init]`(Cordis 类插件 init 钩子)
2165
+ * dsh-auto-flow 的 Host 半:服务类本体,负责把工作流能力挂到 ctx 上。
2166
+ * 能力按文件拆开:配置 → config.ts、领域 → domain.ts、执行引擎 → engine.ts、
2167
+ * 工作区文件 → workspace.ts。本文件只做「组装 + @Remote 方法」。
77
2168
  */
78
2169
  var __runInitializers = function(thisArg, initializers, value) {
79
2170
  var useValue = arguments.length > 2;
@@ -115,168 +2206,249 @@ var __esDecorate = function(ctor, descriptorIn, decorators, contextIn, initializ
115
2206
  if (target) Object.defineProperty(target, contextIn.name, descriptor);
116
2207
  done = true;
117
2208
  };
118
- /** 模型可见的工具名。 */
119
- const TOOL_NAME = "plugin_auto_flow";
120
- /** 斜杠命令名(小写、不带斜杠前缀)。 */
121
- const COMMAND_NAME = "auto-flow";
122
- /** 设置命名空间:与插件短名一致(小写字母开头,字母/数字/连字符)。 */
2209
+ /** 设置命名空间,与插件短名一致。 */
123
2210
  const SETTINGS_NS = "auto-flow";
124
- /** 默认问候前缀。 */
125
- const DEFAULT_PREFIX = "Hello";
126
- /** 单条 `exec` 命令允许的最大 UTF-8 字节长度(随部署变化,可经 Config / settings 覆盖)。 */
127
- const DEFAULT_MAX_COMMAND_BYTES = 1024;
128
- /** dsh-auto-flow 的领域错误:extends HarnessError 以便工具结果/重放保留失败类别。 */
129
- var GreetingError = class extends HarnessError {
130
- constructor(message, code) {
131
- super(message, code);
2211
+ /** 审批等待超时:超过此时长仍无人放行/驳回则自动取消该运行,回收内存。 */
2212
+ const APPROVAL_TIMEOUT_MS = 18e5;
2213
+ /** 调用工作流节点(硬→硬复用)的嵌套深度上限:防止工作流 A→B→A 无限递归。 */
2214
+ const MAX_WORKFLOW_DEPTH = 8;
2215
+ /** 追加进会话事件的节点输出/结果字符上限(会话日志卫生;domain 表仍存全量)。 */
2216
+ const SESSION_EVENT_OUTPUT_CAP = 2e3;
2217
+ /** 截断一条要进会话日志的 JSON 值(字符串截断,其余原样透传)。 */
2218
+ function capFlowValue(value) {
2219
+ if (typeof value === "string" && value.length > SESSION_EVENT_OUTPUT_CAP) return `${value.slice(0, SESSION_EVENT_OUTPUT_CAP)}…[已截断]`;
2220
+ return value;
2221
+ }
2222
+ /** 对追加进会话日志的运行事件做输出截断(仅截断 node-result.output 与 run-end.result 两个大字段)。 */
2223
+ function capSessionEvent(type, payload) {
2224
+ if (type === "auto-flow/node-result") {
2225
+ const p = payload;
2226
+ if (p.output !== void 0) return {
2227
+ ...payload,
2228
+ output: capFlowValue(p.output)
2229
+ };
132
2230
  }
133
- };
134
- /** 在边界处校验并归一化一份配置。 */
135
- function resolveConfig(config = {}) {
136
- const prefix = (config.prefix ?? DEFAULT_PREFIX).trim();
137
- if (prefix === "") throw new GreetingError("auto-flow: prefix 不能是空字符串", "GREETING_EMPTY_PREFIX");
138
- const maxCommandBytes = config.maxCommandBytes ?? DEFAULT_MAX_COMMAND_BYTES;
139
- if (!Number.isSafeInteger(maxCommandBytes) || maxCommandBytes < 1) throw new GreetingError(`auto-flow: maxCommandBytes 必须是正的安全整数,收到 ${String(maxCommandBytes)}`, "GREETING_INVALID_MAX_BYTES");
140
- return {
141
- prefix,
142
- maxCommandBytes
143
- };
2231
+ if (type === "auto-flow/run-end") {
2232
+ const p = payload;
2233
+ if (p.result !== void 0) return {
2234
+ ...payload,
2235
+ result: capFlowValue(p.result)
2236
+ };
2237
+ }
2238
+ return payload;
144
2239
  }
145
- /** 配置 schema:既是 Loader 校验的类静态 Config,也是 settings namespace 的 schema。 */
146
- const ConfigSchema = z.object({
147
- prefix: z.string().default(DEFAULT_PREFIX),
148
- maxCommandBytes: z.number().step(1).min(1).default(DEFAULT_MAX_COMMAND_BYTES)
149
- });
150
- /** 把 ShellRunResult 映射为客户端安全的 ExecForegroundResult(保留截断/沙箱事实)。 */
151
- function mapRunResult(result) {
2240
+ /** 派生一份工作流文档暴露给模型/命令的输入键(= 全部表单节点的字段键并集)。 */
2241
+ function workflowInputs(nodes) {
2242
+ const keys = /* @__PURE__ */ new Set();
2243
+ for (const node of nodes) {
2244
+ if ((typeof node.data.nodeType === "string" ? node.data.nodeType : node.type) !== "form") continue;
2245
+ for (const key of Object.keys(node.data)) {
2246
+ if (key === "nodeType" || key === "fields") continue;
2247
+ keys.add(key);
2248
+ }
2249
+ }
2250
+ return [...keys];
2251
+ }
2252
+ /** 读取调用方 agent 的会话日志(最近用户回合注入 image/task 的输入源)。 */
2253
+ function sessionEventsOf(agent) {
2254
+ if (agent === null || typeof agent !== "object") return void 0;
2255
+ const session = agent.session;
2256
+ if (session === void 0 || !Array.isArray(session.events)) return void 0;
2257
+ return session.events;
2258
+ }
2259
+ /** 读取 agent 句柄的会话 id(dsh 里 agent.id 即 session id,归属标签用)。 */
2260
+ function sessionIdOf(agent) {
2261
+ if (agent === null || typeof agent !== "object") return void 0;
2262
+ const id = agent.id;
2263
+ return typeof id === "string" ? id : void 0;
2264
+ }
2265
+ /** 读取 agent 句柄所属会话的工作目录(session.header.cwd,绝对路径)。 */
2266
+ function sessionCwdOf(agent) {
2267
+ if (agent === null || typeof agent !== "object") return void 0;
2268
+ const cwd = agent.session?.header?.cwd;
2269
+ return typeof cwd === "string" && cwd !== "" ? cwd : void 0;
2270
+ }
2271
+ /** 把运行参数合并进表单节点(args 覆盖表单默认值);返回浅拷贝后的画布。 */
2272
+ function applyRunArgs(flow, args) {
152
2273
  return {
153
- kind: "foreground",
154
- exitCode: result.exitCode,
155
- signal: result.signal,
156
- timedOut: result.timedOut,
157
- aborted: result.aborted,
158
- timeoutMs: result.timeoutMs,
159
- stdout: {
160
- text: result.stdout.text,
161
- truncated: result.stdout.truncated,
162
- ...result.stdout.spillPath !== void 0 ? { spillPath: result.stdout.spillPath } : {}
163
- },
164
- stderr: {
165
- text: result.stderr.text,
166
- truncated: result.stderr.truncated,
167
- ...result.stderr.spillPath !== void 0 ? { spillPath: result.stderr.spillPath } : {}
168
- },
169
- ...result.sandbox !== void 0 ? { sandbox: {
170
- mode: result.sandbox.mode,
171
- denied: result.sandbox.denied,
172
- ...result.sandbox.enforcement !== void 0 ? { enforcement: result.sandbox.enforcement } : {},
173
- ...result.sandbox.runnerFailed !== void 0 ? { runnerFailed: result.sandbox.runnerFailed } : {}
174
- } } : {}
2274
+ nodes: flow.nodes.map((node) => {
2275
+ if ((typeof node.data.nodeType === "string" ? node.data.nodeType : node.type) !== "form") return node;
2276
+ return {
2277
+ ...node,
2278
+ data: {
2279
+ ...node.data,
2280
+ ...args
2281
+ }
2282
+ };
2283
+ }),
2284
+ edges: flow.edges
175
2285
  };
176
2286
  }
177
- /** 后台进程增量读取:附上丢失/溢出提示(与 tool-bash 的 renderProcessRead 同构)。 */
178
- function renderProcessRead(read) {
179
- const notices = [];
180
- if (read.lossy) {
181
- const paths = [read.stdoutSpillPath, read.stderrSpillPath].filter((path) => path !== void 0);
182
- notices.push(`[部分输出被丢弃;完整输出:${paths.length > 0 ? paths.join(", ") : "(unavailable)"}]`);
183
- }
184
- if (notices.length === 0) return read.delta;
185
- return `${read.delta}${read.delta.length > 0 && !read.delta.endsWith("\n") ? "\n" : ""}${notices.join("\n")}`;
186
- }
2287
+ //#endregion
2288
+ //#region src/index.ts
187
2289
  /**
188
- * 示例服务:类本身就是插件——static inject / Config 是 Cordis 类插件的约定,
189
- * 构造器里的注册(工具/事件/设置/命令)随插件卸载自动回收。
190
- *
191
- * 平台服务消费方式(跨插件协作走 Cordis 服务、禁止互相 import):
192
- * - 必需依赖用 `static inject`(tools);
193
- * - 可选依赖用 `ctx.inject([...], cb)`(settings / commands / systemPrompt /
194
- * autoFlowFormatter)——服务缺失时插件照常加载;
195
- * - shell / sandboxPolicy 是可选服务:用 `ctx.get(...)` 读取,未组合时
196
- * `exec` 报错、插件其余部分照常工作;
197
- * - 持久层异步打开走 `[Service.init]`(storageDomain 可选:缺省退化为内存态)。
2290
+ * Host 半入口:只 default-export 服务供 Loader 装配。
2291
+ * 业务编排在 src/host/;tsdown host 段以此为 entry(连同 src/types.ts)。
198
2292
  */
199
- let AutoFlowService = (() => {
2293
+ var src_default = (() => {
200
2294
  let _classSuper = TypertRemoteService;
201
2295
  let _instanceExtraInitializers = [];
202
- let _send_decorators;
203
- let _list_decorators;
204
- let _last_decorators;
205
- let _exec_decorators;
206
- let _execRead_decorators;
207
- let _execKill_decorators;
2296
+ let _listFlows_decorators;
2297
+ let _listRuns_decorators;
2298
+ let _loadFlow_decorators;
2299
+ let _saveFlow_decorators;
2300
+ let _deleteFlow_decorators;
2301
+ let _listScripts_decorators;
2302
+ let _listLlmRoutes_decorators;
2303
+ let _listRouteModels_decorators;
2304
+ let _runFlow_decorators;
2305
+ let _runState_decorators;
2306
+ let _cancelRun_decorators;
2307
+ let _approveRun_decorators;
208
2308
  return class AutoFlowService extends _classSuper {
209
2309
  static {
210
2310
  const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
211
- _send_decorators = [Remote("send")];
212
- _list_decorators = [Remote("list")];
213
- _last_decorators = [Remote("last")];
214
- _exec_decorators = [Remote("exec")];
215
- _execRead_decorators = [Remote("execRead")];
216
- _execKill_decorators = [Remote("execKill")];
217
- __esDecorate(this, null, _send_decorators, {
2311
+ _listFlows_decorators = [Remote("listFlows")];
2312
+ _listRuns_decorators = [Remote("listRuns")];
2313
+ _loadFlow_decorators = [Remote("loadFlow")];
2314
+ _saveFlow_decorators = [Remote("saveFlow")];
2315
+ _deleteFlow_decorators = [Remote("deleteFlow")];
2316
+ _listScripts_decorators = [Remote("listScripts")];
2317
+ _listLlmRoutes_decorators = [Remote("listLlmRoutes")];
2318
+ _listRouteModels_decorators = [Remote("listRouteModels")];
2319
+ _runFlow_decorators = [Remote("runFlow")];
2320
+ _runState_decorators = [Remote("runState")];
2321
+ _cancelRun_decorators = [Remote("cancelRun")];
2322
+ _approveRun_decorators = [Remote("approveRun")];
2323
+ __esDecorate(this, null, _listFlows_decorators, {
2324
+ kind: "method",
2325
+ name: "listFlows",
2326
+ static: false,
2327
+ private: false,
2328
+ access: {
2329
+ has: (obj) => "listFlows" in obj,
2330
+ get: (obj) => obj.listFlows
2331
+ },
2332
+ metadata: _metadata
2333
+ }, null, _instanceExtraInitializers);
2334
+ __esDecorate(this, null, _listRuns_decorators, {
2335
+ kind: "method",
2336
+ name: "listRuns",
2337
+ static: false,
2338
+ private: false,
2339
+ access: {
2340
+ has: (obj) => "listRuns" in obj,
2341
+ get: (obj) => obj.listRuns
2342
+ },
2343
+ metadata: _metadata
2344
+ }, null, _instanceExtraInitializers);
2345
+ __esDecorate(this, null, _loadFlow_decorators, {
2346
+ kind: "method",
2347
+ name: "loadFlow",
2348
+ static: false,
2349
+ private: false,
2350
+ access: {
2351
+ has: (obj) => "loadFlow" in obj,
2352
+ get: (obj) => obj.loadFlow
2353
+ },
2354
+ metadata: _metadata
2355
+ }, null, _instanceExtraInitializers);
2356
+ __esDecorate(this, null, _saveFlow_decorators, {
2357
+ kind: "method",
2358
+ name: "saveFlow",
2359
+ static: false,
2360
+ private: false,
2361
+ access: {
2362
+ has: (obj) => "saveFlow" in obj,
2363
+ get: (obj) => obj.saveFlow
2364
+ },
2365
+ metadata: _metadata
2366
+ }, null, _instanceExtraInitializers);
2367
+ __esDecorate(this, null, _deleteFlow_decorators, {
2368
+ kind: "method",
2369
+ name: "deleteFlow",
2370
+ static: false,
2371
+ private: false,
2372
+ access: {
2373
+ has: (obj) => "deleteFlow" in obj,
2374
+ get: (obj) => obj.deleteFlow
2375
+ },
2376
+ metadata: _metadata
2377
+ }, null, _instanceExtraInitializers);
2378
+ __esDecorate(this, null, _listScripts_decorators, {
218
2379
  kind: "method",
219
- name: "send",
2380
+ name: "listScripts",
220
2381
  static: false,
221
2382
  private: false,
222
2383
  access: {
223
- has: (obj) => "send" in obj,
224
- get: (obj) => obj.send
2384
+ has: (obj) => "listScripts" in obj,
2385
+ get: (obj) => obj.listScripts
225
2386
  },
226
2387
  metadata: _metadata
227
2388
  }, null, _instanceExtraInitializers);
228
- __esDecorate(this, null, _list_decorators, {
2389
+ __esDecorate(this, null, _listLlmRoutes_decorators, {
229
2390
  kind: "method",
230
- name: "list",
2391
+ name: "listLlmRoutes",
231
2392
  static: false,
232
2393
  private: false,
233
2394
  access: {
234
- has: (obj) => "list" in obj,
235
- get: (obj) => obj.list
2395
+ has: (obj) => "listLlmRoutes" in obj,
2396
+ get: (obj) => obj.listLlmRoutes
236
2397
  },
237
2398
  metadata: _metadata
238
2399
  }, null, _instanceExtraInitializers);
239
- __esDecorate(this, null, _last_decorators, {
2400
+ __esDecorate(this, null, _listRouteModels_decorators, {
240
2401
  kind: "method",
241
- name: "last",
2402
+ name: "listRouteModels",
242
2403
  static: false,
243
2404
  private: false,
244
2405
  access: {
245
- has: (obj) => "last" in obj,
246
- get: (obj) => obj.last
2406
+ has: (obj) => "listRouteModels" in obj,
2407
+ get: (obj) => obj.listRouteModels
247
2408
  },
248
2409
  metadata: _metadata
249
2410
  }, null, _instanceExtraInitializers);
250
- __esDecorate(this, null, _exec_decorators, {
2411
+ __esDecorate(this, null, _runFlow_decorators, {
251
2412
  kind: "method",
252
- name: "exec",
2413
+ name: "runFlow",
253
2414
  static: false,
254
2415
  private: false,
255
2416
  access: {
256
- has: (obj) => "exec" in obj,
257
- get: (obj) => obj.exec
2417
+ has: (obj) => "runFlow" in obj,
2418
+ get: (obj) => obj.runFlow
258
2419
  },
259
2420
  metadata: _metadata
260
2421
  }, null, _instanceExtraInitializers);
261
- __esDecorate(this, null, _execRead_decorators, {
2422
+ __esDecorate(this, null, _runState_decorators, {
262
2423
  kind: "method",
263
- name: "execRead",
2424
+ name: "runState",
264
2425
  static: false,
265
2426
  private: false,
266
2427
  access: {
267
- has: (obj) => "execRead" in obj,
268
- get: (obj) => obj.execRead
2428
+ has: (obj) => "runState" in obj,
2429
+ get: (obj) => obj.runState
269
2430
  },
270
2431
  metadata: _metadata
271
2432
  }, null, _instanceExtraInitializers);
272
- __esDecorate(this, null, _execKill_decorators, {
2433
+ __esDecorate(this, null, _cancelRun_decorators, {
273
2434
  kind: "method",
274
- name: "execKill",
2435
+ name: "cancelRun",
275
2436
  static: false,
276
2437
  private: false,
277
2438
  access: {
278
- has: (obj) => "execKill" in obj,
279
- get: (obj) => obj.execKill
2439
+ has: (obj) => "cancelRun" in obj,
2440
+ get: (obj) => obj.cancelRun
2441
+ },
2442
+ metadata: _metadata
2443
+ }, null, _instanceExtraInitializers);
2444
+ __esDecorate(this, null, _approveRun_decorators, {
2445
+ kind: "method",
2446
+ name: "approveRun",
2447
+ static: false,
2448
+ private: false,
2449
+ access: {
2450
+ has: (obj) => "approveRun" in obj,
2451
+ get: (obj) => obj.approveRun
280
2452
  },
281
2453
  metadata: _metadata
282
2454
  }, null, _instanceExtraInitializers);
@@ -287,121 +2459,39 @@ let AutoFlowService = (() => {
287
2459
  value: _metadata
288
2460
  });
289
2461
  }
290
- /** 依赖的服务;缺失时 Loader 会等待其出现。 */
291
- static inject = ["tools"];
292
- /** Config 的 Schema(Cordis 类插件约定:Loader 读类静态 `Config` 校验注入并补齐默认值;
293
- * 模块级同名导出不会被 Loader 识别——生成插件请把 Schema 放在类上)。 */
2462
+ /** Config 的 Schema。Cordis 约定:Loader 读类静态 Config,请放在类上(模块级同名导出不会被识别)。 */
294
2463
  static Config = ConfigSchema;
295
2464
  /** 校验后的配置(settings scope 可在运行时覆盖,故非 readonly)。 */
296
2465
  resolved = __runInitializers(this, _instanceExtraInitializers);
297
- /** 问候的内存视图(domain storage 是持久层)。 */
298
- greetings = [];
299
- /** 已打开的持久化领域;无后端时保持 null(内存态兜底)。 */
2466
+ /** 已打开的持久化领域;未组合 storageDomain 时保持 null。 */
300
2467
  storage = null;
301
- /** 第二包提供的格式化服务;未安装第二包时保持 null(可选依赖)。 */
302
- formatter = null;
303
- /** 最近一次提交的问候(由 `autoFlow/recorded` 监听器维护,供 @Remote last)。 */
304
- lastRecorded = null;
305
- /** 后台进程句柄(@Remote 轻执行自管;不依赖 ctx.jobs,见 exec 说明)。 */
306
- background = /* @__PURE__ */ new Map();
307
- nextBgId = 1;
308
- nextId = 1;
2468
+ /** 运行中的工作流运行时(runId → 状态 + 审批放行句柄);终态后回收。 */
2469
+ runRuntimes = /* @__PURE__ */ new Map();
2470
+ /** 进行中运行的中止控制器(runId → AbortController);cancelRun 触发,终态回收。 */
2471
+ runControllers = /* @__PURE__ */ new Map();
2472
+ /** 插件自持的运行日志(追加式按天 JSONL;终态 settled 记录只写一次)。 */
2473
+ runStore;
2474
+ /** 已追加进运行日志的 runId(终态 append 幂等守卫)。 */
2475
+ persistedRunIds = /* @__PURE__ */ new Set();
2476
+ /** 进行中的终态追加(runId → append Promise);结算返回前 await 它保证落盘先于结算。 */
2477
+ pendingPersists = /* @__PURE__ */ new Map();
309
2478
  constructor(ctx, config = {}) {
310
2479
  super(ctx, "autoFlow");
311
2480
  this.resolved = resolveConfig(config);
312
- this.registerTool(ctx);
2481
+ this.rebuildRunStore();
313
2482
  this.registerSettings(ctx, config);
314
- this.registerCommand(ctx);
315
- this.registerSystemPrompt(ctx);
316
- this.registerCore(ctx);
317
- this.registerEvents(ctx);
318
- this.registerBackgroundCleanup(ctx);
2483
+ this.registerStorageCleanup(ctx);
2484
+ registerAutoFlowTools(ctx, this);
2485
+ registerWorkflowExperience(ctx, this);
319
2486
  }
320
- /**
321
- * 异步初始化钩子(Cordis 类插件约定):构造完成后运行。持久层在这里打开——
322
- * 与 @deepseek-ai/dsh-message-feedback 的 `[Service.init]` 同构;storageDomain
323
- * 是可选服务,缺失时保持内存态兜底。
324
- */
2487
+ /** 异步初始化:打开持久层。storageDomain 是本插件硬依赖——缺失时大声失败,
2488
+ * 不静默退化成内存态(否则 saveFlow 假装成功但重启即丢数据,是数据丢失陷阱)。 */
325
2489
  async [Service.init]() {
326
2490
  const storageDomain = this.ctx.get("storageDomain");
327
- if (storageDomain === void 0) return;
328
- try {
329
- this.storage = await storageDomain.open(GreetingDomain);
330
- this.hydrateFromStorage();
331
- } catch {}
332
- }
333
- /** 模型工具:复用 @Remote send;presentCall + presentResult 双态展示。 */
334
- registerTool(ctx) {
335
- ctx.tools.register(defineTool({
336
- name: TOOL_NAME,
337
- description: "追加一条问候记录,返回新记录与当前总数。",
338
- parameters: { text: {
339
- type: "string",
340
- required: true,
341
- description: "问候内容"
342
- } },
343
- output: {
344
- schema: {
345
- type: "object",
346
- additionalProperties: false,
347
- properties: {
348
- hello: {
349
- type: "object",
350
- required: true,
351
- additionalProperties: false,
352
- properties: {
353
- id: {
354
- type: "integer",
355
- required: true
356
- },
357
- text: {
358
- type: "string",
359
- required: true
360
- },
361
- createdAt: {
362
- type: "integer",
363
- required: true
364
- }
365
- }
366
- },
367
- total: {
368
- type: "integer",
369
- required: true
370
- }
371
- }
372
- },
373
- render: (_args, value) => [{
374
- type: "text",
375
- text: `已记录:${value.hello.text}(共 ${value.total} 条)`
376
- }]
377
- },
378
- presentCall: (args) => ({
379
- card: "generic",
380
- title: "记录问候",
381
- kind: "other",
382
- rawInput: args.text
383
- }),
384
- presentResult: (_args, result) => {
385
- const block = result.content[0];
386
- if (block === void 0 || block.type !== "text") return void 0;
387
- return {
388
- card: "generic",
389
- title: "问候已记录",
390
- content: [{
391
- type: "text",
392
- text: block.text
393
- }]
394
- };
395
- },
396
- execute: async (args) => {
397
- return {
398
- hello: await this.send(args.text),
399
- total: this.greetings.length
400
- };
401
- }
402
- }));
2491
+ if (storageDomain === void 0) throw new Error("auto-flow: 缺少 storageDomain 服务,无法持久化工作流与运行记录;请确保已装配 @deepseek-ai/dsh-storage-domain(本插件不提供内存兜底)");
2492
+ this.storage = await storageDomain.open(AutoFlowDomain);
403
2493
  }
404
- /** 设置:同一份 Config schema 注册为可编辑 namespace,入口配置作为 base 层。 */
2494
+ /** 把同一份 Config schema 注册为可编辑的 settings namespace。 */
405
2495
  registerSettings(ctx, config) {
406
2496
  ctx.inject(["settings"], (settingsCtx) => {
407
2497
  let readConfig = () => config;
@@ -414,179 +2504,584 @@ let AutoFlowService = (() => {
414
2504
  },
415
2505
  onChange: () => {
416
2506
  this.resolved = resolveConfig(readConfig());
2507
+ this.rebuildRunStore();
417
2508
  }
418
2509
  });
419
2510
  });
420
2511
  }
421
- /** 从持久化领域恢复内存视图与自增 id。 */
422
- hydrateFromStorage() {
423
- if (this.storage === null) return;
424
- const table = this.storage.table("greetings");
425
- const restored = [];
426
- let maxId = 0;
427
- for (const [, greeting] of table.entries()) {
428
- restored.push(greeting);
429
- if (greeting.id > maxId) maxId = greeting.id;
430
- }
431
- restored.sort((a, b) => a.id - b.id);
432
- this.greetings.length = 0;
433
- this.greetings.push(...restored);
434
- this.nextId = maxId + 1;
435
- invariant(this.greetings.length === restored.length, "水合后的内存视图应与恢复记录一致");
436
- }
437
- /** 斜杠命令:用户键入 /auto-flow 后直接进 handler(不经过模型)。 */
438
- registerCommand(ctx) {
439
- ctx.inject(["commands"], (commandsCtx) => {
440
- commandsCtx.commands.register({
441
- name: COMMAND_NAME,
442
- description: "记录一条问候并返回当前总数",
443
- input: { hint: "问候内容" },
444
- handler: async (invocation) => {
445
- const text = invocation.rawInput.trim();
446
- if (text === "") return {
447
- kind: "error",
448
- text: "问候内容不能为空"
449
- };
450
- return {
451
- kind: "success",
452
- text: `已记录:${(await this.send(text)).text}(共 ${this.greetings.length} 条)`
453
- };
454
- }
455
- });
456
- });
2512
+ /** 卸载时中止在飞运行并关闭已打开的领域(Domain 句柄由调用方持有并按 effect 回收)。 */
2513
+ registerStorageCleanup(ctx) {
2514
+ ctx.effect(() => () => {
2515
+ for (const controller of this.runControllers.values()) controller.abort();
2516
+ this.runControllers.clear();
2517
+ return this.storage?.close();
2518
+ }, "auto-flow: 中止在飞运行并关闭持久化领域");
457
2519
  }
458
- /** 系统提示词:贡献一条模型可见指引(name 唯一、order 升序拼接)。 */
459
- registerSystemPrompt(ctx) {
460
- ctx.inject(["systemPrompt"], (promptCtx) => {
461
- promptCtx.systemPrompt.section({
462
- name: "auto-flow",
463
- order: 900,
464
- text: "auto-flow: 问候通过模型工具 plugin_auto_flow 或斜杠命令 /auto-flow 追加,落插件 domain storage;exec 轻执行输出只回调用方、不进会话日志。"
465
- });
466
- });
2520
+ /** 依据当前 resolved 配置重建运行日志(workspaceDir / 保留期变更时随之重建)。 */
2521
+ rebuildRunStore() {
2522
+ this.runStore = new WorkflowRunStore(path.resolve(this.resolved.workspaceDir, "runs"), this.resolved.runLogRetentionDays, this.ctx.logger);
467
2523
  }
468
- /** 事件:监听自身域事件维护 lastRecorded 缓存(演示 ctx.on —— 监听器随 fiber 回收)。
469
- * 其它插件也可 ctx.on('autoFlow/recorded', ...) 订阅,实现跨插件实时通知。 */
470
- registerEvents(ctx) {
471
- ctx.on("autoFlow/recorded", ({ greeting }) => {
472
- this.lastRecorded = greeting;
2524
+ /** 列举全部工作流摘要(id + 名称 + 描述 + 节点数 + 输入键;按 id 升序)。 */
2525
+ listFlows() {
2526
+ if (this.storage === null) return Promise.resolve([]);
2527
+ const rows = [...this.storage.table("flows").entries()].map(([id, stored]) => {
2528
+ const def = decodeWorkflowDefinition(id, stored);
2529
+ return {
2530
+ id,
2531
+ name: def.name,
2532
+ description: def.description,
2533
+ nodeCount: def.nodes.length,
2534
+ inputs: workflowInputs(def.nodes)
2535
+ };
473
2536
  });
2537
+ rows.sort((a, b) => a.id.localeCompare(b.id));
2538
+ return Promise.resolve(rows);
474
2539
  }
475
- /** 卸载时回收所有未结束的后台进程(@Remote 轻执行自管句柄,必须随 fiber 清理)。 */
476
- registerBackgroundCleanup(ctx) {
477
- ctx.effect(() => () => {
478
- for (const proc of this.background.values()) proc.kill();
479
- this.background.clear();
480
- }, "auto-flow: 后台进程清理");
481
- }
482
- /** 跨插件服务消费:第二包(dsh-auto-flow-core)可选提供 autoFlowFormatter。
483
- * 用 ctx.inject 而非 static inject——第二包未安装时插件照常加载(send 走无格式化兜底)。 */
484
- registerCore(ctx) {
485
- ctx.inject(["autoFlowFormatter"], (coreCtx) => {
486
- this.formatter = coreCtx.get("autoFlowFormatter");
2540
+ /** 列举历史运行摘要(按 updatedAt 倒序,来自插件自持运行日志;支持按会话/工作流过滤,含归属标签)。
2541
+ * @param request - 可选过滤:发起会话与工作流(两个条件可同时使用)。 */
2542
+ async listRuns(request) {
2543
+ return (await this.runStore.list({
2544
+ ...request.sessionId !== void 0 ? { sessionId: request.sessionId } : {},
2545
+ ...request.workflowId !== void 0 ? { workflowId: request.workflowId } : {}
2546
+ })).map((state) => ({
2547
+ runId: state.runId,
2548
+ workflowId: state.workflowId,
2549
+ name: state.name,
2550
+ ...state.sessionId !== void 0 ? { sessionId: state.sessionId } : {},
2551
+ status: state.status,
2552
+ nodeCount: state.steps.length,
2553
+ createdAt: state.createdAt,
2554
+ updatedAt: state.updatedAt
2555
+ }));
2556
+ }
2557
+ /** 按 id 读取工作流文档(不存在或未组合 storageDomain 时返回 null)。 */
2558
+ loadFlow(flowId) {
2559
+ if (this.storage === null) return Promise.resolve(null);
2560
+ const stored = this.storage.table("flows").get(flowId);
2561
+ return Promise.resolve(stored === void 0 ? null : decodeWorkflowDefinition(flowId, stored));
2562
+ }
2563
+ /** 按 id 保存工作流文档(domain 打开时落盘,否则仅返回原样;失败大声抛出)。 */
2564
+ async saveFlow(flowId, flow) {
2565
+ if (this.storage !== null) await this.storage.table("flows").put(flowId, flow);
2566
+ return flow;
2567
+ }
2568
+ /** 按 id 删除工作流。 */
2569
+ async deleteFlow(flowId) {
2570
+ if (this.storage === null) return { deleted: false };
2571
+ return { deleted: await this.storage.table("flows").delete(flowId) };
2572
+ }
2573
+ /** 列出可运行脚本清单(展示名/描述/参数/产物装配规则;playwright-ag 为准,缺失时返回空)。 */
2574
+ async listScripts() {
2575
+ const ag = this.ctx.get("playwrightAg");
2576
+ if (ag === void 0) return [];
2577
+ return ag.listScripts();
2578
+ }
2579
+ /** 列出实时 LLM 模型路由(llm/vision/agent 节点的 provider 下拉)。 */
2580
+ listLlmRoutes() {
2581
+ const llm = this.ctx.get("llm");
2582
+ if (llm === void 0) return { routes: [] };
2583
+ return { routes: llm.listProviders().map((route) => ({
2584
+ provider: route.id,
2585
+ displayName: route.name
2586
+ })) };
2587
+ }
2588
+ /** 列出某实时模型路由上的模型目录(llm/vision/agent 节点的 model 下拉)。 */
2589
+ async listRouteModels(request) {
2590
+ const llm = this.ctx.get("llm");
2591
+ if (llm === void 0) return { models: [] };
2592
+ const provider = request.provider.trim();
2593
+ if (provider === "") return { models: [] };
2594
+ try {
2595
+ return { models: (await llm.listModels(provider)).map((model) => ({
2596
+ id: model.id,
2597
+ name: model.name
2598
+ })) };
2599
+ } catch (err) {
2600
+ return {
2601
+ models: [],
2602
+ error: err instanceof Error ? err.message : String(err)
2603
+ };
2604
+ }
2605
+ }
2606
+ /**
2607
+ * 启动一次工作流运行(@Remote,画布触发):按层级并发执行 表单→脚本→审批,审批经画布 approveRun/cancelRun 放行。
2608
+ * 返回 runId 即返回,进度经 runState 轮询;校验失败抛 invalid-flow(含诊断项)。
2609
+ * sessionId 用于把产物落到「当前会话工作区」(缺省回退配置 workspaceDir)。
2610
+ */
2611
+ async runFlow(request) {
2612
+ const workspaceRoot = this.resolveWorkspaceRoot(request.sessionId);
2613
+ const { runId, promise } = await this.startRun(request.flow, (runtime, step, options) => this.canvasWaitApproval(runtime, step, options), {
2614
+ ...request.workflowId !== void 0 ? { workflowId: request.workflowId } : {},
2615
+ ...request.name !== void 0 ? { name: request.name } : {},
2616
+ ...request.sessionId !== void 0 ? { sessionId: request.sessionId } : {},
2617
+ workspaceRoot
487
2618
  });
2619
+ return { runId };
2620
+ }
2621
+ /** 解析本次运行应写入的工作区根目录:优先触发会话的 header.cwd,否则配置 workspaceDir 兜底。 */
2622
+ resolveWorkspaceRoot(sessionId) {
2623
+ if (sessionId !== void 0) {
2624
+ const cwd = this.ctx.get("sessions")?.get(sessionId)?.header?.cwd;
2625
+ if (cwd !== void 0 && cwd !== "") return cwd;
2626
+ }
2627
+ return this.resolved.workspaceDir;
488
2628
  }
489
2629
  /**
490
- * 追加一条问候并返回记录(写透 domain storage;若有第二包则先格式化;提交后发域事件)。
491
- *
492
- * 不追加自定义会话事件:harness 的 session 持久化读路径会拒绝 `KNOWN_SESSION_EVENT_TYPES`
493
- * 之外的事件类型(除非信封标记 `ignorable`,而 `Session.append` 无此参数),把插件
494
- * 自定义事件写进会话日志会导致该会话在重启后无法打开。
2630
+ * 启动一次运行并返回结算句柄(宿主内部,供 @Remote runFlow 与模型工具共用)。
2631
+ * @param flow 画布(本方法负责校验)。
2632
+ * @param waitApproval 审批等待器工厂 (runtime, step, options) => Promise<string>(返回选中的选项标签)。
2633
+ * @param runContext 运行归属与触发方:workflowId/name/sessionId 是运行记录归属标签;
2634
+ * workspaceRoot 是产物根目录(缺省配置 workspaceDir);有 parent 且部署有 subagents 时
2635
+ * AI 智能体节点才可用;recordSession 存在时把 auto-flow/* 运行事实追加为会话事件(会话进度);
2636
+ * depth 是调用工作流节点的嵌套深度。
495
2637
  */
496
- async send(text) {
497
- const raw = this.resolved.prefix + text;
498
- const greeting = {
499
- id: this.nextId,
500
- text: this.formatter !== null ? this.formatter.format(raw) : raw,
501
- createdAt: Date.now()
2638
+ async startRun(flow, waitApproval, runContext = {}) {
2639
+ const ag = this.ctx.get("playwrightAg");
2640
+ if (ag === void 0) throw new RemoteError("autoFlow/playwright-missing", "运行工作流需要组合 playwright-ag 插件", {});
2641
+ const scripts = await ag.listScripts();
2642
+ const labelByScript = new Map(scripts.map((s) => [s.name, s.label]));
2643
+ const artifactsByScript = new Map(scripts.flatMap((s) => s.artifacts !== void 0 ? [[s.name, s.artifacts]] : []));
2644
+ const validation = validateFlow(flow, new Map(scripts.map((s) => [s.name, s.params])));
2645
+ if (!validation.valid) {
2646
+ const detail = validation.diagnostics.map((d) => d.message).join(";");
2647
+ throw new RemoteError("autoFlow/invalid-flow", `工作流校验失败:${detail}`, { diagnostics: validation.diagnostics });
2648
+ }
2649
+ const runId = `run-${crypto.randomUUID()}`;
2650
+ const workspaceDir = path.resolve(runContext.workspaceRoot ?? this.resolved.workspaceDir, runId);
2651
+ const state = {
2652
+ runId,
2653
+ workflowId: runContext.workflowId ?? "",
2654
+ name: runContext.name ?? "",
2655
+ ...runContext.sessionId !== void 0 ? { sessionId: runContext.sessionId } : {},
2656
+ status: "running",
2657
+ workspaceDir,
2658
+ steps: buildRunSteps(flow, (script) => labelByScript.get(script) ?? script),
2659
+ createdAt: Date.now(),
2660
+ updatedAt: Date.now()
2661
+ };
2662
+ const runtime = {
2663
+ state,
2664
+ delegatedApprovals: []
2665
+ };
2666
+ this.runRuntimes.set(runId, runtime);
2667
+ const rootRuntime = runContext.rootRuntime ?? runtime;
2668
+ const runController = new AbortController();
2669
+ this.runControllers.set(runId, runController);
2670
+ const externalSignal = runContext.signal;
2671
+ if (externalSignal !== void 0) {
2672
+ if (externalSignal.aborted) runController.abort();
2673
+ else externalSignal.addEventListener("abort", () => runController.abort(), { once: true });
2674
+ }
2675
+ const runSignal = runController.signal;
2676
+ const subagents = this.ctx.get("subagents");
2677
+ const agents = this.ctx.get("agents");
2678
+ const parent = runContext.parent;
2679
+ let bareParent;
2680
+ const resolveParent = async () => {
2681
+ if (parent !== void 0) return parent;
2682
+ if (agents === void 0) return void 0;
2683
+ if (bareParent === void 0) try {
2684
+ bareParent = await agents.create({
2685
+ sessionId: `auto-flow:${runId}`,
2686
+ meta: {
2687
+ cwd: workspaceDir,
2688
+ delegationDepth: 0
2689
+ }
2690
+ });
2691
+ } catch (err) {
2692
+ throw new Error(`无法为画布运行创建工作流父代理:${err instanceof Error ? err.message : String(err)}`);
2693
+ }
2694
+ return bareParent.agent;
2695
+ };
2696
+ const agentFace = subagents !== void 0 && (parent !== void 0 || agents !== void 0) ? { run: async (task, options) => {
2697
+ try {
2698
+ const resolved = await resolveParent();
2699
+ if (resolved === void 0) return {
2700
+ ok: false,
2701
+ error: "AI 智能体节点需要子代理能力,但当前部署未组合 agents 服务且非 agent 触发"
2702
+ };
2703
+ return await this.delegateToAgent(subagents, resolved, task, options ?? {}, runContext.signal);
2704
+ } catch (err) {
2705
+ return {
2706
+ ok: false,
2707
+ error: err instanceof Error ? err.message : String(err)
2708
+ };
2709
+ }
2710
+ } } : void 0;
2711
+ return {
2712
+ runId,
2713
+ promise: new WorkflowEngine({
2714
+ runScript: async (script, payload, options) => {
2715
+ const { signal, ...runOptions } = options;
2716
+ const result = await ag.runScript(script, payload, runOptions, signal);
2717
+ return result.ok ? {
2718
+ ok: true,
2719
+ data: result.data,
2720
+ durationMs: result.meta.durationMs
2721
+ } : {
2722
+ ok: false,
2723
+ error: result.error.message,
2724
+ code: result.error.code,
2725
+ durationMs: result.meta.durationMs
2726
+ };
2727
+ },
2728
+ persist: (s) => void this.persistRun(s),
2729
+ waitApproval: (step, options) => waitApproval(runtime, step, options),
2730
+ resolveArtifacts: (script) => artifactsByScript.get(script),
2731
+ llm: this.ctx.get("llm"),
2732
+ shell: this.ctx.get("shell"),
2733
+ attachments: this.ctx.get("attachments"),
2734
+ agentDefaultModel: this.ctx.get("agentDefaultModel"),
2735
+ agent: agentFace,
2736
+ runWorkflow: (workflowId, args) => this.runNestedWorkflow(workflowId, args, rootRuntime, parent, runContext.recordSession, runSignal, runContext.depth ?? 0),
2737
+ signal: runSignal,
2738
+ emit: (type, payload) => {
2739
+ try {
2740
+ this.ctx.emit(type, payload);
2741
+ } catch {}
2742
+ const recordSession = runContext.recordSession;
2743
+ if (recordSession !== void 0) try {
2744
+ recordSession.append(type, capSessionEvent(type, payload));
2745
+ } catch {}
2746
+ }
2747
+ }).run(state, flow).then(async () => {
2748
+ await this.persistRun(state);
2749
+ const result = runResultOf(state);
2750
+ const failed = state.steps.find((s) => s.status === "failed");
2751
+ return {
2752
+ runId: state.runId,
2753
+ ok: state.status === "completed",
2754
+ ...result !== void 0 ? { result } : {},
2755
+ ...failed?.error !== void 0 ? { error: failed.error } : {},
2756
+ steps: state.steps
2757
+ };
2758
+ }).catch(async (err) => {
2759
+ const message = err instanceof Error ? err.message : String(err);
2760
+ this.failRun(runtime, message);
2761
+ await this.persistRun(state);
2762
+ return {
2763
+ runId: state.runId,
2764
+ ok: false,
2765
+ error: message,
2766
+ steps: state.steps
2767
+ };
2768
+ }).finally(() => {
2769
+ if (bareParent !== void 0) bareParent.dispose();
2770
+ })
502
2771
  };
503
- invariant(Number.isSafeInteger(greeting.id) && greeting.id >= 1, "greeting id 必须是正安全整数");
504
- this.nextId += 1;
505
- this.greetings.push(greeting);
506
- if (this.storage !== null) try {
507
- await this.storage.table("greetings").put(String(greeting.id), greeting);
508
- await this.storage.global.set({ lastGreetedAt: greeting.createdAt });
509
- } catch {}
510
- this.ctx.emit("autoFlow/recorded", { greeting });
511
- return greeting;
512
- }
513
- /** 返回全部问候记录(内存视图,已由 domain storage 水合)。 */
514
- list() {
515
- return Promise.resolve([...this.greetings]);
516
- }
517
- /** 返回最近一次提交的问候(由域事件监听器维护),首次提交前为 null。 */
518
- last() {
519
- return Promise.resolve(this.lastRecorded);
2772
+ }
2773
+ /** 画布审批等待器:挂起直至 approveRun/cancelRun 放行;30 分钟无人处理则自动取消。 */
2774
+ canvasWaitApproval(runtime, _step, _options) {
2775
+ return new Promise((resolve) => {
2776
+ runtime.approveResolve = resolve;
2777
+ runtime.approvalTimer = setTimeout(() => {
2778
+ runtime.approveResolve = void 0;
2779
+ runtime.approvalTimer = void 0;
2780
+ if (runtime.state.status === "waiting") {
2781
+ runtime.state.status = "cancelled";
2782
+ runtime.state.updatedAt = Date.now();
2783
+ this.persistRun(runtime.state);
2784
+ }
2785
+ resolve("");
2786
+ }, APPROVAL_TIMEOUT_MS);
2787
+ });
2788
+ }
2789
+ /** 审批提问等待器:经 userQuestions 交互卡片让用户选择;无交互 UI 时回退画布审批。 */
2790
+ async chatWaitApproval(runtime, step, agent, signal, options) {
2791
+ const userQuestions = this.ctx.get("userQuestions");
2792
+ if (userQuestions === void 0) return this.canvasWaitApproval(runtime, step, options);
2793
+ const abortLabel = options.find((o) => o.action === "abort")?.label ?? "";
2794
+ try {
2795
+ return (await userQuestions.ask({
2796
+ questions: [{
2797
+ id: "approve",
2798
+ question: `审批节点「${step.label}」:请选择`,
2799
+ header: "工作流审批",
2800
+ options: options.map((o) => ({ label: o.label }))
2801
+ }],
2802
+ agent,
2803
+ ...signal !== void 0 ? { signal } : {}
2804
+ })).answers[0]?.selected?.[0] ?? abortLabel;
2805
+ } catch {
2806
+ return abortLabel;
2807
+ }
520
2808
  }
521
2809
  /**
522
- * 执行一条命令行(宿主内经 ctx.shell,与 bash 工具同一执行器,绝不经过模型)。
523
- * 输出只回调用方、不进会话日志。@Remote 严格契约保证浏览器端完整类型与校验。
524
- *
525
- * 相比模型工具(@deepseek-ai/dsh-tool-bash)的 shell 面,@Remote 轻执行只能拿到
526
- * 能映射过来的子集:timeoutMs / stdoutMaxBytes / sandboxPolicy / 后台任务 / 结果保真。
527
- * 模型工具专属的 seam(shellEnv.collect 需要 ToolExecution、sandbox 升级需要 approval
528
- * + callId、abort 需要 ToolExecution.signal)不适用于 @Remote,此处不做。
529
- *
530
- * 注意:@Remote 方法不能带 `agent: Agent` 参数——第三方仓库的 typert 生成器无法解析
531
- * `agent` lookup(其 wire 类型 SessionId 是 branded 类型,官方 monorepo 内才可用)。
532
- * 因此沙箱策略退化为「部署默认」(`sandboxPolicy.resolve()` 无 session)。
533
- *
534
- * 后台执行不走 ctx.jobs(那是给模型工具 job_output/job_kill 用的,需要 agent owner +
535
- * dsh-tool-jobs 控制器,@Remote 里拿不到):直接用 `shell.start()` 拿 ShellProcess
536
- * 句柄,自管在 this.background Map 里,execRead/execKill 直接操作句柄;卸载时统一 kill。
537
- *
538
- * shell / sandboxPolicy 是可选依赖:未组合时抛对应 RemoteError,插件其余照常。
2810
+ * 调用工作流节点(硬→硬复用)的嵌套运行:加载目标工作流、合并 args、复用本运行的
2811
+ * parent/session/signal 再跑一次 startRun,并把其 Output 值作为节点结果回灌。
2812
+ * @param workflowId 目标工作流 id。
2813
+ * @param args 覆盖目标工作流表单默认值的运行参数。
2814
+ * @param parent 外层运行的父代理(审批卡片与会话归属用)。
2815
+ * @param recordSession 外层运行的会话记录目标(嵌套进度追加到同一会话)。
2816
+ * @param signal 外层运行的取消信号(取消外层即中止嵌套运行)。
2817
+ * @param depth 当前嵌套深度(0 = 顶层);达到上限即拒绝,防无限递归。
539
2818
  */
540
- async exec(request) {
541
- const command = request.command.trim();
542
- if (command === "") throw new RemoteError("gateway/bad-request", "命令不能为空", {});
543
- const actualBytes = Buffer.byteLength(command, "utf8");
544
- if (actualBytes > this.resolved.maxCommandBytes) throw new RemoteError("autoFlow/command-too-long", `命令超过 ${this.resolved.maxCommandBytes} 字节`, {
545
- maxBytes: this.resolved.maxCommandBytes,
546
- actualBytes
2819
+ async runNestedWorkflow(workflowId, args, rootRuntime, parent, recordSession, signal, depth) {
2820
+ if (depth >= MAX_WORKFLOW_DEPTH) return {
2821
+ ok: false,
2822
+ error: `工作流嵌套调用超过深度上限 ${MAX_WORKFLOW_DEPTH}`
2823
+ };
2824
+ const loaded = await this.loadFlow(workflowId);
2825
+ if (loaded === null) return {
2826
+ ok: false,
2827
+ error: `工作流 ${workflowId} 不存在`
2828
+ };
2829
+ const { promise } = await this.startRun(applyRunArgs({
2830
+ nodes: loaded.nodes,
2831
+ edges: loaded.edges
2832
+ }, args), (nestedRuntime, step, options) => parent !== void 0 ? this.chatWaitApproval(nestedRuntime, step, parent, signal, options) : this.delegatedCanvasWaitApproval(rootRuntime, nestedRuntime, step, options), {
2833
+ workflowId,
2834
+ name: loaded.name,
2835
+ ...parent !== void 0 ? { parent } : {},
2836
+ ...recordSession !== void 0 ? { recordSession } : {},
2837
+ signal,
2838
+ depth: depth + 1,
2839
+ rootRuntime
547
2840
  });
548
- const shell = this.ctx.get("shell");
549
- if (shell === void 0) throw new RemoteError("autoFlow/shell-missing", "exec 需要组合 shell 提供方(@deepseek-ai/dsh-shell)", {});
550
- const sandboxPolicy = this.ctx.get("sandboxPolicy")?.resolve();
551
- const spec = shell.resolve({
552
- command,
553
- ...request.workdir !== void 0 ? { workdir: request.workdir } : {},
554
- ...request.timeoutMs !== void 0 ? { timeoutMs: request.timeoutMs } : {},
555
- ...request.stdoutMaxBytes !== void 0 ? { stdoutMaxBytes: request.stdoutMaxBytes } : {},
556
- ...sandboxPolicy !== void 0 ? { sandboxPolicy } : {}
2841
+ const outcome = await promise;
2842
+ if (!outcome.ok) return {
2843
+ ok: false,
2844
+ error: outcome.error ?? "运行失败"
2845
+ };
2846
+ return {
2847
+ ok: true,
2848
+ result: outcome.result
2849
+ };
2850
+ }
2851
+ /** 画布场景的嵌套审批上抛:把嵌套运行加入根运行的上抛队列,画布经根 runId 即可看到/操作它。 */
2852
+ delegatedCanvasWaitApproval(rootRuntime, nestedRuntime, step, options) {
2853
+ nestedRuntime.delegatedBy = rootRuntime;
2854
+ rootRuntime.delegatedApprovals.push({ runtime: nestedRuntime });
2855
+ return this.canvasWaitApproval(nestedRuntime, step, options);
2856
+ }
2857
+ /** 供模型工具:按 flowId 加载并运行,args 覆盖表单默认值,等待完成返回结算结果。
2858
+ * 同时把最近一条用户消息的图片/文本补填进缺失的 image/task 输入(会话上下文注入)。 */
2859
+ async runForAgent(flowId, args, agent, signal) {
2860
+ const loaded = await this.loadFlow(flowId);
2861
+ if (loaded === null) return {
2862
+ runId: "",
2863
+ ok: false,
2864
+ error: `工作流 ${flowId} 不存在`,
2865
+ steps: []
2866
+ };
2867
+ const recordSession = agent !== null && typeof agent === "object" ? agent.session : void 0;
2868
+ const merged = await this.resolveSessionRunArgs(loaded, args, sessionEventsOf(agent), signal);
2869
+ const sessionId = sessionIdOf(agent);
2870
+ const { promise } = await this.startRun(applyRunArgs({
2871
+ nodes: loaded.nodes,
2872
+ edges: loaded.edges
2873
+ }, merged ?? args), (runtime, step, options) => this.chatWaitApproval(runtime, step, agent, signal, options), {
2874
+ workflowId: flowId,
2875
+ name: loaded.name,
2876
+ parent: agent,
2877
+ workspaceRoot: sessionCwdOf(agent) ?? this.resolved.workspaceDir,
2878
+ ...signal !== void 0 ? { signal } : {},
2879
+ ...recordSession !== void 0 ? { recordSession } : {},
2880
+ ...sessionId !== void 0 ? { sessionId } : {}
557
2881
  });
558
- if (request.runInBackground === true) {
559
- const proc = shell.start(spec);
560
- const jobId = `auto-flow-${this.nextBgId}`;
561
- this.nextBgId += 1;
562
- this.background.set(jobId, proc);
2882
+ return promise;
2883
+ }
2884
+ /** 把最近一条用户消息的图片/文本补填进运行参数(image/task);无会话或已显式传入时原样返回。 */
2885
+ async resolveSessionRunArgs(workflow, args, events, signal) {
2886
+ if (events === void 0) return args;
2887
+ const turn = latestUserTurn(events);
2888
+ const inputs = workflowInputs(workflow.nodes);
2889
+ const first = turn?.images[0];
2890
+ const imageBlank = args?.image === void 0 || args.image === null || typeof args.image === "string" && args.image.trim() === "";
2891
+ let imagePath;
2892
+ if (first !== void 0 && workflowAcceptsImageInput(inputs, workflow.nodes) && imageBlank) {
2893
+ const attachments = this.ctx.get("attachments");
2894
+ if (attachments !== void 0) try {
2895
+ imagePath = await materializeSessionImage(attachments, first, path.resolve(this.resolved.workspaceDir, "uploads"), signal);
2896
+ } catch (err) {
2897
+ this.ctx.logger.warn("auto-flow: 会话图片落盘失败:%s", err instanceof Error ? err.message : String(err));
2898
+ }
2899
+ }
2900
+ return mergeSessionRunArgs(inputs, workflow.nodes, args, turn, imagePath);
2901
+ }
2902
+ /** 执行 /workflow 命令:不带参数列出工作流;/workflow <id> [key=value ...] 运行并等待结算。 */
2903
+ async runWorkflowCommand(invocation) {
2904
+ const line = parseWorkflowCommandLine(invocation.rawInput);
2905
+ if (line.kind === "error") return {
2906
+ kind: "error",
2907
+ text: line.error
2908
+ };
2909
+ if (line.kind === "list") {
2910
+ const flows = await this.listFlows();
2911
+ if (flows.length === 0) return {
2912
+ kind: "success",
2913
+ text: "(没有已保存的工作流)"
2914
+ };
563
2915
  return {
564
- kind: "background",
565
- jobId
2916
+ kind: "success",
2917
+ text: renderFlowList(flows)
566
2918
  };
567
2919
  }
568
- return mapRunResult(await shell.run(spec));
569
- }
570
- /** 读后台进程的下一次增量/终态输出(自管 ShellProcess 句柄,不依赖 ctx.jobs)。 */
571
- async execRead(jobId) {
572
- const proc = this.background.get(jobId);
573
- if (proc === void 0) throw new RemoteError("autoFlow/job-not-found", "后台任务不存在或已结束", {});
574
- const settled = proc.status === "completed" || proc.status === "killed";
2920
+ const resolved = resolveWorkflowTarget(await this.listFlows(), line.target);
2921
+ if (!resolved.ok) return {
2922
+ kind: "error",
2923
+ text: resolved.error
2924
+ };
2925
+ const outcome = await this.runForAgent(resolved.id, line.args, invocation.agent, invocation.signal);
2926
+ if (!outcome.ok) return {
2927
+ kind: "error",
2928
+ text: outcome.error ?? "运行失败"
2929
+ };
2930
+ const value = outcome.result;
575
2931
  return {
576
- text: renderProcessRead(proc.readOutput()),
577
- status: proc.status,
578
- ...settled ? { detail: proc.signal !== null ? `signal: ${proc.signal}` : `exit code: ${proc.exitCode ?? 0}` } : {}
2932
+ kind: "success",
2933
+ text: `运行完成。\n${value === void 0 ? "(无返回值)" : typeof value === "string" ? value : JSON.stringify(value, null, 2)}`
2934
+ };
2935
+ }
2936
+ /** 把 AI 智能体节点的子任务委托给子代理(LLM + 工具循环),返回最终文本。 */
2937
+ async delegateToAgent(subagents, parent, task, options, signal) {
2938
+ const providerName = subagents.list()[0];
2939
+ if (providerName === void 0) return {
2940
+ ok: false,
2941
+ error: "subagents 服务未注册任何子代理 provider"
579
2942
  };
2943
+ const maxDepth = subagents.getProvider(providerName)?.capabilities.depthLimit === true ? this.resolved.maxAgentDepth : void 0;
2944
+ let run;
2945
+ try {
2946
+ run = await subagents.start(providerName, {
2947
+ ...options.label !== void 0 ? { label: options.label } : {},
2948
+ prompt: [{
2949
+ type: "text",
2950
+ text: task
2951
+ }],
2952
+ parent,
2953
+ ...options.provider !== void 0 || options.model !== void 0 ? { agentOptions: {
2954
+ ...options.provider !== void 0 ? { provider: options.provider } : {},
2955
+ ...options.model !== void 0 ? { model: options.model } : {}
2956
+ } } : {},
2957
+ ...maxDepth !== void 0 ? { maxDepth } : {},
2958
+ signal: signal ?? new AbortController().signal
2959
+ });
2960
+ const result = await run.result;
2961
+ if (result.stopReason !== "completed") return {
2962
+ ok: false,
2963
+ error: result.diagnostic ?? `子代理未完成(${result.stopReason})`
2964
+ };
2965
+ return {
2966
+ ok: true,
2967
+ text: result.output.map((block) => block.type === "text" && typeof block.text === "string" ? block.text : "").join("").trim()
2968
+ };
2969
+ } catch (err) {
2970
+ return {
2971
+ ok: false,
2972
+ error: err instanceof Error ? err.message : String(err)
2973
+ };
2974
+ } finally {
2975
+ if (run !== void 0) await run.dispose();
2976
+ }
2977
+ }
2978
+ /** 轮询运行状态(内存权威;未在内存中时读运行日志兜底)。 */
2979
+ async runState(runId) {
2980
+ const runtime = this.resolveRuntime(runId);
2981
+ if (runtime !== void 0) return runtime.state;
2982
+ return await this.runStore.read(runId) ?? null;
2983
+ }
2984
+ /** 取消运行:协作式取消,中止在飞的脚本/LLM/shell/HTTP,引擎在下一个步骤边界停下;若正等待审批则放行让引擎退出。
2985
+ * 画布面向的根运行取消时会连带取消其整个审批上抛队列中的嵌套运行。 */
2986
+ async cancelRun(runId) {
2987
+ const runtime = this.runRuntimes.get(runId);
2988
+ if (runtime === void 0) throw new RemoteError("autoFlow/run-not-found", "运行不存在或已结束", {});
2989
+ const root = runtime.delegatedBy ?? runtime;
2990
+ this.runControllers.get(root.state.runId)?.abort();
2991
+ const targets = [root, ...root.delegatedApprovals.map((d) => d.runtime)];
2992
+ root.delegatedApprovals = [];
2993
+ for (const target of targets) {
2994
+ if (target.state.status === "completed" || target.state.status === "failed" || target.state.status === "cancelled") continue;
2995
+ target.state.status = "cancelled";
2996
+ target.state.updatedAt = Date.now();
2997
+ this.clearApprovalTimer(target);
2998
+ target.approveResolve?.("");
2999
+ target.approveResolve = void 0;
3000
+ await this.persistRun(target.state);
3001
+ }
3002
+ return runtime.state;
3003
+ }
3004
+ /** 审批选择:choice = 审批节点的某个选项标签;画布用根 runId 操作时,自动作用于当前待处理的嵌套审批(队首)。 */
3005
+ async approveRun(runId, choice) {
3006
+ const runtime = this.runRuntimes.get(runId);
3007
+ if (runtime === void 0) throw new RemoteError("autoFlow/run-not-found", "运行不存在或已结束", {});
3008
+ const pending = this.takePendingApproval(runtime);
3009
+ this.clearApprovalTimer(pending);
3010
+ pending.approveResolve?.(choice);
3011
+ pending.approveResolve = void 0;
3012
+ return pending.state;
3013
+ }
3014
+ /**
3015
+ * 解析运行时的「画布可见运行」:若传入的是画布面向的根 runId 且其审批上抛队列非空,
3016
+ * 则返回队首待处理的嵌套运行(跳过已不再等待的残留项);否则返回该运行时本身。
3017
+ */
3018
+ resolveRuntime(runId) {
3019
+ const runtime = this.runRuntimes.get(runId);
3020
+ if (runtime === void 0) return void 0;
3021
+ while (runtime.delegatedApprovals.length > 0) {
3022
+ const front = runtime.delegatedApprovals[0].runtime;
3023
+ if (front.state.status === "waiting") return front;
3024
+ runtime.delegatedApprovals.shift();
3025
+ }
3026
+ return runtime;
3027
+ }
3028
+ /**
3029
+ * 从审批上抛队列中取出当前应处理的运行时:
3030
+ * - 若传入的是被上抛的嵌套运行时(有反向指针),从根的队列里移除自身并返回自身;
3031
+ * - 若传入的是根运行,取出并返回队首嵌套运行,队列为空时返回根自身。
3032
+ */
3033
+ takePendingApproval(runtime) {
3034
+ const root = runtime.delegatedBy ?? runtime;
3035
+ if (runtime.delegatedBy !== void 0) {
3036
+ const idx = root.delegatedApprovals.findIndex((d) => d.runtime === runtime);
3037
+ if (idx >= 0) root.delegatedApprovals.splice(idx, 1);
3038
+ return runtime;
3039
+ }
3040
+ return root.delegatedApprovals.shift()?.runtime ?? runtime;
3041
+ }
3042
+ /** 清除审批等待超时定时器(放行/驳回/终态时调用)。 */
3043
+ clearApprovalTimer(runtime) {
3044
+ if (runtime.approvalTimer !== void 0) {
3045
+ clearTimeout(runtime.approvalTimer);
3046
+ runtime.approvalTimer = void 0;
3047
+ }
580
3048
  }
581
- /** 请求取消后台进程(自管 ShellProcess 句柄,不依赖 ctx.jobs)。 */
582
- async execKill(jobId) {
583
- const proc = this.background.get(jobId);
584
- if (proc === void 0) return { kind: "already-finished" };
585
- return { kind: proc.kill() ? "requested" : "already-finished" };
3049
+ failRun(runtime, message) {
3050
+ const { state } = runtime;
3051
+ const running = state.steps.find((s) => s.status === "running");
3052
+ if (running !== void 0) {
3053
+ running.status = "failed";
3054
+ running.error = message;
3055
+ }
3056
+ state.status = "failed";
3057
+ state.updatedAt = Date.now();
3058
+ this.persistRun(state);
3059
+ }
3060
+ persistRun(state) {
3061
+ if (!(state.status === "completed" || state.status === "failed" || state.status === "cancelled")) return Promise.resolve();
3062
+ const existing = this.pendingPersists.get(state.runId);
3063
+ if (existing !== void 0) return existing;
3064
+ const pending = (async () => {
3065
+ try {
3066
+ if (!this.persistedRunIds.has(state.runId)) {
3067
+ this.persistedRunIds.add(state.runId);
3068
+ await this.runStore.append(state);
3069
+ }
3070
+ } catch (err) {
3071
+ this.ctx.logger.warn("auto-flow: 运行记录追加失败:%s", err instanceof Error ? err.message : String(err));
3072
+ }
3073
+ const runtime = this.runRuntimes.get(state.runId);
3074
+ if (runtime !== void 0) this.clearApprovalTimer(runtime);
3075
+ this.runRuntimes.delete(state.runId);
3076
+ this.runControllers.delete(state.runId);
3077
+ this.pendingPersists.delete(state.runId);
3078
+ })();
3079
+ this.pendingPersists.set(state.runId, pending);
3080
+ return pending;
586
3081
  }
587
3082
  };
588
3083
  })();
589
3084
  //#endregion
590
- export { AutoFlowService, AutoFlowService as default };
3085
+ export { src_default as default };
591
3086
 
592
3087
  //# sourceMappingURL=index.js.map