@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
@@ -14,7 +14,7 @@
14
14
  */
15
15
  import { resolveBolloonHome } from '../setup/setup-store.js';
16
16
  import { collectVersionInfo, renderVersionText, renderVersionJson, DEV_CHANNEL_WARNING, DEV_BACK_TO_STABLE_HINT, channelKindOf, isDevIdentity, } from '../utils/version-info.js';
17
- import { checkForUpdate, buildUpdatePlan, renderUpdatePlan, applyUpdate, readUpdateStatus, readHistory, renderHistory, updateSummaryFor, compareVersions, checkExitCode, REFUSED_STATUSES, } from '../utils/update-manager.js';
17
+ import { checkForUpdate, buildUpdatePlan, renderUpdatePlan, applyUpdate, readUpdateStatus, readHistory, renderHistory, updateSummaryFor, compareVersions, REFUSED_STATUSES, } from '../utils/update-manager.js';
18
18
  import { renderGithubReportLine } from '../utils/dual-source.js';
19
19
  import { runDoctor, renderDoctor } from '../utils/update-health.js';
20
20
  import { detectRuntimeReport, bootstrapRuntimes, renderRuntimeReport, renderBootstrapPlan, RUNTIME_MIN, readRuntimeConfigWithFallback, } from '../utils/runtime-bootstrap.js';
@@ -148,7 +148,7 @@ export function renderCheckResult(r) {
148
148
  L.push(`说明: ${r.reason}`);
149
149
  L.push('');
150
150
  if (r.status === 'update_available') {
151
- L.push(`下一步: ${BOLD}bolloon update plan --channel ${r.channel}${RESET} 看计划, ${BOLD}bolloon update now --channel ${r.channel}${RESET} 执行更新`);
151
+ L.push(`下一步: ${BOLD}bolloon update --channel ${r.channel}${RESET} 直接更新 (会先打印计划与风险检查); 只想看计划不动任何东西: ${BOLD}bolloon update --dry-run${RESET}`);
152
152
  if (dev)
153
153
  L.push(DEV_CHANNEL_WARNING), L.push(DEV_BACK_TO_STABLE_HINT);
154
154
  }
@@ -183,7 +183,7 @@ export function renderStatusReport(s) {
183
183
  : 'stable (npm registry)'}`);
184
184
  L.push(`身份标识: ${dev ? `commit ${s.installedDevSha || '未知'}` : s.currentVersion}`);
185
185
  L.push(`能切回: ${s.switchableTo
186
- ? `${s.switchableTo.channel} (${s.switchableTo.source}${s.switchableTo.target ? ` @ ${s.switchableTo.target}` : ''}) — 一键切: bolloon update now --channel ${s.switchableTo.channel}`
186
+ ? `${s.switchableTo.channel} (${s.switchableTo.source}${s.switchableTo.target ? ` @ ${s.switchableTo.target}` : ''}) — 一键切: bolloon update --channel ${s.switchableTo.channel}`
187
187
  : '无 (未知来源)'}`);
188
188
  if (s.lastDevSha && !dev)
189
189
  L.push(`上次 dev: commit ${s.lastDevSha}${s.devCheckedAt ? ` @ ${s.devCheckedAt}` : ''} (已切回 stable)`);
@@ -218,7 +218,7 @@ export function renderStatusReport(s) {
218
218
  // ── update 子命令 ───────────────────────────────────────────────────────────
219
219
  export async function runUpdateCommand(args = []) {
220
220
  const home = resolveBolloonHome();
221
- // 裸词优先 (bolloon update plan / status / history / now / wait / force / json / --channel dev),
221
+ // 裸词优先 (bolloon update / status / history / wait / force / dry-run / json / --channel dev),
222
222
  // 同时兼容旧的 --plan / --status / ... 写法 (已有脚本不断裂)。
223
223
  const has = (...f) => f.some((x) => args.includes(x));
224
224
  const json = has('json', '--json');
@@ -228,6 +228,28 @@ export async function runUpdateCommand(args = []) {
228
228
  return 2;
229
229
  }
230
230
  const channel = ch.channel;
231
+ // `update --help` 必须是**只读**的 (三合一后默认会真装, 这条不拦就会「看帮助反而装了东西」)
232
+ if (has('help', '-h', '--help')) {
233
+ out(UPDATE_HELP.trim());
234
+ return 0;
235
+ }
236
+ /**
237
+ * 防误触 (2026-09-28, 与三合一同时加的): `update` 现在**默认就执行更新** ——
238
+ * 于是打错一个词 (如 `bolloon update plna`) 会直接装东西, 这是不可接受的。
239
+ * 白名单之外、又不是数字、又不是 --channel 的取值的裸词 ⇒ 拒绝执行并打用法。
240
+ */
241
+ {
242
+ const KNOWN_BARE = new Set(['status', 'history', 'wait', 'force', 'dry-run', 'plan', 'json', 'now']);
243
+ const valIdx = new Set();
244
+ args.forEach((a, i) => { if (a === '--channel' || a === 'channel')
245
+ valIdx.add(i + 1); });
246
+ const unknown = args.filter((a, i) => !a.startsWith('-') && !KNOWN_BARE.has(a) && !/^\d+$/.test(a) && !valIdx.has(i));
247
+ if (unknown.length > 0) {
248
+ err(`${RED}✗ 不认识 ${BOLD}update ${unknown[0]}${RESET} —— 已拒绝执行 (什么都没装)`);
249
+ err(` 用法: bolloon update [wait|force] [--dry-run|status|history [N]] [--channel stable|dev]`);
250
+ return 2;
251
+ }
252
+ }
231
253
  // ── 状态 ──
232
254
  if (has('status', '--status')) {
233
255
  const s = await readUpdateStatus({ home, channel });
@@ -250,8 +272,9 @@ export async function runUpdateCommand(args = []) {
250
272
  out(renderHistory(recs));
251
273
  return 0;
252
274
  }
253
- // ── 计划 ──
254
- if (has('plan', '--plan')) {
275
+ // ── 只读: --dry-run (2026-09-28 leo: `update` 三个动作合成一个, 默认直接执行;
276
+ // 只读位改由 `--dry-run` 承接 —— 旧写法 `plan`/`--plan` 等价于它, **只打印、不装任何东西**) ──
277
+ if (has('dry-run', '--dry-run', 'plan', '--plan')) {
255
278
  const plan = await buildUpdatePlan({ home, force: true, channel });
256
279
  if (json) {
257
280
  out(JSON.stringify(plan, null, 2));
@@ -260,10 +283,15 @@ export async function runUpdateCommand(args = []) {
260
283
  out(renderUpdatePlan(plan));
261
284
  if (plan.channelKind === 'git-ref')
262
285
  printDevNotice();
286
+ out('');
287
+ out(`${YELLOW}本次只打印计划, 没有安装任何东西${RESET} —— 要真更新就运行 ${BOLD}bolloon update${RESET}`);
263
288
  return 0;
264
289
  }
265
- // ── 执行 ──
266
- if (has('now', '--now')) {
290
+ // ── 默认动作 (三合一): 计划 + 风险检查 + 直接执行 ──
291
+ {
292
+ if (has('now', '--now')) {
293
+ out(`${YELLOW}提示${RESET}: \`now\` 已去掉 —— \`bolloon update\` 现在直接执行更新 (本条按新行为继续)。`);
294
+ }
267
295
  const wait = has('wait', '--wait');
