@bolloon/bolloon-agent 0.1.15 → 0.1.17

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bolloon/bolloon-agent",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "type": "module",
5
5
  "description": "P2P AI Document Agent - 全局安装后执行 `bolloon` 启动产品",
6
6
  "main": "dist/cli.js",
@@ -32,7 +32,7 @@
32
32
  "src/constraint-runtime"
33
33
  ],
34
34
  "dependencies": {
35
- "@bolloon/bolloon-agent": "^0.1.15",
35
+ "@bolloon/bolloon-agent": "^0.1.17",
36
36
  "@bolloon/constraint-runtime": "0.1.0",
37
37
  "@chainsafe/libp2p-noise": "^17.0.0",
38
38
  "@chainsafe/libp2p-yamux": "^8.0.1",
@@ -12,8 +12,11 @@ const DIST_WEB = path.join(ROOT, 'dist', 'web');
12
12
  async function main() {
13
13
  console.log('[build-web] 开始构建...');
14
14
 
15
- // 清理并创建目录
16
- await fs.rm(DIST_WEB, { recursive: true, force: true });
15
+ // 重要: 不能 rm -rf 整个 dist/web/, 否则会删掉 build:main 编译出来的
16
+ // dist/web/server.js. 只清理 web 静态资源, 保留 server.js.
17
+ for (const f of ['index.html', 'api-config.html', 'style.css', 'client.js', 'components']) {
18
+ await fs.rm(path.join(DIST_WEB, f), { recursive: true, force: true });
19
+ }
17
20
  await fs.mkdir(DIST_WEB, { recursive: true });
18
21
  await fs.mkdir(path.join(DIST_WEB, 'components', 'p2p'), { recursive: true });
19
22
 
@@ -54,6 +57,10 @@ async function main() {
54
57
  await fs.copyFile(path.join(ROOT, 'src/web/api-config.html'), path.join(DIST_WEB, 'api-config.html'));
55
58
  await fs.copyFile(path.join(ROOT, 'src/web/style.css'), path.join(DIST_WEB, 'style.css'));
56
59
  await fs.copyFile(path.join(ROOT, 'src/web/client.js'), path.join(DIST_WEB, 'client.js'));
60
+ // 复制 PWA manifest (index.html 里有 <link rel="manifest">, 否则浏览器会 404)
61
+ await fs.copyFile(path.join(ROOT, 'src/web/manifest.json'), path.join(DIST_WEB, 'manifest.json'));
62
+ // 复制 icons 目录 (manifest.json 里引用了 favicon 等)
63
+ await fs.cp(path.join(ROOT, 'src/web/icons'), path.join(DIST_WEB, 'icons'), { recursive: true });
57
64
 
58
65
  console.log('[build-web] 完成!');
59
66
  }
@@ -1115,7 +1115,10 @@ ${this.getToolDefinitions()}
1115
1115
  let lastQualityScore = 0;
1116
1116
  let refineAttempts = 0;
1117
1117
  let consecutiveErrors = 0;
1118
+ let lastFailedTool = ''; // 跟踪最近一次失败的 tool name
1119
+ let lastFailedToolCount = 0; // 最近失败工具的连续失败次数
1118
1120
  const MAX_CONSECUTIVE_ERRORS = 3;
1121
+ const MAX_SAME_TOOL_FAILURES = 3; // 同一工具连续失败 3 次, 强制让 LLM 给出最终答案
1119
1122
 
1120
1123
  // 发送循环开始的事件
1121
1124
  if (onStream) {
@@ -1260,17 +1263,36 @@ ${toolDefs}
1260
1263
  // 不 break,继续下一次循环
1261
1264
  } else {
1262
1265
  consecutiveErrors++;
1263
- console.warn(`[PiAgent] 工具执行失败 (${consecutiveErrors}/${MAX_CONSECUTIVE_ERRORS}): ${result.error}`);
1266
+ // 跟踪同一工具连续失败次数
1267
+ if (toolCall.name === lastFailedTool) {
1268
+ lastFailedToolCount++;
1269
+ } else {
1270
+ lastFailedTool = toolCall.name;
1271
+ lastFailedToolCount = 1;
1272
+ }
1273
+ console.warn(`[PiAgent] 工具 ${toolCall.name} 执行失败 (${lastFailedToolCount}/${MAX_SAME_TOOL_FAILURES}): ${result.error}`);
1274
+
1275
+ // 同一工具连续失败达到上限, 不再重试, 强制 LLM 给出最终答案
1276
+ if (lastFailedToolCount >= MAX_SAME_TOOL_FAILURES) {
1277
+ console.log(`[PiAgent] 工具 ${toolCall.name} 连续 ${MAX_SAME_TOOL_FAILURES} 次失败, 放弃并要求直接回答`);
1278
+ this.messageHistory.push({
1279
+ role: 'system',
1280
+ content: `[注意] 工具 ${toolCall.name} 在这个上下文中不可用 (连续 ${MAX_SAME_TOOL_FAILURES} 次失败: ${result.error}). 请不要再次调用它, 直接用你已知的信息回答用户, 并在回答开头标记 <final gen>.`
1281
+ });
1282
+ lastFailedTool = '';
1283
+ lastFailedToolCount = 0;
1284
+ consecutiveErrors = 0;
1285
+ continue; // 让 LLM 看到系统提示后再决定
1286
+ }
1264
1287
 
1265
- // 连续错误达到上限,尝试换一种方式
1288
+ // 连续错误达到上限(混合不同工具), 尝试换一种方式
1266
1289
  if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
1267
1290
  console.log(`[PiAgent] 连续 ${MAX_CONSECUTIVE_ERRORS} 次错误,尝试换一种方式处理`);
1268
- // 添加错误上下文,让 LLM 换一种方式
1269
1291
  this.messageHistory.push({
1270
1292
  role: 'system',
1271
- content: `[注意] 前面的工具调用连续失败。请尝试其他工具或换一种方式完成用户请求。`
1293
+ content: `[注意] 前面的工具调用连续失败。请尝试其他工具或换一种方式完成用户请求, 或用 <final gen> 给出最终回答.`
1272
1294
  });
1273
- consecutiveErrors = 0; // 重置以继续尝试
1295
+ consecutiveErrors = 0;
1274
1296
  }
1275
1297
  }
1276
1298
  } catch (execError) {
@@ -1323,9 +1345,14 @@ ${toolDefs}
1323
1345
  }
