feat: add consultation and product domain registries

This commit is contained in:
Jesse_Chen
2026-08-15 06:04:23 +08:00
parent 5012ff7212
commit 6d192ad175
46 changed files with 1404 additions and 292 deletions
@@ -195,7 +195,7 @@ export function GeneratePersonalReportButton({
className="inline-flex min-h-11 shrink-0 items-center gap-1.5 whitespace-nowrap rounded-lg border border-border bg-canvas px-3 py-1.5 text-sm text-ink transition-colors hover:bg-canvas-muted disabled:pointer-events-none disabled:opacity-50"
onClick={() => void handleGenerate()}
disabled={submitting}
title="根据已确认的个人出生资料生成完整报告"
title="根据已采用或已确认的个人出生资料生成完整报告"
>
<FileText aria-hidden="true" className="size-4" />
{submitting ? "正在创建…" : "生成完整报告"}
@@ -149,7 +149,7 @@ export function PersonalReportCenter() {
<section className="report-center-hero">
<div>
<h1>个人报告</h1>
<p>基于你已确认的出生资料生成,与某一次对话无关。创建后会在后台继续处理,你无需停留等待。</p>
<p>基于你已采用或已确认的出生资料生成,与某一次对话无关。创建后会在后台继续处理,你无需停留等待。</p>
<p className="report-center-meta" aria-label="报告概览">
共 {state.reports.length} 份 · 已完成 {state.reports.filter((report) => report.status === "ready").length} 份 · {hasGenerating ? "有报告生成中" : latestReady ? "最新报告可查看" : "尚待生成"}
</p>
@@ -38,7 +38,7 @@ const CLAIM_STATUS_LABELS: Record<ClaimStatus, string> = {
const BIRTH_TIME_STATUS_LABELS: Record<string, string> = {
reported: "用户申报时间",
candidate: "候选时间(未确认)",
accepted: "已接受时间",
accepted: "已采用时间(未确认)",
confirmed: "已确认时间",
};
@@ -67,6 +67,7 @@ type RenderMessage = ChatMessageView & {
renderKey: string;
activeActivity?: PublicActivity;
completedReceipt?: CompletedActivityReceiptView;
failed?: boolean;
turnId?: string;
};
@@ -114,16 +115,30 @@ function activityPhase(tool: PublicRectificationTool): NonNullable<ChatMessageVi
return "evidence-validation";
}
function hasActivityReceipt(receipt: CompletedActivityReceiptView): boolean {
return receipt.steps.length > 0 || receipt.methods.length > 0 || Boolean(receipt.failedTool);
}
function persistedTurnFailed(turn: PersistedTurn): boolean {
return turn.status === "failed"
|| turn.status === "retryable"
|| turn.receipt?.status === "failed"
|| turn.receipt?.status === "degraded"
|| turn.receipt?.status === "blocked";
}
function messagesFromTurns(initialTurns: readonly PersistedTurn[]): RenderMessage[] {
return initialTurns.flatMap((turn, index): RenderMessage[] => {
const key = `persisted-${turn.id}-${index}`;
if (turn.role === "assistant") {
const failed = persistedTurnFailed(turn);
return [{
role: "assistant",
text: turn.text ?? "",
text: failed ? "" : turn.text ?? "",
renderKey: key,
state: turn.status === "completed" ? "settled" : "thinking",
state: turn.status === "completed" || failed ? "settled" : "thinking",
completedReceipt: completedReceiptFromPersisted(turn.receipt),
failed,
turnId: turn.id,
}];
}
@@ -251,7 +266,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
if (!response.ok) {
const payload = await response.json().catch(() => null);
const message = payload?.message || payload?.error || `请求失败(${response.status})`;
setMessages((current) => current.filter((item) => item.renderKey !== assistantRenderKey));
setMessages((current) => current.filter((message) => message.renderKey !== assistantRenderKey));
if (payload?.code === "profile_incomplete") {
onProfileIncomplete?.();
return;
@@ -265,7 +280,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
return;
}
if (!response.body) {
setMessages((current) => current.filter((item) => item.renderKey !== assistantRenderKey));
setMessages((current) => current.filter((message) => message.renderKey !== assistantRenderKey));
setError("服务暂时不可用,请稍后再试。");
return;
}
@@ -302,7 +317,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
raw += event.text;
const parsed = parseAgentReplyBody(raw);
setMessages((current) => current.map((message) => message.renderKey === assistantRenderKey
? { ...message, text: parsed.text, state: "streaming", activeActivity: undefined }
? { ...message, text: parsed.text, state: "streaming" }
: message));
} else if (event.type === "run.failed") {
streamFailed = true;
@@ -343,18 +358,31 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
const parsed = parseAgentReplyBody(raw);
const succeeded = completed && !streamFailed && Boolean(parsed.text);
setMessages((current) => succeeded
? current.map((message) => message.renderKey === assistantRenderKey
? {
setMessages((current) => current.flatMap((message): RenderMessage[] => {
if (message.renderKey !== assistantRenderKey) return [message];
if (succeeded) {
return [{
...message,
text: parsed.text,
state: "settled",
activeActivity: undefined,
completedReceipt,
failed: false,
turnId: completedTurnId,
}
: message)
: current.filter((message) => message.renderKey !== assistantRenderKey));
}];
}
if (streamFailed || hasActivityReceipt(completedReceipt)) {
return [{
...message,
text: "",
state: "settled",
activeActivity: undefined,
completedReceipt,
failed: true,
}];
}
return [];
}));
if (!succeeded && completedReceipt.failedTool) {
setError(completedReceipt.failedTool === "rectification-compare-candidates"
? "候选比较未完成,当前进度已保留。"
@@ -372,7 +400,19 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
}
} catch {
setError("生时校正暂时不可用,请稍后再试。");
setMessages((current) => current.filter((message) => message.renderKey !== assistantRenderKey));
setMessages((current) => current.flatMap((message): RenderMessage[] => {
if (message.renderKey !== assistantRenderKey) return [message];
return hasActivityReceipt(completedReceipt)
? [{
...message,
text: "",
state: "settled",
activeActivity: undefined,
completedReceipt,
failed: true,
}]
: [];
}));
} finally {
setPending(false);
}
@@ -474,6 +514,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
.find((message) => (
message.role === "assistant"
&& message.state === "settled"
&& !message.failed
&& Boolean(message.turnId)
&& Boolean(message.text)
))?.renderKey;
@@ -491,6 +532,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
{messages.map((message) => {
const showActions = message.role === "assistant"
&& message.state === "settled"
&& !message.failed
&& Boolean(message.text);
const regenerating = regeneratingMessageKey === message.renderKey;
const canRegenerate = message.renderKey === latestRegeneratableKey
@@ -502,18 +544,25 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
: message;
return (
<div key={message.renderKey} className="rectification-message-wrap rectification-message-entry">
{message.state === "settled" && message.failed && !message.completedReceipt?.failedTool && (
<p className="rectification-activity-failure" role="status">
本轮处理未完成,已保留服务端记录的执行进度。
</p>
)}
{message.state === "settled" && message.completedReceipt && (
<CompletedActivityReceipt receipt={message.completedReceipt} />
)}
<ChatMessageRow
message={{
...displayedMessage,
activity: displayedMessage.activeActivity
? { phase: activityPhase(displayedMessage.activeActivity.tool), label: displayedMessage.activeActivity.label }
: undefined,
}}
showActivity={displayedMessage.state === "thinking" || Boolean(displayedMessage.activeActivity)}
/>
{(!message.failed || Boolean(displayedMessage.text) || regenerating) && (
<ChatMessageRow
message={{
...displayedMessage,
activity: displayedMessage.activeActivity
? { phase: activityPhase(displayedMessage.activeActivity.tool), label: displayedMessage.activeActivity.label }
: undefined,
}}
showActivity={displayedMessage.state === "thinking" || Boolean(displayedMessage.activeActivity)}
/>
)}
{showActions && !regenerating && (
<div className="rectification-message-actions" aria-label="Agent 回答操作">
<button
@@ -564,9 +613,9 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
{candidateResult?.selectionAllowed && (
<section className="rectification-candidates" aria-label="生时校正候选时间">
<div className="rectification-candidates-heading">
<strong>{candidateResult.confirmationAllowed ? "确认校正时间" : "当前可能的出生时间"}</strong>
<strong>当前可能的出生时间</strong>
<span>这是根据你目前提供的人生事件推算出的几种可能时间。可以先采用一个作为当前排盘时间,也可以继续补充事件;新增证据后,候选和相对支持度会重新计算,准确度还可以继续提高。</span>
<span>相对支持度不是统计概率,采用不会覆盖原始填报时间。</span>
<span>相对支持度不是统计概率;采用不等于确认出生时间,也不会覆盖原始填报时间。</span>
</div>
<div className="rectification-candidate-list">
{candidateResult.candidates.map((candidate) => {
@@ -597,7 +646,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
)}
{savedTime && (
<p className="rectification-saved" role="status">
{savedStatus === "confirmed" ? "已确认校正时间" : "校正采用时间"}:{savedTime}。后续排盘将使用该时间;你仍可继续补充事件或改选其他候选。
{savedStatus === "confirmed" ? "已确认校正时间" : "已采用校正时间(未确认)"}:{savedTime}。后续排盘将使用该时间;你仍可继续补充事件或改选其他候选。
</p>
)}
{error && <p className="error-message" role="alert">{error}</p>}