merge: sync latest staging into rectification v9

# Conflicts:
#	docs/BUG_HISTORY.md
This commit is contained in:
Jesse
2026-08-11 18:08:42 +08:00
29 changed files with 1893 additions and 310 deletions
@@ -4,6 +4,8 @@ import { useGetIdentity } from "@refinedev/core";
import { App, Button, Space, Tag, Typography, type TableColumnsType } from "antd";
import { useState } from "react";
import { ReasonActionModal } from "./reason-action-modal";
import { formatAdminDate, ResourceTable } from "@/components/admin/resource-table";
import { adminRequestJson, type AdminIdentity } from "@/lib/admin/providers";
@@ -36,6 +38,9 @@ export default function UsersPage() {
const { data: identity } = useGetIdentity<AdminIdentity>();
const [revealed, setRevealed] = useState<Record<string, RevealedBirthData>>({});
const [revealingId, setRevealingId] = useState<string | null>(null);
const [resetTarget, setResetTarget] = useState<UserRecord | null>(null);
const [resetting, setResetting] = useState(false);
const canReset = Boolean(identity?.permissions.includes("admin.users.manage_roles"));
const canReveal = Boolean(identity?.permissions.includes("admin.customers.birth_data.read"));
async function revealBirthData(userId: string) {
@@ -53,6 +58,36 @@ export default function UsersPage() {
}
}
async function resetAccount(reason: string) {
if (!resetTarget) return;
setResetting(true);
try {
await adminRequestJson<{ data: { credits: number } }>(
"/api/admin/customers/reset",
{
method: "POST",
body: JSON.stringify({
userId: resetTarget.id,
confirmation: "RESET",
reason,
}),
},
);
message.success(`已重置 ${resetTarget.email},登录身份、管理员角色和积分保持不变`);
setRevealed((current) => {
const next = { ...current };
delete next[resetTarget.id];
return next;
});
setResetTarget(null);
} catch (error) {
message.error(error instanceof Error ? error.message : "重置账号失败");
throw error;
} finally {
setResetting(false);
}
}
const maskedValue = (value: string | null | undefined) => value || "未填写";
const columns: TableColumnsType<UserRecord> = [
{ title: "邮箱", dataIndex: "email", sorter: true },
@@ -82,7 +117,25 @@ export default function UsersPage() {
{ title: "邮箱验证", dataIndex: "emailVerified", render: (value) => value ? "已验证" : "未验证" },
{ title: "状态", dataIndex: "banned", render: (value) => value ? <Tag color="red"></Tag> : <Tag color="green"></Tag> },
{ title: "注册时间", dataIndex: "createdAt", sorter: true, render: formatAdminDate },
...(canReset ? [{
title: "操作",
render: (_: unknown, item: UserRecord) => <Button danger size="small" onClick={() => setResetTarget(item)}>
</Button>,
}] : []),
];
return <ResourceTable<UserRecord> resource="customers" title="用户资料(列表始终脱敏)" columns={columns} />;
return <>
<ResourceTable<UserRecord> resource="customers" title="用户资料(列表始终脱敏)" columns={columns} />
<ReasonActionModal
open={Boolean(resetTarget)}
title={`确认重置 ${resetTarget?.email ?? "该账号"} 的资料与会话?登录身份、管理员角色、积分及账务审计记录会保留。`}
okText="确认重置"
danger
confirmLoading={resetting}
reauthPermission="admin.users.manage_roles"
onCancel={() => setResetTarget(null)}
onSubmit={resetAccount}
/>
</>;
}
+11 -4
View File
@@ -23,6 +23,16 @@ export function ChatMessageRow({ message }: { readonly message: ChatMessageView
: message.state === "streaming"
? "Jyotisha 正在回答"
: "Jyotisha";
const activityState = message.activity
? ({
"loading-method": "searching",
"chart-calculation": "solving",
"evidence-validation": "working",
"answer-composition": "composing",
} as const)[message.activity.phase]
: message.state === "thinking" ? "working" : "composing";
const activityLabel = message.activity?.label
?? (message.state === "thinking" ? "正在核对星盘信息…" : undefined);
useGSAP(() => {
if (!messageRow.current) return;
@@ -54,10 +64,7 @@ export function ChatMessageRow({ message }: { readonly message: ChatMessageView
{message.role === "assistant" ? (
<>
{message.state !== "settled" && (
<AgentActivityStatus
state={message.state === "thinking" ? "working" : "composing"}
label={message.state === "thinking" ? "正在核对星盘信息…" : undefined}
/>
<AgentActivityStatus state={activityState} label={activityLabel} />
)}
{message.text && <ChatMessageContent text={message.text} />}
</>