@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.
- package/LICENSE +202 -0
- package/NOTICE +13 -0
- package/README.md +40 -2
- package/catalog/README.md +63 -0
- package/catalog/claude-opus-5-5.json +194 -0
- package/catalog/claude-sonnet-5-5.json +194 -0
- package/catalog/deepseek-flash.json +203 -0
- package/catalog/gpt-5.6-sol.json +152 -0
- package/catalog/gpt-6-astra.json +184 -0
- package/catalog/gpt-6-luna.json +178 -0
- package/catalog/gpt-6-sol.json +182 -0
- package/catalog/gpt-6.1-sol.json +187 -0
- package/dist/adapter-options.d.ts +5 -0
- package/dist/adapter-options.js +41 -0
- package/dist/auth.d.ts +2 -0
- package/dist/auth.js +42 -0
- package/dist/backends/anthropic-tools.d.ts +13 -0
- package/dist/backends/anthropic-tools.js +41 -0
- package/dist/backends/anthropic.d.ts +9 -0
- package/dist/backends/anthropic.js +356 -0
- package/dist/backends/chat.d.ts +16 -0
- package/dist/backends/chat.js +284 -0
- package/dist/backends/deepseek.d.ts +6 -0
- package/dist/backends/deepseek.js +37 -0
- package/dist/backends/responses.d.ts +5 -0
- package/dist/backends/responses.js +227 -0
- package/dist/carrier.d.ts +22 -0
- package/dist/carrier.js +61 -0
- package/dist/catalog.d.ts +8 -0
- package/dist/catalog.js +40 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +64 -0
- package/dist/compaction-handoff.d.ts +13 -0
- package/dist/compaction-handoff.js +126 -0
- package/dist/compaction-history.d.ts +7 -0
- package/dist/compaction-history.js +20 -0
- package/dist/compaction.d.ts +20 -0
- package/dist/compaction.js +134 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.js +170 -0
- package/dist/errors.d.ts +43 -0
- package/dist/errors.js +141 -0
- package/dist/headers.d.ts +6 -0
- package/dist/headers.js +39 -0
- package/dist/history.d.ts +7 -0
- package/dist/history.js +85 -0
- package/dist/ids.d.ts +1 -0
- package/dist/ids.js +4 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/inspect/capture.d.ts +119 -0
- package/dist/inspect/capture.js +370 -0
- package/dist/inspect/http.d.ts +7 -0
- package/dist/inspect/http.js +149 -0
- package/dist/inspect/view.d.ts +239 -0
- package/dist/inspect/view.js +388 -0
- package/dist/model-catalog.d.ts +18 -0
- package/dist/model-catalog.js +16 -0
- package/dist/profiles.d.ts +6 -0
- package/dist/profiles.js +63 -0
- package/dist/prompts.d.ts +5 -0
- package/dist/prompts.js +118 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.js +232 -0
- package/dist/sse.d.ts +12 -0
- package/dist/sse.js +93 -0
- package/dist/tool-history.d.ts +4 -0
- package/dist/tool-history.js +73 -0
- package/dist/tools.d.ts +35 -0
- package/dist/tools.js +193 -0
- package/dist/types.d.ts +125 -0
- package/dist/types.js +1 -0
- package/dist/upstream.d.ts +3 -0
- package/dist/upstream.js +89 -0
- package/dist/writer.d.ts +95 -0
- package/dist/writer.js +430 -0
- package/docs/backends-anthropic.md +88 -0
- package/docs/backends-deepseek-chat.md +75 -0
- package/docs/backends-responses.md +33 -0
- package/docs/compaction.md +180 -0
- package/docs/config.md +271 -0
- package/docs/design.md +97 -0
- package/docs/inspect.md +148 -0
- package/docs/reasoning.md +72 -0
- package/docs/releasing.md +50 -0
- package/docs/responses-stream.md +90 -0
- package/docs/testing.md +177 -0
- package/docs/tools.md +68 -0
- package/examples/config.json +35 -0
- package/examples/gpt-claude.json +27 -0
- package/package.json +46 -4
- package/ui/api.js +64 -0
- package/ui/app.css +379 -0
- package/ui/app.js +489 -0
- package/ui/detail.js +713 -0
- package/ui/diff.js +281 -0
- package/ui/dom.js +258 -0
- package/ui/events.js +376 -0
- package/ui/favicon.svg +1 -0
- package/ui/index.html +58 -0
- package/ui/labels.js +106 -0
- package/ui/list.js +180 -0
- package/ui/structure.js +285 -0
- package/ui/tree.js +158 -0
package/ui/detail.js
ADDED
|
@@ -0,0 +1,713 @@
|
|
|
1
|
+
// Detail of one exchange: header, the four-stage pipeline, and up to two panes
|
|
2
|
+
// (overview, a stage body in one of several view modes, annotations, diff).
|
|
3
|
+
import { h, btn, fill, copyText, download, fmtBytes, fmtDateTime, fmtDur, fmtInt, fmtTime, kvTable, oneLine, safeName, shortId, strBlock, toast, tryJson, put } from './dom.js';
|
|
4
|
+
import { jsonTree } from './tree.js';
|
|
5
|
+
import { structureView, newStructState, focusPointer } from './structure.js';
|
|
6
|
+
import { eventsView, newEventsState, finalObject, inputTransformationReports, parseEvents, usageText } from './events.js';
|
|
7
|
+
import { diffView } from './diff.js';
|
|
8
|
+
import { annChip, annDetail, annIndex, callLabel, decisionClass, decisionText, END_LABEL, INPUT_TRANSFORMATION_LABEL, KIND_LABEL, KIND_TEXT, OUTCOME_LABEL, reasonText, sourceText } from './labels.js';
|
|
9
|
+
import { OUTCOME_CLASS, threadKey, byNewest } from './list.js';
|
|
10
|
+
import { exportDoc } from './api.js';
|
|
11
|
+
|
|
12
|
+
const RAW_LIMIT = 200_000;
|
|
13
|
+
const VIEW_STATES = 80;
|
|
14
|
+
const REQUEST_MODES = [['struct', '结构'], ['json', 'JSON'], ['raw', '原始'], ['headers', '头部']];
|
|
15
|
+
const RESPONSE_MODES = [['struct', '结构'], ['events', '事件'], ['json', 'JSON'], ['raw', '原始'], ['headers', '头部']];
|
|
16
|
+
|
|
17
|
+
// ---------- stage model ----------
|
|
18
|
+
|
|
19
|
+
/** Stable key per upstream call: its label, numbered when a label repeats. */
|
|
20
|
+
function callKeys(calls) {
|
|
21
|
+
const seen = new Map();
|
|
22
|
+
return (calls ?? []).map((c) => {
|
|
23
|
+
const n = (seen.get(c.label) ?? 0) + 1;
|
|
24
|
+
seen.set(c.label, n);
|
|
25
|
+
return n > 1 ? `${c.label}#${n}` : String(c.label);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** All panes of an exchange, in pipeline order. */
|
|
30
|
+
export function stagesOf(d) {
|
|
31
|
+
const keys = callKeys(d.upstream);
|
|
32
|
+
const out = [{ key: 'overview', type: 'overview', title: '概要' },
|
|
33
|
+
{ key: 'req', type: 'request', num: '①', title: 'codeks → bridge', sub: '客户端请求', target: 'client.request', msg: d.client?.request ?? null }];
|
|
34
|
+
(d.upstream ?? []).forEach((call, i) => {
|
|
35
|
+
const label = callLabel(call.label);
|
|
36
|
+
out.push({ key: `up:${keys[i]}`, type: 'request', num: '②', title: 'bridge → 上游', sub: label, call, msg: call });
|
|
37
|
+
out.push({ key: `upres:${keys[i]}`, type: 'response', num: '③', title: '上游 → bridge', sub: label, call, resp: call.response ?? null });
|
|
38
|
+
});
|
|
39
|
+
out.push({ key: 'res', type: 'response', num: '④', title: 'bridge → codeks', sub: '客户端响应', target: 'client.response', resp: d.client?.response ?? null });
|
|
40
|
+
out.push({ key: 'ann', type: 'ann', title: `标注 ${d.annotations?.length ?? 0}` });
|
|
41
|
+
out.push({ key: 'diff', type: 'diff', title: '对比' });
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const parsed = new WeakMap();
|
|
46
|
+
/** Parses a Body once: {ok, value} or {ok:false, error}. */
|
|
47
|
+
export function parseBody(body) {
|
|
48
|
+
if (!body) return { ok: false, error: '没有正文' };
|
|
49
|
+
let r = parsed.get(body);
|
|
50
|
+
if (!r) parsed.set(body, (r = tryJson(body.text)));
|
|
51
|
+
return r;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const respSig = (r) => (r ? `${r.status}|${r.end}|${r.ended_ms}|${r.body?.bytes}|${r.events?.length}|${r.error}` : 'none');
|
|
55
|
+
|
|
56
|
+
function paneSig(stage, d) {
|
|
57
|
+
if (!stage) return 'missing';
|
|
58
|
+
switch (stage.type) {
|
|
59
|
+
case 'request': return `${stage.msg?.body?.bytes}|${stage.call?.error ?? ''}|${stage.call?.response ? 1 : 0}`;
|
|
60
|
+
case 'response': return `${respSig(stage.resp)}|${stage.call?.error ?? ''}|${d.annotations?.length}`;
|
|
61
|
+
case 'diff': return 'diff';
|
|
62
|
+
default: return `${d.rev}|${respSig(d.client?.response)}|${(d.upstream ?? []).map((c) => respSig(c.response)).join(',')}|${d.annotations?.length}`;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const respClass = (r, err) => {
|
|
67
|
+
if (err || r?.error || (r?.status ?? 0) >= 400 || r?.end === 'error') return 'bad';
|
|
68
|
+
if (!r) return 'dim';
|
|
69
|
+
if (r.end === 'active') return 'info';
|
|
70
|
+
if (r.end === 'cancelled') return 'warn';
|
|
71
|
+
return 'ok';
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// ---------- small renderers ----------
|
|
75
|
+
|
|
76
|
+
function valueCell(v) {
|
|
77
|
+
if (v === null || v === undefined) return h('span', { class: 'null' }, String(v));
|
|
78
|
+
if (typeof v === 'object') {
|
|
79
|
+
const s = JSON.stringify(v);
|
|
80
|
+
return s.length <= 200 ? h('span', { class: 'mono' }, s) : jsonTree(v, { auto: 1 });
|
|
81
|
+
}
|
|
82
|
+
if (typeof v === 'string' && v.length > 300) return strBlock(v);
|
|
83
|
+
return h('span', { class: `mono ${typeof v}` }, typeof v === 'string' ? v : String(v));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Generic key/value table of every field of an object, with Chinese hints for known keys. */
|
|
87
|
+
function fieldsTable(obj, hints, skip = []) {
|
|
88
|
+
const rows = [];
|
|
89
|
+
for (const [k, v] of Object.entries(obj ?? {})) {
|
|
90
|
+
if (skip.includes(k)) continue;
|
|
91
|
+
rows.push([h('span', null, hints[k] ? `${hints[k]} ` : '', h('span', { class: 'mono dim' }, k)), valueCell(v)]);
|
|
92
|
+
}
|
|
93
|
+
return kvTable(rows, 'fields-kv');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Renders server-reported input changes separately from local replay annotations. */
|
|
97
|
+
function serverTransformations(resp) {
|
|
98
|
+
const reports = inputTransformationReports(resp);
|
|
99
|
+
if (!reports.length) return null;
|
|
100
|
+
return h('section', { class: 'server-transformations' },
|
|
101
|
+
h('div', { class: 'note' },
|
|
102
|
+
'上游服务端报告 · input_transformations。与 bridge 本地回放过滤分开记录;path 指向本次上游请求。丢弃 thinking 可能损失推理上下文,允许失配不代表已丢弃。'),
|
|
103
|
+
reports.map(report => {
|
|
104
|
+
const where = `${report.event === null ? 'JSON 正文' : `SSE 事件 #${report.event}`} ${report.pointer}`;
|
|
105
|
+
const entries = Array.isArray(report.value) ? report.value : null;
|
|
106
|
+
return h('div', { class: 'transformation-report' },
|
|
107
|
+
h('div', { class: 'sub-title mono' }, where),
|
|
108
|
+
entries?.length === 0 ? h('div', { class: 'dim' }, '服务端报告为空 []') : null,
|
|
109
|
+
entries ? entries.map(entry => h('details', { class: 'transformation-entry' },
|
|
110
|
+
h('summary', null,
|
|
111
|
+
h('span', { class: `chip ${entry?.type === 'thinking_dropped' ? 'warn' : 'info'}` },
|
|
112
|
+
INPUT_TRANSFORMATION_LABEL[entry?.type] ?? entry?.type ?? '未知变换'),
|
|
113
|
+
' ', h('span', { class: 'mono' }, [entry?.path, entry?.reason].filter(x => x != null).map(x => typeof x === 'string' ? x : JSON.stringify(x)).join(' · '))),
|
|
114
|
+
jsonTree(entry, { auto: 2 }))) : jsonTree(report.value, { auto: 2 }));
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const ROUTE_HINTS = {
|
|
119
|
+
alias: '别名', catalog: '目录模型', adapter: '适配器', api: '上游 API', upstream: '上游配置', base_url: 'base_url', upstream_model: '上游模型',
|
|
120
|
+
origin_id: '来源指纹', reasoning_format: 'reasoning 格式', native_compaction_format: '原生压缩格式', compaction_mode: '压缩模式',
|
|
121
|
+
native_replay: '回放原生压缩', replay: 'replay 白名单', responses_lite: 'responses_lite',
|
|
122
|
+
};
|
|
123
|
+
const SUMMARY_HINTS = {
|
|
124
|
+
id: 'ID', seq: '序号', rev: '版本', started_at: '开始', duration_ms: '耗时', active: '进行中', model: '模型别名', adapter: '适配器', api: 'API',
|
|
125
|
+
upstream: '上游配置', upstream_model: '上游模型', kind: '类型', stream: '流式', status: '状态码', outcome: '结果', error: '错误', usage: '用量',
|
|
126
|
+
thread: '线程', input_items: 'input 项数', preview: '预览', upstream_calls: '上游调用', bytes: '大小',
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
function headersTable(headers, first) {
|
|
130
|
+
const rows = first ? [...first] : [];
|
|
131
|
+
for (const [k, v] of Object.entries(headers ?? {})) {
|
|
132
|
+
const s = Array.isArray(v) ? v.join(', ') : String(v);
|
|
133
|
+
rows.push([h('span', { class: 'mono' }, k), /^\[redacted/.test(s) ? h('span', { class: 'chip warn mono' }, s) : h('span', { class: 'mono hval' }, s)]);
|
|
134
|
+
}
|
|
135
|
+
if (!rows.length) return h('div', { class: 'dim' }, '没有头部');
|
|
136
|
+
return kvTable(rows, 'headers');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function rawView(text, name, isJson, ext) {
|
|
140
|
+
let full = false;
|
|
141
|
+
let pretty = false;
|
|
142
|
+
let prettyText = null;
|
|
143
|
+
const pre = h('pre', { class: 'raw' });
|
|
144
|
+
const info = h('span', { class: 'dim' });
|
|
145
|
+
const more = btn('显示全部', () => { full = true; render(); });
|
|
146
|
+
const render = () => {
|
|
147
|
+
if (pretty && prettyText == null) prettyText = JSON.stringify(JSON.parse(text), null, 2);
|
|
148
|
+
const s = pretty ? prettyText : text;
|
|
149
|
+
const cut = !full && s.length > RAW_LIMIT;
|
|
150
|
+
pre.textContent = cut ? s.slice(0, RAW_LIMIT) : s;
|
|
151
|
+
info.textContent = `${fmtInt(s.length)} 字符${cut ? ` · 仅显示前 ${fmtInt(RAW_LIMIT)}` : ''}`;
|
|
152
|
+
more.hidden = !cut;
|
|
153
|
+
};
|
|
154
|
+
const tools = h('div', { class: 'sv-tools' }, info, more,
|
|
155
|
+
isJson ? h('label', { class: 'check' }, h('input', { type: 'checkbox', onchange: (e) => { pretty = e.target.checked; render(); } }), '格式化 JSON') : null,
|
|
156
|
+
btn('复制', () => copyText(text)), btn('下载', () => download(`${name}.${ext}`, text, 'text/plain')));
|
|
157
|
+
render();
|
|
158
|
+
return h('div', { class: 'rawbox' }, tools, pre);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Annotation pointers usable in the structure view of the client response. */
|
|
162
|
+
function responsePointers(d) {
|
|
163
|
+
const resp = d.client?.response;
|
|
164
|
+
const { byPointer, byEvent } = annIndex(d.annotations, 'client.response');
|
|
165
|
+
if (!byEvent.size) return byPointer;
|
|
166
|
+
const events = parseEvents(resp) ?? [];
|
|
167
|
+
const out = new Map(byPointer);
|
|
168
|
+
for (const [i, list] of byEvent) {
|
|
169
|
+
const oi = events[i]?.json?.output_index;
|
|
170
|
+
if (oi == null) continue;
|
|
171
|
+
for (const a of list) {
|
|
172
|
+
const rest = (a.pointer ?? '').replace(/^\/item/, '');
|
|
173
|
+
const p = `/output/${oi}${rest}`;
|
|
174
|
+
out.set(p, [...(out.get(p) ?? []), a]);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ---------- the view ----------
|
|
181
|
+
|
|
182
|
+
export class DetailView {
|
|
183
|
+
/** ctx: { prefs, savePrefs(), getDetail(id), summaries(), summary(id), live(), select(id), refresh() } */
|
|
184
|
+
constructor(root, ctx) {
|
|
185
|
+
this.root = root;
|
|
186
|
+
this.ctx = ctx;
|
|
187
|
+
this.detail = null;
|
|
188
|
+
this.states = new Map();
|
|
189
|
+
this.diffSel = new Map();
|
|
190
|
+
this.panes = [{ key: ctx.prefs.pane ?? 'req', el: h('section', { class: 'pane' }), sig: null }];
|
|
191
|
+
this.headEl = h('div', { class: 'd-head' });
|
|
192
|
+
this.navEl = h('nav', { class: 'pipe' });
|
|
193
|
+
this.panesEl = h('div', { class: 'panes' }, this.panes[0].el);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
clear(message) {
|
|
197
|
+
this.detail = null;
|
|
198
|
+
this.root.replaceChildren(h('div', { class: 'empty' }, message));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Shows a detail; same exchange re-renders only panes whose data changed. */
|
|
202
|
+
show(d, { gone = false } = {}) {
|
|
203
|
+
const same = this.detail?.id === d.id;
|
|
204
|
+
this.detail = d;
|
|
205
|
+
this.gone = gone;
|
|
206
|
+
if (!this.root.contains(this.panesEl)) this.root.replaceChildren(this.headEl, this.navEl, this.panesEl);
|
|
207
|
+
const stages = stagesOf(d);
|
|
208
|
+
this.stages = new Map(stages.map((s) => [s.key, s]));
|
|
209
|
+
for (const p of this.panes) if (!this.stages.has(p.key)) p.key = this.fallbackKey(p.key);
|
|
210
|
+
this.renderHead(d);
|
|
211
|
+
this.renderNav(stages);
|
|
212
|
+
for (const p of this.panes) {
|
|
213
|
+
const sig = `${d.id}|${p.key}|${paneSig(this.stages.get(p.key), d)}`;
|
|
214
|
+
if (!same || sig !== p.sig) this.renderPane(p, same);
|
|
215
|
+
p.sig = sig;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
fallbackKey(key) {
|
|
220
|
+
const [kind, label] = key.split(':');
|
|
221
|
+
if (label) {
|
|
222
|
+
const base = label.replace(/#\d+$/, '');
|
|
223
|
+
const hit = [...this.stages.keys()].find((k) => k.startsWith(`${kind}:${base}`)) ?? [...this.stages.keys()].find((k) => k.startsWith(`${kind}:`));
|
|
224
|
+
if (hit) return hit;
|
|
225
|
+
return kind === 'upres' ? 'res' : 'req';
|
|
226
|
+
}
|
|
227
|
+
return 'req';
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
open(key, { second = false, mode } = {}) {
|
|
231
|
+
if (!this.detail) return;
|
|
232
|
+
if (mode) this.setMode(this.stages.get(key), mode);
|
|
233
|
+
if (second) {
|
|
234
|
+
if (!this.panes[1]) {
|
|
235
|
+
this.panes.push({ key, el: h('section', { class: 'pane' }), sig: null });
|
|
236
|
+
put(this.panesEl, this.panes[1].el);
|
|
237
|
+
this.panesEl.classList.add('split');
|
|
238
|
+
}
|
|
239
|
+
this.panes[1].key = key;
|
|
240
|
+
this.panes[1].sig = null;
|
|
241
|
+
} else {
|
|
242
|
+
this.panes[0].key = key;
|
|
243
|
+
this.panes[0].sig = null;
|
|
244
|
+
this.ctx.prefs.pane = key;
|
|
245
|
+
this.ctx.savePrefs();
|
|
246
|
+
}
|
|
247
|
+
this.show(this.detail, { gone: this.gone });
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
closeSecond() {
|
|
251
|
+
const p = this.panes.pop();
|
|
252
|
+
p?.el.remove();
|
|
253
|
+
this.panesEl.classList.remove('split');
|
|
254
|
+
this.renderNav(stagesOf(this.detail));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
viewState(key, mode, make) {
|
|
258
|
+
const k = `${this.detail.id}|${key}|${mode}`;
|
|
259
|
+
let st = this.states.get(k);
|
|
260
|
+
if (st) { this.states.delete(k); this.states.set(k, st); return st; }
|
|
261
|
+
st = make();
|
|
262
|
+
this.states.set(k, st);
|
|
263
|
+
if (this.states.size > VIEW_STATES) this.states.delete(this.states.keys().next().value);
|
|
264
|
+
return st;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
modeOf(stage) {
|
|
268
|
+
const modes = this.ctx.prefs.modes;
|
|
269
|
+
let m = modes[stage.type] ?? (stage.type === 'response' ? 'events' : 'struct');
|
|
270
|
+
if (m === 'events' && !stage.resp?.events) m = 'struct';
|
|
271
|
+
return m;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
setMode(stage, mode) {
|
|
275
|
+
if (stage) this.ctx.prefs.modes[stage.type] = mode;
|
|
276
|
+
this.ctx.savePrefs();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ---------- header and pipeline ----------
|
|
280
|
+
|
|
281
|
+
renderHead(d) {
|
|
282
|
+
const th = d.thread ?? {};
|
|
283
|
+
const usage = d.usage ? usageText(d.usage) : '';
|
|
284
|
+
const actions = h('span', { class: 'd-actions' },
|
|
285
|
+
btn('导出此条', () => {
|
|
286
|
+
download(`codeks-bridge-${safeName(d.id)}.json`, JSON.stringify(exportDoc([d]), null, 1));
|
|
287
|
+
}, '', '下载这条记录(可在“导入”中离线查看)'),
|
|
288
|
+
btn('复制 ID', () => copyText(d.id)),
|
|
289
|
+
this.ctx.live() ? btn('刷新', () => this.ctx.refresh(), '', '重新获取这条记录') : null);
|
|
290
|
+
const threadChips = [
|
|
291
|
+
th.thread_id ? h('span', { class: 'chip sm id', title: `thread_id ${th.thread_id}` }, `线程 ${shortId(th.thread_id, 12)}`) : null,
|
|
292
|
+
th.session_id && th.session_id !== th.thread_id ? h('span', { class: 'chip sm id', title: `session_id ${th.session_id}` }, `会话 ${shortId(th.session_id, 12)}`) : null,
|
|
293
|
+
th.parent_thread_id ? h('span', { class: 'chip sm dim', title: `x-codex-parent-thread-id ${th.parent_thread_id}` }, `父线程 ${shortId(th.parent_thread_id, 12)}`) : null,
|
|
294
|
+
th.subagent ? h('span', { class: 'chip sm sub', title: 'x-openai-subagent' }, `子代理 ${th.subagent}`) : null,
|
|
295
|
+
th.window_id ? h('span', { class: 'chip sm dim', title: `x-codex-window-id ${th.window_id}` }, `窗口 ${shortId(th.window_id, 12)}`) : null,
|
|
296
|
+
];
|
|
297
|
+
fill(this.headEl,
|
|
298
|
+
h('div', { class: 'd-line' },
|
|
299
|
+
h('span', { class: 'd-seq' }, `#${d.seq ?? '?'}`),
|
|
300
|
+
h('span', { class: 'mono dim', title: 'exchange id' }, d.id),
|
|
301
|
+
h('span', { class: 'mono' }, fmtDateTime(d.started_at)),
|
|
302
|
+
h('span', { class: `chip kind k-${d.kind}` }, KIND_LABEL[d.kind] ?? d.kind),
|
|
303
|
+
h('span', { class: 'd-model' }, d.model ?? '(无模型)', d.adapter ? ` → ${d.adapter}` : '', d.upstream_model ? h('span', { class: 'dim' }, ` (${d.upstream_model})`) : null),
|
|
304
|
+
h('span', { class: `chip ${OUTCOME_CLASS[d.outcome] ?? 'dim'}` }, `${OUTCOME_LABEL[d.outcome] ?? d.outcome}${d.status != null ? ` · ${d.status}` : ''}`),
|
|
305
|
+
h('span', { class: 'mono' }, d.active ? '进行中…' : fmtDur(d.duration_ms)),
|
|
306
|
+
usage ? h('span', { class: 'mono dim', title: JSON.stringify(d.usage, null, 1) }, usage) : null,
|
|
307
|
+
this.gone ? h('span', { class: 'chip warn', title: '已从 bridge 清空或淘汰;这里显示的是浏览器缓存' }, '已从 bridge 移除') : null,
|
|
308
|
+
h('span', { class: 'grow' }), actions),
|
|
309
|
+
threadChips.some(Boolean) || d.error ? h('div', { class: 'd-line' }, threadChips, d.error ? h('span', { class: 'err' }, d.error) : null) : null);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
renderNav(stages) {
|
|
313
|
+
const sel0 = this.panes[0]?.key;
|
|
314
|
+
const sel1 = this.panes[1]?.key;
|
|
315
|
+
const card = (s, main, sub, cls = '', tip = sub) => {
|
|
316
|
+
const el = h('button', {
|
|
317
|
+
type: 'button', class: `card ${cls}${s.key === sel0 ? ' sel' : ''}${s.key === sel1 ? ' sel2' : ''}`, 'data-key': s.key,
|
|
318
|
+
title: `${tip ? `${tip}\n` : ''}Shift+点击:在右侧并排打开`,
|
|
319
|
+
onclick: (e) => this.open(s.key, { second: e.shiftKey }),
|
|
320
|
+
}, h('span', { class: 'card-main' }, main), sub ? h('span', { class: 'card-sub' }, sub) : null);
|
|
321
|
+
return el;
|
|
322
|
+
};
|
|
323
|
+
const d = this.detail;
|
|
324
|
+
const get = (k) => stages.find((s) => s.key === k);
|
|
325
|
+
const req = get('req');
|
|
326
|
+
const res = get('res');
|
|
327
|
+
const reqSub = [fmtBytes(req.msg?.body?.bytes), d.input_items != null ? `${d.input_items} 项` : null].filter(Boolean).join(' · ');
|
|
328
|
+
const respSub = (r, err) => (err ? '传输错误' : r ? [r.status ?? '–', END_LABEL[r.end] ?? r.end, r.events ? `${fmtInt(r.events.length)} 事件` : fmtBytes(r.body?.bytes)].join(' · ') : '无响应');
|
|
329
|
+
const calls = stages.filter((s) => s.key.startsWith('up:'));
|
|
330
|
+
const callBox = h('div', { class: 'pipe-calls' }, calls.length ? calls.map((s) => {
|
|
331
|
+
const r = get(`upres:${s.key.slice(3)}`);
|
|
332
|
+
return h('div', { class: 'pipe-call' },
|
|
333
|
+
h('span', { class: `call-label l-${s.call.label}` }, s.sub),
|
|
334
|
+
card(s, `② bridge → 上游`, `${s.call.method ?? ''} ${fmtBytes(s.msg?.body?.bytes)}`),
|
|
335
|
+
h('span', { class: 'arrow' }, '→'),
|
|
336
|
+
card(r, `③ 上游 → bridge`, respSub(r.resp, s.call.error), respClass(r.resp, s.call.error)));
|
|
337
|
+
}) : h('span', { class: 'dim' }, '(没有上游调用)'));
|
|
338
|
+
const annCount = d.annotations?.length ?? 0;
|
|
339
|
+
const routeSub = d.route ? `${d.route.alias ?? ''} · ${d.route.compaction_mode ?? ''}` : '无路由';
|
|
340
|
+
fill(this.navEl,
|
|
341
|
+
h('div', { class: 'pipe-side' }, card(get('overview'), '概要', null, 'mini', routeSub), card(get('diff'), '对比', null, 'mini', '与同线程上一轮或任选两条比较')),
|
|
342
|
+
card(req, '① codeks → bridge', reqSub),
|
|
343
|
+
h('span', { class: 'arrow' }, '→'), callBox, h('span', { class: 'arrow' }, '→'),
|
|
344
|
+
card(res, '④ bridge → codeks', respSub(res.resp), respClass(res.resp)),
|
|
345
|
+
h('span', { class: 'grow' }),
|
|
346
|
+
card(get('ann'), `标注 ${annCount}`, annCount ? annSummary(d.annotations) : '无', `narrow${annCount ? ' info' : ''}`),
|
|
347
|
+
this.panes[1] ? btn('✕ 关闭并排', () => this.closeSecond(), 'close2') : null);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ---------- panes ----------
|
|
351
|
+
|
|
352
|
+
renderPane(p, keepScroll) {
|
|
353
|
+
const body = p.el.querySelector('.pane-body');
|
|
354
|
+
const scroll = keepScroll && body ? body.scrollTop : 0;
|
|
355
|
+
const stage = this.stages.get(p.key);
|
|
356
|
+
let content;
|
|
357
|
+
let head;
|
|
358
|
+
try {
|
|
359
|
+
if (stage.type === 'overview') [head, content] = [this.paneHead(stage, p), this.overview()];
|
|
360
|
+
else if (stage.type === 'ann') [head, content] = [this.paneHead(stage, p), this.annotations()];
|
|
361
|
+
else if (stage.type === 'diff') [head, content] = [this.paneHead(stage, p), this.diffPane()];
|
|
362
|
+
else {
|
|
363
|
+
const mode = this.modeOf(stage);
|
|
364
|
+
head = this.paneHead(stage, p, mode);
|
|
365
|
+
content = this.stageBody(stage, mode);
|
|
366
|
+
}
|
|
367
|
+
} catch (e) {
|
|
368
|
+
console.error(e);
|
|
369
|
+
content = h('div', { class: 'err' }, `渲染失败:${e.message}`);
|
|
370
|
+
}
|
|
371
|
+
const nb = h('div', { class: 'pane-body' }, content);
|
|
372
|
+
p.el.replaceChildren(head ?? h('div'), nb);
|
|
373
|
+
if (scroll) nb.scrollTop = scroll;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
paneHead(stage, p, mode) {
|
|
377
|
+
const title = stage.num ? `${stage.num} ${stage.title} · ${stage.sub}` : stage.title;
|
|
378
|
+
const head = h('div', { class: 'pane-head' }, h('span', { class: 'pane-title' }, title));
|
|
379
|
+
if (mode) {
|
|
380
|
+
const modes = stage.type === 'request' ? REQUEST_MODES : RESPONSE_MODES.filter(([m]) => m !== 'events' || stage.resp?.events);
|
|
381
|
+
put(head, h('span', { class: 'tabs' }, modes.map(([m, label]) => h('button', {
|
|
382
|
+
type: 'button', class: `tab${m === mode ? ' on' : ''}`,
|
|
383
|
+
onclick: () => { this.setMode(stage, m); for (const x of this.panes) if (this.stages.get(x.key)?.type === stage.type) { x.sig = null; this.renderPane(x, false); } },
|
|
384
|
+
}, label))));
|
|
385
|
+
put(head, this.stageInfo(stage));
|
|
386
|
+
}
|
|
387
|
+
if (p === this.panes[1]) put(head, h('span', { class: 'grow' }), btn('✕', () => this.closeSecond(), '', '关闭并排'));
|
|
388
|
+
return head;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
stageInfo(stage) {
|
|
392
|
+
const out = h('span', { class: 'pane-info' });
|
|
393
|
+
const body = stage.type === 'request' ? stage.msg?.body : stage.resp?.body;
|
|
394
|
+
if (stage.type === 'request') {
|
|
395
|
+
const m = stage.msg;
|
|
396
|
+
if (m) put(out, h('span', { class: 'mono', title: m.url ?? m.path }, `${m.method ?? ''} ${oneLine(m.url ?? m.path ?? '', 80)}`));
|
|
397
|
+
if (stage.call?.started_ms != null) put(out, h('span', { class: 'dim' }, `开始 +${fmtDur(stage.call.started_ms)}`));
|
|
398
|
+
if (stage.call?.error) put(out, h('span', { class: 'chip bad' }, `传输错误:${stage.call.error}`));
|
|
399
|
+
} else {
|
|
400
|
+
const r = stage.resp;
|
|
401
|
+
if (!r) put(out, h('span', { class: 'dim' }, stage.call?.error ? `传输错误:${stage.call.error}` : '尚无响应'));
|
|
402
|
+
else {
|
|
403
|
+
put(out, h('span', { class: `chip ${respClass(r)}` }, `${r.status ?? '–'} ${r.status_text ?? ''}`.trim()),
|
|
404
|
+
h('span', { class: `chip ${r.end === 'active' ? 'info' : 'dim'}`, title: `end: ${r.end}` }, END_LABEL[r.end] ?? r.end ?? '–'),
|
|
405
|
+
r.headers_ms != null ? h('span', { class: 'dim', title: '自请求开始到响应头的时间' }, `首包 ${fmtDur(r.headers_ms)}`) : null,
|
|
406
|
+
r.ended_ms != null ? h('span', { class: 'dim', title: '自请求开始到结束的时间' }, `结束 ${fmtDur(r.ended_ms)}`) : null,
|
|
407
|
+
r.events ? h('span', { class: 'dim' }, `${fmtInt(r.events.length)} 事件`) : h('span', { class: 'dim' }, '非 SSE'),
|
|
408
|
+
r.error ? h('span', { class: 'chip bad', title: r.error }, oneLine(`错误:${r.error}`, 80)) : null);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if (body) {
|
|
412
|
+
put(out, h('span', { class: 'dim', title: `${fmtInt(body.bytes)} 字节` }, fmtBytes(body.bytes)));
|
|
413
|
+
put(out, btn('复制正文', () => copyText(body.text)), btn('下载正文', () => download(this.fileName(stage, stage.resp?.events ? 'sse.txt' : 'json'), body.text, 'text/plain')));
|
|
414
|
+
}
|
|
415
|
+
return out;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
fileName(stage, ext) {
|
|
419
|
+
return `${safeName(this.detail.id)}-${safeName(stage.key.replace(':', '-'))}.${ext}`;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
stageBody(stage, mode) {
|
|
423
|
+
const d = this.detail;
|
|
424
|
+
const isReq = stage.type === 'request';
|
|
425
|
+
const msg = stage.msg;
|
|
426
|
+
const resp = stage.resp;
|
|
427
|
+
const body = isReq ? msg?.body : resp?.body;
|
|
428
|
+
if (!isReq && !resp) {
|
|
429
|
+
return h('div', { class: 'empty small' }, stage.call?.error ? `上游传输错误,没有响应:${stage.call.error}` : d.active ? '等待响应…' : '没有响应');
|
|
430
|
+
}
|
|
431
|
+
if (!isReq && mode === 'headers') {
|
|
432
|
+
return headersTable(resp.headers, [['状态', h('span', { class: 'mono' }, `${resp.status ?? '–'} ${resp.status_text ?? ''}`)]]);
|
|
433
|
+
}
|
|
434
|
+
if (isReq && mode === 'headers') {
|
|
435
|
+
return headersTable(msg?.headers, [['请求', h('span', { class: 'mono' }, `${msg?.method ?? ''} ${msg?.url ?? msg?.path ?? ''}`)]]);
|
|
436
|
+
}
|
|
437
|
+
if (mode === 'raw') return body ? rawView(body.text, this.fileName(stage, 'body'), parseBody(body).ok, resp?.events ? 'sse.txt' : 'json') : h('div', { class: 'dim' }, '没有正文');
|
|
438
|
+
const ann = stage.target === 'client.request' ? annIndex(d.annotations, 'client.request').byPointer
|
|
439
|
+
: stage.target === 'client.response' ? responsePointers(d) : null;
|
|
440
|
+
const chip = (a, onClick) => annChip(a, onClick);
|
|
441
|
+
if (mode === 'events' && resp?.events) {
|
|
442
|
+
const { byEvent } = annIndex(d.annotations, stage.target === 'client.response' ? 'client.response' : '-');
|
|
443
|
+
return eventsView(resp, {
|
|
444
|
+
state: this.viewState(stage.key, 'events', newEventsState), prefs: this.ctx.prefs,
|
|
445
|
+
onPrefs: () => { this.ctx.savePrefs(); for (const p of this.panes) { if (this.modeOf(this.stages.get(p.key) ?? {}) === 'events') this.renderPane(p, true); } },
|
|
446
|
+
annByEvent: byEvent, annChip: chip, annDetail,
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
if (mode === 'json') {
|
|
450
|
+
const st = this.viewState(stage.key, 'json', () => ({ exp: new Set() }));
|
|
451
|
+
if (resp?.events) {
|
|
452
|
+
const events = parseEvents(resp).map((e) => ({ t: e.t, event: e.event, data: e.json !== undefined ? e.json : e.data }));
|
|
453
|
+
return h('div', null, h('div', { class: 'note' }, `全部 ${fmtInt(events.length)} 个 SSE 事件(data 已解析为 JSON)`), jsonTree(events, { state: st, label: 'events', auto: 1 }));
|
|
454
|
+
}
|
|
455
|
+
const r = parseBody(body);
|
|
456
|
+
if (!r.ok) return h('div', null, h('div', { class: 'note' }, `正文不是合法 JSON:${r.error}`), strBlock(body?.text ?? ''));
|
|
457
|
+
return jsonTree(r.value, { state: st, ann, annChip: chip, label: 'body', auto: 1 });
|
|
458
|
+
}
|
|
459
|
+
// Structure view.
|
|
460
|
+
const st = this.viewState(stage.key, 'struct', newStructState);
|
|
461
|
+
if (isReq) {
|
|
462
|
+
const r = parseBody(body);
|
|
463
|
+
if (!r.ok) return h('div', null, h('div', { class: 'note' }, `正文不是合法 JSON:${r.error}`), strBlock(body?.text ?? ''));
|
|
464
|
+
return structureView(r.value, { state: st, ann, annChip: chip });
|
|
465
|
+
}
|
|
466
|
+
const fo = finalObject(resp);
|
|
467
|
+
return h('div', null,
|
|
468
|
+
stage.call ? serverTransformations(resp) : null,
|
|
469
|
+
structureView(fo.value, { state: st, ann, annChip: chip, note: fo.note }));
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// ---------- overview ----------
|
|
473
|
+
|
|
474
|
+
overview() {
|
|
475
|
+
const d = this.detail;
|
|
476
|
+
const box = h('div', { class: 'overview' });
|
|
477
|
+
put(box, h('h4', null, '路由'), d.route ? fieldsTable(d.route, ROUTE_HINTS) : h('div', { class: 'dim' }, '没有路由信息(请求无法解析或路由失败)'));
|
|
478
|
+
put(box, h('h4', null, '时间线'), this.timeline());
|
|
479
|
+
const keys = callKeys(d.upstream);
|
|
480
|
+
if (d.upstream?.length) {
|
|
481
|
+
put(box, h('h4', null, `上游调用(${d.upstream.length})`), h('table', { class: 'calls' },
|
|
482
|
+
h('thead', null, h('tr', null, ['标签', '请求', '状态', '结束', '开始', '首包', '结束于', '请求大小', '响应大小', '错误'].map((x) => h('th', null, x)))),
|
|
483
|
+
h('tbody', null, d.upstream.map((c, i) => {
|
|
484
|
+
const r = c.response;
|
|
485
|
+
const tr = h('tr', { class: 'clickable', title: '打开该调用的上游响应', onclick: () => this.open(`upres:${keys[i]}`) },
|
|
486
|
+
h('td', null, h('span', { class: `call-label l-${c.label}` }, callLabel(c.label))),
|
|
487
|
+
h('td', { class: 'mono' }, `${c.method ?? ''} ${oneLine(c.url ?? '', 70)}`),
|
|
488
|
+
h('td', null, h('span', { class: `chip ${respClass(r, c.error)}` }, r?.status ?? '–')),
|
|
489
|
+
h('td', null, END_LABEL[r?.end] ?? r?.end ?? '–'),
|
|
490
|
+
h('td', { class: 'mono' }, c.started_ms != null ? `+${fmtDur(c.started_ms)}` : '–'),
|
|
491
|
+
h('td', { class: 'mono' }, r?.headers_ms != null ? fmtDur(r.headers_ms) : '–'),
|
|
492
|
+
h('td', { class: 'mono' }, r?.ended_ms != null ? fmtDur(r.ended_ms) : '–'),
|
|
493
|
+
h('td', { class: 'mono' }, fmtBytes(c.body?.bytes)),
|
|
494
|
+
h('td', { class: 'mono' }, fmtBytes(r?.body?.bytes)),
|
|
495
|
+
h('td', { class: 'err' }, c.error ?? r?.error ?? ''));
|
|
496
|
+
return tr;
|
|
497
|
+
}))));
|
|
498
|
+
}
|
|
499
|
+
if (d.annotations?.length) {
|
|
500
|
+
put(box, h('h4', null, '标注'), h('div', null, h('span', { class: 'chip info' }, annSummary(d.annotations)), ' ', btn('查看全部标注', () => this.open('ann'))));
|
|
501
|
+
}
|
|
502
|
+
for (const [i, call] of (d.upstream ?? []).entries()) {
|
|
503
|
+
const reports = serverTransformations(call.response);
|
|
504
|
+
if (reports) {
|
|
505
|
+
put(box, h('h4', null, `服务端输入变换 · ${callLabel(call.label)} (${keys[i]})`),
|
|
506
|
+
btn('查看上游响应', () => this.open(`upres:${keys[i]}`)), reports);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
put(box, h('h4', null, '记录字段'), fieldsTable(d, SUMMARY_HINTS, ['route', 'client', 'upstream', 'annotations']));
|
|
510
|
+
return box;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
timeline() {
|
|
514
|
+
const d = this.detail;
|
|
515
|
+
const rows = [];
|
|
516
|
+
const keys = callKeys(d.upstream);
|
|
517
|
+
const cr = d.client?.response;
|
|
518
|
+
(d.upstream ?? []).forEach((c, i) => rows.push({ label: callLabel(c.label), start: c.started_ms ?? 0, head: c.response?.headers_ms, end: c.response?.ended_ms, cls: respClass(c.response, c.error), key: `upres:${keys[i]}` }));
|
|
519
|
+
rows.push({ label: 'bridge → codeks', start: 0, head: cr?.headers_ms, end: cr?.ended_ms, cls: respClass(cr), key: 'res' });
|
|
520
|
+
const now = d.active ? Date.now() - d.started_at : 0;
|
|
521
|
+
const total = Math.max(1, d.duration_ms ?? 0, now, ...rows.map((r) => r.end ?? r.head ?? r.start));
|
|
522
|
+
const pct = (v) => `${Math.max(0, Math.min(100, (v / total) * 100)).toFixed(2)}%`;
|
|
523
|
+
return h('div', { class: 'timeline' }, rows.map((r) => {
|
|
524
|
+
const end = r.end ?? (d.active ? total : r.head ?? r.start);
|
|
525
|
+
const bar = h('span', { class: `tl-bar ${r.cls}` });
|
|
526
|
+
bar.style.left = pct(r.start);
|
|
527
|
+
bar.style.width = pct(Math.max(end - r.start, total / 400));
|
|
528
|
+
const mark = r.head != null ? h('span', { class: 'tl-head', title: `首包 ${fmtDur(r.head)}` }) : null;
|
|
529
|
+
if (mark) mark.style.left = pct(r.head);
|
|
530
|
+
return h('div', { class: 'tl-row clickable', onclick: () => this.open(r.key) },
|
|
531
|
+
h('span', { class: 'tl-label' }, r.label),
|
|
532
|
+
h('span', { class: 'tl-track' }, bar, mark),
|
|
533
|
+
h('span', { class: 'tl-num mono' }, `${fmtDur(r.start)} → ${r.head != null ? fmtDur(r.head) : '–'} → ${r.end != null ? fmtDur(r.end) : d.active ? '…' : '–'}`));
|
|
534
|
+
}), h('div', { class: 'dim tl-axis' }, `0 → ${fmtDur(total)}(开始 → 首包 → 结束,相对请求开始)`));
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// ---------- annotations ----------
|
|
538
|
+
|
|
539
|
+
annotations() {
|
|
540
|
+
const d = this.detail;
|
|
541
|
+
const list = d.annotations ?? [];
|
|
542
|
+
if (!list.length) return h('div', { class: 'empty small' }, '这条记录没有标注(没有 bridge 载荷的 reasoning 或压缩项)');
|
|
543
|
+
const tbody = h('tbody');
|
|
544
|
+
list.forEach((a, i) => {
|
|
545
|
+
const where = `${a.target === 'client.request' ? '① 请求' : '④ 响应'}${a.event != null ? ` 事件 #${a.event}` : ''} ${a.pointer ?? ''}`;
|
|
546
|
+
const tr = h('tr', { class: 'clickable' },
|
|
547
|
+
h('td', { class: 'mono dim' }, String(i)),
|
|
548
|
+
h('td', { class: 'mono' }, where),
|
|
549
|
+
h('td', null, KIND_TEXT[a.kind] ?? a.kind),
|
|
550
|
+
h('td', null, h('span', { class: `chip ${decisionClass(a.decision)}` }, decisionText(a.decision))),
|
|
551
|
+
h('td', null, reasonText(a), h('span', { class: 'dim mono' }, ` ${a.reason}`)),
|
|
552
|
+
h('td', null, sourceText(a.source) || '–'),
|
|
553
|
+
h('td', { class: 'mono dim' }, a.format ?? '–'),
|
|
554
|
+
h('td', null, btn('定位', () => this.locate(a), '', '在结构/事件视图中定位这一项')));
|
|
555
|
+
const detailRow = h('tr', { class: 'field-detail' }, h('td', { colSpan: 8 }));
|
|
556
|
+
tr.addEventListener('click', (e) => {
|
|
557
|
+
if (e.target.closest('button') || String(window.getSelection?.() ?? '')) return;
|
|
558
|
+
if (detailRow.isConnected) detailRow.remove();
|
|
559
|
+
else { if (!detailRow.firstChild.firstChild) put(detailRow.firstChild, annDetail(a)); tr.after(detailRow); }
|
|
560
|
+
});
|
|
561
|
+
put(tbody, tr);
|
|
562
|
+
});
|
|
563
|
+
return h('div', null,
|
|
564
|
+
h('div', { class: 'note' }, `bridge 按真实回放规则计算:${annSummary(list)}。点击行查看解码后的载荷,“定位”跳到对应项。`),
|
|
565
|
+
h('table', { class: 'anns' }, h('thead', null, h('tr', null, ['#', '位置', '类型', '决定', '原因', '来源', '格式', ''].map((x) => h('th', null, x)))), tbody));
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/** Opens the item or event an annotation points at, expanded and highlighted. */
|
|
569
|
+
locate(a) {
|
|
570
|
+
const isReq = a.target === 'client.request';
|
|
571
|
+
const key = isReq ? 'req' : 'res';
|
|
572
|
+
const stage = this.stages.get(key);
|
|
573
|
+
const mode = !isReq && a.event != null && stage.resp?.events ? 'events' : 'struct';
|
|
574
|
+
let selector;
|
|
575
|
+
if (mode === 'events') {
|
|
576
|
+
const st = this.viewState(key, 'events', newEventsState);
|
|
577
|
+
st.open.add(`e${a.event}`);
|
|
578
|
+
st.type = '';
|
|
579
|
+
st.q = '';
|
|
580
|
+
st.shown = Infinity;
|
|
581
|
+
selector = `[data-ev="${a.event}"]`;
|
|
582
|
+
} else {
|
|
583
|
+
const st = this.viewState(key, 'struct', newStructState);
|
|
584
|
+
st.filters.clear();
|
|
585
|
+
let pointer = a.pointer;
|
|
586
|
+
if (!isReq && a.event != null) {
|
|
587
|
+
const oi = parseEvents(stage.resp)?.[a.event]?.json?.output_index;
|
|
588
|
+
pointer = oi != null ? `/output/${oi}` : null;
|
|
589
|
+
}
|
|
590
|
+
const row = focusPointer(st, pointer);
|
|
591
|
+
selector = row ? `[data-ptr="${CSS.escape(row)}"]` : null;
|
|
592
|
+
}
|
|
593
|
+
this.open(key, { mode });
|
|
594
|
+
const pane = this.panes[0];
|
|
595
|
+
requestAnimationFrame(() => {
|
|
596
|
+
const el = selector && pane.el.querySelector(selector);
|
|
597
|
+
if (!el) { toast('没有找到对应的项'); return; }
|
|
598
|
+
el.scrollIntoView({ block: 'center' });
|
|
599
|
+
el.classList.add('flash');
|
|
600
|
+
setTimeout(() => el.classList.remove('flash'), 1800);
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// ---------- diff ----------
|
|
605
|
+
|
|
606
|
+
/** Previous exchange in the same thread, optionally one with an upstream call of that label. */
|
|
607
|
+
previous(d, label) {
|
|
608
|
+
const key = threadKey(d);
|
|
609
|
+
if (!key) return null;
|
|
610
|
+
return [...this.ctx.summaries().values()].filter((s) => s.id !== d.id && threadKey(s) === key && byNewest(d, s) < 0 &&
|
|
611
|
+
(!label || (s.upstream_calls ?? []).some((c) => c.label === label))).sort(byNewest)[0] ?? null;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
diffPane() {
|
|
615
|
+
const d = this.detail;
|
|
616
|
+
let sel = this.diffSel.get(d.id);
|
|
617
|
+
if (!sel) {
|
|
618
|
+
const prev = this.previous(d);
|
|
619
|
+
sel = { a: { id: prev?.id ?? null, stage: 'req' }, b: { id: d.id, stage: 'req' } };
|
|
620
|
+
this.diffSel.set(d.id, sel);
|
|
621
|
+
}
|
|
622
|
+
const out = h('div', { class: 'diff-result' });
|
|
623
|
+
const run = () => this.runDiff(sel, out);
|
|
624
|
+
const side = (name, s) => {
|
|
625
|
+
const exSel = h('select', { class: 'sel ex-sel' });
|
|
626
|
+
const stSel = h('select', { class: 'sel' });
|
|
627
|
+
const fillStages = () => {
|
|
628
|
+
const sum = this.ctx.summary(s.id);
|
|
629
|
+
const opts = stageOptions(sum);
|
|
630
|
+
if (!opts.some(([k]) => k === s.stage)) s.stage = opts[0]?.[0] ?? 'req';
|
|
631
|
+
stSel.replaceChildren(...opts.map(([k, label]) => h('option', { value: k, selected: k === s.stage }, label)));
|
|
632
|
+
};
|
|
633
|
+
const all = [...this.ctx.summaries().values()].sort(byNewest);
|
|
634
|
+
const key = threadKey(d);
|
|
635
|
+
const mk = (x) => h('option', { value: x.id, selected: x.id === s.id }, `#${x.seq ?? '?'} ${fmtTime(x.started_at)} ${x.model ?? '?'} ${KIND_LABEL[x.kind] ?? ''}${x.id === d.id ? '(当前)' : ''}`);
|
|
636
|
+
const same = all.filter((x) => key && threadKey(x) === key);
|
|
637
|
+
const other = all.filter((x) => !key || threadKey(x) !== key);
|
|
638
|
+
put(exSel, h('option', { value: '', selected: !s.id }, '(选择记录)'),
|
|
639
|
+
same.length ? h('optgroup', { label: '同一线程' }, same.map(mk)) : null,
|
|
640
|
+
other.length ? h('optgroup', { label: key ? '其他线程' : '全部' }, other.map(mk)) : null);
|
|
641
|
+
exSel.addEventListener('change', () => { s.id = exSel.value || null; fillStages(); run(); });
|
|
642
|
+
stSel.addEventListener('change', () => { s.stage = stSel.value; run(); });
|
|
643
|
+
fillStages();
|
|
644
|
+
return h('span', { class: 'diff-side' }, h('b', null, name), exSel, stSel);
|
|
645
|
+
};
|
|
646
|
+
const preset = (label, stage, callLbl) => btn(label, () => {
|
|
647
|
+
const prev = this.previous(d, callLbl);
|
|
648
|
+
if (!prev) { toast(callLbl ? `同一线程中没有更早的带“${callLabel(callLbl)}”的记录` : '同一线程中没有更早的记录'); return; }
|
|
649
|
+
sel.a = { id: prev.id, stage };
|
|
650
|
+
sel.b = { id: d.id, stage };
|
|
651
|
+
this.renderPane(this.panes.find((p) => p.key === 'diff') ?? this.panes[0], false);
|
|
652
|
+
});
|
|
653
|
+
const labels = [...new Set((d.upstream ?? []).map((c) => c.label))];
|
|
654
|
+
const tools = h('div', { class: 'diff-tools' },
|
|
655
|
+
side('A', sel.a), btn('⇄', () => { [sel.a, sel.b] = [sel.b, sel.a]; this.renderPane(this.panes.find((p) => p.key === 'diff') ?? this.panes[0], false); }, '', '交换 A 与 B'), side('B', sel.b));
|
|
656
|
+
const presets = h('div', { class: 'diff-tools' }, h('span', { class: 'dim' }, '快捷:'),
|
|
657
|
+
preset('上一轮 · 客户端请求', 'req'),
|
|
658
|
+
labels.map((l) => preset(`上一轮 · 上游请求·${callLabel(l)}`, `up:${l}`, l)),
|
|
659
|
+
preset('上一轮 · 客户端响应', 'res'));
|
|
660
|
+
queueMicrotask(run);
|
|
661
|
+
return h('div', { class: 'diffpane' }, tools, presets, out);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
async runDiff(sel, out) {
|
|
665
|
+
const token = (this.diffToken = (this.diffToken ?? 0) + 1);
|
|
666
|
+
if (!sel.a.id || !sel.b.id) {
|
|
667
|
+
out.replaceChildren(h('div', { class: 'empty small' }, sel.a.id || sel.b.id ? '请选择 A 与 B 两侧的记录' : '同一线程中没有更早的记录;请在上方手动选择 A。'));
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
out.replaceChildren(h('div', { class: 'dim' }, '加载并比较中…'));
|
|
671
|
+
try {
|
|
672
|
+
const [a, b] = await Promise.all([this.diffValue(sel.a), this.diffValue(sel.b)]);
|
|
673
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
674
|
+
if (token !== this.diffToken) return;
|
|
675
|
+
out.replaceChildren(h('div', { class: 'diff-legend dim' }, `A = ${a.label} B = ${b.label}`), diffView(a.value, b.value, { label: 'body' }));
|
|
676
|
+
} catch (e) {
|
|
677
|
+
if (token === this.diffToken) out.replaceChildren(h('div', { class: 'err' }, `无法比较:${e.message}`));
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
async diffValue(s) {
|
|
682
|
+
const det = await this.ctx.getDetail(s.id);
|
|
683
|
+
const stage = stagesOf(det).find((x) => x.key === s.stage);
|
|
684
|
+
if (!stage) throw new Error(`记录 ${s.id} 没有阶段 ${s.stage}`);
|
|
685
|
+
const label = `#${det.seq} ${fmtTime(det.started_at)} ${det.model ?? ''} · ${stage.num} ${stage.sub}`;
|
|
686
|
+
if (stage.type === 'request') {
|
|
687
|
+
const r = parseBody(stage.msg?.body);
|
|
688
|
+
return { label, value: r.ok ? r.value : stage.msg?.body?.text ?? null };
|
|
689
|
+
}
|
|
690
|
+
return { label, value: finalObject(stage.resp)?.value ?? null };
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function stageOptions(sum) {
|
|
695
|
+
if (!sum) return [['req', '① 客户端请求'], ['res', '④ 客户端响应(最终对象)']];
|
|
696
|
+
const keys = callKeys(sum.upstream_calls ?? sum.upstream);
|
|
697
|
+
const out = [['req', '① 客户端请求']];
|
|
698
|
+
keys.forEach((k, i) => {
|
|
699
|
+
const l = callLabel((sum.upstream_calls ?? sum.upstream)[i].label);
|
|
700
|
+
out.push([`up:${k}`, `② 上游请求 · ${l}`], [`upres:${k}`, `③ 上游响应 · ${l}(最终对象)`]);
|
|
701
|
+
});
|
|
702
|
+
out.push(['res', '④ 客户端响应(最终对象)']);
|
|
703
|
+
return out;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function annSummary(list) {
|
|
707
|
+
const counts = new Map();
|
|
708
|
+
for (const a of list) {
|
|
709
|
+
const k = `${KIND_TEXT[a.kind] ?? a.kind} ${decisionText(a.decision)}`;
|
|
710
|
+
counts.set(k, (counts.get(k) ?? 0) + 1);
|
|
711
|
+
}
|
|
712
|
+
return [...counts].map(([k, n]) => `${k} ${n}`).join(' · ');
|
|
713
|
+
}
|