@vincentbanana/bananacli 0.4.8 → 0.5.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.
- package/dist/config/config.js +1 -0
- package/dist/index.js +6 -4
- package/dist/server/ui-server.js +335 -5
- package/dist/tools/plugins.js +36 -8
- package/dist/ui/app.js +1606 -418
- package/dist/ui/index.html +120 -36
- package/dist/ui/style.css +1310 -1
- package/package.json +1 -1
package/dist/config/config.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -241,19 +241,21 @@ async function main() {
|
|
|
241
241
|
if (resolved.noTools)
|
|
242
242
|
ui.hint('已禁用全部工具(--no-tools)。');
|
|
243
243
|
// 加载用户级 / 项目级插件工具(.banana/tools/*.mjs)
|
|
244
|
+
let pluginTools = [];
|
|
244
245
|
if (!resolved.noTools) {
|
|
245
246
|
const pluginResult = await loadPluginTools([join(bananaHome(), 'tools'), join(config.workspace, '.banana', 'tools')], new Set(allTools.map((tool) => tool.name)));
|
|
246
247
|
for (const error of pluginResult.errors) {
|
|
247
248
|
ui.warn(`插件未加载: ${error}`);
|
|
248
249
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
250
|
+
pluginTools = pluginResult.tools.filter((tool) => !config.disabledPlugins.includes(tool.name));
|
|
251
|
+
if (pluginTools.length) {
|
|
252
|
+
tools.push(...pluginTools);
|
|
253
|
+
ui.hint(`已加载 ${pluginTools.length} 个插件工具: ${pluginTools.map((tool) => tool.name).join(', ')}`);
|
|
252
254
|
}
|
|
253
255
|
}
|
|
254
256
|
// GUI 模式:本地服务 + 浏览器,不进入终端对话(不需要先有可用 provider)
|
|
255
257
|
if (resolved.ui) {
|
|
256
|
-
const handle = await startUiServer({ config, store, tools, port: resolved.port });
|
|
258
|
+
const handle = await startUiServer({ config, store, tools, pluginTools, port: resolved.port });
|
|
257
259
|
ui.line();
|
|
258
260
|
ui.ok(`GUI 已启动:${handle.url}`);
|
|
259
261
|
ui.hint('只监听 127.0.0.1,URL 里的 token 是访问凭证,不要分享。按 Ctrl+C 退出。');
|
package/dist/server/ui-server.js
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
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
15
|
import { WebSocketServer } from 'ws';
|
|
8
|
-
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
9
17
|
import { createRequire } from 'node:module';
|
|
10
18
|
import { TerminalManager, loadPty, normalizeCwd } from "./terminal.js";
|
|
11
19
|
import { bananaHome } from "../config/config.js";
|
|
@@ -16,7 +24,7 @@ import { estimateCost, formatCost, resolvePricing } from "../providers/pricing.j
|
|
|
16
24
|
import { SessionStore } from "../session/store.js";
|
|
17
25
|
import { BackupStore, newTurnId } from "../session/backups.js";
|
|
18
26
|
import { allTools, permissionFor, summarizeInput } from "../tools/index.js";
|
|
19
|
-
import { loadPluginTools } from "../tools/plugins.js";
|
|
27
|
+
import { loadPluginTools, pluginTemplate } from "../tools/plugins.js";
|
|
20
28
|
import { resolvePath } from "../tools/fs.js";
|
|
21
29
|
import { commands, findCommand, suggestCommand } from "../cli/commands.js";
|
|
22
30
|
import { completeCommand, completeFileReference } from "../cli/completion.js";
|
|
@@ -76,6 +84,111 @@ let activeAssetsDir = null;
|
|
|
76
84
|
function assetRoot() {
|
|
77
85
|
return activeAssetsDir ?? resolveAssetsDir();
|
|
78
86
|
}
|
|
87
|
+
/** 行级 diff:小文件用 LCS,大文件退化成整段替换,避免把服务卡住。 */
|
|
88
|
+
function lineDiff(before, after) {
|
|
89
|
+
const left = before ? before.split(/\r?\n/) : [];
|
|
90
|
+
const right = after ? after.split(/\r?\n/) : [];
|
|
91
|
+
const n = left.length;
|
|
92
|
+
const m = right.length;
|
|
93
|
+
const ops = [];
|
|
94
|
+
let additions = 0;
|
|
95
|
+
let deletions = 0;
|
|
96
|
+
if (n * m > 1_200_000) {
|
|
97
|
+
for (const text of left) {
|
|
98
|
+
ops.push({ type: 'del', text });
|
|
99
|
+
deletions += 1;
|
|
100
|
+
}
|
|
101
|
+
for (const text of right) {
|
|
102
|
+
ops.push({ type: 'add', text });
|
|
103
|
+
additions += 1;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
const width = m + 1;
|
|
108
|
+
const table = new Uint32Array((n + 1) * width);
|
|
109
|
+
for (let i = n - 1; i >= 0; i -= 1) {
|
|
110
|
+
for (let j = m - 1; j >= 0; j -= 1) {
|
|
111
|
+
const diag = table[(i + 1) * width + j + 1] ?? 0;
|
|
112
|
+
const down = table[(i + 1) * width + j] ?? 0;
|
|
113
|
+
const rightward = table[i * width + j + 1] ?? 0;
|
|
114
|
+
table[i * width + j] = left[i] === right[j] ? diag + 1 : Math.max(down, rightward);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
let i = 0;
|
|
118
|
+
let j = 0;
|
|
119
|
+
while (i < n && j < m) {
|
|
120
|
+
const a = left[i] ?? '';
|
|
121
|
+
const b = right[j] ?? '';
|
|
122
|
+
const down = table[(i + 1) * width + j] ?? 0;
|
|
123
|
+
const rightward = table[i * width + j + 1] ?? 0;
|
|
124
|
+
if (a === b) {
|
|
125
|
+
ops.push({ type: 'ctx', text: a });
|
|
126
|
+
i += 1;
|
|
127
|
+
j += 1;
|
|
128
|
+
}
|
|
129
|
+
else if (down >= rightward) {
|
|
130
|
+
ops.push({ type: 'del', text: a });
|
|
131
|
+
deletions += 1;
|
|
132
|
+
i += 1;
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
ops.push({ type: 'add', text: b });
|
|
136
|
+
additions += 1;
|
|
137
|
+
j += 1;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
while (i < n) {
|
|
141
|
+
ops.push({ type: 'del', text: left[i++] ?? '' });
|
|
142
|
+
deletions += 1;
|
|
143
|
+
}
|
|
144
|
+
while (j < m) {
|
|
145
|
+
ops.push({ type: 'add', text: right[j++] ?? '' });
|
|
146
|
+
additions += 1;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// 只保留改动附近 3 行上下文,跳过的部分用省略行代替
|
|
150
|
+
const keep = new Set();
|
|
151
|
+
ops.forEach((op, index) => {
|
|
152
|
+
if (op.type === 'ctx')
|
|
153
|
+
return;
|
|
154
|
+
for (let k = Math.max(0, index - 3); k <= Math.min(ops.length - 1, index + 3); k += 1)
|
|
155
|
+
keep.add(k);
|
|
156
|
+
});
|
|
157
|
+
const lines = [];
|
|
158
|
+
let oldLine = 1;
|
|
159
|
+
let newLine = 1;
|
|
160
|
+
let lastKept = true;
|
|
161
|
+
let truncated = lines.length >= 800;
|
|
162
|
+
ops.forEach((op, index) => {
|
|
163
|
+
if (!keep.has(index)) {
|
|
164
|
+
if (lastKept && lines.length < 800) {
|
|
165
|
+
lines.push({ type: 'ctx', text: '…', oldLine: null, newLine: null });
|
|
166
|
+
}
|
|
167
|
+
lastKept = false;
|
|
168
|
+
if (op.type !== 'add')
|
|
169
|
+
oldLine += 1;
|
|
170
|
+
if (op.type !== 'del')
|
|
171
|
+
newLine += 1;
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
lastKept = true;
|
|
175
|
+
if (lines.length >= 800) {
|
|
176
|
+
truncated = true;
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
lines.push({
|
|
180
|
+
type: op.type,
|
|
181
|
+
text: op.text,
|
|
182
|
+
oldLine: op.type === 'add' ? null : oldLine,
|
|
183
|
+
newLine: op.type === 'del' ? null : newLine,
|
|
184
|
+
});
|
|
185
|
+
if (op.type !== 'add')
|
|
186
|
+
oldLine += 1;
|
|
187
|
+
if (op.type !== 'del')
|
|
188
|
+
newLine += 1;
|
|
189
|
+
});
|
|
190
|
+
return { lines, additions, deletions, truncated };
|
|
191
|
+
}
|
|
79
192
|
/** 启动只监听回环地址的本地 GUI 服务;token 保护所有接口。 */
|
|
80
193
|
export async function startUiServer(options) {
|
|
81
194
|
const { config, store } = options;
|
|
@@ -115,6 +228,9 @@ export async function startUiServer(options) {
|
|
|
115
228
|
workspace: config.workspace,
|
|
116
229
|
platform: process.platform,
|
|
117
230
|
permissions: config.permissions,
|
|
231
|
+
systemPrompt: config.systemPrompt,
|
|
232
|
+
showReasoning: config.showReasoning,
|
|
233
|
+
disabledPlugins: config.disabledPlugins,
|
|
118
234
|
tools: options.tools.map((tool) => ({ name: tool.name, approval: tool.approval, permission: permissionFor(config.permissions, tool) })),
|
|
119
235
|
session: {
|
|
120
236
|
id: session.id,
|
|
@@ -130,6 +246,29 @@ export async function startUiServer(options) {
|
|
|
130
246
|
updatedAt: item.updatedAt,
|
|
131
247
|
turns: item.turns,
|
|
132
248
|
}));
|
|
249
|
+
// ---------- 插件:本地插件工坊 ----------
|
|
250
|
+
const builtinTools = options.tools.filter((tool) => !(options.pluginTools ?? []).some((plugin) => plugin.name === tool.name));
|
|
251
|
+
const pluginDirs = () => [
|
|
252
|
+
{ id: 'global', label: '全局插件', path: join(bananaHome(), 'tools') },
|
|
253
|
+
{ id: 'project', label: '项目插件', path: join(config.workspace, '.banana', 'tools') },
|
|
254
|
+
];
|
|
255
|
+
const syncPlugins = async () => {
|
|
256
|
+
const dirs = pluginDirs();
|
|
257
|
+
const result = await loadPluginTools(dirs.map((dir) => dir.path), new Set(builtinTools.map((tool) => tool.name)));
|
|
258
|
+
const disabled = new Set(config.disabledPlugins);
|
|
259
|
+
const enabledTools = result.tools.filter((tool) => !disabled.has(tool.name));
|
|
260
|
+
options.tools.splice(0, options.tools.length, ...builtinTools, ...enabledTools);
|
|
261
|
+
return {
|
|
262
|
+
dirs,
|
|
263
|
+
plugins: result.plugins.map((plugin) => ({
|
|
264
|
+
...plugin,
|
|
265
|
+
disabled: disabled.has(plugin.name),
|
|
266
|
+
enabled: plugin.ok && !disabled.has(plugin.name),
|
|
267
|
+
})),
|
|
268
|
+
errors: result.errors,
|
|
269
|
+
toolCount: options.tools.length,
|
|
270
|
+
};
|
|
271
|
+
};
|
|
133
272
|
const server = createServer((req, res) => {
|
|
134
273
|
handle(req, res).catch((error) => {
|
|
135
274
|
if (!res.headersSent)
|
|
@@ -183,8 +322,12 @@ export async function startUiServer(options) {
|
|
|
183
322
|
}
|
|
184
323
|
let session = turn?.session ??
|
|
185
324
|
(activeId ? await store.load(activeId) : null) ??
|
|
186
|
-
(await store.lastActive().then(async (id) => (id ? await store.load(id) : null)))
|
|
187
|
-
|
|
325
|
+
(await store.lastActive().then(async (id) => (id ? await store.load(id) : null)));
|
|
326
|
+
if (!session) {
|
|
327
|
+
// 空会话也立刻落盘:否则每个请求都会重新 create,会话 id 一直变
|
|
328
|
+
session = store.create();
|
|
329
|
+
await store.save(session);
|
|
330
|
+
}
|
|
188
331
|
activeId = session.id;
|
|
189
332
|
if (path === '/api/info' && req.method === 'GET')
|
|
190
333
|
return json(res, 200, await info(session));
|
|
@@ -273,6 +416,104 @@ export async function startUiServer(options) {
|
|
|
273
416
|
const body = await readJson(req);
|
|
274
417
|
return runCliCommand(body.input ?? '', res, session);
|
|
275
418
|
}
|
|
419
|
+
if (path === '/api/find' && req.method === 'GET') {
|
|
420
|
+
const query = (url.searchParams.get('q') ?? '').trim().toLowerCase();
|
|
421
|
+
const limit = Math.max(1, Math.min(120, Number(url.searchParams.get('limit') ?? 60) || 60));
|
|
422
|
+
const skip = new Set([
|
|
423
|
+
'.git', 'node_modules', 'dist', 'build', 'out', 'coverage', '.next', '.nuxt',
|
|
424
|
+
'.venv', 'venv', '__pycache__', '.idea', '.vscode', '.cache', '.banana', 'vendor',
|
|
425
|
+
]);
|
|
426
|
+
const results = [];
|
|
427
|
+
let scanned = 0;
|
|
428
|
+
const walk = async (dir, rel, depth) => {
|
|
429
|
+
if (results.length >= limit || scanned > 6000 || depth > 8)
|
|
430
|
+
return;
|
|
431
|
+
let entries;
|
|
432
|
+
try {
|
|
433
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
439
|
+
for (const entry of entries) {
|
|
440
|
+
if (results.length >= limit || scanned > 6000)
|
|
441
|
+
return;
|
|
442
|
+
if (entry.name.startsWith('.') && entry.name !== '.env.example')
|
|
443
|
+
continue;
|
|
444
|
+
if (skip.has(entry.name))
|
|
445
|
+
continue;
|
|
446
|
+
const childRel = rel ? `${rel}/${entry.name}` : entry.name;
|
|
447
|
+
if (entry.isDirectory()) {
|
|
448
|
+
await walk(join(dir, entry.name), childRel, depth + 1);
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
if (!entry.isFile())
|
|
452
|
+
continue;
|
|
453
|
+
scanned += 1;
|
|
454
|
+
if (!query || childRel.toLowerCase().includes(query))
|
|
455
|
+
results.push(childRel);
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
try {
|
|
459
|
+
await walk(config.workspace, '', 0);
|
|
460
|
+
return json(res, 200, { files: results, scanned });
|
|
461
|
+
}
|
|
462
|
+
catch (error) {
|
|
463
|
+
return json(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
if (path === '/api/changes' && req.method === 'GET') {
|
|
467
|
+
try {
|
|
468
|
+
const store = backups(session.id);
|
|
469
|
+
const manifest = await store.load();
|
|
470
|
+
const turns = [...manifest.turns].reverse().slice(0, 8);
|
|
471
|
+
const payload = [];
|
|
472
|
+
for (const turn of turns) {
|
|
473
|
+
const files = [];
|
|
474
|
+
for (const change of turn.changes.slice(0, 40)) {
|
|
475
|
+
let before = '';
|
|
476
|
+
let after = '';
|
|
477
|
+
if (change.backup) {
|
|
478
|
+
try {
|
|
479
|
+
before = await readFile(join(store.dir, turn.turnId, change.backup), 'utf8');
|
|
480
|
+
}
|
|
481
|
+
catch {
|
|
482
|
+
before = '';
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
try {
|
|
486
|
+
const info = await stat(change.path);
|
|
487
|
+
if (info.isFile() && info.size <= MAX_FILE_BYTES)
|
|
488
|
+
after = await readFile(change.path, 'utf8');
|
|
489
|
+
}
|
|
490
|
+
catch {
|
|
491
|
+
after = '';
|
|
492
|
+
}
|
|
493
|
+
const diff = lineDiff(before, after);
|
|
494
|
+
files.push({
|
|
495
|
+
path: change.relative,
|
|
496
|
+
absolute: change.path,
|
|
497
|
+
existed: change.existed,
|
|
498
|
+
missing: after === '' && change.existed,
|
|
499
|
+
additions: diff.additions,
|
|
500
|
+
deletions: diff.deletions,
|
|
501
|
+
truncated: diff.truncated,
|
|
502
|
+
lines: diff.lines,
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
if (files.length)
|
|
506
|
+
payload.push({ turnId: turn.turnId, at: turn.at, files });
|
|
507
|
+
}
|
|
508
|
+
return json(res, 200, {
|
|
509
|
+
turns: payload,
|
|
510
|
+
totalFiles: payload.reduce((sum, turn) => sum + turn.files.length, 0),
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
catch (error) {
|
|
514
|
+
return json(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
515
|
+
}
|
|
516
|
+
}
|
|
276
517
|
if (path === '/api/commands' && req.method === 'GET') {
|
|
277
518
|
return json(res, 200, {
|
|
278
519
|
commands: commands.map((command) => ({
|
|
@@ -310,6 +551,9 @@ export async function startUiServer(options) {
|
|
|
310
551
|
else if (body.action === 'system' && body.value) {
|
|
311
552
|
config.systemPrompt = body.value;
|
|
312
553
|
}
|
|
554
|
+
else if (body.action === 'showReasoning') {
|
|
555
|
+
config.showReasoning = body.value === 'true' || body.value === '1';
|
|
556
|
+
}
|
|
313
557
|
else if (body.action === 'workspace' && body.value) {
|
|
314
558
|
const next = resolve(String(body.value).trim());
|
|
315
559
|
if (!existsSync(next)) {
|
|
@@ -379,6 +623,92 @@ export async function startUiServer(options) {
|
|
|
379
623
|
activeId = target.id;
|
|
380
624
|
return chat(body, res, target);
|
|
381
625
|
}
|
|
626
|
+
if (path === '/api/plugins' && req.method === 'GET') {
|
|
627
|
+
return json(res, 200, await syncPlugins());
|
|
628
|
+
}
|
|
629
|
+
if (path === '/api/plugins' && req.method === 'POST') {
|
|
630
|
+
const body = await readJson(req);
|
|
631
|
+
const action = String(body.action ?? '');
|
|
632
|
+
const { saveConfig } = await import("../config/config.js");
|
|
633
|
+
if (action === 'reload') {
|
|
634
|
+
return json(res, 200, await syncPlugins());
|
|
635
|
+
}
|
|
636
|
+
if (action === 'toggle' && body.name) {
|
|
637
|
+
const name = String(body.name);
|
|
638
|
+
const set = new Set(config.disabledPlugins);
|
|
639
|
+
if (body.disabled)
|
|
640
|
+
set.add(name);
|
|
641
|
+
else
|
|
642
|
+
set.delete(name);
|
|
643
|
+
config.disabledPlugins = [...set];
|
|
644
|
+
await saveConfig(config);
|
|
645
|
+
return json(res, 200, await syncPlugins());
|
|
646
|
+
}
|
|
647
|
+
if (action === 'create') {
|
|
648
|
+
const scope = body.scope === 'project' ? 'project' : 'global';
|
|
649
|
+
const dir = pluginDirs().find((item) => item.id === scope).path;
|
|
650
|
+
await mkdir(dir, { recursive: true });
|
|
651
|
+
const safe = String(body.name ?? 'my_tool').replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 48) || 'my_tool';
|
|
652
|
+
let file = join(dir, `${safe}.mjs`);
|
|
653
|
+
let index = 2;
|
|
654
|
+
while (existsSync(file))
|
|
655
|
+
file = join(dir, `${safe}-${index++}.mjs`);
|
|
656
|
+
await writeFile(file, pluginTemplate(safe), 'utf8');
|
|
657
|
+
return json(res, 200, { file, ...(await syncPlugins()) });
|
|
658
|
+
}
|
|
659
|
+
if (action === 'open') {
|
|
660
|
+
const scope = body.scope === 'project' ? 'project' : 'global';
|
|
661
|
+
const dir = pluginDirs().find((item) => item.id === scope).path;
|
|
662
|
+
await mkdir(dir, { recursive: true });
|
|
663
|
+
const command = process.platform === 'darwin'
|
|
664
|
+
? { file: 'open', args: [dir] }
|
|
665
|
+
: process.platform === 'win32'
|
|
666
|
+
? { file: 'explorer.exe', args: [dir] }
|
|
667
|
+
: { file: 'xdg-open', args: [dir] };
|
|
668
|
+
try {
|
|
669
|
+
spawn(command.file, command.args, { detached: true, stdio: 'ignore', windowsHide: true }).unref();
|
|
670
|
+
}
|
|
671
|
+
catch {
|
|
672
|
+
return json(res, 400, { error: '打不开文件管理器,请手动打开:' + dir });
|
|
673
|
+
}
|
|
674
|
+
return json(res, 200, { dir });
|
|
675
|
+
}
|
|
676
|
+
if (action === 'test' && body.file) {
|
|
677
|
+
const file = resolve(String(body.file));
|
|
678
|
+
const allowed = pluginDirs().some((dir) => {
|
|
679
|
+
const root = resolve(dir.path);
|
|
680
|
+
return file === root || file.startsWith(root + sep);
|
|
681
|
+
});
|
|
682
|
+
if (!allowed)
|
|
683
|
+
return json(res, 400, { error: '只能运行插件目录里的文件' });
|
|
684
|
+
if (!existsSync(file))
|
|
685
|
+
return json(res, 400, { error: '插件文件不存在' });
|
|
686
|
+
try {
|
|
687
|
+
const module = (await import(__rewriteRelativeImportExtension(`${pathToFileURL(file).href}?t=${Date.now()}`)));
|
|
688
|
+
const definition = module.default ?? module;
|
|
689
|
+
if (typeof definition.run !== 'function')
|
|
690
|
+
throw new Error('插件没有导出 run 函数');
|
|
691
|
+
const input = body.input && typeof body.input === 'object'
|
|
692
|
+
? body.input
|
|
693
|
+
: (() => {
|
|
694
|
+
try {
|
|
695
|
+
return JSON.parse(String(body.input ?? '{}'));
|
|
696
|
+
}
|
|
697
|
+
catch {
|
|
698
|
+
return {};
|
|
699
|
+
}
|
|
700
|
+
})();
|
|
701
|
+
const result = await definition.run(input, {});
|
|
702
|
+
return json(res, 200, {
|
|
703
|
+
result: typeof result === 'string' ? result : JSON.stringify(result, null, 2),
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
catch (error) {
|
|
707
|
+
return json(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
return json(res, 400, { error: '未知的插件操作' });
|
|
711
|
+
}
|
|
382
712
|
if (path === '/api/shells' && req.method === 'GET') {
|
|
383
713
|
return json(res, 200, {
|
|
384
714
|
shells: terminals.shells,
|
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') {
|