refactor(db): 统一数据库 schema — 软删除、命名规范、约束标准化

- 全表新增 deleted_at 列,统一软删除替代硬删除+archived_at
- models.model_id 重命名为 external_id,消除语义混淆
- conversations.model_id 改为可空(模型为建议而非绑定)
- messages 新增 updated_at,移除 CASCADE 改为 DAO 层级联
- 移除 DB 层 UNIQUE 约束,改为应用层检查(配合软删除)
- 新增 helpers.ts(baseColumns + 构造层防御)、ESLint 规则、契约测试
- 迁移 0004 补全 CHECK 约束(providers.type/materials.status/messages.role)
- DAO 层全面重写:级联软删除、应用层唯一、provider 删除保护
- 路由/前端/测试全量适配 externalId 重命名及类型变更
This commit is contained in:
2026-06-05 01:02:23 +08:00
parent e25b2537fd
commit db40d04dc5
37 changed files with 1564 additions and 324 deletions

View File

@@ -13,7 +13,7 @@ import {
updateConversation,
updateConversationTimestamp,
} from "../../db/conversations";
import { getModelWithProvider } from "../../db/models";
import { getModelWithProvider, listModels } from "../../db/models";
import { createApiError, jsonResponse } from "../../helpers";
import { validateIdParam } from "../../middleware";
@@ -79,13 +79,23 @@ export async function handleSendChat(req: Request, db: Database, mode: RuntimeMo
let model;
try {
const result = getModelWithProvider(db, conversation.modelId);
let effectiveModelId = conversation.modelId;
if (!effectiveModelId) {
const fallback = listModels(db, { page: 1, pageSize: 1 });
const firstModel = fallback.items[0];
if (!firstModel) {
return jsonResponse(createApiError("没有可用的模型,请先配置模型", 400), { mode, status: 400 });
}
effectiveModelId = firstModel.id;
}
const result = getModelWithProvider(db, effectiveModelId);
if ("error" in result) {
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
}
const registry = buildProviderRegistry(db);
model = registry.languageModel(`${result.provider.id}:${result.model.modelId}`);
model = registry.languageModel(`${result.provider.id}:${result.model.externalId}`);
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return jsonResponse(createApiError(`模型初始化失败:${msg}`, 500), { mode, status: 500 });

View File

@@ -25,8 +25,8 @@ export async function handleCreateModel(
return jsonResponse(createApiError("name is required", 400), { mode, status: 400 });
}
if (!body.modelId || typeof body.modelId !== "string") {
return jsonResponse(createApiError("modelId is required", 400), { mode, status: 400 });
if (!body.externalId || typeof body.externalId !== "string") {
return jsonResponse(createApiError("externalId is required", 400), { mode, status: 400 });
}
if (!body.providerId || typeof body.providerId !== "string") {

View File

@@ -25,8 +25,8 @@ export async function handleTestModelConfig(
return jsonResponse(createApiError("providerId is required", 400), { mode, status: 400 });
}
if (!body.modelId || typeof body.modelId !== "string") {
return jsonResponse(createApiError("modelId is required", 400), { mode, status: 400 });
if (!body.externalId || typeof body.externalId !== "string") {
return jsonResponse(createApiError("externalId is required", 400), { mode, status: 400 });
}
const providerResult = getProvider(db, body.providerId);
@@ -41,7 +41,7 @@ export async function handleTestModelConfig(
{
apiKey: providerResult.provider.apiKey,
baseUrl: providerResult.provider.baseUrl,
modelId: body.modelId,
modelId: body.externalId,
name: providerResult.provider.name,
type: providerResult.provider.type,
},
@@ -50,7 +50,7 @@ export async function handleTestModelConfig(
if (!testResult.ok) {
logger.warn(
{ message: testResult.message, modelId: body.modelId, providerId: body.providerId },
{ externalId: body.externalId, message: testResult.message, providerId: body.providerId },
"模型连接测试失败",
);
}