@bolloon/bolloon-agent 0.2.1 → 0.2.2

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,479 @@
1
+ /**
2
+ * judgment-protocol — bolloon 的 4 个核心协议消息
3
+ *
4
+ * 协议定义见 docs/真正要做的事.md §3.2:
5
+ * - ask (A→B): 就决策 X 发起 3 个反方蒸馏
6
+ * - dissent (A↔B): 双方反方观点互见
7
+ * - align (A↔B): 基于反方互见形成对齐结论
8
+ * - reflect (本地): 整链闭环后沉淀成 judgment
9
+ *
10
+ * Transport: iroh (复用现有 IrohTransport, 不另起).
11
+ * 4 个消息 kind: 'judgment_ask' | 'judgment_dissent' | 'judgment_align'
12
+ * reflect 不上链, 直接落 ~/.bolloon/judgments/{askId}.yaml
13
+ *
14
+ * 设计取舍:
15
+ * - 协议消息是 askId-keyed 的 (一个决策一条链)
16
+ * - dissent 至少 3 个反方 (协议硬约束, 协议四要素里"反方互见"的最小颗粒)
17
+ * - dissent 不要求显式命令 — 收到 ask 自动蒸馏本方 3 反方回发
18
+ * - align 必须引用 askId + at least 1 dissentId per side, 否则视为未基于"互见"
19
+ * - reflect 是本地, 写盘后 emit 一个 'reflected' 事件, UI 可订阅
20
+ *
21
+ * 蒸馏 (distillDissent) 调用现有 human-value-store 的判断力:
22
+ * loadAllJudgments + getValueProfile, 用 LLM 生成 3 个反方.
23
+ * LLM 不可用时降级到启发式 (从历史 judgment 抽最近 3 条 reasons 当反方骨架).
24
+ */
25
+ import * as crypto from 'crypto';
26
+ import * as fs from 'fs/promises';
27
+ import * as path from 'path';
28
+ import * as os from 'os';
29
+ import { EventEmitter } from 'events';
30
+ import { irohTransport as defaultIrohTransport } from '../network/iroh-transport.js';
31
+ import { loadAllJudgments, getValueProfile, storeHumanJudgment, getRelevantValues } from '../pi-ecosystem-judgment/human-value-store.js';
32
+ class JudgmentEventBus extends EventEmitter {
33
+ }
34
+ export const judgmentEventBus = new JudgmentEventBus();
35
+ // ============================================================================
36
+ // 持久化 (本地 chain registry)
37
+ // ~/.bolloon/judgments/chains/{askId}.json
38
+ // ~/.bolloon/judgments/yaml/{askId}.yaml (reflect 后写出)
39
+ // ============================================================================
40
+ function homeDir() {
41
+ return process.env.HOME || process.env.USERPROFILE || os.homedir();
42
+ }
43
+ function chainsDir() {
44
+ return path.join(homeDir(), '.bolloon', 'judgments', 'chains');
45
+ }
46
+ function yamlDir() {
47
+ return path.join(homeDir(), '.bolloon', 'judgments', 'yaml');
48
+ }
49
+ function chainPath(askId) {
50
+ return path.join(chainsDir(), `${askId}.json`);
51
+ }
52
+ async function ensureDirs() {
53
+ await fs.mkdir(chainsDir(), { recursive: true });
54
+ await fs.mkdir(yamlDir(), { recursive: true });
55
+ }
56
+ async function readChain(askId) {
57
+ try {
58
+ const buf = await fs.readFile(chainPath(askId), 'utf-8');
59
+ return JSON.parse(buf);
60
+ }
61
+ catch {
62
+ return null;
63
+ }
64
+ }
65
+ async function writeChain(chain) {
66
+ await ensureDirs();
67
+ await fs.writeFile(chainPath(chain.ask.askId), JSON.stringify(chain, null, 2), 'utf-8');
68
+ }
69
+ // ============================================================================
70
+ // 帧构造 / 解析
71
+ // ============================================================================
72
+ function encodeAskFrame(ask) {
73
+ return new TextEncoder().encode(JSON.stringify({ kind: 'judgment_ask', payload: ask, ts: ask.ts }));
74
+ }
75
+ function encodeDissentFrame(d) {
76
+ return new TextEncoder().encode(JSON.stringify({ kind: 'judgment_dissent', payload: d, ts: d.ts }));
77
+ }
78
+ function encodeAlignFrame(a) {
79
+ return new TextEncoder().encode(JSON.stringify({ kind: 'judgment_align', payload: a, ts: a.ts }));
80
+ }
81
+ function decodeFrame(buf) {
82
+ try {
83
+ const obj = JSON.parse(new TextDecoder().decode(buf));
84
+ if (typeof obj?.kind === 'string' && obj.payload)
85
+ return obj;
86
+ return null;
87
+ }
88
+ catch {
89
+ return null;
90
+ }
91
+ }
92
+ const states = new WeakMap();
93
+ function getState(transport) {
94
+ let s = states.get(transport);
95
+ if (!s) {
96
+ s = { listenersInstalled: false };
97
+ states.set(transport, s);
98
+ }
99
+ return s;
100
+ }
101
+ function ensureListeners(transport) {
102
+ const s = getState(transport);
103
+ if (s.listenersInstalled)
104
+ return;
105
+ s.listenersInstalled = true;
106
+ transport.onMessage('judgment_ask', async (msg) => {
107
+ const frame = decodeFrame(msg.payload);
108
+ if (!frame)
109
+ return;
110
+ const ask = frame.payload;
111
+ await onAskReceived(transport, ask);
112
+ });
113
+ transport.onMessage('judgment_dissent', async (msg) => {
114
+ const frame = decodeFrame(msg.payload);
115
+ if (!frame)
116
+ return;
117
+ const dissent = frame.payload;
118
+ await onDissentReceived(transport, dissent);
119
+ });
120
+ transport.onMessage('judgment_align', async (msg) => {
121
+ const frame = decodeFrame(msg.payload);
122
+ if (!frame)
123
+ return;
124
+ const align = frame.payload;
125
+ await onAlignReceived(transport, align);
126
+ });
127
+ }
128
+ // ============================================================================
129
+ // 蒸馏 (dissent 生成) — 协议核心载荷
130
+ // ============================================================================
131
+ /**
132
+ * 基于本方判断力 + ask 内容, 蒸馏 3 个反方观点.
133
+ * 协议硬约束: 至少 3 个, 最多 5 个.
134
+ */
135
+ export async function distillDissent(ask) {
136
+ const allJudgments = await loadAllJudgments().catch(() => []);
137
+ const valueProfile = await getValueProfile('me').catch(() => null);
138
+ const relevant = await getRelevantValues(ask.decision, undefined).catch(() => []);
139
+ const valueHint = valueProfile ? [
140
+ `quality_focus=${valueProfile.quality_focus.toFixed(2)}`,
141
+ `efficiency_focus=${valueProfile.efficiency_focus.toFixed(2)}`,
142
+ `safety_focus=${valueProfile.safety_focus.toFixed(2)}`,
143
+ `collaboration_focus=${valueProfile.collaboration_focus.toFixed(2)}`,
144
+ `learning_focus=${valueProfile.learning_focus.toFixed(2)}`,
145
+ ].join(', ') : '';
146
+ const recentReasons = allJudgments
147
+ .filter((j) => j.reasons?.length > 0)
148
+ .slice(-10)
149
+ .flatMap((j) => j.reasons || [])
150
+ .slice(0, 20);
151
+ const prompt = `你是用户的判断力代理. 用户发起了一个决定: "${ask.decision}"
152
+ ${ask.context ? `\n补充: ${ask.context}` : ''}
153
+
154
+ [用户价值画像] ${valueHint}
155
+ [相关历史判断 (${relevant.length} 条)] ${relevant.slice(0, 3).map((j) => j.decision || '').join(' | ').slice(0, 300)}
156
+ [历史理由样本] ${recentReasons.join(' | ').slice(0, 400) || '(无)'}
157
+
158
+ 请基于上述判断力, 生成正好 3 个"反方观点" (用户应该考虑但可能忽略的风险/盲点).
159
+ 要求:
160
+ - 每个反方 1-2 句, 总长 < 80 字
161
+ - 必须是这个特定决定的"反对声音", 不是通用建议
162
+ - 第 1 个: 历史/经验类风险
163
+ - 第 2 个: 关系/信任类风险
164
+ - 第 3 个: 隐藏代价/时间类风险
165
+ - 用 JSON 数组返回: ["反方1", "反方2", "反方3"]`;
166
+ // LLM 调用 (OpenAI 兼容, 复用 p2p-chat-tools 的方式)
167
+ let dissents = [];
168
+ try {
169
+ const openaiKey = process.env.OPENAI_API_KEY;
170
+ if (!openaiKey)
171
+ throw new Error('no LLM key');
172
+ const openaiBase = process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1';
173
+ const openaiModel = process.env.OPENAI_MODEL || 'gpt-4';
174
+ const r = await fetch(`${openaiBase}/chat/completions`, {
175
+ method: 'POST',
176
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${openaiKey}` },
177
+ body: JSON.stringify({
178
+ model: openaiModel,
179
+ messages: [
180
+ { role: 'system', content: '你只输出 JSON 数组, 不要解释. 必须正好 3 个元素.' },
181
+ { role: 'user', content: prompt },
182
+ ],
183
+ temperature: 0.7,
184
+ max_tokens: 400,
185
+ }),
186
+ signal: AbortSignal.timeout(20000),
187
+ });
188
+ if (r.ok) {
189
+ const data = await r.json();
190
+ const text = (data.choices?.[0]?.message?.content || '').trim();
191
+ // 尝试提取 JSON 数组
192
+ const match = text.match(/\[[\s\S]*?\]/);
193
+ if (match) {
194
+ const parsed = JSON.parse(match[0]);
195
+ if (Array.isArray(parsed))
196
+ dissents = parsed.filter((s) => typeof s === 'string' && s.trim()).slice(0, 5);
197
+ }
198
+ }
199
+ }
200
+ catch (e) {
201
+ console.warn('[judgment-protocol] LLM distill failed:', e.message);
202
+ }
203
+ // Fallback: 启发式 — 从历史 judgment 抽 3 条理由做骨架
204
+ if (dissents.length < 3) {
205
+ const fallback = recentReasons.slice(0, 5);
206
+ while (dissents.length < 3 && fallback.length > 0) {
207
+ dissents.push(`[启发式] ${fallback.shift()}`);
208
+ }
209
+ }
210
+ while (dissents.length < 3) {
211
+ dissents.push(`[占位反方 ${dissents.length + 1}] 这个决定可能忽略的盲点 (LLM 不可用, 请人工补充)`);
212
+ }
213
+ return dissents.slice(0, 5);
214
+ }
215
+ // ============================================================================
216
+ // 协议 handler — 收到对方消息时
217
+ // ============================================================================
218
+ async function onAskReceived(transport, ask) {
219
+ console.log(`[judgment-protocol] ask 收到: "${ask.decision.slice(0, 40)}..." from ${ask.proposerNodeId.slice(0, 12)}`);
220
+ // 1) 落本地 chain (init open)
221
+ let chain = await readChain(ask.askId);
222
+ if (!chain) {
223
+ chain = { ask, dissents: [], aligns: [], status: 'open', createdAt: Date.now() };
224
+ await writeChain(chain);
225
+ }
226
+ judgmentEventBus.emit('judgment', { kind: 'ask_received', askId: ask.askId, fromNodeId: ask.proposerNodeId });
227
+ // 2) 自动蒸馏我方 3 反方, 作为 dissent 回发 (协议: ask 必触发 dissent)
228
+ const dissents = await distillDissent(ask);
229
+ const myNodeId = transport.getNodeId() || 'unknown';
230
+ const dissent = {
231
+ dissentId: crypto.randomUUID(),
232
+ askId: ask.askId,
233
+ fromNodeId: myNodeId,
234
+ dissents,
235
+ ts: Date.now(),
236
+ };
237
+ await sendDissentInternal(transport, ask.proposerNodeId, dissent);
238
+ }
239
+ async function onDissentReceived(transport, dissent) {
240
+ console.log(`[judgment-protocol] dissent 收到: ${dissent.dissents.length} 反方 for ask ${dissent.askId.slice(0, 8)} from ${dissent.fromNodeId.slice(0, 12)}`);
241
+ const chain = await readChain(dissent.askId);
242
+ if (!chain) {
243
+ console.warn(`[judgment-protocol] 收到 dissent 但找不到对应 ask ${dissent.askId}, 跳过`);
244
+ return;
245
+ }
246
+ // 去重 (同一节点可能重发)
247
+ if (!chain.dissents.some((d) => d.dissentId === dissent.dissentId)) {
248
+ chain.dissents.push(dissent);
249
+ await writeChain(chain);
250
+ }
251
+ judgmentEventBus.emit('judgment', { kind: 'dissent_received', dissentId: dissent.dissentId, askId: dissent.askId, fromNodeId: dissent.fromNodeId });
252
+ }
253
+ async function onAlignReceived(transport, align) {
254
+ console.log(`[judgment-protocol] align 收到: "${align.conclusion.slice(0, 40)}..." for ask ${align.askId.slice(0, 8)}`);
255
+ const chain = await readChain(align.askId);
256
+ if (!chain) {
257
+ console.warn(`[judgment-protocol] 收到 align 但找不到对应 ask ${align.askId}, 跳过`);
258
+ return;
259
+ }
260
+ if (!chain.aligns.some((a) => a.alignId === align.alignId)) {
261
+ chain.aligns.push(align);
262
+ chain.status = 'aligned';
263
+ chain.closedAt = Date.now();
264
+ await writeChain(chain);
265
+ }
266
+ judgmentEventBus.emit('judgment', { kind: 'align_received', alignId: align.alignId, askId: align.askId, fromNodeId: align.fromNodeId });
267
+ }
268
+ // ============================================================================
269
+ // 公共 API — 调用方 (CLI / Web / Skill) 用
270
+ // ============================================================================
271
+ export async function initJudgmentProtocol(transport = defaultIrohTransport) {
272
+ ensureListeners(transport);
273
+ await ensureDirs();
274
+ console.log('[judgment-protocol] initialized, chains dir =', chainsDir());
275
+ }
276
+ export async function sendAsk(peerDID, decision, opts = {}, transport = defaultIrohTransport) {
277
+ ensureListeners(transport);
278
+ const myNodeId = transport.getNodeId() || 'unknown';
279
+ const ask = {
280
+ askId: crypto.randomUUID(),
281
+ decision,
282
+ proposerNodeId: myNodeId,
283
+ proposerName: opts.proposerName,
284
+ context: opts.context?.slice(0, 500),
285
+ ts: Date.now(),
286
+ };
287
+ // 先落本地 chain (open)
288
+ await writeChain({ ask, dissents: [], aligns: [], status: 'open', createdAt: Date.now() });
289
+ // 上链
290
+ const ok = await transport.sendMessage(peerDID, 'judgment_ask', encodeAskFrame(ask));
291
+ console.log(`[judgment-protocol] ask -> ${peerDID.slice(0, 12)}: "${decision.slice(0, 40)}..." (${ok ? 'SENT' : 'FAIL'})`);
292
+ judgmentEventBus.emit('judgment', { kind: 'ask_sent', askId: ask.askId, peerDID });
293
+ return ask;
294
+ }
295
+ async function sendDissentInternal(transport, peerDID, dissent) {
296
+ // 落本地 chain
297
+ const chain = await readChain(dissent.askId);
298
+ if (chain && !chain.dissents.some((d) => d.dissentId === dissent.dissentId)) {
299
+ chain.dissents.push(dissent);
300
+ await writeChain(chain);
301
+ }
302
+ // 上链
303
+ const ok = await transport.sendMessage(peerDID, 'judgment_dissent', encodeDissentFrame(dissent));
304
+ console.log(`[judgment-protocol] dissent -> ${peerDID.slice(0, 12)}: ${dissent.dissents.length} 反方 (${ok ? 'SENT' : 'FAIL'})`);
305
+ judgmentEventBus.emit('judgment', { kind: 'dissent_sent', dissentId: dissent.dissentId, askId: dissent.askId, peerDID });
306
+ }
307
+ /**
308
+ * 显式补发 dissent (例如收到 ask 后 LLM 还没好, 后续可重发更准的).
309
+ * 协议上 dissent 在 ask 收到时自动回发, 这个 API 是给"主动想换反方"用的.
310
+ */
311
+ export async function sendDissent(peerDID, askId, dissents, transport = defaultIrohTransport) {
312
+ ensureListeners(transport);
313
+ if (dissents.length < 3 || dissents.length > 5) {
314
+ throw new Error(`协议硬约束: dissent 必须 3-5 个, 实际 ${dissents.length}`);
315
+ }
316
+ const myNodeId = transport.getNodeId() || 'unknown';
317
+ const dissent = {
318
+ dissentId: crypto.randomUUID(),
319
+ askId,
320
+ fromNodeId: myNodeId,
321
+ dissents,
322
+ ts: Date.now(),
323
+ };
324
+ await sendDissentInternal(transport, peerDID, dissent);
325
+ return dissent;
326
+ }
327
+ /**
328
+ * align — 显式协议消息, 必须基于 askId + 双方 dissentIds.
329
+ * 协议校验: basedOn.dissentIds 必须非空 (否则视为"未基于互见").
330
+ */
331
+ export async function sendAlign(peerDID, askId, conclusion, dissentIds, transport = defaultIrohTransport) {
332
+ ensureListeners(transport);
333
+ if (dissentIds.length === 0) {
334
+ throw new Error('协议硬约束: align 必须引用至少 1 条 dissent (否则不是基于反方互见)');
335
+ }
336
+ const myNodeId = transport.getNodeId() || 'unknown';
337
+ const align = {
338
+ alignId: crypto.randomUUID(),
339
+ askId,
340
+ fromNodeId: myNodeId,
341
+ conclusion,
342
+ basedOn: { askId, dissentIds },
343
+ ts: Date.now(),
344
+ };
345
+ // 落本地 chain
346
+ const chain = await readChain(askId);
347
+ if (chain) {
348
+ chain.aligns.push(align);
349
+ chain.status = 'aligned';
350
+ chain.closedAt = Date.now();
351
+ await writeChain(chain);
352
+ }
353
+ // 上链
354
+ const ok = await transport.sendMessage(peerDID, 'judgment_align', encodeAlignFrame(align));
355
+ console.log(`[judgment-protocol] align -> ${peerDID.slice(0, 12)}: "${conclusion.slice(0, 40)}..." (${ok ? 'SENT' : 'FAIL'})`);
356
+ judgmentEventBus.emit('judgment', { kind: 'align_sent', alignId: align.alignId, askId, peerDID });
357
+ return align;
358
+ }
359
+ /**
360
+ * reflect — 把整链 (ask + 所有 dissent + 所有 align) 沉淀成 1 个 HumanJudgment,
361
+ * 写进 ~/.bolloon/judgments/yaml/{askId}.yaml, 并调 storeHumanJudgment 注入判断力库.
362
+ * 这是协议的"复利起点": 下次 distillDissent 会看到这条.
363
+ */
364
+ export async function reflect(askId, transport = defaultIrohTransport) {
365
+ const chain = await readChain(askId);
366
+ if (!chain)
367
+ return null;
368
+ if (chain.status === 'reflected') {
369
+ console.warn(`[judgment-protocol] ask ${askId} 已 reflect 过, 跳过`);
370
+ return null;
371
+ }
372
+ // 1) 序列化整链成 yaml (手搓, 避免引 js-yaml 依赖)
373
+ const yamlLines = [];
374
+ yamlLines.push(`# bolloon judgment reflection`);
375
+ yamlLines.push(`# generated: ${new Date().toISOString()}`);
376
+ yamlLines.push(`askId: ${chain.ask.askId}`);
377
+ yamlLines.push(`decision: ${yamlEscape(chain.ask.decision)}`);
378
+ yamlLines.push(`proposer: ${chain.ask.proposerNodeId}`);
379
+ yamlLines.push(`createdAt: "${new Date(chain.createdAt).toISOString()}"`);
380
+ yamlLines.push(`status: reflected`);
381
+ yamlLines.push('');
382
+ yamlLines.push(`dissents:`);
383
+ for (const d of chain.dissents) {
384
+ yamlLines.push(` - from: ${d.fromNodeId}`);
385
+ yamlLines.push(` ts: "${new Date(d.ts).toISOString()}"`);
386
+ yamlLines.push(` points:`);
387
+ for (const p of d.dissents) {
388
+ yamlLines.push(` - ${yamlEscape(p)}`);
389
+ }
390
+ }
391
+ yamlLines.push('');
392
+ yamlLines.push(`aligns:`);
393
+ for (const a of chain.aligns) {
394
+ yamlLines.push(` - from: ${a.fromNodeId}`);
395
+ yamlLines.push(` ts: "${new Date(a.ts).toISOString()}"`);
396
+ yamlLines.push(` conclusion: ${yamlEscape(a.conclusion)}`);
397
+ yamlLines.push(` basedOnDissentIds: [${a.basedOn.dissentIds.join(', ')}]`);
398
+ }
399
+ yamlLines.push('');
400
+ await ensureDirs();
401
+ const yPath = path.join(yamlDir(), `${askId}.yaml`);
402
+ await fs.writeFile(yPath, yamlLines.join('\n'), 'utf-8');
403
+ // 2) 注入判断力库 (供下次 distillDissent 用)
404
+ let judgmentId = '';
405
+ try {
406
+ // 找出我方的 dissent (如果有) 作为本节点判断
407
+ const myNodeId = transport.getNodeId() || '';
408
+ const myDissent = chain.dissents.find((d) => d.fromNodeId === myNodeId);
409
+ const lastAlign = chain.aligns[chain.aligns.length - 1];
410
+ const stored = await storeHumanJudgment({
411
+ decision: `judgment-protocol: ${chain.ask.decision.slice(0, 80)}`,
412
+ reasoning: [
413
+ `ask: ${chain.ask.decision}`,
414
+ myDissent ? `my-dissent: ${myDissent.dissents.join(' / ')}` : '',
415
+ lastAlign ? `conclusion: ${lastAlign.conclusion}` : '',
416
+ ].filter(Boolean),
417
+ confidence: chain.aligns.length > 0 ? 0.8 : 0.5,
418
+ domain: 'judgment-protocol',
419
+ tags: ['judgment-protocol', `ask:${askId.slice(0, 8)}`],
420
+ source: 'judgment-protocol',
421
+ });
422
+ judgmentId = stored.id;
423
+ }
424
+ catch (e) {
425
+ console.warn('[judgment-protocol] storeHumanJudgment 失败 (yaml 已写):', e.message);
426
+ }
427
+ // 3) 更新 chain 状态
428
+ chain.status = 'reflected';
429
+ chain.closedAt = Date.now();
430
+ await writeChain(chain);
431
+ console.log(`[judgment-protocol] reflect 完成: askId=${askId.slice(0, 8)} yaml=${yPath}`);
432
+ judgmentEventBus.emit('judgment', { kind: 'reflected', askId, judgmentId });
433
+ return { judgmentId, yamlPath: yPath };
434
+ }
435
+ /** 读 ask 的整链 (UI / CLI 用) */
436
+ export async function getChain(askId) {
437
+ return readChain(askId);
438
+ }
439
+ /** 列所有 ask (按时间倒序) */
440
+ export async function listChains(limit = 50) {
441
+ await ensureDirs();
442
+ const files = (await fs.readdir(chainsDir()))
443
+ .filter((f) => f.endsWith('.json'))
444
+ .sort()
445
+ .reverse()
446
+ .slice(0, limit);
447
+ const out = [];
448
+ for (const f of files) {
449
+ try {
450
+ const buf = await fs.readFile(path.join(chainsDir(), f), 'utf-8');
451
+ out.push(JSON.parse(buf));
452
+ }
453
+ catch { }
454
+ }
455
+ return out;
456
+ }
457
+ // yaml 字符串转义: 包单引号, 单引号变两个
458
+ function yamlEscape(s) {
459
+ if (!s)
460
+ return "''";
461
+ // 单行无特殊字符 → 直接包单引号
462
+ if (!/['"\\:#\n]/.test(s) && s.length < 200)
463
+ return `'${s.replace(/'/g, "''")}'`;
464
+ // 多行 → block scalar
465
+ return `|\n ${s.replace(/\n/g, '\n ')}`;
466
+ }
467
+ // ============================================================================
468
+ // 顶层导出 (CLI / Web 调)
469
+ // ============================================================================
470
+ export const judgmentProtocol = {
471
+ init: initJudgmentProtocol,
472
+ sendAsk,
473
+ sendDissent,
474
+ sendAlign,
475
+ reflect,
476
+ distillDissent,
477
+ getChain,
478
+ listChains,
479
+ };
@@ -2945,6 +2945,7 @@ Workspace root folder: ${this.cwd}
2945
2945
  ];
2946
2946
  for (const p of jsonPatterns) {
2947
2947
  const m = content.match(p);
2948
+ console.log(`[parseToolCall DEBUG] jsonPattern match: ${m ? `name=${m[1]}, args=${JSON.stringify(m[2]).slice(0, 80)}` : 'null'}`);
2948
2949
  if (m) {
2949
2950
  const name = m[1];
2950
2951
  let args = {};
@@ -2955,6 +2956,14 @@ Workspace root folder: ${this.cwd}
2955
2956
  }
2956
2957
  }
2957
2958
  catch { /* 解析失败保持 args={} */ }
2959
+ // 2026-06-19: 自动 split command 字段 — LLM 常把 "git status" 整体当 command
2960
+ // shell_exec execute 要求 command="git", args="status" 拆分形式
2961
+ // 这里检测 command 含空格且 args 字段空, 自动按空格 split
2962
+ if (typeof args.command === 'string' && args.command.includes(' ') && !args.args) {
2963
+ const parts = args.command.split(/\s+/);
2964
+ args.command = parts[0];
2965
+ args.args = parts.slice(1).join(' ');
2966
+ }
2958
2967
  const resolved = this.resolveToolName(name);
2959
2968
  if (resolved) {
2960
2969
  return { name: resolved, args };
@@ -3010,6 +3019,9 @@ Workspace root folder: ${this.cwd}
3010
3019
  }
3011
3020
  }
3012
3021
  }
3022
+ // 2026-06-19 修: 先剥离 <think>...</think> 思考块, 避免旧正则 `<(\w+)>([\s\S]*?)<\/\1>` 优先匹配到 <think>
3023
+ // 之前 LLM 输出含 <think>...</think><invoke name="X">...</invoke> 时, <think> 先 match, name="think" 不在 tools → return null → 后续 <invoke> 没机会 match
3024
+ const strippedContent = content.replace(/<think>[\s\S]*?<\/think>/g, '');
3013
3025
  const patterns = [
3014
3026
  // 2026-06-19 修: minimax/Hermes 自闭合 XML 格式 <invoke name="X">...</invoke>
3015
3027
  // 实际 LLM 习惯: <invoke name="shell_exec"><command>sed</command><args>["-n","1060,1095p"]</args></invoke>
@@ -3036,10 +3048,11 @@ Workspace root folder: ${this.cwd}
3036
3048
  /<(\w+)>([\s\S]*?)<\/\1>/,
3037
3049
  ];
3038
3050
  for (const pattern of patterns) {
3039
- const match = content.match(pattern);
3051
+ const match = strippedContent.match(pattern);
3040
3052
  if (match) {
3041
3053
  const name = match[1];
3042
3054
  let args = {};
3055
+ console.log(`[parseToolCall DEBUG] pattern matched: name=${name}, rawArgs=${(match[2] || '').slice(0, 100)}`);
3043
3056
  const rawArgs = match[2] || '';
3044
3057
  if (rawArgs && rawArgs.trim().startsWith('{')) {
3045
3058
  // JSON 形式, 尝试解析
@@ -3095,6 +3108,12 @@ Workspace root folder: ${this.cwd}
3095
3108
  }
3096
3109
  }
3097
3110
  if (this.tools.has(name)) {
3111
+ // 2026-06-19: auto-split command field — shell_exec expects command='git', args='status' split
3112
+ if (typeof args.command === 'string' && args.command.includes(' ') && !args.args) {
3113
+ const parts = args.command.split(/\s+/);
3114
+ args.command = parts[0];
3115
+ args.args = parts.slice(1).join(' ');
3116
+ }
3098
3117
  return { name, args };
3099
3118
  }
3100
3119
  const resolved = this.resolveToolName(name);
@@ -51,6 +51,7 @@ export function startWebServer(preferredPort) {
51
51
  webServerProcess.stderr?.on('data', (data) => {
52
52
  stderrData += data.toString();
53
53
  process.stderr.write(`[WebServer ERR] ${data}`);
54
+ log(`[WebServer stderr] ${data.toString().trim()}`, 'warn');
54
55
  });
55
56
  webServerProcess.on('error', (err) => {
56
57
  log(`Web 服务器启动失败: ${err.message}`, 'error');
@@ -59,7 +60,7 @@ export function startWebServer(preferredPort) {
59
60
  webServerProcess.on('exit', (code, signal) => {
60
61
  log(`Web 服务器退出: code=${code} signal=${signal}`);
61
62
  if (code !== 0 && code !== null) {
62
- log(`Web 服务器 stderr: ${stderrData}`, 'error');
63
+ log(`Web 服务器 stderr: ${stderrData.slice(0, 500)}`, 'error');
63
64
  }
64
65
  webServerProcess = null;
65
66
  });
@@ -0,0 +1,49 @@
1
+ import UIKit
2
+ import Capacitor
3
+
4
+ @UIApplicationMain
5
+ class AppDelegate: UIResponder, UIApplicationDelegate {
6
+
7
+ var window: UIWindow?
8
+
9
+ func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
10
+ // Override point for customization after application launch.
11
+ return true
12
+ }
13
+
14
+ func applicationWillResignActive(_ application: UIApplication) {
15
+ // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
16
+ // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
17
+ }
18
+
19
+ func applicationDidEnterBackground(_ application: UIApplication) {
20
+ // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
21
+ // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
22
+ }
23
+
24
+ func applicationWillEnterForeground(_ application: UIApplication) {
25
+ // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
26
+ }
27
+
28
+ func applicationDidBecomeActive(_ application: UIApplication) {
29
+ // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
30
+ }
31
+
32
+ func applicationWillTerminate(_ application: UIApplication) {
33
+ // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
34
+ }
35
+
36
+ func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
37
+ // Called when the app was launched with a url. Feel free to add additional processing here,
38
+ // but if you want the App API to support tracking app url opens, make sure to keep this call
39
+ return ApplicationDelegateProxy.shared.application(app, open: url, options: options)
40
+ }
41
+
42
+ func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
43
+ // Called when the app was launched with an activity, including Universal Links.
44
+ // Feel free to add additional processing here, but if you want the App API to support
45
+ // tracking app url opens, make sure to keep this call
46
+ return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
47
+ }
48
+
49
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "images" : [
3
+ {
4
+ "filename" : "AppIcon-512@2x.png",
5
+ "idiom" : "universal",
6
+ "platform" : "ios",
7
+ "size" : "1024x1024"
8
+ }
9
+ ],
10
+ "info" : {
11
+ "author" : "xcode",
12
+ "version" : 1
13
+ }
14
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "info" : {
3
+ "version" : 1,
4
+ "author" : "xcode"
5
+ }
6
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "images" : [
3
+ {
4
+ "idiom" : "universal",
5
+ "filename" : "splash-2732x2732-2.png",
6
+ "scale" : "1x"
7
+ },
8
+ {
9
+ "idiom" : "universal",
10
+ "filename" : "splash-2732x2732-1.png",
11
+ "scale" : "2x"
12
+ },
13
+ {
14
+ "idiom" : "universal",
15
+ "filename" : "splash-2732x2732.png",
16
+ "scale" : "3x"
17
+ }
18
+ ],
19
+ "info" : {
20
+ "version" : 1,
21
+ "author" : "xcode"
22
+ }
23
+ }