fix(rectification): align activity with real agent work
Independent Staging Quality Gate / validate (push) Successful in 14m58s
Independent Staging Quality Gate / publish (push) Successful in 8m5s

This commit is contained in:
Jesse_Chen
2026-08-13 14:43:54 +08:00
parent 62d12c5c06
commit 89f96dcfa5
19 changed files with 1149 additions and 199 deletions
@@ -21,7 +21,7 @@ export function AgentActivityStatus({
return (
<div className="agent-activity-status" role="status">
<ThinkingOrb aria-hidden="true" state={state} size={20} />
<span>{label}</span>
<span key={label} className="agent-activity-status__text">{label}</span>
</div>
);
}
+9 -3
View File
@@ -16,7 +16,13 @@ export function AgentAvatar() {
return <span className="agent-avatar" aria-hidden="true" />;
}
export function ChatMessageRow({ message }: { readonly message: ChatMessageView }) {
export function ChatMessageRow({
message,
showActivity = message.state !== "settled",
}: Readonly<{
message: ChatMessageView;
showActivity?: boolean;
}>) {
const messageRow = useRef<HTMLElement>(null);
const assistantLabel = message.state === "thinking"
? "Jyotisha 正在分析"
@@ -32,7 +38,7 @@ export function ChatMessageRow({ message }: { readonly message: ChatMessageView
} as const)[message.activity.phase]
: message.state === "thinking" ? "working" : "composing";
const activityLabel = message.activity?.label
?? (message.state === "thinking" ? "正在核对星盘信息…" : undefined);
?? (message.state === "thinking" ? "正在处理…" : undefined);
useGSAP(() => {
if (!messageRow.current) return;
@@ -63,7 +69,7 @@ export function ChatMessageRow({ message }: { readonly message: ChatMessageView
<div className="message-bubble">
{message.role === "assistant" ? (
<>
{message.state !== "settled" && (
{showActivity && (
<AgentActivityStatus state={activityState} label={activityLabel} />
)}
{message.text && <ChatMessageContent text={message.text} />}
@@ -0,0 +1,143 @@
"use client";
import { Check, ChevronDown } from "lucide-react";
import type {
PublicRectificationMethod,
PublicRectificationTool,
} from "@/lib/rectification-agentic/v9/public-receipt";
export type CompletedActivityReceiptView = Readonly<{
steps: readonly PublicRectificationTool[];
methods: readonly PublicRectificationMethod[];
failedTool?: PublicRectificationTool;
}>;
const TOOL_LABELS: Readonly<Record<PublicRectificationTool, string>> = {
"rectification-read-case": "读取校正记录",
"rectification-propose-evidence": "整理事件证据",
"rectification-confirm-evidence": "确认事件证据",
"rectification-revise-evidence": "修订事件证据",
"rectification-compare-candidates": "比较候选时间",
"rectification-read-diagnostics": "检查候选稳健性",
"rectification-offer-candidates": "生成候选建议",
"rectification-accept-candidate": "采用候选时间",
"rectification-confirm-birth-time": "确认校正时间",
"rectification-close-case": "完成校正记录",
};
const METHOD_LABELS: Readonly<Record<PublicRectificationMethod, string>> = {
"d1-rashi": "D1 本命盘",
"d2-hora": "D2 财帛分盘",
"d4-chaturthamsha": "D4 迁移分盘",
"d9-navamsa": "D9 婚姻分盘",
"d10-dashamsa": "D10 事业分盘",
"d11-labhamsha": "D11 收益分盘",
"d24-chaturvimshamsha": "D24 教育分盘",
"d30-trimshamsha": "D30 健康压力分盘",
"vimshottari-dasha": "Vimshottari",
"narayana-dasha": "Narayana",
gochara: "Gochara",
ashtakavarga: "Ashtakavarga",
shadbala: "Shadbala",
"arudha-pada": "Arudha Pada",
"functional-benefic-malefic": "本命功能吉凶星",
};
const METHOD_GROUPS: readonly Readonly<{
label: string;
methods: readonly PublicRectificationMethod[];
}>[] = [
{
label: "基础判断",
methods: [
"d1-rashi",
"functional-benefic-malefic",
"vimshottari-dasha",
"narayana-dasha",
"arudha-pada",
],
},
{
label: "主题分盘",
methods: [
"d2-hora",
"d4-chaturthamsha",
"d9-navamsa",
"d10-dashamsa",
"d11-labhamsha",
"d24-chaturvimshamsha",
"d30-trimshamsha",
],
},
{
label: "交叉验证",
methods: ["gochara", "ashtakavarga", "shadbala"],
},
];
function failureLabel(tool: PublicRectificationTool): string {
return tool === "rectification-compare-candidates"
? "候选比较未完成,当前进度已保留"
: `${TOOL_LABELS[tool]}未完成,当前进度已保留`;
}
export function CompletedActivityReceipt({
receipt,
}: Readonly<{ receipt: CompletedActivityReceiptView }>) {
const stepLabels = receipt.steps.map((tool) => TOOL_LABELS[tool]);
const methodSet = new Set(receipt.methods);
const methodGroups = METHOD_GROUPS.map((group) => ({
label: group.label,
methods: group.methods.filter((method) => methodSet.has(method)).map((method) => METHOD_LABELS[method]),
})).filter((group) => group.methods.length > 0);
const hasDetails = stepLabels.length > 0 || methodGroups.length > 0;
const summary = receipt.failedTool
? failureLabel(receipt.failedTool)
: `本轮完成 · ${stepLabels.length} 个步骤 · ${receipt.methods.length} 项计算依据`;
if (!hasDetails) {
return receipt.failedTool
? <p className="rectification-activity-failure">{summary}</p>
: null;
}
return (
<details className={`rectification-activity-receipt${receipt.failedTool ? " is-failed" : ""}`}>
<summary>
<span className="rectification-activity-receipt__summary-main">
{receipt.failedTool ? (
<span className="rectification-activity-receipt__failure-mark" aria-hidden="true">!</span>
) : (
<Check aria-hidden="true" size={14} strokeWidth={2.25} />
)}
<span>{summary}</span>
</span>
<span className="rectification-activity-receipt__toggle">
<ChevronDown aria-hidden="true" size={14} />
</span>
</summary>
<div className="rectification-activity-receipt__details">
{stepLabels.length > 0 && (
<section aria-label="执行步骤">
<h3></h3>
<p>{stepLabels.join(" · ")}</p>
</section>
)}
{methodGroups.length > 0 && (
<section aria-label="计算依据">
<h3></h3>
<div className="rectification-activity-receipt__method-groups">
{methodGroups.map((group) => (
<p key={group.label}>
<strong>{group.label}</strong>
{group.methods.join("、")}
</p>
))}
</div>
</section>
)}
</div>
</details>
);
}
@@ -18,6 +18,10 @@ import {
} from "@/lib/rectification-agentic/v9/public-receipt";
import type { PublicLanguageModel } from "@/lib/public-models";
import { ChatMessageRow } from "./chat-message-row";
import {
CompletedActivityReceipt,
type CompletedActivityReceiptView,
} from "./completed-activity-receipt";
import { ModelSelector } from "./model-selector";
import { Button } from "./ui/button";
import { Textarea } from "./ui/textarea";
@@ -59,55 +63,43 @@ type RectificationAgenticChatProps = Readonly<{
type RenderMessage = ChatMessageView & {
renderKey: string;
/** V9 execution-receipt steps; separate from shared consultation activity. */
receiptActivity?: ReceiptActivity;
receiptStatus?: string;
activeActivity?: PublicActivity;
completedReceipt?: CompletedActivityReceiptView;
};
type ReceiptActivity = Readonly<{
steps: readonly string[];
methods: readonly string[];
type PublicActivity = Readonly<{
tool: PublicRectificationTool;
label: string;
}>;
const TOOL_LABELS: Readonly<Record<PublicRectificationTool, string>> = {
"rectification-read-case": "读取校正记录",
"rectification-propose-evidence": "整理事件证据",
"rectification-confirm-evidence": "确认事件证据",
"rectification-revise-evidence": "修订事件证据",
"rectification-compare-candidates": "比较候选时间",
"rectification-read-diagnostics": "检查候选稳健性",
"rectification-offer-candidates": "生成候选建议",
"rectification-accept-candidate": "采用候选时间",
"rectification-confirm-birth-time": "确认校正时间",
"rectification-close-case": "关闭校正记录",
const ACTIVE_TOOL_LABELS: Readonly<Record<PublicRectificationTool, string>> = {
"rectification-read-case": "正在读取校正记录",
"rectification-propose-evidence": "正在整理事件证据",
"rectification-confirm-evidence": "正在确认事件证据",
"rectification-revise-evidence": "正在修订事件证据",
"rectification-compare-candidates": "正在比较候选时间",
"rectification-read-diagnostics": "正在检查候选稳健性",
"rectification-offer-candidates": "正在生成候选建议",
"rectification-accept-candidate": "正在采用候选时间",
"rectification-confirm-birth-time": "正在确认校正时间",
"rectification-close-case": "正在完成校正记录",
};
const METHOD_LABELS: Readonly<Record<PublicRectificationMethod, string>> = {
"d1-rashi": "D1 本命盘",
"d2-hora": "D2 财帛分盘",
"d4-chaturthamsha": "D4 地产迁移分盘",
"d9-navamsa": "D9 婚姻分盘",
"d10-dashamsa": "D10 事业分盘",
"d11-labhamsha": "D11 收益分盘",
"d24-chaturvimshamsha": "D24 教育分盘",
"d30-trimshamsha": "D30 健康压力分盘",
"vimshottari-dasha": "Vimshottari 大运",
"narayana-dasha": "Narayana 大运",
gochara: "Gochara 行运",
ashtakavarga: "Ashtakavarga",
shadbala: "Shadbala",
"arudha-pada": "Arudha Pada",
"functional-benefic-malefic": "功能吉凶星",
};
function activityFromReceipt(receipt: PersistedTurn["receipt"]): ReceiptActivity {
function completedReceiptFromPersisted(receipt: PersistedTurn["receipt"]): CompletedActivityReceiptView {
if (!receipt) return { steps: [], methods: [] };
return {
steps: [...new Set((receipt.tools ?? []).filter(isPublicRectificationTool).map((tool) => TOOL_LABELS[tool]))],
methods: [...new Set((receipt.methods ?? []).filter(isPublicRectificationMethod).map((method) => METHOD_LABELS[method]))],
steps: [...new Set((receipt.tools ?? []).filter(isPublicRectificationTool))],
methods: [...new Set((receipt.methods ?? []).filter(isPublicRectificationMethod))],
};
}
function activityPhase(tool: PublicRectificationTool): NonNullable<ChatMessageView["activity"]>["phase"] {
if (tool === "rectification-compare-candidates" || tool === "rectification-read-diagnostics") {
return "chart-calculation";
}
return "evidence-validation";
}
function messagesFromTurns(initialTurns: readonly PersistedTurn[]): RenderMessage[] {
return initialTurns.flatMap((turn, index): RenderMessage[] => {
const key = `persisted-${turn.id}-${index}`;
@@ -117,8 +109,7 @@ function messagesFromTurns(initialTurns: readonly PersistedTurn[]): RenderMessag
text: turn.text ?? "",
renderKey: key,
state: turn.status === "completed" ? "settled" : "thinking",
receiptActivity: activityFromReceipt(turn.receipt),
receiptStatus: turn.receipt?.status,
completedReceipt: completedReceiptFromPersisted(turn.receipt),
}];
}
return [{
@@ -218,14 +209,15 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
...(action === "message"
? [{ role: "user", text: trimmed, renderKey: userRenderKey, state: "settled" } satisfies RenderMessage]
: []),
{ role: "assistant", text: "", renderKey: assistantRenderKey, state: "thinking", receiptActivity: { steps: [], methods: [] } },
{ role: "assistant", text: "", renderKey: assistantRenderKey, state: "thinking" },
]);
setDraft("");
let raw = "";
let liveActivity: ReceiptActivity = { steps: [], methods: [] };
const activitySteps = new Set<string>();
const activityMethods = new Set<string>();
let completedReceipt: CompletedActivityReceiptView = { steps: [], methods: [] };
const completedSteps = new Set<PublicRectificationTool>();
const completedMethods = new Set<PublicRectificationMethod>();
let failedTool: PublicRectificationTool | undefined;
try {
const response = await fetch("/api/rectification/agent", {
method: "POST",
@@ -274,9 +266,16 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
buffer = lines.pop() ?? "";
for (const line of lines) {
if (!line.trim()) continue;
let event: { type?: unknown; text?: unknown; message?: unknown; tool?: unknown; methods?: unknown };
let event: {
type?: unknown;
status?: unknown;
text?: unknown;
message?: unknown;
tool?: unknown;
methods?: unknown;
};
try {
event = JSON.parse(line) as { type?: unknown; text?: unknown; message?: unknown; tool?: unknown; methods?: unknown };
event = JSON.parse(line) as typeof event;
} catch {
continue;
}
@@ -285,7 +284,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" }
? { ...message, text: parsed.text, state: "streaming", activeActivity: undefined }
: message));
} else if (event.type === "run.failed") {
streamFailed = true;
@@ -294,20 +293,37 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
setError(typeof event.message === "string" ? event.message : "生时校正暂时不可用,请稍后再试。");
} else if (event.type === "run.completed") {
completed = true;
} else {
} else if (event.type === "tool.activity") {
const tool = isPublicRectificationTool(event.tool) ? event.tool : null;
if (tool) activitySteps.add(TOOL_LABELS[tool]);
if (Array.isArray(event.methods)) {
for (const method of event.methods) {
if (isPublicRectificationMethod(method)) activityMethods.add(METHOD_LABELS[method]);
}
}
if (tool || activityMethods.size !== liveActivity.methods.length) {
liveActivity = { steps: [...activitySteps], methods: [...activityMethods] };
if (!tool) continue;
if (event.status === "started") {
const activeActivity = { tool, label: ACTIVE_TOOL_LABELS[tool] } satisfies PublicActivity;
setMessages((current) => current.map((message) => message.renderKey === assistantRenderKey
? { ...message, receiptActivity: liveActivity }
? { ...message, activeActivity }
: message));
continue;
}
if (event.status === "completed") {
completedSteps.add(tool);
if (Array.isArray(event.methods)) {
for (const method of event.methods) {
if (isPublicRectificationMethod(method)) completedMethods.add(method);
}
}
} else if (event.status === "failed") {
failedTool = tool;
}
completedReceipt = {
steps: [...completedSteps],
methods: [...completedMethods],
...(failedTool ? { failedTool } : {}),
};
setMessages((current) => current.map((message) => message.renderKey === assistantRenderKey
? {
...message,
activeActivity: message.activeActivity?.tool === tool ? undefined : message.activeActivity,
}
: message));
}
}
}
@@ -316,9 +332,20 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
const succeeded = completed && !streamFailed && Boolean(parsed.text);
setMessages((current) => succeeded
? current.map((message) => message.renderKey === assistantRenderKey
? { ...message, text: parsed.text, state: "settled", receiptActivity: liveActivity }
? {
...message,
text: parsed.text,
state: "settled",
activeActivity: undefined,
completedReceipt,
}
: message)
: current.filter((message) => message.renderKey !== assistantRenderKey));
if (!succeeded && failedTool) {
setError(failedTool === "rectification-compare-candidates"
? "候选比较未完成,当前进度已保留。"
: "本轮处理未完成,当前进度已保留。请稍后再试。");
}
if (succeeded) {
onMessagesChange?.([
...(action === "message" ? [{ role: "user" as const, text: trimmed }] : []),
@@ -396,23 +423,18 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
)}
{messages.map((message) => (
<div key={message.renderKey} className="rectification-message-wrap">
{message.receiptActivity && (message.receiptActivity.steps.length > 0 || message.receiptActivity.methods.length > 0) && (
<details className="rectification-activity">
<summary></summary>
{message.receiptActivity.steps.length > 0 && (
<ol>
{message.receiptActivity.steps.map((step) => <li key={step}>{step}</li>)}
</ol>
)}
{message.receiptActivity.methods.length > 0 && (
<div className="rectification-activity-methods">
<span>使</span>
<ul>{message.receiptActivity.methods.map((method) => <li key={method}>{method}</li>)}</ul>
</div>
)}
</details>
<ChatMessageRow
message={{
...message,
activity: message.activeActivity
? { phase: activityPhase(message.activeActivity.tool), label: message.activeActivity.label }
: undefined,
}}
showActivity={message.state === "thinking" || Boolean(message.activeActivity)}
/>
{message.state === "settled" && message.completedReceipt && (
<CompletedActivityReceipt receipt={message.completedReceipt} />
)}
<ChatMessageRow message={message} />
</div>
))}
{candidateResult?.selectionAllowed && candidateResult.candidates.length > 0 && (
@@ -441,7 +463,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
</span>
<span className="rectification-candidate-support"> {candidate.relativeSupport}</span>
<span className="rectification-candidate-action">
{selected ? "已采用" : acceptingTime === candidate.time ? "保存中…" : candidateResult.selectedTime ? "改选为此时间" : "采用此时间"}
{selected ? "已采用" : acceptingTime === candidate.time ? "正在采用…" : candidateResult.selectedTime ? "改选为此时间" : "采用此时间"}
</span>
</button>
);