@fieldwangai/agentflow 0.1.149 → 0.1.154
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/README.md +20 -33
- package/README.zh-CN.md +14 -34
- package/agents/agentflow-node-executor-code.md +13 -3
- package/agents/agentflow-node-executor-planning.md +13 -3
- package/agents/agentflow-node-executor-requirement.md +13 -3
- package/agents/agentflow-node-executor-test.md +13 -3
- package/agents/agentflow-node-executor-ui.md +13 -3
- package/agents/agentflow-node-executor.md +13 -3
- package/agents/en/agentflow-node-executor.md +13 -3
- package/agents/zh/agentflow-node-executor.md +13 -3
- package/bin/agentflow.mjs +3 -3
- package/bin/lib/admin-builtin-pipelines.mjs +3 -2
- package/bin/lib/agent-runners.mjs +0 -929
- package/bin/lib/auth.mjs +2 -1
- package/bin/lib/catalog-agents.mjs +2 -1
- package/bin/lib/catalog-flows.mjs +167 -88
- package/bin/lib/composer-agent.mjs +3 -793
- package/bin/lib/composer-skill-router.mjs +3 -323
- package/bin/lib/exec-buffered.mjs +32 -0
- package/bin/lib/flow-dsl/cli.mjs +276 -0
- package/bin/lib/flow-dsl/codegen.mjs +358 -0
- package/bin/lib/flow-dsl/defs.mjs +103 -0
- package/bin/lib/flow-dsl/index.mjs +77 -0
- package/bin/lib/flow-dsl/ir.mjs +305 -0
- package/bin/lib/flow-dsl/layout.mjs +115 -0
- package/bin/lib/flow-dsl/legacy-yaml.mjs +268 -0
- package/bin/lib/flow-dsl/lint.mjs +241 -0
- package/bin/lib/flow-dsl/packages.mjs +251 -0
- package/bin/lib/flow-dsl/parser.mjs +444 -0
- package/bin/lib/flow-import.mjs +96 -26
- package/bin/lib/flow-write.mjs +48 -12
- package/bin/lib/help.mjs +10 -30
- package/bin/lib/html-escape.mjs +19 -0
- package/bin/lib/http-util.mjs +49 -0
- package/bin/lib/legacy-flow-execution.mjs +43 -0
- package/bin/lib/locales/en.json +1 -1
- package/bin/lib/locales/zh.json +1 -1
- package/bin/lib/main.mjs +153 -283
- package/bin/lib/marketplace.mjs +206 -61
- package/bin/lib/model-config.mjs +10 -0
- package/bin/lib/node-package-archive.mjs +240 -0
- package/bin/lib/node-package-bootstrap.mjs +72 -0
- package/bin/lib/node-package-manifest.mjs +174 -0
- package/bin/lib/paths.mjs +42 -19
- package/bin/lib/prd-workflow-routes.mjs +2949 -0
- package/bin/lib/prd-workflow-server.mjs +4884 -0
- package/bin/lib/run-events.mjs +0 -66
- package/bin/lib/schedule-config.mjs +4 -4
- package/bin/lib/skill-runtime.mjs +35 -0
- package/bin/lib/startup-storage-migrations.mjs +274 -0
- package/bin/lib/ui-server.mjs +1106 -18013
- package/bin/lib/user-env.mjs +16 -0
- package/bin/lib/workspace-auto-layout.mjs +234 -0
- package/bin/lib/workspace-flow-store.mjs +478 -0
- package/bin/lib/workspace-graph-merge.mjs +41 -34
- package/bin/lib/workspace-preview.mjs +74 -0
- package/bin/lib/workspace-routes.mjs +3088 -0
- package/bin/lib/workspace-server.mjs +6199 -0
- package/bin/lib/workspace-state.mjs +331 -0
- package/bin/lib/workspace.mjs +2 -1
- package/builtin/nodes/agent_subAgent.md +1 -0
- package/builtin/nodes/control_agent_toBool.md +2 -0
- package/builtin/nodes/control_anyOne.md +2 -0
- package/builtin/nodes/control_cancelled.md +2 -0
- package/builtin/nodes/control_cd_workspace.md +5 -11
- package/builtin/nodes/control_delay.md +2 -0
- package/builtin/nodes/control_end.md +2 -0
- package/builtin/nodes/control_if.md +1 -0
- package/builtin/nodes/control_interval_loop.md +2 -0
- package/builtin/nodes/control_load_mcp.md +24 -0
- package/builtin/nodes/control_load_skills.md +8 -23
- package/builtin/nodes/control_start.md +2 -0
- package/builtin/nodes/control_toBool.md +2 -0
- package/builtin/nodes/control_user_workspace.md +2 -0
- package/builtin/nodes/control_wait_until.md +2 -0
- package/builtin/nodes/display_ascii.md +1 -0
- package/builtin/nodes/display_chart.md +1 -0
- package/builtin/nodes/display_html.md +1 -0
- package/builtin/nodes/display_image.md +1 -0
- package/builtin/nodes/display_markdown.md +1 -0
- package/builtin/nodes/display_mermaid.md +1 -0
- package/builtin/nodes/display_react_app.md +1 -0
- package/builtin/nodes/display_table.md +1 -0
- package/builtin/nodes/provide_bool.md +1 -0
- package/builtin/nodes/provide_file.md +1 -0
- package/builtin/nodes/provide_password.md +1 -0
- package/builtin/nodes/provide_str.md +1 -0
- package/builtin/nodes/tool_display_share_link.md +1 -0
- package/builtin/nodes/tool_get_env.md +2 -0
- package/builtin/nodes/tool_git_checkout.md +1 -0
- package/builtin/nodes/tool_git_worktree_load.md +1 -0
- package/builtin/nodes/tool_git_worktree_unload.md +1 -0
- package/builtin/nodes/tool_gitlab_create_mr.md +1 -0
- package/builtin/nodes/tool_jenkins_build.md +2 -0
- package/builtin/nodes/tool_load_key.md +2 -0
- package/builtin/nodes/tool_nodejs.md +17 -19
- package/builtin/nodes/tool_print.md +2 -0
- package/builtin/nodes/tool_save_key.md +2 -0
- package/builtin/nodes/tool_set_run_env.md +1 -0
- package/builtin/nodes/tool_user_ask.md +2 -0
- package/builtin/nodes/tool_user_check.md +2 -0
- package/builtin/nodes/tool_wecom_send_app_markdown.md +1 -0
- package/builtin/nodes/tool_wecom_send_group_markdown.md +1 -0
- package/builtin/nodes/workspace_one_click_task.md +44 -0
- package/builtin/nodes/workspace_run.md +16 -0
- package/builtin/nodes/workspace_scheduled_run.md +16 -0
- package/builtin/pipelines/module-migrate/scripts/gate.mjs +37 -0
- package/builtin/pipelines/module-migrate/scripts/static-check.mjs +82 -0
- package/builtin/pipelines/module-migrate/workspace.flow.js +172 -0
- package/builtin/pipelines/module-migrate/workspace.layout.json +134 -0
- package/builtin/pipelines/new/scripts/lint-flow.mjs +38 -0
- package/builtin/pipelines/new/workspace.flow.js +91 -0
- package/builtin/pipelines/new/workspace.layout.json +70 -0
- package/builtin/web-ui/dist/assets/{WorkflowAssistantThread-DPbR2UaN.js → WorkflowAssistantThread-CKClwj96.js} +1 -1
- package/builtin/web-ui/dist/assets/index-BZ5KqLur.js +870 -0
- package/builtin/web-ui/dist/assets/index-CEXmmwM2.css +1 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +2 -1
- package/reference/flow-control-capabilities.md +77 -158
- package/reference/flow-layout.md +1 -1
- package/reference/flow-prompt-handler-check.md +2 -2
- package/skills/agentflow-author-flow/SKILL.md +8 -47
- package/skills/agentflow-cli/SKILL.md +158 -25
- package/skills/agentflow-cli/agents/openai.yaml +2 -2
- package/skills/agentflow-cli/scripts/agentflow-cli.mjs +668 -18
- package/skills/agentflow-cli/scripts/agentflow-runtime.mjs +97 -0
- package/skills/agentflow-flow-add-instances/SKILL.md +7 -248
- package/skills/agentflow-flow-dsl/SKILL.md +206 -0
- package/skills/agentflow-flow-dsl/agents/openai.yaml +4 -0
- package/skills/agentflow-flow-dsl/references/node-calls.md +39 -0
- package/skills/agentflow-flow-edit-node-fields/SKILL.md +4 -73
- package/skills/agentflow-flow-recipes/SKILL.md +7 -18
- package/skills/agentflow-flow-recipes/references/recipes.md +97 -43
- package/skills/agentflow-flow-sync-ui/SKILL.md +3 -54
- package/skills/agentflow-node-dsl/SKILL.md +212 -0
- package/skills/agentflow-node-dsl/agents/openai.yaml +4 -0
- package/skills/agentflow-node-reference/SKILL.md +5 -4
- package/skills/agentflow-node-reference/references/builtin-nodes.md +166 -115
- package/skills/agentflow-placeholder-reference/SKILL.md +2 -2
- package/skills/agentflow-runtime-reference/SKILL.md +2 -2
- package/skills/agentflow-runtime-reference/references/runtime.md +1 -1
- package/skills/agentflow-workspace-ascii/SKILL.md +9 -16
- package/skills/agentflow-workspace-graph/SKILL.md +62 -48
- package/skills/agentflow-workspace-html/SKILL.md +7 -2
- package/skills/agentflow-workspace-image/SKILL.md +6 -2
- package/skills/agentflow-workspace-markdown/SKILL.md +14 -21
- package/skills/agentflow-workspace-mermaid/SKILL.md +8 -16
- package/bin/lib/api-runner.mjs +0 -387
- package/bin/lib/apply.mjs +0 -903
- package/bin/lib/composer-flow-instances.mjs +0 -68
- package/bin/lib/composer-flow-skeleton.mjs +0 -334
- package/bin/lib/composer-flow-validate.mjs +0 -47
- package/bin/lib/composer-model-router.mjs +0 -185
- package/bin/lib/composer-node-schema.mjs +0 -303
- package/bin/lib/composer-planner.mjs +0 -751
- package/bin/lib/composer-script-ops.mjs +0 -233
- package/bin/lib/flow-static-preview.mjs +0 -104
- package/bin/lib/hub-login.mjs +0 -54
- package/bin/lib/hub-publish.mjs +0 -159
- package/bin/lib/hub-remote.mjs +0 -189
- package/bin/lib/hub.mjs +0 -299
- package/bin/lib/jenkins.mjs +0 -380
- package/bin/lib/node-execute.mjs +0 -539
- package/bin/lib/normalize-node-tool-command.mjs +0 -97
- package/bin/lib/runtime-context.mjs +0 -243
- package/bin/lib/scheduler.mjs +0 -601
- package/bin/lib/ui-print.mjs +0 -94
- package/bin/pipeline/build-node-prompt.mjs +0 -271
- package/bin/pipeline/check-cache.mjs +0 -191
- package/bin/pipeline/check-flow.mjs +0 -543
- package/bin/pipeline/collect-nodes.mjs +0 -212
- package/bin/pipeline/compute-cache-md5.mjs +0 -177
- package/bin/pipeline/ensure-run-dir.mjs +0 -71
- package/bin/pipeline/gc.mjs +0 -129
- package/bin/pipeline/get-env.mjs +0 -59
- package/bin/pipeline/get-resolved-values.mjs +0 -344
- package/bin/pipeline/load-key.mjs +0 -62
- package/bin/pipeline/parse-flow.mjs +0 -708
- package/bin/pipeline/post-process-control-if.mjs +0 -23
- package/bin/pipeline/post-process-node.mjs +0 -490
- package/bin/pipeline/pre-process-node.mjs +0 -1430
- package/bin/pipeline/resolve-inputs.mjs +0 -201
- package/bin/pipeline/run-tool-nodejs.mjs +0 -167
- package/bin/pipeline/save-key.mjs +0 -93
- package/bin/pipeline/snapshot-prior-round.mjs +0 -70
- package/bin/pipeline/validate-for-ui.mjs +0 -234
- package/bin/pipeline/validate-script-output.mjs +0 -130
- package/bin/pipeline/write-result.mjs +0 -182
- package/builtin/pipelines/module-migrate/flow.yaml +0 -819
- package/builtin/pipelines/new/flow.yaml +0 -545
- package/builtin/pipelines/new/scripts/check-flow.mjs +0 -9
- package/builtin/pipelines/new/scripts/collect-nodes.mjs +0 -211
- package/builtin/web-ui/dist/assets/index-BSCpd5la.js +0 -889
- package/builtin/web-ui/dist/assets/index-DZ328oSo.css +0 -1
- package/skills/agentflow-node-authoring/SKILL.md +0 -57
package/bin/lib/marketplace.mjs
CHANGED
|
@@ -8,7 +8,16 @@ import {
|
|
|
8
8
|
MARKETPLACE_PACKAGES_DIR,
|
|
9
9
|
PIPELINES_DIR,
|
|
10
10
|
getUserPipelinesRoot,
|
|
11
|
+
isFlowDir,
|
|
11
12
|
} from "./paths.mjs";
|
|
13
|
+
import { NODE_PACKAGE_ENTRY, isNodePackageDir, readNodePackageManifest } from "./node-package-manifest.mjs";
|
|
14
|
+
import {
|
|
15
|
+
NODE_PACKAGE_METADATA_FILENAME,
|
|
16
|
+
createNodePackageArchive,
|
|
17
|
+
inspectNodePackageArchive,
|
|
18
|
+
inspectNodePackageDirectory,
|
|
19
|
+
writeNodePackageFiles,
|
|
20
|
+
} from "./node-package-archive.mjs";
|
|
12
21
|
|
|
13
22
|
const NODE_MANIFEST = "node.yaml";
|
|
14
23
|
const COLLECTION_MANIFEST = "collection.yaml";
|
|
@@ -80,6 +89,21 @@ function normalizeSlotList(value) {
|
|
|
80
89
|
});
|
|
81
90
|
}
|
|
82
91
|
|
|
92
|
+
/**
|
|
93
|
+
* 读取节点包清单:`index.mjs` 的静态声明优先,回退 `node.yaml`。
|
|
94
|
+
* 声明写得不合法时不静默吞掉——按目录名报错,否则节点会莫名从面板消失。
|
|
95
|
+
*/
|
|
96
|
+
function readNodeManifestRaw(dir) {
|
|
97
|
+
try {
|
|
98
|
+
const manifest = readNodePackageManifest(dir, readYamlObject);
|
|
99
|
+
const metadata = readJsonObject(path.join(dir, NODE_PACKAGE_METADATA_FILENAME));
|
|
100
|
+
return manifest && metadata ? { ...manifest, ...metadata } : manifest;
|
|
101
|
+
} catch (e) {
|
|
102
|
+
console.warn(`[agentflow] 节点包 ${path.basename(dir)} 清单无效:${(e && e.message) || e}`);
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
83
107
|
function normalizeManifest(raw, packageDir, source = "workspace") {
|
|
84
108
|
if (!raw || typeof raw !== "object") return null;
|
|
85
109
|
const id = raw.id != null ? String(raw.id).trim() : path.basename(packageDir);
|
|
@@ -187,7 +211,7 @@ function collectFlowDirs(rootDir, source, archived = false) {
|
|
|
187
211
|
for (const entry of entries) {
|
|
188
212
|
if (!entry.isDirectory() || entry.name === ARCHIVED_PIPELINES_DIR_NAME) continue;
|
|
189
213
|
const dir = path.join(rootDir, entry.name);
|
|
190
|
-
if (!
|
|
214
|
+
if (!isFlowDir(dir)) continue;
|
|
191
215
|
out.push({ flowId: entry.name, flowSource: source, archived, flowDir: dir });
|
|
192
216
|
}
|
|
193
217
|
return out;
|
|
@@ -263,11 +287,11 @@ function findNodePackageDir(workspaceRoot, id, version) {
|
|
|
263
287
|
const nodeBase = path.join(root, "nodes", id);
|
|
264
288
|
if (version) {
|
|
265
289
|
const direct = path.join(nodeBase, version);
|
|
266
|
-
if (
|
|
290
|
+
if (isNodePackageDir(direct)) return direct;
|
|
267
291
|
} else {
|
|
268
292
|
for (const v of sortVersionsDesc(listVersionDirs(nodeBase))) {
|
|
269
293
|
const direct = path.join(nodeBase, v);
|
|
270
|
-
if (
|
|
294
|
+
if (isNodePackageDir(direct)) return direct;
|
|
271
295
|
}
|
|
272
296
|
}
|
|
273
297
|
return null;
|
|
@@ -289,13 +313,13 @@ function iterCollectionNodeDirs(workspaceRoot, collectionDeps = []) {
|
|
|
289
313
|
for (const entry of fs.readdirSync(nodesRoot, { withFileTypes: true })) {
|
|
290
314
|
if (!entry.isDirectory()) continue;
|
|
291
315
|
const direct = path.join(nodesRoot, entry.name);
|
|
292
|
-
if (
|
|
316
|
+
if (isNodePackageDir(direct)) {
|
|
293
317
|
out.push(direct);
|
|
294
318
|
continue;
|
|
295
319
|
}
|
|
296
320
|
for (const nodeVersion of sortVersionsDesc(listVersionDirs(direct))) {
|
|
297
321
|
const versioned = path.join(direct, nodeVersion);
|
|
298
|
-
if (
|
|
322
|
+
if (isNodePackageDir(versioned)) out.push(versioned);
|
|
299
323
|
}
|
|
300
324
|
}
|
|
301
325
|
}
|
|
@@ -317,6 +341,13 @@ function dependencyVersion(flowData, id) {
|
|
|
317
341
|
return null;
|
|
318
342
|
}
|
|
319
343
|
|
|
344
|
+
/**
|
|
345
|
+
* 只读,没有写入方——写 `agentflow.lock.json` 的是已删除的 `install-node`。
|
|
346
|
+
*
|
|
347
|
+
* 留着是因为磁盘上仍可能有它当年写下的锁文件,那些流程的版本钉应该继续生效。代码化流程
|
|
348
|
+
* 不依赖它:版本钉在实例的 `marketplaceRef`(`marketplace:<id>@<version>`)上,解析时
|
|
349
|
+
* `parsed.version` 先命中,这条兜底根本走不到。
|
|
350
|
+
*/
|
|
320
351
|
function lockVersion(flowDir, id) {
|
|
321
352
|
const lock = readJsonObject(path.join(flowDir, LOCK_FILENAME));
|
|
322
353
|
const entry = lock && lock.nodes && lock.nodes[id];
|
|
@@ -330,18 +361,58 @@ function collectionDeps(flowData) {
|
|
|
330
361
|
return deps && Array.isArray(deps.collections) ? deps.collections : [];
|
|
331
362
|
}
|
|
332
363
|
|
|
364
|
+
/**
|
|
365
|
+
* flow 自带的代码节点包:`<flowDir>/nodes/<dirName>/`,不带版本子目录。
|
|
366
|
+
*
|
|
367
|
+
* 这类包跟着 flow 走——AI 生成流程时可以顺手把节点实现写在流程目录里,不必先发布到
|
|
368
|
+
* marketplace。id 以包声明为准(目录名只是回退),所以目录名和 id 可以不一致。
|
|
369
|
+
*/
|
|
370
|
+
function findFlowLocalNodePackageDir(flowDir, id, requestedVersion) {
|
|
371
|
+
const nodesRoot = flowDir ? path.join(path.resolve(flowDir), "nodes") : "";
|
|
372
|
+
if (!nodesRoot || !fs.existsSync(nodesRoot) || !fs.statSync(nodesRoot).isDirectory()) return null;
|
|
373
|
+
for (const entry of fs.readdirSync(nodesRoot, { withFileTypes: true })) {
|
|
374
|
+
if (!entry.isDirectory()) continue;
|
|
375
|
+
const dir = path.join(nodesRoot, entry.name);
|
|
376
|
+
if (!isNodePackageDir(dir)) continue;
|
|
377
|
+
const manifest = normalizeManifest(readNodeManifestRaw(dir), dir, "flow");
|
|
378
|
+
if (!manifest || manifest.id !== id) continue;
|
|
379
|
+
if (requestedVersion && manifest.version !== requestedVersion) continue;
|
|
380
|
+
return dir;
|
|
381
|
+
}
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* 列出一个 `nodes/` 目录下的所有代码节点包(子目录形式,不带版本层)。
|
|
387
|
+
* 供节点目录把 flow 自带的节点一起摆进面板。
|
|
388
|
+
*/
|
|
389
|
+
export function listNodePackagesInDir(nodesRoot) {
|
|
390
|
+
const out = [];
|
|
391
|
+
const root = String(nodesRoot || "");
|
|
392
|
+
if (!root || !fs.existsSync(root) || !fs.statSync(root).isDirectory()) return out;
|
|
393
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
394
|
+
if (!entry.isDirectory()) continue;
|
|
395
|
+
const dir = path.join(root, entry.name);
|
|
396
|
+
if (!isNodePackageDir(dir)) continue;
|
|
397
|
+
const manifest = normalizeManifest(readNodeManifestRaw(dir), dir, "flow");
|
|
398
|
+
if (manifest) out.push(manifest);
|
|
399
|
+
}
|
|
400
|
+
return out.sort((a, b) => a.id.localeCompare(b.id));
|
|
401
|
+
}
|
|
402
|
+
|
|
333
403
|
export function resolveMarketplaceNodePackage(workspaceRoot, flowDir, definitionId, flowData = null, opts = {}) {
|
|
334
404
|
const parsed = parseMarketplaceDefinitionId(definitionId);
|
|
335
405
|
if (!parsed) return null;
|
|
336
406
|
const id = parsed.id;
|
|
337
407
|
const requestedVersion = parsed.version || dependencyVersion(flowData, id) || lockVersion(flowDir, id);
|
|
338
|
-
|
|
339
|
-
let
|
|
408
|
+
// flow 自带的包优先:流程目录里的实现就是这个流程要用的那份,不该被同名的已发布包顶掉
|
|
409
|
+
let packageDir = findFlowLocalNodePackageDir(flowDir, id, requestedVersion);
|
|
410
|
+
let packageSource = packageDir ? "flow" : "marketplace";
|
|
411
|
+
if (!packageDir) packageDir = findNodePackageDir(workspaceRoot, id, requestedVersion);
|
|
340
412
|
|
|
341
413
|
if (!packageDir) {
|
|
342
414
|
for (const dir of iterCollectionNodeDirs(workspaceRoot, collectionDeps(flowData))) {
|
|
343
|
-
const
|
|
344
|
-
const manifest = normalizeManifest(raw, dir, "collection");
|
|
415
|
+
const manifest = normalizeManifest(readNodeManifestRaw(dir), dir, "collection");
|
|
345
416
|
if (!manifest || manifest.id !== id) continue;
|
|
346
417
|
if (requestedVersion && manifest.version !== requestedVersion) continue;
|
|
347
418
|
packageDir = dir;
|
|
@@ -351,7 +422,7 @@ export function resolveMarketplaceNodePackage(workspaceRoot, flowDir, definition
|
|
|
351
422
|
}
|
|
352
423
|
|
|
353
424
|
if (!packageDir) return null;
|
|
354
|
-
const manifest = normalizeManifest(
|
|
425
|
+
const manifest = normalizeManifest(readNodeManifestRaw(packageDir), packageDir, packageSource);
|
|
355
426
|
if (!manifest) return null;
|
|
356
427
|
if (!canAccessMarketplaceNode(manifest, opts)) return null;
|
|
357
428
|
return {
|
|
@@ -366,7 +437,7 @@ export function listMarketplaceNodes(workspaceRoot, flowData = null, opts = {})
|
|
|
366
437
|
const out = [];
|
|
367
438
|
const seen = new Set();
|
|
368
439
|
const addManifest = (dir, source = "marketplace") => {
|
|
369
|
-
const manifest = normalizeManifest(
|
|
440
|
+
const manifest = normalizeManifest(readNodeManifestRaw(dir), dir, source);
|
|
370
441
|
if (!manifest) return;
|
|
371
442
|
if (!canAccessMarketplaceNode(manifest, opts)) return;
|
|
372
443
|
const key = `${manifest.id}@${manifest.version}`;
|
|
@@ -404,6 +475,15 @@ export function listMarketplacePackages(workspaceRoot, opts = {}) {
|
|
|
404
475
|
inputs: n.input,
|
|
405
476
|
outputs: n.output,
|
|
406
477
|
packagedFiles: Array.isArray(n.packagedFiles) ? n.packagedFiles : [],
|
|
478
|
+
fileList: Array.isArray(n.fileList) ? n.fileList : [],
|
|
479
|
+
fileCount: Number(n.fileCount) || 0,
|
|
480
|
+
totalBytes: Number(n.totalBytes) || 0,
|
|
481
|
+
contentSha256: String(n.contentSha256 || ""),
|
|
482
|
+
archiveSha256: String(n.archiveSha256 || ""),
|
|
483
|
+
installedFrom: String(n.installedFrom || ""),
|
|
484
|
+
installedAt: String(n.installedAt || ""),
|
|
485
|
+
ownerUserId: n.ownerUserId || n.createdBy || "",
|
|
486
|
+
createdBy: n.createdBy || n.ownerUserId || "",
|
|
407
487
|
packageDir: n.packageDir,
|
|
408
488
|
usage: listMarketplaceNodeUsages(workspaceRoot, n.id, n.version, opts),
|
|
409
489
|
}));
|
|
@@ -519,42 +599,126 @@ export function deleteMarketplaceFlowSnippetPackage(workspaceRoot, id, version,
|
|
|
519
599
|
return { ok: true, id, version, packageDir };
|
|
520
600
|
}
|
|
521
601
|
|
|
522
|
-
export function writeFlowMarketplaceLock(workspaceRoot, flowDir, flowData, opts = {}) {
|
|
523
|
-
if (!flowData || !flowData.instances || typeof flowData.instances !== "object") return null;
|
|
524
|
-
const nodes = {};
|
|
525
|
-
for (const inst of Object.values(flowData.instances)) {
|
|
526
|
-
const defId = inst && (inst.marketplaceRef || inst.definitionId);
|
|
527
|
-
const resolved = resolveMarketplaceNodePackage(workspaceRoot, flowDir, defId, flowData, opts);
|
|
528
|
-
if (!resolved) continue;
|
|
529
|
-
nodes[resolved.id] = {
|
|
530
|
-
version: resolved.version,
|
|
531
|
-
resolved: path.relative(flowDir, resolved.packageDir).replace(/\\/g, "/"),
|
|
532
|
-
definitionId: resolved.resolvedDefinitionId,
|
|
533
|
-
};
|
|
534
|
-
}
|
|
535
|
-
const lockPath = path.join(flowDir, LOCK_FILENAME);
|
|
536
|
-
if (Object.keys(nodes).length === 0) return null;
|
|
537
|
-
const lock = readJsonObject(lockPath) || {};
|
|
538
|
-
const next = {
|
|
539
|
-
...lock,
|
|
540
|
-
version: 1,
|
|
541
|
-
updatedAt: new Date().toISOString(),
|
|
542
|
-
nodes: { ...(lock.nodes && typeof lock.nodes === "object" ? lock.nodes : {}), ...nodes },
|
|
543
|
-
};
|
|
544
|
-
fs.writeFileSync(lockPath, JSON.stringify(next, null, 2) + "\n", "utf-8");
|
|
545
|
-
return next;
|
|
546
|
-
}
|
|
547
602
|
|
|
548
|
-
export function publishNodePackage(workspaceRoot, sourceDir) {
|
|
603
|
+
export function publishNodePackage(workspaceRoot, sourceDir, opts = {}) {
|
|
549
604
|
const src = path.resolve(sourceDir);
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
605
|
+
// 走和读取同一条路径:`index.mjs` 的静态声明优先,回落 `node.yaml`。只认后者的话,
|
|
606
|
+
// 一个目录扫描、面板、运行时都跑得通的 index.mjs 包偏偏发布不出去。
|
|
607
|
+
const inspected = inspectNodePackageDirectory(src, { allowLegacyManifest: true });
|
|
608
|
+
if (!inspected.ok) return inspected;
|
|
609
|
+
const manifest = normalizeManifest(inspected.manifest, src);
|
|
610
|
+
if (!manifest) return { ok: false, error: `Invalid node package manifest: ${src}` };
|
|
611
|
+
const dest = resolveWorkspaceNodePackageDir(workspaceRoot, manifest.id, manifest.version);
|
|
612
|
+
if (!dest) return { ok: false, error: "Invalid marketplace node id or version" };
|
|
613
|
+
const existing = fs.existsSync(dest) ? inspectNodePackageDirectory(dest, { allowLegacyManifest: true }) : null;
|
|
614
|
+
if (existing?.ok && existing.contentSha256 === inspected.contentSha256) {
|
|
615
|
+
return {
|
|
616
|
+
ok: true,
|
|
617
|
+
alreadyExists: true,
|
|
618
|
+
id: manifest.id,
|
|
619
|
+
version: manifest.version,
|
|
620
|
+
packageDir: dest,
|
|
621
|
+
definitionId: manifest.definitionId,
|
|
622
|
+
contentSha256: inspected.contentSha256,
|
|
623
|
+
fileList: inspected.fileList,
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
if (existing?.ok && opts.immutable === true) {
|
|
627
|
+
return { ok: false, conflict: true, error: `${manifest.id}@${manifest.version} 已存在且内容不同,请提升版本号` };
|
|
628
|
+
}
|
|
554
629
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
555
630
|
fs.rmSync(dest, { recursive: true, force: true });
|
|
556
631
|
fs.cpSync(src, dest, { recursive: true });
|
|
557
|
-
|
|
632
|
+
const ownerUserId = String(opts.ownerUserId || opts.userId || manifest.ownerUserId || manifest.createdBy || "").trim();
|
|
633
|
+
fs.writeFileSync(path.join(dest, NODE_PACKAGE_METADATA_FILENAME), `${JSON.stringify({
|
|
634
|
+
...(ownerUserId ? { ownerUserId, createdBy: ownerUserId } : {}),
|
|
635
|
+
contentSha256: inspected.contentSha256,
|
|
636
|
+
fileList: inspected.fileList,
|
|
637
|
+
fileCount: inspected.fileCount,
|
|
638
|
+
totalBytes: inspected.totalBytes,
|
|
639
|
+
publishedAt: new Date().toISOString(),
|
|
640
|
+
}, null, 2)}\n`, "utf-8");
|
|
641
|
+
return {
|
|
642
|
+
ok: true,
|
|
643
|
+
id: manifest.id,
|
|
644
|
+
version: manifest.version,
|
|
645
|
+
packageDir: dest,
|
|
646
|
+
definitionId: manifest.definitionId,
|
|
647
|
+
contentSha256: inspected.contentSha256,
|
|
648
|
+
fileList: inspected.fileList,
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
export function publishNodePackageArchive(workspaceRoot, archiveInput, opts = {}) {
|
|
653
|
+
const inspected = inspectNodePackageArchive(archiveInput);
|
|
654
|
+
if (!inspected.ok) return inspected;
|
|
655
|
+
const manifest = normalizeManifest(inspected.manifest, "");
|
|
656
|
+
if (!manifest) return { ok: false, error: "Invalid node package manifest" };
|
|
657
|
+
const dest = resolveWorkspaceNodePackageDir(workspaceRoot, manifest.id, manifest.version);
|
|
658
|
+
if (!dest) return { ok: false, error: "Invalid marketplace node id or version" };
|
|
659
|
+
const existing = fs.existsSync(dest) ? inspectNodePackageDirectory(dest) : null;
|
|
660
|
+
if (existing?.ok) {
|
|
661
|
+
if (existing.contentSha256 !== inspected.contentSha256) {
|
|
662
|
+
return { ok: false, conflict: true, error: `${manifest.id}@${manifest.version} 已存在且内容不同,请提升版本号` };
|
|
663
|
+
}
|
|
664
|
+
return {
|
|
665
|
+
ok: true,
|
|
666
|
+
alreadyExists: true,
|
|
667
|
+
id: manifest.id,
|
|
668
|
+
version: manifest.version,
|
|
669
|
+
definitionId: manifest.definitionId,
|
|
670
|
+
packageDir: dest,
|
|
671
|
+
contentSha256: inspected.contentSha256,
|
|
672
|
+
archiveSha256: inspected.archiveSha256,
|
|
673
|
+
fileList: inspected.fileList,
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
const parent = path.dirname(dest);
|
|
677
|
+
fs.mkdirSync(parent, { recursive: true });
|
|
678
|
+
// 先在同一父目录完整写好再 rename。这样进程中断或磁盘异常时不会留下一个被目录扫描到、
|
|
679
|
+
// 但脚本只写了一半的版本。
|
|
680
|
+
const staging = fs.mkdtempSync(path.join(parent, `.${manifest.version}.installing-`));
|
|
681
|
+
const ownerUserId = String(opts.ownerUserId || opts.userId || "").trim();
|
|
682
|
+
const installedFrom = String(opts.installedFrom || "").trim();
|
|
683
|
+
const installedAt = String(opts.installedAt || "").trim();
|
|
684
|
+
try {
|
|
685
|
+
const written = writeNodePackageFiles(staging, inspected.files);
|
|
686
|
+
if (!written.ok) return written;
|
|
687
|
+
fs.writeFileSync(path.join(staging, NODE_PACKAGE_METADATA_FILENAME), `${JSON.stringify({
|
|
688
|
+
...(ownerUserId ? { ownerUserId, createdBy: ownerUserId } : {}),
|
|
689
|
+
contentSha256: inspected.contentSha256,
|
|
690
|
+
archiveSha256: inspected.archiveSha256,
|
|
691
|
+
fileList: inspected.fileList,
|
|
692
|
+
fileCount: inspected.fileCount,
|
|
693
|
+
totalBytes: inspected.totalBytes,
|
|
694
|
+
publishedAt: new Date().toISOString(),
|
|
695
|
+
...(installedFrom ? { installedFrom } : {}),
|
|
696
|
+
...(installedAt ? { installedAt } : {}),
|
|
697
|
+
}, null, 2)}\n`, "utf-8");
|
|
698
|
+
fs.renameSync(staging, dest);
|
|
699
|
+
} catch (error) {
|
|
700
|
+
return { ok: false, error: error?.message || String(error) };
|
|
701
|
+
} finally {
|
|
702
|
+
if (fs.existsSync(staging)) fs.rmSync(staging, { recursive: true, force: true });
|
|
703
|
+
}
|
|
704
|
+
return {
|
|
705
|
+
ok: true,
|
|
706
|
+
id: manifest.id,
|
|
707
|
+
version: manifest.version,
|
|
708
|
+
definitionId: manifest.definitionId,
|
|
709
|
+
packageDir: dest,
|
|
710
|
+
contentSha256: inspected.contentSha256,
|
|
711
|
+
archiveSha256: inspected.archiveSha256,
|
|
712
|
+
fileList: inspected.fileList,
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
export function nodePackageArchive(workspaceRoot, id, version, opts = {}) {
|
|
717
|
+
const packageDir = resolveWorkspaceNodePackageDir(workspaceRoot, id, version);
|
|
718
|
+
if (!packageDir || !fs.existsSync(packageDir)) return { ok: false, error: `Node package not found: ${id}@${version}` };
|
|
719
|
+
const manifest = normalizeManifest(readNodeManifestRaw(packageDir), packageDir, "marketplace");
|
|
720
|
+
if (!manifest || !canAccessMarketplaceNode(manifest, opts)) return { ok: false, error: "Node package permission denied" };
|
|
721
|
+
return createNodePackageArchive(packageDir);
|
|
558
722
|
}
|
|
559
723
|
|
|
560
724
|
function safePackageId(raw) {
|
|
@@ -907,22 +1071,3 @@ export function publishFlowSnippet(workspaceRoot, payload = {}, opts = {}) {
|
|
|
907
1071
|
);
|
|
908
1072
|
return { ok: true, id, version, packageDir: dest, snippet: manifest.snippet };
|
|
909
1073
|
}
|
|
910
|
-
|
|
911
|
-
export function installFlowDependency(workspaceRoot, flowDir, spec, opts = {}) {
|
|
912
|
-
const parsed = parseMarketplaceDefinitionId(spec.startsWith("marketplace:") ? spec : `marketplace:${spec}`);
|
|
913
|
-
if (!parsed) return { ok: false, error: `Invalid marketplace node spec: ${spec}` };
|
|
914
|
-
const resolved = resolveMarketplaceNodePackage(workspaceRoot, flowDir, `marketplace:${parsed.id}${parsed.version ? `@${parsed.version}` : ""}`, { dependencies: {} }, opts);
|
|
915
|
-
if (!resolved) return { ok: false, error: `Marketplace node not found: ${spec}` };
|
|
916
|
-
|
|
917
|
-
const flowYamlPath = path.join(flowDir, "flow.yaml");
|
|
918
|
-
const data = readYamlObject(flowYamlPath);
|
|
919
|
-
if (!data) return { ok: false, error: `Invalid flow.yaml: ${flowYamlPath}` };
|
|
920
|
-
const deps = data.dependencies && typeof data.dependencies === "object" ? data.dependencies : {};
|
|
921
|
-
const nodes = Array.isArray(deps.nodes) ? deps.nodes : [];
|
|
922
|
-
const exists = nodes.some((item) => (typeof item === "string" ? item === resolved.id : item && item.id === resolved.id));
|
|
923
|
-
if (!exists) nodes.push({ id: resolved.id, version: resolved.version });
|
|
924
|
-
data.dependencies = { ...deps, nodes };
|
|
925
|
-
fs.writeFileSync(flowYamlPath, yaml.dump(data, { lineWidth: -1 }), "utf-8");
|
|
926
|
-
writeFlowMarketplaceLock(workspaceRoot, flowDir, data, opts);
|
|
927
|
-
return { ok: true, id: resolved.id, version: resolved.version, definitionId: `marketplace:${resolved.id}@${resolved.version}` };
|
|
928
|
-
}
|
package/bin/lib/model-config.mjs
CHANGED
|
@@ -150,3 +150,13 @@ export function resolveCliAndModel(workspaceRoot, nodeModel, agentModelOverride)
|
|
|
150
150
|
label: model === "Auto" ? "cursor: Auto" : `cursor: ${model}`,
|
|
151
151
|
};
|
|
152
152
|
}
|
|
153
|
+
|
|
154
|
+
/** 解析 `api:<provider>/<model>` 形式的模型串;无 provider 时默认 openai。 */
|
|
155
|
+
export function parseApiModel(str) {
|
|
156
|
+
const s = String(str ?? "").replace(/^api:/, "");
|
|
157
|
+
const slash = s.indexOf("/");
|
|
158
|
+
if (slash < 0) return { provider: "openai", model: s };
|
|
159
|
+
const provider = s.slice(0, slash).toLowerCase();
|
|
160
|
+
const model = s.slice(slash + 1);
|
|
161
|
+
return { provider, model };
|
|
162
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Portable node-package archives.
|
|
3
|
+
*
|
|
4
|
+
* On disk a node package is a directory. ZIP is only the transport form. The
|
|
5
|
+
* archive always has `index.mjs` at its root and may contain relative scripts,
|
|
6
|
+
* templates, and assets. We validate paths and size limits before either
|
|
7
|
+
* packing or installing so an uploaded package cannot escape its destination.
|
|
8
|
+
*/
|
|
9
|
+
import crypto from "node:crypto";
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
|
|
14
|
+
import { unzipSync, zipSync } from "fflate";
|
|
15
|
+
import yaml from "js-yaml";
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
NODE_PACKAGE_ENTRY,
|
|
19
|
+
nodePackageExportsRun,
|
|
20
|
+
readNodePackageManifest,
|
|
21
|
+
} from "./node-package-manifest.mjs";
|
|
22
|
+
|
|
23
|
+
export const NODE_PACKAGE_MAX_UNCOMPRESSED_BYTES = 8 * 1024 * 1024;
|
|
24
|
+
export const NODE_PACKAGE_MAX_FILE_ENTRIES = 500;
|
|
25
|
+
export const NODE_PACKAGE_MAX_ARCHIVE_BYTES = 10 * 1024 * 1024;
|
|
26
|
+
export const NODE_PACKAGE_METADATA_FILENAME = ".agentflow-package.json";
|
|
27
|
+
|
|
28
|
+
const FORBIDDEN_SEGMENTS = new Set([
|
|
29
|
+
".git",
|
|
30
|
+
".ssh",
|
|
31
|
+
"__MACOSX",
|
|
32
|
+
"node_modules",
|
|
33
|
+
]);
|
|
34
|
+
const FORBIDDEN_FILES = new Set([
|
|
35
|
+
".env",
|
|
36
|
+
".env.local",
|
|
37
|
+
".npmrc",
|
|
38
|
+
".DS_Store",
|
|
39
|
+
NODE_PACKAGE_METADATA_FILENAME,
|
|
40
|
+
"id_rsa",
|
|
41
|
+
"id_ed25519",
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
export function sanitizeNodePackageEntryPath(raw) {
|
|
45
|
+
if (typeof raw !== "string") return null;
|
|
46
|
+
// ZIP 规范只使用 `/`。不能把绝对路径或反斜杠悄悄“修正”为相对路径,否则两个不同的
|
|
47
|
+
// archive entry 可能在安装时落到同一个文件,路径穿越也会从拒绝变成接受。
|
|
48
|
+
const value = raw.replace(/^\uFEFF/, "");
|
|
49
|
+
if (!value || value.includes("\0") || value.includes("\\") || value.startsWith("/") || /^[A-Za-z]:/.test(value)) return null;
|
|
50
|
+
const parts = value.split("/").filter(Boolean);
|
|
51
|
+
if (!parts.length || parts.some((part) => part === "." || part === "..")) return null;
|
|
52
|
+
if (parts.some((part) => FORBIDDEN_SEGMENTS.has(part))) return null;
|
|
53
|
+
const base = parts.at(-1);
|
|
54
|
+
if (FORBIDDEN_FILES.has(base) || /^\.env\./.test(base) || /\.(?:pem|key|p12|pfx)$/i.test(base)) return null;
|
|
55
|
+
if (parts.some((part) => part.startsWith("._"))) return null;
|
|
56
|
+
return parts.join("/");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function stableContentHash(files) {
|
|
60
|
+
const hash = crypto.createHash("sha256");
|
|
61
|
+
for (const [rel, content] of [...files.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
62
|
+
hash.update(rel);
|
|
63
|
+
hash.update("\0");
|
|
64
|
+
hash.update(content);
|
|
65
|
+
hash.update("\0");
|
|
66
|
+
}
|
|
67
|
+
return hash.digest("hex");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function validateFilesMap(files, opts = {}) {
|
|
71
|
+
if (!(files instanceof Map) || files.size === 0) {
|
|
72
|
+
return { ok: false, error: "节点包内没有文件" };
|
|
73
|
+
}
|
|
74
|
+
if (files.size > NODE_PACKAGE_MAX_FILE_ENTRIES) {
|
|
75
|
+
return { ok: false, error: `节点包文件数量超过限制(最多 ${NODE_PACKAGE_MAX_FILE_ENTRIES} 个)` };
|
|
76
|
+
}
|
|
77
|
+
let totalBytes = 0;
|
|
78
|
+
for (const [rel, content] of files) {
|
|
79
|
+
const safe = sanitizeNodePackageEntryPath(rel);
|
|
80
|
+
if (!safe || safe !== rel) return { ok: false, error: `节点包包含非法或敏感路径:${rel}` };
|
|
81
|
+
totalBytes += content.length;
|
|
82
|
+
if (totalBytes > NODE_PACKAGE_MAX_UNCOMPRESSED_BYTES) {
|
|
83
|
+
return { ok: false, error: "节点包解压后总大小超过限制(8MB)" };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (!files.has(NODE_PACKAGE_ENTRY) && !(opts.allowLegacyManifest && files.has("node.yaml"))) {
|
|
87
|
+
return { ok: false, error: `节点包根目录缺少 ${NODE_PACKAGE_ENTRY}(旧包可使用 node.yaml)` };
|
|
88
|
+
}
|
|
89
|
+
return { ok: true, totalBytes };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function collectDirectoryFiles(packageDir) {
|
|
93
|
+
const root = path.resolve(packageDir);
|
|
94
|
+
const files = new Map();
|
|
95
|
+
const walk = (dir) => {
|
|
96
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
97
|
+
for (const entry of entries) {
|
|
98
|
+
if (entry.name === NODE_PACKAGE_METADATA_FILENAME) continue;
|
|
99
|
+
const abs = path.join(dir, entry.name);
|
|
100
|
+
const stat = fs.lstatSync(abs);
|
|
101
|
+
if (stat.isSymbolicLink()) throw new Error(`节点包不允许符号链接:${path.relative(root, abs)}`);
|
|
102
|
+
const rel = path.relative(root, abs).replace(/\\/g, "/");
|
|
103
|
+
const safe = sanitizeNodePackageEntryPath(rel);
|
|
104
|
+
if (!safe || safe !== rel) throw new Error(`节点包包含非法或敏感路径:${rel}`);
|
|
105
|
+
if (stat.isDirectory()) {
|
|
106
|
+
walk(abs);
|
|
107
|
+
} else if (stat.isFile()) {
|
|
108
|
+
files.set(rel, fs.readFileSync(abs));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
walk(root);
|
|
113
|
+
return files;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function manifestFromTemporaryFiles(files, opts = {}) {
|
|
117
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "agentflow-node-package-"));
|
|
118
|
+
try {
|
|
119
|
+
for (const [rel, content] of files) {
|
|
120
|
+
const abs = path.join(tempRoot, ...rel.split("/"));
|
|
121
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
122
|
+
fs.writeFileSync(abs, content);
|
|
123
|
+
}
|
|
124
|
+
const manifest = readNodePackageManifest(tempRoot, (yamlPath) => {
|
|
125
|
+
if (!opts.allowLegacyManifest || !fs.existsSync(yamlPath)) return null;
|
|
126
|
+
const parsed = yaml.load(fs.readFileSync(yamlPath, "utf-8"));
|
|
127
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
128
|
+
});
|
|
129
|
+
if (!manifest) return { ok: false, error: `${NODE_PACKAGE_ENTRY} 缺少有效的 export default 声明,node.yaml 也无有效清单` };
|
|
130
|
+
if (files.has(NODE_PACKAGE_ENTRY) && !nodePackageExportsRun(path.join(tempRoot, NODE_PACKAGE_ENTRY))) {
|
|
131
|
+
return { ok: false, error: `${NODE_PACKAGE_ENTRY} 缺少 export function run` };
|
|
132
|
+
}
|
|
133
|
+
return { ok: true, manifest };
|
|
134
|
+
} catch (error) {
|
|
135
|
+
return { ok: false, error: error?.message || String(error) };
|
|
136
|
+
} finally {
|
|
137
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function inspectNodePackageDirectory(packageDir, opts = {}) {
|
|
142
|
+
try {
|
|
143
|
+
const root = path.resolve(packageDir);
|
|
144
|
+
if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) {
|
|
145
|
+
return { ok: false, error: `节点包目录不存在:${root}` };
|
|
146
|
+
}
|
|
147
|
+
const files = collectDirectoryFiles(root);
|
|
148
|
+
const checked = validateFilesMap(files, opts);
|
|
149
|
+
if (!checked.ok) return checked;
|
|
150
|
+
const parsed = manifestFromTemporaryFiles(files, opts);
|
|
151
|
+
if (!parsed.ok) return parsed;
|
|
152
|
+
return {
|
|
153
|
+
ok: true,
|
|
154
|
+
packageDir: root,
|
|
155
|
+
manifest: parsed.manifest,
|
|
156
|
+
files,
|
|
157
|
+
fileList: [...files.keys()].sort(),
|
|
158
|
+
fileCount: files.size,
|
|
159
|
+
totalBytes: checked.totalBytes,
|
|
160
|
+
contentSha256: stableContentHash(files),
|
|
161
|
+
};
|
|
162
|
+
} catch (error) {
|
|
163
|
+
return { ok: false, error: error?.message || String(error) };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function createNodePackageArchive(packageDir) {
|
|
168
|
+
const inspected = inspectNodePackageDirectory(packageDir);
|
|
169
|
+
if (!inspected.ok) return inspected;
|
|
170
|
+
const entries = Object.fromEntries([...inspected.files].map(([rel, content]) => [rel, new Uint8Array(content)]));
|
|
171
|
+
const archive = Buffer.from(zipSync(entries, { level: 6 }));
|
|
172
|
+
if (archive.length > NODE_PACKAGE_MAX_ARCHIVE_BYTES) {
|
|
173
|
+
return { ok: false, error: "节点包 ZIP 超过限制(10MB)" };
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
...inspected,
|
|
177
|
+
archive,
|
|
178
|
+
archiveSha256: crypto.createHash("sha256").update(archive).digest("hex"),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function stripOptionalRoot(files) {
|
|
183
|
+
const roots = new Set([...files.keys()].map((rel) => rel.split("/")[0]));
|
|
184
|
+
if (roots.size !== 1 || files.has(NODE_PACKAGE_ENTRY)) return files;
|
|
185
|
+
const [root] = [...roots];
|
|
186
|
+
const prefix = `${root}/`;
|
|
187
|
+
if (!files.has(`${prefix}${NODE_PACKAGE_ENTRY}`)) return files;
|
|
188
|
+
return new Map([...files].map(([rel, content]) => [rel.slice(prefix.length), content]));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function inspectNodePackageArchive(input) {
|
|
192
|
+
try {
|
|
193
|
+
const archive = Buffer.from(input || []);
|
|
194
|
+
if (!archive.length) return { ok: false, error: "节点包 ZIP 为空" };
|
|
195
|
+
if (archive.length > NODE_PACKAGE_MAX_ARCHIVE_BYTES) return { ok: false, error: "节点包 ZIP 超过限制(10MB)" };
|
|
196
|
+
const raw = unzipSync(new Uint8Array(archive));
|
|
197
|
+
let files = new Map();
|
|
198
|
+
for (const [rawRel, value] of Object.entries(raw)) {
|
|
199
|
+
if (rawRel.endsWith("/")) continue;
|
|
200
|
+
const rel = sanitizeNodePackageEntryPath(rawRel);
|
|
201
|
+
if (!rel) return { ok: false, error: `节点包包含非法或敏感路径:${rawRel}` };
|
|
202
|
+
if (files.has(rel)) return { ok: false, error: `节点包包含重复路径:${rawRel}` };
|
|
203
|
+
files.set(rel, Buffer.from(value));
|
|
204
|
+
}
|
|
205
|
+
files = stripOptionalRoot(files);
|
|
206
|
+
const checked = validateFilesMap(files);
|
|
207
|
+
if (!checked.ok) return checked;
|
|
208
|
+
const parsed = manifestFromTemporaryFiles(files);
|
|
209
|
+
if (!parsed.ok) return parsed;
|
|
210
|
+
return {
|
|
211
|
+
ok: true,
|
|
212
|
+
manifest: parsed.manifest,
|
|
213
|
+
files,
|
|
214
|
+
fileList: [...files.keys()].sort(),
|
|
215
|
+
fileCount: files.size,
|
|
216
|
+
totalBytes: checked.totalBytes,
|
|
217
|
+
contentSha256: stableContentHash(files),
|
|
218
|
+
archiveSha256: crypto.createHash("sha256").update(archive).digest("hex"),
|
|
219
|
+
archive,
|
|
220
|
+
};
|
|
221
|
+
} catch (error) {
|
|
222
|
+
return { ok: false, error: `节点包 ZIP 解析失败:${error?.message || String(error)}` };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function writeNodePackageFiles(targetDir, files) {
|
|
227
|
+
const target = path.resolve(targetDir);
|
|
228
|
+
const checked = validateFilesMap(files);
|
|
229
|
+
if (!checked.ok) return checked;
|
|
230
|
+
fs.mkdirSync(target, { recursive: true });
|
|
231
|
+
for (const [rel, content] of files) {
|
|
232
|
+
const abs = path.resolve(target, ...rel.split("/"));
|
|
233
|
+
if (abs !== target && !abs.startsWith(`${target}${path.sep}`)) {
|
|
234
|
+
return { ok: false, error: `节点包路径越界:${rel}` };
|
|
235
|
+
}
|
|
236
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
237
|
+
fs.writeFileSync(abs, content);
|
|
238
|
+
}
|
|
239
|
+
return { ok: true, totalBytes: checked.totalBytes };
|
|
240
|
+
}
|