@seastudio/sdk 3.0.5 → 3.1.0

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.
@@ -1,1210 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- var commander = require('commander');
5
- var inquirer = require('inquirer');
6
- var promises = require('fs/promises');
7
- var path = require('path');
8
- var fs = require('fs');
9
- var url = require('url');
10
- var chalk = require('chalk');
11
- var ora2 = require('ora');
12
- var child_process = require('child_process');
13
- var os = require('os');
14
- var https = require('https');
15
- var promises$1 = require('stream/promises');
16
- var archiver = require('archiver');
17
- var tar = require('tar');
18
- var unzipper = require('unzipper');
19
-
20
- var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
21
- function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
22
-
23
- var inquirer__default = /*#__PURE__*/_interopDefault(inquirer);
24
- var chalk__default = /*#__PURE__*/_interopDefault(chalk);
25
- var ora2__default = /*#__PURE__*/_interopDefault(ora2);
26
- var https__default = /*#__PURE__*/_interopDefault(https);
27
- var archiver__default = /*#__PURE__*/_interopDefault(archiver);
28
-
29
- var __filename$1 = url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
30
- var __dirname$1 = path.dirname(__filename$1);
31
- function validatePluginId(input) {
32
- if (!input.trim()) {
33
- return "\u63D2\u4EF6 ID \u4E0D\u80FD\u4E3A\u7A7A";
34
- }
35
- if (input.startsWith("seastudio.")) {
36
- return '\u63D2\u4EF6 ID \u4E0D\u80FD\u4EE5 "seastudio." \u5F00\u5934\uFF08\u4FDD\u7559\u524D\u7F00\uFF09';
37
- }
38
- if (!/^[a-z0-9-]+\.[a-z0-9-]+$/.test(input)) {
39
- return '\u63D2\u4EF6 ID \u683C\u5F0F\u5E94\u4E3A "namespace.name"\uFF0C\u4F8B\u5982 "mycompany.my-plugin"';
40
- }
41
- return true;
42
- }
43
- function validateProtocolHost(input) {
44
- if (!input.trim()) {
45
- return "Protocol Host \u4E0D\u80FD\u4E3A\u7A7A";
46
- }
47
- if (!/^[a-z0-9-]+$/.test(input)) {
48
- return "Protocol Host \u53EA\u80FD\u5305\u542B\u5C0F\u5199\u5B57\u6BCD\u3001\u6570\u5B57\u548C\u8FDE\u5B57\u7B26";
49
- }
50
- return true;
51
- }
52
- async function getTemplatesDir() {
53
- const possiblePaths = [
54
- // 开发时:相对于 src 目录
55
- path.resolve(__dirname$1, "../../templates"),
56
- // 打包后:相对于 dist 目录
57
- path.resolve(__dirname$1, "../../../src/develop-tool/templates"),
58
- // npm 安装后:相对于 node_modules
59
- path.resolve(__dirname$1, "../../../../src/develop-tool/templates")
60
- ];
61
- for (const p of possiblePaths) {
62
- if (fs.existsSync(p)) {
63
- return p;
64
- }
65
- }
66
- throw new Error("\u65E0\u6CD5\u627E\u5230\u6A21\u677F\u76EE\u5F55");
67
- }
68
- async function copyDirectory(src, dest, replacements) {
69
- await promises.mkdir(dest, { recursive: true });
70
- const entries = await promises.readdir(src, { withFileTypes: true });
71
- for (const entry of entries) {
72
- const srcPath = path.join(src, entry.name);
73
- let destName = entry.name;
74
- if (destName.endsWith(".tmpl")) {
75
- destName = destName.slice(0, -5);
76
- }
77
- const destPath = path.join(dest, destName);
78
- if (entry.isDirectory()) {
79
- await copyDirectory(srcPath, destPath, replacements);
80
- } else {
81
- let content = await promises.readFile(srcPath, "utf-8");
82
- for (const [key, value] of Object.entries(replacements)) {
83
- content = content.replace(new RegExp(`\\{\\{${key}\\}\\}`, "g"), value);
84
- }
85
- await promises.writeFile(destPath, content, "utf-8");
86
- }
87
- }
88
- }
89
- var createCommand = new commander.Command("create").description("\u521B\u5EFA\u65B0\u7684 SeaStudio \u63D2\u4EF6\u6216 Agent \u9879\u76EE").argument("[directory]", "\u9879\u76EE\u76EE\u5F55\u540D\u79F0").option("--type <type>", "\u9879\u76EE\u7C7B\u578B (plugin | agent)").action(async (directory, options) => {
90
- console.log(chalk__default.default.cyan("\n\u{1F680} SeaStudio \u521B\u5EFA\u5411\u5BFC\n"));
91
- const answers = await inquirer__default.default.prompt([
92
- {
93
- type: "list",
94
- name: "projectType",
95
- message: "\u9879\u76EE\u7C7B\u578B:",
96
- choices: [
97
- { name: "Plugin", value: "plugin" },
98
- { name: "Agent", value: "agent" }
99
- ],
100
- default: options?.type === "agent" ? "agent" : "plugin",
101
- when: () => options?.type !== "plugin" && options?.type !== "agent"
102
- },
103
- {
104
- type: "input",
105
- name: "pluginId",
106
- message: (current) => current.projectType === "agent" ? "Agent ID (\u4F8B\u5982 mycompany.my-agent):" : "\u63D2\u4EF6 ID (\u4F8B\u5982 mycompany.awesome-tool):",
107
- validate: validatePluginId
108
- },
109
- {
110
- type: "input",
111
- name: "pluginName",
112
- message: (current) => current.projectType === "agent" ? "Agent \u540D\u79F0:" : "\u63D2\u4EF6\u540D\u79F0:",
113
- validate: (input) => input.trim() ? true : "\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A"
114
- },
115
- {
116
- type: "input",
117
- name: "protocolHost",
118
- message: "Protocol Host (\u7528\u4E8E URL\uFF0C\u4F8B\u5982 awesome-tool):",
119
- default: (answers2) => {
120
- const parts = answers2.pluginId?.split(".");
121
- return parts?.[parts.length - 1] || "";
122
- },
123
- validate: validateProtocolHost
124
- },
125
- {
126
- type: "list",
127
- name: "category",
128
- message: "\u63D2\u4EF6\u5206\u7C7B:",
129
- choices: [
130
- { name: "B - \u6807\u7B7E\u9875\u533A\u57DF\uFF08\u63A8\u8350\uFF09", value: "B" },
131
- { name: "A - \u56FA\u5B9A\u533A\u57DF", value: "A" }
132
- ],
133
- default: "B",
134
- when: (current) => (options?.type || current.projectType) !== "agent"
135
- },
136
- {
137
- type: "list",
138
- name: "backendMode",
139
- message: "Agent \u540E\u7AEF\u6A21\u5F0F:",
140
- choices: [
141
- { name: "external - \u8FDE\u63A5\u5916\u90E8\u540E\u7AEF", value: "external" },
142
- { name: "hosted - Host \u6258\u7BA1\u672C\u5730\u540E\u7AEF", value: "hosted" }
143
- ],
144
- default: "external",
145
- when: (current) => (options?.type || current.projectType) === "agent"
146
- },
147
- {
148
- type: "input",
149
- name: "backendUrl",
150
- message: "\u9ED8\u8BA4\u540E\u7AEF\u5730\u5740:",
151
- default: "http://localhost:3000",
152
- when: (current) => current.backendMode === "external"
153
- },
154
- {
155
- type: "list",
156
- name: "backendRuntime",
157
- message: "Host \u6258\u7BA1\u540E\u7AEF\u8FD0\u884C\u65F6:",
158
- choices: [
159
- { name: "Python", value: "python" },
160
- { name: "Node.js", value: "node" }
161
- ],
162
- default: "python",
163
- when: (current) => current.backendMode === "hosted"
164
- },
165
- {
166
- type: "input",
167
- name: "backendEntry",
168
- message: "Host \u6258\u7BA1\u540E\u7AEF\u5165\u53E3\u6587\u4EF6:",
169
- default: (current) => current.backendRuntime === "node" ? "server.js" : "run.py",
170
- when: (current) => current.backendMode === "hosted"
171
- },
172
- {
173
- type: "number",
174
- name: "backendPort",
175
- message: "Host \u6258\u7BA1\u540E\u7AEF\u9ED8\u8BA4\u7AEF\u53E3:",
176
- default: 9201,
177
- when: (current) => current.backendMode === "hosted"
178
- }
179
- ]);
180
- const projectType = options?.type === "plugin" || options?.type === "agent" ? options.type : answers.projectType;
181
- const projectDir = directory || answers.protocolHost;
182
- const fullPath = path.resolve(process.cwd(), projectDir);
183
- if (fs.existsSync(fullPath)) {
184
- console.log(chalk__default.default.red(`
185
- \u274C \u76EE\u5F55 "${projectDir}" \u5DF2\u5B58\u5728
186
- `));
187
- process.exit(1);
188
- }
189
- const spinner = ora2__default.default(`\u521B\u5EFA${projectType === "agent" ? " Agent" : "\u63D2\u4EF6"}\u9879\u76EE...`).start();
190
- try {
191
- const templatesDir = await getTemplatesDir();
192
- const pluginTemplateDir = path.join(templatesDir, "plugin");
193
- if (!fs.existsSync(pluginTemplateDir)) {
194
- throw new Error(`\u6A21\u677F\u76EE\u5F55\u4E0D\u5B58\u5728: ${pluginTemplateDir}`);
195
- }
196
- const replacements = {
197
- pluginId: answers.pluginId,
198
- pluginName: answers.pluginName,
199
- protocolHost: answers.protocolHost,
200
- category: answers.category || "B"
201
- };
202
- await copyDirectory(pluginTemplateDir, fullPath, replacements);
203
- if (projectType === "agent") {
204
- const backendMode = answers.backendMode || "external";
205
- const agentConfig = backendMode === "external" ? {
206
- type: "agent",
207
- id: answers.pluginId,
208
- name: answers.pluginName,
209
- version: "1.0.0",
210
- protocolHost: answers.protocolHost,
211
- frontendDir: "frontend",
212
- backend: {
213
- mode: "external",
214
- url: answers.backendUrl || "http://localhost:3000",
215
- healthEndpoint: "/agent/info"
216
- }
217
- } : {
218
- type: "agent",
219
- id: answers.pluginId,
220
- name: answers.pluginName,
221
- version: "1.0.0",
222
- protocolHost: answers.protocolHost,
223
- frontendDir: "frontend",
224
- backendDir: "backend",
225
- backend: {
226
- mode: "hosted",
227
- runtime: answers.backendRuntime || "python",
228
- entry: answers.backendEntry || "run.py",
229
- port: answers.backendPort || 9201,
230
- healthEndpoint: "/agent/info",
231
- autoStart: true
232
- }
233
- };
234
- const agentReadme = `# ${answers.pluginName}
235
-
236
- SeaStudio \u7B2C\u4E09\u65B9 Agent
237
-
238
- ## \u5F00\u53D1
239
-
240
- \`\`\`bash
241
- cd frontend
242
- npm install
243
- npm run dev
244
- \`\`\`
245
-
246
- ## \u6784\u5EFA
247
-
248
- \`\`\`bash
249
- cd frontend
250
- npm run build
251
- \`\`\`
252
-
253
- ## \u6253\u5305
254
-
255
- \`\`\`bash
256
- seastudio pack
257
- \`\`\`
258
-
259
- \u6253\u5305\u540E\u4F1A\u751F\u6210 \`${answers.protocolHost}.seaagent\` \u6587\u4EF6\u3002
260
-
261
- ## \u8FD0\u884C\u6A21\u5F0F
262
-
263
- - backend.mode: \`${backendMode}\`
264
- - protocolHost: \`${answers.protocolHost}\`
265
-
266
- ## \u5B89\u88C5
267
-
268
- 1. \u6253\u5F00 SeaStudio
269
- 2. \u8BBE\u7F6E \u2192 Agent \u7BA1\u7406 \u2192 \u5BFC\u5165 Agent
270
- 3. \u9009\u62E9 \`.seaagent\` \u6587\u4EF6
271
-
272
- ## \u8BB8\u53EF\u8BC1
273
-
274
- MIT
275
- `;
276
- const agentApp = `import { useEffect, useMemo, useState } from 'react';
277
-
278
- interface InitState {
279
- agentId: string;
280
- backendUrl: string;
281
- backendMode: 'external' | 'hosted';
282
- healthEndpoint: string;
283
- theme: 'light' | 'dark';
284
- }
285
-
286
- function App() {
287
- const [initState, setInitState] = useState<InitState | null>(null);
288
-
289
- useEffect(() => {
290
- const readyPayload = {
291
- type: 'mcp',
292
- payload: {
293
- jsonrpc: '2.0',
294
- method: 'agent:ready',
295
- params: {},
296
- },
297
- };
298
- window.parent.postMessage(readyPayload, '*');
299
-
300
- const handleMessage = (event: MessageEvent) => {
301
- const data = event.data as {
302
- type?: string;
303
- payload?: { method?: string; params?: Record<string, unknown> };
304
- };
305
- if (data?.type !== 'mcp' || data?.payload?.method !== 'seastudio:init') {
306
- return;
307
- }
308
- const params = data.payload.params || {};
309
- setInitState({
310
- agentId: String(params.agentId || ''),
311
- backendUrl: String(params.backendUrl || ''),
312
- backendMode: (params.backendMode === 'hosted' ? 'hosted' : 'external'),
313
- healthEndpoint: String(params.healthEndpoint || '/agent/info'),
314
- theme: params.theme === 'light' ? 'light' : 'dark',
315
- });
316
- };
317
-
318
- window.addEventListener('message', handleMessage);
319
- return () => window.removeEventListener('message', handleMessage);
320
- }, []);
321
-
322
- const statusText = useMemo(() => {
323
- if (!initState?.backendUrl) {
324
- return '\u7B49\u5F85 Host \u6CE8\u5165\u540E\u7AEF\u914D\u7F6E';
325
- }
326
- return initState.backendUrl;
327
- }, [initState]);
328
-
329
- return (
330
- <div className="min-h-screen bg-background text-foreground p-6">
331
- <h1 className="text-2xl font-bold mb-4">${answers.pluginName}</h1>
332
- <div className="space-y-4">
333
- <div className="p-4 rounded-lg bg-muted/50 border border-border">
334
- <h2 className="text-sm font-medium text-muted-foreground mb-2">\u8FD0\u884C\u4FE1\u606F</h2>
335
- <p className="text-sm">Agent ID: {initState?.agentId || '${answers.pluginId}'}</p>
336
- <p className="text-sm">\u540E\u7AEF\u6A21\u5F0F: {initState?.backendMode || '${backendMode}'}</p>
337
- <p className="text-sm break-all">\u540E\u7AEF\u5730\u5740: {statusText}</p>
338
- <p className="text-sm">\u5065\u5EB7\u68C0\u67E5: {initState?.healthEndpoint || '/agent/info'}</p>
339
- </div>
340
- <div className="p-4 rounded-lg bg-muted/50 border border-border">
341
- <h2 className="text-sm font-medium text-muted-foreground mb-2">\u5F00\u59CB\u5F00\u53D1</h2>
342
- <p className="text-sm text-muted-foreground">
343
- \u7F16\u8F91\u6B64\u6587\u4EF6 (frontend/src/App.tsx) \u6765\u6784\u5EFA\u4F60\u7684 Agent \u524D\u7AEF\u3002
344
- </p>
345
- </div>
346
- </div>
347
- </div>
348
- );
349
- }
350
-
351
- export default App;
352
- `;
353
- await promises.writeFile(path.join(fullPath, "seastudio.config.json"), `${JSON.stringify(agentConfig, null, 2)}
354
- `, "utf-8");
355
- await promises.writeFile(path.join(fullPath, "README.md"), agentReadme, "utf-8");
356
- await promises.writeFile(path.join(fullPath, "frontend", "src", "App.tsx"), agentApp, "utf-8");
357
- if (backendMode === "hosted") {
358
- await promises.mkdir(path.join(fullPath, "backend"), { recursive: true });
359
- if ((answers.backendRuntime || "python") === "python") {
360
- const pythonEntry = `from fastapi import FastAPI
361
- import uvicorn
362
- import os
363
-
364
- app = FastAPI()
365
-
366
- @app.get("/health")
367
- def health():
368
- return {"success": True}
369
-
370
- @app.get("/agent/info")
371
- def info():
372
- return {
373
- "id": "${answers.pluginId}",
374
- "name": "${answers.pluginName}",
375
- "description": "Hosted Agent backend"
376
- }
377
-
378
- if __name__ == "__main__":
379
- port = int(os.getenv("PORT", "${answers.backendPort || 9201}"))
380
- uvicorn.run(app, host="127.0.0.1", port=port)
381
- `;
382
- await promises.writeFile(path.join(fullPath, "backend", answers.backendEntry || "run.py"), pythonEntry, "utf-8");
383
- } else {
384
- const nodeEntry = `import express from 'express';
385
-
386
- const app = express();
387
- const port = Number(process.env.PORT || ${answers.backendPort || 9201});
388
-
389
- app.get('/health', (_req, res) => {
390
- res.json({ success: true });
391
- });
392
-
393
- app.get('/agent/info', (_req, res) => {
394
- res.json({
395
- id: '${answers.pluginId}',
396
- name: '${answers.pluginName}',
397
- description: 'Hosted Agent backend',
398
- });
399
- });
400
-
401
- app.listen(port, '127.0.0.1', () => {
402
- console.log(\`Agent backend listening on \${port}\`);
403
- });
404
- `;
405
- const backendPackage = {
406
- name: `${answers.pluginId}-backend`,
407
- private: true,
408
- type: "module",
409
- dependencies: {
410
- express: "^4.21.2"
411
- }
412
- };
413
- await promises.writeFile(path.join(fullPath, "backend", answers.backendEntry || "server.js"), nodeEntry, "utf-8");
414
- await promises.writeFile(path.join(fullPath, "backend", "package.json"), `${JSON.stringify(backendPackage, null, 2)}
415
- `, "utf-8");
416
- }
417
- }
418
- }
419
- spinner.succeed(`${projectType === "agent" ? "Agent" : "\u63D2\u4EF6"}\u9879\u76EE\u521B\u5EFA\u6210\u529F`);
420
- console.log(chalk__default.default.green(`
421
- \u2705 ${projectType === "agent" ? "Agent" : "\u63D2\u4EF6"}\u9879\u76EE\u5DF2\u521B\u5EFA\u5728 ${chalk__default.default.bold(fullPath)}
422
- `));
423
- console.log(chalk__default.default.cyan("\u4E0B\u4E00\u6B65\u64CD\u4F5C:\n"));
424
- console.log(` ${chalk__default.default.yellow("cd")} ${projectDir}/frontend`);
425
- console.log(` ${chalk__default.default.yellow("npm install")}`);
426
- console.log(` ${chalk__default.default.yellow("npm run dev")}
427
- `);
428
- console.log(chalk__default.default.cyan("\u6253\u5305\u53D1\u5E03:\n"));
429
- console.log(` ${chalk__default.default.yellow("npm run build")}`);
430
- console.log(` ${chalk__default.default.yellow("cd ..")}`);
431
- console.log(` ${chalk__default.default.yellow("seastudio pack")}
432
- `);
433
- } catch (error) {
434
- spinner.fail("\u521B\u5EFA\u5931\u8D25");
435
- console.error(chalk__default.default.red(`
436
- \u274C ${error instanceof Error ? error.message : "\u672A\u77E5\u9519\u8BEF"}
437
- `));
438
- process.exit(1);
439
- }
440
- });
441
- function normalizeTools(tools) {
442
- return tools.map((tool) => JSON.parse(JSON.stringify({
443
- name: tool.name,
444
- description: tool.description,
445
- inputSchema: tool.inputSchema,
446
- ...tool.outputSchema ? { outputSchema: tool.outputSchema } : {},
447
- ...tool.annotations ? { annotations: tool.annotations } : {}
448
- })));
449
- }
450
- function loadManifestFromEntry(entryPath) {
451
- const readerScript = `
452
- import { pathToFileURL } from 'node:url';
453
- const entryPath = process.argv[1];
454
- const mod = await import(pathToFileURL(entryPath).href);
455
- const manifest = mod.pluginMcpManifest ?? (Array.isArray(mod.mcpTools)
456
- ? { schemaVersion: 1, tools: mod.mcpTools }
457
- : null);
458
- if (!manifest) {
459
- throw new Error('frontend/src/mcp-entry.ts \u5FC5\u987B\u5BFC\u51FA pluginMcpManifest \u6216 mcpTools');
460
- }
461
- process.stdout.write(JSON.stringify(manifest));
462
- `;
463
- const result = child_process.spawnSync(process.execPath, [
464
- "--experimental-strip-types",
465
- "--experimental-specifier-resolution=node",
466
- "--input-type=module",
467
- "-e",
468
- readerScript,
469
- entryPath
470
- ], {
471
- encoding: "utf-8"
472
- });
473
- if (result.status !== 0) {
474
- throw new Error((result.stderr || result.stdout || "\u8BFB\u53D6 frontend/src/mcp-entry.ts \u5931\u8D25").trim());
475
- }
476
- const parsed = JSON.parse(result.stdout || "{}");
477
- return {
478
- schemaVersion: 1,
479
- tools: normalizeTools(parsed.tools ?? [])
480
- };
481
- }
482
- async function generatePluginMcpManifest(projectDir) {
483
- const frontendDir = fs.existsSync(path.join(projectDir, "src")) ? projectDir : path.join(projectDir, "frontend");
484
- const entryPath = path.join(frontendDir, "src", "mcp-entry.ts");
485
- const outputPath = path.join(frontendDir, "dist", "mcp-manifest.json");
486
- if (!fs.existsSync(entryPath)) {
487
- throw new Error(`\u672A\u627E\u5230\u6807\u51C6 MCP \u5165\u53E3\u6587\u4EF6: ${entryPath}`);
488
- }
489
- const manifest = loadManifestFromEntry(entryPath);
490
- await promises.mkdir(path.dirname(outputPath), { recursive: true });
491
- await promises.writeFile(outputPath, `${JSON.stringify(manifest, null, 2)}
492
- `, "utf-8");
493
- return {
494
- outputPath,
495
- toolCount: manifest.tools.length
496
- };
497
- }
498
- var generateMcpManifestCommand = new commander.Command("generate-mcp-manifest").description("\u4ECE frontend/src/mcp-entry.ts \u751F\u6210 dist/mcp-manifest.json").option("-d, --dir <directory>", "\u63D2\u4EF6\u9879\u76EE\u76EE\u5F55", ".").action(async (options) => {
499
- try {
500
- const projectDir = path.resolve(process.cwd(), options.dir || ".");
501
- const result = await generatePluginMcpManifest(projectDir);
502
- console.log(`Generated ${result.outputPath} (${result.toolCount} tools)`);
503
- } catch (error) {
504
- console.error(error instanceof Error ? error.message : String(error));
505
- process.exit(1);
506
- }
507
- });
508
- var PYTHON_STANDALONE_RELEASE_API = "https://api.github.com/repos/astral-sh/python-build-standalone/releases/latest";
509
- var TARGET_PYTHON_MINOR = Number(process.env.SEASTUDIO_AGENT_PYTHON_MINOR) || 12;
510
- var MAX_PYTHON_MINOR = 13;
511
- var PYTHON_RUNTIME_DIRNAME = ".python-runtime";
512
- var PYTHON_VENDOR_DIRNAME = ".seastudio-python-vendor";
513
- var PYTHON_RUNTIME_MANIFEST = ".seastudio-python-runtime-manifest.json";
514
- var PYTHON_VENDOR_MANIFEST = ".seastudio-python-vendor-manifest.json";
515
- var PYTHON_CACHE_DIR = path.join(os.tmpdir(), "seastudio-python-runtime-cache");
516
- var NETWORK_RETRY_COUNT = 3;
517
- var DOWNLOAD_PROGRESS_LOG_INTERVAL_MS = 5e3;
518
- function logPackStep(message) {
519
- console.log(chalk__default.default.cyan(`[pack] ${message}`));
520
- }
521
- function formatBytes(bytes) {
522
- if (!Number.isFinite(bytes) || bytes <= 0) {
523
- return "0 B";
524
- }
525
- if (bytes < 1024) return `${bytes.toFixed(0)} B`;
526
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
527
- if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
528
- return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
529
- }
530
- function formatPercent(value) {
531
- if (!Number.isFinite(value)) {
532
- return "0.0%";
533
- }
534
- return `${value.toFixed(1)}%`;
535
- }
536
- function formatDuration(ms) {
537
- const totalSeconds = Math.max(0, Math.floor(ms / 1e3));
538
- const minutes = Math.floor(totalSeconds / 60);
539
- const seconds = totalSeconds % 60;
540
- if (minutes <= 0) {
541
- return `${seconds}s`;
542
- }
543
- return `${minutes}m ${seconds}s`;
544
- }
545
- function updateSpinnerText(spinner, text) {
546
- if (!spinner?.isSpinning) {
547
- return;
548
- }
549
- spinner.text = text;
550
- }
551
- async function streamCommandWithProgress(params) {
552
- const { command, args, cwd, spinner, label } = params;
553
- if (spinner?.isSpinning) {
554
- spinner.stop();
555
- }
556
- logPackStep(`${label}\uFF08\u5B9E\u65F6\u8F93\u51FA\u5F00\u59CB\uFF09`);
557
- return await new Promise((resolvePromise, reject) => {
558
- const child = child_process.spawn(command, args, {
559
- cwd,
560
- stdio: ["ignore", "pipe", "pipe"],
561
- env: {
562
- ...process.env,
563
- PIP_PROGRESS_BAR: "on"
564
- }
565
- });
566
- let stdout = "";
567
- let stderr = "";
568
- child.stdout.on("data", (chunk) => {
569
- const text = chunk.toString();
570
- stdout += text;
571
- process.stdout.write(text);
572
- });
573
- child.stderr.on("data", (chunk) => {
574
- const text = chunk.toString();
575
- stderr += text;
576
- process.stderr.write(text);
577
- });
578
- child.on("error", reject);
579
- child.on("close", (code) => {
580
- spinner?.start(`${label}\u5B8C\u6210`);
581
- resolvePromise({
582
- stdout,
583
- stderr,
584
- code: code ?? 0
585
- });
586
- });
587
- });
588
- }
589
- async function validateProjectConfig(configPath) {
590
- if (!fs.existsSync(configPath)) {
591
- throw new Error("\u672A\u627E\u5230 seastudio.config.json \u6587\u4EF6");
592
- }
593
- const content = await promises.readFile(configPath, "utf-8");
594
- const config = JSON.parse(content);
595
- if (config.type !== "plugin" && config.type !== "agent") {
596
- throw new Error(`\u914D\u7F6E\u6587\u4EF6 type \u5FC5\u987B\u4E3A "plugin" \u6216 "agent"\uFF0C\u5F53\u524D\u4E3A "${config.type}"`);
597
- }
598
- if (!config.id) {
599
- throw new Error("\u914D\u7F6E\u6587\u4EF6\u7F3A\u5C11 id \u5B57\u6BB5");
600
- }
601
- if (!config.name) {
602
- throw new Error("\u914D\u7F6E\u6587\u4EF6\u7F3A\u5C11 name \u5B57\u6BB5");
603
- }
604
- if (!config.protocolHost) {
605
- throw new Error("\u914D\u7F6E\u6587\u4EF6\u7F3A\u5C11 protocolHost \u5B57\u6BB5");
606
- }
607
- if (config.type === "plugin") {
608
- if (!config.tab?.id || !config.tab?.title) {
609
- throw new Error("\u914D\u7F6E\u6587\u4EF6\u7F3A\u5C11 tab \u914D\u7F6E");
610
- }
611
- }
612
- return config;
613
- }
614
- async function getFileSize(filePath) {
615
- const stats = await promises.stat(filePath);
616
- const bytes = stats.size;
617
- if (bytes < 1024) return `${bytes} B`;
618
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
619
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
620
- }
621
- function resolvePythonStandaloneTarget(platform, arch) {
622
- if (platform === "darwin") {
623
- if (arch === "arm64") return "aarch64-apple-darwin";
624
- if (arch === "x64") return "x86_64-apple-darwin";
625
- }
626
- if (platform === "win32") {
627
- if (arch === "x64") return "x86_64-pc-windows-msvc";
628
- if (arch === "ia32") return "i686-pc-windows-msvc";
629
- }
630
- if (platform === "linux") {
631
- if (arch === "x64") return "x86_64-unknown-linux-gnu";
632
- if (arch === "arm64") return "aarch64-unknown-linux-gnu";
633
- }
634
- return null;
635
- }
636
- function pickPythonRuntimeAsset(assets, target) {
637
- const suffixes = [
638
- `-${target}-install_only_stripped.tar.gz`,
639
- `-${target}-install_only.tar.gz`
640
- ];
641
- const matches = assets.filter((asset) => {
642
- const name = asset?.name || "";
643
- if (!name.startsWith("cpython-") || !suffixes.some((suffix) => name.endsWith(suffix))) {
644
- return false;
645
- }
646
- const versionMatch = name.match(/cpython-(\d+)\.(\d+)\.(\d+)/);
647
- if (!versionMatch) {
648
- return false;
649
- }
650
- const major = Number(versionMatch[1]);
651
- const minor = Number(versionMatch[2]);
652
- return major === 3 && minor >= TARGET_PYTHON_MINOR && minor <= MAX_PYTHON_MINOR;
653
- });
654
- if (matches.length === 0) {
655
- return null;
656
- }
657
- matches.sort((a, b) => {
658
- const aMatch = (a.name || "").match(/cpython-(\d+)\.(\d+)\.(\d+)/);
659
- const bMatch = (b.name || "").match(/cpython-(\d+)\.(\d+)\.(\d+)/);
660
- if (!aMatch || !bMatch) {
661
- return 0;
662
- }
663
- const aMinor = Number(aMatch[2]);
664
- const bMinor = Number(bMatch[2]);
665
- if (aMinor !== bMinor) {
666
- return aMinor - bMinor;
667
- }
668
- const aPatch = Number(aMatch[3]);
669
- const bPatch = Number(bMatch[3]);
670
- return bPatch - aPatch;
671
- });
672
- return matches[0];
673
- }
674
- async function fetchText(url, headers) {
675
- return new Promise((resolve4, reject) => {
676
- https__default.default.get(url, { headers }, (res) => {
677
- if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
678
- res.resume();
679
- fetchText(res.headers.location, headers).then(resolve4).catch(reject);
680
- return;
681
- }
682
- if (res.statusCode && res.statusCode >= 400) {
683
- reject(new Error(`Request failed: ${res.statusCode}`));
684
- res.resume();
685
- return;
686
- }
687
- const chunks = [];
688
- res.on("data", (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
689
- res.on("end", () => resolve4(Buffer.concat(chunks).toString("utf8")));
690
- res.on("error", reject);
691
- }).on("error", reject);
692
- });
693
- }
694
- async function sleep(ms) {
695
- await new Promise((resolve4) => setTimeout(resolve4, ms));
696
- }
697
- async function withRetries(label, task) {
698
- let lastError;
699
- for (let attempt = 1; attempt <= NETWORK_RETRY_COUNT; attempt += 1) {
700
- try {
701
- if (attempt > 1) {
702
- logPackStep(`${label}\uFF1A\u7B2C ${attempt} \u6B21\u5C1D\u8BD5`);
703
- }
704
- return await task();
705
- } catch (error) {
706
- lastError = error;
707
- if (attempt >= NETWORK_RETRY_COUNT) {
708
- break;
709
- }
710
- await sleep(1e3 * attempt);
711
- }
712
- }
713
- throw new Error(`${label} failed after ${NETWORK_RETRY_COUNT} attempts: ${lastError instanceof Error ? lastError.message : String(lastError)}`);
714
- }
715
- async function fetchJson(url) {
716
- const text = await withRetries("Fetch python runtime release metadata", () => fetchText(url, {
717
- "User-Agent": "seastudio-sdk-pack",
718
- "Accept": "application/vnd.github+json"
719
- }));
720
- return JSON.parse(text);
721
- }
722
- async function downloadToFile(url, dest, options = {}, redirectCount = 0) {
723
- if (redirectCount > 5) {
724
- throw new Error(`Too many redirects: ${url}`);
725
- }
726
- await new Promise((resolvePromise, reject) => {
727
- https__default.default.get(url, { headers: { "User-Agent": "seastudio-sdk-pack" } }, (res) => {
728
- if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
729
- res.resume();
730
- downloadToFile(res.headers.location, dest, options, redirectCount + 1).then(resolvePromise).catch(reject);
731
- return;
732
- }
733
- if (res.statusCode && res.statusCode >= 400) {
734
- reject(new Error(`Download failed: ${res.statusCode}`));
735
- res.resume();
736
- return;
737
- }
738
- const totalBytes = Number(res.headers["content-length"] || 0);
739
- let downloadedBytes = 0;
740
- const startedAt = Date.now();
741
- let lastUpdateAt = 0;
742
- let lastLoggedAt = 0;
743
- const label = options.label || "\u4E0B\u8F7D\u4E2D";
744
- res.on("data", (chunk) => {
745
- downloadedBytes += Buffer.isBuffer(chunk) ? chunk.length : Buffer.byteLength(String(chunk));
746
- const now = Date.now();
747
- const elapsedMs = Math.max(1, now - startedAt);
748
- const speed = downloadedBytes / (elapsedMs / 1e3);
749
- const progressText = totalBytes > 0 ? `${label} ${formatPercent(downloadedBytes / totalBytes * 100)} (${formatBytes(downloadedBytes)}/${formatBytes(totalBytes)}, ${formatBytes(speed)}/s)` : `${label} ${formatBytes(downloadedBytes)} (${formatBytes(speed)}/s)`;
750
- if (now - lastUpdateAt >= 200) {
751
- lastUpdateAt = now;
752
- updateSpinnerText(options.spinner, progressText);
753
- }
754
- if (now - lastLoggedAt >= DOWNLOAD_PROGRESS_LOG_INTERVAL_MS) {
755
- lastLoggedAt = now;
756
- logPackStep(progressText);
757
- }
758
- });
759
- const fileStream = fs.createWriteStream(dest);
760
- promises$1.pipeline(res, fileStream).then(() => {
761
- const elapsedMs = Math.max(1, Date.now() - startedAt);
762
- const speed = downloadedBytes / (elapsedMs / 1e3);
763
- const finalText = totalBytes > 0 ? `${label}\u5B8C\u6210 ${formatPercent(100)} (${formatBytes(downloadedBytes)}/${formatBytes(totalBytes)}, ${formatBytes(speed)}/s, ${formatDuration(elapsedMs)})` : `${label}\u5B8C\u6210 ${formatBytes(downloadedBytes)} (${formatBytes(speed)}/s, ${formatDuration(elapsedMs)})`;
764
- updateSpinnerText(options.spinner, finalText);
765
- logPackStep(finalText);
766
- resolvePromise();
767
- }).catch(reject);
768
- }).on("error", reject);
769
- });
770
- }
771
- async function extractTarWithProgress(params) {
772
- let entryCount = 0;
773
- let fileCount = 0;
774
- let totalEntryBytes = 0;
775
- let lastUpdateAt = 0;
776
- const startedAt = Date.now();
777
- await tar.x({
778
- file: params.file,
779
- cwd: params.cwd,
780
- gzip: true,
781
- onReadEntry: (entry) => {
782
- entryCount += 1;
783
- if (entry.type !== "Directory") {
784
- fileCount += 1;
785
- }
786
- totalEntryBytes += Number(entry.size || 0);
787
- const now = Date.now();
788
- if (now - lastUpdateAt < 200) {
789
- return;
790
- }
791
- lastUpdateAt = now;
792
- updateSpinnerText(
793
- params.spinner,
794
- `${params.label} \u5DF2\u5904\u7406 ${entryCount} \u9879 / ${fileCount} \u6587\u4EF6 (${formatBytes(totalEntryBytes)})`
795
- );
796
- }
797
- });
798
- updateSpinnerText(
799
- params.spinner,
800
- `${params.label}\u5B8C\u6210\uFF0C\u5171 ${entryCount} \u9879 / ${fileCount} \u6587\u4EF6 (${formatBytes(totalEntryBytes)}, ${formatDuration(Date.now() - startedAt)})`
801
- );
802
- }
803
- function isCorruptedArchiveError(error) {
804
- const message = error instanceof Error ? error.message : String(error);
805
- return /unexpected end of file|invalid distance|incorrect data check|zlib/i.test(message);
806
- }
807
- async function materializeSymlinks(rootDir, currentDir = rootDir) {
808
- const entries = await promises.readdir(currentDir, { withFileTypes: true });
809
- for (const entry of entries) {
810
- const fullPath = path.join(currentDir, entry.name);
811
- if (entry.isDirectory()) {
812
- await materializeSymlinks(rootDir, fullPath);
813
- continue;
814
- }
815
- const stats = await promises.lstat(fullPath);
816
- if (!stats.isSymbolicLink()) {
817
- continue;
818
- }
819
- const currentTarget = await promises.readlink(fullPath);
820
- const resolvedTarget = currentTarget.startsWith("/") ? currentTarget : path.resolve(path.dirname(fullPath), currentTarget);
821
- if (!resolvedTarget.startsWith(rootDir) || !fs.existsSync(resolvedTarget)) {
822
- continue;
823
- }
824
- const targetStats = await promises.stat(resolvedTarget);
825
- await promises.unlink(fullPath);
826
- await promises.copyFile(resolvedTarget, fullPath);
827
- await promises.chmod(fullPath, targetStats.mode);
828
- logPackStep(`\u5B9E\u4F53\u5316\u8F6F\u94FE\u63A5 ${fullPath} -> ${resolvedTarget}`);
829
- }
830
- }
831
- async function buildPythonRuntimeDirectory(spinner) {
832
- const target = resolvePythonStandaloneTarget(process.platform, process.arch);
833
- if (!target) {
834
- throw new Error(`\u5F53\u524D\u5E73\u53F0\u6682\u4E0D\u652F\u6301\u6253\u5305 Python runtime: ${process.platform} ${process.arch}`);
835
- }
836
- logPackStep(`\u51C6\u5907 Python runtime\uFF0C\u76EE\u6807\u5E73\u53F0 ${target}`);
837
- updateSpinnerText(spinner, "\u51C6\u5907 Agent \u81EA\u5E26 Python runtime... \u8BFB\u53D6 runtime \u5143\u6570\u636E");
838
- logPackStep("\u62C9\u53D6 python-build-standalone \u6700\u65B0 release \u4FE1\u606F");
839
- const release = await fetchJson(
840
- PYTHON_STANDALONE_RELEASE_API
841
- );
842
- const asset = pickPythonRuntimeAsset(release.assets || [], target);
843
- if (!asset?.name || !asset.browser_download_url) {
844
- throw new Error(
845
- `\u672A\u627E\u5230\u9002\u914D ${target} \u7684 Python runtime\uFF08\u8981\u6C42 minor >= ${TARGET_PYTHON_MINOR}\uFF09`
846
- );
847
- }
848
- await promises.mkdir(PYTHON_CACHE_DIR, { recursive: true });
849
- const archivePath = path.join(PYTHON_CACHE_DIR, asset.name);
850
- if (!fs.existsSync(archivePath)) {
851
- logPackStep(`\u4E0B\u8F7D Python runtime \u538B\u7F29\u5305 ${asset.name}`);
852
- await withRetries("Download python runtime archive", () => downloadToFile(
853
- asset.browser_download_url,
854
- archivePath,
855
- {
856
- spinner,
857
- label: `\u4E0B\u8F7D Python runtime ${asset.name}`
858
- }
859
- ));
860
- } else {
861
- logPackStep(`\u590D\u7528\u672C\u5730 Python runtime \u7F13\u5B58 ${asset.name}`);
862
- updateSpinnerText(spinner, `\u51C6\u5907 Agent \u81EA\u5E26 Python runtime... \u590D\u7528\u7F13\u5B58 ${asset.name}`);
863
- }
864
- const tempRoot = await promises.mkdtemp(path.join(os.tmpdir(), "seastudio-python-runtime-"));
865
- try {
866
- logPackStep(`\u89E3\u538B Python runtime \u538B\u7F29\u5305 ${asset.name}`);
867
- updateSpinnerText(spinner, `\u89E3\u538B Python runtime ${asset.name}...`);
868
- await extractTarWithProgress({
869
- file: archivePath,
870
- cwd: tempRoot,
871
- spinner,
872
- label: `\u89E3\u538B Python runtime ${asset.name}`
873
- });
874
- } catch (error) {
875
- if (!isCorruptedArchiveError(error)) {
876
- await promises.rm(tempRoot, { recursive: true, force: true }).catch(() => {
877
- });
878
- throw error;
879
- }
880
- logPackStep(`\u68C0\u6D4B\u5230\u635F\u574F\u7F13\u5B58\uFF0C\u5220\u9664\u5E76\u91CD\u65B0\u4E0B\u8F7D ${asset.name}`);
881
- updateSpinnerText(spinner, `\u68C0\u6D4B\u5230\u7F13\u5B58\u635F\u574F\uFF0C\u91CD\u65B0\u4E0B\u8F7D ${asset.name}...`);
882
- await promises.rm(archivePath, { force: true }).catch(() => {
883
- });
884
- await withRetries("Re-download corrupted python runtime archive", () => downloadToFile(
885
- asset.browser_download_url,
886
- archivePath,
887
- {
888
- spinner,
889
- label: `\u91CD\u65B0\u4E0B\u8F7D Python runtime ${asset.name}`
890
- }
891
- ));
892
- await promises.rm(tempRoot, { recursive: true, force: true }).catch(() => {
893
- });
894
- const retryTempRoot = await promises.mkdtemp(path.join(os.tmpdir(), "seastudio-python-runtime-"));
895
- try {
896
- logPackStep(`\u91CD\u65B0\u89E3\u538B Python runtime \u538B\u7F29\u5305 ${asset.name}`);
897
- updateSpinnerText(spinner, `\u91CD\u65B0\u89E3\u538B Python runtime ${asset.name}...`);
898
- await extractTarWithProgress({
899
- file: archivePath,
900
- cwd: retryTempRoot,
901
- spinner,
902
- label: `\u91CD\u65B0\u89E3\u538B Python runtime ${asset.name}`
903
- });
904
- return finalizePythonRuntimeDirectory(retryTempRoot, asset.name, target);
905
- } catch (retryError) {
906
- await promises.rm(retryTempRoot, { recursive: true, force: true }).catch(() => {
907
- });
908
- throw retryError;
909
- }
910
- }
911
- return finalizePythonRuntimeDirectory(tempRoot, asset.name, target);
912
- }
913
- async function finalizePythonRuntimeDirectory(tempRoot, assetName, target) {
914
- const extractedRoot = path.join(tempRoot, "python");
915
- if (!fs.existsSync(extractedRoot)) {
916
- await promises.rm(tempRoot, { recursive: true, force: true }).catch(() => {
917
- });
918
- throw new Error(`Python runtime \u538B\u7F29\u5305\u5E03\u5C40\u4E0D\u7B26\u5408\u9884\u671F: ${assetName}`);
919
- }
920
- const pythonCommand = process.platform === "win32" ? path.join(extractedRoot, "python.exe") : path.join(extractedRoot, "bin", "python3");
921
- if (!fs.existsSync(pythonCommand)) {
922
- await promises.rm(tempRoot, { recursive: true, force: true }).catch(() => {
923
- });
924
- throw new Error(`Python runtime \u7F3A\u5C11\u89E3\u91CA\u5668: ${pythonCommand}`);
925
- }
926
- logPackStep(`\u63A2\u6D4B Python runtime \u7248\u672C ${pythonCommand}`);
927
- const versionResult = child_process.spawnSync(pythonCommand, ["--version"], { encoding: "utf-8" });
928
- if (versionResult.status !== 0) {
929
- await promises.rm(tempRoot, { recursive: true, force: true }).catch(() => {
930
- });
931
- throw new Error(
932
- `Python runtime \u7248\u672C\u63A2\u6D4B\u5931\u8D25: ${versionResult.stderr?.trim() || versionResult.stdout?.trim() || "unknown error"}`
933
- );
934
- }
935
- const manifestPath = path.join(tempRoot, PYTHON_RUNTIME_MANIFEST);
936
- const pythonVersion = `${versionResult.stdout || versionResult.stderr}`.trim();
937
- logPackStep(`Python runtime \u5C31\u7EEA\uFF1A${pythonVersion}`);
938
- await materializeSymlinks(extractedRoot);
939
- await promises.writeFile(manifestPath, `${JSON.stringify({
940
- pythonVersion,
941
- target,
942
- platform: process.platform,
943
- arch: process.arch,
944
- assetName,
945
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
946
- }, null, 2)}
947
- `, "utf-8");
948
- return {
949
- runtimeDir: extractedRoot,
950
- manifestPath,
951
- pythonCommand,
952
- pythonVersion
953
- };
954
- }
955
- async function buildPythonVendorDirectory(projectDir, backendDirName, pythonCommand, pythonVersion, spinner) {
956
- const backendDir = path.join(projectDir, backendDirName);
957
- const requirementsPath = path.join(backendDir, "requirements.txt");
958
- if (!fs.existsSync(requirementsPath)) {
959
- logPackStep("\u672A\u627E\u5230 requirements.txt\uFF0C\u8DF3\u8FC7 Python vendor \u4F9D\u8D56");
960
- return null;
961
- }
962
- const tempRoot = await promises.mkdtemp(path.join(os.tmpdir(), "seastudio-python-vendor-"));
963
- const vendorDir = path.join(tempRoot, PYTHON_VENDOR_DIRNAME);
964
- const manifestPath = path.join(tempRoot, PYTHON_VENDOR_MANIFEST);
965
- logPackStep(`\u5B89\u88C5 Python vendor \u4F9D\u8D56\uFF0C\u4F7F\u7528 ${pythonVersion}`);
966
- updateSpinnerText(spinner, `\u5B89\u88C5 Python vendor \u4F9D\u8D56... \u4F7F\u7528 ${pythonVersion}`);
967
- const installResult = await streamCommandWithProgress({
968
- command: pythonCommand,
969
- args: ["-m", "pip", "install", "-r", requirementsPath, "--target", vendorDir, "--upgrade", "--progress-bar", "on"],
970
- cwd: backendDir,
971
- spinner,
972
- label: "\u5B89\u88C5 Python vendor \u4F9D\u8D56"
973
- });
974
- if (installResult.code !== 0) {
975
- await promises.rm(tempRoot, { recursive: true, force: true }).catch(() => {
976
- });
977
- throw new Error(
978
- `Python \u4F9D\u8D56 vendor \u5931\u8D25: ${installResult.stderr.trim() || installResult.stdout.trim() || "unknown error"}`
979
- );
980
- }
981
- logPackStep("Python vendor \u4F9D\u8D56\u5B89\u88C5\u5B8C\u6210");
982
- updateSpinnerText(spinner, "Python vendor \u4F9D\u8D56\u5B89\u88C5\u5B8C\u6210\uFF0C\u5199\u5165 manifest...");
983
- const versionResult = child_process.spawnSync(pythonCommand, ["--version"], { encoding: "utf-8" });
984
- const manifest = {
985
- pythonCommand,
986
- pythonVersion: `${versionResult.stdout || versionResult.stderr}`.trim() || pythonVersion,
987
- platform: process.platform,
988
- arch: process.arch,
989
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
990
- requirementsFile: "requirements.txt"
991
- };
992
- await promises.writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}
993
- `, "utf-8");
994
- return {
995
- vendorDir,
996
- manifestPath
997
- };
998
- }
999
- var packCommand = new commander.Command("pack").description("\u6253\u5305\u63D2\u4EF6\u6216 Agent").option("-d, --dir <directory>", "\u9879\u76EE\u76EE\u5F55", ".").option("-o, --output <file>", "\u8F93\u51FA\u6587\u4EF6\u540D").action(async (options) => {
1000
- const projectDir = path.resolve(process.cwd(), options.dir);
1001
- const configPath = path.join(projectDir, "seastudio.config.json");
1002
- const frontendDistDir = path.join(projectDir, "frontend", "dist");
1003
- let pythonRuntimeArtifacts = null;
1004
- let pythonVendorArtifacts = null;
1005
- console.log(chalk__default.default.cyan("\n\u{1F4E6} SeaStudio \u6253\u5305\n"));
1006
- const spinnerConfig = ora2__default.default("\u8BFB\u53D6 seastudio.config.json...").start();
1007
- let config;
1008
- try {
1009
- config = await validateProjectConfig(configPath);
1010
- spinnerConfig.succeed(`${config.type === "agent" ? "Agent" : "\u63D2\u4EF6"}: ${chalk__default.default.bold(config.name)} (${config.id})`);
1011
- } catch (error) {
1012
- spinnerConfig.fail("\u914D\u7F6E\u9A8C\u8BC1\u5931\u8D25");
1013
- console.error(chalk__default.default.red(`
1014
- \u274C ${error instanceof Error ? error.message : "\u672A\u77E5\u9519\u8BEF"}
1015
- `));
1016
- process.exit(1);
1017
- }
1018
- const isRemotePlugin = config.type === "plugin" && config.uiType === "remote";
1019
- if (!isRemotePlugin) {
1020
- const spinnerDist = ora2__default.default("\u68C0\u67E5 frontend build...").start();
1021
- const distIndexPath = path.join(frontendDistDir, "index.html");
1022
- if (!fs.existsSync(distIndexPath)) {
1023
- spinnerDist.fail("\u672A\u627E\u5230 frontend/dist/index.html");
1024
- console.log(chalk__default.default.yellow("\n\u63D0\u793A: \u8BF7\u5148\u6267\u884C frontend build\n"));
1025
- console.log(` ${chalk__default.default.yellow("cd")} frontend`);
1026
- console.log(` ${chalk__default.default.yellow("npm run build")}
1027
- `);
1028
- process.exit(1);
1029
- }
1030
- if (config.type === "plugin") {
1031
- const generated = await generatePluginMcpManifest(projectDir);
1032
- logPackStep(`\u751F\u6210 MCP manifest\uFF1A${generated.outputPath} (${generated.toolCount} tools)`);
1033
- }
1034
- spinnerDist.succeed("frontend/dist/index.html \u5DF2\u627E\u5230");
1035
- } else {
1036
- const spinnerDist = ora2__default.default("\u8FDC\u7A0B\u63D2\u4EF6\uFF0C\u8DF3\u8FC7 frontend build \u68C0\u67E5").start();
1037
- if (!config.remoteUrl) {
1038
- spinnerDist.fail("\u8FDC\u7A0B\u63D2\u4EF6\u7F3A\u5C11 remoteUrl");
1039
- process.exit(1);
1040
- }
1041
- spinnerDist.succeed(`\u8FDC\u7A0B\u63D2\u4EF6: ${config.remoteUrl}`);
1042
- }
1043
- const outputFileName = options.output || `${config.protocolHost}.${config.type === "agent" ? "seaagent" : "seaplugin"}`;
1044
- const outputPath = path.resolve(projectDir, outputFileName);
1045
- const spinnerPack = ora2__default.default(`\u521B\u5EFA${config.type === "agent" ? " Agent" : "\u63D2\u4EF6"}\u5305...`).start();
1046
- try {
1047
- const output = fs.createWriteStream(outputPath);
1048
- const archive = archiver__default.default("zip", { zlib: { level: 9 } });
1049
- const archivePromise = new Promise((resolvePromise, reject) => {
1050
- output.on("close", () => resolvePromise());
1051
- archive.on("error", (err) => reject(err));
1052
- });
1053
- let lastArchiveUpdateAt = 0;
1054
- archive.on("progress", (event) => {
1055
- const now = Date.now();
1056
- if (now - lastArchiveUpdateAt < 200) {
1057
- return;
1058
- }
1059
- lastArchiveUpdateAt = now;
1060
- const totalBytes = Number(event.fs.totalBytes || 0);
1061
- const processedBytes = Number(event.fs.processedBytes || 0);
1062
- const totalEntries = Number(event.entries.total || 0);
1063
- const processedEntries = Number(event.entries.processed || 0);
1064
- const percent = totalBytes > 0 ? formatPercent(processedBytes / totalBytes * 100) : "...";
1065
- spinnerPack.text = `\u5199\u5165\u538B\u7F29\u5305\u4E2D ${percent} (${formatBytes(processedBytes)}/${formatBytes(totalBytes || processedBytes)}, ${processedEntries}/${totalEntries || processedEntries} \u9879)`;
1066
- });
1067
- archive.pipe(output);
1068
- archive.file(configPath, { name: "seastudio.config.json" });
1069
- if (!isRemotePlugin) {
1070
- archive.directory(frontendDistDir, "dist");
1071
- }
1072
- const iconPath = path.join(projectDir, "icon.png");
1073
- if (fs.existsSync(iconPath)) {
1074
- archive.file(iconPath, { name: "icon.png" });
1075
- }
1076
- if (config.type === "agent" && config.backend?.mode === "hosted") {
1077
- const backendDir = path.join(projectDir, config.backendDir || "backend");
1078
- if (fs.existsSync(backendDir)) {
1079
- if (config.backend.runtime === "python") {
1080
- spinnerPack.text = "\u51C6\u5907 Agent \u81EA\u5E26 Python runtime...";
1081
- pythonRuntimeArtifacts = await buildPythonRuntimeDirectory(spinnerPack);
1082
- spinnerPack.text = "\u6253\u5305 Python vendored \u4F9D\u8D56...";
1083
- pythonVendorArtifacts = await buildPythonVendorDirectory(
1084
- projectDir,
1085
- config.backendDir || "backend",
1086
- pythonRuntimeArtifacts.pythonCommand,
1087
- pythonRuntimeArtifacts.pythonVersion,
1088
- spinnerPack
1089
- );
1090
- }
1091
- logPackStep(`\u5199\u5165 backend \u76EE\u5F55\u5230\u538B\u7F29\u5305\uFF1A${config.backendDir || "backend"}`);
1092
- archive.directory(backendDir, config.backendDir || "backend");
1093
- if (pythonRuntimeArtifacts) {
1094
- logPackStep(`\u5199\u5165 Python runtime \u5230\u538B\u7F29\u5305\uFF1A${config.backendDir || "backend"}/${PYTHON_RUNTIME_DIRNAME}`);
1095
- archive.directory(
1096
- pythonRuntimeArtifacts.runtimeDir,
1097
- `${config.backendDir || "backend"}/${PYTHON_RUNTIME_DIRNAME}`
1098
- );
1099
- archive.file(
1100
- pythonRuntimeArtifacts.manifestPath,
1101
- { name: `${config.backendDir || "backend"}/${PYTHON_RUNTIME_MANIFEST}` }
1102
- );
1103
- }
1104
- if (pythonVendorArtifacts) {
1105
- logPackStep(`\u5199\u5165 Python vendor \u4F9D\u8D56\u5230\u538B\u7F29\u5305\uFF1A${config.backendDir || "backend"}/${PYTHON_VENDOR_DIRNAME}`);
1106
- archive.directory(
1107
- pythonVendorArtifacts.vendorDir,
1108
- `${config.backendDir || "backend"}/${PYTHON_VENDOR_DIRNAME}`
1109
- );
1110
- archive.file(
1111
- pythonVendorArtifacts.manifestPath,
1112
- { name: `${config.backendDir || "backend"}/${PYTHON_VENDOR_MANIFEST}` }
1113
- );
1114
- }
1115
- }
1116
- }
1117
- spinnerPack.text = `\u538B\u7F29 ${config.type === "agent" ? "Agent" : "\u63D2\u4EF6"} \u6587\u4EF6\u4E2D...`;
1118
- await archive.finalize();
1119
- await archivePromise;
1120
- const fileSize = await getFileSize(outputPath);
1121
- spinnerPack.succeed(`${config.type === "agent" ? "Agent" : "\u63D2\u4EF6"}\u5305\u521B\u5EFA\u5B8C\u6210`);
1122
- console.log(chalk__default.default.green(`
1123
- \u2705 \u5DF2\u521B\u5EFA ${chalk__default.default.bold(outputFileName)} (${fileSize})
1124
- `));
1125
- console.log(chalk__default.default.cyan("\u5B89\u88C5\u65B9\u5F0F:\n"));
1126
- console.log(" 1. \u6253\u5F00 SeaStudio");
1127
- console.log(` 2. \u8BBE\u7F6E \u2192 ${config.type === "agent" ? "Agent \u7BA1\u7406" : "\u63D2\u4EF6\u7BA1\u7406"} \u2192 ${config.type === "agent" ? "\u5BFC\u5165 Agent" : "\u6DFB\u52A0\u63D2\u4EF6"}`);
1128
- console.log(` 3. \u9009\u62E9 ${chalk__default.default.bold(outputFileName)} \u6587\u4EF6
1129
- `);
1130
- } catch (error) {
1131
- spinnerPack.fail("\u6253\u5305\u5931\u8D25");
1132
- console.error(chalk__default.default.red(`
1133
- \u274C ${error instanceof Error ? error.message : "\u672A\u77E5\u9519\u8BEF"}
1134
- `));
1135
- process.exit(1);
1136
- } finally {
1137
- if (pythonRuntimeArtifacts) {
1138
- await promises.rm(path.join(pythonRuntimeArtifacts.runtimeDir, ".."), { recursive: true, force: true }).catch(() => {
1139
- });
1140
- }
1141
- if (pythonVendorArtifacts) {
1142
- await promises.rm(path.join(pythonVendorArtifacts.vendorDir, ".."), { recursive: true, force: true }).catch(() => {
1143
- });
1144
- }
1145
- }
1146
- });
1147
- var installCommand = new commander.Command("install").description("\u5B89\u88C5 .seaplugin \u6216 .seaagent \u6587\u4EF6\u5230\u6307\u5B9A\u76EE\u5F55").argument("<archive>", ".seaplugin \u6216 .seaagent \u6587\u4EF6\u8DEF\u5F84").option("--plugins-dir <dir>", "\u63D2\u4EF6\u5B89\u88C5\u76EE\u5F55").option("--agents-dir <dir>", "Agent \u5B89\u88C5\u76EE\u5F55").option("--force", "\u5F3A\u5236\u8986\u76D6\u5DF2\u5B58\u5728\u7684\u9879\u76EE", false).action(async (archivePath, options) => {
1148
- try {
1149
- if (!fs.existsSync(archivePath)) {
1150
- console.error(`\u274C \u6587\u4EF6\u4E0D\u5B58\u5728: ${archivePath}`);
1151
- process.exit(1);
1152
- }
1153
- const isPlugin = archivePath.endsWith(".seaplugin");
1154
- const isAgent = archivePath.endsWith(".seaagent");
1155
- if (!isPlugin && !isAgent) {
1156
- console.error("\u274C \u6587\u4EF6\u5FC5\u987B\u662F .seaplugin \u6216 .seaagent \u683C\u5F0F");
1157
- process.exit(1);
1158
- }
1159
- const targetRoot = isPlugin ? options.pluginsDir : options.agentsDir;
1160
- if (!targetRoot) {
1161
- console.error(`\u274C \u5FC5\u987B\u6307\u5B9A --${isPlugin ? "plugins-dir" : "agents-dir"} \u53C2\u6570`);
1162
- process.exit(1);
1163
- }
1164
- const tempDir = path.join(targetRoot, ".temp-install-" + Date.now());
1165
- fs.mkdirSync(tempDir, { recursive: true });
1166
- console.log(`\u{1F4E6} \u89E3\u538B\u5B89\u88C5\u5305: ${path.basename(archivePath)}`);
1167
- await new Promise((resolve4, reject) => {
1168
- fs.createReadStream(archivePath).pipe(unzipper.Extract({ path: tempDir })).on("close", resolve4).on("error", reject);
1169
- });
1170
- const configPath = path.join(tempDir, "seastudio.config.json");
1171
- if (!fs.existsSync(configPath)) {
1172
- await promises.rm(tempDir, { recursive: true, force: true });
1173
- console.error("\u274C \u65E0\u6548\u7684\u5B89\u88C5\u5305: \u7F3A\u5C11 seastudio.config.json");
1174
- process.exit(1);
1175
- }
1176
- const configContent = await promises.readFile(configPath, "utf-8");
1177
- const config = JSON.parse(configContent);
1178
- if (!config.id || !config.protocolHost) {
1179
- await promises.rm(tempDir, { recursive: true, force: true });
1180
- console.error("\u274C \u65E0\u6548\u7684\u914D\u7F6E: \u7F3A\u5C11 id \u6216 protocolHost");
1181
- process.exit(1);
1182
- }
1183
- const targetDir = path.join(targetRoot, config.protocolHost);
1184
- if (fs.existsSync(targetDir)) {
1185
- if (!options.force) {
1186
- await promises.rm(tempDir, { recursive: true, force: true });
1187
- console.log(`\u26A0\uFE0F \u5DF2\u5B58\u5728: ${config.protocolHost}\uFF0C\u4F7F\u7528 --force \u8986\u76D6`);
1188
- process.exit(0);
1189
- }
1190
- console.log(`\u{1F504} \u8986\u76D6\u5DF2\u5B58\u5728\u7684\u9879\u76EE: ${config.protocolHost}`);
1191
- await promises.rm(targetDir, { recursive: true, force: true });
1192
- }
1193
- const { rename } = await import('fs/promises');
1194
- await rename(tempDir, targetDir);
1195
- console.log(`\u2705 ${isPlugin ? "\u63D2\u4EF6" : "Agent"}\u5B89\u88C5\u6210\u529F: ${config.name} (${config.protocolHost})`);
1196
- console.log(` \u4F4D\u7F6E: ${targetDir}`);
1197
- } catch (error) {
1198
- console.error("\u274C \u5B89\u88C5\u5931\u8D25:", error instanceof Error ? error.message : error);
1199
- process.exit(1);
1200
- }
1201
- });
1202
-
1203
- // src/develop-tool/cli/index.ts
1204
- var program = new commander.Command();
1205
- program.name("seastudio").description("SeaStudio Plugin Development CLI").version("1.0.0");
1206
- program.addCommand(createCommand);
1207
- program.addCommand(generateMcpManifestCommand);
1208
- program.addCommand(packCommand);
1209
- program.addCommand(installCommand);
1210
- program.parse();