@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/app.js
ADDED
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
// Inspector controller: polling the bridge, offline import, top bar, list and detail wiring.
|
|
2
|
+
import { h, btn, fill, download, fmtBytes, fmtDateTime, toast } from './dom.js';
|
|
3
|
+
import { api, ApiError, getToken, setToken, parseExport, exportDoc, stamp } from './api.js';
|
|
4
|
+
import { ExchangeList, FILTER_CHIPS, byNewest } from './list.js';
|
|
5
|
+
import { DetailView } from './detail.js';
|
|
6
|
+
|
|
7
|
+
const POLL_MS = 1500;
|
|
8
|
+
const ACTIVE_DETAIL_MS = 2000;
|
|
9
|
+
const DETAIL_CACHE = 8;
|
|
10
|
+
const PREFS_KEY = 'codeks-inspector-prefs';
|
|
11
|
+
|
|
12
|
+
const $ = (id) => document.getElementById(id);
|
|
13
|
+
|
|
14
|
+
// ---------- preferences ----------
|
|
15
|
+
|
|
16
|
+
const prefs = Object.assign({ listW: 440, group: false, chips: {}, keepalive: false, merge: true, modes: {}, pane: 'req' },
|
|
17
|
+
(() => { try { return JSON.parse(localStorage.getItem(PREFS_KEY)) ?? {}; } catch { return {}; } })());
|
|
18
|
+
const savePrefs = () => { try { localStorage.setItem(PREFS_KEY, JSON.stringify(prefs)); } catch { /* storage full or disabled */ } };
|
|
19
|
+
|
|
20
|
+
// ---------- store ----------
|
|
21
|
+
|
|
22
|
+
const store = {
|
|
23
|
+
mode: 'live', // 'live' | 'offline'
|
|
24
|
+
offline: null, // {name, exported_at}
|
|
25
|
+
state: null,
|
|
26
|
+
rev: 0,
|
|
27
|
+
startedAt: null,
|
|
28
|
+
map: new Map(), // id -> Summary (offline: Detail)
|
|
29
|
+
cache: new Map(), // id -> {rev, detail, at}
|
|
30
|
+
selected: null,
|
|
31
|
+
selectToken: 0,
|
|
32
|
+
generation: 0, // Invalidates async work when the data source is replaced or reset.
|
|
33
|
+
conn: 'connecting', // 'ok' | 'down' | 'auth' | 'forbidden' | 'connecting'
|
|
34
|
+
connError: '',
|
|
35
|
+
timer: null,
|
|
36
|
+
busy: null, // Generation of the active poll, if any.
|
|
37
|
+
authOpen: false,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const list = new ExchangeList($('list'), $('list-info'), { onSelect: (id) => select(id) });
|
|
41
|
+
list.rerender = () => renderList();
|
|
42
|
+
|
|
43
|
+
const detail = new DetailView($('detail'), {
|
|
44
|
+
prefs,
|
|
45
|
+
savePrefs,
|
|
46
|
+
live: () => store.mode === 'live',
|
|
47
|
+
summaries: () => store.map,
|
|
48
|
+
summary: (id) => store.map.get(id) ?? store.cache.get(id)?.detail ?? null,
|
|
49
|
+
getDetail: (id) => getDetail(id),
|
|
50
|
+
refresh: () => refreshSelected(true),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// ---------- data access ----------
|
|
54
|
+
|
|
55
|
+
async function getDetail(id, fresh = false) {
|
|
56
|
+
const generation = store.generation;
|
|
57
|
+
if (store.mode === 'offline') {
|
|
58
|
+
const d = store.map.get(id);
|
|
59
|
+
if (!d) throw new Error(`导入文件中没有记录 ${id}`);
|
|
60
|
+
return d;
|
|
61
|
+
}
|
|
62
|
+
const sum = store.map.get(id);
|
|
63
|
+
const hit = store.cache.get(id);
|
|
64
|
+
if (hit && !fresh && (!sum || (sum.rev === hit.rev && !sum.active))) {
|
|
65
|
+
store.cache.delete(id);
|
|
66
|
+
store.cache.set(id, hit);
|
|
67
|
+
return hit.detail;
|
|
68
|
+
}
|
|
69
|
+
if (hit && !sum && !fresh) return hit.detail;
|
|
70
|
+
const d = await api(`exchanges/${encodeURIComponent(id)}`);
|
|
71
|
+
if (generation !== store.generation) throw new Error('数据来源已切换');
|
|
72
|
+
store.cache.delete(id);
|
|
73
|
+
store.cache.set(id, { rev: d.rev, detail: d, at: Date.now() });
|
|
74
|
+
while (store.cache.size > DETAIL_CACHE) {
|
|
75
|
+
const oldest = [...store.cache.keys()].find((k) => k !== store.selected);
|
|
76
|
+
store.cache.delete(oldest);
|
|
77
|
+
}
|
|
78
|
+
return d;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function select(id, { scroll = false } = {}) {
|
|
82
|
+
if (!id) return;
|
|
83
|
+
store.selected = id;
|
|
84
|
+
list.setSelected(id);
|
|
85
|
+
if (scroll) list.scrollToSelected();
|
|
86
|
+
history.replaceState(null, '', `#${encodeURIComponent(id)}`);
|
|
87
|
+
const token = ++store.selectToken;
|
|
88
|
+
const generation = store.generation;
|
|
89
|
+
const cached = store.mode === 'offline' ? store.map.get(id) : store.cache.get(id)?.detail;
|
|
90
|
+
if (cached && cached.id === id) detail.show(cached, { gone: store.mode === 'live' && !store.map.has(id) });
|
|
91
|
+
else detail.clear('加载中…');
|
|
92
|
+
try {
|
|
93
|
+
const d = await getDetail(id);
|
|
94
|
+
if (generation !== store.generation || token !== store.selectToken) return;
|
|
95
|
+
detail.show(d, { gone: store.mode === 'live' && !store.map.has(id) });
|
|
96
|
+
} catch (e) {
|
|
97
|
+
if (generation !== store.generation || token !== store.selectToken) return;
|
|
98
|
+
if (e instanceof ApiError && e.status === 404) detail.clear('这条记录已被清空或淘汰(bridge 上已不存在)。');
|
|
99
|
+
else if (e instanceof ApiError && e.status === 401) { detail.clear('需要令牌'); askToken(); }
|
|
100
|
+
else detail.clear(`加载失败:${e.message}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Re-fetches the selected exchange when it changed or is still streaming. */
|
|
105
|
+
async function refreshSelected(force = false) {
|
|
106
|
+
const id = store.selected;
|
|
107
|
+
if (!id || store.mode !== 'live') return;
|
|
108
|
+
const generation = store.generation;
|
|
109
|
+
const token = store.selectToken;
|
|
110
|
+
const sum = store.map.get(id);
|
|
111
|
+
const hit = store.cache.get(id);
|
|
112
|
+
if (!sum) { if (detail.detail?.id === id && !detail.gone) detail.show(detail.detail, { gone: true }); return; }
|
|
113
|
+
const stale = !hit || hit.rev !== sum.rev || (sum.active && Date.now() - hit.at >= ACTIVE_DETAIL_MS) || (hit.detail.active && !sum.active);
|
|
114
|
+
if (!force && !stale) return;
|
|
115
|
+
try {
|
|
116
|
+
const d = await getDetail(id, true);
|
|
117
|
+
if (generation !== store.generation || token !== store.selectToken) return;
|
|
118
|
+
detail.show(d);
|
|
119
|
+
if (force) toast('已刷新');
|
|
120
|
+
} catch (e) {
|
|
121
|
+
if (generation !== store.generation || token !== store.selectToken) return;
|
|
122
|
+
if (force) toast(`刷新失败:${e.message}`, true);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ---------- polling ----------
|
|
127
|
+
|
|
128
|
+
function schedule(ms = POLL_MS) {
|
|
129
|
+
clearTimeout(store.timer);
|
|
130
|
+
store.timer = null;
|
|
131
|
+
if (store.mode !== 'live' || document.hidden || store.authOpen) return;
|
|
132
|
+
store.timer = setTimeout(tick, ms);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function tick() {
|
|
136
|
+
if (store.mode !== 'live' || store.busy === store.generation) return;
|
|
137
|
+
let generation = store.generation;
|
|
138
|
+
store.busy = generation;
|
|
139
|
+
let next = POLL_MS;
|
|
140
|
+
try {
|
|
141
|
+
const st = await api('state');
|
|
142
|
+
if (generation !== store.generation) return;
|
|
143
|
+
if (store.startedAt != null && st.started_at !== store.startedAt) {
|
|
144
|
+
resetLive('bridge 已重启,记录已重置');
|
|
145
|
+
generation = store.generation;
|
|
146
|
+
store.busy = generation;
|
|
147
|
+
}
|
|
148
|
+
store.startedAt = st.started_at;
|
|
149
|
+
store.state = st;
|
|
150
|
+
if (st.rev !== store.rev || store.rev === 0) {
|
|
151
|
+
const r = await api(`exchanges?since=${store.rev}`);
|
|
152
|
+
if (generation !== store.generation) return;
|
|
153
|
+
applyList(r);
|
|
154
|
+
generation = store.generation;
|
|
155
|
+
store.busy = generation;
|
|
156
|
+
}
|
|
157
|
+
setConn('ok');
|
|
158
|
+
renderTop();
|
|
159
|
+
await refreshSelected();
|
|
160
|
+
} catch (e) {
|
|
161
|
+
if (generation === store.generation) next = handleError(e);
|
|
162
|
+
} finally {
|
|
163
|
+
if (generation === store.generation) {
|
|
164
|
+
store.busy = null;
|
|
165
|
+
schedule(next);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function applyList(r) {
|
|
171
|
+
const ids = new Set(r.ids ?? []);
|
|
172
|
+
if (r.rev < store.rev) { resetLive(); }
|
|
173
|
+
for (const s of r.exchanges ?? []) store.map.set(s.id, s);
|
|
174
|
+
for (const id of [...store.map.keys()]) if (!ids.has(id)) store.map.delete(id);
|
|
175
|
+
// Unknown ids mean a missed update (e.g. after a reset): resync from scratch next tick.
|
|
176
|
+
const missing = [...ids].some((id) => !store.map.has(id));
|
|
177
|
+
store.rev = missing ? 0 : r.rev;
|
|
178
|
+
renderList();
|
|
179
|
+
if (!store.selected && store.map.size) {
|
|
180
|
+
const want = decodeURIComponent(location.hash.slice(1));
|
|
181
|
+
const first = store.map.has(want) ? want : [...store.map.values()].sort(byNewest)[0].id;
|
|
182
|
+
select(first, { scroll: true });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function resetLive(message) {
|
|
187
|
+
invalidateSource();
|
|
188
|
+
store.map.clear();
|
|
189
|
+
store.rev = 0;
|
|
190
|
+
store.selected = null;
|
|
191
|
+
if (message) toast(message);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function invalidateSource() {
|
|
195
|
+
store.generation++;
|
|
196
|
+
store.selectToken++;
|
|
197
|
+
store.busy = null;
|
|
198
|
+
store.cache.clear();
|
|
199
|
+
detail.clear('加载中…');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function handleError(e) {
|
|
203
|
+
if (!(e instanceof ApiError)) { console.error(e); setConn('down', String(e.message || e)); return 5000; }
|
|
204
|
+
if (e.status === 401) { setConn('auth'); askToken(getToken() ? '令牌无效,请重新输入' : ''); return POLL_MS; }
|
|
205
|
+
if (e.status === 403) { setConn('forbidden', e.message); return 5000; }
|
|
206
|
+
setConn('down', e.message);
|
|
207
|
+
return e.status === 0 ? 5000 : 3000;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function setConn(conn, message = '') {
|
|
211
|
+
const changed = store.conn !== conn;
|
|
212
|
+
store.conn = conn;
|
|
213
|
+
store.connError = message;
|
|
214
|
+
renderTop();
|
|
215
|
+
if (changed && !store.map.size) renderList(); // refresh the empty-list hint
|
|
216
|
+
const banner = $('banner');
|
|
217
|
+
const show = (...nodes) => { fill(banner, ...nodes); banner.hidden = false; };
|
|
218
|
+
if (store.mode === 'offline') { banner.hidden = true; return; }
|
|
219
|
+
if (conn === 'down') show(`无法连接 bridge API(${message}),稍后自动重试。也可以点击“导入”离线查看导出文件。`);
|
|
220
|
+
else if (conn === 'forbidden') show('403:bridge 没有配置 inspector 令牌时,只接受从 loopback 地址(如 127.0.0.1)同源打开的页面。请改用本机地址打开,或在 bridge 上配置令牌。', h('span', { class: 'dim' }, ` ${message}`));
|
|
221
|
+
else if (conn === 'auth' && !store.authOpen) show('需要访问令牌。', btn('输入令牌', () => askToken()));
|
|
222
|
+
else banner.hidden = true;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ---------- auth ----------
|
|
226
|
+
|
|
227
|
+
function askToken(error = '') {
|
|
228
|
+
const dlg = $('token-dialog');
|
|
229
|
+
$('token-error').textContent = error;
|
|
230
|
+
$('token-error').hidden = !error;
|
|
231
|
+
if (dlg.open) return;
|
|
232
|
+
store.authOpen = true;
|
|
233
|
+
clearTimeout(store.timer);
|
|
234
|
+
$('token-input').value = '';
|
|
235
|
+
dlg.showModal();
|
|
236
|
+
$('token-input').focus();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
$('token-form').addEventListener('submit', (e) => {
|
|
240
|
+
e.preventDefault();
|
|
241
|
+
const value = $('token-input').value.trim();
|
|
242
|
+
if (!value) return;
|
|
243
|
+
setToken(value);
|
|
244
|
+
store.authOpen = false;
|
|
245
|
+
$('token-dialog').close();
|
|
246
|
+
tick();
|
|
247
|
+
});
|
|
248
|
+
$('token-cancel').addEventListener('click', () => $('token-dialog').close());
|
|
249
|
+
$('token-dialog').addEventListener('close', () => {
|
|
250
|
+
if (store.authOpen) { store.authOpen = false; setConn('auth'); }
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// ---------- top bar ----------
|
|
254
|
+
|
|
255
|
+
function renderTop() {
|
|
256
|
+
const st = store.state;
|
|
257
|
+
const offline = store.mode === 'offline';
|
|
258
|
+
const conn = $('conn');
|
|
259
|
+
const set = (text, cls, title = '') => { conn.textContent = text; conn.className = `chip ${cls}`; conn.title = title; };
|
|
260
|
+
if (offline) set(`离线 · ${store.offline.name}`, 'warn', `导出于 ${fmtDateTime(store.offline.exported_at)} · ${store.map.size} 条记录`);
|
|
261
|
+
else if (store.conn === 'ok') set('● 已连接', 'ok', st ? `bridge 启动于 ${fmtDateTime(st.started_at)}` : '');
|
|
262
|
+
else if (store.conn === 'auth') set('需要令牌', 'warn');
|
|
263
|
+
else if (store.conn === 'forbidden') set('403 禁止访问', 'bad', store.connError);
|
|
264
|
+
else if (store.conn === 'down') set('未连接', 'bad', store.connError);
|
|
265
|
+
else set('连接中…', 'dim');
|
|
266
|
+
|
|
267
|
+
const cap = $('capture');
|
|
268
|
+
const capturing = !offline && !!st?.capturing;
|
|
269
|
+
cap.disabled = offline || !st;
|
|
270
|
+
cap.textContent = capturing ? '■ 停止捕获' : '● 开始捕获';
|
|
271
|
+
cap.classList.toggle('on', capturing);
|
|
272
|
+
cap.title = capturing ? '正在捕获新请求;点击停止(已存储的记录保留)' : '当前未捕获;点击开始捕获新请求';
|
|
273
|
+
document.body.classList.toggle('capturing', capturing);
|
|
274
|
+
$('clear').disabled = offline || !st;
|
|
275
|
+
$('live').hidden = !offline;
|
|
276
|
+
fill($('mem'), ...memNodes(st, offline));
|
|
277
|
+
$('models').hidden = offline || !st?.models?.length;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function memNodes(st, offline) {
|
|
281
|
+
if (offline) return [h('span', { class: 'dim' }, `${store.map.size} 条记录`)];
|
|
282
|
+
if (!st) return [];
|
|
283
|
+
const meter = h('meter', { min: 0, max: st.max_bytes || 1, value: st.bytes, low: (st.max_bytes || 1) * 0.7, high: (st.max_bytes || 1) * 0.9, optimum: 0 });
|
|
284
|
+
return [h('span', { title: '存储的近似大小 / 上限(超过后淘汰最早的已结束记录)' }, `内存 ${fmtBytes(st.bytes)} / ${fmtBytes(st.max_bytes)}`), meter,
|
|
285
|
+
h('span', { class: 'dim' }, `${st.exchanges} 条 · 进行中 ${st.active}${st.evicted ? ` · 已淘汰 ${st.evicted}` : ''}`)];
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function renderModels() {
|
|
289
|
+
const models = store.state?.models ?? [];
|
|
290
|
+
const cols = [...new Set(models.flatMap((m) => Object.keys(m)))];
|
|
291
|
+
$('models-body').replaceChildren(h('table', { class: 'calls' },
|
|
292
|
+
h('thead', null, h('tr', null, cols.map((c) => h('th', null, c)))),
|
|
293
|
+
h('tbody', null, models.map((m) => h('tr', { class: 'clickable', title: '按此别名筛选', onclick: () => { setFilter(m.alias ?? ''); $('models').open = false; } },
|
|
294
|
+
cols.map((c) => h('td', { class: 'mono' }, typeof m[c] === 'object' && m[c] !== null ? JSON.stringify(m[c]) : String(m[c] ?? '–'))))))));
|
|
295
|
+
}
|
|
296
|
+
$('models').addEventListener('toggle', () => { if ($('models').open) renderModels(); });
|
|
297
|
+
|
|
298
|
+
$('capture').addEventListener('click', async () => {
|
|
299
|
+
const generation = store.generation;
|
|
300
|
+
try {
|
|
301
|
+
const state = await api('capture', { method: 'POST', json: { enabled: !store.state?.capturing } });
|
|
302
|
+
if (generation !== store.generation) return;
|
|
303
|
+
store.state = state;
|
|
304
|
+
renderTop();
|
|
305
|
+
toast(store.state.capturing ? '已开始捕获' : '已停止捕获(已存储的记录保留)');
|
|
306
|
+
} catch (e) { if (generation === store.generation) { handleError(e); toast(`操作失败:${e.message}`, true); } }
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
$('clear').addEventListener('click', async () => {
|
|
310
|
+
if (!confirm('删除 bridge 上所有已捕获的记录?')) return;
|
|
311
|
+
const generation = store.generation;
|
|
312
|
+
try {
|
|
313
|
+
const state = await api('exchanges', { method: 'DELETE' });
|
|
314
|
+
if (generation !== store.generation) return;
|
|
315
|
+
resetLive();
|
|
316
|
+
store.state = state;
|
|
317
|
+
detail.clear('已清空。');
|
|
318
|
+
renderList();
|
|
319
|
+
renderTop();
|
|
320
|
+
tick();
|
|
321
|
+
} catch (e) { if (generation === store.generation) { handleError(e); toast(`清空失败:${e.message}`, true); } }
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
$('export-all').addEventListener('click', async () => {
|
|
325
|
+
try {
|
|
326
|
+
if (store.mode === 'offline') {
|
|
327
|
+
download(`codeks-bridge-capture-${stamp()}.json`, JSON.stringify(exportDoc([...store.map.values()].sort(byNewest).reverse())));
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
toast('正在导出…');
|
|
331
|
+
const res = await api('export', { raw: true });
|
|
332
|
+
const name = /filename="?([^";]+)"?/.exec(res.headers.get('content-disposition') ?? '')?.[1] ?? `codeks-bridge-capture-${stamp()}.json`;
|
|
333
|
+
download(name, await res.blob());
|
|
334
|
+
} catch (e) { handleError(e); toast(`导出失败:${e.message}`, true); }
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// ---------- import / offline ----------
|
|
338
|
+
|
|
339
|
+
$('import').addEventListener('click', () => $('import-file').click());
|
|
340
|
+
$('import-file').addEventListener('change', async (e) => {
|
|
341
|
+
const file = e.target.files?.[0];
|
|
342
|
+
e.target.value = '';
|
|
343
|
+
if (file) importFile(file);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
async function importFile(file) {
|
|
347
|
+
try {
|
|
348
|
+
const doc = parseExport(await file.text());
|
|
349
|
+
enterOffline(doc, file.name);
|
|
350
|
+
toast(`已导入 ${doc.exchanges.length} 条记录(离线查看)`);
|
|
351
|
+
} catch (e) {
|
|
352
|
+
toast(`导入失败:${e.message}`, true);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function enterOffline(doc, name) {
|
|
357
|
+
invalidateSource();
|
|
358
|
+
store.mode = 'offline';
|
|
359
|
+
clearTimeout(store.timer);
|
|
360
|
+
store.offline = { name, exported_at: doc.exported_at };
|
|
361
|
+
store.map = new Map(doc.exchanges.filter((d) => d && d.id != null).map((d) => [String(d.id), d]));
|
|
362
|
+
store.selected = null;
|
|
363
|
+
$('banner').hidden = true;
|
|
364
|
+
renderTop();
|
|
365
|
+
renderList();
|
|
366
|
+
const first = [...store.map.values()].sort(byNewest)[0];
|
|
367
|
+
if (first) select(first.id, { scroll: true });
|
|
368
|
+
else detail.clear('导入文件里没有记录。');
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
$('live').addEventListener('click', () => {
|
|
372
|
+
resetLive();
|
|
373
|
+
store.mode = 'live';
|
|
374
|
+
store.offline = null;
|
|
375
|
+
store.state = null;
|
|
376
|
+
store.startedAt = null;
|
|
377
|
+
detail.clear('正在连接 bridge…');
|
|
378
|
+
renderTop();
|
|
379
|
+
renderList();
|
|
380
|
+
tick();
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
let dragDepth = 0;
|
|
384
|
+
const hasFiles = (e) => [...(e.dataTransfer?.types ?? [])].includes('Files');
|
|
385
|
+
document.addEventListener('dragenter', (e) => { if (hasFiles(e)) { dragDepth++; $('drop').hidden = false; } });
|
|
386
|
+
document.addEventListener('dragleave', (e) => { if (hasFiles(e) && --dragDepth <= 0) { dragDepth = 0; $('drop').hidden = true; } });
|
|
387
|
+
document.addEventListener('dragover', (e) => { if (hasFiles(e)) e.preventDefault(); });
|
|
388
|
+
document.addEventListener('drop', (e) => {
|
|
389
|
+
if (!hasFiles(e)) return;
|
|
390
|
+
e.preventDefault();
|
|
391
|
+
dragDepth = 0;
|
|
392
|
+
$('drop').hidden = true;
|
|
393
|
+
const file = e.dataTransfer.files[0];
|
|
394
|
+
if (file) importFile(file);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// ---------- list, filter and chips ----------
|
|
398
|
+
|
|
399
|
+
function emptyText() {
|
|
400
|
+
if (store.mode === 'offline') return '导入文件里没有记录';
|
|
401
|
+
if (!store.state) return { connecting: '连接中…', auth: '需要访问令牌(见上方提示)', forbidden: '403 禁止访问(见上方提示)', down: '无法连接 bridge(见上方提示)' }[store.conn] ?? '无法读取 bridge 状态';
|
|
402
|
+
return store.state.capturing ? '正在捕获,等待 codeks 发来请求…' : '尚未开始捕获。点击上方“● 开始捕获”,然后在 codeks 中继续对话。';
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function renderList() {
|
|
406
|
+
list.render(store.map, { filter: $('filter').value, chips: prefs.chips, group: prefs.group, empty: emptyText() });
|
|
407
|
+
if (!store.selected && !store.map.size) detail.clear(store.mode === 'offline' ? '导入文件里没有记录。' : '在左侧选择一条记录。');
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function setFilter(text) {
|
|
411
|
+
$('filter').value = text;
|
|
412
|
+
renderList();
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
$('filter').addEventListener('input', () => renderList());
|
|
416
|
+
$('filter').addEventListener('keydown', (e) => {
|
|
417
|
+
if (e.key === 'Escape') { setFilter(''); $('filter').blur(); }
|
|
418
|
+
if (e.key === 'Enter' || e.key === 'ArrowDown') { e.preventDefault(); $('list').focus(); select(list.neighbor(0) ?? list.order[0], { scroll: true }); }
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
for (const [key, label, title] of FILTER_CHIPS) {
|
|
422
|
+
const b = h('button', { type: 'button', class: `btn toggle${prefs.chips[key] ? ' on' : ''}`, title }, label);
|
|
423
|
+
b.addEventListener('click', () => {
|
|
424
|
+
prefs.chips[key] = !prefs.chips[key];
|
|
425
|
+
b.classList.toggle('on', prefs.chips[key]);
|
|
426
|
+
savePrefs();
|
|
427
|
+
renderList();
|
|
428
|
+
});
|
|
429
|
+
$('chips').append(b);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
$('group').classList.toggle('on', prefs.group);
|
|
433
|
+
$('group').addEventListener('click', () => {
|
|
434
|
+
prefs.group = !prefs.group;
|
|
435
|
+
$('group').classList.toggle('on', prefs.group);
|
|
436
|
+
savePrefs();
|
|
437
|
+
renderList();
|
|
438
|
+
list.scrollToSelected();
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
// ---------- keyboard ----------
|
|
442
|
+
|
|
443
|
+
document.addEventListener('keydown', (e) => {
|
|
444
|
+
if (e.defaultPrevented || e.ctrlKey || e.metaKey || e.altKey) return;
|
|
445
|
+
if (e.target.closest?.('input, textarea, select, dialog, [contenteditable]')) return;
|
|
446
|
+
if (e.key === '/') { e.preventDefault(); $('filter').focus(); $('filter').select(); return; }
|
|
447
|
+
const delta = { ArrowDown: 1, ArrowUp: -1, j: 1, k: -1 }[e.key];
|
|
448
|
+
if (delta && (e.target === document.body || e.target.closest?.('#list, .list-pane'))) {
|
|
449
|
+
e.preventDefault();
|
|
450
|
+
const id = list.neighbor(delta);
|
|
451
|
+
if (id && id !== store.selected) select(id, { scroll: true });
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
// ---------- splitter ----------
|
|
456
|
+
|
|
457
|
+
const main = $('main');
|
|
458
|
+
const applyWidth = () => main.style.setProperty('--list-w', `${prefs.listW}px`);
|
|
459
|
+
applyWidth();
|
|
460
|
+
$('splitter').addEventListener('pointerdown', (e) => {
|
|
461
|
+
e.preventDefault();
|
|
462
|
+
const sp = $('splitter');
|
|
463
|
+
sp.setPointerCapture(e.pointerId);
|
|
464
|
+
const move = (ev) => {
|
|
465
|
+
prefs.listW = Math.round(Math.max(240, Math.min(window.innerWidth * 0.7, ev.clientX - main.getBoundingClientRect().left)));
|
|
466
|
+
applyWidth();
|
|
467
|
+
};
|
|
468
|
+
const up = () => { sp.removeEventListener('pointermove', move); sp.removeEventListener('pointerup', up); savePrefs(); };
|
|
469
|
+
sp.addEventListener('pointermove', move);
|
|
470
|
+
sp.addEventListener('pointerup', up);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// ---------- start ----------
|
|
474
|
+
|
|
475
|
+
document.addEventListener('visibilitychange', () => {
|
|
476
|
+
if (store.mode !== 'live') return;
|
|
477
|
+
if (document.hidden) clearTimeout(store.timer);
|
|
478
|
+
else tick();
|
|
479
|
+
});
|
|
480
|
+
window.addEventListener('hashchange', () => {
|
|
481
|
+
const id = decodeURIComponent(location.hash.slice(1));
|
|
482
|
+
if (id && id !== store.selected && store.map.has(id)) select(id, { scroll: true });
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
$('boot')?.remove();
|
|
486
|
+
detail.clear('正在连接 bridge…');
|
|
487
|
+
renderTop();
|
|
488
|
+
renderList();
|
|
489
|
+
tick();
|