@bolloon/bolloon-agent 0.4.23 → 0.4.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,194 @@
1
+ /**
2
+ * routes-x402-info.ts — 微支付信息服务 (x402) 的 HTTP 接口
3
+ *
4
+ * 免费可读 (用于发现/索引/比价):
5
+ * GET /api/x402/info 本机已发布的信息列表 (只有元数据)
6
+ * GET /api/x402/info/:id/meta 单条元数据
7
+ * 付费才给内容 (x402 标准 402 流程):
8
+ * GET /api/x402/info/:id 无 X-PAYMENT → 402 (accepts …); 付款通过 → 200 带签名信封
9
+ * 管理:
10
+ * POST /api/x402/info 发布 (body: title/category/content/price/source)
11
+ * DELETE /api/x402/info/:id 下架
12
+ * 买方辅助 (电脑端及手机端代理调用):
13
+ * POST /api/x402/info/buy 服务端代付并验真 (url, max_payment, allow_local_dev)
14
+ * POST /api/x402/info/verify 验真一个信封 (信封 JSON 或 URL)
15
+ *
16
+ * 手机端: 通过 desktopBaseUrl 转发这四个接口即可完成"浏览 → 购买 → 验真"。
17
+ */
18
+ import * as path from 'path';
19
+ import * as os from 'os';
20
+ import { listInfo, getStoredInfo, publishInfo, removeInfo, buildPaymentRequired, checkAndSettlePayment, buyInfo, } from '../agents/x402/paid-info-store.js';
21
+ import { buildSignedEnvelope, verifyEnvelope, summarizeVerify } from '../agents/x402/paid-info-protocol.js';
22
+ import { makeDidResolver } from '../agents/x402/paid-info-tools.js';
23
+ /** 本机 DIAP 身份 (签发信封用) — 缺失时明确报错, 不静默造一个 */
24
+ async function loadProviderKeypair() {
25
+ try {
26
+ const { KeyManager } = await import('@diap/sdk');
27
+ const file = path.join(os.homedir(), '.bolloon', 'identity.json');
28
+ return await KeyManager.fromFile(file);
29
+ }
30
+ catch {
31
+ return null;
32
+ }
33
+ }
34
+ export function registerX402InfoRoutes(app) {
35
+ // ---- 列表 / 元数据 (免费) ----
36
+ app.get('/api/x402/info', async (req, res) => {
37
+ try {
38
+ const cat = String(req.query?.category || '').toLowerCase();
39
+ const items = (await listInfo()).filter((i) => !cat || i.category === cat);
40
+ res.json({ count: items.length, items });
41
+ }
42
+ catch (e) {
43
+ res.status(500).json({ error: e?.message });
44
+ }
45
+ });
46
+ app.get('/api/x402/info/:id/meta', async (req, res) => {
47
+ const stored = await getStoredInfo(String(req.params.id));
48
+ if (!stored)
49
+ return res.status(404).json({ error: '信息不存在' });
50
+ res.json({ item: stored.item });
51
+ });
52
+ // ---- 发布 / 下架 ----
53
+ app.post('/api/x402/info', async (req, res) => {
54
+ try {
55
+ const b = req.body || {};
56
+ const kp = await loadProviderKeypair();
57
+ if (!kp?.did) {
58
+ return res.status(400).json({ error: '缺少本机 DIAP 身份 (~/.bolloon/identity.json) — 先 publish_did 或跑 bolloon setup' });
59
+ }
60
+ const item = await publishInfo({
61
+ title: String(b.title || ''),
62
+ category: b.category,
63
+ content: String(b.content ?? ''),
64
+ description: b.description,
65
+ price: {
66
+ amount: String(b.price?.amount ?? '0'),
67
+ currency: b.price?.currency === 'ETH' ? 'ETH' : 'USDC',
68
+ network: String(b.price?.network || 'base-sepolia'),
69
+ payTo: String(b.price?.payTo || ''),
70
+ },
71
+ source: {
72
+ kind: b.source?.kind || 'self',
73
+ refs: Array.isArray(b.source?.refs) ? b.source.refs.map(String) : [],
74
+ note: b.source?.note,
75
+ },
76
+ provider: { did: kp.did, name: b.providerName || undefined, agentId: b.agentId || undefined },
77
+ contentCid: b.contentCid,
78
+ });
79
+ res.json({ ok: true, item });
80
+ }
81
+ catch (e) {
82
+ res.status(400).json({ error: e?.message });
83
+ }
84
+ });
85
+ app.delete('/api/x402/info/:id', async (req, res) => {
86
+ const ok = await removeInfo(String(req.params.id));
87
+ res.json({ ok });
88
+ });
89
+ // ---- 付费取内容 (x402 核心路径) ----
90
+ app.get('/api/x402/info/:id', async (req, res) => {
91
+ try {
92
+ const id = String(req.params.id);
93
+ const stored = await getStoredInfo(id);
94
+ if (!stored)
95
+ return res.status(404).json({ error: '信息不存在' });
96
+ const baseUrl = `${req.protocol || 'http'}://${req.headers?.host || `127.0.0.1:${process.env.PORT || 54188}`}`;
97
+ const resourceUrl = `${baseUrl}/api/x402/info/${id}`;
98
+ const requirements = buildPaymentRequired(stored.item, resourceUrl);
99
+ const paymentHeader = req.headers?.['x-payment'] || req.headers?.['X-PAYMENT'];
100
+ // 未付款 → 标准 402
101
+ if (!paymentHeader) {
102
+ return res.status(402)
103
+ .set('X-PAYMENT-REQUIRED', JSON.stringify(requirements.accepts))
104
+ .json({ ...requirements, error: '需要 x402 微支付' });
105
+ }
106
+ // 付款校验 + 结算
107
+ const pay = await checkAndSettlePayment({
108
+ paymentHeader: String(paymentHeader),
109
+ requirements,
110
+ });
111
+ if (!pay.ok) {
112
+ return res.status(402).json({ ...requirements, error: pay.error });
113
+ }
114
+ // 签发信封 (内容 + DIAP Ed25519 签名 + 支付凭据绑定)
115
+ const kp = await loadProviderKeypair();
116
+ if (!kp) {
117
+ return res.status(500).json({ error: '缺少本机 DIAP 身份, 无法签发信封 (已收到的付款请人工处理)' });
118
+ }
119
+ const envelope = await buildSignedEnvelope({
120
+ item: stored.item,
121
+ content: stored.content,
122
+ keypair: kp,
123
+ payment: {
124
+ mode: pay.mode === 'facilitator' ? 'facilitator' : 'local-dev',
125
+ receipt: String(pay.receipt || ''),
126
+ txHash: pay.txHash,
127
+ network: pay.network || stored.item.price.network,
128
+ amount: stored.item.price.amount,
129
+ currency: stored.item.price.currency,
130
+ payer: pay.payer,
131
+ settledAt: new Date().toISOString(),
132
+ },
133
+ });
134
+ res.set('X-PAYMENT-RESPONSE', pay.receipt || '');
135
+ res.json(envelope);
136
+ }
137
+ catch (e) {
138
+ res.status(500).json({ error: e?.message });
139
+ }
140
+ });
141
+ // ---- 买方辅助: 代付 + 验真 ----
142
+ app.post('/api/x402/info/buy', async (req, res) => {
143
+ try {
144
+ const b = req.body || {};
145
+ const url = String(b.url || '');
146
+ if (!url)
147
+ return res.status(400).json({ error: 'url 必填' });
148
+ const privateKey = String(b.walletPrivateKey || process.env.X402_PRIVATE_KEY || '');
149
+ const r = await buyInfo({
150
+ url,
151
+ privateKey: privateKey || undefined,
152
+ maxPaymentAmount: b.maxPayment ? String(b.maxPayment) : undefined,
153
+ network: b.network ? String(b.network) : undefined,
154
+ allowLocalDev: b.allowLocalDev === true || process.env.BOLLOON_X402_LOCAL_VERIFY === '1',
155
+ resolveDid: makeDidResolver(),
156
+ expectItemId: b.itemId ? String(b.itemId) : undefined,
157
+ });
158
+ if (!r.ok)
159
+ return res.status(400).json({ error: r.error, status: r.status });
160
+ res.json({
161
+ ok: true,
162
+ item: r.envelope?.item,
163
+ content: r.envelope?.content,
164
+ envelope: r.envelope,
165
+ verify: r.verify,
166
+ verifySummary: r.verify ? summarizeVerify(r.verify) : null,
167
+ payment: r.payment,
168
+ });
169
+ }
170
+ catch (e) {
171
+ res.status(500).json({ error: e?.message });
172
+ }
173
+ });
174
+ app.post('/api/x402/info/verify', async (req, res) => {
175
+ try {
176
+ const b = req.body || {};
177
+ let env = b.envelope;
178
+ if (!env && b.url) {
179
+ const r = await fetch(String(b.url));
180
+ env = await r.json();
181
+ }
182
+ if (!env)
183
+ return res.status(400).json({ error: '需要 envelope 或 url' });
184
+ const report = await verifyEnvelope(env, {
185
+ resolveDid: makeDidResolver(),
186
+ expectItemId: b.itemId ? String(b.itemId) : undefined,
187
+ });
188
+ res.json({ ok: report.ok, report, summary: summarizeVerify(report) });
189
+ }
190
+ catch (e) {
191
+ res.status(500).json({ error: e?.message });
192
+ }
193
+ });
194
+ }
@@ -13,6 +13,8 @@ import { registerJudgmentsRoutes } from './routes-judgments.js';
13
13
  import { registerLlmConfigRoutes } from './routes-llm-config.js';
