gongxiangp2/lib/ai-model-verification-state.ts

38 lines
3.1 KiB
TypeScript

import { createHash } from 'node:crypto'
import { prisma } from './prisma'
import type { AiModelDatabase } from './ai-model-repository'
import type { AiModelStage, AiModelVerificationSummary } from './ai-model-types'
import { aiModelCapabilityFingerprint } from './ai-model-capabilities'
import { AI_MODEL_ADAPTER_VERSION, type AiModelProtocol } from './ai-model-protocol'
export interface AiModelVerificationRow {
id: string; modelKey: string; stage: AiModelStage; contextHash: string; idempotencyKey: string; actorId: string;
status: 'running' | 'passed' | 'failed' | 'unknown'; requestId: string | null; taskId: string | null;
message: string | null; evidence: Record<string, unknown>; startedAt: Date; deadlineAt: Date; finishedAt: Date | null;
}
export const AI_MODEL_VERIFICATION_COLUMNS = `id,model_key AS "modelKey",stage,context_hash AS "contextHash",idempotency_key AS "idempotencyKey",actor_id AS "actorId",status,request_id AS "requestId",task_id AS "taskId",message,evidence,started_at AS "startedAt",deadline_at AS "deadlineAt",finished_at AS "finishedAt"`
export async function listAiModelVerifications(db: AiModelDatabase = prisma): Promise<AiModelVerificationRow[]> {
return db.$queryRawUnsafe<AiModelVerificationRow[]>(`SELECT DISTINCT ON (model_key,stage) ${AI_MODEL_VERIFICATION_COLUMNS} FROM ai_model_verifications ORDER BY model_key,stage,started_at DESC,id DESC`)
}
/** Private, never returned through DTOs. A key rotation invalidates evidence without depending on UI revisions. */
export function aiModelVerificationContext(baseUrl: string, modelId: string, metadata: unknown, apiKey: string, protocol: AiModelProtocol): string {
return createHash('sha256').update(JSON.stringify({ baseUrl, modelId, capability: aiModelCapabilityFingerprint(metadata),
credential: createHash('sha256').update(apiKey).digest('hex'), adapter: AI_MODEL_ADAPTER_VERSION, protocol })).digest('hex')
}
export function verificationEffectiveStatus(row: AiModelVerificationRow): AiModelVerificationRow['status'] {
return row.status === 'running' && new Date(row.deadlineAt).getTime() <= Date.now() ? 'unknown' : row.status
}
export function aiModelVerificationSummary(rows: AiModelVerificationRow[], modelKey: string, stage: AiModelStage, contextHash: string | null): AiModelVerificationSummary {
const row = rows.find(value => value.modelKey === modelKey && value.stage === stage)
if (!row) return { status: 'untested', id: null, checkedAt: null, requestId: null, message: null }
const effective = verificationEffectiveStatus(row)
const status = effective === 'unknown' ? 'unknown' : row.contextHash !== contextHash ? 'stale' : effective
return { status, id: row.id, checkedAt: row.finishedAt?.toISOString() || row.startedAt.toISOString(), requestId: row.requestId,
canResume: Boolean(row.taskId && row.contextHash === contextHash && ['unknown', 'running'].includes(effective)),
message: status === 'stale' ? '连接、路由或模型能力已改变,请重新测试' : status === 'unknown' ? '请求结果尚未确认,已暂停重复测试;请核查上游请求记录' : row.message }
}