feat: 全栈 Logger 依赖注入 — DB/Route/AI 层传参 + 前端 Logger + 测试更新 + 归档 add-frontend-logger
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user