14
14
  import { registerExternalEngineRoutes } from './routes-external-engines.js';
15
15
  import { registerTaskRoutes } from './routes-tasks.js';
16
+ // 2026-09-13: 微支付信息服务 (x402) 路由
17
+ import { registerX402InfoRoutes } from './routes-x402-info.js';
16
18
  import { loadPeerTier, recordInteraction, tierLabel } from '../social/dunbar-tier.js';
17
19
  import { registerHearthRoutes } from './routes-hearth.js';
18
20
  import { loadOrCreateAgentIdentity } from '../agents/agent-identity.js';
@@ -2406,6 +2408,41 @@ export async function createWebServer(port = 3000, options = {}) {
2406
2408
  catch (hbErr) {
2407
2409
  console.warn('[heartbeat] 启动失败 (non-fatal):', hbErr?.message);
2408
2410
  }
2411
+ // 2026-09-13: 定时任务调度器 (clock) — 带 tick 锁 + 执行记录 + 看门狗 + 勿扰闸门.
2412
+ // 勿扰: 主任务 (人类这条消息跑 agent) 期间 tick 会被 resolveDnd 挡下并记 deferred,
2413
+ // 主任务结束后下一个 tick 补跑, 不抢前台、不刷屏 (事件只走 monitor.log / SSE)。
2414
+ try {
2415
+ const { startCronScheduler, setCronLogSink, enterMainTask, exitMainTask } = await import('../cron/index.js');
2416
+ setCronLogSink((evt) => {
2417
+ try {
2418
+ broadcast({ type: 'cron', ...evt });
2419
+ }
2420
+ catch { /* 广播失败不致命 */ }
2421
+ });
2422
+ const cronHandle = await startCronScheduler({
2423
+ intervalMs: Number(process.env.BOLLOON_CRON_HEARTBEAT_MS) || 60_000,
2424
+ exec: async (job) => {
2425
+ const channels = await loadChannels();
2426
+ const target = channels.find((c) => c.currentSessionId) || channels[0];
2427
+ if (!target)
2428
+ throw new Error('无可用 channel, 跳过定时任务');
2429
+ const agent = await getAgentForChannel(target.id);
2430
+ if (!agent || typeof agent.promptStream !== 'function') {
2431
+ throw new Error('无可用 agent, 跳过定时任务');
2432
+ }
2433
+ // 前缀 [cron] 标记后台执行来源 (供审计/日志区分人类消息与定时任务)
2434
+ await agent.promptStream(`[cron] ${job.name}: ${job.prompt}`, () => { }, undefined, target.id);
2435
+ },
2436
+ });
2437
+ global.cronScheduler = cronHandle;
2438
+ if (!cronHandle.disabled)
2439
+ console.log(`[cron] 调度器已启动 (每 ${Math.round((Number(process.env.BOLLOON_CRON_HEARTBEAT_MS) || 60_000) / 1000)}s 一轮, tick 带锁 + DND)`);
2440
+ // 暴露给 /message 使用 (主任务闸门)
2441
+ global.__bolloonMainTask = { enterMainTask, exitMainTask };
2442
+ }
2443
+ catch (cronErr) {
2444
+ console.warn('[cron] 调度器启动失败 (non-fatal):', cronErr?.message);
2445
+ }
2409
2446
  // 社交决策: 让本地 agent (用第一个本地 channel 的身份) 判断是否主动联络某 peer
2410
2447
  // 目标感知: ctx.goal 是当前要达成的目标, 决策应服务于它, 达成后可声明 goalAchieved 进入 RESTING
2411
2448
  async function llmSocialDecide(ctx) {
@@ -2821,6 +2858,180 @@ ${goalDesc}
2821
2858
  res.status(500).json({ error: e?.message });
2822
2859
  }
2823
2860
  });
