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/chat-screen.tsx b/src/app/chat/chat-screen.tsx
index af29ff6e..e010fac4 100644
--- a/src/app/chat/chat-screen.tsx
+++ b/src/app/chat/chat-screen.tsx
@@ -1,28 +1,17 @@
"use client";
/**
* ChatScreen 编排层
- *
- * 子组件职责:
- * - ChatHeader:顶部栏(游客 banner / 菜单按钮)
- * - ChatArea:消息列表
- * - ChatInputBar:输入栏(文字 + 发送 + More)
- * - PwaInstallOverlay:PWA 安装提示触发器
- * - BrowserHintOverlay:浏览器提示
- *
- * 鉴权解耦(事件驱动):
- * - chat 机器不感知鉴权 / 不管 WebSocket
- * - auth → chat 的生命周期同步由根级 负责
- * - ChatScreen 只派生 UI 展示所需的 isGuest
*/
import { useEffect, useRef, useState } from "react";
import Image from "next/image";
import { useAuthState } from "@/stores/auth/auth-context";
import type { LoginStatus } from "@/data/dto/auth";
+import { AppStorage } from "@/data/storage/app/app_storage";
import { AuthStorage } from "@/data/storage/auth/auth_storage";
import { UserStorage } from "@/data/storage/user/user_storage";
import { useChatState } from "@/stores/chat/chat-context";
-import { AppEnvUtil, BrowserDetector, UrlLauncherUtil } from "@/utils";
+import { AppEnvUtil, BrowserDetector, todayString, UrlLauncherUtil } from "@/utils";
import { ROUTE_BUILDERS, ROUTES } from "@/router/routes";
import { MobileShell } from "@/app/_components/core";
@@ -68,16 +57,36 @@ export function ChatScreen() {
useEffect(() => {
if (!authState.hasInitialized || authState.isLoading) return;
- if (authState.loginStatus !== "facebook") return;
- if (externalBrowserPromptShownRef.current) return;
- if (!BrowserDetector.isInAppBrowser()) return;
+ const isDev = AppEnvUtil.isDevelopment();
+ const canTriggerPrompt =
+ isDev ||
+ (authState.loginStatus === "facebook" && BrowserDetector.isInAppBrowser());
+ if (!canTriggerPrompt) return;
- externalBrowserPromptShownRef.current = true;
- const timer = window.setTimeout(() => {
- setShowExternalBrowserDialog(true);
- }, 3000);
+ let mounted = true;
+ let timer: number | undefined;
- return () => window.clearTimeout(timer);
+ const init = async () => {
+ if (externalBrowserPromptShownRef.current) return;
+
+ const today = todayString();
+ const canShowResult = await AppStorage.canShowExternalBrowserDialog(today);
+ if (!mounted || !canShowResult.success || !canShowResult.data) return;
+
+ externalBrowserPromptShownRef.current = true;
+ timer = window.setTimeout(() => {
+ if (!mounted) return;
+ setShowExternalBrowserDialog(true);
+ void AppStorage.recordExternalBrowserDialogShown(today);
+ }, 3000);
+ };
+
+ void init();
+
+ return () => {
+ mounted = false;
+ if (timer !== undefined) window.clearTimeout(timer);
+ };
}, [
authState.hasInitialized,
authState.isLoading,
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/components/external-browser-dialog.module.css b/src/app/chat/components/external-browser-dialog.module.css
index d33f2f7c..49fbb2f5 100644
--- a/src/app/chat/components/external-browser-dialog.module.css
+++ b/src/app/chat/components/external-browser-dialog.module.css
@@ -5,18 +5,17 @@
display: flex;
align-items: center;
justify-content: center;
- padding: var(--spacing-md, 16px);
background: rgba(0, 0, 0, 0.5);
}
.dialog {
- width: 100%;
- max-width: 360px;
- padding: 24px 18px 18px;
+ width: calc(100% - 40px);
+ max-width: var(--pwa-install-dialog-max-width, 360px);
+ padding: 24px 16px 16px;
text-align: center;
background: var(--color-page-background, #ffffff);
- border-radius: 32px;
- box-shadow: 0 20px 48px rgba(0, 0, 0, 0.22);
+ border-radius: 40px;
+ box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
}
.title {
@@ -28,35 +27,40 @@
}
.content {
- margin: 0 12px 22px;
- font-size: var(--font-size-md, 15px);
+ margin: 0 24px var(--spacing-xxl, 24px);
+ font-size: var(--font-size-lg, 16px);
line-height: 1.5;
- color: var(--color-text-secondary, #555555);
+ text-align: left;
+ color: #393939;
white-space: pre-line;
}
.actions {
display: flex;
gap: var(--spacing-md, 12px);
+ width: 100%;
}
.button {
flex: 1 1 auto;
- height: 44px;
+ height: var(--pwa-button-height, 44px);
border: 0;
- border-radius: 999px;
- font-size: var(--font-size-md, 15px);
- font-weight: 700;
+ border-radius: var(--radius-bottom-sheet, 28px);
+ font-size: var(--font-size-lg, 16px);
+ font-weight: 600;
cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
}
.secondary {
- color: var(--color-text-secondary, #555555);
- background: #efefef;
+ color: var(--color-page-background, #ffffff);
+ background: var(--color-text-secondary, #9e9e9e);
}
.primary {
- color: #ffffff;
+ color: var(--color-page-background, #ffffff);
background: linear-gradient(
var(--color-button-gradient-start, #ff67e0),
var(--color-button-gradient-end, #ff52a2)
diff --git a/src/app/chat/components/external-browser-dialog.tsx b/src/app/chat/components/external-browser-dialog.tsx
index 25ff8b17..0d732c84 100644
--- a/src/app/chat/components/external-browser-dialog.tsx
+++ b/src/app/chat/components/external-browser-dialog.tsx
@@ -27,8 +27,7 @@ export function ExternalBrowserDialog({
Open in browser
- Sync your Facebook profile to an external browser{"\n"}
- for the best chat experience.
+ Sync your Facebook profile to an external browser for the best chat experience.
{
// 把错误送到 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..7c5f4330 100644
--- a/src/data/services/api/auth_api.ts
+++ b/src/data/services/api/auth_api.ts
@@ -9,20 +9,25 @@ import { z } from "zod";
import { httpClient } from "./http_client";
import { ApiPath } from "./api_path";
import { ApiEnvelope, unwrap } from "./response_helper";
-import { AppleLoginRequest } from "@/data/dto/auth/apple_login_request";
-import { FacebookLoginRequest } from "@/data/dto/auth/facebook_login_request";
-import { FbIdLoginRequest } from "@/data/dto/auth/fb_id_login_request";
-import { GoogleLoginRequest } from "@/data/dto/auth/google_login_request";
-import { GuestLoginRequest } from "@/data/dto/auth/guest_login_request";
-import { GuestLoginResponse } from "@/data/dto/auth/guest_login_response";
-import { LoginRequest } from "@/data/dto/auth/login_request";
-import { LoginResponse } from "@/data/dto/auth/login_response";
-import { RefreshTokenRequest } from "@/data/dto/auth/refresh_token_request";
-import { RefreshTokenResponse } from "@/data/dto/auth/refresh_token_response";
-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 {
+ AppleLoginRequest,
+ FacebookLoginRequest,
+ FbIdLoginRequest,
+ GoogleLoginRequest,
+ GuestLoginRequest,
+ GuestLoginResponse,
+ LoginRequest,
+ LoginResponse,
+ RefreshTokenRequest,
+ RefreshTokenResponse,
+ RegisterRequest,
+ SendCodeRequest,
+} from "@/data/dto/auth";
+import { User } from "@/data/dto/user";
import type { UserData } from "@/data/schemas/user/user";
+import { Logger } from "@/utils";
+
+const log = new Logger("DataServicesApiAuthApi");
export class AuthApi {
/**
@@ -123,10 +128,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 +141,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/data/services/api/chat_api.ts b/src/data/services/api/chat_api.ts
index 5fe6873d..ae780c88 100644
--- a/src/data/services/api/chat_api.ts
+++ b/src/data/services/api/chat_api.ts
@@ -7,14 +7,16 @@
import { httpClient } from "./http_client";
import { ApiPath } from "./api_path";
import { ApiEnvelope, unwrap } from "./response_helper";
-import { ChatHistoryResponse } from "@/data/dto/chat/chat_history_response";
-import { ChatSendResponse } from "@/data/dto/chat/chat_send_response";
-import { ChatSyncData } from "@/data/dto/chat/chat_sync_data";
-import { ChatSyncRequest } from "@/data/dto/chat/chat_sync_request";
-import { ImageUploadResponse } from "@/data/dto/chat/image_upload_response";
-import { SendMessageRequest } from "@/data/dto/chat/send_message_request";
-import { SttData } from "@/data/dto/chat/stt_data";
-import { SyncMessage } from "@/data/dto/chat/sync_message";
+import {
+ ChatHistoryResponse,
+ ChatSendResponse,
+ ChatSyncData,
+ ChatSyncRequest,
+ ImageUploadResponse,
+ SendMessageRequest,
+ SttData,
+ SyncMessage,
+} from "@/data/dto/chat";
export class ChatApi {
/**
diff --git a/src/data/services/api/metrics_api.ts b/src/data/services/api/metrics_api.ts
index 875b5d06..78ebc402 100644
--- a/src/data/services/api/metrics_api.ts
+++ b/src/data/services/api/metrics_api.ts
@@ -7,8 +7,7 @@
import { httpClient } from "./http_client";
import { ApiPath } from "./api_path";
import { ApiEnvelope, unwrapOptional } from "./response_helper";
-import { AppEvent } from "@/data/dto/metrics/app_event";
-import { PwaEvent } from "@/data/dto/metrics/pwa_event";
+import { AppEvent, PwaEvent } from "@/data/dto/metrics";
export class MetricsApi {
/**
diff --git a/src/data/services/api/user_api.ts b/src/data/services/api/user_api.ts
index e3ec5e45..7f730a77 100644
--- a/src/data/services/api/user_api.ts
+++ b/src/data/services/api/user_api.ts
@@ -7,11 +7,13 @@
import { httpClient } from "./http_client";
import { ApiPath } from "./api_path";
import { ApiEnvelope, unwrap } from "./response_helper";
-import { CreditsData } from "@/data/dto/user/credits_data";
-import { CreditsHistoryData } from "@/data/dto/user/credits_history_data";
-import { UpdateProfileRequest } from "@/data/dto/user/update_profile_request";
-import { User } from "@/data/dto/user/user";
-import { UserStatsResponse } from "@/data/dto/user/user_stats_response";
+import {
+ CreditsData,
+ CreditsHistoryData,
+ UpdateProfileRequest,
+ User,
+ UserStatsResponse,
+} from "@/data/dto/user";
import type { UserData } from "@/data/schemas/user/user";
export class UserApi {
diff --git a/src/data/storage/app/app_storage.ts b/src/data/storage/app/app_storage.ts
index 5adf64e4..979c52bc 100644
--- a/src/data/storage/app/app_storage.ts
+++ b/src/data/storage/app/app_storage.ts
@@ -30,6 +30,26 @@ export class AppStorage {
return SpAsyncUtil.setString(StorageKeys.lastPwaDialogShown, todayString);
}
+ // ---- external browser dialog ----
+
+ static async canShowExternalBrowserDialog(
+ todayString: string,
+ ): Promise> {
+ return AppStorage.canReportKey(
+ StorageKeys.lastExternalBrowserDialogShown,
+ todayString,
+ );
+ }
+
+ static recordExternalBrowserDialogShown(
+ todayString: string,
+ ): Promise> {
+ return SpAsyncUtil.setString(
+ StorageKeys.lastExternalBrowserDialogShown,
+ todayString,
+ );
+ }
+
// ---- PWA event reporting ----
static canReportPwaEvent(todayString: string): Promise> {
diff --git a/src/data/storage/storage_keys.ts b/src/data/storage/storage_keys.ts
index c633fa55..f5cfb04e 100644
--- a/src/data/storage/storage_keys.ts
+++ b/src/data/storage/storage_keys.ts
@@ -26,6 +26,7 @@ export const StorageKeys = {
// pwa / app info
lastPwaDialogShown: "last_pwa_dialog_shown",
+ lastExternalBrowserDialogShown: "last_external_browser_dialog_shown",
lastPwaEventReported: "last_pwa_event_reported",
lastAppInfoReported: "last_app_info_reported",
} as const;
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) */