@fieldwangai/agentflow 0.1.37 → 0.1.39

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.
@@ -0,0 +1,110 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import {
4
+ getAgentflowDataRoot,
5
+ getUserPipelinesRoot,
6
+ } from "./paths.mjs";
7
+
8
+ const CONFIG_FILENAME = "admin-builtin-pipelines.json";
9
+
10
+ function configPath() {
11
+ return path.join(getAgentflowDataRoot(), "admin", CONFIG_FILENAME);
12
+ }
13
+
14
+ function normalizeConfig(raw) {
15
+ const hiddenBuiltins = Array.isArray(raw?.hiddenBuiltins)
16
+ ? raw.hiddenBuiltins.map((id) => String(id || "").trim()).filter(Boolean)
17
+ : [];
18
+ const promoted = Array.isArray(raw?.promoted)
19
+ ? raw.promoted.map((item) => ({
20
+ id: String(item?.id || item?.flowId || "").trim(),
21
+ ownerUserId: String(item?.ownerUserId || "").trim(),
22
+ source: "user",
23
+ createdAt: String(item?.createdAt || ""),
24
+ createdBy: String(item?.createdBy || ""),
25
+ })).filter((item) => item.id && item.ownerUserId)
26
+ : [];
27
+ return {
28
+ hiddenBuiltins: Array.from(new Set(hiddenBuiltins)),
29
+ promoted: promoted.filter((item, index, list) => (
30
+ list.findIndex((other) => other.id === item.id) === index
31
+ )),
32
+ };
33
+ }
34
+
35
+ export function readAdminBuiltinPipelineConfig() {
36
+ try {
37
+ const p = configPath();
38
+ if (!fs.existsSync(p)) return normalizeConfig({});
39
+ return normalizeConfig(JSON.parse(fs.readFileSync(p, "utf-8")));
40
+ } catch {
41
+ return normalizeConfig({});
42
+ }
43
+ }
44
+
45
+ export function writeAdminBuiltinPipelineConfig(config) {
46
+ const next = normalizeConfig(config);
47
+ const p = configPath();
48
+ fs.mkdirSync(path.dirname(p), { recursive: true });
49
+ fs.writeFileSync(p, JSON.stringify(next, null, 2) + "\n", "utf-8");
50
+ return next;
51
+ }
52
+
53
+ export function resolveAdminBuiltinPipelineDir(flowId) {
54
+ const id = String(flowId || "").trim();
55
+ if (!id) return "";
56
+ const config = readAdminBuiltinPipelineConfig();
57
+ const entry = config.promoted.find((item) => item.id === id);
58
+ if (!entry) return "";
59
+ const dir = path.join(getUserPipelinesRoot(entry.ownerUserId), entry.id);
60
+ return fs.existsSync(path.join(dir, "flow.yaml")) ? dir : "";
61
+ }
62
+
63
+ export function updateAdminBuiltinPipelineConfig(action, payload = {}, actor = {}) {
64
+ const config = readAdminBuiltinPipelineConfig();
65
+ const flowId = String(payload.flowId || "").trim();
66
+ if (!flowId) return { ok: false, error: "Missing flowId" };
67
+ if (action === "promote") {
68
+ const ownerUserId = String(payload.ownerUserId || actor.userId || "").trim();
69
+ if (!ownerUserId) return { ok: false, error: "Missing ownerUserId" };
70
+ const dir = path.join(getUserPipelinesRoot(ownerUserId), flowId);
71
+ if (!fs.existsSync(path.join(dir, "flow.yaml"))) return { ok: false, error: "Pipeline not found" };
72
+ const promoted = config.promoted.filter((item) => item.id !== flowId);
73
+ promoted.unshift({
74
+ id: flowId,
75
+ ownerUserId,
76
+ source: "user",
77
+ createdAt: new Date().toISOString(),
78
+ createdBy: String(actor.userId || ""),
79
+ });
80
+ return { ok: true, config: writeAdminBuiltinPipelineConfig({ ...config, promoted }) };
81
+ }
82
+ if (action === "unpromote") {
83
+ return {
84
+ ok: true,
85
+ config: writeAdminBuiltinPipelineConfig({
86
+ ...config,
87
+ promoted: config.promoted.filter((item) => item.id !== flowId),
88
+ }),
89
+ };
90
+ }
91
+ if (action === "hide-builtin") {
92
+ return {
93
+ ok: true,
94
+ config: writeAdminBuiltinPipelineConfig({
95
+ ...config,
96
+ hiddenBuiltins: Array.from(new Set([...config.hiddenBuiltins, flowId])),
97
+ }),
98
+ };
99
+ }
100
+ if (action === "show-builtin") {
101
+ return {
102
+ ok: true,
103
+ config: writeAdminBuiltinPipelineConfig({
104
+ ...config,
105
+ hiddenBuiltins: config.hiddenBuiltins.filter((id) => id !== flowId),
106
+ }),
107
+ };
108
+ }
109
+ return { ok: false, error: "Invalid action" };
110
+ }
@@ -15,6 +15,10 @@ import {
15
15
  USER_AGENTFLOW_PIPELINES_LABEL,
16
16
  getUserPipelinesRoot,
17
17
  } from "./paths.mjs";
