cc-viewer 1.7.9 → 1.7.11
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/dist/assets/{App-CYvr5YQv.js → App-o7gt7lIy.js} +2 -2
- package/dist/assets/{MdxEditorPanel-D1MOJabL.js → MdxEditorPanel-Br1zGDDJ.js} +1 -1
- package/dist/assets/{Mobile-B7GXxkfK.js → Mobile-C_HxuKWt.js} +1 -1
- package/dist/assets/ProxyStatsModal-Boxtr7ht.js +1 -0
- package/dist/assets/{index-f4nPU7KM.css → index-CGSyt5Ow.css} +1 -1
- package/dist/assets/{index-mpHzrAV9.js → index-gH_W4sVT.js} +2 -2
- package/dist/assets/{seqResourceLoaders-DMyPYdA-.js → seqResourceLoaders-ByYE7WxI.js} +2 -2
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/server/i18n.js +0 -5
- package/server/lib/ccswitch-import.js +76 -15
- package/server/lib/proxy-retry.js +100 -6
- package/server/lib/stats-worker.js +9 -4
- package/server/lib/v2/migrate-prompt.js +3 -0
- package/dist/assets/ProxyStatsModal-BqH9cIks.js +0 -1
package/dist/index.html
CHANGED
|
@@ -21,11 +21,11 @@
|
|
|
21
21
|
// 整体显示大小已弃用 CSS zoom:Electron 改用 webFrame.setZoomFactor(首屏抢占见
|
|
22
22
|
// electron/tab-content-preload.js),纯浏览器交由用户用浏览器自带快捷键缩放,故此处不再设 zoom。
|
|
23
23
|
</script>
|
|
24
|
-
<script type="module" crossorigin src="./assets/index-
|
|
24
|
+
<script type="module" crossorigin src="./assets/index-gH_W4sVT.js"></script>
|
|
25
25
|
<link rel="modulepreload" crossorigin href="./assets/vendor-antd-DADYo_zg.js">
|
|
26
26
|
<link rel="modulepreload" crossorigin href="./assets/vendor-codemirror-tF6HNoR6.js">
|
|
27
27
|
<link rel="modulepreload" crossorigin href="./assets/vendor-mdxeditor-CFAmRN3Y.js">
|
|
28
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
28
|
+
<link rel="stylesheet" crossorigin href="./assets/index-CGSyt5Ow.css">
|
|
29
29
|
</head>
|
|
30
30
|
<body>
|
|
31
31
|
<!-- Pre-hydration splash: no SPA CSS yet — keep font-family in sync with --font-ui in src/global.css -->
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cc-viewer",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.11",
|
|
4
4
|
"description": "Claude Code logging, visualization, and management toolkit — launch a web viewer alongside Claude Code with full request/response tracing, proxy, and mobile support",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "server.js",
|
package/server/i18n.js
CHANGED
|
@@ -2975,11 +2975,6 @@ const i18nData = {
|
|
|
2975
2975
|
"tr": "IM botu '{id}' zaten çalışıyor (pid {pid}); ikinci bir örnek başlatılması reddedildi.",
|
|
2976
2976
|
"uk": "IM-бот '{id}' вже запущено (pid {pid}); запуск другого екземпляра відхилено."
|
|
2977
2977
|
},
|
|
2978
|
-
"server.proxyStats.starting": { "zh": "代理重试统计已启动", "en": "Proxy retry stats started", "zh-TW": "代理重試統計已啟動", "ko": "프록시 재시도 통계가 시작됨", "ja": "プロキシリトライ統計を開始しました", "de": "Proxy-Wiederholungsstatistiken gestartet", "es": "Estadísticas de reintentos de proxy iniciadas", "fr": "Statistiques de relances du proxy démarrées", "it": "Statistiche retry del proxy avviate", "da": "Proxy-genforsøgsstatistik startet", "pl": "Statystyki ponownych prób proxy uruchomione", "ru": "Статистика повторов прокси запущена", "ar": "بدأت إحصائيات إعادة محاولات الوكيل", "no": "Proxy-retry-statistikk startet", "pt-BR": "Estatísticas de retry do proxy iniciadas", "th": "เริ่มสถิติการลองใหม่ของพร็อกซีแล้ว", "tr": "Proxy yeniden deneme istatistikleri başladı", "uk": "Статистику повторів проксі запущено" },
|
|
2979
|
-
"server.proxyStats.modeLabel": { "zh": "重试模式", "en": "Retry mode", "zh-TW": "重試模式", "ko": "재시도 모드", "ja": "リトライモード", "de": "Wiederholungsmodus", "es": "Modo de reintento", "fr": "Mode de relance", "it": "Modalità retry", "da": "Genforsøgstilstand", "pl": "Tryb ponownych prób", "ru": "Режим повторов", "ar": "وضع إعادة المحاولة", "no": "Retry-modus", "pt-BR": "Modo de retry", "th": "โหมดการลองใหม่", "tr": "Yeniden deneme modu", "uk": "Режим повторів" },
|
|
2980
|
-
"server.proxyStats.disabled": { "zh": "代理重试已禁用", "en": "Proxy retry disabled", "zh-TW": "代理重試已停用", "ko": "프록시 재시도 비활성화됨", "ja": "プロキシリトライは無効です", "de": "Proxy-Wiederholung deaktiviert", "es": "Reintento de proxy deshabilitado", "fr": "Relance du proxy désactivée", "it": "Retry del proxy disattivato", "da": "Proxy-genforsøg deaktiveret", "pl": "Ponowne próby proxy wyłączone", "ru": "Повторы прокси отключены", "ar": "تم تعطيل إعادة محاولات الوكيل", "no": "Proxy-retry deaktivert", "pt-BR": "Retry do proxy desativado", "th": "ปิดการลองใหม่ของพร็อกซีแล้ว", "tr": "Proxy yeniden deneme devre dışı", "uk": "Повтори проксі вимкнено" },
|
|
2981
|
-
"server.proxyStats.configLoaded": { "zh": "代理重试配置已加载", "en": "Proxy retry config loaded", "zh-TW": "代理重試設定已載入", "ko": "프록시 재시도 설정이 로드됨", "ja": "プロキシリトライ設定を読み込みました", "de": "Proxy-Wiederholungskonfiguration geladen", "es": "Configuración de reintento de proxy cargada", "fr": "Configuration de relance du proxy chargée", "it": "Configurazione retry del proxy caricata", "da": "Proxy-genforsøgskonfiguration indlæst", "pl": "Konfiguracja ponownych prób proxy załadowana", "ru": "Конфигурация повторов прокси загружена", "ar": "تم تحميل تكوين إعادة محاولات الوكيل", "no": "Proxy-retry-konfigurasjon lastet", "pt-BR": "Configuração de retry do proxy carregada", "th": "โหลดการตั้งค่าการลองใหม่ของพร็อกซีแล้ว", "tr": "Proxy yeniden deneme yapılandırması yüklendi", "uk": "Конфігурацію повторів проксі завантажено" },
|
|
2982
|
-
"server.proxyStats.refreshed": { "zh": "代理统计已刷新", "en": "Proxy stats refreshed", "zh-TW": "代理統計已重新整理", "ko": "프록시 통계가 새로고침됨", "ja": "プロキシ統計を更新しました", "de": "Proxy-Statistiken aktualisiert", "es": "Estadísticas de proxy actualizadas", "fr": "Statistiques du proxy actualisées", "it": "Statistiche del proxy aggiornate", "da": "Proxy-statistik opdateret", "pl": "Statystyki proxy odświeżone", "ru": "Статистика прокси обновлена", "ar": "تم تحديث إحصائيات الوكيل", "no": "Proxy-statistikk oppdatert", "pt-BR": "Estatísticas do proxy atualizadas", "th": "รีเฟรชสถิติพร็อกซีแล้ว", "tr": "Proxy istatistikleri yenilendi", "uk": "Статистику проксі оновлено" }
|
|
2983
2978
|
};
|
|
2984
2979
|
|
|
2985
2980
|
// 将 { key: { lang: text } } 转换为 { lang: { key: text } }
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
// is delegated to the caller (preferences.js POST /api/ccswitch-import).
|
|
12
12
|
//
|
|
13
13
|
// Design notes:
|
|
14
|
-
// - Open SQLite read-only (readOnly:true) to avoid SQLITE_BUSY locks while cc-switch is running
|
|
14
|
+
// - Open SQLite read-only by default (readOnly:true) to avoid SQLITE_BUSY locks while cc-switch is running;
|
|
15
|
+
// escalate once to a read-write + query_only connection only when a malformed leftover journal forces
|
|
16
|
+
// SQLITE_READONLY recovery (see readCcSwitchProviders)
|
|
15
17
|
// - Multi-path probing: each platform tries the standard Tauri dir first, then falls back to ~/.cc-switch/
|
|
16
18
|
// - settings_config parsing is fully fault-tolerant: null / non-JSON / missing env are all skipped, never throws
|
|
17
19
|
// - Profile ids get a ccs_ prefix to distinguish from user-created proxy_ prefixed ones; updates are idempotent (re-imports update, never duplicate)
|
|
@@ -155,18 +157,31 @@ export async function readCcSwitchProviders(dbPath) {
|
|
|
155
157
|
// localized message off it (ui.proxy.ccswitchNodeUnsupported).
|
|
156
158
|
return { profiles: [], error: 'node:sqlite unavailable on this runtime (requires Node >= 22.5 with --experimental-sqlite, or >= 23.4)' };
|
|
157
159
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
160
|
+
|
|
161
|
+
// Open the db. Two contention modes from cc-switch to recover from:
|
|
162
|
+
// (1) A leftover MALFORMED cc-switch.db-journal (cc-switch killed mid-write, leaving a
|
|
163
|
+
// truncated/corrupt rollback journal) forces SQLite to discard it on the first page
|
|
164
|
+
// access — a write — which a read-only connection refuses with SQLITE_READONLY
|
|
165
|
+
// ("attempt to write a readonly database"). Escalate once to a read-write open guarded
|
|
166
|
+
// by PRAGMA query_only=ON (lets SQLite recover, blocks our own writes), then retry.
|
|
167
|
+
// Mirrors what cc-switch itself does on its next normal launch.
|
|
168
|
+
// (2) A running cc-switch holding an EXCLUSIVE write lock (its real contention mode — a
|
|
169
|
+
// valid hot journal under BEGIN EXCLUSIVE blocks even read-only readers, unlike
|
|
170
|
+
// BEGIN IMMEDIATE) makes the read-only connection's first query throw SQLITE_BUSY
|
|
171
|
+
// ("database is locked"). Retry once after a short backoff (cc-switch's writes are
|
|
172
|
+
// short — transient locks usually clear), and if still held surface a friendly message
|
|
173
|
+
// rather than the opaque `query failed: database is locked`.
|
|
174
|
+
const primaryErrCode = (e) => Number.isInteger(e && e.errcode) ? (e.errcode & 0xff) : null;
|
|
175
|
+
const isReadonlyErr = (e) => primaryErrCode(e) === 8
|
|
176
|
+
|| /readonly/i.test(e && (e.errstr || e.message || ''));
|
|
177
|
+
const isBusyErr = (e) => [5, 6].includes(primaryErrCode(e))
|
|
178
|
+
|| /locked|busy/i.test(e && (e.errstr || e.message || ''));
|
|
179
|
+
|
|
180
|
+
// Run the providers read against a given connection; returns the result object or throws.
|
|
181
|
+
const readProviders = (db) => {
|
|
166
182
|
// providers table existence check. A query that throws here means the file is
|
|
167
183
|
// unreadable as a SQLite db (corrupt / non-SQLite / truncated) — the real cause
|
|
168
|
-
// must surface, not be masked as "table not found".
|
|
169
|
-
// outer catch, which formats it as `query failed: <message>`.
|
|
184
|
+
// must surface, not be masked as "table not found".
|
|
170
185
|
const r = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='providers'").get();
|
|
171
186
|
if (!r) return { profiles: [], error: `providers table not found in ${dbPath}` };
|
|
172
187
|
|
|
@@ -184,10 +199,56 @@ export async function readCcSwitchProviders(dbPath) {
|
|
|
184
199
|
}
|
|
185
200
|
}
|
|
186
201
|
return { profiles, currentId, error: null };
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// Open read-only and attempt the read. Two recovery paths:
|
|
205
|
+
// - SQLITE_BUSY ("database is locked"): cc-switch is running and holding an EXCLUSIVE write
|
|
206
|
+
// lock (its real contention mode — a valid hot journal under BEGIN EXCLUSIVE blocks even
|
|
207
|
+
// read-only readers, unlike BEGIN IMMEDIATE). The lock is transient (cc-switch mid-write),
|
|
208
|
+
// so retry once after a short backoff; if still held, surface a friendly, actionable
|
|
209
|
+
// message instead of the opaque `query failed: database is locked` wrapper.
|
|
210
|
+
// - SQLITE_READONLY ("attempt to write a readonly database"): a malformed leftover journal
|
|
211
|
+
// (cc-switch killed mid-write) forces SQLite to discard it — a write the read-only
|
|
212
|
+
// connection refuses. Escalate once to a read-write open guarded by PRAGMA query_only=ON
|
|
213
|
+
// (lets SQLite recover, blocks our own writes), then retry. Mirrors cc-switch's own
|
|
214
|
+
// next-launch recovery.
|
|
215
|
+
// Other errors propagate to the outer catch (formatted as `query failed: <message>`),
|
|
216
|
+
// preserving real-cause surfacing (e.g. "file is not a database").
|
|
217
|
+
const LOCKED_MSG = 'cc-switch db is locked (cc-switch may be running); retry shortly';
|
|
218
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
219
|
+
|
|
220
|
+
// Keep the BUSY retry and READONLY escalation as independent one-shot budgets. Every open,
|
|
221
|
+
// PRAGMA and query attempt passes through the same classifier, so a real state transition such
|
|
222
|
+
// as BUSY (cc-switch was writing) → READONLY (it crashed and left a malformed journal) can use
|
|
223
|
+
// both recoveries in sequence instead of escaping through a nested retry catch.
|
|
224
|
+
let useRecoveryConnection = false;
|
|
225
|
+
let busyRetried = false;
|
|
226
|
+
while (true) {
|
|
227
|
+
let db = null;
|
|
228
|
+
let phase = 'open';
|
|
229
|
+
try {
|
|
230
|
+
db = useRecoveryConnection
|
|
231
|
+
? new DatabaseSync(dbPath)
|
|
232
|
+
: new DatabaseSync(dbPath, { readOnly: true });
|
|
233
|
+
phase = 'query';
|
|
234
|
+
if (useRecoveryConnection) db.exec('PRAGMA query_only = ON');
|
|
235
|
+
return readProviders(db);
|
|
236
|
+
} catch (err) {
|
|
237
|
+
if (isBusyErr(err)) {
|
|
238
|
+
if (busyRetried) return { profiles: [], error: LOCKED_MSG };
|
|
239
|
+
busyRetried = true;
|
|
240
|
+
await sleep(200);
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (isReadonlyErr(err) && !useRecoveryConnection) {
|
|
244
|
+
useRecoveryConnection = true;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
const prefix = phase === 'open' ? 'cannot open db' : 'query failed';
|
|
248
|
+
return { profiles: [], error: `${prefix}: ${err && err.message}` };
|
|
249
|
+
} finally {
|
|
250
|
+
try { db && db.close(); } catch { /* best effort */ }
|
|
251
|
+
}
|
|
191
252
|
}
|
|
192
253
|
}
|
|
193
254
|
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
// - race/stagger use AbortController; cancelled requests must be released correctly.
|
|
18
18
|
import { resolveProfileModel } from './interceptor-core.js';
|
|
19
19
|
import { readFileSync, existsSync } from 'node:fs';
|
|
20
|
+
import { reportSwallowed } from './error-report.js';
|
|
20
21
|
|
|
21
22
|
// ── Configuration ─────────────────────────────────────────────────
|
|
22
23
|
|
|
@@ -162,7 +163,11 @@ export function resolveRetryConfig(env = process.env, options = {}) {
|
|
|
162
163
|
const fileRaw = JSON.parse(readFileSync(_retryConfigPath, 'utf-8'));
|
|
163
164
|
Object.assign(cfg, validateRetryConfig(fileRaw));
|
|
164
165
|
}
|
|
165
|
-
} catch {
|
|
166
|
+
} catch (err) {
|
|
167
|
+
// retry-config.json written by the UI is corrupt/unreadable → fall back to env.
|
|
168
|
+
// Not fatal, but a silent swallow would hide a config the user believes is active.
|
|
169
|
+
reportSwallowed('proxyRetry.load-config-file', err);
|
|
170
|
+
}
|
|
166
171
|
}
|
|
167
172
|
|
|
168
173
|
return cfg;
|
|
@@ -219,7 +224,10 @@ export function isStreamResponse(response) {
|
|
|
219
224
|
try {
|
|
220
225
|
const ct = response?.headers?.get?.('content-type') || '';
|
|
221
226
|
return typeof ct === 'string' && ct.toLowerCase().includes('text/event-stream');
|
|
222
|
-
} catch {
|
|
227
|
+
} catch (err) {
|
|
228
|
+
// A misbehaving header object would make us misclassify the response, which
|
|
229
|
+
// changes retry behavior (streaming 200 is never retried). Surface it.
|
|
230
|
+
reportSwallowed('proxyRetry.is-stream-response', err);
|
|
223
231
|
return false;
|
|
224
232
|
}
|
|
225
233
|
}
|
|
@@ -233,7 +241,10 @@ export function extractModel(body) {
|
|
|
233
241
|
const s = typeof body === 'string' ? body : body.toString('utf-8');
|
|
234
242
|
const obj = JSON.parse(s);
|
|
235
243
|
return typeof obj.model === 'string' ? obj.model : '';
|
|
236
|
-
} catch {
|
|
244
|
+
} catch (err) {
|
|
245
|
+
// Unparseable body → stats detail record loses the model field. Surface it
|
|
246
|
+
// so a regression in request-body handling isn't hidden behind empty models.
|
|
247
|
+
reportSwallowed('proxyRetry.extract-model', err);
|
|
237
248
|
return '';
|
|
238
249
|
}
|
|
239
250
|
}
|
|
@@ -253,7 +264,11 @@ export function applyModelReplacement(body, profile) {
|
|
|
253
264
|
if (!target) return body;
|
|
254
265
|
obj.model = target;
|
|
255
266
|
return JSON.stringify(obj);
|
|
256
|
-
} catch {
|
|
267
|
+
} catch (err) {
|
|
268
|
+
// JSON.parse failure means model replacement silently no-ops; the request
|
|
269
|
+
// still goes out with the original (un-replaced) model. Surface it so the
|
|
270
|
+
// mismatch between configured replacement and actual body isn't silent.
|
|
271
|
+
reportSwallowed('proxyRetry.apply-model-replacement', err);
|
|
257
272
|
return body;
|
|
258
273
|
}
|
|
259
274
|
}
|
|
@@ -299,13 +314,67 @@ function computeWaitMs(status, retryAfterHeader, cfg) {
|
|
|
299
314
|
|
|
300
315
|
// ── Single fetch wrapper ─────────────────────────────────────────
|
|
301
316
|
|
|
317
|
+
/**
|
|
318
|
+
* Attaches a streaming-idle watchdog to a streaming response body.
|
|
319
|
+
*
|
|
320
|
+
* Why: connectTimeoutMs only bounds time-to-HEADERS; once headers arrive the
|
|
321
|
+
* connect timer is cleared and the retry loop breaks (streaming 200 is never
|
|
322
|
+
* retried — retry-before-first-byte strategy). If the upstream then stalls
|
|
323
|
+
* (200 headers but no body chunk ever arrives — a hung upstream), the piped
|
|
324
|
+
* response would hang indefinitely, pinning the client socket and the upstream
|
|
325
|
+
* socket until the client gives up. streamIdleTimeoutMs bounds the max gap
|
|
326
|
+
* between two chunks; exceeding it errors the body so proxy.js's pipeline
|
|
327
|
+
* surfaces the stall instead of hanging.
|
|
328
|
+
*
|
|
329
|
+
* Implemented as a TransformStream pass-through so response.body stays a valid
|
|
330
|
+
* ReadableStream (Readable.fromWeb in proxy.js keeps working): each enqueued
|
|
331
|
+
* chunk resets the timer; a stalled stream fires the timer, which calls
|
|
332
|
+
* controller.error(), aborting the fetch's underlying body and breaking the
|
|
333
|
+
* pipeline.
|
|
334
|
+
*
|
|
335
|
+
* @param {ReadableStream} body original streaming body
|
|
336
|
+
* @param {number} idleMs max gap between chunks (0 = disabled)
|
|
337
|
+
* @param {AbortSignal} signal external signal (race/stagger loser cancel + client disconnect)
|
|
338
|
+
* @returns {ReadableStream} watched body (same chunks, bounded idle)
|
|
339
|
+
*/
|
|
340
|
+
function applyStreamIdleWatchdog(body, idleMs, signal) {
|
|
341
|
+
if (!body || typeof body?.pipeThrough !== 'function') return body;
|
|
342
|
+
if (!idleMs || idleMs <= 0) return body;
|
|
343
|
+
let timer = null;
|
|
344
|
+
let aborted = false;
|
|
345
|
+
const arm = () => {
|
|
346
|
+
if (timer) clearTimeout(timer);
|
|
347
|
+
timer = setTimeout(() => {
|
|
348
|
+
aborted = true;
|
|
349
|
+
controller.error(new Error(`proxy stream idle timeout (${idleMs}ms)`));
|
|
350
|
+
}, idleMs);
|
|
351
|
+
};
|
|
352
|
+
let controller;
|
|
353
|
+
const transform = new TransformStream({
|
|
354
|
+
start(ctl) { controller = ctl; arm(); if (signal) signal.addEventListener('abort', disarm, { once: true }); },
|
|
355
|
+
transform(chunk, ctl) {
|
|
356
|
+
if (aborted) return; // already errored — drop late chunks
|
|
357
|
+
ctl.enqueue(chunk);
|
|
358
|
+
arm(); // reset on each chunk
|
|
359
|
+
},
|
|
360
|
+
flush() { disarm(); },
|
|
361
|
+
cancel() { disarm(); },
|
|
362
|
+
});
|
|
363
|
+
function disarm() { if (timer) { clearTimeout(timer); timer = null; } }
|
|
364
|
+
// teeThrough keeps our transform in the path; pipeThrough returns the readable end.
|
|
365
|
+
// Only pass signal when present — pipeThrough rejects a null/undefined signal.
|
|
366
|
+
return signal
|
|
367
|
+
? body.pipeThrough(transform, { signal })
|
|
368
|
+
: body.pipeThrough(transform);
|
|
369
|
+
}
|
|
370
|
+
|
|
302
371
|
/**
|
|
303
372
|
* Executes a single fetch request with the x-cc-viewer-trace header + network proxy dispatcher.
|
|
304
373
|
* Returns the raw Response. Does not throw (on network errors returns { __networkError: true, status: 0 }).
|
|
305
374
|
*
|
|
306
375
|
* @param {string} url full URL
|
|
307
376
|
* @param {object} fetchOptions method/headers/body
|
|
308
|
-
* @param {object} ctx { dispatcher, connectTimeoutMs, signal }
|
|
377
|
+
* @param {object} ctx { dispatcher, connectTimeoutMs, streamIdleTimeoutMs, signal }
|
|
309
378
|
*/
|
|
310
379
|
async function singleFetch(url, fetchOptions, ctx) {
|
|
311
380
|
const opts = {
|
|
@@ -342,10 +411,25 @@ async function singleFetch(url, fetchOptions, ctx) {
|
|
|
342
411
|
|
|
343
412
|
try {
|
|
344
413
|
const response = await fetch(url, opts);
|
|
414
|
+
// Streaming responses: attach the idle watchdog so a hung body (headers in,
|
|
415
|
+
// no chunks) breaks within streamIdleTimeoutMs instead of pinning sockets.
|
|
416
|
+
// connectTimeoutMs already cleared below can't help — it only bound headers.
|
|
417
|
+
if (response?.body && ctx.streamIdleTimeoutMs > 0 && isStreamResponse(response)) {
|
|
418
|
+
const watched = applyStreamIdleWatchdog(response.body, ctx.streamIdleTimeoutMs, ctx.signal);
|
|
419
|
+
return new Response(watched, {
|
|
420
|
+
status: response.status,
|
|
421
|
+
statusText: response.statusText,
|
|
422
|
+
headers: response.headers,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
345
425
|
return response;
|
|
346
426
|
} catch (err) {
|
|
347
427
|
// Network error/timeout/cancellation → return a pseudo response; status=0 indicates an error
|
|
348
428
|
const aborted = ctx.signal?.aborted || timeoutCtl?.signal.aborted;
|
|
429
|
+
// Aborts are expected (race loser cancellation, client disconnect, connect
|
|
430
|
+
// timeout) — not diagnostic. Only surface genuine network errors so a
|
|
431
|
+
// failing upstream isn't hidden behind status=0 pseudo-responses.
|
|
432
|
+
if (err && !aborted) reportSwallowed('proxyRetry.single-fetch', err);
|
|
349
433
|
return {
|
|
350
434
|
__networkError: true,
|
|
351
435
|
__aborted: !!aborted,
|
|
@@ -416,7 +500,17 @@ export async function executeRequest({ url, fetchOptions, retryConfig, ctx }) {
|
|
|
416
500
|
// headers well past 10s — so with retry disabled we must not introduce a new
|
|
417
501
|
// failure mode. The timeout applies only when a retry mode is active.
|
|
418
502
|
const effectiveConnectTimeoutMs = cfg.mode === 'off' ? 0 : cfg.connectTimeoutMs;
|
|
419
|
-
|
|
503
|
+
// streamIdleTimeoutMs is gated to retry modes only (NOT off), mirroring the
|
|
504
|
+
// connectTimeoutMs off-exclusion above. The watchdog wraps response.body in a
|
|
505
|
+
// TransformStream, but the interceptor (server/interceptor.js) already
|
|
506
|
+
// reconstructs response.body via getReader() + a new ReadableStream for
|
|
507
|
+
// logging/live-streaming; under concurrent load the watchdog's pipeThrough
|
|
508
|
+
// races that reconstruction and surfaces as a spurious `fetch failed` →
|
|
509
|
+
// status 0 → 502. off mode is the legacy pass-through path (no retry), so the
|
|
510
|
+
// watchdog's value (bound idle on a hung stream) is marginal here and the
|
|
511
|
+
// interceptor already observes the stream — serial/race/stagger keep the guard.
|
|
512
|
+
const effectiveStreamIdleMs = cfg.mode === 'off' ? 0 : (cfg.streamIdleTimeoutMs > 0 ? cfg.streamIdleTimeoutMs : 0);
|
|
513
|
+
const commonCtx = { dispatcher, connectTimeoutMs: effectiveConnectTimeoutMs, streamIdleTimeoutMs: effectiveStreamIdleMs };
|
|
420
514
|
|
|
421
515
|
if (cfg.mode === 'off' || cfg.mode === 'serial') {
|
|
422
516
|
// off / serial: serial retry. off = no retry (break on any status); serial = controlled by maxRetries (0=infinite, capped by deadline)
|
|
@@ -177,11 +177,16 @@ function generateProjectStats(projectDir, projectName, onlyFile) {
|
|
|
177
177
|
// Proxy retry shards (proxy_YYYY-MM-DD.jsonl) live at the project top level
|
|
178
178
|
// and can exist without any v2 session (proxy-only usage) — only bail out
|
|
179
179
|
// when BOTH are absent so aggregateProxyStats still runs for proxy-only dirs.
|
|
180
|
-
|
|
180
|
+
// Existence-only check: the full sorted file list is read once inside
|
|
181
|
+
// aggregateProxyStats (called below), so here we just need to know whether
|
|
182
|
+
// ANY proxy shard exists — short-circuit avoids a redundant full readdir+filter.
|
|
183
|
+
let hasProxyFiles = false;
|
|
181
184
|
try {
|
|
182
|
-
|
|
185
|
+
for (const f of readdirSync(projectDir)) {
|
|
186
|
+
if (f.startsWith('proxy_') && f.endsWith('.jsonl')) { hasProxyFiles = true; break; }
|
|
187
|
+
}
|
|
183
188
|
} catch { /* unreadable project dir → nothing to aggregate from it either */ }
|
|
184
|
-
if (sessionIds.length === 0 &&
|
|
189
|
+
if (sessionIds.length === 0 && !hasProxyFiles) return;
|
|
185
190
|
|
|
186
191
|
const filesStats = {};
|
|
187
192
|
const topModels = {};
|
|
@@ -249,7 +254,7 @@ function generateProjectStats(projectDir, projectName, onlyFile) {
|
|
|
249
254
|
|
|
250
255
|
// No parsable session yet (dirs without journals) — keep whatever exists,
|
|
251
256
|
// unless proxy shards are present (they alone justify a stats write).
|
|
252
|
-
if (Object.keys(filesStats).length === 0 &&
|
|
257
|
+
if (Object.keys(filesStats).length === 0 && !hasProxyFiles) return;
|
|
253
258
|
|
|
254
259
|
// 计算全局汇总
|
|
255
260
|
let totalRequests = 0;
|
|
@@ -12,6 +12,9 @@ import { listV1Files, listConvertibleProjects, readConvertState } from './conver
|
|
|
12
12
|
/** Pending v1 files + bytes of ONE project dir. */
|
|
13
13
|
function pendingOf(projectDir) {
|
|
14
14
|
const state = readConvertState(projectDir);
|
|
15
|
+
// Migration already completed — don't re-prompt, even if v1 files grew
|
|
16
|
+
// (dual-write captures new entries in v2).
|
|
17
|
+
if (state && state.status === 'done') return { files: 0, totalBytes: 0 };
|
|
15
18
|
const doneAtSize = new Map(
|
|
16
19
|
(state && Array.isArray(state.files) ? state.files : [])
|
|
17
20
|
.filter((f) => f && f.done)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{j as jsxRuntimeExports}from"./vendor-codemirror-tF6HNoR6.js";import{a as reactExports,I as Icon,b as _extends,R as React,q as Tag,S as Spin,E as Empty,t as Card,u as Statistic,v as Space,F as ForwardTable,n as Switch,i as Button,k as RefIcon$3,w as Segmented,l as Select,x as TypedInputNumber,T as Tooltip,s as staticMethods}from"./vendor-antd-DADYo_zg.js";import{t,a as apiUrl}from"./index-mpHzrAV9.js";import{a5 as reportSwallowed,C as ConceptHelp}from"./seqResourceLoaders-DMyPYdA-.js";import"./vendor-mdxeditor-CFAmRN3Y.js";import"./vendor-xterm-DckPrCx1.js";import"./vendor-markdown-DOJHsAxX.js";import"./vendor-highlight-IFTJ9nxW.js";import"./vendor-virtuoso-TxPxvPjL.js";var CheckCircleOutlined$1={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M699 353h-46.9c-10.2 0-19.9 4.9-25.9 13.3L469 584.3l-71.2-98.8c-6-8.3-15.6-13.3-25.9-13.3H325c-6.5 0-10.3 7.4-6.5 12.7l124.6 172.8a31.8 31.8 0 0051.7 0l210.6-292c3.9-5.3.1-12.7-6.4-12.7z"}},{tag:"path",attrs:{d:"M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z"}}]},name:"check-circle",theme:"outlined"},CheckCircleOutlined=function(props,ref){return reactExports.createElement(Icon,_extends({},props,{ref:ref,icon:CheckCircleOutlined$1}))},RefIcon$2=reactExports.forwardRef(CheckCircleOutlined),CloseCircleOutlined$1={icon:{tag:"svg",attrs:{"fill-rule":"evenodd",viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M512 64c247.4 0 448 200.6 448 448S759.4 960 512 960 64 759.4 64 512 264.6 64 512 64zm0 76c-205.4 0-372 166.6-372 372s166.6 372 372 372 372-166.6 372-372-166.6-372-372-372zm128.01 198.83c.03 0 .05.01.09.06l45.02 45.01a.2.2 0 01.05.09.12.12 0 010 .07c0 .02-.01.04-.05.08L557.25 512l127.87 127.86a.27.27 0 01.05.06v.02a.12.12 0 010 .07c0 .03-.01.05-.05.09l-45.02 45.02a.2.2 0 01-.09.05.12.12 0 01-.07 0c-.02 0-.04-.01-.08-.05L512 557.25 384.14 685.12c-.04.04-.06.05-.08.05a.12.12 0 01-.07 0c-.03 0-.05-.01-.09-.05l-45.02-45.02a.2.2 0 01-.05-.09.12.12 0 010-.07c0-.02.01-.04.06-.08L466.75 512 338.88 384.14a.27.27 0 01-.05-.06l-.01-.02a.12.12 0 010-.07c0-.03.01-.05.05-.09l45.02-45.02a.2.2 0 01.09-.05.12.12 0 01.07 0c.02 0 .04.01.08.06L512 466.75l127.86-127.86c.04-.05.06-.06.08-.06a.12.12 0 01.07 0z"}}]},name:"close-circle",theme:"outlined"},CloseCircleOutlined=function(props,ref){return reactExports.createElement(Icon,_extends({},props,{ref:ref,icon:CloseCircleOutlined$1}))},RefIcon$1=reactExports.forwardRef(CloseCircleOutlined),UndoOutlined$1={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M511.4 124C290.5 124.3 112 303 112 523.9c0 128 60.2 242 153.8 315.2l-37.5 48c-4.1 5.3-.3 13 6.3 12.9l167-.8c5.2 0 9-4.9 7.7-9.9L369.8 727a8 8 0 00-14.1-3L315 776.1c-10.2-8-20-16.7-29.3-26a318.64 318.64 0 01-68.6-101.7C200.4 609 192 567.1 192 523.9s8.4-85.1 25.1-124.5c16.1-38.1 39.2-72.3 68.6-101.7 29.4-29.4 63.6-52.5 101.7-68.6C426.9 212.4 468.8 204 512 204s85.1 8.4 124.5 25.1c38.1 16.1 72.3 39.2 101.7 68.6 29.4 29.4 52.5 63.6 68.6 101.7 16.7 39.4 25.1 81.3 25.1 124.5s-8.4 85.1-25.1 124.5a318.64 318.64 0 01-68.6 101.7c-7.5 7.5-15.3 14.5-23.4 21.2a7.93 7.93 0 00-1.2 11.1l39.4 50.5c2.8 3.5 7.9 4.1 11.4 1.3C854.5 760.8 912 649.1 912 523.9c0-221.1-179.4-400.2-400.6-399.9z"}}]},name:"undo",theme:"outlined"},UndoOutlined=function(props,ref){return reactExports.createElement(Icon,_extends({},props,{ref:ref,icon:UndoOutlined$1}))},RefIcon=reactExports.forwardRef(UndoOutlined);const styles$3_wrap="_wrap_1glwx_3",styles$3_svg="_svg_1glwx_7",styles$3_bar="_bar_1glwx_12",styles$3_axisLine="_axisLine_1glwx_21",styles$3_gridLine="_gridLine_1glwx_25",styles$3_vLabel="_vLabel_1glwx_30",styles$3_vValue="_vValue_1glwx_34",styles$3_legend="_legend_1glwx_38",styles$3_legendItem="_legendItem_1glwx_46",styles$3_legendSwatch="_legendSwatch_1glwx_51";function groupedBarTitle(label2,value,bi,legend2,valueFormatter){const formatted=valueFormatter(value),name=Array.isArray(legend2)?legend2[bi]:void 0;return name?`${label2} (${name}): ${formatted}`:`${label2}: ${formatted}`}function singleBarTitle(label2,value,valueFormatter){return`${label2}: ${valueFormatter(value)}`}const DEFAULT_COLORS=["var(--color-primary, #1668dc)","var(--color-success, #22c55e)","var(--color-warning, #f59e0b)"];const BarChart$1=React.memo(function({data:data=[],height:height=160,grouped:grouped=!1,valueFormatter:valueFormatter=n=>String(n),maxBars:maxBars,legend:legend2,ariaLabel:ariaLabel}){var _a,_b;const wrapRef=reactExports.useRef(null),[width,setWidth]=reactExports.useState(0);reactExports.useEffect(()=>{if(!wrapRef.current)return;const el=wrapRef.current,update=()=>setWidth(el.clientWidth||0);if(update(),"undefined"==typeof ResizeObserver)return;const ro=new ResizeObserver(update);return ro.observe(el),()=>{try{ro.disconnect()}catch{}}},[]);const rows=function(data,{maxBars:maxBars,grouped:grouped=!1}={}){var _a,_b;if(!maxBars||!Array.isArray(data)||data.length<=maxBars)return data;const head=data.slice(0,maxBars),tail=data.slice(maxBars);if(grouped){const seriesCount=(null==(_b=null==(_a=head[0])?void 0:_a.values)?void 0:_b.length)||1,sums=new Array(seriesCount).fill(0);for(const d of tail){const vals=d.values||[];for(let i=0;i<seriesCount;i++)sums[i]+=vals[i]||0}return[...head,{label:"…",value:sums[0],values:sums}]}const otherCount=tail.reduce((s,d)=>s+(d.value||0),0);return[...head,{label:"…",value:otherCount,values:[otherCount]}]}(Array.isArray(data)?data:[],{maxBars:maxBars,grouped:grouped});if(!rows||0===rows.length||0===width)return jsxRuntimeExports.jsx("div",{ref:wrapRef,className:styles$3_wrap,style:{height:height}});const legendEl=grouped&&Array.isArray(legend2)&&legend2.length>0?jsxRuntimeExports.jsx("div",{className:styles$3_legend,"aria-hidden":"true",children:legend2.map((label2,i)=>jsxRuntimeExports.jsxs("span",{className:styles$3_legendItem,children:[jsxRuntimeExports.jsx("span",{className:styles$3_legendSwatch,style:{background:DEFAULT_COLORS[i%DEFAULT_COLORS.length]}}),label2]},i))}):null,innerW=Math.max(0,width-28),innerH=Math.max(0,height-28-16),maxVal=rows.reduce((m,d)=>{const vals=grouped?d.values||[0]:[d.value||0];for(const v of vals)v>m&&(m=v);return m},1),groupW=innerW/rows.length,barW=grouped?Math.max(2,.6*groupW/Math.max(1,(null==(_b=null==(_a=rows[0])?void 0:_a.values)?void 0:_b.length)||1)):Math.max(2,.5*groupW),baseline=28+innerH;return jsxRuntimeExports.jsxs("div",{ref:wrapRef,className:styles$3_wrap,style:{height:height},children:[legendEl,jsxRuntimeExports.jsxs("svg",{className:styles$3_svg,height:height,width:width,role:"img","aria-label":ariaLabel,children:[[.25,.5,.75].map(f=>jsxRuntimeExports.jsx("line",{className:styles$3_gridLine,x1:28,x2:width,y1:28+innerH*(1-f),y2:28+innerH*(1-f)},f)),jsxRuntimeExports.jsx("line",{className:styles$3_axisLine,x1:28,x2:width,y1:baseline,y2:baseline}),rows.map((d,gi)=>{var _a2,_b2;const gx=28+gi*groupW+(groupW-(grouped?((null==(_b2=null==(_a2=rows[0])?void 0:_a2.values)?void 0:_b2.length)||1)*barW:barW))/2,vals=grouped?d.values||[0]:[d.value||0];return jsxRuntimeExports.jsxs("g",{children:[vals.map((v,bi)=>{const h=Math.max(0,v/maxVal*innerH),x=gx+bi*barW,y=baseline-h,color=grouped?DEFAULT_COLORS[bi%DEFAULT_COLORS.length]:DEFAULT_COLORS[0];return jsxRuntimeExports.jsxs("g",{children:[jsxRuntimeExports.jsx("rect",{className:styles$3_bar,x:x,y:y,width:Math.max(1,barW-1),height:h,fill:color,rx:1,children:jsxRuntimeExports.jsx("title",{children:grouped?groupedBarTitle(d.label,v,bi,legend2,valueFormatter):singleBarTitle(d.label,v,valueFormatter)})}),!grouped&&barW>=18&&jsxRuntimeExports.jsx("text",{className:styles$3_vValue,x:x+barW/2,y:Math.max(8,y-4),textAnchor:"middle",children:valueFormatter(v)})]},bi)}),jsxRuntimeExports.jsx("text",{className:styles$3_vLabel,x:28+gi*groupW+groupW/2,y:height-4,textAnchor:"middle",children:String(d.label).slice(0,8)})]},gi)})]})]})});function fmtMs(ms){return ms>0?ms<1e3?`${Math.round(ms)} ms`:`${(ms/1e3).toFixed(2)} s`:"-"}function statusColor(status){return 0===status?"default":status<400?"green":status<500?"orange":"red"}function availColor(pct){return pct>=95?"var(--color-success, #22c55e)":pct>=80?"var(--color-warning, #f59e0b)":"var(--color-error, #ef4444)"}function dominantFailCell(bucket){return bucket&&null!=bucket.dominantFailStatus?jsxRuntimeExports.jsxs(Tag,{color:statusColor(bucket.dominantFailStatus),children:[bucket.dominantFailStatus," × ",bucket.dominantFailCount]}):null}const styles$2_embedded="_embedded_sdeqo_7",styles$2_centerState="_centerState_sdeqo_11",styles$2_header="_header_sdeqo_20",styles$2_cardRow="_cardRow_sdeqo_23",styles$2_statCard="_statCard_sdeqo_33",styles$2_section="_section_sdeqo_70",styles$2_streakRow="_streakRow_sdeqo_83",styles$2_chartWrap="_chartWrap_sdeqo_91",styles$2_chartTitle="_chartTitle_sdeqo_96",styles$2_codeTags="_codeTags_sdeqo_105",styles$2_topRow="_topRow_sdeqo_112",styles$2_rescuedBadge="_rescuedBadge_sdeqo_115",fmtPercent=n=>`${n}%`;function ProxyStatsDashboard(){const[data,setData]=reactExports.useState(null),[loading,setLoading]=reactExports.useState(!0),[autoRefresh,setAutoRefresh]=reactExports.useState(!0),fetchData=reactExports.useCallback(()=>{setLoading(!0),fetch(apiUrl("/api/proxy-stats")).then(res=>{if(!res.ok)throw new Error(`HTTP ${res.status}`);return res.json()}).then(d=>{setData(d.proxyStats),setLoading(!1)}).catch(err=>{reportSwallowed("proxyStats.fetch",err),setLoading(!1)})},[]);reactExports.useEffect(()=>{fetchData()},[fetchData]),reactExports.useEffect(()=>{if(!autoRefresh)return;const id=setInterval(fetchData,15e3);return()=>clearInterval(id)},[autoRefresh,fetchData]);const byModelBars=reactExports.useMemo(()=>((null==data?void 0:data.byModel)||[]).map(m=>({label:m.model,values:[m.upstreamAvailabilityPct,m.availabilityPct]})),[data]),retryDistBars=reactExports.useMemo(()=>((null==data?void 0:data.retryDistribution)||[]).map(d=>({label:String(d.retries),value:d.count})),[data]),retryBurdenBars=reactExports.useMemo(()=>((null==data?void 0:data.retryBurden)||[]).map(b=>{return{label:(key=b.key,t(`ui.proxyStats.retryBurdenBuckets.${key}`)),value:b.count};var key}),[data]),recentRecords=reactExports.useMemo(()=>((null==data?void 0:data.recentRecords)||[]).filter(r=>{const status=Number(r.final_status);return 0===status||status>=400||(Number(r.retries)||0)>0}),[data]);if(loading&&!data)return jsxRuntimeExports.jsx("div",{className:`${styles$2_embedded} ${styles$2_centerState}`,children:jsxRuntimeExports.jsx(Spin,{size:"large"})});if(!data||!data.summary||0===data.summary.totalRequests)return jsxRuntimeExports.jsxs("div",{className:styles$2_embedded,children:[jsxRuntimeExports.jsx(DashboardHeader,{onRefresh:fetchData,autoRefresh:autoRefresh,setAutoRefresh:setAutoRefresh}),jsxRuntimeExports.jsx("div",{className:styles$2_centerState,children:jsxRuntimeExports.jsx(Empty,{description:t("ui.proxyStats.noData")})})]});const s=data.summary,rescued=Math.max(0,s.totalSucceeded-s.totalFirstOk);return jsxRuntimeExports.jsxs("div",{className:styles$2_embedded,children:[jsxRuntimeExports.jsx(DashboardHeader,{onRefresh:fetchData,autoRefresh:autoRefresh,setAutoRefresh:setAutoRefresh}),jsxRuntimeExports.jsxs("div",{className:styles$2_cardRow,children:[jsxRuntimeExports.jsx(Card,{className:styles$2_statCard,children:jsxRuntimeExports.jsx(Statistic,{title:t("ui.proxyStats.totalRequests"),value:s.totalRequests})}),jsxRuntimeExports.jsx(Card,{className:styles$2_statCard,children:jsxRuntimeExports.jsx(Statistic,{title:t("ui.proxyStats.totalRetries"),value:s.totalRetries,suffix:s.totalRequests?`(${(s.totalRetries/s.totalRequests).toFixed(1)}/req)`:""})}),jsxRuntimeExports.jsx(Card,{className:styles$2_statCard,style:{"--stat-accent":availColor(s.upstreamAvailabilityPct)},children:jsxRuntimeExports.jsx(Statistic,{title:t("ui.proxyStats.upstreamAvailability"),value:s.upstreamAvailabilityPct,suffix:"%",valueStyle:{color:availColor(s.upstreamAvailabilityPct)}})}),jsxRuntimeExports.jsxs(Card,{className:styles$2_statCard,style:{"--stat-accent":availColor(s.downstreamAvailabilityPct)},children:[jsxRuntimeExports.jsx(Statistic,{title:t("ui.proxyStats.downstreamAvailability"),value:s.downstreamAvailabilityPct,suffix:"%",valueStyle:{color:availColor(s.downstreamAvailabilityPct)}}),rescued>0&&jsxRuntimeExports.jsx("span",{className:styles$2_rescuedBadge,children:t("ui.proxyStats.rescuedByRetry",{count:rescued})})]}),jsxRuntimeExports.jsx(Card,{className:styles$2_statCard,style:{"--stat-accent":"var(--color-error, #ef4444)"},children:jsxRuntimeExports.jsx(Statistic,{title:t("ui.proxyStats.failedRequests"),value:s.totalFailed,valueStyle:{color:"var(--color-error, #ef4444)"}})}),jsxRuntimeExports.jsx(Card,{className:styles$2_statCard,children:jsxRuntimeExports.jsx(Statistic,{title:t("ui.proxyStats.p95Duration"),value:fmtMs(s.p95Ms)})})]}),jsxRuntimeExports.jsxs(Card,{title:t("ui.proxyStats.availabilityAnalysis"),className:styles$2_section,children:[jsxRuntimeExports.jsxs("div",{className:styles$2_streakRow,children:[jsxRuntimeExports.jsxs(Space,{children:[jsxRuntimeExports.jsxs("span",{children:[t("ui.proxyStats.currentStreak"),":"]}),jsxRuntimeExports.jsxs(Tag,{icon:"success"===s.currentStreakType?jsxRuntimeExports.jsx(RefIcon$2,{}):jsxRuntimeExports.jsx(RefIcon$1,{}),color:"success"===s.currentStreakType?"green":"red",children:["success"===s.currentStreakType?t("ui.proxyStats.success"):t("ui.proxyStats.failure")," × ",s.currentStreakCount]})]}),jsxRuntimeExports.jsxs(Space,{children:[jsxRuntimeExports.jsxs("span",{children:[t("ui.proxyStats.worstFailureStreak"),":"]}),jsxRuntimeExports.jsx(Tag,{color:"red",children:s.worstFailureStreak})]})]}),jsxRuntimeExports.jsxs("div",{className:styles$2_chartWrap,children:[jsxRuntimeExports.jsx("div",{className:styles$2_chartTitle,children:t("ui.proxyStats.upstreamVsDownstream")}),jsxRuntimeExports.jsx(BarChart$1,{height:180,grouped:!0,legend:[t("ui.proxyStats.upstreamAvailability"),t("ui.proxyStats.downstreamAvailability")],ariaLabel:t("ui.proxyStats.upstreamVsDownstream"),data:byModelBars,valueFormatter:fmtPercent,maxBars:10})]}),jsxRuntimeExports.jsx(ForwardTable,{dataSource:data.byModel||[],rowKey:"model",size:"small",pagination:!1,className:styles$2_section,columns:[{title:t("ui.proxyStats.byModel"),dataIndex:"model",key:"model"},{title:t("ui.proxyStats.totalRequests"),dataIndex:"requests",key:"requests"},{title:t("ui.proxyStats.retries"),dataIndex:"retries",key:"retries"},{title:t("ui.proxyStats.upstreamAvailability"),dataIndex:"upstreamAvailabilityPct",key:"upAvail",render:v=>`${v}%`},{title:t("ui.proxyStats.downstreamAvailability"),dataIndex:"availabilityPct",key:"dsAvail",render:v=>`${v}%`},{title:t("ui.proxyStats.dominantFail"),key:"domFail",render:(_,r)=>dominantFailCell(r)||"-"},{title:t("ui.proxyStats.p95Duration"),dataIndex:"p95Ms",key:"p95",render:fmtMs}]})]}),jsxRuntimeExports.jsxs(Card,{title:t("ui.proxyStats.retryDistribution"),className:styles$2_section,children:[jsxRuntimeExports.jsx("div",{className:styles$2_chartWrap,children:jsxRuntimeExports.jsx(BarChart$1,{height:160,ariaLabel:t("ui.proxyStats.retryDistribution"),data:retryDistBars,maxBars:15})}),jsxRuntimeExports.jsxs("div",{className:styles$2_chartWrap,children:[jsxRuntimeExports.jsx("div",{className:styles$2_chartTitle,children:t("ui.proxyStats.retryBurden")}),jsxRuntimeExports.jsx(BarChart$1,{height:160,ariaLabel:t("ui.proxyStats.retryBurden"),data:retryBurdenBars})]}),jsxRuntimeExports.jsxs("div",{className:styles$2_codeTags,children:[jsxRuntimeExports.jsxs("span",{children:[t("ui.proxyStats.retryCodes"),":"]}),jsxRuntimeExports.jsx(Space,{wrap:!0,children:(data.retryCodes||[]).map(c=>jsxRuntimeExports.jsxs(Tag,{color:statusColor(c.code),children:[c.code," × ",c.count]},c.code))})]})]}),jsxRuntimeExports.jsxs(Card,{title:t("ui.proxyStats.durationAnalysis"),className:styles$2_section,children:[jsxRuntimeExports.jsxs("div",{className:styles$2_cardRow,children:[jsxRuntimeExports.jsx(Card,{className:styles$2_statCard,children:jsxRuntimeExports.jsx(Statistic,{title:t("ui.proxyStats.p50Duration"),value:fmtMs(s.p50Ms)})}),jsxRuntimeExports.jsx(Card,{className:styles$2_statCard,children:jsxRuntimeExports.jsx(Statistic,{title:t("ui.proxyStats.p95Duration"),value:fmtMs(s.p95Ms)})}),jsxRuntimeExports.jsx(Card,{className:styles$2_statCard,children:jsxRuntimeExports.jsx(Statistic,{title:t("ui.proxyStats.p99Duration"),value:fmtMs(s.p99Ms)})}),jsxRuntimeExports.jsx(Card,{className:styles$2_statCard,children:jsxRuntimeExports.jsx(Statistic,{title:t("ui.proxyStats.maxDuration"),value:fmtMs(s.maxMs)})}),jsxRuntimeExports.jsx(Card,{className:styles$2_statCard,children:jsxRuntimeExports.jsx(Statistic,{title:t("ui.proxyStats.avgDuration"),value:fmtMs(s.avgMs)})})]}),jsxRuntimeExports.jsxs("div",{className:styles$2_topRow,children:[jsxRuntimeExports.jsx(DurationRecordTable,{title:t("ui.proxyStats.slowest"),record:data.slowest}),jsxRuntimeExports.jsx(DurationRecordTable,{title:t("ui.proxyStats.fastest"),record:data.fastest})]})]}),data.byPath&&data.byPath.length>0&&jsxRuntimeExports.jsx(Card,{title:t("ui.proxyStats.byPath"),className:styles$2_section,children:jsxRuntimeExports.jsx(ForwardTable,{dataSource:data.byPath,rowKey:"path",size:"small",pagination:!1,columns:[{title:t("ui.proxyStats.colPath"),dataIndex:"path",key:"path",ellipsis:!0},{title:t("ui.proxyStats.totalRequests"),dataIndex:"requests",key:"requests"},{title:t("ui.proxyStats.retries"),dataIndex:"retries",key:"retries"},{title:t("ui.proxyStats.downstreamAvailability"),dataIndex:"availabilityPct",key:"avail",render:v=>`${v}%`},{title:t("ui.proxyStats.dominantFail"),key:"domFail",render:(_,r)=>dominantFailCell(r)||"-"}]})}),data.byProfile&&data.byProfile.length>0&&jsxRuntimeExports.jsx(Card,{title:t("ui.proxyStats.byProfile"),className:styles$2_section,children:jsxRuntimeExports.jsx(ForwardTable,{dataSource:data.byProfile,rowKey:"profile_id",size:"small",pagination:!1,columns:[{title:t("ui.proxyStats.byProfile"),dataIndex:"profile_name",key:"profile_name"},{title:t("ui.proxyStats.totalRequests"),dataIndex:"requests",key:"requests"},{title:t("ui.proxyStats.retries"),dataIndex:"retries",key:"retries"},{title:t("ui.proxyStats.upstreamAvailability"),dataIndex:"upstreamAvailabilityPct",key:"upAvail",render:v=>`${v}%`},{title:t("ui.proxyStats.downstreamAvailability"),dataIndex:"availabilityPct",key:"dsAvail",render:v=>`${v}%`},{title:t("ui.proxyStats.dominantFail"),key:"domFail",render:(_,r)=>dominantFailCell(r)||"-"},{title:t("ui.proxyStats.p95Duration"),dataIndex:"p95Ms",key:"p95",render:fmtMs}]})}),jsxRuntimeExports.jsx(Card,{title:t("ui.proxyStats.recentErrors"),className:styles$2_section,children:jsxRuntimeExports.jsx(ForwardTable,{dataSource:recentRecords,rowKey:"ts",size:"small",pagination:{pageSize:20,size:"small"},scroll:{x:"max-content"},columns:[{title:t("ui.proxyStats.colTime"),dataIndex:"ts",key:"ts",width:180,ellipsis:!0,render:v=>v?new Date(v).toLocaleString():"-"},{title:t("ui.proxyStats.colMethod"),dataIndex:"method",key:"method",width:70},{title:t("ui.proxyStats.colPath"),dataIndex:"path",key:"path",ellipsis:!0},{title:t("ui.proxyStats.colModel"),dataIndex:"model",key:"model",ellipsis:!0},{title:t("ui.proxyStats.colStatus"),dataIndex:"final_status",key:"status",width:70,render:v=>jsxRuntimeExports.jsx(Tag,{color:statusColor(v),children:v||"-"})},{title:t("ui.proxyStats.attempts"),dataIndex:"attempts",key:"attempts",width:80},{title:t("ui.proxyStats.retries"),dataIndex:"retries",key:"retries",width:80},{title:t("ui.proxyStats.colDuration"),dataIndex:"duration_ms",key:"dur",width:100,render:fmtMs},{title:t("ui.proxyStats.retryCodes"),dataIndex:"retry_codes",key:"rc",width:120,render:v=>v&&v.length?v.map(c=>jsxRuntimeExports.jsx(Tag,{color:statusColor(c),children:c},c)):"-"}]})})]})}function DurationRecordTable({title:title,record:record}){return jsxRuntimeExports.jsx(ForwardTable,{title:()=>title,dataSource:record?[record]:[],rowKey:"ts",size:"small",pagination:!1,columns:[{title:t("ui.proxyStats.colPath"),dataIndex:"path",key:"path",ellipsis:!0},{title:t("ui.proxyStats.colModel"),dataIndex:"model",key:"model",ellipsis:!0},{title:t("ui.proxyStats.attempts"),dataIndex:"attempts",key:"attempts"},{title:t("ui.proxyStats.colDuration"),dataIndex:"duration_ms",key:"dur",render:fmtMs}]})}function DashboardHeader({onRefresh:onRefresh,autoRefresh:autoRefresh,setAutoRefresh:setAutoRefresh}){return jsxRuntimeExports.jsx("div",{className:styles$2_header,children:jsxRuntimeExports.jsxs(Space,{children:[jsxRuntimeExports.jsx("span",{children:t("ui.proxyStats.autoRefresh")}),jsxRuntimeExports.jsx(Switch,{checked:autoRefresh,onChange:setAutoRefresh,size:"small","aria-label":t("ui.proxyStats.autoRefresh")}),jsxRuntimeExports.jsx(Button,{size:"small",icon:jsxRuntimeExports.jsx(RefIcon$3,{}),onClick:onRefresh,children:t("ui.proxyStats.refresh")})]})})}const styles$1_form="_form_ntb31_3",styles$1_configGrid="_configGrid_ntb31_9",styles$1_configGroup="_configGroup_ntb31_18",styles$1_groupTitle="_groupTitle_ntb31_26",styles$1_row="_row_ntb31_41",styles$1_label="_label_ntb31_53",styles$1_inputCell="_inputCell_ntb31_68",styles$1_hint="_hint_ntb31_76",styles$1_fullWidth="_fullWidth_ntb31_83",styles$1_numField="_numField_ntb31_87",styles$1_numInput="_numInput_ntb31_96",styles$1_unit="_unit_ntb31_101",styles$1_reservedTag="_reservedTag_ntb31_106",styles$1_footer="_footer_ntb31_115",styles$1_footerRight="_footerRight_ntb31_123",styles$1_note="_note_ntb31_128",MODE_VALUES=["off","serial","race","stagger"],STATUS_CODE_OPTIONS=[429,500,502,503,504,508,529,408],NUMERIC_FIELDS=[{key:"retryIntervalMs",unit:"ms",min:0,max:6e5},{key:"retryInterval429Ms",unit:"ms",min:0,max:6e5},{key:"maxRetries",unit:"",min:0,max:1e3},{key:"maxConcurrent",unit:"",min:1,max:100},{key:"connectTimeoutMs",unit:"ms",min:0,max:6e5}];function RetryConfigForm({config:config,defaults:defaults,onSave:onSave}){const[form2,setForm]=reactExports.useState(null),[dirty,setDirty]=reactExports.useState(!1),initializedRef=reactExports.useRef(!1);reactExports.useEffect(()=>{config&&(initializedRef.current?dirty||setForm({...config}):(setForm({...config}),initializedRef.current=!0))},[config,dirty]);const def=defaults||{},patch=(key,value)=>{setDirty(!0),setForm(prev=>prev?{...prev,[key]:value}:prev)},renderResetBtn=key=>(key=>JSON.stringify(null==form2?void 0:form2[key])!==JSON.stringify(def[key]))(key)?jsxRuntimeExports.jsx(Tooltip,{title:t("ui.retryConfig.resetDefault"),children:jsxRuntimeExports.jsx(Button,{type:"text",size:"small",icon:jsxRuntimeExports.jsx(RefIcon,{}),onClick:()=>(key=>{setDirty(!0),setForm(prev=>prev?{...prev,[key]:def[key]}:prev)})(key)})}):null;return form2?jsxRuntimeExports.jsxs("div",{className:styles$1_form,children:[jsxRuntimeExports.jsxs("div",{className:styles$1_configGrid,children:[jsxRuntimeExports.jsxs("section",{className:styles$1_configGroup,children:[jsxRuntimeExports.jsx("div",{className:styles$1_groupTitle,children:t("ui.retryConfig.groupStrategy")}),jsxRuntimeExports.jsxs("div",{className:styles$1_row,children:[jsxRuntimeExports.jsxs("span",{className:styles$1_label,children:[t("ui.retryConfig.mode"),renderResetBtn("mode")]}),jsxRuntimeExports.jsxs("div",{className:styles$1_inputCell,children:[jsxRuntimeExports.jsx(Segmented,{value:form2.mode||"off",options:MODE_VALUES.map(v=>({value:v,label:t(`ui.retryConfig.modeLabel.${v}`)})),onChange:v=>patch("mode",v)}),jsxRuntimeExports.jsx("div",{className:styles$1_hint,children:t(`ui.retryConfig.modeHint.${form2.mode||"off"}`)})]})]}),jsxRuntimeExports.jsxs("div",{className:styles$1_row,children:[jsxRuntimeExports.jsxs("span",{className:styles$1_label,children:[t("ui.retryConfig.statusCodes"),renderResetBtn("retryStatusCodes")]}),jsxRuntimeExports.jsxs("div",{className:styles$1_inputCell,children:[jsxRuntimeExports.jsx(Select,{mode:"multiple",className:styles$1_fullWidth,value:form2.retryStatusCodes||[],onChange:v=>patch("retryStatusCodes",v.map(Number).filter(n=>Number.isFinite(n)&&n>0)),options:STATUS_CODE_OPTIONS.map(c=>({label:String(c),value:c}))}),jsxRuntimeExports.jsx("div",{className:styles$1_hint,children:t("ui.retryConfig.statusCodesHint")})]})]})]}),jsxRuntimeExports.jsxs("section",{className:styles$1_configGroup,children:[jsxRuntimeExports.jsx("div",{className:styles$1_groupTitle,children:t("ui.retryConfig.groupExecution")}),NUMERIC_FIELDS.map(f=>jsxRuntimeExports.jsxs("div",{className:styles$1_row,children:[jsxRuntimeExports.jsxs("span",{className:styles$1_label,children:[t(`ui.retryConfig.${f.key}`),renderResetBtn(f.key)]}),jsxRuntimeExports.jsx("div",{className:styles$1_inputCell,children:jsxRuntimeExports.jsxs("div",{className:styles$1_numField,children:[jsxRuntimeExports.jsx(TypedInputNumber,{className:styles$1_numInput,value:form2[f.key],min:f.min,max:f.max,onChange:v=>patch(f.key,v)}),f.unit&&jsxRuntimeExports.jsx("span",{className:styles$1_unit,children:f.unit})]})})]},f.key)),jsxRuntimeExports.jsxs("div",{className:styles$1_row,children:[jsxRuntimeExports.jsxs("span",{className:styles$1_label,children:[t("ui.retryConfig.streamIdleTimeoutMs"),jsxRuntimeExports.jsx(Tooltip,{title:t("ui.retryConfig.streamIdleReserved"),children:jsxRuntimeExports.jsx("span",{className:styles$1_reservedTag,children:t("ui.retryConfig.reserved")})})]}),jsxRuntimeExports.jsx("div",{className:styles$1_inputCell,children:jsxRuntimeExports.jsxs("div",{className:styles$1_numField,children:[jsxRuntimeExports.jsx(TypedInputNumber,{className:styles$1_numInput,value:form2.streamIdleTimeoutMs,disabled:!0}),jsxRuntimeExports.jsx("span",{className:styles$1_unit,children:"ms"})]})})]})]})]}),jsxRuntimeExports.jsxs("div",{className:styles$1_footer,children:[jsxRuntimeExports.jsx(Button,{icon:jsxRuntimeExports.jsx(RefIcon,{}),onClick:()=>{setDirty(!0),setForm({...def})},children:t("ui.retryConfig.resetAll")}),jsxRuntimeExports.jsx("div",{className:styles$1_footerRight,children:jsxRuntimeExports.jsx(Button,{type:"primary",onClick:async()=>{if(form2)if(MODE_VALUES.includes(form2.mode))try{await onSave(form2),staticMethods.success(t("ui.retryConfig.saved")),setDirty(!1)}catch(err){reportSwallowed("retryConfig.save",err)}else staticMethods.warning(t("ui.retryConfig.invalidMode"))},children:t("ui.retryConfig.save")})})]}),jsxRuntimeExports.jsx("div",{className:styles$1_note,children:t("ui.retryConfig.note")})]}):jsxRuntimeExports.jsx("div",{className:styles$1_form,style:{display:"flex",justifyContent:"center",alignItems:"center",padding:"40px 0"},children:jsxRuntimeExports.jsx(Spin,{size:"small"})})}const styles_shell="_shell_1po19_3",styles_toolbar="_toolbar_1po19_5",styles_toolbarTitle="_toolbarTitle_1po19_16",styles_toolbarActions="_toolbarActions_1po19_27",styles_body="_body_1po19_39",styles_configFormWrap="_configFormWrap_1po19_43",styles_configHeader="_configHeader_1po19_44",styles_configHeaderTitle="_configHeaderTitle_1po19_45",styles_configScroll="_configScroll_1po19_35";function ProxyStatsModal({retryConfig:retryConfig,retryDefaults:retryDefaults,onRetryConfigChange:onRetryConfigChange}){const[tab,setTab]=reactExports.useState("config");return jsxRuntimeExports.jsxs("div",{className:styles_shell,children:[jsxRuntimeExports.jsxs("div",{className:styles_toolbar,children:[jsxRuntimeExports.jsx("h3",{className:styles_toolbarTitle,children:t("ui.proxyStats.title")}),jsxRuntimeExports.jsx("div",{className:styles_toolbarActions,children:jsxRuntimeExports.jsx(Segmented,{size:"small",value:tab,onChange:setTab,options:[{label:t("ui.proxyStats.tabConfig"),value:"config"},{label:t("ui.proxyStats.tabStats"),value:"stats"}]})})]}),jsxRuntimeExports.jsx("div",{className:styles_body,children:"config"===tab?jsxRuntimeExports.jsx("div",{className:styles_configScroll,children:jsxRuntimeExports.jsxs("div",{className:styles_configFormWrap,children:[jsxRuntimeExports.jsxs("div",{className:styles_configHeader,children:[jsxRuntimeExports.jsx("span",{className:styles_configHeaderTitle,children:t("ui.retryConfig.title")}),jsxRuntimeExports.jsx(ConceptHelp,{doc:"RetryConfig",zIndex:1100})]}),jsxRuntimeExports.jsx(RetryConfigForm,{config:retryConfig,defaults:retryDefaults,onSave:async formData=>{await onRetryConfigChange(formData)}})]})}):jsxRuntimeExports.jsx(ProxyStatsDashboard,{})})]})}export{ProxyStatsModal as default};
|