@vincentbanana/banana-gui 0.3.0 → 0.4.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 CHANGED
@@ -18,6 +18,14 @@ banana-gui-desktop # 同上,等价的独立命令
18
18
 
19
19
  electron 是可选依赖(`optionalDependencies`),npm 安装时会自动带上;万一某个平台装不上,`banana-gui` 仍然照常工作,只是 `--desktop` 会提示你用浏览器版。
20
20
 
21
+ > **国内网络注意**:electron 的二进制默认从 GitHub 下载,国内经常连不上(连不上时 npm 会静默跳过它,浏览器版不受影响)。想要桌面版就用镜像重装一次:
22
+ >
23
+ > ```bash
24
+ > npm install -g electron --electron_mirror=https://npmmirror.com/mirrors/electron/
25
+ > ```
26
+ >
27
+ > 装完 `banana-gui --desktop` 就能开窗口了。
28
+
21
29
  首次启动会进配置向导(选 provider、贴 API key、选权限、测连接),配置与 CLI 共用 `~/.banana/config.json`。
22
30
 
23
31
  ## 用法
@@ -59,7 +59,13 @@ if (!binary) {
59
59
  }
60
60
 
61
61
  const main = join(here, '..', 'desktop', 'main.cjs');
62
- const child = spawn(binary, [main, ...args], { stdio: 'inherit' });
62
+ const child = spawn(binary, [main, ...args], {
63
+ stdio: 'inherit',
64
+ env: {
65
+ ...process.env,
66
+ BANANA_UI_DIR: join(here, '..', 'ui'),
67
+ },
68
+ });
63
69
  child.on('error', (error) => {
64
70
  console.error('桌面版启动失败:' + error.message);
65
71
  console.error('可以先用浏览器版:banana-gui');
@@ -9,10 +9,10 @@ import { fileURLToPath } from 'node:url';
9
9
 
10
10
  const require = createRequire(import.meta.url);
11
11
  const pkg = require('../package.json');
12
+ const here = dirname(fileURLToPath(import.meta.url));
12
13
 
14
+ /** CLI 内核入口。依赖可能被 npm 提升到上层 node_modules,用 require 解析更稳。 */
13
15
  function resolveCli() {
14
- // 依赖可能被 npm 提升到上层 node_modules,用 require 解析更稳;
15
- // 注意只能解析包根(子路径受 exports 限制)
16
16
  try {
17
17
  return require.resolve('@vincentbanana/bananacli');
18
18
  } catch {
@@ -25,6 +25,7 @@ function electronBinary() {
25
25
  for (const specifier of ['electron', '@vincentbanana/banana-gui/node_modules/electron']) {
26
26
  try {
27
27
  const resolved = require.resolve(specifier);
28
+ // electron 包的主入口导出的是可执行文件路径
28
29
  const binary = require(resolved);
29
30
  if (typeof binary === 'string') return binary;
30
31
  } catch {
@@ -35,7 +36,6 @@ function electronBinary() {
35
36
  }
36
37
 
37
38
  const args = process.argv.slice(2);
38
- const passthrough = ['--port', '--cwd', '--provider', '--model', '--base-url', '--api-key', '--setup'];
39
39
 
40
40
  if (args.includes('--version') || args.includes('-v')) {
41
41
  console.log(pkg.version);
@@ -51,7 +51,7 @@ if (args.includes('--help') || args.includes('-h')) {
51
51
  console.log(' --cwd <目录> 指定工作区');
52
52
  console.log(' --provider <名> 指定 provider');
53
53
  console.log(' --model <名> 指定模型');
54
- console.log(' --open 启动后打开浏览器(默认已开启)');
54
+ console.log(' --no-open 不自动打开浏览器');
55
55
  console.log(' --version 显示版本');
56
56
  console.log(' --help 显示帮助');
57
57
  console.log('');
@@ -66,54 +66,45 @@ if (!entry) {
66
66
  process.exit(1);
67
67
  }
68
68
 
69
- // --desktop:交给 electron 开独立窗口,本进程不参与
69
+ // 界面资源用本包自带的那份,保证 GUI 包和它的 ui/ 版本一致
70
+ const uiDir = join(here, '..', 'ui');
71
+
72
+ /** 用户参数原样转给内核;只摘掉本启动器自己的开关。 */
73
+ function kernelArgs() {
74
+ const rest = [];
75
+ for (const arg of args) {
76
+ if (arg === '--desktop' || arg === '--open') continue;
77
+ rest.push(arg);
78
+ }
79
+ return rest;
80
+ }
81
+
70
82
  if (args.includes('--desktop')) {
83
+ // --desktop:交给 electron 开独立窗口,本进程只做转发
71
84
  const binary = electronBinary();
72
85
  if (!binary) {
73
- console.error('没装 electron,用不了独立窗口。补装一次即可:npm install -g electron');
74
- console.error('(不装也没关系,直接运行 banana-gui 用浏览器版,功能完全一样)');
86
+ console.error('没装 electron,用不了独立窗口。补装一次即可:');
87
+ console.error(' npm install -g electron --electron_mirror=https://npmmirror.com/mirrors/electron/');
88
+ console.error('(国内网络必须加镜像参数,GitHub 连不上会装不了)');
89
+ console.error('不装也没关系:直接运行 banana-gui 用浏览器版,功能完全一样。');
75
90
  process.exit(1);
76
91
  }
77
- const rest = args.filter((arg) => arg !== '--desktop');
78
- const desktop = spawn(binary, [join(dirname(fileURLToPath(import.meta.url)), '..', 'desktop', 'main.cjs'), ...rest], {
79
- stdio: 'inherit',
80
- });
81
- desktop.on('exit', (code) => process.exit(code ?? 0));
82
- for (const signal of ['SIGINT', 'SIGTERM']) process.on(signal, () => desktop.kill(signal));
92
+ launch(binary, [join(here, '..', 'desktop', 'main.cjs'), ...kernelArgs()], {});
83
93
  } else {
84
- startBrowser(entry);
94
+ launch(process.execPath, [entry, '--ui', ...kernelArgs()], { BANANA_UI_DIR: uiDir });
85
95
  }
86
96
 
87
- function startBrowser(entry) {
88
- const cliArgs = ['--ui'];
89
- let forwarding = false;
90
- for (const arg of args) {
91
- if (arg === '--open') {
92
- forwarding = false;
93
- continue;
94
- }
95
- forwarding = passthrough.includes(arg) || forwarding;
96
- cliArgs.push(arg);
97
- }
98
-
99
- const child = spawn(process.execPath, [entry, ...cliArgs], { stdio: 'inherit' });
97
+ function launch(command, commandArgs, extraEnv) {
98
+ const child = spawn(command, commandArgs, {
99
+ stdio: 'inherit',
100
+ env: { ...process.env, ...extraEnv },
101
+ });
102
+ child.on('error', (error) => {
103
+ console.error('启动失败:' + error.message);
104
+ process.exit(1);
105
+ });
100
106
  child.on('exit', (code) => process.exit(code ?? 0));
101
107
  for (const signal of ['SIGINT', 'SIGTERM']) {
102
108
  process.on(signal, () => child.kill(signal));
103
109
  }
104
110
  }
105
- let forwarding = false;
106
- for (const arg of args) {
107
- if (arg === '--open') {
108
- forwarding = false;
109
- continue;
110
- }
111
- forwarding = passthrough.includes(arg) || forwarding;
112
- cliArgs.push(arg);
113
- }
114
-
115
- const child = spawn(process.execPath, [entry, ...cliArgs], { stdio: 'inherit' });
116
- child.on('exit', (code) => process.exit(code ?? 0));
117
- for (const signal of ['SIGINT', 'SIGTERM']) {
118
- process.on(signal, () => child.kill(signal));
119
- }
package/desktop/main.cjs CHANGED
@@ -36,7 +36,11 @@ function startServer() {
36
36
  }
37
37
  child = spawn(process.execPath, [entry, ...serverArgs()], {
38
38
  stdio: ['ignore', 'pipe', 'pipe'],
39
- env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' },
39
+ env: {
40
+ ...process.env,
41
+ ELECTRON_RUN_AS_NODE: '1',
42
+ BANANA_UI_DIR: process.env.BANANA_UI_DIR ?? join(__dirname, '..', 'ui'),
43
+ },
40
44
  });
41
45
  return new Promise((resolve, reject) => {
42
46
  let buffer = '';
@@ -81,6 +85,28 @@ async function createWindow() {
81
85
  shell.openExternal(target);
82
86
  return { action: 'deny' };
83
87
  });
88
+ if (process.argv.includes('--selftest')) await selfTest(url);
89
+ }
90
+
91
+ /** --selftest:不靠人眼看,直接在页面里断言界面渲染成功,然后退出。 */
92
+ async function selfTest(url) {
93
+ try {
94
+ const probe = await win.webContents.executeJavaScript(`(() => ({
95
+ title: document.title,
96
+ composed: Boolean(document.querySelector('#composer')),
97
+ sidebar: Boolean(document.querySelector('#sidebar')),
98
+ scriptLoaded: typeof window.fetch === 'function' && Boolean(document.querySelector('#input')),
99
+ theme: getComputedStyle(document.documentElement).getPropertyValue('--bg').trim(),
100
+ }))()`);
101
+ const ok = probe.title === 'Banana' && probe.composed && probe.sidebar && probe.scriptLoaded;
102
+ console.log('SELFTEST ' + JSON.stringify({ ok, url: url.replace(/\/t\/[a-f0-9]+\//, '/t/<token>/'), ...probe }));
103
+ child?.kill();
104
+ app.exit(ok ? 0 : 1);
105
+ } catch (error) {
106
+ console.log('SELFTEST ' + JSON.stringify({ ok: false, error: String(error?.message ?? error) }));
107
+ child?.kill();
108
+ app.exit(1);
109
+ }
84
110
  }
85
111
 
86
112
  const menuTemplate = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincentbanana/banana-gui",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Banana 的图形界面:本地服务 + 浏览器。黑白主题,自绘图标,无第三方 UI 依赖",
5
5
  "type": "module",
6
6
  "engines": {
@@ -65,13 +65,13 @@
65
65
  }
66
66
  },
67
67
  "optionalDependencies": {
68
- "electron": "^33.0.0"
68
+ "electron": "^33.4.11"
69
69
  },
70
70
  "devDependencies": {
71
71
  "electron-builder": "^25.0.0"
72
72
  },
73
73
  "dependencies": {
74
- "@vincentbanana/bananacli": "^0.3.0"
74
+ "@vincentbanana/bananacli": "^0.4.0"
75
75
  },
76
76
  "license": "MIT",
77
77
  "publishConfig": {
package/ui/app.js CHANGED
@@ -130,6 +130,11 @@ function relativeTime(iso) {
130
130
  return `${Math.round(hours / 24)} 天前`;
131
131
  }
132
132
 
133
+ /** 窄屏下侧边栏是覆盖层,选完会话必须收起来,否则挡住对话。 */
134
+ function closeSidebarIfOverlay() {
135
+ if (window.matchMedia('(max-width: 900px)').matches) $('sidebar').classList.remove('open');
136
+ }
137
+
133
138
  function renderSidebar() {
134
139
  const list = $('sessionList');
135
140
  list.innerHTML = '';
@@ -147,7 +152,10 @@ function renderSidebar() {
147
152
 
148
153
  node.addEventListener('click', (event) => {
149
154
  if (event.target.closest('[data-del], [data-menu]')) return;
150
- post('session', { action: 'switch', ref: item.id }).then(() => refresh(true));
155
+ post('session', { action: 'switch', ref: item.id }).then(() => {
156
+ refresh(true);
157
+ closeSidebarIfOverlay();
158
+ });
151
159
  });
152
160
 
153
161
  node.querySelector('[data-del]').addEventListener('click', (event) => {
@@ -201,6 +209,7 @@ function openSessionMenu(item, anchor) {
201
209
  if (action === 'switch') {
202
210
  await post('session', { action: 'switch', ref: item.id });
203
211
  await refresh(true);
212
+ closeSidebarIfOverlay();
204
213
  } else if (action === 'rename') {
205
214
  const title = prompt('新的标题', item.title || '');
206
215
  if (title && title.trim()) {
@@ -1210,6 +1219,14 @@ function bind() {
1210
1219
  }
1211
1220
  });
1212
1221
 
1222
+ // 窄屏下点空白处收起侧边栏(点侧边栏内部或开合按钮不算)
1223
+ document.addEventListener('click', (event) => {
1224
+ if (!window.matchMedia('(max-width: 900px)').matches) return;
1225
+ if (!$('sidebar').classList.contains('open')) return;
1226
+ if (event.target.closest('#sidebar, #toggleSidebar')) return;
1227
+ $('sidebar').classList.remove('open');
1228
+ });
1229
+
1213
1230
  $('input').addEventListener('paste', (event) => {
1214
1231
  for (const item of event.clipboardData?.files ?? []) attach(item);
1215
1232
  });