@fastgpt-plugin/cli 0.1.0-beta.3 → 0.1.0-beta.5

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/dist/index.js CHANGED
@@ -1,262 +1,203 @@
1
1
  #!/usr/bin/env node
2
- import { consola } from "consola/basic";
3
- import { Command } from "commander";
4
- import path from "node:path";
2
+ import { registerHooks, stripTypeScriptTypes } from "node:module";
5
3
  import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
+ import { consola } from "consola/basic";
6
7
  import { build } from "tsdown";
7
- import { Visitor, parseSync } from "oxc-parser";
8
- import { createHash } from "node:crypto";
8
+ import z from "zod";
9
9
  import { input, select } from "@inquirer/prompts";
10
10
  import { kebabCase } from "es-toolkit";
11
- import { createWriteStream } from "node:fs";
11
+ import { createHash, randomUUID } from "node:crypto";
12
+ import fs$1, { createWriteStream } from "node:fs";
13
+ import { Readable } from "stream";
14
+ import { Readable as Readable$1 } from "node:stream";
12
15
  import { ZipFile } from "yazl";
16
+ import { Command } from "commander";
13
17
 
14
18
  //#region src/helpers.ts
15
19
  const logger = consola;
16
20
 
17
21
  //#endregion
18
- //#region package.json
19
- var name = "@fastgpt-plugin/cli";
20
- var version = "0.1.0-beta.3";
22
+ //#region ../../packages/domain/src/value-objects/i18n-string.vo.ts
23
+ const I18nLangSchema = z.enum([
24
+ "en",
25
+ "zh-CN",
26
+ "zh-Hant"
27
+ ]);
28
+ const I18nLangEnum = I18nLangSchema.enum;
29
+ const I18nStringSchema = z.object({
30
+ [I18nLangEnum.en]: z.string(),
31
+ [I18nLangEnum["zh-CN"]]: z.string().optional(),
32
+ [I18nLangEnum["zh-Hant"]]: z.string().optional()
33
+ });
34
+ const I18nStringStrictSchema = z.object({
35
+ [I18nLangEnum.en]: z.string(),
36
+ [I18nLangEnum["zh-CN"]]: z.string(),
37
+ [I18nLangEnum["zh-Hant"]]: z.string()
38
+ });
21
39
 
22
40
  //#endregion
23
- //#region src/constants.ts
24
- const CLI_NAME = name;
25
- const CLI_VERSION = version;
26
- const DEFAULT_PLUGIN_NAME = "./packages/my-tool";
27
- const DEFAULT_PLUGIN_DESCRIPTION = "This is a FastGPT plugin";
28
- const TOOL_TEMPLATES_DIR = path.join(import.meta.dirname, "../templates");
41
+ //#region ../../packages/domain/src/value-objects/permission.vo.ts
42
+ const PluginPermissionEnumSchema = z.enum([
43
+ "userInfo:read",
44
+ "teamInfo:read",
45
+ "model:read",
46
+ "dataset:read",
47
+ "file-upload:allow"
48
+ ]);
49
+ const PluginPermissionEnum = PluginPermissionEnumSchema.enum;
29
50
 
30
51
  //#endregion
31
- //#region src/commands/base.ts
32
- /**
33
- * 所有子命令的基类:
34
- * - 统一命令接口类型
35
- * - 强制实现 register 方法,用于向 Commander 注册自身
36
- */
37
- var BaseCommand = class {};
52
+ //#region ../../packages/domain/src/value-objects/plugin.vo.ts
53
+ const PluginUniqueIdSchema = z.object({
54
+ pluginId: z.string(),
55
+ version: z.string(),
56
+ etag: z.string()
57
+ });
58
+ const PluginSourceSchema = z.literal("system").or(z.string());
59
+ const PluginTagListSchema = z.array(z.record(z.string(), I18nStringStrictSchema));
60
+ const PluginRuntimeModeSchema = z.enum(["localPool", "serverless"]);
61
+ const PluginRuntimeModeEnum = PluginRuntimeModeSchema.enum;
62
+ const UserPluginIdSchema = z.object({
63
+ pluginId: z.string(),
64
+ version: z.string().optional(),
65
+ source: PluginSourceSchema.optional()
66
+ });
38
67
 
39
68
  //#endregion
40
- //#region src/build/etag.ts
41
- /**
42
- * 计算单工具的 etag。
43
- *
44
- * 规则:
45
- * - 仅计算 src/ 目录下的源码文件
46
- * - 排除测试相关文件:
47
- * - 文件名包含 .test. 或 .spec.
48
- * - 位于 __tests__ 目录中的文件
49
- * - 参与计算的每一行格式为:
50
- * <relative-path>:<file-content-md5>\n
51
- * 其中 relative-path 相对于 rootDir,统一使用 / 作为分隔符。
52
- * - 对拼接后的所有行再次计算 md5,得到最终 etag。
53
- */
54
- async function computeSingleToolEtag(options) {
55
- const srcDir = path.join(options.rootDir, "src");
56
- if (!await pathExists$1(srcDir)) return md5("");
57
- const files = [];
58
- const walk = async (dir) => {
59
- const entries = await fs.readdir(dir, { withFileTypes: true });
60
- for (const entry of entries) {
61
- const full = path.join(dir, entry.name);
62
- if (entry.isDirectory()) {
63
- if (entry.name === "__tests__") continue;
64
- await walk(full);
65
- continue;
66
- }
67
- if (!entry.isFile()) continue;
68
- const filename = entry.name;
69
- if (filename.includes(".test.") || filename.includes(".spec.")) continue;
70
- const relFromRoot = path.relative(options.rootDir, full).split(path.sep).join("/");
71
- if (!relFromRoot.startsWith("src/")) continue;
72
- files.push({
73
- rel: relFromRoot,
74
- abs: full
75
- });
76
- }
77
- };
78
- await walk(srcDir);
79
- files.sort((a, b) => a.rel.localeCompare(b.rel));
80
- let aggregate = "";
81
- for (const file of files) {
82
- const fileHash = md5(await fs.readFile(file.abs));
83
- aggregate += `${file.rel}:${fileHash}\n`;
84
- }
85
- return md5(aggregate);
86
- }
87
- /**
88
- * 计算工具集的 etag。
89
- *
90
- * 规则:
91
- * - 首先为每个子工具单独计算 etag(同单工具规则)
92
- * - 然后按子工具名称排序,拼接行:
93
- * <tool-suite-id>/<child-name>:<child-etag>\n
94
- * - 对拼接后的所有行再次计算 md5,得到工具集的 etag。
95
- */
96
- async function computeToolSuiteEtag(options) {
97
- const { rootDir, toolId } = options;
98
- const children = [...options.children].sort();
99
- const lines = [];
100
- for (const child of children) {
101
- const childEtag = await computeSingleToolEtag({ rootDir: path.join(rootDir, "children", child) });
102
- lines.push(`${toolId}/${child}:${childEtag}`);
103
- }
104
- return md5(lines.length > 0 ? `${lines.join("\n")}\n` : "");
105
- }
106
- async function pathExists$1(p) {
107
- try {
108
- await fs.access(p);
109
- return true;
110
- } catch {
111
- return false;
112
- }
113
- }
114
- function md5(input) {
115
- const hash = createHash("md5");
116
- hash.update(input);
117
- return hash.digest("hex");
118
- }
69
+ //#region ../../packages/domain/src/entities/plugin-base.entity.ts
70
+ const PluginTagSchema = z.enum([
71
+ "tools",
72
+ "search",
73
+ "multimodal",
74
+ "communication",
75
+ "finance",
76
+ "design",
77
+ "productivity",
78
+ "news",
79
+ "entertainment",
80
+ "social",
81
+ "scientific",
82
+ "other"
83
+ ]);
84
+ const PluginTagEnum = PluginTagSchema.enum;
85
+ const PluginTypeSchema = z.enum(["tool"]);
86
+ const PluginTypeEnum = PluginTypeSchema.enum;
87
+ const PluginStatusEnumSchema = z.enum([
88
+ "active",
89
+ "pending",
90
+ "disabled"
91
+ ]);
92
+ const PluginStatusEnum = PluginStatusEnumSchema.enum;
93
+ const PluginBaseSchema = z.object({
94
+ pluginId: z.string(),
95
+ version: z.string(),
96
+ etag: z.string(),
97
+ type: PluginTypeSchema,
98
+ author: z.string().optional(),
99
+ repoUrl: z.string().optional(),
100
+ name: I18nStringSchema,
101
+ icon: z.string(),
102
+ tutorialUrl: z.url().optional(),
103
+ readmeUrl: z.url().optional(),
104
+ description: I18nStringSchema,
105
+ tags: z.array(PluginTagSchema).optional(),
106
+ versionDescription: I18nStringSchema.optional(),
107
+ permission: z.array(PluginPermissionEnumSchema).optional()
108
+ });
119
109
 
120
110
  //#endregion
