@wacry/codeks-bridge 0.0.0-stage → 0.0.1

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 (99) 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/auth.d.ts +2 -0
  14. package/dist/auth.js +42 -0
  15. package/dist/backends/anthropic.d.ts +8 -0
  16. package/dist/backends/anthropic.js +309 -0
  17. package/dist/backends/chat.d.ts +16 -0
  18. package/dist/backends/chat.js +284 -0
  19. package/dist/backends/deepseek.d.ts +6 -0
  20. package/dist/backends/deepseek.js +37 -0
  21. package/dist/backends/responses.d.ts +5 -0
  22. package/dist/backends/responses.js +205 -0
  23. package/dist/carrier.d.ts +22 -0
  24. package/dist/carrier.js +61 -0
  25. package/dist/catalog.d.ts +8 -0
  26. package/dist/catalog.js +40 -0
  27. package/dist/cli.d.ts +2 -0
  28. package/dist/cli.js +64 -0
  29. package/dist/compaction-handoff.d.ts +13 -0
  30. package/dist/compaction-handoff.js +126 -0
  31. package/dist/compaction-history.d.ts +7 -0
  32. package/dist/compaction-history.js +20 -0
  33. package/dist/compaction.d.ts +20 -0
  34. package/dist/compaction.js +134 -0
  35. package/dist/config.d.ts +6 -0
  36. package/dist/config.js +167 -0
  37. package/dist/errors.d.ts +43 -0
  38. package/dist/errors.js +141 -0
  39. package/dist/headers.d.ts +6 -0
  40. package/dist/headers.js +39 -0
  41. package/dist/history.d.ts +7 -0
  42. package/dist/history.js +85 -0
  43. package/dist/ids.d.ts +1 -0
  44. package/dist/ids.js +4 -0
  45. package/dist/index.d.ts +5 -0
  46. package/dist/index.js +4 -0
  47. package/dist/inspect/capture.d.ts +119 -0
  48. package/dist/inspect/capture.js +370 -0
  49. package/dist/inspect/http.d.ts +7 -0
  50. package/dist/inspect/http.js +149 -0
  51. package/dist/inspect/view.d.ts +239 -0
  52. package/dist/inspect/view.js +388 -0
  53. package/dist/model-catalog.d.ts +18 -0
  54. package/dist/model-catalog.js +16 -0
  55. package/dist/profiles.d.ts +6 -0
  56. package/dist/profiles.js +63 -0
  57. package/dist/prompts.d.ts +5 -0
  58. package/dist/prompts.js +118 -0
  59. package/dist/server.d.ts +4 -0
  60. package/dist/server.js +232 -0
  61. package/dist/sse.d.ts +12 -0
  62. package/dist/sse.js +93 -0
  63. package/dist/tool-history.d.ts +4 -0
  64. package/dist/tool-history.js +72 -0
  65. package/dist/tools.d.ts +33 -0
  66. package/dist/tools.js +182 -0
  67. package/dist/types.d.ts +115 -0
  68. package/dist/types.js +1 -0
  69. package/dist/upstream.d.ts +3 -0
  70. package/dist/upstream.js +84 -0
  71. package/dist/writer.d.ts +95 -0
  72. package/dist/writer.js +430 -0
  73. package/docs/backends-anthropic.md +61 -0
  74. package/docs/backends-deepseek-chat.md +75 -0
  75. package/docs/backends-responses.md +33 -0
  76. package/docs/compaction.md +176 -0
  77. package/docs/config.md +231 -0
  78. package/docs/design.md +93 -0
  79. package/docs/inspect.md +138 -0
  80. package/docs/reasoning.md +72 -0
  81. package/docs/releasing.md +50 -0
  82. package/docs/responses-stream.md +90 -0
  83. package/docs/testing.md +177 -0
  84. package/docs/tools.md +55 -0
  85. package/examples/config.json +35 -0
  86. package/package.json +46 -4
  87. package/ui/api.js +64 -0
  88. package/ui/app.css +375 -0
  89. package/ui/app.js +489 -0
  90. package/ui/detail.js +682 -0
  91. package/ui/diff.js +281 -0
  92. package/ui/dom.js +258 -0
  93. package/ui/events.js +351 -0
  94. package/ui/favicon.svg +1 -0
  95. package/ui/index.html +58 -0
  96. package/ui/labels.js +101 -0
  97. package/ui/list.js +180 -0
  98. package/ui/structure.js +285 -0
  99. package/ui/tree.js +158 -0
