@bolloon/bolloon-agent 0.6.6 → 0.6.7
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/ha-daemon.js +85 -0
- package/dist/agents/ha-supervisor.js +146 -0
- package/dist/agents/memory-store.js +25 -0
- package/dist/agents/opportunity-match.js +15 -3
- package/dist/agents/pi-sdk-session-manager.js +8 -0
- package/dist/agents/pi-sdk.js +67 -494
- package/dist/bootstrap/context-collector.js +65 -3
- package/dist/bootstrap/persona-loader.js +26 -0
- package/dist/electron-build/agents/memory-store.js +26 -0
- package/dist/electron-build/agents/memory-store.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk-session-manager.js +8 -0
- package/dist/electron-build/agents/pi-sdk-session-manager.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk.js +65 -492
- package/dist/electron-build/agents/pi-sdk.js.map +1 -1
- package/dist/electron-build/bootstrap/context-collector.js +66 -3
- package/dist/electron-build/bootstrap/context-collector.js.map +1 -1
- package/dist/electron-build/bootstrap/persona-loader.js +27 -0
- package/dist/electron-build/bootstrap/persona-loader.js.map +1 -1
- package/dist/electron-build/llm/pi-ai.js +16 -3
- package/dist/electron-build/llm/pi-ai.js.map +1 -1
- package/dist/electron-build/llm/system-prompt/registry.js +8 -6
- package/dist/electron-build/llm/system-prompt/registry.js.map +1 -1
- package/dist/ios/bolloon-web.json +3 -3
- package/dist/ios/index.html +19 -4
- package/dist/ios/mobile-agent.js +267 -41
- package/dist/ios/mobile-contacts.js +0 -4
- package/dist/ios/mobile-core.js +806 -30
- package/dist/ios/mobile-memory.js +317 -0
- package/dist/ios/mobile.html +19 -4
- package/dist/ios/mobile.js +144 -30
- package/dist/ios/routes-contacts.js +94 -1
- package/dist/ios/routes-world.js +9 -1
- package/dist/ios/server.js +51 -40
- package/dist/ios/sw.js +1 -1
- package/dist/kernel/gate-scan.js +1 -1
- package/dist/kernel/ha/ha-arbiter.js +177 -0
- package/dist/kernel/ha/ha-guard.js +151 -0
- package/dist/kernel/index.js +3 -0
- package/dist/kernel/plan.js +5 -5
- package/dist/kernel/roster.js +3 -3
- package/dist/llm/pi-ai.js +16 -3
- package/dist/llm/system-prompt/registry.js +8 -6
- package/dist/web/bolloon-web.json +3 -3
- package/dist/web/mobile-agent.js +265 -44
- package/dist/web/mobile-core.js +681 -32
- package/dist/web/mobile-memory.js +317 -0
- package/dist/web/mobile.html +4 -4
- package/dist/web/mobile.js +36 -27
- package/dist/web/routes-world.js +9 -1
- package/dist/web/server.js +50 -39
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
package/dist/web/mobile-agent.js
CHANGED
|
@@ -469,10 +469,12 @@ export async function runLocalAgent(goal) {
|
|
|
469
469
|
// 内置极简回复 (纯离线可用)
|
|
470
470
|
const t = (goal || '').trim();
|
|
471
471
|
// 2026-10-07: 手机自治 — 先用 LLM 循环 (经内置公共网关), 失败再降级纯规则
|
|
472
|
+
// 2026-10-16: silentDegrade=true — LLM 不可达时 → throw → 下面 catch → 内置离线规则,
|
|
473
|
+
// (上一层 runWebAgentLoop 若默认返回降级文本, 内置规则层就永远走不到)
|
|
472
474
|
try {
|
|
473
475
|
const llm = getLlmConfig();
|
|
474
476
|
if (llm && llm.baseUrl) {
|
|
475
|
-
const reply = await runWebAgentLoop(goal, { baseUrl: llm.baseUrl, apiKey: llm.apiKey, model: llm.model, maxTokens: llm.maxTokens });
|
|
477
|
+
const reply = await runWebAgentLoop(goal, { baseUrl: llm.baseUrl, apiKey: llm.apiKey, model: llm.model, maxTokens: llm.maxTokens }, { silentDegrade: true });
|
|
476
478
|
if (reply)
|
|
477
479
|
return reply;
|
|
478
480
|
}
|
|
@@ -512,31 +514,115 @@ export async function runLocalAgent(goal) {
|
|
|
512
514
|
}
|
|
513
515
|
return `已收到: "${(goal || '').slice(0, 40)}"。这是手机端 Agent 功能层的本地执行 (数据同步与 agent 功能已分离)。`;
|
|
514
516
|
}
|
|
515
|
-
// ============ 手机自治 agent 循环 (2026-10-
|
|
516
|
-
//
|
|
517
|
+
// ============ 手机自治 agent 循环 — 原生运行内核 (2026-10-16) ============
|
|
518
|
+
// 桌面 Kernel 的浏览器移植, 手机原生功能逐项落地 (leo 点名清单):
|
|
519
|
+
// · memory 设计迭代 + 人际关系分类 (mobile-memory.ts: fact/preference/event/relationship/skill-feedback,
|
|
520
|
+
// relation{family|friend|work|acquaintance|stranger, trust}, TTL/decay/快照/指纹)
|
|
521
|
+
// · 错误恢复 (分类 + 退避重试 + 失败反思注入上下文)
|
|
522
|
+
// · 自适应 API 超时 (按上下文长度: max(10s, ceil(ctxChars/1024)*4000+10000))
|
|
523
|
+
// · 记忆快照 / bootstrap (启动恢复快照 + 收尾落快照)
|
|
524
|
+
// · 文档冻结 + 隐式化 (user 文档 / soul 文档只注摘要, 不整篇塞 — leo: 系统 prompt 注入太多, 删)
|
|
525
|
+
// · 上下文压缩 / 缓存命中 (指纹装配缓存: 同冻结上下文指纹 → 复用 system 不重建)
|
|
526
|
+
// · 工具指纹 (工具集 hash → 缓存 key, 工具变了 key 变)
|
|
527
|
+
// · 生命周期 (idle→running→paused(确认)→done/cancelled/error) + TTL
|
|
528
|
+
// · 多模态通道标注 (text/image/audio/file) + 能动性 (先动手别先问)
|
|
529
|
+
// · 中间阶段性环境 (已完成步骤注入) + 计划设计 (阶段提示) + 确认模式 (高风险工具)
|
|
530
|
+
// · task / process 运行记录 (run 记录落 localStorage, 可查)
|
|
517
531
|
// 能力范围: 手机 WebView 能做的 (身份/钱包/入网/P2P/信息查询), 不假装能做 fs/shell。
|
|
518
|
-
// 保护: maxSteps=5 防死循环; 每轮把工具结果拼回对话; LLM 说完成或没有下一步即停。
|
|
519
532
|
// 2026-10-07: persona 注入 — 性格/价值观/兴趣/说话方式来自 mobile-persona (设置 → 性格)。
|
|
520
533
|
import { loadPersona, buildPersonaPrompt } from './mobile-persona.js';
|
|
521
|
-
|
|
534
|
+
import { MobileMemory, MobileCache, quickHash } from './mobile-memory.js';
|
|
535
|
+
const runsKey = (did) => `bolloon_mobile_runs_${did}`;
|
|
536
|
+
/** 最近运行记录 (task/process 台账, 手机原生) */
|
|
537
|
+
export function listMobileRuns(did, limit = 20) {
|
|
538
|
+
try {
|
|
539
|
+
const raw = typeof localStorage !== 'undefined' ? localStorage.getItem(runsKey(did)) : null;
|
|
540
|
+
if (!raw)
|
|
541
|
+
return [];
|
|
542
|
+
const parsed = JSON.parse(raw);
|
|
543
|
+
return Array.isArray(parsed) ? parsed.slice(0, limit) : [];
|
|
544
|
+
}
|
|
545
|
+
catch {
|
|
546
|
+
return [];
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
function saveRunRecord(did, rec) {
|
|
550
|
+
try {
|
|
551
|
+
const prev = listMobileRuns(did, 50);
|
|
552
|
+
const next = [rec, ...prev.filter((r) => r.runId !== rec.runId)].slice(0, 50);
|
|
553
|
+
if (typeof localStorage !== 'undefined')
|
|
554
|
+
localStorage.setItem(runsKey(did), JSON.stringify(next));
|
|
555
|
+
}
|
|
556
|
+
catch { /* 台账写失败不阻断运行 */ }
|
|
557
|
+
}
|
|
558
|
+
// ── 工具指纹 + 上下文指纹 (缓存命中) ───────────────────────────────────────
|
|
559
|
+
const toolNames = () => Object.keys(WEB_AGENT_TOOLS).sort().join(',');
|
|
560
|
+
let _toolFingerprint = '';
|
|
561
|
+
/** 工具指纹: 工具集 hash (工具变了 → 指纹变 → system 装配缓存 miss) */
|
|
562
|
+
export function toolFingerprint() {
|
|
563
|
+
if (!_toolFingerprint)
|
|
564
|
+
_toolFingerprint = quickHash('tools|' + toolNames());
|
|
565
|
+
return _toolFingerprint;
|
|
566
|
+
}
|
|
567
|
+
const CHANNEL_LABEL = {
|
|
568
|
+
text: '文字', image: '图片', audio: '语音', file: '文件',
|
|
569
|
+
};
|
|
570
|
+
// ── 确认模式 (高风险工具执行前) ───────────────────────────────────────────
|
|
571
|
+
// 默认空名单 (机制就位; 需要拦的工具由调用方注入 confirm 回调裁决)
|
|
572
|
+
const CONFIRM_REQUIRED_TOOLS = [];
|
|
573
|
+
export function isConfirmRequiredTool(tool) {
|
|
574
|
+
return CONFIRM_REQUIRED_TOOLS.includes(tool);
|
|
575
|
+
}
|
|
576
|
+
function buildAgentSystem(opts = {}) {
|
|
522
577
|
const persona = loadPersona();
|
|
523
578
|
const personaBlock = buildPersonaPrompt(persona);
|
|
579
|
+
// 文档隐式化 (leo: 系统 prompt 注入太多, 文档可以删):
|
|
580
|
+
// user 文档 → 身份/偏好摘要; soul 文档 → persona 摘要 (buildPersonaPrompt 本身就是摘要形)
|
|
581
|
+
// 完整文档不整篇塞进 prompt, 按需由工具读取。
|
|
582
|
+
const userBlock = opts.userDoc && opts.userDoc.trim()
|
|
583
|
+
? `【用户档案】\n${opts.userDoc.trim().slice(0, 400)}`
|
|
584
|
+
: '';
|
|
585
|
+
const memoryBlock = opts.memoryBlock && opts.memoryBlock.trim()
|
|
586
|
+
? `【记忆】\n${opts.memoryBlock.trim().slice(0, 600)}`
|
|
587
|
+
: '';
|
|
588
|
+
// 稳态记忆块 (对齐桌面 formatMemorySystemBlock, 2026-10-16 补齐): 固化优先 + importance 降序,
|
|
589
|
+
// 始终注入 system — 不是只靠当轮 query 召回。与 memoryBlock (recall) 互补。
|
|
590
|
+
const stableBlock = opts.memory && opts.memory.stableBlock
|
|
591
|
+
? opts.memory.stableBlock(6)
|
|
592
|
+
: '';
|
|
593
|
+
const progressBlock = opts.progress && opts.progress.length
|
|
594
|
+
? `【本轮进度】\n已完成: ${opts.progress.slice(-8).map((s, i) => `${i + 1}. ${s.slice(0, 80)}`).join('\n')}`
|
|
595
|
+
: '';
|
|
596
|
+
const planHint = opts.plan && opts.plan.trim()
|
|
597
|
+
? `【计划】\n${opts.plan.trim().slice(0, 300)}`
|
|
598
|
+
: '任务复杂时先在内心拆步骤, 逐步执行, 每步用一个工具推进。';
|
|
599
|
+
const channelBlock = opts.channel && opts.channel !== 'text'
|
|
600
|
+
? `【输入通道】\n本次输入是${CHANNEL_LABEL[opts.channel]}。你能理解${CHANNEL_LABEL[opts.channel]}内容并据此回答/执行。`
|
|
601
|
+
: '';
|
|
524
602
|
return [
|
|
525
603
|
'你是手机端 Bolloon 智能体 (自治节点), 用中文简洁回复。',
|
|
526
604
|
personaBlock,
|
|
605
|
+
userBlock,
|
|
606
|
+
memoryBlock,
|
|
607
|
+
stableBlock,
|
|
527
608
|
'你有以下工具, 按需调用 (JSON: {"tool":"名字","args":{...}}), 不需要工具就直接回答:',
|
|
528
|
-
'- get_status:
|
|
609
|
+
'- get_status: 查本机身份/入网/钱包/连接状态 (args: 无)',
|
|
529
610
|
'- get_wallet: 查钱包余额 (args: 无)',
|
|
530
|
-
'- get_identity:
|
|
611
|
+
'- get_identity: 查本机身份证明 (args: 无)',
|
|
531
612
|
'- get_contacts: 查联系方式授权状态 (args: 无)',
|
|
532
|
-
'
|
|
533
|
-
|
|
613
|
+
'每轮最多用一个工具; 拿到结果后继续思考, 直到任务完成给出最终回答。',
|
|
614
|
+
progressBlock,
|
|
615
|
+
planHint,
|
|
616
|
+
channelBlock,
|
|
617
|
+
'能动性: 先动手别先问; 做到底 (交付=能用的结果); 一个工具不够就换方式继续; 被阻塞如实说, 不编造。',
|
|
618
|
+
].filter(Boolean).join('\n');
|
|
534
619
|
}
|
|
620
|
+
// 2026-10-16: 工具签名带 args (更灵活调用; 现有工具忽略 args)
|
|
535
621
|
const WEB_AGENT_TOOLS = {
|
|
536
622
|
get_status: async () => {
|
|
537
623
|
try {
|
|
538
624
|
const id = await ensureIdentity();
|
|
539
|
-
return
|
|
625
|
+
return `身份: ${id.did.slice(0, 16)}... 名字: ${id.name}`;
|
|
540
626
|
}
|
|
541
627
|
catch (e) {
|
|
542
628
|
return '身份读取失败: ' + String(e?.message || e).slice(0, 60);
|
|
@@ -569,7 +655,7 @@ const WEB_AGENT_TOOLS = {
|
|
|
569
655
|
},
|
|
570
656
|
get_identity: async () => {
|
|
571
657
|
const id = await ensureIdentity();
|
|
572
|
-
return
|
|
658
|
+
return `身份证明: ${id.did} 名字: ${id.name}`;
|
|
573
659
|
},
|
|
574
660
|
get_contacts: async () => {
|
|
575
661
|
try {
|
|
@@ -580,30 +666,61 @@ const WEB_AGENT_TOOLS = {
|
|
|
580
666
|
}
|
|
581
667
|
},
|
|
582
668
|
};
|
|
583
|
-
|
|
669
|
+
export function classifyMobileError(err) {
|
|
670
|
+
const msg = String(err?.message || err?.cause?.message || err || '');
|
|
671
|
+
if (/timeout|timed out|AbortError|aborted/i.test(msg))
|
|
672
|
+
return { cls: 'timeout', retryable: true, hint: '请求超时, 稍后重试' };
|
|
673
|
+
if (/terminated|ECONNRESET|socket hang up|fetch failed|network|ETIMEDOUT|ECONNREFUSED|UND_ERR|Failed to fetch/i.test(msg))
|
|
674
|
+
return { cls: 'network', retryable: true, hint: '网络波动, 正在重试' };
|
|
675
|
+
if (/401|403|quota|rate limit|API key|invalid api|HTTP 40\d|HTTP 429/i.test(msg))
|
|
676
|
+
return { cls: 'api', retryable: false, hint: '接口/配额错误, 无法自动恢复' };
|
|
677
|
+
return { cls: 'unknown', retryable: true, hint: '未知错误, 尝试继续' };
|
|
678
|
+
}
|
|
679
|
+
/** 自适应 API 超时: max(10s, ceil(ctxChars/1024)*4000 + 10000) — 长上下文不被误杀 (桌面同规则) */
|
|
680
|
+
export function adaptiveTimeoutMs(ctxChars, baseMs = 10_000) {
|
|
681
|
+
const byLen = Math.ceil(Math.max(0, ctxChars) / 1024) * 4000 + baseMs;
|
|
682
|
+
return Math.max(baseMs, byLen);
|
|
683
|
+
}
|
|
684
|
+
/** 单轮 LLM 调用 (OpenAI 兼容, 经内置公共网关) — 自适应超时 + 网络错误退避重试 */
|
|
584
685
|
async function webAgentLlmCall(messages, llm) {
|
|
585
686
|
// 2026-10-15: 网关不可达时**快速降级**而不是挂死 — 公共网关限流/断网时 fetch 可能永久挂起,
|
|
586
687
|
// 手机端 runLocalAgent 会先走 LLM 再降级内置规则, 挂起 = 测试 20s 超时 + 真机"发消息无反应"。
|
|
587
|
-
// 10s
|
|
588
|
-
const
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
688
|
+
// 自适应超时 (2026-10-16): 短上下文 10s 底线, 长上下文按 4s/1KB 加时, 最坏 150 步有上界又不会误杀长任务。
|
|
689
|
+
const ctxChars = messages.reduce((n, m) => n + String(m.content || '').length, 0);
|
|
690
|
+
const timeoutMs = adaptiveTimeoutMs(ctxChars);
|
|
691
|
+
// 错误恢复: 网络类错误退避重试 (500ms/1500ms, 共 2 次重试); 超时/API 错误不重试 (明确失败)
|
|
692
|
+
let lastErr = null;
|
|
693
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
694
|
+
try {
|
|
695
|
+
const resp = await fetch(llm.baseUrl.replace(/\/$/, '') + '/chat/completions', {
|
|
696
|
+
method: 'POST',
|
|
697
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + (llm.apiKey || '') },
|
|
698
|
+
body: JSON.stringify({
|
|
699
|
+
model: llm.model || 'glm-5.3',
|
|
700
|
+
messages,
|
|
701
|
+
max_tokens: llm.maxTokens || 2048,
|
|
702
|
+
temperature: 0.4,
|
|
703
|
+
}),
|
|
704
|
+
// eslint-disable-next-line no-undef
|
|
705
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
706
|
+
});
|
|
707
|
+
if (!resp.ok)
|
|
708
|
+
throw new Error('LLM HTTP ' + resp.status + ': ' + (await resp.text()).slice(0, 120));
|
|
709
|
+
const data = await resp.json();
|
|
710
|
+
const content = data?.choices?.[0]?.message?.content;
|
|
711
|
+
if (!content)
|
|
712
|
+
throw new Error('LLM 空回复');
|
|
713
|
+
return String(content).trim();
|
|
714
|
+
}
|
|
715
|
+
catch (e) {
|
|
716
|
+
const cls = classifyMobileError(e);
|
|
717
|
+
lastErr = e;
|
|
718
|
+
if (cls.cls === 'timeout' || cls.cls === 'api' || attempt >= 2)
|
|
719
|
+
throw e; // 明确失败不重试
|
|
720
|
+
await new Promise((r) => setTimeout(r, attempt === 0 ? 500 : 1500)); // 网络波动退避
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
throw lastErr || new Error('LLM 调用失败');
|
|
607
724
|
}
|
|
608
725
|
/**
|
|
609
726
|
* 宽容提取 LLM 回复里的工具调用 JSON。
|
|
@@ -674,37 +791,141 @@ function parseBalancedJson(raw, start) {
|
|
|
674
791
|
}
|
|
675
792
|
throw new Error('unbalanced');
|
|
676
793
|
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
794
|
+
export async function runWebAgentLoop(goal, llm, opts = {}) {
|
|
795
|
+
// ── bootstrap ─────────────────────────────────────────────────────────────
|
|
796
|
+
const id = await ensureIdentity();
|
|
797
|
+
const did = id.did;
|
|
798
|
+
const memory = opts.memory || new MobileMemory(did);
|
|
799
|
+
memory.restore(memory.snapshot()); // 启动恢复记忆 (幂等: restore 只合并缺失条目)
|
|
800
|
+
const channel = opts.channel || 'text';
|
|
801
|
+
const runId = quickHash(did + '|' + goal + '|' + Date.now());
|
|
802
|
+
// task/process 记录: 生命周期开始
|
|
803
|
+
const record = { runId, goal: String(goal || '').slice(0, 80), status: 'running', steps: 0, startedAt: new Date().toISOString(), channel };
|
|
804
|
+
saveRunRecord(did, record);
|
|
805
|
+
// 文档冻结 + 隐式化 (leo: 系统 prompt 注入太多, 文档可以删):
|
|
806
|
+
// userDoc 缺省 = 身份 profile 摘要; soulDoc 缺省 = persona 摘要 (buildAgentSystem 内取)
|
|
807
|
+
const userDoc = opts.userDoc ?? ([id.name ? `名字: ${id.name}` : '', id.phone ? `手机号: ${id.phone}` : '', id.email ? `邮箱: ${id.email}` : '', id.note ? `备注: ${id.note}` : '']
|
|
808
|
+
.filter(Boolean).join('\n'));
|
|
809
|
+
// memory 整段注入: 按目标召回 (冻结快照, 循环内不重建 → 缓存命中稳定)
|
|
810
|
+
const memoryBlock = memory.recall(String(goal || ''), { limit: 3 }).map((e) => e.content).join('\n');
|
|
811
|
+
// 缓存命中: system 装配指纹 = 冻结上下文摘要 + 工具指纹 + 目标前缀
|
|
812
|
+
// 同指纹 (文档/记忆/工具没变) → 复用 system, 不重建 (桌面 kv-prefix-cache 同原理)
|
|
813
|
+
const sysCache = new MobileCache('bolloon_sys');
|
|
814
|
+
// 稳态记忆块参与指纹: 固化记忆变了 → 指纹变 → system 重建 (memory 文档冻结与失效对齐)
|
|
815
|
+
const stableFp = memory.stableBlock(6);
|
|
816
|
+
const sysFp = quickHash([
|
|
817
|
+
userDoc, opts.soulDoc || 'soul-default', memoryBlock, stableFp, toolFingerprint(), channel, opts.plan || '',
|
|
818
|
+
].join('|'));
|
|
682
819
|
const messages = [
|
|
683
|
-
{ role: 'system', content: buildAgentSystem() },
|
|
684
|
-
{ role: 'user', content: goal },
|
|
820
|
+
{ role: 'system', content: sysCache.get(sysFp, 10 * 60_000) || buildAgentSystem({ goal, memoryBlock, userDoc, soulDoc: opts.soulDoc, channel, plan: opts.plan, memory }) },
|
|
821
|
+
{ role: 'user', content: String(goal || '') },
|
|
685
822
|
];
|
|
686
|
-
|
|
823
|
+
sysCache.set(sysFp, messages[0].content); // 冻结 system 进缓存 (TTL 10min)
|
|
824
|
+
const MAX_STEPS = 150;
|
|
825
|
+
const NO_PROGRESS_LIMIT = 10; // 同一工具+参数连续 N 轮 → 死循环
|
|
826
|
+
const COMPACT_AT = 30; // 消息超过 N 条 → 压缩中间轮
|
|
687
827
|
const steps = [];
|
|
828
|
+
let lastToolSig = '';
|
|
829
|
+
let noProgress = 0;
|
|
688
830
|
for (let i = 0; i < MAX_STEPS; i++) {
|
|
689
|
-
|
|
831
|
+
// 生命周期: 外部取消 → cancelled
|
|
832
|
+
if (opts.signal?.aborted) {
|
|
833
|
+
record.status = 'cancelled';
|
|
834
|
+
record.finishedAt = new Date().toISOString();
|
|
835
|
+
saveRunRecord(did, record);
|
|
836
|
+
_lastWorklog = [...steps, '⚠ 已取消'];
|
|
837
|
+
return `[手机自治执行 ${steps.length} 步 · 已取消]`;
|
|
838
|
+
}
|
|
839
|
+
let raw;
|
|
840
|
+
try {
|
|
841
|
+
raw = await webAgentLlmCall(messages, llm);
|
|
842
|
+
}
|
|
843
|
+
catch (e) {
|
|
844
|
+
// 错误恢复: LLM 调用失败 → 记录 error 生命周期 + 降级
|
|
845
|
+
const hint = classifyMobileError(e).hint;
|
|
846
|
+
record.status = 'error';
|
|
847
|
+
record.error = String(e?.message || e).slice(0, 120);
|
|
848
|
+
record.finishedAt = new Date().toISOString();
|
|
849
|
+
saveRunRecord(did, record);
|
|
850
|
+
_lastWorklog = [...steps, `⚠ ${hint}`];
|
|
851
|
+
// 0 步就失败 + silentDegrade → 抛错让调用方降级 (runLocalAgent 的 catch → 本地内置规则);
|
|
852
|
+
// 否则 (自治调用默认) 返回降级文本: 0 步 → 短提示, 已执行 N 步 → 部分成果摘要, 不让用户丢已完成的步骤。
|
|
853
|
+
if (!steps.length && opts.silentDegrade)
|
|
854
|
+
throw e;
|
|
855
|
+
if (!steps.length)
|
|
856
|
+
return `(${hint}, 请稍后重试)`;
|
|
857
|
+
return `[手机自治执行 ${steps.length} 步 · ${hint}]\n已完成的步骤结果如下:\n${steps.join('\n')}\n(${hint}, 请稍后重试)`;
|
|
858
|
+
}
|
|
690
859
|
// 2026-10-07 (模拟器验证发现): LLM 常输出前置文本+JSON ("好的,我先查询...\n{\"tool\":...}")
|
|
691
860
|
// 旧正则要求整段是 JSON → 工具调用被当普通回答, 闭环断。宽容提取第一个 JSON 对象。
|
|
692
861
|
const toolJson = extractToolCall(raw);
|
|
693
862
|
if (toolJson && typeof toolJson.tool === 'string' && WEB_AGENT_TOOLS[toolJson.tool]) {
|
|
694
|
-
|
|
863
|
+
// 无进展检测 (leo 2026-10-14: 不用超时截止任务周期, 任务该做多久做多久 —
|
|
864
|
+
// 但连续重复同一动作不放行, 由无进展兜底决定什么时候算完)
|
|
865
|
+
const sig = toolJson.tool + '|' + JSON.stringify(toolJson.args || {});
|
|
866
|
+
noProgress = (sig === lastToolSig) ? noProgress + 1 : 0;
|
|
867
|
+
lastToolSig = sig;
|
|
868
|
+
if (noProgress >= NO_PROGRESS_LIMIT) {
|
|
869
|
+
record.status = 'done';
|
|
870
|
+
record.finishedAt = new Date().toISOString();
|
|
871
|
+
saveRunRecord(did, record);
|
|
872
|
+
_lastWorklog = [...steps, `⚠ 连续 ${NO_PROGRESS_LIMIT} 轮重复同一工具调用 (${toolJson.tool}), 判断死循环提前停止`];
|
|
873
|
+
return `[手机自治执行 ${steps.length} 步 · 检测到重复动作提前停止]\n` + raw.slice(0, 200);
|
|
874
|
+
}
|
|
875
|
+
// 确认模式: 高风险工具 → paused → confirm 回调裁决 (拒绝 = 跳过执行, 结果如实告知)
|
|
876
|
+
if (isConfirmRequiredTool(toolJson.tool)) {
|
|
877
|
+
const allow = opts.confirm ? await opts.confirm(toolJson.tool, toolJson.args) : false;
|
|
878
|
+
if (!allow) {
|
|
879
|
+
const denied = `工具 ${toolJson.tool} 需要确认, 本次未执行 (请用户批准后重试)。`;
|
|
880
|
+
steps.push(`🔒 ${toolJson.tool} → ${denied}`);
|
|
881
|
+
messages.push({ role: 'assistant', content: raw });
|
|
882
|
+
messages.push({ role: 'user', content: `工具结果: ${denied}` });
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
// 工具执行 (带 args — 更灵活调用)
|
|
887
|
+
const out = await WEB_AGENT_TOOLS[toolJson.tool](toolJson.args || {});
|
|
695
888
|
steps.push(`🔧 ${toolJson.tool} → ${out.slice(0, 80)}`);
|
|
889
|
+
record.steps = steps.length;
|
|
890
|
+
saveRunRecord(did, record);
|
|
891
|
+
// 中间阶段性环境: 回调 (UI 进度)
|
|
892
|
+
if (opts.onProgress) {
|
|
893
|
+
try {
|
|
894
|
+
opts.onProgress({ step: steps.length, tool: toolJson.tool, output: out.slice(0, 120) });
|
|
895
|
+
}
|
|
896
|
+
catch { /* 回调失败不影响循环 */ }
|
|
897
|
+
}
|
|
898
|
+
if (steps.length > 60)
|
|
899
|
+
steps.splice(0, steps.length - 60); // 摘要只留最近 60 条, 不无限涨
|
|
696
900
|
messages.push({ role: 'assistant', content: raw });
|
|
697
901
|
messages.push({ role: 'user', content: `工具结果: ${out}` });
|
|
902
|
+
// 上下文压缩: 超过阈值 → 中间轮压成一条摘要 (system + 目标 + 最近 K 轮保留)
|
|
903
|
+
if (messages.length > COMPACT_AT) {
|
|
904
|
+
const keep = messages.slice(0, 2);
|
|
905
|
+
const recent = messages.slice(-10);
|
|
906
|
+
const summary = `(中间 ${messages.length - 12} 轮对话已压缩)已完成步骤: ${steps.slice(-12).map((s) => s.slice(0, 60)).join(' | ')}`;
|
|
907
|
+
keep.push({ role: 'user', content: summary });
|
|
908
|
+
messages.length = 0;
|
|
909
|
+
messages.push(...keep, ...recent);
|
|
910
|
+
}
|
|
698
911
|
continue;
|
|
699
912
|
}
|
|
700
|
-
// 无工具调用 = 最终回答
|
|
913
|
+
// 无工具调用 = 最终回答 → 记忆迭代 (新事实/偏好/关系入库) + 快照落盘 + done
|
|
914
|
+
memory.autoIterate({ goal: String(goal || ''), toolResults: steps, reply: raw, source: 'session' });
|
|
915
|
+
memory.snapshot(); // 记忆快照: 收尾落盘 (bootstrap 下次恢复)
|
|
916
|
+
record.status = 'done';
|
|
917
|
+
record.finishedAt = new Date().toISOString();
|
|
918
|
+
saveRunRecord(did, record);
|
|
701
919
|
_lastWorklog = steps.length ? [...steps, `💬 ${raw.slice(0, 60)}`] : [`💬 ${raw.slice(0, 60)}`];
|
|
702
920
|
if (steps.length)
|
|
703
921
|
return `[手机自治执行 ${steps.length} 步]\n` + raw;
|
|
704
922
|
return raw;
|
|
705
923
|
}
|
|
706
|
-
// 循环保护: 没收敛就给最后一条
|
|
924
|
+
// 循环保护: 没收敛就给最后一条 (生命周期 done)
|
|
707
925
|
const last = messages[messages.length - 1]?.content || '';
|
|
926
|
+
record.status = 'done';
|
|
927
|
+
record.finishedAt = new Date().toISOString();
|
|
928
|
+
saveRunRecord(did, record);
|
|
708
929
|
_lastWorklog = [...steps, '⚠ 达到步数上限, 已停止'];
|
|
709
930
|
return `[手机自治执行, 达到 ${MAX_STEPS} 步上限]\n${last.slice(0, 200)}`;
|
|
710
931
|
}
|