@bolloon/bolloon-agent 0.6.5 → 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/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/ha-daemon.js +85 -0
- package/dist/agents/ha-supervisor.js +146 -0
- 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 +384 -0
- package/dist/agents/opportunity-match.js +15 -3
- package/dist/agents/opportunity-web-search.js +39 -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-session-manager.js +8 -0
- package/dist/agents/pi-sdk-tools.js +180 -5
- package/dist/agents/pi-sdk.js +210 -503
- 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/bootstrap/context-collector.js +67 -7
- package/dist/bootstrap/context-hierarchy.js +13 -1
- package/dist/bootstrap/persona-loader.js +26 -0
- 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 +430 -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-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-tools.js +180 -5
- package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk.js +208 -501
- 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 +68 -7
- 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/bootstrap/persona-loader.js +27 -0
- package/dist/electron-build/bootstrap/persona-loader.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 +112 -38
- 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/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 +74 -35
- package/dist/ios/icons/icon-1400x1400.png +0 -0
- package/dist/ios/icons/icon-1400x1400.webp +0 -0
- package/dist/ios/index.html +122 -108
- package/dist/ios/mobile-agent.js +572 -45
- package/dist/ios/mobile-contacts.js +0 -4
- package/dist/ios/mobile-core.js +8079 -4590
- package/dist/ios/mobile-harness.js +1004 -0
- package/dist/ios/mobile-memory.js +317 -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 +122 -108
- package/dist/ios/mobile.js +434 -152
- package/dist/ios/routes-contacts.js +94 -1
- package/dist/ios/routes-world.js +13 -2
- package/dist/ios/server.js +54 -41
- package/dist/ios/style.css +13 -5
- 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 +2 -2
- package/dist/kernel/ha/ha-arbiter.js +177 -0
- package/dist/kernel/ha/ha-guard.js +151 -0
- package/dist/kernel/index.js +36 -0
- package/dist/kernel/model-runtime.js +8 -1
- package/dist/kernel/plan.js +5 -5
- 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 +110 -37
- package/dist/llm/system-prompt/registry.js +8 -6
- 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 +74 -35
- package/dist/web/icons/icon-1400x1400.png +0 -0
- package/dist/web/icons/icon-1400x1400.webp +0 -0
- package/dist/web/index.html +37 -25
- package/dist/web/mobile-agent.js +572 -45
- package/dist/web/mobile-contacts.js +0 -4
- package/dist/web/mobile-core.js +8079 -4590
- package/dist/web/mobile-harness.js +1004 -0
- package/dist/web/mobile-memory.js +317 -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 +122 -108
- package/dist/web/mobile.js +434 -152
- package/dist/web/routes-contacts.js +94 -1
- package/dist/web/routes-world.js +13 -2
- package/dist/web/server.js +54 -41
- package/dist/web/style.css +13 -5
- 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
package/dist/web/mobile-agent.js
CHANGED
|
@@ -10,19 +10,77 @@
|
|
|
10
10
|
* agent.info : 请求/响应 对端 DID 与能力
|
|
11
11
|
* - 本层只负责"agent 智能", 不碰存储; session 落库由 mobile-core 协调 data 层做.
|
|
12
12
|
*/
|
|
13
|
-
// ============ 身份 (
|
|
13
|
+
// ============ 身份 (DIAP KeyManager — @diap/sdk/browser, 与桌面同一套) ============
|
|
14
|
+
// 2026-10-07: 手机端身份改用 @diap/sdk 原生实现 (KeyManager 生成 Ed25519 密钥对 → did:key),
|
|
15
|
+
// 与桌面 agent-identity / x402 签名完全同构: privateKey 32B 种子 + publicKey + did:key:z...
|
|
16
|
+
// 签名走 @noble/ed25519 signAsync (与桌面 ed25519Sign 同结果)。
|
|
14
17
|
const IDENTITY_DB = 'bolloon-mobile';
|
|
15
18
|
let _identity = null;
|
|
16
19
|
// #2 手机自动入网 (browser-safe gateway, 与桌面同一协议): 检测到链接自动 join
|
|
17
20
|
import { mobileAutoJoinGateway } from './mobile-gateway.js';
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
// @diap/sdk/browser — KeyManager (密钥生成/DID 派生), @noble/ed25519 — signAsync (签名)
|
|
22
|
+
// 两个都是纯 JS 无 Node 依赖, 浏览器可用。动态 import 避免拖慢首屏。
|
|
23
|
+
let _keyMgr = null;
|
|
24
|
+
let _ed25519 = null;
|
|
25
|
+
async function loadDiap() {
|
|
26
|
+
if (_keyMgr && _ed25519)
|
|
27
|
+
return { KeyManager: _keyMgr, ed25519: _ed25519 };
|
|
28
|
+
const km = await import('@diap/sdk/browser');
|
|
29
|
+
const ed = await import('@noble/ed25519');
|
|
30
|
+
_keyMgr = km.KeyManager;
|
|
31
|
+
_ed25519 = ed;
|
|
32
|
+
return { KeyManager: km.KeyManager, ed25519: ed };
|
|
33
|
+
}
|
|
34
|
+
function bytesOf(s) {
|
|
35
|
+
const src = new TextEncoder().encode(s);
|
|
36
|
+
const out = new Uint8Array(new ArrayBuffer(src.byteLength));
|
|
37
|
+
out.set(src);
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
function bufToHex(buf) {
|
|
41
|
+
return Array.from(buf).map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
42
|
+
}
|
|
43
|
+
async function sha256Hex(text) {
|
|
44
|
+
const d = await crypto.subtle.digest('SHA-256', bytesOf(text));
|
|
45
|
+
return Array.from(new Uint8Array(d)).map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
46
|
+
}
|
|
47
|
+
function b64(buf) {
|
|
48
|
+
let out = '';
|
|
49
|
+
for (let i = 0; i < buf.length; i++)
|
|
50
|
+
out += String.fromCharCode(buf[i]);
|
|
51
|
+
return btoa(out);
|
|
52
|
+
}
|
|
53
|
+
function b64ToBytes(b64s) {
|
|
54
|
+
const bin = atob(b64s);
|
|
55
|
+
const out = new Uint8Array(bin.length);
|
|
56
|
+
for (let i = 0; i < bin.length; i++)
|
|
57
|
+
out[i] = bin.charCodeAt(i);
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
/** 用身份私钥 (32B hex) 签名任意文本 → base64 签名 + 公钥 + DID (与桌面 ed25519Sign 同语义) */
|
|
61
|
+
export async function signWithIdentity(payload) {
|
|
62
|
+
const id = await ensureIdentity();
|
|
63
|
+
if (!id.privateKey)
|
|
64
|
+
throw new Error('identity_signing_unavailable: 本机身份无私钥');
|
|
65
|
+
const { ed25519 } = await loadDiap();
|
|
66
|
+
const priv = Uint8Array.from(id.privateKey.match(/.{2}/g).map((h) => parseInt(h, 16)));
|
|
67
|
+
const sig = await ed25519.signAsync(bytesOf(String(payload ?? '')), priv);
|
|
68
|
+
return { did: id.did, publicKey: id.publicKey, signature: b64(new Uint8Array(sig)), alg: 'ed25519' };
|
|
69
|
+
}
|
|
70
|
+
/** 验证身份签名 (用身份公钥验) — 与桌面 ed25519Verify 同语义 */
|
|
71
|
+
export async function verifyIdentitySignature(payload, signatureB64, publicKeyHex) {
|
|
72
|
+
try {
|
|
73
|
+
const { ed25519 } = await loadDiap();
|
|
74
|
+
const pub = Uint8Array.from(publicKeyHex.match(/.{2}/g).map((h) => parseInt(h, 16)));
|
|
75
|
+
return ed25519.verifyAsync(b64ToBytes(signatureB64), bytesOf(String(payload ?? '')), pub);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
22
80
|
}
|
|
23
81
|
function openIdentityDb() {
|
|
24
82
|
return new Promise((resolve, reject) => {
|
|
25
|
-
const req = indexedDB.open(IDENTITY_DB,
|
|
83
|
+
const req = indexedDB.open(IDENTITY_DB, 2);
|
|
26
84
|
req.onupgradeneeded = () => {
|
|
27
85
|
const db = req.result;
|
|
28
86
|
if (!db.objectStoreNames.contains('kv'))
|
|
@@ -80,18 +138,37 @@ export async function loginIdentity(name) {
|
|
|
80
138
|
_identity = next;
|
|
81
139
|
return { ...next, loggedIn: true };
|
|
82
140
|
}
|
|
141
|
+
/** 更新身份附带条件 (邮箱/手机号/备注) — 只存本机 */
|
|
142
|
+
export async function updateIdentityProfile(partial) {
|
|
143
|
+
const id = await ensureIdentity();
|
|
144
|
+
const next = {
|
|
145
|
+
...id,
|
|
146
|
+
name: typeof partial.name === 'string' && partial.name.trim() ? partial.name.trim() : id.name,
|
|
147
|
+
email: typeof partial.email === 'string' ? partial.email.trim() : id.email,
|
|
148
|
+
phone: typeof partial.phone === 'string' ? partial.phone.trim() : id.phone,
|
|
149
|
+
note: typeof partial.note === 'string' ? partial.note.trim() : id.note,
|
|
150
|
+
};
|
|
151
|
+
await _kvPut('identity', next);
|
|
152
|
+
_identity = next;
|
|
153
|
+
return next;
|
|
154
|
+
}
|
|
83
155
|
/** 注销: 清除登录态 (保留设备 DID, 不影响 P2P/频道) */
|
|
84
156
|
export async function logoutIdentity() {
|
|
85
157
|
await _kvPut('loggedIn', false);
|
|
86
158
|
return { ok: true };
|
|
87
159
|
}
|
|
88
|
-
/** 身份状态 (
|
|
160
|
+
/** 身份状态 (含登录态 + 附带条件; 未登录时 name 置空) */
|
|
89
161
|
export async function identityStatus() {
|
|
90
162
|
const id = await ensureIdentity();
|
|
91
163
|
const loggedIn = (await _kvGet('loggedIn')) === true;
|
|
92
|
-
return {
|
|
164
|
+
return {
|
|
165
|
+
did: id.did, didShort: id.did ? id.did.slice(0, 12) : '',
|
|
166
|
+
name: loggedIn ? id.name : '', createdAt: id.createdAt, loggedIn,
|
|
167
|
+
publicKey: id.publicKey || '',
|
|
168
|
+
email: id.email || '', phone: id.phone || '', note: id.note || '',
|
|
169
|
+
};
|
|
93
170
|
}
|
|
94
|
-
/**
|
|
171
|
+
/** 获取本机身份 (首次用 DIAP KeyManager 生成 Ed25519 → did:key; 持久化) */
|
|
95
172
|
export async function ensureIdentity() {
|
|
96
173
|
if (_identity)
|
|
97
174
|
return _identity;
|
|
@@ -108,7 +185,27 @@ export async function ensureIdentity() {
|
|
|
108
185
|
_identity = id;
|
|
109
186
|
return id;
|
|
110
187
|
}
|
|
111
|
-
|
|
188
|
+
// 2026-10-07: 用 @diap/sdk KeyManager 生成 (与桌面同一套) — DID 即公钥指纹, 可签名
|
|
189
|
+
let kp;
|
|
190
|
+
try {
|
|
191
|
+
const { KeyManager } = await loadDiap();
|
|
192
|
+
kp = KeyManager.generate();
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
// 老 WebView 加载不了 @diap → 退化为哈希 DID (只读, 无签名)
|
|
196
|
+
const digest = await sha256Hex(String(Date.now()) + Math.random());
|
|
197
|
+
const legacy = { did: 'did:blln:' + digest.slice(0, 32), publicKey: '', privateKey: '', name: 'blln-mobile', createdAt: Date.now() };
|
|
198
|
+
await _kvPut('identity', legacy);
|
|
199
|
+
_identity = legacy;
|
|
200
|
+
return legacy;
|
|
201
|
+
}
|
|
202
|
+
const fresh = {
|
|
203
|
+
did: kp.did,
|
|
204
|
+
publicKey: bufToHex(kp.publicKey),
|
|
205
|
+
privateKey: bufToHex(kp.privateKey),
|
|
206
|
+
name: 'blln-mobile',
|
|
207
|
+
createdAt: Date.now(),
|
|
208
|
+
};
|
|
112
209
|
await new Promise((resolve, reject) => {
|
|
113
210
|
const tx = db.transaction('kv', 'readwrite');
|
|
114
211
|
tx.objectStore('kv').put(fresh, 'identity');
|
|
@@ -119,25 +216,35 @@ export async function ensureIdentity() {
|
|
|
119
216
|
return fresh;
|
|
120
217
|
}
|
|
121
218
|
catch {
|
|
122
|
-
const
|
|
219
|
+
const digest = await sha256Hex(String(Date.now()) + Math.random());
|
|
220
|
+
const fallback = { did: 'did:blln:' + digest.slice(0, 32), publicKey: '', privateKey: '', name: 'blln-mobile', createdAt: Date.now() };
|
|
123
221
|
_identity = fallback;
|
|
124
222
|
return fallback;
|
|
125
223
|
}
|
|
126
224
|
}
|
|
127
225
|
// ============ LLM 配置 (桌面同步注入 / 手机默认) ============
|
|
226
|
+
// 2026-10-07: 开箱即用 — 手机独立运行时无需任何 API 配置。
|
|
227
|
+
// 内置公共网关 (Cloudflare Worker: api.bolloon.cn) 转发到 GLM 上游;
|
|
228
|
+
// 公共通道每天 ≤500 次/人; 桌面同步注入自己的 key 后自动覆盖。
|
|
229
|
+
const BUILTIN_LLM = Object.freeze({
|
|
230
|
+
baseUrl: 'https://api.bolloon.cn/v1',
|
|
231
|
+
apiKey: 'bolloon-free',
|
|
232
|
+
model: 'glm-5.3',
|
|
233
|
+
maxTokens: 16384,
|
|
234
|
+
});
|
|
128
235
|
let _llmConfig = null;
|
|
129
236
|
/** 最近一次 runLocalAgent 的执行过程摘要 (每步 onStep 文本), 由 message.send 广播给工作记录 UI */
|
|
130
237
|
let _lastWorklog = [];
|
|
131
238
|
export function getLastWorklog() { return _lastWorklog; }
|
|
132
|
-
/** 注入 LLM 配置 (由 mobile-core 在 data.llm-config.reply 同步后调用) */
|
|
239
|
+
/** 注入 LLM 配置 (由 mobile-core 在 data.llm-config.reply 同步后调用); null = 用内置免费网关 */
|
|
133
240
|
export function setLlmConfig(cfg) {
|
|
134
241
|
_llmConfig = cfg;
|
|
135
242
|
// 立即注入 native bridge (agentConfigure), 让 AgentRuntime 马上可用, 不必等下次 runLocalAgent
|
|
136
243
|
applyLlmConfigToBridge().catch(() => { });
|
|
137
244
|
}
|
|
138
|
-
/** 当前 LLM 配置 (
|
|
245
|
+
/** 当前 LLM 配置 (未同步则返回内置免费网关 → 手机开箱即用) */
|
|
139
246
|
export function getLlmConfig() {
|
|
140
|
-
return _llmConfig;
|
|
247
|
+
return _llmConfig || BUILTIN_LLM;
|
|
141
248
|
}
|
|
142
249
|
/** 把配置注入 Capacitor RokidBridge (agentConfigure), 让 Kotlin AgentRuntime 用同步来的 LLM */
|
|
143
250
|
async function applyLlmConfigToBridge() {
|
|
@@ -346,8 +453,6 @@ export function formatMobileJoinResult(r) {
|
|
|
346
453
|
// ============ 本地执行 (Kotlin AgentRuntime / 离线兜底) ============
|
|
347
454
|
/** 手机端本地 agent 执行 (优先 Kotlin, 离线内置规则) */
|
|
348
455
|
export async function runLocalAgent(goal) {
|
|
349
|
-
const win = typeof window !== 'undefined' ? window : null;
|
|
350
|
-
const cap = win?.Capacitor;
|
|
351
456
|
// 2026-09-15: 「读入网说明 → 入网」在手机端本地自足执行 (先于 Kotlin 桥: 原生工具集里没有入网能力,
|
|
352
457
|
// 交给它只会得到空转回复)。这样浏览器 / WebView / 真机三种环境行为一致。
|
|
353
458
|
const joinDocUrl = detectJoinDocUrl(goal);
|
|
@@ -359,20 +464,22 @@ export async function runLocalAgent(goal) {
|
|
|
359
464
|
_lastWorklog = [..._lastWorklog, ...r.steps.map((s) => `${s.ok ? '✓' : '✗'} ${s.step}: ${s.note}`)];
|
|
360
465
|
return formatMobileJoinResult(r);
|
|
361
466
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
try {
|
|
365
|
-
await applyLlmConfigToBridge();
|
|
366
|
-
const r = await bridge.runAgent({ goal });
|
|
367
|
-
_lastWorklog = (r && Array.isArray(r.worklog)) ? r.worklog.map((x) => String(x)) : [];
|
|
368
|
-
return r?.result || '(无返回)';
|
|
369
|
-
}
|
|
370
|
-
catch (e) {
|
|
371
|
-
throw new Error('AgentRuntime: ' + String(e?.message || e).slice(0, 60));
|
|
372
|
-
}
|
|
373
|
-
}
|
|
467
|
+
// 普通手机 Agent 永远走 WebView 本地执行链。
|
|
468
|
+
// Android 无障碍仅适用于未来的跨 App 控制,不得阻塞手机端基础执行;iOS 与 Android 因此行为一致。
|
|
374
469
|
// 内置极简回复 (纯离线可用)
|
|
375
470
|
const t = (goal || '').trim();
|
|
471
|
+
// 2026-10-07: 手机自治 — 先用 LLM 循环 (经内置公共网关), 失败再降级纯规则
|
|
472
|
+
// 2026-10-16: silentDegrade=true — LLM 不可达时 → throw → 下面 catch → 内置离线规则,
|
|
473
|
+
// (上一层 runWebAgentLoop 若默认返回降级文本, 内置规则层就永远走不到)
|
|
474
|
+
try {
|
|
475
|
+
const llm = getLlmConfig();
|
|
476
|
+
if (llm && llm.baseUrl) {
|
|
477
|
+
const reply = await runWebAgentLoop(goal, { baseUrl: llm.baseUrl, apiKey: llm.apiKey, model: llm.model, maxTokens: llm.maxTokens }, { silentDegrade: true });
|
|
478
|
+
if (reply)
|
|
479
|
+
return reply;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
catch { /* 网络/网关不可达 → 降级内置规则 */ }
|
|
376
483
|
if (t.includes('你好') || t === 'hi' || t === 'hello')
|
|
377
484
|
return '你好! 我是炁球 (Bolloon), 已在手机本地独立运行 (Agent 功能层 + 数据同步层分离)。';
|
|
378
485
|
if (t.includes('身份') || t.includes('did')) {
|
|
@@ -407,6 +514,421 @@ export async function runLocalAgent(goal) {
|
|
|
407
514
|
}
|
|
408
515
|
return `已收到: "${(goal || '').slice(0, 40)}"。这是手机端 Agent 功能层的本地执行 (数据同步与 agent 功能已分离)。`;
|
|
409
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, 可查)
|
|
531
|
+
// 能力范围: 手机 WebView 能做的 (身份/钱包/入网/P2P/信息查询), 不假装能做 fs/shell。
|
|
532
|
+
// 2026-10-07: persona 注入 — 性格/价值观/兴趣/说话方式来自 mobile-persona (设置 → 性格)。
|
|
533
|
+
import { loadPersona, buildPersonaPrompt } from './mobile-persona.js';
|
|
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 = {}) {
|
|
577
|
+
const persona = loadPersona();
|
|
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
|
+
: '';
|
|
602
|
+
return [
|
|
603
|
+
'你是手机端 Bolloon 智能体 (自治节点), 用中文简洁回复。',
|
|
604
|
+
personaBlock,
|
|
605
|
+
userBlock,
|
|
606
|
+
memoryBlock,
|
|
607
|
+
stableBlock,
|
|
608
|
+
'你有以下工具, 按需调用 (JSON: {"tool":"名字","args":{...}}), 不需要工具就直接回答:',
|
|
609
|
+
'- get_status: 查本机身份/入网/钱包/连接状态 (args: 无)',
|
|
610
|
+
'- get_wallet: 查钱包余额 (args: 无)',
|
|
611
|
+
'- get_identity: 查本机身份证明 (args: 无)',
|
|
612
|
+
'- get_contacts: 查联系方式授权状态 (args: 无)',
|
|
613
|
+
'每轮最多用一个工具; 拿到结果后继续思考, 直到任务完成给出最终回答。',
|
|
614
|
+
progressBlock,
|
|
615
|
+
planHint,
|
|
616
|
+
channelBlock,
|
|
617
|
+
'能动性: 先动手别先问; 做到底 (交付=能用的结果); 一个工具不够就换方式继续; 被阻塞如实说, 不编造。',
|
|
618
|
+
].filter(Boolean).join('\n');
|
|
619
|
+
}
|
|
620
|
+
// 2026-10-16: 工具签名带 args (更灵活调用; 现有工具忽略 args)
|
|
621
|
+
const WEB_AGENT_TOOLS = {
|
|
622
|
+
get_status: async () => {
|
|
623
|
+
try {
|
|
624
|
+
const id = await ensureIdentity();
|
|
625
|
+
return `身份: ${id.did.slice(0, 16)}... 名字: ${id.name}`;
|
|
626
|
+
}
|
|
627
|
+
catch (e) {
|
|
628
|
+
return '身份读取失败: ' + String(e?.message || e).slice(0, 60);
|
|
629
|
+
}
|
|
630
|
+
},
|
|
631
|
+
get_wallet: async () => {
|
|
632
|
+
try {
|
|
633
|
+
const id = await ensureIdentity();
|
|
634
|
+
const w = await import('./mobile-wallet.js');
|
|
635
|
+
const info = await w.walletForAgent(id.did);
|
|
636
|
+
if (!info.exists)
|
|
637
|
+
return '本机没有授权给当前智能体的钱包 (我 → 设置 → 钱包 可创建/授权)。';
|
|
638
|
+
const w0 = info.wallets[0];
|
|
639
|
+
let s = `钱包: ${w0.name || w0.id} (${w0.address.slice(0, 12)}...)`;
|
|
640
|
+
if (w0.unlocked) {
|
|
641
|
+
try {
|
|
642
|
+
s += ` 余额: ${await w.walletBalance(w0.id)}`;
|
|
643
|
+
}
|
|
644
|
+
catch {
|
|
645
|
+
s += ' 余额: 查询失败';
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
else
|
|
649
|
+
s += ' (未解锁)';
|
|
650
|
+
return s;
|
|
651
|
+
}
|
|
652
|
+
catch (e) {
|
|
653
|
+
return '钱包查询失败: ' + String(e?.message || e).slice(0, 60);
|
|
654
|
+
}
|
|
655
|
+
},
|
|
656
|
+
get_identity: async () => {
|
|
657
|
+
const id = await ensureIdentity();
|
|
658
|
+
return `身份证明: ${id.did} 名字: ${id.name}`;
|
|
659
|
+
},
|
|
660
|
+
get_contacts: async () => {
|
|
661
|
+
try {
|
|
662
|
+
return '联系方式与授权模块可用 (详见「我 → 联系方式与授权」)。';
|
|
663
|
+
}
|
|
664
|
+
catch {
|
|
665
|
+
return '联系人模块暂不可用';
|
|
666
|
+
}
|
|
667
|
+
},
|
|
668
|
+
};
|
|
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 兼容, 经内置公共网关) — 自适应超时 + 网络错误退避重试 */
|
|
685
|
+
async function webAgentLlmCall(messages, llm) {
|
|
686
|
+
// 2026-10-15: 网关不可达时**快速降级**而不是挂死 — 公共网关限流/断网时 fetch 可能永久挂起,
|
|
687
|
+
// 手机端 runLocalAgent 会先走 LLM 再降级内置规则, 挂起 = 测试 20s 超时 + 真机"发消息无反应"。
|
|
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 调用失败');
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* 宽容提取 LLM 回复里的工具调用 JSON。
|
|
727
|
+
* 匹配模式: {"tool":"...","args":{...}} — 容忍前置文本、代码块围栏、尾部文本。
|
|
728
|
+
* 用花括号平衡计数提取完整对象 (非贪婪正则会被内层嵌套 {} 截断 — 模拟器验证踩到)。
|
|
729
|
+
* 找不到合法 tool 字段返回 null (当作普通回答)。
|
|
730
|
+
*/
|
|
731
|
+
function extractToolCall(raw) {
|
|
732
|
+
if (!raw)
|
|
733
|
+
return null;
|
|
734
|
+
// 1) 直接找第一个 '{"' 开头、括号平衡的完整 JSON 对象
|
|
735
|
+
const start = raw.indexOf('{"');
|
|
736
|
+
if (start >= 0) {
|
|
737
|
+
try {
|
|
738
|
+
const obj = parseBalancedJson(raw, start);
|
|
739
|
+
if (obj && typeof obj.tool === 'string') {
|
|
740
|
+
return { tool: obj.tool, args: (obj.args && typeof obj.args === 'object' ? obj.args : {}) };
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
catch { /* 不是 JSON, 继续 */ }
|
|
744
|
+
}
|
|
745
|
+
// 2) 代码块围栏内的 JSON (```json ... ```)
|
|
746
|
+
const fm = /```(?:json)?\s*([\s\S]*?)```/.exec(raw);
|
|
747
|
+
if (fm) {
|
|
748
|
+
try {
|
|
749
|
+
const obj = JSON.parse(fm[1]);
|
|
750
|
+
if (obj && typeof obj.tool === 'string') {
|
|
751
|
+
return { tool: obj.tool, args: (obj.args && typeof obj.args === 'object' ? obj.args : {}) };
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
catch { /* 忽略 */ }
|
|
755
|
+
}
|
|
756
|
+
return null;
|
|
757
|
+
}
|
|
758
|
+
/** 从 start 位置解析一个括号平衡的 JSON 对象 (容忍内部嵌套字符串/大括号) */
|
|
759
|
+
function parseBalancedJson(raw, start) {
|
|
760
|
+
if (raw[start] !== '{')
|
|
761
|
+
throw new Error('not object');
|
|
762
|
+
let depth = 0;
|
|
763
|
+
let inString = false;
|
|
764
|
+
let escaped = false;
|
|
765
|
+
for (let i = start; i < raw.length; i++) {
|
|
766
|
+
const ch = raw[i];
|
|
767
|
+
if (inString) {
|
|
768
|
+
if (escaped) {
|
|
769
|
+
escaped = false;
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
if (ch === '\\') {
|
|
773
|
+
escaped = true;
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
if (ch === '"')
|
|
777
|
+
inString = false;
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
if (ch === '"') {
|
|
781
|
+
inString = true;
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
if (ch === '{')
|
|
785
|
+
depth++;
|
|
786
|
+
else if (ch === '}') {
|
|
787
|
+
depth--;
|
|
788
|
+
if (depth === 0)
|
|
789
|
+
return JSON.parse(raw.slice(start, i + 1));
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
throw new Error('unbalanced');
|
|
793
|
+
}
|
|
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('|'));
|
|
819
|
+
const messages = [
|
|
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 || '') },
|
|
822
|
+
];
|
|
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 条 → 压缩中间轮
|
|
827
|
+
const steps = [];
|
|
828
|
+
let lastToolSig = '';
|
|
829
|
+
let noProgress = 0;
|
|
830
|
+
for (let i = 0; i < MAX_STEPS; i++) {
|
|
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
|
+
}
|
|
859
|
+
// 2026-10-07 (模拟器验证发现): LLM 常输出前置文本+JSON ("好的,我先查询...\n{\"tool\":...}")
|
|
860
|
+
// 旧正则要求整段是 JSON → 工具调用被当普通回答, 闭环断。宽容提取第一个 JSON 对象。
|
|
861
|
+
const toolJson = extractToolCall(raw);
|
|
862
|
+
if (toolJson && typeof toolJson.tool === 'string' && WEB_AGENT_TOOLS[toolJson.tool]) {
|
|
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 || {});
|
|
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 条, 不无限涨
|
|
900
|
+
messages.push({ role: 'assistant', content: raw });
|
|
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
|
+
}
|
|
911
|
+
continue;
|
|
912
|
+
}
|
|
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);
|
|
919
|
+
_lastWorklog = steps.length ? [...steps, `💬 ${raw.slice(0, 60)}`] : [`💬 ${raw.slice(0, 60)}`];
|
|
920
|
+
if (steps.length)
|
|
921
|
+
return `[手机自治执行 ${steps.length} 步]\n` + raw;
|
|
922
|
+
return raw;
|
|
923
|
+
}
|
|
924
|
+
// 循环保护: 没收敛就给最后一条 (生命周期 done)
|
|
925
|
+
const last = messages[messages.length - 1]?.content || '';
|
|
926
|
+
record.status = 'done';
|
|
927
|
+
record.finishedAt = new Date().toISOString();
|
|
928
|
+
saveRunRecord(did, record);
|
|
929
|
+
_lastWorklog = [...steps, '⚠ 达到步数上限, 已停止'];
|
|
930
|
+
return `[手机自治执行, 达到 ${MAX_STEPS} 步上限]\n${last.slice(0, 200)}`;
|
|
931
|
+
}
|
|
410
932
|
let _send = null;
|
|
411
933
|
let _ownDid = '';
|
|
412
934
|
const replyHandlers = new Set();
|
|
@@ -537,31 +1059,33 @@ export async function runPhoneAgent(goal) {
|
|
|
537
1059
|
const id = await ensureIdentity();
|
|
538
1060
|
try {
|
|
539
1061
|
if (native) {
|
|
540
|
-
await
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
1062
|
+
const st = await bridge.agentStatus?.();
|
|
1063
|
+
if (st?.accessibilityReady !== false) {
|
|
1064
|
+
await applyLlmConfigToBridge();
|
|
1065
|
+
const r = await bridge.runAgent({ goal });
|
|
1066
|
+
const result = r?.result || '(无返回)';
|
|
1067
|
+
return {
|
|
1068
|
+
ok: !/^\[Agent 异常|^STOPPED|^MAX_STEPS/.test(result),
|
|
1069
|
+
goal,
|
|
1070
|
+
result,
|
|
1071
|
+
did: id.did,
|
|
1072
|
+
mode: 'native',
|
|
1073
|
+
agentId: r?.agentId,
|
|
1074
|
+
stepCount: r?.stepCount,
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
553
1077
|
}
|
|
554
1078
|
// 离线 fallback: 内置规则 (不依赖 LLM/原生执行能力, 手机仍自治可用)
|
|
555
1079
|
const reply = await runLocalAgent(goal);
|
|
556
1080
|
return {
|
|
557
1081
|
ok: true, goal, result: reply, did: id.did, mode: 'fallback',
|
|
558
|
-
hint: '
|
|
1082
|
+
hint: '当前使用手机本地 Agent;Android/iOS 均可直接执行手机端任务。',
|
|
559
1083
|
};
|
|
560
1084
|
}
|
|
561
1085
|
catch (e) {
|
|
562
1086
|
return {
|
|
563
1087
|
ok: false, goal, error: String(e?.message || e).slice(0, 100), did: id.did, mode: native ? 'native' : 'fallback',
|
|
564
|
-
hint: '
|
|
1088
|
+
hint: '任务没跑起来。请先检查「我 → 设置 → API 配置」中的 baseUrl、apiKey 和 model。',
|
|
565
1089
|
};
|
|
566
1090
|
}
|
|
567
1091
|
}
|
|
@@ -583,12 +1107,15 @@ export async function phoneStatus() {
|
|
|
583
1107
|
}
|
|
584
1108
|
catch { /* 忽略 */ }
|
|
585
1109
|
}
|
|
1110
|
+
const capabilities = ['chat', 'local-agent'];
|
|
1111
|
+
if (!native || accReady)
|
|
1112
|
+
capabilities.push('phone-control');
|
|
586
1113
|
return {
|
|
587
1114
|
ok: true,
|
|
588
1115
|
did: id.did,
|
|
589
1116
|
mode: native ? 'native' : 'fallback',
|
|
590
1117
|
llm,
|
|
591
|
-
capabilities
|
|
1118
|
+
capabilities,
|
|
592
1119
|
};
|
|
593
1120
|
}
|
|
594
1121
|
/** 取消当前手机 Agent 任务 (phone.agent.cancel) */
|
|
@@ -639,4 +1166,4 @@ export async function handleIncomingPhoneMessage(type, payload, fromPeer) {
|
|
|
639
1166
|
}
|
|
640
1167
|
catch { /* 控制消息处理失败静默 */ }
|
|
641
1168
|
}
|
|
642
|
-
export default { ensureIdentity, loginIdentity, logoutIdentity, identityStatus, runLocalAgent, setAgentTransport, onAgentReply, callRemoteAgent, handleIncomingAgentMessage, notifyAgentReply, onInboundChat, setLlmConfig, getLlmConfig, runPhoneAgent, phoneStatus, cancelPhoneAgent, handleIncomingPhoneMessage };
|
|
1169
|
+
export default { ensureIdentity, loginIdentity, logoutIdentity, identityStatus, runLocalAgent, setAgentTransport, onAgentReply, callRemoteAgent, handleIncomingAgentMessage, notifyAgentReply, onInboundChat, setLlmConfig, getLlmConfig, runPhoneAgent, phoneStatus, cancelPhoneAgent, handleIncomingPhoneMessage, runWebAgentLoop };
|