1324
1346
 
1325
1347
  if (!finalResponse) {
1326
- finalResponse = '任务处理超时,请尝试更具体的请求。';
1348
+ // 走到这里通常是 LLM 一直在调同一个不存在的工具, 没输出 <final gen>
1349
+ // 把已知的失败信息也带回去, 让用户知道发生了什么
1350
+ const reason = lastFailedTool
1351
+ ? `(工具 ${lastFailedTool} 连续 ${MAX_SAME_TOOL_FAILURES} 次失败, 已放弃)`
1352
+ : `(共 ${iteration - 1} 轮无最终输出)`;
1353
+ finalResponse = `抱歉,任务未能完成 ${reason}。请换个方式提问,或明确告诉 agent 不要调用工具。`;
1327
1354
  if (onStream) {
1328
- onStream({ type: 'error', content: '⚠️ 任务处理超时', tool: 'system' });
1355
+ onStream({ type: 'error', content: `⚠️ 任务未完成: ${reason}`, tool: 'system' });
1329
1356
  }
1330
1357
  }
1331
1358
 
@@ -1654,9 +1681,17 @@ ${this.extractOperationsFromRef(operationsRef)}
1654
1681
  );
1655
1682
 
1656
1683
  const name = response.reply.trim();
1657
- if (name && name.length <= 20 && name !== '智能体') {
1658
- return `Agent | ${name}`;
1684
+ // 拒绝错误回退串 (LLM 不可用时返回的占位文本)
1685
+ if (!name) return null;
1686
+ if (/^(抱歉|对不起|sorry|error|错误|失败|暂不可用|服务不可用)/i.test(name)) {
1687
+ console.log(`[suggestRename] 拒绝错误回退: "${name}"`);
1688
+ return null;
1659
1689
  }
1690
+ if (name.length > 20) return null;
1691
+ if (name === '智能体') return null;
1692
+ // 拒绝纯符号/标点
1693
+ if (!/[一-鿿\w]/.test(name)) return null;
1694
+ return `Agent | ${name}`;
1660
1695
  } catch {
1661
1696
  // ignore
1662
1697
  }
package/src/web/server.ts CHANGED
@@ -117,6 +117,13 @@ async function saveChannels(channels: Channel[]): Promise<void> {
117
117
  return rest as Channel;
118
118
  });
119
119
  const jsonStr = JSON.stringify(sanitized, null, 2);
120
+
121
+ // 写盘保护: 内容和上次完全一致就跳过, 避免 SSE ping / 重新 init 触发的无意义写盘
122
+ if (jsonStr === lastChannelsJson) {
123
+ return; // 静默跳过, 不打日志
124
+ }
125
+ lastChannelsJson = jsonStr;
126
+
120
127
  console.log('[saveChannels] 保存频道数据, 数量:', sanitized.length);
121
128
  console.log('[saveChannels] JSON 长度:', jsonStr.length);
122
129
  await fs.writeFile(CHANNELS_PATH, jsonStr);
@@ -124,6 +131,9 @@ async function saveChannels(channels: Channel[]): Promise<void> {
124
131
  lastChannelsWriteAt = Date.now();
125
132
  }
126
133
 
134
+ // 写盘去重: 上次写盘内容, 用于跳过幂等调用
135
+ let lastChannelsJson = '';
136
+
127
137
  // 模块级: 最近一次 channels.json 写盘时间. saveChannels 在模块顶层,
128
138
  // getChannelsWithDID 在 createWebServer 内部, 跨作用域用模块变量桥接.
129
139
  let lastChannelsWriteAt = 0;