18
+ import {
19
+ readAdminBuiltinPipelineConfig,
20
+ resolveAdminBuiltinPipelineDir,
21
+ } from "./admin-builtin-pipelines.mjs";
18
22
  import { Table } from "./table.mjs";
19
23
  import { listMarketplaceNodes, parseMarketplaceDefinitionId, resolveMarketplaceNodePackage } from "./marketplace.mjs";
20
24
 
@@ -53,12 +57,27 @@ export function readPipelineListDescription(flowDir) {
53
57
  export function listFlowsJson(workspaceRoot, opts = {}) {
54
58
  const root = path.resolve(workspaceRoot);
55
59
  const out = [];
60
+ const adminBuiltinConfig = readAdminBuiltinPipelineConfig();
61
+ const hiddenBuiltins = new Set(adminBuiltinConfig.hiddenBuiltins);
56
62
  const fromBuiltin = collectPipelineNamesFromDir(PACKAGE_BUILTIN_PIPELINES_DIR);
57
63
  for (const name of fromBuiltin) {
64
+ if (hiddenBuiltins.has(name)) continue;
58
65
  const dir = path.join(PACKAGE_BUILTIN_PIPELINES_DIR, name);
59
66
  const description = readPipelineListDescription(dir);
60
67
  out.push({ id: name, path: dir, source: "builtin", ...(description ? { description } : {}) });
61
68
  }
69
+ for (const item of adminBuiltinConfig.promoted) {
70
+ const dir = resolveAdminBuiltinPipelineDir(item.id);
71
+ if (!dir) continue;
72
+ const description = readPipelineListDescription(dir);
73
+ out.push({
74
+ id: item.id,
75
+ path: dir,
76
+ source: "admin",
77
+ ownerUserId: item.ownerUserId,
78
+ ...(description ? { description } : {}),
79
+ });
80
+ }
62
81
  const userPipelinesRoot = getUserPipelinesRoot(opts.userId);
63
82
  const fromUserData = collectPipelineNamesFromDir(userPipelinesRoot);
64
83
  for (const name of fromUserData) {
@@ -108,7 +127,7 @@ export function listFlowsJson(workspaceRoot, opts = {}) {
108
127
  out.push({ id: name, path: dir, source: "workspace", archived: true, ...(description ? { description } : {}) });
109
128
  workspaceArchivedIds.add(name);
110
129
  }
111
- const sourceRank = (s) => (s === "builtin" ? 0 : s === "user" ? 1 : 2);
130
+ const sourceRank = (s) => (s === "builtin" ? 0 : s === "admin" ? 1 : s === "user" ? 2 : 3);
112
131
  const archRank = (a) => (a.archived ? 1 : 0);
113
132
  out.sort(
114
133
  (a, b) =>
@@ -302,6 +321,9 @@ export function listNodesJson(workspaceRoot, flowId, flowSource, opts = {}) {
302
321
  }
303
322
  }
304
323
  } catch (_) {}
324
+ } else if (flowSource === "admin") {
325
+ const flowDir = resolveAdminBuiltinPipelineDir(flowId);
326
+ if (flowDir) addFromDir(path.join(flowDir, "nodes"), "flow", flowId);
305
327
  } else if (flowSource === "user") {
306
328
  if (archived) {
307
329
  addFromDir(path.join(userPipelinesRoot, ARCHIVED_PIPELINES_DIR_NAME, flowId, "nodes"), "flow", flowId);
@@ -384,6 +406,8 @@ export function readFlowJson(workspaceRoot, flowId, flowSource, options = {}) {
384
406
 
385
407
  if (flowSource === "builtin") {
386
408
  flowDir = path.join(PACKAGE_BUILTIN_PIPELINES_DIR, flowId);
409
+ } else if (flowSource === "admin") {
410
+ flowDir = resolveAdminBuiltinPipelineDir(flowId);
387
411
  } else if (flowSource === "user") {
388
412
  flowDir = path.join(userPipelinesRoot, flowId);
389
413
  } else if (flowSource === "workspace") {
@@ -455,6 +479,9 @@ export function getFlowYamlAbs(workspaceRoot, flowId, flowSource, options = {})
455
479
 
456
480
  if (flowSource === "builtin") {
457
481
  yamlPath = path.join(PACKAGE_BUILTIN_PIPELINES_DIR, flowId, "flow.yaml");
482
+ } else if (flowSource === "admin") {
483
+ const flowDir = resolveAdminBuiltinPipelineDir(flowId);
484
+ yamlPath = flowDir ? path.join(flowDir, "flow.yaml") : "";
458
485
  } else if (flowSource === "user") {
459
486
  yamlPath = path.join(userPipelinesRoot, flowId, "flow.yaml");
460
487
  if (!fs.existsSync(yamlPath)) {
@@ -523,6 +550,9 @@ export function readNodeJson(workspaceRoot, nodeId, flowId, flowSource, opts = {
523
550
  if (flowId && flowSource) {
524
551
  if (flowSource === "builtin") {
525
552
  pathsToTry.push(path.join(PACKAGE_BUILTIN_PIPELINES_DIR, flowId, "nodes", fileName));
553
+ } else if (flowSource === "admin") {
554
+ const flowDir = resolveAdminBuiltinPipelineDir(flowId);
555
+ if (flowDir) pathsToTry.push(path.join(flowDir, "nodes", fileName));
526
556
  } else if (flowSource === "user") {
527
557
  if (archived) {
528
558
  pathsToTry.push(path.join(userPipelinesRoot, ARCHIVED_PIPELINES_DIR_NAME, flowId, "nodes", fileName));
@@ -637,6 +667,9 @@ function resolveMarkdownNodeFile(workspaceRoot, nodeId, flowId, flowSource, opts
637
667
  if (flowId && flowSource) {
638
668
  if (flowSource === "builtin") {
639
669
  pathsToTry.push(path.join(PACKAGE_BUILTIN_PIPELINES_DIR, flowId, "nodes", fileName));
670
+ } else if (flowSource === "admin") {
671
+ const flowDir = resolveAdminBuiltinPipelineDir(flowId);
672
+ if (flowDir) pathsToTry.push(path.join(flowDir, "nodes", fileName));
640
673
  } else if (flowSource === "user") {
641
674
  if (archived) {
642
675
  pathsToTry.push(path.join(userPipelinesRoot, ARCHIVED_PIPELINES_DIR_NAME, flowId, "nodes", fileName));
@@ -797,7 +830,7 @@ export function listPipelines(workspaceRoot) {
797
830
  style: { head: [], border: ["grey"] },
798
831
  });
799
832
  for (const row of rows) {
800
- const sourceLabel = row.source === "builtin" ? "builtin" : row.source === "workspace" ? "workspace" : "user";
833
+ const sourceLabel = row.source === "builtin" || row.source === "admin" ? "builtin" : row.source === "workspace" ? "workspace" : "user";
801
834
  table.push([row.id, sourceLabel, `agentflow apply ${row.id}`]);
802
835
  }
803
836
  log.info("\n" + chalk.bold("Pipelines"));
@@ -333,6 +333,10 @@
333
333
  "displayName": "Chart Display",
334
334
  "description": "Render ChartSpec/ECharts JSON on the Workspace canvas and pass the JSON downstream"
335
335
  },
336
+ "display_table": {
337
+ "displayName": "Table Display",
338
+ "description": "Render JSON, Markdown, CSV, or TSV table data on the Workspace canvas and pass the text downstream"
339
+ },
336
340
  "provide_str": {
337
341
  "displayName": "Text",
338
342
  "description": "Provide a text value directly, value will be passed to downstream as-is"
@@ -333,6 +333,10 @@
333
333
  "displayName": "图表展示",
334
334
  "description": "在 Workspace 画布中渲染 ChartSpec/ECharts JSON,并将 JSON 继续传给下游"
335
335
  },
336
+ "display_table": {
337
+ "displayName": "表格展示",
338
+ "description": "在 Workspace 画布中渲染 JSON、Markdown、CSV 或 TSV 表格,并将文本继续传给下游"
339
+ },
336
340
  "provide_str": {
337
341
  "displayName": "文本",
338
342
  "description": "直接提供一段文本,value 会原样供下游引用"
@@ -449,6 +449,24 @@ export function deleteMarketplaceNodePackage(workspaceRoot, id, version, opts =
449
449
  return { ok: true, id, version, packageDir };
450
450
  }
451
451
 
452
+ export function deleteMarketplaceFlowSnippetPackage(workspaceRoot, id, version) {
453
+ const packageDir = resolveWorkspaceFlowSnippetPackageDir(workspaceRoot, id, version);
454
+ if (!packageDir) return { ok: false, error: "Invalid flow snippet id or version" };
455
+ if (!fs.existsSync(path.join(packageDir, FLOW_SNIPPET_MANIFEST))) {
456
+ return { ok: false, error: `Flow snippet package not found: ${id}@${version}` };
457
+ }
458
+ fs.rmSync(packageDir, { recursive: true, force: true });
459
+ const versionRoot = path.dirname(packageDir);
460
+ try {
461
+ if (fs.existsSync(versionRoot) && fs.readdirSync(versionRoot).length === 0) {
462
+ fs.rmdirSync(versionRoot);
463
+ }
464
+ } catch {
465
+ /* keep non-empty or unreadable parent */
466
+ }
467
+ return { ok: true, id, version, packageDir };
468
+ }
469
+
452
470
  export function writeFlowMarketplaceLock(workspaceRoot, flowDir, flowData) {
453
471
  if (!flowData || !flowData.instances || typeof flowData.instances !== "object") return null;
454
472
  const nodes = {};
@@ -9,6 +9,7 @@ import fs from "fs";
9
9
  import http from "http";
10
10
  import os from "os";
11
11
  import path from "path";
12
+ import crypto from "crypto";
12
13
  import { execFile, spawn } from "child_process";
13
14
  import busboy from "busboy";
14
15
  import { log } from "./log.mjs";
@@ -40,6 +41,7 @@ import {
40
41
  import { t } from "./i18n.mjs";
41
42
  import {
42
43
  PACKAGE_ROOT,
44
+ getAgentflowDataRoot,
43
45
  getAgentflowUserConfigAbs,
44
46
  getAgentflowUserDataRoot,
45
47
  getModelListsAbs,
@@ -75,6 +77,7 @@ import { runNodeScript } from "./pipeline-scripts.mjs";
75
77
  import { readFlowSchedule, writeFlowSchedule } from "./schedule-config.mjs";
76
78
  import { listScheduleStatuses } from "./scheduler.mjs";
77
79
  import {
80
+ deleteMarketplaceFlowSnippetPackage,
78
81
  deleteMarketplaceNodePackage,
79
82
  installFlowDependency,
80
83
  listMarketplaceFlowSnippets,
@@ -95,6 +98,10 @@ import {
95
98
  readUserAllowlist,
96
99
  } from "./auth.mjs";
97
100
  import { readUserEnvObject, readUserEnvRows, writeUserEnvRows } from "./user-env.mjs";
101
+ import {
102
+ readAdminBuiltinPipelineConfig,
103
+ updateAdminBuiltinPipelineConfig,
104
+ } from "./admin-builtin-pipelines.mjs";
98
105
 
99
106
  const MIME = {
100
107
  ".html": "text/html; charset=utf-8",
@@ -159,6 +166,48 @@ function json(res, status, obj) {
159
166
  res.end(body);
160
167
  }
161
168
 
169
+ function feedbackStorePath() {
170
+ return path.join(getAgentflowDataRoot(), "feedback", "feedback.json");
171
+ }
172
+
173
+ function readFeedbackItems() {
174
+ try {
175
+ const p = feedbackStorePath();
176
+ if (!fs.existsSync(p)) return [];
177
+ const data = JSON.parse(fs.readFileSync(p, "utf-8"));
178
+ return Array.isArray(data) ? data.filter((item) => item && typeof item === "object") : [];
179
+ } catch {
180
+ return [];
181
+ }
182
+ }
183
+
184
+ function writeFeedbackItems(items) {
185
+ const p = feedbackStorePath();
186
+ fs.mkdirSync(path.dirname(p), { recursive: true });
187
+ fs.writeFileSync(p, JSON.stringify(Array.isArray(items) ? items : [], null, 2) + "\n", "utf-8");
188
+ }
189
+
190
+ function createFeedbackItem(payload, user) {
191
+ const title = String(payload?.title || "").trim().slice(0, 120);
192
+ const content = String(payload?.content || "").trim().slice(0, 5000);
193
+ const contact = String(payload?.contact || "").trim().slice(0, 160);
194
+ const pageUrl = String(payload?.pageUrl || "").trim().slice(0, 500);
195
+ if (!title) return { error: "Missing feedback title" };
196
+ if (!content) return { error: "Missing feedback content" };
197
+ return {
198
+ item: {
199
+ id: `fb_${Date.now().toString(36)}_${crypto.randomBytes(5).toString("hex")}`,
200
+ title,
201
+ content,
202
+ contact,
203
+ pageUrl,
204
+ userId: String(user?.userId || ""),
205
+ username: String(user?.username || user?.userId || ""),
206
+ createdAt: new Date().toISOString(),
207
+ },
208
+ };
209
+ }
210
+
162
211
  function skillCollectionsAbs(userCtx = {}) {
163
212
  return path.join(getAgentflowUserDataRoot(userCtx.userId), SKILL_COLLECTIONS_FILENAME);
164
213
  }
@@ -959,13 +1008,38 @@ function normalizeSkillhubSearchPayload(raw) {
959
1008
 
960
1009
  function normalizeSkillhubListPayload(raw) {
961
1010
  const arr = Array.isArray(raw) ? raw : [];
962
- return arr.map((x) => ({
963
- name: String(x?.name ?? ""),
964
- baseDir: String(x?.baseDir ?? ""),
965
- path: String(x?.path ?? ""),
966
- kind: String(x?.kind ?? ""),
967
- agent: String(x?.agent ?? ""),
968
- })).filter((x) => x.name);
1011
+ return arr.map((x) => {
1012
+ const pathValue = String(x?.path ?? "");
1013
+ const targetPath = String(x?.targetPath ?? x?.target ?? "");
1014
+ const metaPaths = [
1015
+ pathValue ? path.join(pathValue, "_meta.json") : "",
1016
+ targetPath ? path.join(targetPath, "_meta.json") : "",
1017
+ ];
1018
+ try {
1019
+ if (pathValue) metaPaths.push(path.join(fs.realpathSync(pathValue), "_meta.json"));
1020
+ } catch {}
1021
+ let meta = {};
1022
+ for (const metaPath of metaPaths) {
1023
+ if (!metaPath || !fs.existsSync(metaPath)) continue;
1024
+ try {
1025
+ meta = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
1026
+ break;
1027
+ } catch {}
1028
+ }
1029
+ return {
1030
+ name: String(x?.name ?? meta?.slug ?? ""),
1031
+ displayName: String(meta?.displayName ?? ""),
1032
+ summary: String(meta?.summary ?? ""),
1033
+ version: String(meta?.version ?? ""),
1034
+ baseDir: String(x?.baseDir ?? ""),
1035
+ path: pathValue,
1036
+ targetPath,
1037
+ kind: String(x?.kind ?? ""),
1038
+ agent: String(x?.agent ?? ""),
1039
+ userName: String(meta?.userName ?? ""),
1040
+ generatedAt: String(meta?.generatedAt ?? ""),
1041
+ };
1042
+ }).filter((x) => x.name);
969
1043
  }
970
1044
 
971
1045
  function skillhubInstallArgs(payload, { uninstall = false } = {}) {
@@ -1217,7 +1291,7 @@ function buildWorkspaceGeneratePrompt(payload) {
1217
1291
  "Workspace 是当前 pipeline 的临时工作区,用于分析、试验、生成中间文件和展示结果。",
1218
1292
  "Workspace 与 Pipeline 各自有独立的 Skill collection;此处只使用当前 Workspace Composer 选择的 collections / skills 作为本次行为规则与编辑依据。",
1219
1293
  "当 Skills 提到修改 flow.yaml / instances / edges / ui 时,在 Workspace 视图下应映射为修改当前工作区的 workspace.graph.json,除非用户显式勾选并要求修改正式 flow.yaml。",
1220
- "workspace.graph.json 使用 JSON:{ version, instances, edges, ui: { nodePositions, nodeSizes } }。instances 的结构与 flow.yaml instances 一致;edges 使用 source/target/sourceHandle/targetHandle;ui.nodePositions 记录节点坐标。",
1294
+ "workspace.graph.json 使用 JSON:{ version, instances, edges, ui: { nodePositions, nodeSizes } }。instances 的结构与 flow.yaml instances 一致;edges 使用 source/target/sourceHandle/targetHandle;ui.nodePositions 记录节点坐标,ui.nodeSizes 记录用户调整过的节点宽高。",
1221
1295
  allowFlowYaml
1222
1296
  ? "用户已允许你考虑正式 flow.yaml;如需修改仍必须明确说明影响。"
1223
1297
  : "默认不要修改正式 flow.yaml;优先在 workspace 文件、workspace.graph.json 或回复内容中完成任务。",
@@ -1329,6 +1403,7 @@ function workspaceDisplayKind(definitionId) {
1329
1403
  if (id === "display_html") return "html";
1330
1404
  if (id === "display_image") return "image";
1331
1405
  if (id === "display_chart") return "chart";
1406
+ if (id === "display_table") return "table";
1332
1407
  return "";
1333
1408
  }
1334
1409
 
@@ -1395,6 +1470,9 @@ function workspaceDownstreamDisplayRequirements(graph, nodeId) {
1395
1470
  if (kinds.has("chart")) {
1396
1471
  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 函数。');
1397
1472
  }
1473
+ if (kinds.has("table")) {
1474
+ rules.push('- 下游连接了表格展示节点:优先只输出表格 JSON,不要解释文字。推荐格式:`{"columns":["列名1","列名2"],"rows":[["值1","值2"]]}`;也可输出对象数组、Markdown 表格、CSV 或 TSV。不要输出 HTML。');
1475
+ }
1398
1476
  return [
1399
1477
  "## 下游输出要求",
1400
1478
  "",
@@ -2199,7 +2277,11 @@ function parseFlowsImportForm(req) {
2199
2277
 
2200
2278
  /** GET 读 flow / nodes / SSE 等 */
2201
2279
  function isValidFlowSourceRead(s) {
2202
- return s === "builtin" || s === "user" || s === "workspace";
2280
+ return s === "builtin" || s === "admin" || s === "user" || s === "workspace";
2281
+ }
2282
+
2283
+ function isReadonlyBuiltinFlowSource(s) {
2284
+ return s === "builtin" || s === "admin";
2203
2285
  }
2204
2286
 
2205
2287
  /** POST 写 flow */
@@ -2251,9 +2333,9 @@ function composerCliWorkspaceForFlowDir(workspaceRoot, _flowDir) {
2251
2333
  * @param {object} p
2252
2334
  * @param {string} p.flowYamlAbs
2253
2335
  * @param {string} p.flowId
2254
- * @param {"builtin" | "user" | "workspace"} p.flowSource
2255
- * @param {string} [p.workspaceWriteDirAbs] builtin 时可写副本根目录(…/pipelines/<flowId>)
2256
- * @param {"user" | "workspace"} [p.editorSyncFlowSource] flow-editor-sync 使用的 flowSource(builtin 时为 workspace)
2336
+ * @param {"builtin" | "admin" | "user" | "workspace"} p.flowSource
2337
+ * @param {string} [p.workspaceWriteDirAbs] 内置来源的可写副本根目录(…/pipelines/<flowId>)
2338
+ * @param {"user" | "workspace"} [p.editorSyncFlowSource] flow-editor-sync 使用的 flowSource(内置来源时为 workspace)
2257
2339
  * @param {string[]} p.instanceIds
2258
2340
  * @param {string} p.userPrompt
2259
2341
  * @param {number} p.uiPort 本地 Web UI 端口(用于 flow 保存后通知浏览器刷新)
@@ -2285,9 +2367,9 @@ function buildComposerPromptWithFlowContext(p) {
2285
2367
  const idsLine =
2286
2368
  p.instanceIds.length > 0 ? p.instanceIds.map(String).join(", ") : "(无,可能为全局修改或新增节点)";
2287
2369
  const builtinExtra =
2288
- p.flowSource === "builtin" && p.workspaceWriteDirAbs
2370
+ isReadonlyBuiltinFlowSource(p.flowSource) && p.workspaceWriteDirAbs
2289
2371
  ? [
2290
- `- 包内 builtin 模板为只读;若保存修改请写入工作区副本目录:${p.workspaceWriteDirAbs}(flow.yaml 与同 id)`,
2372
+ `- 内置模板为只读;若保存修改请写入工作区副本目录:${p.workspaceWriteDirAbs}(flow.yaml 与同 id)`,
2291
2373
  "- 保存后刷新 Web 画布时,flow-editor-sync 的 JSON 须使用 flowSource: workspace(与上方 curl 一致)。",
2292
2374
  ]
2293
2375
  : [];
@@ -2434,6 +2516,63 @@ export function startUiServer({
2434
2516
  return;
2435
2517
  }
2436
2518
 
2519
+ if (url.pathname === "/api/feedback") {
2520
+ if (req.method === "POST") {
2521
+ let payload;
2522
+ try {
2523
+ payload = JSON.parse(await readBody(req));
2524
+ } catch {
2525
+ json(res, 400, { error: "Invalid JSON body" });
2526
+ return;
2527
+ }
2528
+ const created = createFeedbackItem(payload, authUser);
2529
+ if (created.error) {
2530
+ json(res, 400, { error: created.error });
2531
+ return;
2532
+ }
2533
+ const items = readFeedbackItems();
2534
+ items.unshift(created.item);
2535
+ writeFeedbackItems(items.slice(0, 1000));
2536
+ json(res, 200, { ok: true, feedback: created.item });
2537
+ return;
2538
+ }
2539
+ if (req.method === "GET") {
2540
+ if (!authUser?.isAdmin) {
2541
+ json(res, 403, { error: "Admin permission required" });
2542
+ return;
2543
+ }
2544
+ json(res, 200, { feedback: readFeedbackItems() });
2545
+ return;
2546
+ }
2547
+ }
2548
+
2549
+ if (url.pathname === "/api/admin/builtin-flows") {
2550
+ if (!authUser?.isAdmin) {
2551
+ json(res, 403, { error: "Admin permission required" });
2552
+ return;
2553
+ }
2554
+ if (req.method === "GET") {
2555
+ json(res, 200, { config: readAdminBuiltinPipelineConfig() });
2556
+ return;
2557
+ }
2558
+ if (req.method === "POST") {
2559
+ let payload;
2560
+ try {
2561
+ payload = JSON.parse(await readBody(req));
2562
+ } catch {
2563
+ json(res, 400, { error: "Invalid JSON body" });
2564
+ return;
2565
+ }
2566
+ const result = updateAdminBuiltinPipelineConfig(payload?.action, payload, authUser);
2567
+ if (!result.ok) {
2568
+ json(res, 400, { error: result.error || "Update failed" });
2569
+ return;
2570
+ }
2571
+ json(res, 200, { ok: true, config: result.config });
2572
+ return;
2573
+ }
2574
+ }
2575
+
2437
2576
  if (url.pathname === "/api/flows") {
2438
2577
  if (req.method === "GET") {
2439
2578
  try {
@@ -2705,7 +2844,7 @@ export function startUiServer({
2705
2844
  flowId: scoped.flowId,
2706
2845
  flowSource: scoped.flowSource,
2707
2846
  archived: scoped.archived,
2708
- writable: !(scoped.archived || scoped.flowSource === "builtin"),
2847
+ writable: !(scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource)),
2709
2848
  });
2710
2849
  } catch (e) {
2711
2850
  json(res, 500, { error: (e && e.message) || String(e) });
@@ -2731,7 +2870,7 @@ export function startUiServer({
2731
2870
  json(res, 400, { error: scoped.error });
2732
2871
  return;
2733
2872
  }
2734
- if (scoped.archived || scoped.flowSource === "builtin") {
2873
+ if (scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource)) {
2735
2874
  json(res, 400, { error: "Cannot write workspace graph for builtin or archived pipeline" });
2736
2875
  return;
2737
2876
  }
@@ -2763,7 +2902,7 @@ export function startUiServer({
2763
2902
  json(res, 400, { error: scoped.error });
2764
2903
  return;
2765
2904
  }
2766
- if (scoped.archived || scoped.flowSource === "builtin") {
2905
+ if (scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource)) {
2767
2906
  json(res, 400, { error: "Cannot run workspace graph for builtin or archived pipeline" });
2768
2907
  return;
2769
2908
  }
@@ -2921,7 +3060,7 @@ export function startUiServer({
2921
3060
  json(res, 400, { error: scoped.error });
2922
3061
  return;
2923
3062
  }
2924
- if (scoped.archived || scoped.flowSource === "builtin") {
3063
+ if (scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource)) {
2925
3064
  json(res, 400, { error: "Cannot write to builtin or archived pipeline workspace" });
2926
3065
  return;
2927
3066
  }
@@ -2957,7 +3096,7 @@ export function startUiServer({
2957
3096
  json(res, 400, { error: scoped.error });
2958
3097
  return;
2959
3098
  }
2960
- if (scoped.archived || scoped.flowSource === "builtin") {
3099
+ if (scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource)) {
2961
3100
  json(res, 400, { error: "Cannot write to builtin or archived pipeline workspace" });
2962
3101
  return;
2963
3102
  }
@@ -2992,7 +3131,7 @@ export function startUiServer({
2992
3131
  json(res, 400, { error: scoped.error });
2993
3132
  return;
2994
3133
  }
2995
- if (scoped.archived || scoped.flowSource === "builtin") {
3134
+ if (scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource)) {
2996
3135
  json(res, 400, { error: "Cannot write to builtin or archived pipeline workspace" });
2997
3136
  return;
2998
3137
  }
@@ -3711,6 +3850,22 @@ export function startUiServer({
3711
3850
  return;
3712
3851
  }
3713
3852
 
3853
+ if (req.method === "DELETE" && url.pathname === "/api/marketplace/flow-snippet") {
3854
+ const id = url.searchParams.get("id") || "";
3855
+ const version = url.searchParams.get("version") || "";
3856
+ if (!id || !version) {
3857
+ json(res, 400, { ok: false, error: "Missing flow snippet id or version" });
3858
+ return;
3859
+ }
3860
+ try {
3861
+ const result = deleteMarketplaceFlowSnippetPackage(root, id, version);
3862
+ json(res, result.ok ? 200 : 400, result);
3863
+ } catch (e) {
3864
+ json(res, 500, { ok: false, error: (e && e.message) || String(e) });
3865
+ }
3866
+ return;
3867
+ }
3868
+
3714
3869
  if (req.method === "POST" && url.pathname === "/api/marketplace/install-node") {
3715
3870
  let payload;
3716
3871
  try {
@@ -4646,7 +4801,7 @@ finishedAt: "${new Date().toISOString()}"
4646
4801
  let workspaceWriteDirAbs;
4647
4802
  let editorSyncFlowSource = flowSource;
4648
4803
  let flowDirForCli = path.dirname(flowYamlAbs);
4649
- if (flowSource === "builtin") {
4804
+ if (isReadonlyBuiltinFlowSource(flowSource)) {
4650
4805
  const w = resolveFlowDirForWrite(root, flowId, "workspace", userCtx);
4651
4806
  if (w.error || !w.flowDir) {
4652
4807
  json(res, 400, { error: w.error || "Could not resolve workspace flow directory" });
@@ -11,6 +11,7 @@ import {
11
11
  PACKAGE_BUILTIN_PIPELINES_DIR,
12
12
  } from "./paths.mjs";
13
13
  import { listAllRunDirs } from "./workspace.mjs";
14
+ import { resolveAdminBuiltinPipelineDir } from "./admin-builtin-pipelines.mjs";
14
15
 
15
16
  /**
16
17
  * 获取目录下的子目录列表
@@ -239,6 +240,8 @@ export function getPipelineFiles(workspaceRoot, flowId, flowSource, archived = f
239
240
  } else {
240
241
  if (flowSource === "builtin") {
241
242
  pipelineDir = path.join(PACKAGE_BUILTIN_PIPELINES_DIR, flowId);
243
+ } else if (flowSource === "admin") {
244
+ pipelineDir = resolveAdminBuiltinPipelineDir(flowId);
242
245
  } else if (flowSource === "user") {
243
246
  pipelineDir = path.join(userPipelinesRoot, flowId);
244
247
  if (!fs.existsSync(pipelineDir)) {
@@ -0,0 +1,31 @@
1
+ ---
2
+ # Built-in node: Table Display
3
+ description: Display table data in workspace canvas; accepts JSON, Markdown table, CSV, or TSV and passes the text downstream
4
+ displayName: Table Display
5
+ input:
6
+ - type: node
7
+ name: prev
8
+ default: ""
9
+ - type: text
10
+ name: content
11
+ default: ""
12
+ required: true
13
+ showOnNode: true
14
+ - type: file
15
+ name: filePath
16
+ default: ""
17
+ showOnNode: false
18
+ - type: text
19
+ name: workspaceContext
20
+ default: ""
21
+ showOnNode: false
22
+ output:
23
+ - type: text
24
+ name: content
25
+ default: ""
26
+ showOnNode: false
27
+ - type: node
28
+ name: next
29
+ default: ""
30
+ ---
31
+ ${content}