@fieldwangai/agentflow 0.1.30 → 0.1.32
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 +26 -2
- package/bin/lib/api-runner.mjs +26 -3
- package/bin/lib/apply.mjs +6 -5
- package/bin/lib/catalog-flows.mjs +30 -5
- package/bin/lib/composer-agent.mjs +2 -1
- package/bin/lib/locales/en.json +4 -0
- package/bin/lib/locales/zh.json +6 -2
- package/bin/lib/marketplace.mjs +124 -2
- package/bin/lib/node-execute.mjs +1 -1
- package/bin/lib/paths.mjs +5 -0
- package/bin/lib/scheduler.mjs +3 -2
- package/bin/lib/ui-server.mjs +639 -8
- package/bin/lib/user-env.mjs +83 -0
- package/bin/pipeline/get-env.mjs +5 -29
- package/bin/pipeline/pre-process-node.mjs +28 -6
- package/bin/pipeline/run-tool-nodejs.mjs +7 -0
- package/builtin/nodes/agent_subAgent.md +6 -3
- package/builtin/nodes/control_cd_workspace.md +8 -6
- package/builtin/nodes/control_load_skills.md +2 -0
- package/builtin/nodes/control_user_workspace.md +20 -0
- package/builtin/nodes/display_ascii.md +5 -0
- package/builtin/nodes/display_markdown.md +5 -0
- package/builtin/nodes/display_mermaid.md +5 -0
- package/builtin/nodes/tool_git_checkout.md +3 -0
- package/builtin/web-ui/dist/assets/index-D0Tkhqr6.css +1 -0
- package/builtin/web-ui/dist/assets/index-DyhW5chp.js +197 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/skills/agentflow-workspace-ascii/SKILL.md +42 -0
- package/skills/agentflow-workspace-graph/SKILL.md +67 -0
- package/skills/agentflow-workspace-markdown/SKILL.md +44 -0
- package/skills/agentflow-workspace-mermaid/SKILL.md +43 -0
- package/builtin/web-ui/dist/assets/index-NdVOJLL9.js +0 -196
- package/builtin/web-ui/dist/assets/index-naVI6LZj.css +0 -1
package/bin/lib/ui-server.mjs
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import fs from "fs";
|
|
9
9
|
import http from "http";
|
|
10
|
+
import os from "os";
|
|
10
11
|
import path from "path";
|
|
11
12
|
import { execFile, spawn } from "child_process";
|
|
12
13
|
import busboy from "busboy";
|
|
@@ -40,6 +41,7 @@ import { t } from "./i18n.mjs";
|
|
|
40
41
|
import {
|
|
41
42
|
PACKAGE_ROOT,
|
|
42
43
|
getAgentflowUserConfigAbs,
|
|
44
|
+
getAgentflowUserDataRoot,
|
|
43
45
|
getModelListsAbs,
|
|
44
46
|
getRunDir,
|
|
45
47
|
} from "./paths.mjs";
|
|
@@ -72,7 +74,7 @@ import {
|
|
|
72
74
|
import { runNodeScript } from "./pipeline-scripts.mjs";
|
|
73
75
|
import { readFlowSchedule, writeFlowSchedule } from "./schedule-config.mjs";
|
|
74
76
|
import { listScheduleStatuses } from "./scheduler.mjs";
|
|
75
|
-
import { installFlowDependency, listMarketplacePackages, publishNodeFromInstance } from "./marketplace.mjs";
|
|
77
|
+
import { deleteMarketplaceNodePackage, installFlowDependency, listMarketplacePackages, publishNodeFromInstance } from "./marketplace.mjs";
|
|
76
78
|
import {
|
|
77
79
|
authSetupRequired,
|
|
78
80
|
buildClearSessionCookie,
|
|
@@ -81,6 +83,7 @@ import {
|
|
|
81
83
|
loginOrCreateUser,
|
|
82
84
|
logoutRequest,
|
|
83
85
|
} from "./auth.mjs";
|
|
86
|
+
import { readUserEnvObject, readUserEnvRows, writeUserEnvRows } from "./user-env.mjs";
|
|
84
87
|
|
|
85
88
|
const MIME = {
|
|
86
89
|
".html": "text/html; charset=utf-8",
|
|
@@ -92,6 +95,49 @@ const MIME = {
|
|
|
92
95
|
};
|
|
93
96
|
|
|
94
97
|
const RUN_CONFIG_FILENAME = "run-config.json";
|
|
98
|
+
const SKILL_COLLECTIONS_FILENAME = "skill-collections.json";
|
|
99
|
+
const BUILTIN_SKILL_COLLECTIONS = [
|
|
100
|
+
{
|
|
101
|
+
id: "pipeline",
|
|
102
|
+
name: "Pipeline",
|
|
103
|
+
defaultKeys: [
|
|
104
|
+
"agentflow-flow-add-instances",
|
|
105
|
+
"agentflow-flow-edit-node-fields",
|
|
106
|
+
"agentflow-flow-recipes",
|
|
107
|
+
"agentflow-flow-sync-ui",
|
|
108
|
+
"agentflow-node-reference",
|
|
109
|
+
"agentflow-placeholder-reference",
|
|
110
|
+
"agentflow-runtime-reference",
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: "workspace",
|
|
115
|
+
name: "Workspace",
|
|
116
|
+
defaultKeys: [
|
|
117
|
+
"agentflow-workspace-graph",
|
|
118
|
+
"agentflow-workspace-markdown",
|
|
119
|
+
"agentflow-workspace-mermaid",
|
|
120
|
+
"agentflow-workspace-ascii",
|
|
121
|
+
"agentflow-node-reference",
|
|
122
|
+
"agentflow-placeholder-reference",
|
|
123
|
+
"agentflow-runtime-reference",
|
|
124
|
+
],
|
|
125
|
+
legacyDefaultKeys: [
|
|
126
|
+
[
|
|
127
|
+
"agentflow-flow-add-instances",
|
|
128
|
+
"agentflow-flow-edit-node-fields",
|
|
129
|
+
"agentflow-node-reference",
|
|
130
|
+
"agentflow-placeholder-reference",
|
|
131
|
+
"agentflow-runtime-reference",
|
|
132
|
+
],
|
|
133
|
+
[
|
|
134
|
+
"agentflow-node-reference",
|
|
135
|
+
"agentflow-placeholder-reference",
|
|
136
|
+
"agentflow-runtime-reference",
|
|
137
|
+
],
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
];
|
|
95
141
|
|
|
96
142
|
function json(res, status, obj) {
|
|
97
143
|
const body = JSON.stringify(obj);
|
|
@@ -102,6 +148,155 @@ function json(res, status, obj) {
|
|
|
102
148
|
res.end(body);
|
|
103
149
|
}
|
|
104
150
|
|
|
151
|
+
function skillCollectionsAbs(userCtx = {}) {
|
|
152
|
+
return path.join(getAgentflowUserDataRoot(userCtx.userId), SKILL_COLLECTIONS_FILENAME);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function slugifySkillCollectionId(name, fallback = "collection") {
|
|
156
|
+
const raw = String(name || "").trim().toLowerCase();
|
|
157
|
+
const id = raw
|
|
158
|
+
.replace(/[^a-z0-9_-]+/g, "-")
|
|
159
|
+
.replace(/^-+|-+$/g, "")
|
|
160
|
+
.slice(0, 48);
|
|
161
|
+
return id || fallback;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function isBuiltinSkillCollectionId(id) {
|
|
165
|
+
return BUILTIN_SKILL_COLLECTIONS.some((collection) => collection.id === id);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function resolveSkillKeys(skillNamesOrKeys = [], availableSkills = []) {
|
|
169
|
+
const byToken = buildSkillKeyLookup(availableSkills);
|
|
170
|
+
return skillNamesOrKeys.map((key) => byToken.get(key)).filter(Boolean);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function buildSkillKeyLookup(availableSkills = []) {
|
|
174
|
+
const byToken = new Map();
|
|
175
|
+
for (const skill of availableSkills) {
|
|
176
|
+
const key = String(skill?.key || "").trim();
|
|
177
|
+
if (!key) continue;
|
|
178
|
+
for (const token of [skill.key, skill.name, skill.id]) {
|
|
179
|
+
const normalized = String(token || "").trim();
|
|
180
|
+
if (normalized && !byToken.has(normalized)) byToken.set(normalized, key);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return byToken;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function defaultSkillKeysForCollection(def, availableSkills = []) {
|
|
187
|
+
const exact = resolveSkillKeys(def.defaultKeys, availableSkills);
|
|
188
|
+
if (exact.length > 0) return exact;
|
|
189
|
+
return availableSkills
|
|
190
|
+
.filter((skill) => String(skill?.name || skill?.id || skill?.key || "").includes("agentflow-"))
|
|
191
|
+
.map((skill) => String(skill.key || "").trim())
|
|
192
|
+
.filter(Boolean);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function sameSkillKeySet(a = [], b = []) {
|
|
196
|
+
if (a.length !== b.length) return false;
|
|
197
|
+
const set = new Set(a);
|
|
198
|
+
return b.every((key) => set.has(key));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function normalizeSkillCollectionConfig(value) {
|
|
202
|
+
const now = Date.now();
|
|
203
|
+
const seenIds = new Set();
|
|
204
|
+
const collections = [];
|
|
205
|
+
const input = Array.isArray(value?.collections) ? value.collections : [];
|
|
206
|
+
for (const item of input) {
|
|
207
|
+
if (!item || typeof item !== "object") continue;
|
|
208
|
+
const name = String(item.name || item.id || "").trim().slice(0, 80);
|
|
209
|
+
if (!name) continue;
|
|
210
|
+
let id = slugifySkillCollectionId(item.id || name);
|
|
211
|
+
let suffix = 2;
|
|
212
|
+
while (seenIds.has(id)) {
|
|
213
|
+
id = `${slugifySkillCollectionId(item.id || name)}-${suffix++}`;
|
|
214
|
+
}
|
|
215
|
+
seenIds.add(id);
|
|
216
|
+
const skillSeen = new Set();
|
|
217
|
+
const skillKeys = [];
|
|
218
|
+
for (const key of Array.isArray(item.skillKeys) ? item.skillKeys : []) {
|
|
219
|
+
const normalized = String(key || "").trim();
|
|
220
|
+
if (!normalized || skillSeen.has(normalized)) continue;
|
|
221
|
+
skillSeen.add(normalized);
|
|
222
|
+
skillKeys.push(normalized);
|
|
223
|
+
}
|
|
224
|
+
collections.push({
|
|
225
|
+
id,
|
|
226
|
+
name,
|
|
227
|
+
skillKeys,
|
|
228
|
+
builtin: Boolean(item.builtin) || isBuiltinSkillCollectionId(id),
|
|
229
|
+
createdAt: Number.isFinite(item.createdAt) ? Number(item.createdAt) : now,
|
|
230
|
+
updatedAt: Number.isFinite(item.updatedAt) ? Number(item.updatedAt) : now,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
return { version: 1, collections };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function withBuiltinSkillCollections(config, availableSkills = []) {
|
|
237
|
+
const normalized = normalizeSkillCollectionConfig(config);
|
|
238
|
+
const byId = new Map(normalized.collections.map((collection) => [collection.id, collection]));
|
|
239
|
+
const out = [];
|
|
240
|
+
const now = Date.now();
|
|
241
|
+
for (const def of BUILTIN_SKILL_COLLECTIONS) {
|
|
242
|
+
const existing = byId.get(def.id);
|
|
243
|
+
if (existing) {
|
|
244
|
+
const nextDefaultKeys = defaultSkillKeysForCollection(def, availableSkills);
|
|
245
|
+
const legacyDefaultSets = (Array.isArray(def.legacyDefaultKeys) ? def.legacyDefaultKeys : [])
|
|
246
|
+
.map((keys) => Array.isArray(keys) ? resolveSkillKeys(keys, availableSkills) : [])
|
|
247
|
+
.filter((keys) => keys.length > 0);
|
|
248
|
+
const shouldMigrateLegacyDefault =
|
|
249
|
+
existing.skillKeys.length > 0 &&
|
|
250
|
+
legacyDefaultSets.some((keys) => sameSkillKeySet(existing.skillKeys, keys));
|
|
251
|
+
out.push({
|
|
252
|
+
...existing,
|
|
253
|
+
name: def.name,
|
|
254
|
+
builtin: true,
|
|
255
|
+
skillKeys: existing.skillKeys.length > 0 && !shouldMigrateLegacyDefault ? existing.skillKeys : nextDefaultKeys,
|
|
256
|
+
});
|
|
257
|
+
byId.delete(def.id);
|
|
258
|
+
} else {
|
|
259
|
+
out.push({
|
|
260
|
+
id: def.id,
|
|
261
|
+
name: def.name,
|
|
262
|
+
builtin: true,
|
|
263
|
+
skillKeys: defaultSkillKeysForCollection(def, availableSkills),
|
|
264
|
+
createdAt: now,
|
|
265
|
+
updatedAt: now,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
out.push(...Array.from(byId.values()).map((collection) => ({ ...collection, builtin: false })));
|
|
270
|
+
return { version: 1, collections: out };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function readSkillCollectionConfig(userCtx = {}, availableSkills = []) {
|
|
274
|
+
const p = skillCollectionsAbs(userCtx);
|
|
275
|
+
try {
|
|
276
|
+
if (!fs.existsSync(p)) return withBuiltinSkillCollections({}, availableSkills);
|
|
277
|
+
return withBuiltinSkillCollections(JSON.parse(fs.readFileSync(p, "utf-8")), availableSkills);
|
|
278
|
+
} catch {
|
|
279
|
+
return withBuiltinSkillCollections({}, availableSkills);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function writeSkillCollectionConfig(userCtx = {}, payload = {}, availableSkills = []) {
|
|
284
|
+
const p = skillCollectionsAbs(userCtx);
|
|
285
|
+
const config = withBuiltinSkillCollections(payload, availableSkills);
|
|
286
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
287
|
+
fs.writeFileSync(p, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
288
|
+
return config;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function runtimeEnvForUser(userCtx = {}, extra = {}) {
|
|
292
|
+
return {
|
|
293
|
+
...process.env,
|
|
294
|
+
...readUserEnvObject(userCtx.userId),
|
|
295
|
+
...extra,
|
|
296
|
+
AGENTFLOW_USER_ID: userCtx.userId || "",
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
105
300
|
function readAgentflowUserConfigObject() {
|
|
106
301
|
const p = getAgentflowUserConfigAbs();
|
|
107
302
|
try {
|
|
@@ -427,25 +622,344 @@ function buildWorkspaceGeneratePrompt(payload) {
|
|
|
427
622
|
"使用 +-|/\\<> 等字符表达结构,尽量保持对齐。",
|
|
428
623
|
].join("\n")
|
|
429
624
|
: [
|
|
430
|
-
"你是
|
|
431
|
-
"
|
|
432
|
-
"
|
|
625
|
+
"你是 AgentFlow Workspace Composer。",
|
|
626
|
+
"优先根据已选择的 Skills 操作 workspace.graph.json,创建或修改 workspace 画布节点、连线与展示节点。",
|
|
627
|
+
"如果用户请求需要项目分析、加载代码、整理流程或生成展示结果,不要只给泛泛回答;应先让 Skills 驱动画布建模,例如创建 Git/工作目录/Load Skills/Agent/Markdown Display 等合适节点。",
|
|
628
|
+
"只有当用户明确只是询问概念或无需画布变更时,才直接输出 Markdown 回复。",
|
|
433
629
|
].join("\n");
|
|
434
630
|
return [
|
|
435
631
|
"你正在 AgentFlow 的 Workspace 工作画布中执行任务。",
|
|
436
632
|
"Workspace 是当前 pipeline 的临时工作区,用于分析、试验、生成中间文件和展示结果。",
|
|
633
|
+
"Workspace 与 Pipeline 各自有独立的 Skill collection;此处只使用当前 Workspace Composer 选择的 collections / skills 作为本次行为规则与编辑依据。",
|
|
634
|
+
"当 Skills 提到修改 flow.yaml / instances / edges / ui 时,在 Workspace 视图下应映射为修改当前工作区的 workspace.graph.json,除非用户显式勾选并要求修改正式 flow.yaml。",
|
|
635
|
+
"workspace.graph.json 使用 JSON:{ version, instances, edges, ui: { nodePositions, nodeSizes } }。instances 的结构与 flow.yaml instances 一致;edges 使用 source/target/sourceHandle/targetHandle;ui.nodePositions 记录节点坐标。",
|
|
437
636
|
allowFlowYaml
|
|
438
637
|
? "用户已允许你考虑正式 flow.yaml;如需修改仍必须明确说明影响。"
|
|
439
638
|
: "默认不要修改正式 flow.yaml;优先在 workspace 文件、workspace.graph.json 或回复内容中完成任务。",
|
|
440
639
|
workspaceGraph ? `\n## 当前 workspace graph\n\n${JSON.stringify(workspaceGraph, null, 2)}` : "",
|
|
441
640
|
selectedNodeIds.length > 0 ? `\n## 当前用户选中的 workspace 节点\n\n${selectedNodeIds.map((id) => `- ${id}`).join("\n")}` : "",
|
|
442
|
-
skillsBlock ? `\n##
|
|
641
|
+
skillsBlock ? `\n## Selected Skills\n\n${skillsBlock}` : "",
|
|
443
642
|
kindInstruction,
|
|
444
643
|
contextBlocks ? `\n## 上下文\n\n${contextBlocks}` : "",
|
|
445
644
|
`\n## 用户 prompt\n\n${userPrompt}`,
|
|
446
645
|
].filter(Boolean).join("\n");
|
|
447
646
|
}
|
|
448
647
|
|
|
648
|
+
function workspaceSlotValue(slot) {
|
|
649
|
+
if (!slot || typeof slot !== "object") return "";
|
|
650
|
+
for (const key of ["value", "default"]) {
|
|
651
|
+
if (slot[key] != null && String(slot[key]).trim()) return String(slot[key]);
|
|
652
|
+
}
|
|
653
|
+
return "";
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function workspaceInstanceText(instance) {
|
|
657
|
+
const body = String(instance?.body || "").trim();
|
|
658
|
+
if (body) return body;
|
|
659
|
+
const slots = [...(Array.isArray(instance?.input) ? instance.input : []), ...(Array.isArray(instance?.output) ? instance.output : [])];
|
|
660
|
+
const textSlot = slots.find((slot) => String(slot?.type || "") === "text" && workspaceSlotValue(slot).trim());
|
|
661
|
+
return textSlot ? workspaceSlotValue(textSlot) : "";
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function workspaceDisplayKind(definitionId) {
|
|
665
|
+
const id = String(definitionId || "");
|
|
666
|
+
if (id === "display_markdown") return "markdown";
|
|
667
|
+
if (id === "display_mermaid") return "mermaid";
|
|
668
|
+
if (id === "display_ascii") return "ascii";
|
|
669
|
+
return "";
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function workspaceRunOrder(graph, runNodeId) {
|
|
673
|
+
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
674
|
+
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
675
|
+
const target = String(runNodeId || "").trim();
|
|
676
|
+
if (!target || !instances[target]) throw new Error("Missing workspace run node");
|
|
677
|
+
const upstream = new Map();
|
|
678
|
+
const downstream = new Map();
|
|
679
|
+
for (const edge of edges) {
|
|
680
|
+
const source = String(edge?.source || "");
|
|
681
|
+
const dest = String(edge?.target || "");
|
|
682
|
+
if (!source || !dest || !instances[source] || !instances[dest]) continue;
|
|
683
|
+
if (!upstream.has(dest)) upstream.set(dest, []);
|
|
684
|
+
upstream.get(dest).push(source);
|
|
685
|
+
if (!downstream.has(source)) downstream.set(source, []);
|
|
686
|
+
downstream.get(source).push(dest);
|
|
687
|
+
}
|
|
688
|
+
const reachable = new Set();
|
|
689
|
+
const visit = (id) => {
|
|
690
|
+
if (!id || reachable.has(id)) return;
|
|
691
|
+
reachable.add(id);
|
|
692
|
+
for (const next of downstream.get(id) || []) visit(next);
|
|
693
|
+
};
|
|
694
|
+
visit(target);
|
|
695
|
+
reachable.delete(target);
|
|
696
|
+
const needed = reachable;
|
|
697
|
+
const indegree = new Map(Array.from(needed).map((id) => [id, 0]));
|
|
698
|
+
for (const id of needed) {
|
|
699
|
+
for (const prev of upstream.get(id) || []) {
|
|
700
|
+
if (needed.has(prev)) indegree.set(id, (indegree.get(id) || 0) + 1);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
const ready = Array.from(needed).filter((id) => (indegree.get(id) || 0) === 0);
|
|
704
|
+
const ordered = [];
|
|
705
|
+
while (ready.length) {
|
|
706
|
+
const id = ready.shift();
|
|
707
|
+
ordered.push(id);
|
|
708
|
+
for (const next of downstream.get(id) || []) {
|
|
709
|
+
if (!needed.has(next)) continue;
|
|
710
|
+
const n = (indegree.get(next) || 0) - 1;
|
|
711
|
+
indegree.set(next, n);
|
|
712
|
+
if (n === 0) ready.push(next);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
if (ordered.length !== needed.size) {
|
|
716
|
+
throw new Error("Workspace run graph contains a cycle");
|
|
717
|
+
}
|
|
718
|
+
return ordered;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function workspaceUpstreamText(graph, nodeId, outputs) {
|
|
722
|
+
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
723
|
+
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
724
|
+
const incoming = edges.filter((edge) => String(edge?.target || "") === String(nodeId));
|
|
725
|
+
const contentEdge = incoming.find((edge) => String(edge?.targetHandle || "") === "input-1") || incoming[0];
|
|
726
|
+
if (!contentEdge) return "";
|
|
727
|
+
const sourceId = String(contentEdge.source || "");
|
|
728
|
+
const out = outputs.get(sourceId);
|
|
729
|
+
if (out != null && String(out).trim()) return String(out);
|
|
730
|
+
return workspaceInstanceText(instances[sourceId]);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function parseWorkspaceSkillKeys(raw) {
|
|
734
|
+
const text = String(raw || "").trim();
|
|
735
|
+
if (!text) return [];
|
|
736
|
+
try {
|
|
737
|
+
const parsed = JSON.parse(text);
|
|
738
|
+
if (Array.isArray(parsed)) return parsed.map((item) => String(item || "").trim()).filter(Boolean);
|
|
739
|
+
} catch {
|
|
740
|
+
/* plain list fallback */
|
|
741
|
+
}
|
|
742
|
+
return text.split(/[\n,]+/).map((item) => item.trim()).filter(Boolean);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function selectedSkillKeysFromInstance(instance) {
|
|
746
|
+
const bodyKeys = parseWorkspaceSkillKeys(instance?.body || "");
|
|
747
|
+
if (bodyKeys.length > 0) return bodyKeys;
|
|
748
|
+
const slots = [...(Array.isArray(instance?.input) ? instance.input : []), ...(Array.isArray(instance?.output) ? instance.output : [])];
|
|
749
|
+
const slot = slots.find((item) => item?.name === "skillsContext") || slots.find((item) => item?.name === "skillKeys");
|
|
750
|
+
return parseWorkspaceSkillKeys(workspaceSlotValue(slot) || "");
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function workspaceUpstreamSkillBlocks(graph, nodeId, outputs) {
|
|
754
|
+
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
755
|
+
return edges
|
|
756
|
+
.filter((edge) => String(edge?.target || "") === String(nodeId))
|
|
757
|
+
.map((edge) => String(outputs.get(String(edge.source || "")) || ""))
|
|
758
|
+
.filter((text) => text.includes("##") || text.includes("Skill"))
|
|
759
|
+
.join("\n\n---\n\n");
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function workspaceWriteDisplayContent(instance, content) {
|
|
763
|
+
const next = { ...(instance || {}) };
|
|
764
|
+
const text = String(content || "");
|
|
765
|
+
next.body = text;
|
|
766
|
+
next.input = (Array.isArray(next.input) ? next.input : []).map((slot) => (
|
|
767
|
+
String(slot?.name || "") === "content" || String(slot?.type || "") === "text"
|
|
768
|
+
? { ...slot, default: text, value: text }
|
|
769
|
+
: slot
|
|
770
|
+
));
|
|
771
|
+
next.output = (Array.isArray(next.output) ? next.output : []).map((slot) => (
|
|
772
|
+
String(slot?.name || "") === "content" || String(slot?.type || "") === "text"
|
|
773
|
+
? { ...slot, default: text, value: text }
|
|
774
|
+
: slot
|
|
775
|
+
));
|
|
776
|
+
return next;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function workspaceUpdateDirectDisplays(graph, sourceId, content) {
|
|
780
|
+
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
781
|
+
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
782
|
+
const updated = [];
|
|
783
|
+
for (const edge of edges) {
|
|
784
|
+
if (String(edge?.source || "") !== String(sourceId)) continue;
|
|
785
|
+
const targetId = String(edge?.target || "");
|
|
786
|
+
const target = instances[targetId];
|
|
787
|
+
if (!target || !workspaceDisplayKind(target.definitionId)) continue;
|
|
788
|
+
instances[targetId] = workspaceWriteDisplayContent(target, content);
|
|
789
|
+
updated.push(targetId);
|
|
790
|
+
}
|
|
791
|
+
return updated;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function workspaceNodePrompt(graph, nodeId, upstreamText, skillsBlock) {
|
|
795
|
+
const instance = graph.instances[nodeId] || {};
|
|
796
|
+
const body = String(instance.body || "").trim();
|
|
797
|
+
const label = String(instance.label || nodeId).trim();
|
|
798
|
+
return [
|
|
799
|
+
"你正在执行 AgentFlow Workspace 画布中的一个临时节点。",
|
|
800
|
+
"只输出该节点要传给下游展示/后续节点的正文,不要解释运行过程。",
|
|
801
|
+
skillsBlock ? `\n## Selected Skills\n\n${skillsBlock}` : "",
|
|
802
|
+
upstreamText ? `\n## 上游上下文\n\n${upstreamText}` : "",
|
|
803
|
+
`\n## 当前节点\n\n- id: ${nodeId}\n- label: ${label}\n- definitionId: ${instance.definitionId || ""}`,
|
|
804
|
+
`\n## 节点任务\n\n${body || upstreamText}`,
|
|
805
|
+
].filter(Boolean).join("\n");
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts = {}) {
|
|
809
|
+
const graph = normalizeWorkspaceGraphPayload(payload.graph || {});
|
|
810
|
+
const runNodeId = String(payload?.runNodeId || "").trim();
|
|
811
|
+
const order = workspaceRunOrder(graph, runNodeId);
|
|
812
|
+
const fallbackSelectedSkillKeys = Array.isArray(payload?.selectedSkills)
|
|
813
|
+
? payload.selectedSkills.map((x) => String(x || "").trim()).filter(Boolean)
|
|
814
|
+
: [];
|
|
815
|
+
const skillsBlockCache = new Map();
|
|
816
|
+
const loadSkillsBlockForKeys = (keys) => {
|
|
817
|
+
const normalized = Array.from(new Set((keys || []).map((x) => String(x || "").trim()).filter(Boolean)));
|
|
818
|
+
const cacheKey = normalized.join("\n");
|
|
819
|
+
if (skillsBlockCache.has(cacheKey)) return skillsBlockCache.get(cacheKey);
|
|
820
|
+
const selectedSkillResources = normalized.length > 0
|
|
821
|
+
? loadResourcesForSkillKeys(normalized, PACKAGE_ROOT, scopedRoot)
|
|
822
|
+
: { skills: [], references: [] };
|
|
823
|
+
const block = normalized.length > 0
|
|
824
|
+
? buildSkillCompactInjectionBlock(selectedSkillResources.skills, selectedSkillResources.references)
|
|
825
|
+
: "";
|
|
826
|
+
skillsBlockCache.set(cacheKey, block);
|
|
827
|
+
return block;
|
|
828
|
+
};
|
|
829
|
+
const outputs = new Map();
|
|
830
|
+
const events = [];
|
|
831
|
+
const emit = (event) => {
|
|
832
|
+
events.push(event);
|
|
833
|
+
if (typeof opts.onEvent === "function") opts.onEvent(event);
|
|
834
|
+
};
|
|
835
|
+
let cwd = scopedRoot;
|
|
836
|
+
const modelKey = typeof payload?.model === "string" ? payload.model.trim() : "";
|
|
837
|
+
|
|
838
|
+
for (const nodeId of order) {
|
|
839
|
+
const instance = graph.instances[nodeId];
|
|
840
|
+
if (!instance) continue;
|
|
841
|
+
const defId = String(instance.definitionId || "");
|
|
842
|
+
emit({ type: "node-start", nodeId, definitionId: defId });
|
|
843
|
+
|
|
844
|
+
if (defId === "workspace_run") {
|
|
845
|
+
continue;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
if (defId === "control_load_skills") {
|
|
849
|
+
const nodeSkillKeys = selectedSkillKeysFromInstance(instance);
|
|
850
|
+
const activeSkillKeys = nodeSkillKeys.length > 0 ? nodeSkillKeys : fallbackSelectedSkillKeys;
|
|
851
|
+
const skillsBlock = loadSkillsBlockForKeys(activeSkillKeys);
|
|
852
|
+
graph.instances[nodeId] = {
|
|
853
|
+
...instance,
|
|
854
|
+
output: (Array.isArray(instance.output) ? instance.output : []).map((slot) => (
|
|
855
|
+
String(slot?.name || "") === "skillsContext" || String(slot?.type || "") === "text"
|
|
856
|
+
? { ...slot, default: skillsBlock, value: skillsBlock }
|
|
857
|
+
: slot
|
|
858
|
+
)),
|
|
859
|
+
};
|
|
860
|
+
outputs.set(nodeId, skillsBlock);
|
|
861
|
+
workspaceUpdateDirectDisplays(graph, nodeId, skillsBlock);
|
|
862
|
+
emit({ type: "graph", nodeId, graph });
|
|
863
|
+
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
if (workspaceDisplayKind(defId)) {
|
|
868
|
+
const content = workspaceUpstreamText(graph, nodeId, outputs);
|
|
869
|
+
graph.instances[nodeId] = workspaceWriteDisplayContent(instance, content);
|
|
870
|
+
outputs.set(nodeId, content);
|
|
871
|
+
emit({ type: "graph", nodeId, graph });
|
|
872
|
+
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
if (defId === "provide_str") {
|
|
877
|
+
const content = workspaceInstanceText(instance);
|
|
878
|
+
outputs.set(nodeId, content);
|
|
879
|
+
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
880
|
+
continue;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
if (defId === "provide_file") {
|
|
884
|
+
const fileValue = workspaceSlotValue(Array.isArray(instance.output) ? instance.output[0] : null) || workspaceInstanceText(instance);
|
|
885
|
+
const abs = path.resolve(scopedRoot, fileValue);
|
|
886
|
+
if (!abs.startsWith(path.resolve(scopedRoot) + path.sep) && abs !== path.resolve(scopedRoot)) {
|
|
887
|
+
throw new Error(`Workspace file is outside root: ${fileValue}`);
|
|
888
|
+
}
|
|
889
|
+
const content = fs.existsSync(abs) && fs.statSync(abs).isFile() ? fs.readFileSync(abs, "utf-8") : fileValue;
|
|
890
|
+
outputs.set(nodeId, content);
|
|
891
|
+
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
if (defId === "control_cd_workspace") {
|
|
896
|
+
const inputText = workspaceUpstreamText(graph, nodeId, outputs);
|
|
897
|
+
const inputSlots = Array.isArray(instance.input) ? instance.input : [];
|
|
898
|
+
const pathSlot = inputSlots.find((slot) => String(slot?.name || "") === "path") ||
|
|
899
|
+
inputSlots.find((slot) => String(slot?.name || "") === "target");
|
|
900
|
+
const candidate = workspaceSlotValue(pathSlot) || workspaceInstanceText(instance) || inputText;
|
|
901
|
+
const abs = candidate ? path.resolve(scopedRoot, candidate) : scopedRoot;
|
|
902
|
+
if (fs.existsSync(abs) && fs.statSync(abs).isDirectory()) cwd = abs;
|
|
903
|
+
outputs.set(nodeId, cwd);
|
|
904
|
+
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
905
|
+
continue;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
if (defId === "control_user_workspace") {
|
|
909
|
+
cwd = path.resolve(os.homedir());
|
|
910
|
+
outputs.set(nodeId, cwd);
|
|
911
|
+
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
const upstreamText = workspaceUpstreamText(graph, nodeId, outputs);
|
|
916
|
+
const body = String(instance.body || "").trim();
|
|
917
|
+
if (defId === "agent_subAgent" && !body && !String(upstreamText || "").trim()) {
|
|
918
|
+
throw new Error(`Workspace node ${nodeId} has no task. Fill the node body or connect upstream text.`);
|
|
919
|
+
}
|
|
920
|
+
const upstreamSkillBlocks = workspaceUpstreamSkillBlocks(graph, nodeId, outputs);
|
|
921
|
+
const prompt = workspaceNodePrompt(graph, nodeId, upstreamText, upstreamSkillBlocks || loadSkillsBlockForKeys(fallbackSelectedSkillKeys));
|
|
922
|
+
let content = "";
|
|
923
|
+
const maxAttempts = 3;
|
|
924
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
925
|
+
let attemptContent = "";
|
|
926
|
+
try {
|
|
927
|
+
const handle = startComposerAgent({
|
|
928
|
+
uiWorkspaceRoot: scopedRoot,
|
|
929
|
+
cliWorkspace: cwd,
|
|
930
|
+
prompt,
|
|
931
|
+
modelKey,
|
|
932
|
+
agentflowUserId: userCtx.userId || "",
|
|
933
|
+
onStreamEvent: (ev) => {
|
|
934
|
+
emit({ ...ev, nodeId });
|
|
935
|
+
if (ev?.type === "natural" && ev.kind === "assistant" && typeof ev.text === "string") {
|
|
936
|
+
attemptContent += (attemptContent ? "\n" : "") + ev.text;
|
|
937
|
+
const updatedDisplays = workspaceUpdateDirectDisplays(graph, nodeId, attemptContent);
|
|
938
|
+
if (updatedDisplays.length) emit({ type: "graph", nodeId, displayNodeIds: updatedDisplays, graph });
|
|
939
|
+
}
|
|
940
|
+
},
|
|
941
|
+
});
|
|
942
|
+
await handle.finished;
|
|
943
|
+
content = attemptContent.trim();
|
|
944
|
+
break;
|
|
945
|
+
} catch (e) {
|
|
946
|
+
if (attempt < maxAttempts && isTransientAgentNetworkError(e)) {
|
|
947
|
+
emit({ type: "status", nodeId, line: `Workspace node retry ${attempt + 1}/${maxAttempts} after network error` });
|
|
948
|
+
await sleepMs(Math.min(1500 * attempt, 5000));
|
|
949
|
+
continue;
|
|
950
|
+
}
|
|
951
|
+
throw e;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
outputs.set(nodeId, content);
|
|
955
|
+
const updatedDisplays = workspaceUpdateDirectDisplays(graph, nodeId, content);
|
|
956
|
+
if (updatedDisplays.length) emit({ type: "graph", nodeId, displayNodeIds: updatedDisplays, graph });
|
|
957
|
+
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
958
|
+
}
|
|
959
|
+
graph.updatedAt = new Date().toISOString();
|
|
960
|
+
return { graph, events, order };
|
|
961
|
+
}
|
|
962
|
+
|
|
449
963
|
function isTransientAgentNetworkError(err) {
|
|
450
964
|
const text = [
|
|
451
965
|
err?.message,
|
|
@@ -1059,6 +1573,60 @@ export function startUiServer({
|
|
|
1059
1573
|
return;
|
|
1060
1574
|
}
|
|
1061
1575
|
|
|
1576
|
+
if (req.method === "POST" && url.pathname === "/api/workspace/run") {
|
|
1577
|
+
let payload;
|
|
1578
|
+
try {
|
|
1579
|
+
payload = JSON.parse(await readBody(req));
|
|
1580
|
+
} catch {
|
|
1581
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
try {
|
|
1585
|
+
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
1586
|
+
flowId: payload.flowId || "",
|
|
1587
|
+
flowSource: payload.flowSource || "user",
|
|
1588
|
+
archived: payload.archived === true || payload.flowArchived === true,
|
|
1589
|
+
}, userCtx);
|
|
1590
|
+
if (scoped.error) {
|
|
1591
|
+
json(res, 400, { error: scoped.error });
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1594
|
+
if (scoped.archived || scoped.flowSource === "builtin") {
|
|
1595
|
+
json(res, 400, { error: "Cannot run workspace graph for builtin or archived pipeline" });
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
const wantsStream = /\bapplication\/x-ndjson\b/i.test(req.headers.accept || "") || payload.stream === true;
|
|
1599
|
+
if (wantsStream) {
|
|
1600
|
+
const graphPath = workspaceGraphPath(scoped.root);
|
|
1601
|
+
res.writeHead(200, {
|
|
1602
|
+
"Content-Type": "application/x-ndjson; charset=utf-8",
|
|
1603
|
+
"Cache-Control": "no-cache",
|
|
1604
|
+
"X-Accel-Buffering": "no",
|
|
1605
|
+
});
|
|
1606
|
+
const writeEvent = (event) => {
|
|
1607
|
+
res.write(JSON.stringify(event) + "\n");
|
|
1608
|
+
};
|
|
1609
|
+
try {
|
|
1610
|
+
const result = await runWorkspaceGraph(root, scoped.root, payload, userCtx, { onEvent: writeEvent });
|
|
1611
|
+
fs.writeFileSync(graphPath, JSON.stringify(result.graph, null, 2) + "\n", "utf-8");
|
|
1612
|
+
writeEvent({ type: "done", ok: true, path: graphPath, graph: result.graph, order: result.order });
|
|
1613
|
+
res.end();
|
|
1614
|
+
} catch (e) {
|
|
1615
|
+
writeEvent({ type: "error", error: (e && e.message) || String(e) });
|
|
1616
|
+
res.end();
|
|
1617
|
+
}
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1620
|
+
const result = await runWorkspaceGraph(root, scoped.root, payload, userCtx);
|
|
1621
|
+
const graphPath = workspaceGraphPath(scoped.root);
|
|
1622
|
+
fs.writeFileSync(graphPath, JSON.stringify(result.graph, null, 2) + "\n", "utf-8");
|
|
1623
|
+
json(res, 200, { ok: true, path: graphPath, ...result });
|
|
1624
|
+
} catch (e) {
|
|
1625
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
1626
|
+
}
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1062
1630
|
if (req.method === "GET" && url.pathname === "/api/workspace/file") {
|
|
1063
1631
|
try {
|
|
1064
1632
|
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
@@ -1252,7 +1820,7 @@ export function startUiServer({
|
|
|
1252
1820
|
agentflowUserId: userCtx.userId || "",
|
|
1253
1821
|
onStreamEvent: (ev) => {
|
|
1254
1822
|
events.push(ev);
|
|
1255
|
-
if (ev?.type === "natural" && typeof ev.text === "string") {
|
|
1823
|
+
if (ev?.type === "natural" && ev.kind === "assistant" && typeof ev.text === "string") {
|
|
1256
1824
|
attemptContent += (attemptContent ? "\n" : "") + ev.text;
|
|
1257
1825
|
}
|
|
1258
1826
|
},
|
|
@@ -1494,6 +2062,32 @@ export function startUiServer({
|
|
|
1494
2062
|
return;
|
|
1495
2063
|
}
|
|
1496
2064
|
|
|
2065
|
+
if (req.method === "GET" && url.pathname === "/api/user-env") {
|
|
2066
|
+
try {
|
|
2067
|
+
json(res, 200, { env: readUserEnvRows(userCtx.userId) });
|
|
2068
|
+
} catch (e) {
|
|
2069
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
2070
|
+
}
|
|
2071
|
+
return;
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
if (req.method === "POST" && url.pathname === "/api/user-env") {
|
|
2075
|
+
let payload;
|
|
2076
|
+
try {
|
|
2077
|
+
payload = JSON.parse(await readBody(req));
|
|
2078
|
+
} catch {
|
|
2079
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
2080
|
+
return;
|
|
2081
|
+
}
|
|
2082
|
+
try {
|
|
2083
|
+
const envRows = writeUserEnvRows(userCtx.userId, payload?.env || []);
|
|
2084
|
+
json(res, 200, { success: true, env: envRows });
|
|
2085
|
+
} catch (e) {
|
|
2086
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
2087
|
+
}
|
|
2088
|
+
return;
|
|
2089
|
+
}
|
|
2090
|
+
|
|
1497
2091
|
if (req.method === "POST" && url.pathname === "/api/update-model-lists") {
|
|
1498
2092
|
try {
|
|
1499
2093
|
let opencodeProviderOverride = "";
|
|
@@ -1686,13 +2280,29 @@ export function startUiServer({
|
|
|
1686
2280
|
|
|
1687
2281
|
if (req.method === "GET" && url.pathname === "/api/marketplace/nodes") {
|
|
1688
2282
|
try {
|
|
1689
|
-
json(res, 200, listMarketplacePackages(root));
|
|
2283
|
+
json(res, 200, listMarketplacePackages(root, userCtx));
|
|
1690
2284
|
} catch (e) {
|
|
1691
2285
|
json(res, 500, { error: (e && e.message) || String(e) });
|
|
1692
2286
|
}
|
|
1693
2287
|
return;
|
|
1694
2288
|
}
|
|
1695
2289
|
|
|
2290
|
+
if (req.method === "DELETE" && url.pathname === "/api/marketplace/node") {
|
|
2291
|
+
const id = url.searchParams.get("id") || "";
|
|
2292
|
+
const version = url.searchParams.get("version") || "";
|
|
2293
|
+
if (!id || !version) {
|
|
2294
|
+
json(res, 400, { ok: false, error: "Missing marketplace node id or version" });
|
|
2295
|
+
return;
|
|
2296
|
+
}
|
|
2297
|
+
try {
|
|
2298
|
+
const result = deleteMarketplaceNodePackage(root, id, version, userCtx);
|
|
2299
|
+
json(res, result.ok ? 200 : 400, result);
|
|
2300
|
+
} catch (e) {
|
|
2301
|
+
json(res, 500, { ok: false, error: (e && e.message) || String(e) });
|
|
2302
|
+
}
|
|
2303
|
+
return;
|
|
2304
|
+
}
|
|
2305
|
+
|
|
1696
2306
|
if (req.method === "POST" && url.pathname === "/api/marketplace/install-node") {
|
|
1697
2307
|
let payload;
|
|
1698
2308
|
try {
|
|
@@ -2386,7 +2996,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
2386
2996
|
child = spawn(process.execPath, args, {
|
|
2387
2997
|
cwd: root,
|
|
2388
2998
|
stdio: ["ignore", "pipe", "pipe"],
|
|
2389
|
-
env: {
|
|
2999
|
+
env: runtimeEnvForUser(userCtx, { FORCE_COLOR: "0" }),
|
|
2390
3000
|
// detached: true 使 child 成为新进程组 leader,/api/flow/run/stop 时
|
|
2391
3001
|
// 用 process.kill(-pid) 可以一次性 SIGTERM 整棵进程树(含 cursor-agent 等孙进程)
|
|
2392
3002
|
detached: true,
|
|
@@ -2514,6 +3124,27 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
2514
3124
|
return;
|
|
2515
3125
|
}
|
|
2516
3126
|
|
|
3127
|
+
if (req.method === "GET" && url.pathname === "/api/skill-collections") {
|
|
3128
|
+
json(res, 200, readSkillCollectionConfig(userCtx, listComposerSkills(PACKAGE_ROOT, root)));
|
|
3129
|
+
return;
|
|
3130
|
+
}
|
|
3131
|
+
|
|
3132
|
+
if (req.method === "POST" && url.pathname === "/api/skill-collections") {
|
|
3133
|
+
let payload;
|
|
3134
|
+
try {
|
|
3135
|
+
payload = JSON.parse(await readBody(req));
|
|
3136
|
+
} catch {
|
|
3137
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
3138
|
+
return;
|
|
3139
|
+
}
|
|
3140
|
+
try {
|
|
3141
|
+
json(res, 200, writeSkillCollectionConfig(userCtx, payload, listComposerSkills(PACKAGE_ROOT, root)));
|
|
3142
|
+
} catch (e) {
|
|
3143
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
3144
|
+
}
|
|
3145
|
+
return;
|
|
3146
|
+
}
|
|
3147
|
+
|
|
2517
3148
|
if (req.method === "GET" && url.pathname === "/api/skills/detail") {
|
|
2518
3149
|
const key = url.searchParams.get("key") || url.searchParams.get("name") || "";
|
|
2519
3150
|
const detail = readComposerSkillDetail(PACKAGE_ROOT, root, key);
|