2861
+ // 2026-09-08: 手机端数据同步 — 一次拉取电脑端全部数据 (登录后手机端同步用; CORS 已在中间件开启)
2862
+ app.get('/api/mobile/snapshot', async (_req, res) => {
2863
+ try {
2864
+ const HOME = process.env.HOME || '/tmp';
2865
+ const out = { ok: true, ts: Date.now(), source: 'desktop' };
2866
+ // 活跃身份 / 会话 (channels.json 是唯一持久化点)
2867
+ try {
2868
+ out.active = JSON.parse(await fs.readFile(`${HOME}/.bolloon/active-channel.json`, 'utf-8'));
2869
+ }
2870
+ catch {
2871
+ out.active = null;
2872
+ }
2873
+ try {
2874
+ const chs = JSON.parse(await fs.readFile(`${HOME}/.bolloon/sessions/channels.json`, 'utf-8'));
2875
+ out.channels = Array.isArray(chs) ? chs : [];
2876
+ }
2877
+ catch {
2878
+ out.channels = [];
2879
+ }
2880
+ // 判断力库
2881
+ try {
2882
+ const j = await import('../pi-ecosystem-judgment/index.js');
2883
+ out.judgments = await j.getAllJudgments();
2884
+ }
2885
+ catch (e) {
2886
+ out.judgments = [];
2887
+ out.judgmentsError = e?.message;
2888
+ }
2889
+ // 本机 skills (~/.bolloon/skills/) — 手机端「智能体控制 (MCP / Skills)」页读取
2890
+ try {
2891
+ const { loadSkillsFromPaths, defaultSkillPaths } = await import('../agents/skill-loader.js');
2892
+ const list = await loadSkillsFromPaths(defaultSkillPaths());
2893
+ out.skills = list.map((k) => ({ name: String(k.name || ''), description: String(k.description || '').slice(0, 200) }));
2894
+ }
2895
+ catch (e) {
2896
+ out.skills = [];
2897
+ out.skillsError = e?.message;
2898
+ }
2899
+ // Agent 服务 Registry (发现层)
2900
+ try {
2901
+ const { getAgentRegistry } = await import('../agents/agent-registry.js');
2902
+ out.services = await getAgentRegistry().list();
2903
+ }
2904
+ catch {
2905
+ out.services = [];
2906
+ }
2907
+ // 数字资源
2908
+ try {
2909
+ const rs = await import('../agents/resource-store.js');
2910
+ out.resources = await rs.listResources({});
2911
+ }
2912
+ catch {
2913
+ out.resources = [];
2914
+ }
2915
+ // 已加入网络
2916
+ try {
2917
+ const { listJoinedNetworks } = await import('../agents/gateway-network.js');
2918
+ out.networks = await listJoinedNetworks();
2919
+ }
2920
+ catch {
2921
+ out.networks = [];
2922
+ }
2923
+ out.counts = {
2924
+ channels: (out.channels || []).length,
2925
+ judgments: (out.judgments || []).length,
2926
+ services: (out.services || []).length,
2927
+ resources: (out.resources || []).length,
2928
+ networks: (out.networks || []).length,
2929
+ };
2930
+ res.json(out);
2931
+ }
2932
+ catch (e) {
2933
+ res.status(500).json({ ok: false, error: e?.message });
2934
+ }
2935
+ });
2936
+ // 2026-09-08: 手机端 P2P 拨入信息 — 手机(WebView)不能 listen, 只能主动拨桌面的 /ws 地址
2937
+ app.get('/api/p2p/mobile-connect', async (_req, res) => {
2938
+ try {
2939
+ const { p2pNetwork } = await import('../network/p2p.js');
2940
+ const wsAddrs = p2pNetwork.getWsMultiaddrs();
2941
+ // 2026-09-11: 中继语义 — 这些 /ws 地址同时是 circuit relay v2 中继地址。
2942
+ // 手机 dial 它之后即可预约, 拿到 <relay>/p2p-circuit/p2p/<手机PeerId> 这个「可拨入地址」。
2943
+ const relay = p2pNetwork.getRelayServiceInfo();
2944
+ const relayAddrs = relay.enabled ? p2pNetwork.getRelayAddrs() : [];
2945
+ res.json({
2946
+ ok: true,
2947
+ peerId: p2pNetwork.getNodePeerId(),
2948
+ wsAddrs,
2949
+ // 单独字段: 明确「这是中继, 不是普通对端」(手机端看到 relayAddrs 就该去预约, 而不是只当 bootstrap)
2950
+ isRelay: relay.enabled,
2951
+ relayAddrs,
2952
+ relayProtocol: relay.protocol,
2953
+ relayReservations: relay.reservations,
2954
+ relayMaxReservations: relay.maxReservations,
2955
+ hint: wsAddrs.length
2956
+ ? (relay.enabled
2957
+ ? '拨 relayAddrs 里的任意地址 → 手机会自动预约中继并拿到 /p2p-circuit 可拨入地址'
2958
+ : '桌面 P2P 在跑, 但中继服务未启用 (中继地址暂不可用; 手机只连不预约)')
2959
+ : '桌面 P2P 未启动或未监听 ws',
2960
+ });
2961
+ }
2962
+ catch (e) {
2963
+ res.status(500).json({ ok: false, error: e?.message });
2964
+ }
2965
+ });
2966
+ // 2026-09-08: 手机端 OrbitDB 库级复制 — 列可复制 store / 读全量条目 / 合并写回
2967
+ app.get('/api/orbitdb/stores', async (_req, res) => {
2968
+ try {
2969
+ const out = [{ name: 'bolloon-cid-store', type: 'keyvalue' }];
2970
+ try {
2971
+ const { getAgentRegistry } = await import('../agents/agent-registry.js');
2972
+ const reg = getAgentRegistry();
2973
+ if (reg && typeof reg.warm === 'function')
2974
+ await reg.warm();
2975
+ if (reg?.storeName)
2976
+ out.push({ name: String(reg.storeName), type: 'keyvalue', address: reg.storeAddress || undefined });
2977
+ }
2978
+ catch { /* registry 未就绪 → 只给 cid store */ }
2979
+ res.json({ ok: true, stores: out });
2980
+ }
2981
+ catch (e) {
2982
+ res.status(500).json({ ok: false, error: e?.message });
2983
+ }
2984
+ });
2985
+ app.get('/api/orbitdb/entries', async (req, res) => {
2986
+ try {
2987
+ const name = String(req.query?.name || '').trim();
2988
+ if (!name)
2989
+ return res.status(400).json({ ok: false, error: 'name 必填' });
2990
+ const { getCIDDatabase } = await import('../orbitdb/cid-database.js');
2991
+ const store = await getCIDDatabase().openStore(name, 'keyvalue');
2992
+ const all = await store.all();
2993
+ res.json({ ok: true, name, address: store.address, entries: all, count: all.length });
2994
+ }
2995
+ catch (e) {
2996
+ res.status(500).json({ ok: false, error: e?.message });
2997
+ }
2998
+ });
2999
+ app.post('/api/orbitdb/merge', async (req, res) => {
3000
+ try {
3001
+ const name = String(req.body?.name || '').trim();
3002
+ const entries = Array.isArray(req.body?.entries) ? req.body.entries : [];
3003
+ if (!name)
3004
+ return res.status(400).json({ ok: false, error: 'name 必填' });
3005
+ const { getCIDDatabase } = await import('../orbitdb/cid-database.js');
3006
+ const store = await getCIDDatabase().openStore(name, 'keyvalue');
3007
+ let written = 0;
3008
+ for (const e of entries) {
3009
+ if (!e || typeof e.key !== 'string')
3010
+ continue;
3011
+ await store.put(e.key, e.value);
3012
+ written++;
3013
+ }
3014
+ res.json({ ok: true, name, address: store.address, written });
3015
+ }
3016
+ catch (e) {
3017
+ res.status(500).json({ ok: false, error: e?.message });
3018
+ }
3019
+ });
3020
+ // 2026-09-15: agent-delegate (manifest 协议 + agent_delegate) **启动即挂载**
3021
+ // 文档 bolloon-gateway-join.md 第 3 节要求 POST /api/agent/register 可用;
3022
+ // 之前只在 iroh 懒初始化 (/api/iroh/info 首次访问) 的收尾挂上 → 没触发时恒 404。
3023
+ let agentDelegateMounted = false;
3024
+ try {
3025
+ const delegateTransport = createIrohDelegateTransport({ verbose: true });
3026
+ // 注意: createAgentDelegateApp 内部声明的是绝对路径 (/api/agent/...),
3027
+ // 所以挂载时**不能**再带 '/api/agent' 前缀 (否则变成 /api/agent/api/agent/... 恒 404)
3028
+ app.use(createAgentDelegateApp(delegateTransport));
3029
+ agentDelegateMounted = true;
3030
+ console.log('[agent-delegate] 已挂载到 /api/agent (启动即用: local-manifest / register / pick / delegate)');
3031
+ }
3032
+ catch (e) {
3033
+ console.warn('[agent-delegate] 挂载失败 (非致命):', e?.message);
3034
+ }
2824
3035
  // 2026-08-14: Agent Gateway — 网络加入 / 分享链接 / 成员 / 状态 (入口要小: 一条链接)
