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 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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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" });
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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
@@ -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 (
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}` : ""}`,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|
||||||
|
|||||||
@@ -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 流回
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
@@ -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) */
|
||||||
|
|||||||
Reference in New Issue
Block a user