package/ui/list.js ADDED
@@ -0,0 +1,180 @@
1
+ // Exchange list: filtering, flat or thread-grouped rows, keyed row reuse and keyboard moves.
2
+ import { h, fmtTime, fmtDur, fmtTok, fmtBytes, oneLine, shortId, hashHue, put } from './dom.js';
3
+ import { KIND_LABEL, OUTCOME_LABEL, callLabel } from './labels.js';
4
+
5
+ // Same-conversation key. Falls back to the window id prefix ("<thread>:<n>") for captures without thread headers.
6
+ export const threadKey = (s) => s.thread?.thread_id ?? s.thread?.session_id ?? (s.thread?.window_id ? String(s.thread.window_id).split(':')[0] : null);
7
+ export const OUTCOME_CLASS = { active: 'info', completed: 'ok', incomplete: 'warn', failed: 'bad', error: 'bad', aborted: 'bad' };
8
+
9
+ // Imported Detail objects may lack the Summary-only fields (older or hand-made exports); derive them.
10
+ const callsOf = (s) => s.upstream_calls ?? s.upstream?.map((c) => ({ label: c.label, status: c.response?.status ?? null, end: c.response?.end ?? (c.error ? 'error' : null) })) ?? [];
11
+ const requestBytes = (s) => s.bytes?.request ?? s.client?.request?.body?.bytes ?? null;
12
+
13
+ export const isErrorish = (s) => ['failed', 'error', 'aborted', 'incomplete'].includes(s.outcome) || (s.status ?? 0) >= 400 ||
14
+ callsOf(s).some((c) => (c.status ?? 0) >= 400 || c.end === 'error' || c.end === 'cancelled');
15
+
16
+ export const byNewest = (a, b) => (b.started_at ?? 0) - (a.started_at ?? 0) || (b.seq ?? 0) - (a.seq ?? 0);
17
+
18
+ export const FILTER_CHIPS = [
19
+ ['errors', '仅错误', '失败、错误、中止、未完成,或任一 HTTP 状态 ≥ 400'],
20
+ ['compaction', '仅压缩', 'kind = compaction'],
21
+ ['active', '进行中', '仍在传输'],
22
+ ['hideInvalid', '隐藏无效', '隐藏 kind = invalid(无法解析或路由失败)'],
23
+ ];
24
+
25
+ const searchCache = new WeakMap();
26
+ function searchText(s) {
27
+ let t = searchCache.get(s);
28
+ if (t === undefined) {
29
+ const th = s.thread ?? {};
30
+ t = [s.id, s.model, s.adapter, s.api, typeof s.upstream === 'string' ? s.upstream : s.route?.upstream, s.upstream_model, s.kind, KIND_LABEL[s.kind], s.outcome, OUTCOME_LABEL[s.outcome],
31
+ s.status, s.error, s.preview, th.session_id, th.thread_id, th.parent_thread_id, th.subagent, th.window_id,
32
+ ...callsOf(s).flatMap((c) => [c.label, callLabel(c.label), c.status, c.end])]
33
+ .filter((x) => x != null).join(' ').toLowerCase();
34
+ searchCache.set(s, t);
35
+ }
36
+ return t;
37
+ }
38
+
39
+ export function matches(s, words, chips) {
40
+ if (chips.errors && !isErrorish(s)) return false;
41
+ if (chips.compaction && s.kind !== 'compaction') return false;
42
+ if (chips.active && !s.active) return false;
43
+ if (chips.hideInvalid && s.kind === 'invalid') return false;
44
+ if (!words.length) return true;
45
+ const text = searchText(s);
46
+ return words.every((w) => text.includes(w));
47
+ }
48
+
49
+ function usageLine(u) {
50
+ if (!u || typeof u !== 'object') return null;
51
+ const parts = [];
52
+ const add = (label, v) => { if (typeof v === 'number') parts.push(`${label} ${fmtTok(v)}`); };
53
+ add('入', u.input_tokens ?? u.prompt_tokens);
54
+ add('出', u.output_tokens ?? u.completion_tokens);
55
+ add('缓存', u.input_tokens_details?.cached_tokens ?? u.cache_read_input_tokens ?? u.prompt_tokens_details?.cached_tokens);
56
+ add('推理', u.output_tokens_details?.reasoning_tokens ?? u.completion_tokens_details?.reasoning_tokens);
57
+ return parts.length ? h('span', { class: 'x-usage', title: JSON.stringify(u, null, 1) }, parts.join(' · ')) : null;
58
+ }
59
+
60
+ const callClass = (c) => ((c.status ?? 0) >= 400 || c.end === 'error' ? 'bad' : c.end === 'active' ? 'info' : c.end === 'cancelled' ? 'warn' : c.status ? 'ok' : 'dim');
61
+
62
+ function rowEl(s, flat, onSelect) {
63
+ const th = s.thread ?? {};
64
+ const el = h('div', { class: `xrow o-${s.outcome}${s.active ? ' active' : ''}`, dataset: { id: s.id }, title: `${s.id} · ${s.kind}` });
65
+ const key = threadKey(s);
66
+ if (key) { el.classList.add('hued'); el.style.setProperty('--thue', hashHue(key)); }
67
+ const status = s.status != null ? ` ${s.status}` : '';
68
+ put(el,
69
+ h('div', { class: 'x1' },
70
+ h('span', { class: 'x-time' }, fmtTime(s.started_at)),
71
+ h('span', { class: `chip kind k-${s.kind}` }, KIND_LABEL[s.kind] ?? s.kind ?? '?'),
72
+ h('span', { class: 'x-model', title: s.upstream_model ? `上游模型 ${s.upstream_model}` : '' },
73
+ s.model ?? '(无模型)', s.adapter ? h('span', { class: 'dim' }, ` → ${s.adapter}`) : null),
74
+ h('span', { class: 'grow' }),
75
+ h('span', { class: `chip ${OUTCOME_CLASS[s.outcome] ?? 'dim'}`, title: s.error ?? '' }, `${OUTCOME_LABEL[s.outcome] ?? s.outcome}${status}`),
76
+ h('span', { class: 'x-dur' }, s.active ? '…' : fmtDur(s.duration_ms))),
77
+ h('div', { class: 'x2' },
78
+ callsOf(s).map((c) => h('span', { class: `chip sm ${callClass(c)}`, title: `end: ${c.end ?? '–'}` }, `${callLabel(c.label)} ${c.status ?? '…'}`)),
79
+ flat && th.subagent ? h('span', { class: 'chip sm sub', title: '子代理(x-openai-subagent)' }, `子代理 ${th.subagent}`) : null,
80
+ flat && th.parent_thread_id ? h('span', { class: 'chip sm dim', title: `父线程 ${th.parent_thread_id}` }, `父 ${shortId(th.parent_thread_id)}`) : null,
81
+ usageLine(s.usage),
82
+ requestBytes(s) != null ? h('span', { class: 'x-size dim', title: '客户端请求大小' }, fmtBytes(requestBytes(s))) : null),
83
+ s.error ? h('div', { class: 'x3 err' }, oneLine(s.error, 160)) : null,
84
+ s.preview ? h('div', { class: 'x3' }, oneLine(s.preview, 200)) : null);
85
+ el.addEventListener('click', (e) => onSelect(s.id, e));
86
+ return el;
87
+ }
88
+
89
+ export class ExchangeList {
90
+ constructor(el, infoEl, { onSelect }) {
91
+ this.el = el;
92
+ this.infoEl = infoEl;
93
+ this.onSelect = onSelect;
94
+ this.rows = new Map(); // id -> {s, flat, el}
95
+ this.order = [];
96
+ this.collapsed = new Set();
97
+ this.selected = null;
98
+ }
99
+
100
+ render(summaries, { filter, chips, group, empty }) {
101
+ const words = filter.toLowerCase().split(/\s+/).filter(Boolean);
102
+ const all = [...summaries.values()];
103
+ const list = all.filter((s) => matches(s, words, chips)).sort(byNewest);
104
+ const flat = !group;
105
+ const rowFor = (s) => {
106
+ let r = this.rows.get(s.id);
107
+ if (!r || r.s !== s || r.flat !== flat) {
108
+ r = { s, flat, el: rowEl(s, flat, this.onSelect) };
109
+ this.rows.set(s.id, r);
110
+ }
111
+ r.el.classList.toggle('sel', s.id === this.selected);
112
+ return r.el;
113
+ };
114
+ for (const id of this.rows.keys()) if (!summaries.has(id)) this.rows.delete(id);
115
+ const out = [];
116
+ this.order = [];
117
+ if (flat) {
118
+ for (const s of list) { out.push(rowFor(s)); this.order.push(s.id); }
119
+ } else {
120
+ const groups = new Map();
121
+ for (const s of list) {
122
+ const k = threadKey(s) ?? '';
123
+ if (!groups.has(k)) groups.set(k, []);
124
+ groups.get(k).push(s);
125
+ }
126
+ // Map thread id -> group key, to link subagent groups to their parent.
127
+ for (const [k, items] of groups) {
128
+ out.push(this.groupHead(k, items, groups));
129
+ if (this.collapsed.has(k)) continue;
130
+ for (const s of items) { out.push(rowFor(s)); this.order.push(s.id); }
131
+ }
132
+ }
133
+ if (!out.length) out.push(h('div', { class: 'empty small' }, all.length ? '没有匹配筛选条件的记录' : empty));
134
+ this.el.replaceChildren(...out);
135
+ this.infoEl.textContent = all.length ? `显示 ${list.length} / 共 ${all.length} 条 · 最新在上` : '';
136
+ }
137
+
138
+ groupHead(k, items, groups) {
139
+ const th = items.find((s) => s.thread)?.thread ?? {};
140
+ const sub = items.find((s) => s.thread?.subagent)?.thread?.subagent;
141
+ const parent = items.find((s) => s.thread?.parent_thread_id)?.thread?.parent_thread_id;
142
+ const models = [...new Set(items.map((s) => s.model).filter(Boolean))];
143
+ const collapsed = this.collapsed.has(k);
144
+ const el = h('div', { class: 'tgroup', title: k ? `thread_id/session_id: ${k}` : '请求头里没有 thread_id / session_id' },
145
+ h('span', { class: 'tn-tog' }, collapsed ? '▸' : '▾'),
146
+ h('span', { class: 'swatch' }),
147
+ h('span', { class: 'mono' }, k ? `线程 ${shortId(k, 12)}` : '(无线程信息)'),
148
+ h('span', { class: 'dim' }, `${items.length} 条 · ${fmtTime(items[0].started_at)}`),
149
+ models.length ? h('span', { class: 'dim' }, models.join(', ')) : null,
150
+ sub ? h('span', { class: 'chip sm sub' }, `子代理 ${sub}`) : null,
151
+ parent ? h('span', {
152
+ class: 'chip sm link-chip', title: `父线程 ${parent}${groups.has(parent) ? '(点击定位)' : '(不在列表中)'}`,
153
+ onclick: (e) => { e.stopPropagation(); const p = groups.get(parent); if (p) this.onSelect(p[0].id, e); },
154
+ }, `↖ 父线程 ${shortId(parent, 10)}`) : null,
155
+ th.window_id ? h('span', { class: 'dim' }, `窗口 ${shortId(th.window_id)}`) : null);
156
+ if (k) { el.classList.add('hued'); el.style.setProperty('--thue', hashHue(k)); }
157
+ el.addEventListener('click', () => {
158
+ if (collapsed) this.collapsed.delete(k); else this.collapsed.add(k);
159
+ this.rerender?.();
160
+ });
161
+ return el;
162
+ }
163
+
164
+ setSelected(id) {
165
+ this.selected = id;
166
+ for (const [rid, r] of this.rows) r.el.classList.toggle('sel', rid === id);
167
+ }
168
+
169
+ scrollToSelected() {
170
+ this.rows.get(this.selected)?.el.scrollIntoView({ block: 'nearest' });
171
+ }
172
+
173
+ /** Returns the id delta rows away from the selection, in display order. */
174
+ neighbor(delta) {
175
+ if (!this.order.length) return null;
176
+ const i = this.order.indexOf(this.selected);
177
+ if (i < 0) return this.order[0];
178
+ return this.order[Math.max(0, Math.min(this.order.length - 1, i + delta))];
179
+ }
180
+ }
@@ -0,0 +1,285 @@
1
+ // Structure view: every top-level field in a table, and one compact row per
2
+ // item for arrays of objects (input, output, messages, system, tools, ...).
3
+ import { h, btn, fmtBytes, fmtInt, hashHue, oneLine, shortId, strBlock, strInline, thumb, isDataImage, CARRIER_RE, put } from './dom.js';
4
+ import { jsonTree, prim, preview, escPtr } from './tree.js';
5
+ import { annDetail } from './labels.js';
6
+
7
+ const ROW_PAGE = 1200;
8
+ const TEXT_KEYS = new Set(['text', 'thinking', 'content', 'output', 'arguments', 'input', 'reasoning_content', 'refusal', 'description', 'partial_json', 'instructions', 'summary']);
9
+
10
+ export const isItemArray = (v) => Array.isArray(v) && v.length > 0 &&
11
+ v.slice(0, 20).every((x) => x && typeof x === 'object' && !Array.isArray(x));
12
+
13
+ export function newStructState() {
14
+ return { openRows: new Set(), openFields: new Set(), openAnn: new Set(), trees: new Map(), filters: new Map(), shown: new Map() };
15
+ }
16
+
17
+ const treeState = (st, key) => {
18
+ let t = st.trees.get(key);
19
+ if (!t) st.trees.set(key, (t = { exp: new Set() }));
20
+ return t;
21
+ };
22
+
23
+ /**
24
+ * opts: { state, ann: Map<pointer, Annotation[]>, annChip(a, onClick), note }
25
+ */
26
+ export function structureView(obj, opts) {
27
+ const st = opts.state;
28
+ const root = h('div', { class: 'struct' });
29
+ if (opts.note) put(root, h('div', { class: 'note' }, opts.note));
30
+ if (obj === null || typeof obj !== 'object') {
31
+ put(root, h('div', { class: 'dim' }, '正文不是 JSON 对象:'), typeof obj === 'string' ? strBlock(obj) : prim(obj));
32
+ return root;
33
+ }
34
+ if (Array.isArray(obj)) {
35
+ put(root, isItemArray(obj) ? itemList('', obj, opts) : jsonTree(obj, { state: treeState(st, '#root') }));
36
+ return root;
37
+ }
38
+ const lists = [];
39
+ const tbody = h('tbody');
40
+ for (const [key, value] of Object.entries(obj)) {
41
+ const isList = isItemArray(value);
42
+ if (isList) lists.push([key, value]);
43
+ fieldRow(tbody, key, value, isList, opts);
44
+ }
45
+ put(root, h('table', { class: 'fields' },
46
+ h('thead', null, h('tr', null, h('th', null, '字段'), h('th', null, '类型'), h('th', null, '值'))), tbody));
47
+ for (const [key, value] of lists) put(root, itemList(key, value, opts));
48
+ return root;
49
+ }
50
+
51
+ function typeText(v) {
52
+ if (Array.isArray(v)) return `array ${v.length}`;
53
+ if (v === null) return 'null';
54
+ if (typeof v === 'object') return `object ${Object.keys(v).length}`;
55
+ if (typeof v === 'string') return `string ${fmtInt(v.length)}`;
56
+ return typeof v;
57
+ }
58
+
59
+ function fieldRow(tbody, key, value, isList, opts) {
60
+ const st = opts.state;
61
+ const ptr = `/${escPtr(key)}`;
62
+ const anns = opts.ann?.get(ptr)?.map((a) => opts.annChip(a));
63
+ const tr = h('tr', { dataset: { ptr } });
64
+ const detail = h('tr', { class: 'field-detail' });
65
+ let valueCell;
66
+ let openable = false;
67
+ let makeDetail = null;
68
+ if (isList) {
69
+ valueCell = h('a', { class: 'link', href: '#', onclick: (e) => { e.preventDefault(); tr.closest('.struct')?.querySelector(`[data-list="${CSS.escape(key)}"]`)?.scrollIntoView({ block: 'start' }); } },
70
+ `${value.length} 项 · 见下方列表`);
71
+ } else if (typeof value === 'string' && (value.length > 160 || value.includes('\n') || isDataImage(value) || CARRIER_RE.test(value))) {
72
+ valueCell = strInline(value, 160);
73
+ openable = true;
74
+ makeDetail = () => strBlock(value, { name: key, tree: (v, label) => jsonTree(v, { label, auto: 2 }) });
75
+ } else if (value && typeof value === 'object') {
76
+ const compact = JSON.stringify(value);
77
+ const small = compact.length <= 160;
78
+ valueCell = small ? h('span', { class: 'mono' }, compact) : h('span', { class: 'tn-pv' }, preview(value, 140));
79
+ openable = true;
80
+ makeDetail = () => jsonTree(value, { state: treeState(st, ptr), base: ptr, ann: opts.ann, annChip: opts.annChip, auto: small ? 3 : 1 });
81
+ } else {
82
+ valueCell = prim(value);
83
+ }
84
+ put(tr, h('td', { class: 'fkey' }, openable ? h('span', { class: 'tn-tog' }, st.openFields.has(key) ? '▾' : '▸') : h('span', { class: 'tn-tog' }), key),
85
+ h('td', { class: 'ftype' }, typeText(value)), h('td', { class: 'fval' }, valueCell, anns));
86
+ put(tbody, tr);
87
+ if (!openable) return;
88
+ tr.classList.add('clickable');
89
+ const td = h('td', { colSpan: 3 });
90
+ put(detail, td);
91
+ const show = (open) => {
92
+ if (open) { st.openFields.add(key); if (!td.firstChild) put(td, makeDetail()); tr.after(detail); }
93
+ else { st.openFields.delete(key); detail.remove(); }
94
+ tr.querySelector('.tn-tog').textContent = open ? '▾' : '▸';
95
+ };
96
+ tr.addEventListener('click', (e) => {
97
+ if (e.target.closest('button, a, .chip, img') || String(window.getSelection?.() ?? '')) return;
98
+ show(!st.openFields.has(key));
99
+ });
100
+ if (st.openFields.has(key)) queueMicrotask(() => show(true));
101
+ }
102
+
103
+ // ---------- item lists ----------
104
+
105
+ function cacheText(cc) {
106
+ if (!cc || typeof cc !== 'object') return 'cache';
107
+ return `cache ${cc.ttl ?? cc.type ?? ''}`.trim();
108
+ }
109
+
110
+ function scan(v, key, depth, info) {
111
+ if (typeof v === 'string') {
112
+ if (key === 'encrypted_content') { info.enc += v.length; info.carrier = CARRIER_RE.exec(v)?.[1] ?? info.carrier ?? 'raw'; }
113
+ else if (key === 'signature') info.sig += v.length;
114
+ else if (isDataImage(v)) info.imgs.push(v);
115
+ else if (key === 'image_url' || key === 'url') { if (/^https?:/.test(v)) info.remote++; }
116
+ else if (TEXT_KEYS.has(key)) { info.text += v.length; if (!info.preview && v.trim()) info.preview = v; }
117
+ return;
118
+ }
119
+ if (!v || typeof v !== 'object' || depth > 6) return;
120
+ if (Array.isArray(v)) { for (const x of v) scan(x, key, depth + 1, info); return; }
121
+ if (v.cache_control) info.cache.add(cacheText(v.cache_control));
122
+ if (v.type === 'redacted_thinking' && typeof v.data === 'string') info.enc += v.data.length;
123
+ if (v.type === 'base64' && typeof v.data === 'string' && /^image\//.test(v.media_type ?? '')) info.imgs.push(`data:${v.media_type};base64,${v.data}`);
124
+ for (const [k, x] of Object.entries(v)) {
125
+ if (k === 'cache_control') continue;
126
+ scan(x, k, depth + 1, info);
127
+ }
128
+ }
129
+
130
+ function partLabel(p) {
131
+ if (!p || typeof p !== 'object') return typeof p;
132
+ const t = p.type ?? '{…}';
133
+ if (t === 'tool_use' || t === 'server_tool_use') return `${t}·${p.name ?? '?'}`;
134
+ return t;
135
+ }
136
+
137
+ /** Badges and metrics for one item of any API (Responses, Messages, Chat, tools). */
138
+ export function describeItem(it) {
139
+ const info = { text: 0, enc: 0, sig: 0, imgs: [], remote: 0, cache: new Set(), preview: '', carrier: null };
140
+ scan(it, null, 0, info);
141
+ const badges = [];
142
+ const b = (text, cls, title, hue) => badges.push({ text: String(text), cls, title, hue });
143
+ if (it.type != null) b(it.type, 'type', 'type', hashHue(String(it.type)));
144
+ if (it.role != null) b(it.role, 'role', 'role', hashHue(`r${it.role}`));
145
+ if (it.phase != null) b(`phase:${it.phase}`, 'phase', 'phase');
146
+ if (it.status != null && it.status !== 'completed') b(it.status, 'dim', 'status');
147
+ const name = it.name ?? it.function?.name;
148
+ if (name != null) b(name, 'name', 'name');
149
+ if (it.namespace != null) b(`ns:${it.namespace}`, 'name', 'namespace');
150
+ for (const k of ['call_id', 'tool_call_id', 'tool_use_id']) if (typeof it[k] === 'string') b(shortId(it[k], 14), 'id', `${k}: ${it[k]}`);
151
+ const parts = [];
152
+ for (const key of ['content', 'output']) {
153
+ if (!Array.isArray(it[key])) continue;
154
+ const counts = new Map();
155
+ for (const p of it[key]) { const l = partLabel(p); counts.set(l, (counts.get(l) ?? 0) + 1); }
156
+ for (const [l, n] of counts) parts.push(n > 1 ? `${l}×${n}` : l);
157
+ }
158
+ if (parts.length) b(parts.join(' '), 'parts', '内容块类型');
159
+ if (Array.isArray(it.summary) && it.summary.length) b(`summary×${it.summary.length}`, 'parts', 'summary');
160
+ if (Array.isArray(it.tool_calls)) b(`tool_calls: ${it.tool_calls.map((c) => c.function?.name ?? c.name ?? '?').join(', ')}`, 'name', 'tool_calls');
161
+ if (Array.isArray(it.tools)) b(`含 ${it.tools.length} 个工具`, 'parts', it.tools.map((t) => t.name ?? t.function?.name ?? t.type).join(', '));
162
+ const params = it.parameters ?? it.input_schema ?? it.function?.parameters;
163
+ if (params?.properties) b(`参数 ${Object.keys(params.properties).length}`, 'dim', Object.keys(params.properties).join(', '));
164
+ for (const c of info.cache) b(c, 'cache', 'cache_control');
165
+ if (info.enc) b(`${info.carrier && info.carrier !== 'raw' ? info.carrier : 'enc'} ${fmtInt(info.enc)}`, info.carrier && info.carrier !== 'raw' ? 'carrier' : 'enc', '加密/载荷长度(字符)');
166
+ if (info.sig) b(`sig ${fmtInt(info.sig)}`, 'enc', 'signature 长度');
167
+ if (it.reasoning_content != null && typeof it.reasoning_content === 'string') b(`reasoning_content ${fmtInt(it.reasoning_content.length)}`, 'enc', 'reasoning_content 长度');
168
+ if (info.remote) b(`远程图片×${info.remote}`, 'dim', '未自动加载');
169
+ return { badges, text: info.text, imgs: info.imgs, preview: info.preview };
170
+ }
171
+
172
+ function badgeEl(x) {
173
+ const el = h('span', { class: `chip ${x.cls}`, title: x.title });
174
+ el.textContent = x.text;
175
+ if (x.hue != null) { el.classList.add('hue'); el.style.setProperty('--hue', x.hue); }
176
+ return el;
177
+ }
178
+
179
+ function itemList(key, items, opts) {
180
+ const st = opts.state;
181
+ const base = key === '' ? '' : `/${escPtr(key)}`;
182
+ const listAnn = new Map();
183
+ for (const [ptr, list] of opts.ann ?? []) {
184
+ if (!ptr.startsWith(`${base}/`)) continue;
185
+ const idx = ptr.slice(base.length + 1).split('/')[0];
186
+ listAnn.set(idx, [...(listAnn.get(idx) ?? []), ...list]);
187
+ }
188
+ let total = 0;
189
+ const descs = items.map((it) => {
190
+ const d = describeItem(it);
191
+ d.size = JSON.stringify(it)?.length ?? 0;
192
+ total += d.size;
193
+ d.search = `${d.badges.map((x) => x.text).join(' ')} ${d.preview.slice(0, 400)}`.toLowerCase();
194
+ return d;
195
+ });
196
+ const rows = h('div', { class: 'items' });
197
+ const filter = h('input', { class: 'list-filter', type: 'search', placeholder: '筛选(类型/角色/名称/文字)', value: st.filters.get(key) ?? '' });
198
+ const count = h('span', { class: 'dim' });
199
+ const sec = h('section', { class: 'item-list', dataset: { list: key } },
200
+ h('div', { class: 'list-head' },
201
+ h('span', { class: 'list-title' }, key || '(根数组)'),
202
+ h('span', { class: 'dim' }, `${items.length} 项 · ${fmtBytes(total)}`), count, filter,
203
+ btn('全部收起', () => { for (const p of [...st.openRows]) if (p.startsWith(`${base}/`)) st.openRows.delete(p); render(); }),
204
+ btn('跳到末尾', () => { st.shown.set(key, Infinity); render(); rows.lastElementChild?.scrollIntoView({ block: 'end' }); })),
205
+ rows);
206
+
207
+ const render = () => {
208
+ rows.replaceChildren();
209
+ const q = filter.value.trim().toLowerCase();
210
+ let shown = 0;
211
+ let matched = 0;
212
+ const limit = st.shown.get(key) ?? ROW_PAGE;
213
+ for (let i = 0; i < items.length; i++) {
214
+ if (q && !descs[i].search.includes(q)) continue;
215
+ matched++;
216
+ if (shown >= limit) continue;
217
+ shown++;
218
+ put(rows, itemRow(i, items[i], descs[i], `${base}/${i}`, listAnn.get(String(i)), opts));
219
+ }
220
+ count.textContent = q ? `匹配 ${matched}` : '';
221
+ if (shown < matched) put(rows, btn(`显示其余 ${fmtInt(matched - shown)} 项`, () => { st.shown.set(key, Infinity); render(); }, 'more'));
222
+ };
223
+ filter.addEventListener('input', () => { st.filters.set(key, filter.value); render(); });
224
+ render();
225
+ return sec;
226
+ }
227
+
228
+ function itemRow(i, item, d, ptr, anns, opts) {
229
+ const st = opts.state;
230
+ const wrap = h('div', { class: 'item', dataset: { ptr } });
231
+ const imgs = d.imgs.slice(0, 3).map((u) => thumb(u));
232
+ if (d.imgs.length > 3) imgs.push(h('span', { class: 'dim' }, `+${d.imgs.length - 3}`));
233
+ const tog = h('span', { class: 'tn-tog' }, '▸');
234
+ const row = h('div', { class: 'item-row' },
235
+ tog, h('span', { class: 'idx' }, `#${i}`),
236
+ h('span', { class: 'badges' }, d.badges.map(badgeEl)),
237
+ imgs,
238
+ h('span', { class: 'pv' }, oneLine(d.preview, 200)),
239
+ anns?.map((a) => opts.annChip(a, () => toggleAnn())),
240
+ h('span', { class: 'metrics', title: `文字 ${fmtInt(d.text)} 字符 · JSON ${fmtInt(d.size)} 字符` }, d.text ? `${fmtInt(d.text)} 字` : '', ` ${fmtBytes(d.size)}`));
241
+ const body = h('div', { class: 'item-body' });
242
+ const annBox = h('div', { class: 'item-anns' });
243
+ put(wrap, row);
244
+ const open = (on) => {
245
+ if (on) {
246
+ st.openRows.add(ptr);
247
+ if (!body.firstChild) put(body, jsonTree(item, { state: treeState(st, ptr), base: ptr, ann: opts.ann, annChip: opts.annChip, auto: 2 }));
248
+ put(wrap, body);
249
+ } else {
250
+ st.openRows.delete(ptr);
251
+ body.remove();
252
+ }
253
+ tog.textContent = on ? '▾' : '▸';
254
+ wrap.classList.toggle('open', on);
255
+ };
256
+ const toggleAnn = (force) => {
257
+ const on = force ?? !st.openAnn.has(ptr);
258
+ if (on) {
259
+ st.openAnn.add(ptr);
260
+ if (!annBox.firstChild) put(annBox, ...anns.map(annDetail));
261
+ row.after(annBox);
262
+ } else {
263
+ st.openAnn.delete(ptr);
264
+ annBox.remove();
265
+ }
266
+ };
267
+ row.addEventListener('click', (e) => {
268
+ if (e.target.closest('button, a, .chip, img') || String(window.getSelection?.() ?? '')) return;
269
+ open(!st.openRows.has(ptr));
270
+ });
271
+ if (anns && st.openAnn.has(ptr)) toggleAnn(true);
272
+ if (st.openRows.has(ptr)) open(true);
273
+ return wrap;
274
+ }
275
+
276
+ /** Marks the item row for a pointer such as /input/7 as open; returns the row pointer. */
277
+ export function focusPointer(st, pointer) {
278
+ const m = /^\/([^/]+)\/(\d+)/.exec(pointer ?? '');
279
+ if (!m) return null;
280
+ const row = `/${m[1]}/${m[2]}`;
281
+ st.openRows.add(row);
282
+ st.openAnn.add(row);
283
+ if (Number(m[2]) >= ROW_PAGE) st.shown.set(m[1].replace(/~1/g, '/').replace(/~0/g, '~'), Infinity);
284
+ return row;
285
+ }
package/ui/tree.js ADDED
@@ -0,0 +1,158 @@
1
+ // Lazy JSON tree: children are created only when a node is expanded.
2
+ import { h, btn, copyText, download, fmtInt, strBlock, isDataImage, thumb, CARRIER_RE, toast, put } from './dom.js';
3
+
4
+ const PAGE = 300;
5
+ const EXPAND_BUDGET = 4000;
6
+
7
+ export const escPtr = (k) => String(k).replace(/~/g, '~0').replace(/\//g, '~1');
8
+
9
+ /**
10
+ * opts.state: { exp: Set<string> } keeps expanded paths across re-renders.
11
+ * opts.ann: Map<pointer, Annotation[]>, opts.annChip(ann) -> Node.
12
+ * opts.label: optional root label. opts.base: pointer prefix of the root.
13
+ */
14
+ export function jsonTree(value, opts = {}) {
15
+ const state = opts.state || { exp: new Set() };
16
+ const base = opts.base || '';
17
+ if (!state.init) { state.init = true; state.exp.add(base); if (opts.auto) autoExpand(value, base, state.exp, opts.auto); }
18
+ const ctx = {
19
+ exp: state.exp,
20
+ ann: opts.ann,
21
+ annChip: opts.annChip,
22
+ subtree: (v, label) => jsonTree(v, { label, auto: 2, annChip: opts.annChip }),
23
+ };
24
+ return h('div', { class: 'tree' }, nodeEl(opts.label ?? null, value, base, ctx));
25
+ }
26
+
27
+ /** Expands small containers near the root (used for item rows). */
28
+ function autoExpand(v, path, set, depth) {
29
+ if (!v || typeof v !== 'object' || depth <= 0) return;
30
+ const keys = Array.isArray(v) ? v.map((_, i) => i) : Object.keys(v);
31
+ if (keys.length > 40) return;
32
+ set.add(path);
33
+ for (const k of keys) autoExpand(v[k], `${path}/${escPtr(k)}`, set, depth - 1);
34
+ }
35
+
36
+ function expandAll(v, path, set, budget) {
37
+ if (!v || typeof v !== 'object') return;
38
+ if (budget.n-- <= 0) { budget.hit = true; return; }
39
+ set.add(path);
40
+ const keys = Array.isArray(v) ? v.map((_, i) => i) : Object.keys(v);
41
+ for (const k of keys.slice(0, PAGE)) {
42
+ if (budget.n <= 0) { budget.hit = true; break; }
43
+ expandAll(v[k], `${path}/${escPtr(k)}`, set, budget);
44
+ }
45
+ }
46
+
47
+ function collapseAll(path, set) {
48
+ for (const p of [...set]) if (p === path || p.startsWith(`${path}/`)) set.delete(p);
49
+ }
50
+
51
+ export function preview(v, max = 90) {
52
+ let out = '';
53
+ if (Array.isArray(v)) {
54
+ const parts = [];
55
+ for (const x of v.slice(0, 6)) {
56
+ if (x && typeof x === 'object' && !Array.isArray(x)) parts.push(x.type ?? x.role ?? '{…}');
57
+ else parts.push(short(x));
58
+ }
59
+ out = parts.join(', ') + (v.length > 6 ? ', …' : '');
60
+ } else if (v && typeof v === 'object') {
61
+ const parts = [];
62
+ for (const [k, x] of Object.entries(v).slice(0, 5)) parts.push(`${k}: ${short(x)}`);
63
+ out = parts.join(', ') + (Object.keys(v).length > 5 ? ', …' : '');
64
+ }
65
+ return out.length > max ? `${out.slice(0, max)}…` : out;
66
+ }
67
+
68
+ function short(x) {
69
+ if (Array.isArray(x)) return `[${x.length}]`;
70
+ if (x && typeof x === 'object') return '{…}';
71
+ if (typeof x === 'string') return JSON.stringify(x.length > 30 ? `${x.slice(0, 30)}…` : x);
72
+ return String(x);
73
+ }
74
+
75
+ export function prim(v) {
76
+ if (typeof v === 'string') return h('span', { class: 'str' }, JSON.stringify(v));
77
+ if (typeof v === 'number') return h('span', { class: 'num' }, String(v));
78
+ if (typeof v === 'boolean') return h('span', { class: 'bool' }, String(v));
79
+ if (v === undefined) return h('span', { class: 'null' }, 'undefined');
80
+ return h('span', { class: 'null' }, 'null');
81
+ }
82
+
83
+ const longString = (v) => typeof v === 'string' && (v.length > 120 || v.includes('\n') || isDataImage(v) || CARRIER_RE.test(v));
84
+
85
+ function nodeEl(key, value, path, ctx) {
86
+ const wrap = h('div', { class: 'tn' });
87
+ const row = h('div', { class: 'tn-row' });
88
+ put(wrap, row);
89
+ const keyEl = key == null ? null : h('span', { class: typeof key === 'number' ? 'tn-idx' : 'tn-key' }, String(key));
90
+ const sep = keyEl ? h('span', { class: 'tn-sep' }, ': ') : null;
91
+ const anns = ctx.ann?.get(path);
92
+ const annEls = anns && ctx.annChip ? anns.map((a) => ctx.annChip(a)) : null;
93
+ const tog = h('span', { class: 'tn-tog' });
94
+
95
+ if (value === null || typeof value !== 'object') {
96
+ if (longString(value)) {
97
+ const label = isDataImage(value) ? [thumb(value), ` 图片 data URL`] : CARRIER_RE.test(value) ? `bridge 载荷 ${value.slice(0, 5)}` : '字符串';
98
+ put(row, tog, keyEl, sep, h('span', { class: 'dim' }, label, ` · ${fmtInt(value.length)} 字符`), annEls);
99
+ put(wrap, h('div', { class: 'tn-str' }, strBlock(value, { name: key ?? 'value', tree: ctx.subtree })));
100
+ } else {
101
+ put(row, tog, keyEl, sep, prim(value), annEls);
102
+ }
103
+ return wrap;
104
+ }
105
+
106
+ const isArr = Array.isArray(value);
107
+ const keys = isArr ? null : Object.keys(value);
108
+ const n = isArr ? value.length : keys.length;
109
+ const sum = h('span', { class: 'tn-sum' }, isArr ? `[${n}]` : `{${n}}`);
110
+ const pv = h('span', { class: 'tn-pv' }, preview(value));
111
+ const acts = h('span', { class: 'tn-acts' },
112
+ btn('复制', () => copyText(JSON.stringify(value, null, 2))),
113
+ n ? btn('全部展开', () => {
114
+ const budget = { n: EXPAND_BUDGET };
115
+ expandAll(value, path, ctx.exp, budget);
116
+ if (budget.hit) toast(`节点过多,只展开了前 ${fmtInt(EXPAND_BUDGET)} 个`);
117
+ wrap.replaceWith(nodeEl(key, value, path, ctx));
118
+ }) : null,
119
+ n ? btn('收起', () => { collapseAll(path, ctx.exp); wrap.replaceWith(nodeEl(key, value, path, ctx)); }) : null,
120
+ key == null ? btn('下载', () => download('value.json', JSON.stringify(value, null, 2))) : null);
121
+ put(row, tog, keyEl, sep, sum, pv, annEls, acts);
122
+ if (!n) return wrap;
123
+
124
+ let kids = null;
125
+ const setOpen = (open) => {
126
+ if (open) {
127
+ ctx.exp.add(path);
128
+ if (!kids) { kids = h('div', { class: 'tn-kids' }); fill(kids, value, keys, path, ctx, 0); }
129
+ put(wrap, kids);
130
+ } else {
131
+ ctx.exp.delete(path);
132
+ kids?.remove();
133
+ }
134
+ tog.textContent = open ? '▾' : '▸';
135
+ pv.hidden = open;
136
+ };
137
+ row.classList.add('tn-click');
138
+ row.addEventListener('click', (e) => {
139
+ if (e.target.closest('button, .chip, a, img')) return;
140
+ if (String(window.getSelection?.() ?? '')) return;
141
+ setOpen(!ctx.exp.has(path));
142
+ });
143
+ setOpen(ctx.exp.has(path));
144
+ return wrap;
145
+ }
146
+
147
+ function fill(kids, value, keys, path, ctx, start) {
148
+ const n = keys ? keys.length : value.length;
149
+ const end = Math.min(n, start + PAGE);
150
+ for (let i = start; i < end; i++) {
151
+ const k = keys ? keys[i] : i;
152
+ put(kids, nodeEl(k, value[k], `${path}/${escPtr(k)}`, ctx));
153
+ }
154
+ if (end < n) {
155
+ const more = btn(`显示更多(还有 ${fmtInt(n - end)} 项)`, () => { more.remove(); fill(kids, value, keys, path, ctx, end); }, 'more');
156
+ put(kids, more);
157
+ }
158
+ }