2825
3036
  app.post('/api/gateway/join', async (req, res) => {
2826
3037
  try {
@@ -2857,6 +3068,36 @@ ${goalDesc}
2857
3068
  res.status(500).json({ error: e?.message });
2858
3069
  }
2859
3070
  });
3071
+ // 2026-09-15: 文档驱动「加入全球智能体网络」— 人类/手机只给一句 read <doc>, 这里做落地执行.
3072
+ // GET: 查入网态 (joined/url/did/networkLink); POST: 真执行 (可指定 url/name/capabilities/force).
3073
+ app.get('/api/gateway/join-global', async (_req, res) => {
3074
+ try {
3075
+ const { getGatewayJoinState, DEFAULT_GATEWAY_JOIN_DOC } = await import('../agents/gateway-join.js');
3076
+ const state = await getGatewayJoinState();
3077
+ res.json({ ok: true, joined: !!state, docUrl: DEFAULT_GATEWAY_JOIN_DOC, state });
3078
+ }
3079
+ catch (e) {
3080
+ res.status(500).json({ ok: false, error: e?.message });
3081
+ }
3082
+ });
3083
+ app.post('/api/gateway/join-global', async (req, res) => {
3084
+ try {
3085
+ const { joinGlobalGateway } = await import('../agents/gateway-join.js');
3086
+ const identity = await loadOrCreateUserIdentity();
3087
+ const caps = String(req.body?.capabilities || '').split(',').map((s) => s.trim()).filter(Boolean);
3088
+ const r = await joinGlobalGateway({
3089
+ url: String(req.body?.url || '').trim() || undefined,
3090
+ did: identity?.did || undefined,
3091
+ name: String(req.body?.name || '').trim() || identity?.name || undefined,
3092
+ capabilities: caps.length ? caps : undefined,
3093
+ force: req.body?.force === true,
3094
+ });
3095
+ res.status(r.ok ? 200 : 400).json(r);
3096
+ }
3097
+ catch (e) {
3098
+ res.status(500).json({ ok: false, error: e?.message });
3099
+ }
3100
+ });
2860
3101
  app.get('/api/gateway/status', async (_req, res) => {
2861
3102
  try {
2862
3103
  const { gatewayStatus } = await import('../agents/agent-gateway.js');
@@ -2956,6 +3197,41 @@ ${goalDesc}
2956
3197
  res.status(500).json({ error: e?.message });
2957
3198
  }
2958
3199
  });
