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

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,9 +1,629 @@
1
1
  #!/usr/bin/env node
2
- import{consola as e}from"consola/basic";import{Command as t}from"commander";import n from"node:path";import r from"node:fs/promises";import{build as i}from"tsdown";import{Visitor as a,parseSync as o}from"oxc-parser";import{createHash as s}from"node:crypto";import{input as c,select as l}from"@inquirer/prompts";import{kebabCase as u}from"es-toolkit";import{createWriteStream as d}from"node:fs";import{ZipFile as f}from"yazl";const p=e;var m=`@fastgpt-plugin/cli`,h=`0.1.0-beta.2`;const g=m,_=h,v=`This is a FastGPT plugin`,y=n.join(import.meta.dirname,`../templates`);var b=class{};async function x(e){let t=n.join(e.rootDir,`src`);if(!await C(t))return w(``);let i=[],a=async t=>{let o=await r.readdir(t,{withFileTypes:!0});for(let r of o){let o=n.join(t,r.name);if(r.isDirectory()){if(r.name===`__tests__`)continue;await a(o);continue}if(!r.isFile())continue;let s=r.name;if(s.includes(`.test.`)||s.includes(`.spec.`))continue;let c=n.relative(e.rootDir,o).split(n.sep).join(`/`);c.startsWith(`src/`)&&i.push({rel:c,abs:o})}};await a(t),i.sort((e,t)=>e.rel.localeCompare(t.rel));let o=``;for(let e of i){let t=w(await r.readFile(e.abs));o+=`${e.rel}:${t}\n`}return w(o)}async function S(e){let{rootDir:t,toolId:r}=e,i=[...e.children].sort(),a=[];for(let e of i){let i=await x({rootDir:n.join(t,`children`,e)});a.push(`${r}/${e}:${i}`)}return w(a.length>0?`${a.join(`
3
- `)}\n`:``)}async function C(e){try{return await r.access(e),!0}catch{return!1}}function w(e){let t=s(`md5`);return t.update(e),t.digest(`hex`)}function T(e){let t=e.split(`/`);return t.at(-3)===`children`?`${t.at(-4)}/${t.at(-2)}`:t.at(-2)||`unknown`}async function E(e){let t=n.join(e,`children`);try{await r.access(t)}catch{return[]}try{return await r.readdir(t)}catch{return[]}}function D(e,t){return e.some(e=>e.type===`Property`&&e.key?.type===`Identifier`&&e.key?.name===t)}function O(e,t){let n=t.sort((e,t)=>t.start-e.start),r=e;for(let e of n)r=r.slice(0,e.start)+e.content+r.slice(e.end);return r}async function k({sourceCode:e,filePath:t}){if(e.includes(`toolId:`))return{code:e,hasToolId:!0};let r=T(t),i=n.dirname(t),s=await E(i),c=await x({rootDir:i}),l=s.length>0?await S({rootDir:i,toolId:r,children:s}):null,u=o(n.basename(t),e,{sourceType:`module`});if(u.errors.length>0)throw Error(`Parse errors: ${u.errors.map(e=>e.message).join(`, `)}`);let d=[],f=[];new a({CallExpression(e){e.callee?.type===`Identifier`&&(e.callee.name===`defineTool`||e.callee.name===`defineToolSet`)&&e.arguments?.[0]?.type===`ObjectExpression`&&f.push(e)}}).visit(u.program);for(let e of f){let t=e.arguments[0];if(t?.type!==`ObjectExpression`)continue;let n=t,i=n.properties||[],a=e.callee?.type===`Identifier`&&e.callee.name===`defineToolSet`,o=!D(i,`toolId`),u=!D(i,`etag`),f=a&&!D(i,`children`)&&s.length>0;if(!o&&!u&&!f)continue;let p=i[i.length-1],m=p?p.end:(n.start??0)+1,h=[];if(o&&h.push(`toolId: '${r}'`),u){let e=a&&l?l:c;h.push(`etag: '${e}'`)}if(f){let e=s.map(e=>e).join(`, `);h.push(`children: [${e}]`);let t=s.map(e=>`import ${e} from './children/${e}';`).join(`
4
- `);d.push({start:0,end:0,content:t+`
5
- `})}if(h.length>0){let e=(p?`,
6
- `:`
7
- `)+h.join(`,
8
- `);d.push({start:m,end:m,content:e})}}return d.length===0?{code:e,hasToolId:!1}:{code:O(e,d),hasToolId:!0}}async function A(e){let t=n.resolve(e.entry);await N(t,`入口目录不存在: ${t}`);let a=n.join(t,`config.ts`);await N(a,`找不到 config.ts 文件: ${a}`);let o=n.join(t,`index.ts`);await N(o,`找不到 index.ts 文件: ${o}`);let s=n.join(t,`.build-temp`);await M(s);let c=Date.now(),l=0,u=0,d=0;try{let o=Date.now(),f=await k({sourceCode:await r.readFile(a,`utf-8`),filePath:a});await r.writeFile(n.join(s,`config.ts`),f.code,`utf-8`),l=Date.now()-o;let m=Date.now();await j(t,s,t),u=Date.now()-m;let h=n.resolve(e.output);await M(h);let g=n.join(s,`index.ts`),_=Date.now();await i({entry:[g],outDir:h,format:[e.format],clean:!0,minify:e.minify,inlineOnly:!1,nodeProtocol:!0,platform:`node`,target:`node22`,dts:!1,treeshake:!0,noExternal:[`*`],outExtensions:()=>({dts:`.d.ts`,js:`.js`})}),d=Date.now()-_;let v=await r.readdir(h),y=Date.now()-c;return p.info([`构建阶段耗时统计:`,`• config 转换: ${P(l)}`,`• 源码复制: ${P(u)}`,`• tsdown 构建: ${P(d)}`,`• 总耗时: ${P(y)}`].join(`
9
- `)),{entryDir:t,outputDir:h,files:v.map(e=>e)}}finally{try{await r.rm(s,{recursive:!0,force:!0})}catch{}}}async function j(e,t,i){let a=await r.readdir(e,{withFileTypes:!0});for(let o of a){if(o.name===`node_modules`||o.name===`dist`||o.name.startsWith(`.build-`))continue;let a=n.join(e,o.name),s=n.join(t,o.name);if(o.isDirectory())await M(s),await j(a,s,i);else if(o.isFile()&&!(o.name===`config.ts`&&e===i))if(o.name===`config.ts`){let e=await k({sourceCode:await r.readFile(a,`utf-8`),filePath:a});await r.writeFile(s,e.code,`utf-8`)}else await r.copyFile(a,s)}}async function M(e){await r.mkdir(e,{recursive:!0})}async function N(e,t){try{await r.access(e)}catch{throw Error(t)}}function P(e){return e<1e3?`${e}ms`:`${(e/1e3).toFixed(2)}s`}var F=class extends b{register(e){e.command(`build`).description(`构建 FastGPT 插件为可分发的包`).option(`-e, --entry <path>`,`工具入口目录`,process.cwd()).option(`-o, --output <path>`,`输出目录`,`./dist`).option(`-m, --minify`,`压缩输出代码`,!1).option(`-f, --format <format>`,`输出格式: esm | cjs`,`esm`).action(async e=>{await this.run({entry:e.entry,output:e.output,minify:e.minify,format:e.format})})}async run(e){let t=Date.now();try{let n=await A(e),r=((Date.now()-t)/1e3).toFixed(2);p.success(`构建完成,用时 ${r}s`),p.info(`输出目录: ${n.outputDir}`),n.files.length>0?(p.info(`生成的文件列表:`),n.files.forEach(e=>{p.info(`• ${e}`)})):p.info(`未检测到输出文件,请检查配置。`)}catch(e){p.error(`构建失败,详情如下:`),p.error(e instanceof Error?e:Error(String(e))),p.info(`如果这是在 CI 中发生的,请查看上方构建日志以获取更多信息。`),process.exit(1)}}};const I={input:c,select:l};async function L(e,t){return e.input({...t,validate:e=>!e||e.trim().length===0?`请输入有效的内容`:!0})}var R=class{deps;constructor(e=I){this.deps=e}async run(e){let t=(()=>{if(e.typeFlag)return e.typeFlag===`tool-suite`?`tool-suite`:`tool`})();if(e.nameArg&&t&&e.descriptionFlag!==void 0)return{name:e.nameArg,cwd:e.cwd,type:t,description:e.descriptionFlag};let n=e.nameArg??await(async()=>await L(this.deps,{message:`插件名称`,default:`./packages/my-tool`}))(),r=t??await(async()=>await this.deps.select({message:`选择插件类型`,choices:[{name:`单工具`,value:`tool`,description:`创建一个独立的工具`},{name:`工具集`,value:`tool-suite`,description:`创建一个包含多个子工具的工具集`}]}))(),i=e.descriptionFlag??await(async()=>await L(this.deps,{message:`插件描述`,default:v}))();return{name:n,cwd:e.cwd,type:r,description:i}}},z=class extends b{register(e){e.command(`create`).description(`创建新的 FastGPT 插件项目`).argument(`[name]`,`插件名称`).option(`-t, --type <type>`,`插件类型: 单工具 | 工具集`).option(`-d, --description <desc>`,`插件描述`).option(`--cwd <path>`,`工作目录`,process.cwd()).action(async(e,t)=>{let n=await new R().run({nameArg:e,typeFlag:t.type,descriptionFlag:t.description,cwd:t.cwd});await this.run(n)})}async run(e){let t=n.resolve(e.cwd,e.name),i=n.join(y,e.type===`tool-suite`?`tool-suite`:`tool`),a=await this.collectTemplateFiles(i),o=e.description??v;await this.ensureDir(t);for(let s of a){let a=n.join(i,s),c=n.join(t,s);await this.ensureDir(n.dirname(c));let l=await r.readFile(a,`utf-8`),u=this.applyPlaceholders(l,e.name,o);await r.writeFile(c,u,`utf-8`)}p.success(`创建插件项目: ${e.name} (${e.type})`,{cwd:e.cwd})}applyPlaceholders(e,t,r){return t=n.basename(t),t=u(t),e.replace(/\{\{name\}\}/g,t).replace(/\{\{description\}\}/g,r)}async collectTemplateFiles(e){let t=[],i=async a=>{let o=await r.readdir(a,{withFileTypes:!0});for(let r of o){let o=n.join(a,r.name);if(r.isDirectory())await i(o);else{let r=n.relative(e,o).replace(/\\/g,`/`);t.push(r)}}};return await i(e),t.sort()}async ensureDir(e){await r.mkdir(e,{recursive:!0})}},B=class extends b{register(e){e.command(`pack`).description(`将 FastGPT 工具或工具集打包为 .pkg 文件`).option(`-e, --entry <path>`,`工具入口目录`,process.cwd()).option(`-o, --output <path>`,`输出目录(默认使用入口目录)`).option(`-n, --name <name>`,`包名称(默认使用入口目录名)`).action(async e=>{await this.run(e)})}async run(e){let t=Date.now(),i=n.resolve(e.entry),a=n.resolve(e.output||i),o=e.name||n.basename(i),s=n.join(a,`${o}.pkg`);try{await U(i,`入口目录不存在: ${i}`);let e=n.join(i,`dist`,`index.js`),o=n.join(i,`logo.svg`),c=n.join(i,`README.md`),l=n.join(i,`assets`),u=n.join(i,`children`);await U(e,`找不到 dist/index.js 文件: ${e}`),await H(a);let m=new f,h=d(s),g=new Promise((e,t)=>{m.outputStream.pipe(h).on(`close`,()=>e()),m.outputStream.on(`error`,e=>t(e)),h.on(`error`,e=>t(e))});if(m.addFile(e,`index.js`),await V(o)&&m.addFile(o,`logo.svg`),await V(c)&&m.addFile(c,`README.md`),await V(l)&&await W(m,l,`assets`),await V(u)){let e=await r.readdir(u,{withFileTypes:!0});for(let t of e){if(!t.isDirectory())continue;let e=t.name,r=n.join(u,e),i=n.join(r,`logo.svg`);m.addEmptyDirectory(`${e}/`),await V(i)&&m.addFile(i,`${e}/logo.svg`)}}m.end(),await g;let _=((Date.now()-t)/1e3).toFixed(2);p.success(`打包完成,用时 ${_}s`),p.info(`输出文件: ${s}`)}catch(e){p.error(`打包失败,详情如下:`),p.error(e instanceof Error?e:Error(String(e))),process.exit(1)}}};async function V(e){try{return await r.access(e),!0}catch{return!1}}async function H(e){await r.mkdir(e,{recursive:!0})}async function U(e,t){try{await r.access(e)}catch{throw Error(t)}}async function W(e,t,i){let a=await r.readdir(t,{withFileTypes:!0});for(let r of a){let a=n.join(t,r.name),o=n.join(i,r.name).split(n.sep).join(`/`);r.isDirectory()?(e.addEmptyDirectory(`${o}/`),await W(e,a,o)):r.isFile()&&e.addFile(a,o)}}function G(){let e=new t;return e.name(g).version(_).description(`FastGPT 插件开发 CLI`),new F().register(e),new z().register(e),new B().register(e),e}async function K(e){await G().parseAsync(e??process.argv)}process.on(`SIGINT`,()=>{process.exit(130)}),(async()=>{try{await K()}catch(e){e instanceof Error&&(e.name===`ExitPromptError`||e.message.includes(`SIGINT`)||e.message.includes(`force closed`))&&process.exit(130),p.error(e),process.exitCode=1}})();export{};
2
+ import { consola } from "consola/basic";
3
+ import { Command } from "commander";
4
+ import path from "node:path";
5
+ import fs from "node:fs/promises";
6
+ import { build } from "tsdown";
7
+ import { Visitor, parseSync } from "oxc-parser";
8
+ import { createHash } from "node:crypto";
9
+ import { input, select } from "@inquirer/prompts";
10
+ import { kebabCase } from "es-toolkit";
11
+ import { createWriteStream } from "node:fs";
12
+ import { ZipFile } from "yazl";
13
+
14
+ //#region src/helpers.ts
15
+ const logger = consola;
16
+
17
+ //#endregion
18
+ //#region package.json
19
+ var name = "@fastgpt-plugin/cli";
20
+ var version = "0.1.0-beta.3";
21
+
22
+ //#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");
29
+
30
+ //#endregion
31
+ //#region src/commands/base.ts
32
+ /**
33
+ * 所有子命令的基类:
34
+ * - 统一命令接口类型
35
+ * - 强制实现 register 方法,用于向 Commander 注册自身
36
+ */
37
+ var BaseCommand = class {};
38
+
39
+ //#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
+ }
119
+
120
+ //#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
+ }
216
+
217
+ //#endregion
218
+ //#region src/build/index.ts
219
+ /**
220
+ * 核心构建逻辑:给定入口目录和输出目录,完成一次工具构建。
221
+ * - 使用 logger 记录关键阶段和耗时
222
+ * - 不调用 process.exit
223
+ * - 失败时抛出 Error
224
+ */
225
+ async function buildToolPackage(options) {
226
+ 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}`);
230
+ const indexPath = path.join(entryDir, "index.ts");
231
+ await assertPathExists$1(indexPath, `找不到 index.ts 文件: ${indexPath}`);
232
+ const tempDir = path.join(entryDir, ".build-temp");
233
+ await ensureDir$1(tempDir);
234
+ const tStart = Date.now();
235
+ let tConfig = 0;
236
+ let tCopy = 0;
237
+ let tBuild = 0;
238
+ 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;
246
+ const tCopyStart = Date.now();
247
+ await copySourceFiles(entryDir, tempDir, entryDir);
248
+ tCopy = Date.now() - tCopyStart;
249
+ const outputDir = path.resolve(options.output);
250
+ await ensureDir$1(outputDir);
251
+ const tempIndexPath = path.join(tempDir, "index.ts");
252
+ const tBuildStart = Date.now();
253
+ await build({
254
+ entry: [tempIndexPath],
255
+ outDir: outputDir,
256
+ format: [options.format],
257
+ clean: true,
258
+ minify: options.minify,
259
+ inlineOnly: false,
260
+ nodeProtocol: true,
261
+ platform: "node",
262
+ target: "node22",
263
+ dts: false,
264
+ treeshake: true,
265
+ noExternal: ["*"],
266
+ outExtensions: () => ({
267
+ dts: ".d.ts",
268
+ js: ".js"
269
+ })
270
+ });
271
+ tBuild = Date.now() - tBuildStart;
272
+ const files = await fs.readdir(outputDir);
273
+ const tTotal = Date.now() - tStart;
274
+ logger.info([
275
+ "构建阶段耗时统计:",
276
+ `• config 转换: ${formatDuration(tConfig)}`,
277
+ `• 源码复制: ${formatDuration(tCopy)}`,
278
+ `• tsdown 构建: ${formatDuration(tBuild)}`,
279
+ `• 总耗时: ${formatDuration(tTotal)}`
280
+ ].join("\n"));
281
+ return {
282
+ entryDir,
283
+ outputDir,
284
+ files: files.map((f) => f)
285
+ };
286
+ } finally {
287
+ try {
288
+ await fs.rm(tempDir, {
289
+ recursive: true,
290
+ force: true
291
+ });
292
+ } catch {}
293
+ }
294
+ }
295
+ /**
296
+ * 从入口目录复制源码到临时目录:
297
+ * - 根目录的 config.ts 已被转换,不再复制
298
+ * - 子目录中的 config.ts 需要保留(子工具自身的配置)
299
+ */
300
+ async function copySourceFiles(sourceDir, targetDir, rootDir) {
301
+ const files = await fs.readdir(sourceDir, { withFileTypes: true });
302
+ for (const file of files) {
303
+ if (file.name === "node_modules" || file.name === "dist" || file.name.startsWith(".build-")) continue;
304
+ const sourcePath = path.join(sourceDir, file.name);
305
+ const targetPath = path.join(targetDir, file.name);
306
+ if (file.isDirectory()) {
307
+ 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);
317
+ }
318
+ }
319
+ }
320
+ async function ensureDir$1(dir) {
321
+ await fs.mkdir(dir, { recursive: true });
322
+ }
323
+ async function assertPathExists$1(p, message) {
324
+ try {
325
+ await fs.access(p);
326
+ } catch {
327
+ throw new Error(message);
328
+ }
329
+ }
330
+ function formatDuration(ms) {
331
+ if (ms < 1e3) return `${ms}ms`;
332
+ return `${(ms / 1e3).toFixed(2)}s`;
333
+ }
334
+
335
+ //#endregion
336
+ //#region src/commands/build.ts
337
+ var BuildCommand = class extends BaseCommand {
338
+ register(parent) {
339
+ parent.command("build").description("构建 FastGPT 插件为可分发的包").option("-e, --entry <path>", "工具入口目录", process.cwd()).option("-o, --output <path>", "输出目录", "./dist").option("-m, --minify", "压缩输出代码", false).option("-f, --format <format>", "输出格式: esm | cjs", "esm").action(async (opts) => {
340
+ await this.run({
341
+ entry: opts.entry,
342
+ output: opts.output,
343
+ minify: opts.minify,
344
+ format: opts.format
345
+ });
346
+ });
347
+ }
348
+ async run(options) {
349
+ const start = Date.now();
350
+ try {
351
+ const result = await buildToolPackage(options);
352
+ const duration = ((Date.now() - start) / 1e3).toFixed(2);
353
+ logger.success(`构建完成,用时 ${duration}s`);
354
+ logger.info(`输出目录: ${result.outputDir}`);
355
+ if (result.files.length > 0) {
356
+ logger.info("生成的文件列表:");
357
+ result.files.forEach((file) => {
358
+ logger.info(`• ${file}`);
359
+ });
360
+ } else logger.info("未检测到输出文件,请检查配置。");
361
+ } catch (error) {
362
+ logger.error("构建失败,详情如下:");
363
+ logger.error(error instanceof Error ? error : new Error(String(error)));
364
+ logger.info("如果这是在 CI 中发生的,请查看上方构建日志以获取更多信息。");
365
+ process.exit(1);
366
+ }
367
+ }
368
+ };
369
+
370
+ //#endregion
371
+ //#region src/prompts/create/deps.ts
372
+ const defaultDeps = {
373
+ input,
374
+ select
375
+ };
376
+
377
+ //#endregion
378
+ //#region src/prompts/create/input-prompt.ts
379
+ async function inputPrompt(deps, options) {
380
+ return deps.input({
381
+ ...options,
382
+ validate: (value) => {
383
+ if (!value || value.trim().length === 0) return "请输入有效的内容";
384
+ return true;
385
+ }
386
+ });
387
+ }
388
+
389
+ //#endregion
390
+ //#region src/prompts/create/prompt.ts
391
+ var CreatePrompt = class {
392
+ deps;
393
+ constructor(deps = defaultDeps) {
394
+ this.deps = deps;
395
+ }
396
+ /**
397
+ * 根据 CLI 传入的原始参数,交互式补全缺失字段,生成完整的 CreatePluginCommandOptions。
398
+ * - 如果 name、type、description 都已提供,则不进入交互流程,直接返回
399
+ * - 否则使用交互式 prompt 补全缺失部分
400
+ */
401
+ async run(input) {
402
+ const getPluginType = () => {
403
+ if (!input.typeFlag) return void 0;
404
+ return input.typeFlag === "tool-suite" ? "tool-suite" : "tool";
405
+ };
406
+ const pluginType = getPluginType();
407
+ if (input.nameArg && pluginType && input.descriptionFlag !== void 0) return {
408
+ name: input.nameArg,
409
+ cwd: input.cwd,
410
+ type: pluginType,
411
+ description: input.descriptionFlag
412
+ };
413
+ const getPluginNameInputPrompt = async () => {
414
+ return await inputPrompt(this.deps, {
415
+ message: "插件名称",
416
+ default: DEFAULT_PLUGIN_NAME
417
+ });
418
+ };
419
+ const name = input.nameArg ?? await getPluginNameInputPrompt();
420
+ const getPluginTypeSelectPrompt = async () => {
421
+ return await this.deps.select({
422
+ message: "选择插件类型",
423
+ choices: [{
424
+ name: "单工具",
425
+ value: "tool",
426
+ description: "创建一个独立的工具"
427
+ }, {
428
+ name: "工具集",
429
+ value: "tool-suite",
430
+ description: "创建一个包含多个子工具的工具集"
431
+ }]
432
+ });
433
+ };
434
+ const type = pluginType ?? await getPluginTypeSelectPrompt();
435
+ const getPluginDescriptionInputPrompt = async () => {
436
+ return await inputPrompt(this.deps, {
437
+ message: "插件描述",
438
+ default: DEFAULT_PLUGIN_DESCRIPTION
439
+ });
440
+ };
441
+ const description = input.descriptionFlag ?? await getPluginDescriptionInputPrompt();
442
+ return {
443
+ name,
444
+ cwd: input.cwd,
445
+ type,
446
+ description
447
+ };
448
+ }
449
+ };
450
+
451
+ //#endregion
452
+ //#region src/commands/create.ts
453
+ var CreateCommand = class extends BaseCommand {
454
+ register(parent) {
455
+ parent.command("create").description("创建新的 FastGPT 插件项目").argument("[name]", "插件名称").option("-t, --type <type>", "插件类型: 单工具 | 工具集").option("-d, --description <desc>", "插件描述").option("--cwd <path>", "工作目录", process.cwd()).action(async (name, opts) => {
456
+ const options = await new CreatePrompt().run({
457
+ nameArg: name,
458
+ typeFlag: opts.type,
459
+ descriptionFlag: opts.description,
460
+ cwd: opts.cwd
461
+ });
462
+ await this.run(options);
463
+ });
464
+ }
465
+ async run(options) {
466
+ const targetDir = path.resolve(options.cwd, options.name);
467
+ const templateDir = path.join(TOOL_TEMPLATES_DIR, options.type === "tool-suite" ? "tool-suite" : "tool");
468
+ const files = await this.collectTemplateFiles(templateDir);
469
+ const description = options.description ?? DEFAULT_PLUGIN_DESCRIPTION;
470
+ await this.ensureDir(targetDir);
471
+ for (const rel of files) {
472
+ const templatePath = path.join(templateDir, rel);
473
+ const filePath = path.join(targetDir, rel);
474
+ await this.ensureDir(path.dirname(filePath));
475
+ const raw = await fs.readFile(templatePath, "utf-8");
476
+ const content = this.applyPlaceholders(raw, options.name, description);
477
+ await fs.writeFile(filePath, content, "utf-8");
478
+ }
479
+ logger.success(`创建插件项目: ${options.name} (${options.type})`, { cwd: options.cwd });
480
+ }
481
+ applyPlaceholders(text, name, description) {
482
+ name = path.basename(name);
483
+ name = kebabCase(name);
484
+ return text.replace(/\{\{name\}\}/g, name).replace(/\{\{description\}\}/g, description);
485
+ }
486
+ async collectTemplateFiles(root) {
487
+ const result = [];
488
+ const walk = async (dir) => {
489
+ const entries = await fs.readdir(dir, { withFileTypes: true });
490
+ for (const entry of entries) {
491
+ const full = path.join(dir, entry.name);
492
+ if (entry.isDirectory()) await walk(full);
493
+ else {
494
+ const rel = path.relative(root, full).replace(/\\/g, "/");
495
+ result.push(rel);
496
+ }
497
+ }
498
+ };
499
+ await walk(root);
500
+ return result.sort();
501
+ }
502
+ async ensureDir(dir) {
503
+ await fs.mkdir(dir, { recursive: true });
504
+ }
505
+ };
506
+
507
+ //#endregion
508
+ //#region src/commands/pack.ts
509
+ var PackCommand = class extends BaseCommand {
510
+ 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) => {
512
+ await this.run(opts);
513
+ });
514
+ }
515
+ async run(options) {
516
+ const start = Date.now();
517
+ const entryDir = path.resolve(options.entry);
518
+ const outputDir = path.resolve(options.output || entryDir);
519
+ const toolName = options.name || path.basename(entryDir);
520
+ const pkgPath = path.join(outputDir, `${toolName}.pkg`);
521
+ 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));
536
+ });
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;
554
+ const duration = ((Date.now() - start) / 1e3).toFixed(2);
555
+ logger.success(`打包完成,用时 ${duration}s`);
556
+ logger.info(`输出文件: ${pkgPath}`);
557
+ } catch (error) {
558
+ logger.error("打包失败,详情如下:");
559
+ logger.error(error instanceof Error ? error : new Error(String(error)));
560
+ process.exit(1);
561
+ }
562
+ }
563
+ };
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);
580
+ }
581
+ }
582
+ async function addDirectoryToZip(zipFile, dir, rootInZip) {
583
+ const entries = await fs.readdir(dir, { withFileTypes: true });
584
+ 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("/");
587
+ if (entry.isDirectory()) {
588
+ zipFile.addEmptyDirectory(`${relPath}/`);
589
+ await addDirectoryToZip(zipFile, fullPath, relPath);
590
+ } else if (entry.isFile()) zipFile.addFile(fullPath, relPath);
591
+ }
592
+ }
593
+
594
+ //#endregion
595
+ //#region src/cmd.ts
596
+ function createProgram() {
597
+ const program = new Command();
598
+ program.name(CLI_NAME).version(CLI_VERSION).description("FastGPT 插件开发 CLI");
599
+ new BuildCommand().register(program);
600
+ new CreateCommand().register(program);
601
+ new PackCommand().register(program);
602
+ return program;
603
+ }
604
+ /**
605
+ * 运行 CLI,解析 process.argv。
606
+ * @param argv 可选,用于测试时注入参数(如 ['node', 'cli', '--version'])
607
+ */
608
+ async function run(argv) {
609
+ await createProgram().parseAsync(argv ?? process.argv);
610
+ }
611
+
612
+ //#endregion
613
+ //#region src/index.ts
614
+ process.on("SIGINT", () => {
615
+ process.exit(130);
616
+ });
617
+ const main = async () => {
618
+ try {
619
+ await run();
620
+ } catch (error) {
621
+ if (error instanceof Error && (error.name === "ExitPromptError" || error.message.includes("SIGINT") || error.message.includes("force closed"))) process.exit(130);
622
+ logger.error(error);
623
+ process.exitCode = 1;
624
+ }
625
+ };
626
+ main();
627
+
628
+ //#endregion
629
+ export { };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fastgpt-plugin/cli",
3
- "version": "0.1.0-beta.2",
3
+ "version": "0.1.0-beta.3",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "publishConfig": {
@@ -22,7 +22,7 @@
22
22
  "prepublishOnly": "npm run build"
23
23
  },
24
24
  "dependencies": {
25
- "@fastgpt-plugin/helpers": "0.0.1-alpha.3",
25
+ "@fastgpt-plugin/helpers": "0.0.1-alpha.7",
26
26
  "@oxc-parser/binding-wasm32-wasi": "^0.112.0",
27
27
  "@inquirer/prompts": "^8.2.0",
28
28
  "commander": "^14.0.3",
@@ -5,7 +5,7 @@
5
5
  ## 开发
6
6
 
7
7
  ```bash
