@bolloon/bolloon-agent 0.6.4 → 0.6.6
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/chain/chain-settlement.js +0 -1
- package/dist/agents/chat-segmenter.js +4 -16
- package/dist/agents/contacts/human-directory.js +155 -0
- package/dist/agents/contacts/tools.js +79 -0
- package/dist/agents/experience-review.js +0 -3
- package/dist/agents/goal-flywheel/goal-change.js +3 -4
- package/dist/agents/goal-resume.js +0 -4
- package/dist/agents/intent-classifier.js +1 -1
- package/dist/agents/iteration-budget.js +12 -0
- package/dist/agents/knowledge-organizer.js +0 -2
- package/dist/agents/lesson-to-skill.js +0 -1
- package/dist/agents/loop-review.js +2 -2
- package/dist/agents/memory-store.js +359 -0
- package/dist/agents/opportunity-inbox.js +147 -0
- package/dist/agents/opportunity-match.js +48 -7
- package/dist/agents/opportunity-web-search.js +222 -0
- package/dist/agents/p2p-chat-tools.js +1 -2
- package/dist/agents/parse-tool-call.js +9 -22
- package/dist/agents/pi-harness.js +1 -2
- package/dist/agents/pi-sdk-tools.js +180 -5
- package/dist/agents/pi-sdk.js +143 -9
- package/dist/agents/process-runner.js +28 -9
- package/dist/agents/react-loop.js +9 -17
- package/dist/agents/runner-resolver.js +4 -3
- package/dist/agents/skill-loader.js +22 -15
- package/dist/agents/skills-manager.js +1 -25
- package/dist/agents/workflow-pivot-loop.js +152 -61
- package/dist/agents/world-watcher.js +30 -1
- package/dist/bootstrap/context-collector.js +2 -4
- package/dist/bootstrap/context-hierarchy.js +13 -1
- package/dist/cli/ink-app.js +44 -17
- package/dist/cli/intent-command.js +22 -12
- package/dist/cli/loading-tui.js +8 -2
- package/dist/cli/log-gate.js +40 -8
- package/dist/cli/mention-data.js +1 -0
- package/dist/cli/reply-hygiene.js +22 -0
- package/dist/cli/trace-line.js +61 -2
- package/dist/electron-build/agents/agent-gateway.js.map +1 -1
- package/dist/electron-build/agents/chain/chain-indexer.js.map +1 -1
- package/dist/electron-build/agents/chain/chain-settlement.js +0 -1
- package/dist/electron-build/agents/chain/chain-settlement.js.map +1 -1
- package/dist/electron-build/agents/contacts/chain.js.map +1 -1
- package/dist/electron-build/agents/contacts/human-directory.js +204 -0
- package/dist/electron-build/agents/contacts/human-directory.js.map +1 -0
- package/dist/electron-build/agents/contacts/policy.js.map +1 -1
- package/dist/electron-build/agents/contacts/tools.js +112 -0
- package/dist/electron-build/agents/contacts/tools.js.map +1 -1
- package/dist/electron-build/agents/experience-review.js +0 -3
- package/dist/electron-build/agents/experience-review.js.map +1 -1
- package/dist/electron-build/agents/external-events.js.map +1 -1
- package/dist/electron-build/agents/goal-flywheel/goal-change.js +3 -4
- package/dist/electron-build/agents/goal-flywheel/goal-change.js.map +1 -1
- package/dist/electron-build/agents/goal-flywheel-wiring.js.map +1 -1
- package/dist/electron-build/agents/goal-resume.js +0 -4
- package/dist/electron-build/agents/goal-resume.js.map +1 -1
- package/dist/electron-build/agents/intent-classifier.js +1 -1
- package/dist/electron-build/agents/lesson-to-skill.js +0 -1
- package/dist/electron-build/agents/lesson-to-skill.js.map +1 -1
- package/dist/electron-build/agents/loop-review.js +2 -2
- package/dist/electron-build/agents/loop-review.js.map +1 -1
- package/dist/electron-build/agents/memory-recall.js.map +1 -1
- package/dist/electron-build/agents/memory-store.js +404 -0
- package/dist/electron-build/agents/memory-store.js.map +1 -0
- package/dist/electron-build/agents/parse-tool-call.js +9 -22
- package/dist/electron-build/agents/parse-tool-call.js.map +1 -1
- package/dist/electron-build/agents/pi-harness.js +1 -2
- package/dist/electron-build/agents/pi-harness.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk-tools.js +180 -5
- package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk.js +143 -9
- package/dist/electron-build/agents/pi-sdk.js.map +1 -1
- package/dist/electron-build/agents/pre-tool-validator.js.map +1 -1
- package/dist/electron-build/agents/process-runner.js +28 -9
- package/dist/electron-build/agents/process-runner.js.map +1 -1
- package/dist/electron-build/agents/runner-resolver.js +4 -3
- package/dist/electron-build/agents/runner-resolver.js.map +1 -1
- package/dist/electron-build/agents/skill-loader.js +22 -16
- package/dist/electron-build/agents/skill-loader.js.map +1 -1
- package/dist/electron-build/agents/skill-supervisor-link.js.map +1 -1
- package/dist/electron-build/agents/skills-manager.js +0 -24
- package/dist/electron-build/agents/skills-manager.js.map +1 -1
- package/dist/electron-build/agents/task/task-runner.js.map +1 -1
- package/dist/electron-build/agents/workflow-engine.js.map +1 -1
- package/dist/electron-build/agents/workflow-pivot-loop.js +155 -62
- package/dist/electron-build/agents/workflow-pivot-loop.js.map +1 -1
- package/dist/electron-build/agents/x402/goal-run-bridge.js.map +1 -1
- package/dist/electron-build/bollharness-integration/gate-state-machine.js.map +1 -1
- package/dist/electron-build/bootstrap/context-collector.js +2 -4
- package/dist/electron-build/bootstrap/context-collector.js.map +1 -1
- package/dist/electron-build/bootstrap/context-hierarchy.js +13 -1
- package/dist/electron-build/bootstrap/context-hierarchy.js.map +1 -1
- package/dist/electron-build/electron-preload.js.map +1 -1
- package/dist/electron-build/external-engines/delegate.js.map +1 -1
- package/dist/electron-build/kernel/channel-actor.js +31 -2
- package/dist/electron-build/kernel/channel-actor.js.map +1 -1
- package/dist/electron-build/kernel/model-runtime.js +8 -1
- package/dist/electron-build/kernel/model-runtime.js.map +1 -1
- package/dist/electron-build/llm/config-store.js +20 -0
- package/dist/electron-build/llm/config-store.js.map +1 -1
- package/dist/electron-build/llm/embedding-adapter.js +75 -0
- package/dist/electron-build/llm/embedding-adapter.js.map +1 -0
- package/dist/electron-build/llm/pi-ai.js +96 -35
- package/dist/electron-build/llm/pi-ai.js.map +1 -1
- package/dist/electron-build/network/agent-network.js.map +1 -1
- package/dist/electron-build/orbitdb/memory-sync.js +76 -0
- package/dist/electron-build/orbitdb/memory-sync.js.map +1 -0
- package/dist/electron-build/pi-ecosystem-judgment/adaptive-scan.js.map +1 -1
- package/dist/electron-build/security/tool-gate.js +2 -0
- package/dist/electron-build/security/tool-gate.js.map +1 -1
- package/dist/electron-build/setup/setup-store.js +34 -6
- package/dist/electron-build/setup/setup-store.js.map +1 -1
- package/dist/electron-build/social/heartbeat.js +18 -5
- package/dist/electron-build/social/heartbeat.js.map +1 -1
- package/dist/electron-build/utils/update-manager.js.map +1 -1
- package/dist/index.js +250 -12
- package/dist/ios/bolloon-web.json +3 -3
- package/dist/ios/client.js +150 -113
- package/dist/ios/icons/icon-1400x1400.png +0 -0
- package/dist/ios/icons/icon-1400x1400.webp +0 -0
- package/dist/ios/index.html +106 -107
- package/dist/ios/mobile-agent.js +346 -45
- package/dist/ios/mobile-core.js +7303 -4590
- package/dist/ios/mobile-harness.js +1004 -0
- package/dist/ios/mobile-orbitdb.js +194 -0
- package/dist/ios/mobile-persona.js +80 -0
- package/dist/ios/mobile-privacy.js +2 -3
- package/dist/ios/mobile.css +286 -21
- package/dist/ios/mobile.html +106 -107
- package/dist/ios/mobile.js +300 -132
- package/dist/ios/routes-world.js +58 -1
- package/dist/ios/server.js +3 -1
- package/dist/ios/style.css +49 -2
- package/dist/ios/sw.js +1 -1
- package/dist/ios/ui/message-renderer.js +4 -18
- package/dist/ios/ui/step-timeline.js +14 -1
- package/dist/kernel/channel-actor.js +30 -2
- package/dist/kernel/gate-scan.js +1 -1
- package/dist/kernel/index.js +33 -0
- package/dist/kernel/model-runtime.js +8 -1
- package/dist/kernel/plan.js +3 -3
- package/dist/kernel/roster.js +3 -3
- package/dist/llm/config-store.js +20 -0
- package/dist/llm/embedding-adapter.js +69 -0
- package/dist/llm/pi-ai.js +94 -34
- package/dist/orbitdb/memory-sync.js +70 -0
- package/dist/pi-ecosystem-judgment/adaptive-scan.js +1 -1
- package/dist/security/tool-gate.js +2 -0
- package/dist/setup/setup-store.js +34 -6
- package/dist/social/heartbeat.js +18 -5
- package/dist/web/bolloon-web.json +3 -3
- package/dist/web/client.js +150 -113
- package/dist/web/icons/icon-1400x1400.png +0 -0
- package/dist/web/icons/icon-1400x1400.webp +0 -0
- package/dist/web/index.html +52 -59
- package/dist/web/mobile-agent.js +351 -45
- package/dist/web/mobile-contacts.js +0 -4
- package/dist/web/mobile-core.js +7430 -4590
- package/dist/web/mobile-harness.js +1004 -0
- package/dist/web/mobile-orbitdb.js +194 -0
- package/dist/web/mobile-persona.js +80 -0
- package/dist/web/mobile-privacy.js +2 -3
- package/dist/web/mobile.css +286 -21
- package/dist/web/mobile.html +121 -107
- package/dist/web/mobile.js +409 -136
- package/dist/web/routes-contacts.js +94 -1
- package/dist/web/routes-world.js +58 -1
- package/dist/web/server.js +4 -2
- package/dist/web/style.css +49 -2
- package/dist/web/sw.js +1 -1
- package/dist/web/ui/message-renderer.js +4 -18
- package/dist/web/ui/step-timeline.js +14 -1
- package/package.json +3 -2
- package/scripts/build-web.ts +16 -1
|
@@ -56,6 +56,8 @@ export function canonicalizeToolCallArguments(argStr) {
|
|
|
56
56
|
}
|
|
57
57
|
/** 2026-08-11 (Hermes build_worker_context 全限幅): 工具输出进 LLM 上下文的上限 */
|
|
58
58
|
export const TOOL_RESULT_MAX_CHARS = 12_000;
|
|
59
|
+
/** 2026-10-12: 单工具执行超时 (慢工具不再拖死整轮; 超时 → 失败结果 + 续跑提示给 LLM 下轮决策) */
|
|
60
|
+
export const TOOL_EXEC_TIMEOUT_MS = (typeof process !== 'undefined' && Number(process.env.BOLLOON_TOOL_TIMEOUT_MS)) || 120_000;
|
|
59
61
|
/** 截断工具输出 (纯函数, 可单测): 超限 → 保留前 max + 可见省略标记 */
|
|
60
62
|
export function truncateToolOutput(output, max = TOOL_RESULT_MAX_CHARS) {
|
|
61
63
|
const s = output ?? '';
|
|
@@ -63,6 +65,34 @@ export function truncateToolOutput(output, max = TOOL_RESULT_MAX_CHARS) {
|
|
|
63
65
|
return s;
|
|
64
66
|
return s.slice(0, max) + `\n… [truncated, ${s.length - max} chars omitted]`;
|
|
65
67
|
}
|
|
68
|
+
/** 2026-10-12 (leo: 「每个命令后面放一个命令执行时间的读秒」): 毫秒 → 人读耗时串 (纯函数, 可单测) */
|
|
69
|
+
export function formatDuration(ms) {
|
|
70
|
+
if (!Number.isFinite(ms) || ms < 0)
|
|
71
|
+
return '(耗时未知)';
|
|
72
|
+
if (ms < 1000)
|
|
73
|
+
return `${ms}ms`;
|
|
74
|
+
const s = ms / 1000;
|
|
75
|
+
if (s < 60)
|
|
76
|
+
return `${s.toFixed(1)}s`;
|
|
77
|
+
const m = Math.floor(s / 60);
|
|
78
|
+
const rem = Math.round(s % 60);
|
|
79
|
+
return `${m}m${rem}s`;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* 2026-10-12 (leo: 「对于 terminal 执行的命令, 你可以提供一个压缩的摘要」):
|
|
83
|
+
* 工具输出进 UI 前的摘要 (纯函数, 可单测) —— 只压 `terminal`/`shell` 类长输出:
|
|
84
|
+
* 折叠空白、截断到 ~300 字符、保留头部 (命令结果的开头最有信息量)。
|
|
85
|
+
* 其它工具原样返回 (read_file 等短输出没必要压)。
|
|
86
|
+
*/
|
|
87
|
+
export function summarizeToolOutput(toolName, output) {
|
|
88
|
+
const s = output ?? '';
|
|
89
|
+
if (!/terminal|shell|bash|exec/i.test(toolName))
|
|
90
|
+
return s;
|
|
91
|
+
const collapsed = s.replace(/[ \t]+/g, ' ').replace(/\n{3,}/g, '\n\n').trim();
|
|
92
|
+
if (collapsed.length <= 300)
|
|
93
|
+
return collapsed;
|
|
94
|
+
return collapsed.slice(0, 300) + `\n… [摘要, 原输出 ${s.length} 字符]`;
|
|
95
|
+
}
|
|
66
96
|
/**
|
|
67
97
|
* Analyze input to determine task complexity
|
|
68
98
|
*/
|
|
@@ -386,24 +416,14 @@ export class WorkflowPivotLoop {
|
|
|
386
416
|
const nativeTools = this.nativeToolCallsToDefinitions(llmResponse.toolCalls || []);
|
|
387
417
|
const pendingTools = nativeTools.length > 0 ? nativeTools : this.extractPendingToolUses(reply);
|
|
388
418
|
if (pendingTools.length === 0) {
|
|
389
|
-
// 2026-
|
|
390
|
-
//
|
|
391
|
-
//
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
response = cleaned || reply;
|
|
395
|
-
this.emit({
|
|
396
|
-
type: 'status', internal: true, // K4-B: 循环遥测 = 内部运行日志 (口径同老 loop)
|
|
397
|
-
content: `✅ 检测到 <final gen> 结束标记, 立即退出 (iter=${this.state.iteration})`,
|
|
398
|
-
tool: 'system',
|
|
399
|
-
});
|
|
400
|
-
return this.createResult(true, response, 'final_gen_marker');
|
|
401
|
-
}
|
|
419
|
+
// 2026-10-12 (leo 拍板: 全项目去掉 <final gen> 截止符):
|
|
420
|
+
// 旧实现这里检测 `<final gen>` 标记 → 立即 break 退出 —— 模型在中间轮次
|
|
421
|
+
// echo 出 "> <final gen>" 类文本 (思考引用/复述契约) 就被正则命中, 循环提前截止,
|
|
422
|
+
// 正是「中间生成了内容, 没办法衔接上就停止了」的根因之一。
|
|
423
|
+
// 现在循环停止只剩: 质量达标 / 连续无进展 / 用户中止 (leo 确认的三条路)。
|
|
402
424
|
// Check if the reply contains tool call intent but couldn't be parsed
|
|
403
|
-
// 2026-07-06: 排除合法的 思考/<final gen> 这些 tag (它们是 sentinel 不是 tool call)
|
|
404
425
|
const cleanedForIntentCheck = reply
|
|
405
|
-
.replace(
|
|
406
|
-
.replace(/<\/?think(?:ing)?>/gi, ''); // 移除
|
|
426
|
+
.replace(/<\/?think(?:ing)?>/gi, ''); // 移除 think 块
|
|
407
427
|
const containsToolCallIntent = cleanedForIntentCheck.includes('调用工具') || cleanedForIntentCheck.includes('tool(') ||
|
|
408
428
|
cleanedForIntentCheck.includes('使用工具') || cleanedForIntentCheck.includes('需要获取') || cleanedForIntentCheck.includes('需要查看') ||
|
|
409
429
|
cleanedForIntentCheck.includes('tool =>') || cleanedForIntentCheck.includes('[TOOL_CALL]') ||
|
|
@@ -421,6 +441,12 @@ export class WorkflowPivotLoop {
|
|
|
421
441
|
}
|
|
422
442
|
// No pending tool uses - this is a normal completion
|
|
423
443
|
this.state.pendingToolUses = [];
|
|
444
|
+
// 2026-10-12 (leo: 「任务进行到一半就卡住了」): **0 工具轮也要把回复写进历史**。
|
|
445
|
+
// 旧代码只在工具执行分支 push assistant —— 纯回复轮 (模型给最终回答) 不进 history,
|
|
446
|
+
// 于是: ① 质量不达标被打回重跑时, 模型下一轮**看不到自己上轮说过什么**;
|
|
447
|
+
// ② 循环因无进展退出时, 兜底取 `lastAssistant?.content` 是**最后一次工具轮**的
|
|
448
|
+
// 空 content ⇒ 屏上 `│ │` 空框。现在: 每轮回复都进历史, 兜底能拿到真正的最终答复。
|
|
449
|
+
this.messageHistory.push({ role: 'assistant', content: reply });
|
|
424
450
|
// Evaluate quality before accepting
|
|
425
451
|
const quality = this.evaluateQuality(reply);
|
|
426
452
|
this.state.qualityScores.push(quality);
|
|
@@ -429,32 +455,38 @@ export class WorkflowPivotLoop {
|
|
|
429
455
|
content: `✅ 检测到最终回复 (质量: ${(quality * 10).toFixed(1)}/10)`,
|
|
430
456
|
tool: 'system'
|
|
431
457
|
});
|
|
432
|
-
//
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
458
|
+
// 2026-10-12 (leo: 「循环跑 1 次就截止, 要能多轮」): 质量未达标时旧代码在
|
|
459
|
+
// iter >= minIterations (默认 2) 就**直接收下** (no_pending_tools) —— 长任务
|
|
460
|
+
// 经常第 2 轮就给出不完整回答被收了, 正是「中间生成内容但衔接不上就停止」。
|
|
461
|
+
// 现在: 质量未达标一律**注入续跑提示 + 继续循环**, 由 maxConsecutiveNoProgress 兜底
|
|
462
|
+
// (真有进展的会持续往下, 空转的到阈值才停) —— 该停才停, 不是早停。
|
|
463
|
+
if (quality < effectiveConfig.qualityThreshold) {
|
|
464
|
+
this.pushContinuationHint(`上一轮回答质量评测 ${(quality * 10).toFixed(1)}/${(effectiveConfig.qualityThreshold * 10).toFixed(1)}, 未达完成标准 — 请对照用户需求检查是否还有未完成的子目标, 需要工具就继续调用, 全部完成再给出完整最终回答.`);
|
|
465
|
+
this.state.consecutiveNoProgress++;
|
|
466
|
+
this.emit({
|
|
467
|
+
type: 'status', internal: true, // K4-B: 循环遥测 = 内部运行日志 (口径同老 loop)
|
|
468
|
+
content: `📊 质量未达标 (${(quality * 10).toFixed(1)}/${(effectiveConfig.qualityThreshold * 10).toFixed(1)}),注入续跑提示,继续循环`,
|
|
469
|
+
tool: 'system'
|
|
470
|
+
});
|
|
471
|
+
continue;
|
|
442
472
|
}
|
|
443
|
-
//
|
|
444
|
-
|
|
445
|
-
this.
|
|
446
|
-
type: 'status', internal: true, // K4-B: 循环遥测 = 内部运行日志 (口径同老 loop)
|
|
447
|
-
content: `📊 质量未达标 (${(quality * 10).toFixed(1)}/${(effectiveConfig.qualityThreshold * 10).toFixed(1)}),继续循环`,
|
|
448
|
-
tool: 'system'
|
|
449
|
-
});
|
|
450
|
-
continue;
|
|
473
|
+
// Quality met — accept
|
|
474
|
+
response = reply;
|
|
475
|
+
return this.createResult(true, reply, 'quality_threshold_met');
|
|
451
476
|
}
|
|
452
477
|
// We have pending tool uses - execute them
|
|
453
478
|
this.state.pendingToolUses = pendingTools;
|
|
454
479
|
this.state.lastMeaningfulWork = this.state.iteration;
|
|
455
480
|
this.state.consecutiveNoProgress = 0;
|
|
456
|
-
|
|
481
|
+
// 2026-10-12 (leo: 「运行工具的时候没有在后台运行, 花了太久, 把时间给 pass 了」):
|
|
482
|
+
// 旧实现 for...of 串行 await + **无超时** —— 一个慢工具 (网络/重 IO) 拖住整轮,
|
|
483
|
+
// 后面的工具全被阻塞, 轮次预算被耗在等待上。
|
|
484
|
+
// 现在: ① 同一轮的多个工具**并行**执行 (老 loop 早已 Promise.all, pivot 是行为退化);
|
|
485
|
+
// ② 每个工具加超时包装 (默认 120s, BOLLOON_TOOL_TIMEOUT_MS 可调) —— 超时返回
|
|
486
|
+
// 失败结果 + 续跑提示, 让 LLM 下轮决定换法/重试, 而不是无限挂起。
|
|
487
|
+
const execTool = async (toolCall) => {
|
|
457
488
|
this.state.toolCallsCount++;
|
|
489
|
+
const toolT0 = Date.now(); // 2026-10-12: 单工具耗时 (step_done 读秒用)
|
|
458
490
|
const tool = this.tools.get(toolCall.name);
|
|
459
491
|
if (!tool) {
|
|
460
492
|
this.emit({
|
|
@@ -478,7 +510,7 @@ export class WorkflowPivotLoop {
|
|
|
478
510
|
role: 'tool',
|
|
479
511
|
content: JSON.stringify({ success: false, error: `Unknown tool: ${toolCall.name}` })
|
|
480
512
|
});
|
|
481
|
-
|
|
513
|
+
return;
|
|
482
514
|
}
|
|
483
515
|
this.emit({
|
|
484
516
|
type: 'tool',
|
|
@@ -515,18 +547,38 @@ export class WorkflowPivotLoop {
|
|
|
515
547
|
// **fail-closed**: 端口抛错 ⇒ 记拒执行文案且**绝不回落** `tool.execute` ——
|
|
516
548
|
// 门坏掉若静默变成"没门"(旧实现那两条 fail-open 路径), 比没有门更危险。
|
|
517
549
|
let result;
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
550
|
+
const executeFn = this.config.guardedExecute
|
|
551
|
+
? () => this.config.guardedExecute(tool, toolCall.args ?? {})
|
|
552
|
+
: () => tool.execute(toolCall.args ?? {});
|
|
553
|
+
// 2026-10-12: 超时包装 — 慢工具不再拖死整轮 (失败结果给 LLM 下轮决策)
|
|
554
|
+
const toolTimeoutMs = TOOL_EXEC_TIMEOUT_MS;
|
|
555
|
+
// 2026-10-13 (leo: 「发送消息过程中的打断和衔接整个流程」): 工具执行阶段也要响应 abort —
|
|
556
|
+
// 之前 Promise.race 只有 executeFn + 超时两路, Ctrl+C 打断要等工具跑完/超时 (最长 120s) 才生效。
|
|
557
|
+
// 现在加第三路 abort: signal 触发立即返回"已中断"结果 → while 顶部 signal.aborted 检查 break。
|
|
558
|
+
const abortWait = new Promise((resolve) => {
|
|
559
|
+
const onAbort = () => resolve({ success: false, error: `已中断: ${toolCall.name} 被打断 (Ctrl+C)` });
|
|
560
|
+
if (signal?.aborted) {
|
|
561
|
+
onAbort();
|
|
562
|
+
return;
|
|
521
563
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
564
|
+
signal?.addEventListener?.('abort', onAbort, { once: true });
|
|
565
|
+
});
|
|
566
|
+
result = await Promise.race([
|
|
567
|
+
(async () => {
|
|
568
|
+
try {
|
|
569
|
+
return await executeFn();
|
|
570
|
+
}
|
|
571
|
+
catch (guardErr) {
|
|
572
|
+
const msg = String(guardErr?.message ?? guardErr);
|
|
573
|
+
return { success: false, error: `拒绝: [harness-error] ${msg}` };
|
|
574
|
+
}
|
|
575
|
+
})(),
|
|
576
|
+
new Promise((resolve) => setTimeout(() => {
|
|
577
|
+
resolve({ success: false, error: `工具超时 (${toolTimeoutMs}ms): ${toolCall.name} 未在时限内返回` });
|
|
578
|
+
}, toolTimeoutMs).unref?.()),
|
|
579
|
+
abortWait,
|
|
580
|
+
]);
|
|
581
|
+
const toolCostMs = Date.now() - toolT0; // 2026-10-12: 实际耗时 (含等待)
|
|
530
582
|
// 2026-08-11: 输出过大 → 续跑提示 (Hermes dropped-tools continuation 模式),
|
|
531
583
|
// 上下文后续可能被压缩, 提醒 LLM 需要精确数据时缩小范围重查
|
|
532
584
|
const outLen = (result.output || '').length;
|
|
@@ -537,19 +589,25 @@ export class WorkflowPivotLoop {
|
|
|
537
589
|
this.emit({
|
|
538
590
|
type: result.success ? 'step_done' : 'step_error',
|
|
539
591
|
content: result.success
|
|
540
|
-
? `${toolCall.name}
|
|
541
|
-
: `${toolCall.name} 失败: ${result.error}`,
|
|
592
|
+
? `${toolCall.name} 成功 · ${formatDuration(toolCostMs)}`
|
|
593
|
+
: `${toolCall.name} 失败: ${result.error}`, // 失败文案带原因即可, 耗时由 UI 从 durationMs 读
|
|
542
594
|
tool: toolCall.name,
|
|
543
595
|
success: result.success,
|
|
544
|
-
output: result.output,
|
|
596
|
+
output: summarizeToolOutput(toolCall.name, result.output),
|
|
545
597
|
error: result.error,
|
|
598
|
+
durationMs: toolCostMs,
|
|
546
599
|
});
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
600
|
+
// 2026-10-12 (leo: 「✅ read_file 成功 不用发出来」):
|
|
601
|
+
// 成功不再发 `✅ X 成功` 状态行 —— step_done 已带 工具名+耗时+摘要, 重复的
|
|
602
|
+
// 成功汇报纯属噪音 (用户原话: 「这种执行工具成功的汇报不用发出来这个 log,
|
|
603
|
+
// 只需要把工具执行的记录写清楚就行了」)。**失败保留** (`❌` error 事件,
|
|
604
|
+
// 需人介入的信号不许吞)。
|
|
605
|
+
if (!result.success) {
|
|
606
|
+
this.emit({
|
|
607
|
+
type: 'error',
|
|
608
|
+
content: `❌ ${toolCall.name} 失败: ${result.error}`
|
|
609
|
+
});
|
|
610
|
+
}
|
|
553
611
|
this.messageHistory.push({
|
|
554
612
|
role: 'assistant',
|
|
555
613
|
content: reply,
|
|
@@ -574,9 +632,29 @@ export class WorkflowPivotLoop {
|
|
|
574
632
|
content: JSON.stringify({ success: false, error: String(execError) })
|
|
575
633
|
});
|
|
576
634
|
}
|
|
577
|
-
}
|
|
635
|
+
};
|
|
636
|
+
await Promise.all(pendingTools.map(execTool));
|
|
578
637
|
}
|
|
579
638
|
catch (error) {
|
|
639
|
+
// 2026-10-13 (leo: 「多步执行的任务会返回空, 直接回复的不会」): 根因是**模型调用超时杀死循环**。
|
|
640
|
+
// 多步任务工具多轮后上下文膨胀 (40KB+), kernel 模型调用 30s 硬超时抛 ModelTimeoutError,
|
|
641
|
+
// 旧代码一律当「循环崩溃」处理 (return createResult(false, response, 'error')) ⇒
|
|
642
|
+
// 工具全干完、response 还是空 → 屏上 `│ │` 空框。
|
|
643
|
+
// 修复: 超时 = 供应商慢, 不是循环崩。emit 状态 + continue 重试下一轮;
|
|
644
|
+
// 真卡死由 maxConsecutiveNoProgress 兜底 (每条路都有终点, 唯独不该有「白干+空框」)。
|
|
645
|
+
const isTimeout = error instanceof Error && (error.name === 'ModelTimeoutError' || /模型调用超时/.test(error.message));
|
|
646
|
+
if (isTimeout) {
|
|
647
|
+
this.state.consecutiveNoProgress++;
|
|
648
|
+
this.emit({
|
|
649
|
+
type: 'status', internal: true, // K4-B: 循环遥测 = 内部运行日志 (口径同老 loop)
|
|
650
|
+
content: `⏳ 模型调用超时 (${String(error.message).replace(/^模型调用超时\s*\(/, '').replace(/\)$/, '')}), 重试下一轮 (连续 ${this.state.consecutiveNoProgress}/${effectiveConfig.maxConsecutiveNoProgress} 次则停)`,
|
|
651
|
+
tool: 'system',
|
|
652
|
+
});
|
|
653
|
+
// 2026-10-13 (leo: 「可以不用超时截止来确定时间周期, 有时候任务需要时间会比较久」):
|
|
654
|
+
// 超时只是这一轮没来得及, 任务该做多久做多久 —— **不**注入「快点给个回答」的收敛提示
|
|
655
|
+
// (那会让模型在任务没做完时草草收尾)。继续推进, 由质量门/无进展兜底决定什么时候算完。
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
580
658
|
this.emit({
|
|
581
659
|
type: 'error',
|
|
582
660
|
content: `❌ 循环异常: ${error}`
|
|
@@ -618,12 +696,18 @@ export class WorkflowPivotLoop {
|
|
|
618
696
|
}
|
|
619
697
|
/**
|
|
620
698
|
* Adapt configuration based on task profile
|
|
699
|
+
*
|
|
700
|
+
* 2026-10-12 (leo: 「工具轮次到了 10 次就截止了」): 旧实现用
|
|
701
|
+
* `Math.min(config.maxIterations, profile.suggestedMaxIterations)` 压迭代上限、
|
|
702
|
+
* `Math.min(config.maxTokenBudget, profile.tokenBudget)` 压 token 预算 ——
|
|
703
|
+
* simple 任务被压到 **10 iter + 800~2400 token**, 而 systemPrompt 本身就 40KB+,
|
|
704
|
+
* 第一次迭代 totalTokens 就超支 → 循环立刻 break, 长任务根本跑不完。
|
|
705
|
+
* 上限保留 createDefaultPivotConfig 的 200/1000/2000 (本来就注释「跑到 N 次也不报超时」),
|
|
706
|
+
* 由质量门 / maxConsecutiveNoProgress / user abort 兜底 —— 循环持久, 该停才停。
|
|
621
707
|
*/
|
|
622
708
|
adaptConfigForTask(profile) {
|
|
623
709
|
return {
|
|
624
710
|
...this.config,
|
|
625
|
-
maxIterations: Math.min(this.config.maxIterations, profile.suggestedMaxIterations),
|
|
626
|
-
maxTokenBudget: Math.min(this.config.maxTokenBudget, profile.tokenBudget),
|
|
627
711
|
complexity: profile.complexity
|
|
628
712
|
};
|
|
629
713
|
}
|
|
@@ -847,6 +931,13 @@ export class WorkflowPivotLoop {
|
|
|
847
931
|
* Evaluate response quality
|
|
848
932
|
*/
|
|
849
933
|
evaluateQuality(response) {
|
|
934
|
+
// 2026-10-13 (leo: 「我发了一个命令, 返回空」): **空回复必须判不合格** —— 旧实现起点 0.7,
|
|
935
|
+
// 空串 '' 也有 0.7 分, 恰好等于默认阈值 ⇒ 空 reply 直接 quality_threshold_met 收尾 ⇒ CLI 空框
|
|
936
|
+
// (「你好」正常因为模型给文本; 「能看到我的仓库吗?」模型某轮返回空 content/tool-only 回复 ⇒ 空串过关)。
|
|
937
|
+
// 现在: 空/纯空白一律 0.3 (低于任何阈值), 打回让模型重给; 非空保持旧评分。
|
|
938
|
+
const t = String(response || '').trim();
|
|
939
|
+
if (!t)
|
|
940
|
+
return 0.3;
|
|
850
941
|
let score = 0.7; // 2026-07-06: 起点 0.7 — 短回复不应被当成低质量打回去重跑
|
|
851
942
|
// Length bonus
|
|
852
943
|
if (response.length > 100)
|
|
@@ -908,7 +999,7 @@ export class WorkflowPivotLoop {
|
|
|
908
999
|
const identityLine = lines.find((l) => l.includes('你是') && !l.includes('(续)')) ?? '';
|
|
909
1000
|
return `[continuation] 你的 persona + tools + judgments 已在首轮 messageHistory 中.
|
|
910
1001
|
${identityLine ? identityLine + '\n' : ''}当前 step 别再读 persona 全文, 继续推进任务即可.
|
|
911
|
-
>
|
|
1002
|
+
> 任务完成前持续调用工具推进; 全部完成后给出完整最终回答.`;
|
|
912
1003
|
}
|
|
913
1004
|
/**
|
|
914
1005
|
* Create result object
|
|
@@ -972,7 +1063,7 @@ ${identityLine ? identityLine + '\n' : ''}当前 step 别再读 persona 全文,
|
|
|
972
1063
|
*/
|
|
973
1064
|
export function createDefaultPivotConfig(complexity) {
|
|
974
1065
|
// 2026-07-06: maxIterations 默认大幅上调 — LLM 自己约束结束时机, 持久循环.
|
|
975
|
-
//
|
|
1066
|
+
// 2026-10-12: 真实停止由 质量达标 / 连续无进展 / user abort 触发 (leo 拍板去掉 <final gen>).
|
|
976
1067
|
// 旧 30/60 iter 在长任务 (eg. 大文档改写) 太短, 现在跑到 N 次也不报超时.
|
|
977
1068
|
const profiles = {
|
|
978
1069
|
simple: {
|
|
@@ -15,7 +15,8 @@ export { readProfile, setProfile };
|
|
|
15
15
|
let watcherTimer = null;
|
|
16
16
|
let watching = false; // 防重叠: 上一轮没跑完就跳过这一轮
|
|
17
17
|
let watcherCount = 0;
|
|
18
|
-
/** 单轮观察: 扫描世界 → 观察日志/校准环自动积累 (分发主体本地版)
|
|
18
|
+
/** 单轮观察: 扫描世界 → 观察日志/校准环自动积累 (分发主体本地版)。
|
|
19
|
+
* 2026-10-05 (leo): 有 active intent / 画像时, 自动上网搜机会 (AI 主动搜索, 非被动响应)。 */
|
|
19
20
|
export async function tickWorldWatcher() {
|
|
20
21
|
if (watching)
|
|
21
22
|
return { ok: true, observed: 0, count: watcherCount, error: '上一轮观察未完成, 跳过本轮 (防重叠)' };
|
|
@@ -23,12 +24,40 @@ export async function tickWorldWatcher() {
|
|
|
23
24
|
try {
|
|
24
25
|
const r = await scanOpportunities({ limit: 50 });
|
|
25
26
|
watcherCount += 1;
|
|
27
|
+
// 2026-10-05: 主动搜索 — 有意图/画像才搜 (无 topic 不烧网络); 失败不影响扫描
|
|
28
|
+
await autoSearchForIntents();
|
|
26
29
|
return { ok: r.ok, observed: r.results.length, count: watcherCount, error: r.error };
|
|
27
30
|
}
|
|
28
31
|
finally {
|
|
29
32
|
watching = false;
|
|
30
33
|
}
|
|
31
34
|
}
|
|
35
|
+
/** 按 active intent + 画像文本自动搜索机会 (AI 主动上网找, 结果进 world/search/) */
|
|
36
|
+
async function autoSearchForIntents() {
|
|
37
|
+
try {
|
|
38
|
+
const { listIntents } = await import('./intent-store.js');
|
|
39
|
+
const intents = await listIntents();
|
|
40
|
+
const active = intents.ok ? intents.intents.filter((i) => i.status === 'active') : [];
|
|
41
|
+
const topics = active.map((i) => i.text);
|
|
42
|
+
if (!topics.length) {
|
|
43
|
+
const profile = await readProfile();
|
|
44
|
+
if (profile && (profile.about || profile.tags.length)) {
|
|
45
|
+
topics.push(profile.about || profile.tags.join(' '));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (!topics.length)
|
|
49
|
+
return; // 无意图无画像 → 不主动搜
|
|
50
|
+
// 每轮至多搜 2 个主题 (网络调用有成本; 已有结果幂等不重复)
|
|
51
|
+
for (const t of topics.slice(0, 2)) {
|
|
52
|
+
const { searchOpportunitiesForTopic } = await import('./opportunity-web-search.js');
|
|
53
|
+
const r = await searchOpportunitiesForTopic(t, { limit: 4 });
|
|
54
|
+
if (r.added > 0) {
|
|
55
|
+
console.info(`[world-watcher] 主动搜索「${t.slice(0, 30)}」新增 ${r.added} 条机会`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch { /* 主动搜索失败静默, 下一轮再试 */ }
|
|
60
|
+
}
|
|
32
61
|
/**
|
|
33
62
|
* 启动世界自动观察器 (服务端启动时调用一次即可; 幂等, 重复调用不叠定时器)。
|
|
34
63
|
* 每 intervalMs (默认 10 分钟) 自动扫描一次 —— 机会进入 AI 视野是主动行为, 不是人触发。
|
|
@@ -214,10 +214,8 @@ function collectEnv() {
|
|
|
214
214
|
let llmProvider = 'unknown';
|
|
215
215
|
try {
|
|
216
216
|
const home = process.env.HOME || os.homedir() || '/tmp';
|
|
217
|
-
// 2026-
|
|
218
|
-
|
|
219
|
-
if (!require('fs').existsSync(cfgPath))
|
|
220
|
-
cfgPath = path.join(home, '.bolloon', 'llm-config.json');
|
|
217
|
+
// 2026-10-06 技术债清理: 只读 canonical bolloon-config.json (旧 llm-config.json 已废弃)
|
|
218
|
+
const cfgPath = path.join(home, '.bolloon', 'bolloon-config.json');
|
|
221
219
|
const cfg = require(cfgPath);
|
|
222
220
|
if (cfg && typeof cfg === 'object' && 'provider' in cfg) {
|
|
223
221
|
llmProvider = String(cfg.provider);
|
|
@@ -71,7 +71,9 @@ async function readRuleFile(filePath) {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
/**
|
|
74
|
-
* 读 project 层: 优先 Bolloon.md, fallback Bolloon.md,
|
|
74
|
+
* 读 project 层: 优先 Bolloon.md, fallback Bolloon.md,
|
|
75
|
+
* 然后 AGENTS.md / CLAUDE.md (2026-10-12: Hermes/Claude Code 仓库约定文件, leo 要求对照 Hermes 主动加载),
|
|
76
|
+
* 最后尝试 .claude/rules/*.md
|
|
75
77
|
*/
|
|
76
78
|
async function readProjectLayer(projectPath, projectRulesDir, maxChars) {
|
|
77
79
|
// 1. Bolloon.md
|
|
@@ -83,6 +85,16 @@ async function readProjectLayer(projectPath, projectRulesDir, maxChars) {
|
|
|
83
85
|
content = await readRuleFile(bolloonMdPath);
|
|
84
86
|
if (content)
|
|
85
87
|
return truncate(content, maxChars);
|
|
88
|
+
// 2b. 2026-10-12 (leo: 「开始的时候没有主动扫描加载这个仓库里面的技能, 看一下 hermes 怎么做的」):
|
|
89
|
+
// Hermes 启动会把当前仓库的 AGENTS.md / CLAUDE.md 自动装进 project context (仓库约定即技能/规则)。
|
|
90
|
+
// Bolloon 只认 Bolloon.md + .claude/rules, 漏了这两个最通用的约定文件 — 补上。
|
|
91
|
+
for (const f of ['AGENTS.md', 'CLAUDE.md']) {
|
|
92
|
+
const p = path.join(path.dirname(projectPath), f);
|
|
93
|
+
content = await readRuleFile(p);
|
|
94
|
+
if (content) {
|
|
95
|
+
return truncate(`### ${f}\n${content}`, maxChars);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
86
98
|
// 3. .claude/rules/*.md (合并所有)
|
|
87
99
|
try {
|
|
88
100
|
const entries = await fs.readdir(projectRulesDir, { withFileTypes: true });
|
package/dist/cli/ink-app.js
CHANGED
|
@@ -15,7 +15,6 @@ import { Static, render, Box, Text, useInput, useApp, useStdout } from 'ink';
|
|
|
15
15
|
import { stripUiNoise, collapsePaste, stripBracketedPaste, looksLikePasteChunk, logPasteChunk, shouldSuppressMention, singleLine, logPopupEvent, PASTE_MENTION_SHIELD_MS } from './input-paste.js';
|
|
16
16
|
import TextInput from 'ink-text-input';
|
|
17
17
|
import { dispWidth, LOADING_FRAMES as KAOMOJI } from './loading-tui.js';
|
|
18
|
-
import { reasoningMode } from './reasoning-view.js';
|
|
19
18
|
import { THEME, fg } from './theme.js';
|
|
20
19
|
import { COMPOSER_PLACEHOLDER, POPUP_TITLE_TAB, POPUP_TITLE_AGENT, POPUP_TITLE_FILE, POPUP_TITLE_COMMAND } from './content.js';
|
|
21
20
|
import { DOUBLE_ESC_MS, STATUS_TICK_MS, THINK_FRAME_MS } from './timing.js';
|
|
@@ -389,30 +388,48 @@ const InkApp = ({ onPrompt, initialStatus, getStatusUpdate, terminalW, terminalH
|
|
|
389
388
|
// 2026-09-08 (Hermes TUI 学习落地): 实时终端尺寸 — 原来 terminalW/H 在 mount 时冻结,
|
|
390
389
|
// 终端 resize 后分隔线/logo 宽度全错位 (Hermes 用 resizeCoalescer + 实时 layout)。
|
|
391
390
|
// 订阅 stdout resize, 每次渲染用最新列数。
|
|
391
|
+
// 2026-10-15 (leo 拖动侧边栏): **变窄**时终端把旧帧满宽行自动折行,
|
|
392
|
+
// Ink 的 eraseLines(lastOutputHeight) 按**未折行**行数擦 ⇒ 擦不干净 ⇒ 旧 footer/分隔线/
|
|
393
|
+
// 思考行残影被推进滚动区; 且 `<Static>` 历史区写进 scrollback 后不会重绘, 残影永远留在那里.
|
|
394
|
+
// 修法 (只对**变窄**): resize 停止 150ms 后整屏重建 —
|
|
395
|
+
// ① 发 `ESC[2J ESC[3J ESC[H` (清可视区 + 清 scrollback + 光标归位) 把残影彻底清掉;
|
|
396
|
+
// ② suspendInk + resumeInk 重挂全新 InkApp: `<Static>` 首次挂载对 store 全量重放
|
|
397
|
+
// (历史一行不少, 按新宽度重新 wrap), footer 按新宽度重画。
|
|
398
|
+
// 变宽不会折行 (旧行宽 ≤ 旧终端宽 < 新终端宽), Ink 自己重画即可, 不重建。
|
|
392
399
|
const { stdout } = useStdout();
|
|
393
400
|
const [termSize, setTermSize] = useState({ w: terminalW, h: terminalH });
|
|
394
401
|
useEffect(() => {
|
|
395
|
-
|
|
396
|
-
let
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
402
|
+
let lastWidth = stdout?.columns || terminalW;
|
|
403
|
+
let rebuildTimer = null;
|
|
404
|
+
const rebuild = () => {
|
|
405
|
+
rebuildTimer = null;
|
|
406
|
+
try {
|
|
407
|
+
// 残影在 scrollback 里 —— eraseLines 够不到, 必须整屏清掉再让 Ink 全量重放
|
|
408
|
+
process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
|
|
409
|
+
suspendInk();
|
|
410
|
+
// 让 React 先完成 unmount 清理, 再重挂 (避免新旧实例抢 stdout)
|
|
411
|
+
setTimeout(() => { try {
|
|
412
|
+
resumeInk();
|
|
413
|
+
}
|
|
414
|
+
catch { /* 重建失败不致命, 下次 resize 再试 */ } }, 0);
|
|
403
415
|
}
|
|
404
|
-
|
|
416
|
+
catch { /* 清屏/重建失败不阻塞 resize 本身 */ }
|
|
405
417
|
};
|
|
406
418
|
const update = () => {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
419
|
+
const w = stdout?.columns || terminalW;
|
|
420
|
+
setTermSize({ w, h: stdout?.rows || terminalH });
|
|
421
|
+
if (w < lastWidth) {
|
|
422
|
+
// 变窄: 折行残影无法增量修复, 停止拖动后整屏重建
|
|
423
|
+
if (rebuildTimer)
|
|
424
|
+
clearTimeout(rebuildTimer);
|
|
425
|
+
rebuildTimer = setTimeout(rebuild, 150);
|
|
426
|
+
}
|
|
427
|
+
lastWidth = w;
|
|
411
428
|
};
|
|
412
429
|
update();
|
|
413
430
|
stdout?.on?.('resize', update);
|
|
414
|
-
return () => {
|
|
415
|
-
clearTimeout(
|
|
431
|
+
return () => { if (rebuildTimer)
|
|
432
|
+
clearTimeout(rebuildTimer); stdout?.removeListener?.('resize', update); };
|
|
416
433
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
417
434
|
}, [stdout]);
|
|
418
435
|
const W = termSize.w;
|
|
@@ -509,6 +526,16 @@ const InkApp = ({ onPrompt, initialStatus, getStatusUpdate, terminalW, terminalH
|
|
|
509
526
|
setTimeout(() => process.exit(0), 2000);
|
|
510
527
|
};
|
|
511
528
|
if (key.ctrl && _input === 'c') {
|
|
529
|
+
// 2026-10-13 (leo: 「按一次 Ctrl+C 是中断当前对话; 按两次才是完整退出; 空闲时按一次就退出」):
|
|
530
|
+
// 走 index.ts 注册的两级语义处理器 —— 有回合在跑且未 armed → 中断回合 (abort kernel actor);
|
|
531
|
+
// 第二次 / 空闲 → 完整退出。Ink raw mode 下 Ctrl+C 被 useInput 拦截, 不会产生 SIGINT,
|
|
532
|
+
// 所以**这里**才是 Ctrl+C 的主入口 (进程级 SIGINT 只是启动早期兜底)。
|
|
533
|
+
const sigintHandler = globalThis.__bolloonCliSigint;
|
|
534
|
+
if (typeof sigintHandler === 'function') {
|
|
535
|
+
sigintHandler();
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
// 兜底: 处理器没注册 (极早期) → 直接退出, 不吞 Ctrl+C
|
|
512
539
|
requestExit();
|
|
513
540
|
return;
|
|
514
541
|
}
|
|
@@ -839,7 +866,7 @@ const InkApp = ({ onPrompt, initialStatus, getStatusUpdate, terminalW, terminalH
|
|
|
839
866
|
}
|
|
840
867
|
catch { /* 探针写不进去绝不影响界面 */ }
|
|
841
868
|
});
|
|
842
|
-
return (_jsxs(_Fragment, { children: [_jsx(Static, { items: msgs, children: (m, i) => _jsx(Text, { children: m }, `m${i}`) }), _jsxs(Box, { flexDirection: "column", width: budget.cols, children: [hasRails && (_jsx(Box, { flexDirection: "column", children: railNames.map((n) => _jsx(Text, { color: THEME.muted, children: `${n}: ${String(widgets[n] ?? '').replace(/\n/g, ' ')}` }, n)) })), popupOpenNow && tabState && (_jsx(MentionPopup, { title: POPUP_TITLE_TAB, items: filtered, sel: safeSel, width: W, maxRows: budget.popupRows })), popupOpenNow && !tabState && mention && (_jsx(MentionPopup, { title: popupTitle, items: filtered, sel: safeSel, width: W, loading: loadingFiles, maxRows: budget.popupRows })), popupOpenNow && !tabState && !mention && picker && (_jsx(MentionPopup, { title: picker.title, items: picker.items, sel: Math.min(picker.sel, picker.items.length - 1), width: W, maxRows: budget.popupRows })), _jsx(Box, { height: 1, width: budget.cols, overflow: "hidden", children: transient ? (_jsx(Text, { children: transient })) : thinking ? (
|
|
869
|
+
return (_jsxs(_Fragment, { children: [_jsx(Static, { items: msgs, children: (m, i) => _jsx(Text, { children: m }, `m${i}`) }), _jsxs(Box, { flexDirection: "column", width: budget.cols, children: [hasRails && (_jsx(Box, { flexDirection: "column", children: railNames.map((n) => _jsx(Text, { color: THEME.muted, children: `${n}: ${String(widgets[n] ?? '').replace(/\n/g, ' ')}` }, n)) })), popupOpenNow && tabState && (_jsx(MentionPopup, { title: POPUP_TITLE_TAB, items: filtered, sel: safeSel, width: W, maxRows: budget.popupRows })), popupOpenNow && !tabState && mention && (_jsx(MentionPopup, { title: popupTitle, items: filtered, sel: safeSel, width: W, loading: loadingFiles, maxRows: budget.popupRows })), popupOpenNow && !tabState && !mention && picker && (_jsx(MentionPopup, { title: picker.title, items: picker.items, sel: Math.min(picker.sel, picker.items.length - 1), width: W, maxRows: budget.popupRows })), _jsx(Box, { height: 1, width: budget.cols, overflow: "hidden", children: transient ? (_jsx(Text, { children: transient })) : thinking ? (_jsxs(Text, { color: "yellow", children: [KAOMOJI[thinkingIdx.current], " \u601D\u8003\u4E2D..."] })) : (_jsx(Text, { color: THEME.muted, children: '· 回车发送 · ↑↓ 历史 · PgUp 回看 · Esc 双击退出' })) }), _jsx(Box, { width: budget.cols, height: 1, overflow: "hidden", children: _jsx(Text, { bold: true, color: THEME.accent, children: ruleFor(Math.max(10, budget.cols - 1)) }) }), _jsx(Box, { height: 1, width: budget.cols, overflow: "hidden", children: _jsx(Text, { children: status }) }), _jsx(Box, { width: budget.cols, height: 1, overflow: "hidden", children: _jsx(Text, { bold: true, color: THEME.accent, children: ruleFor(Math.max(10, budget.cols - 1)) }) }), _jsxs(Box, { width: budget.cols, height: 1, overflow: "hidden", children: [_jsx(Text, { bold: true, color: THEME.accent, children: "\u276F " }), _jsx(Box, { width: Math.max(10, budget.cols - 2), height: 1, overflow: "hidden", flexShrink: 0, children: _jsx(TextInput, { value: singleLine(input), onChange: setInput, onSubmit: onSubmit, focus: !popupOpenNow, placeholder: COMPOSER_PLACEHOLDER }, tiKey) })] }), _jsx(Box, { width: budget.cols, height: 1, overflow: "hidden", children: _jsx(Text, { bold: true, color: THEME.accent, children: ruleFor(Math.max(10, budget.cols - 1)) }) })] })] }));
|
|
843
870
|
};
|
|
844
871
|
export { InkApp };
|
|
845
872
|
// ─── 启动 ────────────────────────────────────────────────────────────────────
|
|
@@ -172,14 +172,26 @@ export async function opportunityCommand(flags) {
|
|
|
172
172
|
human: INTENT_USAGE,
|
|
173
173
|
};
|
|
174
174
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
175
|
+
/** 2026-10-06 (用户: 世界模式用表格呈现): ASCII 表格机会列表 — 列 = 分数/来源/标题/预算/id */
|
|
176
|
+
export function opportunityTable(results) {
|
|
177
|
+
if (!results.length)
|
|
178
|
+
return [' (没有机会)'];
|
|
179
|
+
const reasonShort = (r) => (r === 'match' ? '匹配' : r === 'inbox' ? '投递' : r === 'search' ? '搜索' : '世界');
|
|
180
|
+
const rows = results.map((o) => {
|
|
181
|
+
const pct = Math.round(o.score * 100);
|
|
182
|
+
const title = o.title.length > 32 ? o.title.slice(0, 31) + '…' : o.title;
|
|
183
|
+
return [String(pct) + '%', reasonShort(o.reason), title, (o.budget ?? '无').slice(0, 14), o.id];
|
|
184
|
+
});
|
|
185
|
+
const w = (idx, min) => Math.max(min, ...rows.map((r) => r[idx].length));
|
|
186
|
+
const wS = w(0, 5), wR = w(1, 4), wT = w(2, 32), wB = w(3, 6), wId = w(4, 8);
|
|
187
|
+
const sep = ` +${'-'.repeat(wS)}+${'-'.repeat(wR)}+${'-'.repeat(wT)}+${'-'.repeat(wB)}+${'-'.repeat(wId)}+`;
|
|
188
|
+
const head = ` | ${'分数'.padStart(wS - 2)} | ${'来源'.padEnd(wR)} | ${'标题'.padEnd(wT)} | ${'预算'.padEnd(wB)} | ${'ID'.padEnd(wId)} |`;
|
|
189
|
+
const line = (c) => ` | ${c[0].padStart(wS)} | ${c[1].padEnd(wR)} | ${c[2].padEnd(wT)} | ${c[3].padEnd(wB)} | ${c[4].padEnd(wId)} |`;
|
|
190
|
+
const out = [sep, head, sep];
|
|
191
|
+
for (const r of rows)
|
|
192
|
+
out.push(line(r));
|
|
193
|
+
out.push(sep);
|
|
194
|
+
return out;
|
|
183
195
|
}
|
|
184
196
|
async function opportunityScan(flags) {
|
|
185
197
|
const head = 'bolloon opportunity scan';
|
|
@@ -201,8 +213,7 @@ async function opportunityScan(flags) {
|
|
|
201
213
|
}
|
|
202
214
|
const data = { count: r.results.length, minScore, opportunities: r.results };
|
|
203
215
|
const human = [title(head), ` 找到 ${r.results.length} 个机会 (score ≥ ${minScore}):`, ''];
|
|
204
|
-
|
|
205
|
-
human.push(...cardRows(o));
|
|
216
|
+
human.push(...opportunityTable(r.results));
|
|
206
217
|
human.push('', hint('用 bolloon opportunity accept <id> 确认, ignore <id> 忽略'));
|
|
207
218
|
return { envelope: okEnvelope('OK', `${r.results.length} 个机会`, data, r.results.map((x) => x.id), null), human: human.join('\n') };
|
|
208
219
|
}
|
|
@@ -219,8 +230,7 @@ async function opportunityList(flags) {
|
|
|
219
230
|
if (!hits.length)
|
|
220
231
|
return { envelope: okEnvelope('OK', `意图「${i.text}」暂无匹配`, { count: 0, opportunities: [] }, [], null), human: `${title(head)}\n 「${i.text}」暂无匹配机会` };
|
|
221
232
|
const human = [title(head), ` 「${i.text}」的 ${hits.length} 个机会:`, ''];
|
|
222
|
-
|
|
223
|
-
human.push(...cardRows(o));
|
|
233
|
+
human.push(...opportunityTable(hits));
|
|
224
234
|
return { envelope: okEnvelope('OK', `${hits.length} 个机会`, { count: hits.length, opportunities: hits }, hits.map((x) => x.id), null), human: human.join('\n') };
|
|
225
235
|
}
|
|
226
236
|
return opportunityScan(flags);
|
package/dist/cli/loading-tui.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* LoadingTUI 原地刷新的启动仪表盘类
|
|
13
13
|
*/
|
|
14
14
|
import { truncateSafe } from './status-segments.js';
|
|
15
|
+
import { toolArgsSummary } from './trace-line.js';
|
|
15
16
|
import * as fs from 'fs';
|
|
16
17
|
import * as path from 'path';
|
|
17
18
|
import { fileURLToPath } from 'url';
|
|
@@ -433,12 +434,17 @@ function colorizeDiff(text) {
|
|
|
433
434
|
});
|
|
434
435
|
return out.join('\n');
|
|
435
436
|
}
|
|
436
|
-
/** 渲染单条工具调用为紧凑列表项 (增量显示, 一行,
|
|
437
|
+
/** 渲染单条工具调用为紧凑列表项 (增量显示, 一行, 参数摘要) */
|
|
437
438
|
export function renderToolCallListItem(item, index, total) {
|
|
438
439
|
const dur = item.durationMs != null ? ` ${C_DIM}${item.durationMs}ms${RESET}` : '';
|
|
439
440
|
const label = item.status === 'error' ? `${C_ERROR}${item.tool}${RESET}` : `${C_ACCENT}${item.tool}${RESET}`;
|
|
441
|
+
// 2026-10-14 (leo: 「调用工具的时候, 把执行的命令缩写也附带上 —— 现在看不到过程」):
|
|
442
|
+
// 工具行附上参数摘要 (terminal→命令 / read_file→路径 / grep→pattern·dir), 灰色、截断;
|
|
443
|
+
// 看不清过程的问题 = 之前只有工具名, 不知道它具体跑了什么。
|
|
444
|
+
const sum = toolArgsSummary(item.tool, item.args);
|
|
445
|
+
const sumPart = sum ? ` ${C_DIM}${sum}${RESET}` : '';
|
|
440
446
|
// 2026-10-01: 工具行也按**保守宽度**截断 —— 超宽会让终端折行、打乱 Ink 光标 (底栏那块就是这么坏的)
|
|
441
|
-
return truncateSafe(` 🔧 ${label}${dur}`, Math.max(20, (process.stdout.columns || 100) - 1));
|
|
447
|
+
return truncateSafe(` 🔧 ${label}${sumPart}${dur}`, Math.max(20, (process.stdout.columns || 100) - 1));
|
|
442
448
|
}
|
|
443
449
|
/** 紧凑渲染工具输出 (检测到 diff 则着色, 否则普通截断) — 当前不显示 body */
|
|
444
450
|
export function renderToolCallBody(item, width = 72) {
|