3200
+ // 2026-09-13: 人机问答通道 (clarify) — 智能体提问, 人类在 Web/手机点选项或输入回答
3201
+ // 智能体侧 clarify 工具 ask() → 这里广播 agent-question → 前端渲染选项按钮
3202
+ void import('../agents/user-questions.js').then(({ userQuestions }) => {
3203
+ userQuestions.onQuestion((q) => {
3204
+ try {
3205
+ broadcast({ type: 'agent-question', question: q });
3206
+ }
3207
+ catch { /* 广播失败不影响提问 (CLI 侧仍能看到) */ }
3208
+ });
3209
+ }).catch(() => { });
3210
+ app.get('/api/questions', async (_req, res) => {
3211
+ try {
3212
+ const { userQuestions } = await import('../agents/user-questions.js');
3213
+ res.json({ pending: await userQuestions.pending() });
3214
+ }
3215
+ catch (e) {
3216
+ res.status(500).json({ error: e?.message });
3217
+ }
3218
+ });
3219
+ // POST /api/questions/answer { id?, text } — 回答 (id 省略则回答最早的一个待处理问题)
3220
+ app.post('/api/questions/answer', async (req, res) => {
3221
+ try {
3222
+ const { id, text } = req.body || {};
3223
+ const { userQuestions } = await import('../agents/user-questions.js');
3224
+ const r = await userQuestions.answer(id ? String(id) : null, String(text ?? ''));
3225
+ if (!r.ok)
3226
+ return res.status(400).json({ error: r.error });
3227
+ // 人类回答后清掉前端提示 + 让 agent 侧的 await 立即返回
3228
+ broadcast({ type: 'agent-question-answered', id: r.question?.id, answer: r.question?.answer });
3229
+ res.json({ ok: true, question: r.question });
3230
+ }
3231
+ catch (e) {
3232
+ res.status(500).json({ error: e?.message });
3233
+ }
3234
+ });
2959
3235
  // 2026-08-13: 人工支付审批 API — YAML 验证门 confirm 的支付请求
