@bolloon/bolloon-agent 0.1.42 → 0.2.1

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.
@@ -0,0 +1,77 @@
1
+ /**
2
+ * 简单 logger: 写日志到 <userData>/logs/bolloon.log, size-based 轮转
3
+ * (5MB 上限, 保留 .1 .2 .3 共 3 个备份 → ~20MB 上限)
4
+ *
5
+ * 同步写是因为 Electron 主进程日志量小, 同步 append 简单可靠, 不丢日志.
6
+ * 量大的会话级 jsonl 走自己路径 (lifecycle-hooks 等), 跟本 logger 无关.
7
+ */
8
+ import * as fs from 'fs';
9
+ import { mainLogPath, logsDir } from './paths';
10
+ const MAX_BYTES = 5 * 1024 * 1024;
11
+ const MAX_BACKUPS = 3; // 保留 bolloon.log.1 .2 .3
12
+ let currentSize = 0;
13
+ let initialized = false;
14
+ function ensureLogFile() {
15
+ if (initialized)
16
+ return;
17
+ initialized = true;
18
+ try {
19
+ fs.mkdirSync(logsDir(), { recursive: true });
20
+ try {
21
+ currentSize = fs.statSync(mainLogPath()).size;
22
+ }
23
+ catch {
24
+ currentSize = 0;
25
+ }
26
+ }
27
+ catch (err) {
28
+ // 失败也无所谓, log() 内部会 swallow
29
+ currentSize = 0;
30
+ console.error('[logger] ensureLogFile failed:', err);
31
+ }
32
+ }
33
+ function rotate() {
34
+ const logPath = mainLogPath();
35
+ for (let i = MAX_BACKUPS; i >= 1; i--) {
36
+ const src = `${logPath}.${i}`;
37
+ const dst = `${logPath}.${i + 1}`;
38
+ if (i === MAX_BACKUPS) {
39
+ try {
40
+ fs.unlinkSync(src);
41
+ }
42
+ catch { /* missing ok */ }
43
+ }
44
+ else {
45
+ try {
46
+ fs.renameSync(src, dst);
47
+ }
48
+ catch { /* missing ok */ }
49
+ }
50
+ }
51
+ try {
52
+ fs.renameSync(logPath, `${logPath}.1`);
53
+ }
54
+ catch { /* missing ok */ }
55
+ currentSize = 0;
56
+ }
57
+ export function log(message, level = 'info') {
58
+ ensureLogFile();
59
+ const ts = new Date().toISOString();
60
+ const line = `[${ts}] [${level}] ${message}\n`;
61
+ if (currentSize + line.length > MAX_BYTES)
62
+ rotate();
63
+ try {
64
+ fs.appendFileSync(mainLogPath(), line);
65
+ currentSize += line.length;
66
+ }
67
+ catch (err) {
68
+ // 写不进就 console, 不要再递归 log
69
+ console.error('[logger] appendFileSync failed:', err);
70
+ }
71
+ if (level === 'error')
72
+ console.error(line.trimEnd());
73
+ else if (level === 'warn')
74
+ console.warn(line.trimEnd());
75
+ else
76
+ console.log(line.trimEnd());
77
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Electron 主进程 lifecycle 入口
3
+ * 单实例锁 → whenReady → createMainWindow → cleanup on quit
4
+ * 启动时: 装 menu / tray / IPC handlers / 首启引导
5
+ */
6
+ import { app, BrowserWindow } from 'electron';
7
+ import { log } from './logger';
8
+ import { createMainWindow, focusMainWindow, getMainWindow } from './window';
9
+ import { killWebServer } from './server';
10
+ import { installAppMenu } from './menu';
11
+ import { createTray, destroyTray } from './tray';
12
+ import { registerCoreIpc } from './ipc';
13
+ import { registerDialogIpc } from './dialogs';
14
+ import { registerFirstRunIpc, maybeShowFirstRun } from './first-run';
15
+ log('Bolloon Electron 启动');
16
+ // 单实例锁
17
+ const gotTheLock = app.requestSingleInstanceLock();
18
+ if (!gotTheLock) {
19
+ log('已有实例在运行,退出');
20
+ app.quit();
21
+ }
22
+ app.on('second-instance', () => {
23
+ focusMainWindow();
24
+ });
25
+ app.whenReady().then(async () => {
26
+ log('Electron 准备好');
27
+ registerCoreIpc();
28
+ registerDialogIpc();
29
+ registerFirstRunIpc();
30
+ installAppMenu(getMainWindow);
31
+ createTray(getMainWindow);
32
+ await createMainWindow();
33
+ // 首启引导 (在主窗口就绪后弹, 不阻塞 UI)
34
+ const win = getMainWindow();
35
+ if (win) {
36
+ win.webContents.once('did-finish-load', () => {
37
+ void maybeShowFirstRun(win);
38
+ });
39
+ }
40
+ app.on('activate', async () => {
41
+ // macOS: dock 图标被点击且没有窗口时, 重开
42
+ if (BrowserWindow.getAllWindows().length === 0) {
43
+ await createMainWindow();
44
+ }
45
+ });
46
+ });
47
+ app.on('window-all-closed', () => {
48
+ log('所有窗口已关闭');
49
+ destroyTray();
50
+ killWebServer();
51
+ if (process.platform !== 'darwin') {
52
+ app.quit();
53
+ }
54
+ });
55
+ app.on('before-quit', () => {
56
+ log('应用即将退出');
57
+ destroyTray();
58
+ killWebServer();
59
+ });
60
+ log('主进程初始化完成');
@@ -0,0 +1,140 @@
1
+ /**
2
+ * 应用菜单 — macOS 标准 + File/Edit/View/Window/Help 自定义项
3
+ * "Open Data Folder" 等会跨平台显示 (mac 也会出现, 不会冲突)
4
+ */
5
+ import { app, Menu, shell } from 'electron';
6
+ import { dataDir, logsDir } from './paths';
7
+ import { log } from './logger';
8
+ function openDataFolder() {
9
+ shell.openPath(dataDir()).catch((e) => log(`open data folder 失败: ${e.message}`, 'error'));
10
+ }
11
+ function openLogsFolder() {
12
+ shell.openPath(logsDir()).catch((e) => log(`open logs folder 失败: ${e.message}`, 'error'));
13
+ }
14
+ function openExternal(url) {
15
+ shell.openExternal(url).catch((e) => log(`openExternal 失败: ${e.message}`, 'error'));
16
+ }
17
+ export function buildAppMenu(getMainWindow) {
18
+ const isMac = process.platform === 'darwin';
19
+ const win = getMainWindow();
20
+ const appMenu = {
21
+ label: app.getName(),
22
+ submenu: [
23
+ { role: 'about' },
24
+ { type: 'separator' },
25
+ { role: 'services' },
26
+ { type: 'separator' },
27
+ { role: 'hide' },
28
+ { role: 'hideOthers' },
29
+ { role: 'unhide' },
30
+ { type: 'separator' },
31
+ { role: 'quit' },
32
+ ],
33
+ };
34
+ const fileMenu = {
35
+ label: 'File',
36
+ submenu: [
37
+ {
38
+ label: 'Open Data Folder',
39
+ accelerator: 'CmdOrCtrl+Shift+D',
40
+ click: () => openDataFolder(),
41
+ },
42
+ {
43
+ label: 'Open Logs Folder',
44
+ accelerator: 'CmdOrCtrl+Shift+L',
45
+ click: () => openLogsFolder(),
46
+ },
47
+ { type: 'separator' },
48
+ isMac ? { role: 'close' } : { role: 'quit' },
49
+ ],
50
+ };
51
+ const editMenu = {
52
+ label: 'Edit',
53
+ submenu: [
54
+ { role: 'undo' },
55
+ { role: 'redo' },
56
+ { type: 'separator' },
57
+ { role: 'cut' },
58
+ { role: 'copy' },
59
+ { role: 'paste' },
60
+ { role: 'selectAll' },
61
+ ],
62
+ };
63
+ const viewMenu = {
64
+ label: 'View',
65
+ submenu: [
66
+ {
67
+ label: 'Reload',
68
+ accelerator: 'CmdOrCtrl+R',
69
+ click: () => win?.webContents.reload(),
70
+ },
71
+ {
72
+ label: 'Force Reload',
73
+ accelerator: 'CmdOrCtrl+Shift+R',
74
+ click: () => win?.webContents.reloadIgnoringCache(),
75
+ },
76
+ {
77
+ label: 'Toggle DevTools',
78
+ accelerator: isMac ? 'Alt+Cmd+I' : 'Ctrl+Shift+I',
79
+ click: () => win?.webContents.toggleDevTools(),
80
+ },
81
+ { type: 'separator' },
82
+ { role: 'resetZoom' },
83
+ { role: 'zoomIn' },
84
+ { role: 'zoomOut' },
85
+ { type: 'separator' },
86
+ { role: 'togglefullscreen' },
87
+ ],
88
+ };
89
+ const windowMenu = {
90
+ label: 'Window',
91
+ submenu: [
92
+ { role: 'minimize' },
93
+ { role: 'zoom' },
94
+ ...(isMac
95
+ ? [{ type: 'separator' }, { role: 'front' }]
96
+ : [{ role: 'close' }]),
97
+ ],
98
+ };
99
+ const helpMenu = {
100
+ role: 'help',
101
+ submenu: [
102
+ {
103
+ label: 'GitHub Repository',
104
+ click: () => openExternal('https://github.com/bolloon/bolloon'),
105
+ },
106
+ {
107
+ label: 'Report Issue',
108
+ click: () => openExternal('https://github.com/bolloon/bolloon/issues'),
109
+ },
110
+ { type: 'separator' },
111
+ {
112
+ label: 'About Bolloon Agent',
113
+ click: () => {
114
+ // 简单 about — 用 dialog 暂代 (避免再造 HTML 窗口)
115
+ // 真实 about panel 在 macOS 是 app.role 触发的, 我们走菜单项即可
116
+ const { dialog } = require('electron');
117
+ dialog.showMessageBox({
118
+ type: 'info',
119
+ title: 'About Bolloon Agent',
120
+ message: `Bolloon Agent v${app.getVersion()}`,
121
+ detail: `P2P AI Document Agent\nData: ${dataDir()}\nLogs: ${logsDir()}`,
122
+ buttons: ['OK'],
123
+ }).catch(() => { });
124
+ },
125
+ },
126
+ ],
127
+ };
128
+ const template = [
129
+ ...(isMac ? [appMenu] : []),
130
+ fileMenu,
131
+ editMenu,
132
+ viewMenu,
133
+ windowMenu,
134
+ helpMenu,
135
+ ];
136
+ return Menu.buildFromTemplate(template);
137
+ }
138
+ export function installAppMenu(getMainWindow) {
139
+ Menu.setApplicationMenu(buildAppMenu(getMainWindow));
140
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * 路径常量 — 单一来源, 避免散落各处
3
+ *
4
+ * dataDir: 用户数据 (LLM config / P2P secret / 频道 JSON 等) — 故意保持在 ~/.bolloon/
5
+ * 与 CLI 共用, 卸载 app 不删数据. 不要改这个路径, 不要做 userData 重定向.
6
+ * userDataDir: Electron 的 app.getPath('userData'), 用来放日志 / 临时 / 锁文件
7
+ * logsDir: userData 下的 logs/ 子目录
8
+ */
9
+ import { app } from 'electron';
10
+ import * as os from 'os';
11
+ import * as path from 'path';
12
+ export function dataDir() {
13
+ return path.join(os.homedir(), '.bolloon');
14
+ }
15
+ export function userDataDir() {
16
+ return app.getPath('userData');
17
+ }
18
+ export function logsDir() {
19
+ return path.join(userDataDir(), 'logs');
20
+ }
21
+ export function mainLogPath() {
22
+ return path.join(logsDir(), 'bolloon.log');
23
+ }
24
+ export function firstRunFlagPath() {
25
+ return path.join(userDataDir(), '.bolloon-first-run-seen');
26
+ }
27
+ /** 资源目录: packaged 下是 process.resourcesPath/build, dev 下是 repo/build/ */
28
+ export function buildResourcesDir() {
29
+ if (app.isPackaged) {
30
+ return path.join(process.resourcesPath, 'build');
31
+ }
32
+ return path.join(app.getAppPath(), 'build');
33
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Spawns dist/web/server.js as a child Node process (via Electron's
3
+ * ELECTRON_RUN_AS_NODE=1) and parses BOLLOON_PORT=NNNN to learn the
4
+ * actually-bound port (which may have drifted from the preferred port
5
+ * due to EADDRINUSE auto-bump).
6
+ */
7
+ import { spawn } from 'child_process';
8
+ import * as path from 'path';
9
+ import { log } from './logger';
10
+ import { WEB_SERVER_STARTUP_TIMEOUT_MS, DEFAULT_HOST } from './config';
11
+ let webServerProcess = null;
12
+ export function getWebServerProcess() {
13
+ return webServerProcess;
14
+ }
15
+ export function killWebServer() {
16
+ if (webServerProcess && !webServerProcess.killed) {
17
+ log('终止 Web 服务器进程');
18
+ webServerProcess.kill();
19
+ }
20
+ webServerProcess = null;
21
+ }
22
+ export function startWebServer(preferredPort) {
23
+ return new Promise((resolve, reject) => {
24
+ const serverScript = path.join(__dirname, '..', 'web', 'server.js');
25
+ log(`启动 Web 服务器进程: ${serverScript}`);
26
+ // ELECTRON_RUN_AS_NODE=1 — packaged 时 process.execPath 是 Electron 二进制,
27
+ // 没这行 require() 会失败. Dev 模式 (electron dist/electron.js) 同样情况.
28
+ webServerProcess = spawn(process.execPath, [serverScript], {
29
+ env: {
30
+ ...process.env,
31
+ ELECTRON_RUN_AS_NODE: '1',
32
+ NODE_ENV: 'production',
33
+ PORT: String(preferredPort),
34
+ ELECTRON_PORT: String(preferredPort),
35
+ BOLLOON_HOST: DEFAULT_HOST,
36
+ },
37
+ stdio: ['ignore', 'pipe', 'pipe'],
38
+ });
39
+ let stdoutBuf = '';
40
+ let stderrData = '';
41
+ webServerProcess.stdout?.on('data', (data) => {
42
+ stdoutBuf += data.toString();
43
+ process.stdout.write(`[WebServer] ${data}`);
44
+ const m = stdoutBuf.match(/^BOLLOON_PORT=(\d+)/m);
45
+ if (m) {
46
+ const actualPort = parseInt(m[1], 10);
47
+ log(`Web 服务器就绪: 实际端口=${actualPort} (preferred=${preferredPort})`);
48
+ resolve({ port: actualPort });
49
+ }
50
+ });
51
+ webServerProcess.stderr?.on('data', (data) => {
52
+ stderrData += data.toString();
53
+ process.stderr.write(`[WebServer ERR] ${data}`);
54
+ });
55
+ webServerProcess.on('error', (err) => {
56
+ log(`Web 服务器启动失败: ${err.message}`, 'error');
57
+ reject(err);
58
+ });
59
+ webServerProcess.on('exit', (code, signal) => {
60
+ log(`Web 服务器退出: code=${code} signal=${signal}`);
61
+ if (code !== 0 && code !== null) {
62
+ log(`Web 服务器 stderr: ${stderrData}`, 'error');
63
+ }
64
+ webServerProcess = null;
65
+ });
66
+ setTimeout(() => {
67
+ if (webServerProcess && !webServerProcess.killed) {
68
+ log(`Web 服务器启动超时 (${WEB_SERVER_STARTUP_TIMEOUT_MS}ms)`, 'error');
69
+ reject(new Error('Web server startup timeout'));
70
+ }
71
+ }, WEB_SERVER_STARTUP_TIMEOUT_MS);
72
+ });
73
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * 系统托盘 — Show / Hide / Quit
3
+ *
4
+ * 图标资源要求 (需用户自备):
5
+ * build/trayTemplate.png (mac, 16x16 + 32x32 @2x, 单色 alpha, 自动暗/亮)
6
+ * build/tray.png (win/linux, 16x16 + 32x32)
7
+ *
8
+ * 若 build 目录没有 tray 图标, 降级为不创建托盘 (主菜单和窗口关闭行为不变)
9
+ */
10
+ import { Tray, Menu, nativeImage, app } from 'electron';
11
+ import * as path from 'path';
12
+ import * as fs from 'fs';
13
+ import { buildResourcesDir } from './paths';
14
+ import { log } from './logger';
15
+ let trayInstance = null;
16
+ function loadTrayIcon() {
17
+ const isMac = process.platform === 'darwin';
18
+ const file = isMac ? 'trayTemplate.png' : 'tray.png';
19
+ const iconPath = path.join(buildResourcesDir(), file);
20
+ if (!fs.existsSync(iconPath)) {
21
+ log(`托盘图标缺失: ${iconPath} (跳过托盘创建)`, 'warn');
22
+ return null;
23
+ }
24
+ const img = nativeImage.createFromPath(iconPath);
25
+ if (isMac)
26
+ img.setTemplateImage(true);
27
+ return img;
28
+ }
29
+ export function createTray(getMainWindow) {
30
+ if (trayInstance)
31
+ return;
32
+ const icon = loadTrayIcon();
33
+ if (!icon)
34
+ return;
35
+ trayInstance = new Tray(icon);
36
+ trayInstance.setToolTip(`Bolloon Agent v${app.getVersion()}`);
37
+ const toggle = () => {
38
+ const w = getMainWindow();
39
+ if (!w)
40
+ return;
41
+ if (w.isVisible() && w.isFocused()) {
42
+ w.hide();
43
+ }
44
+ else {
45
+ if (w.isMinimized())
46
+ w.restore();
47
+ w.show();
48
+ w.focus();
49
+ }
50
+ };
51
+ const menu = Menu.buildFromTemplate([
52
+ { label: 'Show / Hide', click: () => toggle() },
53
+ { type: 'separator' },
54
+ {
55
+ label: 'Quit',
56
+ click: () => {
57
+ log('托盘菜单 Quit');
58
+ app.quit();
59
+ },
60
+ },
61
+ ]);
62
+ trayInstance.setContextMenu(menu);
63
+ // mac 默认 click 弹菜单, win/linux 默认 click 触发自定义
64
+ if (process.platform !== 'darwin') {
65
+ trayInstance.on('click', () => toggle());
66
+ }
67
+ }
68
+ export function destroyTray() {
69
+ if (trayInstance) {
70
+ trayInstance.destroy();
71
+ trayInstance = null;
72
+ }
73
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * 主窗口工厂 — preload 路径解析 + dev/prod loadURL + 外部链接走系统浏览器
3
+ */
4
+ import { BrowserWindow, shell } from 'electron';
5
+ import * as path from 'path';
6
+ import { log } from './logger';
7
+ import { isDev, MAIN_WINDOW_DEFAULT, MAIN_WINDOW_MIN, preferredPort } from './config';
8
+ import { startWebServer } from './server';
9
+ let mainWindow = null;
10
+ export function getMainWindow() {
11
+ return mainWindow;
12
+ }
13
+ export async function createMainWindow() {
14
+ log('创建主窗口...');
15
+ mainWindow = new BrowserWindow({
16
+ width: MAIN_WINDOW_DEFAULT.width,
17
+ height: MAIN_WINDOW_DEFAULT.height,
18
+ minWidth: MAIN_WINDOW_MIN.width,
19
+ minHeight: MAIN_WINDOW_MIN.height,
20
+ title: 'Bolloon Agent',
21
+ webPreferences: {
22
+ nodeIntegration: false,
23
+ contextIsolation: true,
24
+ sandbox: false, // MCP/spawn 等需要 fs; 真正的隔离靠 preload 边界
25
+ preload: path.join(__dirname, '..', 'electron-preload.js'),
26
+ },
27
+ show: false,
28
+ });
29
+ if (isDev) {
30
+ // dev:web 用 tsx 起 server, 端口固定 preferredPort, 不会 EADDRINUSE 自增
31
+ const port = preferredPort();
32
+ mainWindow.loadURL(`http://localhost:${port}`);
33
+ mainWindow.webContents.openDevTools();
34
+ }
35
+ else {
36
+ try {
37
+ log('启动内置 Web 服务器...');
38
+ const { port: actualPort } = await startWebServer(preferredPort());
39
+ mainWindow.loadURL(`http://localhost:${actualPort}`);
40
+ }
41
+ catch (err) {
42
+ log(`启动服务器失败: ${err.message}`, 'error');
43
+ console.error('启动服务器失败:', err);
44
+ }
45
+ }
46
+ mainWindow.once('ready-to-show', () => {
47
+ mainWindow?.show();
48
+ log('窗口已显示');
49
+ });
50
+ // 外部链接走系统浏览器, 不在 app 内开新窗口
51
+ mainWindow.webContents.setWindowOpenHandler(({ url }) => {
52
+ if (url.startsWith('http://') || url.startsWith('https://')) {
53
+ shell.openExternal(url);
54
+ }
55
+ return { action: 'deny' };
56
+ });
57
+ mainWindow.on('closed', () => {
58
+ mainWindow = null;
59
+ });
60
+ log('窗口创建完成');
61
+ }
62
+ export function focusMainWindow() {
63
+ if (!mainWindow)
64
+ return;
65
+ if (mainWindow.isMinimized())
66
+ mainWindow.restore();
67
+ mainWindow.show();
68
+ mainWindow.focus();
69
+ }
package/dist/index.js CHANGED
@@ -1928,9 +1928,12 @@ async function main() {
1928
1928
  }
1929
1929
  const { createWebServer, openBrowser } = await import('./web/server.js');
1930
1930
  s.info(`启动 Web 服务端口 ${port}...`);
1931
- const { port: actualPort } = await createWebServer(port, { selfImprove });
1932
- s.success(`浏览器已打开 http://localhost:${actualPort}`);
1933
- openBrowser(`http://localhost:${actualPort}`);
1931
+ // 2026-06-24: CLI 默认 loopback bind (安全), LAN 访问需 BOLLOON_HOST=0.0.0.0
1932
+ const bindHost = process.env.BOLLOON_HOST;
1933
+ const { port: actualPort } = await createWebServer(port, { selfImprove, ...(bindHost ? { host: bindHost } : {}) });
1934
+ const displayHost = bindHost ?? '127.0.0.1';
1935
+ s.success(`浏览器已打开 → http://${displayHost}:${actualPort}`);
1936
+ openBrowser(`http://${displayHost}:${actualPort}`);
1934
1937
  }
1935
1938
  else if (isNonInteractive) {
1936
1939
  console.log = originalLog;
@@ -32,6 +32,10 @@ const TOOL_WHITELIST = new Set([
32
32
  'vitest_run', 'tsc_check',
33
33
  // 2026-06-19: Agent Mesh 通信工具 (跟 pi-sdk.ts registerTools 同步)
34
34
  'check_inbox', 'send_to_peer', 'p2p_broadcast', 'send_to_local_agent', 'list_local_agents', 'agent_call',
35
+ // 2026-06-24: Wallet + Polymarket + Safe 工具 (跟 pi-sdk.ts _registerWalletTools 同步)
36
+ 'wallet_create', 'wallet_import', 'wallet_get_balance', 'wallet_sign_message', 'wallet_send_tx', 'wallet_transfer_token', 'wallet_autopay',
37
+ 'polymarket_list_markets', 'polymarket_get_market', 'polymarket_get_orders', 'polymarket_create_order', 'polymarket_cancel_order',
38
+ 'safe_deploy',
35
39
  // MCP 注册的工具
36
40
  'mcp_tool',
37
41
  ]);