@bolloon/bolloon-agent 0.4.33 → 0.5.0
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/contacts/chain.js +27 -7
- package/dist/agents/execution-supervisor.js +493 -35
- package/dist/agents/external-events.js +35 -6
- package/dist/agents/gateway-group.js +100 -12
- package/dist/agents/goal-criteria.js +10 -3
- package/dist/agents/goal-flywheel/block-executor.js +346 -0
- package/dist/agents/goal-flywheel/continuation-decision.js +592 -0
- package/dist/agents/goal-flywheel/goal-change.js +754 -0
- package/dist/agents/goal-flywheel/index.js +26 -0
- package/dist/agents/goal-flywheel/memory-layers.js +227 -0
- package/dist/agents/goal-flywheel/run-closure.js +809 -0
- package/dist/agents/goal-flywheel/skill-candidate.js +628 -0
- package/dist/agents/goal-flywheel/types.js +329 -0
- package/dist/agents/goal-flywheel/wiring/change.js +288 -0
- package/dist/agents/goal-flywheel/wiring/closure.js +697 -0
- package/dist/agents/goal-flywheel/wiring/continuation.js +432 -0
- package/dist/agents/goal-flywheel/wiring/contract.js +270 -0
- package/dist/agents/goal-flywheel/wiring/index.js +26 -0
- package/dist/agents/goal-flywheel/wiring/monitor.js +254 -0
- package/dist/agents/goal-flywheel/wiring/seams.js +592 -0
- package/dist/agents/goal-flywheel/work-contract.js +478 -0
- package/dist/agents/goal-flywheel/work-monitor.js +488 -0
- package/dist/agents/goal-flywheel-wiring.js +1870 -0
- package/dist/agents/goal-state-reducer.js +406 -0
- package/dist/agents/goal-store.js +35 -1
- package/dist/agents/mobile-flywheel-view.js +144 -0
- package/dist/agents/mobile-task-actions-verify.js +74 -0
- package/dist/agents/mobile-task-actions.js +178 -0
- package/dist/agents/mobile-task-views.js +199 -0
- package/dist/agents/network-pulse.js +304 -18
- package/dist/agents/pi-sdk.js +75 -20
- package/dist/agents/run-store.js +18 -5
- package/dist/agents/skill-readiness.js +52 -10
- package/dist/agents/subagent-manager.js +209 -3
- package/dist/agents/task/task-runner.js +9 -3
- package/dist/agents/task-group.js +8 -51
- package/dist/agents/task-public-text.js +89 -0
- package/dist/cli/commands/index.js +1 -0
- package/dist/cli/commands/tasks.js +350 -2
- package/dist/cli/identity-command.js +177 -0
- package/dist/cli/setup-wizard.js +71 -0
- package/dist/cli/update-commands.js +150 -24
- package/dist/cli-entry.js +12 -2
- package/dist/electron-build/agents/contacts/chain.js +26 -6
- package/dist/electron-build/agents/contacts/chain.js.map +1 -1
- package/dist/electron-build/agents/execution-supervisor.js +492 -34
- package/dist/electron-build/agents/execution-supervisor.js.map +1 -1
- package/dist/electron-build/agents/external-events.js +34 -5
- package/dist/electron-build/agents/external-events.js.map +1 -1
- package/dist/electron-build/agents/goal-criteria.js +10 -3
- package/dist/electron-build/agents/goal-criteria.js.map +1 -1
- package/dist/electron-build/agents/goal-flywheel/continuation-decision.js +598 -0
- package/dist/electron-build/agents/goal-flywheel/continuation-decision.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/goal-change.js +773 -0
- package/dist/electron-build/agents/goal-flywheel/goal-change.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/memory-layers.js +269 -0
- package/dist/electron-build/agents/goal-flywheel/memory-layers.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/run-closure.js +818 -0
- package/dist/electron-build/agents/goal-flywheel/run-closure.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/skill-candidate.js +642 -0
- package/dist/electron-build/agents/goal-flywheel/skill-candidate.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/types.js +333 -0
- package/dist/electron-build/agents/goal-flywheel/types.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/change.js +294 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/change.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/closure.js +742 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/closure.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/continuation.js +444 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/continuation.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/contract.js +279 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/contract.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/index.js +111 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/index.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/monitor.js +264 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/monitor.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/seams.js +612 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/seams.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/work-contract.js +488 -0
- package/dist/electron-build/agents/goal-flywheel/work-contract.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/work-monitor.js +501 -0
- package/dist/electron-build/agents/goal-flywheel/work-monitor.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel-wiring.js +1962 -0
- package/dist/electron-build/agents/goal-flywheel-wiring.js.map +1 -0
- package/dist/electron-build/agents/goal-state-reducer.js +414 -0
- package/dist/electron-build/agents/goal-state-reducer.js.map +1 -0
- package/dist/electron-build/agents/goal-store.js +35 -1
- package/dist/electron-build/agents/goal-store.js.map +1 -1
- package/dist/electron-build/agents/network-pulse.js +307 -19
- package/dist/electron-build/agents/network-pulse.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk.js +74 -19
- package/dist/electron-build/agents/pi-sdk.js.map +1 -1
- package/dist/electron-build/agents/run-store.js +20 -5
- package/dist/electron-build/agents/run-store.js.map +1 -1
- package/dist/electron-build/agents/skill-readiness.js +52 -9
- package/dist/electron-build/agents/skill-readiness.js.map +1 -1
- package/dist/electron-build/agents/task/task-runner.js +8 -2
- package/dist/electron-build/agents/task/task-runner.js.map +1 -1
- package/dist/electron-build/orbitdb/cid-database.js +83 -17
- package/dist/electron-build/orbitdb/cid-database.js.map +1 -1
- package/dist/electron-build/orbitdb/ipfs-node.js +31 -2
- package/dist/electron-build/orbitdb/ipfs-node.js.map +1 -1
- package/dist/electron-build/utils/dual-source.js +471 -0
- package/dist/electron-build/utils/dual-source.js.map +1 -0
- package/dist/electron-build/utils/update-manager.js +331 -57
- package/dist/electron-build/utils/update-manager.js.map +1 -1
- package/dist/electron-build/utils/update-state.js +16 -1
- package/dist/electron-build/utils/update-state.js.map +1 -1
- package/dist/electron-build/utils/version-info.js +82 -2
- package/dist/electron-build/utils/version-info.js.map +1 -1
- package/dist/index.js +103 -12
- package/dist/ios/chain-explorer.js +617 -0
- package/dist/ios/explorer.html +163 -0
- package/dist/ios/index.html +96 -0
- package/dist/ios/mobile-agent.js +2 -2
- package/dist/ios/mobile-contacts.js +329 -0
- package/dist/ios/mobile-core.js +1379 -226
- package/dist/ios/mobile-privacy.js +185 -0
- package/dist/ios/mobile.css +19 -0
- package/dist/ios/mobile.html +96 -0
- package/dist/ios/mobile.js +706 -7
- package/dist/ios/routes-chain-index.js +108 -0
- package/dist/ios/routes-contacts.js +420 -0
- package/dist/ios/routes-onchain-trade.js +99 -0
- package/dist/ios/routes-x402-info.js +1 -0
- package/dist/ios/server.js +762 -0
- package/dist/ios/task-frame-server.js +211 -0
- package/dist/orbitdb/cid-database.js +81 -16
- package/dist/orbitdb/ipfs-node.js +31 -2
- package/dist/utils/dual-source.js +426 -0
- package/dist/utils/update-cli.js +16 -21
- package/dist/utils/update-health.js +35 -5
- package/dist/utils/update-manager.js +329 -57
- package/dist/utils/update-state.js +16 -1
- package/dist/utils/version-info.js +75 -1
- package/dist/web/mobile-contacts.js +44 -11
- package/dist/web/mobile-core.js +1197 -513
- package/dist/web/mobile-tasks.js +339 -0
- package/dist/web/mobile.css +19 -0
- package/dist/web/mobile.html +64 -0
- package/dist/web/mobile.js +531 -1
- package/dist/web/routes-mobile-tasks.js +353 -0
- package/dist/web/server.js +163 -4
- package/package.json +3 -1
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* task-frame-server.ts — bolloon-task/1 任务帧接收端 (P3, 2026-09-21)
|
|
3
|
+
*
|
|
4
|
+
* 把 `task-transport` 发来的帧 (request / accept / reject / result) 接到**本机收件箱与台账**:
|
|
5
|
+
* · 每个信封都**必须过验签** (`verifyTaskEnvelope` + 帧里带来的公钥), 验不过 → 如实拒, 不落盘;
|
|
6
|
+
* · `task_request` 只落**待处理收件箱** (`~/.bolloon/tasks/inbox/`): 不执行、不付款、不自动接单;
|
|
7
|
+
* · 状态推进一律走 `task-contract.checkTaskMove` (非法迁移拒绝, 不静默修正);
|
|
8
|
+
* · 与 `agent-delegate-server` 同一套 express + JSON 形状 (薄; 不含业务判断)。
|
|
9
|
+
*
|
|
10
|
+
* 起法 (E2E / 长跑节点):
|
|
11
|
+
* npx tsx scripts/task-frame-node.ts --port 54901 --home /tmp/nodeA
|
|
12
|
+
*
|
|
13
|
+
* 安全口径 (如实): 收帧端点**不设身份认证** —— 它就是一个"信箱": 收到的东西一律先验签, 再只能落
|
|
14
|
+
* 待处理收件箱 (没有任何执行/付款副作用)。要不要接单是**本机用户**用 `bolloon task accept` 决定的事。
|
|
15
|
+
*/
|
|
16
|
+
import express from 'express';
|
|
17
|
+
import { validateTaskRequest, verifyTaskEnvelope, checkTaskMove, } from '../agents/task-contract.js';
|
|
18
|
+
import { verifierFor } from '../agents/local-signer.js';
|
|
19
|
+
import { saveIncomingRequest, readInboxItem, patchInboxItem, readLocalTask, patchLocalTask, saveLocalTask, saveResult, saveBody, listInbox, } from '../agents/task-inbox.js';
|
|
20
|
+
import { parseTaskFrame } from '../agents/task-transport.js';
|
|
21
|
+
/** 校验一帧的信封签名 (帧里带的是公钥 hex; 这是公开材料, 不涉及任何私钥) */
|
|
22
|
+
async function verifyFrame(frame) {
|
|
23
|
+
const v = verifierFor(frame.signer?.publicKeyHex || '');
|
|
24
|
+
if (!v)
|
|
25
|
+
return false;
|
|
26
|
+
return await verifyTaskEnvelope(frame.envelope, v);
|
|
27
|
+
}
|
|
28
|
+
/** 按 14 态状态机逐步推进 (非法就停, 返回停在哪一步) */
|
|
29
|
+
function advance(state, steps) {
|
|
30
|
+
let cur = state;
|
|
31
|
+
const applied = [];
|
|
32
|
+
for (const to of steps) {
|
|
33
|
+
if (cur === to)
|
|
34
|
+
continue;
|
|
35
|
+
const mv = checkTaskMove(cur, to);
|
|
36
|
+
if (!mv.ok)
|
|
37
|
+
return { state: cur, applied, rejected: mv.reason };
|
|
38
|
+
cur = to;
|
|
39
|
+
applied.push(to);
|
|
40
|
+
}
|
|
41
|
+
return { state: cur, applied };
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* 处理一帧 —— 唯一入口 (HTTP / iroh / 测试都走这里, 语义只有一套)。
|
|
45
|
+
*/
|
|
46
|
+
export async function handleTaskFrame(frame, opts = {}) {
|
|
47
|
+
const home = opts.home;
|
|
48
|
+
const env = frame.envelope;
|
|
49
|
+
const verified = await verifyFrame(frame);
|
|
50
|
+
switch (frame.frame) {
|
|
51
|
+
case 'task_request': {
|
|
52
|
+
if (!verified) {
|
|
53
|
+
return { ok: false, error: 'SIGNATURE_INVALID', detail: '任务请求信封验签不过 (用帧里 signer.publicKeyHex 验的), 不落盘' };
|
|
54
|
+
}
|
|
55
|
+
const req = env;
|
|
56
|
+
const v = validateTaskRequest(req, { now: Date.now() });
|
|
57
|
+
if (!v.ok) {
|
|
58
|
+
// 契约层说不行 → 如实拒 (版本/字段/预算/deadline/签名逐条在 issues 里)
|
|
59
|
+
return { ok: false, error: 'INVALID_REQUEST', detail: v.issues.join('; ').slice(0, 400) };
|
|
60
|
+
}
|
|
61
|
+
const saved = saveIncomingRequest(req, {
|
|
62
|
+
home,
|
|
63
|
+
requestVerified: true,
|
|
64
|
+
buyerPublicKeyHex: frame.signer.publicKeyHex,
|
|
65
|
+
replyTo: frame.replyTo || null,
|
|
66
|
+
});
|
|
67
|
+
if (!saved.ok)
|
|
68
|
+
return { ok: false, error: 'INBOX_WRITE_FAILED', detail: saved.error };
|
|
69
|
+
const known = !opts.capabilities?.length || opts.capabilities.includes(req.capability);
|
|
70
|
+
return {
|
|
71
|
+
ok: true,
|
|
72
|
+
duplicate: saved.dup,
|
|
73
|
+
note: saved.dup
|
|
74
|
+
? `同一 requestId 已在收件箱 (幂等: 不重复接受/不重复执行/不重复收费) — ${saved.reason}`
|
|
75
|
+
: `已落待处理收件箱 (不执行、不付款、不自动接单)${known ? '' : ` · 本机没有声明能力 '${req.capability}' (接单时会被拒)`}`,
|
|
76
|
+
facts: { requestId: req.requestId, taskId: req.taskId, state: saved.item?.state ?? 'pending', receivedAt: saved.item?.receivedAt },
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
case 'task_accept': {
|
|
80
|
+
if (!verified)
|
|
81
|
+
return { ok: false, error: 'SIGNATURE_INVALID', detail: '接受回执验签不过' };
|
|
82
|
+
const acc = env;
|
|
83
|
+
const cur = readLocalTask(acc.requestId, home);
|
|
84
|
+
if (!cur) {
|
|
85
|
+
// 没有本地台账: 只存回执本身, 并如实标注"没有对应台账"
|
|
86
|
+
const rec = {
|
|
87
|
+
protocol: 'bolloon-task/1',
|
|
88
|
+
requestId: acc.requestId,
|
|
89
|
+
taskId: acc.taskId,
|
|
90
|
+
capability: '',
|
|
91
|
+
instruction: '',
|
|
92
|
+
buyerDid: '',
|
|
93
|
+
providerDid: acc.providerDid,
|
|
94
|
+
target: frame.signer.did,
|
|
95
|
+
transportKind: 'inbound',
|
|
96
|
+
state: 'accepted',
|
|
97
|
+
sentAt: frame.sentAt,
|
|
98
|
+
updatedAt: Date.now(),
|
|
99
|
+
requestSignature: '',
|
|
100
|
+
accept: acc,
|
|
101
|
+
notes: ['本机没有对应的本地任务台账 (只存了对端回执; 不伪造本地发送记录)'],
|
|
102
|
+
};
|
|
103
|
+
saveLocalTask(rec, home);
|
|
104
|
+
return { ok: true, note: '已存对端接受回执 (本机没有对应的本地台账)', facts: { requestId: acc.requestId, state: 'accepted' } };
|
|
105
|
+
}
|
|
106
|
+
const mv = advance(cur.state, ['accepted']);
|
|
107
|
+
if (mv.rejected) {
|
|
108
|
+
return { ok: false, error: 'TASK_TRANSITION_REJECTED', detail: `对端回执 accepted 与本机状态 ${cur.state} 不合: ${mv.rejected}` };
|
|
109
|
+
}
|
|
110
|
+
patchLocalTask(acc.requestId, { state: mv.state, accept: acc, providerPublicKeyHex: frame.signer.publicKeyHex, notes: [...cur.notes, `收到接受回执 ${new Date(frame.sentAt).toISOString()}`] }, home);
|
|
111
|
+
const inb = readInboxItem(acc.requestId, home);
|
|
112
|
+
if (inb)
|
|
113
|
+
patchInboxItem(acc.requestId, { state: 'accepted', accept: acc, providerPublicKeyHex: frame.signer.publicKeyHex }, home);
|
|
114
|
+
return { ok: true, note: `接受回执已入库 (state=${mv.state})`, facts: { requestId: acc.requestId, state: mv.state } };
|
|
115
|
+
}
|
|
116
|
+
case 'task_reject': {
|
|
117
|
+
if (!verified)
|
|
118
|
+
return { ok: false, error: 'SIGNATURE_INVALID', detail: '拒绝回执验签不过' };
|
|
119
|
+
const rej = env;
|
|
120
|
+
const cur = readLocalTask(rej.requestId, home);
|
|
121
|
+
if (!cur) {
|
|
122
|
+
return { ok: true, note: '收到拒绝对端回执, 本机没有对应台账 (不伪造)', facts: { requestId: rej.requestId, state: 'rejected', reason: rej.reason } };
|
|
123
|
+
}
|
|
124
|
+
const mv = advance(cur.state, ['rejected']);
|
|
125
|
+
if (mv.rejected)
|
|
126
|
+
return { ok: false, error: 'TASK_TRANSITION_REJECTED', detail: `对端回执 rejected 与本机状态 ${cur.state} 不合: ${mv.rejected}` };
|
|
127
|
+
patchLocalTask(rej.requestId, { state: mv.state, reject: rej, notes: [...cur.notes, `被拒: ${String(rej.reason).slice(0, 200)}`] }, home);
|
|
128
|
+
const inb = readInboxItem(rej.requestId, home);
|
|
129
|
+
if (inb)
|
|
130
|
+
patchInboxItem(rej.requestId, { state: 'rejected', reject: rej }, home);
|
|
131
|
+
return { ok: true, note: `拒绝回执已入库 (state=${mv.state})`, facts: { requestId: rej.requestId, state: mv.state, reason: rej.reason } };
|
|
132
|
+
}
|
|
133
|
+
case 'task_result': {
|
|
134
|
+
if (!verified)
|
|
135
|
+
return { ok: false, error: 'SIGNATURE_INVALID', detail: '结果信封验签不过 (不落地未验签的结果)' };
|
|
136
|
+
const res = env;
|
|
137
|
+
// ① 存结果信封 (真实内容哈希/CID + 签名)
|
|
138
|
+
const w = saveResult(res, home);
|
|
139
|
+
if (!w.ok)
|
|
140
|
+
return { ok: false, error: 'RESULT_WRITE_FAILED', detail: w.error };
|
|
141
|
+
// ② 正文 (若随帧带来): 落**私有一层**, 并用签名里的 contentHash 复核
|
|
142
|
+
let bodyFacts = { bodyPresent: false };
|
|
143
|
+
if (typeof frame.body === 'string' && frame.body.length) {
|
|
144
|
+
const saved = saveBody(res.taskId, frame.body, home);
|
|
145
|
+
const matches = !!saved.contentHash && !!res.contentHash && saved.contentHash === res.contentHash;
|
|
146
|
+
bodyFacts = {
|
|
147
|
+
bodyPresent: true,
|
|
148
|
+
bodyBytes: saved.bytes,
|
|
149
|
+
bodyHashMatches: matches,
|
|
150
|
+
bodyNote: matches ? '正文哈希与签名里的 contentHash 一致' : '正文哈希与签名里的 contentHash **不一致** (如实记录)',
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
// ③ 状态推进: submitted/accepted → running → delivered (逐条过 checkTaskMove)
|
|
154
|
+
const cur = readLocalTask(res.requestId, home);
|
|
155
|
+
if (cur) {
|
|
156
|
+
const mv = advance(cur.state, ['accepted', 'running', 'delivered']);
|
|
157
|
+
if (mv.rejected)
|
|
158
|
+
return { ok: false, error: 'TASK_TRANSITION_REJECTED', detail: `结果回执与本机状态 ${cur.state} 不合: ${mv.rejected}` };
|
|
159
|
+
patchLocalTask(res.requestId, {
|
|
160
|
+
state: mv.state, result: res, resultVerified: verified,
|
|
161
|
+
providerPublicKeyHex: frame.signer.publicKeyHex,
|
|
162
|
+
notes: [...cur.notes, `收到结果 ${new Date(frame.sentAt).toISOString()} (contentHash=${(res.contentHash || '').slice(0, 12)}…)`],
|
|
163
|
+
}, home);
|
|
164
|
+
const inb = readInboxItem(res.requestId, home);
|
|
165
|
+
if (inb)
|
|
166
|
+
patchInboxItem(res.requestId, { state: 'delivered', result: res, resultVerified: verified, providerPublicKeyHex: frame.signer.publicKeyHex }, home);
|
|
167
|
+
return { ok: true, note: `结果已入库 (state=${mv.state}, 签名验过=${verified})`, facts: { requestId: res.requestId, taskId: res.taskId, state: mv.state, protocolVerified: verified, ...bodyFacts } };
|
|
168
|
+
}
|
|
169
|
+
const inb = readInboxItem(res.requestId, home);
|
|
170
|
+
if (inb) {
|
|
171
|
+
patchInboxItem(res.requestId, { state: 'delivered', result: res, resultVerified: verified, providerPublicKeyHex: frame.signer.publicKeyHex }, home);
|
|
172
|
+
return { ok: true, note: `结果已入库到收件箱条目 (state=delivered)`, facts: { requestId: res.requestId, taskId: res.taskId, protocolVerified: verified, ...bodyFacts } };
|
|
173
|
+
}
|
|
174
|
+
return { ok: true, note: '结果信封已存, 但本机没有对应的请求/台账 (不伪造状态)', facts: { taskId: res.taskId, protocolVerified: verified, ...bodyFacts } };
|
|
175
|
+
}
|
|
176
|
+
default:
|
|
177
|
+
return { ok: false, error: 'UNKNOWN_FRAME', detail: `未知帧类型: ${String(frame.frame)}` };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/** express app: POST /api/task/frame + GET /api/task/health */
|
|
181
|
+
export function createTaskFrameApp(opts = {}) {
|
|
182
|
+
const app = express();
|
|
183
|
+
app.use(express.json({ limit: '2mb' }));
|
|
184
|
+
app.get('/api/task/health', (_req, res) => {
|
|
185
|
+
res.json({
|
|
186
|
+
ok: true,
|
|
187
|
+
schema: 'bolloon-task-frame/1',
|
|
188
|
+
selfDid: opts.selfDid || null,
|
|
189
|
+
capabilities: opts.capabilities || [],
|
|
190
|
+
inboxPending: listInbox(opts.home).filter((i) => i.state === 'pending').length,
|
|
191
|
+
note: '任务帧端点 (只落待处理收件箱; 不执行、不付款)',
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
app.post('/api/task/frame', async (req, res) => {
|
|
195
|
+
const raw = req.body?.frame;
|
|
196
|
+
if (!raw)
|
|
197
|
+
return res.status(400).json({ ok: false, error: 'BAD_FRAME', detail: '缺 frame' });
|
|
198
|
+
const parsed = parseTaskFrame(typeof raw === 'string' ? raw : JSON.stringify(raw));
|
|
199
|
+
if (!parsed.ok)
|
|
200
|
+
return res.status(400).json({ ok: false, error: 'BAD_FRAME', detail: parsed.error });
|
|
201
|
+
try {
|
|
202
|
+
const reply = await handleTaskFrame(parsed.frame, opts);
|
|
203
|
+
// 帧级失败用 200 + ok:false 表达 (与 CLI 信封同一条纪律: 失败也结构化, 不靠 HTTP 状态猜)
|
|
204
|
+
return res.status(200).json({ ok: reply.ok, ...(reply.duplicate !== undefined ? { duplicate: reply.duplicate } : {}), ...(reply.note ? { note: reply.note } : {}), ...(reply.error ? { error: reply.error } : {}), ...(reply.detail ? { detail: reply.detail } : {}), ...(reply.facts ? { facts: reply.facts } : {}) });
|
|
205
|
+
}
|
|
206
|
+
catch (e) {
|
|
207
|
+
return res.status(500).json({ ok: false, error: 'HANDLER_THREW', detail: String(e?.message || e).slice(0, 300) });
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
return app;
|
|
211
|
+
}
|
|
@@ -23,9 +23,41 @@ import * as dagCbor from '@ipld/dag-cbor';
|
|
|
23
23
|
import { sha256 } from 'multiformats/hashes/sha2';
|
|
24
24
|
import { concat as uint8Concat } from 'uint8arrays/concat';
|
|
25
25
|
import { createOrbitDB } from '@orbitdb/core';
|
|
26
|
+
import * as orbitdbCoreNs from '@orbitdb/core';
|
|
27
|
+
/**
|
|
28
|
+
* 运行时取 `IPFSAccessController` (2026-09-24)。
|
|
29
|
+
*
|
|
30
|
+
* 它在运行时是**真具名导出** (`@orbitdb/core` 4.0.0 `src/index.js:26`, 经
|
|
31
|
+
* `./access-controllers/index.js` 二级再导出), 但该包是**纯 JS 包**: `package.json`
|
|
32
|
+
* 既无 `types` 也无 `exports`、包内没有任何 `.d.ts` —— TS7 从 JS 推断导出集时看不见
|
|
33
|
+
* 这个二级再导出, `import { IPFSAccessController }` 直接 TS2305。
|
|
34
|
+
* 所以从命名空间按需取一次并显式收窄到已知形状; 不给整个模块上 `any`
|
|
35
|
+
* (createOrbitDB / OrbitDB / KeyValue 仍照旧受类型检查)。
|
|
36
|
+
*/
|
|
37
|
+
const IPFSAccessController = orbitdbCoreNs.IPFSAccessController;
|
|
26
38
|
import { createBolloonIpfs } from './ipfs-node.js';
|
|
27
39
|
import * as path from 'path';
|
|
28
40
|
import * as os from 'os';
|
|
41
|
+
/**
|
|
42
|
+
* store 打不开 (地址不可解析 / 区块不在本地且没有 block broker 能去网上取)。
|
|
43
|
+
*
|
|
44
|
+
* 2026-09-24: 原来 `openStoreByAddress` 失败返回 null, 上游 (gateway-group) 把它当"空群",
|
|
45
|
+
* 于是"读不到"被显示成"没有消息"。改成**抛**: 打不开 ≠ 没内容。
|
|
46
|
+
*/
|
|
47
|
+
export class OrbitDBStoreUnreachableError extends Error {
|
|
48
|
+
address;
|
|
49
|
+
cause;
|
|
50
|
+
dataDir;
|
|
51
|
+
code = 'STORE_UNREACHABLE';
|
|
52
|
+
constructor(address, cause, dataDir) {
|
|
53
|
+
const why = String(cause?.message ?? cause ?? '未知原因').slice(0, 200);
|
|
54
|
+
super(`store 不可达 (${address}): ${why}`);
|
|
55
|
+
this.address = address;
|
|
56
|
+
this.cause = cause;
|
|
57
|
+
this.dataDir = dataDir;
|
|
58
|
+
this.name = 'OrbitDBStoreUnreachableError';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
29
61
|
/** 内容 → CID (dag-cbor, sha2-256, codec 0x71); 先 JSON 清洗 (dag-cbor 不支持 undefined) */
|
|
30
62
|
export async function contentToCid(obj) {
|
|
31
63
|
const cleaned = JSON.parse(JSON.stringify(obj)); // 丢弃 undefined 字段
|
|
@@ -34,6 +66,26 @@ export async function contentToCid(obj) {
|
|
|
34
66
|
return CID.createV1(0x71, hash).toString();
|
|
35
67
|
}
|
|
36
68
|
const home = () => process.env.HOME || os.homedir() || '/tmp';
|
|
69
|
+
/**
|
|
70
|
+
* OrbitDB 身份在 keystore 里的槽名 (2026-09-24)。
|
|
71
|
+
* 固定槽名 = 同一 dataDir 下每个进程读到**同一对密钥 / 同一个身份**。
|
|
72
|
+
* 不固定 (@orbitdb/core 默认 createId() 随机 32 位) 会让每个进程变成新写入者,
|
|
73
|
+
* 于是 `canAppend` 拿新身份去比对老 manifest 的 write 列表 → 一律拒绝。
|
|
74
|
+
*/
|
|
75
|
+
const ORBITDB_IDENTITY_ID = 'bolloon';
|
|
76
|
+
/**
|
|
77
|
+
* `opts.accessController.write` → OrbitDB v4 的大写 `AccessController` 工厂 (2026-09-24)。
|
|
78
|
+
*
|
|
79
|
+
* @orbitdb/core v4 的 `open(address, options)` **只认大写 `AccessController`** (一个构造函数),
|
|
80
|
+
* 小写 `accessController: { write: [...] }` 会被静默丢弃 → 新 store 落回默认策略
|
|
81
|
+
* (`IPFSAccessController`: `write = write || [创建者身份 id]`, 即创建者独占写)。
|
|
82
|
+
* 于是 gateway-group 一直传的 `write:['*']` (「群成员可广播」) **从来没生效过**。
|
|
83
|
+
* 这里把已成文的选项接上; 对已存在的地址, manifest 里的 AC 仍优先 (这里不改变既有群)。
|
|
84
|
+
*/
|
|
85
|
+
function accessControllerOption(opts) {
|
|
86
|
+
const write = opts?.accessController?.write;
|
|
87
|
+
return Array.isArray(write) && write.length ? { AccessController: IPFSAccessController({ write }) } : {};
|
|
88
|
+
}
|
|
37
89
|
/**
|
|
38
90
|
* OrbitDB 后端实现。单例: 同一进程只建一个 (helia/OrbitDB 都是重量级节点)。
|
|
39
91
|
*/
|
|
@@ -46,15 +98,29 @@ export class OrbitDBAdapter {
|
|
|
46
98
|
constructor(dataDir = path.join(home(), '.bolloon', 'orbitdb')) {
|
|
47
99
|
this.dataDir = dataDir;
|
|
48
100
|
}
|
|
101
|
+
/** 落盘位置 (未初始化时 null; 诊断/验收用) */
|
|
102
|
+
get ipfsPaths() {
|
|
103
|
+
return this.node?.paths ?? null;
|
|
104
|
+
}
|
|
49
105
|
/** 懒初始化: 首次使用时启动 helia + OrbitDB + 打开 keyvalue store */
|
|
50
106
|
async ensure() {
|
|
51
107
|
if (this.db)
|
|
52
108
|
return;
|
|
53
109
|
this.node = await createBolloonIpfs(path.join(this.dataDir, 'ipfs'));
|
|
54
|
-
|
|
110
|
+
// 2026-09-24: 身份必须**跨进程稳定**。不给 id 时 @orbitdb/core 用 createId()
|
|
111
|
+
// (32 位随机串) 当 keystore 槽名 (src/orbitdb.js:43 `id = id || await createId()`
|
|
112
|
+
// → :61 `createIdentity({ id })`) → 每个进程一对新密钥 → 新身份 → 写自己的 store
|
|
113
|
+
// 会被访问控制拒掉 ("Key … is not allowed to write to the log")。
|
|
114
|
+
// 给固定槽名后, 密钥从 <dataDir>/stores/keystore 读出复用 → 同 dataDir 同身份。
|
|
115
|
+
// 类型注: `id` 是 @orbitdb/core 的真选项 (src/orbitdb.js:33), 但 TS7 从 JS 推断出的
|
|
116
|
+
// 参数类型漏了它 → 显式取 Parameters<typeof createOrbitDB>[0] 再补 { id }
|
|
117
|
+
// (不裸 any 掉整个入参)。
|
|
118
|
+
const init = {
|
|
55
119
|
ipfs: this.node.helia,
|
|
56
120
|
directory: path.join(this.dataDir, 'stores'),
|
|
57
|
-
|
|
121
|
+
id: ORBITDB_IDENTITY_ID,
|
|
122
|
+
};
|
|
123
|
+
this._orbitdb = await createOrbitDB(init);
|
|
58
124
|
this.db = await this._orbitdb.open('bolloon-cid-store', { type: 'keyvalue' });
|
|
59
125
|
// 共享底层实例 (只读暴露)
|
|
60
126
|
this.orbitdb = this._orbitdb;
|
|
@@ -192,33 +258,32 @@ export class OrbitDBAdapter {
|
|
|
192
258
|
*/
|
|
193
259
|
async openStore(name, type = 'keyvalue', opts) {
|
|
194
260
|
await this.ensure();
|
|
195
|
-
const options = { type };
|
|
196
|
-
if (opts?.accessController)
|
|
197
|
-
options.accessController = opts.accessController;
|
|
261
|
+
const options = { type, ...accessControllerOption(opts) };
|
|
198
262
|
const raw = (await this._orbitdb.open(name, options));
|
|
199
263
|
return this.wrapStore(raw);
|
|
200
264
|
}
|
|
201
265
|
/**
|
|
202
266
|
* 2026-08-14: 按地址打开远端 store (Agent Gateway 共享网络 / 群组)。
|
|
203
|
-
*
|
|
204
|
-
*
|
|
267
|
+
*
|
|
268
|
+
* 2026-09-24: 失败改为**抛 OrbitDBStoreUnreachableError** (含原始错误原因),
|
|
269
|
+
* 不再 `console.warn` + return null —— 那让"整个群读不到"在上层显示成"群里没有消息"。
|
|
270
|
+
*
|
|
271
|
+
* 2026-09-24 (修正既有错述): **不再往 OrbitDB 传 `replica`** ——
|
|
272
|
+
* `@orbitdb/core` 4.0.0 的 `open()` 参数表里没有它 (src/orbitdb.js:118), 传了也被丢,
|
|
273
|
+
* 之前的"replica=true 只读副本"是**不存在的语义**(等于什么都没做)。入参保留以兼容
|
|
274
|
+
* 既有调用点, 但不再冒充 OrbitDB 选项。可写与否由 manifest 里的 ACL + 调用方是否 `put` 决定。
|
|
275
|
+
* `opts.accessController` 在**打开既有地址**时同样不生效 (见接口注释: AC 由 manifest 决定)。
|
|
205
276
|
*/
|
|
206
277
|
async openStoreByAddress(address, type = 'keyvalue', opts) {
|
|
207
278
|
await this.ensure();
|
|
208
279
|
try {
|
|
209
|
-
const options = { type };
|
|
210
|
-
if (opts?.replica === false)
|
|
211
|
-
options.replica = false;
|
|
212
|
-
else
|
|
213
|
-
options.replica = true;
|
|
214
|
-
if (opts?.accessController)
|
|
215
|
-
options.accessController = opts.accessController;
|
|
280
|
+
const options = { type, ...accessControllerOption(opts) };
|
|
216
281
|
const raw = (await this._orbitdb.open(address, options));
|
|
217
282
|
return this.wrapStore(raw);
|
|
218
283
|
}
|
|
219
284
|
catch (err) {
|
|
220
|
-
|
|
221
|
-
|
|
285
|
+
// 大声失败: 带上地址 + 原始原因 (最常见: "No block brokers capable of retrieving blocks ...")
|
|
286
|
+
throw new OrbitDBStoreUnreachableError(address, err, this.dataDir);
|
|
222
287
|
}
|
|
223
288
|
}
|
|
224
289
|
async close() {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ipfs-node.ts — Bolloon 的 OrbitDB 底层 IPFS 节点工厂 (2026-08-06)
|
|
2
|
+
* ipfs-node.ts — Bolloon 的 OrbitDB 底层 IPFS 节点工厂 (2026-08-06; 落盘 2026-09-24)
|
|
3
3
|
*
|
|
4
4
|
* 基于 helia 7:
|
|
5
5
|
* - createHelia() 内部已 withLibp2p 但**不传 opts** → 无法自定义 services
|
|
@@ -10,10 +10,26 @@
|
|
|
10
10
|
* 必须显式列出要保留的默认服务 (dht/identify/keychain/...)
|
|
11
11
|
* - withLibp2p().start() 之后 libp2p getter 才可用 (之前抛 NotStartedError)
|
|
12
12
|
*
|
|
13
|
+
* 2026-09-24 **区块/datastore 落盘修复** (跨进程可重开的根因):
|
|
14
|
+
* 原来 createHeliaLight 只传 codecs/hashers → helia 用默认 MemoryBlockstore / MemoryDatastore,
|
|
15
|
+
* 而 OrbitDB 的 manifest 与 oplog 条目都走 IPFSBlockStorage(ipfs.blockstore) 存
|
|
16
|
+
* (@orbitdb/core/src/storage/ipfs-block.js)。于是 `~/.bolloon/orbitdb/` 下只有 stores/
|
|
17
|
+
* (keystore + log/_heads 的 LevelDB), 没有 ipfs/ 块存储 —— store 只活在创建它的那个进程里。
|
|
18
|
+
* 新进程用地址重开必挂在 manifestStore.get():
|
|
19
|
+
* "No block brokers capable of retrieving blocks are configured, the CID bafyrei… cannot be fetched"
|
|
20
|
+
* 现在: FsBlockstore(dataDir/blocks) + FsDatastore(dataDir/datastore) 真落盘。
|
|
21
|
+
* · datastore 顺带持久化 pin (helia pins) 与 libp2p 私钥
|
|
22
|
+
* (@helia/libp2p: loadOrCreateSelfKey(helia.datastore) + opts.datastore ??= helia.datastore)
|
|
23
|
+
* → 同一 dataDir 的 peerId 跨进程稳定。
|
|
24
|
+
* · 这两个包是 Node 专用 (node:fs / node:path); 本文件只被 Node 侧 import,
|
|
25
|
+
* 浏览器入口 (client.ts / mobile-core.ts / a2ui-client.tsx) 到不了这里 —— 不伤 Web 包。
|
|
26
|
+
*
|
|
13
27
|
* 跑法: npx tsx scripts/smoke-orbitdb.ts
|
|
14
28
|
*/
|
|
15
29
|
import { createHeliaLight } from 'helia';
|
|
16
30
|
import { withLibp2p } from '@helia/libp2p';
|
|
31
|
+
import { FsBlockstore } from 'blockstore-fs';
|
|
32
|
+
import { FsDatastore } from 'datastore-fs';
|
|
17
33
|
import * as dagCbor from '@ipld/dag-cbor';
|
|
18
34
|
import * as dagJson from '@ipld/dag-json';
|
|
19
35
|
import * as json from 'multiformats/codecs/json';
|
|
@@ -37,13 +53,25 @@ import * as os from 'os';
|
|
|
37
53
|
/**
|
|
38
54
|
* 创建 Bolloon 用的 helia 节点 (libp2p 完整默认服务 + gossipsub pubsub)。
|
|
39
55
|
* dataDir 持久化节点身份/数据 (默认 ~/.bolloon/orbitdb-ipfs)。
|
|
56
|
+
*
|
|
57
|
+
* 2026-09-24: dataDir 下真落两个目录 —— `blocks/` (区块) 与 `datastore/` (pin + peerkey)。
|
|
58
|
+
* 目录不可写时**直接抛** (不退回内存: 静默退回正好是"新进程读不到群"的根因)。
|
|
40
59
|
*/
|
|
41
60
|
export async function createBolloonIpfs(dataDir) {
|
|
42
|
-
const dir = dataDir ?? path.join(process.env.HOME || os.homedir() || '/tmp', '.bolloon', 'orbitdb-ipfs');
|
|
61
|
+
const dir = path.resolve(dataDir ?? path.join(process.env.HOME || os.homedir() || '/tmp', '.bolloon', 'orbitdb-ipfs'));
|
|
62
|
+
const blocksDir = path.join(dir, 'blocks');
|
|
63
|
+
const datastoreDir = path.join(dir, 'datastore');
|
|
64
|
+
// 落盘存储: 不传这两个, helia 默认 MemoryBlockstore/MemoryDatastore → 进程退出即丢区块
|
|
65
|
+
const blockstore = new FsBlockstore(blocksDir);
|
|
66
|
+
const datastore = new FsDatastore(datastoreDir);
|
|
67
|
+
await blockstore.open(); // 建目录 + F_OK|W_OK 探测, 失败抛 OpenFailedError (不静默)
|
|
68
|
+
await datastore.open();
|
|
43
69
|
// createHeliaLight 无 libp2p → withLibp2p 手动装配 (可传 services)
|
|
44
70
|
// codecs/hashers 照抄 createHelia 默认: OrbitDB 的 log entry 用 dag-cbor (codec 113),
|
|
45
71
|
// 不注册会报 "Could not load codec for 113"
|
|
46
72
|
const helia = withLibp2p(createHeliaLight({
|
|
73
|
+
blockstore,
|
|
74
|
+
datastore,
|
|
47
75
|
codecs: [dagCbor, dagJson, json],
|
|
48
76
|
hashers: [sha512],
|
|
49
77
|
}), {
|
|
@@ -71,6 +99,7 @@ export async function createBolloonIpfs(dataDir) {
|
|
|
71
99
|
return {
|
|
72
100
|
helia,
|
|
73
101
|
peerId,
|
|
102
|
+
paths: { dataDir: dir, blocksDir, datastoreDir },
|
|
74
103
|
start: async () => { await helia.start(); },
|
|
75
104
|
stop: async () => { await helia.stop(); },
|
|
76
105
|
};
|