56 lines
5.2 KiB
TypeScript
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' | 'verification'> { 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, 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 } }
|