@fieldwangai/agentflow 0.1.25

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.
Files changed (138) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +201 -0
  3. package/README.zh-CN.md +201 -0
  4. package/agents/agentflow-node-executor-code.md +32 -0
  5. package/agents/agentflow-node-executor-planning.md +32 -0
  6. package/agents/agentflow-node-executor-requirement.md +32 -0
  7. package/agents/agentflow-node-executor-test.md +32 -0
  8. package/agents/agentflow-node-executor-ui.md +32 -0
  9. package/agents/agentflow-node-executor.md +32 -0
  10. package/agents/agents.json +8 -0
  11. package/agents/en/agentflow-node-executor.md +32 -0
  12. package/agents/zh/agentflow-node-executor.md +32 -0
  13. package/bin/agentflow.mjs +52 -0
  14. package/bin/ensure-workspace-reference.mjs +35 -0
  15. package/bin/lib/agent-runners.mjs +1199 -0
  16. package/bin/lib/agents-path.mjs +61 -0
  17. package/bin/lib/api-runner.mjs +361 -0
  18. package/bin/lib/apply.mjs +852 -0
  19. package/bin/lib/catalog-agents.mjs +300 -0
  20. package/bin/lib/catalog-flows.mjs +532 -0
  21. package/bin/lib/composer-agent.mjs +884 -0
  22. package/bin/lib/composer-flow-instances.mjs +68 -0
  23. package/bin/lib/composer-flow-skeleton.mjs +334 -0
  24. package/bin/lib/composer-flow-validate.mjs +47 -0
  25. package/bin/lib/composer-log.mjs +197 -0
  26. package/bin/lib/composer-model-router.mjs +160 -0
  27. package/bin/lib/composer-node-schema.mjs +299 -0
  28. package/bin/lib/composer-planner.mjs +749 -0
  29. package/bin/lib/composer-script-ops.mjs +233 -0
  30. package/bin/lib/composer-skill-router.mjs +384 -0
  31. package/bin/lib/flow-import.mjs +305 -0
  32. package/bin/lib/flow-normalize.mjs +71 -0
  33. package/bin/lib/flow-write.mjs +395 -0
  34. package/bin/lib/help.mjs +139 -0
  35. package/bin/lib/hub-login.mjs +54 -0
  36. package/bin/lib/hub-publish.mjs +159 -0
  37. package/bin/lib/hub-remote.mjs +189 -0
  38. package/bin/lib/hub.mjs +299 -0
  39. package/bin/lib/i18n.mjs +233 -0
  40. package/bin/lib/locales/en.json +344 -0
  41. package/bin/lib/locales/zh.json +344 -0
  42. package/bin/lib/log.mjs +37 -0
  43. package/bin/lib/main.mjs +611 -0
  44. package/bin/lib/model-config.mjs +118 -0
  45. package/bin/lib/model-lists.mjs +188 -0
  46. package/bin/lib/node-exec-context.mjs +336 -0
  47. package/bin/lib/node-execute.mjs +513 -0
  48. package/bin/lib/normalize-node-tool-command.mjs +97 -0
  49. package/bin/lib/paths.mjs +216 -0
  50. package/bin/lib/pipeline-scripts.mjs +41 -0
  51. package/bin/lib/recent-runs.mjs +173 -0
  52. package/bin/lib/run-apply-active-lock.mjs +82 -0
  53. package/bin/lib/run-events.mjs +85 -0
  54. package/bin/lib/run-node-statuses-from-disk.mjs +85 -0
  55. package/bin/lib/schedule-config.mjs +227 -0
  56. package/bin/lib/scheduler.mjs +312 -0
  57. package/bin/lib/table.mjs +4 -0
  58. package/bin/lib/terminal.mjs +42 -0
  59. package/bin/lib/ui-print.mjs +94 -0
  60. package/bin/lib/ui-server.mjs +2113 -0
  61. package/bin/lib/workspace-tree.mjs +266 -0
  62. package/bin/lib/workspace.mjs +180 -0
  63. package/bin/pipeline/build-node-prompt.mjs +179 -0
  64. package/bin/pipeline/check-cache.mjs +191 -0
  65. package/bin/pipeline/check-flow.mjs +543 -0
  66. package/bin/pipeline/collect-nodes.mjs +212 -0
  67. package/bin/pipeline/compute-cache-md5.mjs +177 -0
  68. package/bin/pipeline/ensure-run-dir.mjs +71 -0
  69. package/bin/pipeline/extract-thinking.mjs +308 -0
  70. package/bin/pipeline/gc.mjs +129 -0
  71. package/bin/pipeline/get-env.mjs +83 -0
  72. package/bin/pipeline/get-exec-id.mjs +145 -0
  73. package/bin/pipeline/get-ready-nodes.mjs +435 -0
  74. package/bin/pipeline/get-resolved-values.mjs +337 -0
  75. package/bin/pipeline/load-key.mjs +62 -0
  76. package/bin/pipeline/parse-bool.mjs +33 -0
  77. package/bin/pipeline/parse-flow.mjs +698 -0
  78. package/bin/pipeline/post-process-control-if.mjs +23 -0
  79. package/bin/pipeline/post-process-node.mjs +490 -0
  80. package/bin/pipeline/pre-process-node.mjs +449 -0
  81. package/bin/pipeline/resolve-inputs.mjs +201 -0
  82. package/bin/pipeline/run-log.mjs +34 -0
  83. package/bin/pipeline/run-tool-nodejs.mjs +160 -0
  84. package/bin/pipeline/save-key.mjs +93 -0
  85. package/bin/pipeline/snapshot-prior-round.mjs +70 -0
  86. package/bin/pipeline/validate-flow.mjs +825 -0
  87. package/bin/pipeline/validate-for-ui.mjs +226 -0
  88. package/bin/pipeline/validate-script-output.mjs +130 -0
  89. package/bin/pipeline/write-result.mjs +182 -0
  90. package/builtin/nodes/agent_subAgent.md +14 -0
  91. package/builtin/nodes/control_agent_toBool.md +20 -0
  92. package/builtin/nodes/control_anyOne.md +17 -0
  93. package/builtin/nodes/control_end.md +11 -0
  94. package/builtin/nodes/control_if.md +20 -0
  95. package/builtin/nodes/control_start.md +11 -0
  96. package/builtin/nodes/control_toBool.md +21 -0
  97. package/builtin/nodes/provide_file.md +11 -0
  98. package/builtin/nodes/provide_str.md +11 -0
  99. package/builtin/nodes/tool_get_env.md +14 -0
  100. package/builtin/nodes/tool_load_key.md +20 -0
  101. package/builtin/nodes/tool_nodejs.md +40 -0
  102. package/builtin/nodes/tool_print.md +14 -0
  103. package/builtin/nodes/tool_save_key.md +20 -0
  104. package/builtin/nodes/tool_user_ask.md +23 -0
  105. package/builtin/nodes/tool_user_check.md +22 -0
  106. package/builtin/pipelines/module-migrate/flow.yaml +819 -0
  107. package/builtin/pipelines/module-migrate/scripts/check_imports.mjs +700 -0
  108. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Makefile +362 -0
  109. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/.deps/Release/obj.target/node_modules/node-addon-api/node_addon_api_except.stamp.d +1 -0
  110. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/.deps/Release/obj.target/tree_sitter_kotlin_binding/bindings/node/binding.o.d +17 -0
  111. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/.deps/Release/obj.target/tree_sitter_kotlin_binding/src/parser.o.d +5 -0
  112. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/.deps/Release/obj.target/tree_sitter_kotlin_binding/src/scanner.o.d +8 -0
  113. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/.deps/Release/tree_sitter_kotlin_binding.node.d +1 -0
  114. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/obj.target/node_modules/node-addon-api/node_addon_api_except.stamp +0 -0
  115. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/obj.target/tree_sitter_kotlin_binding/bindings/node/binding.o +0 -0
  116. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/obj.target/tree_sitter_kotlin_binding/src/parser.o +0 -0
  117. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/obj.target/tree_sitter_kotlin_binding/src/scanner.o +0 -0
  118. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/tree_sitter_kotlin_binding.node +0 -0
  119. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/binding.Makefile +6 -0
  120. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/gyp-mac-tool +768 -0
  121. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/node_modules/node-addon-api/node_addon_api.Makefile +6 -0
  122. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/node_modules/node-addon-api/node_addon_api.target.mk +122 -0
  123. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/node_modules/node-addon-api/node_addon_api_except.target.mk +126 -0
  124. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/node_modules/node-addon-api/node_addon_api_maybe.target.mk +122 -0
  125. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/tree_sitter_kotlin_binding.target.mk +203 -0
  126. package/builtin/pipelines/new/flow.yaml +545 -0
  127. package/builtin/pipelines/new/scripts/check-flow.mjs +9 -0
  128. package/builtin/pipelines/new/scripts/collect-nodes.mjs +211 -0
  129. package/builtin/pipelines/scripts/adjust-node-positions.mjs +113 -0
  130. package/builtin/web-ui/dist/agentflow-icon.svg +23 -0
  131. package/builtin/web-ui/dist/assets/index-CZkUPcXE.css +1 -0
  132. package/builtin/web-ui/dist/assets/index-DkkhNESc.js +190 -0
  133. package/builtin/web-ui/dist/index.html +24 -0
  134. package/package.json +67 -0
  135. package/reference/flow-control-capabilities.md +274 -0
  136. package/reference/flow-layout.md +84 -0
  137. package/reference/flow-prompt-handler-check.md +12 -0
  138. package/reference/flow-result-semantics.md +14 -0
