@fieldwangai/agentflow 0.1.29 → 0.1.31
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/agents/agentflow-node-executor-code.md +3 -2
- package/agents/agentflow-node-executor-planning.md +3 -2
- package/agents/agentflow-node-executor-requirement.md +3 -2
- package/agents/agentflow-node-executor-test.md +3 -2
- package/agents/agentflow-node-executor-ui.md +3 -2
- package/agents/agentflow-node-executor.md +3 -2
- package/agents/en/agentflow-node-executor.md +3 -2
- package/agents/zh/agentflow-node-executor.md +3 -2
- package/bin/lib/agent-runners.mjs +63 -14
- package/bin/lib/api-runner.mjs +30 -4
- package/bin/lib/apply.mjs +6 -5
- package/bin/lib/auth.mjs +240 -0
- package/bin/lib/catalog-agents.mjs +2 -2
- package/bin/lib/catalog-flows.mjs +196 -17
- package/bin/lib/composer-agent.mjs +22 -1
- package/bin/lib/composer-skill-router.mjs +10 -78
- package/bin/lib/flow-import.mjs +2 -2
- package/bin/lib/flow-write.mjs +20 -20
- package/bin/lib/help.mjs +2 -2
- package/bin/lib/locales/en.json +29 -1
- package/bin/lib/locales/zh.json +31 -3
- package/bin/lib/main.mjs +6 -1
- package/bin/lib/node-exec-context.mjs +5 -5
- package/bin/lib/node-execute.mjs +15 -10
- package/bin/lib/paths.mjs +69 -13
- package/bin/lib/recent-runs.mjs +2 -2
- package/bin/lib/run-node-statuses-from-disk.mjs +3 -3
- package/bin/lib/runtime-context.mjs +225 -0
- package/bin/lib/scheduler.mjs +42 -38
- package/bin/lib/skill-registry.mjs +145 -0
- package/bin/lib/ui-server.mjs +1517 -57
- package/bin/lib/user-env.mjs +83 -0
- package/bin/lib/workspace-tree.mjs +4 -3
- package/bin/lib/workspace.mjs +9 -11
- package/bin/pipeline/build-node-prompt.mjs +29 -4
- package/bin/pipeline/get-env.mjs +5 -29
- package/bin/pipeline/get-exec-id.mjs +2 -2
- package/bin/pipeline/get-resolved-values.mjs +1 -0
- package/bin/pipeline/pre-process-node.mjs +328 -6
- package/bin/pipeline/run-tool-nodejs.mjs +7 -0
- package/bin/pipeline/validate-flow.mjs +2 -0
- package/builtin/nodes/agent_subAgent.md +12 -3
- package/builtin/nodes/control_cd_workspace.md +45 -0
- package/builtin/nodes/control_load_skills.md +50 -0
- package/builtin/nodes/control_user_workspace.md +20 -0
- package/builtin/nodes/display_ascii.md +22 -0
- package/builtin/nodes/display_markdown.md +22 -0
- package/builtin/nodes/display_mermaid.md +22 -0
- package/builtin/nodes/tool_git_checkout.md +57 -0
- package/builtin/nodes/tool_nodejs.md +8 -1
- package/builtin/nodes/tool_print.md +4 -1
- package/builtin/web-ui/dist/assets/index-BVWwQpvg.css +1 -0
- package/builtin/web-ui/dist/assets/index-CvNy1n3f.js +197 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/skills/agentflow-flow-recipes/SKILL.md +24 -0
- package/skills/agentflow-flow-recipes/references/recipes.md +63 -0
- package/skills/agentflow-node-reference/SKILL.md +25 -0
- package/skills/agentflow-node-reference/references/builtin-nodes.md +210 -0
- package/skills/agentflow-placeholder-reference/SKILL.md +24 -0
- package/skills/agentflow-placeholder-reference/references/placeholders.md +20 -0
- package/skills/agentflow-runtime-reference/SKILL.md +25 -0
- package/skills/agentflow-runtime-reference/references/runtime.md +64 -0
- package/skills/agentflow-workspace-ascii/SKILL.md +42 -0
- package/skills/agentflow-workspace-graph/SKILL.md +67 -0
- package/skills/agentflow-workspace-markdown/SKILL.md +44 -0
- package/skills/agentflow-workspace-mermaid/SKILL.md +43 -0
- package/builtin/web-ui/dist/assets/index-0vJxkTJz.css +0 -1
- package/builtin/web-ui/dist/assets/index-h69bpxLI.js +0 -190
package/bin/lib/auth.mjs
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import {
|
|
5
|
+
ARCHIVED_PIPELINES_DIR_NAME,
|
|
6
|
+
getAgentflowDataRoot,
|
|
7
|
+
getUserPipelinesRoot,
|
|
8
|
+
sanitizeAgentflowUserId,
|
|
9
|
+
} from "./paths.mjs";
|
|
10
|
+
|
|
11
|
+
const SESSION_COOKIE = "af_session";
|
|
12
|
+
const SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
13
|
+
|
|
14
|
+
function authRoot() {
|
|
15
|
+
return path.join(getAgentflowDataRoot(), "auth");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function usersPath() {
|
|
19
|
+
return path.join(authRoot(), "users.json");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function sessionsPath() {
|
|
23
|
+
return path.join(authRoot(), "sessions.json");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function readJsonObject(filePath) {
|
|
27
|
+
try {
|
|
28
|
+
if (!fs.existsSync(filePath)) return {};
|
|
29
|
+
const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
30
|
+
return data && typeof data === "object" && !Array.isArray(data) ? data : {};
|
|
31
|
+
} catch {
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function writeJsonObject(filePath, data) {
|
|
37
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
38
|
+
fs.writeFileSync(filePath, JSON.stringify(data && typeof data === "object" ? data : {}, null, 2) + "\n", "utf-8");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function hashPassword(password, salt = crypto.randomBytes(16).toString("hex")) {
|
|
42
|
+
const hash = crypto.scryptSync(String(password), salt, 64).toString("hex");
|
|
43
|
+
return { salt, hash };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function verifyPassword(password, record) {
|
|
47
|
+
if (!record || typeof record.salt !== "string" || typeof record.hash !== "string") return false;
|
|
48
|
+
const next = hashPassword(password, record.salt).hash;
|
|
49
|
+
try {
|
|
50
|
+
return crypto.timingSafeEqual(Buffer.from(next, "hex"), Buffer.from(record.hash, "hex"));
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function hashToken(token) {
|
|
57
|
+
return crypto.createHash("sha256").update(String(token)).digest("hex");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function parseCookies(header) {
|
|
61
|
+
const out = {};
|
|
62
|
+
for (const part of String(header || "").split(";")) {
|
|
63
|
+
const idx = part.indexOf("=");
|
|
64
|
+
if (idx <= 0) continue;
|
|
65
|
+
const key = part.slice(0, idx).trim();
|
|
66
|
+
const value = part.slice(idx + 1).trim();
|
|
67
|
+
if (!key) continue;
|
|
68
|
+
try {
|
|
69
|
+
out[key] = decodeURIComponent(value);
|
|
70
|
+
} catch {
|
|
71
|
+
out[key] = value;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function readAuthUsers() {
|
|
78
|
+
return readJsonObject(usersPath());
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function authSetupRequired() {
|
|
82
|
+
return Object.keys(readAuthUsers()).length === 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function listFlowDirs(root) {
|
|
86
|
+
try {
|
|
87
|
+
if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) return [];
|
|
88
|
+
return fs.readdirSync(root, { withFileTypes: true })
|
|
89
|
+
.filter((entry) => entry.isDirectory())
|
|
90
|
+
.filter((entry) => entry.name !== ARCHIVED_PIPELINES_DIR_NAME)
|
|
91
|
+
.filter((entry) => fs.existsSync(path.join(root, entry.name, "flow.yaml")))
|
|
92
|
+
.map((entry) => entry.name)
|
|
93
|
+
.sort((a, b) => a.localeCompare(b));
|
|
94
|
+
} catch {
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function copyMissingFlowDirs(sourceRoot, targetRoot, relativeRoot = "") {
|
|
100
|
+
const fromRoot = path.join(sourceRoot, relativeRoot);
|
|
101
|
+
const toRoot = path.join(targetRoot, relativeRoot);
|
|
102
|
+
const copied = [];
|
|
103
|
+
const skipped = [];
|
|
104
|
+
for (const name of listFlowDirs(fromRoot)) {
|
|
105
|
+
const fromDir = path.join(fromRoot, name);
|
|
106
|
+
const toDir = path.join(toRoot, name);
|
|
107
|
+
if (fs.existsSync(toDir)) {
|
|
108
|
+
skipped.push(path.join(relativeRoot, name).replace(/\\/g, "/"));
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
fs.mkdirSync(path.dirname(toDir), { recursive: true });
|
|
112
|
+
fs.cpSync(fromDir, toDir, { recursive: true });
|
|
113
|
+
copied.push(path.join(relativeRoot, name).replace(/\\/g, "/"));
|
|
114
|
+
}
|
|
115
|
+
return { copied, skipped };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function migrateLegacyPipelinesToAdminUser(userId) {
|
|
119
|
+
const safeUserId = sanitizeAgentflowUserId(userId);
|
|
120
|
+
if (!safeUserId) return { copied: [], skipped: [], source: "", target: "", error: "invalid userId" };
|
|
121
|
+
|
|
122
|
+
const source = getUserPipelinesRoot("");
|
|
123
|
+
const target = getUserPipelinesRoot(safeUserId);
|
|
124
|
+
if (path.resolve(source) === path.resolve(target)) {
|
|
125
|
+
return { copied: [], skipped: [], source, target };
|
|
126
|
+
}
|
|
127
|
+
if (!fs.existsSync(source)) {
|
|
128
|
+
return { copied: [], skipped: [], source, target };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const active = copyMissingFlowDirs(source, target);
|
|
132
|
+
const archived = copyMissingFlowDirs(source, target, ARCHIVED_PIPELINES_DIR_NAME);
|
|
133
|
+
return {
|
|
134
|
+
copied: [...active.copied, ...archived.copied],
|
|
135
|
+
skipped: [...active.skipped, ...archived.skipped],
|
|
136
|
+
source,
|
|
137
|
+
target,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function getSessionCookieName() {
|
|
142
|
+
return SESSION_COOKIE;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function buildSessionCookie(token) {
|
|
146
|
+
const attrs = [
|
|
147
|
+
`${SESSION_COOKIE}=${encodeURIComponent(token)}`,
|
|
148
|
+
"Path=/",
|
|
149
|
+
"HttpOnly",
|
|
150
|
+
"SameSite=Lax",
|
|
151
|
+
`Max-Age=${Math.floor(SESSION_TTL_MS / 1000)}`,
|
|
152
|
+
];
|
|
153
|
+
return attrs.join("; ");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function buildClearSessionCookie() {
|
|
157
|
+
return `${SESSION_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function getAuthUserFromRequest(req) {
|
|
161
|
+
const token = parseCookies(req.headers.cookie || "")[SESSION_COOKIE];
|
|
162
|
+
if (!token) return null;
|
|
163
|
+
const sessions = readJsonObject(sessionsPath());
|
|
164
|
+
const key = hashToken(token);
|
|
165
|
+
const session = sessions[key];
|
|
166
|
+
if (!session || typeof session.userId !== "string") return null;
|
|
167
|
+
if (Number(session.expiresAt) <= Date.now()) {
|
|
168
|
+
delete sessions[key];
|
|
169
|
+
writeJsonObject(sessionsPath(), sessions);
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
const users = readAuthUsers();
|
|
173
|
+
const user = users[session.userId];
|
|
174
|
+
if (!user) return null;
|
|
175
|
+
return {
|
|
176
|
+
userId: session.userId,
|
|
177
|
+
username: user.username || session.userId,
|
|
178
|
+
isAdmin: Boolean(user.isAdmin),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function loginOrCreateUser(username, password) {
|
|
183
|
+
const userId = sanitizeAgentflowUserId(username);
|
|
184
|
+
if (!userId) {
|
|
185
|
+
return { ok: false, error: "用户名须以字母开头,仅可使用字母、数字、下划线与连字符,最多 64 字符" };
|
|
186
|
+
}
|
|
187
|
+
const pwd = String(password || "");
|
|
188
|
+
if (pwd.length < 4) return { ok: false, error: "密码至少 4 位" };
|
|
189
|
+
|
|
190
|
+
const users = readAuthUsers();
|
|
191
|
+
const firstUser = Object.keys(users).length === 0;
|
|
192
|
+
let user = users[userId];
|
|
193
|
+
if (!user) {
|
|
194
|
+
const hashed = hashPassword(pwd);
|
|
195
|
+
user = {
|
|
196
|
+
userId,
|
|
197
|
+
username: String(username).trim(),
|
|
198
|
+
salt: hashed.salt,
|
|
199
|
+
hash: hashed.hash,
|
|
200
|
+
isAdmin: firstUser,
|
|
201
|
+
createdAt: new Date().toISOString(),
|
|
202
|
+
};
|
|
203
|
+
users[userId] = user;
|
|
204
|
+
writeJsonObject(usersPath(), users);
|
|
205
|
+
} else if (!verifyPassword(pwd, user)) {
|
|
206
|
+
return { ok: false, error: "用户名或密码错误" };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let migration = null;
|
|
210
|
+
if (Boolean(user.isAdmin)) {
|
|
211
|
+
try {
|
|
212
|
+
migration = migrateLegacyPipelinesToAdminUser(userId);
|
|
213
|
+
} catch (e) {
|
|
214
|
+
migration = { copied: [], skipped: [], error: (e && e.message) || String(e) };
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const token = crypto.randomBytes(32).toString("base64url");
|
|
219
|
+
const sessions = readJsonObject(sessionsPath());
|
|
220
|
+
sessions[hashToken(token)] = {
|
|
221
|
+
userId,
|
|
222
|
+
createdAt: Date.now(),
|
|
223
|
+
expiresAt: Date.now() + SESSION_TTL_MS,
|
|
224
|
+
};
|
|
225
|
+
writeJsonObject(sessionsPath(), sessions);
|
|
226
|
+
return {
|
|
227
|
+
ok: true,
|
|
228
|
+
token,
|
|
229
|
+
user: { userId, username: user.username || userId, isAdmin: Boolean(user.isAdmin) },
|
|
230
|
+
migration,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function logoutRequest(req) {
|
|
235
|
+
const token = parseCookies(req.headers.cookie || "")[SESSION_COOKIE];
|
|
236
|
+
if (!token) return;
|
|
237
|
+
const sessions = readJsonObject(sessionsPath());
|
|
238
|
+
delete sessions[hashToken(token)];
|
|
239
|
+
writeJsonObject(sessionsPath(), sessions);
|
|
240
|
+
}
|
|
@@ -278,10 +278,10 @@ description: ${description != null ? String(description).replace(/\n/g, " ") : "
|
|
|
278
278
|
}
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
-
export function copyBuiltinJson(workspaceRoot, flowId, targetFlowId) {
|
|
281
|
+
export function copyBuiltinJson(workspaceRoot, flowId, targetFlowId, opts = {}) {
|
|
282
282
|
const destId = (targetFlowId && targetFlowId.trim()) || flowId;
|
|
283
283
|
const srcDir = path.join(PACKAGE_BUILTIN_PIPELINES_DIR, flowId);
|
|
284
|
-
const pipelinesRoot = getUserPipelinesRoot();
|
|
284
|
+
const pipelinesRoot = getUserPipelinesRoot(opts.userId);
|
|
285
285
|
const destDir = path.join(pipelinesRoot, destId);
|
|
286
286
|
if (!fs.existsSync(srcDir) || !fs.existsSync(path.join(srcDir, "flow.yaml"))) {
|
|
287
287
|
return { success: false, error: t("catalog.builtin_flow_not_found") };
|
|
@@ -50,7 +50,7 @@ export function readPipelineListDescription(flowDir) {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
export function listFlowsJson(workspaceRoot) {
|
|
53
|
+
export function listFlowsJson(workspaceRoot, opts = {}) {
|
|
54
54
|
const root = path.resolve(workspaceRoot);
|
|
55
55
|
const out = [];
|
|
56
56
|
const fromBuiltin = collectPipelineNamesFromDir(PACKAGE_BUILTIN_PIPELINES_DIR);
|
|
@@ -59,7 +59,7 @@ export function listFlowsJson(workspaceRoot) {
|
|
|
59
59
|
const description = readPipelineListDescription(dir);
|
|
60
60
|
out.push({ id: name, path: dir, source: "builtin", ...(description ? { description } : {}) });
|
|
61
61
|
}
|
|
62
|
-
const userPipelinesRoot = getUserPipelinesRoot();
|
|
62
|
+
const userPipelinesRoot = getUserPipelinesRoot(opts.userId);
|
|
63
63
|
const fromUserData = collectPipelineNamesFromDir(userPipelinesRoot);
|
|
64
64
|
for (const name of fromUserData) {
|
|
65
65
|
if (name === ARCHIVED_PIPELINES_DIR_NAME) continue;
|
|
@@ -129,7 +129,10 @@ function normalizeFrontmatterSlots(arr) {
|
|
|
129
129
|
let def = item.default !== undefined && item.default !== null ? item.default : item.value;
|
|
130
130
|
if (def === undefined || def === null) def = "";
|
|
131
131
|
else if (typeof def !== "string") def = String(def);
|
|
132
|
-
|
|
132
|
+
const slot = { type, name, default: def };
|
|
133
|
+
if (item.required != null) slot.required = Boolean(item.required);
|
|
134
|
+
if (item.showOnNode != null) slot.showOnNode = Boolean(item.showOnNode);
|
|
135
|
+
return slot;
|
|
133
136
|
});
|
|
134
137
|
}
|
|
135
138
|
|
|
@@ -200,6 +203,7 @@ export function parseNodeFrontmatter(raw) {
|
|
|
200
203
|
export function listNodesJson(workspaceRoot, flowId, flowSource, opts = {}) {
|
|
201
204
|
const root = path.resolve(workspaceRoot);
|
|
202
205
|
const archived = Boolean(opts.archived);
|
|
206
|
+
const userPipelinesRoot = getUserPipelinesRoot(opts.userId);
|
|
203
207
|
const byId = new Map();
|
|
204
208
|
const pipelineTranslations = {};
|
|
205
209
|
let marketplaceFlowData = null;
|
|
@@ -293,13 +297,9 @@ export function listNodesJson(workspaceRoot, flowId, flowSource, opts = {}) {
|
|
|
293
297
|
} catch (_) {}
|
|
294
298
|
} else if (flowSource === "user") {
|
|
295
299
|
if (archived) {
|
|
296
|
-
addFromDir(
|
|
297
|
-
path.join(getUserPipelinesRoot(), ARCHIVED_PIPELINES_DIR_NAME, flowId, "nodes"),
|
|
298
|
-
"flow",
|
|
299
|
-
flowId,
|
|
300
|
-
);
|
|
300
|
+
addFromDir(path.join(userPipelinesRoot, ARCHIVED_PIPELINES_DIR_NAME, flowId, "nodes"), "flow", flowId);
|
|
301
301
|
} else {
|
|
302
|
-
addFromDir(path.join(
|
|
302
|
+
addFromDir(path.join(userPipelinesRoot, flowId, "nodes"), "flow", flowId);
|
|
303
303
|
addFromDir(path.join(root, PIPELINES_DIR, flowId, "nodes"), "flow", flowId);
|
|
304
304
|
addFromDir(path.join(root, LEGACY_PIPELINES_DIR, flowId, "nodes"), "flow", flowId);
|
|
305
305
|
}
|
|
@@ -342,13 +342,14 @@ export function printNodesTable(list) {
|
|
|
342
342
|
export function readFlowJson(workspaceRoot, flowId, flowSource, options = {}) {
|
|
343
343
|
const root = path.resolve(workspaceRoot);
|
|
344
344
|
const archived = Boolean(options.archived);
|
|
345
|
+
const userPipelinesRoot = getUserPipelinesRoot(options.userId);
|
|
345
346
|
let flowDir;
|
|
346
347
|
if (archived) {
|
|
347
348
|
if (flowSource === "builtin") {
|
|
348
349
|
return { error: t("catalog.builtin_flow_archive_not_supported") };
|
|
349
350
|
}
|
|
350
351
|
if (flowSource === "user") {
|
|
351
|
-
flowDir = path.join(
|
|
352
|
+
flowDir = path.join(userPipelinesRoot, ARCHIVED_PIPELINES_DIR_NAME, flowId);
|
|
352
353
|
} else if (flowSource === "workspace") {
|
|
353
354
|
flowDir = path.join(root, PIPELINES_DIR, ARCHIVED_PIPELINES_DIR_NAME, flowId);
|
|
354
355
|
} else {
|
|
@@ -377,7 +378,7 @@ export function readFlowJson(workspaceRoot, flowId, flowSource, options = {}) {
|
|
|
377
378
|
if (flowSource === "builtin") {
|
|
378
379
|
flowDir = path.join(PACKAGE_BUILTIN_PIPELINES_DIR, flowId);
|
|
379
380
|
} else if (flowSource === "user") {
|
|
380
|
-
flowDir = path.join(
|
|
381
|
+
flowDir = path.join(userPipelinesRoot, flowId);
|
|
381
382
|
} else if (flowSource === "workspace") {
|
|
382
383
|
flowDir = path.join(root, PIPELINES_DIR, flowId);
|
|
383
384
|
} else {
|
|
@@ -422,13 +423,14 @@ export function readFlowJson(workspaceRoot, flowId, flowSource, options = {}) {
|
|
|
422
423
|
export function getFlowYamlAbs(workspaceRoot, flowId, flowSource, options = {}) {
|
|
423
424
|
const root = path.resolve(workspaceRoot);
|
|
424
425
|
const archived = Boolean(options.archived);
|
|
426
|
+
const userPipelinesRoot = getUserPipelinesRoot(options.userId);
|
|
425
427
|
let yamlPath;
|
|
426
428
|
if (archived) {
|
|
427
429
|
if (flowSource === "builtin") {
|
|
428
430
|
return { error: t("catalog.builtin_flow_archive_path_not_supported") };
|
|
429
431
|
}
|
|
430
432
|
if (flowSource === "user") {
|
|
431
|
-
yamlPath = path.join(
|
|
433
|
+
yamlPath = path.join(userPipelinesRoot, ARCHIVED_PIPELINES_DIR_NAME, flowId, "flow.yaml");
|
|
432
434
|
} else if (flowSource === "workspace") {
|
|
433
435
|
yamlPath = path.join(root, PIPELINES_DIR, ARCHIVED_PIPELINES_DIR_NAME, flowId, "flow.yaml");
|
|
434
436
|
if (!fs.existsSync(yamlPath)) {
|
|
@@ -447,7 +449,7 @@ export function getFlowYamlAbs(workspaceRoot, flowId, flowSource, options = {})
|
|
|
447
449
|
if (flowSource === "builtin") {
|
|
448
450
|
yamlPath = path.join(PACKAGE_BUILTIN_PIPELINES_DIR, flowId, "flow.yaml");
|
|
449
451
|
} else if (flowSource === "user") {
|
|
450
|
-
yamlPath = path.join(
|
|
452
|
+
yamlPath = path.join(userPipelinesRoot, flowId, "flow.yaml");
|
|
451
453
|
if (!fs.existsSync(yamlPath)) {
|
|
452
454
|
const alt = path.join(root, PIPELINES_DIR, flowId, "flow.yaml");
|
|
453
455
|
if (fs.existsSync(alt)) yamlPath = alt;
|
|
@@ -474,6 +476,7 @@ export function getFlowYamlAbs(workspaceRoot, flowId, flowSource, options = {})
|
|
|
474
476
|
export function readNodeJson(workspaceRoot, nodeId, flowId, flowSource, opts = {}) {
|
|
475
477
|
const root = path.resolve(workspaceRoot);
|
|
476
478
|
const archived = Boolean(opts.archived);
|
|
479
|
+
const userPipelinesRoot = getUserPipelinesRoot(opts.userId);
|
|
477
480
|
const marketSpec = parseMarketplaceDefinitionId(nodeId);
|
|
478
481
|
if (marketSpec) {
|
|
479
482
|
let flowDir = root;
|
|
@@ -515,11 +518,9 @@ export function readNodeJson(workspaceRoot, nodeId, flowId, flowSource, opts = {
|
|
|
515
518
|
pathsToTry.push(path.join(PACKAGE_BUILTIN_PIPELINES_DIR, flowId, "nodes", fileName));
|
|
516
519
|
} else if (flowSource === "user") {
|
|
517
520
|
if (archived) {
|
|
518
|
-
pathsToTry.push(
|
|
519
|
-
path.join(getUserPipelinesRoot(), ARCHIVED_PIPELINES_DIR_NAME, flowId, "nodes", fileName),
|
|
520
|
-
);
|
|
521
|
+
pathsToTry.push(path.join(userPipelinesRoot, ARCHIVED_PIPELINES_DIR_NAME, flowId, "nodes", fileName));
|
|
521
522
|
} else {
|
|
522
|
-
pathsToTry.push(path.join(
|
|
523
|
+
pathsToTry.push(path.join(userPipelinesRoot, flowId, "nodes", fileName));
|
|
523
524
|
pathsToTry.push(path.join(root, PIPELINES_DIR, flowId, "nodes", fileName));
|
|
524
525
|
pathsToTry.push(path.join(root, LEGACY_PIPELINES_DIR, flowId, "nodes", fileName));
|
|
525
526
|
}
|
|
@@ -568,6 +569,184 @@ export function readNodeJson(workspaceRoot, nodeId, flowId, flowSource, opts = {
|
|
|
568
569
|
return { error: "Node not found: " + nodeId };
|
|
569
570
|
}
|
|
570
571
|
|
|
572
|
+
const NODE_DETAIL_MAX_FILES = 300;
|
|
573
|
+
const NODE_FILE_MAX_BYTES = 256 * 1024;
|
|
574
|
+
|
|
575
|
+
function isTextPreviewPath(filePath) {
|
|
576
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
577
|
+
return [
|
|
578
|
+
".md", ".mdx", ".txt", ".json", ".yaml", ".yml", ".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx",
|
|
579
|
+
".py", ".sh", ".css", ".html", ".xml", ".toml", ".ini", ".env", ".sql",
|
|
580
|
+
].includes(ext);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function listNodeFiles(baseDir, allowAllFiles, primaryFilePath = "") {
|
|
584
|
+
const root = path.resolve(baseDir);
|
|
585
|
+
const out = [];
|
|
586
|
+
const addFile = (filePath) => {
|
|
587
|
+
if (out.length >= NODE_DETAIL_MAX_FILES) return;
|
|
588
|
+
try {
|
|
589
|
+
const stat = fs.statSync(filePath);
|
|
590
|
+
if (!stat.isFile()) return;
|
|
591
|
+
const rel = path.relative(root, filePath).replace(/\\/g, "/");
|
|
592
|
+
out.push({
|
|
593
|
+
path: rel,
|
|
594
|
+
size: stat.size,
|
|
595
|
+
previewable: isTextPreviewPath(filePath),
|
|
596
|
+
});
|
|
597
|
+
} catch (_) {}
|
|
598
|
+
};
|
|
599
|
+
if (!allowAllFiles) {
|
|
600
|
+
if (primaryFilePath) addFile(primaryFilePath);
|
|
601
|
+
return out;
|
|
602
|
+
}
|
|
603
|
+
const walk = (dir) => {
|
|
604
|
+
if (out.length >= NODE_DETAIL_MAX_FILES) return;
|
|
605
|
+
let entries = [];
|
|
606
|
+
try {
|
|
607
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
608
|
+
} catch {
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
612
|
+
for (const entry of entries) {
|
|
613
|
+
if (out.length >= NODE_DETAIL_MAX_FILES) return;
|
|
614
|
+
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
615
|
+
const p = path.join(dir, entry.name);
|
|
616
|
+
if (entry.isDirectory()) walk(p);
|
|
617
|
+
else if (entry.isFile()) addFile(p);
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
walk(root);
|
|
621
|
+
return out;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function resolveMarkdownNodeFile(workspaceRoot, nodeId, flowId, flowSource, opts = {}) {
|
|
625
|
+
const root = path.resolve(workspaceRoot);
|
|
626
|
+
const archived = Boolean(opts.archived);
|
|
627
|
+
const userPipelinesRoot = getUserPipelinesRoot(opts.userId);
|
|
628
|
+
const fileName = nodeId.endsWith(".md") ? nodeId : `${nodeId}.md`;
|
|
629
|
+
const pathsToTry = [];
|
|
630
|
+
if (flowId && flowSource) {
|
|
631
|
+
if (flowSource === "builtin") {
|
|
632
|
+
pathsToTry.push(path.join(PACKAGE_BUILTIN_PIPELINES_DIR, flowId, "nodes", fileName));
|
|
633
|
+
} else if (flowSource === "user") {
|
|
634
|
+
if (archived) {
|
|
635
|
+
pathsToTry.push(path.join(userPipelinesRoot, ARCHIVED_PIPELINES_DIR_NAME, flowId, "nodes", fileName));
|
|
636
|
+
} else {
|
|
637
|
+
pathsToTry.push(path.join(userPipelinesRoot, flowId, "nodes", fileName));
|
|
638
|
+
pathsToTry.push(path.join(root, PIPELINES_DIR, flowId, "nodes", fileName));
|
|
639
|
+
pathsToTry.push(path.join(root, LEGACY_PIPELINES_DIR, flowId, "nodes", fileName));
|
|
640
|
+
}
|
|
641
|
+
} else if (flowSource === "workspace") {
|
|
642
|
+
if (archived) {
|
|
643
|
+
pathsToTry.push(path.join(root, PIPELINES_DIR, ARCHIVED_PIPELINES_DIR_NAME, flowId, "nodes", fileName));
|
|
644
|
+
pathsToTry.push(path.join(root, LEGACY_PIPELINES_DIR, ARCHIVED_PIPELINES_DIR_NAME, flowId, "nodes", fileName));
|
|
645
|
+
} else {
|
|
646
|
+
pathsToTry.push(path.join(root, PIPELINES_DIR, flowId, "nodes", fileName));
|
|
647
|
+
pathsToTry.push(path.join(root, LEGACY_PIPELINES_DIR, flowId, "nodes", fileName));
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
pathsToTry.push(path.join(root, PROJECT_NODES_DIR, fileName));
|
|
652
|
+
pathsToTry.push(path.join(root, LEGACY_NODES_DIR, fileName));
|
|
653
|
+
pathsToTry.push(path.join(PACKAGE_BUILTIN_NODES_DIR, fileName));
|
|
654
|
+
return pathsToTry.find((p) => fs.existsSync(p) && fs.statSync(p).isFile()) || "";
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function readNodeUsage(workspaceRoot, nodeId, opts = {}) {
|
|
658
|
+
const usage = [];
|
|
659
|
+
for (const flow of listFlowsJson(workspaceRoot, opts)) {
|
|
660
|
+
const flowPath = getFlowYamlAbs(workspaceRoot, flow.id, flow.source || "user", { archived: Boolean(flow.archived), userId: opts.userId });
|
|
661
|
+
if (!flowPath.path) continue;
|
|
662
|
+
try {
|
|
663
|
+
const data = yaml.load(fs.readFileSync(flowPath.path, "utf-8"));
|
|
664
|
+
const instances = data && typeof data === "object" ? data.instances : null;
|
|
665
|
+
if (!instances || typeof instances !== "object") continue;
|
|
666
|
+
const hits = Object.entries(instances)
|
|
667
|
+
.filter(([, inst]) => inst && inst.definitionId === nodeId)
|
|
668
|
+
.map(([instanceId, inst]) => ({ instanceId, label: inst.label || instanceId }));
|
|
669
|
+
if (hits.length > 0) {
|
|
670
|
+
usage.push({ flowId: flow.id, flowSource: flow.source || "user", archived: Boolean(flow.archived), instances: hits });
|
|
671
|
+
}
|
|
672
|
+
} catch (_) {}
|
|
673
|
+
}
|
|
674
|
+
return usage;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function resolveNodeFileScope(workspaceRoot, nodeId, flowId, flowSource, opts = {}) {
|
|
678
|
+
const marketSpec = parseMarketplaceDefinitionId(nodeId);
|
|
679
|
+
if (marketSpec) {
|
|
680
|
+
let flowDir = path.resolve(workspaceRoot);
|
|
681
|
+
let flowData = null;
|
|
682
|
+
if (flowId && flowSource) {
|
|
683
|
+
const flowPath = getFlowYamlAbs(workspaceRoot, flowId, flowSource, opts);
|
|
684
|
+
if (flowPath.path) {
|
|
685
|
+
flowDir = path.dirname(flowPath.path);
|
|
686
|
+
try {
|
|
687
|
+
const parsed = yaml.load(fs.readFileSync(flowPath.path, "utf-8"));
|
|
688
|
+
if (parsed && typeof parsed === "object") flowData = parsed;
|
|
689
|
+
} catch (_) {}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
const resolved = resolveMarketplaceNodePackage(workspaceRoot, flowDir, nodeId, flowData);
|
|
693
|
+
if (!resolved) return null;
|
|
694
|
+
return { baseDir: resolved.packageDir, allowAllFiles: true, primaryFilePath: path.join(resolved.packageDir, "node.yaml"), manifest: resolved };
|
|
695
|
+
}
|
|
696
|
+
const filePath = resolveMarkdownNodeFile(workspaceRoot, nodeId, flowId, flowSource, opts);
|
|
697
|
+
if (!filePath) return null;
|
|
698
|
+
return { baseDir: path.dirname(filePath), allowAllFiles: false, primaryFilePath: filePath, manifest: null };
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
export function readNodeDetailJson(workspaceRoot, nodeId, flowId = "", flowSource = "", opts = {}) {
|
|
702
|
+
if (!nodeId) return { error: "Missing node id" };
|
|
703
|
+
const node = readNodeJson(workspaceRoot, nodeId, flowId, flowSource, opts);
|
|
704
|
+
if (node.error) return node;
|
|
705
|
+
const scope = resolveNodeFileScope(workspaceRoot, nodeId, flowId, flowSource, opts);
|
|
706
|
+
const files = scope ? listNodeFiles(scope.baseDir, scope.allowAllFiles, scope.primaryFilePath) : [];
|
|
707
|
+
return {
|
|
708
|
+
node: { id: nodeId, ...node },
|
|
709
|
+
readOnly: true,
|
|
710
|
+
manifest: scope?.manifest || null,
|
|
711
|
+
runtime: node.runtime || scope?.manifest?.runtime || null,
|
|
712
|
+
body: node.executionLogic || "",
|
|
713
|
+
baseDir: scope?.baseDir || "",
|
|
714
|
+
files,
|
|
715
|
+
usage: readNodeUsage(workspaceRoot, nodeId, opts),
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
export function readNodeFilePreview(workspaceRoot, nodeId, relPath, flowId = "", flowSource = "", opts = {}) {
|
|
720
|
+
if (!nodeId) return { error: "Missing node id" };
|
|
721
|
+
const rel = String(relPath || "").trim();
|
|
722
|
+
if (!rel || rel.includes("\0") || path.isAbsolute(rel) || rel.split(/[\\/]+/).includes("..")) {
|
|
723
|
+
return { error: "Invalid file path" };
|
|
724
|
+
}
|
|
725
|
+
const scope = resolveNodeFileScope(workspaceRoot, nodeId, flowId, flowSource, opts);
|
|
726
|
+
if (!scope) return { error: "Node files not found" };
|
|
727
|
+
if (!scope.allowAllFiles) {
|
|
728
|
+
const primaryRel = path.relative(scope.baseDir, scope.primaryFilePath).replace(/\\/g, "/");
|
|
729
|
+
if (rel !== primaryRel) return { error: "File is outside node preview scope" };
|
|
730
|
+
}
|
|
731
|
+
const base = path.resolve(scope.baseDir);
|
|
732
|
+
const abs = path.resolve(base, rel);
|
|
733
|
+
if (abs !== base && !abs.startsWith(base + path.sep)) return { error: "File is outside node package" };
|
|
734
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) return { error: "File not found" };
|
|
735
|
+
const stat = fs.statSync(abs);
|
|
736
|
+
if (!isTextPreviewPath(abs)) {
|
|
737
|
+
return { path: rel, size: stat.size, binary: true, content: "", truncated: false };
|
|
738
|
+
}
|
|
739
|
+
const fd = fs.openSync(abs, "r");
|
|
740
|
+
try {
|
|
741
|
+
const len = Math.min(stat.size, NODE_FILE_MAX_BYTES);
|
|
742
|
+
const buf = Buffer.alloc(len);
|
|
743
|
+
fs.readSync(fd, buf, 0, len, 0);
|
|
744
|
+
return { path: rel, size: stat.size, binary: false, content: buf.toString("utf-8"), truncated: stat.size > len };
|
|
745
|
+
} finally {
|
|
746
|
+
fs.closeSync(fd);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
571
750
|
/** 列出所有 pipeline(包内 builtin + ~/agentflow/pipelines + 项目内 .workspace/.cursor agentflow/pipelines);nodes 见 PROJECT_NODES_DIR / LEGACY_NODES_DIR */
|
|
572
751
|
export function listPipelines(workspaceRoot) {
|
|
573
752
|
const rows = listFlowsJson(workspaceRoot);
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import fs from "fs";
|
|
8
8
|
import path from "path";
|
|
9
|
-
import { getAgentflowDataRoot } from "./paths.mjs";
|
|
9
|
+
import { getAgentflowDataRoot, sanitizeAgentflowUserId } from "./paths.mjs";
|
|
10
|
+
import { readUserEnvObject } from "./user-env.mjs";
|
|
10
11
|
import { resolveCliAndModel } from "./model-config.mjs";
|
|
11
12
|
import { runClaudeCodeAgentWithPrompt, runCursorAgentWithPrompt, runOpenCodeAgentWithPrompt } from "./agent-runners.mjs";
|
|
12
13
|
import { planComposerTasks, hasPlannerApiAvailable, shouldUsePhased, classifyComplexity, classifyTaskComplexity, PHASED_DEFINITIONS } from "./composer-planner.mjs";
|
|
@@ -23,6 +24,11 @@ const MAX_PROMPT_CHARS = 500_000;
|
|
|
23
24
|
const MAX_COMPOSER_VALIDATION_REPAIR = 5;
|
|
24
25
|
const MAX_SCRIPT_INJECT_BYTES = 30_000;
|
|
25
26
|
|
|
27
|
+
function agentflowUserEnv(userId) {
|
|
28
|
+
const safe = sanitizeAgentflowUserId(userId);
|
|
29
|
+
return safe ? { ...readUserEnvObject(safe), AGENTFLOW_USER_ID: safe } : {};
|
|
30
|
+
}
|
|
31
|
+
|
|
26
32
|
// ─── script 内容注入辅助 ─────────────────────────────────────────────────
|
|
27
33
|
|
|
28
34
|
/**
|
|
@@ -113,10 +119,12 @@ export function startComposerAgent(opts) {
|
|
|
113
119
|
const cliWs = opts.cliWorkspace ? String(opts.cliWorkspace) : getAgentflowDataRoot();
|
|
114
120
|
const modelKey = opts.modelKey != null ? String(opts.modelKey).trim() : "";
|
|
115
121
|
const { cli, model } = resolveCliAndModel(uiRoot, modelKey || null, null);
|
|
122
|
+
const env = agentflowUserEnv(opts.agentflowUserId);
|
|
116
123
|
|
|
117
124
|
const common = {
|
|
118
125
|
onStreamEvent: opts.onStreamEvent,
|
|
119
126
|
force: Boolean(opts.force),
|
|
127
|
+
env,
|
|
120
128
|
};
|
|
121
129
|
|
|
122
130
|
if (cli === "opencode") {
|
|
@@ -185,6 +193,10 @@ function buildAgentStepPrompt(step, flowContext) {
|
|
|
185
193
|
parts.push(`- flowId:${flowContext.flowId}`);
|
|
186
194
|
parts.push(`- flowSource:${flowContext.flowSource || "user"}`);
|
|
187
195
|
}
|
|
196
|
+
if (flowContext.userId) {
|
|
197
|
+
parts.push(`- agentflow 用户:${flowContext.userId}`);
|
|
198
|
+
parts.push(`- 执行 agentflow 命令时保留当前环境中的 AGENTFLOW_USER_ID,必要时显式前置 AGENTFLOW_USER_ID='${flowContext.userId}'。`);
|
|
199
|
+
}
|
|
188
200
|
if (flowContext.skillsHint) {
|
|
189
201
|
parts.push(flowContext.skillsHint);
|
|
190
202
|
}
|
|
@@ -344,6 +356,7 @@ export async function runComposerPostFlowValidationAndRepair(opts) {
|
|
|
344
356
|
const flowYamlAbs = String(opts.flowYamlAbs || "").trim();
|
|
345
357
|
const cliWs = opts.cliWorkspace ? String(opts.cliWorkspace) : getAgentflowDataRoot();
|
|
346
358
|
const maxRepair = Math.max(1, Math.min(10, Number(opts.maxRepairAttempts) || MAX_COMPOSER_VALIDATION_REPAIR));
|
|
359
|
+
const env = agentflowUserEnv(opts.agentflowUserId || opts.flowContext?.userId);
|
|
347
360
|
|
|
348
361
|
if (!uiRoot || !flowYamlAbs) {
|
|
349
362
|
return { ok: true, result: { skipped: true } };
|
|
@@ -405,6 +418,7 @@ export async function runComposerPostFlowValidationAndRepair(opts) {
|
|
|
405
418
|
onStreamEvent: stepEmit,
|
|
406
419
|
model: model || undefined,
|
|
407
420
|
force: Boolean(opts.force),
|
|
421
|
+
env,
|
|
408
422
|
});
|
|
409
423
|
setChild(handle.child);
|
|
410
424
|
await handle.finished;
|
|
@@ -413,6 +427,7 @@ export async function runComposerPostFlowValidationAndRepair(opts) {
|
|
|
413
427
|
onStreamEvent: stepEmit,
|
|
414
428
|
model: model || undefined,
|
|
415
429
|
force: Boolean(opts.force),
|
|
430
|
+
env,
|
|
416
431
|
});
|
|
417
432
|
setChild(handle.child);
|
|
418
433
|
await handle.finished;
|
|
@@ -421,6 +436,7 @@ export async function runComposerPostFlowValidationAndRepair(opts) {
|
|
|
421
436
|
onStreamEvent: stepEmit,
|
|
422
437
|
model: model || undefined,
|
|
423
438
|
force: Boolean(opts.force),
|
|
439
|
+
env,
|
|
424
440
|
});
|
|
425
441
|
setChild(handle.child);
|
|
426
442
|
await handle.finished;
|
|
@@ -490,6 +506,7 @@ export function startComposerMultiStep(opts) {
|
|
|
490
506
|
const emit = typeof opts.onStreamEvent === "function" ? opts.onStreamEvent : () => {};
|
|
491
507
|
let aborted = false;
|
|
492
508
|
let currentChild = null;
|
|
509
|
+
const env = agentflowUserEnv(opts.agentflowUserId || opts.flowContext?.userId);
|
|
493
510
|
|
|
494
511
|
const abort = () => {
|
|
495
512
|
aborted = true;
|
|
@@ -674,6 +691,7 @@ export function startComposerMultiStep(opts) {
|
|
|
674
691
|
onStreamEvent: stepEmit,
|
|
675
692
|
model: model || undefined,
|
|
676
693
|
force: Boolean(opts.force),
|
|
694
|
+
env,
|
|
677
695
|
});
|
|
678
696
|
currentChild = handle.child;
|
|
679
697
|
await handle.finished;
|
|
@@ -682,6 +700,7 @@ export function startComposerMultiStep(opts) {
|
|
|
682
700
|
onStreamEvent: stepEmit,
|
|
683
701
|
model: model || undefined,
|
|
684
702
|
force: Boolean(opts.force),
|
|
703
|
+
env,
|
|
685
704
|
});
|
|
686
705
|
currentChild = handle.child;
|
|
687
706
|
await handle.finished;
|
|
@@ -690,6 +709,7 @@ export function startComposerMultiStep(opts) {
|
|
|
690
709
|
onStreamEvent: stepEmit,
|
|
691
710
|
model: model || undefined,
|
|
692
711
|
force: Boolean(opts.force),
|
|
712
|
+
env,
|
|
693
713
|
});
|
|
694
714
|
currentChild = handle.child;
|
|
695
715
|
await handle.finished;
|
|
@@ -757,6 +777,7 @@ export function startComposerMultiStep(opts) {
|
|
|
757
777
|
flowContext: opts.flowContext,
|
|
758
778
|
modelKey: opts.modelKey,
|
|
759
779
|
force: Boolean(opts.force),
|
|
780
|
+
agentflowUserId: opts.agentflowUserId || opts.flowContext?.userId,
|
|
760
781
|
onStreamEvent: emit,
|
|
761
782
|
getAborted: () => aborted,
|
|
762
783
|
setCurrentChild: (c) => {
|