@bolloon/bolloon-agent 0.3.12 → 0.3.14

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.
@@ -17,35 +17,67 @@ import { saveWindow as saveSessionWindow, loadWindow as loadSessionWindow } from
17
17
  let lastChannelsJson = '';
18
18
  // 写盘保护: 任何调用 saveChannels 后更新
19
19
  let lastChannelsWriteAt = 0;
20
+ // 2026-07-24: 简单的互斥锁, 防止并发 loadChannels→modify→saveChannels 的经典 read-modify-write 竞争
21
+ let channelsLock = Promise.resolve();
20
22
  export function getLastChannelsWriteAt() {
21
23
  return lastChannelsWriteAt;
22
24
  }
23
- export async function loadChannels() {
25
+ /** channels 执行原子化的 read-modify-write, 自带互斥锁 */
26
+ export async function updateChannels(fn) {
27
+ channelsLock = channelsLock.then(async () => {
28
+ const chs = await rawLoadChannels();
29
+ const result = fn(chs);
30
+ await rawSaveChannels(result);
31
+ return result;
32
+ });
33
+ return channelsLock;
34
+ }
35
+ async function rawLoadChannels() {
24
36
  try {
25
37
  const data = await fs.readFile(CHANNELS_PATH, 'utf-8');
26
38
  return JSON.parse(data);
27
39
  }
28
- catch {
40
+ catch (readErr) {
41
+ // 2026-07-24: 主文件损坏时尝试从 .tmp 恢复
42
+ if (readErr?.code !== 'ENOENT') {
43
+ console.warn('[loadChannels] channels.json 解析失败, 尝试从 .tmp 恢复:', readErr?.message?.slice(0, 80));
44
+ try {
45
+ const tmpData = await fs.readFile(CHANNELS_PATH + '.tmp', 'utf-8');
46
+ const recovered = JSON.parse(tmpData);
47
+ console.log(`[loadChannels] 从 .tmp 恢复成功: ${recovered.length} 个 channel`);
48
+ // 立即把恢复的内容写回主文件
49
+ await fs.writeFile(CHANNELS_PATH, tmpData, 'utf-8');
50
+ return recovered;
51
+ }
52
+ catch (tmpErr) {
53
+ console.warn('[loadChannels] .tmp 恢复也失败:', tmpErr?.message?.slice(0, 80));
54
+ }
55
+ }
29
56
  return [];
30
57
  }
31
58
  }
32
- export async function saveChannels(channels) {
33
- // 写盘前剥掉任何遗留的 didDocument 字段, 防止历史脏数据撑大文件
59
+ async function rawSaveChannels(channels) {
34
60
  const sanitized = channels.map(ch => {
35
61
  const { didDocument: _omit, ...rest } = ch;
36
62
  return rest;
37
63
  });
38
64
  const jsonStr = JSON.stringify(sanitized, null, 2);
39
- // 写盘保护: 内容和上次完全一致就跳过, 避免 SSE ping / 重新 init 触发的无意义写盘
40
- if (jsonStr === lastChannelsJson) {
65
+ if (jsonStr === lastChannelsJson)
41
66
  return;
42
- }
43
67
  lastChannelsJson = jsonStr;
44
68
  console.log('[saveChannels] 保存频道数据, 数量:', sanitized.length);
45
- console.log('[saveChannels] JSON 长度:', jsonStr.length);
46
- await fs.writeFile(CHANNELS_PATH, jsonStr);
69
+ // 2026-07-24: 原子写入 — 先写 .tmp rename, 防止崩溃导致 channels.json 损坏
70
+ const tmpPath = CHANNELS_PATH + '.tmp';
71
+ await fs.writeFile(tmpPath, jsonStr, 'utf-8');
72
+ await fs.rename(tmpPath, CHANNELS_PATH);
47
73
  lastChannelsWriteAt = Date.now();
48
74
  }
75
+ export async function loadChannels() {
76
+ return rawLoadChannels();
77
+ }
78
+ export async function saveChannels(channels) {
79
+ return rawSaveChannels(channels);
80
+ }
49
81
  /**
50
82
  * loadSession 加 L0 window fallback 链 (2026-07-07 P0-B):
51
83
  * 1) full session.json (主路径)
@@ -3728,6 +3728,77 @@ ${goalDesc}
3728
3728
  res.status(500).json({ error: err.message });
3729
3729
  }
3730
3730
  });
3731
+ /**
3732
+ * 存储加密私钥: 客户端用 DID 派生 AES-GCM 密钥加密私钥后上传.
3733
+ * body: { encryptedPrivateKey (base64), encryptedPrivateKeyIv (base64), autoPayEnabled? }
3734
+ */
3735
+ app.post('/channels/:channelId/encrypted-key', async (req, res) => {
3736
+ try {
3737
+ const { channelId } = req.params;
3738
+ const { encryptedPrivateKey, encryptedPrivateKeyIv, autoPayEnabled } = req.body || {};
3739
+ if (!encryptedPrivateKey || !encryptedPrivateKeyIv) {
3740
+ return res.status(400).json({ error: '缺少必填字段: encryptedPrivateKey, encryptedPrivateKeyIv' });
3741
+ }
3742
+ const channels = await loadChannels();
3743
+ const channel = channels.find(c => c.id === channelId);
3744
+ if (!channel)
3745
+ return res.status(404).json({ error: 'Channel not found' });
3746
+ channel.encryptedPrivateKey = encryptedPrivateKey;
3747
+ channel.encryptedPrivateKeyIv = encryptedPrivateKeyIv;
3748
+ if (typeof autoPayEnabled === 'boolean') {
3749
+ channel.autoPayEnabled = autoPayEnabled;
3750
+ }
3751
+ channel.updatedAt = new Date().toISOString();
3752
+ await saveChannels(channels);
3753
+ console.log(`[Wallet] channel ${channelId} 已存储加密私钥 (autoPay=${channel.autoPayEnabled})`);
3754
+ res.json(channel);
3755
+ }
3756
+ catch (err) {
3757
+ res.status(500).json({ error: err.message });
3758
+ }
3759
+ });
3760
+ /** 清除加密私钥 (用户选择不再自动支付) */
3761
+ app.delete('/channels/:channelId/encrypted-key', async (req, res) => {
3762
+ try {
3763
+ const { channelId } = req.params;
3764
+ const channels = await loadChannels();
3765
+ const channel = channels.find(c => c.id === channelId);
3766
+ if (!channel)
3767
+ return res.status(404).json({ error: 'Channel not found' });
3768
+ channel.encryptedPrivateKey = undefined;
3769
+ channel.encryptedPrivateKeyIv = undefined;
3770
+ channel.autoPayEnabled = false;
3771
+ channel.updatedAt = new Date().toISOString();
3772
+ await saveChannels(channels);
3773
+ console.log(`[Wallet] channel ${channelId} 已清除加密私钥`);
3774
+ res.json(channel);
3775
+ }
3776
+ catch (err) {
3777
+ res.status(500).json({ error: err.message });
3778
+ }
3779
+ });
3780
+ /** 切换 autoPay 开关 */
3781
+ app.patch('/channels/:channelId/auto-pay', async (req, res) => {
3782
+ try {
3783
+ const { channelId } = req.params;
3784
+ const { autoPayEnabled } = req.body || {};
3785
+ if (typeof autoPayEnabled !== 'boolean') {
3786
+ return res.status(400).json({ error: 'autoPayEnabled 必须是 boolean' });
3787
+ }
3788
+ const channels = await loadChannels();
3789
+ const channel = channels.find(c => c.id === channelId);
3790
+ if (!channel)
3791
+ return res.status(404).json({ error: 'Channel not found' });
3792
+ channel.autoPayEnabled = autoPayEnabled;
3793
+ channel.updatedAt = new Date().toISOString();
3794
+ await saveChannels(channels);
3795
+ console.log(`[Wallet] channel ${channelId} autoPay → ${autoPayEnabled}`);
3796
+ res.json(channel);
3797
+ }
3798
+ catch (err) {
3799
+ res.status(500).json({ error: err.message });
3800
+ }
3801
+ });
3731
3802
  // 2026-07-07 P1-C: 列出 channel 的项目事件日志 (L2)
3732
3803
  // 用于客户端时间线折叠块 + LLM prompt 注入
3733
3804
  app.get('/api/events/:channelId', async (req, res) => {
@@ -2123,6 +2123,31 @@ body {
2123
2123
  border-color: var(--accent);
2124
2124
  }
2125
2125
 
2126
+ .wallet-row .wallet-badges {
2127
+ display: flex;
2128
+ gap: 4px;
2129
+ margin-top: 2px;
2130
+ }
2131
+
2132
+ .wallet-row .wallet-badge {
2133
+ font-size: 9px;
2134
+ font-weight: 700;
2135
+ padding: 1px 5px;
2136
+ border-radius: 3px;
2137
+ letter-spacing: 0.3px;
2138
+ text-transform: uppercase;
2139
+ }
2140
+
2141
+ .wallet-row .badge-autopay {
2142
+ background: #1a6d1a;
2143
+ color: #8f8;
2144
+ }
2145
+
2146
+ .wallet-row .badge-stored {
2147
+ background: #1a4d6d;
2148
+ color: #8cf;
2149
+ }
2150
+
2126
2151
  .task-item {
2127
2152
  background: var(--bg-secondary);
2128
2153
  border: 1px solid var(--border);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bolloon/bolloon-agent",
3
- "version": "0.3.12",
3
+ "version": "0.3.14",
4
4
  "type": "module",
5
5
  "description": "P2P AI Document Agent - 全局安装后执行 `bolloon` 启动产品",
6
6
  "main": "dist/cli-entry.js",
@@ -48,7 +48,7 @@
48
48
  "src/constraint-runtime"
49
49
  ],
50
50
  "dependencies": {
51
- "@bolloon/bolloon-agent": "^0.3.10",
51
+ "@bolloon/bolloon-agent": "^0.3.13",
52
52
  "@bolloon/constraint-runtime": "0.1.0",
53
53
  "@capacitor/core": "^8.4.1",
54
54
  "@capacitor/ios": "^8.4.1",
@@ -65,6 +65,24 @@
65
65
  "@multiformats/multiaddr": "^13.0.3",
66
66
  "@noble/hashes": "^1.3.0",
67
67
  "@rayhanadev/iroh": "^0.1.1",
68
+ "@x402/aptos": "^2.19.0",
69
+ "@x402/avm": "^2.19.0",
70
+ "@x402/axios": "^2.19.0",
71
+ "@x402/core": "^2.19.0",
72
+ "@x402/evm": "^2.19.0",
73
+ "@x402/express": "^2.19.0",
74
+ "@x402/extensions": "^2.19.0",
75
+ "@x402/fastify": "^2.19.0",
76
+ "@x402/fetch": "^2.19.0",
77
+ "@x402/hedera": "^2.19.0",
78
+ "@x402/hono": "^2.19.0",
79
+ "@x402/keeta": "^2.19.0",
80
+ "@x402/mcp": "^2.19.0",
81
+ "@x402/next": "^2.19.0",
82
+ "@x402/paywall": "^2.19.0",
83
+ "@x402/stellar": "^2.19.0",
84
+ "@x402/svm": "^2.19.0",
85
+ "@x402/tvm": "^2.19.0",
68
86
  "b4a": "^1.8.1",
69
87
  "dotenv": "^17.4.2",
70
88
  "esbuild": "^0.24.0",
@@ -155,6 +173,7 @@
155
173
  "asarUnpack": [
156
174
  "node_modules/@diap/**/*",
157
175
  "node_modules/@rayhanadev/**/*",
176
+ "node_modules/@x402/**/*",
158
177
  "node_modules/libp2p/**/*",
159
178
  "node_modules/@libp2p/**/*"
160
179
  ]