@wacry/codeks-bridge 0.0.0-stage → 0.0.3

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 (104) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +13 -0
  3. package/README.md +40 -2
  4. package/catalog/README.md +63 -0
  5. package/catalog/claude-opus-5-5.json +194 -0
  6. package/catalog/claude-sonnet-5-5.json +194 -0
  7. package/catalog/deepseek-flash.json +203 -0
  8. package/catalog/gpt-5.6-sol.json +152 -0
  9. package/catalog/gpt-6-astra.json +184 -0
  10. package/catalog/gpt-6-luna.json +178 -0
  11. package/catalog/gpt-6-sol.json +182 -0
  12. package/catalog/gpt-6.1-sol.json +187 -0
  13. package/dist/adapter-options.d.ts +5 -0
  14. package/dist/adapter-options.js +41 -0
  15. package/dist/auth.d.ts +2 -0
  16. package/dist/auth.js +42 -0
  17. package/dist/backends/anthropic-tools.d.ts +13 -0
  18. package/dist/backends/anthropic-tools.js +41 -0
  19. package/dist/backends/anthropic.d.ts +9 -0
  20. package/dist/backends/anthropic.js +356 -0
  21. package/dist/backends/chat.d.ts +16 -0
  22. package/dist/backends/chat.js +284 -0
  23. package/dist/backends/deepseek.d.ts +6 -0
  24. package/dist/backends/deepseek.js +37 -0
  25. package/dist/backends/responses.d.ts +5 -0
  26. package/dist/backends/responses.js +227 -0
  27. package/dist/carrier.d.ts +22 -0
  28. package/dist/carrier.js +61 -0
  29. package/dist/catalog.d.ts +8 -0
  30. package/dist/catalog.js +40 -0
  31. package/dist/cli.d.ts +2 -0
  32. package/dist/cli.js +64 -0
  33. package/dist/compaction-handoff.d.ts +13 -0
  34. package/dist/compaction-handoff.js +126 -0
  35. package/dist/compaction-history.d.ts +7 -0
  36. package/dist/compaction-history.js +20 -0
  37. package/dist/compaction.d.ts +20 -0
  38. package/dist/compaction.js +134 -0
  39. package/dist/config.d.ts +6 -0
  40. package/dist/config.js +170 -0
  41. package/dist/errors.d.ts +43 -0
  42. package/dist/errors.js +141 -0
  43. package/dist/headers.d.ts +6 -0
  44. package/dist/headers.js +39 -0
  45. package/dist/history.d.ts +7 -0
  46. package/dist/history.js +85 -0
  47. package/dist/ids.d.ts +1 -0
  48. package/dist/ids.js +4 -0
  49. package/dist/index.d.ts +5 -0
  50. package/dist/index.js +4 -0
  51. package/dist/inspect/capture.d.ts +119 -0
  52. package/dist/inspect/capture.js +370 -0
  53. package/dist/inspect/http.d.ts +7 -0
  54. package/dist/inspect/http.js +149 -0
  55. package/dist/inspect/view.d.ts +239 -0
  56. package/dist/inspect/view.js +388 -0
  57. package/dist/model-catalog.d.ts +18 -0
  58. package/dist/model-catalog.js +16 -0
  59. package/dist/profiles.d.ts +6 -0
  60. package/dist/profiles.js +63 -0
  61. package/dist/prompts.d.ts +5 -0
  62. package/dist/prompts.js +118 -0
  63. package/dist/server.d.ts +4 -0
  64. package/dist/server.js +232 -0
  65. package/dist/sse.d.ts +12 -0
  66. package/dist/sse.js +93 -0
  67. package/dist/tool-history.d.ts +4 -0
  68. package/dist/tool-history.js +73 -0
  69. package/dist/tools.d.ts +35 -0
  70. package/dist/tools.js +193 -0
  71. package/dist/types.d.ts +125 -0
  72. package/dist/types.js +1 -0
  73. package/dist/upstream.d.ts +3 -0
  74. package/dist/upstream.js +89 -0
  75. package/dist/writer.d.ts +95 -0
  76. package/dist/writer.js +430 -0
  77. package/docs/backends-anthropic.md +88 -0
  78. package/docs/backends-deepseek-chat.md +75 -0
  79. package/docs/backends-responses.md +33 -0
  80. package/docs/compaction.md +180 -0
  81. package/docs/config.md +271 -0
  82. package/docs/design.md +97 -0
  83. package/docs/inspect.md +148 -0
  84. package/docs/reasoning.md +72 -0
  85. package/docs/releasing.md +50 -0
  86. package/docs/responses-stream.md +90 -0
  87. package/docs/testing.md +177 -0
  88. package/docs/tools.md +68 -0
  89. package/examples/config.json +35 -0
  90. package/examples/gpt-claude.json +27 -0
  91. package/package.json +46 -4
  92. package/ui/api.js +64 -0
  93. package/ui/app.css +379 -0
  94. package/ui/app.js +489 -0
  95. package/ui/detail.js +713 -0
  96. package/ui/diff.js +281 -0
  97. package/ui/dom.js +258 -0
  98. package/ui/events.js +376 -0
  99. package/ui/favicon.svg +1 -0
  100. package/ui/index.html +58 -0
  101. package/ui/labels.js +106 -0
  102. package/ui/list.js +180 -0
  103. package/ui/structure.js +285 -0
  104. package/ui/tree.js +158 -0
