@fieldwangai/agentflow 0.1.34 → 0.1.36
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/bin/lib/agent-runners.mjs +17 -0
- package/bin/lib/auth.mjs +58 -0
- package/bin/lib/catalog-flows.mjs +3 -1
- package/bin/lib/composer-agent.mjs +4 -0
- package/bin/lib/composer-skill-router.mjs +23 -4
- package/bin/lib/git-worktree.mjs +57 -8
- package/bin/lib/locales/en.json +4 -0
- package/bin/lib/locales/zh.json +4 -0
- package/bin/lib/marketplace.mjs +21 -4
- package/bin/lib/runtime-context.mjs +22 -4
- package/bin/lib/skill-registry.mjs +49 -3
- package/bin/lib/ui-server.mjs +1177 -29
- package/bin/pipeline/pre-process-node.mjs +4 -0
- package/builtin/nodes/agent_subAgent.md +4 -1
- package/builtin/nodes/display_chart.md +31 -0
- package/builtin/nodes/tool_git_worktree_load.md +10 -0
- package/builtin/nodes/tool_nodejs.md +3 -0
- package/builtin/web-ui/dist/assets/index-7-343AUn.js +214 -0
- package/builtin/web-ui/dist/assets/index-CPsrRISH.css +1 -0
- package/builtin/web-ui/dist/assets/index-DgQRkS4v.js +61 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/builtin/web-ui/dist/assets/index-B1j_UaHw.js +0 -202
- package/builtin/web-ui/dist/assets/index-ChiTnW0H.css +0 -1
package/bin/lib/ui-server.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 本地 HTTP:静态 UI + /api/flows(GET/POST/HEAD)、/api/flows/import(POST multipart 导入 .yaml/.zip)、/api/flow/archive(POST)、/api/flow/delete(POST 永久删除)、/api/model-lists、/api/ui-context、/api/pipeline-recent-runs、/api/run-node-statuses(GET 某次 run 各节点磁盘状态)、/api/workspace-tree(GET 工作区目录树)、/api/nodes、/api/flow(GET/POST)、
|
|
3
|
-
* /api/flow-editor-sync(POST 通知画布刷新)、/api/flow-editor-sync-events(GET SSE)、/api/flow/run(POST NDJSON 流式执行 agentflow apply --machine-readable)、/api/flow/run/stop(POST
|
|
3
|
+
* /api/flow-editor-sync(POST 通知画布刷新)、/api/flow-editor-sync-events(GET SSE)、/api/flow/run(POST NDJSON 流式执行 agentflow apply --machine-readable)、/api/flow/run/stop(POST 终止运行)、/api/workspace/run/stop(POST 终止 Workspace 临时运行)、
|
|
4
4
|
* /api/composer-agent(POST NDJSON;有 flow 时结束后 validate-flow,失败则自动 agent 修复至多 5 次)、
|
|
5
5
|
* /api/agentflow-config(GET/POST 读写 ~/agentflow/config.json 的 opencodeProvider;POST 后执行 update-model-lists)、/api/update-model-lists(POST 可选 JSON body.opencodeProvider 覆盖本次拉取用的 Provider,未保存 config 也可用);
|
|
6
6
|
* listen 后后台 updateModelLists
|
|
@@ -89,8 +89,10 @@ import {
|
|
|
89
89
|
buildClearSessionCookie,
|
|
90
90
|
buildSessionCookie,
|
|
91
91
|
getAuthUserFromRequest,
|
|
92
|
+
isAuthUserAllowed,
|
|
92
93
|
loginOrCreateUser,
|
|
93
94
|
logoutRequest,
|
|
95
|
+
readUserAllowlist,
|
|
94
96
|
} from "./auth.mjs";
|
|
95
97
|
import { readUserEnvObject, readUserEnvRows, writeUserEnvRows } from "./user-env.mjs";
|
|
96
98
|
|
|
@@ -297,6 +299,45 @@ function writeSkillCollectionConfig(userCtx = {}, payload = {}, availableSkills
|
|
|
297
299
|
return config;
|
|
298
300
|
}
|
|
299
301
|
|
|
302
|
+
function upsertSkillhubCollectionGroup(userCtx = {}, collectionId = "", beforeSkills = [], afterSkills = [], collectionName = "") {
|
|
303
|
+
const rawCollectionId = String(collectionId || "").trim();
|
|
304
|
+
if (!rawCollectionId) return null;
|
|
305
|
+
const beforeKeys = new Set((Array.isArray(beforeSkills) ? beforeSkills : []).map((skill) => String(skill?.key || "")).filter(Boolean));
|
|
306
|
+
const addedKeys = (Array.isArray(afterSkills) ? afterSkills : [])
|
|
307
|
+
.map((skill) => String(skill?.key || "").trim())
|
|
308
|
+
.filter((key) => key && !beforeKeys.has(key));
|
|
309
|
+
const config = readSkillCollectionConfig(userCtx, afterSkills);
|
|
310
|
+
const groupId = slugifySkillCollectionId(`skillhub-collection-${rawCollectionId}`, "skillhub-collection");
|
|
311
|
+
const now = Date.now();
|
|
312
|
+
const existing = config.collections.find((collection) => collection.id === groupId);
|
|
313
|
+
const existingKeys = Array.isArray(existing?.skillKeys) ? existing.skillKeys : [];
|
|
314
|
+
const mergedKeys = Array.from(new Set([...existingKeys, ...addedKeys]));
|
|
315
|
+
const nextCollections = config.collections.filter((collection) => collection.id !== groupId);
|
|
316
|
+
nextCollections.push({
|
|
317
|
+
id: groupId,
|
|
318
|
+
name: String(collectionName || "").trim() || `SkillHub Collection ${rawCollectionId}`,
|
|
319
|
+
skillKeys: mergedKeys,
|
|
320
|
+
builtin: false,
|
|
321
|
+
createdAt: Number.isFinite(existing?.createdAt) ? existing.createdAt : now,
|
|
322
|
+
updatedAt: now,
|
|
323
|
+
});
|
|
324
|
+
return writeSkillCollectionConfig(userCtx, { version: 1, collections: nextCollections }, afterSkills);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function removeSkillhubCollectionGroup(userCtx = {}, collectionId = "", root = process.cwd()) {
|
|
328
|
+
const rawCollectionId = String(collectionId || "").trim();
|
|
329
|
+
if (!rawCollectionId) return null;
|
|
330
|
+
const availableSkills = listComposerSkills(PACKAGE_ROOT, root);
|
|
331
|
+
const config = readSkillCollectionConfig(userCtx, availableSkills);
|
|
332
|
+
const groupId = slugifySkillCollectionId(`skillhub-collection-${rawCollectionId}`, "skillhub-collection");
|
|
333
|
+
if (!config.collections.some((collection) => collection.id === groupId)) return config;
|
|
334
|
+
return writeSkillCollectionConfig(
|
|
335
|
+
userCtx,
|
|
336
|
+
{ version: 1, collections: config.collections.filter((collection) => collection.id !== groupId) },
|
|
337
|
+
availableSkills,
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
300
341
|
function runtimeEnvForUser(userCtx = {}, extra = {}) {
|
|
301
342
|
return {
|
|
302
343
|
...process.env,
|
|
@@ -317,6 +358,448 @@ function readAgentflowUserConfigObject() {
|
|
|
317
358
|
}
|
|
318
359
|
}
|
|
319
360
|
|
|
361
|
+
function cursorMcpConfigPath() {
|
|
362
|
+
return path.join(os.homedir(), ".cursor", "mcp.json");
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function readCursorMcpConfig() {
|
|
366
|
+
const p = cursorMcpConfigPath();
|
|
367
|
+
try {
|
|
368
|
+
if (!fs.existsSync(p)) return { mcpServers: {} };
|
|
369
|
+
const data = JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
370
|
+
return data && typeof data === "object" && !Array.isArray(data) ? data : { mcpServers: {} };
|
|
371
|
+
} catch {
|
|
372
|
+
return { mcpServers: {} };
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function userMcpPrivatePath(userCtx = {}) {
|
|
377
|
+
return path.join(getAgentflowUserDataRoot(userCtx.userId), "mcp-private.json");
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function readUserMcpPrivate(userCtx = {}) {
|
|
381
|
+
const p = userMcpPrivatePath(userCtx);
|
|
382
|
+
try {
|
|
383
|
+
if (!fs.existsSync(p)) return { version: 1, servers: {} };
|
|
384
|
+
const data = JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
385
|
+
const servers = data?.servers && typeof data.servers === "object" && !Array.isArray(data.servers) ? data.servers : {};
|
|
386
|
+
return { version: 1, servers };
|
|
387
|
+
} catch {
|
|
388
|
+
return { version: 1, servers: {} };
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function writeUserMcpPrivate(userCtx = {}, data = {}) {
|
|
393
|
+
const p = userMcpPrivatePath(userCtx);
|
|
394
|
+
const servers = data?.servers && typeof data.servers === "object" && !Array.isArray(data.servers) ? data.servers : {};
|
|
395
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
396
|
+
fs.writeFileSync(p, JSON.stringify({ version: 1, servers }, null, 2) + "\n", "utf-8");
|
|
397
|
+
return { version: 1, servers };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function normalizeMcpPrivateKeys(keys) {
|
|
401
|
+
return new Set((Array.isArray(keys) ? keys : []).map((key) => String(key || "").trim()).filter(Boolean));
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function pickObjectKeys(obj, keys) {
|
|
405
|
+
const out = {};
|
|
406
|
+
for (const key of keys) {
|
|
407
|
+
if (obj && Object.prototype.hasOwnProperty.call(obj, key)) out[key] = String(obj[key] ?? "");
|
|
408
|
+
}
|
|
409
|
+
return out;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function omitObjectKeys(obj, keys) {
|
|
413
|
+
const out = {};
|
|
414
|
+
for (const [key, value] of Object.entries(obj && typeof obj === "object" ? obj : {})) {
|
|
415
|
+
if (!keys.has(key)) out[key] = value;
|
|
416
|
+
}
|
|
417
|
+
return out;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function privateKeyMetadataFromConfig(configValue = {}) {
|
|
421
|
+
const meta = configValue?.__agentflowPrivateKeys;
|
|
422
|
+
const env = Array.isArray(meta?.env) ? meta.env.map((key) => String(key || "").trim()).filter(Boolean) : [];
|
|
423
|
+
const headers = Array.isArray(meta?.headers) ? meta.headers.map((key) => String(key || "").trim()).filter(Boolean) : [];
|
|
424
|
+
return { env: Array.from(new Set(env)), headers: Array.from(new Set(headers)) };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function withPrivatePlaceholders(obj, keys) {
|
|
428
|
+
const out = { ...(obj && typeof obj === "object" && !Array.isArray(obj) ? obj : {}) };
|
|
429
|
+
for (const key of keys) {
|
|
430
|
+
if (key && !Object.prototype.hasOwnProperty.call(out, key)) out[key] = "";
|
|
431
|
+
}
|
|
432
|
+
return out;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function normalizeMcpServerConfig(value) {
|
|
436
|
+
const raw = value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
437
|
+
const next = {};
|
|
438
|
+
const url = typeof raw.url === "string" ? raw.url.trim() : "";
|
|
439
|
+
const command = typeof raw.command === "string" ? raw.command.trim() : "";
|
|
440
|
+
const description = typeof raw.description === "string" ? raw.description.trim() : "";
|
|
441
|
+
if (url) next.url = url;
|
|
442
|
+
if (command) next.command = command;
|
|
443
|
+
if (Array.isArray(raw.args)) next.args = raw.args.map((x) => String(x)).filter((x) => x.length > 0);
|
|
444
|
+
if (raw.env && typeof raw.env === "object" && !Array.isArray(raw.env)) {
|
|
445
|
+
const env = {};
|
|
446
|
+
for (const [k, v] of Object.entries(raw.env)) {
|
|
447
|
+
const key = String(k || "").trim();
|
|
448
|
+
if (key) env[key] = String(v ?? "");
|
|
449
|
+
}
|
|
450
|
+
if (Object.keys(env).length) next.env = env;
|
|
451
|
+
}
|
|
452
|
+
if (raw.headers && typeof raw.headers === "object" && !Array.isArray(raw.headers)) {
|
|
453
|
+
const headers = {};
|
|
454
|
+
for (const [k, v] of Object.entries(raw.headers)) {
|
|
455
|
+
const key = String(k || "").trim();
|
|
456
|
+
if (key) headers[key] = String(v ?? "");
|
|
457
|
+
}
|
|
458
|
+
if (Object.keys(headers).length) next.headers = headers;
|
|
459
|
+
}
|
|
460
|
+
if (description) next.description = description;
|
|
461
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
462
|
+
if (["url", "command", "args", "env", "headers", "description"].includes(k)) continue;
|
|
463
|
+
next[k] = v;
|
|
464
|
+
}
|
|
465
|
+
return next;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function readCursorMcpServers(userCtx = {}) {
|
|
469
|
+
const config = readCursorMcpConfig();
|
|
470
|
+
const privateConfig = readUserMcpPrivate(userCtx);
|
|
471
|
+
const rawServers = config.mcpServers && typeof config.mcpServers === "object" && !Array.isArray(config.mcpServers)
|
|
472
|
+
? config.mcpServers
|
|
473
|
+
: {};
|
|
474
|
+
const servers = Object.entries(rawServers).map(([name, value]) => {
|
|
475
|
+
const publicValue = normalizeMcpServerConfig(value);
|
|
476
|
+
const privateValue = privateConfig.servers?.[name] && typeof privateConfig.servers[name] === "object" ? privateConfig.servers[name] : {};
|
|
477
|
+
const privateEnv = privateValue.env && typeof privateValue.env === "object" && !Array.isArray(privateValue.env) ? privateValue.env : {};
|
|
478
|
+
const privateHeaders = privateValue.headers && typeof privateValue.headers === "object" && !Array.isArray(privateValue.headers) ? privateValue.headers : {};
|
|
479
|
+
const privateMeta = privateKeyMetadataFromConfig(publicValue);
|
|
480
|
+
const privateEnvKeys = Array.from(new Set([...privateMeta.env, ...Object.keys(privateEnv)]));
|
|
481
|
+
const privateHeaderKeys = Array.from(new Set([...privateMeta.headers, ...Object.keys(privateHeaders)]));
|
|
482
|
+
const configValue = {
|
|
483
|
+
...publicValue,
|
|
484
|
+
env: { ...withPrivatePlaceholders(publicValue.env || {}, privateEnvKeys), ...privateEnv },
|
|
485
|
+
headers: { ...withPrivatePlaceholders(publicValue.headers || {}, privateHeaderKeys), ...privateHeaders },
|
|
486
|
+
};
|
|
487
|
+
return {
|
|
488
|
+
name,
|
|
489
|
+
type: configValue.url ? "url" : "command",
|
|
490
|
+
url: typeof configValue.url === "string" ? configValue.url : "",
|
|
491
|
+
command: typeof configValue.command === "string" ? configValue.command : "",
|
|
492
|
+
args: Array.isArray(configValue.args) ? configValue.args : [],
|
|
493
|
+
env: configValue.env && typeof configValue.env === "object" ? configValue.env : {},
|
|
494
|
+
headers: configValue.headers && typeof configValue.headers === "object" ? configValue.headers : {},
|
|
495
|
+
description: typeof configValue.description === "string" ? configValue.description : "",
|
|
496
|
+
raw: configValue,
|
|
497
|
+
privateEnvKeys,
|
|
498
|
+
privateHeaderKeys,
|
|
499
|
+
};
|
|
500
|
+
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
501
|
+
return { path: cursorMcpConfigPath(), servers };
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function writeCursorMcpServer(payload = {}, userCtx = {}) {
|
|
505
|
+
const name = String(payload?.name || "").trim();
|
|
506
|
+
const nextName = String(payload?.nextName || payload?.name || "").trim();
|
|
507
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(nextName)) throw new Error("Invalid MCP name");
|
|
508
|
+
const server = normalizeMcpServerConfig(payload?.server);
|
|
509
|
+
if (!server.url && !server.command) throw new Error("MCP server requires url or command");
|
|
510
|
+
const privateEnvKeys = normalizeMcpPrivateKeys(payload?.privateEnvKeys);
|
|
511
|
+
const privateHeaderKeys = normalizeMcpPrivateKeys(payload?.privateHeaderKeys);
|
|
512
|
+
const privateEnv = pickObjectKeys(server.env || {}, privateEnvKeys);
|
|
513
|
+
const privateHeaders = pickObjectKeys(server.headers || {}, privateHeaderKeys);
|
|
514
|
+
const publicServer = {
|
|
515
|
+
...server,
|
|
516
|
+
env: omitObjectKeys(server.env || {}, privateEnvKeys),
|
|
517
|
+
headers: omitObjectKeys(server.headers || {}, privateHeaderKeys),
|
|
518
|
+
};
|
|
519
|
+
publicServer.env = withPrivatePlaceholders(publicServer.env, privateEnvKeys);
|
|
520
|
+
publicServer.headers = withPrivatePlaceholders(publicServer.headers, privateHeaderKeys);
|
|
521
|
+
if (privateEnvKeys.size || privateHeaderKeys.size) {
|
|
522
|
+
publicServer.__agentflowPrivateKeys = {
|
|
523
|
+
...(privateEnvKeys.size ? { env: Array.from(privateEnvKeys) } : {}),
|
|
524
|
+
...(privateHeaderKeys.size ? { headers: Array.from(privateHeaderKeys) } : {}),
|
|
525
|
+
};
|
|
526
|
+
} else {
|
|
527
|
+
delete publicServer.__agentflowPrivateKeys;
|
|
528
|
+
}
|
|
529
|
+
if (!Object.keys(publicServer.env).length) delete publicServer.env;
|
|
530
|
+
if (!Object.keys(publicServer.headers).length) delete publicServer.headers;
|
|
531
|
+
const p = cursorMcpConfigPath();
|
|
532
|
+
const config = readCursorMcpConfig();
|
|
533
|
+
const mcpServers = config.mcpServers && typeof config.mcpServers === "object" && !Array.isArray(config.mcpServers)
|
|
534
|
+
? { ...config.mcpServers }
|
|
535
|
+
: {};
|
|
536
|
+
if (name && name !== nextName) delete mcpServers[name];
|
|
537
|
+
mcpServers[nextName] = publicServer;
|
|
538
|
+
const next = { ...config, mcpServers };
|
|
539
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
540
|
+
fs.writeFileSync(p, JSON.stringify(next, null, 2) + "\n", "utf-8");
|
|
541
|
+
const privateConfig = readUserMcpPrivate(userCtx);
|
|
542
|
+
const privateServers = { ...(privateConfig.servers || {}) };
|
|
543
|
+
if (name && name !== nextName) delete privateServers[name];
|
|
544
|
+
if (Object.keys(privateEnv).length || Object.keys(privateHeaders).length) {
|
|
545
|
+
privateServers[nextName] = {
|
|
546
|
+
...(Object.keys(privateEnv).length ? { env: privateEnv } : {}),
|
|
547
|
+
...(Object.keys(privateHeaders).length ? { headers: privateHeaders } : {}),
|
|
548
|
+
};
|
|
549
|
+
} else {
|
|
550
|
+
delete privateServers[nextName];
|
|
551
|
+
}
|
|
552
|
+
writeUserMcpPrivate(userCtx, { servers: privateServers });
|
|
553
|
+
return readCursorMcpServers(userCtx);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function deleteCursorMcpServer(name, userCtx = {}) {
|
|
557
|
+
const key = String(name || "").trim();
|
|
558
|
+
if (!key) throw new Error("Missing MCP name");
|
|
559
|
+
const p = cursorMcpConfigPath();
|
|
560
|
+
const config = readCursorMcpConfig();
|
|
561
|
+
const mcpServers = config.mcpServers && typeof config.mcpServers === "object" && !Array.isArray(config.mcpServers)
|
|
562
|
+
? { ...config.mcpServers }
|
|
563
|
+
: {};
|
|
564
|
+
delete mcpServers[key];
|
|
565
|
+
const next = { ...config, mcpServers };
|
|
566
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
567
|
+
fs.writeFileSync(p, JSON.stringify(next, null, 2) + "\n", "utf-8");
|
|
568
|
+
const privateConfig = readUserMcpPrivate(userCtx);
|
|
569
|
+
const privateServers = { ...(privateConfig.servers || {}) };
|
|
570
|
+
delete privateServers[key];
|
|
571
|
+
writeUserMcpPrivate(userCtx, { servers: privateServers });
|
|
572
|
+
return readCursorMcpServers(userCtx);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function compactErrorMessage(error) {
|
|
576
|
+
const text = String(error?.message || error || "").trim();
|
|
577
|
+
return text.length > 260 ? `${text.slice(0, 257)}...` : text;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function parseMcpSsePayload(text) {
|
|
581
|
+
const events = [];
|
|
582
|
+
let data = [];
|
|
583
|
+
for (const rawLine of String(text || "").split(/\r?\n/g)) {
|
|
584
|
+
const line = rawLine.trimEnd();
|
|
585
|
+
if (!line) {
|
|
586
|
+
if (data.length) {
|
|
587
|
+
const joined = data.join("\n").trim();
|
|
588
|
+
if (joined) events.push(joined);
|
|
589
|
+
data = [];
|
|
590
|
+
}
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
if (line.startsWith("data:")) data.push(line.slice(5).trimStart());
|
|
594
|
+
}
|
|
595
|
+
if (data.length) events.push(data.join("\n").trim());
|
|
596
|
+
for (const event of events) {
|
|
597
|
+
try {
|
|
598
|
+
const parsed = JSON.parse(event);
|
|
599
|
+
if (parsed && typeof parsed === "object") return parsed;
|
|
600
|
+
} catch {}
|
|
601
|
+
}
|
|
602
|
+
return null;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
async function mcpHttpRequest(url, headers, body, sessionId = "") {
|
|
606
|
+
const controller = new AbortController();
|
|
607
|
+
const timer = setTimeout(() => controller.abort(), 8000);
|
|
608
|
+
try {
|
|
609
|
+
const response = await fetch(url, {
|
|
610
|
+
method: "POST",
|
|
611
|
+
headers: {
|
|
612
|
+
"Accept": "application/json, text/event-stream",
|
|
613
|
+
"Content-Type": "application/json",
|
|
614
|
+
...(headers || {}),
|
|
615
|
+
...(sessionId ? { "Mcp-Session-Id": sessionId } : {}),
|
|
616
|
+
},
|
|
617
|
+
body: JSON.stringify(body),
|
|
618
|
+
signal: controller.signal,
|
|
619
|
+
});
|
|
620
|
+
const text = await response.text();
|
|
621
|
+
if (!response.ok) throw new Error(`${response.status} ${response.statusText}: ${text.slice(0, 180)}`);
|
|
622
|
+
const contentType = String(response.headers.get("content-type") || "").toLowerCase();
|
|
623
|
+
const parsed = contentType.includes("text/event-stream") ? parseMcpSsePayload(text) : JSON.parse(text || "{}");
|
|
624
|
+
return { message: parsed, sessionId: response.headers.get("mcp-session-id") || sessionId };
|
|
625
|
+
} finally {
|
|
626
|
+
clearTimeout(timer);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
async function checkMcpHttpServer(server) {
|
|
631
|
+
const url = String(server?.raw?.url || server?.url || "").trim();
|
|
632
|
+
if (!url) throw new Error("Missing MCP URL");
|
|
633
|
+
const headers = server?.raw?.headers && typeof server.raw.headers === "object" ? server.raw.headers : {};
|
|
634
|
+
const init = await mcpHttpRequest(url, headers, {
|
|
635
|
+
jsonrpc: "2.0",
|
|
636
|
+
id: 1,
|
|
637
|
+
method: "initialize",
|
|
638
|
+
params: {
|
|
639
|
+
protocolVersion: "2024-11-05",
|
|
640
|
+
capabilities: {},
|
|
641
|
+
clientInfo: { name: "agentflow", version: "0.1.0" },
|
|
642
|
+
},
|
|
643
|
+
});
|
|
644
|
+
if (init.message?.error) throw new Error(init.message.error.message || "MCP initialize failed");
|
|
645
|
+
await mcpHttpRequest(url, headers, {
|
|
646
|
+
jsonrpc: "2.0",
|
|
647
|
+
method: "notifications/initialized",
|
|
648
|
+
params: {},
|
|
649
|
+
}, init.sessionId).catch(() => null);
|
|
650
|
+
const tools = await mcpHttpRequest(url, headers, {
|
|
651
|
+
jsonrpc: "2.0",
|
|
652
|
+
id: 2,
|
|
653
|
+
method: "tools/list",
|
|
654
|
+
params: {},
|
|
655
|
+
}, init.sessionId);
|
|
656
|
+
if (tools.message?.error) throw new Error(tools.message.error.message || "MCP tools/list failed");
|
|
657
|
+
return Array.isArray(tools.message?.result?.tools) ? tools.message.result.tools : [];
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
async function checkMcpStdioServer(server) {
|
|
661
|
+
const command = String(server?.raw?.command || server?.command || "").trim();
|
|
662
|
+
if (!command) throw new Error("Missing MCP command");
|
|
663
|
+
const args = Array.isArray(server?.raw?.args) ? server.raw.args.map(String) : [];
|
|
664
|
+
const env = server?.raw?.env && typeof server.raw.env === "object" ? server.raw.env : {};
|
|
665
|
+
const child = spawn(command, args, {
|
|
666
|
+
cwd: os.homedir(),
|
|
667
|
+
env: { ...process.env, ...env },
|
|
668
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
669
|
+
});
|
|
670
|
+
let buffer = "";
|
|
671
|
+
let stderr = "";
|
|
672
|
+
let processError = null;
|
|
673
|
+
const pending = new Map();
|
|
674
|
+
let nextId = 1;
|
|
675
|
+
const cleanup = () => {
|
|
676
|
+
for (const [, request] of pending) clearTimeout(request.timer);
|
|
677
|
+
pending.clear();
|
|
678
|
+
if (!child.killed) child.kill("SIGTERM");
|
|
679
|
+
};
|
|
680
|
+
const rejectPending = (error) => {
|
|
681
|
+
for (const [, request] of pending) {
|
|
682
|
+
clearTimeout(request.timer);
|
|
683
|
+
request.reject(error);
|
|
684
|
+
}
|
|
685
|
+
pending.clear();
|
|
686
|
+
};
|
|
687
|
+
child.on("error", (error) => {
|
|
688
|
+
processError = error;
|
|
689
|
+
rejectPending(error);
|
|
690
|
+
});
|
|
691
|
+
child.stdin.on("error", (error) => {
|
|
692
|
+
processError = error;
|
|
693
|
+
rejectPending(error);
|
|
694
|
+
});
|
|
695
|
+
child.stdout.setEncoding("utf8");
|
|
696
|
+
child.stderr.setEncoding("utf8");
|
|
697
|
+
child.stderr.on("data", (chunk) => {
|
|
698
|
+
stderr += String(chunk || "");
|
|
699
|
+
if (stderr.length > 2000) stderr = stderr.slice(-2000);
|
|
700
|
+
});
|
|
701
|
+
child.stdout.on("data", (chunk) => {
|
|
702
|
+
buffer += String(chunk || "");
|
|
703
|
+
const lines = buffer.split(/\r?\n/g);
|
|
704
|
+
buffer = lines.pop() || "";
|
|
705
|
+
for (const line of lines) {
|
|
706
|
+
const text = line.trim();
|
|
707
|
+
if (!text) continue;
|
|
708
|
+
let message = null;
|
|
709
|
+
try {
|
|
710
|
+
message = JSON.parse(text);
|
|
711
|
+
} catch {
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
714
|
+
const request = pending.get(message.id);
|
|
715
|
+
if (request) {
|
|
716
|
+
pending.delete(message.id);
|
|
717
|
+
clearTimeout(request.timer);
|
|
718
|
+
request.resolve(message);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
const send = (method, params = {}, timeoutMs = 8000) => new Promise((resolve, reject) => {
|
|
723
|
+
if (processError) {
|
|
724
|
+
reject(processError);
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
const id = nextId++;
|
|
728
|
+
const timer = setTimeout(() => {
|
|
729
|
+
pending.delete(id);
|
|
730
|
+
reject(new Error(`${method} timed out${stderr.trim() ? `: ${stderr.trim().slice(-220)}` : ""}`));
|
|
731
|
+
}, timeoutMs);
|
|
732
|
+
pending.set(id, { resolve, reject, timer });
|
|
733
|
+
child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n", (error) => {
|
|
734
|
+
if (!error) return;
|
|
735
|
+
pending.delete(id);
|
|
736
|
+
clearTimeout(timer);
|
|
737
|
+
reject(error);
|
|
738
|
+
});
|
|
739
|
+
});
|
|
740
|
+
const notify = (method, params = {}) => {
|
|
741
|
+
child.stdin.write(JSON.stringify({ jsonrpc: "2.0", method, params }) + "\n");
|
|
742
|
+
};
|
|
743
|
+
try {
|
|
744
|
+
const init = await send("initialize", {
|
|
745
|
+
protocolVersion: "2024-11-05",
|
|
746
|
+
capabilities: {},
|
|
747
|
+
clientInfo: { name: "agentflow", version: "0.1.0" },
|
|
748
|
+
});
|
|
749
|
+
if (init?.error) throw new Error(init.error.message || "MCP initialize failed");
|
|
750
|
+
notify("notifications/initialized", {});
|
|
751
|
+
const tools = await send("tools/list", {}, 8000);
|
|
752
|
+
if (tools?.error) throw new Error(tools.error.message || "MCP tools/list failed");
|
|
753
|
+
return Array.isArray(tools?.result?.tools) ? tools.result.tools : [];
|
|
754
|
+
} finally {
|
|
755
|
+
cleanup();
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
async function checkMcpServer(server) {
|
|
760
|
+
const startedAt = Date.now();
|
|
761
|
+
try {
|
|
762
|
+
const tools = server?.type === "url" || server?.raw?.url
|
|
763
|
+
? await checkMcpHttpServer(server)
|
|
764
|
+
: await checkMcpStdioServer(server);
|
|
765
|
+
return {
|
|
766
|
+
name: server.name,
|
|
767
|
+
ok: true,
|
|
768
|
+
status: "enabled",
|
|
769
|
+
toolCount: tools.length,
|
|
770
|
+
tools: tools.map((tool) => ({
|
|
771
|
+
name: String(tool?.name || ""),
|
|
772
|
+
description: String(tool?.description || ""),
|
|
773
|
+
})).filter((tool) => tool.name),
|
|
774
|
+
checkedAt: new Date().toISOString(),
|
|
775
|
+
elapsedMs: Date.now() - startedAt,
|
|
776
|
+
};
|
|
777
|
+
} catch (error) {
|
|
778
|
+
return {
|
|
779
|
+
name: server?.name || "",
|
|
780
|
+
ok: false,
|
|
781
|
+
status: "error",
|
|
782
|
+
error: compactErrorMessage(error),
|
|
783
|
+
toolCount: 0,
|
|
784
|
+
tools: [],
|
|
785
|
+
checkedAt: new Date().toISOString(),
|
|
786
|
+
elapsedMs: Date.now() - startedAt,
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
async function checkCursorMcpServers(name = "", userCtx = {}) {
|
|
792
|
+
const { servers } = readCursorMcpServers(userCtx);
|
|
793
|
+
const targetName = String(name || "").trim();
|
|
794
|
+
const targets = targetName ? servers.filter((server) => server.name === targetName) : servers;
|
|
795
|
+
if (targetName && targets.length === 0) throw new Error("MCP server not found");
|
|
796
|
+
const results = [];
|
|
797
|
+
for (const server of targets) {
|
|
798
|
+
results.push(await checkMcpServer(server));
|
|
799
|
+
}
|
|
800
|
+
return { results };
|
|
801
|
+
}
|
|
802
|
+
|
|
320
803
|
function readModelListsFromDisk(workspaceRoot) {
|
|
321
804
|
const p = getModelListsAbs();
|
|
322
805
|
const empty = {
|
|
@@ -344,6 +827,8 @@ function readModelListsFromDisk(workspaceRoot) {
|
|
|
344
827
|
}
|
|
345
828
|
|
|
346
829
|
const SKILLHUB_TIMEOUT_MS = 60_000;
|
|
830
|
+
const SKILLHUB_API_BASE = String(process.env.SKILLHUB_API_BASE || "https://skillhub.bigo.sg/api/v1").replace(/\/+$/, "");
|
|
831
|
+
const skillhubCollectionInfoCache = new Map();
|
|
347
832
|
|
|
348
833
|
function runSkillhub(args, opts = {}) {
|
|
349
834
|
return new Promise((resolve) => {
|
|
@@ -369,6 +854,72 @@ function runSkillhub(args, opts = {}) {
|
|
|
369
854
|
});
|
|
370
855
|
}
|
|
371
856
|
|
|
857
|
+
function readSkillhubAuthToken() {
|
|
858
|
+
try {
|
|
859
|
+
const p = path.join(os.homedir(), ".skillhub", "auth.json");
|
|
860
|
+
if (!fs.existsSync(p)) return "";
|
|
861
|
+
const data = JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
862
|
+
return String(data?.token || data?.accessToken || data?.access_token || "").trim();
|
|
863
|
+
} catch {
|
|
864
|
+
return "";
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function normalizeSkillhubCollectionInfo(raw, collectionId) {
|
|
869
|
+
const data = raw?.data && typeof raw.data === "object"
|
|
870
|
+
? raw.data
|
|
871
|
+
: raw?.collection && typeof raw.collection === "object"
|
|
872
|
+
? raw.collection
|
|
873
|
+
: raw?.item && typeof raw.item === "object"
|
|
874
|
+
? raw.item
|
|
875
|
+
: raw && typeof raw === "object"
|
|
876
|
+
? raw
|
|
877
|
+
: {};
|
|
878
|
+
const id = String(data.id ?? collectionId ?? "").trim();
|
|
879
|
+
const name = String(data.name ?? data.displayName ?? data.display_name ?? data.title ?? "").trim();
|
|
880
|
+
const summary = String(data.description ?? data.summary ?? data.subtitle ?? "").trim();
|
|
881
|
+
const version = String(data.version ?? data.latestVersion ?? data.latest_version ?? "").trim();
|
|
882
|
+
const tags = Array.isArray(data.tags) ? data.tags.map(String).filter(Boolean) : [];
|
|
883
|
+
if (!id && !name) return null;
|
|
884
|
+
return {
|
|
885
|
+
id: id || String(collectionId || ""),
|
|
886
|
+
collection: id || String(collectionId || ""),
|
|
887
|
+
kind: "collection",
|
|
888
|
+
slug: "",
|
|
889
|
+
name: name || `Collection ${collectionId}`,
|
|
890
|
+
summary: summary || "按 Collection ID 安装该合集中的全部 Skills。",
|
|
891
|
+
version,
|
|
892
|
+
tags,
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
async function fetchSkillhubCollectionInfo(collectionId) {
|
|
897
|
+
const id = String(collectionId || "").trim();
|
|
898
|
+
if (!id) return null;
|
|
899
|
+
const cached = skillhubCollectionInfoCache.get(id);
|
|
900
|
+
if (cached) return cached;
|
|
901
|
+
if (typeof fetch !== "function") return null;
|
|
902
|
+
const controller = new AbortController();
|
|
903
|
+
const timer = setTimeout(() => controller.abort(), 8000);
|
|
904
|
+
try {
|
|
905
|
+
const token = readSkillhubAuthToken();
|
|
906
|
+
const headers = token ? { Authorization: `Bearer ${token}` } : {};
|
|
907
|
+
const r = await fetch(`${SKILLHUB_API_BASE}/collections/${encodeURIComponent(id)}`, {
|
|
908
|
+
headers,
|
|
909
|
+
signal: controller.signal,
|
|
910
|
+
});
|
|
911
|
+
if (!r.ok) return null;
|
|
912
|
+
const raw = await r.json().catch(() => null);
|
|
913
|
+
const info = normalizeSkillhubCollectionInfo(raw, id);
|
|
914
|
+
if (info) skillhubCollectionInfoCache.set(id, info);
|
|
915
|
+
return info;
|
|
916
|
+
} catch {
|
|
917
|
+
return null;
|
|
918
|
+
} finally {
|
|
919
|
+
clearTimeout(timer);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
372
923
|
function parseJsonText(text, fallback = null) {
|
|
373
924
|
const s = String(text || "").trim();
|
|
374
925
|
if (!s) return fallback;
|
|
@@ -394,11 +945,13 @@ function normalizeSkillhubSearchPayload(raw) {
|
|
|
394
945
|
const slug = String(x.slug ?? x.name ?? x.displayName ?? x.display_name ?? id ?? "").trim();
|
|
395
946
|
return {
|
|
396
947
|
id: String(id || slug),
|
|
948
|
+
skillId: String(id || ""),
|
|
397
949
|
slug,
|
|
398
950
|
name: String(x.displayName ?? x.display_name ?? x.name ?? slug),
|
|
399
951
|
summary: String(x.summary ?? x.description ?? ""),
|
|
400
952
|
version: String(x.version ?? x.latestVersion ?? x.latest_version ?? ""),
|
|
401
953
|
tags: Array.isArray(x.tags) ? x.tags.map(String) : [],
|
|
954
|
+
kind: "skill",
|
|
402
955
|
};
|
|
403
956
|
}).filter((x) => x.slug || x.name),
|
|
404
957
|
};
|
|
@@ -602,6 +1155,21 @@ function resolveWorkspaceScopeRoot(workspaceRoot, params = {}, opts = {}) {
|
|
|
602
1155
|
return { root: path.resolve(result.path), flowId, flowSource, archived };
|
|
603
1156
|
}
|
|
604
1157
|
|
|
1158
|
+
function workspaceSearchGuardrailsBlock() {
|
|
1159
|
+
return [
|
|
1160
|
+
"## 检索约束",
|
|
1161
|
+
"",
|
|
1162
|
+
"默认不要读取、搜索或 Glob 历史运行产物;除非用户明确要求分析历史 run/log,否则必须排除:",
|
|
1163
|
+
"- `**/runBuild/**`",
|
|
1164
|
+
"- `**/logs/**`",
|
|
1165
|
+
"- `.workspace/agentflow/**/runBuild/**`",
|
|
1166
|
+
"- `~/agentflow/runBuild/**`",
|
|
1167
|
+
"- `node_modules/**`、`dist/**` 等依赖或构建产物",
|
|
1168
|
+
"",
|
|
1169
|
+
"使用 grep/rg/find/Glob 等工具时,应把上述路径作为 exclude/glob ignore;不要从历史 runBuild/logs 中推断业务事实、指标资产或 skill 文档。",
|
|
1170
|
+
].join("\n");
|
|
1171
|
+
}
|
|
1172
|
+
|
|
605
1173
|
function buildWorkspaceGeneratePrompt(payload) {
|
|
606
1174
|
const userPrompt = String(payload?.prompt || "").trim();
|
|
607
1175
|
const outputKind = String(payload?.outputKind || payload?.kind || "markdown").trim().toLowerCase();
|
|
@@ -652,6 +1220,7 @@ function buildWorkspaceGeneratePrompt(payload) {
|
|
|
652
1220
|
allowFlowYaml
|
|
653
1221
|
? "用户已允许你考虑正式 flow.yaml;如需修改仍必须明确说明影响。"
|
|
654
1222
|
: "默认不要修改正式 flow.yaml;优先在 workspace 文件、workspace.graph.json 或回复内容中完成任务。",
|
|
1223
|
+
workspaceSearchGuardrailsBlock(),
|
|
655
1224
|
workspaceGraph ? `\n## 当前 workspace graph\n\n${JSON.stringify(workspaceGraph, null, 2)}` : "",
|
|
656
1225
|
selectedNodeIds.length > 0 ? `\n## 当前用户选中的 workspace 节点\n\n${selectedNodeIds.map((id) => `- ${id}`).join("\n")}` : "",
|
|
657
1226
|
skillsBlock ? `\n## Selected Skills\n\n${skillsBlock}` : "",
|
|
@@ -661,6 +1230,47 @@ function buildWorkspaceGeneratePrompt(payload) {
|
|
|
661
1230
|
].filter(Boolean).join("\n");
|
|
662
1231
|
}
|
|
663
1232
|
|
|
1233
|
+
function buildWorkspaceNodeChatPrompt(payload) {
|
|
1234
|
+
const node = payload?.node && typeof payload.node === "object" ? payload.node : {};
|
|
1235
|
+
const userMessage = String(payload?.message || "").trim();
|
|
1236
|
+
const currentContent = String(payload?.currentContent || "").trim();
|
|
1237
|
+
const nodeKind = String(payload?.nodeKind || payload?.kind || "markdown").trim().toLowerCase();
|
|
1238
|
+
const history = Array.isArray(payload?.messages) ? payload.messages : [];
|
|
1239
|
+
const historyBlock = history
|
|
1240
|
+
.slice(-8)
|
|
1241
|
+
.map((msg) => {
|
|
1242
|
+
const role = String(msg?.role || "user").trim() === "assistant" ? "assistant" : "user";
|
|
1243
|
+
const text = String(msg?.text || "").trim();
|
|
1244
|
+
return text ? `${role}: ${text}` : "";
|
|
1245
|
+
})
|
|
1246
|
+
.filter(Boolean)
|
|
1247
|
+
.join("\n\n");
|
|
1248
|
+
const outputRule =
|
|
1249
|
+
nodeKind === "html"
|
|
1250
|
+
? "只输出完整或片段 HTML,不要解释,不要包裹 Markdown 代码围栏。"
|
|
1251
|
+
: nodeKind === "image"
|
|
1252
|
+
? "只输出新的图片 src,可以是 URL、data URL 或文件路径,不要解释。"
|
|
1253
|
+
: nodeKind === "mermaid"
|
|
1254
|
+
? "只输出 Mermaid 源码,不要解释,不要包裹 Markdown 代码围栏。"
|
|
1255
|
+
: nodeKind === "ascii"
|
|
1256
|
+
? "只输出 ASCII 正文,不要解释,不要包裹 Markdown 代码围栏。"
|
|
1257
|
+
: "只输出新的 Markdown 正文,不要解释,不要包裹 Markdown 代码围栏。";
|
|
1258
|
+
return [
|
|
1259
|
+
"你正在微调 AgentFlow Workspace 画布中的单个展示节点。",
|
|
1260
|
+
"根据用户 follow-up 和当前节点内容,生成一个可直接替换当前节点展示内容的候选版本。",
|
|
1261
|
+
outputRule,
|
|
1262
|
+
"",
|
|
1263
|
+
"## 当前节点",
|
|
1264
|
+
`- id: ${String(node.id || "").trim() || "(unknown)"}`,
|
|
1265
|
+
`- label: ${String(node.label || "").trim() || "(unnamed)"}`,
|
|
1266
|
+
`- definitionId: ${String(node.definitionId || "").trim() || "(unknown)"}`,
|
|
1267
|
+
`- kind: ${nodeKind}`,
|
|
1268
|
+
currentContent ? `\n## 当前展示内容\n\n${currentContent}` : "",
|
|
1269
|
+
historyBlock ? `\n## 本节点对话历史\n\n${historyBlock}` : "",
|
|
1270
|
+
`\n## 用户 follow-up\n\n${userMessage}`,
|
|
1271
|
+
].filter(Boolean).join("\n");
|
|
1272
|
+
}
|
|
1273
|
+
|
|
664
1274
|
function workspaceSlotValue(slot) {
|
|
665
1275
|
if (!slot || typeof slot !== "object") return "";
|
|
666
1276
|
for (const key of ["value", "default"]) {
|
|
@@ -717,9 +1327,82 @@ function workspaceDisplayKind(definitionId) {
|
|
|
717
1327
|
if (id === "display_ascii") return "ascii";
|
|
718
1328
|
if (id === "display_html") return "html";
|
|
719
1329
|
if (id === "display_image") return "image";
|
|
1330
|
+
if (id === "display_chart") return "chart";
|
|
720
1331
|
return "";
|
|
721
1332
|
}
|
|
722
1333
|
|
|
1334
|
+
function normalizeHtmlDisplayContent(content) {
|
|
1335
|
+
let text = String(content || "").trim();
|
|
1336
|
+
if (!text) return "";
|
|
1337
|
+
const fenced = text.match(/```(?:html|HTML)?\s*\n?([\s\S]*?)```/);
|
|
1338
|
+
if (fenced && fenced[1]) text = fenced[1].trim();
|
|
1339
|
+
else {
|
|
1340
|
+
const openFence = text.match(/```(?:html|HTML)?\s*\n?([\s\S]*)$/);
|
|
1341
|
+
if (openFence && openFence[1]) text = openFence[1].trim();
|
|
1342
|
+
}
|
|
1343
|
+
text = text.replace(/^html\s*\n/i, "").replace(/```\s*$/g, "").trim();
|
|
1344
|
+
const markerPatterns = [
|
|
1345
|
+
/<!doctype\b/i,
|
|
1346
|
+
/<html\b/i,
|
|
1347
|
+
/<head\b/i,
|
|
1348
|
+
/<body\b/i,
|
|
1349
|
+
/<style\b/i,
|
|
1350
|
+
/<script\b/i,
|
|
1351
|
+
/<main\b/i,
|
|
1352
|
+
/<section\b/i,
|
|
1353
|
+
/<article\b/i,
|
|
1354
|
+
/<div\b/i,
|
|
1355
|
+
/<svg\b/i,
|
|
1356
|
+
/<canvas\b/i,
|
|
1357
|
+
];
|
|
1358
|
+
const firstHtmlIndex = markerPatterns.reduce((best, pattern) => {
|
|
1359
|
+
const match = pattern.exec(text);
|
|
1360
|
+
if (!match) return best;
|
|
1361
|
+
return best < 0 ? match.index : Math.min(best, match.index);
|
|
1362
|
+
}, -1);
|
|
1363
|
+
if (firstHtmlIndex > 0) text = text.slice(firstHtmlIndex).trim();
|
|
1364
|
+
return text;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
function workspaceDownstreamDisplayRequirements(graph, nodeId) {
|
|
1368
|
+
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
1369
|
+
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
1370
|
+
const kinds = new Set();
|
|
1371
|
+
for (const edge of edges) {
|
|
1372
|
+
if (String(edge?.source || "") !== String(nodeId)) continue;
|
|
1373
|
+
const target = instances[String(edge?.target || "")];
|
|
1374
|
+
const kind = workspaceDisplayKind(target?.definitionId);
|
|
1375
|
+
if (kind) kinds.add(kind);
|
|
1376
|
+
}
|
|
1377
|
+
if (kinds.size === 0) return "";
|
|
1378
|
+
const rules = [];
|
|
1379
|
+
if (kinds.has("html")) {
|
|
1380
|
+
rules.push("- 下游连接了 HTML 展示节点:输出可直接放入 iframe 渲染的 HTML。可以是完整 HTML 文档或 HTML fragment;不要使用 Markdown 代码围栏;不要解释生成过程。");
|
|
1381
|
+
}
|
|
1382
|
+
if (kinds.has("markdown")) {
|
|
1383
|
+
rules.push("- 下游连接了 Markdown 展示节点:输出 Markdown 正文;不要包裹在代码围栏中,除非正文确实需要代码块。");
|
|
1384
|
+
}
|
|
1385
|
+
if (kinds.has("mermaid")) {
|
|
1386
|
+
rules.push("- 下游连接了 Mermaid 展示节点:只输出 Mermaid 图表代码,例如 flowchart/sequenceDiagram;不要使用 Markdown 代码围栏;不要附加解释。");
|
|
1387
|
+
}
|
|
1388
|
+
if (kinds.has("ascii")) {
|
|
1389
|
+
rules.push("- 下游连接了 ASCII 展示节点:输出纯文本/ASCII 图或表格;不要输出 HTML 或 Markdown 装饰。");
|
|
1390
|
+
}
|
|
1391
|
+
if (kinds.has("image")) {
|
|
1392
|
+
rules.push("- 下游连接了图片展示节点:输出可作为 img src 使用的图片地址、data URL 或 base64 data URL;不要输出 Markdown 图片语法或解释文字。");
|
|
1393
|
+
}
|
|
1394
|
+
if (kinds.has("chart")) {
|
|
1395
|
+
rules.push('- 下游连接了 Chart 展示节点:只输出 ChartSpec JSON 对象,不要 Markdown 代码围栏,不要解释文字。格式必须包含 `"type":"chart"`、`"version":"1.0"`、`"renderer":"echarts"`、`"option"`;`option.series[].type` 只使用 line/bar/pie/scatter/radar/heatmap/tree/treemap/sunburst/sankey/graph/gauge/funnel;不要输出 HTML、script、iframe 或 JS 函数。');
|
|
1396
|
+
}
|
|
1397
|
+
return [
|
|
1398
|
+
"## 下游输出要求",
|
|
1399
|
+
"",
|
|
1400
|
+
...rules,
|
|
1401
|
+
"",
|
|
1402
|
+
"如果用户任务与下游展示格式没有冲突,优先满足上述格式要求;如果用户明确指定了其他格式,以用户任务为准。",
|
|
1403
|
+
].join("\n");
|
|
1404
|
+
}
|
|
1405
|
+
|
|
723
1406
|
function workspaceRunPlan(graph, runNodeId) {
|
|
724
1407
|
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
725
1408
|
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
@@ -786,6 +1469,37 @@ function workspaceUpstreamText(graph, nodeId, outputs) {
|
|
|
786
1469
|
return workspaceInstanceText(instances[sourceId]);
|
|
787
1470
|
}
|
|
788
1471
|
|
|
1472
|
+
function workspaceHandleIndex(handle, prefix) {
|
|
1473
|
+
const match = String(handle || "").match(new RegExp(`^${prefix}-(\\d+)$`));
|
|
1474
|
+
return match ? Number(match[1]) : 0;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
function workspaceTargetSlotForEdge(graph, edge) {
|
|
1478
|
+
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
1479
|
+
const target = instances[String(edge?.target || "")];
|
|
1480
|
+
const input = Array.isArray(target?.input) ? target.input : [];
|
|
1481
|
+
return input[workspaceHandleIndex(edge?.targetHandle, "input")] || null;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
function isWorkspaceSemanticInputSlot(slot) {
|
|
1485
|
+
const name = String(slot?.name || "");
|
|
1486
|
+
const type = String(slot?.type || "");
|
|
1487
|
+
return type === "node" || name === "prev" || name === "next" || name === "skillsContext" || name === "mcpContext" || name === "workspaceContext" || name === "gitContext";
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
function workspaceTaskUpstreamText(graph, nodeId, outputs) {
|
|
1491
|
+
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
1492
|
+
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
1493
|
+
const incoming = edges.filter((edge) => String(edge?.target || "") === String(nodeId));
|
|
1494
|
+
const contentEdges = incoming.filter((edge) => !isWorkspaceSemanticInputSlot(workspaceTargetSlotForEdge(graph, edge)));
|
|
1495
|
+
const contentEdge = contentEdges.find((edge) => String(edge?.targetHandle || "") === "input-1") || contentEdges[0];
|
|
1496
|
+
if (!contentEdge) return "";
|
|
1497
|
+
const sourceId = String(contentEdge.source || "");
|
|
1498
|
+
const out = outputs.get(sourceId);
|
|
1499
|
+
if (out != null && String(out).trim()) return String(out);
|
|
1500
|
+
return workspaceInstanceText(instances[sourceId]);
|
|
1501
|
+
}
|
|
1502
|
+
|
|
789
1503
|
function parseWorkspaceSkillKeys(raw) {
|
|
790
1504
|
const text = String(raw || "").trim();
|
|
791
1505
|
if (!text) return [];
|
|
@@ -806,19 +1520,119 @@ function selectedSkillKeysFromInstance(instance) {
|
|
|
806
1520
|
return parseWorkspaceSkillKeys(workspaceSlotValue(slot) || "");
|
|
807
1521
|
}
|
|
808
1522
|
|
|
1523
|
+
function selectedMcpServerNamesFromInstance(instance) {
|
|
1524
|
+
const bodyNames = parseWorkspaceSkillKeys(instance?.body || "");
|
|
1525
|
+
if (bodyNames.length > 0) return bodyNames;
|
|
1526
|
+
const slots = [...(Array.isArray(instance?.input) ? instance.input : []), ...(Array.isArray(instance?.output) ? instance.output : [])];
|
|
1527
|
+
const slot = slots.find((item) => item?.name === "mcpContext") || slots.find((item) => item?.name === "serverNames");
|
|
1528
|
+
return parseWorkspaceSkillKeys(workspaceSlotValue(slot) || "");
|
|
1529
|
+
}
|
|
1530
|
+
|
|
809
1531
|
function workspaceUpstreamSkillBlocks(graph, nodeId, outputs) {
|
|
810
1532
|
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
811
|
-
|
|
1533
|
+
const blocks = edges
|
|
812
1534
|
.filter((edge) => String(edge?.target || "") === String(nodeId))
|
|
1535
|
+
.filter((edge) => {
|
|
1536
|
+
const slot = workspaceTargetSlotForEdge(graph, edge);
|
|
1537
|
+
return String(slot?.name || "") === "skillsContext";
|
|
1538
|
+
})
|
|
813
1539
|
.map((edge) => String(outputs.get(String(edge.source || "")) || ""))
|
|
814
|
-
.filter((text) => text.includes("
|
|
815
|
-
.
|
|
1540
|
+
.filter((text) => text.includes("Skill") || text.includes("skill"))
|
|
1541
|
+
.flatMap((text) => text.split(/\n\s*---\s*\n/g))
|
|
1542
|
+
.map((text) => text.trim())
|
|
1543
|
+
.filter(Boolean);
|
|
1544
|
+
return Array.from(new Set(blocks)).join("\n\n---\n\n");
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
function workspaceUpstreamMcpBlocks(graph, nodeId, outputs) {
|
|
1548
|
+
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
1549
|
+
const blocks = edges
|
|
1550
|
+
.filter((edge) => String(edge?.target || "") === String(nodeId))
|
|
1551
|
+
.filter((edge) => {
|
|
1552
|
+
const slot = workspaceTargetSlotForEdge(graph, edge);
|
|
1553
|
+
return String(slot?.name || "") === "mcpContext";
|
|
1554
|
+
})
|
|
1555
|
+
.map((edge) => String(outputs.get(String(edge.source || "")) || ""))
|
|
1556
|
+
.filter((text) => text.includes("MCP") || text.includes("mcp"))
|
|
1557
|
+
.flatMap((text) => text.split(/\n\s*---\s*\n/g))
|
|
1558
|
+
.map((text) => text.trim())
|
|
1559
|
+
.filter(Boolean);
|
|
1560
|
+
return Array.from(new Set(blocks)).join("\n\n---\n\n");
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
function mergeWorkspaceSkillBlocks(...values) {
|
|
1564
|
+
const blocks = values
|
|
1565
|
+
.map((value) => String(value || ""))
|
|
1566
|
+
.filter(Boolean)
|
|
1567
|
+
.flatMap((text) => text.split(/\n\s*---\s*\n/g))
|
|
1568
|
+
.map((text) => text.trim())
|
|
1569
|
+
.filter(Boolean);
|
|
1570
|
+
return Array.from(new Set(blocks)).join("\n\n---\n\n");
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
function buildWorkspaceSkillManifestBlock(skills, selectedKeys = []) {
|
|
1574
|
+
const normalizedKeys = Array.from(new Set((selectedKeys || []).map((x) => String(x || "").trim()).filter(Boolean)));
|
|
1575
|
+
const rows = (Array.isArray(skills) ? skills : []).map((skill) => {
|
|
1576
|
+
const id = String(skill?.id || "").trim();
|
|
1577
|
+
const absPath = String(skill?.absPath || "").trim();
|
|
1578
|
+
if (!id && !absPath) return "";
|
|
1579
|
+
return `- \`${id || path.basename(absPath)}\`${absPath ? `: ${absPath}` : ""}`;
|
|
1580
|
+
}).filter(Boolean);
|
|
1581
|
+
if (!rows.length && !normalizedKeys.length) return "";
|
|
1582
|
+
return [
|
|
1583
|
+
"### Workspace Skills Manifest",
|
|
1584
|
+
"",
|
|
1585
|
+
"这些 skills 已在当前 workspace 中可用。不要默认展开或复述其内容;仅当节点任务明确需要时,按路径 Read 对应 SKILL.md。",
|
|
1586
|
+
"",
|
|
1587
|
+
...(
|
|
1588
|
+
rows.length
|
|
1589
|
+
? rows
|
|
1590
|
+
: normalizedKeys.map((key) => `- \`${key}\``)
|
|
1591
|
+
),
|
|
1592
|
+
].join("\n");
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
function buildWorkspaceMcpManifestBlock(results, servers = [], selectedNames = []) {
|
|
1596
|
+
const serverByName = new Map((Array.isArray(servers) ? servers : []).map((server) => [String(server?.name || ""), server]));
|
|
1597
|
+
const normalizedNames = Array.from(new Set((selectedNames || []).map((x) => String(x || "").trim()).filter(Boolean)));
|
|
1598
|
+
const targets = (Array.isArray(results) ? results : []).filter((item) => !normalizedNames.length || normalizedNames.includes(String(item?.name || "")));
|
|
1599
|
+
const rows = [];
|
|
1600
|
+
for (const result of targets) {
|
|
1601
|
+
const name = String(result?.name || "").trim();
|
|
1602
|
+
if (!name) continue;
|
|
1603
|
+
const server = serverByName.get(name) || {};
|
|
1604
|
+
const description = String(server?.description || "").trim();
|
|
1605
|
+
if (!result?.ok) {
|
|
1606
|
+
rows.push(`- MCP server \`${name}\`: unavailable${result?.error ? ` (${String(result.error)})` : ""}`);
|
|
1607
|
+
continue;
|
|
1608
|
+
}
|
|
1609
|
+
rows.push(`- MCP server \`${name}\`${description ? `: ${description}` : ""}`);
|
|
1610
|
+
const tools = Array.isArray(result?.tools) ? result.tools : [];
|
|
1611
|
+
if (!tools.length) {
|
|
1612
|
+
rows.push(" - no tools reported");
|
|
1613
|
+
continue;
|
|
1614
|
+
}
|
|
1615
|
+
for (const tool of tools.slice(0, 80)) {
|
|
1616
|
+
const toolName = String(tool?.name || "").trim();
|
|
1617
|
+
if (!toolName) continue;
|
|
1618
|
+
const toolDescription = String(tool?.description || "").trim();
|
|
1619
|
+
rows.push(` - tool \`${toolName}\`${toolDescription ? `: ${toolDescription}` : ""}`);
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
if (!rows.length && !normalizedNames.length) return "";
|
|
1623
|
+
return [
|
|
1624
|
+
"### Workspace MCP Manifest",
|
|
1625
|
+
"",
|
|
1626
|
+
"这些 MCP servers/tools 已在当前 Agent 运行器中可用。需要外部工具能力时,优先使用下列 MCP 工具;不要声称调用了工具,除非实际工具调用成功。",
|
|
1627
|
+
"",
|
|
1628
|
+
...(rows.length ? rows : normalizedNames.map((name) => `- MCP server \`${name}\``)),
|
|
1629
|
+
].join("\n");
|
|
816
1630
|
}
|
|
817
1631
|
|
|
818
1632
|
function workspaceWriteDisplayContent(instance, content) {
|
|
819
1633
|
const next = { ...(instance || {}) };
|
|
820
|
-
const text = String(content || "");
|
|
821
1634
|
const kind = workspaceDisplayKind(next.definitionId);
|
|
1635
|
+
const text = kind === "html" ? normalizeHtmlDisplayContent(content) : String(content || "");
|
|
822
1636
|
const primaryName = kind === "image" ? "src" : "content";
|
|
823
1637
|
next.body = text;
|
|
824
1638
|
next.input = (Array.isArray(next.input) ? next.input : []).map((slot) => (
|
|
@@ -849,15 +1663,19 @@ function workspaceUpdateDirectDisplays(graph, sourceId, content) {
|
|
|
849
1663
|
return updated;
|
|
850
1664
|
}
|
|
851
1665
|
|
|
852
|
-
function workspaceNodePrompt(graph, nodeId, upstreamText, skillsBlock) {
|
|
1666
|
+
function workspaceNodePrompt(graph, nodeId, upstreamText, skillsBlock, mcpBlock = "") {
|
|
853
1667
|
const instance = graph.instances[nodeId] || {};
|
|
854
1668
|
const body = String(instance.body || "").trim();
|
|
855
1669
|
const label = String(instance.label || nodeId).trim();
|
|
1670
|
+
const downstreamRequirements = workspaceDownstreamDisplayRequirements(graph, nodeId);
|
|
856
1671
|
return [
|
|
857
1672
|
"你正在执行 AgentFlow Workspace 画布中的一个临时节点。",
|
|
858
1673
|
"只输出该节点要传给下游展示/后续节点的正文,不要解释运行过程。",
|
|
859
|
-
|
|
1674
|
+
workspaceSearchGuardrailsBlock(),
|
|
1675
|
+
skillsBlock ? `\n## Available Skills\n\n${skillsBlock}` : "",
|
|
1676
|
+
mcpBlock ? `\n## Available MCP\n\n${mcpBlock}` : "",
|
|
860
1677
|
upstreamText ? `\n## 上游上下文\n\n${upstreamText}` : "",
|
|
1678
|
+
downstreamRequirements ? `\n${downstreamRequirements}` : "",
|
|
861
1679
|
`\n## 当前节点\n\n- id: ${nodeId}\n- label: ${label}\n- definitionId: ${instance.definitionId || ""}`,
|
|
862
1680
|
`\n## 节点任务\n\n${body || upstreamText}`,
|
|
863
1681
|
].filter(Boolean).join("\n");
|
|
@@ -867,6 +1685,14 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
867
1685
|
const graph = normalizeWorkspaceGraphPayload(payload.graph || {});
|
|
868
1686
|
const runNodeId = String(payload?.runNodeId || "").trim();
|
|
869
1687
|
const { order, pauseNodeIds } = workspaceRunPlan(graph, runNodeId);
|
|
1688
|
+
const signal = opts.signal || null;
|
|
1689
|
+
const throwIfAborted = () => {
|
|
1690
|
+
if (signal?.aborted) {
|
|
1691
|
+
const err = new Error("Workspace run stopped");
|
|
1692
|
+
err.code = "WORKSPACE_RUN_ABORTED";
|
|
1693
|
+
throw err;
|
|
1694
|
+
}
|
|
1695
|
+
};
|
|
870
1696
|
const fallbackSelectedSkillKeys = Array.isArray(payload?.selectedSkills)
|
|
871
1697
|
? payload.selectedSkills.map((x) => String(x || "").trim()).filter(Boolean)
|
|
872
1698
|
: [];
|
|
@@ -879,21 +1705,42 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
879
1705
|
? loadResourcesForSkillKeys(normalized, PACKAGE_ROOT, scopedRoot)
|
|
880
1706
|
: { skills: [], references: [] };
|
|
881
1707
|
const block = normalized.length > 0
|
|
882
|
-
?
|
|
1708
|
+
? buildWorkspaceSkillManifestBlock(selectedSkillResources.skills, normalized)
|
|
883
1709
|
: "";
|
|
884
1710
|
skillsBlockCache.set(cacheKey, block);
|
|
885
1711
|
return block;
|
|
886
1712
|
};
|
|
1713
|
+
const mcpBlockCache = new Map();
|
|
1714
|
+
const loadMcpBlockForNames = async (names) => {
|
|
1715
|
+
const normalized = Array.from(new Set((names || []).map((x) => String(x || "").trim()).filter(Boolean)));
|
|
1716
|
+
if (!normalized.length) return "";
|
|
1717
|
+
const cacheKey = normalized.join("\n");
|
|
1718
|
+
if (mcpBlockCache.has(cacheKey)) return mcpBlockCache.get(cacheKey);
|
|
1719
|
+
const { servers } = readCursorMcpServers(userCtx);
|
|
1720
|
+
const results = [];
|
|
1721
|
+
for (const name of normalized) {
|
|
1722
|
+
const checked = await checkCursorMcpServers(name, userCtx);
|
|
1723
|
+
results.push(...(Array.isArray(checked.results) ? checked.results : []));
|
|
1724
|
+
}
|
|
1725
|
+
const block = buildWorkspaceMcpManifestBlock(results, servers, normalized);
|
|
1726
|
+
mcpBlockCache.set(cacheKey, block);
|
|
1727
|
+
return block;
|
|
1728
|
+
};
|
|
887
1729
|
const outputs = new Map();
|
|
888
1730
|
const events = [];
|
|
889
1731
|
const emit = (event) => {
|
|
890
1732
|
events.push(event);
|
|
891
1733
|
if (typeof opts.onEvent === "function") opts.onEvent(event);
|
|
892
1734
|
};
|
|
1735
|
+
const emitTiming = (nodeId, label, startedAt, extra = {}) => {
|
|
1736
|
+
const elapsedMs = Math.max(0, Date.now() - startedAt);
|
|
1737
|
+
emit({ type: "status", nodeId, line: `Timing ${label}: ${elapsedMs}ms`, timing: { label, elapsedMs, ...extra } });
|
|
1738
|
+
};
|
|
893
1739
|
let cwd = scopedRoot;
|
|
894
1740
|
const modelKey = typeof payload?.model === "string" ? payload.model.trim() : "";
|
|
895
1741
|
|
|
896
1742
|
for (const nodeId of order) {
|
|
1743
|
+
throwIfAborted();
|
|
897
1744
|
const instance = graph.instances[nodeId];
|
|
898
1745
|
if (!instance) continue;
|
|
899
1746
|
const defId = String(instance.definitionId || "");
|
|
@@ -904,9 +1751,11 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
904
1751
|
}
|
|
905
1752
|
|
|
906
1753
|
if (defId === "control_load_skills") {
|
|
1754
|
+
const skillStartedAt = Date.now();
|
|
907
1755
|
const nodeSkillKeys = selectedSkillKeysFromInstance(instance);
|
|
908
1756
|
const activeSkillKeys = nodeSkillKeys.length > 0 ? nodeSkillKeys : fallbackSelectedSkillKeys;
|
|
909
1757
|
const skillsBlock = loadSkillsBlockForKeys(activeSkillKeys);
|
|
1758
|
+
emitTiming(nodeId, "load-skills", skillStartedAt, { skillCount: activeSkillKeys.length, charCount: skillsBlock.length });
|
|
910
1759
|
graph.instances[nodeId] = {
|
|
911
1760
|
...instance,
|
|
912
1761
|
output: (Array.isArray(instance.output) ? instance.output : []).map((slot) => (
|
|
@@ -922,6 +1771,26 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
922
1771
|
continue;
|
|
923
1772
|
}
|
|
924
1773
|
|
|
1774
|
+
if (defId === "control_load_mcp") {
|
|
1775
|
+
const mcpStartedAt = Date.now();
|
|
1776
|
+
const serverNames = selectedMcpServerNamesFromInstance(instance);
|
|
1777
|
+
const mcpBlock = await loadMcpBlockForNames(serverNames);
|
|
1778
|
+
emitTiming(nodeId, "load-mcp", mcpStartedAt, { serverCount: serverNames.length, charCount: mcpBlock.length });
|
|
1779
|
+
graph.instances[nodeId] = {
|
|
1780
|
+
...instance,
|
|
1781
|
+
output: (Array.isArray(instance.output) ? instance.output : []).map((slot) => (
|
|
1782
|
+
String(slot?.name || "") === "mcpContext" || String(slot?.type || "") === "text"
|
|
1783
|
+
? { ...slot, default: mcpBlock, value: mcpBlock }
|
|
1784
|
+
: slot
|
|
1785
|
+
)),
|
|
1786
|
+
};
|
|
1787
|
+
outputs.set(nodeId, mcpBlock);
|
|
1788
|
+
workspaceUpdateDirectDisplays(graph, nodeId, mcpBlock);
|
|
1789
|
+
emit({ type: "graph", nodeId, graph });
|
|
1790
|
+
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
1791
|
+
continue;
|
|
1792
|
+
}
|
|
1793
|
+
|
|
925
1794
|
if (workspaceDisplayKind(defId)) {
|
|
926
1795
|
const content = workspaceUpstreamText(graph, nodeId, outputs);
|
|
927
1796
|
graph.instances[nodeId] = workspaceWriteDisplayContent(instance, content);
|
|
@@ -1057,7 +1926,10 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
1057
1926
|
const rawWorktreePath = workspaceSlotValue(workspaceSlotByName(instance, "worktreePath")).trim();
|
|
1058
1927
|
const worktreePath = rawWorktreePath ? workspaceResolvePath(cwd, rawWorktreePath) : (gitContext?.worktreePath ? path.resolve(gitContext.worktreePath) : "");
|
|
1059
1928
|
const previousCwd = cwd;
|
|
1060
|
-
const
|
|
1929
|
+
const force = ["true", "1", "yes", "on"].includes(workspaceSlotValue(workspaceSlotByName(instance, "force")).trim().toLowerCase());
|
|
1930
|
+
const pruneMissingRaw = workspaceSlotValue(workspaceSlotByName(instance, "pruneMissing")).trim().toLowerCase();
|
|
1931
|
+
const pruneMissing = pruneMissingRaw !== "false";
|
|
1932
|
+
const result = loadGitWorktree({ repoPath, branch, worktreePath, pipelineWorkspace: scopedRoot, force, pruneMissing });
|
|
1061
1933
|
const outGitContext = buildGitContext({
|
|
1062
1934
|
repoPath: result.repoRoot,
|
|
1063
1935
|
worktreePath: result.worktreePath,
|
|
@@ -1154,18 +2026,25 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
1154
2026
|
continue;
|
|
1155
2027
|
}
|
|
1156
2028
|
|
|
1157
|
-
const
|
|
2029
|
+
const prepareStartedAt = Date.now();
|
|
2030
|
+
const upstreamText = workspaceTaskUpstreamText(graph, nodeId, outputs);
|
|
1158
2031
|
const body = String(instance.body || "").trim();
|
|
1159
2032
|
if (defId === "agent_subAgent" && !body && !String(upstreamText || "").trim()) {
|
|
1160
2033
|
throw new Error(`Workspace node ${nodeId} has no task. Fill the node body or connect upstream text.`);
|
|
1161
2034
|
}
|
|
1162
2035
|
const upstreamSkillBlocks = workspaceUpstreamSkillBlocks(graph, nodeId, outputs);
|
|
1163
|
-
const
|
|
2036
|
+
const promptSkillsBlock = mergeWorkspaceSkillBlocks(upstreamSkillBlocks, upstreamSkillBlocks ? "" : loadSkillsBlockForKeys(fallbackSelectedSkillKeys));
|
|
2037
|
+
const promptMcpBlock = workspaceUpstreamMcpBlocks(graph, nodeId, outputs);
|
|
2038
|
+
const prompt = workspaceNodePrompt(graph, nodeId, upstreamText, promptSkillsBlock, promptMcpBlock);
|
|
2039
|
+
emitTiming(nodeId, "prepare-agent-prompt", prepareStartedAt, { promptChars: prompt.length, upstreamChars: String(upstreamText || "").length, skillsChars: promptSkillsBlock.length, mcpChars: promptMcpBlock.length });
|
|
2040
|
+
emit({ type: "natural", kind: "prompt", nodeId, text: prompt });
|
|
1164
2041
|
let content = "";
|
|
1165
2042
|
const maxAttempts = 3;
|
|
1166
2043
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
1167
2044
|
let attemptContent = "";
|
|
1168
2045
|
try {
|
|
2046
|
+
const spawnStartedAt = Date.now();
|
|
2047
|
+
let firstAgentEventSeen = false;
|
|
1169
2048
|
const handle = startComposerAgent({
|
|
1170
2049
|
uiWorkspaceRoot: scopedRoot,
|
|
1171
2050
|
cliWorkspace: cwd,
|
|
@@ -1173,21 +2052,36 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
1173
2052
|
modelKey,
|
|
1174
2053
|
agentflowUserId: userCtx.userId || "",
|
|
1175
2054
|
onStreamEvent: (ev) => {
|
|
2055
|
+
if (!firstAgentEventSeen) {
|
|
2056
|
+
firstAgentEventSeen = true;
|
|
2057
|
+
emitTiming(nodeId, "agent-first-event", spawnStartedAt, { attempt, firstType: ev?.type || "" });
|
|
2058
|
+
}
|
|
1176
2059
|
emit({ ...ev, nodeId });
|
|
1177
2060
|
if (ev?.type === "natural" && ev.kind === "assistant" && typeof ev.text === "string") {
|
|
1178
2061
|
attemptContent += (attemptContent ? "\n" : "") + ev.text;
|
|
1179
|
-
const updatedDisplays = workspaceUpdateDirectDisplays(graph, nodeId, attemptContent);
|
|
1180
|
-
if (updatedDisplays.length) emit({ type: "graph", nodeId, displayNodeIds: updatedDisplays, graph });
|
|
1181
2062
|
}
|
|
1182
2063
|
},
|
|
2064
|
+
onToolCall: (subtype, toolName) => {
|
|
2065
|
+
const sub = subtype ? String(subtype) : "";
|
|
2066
|
+
const tool = toolName ? String(toolName) : "";
|
|
2067
|
+
emit({ type: "status", nodeId, line: `工具 ${tool || "thinking"}${sub ? ` (${sub})` : ""}` });
|
|
2068
|
+
},
|
|
1183
2069
|
});
|
|
1184
|
-
|
|
2070
|
+
if (typeof opts.onActiveChild === "function") opts.onActiveChild(handle.child || null);
|
|
2071
|
+
emitTiming(nodeId, "spawn-agent", spawnStartedAt, { attempt });
|
|
2072
|
+
try {
|
|
2073
|
+
await handle.finished;
|
|
2074
|
+
} finally {
|
|
2075
|
+
if (typeof opts.onActiveChild === "function") opts.onActiveChild(null);
|
|
2076
|
+
}
|
|
2077
|
+
throwIfAborted();
|
|
1185
2078
|
content = attemptContent.trim();
|
|
1186
2079
|
break;
|
|
1187
2080
|
} catch (e) {
|
|
2081
|
+
if (signal?.aborted || e?.code === "WORKSPACE_RUN_ABORTED") throwIfAborted();
|
|
1188
2082
|
if (attempt < maxAttempts && isTransientAgentNetworkError(e)) {
|
|
1189
2083
|
emit({ type: "status", nodeId, line: `Workspace node retry ${attempt + 1}/${maxAttempts} after network error` });
|
|
1190
|
-
await sleepMs(Math.min(1500 * attempt, 5000));
|
|
2084
|
+
await sleepMs(Math.min(1500 * attempt, 5000), signal);
|
|
1191
2085
|
continue;
|
|
1192
2086
|
}
|
|
1193
2087
|
throw e;
|
|
@@ -1205,6 +2099,10 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
1205
2099
|
return { graph, events, order, pauseNodeIds };
|
|
1206
2100
|
}
|
|
1207
2101
|
|
|
2102
|
+
function isWorkspaceRunAbortError(err) {
|
|
2103
|
+
return err?.code === "WORKSPACE_RUN_ABORTED" || /Workspace run stopped/i.test(String(err?.message || ""));
|
|
2104
|
+
}
|
|
2105
|
+
|
|
1208
2106
|
function isTransientAgentNetworkError(err) {
|
|
1209
2107
|
const text = [
|
|
1210
2108
|
err?.message,
|
|
@@ -1221,8 +2119,17 @@ function isTransientAgentNetworkError(err) {
|
|
|
1221
2119
|
/socket hang up/i.test(text);
|
|
1222
2120
|
}
|
|
1223
2121
|
|
|
1224
|
-
function sleepMs(ms) {
|
|
1225
|
-
|
|
2122
|
+
function sleepMs(ms, signal = null) {
|
|
2123
|
+
if (signal?.aborted) return Promise.resolve();
|
|
2124
|
+
return new Promise((resolve) => {
|
|
2125
|
+
const timer = setTimeout(resolve, ms);
|
|
2126
|
+
if (signal) {
|
|
2127
|
+
signal.addEventListener("abort", () => {
|
|
2128
|
+
clearTimeout(timer);
|
|
2129
|
+
resolve();
|
|
2130
|
+
}, { once: true });
|
|
2131
|
+
}
|
|
2132
|
+
});
|
|
1226
2133
|
}
|
|
1227
2134
|
|
|
1228
2135
|
/** ZIP 本地头:PK\x03\x04 / \x05\x06 / \x07\x08 */
|
|
@@ -1327,6 +2234,12 @@ function broadcastFlowEditorSync(flowId, flowSource, flowArchived = false, userI
|
|
|
1327
2234
|
|
|
1328
2235
|
/** 正在执行的 flow run(flowId → { child, runUuid });同一 flow 只允许一个 run */
|
|
1329
2236
|
const activeFlowRuns = new Map();
|
|
2237
|
+
/** 正在执行的 Workspace 临时 run(flowId → { controller, child });同一 flow 只允许一个 run */
|
|
2238
|
+
const activeWorkspaceRuns = new Map();
|
|
2239
|
+
|
|
2240
|
+
function workspaceRunKey(userCtx, flowSource, flowId) {
|
|
2241
|
+
return `${userCtx?.userId || ""}:${flowSource || "user"}:${flowId}`;
|
|
2242
|
+
}
|
|
1330
2243
|
|
|
1331
2244
|
/** Cursor/OpenCode 执行目录统一使用当前 UI 启动 workspace。 */
|
|
1332
2245
|
function composerCliWorkspaceForFlowDir(workspaceRoot, _flowDir) {
|
|
@@ -1461,7 +2374,16 @@ export function startUiServer({
|
|
|
1461
2374
|
|
|
1462
2375
|
if (url.pathname === "/api/auth/me" && req.method === "GET") {
|
|
1463
2376
|
const user = getAuthUserFromRequest(req);
|
|
1464
|
-
|
|
2377
|
+
const allowed = user ? isAuthUserAllowed(user) : true;
|
|
2378
|
+
const allowlist = readUserAllowlist();
|
|
2379
|
+
json(res, 200, {
|
|
2380
|
+
authenticated: Boolean(user && allowed),
|
|
2381
|
+
user: user && allowed ? user : null,
|
|
2382
|
+
setupRequired: authSetupRequired(),
|
|
2383
|
+
allowlistEnabled: allowlist.enabled,
|
|
2384
|
+
forbidden: Boolean(user && !allowed),
|
|
2385
|
+
error: user && !allowed ? "用户不在白名单中,请联系管理员开通访问权限" : "",
|
|
2386
|
+
});
|
|
1465
2387
|
return;
|
|
1466
2388
|
}
|
|
1467
2389
|
|
|
@@ -1475,7 +2397,7 @@ export function startUiServer({
|
|
|
1475
2397
|
}
|
|
1476
2398
|
const result = loginOrCreateUser(payload?.username, payload?.password);
|
|
1477
2399
|
if (!result.ok) {
|
|
1478
|
-
json(res, 401, { error: result.error || "Login failed", setupRequired: authSetupRequired() });
|
|
2400
|
+
json(res, result.forbidden ? 403 : 401, { error: result.error || "Login failed", setupRequired: authSetupRequired() });
|
|
1479
2401
|
return;
|
|
1480
2402
|
}
|
|
1481
2403
|
const body = JSON.stringify({ authenticated: true, user: result.user, setupRequired: false, migration: result.migration || null });
|
|
@@ -1506,6 +2428,10 @@ export function startUiServer({
|
|
|
1506
2428
|
json(res, 401, { error: "Authentication required", setupRequired: authSetupRequired() });
|
|
1507
2429
|
return;
|
|
1508
2430
|
}
|
|
2431
|
+
if (url.pathname.startsWith("/api/") && authUser && !isAuthUserAllowed(authUser)) {
|
|
2432
|
+
json(res, 403, { error: "用户不在白名单中,请联系管理员开通访问权限" });
|
|
2433
|
+
return;
|
|
2434
|
+
}
|
|
1509
2435
|
|
|
1510
2436
|
if (url.pathname === "/api/flows") {
|
|
1511
2437
|
if (req.method === "GET") {
|
|
@@ -1841,6 +2767,34 @@ export function startUiServer({
|
|
|
1841
2767
|
return;
|
|
1842
2768
|
}
|
|
1843
2769
|
const wantsStream = /\bapplication\/x-ndjson\b/i.test(req.headers.accept || "") || payload.stream === true;
|
|
2770
|
+
const flowId = String(payload.flowId || "").trim();
|
|
2771
|
+
if (!flowId) {
|
|
2772
|
+
json(res, 400, { error: "Missing flowId" });
|
|
2773
|
+
return;
|
|
2774
|
+
}
|
|
2775
|
+
const runKey = workspaceRunKey(userCtx, scoped.flowSource || payload.flowSource || "user", flowId);
|
|
2776
|
+
if (activeWorkspaceRuns.has(runKey)) {
|
|
2777
|
+
json(res, 409, { error: "该 Workspace 正在运行" });
|
|
2778
|
+
return;
|
|
2779
|
+
}
|
|
2780
|
+
const controller = new AbortController();
|
|
2781
|
+
const runEntry = {
|
|
2782
|
+
controller,
|
|
2783
|
+
child: null,
|
|
2784
|
+
stopChild() {
|
|
2785
|
+
if (this.child && !this.child.killed) {
|
|
2786
|
+
try { this.child.kill("SIGTERM"); } catch (_) {}
|
|
2787
|
+
}
|
|
2788
|
+
},
|
|
2789
|
+
};
|
|
2790
|
+
activeWorkspaceRuns.set(runKey, runEntry);
|
|
2791
|
+
const setActiveChild = (child) => {
|
|
2792
|
+
runEntry.child = child || null;
|
|
2793
|
+
if (controller.signal.aborted) runEntry.stopChild();
|
|
2794
|
+
};
|
|
2795
|
+
const clearActiveRun = () => {
|
|
2796
|
+
if (activeWorkspaceRuns.get(runKey) === runEntry) activeWorkspaceRuns.delete(runKey);
|
|
2797
|
+
};
|
|
1844
2798
|
if (wantsStream) {
|
|
1845
2799
|
const graphPath = workspaceGraphPath(scoped.root);
|
|
1846
2800
|
res.writeHead(200, {
|
|
@@ -1849,29 +2803,77 @@ export function startUiServer({
|
|
|
1849
2803
|
"X-Accel-Buffering": "no",
|
|
1850
2804
|
});
|
|
1851
2805
|
const writeEvent = (event) => {
|
|
1852
|
-
res.write(JSON.stringify(event) + "\n");
|
|
2806
|
+
try { res.write(JSON.stringify(event) + "\n"); } catch (_) {}
|
|
1853
2807
|
};
|
|
1854
2808
|
try {
|
|
1855
|
-
const result = await runWorkspaceGraph(root, scoped.root, payload, userCtx, {
|
|
2809
|
+
const result = await runWorkspaceGraph(root, scoped.root, payload, userCtx, {
|
|
2810
|
+
onEvent: writeEvent,
|
|
2811
|
+
signal: controller.signal,
|
|
2812
|
+
onActiveChild: setActiveChild,
|
|
2813
|
+
});
|
|
1856
2814
|
fs.writeFileSync(graphPath, JSON.stringify(result.graph, null, 2) + "\n", "utf-8");
|
|
1857
2815
|
writeEvent({ type: "done", ok: true, path: graphPath, graph: result.graph, order: result.order, pauseNodeIds: result.pauseNodeIds || [] });
|
|
1858
2816
|
res.end();
|
|
1859
2817
|
} catch (e) {
|
|
1860
|
-
|
|
2818
|
+
if (isWorkspaceRunAbortError(e) || controller.signal.aborted) {
|
|
2819
|
+
writeEvent({ type: "stopped", ok: false, stopped: true, message: "Workspace run stopped" });
|
|
2820
|
+
} else {
|
|
2821
|
+
writeEvent({ type: "error", error: (e && e.message) || String(e) });
|
|
2822
|
+
}
|
|
1861
2823
|
res.end();
|
|
2824
|
+
} finally {
|
|
2825
|
+
clearActiveRun();
|
|
1862
2826
|
}
|
|
1863
2827
|
return;
|
|
1864
2828
|
}
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
2829
|
+
try {
|
|
2830
|
+
const result = await runWorkspaceGraph(root, scoped.root, payload, userCtx, {
|
|
2831
|
+
signal: controller.signal,
|
|
2832
|
+
onActiveChild: setActiveChild,
|
|
2833
|
+
});
|
|
2834
|
+
const graphPath = workspaceGraphPath(scoped.root);
|
|
2835
|
+
fs.writeFileSync(graphPath, JSON.stringify(result.graph, null, 2) + "\n", "utf-8");
|
|
2836
|
+
json(res, 200, { ok: true, path: graphPath, ...result });
|
|
2837
|
+
} catch (e) {
|
|
2838
|
+
if (isWorkspaceRunAbortError(e) || controller.signal.aborted) {
|
|
2839
|
+
json(res, 200, { ok: false, stopped: true, message: "Workspace run stopped" });
|
|
2840
|
+
} else {
|
|
2841
|
+
throw e;
|
|
2842
|
+
}
|
|
2843
|
+
} finally {
|
|
2844
|
+
clearActiveRun();
|
|
2845
|
+
}
|
|
1869
2846
|
} catch (e) {
|
|
1870
2847
|
json(res, 500, { error: (e && e.message) || String(e) });
|
|
1871
2848
|
}
|
|
1872
2849
|
return;
|
|
1873
2850
|
}
|
|
1874
2851
|
|
|
2852
|
+
if (req.method === "POST" && url.pathname === "/api/workspace/run/stop") {
|
|
2853
|
+
let payload;
|
|
2854
|
+
try {
|
|
2855
|
+
payload = JSON.parse(await readBody(req));
|
|
2856
|
+
} catch {
|
|
2857
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
2858
|
+
return;
|
|
2859
|
+
}
|
|
2860
|
+
const flowId = typeof payload.flowId === "string" ? payload.flowId.trim() : "";
|
|
2861
|
+
if (!flowId) {
|
|
2862
|
+
json(res, 400, { error: "Missing flowId" });
|
|
2863
|
+
return;
|
|
2864
|
+
}
|
|
2865
|
+
const runKey = workspaceRunKey(userCtx, payload.flowSource || "user", flowId);
|
|
2866
|
+
const entry = activeWorkspaceRuns.get(runKey);
|
|
2867
|
+
if (!entry) {
|
|
2868
|
+
json(res, 404, { error: "该 Workspace 未在运行" });
|
|
2869
|
+
return;
|
|
2870
|
+
}
|
|
2871
|
+
try { entry.controller?.abort(); } catch (_) {}
|
|
2872
|
+
try { entry.stopChild?.(); } catch (_) {}
|
|
2873
|
+
json(res, 200, { ok: true, stopped: true });
|
|
2874
|
+
return;
|
|
2875
|
+
}
|
|
2876
|
+
|
|
1875
2877
|
if (req.method === "GET" && url.pathname === "/api/workspace/file") {
|
|
1876
2878
|
try {
|
|
1877
2879
|
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
@@ -2091,6 +3093,61 @@ export function startUiServer({
|
|
|
2091
3093
|
return;
|
|
2092
3094
|
}
|
|
2093
3095
|
|
|
3096
|
+
if (req.method === "POST" && url.pathname === "/api/workspace/node-chat") {
|
|
3097
|
+
let payload;
|
|
3098
|
+
try {
|
|
3099
|
+
payload = JSON.parse(await readBody(req));
|
|
3100
|
+
} catch {
|
|
3101
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
3102
|
+
return;
|
|
3103
|
+
}
|
|
3104
|
+
const message = String(payload?.message || "").trim();
|
|
3105
|
+
if (!message) {
|
|
3106
|
+
json(res, 400, { error: "Missing message" });
|
|
3107
|
+
return;
|
|
3108
|
+
}
|
|
3109
|
+
try {
|
|
3110
|
+
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
3111
|
+
flowId: payload.flowId || "",
|
|
3112
|
+
flowSource: payload.flowSource || "user",
|
|
3113
|
+
archived: payload.archived === true || payload.flowArchived === true,
|
|
3114
|
+
}, userCtx);
|
|
3115
|
+
if (scoped.error) {
|
|
3116
|
+
json(res, 400, { error: scoped.error });
|
|
3117
|
+
return;
|
|
3118
|
+
}
|
|
3119
|
+
const promptText = buildWorkspaceNodeChatPrompt(payload);
|
|
3120
|
+
const modelKey = typeof payload?.model === "string" ? payload.model.trim() : "";
|
|
3121
|
+
let content = "";
|
|
3122
|
+
const events = [];
|
|
3123
|
+
const handle = startComposerAgent({
|
|
3124
|
+
uiWorkspaceRoot: scoped.root,
|
|
3125
|
+
cliWorkspace: scoped.root,
|
|
3126
|
+
prompt: promptText,
|
|
3127
|
+
modelKey,
|
|
3128
|
+
agentflowUserId: userCtx.userId || "",
|
|
3129
|
+
onStreamEvent: (ev) => {
|
|
3130
|
+
events.push(ev);
|
|
3131
|
+
if (ev?.type === "natural" && ev.kind === "assistant" && typeof ev.text === "string") {
|
|
3132
|
+
content += (content ? "\n" : "") + ev.text;
|
|
3133
|
+
}
|
|
3134
|
+
},
|
|
3135
|
+
});
|
|
3136
|
+
await handle.finished;
|
|
3137
|
+
const candidateContent = content.trim();
|
|
3138
|
+
json(res, 200, {
|
|
3139
|
+
ok: true,
|
|
3140
|
+
sessionId: String(payload?.sessionId || "") || `nodechat_${Date.now()}`,
|
|
3141
|
+
reply: candidateContent,
|
|
3142
|
+
candidateContent,
|
|
3143
|
+
events,
|
|
3144
|
+
});
|
|
3145
|
+
} catch (e) {
|
|
3146
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
3147
|
+
}
|
|
3148
|
+
return;
|
|
3149
|
+
}
|
|
3150
|
+
|
|
2094
3151
|
if (req.method === "GET" && url.pathname === "/api/pipeline-files") {
|
|
2095
3152
|
const flowId = url.searchParams.get("flowId");
|
|
2096
3153
|
const flowSource = url.searchParams.get("flowSource") || "user";
|
|
@@ -2307,6 +3364,64 @@ export function startUiServer({
|
|
|
2307
3364
|
return;
|
|
2308
3365
|
}
|
|
2309
3366
|
|
|
3367
|
+
if (req.method === "GET" && url.pathname === "/api/mcps") {
|
|
3368
|
+
try {
|
|
3369
|
+
json(res, 200, readCursorMcpServers(userCtx));
|
|
3370
|
+
} catch (e) {
|
|
3371
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
3372
|
+
}
|
|
3373
|
+
return;
|
|
3374
|
+
}
|
|
3375
|
+
|
|
3376
|
+
if (req.method === "POST" && url.pathname === "/api/mcps") {
|
|
3377
|
+
let payload;
|
|
3378
|
+
try {
|
|
3379
|
+
payload = JSON.parse(await readBody(req));
|
|
3380
|
+
} catch {
|
|
3381
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
3382
|
+
return;
|
|
3383
|
+
}
|
|
3384
|
+
try {
|
|
3385
|
+
json(res, 200, writeCursorMcpServer(payload, userCtx));
|
|
3386
|
+
} catch (e) {
|
|
3387
|
+
json(res, 400, { error: (e && e.message) || String(e) });
|
|
3388
|
+
}
|
|
3389
|
+
return;
|
|
3390
|
+
}
|
|
3391
|
+
|
|
3392
|
+
if (req.method === "POST" && url.pathname === "/api/mcps/delete") {
|
|
3393
|
+
let payload;
|
|
3394
|
+
try {
|
|
3395
|
+
payload = JSON.parse(await readBody(req));
|
|
3396
|
+
} catch {
|
|
3397
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
3398
|
+
return;
|
|
3399
|
+
}
|
|
3400
|
+
try {
|
|
3401
|
+
json(res, 200, deleteCursorMcpServer(payload?.name, userCtx));
|
|
3402
|
+
} catch (e) {
|
|
3403
|
+
json(res, 400, { error: (e && e.message) || String(e) });
|
|
3404
|
+
}
|
|
3405
|
+
return;
|
|
3406
|
+
}
|
|
3407
|
+
|
|
3408
|
+
if (req.method === "POST" && url.pathname === "/api/mcps/check") {
|
|
3409
|
+
let payload;
|
|
3410
|
+
try {
|
|
3411
|
+
const raw = await readBody(req);
|
|
3412
|
+
payload = raw && String(raw).trim() ? JSON.parse(raw) : {};
|
|
3413
|
+
} catch {
|
|
3414
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
3415
|
+
return;
|
|
3416
|
+
}
|
|
3417
|
+
try {
|
|
3418
|
+
json(res, 200, await checkCursorMcpServers(payload?.name || "", userCtx));
|
|
3419
|
+
} catch (e) {
|
|
3420
|
+
json(res, 400, { error: (e && e.message) || String(e) });
|
|
3421
|
+
}
|
|
3422
|
+
return;
|
|
3423
|
+
}
|
|
3424
|
+
|
|
2310
3425
|
if (req.method === "GET" && url.pathname === "/api/user-env") {
|
|
2311
3426
|
try {
|
|
2312
3427
|
json(res, 200, { env: readUserEnvRows(userCtx.userId) });
|
|
@@ -2384,16 +3499,41 @@ export function startUiServer({
|
|
|
2384
3499
|
|
|
2385
3500
|
if (req.method === "GET" && url.pathname === "/api/skillhub/search") {
|
|
2386
3501
|
const q = (url.searchParams.get("q") || "").trim();
|
|
3502
|
+
const mode = (url.searchParams.get("mode") || "keyword").trim();
|
|
2387
3503
|
if (!q) {
|
|
2388
3504
|
json(res, 200, { total: 0, items: [] });
|
|
2389
3505
|
return;
|
|
2390
3506
|
}
|
|
3507
|
+
if (mode === "collectionId") {
|
|
3508
|
+
const info = await fetchSkillhubCollectionInfo(q);
|
|
3509
|
+
json(res, 200, {
|
|
3510
|
+
total: 1,
|
|
3511
|
+
mode,
|
|
3512
|
+
items: [info || {
|
|
3513
|
+
id: `collection:${q}`,
|
|
3514
|
+
collection: q,
|
|
3515
|
+
kind: "collection",
|
|
3516
|
+
slug: "",
|
|
3517
|
+
name: `Collection ${q}`,
|
|
3518
|
+
summary: "按 Collection ID 安装该合集中的全部 Skills。",
|
|
3519
|
+
version: "",
|
|
3520
|
+
tags: [],
|
|
3521
|
+
}],
|
|
3522
|
+
});
|
|
3523
|
+
return;
|
|
3524
|
+
}
|
|
2391
3525
|
const result = await runSkillhub(["search", "-q", q], { cwd: root });
|
|
2392
3526
|
if (!result.ok) {
|
|
2393
3527
|
json(res, 500, { error: result.error, stdout: result.stdout });
|
|
2394
3528
|
return;
|
|
2395
3529
|
}
|
|
2396
|
-
|
|
3530
|
+
const payload = normalizeSkillhubSearchPayload(parseJsonText(result.stdout, {}));
|
|
3531
|
+
if (mode === "skillId") {
|
|
3532
|
+
const filtered = payload.items.filter((item) => item.skillId === q || item.id === q);
|
|
3533
|
+
json(res, 200, { ...payload, mode, total: filtered.length, items: filtered });
|
|
3534
|
+
return;
|
|
3535
|
+
}
|
|
3536
|
+
json(res, 200, { ...payload, mode });
|
|
2397
3537
|
return;
|
|
2398
3538
|
}
|
|
2399
3539
|
|
|
@@ -2410,12 +3550,19 @@ export function startUiServer({
|
|
|
2410
3550
|
json(res, 400, { error: "Missing skill slug or collection" });
|
|
2411
3551
|
return;
|
|
2412
3552
|
}
|
|
3553
|
+
const beforeSkills = payload?.collection ? listComposerSkills(PACKAGE_ROOT, root) : [];
|
|
2413
3554
|
const result = await runSkillhub(args, { cwd: root, timeoutMs: 180_000, maxBuffer: 4 * 1024 * 1024 });
|
|
2414
3555
|
if (!result.ok) {
|
|
2415
3556
|
json(res, 500, { error: result.error, stdout: result.stdout });
|
|
2416
3557
|
return;
|
|
2417
3558
|
}
|
|
2418
|
-
|
|
3559
|
+
let skillCollections = null;
|
|
3560
|
+
if (payload?.collection) {
|
|
3561
|
+
const afterSkills = listComposerSkills(PACKAGE_ROOT, root);
|
|
3562
|
+
const collectionName = String(payload.collectionName || payload.name || "").trim();
|
|
3563
|
+
skillCollections = upsertSkillhubCollectionGroup(userCtx, payload.collection, beforeSkills, afterSkills, collectionName);
|
|
3564
|
+
}
|
|
3565
|
+
json(res, 200, { ok: true, stdout: result.stdout, skillCollections });
|
|
2419
3566
|
return;
|
|
2420
3567
|
}
|
|
2421
3568
|
|
|
@@ -2437,7 +3584,8 @@ export function startUiServer({
|
|
|
2437
3584
|
json(res, 500, { error: result.error, stdout: result.stdout });
|
|
2438
3585
|
return;
|
|
2439
3586
|
}
|
|
2440
|
-
|
|
3587
|
+
const skillCollections = payload?.collection ? removeSkillhubCollectionGroup(userCtx, payload.collection, root) : null;
|
|
3588
|
+
json(res, 200, { ok: true, stdout: result.stdout, skillCollections });
|
|
2441
3589
|
return;
|
|
2442
3590
|
}
|
|
2443
3591
|
|