refactor(logging): centralize console output
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -16,6 +16,9 @@ import { useRouter } from "next/navigation";
|
||||
import { AuthStorage } from "@/data/storage/auth/auth_storage";
|
||||
import { UserStorage } from "@/data/storage/user/user_storage";
|
||||
import { ROUTES } from "@/router/routes";
|
||||
import { Logger } from "@/utils";
|
||||
|
||||
const log = new Logger("AppChatDeviceidDeviceIdDeepLinkPersist");
|
||||
|
||||
interface DeepLinkPersistProps {
|
||||
deviceId: string;
|
||||
@@ -43,7 +46,7 @@ export default function DeepLinkPersist({
|
||||
}
|
||||
} catch (err) {
|
||||
// 写不进 localStorage 时(例如隐私模式)也不阻塞跳转;记录到 console 即可。
|
||||
console.warn("[DeepLinkPersist] failed to persist", err);
|
||||
log.warn("[DeepLinkPersist] failed to persist", err);
|
||||
} finally {
|
||||
setDone(true);
|
||||
router.replace(ROUTES.chat);
|
||||
|
||||
+4
-1
@@ -14,6 +14,9 @@ import { useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
import { ROUTES } from "@/router/routes";
|
||||
import { Logger } from "@/utils";
|
||||
|
||||
const log = new Logger("AppError");
|
||||
|
||||
interface ErrorProps {
|
||||
error: Error & { digest?: string };
|
||||
@@ -23,7 +26,7 @@ interface ErrorProps {
|
||||
export default function GlobalError({ error, unstable_retry }: ErrorProps) {
|
||||
useEffect(() => {
|
||||
// 把错误送到 console;后续接 Sentry/Datadog 时改这里。
|
||||
console.error("[app/error.tsx]", error);
|
||||
log.error("[app/error.tsx]", error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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}` : ""}`,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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<string | null> {
|
||||
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");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -8,8 +8,12 @@
|
||||
* - 消费者可声明每条消息是否成功消费;不成功则重新入队
|
||||
* - 队列可在任意时刻清空(dispose)
|
||||
*/
|
||||
import { Logger } from "@/utils";
|
||||
|
||||
export type MessageConsumer = (content: string) => boolean | Promise<boolean>;
|
||||
|
||||
const log = new Logger("MessageQueue");
|
||||
|
||||
export class MessageQueue {
|
||||
private queue: string[] = [];
|
||||
private timer: ReturnType<typeof setTimeout> | 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) {
|
||||
|
||||
@@ -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<Result<GuestLoginResponse>> {
|
||||
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<Result<User>> {
|
||||
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<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,9 @@ import { RegisterRequest } from "@/data/dto/auth/register_request";
|
||||
import { SendCodeRequest } from "@/data/dto/auth/send_code_request";
|
||||
import { User } from "@/data/dto/user/user";
|
||||
import type { UserData } from "@/data/schemas/user/user";
|
||||
import { Logger } from "@/utils";
|
||||
|
||||
const log = new Logger("DataServicesApiAuthApi");
|
||||
|
||||
export class AuthApi {
|
||||
/**
|
||||
@@ -123,10 +126,10 @@ export class AuthApi {
|
||||
method: "POST",
|
||||
body: body.toJson(),
|
||||
});
|
||||
console.log("[AuthApi.guestLogin] raw envelope from backend", env);
|
||||
log.debug("[AuthApi.guestLogin] raw envelope from backend", env);
|
||||
const unwrapped = unwrap(env) as Record<string, unknown>;
|
||||
const userObj = unwrapped.user as { lastMessageAt?: unknown } | undefined;
|
||||
console.log("[AuthApi.guestLogin] unwrapped data (pre-parse)", {
|
||||
log.debug("[AuthApi.guestLogin] unwrapped data (pre-parse)", {
|
||||
hasUser: "user" in unwrapped,
|
||||
userKeys: userObj && typeof userObj === "object" ? Object.keys(userObj) : null,
|
||||
lastMessageAt: userObj?.lastMessageAt,
|
||||
@@ -136,13 +139,13 @@ export class AuthApi {
|
||||
return GuestLoginResponse.fromJson(unwrapped);
|
||||
} catch (e) {
|
||||
if (e instanceof z.ZodError) {
|
||||
console.error("[AuthApi.guestLogin] ZodError parsing response", {
|
||||
log.error("[AuthApi.guestLogin] ZodError parsing response", {
|
||||
issueCount: e.issues.length,
|
||||
firstIssue: e.issues[0],
|
||||
allPaths: e.issues.map((i) => i.path),
|
||||
});
|
||||
} else {
|
||||
console.error("[AuthApi.guestLogin] unexpected error", e);
|
||||
log.error("[AuthApi.guestLogin] unexpected error", e);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -20,17 +20,17 @@ const authRepo: IAuthRepository = authRepository;
|
||||
|
||||
export const emailLoginActor = fromPromise<LoginStatus, { email: string; password: string }>(
|
||||
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<void> {
|
||||
// 受保护 API 401 会由 HTTP 层清掉 token 回到 splash,无需在此处主动校验。
|
||||
|
||||
export const guestLoginActor = fromPromise<LoginStatus, void>(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,
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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: "<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);
|
||||
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: "<actor path>",
|
||||
});
|
||||
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<ChatEvent, { token: string }>(
|
||||
({ 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: "<actor path>",
|
||||
});
|
||||
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<ChatEvent, { token: string }>(
|
||||
});
|
||||
};
|
||||
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<ChatEvent, { token: string }>(
|
||||
*/
|
||||
export const sendMessageWsActor = fromPromise<void, { content: string }>(
|
||||
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 流回
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
+70
-10
@@ -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<PinoLogger["debug"]>;
|
||||
|
||||
/**
|
||||
* 全局 pino root logger(单例,进程级复用)。
|
||||
*
|
||||
@@ -45,20 +48,77 @@ export class Logger {
|
||||
this.logger = rootLogger.child({ component });
|
||||
}
|
||||
|
||||
debug(...args: Parameters<PinoLogger["debug"]>): void {
|
||||
this.logger.debug(...args);
|
||||
debug(...args: LogArgs): void {
|
||||
this.logger.debug(...Logger.normalizeLogArgs(args));
|
||||
}
|
||||
info(...args: Parameters<PinoLogger["info"]>): void {
|
||||
this.logger.info(...args);
|
||||
info(...args: LogArgs): void {
|
||||
this.logger.info(...Logger.normalizeLogArgs(args));
|
||||
}
|
||||
warn(...args: Parameters<PinoLogger["warn"]>): void {
|
||||
this.logger.warn(...args);
|
||||
warn(...args: LogArgs): void {
|
||||
this.logger.warn(...Logger.normalizeLogArgs(args));
|
||||
}
|
||||
error(...args: Parameters<PinoLogger["error"]>): void {
|
||||
this.logger.error(...args);
|
||||
error(...args: LogArgs): void {
|
||||
this.logger.error(...Logger.normalizeLogArgs(args));
|
||||
}
|
||||
fatal(...args: Parameters<PinoLogger["fatal"]>): 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) */
|
||||
|
||||
Reference in New Issue
Block a user