2960
3236
  app.get('/api/payments/pending', async (_req, res) => {
2961
3237
  try {
@@ -3571,6 +3847,19 @@ ${goalDesc}
3571
3847
  if (!text) {
3572
3848
  return res.status(400).json({ error: 'No text provided' });
3573
3849
  }
3850
+ // 2026-09-13: 主任务闸门 — 人类这条消息跑 agent 期间, 后台定时任务进入勿扰 (DND).
3851
+ // 用 res 'close' 释放 (正常结束/中断都覆盖), 不需要改动本 handler 内部的分支.
3852
+ const mainTask = global.__bolloonMainTask;
3853
+ if (mainTask?.enterMainTask) {
3854
+ try {
3855
+ mainTask.enterMainTask('web-message');
3856
+ }
3857
+ catch { /* 闸门失败不阻塞对话 */ }
3858
+ res.on('close', () => { try {
3859
+ mainTask.exitMainTask();
3860
+ }
3861
+ catch { /* 忽略 */ } });
3862
+ }
3574
3863
  // 2026-08-02: / 斜杠命令路由 — 用户输入 /plan /todo /task 等快捷命令时,
3575
3864
  // 直接把命令转成结构化指令注入 contextHint, 引导 LLM 调用对应工具.
3576
3865
  // 前端 / 菜单插入的是 "/plan 目标; 步骤" 这类文本, 这里解析成提示.
@@ -5746,6 +6035,8 @@ ${goalDesc}
5746
6035
  });
5747
6036
  // 2026-07-06: Task Queue API 抽到 ./routes-tasks.ts
5748
6037
  registerTaskRoutes(app, { broadcast, getAgentForChannel });
6038
+ // 2026-09-13: 微支付信息服务 (x402) — 发布 / 402 收款 / 买方代付 / 验真
6039
+ registerX402InfoRoutes(app);
5749
6040
  // 2026-07-06: LLM/Video/Audio 配置路由抽到 ./routes-llm-config.ts
5750
6041
  registerLlmConfigRoutes(app);
5751
6042
  // 2026-07-22: 外部编码智能体 (codex/claude-code/opencode/openclaw/hermes/实验 API)
@@ -6293,15 +6584,18 @@ ${goalDesc}
6293
6584
  };
