-
Notifications
You must be signed in to change notification settings - Fork 352
feat(sites): 站点级模型探测 —— 实时日志、延迟阈值与自动禁用 #510
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
46f23dd
3792475
cb50784
078ac4a
9939234
e5a8a0b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| ALTER TABLE `sites` ADD `post_refresh_probe_enabled` integer DEFAULT false; | ||
| --> statement-breakpoint | ||
| ALTER TABLE `sites` ADD `post_refresh_probe_model` text DEFAULT ''; | ||
| --> statement-breakpoint | ||
| ALTER TABLE `sites` ADD `post_refresh_probe_scope` text DEFAULT 'single'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| ALTER TABLE `sites` ADD `post_refresh_probe_latency_threshold_ms` integer DEFAULT 0; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,11 @@ import { | |
| import { getSiteInitializationPreset } from '../../../shared/siteInitializationPresets.js'; | ||
| import { normalizeSiteApiEndpointBaseUrl } from '../../services/siteApiEndpointService.js'; | ||
| import { analyzePrimarySiteUrl } from '../../../shared/sitePrimaryUrl.js'; | ||
| import { probeSiteModels } from '../../services/modelService.js'; | ||
|
|
||
| function sseWrite(raw: import('http').ServerResponse, event: string, data: unknown) { | ||
| try { raw.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`); } catch { /* ignore */ } | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| function normalizeSiteStatus(input: unknown): 'active' | 'disabled' | null { | ||
| if (input === undefined || input === null) return null; | ||
|
|
@@ -683,6 +688,14 @@ export async function sitesRoutes(app: FastifyInstance) { | |
| if (body.isPinned !== undefined) updates.isPinned = normalizedPinned; | ||
| 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 = 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'; | ||
|
Comment on lines
+691
to
+694
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validate probe settings instead of silently coercing them.
Suggested fix const anyBody = body as Record<string, unknown>;
- if (anyBody.postRefreshProbeEnabled !== undefined) updates.postRefreshProbeEnabled = Boolean(anyBody.postRefreshProbeEnabled);
+ if (anyBody.postRefreshProbeEnabled !== undefined) {
+ const normalizedProbeEnabled = normalizePinnedFlag(anyBody.postRefreshProbeEnabled);
+ if (normalizedProbeEnabled === null) {
+ return reply.code(400).send({ error: 'Invalid postRefreshProbeEnabled value. Expected boolean.' });
+ }
+ updates.postRefreshProbeEnabled = normalizedProbeEnabled;
+ }
if (anyBody.postRefreshProbeModel !== undefined) updates.postRefreshProbeModel = String(anyBody.postRefreshProbeModel || '').trim();
- if (anyBody.postRefreshProbeScope !== undefined) updates.postRefreshProbeScope = anyBody.postRefreshProbeScope === 'all' ? 'all' : 'single';
+ if (anyBody.postRefreshProbeScope !== undefined) {
+ if (anyBody.postRefreshProbeScope !== 'single' && anyBody.postRefreshProbeScope !== 'all') {
+ return reply.code(400).send({ error: 'Invalid postRefreshProbeScope value. Expected single or all.' });
+ }
+ updates.postRefreshProbeScope = anyBody.postRefreshProbeScope;
+ }🤖 Prompt for AI Agents |
||
| 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) => { | ||
|
|
@@ -887,6 +900,66 @@ export async function sitesRoutes(app: FastifyInstance) { | |
| return { siteId: id, models }; | ||
| }); | ||
|
|
||
| // Manually probe site models now (one-shot JSON) | ||
| app.post<{ Params: { id: string }; Body: unknown }>('/api/sites/:id/probe-now', async (request, reply) => { | ||
| const id = parseInt(request.params.id); | ||
| if (Number.isNaN(id)) { | ||
| return reply.code(400).send({ error: 'Invalid site id' }); | ||
| } | ||
| 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 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 }); | ||
| } | ||
| return result; | ||
| }); | ||
|
|
||
| // Streaming probe via SSE | ||
| app.get<{ Params: { id: string }; Querystring: { scope?: string; modelName?: string; latencyThresholdMs?: string } }>( | ||
| '/api/sites/:id/probe-stream', | ||
| async (request, reply) => { | ||
| reply.hijack(); | ||
| reply.raw.writeHead(200, { | ||
| 'Content-Type': 'text/event-stream', | ||
| 'Cache-Control': 'no-cache', | ||
| 'Connection': 'keep-alive', | ||
| }); | ||
|
|
||
| const id = parseInt(request.params.id); | ||
| if (Number.isNaN(id)) { | ||
| sseWrite(reply.raw, 'error', { message: 'Invalid site id' }); | ||
| reply.raw.end(); | ||
| return; | ||
| } | ||
|
|
||
| const q = request.query; | ||
| const scope = q.scope === 'all' ? 'all' : q.scope === 'single' ? 'single' : undefined; | ||
| const modelName = q.modelName?.trim() || undefined; | ||
| 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, signal: probeAbort.signal }, (ev) => { | ||
| sseWrite(reply.raw, ev.type, ev); | ||
| }); | ||
| if (!probeAbort.signal.aborted) { | ||
| sseWrite(reply.raw, 'complete', result); | ||
| } | ||
| } catch (err: any) { | ||
| sseWrite(reply.raw, 'error', { message: err?.message || '探测失败' }); | ||
| } | ||
| reply.raw.end(); | ||
| }, | ||
| ); | ||
|
|
||
| // Detect platform for a URL | ||
| app.post<{ Body: unknown }>('/api/sites/detect', async (request, reply) => { | ||
| const parsedBody = parseSiteDetectPayload(request.body); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: cita-777/metapi
Length of output: 1015
🏁 Script executed:
Repository: cita-777/metapi
Length of output: 297
🏁 Script executed:
Repository: cita-777/metapi
Length of output: 1700
🏁 Script executed:
Repository: cita-777/metapi
Length of output: 489
🏁 Script executed:
Repository: cita-777/metapi
Length of output: 1354
Regenerate Drizzle migration artifacts and SQL patches for the three new schema columns.
The schema definition in
src/server/db/schema.tswas updated with the three new columns, but the corresponding migration snapshot and generated SQL patches were not regenerated. The new columns are absent from:drizzle/meta/0024_snapshot.json(latest snapshot)drizzle/meta/_journal.jsonsrc/server/db/generated/mysql.bootstrap.sql,mysql.upgrade.sql,postgres.bootstrap.sql,postgres.upgrade.sqlDatabase upgrades will fail without these artifacts. Regenerate them using Drizzle's migration tooling to ensure all three outputs stay synchronized (Drizzle schema + SQLite migration history + checked-in SQL patches).
🤖 Prompt for AI Agents