@fieldwangai/agentflow 0.1.34 → 0.1.35
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/composer-agent.mjs +2 -0
- package/bin/lib/composer-skill-router.mjs +23 -4
- package/bin/lib/skill-registry.mjs +46 -2
- package/bin/lib/ui-server.mjs +884 -13
- package/builtin/web-ui/dist/assets/index-BzmhleR9.css +1 -0
- package/builtin/web-ui/dist/assets/index-DEeZI5V6.js +214 -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
|
@@ -297,6 +297,45 @@ function writeSkillCollectionConfig(userCtx = {}, payload = {}, availableSkills
|
|
|
297
297
|
return config;
|
|
298
298
|
}
|
|
299
299
|
|
|
300
|
+
function upsertSkillhubCollectionGroup(userCtx = {}, collectionId = "", beforeSkills = [], afterSkills = [], collectionName = "") {
|
|
301
|
+
const rawCollectionId = String(collectionId || "").trim();
|
|
302
|
+
if (!rawCollectionId) return null;
|
|
303
|
+
const beforeKeys = new Set((Array.isArray(beforeSkills) ? beforeSkills : []).map((skill) => String(skill?.key || "")).filter(Boolean));
|
|
304
|
+
const addedKeys = (Array.isArray(afterSkills) ? afterSkills : [])
|
|
305
|
+
.map((skill) => String(skill?.key || "").trim())
|
|
306
|
+
.filter((key) => key && !beforeKeys.has(key));
|
|
307
|
+
const config = readSkillCollectionConfig(userCtx, afterSkills);
|
|
308
|
+
const groupId = slugifySkillCollectionId(`skillhub-collection-${rawCollectionId}`, "skillhub-collection");
|
|
309
|
+
const now = Date.now();
|
|
310
|
+
const existing = config.collections.find((collection) => collection.id === groupId);
|
|
311
|
+
const existingKeys = Array.isArray(existing?.skillKeys) ? existing.skillKeys : [];
|
|
312
|
+
const mergedKeys = Array.from(new Set([...existingKeys, ...addedKeys]));
|
|
313
|
+
const nextCollections = config.collections.filter((collection) => collection.id !== groupId);
|
|
314
|
+
nextCollections.push({
|
|
315
|
+
id: groupId,
|
|
316
|
+
name: String(collectionName || "").trim() || `SkillHub Collection ${rawCollectionId}`,
|
|
317
|
+
skillKeys: mergedKeys,
|
|
318
|
+
builtin: false,
|
|
319
|
+
createdAt: Number.isFinite(existing?.createdAt) ? existing.createdAt : now,
|
|
320
|
+
updatedAt: now,
|
|
321
|
+
});
|
|
322
|
+
return writeSkillCollectionConfig(userCtx, { version: 1, collections: nextCollections }, afterSkills);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function removeSkillhubCollectionGroup(userCtx = {}, collectionId = "", root = process.cwd()) {
|
|
326
|
+
const rawCollectionId = String(collectionId || "").trim();
|
|
327
|
+
if (!rawCollectionId) return null;
|
|
328
|
+
const availableSkills = listComposerSkills(PACKAGE_ROOT, root);
|
|
329
|
+
const config = readSkillCollectionConfig(userCtx, availableSkills);
|
|
330
|
+
const groupId = slugifySkillCollectionId(`skillhub-collection-${rawCollectionId}`, "skillhub-collection");
|
|
331
|
+
if (!config.collections.some((collection) => collection.id === groupId)) return config;
|
|
332
|
+
return writeSkillCollectionConfig(
|
|
333
|
+
userCtx,
|
|
334
|
+
{ version: 1, collections: config.collections.filter((collection) => collection.id !== groupId) },
|
|
335
|
+
availableSkills,
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
300
339
|
function runtimeEnvForUser(userCtx = {}, extra = {}) {
|
|
301
340
|
return {
|
|
302
341
|
...process.env,
|
|
@@ -317,6 +356,420 @@ function readAgentflowUserConfigObject() {
|
|
|
317
356
|
}
|
|
318
357
|
}
|
|
319
358
|
|
|
359
|
+
function cursorMcpConfigPath() {
|
|
360
|
+
return path.join(os.homedir(), ".cursor", "mcp.json");
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function readCursorMcpConfig() {
|
|
364
|
+
const p = cursorMcpConfigPath();
|
|
365
|
+
try {
|
|
366
|
+
if (!fs.existsSync(p)) return { mcpServers: {} };
|
|
367
|
+
const data = JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
368
|
+
return data && typeof data === "object" && !Array.isArray(data) ? data : { mcpServers: {} };
|
|
369
|
+
} catch {
|
|
370
|
+
return { mcpServers: {} };
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function userMcpPrivatePath(userCtx = {}) {
|
|
375
|
+
return path.join(getAgentflowUserDataRoot(userCtx.userId), "mcp-private.json");
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function readUserMcpPrivate(userCtx = {}) {
|
|
379
|
+
const p = userMcpPrivatePath(userCtx);
|
|
380
|
+
try {
|
|
381
|
+
if (!fs.existsSync(p)) return { version: 1, servers: {} };
|
|
382
|
+
const data = JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
383
|
+
const servers = data?.servers && typeof data.servers === "object" && !Array.isArray(data.servers) ? data.servers : {};
|
|
384
|
+
return { version: 1, servers };
|
|
385
|
+
} catch {
|
|
386
|
+
return { version: 1, servers: {} };
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function writeUserMcpPrivate(userCtx = {}, data = {}) {
|
|
391
|
+
const p = userMcpPrivatePath(userCtx);
|
|
392
|
+
const servers = data?.servers && typeof data.servers === "object" && !Array.isArray(data.servers) ? data.servers : {};
|
|
393
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
394
|
+
fs.writeFileSync(p, JSON.stringify({ version: 1, servers }, null, 2) + "\n", "utf-8");
|
|
395
|
+
return { version: 1, servers };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function normalizeMcpPrivateKeys(keys) {
|
|
399
|
+
return new Set((Array.isArray(keys) ? keys : []).map((key) => String(key || "").trim()).filter(Boolean));
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function pickObjectKeys(obj, keys) {
|
|
403
|
+
const out = {};
|
|
404
|
+
for (const key of keys) {
|
|
405
|
+
if (obj && Object.prototype.hasOwnProperty.call(obj, key)) out[key] = String(obj[key] ?? "");
|
|
406
|
+
}
|
|
407
|
+
return out;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function omitObjectKeys(obj, keys) {
|
|
411
|
+
const out = {};
|
|
412
|
+
for (const [key, value] of Object.entries(obj && typeof obj === "object" ? obj : {})) {
|
|
413
|
+
if (!keys.has(key)) out[key] = value;
|
|
414
|
+
}
|
|
415
|
+
return out;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function normalizeMcpServerConfig(value) {
|
|
419
|
+
const raw = value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
420
|
+
const next = {};
|
|
421
|
+
const url = typeof raw.url === "string" ? raw.url.trim() : "";
|
|
422
|
+
const command = typeof raw.command === "string" ? raw.command.trim() : "";
|
|
423
|
+
const description = typeof raw.description === "string" ? raw.description.trim() : "";
|
|
424
|
+
if (url) next.url = url;
|
|
425
|
+
if (command) next.command = command;
|
|
426
|
+
if (Array.isArray(raw.args)) next.args = raw.args.map((x) => String(x)).filter((x) => x.length > 0);
|
|
427
|
+
if (raw.env && typeof raw.env === "object" && !Array.isArray(raw.env)) {
|
|
428
|
+
const env = {};
|
|
429
|
+
for (const [k, v] of Object.entries(raw.env)) {
|
|
430
|
+
const key = String(k || "").trim();
|
|
431
|
+
if (key) env[key] = String(v ?? "");
|
|
432
|
+
}
|
|
433
|
+
if (Object.keys(env).length) next.env = env;
|
|
434
|
+
}
|
|
435
|
+
if (raw.headers && typeof raw.headers === "object" && !Array.isArray(raw.headers)) {
|
|
436
|
+
const headers = {};
|
|
437
|
+
for (const [k, v] of Object.entries(raw.headers)) {
|
|
438
|
+
const key = String(k || "").trim();
|
|
439
|
+
if (key) headers[key] = String(v ?? "");
|
|
440
|
+
}
|
|
441
|
+
if (Object.keys(headers).length) next.headers = headers;
|
|
442
|
+
}
|
|
443
|
+
if (description) next.description = description;
|
|
444
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
445
|
+
if (["url", "command", "args", "env", "headers", "description"].includes(k)) continue;
|
|
446
|
+
next[k] = v;
|
|
447
|
+
}
|
|
448
|
+
return next;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function readCursorMcpServers(userCtx = {}) {
|
|
452
|
+
const config = readCursorMcpConfig();
|
|
453
|
+
const privateConfig = readUserMcpPrivate(userCtx);
|
|
454
|
+
const rawServers = config.mcpServers && typeof config.mcpServers === "object" && !Array.isArray(config.mcpServers)
|
|
455
|
+
? config.mcpServers
|
|
456
|
+
: {};
|
|
457
|
+
const servers = Object.entries(rawServers).map(([name, value]) => {
|
|
458
|
+
const publicValue = normalizeMcpServerConfig(value);
|
|
459
|
+
const privateValue = privateConfig.servers?.[name] && typeof privateConfig.servers[name] === "object" ? privateConfig.servers[name] : {};
|
|
460
|
+
const privateEnv = privateValue.env && typeof privateValue.env === "object" && !Array.isArray(privateValue.env) ? privateValue.env : {};
|
|
461
|
+
const privateHeaders = privateValue.headers && typeof privateValue.headers === "object" && !Array.isArray(privateValue.headers) ? privateValue.headers : {};
|
|
462
|
+
const configValue = {
|
|
463
|
+
...publicValue,
|
|
464
|
+
env: { ...(publicValue.env || {}), ...privateEnv },
|
|
465
|
+
headers: { ...(publicValue.headers || {}), ...privateHeaders },
|
|
466
|
+
};
|
|
467
|
+
return {
|
|
468
|
+
name,
|
|
469
|
+
type: configValue.url ? "url" : "command",
|
|
470
|
+
url: typeof configValue.url === "string" ? configValue.url : "",
|
|
471
|
+
command: typeof configValue.command === "string" ? configValue.command : "",
|
|
472
|
+
args: Array.isArray(configValue.args) ? configValue.args : [],
|
|
473
|
+
env: configValue.env && typeof configValue.env === "object" ? configValue.env : {},
|
|
474
|
+
headers: configValue.headers && typeof configValue.headers === "object" ? configValue.headers : {},
|
|
475
|
+
description: typeof configValue.description === "string" ? configValue.description : "",
|
|
476
|
+
raw: configValue,
|
|
477
|
+
privateEnvKeys: Object.keys(privateEnv),
|
|
478
|
+
privateHeaderKeys: Object.keys(privateHeaders),
|
|
479
|
+
};
|
|
480
|
+
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
481
|
+
return { path: cursorMcpConfigPath(), servers };
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function writeCursorMcpServer(payload = {}, userCtx = {}) {
|
|
485
|
+
const name = String(payload?.name || "").trim();
|
|
486
|
+
const nextName = String(payload?.nextName || payload?.name || "").trim();
|
|
487
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(nextName)) throw new Error("Invalid MCP name");
|
|
488
|
+
const server = normalizeMcpServerConfig(payload?.server);
|
|
489
|
+
if (!server.url && !server.command) throw new Error("MCP server requires url or command");
|
|
490
|
+
const privateEnvKeys = normalizeMcpPrivateKeys(payload?.privateEnvKeys);
|
|
491
|
+
const privateHeaderKeys = normalizeMcpPrivateKeys(payload?.privateHeaderKeys);
|
|
492
|
+
const privateEnv = pickObjectKeys(server.env || {}, privateEnvKeys);
|
|
493
|
+
const privateHeaders = pickObjectKeys(server.headers || {}, privateHeaderKeys);
|
|
494
|
+
const publicServer = {
|
|
495
|
+
...server,
|
|
496
|
+
env: omitObjectKeys(server.env || {}, privateEnvKeys),
|
|
497
|
+
headers: omitObjectKeys(server.headers || {}, privateHeaderKeys),
|
|
498
|
+
};
|
|
499
|
+
if (!Object.keys(publicServer.env).length) delete publicServer.env;
|
|
500
|
+
if (!Object.keys(publicServer.headers).length) delete publicServer.headers;
|
|
501
|
+
const p = cursorMcpConfigPath();
|
|
502
|
+
const config = readCursorMcpConfig();
|
|
503
|
+
const mcpServers = config.mcpServers && typeof config.mcpServers === "object" && !Array.isArray(config.mcpServers)
|
|
504
|
+
? { ...config.mcpServers }
|
|
505
|
+
: {};
|
|
506
|
+
if (name && name !== nextName) delete mcpServers[name];
|
|
507
|
+
mcpServers[nextName] = publicServer;
|
|
508
|
+
const next = { ...config, mcpServers };
|
|
509
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
510
|
+
fs.writeFileSync(p, JSON.stringify(next, null, 2) + "\n", "utf-8");
|
|
511
|
+
const privateConfig = readUserMcpPrivate(userCtx);
|
|
512
|
+
const privateServers = { ...(privateConfig.servers || {}) };
|
|
513
|
+
if (name && name !== nextName) delete privateServers[name];
|
|
514
|
+
if (Object.keys(privateEnv).length || Object.keys(privateHeaders).length) {
|
|
515
|
+
privateServers[nextName] = {
|
|
516
|
+
...(Object.keys(privateEnv).length ? { env: privateEnv } : {}),
|
|
517
|
+
...(Object.keys(privateHeaders).length ? { headers: privateHeaders } : {}),
|
|
518
|
+
};
|
|
519
|
+
} else {
|
|
520
|
+
delete privateServers[nextName];
|
|
521
|
+
}
|
|
522
|
+
writeUserMcpPrivate(userCtx, { servers: privateServers });
|
|
523
|
+
return readCursorMcpServers(userCtx);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function deleteCursorMcpServer(name, userCtx = {}) {
|
|
527
|
+
const key = String(name || "").trim();
|
|
528
|
+
if (!key) throw new Error("Missing MCP name");
|
|
529
|
+
const p = cursorMcpConfigPath();
|
|
530
|
+
const config = readCursorMcpConfig();
|
|
531
|
+
const mcpServers = config.mcpServers && typeof config.mcpServers === "object" && !Array.isArray(config.mcpServers)
|
|
532
|
+
? { ...config.mcpServers }
|
|
533
|
+
: {};
|
|
534
|
+
delete mcpServers[key];
|
|
535
|
+
const next = { ...config, mcpServers };
|
|
536
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
537
|
+
fs.writeFileSync(p, JSON.stringify(next, null, 2) + "\n", "utf-8");
|
|
538
|
+
const privateConfig = readUserMcpPrivate(userCtx);
|
|
539
|
+
const privateServers = { ...(privateConfig.servers || {}) };
|
|
540
|
+
delete privateServers[key];
|
|
541
|
+
writeUserMcpPrivate(userCtx, { servers: privateServers });
|
|
542
|
+
return readCursorMcpServers(userCtx);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function compactErrorMessage(error) {
|
|
546
|
+
const text = String(error?.message || error || "").trim();
|
|
547
|
+
return text.length > 260 ? `${text.slice(0, 257)}...` : text;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function parseMcpSsePayload(text) {
|
|
551
|
+
const events = [];
|
|
552
|
+
let data = [];
|
|
553
|
+
for (const rawLine of String(text || "").split(/\r?\n/g)) {
|
|
554
|
+
const line = rawLine.trimEnd();
|
|
555
|
+
if (!line) {
|
|
556
|
+
if (data.length) {
|
|
557
|
+
const joined = data.join("\n").trim();
|
|
558
|
+
if (joined) events.push(joined);
|
|
559
|
+
data = [];
|
|
560
|
+
}
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
if (line.startsWith("data:")) data.push(line.slice(5).trimStart());
|
|
564
|
+
}
|
|
565
|
+
if (data.length) events.push(data.join("\n").trim());
|
|
566
|
+
for (const event of events) {
|
|
567
|
+
try {
|
|
568
|
+
const parsed = JSON.parse(event);
|
|
569
|
+
if (parsed && typeof parsed === "object") return parsed;
|
|
570
|
+
} catch {}
|
|
571
|
+
}
|
|
572
|
+
return null;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
async function mcpHttpRequest(url, headers, body, sessionId = "") {
|
|
576
|
+
const controller = new AbortController();
|
|
577
|
+
const timer = setTimeout(() => controller.abort(), 8000);
|
|
578
|
+
try {
|
|
579
|
+
const response = await fetch(url, {
|
|
580
|
+
method: "POST",
|
|
581
|
+
headers: {
|
|
582
|
+
"Accept": "application/json, text/event-stream",
|
|
583
|
+
"Content-Type": "application/json",
|
|
584
|
+
...(headers || {}),
|
|
585
|
+
...(sessionId ? { "Mcp-Session-Id": sessionId } : {}),
|
|
586
|
+
},
|
|
587
|
+
body: JSON.stringify(body),
|
|
588
|
+
signal: controller.signal,
|
|
589
|
+
});
|
|
590
|
+
const text = await response.text();
|
|
591
|
+
if (!response.ok) throw new Error(`${response.status} ${response.statusText}: ${text.slice(0, 180)}`);
|
|
592
|
+
const contentType = String(response.headers.get("content-type") || "").toLowerCase();
|
|
593
|
+
const parsed = contentType.includes("text/event-stream") ? parseMcpSsePayload(text) : JSON.parse(text || "{}");
|
|
594
|
+
return { message: parsed, sessionId: response.headers.get("mcp-session-id") || sessionId };
|
|
595
|
+
} finally {
|
|
596
|
+
clearTimeout(timer);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
async function checkMcpHttpServer(server) {
|
|
601
|
+
const url = String(server?.raw?.url || server?.url || "").trim();
|
|
602
|
+
if (!url) throw new Error("Missing MCP URL");
|
|
603
|
+
const headers = server?.raw?.headers && typeof server.raw.headers === "object" ? server.raw.headers : {};
|
|
604
|
+
const init = await mcpHttpRequest(url, headers, {
|
|
605
|
+
jsonrpc: "2.0",
|
|
606
|
+
id: 1,
|
|
607
|
+
method: "initialize",
|
|
608
|
+
params: {
|
|
609
|
+
protocolVersion: "2024-11-05",
|
|
610
|
+
capabilities: {},
|
|
611
|
+
clientInfo: { name: "agentflow", version: "0.1.0" },
|
|
612
|
+
},
|
|
613
|
+
});
|
|
614
|
+
if (init.message?.error) throw new Error(init.message.error.message || "MCP initialize failed");
|
|
615
|
+
await mcpHttpRequest(url, headers, {
|
|
616
|
+
jsonrpc: "2.0",
|
|
617
|
+
method: "notifications/initialized",
|
|
618
|
+
params: {},
|
|
619
|
+
}, init.sessionId).catch(() => null);
|
|
620
|
+
const tools = await mcpHttpRequest(url, headers, {
|
|
621
|
+
jsonrpc: "2.0",
|
|
622
|
+
id: 2,
|
|
623
|
+
method: "tools/list",
|
|
624
|
+
params: {},
|
|
625
|
+
}, init.sessionId);
|
|
626
|
+
if (tools.message?.error) throw new Error(tools.message.error.message || "MCP tools/list failed");
|
|
627
|
+
return Array.isArray(tools.message?.result?.tools) ? tools.message.result.tools : [];
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
async function checkMcpStdioServer(server) {
|
|
631
|
+
const command = String(server?.raw?.command || server?.command || "").trim();
|
|
632
|
+
if (!command) throw new Error("Missing MCP command");
|
|
633
|
+
const args = Array.isArray(server?.raw?.args) ? server.raw.args.map(String) : [];
|
|
634
|
+
const env = server?.raw?.env && typeof server.raw.env === "object" ? server.raw.env : {};
|
|
635
|
+
const child = spawn(command, args, {
|
|
636
|
+
cwd: os.homedir(),
|
|
637
|
+
env: { ...process.env, ...env },
|
|
638
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
639
|
+
});
|
|
640
|
+
let buffer = "";
|
|
641
|
+
let stderr = "";
|
|
642
|
+
let processError = null;
|
|
643
|
+
const pending = new Map();
|
|
644
|
+
let nextId = 1;
|
|
645
|
+
const cleanup = () => {
|
|
646
|
+
for (const [, request] of pending) clearTimeout(request.timer);
|
|
647
|
+
pending.clear();
|
|
648
|
+
if (!child.killed) child.kill("SIGTERM");
|
|
649
|
+
};
|
|
650
|
+
const rejectPending = (error) => {
|
|
651
|
+
for (const [, request] of pending) {
|
|
652
|
+
clearTimeout(request.timer);
|
|
653
|
+
request.reject(error);
|
|
654
|
+
}
|
|
655
|
+
pending.clear();
|
|
656
|
+
};
|
|
657
|
+
child.on("error", (error) => {
|
|
658
|
+
processError = error;
|
|
659
|
+
rejectPending(error);
|
|
660
|
+
});
|
|
661
|
+
child.stdin.on("error", (error) => {
|
|
662
|
+
processError = error;
|
|
663
|
+
rejectPending(error);
|
|
664
|
+
});
|
|
665
|
+
child.stdout.setEncoding("utf8");
|
|
666
|
+
child.stderr.setEncoding("utf8");
|
|
667
|
+
child.stderr.on("data", (chunk) => {
|
|
668
|
+
stderr += String(chunk || "");
|
|
669
|
+
if (stderr.length > 2000) stderr = stderr.slice(-2000);
|
|
670
|
+
});
|
|
671
|
+
child.stdout.on("data", (chunk) => {
|
|
672
|
+
buffer += String(chunk || "");
|
|
673
|
+
const lines = buffer.split(/\r?\n/g);
|
|
674
|
+
buffer = lines.pop() || "";
|
|
675
|
+
for (const line of lines) {
|
|
676
|
+
const text = line.trim();
|
|
677
|
+
if (!text) continue;
|
|
678
|
+
let message = null;
|
|
679
|
+
try {
|
|
680
|
+
message = JSON.parse(text);
|
|
681
|
+
} catch {
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
const request = pending.get(message.id);
|
|
685
|
+
if (request) {
|
|
686
|
+
pending.delete(message.id);
|
|
687
|
+
clearTimeout(request.timer);
|
|
688
|
+
request.resolve(message);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
const send = (method, params = {}, timeoutMs = 8000) => new Promise((resolve, reject) => {
|
|
693
|
+
if (processError) {
|
|
694
|
+
reject(processError);
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
const id = nextId++;
|
|
698
|
+
const timer = setTimeout(() => {
|
|
699
|
+
pending.delete(id);
|
|
700
|
+
reject(new Error(`${method} timed out${stderr.trim() ? `: ${stderr.trim().slice(-220)}` : ""}`));
|
|
701
|
+
}, timeoutMs);
|
|
702
|
+
pending.set(id, { resolve, reject, timer });
|
|
703
|
+
child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n", (error) => {
|
|
704
|
+
if (!error) return;
|
|
705
|
+
pending.delete(id);
|
|
706
|
+
clearTimeout(timer);
|
|
707
|
+
reject(error);
|
|
708
|
+
});
|
|
709
|
+
});
|
|
710
|
+
const notify = (method, params = {}) => {
|
|
711
|
+
child.stdin.write(JSON.stringify({ jsonrpc: "2.0", method, params }) + "\n");
|
|
712
|
+
};
|
|
713
|
+
try {
|
|
714
|
+
const init = await send("initialize", {
|
|
715
|
+
protocolVersion: "2024-11-05",
|
|
716
|
+
capabilities: {},
|
|
717
|
+
clientInfo: { name: "agentflow", version: "0.1.0" },
|
|
718
|
+
});
|
|
719
|
+
if (init?.error) throw new Error(init.error.message || "MCP initialize failed");
|
|
720
|
+
notify("notifications/initialized", {});
|
|
721
|
+
const tools = await send("tools/list", {}, 8000);
|
|
722
|
+
if (tools?.error) throw new Error(tools.error.message || "MCP tools/list failed");
|
|
723
|
+
return Array.isArray(tools?.result?.tools) ? tools.result.tools : [];
|
|
724
|
+
} finally {
|
|
725
|
+
cleanup();
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
async function checkMcpServer(server) {
|
|
730
|
+
const startedAt = Date.now();
|
|
731
|
+
try {
|
|
732
|
+
const tools = server?.type === "url" || server?.raw?.url
|
|
733
|
+
? await checkMcpHttpServer(server)
|
|
734
|
+
: await checkMcpStdioServer(server);
|
|
735
|
+
return {
|
|
736
|
+
name: server.name,
|
|
737
|
+
ok: true,
|
|
738
|
+
status: "enabled",
|
|
739
|
+
toolCount: tools.length,
|
|
740
|
+
tools: tools.map((tool) => ({
|
|
741
|
+
name: String(tool?.name || ""),
|
|
742
|
+
description: String(tool?.description || ""),
|
|
743
|
+
})).filter((tool) => tool.name),
|
|
744
|
+
checkedAt: new Date().toISOString(),
|
|
745
|
+
elapsedMs: Date.now() - startedAt,
|
|
746
|
+
};
|
|
747
|
+
} catch (error) {
|
|
748
|
+
return {
|
|
749
|
+
name: server?.name || "",
|
|
750
|
+
ok: false,
|
|
751
|
+
status: "error",
|
|
752
|
+
error: compactErrorMessage(error),
|
|
753
|
+
toolCount: 0,
|
|
754
|
+
tools: [],
|
|
755
|
+
checkedAt: new Date().toISOString(),
|
|
756
|
+
elapsedMs: Date.now() - startedAt,
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
async function checkCursorMcpServers(name = "", userCtx = {}) {
|
|
762
|
+
const { servers } = readCursorMcpServers(userCtx);
|
|
763
|
+
const targetName = String(name || "").trim();
|
|
764
|
+
const targets = targetName ? servers.filter((server) => server.name === targetName) : servers;
|
|
765
|
+
if (targetName && targets.length === 0) throw new Error("MCP server not found");
|
|
766
|
+
const results = [];
|
|
767
|
+
for (const server of targets) {
|
|
768
|
+
results.push(await checkMcpServer(server));
|
|
769
|
+
}
|
|
770
|
+
return { results };
|
|
771
|
+
}
|
|
772
|
+
|
|
320
773
|
function readModelListsFromDisk(workspaceRoot) {
|
|
321
774
|
const p = getModelListsAbs();
|
|
322
775
|
const empty = {
|
|
@@ -344,6 +797,8 @@ function readModelListsFromDisk(workspaceRoot) {
|
|
|
344
797
|
}
|
|
345
798
|
|
|
346
799
|
const SKILLHUB_TIMEOUT_MS = 60_000;
|
|
800
|
+
const SKILLHUB_API_BASE = String(process.env.SKILLHUB_API_BASE || "https://skillhub.bigo.sg/api/v1").replace(/\/+$/, "");
|
|
801
|
+
const skillhubCollectionInfoCache = new Map();
|
|
347
802
|
|
|
348
803
|
function runSkillhub(args, opts = {}) {
|
|
349
804
|
return new Promise((resolve) => {
|
|
@@ -369,6 +824,72 @@ function runSkillhub(args, opts = {}) {
|
|
|
369
824
|
});
|
|
370
825
|
}
|
|
371
826
|
|
|
827
|
+
function readSkillhubAuthToken() {
|
|
828
|
+
try {
|
|
829
|
+
const p = path.join(os.homedir(), ".skillhub", "auth.json");
|
|
830
|
+
if (!fs.existsSync(p)) return "";
|
|
831
|
+
const data = JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
832
|
+
return String(data?.token || data?.accessToken || data?.access_token || "").trim();
|
|
833
|
+
} catch {
|
|
834
|
+
return "";
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function normalizeSkillhubCollectionInfo(raw, collectionId) {
|
|
839
|
+
const data = raw?.data && typeof raw.data === "object"
|
|
840
|
+
? raw.data
|
|
841
|
+
: raw?.collection && typeof raw.collection === "object"
|
|
842
|
+
? raw.collection
|
|
843
|
+
: raw?.item && typeof raw.item === "object"
|
|
844
|
+
? raw.item
|
|
845
|
+
: raw && typeof raw === "object"
|
|
846
|
+
? raw
|
|
847
|
+
: {};
|
|
848
|
+
const id = String(data.id ?? collectionId ?? "").trim();
|
|
849
|
+
const name = String(data.name ?? data.displayName ?? data.display_name ?? data.title ?? "").trim();
|
|
850
|
+
const summary = String(data.description ?? data.summary ?? data.subtitle ?? "").trim();
|
|
851
|
+
const version = String(data.version ?? data.latestVersion ?? data.latest_version ?? "").trim();
|
|
852
|
+
const tags = Array.isArray(data.tags) ? data.tags.map(String).filter(Boolean) : [];
|
|
853
|
+
if (!id && !name) return null;
|
|
854
|
+
return {
|
|
855
|
+
id: id || String(collectionId || ""),
|
|
856
|
+
collection: id || String(collectionId || ""),
|
|
857
|
+
kind: "collection",
|
|
858
|
+
slug: "",
|
|
859
|
+
name: name || `Collection ${collectionId}`,
|
|
860
|
+
summary: summary || "按 Collection ID 安装该合集中的全部 Skills。",
|
|
861
|
+
version,
|
|
862
|
+
tags,
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
async function fetchSkillhubCollectionInfo(collectionId) {
|
|
867
|
+
const id = String(collectionId || "").trim();
|
|
868
|
+
if (!id) return null;
|
|
869
|
+
const cached = skillhubCollectionInfoCache.get(id);
|
|
870
|
+
if (cached) return cached;
|
|
871
|
+
if (typeof fetch !== "function") return null;
|
|
872
|
+
const controller = new AbortController();
|
|
873
|
+
const timer = setTimeout(() => controller.abort(), 8000);
|
|
874
|
+
try {
|
|
875
|
+
const token = readSkillhubAuthToken();
|
|
876
|
+
const headers = token ? { Authorization: `Bearer ${token}` } : {};
|
|
877
|
+
const r = await fetch(`${SKILLHUB_API_BASE}/collections/${encodeURIComponent(id)}`, {
|
|
878
|
+
headers,
|
|
879
|
+
signal: controller.signal,
|
|
880
|
+
});
|
|
881
|
+
if (!r.ok) return null;
|
|
882
|
+
const raw = await r.json().catch(() => null);
|
|
883
|
+
const info = normalizeSkillhubCollectionInfo(raw, id);
|
|
884
|
+
if (info) skillhubCollectionInfoCache.set(id, info);
|
|
885
|
+
return info;
|
|
886
|
+
} catch {
|
|
887
|
+
return null;
|
|
888
|
+
} finally {
|
|
889
|
+
clearTimeout(timer);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
372
893
|
function parseJsonText(text, fallback = null) {
|
|
373
894
|
const s = String(text || "").trim();
|
|
374
895
|
if (!s) return fallback;
|
|
@@ -394,11 +915,13 @@ function normalizeSkillhubSearchPayload(raw) {
|
|
|
394
915
|
const slug = String(x.slug ?? x.name ?? x.displayName ?? x.display_name ?? id ?? "").trim();
|
|
395
916
|
return {
|
|
396
917
|
id: String(id || slug),
|
|
918
|
+
skillId: String(id || ""),
|
|
397
919
|
slug,
|
|
398
920
|
name: String(x.displayName ?? x.display_name ?? x.name ?? slug),
|
|
399
921
|
summary: String(x.summary ?? x.description ?? ""),
|
|
400
922
|
version: String(x.version ?? x.latestVersion ?? x.latest_version ?? ""),
|
|
401
923
|
tags: Array.isArray(x.tags) ? x.tags.map(String) : [],
|
|
924
|
+
kind: "skill",
|
|
402
925
|
};
|
|
403
926
|
}).filter((x) => x.slug || x.name),
|
|
404
927
|
};
|
|
@@ -661,6 +1184,47 @@ function buildWorkspaceGeneratePrompt(payload) {
|
|
|
661
1184
|
].filter(Boolean).join("\n");
|
|
662
1185
|
}
|
|
663
1186
|
|
|
1187
|
+
function buildWorkspaceNodeChatPrompt(payload) {
|
|
1188
|
+
const node = payload?.node && typeof payload.node === "object" ? payload.node : {};
|
|
1189
|
+
const userMessage = String(payload?.message || "").trim();
|
|
1190
|
+
const currentContent = String(payload?.currentContent || "").trim();
|
|
1191
|
+
const nodeKind = String(payload?.nodeKind || payload?.kind || "markdown").trim().toLowerCase();
|
|
1192
|
+
const history = Array.isArray(payload?.messages) ? payload.messages : [];
|
|
1193
|
+
const historyBlock = history
|
|
1194
|
+
.slice(-8)
|
|
1195
|
+
.map((msg) => {
|
|
1196
|
+
const role = String(msg?.role || "user").trim() === "assistant" ? "assistant" : "user";
|
|
1197
|
+
const text = String(msg?.text || "").trim();
|
|
1198
|
+
return text ? `${role}: ${text}` : "";
|
|
1199
|
+
})
|
|
1200
|
+
.filter(Boolean)
|
|
1201
|
+
.join("\n\n");
|
|
1202
|
+
const outputRule =
|
|
1203
|
+
nodeKind === "html"
|
|
1204
|
+
? "只输出完整或片段 HTML,不要解释,不要包裹 Markdown 代码围栏。"
|
|
1205
|
+
: nodeKind === "image"
|
|
1206
|
+
? "只输出新的图片 src,可以是 URL、data URL 或文件路径,不要解释。"
|
|
1207
|
+
: nodeKind === "mermaid"
|
|
1208
|
+
? "只输出 Mermaid 源码,不要解释,不要包裹 Markdown 代码围栏。"
|
|
1209
|
+
: nodeKind === "ascii"
|
|
1210
|
+
? "只输出 ASCII 正文,不要解释,不要包裹 Markdown 代码围栏。"
|
|
1211
|
+
: "只输出新的 Markdown 正文,不要解释,不要包裹 Markdown 代码围栏。";
|
|
1212
|
+
return [
|
|
1213
|
+
"你正在微调 AgentFlow Workspace 画布中的单个展示节点。",
|
|
1214
|
+
"根据用户 follow-up 和当前节点内容,生成一个可直接替换当前节点展示内容的候选版本。",
|
|
1215
|
+
outputRule,
|
|
1216
|
+
"",
|
|
1217
|
+
"## 当前节点",
|
|
1218
|
+
`- id: ${String(node.id || "").trim() || "(unknown)"}`,
|
|
1219
|
+
`- label: ${String(node.label || "").trim() || "(unnamed)"}`,
|
|
1220
|
+
`- definitionId: ${String(node.definitionId || "").trim() || "(unknown)"}`,
|
|
1221
|
+
`- kind: ${nodeKind}`,
|
|
1222
|
+
currentContent ? `\n## 当前展示内容\n\n${currentContent}` : "",
|
|
1223
|
+
historyBlock ? `\n## 本节点对话历史\n\n${historyBlock}` : "",
|
|
1224
|
+
`\n## 用户 follow-up\n\n${userMessage}`,
|
|
1225
|
+
].filter(Boolean).join("\n");
|
|
1226
|
+
}
|
|
1227
|
+
|
|
664
1228
|
function workspaceSlotValue(slot) {
|
|
665
1229
|
if (!slot || typeof slot !== "object") return "";
|
|
666
1230
|
for (const key of ["value", "default"]) {
|
|
@@ -720,6 +1284,75 @@ function workspaceDisplayKind(definitionId) {
|
|
|
720
1284
|
return "";
|
|
721
1285
|
}
|
|
722
1286
|
|
|
1287
|
+
function normalizeHtmlDisplayContent(content) {
|
|
1288
|
+
let text = String(content || "").trim();
|
|
1289
|
+
if (!text) return "";
|
|
1290
|
+
const fenced = text.match(/```(?:html|HTML)?\s*\n?([\s\S]*?)```/);
|
|
1291
|
+
if (fenced && fenced[1]) text = fenced[1].trim();
|
|
1292
|
+
else {
|
|
1293
|
+
const openFence = text.match(/```(?:html|HTML)?\s*\n?([\s\S]*)$/);
|
|
1294
|
+
if (openFence && openFence[1]) text = openFence[1].trim();
|
|
1295
|
+
}
|
|
1296
|
+
text = text.replace(/^html\s*\n/i, "").replace(/```\s*$/g, "").trim();
|
|
1297
|
+
const markerPatterns = [
|
|
1298
|
+
/<!doctype\b/i,
|
|
1299
|
+
/<html\b/i,
|
|
1300
|
+
/<head\b/i,
|
|
1301
|
+
/<body\b/i,
|
|
1302
|
+
/<style\b/i,
|
|
1303
|
+
/<script\b/i,
|
|
1304
|
+
/<main\b/i,
|
|
1305
|
+
/<section\b/i,
|
|
1306
|
+
/<article\b/i,
|
|
1307
|
+
/<div\b/i,
|
|
1308
|
+
/<svg\b/i,
|
|
1309
|
+
/<canvas\b/i,
|
|
1310
|
+
];
|
|
1311
|
+
const firstHtmlIndex = markerPatterns.reduce((best, pattern) => {
|
|
1312
|
+
const match = pattern.exec(text);
|
|
1313
|
+
if (!match) return best;
|
|
1314
|
+
return best < 0 ? match.index : Math.min(best, match.index);
|
|
1315
|
+
}, -1);
|
|
1316
|
+
if (firstHtmlIndex > 0) text = text.slice(firstHtmlIndex).trim();
|
|
1317
|
+
return text;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
function workspaceDownstreamDisplayRequirements(graph, nodeId) {
|
|
1321
|
+
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
1322
|
+
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
1323
|
+
const kinds = new Set();
|
|
1324
|
+
for (const edge of edges) {
|
|
1325
|
+
if (String(edge?.source || "") !== String(nodeId)) continue;
|
|
1326
|
+
const target = instances[String(edge?.target || "")];
|
|
1327
|
+
const kind = workspaceDisplayKind(target?.definitionId);
|
|
1328
|
+
if (kind) kinds.add(kind);
|
|
1329
|
+
}
|
|
1330
|
+
if (kinds.size === 0) return "";
|
|
1331
|
+
const rules = [];
|
|
1332
|
+
if (kinds.has("html")) {
|
|
1333
|
+
rules.push("- 下游连接了 HTML 展示节点:输出可直接放入 iframe 渲染的 HTML。可以是完整 HTML 文档或 HTML fragment;不要使用 Markdown 代码围栏;不要解释生成过程。");
|
|
1334
|
+
}
|
|
1335
|
+
if (kinds.has("markdown")) {
|
|
1336
|
+
rules.push("- 下游连接了 Markdown 展示节点:输出 Markdown 正文;不要包裹在代码围栏中,除非正文确实需要代码块。");
|
|
1337
|
+
}
|
|
1338
|
+
if (kinds.has("mermaid")) {
|
|
1339
|
+
rules.push("- 下游连接了 Mermaid 展示节点:只输出 Mermaid 图表代码,例如 flowchart/sequenceDiagram;不要使用 Markdown 代码围栏;不要附加解释。");
|
|
1340
|
+
}
|
|
1341
|
+
if (kinds.has("ascii")) {
|
|
1342
|
+
rules.push("- 下游连接了 ASCII 展示节点:输出纯文本/ASCII 图或表格;不要输出 HTML 或 Markdown 装饰。");
|
|
1343
|
+
}
|
|
1344
|
+
if (kinds.has("image")) {
|
|
1345
|
+
rules.push("- 下游连接了图片展示节点:输出可作为 img src 使用的图片地址、data URL 或 base64 data URL;不要输出 Markdown 图片语法或解释文字。");
|
|
1346
|
+
}
|
|
1347
|
+
return [
|
|
1348
|
+
"## 下游输出要求",
|
|
1349
|
+
"",
|
|
1350
|
+
...rules,
|
|
1351
|
+
"",
|
|
1352
|
+
"如果用户任务与下游展示格式没有冲突,优先满足上述格式要求;如果用户明确指定了其他格式,以用户任务为准。",
|
|
1353
|
+
].join("\n");
|
|
1354
|
+
}
|
|
1355
|
+
|
|
723
1356
|
function workspaceRunPlan(graph, runNodeId) {
|
|
724
1357
|
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
725
1358
|
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
@@ -786,6 +1419,37 @@ function workspaceUpstreamText(graph, nodeId, outputs) {
|
|
|
786
1419
|
return workspaceInstanceText(instances[sourceId]);
|
|
787
1420
|
}
|
|
788
1421
|
|
|
1422
|
+
function workspaceHandleIndex(handle, prefix) {
|
|
1423
|
+
const match = String(handle || "").match(new RegExp(`^${prefix}-(\\d+)$`));
|
|
1424
|
+
return match ? Number(match[1]) : 0;
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
function workspaceTargetSlotForEdge(graph, edge) {
|
|
1428
|
+
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
1429
|
+
const target = instances[String(edge?.target || "")];
|
|
1430
|
+
const input = Array.isArray(target?.input) ? target.input : [];
|
|
1431
|
+
return input[workspaceHandleIndex(edge?.targetHandle, "input")] || null;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
function isWorkspaceSemanticInputSlot(slot) {
|
|
1435
|
+
const name = String(slot?.name || "");
|
|
1436
|
+
const type = String(slot?.type || "");
|
|
1437
|
+
return type === "node" || name === "prev" || name === "next" || name === "skillsContext" || name === "workspaceContext" || name === "gitContext";
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
function workspaceTaskUpstreamText(graph, nodeId, outputs) {
|
|
1441
|
+
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
1442
|
+
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
1443
|
+
const incoming = edges.filter((edge) => String(edge?.target || "") === String(nodeId));
|
|
1444
|
+
const contentEdges = incoming.filter((edge) => !isWorkspaceSemanticInputSlot(workspaceTargetSlotForEdge(graph, edge)));
|
|
1445
|
+
const contentEdge = contentEdges.find((edge) => String(edge?.targetHandle || "") === "input-1") || contentEdges[0];
|
|
1446
|
+
if (!contentEdge) return "";
|
|
1447
|
+
const sourceId = String(contentEdge.source || "");
|
|
1448
|
+
const out = outputs.get(sourceId);
|
|
1449
|
+
if (out != null && String(out).trim()) return String(out);
|
|
1450
|
+
return workspaceInstanceText(instances[sourceId]);
|
|
1451
|
+
}
|
|
1452
|
+
|
|
789
1453
|
function parseWorkspaceSkillKeys(raw) {
|
|
790
1454
|
const text = String(raw || "").trim();
|
|
791
1455
|
if (!text) return [];
|
|
@@ -808,17 +1472,56 @@ function selectedSkillKeysFromInstance(instance) {
|
|
|
808
1472
|
|
|
809
1473
|
function workspaceUpstreamSkillBlocks(graph, nodeId, outputs) {
|
|
810
1474
|
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
811
|
-
|
|
1475
|
+
const blocks = edges
|
|
812
1476
|
.filter((edge) => String(edge?.target || "") === String(nodeId))
|
|
1477
|
+
.filter((edge) => {
|
|
1478
|
+
const slot = workspaceTargetSlotForEdge(graph, edge);
|
|
1479
|
+
return String(slot?.name || "") === "skillsContext";
|
|
1480
|
+
})
|
|
813
1481
|
.map((edge) => String(outputs.get(String(edge.source || "")) || ""))
|
|
814
|
-
.filter((text) => text.includes("
|
|
815
|
-
.
|
|
1482
|
+
.filter((text) => text.includes("Skill") || text.includes("skill"))
|
|
1483
|
+
.flatMap((text) => text.split(/\n\s*---\s*\n/g))
|
|
1484
|
+
.map((text) => text.trim())
|
|
1485
|
+
.filter(Boolean);
|
|
1486
|
+
return Array.from(new Set(blocks)).join("\n\n---\n\n");
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
function mergeWorkspaceSkillBlocks(...values) {
|
|
1490
|
+
const blocks = values
|
|
1491
|
+
.map((value) => String(value || ""))
|
|
1492
|
+
.filter(Boolean)
|
|
1493
|
+
.flatMap((text) => text.split(/\n\s*---\s*\n/g))
|
|
1494
|
+
.map((text) => text.trim())
|
|
1495
|
+
.filter(Boolean);
|
|
1496
|
+
return Array.from(new Set(blocks)).join("\n\n---\n\n");
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
function buildWorkspaceSkillManifestBlock(skills, selectedKeys = []) {
|
|
1500
|
+
const normalizedKeys = Array.from(new Set((selectedKeys || []).map((x) => String(x || "").trim()).filter(Boolean)));
|
|
1501
|
+
const rows = (Array.isArray(skills) ? skills : []).map((skill) => {
|
|
1502
|
+
const id = String(skill?.id || "").trim();
|
|
1503
|
+
const absPath = String(skill?.absPath || "").trim();
|
|
1504
|
+
if (!id && !absPath) return "";
|
|
1505
|
+
return `- \`${id || path.basename(absPath)}\`${absPath ? `: ${absPath}` : ""}`;
|
|
1506
|
+
}).filter(Boolean);
|
|
1507
|
+
if (!rows.length && !normalizedKeys.length) return "";
|
|
1508
|
+
return [
|
|
1509
|
+
"### Workspace Skills Manifest",
|
|
1510
|
+
"",
|
|
1511
|
+
"这些 skills 已在当前 workspace 中可用。不要默认展开或复述其内容;仅当节点任务明确需要时,按路径 Read 对应 SKILL.md。",
|
|
1512
|
+
"",
|
|
1513
|
+
...(
|
|
1514
|
+
rows.length
|
|
1515
|
+
? rows
|
|
1516
|
+
: normalizedKeys.map((key) => `- \`${key}\``)
|
|
1517
|
+
),
|
|
1518
|
+
].join("\n");
|
|
816
1519
|
}
|
|
817
1520
|
|
|
818
1521
|
function workspaceWriteDisplayContent(instance, content) {
|
|
819
1522
|
const next = { ...(instance || {}) };
|
|
820
|
-
const text = String(content || "");
|
|
821
1523
|
const kind = workspaceDisplayKind(next.definitionId);
|
|
1524
|
+
const text = kind === "html" ? normalizeHtmlDisplayContent(content) : String(content || "");
|
|
822
1525
|
const primaryName = kind === "image" ? "src" : "content";
|
|
823
1526
|
next.body = text;
|
|
824
1527
|
next.input = (Array.isArray(next.input) ? next.input : []).map((slot) => (
|
|
@@ -853,11 +1556,13 @@ function workspaceNodePrompt(graph, nodeId, upstreamText, skillsBlock) {
|
|
|
853
1556
|
const instance = graph.instances[nodeId] || {};
|
|
854
1557
|
const body = String(instance.body || "").trim();
|
|
855
1558
|
const label = String(instance.label || nodeId).trim();
|
|
1559
|
+
const downstreamRequirements = workspaceDownstreamDisplayRequirements(graph, nodeId);
|
|
856
1560
|
return [
|
|
857
1561
|
"你正在执行 AgentFlow Workspace 画布中的一个临时节点。",
|
|
858
1562
|
"只输出该节点要传给下游展示/后续节点的正文,不要解释运行过程。",
|
|
859
|
-
skillsBlock ? `\n##
|
|
1563
|
+
skillsBlock ? `\n## Available Skills\n\n${skillsBlock}` : "",
|
|
860
1564
|
upstreamText ? `\n## 上游上下文\n\n${upstreamText}` : "",
|
|
1565
|
+
downstreamRequirements ? `\n${downstreamRequirements}` : "",
|
|
861
1566
|
`\n## 当前节点\n\n- id: ${nodeId}\n- label: ${label}\n- definitionId: ${instance.definitionId || ""}`,
|
|
862
1567
|
`\n## 节点任务\n\n${body || upstreamText}`,
|
|
863
1568
|
].filter(Boolean).join("\n");
|
|
@@ -879,7 +1584,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
879
1584
|
? loadResourcesForSkillKeys(normalized, PACKAGE_ROOT, scopedRoot)
|
|
880
1585
|
: { skills: [], references: [] };
|
|
881
1586
|
const block = normalized.length > 0
|
|
882
|
-
?
|
|
1587
|
+
? buildWorkspaceSkillManifestBlock(selectedSkillResources.skills, normalized)
|
|
883
1588
|
: "";
|
|
884
1589
|
skillsBlockCache.set(cacheKey, block);
|
|
885
1590
|
return block;
|
|
@@ -890,6 +1595,10 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
890
1595
|
events.push(event);
|
|
891
1596
|
if (typeof opts.onEvent === "function") opts.onEvent(event);
|
|
892
1597
|
};
|
|
1598
|
+
const emitTiming = (nodeId, label, startedAt, extra = {}) => {
|
|
1599
|
+
const elapsedMs = Math.max(0, Date.now() - startedAt);
|
|
1600
|
+
emit({ type: "status", nodeId, line: `Timing ${label}: ${elapsedMs}ms`, timing: { label, elapsedMs, ...extra } });
|
|
1601
|
+
};
|
|
893
1602
|
let cwd = scopedRoot;
|
|
894
1603
|
const modelKey = typeof payload?.model === "string" ? payload.model.trim() : "";
|
|
895
1604
|
|
|
@@ -904,9 +1613,11 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
904
1613
|
}
|
|
905
1614
|
|
|
906
1615
|
if (defId === "control_load_skills") {
|
|
1616
|
+
const skillStartedAt = Date.now();
|
|
907
1617
|
const nodeSkillKeys = selectedSkillKeysFromInstance(instance);
|
|
908
1618
|
const activeSkillKeys = nodeSkillKeys.length > 0 ? nodeSkillKeys : fallbackSelectedSkillKeys;
|
|
909
1619
|
const skillsBlock = loadSkillsBlockForKeys(activeSkillKeys);
|
|
1620
|
+
emitTiming(nodeId, "load-skills", skillStartedAt, { skillCount: activeSkillKeys.length, charCount: skillsBlock.length });
|
|
910
1621
|
graph.instances[nodeId] = {
|
|
911
1622
|
...instance,
|
|
912
1623
|
output: (Array.isArray(instance.output) ? instance.output : []).map((slot) => (
|
|
@@ -1154,18 +1865,24 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
1154
1865
|
continue;
|
|
1155
1866
|
}
|
|
1156
1867
|
|
|
1157
|
-
const
|
|
1868
|
+
const prepareStartedAt = Date.now();
|
|
1869
|
+
const upstreamText = workspaceTaskUpstreamText(graph, nodeId, outputs);
|
|
1158
1870
|
const body = String(instance.body || "").trim();
|
|
1159
1871
|
if (defId === "agent_subAgent" && !body && !String(upstreamText || "").trim()) {
|
|
1160
1872
|
throw new Error(`Workspace node ${nodeId} has no task. Fill the node body or connect upstream text.`);
|
|
1161
1873
|
}
|
|
1162
1874
|
const upstreamSkillBlocks = workspaceUpstreamSkillBlocks(graph, nodeId, outputs);
|
|
1163
|
-
const
|
|
1875
|
+
const promptSkillsBlock = mergeWorkspaceSkillBlocks(upstreamSkillBlocks, upstreamSkillBlocks ? "" : loadSkillsBlockForKeys(fallbackSelectedSkillKeys));
|
|
1876
|
+
const prompt = workspaceNodePrompt(graph, nodeId, upstreamText, promptSkillsBlock);
|
|
1877
|
+
emitTiming(nodeId, "prepare-agent-prompt", prepareStartedAt, { promptChars: prompt.length, upstreamChars: String(upstreamText || "").length, skillsChars: promptSkillsBlock.length });
|
|
1878
|
+
emit({ type: "natural", kind: "prompt", nodeId, text: prompt });
|
|
1164
1879
|
let content = "";
|
|
1165
1880
|
const maxAttempts = 3;
|
|
1166
1881
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
1167
1882
|
let attemptContent = "";
|
|
1168
1883
|
try {
|
|
1884
|
+
const spawnStartedAt = Date.now();
|
|
1885
|
+
let firstAgentEventSeen = false;
|
|
1169
1886
|
const handle = startComposerAgent({
|
|
1170
1887
|
uiWorkspaceRoot: scopedRoot,
|
|
1171
1888
|
cliWorkspace: cwd,
|
|
@@ -1173,14 +1890,22 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
1173
1890
|
modelKey,
|
|
1174
1891
|
agentflowUserId: userCtx.userId || "",
|
|
1175
1892
|
onStreamEvent: (ev) => {
|
|
1893
|
+
if (!firstAgentEventSeen) {
|
|
1894
|
+
firstAgentEventSeen = true;
|
|
1895
|
+
emitTiming(nodeId, "agent-first-event", spawnStartedAt, { attempt, firstType: ev?.type || "" });
|
|
1896
|
+
}
|
|
1176
1897
|
emit({ ...ev, nodeId });
|
|
1177
1898
|
if (ev?.type === "natural" && ev.kind === "assistant" && typeof ev.text === "string") {
|
|
1178
1899
|
attemptContent += (attemptContent ? "\n" : "") + ev.text;
|
|
1179
|
-
const updatedDisplays = workspaceUpdateDirectDisplays(graph, nodeId, attemptContent);
|
|
1180
|
-
if (updatedDisplays.length) emit({ type: "graph", nodeId, displayNodeIds: updatedDisplays, graph });
|
|
1181
1900
|
}
|
|
1182
1901
|
},
|
|
1902
|
+
onToolCall: (subtype, toolName) => {
|
|
1903
|
+
const sub = subtype ? String(subtype) : "";
|
|
1904
|
+
const tool = toolName ? String(toolName) : "";
|
|
1905
|
+
emit({ type: "status", nodeId, line: `工具 ${tool || "thinking"}${sub ? ` (${sub})` : ""}` });
|
|
1906
|
+
},
|
|
1183
1907
|
});
|
|
1908
|
+
emitTiming(nodeId, "spawn-agent", spawnStartedAt, { attempt });
|
|
1184
1909
|
await handle.finished;
|
|
1185
1910
|
content = attemptContent.trim();
|
|
1186
1911
|
break;
|
|
@@ -2091,6 +2816,61 @@ export function startUiServer({
|
|
|
2091
2816
|
return;
|
|
2092
2817
|
}
|
|
2093
2818
|
|
|
2819
|
+
if (req.method === "POST" && url.pathname === "/api/workspace/node-chat") {
|
|
2820
|
+
let payload;
|
|
2821
|
+
try {
|
|
2822
|
+
payload = JSON.parse(await readBody(req));
|
|
2823
|
+
} catch {
|
|
2824
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
2825
|
+
return;
|
|
2826
|
+
}
|
|
2827
|
+
const message = String(payload?.message || "").trim();
|
|
2828
|
+
if (!message) {
|
|
2829
|
+
json(res, 400, { error: "Missing message" });
|
|
2830
|
+
return;
|
|
2831
|
+
}
|
|
2832
|
+
try {
|
|
2833
|
+
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
2834
|
+
flowId: payload.flowId || "",
|
|
2835
|
+
flowSource: payload.flowSource || "user",
|
|
2836
|
+
archived: payload.archived === true || payload.flowArchived === true,
|
|
2837
|
+
}, userCtx);
|
|
2838
|
+
if (scoped.error) {
|
|
2839
|
+
json(res, 400, { error: scoped.error });
|
|
2840
|
+
return;
|
|
2841
|
+
}
|
|
2842
|
+
const promptText = buildWorkspaceNodeChatPrompt(payload);
|
|
2843
|
+
const modelKey = typeof payload?.model === "string" ? payload.model.trim() : "";
|
|
2844
|
+
let content = "";
|
|
2845
|
+
const events = [];
|
|
2846
|
+
const handle = startComposerAgent({
|
|
2847
|
+
uiWorkspaceRoot: scoped.root,
|
|
2848
|
+
cliWorkspace: scoped.root,
|
|
2849
|
+
prompt: promptText,
|
|
2850
|
+
modelKey,
|
|
2851
|
+
agentflowUserId: userCtx.userId || "",
|
|
2852
|
+
onStreamEvent: (ev) => {
|
|
2853
|
+
events.push(ev);
|
|
2854
|
+
if (ev?.type === "natural" && ev.kind === "assistant" && typeof ev.text === "string") {
|
|
2855
|
+
content += (content ? "\n" : "") + ev.text;
|
|
2856
|
+
}
|
|
2857
|
+
},
|
|
2858
|
+
});
|
|
2859
|
+
await handle.finished;
|
|
2860
|
+
const candidateContent = content.trim();
|
|
2861
|
+
json(res, 200, {
|
|
2862
|
+
ok: true,
|
|
2863
|
+
sessionId: String(payload?.sessionId || "") || `nodechat_${Date.now()}`,
|
|
2864
|
+
reply: candidateContent,
|
|
2865
|
+
candidateContent,
|
|
2866
|
+
events,
|
|
2867
|
+
});
|
|
2868
|
+
} catch (e) {
|
|
2869
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
2870
|
+
}
|
|
2871
|
+
return;
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2094
2874
|
if (req.method === "GET" && url.pathname === "/api/pipeline-files") {
|
|
2095
2875
|
const flowId = url.searchParams.get("flowId");
|
|
2096
2876
|
const flowSource = url.searchParams.get("flowSource") || "user";
|
|
@@ -2307,6 +3087,64 @@ export function startUiServer({
|
|
|
2307
3087
|
return;
|
|
2308
3088
|
}
|
|
2309
3089
|
|
|
3090
|
+
if (req.method === "GET" && url.pathname === "/api/mcps") {
|
|
3091
|
+
try {
|
|
3092
|
+
json(res, 200, readCursorMcpServers(userCtx));
|
|
3093
|
+
} catch (e) {
|
|
3094
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
3095
|
+
}
|
|
3096
|
+
return;
|
|
3097
|
+
}
|
|
3098
|
+
|
|
3099
|
+
if (req.method === "POST" && url.pathname === "/api/mcps") {
|
|
3100
|
+
let payload;
|
|
3101
|
+
try {
|
|
3102
|
+
payload = JSON.parse(await readBody(req));
|
|
3103
|
+
} catch {
|
|
3104
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
3105
|
+
return;
|
|
3106
|
+
}
|
|
3107
|
+
try {
|
|
3108
|
+
json(res, 200, writeCursorMcpServer(payload, userCtx));
|
|
3109
|
+
} catch (e) {
|
|
3110
|
+
json(res, 400, { error: (e && e.message) || String(e) });
|
|
3111
|
+
}
|
|
3112
|
+
return;
|
|
3113
|
+
}
|
|
3114
|
+
|
|
3115
|
+
if (req.method === "POST" && url.pathname === "/api/mcps/delete") {
|
|
3116
|
+
let payload;
|
|
3117
|
+
try {
|
|
3118
|
+
payload = JSON.parse(await readBody(req));
|
|
3119
|
+
} catch {
|
|
3120
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
3121
|
+
return;
|
|
3122
|
+
}
|
|
3123
|
+
try {
|
|
3124
|
+
json(res, 200, deleteCursorMcpServer(payload?.name, userCtx));
|
|
3125
|
+
} catch (e) {
|
|
3126
|
+
json(res, 400, { error: (e && e.message) || String(e) });
|
|
3127
|
+
}
|
|
3128
|
+
return;
|
|
3129
|
+
}
|
|
3130
|
+
|
|
3131
|
+
if (req.method === "POST" && url.pathname === "/api/mcps/check") {
|
|
3132
|
+
let payload;
|
|
3133
|
+
try {
|
|
3134
|
+
const raw = await readBody(req);
|
|
3135
|
+
payload = raw && String(raw).trim() ? JSON.parse(raw) : {};
|
|
3136
|
+
} catch {
|
|
3137
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
3138
|
+
return;
|
|
3139
|
+
}
|
|
3140
|
+
try {
|
|
3141
|
+
json(res, 200, await checkCursorMcpServers(payload?.name || "", userCtx));
|
|
3142
|
+
} catch (e) {
|
|
3143
|
+
json(res, 400, { error: (e && e.message) || String(e) });
|
|
3144
|
+
}
|
|
3145
|
+
return;
|
|
3146
|
+
}
|
|
3147
|
+
|
|
2310
3148
|
if (req.method === "GET" && url.pathname === "/api/user-env") {
|
|
2311
3149
|
try {
|
|
2312
3150
|
json(res, 200, { env: readUserEnvRows(userCtx.userId) });
|
|
@@ -2384,16 +3222,41 @@ export function startUiServer({
|
|
|
2384
3222
|
|
|
2385
3223
|
if (req.method === "GET" && url.pathname === "/api/skillhub/search") {
|
|
2386
3224
|
const q = (url.searchParams.get("q") || "").trim();
|
|
3225
|
+
const mode = (url.searchParams.get("mode") || "keyword").trim();
|
|
2387
3226
|
if (!q) {
|
|
2388
3227
|
json(res, 200, { total: 0, items: [] });
|
|
2389
3228
|
return;
|
|
2390
3229
|
}
|
|
3230
|
+
if (mode === "collectionId") {
|
|
3231
|
+
const info = await fetchSkillhubCollectionInfo(q);
|
|
3232
|
+
json(res, 200, {
|
|
3233
|
+
total: 1,
|
|
3234
|
+
mode,
|
|
3235
|
+
items: [info || {
|
|
3236
|
+
id: `collection:${q}`,
|
|
3237
|
+
collection: q,
|
|
3238
|
+
kind: "collection",
|
|
3239
|
+
slug: "",
|
|
3240
|
+
name: `Collection ${q}`,
|
|
3241
|
+
summary: "按 Collection ID 安装该合集中的全部 Skills。",
|
|
3242
|
+
version: "",
|
|
3243
|
+
tags: [],
|
|
3244
|
+
}],
|
|
3245
|
+
});
|
|
3246
|
+
return;
|
|
3247
|
+
}
|
|
2391
3248
|
const result = await runSkillhub(["search", "-q", q], { cwd: root });
|
|
2392
3249
|
if (!result.ok) {
|
|
2393
3250
|
json(res, 500, { error: result.error, stdout: result.stdout });
|
|
2394
3251
|
return;
|
|
2395
3252
|
}
|
|
2396
|
-
|
|
3253
|
+
const payload = normalizeSkillhubSearchPayload(parseJsonText(result.stdout, {}));
|
|
3254
|
+
if (mode === "skillId") {
|
|
3255
|
+
const filtered = payload.items.filter((item) => item.skillId === q || item.id === q);
|
|
3256
|
+
json(res, 200, { ...payload, mode, total: filtered.length, items: filtered });
|
|
3257
|
+
return;
|
|
3258
|
+
}
|
|
3259
|
+
json(res, 200, { ...payload, mode });
|
|
2397
3260
|
return;
|
|
2398
3261
|
}
|
|
2399
3262
|
|
|
@@ -2410,12 +3273,19 @@ export function startUiServer({
|
|
|
2410
3273
|
json(res, 400, { error: "Missing skill slug or collection" });
|
|
2411
3274
|
return;
|
|
2412
3275
|
}
|
|
3276
|
+
const beforeSkills = payload?.collection ? listComposerSkills(PACKAGE_ROOT, root) : [];
|
|
2413
3277
|
const result = await runSkillhub(args, { cwd: root, timeoutMs: 180_000, maxBuffer: 4 * 1024 * 1024 });
|
|
2414
3278
|
if (!result.ok) {
|
|
2415
3279
|
json(res, 500, { error: result.error, stdout: result.stdout });
|
|
2416
3280
|
return;
|
|
2417
3281
|
}
|
|
2418
|
-
|
|
3282
|
+
let skillCollections = null;
|
|
3283
|
+
if (payload?.collection) {
|
|
3284
|
+
const afterSkills = listComposerSkills(PACKAGE_ROOT, root);
|
|
3285
|
+
const collectionName = String(payload.collectionName || payload.name || "").trim();
|
|
3286
|
+
skillCollections = upsertSkillhubCollectionGroup(userCtx, payload.collection, beforeSkills, afterSkills, collectionName);
|
|
3287
|
+
}
|
|
3288
|
+
json(res, 200, { ok: true, stdout: result.stdout, skillCollections });
|
|
2419
3289
|
return;
|
|
2420
3290
|
}
|
|
2421
3291
|
|
|
@@ -2437,7 +3307,8 @@ export function startUiServer({
|
|
|
2437
3307
|
json(res, 500, { error: result.error, stdout: result.stdout });
|
|
2438
3308
|
return;
|
|
2439
3309
|
}
|
|
2440
|
-
|
|
3310
|
+
const skillCollections = payload?.collection ? removeSkillhubCollectionGroup(userCtx, payload.collection, root) : null;
|
|
3311
|
+
json(res, 200, { ok: true, stdout: result.stdout, skillCollections });
|
|
2441
3312
|
return;
|
|
2442
3313
|
}
|
|
2443
3314
|
|