package/ui/diff.js ADDED
@@ -0,0 +1,281 @@
1
+ // Structural JSON diff: objects by key, arrays by LCS on exact item identity
2
+ // (common prefix/suffix trimmed first), replaced runs paired by type/role.
3
+ import { h, btn, copyText, fmtInt, oneLine, strBlock, put } from './dom.js';
4
+ import { jsonTree, prim, preview } from './tree.js';
5
+
6
+ const LCS_CELLS = 6e6;
7
+ const PAGE = 300;
8
+
9
+ const kindOf = (v) => (Array.isArray(v) ? 'arr' : v === null ? 'null' : typeof v === 'object' ? 'obj' : typeof v);
10
+ const sig = (v) => (v && typeof v === 'object' && !Array.isArray(v) ? `o|${v.type ?? ''}|${v.role ?? ''}` : kindOf(v));
11
+
12
+ /** Diff node: {t:'eq'|'add'|'del'|'mod'|'obj'|'arr', ...}; n = number of changes below. */
13
+ export function diff(a, b) {
14
+ if (a === b) return { t: 'eq', v: a, n: 0 };
15
+ const ka = kindOf(a);
16
+ if (ka !== kindOf(b) || (ka !== 'obj' && ka !== 'arr')) return { t: 'mod', a, b, n: 1 };
17
+ return ka === 'obj' ? diffObj(a, b) : diffArr(a, b);
18
+ }
19
+
20
+ function same(a, b) {
21
+ if (a === b) return true;
22
+ if (typeof a !== 'object' || typeof b !== 'object' || !a || !b) return false;
23
+ return JSON.stringify(a) === JSON.stringify(b);
24
+ }
25
+
26
+ function diffObj(a, b) {
27
+ const entries = [];
28
+ let n = 0;
29
+ for (const k of Object.keys(a)) {
30
+ if (!Object.hasOwn(b, k)) { entries.push([k, { t: 'del', v: a[k], n: 1 }]); n++; continue; }
31
+ const d = same(a[k], b[k]) ? { t: 'eq', v: a[k], n: 0 } : diff(a[k], b[k]);
32
+ entries.push([k, d]);
33
+ n += d.n;
34
+ }
35
+ for (const k of Object.keys(b)) if (!Object.hasOwn(a, k)) { entries.push([k, { t: 'add', v: b[k], n: 1 }]); n++; }
36
+ return { t: 'obj', entries, n };
37
+ }
38
+
39
+ function diffArr(a, b) {
40
+ const ids = new Map();
41
+ const id = (v) => {
42
+ const s = typeof v === 'string' ? `s${v}` : JSON.stringify(v) ?? 'u';
43
+ let k = ids.get(s);
44
+ if (k === undefined) ids.set(s, (k = ids.size));
45
+ return k;
46
+ };
47
+ const A = a.map(id);
48
+ const B = b.map(id);
49
+ let pre = 0;
50
+ while (pre < A.length && pre < B.length && A[pre] === B[pre]) pre++;
51
+ let suf = 0;
52
+ while (suf < A.length - pre && suf < B.length - pre && A[A.length - 1 - suf] === B[B.length - 1 - suf]) suf++;
53
+ const ops = [];
54
+ for (let i = 0; i < pre; i++) ops.push({ t: 'eq', ai: i, bi: i, v: a[i] });
55
+ const raw = lcsOps(A.slice(pre, A.length - suf), B.slice(pre, B.length - suf), pre);
56
+ let n = 0;
57
+ // Pair replaced runs (del... add...) by type/role so changed items show nested diffs.
58
+ for (let k = 0; k < raw.length;) {
59
+ if (raw[k].t === 'eq') { ops.push({ ...raw[k], v: a[raw[k].ai] }); k++; continue; }
60
+ const dels = [];
61
+ const adds = [];
62
+ while (k < raw.length && raw[k].t !== 'eq') { (raw[k].t === 'del' ? dels : adds).push(raw[k]); k++; }
63
+ let i = 0;
64
+ let j = 0;
65
+ while (i < dels.length || j < adds.length) {
66
+ const da = dels[i];
67
+ const ab = adds[j];
68
+ if (da && ab && sig(a[da.ai]) === sig(b[ab.bi])) {
69
+ const d = diff(a[da.ai], b[ab.bi]);
70
+ ops.push({ t: 'chg', ai: da.ai, bi: ab.bi, d });
71
+ n += d.n;
72
+ i++; j++;
73
+ } else if (ab && (!da || adds.slice(j).some((x) => sig(b[x.bi]) === sig(a[da.ai])))) {
74
+ ops.push({ t: 'add', bi: ab.bi, v: b[ab.bi] }); n++; j++;
75
+ } else {
76
+ ops.push({ t: 'del', ai: da.ai, v: a[da.ai] }); n++; i++;
77
+ }
78
+ }
79
+ }
80
+ for (let i = 0; i < suf; i++) ops.push({ t: 'eq', ai: A.length - suf + i, bi: B.length - suf + i, v: a[A.length - suf + i] });
81
+ return { t: 'arr', ops, n, la: a.length, lb: b.length };
82
+ }
83
+
84
+ function lcsOps(A, B, off) {
85
+ const n = A.length;
86
+ const m = B.length;
87
+ const ops = [];
88
+ if (!n || !m || n * m > LCS_CELLS) {
89
+ // Too large for LCS: positional fallback keeps the view responsive.
90
+ for (let i = 0; i < n; i++) ops.push({ t: 'del', ai: off + i });
91
+ for (let j = 0; j < m; j++) ops.push({ t: 'add', bi: off + j });
92
+ return ops;
93
+ }
94
+ const w = m + 1;
95
+ const T = n * m < 4e6 && n < 65535 && m < 65535 ? new Uint16Array((n + 1) * w) : new Uint32Array((n + 1) * w);
96
+ for (let i = n - 1; i >= 0; i--) {
97
+ for (let j = m - 1; j >= 0; j--) {
98
+ T[i * w + j] = A[i] === B[j] ? T[(i + 1) * w + j + 1] + 1 : Math.max(T[(i + 1) * w + j], T[i * w + j + 1]);
99
+ }
100
+ }
101
+ let i = 0;
102
+ let j = 0;
103
+ while (i < n && j < m) {
104
+ if (A[i] === B[j]) { ops.push({ t: 'eq', ai: off + i, bi: off + j }); i++; j++; }
105
+ else if (T[(i + 1) * w + j] >= T[i * w + j + 1]) { ops.push({ t: 'del', ai: off + i }); i++; }
106
+ else { ops.push({ t: 'add', bi: off + j }); j++; }
107
+ }
108
+ for (; i < n; i++) ops.push({ t: 'del', ai: off + i });
109
+ for (; j < m; j++) ops.push({ t: 'add', bi: off + j });
110
+ return ops;
111
+ }
112
+
113
+ // ---------- rendering ----------
114
+
115
+ export function diffView(a, b, opts = {}) {
116
+ const t0 = performance.now();
117
+ const d = diff(a, b);
118
+ const ms = performance.now() - t0;
119
+ const stats = { add: 0, del: 0, mod: 0 };
120
+ count(d, stats);
121
+ const root = h('div', { class: 'diff' },
122
+ h('div', { class: 'diff-stats' },
123
+ d.n ? [h('span', { class: 'chip ok' }, `+${fmtInt(stats.add)} 新增`), h('span', { class: 'chip bad' }, `−${fmtInt(stats.del)} 删除`), h('span', { class: 'chip warn' }, `~${fmtInt(stats.mod)} 修改`)] : h('span', { class: 'chip ok' }, '完全相同'),
124
+ h('span', { class: 'dim' }, `比较耗时 ${ms.toFixed(0)} ms`)),
125
+ h('div', { class: 'diff-tree' }, node(opts.label ?? null, d, 0)));
126
+ return root;
127
+ }
128
+
129
+ function count(d, s) {
130
+ if (d.t === 'add') s.add++;
131
+ else if (d.t === 'del') s.del++;
132
+ else if (d.t === 'mod') s.mod++;
133
+ else if (d.t === 'obj') for (const [, x] of d.entries) count(x, s);
134
+ else if (d.t === 'arr') for (const op of d.ops) { if (op.t === 'add') s.add++; else if (op.t === 'del') s.del++; else if (op.t === 'chg') count(op.d, s); }
135
+ }
136
+
137
+ const keyEl = (k) => (k == null ? null : h('span', { class: typeof k === 'number' || /^[ab]\[/.test(k) ? 'tn-idx' : 'tn-key' }, String(k)));
138
+ const sepEl = (k) => (k == null ? null : h('span', { class: 'tn-sep' }, ': '));
139
+
140
+ function valueEl(v) {
141
+ if (v && typeof v === 'object') {
142
+ return h('span', null, h('span', { class: 'tn-sum' }, Array.isArray(v) ? `[${v.length}]` : `{${Object.keys(v).length}}`), h('span', { class: 'tn-pv' }, preview(v)));
143
+ }
144
+ if (typeof v === 'string' && v.length > 200) return h('span', { class: 'str' }, JSON.stringify(oneLine(v, 200)).slice(0, -1) + '…"', h('span', { class: 'dim' }, ` (${fmtInt(v.length)} 字符)`));
145
+ return prim(v);
146
+ }
147
+
148
+ /** A row whose value can be expanded into a plain JSON tree. */
149
+ function leafRow(cls, sign, k, v) {
150
+ const wrap = h('div', { class: `dn ${cls}` });
151
+ const tog = h('span', { class: 'tn-tog' }, v && typeof v === 'object' ? '▸' : '');
152
+ const row = h('div', { class: 'dn-row' }, h('span', { class: 'sign' }, sign), tog, keyEl(k), sepEl(k), valueEl(v));
153
+ put(wrap, row);
154
+ if ((v && typeof v === 'object') || (typeof v === 'string' && v.length > 200)) {
155
+ let body = null;
156
+ row.classList.add('tn-click');
157
+ row.addEventListener('click', (e) => {
158
+ if (e.target.closest('button')) return;
159
+ if (body) { body.remove(); body = null; tog.textContent = '▸'; return; }
160
+ body = h('div', { class: 'dn-body' }, typeof v === 'string' ? strBlock(v) : jsonTree(v, { auto: 1 }));
161
+ put(wrap, body);
162
+ tog.textContent = '▾';
163
+ });
164
+ }
165
+ return wrap;
166
+ }
167
+
168
+ function node(k, d, depth) {
169
+ if (d.t === 'eq') return leafRow('eq', ' ', k, d.v);
170
+ if (d.t === 'add') return leafRow('add', '+', k, d.v);
171
+ if (d.t === 'del') return leafRow('del', '−', k, d.v);
172
+ if (d.t === 'mod') return modRow(k, d.a, d.b);
173
+ const wrap = h('div', { class: 'dn ch' });
174
+ const tog = h('span', { class: 'tn-tog' }, '▾');
175
+ const label = d.t === 'arr' ? `[${d.la} → ${d.lb}]` : '{…}';
176
+ const row = h('div', { class: 'dn-row tn-click' }, h('span', { class: 'sign' }, '~'), tog, keyEl(k), sepEl(k),
177
+ h('span', { class: 'tn-sum' }, label), h('span', { class: 'chip warn' }, `${fmtInt(d.n)} 处变化`));
178
+ const kids = h('div', { class: 'tn-kids' });
179
+ put(wrap, row, kids);
180
+ const units = d.t === 'obj' ? d.entries.map(([key, x]) => ({ eq: x.t === 'eq', key, x })) : d.ops.map((op) => ({ eq: op.t === 'eq', op }));
181
+ fillUnits(kids, units, d.t, depth, 0);
182
+ row.addEventListener('click', () => {
183
+ const open = !kids.hidden;
184
+ kids.hidden = open;
185
+ tog.textContent = open ? '▸' : '▾';
186
+ });
187
+ return wrap;
188
+ }
189
+
190
+ function unitEl(u, kind, depth) {
191
+ if (kind === 'obj') return node(u.key, u.x, depth + 1);
192
+ const op = u.op;
193
+ if (op.t === 'eq') return leafRow('eq', ' ', op.ai === op.bi ? op.ai : `a[${op.ai}]→b[${op.bi}]`, op.v);
194
+ if (op.t === 'add') return leafRow('add', '+', `b[${op.bi}]`, op.v);
195
+ if (op.t === 'del') return leafRow('del', '−', `a[${op.ai}]`, op.v);
196
+ return node(op.ai === op.bi ? op.ai : `a[${op.ai}]→b[${op.bi}]`, op.d, depth + 1);
197
+ }
198
+
199
+ /** Renders units, collapsing unchanged runs into a single clickable row. */
200
+ function fillUnits(box, units, kind, depth, start) {
201
+ let rendered = 0;
202
+ let k = start;
203
+ while (k < units.length) {
204
+ if (rendered >= PAGE) {
205
+ const rest = units.length - k;
206
+ const at = k;
207
+ const more = btn(`显示其余 ${fmtInt(rest)} 项`, () => { more.remove(); fillUnits(box, units, kind, depth, at); }, 'more');
208
+ put(box, more);
209
+ return;
210
+ }
211
+ if (units[k].eq) {
212
+ let e = k;
213
+ while (e < units.length && units[e].eq) e++;
214
+ const run = units.slice(k, e);
215
+ if (run.length <= 2) for (const u of run) put(box, unitEl(u, kind, depth));
216
+ else put(box, eqRun(run, kind, depth));
217
+ rendered++;
218
+ k = e;
219
+ continue;
220
+ }
221
+ put(box, unitEl(units[k], kind, depth));
222
+ rendered++;
223
+ k++;
224
+ }
225
+ }
226
+
227
+ function eqRun(run, kind, depth) {
228
+ const label = kind === 'obj'
229
+ ? `… ${run.length} 个键相同:${run.slice(0, 6).map((u) => u.key).join(', ')}${run.length > 6 ? ', …' : ''}`
230
+ : `… ${run.length} 项相同(a[${run[0].op.ai}–${run[run.length - 1].op.ai}]${run[0].op.ai !== run[0].op.bi ? ` = b[${run[0].op.bi}–${run[run.length - 1].op.bi}]` : ''})`;
231
+ const box = h('div', { class: 'dn eqrun' });
232
+ const row = h('div', { class: 'dn-row tn-click', title: '点击展开相同部分' }, h('span', { class: 'sign' }, ' '), h('span', { class: 'tn-tog' }, '▸'), h('span', { class: 'dim' }, label));
233
+ put(box, row);
234
+ row.addEventListener('click', () => {
235
+ const kids = h('div', { class: 'tn-kids' });
236
+ fillUnits(kids, run.map((u) => ({ ...u, eq: false })), kind, depth, 0);
237
+ box.replaceWith(kids);
238
+ });
239
+ return box;
240
+ }
241
+
242
+ function modRow(k, a, b) {
243
+ const wrap = h('div', { class: 'dn mod' });
244
+ if (typeof a === 'string' && typeof b === 'string' && (a.length > 160 || b.length > 160 || a.includes('\n') || b.includes('\n'))) {
245
+ put(wrap, h('div', { class: 'dn-row' }, h('span', { class: 'sign' }, '~'), h('span', { class: 'tn-tog' }), keyEl(k), sepEl(k),
246
+ h('span', { class: 'dim' }, `字符串 ${fmtInt(a.length)} → ${fmtInt(b.length)} 字符`),
247
+ btn('复制 A', () => copyText(a)), btn('复制 B', () => copyText(b))), strDiff(a, b));
248
+ return wrap;
249
+ }
250
+ put(wrap, h('div', { class: 'dn-row' }, h('span', { class: 'sign' }, '~'), h('span', { class: 'tn-tog' }), keyEl(k), sepEl(k),
251
+ h('span', { class: 'old' }, valueEl(a)), h('span', { class: 'arrow' }, ' → '), h('span', { class: 'new' }, valueEl(b))));
252
+ return wrap;
253
+ }
254
+
255
+ /** Shows the changed window of two strings with shared prefix/suffix context. */
256
+ function strDiff(a, b) {
257
+ const n = Math.min(a.length, b.length);
258
+ let p = 0;
259
+ while (p < n && a.charCodeAt(p) === b.charCodeAt(p)) p++;
260
+ let s = 0;
261
+ while (s < n - p && a.charCodeAt(a.length - 1 - s) === b.charCodeAt(b.length - 1 - s)) s++;
262
+ const C = 160;
263
+ const box = h('div', { class: 'sdiff' });
264
+ put(box, h('div', { class: 'dim' }, `共同前缀 ${fmtInt(p)} 字符 · 共同后缀 ${fmtInt(s)} 字符`));
265
+ const pre = h('pre', { class: 'sv-text' });
266
+ if (p > C) put(pre, h('span', { class: 'dim' }, `…[${fmtInt(p - C)} 字符相同]…`));
267
+ put(pre, h('span', { class: 'ctx' }, a.slice(Math.max(0, p - C), p)));
268
+ const mid = (t, cls) => {
269
+ const el = h('span', { class: cls });
270
+ el.textContent = t.length > 3000 ? `${t.slice(0, 1500)} … 省略 ${fmtInt(t.length - 3000)} 字符 … ${t.slice(-1500)}` : t;
271
+ return el;
272
+ };
273
+ const ma = a.slice(p, a.length - s);
274
+ const mb = b.slice(p, b.length - s);
275
+ if (ma) put(pre, mid(ma, 'old'));
276
+ if (mb) put(pre, mid(mb, 'new'));
277
+ put(pre, h('span', { class: 'ctx' }, a.slice(a.length - s, a.length - s + C)));
278
+ if (s > C) put(pre, h('span', { class: 'dim' }, `…[${fmtInt(s - C)} 字符相同]…`));
279
+ put(box, pre);
280
+ return box;
281
+ }
package/ui/dom.js ADDED
@@ -0,0 +1,258 @@
1
+ // Shared DOM, formatting and value-rendering helpers for the inspector UI.
2
+ // All captured content is untrusted: build nodes with createElement/textContent only.
3
+
4
+ export function h(tag, props, ...kids) {
5
+ const el = document.createElement(tag);
6
+ if (props) {
7
+ for (const [k, v] of Object.entries(props)) {
8
+ if (v == null || v === false) continue;
9
+ if (k === 'class') el.className = v;
10
+ else if (k === 'text') el.textContent = v;
11
+ else if (k.startsWith('on')) el.addEventListener(k.slice(2), v);
12
+ else if (k === 'style') Object.assign(el.style, v);
13
+ else if (k === 'dataset') Object.assign(el.dataset, v);
14
+ else if (typeof v !== 'string' && k in el) el[k] = v;
15
+ else el.setAttribute(k, v === true ? '' : String(v));
16
+ }
17
+ }
18
+ add(el, kids);
19
+ return el;
20
+ }
21
+
22
+ function add(el, kids) {
23
+ for (const k of kids) {
24
+ if (k == null || k === false) continue;
25
+ if (Array.isArray(k)) add(el, k);
26
+ else el.append(k instanceof Node ? k : String(k));
27
+ }
28
+ }
29
+
30
+ /** Appends kids to el, skipping null/false and flattening arrays like h(). */
31
+ export function put(el, ...kids) { add(el, kids); return el; }
32
+
33
+ /** Replaces the children of el, skipping null/false and flattening arrays like h(). */
34
+ export function fill(el, ...kids) {
35
+ el.replaceChildren();
36
+ add(el, kids);
37
+ return el;
38
+ }
39
+
40
+ export const btn = (label, onclick, cls = '', title) =>
41
+ h('button', { type: 'button', class: `btn ${cls}`.trim(), title, onclick: (e) => { e.stopPropagation(); onclick(e); } }, label);
42
+
43
+ // ---------- formatting ----------
44
+
45
+ const pad = (n, w = 2) => String(n).padStart(w, '0');
46
+
47
+ export function fmtTime(ms) {
48
+ if (ms == null) return '–';
49
+ const d = new Date(ms);
50
+ return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds(), 3)}`;
51
+ }
52
+
53
+ export function fmtDateTime(ms) {
54
+ if (ms == null) return '–';
55
+ const d = new Date(ms);
56
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${fmtTime(ms)}`;
57
+ }
58
+
59
+ export function fmtDur(ms) {
60
+ if (ms == null) return '–';
61
+ if (ms < 1000) return `${Math.round(ms)} ms`;
62
+ if (ms < 60000) return `${(ms / 1000).toFixed(ms < 10000 ? 2 : 1)} s`;
63
+ const m = Math.floor(ms / 60000);
64
+ return `${m}m${pad(Math.round((ms % 60000) / 1000))}s`;
65
+ }
66
+
67
+ export function fmtOffset(ms) {
68
+ if (ms == null) return '';
69
+ return ms < 1000 ? `+${Math.round(ms)}ms` : `+${(ms / 1000).toFixed(2)}s`;
70
+ }
71
+
72
+ export function fmtBytes(n) {
73
+ if (n == null) return '–';
74
+ if (n < 1024) return `${n} B`;
75
+ if (n < 1048576) return `${(n / 1024).toFixed(1)} KB`;
76
+ if (n < 1073741824) return `${(n / 1048576).toFixed(n < 10485760 ? 2 : 1)} MB`;
77
+ return `${(n / 1073741824).toFixed(2)} GB`;
78
+ }
79
+
80
+ export const fmtInt = (n) => (n == null ? '–' : Number(n).toLocaleString('en-US'));
81
+
82
+ export function fmtTok(n) {
83
+ if (n == null) return '–';
84
+ if (n >= 1e6) return `${(n / 1e6).toFixed(2)}M`;
85
+ if (n >= 1e4) return `${(n / 1e3).toFixed(1)}K`;
86
+ return String(n);
87
+ }
88
+
89
+ export function oneLine(s, max = 160) {
90
+ if (typeof s !== 'string') return '';
91
+ const t = s.slice(0, max * 3).replace(/\s+/g, ' ').trim();
92
+ return t.length > max ? `${t.slice(0, max)}…` : t;
93
+ }
94
+
95
+ export const shortId = (s, n = 8) => (typeof s === 'string' && s.length > n + 2 ? `${s.slice(0, n)}…` : s);
96
+
97
+ export function hashHue(s) {
98
+ let x = 2166136261;
99
+ for (let i = 0; i < s.length; i++) x = Math.imul(x ^ s.charCodeAt(i), 16777619);
100
+ return (x >>> 0) % 360;
101
+ }
102
+
103
+ export function tryJson(text) {
104
+ if (typeof text !== 'string') return { ok: false };
105
+ try { return { ok: true, value: JSON.parse(text) }; } catch (e) { return { ok: false, error: String(e.message || e) }; }
106
+ }
107
+
108
+ // ---------- clipboard / download / toast ----------
109
+
110
+ let toastTimer;
111
+ export function toast(msg, isError = false) {
112
+ let el = document.getElementById('toast');
113
+ if (!el) { el = h('div', { id: 'toast' }); document.body.append(el); }
114
+ el.textContent = msg;
115
+ el.className = `toast show${isError ? ' err' : ''}`;
116
+ clearTimeout(toastTimer);
117
+ toastTimer = setTimeout(() => { el.classList.remove('show'); }, isError ? 5000 : 1800);
118
+ }
119
+
120
+ export async function copyText(text) {
121
+ text = String(text);
122
+ try {
123
+ await navigator.clipboard.writeText(text);
124
+ } catch {
125
+ const ta = h('textarea', { style: { position: 'fixed', top: '0', left: '0', opacity: '0' } });
126
+ ta.value = text;
127
+ document.body.append(ta);
128
+ ta.select();
129
+ document.execCommand('copy');
130
+ ta.remove();
131
+ }
132
+ toast(`已复制 ${fmtInt(text.length)} 字符`);
133
+ }
134
+
135
+ export function download(name, data, type = 'application/json') {
136
+ const blob = data instanceof Blob ? data : new Blob([data], { type });
137
+ const url = URL.createObjectURL(blob);
138
+ const a = h('a', { href: url, download: name });
139
+ document.body.append(a);
140
+ a.click();
141
+ a.remove();
142
+ setTimeout(() => URL.revokeObjectURL(url), 60000);
143
+ }
144
+
145
+ export const safeName = (s) => String(s).replace(/[^\w.-]+/g, '_').slice(0, 80);
146
+
147
+ // ---------- bridge carriers and data URLs ----------
148
+
149
+ function b64ToBytes(b64) {
150
+ const s = b64.replace(/-/g, '+').replace(/_/g, '/').replace(/\s+/g, '');
151
+ const bin = atob(s + '==='.slice((s.length + 3) % 4));
152
+ const out = new Uint8Array(bin.length);
153
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
154
+ return out;
155
+ }
156
+
157
+ export const CARRIER_RE = /^(cxbr1|cxbc1):/;
158
+
159
+ /** Decodes a bridge state envelope: prefix + base64url(JSON). */
160
+ export function decodeCarrier(s) {
161
+ const m = CARRIER_RE.exec(s);
162
+ if (!m) return null;
163
+ try {
164
+ return { prefix: m[1], value: JSON.parse(new TextDecoder().decode(b64ToBytes(s.slice(m[0].length)))) };
165
+ } catch (e) {
166
+ return { prefix: m[1], error: String(e.message || e) };
167
+ }
168
+ }
169
+
170
+ const DATA_IMG_RE = /^data:(image\/[a-z0-9.+-]+);base64,/i;
171
+ export const isDataImage = (s) => typeof s === 'string' && DATA_IMG_RE.test(s.slice(0, 64));
172
+
173
+ export function dataUrlBlob(url) {
174
+ const m = /^data:([^;,]*)(;base64)?,/i.exec(url.slice(0, 200));
175
+ if (!m) return new Blob([url], { type: 'text/plain' });
176
+ const body = url.slice(m[0].length);
177
+ return new Blob([m[2] ? b64ToBytes(body) : decodeURIComponent(body)], { type: m[1] || 'application/octet-stream' });
178
+ }
179
+
180
+ /** Small image thumbnail; data: URLs only, remote URLs are shown as text. */
181
+ export function thumb(url, size = 'sm') {
182
+ if (!isDataImage(url)) return h('span', { class: 'chip dim', title: url }, '远程图片');
183
+ const img = h('img', { class: `thumb ${size}`, src: url, alt: 'image', title: '点击放大' });
184
+ img.addEventListener('click', (e) => { e.stopPropagation(); img.classList.toggle('big'); });
185
+ return img;
186
+ }
187
+
188
+ // ---------- string rendering ----------
189
+
190
+ const FOLD = 3000, HEAD = 1500, TAIL = 1500;
191
+
192
+ /**
193
+ * A block view for a string: shows head + omitted marker + tail for long text,
194
+ * with buttons to expand, copy, download, decode carriers and parse JSON.
195
+ */
196
+ export function strBlock(s, opts = {}) {
197
+ const name = opts.name || 'text';
198
+ const box = h('div', { class: 'sv' });
199
+ if (isDataImage(s)) {
200
+ const mime = DATA_IMG_RE.exec(s.slice(0, 64))[1];
201
+ put(box, h('div', { class: 'sv-tools' },
202
+ thumb(s, 'md'),
203
+ h('span', { class: 'dim' }, `${mime} · ${fmtBytes(s.length)}(data URL)`),
204
+ btn('复制', () => copyText(s)),
205
+ btn('下载图片', () => download(`${name}.${mime.split('/')[1].replace(/\W.*/, '')}`, dataUrlBlob(s)))));
206
+ return box;
207
+ }
208
+ const pre = h('pre', { class: 'sv-text' });
209
+ const extra = h('div', { class: 'sv-extra' });
210
+ let full = false;
211
+ let foldBtn = null;
212
+ const render = () => {
213
+ if (foldBtn) foldBtn.textContent = full ? '折叠' : '展开全部';
214
+ pre.replaceChildren();
215
+ if (full || s.length <= FOLD) put(pre, s);
216
+ else {
217
+ put(pre, s.slice(0, HEAD),
218
+ h('span', { class: 'sv-omit', title: '点击展开全部', onclick: (e) => { e.stopPropagation(); full = true; render(); } },
219
+ ` … 省略 ${fmtInt(s.length - HEAD - TAIL)} 字符 … `),
220
+ s.slice(-TAIL));
221
+ }
222
+ };
223
+ render();
224
+ const tools = h('div', { class: 'sv-tools' }, h('span', { class: 'dim' }, `${fmtInt(s.length)} 字符`));
225
+ if (s.length > FOLD) put(tools, foldBtn = btn('展开全部', () => { full = !full; render(); }));
226
+ put(tools, btn('复制', () => copyText(s)), btn('下载', () => download(`${name}.txt`, s, 'text/plain')));
227
+ if (CARRIER_RE.test(s) && opts.tree) {
228
+ put(tools, btn('解码载荷', () => toggleExtra(extra, () => {
229
+ const d = decodeCarrier(s);
230
+ return d.error ? h('div', { class: 'err' }, `解码失败:${d.error}`) : opts.tree(d.value, `${d.prefix} 解码`);
231
+ })));
232
+ } else if (opts.tree && /^\s*[[{]/.test(s.slice(0, 20))) {
233
+ put(tools, btn('按 JSON 解析', () => toggleExtra(extra, () => {
234
+ const r = tryJson(s);
235
+ return r.ok ? opts.tree(r.value, 'JSON') : h('div', { class: 'err' }, `不是合法 JSON:${r.error}`);
236
+ })));
237
+ }
238
+ put(box, tools, pre, extra);
239
+ return box;
240
+ }
241
+
242
+ function toggleExtra(extra, make) {
243
+ if (extra.firstChild) extra.replaceChildren();
244
+ else put(extra, make());
245
+ }
246
+
247
+ /** Inline string preview used in compact rows and tables. */
248
+ export function strInline(s, max = 120) {
249
+ if (isDataImage(s)) return h('span', { class: 'str' }, thumb(s), ` data URL ${fmtBytes(s.length)}`);
250
+ if (s.length <= max && !s.includes('\n')) return h('span', { class: 'str' }, JSON.stringify(s));
251
+ return h('span', { class: 'str' }, JSON.stringify(oneLine(s, max)).slice(0, -1) + '…"', h('span', { class: 'dim' }, ` (${fmtInt(s.length)} 字符)`));
252
+ }
253
+
254
+ /** Simple key/value table; values may be nodes or strings. */
255
+ export function kvTable(rows, cls = '') {
256
+ return h('table', { class: `kv ${cls}`.trim() },
257
+ h('tbody', null, rows.map(([k, v, title]) => h('tr', null, h('th', { title }, k), h('td', null, v)))));
258
+ }