@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.
- package/README.md +4 -4
- package/dist/agents/chain/transfer-classify.js +99 -0
- package/dist/agents/chain/transfer-index.js +354 -0
- package/dist/agents/identity/address-binding.js +613 -0
- package/dist/agents/network-pulse.js +706 -27
- package/dist/agents/pi-sdk-tools.js +67 -15
- package/dist/agents/pi-sdk.js +7 -1
- package/dist/agents/session-store.js +144 -3
- package/dist/agents/wallet-tools.js +63 -0
- package/dist/agents/x402/direct-payment.js +689 -0
- package/dist/agents/x402/order-identity.js +408 -0
- package/dist/agents/x402/paid-info-store.js +17 -0
- package/dist/agents/x402/seller-signing.js +614 -0
- package/dist/agents/x402/seller-summary.js +398 -0
- package/dist/cli/identity-command.js +327 -2
- package/dist/cli/ink-app.js +79 -27
- package/dist/cli/input-history.js +119 -0
- package/dist/cli/loading-tui.js +66 -6
- package/dist/cli/log-gate.js +39 -1
- package/dist/cli/protocol-envelope.js +4 -0
- package/dist/cli/session-summary.js +129 -0
- package/dist/cli/session-tree.js +141 -0
- package/dist/cli/status-segments.js +70 -0
- package/dist/cli/stores.js +16 -0
- package/dist/cli/update-commands.js +41 -25
- package/dist/cli/wallet-store.js +159 -0
- package/dist/cli/wallet-table.js +46 -0
- package/dist/cli/x402-buyer-command.js +463 -0
- package/dist/cli/x402-seller-command.js +289 -0
- package/dist/cli-entry.js +43 -5
- package/dist/electron-build/agents/chain/transfer-classify.js +106 -0
- package/dist/electron-build/agents/chain/transfer-classify.js.map +1 -0
- package/dist/electron-build/agents/chain/transfer-index.js +400 -0
- package/dist/electron-build/agents/chain/transfer-index.js.map +1 -0
- package/dist/electron-build/agents/identity/address-binding.js +672 -0
- package/dist/electron-build/agents/identity/address-binding.js.map +1 -0
- package/dist/electron-build/agents/network-pulse.js +714 -28
- package/dist/electron-build/agents/network-pulse.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk-tools.js +68 -15
- package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk.js +7 -1
- package/dist/electron-build/agents/pi-sdk.js.map +1 -1
- package/dist/electron-build/agents/session-store.js +144 -3
- package/dist/electron-build/agents/session-store.js.map +1 -1
- package/dist/electron-build/agents/wallet-tools.js +102 -0
- package/dist/electron-build/agents/wallet-tools.js.map +1 -0
- package/dist/electron-build/agents/x402/direct-payment.js +744 -0
- package/dist/electron-build/agents/x402/direct-payment.js.map +1 -0
- package/dist/electron-build/agents/x402/order-identity.js +458 -0
- package/dist/electron-build/agents/x402/order-identity.js.map +1 -0
- package/dist/electron-build/agents/x402/paid-info-protocol.js.map +1 -1
- package/dist/electron-build/agents/x402/paid-info-store.js +18 -0
- package/dist/electron-build/agents/x402/paid-info-store.js.map +1 -1
- package/dist/electron-build/agents/x402/seller-signing.js +682 -0
- package/dist/electron-build/agents/x402/seller-signing.js.map +1 -0
- package/dist/electron-build/cli/wallet-store.js +200 -0
- package/dist/electron-build/cli/wallet-store.js.map +1 -0
- package/dist/electron-build/llm/pi-ai.js +25 -0
- package/dist/electron-build/llm/pi-ai.js.map +1 -1
- package/dist/electron-build/utils/auto-update.js +2 -2
- package/dist/electron-build/utils/auto-update.js.map +1 -1
- package/dist/electron-build/utils/runtime-bootstrap.js +1 -1
- package/dist/electron-build/utils/runtime-bootstrap.js.map +1 -1
- package/dist/electron-build/utils/update-manager.js +3 -3
- package/dist/electron-build/utils/update-manager.js.map +1 -1
- package/dist/electron-build/utils/version-identity.js +1 -1
- package/dist/electron-build/utils/version-identity.js.map +1 -1
- package/dist/electron-build/web/routes-x402-info.js +28 -1
- package/dist/electron-build/web/routes-x402-info.js.map +1 -1
- package/dist/index.js +829 -104
- package/dist/llm/pi-ai.js +23 -0
- package/dist/utils/auto-update.js +2 -2
- package/dist/utils/runtime-bootstrap.js +1 -1
- package/dist/utils/update-health.js +2 -2
- package/dist/utils/update-manager.js +3 -3
- package/dist/utils/version-identity.js +1 -1
- package/dist/web/bolloon-web.json +3 -3
- package/dist/web/routes-x402-info.js +28 -1
- package/dist/web/sw.js +1 -1
- package/package.json +2 -1
|
@@ -17,10 +17,13 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import * as fs from 'fs/promises';
|
|
19
19
|
import * as os from 'os';
|
|
20
|
+
import * as path from 'path';
|
|
20
21
|
import * as crypto from 'crypto';
|
|
21
|
-
import { okEnvelope, failEnvelope, line, title, hint, plain, has, } from './protocol-envelope.js';
|
|
22
|
+
import { okEnvelope, failEnvelope, line, title, hint, plain, has, opt, } from './protocol-envelope.js';
|
|
22
23
|
import { getLocalIdentityFile, initLocalIdentity } from './setup-wizard.js';
|
|
23
24
|
import { AUDIT_FORBIDDEN_KEYS } from '../agents/task-contract.js';
|
|
25
|
+
import { KeyManager } from '@diap/sdk';
|
|
26
|
+
import { ADDRESS_BINDING_PROTOCOL, BINDING_INDEX_FILENAME, DEFAULT_BINDING_CHAIN_ID, BINDING_ADDRESS_RE, bindingsDir, bindingIndexPath, newBindingNonce, createAddressBinding, verifyAddressBinding, payerIdentityOf, writeBindingRecord, readBindingRecord, readBindingIndex, rebuildBindingIndex, loadPayerIdentityIndex, buildPublicBindingProjection, didShort, } from '../agents/identity/address-binding.js';
|
|
24
27
|
export const IDENTITY_USAGE = `
|
|
25
28
|
${title('bolloon identity')}
|
|
26
29
|
bolloon identity init [--force] [--json]
|
|
@@ -31,6 +34,25 @@ ${title('bolloon identity')}
|
|
|
31
34
|
**绝不打印私钥** (输出只有 did / 文件路径 / 文件权限)
|
|
32
35
|
bolloon identity show [--json]
|
|
33
36
|
看本机身份: did · 公钥指纹(sha256 前 16 位) · 文件权限 · 创建时间 (不含私钥)
|
|
37
|
+
bolloon identity bind-address --address 0x…全小写 --address-key-file <path> [--label <名>]
|
|
38
|
+
[--did-key-file <path>] [--chain-id 8453] [--expires-at <ISO8601>]
|
|
39
|
+
[--bindings-dir <dir>] [--json]
|
|
40
|
+
登记「地址 ↔ DID」绑定 (**链下登记, 可离线验签**): 同一份声明正文被**两侧**签名 ——
|
|
41
|
+
· sig_did = DID 私钥 (Ed25519, ~/.bolloon/identity.json) 对 canonical 正文签名
|
|
42
|
+
· sig_addr = 该以太地址私钥 (EIP-191 personal_sign) 对**同一份**正文签名
|
|
43
|
+
验证 = 两侧都过才算数 (少一侧 = 拒, 不降级)。落盘前**自验不通过就整条失败** (不留半成品)。
|
|
44
|
+
红线: **没有默认钱包路径** (--address-key-file 必填, 以免手滑拿主钱包签); 私钥不进任何输出。
|
|
45
|
+
名字短写: --label 会被清洗成 [A-Za-z0-9._-] + 中日文 (≤24 字符), 另有一枚 label_sig 覆盖它。
|
|
46
|
+
bolloon identity bindings list [--bindings-dir <dir>] [--json]
|
|
47
|
+
列本机绑定库 (~/.bolloon/bindings): id · 名字短写 · DID 短写 · 地址(仅本机) · 链 · 签发时间
|
|
48
|
+
**重验**: 每条都当场验一遍, 不通过的如实列出 (它们不会进快照)
|
|
49
|
+
bolloon identity bindings show <file> [--json]
|
|
50
|
+
看一条绑定的正文/签名/判据 (地址与 DID 全文只在本机命令里出; 页面上只出短写)
|
|
51
|
+
bolloon identity bindings verify <file> [--bindings-dir <dir>] [--json]
|
|
52
|
+
离线复验一条绑定: 逐条列判据 (双侧签名 · 正文 canonical · did↔公钥自洽 · 时效 · nonce 一次性);
|
|
53
|
+
**不过 = 非零退出码**
|
|
54
|
+
bolloon identity bindings publish --out <path> [--bindings-dir <dir>] [--json]
|
|
55
|
+
写出**对外投影** (显式路径): 只有短写 (name_short / did_short) + 地址哈希; 地址与完整 DID 不出
|
|
34
56
|
|
|
35
57
|
选项: --json · --quiet · --force (只对 init 有效)
|
|
36
58
|
说明: 只建 identity.json; 供应商/API key 仍走 bolloon setup (或 bolloon setup --provider … --api-key …);
|
|
@@ -170,8 +192,311 @@ export async function identityCommand(flags) {
|
|
|
170
192
|
return identityInit(flags);
|
|
171
193
|
if (sub === 'show' || sub === 'status')
|
|
172
194
|
return identityShow(flags);
|
|
195
|
+
if (sub === 'bind-address')
|
|
196
|
+
return identityBindAddress(flags);
|
|
197
|
+
if (sub === 'bindings')
|
|
198
|
+
return identityBindings(flags);
|
|
173
199
|
return {
|
|
174
|
-
envelope: failEnvelope('INVALID_ARGUMENT', sub ? `未知 identity 子命令: ${sub}` : '缺少 identity 子命令 (init | show)', { usage: plain(IDENTITY_USAGE.trim()), accepted: ['bolloon identity init', 'bolloon identity show'] }, [], 'needs_human'),
|
|
200
|
+
envelope: failEnvelope('INVALID_ARGUMENT', sub ? `未知 identity 子命令: ${sub}` : '缺少 identity 子命令 (init | show | bind-address | bindings)', { usage: plain(IDENTITY_USAGE.trim()), accepted: ['bolloon identity init', 'bolloon identity show', 'bolloon identity bind-address --address 0x… --address-key-file <path>', 'bolloon identity bindings list|show|verify|publish'] }, [], 'needs_human'),
|
|
175
201
|
human: IDENTITY_USAGE,
|
|
176
202
|
};
|
|
177
203
|
}
|
|
204
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
205
|
+
// 地址 ↔ DID 绑定登记 (`diap-address-binding/1`) —— 2026-09-29
|
|
206
|
+
// 让网关能把链上付款行里的**付款方地址**翻成**智能体身份**。
|
|
207
|
+
// 红线 (逐条都在这段代码里落实):
|
|
208
|
+
// · **不默认任何钱包路径** —— `--address-key-file` 必填, 免得手滑拿主钱包签名;
|
|
209
|
+
// · 私钥只在进程内用一次, **不进 stdout / 不进绑定文件 / 不进日志** (只打印地址);
|
|
210
|
+
// · 绑定库的 `nonce` 一次性: 生成时先查索引, 撞了就换;
|
|
211
|
+
// · 落盘前必须**自验通过**(双侧签名), 否则整条命令失败, 不留半成品。
|
|
212
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
213
|
+
/** 读一个"以太地址私钥"文件: 认 JSON 的 `privateKey` 字段, 也认纯文本 (0x + 64 hex) */
|
|
214
|
+
async function readAddressKeyFile(file) {
|
|
215
|
+
const raw = (await fs.readFile(file, 'utf-8')).trim();
|
|
216
|
+
let pk = '';
|
|
217
|
+
if (raw.startsWith('{')) {
|
|
218
|
+
const j = JSON.parse(raw);
|
|
219
|
+
pk = String(j?.privateKey ?? j?.private_key ?? j?.key ?? '');
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
pk = raw.split(/\s+/)[0];
|
|
223
|
+
}
|
|
224
|
+
pk = pk.trim();
|
|
225
|
+
if (!/^(0x)?[0-9a-fA-F]{64}$/.test(pk)) {
|
|
226
|
+
throw new Error(`地址私钥文件里没找到 32 字节私钥 (JSON 的 privateKey 字段或纯文本 0x+64 hex): ${file}`);
|
|
227
|
+
}
|
|
228
|
+
return pk.startsWith('0x') ? pk : `0x${pk}`;
|
|
229
|
+
}
|
|
230
|
+
/** `bolloon identity bind-address --address 0x… --address-key-file <path> [--label …]` */
|
|
231
|
+
async function identityBindAddress(flags) {
|
|
232
|
+
const head = 'bolloon identity bind-address';
|
|
233
|
+
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
234
|
+
const address = String(opt(flags, '--address') || '').trim().toLowerCase();
|
|
235
|
+
const keyFile = opt(flags, '--address-key-file');
|
|
236
|
+
const didFile = opt(flags, '--did-key-file') || getLocalIdentityFile(home);
|
|
237
|
+
const dir = opt(flags, '--bindings-dir') || bindingsDir(home);
|
|
238
|
+
const label = opt(flags, '--label');
|
|
239
|
+
const chainIdRaw = opt(flags, '--chain-id');
|
|
240
|
+
const expiresAt = opt(flags, '--expires-at') || null;
|
|
241
|
+
const bad = (code, msg, data = {}) => ({
|
|
242
|
+
envelope: failEnvelope(code, msg, { ...data, keyMaterialInOutput: false }, [], 'needs_human'),
|
|
243
|
+
human: `${title(head)}\n 原因: ${msg}\n\n${hint(IDENTITY_USAGE.trim())}`,
|
|
244
|
+
});
|
|
245
|
+
if (!address || !BINDING_ADDRESS_RE.test(address)) {
|
|
246
|
+
return bad('INVALID_ARGUMENT', `--address 必须是**全小写** 0x + 40 位十六进制 (实得 ${JSON.stringify(opt(flags, '--address') ?? '')})`, { address: address || null });
|
|
247
|
+
}
|
|
248
|
+
if (!keyFile) {
|
|
249
|
+
return bad('INVALID_ARGUMENT', '必须显式给 --address-key-file <path> —— 本命令**没有默认钱包**: 不替你在任何目录里找一个私钥来签 (以免拿主钱包误签)', { address, needs: '--address-key-file' });
|
|
250
|
+
}
|
|
251
|
+
const chainId = chainIdRaw ? Number(chainIdRaw) : DEFAULT_BINDING_CHAIN_ID;
|
|
252
|
+
if (!Number.isInteger(chainId) || chainId <= 0)
|
|
253
|
+
return bad('INVALID_ARGUMENT', `--chain-id 必须是正整数 (实得 ${JSON.stringify(chainIdRaw)})`, { chainId: chainIdRaw ?? null });
|
|
254
|
+
if (expiresAt && !Number.isFinite(Date.parse(expiresAt)))
|
|
255
|
+
return bad('INVALID_ARGUMENT', `--expires-at 必须是 ISO8601 时间串 (实得 ${expiresAt})`, { expiresAt });
|
|
256
|
+
// 地址私钥 (只读一次, 不进任何输出)
|
|
257
|
+
let addressKey = '';
|
|
258
|
+
try {
|
|
259
|
+
addressKey = await readAddressKeyFile(keyFile);
|
|
260
|
+
}
|
|
261
|
+
catch (e) {
|
|
262
|
+
return bad('NOT_FOUND', `读地址私钥失败: ${String(e?.message || e).slice(0, 160)}`, { addressKeyFile: keyFile });
|
|
263
|
+
}
|
|
264
|
+
// DID 身份 (与 `agent-identity.ts` / 卖方签名同源: KeyManager)
|
|
265
|
+
let did = '';
|
|
266
|
+
let didPrivateKeyHex = '';
|
|
267
|
+
let didPublicKeyHex = '';
|
|
268
|
+
try {
|
|
269
|
+
const kp = await KeyManager.fromFile(didFile);
|
|
270
|
+
did = String(kp?.did || '');
|
|
271
|
+
didPrivateKeyHex = Buffer.from(kp.privateKey).toString('hex');
|
|
272
|
+
didPublicKeyHex = Buffer.from(kp.publicKey).toString('hex');
|
|
273
|
+
}
|
|
274
|
+
catch (e) {
|
|
275
|
+
return bad('NOT_FOUND', `读本机 DID 身份失败 (${didFile}): ${String(e?.message || e).slice(0, 160)} —— 没有身份先跑 bolloon identity init`, { didKeyFile: didFile });
|
|
276
|
+
}
|
|
277
|
+
// nonce 一次性: 生成前先看本机索引里有没有撞 (撞了就换一枚)
|
|
278
|
+
let index = readBindingIndex({ dir });
|
|
279
|
+
if (!index) {
|
|
280
|
+
try {
|
|
281
|
+
index = await rebuildBindingIndex({ dir });
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
index = null;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
const used = new Set(Object.keys(index?.nonces || {}));
|
|
288
|
+
let nonce = newBindingNonce();
|
|
289
|
+
for (let i = 0; i < 8 && used.has(nonce); i++)
|
|
290
|
+
nonce = newBindingNonce();
|
|
291
|
+
let rec;
|
|
292
|
+
try {
|
|
293
|
+
rec = await createAddressBinding({ did, didPrivateKeyHex, didPublicKeyHex, addressPrivateKeyHex: addressKey, address, chainId, expiresAt, nonce, label, createdBy: 'bolloon identity bind-address' });
|
|
294
|
+
}
|
|
295
|
+
catch (e) {
|
|
296
|
+
return bad('POLICY_DENIED', `造绑定失败 (未落盘): ${String(e?.message || e).slice(0, 200)}`, { address, chainId, didShort: didShort(did) });
|
|
297
|
+
}
|
|
298
|
+
finally {
|
|
299
|
+
addressKey = ''; // 用完即弃 (不留到后面任何一行)
|
|
300
|
+
}
|
|
301
|
+
let file = '';
|
|
302
|
+
try {
|
|
303
|
+
const w = await writeBindingRecord(rec, { dir });
|
|
304
|
+
file = w.file;
|
|
305
|
+
}
|
|
306
|
+
catch (e) {
|
|
307
|
+
return bad('DUPLICATE_REQUEST', `绑定落盘失败: ${String(e?.message || e).slice(0, 160)}`, { id: rec.id, dir });
|
|
308
|
+
}
|
|
309
|
+
const ident = payerIdentityOf(rec);
|
|
310
|
+
const payload = {
|
|
311
|
+
action: 'bound',
|
|
312
|
+
id: rec.id,
|
|
313
|
+
protocol: ADDRESS_BINDING_PROTOCOL,
|
|
314
|
+
address,
|
|
315
|
+
did,
|
|
316
|
+
didShort: ident.did_short,
|
|
317
|
+
nameShort: ident.name_short,
|
|
318
|
+
label: rec.label ?? null,
|
|
319
|
+
chainId: rec.statement.chainId,
|
|
320
|
+
issuedAt: rec.statement.issuedAt,
|
|
321
|
+
expiresAt: rec.statement.expiresAt,
|
|
322
|
+
nonce: rec.statement.nonce,
|
|
323
|
+
statementJson: rec.statement_json,
|
|
324
|
+
sigDidAlg: 'ed25519',
|
|
325
|
+
sigAddrAlg: 'eip-191-personal-sign',
|
|
326
|
+
verified: true,
|
|
327
|
+
verifiedChecks: rec.verified_checks ?? [],
|
|
328
|
+
file,
|
|
329
|
+
bindingsDir: dir,
|
|
330
|
+
didKeyFile: didFile,
|
|
331
|
+
addressKeyFile: keyFile,
|
|
332
|
+
keyMaterialInOutput: false,
|
|
333
|
+
semantics: '链下登记(可离线验签) —— 不是链上事实',
|
|
334
|
+
};
|
|
335
|
+
return {
|
|
336
|
+
envelope: okEnvelope('OK', `已登记地址↔DID 绑定 (${rec.id} · 双侧签名自验通过)`, payload, [
|
|
337
|
+
`off-chain signed statement: ${rec.statement_json}`,
|
|
338
|
+
], null),
|
|
339
|
+
human: [
|
|
340
|
+
title(head),
|
|
341
|
+
line('绑定 id', rec.id),
|
|
342
|
+
line('协议', ADDRESS_BINDING_PROTOCOL),
|
|
343
|
+
line('地址', address),
|
|
344
|
+
line('DID', did),
|
|
345
|
+
line('DID 短写', ident.did_short),
|
|
346
|
+
line('智能体名', ident.name_short),
|
|
347
|
+
line('chainId', rec.statement.chainId),
|
|
348
|
+
line('issuedAt', rec.statement.issuedAt),
|
|
349
|
+
line('expiresAt', rec.statement.expiresAt ?? '(不过期)'),
|
|
350
|
+
line('绑定文件', file),
|
|
351
|
+
line('自验判据', (rec.verified_checks || []).join(' · ')),
|
|
352
|
+
'',
|
|
353
|
+
` ${hint('口径: 「链下登记(可离线验签)」—— 这是签名声明, 不是链上事实')}`,
|
|
354
|
+
' 私钥没进任何输出; 地址私钥只在本进程内用了一瞬',
|
|
355
|
+
` 复核: bolloon identity bindings verify ${file}`,
|
|
356
|
+
].join('\n'),
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
/** `bolloon identity bindings list|show|verify|publish` */
|
|
360
|
+
async function identityBindings(flags) {
|
|
361
|
+
const head = 'bolloon identity bindings';
|
|
362
|
+
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
363
|
+
const dir = opt(flags, '--bindings-dir') || bindingsDir(home);
|
|
364
|
+
const sub = String(flags.positionals[1] ?? '').trim().toLowerCase();
|
|
365
|
+
const rest = flags.positionals.slice(2).filter((p) => p && p !== 'true');
|
|
366
|
+
const fail = (code, msg, data = {}) => ({
|
|
367
|
+
envelope: failEnvelope(code, msg, { ...data, bindingsDir: dir, keyMaterialInOutput: false }, [], 'needs_human'),
|
|
368
|
+
human: `${title(head)}\n 原因: ${msg}\n\n${hint('用法: bindings list [--bindings-dir <dir>] · bindings show <file> · bindings verify <file> [--bindings-dir <dir>] · bindings publish --out <path>')}`,
|
|
369
|
+
});
|
|
370
|
+
if (sub === 'list' || sub === '') {
|
|
371
|
+
let index = readBindingIndex({ dir });
|
|
372
|
+
let rebuilt = false;
|
|
373
|
+
if (!index) {
|
|
374
|
+
index = await rebuildBindingIndex({ dir });
|
|
375
|
+
rebuilt = true;
|
|
376
|
+
}
|
|
377
|
+
const rows = index.bindings.map((b) => ({
|
|
378
|
+
id: b.id, address: b.address, name_short: b.name_short, did_short: b.did_short,
|
|
379
|
+
chainId: b.chainId, issuedAt: b.issuedAt, expiresAt: b.expiresAt, verified_at: b.verified_at, file: b.file,
|
|
380
|
+
}));
|
|
381
|
+
const loaded = await loadPayerIdentityIndex({ dir, now: Date.now() });
|
|
382
|
+
return {
|
|
383
|
+
envelope: okEnvelope('OK', `${rows.length} 条绑定 (重验通过 ${loaded.verified}${loaded.rejected.length ? ` · 拒 ${loaded.rejected.length}` : ''})`, {
|
|
384
|
+
action: 'list', bindingsDir: dir, indexFile: bindingIndexPath(home) === path.join(dir, BINDING_INDEX_FILENAME) ? bindingIndexPath(home) : path.join(dir, BINDING_INDEX_FILENAME),
|
|
385
|
+
rebuilt, count: rows.length, verified: loaded.verified, rejected: loaded.rejected, reason: loaded.reason,
|
|
386
|
+
bindings: rows, keyMaterialInOutput: false,
|
|
387
|
+
semantics: '链下登记(可离线验签) —— 每行是签名声明, 不是链上事实; address 只在本机索引里',
|
|
388
|
+
}, [], null),
|
|
389
|
+
human: [
|
|
390
|
+
title(head + ' list'),
|
|
391
|
+
line('绑定库', dir),
|
|
392
|
+
line('条数', rows.length),
|
|
393
|
+
line('重验', loaded.reason),
|
|
394
|
+
...(rows.length
|
|
395
|
+
? ['', ...rows.map((r) => ` ${r.id} ${r.name_short.padEnd(20)} ${r.did_short} ${r.address} chain=${r.chainId} ${r.issuedAt}`)]
|
|
396
|
+
: ['', ' (空 —— 还没有地址↔DID 登记; 地址不出公开面, 名字来自链下签名声明)']),
|
|
397
|
+
...(loaded.rejected.length ? ['', ` ⚠ 有 ${loaded.rejected.length} 条重验不过 (不进快照): ${loaded.rejected.map((r) => `${r.id}:${r.reasons.join(',')}`).join(' | ').slice(0, 200)}`] : []),
|
|
398
|
+
].join('\n'),
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
if (sub === 'show' || sub === 'verify') {
|
|
402
|
+
const file = rest[0] ? path.resolve(rest[0]) : '';
|
|
403
|
+
if (!file)
|
|
404
|
+
return fail('INVALID_ARGUMENT', `bindings ${sub} 需要文件路径: <file>`);
|
|
405
|
+
let rec;
|
|
406
|
+
try {
|
|
407
|
+
rec = readBindingRecord(file);
|
|
408
|
+
}
|
|
409
|
+
catch (e) {
|
|
410
|
+
return fail('NOT_FOUND', `读绑定文件失败: ${String(e?.message || e).slice(0, 160)}`, { file });
|
|
411
|
+
}
|
|
412
|
+
const index = readBindingIndex({ dir });
|
|
413
|
+
const verdict = await verifyAddressBinding(rec, { now: Date.now(), ...(index ? { nonces: index.nonces } : {}) });
|
|
414
|
+
if (sub === 'show') {
|
|
415
|
+
const ident = verdict.ok ? payerIdentityOf(rec) : null;
|
|
416
|
+
return {
|
|
417
|
+
envelope: okEnvelope('OK', `绑定 ${rec.id} (验签 ${verdict.ok ? '通过' : '不通过'})`, {
|
|
418
|
+
action: 'show', file, id: rec.id, protocol: rec.protocol,
|
|
419
|
+
statement: rec.statement, statementJson: rec.statement_json,
|
|
420
|
+
didPublicKey: rec.did_public_key, label: rec.label ?? null, labelCovered: verdict.label_covered,
|
|
421
|
+
sigDid: rec.sig_did, sigAddr: rec.sig_addr,
|
|
422
|
+
verified: verdict.ok, verifyReasons: verdict.reasons, checks: verdict.checks,
|
|
423
|
+
nameShort: ident ? ident.name_short : null, didShort: ident ? ident.did_short : null,
|
|
424
|
+
verified_at: rec.verified_at ?? null, created_by: rec.created_by ?? null,
|
|
425
|
+
keyMaterialInOutput: false,
|
|
426
|
+
semantics: '链下登记(可离线验签) —— 不是链上事实',
|
|
427
|
+
}, [], null),
|
|
428
|
+
human: [
|
|
429
|
+
title(head + ' show'),
|
|
430
|
+
line('文件', file),
|
|
431
|
+
line('id', rec.id),
|
|
432
|
+
line('正文 (canonical)', rec.statement_json),
|
|
433
|
+
line('DID', rec.statement.did),
|
|
434
|
+
line('地址', rec.statement.address),
|
|
435
|
+
line('chainId', rec.statement.chainId),
|
|
436
|
+
line('label', rec.label ?? '(无)'),
|
|
437
|
+
line('名字覆盖', verdict.label_covered === null ? '(无 label)' : (verdict.label_covered ? 'label_sig 有效' : 'label_sig 无效')),
|
|
438
|
+
line('sig_did', `${String(rec.sig_did || '').slice(0, 24)}… (ed25519, ${verdict.sig_did_ok ? '通过' : '不通过'})`),
|
|
439
|
+
line('sig_addr', `${String(rec.sig_addr || '').slice(0, 20)}… (eip-191, ${verdict.sig_addr_ok ? '通过' : '不通过'})`),
|
|
440
|
+
line('验签结论', verdict.ok ? '✅ 通过 (两侧)' : `❌ 拒: ${verdict.reasons.join(' | ')}`),
|
|
441
|
+
'',
|
|
442
|
+
` ${hint('名字与短写只在**验签通过**时才算数; 未通过一律不出名字/不出 DID')}`,
|
|
443
|
+
].join('\n'),
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
// verify: 判据逐条列出; 不过 = 非零退出码
|
|
447
|
+
const humanLines = [
|
|
448
|
+
title(head + ' verify'),
|
|
449
|
+
line('文件', file),
|
|
450
|
+
line('id', rec.id),
|
|
451
|
+
` 结论: ${verdict.ok ? '✅ 通过 (双侧签名 + 正文 canonical + 时效/nonce)' : `❌ 拒 (${verdict.reasons.join(' | ')})`}`,
|
|
452
|
+
'',
|
|
453
|
+
...verdict.checks.map((c) => ` ${c.ok ? '✔' : '✘'} ${c.name.padEnd(24)} ${String(c.detail).slice(0, 120)}`),
|
|
454
|
+
];
|
|
455
|
+
const payload = {
|
|
456
|
+
action: 'verify', file, id: rec.id, ok: verdict.ok, reasons: verdict.reasons,
|
|
457
|
+
checks: verdict.checks, sigDidOk: verdict.sig_did_ok, sigAddrOk: verdict.sig_addr_ok,
|
|
458
|
+
labelCovered: verdict.label_covered, address: verdict.address, did: verdict.did,
|
|
459
|
+
nonceChecked: !!index, keyMaterialInOutput: false,
|
|
460
|
+
semantics: '链下登记(可离线验签) —— 不是链上事实',
|
|
461
|
+
};
|
|
462
|
+
return verdict.ok
|
|
463
|
+
? { envelope: okEnvelope('OK', `验签通过: ${rec.id}`, payload, [], null), human: humanLines.join('\n') }
|
|
464
|
+
: {
|
|
465
|
+
envelope: failEnvelope('SIGNATURE_INVALID', `验签不通过: ${rec.id} —— ${verdict.reasons.join(' | ')}`, payload, [], 'needs_human'),
|
|
466
|
+
human: humanLines.join('\n'),
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
if (sub === 'publish') {
|
|
470
|
+
const out = opt(flags, '--out');
|
|
471
|
+
if (!out)
|
|
472
|
+
return fail('INVALID_ARGUMENT', '缺少 --out <path> (显式路径, 不写死目录)');
|
|
473
|
+
const proj = await buildPublicBindingProjection({ dir });
|
|
474
|
+
const target = path.resolve(out);
|
|
475
|
+
try {
|
|
476
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
477
|
+
await fs.writeFile(target, JSON.stringify(proj, null, 2), 'utf-8');
|
|
478
|
+
}
|
|
479
|
+
catch (e) {
|
|
480
|
+
return fail('INTERNAL_ERROR', `写投影失败 (${target}): ${String(e?.message || e).slice(0, 160)}`);
|
|
481
|
+
}
|
|
482
|
+
return {
|
|
483
|
+
envelope: okEnvelope('OK', `已写出对外投影 ${proj.count} 条 → ${target}`, {
|
|
484
|
+
action: 'publish', out: target, count: proj.count, protocol: proj.protocol, kind: proj.kind,
|
|
485
|
+
// 对外面只出短写: 地址只以 sha256 前 16 位出现
|
|
486
|
+
rows: proj.bindings.map((b) => ({ id: b.id, address_hash: b.address_hash, name_short: b.name_short, did_short: b.did_short })),
|
|
487
|
+
keyMaterialInOutput: false,
|
|
488
|
+
semantics: '链下登记(可离线验签) —— 对外只出短写; 地址不出 (只有哈希)',
|
|
489
|
+
}, [], null),
|
|
490
|
+
human: [
|
|
491
|
+
title(head + ' publish'),
|
|
492
|
+
line('写入', target),
|
|
493
|
+
line('条数', proj.count),
|
|
494
|
+
line('协议', proj.protocol),
|
|
495
|
+
...proj.bindings.map((b) => ` ${b.id} ${b.name_short.padEnd(20)} ${b.did_short} ${b.address_hash}`),
|
|
496
|
+
'',
|
|
497
|
+
` ${hint('对外面里没有完整地址、没有完整 DID —— 网关是**本机**按地址匹配的')}`,
|
|
498
|
+
].join('\n'),
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
return fail('INVALID_ARGUMENT', `未知 bindings 子命令: ${sub || '(空)'} (list | show | verify | publish)`);
|
|
502
|
+
}
|
package/dist/cli/ink-app.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* ink-app.tsx — Ink (React for CLI) 渲染入口
|
|
4
4
|
*
|
|
@@ -8,8 +8,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
8
8
|
* ↑/↓ 导航, Tab/Enter 选中, Esc 关闭, 弹出窗打开时 TextInput 让出焦点 (focus=false)
|
|
9
9
|
*/
|
|
10
10
|
import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
|
11
|
+
import { loadInputHistory, appendInputHistory, MEMORY_CAP } from './input-history.js';
|
|
11
12
|
import * as fs from 'fs';
|
|
12
|
-
import { render, Box, Text, useInput, useApp, useStdout } from 'ink';
|
|
13
|
+
import { Static, render, Box, Text, useInput, useApp, useStdout } from 'ink';
|
|
13
14
|
import TextInput from 'ink-text-input';
|
|
14
15
|
import { dispWidth, LOADING_FRAMES as KAOMOJI } from './loading-tui.js';
|
|
15
16
|
import { THEME, fg } from './theme.js';
|
|
@@ -53,6 +54,19 @@ const MIN_HISTORY_LINES = 1;
|
|
|
53
54
|
/** 弹层最多占几行 (给历史区留出至少 1 行消息, 免得弹窗把历史区吃光) */
|
|
54
55
|
const POPUP_MAX_ROWS = 8;
|
|
55
56
|
/** 一屏布局账本 (纯函数 —— 验收门直接喂尺寸复核) */
|
|
57
|
+
/**
|
|
58
|
+
* 启动面板最多能占几行 = 消息窗口真高 (与渲染同一套预算; 面板按它裁, 免得高过窗口).
|
|
59
|
+
* 2026-09-30: 面板高过窗口时 Ink 清不掉上一帧 ⇒ 退化成"追加", 顶框被反复重画.
|
|
60
|
+
*/
|
|
61
|
+
export function bootPanelMaxLines(rows, cols) {
|
|
62
|
+
const b = layoutBudget({ rows: Math.max(8, Math.floor(rows || 24) - 2), cols: Math.floor(cols || 80) });
|
|
63
|
+
return Math.max(4, b.msgH);
|
|
64
|
+
}
|
|
65
|
+
/** 面板区高度 (窗口的 60%; 会话区永远留 >=3 行) —— 给启动面板建盒子时对齐用 */
|
|
66
|
+
export function bootPanelRegionLines(rows, cols) {
|
|
67
|
+
const b = layoutBudget({ rows: Math.max(8, Math.floor(rows || 24) - 2), cols: Math.floor(cols || 80) });
|
|
68
|
+
return Math.max(4, Math.min(Math.floor(b.msgH * 0.6), b.msgH - 3));
|
|
69
|
+
}
|
|
56
70
|
export function layoutBudget(opts) {
|
|
57
71
|
const rows = Math.max(RESERVED_LINES + MIN_HISTORY_LINES, Math.floor(opts.rows || 24));
|
|
58
72
|
const cols = Math.max(20, Math.floor(opts.cols || 80));
|
|
@@ -157,6 +171,14 @@ const InkApp = ({ onPrompt, initialStatus, getStatusUpdate, terminalW, terminalH
|
|
|
157
171
|
}, []);
|
|
158
172
|
// ── 输入历史 (↑/↓ 切换) ───────────────────────────────────────────────────
|
|
159
173
|
const historyRef = useRef([]);
|
|
174
|
+
// 2026-09-30 (leo: 「输入历史有落盘文件夹吗,要实现一下」): 启动时把落盘历史读回内存 —— 重启后 ↑ 还能翻出来。
|
|
175
|
+
// 异步读, 读回来直接替换 ref (空文件/读失败 = 保持空, 不打扰输入)。
|
|
176
|
+
useEffect(() => {
|
|
177
|
+
let alive = true;
|
|
178
|
+
loadInputHistory().then(h => { if (alive && h.length)
|
|
179
|
+
historyRef.current = h; }).catch(() => { });
|
|
180
|
+
return () => { alive = false; };
|
|
181
|
+
}, []);
|
|
160
182
|
const historyIdxRef = useRef(-1); // -1 = 正在编辑新草稿
|
|
161
183
|
const draftRef = useRef('');
|
|
162
184
|
// 加载当前 mention 的候选 (agent/command 缓存一次; file 每次打开重扫)
|
|
@@ -405,18 +427,48 @@ const InkApp = ({ onPrompt, initialStatus, getStatusUpdate, terminalW, terminalH
|
|
|
405
427
|
lastSubmitRef.current = { t: now, v: trimmed };
|
|
406
428
|
if (!trimmed)
|
|
407
429
|
return;
|
|
408
|
-
// 入历史 (
|
|
430
|
+
// 入历史 (去重最近一条) + **落盘** (~/.bolloon/history/input-<渠道>.jsonl, 0600)
|
|
431
|
+
// 秘密形态的输入 (私钥/助记词) 由 appendInputHistory 拦下不写盘 —— 输入历史是纯文本, 写进去就是事故。
|
|
409
432
|
const hist = historyRef.current;
|
|
410
433
|
if (hist[hist.length - 1] !== trimmed)
|
|
411
434
|
hist.push(trimmed);
|
|
412
|
-
if (hist.length >
|
|
435
|
+
if (hist.length > MEMORY_CAP)
|
|
413
436
|
hist.shift();
|
|
437
|
+
void appendInputHistory(trimmed);
|
|
414
438
|
historyIdxRef.current = -1;
|
|
415
439
|
draftRef.current = '';
|
|
416
440
|
setInput('');
|
|
417
441
|
// 用户消息由 processInput 统一通过 appendLine(renderUserMessage) 显示
|
|
418
442
|
onPrompt(trimmed);
|
|
419
443
|
}, [onPrompt]);
|
|
444
|
+
/**
|
|
445
|
+
* 输入历史: 上一/下一条。
|
|
446
|
+
* 2026-09-30 (leo: 「历史的上下切换遇到 /session 等指令就无法切换了」) —— 抽出来是因为
|
|
447
|
+
* 历史里一旦出现 `/xxx`, 恢复出来的输入会**打开命令补全弹窗**, 而弹窗把 ↑/↓ 当"移动候选"吃掉
|
|
448
|
+
* ⇒ 历史再也切不动。现在: **空输入** 或 **已在历史态** 时 ↑/↓ 一律归历史 (见下面优先块)。
|
|
449
|
+
*/
|
|
450
|
+
const histUp = useCallback(() => {
|
|
451
|
+
const hist = historyRef.current;
|
|
452
|
+
if (hist.length === 0)
|
|
453
|
+
return;
|
|
454
|
+
if (historyIdxRef.current === -1)
|
|
455
|
+
draftRef.current = inputRef.current;
|
|
456
|
+
if (historyIdxRef.current < hist.length - 1) {
|
|
457
|
+
historyIdxRef.current += 1;
|
|
458
|
+
setInput(hist[hist.length - 1 - historyIdxRef.current]);
|
|
459
|
+
setTiKey(k => k + 1);
|
|
460
|
+
}
|
|
461
|
+
}, []);
|
|
462
|
+
const histDown = useCallback(() => {
|
|
463
|
+
if (historyIdxRef.current === -1)
|
|
464
|
+
return;
|
|
465
|
+
historyIdxRef.current -= 1;
|
|
466
|
+
if (historyIdxRef.current === -1)
|
|
467
|
+
setInput(draftRef.current);
|
|
468
|
+
else
|
|
469
|
+
setInput(historyRef.current[historyRef.current.length - 1 - historyIdxRef.current]);
|
|
470
|
+
setTiKey(k => k + 1);
|
|
471
|
+
}, []);
|
|
420
472
|
useInput((_input, key) => {
|
|
421
473
|
// 退出请求: 通知 startCLI resolve → 走清理 → process.exit (带兜底)
|
|
422
474
|
const requestExit = () => {
|
|
@@ -429,6 +481,16 @@ const InkApp = ({ onPrompt, initialStatus, getStatusUpdate, terminalW, terminalH
|
|
|
429
481
|
requestExit();
|
|
430
482
|
return;
|
|
431
483
|
}
|
|
484
|
+
// ── 输入历史优先 (比补全弹窗更优先) ──
|
|
485
|
+
// 规则: 空输入, 或已经在历史态 (historyIdx !== -1) ⇒ ↑/↓ 归历史, 弹窗/选择器都让路。
|
|
486
|
+
// 只有"用户自己敲了内容且不在历史态"时, 补全弹窗才拥有 ↑/↓。
|
|
487
|
+
if ((key.upArrow || key.downArrow) && (historyIdxRef.current !== -1 || !inputRef.current.trim())) {
|
|
488
|
+
if (key.upArrow)
|
|
489
|
+
histUp();
|
|
490
|
+
else
|
|
491
|
+
histDown();
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
432
494
|
// ── 程序化选择器: 全键接管 (↑↓ 选择, Enter 确认, Esc 关闭) ──
|
|
433
495
|
if (picker) {
|
|
434
496
|
const itemsArg = picker.items;
|
|
@@ -533,7 +595,7 @@ const InkApp = ({ onPrompt, initialStatus, getStatusUpdate, terminalW, terminalH
|
|
|
533
595
|
}
|
|
534
596
|
// ── 正常模式 ──
|
|
535
597
|
// #7 输入层: 滚动键走数据化 keymap (Ctrl+U/D / PgUp/PgDn / Home-End / Alt+↑↓ / Ctrl+Home-End)
|
|
536
|
-
if (totalLines > availH) {
|
|
598
|
+
if (false && totalLines > availH) { // 无限高度: 不再有 app 内虚拟滚动 (PgUp 归终端)
|
|
537
599
|
const pg = Math.max(6, availH - 2);
|
|
538
600
|
const maxT = Math.max(0, totalLines - availH);
|
|
539
601
|
const act = resolveNormalKey(key, { scrollable: true, input: _input });
|
|
@@ -563,27 +625,11 @@ const InkApp = ({ onPrompt, initialStatus, getStatusUpdate, terminalW, terminalH
|
|
|
563
625
|
}
|
|
564
626
|
// ↑/↓ 切换输入历史 (TextInput 本身忽略 up/down, 无冲突)
|
|
565
627
|
if (key.upArrow) {
|
|
566
|
-
|
|
567
|
-
if (hist.length === 0)
|
|
568
|
-
return;
|
|
569
|
-
if (historyIdxRef.current === -1)
|
|
570
|
-
draftRef.current = input;
|
|
571
|
-
if (historyIdxRef.current < hist.length - 1) {
|
|
572
|
-
historyIdxRef.current += 1;
|
|
573
|
-
setInput(hist[hist.length - 1 - historyIdxRef.current]);
|
|
574
|
-
setTiKey(k => k + 1);
|
|
575
|
-
}
|
|
628
|
+
histUp();
|
|
576
629
|
return;
|
|
577
630
|
}
|
|
578
631
|
if (key.downArrow) {
|
|
579
|
-
|
|
580
|
-
return;
|
|
581
|
-
historyIdxRef.current -= 1;
|
|
582
|
-
if (historyIdxRef.current === -1)
|
|
583
|
-
setInput(draftRef.current);
|
|
584
|
-
else
|
|
585
|
-
setInput(historyRef.current[historyRef.current.length - 1 - historyIdxRef.current]);
|
|
586
|
-
setTiKey(k => k + 1);
|
|
632
|
+
histDown();
|
|
587
633
|
return;
|
|
588
634
|
}
|
|
589
635
|
// 双击 Esc 退出当前进程: 第一击提示, 500ms 内第二击退出
|
|
@@ -672,7 +718,11 @@ const InkApp = ({ onPrompt, initialStatus, getStatusUpdate, terminalW, terminalH
|
|
|
672
718
|
const sticky = stickRef.current;
|
|
673
719
|
const popupOpenNow = !!(popupOpen || picker);
|
|
674
720
|
const budget = layoutBudget({
|
|
675
|
-
|
|
721
|
+
// 2026-09-30 (leo 报「顶部重绘」+ 真机日志坐实): 帧高 = 终端行数 - 2.
|
|
722
|
+
// 整帧 == 终端高时, 每次 flush (状态栏每秒一 tick) 都把最上一行滚进 scrollback
|
|
723
|
+
// ⇒ 面板被一遍遍重复留在屏上 (实测一次运行重画 9 次). 留 2 行余量 (终端可视行数
|
|
724
|
+
// 常比报告值矮 1~2 行) 即根治.
|
|
725
|
+
rows: Math.max(8, termSize.h - 2), cols: W,
|
|
676
726
|
popupRows: popupOpenNow ? POPUP_MAX_ROWS : 0,
|
|
677
727
|
});
|
|
678
728
|
const msgH = budget.msgH; // 消息窗口高度 (弹层占的那几行已经扣掉)
|
|
@@ -694,8 +744,10 @@ const InkApp = ({ onPrompt, initialStatus, getStatusUpdate, terminalW, terminalH
|
|
|
694
744
|
// 严格钳制: 端界不越过 msgH, 保证 transcript 内容行数 ≤ 分区高度 (互不覆盖)
|
|
695
745
|
while (end + 1 < msgs.length && cumulative[end + 1] <= top + msgH)
|
|
696
746
|
end++;
|
|
697
|
-
|
|
698
|
-
|
|
747
|
+
void start;
|
|
748
|
+
void end;
|
|
749
|
+
const visible = msgs; // 2026-09-30 (leo: 「我要无限高度」): 全部内容交给 <Static>, 不再切片
|
|
750
|
+
const scrolledOut = false; // 虚拟滚动退场 —— 滚动交给终端 scrollback
|
|
699
751
|
// 测试/诊断探针: 把**真实用到的**布局与滚动状态写一份 JSONL (验收门按它做精确断言;
|
|
700
752
|
// 不设 BOLLOON_TUI_PROBE 时零开销 —— 只在渲染后追加一行)。
|
|
701
753
|
// 注: 走静态 import —— dist 是 ESM, 这里 `require()` 不存在 (踩过一次: 探针静默不写 + 树停更)。
|
|
@@ -715,7 +767,7 @@ const InkApp = ({ onPrompt, initialStatus, getStatusUpdate, terminalW, terminalH
|
|
|
715
767
|
}
|
|
716
768
|
catch { /* 探针写不进去绝不影响界面 */ }
|
|
717
769
|
});
|
|
718
|
-
return (_jsxs(
|
|
770
|
+
return (_jsxs(_Fragment, { children: [_jsx(Static, { items: msgs, children: (m, i) => _jsx(Text, { children: m }, `m${i}`) }), _jsxs(Box, { flexDirection: "column", width: budget.cols, children: [hasRails && (_jsx(Box, { flexDirection: "column", children: railNames.map((n) => _jsx(Text, { color: THEME.muted, children: `${n}: ${String(widgets[n] ?? '').replace(/\n/g, ' ')}` }, n)) })), popupOpenNow && tabState && (_jsx(MentionPopup, { title: POPUP_TITLE_TAB, items: filtered, sel: safeSel, width: W, maxRows: budget.popupRows })), popupOpenNow && !tabState && mention && (_jsx(MentionPopup, { title: popupTitle, items: filtered, sel: safeSel, width: W, loading: loadingFiles, maxRows: budget.popupRows })), popupOpenNow && !tabState && !mention && picker && (_jsx(MentionPopup, { title: picker.title, items: picker.items, sel: Math.min(picker.sel, picker.items.length - 1), width: W, maxRows: budget.popupRows })), _jsx(Box, { height: 1, width: budget.cols, overflow: "hidden", children: transient ? (_jsx(Text, { children: transient })) : thinking ? (_jsxs(Text, { color: "yellow", children: [KAOMOJI[thinkingIdx.current], " \u601D\u8003\u4E2D..."] })) : (_jsx(Text, { color: THEME.muted, children: '· 回车发送 · ↑↓ 历史 · PgUp 回看 · Esc 双击退出' })) }), _jsx(Box, { children: _jsx(Text, { bold: true, color: THEME.accent, children: '─'.repeat(Math.max(10, W - 1)) }) }), _jsx(Box, { height: 1, width: budget.cols, overflow: "hidden", children: _jsx(Text, { children: status }) }), _jsx(Box, { children: _jsx(Text, { bold: true, color: THEME.accent, children: '─'.repeat(Math.max(10, W - 1)) }) }), _jsxs(Box, { width: budget.cols, height: 1, overflow: "hidden", children: [_jsx(Text, { bold: true, color: THEME.accent, children: "\u276F " }), _jsx(Box, { width: Math.max(10, budget.cols - 2), height: 1, overflow: "hidden", flexShrink: 0, children: _jsx(TextInput, { value: input, onChange: setInput, onSubmit: onSubmit, focus: !popupOpenNow, placeholder: COMPOSER_PLACEHOLDER }, tiKey) })] }), _jsx(Box, { children: _jsx(Text, { bold: true, color: THEME.accent, children: '─'.repeat(Math.max(10, W - 1)) }) })] })] }));
|
|
719
771
|
};
|
|
720
772
|
export { InkApp };
|
|
721
773
|
// ─── 启动 ────────────────────────────────────────────────────────────────────
|