@bolloon/bolloon-agent 0.5.2 → 0.5.4

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.
Files changed (80) hide show
  1. package/README.md +4 -4
  2. package/dist/agents/chain/transfer-classify.js +99 -0
  3. package/dist/agents/chain/transfer-index.js +354 -0
  4. package/dist/agents/identity/address-binding.js +613 -0
  5. package/dist/agents/network-pulse.js +706 -27
  6. package/dist/agents/pi-sdk-tools.js +67 -15
  7. package/dist/agents/pi-sdk.js +7 -1
  8. package/dist/agents/session-store.js +144 -3
  9. package/dist/agents/wallet-tools.js +63 -0
  10. package/dist/agents/x402/direct-payment.js +689 -0
  11. package/dist/agents/x402/order-identity.js +408 -0
  12. package/dist/agents/x402/paid-info-store.js +17 -0
  13. package/dist/agents/x402/seller-signing.js +614 -0
  14. package/dist/agents/x402/seller-summary.js +398 -0
  15. package/dist/cli/identity-command.js +327 -2
  16. package/dist/cli/ink-app.js +79 -27
  17. package/dist/cli/input-history.js +119 -0
  18. package/dist/cli/loading-tui.js +66 -6
  19. package/dist/cli/log-gate.js +39 -1
  20. package/dist/cli/protocol-envelope.js +4 -0
  21. package/dist/cli/session-summary.js +129 -0
  22. package/dist/cli/session-tree.js +141 -0
  23. package/dist/cli/status-segments.js +70 -0
  24. package/dist/cli/stores.js +16 -0
  25. package/dist/cli/update-commands.js +41 -25
  26. package/dist/cli/wallet-store.js +159 -0
  27. package/dist/cli/wallet-table.js +46 -0
  28. package/dist/cli/x402-buyer-command.js +463 -0
  29. package/dist/cli/x402-seller-command.js +289 -0
  30. package/dist/cli-entry.js +43 -5
  31. package/dist/electron-build/agents/chain/transfer-classify.js +106 -0
  32. package/dist/electron-build/agents/chain/transfer-classify.js.map +1 -0
  33. package/dist/electron-build/agents/chain/transfer-index.js +400 -0
  34. package/dist/electron-build/agents/chain/transfer-index.js.map +1 -0
  35. package/dist/electron-build/agents/identity/address-binding.js +672 -0
  36. package/dist/electron-build/agents/identity/address-binding.js.map +1 -0
  37. package/dist/electron-build/agents/network-pulse.js +714 -28
  38. package/dist/electron-build/agents/network-pulse.js.map +1 -1
  39. package/dist/electron-build/agents/pi-sdk-tools.js +68 -15
  40. package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
  41. package/dist/electron-build/agents/pi-sdk.js +7 -1
  42. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  43. package/dist/electron-build/agents/session-store.js +144 -3
  44. package/dist/electron-build/agents/session-store.js.map +1 -1
  45. package/dist/electron-build/agents/wallet-tools.js +102 -0
  46. package/dist/electron-build/agents/wallet-tools.js.map +1 -0
  47. package/dist/electron-build/agents/x402/direct-payment.js +744 -0
  48. package/dist/electron-build/agents/x402/direct-payment.js.map +1 -0
  49. package/dist/electron-build/agents/x402/order-identity.js +458 -0
  50. package/dist/electron-build/agents/x402/order-identity.js.map +1 -0
  51. package/dist/electron-build/agents/x402/paid-info-protocol.js.map +1 -1
  52. package/dist/electron-build/agents/x402/paid-info-store.js +18 -0
  53. package/dist/electron-build/agents/x402/paid-info-store.js.map +1 -1
  54. package/dist/electron-build/agents/x402/seller-signing.js +682 -0
  55. package/dist/electron-build/agents/x402/seller-signing.js.map +1 -0
  56. package/dist/electron-build/cli/wallet-store.js +200 -0
  57. package/dist/electron-build/cli/wallet-store.js.map +1 -0
  58. package/dist/electron-build/llm/pi-ai.js +25 -0
  59. package/dist/electron-build/llm/pi-ai.js.map +1 -1
  60. package/dist/electron-build/utils/auto-update.js +2 -2
  61. package/dist/electron-build/utils/auto-update.js.map +1 -1
  62. package/dist/electron-build/utils/runtime-bootstrap.js +1 -1
  63. package/dist/electron-build/utils/runtime-bootstrap.js.map +1 -1
  64. package/dist/electron-build/utils/update-manager.js +3 -3
  65. package/dist/electron-build/utils/update-manager.js.map +1 -1
  66. package/dist/electron-build/utils/version-identity.js +1 -1
  67. package/dist/electron-build/utils/version-identity.js.map +1 -1
  68. package/dist/electron-build/web/routes-x402-info.js +28 -1
  69. package/dist/electron-build/web/routes-x402-info.js.map +1 -1
  70. package/dist/index.js +829 -104
  71. package/dist/llm/pi-ai.js +23 -0
  72. package/dist/utils/auto-update.js +2 -2
  73. package/dist/utils/runtime-bootstrap.js +1 -1
  74. package/dist/utils/update-health.js +2 -2
  75. package/dist/utils/update-manager.js +3 -3
  76. package/dist/utils/version-identity.js +1 -1
  77. package/dist/web/bolloon-web.json +3 -3
  78. package/dist/web/routes-x402-info.js +28 -1
  79. package/dist/web/sw.js +1 -1
  80. package/package.json +2 -1
