refactor(logging): centralize console output

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