121
- //#region src/build/ast-transform.ts
122
- function extractToolId(filePath) {
123
- const stack = filePath.split("/");
124
- if (stack.at(-3) === "children") return `${stack.at(-4)}/${stack.at(-2)}`;
125
- return stack.at(-2) || "unknown";
126
- }
127
- async function getChildrenList(configDir) {
128
- const childrenDir = path.join(configDir, "children");
129
- try {
130
- await fs.access(childrenDir);
131
- } catch {
132
- return [];
133
- }
134
- try {
135
- return await fs.readdir(childrenDir);
136
- } catch {
137
- return [];
138
- }
139
- }
140
- function hasProperty(properties, keyName) {
141
- return properties.some((prop) => prop.type === "Property" && prop.key?.type === "Identifier" && prop.key?.name === keyName);
142
- }
143
- function applyModifications(sourceCode, modifications) {
144
- const sorted = modifications.sort((a, b) => b.start - a.start);
145
- let result = sourceCode;
146
- for (const mod of sorted) result = result.slice(0, mod.start) + mod.content + result.slice(mod.end);
147
- return result;
148
- }
149
- async function transformToolConfig({ sourceCode, filePath }) {
150
- if (sourceCode.includes("toolId:")) return {
151
- code: sourceCode,
152
- hasToolId: true
153
- };
154
- const toolId = extractToolId(filePath);
155
- const configDir = path.dirname(filePath);
156
- const childrenList = await getChildrenList(configDir);
157
- const singleToolEtag = await computeSingleToolEtag({ rootDir: configDir });
158
- const toolSuiteEtag = childrenList.length > 0 ? await computeToolSuiteEtag({
159
- rootDir: configDir,
160
- toolId,
161
- children: childrenList
162
- }) : null;
163
- const result = parseSync(path.basename(filePath), sourceCode, { sourceType: "module" });
164
- if (result.errors.length > 0) throw new Error(`Parse errors: ${result.errors.map((e) => e.message).join(", ")}`);
165
- const modifications = [];
166
- const targetCallExpressions = [];
167
- new Visitor({ CallExpression(node) {
168
- if (node.callee?.type === "Identifier" && (node.callee.name === "defineTool" || node.callee.name === "defineToolSet") && node.arguments?.[0]?.type === "ObjectExpression") targetCallExpressions.push(node);
169
- } }).visit(result.program);
170
- for (const node of targetCallExpressions) {
171
- const firstArg = node.arguments[0];
172
- if (firstArg?.type !== "ObjectExpression") continue;
173
- const objExpr = firstArg;
174
- const properties = objExpr.properties || [];
175
- const isToolSet = node.callee?.type === "Identifier" && node.callee.name === "defineToolSet";
176
- const needsToolId = !hasProperty(properties, "toolId");
177
- const needsEtag = !hasProperty(properties, "etag");
178
- const needsChildren = isToolSet && !hasProperty(properties, "children") && childrenList.length > 0;
179
- if (!needsToolId && !needsEtag && !needsChildren) continue;
180
- const lastProperty = properties[properties.length - 1];
181
- const insertPos = lastProperty ? lastProperty.end : (objExpr.start ?? 0) + 1;
182
- const newProperties = [];
183
- if (needsToolId) newProperties.push(`toolId: '${toolId}'`);
184
- if (needsEtag) {
185
- const etagValue = isToolSet && toolSuiteEtag ? toolSuiteEtag : singleToolEtag;
186
- newProperties.push(`etag: '${etagValue}'`);
187
- }
188
- if (needsChildren) {
189
- const childrenArray = childrenList.map((child) => child).join(", ");
190
- newProperties.push(`children: [${childrenArray}]`);
191
- const importStatements = childrenList.map((child) => `import ${child} from './children/${child}';`).join("\n");
192
- modifications.push({
193
- start: 0,
194
- end: 0,
195
- content: importStatements + "\n"
196
- });
197
- }
198
- if (newProperties.length > 0) {
199
- const content = (lastProperty ? ",\n " : "\n ") + newProperties.join(",\n ");
200
- modifications.push({
201
- start: insertPos,
202
- end: insertPos,
203
- content
204
- });
205
- }
206
- }
207
- if (modifications.length === 0) return {
208
- code: sourceCode,
209
- hasToolId: false
210
- };
211
- return {
212
- code: applyModifications(sourceCode, modifications),
213
- hasToolId: true
214
- };
215
- }
111
+ //#region ../../packages/domain/src/entities/tool.entity.ts
112
+ const ToolSetChildItemSchema = z.object({
113
+ id: z.string(),
114
+ name: I18nStringSchema,
115
+ description: I18nStringSchema.optional(),
116
+ icon: z.string(),
117
+ toolDescription: z.string(),
118
+ inputSchema: z.any(),
119
+ outputSchema: z.any()
120
+ });
121
+ const ToolSchema = z.object({
122
+ ...PluginBaseSchema.shape,
123
+ type: z.literal(PluginTypeEnum.tool),
124
+ toolDescription: z.string(),
125
+ inputSchema: z.any().optional(),
126
+ outputSchema: z.any().optional(),
127
+ secretSchema: z.any().optional(),
128
+ children: z.array(ToolSetChildItemSchema).optional()
129
+ });
130
+ const ToolSetSchema = z.object({
131
+ ...PluginBaseSchema.shape,
132
+ type: z.literal(PluginTypeEnum.tool),
133
+ meta: z.object({
134
+ secretSchema: z.any().optional(),
135
+ toolDescription: z.string()
136
+ })
137
+ });
138
+
139
+ //#endregion
140
+ //#region ../../packages/domain/src/value-objects/plugin/plugin-manifest.vo.ts
141
+ const PluginManifestBaseSchema = z.object({ ...PluginBaseSchema.omit({
142
+ etag: true,
143
+ readmeUrl: true
144
+ }).shape });
145
+ const ToolManifestSchema = z.object({
146
+ ...PluginManifestBaseSchema.shape,
147
+ type: z.literal(PluginTypeEnum.tool),
148
+ toolDescription: z.string(),
149
+ inputSchema: z.any().optional(),
150
+ outputSchema: z.any().optional(),
151
+ secretSchema: z.any(),
152
+ children: z.array(z.object({
153
+ id: z.string(),
154
+ description: I18nStringSchema,
155
+ name: I18nStringSchema,
156
+ toolDescription: z.string(),
157
+ icon: z.string(),
158
+ inputSchema: z.any(),
159
+ outputSchema: z.any()
160
+ })).min(1).optional()
161
+ });
216
162
 
217
163
  //#endregion
218
164
  //#region src/build/index.ts
219
165
  /**
220
166
  * 核心构建逻辑:给定入口目录和输出目录,完成一次工具构建。
221
- * - 使用 logger 记录关键阶段和耗时
222
- * - 不调用 process.exit
223
- * - 失败时抛出 Error
167
+ *
168
+ * 输出:
169
+ * dist/index.js
170
+ * dist/manifest.json
171
+ * dist/logo.* / dist/<childId>.logo.*
172
+ * dist/README.md(可选)
173
+ * dist/assets/**(可选)
224
174
  */
225
175
  async function buildToolPackage(options) {
226
176
  const entryDir = path.resolve(options.entry);
227
- await assertPathExists$1(entryDir, `入口目录不存在: ${entryDir}`);
228
- const configPath = path.join(entryDir, "config.ts");
229
- await assertPathExists$1(configPath, `找不到 config.ts 文件: ${configPath}`);
177
+ const outputDir = path.resolve(options.output);
230
178
  const indexPath = path.join(entryDir, "index.ts");
231
- await assertPathExists$1(indexPath, `找不到 index.ts 文件: ${indexPath}`);
232
179
  const tempDir = path.join(entryDir, ".build-temp");
233
- await ensureDir$1(tempDir);
180
+ await assertPathExists$1(entryDir, `入口目录不存在: ${entryDir}`);
181
+ await assertPathExists$1(indexPath, `找不到 index.ts 文件: ${indexPath}`);
234
182
  const tStart = Date.now();
235
- let tConfig = 0;
236
183
  let tCopy = 0;
237
184
  let tBuild = 0;
185
+ let tAssemble = 0;
238
186
  try {
239
- const tConfigStart = Date.now();
240
- const transformed = await transformToolConfig({
241
- sourceCode: await fs.readFile(configPath, "utf-8"),
242
- filePath: configPath
243
- });
244
- await fs.writeFile(path.join(tempDir, "config.ts"), transformed.code, "utf-8");
245
- tConfig = Date.now() - tConfigStart;
187
+ await resetDir(tempDir);
246
188
  const tCopyStart = Date.now();
247
- await copySourceFiles(entryDir, tempDir, entryDir);
189
+ await copySourceFiles(entryDir, tempDir);
248
190
  tCopy = Date.now() - tCopyStart;
249
- const outputDir = path.resolve(options.output);
250
- await ensureDir$1(outputDir);
191
+ await resetDir(outputDir);
251
192
  const tempIndexPath = path.join(tempDir, "index.ts");
252
193
  const tBuildStart = Date.now();
253
194
  await build({
254
- entry: [tempIndexPath],
195
+ entry: { index: tempIndexPath },
255
196
  outDir: outputDir,
256
197
  format: [options.format],
257
198
  clean: true,
258
199
  minify: options.minify,
259
- inlineOnly: false,
200
+ inlineOnly: ["*"],
260
201
  nodeProtocol: true,
261
202
  platform: "node",
262
203
  target: "node22",
@@ -269,19 +210,25 @@ async function buildToolPackage(options) {
269
210
  })
270
211
  });
271
212
  tBuild = Date.now() - tBuildStart;
272
- const files = await fs.readdir(outputDir);
213
+ const tAssembleStart = Date.now();
214
+ const plan = await createBuildPlan(entryDir, await loadPackableTool(path.join(outputDir, "index.js")));
215
+ await fs.writeFile(path.join(outputDir, "manifest.json"), JSON.stringify(plan.manifest, null, 2), "utf-8");
216
+ await copyLogoFiles(outputDir, plan.logoFiles);
217
+ await copyOptionalStaticFiles(entryDir, outputDir);
218
+ tAssemble = Date.now() - tAssembleStart;
219
+ const files = await collectRelativeFiles(outputDir);
273
220
  const tTotal = Date.now() - tStart;
274
221
  logger.info([
275
222
  "构建阶段耗时统计:",
276
- `• config 转换: ${formatDuration(tConfig)}`,
277
- `• 源码复制: ${formatDuration(tCopy)}`,
278
- `• tsdown 构建: ${formatDuration(tBuild)}`,
279
- `• 总耗时: ${formatDuration(tTotal)}`
223
+ `• 源码复制: ${formatDuration(tCopy)}`,
224
+ `• tsdown 构建: ${formatDuration(tBuild)}`,
225
+ `• 产物组装: ${formatDuration(tAssemble)}`,
226
+ `• 总耗时: ${formatDuration(tTotal)}`
280
227
  ].join("\n"));
281
228
  return {
282
229
  entryDir,
283
230
  outputDir,
284
- files: files.map((f) => f)
231
+ files
285
232
  };
286
233
  } finally {
287
234
  try {
@@ -292,12 +239,69 @@ async function buildToolPackage(options) {
292
239
  } catch {}
293
240
  }
294
241
  }
