@bolloon/bolloon-agent 0.3.3 → 0.3.5
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/pi-sdk-session-manager.js +1 -1
- package/dist/agents/pi-sdk.js +59 -11
- package/dist/bootstrap/remote-mirror.js +37 -4
- package/dist/cli/interface.js +10 -5
- package/dist/cli-entry.js +12 -2
- package/dist/index.js +18 -3
- package/dist/llm/config-store.js +5 -2
- package/dist/llm/pi-ai.js +21 -18
- package/dist/llm/tool-manifest/index.js +59 -0
- package/dist/web/client.js +22 -2
- package/dist/web/server-v3-p2p.js +2 -0
- package/dist/web/server.js +65 -3
- package/dist/web/ui/message-renderer.js +21 -1
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ import * as fs from 'fs/promises';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { getGlobalSharedContext } from '../social/global-shared-context.js';
|
|
4
4
|
import { Session, saveSession, loadSession } from '@bolloon/constraint-runtime';
|
|
5
|
-
|
|
5
|
+
import { SHARED_SESSION_PATH } from '../web/server-types.js';
|
|
6
6
|
const PERSONA_PATH = path.join(process.env.HOME || '/tmp', '.bolloon', 'persona.json');
|
|
7
7
|
/**
|
|
8
8
|
* PiSessionManager — 负责:
|
package/dist/agents/pi-sdk.js
CHANGED
|
@@ -842,11 +842,24 @@ ${this.getToolDefinitions()}
|
|
|
842
842
|
// 真正折叠需要把 pi-sdk 历史灌回 pivot 的 history 数组 — 侵入较大.
|
|
843
843
|
// 当前 priority: 临时传空实现, 让 budget 公式放够 (workflow-pivot-loop.ts line 220)
|
|
844
844
|
// 不再撞预算. 这条路径留作技术债.
|
|
845
|
+
// 2026-07-17 Bug 1 修: 注入 messageHistory (hydrateMessageHistory 从 session JSON 回灌的) 到 system prompt
|
|
846
|
+
// pivot loop execute() 内部自己维护 messageHistory, 跟 pi-sdk 的 this.messageHistory 隔离,
|
|
847
|
+
// 不注入的话 LLM 看不到历史对话, 每次都是新对话.
|
|
848
|
+
const historyLines = [];
|
|
849
|
+
const historyToInject = this.messageHistory.slice(-20, -1);
|
|
850
|
+
for (const m of historyToInject) {
|
|
851
|
+
const roleLabel = m.role === 'user' ? '用户' : m.role === 'assistant' ? '你' : m.role === 'tool' ? '工具结果' : m.role;
|
|
852
|
+
const text = (m.content || '').slice(0, 2000);
|
|
853
|
+
if (text)
|
|
854
|
+
historyLines.push(`[${roleLabel}]: ${text}`);
|
|
855
|
+
}
|
|
856
|
+
const historyBlock = historyLines.length > 0
|
|
857
|
+
? `\n\n【历史对话 (最近 ${historyLines.length} 条)】\n${historyLines.join('\n')}\n【历史对话结束】`
|
|
858
|
+
: '';
|
|
845
859
|
const onCompact = async () => {
|
|
846
860
|
// no-op (best-effort hook for future pi-sdk/pivot history sync)
|
|
847
861
|
};
|
|
848
|
-
const result = await loop.execute(input, llm, systemPrompt, this.currentOnStream ?? undefined, this.currentSignal ?? undefined, onCompact);
|
|
849
|
-
this.messageHistory.push({ role: 'user', content: input });
|
|
862
|
+
const result = await loop.execute(input, llm, systemPrompt + historyBlock, this.currentOnStream ?? undefined, this.currentSignal ?? undefined, onCompact);
|
|
850
863
|
if (result.response) {
|
|
851
864
|
this.messageHistory.push({ role: 'assistant', content: result.response });
|
|
852
865
|
}
|
|
@@ -1000,7 +1013,9 @@ ${toolDefs}
|
|
|
1000
1013
|
// 2. reactive compaction (prompt 估算超阈值, 跑压缩)
|
|
1001
1014
|
// 3. prompt-too-long (LLM 报错 4xxx token 错误, 跑 reactive compaction 再试 1 次)
|
|
1002
1015
|
// 失败静默: 全部重试失败 → 空 reply (上层用 no tool_use 终止)
|
|
1003
|
-
|
|
1016
|
+
// Bug 5: pass tool IDs for native OpenAI tool calling
|
|
1017
|
+
const toolIds = Array.from(this.tools.keys());
|
|
1018
|
+
const response = await this.callLlmWithRecovery(llm, messages, systemPrompt, signal, onStream, toolIds);
|
|
1004
1019
|
const reply = (response.reply || '').trim();
|
|
1005
1020
|
// 2026-06-30: OpenAI 协议 native tool_calls (LLM 真产了 tool_call 时, minimax/M3 会返回 id)
|
|
1006
1021
|
const nativeToolCalls = response.toolCalls;
|
|
@@ -1067,11 +1082,39 @@ ${toolDefs}
|
|
|
1067
1082
|
// 2026-06-19 架构 fix: parseToolCall 优先于 isFinalResponse
|
|
1068
1083
|
// 之前: 思考块里的 "<final gen>" 触发 isFinalResponse 提前 break, 工具从未真正执行
|
|
1069
1084
|
// 现在: 先尝试解析 tool_call, 有就执行; 没有才检查是不是真正的 final gen
|
|
1070
|
-
|
|
1085
|
+
// Bug 5 (2026-07-17): 优先用 LLM 的 native tool_calls (response.toolCalls), 再回退到文本解析
|
|
1086
|
+
// deepseek-v4-flash 用 OpenAI 协议 tools 时, 会真返回结构化 tool_calls 数组
|
|
1087
|
+
// 之前 nativeToolCalls 被读了不用, 只查 reply 文本, 导致 LLM 明明选了工具但代码找不到
|
|
1088
|
+
let toolCall = null;
|
|
1089
|
+
if (nativeToolCalls && nativeToolCalls.length > 0) {
|
|
1090
|
+
const nc = nativeToolCalls[0];
|
|
1091
|
+
// OpenAI 协议: { id, type: 'function', function: { name, arguments: JSON string } }
|
|
1092
|
+
// 转换成 internal { name, args, id }
|
|
1093
|
+
try {
|
|
1094
|
+
const args = typeof nc.function?.arguments === 'string'
|
|
1095
|
+
? JSON.parse(nc.function.arguments)
|
|
1096
|
+
: (nc.function?.arguments || {});
|
|
1097
|
+
toolCall = {
|
|
1098
|
+
name: nc.function?.name,
|
|
1099
|
+
args,
|
|
1100
|
+
id: nc.id || `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
1101
|
+
};
|
|
1102
|
+
console.log(`[PiAgent] 用 native tool_call: ${toolCall.name} (id=${toolCall.id})`);
|
|
1103
|
+
}
|
|
1104
|
+
catch (err) {
|
|
1105
|
+
console.warn(`[PiAgent] 解析 native tool_call 失败, 回退到文本解析: ${err.message?.slice(0, 100)}`);
|
|
1106
|
+
toolCall = null;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
if (!toolCall) {
|
|
1110
|
+
toolCall = this.parseToolCall(reply);
|
|
1111
|
+
}
|
|
1071
1112
|
// 2026-06-30 修: 给 toolCall 分配稳定 id, 让后续 tool result 能引用同一个 id
|
|
1072
1113
|
// OpenAI 协议要求 messages 里 tool result 必须有对应的 tool_call_id, 否则 400
|
|
1073
|
-
if (toolCall) {
|
|
1114
|
+
if (toolCall && !toolCall.id) {
|
|
1074
1115
|
toolCall.id = `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
1116
|
+
}
|
|
1117
|
+
if (toolCall) {
|
|
1075
1118
|
this.messageHistory.push({
|
|
1076
1119
|
role: 'assistant',
|
|
1077
1120
|
content: reply,
|
|
@@ -1526,10 +1569,14 @@ Workspace root folder: ${this.cwd}
|
|
|
1526
1569
|
// bolloon 之前把所有 tool result 包成 "[工具结果] ..." 当 user/assistant role 发, minimax 严格校验失败
|
|
1527
1570
|
// 现在: 保留 role='tool' + 加 tool_call_id 字段 (用 messageHistory 里自己生成的 id)
|
|
1528
1571
|
if (role === 'tool') {
|
|
1529
|
-
const
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1572
|
+
const toolCallId = m.toolCallId || m.toolCall?.id || '';
|
|
1573
|
+
const result = m.toolResult;
|
|
1574
|
+
out.push({
|
|
1575
|
+
role: 'tool',
|
|
1576
|
+
content: result ? (typeof result === 'string' ? result : JSON.stringify(result)) : content,
|
|
1577
|
+
tool_call_id: toolCallId,
|
|
1578
|
+
name: m.toolCall?.name || '',
|
|
1579
|
+
});
|
|
1533
1580
|
continue;
|
|
1534
1581
|
}
|
|
1535
1582
|
// system role (router hint 等) 直接保留
|
|
@@ -1593,7 +1640,7 @@ Workspace root folder: ${this.cwd}
|
|
|
1593
1640
|
*
|
|
1594
1641
|
* 失败静默: 全部失败 → 返回空 reply, 让上层 no-tool_use 终止
|
|
1595
1642
|
*/
|
|
1596
|
-
async callLlmWithRecovery(llm, contextOrMessages, systemPrompt, signal, onStream) {
|
|
1643
|
+
async callLlmWithRecovery(llm, contextOrMessages, systemPrompt, signal, onStream, tools) {
|
|
1597
1644
|
// Reactive compaction 预检: 估算 token 超 80% 阈值, 跑一次
|
|
1598
1645
|
const estimated = this.estimateHistoryTokens();
|
|
1599
1646
|
if (estimated > this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD * 0.8) {
|
|
@@ -1646,7 +1693,8 @@ Workspace root folder: ${this.cwd}
|
|
|
1646
1693
|
try {
|
|
1647
1694
|
// M3.5 (2026-06-17): 传 messages 数组 (如果 contextOrMessages 是数组) 或字符串
|
|
1648
1695
|
// 数组版让 LLM 看到结构化的 user/assistant/tool role, 而不是把 history 拼成单字符串
|
|
1649
|
-
|
|
1696
|
+
// Bug 5: pass tool IDs for native OpenAI tool calling
|
|
1697
|
+
const response = await llm.chat(contextOrMessages, systemPrompt, signal, tools);
|
|
1650
1698
|
// 2026-06-30: 透传 toolCalls (OpenAI 协议 native) 给上层, 让 assistant message 能 emit 真 id
|
|
1651
1699
|
return { reply: response.reply || '', toolCalls: response.toolCalls };
|
|
1652
1700
|
}
|
|
@@ -15,11 +15,43 @@
|
|
|
15
15
|
* 触发:
|
|
16
16
|
* - server.ts: agent.history.get.reply handler 写完 → mirrorRemoteHistory
|
|
17
17
|
* - client.ts: openRemoteChannelChat → loadRemoteHistory 优先读镜像
|
|
18
|
+
*
|
|
19
|
+
* 2026-07-17: 加写盘重试 → 短暂文件系统抖动不吞消息
|
|
18
20
|
*/
|
|
19
21
|
import * as fs from 'fs/promises';
|
|
20
22
|
import * as path from 'path';
|
|
21
23
|
import * as os from 'os';
|
|
22
24
|
import { saveWindow as saveSessionWindow } from './session-window.js';
|
|
25
|
+
// ============== 重试 ==============
|
|
26
|
+
const MIRROR_RETRIES = 3;
|
|
27
|
+
const MIRROR_BACKOFF_MS = 200;
|
|
28
|
+
const MAX_BACKOFF_MS = 3000;
|
|
29
|
+
const RETRYABLE_CODES = new Set([
|
|
30
|
+
'EBUSY', 'EAGAIN', 'EMFILE', 'ENFILE', 'ENOSPC', 'EIO',
|
|
31
|
+
]);
|
|
32
|
+
function isRetryable(e) {
|
|
33
|
+
if (!e || typeof e !== 'object')
|
|
34
|
+
return false;
|
|
35
|
+
const code = e.code || '';
|
|
36
|
+
return RETRYABLE_CODES.has(code);
|
|
37
|
+
}
|
|
38
|
+
async function withRetry(fn, label) {
|
|
39
|
+
for (let attempt = 0; attempt <= MIRROR_RETRIES; attempt++) {
|
|
40
|
+
try {
|
|
41
|
+
return await fn();
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
if (attempt < MIRROR_RETRIES && isRetryable(e)) {
|
|
45
|
+
const ms = Math.min(MIRROR_BACKOFF_MS * Math.pow(2, attempt), MAX_BACKOFF_MS);
|
|
46
|
+
console.warn(`[mirror] ${label} 失败 (attempt ${attempt + 1}/${MIRROR_RETRIES}), ${ms}ms 后重试: ${e?.message || e}`);
|
|
47
|
+
await new Promise(r => setTimeout(r, ms));
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
throw e;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
throw new Error(`unreachable: ${label}`);
|
|
54
|
+
}
|
|
23
55
|
// ============== 路径 ==============
|
|
24
56
|
function sanitize(s) {
|
|
25
57
|
return s.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 128);
|
|
@@ -37,14 +69,14 @@ export function getRemoteMirrorWindowPath(targetPublicKey, channelId, home) {
|
|
|
37
69
|
// ============== 写入 ==============
|
|
38
70
|
/**
|
|
39
71
|
* 把 A 端的 history 镜像到 B 端本地. atomic (单文件 writeFile, 中途崩溃顶多旧版本留下).
|
|
40
|
-
*
|
|
72
|
+
* 失败自动重试 MIRROR_RETRIES 次 (暂态文件系统错误), 最终失败静默不阻塞 RPC reply.
|
|
41
73
|
*/
|
|
42
74
|
export async function mirrorRemoteHistory(opts) {
|
|
43
75
|
try {
|
|
44
76
|
const home = opts.home || os.homedir();
|
|
45
77
|
const mirrorPath = getRemoteMirrorPath(opts.targetPublicKey, opts.channelId, home);
|
|
46
78
|
const windowPath = getRemoteMirrorWindowPath(opts.targetPublicKey, opts.channelId, home);
|
|
47
|
-
await fs.mkdir(path.dirname(mirrorPath), { recursive: true });
|
|
79
|
+
await withRetry(() => fs.mkdir(path.dirname(mirrorPath), { recursive: true }), 'mkdir');
|
|
48
80
|
// 主体镜像
|
|
49
81
|
const payload = {
|
|
50
82
|
channelId: opts.channelId,
|
|
@@ -54,12 +86,13 @@ export async function mirrorRemoteHistory(opts) {
|
|
|
54
86
|
lastUpdated: opts.lastUpdated || new Date().toISOString(),
|
|
55
87
|
mirroredAt: new Date().toISOString(),
|
|
56
88
|
};
|
|
57
|
-
await fs.writeFile(mirrorPath, JSON.stringify(payload, null, 2), 'utf-8');
|
|
89
|
+
await withRetry(() => fs.writeFile(mirrorPath, JSON.stringify(payload, null, 2), 'utf-8'), 'write mirror');
|
|
58
90
|
// 窗口联动
|
|
59
|
-
await saveSessionWindow(opts.channelId, `remote-${opts.targetPublicKey.slice(0, 12)}`, opts.messages, { home, windowSize: 30 });
|
|
91
|
+
await withRetry(() => saveSessionWindow(opts.channelId, `remote-${opts.targetPublicKey.slice(0, 12)}`, opts.messages, { home, windowSize: 30 }), 'write window');
|
|
60
92
|
return { ok: true, mirrorPath, windowPath };
|
|
61
93
|
}
|
|
62
94
|
catch (e) {
|
|
95
|
+
console.warn(`[mirror] 最终失败: ${e?.message || e}`);
|
|
63
96
|
return { ok: false, error: e?.message || String(e) };
|
|
64
97
|
}
|
|
65
98
|
}
|
package/dist/cli/interface.js
CHANGED
|
@@ -8,11 +8,14 @@ export class CLIInterface {
|
|
|
8
8
|
agent = null;
|
|
9
9
|
localNode = null;
|
|
10
10
|
rl;
|
|
11
|
-
|
|
11
|
+
// 2026-07-20 Bug 3: quiet 模式压制 console.error
|
|
12
|
+
_quiet;
|
|
13
|
+
constructor(quiet = false) {
|
|
12
14
|
this.rl = readline.createInterface({
|
|
13
15
|
input: process.stdin,
|
|
14
16
|
output: process.stdout
|
|
15
17
|
});
|
|
18
|
+
this._quiet = quiet;
|
|
16
19
|
}
|
|
17
20
|
async start() {
|
|
18
21
|
console.log('\n🤖 AI文档智能体 P2P 网络');
|
|
@@ -105,7 +108,8 @@ export class CLIInterface {
|
|
|
105
108
|
}
|
|
106
109
|
}
|
|
107
110
|
catch (e) {
|
|
108
|
-
|
|
111
|
+
if (!this._quiet)
|
|
112
|
+
console.error('错误:', e);
|
|
109
113
|
}
|
|
110
114
|
}
|
|
111
115
|
}
|
|
@@ -190,12 +194,13 @@ export class CLIInterface {
|
|
|
190
194
|
stdio: 'inherit',
|
|
191
195
|
});
|
|
192
196
|
child.on('close', (code) => {
|
|
193
|
-
if (code !== 0)
|
|
194
|
-
console.
|
|
197
|
+
if (code !== 0 && !this._quiet)
|
|
198
|
+
console.warn(`chat 子进程退出码 ${code}`);
|
|
195
199
|
resolve();
|
|
196
200
|
});
|
|
197
201
|
child.on('error', (err) => {
|
|
198
|
-
|
|
202
|
+
if (!this._quiet)
|
|
203
|
+
console.warn('chat 子进程启动失败:', err.message);
|
|
199
204
|
resolve();
|
|
200
205
|
});
|
|
201
206
|
});
|
package/dist/cli-entry.js
CHANGED
|
@@ -22,8 +22,18 @@ const CYAN = '\x1b[36m';
|
|
|
22
22
|
const YELLOW = '\x1b[33m';
|
|
23
23
|
const GREEN = '\x1b[32m';
|
|
24
24
|
const MAGENTA = '\x1b[35m';
|
|
25
|
-
// 版本信息 —
|
|
26
|
-
const VERSION =
|
|
25
|
+
// 版本信息 — 2026-07-20 Bug 3: 从 package.json 读取, 不再硬编码
|
|
26
|
+
const VERSION = (() => {
|
|
27
|
+
try {
|
|
28
|
+
const entryDir = path.dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
const pkgPath = path.resolve(entryDir, '..', 'package.json');
|
|
30
|
+
const raw = fs.readFileSync(pkgPath, 'utf-8');
|
|
31
|
+
return JSON.parse(raw).version || '0.0.0';
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return '0.0.0';
|
|
35
|
+
}
|
|
36
|
+
})();
|
|
27
37
|
function log(msg, color = RESET) {
|
|
28
38
|
console.log(`${color}${msg}${RESET}`);
|
|
29
39
|
}
|
package/dist/index.js
CHANGED
|
@@ -14,6 +14,16 @@ import { createBollharnessIntegration } from './bollharness-integration/index.js
|
|
|
14
14
|
import * as readline from 'readline';
|
|
15
15
|
import { LoadingTUI } from './cli/loading-tui.js';
|
|
16
16
|
// 启动时自动检查更新已禁用 (改用 --update-check / --update-now 显式触发)
|
|
17
|
+
import { createRequire } from 'module';
|
|
18
|
+
const _require = createRequire(import.meta.url);
|
|
19
|
+
const _BOLLOON_VERSION = (() => {
|
|
20
|
+
try {
|
|
21
|
+
return _require('../package.json').version || '0.0.0';
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return '0.0.0';
|
|
25
|
+
}
|
|
26
|
+
})();
|
|
17
27
|
const RESET = '\x1b[0m';
|
|
18
28
|
const BOLD = '\x1b[1m';
|
|
19
29
|
const DIM = '\x1b[2m';
|
|
@@ -32,11 +42,16 @@ const CLEAR_LINE = '\x1b[2K';
|
|
|
32
42
|
const HIDE_CURSOR = '\x1b[?25l';
|
|
33
43
|
const SHOW_CURSOR = '\x1b[?25h';
|
|
34
44
|
const s = {
|
|
35
|
-
banner: () =>
|
|
45
|
+
banner: () => {
|
|
46
|
+
const verStr = ` v${_BOLLOON_VERSION}`;
|
|
47
|
+
const pad = Math.max(0, 39 - 17 - verStr.length);
|
|
48
|
+
const spaces = ' '.repeat(pad);
|
|
49
|
+
console.log(`\n${CYAN}${BOLD}
|
|
36
50
|
╔═══════════════════════════════════════════╗
|
|
37
|
-
║ ${WHITE}🤖 Bolloon ${CYAN}
|
|
51
|
+
║ ${WHITE}🤖 Bolloon ${CYAN}${verStr}${spaces}║
|
|
38
52
|
║ ${WHITE}P2P AI Document Processor${CYAN} ║
|
|
39
|
-
╚═══════════════════════════════════════════╝${RESET}\n`)
|
|
53
|
+
╚═══════════════════════════════════════════╝${RESET}\n`);
|
|
54
|
+
},
|
|
40
55
|
step: (num, total, text, status) => {
|
|
41
56
|
const check = status === 'ok' ? `${GREEN}✓` :
|
|
42
57
|
status === 'loading' ? `${YELLOW}⟳` :
|
package/dist/llm/config-store.js
CHANGED
|
@@ -65,7 +65,9 @@ export const DEFAULT_PROVIDER_CONFIGS = {
|
|
|
65
65
|
enabled: false,
|
|
66
66
|
apiKey: '',
|
|
67
67
|
baseUrl: 'https://api.deepseek.com/v1',
|
|
68
|
-
|
|
68
|
+
// 2026-07-17: deepseek-chat (V3) 已不在官方 model list, 迁到 V4 系列 — deepseek-v4-flash
|
|
69
|
+
// 1M context, 支持 tool calls, 默认 thinking mode (官方 https://api-docs.deepseek.com/quick_start/pricing)
|
|
70
|
+
model: 'deepseek-v4-flash',
|
|
69
71
|
temperature: 0.7,
|
|
70
72
|
maxTokens: 4096,
|
|
71
73
|
requiresApiKey: true
|
|
@@ -134,7 +136,8 @@ export const PROVIDER_INFO = {
|
|
|
134
136
|
requiresApiKey: true,
|
|
135
137
|
models: ['MiniMax-M3', 'MiniMax-M2.7', 'MiniMax-M2', 'MiniMax-M2.1-highspeed', 'MiniMax-M2.7-highspeed']
|
|
136
138
|
},
|
|
137
|
-
|
|
139
|
+
// 2026-07-17: V3 系列 (deepseek-chat / deepseek-reasoner) 官方已下线, 改 V4
|
|
140
|
+
deepseek: { name: 'DeepSeek', description: '深度求索大模型 (V4)', requiresApiKey: true, models: ['deepseek-v4-flash', 'deepseek-v4-pro'] },
|
|
138
141
|
kimi: { name: 'Kimi (月之暗面)', description: 'Moonshot 长上下文模型', requiresApiKey: true, models: ['moonshot-v1-8k', 'moonshot-v1-32k', 'moonshot-v1-128k'] },
|
|
139
142
|
glm: { name: 'GLM (智谱)', description: '智谱 ChatGLM 系列模型', requiresApiKey: true, models: ['glm-4-flash', 'glm-4', 'glm-4-plus', 'glm-4-air', 'glm-4-airx'] },
|
|
140
143
|
qwen: { name: 'Qwen (通义千问)', description: '阿里云通义千问系列', requiresApiKey: true, models: ['qwen-plus', 'qwen-max', 'qwen-turbo', 'qwen-long'] },
|
package/dist/llm/pi-ai.js
CHANGED
|
@@ -68,7 +68,7 @@ export class PiAIModel {
|
|
|
68
68
|
* LLM 看不到 tool 调用的真实结果,导致 CLI loop 卡死.
|
|
69
69
|
* 现在 messages 数组版本保留 role 语义, LLM 能正确看到工具返回.
|
|
70
70
|
*/
|
|
71
|
-
async chat(messageOrMessages, contextOrSystemPrompt, signal) {
|
|
71
|
+
async chat(messageOrMessages, contextOrSystemPrompt, signal, tools) {
|
|
72
72
|
const systemPrompt = await this.buildSystemPromptAsync(contextOrSystemPrompt);
|
|
73
73
|
let messages;
|
|
74
74
|
if (Array.isArray(messageOrMessages)) {
|
|
@@ -93,6 +93,7 @@ export class PiAIModel {
|
|
|
93
93
|
temperature: 0.8,
|
|
94
94
|
maxTokens: 16384, // 2026-06-17: 提到 16384 — agent 注入 16K+ system prompt + 8K tool defs 时, 8K 撞上限返回空 content (见 memory: bolloon-llm-empty-large-prompt)
|
|
95
95
|
signal,
|
|
96
|
+
tools, // pass through for native tool calling
|
|
96
97
|
});
|
|
97
98
|
return { reply: response.reply, toolCalls: response.toolCalls };
|
|
98
99
|
}
|
|
@@ -166,9 +167,10 @@ export class PiAIModel {
|
|
|
166
167
|
console.warn('[pi-ai] systemPrepend 失败:', err?.message?.slice(0, 100));
|
|
167
168
|
}
|
|
168
169
|
}
|
|
170
|
+
let openaiTools;
|
|
169
171
|
if (tools && tools.length > 0) {
|
|
170
172
|
try {
|
|
171
|
-
const { getToolManifest, formatForPrompt } = await import('./tool-manifest/index.js');
|
|
173
|
+
const { getToolManifest, formatForPrompt, formatForOpenAI } = await import('./tool-manifest/index.js');
|
|
172
174
|
const manifests = tools
|
|
173
175
|
.map((id) => getToolManifest(id))
|
|
174
176
|
.filter((m) => m !== undefined);
|
|
@@ -178,6 +180,8 @@ export class PiAIModel {
|
|
|
178
180
|
{ role: 'system', content: toolPrompt },
|
|
179
181
|
...messages,
|
|
180
182
|
];
|
|
183
|
+
// Bug 3: 从 manifests 生成原生 OpenAI tools 格式
|
|
184
|
+
openaiTools = formatForOpenAI(manifests);
|
|
181
185
|
}
|
|
182
186
|
}
|
|
183
187
|
catch (err) {
|
|
@@ -192,7 +196,7 @@ export class PiAIModel {
|
|
|
192
196
|
case 'glm':
|
|
193
197
|
case 'qwen':
|
|
194
198
|
case 'mimo':
|
|
195
|
-
return this.callOpenAI(finalMessages, temperature, maxTokens, signal);
|
|
199
|
+
return this.callOpenAI(finalMessages, temperature, maxTokens, signal, openaiTools);
|
|
196
200
|
case 'anthropic':
|
|
197
201
|
return this.callAnthropic(finalMessages, temperature, maxTokens, signal);
|
|
198
202
|
case 'ollama':
|
|
@@ -259,7 +263,8 @@ export class PiAIModel {
|
|
|
259
263
|
// The 3.x line ships as `-flash` only — there is no `gemini-3.x-pro`.
|
|
260
264
|
gemini: this.config.model || 'gemini-2.5-pro',
|
|
261
265
|
minimax: this.config.model || process.env.MINIMAX_MODEL || 'MiniMax-M3',
|
|
262
|
-
|
|
266
|
+
// 2026-07-17: deepseek-chat (V3) 官方已下线, 迁 deepseek-v4-flash
|
|
267
|
+
deepseek: this.config.model || process.env.DEEPSEEK_MODEL || 'deepseek-v4-flash',
|
|
263
268
|
kimi: this.config.model || process.env.KIMI_MODEL || process.env.MOONSHOT_MODEL || 'moonshot-v1-8k',
|
|
264
269
|
glm: this.config.model || process.env.GLM_MODEL || process.env.ZHIPU_MODEL || 'glm-4-flash',
|
|
265
270
|
qwen: this.config.model || process.env.QWEN_MODEL || process.env.DASHSCOPE_MODEL || 'qwen-plus',
|
|
@@ -269,7 +274,7 @@ export class PiAIModel {
|
|
|
269
274
|
};
|
|
270
275
|
return modelMap[this.provider];
|
|
271
276
|
}
|
|
272
|
-
async callOpenAI(messages, temperature, maxTokens, signal) {
|
|
277
|
+
async callOpenAI(messages, temperature, maxTokens, signal, tools) {
|
|
273
278
|
const apiKey = this.getApiKey();
|
|
274
279
|
if (!apiKey) {
|
|
275
280
|
throw new Error('OPENAI_API_KEY not set');
|
|
@@ -280,14 +285,12 @@ export class PiAIModel {
|
|
|
280
285
|
temperature,
|
|
281
286
|
max_tokens: maxTokens
|
|
282
287
|
};
|
|
283
|
-
//
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
// 现在外层加 2 次重试 + 退避, 让 90%+ 的一次调用不出现错误
|
|
288
|
+
// Bug 3: 传入原生 tools 参数 + tool_choice auto, LLM 返回结构化 tool_calls
|
|
289
|
+
if (tools && tools.length > 0) {
|
|
290
|
+
requestBody.tools = tools;
|
|
291
|
+
requestBody.tool_choice = 'auto';
|
|
292
|
+
}
|
|
289
293
|
let lastFinishReason = '';
|
|
290
|
-
// 2026-07-06: 加分阶段 instrumentation — 让"9.8s 大头是哪段"可定位
|
|
291
294
|
const _t0 = Date.now();
|
|
292
295
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
293
296
|
const _tFetch = Date.now();
|
|
@@ -313,24 +316,23 @@ export class PiAIModel {
|
|
|
313
316
|
const content = choice?.message?.content || '';
|
|
314
317
|
const toolCalls = choice?.message?.tool_calls;
|
|
315
318
|
lastFinishReason = choice?.finish_reason || '';
|
|
316
|
-
|
|
319
|
+
// Bug 7: tool_calls 存在时不走重试 — LLM 选工具时 content 空是合法的
|
|
320
|
+
if (content || (toolCalls && toolCalls.length > 0)) {
|
|
317
321
|
if (lastFinishReason === 'length') {
|
|
318
322
|
console.warn(`[pi-ai] hit max_tokens ceiling (model=${this.mapModel()}, max_tokens=${maxTokens}) — caller should trim prompt or raise cap`);
|
|
319
323
|
}
|
|
320
|
-
// 2026-07-06: 日志打 fetch/network/parse 三段 + prompt 体积, 以后 LLM 调用慢直接看这里定位
|
|
321
324
|
const _tAfter = Date.now();
|
|
322
325
|
const promptBytes = JSON.stringify(messages).length;
|
|
323
|
-
console.log(`[pi-ai timing] total=${_tAfter - _t0}ms attempt=${attempt + 1} fetch=${_tResp - _tFetch}ms parse=${_tParse - _tResp}ms reply=${content.length}B model=${this.mapModel()} prompt=${promptBytes}B`);
|
|
326
|
+
console.log(`[pi-ai timing] total=${_tAfter - _t0}ms attempt=${attempt + 1} fetch=${_tResp - _tFetch}ms parse=${_tParse - _tResp}ms reply=${content.length}B toolCalls=${toolCalls?.length ?? 0} model=${this.mapModel()} prompt=${promptBytes}B`);
|
|
324
327
|
return { reply: content, toolCalls: toolCalls && toolCalls.length > 0 ? toolCalls : undefined };
|
|
325
328
|
}
|
|
326
|
-
// 空 content: 200 但 content="" → minimax 上游偶发, 退避后重试
|
|
327
329
|
console.warn(`[pi-ai] attempt ${attempt + 1}/3: 空 content (finish_reason=${lastFinishReason}), 退避 1.5s 重试`);
|
|
328
330
|
const _tSleep = Date.now();
|
|
329
331
|
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
330
332
|
console.log(`[pi-ai timing] attempt=${attempt + 1} empty; backoff=${Date.now() - _tSleep}ms; total=${Date.now() - _t0}ms so far`);
|
|
331
333
|
}
|
|
332
334
|
console.warn(`[pi-ai] 3 次重试都返回空 content (finish_reason=${lastFinishReason})`);
|
|
333
|
-
return { reply: '' };
|
|
335
|
+
return { reply: '' };
|
|
334
336
|
}
|
|
335
337
|
async callAnthropic(messages, temperature, maxTokens, signal) {
|
|
336
338
|
const apiKey = this.getApiKey();
|
|
@@ -600,7 +602,8 @@ function detectModel(provider) {
|
|
|
600
602
|
openrouter: 'anthropic/claude-sonnet-4.5',
|
|
601
603
|
gemini: 'gemini-2.5-pro',
|
|
602
604
|
minimax: 'MiniMax-M3',
|
|
603
|
-
|
|
605
|
+
// 2026-07-17: V3 官方下线, 迁 V4
|
|
606
|
+
deepseek: 'deepseek-v4-flash',
|
|
604
607
|
kimi: 'moonshot-v1-8k',
|
|
605
608
|
glm: 'glm-4-flash',
|
|
606
609
|
qwen: 'qwen-plus',
|
|
@@ -48,6 +48,65 @@ export function getToolsByLayer(layerId) {
|
|
|
48
48
|
*
|
|
49
49
|
* 不包含: 完整 parameters schema (那是 PiAI 客户端在调用时读)
|
|
50
50
|
*/
|
|
51
|
+
/**
|
|
52
|
+
* 把 ToolManifest 转成 OpenAI function calling 格式 (tools 数组)
|
|
53
|
+
* 用于 native tool_choice: "auto" 模式, 让 LLM 选择调用.
|
|
54
|
+
*
|
|
55
|
+
* 注意: 递归处理嵌套参数 (type='object' 的 properties / type='array' 的 items)
|
|
56
|
+
*/
|
|
57
|
+
export function formatForOpenAI(tools) {
|
|
58
|
+
const list = tools ?? ALL;
|
|
59
|
+
return list.map((t) => {
|
|
60
|
+
const properties = {};
|
|
61
|
+
const required = [];
|
|
62
|
+
for (const p of t.parameters) {
|
|
63
|
+
properties[p.name] = convertParameter(p);
|
|
64
|
+
if (p.required)
|
|
65
|
+
required.push(p.name);
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
type: 'function',
|
|
69
|
+
function: {
|
|
70
|
+
name: t.id,
|
|
71
|
+
description: t.oneLine,
|
|
72
|
+
parameters: {
|
|
73
|
+
type: 'object',
|
|
74
|
+
properties,
|
|
75
|
+
required,
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
function convertParameter(p) {
|
|
82
|
+
const schema = { type: p.type === 'enum' ? 'string' : p.type };
|
|
83
|
+
if (p.description)
|
|
84
|
+
schema.description = p.description;
|
|
85
|
+
if (p.enumValues)
|
|
86
|
+
schema.enum = p.enumValues;
|
|
87
|
+
if (p.default !== undefined)
|
|
88
|
+
schema.default = p.default;
|
|
89
|
+
if (p.minimum !== undefined)
|
|
90
|
+
schema.minimum = p.minimum;
|
|
91
|
+
if (p.maximum !== undefined)
|
|
92
|
+
schema.maximum = p.maximum;
|
|
93
|
+
if (p.format)
|
|
94
|
+
schema.format = p.format;
|
|
95
|
+
if (p.type === 'object' && p.properties) {
|
|
96
|
+
schema.properties = {};
|
|
97
|
+
for (const sub of p.properties) {
|
|
98
|
+
schema.properties[sub.name] = convertParameter(sub);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (p.type === 'array' && p.items) {
|
|
102
|
+
schema.items = convertParameter(p.items);
|
|
103
|
+
if (p.minItems !== undefined)
|
|
104
|
+
schema.minItems = p.minItems;
|
|
105
|
+
if (p.maxItems !== undefined)
|
|
106
|
+
schema.maxItems = p.maxItems;
|
|
107
|
+
}
|
|
108
|
+
return schema;
|
|
109
|
+
}
|
|
51
110
|
export function formatForPrompt(tools) {
|
|
52
111
|
const list = tools ?? ALL;
|
|
53
112
|
const lines = [
|
package/dist/web/client.js
CHANGED
|
@@ -782,6 +782,7 @@
|
|
|
782
782
|
addMessage: () => addMessage,
|
|
783
783
|
escapeHtml: () => escapeHtml,
|
|
784
784
|
finalizeTimelineAsMessage: () => finalizeTimelineAsMessage,
|
|
785
|
+
flushStepEventBuffer: () => flushStepEventBuffer,
|
|
785
786
|
getMessagesContainerForCurrent: () => getMessagesContainerForCurrent,
|
|
786
787
|
handleStepEvent: () => handleStepEvent,
|
|
787
788
|
handleStreamTokenEvent: () => handleStreamTokenEvent,
|
|
@@ -939,6 +940,7 @@
|
|
|
939
940
|
}
|
|
940
941
|
if (type === "ai" && msgContainer) {
|
|
941
942
|
mountStepTimeline(div, currentChannelId2);
|
|
943
|
+
flushStepEventBuffer(currentChannelId2, ctx);
|
|
942
944
|
}
|
|
943
945
|
div.appendChild(time);
|
|
944
946
|
if (msgContainer) {
|
|
@@ -1143,7 +1145,14 @@
|
|
|
1143
1145
|
let target = streamingMessageEl && streamingMessageEl.isConnected ? streamingMessageEl : null;
|
|
1144
1146
|
if (!target) {
|
|
1145
1147
|
const aiMsgs = container.querySelectorAll(".message-ai");
|
|
1146
|
-
if (aiMsgs.length === 0)
|
|
1148
|
+
if (aiMsgs.length === 0) {
|
|
1149
|
+
if (currentChannelId2) {
|
|
1150
|
+
const buf = stepEventBuffer.get(currentChannelId2) || [];
|
|
1151
|
+
buf.push(data);
|
|
1152
|
+
stepEventBuffer.set(currentChannelId2, buf);
|
|
1153
|
+
}
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1147
1156
|
target = aiMsgs[aiMsgs.length - 1];
|
|
1148
1157
|
}
|
|
1149
1158
|
if (!target) return;
|
|
@@ -1157,6 +1166,15 @@
|
|
|
1157
1166
|
error: data.error
|
|
1158
1167
|
});
|
|
1159
1168
|
}
|
|
1169
|
+
function flushStepEventBuffer(channelId, ctx) {
|
|
1170
|
+
if (!channelId) return;
|
|
1171
|
+
const buf = stepEventBuffer.get(channelId);
|
|
1172
|
+
if (!buf || buf.length === 0) return;
|
|
1173
|
+
stepEventBuffer.delete(channelId);
|
|
1174
|
+
for (const evt of buf) {
|
|
1175
|
+
handleStepEvent(evt, ctx);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1160
1178
|
function resetRendererState() {
|
|
1161
1179
|
streamingMessageEl = null;
|
|
1162
1180
|
streamingTextNode = null;
|
|
@@ -1168,7 +1186,7 @@
|
|
|
1168
1186
|
scrollToBottomTimer = null;
|
|
1169
1187
|
}
|
|
1170
1188
|
}
|
|
1171
|
-
var streamingMessageEl, streamingTextNode, streamingText, lastUserCommand, lastAiContent, scrollToBottomTimer, MessageRenderer;
|
|
1189
|
+
var streamingMessageEl, streamingTextNode, streamingText, lastUserCommand, lastAiContent, stepEventBuffer, scrollToBottomTimer, MessageRenderer;
|
|
1172
1190
|
var init_message_renderer = __esm({
|
|
1173
1191
|
"src/web/ui/message-renderer.ts"() {
|
|
1174
1192
|
"use strict";
|
|
@@ -1179,12 +1197,14 @@
|
|
|
1179
1197
|
streamingText = "";
|
|
1180
1198
|
lastUserCommand = "";
|
|
1181
1199
|
lastAiContent = "";
|
|
1200
|
+
stepEventBuffer = /* @__PURE__ */ new Map();
|
|
1182
1201
|
scrollToBottomTimer = null;
|
|
1183
1202
|
MessageRenderer = {
|
|
1184
1203
|
addMessage,
|
|
1185
1204
|
handleStreamTokenEvent,
|
|
1186
1205
|
finalizeTimelineAsMessage,
|
|
1187
1206
|
handleStepEvent,
|
|
1207
|
+
flushStepEventBuffer,
|
|
1188
1208
|
escapeHtml,
|
|
1189
1209
|
getMessagesContainerForCurrent,
|
|
1190
1210
|
resetRendererState
|
|
@@ -57,6 +57,8 @@ export function sanitizeChannelForPeer(ch, peerPublicKey) {
|
|
|
57
57
|
updatedAt: ch.updatedAt,
|
|
58
58
|
hasWallet: !!ch.walletAddress,
|
|
59
59
|
share_id: ch.share_id,
|
|
60
|
+
// 2026-07-20 Bug 2: 保留 ownerPublicKey, 前端用 peerId 区分多节点
|
|
61
|
+
_ownerPublicKey: ch.publicKey,
|
|
60
62
|
};
|
|
61
63
|
}
|
|
62
64
|
/** v3 新增: 判断 channel 是否分享给 peerPublicKey */
|
package/dist/web/server.js
CHANGED
|
@@ -14,9 +14,9 @@ import { registerLlmConfigRoutes } from './routes-llm-config.js';
|
|
|
14
14
|
import { registerTaskRoutes } from './routes-tasks.js';
|
|
15
15
|
import { registerHearthRoutes } from './routes-hearth.js';
|
|
16
16
|
// 2026-07-06: 类型抽到 ./server-types.ts (channel / session / task / sse client / iroh info / paths)
|
|
17
|
-
import { SESSION_CACHE_PATH, IPFS_ENDPOINT, } from './server-types.js';
|
|
17
|
+
import { SESSION_CACHE_PATH, SHARED_SESSION_PATH, IPFS_ENDPOINT, } from './server-types.js';
|
|
18
18
|
// 同时也 re-export 出去 (其它地方可能从 './server.js' 引用)
|
|
19
|
-
export { CHANNELS_PATH, SESSION_CACHE_PATH, THEME_PATH, TASK_QUEUE_PATH, IPFS_ENDPOINT, } from './server-types.js';
|
|
19
|
+
export { CHANNELS_PATH, SESSION_CACHE_PATH, SHARED_SESSION_PATH, THEME_PATH, TASK_QUEUE_PATH, IPFS_ENDPOINT, } from './server-types.js';
|
|
20
20
|
// 读自身 package.json 拿 version (health endpoint 用)
|
|
21
21
|
// 路径: src/web/server.ts → ../../package.json (编译后 dist/web/server.js)
|
|
22
22
|
let cachedVersion = null;
|
|
@@ -108,7 +108,6 @@ function resolveWebRoot() {
|
|
|
108
108
|
}
|
|
109
109
|
const webRoot = resolveWebRoot();
|
|
110
110
|
console.log(`[web] webRoot = ${webRoot}`);
|
|
111
|
-
const SHARED_SESSION_PATH = path.join(process.env.HOME || '/tmp', '.bolloon', 'sessions');
|
|
112
111
|
// iroh P2P 状态
|
|
113
112
|
let irohNodeInfo = null;
|
|
114
113
|
let irohInitialized = false;
|
|
@@ -1230,12 +1229,73 @@ async function getAgentForChannel(channelId, channelDid, channelName, channelDid
|
|
|
1230
1229
|
}
|
|
1231
1230
|
// 2026-07-06: CreateWebServerOptions 抽到 ./server-types.ts (顶部 re-export)
|
|
1232
1231
|
let selfImproveEnabled = false;
|
|
1232
|
+
// ========== 端口锁 + 优雅关闭 ==========
|
|
1233
|
+
const LOCK_PATH = path.join(os.homedir(), '.bolloon', 'port.lock.json');
|
|
1234
|
+
let activeServer = null;
|
|
1235
|
+
let cleanupDone = false;
|
|
1236
|
+
function cleanupAndExit(signal) {
|
|
1237
|
+
if (cleanupDone)
|
|
1238
|
+
return;
|
|
1239
|
+
cleanupDone = true;
|
|
1240
|
+
console.log(`[server] 收到 ${signal}, 开始清理...`);
|
|
1241
|
+
try {
|
|
1242
|
+
fsSync.unlinkSync(LOCK_PATH);
|
|
1243
|
+
}
|
|
1244
|
+
catch (e) {
|
|
1245
|
+
if (e?.code !== 'ENOENT')
|
|
1246
|
+
console.warn(`[port-lock] 删锁失败:`, e?.message);
|
|
1247
|
+
}
|
|
1248
|
+
if (activeServer) {
|
|
1249
|
+
activeServer.close(() => { process.exit(0); });
|
|
1250
|
+
setTimeout(() => process.exit(0), 5000);
|
|
1251
|
+
}
|
|
1252
|
+
else {
|
|
1253
|
+
process.exit(0);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
function writeLock(port) {
|
|
1257
|
+
try {
|
|
1258
|
+
fsSync.writeFileSync(LOCK_PATH, JSON.stringify({ port, pid: process.pid, startedAt: new Date().toISOString() }));
|
|
1259
|
+
}
|
|
1260
|
+
catch (e) {
|
|
1261
|
+
console.warn(`[port-lock] 写锁文件失败:`, e?.message);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
function checkStaleLock(startPort) {
|
|
1265
|
+
try {
|
|
1266
|
+
const raw = fsSync.readFileSync(LOCK_PATH, 'utf-8');
|
|
1267
|
+
const lock = JSON.parse(raw);
|
|
1268
|
+
if (!lock?.pid || lock.pid === process.pid)
|
|
1269
|
+
return;
|
|
1270
|
+
if (lock.port < startPort || lock.port > startPort + 10)
|
|
1271
|
+
return;
|
|
1272
|
+
try {
|
|
1273
|
+
process.kill(lock.pid, 0);
|
|
1274
|
+
console.warn(`⚠ 旧进程 PID ${lock.pid} 仍存活 (端口 ${lock.port}), 尝试终止...`);
|
|
1275
|
+
process.kill(lock.pid, 'SIGTERM');
|
|
1276
|
+
}
|
|
1277
|
+
catch (e2) {
|
|
1278
|
+
if (e2?.code === 'ESRCH') {
|
|
1279
|
+
console.log(`[port-lock] 上一实例 (PID ${lock.pid}) 已结束`);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
catch (e) {
|
|
1284
|
+
if (e?.code !== 'ENOENT')
|
|
1285
|
+
console.warn(`[port-lock] 读取失败:`, e?.message);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1233
1288
|
export async function createWebServer(port = 3000, options = {}) {
|
|
1234
1289
|
selfImproveEnabled = options.selfImprove ?? false;
|
|
1235
1290
|
// 防止 P2P DHT 超时等错误导致进程崩溃
|
|
1236
1291
|
process.on('unhandledRejection', (reason, promise) => {
|
|
1237
1292
|
console.error('[警告] 未处理的 Promise 拒绝:', reason);
|
|
1238
1293
|
});
|
|
1294
|
+
// 优雅关闭信号
|
|
1295
|
+
process.on('SIGTERM', () => cleanupAndExit('SIGTERM'));
|
|
1296
|
+
process.on('SIGINT', () => cleanupAndExit('SIGINT'));
|
|
1297
|
+
// 启动前检查残存锁文件
|
|
1298
|
+
checkStaleLock(port);
|
|
1239
1299
|
// Bolloon Bootstrap (幂等, 重复调不会重复挂定时器)
|
|
1240
1300
|
// 这里独立调一次以保证 CLI-only 模式 (无 index.ts 引导) 也能 bootstrap
|
|
1241
1301
|
try {
|
|
@@ -5326,6 +5386,8 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
5326
5386
|
}
|
|
5327
5387
|
catch { }
|
|
5328
5388
|
});
|
|
5389
|
+
activeServer = currentServer;
|
|
5390
|
+
writeLock(currentPort);
|
|
5329
5391
|
resolve({ app, server: currentServer, port: currentPort });
|
|
5330
5392
|
});
|
|
5331
5393
|
};
|
|
@@ -10,6 +10,7 @@ let streamingTextNode = null;
|
|
|
10
10
|
let streamingText = "";
|
|
11
11
|
let lastUserCommand = "";
|
|
12
12
|
let lastAiContent = "";
|
|
13
|
+
const stepEventBuffer = /* @__PURE__ */ new Map();
|
|
13
14
|
function hasStreamingText() {
|
|
14
15
|
return streamingText.length > 0;
|
|
15
16
|
}
|
|
@@ -160,6 +161,7 @@ function addMessage(content, type, save = true, container, usedJudgmentIds = [],
|
|
|
160
161
|
}
|
|
161
162
|
if (type === "ai" && msgContainer) {
|
|
162
163
|
mountStepTimeline(div, currentChannelId);
|
|
164
|
+
flushStepEventBuffer(currentChannelId, ctx);
|
|
163
165
|
}
|
|
164
166
|
div.appendChild(time);
|
|
165
167
|
if (msgContainer) {
|
|
@@ -364,7 +366,14 @@ function handleStepEvent(data, ctx = { messagesEl: null, messagesContainers: /*
|
|
|
364
366
|
let target = streamingMessageEl && streamingMessageEl.isConnected ? streamingMessageEl : null;
|
|
365
367
|
if (!target) {
|
|
366
368
|
const aiMsgs = container.querySelectorAll(".message-ai");
|
|
367
|
-
if (aiMsgs.length === 0)
|
|
369
|
+
if (aiMsgs.length === 0) {
|
|
370
|
+
if (currentChannelId) {
|
|
371
|
+
const buf = stepEventBuffer.get(currentChannelId) || [];
|
|
372
|
+
buf.push(data);
|
|
373
|
+
stepEventBuffer.set(currentChannelId, buf);
|
|
374
|
+
}
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
368
377
|
target = aiMsgs[aiMsgs.length - 1];
|
|
369
378
|
}
|
|
370
379
|
if (!target) return;
|
|
@@ -378,6 +387,15 @@ function handleStepEvent(data, ctx = { messagesEl: null, messagesContainers: /*
|
|
|
378
387
|
error: data.error
|
|
379
388
|
});
|
|
380
389
|
}
|
|
390
|
+
function flushStepEventBuffer(channelId, ctx) {
|
|
391
|
+
if (!channelId) return;
|
|
392
|
+
const buf = stepEventBuffer.get(channelId);
|
|
393
|
+
if (!buf || buf.length === 0) return;
|
|
394
|
+
stepEventBuffer.delete(channelId);
|
|
395
|
+
for (const evt of buf) {
|
|
396
|
+
handleStepEvent(evt, ctx);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
381
399
|
function resetRendererState() {
|
|
382
400
|
streamingMessageEl = null;
|
|
383
401
|
streamingTextNode = null;
|
|
@@ -394,6 +412,7 @@ const MessageRenderer = {
|
|
|
394
412
|
handleStreamTokenEvent,
|
|
395
413
|
finalizeTimelineAsMessage,
|
|
396
414
|
handleStepEvent,
|
|
415
|
+
flushStepEventBuffer,
|
|
397
416
|
escapeHtml,
|
|
398
417
|
getMessagesContainerForCurrent,
|
|
399
418
|
resetRendererState
|
|
@@ -406,6 +425,7 @@ export {
|
|
|
406
425
|
addMessage,
|
|
407
426
|
escapeHtml,
|
|
408
427
|
finalizeTimelineAsMessage,
|
|
428
|
+
flushStepEventBuffer,
|
|
409
429
|
getMessagesContainerForCurrent,
|
|
410
430
|
handleStepEvent,
|
|
411
431
|
handleStreamTokenEvent,
|