diff --git a/docs/development/README.md b/docs/development/README.md index b565506..37a169c 100644 --- a/docs/development/README.md +++ b/docs/development/README.md @@ -33,7 +33,7 @@ AI 工具必须严格遵守以下全部约束。 **前端**:优先复用已有组件/hooks/依赖库;确需新增依赖时先说明原因。 -**Zod**:已引入但未使用(预留),配置校验用 TypeBox + Ajv,不得混用 Zod。 +**Zod**:AI 工具层(`src/server/ai/tools/`)使用 Zod 定义 `tool()` 的 `inputSchema`,以满足 AI SDK 对 `ZodSchema` 的类型推断要求,属于框架级约束而非项目选型冲突。配置校验层使用 TypeBox + Ajv,两层级各司其职,不混用。 ### 目录边界 diff --git a/eslint-rules/enforce-catch-type.js b/eslint-rules/enforce-catch-type.js index ec01471..a007b52 100644 --- a/eslint-rules/enforce-catch-type.js +++ b/eslint-rules/enforce-catch-type.js @@ -25,6 +25,9 @@ export const enforceCatchType = { if (type?.type === "TSTypeReference") { return type.typeName?.name === "unknown"; } + if (type?.type === "TSUnknownKeyword") { + return true; + } return false; } @@ -45,12 +48,7 @@ export const enforceCatchType = { } } - if ( - body && - body.type === "BlockStatement" && - body.body.length === 0 && - !hasCommentsInBody(body) - ) { + if (body && body.type === "BlockStatement" && body.body.length === 0 && !hasCommentsInBody(body)) { context.report({ node: body, messageId: "emptyCatchNoComment" }); } } @@ -59,4 +57,4 @@ export const enforceCatchType = { CatchClause: check, }; }, -}; \ No newline at end of file +}; diff --git a/eslint-rules/no-empty-function.js b/eslint-rules/no-empty-function.js index 2d9072b..b6e597c 100644 --- a/eslint-rules/no-empty-function.js +++ b/eslint-rules/no-empty-function.js @@ -2,14 +2,10 @@ export const noEmptyFunction = { meta: { type: "problem", docs: { - description: - "禁止空函数体,并提供项目约定的修复指引:生产代码使用 () => undefined,测试代码使用 () => {} + eslint-disable", + description: "禁止空函数体。修复方式:在函数体内添加注释说明为何为空实现(如接口契约、测试桩、noop)。", }, messages: { - unexpectedProduction: - "生产代码中空函数应使用 () => undefined 明确表意(如 noop/voidLog)。如果确需空实现且为接口契约,请添加注释说明原因。", - unexpectedTest: - "测试代码中空函数使用 () => {} 并在文件顶部添加 /* eslint-disable @typescript-eslint/no-empty-function */。", + unexpected: "空函数体禁止使用。请在 {} 内添加注释说明原因,例如:/* 实现 Logger 接口契约,有意静默丢弃 */。", }, schema: [], }, @@ -17,17 +13,11 @@ export const noEmptyFunction = { create(context) { const sourceCode = context.sourceCode ?? context.getSourceCode(); - const allowedFunctionTypes = new Set([ - "ArrowFunctionExpression", - "FunctionDeclaration", - "FunctionExpression", - ]); + const allowedFunctionTypes = new Set(["ArrowFunctionExpression", "FunctionDeclaration", "FunctionExpression"]); function isEmptyBody(body) { return ( - body.type === "BlockStatement" && - body.body.length === 0 && - sourceCode.getCommentsInside(body).length === 0 + body.type === "BlockStatement" && body.body.length === 0 && sourceCode.getCommentsInside(body).length === 0 ); } @@ -54,12 +44,9 @@ export const noEmptyFunction = { if (isPrivateOrProtectedConstructor(node)) return; if (isOverrideMethod(node)) return; - const isTest = /[\\/]tests?[\\/]/.test(context.filename ?? "") || context.filename?.includes("test"); - context.report({ node, - messageId: isTest ? "unexpectedTest" : "unexpectedProduction", - data: { name: node.id?.name ?? node.key?.name ?? "function" }, + messageId: "unexpected", }); } diff --git a/src/server/bootstrap.ts b/src/server/bootstrap.ts index 465b5fb..1b7df44 100644 --- a/src/server/bootstrap.ts +++ b/src/server/bootstrap.ts @@ -47,7 +47,7 @@ export async function bootstrap(options: BootstrapOptions, dependencies: Bootstr try { logger = await buildLogger(config.logging, options.mode, options.version); - } catch (logInitError) { + } catch (logInitError: unknown) { createFallback().fatal( `日志初始化失败: ${logInitError instanceof Error ? logInitError.message : String(logInitError)}`, ); @@ -83,7 +83,7 @@ export async function bootstrap(options: BootstrapOptions, dependencies: Bootstr staticAssets: options.staticAssets, version: options.version, }); - } catch (error) { + } catch (error: unknown) { if (logger) { logger.fatal({ error: error instanceof Error ? error.message : String(error) }, "启动失败"); logger.flush(); diff --git a/src/server/config.ts b/src/server/config.ts index 45c5050..8fbcda4 100644 --- a/src/server/config.ts +++ b/src/server/config.ts @@ -192,7 +192,7 @@ function validateLoggingConfig(logging: LoggingConfig | undefined, issues: Confi if (bytes <= 0) { issues.push(issue("invalid-value", "server.logging.file.rotation.size", "滚动大小必须为正整数字节数")); } - } catch (error) { + } catch (error: unknown) { issues.push( issue( "invalid-value", diff --git a/src/server/db/index.ts b/src/server/db/index.ts index 403d0cd..c14b0a1 100644 --- a/src/server/db/index.ts +++ b/src/server/db/index.ts @@ -11,4 +11,13 @@ export { } from "./conversations"; export { loadMigrationsFromDir, type MigrationRecord } from "./load-migrations"; export { runMigrations } from "./migrate"; +export { + createModel, + deleteModel, + getModel, + getModelsByProviderId, + getModelWithProvider, + listModels, + updateModel, +} from "./models"; export { conversations, messages, projects, schemaMigrations } from "./schema"; diff --git a/src/server/db/models.ts b/src/server/db/models.ts index 12a3c5e..5ba8a13 100644 --- a/src/server/db/models.ts +++ b/src/server/db/models.ts @@ -90,6 +90,38 @@ export function getModelsByProviderId(raw: Database, providerId: string): number return Number(result?.count ?? 0); } +export function getModelWithProvider( + raw: Database, + modelId: string, +): + | { error: string; status: number } + | { + model: { modelId: string; name: string; providerId: string }; + provider: { apiKey: string; baseUrl: string; id: string; type: string }; + } { + const db = wrap(raw); + const row = db.select().from(models).where(eq(models.id, modelId)).get(); + + if (!row) return { error: "模型不存在", status: 404 }; + + const providerRow = db.select().from(providers).where(eq(providers.id, row.providerId)).get(); + if (!providerRow) return { error: "供应商不存在", status: 404 }; + + return { + model: { + modelId: row.modelId, + name: row.name, + providerId: row.providerId, + }, + provider: { + apiKey: providerRow.apiKey, + baseUrl: providerRow.baseUrl, + id: providerRow.id, + type: providerRow.type, + }, + }; +} + export function listModels( raw: Database, options: { keyword?: string; page: number; pageSize: number; providerId?: string }, diff --git a/src/server/routes/chat/send.ts b/src/server/routes/chat/send.ts index 588f236..de05010 100644 --- a/src/server/routes/chat/send.ts +++ b/src/server/routes/chat/send.ts @@ -1,21 +1,19 @@ import type Database from "bun:sqlite"; import { createAgentUIStreamResponse, generateText, type UIMessage } from "ai"; -import { eq } from "drizzle-orm"; import type { RuntimeMode } from "../../../shared/api"; import type { Logger } from "../../logger"; import { createAlfredAgent } from "../../ai/agents/alfred-agent"; import { buildProviderRegistry } from "../../ai/registry"; -import { wrap } from "../../db/connection"; import { createMessage, getConversation, updateConversation, updateConversationTimestamp, } from "../../db/conversations"; -import { models, providers } from "../../db/schema"; +import { getModelWithProvider } from "../../db/models"; import { createApiError, jsonResponse } from "../../helpers"; import { validateIdParam } from "../../middleware"; @@ -29,7 +27,7 @@ export async function handleSendChat(req: Request, db: Database, mode: RuntimeMo let body: { conversationId?: string; messages?: UIMessage[] }; try { body = (await req.json()) as typeof body; - } catch (e) { + } catch (e: unknown) { logger.warn({ error: e instanceof Error ? e.message : String(e) }, "请求 JSON 解析失败"); return jsonResponse(createApiError("Invalid JSON body", 400), { mode, status: 400 }); } @@ -81,19 +79,13 @@ export async function handleSendChat(req: Request, db: Database, mode: RuntimeMo let model; try { - const d = wrap(db); - const modelRow = d.select().from(models).where(eq(models.id, conversation.modelId)).get(); - if (!modelRow) { - return jsonResponse(createApiError(`模型不存在: ${conversation.modelId}`, 400), { mode, status: 400 }); - } - - const providerRow = d.select().from(providers).where(eq(providers.id, modelRow.providerId)).get(); - if (!providerRow) { - return jsonResponse(createApiError(`供应商不存在: ${modelRow.providerId}`, 400), { mode, status: 400 }); + const result = getModelWithProvider(db, conversation.modelId); + if ("error" in result) { + return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status }); } const registry = buildProviderRegistry(db); - model = registry.languageModel(`${providerRow.id}:${modelRow.modelId}`); + model = registry.languageModel(`${result.provider.id}:${result.model.modelId}`); } catch (e: unknown) { const msg = e instanceof Error ? e.message : String(e); return jsonResponse(createApiError(`模型初始化失败:${msg}`, 500), { mode, status: 500 }); diff --git a/src/server/routes/projects/update.ts b/src/server/routes/projects/update.ts index f90d759..fcacfb5 100644 --- a/src/server/routes/projects/update.ts +++ b/src/server/routes/projects/update.ts @@ -25,10 +25,11 @@ export async function handleUpdateProject( } catch (e: unknown) { logger.warn({ error: e instanceof Error ? e.message : String(e) }, "请求 JSON 解析失败"); return jsonResponse(createApiError("Invalid JSON body", 400), { mode, status: 400 }); - return jsonResponse(createApiError("Invalid JSON body", 400), { mode, status: 400 }); } - if (!body.name && !body.description && body.name !== "" && body.description !== "") { + const hasName = body.name !== undefined && body.name.trim() !== ""; + const hasDescription = body.description !== undefined && body.description.trim() !== ""; + if (!hasName && !hasDescription) { return jsonResponse(createApiError("At least one of name or description is required", 400), { mode, status: 400 }); } diff --git a/src/server/routes/providers/update.ts b/src/server/routes/providers/update.ts index 4d7c94e..5a9205c 100644 --- a/src/server/routes/providers/update.ts +++ b/src/server/routes/providers/update.ts @@ -34,6 +34,13 @@ export async function handleUpdateProvider( }); } + if (body.name === undefined && body.baseUrl === undefined && body.apiKey === undefined && body.type === undefined) { + return jsonResponse(createApiError("至少需要提供 name, baseUrl, apiKey 或 type 中的一个字段", 400), { + mode, + status: 400, + }); + } + const result = updateProvider(db, validated.id, body, logger); if ("error" in result) { return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status }); diff --git a/src/web/hooks/use-conversations.ts b/src/web/hooks/use-conversations.ts index f1ce7f5..f417f7d 100644 --- a/src/web/hooks/use-conversations.ts +++ b/src/web/hooks/use-conversations.ts @@ -29,7 +29,7 @@ export async function fetchConversation(projectId: string, conversationId: strin try { const response = await fetch(`/api/projects/${projectId}/conversations/${conversationId}`); return handleResponse(response, (data) => (data as ConversationResponse).conversation); - } catch (err) { + } catch (err: unknown) { logger.error("获取会话失败", { conversationId, error: err instanceof Error ? err.message : String(err), @@ -61,7 +61,7 @@ export async function updateConversation( method: "PATCH", }); return handleResponse(response, (data) => (data as ConversationResponse).conversation); - } catch (err) { + } catch (err: unknown) { logger.error("更新会话失败", { conversationId, error: err instanceof Error ? err.message : String(err), diff --git a/src/web/pages/models/components/ModelFormModal.tsx b/src/web/pages/models/components/ModelFormModal.tsx index 3401bf8..e5595a8 100644 --- a/src/web/pages/models/components/ModelFormModal.tsx +++ b/src/web/pages/models/components/ModelFormModal.tsx @@ -109,7 +109,7 @@ export function ModelFormModal({ message.success("模型已创建"); } onOpenChange(false); - } catch (err) { + } catch (err: unknown) { if (err instanceof Error) { message.error(err.message); } @@ -136,7 +136,7 @@ export function ModelFormModal({ } else { message.error(result.message); } - } catch (err) { + } catch (err: unknown) { message.error((err as Error).message); } finally { setTesting(false); diff --git a/src/web/pages/models/components/ModelTable.tsx b/src/web/pages/models/components/ModelTable.tsx index 9c0825b..2464561 100644 --- a/src/web/pages/models/components/ModelTable.tsx +++ b/src/web/pages/models/components/ModelTable.tsx @@ -69,7 +69,7 @@ export function ModelTable({ try { await onDelete(id); message.success("模型已删除"); - } catch (err) { + } catch (err: unknown) { message.error((err as Error).message); } }; diff --git a/src/web/pages/models/components/ProviderFormModal.tsx b/src/web/pages/models/components/ProviderFormModal.tsx index 84d8194..a955415 100644 --- a/src/web/pages/models/components/ProviderFormModal.tsx +++ b/src/web/pages/models/components/ProviderFormModal.tsx @@ -83,7 +83,7 @@ export function ProviderFormModal({ message.success("供应商已创建"); } onOpenChange(false); - } catch (err) { + } catch (err: unknown) { if (err instanceof Error) { message.error(err.message); } @@ -105,7 +105,7 @@ export function ProviderFormModal({ } else { message.error(result.message); } - } catch (err) { + } catch (err: unknown) { if (err instanceof Error) { message.error(err.message); } @@ -135,10 +135,18 @@ export function ProviderFormModal({