@bolloon/bolloon-agent 0.3.22 → 0.3.23
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/agents/deny-pipeline.js +116 -0
- package/dist/agents/parse-tool-call.js +26 -4
- package/dist/agents/pi-sdk.js +247 -50
- package/dist/agents/session-store.js +87 -2
- package/dist/bootstrap/snip-collapse.js +135 -0
- package/dist/cli/loading-tui.js +64 -10
- package/dist/electron/config.js +14 -9
- package/dist/electron/dialogs.js +53 -16
- package/dist/electron/first-run.js +65 -24
- package/dist/electron/ipc.js +14 -10
- package/dist/electron/logger.js +44 -7
- package/dist/electron/main.js +45 -42
- package/dist/electron/menu.js +18 -13
- package/dist/electron/paths.js +54 -12
- package/dist/electron/server.js +57 -18
- package/dist/electron/tray.js +53 -15
- package/dist/electron/window.js +61 -22
- package/dist/electron-preload.js +19 -16
- package/dist/electron.js +4 -1
- package/dist/external-engines/delegate.js +19 -0
- package/dist/hooks/hooks-engine.js +329 -0
- package/dist/index.js +41 -23
- package/dist/llm/pi-ai.js +5 -17
- package/dist/security/tool-gate.js +8 -1
- package/dist/social/dunbar-tier.js +409 -0
- package/dist/utils/auto-update.js +51 -12
- package/dist/web/client.js +4833 -4328
- package/dist/web/components/p2p/index.js +234 -276
- package/dist/web/server.js +17 -3
- package/dist/web/style.css +2 -2
- package/dist/web/ui/message-renderer.js +396 -535
- package/dist/web/ui/step-timeline.js +273 -372
- package/package.json +24 -24
- package/dist/web/components/p2p/P2PModal.js +0 -188
- package/dist/web/components/p2p/p2p-modal.js +0 -664
- package/dist/web/components/p2p/p2p-tools.js +0 -248
package/dist/electron/window.js
CHANGED
|
@@ -1,25 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.getMainWindow = getMainWindow;
|
|
37
|
+
exports.createMainWindow = createMainWindow;
|
|
38
|
+
exports.focusMainWindow = focusMainWindow;
|
|
1
39
|
/**
|
|
2
40
|
* 主窗口工厂 — preload 路径解析 + dev/prod loadURL + 外部链接走系统浏览器
|
|
3
41
|
*/
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
42
|
+
const electron_1 = require("electron");
|
|
43
|
+
const path = __importStar(require("path"));
|
|
44
|
+
const logger_1 = require("./logger");
|
|
45
|
+
const config_1 = require("./config");
|
|
46
|
+
const server_1 = require("./server");
|
|
9
47
|
const g = globalThis;
|
|
10
48
|
const __basedir = g.__dirname
|
|
11
49
|
?? (g.__filename ? path.dirname(g.__filename) : process.cwd());
|
|
12
50
|
let mainWindow = null;
|
|
13
|
-
|
|
51
|
+
function getMainWindow() {
|
|
14
52
|
return mainWindow;
|
|
15
53
|
}
|
|
16
|
-
|
|
17
|
-
log('创建主窗口...');
|
|
18
|
-
mainWindow = new BrowserWindow({
|
|
19
|
-
width: MAIN_WINDOW_DEFAULT.width,
|
|
20
|
-
height: MAIN_WINDOW_DEFAULT.height,
|
|
21
|
-
minWidth: MAIN_WINDOW_MIN.width,
|
|
22
|
-
minHeight: MAIN_WINDOW_MIN.height,
|
|
54
|
+
async function createMainWindow() {
|
|
55
|
+
(0, logger_1.log)('创建主窗口...');
|
|
56
|
+
mainWindow = new electron_1.BrowserWindow({
|
|
57
|
+
width: config_1.MAIN_WINDOW_DEFAULT.width,
|
|
58
|
+
height: config_1.MAIN_WINDOW_DEFAULT.height,
|
|
59
|
+
minWidth: config_1.MAIN_WINDOW_MIN.width,
|
|
60
|
+
minHeight: config_1.MAIN_WINDOW_MIN.height,
|
|
23
61
|
title: 'Bolloon Agent',
|
|
24
62
|
webPreferences: {
|
|
25
63
|
nodeIntegration: false,
|
|
@@ -29,40 +67,40 @@ export async function createMainWindow() {
|
|
|
29
67
|
},
|
|
30
68
|
show: false,
|
|
31
69
|
});
|
|
32
|
-
if (isDev) {
|
|
70
|
+
if (config_1.isDev) {
|
|
33
71
|
// dev:web 用 tsx 起 server, 端口固定 preferredPort, 不会 EADDRINUSE 自增
|
|
34
|
-
const port = preferredPort();
|
|
72
|
+
const port = (0, config_1.preferredPort)();
|
|
35
73
|
mainWindow.loadURL(`http://localhost:${port}`);
|
|
36
74
|
mainWindow.webContents.openDevTools();
|
|
37
75
|
}
|
|
38
76
|
else {
|
|
39
77
|
try {
|
|
40
|
-
log('启动内置 Web 服务器...');
|
|
41
|
-
const { port: actualPort } = await startWebServer(preferredPort());
|
|
78
|
+
(0, logger_1.log)('启动内置 Web 服务器...');
|
|
79
|
+
const { port: actualPort } = await (0, server_1.startWebServer)((0, config_1.preferredPort)());
|
|
42
80
|
mainWindow.loadURL(`http://localhost:${actualPort}`);
|
|
43
81
|
}
|
|
44
82
|
catch (err) {
|
|
45
|
-
log(`启动服务器失败: ${err.message}`, 'error');
|
|
83
|
+
(0, logger_1.log)(`启动服务器失败: ${err.message}`, 'error');
|
|
46
84
|
console.error('启动服务器失败:', err);
|
|
47
85
|
}
|
|
48
86
|
}
|
|
49
87
|
mainWindow.once('ready-to-show', () => {
|
|
50
88
|
mainWindow?.show();
|
|
51
|
-
log('窗口已显示');
|
|
89
|
+
(0, logger_1.log)('窗口已显示');
|
|
52
90
|
});
|
|
53
91
|
// 外部链接走系统浏览器, 不在 app 内开新窗口
|
|
54
92
|
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
|
55
93
|
if (url.startsWith('http://') || url.startsWith('https://')) {
|
|
56
|
-
shell.openExternal(url);
|
|
94
|
+
electron_1.shell.openExternal(url);
|
|
57
95
|
}
|
|
58
96
|
return { action: 'deny' };
|
|
59
97
|
});
|
|
60
98
|
mainWindow.on('closed', () => {
|
|
61
99
|
mainWindow = null;
|
|
62
100
|
});
|
|
63
|
-
log('窗口创建完成');
|
|
101
|
+
(0, logger_1.log)('窗口创建完成');
|
|
64
102
|
}
|
|
65
|
-
|
|
103
|
+
function focusMainWindow() {
|
|
66
104
|
if (!mainWindow)
|
|
67
105
|
return;
|
|
68
106
|
if (mainWindow.isMinimized())
|
|
@@ -70,3 +108,4 @@ export function focusMainWindow() {
|
|
|
70
108
|
mainWindow.show();
|
|
71
109
|
mainWindow.focus();
|
|
72
110
|
}
|
|
111
|
+
//# sourceMappingURL=window.js.map
|
package/dist/electron-preload.js
CHANGED
|
@@ -1,29 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
1
3
|
/**
|
|
2
4
|
* Electron Preload 脚本
|
|
3
5
|
* 在渲染进程和主进程之间建立安全的通信桥梁
|
|
4
6
|
* contextIsolation: true, nodeIntegration: false — 只能通过这里暴露的 API 触达主进程
|
|
5
7
|
*/
|
|
6
|
-
|
|
7
|
-
contextBridge.exposeInMainWorld('electronAPI', {
|
|
8
|
+
const electron_1 = require("electron");
|
|
9
|
+
electron_1.contextBridge.exposeInMainWorld('electronAPI', {
|
|
8
10
|
// === 原有 (保留) ===
|
|
9
|
-
getVersion: () => ipcRenderer.invoke('get-version'),
|
|
10
|
-
getUserDataPath: () => ipcRenderer.invoke('get-user-data-path'),
|
|
11
|
-
openExternal: (url) => ipcRenderer.invoke('open-external', url),
|
|
11
|
+
getVersion: () => electron_1.ipcRenderer.invoke('get-version'),
|
|
12
|
+
getUserDataPath: () => electron_1.ipcRenderer.invoke('get-user-data-path'),
|
|
13
|
+
openExternal: (url) => electron_1.ipcRenderer.invoke('open-external', url),
|
|
12
14
|
// === 新增: 数据目录 ===
|
|
13
|
-
getDataPath: () => ipcRenderer.invoke('get-data-path'),
|
|
15
|
+
getDataPath: () => electron_1.ipcRenderer.invoke('get-data-path'),
|
|
14
16
|
// === 新增: 文件 dialog ===
|
|
15
|
-
openFile: (opts) => ipcRenderer.invoke('dialog:open-file', opts),
|
|
16
|
-
saveFile: (opts) => ipcRenderer.invoke('dialog:save-file', opts),
|
|
17
|
-
openDirectory: (opts) => ipcRenderer.invoke('dialog:open-directory', opts),
|
|
17
|
+
openFile: (opts) => electron_1.ipcRenderer.invoke('dialog:open-file', opts),
|
|
18
|
+
saveFile: (opts) => electron_1.ipcRenderer.invoke('dialog:save-file', opts),
|
|
19
|
+
openDirectory: (opts) => electron_1.ipcRenderer.invoke('dialog:open-directory', opts),
|
|
18
20
|
// === 新增: 文件系统 (限大小, 主进程守卫) ===
|
|
19
|
-
readTextFile: (opts) => ipcRenderer.invoke('fs:read-text-file', opts),
|
|
20
|
-
writeTextFile: (opts) => ipcRenderer.invoke('fs:write-text-file', opts),
|
|
21
|
-
pathExists: (opts) => ipcRenderer.invoke('fs:path-exists', opts),
|
|
21
|
+
readTextFile: (opts) => electron_1.ipcRenderer.invoke('fs:read-text-file', opts),
|
|
22
|
+
writeTextFile: (opts) => electron_1.ipcRenderer.invoke('fs:write-text-file', opts),
|
|
23
|
+
pathExists: (opts) => electron_1.ipcRenderer.invoke('fs:path-exists', opts),
|
|
22
24
|
// === 新增: 首启引导 ===
|
|
23
|
-
getFirstRunSeen: () => ipcRenderer.invoke('first-run:seen'),
|
|
24
|
-
markFirstRunSeen: () => ipcRenderer.invoke('first-run:mark-seen'),
|
|
25
|
-
getDataPathSync: () => ipcRenderer.invoke('first-run:data-dir'),
|
|
26
|
-
getLogsPathSync: () => ipcRenderer.invoke('first-run:logs-dir'),
|
|
25
|
+
getFirstRunSeen: () => electron_1.ipcRenderer.invoke('first-run:seen'),
|
|
26
|
+
markFirstRunSeen: () => electron_1.ipcRenderer.invoke('first-run:mark-seen'),
|
|
27
|
+
getDataPathSync: () => electron_1.ipcRenderer.invoke('first-run:data-dir'),
|
|
28
|
+
getLogsPathSync: () => electron_1.ipcRenderer.invoke('first-run:logs-dir'),
|
|
27
29
|
// === 元数据 ===
|
|
28
30
|
platform: process.platform,
|
|
29
31
|
});
|
|
32
|
+
//# sourceMappingURL=electron-preload.js.map
|
package/dist/electron.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
1
3
|
/**
|
|
2
4
|
* Electron 入口 shim — 真正逻辑在 src/electron/main.ts
|
|
3
5
|
* (保留 src/electron.ts 平铺入口, 不动 package.json 的 dist/electron.js 解析)
|
|
4
6
|
*/
|
|
5
|
-
|
|
7
|
+
require("./electron/main");
|
|
8
|
+
//# sourceMappingURL=electron.js.map
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
* - experiment 引擎是 API 供应商不是 CLI, 不支持委派 (提示改用 import)
|
|
11
11
|
*/
|
|
12
12
|
import { spawn } from 'child_process';
|
|
13
|
+
import * as fs from 'fs/promises';
|
|
14
|
+
import * as path from 'path';
|
|
15
|
+
import * as os from 'os';
|
|
13
16
|
import { discoverEngines, buildDelegateArgs } from './discovery.js';
|
|
14
17
|
function delegateTimeoutMs() {
|
|
15
18
|
const env = Number(process.env.BOLLOON_ENGINE_DELEGATE_TIMEOUT_MS);
|
|
@@ -133,6 +136,22 @@ export async function delegateToEngine(id, prompt, opts = {}) {
|
|
|
133
136
|
catch { /* noop */ }
|
|
134
137
|
killTree();
|
|
135
138
|
const combined = (stdout + (stderr ? `\n[stderr]\n${stderr}` : '')).trim();
|
|
139
|
+
// 2026-07-29: Sidechain transcript — 保存委派完整记录
|
|
140
|
+
try {
|
|
141
|
+
const sidechainDir = path.join(os.homedir(), '.bolloon', 'sidechains');
|
|
142
|
+
fs.mkdir(sidechainDir, { recursive: true });
|
|
143
|
+
const ts = Date.now();
|
|
144
|
+
const filePath = path.join(sidechainDir, `${ts}-${trimmedId}.jsonl`);
|
|
145
|
+
const entry = JSON.stringify({
|
|
146
|
+
ts, engineId: trimmedId, prompt: trimmedPrompt,
|
|
147
|
+
stdout: stdout.slice(0, 100_000),
|
|
148
|
+
stderr: stderr.slice(0, 10_000),
|
|
149
|
+
exitCode: code, duration: Date.now() - ts, model: opts.model || null,
|
|
150
|
+
}) + '\n';
|
|
151
|
+
// fire-and-forget, 不阻塞主流程
|
|
152
|
+
fs.appendFile(filePath, entry, 'utf-8').catch(() => { });
|
|
153
|
+
}
|
|
154
|
+
catch { /* sidechain 写入失败静默 */ }
|
|
136
155
|
if (code === 0) {
|
|
137
156
|
resolve({ success: true, output: combined || '(无输出)', exitCode: code });
|
|
138
157
|
}
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hooks-engine.ts — Phase 4: Hook 机制 (2026-07-29)
|
|
3
|
+
*
|
|
4
|
+
* Claude Code 式 hook 系统: 在 agent 循环的关键点触发外部回调.
|
|
5
|
+
*
|
|
6
|
+
* 设计决策:
|
|
7
|
+
* - 事件驱动 (EventEmitter): 注册 → 触发 → 执行回调
|
|
8
|
+
* - 2 种执行模式: shell (同步/异步 command) + llm (LLM 评估)
|
|
9
|
+
* - 回调可以拒绝/修改/记录/中断流程
|
|
10
|
+
* - 配置从 ~/.bolloon/hooks.yaml 加载
|
|
11
|
+
* - Hook 是零 context 成本 — 不在 prompt 内, 不消耗 token
|
|
12
|
+
*/
|
|
13
|
+
import * as fs from 'fs/promises';
|
|
14
|
+
import * as path from 'path';
|
|
15
|
+
import * as os from 'os';
|
|
16
|
+
import { spawn } from 'child_process';
|
|
17
|
+
import { getMinimax } from '../constraints/index.js';
|
|
18
|
+
// ============== 类型 ==============
|
|
19
|
+
/** 支持的 hook 事件类型 */
|
|
20
|
+
export const HOOK_EVENT_TYPES = [
|
|
21
|
+
'preToolUse', // 工具调用前 — 可返回 {deny, reason} 拒绝
|
|
22
|
+
'postToolUse', // 工具调用后 — 可注入系统提示
|
|
23
|
+
'onMessage', // 用户/助手消息后
|
|
24
|
+
'onSessionStart', // 会话开始
|
|
25
|
+
'onSessionEnd', // 会话结束
|
|
26
|
+
'onLoopStart', // ReAct 循环开始
|
|
27
|
+
'onLoopEnd', // ReAct 循环结束
|
|
28
|
+
'onError', // 发生 API/工具错误
|
|
29
|
+
];
|
|
30
|
+
// ============== Hook 引擎 ==============
|
|
31
|
+
export class HooksEngine {
|
|
32
|
+
hooks = new Map();
|
|
33
|
+
configPath;
|
|
34
|
+
constructor(home) {
|
|
35
|
+
this.configPath = path.join(home || os.homedir(), '.bolloon', 'hooks.yaml');
|
|
36
|
+
}
|
|
37
|
+
/** 注册一个 hook */
|
|
38
|
+
register(config) {
|
|
39
|
+
if (!config.id)
|
|
40
|
+
throw new Error('Hook id 必填');
|
|
41
|
+
if (!HOOK_EVENT_TYPES.includes(config.event)) {
|
|
42
|
+
throw new Error(`未知 hook 事件: ${config.event}, 可用: ${HOOK_EVENT_TYPES.join(', ')}`);
|
|
43
|
+
}
|
|
44
|
+
this.hooks.set(config.id, { ...config, enabled: config.enabled ?? true });
|
|
45
|
+
}
|
|
46
|
+
/** 批量注册 */
|
|
47
|
+
registerMany(configs) {
|
|
48
|
+
for (const c of configs)
|
|
49
|
+
this.register(c);
|
|
50
|
+
}
|
|
51
|
+
/** 注销 hook */
|
|
52
|
+
unregister(id) {
|
|
53
|
+
return this.hooks.delete(id);
|
|
54
|
+
}
|
|
55
|
+
/** 获取指定事件的所有启用的 hook */
|
|
56
|
+
getHooks(event) {
|
|
57
|
+
return Array.from(this.hooks.values()).filter(h => h.enabled !== false && h.event === event);
|
|
58
|
+
}
|
|
59
|
+
/** 获取所有 hook */
|
|
60
|
+
listAll() {
|
|
61
|
+
return Array.from(this.hooks.values());
|
|
62
|
+
}
|
|
63
|
+
/** 启用/禁用 */
|
|
64
|
+
setEnabled(id, enabled) {
|
|
65
|
+
const h = this.hooks.get(id);
|
|
66
|
+
if (!h)
|
|
67
|
+
return false;
|
|
68
|
+
h.enabled = enabled;
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* 触发一个事件, 执行所有匹配的 hook.
|
|
73
|
+
* 返回 HookResult 数组.
|
|
74
|
+
*
|
|
75
|
+
* preToolUse 的 deny 结果: 如果任意 hook 返回 deny=true, 该工具被拒绝.
|
|
76
|
+
*/
|
|
77
|
+
async fire(event, ctx) {
|
|
78
|
+
const matched = this.getHooks(event);
|
|
79
|
+
if (matched.length === 0)
|
|
80
|
+
return [];
|
|
81
|
+
const results = [];
|
|
82
|
+
for (const hook of matched) {
|
|
83
|
+
// preToolUse 工具过滤
|
|
84
|
+
if (event === 'preToolUse' && hook.toolFilter && ctx.toolName) {
|
|
85
|
+
if (!hook.toolFilter.includes(ctx.toolName))
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
const result = await this.executeHook(hook, ctx);
|
|
90
|
+
results.push(result);
|
|
91
|
+
}
|
|
92
|
+
catch (e) {
|
|
93
|
+
results.push({
|
|
94
|
+
id: hook.id,
|
|
95
|
+
error: String(e),
|
|
96
|
+
elapsedMs: 0,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return results;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* 检查 preToolUse hook 是否拒绝当前工具.
|
|
104
|
+
* 返回第一个 deny 的 HookResult, 或 null.
|
|
105
|
+
*/
|
|
106
|
+
async checkToolUse(toolName, args) {
|
|
107
|
+
const results = await this.fire('preToolUse', {
|
|
108
|
+
event: 'preToolUse',
|
|
109
|
+
toolName,
|
|
110
|
+
toolArgs: args,
|
|
111
|
+
});
|
|
112
|
+
for (const r of results) {
|
|
113
|
+
if (r.deny)
|
|
114
|
+
return r;
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* 执行单个 hook.
|
|
120
|
+
*/
|
|
121
|
+
async executeHook(hook, ctx) {
|
|
122
|
+
const start = Date.now();
|
|
123
|
+
const timeoutMs = hook.timeoutMs ?? 5000;
|
|
124
|
+
if (hook.mode === 'shell' && hook.command) {
|
|
125
|
+
return this.execShell(hook, ctx, start, timeoutMs);
|
|
126
|
+
}
|
|
127
|
+
if (hook.mode === 'llm' && hook.prompt) {
|
|
128
|
+
return this.execLlm(hook, ctx, start, timeoutMs);
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
id: hook.id,
|
|
132
|
+
error: `不支持的执行模式: ${hook.mode}`,
|
|
133
|
+
elapsedMs: Date.now() - start,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* shell 模式: spawn 子进程执行命令.
|
|
138
|
+
* 环境变量 HOOK_EVENT / HOOK_TOOL / HOOK_ARGS / HOOK_RESULT 传递上下文.
|
|
139
|
+
*/
|
|
140
|
+
async execShell(hook, ctx, start, timeoutMs) {
|
|
141
|
+
return new Promise((resolve) => {
|
|
142
|
+
const env = {
|
|
143
|
+
...process.env,
|
|
144
|
+
HOOK_EVENT: ctx.event,
|
|
145
|
+
HOOK_TOOL: ctx.toolName || '',
|
|
146
|
+
HOOK_ARGS: ctx.toolArgs ? JSON.stringify(ctx.toolArgs) : '',
|
|
147
|
+
HOOK_RESULT: ctx.toolResult ? JSON.stringify(ctx.toolResult).slice(0, 2000) : '',
|
|
148
|
+
HOOK_MESSAGE: ctx.messageContent || '',
|
|
149
|
+
HOOK_CHANNEL: ctx.channelId || '',
|
|
150
|
+
HOOK_SESSION: ctx.sessionId || '',
|
|
151
|
+
HOOK_ERROR: ctx.error || '',
|
|
152
|
+
};
|
|
153
|
+
const proc = spawn('sh', ['-c', hook.command], {
|
|
154
|
+
env,
|
|
155
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
156
|
+
timeout: timeoutMs,
|
|
157
|
+
});
|
|
158
|
+
let stdout = '';
|
|
159
|
+
let stderr = '';
|
|
160
|
+
proc.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
161
|
+
proc.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
162
|
+
const timer = setTimeout(() => {
|
|
163
|
+
proc.kill('SIGTERM');
|
|
164
|
+
resolve({
|
|
165
|
+
id: hook.id,
|
|
166
|
+
rawOutput: stdout.slice(0, 1000),
|
|
167
|
+
timedOut: true,
|
|
168
|
+
elapsedMs: Date.now() - start,
|
|
169
|
+
});
|
|
170
|
+
}, timeoutMs);
|
|
171
|
+
proc.on('close', (code) => {
|
|
172
|
+
clearTimeout(timer);
|
|
173
|
+
const elapsed = Date.now() - start;
|
|
174
|
+
// 解析 stdout: 如果是 JSON, 提取 deny/系统注入
|
|
175
|
+
let deny = false;
|
|
176
|
+
let reason = '';
|
|
177
|
+
let systemAddition = '';
|
|
178
|
+
try {
|
|
179
|
+
const parsed = JSON.parse(stdout.trim());
|
|
180
|
+
if (parsed.deny) {
|
|
181
|
+
deny = true;
|
|
182
|
+
reason = parsed.reason || 'Hook 拒绝';
|
|
183
|
+
}
|
|
184
|
+
if (parsed.systemAddition) {
|
|
185
|
+
systemAddition = parsed.systemAddition;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
// 不是 JSON, 当普通输出
|
|
190
|
+
}
|
|
191
|
+
resolve({
|
|
192
|
+
id: hook.id,
|
|
193
|
+
deny,
|
|
194
|
+
reason,
|
|
195
|
+
systemAddition,
|
|
196
|
+
rawOutput: stdout.slice(0, 1000),
|
|
197
|
+
elapsedMs: elapsed,
|
|
198
|
+
error: code !== 0 ? `exit ${code}: ${stderr.slice(0, 200)}` : undefined,
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
proc.on('error', (err) => {
|
|
202
|
+
clearTimeout(timer);
|
|
203
|
+
resolve({
|
|
204
|
+
id: hook.id,
|
|
205
|
+
error: String(err),
|
|
206
|
+
elapsedMs: Date.now() - start,
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* LLM 模式: 用当前 LLM 评估 hook prompt.
|
|
213
|
+
* 将上下文注入 prompt, 获取 LLM 的判断.
|
|
214
|
+
*/
|
|
215
|
+
async execLlm(hook, ctx, start, timeoutMs) {
|
|
216
|
+
try {
|
|
217
|
+
const minimax = getMinimax();
|
|
218
|
+
if (!minimax) {
|
|
219
|
+
return { id: hook.id, error: 'LLM 不可用 (minimax 未初始化)', elapsedMs: Date.now() - start };
|
|
220
|
+
}
|
|
221
|
+
// 构建评估 prompt
|
|
222
|
+
const contextStr = [
|
|
223
|
+
`事件: ${ctx.event}`,
|
|
224
|
+
ctx.toolName ? `工具: ${ctx.toolName}` : '',
|
|
225
|
+
ctx.toolArgs ? `参数: ${JSON.stringify(ctx.toolArgs).slice(0, 500)}` : '',
|
|
226
|
+
ctx.toolResult ? `结果: ${JSON.stringify(ctx.toolResult).slice(0, 500)}` : '',
|
|
227
|
+
ctx.messageContent ? `消息: ${ctx.messageContent.slice(0, 500)}` : '',
|
|
228
|
+
ctx.error ? `错误: ${ctx.error}` : '',
|
|
229
|
+
].filter(Boolean).join('\n');
|
|
230
|
+
const fullPrompt = `【Hook 评估】\n${contextStr}\n\n【规则】\n${hook.prompt}\n\n请以 JSON 格式回答, 包含字段: deny (boolean), reason (string), systemAddition (string, 可选).`;
|
|
231
|
+
const response = await minimax.chat(fullPrompt, '', undefined);
|
|
232
|
+
const reply = (response.reply || '').trim();
|
|
233
|
+
let parsed;
|
|
234
|
+
try {
|
|
235
|
+
// 尝试提取 JSON
|
|
236
|
+
const jsonMatch = reply.match(/\{[\s\S]*\}/);
|
|
237
|
+
parsed = jsonMatch ? JSON.parse(jsonMatch[0]) : { deny: false };
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
parsed = { deny: false };
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
id: hook.id,
|
|
244
|
+
deny: parsed.deny === true,
|
|
245
|
+
reason: parsed.reason || '',
|
|
246
|
+
systemAddition: parsed.systemAddition || '',
|
|
247
|
+
rawOutput: reply.slice(0, 1000),
|
|
248
|
+
elapsedMs: Date.now() - start,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
catch (e) {
|
|
252
|
+
return {
|
|
253
|
+
id: hook.id,
|
|
254
|
+
error: String(e),
|
|
255
|
+
elapsedMs: Date.now() - start,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* 从 ~/.bolloon/hooks.yaml 加载配置.
|
|
261
|
+
* 失败静默 (没有 hook 配置也正常).
|
|
262
|
+
*/
|
|
263
|
+
async loadFromConfig() {
|
|
264
|
+
try {
|
|
265
|
+
const raw = await fs.readFile(this.configPath, 'utf-8');
|
|
266
|
+
// 简单解析 YAML-like: id/event/mode/command/prompt/toolFilter
|
|
267
|
+
const lines = raw.split('\n');
|
|
268
|
+
let current = {};
|
|
269
|
+
for (const line of lines) {
|
|
270
|
+
const trimmed = line.trim();
|
|
271
|
+
if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('---'))
|
|
272
|
+
continue;
|
|
273
|
+
// hook 分隔: 空行或 ---
|
|
274
|
+
if (trimmed === '---' || (trimmed.length === 0 && current.id)) {
|
|
275
|
+
if (current.id && current.event) {
|
|
276
|
+
this.register(current);
|
|
277
|
+
}
|
|
278
|
+
current = {};
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
const sep = trimmed.indexOf(':');
|
|
282
|
+
if (sep < 0)
|
|
283
|
+
continue;
|
|
284
|
+
const key = trimmed.slice(0, sep).trim();
|
|
285
|
+
const val = trimmed.slice(sep + 1).trim();
|
|
286
|
+
switch (key) {
|
|
287
|
+
case 'id':
|
|
288
|
+
current.id = val;
|
|
289
|
+
break;
|
|
290
|
+
case 'event':
|
|
291
|
+
current.event = val;
|
|
292
|
+
break;
|
|
293
|
+
case 'mode':
|
|
294
|
+
current.mode = val;
|
|
295
|
+
break;
|
|
296
|
+
case 'command':
|
|
297
|
+
current.command = val;
|
|
298
|
+
break;
|
|
299
|
+
case 'prompt':
|
|
300
|
+
current.prompt = val;
|
|
301
|
+
break;
|
|
302
|
+
case 'timeout_ms':
|
|
303
|
+
current.timeoutMs = parseInt(val, 10) || 5000;
|
|
304
|
+
break;
|
|
305
|
+
case 'enabled':
|
|
306
|
+
current.enabled = val === 'true';
|
|
307
|
+
break;
|
|
308
|
+
case 'description':
|
|
309
|
+
current.description = val;
|
|
310
|
+
break;
|
|
311
|
+
case 'tool_filter':
|
|
312
|
+
current.toolFilter = val.split(',').map(s => s.trim()).filter(Boolean);
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
// 最后一条
|
|
317
|
+
if (current.id && current.event) {
|
|
318
|
+
this.register(current);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
// 文件不存在 / 无法解析 — 静默
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
/** 获取默认配置路径 */
|
|
326
|
+
getConfigPath() {
|
|
327
|
+
return this.configPath;
|
|
328
|
+
}
|
|
329
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -15,7 +15,7 @@ import { createSubAgentManager } from './agents/subagent-manager.js';
|
|
|
15
15
|
import { getGlobalSharedContext } from './social/global-shared-context.js';
|
|
16
16
|
import { createBollharnessIntegration } from './bollharness-integration/index.js';
|
|
17
17
|
import * as readline from 'readline';
|
|
18
|
-
import { printBanner, renderDialog, renderUserMessage, renderAgentMessage,
|
|
18
|
+
import { printBanner, renderDialog, renderUserMessage, renderAgentMessage, renderToolCallListItem, renderToolCallBody, renderToolCallsFooter, termWidth, brandArtLines, boxTop, boxRow, boxBottom, dispWidth } from './cli/loading-tui.js';
|
|
19
19
|
// 启动自动检查更新:后台、节流、检测到新版本自动安装(可被 --no-update / BOLLOON_SKIP_UPDATE 关闭)
|
|
20
20
|
import { createRequire } from 'module';
|
|
21
21
|
const _require = createRequire(import.meta.url);
|
|
@@ -90,15 +90,21 @@ const s = {
|
|
|
90
90
|
console.log();
|
|
91
91
|
},
|
|
92
92
|
Thinking: () => {
|
|
93
|
-
const frames = ['
|
|
93
|
+
const frames = ['(`・ω・´)', '(´・_・`)', '(。•́︿•̀。)', 'ᕙ(▀̿̿Ĺ̯̿̿▀̿ ̿)ᕗ', '(◕‿◕)'];
|
|
94
94
|
let i = 0;
|
|
95
|
+
let dots = 0;
|
|
96
|
+
const frame = frames[0];
|
|
97
|
+
process.stdout.write(` ${frame} 思考...`);
|
|
95
98
|
return setInterval(() => {
|
|
96
|
-
|
|
97
|
-
|
|
99
|
+
i = (i + 1) % frames.length;
|
|
100
|
+
dots = (dots + 1) % 4;
|
|
101
|
+
const dotStr = '.'.repeat(dots || 1);
|
|
102
|
+
process.stdout.write(`\r ${frames[i]} 思考${dotStr} `);
|
|
103
|
+
}, 600);
|
|
98
104
|
},
|
|
99
105
|
clearThinking: (interval) => {
|
|
100
106
|
clearInterval(interval);
|
|
101
|
-
process.stdout.write('\r' + ' '.repeat(
|
|
107
|
+
process.stdout.write('\r' + ' '.repeat(40) + '\r');
|
|
102
108
|
},
|
|
103
109
|
dialog: async (title, promptText) => {
|
|
104
110
|
return new Promise((resolve) => {
|
|
@@ -423,6 +429,8 @@ async function replReadline(comm) {
|
|
|
423
429
|
const prefix = queueMode ? `${C_WARN}▸${RST}` : `${C_ACCENT}❯${RST}`;
|
|
424
430
|
const raw = await new Promise(resolve => rl.question(`\n${sepLine}\n${statusBarLine()}\n${sepLine}\n${prefix} `, resolve));
|
|
425
431
|
const trimmed = raw.trim();
|
|
432
|
+
// 清除 readline echo 行, 避免与 renderUserMessage 重复
|
|
433
|
+
process.stdout.write('\r\x1b[K');
|
|
426
434
|
process.stdout.write(`\n${sepLine}\n\n\n\n\n`);
|
|
427
435
|
if (!trimmed)
|
|
428
436
|
continue;
|
|
@@ -556,41 +564,51 @@ async function processInput(input, comm) {
|
|
|
556
564
|
try {
|
|
557
565
|
// 双横线分割
|
|
558
566
|
process.stdout.write(`${C_DIM}${'─'.repeat(8)} · ${'─'.repeat(8)}${RESET}\n`);
|
|
559
|
-
// 已发送消息框
|
|
560
567
|
process.stdout.write(renderUserMessage(trimmed) + '\n');
|
|
561
568
|
const a = await getAgent();
|
|
562
569
|
const boxW = Math.min(termWidth() - 2, 76);
|
|
563
|
-
const
|
|
564
|
-
let
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
570
|
+
const toolCalls = [];
|
|
571
|
+
let toolCounter = 0;
|
|
572
|
+
// 启动 thinking 加载动画 (颜文字), 第一个工具事件或 prompt 完成后停止
|
|
573
|
+
let thinkingInterval = null;
|
|
574
|
+
const stopThinking = () => {
|
|
575
|
+
if (thinkingInterval) {
|
|
576
|
+
s.clearThinking(thinkingInterval);
|
|
577
|
+
thinkingInterval = null;
|
|
569
578
|
}
|
|
570
579
|
};
|
|
580
|
+
thinkingInterval = s.Thinking();
|
|
571
581
|
const onStream = (e) => {
|
|
572
582
|
if (e.type === 'step_start') {
|
|
573
|
-
|
|
583
|
+
toolCounter++;
|
|
584
|
+
toolCalls.push({ tool: e.tool, args: e.args, _t: Date.now() });
|
|
574
585
|
}
|
|
575
586
|
else if (e.type === 'step_done' || e.type === 'step_error') {
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
if (!firstEvent)
|
|
580
|
-
process.stdout.write(flowConnector(boxW) + '\n');
|
|
581
|
-
process.stdout.write(renderToolCall({
|
|
587
|
+
stopThinking();
|
|
588
|
+
const p = toolCalls.shift();
|
|
589
|
+
const doneItem = {
|
|
582
590
|
tool: e.tool ?? p?.tool ?? '?',
|
|
583
591
|
args: p?.args,
|
|
584
592
|
status: e.type === 'step_done' ? 'ok' : 'error',
|
|
585
593
|
output: e.output,
|
|
586
594
|
error: e.error,
|
|
587
|
-
durationMs: p ? Date.now() - p.
|
|
588
|
-
|
|
589
|
-
|
|
595
|
+
durationMs: p ? Date.now() - p._t : undefined,
|
|
596
|
+
};
|
|
597
|
+
process.stdout.write(renderToolCallListItem(doneItem, toolCalls.length + 1, toolCounter) + '\n');
|
|
598
|
+
const bodyText = e.type === 'step_done' ? e.output : e.error;
|
|
599
|
+
if (bodyText && bodyText.length > 0) {
|
|
600
|
+
const bodyRendered = renderToolCallBody(doneItem, boxW);
|
|
601
|
+
if (bodyRendered)
|
|
602
|
+
process.stdout.write(bodyRendered + '\n');
|
|
603
|
+
}
|
|
604
|
+
// 全部完成时打印 footer
|
|
605
|
+
if (toolCalls.length === 0 && toolCounter > 0) {
|
|
606
|
+
process.stdout.write(renderToolCallsFooter(toolCounter) + '\n');
|
|
607
|
+
}
|
|
590
608
|
}
|
|
591
609
|
};
|
|
592
610
|
const response = await a.prompt(trimmed, { onStream });
|
|
593
|
-
|
|
611
|
+
stopThinking();
|
|
594
612
|
// 智能体回复框 (圆角)
|
|
595
613
|
process.stdout.write(renderAgentMessage(response) + '\n');
|
|
596
614
|
// 更新底部状态栏: 上下文进度
|