268
296
  const force = has('force', '--force', '-f');
269
297
  const plan = await buildUpdatePlan({ home, force: true, channel });
@@ -332,18 +360,6 @@ export async function runUpdateCommand(args = []) {
332
360
  err(` 已回滚到 ${res.from}`);
333
361
  return 1;
334
362
  }
335
- // ── 默认: 只检查, 给清晰结论 (显式命令 = 忽略节流) ──
336
- const r = await checkForUpdate({ home, force: true, channel });
337
- if (json) {
338
- out(JSON.stringify(r, null, 2));
339
- return checkExitCode(r.status);
340
- }
341
- out(renderCheckResult(r));
342
- if (r.status !== 'up_to_date' && r.status !== 'check_skipped') {
343
- out('');
344
- out(`${YELLOW}提示${RESET}: 本次只检查, 没有安装任何东西。`);
345
- }
346
- return checkExitCode(r.status);
347
363
  }
348
364
  // ── doctor ──────────────────────────────────────────────────────────────────
349
365
  export async function runDoctorCommand(args = []) {
@@ -441,12 +457,12 @@ ${BOLD}版本与更新:${RESET} (子命令一律裸词, 不带 -- 前缀)
441
457
  bolloon --version 版本 + 安装方式/目录/入口/通道/来源身份/上游/是否最新
442
458
  bolloon --version verbose 诊断版 (构建时间/git/Node/npm/Python/配置目录/最近更新)
443
459
  bolloon --version json 机器版 (同一份 VersionInfo, 给脚本与问题报告)
444
- bolloon update 只检查并给结论 (不安装任何东西)
445
- bolloon update plan 更新计划: 要更新什么 / 不会动什么 / 风险检查
460
+ bolloon update 直接执行更新 (先打印计划与风险检查, 再真装; 不写 plan/now)
461
+ bolloon update --dry-run 只看计划与风险检查, 一个字节都不动 (旧 update plan 等价于它)
462
+ bolloon update wait 等当前 Run 结束后再更新 (有长期任务时的默认建议)
463
+ bolloon update force 忽略"有长期任务在跑"的提醒 (阻塞项仍然阻塞)
446
464
  bolloon update status 更新状态: 装的哪个源+版本/commit+能切回哪/最近检查/失败/开关
447
465
  bolloon update history [N] 最近 N 次更新 (时间 · 版本变化 · 结果 · 耗时 · 原因)
448
- bolloon update now 真正执行更新 (默认先看计划)
449
- bolloon update now wait 等当前 Run 结束后再更新 (有长期任务时的默认建议)
450
466
  bolloon doctor 安装入口 + 版本事实 + 更新状态 + 运行时 是否自洽
451
467
  bolloon doctor json 同上, 机器可读
452
468
  bolloon runtime [plan|install|json] 运行时 (Node/npm/Git/Python) 检查与安装 (install 需显式 yes)
@@ -456,7 +472,7 @@ ${BOLD}双源 (--channel):${RESET}
456
472
  比较语义 = semver (npm 的 latest ↔ GitHub 同名 tag)
457
473
  --channel dev GitHub master HEAD 是唯一源; 比较语义 = git ref + commit sha (不用 semver);
458
474
  身份 = <版本>+dev.<sha7>; 未走发布门, 会打印警告并记录 sha
459
- 一键回稳定版: bolloon update now --channel stable
475
+ 一键回稳定版: bolloon update --channel stable
460
476
  --channel 只覆盖本次进程, 不落盘 (config 里的 updateChannel 仍是默认值)
461
477
  源不可达 / 版本不存在 → 拒绝并说清 (退出码 2), 不会静默装回旧版
462
478
 
@@ -0,0 +1,159 @@
1
+ /**
2
+ * wallet-store.ts — bolloon 自己的钱包台账: **一个目录, 一钱包一文件** (leo 2026-09-30)
3
+ *
4
+ * 事实与边界:
5
+ * · agent 工具 `wallet_create` / `wallet_import` 只**生成/解析**钱包, **从不落盘**
6
+ * (`constraint-runtime/src/tools/WalletTools/createWallet.ts` 纯 `Wallet.createRandom()`) ⇒
7
+ * `/wallet` 一直是「钱包 (0)」不是路径写错, 是**从来没写过文件**。
8
+ * · 台账目录 = `~/.bolloon/wallets/`, **一钱包一文件** `<名字>.json` (**0600**)。
9
+ * 放这儿而不是仓库里: 仓库是公开的 (`private:false`), 私钥进 git 就完了。
10
+ * · 文件里允许有私钥/助记词 (签名要用), 但**命令输出永不回显** —— 只给地址 + 路径。
11
+ * · 不读别的 agent 的钱包目录 (`~/.hermes/wallets/…`): `chain-config.ts` 定的硬规则 ——
12
+ * 那不属于本进程, 读了就是越权。要收编必须由**用户显式给路径** (`/wallet import <path>`)。
13
+ * · 老的单文件台账 (`~/.bolloon/wallets.json`) 若存在 ⇒ 一次性拆进目录, 原文件改名 `.migrated`
14
+ * (不删: 用户的数据, 出问题能翻回去)。
15
+ */
16
+ import * as fs from 'fs/promises';
17
+ import * as path from 'path';
18
+ /** 一钱包一文件所在目录 */
19
+ export function walletsDir() {
20
+ return path.join(process.env.HOME || '/tmp', '.bolloon', 'wallets');
21
+ }
22
+ /** 文件名安全化 (只保留字母数字与 - _ .) */
23
+ function safeName(name, fallback = 'wallet') {
24
+ const s = String(name || '').trim().replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '');
25
+ return s || fallback;
26
+ }
27
+ async function readOne(file) {
28
+ try {
29
+ const raw = JSON.parse(await fs.readFile(file, 'utf-8'));
30
+ const address = raw?.address;
31
+ if (typeof address !== 'string' || !address.startsWith('0x'))
32
+ return null;
33
+ return {
34
+ name: String(raw?.name || path.basename(file).replace(/\.json$/, '')),
35
+ address,
36
+ network: String(raw?.network || '—'),
37
+ source: raw?.source === 'imported' ? 'imported' : 'created',
38
+ createdAt: String(raw?.createdAt || ''),
39
+ privateKey: typeof raw?.privateKey === 'string' ? raw.privateKey : undefined,
40
+ mnemonic: typeof raw?.mnemonic === 'string' ? raw.mnemonic : undefined,
41
+ };
42
+ }
43
+ catch {
44
+ return null; // 坏文件不挡列表 —— 但下面 loadWallets 会把"读不了的"计数报出去
45
+ }
46
+ }
47
+ /** 老单文件台账拆进目录 (一次性; 原文件改名 .migrated 保留) */
48
+ async function migrateLegacySingleFile(dir) {
49
+ const legacy = path.join(process.env.HOME || '/tmp', '.bolloon', 'wallets.json');
50
+ let raw;
51
+ try {
52
+ raw = JSON.parse(await fs.readFile(legacy, 'utf-8'));
53
+ }
54
+ catch {
55
+ return 0;
56
+ }
57
+ const list = Array.isArray(raw) ? raw : Array.isArray(raw?.wallets) ? raw.wallets : [];
58
+ let n = 0;
59
+ for (const w of list) {
60
+ if (!w?.address)
61
+ continue;
62
+ const rec = {
63
+ name: safeName(w.name || `wallet-${n + 1}`), address: String(w.address),
64
+ network: String(w.network || '—'), source: w.source === 'imported' ? 'imported' : 'created',
65
+ createdAt: String(w.createdAt || new Date().toISOString()),
66
+ privateKey: typeof w.privateKey === 'string' ? w.privateKey : undefined,
67
+ mnemonic: typeof w.mnemonic === 'string' ? w.mnemonic : undefined,
68
+ };
69
+ await writeWalletFile(dir, rec);
70
+ n++;
71
+ }
72
+ try {
73
+ await fs.rename(legacy, `${legacy}.migrated`);
74
+ }
75
+ catch { /* 改名失败就留着, 下次再试 */ }
76
+ return n;
77
+ }
78
+ async function writeWalletFile(dir, rec) {
79
+ await fs.mkdir(dir, { recursive: true });
80
+ const file = path.join(dir, `${safeName(rec.name)}.json`);
81
+ const tmp = `${file}.tmp`;
82
+ await fs.writeFile(tmp, JSON.stringify(rec, null, 2), { encoding: 'utf-8', mode: 0o600 });
83
+ await fs.rename(tmp, file);
84
+ try {
85
+ await fs.chmod(file, 0o600);
86
+ }
87
+ catch { /* 已足够 */ }
88
+ return file;
89
+ }
90
+ /** 迁移只做一次 (模块级旗标) —— 每次进程首次读台账时试一次 */
91
+ let legacyMigrated = false;
92
+ /** 读全部钱包 (一钱包一文件, 按文件名排序) */
93
+ export async function loadWallets() {
94
+ const dir = walletsDir();
95
+ await fs.mkdir(dir, { recursive: true }).catch(() => { });
96
+ // 老单文件台账: 每进程首次读的时候试一次迁移 (旗标防重复; 迁移里自己判断有没有老文件)
97
+ if (!legacyMigrated) {
98
+ legacyMigrated = true;
99
+ const n = await migrateLegacySingleFile(dir);
100
+ if (n > 0) { /* 迁移来的文件就在下面一起读 */ }
101
+ }
102
+ let names = [];
103
+ try {
104
+ names = (await fs.readdir(dir)).filter(n => n.endsWith('.json') && !n.endsWith('.tmp')).sort();
105
+ }
106
+ catch (e) {
107
+ throw new Error(`钱包目录读不了 (${dir}): ${String(e?.message || e).slice(0, 120)}`);
108
+ }
109
+ const out = [];
110
+ for (const n of names) {
111
+ const rec = await readOne(path.join(dir, n));
112
+ if (rec)
113
+ out.push(rec);
114
+ }
115
+ return out;
116
+ }
117
+ /**
118
+ * 写一个钱包进目录。
119
+ * · 同地址**不新建文件**: 合并进那条 (不覆盖已有私钥/助记词)
120
+ * · 名字撞了就自动加后缀 (-2, -3 …)
121
+ */
122
+ export async function addWallet(rec) {
123
+ const dir = walletsDir();
124
+ const existing = await loadWallets();
125
+ const dup = existing.find(w => w.address.toLowerCase() === rec.address.toLowerCase());
126
+ if (dup) {
127
+ const merged = {
128
+ ...rec,
129
+ name: dup.name, // 保留原来的文件名/名字
130
+ privateKey: dup.privateKey || rec.privateKey,
131
+ mnemonic: dup.mnemonic || rec.mnemonic,
132
+ createdAt: dup.createdAt || rec.createdAt,
133
+ };
134
+ const file = await writeWalletFile(dir, merged);
135
+ return { rec: merged, replaced: true, file };
136
+ }
137
+ let name = safeName(rec.name);
138
+ const used = new Set(existing.map(w => w.name));
139
+ for (let i = 2; used.has(name); i++)
140
+ name = `${safeName(rec.name)}-${i}`;
141
+ const out = { ...rec, name };
142
+ const file = await writeWalletFile(dir, out);
143
+ return { rec: out, replaced: false, file };
144
+ }
145
+ /** 按 # / 地址 / 名字挑一个 */
146
+ export function pickWallet(list, spec) {
147
+ if (!spec)
148
+ return list[0];
149
+ const n = Number(spec);
150
+ if (Number.isFinite(n) && n >= 1)
151
+ return list[n - 1];
152
+ if (spec.startsWith('0x'))
153
+ return list.find(w => w.address.toLowerCase() === spec.toLowerCase());
154
+ return list.find(w => w.name === spec);
155
+ }
156
+ /** 地址缩写 (单行显示用) */
157
+ export function shortAddr(a) {
158
+ return a && a.length > 12 ? `${a.slice(0, 6)}…${a.slice(-4)}` : String(a || '?');
159
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * wallet-table.ts — `/wallet` 的钱包表格渲染 (leo 2026-09-30: 「钱包格式也统一一个表格」)
3
+ *
4
+ * 为什么单独抽出来: 表格的**对齐/裁宽**必须能确定性验证 —— 之前它内联在 CLI 的 /wallet 分支里,
5
+ * 只能靠 PTY 抓包看 (那工具这轮抽风 6 次只成 2 次), 验证变成了碰运气。
6
+ * 抽成纯函数 (进 list, 出 string[] 无 ANSI) ⇒ 可以直接跑、直接看列宽对不对 (含中文名列)。
7
+ *
8
+ * 与 `/sessions` 同一套口径: 列宽按终端宽动态分配; 中文按**显示宽**对齐 (dispWidth);
9
+ * 地址优先给满 42 字符 (能直接复制), 列宽不足才退回缩写 (0x1234…abcd) —— 不截半截地址。
10
+ */
11
+ import { dispWidth, truncate } from './loading-tui.js';
12
+ /**
13
+ * 返回表格行 (无 ANSI): [表头, 分隔线, ...数据行]
14
+ * 调用方负责加颜色前缀/缩进。
15
+ */
16
+ export function renderWalletTable(list, opts) {
17
+ const zh = opts.lang !== 'en';
18
+ const PAD = (v, w) => v + ' '.repeat(Math.max(0, w - dispWidth(v)));
19
+ const avail = Math.max(60, opts.width - 2);
20
+ const wN = 3, wChain = 8, wSrc = 6, wDate = 10;
21
+ const fixed = wN + wChain + wSrc + wDate + 5; // 5 = 列间空格
22
+ const wAddr = Math.max(13, Math.min(42, Math.floor((avail - fixed) * 0.55)));
23
+ const wName = Math.max(12, avail - fixed - wAddr);
24
+ const cols = [wN, wName, wAddr, wChain, wSrc, wDate];
25
+ const hdr = [
26
+ PAD('#', wN),
27
+ PAD(zh ? '名字' : 'Name', wName),
28
+ PAD(zh ? '地址' : 'Address', wAddr),
29
+ PAD(zh ? '链' : 'Chain', wChain),
30
+ PAD(zh ? '来源' : 'Source', wSrc),
31
+ PAD(zh ? '建立' : 'Created', wDate),
32
+ ].join(' ');
33
+ const rule = cols.map(w => '─'.repeat(w)).join(' ');
34
+ const rows = list.map((w, k) => {
35
+ const addr = wAddr >= 42 ? w.address : opts.shortAddr(w.address);
36
+ return [
37
+ PAD(String(k + 1), wN),
38
+ PAD(truncate(w.name, wName), wName),
39
+ PAD(truncate(addr, wAddr), wAddr),
40
+ PAD(truncate(w.network || '—', wChain), wChain),
41
+ PAD(w.source === 'imported' ? (zh ? '导入' : 'imported') : (zh ? '生成' : 'created'), wSrc),
42
+ PAD(String(w.createdAt).slice(0, 10), wDate),
43
+ ].join(' ');
44
+ });
45
+ return [hdr, rule, ...rows];
46
+ }