8
- bun install
9
- bun run build
10
- bun run test
8
+ pnpm install
9
+ pnpm run build
10
+ pnpm run test
11
11
  ```
@@ -10,7 +10,6 @@ export default defineTool({
10
10
  'zh-CN': '{{description}}',
11
11
  en: '{{description}}'
12
12
  },
13
- icon: 'core/workflow/template/{{name}}',
14
13
  versionList: [
15
14
  {
16
15
  value: '0.0.1',
@@ -12,6 +12,8 @@
12
12
  "isolatedModules": true,
13
13
  "noUncheckedSideEffectImports": true,
14
14
  "moduleDetection": "force",
15
- "skipLibCheck": true
16
- }
15
+ "skipLibCheck": true,
16
+ "noEmit": true
17
+ },
18
+ "exclude": ["**/*.test.ts", "**/*.spec.ts", "**/*.test.tsx", "**/*.spec.tsx"]
17
19
  }
@@ -5,7 +5,7 @@
5
5
  ## 开发
6
6
 
7
7
  ```bash
8
- bun install
9
- bun run build
10
- bun run test
8
+ pnpm install
9
+ pnpm run build
10
+ pnpm run test
11
11
  ```
@@ -10,7 +10,6 @@ export default defineTool({
10
10
  'zh-CN': '{{description}}',
11
11
  en: '{{description}}'
12
12
  },
13
- icon: 'core/workflow/template/{{name}}',
14
13
  versionList: [
15
14
  {
16
15
  value: '0.0.1',
@@ -1,29 +1,14 @@
1
- import { defineTool, ToolTagEnum, WorkflowIOValueTypeEnum } from '@fastgpt-plugin/helpers';
1
+ import { defineToolSet, ToolTagEnum } from '@fastgpt-plugin/helpers';
2
2
 
3
- export default defineTool({
4
- tags: [ToolTagEnum.tools],
3
+ export default defineToolSet({
5
4
  name: {
6
5
  'zh-CN': '{{name}}',
7
6
  en: '{{name}}'
8
7
  },
8
+ tags: [ToolTagEnum.tools],
9
9
  description: {
10
10
  'zh-CN': '{{description}}',
11
11
  en: '{{description}}'
12
12
  },
13
- icon: 'core/workflow/template/{{name}}',
14
- versionList: [
15
- {
16
- value: '0.0.1',
17
- description: 'Default version',
18
- inputs: [],
19
- outputs: [
20
- {
21
- key: 'time',
22
- valueType: WorkflowIOValueTypeEnum.string,
23
- label: '时间',
24
- description: '当前时间'
25
- }
26
- ]
27
- }
28
- ]
13
+ secretInputConfig: []
29
14
  });
@@ -1,2 +1,6 @@
1
- export { default as config } from './config';
2
- export { default as toolFromChild } from './children/tool/index';
1
+ import { exportToolSet } from '@fastgpt-plugin/helpers/tools/helper';
2
+ import config from './config';
3
+
4
+ export default exportToolSet({
5
+ config
6
+ });
@@ -12,6 +12,8 @@
12
12
  "isolatedModules": true,
13
13
  "noUncheckedSideEffectImports": true,
14
14
  "moduleDetection": "force",
15
- "skipLibCheck": true
16
- }
15
+ "skipLibCheck": true,
16
+ "noEmit": true
17
+ },
18
+ "exclude": ["**/*.test.ts", "**/*.spec.ts", "**/*.test.tsx", "**/*.spec.tsx"]
17
19
  }