@vincentbanana/banana-gui 0.1.0 → 0.2.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/README.md +30 -5
- package/bin/banana-gui.mjs +37 -17
- package/desktop/main.cjs +113 -0
- package/package.json +42 -4
- package/ui/app.js +568 -28
- package/ui/index.html +19 -1
- package/ui/style.css +257 -3
package/README.md
CHANGED
|
@@ -22,13 +22,38 @@ banana-gui --provider deepseek --model deepseek-chat
|
|
|
22
22
|
|
|
23
23
|
## 特性
|
|
24
24
|
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
25
|
+
- **和 CLI 同一套内核**:agent 循环、工具、权限、斜杠命令全部来自 `@vincentbanana/bananacli`,两边行为完全一致
|
|
26
|
+
- 左侧会话列表:新建 / 切换 / 重命名 / 导出(Markdown、JSON)/ 删除
|
|
27
|
+
- 输入 `/` 调出命令面板(`/help` 看全部),输入 `@` 补全项目文件
|
|
28
|
+
- 流式回复、工具调用实时状态,点工具行可展开完整输出
|
|
29
|
+
- 危险操作弹窗确认,写文件前给 diff
|
|
28
30
|
- 一键撤销上一轮文件改动
|
|
31
|
+
- 右侧项目文件面板:浏览目录、预览文件、一键插入引用
|
|
29
32
|
- 粘贴或拖拽图片,随消息一起发送
|
|
30
|
-
-
|
|
31
|
-
-
|
|
33
|
+
- 模型、权限、API Key、工作区、系统提示词在设置里改,改完立刻生效
|
|
34
|
+
- 权限设置写清了每种模式的含义和风险,新手也不会懵
|
|
35
|
+
- 三态主题:跟随系统 / 浅色 / 深色,纯黑白无渐变
|
|
36
|
+
- 没配 API Key 也能打开界面,照着引导填完即可用
|
|
37
|
+
|
|
38
|
+
## 桌面版
|
|
39
|
+
|
|
40
|
+
仓库里有 Electron 主进程,可以打包成独立应用:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm install # 装 electron 与 electron-builder
|
|
44
|
+
npm run desktop # 本地直接跑桌面版
|
|
45
|
+
npm run dist:win # 出 Windows 安装包(也支持 dist:mac / dist:linux)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
npm 安装的 `banana-gui` 仍是浏览器版,零原生依赖,任何装了 Node 20+ 的系统都能跑。
|
|
49
|
+
|
|
50
|
+
## 配置
|
|
51
|
+
|
|
52
|
+
首次启动会提示配置向导。GUI 与 CLI 共用 `~/.banana/config.json`:
|
|
53
|
+
|
|
54
|
+
- 也可以直接在界面的设置面板里填 API Key、切换模型和权限
|
|
55
|
+
- 环境变量优先级更高,例如 `DEEPSEEK_API_KEY`
|
|
56
|
+
- API Key 只写在你自己的机器上,不会进代码仓库
|
|
32
57
|
|
|
33
58
|
## 安全
|
|
34
59
|
|
package/bin/banana-gui.mjs
CHANGED
|
@@ -2,29 +2,50 @@
|
|
|
2
2
|
// Banana GUI 启动器:复用 CLI 包的 agent 内核,用本包自带的界面资源。
|
|
3
3
|
// 安装:npm install -g @vincentbanana/banana-gui
|
|
4
4
|
// 启动:banana-gui(首次会进配置向导)
|
|
5
|
-
import {
|
|
6
|
-
import { dirname, join } from 'node:path';
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
7
6
|
import { spawn } from 'node:child_process';
|
|
8
7
|
|
|
9
|
-
const
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
|
|
10
|
+
function resolveCli() {
|
|
11
|
+
// 依赖可能被 npm 提升到上层 node_modules,用 require 解析更稳;
|
|
12
|
+
// 注意只能解析包根(子路径受 exports 限制)
|
|
13
|
+
try {
|
|
14
|
+
return require.resolve('@vincentbanana/bananacli');
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
10
20
|
const args = process.argv.slice(2);
|
|
11
21
|
const passthrough = ['--port', '--cwd', '--provider', '--model', '--base-url', '--api-key', '--setup'];
|
|
12
22
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if (arg === '--help' || arg === '-h' || arg === '--version' || arg === '-v') {
|
|
17
|
-
console.log('banana-gui — Banana 的图形界面');
|
|
18
|
-
console.log('');
|
|
19
|
-
console.log('用法: banana-gui [选项]');
|
|
20
|
-
console.log(' --port <端口> 指定端口(默认随机)');
|
|
21
|
-
console.log(' --cwd <目录> 指定工作区');
|
|
22
|
-
console.log(' --provider <名> 指定 provider');
|
|
23
|
-
console.log(' --model <名> 指定模型');
|
|
24
|
-
console.log(' --open 启动后打开浏览器(默认已开启)');
|
|
25
|
-
console.log(' --help 显示帮助');
|
|
23
|
+
if (args.some((arg) => arg === '--help' || arg === '-h' || args.length === 0 || arg === '--version' || arg === '-v')) {
|
|
24
|
+
if (args.includes('--version') || args.includes('-v')) {
|
|
25
|
+
console.log('0.1.1');
|
|
26
26
|
process.exit(0);
|
|
27
27
|
}
|
|
28
|
+
console.log('banana-gui — Banana 的图形界面');
|
|
29
|
+
console.log('');
|
|
30
|
+
console.log('用法: banana-gui [选项]');
|
|
31
|
+
console.log(' --port <端口> 指定端口(默认随机)');
|
|
32
|
+
console.log(' --cwd <目录> 指定工作区');
|
|
33
|
+
console.log(' --provider <名> 指定 provider');
|
|
34
|
+
console.log(' --model <名> 指定模型');
|
|
35
|
+
console.log(' --open 启动后打开浏览器(默认已开启)');
|
|
36
|
+
console.log(' --help 显示帮助');
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const entry = resolveCli();
|
|
41
|
+
if (!entry) {
|
|
42
|
+
console.error('找不到依赖 @vincentbanana/bananacli,请重新安装:npm install -g @vincentbanana/banana-gui');
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const cliArgs = ['--ui'];
|
|
47
|
+
let forwarding = false;
|
|
48
|
+
for (const arg of args) {
|
|
28
49
|
if (arg === '--open') {
|
|
29
50
|
forwarding = false;
|
|
30
51
|
continue;
|
|
@@ -33,7 +54,6 @@ for (const arg of args) {
|
|
|
33
54
|
cliArgs.push(arg);
|
|
34
55
|
}
|
|
35
56
|
|
|
36
|
-
const entry = join(here, '..', 'node_modules', '@vincentbanana', 'bananacli', 'dist', 'index.js');
|
|
37
57
|
const child = spawn(process.execPath, [entry, ...cliArgs], { stdio: 'inherit' });
|
|
38
58
|
child.on('exit', (code) => process.exit(code ?? 0));
|
|
39
59
|
for (const signal of ['SIGINT', 'SIGTERM']) {
|
package/desktop/main.cjs
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Banana 桌面版主进程:拉起本地 GUI 服务,再用窗口加载它。
|
|
2
|
+
// 界面资源仍来自本包 ui/,agent 内核来自 @vincentbanana/bananacli。
|
|
3
|
+
const { app, BrowserWindow, Menu, shell, dialog } = require('electron');
|
|
4
|
+
const { spawn } = require('node:child_process');
|
|
5
|
+
const { createRequire } = require('node:module');
|
|
6
|
+
|
|
7
|
+
const require_ = createRequire(__filename);
|
|
8
|
+
let child = null;
|
|
9
|
+
let win = null;
|
|
10
|
+
|
|
11
|
+
function startServer() {
|
|
12
|
+
const entry = require_.resolve('@vincentbanana/bananacli');
|
|
13
|
+
// 端口交给 CLI 随机分配,避免多开时互相抢占
|
|
14
|
+
child = spawn(process.execPath, [entry, '--ui', '--no-open'], {
|
|
15
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
16
|
+
env: process.env,
|
|
17
|
+
});
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
let buffer = '';
|
|
20
|
+
const onData = (chunk) => {
|
|
21
|
+
buffer += chunk.toString('utf8');
|
|
22
|
+
const match = buffer.match(/http:\/\/127\.0\.0\.1:\d+\/t\/[a-f0-9]+\//);
|
|
23
|
+
if (match) {
|
|
24
|
+
child.stdout.off('data', onData);
|
|
25
|
+
resolve(match[0]);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
child.stdout.on('data', onData);
|
|
29
|
+
child.stderr.on('data', (chunk) => process.stderr.write(`[banana] ${chunk}`));
|
|
30
|
+
child.on('error', reject);
|
|
31
|
+
child.on('exit', (code) => {
|
|
32
|
+
if (!win) return;
|
|
33
|
+
dialog.showErrorBox('Banana 已退出', `本地服务意外结束(退出码 ${code})。`);
|
|
34
|
+
app.quit();
|
|
35
|
+
});
|
|
36
|
+
setTimeout(() => reject(new Error('启动超时:本地服务没有在 30 秒内就绪')), 30_000);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function createWindow() {
|
|
41
|
+
const url = await startServer();
|
|
42
|
+
win = new BrowserWindow({
|
|
43
|
+
width: 1180,
|
|
44
|
+
height: 820,
|
|
45
|
+
minWidth: 720,
|
|
46
|
+
minHeight: 520,
|
|
47
|
+
backgroundColor: '#ffffff',
|
|
48
|
+
title: 'Banana',
|
|
49
|
+
autoHideMenuBar: true,
|
|
50
|
+
webPreferences: { contextIsolation: true, nodeIntegration: false, sandbox: true },
|
|
51
|
+
});
|
|
52
|
+
await win.loadURL(url);
|
|
53
|
+
win.on('closed', () => {
|
|
54
|
+
win = null;
|
|
55
|
+
});
|
|
56
|
+
// 外部链接交给系统浏览器,窗口只负责本地界面
|
|
57
|
+
win.webContents.setWindowOpenHandler(({ url: target }) => {
|
|
58
|
+
shell.openExternal(target);
|
|
59
|
+
return { action: 'deny' };
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const menuTemplate = [
|
|
64
|
+
...(process.platform === 'darwin' ? [{ role: 'appMenu' }] : []),
|
|
65
|
+
{
|
|
66
|
+
label: '文件',
|
|
67
|
+
submenu: [
|
|
68
|
+
{
|
|
69
|
+
label: '新建对话',
|
|
70
|
+
accelerator: 'CmdOrCtrl+N',
|
|
71
|
+
click: () => win?.webContents.executeJavaScript("document.getElementById('newChat')?.click()"),
|
|
72
|
+
},
|
|
73
|
+
{ type: 'separator' },
|
|
74
|
+
process.platform === 'darwin' ? { role: 'close' } : { role: 'quit', label: '退出' },
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
{ role: 'editMenu', label: '编辑' },
|
|
78
|
+
{
|
|
79
|
+
label: '视图',
|
|
80
|
+
submenu: [
|
|
81
|
+
{
|
|
82
|
+
label: '项目文件',
|
|
83
|
+
accelerator: 'CmdOrCtrl+Shift+F',
|
|
84
|
+
click: () => win?.webContents.executeJavaScript("document.getElementById('filesBtn')?.click()"),
|
|
85
|
+
},
|
|
86
|
+
{ type: 'separator' },
|
|
87
|
+
{ role: 'reload', label: '重新加载' },
|
|
88
|
+
{ role: 'toggleDevTools', label: '开发者工具' },
|
|
89
|
+
{ role: 'togglefullscreen', label: '全屏' },
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
{ role: 'windowMenu', label: '窗口' },
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
app.whenReady().then(() => {
|
|
96
|
+
Menu.setApplicationMenu(Menu.buildFromTemplate(menuTemplate));
|
|
97
|
+
createWindow().catch((error) => {
|
|
98
|
+
dialog.showErrorBox('Banana 启动失败', String(error?.message ?? error));
|
|
99
|
+
app.quit();
|
|
100
|
+
});
|
|
101
|
+
app.on('activate', () => {
|
|
102
|
+
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
app.on('window-all-closed', () => {
|
|
107
|
+
if (process.platform !== 'darwin') app.quit();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
app.on('before-quit', () => {
|
|
111
|
+
child?.kill();
|
|
112
|
+
child = null;
|
|
113
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincentbanana/banana-gui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Banana 的图形界面:本地服务 + 浏览器。黑白主题,自绘图标,无第三方 UI 依赖",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -12,14 +12,52 @@
|
|
|
12
12
|
"files": [
|
|
13
13
|
"bin",
|
|
14
14
|
"ui",
|
|
15
|
+
"desktop",
|
|
15
16
|
"README.md"
|
|
16
17
|
],
|
|
17
18
|
"scripts": {
|
|
18
19
|
"sync": "node ../scripts/sync-gui.mjs",
|
|
19
|
-
"test": "node test/gui-package.test.mjs"
|
|
20
|
+
"test": "node test/gui-package.test.mjs",
|
|
21
|
+
"desktop": "electron desktop/main.cjs",
|
|
22
|
+
"pack": "electron-builder --dir",
|
|
23
|
+
"dist": "electron-builder",
|
|
24
|
+
"dist:win": "electron-builder --win",
|
|
25
|
+
"dist:mac": "electron-builder --mac",
|
|
26
|
+
"dist:linux": "electron-builder --linux"
|
|
27
|
+
},
|
|
28
|
+
"build": {
|
|
29
|
+
"appId": "dev.banana.gui",
|
|
30
|
+
"productName": "Banana",
|
|
31
|
+
"files": [
|
|
32
|
+
"desktop/**/*",
|
|
33
|
+
"ui/**/*",
|
|
34
|
+
"bin/**/*",
|
|
35
|
+
"package.json"
|
|
36
|
+
],
|
|
37
|
+
"mac": {
|
|
38
|
+
"category": "public.app-category.developer-tools",
|
|
39
|
+
"target": [
|
|
40
|
+
"dmg"
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
"win": {
|
|
44
|
+
"target": [
|
|
45
|
+
"nsis"
|
|
46
|
+
]
|
|
47
|
+
},
|
|
48
|
+
"linux": {
|
|
49
|
+
"target": [
|
|
50
|
+
"AppImage"
|
|
51
|
+
],
|
|
52
|
+
"category": "Development"
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"electron": "^33.0.0",
|
|
57
|
+
"electron-builder": "^25.0.0"
|
|
20
58
|
},
|
|
21
59
|
"dependencies": {
|
|
22
|
-
"@vincentbanana/bananacli": "^0.
|
|
60
|
+
"@vincentbanana/bananacli": "^0.3.0"
|
|
23
61
|
},
|
|
24
62
|
"keywords": [
|
|
25
63
|
"ai",
|
|
@@ -33,4 +71,4 @@
|
|
|
33
71
|
"publishConfig": {
|
|
34
72
|
"access": "public"
|
|
35
73
|
}
|
|
36
|
-
}
|
|
74
|
+
}
|