295
- /**
296
- * 从入口目录复制源码到临时目录:
297
- * - 根目录的 config.ts 已被转换,不再复制
298
- * - 子目录中的 config.ts 需要保留(子工具自身的配置)
299
- */
300
- async function copySourceFiles(sourceDir, targetDir, rootDir) {
242
+ async function loadPackableTool(indexJsPath) {
243
+ await assertPathExists$1(indexJsPath, `找不到编译产物 index.js:${indexJsPath}`);
244
+ const tool = (await import(`${pathToFileURL(indexJsPath).href}?t=${Date.now()}`)).default;
245
+ if (!isPackableToolExport$1(tool)) throw new Error("index.ts 的 default export 必须是 defineTool()/defineToolSet() 返回的 factory-sdk 实例");
246
+ return tool;
247
+ }
248
+ async function createBuildPlan(entryDir, tool) {
249
+ const rootLogoPath = await findRootLogoFile(entryDir);
250
+ if (!rootLogoPath) throw new Error(`找不到主 logo 文件,请在 ${entryDir} 下提供 logo.*`);
251
+ const rootLogoName = path.basename(rootLogoPath);
252
+ const logoFiles = new Map([[rootLogoName, rootLogoPath]]);
253
+ const userManifest = tool.getUserToolManifest();
254
+ const secretSchema = z.toJSONSchema(tool.getSecretSchema());
255
+ const toolDescription = pickToolDescription$1(getOptionalString$1(userManifest.toolDescription), userManifest.description);
256
+ const childManifests = tool.getChildManifests();
257
+ const manifestCandidate = {
258
+ ...userManifest,
259
+ type: "tool",
260
+ icon: rootLogoName,
261
+ toolDescription,
262
+ secretSchema
263
+ };
264
+ if (childManifests.length === 0) {
265
+ const handler = tool.getToolHandler();
266
+ if (!handler) throw new Error("default export 未注册任何工具 handler");
267
+ manifestCandidate.inputSchema = z.toJSONSchema(handler.inputSchema);
268
+ manifestCandidate.outputSchema = z.toJSONSchema(handler.outputSchema);
269
+ } else manifestCandidate.children = await Promise.all(childManifests.map(async (child) => {
270
+ const handler = tool.getToolHandler(child.id);
271
+ if (!handler) throw new Error(`找不到子工具 handler: ${child.id}`);
272
+ const childLogoPath = await findChildLogoFile(entryDir, child.id) ?? rootLogoPath;
273
+ const childLogoName = path.basename(childLogoPath);
274
+ logoFiles.set(childLogoName, childLogoPath);
275
+ return {
276
+ id: child.id,
277
+ description: child.description,
278
+ name: child.name,
279
+ icon: childLogoName,
280
+ toolDescription: pickToolDescription$1(child.toolDescription, child.description),
281
+ inputSchema: z.toJSONSchema(handler.inputSchema),
282
+ outputSchema: z.toJSONSchema(handler.outputSchema)
283
+ };
284
+ }));
285
+ return {
286
+ manifest: ToolManifestSchema.parse(manifestCandidate),
287
+ logoFiles: [...logoFiles.entries()].map(([fileName, sourcePath]) => ({
288
+ fileName,
289
+ sourcePath
290
+ }))
291
+ };
292
+ }
293
+ async function copyLogoFiles(outputDir, logoFiles) {
294
+ await Promise.all(logoFiles.map(async ({ fileName, sourcePath }) => {
295
+ await fs.copyFile(sourcePath, path.join(outputDir, fileName));
296
+ }));
297
+ }
298
+ async function copyOptionalStaticFiles(entryDir, outputDir) {
299
+ const readmePath = path.join(entryDir, "README.md");
300
+ if (await pathExists$1(readmePath)) await fs.copyFile(readmePath, path.join(outputDir, "README.md"));
301
+ const assetsDir = path.join(entryDir, "assets");
302
+ if (await pathExists$1(assetsDir)) await copyDirectory(assetsDir, path.join(outputDir, "assets"));
303
+ }
304
+ async function copySourceFiles(sourceDir, targetDir) {
301
305
  const files = await fs.readdir(sourceDir, { withFileTypes: true });
302
306
  for (const file of files) {
303
307
  if (file.name === "node_modules" || file.name === "dist" || file.name.startsWith(".build-")) continue;
@@ -305,21 +309,88 @@ async function copySourceFiles(sourceDir, targetDir, rootDir) {
305
309
  const targetPath = path.join(targetDir, file.name);
306
310
  if (file.isDirectory()) {
307
311
  await ensureDir$1(targetPath);
308
- await copySourceFiles(sourcePath, targetPath, rootDir);
309
- } else if (file.isFile()) {
310
- if (!(file.name === "config.ts" && sourceDir === rootDir)) if (file.name === "config.ts") {
311
- const transformed = await transformToolConfig({
312
- sourceCode: await fs.readFile(sourcePath, "utf-8"),
313
- filePath: sourcePath
314
- });
315
- await fs.writeFile(targetPath, transformed.code, "utf-8");
316
- } else await fs.copyFile(sourcePath, targetPath);
312
+ await copySourceFiles(sourcePath, targetPath);
313
+ } else if (file.isFile()) await fs.copyFile(sourcePath, targetPath);
314
+ }
315
+ }
316
+ async function collectRelativeFiles(dir, baseDir = dir) {
317
+ const entries = await fs.readdir(dir, { withFileTypes: true });
318
+ const files = [];
319
+ for (const entry of entries) {
320
+ const fullPath = path.join(dir, entry.name);
321
+ if (entry.isDirectory()) {
322
+ files.push(...await collectRelativeFiles(fullPath, baseDir));
323
+ continue;
317
324
  }
325
+ if (entry.isFile()) files.push(path.relative(baseDir, fullPath));
326
+ }
327
+ return files.sort();
328
+ }
329
+ async function findRootLogoFile(entryDir) {
330
+ return findMatchingFile(entryDir, /^logo\./i);
331
+ }
332
+ async function findChildLogoFile(entryDir, childId) {
333
+ const escapedChildId = escapeRegExp(childId);
334
+ return findMatchingFile(entryDir, new RegExp(`^${escapedChildId}\\.logo\\.`, "i"));
335
+ }
336
+ async function findMatchingFile(dir, pattern) {
337
+ try {
338
+ const matched = (await fs.readdir(dir, { withFileTypes: true })).find((entry) => entry.isFile() && pattern.test(entry.name));
339
+ return matched ? path.join(dir, matched.name) : null;
340
+ } catch {
341
+ return null;
342
+ }
343
+ }
344
+ async function copyDirectory(sourceDir, targetDir) {
345
+ await ensureDir$1(targetDir);
346
+ const entries = await fs.readdir(sourceDir, { withFileTypes: true });
347
+ for (const entry of entries) {
348
+ const sourcePath = path.join(sourceDir, entry.name);
349
+ const targetPath = path.join(targetDir, entry.name);
350
+ if (entry.isDirectory()) await copyDirectory(sourcePath, targetPath);
351
+ else if (entry.isFile()) await fs.copyFile(sourcePath, targetPath);
352
+ }
353
+ }
354
+ function isPackableToolExport$1(value) {
355
+ return Boolean(value && typeof value === "object" && "getUserToolManifest" in value && typeof value.getUserToolManifest === "function" && "getSecretSchema" in value && typeof value.getSecretSchema === "function" && "getToolHandler" in value && typeof value.getToolHandler === "function" && "getChildManifests" in value && typeof value.getChildManifests === "function");
356
+ }
357
+ function pickToolDescription$1(explicitDescription, fallbackSource) {
358
+ if (explicitDescription && explicitDescription.trim().length > 0) return explicitDescription;
359
+ if (fallbackSource && typeof fallbackSource === "object") {
360
+ const localized = fallbackSource;
361
+ const preferred = [
362
+ localized["zh-CN"],
363
+ localized.en,
364
+ ...Object.values(localized)
365
+ ].find((value) => typeof value === "string" && value.trim().length > 0);
366
+ if (typeof preferred === "string") return preferred;
367
+ }
368
+ return "";
369
+ }
370
+ function getOptionalString$1(value) {
371
+ return typeof value === "string" ? value : void 0;
372
+ }
373
+ function escapeRegExp(value) {
374
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
375
+ }
376
+ async function pathExists$1(p) {
377
+ try {
378
+ await fs.access(p);
379
+ return true;
380
+ } catch {
381
+ return false;
318
382
  }
319
383
  }
320
384
  async function ensureDir$1(dir) {
321
385
  await fs.mkdir(dir, { recursive: true });
322
386
  }
387
+ async function resetDir(dir) {
388
+ await fs.rm(dir, {
389
+ recursive: true,
390
+ force: true
391
+ });
392
+ await ensureDir$1(dir);
393
+ }
323
394
  async function assertPathExists$1(p, message) {
324
395
  try {
325
396
  await fs.access(p);
@@ -332,6 +403,15 @@ function formatDuration(ms) {
332
403
  return `${(ms / 1e3).toFixed(2)}s`;
333
404
  }
334
405
 
406
+ //#endregion
407
+ //#region src/commands/base.ts
408
+ /**
409
+ * 所有子命令的基类:
410
+ * - 统一命令接口类型
411
+ * - 强制实现 register 方法,用于向 Commander 注册自身
412
+ */
413
+ var BaseCommand = class {};
414
+
335
415
  //#endregion
336
416
  //#region src/commands/build.ts
337
417
  var BuildCommand = class extends BaseCommand {
@@ -367,6 +447,100 @@ var BuildCommand = class extends BaseCommand {
367
447
  }
368
448
  };
369
449
 
450
+ //#endregion
451
+ //#region src/check/index.ts
452
+ /**
453
+ * 检查构建产物的正确性:
454
+ * - dist/index.js 存在
455
+ * - dist/manifest.json 存在且通过 Schema 校验
456
+ * - manifest.json 引用的 logo 文件存在
457
+ */
458
+ async function checkBuildOutput(options) {
459
+ const outputDir = path.resolve(options.output);
460
+ const errors = [];
461
+ const warnings = [];
462
+ const indexJsPath = path.join(outputDir, "index.js");
463
+ if (!await pathExists(indexJsPath)) errors.push(`缺少构建产物 dist/index.js:${indexJsPath}`);
464
+ const manifestPath = path.join(outputDir, "manifest.json");
465
+ const manifestContent = await readFileSafe(manifestPath);
466
+ if (manifestContent === null) errors.push(`缺少 dist/manifest.json:${manifestPath}`);
467
+ else {
468
+ let rawManifest;
469
+ try {
470
+ rawManifest = JSON.parse(manifestContent);
471
+ } catch (e) {
472
+ errors.push(`manifest.json 解析失败(JSON 语法错误):${e.message}`);
473
+ }
474
+ if (rawManifest !== void 0) {
475
+ const result = ToolManifestSchema.safeParse(rawManifest);
476
+ if (!result.success) for (const issue of result.error.issues) errors.push(`manifest.json 校验失败 [${issue.path.join(".")}]: ${issue.message}`);
477
+ else await validateManifestAssets(result.data, outputDir, errors, warnings);
478
+ }
479
+ }
480
+ return {
481
+ ok: errors.length === 0,
482
+ errors,
483
+ warnings
484
+ };
485
+ }
486
+ async function validateManifestAssets(manifest, outputDir, errors, warnings) {
487
+ if (!await pathExists(path.join(outputDir, manifest.icon))) errors.push(`manifest.json 引用的主 logo 不存在:${manifest.icon}`);
488
+ if (manifest.children && manifest.children.length > 0) {
489
+ for (const child of manifest.children) if (!await pathExists(path.join(outputDir, child.icon))) errors.push(`manifest.json 中子工具 "${child.id}" 的 logo 不存在:${child.icon}`);
490
+ }
491
+ if (!await pathExists(path.join(outputDir, "README.md"))) warnings.push("dist/README.md 不存在");
492
+ }
493
+ async function readFileSafe(filePath) {
494
+ try {
495
+ return await fs.readFile(filePath, "utf-8");
496
+ } catch {
497
+ return null;
498
+ }
499
+ }
500
+ async function pathExists(p) {
501
+ try {
502
+ await fs.access(p);
503
+ return true;
504
+ } catch {
505
+ return false;
506
+ }
507
+ }
508
+
509
+ //#endregion
510
+ //#region src/commands/check.ts
511
+ var CheckCommand = class extends BaseCommand {
512
+ register(parent) {
513
+ parent.command("check").description("检查构建产物的正确性(manifest.json、config.json、index.js)").option("-e, --entry <path>", "工具源码目录", process.cwd()).option("-o, --output <path>", "构建输出目录", "./dist").action(async (opts) => {
514
+ await this.run(opts);
515
+ });
516
+ }
517
+ async run(options) {
518
+ logger.info(`检查目录: ${options.entry}`);
519
+ logger.info(`产物目录: ${options.output}`);
520
+ const result = await checkBuildOutput(options);
521
+ if (result.warnings.length > 0) for (const w of result.warnings) logger.warn(w);
522
+ if (result.errors.length > 0) {
523
+ logger.error(`检查失败,共 ${result.errors.length} 个错误:`);
524
+ for (const e of result.errors) logger.error(` ✗ ${e}`);
525
+ process.exit(1);
526
+ }
527
+ logger.success("检查通过,构建产物格式正确");
528
+ }
529
+ };
530
+
531
+ //#endregion
532
+ //#region package.json
533
+ var name = "@fastgpt-plugin/cli";
534
+ var version = "0.1.0-beta.5";
535
+
536
+ //#endregion
537
+ //#region src/constants.ts
538
+ const CLI_NAME = name;
539
+ const CLI_VERSION = version;
540
+ const DEFAULT_PLUGIN_NAME = "./packages/my-tool";
541
+ const DEFAULT_PLUGIN_DESCRIPTION = "This is a FastGPT plugin";
542
+ const TOOL_TEMPLATES_DIR = path.join(import.meta.dirname, "../templates");
543
+
370
544
  //#endregion
371
545
  //#region src/prompts/create/deps.ts
372
546
  const defaultDeps = {
@@ -464,7 +638,7 @@ var CreateCommand = class extends BaseCommand {
464
638
  }
465
639
  async run(options) {
466
640
  const targetDir = path.resolve(options.cwd, options.name);
467
- const templateDir = path.join(TOOL_TEMPLATES_DIR, options.type === "tool-suite" ? "tool-suite" : "tool");
641
+ const templateDir = path.join(TOOL_TEMPLATES_DIR, "tool");
468
642
  const files = await this.collectTemplateFiles(templateDir);
469
643
  const description = options.description ?? DEFAULT_PLUGIN_DESCRIPTION;
470
644
  await this.ensureDir(targetDir);
@@ -476,6 +650,7 @@ var CreateCommand = class extends BaseCommand {
476
650
  const content = this.applyPlaceholders(raw, options.name, description);
477
651
  await fs.writeFile(filePath, content, "utf-8");
478
652
  }
653
+ await this.finalizeIndexTemplate(targetDir, options.type);
479
654
  logger.success(`创建插件项目: ${options.name} (${options.type})`, { cwd: options.cwd });
480
655
  }
481
656
  applyPlaceholders(text, name, description) {
@@ -499,58 +674,920 @@ var CreateCommand = class extends BaseCommand {
499
674
  await walk(root);
500
675
  return result.sort();
501
676
  }
677
+ async finalizeIndexTemplate(targetDir, type) {
678
+ const toolIndexPath = path.join(targetDir, "index.tool.ts");
679
+ const toolSetIndexPath = path.join(targetDir, "index.toolset.ts");
680
+ const finalIndexPath = path.join(targetDir, "index.ts");
681
+ const sourcePath = type === "tool-suite" ? toolSetIndexPath : toolIndexPath;
682
+ const cleanupTargets = [toolIndexPath, toolSetIndexPath];
683
+ await fs.rename(sourcePath, finalIndexPath);
684
+ await Promise.all(cleanupTargets.map(async (filePath) => {
685
+ if (filePath === sourcePath) return;
686
+ await fs.rm(filePath, { force: true });
687
+ }));
688
+ }
502
689
  async ensureDir(dir) {
503
690
  await fs.mkdir(dir, { recursive: true });
504
691
  }
505
692
  };
506
693
 
694
+ //#endregion
695
+ //#region src/debug/import-hooks.ts
696
+ const SOURCE_EXTENSIONS = [
697
+ ".ts",
698
+ ".tsx",
699
+ ".mts",
700
+ ".cts"
701
+ ];
702
+ const RESOLVE_EXTENSIONS = [
703
+ "",
704
+ ...SOURCE_EXTENSIONS,
705
+ ".js",
706
+ ".mjs",
707
+ ".cjs",
708
+ ".json"
709
+ ];
710
+ let hooksRegistered = false;
711
+ function ensureDebugImportHooks() {
712
+ if (hooksRegistered) return;
713
+ hooksRegistered = true;
714
+ const workspaceRoot = findWorkspaceRoot(path.dirname(fileURLToPath(import.meta.url)));
715
+ registerHooks({
716
+ resolve(specifier, context, nextResolve) {
717
+ const resolved = resolveSpecifier({
718
+ specifier,
719
+ parentURL: context.parentURL,
720
+ workspaceRoot
721
+ });
722
+ if (resolved) return {
723
+ url: resolved,
724
+ shortCircuit: true
725
+ };
726
+ return nextResolve(specifier, context);
727
+ },
728
+ load(url, context, nextLoad) {
729
+ const parsed = new URL(url);
730
+ const ext = path.extname(parsed.pathname);
731
+ if (SOURCE_EXTENSIONS.includes(ext)) return {
732
+ format: "module",
733
+ shortCircuit: true,
734
+ source: stripTypeScriptTypes(fs$1.readFileSync(fileURLToPath(parsed), "utf-8"), {
735
+ mode: "transform",
736
+ sourceMap: false
737
+ })
738
+ };
739
+ return nextLoad(url, context);
740
+ }
741
+ });
742
+ }
743
+ function resolveSpecifier({ specifier, parentURL, workspaceRoot }) {
744
+ if (specifier.startsWith("node:")) return null;
745
+ if (specifier.startsWith("file:")) return toFileURL(resolveExistingPath(fileURLToPath(new URL(specifier))));
746
+ if (specifier.startsWith("./") || specifier.startsWith("../")) {
747
+ if (!parentURL?.startsWith("file:")) return null;
748
+ const parentPath = fileURLToPath(new URL(parentURL));
749
+ return toFileURL(resolveExistingPath(path.resolve(path.dirname(parentPath), specifier)));
750
+ }
751
+ if (path.isAbsolute(specifier)) return toFileURL(resolveExistingPath(specifier));
752
+ if (!workspaceRoot) return null;
753
+ return toFileURL(resolveWorkspaceAlias(specifier, workspaceRoot));
754
+ }
755
+ function resolveWorkspaceAlias(specifier, workspaceRoot) {
756
+ const exactAliasMap = new Map([
757
+ ["@fastgpt-plugin/sdk-factory", path.join(workspaceRoot, "sdk/factory/src/index.ts")],
758
+ ["@fastgpt-plugin/sdk-client", path.join(workspaceRoot, "sdk/client/src/index.ts")],
759
+ ["sdk/factory/src", path.join(workspaceRoot, "sdk/factory/src/index.ts")],
760
+ ["sdk/factory/dist", path.join(workspaceRoot, "sdk/factory/src/index.ts")],
761
+ ["sdk/client/src", path.join(workspaceRoot, "sdk/client/src/index.ts")],
762
+ ["sdk/client/dist", path.join(workspaceRoot, "sdk/client/src/index.ts")]
763
+ ]);
764
+ const prefixAliasMap = [
765
+ ["@fastgpt-plugin/sdk-factory/", path.join(workspaceRoot, "sdk/factory/src")],
766
+ ["@fastgpt-plugin/sdk-client/", path.join(workspaceRoot, "sdk/client/src")],
767
+ ["@domain/", path.join(workspaceRoot, "packages/domain/src")],
768
+ ["@usecase/", path.join(workspaceRoot, "packages/usecase/src")],
769
+ ["@shared/", path.join(workspaceRoot, "packages/shared/src")],
770
+ ["@interface-adapter/", path.join(workspaceRoot, "packages/interface-adapter/src")],
771
+ ["@infrastructure/", path.join(workspaceRoot, "packages/infrastructure/src")],
772
+ ["@fastgpt-plugin/cli/", path.join(workspaceRoot, "apps/cli/src")],
773
+ ["sdk/factory/src/", path.join(workspaceRoot, "sdk/factory/src")],
774
+ ["sdk/factory/dist/", path.join(workspaceRoot, "sdk/factory/src")],
775
+ ["sdk/client/src/", path.join(workspaceRoot, "sdk/client/src")],
776
+ ["sdk/client/dist/", path.join(workspaceRoot, "sdk/client/src")]
777
+ ];
778
+ const exact = exactAliasMap.get(specifier);
779
+ if (exact) return resolveExistingPath(exact);
780
+ for (const [prefix, targetDir] of prefixAliasMap) {
781
+ if (!specifier.startsWith(prefix)) continue;
782
+ const subPath = specifier.slice(prefix.length);
783
+ return resolveExistingPath(path.join(targetDir, subPath));
784
+ }
785
+ return null;
786
+ }
787
+ function resolveExistingPath(targetPath) {
788
+ const normalizedTarget = path.normalize(targetPath);
789
+ if (fs$1.existsSync(normalizedTarget) && fs$1.statSync(normalizedTarget).isFile()) return normalizedTarget;
790
+ for (const ext of RESOLVE_EXTENSIONS) {
791
+ const candidate = ext ? `${normalizedTarget}${ext}` : normalizedTarget;
792
+ if (fs$1.existsSync(candidate) && fs$1.statSync(candidate).isFile()) return candidate;
793
+ }
794
+ if (fs$1.existsSync(normalizedTarget) && fs$1.statSync(normalizedTarget).isDirectory()) for (const ext of RESOLVE_EXTENSIONS.filter(Boolean)) {
795
+ const candidate = path.join(normalizedTarget, `index${ext}`);
796
+ if (fs$1.existsSync(candidate) && fs$1.statSync(candidate).isFile()) return candidate;
797
+ }
798
+ return null;
799
+ }
800
+ function toFileURL(targetPath) {
801
+ if (!targetPath) return null;
802
+ return pathToFileURL(targetPath).href;
803
+ }
804
+ function findWorkspaceRoot(startDir) {
805
+ let currentDir = startDir;
806
+ while (true) {
807
+ const sdkFactoryIndex = path.join(currentDir, "sdk/factory/src/index.ts");
808
+ const domainDir = path.join(currentDir, "packages/domain/src");
809
+ if (fs$1.existsSync(sdkFactoryIndex) && fs$1.existsSync(domainDir)) return currentDir;
810
+ const parentDir = path.dirname(currentDir);
811
+ if (parentDir === currentDir) return;
812
+ currentDir = parentDir;
813
+ }
814
+ }
815
+
816
+ //#endregion
817
+ //#region ../../packages/domain/src/value-objects/file/MIME.vo.ts
818
+ const CommonMIMETypes = [
819
+ "application/javascript",
820
+ "application/json",
821
+ "application/yaml",
822
+ "application/zip",
823
+ "image/jpeg",
824
+ "image/png",
825
+ "image/gif",
826
+ "image/webp",
827
+ "image/svg+xml",
828
+ "application/pdf",
829
+ "text/plain",
830
+ "text/csv",
831
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
832
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
833
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation",
834
+ "application/msword",
835
+ "application/vnd.ms-excel",
836
+ "application/vnd.ms-powerpoint",
837
+ "text/markdown",
838
+ "audio/mpeg",
839
+ "video/mp4",
840
+ "audio/wav",
841
+ "text/html",
842
+ "application/xml",
843
+ "application/gzip",
844
+ "application/octet-stream",
845
+ "multipart/form-data",
846
+ "text/event-stream"
847
+ ];
848
+ const MIMESchema = z.enum(CommonMIMETypes).and(z.string().regex(/^[^/]+\/[^/]+$/));
849
+
850
+ //#endregion
851
+ //#region ../../packages/domain/src/value-objects/file/file.vo.ts
852
+ const FileMetaSchema = z.object({
853
+ fileKey: z.string(),
854
+ fileName: z.string(),
855
+ contentType: MIMESchema,
856
+ size: z.number(),
857
+ etag: z.string(),
858
+ createTime: z.date()
859
+ });
860
+ const FileCreateSchema = z.object({
861
+ fileKey: z.string().optional(),
862
+ path: z.string().optional(),
863
+ fileName: z.string().optional(),
864
+ contentType: MIMESchema.optional(),
865
+ overwrite: z.boolean().optional(),
866
+ file: z.union([z.instanceof(Readable, { error: "Stream cannot be empty" }), z.union([z.instanceof(Buffer, { error: "Buffer is required" }), z.instanceof(Uint8Array, { error: "Uint8Array is required" })]).transform((data) => {
867
+ if (data instanceof Uint8Array && !(data instanceof Buffer)) return Buffer.from(data);
868
+ return data;
869
+ })])
870
+ });
871
+
872
+ //#endregion
873
+ //#region ../../packages/domain/src/ports/invoke.port.ts
874
+ const InvokeUploadFileInputSchema = z.object({ ...FileCreateSchema.omit({
875
+ fileKey: true,
876
+ overwrite: true,
877
+ path: true
878
+ }).shape });
879
+ const InvokeUploadFileOutputSchema = z.object({
880
+ ...FileMetaSchema.omit({ fileKey: true }).shape,
881
+ accessURL: z.string()
882
+ });
883
+ const InvokeMethodEnumSchema = z.enum(["uploadFile"]);
884
+ const InvokeMethodEnum = InvokeMethodEnumSchema.enum;
885
+
886
+ //#endregion
887
+ //#region ../../packages/domain/src/value-objects/stream.vo.ts
888
+ var StreamData = class StreamData {
889
+ stream;
890
+ closed = false;
891
+ constructor(initializer) {
892
+ this.stream = new Readable$1({
893
+ objectMode: true,
894
+ read() {}
895
+ });
896
+ initializer?.(this);
897
+ }
898
+ static create(initializer) {
899
+ return new StreamData(initializer);
900
+ }
901
+ write(data) {
902
+ this.ensureWritable();
903
+ this.stream.push(data);
904
+ }
905
+ send(data) {
906
+ this.write(data);
907
+ }
908
+ close() {
909
+ if (this.closed) return;
910
+ this.closed = true;
911
+ this.stream.push(null);
912
+ }
913
+ end() {
914
+ this.close();
915
+ }
916
+ fail(error) {
917
+ this.closed = true;
918
+ this.stream.destroy(error);
919
+ }
920
+ toReadable() {
921
+ return this.stream;
922
+ }
923
+ onData(listener) {
924
+ this.stream.on("data", listener);
925
+ return this;
926
+ }
927
+ onEnd(listener) {
928
+ this.stream.on("end", listener);
929
+ return this;
930
+ }
931
+ onError(listener) {
932
+ this.stream.on("error", listener);
933
+ return this;
934
+ }
935
+ async consume(consumer) {
936
+ for await (const chunk of this.values()) await consumer(chunk);
937
+ }
938
+ async *values() {
939
+ for await (const chunk of this.stream) yield chunk;
940
+ }
941
+ ensureWritable() {
942
+ if (this.closed || this.stream.destroyed) throw new Error("StreamData is already closed");
943
+ }
944
+ };
945
+
946
+ //#endregion
947
+ //#region ../../packages/domain/src/value-objects/system-var.vo.ts
948
+ const SystemVarSchema = z.object({
949
+ user: z.object({
950
+ id: z.string(),
951
+ username: z.string(),
952
+ contact: z.string(),
953
+ membername: z.string(),
954
+ teamName: z.string(),
955
+ teamId: z.string(),
956
+ name: z.string()
957
+ }),
958
+ app: z.object({
959
+ id: z.string(),
960
+ name: z.string()
961
+ }),
962
+ tool: z.object({
963
+ id: z.string(),
964
+ version: z.string(),
965
+ prefix: z.string().optional()
966
+ }).passthrough(),
967
+ time: z.string()
968
+ });
969
+
970
+ //#endregion
971
+ //#region ../../packages/domain/src/value-objects/tool.vo.ts
972
+ const ToolHandlerReturnSchema = z.record(z.string(), z.unknown());
973
+ const StreamMessageTypeSchema = z.enum([
974
+ "response",
975
+ "error",
976
+ "stream"
977
+ ]);
978
+ const StreamMessageTypeEnum = StreamMessageTypeSchema.enum;
979
+ const StreamDataAnswerTypeSchema = z.enum(["answer", "fastAnswer"]);
980
+ const StreamDataAnswerTypeEnum = StreamDataAnswerTypeSchema.enum;
981
+ const ToolAnswerSchema = z.object({
982
+ type: StreamDataAnswerTypeSchema,
983
+ content: z.string()
984
+ });
985
+ const ToolStreamMessageSchema = z.discriminatedUnion("type", [
986
+ z.object({
987
+ type: z.literal(StreamMessageTypeEnum.response),
988
+ data: ToolHandlerReturnSchema
989
+ }),
990
+ z.object({
991
+ type: z.literal(StreamMessageTypeEnum.stream),
992
+ data: ToolAnswerSchema
993
+ }),
994
+ z.object({
995
+ type: z.literal(StreamMessageTypeEnum.error),
996
+ data: z.string()
997
+ })
998
+ ]);
999
+ const ToolRunContextSchema = z.object({ systemVar: z.record(z.string(), z.unknown()) });
1000
+ const ToolRunInputSchema = z.object({
1001
+ pluginId: z.string(),
1002
+ version: z.preprocess((value) => {
1003
+ if (value === "") return void 0;
1004
+ return value;
1005
+ }, z.string().optional()),
1006
+ source: PluginSourceSchema.optional(),
1007
+ childId: z.string().optional(),
1008
+ input: z.record(z.string(), z.unknown()),
1009
+ secrets: z.record(z.string(), z.unknown()).optional(),
1010
+ systemVar: SystemVarSchema
1011
+ });
1012
+
1013
+ //#endregion
1014
+ //#region ../../packages/infrastructure/src/plugin/plugin-runtime/drivers/local-pool/ipc-channel.ts
1015
+ const HOST_INVOKE_METHOD = "__host_invoke__";
1016
+
1017
+ //#endregion
1018
+ //#region src/debug/runtime.ts
1019
+ const LOCAL_DEBUG_RUNTIME_GLOBAL_KEY = "__FASTGPT_PLUGIN_LOCAL_DEBUG_RUNTIME__";
1020
+ const LOCAL_DEBUG_DUPLEX_REPLY_MARK = "__localDebugDuplexReply__";
1021
+ var LocalDebugChannel = class {
1022
+ constructor(runtime) {
1023
+ this.runtime = runtime;
1024
+ }
1025
+ setRequestHandler(handler) {
1026
+ this.runtime.setPluginRequestHandler(handler);
1027
+ }
1028
+ async requestDuplex(method, params, options) {
1029
+ return this.runtime.invokeHost(method, params, {
1030
+ ...options,
1031
+ streamName: method
1032
+ });
1033
+ }
1034
+ replyDuplex(_message, result, options) {
1035
+ return {
1036
+ [LOCAL_DEBUG_DUPLEX_REPLY_MARK]: true,
1037
+ ...result !== void 0 ? { result } : {},
1038
+ ...options?.output !== void 0 ? { output: options.output } : {},
1039
+ ...options ? { options: {
1040
+ traceId: options.traceId,
1041
+ outputMeta: options.outputMeta,
1042
+ outputStreamId: options.outputStreamId
1043
+ } } : {}
1044
+ };
1045
+ }
1046
+ sendReady() {
1047
+ this.runtime.markReady();
1048
+ }
1049
+ };
1050
+ var LocalDebugRuntime = class {
1051
+ pluginRequestHandler = null;
1052
+ hostRequestHandler = null;
1053
+ ready = false;
1054
+ readyPromise;
1055
+ resolveReady;
1056
+ pluginChannel;
1057
+ constructor() {
1058
+ this.pluginChannel = new LocalDebugChannel(this);
1059
+ this.readyPromise = new Promise((resolve) => {
1060
+ this.resolveReady = resolve;
1061
+ });
1062
+ }
1063
+ setPluginRequestHandler(handler) {
1064
+ this.pluginRequestHandler = handler;
1065
+ }
1066
+ setHostRequestHandler(handler) {
1067
+ this.hostRequestHandler = handler;
1068
+ }
1069
+ markReady() {
1070
+ if (this.ready) return;
1071
+ this.ready = true;
1072
+ this.resolveReady();
1073
+ }
1074
+ async waitUntilReady() {
1075
+ await this.readyPromise;
1076
+ }
1077
+ async invokePlugin(method, params, options) {
1078
+ if (!this.pluginRequestHandler) throw new Error("Plugin request handler is not initialized.");
1079
+ return this.dispatch(this.pluginRequestHandler, method, params, {
1080
+ ...options,
1081
+ streamName: method
1082
+ });
1083
+ }
1084
+ async invokeHost(method, params, options) {
1085
+ if (!this.hostRequestHandler) throw new Error(`Local debug host handler is not configured: ${method}`);
1086
+ const message = createMessage(method, params, options);
1087
+ return toDuplexResponse(message, await this.hostRequestHandler({
1088
+ message,
1089
+ method,
1090
+ args: params,
1091
+ traceId: options.traceId,
1092
+ input: options.input,
1093
+ replyDuplex: (result, replyOptions) => this.pluginChannel.replyDuplex(message, result, replyOptions)
1094
+ }), options);
1095
+ }
1096
+ async dispatch(handler, method, params, options) {
1097
+ const message = createMessage(method, params, options);
1098
+ return toDuplexResponse(message, await handler(message), options);
1099
+ }
1100
+ };
1101
+ const createLocalDebugRuntime = () => new LocalDebugRuntime();
1102
+ const setCurrentLocalDebugRuntime = (runtime) => {
1103
+ const store = globalThis;
1104
+ if (!runtime) {
1105
+ delete store[LOCAL_DEBUG_RUNTIME_GLOBAL_KEY];
1106
+ return;
1107
+ }
1108
+ store[LOCAL_DEBUG_RUNTIME_GLOBAL_KEY] = runtime;
1109
+ };
1110
+ function createMessage(method, params, options) {
1111
+ return {
1112
+ id: options?.requestId ?? randomUUID(),
1113
+ messageType: "request",
1114
+ method,
1115
+ params,
1116
+ traceId: options?.traceId,
1117
+ timestamp: Date.now()
1118
+ };
1119
+ }
1120
+ function isLocalDebugReplyDescriptor(value) {
1121
+ return Boolean(value && typeof value === "object" && LOCAL_DEBUG_DUPLEX_REPLY_MARK in value && value[LOCAL_DEBUG_DUPLEX_REPLY_MARK] === true);
1122
+ }
1123
+ function toDuplexResponse(message, response, options) {
1124
+ const inputDone = options?.input !== void 0 ? Promise.resolve() : void 0;
1125
+ if (!isLocalDebugReplyDescriptor(response)) return {
1126
+ requestId: message.id,
1127
+ result: response,
1128
+ ...inputDone ? { inputDone } : {}
1129
+ };
1130
+ const output = response.output !== void 0 ? createIncomingStream({
1131
+ source: response.output,
1132
+ streamName: response.options?.outputStreamId ?? options?.streamName ?? message.method ?? "stream",
1133
+ streamId: response.options?.outputStreamId ?? randomUUID(),
1134
+ traceId: response.options?.traceId ?? message.traceId,
1135
+ meta: response.options?.outputMeta
1136
+ }) : void 0;
1137
+ return {
1138
+ requestId: message.id,
1139
+ result: response.result,
1140
+ ...output ? { output } : {},
1141
+ ...inputDone ? { inputDone } : {}
1142
+ };
1143
+ }
1144
+ function createIncomingStream({ source, streamName, streamId, traceId, meta }) {
1145
+ return {
1146
+ streamId,
1147
+ streamName,
1148
+ stream: toStreamData(source),
1149
+ ...traceId ? { traceId } : {},
1150
+ ...meta !== void 0 ? { meta } : {}
1151
+ };
1152
+ }
1153
+ function toStreamData(source) {
1154
+ if (source instanceof StreamData) return source;
1155
+ const output = StreamData.create();
1156
+ (async () => {
1157
+ try {
1158
+ for await (const chunk of source) output.send(chunk);
1159
+ output.end();
1160
+ } catch (error) {
1161
+ output.fail(error instanceof Error ? error : new Error(String(error)));
1162
+ }
1163
+ })();
1164
+ return output;
1165
+ }
1166
+
1167
+ //#endregion
1168
+ //#region src/debug/session.ts
1169
+ async function loadDebugSession({ entryDir, uploadDir }) {
1170
+ const resolvedEntryDir = path.resolve(entryDir);
1171
+ const indexPath = path.join(resolvedEntryDir, "index.ts");
1172
+ await assertPathExists(resolvedEntryDir, `调试目录不存在: ${resolvedEntryDir}`);
1173
+ await assertPathExists(indexPath, `找不到 index.ts: ${indexPath}`);
1174
+ ensureDebugImportHooks();
1175
+ const runtime = createLocalDebugRuntime();
1176
+ runtime.setHostRequestHandler(createVirtualHostHandler(uploadDir));
1177
+ const previousRuntimeMode = process.env.RUNTIME_MODE;
1178
+ process.env.RUNTIME_MODE = "dev";
1179
+ setCurrentLocalDebugRuntime(runtime);
1180
+ try {
1181
+ const tool = (await import(`${pathToFileURL(indexPath).href}#fastgpt-plugin-debug-${Date.now()}-${Math.random().toString(16).slice(2)}`)).default;
1182
+ if (!isPackableToolExport(tool)) throw new Error("index.ts 的 default export 必须是 defineTool()/defineToolSet() 返回的 factory-sdk 实例");
1183
+ await runtime.waitUntilReady();
1184
+ return {
1185
+ runtime,
1186
+ uploadDir: path.resolve(uploadDir),
1187
+ snapshot: createDebugSnapshot(tool, {
1188
+ entryDir: resolvedEntryDir,
1189
+ indexPath
1190
+ })
1191
+ };
1192
+ } finally {
1193
+ setCurrentLocalDebugRuntime(void 0);
1194
+ if (previousRuntimeMode === void 0) delete process.env.RUNTIME_MODE;
1195
+ else process.env.RUNTIME_MODE = previousRuntimeMode;
1196
+ }
1197
+ }
1198
+ async function runDebugTool({ runtime, snapshot, toolId, input, secrets, systemVar }) {
1199
+ const targetTool = pickTargetTool(snapshot, toolId);
1200
+ const mergedSystemVar = createDebugSystemVar(snapshot, targetTool.id, systemVar);
1201
+ const response = await runtime.invokePlugin("run", {
1202
+ input,
1203
+ systemVar: mergedSystemVar,
1204
+ ...snapshot.isToolSet ? { childId: targetTool.id } : {},
1205
+ ...secrets ? { secrets } : {}
1206
+ }, { traceId: randomUUID() });
1207
+ if (!response.output) throw new Error("调试运行未返回输出流。");
1208
+ const streamMessages = [];
1209
+ let finalResponse;
1210
+ let finalError;
1211
+ await response.output.stream.consume(async (chunk) => {
1212
+ const parsed = ToolStreamMessageSchema.parse(chunk);
1213
+ streamMessages.push(parsed);
1214
+ if (parsed.type === "response") finalResponse = parsed.data;
1215
+ if (parsed.type === "error") finalError = parsed.data;
1216
+ });
1217
+ return {
1218
+ systemVar: mergedSystemVar,
1219
+ response: finalResponse,
1220
+ error: finalError,
1221
+ streamMessages
1222
+ };
1223
+ }
1224
+ function createDebugSystemVar(snapshot, toolId, overrides) {
1225
+ const base = {
1226
+ user: {
1227
+ id: "debug-user",
1228
+ username: "debug-user",
1229
+ contact: "debug@example.com",
1230
+ membername: "debug-member",
1231
+ teamName: "debug-team",
1232
+ teamId: "debug-team-id",
1233
+ name: "Local Debug User"
1234
+ },
1235
+ app: {
1236
+ id: "debug-app",
1237
+ name: "FastGPT Local Debug"
1238
+ },
1239
+ tool: {
1240
+ id: snapshot.pluginId,
1241
+ version: snapshot.version,
1242
+ prefix: snapshot.isToolSet ? toolId : snapshot.pluginId,
1243
+ token: "debug-token",
1244
+ accessToken: "debug-access-token"
1245
+ },
1246
+ time: (/* @__PURE__ */ new Date()).toISOString()
1247
+ };
1248
+ return SystemVarSchema.parse({
1249
+ ...base,
1250
+ ...overrides,
1251
+ user: {
1252
+ ...base.user,
1253
+ ...overrides?.user ?? {}
1254
+ },
1255
+ app: {
1256
+ ...base.app,
1257
+ ...overrides?.app ?? {}
1258
+ },
1259
+ tool: {
1260
+ ...base.tool,
1261
+ ...overrides?.tool ?? {}
1262
+ }
1263
+ });
1264
+ }
1265
+ function createDebugSnapshot(tool, location) {
1266
+ const userManifest = tool.getUserToolManifest();
1267
+ const children = tool.getChildManifests();
1268
+ const secretSchema = z.toJSONSchema(tool.getSecretSchema());
1269
+ const tools = children.length === 0 ? [createSingleToolSnapshot(tool, userManifest)] : children.map((child) => createChildToolSnapshot(tool, child));
1270
+ return {
1271
+ entryDir: location.entryDir,
1272
+ indexPath: location.indexPath,
1273
+ pluginId: getRequiredString(userManifest.pluginId, "缺少 pluginId"),
1274
+ version: getRequiredString(userManifest.version, "缺少 version"),
1275
+ name: pickLocalizedText(userManifest.name),
1276
+ description: pickLocalizedText(userManifest.description),
1277
+ toolDescription: pickToolDescription(getOptionalString(userManifest.toolDescription), userManifest.description),
1278
+ author: getOptionalString(userManifest.author),
1279
+ tags: toStringArray(userManifest.tags),
1280
+ permissions: toStringArray(userManifest.permission),
1281
+ secretSchema: ensurePlainObject(secretSchema),
1282
+ isToolSet: children.length > 0,
1283
+ tools
1284
+ };
1285
+ }
1286
+ function createSingleToolSnapshot(tool, userManifest) {
1287
+ const handler = tool.getToolHandler();
1288
+ if (!handler) throw new Error("default export 未注册任何工具 handler");
1289
+ return {
1290
+ id: "tool",
1291
+ name: pickLocalizedText(userManifest.name),
1292
+ description: pickLocalizedText(userManifest.description),
1293
+ toolDescription: pickToolDescription(getOptionalString(userManifest.toolDescription), userManifest.description),
1294
+ inputSchema: ensurePlainObject(z.toJSONSchema(handler.inputSchema)),
1295
+ outputSchema: ensurePlainObject(z.toJSONSchema(handler.outputSchema))
1296
+ };
1297
+ }
1298
+ function createChildToolSnapshot(tool, child) {
1299
+ const handler = tool.getToolHandler(child.id);
1300
+ if (!handler) throw new Error(`找不到子工具 handler: ${child.id}`);
1301
+ return {
1302
+ id: child.id,
1303
+ name: pickLocalizedText(child.name),
1304
+ description: pickLocalizedText(child.description),
1305
+ toolDescription: pickToolDescription(child.toolDescription, child.description),
1306
+ inputSchema: ensurePlainObject(z.toJSONSchema(handler.inputSchema)),
1307
+ outputSchema: ensurePlainObject(z.toJSONSchema(handler.outputSchema))
1308
+ };
1309
+ }
1310
+ function pickTargetTool(snapshot, toolId) {
1311
+ if (!snapshot.isToolSet) return snapshot.tools[0];
1312
+ if (!toolId) throw new Error("当前插件是工具集,请通过 --tool <childId> 指定要调试的子工具。");
1313
+ const tool = snapshot.tools.find((item) => item.id === toolId);
1314
+ if (!tool) throw new Error(`找不到子工具: ${toolId}`);
1315
+ return tool;
1316
+ }
1317
+ function createVirtualHostHandler(uploadDir) {
1318
+ return async ({ method, args, input }) => {
1319
+ if (method !== HOST_INVOKE_METHOD) throw new Error(`本地虚拟环境暂不支持反向调用: ${method}`);
1320
+ const payload = ensurePlainObject(args);
1321
+ const invokeMethod = payload.method;
1322
+ const invokeArgs = ensurePlainObject(payload.args);
1323
+ switch (invokeMethod) {
1324
+ case InvokeMethodEnum.uploadFile: {
1325
+ const buffer = await readSourceToBuffer(input);
1326
+ const fileName = sanitizeFileName(typeof invokeArgs.fileName === "string" && invokeArgs.fileName.trim().length > 0 ? invokeArgs.fileName : "debug-upload.bin");
1327
+ const contentType = typeof invokeArgs.contentType === "string" && invokeArgs.contentType.trim().length > 0 ? invokeArgs.contentType : "application/octet-stream";
1328
+ const saveDir = path.resolve(uploadDir);
1329
+ const outputPath = path.join(saveDir, `${Date.now()}-${randomUUID().slice(0, 8)}-${fileName}`);
1330
+ await fs.mkdir(saveDir, { recursive: true });
1331
+ await fs.writeFile(outputPath, buffer);
1332
+ return {
1333
+ fileName,
1334
+ contentType,
1335
+ size: buffer.length,
1336
+ etag: createHash("md5").update(buffer).digest("hex"),
1337
+ createTime: /* @__PURE__ */ new Date(),
1338
+ accessURL: pathToFileURL(outputPath).href
1339
+ };
1340
+ }
1341
+ default: throw new Error(`本地虚拟环境暂不支持反向调用: ${String(invokeMethod)}`);
1342
+ }
1343
+ };
1344
+ }
1345
+ async function readSourceToBuffer(source) {
1346
+ if (!source) return Buffer.alloc(0);
1347
+ const chunks = [];
1348
+ const iterable = source instanceof StreamData ? source.values() : source;
1349
+ for await (const chunk of iterable) chunks.push(toBuffer(chunk));
1350
+ return Buffer.concat(chunks);
1351
+ }
1352
+ function toBuffer(value) {
1353
+ if (Buffer.isBuffer(value)) return value;
1354
+ if (value instanceof Uint8Array) return Buffer.from(value);
1355
+ if (typeof value === "string") return Buffer.from(value);
1356
+ return Buffer.from(JSON.stringify(value));
1357
+ }
1358
+ function isPackableToolExport(value) {
1359
+ return Boolean(value && typeof value === "object" && "getUserToolManifest" in value && typeof value.getUserToolManifest === "function" && "getSecretSchema" in value && typeof value.getSecretSchema === "function" && "getToolHandler" in value && typeof value.getToolHandler === "function" && "getChildManifests" in value && typeof value.getChildManifests === "function");
1360
+ }
1361
+ function pickLocalizedText(value) {
1362
+ if (typeof value === "string") return value;
1363
+ if (value && typeof value === "object") {
1364
+ const localized = value;
1365
+ const preferred = [
1366
+ localized["zh-CN"],
1367
+ localized.en,
1368
+ ...Object.values(localized)
1369
+ ].find((item) => typeof item === "string" && item.trim().length > 0);
1370
+ if (typeof preferred === "string") return preferred;
1371
+ }
1372
+ return "";
1373
+ }
1374
+ function pickToolDescription(explicitDescription, fallbackSource) {
1375
+ if (explicitDescription && explicitDescription.trim().length > 0) return explicitDescription;
1376
+ return pickLocalizedText(fallbackSource);
1377
+ }
1378
+ function getRequiredString(value, message) {
1379
+ if (typeof value === "string" && value.trim().length > 0) return value;
1380
+ throw new Error(message);
1381
+ }
1382
+ function getOptionalString(value) {
1383
+ return typeof value === "string" ? value : void 0;
1384
+ }
1385
+ function toStringArray(value) {
1386
+ if (!Array.isArray(value)) return;
1387
+ const items = value.filter((item) => typeof item === "string");
1388
+ return items.length > 0 ? items : void 0;
1389
+ }
1390
+ function ensurePlainObject(value) {
1391
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
1392
+ }
1393
+ function sanitizeFileName(fileName) {
1394
+ return Array.from(fileName, (char) => {
1395
+ const code = char.charCodeAt(0);
1396
+ if (INVALID_FILE_NAME_CHARS.has(char) || code <= 31) return "_";
1397
+ return char;
1398
+ }).join("");
1399
+ }
1400
+ const INVALID_FILE_NAME_CHARS = new Set([
1401
+ "<",
1402
+ ">",
1403
+ ":",
1404
+ "\"",
1405
+ "/",
1406
+ "\\",
1407
+ "|",
1408
+ "?",
1409
+ "*"
1410
+ ]);
1411
+ async function assertPathExists(targetPath, message) {
1412
+ try {
1413
+ await fs.access(targetPath);
1414
+ } catch {
1415
+ throw new Error(message);
1416
+ }
1417
+ }
1418
+
1419
+ //#endregion
1420
+ //#region src/commands/debug.ts
1421
+ var DebugCommand = class extends BaseCommand {
1422
+ register(parent) {
1423
+ parent.command("debug <entry>").description("直接加载本地插件目录并调试工具").option("-t, --tool <childId>", "工具集子工具 ID").option("--run", "立即执行一次工具调试", false).option("-i, --input <json>", "工具输入 JSON 字符串").option("--input-file <path>", "工具输入 JSON 文件路径").option("--secrets <json>", "secrets JSON 字符串").option("--secrets-file <path>", "secrets JSON 文件路径").option("--system-var <json>", "systemVar JSON 字符串").option("--system-var-file <path>", "systemVar JSON 文件路径").option("--upload-dir <path>", "虚拟 uploadFile 的输出目录").action(async (entry, opts) => {
1424
+ await this.run(entry, opts);
1425
+ });
1426
+ }
1427
+ async run(entry, options) {
1428
+ const entryDir = path.resolve(entry);
1429
+ const uploadDir = path.resolve(options.uploadDir ?? path.join(entryDir, ".fastgpt-plugin-debug/uploads"));
1430
+ const session = await loadDebugSession({
1431
+ entryDir,
1432
+ uploadDir
1433
+ });
1434
+ this.printSnapshot(session.snapshot, uploadDir);
1435
+ if (!options.run) return;
1436
+ const input = await this.readJsonOption(options.input, options.inputFile, "input");
1437
+ const secrets = await this.readJsonOption(options.secrets, options.secretsFile, "secrets");
1438
+ const systemVar = await this.readJsonOption(options.systemVar, options.systemVarFile, "systemVar");
1439
+ const result = await runDebugTool({
1440
+ runtime: session.runtime,
1441
+ snapshot: session.snapshot,
1442
+ toolId: options.tool,
1443
+ input: input ?? {},
1444
+ secrets,
1445
+ systemVar
1446
+ });
1447
+ logger.success(`本地调试执行完成: ${session.snapshot.isToolSet ? options.tool ?? "" : session.snapshot.pluginId}`.trim());
1448
+ logger.info(`虚拟时间: ${result.systemVar.time}`);
1449
+ this.printStreamMessages(result.streamMessages);
1450
+ if (result.response) logger.info(`返回结果:\n${JSON.stringify(result.response, null, 2)}`);
1451
+ if (result.error) {
1452
+ logger.error(result.error);
1453
+ process.exit(1);
1454
+ }
1455
+ }
1456
+ printSnapshot(snapshot, uploadDir) {
1457
+ const lines = [
1458
+ "插件信息",
1459
+ ` entry: ${snapshot.entryDir}`,
1460
+ ` index: ${snapshot.indexPath}`,
1461
+ ` pluginId: ${snapshot.pluginId}`,
1462
+ ` version: ${snapshot.version}`,
1463
+ ` name: ${snapshot.name || "-"}`,
1464
+ ` description: ${snapshot.description || "-"}`,
1465
+ ` toolDescription: ${snapshot.toolDescription || "-"}`,
1466
+ ` author: ${snapshot.author ?? "-"}`,
1467
+ ` tags: ${snapshot.tags?.join(", ") ?? "-"}`,
1468
+ ` permissions: ${snapshot.permissions?.join(", ") ?? "-"}`,
1469
+ ` uploadDir: ${uploadDir}`,
1470
+ ` secretKeys: ${this.formatSchemaKeys(snapshot.secretSchema)}`
1471
+ ];
1472
+ logger.info(lines.join("\n"));
1473
+ const toolLines = ["可调试工具"];
1474
+ snapshot.tools.forEach((tool) => {
1475
+ toolLines.push(` - id: ${tool.id}`);
1476
+ toolLines.push(` name: ${tool.name || "-"}`);
1477
+ toolLines.push(` description: ${tool.description || "-"}`);
1478
+ toolLines.push(` inputKeys: ${this.formatSchemaKeys(tool.inputSchema)}`);
1479
+ toolLines.push(` command: ${this.buildCommand(snapshot, tool)}`);
1480
+ });
1481
+ logger.info(toolLines.join("\n"));
1482
+ }
1483
+ printStreamMessages(messages) {
1484
+ if (messages.length === 0) {
1485
+ logger.info("流式输出: 无");
1486
+ return;
1487
+ }
1488
+ const lines = ["流式输出"];
1489
+ messages.forEach((message) => {
1490
+ switch (message.type) {
1491
+ case "stream":
1492
+ lines.push(` [${message.data.type}] ${message.data.content}`);
1493
+ break;
1494
+ case "response":
1495
+ lines.push(" [response] 已收到最终返回结果");
1496
+ break;
1497
+ case "error":
1498
+ lines.push(` [error] ${message.data}`);
1499
+ break;
1500
+ }
1501
+ });
1502
+ logger.info(lines.join("\n"));
1503
+ }
1504
+ buildCommand(snapshot, tool) {
1505
+ return [
1506
+ "fastgpt-plugin",
1507
+ "debug",
1508
+ snapshot.entryDir,
1509
+ "--run",
1510
+ ...snapshot.isToolSet ? ["--tool", tool.id] : [],
1511
+ "--input",
1512
+ JSON.stringify(this.createInputExample(tool.inputSchema))
1513
+ ].map(quoteShellArg).join(" ");
1514
+ }
1515
+ createInputExample(schema) {
1516
+ if (Array.isArray(schema.enum) && schema.enum.length > 0) return schema.enum[0];
1517
+ if ("const" in schema) return schema.const;
1518
+ if (Array.isArray(schema.anyOf) && schema.anyOf.length > 0) return this.createInputExample(asObject(schema.anyOf[0]));
1519
+ if (Array.isArray(schema.oneOf) && schema.oneOf.length > 0) return this.createInputExample(asObject(schema.oneOf[0]));
1520
+ switch (schema.type) {
1521
+ case "object": {
1522
+ const properties = asObject(schema.properties);
1523
+ return Object.fromEntries(Object.entries(properties).map(([key, value]) => [key, this.createInputExample(asObject(value))]));
1524
+ }
1525
+ case "array": return [];
1526
+ case "integer":
1527
+ case "number": return 0;
1528
+ case "boolean": return true;
1529
+ case "string": return "";
1530
+ default: return {};
1531
+ }
1532
+ }
1533
+ formatSchemaKeys(schema) {
1534
+ const properties = asObject(schema.properties);
1535
+ const keys = Object.keys(properties);
1536
+ return keys.length > 0 ? keys.join(", ") : "-";
1537
+ }
1538
+ async readJsonOption(inlineValue, filePath, label) {
1539
+ if (!inlineValue && !filePath) return;
1540
+ if (inlineValue && filePath) throw new Error(`${label} 只能传入一种来源:JSON 字符串或 JSON 文件。`);
1541
+ const raw = inlineValue ?? await fs.readFile(path.resolve(filePath), "utf-8");
1542
+ try {
1543
+ const parsed = JSON.parse(raw);
1544
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error();
1545
+ return parsed;
1546
+ } catch {
1547
+ throw new Error(`${label} 解析失败,请提供合法的 JSON object。`);
1548
+ }
1549
+ }
1550
+ };
1551
+ function asObject(value) {
1552
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
1553
+ }
1554
+ function quoteShellArg(value) {
1555
+ if (/^[a-zA-Z0-9_./:@-]+$/.test(value)) return value;
1556
+ return `'${value.replace(/'/g, `'"'"'`)}'`;
1557
+ }
1558
+
507
1559
  //#endregion
508
1560
  //#region src/commands/pack.ts
1561
+ /**
1562
+ * `.pkg` 文件结构:
1563
+ * index.js
1564
+ * manifest.json
1565
+ * *.logo.*
1566
+ * README.md(可选)
1567
+ * assets/**(可选)
1568
+ */
509
1569
  var PackCommand = class extends BaseCommand {
510
1570
  register(parent) {
511
- parent.command("pack").description(" FastGPT 工具或工具集打包为 .pkg 文件").option("-e, --entry <path>", "工具入口目录", process.cwd()).option("-o, --output <path>", "输出目录(默认使用入口目录)").option("-n, --name <name>", "包名称(默认使用入口目录名)").action(async (opts) => {
1571
+ parent.command("pack").description("基于 build 产物打包 FastGPT 工具或工具集").option("-e, --entry <path>", "工具源码根目录", process.cwd()).option("-d, --dist <path>", "构建产物目录", "./dist").option("-o, --output <path>", "pkg 输出目录(默认使用入口目录)").option("-n, --name <name>", "包名称(默认使用入口目录名)").action(async (opts) => {
512
1572
  await this.run(opts);
513
1573
  });
514
1574
  }
515
1575
  async run(options) {
516
1576
  const start = Date.now();
517
1577
  const entryDir = path.resolve(options.entry);
1578
+ const distDir = path.resolve(entryDir, options.dist);
518
1579
  const outputDir = path.resolve(options.output || entryDir);
519
- const toolName = options.name || path.basename(entryDir);
520
- const pkgPath = path.join(outputDir, `${toolName}.pkg`);
1580
+ const packageName = options.name || path.basename(entryDir);
1581
+ const pkgPath = path.join(outputDir, `${packageName}.pkg`);
521
1582
  try {
522
- await assertPathExists(entryDir, `入口目录不存在: ${entryDir}`);
523
- const distIndexPath = path.join(entryDir, "dist", "index.js");
524
- const logoPath = path.join(entryDir, "logo.svg");
525
- const readmePath = path.join(entryDir, "README.md");
526
- const assetsDir = path.join(entryDir, "assets");
527
- const childrenDir = path.join(entryDir, "children");
528
- await assertPathExists(distIndexPath, `找不到 dist/index.js 文件: ${distIndexPath}`);
529
- await ensureDir(outputDir);
530
- const zipFile = new ZipFile();
531
- const outStream = createWriteStream(pkgPath);
532
- const zipPromise = new Promise((resolve, reject) => {
533
- zipFile.outputStream.pipe(outStream).on("close", () => resolve());
534
- zipFile.outputStream.on("error", (err) => reject(err));
535
- outStream.on("error", (err) => reject(err));
1583
+ await buildToolPackage({
1584
+ entry: entryDir,
1585
+ output: distDir,
1586
+ minify: false,
1587
+ format: "esm"
536
1588
  });
537
- zipFile.addFile(distIndexPath, "index.js");
538
- if (await pathExists(logoPath)) zipFile.addFile(logoPath, "logo.svg");
539
- if (await pathExists(readmePath)) zipFile.addFile(readmePath, "README.md");
540
- if (await pathExists(assetsDir)) await addDirectoryToZip(zipFile, assetsDir, "assets");
541
- if (await pathExists(childrenDir)) {
542
- const children = await fs.readdir(childrenDir, { withFileTypes: true });
543
- for (const child of children) {
544
- if (!child.isDirectory()) continue;
545
- const childName = child.name;
546
- const childDir = path.join(childrenDir, childName);
547
- const childLogoPath = path.join(childDir, "logo.svg");
548
- zipFile.addEmptyDirectory(`${childName}/`);
549
- if (await pathExists(childLogoPath)) zipFile.addFile(childLogoPath, `${childName}/logo.svg`);
550
- }
551
- }
552
- zipFile.end();
553
- await zipPromise;
1589
+ await ensureDir(outputDir);
1590
+ await zipDirectory(distDir, pkgPath);
554
1591
  const duration = ((Date.now() - start) / 1e3).toFixed(2);
555
1592
  logger.success(`打包完成,用时 ${duration}s`);
556
1593
  logger.info(`输出文件: ${pkgPath}`);
@@ -561,34 +1598,41 @@ var PackCommand = class extends BaseCommand {
561
1598
  }
562
1599
  }
563
1600
  };
564
- async function pathExists(p) {
565
- try {
566
- await fs.access(p);
567
- return true;
568
- } catch {
569
- return false;
570
- }
571
- }
572
- async function ensureDir(dir) {
573
- await fs.mkdir(dir, { recursive: true });
574
- }
575
- async function assertPathExists(p, message) {
576
- try {
577
- await fs.access(p);
578
- } catch {
579
- throw new Error(message);
1601
+ async function zipDirectory(sourceDir, pkgPath) {
1602
+ const zipFile = new ZipFile();
1603
+ const outStream = createWriteStream(pkgPath);
1604
+ const zipPromise = new Promise((resolve, reject) => {
1605
+ zipFile.outputStream.pipe(outStream).on("close", () => resolve());
1606
+ zipFile.outputStream.on("error", (err) => reject(err));
1607
+ outStream.on("error", (err) => reject(err));
1608
+ });
1609
+ const files = await collectFiles(sourceDir);
1610
+ const normalizedPkgPath = path.resolve(pkgPath);
1611
+ for (const file of files) {
1612
+ if (path.resolve(file.absolutePath) === normalizedPkgPath) continue;
1613
+ zipFile.addFile(file.absolutePath, file.relativePath);
580
1614
  }
1615
+ zipFile.end();
1616
+ await zipPromise;
581
1617
  }
582
- async function addDirectoryToZip(zipFile, dir, rootInZip) {
1618
+ async function collectFiles(dir, baseDir = dir) {
583
1619
  const entries = await fs.readdir(dir, { withFileTypes: true });
1620
+ const files = [];
584
1621
  for (const entry of entries) {
585
- const fullPath = path.join(dir, entry.name);
586
- const relPath = path.join(rootInZip, entry.name).split(path.sep).join("/");
1622
+ const absolutePath = path.join(dir, entry.name);
587
1623
  if (entry.isDirectory()) {
588
- zipFile.addEmptyDirectory(`${relPath}/`);
589
- await addDirectoryToZip(zipFile, fullPath, relPath);
590
- } else if (entry.isFile()) zipFile.addFile(fullPath, relPath);
1624
+ files.push(...await collectFiles(absolutePath, baseDir));
1625
+ continue;
1626
+ }
1627
+ if (entry.isFile()) files.push({
1628
+ absolutePath,
1629
+ relativePath: path.relative(baseDir, absolutePath)
1630
+ });
591
1631
  }
1632
+ return files;
1633
+ }
1634
+ async function ensureDir(dir) {
1635
+ await fs.mkdir(dir, { recursive: true });
592
1636
  }
593
1637
 
594
1638
  //#endregion
@@ -597,7 +1641,9 @@ function createProgram() {
597
1641
  const program = new Command();
598
1642
  program.name(CLI_NAME).version(CLI_VERSION).description("FastGPT 插件开发 CLI");
599
1643
  new BuildCommand().register(program);
1644
+ new CheckCommand().register(program);
600
1645
  new CreateCommand().register(program);
1646
+ new DebugCommand().register(program);
601
1647
  new PackCommand().register(program);
602
1648
  return program;
603
1649
  }