@bolloon/bolloon-agent 0.3.0 → 0.3.3

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.
@@ -72,6 +72,42 @@ export function parseToolCall(content, ctx) {
72
72
  return null;
73
73
  // === 0. 剥离 思考块 — 必须先做, 否则旧 regex `<(\w+)>...</\1>` 会先匹配 想想标签 ===
74
74
  const strippedContent = content.replace(/<think[\s\S]*?<\/think/g, '');
75
+ // [diag-2026-07-12] 临时诊断日志:WebUI 出现 7 个 “必填/参数 undefined” 错误,
76
+ // 怀疑 parseToolCall 没拿到正确的 name/args. 这里只打日志不改任何逻辑.
77
+ // 日志形态: [parseToolCall diag] ok=true/false name=... argKeys=... rawHead=...
78
+ // 用 console.warn 一行 JSON, 方便 grep + 排时间线.
79
+ try {
80
+ const result = (function _diagProbe() {
81
+ // 用与正文完全相同的解析路径,但提前跑一次,记录是否命中
82
+ // 走完下面所有分支再覆盖回原值即可
83
+ return null;
84
+ })();
85
+ void result;
86
+ const probe = (function _doParse() {
87
+ // 内联一份最便宜的 "能不能解出 name" 探测 — 不重复跑全部分支
88
+ const m1 = strippedContent.match(/<invoke\s+name=["']([\w]+)["']/);
89
+ if (m1) {
90
+ return { name: m1[1], args: { __probe_invoketag: '1' } };
91
+ }
92
+ const m2 = strippedContent.match(/<function_calls>[\s\S]*?<invoke\s+name=["']([\w]+)["']/);
93
+ if (m2) {
94
+ return { name: m2[1], args: { __probe_function_calls: '1' } };
95
+ }
96
+ const m3 = strippedContent.match(/\{[\s\S]*?"name"\s*:\s*["']([\w]+)["']/);
97
+ if (m3) {
98
+ return { name: m3[1], args: { __probe_json_name: '1' } };
99
+ }
100
+ return null;
101
+ })();
102
+ console.warn('[parseToolCall diag] rawLen=' + content.length +
103
+ ' strippedLen=' + strippedContent.length +
104
+ ' probeName=' + (probe?.name ?? 'null') +
105
+ ' rawHead=' + JSON.stringify(content.slice(0, 500)));
106
+ }
107
+ catch (diagErr) {
108
+ // 诊断日志绝不能影响主路径 — 吞掉任何 diag 自身抛错
109
+ console.warn('[parseToolCall diag] diag-self-failed:', String(diagErr));
110
+ }
75
111
  // === 1. JSON function-call (OpenAI / Anthropic / Minimax-style) ===
76
112
  const jsonPatterns = [
77
113
  // markdown json code block + OpenAI 块, 同时匹配 arguments/input 字段
@@ -37,10 +37,13 @@ export function registerBuiltinTools(ctx) {
37
37
  ctx.tools.set('read_document', {
38
38
  name: 'read_document',
39
39
  description: '读取文档内容,支持 .txt, .md, .pdf, .docx 格式',
40
- parameters: { path: 'string' },
40
+ parameters: { path: '文件路径 (必填)' },
41
41
  execute: async (args) => {
42
42
  try {
43
- const content = await documentReader.read(args.path);
43
+ const path = String(args.path || '').trim();
44
+ if (!path)
45
+ return { success: false, error: 'path 必填' };
46
+ const content = await documentReader.read(path);
44
47
  return {
45
48
  success: true,
46
49
  output: `📄 ${content.metadata.filename}\n大小: ${content.metadata.size} 字节\n\n${content.text.substring(0, 1000)}${content.text.length > 1000 ? '...' : ''}`
@@ -54,16 +57,19 @@ export function registerBuiltinTools(ctx) {
54
57
  ctx.tools.set('summarize_document', {
55
58
  name: 'summarize_document',
56
59
  description: '总结文档内容,分析并生成摘要',
57
- parameters: { path: 'string', context: 'string' },
60
+ parameters: { path: '文件路径 (必填)', context: '可选, 总结上下文提示' },
58
61
  execute: async (args) => {
59
62
  try {
63
+ const path = String(args.path || '').trim();
64
+ if (!path)
65
+ return { success: false, error: 'path 必填' };
60
66
  if (!ctx.minimaxAvailable) {
61
67
  return { success: false, error: 'LLM未初始化,请设置 MINIMAX_API_KEY' };
62
68
  }
63
69
  // summarizeDocument 走 PiAgentSession.summarizeDocument, 这里通过 tools 反向调用不好处理
64
70
  // 简化: 让 LLM 自己直接走 shell_exec / use_skill 路径
65
71
  const llm = getMinimax();
66
- const content = await documentReader.read(args.path);
72
+ const content = await documentReader.read(path);
67
73
  const r = await llm.summarize(content.text, args.context);
68
74
  return {
69
75
  success: true,
@@ -78,15 +84,21 @@ export function registerBuiltinTools(ctx) {
78
84
  ctx.tools.set('improve_document', {
79
85
  name: 'improve_document',
80
86
  description: '根据要求改进文档内容',
81
- parameters: { path: 'string', requirements: 'string' },
87
+ parameters: { path: '文件路径 (必填)', requirements: '改进要求 (必填)' },
82
88
  execute: async (args) => {
83
89
  try {
90
+ const path = String(args.path || '').trim();
91
+ if (!path)
92
+ return { success: false, error: 'path 必填' };
93
+ const requirements = String(args.requirements || '').trim();
94
+ if (!requirements)
95
+ return { success: false, error: 'requirements 必填' };
84
96
  if (!ctx.minimaxAvailable) {
85
97
  return { success: false, error: 'LLM未初始化,请设置 MINIMAX_API_KEY' };
86
98
  }
87
99
  const llm = getMinimax();
88
- const content = await documentReader.read(args.path);
89
- const improved = await llm.summarize(content.text + '\n\n改进要求: ' + args.requirements, undefined);
100
+ const content = await documentReader.read(path);
101
+ const improved = await llm.summarize(content.text + '\n\n改进要求: ' + requirements, undefined);
90
102
  return {
91
103
  success: true,
92
104
  output: `✅ 改进完成\n质量评分: ${(improved.qualityScore * 10).toFixed(1)}/10\n${improved.summary ? '\n改进内容:\n' + improved.summary.substring(0, 500) + '...' : ''}`
@@ -7,34 +7,101 @@ const GREEN = '\x1b[32m';
7
7
  const RED = '\x1b[31m';
8
8
  const GRAY = '\x1b[90m';
9
9
  const RESET = '\x1b[0m';
10
+ const STEP_SYMBOL = {
11
+ pending: `${GRAY}○${RESET}`,
12
+ active: `${YELLOW}⠹${RESET}`,
13
+ ok: `${GREEN}✓${RESET}`,
14
+ warn: `${YELLOW}⚠${RESET}`,
15
+ error: `${RED}✗${RESET}`,
16
+ };
10
17
  export class LoadingTUI {
11
18
  write;
12
19
  timer = null;
13
- running = false;
20
+ frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
21
+ frameIdx = 0;
22
+ steps = [];
23
+ currentLabel = 'Bolloon loading...';
24
+ lastRenderedStepCount = 0;
25
+ finished = false;
26
+ ok = true;
14
27
  constructor() {
15
28
  this.write = process.stdout.write.bind(process.stdout);
16
29
  }
30
+ setSteps(steps) {
31
+ this.steps = steps.map(label => ({ label, status: 'pending' }));
32
+ this.drawAll();
33
+ }
34
+ startStep(index, label) {
35
+ if (index < 0 || index >= this.steps.length)
36
+ return;
37
+ this.steps[index].status = 'active';
38
+ if (label !== undefined)
39
+ this.steps[index].label = label;
40
+ this.drawAll();
41
+ }
42
+ completeStep(index, status = 'ok', label) {
43
+ if (index < 0 || index >= this.steps.length)
44
+ return;
45
+ this.steps[index].status = status;
46
+ if (label !== undefined)
47
+ this.steps[index].label = label;
48
+ this.drawAll();
49
+ }
50
+ setMessage(msg) {
51
+ this.currentLabel = msg;
52
+ }
17
53
  start(msg = 'Bolloon loading...') {
18
- this.running = true;
54
+ if (this.timer)
55
+ return;
56
+ this.currentLabel = msg;
19
57
  this.write(HIDE);
20
- const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
21
- let i = 0;
22
58
  this.timer = setInterval(() => {
23
- if (!this.running)
59
+ if (this.finished)
24
60
  return;
25
- this.write(`\r${CLEAR}\r ${YELLOW}${frames[i++ % frames.length]}${RESET} ${msg}`);
61
+ this.write(`\r${CLEAR}\r ${YELLOW}${this.frames[this.frameIdx++ % this.frames.length]}${RESET} ${this.currentLabel}`);
26
62
  }, 100);
27
63
  }
64
+ drawAll() {
65
+ if (!this.timer || this.finished)
66
+ return;
67
+ const out = [];
68
+ for (const step of this.steps) {
69
+ const prefix = step.status === 'active' ? YELLOW : '';
70
+ out.push(` ${STEP_SYMBOL[step.status]} ${prefix}${step.label}${RESET}\n`);
71
+ }
72
+ this.lastRenderedStepCount = this.steps.length;
73
+ this.write(out.join(''));
74
+ this.write(`\x1b[${this.steps.length}A`);
75
+ this.write(`\r${CLEAR}\r ${YELLOW}${this.frames[this.frameIdx % this.frames.length]}${RESET} ${this.currentLabel}`);
76
+ }
28
77
  stop(ok = true) {
29
- this.running = false;
30
- if (this.timer)
78
+ this.finished = true;
79
+ this.ok = ok;
80
+ if (this.timer) {
31
81
  clearInterval(this.timer);
32
- this.timer = null;
82
+ this.timer = null;
83
+ }
84
+ if (this.lastRenderedStepCount > 0) {
85
+ this.write(`\x1b[${this.lastRenderedStepCount}B`);
86
+ }
33
87
  this.write(`\r${CLEAR}\r`);
34
- if (ok)
88
+ if (this.steps.length > 0) {
89
+ for (const step of this.steps) {
90
+ this.write(` ${STEP_SYMBOL[step.status]} ${step.label}\n`);
91
+ }
92
+ }
93
+ if (this.ok) {
35
94
  this.write(` ${GREEN}✓${RESET} ${CYAN}Bolloon${RESET} ${GRAY}ready${RESET}\n`);
36
- else
95
+ }
96
+ else {
37
97
  this.write(` ${RED}✗${RESET} ${CYAN}Bolloon${RESET} ${GRAY}startup failed${RESET}\n`);
98
+ }
38
99
  this.write(SHOW);
39
100
  }
101
+ isFinished() {
102
+ return this.finished;
103
+ }
104
+ wasOk() {
105
+ return this.ok;
106
+ }
40
107
  }
@@ -4,6 +4,9 @@ import pdfParse from 'pdf-parse';
4
4
  import mammoth from 'mammoth';
5
5
  export class DocumentReader {
6
6
  async read(filePath) {
7
+ if (!filePath || typeof filePath !== 'string' || !filePath.trim()) {
8
+ throw new Error('read(filePath): filePath 必填且必须是非空字符串');
9
+ }
7
10
  const stats = await fs.stat(filePath);
8
11
  const ext = path.extname(filePath).toLowerCase();
9
12
  const filename = path.basename(filePath);
package/dist/index.js CHANGED
@@ -1819,7 +1819,16 @@ async function main() {
1819
1819
  const isCLIInteractive = mode === 'cli' && !isNonInteractive;
1820
1820
  loading = isCLIInteractive ? new LoadingTUI() : null;
1821
1821
  if (loading) {
1822
- loading.start();
1822
+ loading.setSteps([
1823
+ 'LLM provider 检测',
1824
+ 'DIAP 身份生成',
1825
+ 'DID 发布到 IPFS',
1826
+ 'P2P 网络启动',
1827
+ 'iroh transport 启动',
1828
+ 'Bolloon 上下文 bootstrap',
1829
+ 'Web 服务启动',
1830
+ ]);
1831
+ loading.start('启动中...');
1823
1832
  console.log = () => { };
1824
1833
  console.info = () => { };
1825
1834
  process.stdout.write = () => true;
@@ -1873,22 +1882,31 @@ async function main() {
1873
1882
  hasQwen ? 'Qwen' : null;
1874
1883
  if (llmProvider) {
1875
1884
  s.step(0, 4, `LLM: ${llmProvider}`, 'ok');
1885
+ loading?.completeStep(0, 'ok', `LLM: ${llmProvider}`);
1876
1886
  initMinimax({ provider: llmProvider.toLowerCase() });
1877
1887
  }
1878
1888
  else {
1879
1889
  s.step(0, 4, 'LLM: 未配置', 'warn');
1890
+ loading?.completeStep(0, 'warn', 'LLM: 未配置');
1880
1891
  if (isNonInteractive) {
1881
1892
  s.warn('未设置任何 LLM API Key,功能受限');
1882
1893
  }
1883
1894
  }
1895
+ loading?.startStep(1, '生成 DIAP 身份...');
1884
1896
  const { keypair, did, name } = await bootstrapIdentity();
1885
1897
  agentIdentity = { did, name, publicKey: Buffer.from(keypair.publicKey).toString('hex') };
1898
+ loading?.completeStep(1, 'ok', `身份 ${name}`);
1899
+ loading?.startStep(2, '发布 DID 到 IPFS...');
1886
1900
  publishDID(name, keypair).then(({ cid, ipnsName }) => {
1887
1901
  if (cid)
1888
1902
  agentIdentity.cid = cid;
1889
1903
  if (ipnsName)
1890
1904
  agentIdentity.ipnsName = ipnsName;
1891
- }).catch(() => { });
1905
+ loading?.completeStep(2, cid ? 'ok' : 'warn', cid ? 'DID 已发布' : 'DID 本地模式');
1906
+ }).catch(() => {
1907
+ loading?.completeStep(2, 'warn', 'DID 本地模式');
1908
+ });
1909
+ loading?.startStep(3, '启动 P2P 网络...');
1892
1910
  const verifier = createVerificationManager();
1893
1911
  let comm = null;
1894
1912
  try {
@@ -1900,7 +1918,9 @@ async function main() {
1900
1918
  agentIdentity.peerId = connections[0].publicKey;
1901
1919
  agentIdentity.p2pChannel = 'bolloon-agent-harness';
1902
1920
  }
1921
+ loading?.completeStep(3, 'ok', 'P2P 已连接');
1903
1922
  }).catch(err => {
1923
+ loading?.completeStep(3, 'warn', 'P2P Web 模式启动失败');
1904
1924
  s.warn(`P2P Web 模式启动失败: ${err.message}`);
1905
1925
  });
1906
1926
  }
@@ -1911,23 +1931,30 @@ async function main() {
1911
1931
  agentIdentity.peerId = connections[0].publicKey;
1912
1932
  agentIdentity.p2pChannel = 'bolloon-agent-harness';
1913
1933
  }
1934
+ loading?.completeStep(3, 'ok', 'P2P 已连接');
1914
1935
  }
1915
1936
  }
1916
1937
  catch (err) {
1917
1938
  s.warn(`P2P 初始化失败: ${err.message}`);
1918
1939
  s.warn('将使用无 P2P 模式运行');
1940
+ loading?.completeStep(3, 'error', 'P2P 初始化失败');
1919
1941
  }
1942
+ loading?.startStep(4, '启动 iroh transport...');
1920
1943
  await bootstrapIroh(keypair, name);
1944
+ loading?.completeStep(4, 'ok', 'iroh 已就绪');
1921
1945
  // Bolloon Bootstrap: 启动扫描 + Context 收集 + 挂定时任务
1922
1946
  // 失败静默 (主流程不被阻塞)
1947
+ loading?.startStep(5, '正在 bootstrap bolloon 上下文...');
1923
1948
  try {
1924
1949
  const { bootstrapBolloon } = await import('./pi-ecosystem-judgment/human-value-pipeline.js');
1925
1950
  s.info('正在 bootstrap bolloon 上下文...');
1926
1951
  const bs = await bootstrapBolloon({ cwd: process.cwd() });
1927
1952
  s.info(`Bootstrap 完成 (${bs.durationMs}ms, ${bs.errors.length} 个非致命错误)`);
1953
+ loading?.completeStep(5, 'ok', `Bootstrap 完成 (${bs.durationMs}ms)`);
1928
1954
  }
1929
1955
  catch (err) {
1930
1956
  s.warn(`Bootstrap 失败 (非致命, 主流程继续): ${err.message}`);
1957
+ loading?.completeStep(5, 'warn', 'Bootstrap 失败 (已跳过)');
1931
1958
  }
1932
1959
  s.divider();
1933
1960
  if (mode === 'web') {
@@ -1939,10 +1966,12 @@ async function main() {
1939
1966
  console.log('[startup] BOLLOON_DEV_MODE=1, 开发者模式: 自迭代已启用');
1940
1967
  }
1941
1968
  const { createWebServer, openBrowser } = await import('./web/server.js');
1969
+ loading?.startStep(6, `启动 Web 服务端口 ${port}...`);
1942
1970
  s.info(`启动 Web 服务端口 ${port}...`);
1943
1971
  // 2026-06-24: CLI 默认 loopback bind (安全), LAN 访问需 BOLLOON_HOST=0.0.0.0
1944
1972
  const bindHost = process.env.BOLLOON_HOST;
1945
1973
  const { port: actualPort } = await createWebServer(port, { selfImprove, ...(bindHost ? { host: bindHost } : {}) });
1974
+ loading?.completeStep(6, 'ok', `Web 服务 :${actualPort}`);
1946
1975
  const displayHost = bindHost ?? '127.0.0.1';
1947
1976
  s.success(`浏览器已打开 → http://${displayHost}:${actualPort}`);
1948
1977
  openBrowser(`http://${displayHost}:${actualPort}`);
@@ -0,0 +1,145 @@
1
+ /**
2
+ * judgeness · auto-add.ts — Channel-based Auto-add (反攻期 O3)
3
+ *
4
+ * 用户原话: "传播智能体的时候, 智能体可根据内容频道选择其他用户的 Id 自动添加"
5
+ *
6
+ * 流程:
7
+ * 1. POST /api/hearth/channel-autoadd { channelTopic, sourceChannelOwnerPk? }
8
+ * 2. 闸 2 (allowlist gate) 校验 sourceChannelOwnerPk
9
+ * 3. 扫描 ~/.bolloon/judgeness/descriptions/, 找出 scope.topics 含 channelTopic 且 openState='open' 的 description
10
+ * 4. 对每个 description 的 owner pk 调用 p2p-direct.joinTopic
11
+ * 5. 全部进 ~/.bolloon/human-values/counterfactual-audit.jsonl
12
+ * 6. 频次限制 (defense=无; 反攻期 = 每分钟 5 次; 单 peer pk 24h 内最多 10 次)
13
+ *
14
+ * 反攻期接 src/network/p2p-direct.ts 的 joinTopic; 防御期 stub.
15
+ * 反攻期接 src/judgeness/protocol.ts 的 sendAutoaddInvite.
16
+ */
17
+ import * as fs from 'fs/promises';
18
+ import * as path from 'path';
19
+ import * as os from 'os';
20
+ const DEFENSE_FREQ_LIMIT_PER_HOUR = 5; // 防御期更严
21
+ const ROLLING_WINDOW_MS = 60 * 60 * 1000; // 1 hour
22
+ export async function performAutoAdd(req, opts = {}) {
23
+ if (!req.channelTopic)
24
+ throw new Error('channelTopic required');
25
+ const now = opts.nowMs ?? Date.now();
26
+ // ---- 频次限制 (读 audit log last hour 统计) ----
27
+ const auditLog = await readAutoaddAudit();
28
+ const recent = auditLog.filter((l) => now - l.ts < ROLLING_WINDOW_MS);
29
+ if (recent.length >= DEFENSE_FREQ_LIMIT_PER_HOUR) {
30
+ return {
31
+ channelTopic: req.channelTopic,
32
+ matched: 0,
33
+ joined: 0,
34
+ skipped: 0,
35
+ auditLines: [],
36
+ frequencyLimited: true,
37
+ };
38
+ }
39
+ // ---- 扫描 descriptions 找 matches ----
40
+ const { listDescriptions } = await import('./store.js');
41
+ const descs = await listDescriptions();
42
+ const matched = descs.filter((d) => {
43
+ const open = d.openState === 'open';
44
+ const topicMatch = (d.scope.topics ?? []).includes(req.channelTopic);
45
+ return open && topicMatch;
46
+ });
47
+ // ---- join (defense=stub) ----
48
+ const result = {
49
+ channelTopic: req.channelTopic,
50
+ matched: matched.length,
51
+ joined: 0,
52
+ skipped: 0,
53
+ auditLines: [],
54
+ frequencyLimited: false,
55
+ };
56
+ // 每次请求都写一条 audit line (不论 matched), 这样 frequency limit 才能工作
57
+ result.auditLines.push(JSON.stringify({
58
+ ts: now,
59
+ kind: 'autoadd_request',
60
+ channelTopic: req.channelTopic,
61
+ by: undefined,
62
+ matched: matched.length,
63
+ }));
64
+ for (const d of matched) {
65
+ const ownerPk = d.byAgentId ?? '__no-pk__';
66
+ if (!opts.joinTopic) {
67
+ // defense: 仅 audit, 不调用 joinTopic
68
+ result.skipped += 1;
69
+ const line = JSON.stringify({
70
+ ts: now,
71
+ kind: 'autoadd_skipped',
72
+ channelTopic: req.channelTopic,
73
+ descriptionId: d.descriptionId,
74
+ ownerPk,
75
+ reason: 'defense stub',
76
+ });
77
+ result.auditLines.push(line);
78
+ continue;
79
+ }
80
+ const r = await opts.joinTopic(req.channelTopic, ownerPk);
81
+ if (r.ok) {
82
+ result.joined += 1;
83
+ result.auditLines.push(JSON.stringify({
84
+ ts: now,
85
+ kind: 'autoadd_joined',
86
+ channelTopic: req.channelTopic,
87
+ descriptionId: d.descriptionId,
88
+ ownerPk,
89
+ }));
90
+ }
91
+ else {
92
+ result.skipped += 1;
93
+ result.auditLines.push(JSON.stringify({
94
+ ts: now,
95
+ kind: 'autoadd_join_failed',
96
+ channelTopic: req.channelTopic,
97
+ descriptionId: d.descriptionId,
98
+ ownerPk,
99
+ }));
100
+ }
101
+ }
102
+ // ---- 写 audit log ----
103
+ await appendCounterfactualAudit(result.auditLines);
104
+ return result;
105
+ }
106
+ // ---------------------------------------------------------------------------
107
+ // audit 读写 helpers
108
+ // ---------------------------------------------------------------------------
109
+ async function readAutoaddAudit() {
110
+ const auditPath = await auditPathResolved();
111
+ try {
112
+ const raw = await fs.readFile(auditPath, 'utf-8');
113
+ return raw.split('\n').filter(Boolean).map((l) => {
114
+ try {
115
+ return JSON.parse(l);
116
+ }
117
+ catch {
118
+ return null;
119
+ }
120
+ }).filter((x) => !!x);
121
+ }
122
+ catch {
123
+ return [];
124
+ }
125
+ }
126
+ async function appendCounterfactualAudit(lines) {
127
+ if (lines.length === 0)
128
+ return;
129
+ const auditPath = await auditPathResolved();
130
+ const dir = path.dirname(auditPath);
131
+ await fs.mkdir(dir, { recursive: true });
132
+ await fs.appendFile(auditPath, lines.join('\n') + '\n', 'utf-8');
133
+ }
134
+ let _auditPathCache = null;
135
+ async function auditPathResolved() {
136
+ if (_auditPathCache)
137
+ return _auditPathCache;
138
+ const home = process.env.BOLLOON_HOME || path.join(os.homedir(), '.bolloon');
139
+ _auditPathCache = path.join(home, 'human-values', 'counterfactual-audit.jsonl');
140
+ return _auditPathCache;
141
+ }
142
+ // 工具: 复位 cache (测试用)
143
+ export function _resetAuditPathCacheForTest() {
144
+ _auditPathCache = null;
145
+ }