fix: address CodeRabbit review for per-site probe
- Fix SSE abort: propagate AbortSignal to worker pool; close connection
immediately on client disconnect without sending 'complete' event
- Fix Boolean coercion: use strict === true/=== 1 check instead of Boolean()
to avoid Boolean('false') === true foot-gun
- Fix probe-now: parse and forward latencyThresholdMs from request body
- Fix api.ts probeSiteNow: add latencyThresholdMs to options type
- Fix backup import: restore postRefreshProbe* fields when inserting sites
so that backup/restore round-trips preserve per-site probe config
- Fix latency threshold persistence: add postRefreshProbeLatencyThresholdMs
column (migration 0026), save/load it in site editor, pass to
runPostRefreshProbeIfEnabled so auto-probe also respects the threshold
- Fix state management: setProbeCompleted(false) at probe start; error
branch also refreshes model lists and sets probeCompleted to show results
- Regenerate schema artifacts (mysql/postgres bootstrap+upgrade, contract)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
CREATE TABLE IF NOT EXISTS `sites` (`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `platform` TEXT NOT NULL, `status` VARCHAR(191) NOT NULL DEFAULT 'active', `api_key` TEXT, `created_at` VARCHAR(191) DEFAULT (DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s')), `updated_at` VARCHAR(191) DEFAULT (DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s')), `is_pinned` BOOLEAN DEFAULT false, `sort_order` INT DEFAULT 0, `proxy_url` TEXT, `use_system_proxy` BOOLEAN DEFAULT false, `custom_headers` JSON, `external_checkin_url` TEXT, `global_weight` DOUBLE DEFAULT 1, `post_refresh_probe_enabled` BOOLEAN DEFAULT false, `post_refresh_probe_model` VARCHAR(191) DEFAULT '', `post_refresh_probe_scope` VARCHAR(191) DEFAULT 'single');
|
||||
CREATE TABLE IF NOT EXISTS `sites` (`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `platform` TEXT NOT NULL, `status` VARCHAR(191) NOT NULL DEFAULT 'active', `api_key` TEXT, `created_at` VARCHAR(191) DEFAULT (DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s')), `updated_at` VARCHAR(191) DEFAULT (DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s')), `is_pinned` BOOLEAN DEFAULT false, `sort_order` INT DEFAULT 0, `proxy_url` TEXT, `use_system_proxy` BOOLEAN DEFAULT false, `custom_headers` JSON, `external_checkin_url` TEXT, `global_weight` DOUBLE DEFAULT 1, `post_refresh_probe_enabled` BOOLEAN DEFAULT false, `post_refresh_probe_model` VARCHAR(191) DEFAULT '', `post_refresh_probe_scope` VARCHAR(191) DEFAULT 'single', `post_refresh_probe_latency_threshold_ms` INT DEFAULT 0);
|
||||
CREATE TABLE IF NOT EXISTS `accounts` (`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY, `site_id` INT NOT NULL, `username` TEXT, `access_token` TEXT NOT NULL, `api_token` TEXT, `balance` DOUBLE DEFAULT 0, `balance_used` DOUBLE DEFAULT 0, `quota` DOUBLE DEFAULT 0, `unit_cost` DOUBLE, `value_score` DOUBLE DEFAULT 0, `status` VARCHAR(191) DEFAULT 'active', `checkin_enabled` BOOLEAN DEFAULT true, `last_checkin_at` VARCHAR(191), `last_balance_refresh` VARCHAR(191), `extra_config` JSON, `created_at` VARCHAR(191) DEFAULT (DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s')), `updated_at` VARCHAR(191) DEFAULT (DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s')), `is_pinned` BOOLEAN DEFAULT false, `sort_order` INT DEFAULT 0, `oauth_provider` TEXT, `oauth_account_key` TEXT, `oauth_project_id` TEXT, FOREIGN KEY (`site_id`) REFERENCES `sites`(`id`) ON DELETE CASCADE);
|
||||
CREATE TABLE IF NOT EXISTS `account_tokens` (`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY, `account_id` INT NOT NULL, `name` TEXT NOT NULL, `token` TEXT NOT NULL, `source` VARCHAR(191) DEFAULT 'manual', `enabled` BOOLEAN DEFAULT true, `is_default` BOOLEAN DEFAULT false, `created_at` VARCHAR(191) DEFAULT (DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s')), `updated_at` VARCHAR(191) DEFAULT (DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s')), `token_group` TEXT, `value_status` VARCHAR(191) NOT NULL DEFAULT 'ready', FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`) ON DELETE CASCADE);
|
||||
CREATE TABLE IF NOT EXISTS `admin_snapshots` (`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY, `namespace` TEXT NOT NULL, `snapshot_key` TEXT NOT NULL, `payload` TEXT NOT NULL, `generated_at` VARCHAR(191) NOT NULL, `expires_at` VARCHAR(191) NOT NULL, `stale_until` VARCHAR(191) NOT NULL, `created_at` VARCHAR(191) DEFAULT (DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s')), `updated_at` VARCHAR(191) DEFAULT (DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s')));
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
ALTER TABLE `sites` ADD COLUMN `post_refresh_probe_enabled` BOOLEAN DEFAULT false;
|
||||
ALTER TABLE `sites` ADD COLUMN `post_refresh_probe_model` VARCHAR(191) DEFAULT '';
|
||||
ALTER TABLE `sites` ADD COLUMN `post_refresh_probe_scope` VARCHAR(191) DEFAULT 'single';
|
||||
ALTER TABLE `sites` ADD COLUMN `post_refresh_probe_latency_threshold_ms` INT DEFAULT 0;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
CREATE TABLE IF NOT EXISTS "sites" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "name" TEXT NOT NULL, "url" TEXT NOT NULL, "platform" TEXT NOT NULL, "status" TEXT NOT NULL DEFAULT 'active', "api_key" TEXT, "created_at" TEXT DEFAULT to_char(timezone('UTC', CURRENT_TIMESTAMP), 'YYYY-MM-DD HH24:MI:SS'), "updated_at" TEXT DEFAULT to_char(timezone('UTC', CURRENT_TIMESTAMP), 'YYYY-MM-DD HH24:MI:SS'), "is_pinned" BOOLEAN DEFAULT false, "sort_order" INTEGER DEFAULT 0, "proxy_url" TEXT, "use_system_proxy" BOOLEAN DEFAULT false, "custom_headers" JSONB, "external_checkin_url" TEXT, "global_weight" DOUBLE PRECISION DEFAULT 1, "post_refresh_probe_enabled" BOOLEAN DEFAULT false, "post_refresh_probe_model" TEXT DEFAULT '', "post_refresh_probe_scope" TEXT DEFAULT 'single');
|
||||
CREATE TABLE IF NOT EXISTS "sites" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "name" TEXT NOT NULL, "url" TEXT NOT NULL, "platform" TEXT NOT NULL, "status" TEXT NOT NULL DEFAULT 'active', "api_key" TEXT, "created_at" TEXT DEFAULT to_char(timezone('UTC', CURRENT_TIMESTAMP), 'YYYY-MM-DD HH24:MI:SS'), "updated_at" TEXT DEFAULT to_char(timezone('UTC', CURRENT_TIMESTAMP), 'YYYY-MM-DD HH24:MI:SS'), "is_pinned" BOOLEAN DEFAULT false, "sort_order" INTEGER DEFAULT 0, "proxy_url" TEXT, "use_system_proxy" BOOLEAN DEFAULT false, "custom_headers" JSONB, "external_checkin_url" TEXT, "global_weight" DOUBLE PRECISION DEFAULT 1, "post_refresh_probe_enabled" BOOLEAN DEFAULT false, "post_refresh_probe_model" TEXT DEFAULT '', "post_refresh_probe_scope" TEXT DEFAULT 'single', "post_refresh_probe_latency_threshold_ms" INTEGER DEFAULT 0);
|
||||
CREATE TABLE IF NOT EXISTS "accounts" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "site_id" INTEGER NOT NULL, "username" TEXT, "access_token" TEXT NOT NULL, "api_token" TEXT, "balance" DOUBLE PRECISION DEFAULT 0, "balance_used" DOUBLE PRECISION DEFAULT 0, "quota" DOUBLE PRECISION DEFAULT 0, "unit_cost" DOUBLE PRECISION, "value_score" DOUBLE PRECISION DEFAULT 0, "status" TEXT DEFAULT 'active', "checkin_enabled" BOOLEAN DEFAULT true, "last_checkin_at" TEXT, "last_balance_refresh" TEXT, "extra_config" JSONB, "created_at" TEXT DEFAULT to_char(timezone('UTC', CURRENT_TIMESTAMP), 'YYYY-MM-DD HH24:MI:SS'), "updated_at" TEXT DEFAULT to_char(timezone('UTC', CURRENT_TIMESTAMP), 'YYYY-MM-DD HH24:MI:SS'), "is_pinned" BOOLEAN DEFAULT false, "sort_order" INTEGER DEFAULT 0, "oauth_provider" TEXT, "oauth_account_key" TEXT, "oauth_project_id" TEXT, FOREIGN KEY ("site_id") REFERENCES "sites"("id") ON DELETE CASCADE);
|
||||
CREATE TABLE IF NOT EXISTS "account_tokens" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "account_id" INTEGER NOT NULL, "name" TEXT NOT NULL, "token" TEXT NOT NULL, "source" TEXT DEFAULT 'manual', "enabled" BOOLEAN DEFAULT true, "is_default" BOOLEAN DEFAULT false, "created_at" TEXT DEFAULT to_char(timezone('UTC', CURRENT_TIMESTAMP), 'YYYY-MM-DD HH24:MI:SS'), "updated_at" TEXT DEFAULT to_char(timezone('UTC', CURRENT_TIMESTAMP), 'YYYY-MM-DD HH24:MI:SS'), "token_group" TEXT, "value_status" TEXT NOT NULL DEFAULT 'ready', FOREIGN KEY ("account_id") REFERENCES "accounts"("id") ON DELETE CASCADE);
|
||||
CREATE TABLE IF NOT EXISTS "admin_snapshots" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "namespace" TEXT NOT NULL, "snapshot_key" TEXT NOT NULL, "payload" TEXT NOT NULL, "generated_at" TEXT NOT NULL, "expires_at" TEXT NOT NULL, "stale_until" TEXT NOT NULL, "created_at" TEXT DEFAULT to_char(timezone('UTC', CURRENT_TIMESTAMP), 'YYYY-MM-DD HH24:MI:SS'), "updated_at" TEXT DEFAULT to_char(timezone('UTC', CURRENT_TIMESTAMP), 'YYYY-MM-DD HH24:MI:SS'));
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
ALTER TABLE "sites" ADD COLUMN "post_refresh_probe_enabled" BOOLEAN DEFAULT false;
|
||||
ALTER TABLE "sites" ADD COLUMN "post_refresh_probe_model" TEXT DEFAULT '';
|
||||
ALTER TABLE "sites" ADD COLUMN "post_refresh_probe_scope" TEXT DEFAULT 'single';
|
||||
ALTER TABLE "sites" ADD COLUMN "post_refresh_probe_latency_threshold_ms" INTEGER DEFAULT 0;
|
||||
|
||||
@@ -2107,6 +2107,12 @@
|
||||
"notNull": false,
|
||||
"defaultValue": "'single'",
|
||||
"primaryKey": false
|
||||
},
|
||||
"post_refresh_probe_latency_threshold_ms": {
|
||||
"logicalType": "integer",
|
||||
"notNull": false,
|
||||
"defaultValue": "0",
|
||||
"primaryKey": false
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -18,6 +18,7 @@ export const sites = sqliteTable('sites', {
|
||||
postRefreshProbeEnabled: integer('post_refresh_probe_enabled', { mode: 'boolean' }).default(false),
|
||||
postRefreshProbeModel: text('post_refresh_probe_model').default(''),
|
||||
postRefreshProbeScope: text('post_refresh_probe_scope').default('single'),
|
||||
postRefreshProbeLatencyThresholdMs: integer('post_refresh_probe_latency_threshold_ms').default(0),
|
||||
createdAt: text('created_at').default(sql`(datetime('now'))`),
|
||||
updatedAt: text('updated_at').default(sql`(datetime('now'))`),
|
||||
}, (table) => ({
|
||||
|
||||
@@ -689,9 +689,13 @@ export async function sitesRoutes(app: FastifyInstance) {
|
||||
if (body.sortOrder !== undefined) updates.sortOrder = normalizedSortOrder;
|
||||
if (body.globalWeight !== undefined) updates.globalWeight = normalizedGlobalWeight;
|
||||
const anyBody = body as Record<string, unknown>;
|
||||
if (anyBody.postRefreshProbeEnabled !== undefined) updates.postRefreshProbeEnabled = Boolean(anyBody.postRefreshProbeEnabled);
|
||||
if (anyBody.postRefreshProbeEnabled !== undefined) updates.postRefreshProbeEnabled = anyBody.postRefreshProbeEnabled === true || anyBody.postRefreshProbeEnabled === 1;
|
||||
if (anyBody.postRefreshProbeModel !== undefined) updates.postRefreshProbeModel = String(anyBody.postRefreshProbeModel || '').trim();
|
||||
if (anyBody.postRefreshProbeScope !== undefined) updates.postRefreshProbeScope = anyBody.postRefreshProbeScope === 'all' ? 'all' : 'single';
|
||||
if (anyBody.postRefreshProbeLatencyThresholdMs !== undefined) {
|
||||
const ms = Number(anyBody.postRefreshProbeLatencyThresholdMs);
|
||||
updates.postRefreshProbeLatencyThresholdMs = Number.isFinite(ms) && ms >= 0 ? Math.trunc(ms) : 0;
|
||||
}
|
||||
updates.updatedAt = new Date().toISOString();
|
||||
try {
|
||||
await db.transaction(async (tx) => {
|
||||
@@ -905,7 +909,9 @@ export async function sitesRoutes(app: FastifyInstance) {
|
||||
const body = request.body as Record<string, unknown> | null;
|
||||
const scope = body?.scope === 'all' ? 'all' : body?.scope === 'single' ? 'single' : undefined;
|
||||
const modelName = typeof body?.modelName === 'string' ? body.modelName.trim() : undefined;
|
||||
const result = await probeSiteModels(id, { scope, modelName });
|
||||
const parsedThresholdBody = Number(body?.latencyThresholdMs ?? 0);
|
||||
const latencyThresholdMsBody = Number.isFinite(parsedThresholdBody) && parsedThresholdBody > 0 ? Math.trunc(parsedThresholdBody) : undefined;
|
||||
const result = await probeSiteModels(id, { scope, modelName, latencyThresholdMs: latencyThresholdMsBody });
|
||||
if (!result.success) {
|
||||
return reply.code(422).send({ error: result.error });
|
||||
}
|
||||
@@ -936,11 +942,17 @@ export async function sitesRoutes(app: FastifyInstance) {
|
||||
const parsedThreshold = parseInt(q.latencyThresholdMs ?? '', 10);
|
||||
const latencyThresholdMs = Number.isFinite(parsedThreshold) && parsedThreshold > 0 ? parsedThreshold : undefined;
|
||||
|
||||
// Propagate client disconnect to the probe worker pool
|
||||
const probeAbort = new AbortController();
|
||||
reply.raw.on('close', () => probeAbort.abort());
|
||||
|
||||
try {
|
||||
const result = await probeSiteModels(id, { scope, modelName, latencyThresholdMs }, (ev) => {
|
||||
const result = await probeSiteModels(id, { scope, modelName, latencyThresholdMs, signal: probeAbort.signal }, (ev) => {
|
||||
sseWrite(reply.raw, ev.type, ev);
|
||||
});
|
||||
sseWrite(reply.raw, 'complete', result);
|
||||
if (!probeAbort.signal.aborted) {
|
||||
sseWrite(reply.raw, 'complete', result);
|
||||
}
|
||||
} catch (err: any) {
|
||||
sseWrite(reply.raw, 'error', { message: err?.message || '探测失败' });
|
||||
}
|
||||
|
||||
@@ -761,6 +761,7 @@ function buildAllApiHubV2AccountsSection(data: RawBackupData): {
|
||||
postRefreshProbeEnabled: false,
|
||||
postRefreshProbeModel: '',
|
||||
postRefreshProbeScope: 'single',
|
||||
postRefreshProbeLatencyThresholdMs: 0,
|
||||
createdAt: input.createdAt,
|
||||
updatedAt: input.updatedAt,
|
||||
});
|
||||
@@ -1007,6 +1008,7 @@ function buildAccountsSectionFromRefBackup(data: RawBackupData): AccountsBackupS
|
||||
postRefreshProbeEnabled: false,
|
||||
postRefreshProbeModel: '',
|
||||
postRefreshProbeScope: 'single',
|
||||
postRefreshProbeLatencyThresholdMs: 0,
|
||||
createdAt: toIsoString(item.created_at),
|
||||
updatedAt: toIsoString(item.updated_at),
|
||||
});
|
||||
@@ -1552,6 +1554,10 @@ async function importAccountsSection(section: AccountsBackupSection): Promise<vo
|
||||
sortOrder: row.sortOrder ?? 0,
|
||||
globalWeight: row.globalWeight ?? 1,
|
||||
apiKey: row.apiKey,
|
||||
postRefreshProbeEnabled: row.postRefreshProbeEnabled ?? false,
|
||||
postRefreshProbeModel: row.postRefreshProbeModel ?? '',
|
||||
postRefreshProbeScope: (row.postRefreshProbeScope === 'all' ? 'all' : 'single') as 'single' | 'all',
|
||||
postRefreshProbeLatencyThresholdMs: row.postRefreshProbeLatencyThresholdMs ?? 0,
|
||||
createdAt: row.createdAt,
|
||||
updatedAt: row.updatedAt,
|
||||
}).run();
|
||||
|
||||
@@ -397,7 +397,7 @@ export type ProbeSiteModelsProgress =
|
||||
|
||||
export async function probeSiteModels(
|
||||
siteId: number,
|
||||
options?: { scope?: 'single' | 'all'; modelName?: string; concurrency?: number; latencyThresholdMs?: number },
|
||||
options?: { scope?: 'single' | 'all'; modelName?: string; concurrency?: number; latencyThresholdMs?: number; signal?: AbortSignal },
|
||||
onProgress?: (event: ProbeSiteModelsProgress) => void,
|
||||
): Promise<ProbeSiteModelsResult> {
|
||||
const empty = (scope: 'single' | 'all', error: string): ProbeSiteModelsResult =>
|
||||
@@ -440,11 +440,12 @@ export async function probeSiteModels(
|
||||
|
||||
// Probe models concurrently, limited by modelAvailabilityProbeConcurrency
|
||||
const concurrency = Math.max(1, options?.concurrency ?? 10);
|
||||
const detailsMap = new Map<string, { modelName: string; status: RuntimeModelProbeStatus; latencyMs: number | null }>();
|
||||
const detailsMap = new Map<string, { modelName: string; status: RuntimeModelProbeStatus; latencyMs: number | null; reason?: string }>();
|
||||
|
||||
let cursor = 0;
|
||||
async function worker() {
|
||||
while (cursor < modelsToProbe.length) {
|
||||
if (options?.signal?.aborted) break;
|
||||
const modelName = modelsToProbe[cursor++];
|
||||
try {
|
||||
const result = await probeRuntimeModel({
|
||||
@@ -530,6 +531,8 @@ async function runPostRefreshProbeIfEnabled(params: {
|
||||
modelsToProbe = [found];
|
||||
}
|
||||
|
||||
// runPostRefreshProbeIfEnabled: apply latency threshold from site config
|
||||
const threshold = params.site.postRefreshProbeLatencyThresholdMs ?? 0;
|
||||
// Probe each model sequentially
|
||||
const details: Array<{ modelName: string; status: RuntimeModelProbeStatus; latencyMs: number | null }> = [];
|
||||
for (const modelName of modelsToProbe) {
|
||||
@@ -540,7 +543,14 @@ async function runPostRefreshProbeIfEnabled(params: {
|
||||
modelName,
|
||||
timeoutMs: config.modelAvailabilityProbeTimeoutMs,
|
||||
});
|
||||
details.push({ modelName, status: result.status, latencyMs: result.latencyMs });
|
||||
const latencyExceeded = (
|
||||
result.status === 'supported'
|
||||
&& threshold > 0
|
||||
&& result.latencyMs != null
|
||||
&& result.latencyMs > threshold
|
||||
);
|
||||
const effectiveStatus: RuntimeModelProbeStatus = latencyExceeded ? 'unsupported' : result.status;
|
||||
details.push({ modelName, status: effectiveStatus, latencyMs: result.latencyMs });
|
||||
} catch (err) {
|
||||
console.warn(`[post-refresh-probe] probe failed for account ${params.account.id} model ${modelName}`, err);
|
||||
details.push({ modelName, status: 'inconclusive', latencyMs: null });
|
||||
|
||||
+1
-1
@@ -786,7 +786,7 @@ export const api = {
|
||||
}),
|
||||
getSiteAvailableModels: (siteId: number) =>
|
||||
request(`/api/sites/${siteId}/available-models`),
|
||||
probeSiteNow: (siteId: number, options?: { scope?: 'single' | 'all'; modelName?: string }) =>
|
||||
probeSiteNow: (siteId: number, options?: { scope?: 'single' | 'all'; modelName?: string; latencyThresholdMs?: number }) =>
|
||||
request(`/api/sites/${siteId}/probe-now`, { method: 'POST', body: JSON.stringify(options || {}) }),
|
||||
|
||||
// Accounts
|
||||
|
||||
+13
-1
@@ -492,6 +492,7 @@ export default function Sites() {
|
||||
setProbeEnabled(!!site.postRefreshProbeEnabled);
|
||||
setProbeModel(typeof site.postRefreshProbeModel === 'string' ? site.postRefreshProbeModel : '');
|
||||
setProbeScope(site.postRefreshProbeScope === 'all' ? 'all' : 'single');
|
||||
setProbeLatencyThreshold(String((site as any).postRefreshProbeLatencyThresholdMs ?? 0));
|
||||
setProbeLog([]);
|
||||
setProbeCompleted(false);
|
||||
probeAbortRef.current?.abort();
|
||||
@@ -563,9 +564,10 @@ export default function Sites() {
|
||||
postRefreshProbeEnabled: probeEnabled,
|
||||
postRefreshProbeModel: probeModel.trim(),
|
||||
postRefreshProbeScope: probeScope,
|
||||
postRefreshProbeLatencyThresholdMs: Math.max(0, parseInt(probeLatencyThreshold, 10) || 0),
|
||||
});
|
||||
setSites((prev) => prev.map((s) => s.id === editor.editingSiteId
|
||||
? { ...s, postRefreshProbeEnabled: probeEnabled, postRefreshProbeModel: probeModel.trim(), postRefreshProbeScope: probeScope }
|
||||
? { ...s, postRefreshProbeEnabled: probeEnabled, postRefreshProbeModel: probeModel.trim(), postRefreshProbeScope: probeScope, postRefreshProbeLatencyThresholdMs: Math.max(0, parseInt(probeLatencyThreshold, 10) || 0) }
|
||||
: s,
|
||||
));
|
||||
toast.success('刷新后探测设置已保存');
|
||||
@@ -588,6 +590,7 @@ export default function Sites() {
|
||||
probeAbortRef.current = controller;
|
||||
setProbing(true);
|
||||
setProbeLog([]);
|
||||
setProbeCompleted(false);
|
||||
|
||||
try {
|
||||
const token = getAuthToken(localStorage);
|
||||
@@ -666,6 +669,15 @@ export default function Sites() {
|
||||
} else if (type === 'error') {
|
||||
addLog(d.message || '探测失败', 'var(--color-error, #ef4444)');
|
||||
toast.error(d.message || '探测失败');
|
||||
// Refresh model state even on error
|
||||
Promise.all([
|
||||
api.getSiteAvailableModels(siteId).then((res: any) => {
|
||||
setAvailableModels(Array.isArray(res?.models) ? res.models : []);
|
||||
}),
|
||||
api.getSiteDisabledModels(siteId).then((res: any) => {
|
||||
setDisabledModels(Array.isArray(res?.models) ? res.models : []);
|
||||
}),
|
||||
]).catch(() => {}).finally(() => setProbeCompleted(true));
|
||||
}
|
||||
} catch { /* ignore parse errors */ }
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user