@vincentbanana/bananacli 0.4.7 → 0.4.9
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/config/config.js +1 -0
- package/dist/index.js +6 -4
- package/dist/server/terminal.js +305 -0
- package/dist/server/ui-server.js +234 -100
- package/dist/tools/plugins.js +36 -8
- package/dist/ui/app.js +1703 -359
- package/dist/ui/index.html +88 -34
- package/dist/ui/style.css +1159 -70
- package/dist/ui/vendor/addon-fit.js +2 -0
- package/dist/ui/vendor/highlight.min.js +1262 -0
- package/dist/ui/vendor/xterm.css +285 -0
- package/dist/ui/vendor/xterm.js +2 -0
- package/package.json +9 -1
package/dist/server/ui-server.js
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
1
9
|
import { createServer } from 'node:http';
|
|
2
10
|
import { randomBytes } from 'node:crypto';
|
|
3
11
|
import { mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
4
12
|
import { existsSync } from 'node:fs';
|
|
5
|
-
import { extname, join, resolve, dirname } from 'node:path';
|
|
13
|
+
import { extname, join, resolve, dirname, sep } from 'node:path';
|
|
6
14
|
import { spawn } from 'node:child_process';
|
|
7
|
-
import {
|
|
15
|
+
import { WebSocketServer } from 'ws';
|
|
16
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
17
|
+
import { createRequire } from 'node:module';
|
|
18
|
+
import { TerminalManager, loadPty, normalizeCwd } from "./terminal.js";
|
|
8
19
|
import { bananaHome } from "../config/config.js";
|
|
9
20
|
import { runTurn } from "../agent/loop.js";
|
|
10
21
|
import { compactMessages } from "../agent/context.js";
|
|
@@ -13,7 +24,7 @@ import { estimateCost, formatCost, resolvePricing } from "../providers/pricing.j
|
|
|
13
24
|
import { SessionStore } from "../session/store.js";
|
|
14
25
|
import { BackupStore, newTurnId } from "../session/backups.js";
|
|
15
26
|
import { allTools, permissionFor, summarizeInput } from "../tools/index.js";
|
|
16
|
-
import { loadPluginTools } from "../tools/plugins.js";
|
|
27
|
+
import { loadPluginTools, pluginTemplate } from "../tools/plugins.js";
|
|
17
28
|
import { resolvePath } from "../tools/fs.js";
|
|
18
29
|
import { commands, findCommand, suggestCommand } from "../cli/commands.js";
|
|
19
30
|
import { completeCommand, completeFileReference } from "../cli/completion.js";
|
|
@@ -24,6 +35,12 @@ const MIME = {
|
|
|
24
35
|
'.js': 'text/javascript; charset=utf-8',
|
|
25
36
|
'.svg': 'image/svg+xml',
|
|
26
37
|
};
|
|
38
|
+
const VENDOR_FILES = {
|
|
39
|
+
'xterm.js': '@xterm/xterm/lib/xterm.js',
|
|
40
|
+
'xterm.css': '@xterm/xterm/css/xterm.css',
|
|
41
|
+
'addon-fit.js': '@xterm/addon-fit/lib/addon-fit.js',
|
|
42
|
+
'highlight.min.js': '@highlightjs/cdn-assets/highlight.min.js',
|
|
43
|
+
};
|
|
27
44
|
const IMAGE_TYPES = {
|
|
28
45
|
'.png': 'image/png',
|
|
29
46
|
'.jpg': 'image/jpeg',
|
|
@@ -106,6 +123,9 @@ export async function startUiServer(options) {
|
|
|
106
123
|
workspace: config.workspace,
|
|
107
124
|
platform: process.platform,
|
|
108
125
|
permissions: config.permissions,
|
|
126
|
+
systemPrompt: config.systemPrompt,
|
|
127
|
+
showReasoning: config.showReasoning,
|
|
128
|
+
disabledPlugins: config.disabledPlugins,
|
|
109
129
|
tools: options.tools.map((tool) => ({ name: tool.name, approval: tool.approval, permission: permissionFor(config.permissions, tool) })),
|
|
110
130
|
session: {
|
|
111
131
|
id: session.id,
|
|
@@ -121,6 +141,29 @@ export async function startUiServer(options) {
|
|
|
121
141
|
updatedAt: item.updatedAt,
|
|
122
142
|
turns: item.turns,
|
|
123
143
|
}));
|
|
144
|
+
// ---------- 插件:本地插件工坊 ----------
|
|
145
|
+
const builtinTools = options.tools.filter((tool) => !(options.pluginTools ?? []).some((plugin) => plugin.name === tool.name));
|
|
146
|
+
const pluginDirs = () => [
|
|
147
|
+
{ id: 'global', label: '全局插件', path: join(bananaHome(), 'tools') },
|
|
148
|
+
{ id: 'project', label: '项目插件', path: join(config.workspace, '.banana', 'tools') },
|
|
149
|
+
];
|
|
150
|
+
const syncPlugins = async () => {
|
|
151
|
+
const dirs = pluginDirs();
|
|
152
|
+
const result = await loadPluginTools(dirs.map((dir) => dir.path), new Set(builtinTools.map((tool) => tool.name)));
|
|
153
|
+
const disabled = new Set(config.disabledPlugins);
|
|
154
|
+
const enabledTools = result.tools.filter((tool) => !disabled.has(tool.name));
|
|
155
|
+
options.tools.splice(0, options.tools.length, ...builtinTools, ...enabledTools);
|
|
156
|
+
return {
|
|
157
|
+
dirs,
|
|
158
|
+
plugins: result.plugins.map((plugin) => ({
|
|
159
|
+
...plugin,
|
|
160
|
+
disabled: disabled.has(plugin.name),
|
|
161
|
+
enabled: plugin.ok && !disabled.has(plugin.name),
|
|
162
|
+
})),
|
|
163
|
+
errors: result.errors,
|
|
164
|
+
toolCount: options.tools.length,
|
|
165
|
+
};
|
|
166
|
+
};
|
|
124
167
|
const server = createServer((req, res) => {
|
|
125
168
|
handle(req, res).catch((error) => {
|
|
126
169
|
if (!res.headersSent)
|
|
@@ -129,6 +172,31 @@ export async function startUiServer(options) {
|
|
|
129
172
|
res.end();
|
|
130
173
|
});
|
|
131
174
|
});
|
|
175
|
+
// 内嵌终端:WebSocket 双向通道 + 真实 PTY(cmd / pwsh / bash)
|
|
176
|
+
const terminals = new TerminalManager();
|
|
177
|
+
void loadPty();
|
|
178
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
179
|
+
server.on('upgrade', (req, socket, head) => {
|
|
180
|
+
try {
|
|
181
|
+
const url = new URL(req.url ?? '/', `http://${host}`);
|
|
182
|
+
if (url.pathname !== `${prefix}/api/terminal/ws`) {
|
|
183
|
+
socket.destroy();
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
187
|
+
const shellId = url.searchParams.get('shell') ?? terminals.defaultShell().id;
|
|
188
|
+
const cwd = normalizeCwd(url.searchParams.get('cwd'), config.workspace);
|
|
189
|
+
const existing = url.searchParams.get('id');
|
|
190
|
+
const session = (existing ? terminals.find(existing) : undefined) ??
|
|
191
|
+
terminals.create(shellId, cwd, Number(url.searchParams.get('cols')) || 100, Number(url.searchParams.get('rows')) || 28);
|
|
192
|
+
ws.send(JSON.stringify({ type: 'id', id: session.id }));
|
|
193
|
+
session.attach(ws);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
socket.destroy();
|
|
198
|
+
}
|
|
199
|
+
});
|
|
132
200
|
async function handle(req, res) {
|
|
133
201
|
const url = new URL(req.url ?? '/', `http://${host}`);
|
|
134
202
|
if (!url.pathname.startsWith(prefix))
|
|
@@ -140,6 +208,13 @@ export async function startUiServer(options) {
|
|
|
140
208
|
return sendFile(res, 'style.css');
|
|
141
209
|
if (path === '/app.js')
|
|
142
210
|
return sendFile(res, 'app.js');
|
|
211
|
+
// 终端与语法高亮的本地资源:只放行固定文件,避免任意路径读取
|
|
212
|
+
if (path.startsWith('/vendor/')) {
|
|
213
|
+
const name = path.slice('/vendor/'.length);
|
|
214
|
+
if (Object.hasOwn(VENDOR_FILES, name))
|
|
215
|
+
return sendFile(res, `vendor/${name}`);
|
|
216
|
+
return notFound(res);
|
|
217
|
+
}
|
|
143
218
|
let session = turn?.session ??
|
|
144
219
|
(activeId ? await store.load(activeId) : null) ??
|
|
145
220
|
(await store.lastActive().then(async (id) => (id ? await store.load(id) : null))) ??
|
|
@@ -232,6 +307,53 @@ export async function startUiServer(options) {
|
|
|
232
307
|
const body = await readJson(req);
|
|
233
308
|
return runCliCommand(body.input ?? '', res, session);
|
|
234
309
|
}
|
|
310
|
+
if (path === '/api/find' && req.method === 'GET') {
|
|
311
|
+
const query = (url.searchParams.get('q') ?? '').trim().toLowerCase();
|
|
312
|
+
const limit = Math.max(1, Math.min(120, Number(url.searchParams.get('limit') ?? 60) || 60));
|
|
313
|
+
const skip = new Set([
|
|
314
|
+
'.git', 'node_modules', 'dist', 'build', 'out', 'coverage', '.next', '.nuxt',
|
|
315
|
+
'.venv', 'venv', '__pycache__', '.idea', '.vscode', '.cache', '.banana', 'vendor',
|
|
316
|
+
]);
|
|
317
|
+
const results = [];
|
|
318
|
+
let scanned = 0;
|
|
319
|
+
const walk = async (dir, rel, depth) => {
|
|
320
|
+
if (results.length >= limit || scanned > 6000 || depth > 8)
|
|
321
|
+
return;
|
|
322
|
+
let entries;
|
|
323
|
+
try {
|
|
324
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
330
|
+
for (const entry of entries) {
|
|
331
|
+
if (results.length >= limit || scanned > 6000)
|
|
332
|
+
return;
|
|
333
|
+
if (entry.name.startsWith('.') && entry.name !== '.env.example')
|
|
334
|
+
continue;
|
|
335
|
+
if (skip.has(entry.name))
|
|
336
|
+
continue;
|
|
337
|
+
const childRel = rel ? `${rel}/${entry.name}` : entry.name;
|
|
338
|
+
if (entry.isDirectory()) {
|
|
339
|
+
await walk(join(dir, entry.name), childRel, depth + 1);
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (!entry.isFile())
|
|
343
|
+
continue;
|
|
344
|
+
scanned += 1;
|
|
345
|
+
if (!query || childRel.toLowerCase().includes(query))
|
|
346
|
+
results.push(childRel);
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
try {
|
|
350
|
+
await walk(config.workspace, '', 0);
|
|
351
|
+
return json(res, 200, { files: results, scanned });
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
return json(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
355
|
+
}
|
|
356
|
+
}
|
|
235
357
|
if (path === '/api/commands' && req.method === 'GET') {
|
|
236
358
|
return json(res, 200, {
|
|
237
359
|
commands: commands.map((command) => ({
|
|
@@ -269,6 +391,9 @@ export async function startUiServer(options) {
|
|
|
269
391
|
else if (body.action === 'system' && body.value) {
|
|
270
392
|
config.systemPrompt = body.value;
|
|
271
393
|
}
|
|
394
|
+
else if (body.action === 'showReasoning') {
|
|
395
|
+
config.showReasoning = body.value === 'true' || body.value === '1';
|
|
396
|
+
}
|
|
272
397
|
else if (body.action === 'workspace' && body.value) {
|
|
273
398
|
const next = resolve(String(body.value).trim());
|
|
274
399
|
if (!existsSync(next)) {
|
|
@@ -338,107 +463,99 @@ export async function startUiServer(options) {
|
|
|
338
463
|
activeId = target.id;
|
|
339
464
|
return chat(body, res, target);
|
|
340
465
|
}
|
|
341
|
-
if (path === '/api/
|
|
342
|
-
|
|
343
|
-
return runTerminal(String(body.command ?? ''), res);
|
|
466
|
+
if (path === '/api/plugins' && req.method === 'GET') {
|
|
467
|
+
return json(res, 200, await syncPlugins());
|
|
344
468
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
'
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
let buffer = (stream === 'stdout' ? tailOut : tailErr) + text;
|
|
390
|
-
const lines = buffer.split('\n');
|
|
391
|
-
buffer = lines.pop() ?? '';
|
|
392
|
-
for (const raw of lines) {
|
|
393
|
-
const line = raw.replace(/\r$/, '');
|
|
394
|
-
if (line.startsWith(marker)) {
|
|
395
|
-
const next = line.slice(marker.length).trim();
|
|
396
|
-
if (next && existsSync(next))
|
|
397
|
-
terminalCwd = next;
|
|
398
|
-
continue;
|
|
469
|
+
if (path === '/api/plugins' && req.method === 'POST') {
|
|
470
|
+
const body = await readJson(req);
|
|
471
|
+
const action = String(body.action ?? '');
|
|
472
|
+
const { saveConfig } = await import("../config/config.js");
|
|
473
|
+
if (action === 'reload') {
|
|
474
|
+
return json(res, 200, await syncPlugins());
|
|
475
|
+
}
|
|
476
|
+
if (action === 'toggle' && body.name) {
|
|
477
|
+
const name = String(body.name);
|
|
478
|
+
const set = new Set(config.disabledPlugins);
|
|
479
|
+
if (body.disabled)
|
|
480
|
+
set.add(name);
|
|
481
|
+
else
|
|
482
|
+
set.delete(name);
|
|
483
|
+
config.disabledPlugins = [...set];
|
|
484
|
+
await saveConfig(config);
|
|
485
|
+
return json(res, 200, await syncPlugins());
|
|
486
|
+
}
|
|
487
|
+
if (action === 'create') {
|
|
488
|
+
const scope = body.scope === 'project' ? 'project' : 'global';
|
|
489
|
+
const dir = pluginDirs().find((item) => item.id === scope).path;
|
|
490
|
+
await mkdir(dir, { recursive: true });
|
|
491
|
+
const safe = String(body.name ?? 'my_tool').replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 48) || 'my_tool';
|
|
492
|
+
let file = join(dir, `${safe}.mjs`);
|
|
493
|
+
let index = 2;
|
|
494
|
+
while (existsSync(file))
|
|
495
|
+
file = join(dir, `${safe}-${index++}.mjs`);
|
|
496
|
+
await writeFile(file, pluginTemplate(safe), 'utf8');
|
|
497
|
+
return json(res, 200, { file, ...(await syncPlugins()) });
|
|
498
|
+
}
|
|
499
|
+
if (action === 'open') {
|
|
500
|
+
const scope = body.scope === 'project' ? 'project' : 'global';
|
|
501
|
+
const dir = pluginDirs().find((item) => item.id === scope).path;
|
|
502
|
+
await mkdir(dir, { recursive: true });
|
|
503
|
+
const command = process.platform === 'darwin'
|
|
504
|
+
? { file: 'open', args: [dir] }
|
|
505
|
+
: process.platform === 'win32'
|
|
506
|
+
? { file: 'explorer.exe', args: [dir] }
|
|
507
|
+
: { file: 'xdg-open', args: [dir] };
|
|
508
|
+
try {
|
|
509
|
+
spawn(command.file, command.args, { detached: true, stdio: 'ignore', windowsHide: true }).unref();
|
|
510
|
+
}
|
|
511
|
+
catch {
|
|
512
|
+
return json(res, 400, { error: '打不开文件管理器,请手动打开:' + dir });
|
|
399
513
|
}
|
|
400
|
-
|
|
514
|
+
return json(res, 200, { dir });
|
|
401
515
|
}
|
|
402
|
-
if (
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
516
|
+
if (action === 'test' && body.file) {
|
|
517
|
+
const file = resolve(String(body.file));
|
|
518
|
+
const allowed = pluginDirs().some((dir) => {
|
|
519
|
+
const root = resolve(dir.path);
|
|
520
|
+
return file === root || file.startsWith(root + sep);
|
|
521
|
+
});
|
|
522
|
+
if (!allowed)
|
|
523
|
+
return json(res, 400, { error: '只能运行插件目录里的文件' });
|
|
524
|
+
if (!existsSync(file))
|
|
525
|
+
return json(res, 400, { error: '插件文件不存在' });
|
|
526
|
+
try {
|
|
527
|
+
const module = (await import(__rewriteRelativeImportExtension(`${pathToFileURL(file).href}?t=${Date.now()}`)));
|
|
528
|
+
const definition = module.default ?? module;
|
|
529
|
+
if (typeof definition.run !== 'function')
|
|
530
|
+
throw new Error('插件没有导出 run 函数');
|
|
531
|
+
const input = body.input && typeof body.input === 'object'
|
|
532
|
+
? body.input
|
|
533
|
+
: (() => {
|
|
534
|
+
try {
|
|
535
|
+
return JSON.parse(String(body.input ?? '{}'));
|
|
536
|
+
}
|
|
537
|
+
catch {
|
|
538
|
+
return {};
|
|
539
|
+
}
|
|
540
|
+
})();
|
|
541
|
+
const result = await definition.run(input, {});
|
|
542
|
+
return json(res, 200, {
|
|
543
|
+
result: typeof result === 'string' ? result : JSON.stringify(result, null, 2),
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
catch (error) {
|
|
547
|
+
return json(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
548
|
+
}
|
|
413
549
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
child.stderr?.on('data', (chunk) => pump(chunk, 'stderr'));
|
|
421
|
-
// 前端断开(按停止 / 关页面)时连同子进程一起收掉
|
|
422
|
-
res.on('close', () => {
|
|
423
|
-
if (terminalChild === child)
|
|
424
|
-
terminalChild = null;
|
|
425
|
-
if (!child.killed)
|
|
426
|
-
child.kill();
|
|
427
|
-
});
|
|
428
|
-
await new Promise((resolvePromise) => {
|
|
429
|
-
child.on('error', (error) => {
|
|
430
|
-
send('out', { stream: 'stderr', data: `启动失败:${error.message}\n` });
|
|
431
|
-
});
|
|
432
|
-
child.on('close', (code) => {
|
|
433
|
-
flush();
|
|
434
|
-
if (terminalChild === child)
|
|
435
|
-
terminalChild = null;
|
|
436
|
-
send('exit', { code: code ?? 0, cwd: terminalWorkdir() });
|
|
437
|
-
if (!res.writableEnded)
|
|
438
|
-
res.end();
|
|
439
|
-
resolvePromise();
|
|
550
|
+
return json(res, 400, { error: '未知的插件操作' });
|
|
551
|
+
}
|
|
552
|
+
if (path === '/api/shells' && req.method === 'GET') {
|
|
553
|
+
return json(res, 200, {
|
|
554
|
+
shells: terminals.shells,
|
|
555
|
+
default: terminals.defaultShell().id,
|
|
440
556
|
});
|
|
441
|
-
}
|
|
557
|
+
}
|
|
558
|
+
return notFound(res);
|
|
442
559
|
}
|
|
443
560
|
/**
|
|
444
561
|
* 执行 CLI 命令:命令表只有一份(cli/commands.ts),GUI 只是换了个输出通道。
|
|
@@ -672,6 +789,8 @@ export async function startUiServer(options) {
|
|
|
672
789
|
token,
|
|
673
790
|
port,
|
|
674
791
|
close: async () => {
|
|
792
|
+
await terminals.dispose();
|
|
793
|
+
wss.close();
|
|
675
794
|
// 浏览器可能留着 keep-alive 连接,先断开再关,否则 Windows 上会触发 libuv 断言
|
|
676
795
|
server.closeAllConnections?.();
|
|
677
796
|
await new Promise((resolvePromise) => server.close(() => resolvePromise()));
|
|
@@ -726,7 +845,22 @@ function notFound(res) {
|
|
|
726
845
|
async function sendFile(res, name) {
|
|
727
846
|
const { readFile } = await import('node:fs/promises');
|
|
728
847
|
try {
|
|
729
|
-
|
|
848
|
+
let target = join(assetRoot(), name);
|
|
849
|
+
// 源码模式(npm start)下 src/ui 里没有打包拷贝的 vendor;
|
|
850
|
+
// 直接从依赖包读,开发时也能秒开终端,不用先 build。
|
|
851
|
+
if (name.startsWith('vendor/') && !existsSync(target)) {
|
|
852
|
+
const vendorName = name.slice('vendor/'.length);
|
|
853
|
+
const specifier = VENDOR_FILES[vendorName];
|
|
854
|
+
if (specifier) {
|
|
855
|
+
try {
|
|
856
|
+
target = createRequire(import.meta.url).resolve(specifier);
|
|
857
|
+
}
|
|
858
|
+
catch {
|
|
859
|
+
/* 依赖缺失,走下面的 404 */
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
const file = await readFile(target);
|
|
730
864
|
res.writeHead(200, {
|
|
731
865
|
'Content-Type': MIME[extname(name)] ?? 'application/octet-stream',
|
|
732
866
|
'Content-Length': file.byteLength,
|
package/dist/tools/plugins.js
CHANGED
|
@@ -78,6 +78,7 @@ function toTool(definition, source) {
|
|
|
78
78
|
export async function loadPluginTools(dirs, existingNames = new Set()) {
|
|
79
79
|
const tools = [];
|
|
80
80
|
const errors = [];
|
|
81
|
+
const plugins = [];
|
|
81
82
|
const seen = new Set(existingNames);
|
|
82
83
|
for (const dir of dirs) {
|
|
83
84
|
const absolute = resolve(dir);
|
|
@@ -99,38 +100,65 @@ export async function loadPluginTools(dirs, existingNames = new Set()) {
|
|
|
99
100
|
if (entry.name.endsWith('.test.mjs'))
|
|
100
101
|
continue;
|
|
101
102
|
const file = join(absolute, entry.name);
|
|
103
|
+
const base = {
|
|
104
|
+
file,
|
|
105
|
+
dir: absolute,
|
|
106
|
+
name: entry.name,
|
|
107
|
+
description: '',
|
|
108
|
+
approval: 'never',
|
|
109
|
+
ok: false,
|
|
110
|
+
};
|
|
102
111
|
try {
|
|
103
112
|
const module = (await import(__rewriteRelativeImportExtension(pathToFileURL(file).href)));
|
|
104
113
|
const definition = (module.default ?? module);
|
|
105
114
|
if (!definition || typeof definition !== 'object') {
|
|
106
|
-
|
|
115
|
+
base.error = '需要一个默认导出的对象';
|
|
116
|
+
errors.push(`${entry.name}: ${base.error}`);
|
|
117
|
+
plugins.push(base);
|
|
107
118
|
continue;
|
|
108
119
|
}
|
|
109
120
|
if (typeof definition.name !== 'string' || !TOOL_NAME.test(definition.name)) {
|
|
110
|
-
|
|
121
|
+
base.error = 'name 必须是字母开头的 1-64 位字母/数字/下划线';
|
|
122
|
+
errors.push(`${entry.name}: ${base.error}`);
|
|
123
|
+
plugins.push(base);
|
|
111
124
|
continue;
|
|
112
125
|
}
|
|
113
|
-
|
|
114
|
-
|
|
126
|
+
base.name = definition.name;
|
|
127
|
+
base.description = typeof definition.description === 'string' ? definition.description : '';
|
|
128
|
+
base.approval = VALID_APPROVALS.includes(definition.approval)
|
|
129
|
+
? definition.approval
|
|
130
|
+
: 'never';
|
|
131
|
+
if (!definition.description || !definition.description.trim()) {
|
|
132
|
+
base.error = '缺少 description';
|
|
133
|
+
errors.push(`${entry.name}: ${base.error}`);
|
|
134
|
+
plugins.push(base);
|
|
115
135
|
continue;
|
|
116
136
|
}
|
|
117
137
|
if (typeof definition.run !== 'function') {
|
|
118
|
-
|
|
138
|
+
base.error = '缺少 run(input, ctx) 函数';
|
|
139
|
+
errors.push(`${entry.name}: ${base.error}`);
|
|
140
|
+
plugins.push(base);
|
|
119
141
|
continue;
|
|
120
142
|
}
|
|
121
143
|
if (seen.has(definition.name)) {
|
|
122
|
-
|
|
144
|
+
base.error = `工具名 ${definition.name} 与已有工具冲突`;
|
|
145
|
+
errors.push(`${entry.name}: ${base.error}`);
|
|
146
|
+
plugins.push(base);
|
|
123
147
|
continue;
|
|
124
148
|
}
|
|
125
149
|
seen.add(definition.name);
|
|
126
150
|
tools.push(toTool(definition, entry.name));
|
|
151
|
+
base.ok = true;
|
|
152
|
+
plugins.push(base);
|
|
127
153
|
}
|
|
128
154
|
catch (error) {
|
|
129
|
-
|
|
155
|
+
base.error = error instanceof Error ? error.message : String(error);
|
|
156
|
+
errors.push(`${entry.name}: ${base.error}`);
|
|
157
|
+
plugins.push(base);
|
|
130
158
|
}
|
|
131
159
|
}
|
|
132
160
|
}
|
|
133
|
-
return { tools, errors };
|
|
161
|
+
return { tools, errors, plugins };
|
|
134
162
|
}
|
|
135
163
|
/** 生成插件模板,方便用户照着写。 */
|
|
136
164
|
export function pluginTemplate(name = 'my_tool') {
|