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

@@ -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,
});
}
}