gongxiangp2/lib/ai-model-repository.ts

56 lines
5.2 KiB
TypeScript

import type { Prisma } from '@prisma/client'
import { prisma } from './prisma'
import { AiModelError } from './ai-model-access'
import type { AiModelEncryptedKey } from './ai-model-crypto'
import type { AiModelCatalogEntry, AiModelDefaults } from './ai-model-types'
export type AiModelDatabase = Prisma.TransactionClient | typeof prisma
export interface AiModelProviderRow {
id: string; name: string; baseUrl: string; enabled: boolean; credentialSource: 'encrypted' | 'environment';
secretCiphertext: string | null; secretNonce: string | null; secretAuthTag: string | null;
legacyBinding: boolean; revision: number; syncedAt: Date | null
}
export interface AiModelRow extends Omit<AiModelCatalogEntry, 'capabilities'> { providerId: string; capabilityMetadata?: unknown }
export interface AiModelConfigurationRow { revision: number; defaults: AiModelDefaults }
const providerColumns = `id, name, base_url AS "baseUrl", enabled, credential_source AS "credentialSource", secret_ciphertext AS "secretCiphertext", secret_nonce AS "secretNonce", secret_auth_tag AS "secretAuthTag", legacy_binding AS "legacyBinding", revision, synced_at AS "syncedAt"`
export async function listAiModelProviderRows(db: AiModelDatabase = prisma): Promise<AiModelProviderRow[]> {
return db.$queryRawUnsafe<AiModelProviderRow[]>(`SELECT ${providerColumns} FROM ai_model_providers ORDER BY created_at, id`)
}
export async function getAiModelProviderRow(id: string, db: AiModelDatabase = prisma): Promise<AiModelProviderRow> {
const rows = await db.$queryRawUnsafe<AiModelProviderRow[]>(`SELECT ${providerColumns} FROM ai_model_providers WHERE id = $1`, id)
if (!rows[0]) throw new AiModelError('模型服务不存在', 404, 'provider_not_found')
return rows[0]
}
export async function listAiModelRows(db: AiModelDatabase = prisma): Promise<AiModelRow[]> {
return db.$queryRawUnsafe<AiModelRow[]>(`SELECT id AS key, provider_id AS "providerId", model_id AS id, enabled, stages, present, compatibility, capability_metadata AS "capabilityMetadata" FROM ai_model_catalog ORDER BY model_id, id`)
}
export async function getAiModelConfiguration(db: AiModelDatabase = prisma): Promise<AiModelConfigurationRow> {
const rows = await db.$queryRawUnsafe<Array<{ revision: number; analysis: string | null; copy: string | null; image: string | null }>>(`SELECT revision, default_analysis AS analysis, default_copy AS copy, default_image AS image FROM ai_model_configuration WHERE id = 'detail-generator'`)
const row = rows[0]
return row ? { revision: row.revision, defaults: { analysis: row.analysis, copy: row.copy, image: row.image } } : { revision: 0, defaults: { analysis: null, copy: null, image: null } }
}
export async function aiModelTransaction<T>(action: (db: Prisma.TransactionClient) => Promise<T>): Promise<T> {
return prisma.$transaction(async db => {
await db.$queryRawUnsafe(`SELECT 1 FROM (SELECT pg_advisory_xact_lock(hashtext('ai-model-registry'))) AS registry_lock`)
return action(db)
}, { timeout: 20000 })
}
export function assertAiModelRevision(actual: number, expected: number): void {
if (actual !== expected) throw new AiModelError('配置已被修改,请刷新后再保存', 409, 'revision_conflict')
}
export async function insertAiModelProvider(db: AiModelDatabase, value: AiModelProviderRow): Promise<void> {
await db.$executeRawUnsafe(`INSERT INTO ai_model_providers (id,name,base_url,enabled,credential_source,secret_ciphertext,secret_nonce,secret_auth_tag,legacy_binding,revision) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)`, value.id, value.name, value.baseUrl, value.enabled, value.credentialSource, value.secretCiphertext, value.secretNonce, value.secretAuthTag, value.legacyBinding, value.revision)
}
export async function updateAiModelProvider(db: AiModelDatabase, value: AiModelProviderRow): Promise<void> {
await db.$executeRawUnsafe(`UPDATE ai_model_providers SET name=$2,base_url=$3,enabled=$4,credential_source=$5,secret_ciphertext=$6,secret_nonce=$7,secret_auth_tag=$8,revision=revision+1,updated_at=NOW() WHERE id=$1`, value.id, value.name, value.baseUrl, value.enabled, value.credentialSource, value.secretCiphertext, value.secretNonce, value.secretAuthTag)
}
export async function saveAiModelDefaults(db: AiModelDatabase, defaults: AiModelDefaults): Promise<void> {
await db.$executeRawUnsafe(`INSERT INTO ai_model_configuration (id,default_analysis,default_copy,default_image,revision) VALUES ('detail-generator',$1,$2,$3,1) ON CONFLICT (id) DO UPDATE SET default_analysis=$1,default_copy=$2,default_image=$3,revision=ai_model_configuration.revision+1,updated_at=NOW()`, defaults.analysis, defaults.copy, defaults.image)
}
export async function aiModelAudit(db: AiModelDatabase, userId: string, action: string, providerId: string | null, details: Record<string, unknown>): Promise<void> {
// Callers supply metadata only. Keys and provider response bodies never enter the audit record.
await db.$executeRawUnsafe(`INSERT INTO ai_model_audit (id,actor_id,action,provider_id,details) VALUES (gen_random_uuid()::text,$1,$2,$3,$4::jsonb)`, userId, action, providerId, JSON.stringify(details))
}
export function encryptedAiModelFields(value: AiModelEncryptedKey | null) { return value || { secretCiphertext: null, secretNonce: null, secretAuthTag: null } }