feat: 全栈 Logger 依赖注入 — DB/Route/AI 层传参 + 前端 Logger + 测试更新 + 归档 add-frontend-logger

This commit is contained in:
2026-06-01 20:32:19 +08:00
parent 4c72754739
commit 844562303c
60 changed files with 1648 additions and 778 deletions

View File

@@ -1,6 +1,8 @@
import { type LanguageModel, stepCountIs, ToolLoopAgent } from "ai";
import { getCurrentTime } from "../tools/get-current-time";
import type { Logger } from "../../logger";
import { createGetCurrentTime } from "../tools/get-current-time";
const SYSTEM_PROMPT = `你是 Alfred一个 AI 助手。
@@ -10,11 +12,11 @@ const SYSTEM_PROMPT = `你是 Alfred一个 AI 助手。
- 给出结论时简洁直接,不要长篇铺垫
- 不确定的事明确说"不确定"`;
export function createAlfredAgent(model: LanguageModel) {
export function createAlfredAgent(model: LanguageModel, logger?: Logger) {
return new ToolLoopAgent({
instructions: SYSTEM_PROMPT,
model,
stopWhen: stepCountIs(20),
tools: { getCurrentTime },
tools: { getCurrentTime: createGetCurrentTime(logger) },
});
}

View File

@@ -5,6 +5,7 @@ import { createOpenAI } from "@ai-sdk/openai";
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
import { createProviderRegistry, generateText } from "ai";
import type { Logger } from "../logger";
import type { AIProviderConfig } from "./types";
export function buildProviderRegistry(db: Database) {
@@ -25,6 +26,7 @@ export function buildProviderRegistry(db: Database) {
export async function testModelConnection(
config: AIProviderConfig & { modelId: string },
logger: Logger,
): Promise<{ message: string; ok: boolean }> {
try {
const provider = createProvider(config);
@@ -36,12 +38,16 @@ export async function testModelConnection(
return { message: "模型连接成功", ok: true };
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
logger.warn({ error: msg, modelId: config.modelId, providerType: config.type }, "模型连接测试失败");
return { message: `模型连接失败:${msg}`, ok: false };
}
}
export async function testProviderConnection(config: AIProviderConfig): Promise<{ message: string; ok: boolean }> {
const baseUrlResult = await probeBaseUrl(config.baseUrl);
export async function testProviderConnection(
config: AIProviderConfig,
logger: Logger,
): Promise<{ message: string; ok: boolean }> {
const baseUrlResult = await probeBaseUrl(config.baseUrl, logger);
if (!baseUrlResult.ok) return baseUrlResult;
const modelsUrl = buildModelsUrl(config.baseUrl);
@@ -82,6 +88,7 @@ export async function testProviderConnection(config: AIProviderConfig): Promise<
};
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
logger.warn({ error: msg, providerType: config.type }, "供应商 /models 请求异常");
return { message: `Base URL 可连接,但 /models 请求异常:${msg};可检查 URL 或忽略此提示。`, ok: true };
}
}
@@ -154,7 +161,7 @@ function getProviders(db: Database): Array<{
}>;
}
async function probeBaseUrl(baseUrl: string): Promise<{ message: string; ok: boolean }> {
async function probeBaseUrl(baseUrl: string, logger: Logger): Promise<{ message: string; ok: boolean }> {
try {
await fetch(baseUrl, {
method: "HEAD",
@@ -163,6 +170,7 @@ async function probeBaseUrl(baseUrl: string): Promise<{ message: string; ok: boo
return { message: "Base URL 可连接", ok: true };
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
logger.warn({ baseUrl, error: msg }, "Base URL 不可达");
return { message: `Base URL 不可达:${msg}`, ok: false };
}
}

View File

@@ -1,7 +1,19 @@
import { tool } from "ai";
import { z } from "zod";
export function formatCurrentTime(timezone?: string) {
import type { Logger } from "../../logger";
export function createGetCurrentTime(logger?: Logger) {
return tool({
description: "获取当前日期和时间。可选指定时区,默认返回本地时间。",
execute: ({ timezone }) => Promise.resolve(formatCurrentTime(timezone, logger)),
inputSchema: z.object({
timezone: z.string().optional().describe("IANA 时区名称,如 'Asia/Shanghai'、'America/New_York'"),
}),
});
}
export function formatCurrentTime(timezone?: string, logger?: Logger) {
const now = new Date();
const iso = now.toISOString();
const timestamp = now.getTime();
@@ -14,7 +26,9 @@ export function formatCurrentTime(timezone?: string) {
timeStyle: "long",
timeZone: timezone,
}).format(now);
} catch {
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
logger?.warn({ error: msg, timezone }, "无效时区,使用默认格式");
local = now.toString();
}
} else {
@@ -26,11 +40,3 @@ export function formatCurrentTime(timezone?: string) {
return { iso, local, timestamp };
}
export const getCurrentTime = tool({
description: "获取当前日期和时间。可选指定时区,默认返回本地时间。",
execute: ({ timezone }) => Promise.resolve(formatCurrentTime(timezone)),
inputSchema: z.object({
timezone: z.string().optional().describe("IANA 时区名称,如 'Asia/Shanghai'、'America/New_York'"),
}),
});

View File

@@ -3,6 +3,7 @@ import type Database from "bun:sqlite";
import { desc, eq } from "drizzle-orm";
import type { Conversation, Message, UpdateConversationRequest } from "../../shared/api";
import type { Logger } from "../logger";
import { paginateQuery, wrap } from "./connection";
import { conversations, messages, models } from "./schema";
@@ -10,6 +11,7 @@ import { conversations, messages, models } from "./schema";
export function createConversation(
raw: Database,
projectId: string,
logger: Logger,
defaultModelId?: string,
): { conversation: Conversation } | { error: string; status: number } {
const db = wrap(raw);
@@ -50,6 +52,7 @@ export function createMessage(
parts?: string;
role: "assistant" | "system" | "user";
},
_logger: Logger,
): Message {
const db = wrap(raw);
const id = crypto.randomUUID();
@@ -78,6 +81,7 @@ export function createMessages(
parts?: string;
role: "assistant" | "system" | "user";
}>,
_logger: Logger,
): Message[] {
const db = wrap(raw);
const now = new Date().toISOString();
@@ -102,7 +106,11 @@ export function createMessages(
return results;
}
export function deleteConversation(raw: Database, id: string): { error: string; status: number } | { success: true } {
export function deleteConversation(
raw: Database,
id: string,
_logger: Logger,
): { error: string; status: number } | { success: true } {
const db = wrap(raw);
const existing = db.select().from(conversations).where(eq(conversations.id, id)).get();
if (!existing) return { error: "会话不存在", status: 404 };
@@ -154,6 +162,7 @@ export function updateConversation(
raw: Database,
id: string,
data: UpdateConversationRequest,
_logger: Logger,
): { conversation: Conversation } | { error: string; status: number } {
const db = wrap(raw);
const existing = db.select().from(conversations).where(eq(conversations.id, id)).get();

View File

@@ -35,9 +35,15 @@ export function runMigrations(db: Database, migrations: MigrationRecord[], dataD
db.transaction(() => {
for (const migration of pending) {
logger.info({ id: migration.id }, "执行 migration");
db.exec(migration.sql);
insertApplied.run(migration.id, migration.checksum, new Date().toISOString());
try {
logger.info({ id: migration.id }, "执行 migration");
db.exec(migration.sql);
insertApplied.run(migration.id, migration.checksum, new Date().toISOString());
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
logger.error({ error: msg, id: migration.id }, "migration 执行失败");
throw e;
}
}
})();

View File

@@ -3,6 +3,7 @@ import type Database from "bun:sqlite";
import { desc, eq, like, or, sql } from "drizzle-orm";
import type { CreateModelRequest, Model, ModelCapability, UpdateModelRequest } from "../../shared/api";
import type { Logger } from "../logger";
import { paginateQuery, wrap } from "./connection";
import { models, providers } from "./schema";
@@ -10,6 +11,7 @@ import { models, providers } from "./schema";
export function createModel(
raw: Database,
request: CreateModelRequest,
logger: Logger,
): { error: string; status: number } | { model: Model } {
const db = wrap(raw);
@@ -49,6 +51,7 @@ export function createModel(
if (msg.includes("UNIQUE constraint")) {
return { error: "该供应商下模型 ID 已存在", status: 409 };
}
logger.error({ error: msg, operation: "create", table: "models" }, "数据库操作失败");
throw e;
}
@@ -56,7 +59,11 @@ export function createModel(
return { model: toModel(row!) };
}
export function deleteModel(raw: Database, id: string): { error: string; status: number } | { success: true } {
export function deleteModel(
raw: Database,
id: string,
_logger: Logger,
): { error: string; status: number } | { success: true } {
const db = wrap(raw);
const existing = db.select().from(models).where(eq(models.id, id)).get();
if (!existing) return { error: "模型不存在", status: 404 };
@@ -111,6 +118,7 @@ export function updateModel(
raw: Database,
id: string,
request: UpdateModelRequest,
logger: Logger,
): { error: string; status: number } | { model: Model } {
const db = wrap(raw);
const existing = db.select().from(models).where(eq(models.id, id)).get();
@@ -164,6 +172,7 @@ export function updateModel(
if (msg.includes("UNIQUE constraint")) {
return { error: "该供应商下模型 ID 已存在", status: 409 };
}
logger.error({ error: msg, operation: "update", table: "models" }, "数据库操作失败");
throw e;
}

View File

@@ -3,11 +3,16 @@ import type Database from "bun:sqlite";
import { desc, eq, like, or } from "drizzle-orm";
import type { CreateProjectRequest, Project, ProjectStatus, UpdateProjectRequest } from "../../shared/api";
import type { Logger } from "../logger";
import { paginateQuery, wrap } from "./connection";
import { projects } from "./schema";
export function archiveProject(raw: Database, id: string): { error: string; status: number } | { project: Project } {
export function archiveProject(
raw: Database,
id: string,
_logger: Logger,
): { error: string; status: number } | { project: Project } {
const db = wrap(raw);
const existing = db.select().from(projects).where(eq(projects.id, id)).get();
if (!existing) return { error: "项目不存在", status: 404 };
@@ -23,6 +28,7 @@ export function archiveProject(raw: Database, id: string): { error: string; stat
export function createProject(
raw: Database,
request: CreateProjectRequest,
logger: Logger,
): { error: string; status: number } | { project: Project } {
const db = wrap(raw);
const name = request.name.trim();
@@ -50,6 +56,7 @@ export function createProject(
if (msg.includes("UNIQUE constraint")) {
return { error: "项目名称已存在", status: 409 };
}
logger.error({ error: msg, operation: "create", table: "projects" }, "数据库操作失败");
throw e;
}
@@ -57,7 +64,11 @@ export function createProject(
return { project: toProject(row!) };
}
export function deleteProject(raw: Database, id: string): { error: string; status: number } | { success: true } {
export function deleteProject(
raw: Database,
id: string,
_logger: Logger,
): { error: string; status: number } | { success: true } {
const db = wrap(raw);
const existing = db.select().from(projects).where(eq(projects.id, id)).get();
if (!existing) return { error: "项目不存在", status: 404 };
@@ -99,7 +110,11 @@ export function listProjects(
});
}
export function restoreProject(raw: Database, id: string): { error: string; status: number } | { project: Project } {
export function restoreProject(
raw: Database,
id: string,
_logger: Logger,
): { error: string; status: number } | { project: Project } {
const db = wrap(raw);
const existing = db.select().from(projects).where(eq(projects.id, id)).get();
if (!existing) return { error: "项目不存在", status: 404 };
@@ -116,6 +131,7 @@ export function updateProject(
raw: Database,
id: string,
request: UpdateProjectRequest,
logger: Logger,
): { error: string; status: number } | { project: Project } {
const db = wrap(raw);
const existing = db.select().from(projects).where(eq(projects.id, id)).get();
@@ -150,6 +166,7 @@ export function updateProject(
if (msg.includes("UNIQUE constraint")) {
return { error: "项目名称已存在", status: 409 };
}
logger.error({ error: msg, operation: "update", table: "projects" }, "数据库操作失败");
throw e;
}

View File

@@ -3,6 +3,7 @@ import type Database from "bun:sqlite";
import { desc, eq, like } from "drizzle-orm";
import type { CreateProviderRequest, Provider, ProviderOption, UpdateProviderRequest } from "../../shared/api";
import type { Logger } from "../logger";
import { paginateQuery, wrap } from "./connection";
import { providers } from "./schema";
@@ -10,6 +11,7 @@ import { providers } from "./schema";
export function createProvider(
raw: Database,
request: CreateProviderRequest,
logger: Logger,
): { error: string; status: number } | { provider: Provider } {
const db = wrap(raw);
const name = request.name.trim();
@@ -41,6 +43,7 @@ export function createProvider(
if (msg.includes("UNIQUE constraint")) {
return { error: "供应商名称已存在", status: 409 };
}
logger.error({ error: msg, operation: "create", table: "providers" }, "数据库操作失败");
throw e;
}
@@ -48,7 +51,11 @@ export function createProvider(
return { provider: toProvider(row!) };
}
export function deleteProvider(raw: Database, id: string): { error: string; status: number } | { success: true } {
export function deleteProvider(
raw: Database,
id: string,
_logger: Logger,
): { error: string; status: number } | { success: true } {
const db = wrap(raw);
const existing = db.select().from(providers).where(eq(providers.id, id)).get();
if (!existing) return { error: "供应商不存在", status: 404 };
@@ -100,6 +107,7 @@ export function updateProvider(
raw: Database,
id: string,
request: UpdateProviderRequest,
logger: Logger,
): { error: string; status: number } | { provider: Provider } {
const db = wrap(raw);
const existing = db.select().from(providers).where(eq(providers.id, id)).get();
@@ -142,6 +150,7 @@ export function updateProvider(
if (msg.includes("UNIQUE constraint")) {
return { error: "供应商名称已存在", status: 409 };
}
logger.error({ error: msg, operation: "update", table: "providers" }, "数据库操作失败");
throw e;
}

View File

@@ -1,12 +1,18 @@
import type Database from "bun:sqlite";
import type { CreateConversationRequest, RuntimeMode } from "../../../shared/api";
import type { Logger } from "../../logger";
import { createConversation } from "../../db/conversations";
import { createApiError, jsonResponse } from "../../helpers";
import { validateIdParam } from "../../middleware";
export async function handleCreateConversation(req: Request, db: Database, mode: RuntimeMode): Promise<Response> {
export async function handleCreateConversation(
req: Request,
db: Database,
mode: RuntimeMode,
logger: Logger,
): Promise<Response> {
const url = new URL(req.url);
const projectId = url.pathname.split("/")[3];
@@ -16,14 +22,16 @@ export async function handleCreateConversation(req: Request, db: Database, mode:
let body: CreateConversationRequest = {};
try {
body = (await req.json()) as CreateConversationRequest;
} catch {
} catch (e: unknown) {
logger.warn({ error: e instanceof Error ? e.message : String(e) }, "请求 JSON 解析失败");
// empty body is ok, defaults will be used
}
const result = createConversation(db, validated.id, body.modelId);
const result = createConversation(db, validated.id, logger, body.modelId);
if ("error" in result) {
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
}
logger.info({ conversationId: result.conversation.id, projectId: validated.id }, "会话创建成功");
return jsonResponse({ conversation: result.conversation }, { mode, status: 201 });
}

View File

@@ -1,12 +1,13 @@
import type Database from "bun:sqlite";
import type { RuntimeMode } from "../../../shared/api";
import type { Logger } from "../../logger";
import { deleteConversation, getConversation } from "../../db/conversations";
import { createApiError, jsonResponse } from "../../helpers";
import { validateIdParam } from "../../middleware";
export function handleDeleteConversation(req: Request, db: Database, mode: RuntimeMode): Response {
export function handleDeleteConversation(req: Request, db: Database, mode: RuntimeMode, logger: Logger): Response {
const parts = new URL(req.url).pathname.split("/");
const projectId = parts[3];
const conversationId = parts[5];
@@ -26,10 +27,11 @@ export function handleDeleteConversation(req: Request, db: Database, mode: Runti
return jsonResponse(createApiError("会话不属于该项目", 403), { mode, status: 403 });
}
const result = deleteConversation(db, validatedConv.id);
const result = deleteConversation(db, validatedConv.id, logger);
if ("error" in result) {
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
}
logger.info({ conversationId: validatedConv.id }, "会话删除成功");
return jsonResponse({ success: true }, { mode });
}

View File

@@ -1,12 +1,13 @@
import type Database from "bun:sqlite";
import type { RuntimeMode } from "../../../shared/api";
import type { Logger } from "../../logger";
import { getConversation } from "../../db/conversations";
import { createApiError, jsonResponse } from "../../helpers";
import { validateIdParam } from "../../middleware";
export function handleGetConversation(req: Request, db: Database, mode: RuntimeMode): Response {
export function handleGetConversation(req: Request, db: Database, mode: RuntimeMode, _logger: Logger): Response {
const parts = new URL(req.url).pathname.split("/");
const projectId = parts[3];
const conversationId = parts[5];

View File

@@ -1,12 +1,13 @@
import type Database from "bun:sqlite";
import type { RuntimeMode } from "../../../shared/api";
import type { Logger } from "../../logger";
import { listConversations } from "../../db/conversations";
import { jsonResponse } from "../../helpers";
import { validateIdParam, validatePagination } from "../../middleware";
export function handleListConversations(req: Request, db: Database, mode: RuntimeMode): Response {
export function handleListConversations(req: Request, db: Database, mode: RuntimeMode, _logger: Logger): Response {
const url = new URL(req.url);
const projectId = url.pathname.split("/")[3];

View File

@@ -1,12 +1,13 @@
import type Database from "bun:sqlite";
import type { RuntimeMode } from "../../../shared/api";
import type { Logger } from "../../logger";
import { getConversation, listMessages } from "../../db/conversations";
import { createApiError, jsonResponse } from "../../helpers";
import { validateIdParam, validatePagination } from "../../middleware";
export function handleListMessages(req: Request, db: Database, mode: RuntimeMode): Response {
export function handleListMessages(req: Request, db: Database, mode: RuntimeMode, _logger: Logger): Response {
const parts = new URL(req.url).pathname.split("/");
const projectId = parts[3];
const conversationId = parts[5];

View File

@@ -29,7 +29,8 @@ 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 {
} catch (e) {
logger.warn({ error: e instanceof Error ? e.message : String(e) }, "请求 JSON 解析失败");
return jsonResponse(createApiError("Invalid JSON body", 400), { mode, status: 400 });
}
@@ -64,12 +65,16 @@ export async function handleSendChat(req: Request, db: Database, mode: RuntimeMo
?.filter((p) => p.type === "text")
.map((p) => p.text)
.join("") ?? "";
createMessage(db, {
content,
conversationId: conversation.id,
parts: JSON.stringify(lastMsg.parts ?? []),
role: "user",
});
createMessage(
db,
{
content,
conversationId: conversation.id,
parts: JSON.stringify(lastMsg.parts ?? []),
role: "user",
},
logger,
);
}
updateConversationTimestamp(db, conversation.id);
@@ -114,12 +119,16 @@ export async function handleSendChat(req: Request, db: Database, mode: RuntimeMo
.filter((p): p is { text: string; type: "text" } => p.type === "text")
.map((p) => p.text)
.join("");
createMessage(db, {
content: text,
conversationId: conversation.id,
parts: JSON.stringify(responseMessage.parts),
role: "assistant",
});
createMessage(
db,
{
content: text,
conversationId: conversation.id,
parts: JSON.stringify(responseMessage.parts),
role: "assistant",
},
logger,
);
updateConversationTimestamp(db, conversation.id);
},
uiMessages: body.messages,
@@ -138,7 +147,7 @@ function generateConversationTitle(
logger: Logger,
): void {
if (firstUserText.length <= 5) {
updateConversation(db, conversationId, { title: firstUserText });
updateConversation(db, conversationId, { title: firstUserText }, logger);
return;
}
@@ -149,13 +158,13 @@ function generateConversationTitle(
})
.then((result) => {
const title = result.text.trim().slice(0, 10);
updateConversation(db, conversationId, { title: title || firstUserText.slice(0, 10) });
updateConversation(db, conversationId, { title: title || firstUserText.slice(0, 10) }, logger);
})
.catch((titleError: unknown) => {
const titleMsg = titleError instanceof Error ? titleError.message : String(titleError);
logger.error({ conversationId, error: titleMsg }, "标题生成失败");
try {
updateConversation(db, conversationId, { title: firstUserText.slice(0, 10) });
updateConversation(db, conversationId, { title: firstUserText.slice(0, 10) }, logger);
} catch {
logger.error({ conversationId }, "标题兜底更新失败");
}

View File

@@ -1,12 +1,18 @@
import type Database from "bun:sqlite";
import type { RuntimeMode, UpdateConversationRequest } from "../../../shared/api";
import type { Logger } from "../../logger";
import { getConversation, updateConversation } from "../../db/conversations";
import { createApiError, jsonResponse } from "../../helpers";
import { validateIdParam } from "../../middleware";
export async function handleUpdateConversation(req: Request, db: Database, mode: RuntimeMode): Promise<Response> {
export async function handleUpdateConversation(
req: Request,
db: Database,
mode: RuntimeMode,
logger: Logger,
): Promise<Response> {
const url = new URL(req.url);
const parts = url.pathname.split("/");
const projectId = parts[3];
@@ -30,7 +36,8 @@ export async function handleUpdateConversation(req: Request, db: Database, mode:
let body: UpdateConversationRequest;
try {
body = (await req.json()) as UpdateConversationRequest;
} catch {
} catch (e: unknown) {
logger.warn({ error: e instanceof Error ? e.message : String(e) }, "请求 JSON 解析失败");
return jsonResponse(createApiError("Invalid JSON body", 400), { mode, status: 400 });
}
@@ -38,10 +45,11 @@ export async function handleUpdateConversation(req: Request, db: Database, mode:
return jsonResponse(createApiError("至少需要传 modelId 或 title", 400), { mode, status: 400 });
}
const result = updateConversation(db, validatedConv.id, body);
const result = updateConversation(db, validatedConv.id, body, logger);
if ("error" in result) {
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
}
logger.info({ conversationId: result.conversation.id }, "会话更新成功");
return jsonResponse({ conversation: result.conversation }, { mode });
}

View File

@@ -1,7 +1,8 @@
import type { RuntimeMode } from "../../shared/api";
import type { Logger } from "../logger";
import { createMetaResponse, jsonResponse } from "../helpers";
export function handleMeta(mode: RuntimeMode, version: string): Response {
export function handleMeta(mode: RuntimeMode, version: string, _logger: Logger): Response {
return jsonResponse(createMetaResponse(version), { mode });
}

View File

@@ -1,16 +1,23 @@
import type Database from "bun:sqlite";
import type { CreateModelRequest, RuntimeMode } from "../../../shared/api";
import type { Logger } from "../../logger";
import { MODEL_CAPABILITIES } from "../../../shared/api";
import { createModel } from "../../db/models";
import { createApiError, jsonResponse } from "../../helpers";
export async function handleCreateModel(req: Request, db: Database, mode: RuntimeMode): Promise<Response> {
export async function handleCreateModel(
req: Request,
db: Database,
mode: RuntimeMode,
logger: Logger,
): Promise<Response> {
let body: CreateModelRequest;
try {
body = (await req.json()) as CreateModelRequest;
} catch {
} catch (e: unknown) {
logger.warn({ error: e instanceof Error ? e.message : String(e) }, "请求 JSON 解析失败");
return jsonResponse(createApiError("Invalid JSON body", 400), { mode, status: 400 });
}
@@ -44,11 +51,15 @@ export async function handleCreateModel(req: Request, db: Database, mode: Runtim
const tokenError = validateOptionalPositiveInteger("maxOutputTokens", body.maxOutputTokens);
if (tokenError) return jsonResponse(createApiError(tokenError, 400), { mode, status: 400 });
const result = createModel(db, body);
const result = createModel(db, body, logger);
if ("error" in result) {
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
}
logger.info(
{ modelId: result.model.id, name: result.model.name, providerId: result.model.providerId },
"模型创建成功",
);
return jsonResponse(result, { mode, status: 201 });
}

View File

@@ -1,22 +1,24 @@
import type Database from "bun:sqlite";
import type { RuntimeMode } from "../../../shared/api";
import type { Logger } from "../../logger";
import { deleteModel } from "../../db/models";
import { createApiError, jsonResponse } from "../../helpers";
import { validateIdParam } from "../../middleware";
export function handleDeleteModel(req: Request, db: Database, mode: RuntimeMode): Response {
export function handleDeleteModel(req: Request, db: Database, mode: RuntimeMode, logger: Logger): Response {
const url = new URL(req.url);
const idStr = url.pathname.split("/")[3];
const validated = validateIdParam(idStr ?? "", mode);
if (validated instanceof Response) return validated;
const result = deleteModel(db, validated.id);
const result = deleteModel(db, validated.id, logger);
if ("error" in result) {
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
}
logger.info({ modelId: validated.id }, "模型删除成功");
return new Response(null, { status: 204 });
}

View File

@@ -1,12 +1,13 @@
import type Database from "bun:sqlite";
import type { RuntimeMode } from "../../../shared/api";
import type { Logger } from "../../logger";
import { getModel } from "../../db/models";
import { createApiError, jsonResponse } from "../../helpers";
import { validateIdParam } from "../../middleware";
export function handleGetModel(req: Request, db: Database, mode: RuntimeMode): Response {
export function handleGetModel(req: Request, db: Database, mode: RuntimeMode, _logger: Logger): Response {
const url = new URL(req.url);
const idStr = url.pathname.split("/")[3];

View File

@@ -1,12 +1,13 @@
import type Database from "bun:sqlite";
import type { RuntimeMode } from "../../../shared/api";
import type { Logger } from "../../logger";
import { listModels } from "../../db/models";
import { jsonResponse } from "../../helpers";
import { validatePagination } from "../../middleware";
export function handleListModels(req: Request, db: Database, mode: RuntimeMode): Response {
export function handleListModels(req: Request, db: Database, mode: RuntimeMode, _logger: Logger): Response {
const url = new URL(req.url);
const pageParam = url.searchParams.get("page");
const pageSizeParam = url.searchParams.get("pageSize");

View File

@@ -1,16 +1,23 @@
import type Database from "bun:sqlite";
import type { RuntimeMode, TestModelRequest } from "../../../shared/api";
import type { Logger } from "../../logger";
import { testModelConnection } from "../../ai/registry";
import { getProvider } from "../../db/providers";
import { createApiError, jsonResponse } from "../../helpers";
export async function handleTestModelConfig(req: Request, db: Database, mode: RuntimeMode): Promise<Response> {
export async function handleTestModelConfig(
req: Request,
db: Database,
mode: RuntimeMode,
logger: Logger,
): Promise<Response> {
let body: TestModelRequest;
try {
body = (await req.json()) as TestModelRequest;
} catch {
} catch (e: unknown) {
logger.warn({ error: e instanceof Error ? e.message : String(e) }, "请求 JSON 解析失败");
return jsonResponse(createApiError("Invalid JSON body", 400), { mode, status: 400 });
}
@@ -30,13 +37,23 @@ export async function handleTestModelConfig(req: Request, db: Database, mode: Ru
});
}
const testResult = await testModelConnection({
apiKey: providerResult.provider.apiKey,
baseUrl: providerResult.provider.baseUrl,
modelId: body.modelId,
name: providerResult.provider.name,
type: providerResult.provider.type,
});
const testResult = await testModelConnection(
{
apiKey: providerResult.provider.apiKey,
baseUrl: providerResult.provider.baseUrl,
modelId: body.modelId,
name: providerResult.provider.name,
type: providerResult.provider.type,
},
logger,
);
if (!testResult.ok) {
logger.warn(
{ message: testResult.message, modelId: body.modelId, providerId: body.providerId },
"模型连接测试失败",
);
}
return jsonResponse({ modelTestResponse: testResult }, { mode });
}

View File

@@ -1,13 +1,19 @@
import type Database from "bun:sqlite";
import type { RuntimeMode, UpdateModelRequest } from "../../../shared/api";
import type { Logger } from "../../logger";
import { MODEL_CAPABILITIES } from "../../../shared/api";
import { updateModel } from "../../db/models";
import { createApiError, jsonResponse } from "../../helpers";
import { validateIdParam } from "../../middleware";
export async function handleUpdateModel(req: Request, db: Database, mode: RuntimeMode): Promise<Response> {
export async function handleUpdateModel(
req: Request,
db: Database,
mode: RuntimeMode,
logger: Logger,
): Promise<Response> {
const url = new URL(req.url);
const idStr = url.pathname.split("/")[3];
@@ -17,7 +23,8 @@ export async function handleUpdateModel(req: Request, db: Database, mode: Runtim
let body: UpdateModelRequest;
try {
body = (await req.json()) as UpdateModelRequest;
} catch {
} catch (e: unknown) {
logger.warn({ error: e instanceof Error ? e.message : String(e) }, "请求 JSON 解析失败");
return jsonResponse(createApiError("Invalid JSON body", 400), { mode, status: 400 });
}
@@ -40,11 +47,12 @@ export async function handleUpdateModel(req: Request, db: Database, mode: Runtim
const tokenError = validateOptionalPositiveInteger("maxOutputTokens", body.maxOutputTokens);
if (tokenError) return jsonResponse(createApiError(tokenError, 400), { mode, status: 400 });
const result = updateModel(db, validated.id, body);
const result = updateModel(db, validated.id, body, logger);
if ("error" in result) {
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
}
logger.info({ modelId: result.model.id }, "模型更新成功");
return jsonResponse(result, { mode });
}

View File

@@ -1,22 +1,24 @@
import type Database from "bun:sqlite";
import type { RuntimeMode } from "../../../shared/api";
import type { Logger } from "../../logger";
import { archiveProject } from "../../db/projects";
import { createApiError, jsonResponse } from "../../helpers";
import { validateIdParam } from "../../middleware";
export function handleArchiveProject(req: Request, db: Database, mode: RuntimeMode): Response {
export function handleArchiveProject(req: Request, db: Database, mode: RuntimeMode, logger: Logger): Response {
const url = new URL(req.url);
const idStr = url.pathname.split("/")[3];
const validated = validateIdParam(idStr ?? "", mode);
if (validated instanceof Response) return validated;
const result = archiveProject(db, validated.id);
const result = archiveProject(db, validated.id, logger);
if ("error" in result) {
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
}
logger.info({ projectId: validated.id }, "项目归档成功");
return jsonResponse(result, { mode });
}

View File

@@ -1,15 +1,22 @@
import type Database from "bun:sqlite";
import type { CreateProjectRequest, RuntimeMode } from "../../../shared/api";
import type { Logger } from "../../logger";
import { createProject } from "../../db/projects";
import { createApiError, jsonResponse } from "../../helpers";
export async function handleCreateProject(req: Request, db: Database, mode: RuntimeMode): Promise<Response> {
export async function handleCreateProject(
req: Request,
db: Database,
mode: RuntimeMode,
logger: Logger,
): Promise<Response> {
let body: CreateProjectRequest;
try {
body = (await req.json()) as CreateProjectRequest;
} catch {
} catch (e: unknown) {
logger.warn({ error: e instanceof Error ? e.message : String(e) }, "请求 JSON 解析失败");
return jsonResponse(createApiError("Invalid JSON body", 400), { mode, status: 400 });
}
@@ -17,10 +24,11 @@ export async function handleCreateProject(req: Request, db: Database, mode: Runt
return jsonResponse(createApiError("name is required", 400), { mode, status: 400 });
}
const result = createProject(db, body);
const result = createProject(db, body, logger);
if ("error" in result) {
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
}
logger.info({ name: result.project.name, projectId: result.project.id }, "项目创建成功");
return jsonResponse(result, { mode, status: 201 });
}

View File

@@ -1,22 +1,24 @@
import type Database from "bun:sqlite";
import type { RuntimeMode } from "../../../shared/api";
import type { Logger } from "../../logger";
import { deleteProject } from "../../db/projects";
import { createApiError, jsonResponse, parseIdFromUrl } from "../../helpers";
import { validateIdParam } from "../../middleware";
export function handleDeleteProject(req: Request, db: Database, mode: RuntimeMode): Response {
export function handleDeleteProject(req: Request, db: Database, mode: RuntimeMode, logger: Logger): Response {
const url = new URL(req.url);
const idStr = parseIdFromUrl(url);
const validated = validateIdParam(idStr ?? "", mode);
if (validated instanceof Response) return validated;
const result = deleteProject(db, validated.id);
const result = deleteProject(db, validated.id, logger);
if ("error" in result) {
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
}
logger.info({ projectId: validated.id }, "项目删除成功");
return new Response(null, { status: 204 });
}

View File

@@ -1,12 +1,13 @@
import type Database from "bun:sqlite";
import type { RuntimeMode } from "../../../shared/api";
import type { Logger } from "../../logger";
import { getProject } from "../../db/projects";
import { jsonResponse } from "../../helpers";
import { validateIdParam } from "../../middleware";
export function handleGetProject(req: Request, db: Database, mode: RuntimeMode): Response {
export function handleGetProject(req: Request, db: Database, mode: RuntimeMode, _logger: Logger): Response {
const url = new URL(req.url);
const idStr = url.pathname.split("/")[3];

View File

@@ -1,12 +1,13 @@
import type Database from "bun:sqlite";
import type { RuntimeMode } from "../../../shared/api";
import type { Logger } from "../../logger";
import { listProjects } from "../../db/projects";
import { createApiError, jsonResponse } from "../../helpers";
import { validatePagination } from "../../middleware";
export function handleListProjects(req: Request, db: Database, mode: RuntimeMode): Response {
export function handleListProjects(req: Request, db: Database, mode: RuntimeMode, _logger: Logger): Response {
const url = new URL(req.url);
const pageParam = url.searchParams.get("page");
const pageSizeParam = url.searchParams.get("pageSize");

View File

@@ -1,22 +1,24 @@
import type Database from "bun:sqlite";
import type { RuntimeMode } from "../../../shared/api";
import type { Logger } from "../../logger";
import { restoreProject } from "../../db/projects";
import { createApiError, jsonResponse } from "../../helpers";
import { validateIdParam } from "../../middleware";
export function handleRestoreProject(req: Request, db: Database, mode: RuntimeMode): Response {
export function handleRestoreProject(req: Request, db: Database, mode: RuntimeMode, logger: Logger): Response {
const url = new URL(req.url);
const idStr = url.pathname.split("/")[3];
const validated = validateIdParam(idStr ?? "", mode);
if (validated instanceof Response) return validated;
const result = restoreProject(db, validated.id);
const result = restoreProject(db, validated.id, logger);
if ("error" in result) {
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
}
logger.info({ projectId: validated.id }, "项目恢复成功");
return jsonResponse(result, { mode });
}

View File

@@ -1,12 +1,18 @@
import type Database from "bun:sqlite";
import type { RuntimeMode, UpdateProjectRequest } from "../../../shared/api";
import type { Logger } from "../../logger";
import { updateProject } from "../../db/projects";
import { createApiError, jsonResponse } from "../../helpers";
import { validateIdParam } from "../../middleware";
export async function handleUpdateProject(req: Request, db: Database, mode: RuntimeMode): Promise<Response> {
export async function handleUpdateProject(
req: Request,
db: Database,
mode: RuntimeMode,
logger: Logger,
): Promise<Response> {
const url = new URL(req.url);
const idStr = url.pathname.split("/")[3];
@@ -16,7 +22,9 @@ export async function handleUpdateProject(req: Request, db: Database, mode: Runt
let body: UpdateProjectRequest;
try {
body = (await req.json()) as UpdateProjectRequest;
} catch {
} 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 });
}
@@ -24,10 +32,11 @@ export async function handleUpdateProject(req: Request, db: Database, mode: Runt
return jsonResponse(createApiError("At least one of name or description is required", 400), { mode, status: 400 });
}
const result = updateProject(db, validated.id, body);
const result = updateProject(db, validated.id, body, logger);
if ("error" in result) {
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
}
logger.info({ projectId: result.project.id }, "项目更新成功");
return jsonResponse(result, { mode });
}

View File

@@ -1,15 +1,22 @@
import type Database from "bun:sqlite";
import type { CreateProviderRequest, RuntimeMode } from "../../../shared/api";
import type { Logger } from "../../logger";
import { createProvider } from "../../db/providers";
import { createApiError, jsonResponse } from "../../helpers";
export async function handleCreateProvider(req: Request, db: Database, mode: RuntimeMode): Promise<Response> {
export async function handleCreateProvider(
req: Request,
db: Database,
mode: RuntimeMode,
logger: Logger,
): Promise<Response> {
let body: CreateProviderRequest;
try {
body = (await req.json()) as CreateProviderRequest;
} catch {
} catch (e: unknown) {
logger.warn({ error: e instanceof Error ? e.message : String(e) }, "请求 JSON 解析失败");
return jsonResponse(createApiError("Invalid JSON body", 400), { mode, status: 400 });
}
@@ -32,10 +39,14 @@ export async function handleCreateProvider(req: Request, db: Database, mode: Run
});
}
const result = createProvider(db, body);
const result = createProvider(db, body, logger);
if ("error" in result) {
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
}
logger.info(
{ name: result.provider.name, providerId: result.provider.id, type: result.provider.type },
"供应商创建成功",
);
return jsonResponse(result, { mode, status: 201 });
}

View File

@@ -1,13 +1,14 @@
import type Database from "bun:sqlite";
import type { RuntimeMode } from "../../../shared/api";
import type { Logger } from "../../logger";
import { getModelsByProviderId } from "../../db/models";
import { deleteProvider } from "../../db/providers";
import { createApiError, jsonResponse, parseIdFromUrl } from "../../helpers";
import { validateIdParam } from "../../middleware";
export function handleDeleteProvider(req: Request, db: Database, mode: RuntimeMode): Response {
export function handleDeleteProvider(req: Request, db: Database, mode: RuntimeMode, logger: Logger): Response {
const url = new URL(req.url);
const idStr = parseIdFromUrl(url);
@@ -19,10 +20,11 @@ export function handleDeleteProvider(req: Request, db: Database, mode: RuntimeMo
return jsonResponse(createApiError("该供应商下存在模型,无法删除", 409), { mode, status: 409 });
}
const result = deleteProvider(db, validated.id);
const result = deleteProvider(db, validated.id, logger);
if ("error" in result) {
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
}
logger.info({ providerId: validated.id }, "供应商删除成功");
return new Response(null, { status: 204 });
}

View File

@@ -1,12 +1,13 @@
import type Database from "bun:sqlite";
import type { RuntimeMode } from "../../../shared/api";
import type { Logger } from "../../logger";
import { getProvider } from "../../db/providers";
import { createApiError, jsonResponse } from "../../helpers";
import { validateIdParam } from "../../middleware";
export function handleGetProvider(req: Request, db: Database, mode: RuntimeMode): Response {
export function handleGetProvider(req: Request, db: Database, mode: RuntimeMode, _logger: Logger): Response {
const url = new URL(req.url);
const idStr = url.pathname.split("/")[3];

View File

@@ -1,12 +1,13 @@
import type Database from "bun:sqlite";
import type { RuntimeMode } from "../../../shared/api";
import type { Logger } from "../../logger";
import { listProviders } from "../../db/providers";
import { jsonResponse } from "../../helpers";
import { validatePagination } from "../../middleware";
export function handleListProviders(req: Request, db: Database, mode: RuntimeMode): Response {
export function handleListProviders(req: Request, db: Database, mode: RuntimeMode, _logger: Logger): Response {
const url = new URL(req.url);
const pageParam = url.searchParams.get("page");
const pageSizeParam = url.searchParams.get("pageSize");

View File

@@ -1,10 +1,11 @@
import type Database from "bun:sqlite";
import type { RuntimeMode } from "../../../shared/api";
import type { Logger } from "../../logger";
import { listProviderOptions } from "../../db/providers";
import { jsonResponse } from "../../helpers";
export function handleListProviderOptions(db: Database, mode: RuntimeMode): Response {
export function handleListProviderOptions(db: Database, mode: RuntimeMode, _logger: Logger): Response {
return jsonResponse({ items: listProviderOptions(db) }, { mode });
}

View File

@@ -1,29 +1,46 @@
import type Database from "bun:sqlite";
import type { CreateProviderRequest, RuntimeMode } from "../../../shared/api";
import type { Logger } from "../../logger";
import { testProviderConnection } from "../../ai/registry";
import { createApiError, jsonResponse } from "../../helpers";
export async function handleTestProviderConfig(req: Request, _db: Database, mode: RuntimeMode): Promise<Response> {
const validated = await readProviderConfig(req, mode);
export async function handleTestProviderConfig(
req: Request,
db: Database,
mode: RuntimeMode,
logger: Logger,
): Promise<Response> {
const validated = await readProviderConfig(req, mode, logger);
if (validated instanceof Response) return validated;
const testResult = await testProviderConnection({
apiKey: validated.apiKey,
baseUrl: validated.baseUrl,
name: validated.name,
type: validated.type,
});
const testResult = await testProviderConnection(
{
apiKey: validated.apiKey,
baseUrl: validated.baseUrl,
name: validated.name,
type: validated.type,
},
logger,
);
if (!testResult.ok) {
logger.warn({ message: testResult.message, name: validated.name, type: validated.type }, "供应商连接测试失败");
}
return jsonResponse({ providerTestResponse: testResult }, { mode });
}
async function readProviderConfig(req: Request, mode: RuntimeMode): Promise<CreateProviderRequest | Response> {
async function readProviderConfig(
req: Request,
mode: RuntimeMode,
logger: Logger,
): Promise<CreateProviderRequest | Response> {
let body: CreateProviderRequest;
try {
body = (await req.json()) as CreateProviderRequest;
} catch {
} catch (e: unknown) {
logger.warn({ error: e instanceof Error ? e.message : String(e) }, "请求 JSON 解析失败");
return jsonResponse(createApiError("Invalid JSON body", 400), { mode, status: 400 });
}

View File

@@ -1,12 +1,18 @@
import type Database from "bun:sqlite";
import type { RuntimeMode, UpdateProviderRequest } from "../../../shared/api";
import type { Logger } from "../../logger";
import { updateProvider } from "../../db/providers";
import { createApiError, jsonResponse } from "../../helpers";
import { validateIdParam } from "../../middleware";
export async function handleUpdateProvider(req: Request, db: Database, mode: RuntimeMode): Promise<Response> {
export async function handleUpdateProvider(
req: Request,
db: Database,
mode: RuntimeMode,
logger: Logger,
): Promise<Response> {
const url = new URL(req.url);
const idStr = url.pathname.split("/")[3];
@@ -16,7 +22,8 @@ export async function handleUpdateProvider(req: Request, db: Database, mode: Run
let body: UpdateProviderRequest;
try {
body = (await req.json()) as UpdateProviderRequest;
} catch {
} catch (e: unknown) {
logger.warn({ error: e instanceof Error ? e.message : String(e) }, "请求 JSON 解析失败");
return jsonResponse(createApiError("Invalid JSON body", 400), { mode, status: 400 });
}
@@ -27,10 +34,11 @@ export async function handleUpdateProvider(req: Request, db: Database, mode: Run
});
}
const result = updateProvider(db, validated.id, body);
const result = updateProvider(db, validated.id, body, logger);
if ("error" in result) {
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
}
logger.info({ providerId: result.provider.id }, "供应商更新成功");
return jsonResponse(result, { mode });
}

View File

@@ -42,7 +42,7 @@ export function startServer(options: StartServerOptions) {
GET: withErrorHandler(
async () => {
const resolvedVersion = await resolveVersion();
return handleMeta(mode, resolvedVersion);
return handleMeta(mode, resolvedVersion, logger);
},
mode,
logger,
@@ -52,7 +52,7 @@ export function startServer(options: StartServerOptions) {
GET: withErrorHandler(
async (req) => {
const { handleListModels } = await import("./routes/models/list");
return handleListModels(req, db, mode);
return handleListModels(req, db, mode, logger);
},
mode,
logger,
@@ -60,7 +60,7 @@ export function startServer(options: StartServerOptions) {
POST: withErrorHandler(
async (req) => {
const { handleCreateModel } = await import("./routes/models/create");
return handleCreateModel(req, db, mode);
return handleCreateModel(req, db, mode, logger);
},
mode,
logger,
@@ -70,7 +70,7 @@ export function startServer(options: StartServerOptions) {
DELETE: withErrorHandler(
async (req) => {
const { handleDeleteModel } = await import("./routes/models/delete");
return handleDeleteModel(req, db, mode);
return handleDeleteModel(req, db, mode, logger);
},
mode,
logger,
@@ -78,7 +78,7 @@ export function startServer(options: StartServerOptions) {
GET: withErrorHandler(
async (req) => {
const { handleGetModel } = await import("./routes/models/get");
return handleGetModel(req, db, mode);
return handleGetModel(req, db, mode, logger);
},
mode,
logger,
@@ -86,7 +86,7 @@ export function startServer(options: StartServerOptions) {
PATCH: withErrorHandler(
async (req) => {
const { handleUpdateModel } = await import("./routes/models/update");
return handleUpdateModel(req, db, mode);
return handleUpdateModel(req, db, mode, logger);
},
mode,
logger,
@@ -96,7 +96,7 @@ export function startServer(options: StartServerOptions) {
POST: withErrorHandler(
async (req) => {
const { handleTestModelConfig } = await import("./routes/models/test");
return handleTestModelConfig(req, db, mode);
return handleTestModelConfig(req, db, mode, logger);
},
mode,
logger,
@@ -106,7 +106,7 @@ export function startServer(options: StartServerOptions) {
GET: withErrorHandler(
async (req) => {
const { handleListProjects } = await import("./routes/projects/list");
return handleListProjects(req, db, mode);
return handleListProjects(req, db, mode, logger);
},
mode,
logger,
@@ -114,7 +114,7 @@ export function startServer(options: StartServerOptions) {
POST: withErrorHandler(
async (req) => {
const { handleCreateProject } = await import("./routes/projects/create");
return handleCreateProject(req, db, mode);
return handleCreateProject(req, db, mode, logger);
},
mode,
logger,
@@ -124,7 +124,7 @@ export function startServer(options: StartServerOptions) {
DELETE: withErrorHandler(
async (req) => {
const { handleDeleteProject } = await import("./routes/projects/delete");
return handleDeleteProject(req, db, mode);
return handleDeleteProject(req, db, mode, logger);
},
mode,
logger,
@@ -132,7 +132,7 @@ export function startServer(options: StartServerOptions) {
GET: withErrorHandler(
async (req) => {
const { handleGetProject } = await import("./routes/projects/get");
return handleGetProject(req, db, mode);
return handleGetProject(req, db, mode, logger);
},
mode,
logger,
@@ -140,7 +140,7 @@ export function startServer(options: StartServerOptions) {
PATCH: withErrorHandler(
async (req) => {
const { handleUpdateProject } = await import("./routes/projects/update");
return handleUpdateProject(req, db, mode);
return handleUpdateProject(req, db, mode, logger);
},
mode,
logger,
@@ -150,7 +150,7 @@ export function startServer(options: StartServerOptions) {
POST: withErrorHandler(
async (req) => {
const { handleArchiveProject } = await import("./routes/projects/archive");
return handleArchiveProject(req, db, mode);
return handleArchiveProject(req, db, mode, logger);
},
mode,
logger,
@@ -170,7 +170,7 @@ export function startServer(options: StartServerOptions) {
GET: withErrorHandler(
async (req) => {
const { handleListConversations } = await import("./routes/chat/list");
return handleListConversations(req, db, mode);
return handleListConversations(req, db, mode, logger);
},
mode,
logger,
@@ -178,7 +178,7 @@ export function startServer(options: StartServerOptions) {
POST: withErrorHandler(
async (req) => {
const { handleCreateConversation } = await import("./routes/chat/create");
return handleCreateConversation(req, db, mode);
return handleCreateConversation(req, db, mode, logger);
},
mode,
logger,
@@ -188,7 +188,7 @@ export function startServer(options: StartServerOptions) {
DELETE: withErrorHandler(
async (req) => {
const { handleDeleteConversation } = await import("./routes/chat/delete");
return handleDeleteConversation(req, db, mode);
return handleDeleteConversation(req, db, mode, logger);
},
mode,
logger,
@@ -196,7 +196,7 @@ export function startServer(options: StartServerOptions) {
GET: withErrorHandler(
async (req) => {
const { handleGetConversation } = await import("./routes/chat/get");
return handleGetConversation(req, db, mode);
return handleGetConversation(req, db, mode, logger);
},
mode,
logger,
@@ -204,7 +204,7 @@ export function startServer(options: StartServerOptions) {
PATCH: withErrorHandler(
async (req) => {
const { handleUpdateConversation } = await import("./routes/chat/update");
return handleUpdateConversation(req, db, mode);
return handleUpdateConversation(req, db, mode, logger);
},
mode,
logger,
@@ -214,7 +214,7 @@ export function startServer(options: StartServerOptions) {
GET: withErrorHandler(
async (req) => {
const { handleListMessages } = await import("./routes/chat/messages");
return handleListMessages(req, db, mode);
return handleListMessages(req, db, mode, logger);
},
mode,
logger,
@@ -224,7 +224,7 @@ export function startServer(options: StartServerOptions) {
POST: withErrorHandler(
async (req) => {
const { handleRestoreProject } = await import("./routes/projects/restore");
return handleRestoreProject(req, db, mode);
return handleRestoreProject(req, db, mode, logger);
},
mode,
logger,
@@ -234,7 +234,7 @@ export function startServer(options: StartServerOptions) {
GET: withErrorHandler(
async (req) => {
const { handleListProviders } = await import("./routes/providers/list");
return handleListProviders(req, db, mode);
return handleListProviders(req, db, mode, logger);
},
mode,
logger,
@@ -242,7 +242,7 @@ export function startServer(options: StartServerOptions) {
POST: withErrorHandler(
async (req) => {
const { handleCreateProvider } = await import("./routes/providers/create");
return handleCreateProvider(req, db, mode);
return handleCreateProvider(req, db, mode, logger);
},
mode,
logger,
@@ -252,7 +252,7 @@ export function startServer(options: StartServerOptions) {
DELETE: withErrorHandler(
async (req) => {
const { handleDeleteProvider } = await import("./routes/providers/delete");
return handleDeleteProvider(req, db, mode);
return handleDeleteProvider(req, db, mode, logger);
},
mode,
logger,
@@ -260,7 +260,7 @@ export function startServer(options: StartServerOptions) {
GET: withErrorHandler(
async (req) => {
const { handleGetProvider } = await import("./routes/providers/get");
return handleGetProvider(req, db, mode);
return handleGetProvider(req, db, mode, logger);
},
mode,
logger,
@@ -268,7 +268,7 @@ export function startServer(options: StartServerOptions) {
PATCH: withErrorHandler(
async (req) => {
const { handleUpdateProvider } = await import("./routes/providers/update");
return handleUpdateProvider(req, db, mode);
return handleUpdateProvider(req, db, mode, logger);
},
mode,
logger,
@@ -278,7 +278,7 @@ export function startServer(options: StartServerOptions) {
GET: withErrorHandler(
async () => {
const { handleListProviderOptions } = await import("./routes/providers/options");
return handleListProviderOptions(db, mode);
return handleListProviderOptions(db, mode, logger);
},
mode,
logger,
@@ -288,7 +288,7 @@ export function startServer(options: StartServerOptions) {
POST: withErrorHandler(
async (req) => {
const { handleTestProviderConfig } = await import("./routes/providers/test");
return handleTestProviderConfig(req, db, mode);
return handleTestProviderConfig(req, db, mode, logger);
},
mode,
logger,

View File

@@ -11,6 +11,7 @@ import {
fetchMessages,
updateConversation,
} from "../../../../hooks/use-conversations";
import { useLogger } from "../../../../hooks/use-logger";
import { useModelList } from "../../../../hooks/use-models";
import { ChatInputArea } from "./ChatInputArea";
import { ReasoningPart } from "./parts/ReasoningPart";
@@ -25,6 +26,7 @@ interface ChatPanelProps {
export function ChatPanel({ conversationId, onConversationCreated, projectId }: ChatPanelProps) {
const { message } = App.useApp();
const logger = useLogger().child({ component: "ChatPanel", page: "workbench" });
const queryClient = useQueryClient();
const [input, setInput] = useState("");
const [editingMessageId, setEditingMessageId] = useState<null | string>(null);
@@ -45,6 +47,7 @@ export function ChatPanel({ conversationId, onConversationCreated, projectId }:
const { messages, regenerate, sendMessage, setMessages, status, stop } = useChat({
onError: (err) => {
logger.error("聊天发送失败", { error: err.message });
void message.error(`发送失败:${err.message}`);
},
transport: new DefaultChatTransport({ api: `/api/projects/${projectId}/chat` }),
@@ -87,6 +90,7 @@ export function ChatPanel({ conversationId, onConversationCreated, projectId }:
} catch (err: unknown) {
if (!cancelled) {
const msg = err instanceof Error ? err.message : String(err);
logger.error("加载历史失败", { conversationId, error: msg, projectId });
void message.error(`加载历史失败:${msg}`);
}
} finally {
@@ -99,22 +103,27 @@ export function ChatPanel({ conversationId, onConversationCreated, projectId }:
return () => {
cancelled = true;
};
}, [conversationId, projectId, setMessages, message]);
}, [conversationId, projectId, setMessages, message, logger]);
useEffect(() => {
if (!conversationId) return;
const firstTextId = textModels[0]?.id;
if (!firstTextId) return;
void fetchConversation(projectId, conversationId).then((conv) => {
if (textModels.every((m) => m.id !== conv.modelId)) {
setSelectedModelId(firstTextId);
void updateConversation(projectId, conversationId, { modelId: firstTextId });
} else {
setSelectedModelId(conv.modelId);
}
});
}, [conversationId, textModels, projectId]);
void fetchConversation(projectId, conversationId)
.then((conv) => {
if (textModels.every((m) => m.id !== conv.modelId)) {
setSelectedModelId(firstTextId);
void updateConversation(projectId, conversationId, { modelId: firstTextId });
} else {
setSelectedModelId(conv.modelId);
}
})
.catch((err: unknown) => {
const msg = err instanceof Error ? err.message : String(err);
logger.warn("获取会话模型信息失败", { conversationId, error: msg, projectId });
});
}, [conversationId, textModels, projectId, logger]);
useEffect(() => {
scrollRef.current?.scrollTo({ behavior: "smooth", top: scrollRef.current.scrollHeight });
@@ -132,10 +141,13 @@ export function ChatPanel({ conversationId, onConversationCreated, projectId }:
(value: string) => {
setSelectedModelId(value);
if (conversationId) {
void updateConversation(projectId, conversationId, { modelId: value });
void updateConversation(projectId, conversationId, { modelId: value }).catch((err: unknown) => {
const msg = err instanceof Error ? err.message : String(err);
logger.warn("更新会话模型失败", { conversationId, error: msg, projectId });
});
}
},
[projectId, conversationId],
[projectId, conversationId, logger],
);
const handleSend = useCallback(async () => {
@@ -153,13 +165,27 @@ export function ChatPanel({ conversationId, onConversationCreated, projectId }:
} catch (err: unknown) {
setInput(text);
const msg = err instanceof Error ? err.message : String(err);
logger.error("创建会话失败", { error: msg, projectId });
void message.error(`创建会话失败:${msg}`);
}
return;
}
void sendMessage({ text }, { body: { conversationId } });
}, [input, sendMessage, conversationId, projectId, onConversationCreated, message, queryClient, displayModelId]);
void sendMessage({ text }, { body: { conversationId } }).catch((err: unknown) => {
const msg = err instanceof Error ? err.message : String(err);
logger.error("发送消息失败", { conversationId, error: msg, projectId });
});
}, [
input,
sendMessage,
conversationId,
projectId,
onConversationCreated,
message,
queryClient,
displayModelId,
logger,
]);
const extractText = useCallback((msg: UIMessage) => {
return msg.parts
@@ -171,11 +197,17 @@ export function ChatPanel({ conversationId, onConversationCreated, projectId }:
const handleCopy = useCallback(
(msg: UIMessage) => {
const text = extractText(msg);
void navigator.clipboard.writeText(text).then(() => {
void message.success("已复制");
});
void navigator.clipboard
.writeText(text)
.then(() => {
void message.success("已复制");
})
.catch((err: unknown) => {
const msg = err instanceof Error ? err.message : String(err);
logger.warn("复制失败", { error: msg });
});
},
[extractText, message],
[extractText, message, logger],
);
const handleEditStart = useCallback(
@@ -192,8 +224,11 @@ export function ChatPanel({ conversationId, onConversationCreated, projectId }:
const idx = messages.findIndex((m) => m.id === editingMessageId);
if (idx === -1) return;
setMessages(messages.slice(0, idx));
void sendMessage({ text: editText }, { body: { conversationId } });
}, [editText, conversationId, messages, editingMessageId, setMessages, sendMessage]);
void sendMessage({ text: editText }, { body: { conversationId } }).catch((err: unknown) => {
const msg = err instanceof Error ? err.message : String(err);
logger.error("重新发送消息失败", { conversationId, error: msg, projectId });
});
}, [editText, conversationId, messages, editingMessageId, setMessages, sendMessage, logger, projectId]);
const handleEditCancel = useCallback(() => {
setEditingMessageId(null);
@@ -202,8 +237,11 @@ export function ChatPanel({ conversationId, onConversationCreated, projectId }:
const handleRegenerate = useCallback(() => {
if (!conversationId) return;
void regenerate({ body: { conversationId } });
}, [regenerate, conversationId]);
void regenerate({ body: { conversationId } }).catch((err: unknown) => {
const msg = err instanceof Error ? err.message : String(err);
logger.error("重新生成失败", { conversationId, error: msg, projectId });
});
}, [regenerate, conversationId, logger, projectId]);
const getCardExtra = useCallback(
(msg: UIMessage, idx: number) => {
@@ -282,7 +320,10 @@ export function ChatPanel({ conversationId, onConversationCreated, projectId }:
void handleSend();
}}
onStop={() => {
void stop();
void stop().catch((err: unknown) => {
const msg = err instanceof Error ? err.message : String(err);
logger.warn("停止聊天失败", { error: msg });
});
}}
/>
</div>
@@ -350,7 +391,10 @@ export function ChatPanel({ conversationId, onConversationCreated, projectId }:
void handleSend();
}}
onStop={() => {
void stop();
void stop().catch((err: unknown) => {
const msg = err instanceof Error ? err.message : String(err);
logger.warn("停止聊天失败", { error: msg });
});
}}
/>
</div>

View File

@@ -7,6 +7,9 @@ import type {
} from "../../shared/api";
import { handleResponse, handleVoidResponse } from "../utils/api";
import { createConsoleLogger } from "../utils/logger";
const logger = createConsoleLogger();
export async function createConversation(projectId: string, modelId?: string): Promise<Conversation> {
const response = await fetch(`/api/projects/${projectId}/conversations`, {
@@ -23,8 +26,17 @@ export async function deleteConversation(projectId: string, conversationId: stri
}
export async function fetchConversation(projectId: string, conversationId: string): Promise<Conversation> {
const response = await fetch(`/api/projects/${projectId}/conversations/${conversationId}`);
return handleResponse(response, (data) => (data as ConversationResponse).conversation);
try {
const response = await fetch(`/api/projects/${projectId}/conversations/${conversationId}`);
return handleResponse(response, (data) => (data as ConversationResponse).conversation);
} catch (err) {
logger.error("获取会话失败", {
conversationId,
error: err instanceof Error ? err.message : String(err),
projectId,
});
throw err;
}
}
export async function fetchConversations(projectId: string): Promise<ConversationListResponse> {
@@ -42,10 +54,19 @@ export async function updateConversation(
conversationId: string,
data: UpdateConversationRequest,
): Promise<Conversation> {
const response = await fetch(`/api/projects/${projectId}/conversations/${conversationId}`, {
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
method: "PATCH",
});
return handleResponse(response, (data) => (data as ConversationResponse).conversation);
try {
const response = await fetch(`/api/projects/${projectId}/conversations/${conversationId}`, {
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
method: "PATCH",
});
return handleResponse(response, (data) => (data as ConversationResponse).conversation);
} catch (err) {
logger.error("更新会话失败", {
conversationId,
error: err instanceof Error ? err.message : String(err),
projectId,
});
throw err;
}
}

View File

@@ -12,8 +12,10 @@ import type {
} from "../../shared/api";
import { handleResponse, handleVoidResponse } from "../utils/api";
import { createConsoleLogger } from "../utils/logger";
const MODELS_KEY = ["models"] as const;
const logger = createConsoleLogger();
export async function createModel(data: CreateModelRequest): Promise<Model> {
const response = await fetch("/api/models", {
@@ -82,7 +84,8 @@ export function useCreateModel() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: createModel,
onSuccess: () => {
onSuccess: (data) => {
logger.info("模型创建成功", { modelId: data.modelId, providerId: data.providerId });
void queryClient.invalidateQueries({ queryKey: MODELS_KEY });
},
});
@@ -92,7 +95,8 @@ export function useDeleteModel() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: deleteModel,
onSuccess: () => {
onSuccess: (_data, variables) => {
logger.info("模型删除成功", { modelId: variables });
void queryClient.invalidateQueries({ queryKey: MODELS_KEY });
},
});
@@ -123,7 +127,8 @@ export function useUpdateModel() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (args: { data: UpdateModelRequest; id: string }) => updateModel(args.id, args.data),
onSuccess: () => {
onSuccess: (data) => {
logger.info("模型更新成功", { modelId: data.modelId, providerId: data.providerId });
void queryClient.invalidateQueries({ queryKey: MODELS_KEY });
},
});

View File

@@ -10,8 +10,10 @@ import type {
} from "../../shared/api";
import { handleResponse, handleVoidResponse } from "../utils/api";
import { createConsoleLogger } from "../utils/logger";
const PROJECTS_KEY = ["projects"] as const;
const logger = createConsoleLogger();
export async function archiveProject(id: string): Promise<Project> {
const response = await fetch(`/api/projects/${id}/archive`, { method: "POST" });
@@ -76,7 +78,8 @@ export function useArchiveProject() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: archiveProject,
onSuccess: () => {
onSuccess: (data) => {
logger.info("项目归档成功", { name: data.name, projectId: data.id });
void queryClient.invalidateQueries({ queryKey: PROJECTS_KEY });
},
});
@@ -86,7 +89,8 @@ export function useCreateProject() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: createProject,
onSuccess: () => {
onSuccess: (data) => {
logger.info("项目创建成功", { name: data.name, projectId: data.id });
void queryClient.invalidateQueries({ queryKey: PROJECTS_KEY });
},
});
@@ -96,7 +100,8 @@ export function useDeleteProject() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: deleteProject,
onSuccess: () => {
onSuccess: (_data, variables) => {
logger.info("项目删除成功", { projectId: variables });
void queryClient.invalidateQueries({ queryKey: PROJECTS_KEY });
},
});
@@ -121,7 +126,8 @@ export function useRestoreProject() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: restoreProject,
onSuccess: () => {
onSuccess: (data) => {
logger.info("项目恢复成功", { name: data.name, projectId: data.id });
void queryClient.invalidateQueries({ queryKey: PROJECTS_KEY });
},
});
@@ -131,7 +137,8 @@ export function useUpdateProject() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (args: { data: UpdateProjectRequest; id: string }) => updateProject(args.id, args.data),
onSuccess: () => {
onSuccess: (data) => {
logger.info("项目更新成功", { name: data.name, projectId: data.id });
void queryClient.invalidateQueries({ queryKey: PROJECTS_KEY });
},
});

View File

@@ -12,9 +12,11 @@ import type {
} from "../../shared/api";
import { handleResponse, handleVoidResponse } from "../utils/api";
import { createConsoleLogger } from "../utils/logger";
const PROVIDERS_KEY = ["providers"] as const;
const MODELS_KEY = ["models"] as const;
const logger = createConsoleLogger();
export async function createProvider(data: CreateProviderRequest): Promise<Provider> {
const response = await fetch("/api/providers", {
@@ -90,7 +92,8 @@ export function useCreateProvider() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: createProvider,
onSuccess: () => {
onSuccess: (data) => {
logger.info("供应商创建成功", { name: data.name, providerId: data.id });
void queryClient.invalidateQueries({ queryKey: PROVIDERS_KEY });
},
});
@@ -100,7 +103,8 @@ export function useDeleteProvider() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: deleteProvider,
onSuccess: () => {
onSuccess: (_data, variables) => {
logger.info("供应商删除成功", { providerId: variables });
void queryClient.invalidateQueries({ queryKey: PROVIDERS_KEY });
void queryClient.invalidateQueries({ queryKey: MODELS_KEY });
},
@@ -139,7 +143,8 @@ export function useUpdateProvider() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (args: { data: UpdateProviderRequest; id: string }) => updateProvider(args.id, args.data),
onSuccess: () => {
onSuccess: (data) => {
logger.info("供应商更新成功", { name: data.name, providerId: data.id });
void queryClient.invalidateQueries({ queryKey: PROVIDERS_KEY });
},
});

View File

@@ -1,4 +1,4 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { MutationCache, QueryCache, QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
@@ -6,8 +6,11 @@ import { BrowserRouter } from "react-router";
import { App } from "./app";
import { ErrorBoundary } from "./components/ErrorBoundary";
import { createConsoleLogger } from "./utils/logger";
import "./styles.css";
const logger = createConsoleLogger();
const queryClient = new QueryClient({
defaultOptions: {
queries: {
@@ -16,6 +19,19 @@ const queryClient = new QueryClient({
staleTime: 5000,
},
},
mutationCache: new MutationCache({
onError: (error: Error, _variables, _context, mutation) => {
logger.error("mutation failed", {
error: error.message,
mutationKey: mutation.options.mutationKey,
});
},
}),
queryCache: new QueryCache({
onError: (error: Error, query) => {
logger.error("query failed", { error: error.message, queryKey: query.queryKey });
},
}),
});
const rootElement = document.getElementById("root");
@@ -36,3 +52,18 @@ createRoot(rootElement).render(
</ErrorBoundary>
</StrictMode>,
);
window.onerror = (message, source, lineno, colno, error) => {
logger.error("未处理的异常", {
colno,
error: error instanceof Error ? error.message : String(error),
lineno,
message,
source,
});
};
window.addEventListener("unhandledrejection", (event: PromiseRejectionEvent) => {
const msg = event.reason instanceof Error ? event.reason.message : String(event.reason);
logger.error("unhandled rejection", { reason: msg });
});

View File

@@ -1,15 +1,49 @@
import { createConsoleLogger } from "./logger";
const logger = createConsoleLogger();
export async function handleResponse<T>(response: Response, extract: (data: unknown) => T): Promise<T> {
const start = performance.now();
if (!response.ok) {
const body = (await response.json().catch(() => null)) as null | { error?: string };
throw new Error(body?.error ?? `HTTP ${response.status}`);
const errorBody = body?.error ?? `HTTP ${response.status}`;
logger.warn("API request failed", {
duration: Math.round(performance.now() - start),
errorBody,
status: response.status,
url: response.url,
});
throw new Error(errorBody);
}
const data: unknown = await response.json();
if (import.meta.env["DEV"]) {
logger.debug("API request", {
duration: Math.round(performance.now() - start),
status: response.status,
url: response.url,
});
}
return extract(data);
}
export async function handleVoidResponse(response: Response): Promise<void> {
const start = performance.now();
if (!response.ok) {
const body = (await response.json().catch(() => null)) as null | { error?: string };
throw new Error(body?.error ?? `HTTP ${response.status}`);
const errorBody = body?.error ?? `HTTP ${response.status}`;
logger.warn("API request failed", {
duration: Math.round(performance.now() - start),
errorBody,
status: response.status,
url: response.url,
});
throw new Error(errorBody);
}
if (import.meta.env["DEV"]) {
logger.debug("API request", {
duration: Math.round(performance.now() - start),
status: response.status,
url: response.url,
});
}
}