@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/events.js ADDED
@@ -0,0 +1,351 @@
1
+ // SSE event list with delta grouping, plus synthesis of the final response
2
+ // object from a stream (used by the structure view and the diff).
3
+ import { h, btn, fmtInt, fmtOffset, oneLine, strBlock, tryJson, shortId, put } from './dom.js';
4
+ import { jsonTree } from './tree.js';
5
+
6
+ const ROW_PAGE = 1000;
7
+
8
+ export const isKeepalive = (ev) => ev.event === 'ping' || ev.event === 'keepalive' || ev.json?.type === 'keepalive' || ev.json?.type === 'ping';
9
+
10
+ /** Parses events once per response object. */
11
+ const parsedCache = new WeakMap();
12
+ export function parseEvents(resp) {
13
+ if (!resp?.events) return null;
14
+ let list = parsedCache.get(resp.events);
15
+ if (!list) {
16
+ list = resp.events.map((e, i) => {
17
+ const r = tryJson(e.data);
18
+ return { i, t: e.t, event: e.event ?? null, data: e.data, json: r.ok ? r.value : undefined };
19
+ });
20
+ parsedCache.set(resp.events, list);
21
+ }
22
+ return list;
23
+ }
24
+
25
+ /** Returns {key, text, label} for streaming delta events of Responses, Messages and Chat. */
26
+ function deltaOf(ev) {
27
+ const j = ev.json;
28
+ if (!j || typeof j !== 'object') return null;
29
+ if (typeof j.type === 'string' && j.type.endsWith('.delta') && typeof j.delta === 'string') {
30
+ return { key: [j.type, j.item_id, j.output_index, j.content_index, j.summary_index].join('|'), text: j.delta, label: j.type };
31
+ }
32
+ if (j.type === 'content_block_delta' && j.delta) {
33
+ const d = j.delta;
34
+ const text = d.text ?? d.thinking ?? d.partial_json ?? d.signature;
35
+ if (typeof text === 'string') return { key: `cbd|${d.type}|${j.index}`, text, label: `content_block_delta · ${d.type}` };
36
+ }
37
+ if (Array.isArray(j.choices) && j.choices.length === 1 && j.choices[0].delta && !j.choices[0].finish_reason && !j.usage) {
38
+ const c = j.choices[0];
39
+ const d = c.delta;
40
+ const keys = Object.keys(d).filter((k) => d[k] != null && d[k] !== '');
41
+ if (keys.length === 1 && (keys[0] === 'content' || keys[0] === 'reasoning_content') && typeof d[keys[0]] === 'string') {
42
+ return { key: `chat|${keys[0]}|${c.index}`, text: d[keys[0]], label: `delta.${keys[0]}` };
43
+ }
44
+ if (keys.length === 1 && keys[0] === 'tool_calls' && d.tool_calls.length === 1) {
45
+ const tc = d.tool_calls[0];
46
+ if (!tc.id && !tc.function?.name && typeof tc.function?.arguments === 'string') {
47
+ return { key: `chat|tool|${c.index}|${tc.index}`, text: tc.function.arguments, label: `delta.tool_calls[${tc.index}].arguments` };
48
+ }
49
+ }
50
+ }
51
+ return null;
52
+ }
53
+
54
+ // Label: the SSE event name, else data.type (most streams are data-only).
55
+ function eventType(ev) {
56
+ return ev.event ?? ev.json?.type ?? (ev.json?.object || (ev.data === '[DONE]' ? '[DONE]' : '(data)'));
57
+ }
58
+
59
+ /** One-line description of an event, picking common fields of all three APIs. */
60
+ function describe(ev) {
61
+ const j = ev.json;
62
+ if (j === undefined) return oneLine(ev.data, 160);
63
+ if (!j || typeof j !== 'object') return JSON.stringify(j);
64
+ const out = [];
65
+ const r = j.response;
66
+ if (r && typeof r === 'object') {
67
+ out.push(`status=${r.status}`);
68
+ if (r.model) out.push(r.model);
69
+ if (r.usage) out.push(usageText(r.usage));
70
+ if (r.error) out.push(`error: ${r.error.message ?? JSON.stringify(r.error)}`);
71
+ if (r.incomplete_details) out.push(JSON.stringify(r.incomplete_details));
72
+ }
73
+ const item = j.item ?? j.content_block ?? j.part;
74
+ if (item && typeof item === 'object') {
75
+ out.push([item.type, item.role, item.name, item.phase && `phase:${item.phase}`].filter(Boolean).join(' '));
76
+ if (typeof item.encrypted_content === 'string') out.push(`encrypted ${fmtInt(item.encrypted_content.length)}${/^cx/.test(item.encrypted_content) ? ` (${item.encrypted_content.slice(0, 5)})` : ''}`);
77
+ const text = item.text ?? item.thinking ?? item.arguments ?? item.content?.[0]?.text;
78
+ if (typeof text === 'string' && text) out.push(oneLine(text, 80));
79
+ if (item.id) out.push(shortId(item.id, 16));
80
+ }
81
+ if (j.output_index != null) out.push(`out#${j.output_index}`);
82
+ if (j.index != null && j.type?.startsWith('content_block')) out.push(`block#${j.index}`);
83
+ if (j.type === 'content_block_delta' && j.delta?.type) {
84
+ const t = j.delta.text ?? j.delta.thinking ?? j.delta.partial_json ?? j.delta.signature;
85
+ out.push(`${j.delta.type}${typeof t === 'string' ? ` ${fmtInt(t.length)} 字符 ${oneLine(t, 60)}` : ''}`);
86
+ }
87
+ if (typeof j.delta === 'string') out.push(oneLine(j.delta, 100));
88
+ if (typeof j.text === 'string') out.push(oneLine(j.text, 100));
89
+ if (j.message && typeof j.message === 'object') out.push([j.message.model, j.message.usage && usageText(j.message.usage)].filter(Boolean).join(' '));
90
+ if (j.delta && typeof j.delta === 'object' && !Array.isArray(j.delta) && j.type === 'message_delta') out.push(JSON.stringify(j.delta));
91
+ if (j.usage) out.push(usageText(j.usage));
92
+ if (Array.isArray(j.choices)) {
93
+ for (const c of j.choices) {
94
+ if (c.finish_reason) out.push(`finish=${c.finish_reason}`);
95
+ if (c.delta?.role) out.push(`role=${c.delta.role}`);
96
+ for (const tc of c.delta?.tool_calls ?? []) out.push(`tool_call[${tc.index}] ${tc.function?.name ?? ''} ${tc.id ?? ''}`.trim());
97
+ }
98
+ }
99
+ if (j.error) out.push(`error: ${j.error.message ?? JSON.stringify(j.error)}`);
100
+ if (typeof j.sequence_number === 'number') out.push(`seq ${j.sequence_number}`);
101
+ return out.filter(Boolean).join(' · ') || oneLine(ev.data, 160);
102
+ }
103
+
104
+ export function usageText(u) {
105
+ if (!u || typeof u !== 'object') return '';
106
+ const pick = (...ks) => { for (const k of ks) { const v = k.split('.').reduce((o, p) => o?.[p], u); if (typeof v === 'number') return v; } return null; };
107
+ const parts = [];
108
+ const add = (label, v) => { if (v != null) parts.push(`${label} ${fmtInt(v)}`); };
109
+ add('in', pick('input_tokens', 'prompt_tokens'));
110
+ add('out', pick('output_tokens', 'completion_tokens'));
111
+ add('cached', pick('input_tokens_details.cached_tokens', 'cache_read_input_tokens', 'prompt_cache_hit_tokens', 'prompt_tokens_details.cached_tokens'));
112
+ add('cache_write', pick('cache_creation_input_tokens'));
113
+ add('reasoning', pick('output_tokens_details.reasoning_tokens', 'completion_tokens_details.reasoning_tokens'));
114
+ return parts.join(' / ');
115
+ }
116
+
117
+ export function newEventsState() {
118
+ return { open: new Set(), openGroups: new Set(), groupEvents: new Set(), trees: new Map(), shown: ROW_PAGE, type: '', q: '' };
119
+ }
120
+
121
+ /**
122
+ * opts: { state, prefs: {keepalive, merge}, onPrefs(), annByEvent: Map<index, Annotation[]>, annChip, annDetail }
123
+ */
124
+ export function eventsView(resp, opts) {
125
+ const st = opts.state;
126
+ const prefs = opts.prefs;
127
+ const all = parseEvents(resp);
128
+ const root = h('div', { class: 'events' });
129
+ const keep = all.filter(isKeepalive).length;
130
+ const visible = prefs.keepalive ? all : all.filter((e) => !isKeepalive(e));
131
+ const types = new Map();
132
+ for (const e of visible) { const t = eventType(e); types.set(t, (types.get(t) ?? 0) + 1); }
133
+ const typeSel = h('select', { class: 'sel', title: '按类型筛选' },
134
+ h('option', { value: '' }, `全部类型(${types.size})`),
135
+ [...types].sort((a, b) => b[1] - a[1]).map(([t, n]) => h('option', { value: t, selected: st.type === t }, `${t} ×${n}`)));
136
+ const q = h('input', { type: 'search', class: 'list-filter', placeholder: '搜索事件 data', value: st.q });
137
+ const list = h('div', { class: 'ev-list' });
138
+ const info = h('span', { class: 'dim' });
139
+ const toggle = (label, key, title) => h('label', { class: 'check', title },
140
+ h('input', { type: 'checkbox', checked: !!prefs[key], onchange: (e) => { prefs[key] = e.target.checked; opts.onPrefs(); } }), label);
141
+ put(root, h('div', { class: 'ev-tools' }, info, typeSel, q,
142
+ toggle(`显示 keepalive/ping(${keep})`, 'keepalive', '心跳事件默认隐藏'),
143
+ toggle('合并连续 delta', 'merge', '同一类型、同一 item/index 的连续 delta 合并为一行')), list);
144
+
145
+ const render = () => {
146
+ list.replaceChildren();
147
+ const needle = st.q.toLowerCase();
148
+ const events = visible.filter((e) => (!st.type || eventType(e) === st.type) && (!needle || e.data.toLowerCase().includes(needle)));
149
+ const rows = [];
150
+ for (const e of events) {
151
+ const d = prefs.merge ? deltaOf(e) : null;
152
+ const last = rows[rows.length - 1];
153
+ if (d && last?.key === d.key) { last.events.push(e); last.parts.push(d.text); continue; }
154
+ rows.push(d ? { key: d.key, label: d.label, events: [e], parts: [d.text] } : { events: [e] });
155
+ }
156
+ info.textContent = `共 ${fmtInt(all.length)} 个事件 · 显示 ${fmtInt(events.length)} · ${fmtInt(rows.length)} 行`;
157
+ const limit = st.shown;
158
+ for (const r of rows.slice(0, limit)) put(list, r.key && r.events.length > 1 ? groupRow(r, opts) : eventRow(r.events[0], opts));
159
+ if (rows.length > limit) put(list, btn(`显示其余 ${fmtInt(rows.length - limit)} 行`, () => { st.shown = Infinity; render(); }, 'more'));
160
+ };
161
+ typeSel.addEventListener('change', () => { st.type = typeSel.value; render(); });
162
+ q.addEventListener('input', () => { st.q = q.value; render(); });
163
+ render();
164
+ return root;
165
+ }
166
+
167
+ function treeState(st, key) {
168
+ let t = st.trees.get(key);
169
+ if (!t) st.trees.set(key, (t = { exp: new Set() }));
170
+ return t;
171
+ }
172
+
173
+ function eventRow(e, opts) {
174
+ const st = opts.state;
175
+ const anns = opts.annByEvent?.get(e.i);
176
+ const wrap = h('div', { class: `ev${isKeepalive(e) ? ' keep' : ''}`, dataset: { ev: e.i } });
177
+ const tog = h('span', { class: 'tn-tog' }, '▸');
178
+ const type = eventType(e);
179
+ const row = h('div', { class: 'ev-row' }, tog,
180
+ h('span', { class: 'ev-t' }, fmtOffset(e.t)),
181
+ h('span', { class: 'ev-i' }, `#${e.i}`),
182
+ h('span', { class: `ev-type${/error|failed/.test(type) ? ' err' : ''}`, title: e.event ? 'SSE event 字段' : 'data.type' }, type),
183
+ e.event && typeof e.json?.type === 'string' && e.json.type !== e.event ? h('span', { class: 'chip dim', title: 'data.type' }, e.json.type) : null,
184
+ h('span', { class: 'pv' }, describe(e)),
185
+ anns?.map((a) => opts.annChip(a, () => open(!st.open.has(key)))));
186
+ const body = h('div', { class: 'ev-body' });
187
+ put(wrap, row);
188
+ const key = `e${e.i}`;
189
+ const open = (on) => {
190
+ if (on) {
191
+ st.open.add(key);
192
+ if (!body.firstChild) {
193
+ if (anns) put(body, ...anns.map(opts.annDetail));
194
+ put(body, e.json !== undefined ? jsonTree(e.json, { state: treeState(st, key), auto: 3 }) : strBlock(e.data, { name: `event-${e.i}` }));
195
+ }
196
+ put(wrap, body);
197
+ } else { st.open.delete(key); body.remove(); }
198
+ tog.textContent = on ? '▾' : '▸';
199
+ };
200
+ row.addEventListener('click', (ev) => {
201
+ if (ev.target.closest('button, .chip') || String(window.getSelection?.() ?? '')) return;
202
+ open(!st.open.has(key));
203
+ });
204
+ if (st.open.has(key)) open(true);
205
+ return wrap;
206
+ }
207
+
208
+ function groupRow(r, opts) {
209
+ const st = opts.state;
210
+ const first = r.events[0];
211
+ const lastEv = r.events[r.events.length - 1];
212
+ const text = r.parts.join('');
213
+ const key = `g${first.i}`;
214
+ const wrap = h('div', { class: 'ev group' });
215
+ const tog = h('span', { class: 'tn-tog' }, '▸');
216
+ const row = h('div', { class: 'ev-row' }, tog,
217
+ h('span', { class: 'ev-t' }, `${fmtOffset(first.t)}→${fmtOffset(lastEv.t).slice(1)}`),
218
+ h('span', { class: 'ev-i' }, `#${first.i}–${lastEv.i}`),
219
+ h('span', { class: 'ev-type' }, r.label),
220
+ h('span', { class: 'chip info' }, `×${r.events.length}`),
221
+ h('span', { class: 'pv' }, oneLine(text, 220) || '(空)'),
222
+ h('span', { class: 'metrics' }, `${fmtInt(text.length)} 字符`));
223
+ const body = h('div', { class: 'ev-body' });
224
+ put(wrap, row);
225
+ const open = (on) => {
226
+ if (on) {
227
+ st.openGroups.add(key);
228
+ if (!body.firstChild) {
229
+ const sub = h('div', { class: 'ev-sub' });
230
+ const showEvents = (show) => {
231
+ if (show) { st.groupEvents.add(key); for (const e of r.events.slice(0, 2000)) put(sub, eventRow(e, opts)); if (r.events.length > 2000) put(sub, h('div', { class: 'dim' }, `仅显示前 2000 个,可在“原始”中查看全部`)); }
232
+ else { st.groupEvents.delete(key); sub.replaceChildren(); }
233
+ };
234
+ put(body, h('div', { class: 'sub-title' }, '合并后的文本'), strBlock(text, { name: 'merged', tree: (v, label) => jsonTree(v, { label, auto: 2 }) }),
235
+ btn(`逐条显示 ${fmtInt(r.events.length)} 个事件`, () => showEvents(!st.groupEvents.has(key))), sub);
236
+ if (st.groupEvents.has(key)) showEvents(true);
237
+ }
238
+ put(wrap, body);
239
+ } else { st.openGroups.delete(key); body.remove(); }
240
+ tog.textContent = on ? '▾' : '▸';
241
+ };
242
+ row.addEventListener('click', (ev) => {
243
+ if (ev.target.closest('button, .chip') || String(window.getSelection?.() ?? '')) return;
244
+ open(!st.openGroups.has(key));
245
+ });
246
+ if (st.openGroups.has(key)) open(true);
247
+ return wrap;
248
+ }
249
+
250
+ // ---------- final object synthesis ----------
251
+
252
+ const TERMINAL = new Set(['response.completed', 'response.incomplete', 'response.failed', 'response.done']);
253
+
254
+ /**
255
+ * Returns { value, note } for a response: the parsed JSON body, the terminal
256
+ * Responses object, or an object rebuilt from Messages / Chat stream events.
257
+ */
258
+ export function finalObject(resp) {
259
+ if (!resp) return null;
260
+ const events = parseEvents(resp);
261
+ if (!events) {
262
+ const r = tryJson(resp.body?.text);
263
+ return r.ok ? { value: r.value, note: null } : { value: resp.body?.text ?? '', note: '正文不是 JSON,显示原文' };
264
+ }
265
+ const data = events.filter((e) => e.json && typeof e.json === 'object');
266
+ if (data.some((e) => typeof e.json.type === 'string' && e.json.type.startsWith('response.'))) return fromResponses(data);
267
+ if (data.some((e) => e.json.type === 'message_start')) return fromMessages(data);
268
+ if (data.some((e) => Array.isArray(e.json.choices))) return fromChat(data);
269
+ return { value: data.map((e) => e.json), note: '无法识别的事件格式:显示全部事件的 data' };
270
+ }
271
+
272
+ function fromResponses(data) {
273
+ const output = [];
274
+ for (const { json: j } of data) {
275
+ if (j.type === 'response.output_item.done') output[j.output_index ?? output.length] = j.item;
276
+ }
277
+ for (let k = data.length - 1; k >= 0; k--) {
278
+ const j = data[k].json;
279
+ if (TERMINAL.has(j.type) && j.response) {
280
+ const note = `取自终止事件 ${j.type}(#${data[k].i})的 response 对象`;
281
+ if (j.response.output?.length || !output.length) return { value: j.response, note };
282
+ return { value: { ...j.response, output }, note: `${note};output 由 output_item.done 按 output_index 合成` };
283
+ }
284
+ }
285
+ const created = data.find((e) => e.json.type === 'response.created')?.json.response ?? {};
286
+ const errors = data.filter((e) => e.json.type === 'error').map((e) => e.json);
287
+ return { value: { ...created, output, ...(errors.length ? { errors } : {}) }, note: '流没有终止事件:由 response.created 与 output_item.done 合成' };
288
+ }
289
+
290
+ function fromMessages(data) {
291
+ let msg = {};
292
+ const blocks = [];
293
+ const partial = new Map();
294
+ const extra = [];
295
+ for (const { json: j } of data) {
296
+ if (j.type === 'message_start') msg = structuredClone(j.message ?? {});
297
+ else if (j.type === 'content_block_start') blocks[j.index] = structuredClone(j.content_block ?? {});
298
+ else if (j.type === 'content_block_delta') {
299
+ const b = (blocks[j.index] ??= {});
300
+ const d = j.delta ?? {};
301
+ if (d.type === 'text_delta') b.text = (b.text ?? '') + d.text;
302
+ else if (d.type === 'thinking_delta') b.thinking = (b.thinking ?? '') + d.thinking;
303
+ else if (d.type === 'signature_delta') b.signature = (b.signature ?? '') + d.signature;
304
+ else if (d.type === 'input_json_delta') partial.set(j.index, (partial.get(j.index) ?? '') + d.partial_json);
305
+ else extra.push(j);
306
+ } else if (j.type === 'content_block_stop') {
307
+ if (partial.has(j.index)) {
308
+ const r = tryJson(partial.get(j.index));
309
+ blocks[j.index].input = r.ok ? r.value : partial.get(j.index);
310
+ }
311
+ } else if (j.type === 'message_delta') {
312
+ Object.assign(msg, j.delta ?? {});
313
+ if (j.usage) msg.usage = { ...(msg.usage ?? {}), ...j.usage };
314
+ } else if (!['message_stop', 'ping'].includes(j.type)) extra.push(j);
315
+ }
316
+ for (const [i, s] of partial) if (blocks[i] && blocks[i].input === undefined) blocks[i].input = s;
317
+ msg.content = blocks;
318
+ if (extra.length) msg['(其他事件)'] = extra;
319
+ return { value: msg, note: '由 Messages SSE 事件合成(message_start + content_block_* + message_delta)' };
320
+ }
321
+
322
+ function fromChat(data) {
323
+ const out = {};
324
+ const choices = [];
325
+ for (const { json: j } of data) {
326
+ for (const k of ['id', 'object', 'created', 'model', 'system_fingerprint']) if (j[k] != null && out[k] == null) out[k] = j[k];
327
+ if (j.usage) out.usage = j.usage;
328
+ for (const c of j.choices ?? []) {
329
+ const ch = (choices[c.index ?? 0] ??= { index: c.index ?? 0, message: {}, finish_reason: null });
330
+ const m = ch.message;
331
+ const d = c.delta ?? c.message ?? {};
332
+ for (const [k, v] of Object.entries(d)) {
333
+ if (k === 'tool_calls' && Array.isArray(v)) {
334
+ m.tool_calls ??= [];
335
+ for (const tc of v) {
336
+ const t = (m.tool_calls[tc.index ?? 0] ??= { function: { arguments: '' } });
337
+ if (tc.id) t.id = tc.id;
338
+ if (tc.type) t.type = tc.type;
339
+ if (tc.function?.name) t.function.name = tc.function.name;
340
+ if (typeof tc.function?.arguments === 'string') t.function.arguments += tc.function.arguments;
341
+ }
342
+ } else if (typeof v === 'string' && (k === 'content' || k === 'reasoning_content')) m[k] = (m[k] ?? '') + v;
343
+ else if (v != null) m[k] = v;
344
+ }
345
+ if (c.finish_reason) ch.finish_reason = c.finish_reason;
346
+ }
347
+ }
348
+ out.object = 'chat.completion(由 chunk 合成)';
349
+ out.choices = choices;
350
+ return { value: out, note: '由 Chat SSE chunk 合成(delta 按 choice/tool_call 合并)' };
351
+ }
package/ui/favicon.svg ADDED
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect width="16" height="16" rx="3" fill="#2f6feb"/><path d="M3 5h4v2H3zm6 0h4v2H9zM3 9h10v2H3z" fill="#fff"/></svg>
package/ui/index.html ADDED
@@ -0,0 +1,58 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <meta name="color-scheme" content="light dark">
7
+ <title>codeks-bridge 检查器</title>
8
+ <link rel="icon" href="favicon.svg" type="image/svg+xml">
9
+ <link rel="stylesheet" href="app.css">
10
+ <script type="module" src="app.js"></script>
11
+ </head>
12
+ <body>
13
+ <header class="top">
14
+ <span class="brand" title="codeks-bridge inspector">bridge 检查器</span>
15
+ <span id="conn" class="chip dim">连接中…</span>
16
+ <button id="capture" type="button" class="btn capture" disabled>开始捕获</button>
17
+ <button id="clear" type="button" class="btn" title="删除所有已捕获的记录">清空</button>
18
+ <button id="export-all" type="button" class="btn" title="下载全部记录(可再导入离线查看)">导出全部</button>
19
+ <button id="import" type="button" class="btn" title="打开导出文件离线查看;也可以把文件拖到页面上">导入</button>
20
+ <button id="live" type="button" class="btn" hidden>返回实时</button>
21
+ <input id="import-file" type="file" accept=".json,application/json" hidden>
22
+ <span id="mem" class="mem"></span>
23
+ <details id="models" class="pop"><summary class="btn">模型</summary><div class="pop-body" id="models-body"></div></details>
24
+ <span class="grow"></span>
25
+ <input id="filter" class="filter" type="search" placeholder="筛选:模型 / 线程 / 预览 / 状态(按 / 聚焦)" autocomplete="off">
26
+ <span id="chips" class="chips"></span>
27
+ <button id="group" type="button" class="btn toggle" title="按 thread_id(或 session_id)分组">按线程分组</button>
28
+ </header>
29
+ <div id="banner" class="banner" hidden></div>
30
+ <main id="main" class="main">
31
+ <section class="list-pane">
32
+ <div id="list-info" class="list-info"></div>
33
+ <div id="list" class="list" tabindex="0"></div>
34
+ </section>
35
+ <div id="splitter" class="splitter" title="拖动调整宽度"></div>
36
+ <section id="detail" class="detail">
37
+ <div id="boot" class="empty">
38
+ <p>正在加载…</p>
39
+ <p class="dim">如果一直停留在这里:请通过 bridge 提供的 /inspect/ 地址打开本页(浏览器不允许从本地文件加载 ES 模块)。</p>
40
+ </div>
41
+ </section>
42
+ </main>
43
+ <dialog id="token-dialog" class="token-dialog">
44
+ <form id="token-form">
45
+ <h3>需要访问令牌</h3>
46
+ <p class="dim">这个 bridge 配置了 inspector 令牌。令牌只保存在当前标签页(sessionStorage)。</p>
47
+ <input id="token-input" type="password" autocomplete="off" placeholder="令牌" required>
48
+ <p id="token-error" class="err" hidden></p>
49
+ <div class="dialog-buttons">
50
+ <button type="button" id="token-cancel" class="btn">取消</button>
51
+ <button type="submit" class="btn primary">确定</button>
52
+ </div>
53
+ </form>
54
+ </dialog>
55
+ <div id="drop" class="drop" hidden>松开以导入导出文件</div>
56
+ <div id="toast" class="toast"></div>
57
+ </body>
58
+ </html>
package/ui/labels.js ADDED
@@ -0,0 +1,101 @@
1
+ // Chinese labels and annotation rendering shared by the views.
2
+ import { h, kvTable, shortId, strBlock, put } from './dom.js';
3
+ import { jsonTree } from './tree.js';
4
+
5
+ export const CALL_LABEL = { main: '主请求', summary: '压缩·文字摘要', native: '压缩·原生', handoff: '压缩·交接复述' };
6
+ export const callLabel = (l) => CALL_LABEL[l] ?? l;
7
+
8
+ export const KIND_LABEL = { turn: '轮次', compaction: '压缩', invalid: '无效' };
9
+ export const OUTCOME_LABEL = { active: '进行中', completed: '完成', incomplete: '未完成', failed: '失败', error: '错误', aborted: '中止' };
10
+ export const END_LABEL = { active: '传输中', complete: '完整', cancelled: '已取消', error: '出错', unread: '终止事件后停止读取' };
11
+
12
+ const DECISION = { replay: '回放', drop: '丢弃', native: '恢复原生', summary: '文字摘要', handoff: '按需文字交接', reject: '拒绝', output: '输出' };
13
+ const DECISION_CLASS = { replay: 'ok', native: 'ok', drop: 'warn', summary: 'warn', handoff: 'warn', reject: 'bad', output: 'info' };
14
+ // Verb shown before the reason text, per decision.
15
+ const VERB = { replay: '回放', drop: '丢弃', reject: '拒绝', summary: '改用文字摘要', handoff: '获取文字交接', native: '恢复原生压缩' };
16
+
17
+ const REASON = {
18
+ reasoning: {
19
+ same_source: '同一来源',
20
+ allowed_source: '来源在 replay 白名单内',
21
+ source_not_allowed: '来源未在 replay 白名单',
22
+ format_mismatch: '载荷格式与目标不匹配',
23
+ not_bridge_state: '不是 bridge 载荷(无 cxbr1 前缀)',
24
+ malformed: 'bridge 载荷损坏,无法解码',
25
+ },
26
+ compaction: {
27
+ native_restored: '',
28
+ no_native: '检查点没有原生表示',
29
+ native_replay_disabled: '目标关闭了 native_replay',
30
+ source_not_allowed: '来源未在原生压缩白名单',
31
+ format_mismatch: '原生格式与目标不匹配',
32
+ not_bridge_state: '不是 bridge 检查点(无 cxbc1 前缀)',
33
+ malformed: 'bridge 检查点损坏,无法解码',
34
+ },
35
+ };
36
+
37
+ export const KIND_TEXT = { reasoning: 'reasoning', compaction: '压缩' };
38
+
39
+ export function reasonText(a) {
40
+ if (a.decision === 'output' || a.reason === 'output') return '输出:bridge 生成的载荷';
41
+ const verb = VERB[a.decision] ?? DECISION[a.decision] ?? a.decision ?? '?';
42
+ const why = REASON[a.kind]?.[a.reason];
43
+ if (why === '') return verb;
44
+ return `${verb}:${why ?? a.reason ?? '未知原因'}`;
45
+ }
46
+
47
+ export const decisionText = (d) => DECISION[d] ?? d ?? '–';
48
+ export const decisionClass = (d) => DECISION_CLASS[d] ?? 'info';
49
+
50
+ export function sourceText(src) {
51
+ if (!src) return '';
52
+ const who = src.aliases?.length ? src.aliases.join('/') : src.model ?? shortId(src.origin_id) ?? '?';
53
+ return `来源 ${who}${src.adapter ? ` (${src.adapter})` : ''}`;
54
+ }
55
+
56
+ export function annChip(a, onClick) {
57
+ const parts = [KIND_TEXT[a.kind] ?? a.kind, a.source ? sourceText(a.source) : (a.decision === 'output' ? null : '来源未知'), reasonText(a)].filter(Boolean);
58
+ return h('span', {
59
+ class: `chip ann ${decisionClass(a.decision)}${onClick ? ' click' : ''}`,
60
+ title: `${a.target}${a.event != null ? ` 事件 #${a.event}` : ''} ${a.pointer ?? ''}\n原因代码:${a.reason}\n格式:${a.format ?? '–'}`,
61
+ onclick: onClick ? (e) => { e.stopPropagation(); onClick(a, e); } : null,
62
+ }, parts.join(' · '));
63
+ }
64
+
65
+ /** Full annotation card: fields, compaction summary text and decoded carrier tree. */
66
+ export function annDetail(a) {
67
+ const src = a.source;
68
+ const rows = [
69
+ ['位置', `${a.target}${a.event != null ? ` · 事件 #${a.event}` : ''} · ${a.pointer ?? '–'}`],
70
+ ['类型', KIND_TEXT[a.kind] ?? a.kind],
71
+ ['决定', h('span', { class: `chip ${decisionClass(a.decision)}` }, decisionText(a.decision))],
72
+ ['原因', `${reasonText(a)}(${a.reason})`],
73
+ ['来源', src ? kvTable(Object.entries(src).map(([k, v]) => [k, Array.isArray(v) ? v.join(', ') : String(v ?? '–')])) : '–'],
74
+ ['格式', a.format ?? '–'],
75
+ ];
76
+ for (const [k, v] of Object.entries(a)) {
77
+ if (!['target', 'event', 'pointer', 'kind', 'decision', 'reason', 'source', 'format', 'decoded'].includes(k)) rows.push([k, JSON.stringify(v)]);
78
+ }
79
+ const box = h('div', { class: 'ann-detail' }, kvTable(rows));
80
+ const summary = a.decoded?.summary;
81
+ if (typeof summary === 'string') put(box, h('div', { class: 'sub-title' }, '文字摘要(decoded.summary)'), strBlock(summary, { name: 'summary' }));
82
+ if (a.decoded != null) put(box, h('div', { class: 'sub-title' }, '解码后的完整载荷(decoded)'), jsonTree(a.decoded, { auto: 3 }));
83
+ return box;
84
+ }
85
+
86
+ /** Maps annotations of one target to row indexes, keyed by the first pointer segment(s). */
87
+ export function annIndex(anns, target) {
88
+ const byPointer = new Map();
89
+ const byEvent = new Map();
90
+ for (const a of anns || []) {
91
+ if (a.target !== target) continue;
92
+ if (target === 'client.response' && a.event != null) push(byEvent, a.event, a);
93
+ else if (a.pointer != null) push(byPointer, a.pointer, a);
94
+ }
95
+ return { byPointer, byEvent };
96
+ }
97
+
98
+ function push(map, k, v) {
99
+ const list = map.get(k);
100
+ if (list) list.push(v); else map.set(k, [v]);
101
+ }