diff --git a/src/app/api/payment/create-checkout-session/route.ts b/src/app/api/payment/create-checkout-session/route.ts index 63f4aa1d..131199a8 100644 --- a/src/app/api/payment/create-checkout-session/route.ts +++ b/src/app/api/payment/create-checkout-session/route.ts @@ -19,6 +19,9 @@ import { SUBSCRIPTION_PLANS } from "@/data/constants/subscription-plans"; import type { SubscriptionPlanId } from "@/data/constants/subscription-plans"; import { getPlanPriceId } from "@/lib/stripe/stripe-products"; import { getStripeClient } from "@/lib/stripe/stripe-client"; +import { Logger } from "@/utils"; + +const log = new Logger("AppApiPaymentCreateCheckoutSessionRoute"); interface CreateCheckoutRequestBody { planId: SubscriptionPlanId; @@ -86,7 +89,7 @@ export async function POST(req: NextRequest) { return NextResponse.json({ url: session.url }); } catch (e) { const err = e as { message?: string; type?: string; code?: string }; - console.error("[api/create-checkout-session] Stripe error", { + log.error("[api/create-checkout-session] Stripe error", { message: err.message, type: err.type, code: err.code, diff --git a/src/app/api/payment/customer-portal/route.ts b/src/app/api/payment/customer-portal/route.ts index aa9e17e6..76925b28 100644 --- a/src/app/api/payment/customer-portal/route.ts +++ b/src/app/api/payment/customer-portal/route.ts @@ -14,6 +14,9 @@ import { NextRequest, NextResponse } from "next/server"; import { getStripeClient } from "@/lib/stripe/stripe-client"; +import { Logger } from "@/utils"; + +const log = new Logger("AppApiPaymentCustomerPortalRoute"); interface CreatePortalRequestBody { customerId: string | null; @@ -56,7 +59,7 @@ export async function POST(req: NextRequest) { return NextResponse.json({ url: session.url }); } catch (e) { const err = e as { message?: string; type?: string; code?: string }; - console.error("[api/customer-portal] Stripe error", { + log.error("[api/customer-portal] Stripe error", { customerId: body.customerId, message: err.message, type: err.type, diff --git a/src/app/api/payment/webhook/route.ts b/src/app/api/payment/webhook/route.ts index 8d4abf94..861da91d 100644 --- a/src/app/api/payment/webhook/route.ts +++ b/src/app/api/payment/webhook/route.ts @@ -27,11 +27,14 @@ import type Stripe from "stripe"; import { dispatchStripeEvent } from "@/lib/stripe/stripe-events"; import { getStripeClient } from "@/lib/stripe/stripe-client"; +import { Logger } from "@/utils"; + +const log = new Logger("AppApiPaymentWebhookRoute"); export async function POST(req: NextRequest) { const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET; if (!webhookSecret) { - console.error("[api/webhook] STRIPE_WEBHOOK_SECRET is not set"); + log.error("[api/webhook] STRIPE_WEBHOOK_SECRET is not set"); return NextResponse.json( { error: "Webhook secret not configured" }, { status: 500 }, @@ -42,7 +45,7 @@ export async function POST(req: NextRequest) { const rawBody = await req.text(); const sigHeader = req.headers.get("stripe-signature"); if (!sigHeader) { - console.error("[api/webhook] Missing stripe-signature header"); + log.error("[api/webhook] Missing stripe-signature header"); return NextResponse.json( { error: "Missing stripe-signature header" }, { status: 400 }, @@ -56,7 +59,7 @@ export async function POST(req: NextRequest) { event = stripe.webhooks.constructEvent(rawBody, sigHeader, webhookSecret); } catch (e) { const err = e as Error; - console.error("[api/webhook] Signature verification failed", { + log.error("[api/webhook] Signature verification failed", { message: err.message, }); return NextResponse.json( @@ -66,7 +69,7 @@ export async function POST(req: NextRequest) { } // 分发到 handler - console.log("[api/webhook] Received event", { + log.debug("[api/webhook] Received event", { eventId: event.id, type: event.type, created: new Date(event.created * 1000).toISOString(), @@ -75,11 +78,11 @@ export async function POST(req: NextRequest) { try { const handled = await dispatchStripeEvent(event); if (!handled) { - console.log(`[api/webhook] Ignored event type: ${event.type}`); + log.debug(`[api/webhook] Ignored event type: ${event.type}`); } } catch (e) { const err = e as Error; - console.error("[api/webhook] Handler error", { + log.error("[api/webhook] Handler error", { eventId: event.id, type: event.type, message: err.message, diff --git a/src/app/auth/auth-screen.tsx b/src/app/auth/auth-screen.tsx index a86a8d4b..a16ca88c 100644 --- a/src/app/auth/auth-screen.tsx +++ b/src/app/auth/auth-screen.tsx @@ -13,6 +13,9 @@ import { ROUTES } from "@/router/routes"; import { AuthBackground, AuthPanel } from "./components"; import styles from "./components/auth-screen.module.css"; +import { Logger } from "@/utils"; + +const log = new Logger("AppAuthAuthScreen"); export function AuthScreen() { const state = useAuthState(); @@ -23,7 +26,7 @@ export function AuthScreen() { // 用 pendingRedirect flag(不是 useRef transition detection)—— // re-mount 时 flag 在 auth context 里持久,区分"刚按了"和"re-visit"准确无误。 useEffect(() => { - console.log("[auth-screen] useEffect (loginStatus + pendingRedirect)", { + log.debug("[auth-screen] useEffect (loginStatus + pendingRedirect)", { pending: state.pendingRedirect, loginStatus: state.loginStatus, willRedirect: @@ -31,7 +34,7 @@ export function AuthScreen() { }); if (state.pendingRedirect && state.loginStatus !== "notLoggedIn") { - console.log( + log.debug( "[auth-screen] useEffect → router.replace(/chat) [via pendingRedirect flag]", ); authDispatch({ type: "AuthClearPendingRedirect" }); diff --git a/src/app/auth/components/email-login-form.tsx b/src/app/auth/components/email-login-form.tsx index 094e58f5..ae65e8e0 100644 --- a/src/app/auth/components/email-login-form.tsx +++ b/src/app/auth/components/email-login-form.tsx @@ -14,6 +14,9 @@ import { AuthTextField } from "./auth-text-field"; import { AuthPrimaryButton } from "./auth-primary-button"; import { AuthErrorMessage } from "./auth-error-message"; import styles from "./email-form.module.css"; +import { Logger } from "@/utils"; + +const log = new Logger("AppAuthComponentsEmailLoginForm"); export interface EmailLoginFormProps { globalError?: string | null; @@ -44,7 +47,7 @@ export function EmailLoginForm({ }; const submit = () => { - console.log("[email-login-form] submit ENTRY", { + log.debug("[email-login-form] submit ENTRY", { emailLength: email.length, emailPreview: email.slice(0, 8), passwordLength: password.length, @@ -53,7 +56,7 @@ export function EmailLoginForm({ const passwordErr = validatePassword(password); if (emailErr || passwordErr) { const firstErr = emailErr ?? passwordErr ?? null; - console.warn( + log.warn( "[email-login-form] submit VALIDATION FAILED (silent return)", { emailErr, passwordErr, surfaced: firstErr }, ); @@ -61,7 +64,7 @@ export function EmailLoginForm({ return; } setValidationError(null); - console.log("[email-login-form] submit dispatching AuthEmailLoginSubmitted"); + log.debug("[email-login-form] submit dispatching AuthEmailLoginSubmitted"); dispatch({ type: "AuthEmailLoginSubmitted", email, password }); }; diff --git a/src/app/auth/components/email-register-form.tsx b/src/app/auth/components/email-register-form.tsx index 9a7c8c04..7be6f5dc 100644 --- a/src/app/auth/components/email-register-form.tsx +++ b/src/app/auth/components/email-register-form.tsx @@ -25,6 +25,9 @@ import { AuthTextField } from "./auth-text-field"; import { AuthPrimaryButton } from "./auth-primary-button"; import { AuthErrorMessage } from "./auth-error-message"; import styles from "./email-form.module.css"; +import { Logger } from "@/utils"; + +const log = new Logger("AppAuthComponentsEmailRegisterForm"); export interface EmailRegisterFormProps { globalError?: string | null; @@ -66,7 +69,7 @@ export function EmailRegisterForm({ }; const submit = () => { - console.log("[email-register-form] submit ENTRY", { + log.debug("[email-register-form] submit ENTRY", { emailLength: email.length, emailPreview: email.slice(0, 8), passwordLength: password.length, @@ -81,7 +84,7 @@ export function EmailRegisterForm({ // 按 username → email → password → confirm 顺序挑第一个错(与字段顺序一致) const firstErr = usernameErr ?? emailErr ?? passwordErr ?? confirmErr ?? null; - console.warn( + log.warn( "[email-register-form] submit VALIDATION FAILED (silent return)", { emailErr, @@ -95,7 +98,7 @@ export function EmailRegisterForm({ return; } setValidationError(null); - console.log( + log.debug( "[email-register-form] submit dispatching AuthEmailRegisterSubmitted", ); dispatch({ diff --git a/src/app/chat/components/chat-input-bar.tsx b/src/app/chat/components/chat-input-bar.tsx index 40bdb923..eb90ede9 100644 --- a/src/app/chat/components/chat-input-bar.tsx +++ b/src/app/chat/components/chat-input-bar.tsx @@ -27,6 +27,9 @@ import { useChatDispatch } from "@/stores/chat/chat-context"; import { ChatInputTextField } from "./chat-input-text-field"; import { ChatSendButton } from "./chat-send-button"; import styles from "./chat-input-bar.module.css"; +import { Logger } from "@/utils"; + +const log = new Logger("AppChatComponentsChatInputBar"); export interface ChatInputBarProps { disabled?: boolean; @@ -42,7 +45,7 @@ export function ChatInputBar({ disabled = false }: ChatInputBarProps) { const handleSend = () => { if (!hasContent) return; - console.log("[chat-input-bar] handleSend", { + log.debug("[chat-input-bar] handleSend", { contentLength: input.length, contentPreview: input.slice(0, 50), hasContent, diff --git a/src/app/chat/deviceid/[deviceId]/DeepLinkPersist.tsx b/src/app/chat/deviceid/[deviceId]/DeepLinkPersist.tsx index 04ca1d7b..97a074c4 100644 --- a/src/app/chat/deviceid/[deviceId]/DeepLinkPersist.tsx +++ b/src/app/chat/deviceid/[deviceId]/DeepLinkPersist.tsx @@ -16,6 +16,9 @@ import { useRouter } from "next/navigation"; import { AuthStorage } from "@/data/storage/auth/auth_storage"; import { UserStorage } from "@/data/storage/user/user_storage"; import { ROUTES } from "@/router/routes"; +import { Logger } from "@/utils"; + +const log = new Logger("AppChatDeviceidDeviceIdDeepLinkPersist"); interface DeepLinkPersistProps { deviceId: string; @@ -43,7 +46,7 @@ export default function DeepLinkPersist({ } } catch (err) { // 写不进 localStorage 时(例如隐私模式)也不阻塞跳转;记录到 console 即可。 - console.warn("[DeepLinkPersist] failed to persist", err); + log.warn("[DeepLinkPersist] failed to persist", err); } finally { setDone(true); router.replace(ROUTES.chat); diff --git a/src/app/error.tsx b/src/app/error.tsx index 5b2ead9c..463eb661 100644 --- a/src/app/error.tsx +++ b/src/app/error.tsx @@ -14,6 +14,9 @@ import { useEffect } from "react"; import Link from "next/link"; import { ROUTES } from "@/router/routes"; +import { Logger } from "@/utils"; + +const log = new Logger("AppError"); interface ErrorProps { error: Error & { digest?: string }; @@ -23,7 +26,7 @@ interface ErrorProps { export default function GlobalError({ error, unstable_retry }: ErrorProps) { useEffect(() => { // 把错误送到 console;后续接 Sentry/Datadog 时改这里。 - console.error("[app/error.tsx]", error); + log.error("[app/error.tsx]", error); }, [error]); return ( diff --git a/src/app/splash/splash-screen.tsx b/src/app/splash/splash-screen.tsx index b5496703..34ff7f03 100644 --- a/src/app/splash/splash-screen.tsx +++ b/src/app/splash/splash-screen.tsx @@ -7,7 +7,7 @@ import { MobileShell } from "@/app/_components/core"; import { useAuthDispatch, useAuthState } from "@/stores/auth/auth-context"; import { AuthStorage } from "@/data/storage/auth/auth_storage"; import { ROUTES } from "@/router/routes"; -import { pwaUtil } from "@/utils"; +import { pwaUtil, Logger } from "@/utils"; import { SplashBackground, @@ -17,6 +17,8 @@ import { } from "./components"; import styles from "./components/splash-screen.module.css"; +const log = new Logger("AppSplashSplashScreen"); + // 尽量早地缓存 beforeinstallprompt,避免用户停留 splash 时浏览器事件已触发并丢失。 if (typeof window !== "undefined") { pwaUtil.prepareInstallPrompt(); @@ -67,7 +69,7 @@ export function SplashScreen() { // ───────────────────────────────────────────────────────────── useEffect(() => { // 调试日志(保留,不删 —— 用于排查"re-mount 误跳 /chat"问题) - console.log("[splash] useEffect ② (loginStatus + pendingRedirect)", { + log.debug("[splash] useEffect ② (loginStatus + pendingRedirect)", { pending: state.pendingRedirect, loginStatus: state.loginStatus, willRedirect: @@ -75,7 +77,7 @@ export function SplashScreen() { }); if (state.pendingRedirect && state.loginStatus !== "notLoggedIn") { - console.log( + log.debug( "[splash] useEffect ② → router.replace(/chat) [via pendingRedirect flag]", ); authDispatch({ type: "AuthClearPendingRedirect" }); diff --git a/src/app/subscription/components/subscription-checkout-button.tsx b/src/app/subscription/components/subscription-checkout-button.tsx index 52d7e14e..07370f90 100644 --- a/src/app/subscription/components/subscription-checkout-button.tsx +++ b/src/app/subscription/components/subscription-checkout-button.tsx @@ -18,6 +18,9 @@ import { ROUTES } from "@/router/routes"; import { SubscriptionCtaButton } from "./subscription-cta-button"; import styles from "./subscription-cta-button.module.css"; +import { Logger } from "@/utils"; + +const log = new Logger("AppSubscriptionComponentsSubscriptionCheckoutButton"); export interface SubscriptionCheckoutButtonProps { planId: SubscriptionPlanId; @@ -64,7 +67,7 @@ export function SubscriptionCheckoutButton({ window.location.href = url; } catch (e) { const err = e as Error; - console.error("[subscription-checkout-button] error", err); + log.error("[subscription-checkout-button] error", err); setError(err.message); setIsLoading(false); } diff --git a/src/app/subscription/components/subscription-manage-button.tsx b/src/app/subscription/components/subscription-manage-button.tsx index d092ae3e..8482fb44 100644 --- a/src/app/subscription/components/subscription-manage-button.tsx +++ b/src/app/subscription/components/subscription-manage-button.tsx @@ -15,6 +15,9 @@ import { ROUTES } from "@/router/routes"; import { SubscriptionCtaButton } from "./subscription-cta-button"; import styles from "./subscription-cta-button.module.css"; +import { Logger } from "@/utils"; + +const log = new Logger("AppSubscriptionComponentsSubscriptionManageButton"); export interface SubscriptionManageButtonProps { /** 从 User.stripeCustomerId 取(webhook 设置) */ @@ -63,7 +66,7 @@ export function SubscriptionManageButton({ customerId }: SubscriptionManageButto window.location.href = url; } catch (e) { const err = e as Error; - console.error("[subscription-manage-button] error", err); + log.error("[subscription-manage-button] error", err); setError(err.message); setIsLoading(false); } diff --git a/src/core/net/interceptor/logging_interceptor.ts b/src/core/net/interceptor/logging_interceptor.ts index bbbf0c52..2064fd63 100644 --- a/src/core/net/interceptor/logging_interceptor.ts +++ b/src/core/net/interceptor/logging_interceptor.ts @@ -6,11 +6,9 @@ */ import type { FetchHook } from "ofetch"; -const TAG = "[API]"; +import { AppEnvUtil, Logger } from "@/utils"; -function isDev(): boolean { - return process.env.NODE_ENV !== "production"; -} +const log = new Logger("ApiLoggingInterceptor"); function getRequestUrl(request: Request | string): string { return typeof request === "string" ? request : request.url; @@ -23,54 +21,32 @@ function getRequestMethod( return request instanceof Request ? request.method : options.method ?? "GET"; } -function formatBody(body: unknown): string { - if (body === undefined || body === null) return ""; - try { - if (body instanceof FormData) { - const entries: string[] = []; - body.forEach((value, key) => { - if (value instanceof File) { - entries.push(`${key}=File(${value.name})`); - } else { - entries.push(`${key}=${String(value)}`); - } - }); - return `FormData{${entries.join(", ")}}`; - } - return JSON.stringify(body); - } catch { - return String(body); - } -} - export const onRequestLogging: FetchHook = async (ctx) => { - if (!isDev()) return; + if (AppEnvUtil.isProduction()) return; const method = getRequestMethod(ctx.request, ctx.options); const url = getRequestUrl(ctx.request); - console.log( - `${TAG} → ${method} ${url}`, - ctx.options.body !== undefined ? formatBody(ctx.options.body) : "" - ); + const body = Logger.formatValue(ctx.options.body); + log.debug(`→ ${method} ${url}${body ? `\nrequest body:\n${body}` : ""}`); }; export const onResponseLogging: FetchHook = async (ctx) => { - if (!isDev() || !ctx.response) return; + if (AppEnvUtil.isProduction() || !ctx.response) return; const method = getRequestMethod(ctx.request, ctx.options); const url = getRequestUrl(ctx.request); - console.log( - `${TAG} ← ${ctx.response.status} ${method} ${url}`, - formatBody(ctx.response._data) + const body = Logger.formatValue(ctx.response._data); + log.debug( + `← ${ctx.response.status} ${method} ${url}${body ? `\nresponse body:\n${body}` : ""}`, ); }; export const onErrorLogging: FetchHook = async (ctx) => { - if (!isDev()) return; + if (AppEnvUtil.isProduction()) return; const status = ctx.response?.status ?? "N/A"; const method = getRequestMethod(ctx.request, ctx.options); const url = getRequestUrl(ctx.request); - console.error( - `${TAG} ✕ ${status} ${method} ${url}`, - ctx.error?.message ?? "Unknown error" + const body = Logger.formatValue(ctx.response?._data); + log.error( + `✕ ${status} ${method} ${url}\nerror: ${ctx.error?.message ?? "Unknown error"}${body ? `\nresponse body:\n${body}` : ""}`, ); }; diff --git a/src/core/net/interceptor/token_interceptor.ts b/src/core/net/interceptor/token_interceptor.ts index 95f089a4..2cbca09c 100644 --- a/src/core/net/interceptor/token_interceptor.ts +++ b/src/core/net/interceptor/token_interceptor.ts @@ -6,9 +6,12 @@ * 原始 Dart: lib/core/net/interceptor/token_interceptor.dart + token_paths.dart */ import type { FetchHook } from "ofetch"; +import { Logger } from "@/utils"; import { ApiPath } from "../../../data/services/api/api_path"; import { AuthStorage } from "../../../data/storage/auth/auth_storage"; +const log = new Logger("TokenInterceptor"); + /** * 不需要 token 的路径 */ @@ -47,41 +50,41 @@ async function getAuthToken(): Promise { const storage = AuthStorage.getInstance(); const loginR = await storage.getLoginToken(); if (loginR.success && loginR.data && loginR.data.length > 0) { - console.log("[token-interceptor] getAuthToken: using LOGIN token", { + log.debug({ length: loginR.data.length, prefix: loginR.data.slice(0, 8), - }); + }, "getAuthToken: using LOGIN token"); return loginR.data; } - console.log("[token-interceptor] getAuthToken: no login token", { + log.debug({ success: loginR.success, hasData: loginR.success ? !!loginR.data : false, - }); + }, "getAuthToken: no login token"); const guestR = await storage.getGuestToken(); if (guestR.success && guestR.data && guestR.data.length > 0) { - console.log( - "[token-interceptor] getAuthToken: using GUEST token (login priority, no login available)", + log.debug( { length: guestR.data.length, prefix: guestR.data.slice(0, 8), }, + "getAuthToken: using GUEST token (login priority, no login available)", ); return guestR.data; } - console.warn("[token-interceptor] getAuthToken: NO token available (will skip Authorization)", { + log.warn({ loginAttempted: true, guestAttempted: true, loginSuccess: loginR.success, guestSuccess: guestR.success, - }); + }, "getAuthToken: NO token available (will skip Authorization)"); return null; } export const onRequestToken: FetchHook = async (ctx) => { const path = getRequestPath(ctx.request); if (!needsToken(path)) { - console.log("[token-interceptor] onRequestToken SKIP (no-token path)", { path }); + log.debug({ path }, "onRequestToken SKIP (no-token path)"); return; } @@ -91,13 +94,13 @@ export const onRequestToken: FetchHook = async (ctx) => { const headers = new Headers(ctx.options.headers); headers.set("Authorization", `Bearer ${token}`); ctx.options.headers = headers; - console.log("[token-interceptor] onRequestToken SET Authorization header (via ctx.options.headers)", { + log.debug({ path, length: token.length, prefix: token.slice(0, 8), - }); + }, "onRequestToken SET Authorization header (via ctx.options.headers)"); } else { - console.warn("[token-interceptor] onRequestToken NO token to set", { path }); + log.warn({ path }, "onRequestToken NO token to set"); } }; diff --git a/src/core/net/message-queue.ts b/src/core/net/message-queue.ts index 34a201d5..f8c929f3 100644 --- a/src/core/net/message-queue.ts +++ b/src/core/net/message-queue.ts @@ -8,8 +8,12 @@ * - 消费者可声明每条消息是否成功消费;不成功则重新入队 * - 队列可在任意时刻清空(dispose) */ +import { Logger } from "@/utils"; + export type MessageConsumer = (content: string) => boolean | Promise; +const log = new Logger("MessageQueue"); + export class MessageQueue { private queue: string[] = []; private timer: ReturnType | null = null; @@ -38,7 +42,7 @@ export class MessageQueue { try { ok = await this.consumer(next); } catch (e) { - console.error("[MessageQueue] consumer error", e); + log.error({ err: e }, "consumer error"); ok = false; } if (!ok) { diff --git a/src/data/repositories/auth_repository.ts b/src/data/repositories/auth_repository.ts index 8fbe7524..409604bb 100644 --- a/src/data/repositories/auth_repository.ts +++ b/src/data/repositories/auth_repository.ts @@ -8,7 +8,7 @@ * - 登录成功后调 `_saveLoginData` 持久化 token + User * - 登出先调 API,再强制清本地态(无论 API 成败) * - `getCurrentUser` 成功后尽力写本地 User 缓存(不因缓存失败影响主流程) - * - 写 storage 失败用 `console.warn` 记录,不让本地缓存失败影响主流程 + * - 写 storage 失败用 `Logger.warn` 记录,不让本地缓存失败影响主流程 * * 原始 Dart: lib/data/repositories/auth_repository_impl.dart */ @@ -28,11 +28,13 @@ import { SendCodeRequest, } from "@/data/dto/auth"; import { User } from "@/data/dto/user"; -import { Result } from "@/utils"; +import { Result, Logger } from "@/utils"; import type { IAuthRepository } from "@/data/repositories/interfaces"; import { AuthStorage, type IAuthStorage } from "@/data/storage/auth"; import { UserStorage, type IUserStorage } from "@/data/storage/user"; +const log = new Logger("DataRepositoriesAuthRepository"); + /** 硬编码平台名,对齐 Dart `PlatformUtil.platformName.toLowerCase()`(Web 平台)。 */ const WEB_PLATFORM = "web"; @@ -106,7 +108,7 @@ export class AuthRepository implements IAuthRepository { await this.api.logout(); } catch (e) { // API 失败也要继续清本地态 - console.warn("[AuthRepository] logout API failed, clearing local anyway", e); + log.warn("[AuthRepository] logout API failed, clearing local anyway", e); } try { await this.storage.clearAuthData(); @@ -123,7 +125,7 @@ export class AuthRepository implements IAuthRepository { * 成功后写 guest token + deviceId + userId。 */ async guestLogin(deviceId: string): Promise> { - console.log("[AuthRepository.guestLogin] API call START", { + log.debug("[AuthRepository.guestLogin] API call START", { deviceIdLength: deviceId.length, deviceIdPrefix: deviceId.slice(0, 8), }); @@ -131,7 +133,7 @@ export class AuthRepository implements IAuthRepository { const response = await this.api.guestLogin( GuestLoginRequest.from({ deviceId }), ); - console.log("[AuthRepository.guestLogin] API call SUCCESS (Zod parsed)", { + log.debug("[AuthRepository.guestLogin] API call SUCCESS (Zod parsed)", { hasToken: !!response.token, hasUser: !!response.user, userLastMessageAt: response.user?.lastMessageAt, @@ -143,7 +145,7 @@ export class AuthRepository implements IAuthRepository { } return response; }).catch((e) => { - console.error("[AuthRepository.guestLogin] API call FAILED", { + log.error("[AuthRepository.guestLogin] API call FAILED", { errorName: e?.name, errorMessage: e?.message, }); @@ -238,14 +240,14 @@ export class AuthRepository implements IAuthRepository { /** * 获取当前登录用户。成功后尽力写本地 User 缓存(offline hydrate), - * 缓存失败用 `console.warn` 记录,不影响主流程。 + * 缓存失败用 `Logger.warn` 记录,不影响主流程。 */ async getCurrentUser(): Promise> { return Result.wrap(async () => { const user = await this.api.getCurrentUser(); const writeResult = await this.userStorage.setUser(user.toJson()); if (!writeResult.success) { - console.warn( + log.warn( "[AuthRepository] failed to cache current user", writeResult.error, ); @@ -272,27 +274,27 @@ export class AuthRepository implements IAuthRepository { /** * 持久化登录态:login token → refresh token → User 对象 → userId。 - * 全部 best-effort,错误用 `console.warn` 记录但不让登录「失败」—— + * 全部 best-effort,错误用 `Logger.warn` 记录但不让登录「失败」—— * 调用方已经从 API 拿到了 LoginResponse,缓存只是离线加速。 */ private async _saveLoginData(data: LoginResponse): Promise { const r1 = await this.storage.setLoginToken(data.token); if (!r1.success) { - console.warn("[AuthRepository] setLoginToken failed", r1.error); + log.warn("[AuthRepository] setLoginToken failed", r1.error); } if (data.refreshToken) { const r2 = await this.storage.setRefreshToken(data.refreshToken); if (!r2.success) { - console.warn("[AuthRepository] setRefreshToken failed", r2.error); + log.warn("[AuthRepository] setRefreshToken failed", r2.error); } } const r3 = await this.userStorage.setUser(data.user.toJson()); if (!r3.success) { - console.warn("[AuthRepository] setUser failed", r3.error); + log.warn("[AuthRepository] setUser failed", r3.error); } const r4 = await this.userStorage.setUserId(data.user.id); if (!r4.success) { - console.warn("[AuthRepository] setUserId failed", r4.error); + log.warn("[AuthRepository] setUserId failed", r4.error); } } } diff --git a/src/data/services/api/auth_api.ts b/src/data/services/api/auth_api.ts index ce618471..82ab4e6b 100644 --- a/src/data/services/api/auth_api.ts +++ b/src/data/services/api/auth_api.ts @@ -23,6 +23,9 @@ import { RegisterRequest } from "@/data/dto/auth/register_request"; import { SendCodeRequest } from "@/data/dto/auth/send_code_request"; import { User } from "@/data/dto/user/user"; import type { UserData } from "@/data/schemas/user/user"; +import { Logger } from "@/utils"; + +const log = new Logger("DataServicesApiAuthApi"); export class AuthApi { /** @@ -123,10 +126,10 @@ export class AuthApi { method: "POST", body: body.toJson(), }); - console.log("[AuthApi.guestLogin] raw envelope from backend", env); + log.debug("[AuthApi.guestLogin] raw envelope from backend", env); const unwrapped = unwrap(env) as Record; const userObj = unwrapped.user as { lastMessageAt?: unknown } | undefined; - console.log("[AuthApi.guestLogin] unwrapped data (pre-parse)", { + log.debug("[AuthApi.guestLogin] unwrapped data (pre-parse)", { hasUser: "user" in unwrapped, userKeys: userObj && typeof userObj === "object" ? Object.keys(userObj) : null, lastMessageAt: userObj?.lastMessageAt, @@ -136,13 +139,13 @@ export class AuthApi { return GuestLoginResponse.fromJson(unwrapped); } catch (e) { if (e instanceof z.ZodError) { - console.error("[AuthApi.guestLogin] ZodError parsing response", { + log.error("[AuthApi.guestLogin] ZodError parsing response", { issueCount: e.issues.length, firstIssue: e.issues[0], allPaths: e.issues.map((i) => i.path), }); } else { - console.error("[AuthApi.guestLogin] unexpected error", e); + log.error("[AuthApi.guestLogin] unexpected error", e); } throw e; } diff --git a/src/lib/stripe/stripe-events.ts b/src/lib/stripe/stripe-events.ts index 5784a291..25d8cad6 100644 --- a/src/lib/stripe/stripe-events.ts +++ b/src/lib/stripe/stripe-events.ts @@ -15,6 +15,9 @@ */ import type Stripe from "stripe"; import { getStripeClient } from "./stripe-client"; +import { Logger } from "@/utils"; + +const log = new Logger("LibStripeStripeEvents"); /** * 4 个事件的主分发器 @@ -65,7 +68,7 @@ async function handleCheckoutCompleted( ? session.subscription : session.subscription?.id; - console.log("[stripe-events] checkout.session.completed", { + log.debug("[stripe-events] checkout.session.completed", { eventId: event.id, customerId, subscriptionId, @@ -99,7 +102,7 @@ async function handleSubscriptionDeleted( ? subscription.customer : subscription.customer.id; - console.log("[stripe-events] customer.subscription.deleted", { + log.debug("[stripe-events] customer.subscription.deleted", { eventId: event.id, customerId, subscriptionId: subscription.id, @@ -130,7 +133,7 @@ async function handleSubscriptionUpdated( ? subscription.customer : subscription.customer.id; - console.log(`[stripe-events] customer.subscription.${event.type}`, { + log.debug(`[stripe-events] customer.subscription.${event.type}`, { eventId: event.id, customerId, subscriptionId: subscription.id, @@ -164,14 +167,14 @@ async function handleInvoicePaymentSucceeded( // 只关心周期续费(不对首次购买重复补充) if (invoice.billing_reason !== "subscription_cycle") { - console.log( + log.debug( `[stripe-events] invoice.payment_succeeded (skipped, billing_reason=${invoice.billing_reason})`, { eventId: event.id, invoiceId: invoice.id }, ); return; } - console.log("[stripe-events] invoice.payment_succeeded (subscription_cycle)", { + log.debug("[stripe-events] invoice.payment_succeeded (subscription_cycle)", { eventId: event.id, invoiceId: invoice.id, customerId, diff --git a/src/stores/auth/auth-actors.ts b/src/stores/auth/auth-actors.ts index fbe3489d..9e6e179f 100644 --- a/src/stores/auth/auth-actors.ts +++ b/src/stores/auth/auth-actors.ts @@ -20,17 +20,17 @@ const authRepo: IAuthRepository = authRepository; export const emailLoginActor = fromPromise( async ({ input }) => { - console.log("[auth-machine] emailLoginActor ENTRY", { + log.debug("[auth-machine] emailLoginActor ENTRY", { emailLength: input.email.length, emailPreview: input.email.slice(0, 8), passwordLength: input.password.length, }); const guestId = await readGuestId(); - console.log("[auth-machine] emailLoginActor calling authRepo.emailLogin", { + log.debug("[auth-machine] emailLoginActor calling authRepo.emailLogin", { hasGuestId: !!guestId, }); const result = await authRepo.emailLogin({ ...input, guestId }); - console.log("[auth-machine] emailLoginActor authRepo.emailLogin DONE", { + log.debug("[auth-machine] emailLoginActor authRepo.emailLogin DONE", { success: result.success, hasData: result.success ? !!result.data : null, errorName: result.success ? null : result.error?.name, @@ -45,7 +45,7 @@ export const emailRegisterThenLoginActor = fromPromise< LoginStatus, { email: string; password: string; username: string; confirmPassword: string } >(async ({ input }) => { - console.log("[auth-machine] emailRegisterThenLoginActor ENTRY", { + log.debug("[auth-machine] emailRegisterThenLoginActor ENTRY", { emailLength: input.email.length, usernameLength: input.username.length, passwordLength: input.password.length, @@ -53,9 +53,9 @@ export const emailRegisterThenLoginActor = fromPromise< }); const guestId = await readGuestId(); - console.log("[auth-machine] emailRegisterThenLoginActor calling authRepo.register"); + log.debug("[auth-machine] emailRegisterThenLoginActor calling authRepo.register"); const registerResult = await authRepo.register({ ...input, guestId }); - console.log("[auth-machine] emailRegisterThenLoginActor authRepo.register DONE", { + log.debug("[auth-machine] emailRegisterThenLoginActor authRepo.register DONE", { success: registerResult.success, errorName: registerResult.success ? null : registerResult.error?.name, errorMessage: registerResult.success ? null : registerResult.error?.message, @@ -63,7 +63,7 @@ export const emailRegisterThenLoginActor = fromPromise< if (Result.isErr(registerResult)) throw registerResult.error; // 注册后自动登录(对齐 Dart 行为) - console.log( + log.debug( "[auth-machine] emailRegisterThenLoginActor calling authRepo.emailLogin (post-register)", ); const loginResult = await authRepo.emailLogin({ @@ -71,7 +71,7 @@ export const emailRegisterThenLoginActor = fromPromise< password: input.password, guestId, }); - console.log( + log.debug( "[auth-machine] emailRegisterThenLoginActor authRepo.emailLogin DONE", { success: loginResult.success, @@ -201,30 +201,30 @@ async function persistFacebookProfile(accessToken: string): Promise { // 受保护 API 401 会由 HTTP 层清掉 token 回到 splash,无需在此处主动校验。 export const guestLoginActor = fromPromise(async () => { - console.log("[auth-machine] guestLoginActor ENTRY"); + log.debug("[auth-machine] guestLoginActor ENTRY"); // 1. 先查本地 guestToken —— 已有就直接进 idle(带 pendingRedirect) const hasTokenR = await AuthStorage.getInstance().hasGuestToken(); - console.log("[auth-machine] guestLoginActor hasGuestToken", { + log.debug("[auth-machine] guestLoginActor hasGuestToken", { success: hasTokenR.success, hasData: hasTokenR.success ? !!hasTokenR.data : null, }); if (hasTokenR.success && hasTokenR.data) { - console.log("[auth-machine] guestLoginActor SHORT-CIRCUIT (has local guestToken) → return guest"); + log.debug("[auth-machine] guestLoginActor SHORT-CIRCUIT (has local guestToken) → return guest"); return "guest" as LoginStatus; } // Result.err(_) 时不抛 —— 落到 API 路径让真正的错误信号出现 // 2. 没有(或读不出)才走完整流程:拿 deviceId → 调后端 const deviceId = await deviceIdentifier.getDeviceId(); - console.log("[auth-machine] guestLoginActor deviceId", { + log.debug("[auth-machine] guestLoginActor deviceId", { length: deviceId.length, prefix: deviceId.slice(0, 8), }); - console.log("[auth-machine] guestLoginActor calling authRepository.guestLogin"); + log.debug("[auth-machine] guestLoginActor calling authRepository.guestLogin"); const result = await authRepository.guestLogin(deviceId); - console.log("[auth-machine] guestLoginActor authRepository.guestLogin DONE", { + log.debug("[auth-machine] guestLoginActor authRepository.guestLogin DONE", { success: result.success, hasData: result.success ? !!result.data : null, errorName: result.success ? null : result.error?.name, diff --git a/src/stores/auth/auth-status-checker.tsx b/src/stores/auth/auth-status-checker.tsx index c6de20b0..256d1119 100644 --- a/src/stores/auth/auth-status-checker.tsx +++ b/src/stores/auth/auth-status-checker.tsx @@ -11,12 +11,15 @@ import { useEffect } from "react"; import { useAuthDispatch } from "./auth-context"; +import { Logger } from "@/utils"; + +const log = new Logger("StoresAuthAuthStatusChecker"); export function AuthStatusChecker() { const dispatch = useAuthDispatch(); useEffect(() => { - console.log("[auth-init] mount → dispatch AuthInit"); + log.debug("[auth-init] mount → dispatch AuthInit"); dispatch({ type: "AuthInit" }); }, [dispatch]); diff --git a/src/stores/chat/chat-machine.actors.ts b/src/stores/chat/chat-machine.actors.ts index 7807edeb..33a9e736 100644 --- a/src/stores/chat/chat-machine.actors.ts +++ b/src/stores/chat/chat-machine.actors.ts @@ -5,7 +5,7 @@ import { fromPromise, fromCallback } from "xstate"; import { createChatWebSocket, ChatWebSocket } from "@/core/net/chat-websocket"; -import { Result } from "@/utils"; +import { Result, Logger } from "@/utils"; import { PAGE_SIZE, @@ -17,6 +17,8 @@ import { } from "./chat-machine.helpers"; import type { ChatEvent } from "./chat-events"; +const log = new Logger("StoresChatChatMachineActors"); + // ============================================================ // 共享 WS 引用(chatWebSocketActor ↔ sendMessageWsActor) // ============================================================ @@ -51,9 +53,9 @@ export const loadQuotaActor = fromPromise<{ remaining: number; total: number; }>(async () => { - console.log("[chat-machine] loadQuotaActor ENTRY"); + log.debug("[chat-machine] loadQuotaActor ENTRY"); const result = await readGuestQuota(); - console.log("[chat-machine] loadQuotaActor DONE", result); + log.debug("[chat-machine] loadQuotaActor DONE", result); return result; }); @@ -74,9 +76,9 @@ export const loadHistoryActor = fromPromise<{ localCount: number; networkCount: number; }>(async () => { - console.log("[chat-machine] loadHistoryActor ENTRY"); + log.debug("[chat-machine] loadHistoryActor ENTRY"); const result = await readAndSyncHistory(); - console.log("[chat-machine] loadHistoryActor DONE", { + log.debug("[chat-machine] loadHistoryActor DONE", { finalCount: result.messages.length, localCount: result.localCount, networkCount: result.networkCount, @@ -99,25 +101,25 @@ export const sendMessageHttpActor = fromPromise< { reply: UiMessage }, { content: string } >(async ({ input, self }) => { - console.log("[chat-machine] sendMessageHttpActor ENTRY", { + log.debug("[chat-machine] sendMessageHttpActor ENTRY", { contentLength: input.content.length, contentPreview: input.content.slice(0, 50), selfId: self.id, selfPath: "", }); - console.log("[chat-machine] sendMessageHttpActor calling chatRepo.sendMessage"); + log.debug("[chat-machine] sendMessageHttpActor calling chatRepo.sendMessage"); const result = await chatRepo.sendMessage(input.content); - console.log("[chat-machine] sendMessageHttpActor chatRepo.sendMessage DONE", { + log.debug("[chat-machine] sendMessageHttpActor chatRepo.sendMessage DONE", { success: result.success, error: result.success ? null : Result.isErr(result) ? result.error : null, }); if (Result.isErr(result)) { - console.error("[chat-machine] sendMessageHttpActor result isErr, throwing"); + log.error("[chat-machine] sendMessageHttpActor result isErr, throwing"); throw result.error; } // 一次发送 = 一次返回 —— 直接转 reply → UiMessage - console.log("[chat-machine] sendMessageHttpActor converting reply to UiMessage", { + log.debug("[chat-machine] sendMessageHttpActor converting reply to UiMessage", { replyLength: result.data.reply.length, replyPreview: result.data.reply.slice(0, 50), messageId: result.data.messageId, @@ -125,7 +127,7 @@ export const sendMessageHttpActor = fromPromise< }); const reply = sendResponseToUiMessage(result.data); - console.log("[chat-machine] sendMessageHttpActor done", { + log.debug("[chat-machine] sendMessageHttpActor done", { replyContentLength: reply.content.length, }); return { reply }; @@ -136,37 +138,37 @@ export const loadMoreHistoryActor = fromPromise< { messages: UiMessage[]; hasMore: boolean; newOffset: number }, { offset: number } >(async ({ input, self }) => { - console.log("[chat-machine] loadMoreHistoryActor ENTRY", { + log.debug("[chat-machine] loadMoreHistoryActor ENTRY", { inputOffset: input.offset, pageSize: PAGE_SIZE, selfPath: "", }); - console.log("[chat-machine] loadMoreHistoryActor calling chatRepo.getHistory", { + log.debug("[chat-machine] loadMoreHistoryActor calling chatRepo.getHistory", { pageSize: PAGE_SIZE, offset: input.offset, }); const result = await chatRepo.getHistory(PAGE_SIZE, input.offset); - console.log("[chat-machine] loadMoreHistoryActor chatRepo.getHistory DONE", { + log.debug("[chat-machine] loadMoreHistoryActor chatRepo.getHistory DONE", { success: result.success, error: result.success ? null : Result.isErr(result) ? result.error : null, responseType: result.success ? typeof result.data : "err", }); if (Result.isErr(result)) { - console.error("[chat-machine] loadMoreHistoryActor result isErr, throwing", { + log.error("[chat-machine] loadMoreHistoryActor result isErr, throwing", { error: result.error, }); throw result.error; } - console.log("[chat-machine] loadMoreHistoryActor result data", { + log.debug("[chat-machine] loadMoreHistoryActor result data", { rawMessagesCount: result.data.messages.length, }); const page = localMessagesToUi(result.data.messages); - console.log("[chat-machine] loadMoreHistoryActor AFTER UiMessage mapping", { + log.debug("[chat-machine] loadMoreHistoryActor AFTER UiMessage mapping", { uiMessagesCount: page.length, }); const hasMore = page.length >= PAGE_SIZE; const newOffset = input.offset + page.length; - console.log("[chat-machine] loadMoreHistoryActor result", { + log.debug("[chat-machine] loadMoreHistoryActor result", { pageSize: page.length, hasMore, newOffset, @@ -194,33 +196,33 @@ export const loadMoreHistoryActor = fromPromise< */ export const chatWebSocketActor = fromCallback( ({ sendBack, input }) => { - console.log("[chat-machine] chatWebSocketActor ENTRY", { + log.debug("[chat-machine] chatWebSocketActor ENTRY", { hasToken: !!input.token, tokenLength: input.token?.length ?? 0, tokenPrefix: input.token?.slice(0, 10) ?? "EMPTY", selfPath: "", }); - console.log("[chat-machine] chatWebSocketActor creating ChatWebSocket instance"); + log.debug("[chat-machine] chatWebSocketActor creating ChatWebSocket instance"); const ws = createChatWebSocket(input.token); - console.log("[chat-machine] chatWebSocketActor createChatWebSocket DONE", { + log.debug("[chat-machine] chatWebSocketActor createChatWebSocket DONE", { wsType: ws.constructor.name, }); // 写入 module-level 共享引用 —— sendMessageWsActor 借此调 ws.sendMessage() activeChatWebSocket = ws; - console.log("[chat-machine] chatWebSocketActor activeChatWebSocket SET", { + log.debug("[chat-machine] chatWebSocketActor activeChatWebSocket SET", { isConnected: ws.isConnected, }); ws.onConnected = (userId) => { - console.log("[chat-machine] WS onConnected", { + log.debug("[chat-machine] WS onConnected", { userId, userIdPrefix: userId?.slice(0, 8) ?? "EMPTY", }); sendBack({ type: "ChatWebSocketConnected", userId }); }; ws.onSentence = (p) => { - console.log("[chat-machine] WS onSentence", { + log.debug("[chat-machine] WS onSentence", { index: p.index, total: p.total, done: p.done, @@ -237,23 +239,23 @@ export const chatWebSocketActor = fromCallback( }); }; ws.onError = (msg) => { - console.error("[chat-machine] WS onError", { + log.error("[chat-machine] WS onError", { errorMessage: msg, errorLength: msg?.length ?? 0, }); sendBack({ type: "ChatWebSocketError", errorMessage: msg }); }; - console.log("[chat-machine] chatWebSocketActor calling ws.connect()"); + log.debug("[chat-machine] chatWebSocketActor calling ws.connect()"); ws.connect(); - console.log("[chat-machine] chatWebSocketActor ws.connect() called"); + log.debug("[chat-machine] chatWebSocketActor ws.connect() called"); return () => { - console.log( + log.debug( "[chat-machine] chatWebSocketActor CLEANUP (parent state exit / logout / cross-transition / unmount)", ); // 清空 module-level 引用 —— 严格用 `=== ws` 判等,避免覆盖更新的实例 if (activeChatWebSocket === ws) { activeChatWebSocket = null; - console.log("[chat-machine] chatWebSocketActor activeChatWebSocket CLEARED"); + log.debug("[chat-machine] chatWebSocketActor activeChatWebSocket CLEARED"); } ws.disconnect(); }; @@ -281,26 +283,26 @@ export const chatWebSocketActor = fromCallback( */ export const sendMessageWsActor = fromPromise( async ({ input }) => { - console.log("[chat-machine] sendMessageWsActor ENTRY", { + log.debug("[chat-machine] sendMessageWsActor ENTRY", { contentLength: input.content.length, contentPreview: input.content.slice(0, 50), }); const ws = getActiveChatWebSocket(); if (!ws) { - console.error("[chat-machine] sendMessageWsActor no activeChatWebSocket"); + log.error("[chat-machine] sendMessageWsActor no activeChatWebSocket"); throw new Error("[chat-machine] sendMessageWs: no active WebSocket (actor not running)"); } - console.log("[chat-machine] sendMessageWsActor calling ws.sendMessage", { + log.debug("[chat-machine] sendMessageWsActor calling ws.sendMessage", { isConnected: ws.isConnected, }); const ok = ws.sendMessage(input.content); if (!ok) { - console.error( + log.error( "[chat-machine] sendMessageWsActor ws.sendMessage returned false (not OPEN)", ); throw new Error("[chat-machine] sendMessageWs: WebSocket not OPEN"); } - console.log( + log.debug( "[chat-machine] sendMessageWsActor ws.sendMessage OK, awaiting AI sentence stream", ); // resolve 不返值 —— AI reply 由 chatWebSocketActor 的 ChatAISentenceReceived 流回 diff --git a/src/stores/chat/chat-machine.helpers.ts b/src/stores/chat/chat-machine.helpers.ts index 49429090..a3c0dbdd 100644 --- a/src/stores/chat/chat-machine.helpers.ts +++ b/src/stores/chat/chat-machine.helpers.ts @@ -25,7 +25,9 @@ import { chatRepository } from "@/data/repositories/chat_repository"; import type { IChatRepository } from "@/data/repositories/interfaces"; import { ChatStorage } from "@/data/storage/chat/chat_storage"; import { ChatSendResponse } from "@/data/dto/chat/chat_send_response"; -import { formatDate, Result } from "@/utils"; +import { formatDate, Result, Logger } from "@/utils"; + +const log = new Logger("StoresChatChatMachineHelpers"); // ============================================================ // Constants @@ -161,7 +163,7 @@ export async function readAndSyncHistory(): Promise<{ Result.isOk(localResult) && localResult.data ? localMessagesToUi(localResult.data) : []; - console.log("[chat-machine] loadHistory LOCAL DONE", { + log.debug("[chat-machine] loadHistory LOCAL DONE", { count: localMessages.length, }); @@ -169,7 +171,7 @@ export async function readAndSyncHistory(): Promise<{ const networkResult = await chatRepo.getHistory(PAGE_SIZE, 0); if (!Result.isOk(networkResult)) { // network 失败 —— 返空 messages(不让 UI 卡住) - console.error( + log.error( "[chat-machine] loadHistory NETWORK FAILED", networkResult.success ? null : (networkResult as { error: unknown }).error, ); @@ -178,7 +180,7 @@ export async function readAndSyncHistory(): Promise<{ const fallbackMessages = localMessages.length === 0 ? [greetingMessage] : localMessages; if (fallbackMessages[0] === greetingMessage) { - console.log( + log.debug( "[chat-machine] loadHistory EMPTY → prepend greeting (network-failed path)", ); } @@ -192,14 +194,14 @@ export async function readAndSyncHistory(): Promise<{ }; } const networkUi = localMessagesToUi(networkResult.data.messages); - console.log("[chat-machine] loadHistory NETWORK DONE", { + log.debug("[chat-machine] loadHistory NETWORK DONE", { count: networkUi.length, }); // 3. 用 network 覆盖 local const saveResult = await chatRepo.saveMessagesToLocal(networkResult.data.messages); const localOverwritten = Result.isOk(saveResult); - console.log("[chat-machine] loadHistory SAVE TO LOCAL DONE", { + log.debug("[chat-machine] loadHistory SAVE TO LOCAL DONE", { localOverwritten, }); @@ -207,7 +209,7 @@ export async function readAndSyncHistory(): Promise<{ const finalMessages = networkUi.length === 0 ? [greetingMessage] : networkUi; if (finalMessages[0] === greetingMessage) { - console.log("[chat-machine] loadHistory EMPTY → prepend greeting"); + log.debug("[chat-machine] loadHistory EMPTY → prepend greeting"); } return { diff --git a/src/stores/chat/chat-machine.ts b/src/stores/chat/chat-machine.ts index 063db9b0..2e6dfc53 100644 --- a/src/stores/chat/chat-machine.ts +++ b/src/stores/chat/chat-machine.ts @@ -33,7 +33,7 @@ import { setup, assign } from "xstate"; import { ChatStorage } from "@/data/storage/chat/chat_storage"; -import { formatDate, todayString } from "@/utils"; +import { formatDate, todayString, Logger } from "@/utils"; import { ChatState, initialState } from "./chat-state"; @@ -47,6 +47,8 @@ import { chatWebSocketActor, } from "./chat-machine.actors"; +const log = new Logger("StoresChatChatMachine"); + /** 游客配额阈值(与 Dart `GuestChatQuota` 保持一致) */ export const GuestChatQuota = { warningThreshold: 5, @@ -93,7 +95,7 @@ export const chatMachine = setup({ ChatStorage.getInstance().setGuestTotalQuota(newTotal); - console.log("[chat-machine] appendUserMessage", { + log.debug("[chat-machine] appendUserMessage", { contentLength: event.content.length, contentPreview: event.content.slice(0, 50), quotaMode: context.wsConnected ? "ws (no decrement)" : "http (decrement)", @@ -134,7 +136,7 @@ export const chatMachine = setup({ void ChatStorage.getInstance().setGuestTotalQuota(newTotal); - console.log("[chat-machine] appendUserImage", { + log.debug("[chat-machine] appendUserImage", { oldMessagesCount: context.messages.length, wsConnected: context.wsConnected, quotaMode: context.wsConnected ? "ws (no decrement)" : "http (decrement)", @@ -177,7 +179,7 @@ export const chatMachine = setup({ }; } } - console.log("[chat-machine] appendOrUpdateAISentence", { + log.debug("[chat-machine] appendOrUpdateAISentence", { index: event.index, total: event.total, done: event.done, diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 3cd858f0..d8f144a2 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -2,6 +2,9 @@ import pino, { type Logger as PinoLogger, type LoggerOptions } from "pino"; import { AppEnvUtil } from "./app-env"; +type LogArgs = unknown[]; +type PinoLogArgs = Parameters; + /** * 全局 pino root logger(单例,进程级复用)。 * @@ -45,20 +48,77 @@ export class Logger { this.logger = rootLogger.child({ component }); } - debug(...args: Parameters): void { - this.logger.debug(...args); + debug(...args: LogArgs): void { + this.logger.debug(...Logger.normalizeLogArgs(args)); } - info(...args: Parameters): void { - this.logger.info(...args); + info(...args: LogArgs): void { + this.logger.info(...Logger.normalizeLogArgs(args)); } - warn(...args: Parameters): void { - this.logger.warn(...args); + warn(...args: LogArgs): void { + this.logger.warn(...Logger.normalizeLogArgs(args)); } - error(...args: Parameters): void { - this.logger.error(...args); + error(...args: LogArgs): void { + this.logger.error(...Logger.normalizeLogArgs(args)); } - fatal(...args: Parameters): void { - this.logger.fatal(...args); + fatal(...args: LogArgs): void { + this.logger.fatal(...Logger.normalizeLogArgs(args)); + } + + /** + * 把日志中的任意数据转成人眼友好的多行文本。 + * - object / array: pretty JSON + * - JSON string: parse 后 pretty JSON + * - FormData / File: 使用轻量描述,避免直接输出二进制 + */ + static formatValue(value: unknown): string { + if (value === undefined || value === null) return ""; + + if (typeof FormData !== "undefined" && value instanceof FormData) { + const entries: string[] = []; + value.forEach((entryValue, key) => { + if (typeof File !== "undefined" && entryValue instanceof File) { + entries.push(`${key}=File(${entryValue.name})`); + } else { + entries.push(`${key}=${String(entryValue)}`); + } + }); + return `FormData{\n ${entries.join(",\n ")}\n}`; + } + + if (typeof value === "string") { + const trimmed = value.trim(); + if (trimmed.length === 0) return ""; + try { + return JSON.stringify(JSON.parse(trimmed), null, 2); + } catch { + return value; + } + } + + try { + return JSON.stringify(value, null, 2); + } catch { + return String(value); + } + } + + private static normalizeLogArgs(args: LogArgs): PinoLogArgs { + const [first, second, ...rest] = args; + if (typeof first !== "string" || second === undefined) return args as PinoLogArgs; + + if (second instanceof Error) { + return [{ err: second }, first, ...rest] as PinoLogArgs; + } + + if ( + typeof second === "object" && + second !== null && + !Array.isArray(second) + ) { + return [second, first, ...rest] as PinoLogArgs; + } + + return args as PinoLogArgs; } /** 透传 raw pino logger(高级用法:自定义 binding / child / serializers) */