6294
6585
  irohInitialized = true;
6295
6586
  // 挂载 agent-delegate app (manifest 协议 + agent_delegate)
6296
- // 必须在 irohInitialized 之后挂, 因为适配器要监听 irohTransport.onMessage
6297
- try {
6298
- const delegateTransport = createIrohDelegateTransport({ verbose: true });
6299
- const delegateApp = createAgentDelegateApp(delegateTransport);
6300
- app.use('/api/agent', delegateApp);
6301
- console.log('[iroh API] agent-delegate app 已挂载到 /api/agent');
6302
- }
6303
- catch (e) {
6304
- console.error('[iroh API] 挂载 agent-delegate app 失败:', e);
6587
+ // 2026-09-15: 已改为启动时提前挂载 (见 createWebServer 的 /api/agent 段), 这里不再重复挂.
6588
+ if (!agentDelegateMounted) {
6589
+ try {
6590
+ const delegateTransport = createIrohDelegateTransport({ verbose: true });
6591
+ const delegateApp = createAgentDelegateApp(delegateTransport);
6592
+ app.use(delegateApp);
6593
+ agentDelegateMounted = true;
6594
+ console.log('[iroh API] agent-delegate app 已挂载到 /api/agent (补挂)');
6595
+ }
6596
+ catch (e) {
6597
+ console.error('[iroh API] 挂载 agent-delegate app 失败:', e);
6598
+ }
6305
6599
  }
6306
6600
  // 设置消息处理
6307
6601
  irohTransport.onMessage('chat', (msg) => {
package/dist/ios/sw.js CHANGED
@@ -1,5 +1,12 @@
1
- /* Bolloon 手机端 Service Worker — app-shell 缓存, 让 iPhone 可"添加到主屏幕"独立运行 */
2
- const CACHE = 'bolloon-mobile-v1';
1
+ /* Bolloon 手机端 Service Worker — app-shell 缓存, 让 iPhone 可"添加到主屏幕"独立运行
2
+ *
3
+ * 2026-09-14 修: 原来是 cache-first + 固定缓存名 (bolloon-mobile-v1) →
4
+ * 一旦装上, 之后每次升级 APK / 重新部署站点, WebView 里跑的仍是**旧的 mobile.js/mobile-core.js**,
5
+ * 表现为「改了 UI 手机上没变化」「升级后还是老界面」。
6
+ * 现在: 代码类资源 (html/js/css/json) 一律 **network-first** (拿不到网才回退缓存),
7
+ * 缓存名带版本号, activate 时清掉所有旧缓存; 图标等静态资源仍 cache-first。
8
+ */
9
+ const CACHE = 'bolloon-mobile-v1.1';
3
10
  const SHELL = [
4
11
  './mobile.html',
5
12
  './mobile.css',
@@ -25,6 +32,9 @@ self.addEventListener('activate', (e) => {
25
32
  );
26
33
  });
27
34
 
35
+ // 代码类资源: 必须联网拿最新的 (离线才回退缓存); 图标/manifest 之类可以 cache-first
36
+ const CODE_EXT = /\.(?:html|js|mjs|css|json|webmanifest)$/i;
37
+
28
38
  self.addEventListener('fetch', (e) => {
29
39
  const req = e.request;
30
40
  if (req.method !== 'GET') return;
@@ -32,6 +42,20 @@ self.addEventListener('fetch', (e) => {
32
42
  try { url = new URL(req.url); } catch { return; }
33
43
  // 跨域 / API / 实时通道不缓存 (registry fetch、WebSocket 等)
34
44
  if (url.origin !== location.origin || url.pathname.includes('/api/')) return;
45
+
46
+ if (CODE_EXT.test(url.pathname) || req.mode === 'navigate') {
47
+ e.respondWith(
48
+ fetch(req).then((res) => {
49
+ if (res && res.ok) {
50
+ const copy = res.clone();
51
+ caches.open(CACHE).then((c) => c.put(req, copy)).catch(() => {});
52
+ }
53
+ return res;
54
+ }).catch(() => caches.match(req).then((hit) => hit || caches.match('./mobile.html'))),
55
+ );
56
+ return;
57
+ }
58
+
35
59
  e.respondWith(
36
60
  caches.match(req).then((hit) => hit || fetch(req).then((res) => {
37
61
  const copy = res.clone();