@@ -0,0 +1,160 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 运行 tool_nodejs 脚本并将结果写入流水线。
4
+ *
5
+ * 成败判定(Unix 哲学):
6
+ * - 进程 exit code 0 → success,非 0 → failed
7
+ * - 若 stdout 为合法 JSON 且含 err_code,err_code 优先(向后兼容旧脚本)
8
+ *
9
+ * stdout 内容 → result 槽位:
10
+ * - JSON 模式:提取 message.result
11
+ * - 纯文本模式:整段 stdout 作为 result
12
+ *
13
+ * 用法:node run-tool-nodejs.mjs <workspaceRoot> <flowName> <uuid> <instanceId> [execId] -- <scriptCmd> [args...]
14
+ */
15
+
16
+ import { spawnSync } from "child_process";
17
+ import fs from "fs";
18
+ import path from "path";
19
+ import { fileURLToPath } from "url";
20
+
21
+ import { getRunDir } from "../lib/paths.mjs";
22
+ import { validateAndParse } from "./validate-script-output.mjs";
23
+ import { writeResult } from "./write-result.mjs";
24
+ import { loadExecId, outputNodeBasename, outputDirForNode } from "./get-exec-id.mjs";
25
+ import { nodeToolCommandToArgv } from "../lib/normalize-node-tool-command.mjs";
26
+ import { buildPipelineScriptPathHint } from "../lib/flow-normalize.mjs";
27
+
28
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
29
+ const MAX_RETRIES = 3;
30
+ const RETRY_DELAY_MS = 1000;
31
+
32
+ function runOnce(workspaceRoot, flowName, uuid, instanceId, execId, scriptArgs) {
33
+ const runDir = getRunDir(workspaceRoot, flowName, uuid);
34
+ const outputDir = path.join(runDir, outputDirForNode(instanceId));
35
+
36
+ const rawLine = scriptArgs.join(" ");
37
+ const { argv, commandLine: normalizedCmd } = nodeToolCommandToArgv(rawLine);
38
+ const child =
39
+ /^node\s/i.test(String(normalizedCmd).trim()) && argv.length >= 1
40
+ ? spawnSync(process.execPath, argv, {
41
+ cwd: workspaceRoot,
42
+ shell: false,
43
+ stdio: ["inherit", "pipe", "pipe"],
44
+ })
45
+ : spawnSync(normalizedCmd, [], {
46
+ cwd: workspaceRoot,
47
+ shell: true,
48
+ stdio: ["inherit", "pipe", "pipe"],
49
+ });
50
+
51
+ const stdout = child.stdout?.toString("utf-8") ?? "";
52
+ const stderr = child.stderr?.toString("utf-8") ?? "";
53
+ const exitCode = child.status ?? 1;
54
+
55
+ if (child.signal) {
56
+ persistStderr(outputDir, instanceId, execId, stderr);
57
+ return { success: false, fatal: true, detail: `Script killed: ${child.signal}` };
58
+ }
59
+
60
+ const { ok, errors, payload } = validateAndParse(stdout);
61
+
62
+ if (!ok) {
63
+ const baseDetail = exitCode !== 0
64
+ ? `脚本退出码 ${exitCode}` + (stderr.trim() ? `:${stderr.trim().slice(0, 200)}` : "")
65
+ : (errors.length ? errors.join("; ") : "脚本无输出");
66
+ const detail = baseDetail + buildPipelineScriptPathHint(stderr);
67
+ writeResult(workspaceRoot, flowName, uuid, instanceId, {
68
+ status: "failed",
69
+ message: detail,
70
+ }, { preserveBody: true, execId });
71
+ persistStderr(outputDir, instanceId, execId, stderr);
72
+ return { success: false, fatal: false, detail };
73
+ }
74
+
75
+ const isSynthetic = Boolean(payload._synthetic);
76
+ const success = isSynthetic ? exitCode === 0 : (payload.err_code === 0);
77
+ const message = payload.message;
78
+
79
+ try {
80
+ fs.mkdirSync(outputDir, { recursive: true });
81
+ // 备份由 snapshotPriorRoundIfNeeded 在 pre-process 入口统一处理。
82
+ for (const slot of Object.keys(message)) {
83
+ if (slot === "_synthetic") continue;
84
+ const content = message[slot];
85
+ if (content == null) continue;
86
+ fs.writeFileSync(
87
+ path.join(outputDir, outputNodeBasename(instanceId, execId, slot)),
88
+ String(content),
89
+ "utf-8",
90
+ );
91
+ }
92
+ writeResult(workspaceRoot, flowName, uuid, instanceId, {
93
+ status: success ? "success" : "failed",
94
+ message: success ? "执行完成" : "执行未通过",
95
+ }, { preserveBody: true, execId });
96
+ } catch (e) {
97
+ writeResult(workspaceRoot, flowName, uuid, instanceId, {
98
+ status: "failed",
99
+ message: e.message || "写入 output/result 异常",
100
+ }, { preserveBody: true, execId });
101
+ persistStderr(outputDir, instanceId, execId, stderr);
102
+ return { success: false, fatal: false, detail: e.message };
103
+ }
104
+
105
+ persistStderr(outputDir, instanceId, execId, stderr);
106
+ return { success, fatal: false, detail: success ? "" : `脚本退出码 ${exitCode}` };
107
+ }
108
+
109
+ function persistStderr(outputDir, instanceId, execId, stderr) {
110
+ if (!stderr) return;
111
+ try {
112
+ fs.mkdirSync(outputDir, { recursive: true });
113
+ fs.writeFileSync(
114
+ path.join(outputDir, outputNodeBasename(instanceId, execId, "stderr")),
115
+ stderr,
116
+ "utf-8",
117
+ );
118
+ } catch (_) {}
119
+ }
120
+
121
+ function main() {
122
+ const args = process.argv.slice(2);
123
+ const sep = args.indexOf("--");
124
+ if (sep < 0 || args.length < sep + 2) {
125
+ console.error(
126
+ "Usage: node run-tool-nodejs.mjs <workspaceRoot> <flowName> <uuid> <instanceId> [execId] -- <scriptCmd> [args...]",
127
+ );
128
+ process.exit(2);
129
+ }
130
+
131
+ const workspaceRoot = path.resolve(args[0]);
132
+ const flowName = args[1];
133
+ const uuid = args[2];
134
+ const instanceId = args[3];
135
+ const execId =
136
+ sep >= 5 && args[4] !== "--"
137
+ ? (parseInt(String(args[4]), 10) || 1)
138
+ : loadExecId(workspaceRoot, flowName, uuid, instanceId);
139
+ const scriptArgs = args.slice(sep + 1);
140
+
141
+ for (let attempt = 1; attempt <= MAX_RETRIES + 1; attempt++) {
142
+ const result = runOnce(workspaceRoot, flowName, uuid, instanceId, execId, scriptArgs);
143
+ if (result.success) {
144
+ process.exit(0);
145
+ }
146
+ if (result.fatal) {
147
+ console.error(result.detail);
148
+ process.exit(1);
149
+ }
150
+ if (attempt <= MAX_RETRIES) {
151
+ console.error(`[tool_nodejs 自愈] ${instanceId} 第 ${attempt}/${MAX_RETRIES} 次重试:${result.detail?.slice(0, 200) || "unknown"}`);
152
+ spawnSync("sleep", [String(RETRY_DELAY_MS / 1000)], { stdio: "ignore" });
153
+ } else {
154
+ console.error(result.detail);
155
+ process.exit(1);
156
+ }
157
+ }
158
+ }
159
+
160
+ main();
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * tool_save_key 执行脚本:按 key 将 value 写入 run 目录下 memory 存储,stdout 输出 tool_nodejs 约定 JSON。
4
+ * 存储路径与格式由本脚本内部实现,节点不感知。key/value 由命令行参数传入,不读 flow。
5
+ * value 若为 run 目录内相对路径则读取文件内容后写入。
6
+ * 用法:node save-key.mjs <workspaceRoot> <flowName> <uuid> <key> [value]
7
+ * 输出(stdout 一行 JSON):成功时 result 为写入的 value;err_code 0=成功 1=失败,无 next。
8
+ */
9
+
10
+ import fs from "fs";
11
+ import path from "path";
12
+
13
+ import { getRunDir } from "../lib/paths.mjs";
14
+
15
+ const MEMORY_FILENAME = "memory.md";
16
+
17
+ function parseMemory(content) {
18
+ const map = new Map();
19
+ for (const line of (content || "").split(/\r?\n/)) {
20
+ const idx = line.indexOf(": ");
21
+ if (idx <= 0) continue;
22
+ const k = line.slice(0, idx).trim();
23
+ const v = line.slice(idx + 2).trim();
24
+ if (k) map.set(k, v);
25
+ }
26
+ return map;
27
+ }
28
+
29
+ function serializeMemory(map) {
30
+ return (
31
+ Array.from(map.entries())
32
+ .map(([k, v]) => `${k}: ${String(v).replace(/\r?\n/g, " ")}`)
33
+ .join("\n") + (map.size ? "\n" : "")
34
+ );
35
+ }
36
+
37
+ function main() {
38
+ const [root, flowName, uuid, keyArg, valueArg] = process.argv.slice(2);
39
+ if (!root || !flowName || !uuid) {
40
+ console.log(
41
+ JSON.stringify({
42
+ err_code: 1,
43
+ message: { result: "" },
44
+ }),
45
+ );
46
+ process.exit(0);
47
+ }
48
+
49
+ const key = keyArg != null ? String(keyArg).trim() : "";
50
+ let value = valueArg != null ? String(valueArg).trim() : "";
51
+
52
+ const workspaceRoot = path.resolve(root);
53
+ const runDir = getRunDir(workspaceRoot, flowName, uuid);
54
+ const memoryPath = path.join(runDir, MEMORY_FILENAME);
55
+
56
+ if (!key) {
57
+ console.log(
58
+ JSON.stringify({
59
+ err_code: 0,
60
+ message: { result: "" },
61
+ }),
62
+ );
63
+ process.exit(0);
64
+ }
65
+
66
+ if (
67
+ value &&
68
+ (value.includes("/") || value.startsWith("output") || value.startsWith("intermediate"))
69
+ ) {
70
+ const valuePath = path.join(runDir, value);
71
+ if (fs.existsSync(valuePath)) {
72
+ try {
73
+ value = fs.readFileSync(valuePath, "utf-8").trim();
74
+ } catch (_) {}
75
+ }
76
+ }
77
+
78
+ const existing = fs.existsSync(memoryPath)
79
+ ? parseMemory(fs.readFileSync(memoryPath, "utf-8"))
80
+ : new Map();
81
+ existing.set(key, value);
82
+ fs.mkdirSync(path.dirname(memoryPath), { recursive: true });
83
+ fs.writeFileSync(memoryPath, serializeMemory(existing), "utf-8");
84
+
85
+ console.log(
86
+ JSON.stringify({
87
+ err_code: 0,
88
+ message: { result: value },
89
+ }),
90
+ );
91
+ }
92
+
93
+ main();
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 节点每轮执行进入 pre-process 时,统一把「上一轮遗留的当前文件」快照到
4
+ * <name>_<priorExecId>.<ext>
5
+ * 形式。这是唯一的备份入口,替代分散在 write-result / build-node-prompt /
6
+ * pre-process 内多种辅助 prompt / get-env / run-tool-nodejs 中的 rename 调用。
7
+ *
8
+ * 不变量:
9
+ * 1. 文件后缀 `_K` 严格对应「第 K 轮结束时的文件内容」,无错位。
10
+ * 2. unsuffixed(无 `_N` 后缀)= 当前最新版本,永远存在;snapshot 用 copy
11
+ * 生成 `_K` 历史快照,不破坏 unsuffixed。下游运行时只 resolve unsuffixed,
12
+ * 不应也不需要 fallback 到 `_K`。
13
+ * 3. 幂等:目标 `_K.ext` 已存在或源文件已经是 `_K.ext` 形态,直接跳过。
14
+ *
15
+ * 调用方:bin/pipeline/pre-process-node.mjs 在计算 execId 后、任何 write 前。
16
+ */
17
+ import fs from "fs";
18
+ import path from "path";
19
+
20
+ import { intermediateDirForNode, outputDirForNode } from "./get-exec-id.mjs";
21
+
22
+ const BACKUP_SUFFIX_RE = /_\d+$/;
23
+
24
+ /**
25
+ * @param {string} runDir - .workspace/agentflow/runBuild/<flowName>/<uuid>
26
+ * @param {string} instanceId
27
+ * @param {number} priorExecId - memory 里记录的「上一轮已完成 execId」,首轮传 0/undefined 直接返回
28
+ */
29
+ export function snapshotPriorRoundIfNeeded(runDir, instanceId, priorExecId) {
30
+ const prior = Number(priorExecId);
31
+ if (!Number.isFinite(prior) || prior < 1) return;
32
+ const suffix = `_${prior}`;
33
+
34
+ const interDir = path.join(runDir, intermediateDirForNode(instanceId));
35
+ snapshotDir(interDir, (f) => f.startsWith(instanceId + "."), suffix);
36
+
37
+ const outDir = path.join(runDir, outputDirForNode(instanceId));
38
+ snapshotDir(outDir, (f) => f.startsWith(`node_${instanceId}_`), suffix);
39
+ }
40
+
41
+ /**
42
+ * @param {string} dir
43
+ * @param {(filename: string) => boolean} predicate
44
+ * @param {string} suffix - `_<N>`
45
+ */
46
+ function snapshotDir(dir, predicate, suffix) {
47
+ if (!fs.existsSync(dir)) return;
48
+ let files;
49
+ try {
50
+ files = fs.readdirSync(dir);
51
+ } catch {
52
+ return;
53
+ }
54
+ for (const f of files) {
55
+ if (!predicate(f)) continue;
56
+ const ext = path.extname(f);
57
+ const base = path.basename(f, ext);
58
+ // 已是备份文件(以 _\d+ 结尾),跳过
59
+ if (BACKUP_SUFFIX_RE.test(base)) continue;
60
+ const to = base + suffix + ext;
61
+ if (to === f) continue;
62
+ const toPath = path.join(dir, to);
63
+ if (fs.existsSync(toPath)) continue; // 幂等
64
+ try {
65
+ fs.copyFileSync(path.join(dir, f), toPath);
66
+ } catch {
67
+ // 并发或权限错误:放弃该文件,其它继续。unsuffixed 保留不动。
68
+ }
69
+ }
70
+ }