@@ -1,4 +1,5 @@
1
1
  import * as fs from 'fs/promises';
2
+ import { loadWalletTool } from './wallet-tools.js';
2
3
  import * as fsSync from 'fs';
3
4
  import * as path from 'path';
4
5
  import { currentPackageRoot } from '../utils/version-info.js';
@@ -2980,13 +2981,16 @@ export function registerBuiltinTools(ctx) {
2980
2981
  const { KeyManager } = await import('@diap/sdk');
2981
2982
  const kp = KeyManager.fromPrivateKey(Buffer.from(identity.privateKey, 'hex'));
2982
2983
  const displayName = args.name ? String(args.name) : agentId || 'bolloon-agent';
2983
- // 1. 确保本地 Kubo (自动安装 + 启动)
2984
+ // 1. 确保本地 Kubo (自动安装 + 启动) —— **已经在跑就别调 SDK 那次注定失败的 init**
2985
+ // (`sdk` 后面发布 IPNS 还要用, 所以模块导入留在函数作用域, 只把这一次调用关进短路里)
2984
2986
  const sdk = await import('@diap/sdk');
2985
- const checkKuboSetup = sdk.checkKuboSetup;
2986
- if (typeof checkKuboSetup === 'function') {
2987
- const setup = await checkKuboSetup(true, true);
2988
- if (!setup?.ready || !setup?.daemonRunning) {
2989
- return { success: false, error: '本地 Kubo 不可用 (自动安装失败), 无法发布到 IPFS' };
2987
+ if (!(await kuboAlreadyRunning())) {
2988
+ const checkKuboSetup = sdk.checkKuboSetup;
2989
+ if (typeof checkKuboSetup === 'function') {
2990
+ const setup = await checkKuboSetup(true, true);
2991
+ if (!setup?.ready || !setup?.daemonRunning) {
2992
+ return { success: false, error: '本地 Kubo 不可用 (自动安装失败), 无法发布到 IPFS' };
2993
+ }
2990
2994
  }
2991
2995
  }
2992
2996
  // 2. 注册 agent → 上传 DID 文档 → CID
@@ -3443,7 +3447,26 @@ export function registerBuiltinTools(ctx) {
3443
3447
  }
3444
3448
  // ─── IPFS/IPNS 通用 helper (2026-08-04) ─────────────────────────────────────
3445
3449
  // 复用 publish_did 的 checkKuboSetup 自动安装/启动本地 Kubo (darwin-arm64 v0.28.0)
3450
+ /**
3451
+ * Kubo 本地节点已经在跑吗 (只读探测: POST /api/v0/version —— Kubo 只接受 POST).
3452
+ *
3453
+ * 2026-09-30 新增 (leo 报「开启过程的日志没去掉」): SDK 的 `checkKuboSetup(true, true)` 即使守护进程
3454
+ * 已经在跑, 仍会跑一遍 `ipfs init` ⇒ 抛 "daemon is running" ⇒ **打一行 [warn](消息里含"失败"二字,
3455
+ * 于是被启动日志门按"错误不许被吞"放行到屏上)** 外加子进程输出, 两行都落在启动面板之前.
3456
+ * 启动路径上所有调用点都先用这个探测短路: 已经在跑就没有任何可装配的.
3457
+ */
3458
+ export async function kuboAlreadyRunning(timeoutMs = 1500) {
3459
+ try {
3460
+ await kuboApi('/api/v0/version', undefined, timeoutMs);
3461
+ return true;
3462
+ }
3463
+ catch {
3464
+ return false;
3465
+ }
3466
+ }
3446
3467
  export async function ensureKuboReady() {
3468
+ if (await kuboAlreadyRunning())
3469
+ return; // 已在跑 ⇒ 没有可装配的, 也别触发那次注定失败的 init
3447
3470
  const sdk = await import('@diap/sdk');
3448
3471
  const checkKuboSetup = sdk.checkKuboSetup;
3449
3472
  if (typeof checkKuboSetup === 'function') {
@@ -3483,9 +3506,24 @@ export function registerWalletTools(ctx) {
3483
3506
  parameters: {},
3484
3507
  execute: async () => {
3485
3508
  try {
3486
- const { createWallet } = await import('../constraint-runtime/dist/tools/WalletTools/createWallet.js').catch(() => import('../constraint-runtime/src/tools/WalletTools/createWallet.js'));
3509
+ const { createWallet } = await loadWalletTool('createWallet');
3487
3510
  const r = await createWallet();
3488
- return { success: true, output: `✅ 钱包创建成功:\n address: ${r.address}\n privateKey: ${r.privateKey}\n mnemonic: ${r.mnemonic}\n createdAt: ${r.createdAt}` };
3511
+ // 2026-09-30: **落台账** (以前只生成不存 ⇒ /wallet 永远「钱包 (0)」)。
3512
+ // 落 ~/.bolloon/wallets/<名字>.json (0600); 输出只给地址 + 路径, 私钥/助记词不回显。
3513
+ let savedPath = '';
3514
+ try {
3515
+ const { addWallet } = await import('../cli/wallet-store.js');
3516
+ const { file } = await addWallet({
3517
+ name: `agent-${new Date().toISOString().slice(0, 19).replace(/[:T]/g, '')}`,
3518
+ address: r.address, network: '—', source: 'created',
3519
+ createdAt: r.createdAt || new Date().toISOString(), privateKey: r.privateKey, mnemonic: r.mnemonic,
3520
+ });
3521
+ savedPath = file;
3522
+ }
3523
+ catch (e) {
3524
+ return { success: true, output: `⚠️ 钱包已生成但**没落台账**: ${String(e?.message || e).slice(0, 120)}\n address: ${r.address}\n (私钥只在本次输出里, 请立刻自己存好)` };
3525
+ }
3526
+ return { success: true, output: `✅ 钱包创建成功 (已落台账)\n address: ${r.address}\n 台账: ${savedPath} (0600 — 私钥/助记词已存, 不回显)\n createdAt: ${r.createdAt}` };
3489
3527
  }
3490
3528
  catch (e) {
3491
3529
  return { success: false, error: `创建失败: ${String(e.message || e)}` };
@@ -3498,9 +3536,23 @@ export function registerWalletTools(ctx) {
3498
3536
  parameters: { mnemonic: '可选, 12/15/18/21/24 词助记词', privateKey: '可选, 0x 开头的私钥' },
3499
3537
  execute: async (args) => {
3500
3538
  try {
3501
- const { importWallet } = await import('../constraint-runtime/dist/tools/WalletTools/importWallet.js').catch(() => import('../constraint-runtime/src/tools/WalletTools/importWallet.js'));
3539
+ const { importWallet } = await loadWalletTool('importWallet');
3502
3540
  const r = await importWallet({ mnemonic: args.mnemonic, privateKey: args.privateKey });
3503
- return { success: true, output: `✅ 钱包导入成功:\n address: ${r.address}\n privateKey: ${r.privateKey}\n source: ${r.source}` };
3541
+ // 2026-09-30: 同 wallet_create —— 导入也落台账 (0600), 输出不回显私钥
3542
+ let impPath = '';
3543
+ try {
3544
+ const { addWallet } = await import('../cli/wallet-store.js');
3545
+ const { file } = await addWallet({
3546
+ name: `imported-${new Date().toISOString().slice(0, 19).replace(/[:T]/g, '')}`,
3547
+ address: r.address, network: '—', source: 'imported',
3548
+ createdAt: new Date().toISOString(), privateKey: args.privateKey, mnemonic: args.mnemonic,
3549
+ });
3550
+ impPath = file;
3551
+ }
3552
+ catch (e) {
3553
+ return { success: true, output: `⚠️ 钱包已导入但**没落台账**: ${String(e?.message || e).slice(0, 120)}\n address: ${r.address}\n (私钥请自己存好, 不会再有第二遍)` };
3554
+ }
3555
+ return { success: true, output: `✅ 钱包导入成功 (已落台账)\n address: ${r.address}\n 台账: ${impPath} (0600 — 私钥已存, 不回显)\n source: ${r.source}` };
3504
3556
  }
3505
3557
  catch (e) {
3506
3558
  return { success: false, error: `导入失败: ${String(e.message || e)}` };
@@ -3513,7 +3565,7 @@ export function registerWalletTools(ctx) {
3513
3565
  parameters: { address: '0x 开头的 EVM 地址 (必填)', rpcUrl: '可选 RPC URL (默认 eth.llamarpc.com)' },
3514
3566
  execute: async (args) => {
3515
3567
  try {
3516
- const { getBalance } = await import('../constraint-runtime/dist/tools/WalletTools/getBalance.js').catch(() => import('../constraint-runtime/src/tools/WalletTools/getBalance.js'));
3568
+ const { getBalance } = await loadWalletTool('getBalance');
3517
3569
  const r = await getBalance({ address: String(args.address), rpcUrl: args.rpcUrl });
3518
3570
  return { success: true, output: `💰 ${r.address}\n ${r.balanceEth} ${r.symbol} (${r.balance} wei)` };
3519
3571
  }
@@ -3528,7 +3580,7 @@ export function registerWalletTools(ctx) {
3528
3580
  parameters: { message: '要签名的消息 (必填)', privateKey: '0x 开头的私钥 (必填)' },
3529
3581
  execute: async (args) => {
3530
3582
  try {
3531
- const { signMessage } = await import('../constraint-runtime/dist/tools/WalletTools/signMessage.js').catch(() => import('../constraint-runtime/src/tools/WalletTools/signMessage.js'));
3583
+ const { signMessage } = await loadWalletTool('signMessage');
3532
3584
  const r = await signMessage({ message: String(args.message), privateKey: String(args.privateKey) });
3533
3585
  return { success: true, output: `✅ 签名完成:\n address: ${r.address}\n message: ${r.message}\n signature: ${r.signature}` };
3534
3586
  }
@@ -3543,7 +3595,7 @@ export function registerWalletTools(ctx) {
3543
3595
  parameters: { privateKey: '私钥 (必填)', to: '接收地址 (必填)', value: '发送 wei 数量 (必填)', data: '可选 0x 开头的 calldata', rpcUrl: '可选 RPC URL' },
3544
3596
  execute: async (args) => {
3545
3597
  try {
3546
- const { sendTransaction } = await import('../constraint-runtime/dist/tools/WalletTools/sendTransaction.js').catch(() => import('../constraint-runtime/src/tools/WalletTools/sendTransaction.js'));
3598
+ const { sendTransaction } = await loadWalletTool('sendTransaction');
3547
3599
  const r = await sendTransaction({
3548
3600
  privateKey: String(args.privateKey),
3549
3601
  to: String(args.to),
@@ -3564,7 +3616,7 @@ export function registerWalletTools(ctx) {
3564
3616
  parameters: { privateKey: '私钥 (必填)', tokenAddress: 'ERC20 合约地址 (必填)', to: '接收地址 (必填)', amount: 'token 数量', decimals: '可选 token decimals', rpcUrl: '可选 RPC URL' },
3565
3617
  execute: async (args) => {
3566
3618
  try {
3567
- const { transferToken } = await import('../constraint-runtime/dist/tools/WalletTools/transferToken.js').catch(() => import('../constraint-runtime/src/tools/WalletTools/transferToken.js'));
3619
+ const { transferToken } = await loadWalletTool('transferToken');
3568
3620
  const r = await transferToken({
3569
3621
  privateKey: String(args.privateKey),
3570
3622
  tokenAddress: String(args.tokenAddress),
@@ -3586,7 +3638,7 @@ export function registerWalletTools(ctx) {
3586
3638
  parameters: { from: '付款方地址 (必填)', to: '收款方地址 (必填)', amount: '金额 (必填)', interval: '周期 (e.g. daily/weekly/monthly)', token: '可选, 默认 ETH' },
3587
3639
  execute: async (args) => {
3588
3640
  try {
3589
- const mod = await import('../constraint-runtime/dist/tools/WalletTools/autoPay.js').catch(() => import('../constraint-runtime/src/tools/WalletTools/autoPay.js'));
3641
+ const mod = await loadWalletTool('autoPay');
3590
3642
  const fn = mod.autoPay || mod.setAutoPay || mod.default;
3591
3643
  if (!fn)
3592
3644
  return { success: false, error: 'autoPay 接口未找到' };
@@ -466,7 +466,13 @@ export class PiAgentSession {
466
466
  }
467
467
  }
468
468
  catch (err) {
469
- console.warn(`[PiAgent] hydrateMessageHistory 失败 (non-fatal): ${err.message?.slice(0, 100)}`);
469
+ // 2026-09-30: "会话文件不存在" 不是失败 —— 新 channel / 还没落过盘的会话就是这种情况
470
+ // (真机噪声: `hydrateMessageHistory 失败 (non-fatal): session not found: ch_bolloon:default`)。
471
+ // 只有**非** not-found 的错误才值得 warn。
472
+ const __msg = String(err?.message || err);
473
+ if (!/session not found|ENOENT/i.test(__msg)) {
474
+ console.warn(`[PiAgent] hydrateMessageHistory 失败 (non-fatal): ${__msg.slice(0, 100)}`);
475
+ }
470
476
  }
471
477
  }
472
478
  /**
@@ -102,6 +102,59 @@ export class SessionStore {
102
102
  * fs.writeFile 抛 EINVAL. 修法: filename 层 escape `:` → `__`, key 保持不变
103
103
  * (load/save/listKeys/deleteKey 全部透明).
104
104
  */
105
+ /**
106
+ * **写**的时候用这个 —— 与 resolveExisting 的区别: 不存在**不抛错**, 返回规范路径 (等于新建)。
107
+ *
108
+ * 2026-09-30 回归事故: 我把 resolveExisting (读语义, 不存在就抛 not found) 批量替换进了
109
+ * saveMessages ⇒ 退出存档对"还没落盘的新会话"直接抛 `session not found`
110
+ * (leo 贴的 `⚠ session 存档失败: SessionStore: session not found: 20260930_175348_870c70`)。
111
+ * 写路径必须容忍"还没有文件" —— 那正是它要创建的东西。
112
+ */
113
+ async resolveWritePath(key) {
114
+ const canonical = this.pathFor(key);
115
+ try {
116
+ await fs.access(canonical);
117
+ return canonical;
118
+ }
119
+ catch { /* 试老名 */ }
120
+ const legacy = path.join(this.cacheDir, `${key}.json`);
121
+ try {
122
+ await fs.access(legacy);
123
+ return legacy;
124
+ }
125
+ catch { /* 新会话 */ }
126
+ return canonical;
127
+ }
128
+ /**
129
+ * 读的时候用这个 —— 兼容**老文件名**。
130
+ *
131
+ * 2026-09-30 发现: 早期版本直接把 `channelId:sessionId` 当文件名写盘 (`real-123:default.json`),
132
+ * 后来 Windows 兼容改成 filenameEscape (`:` → `__`), 但**老文件没迁移** ⇒ 用 pathFor 去读
133
+ * 老会话一律 ENOENT (`/sessions` 里那些 `real-*:default` 全是 Preview `—`, 也续不上)。
134
+ * 这里: 先试新名, 不存在再试老名; 老名存在时**顺手迁移**成新名 (迁移失败就继续读老名)。
135
+ */
136
+ async resolveExisting(key) {
137
+ const canonical = this.pathFor(key);
138
+ try {
139
+ await fs.access(canonical);
140
+ return canonical;
141
+ }
142
+ catch { /* 试老名 */ }
143
+ const legacy = path.join(this.cacheDir, `${key}.json`);
144
+ try {
145
+ await fs.access(legacy);
146
+ try {
147
+ await fs.rename(legacy, canonical);
148
+ }
149
+ catch {
150
+ return legacy;
151
+ } // 迁移失败也能读
152
+ return canonical;
153
+ }
154
+ catch {
155
+ throw new Error(`SessionStore: session not found: ${key}`);
156
+ }
157
+ }
105
158
  pathFor(key) {
106
159
  if (!key || key.includes('/') || key.includes('..')) {
107
160
  throw new Error(`SessionStore: invalid key ${JSON.stringify(key)}`);
@@ -135,7 +188,7 @@ export class SessionStore {
135
188
  totalCount: messages.length,
136
189
  },
137
190
  };
138
- const filePath = this.pathFor(key);
191
+ const filePath = await this.resolveWritePath(key); // 写路径: 新会话就建 (不抛 not found)
139
192
  const tmpPath = `${filePath}.tmp`;
140
193
  await fs.writeFile(tmpPath, JSON.stringify(payload, null, 2), 'utf-8');
141
194
  await fs.rename(tmpPath, filePath);
@@ -177,7 +230,7 @@ export class SessionStore {
177
230
  totalCount: messages.length,
178
231
  },
179
232
  };
180
- const filePath = this.pathFor(key);
233
+ const filePath = this.pathFor(key); // 同步保存一律写规范文件名
181
234
  const tmpPath = `${filePath}.tmp`;
182
235
  fsSyncWrite(tmpPath, JSON.stringify(payload, null, 2));
183
236
  fsSyncRename(tmpPath, filePath);
@@ -192,7 +245,11 @@ export class SessionStore {
192
245
  if (!key || key.includes('/') || key.includes('..')) {
193
246
  throw new Error(`SessionStore: invalid key ${JSON.stringify(key)}`);
194
247
  }
195
- const filePath = this.pathFor(key);
248
+ // 2026-09-30 回归修复: 读路径**不许**在"会话不存在"时抛错 —— 仓里的契约是
249
+ // `loadMessages('cli:nonexistent') === null` (见 src/test/session-store.test.ts)。
250
+ // 用容错解析 (不存在 ⇒ 返回规范路径), 随后的 readFile 命中 ENOENT ⇒ return null。
251
+ // 老文件名(`real-123:default.json` 那批)由 resolveWritePath 兼容: 存在就返回老路径。
252
+ const filePath = await this.resolveWritePath(key);
196
253
  let raw;
197
254
  try {
198
255
  raw = await fs.readFile(filePath, 'utf-8');
@@ -222,6 +279,90 @@ export class SessionStore {
222
279
  return [];
223
280
  }
224
281
  }
282
+ /**
283
+ * 列最近改动的会话 (按 mtime 倒序), 带条数与首条用户消息预览。
284
+ *
285
+ * 2026-09-30 (leo: 「/session 没有反应, 没展示最新记录, 无法选择继续」): 只为"选一个继续"服务 ——
286
+ * **只解析最新的 limit 个文件**(缓存目录可能上千个, 全量解析会卡住输入), 解析失败的那条如实记 0。
287
+ */
288
+ async listRecent(limit = 10) {
289
+ const out = [];
290
+ let files = [];
291
+ try {
292
+ files = await fs.readdir(this.cacheDir);
293
+ }
294
+ catch {
295
+ return out;
296
+ }
297
+ const cands = [];
298
+ for (const f of files) {
299
+ if (!f.endsWith('.json') || f.endsWith('.tmp'))
300
+ continue;
301
+ try {
302
+ const st = await fs.stat(path.join(this.cacheDir, f));
303
+ cands.push({ key: SessionStore.filenameUnescape(f.slice(0, -'.json'.length)), mtimeMs: st.mtimeMs });
304
+ }
305
+ catch { /* 单条 stat 失败跳过 */ }
306
+ }
307
+ cands.sort((a, b) => b.mtimeMs - a.mtimeMs);
308
+ for (const c of cands.slice(0, Math.max(1, limit))) {
309
+ let messages = 0;
310
+ let preview = '';
311
+ let title = c.key; // 会话文件里没有 title 字段 ⇒ 默认就是 key (web 侧才有会话名)
312
+ try {
313
+ // 兼容老文件名 (`real-123:default.json` 未转义那批) —— 读不到规范名就试原名
314
+ let text = '';
315
+ try {
316
+ text = await fs.readFile(this.pathFor(c.key), 'utf-8');
317
+ }
318
+ catch {
319
+ text = await fs.readFile(path.join(this.cacheDir, `${c.key}.json`), 'utf-8');
320
+ }
321
+ const raw = JSON.parse(text);
322
+ const arr = Array.isArray(raw) ? raw : (raw?.messages || []);
323
+ messages = Array.isArray(arr) ? arr.length : 0;
324
+ const md = (!Array.isArray(raw) && raw?.metadata) || {};
325
+ if (typeof md.title === 'string' && md.title.trim())
326
+ title = md.title.trim();
327
+ const mdPreview = typeof md.preview === 'string' ? md.preview.trim() : '';
328
+ // 预览要用**真实用户话**, 跳过自动注入的上下文块 (否则全是 <!-- current-turn … -->)
329
+ const isInjected = (x) => !!x && (/^<!--/.test(x.trim()) || /^#\s*你的项目上下文/.test(x.trim()) || /^\[cron\]/.test(x.trim()));
330
+ const first = (Array.isArray(arr) ? arr : []).find((m) => m && m.role === 'user' && m.content && !isInjected(String(m.content)));
331
+ preview = mdPreview || String(first?.content || '').replace(/\s+/g, ' ').trim().slice(0, 120);
332
+ }
333
+ catch { /* 损坏文件: 仍列出, 只是 0 条无预览 */ }
334
+ out.push({ key: c.key, title, mtimeMs: c.mtimeMs, messages, preview });
335
+ }
336
+ return out;
337
+ }
338
+ /**
339
+ * 合并写回会话文件的 metadata (原子: 先写 .tmp 再 rename)。
340
+ * 2026-09-30 (leo: 「Title/Preview 要有内容, 使用 AI 来总结进去」): AI 总结结果落在
341
+ * `metadata.{title,preview,summarizedAt}`, **只算一次**, 后续 /sessions 直接读。
342
+ * 原子写是硬要求 —— 会话文件是用户的会话历史, 半截文件比没有标题糟得多。
343
+ */
344
+ async updateMetadata(key, patch) {
345
+ if (!key || key.includes('/') || key.includes('..'))
346
+ throw new Error(`SessionStore: invalid key ${JSON.stringify(key)}`);
347
+ const file = await this.resolveWritePath(key);
348
+ let raw = null;
349
+ try {
350
+ raw = JSON.parse(await fs.readFile(file, 'utf-8'));
351
+ }
352
+ catch (e) {
353
+ if (e?.code !== 'ENOENT')
354
+ throw new Error(`会话文件读不了 (${file}): ${String(e?.message || e).slice(0, 120)}`);
355
+ // 还没有文件 ⇒ 建成只有 metadata 的会话 (元数据更新对新会话应当有效, 不是错误)
356
+ }
357
+ const next = raw === null
358
+ ? { key, messages: [], metadata: { ...patch } }
359
+ : Array.isArray(raw)
360
+ ? { key, messages: raw, metadata: { ...patch } }
361
+ : { ...raw, metadata: { ...(raw?.metadata || {}), ...patch } };
362
+ const tmp = `${file}.tmp`;
363
+ await fs.writeFile(tmp, JSON.stringify(next, null, 2), 'utf-8');
364
+ await fs.rename(tmp, file);
365
+ }
225
366
  /** 删一条 session — 失败抛错. */
226
367
  async deleteKey(key) {
227
368
  if (!key || key.includes('/') || key.includes('..')) {
@@ -0,0 +1,63 @@
1
+ /**
2
+ * wallet-tools.ts — 钱包工具的**双布局加载器** (2026-09-30)
3
+ *
4
+ * 为什么要有这个文件:
5
+ * 钱包真身是 `constraint-runtime` 里的 `tools/WalletTools/*`。构建 (`npm run build:main` =
6
+ * `tsc && node scripts/copy-constraint-runtime.mjs`) 把 `src/constraint-runtime/dist/*`
7
+ * 复制成 `dist/constraint-runtime/*` —— **少一层 `dist/`**。于是同一句相对导入:
8
+ * `../constraint-runtime/dist/tools/WalletTools/createWallet.js`
9
+ * 在源码态 (src/agents) 能解析 ✓, 构建后 (dist/agents) 解析到 `dist/constraint-runtime/dist/…` ✗
10
+ * ⇒ `wallet_create` 报 `Cannot find module '…/dist/constraint-runtime/src/tools/…'` (leo 2026-09-30 贴的屏),
11
+ * 而 `/wallet` 又因为"从来没写过文件"显示「钱包 (0)」—— 两个毛病叠在一起, 看着像"钱包功能不存在"。
12
+ *
13
+ * 规则: **按存在的路径挑**, 挑不到就抛错并**列出试过的路径** (不许含糊成"未知错误")。
14
+ * 注意: 这里用**变量**给 `import()` ⇒ 不做静态解析 (跨布局本来就不能静态解析)。
15
+ */
16
+ import * as path from 'path';
17
+ import { pathToFileURL } from 'url';
18
+ // 2026-09-30: 不用 import.meta —— 本文件会被 electron 那条 CJS 链间接引到,
19
+ // `tsc -p tsconfig.electron.json` 会报 TS1343 (module: CommonJS 下不允许 import.meta),
20
+ // 而 `npm publish` 的 prepublishOnly (build:all → build:electron) 会因此挂住。
21
+ // 仓里现成的替代: cjsModuleDir() (CJS/ESM 都能用) + currentPackageRoot() + firstExisting()。
22
+ import { cjsModuleDir, firstExisting } from '../utils/module-context.js';
23
+ import { currentPackageRoot } from '../utils/version-info.js';
24
+ /** 候选路径 (先近后远; 覆盖 dev / dist 两种布局) */
25
+ export function walletToolCandidates(name) {
26
+ const root = (() => { try {
27
+ return currentPackageRoot();
28
+ }
29
+ catch {
30
+ return process.cwd();
31
+ } })();
32
+ const here = cjsModuleDir() || path.join(root, 'dist', 'agents'); // src/agents 或 dist/agents
33
+ return [
34
+ // ① 构建后布局: dist/constraint-runtime/tools/… (从 dist/agents 出发)
35
+ path.join(here, '..', 'constraint-runtime', 'tools', 'WalletTools', `${name}.js`),
36
+ // ② 源码态布局: src/constraint-runtime/dist/tools/… (从 src/agents 出发)
37
+ path.join(here, '..', 'constraint-runtime', 'dist', 'tools', 'WalletTools', `${name}.js`),
38
+ // ③ 兜底: 从仓根分别按两种布局找
39
+ path.join(root, 'dist', 'constraint-runtime', 'tools', 'WalletTools', `${name}.js`),
40
+ path.join(root, 'src', 'constraint-runtime', 'dist', 'tools', 'WalletTools', `${name}.js`),
41
+ ];
42
+ }
43
+ /** 加载一个钱包工具模块 (挑第一个真实存在的; 都不在就带着"试过哪些"报错) */
44
+ export async function loadWalletTool(name) {
45
+ const tried = walletToolCandidates(name);
46
+ const hit = firstExisting(tried);
47
+ if (hit)
48
+ return (await import(pathToFileURL(hit).href));
49
+ throw new Error(`找不到钱包工具 ${name} (试过 ${tried.length} 个路径):\n ${tried.join('\n ')}`);
50
+ }
51
+ /** 给诊断用: 当前各自解析到哪 (打印用的字符串, 不加载) */
52
+ export function walletToolLayout() {
53
+ const names = ['createWallet', 'getBalance', 'signMessage'];
54
+ const out = [];
55
+ for (const n of names) {
56
+ for (const c of walletToolCandidates(n)) {
57
+ if (out.some(o => o.pattern === c))
58
+ continue;
59
+ out.push({ pattern: c, exists: firstExisting([c]) !== null });
60
+ }
61
+ }
62
+ return out;
63
+ }