fix(rectification): surface real activity context
Staging Backend Quality Gate / validate (pull_request) Failing after 12m4s
Staging Backend Quality Gate / publish (pull_request) Has been skipped

This commit is contained in:
Jesse_Chen
2026-08-12 09:28:47 +08:00
parent 689799102e
commit 397b6ef7c2
20 changed files with 907 additions and 98 deletions
@@ -5,7 +5,12 @@ import { useCallback, useEffect, useRef, useState } from "react";
import { parseAgentReply } from "@/lib/agent-reply";
import type { ChatMessage, ChatMessageView } from "@/lib/chat-message-view";
import { membershipHref } from "@/lib/membership";
import { PUBLIC_RECTIFICATION_PHASES, type PublicRectificationPhase } from "@/lib/rectification-agentic/v9/public-receipt";
import {
isPublicRectificationMethod,
isPublicRectificationTool,
type PublicRectificationMethod,
type PublicRectificationTool,
} from "@/lib/rectification-agentic/v9/public-receipt";
import type { PublicLanguageModel } from "@/lib/public-models";
import { ChatMessageRow } from "./chat-message-row";
import { ModelSelector } from "./model-selector";
@@ -21,6 +26,7 @@ type PersistedTurn = Readonly<{
status: string;
phases: readonly string[];
tools: readonly string[];
methods?: readonly string[];
skill_name?: string;
skill_version?: string;
}> | null;
@@ -58,38 +64,52 @@ type RectificationAgenticChatProps = Readonly<{
type RenderMessage = ChatMessageView & {
renderKey: string;
/** V9 execution-receipt steps; separate from shared consultation activity. */
receiptActivity?: readonly string[];
receiptActivity?: ReceiptActivity;
receiptStatus?: string;
};
const PHASE_LABELS: Readonly<Partial<Record<PublicRectificationPhase, string>>> = {
"run.started": "开始本轮执行",
"skill.started": "正在加载专用方法",
"skill.loaded": "专用方法已加载",
"case.loaded": "已读取校正记录",
"evidence.proposed": "记录了一条事件草稿",
"evidence.confirmed": "事件已确认",
"candidates.comparing": "正在比较候选时间",
"candidates.updated": "候选已更新",
"diagnostics.completed": "稳健性诊断完成",
"candidate.accepted": "已采用候选时间",
"birth_time.confirmed": "校正时间已确认",
"run.completed": "本轮完成",
"run.failed": "本轮失败",
type ReceiptActivity = Readonly<{
steps: readonly string[];
methods: readonly 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": "关闭校正记录",
};
function isPublicPhase(value: unknown): value is PublicRectificationPhase {
return typeof value === "string" && (PUBLIC_RECTIFICATION_PHASES as readonly string[]).includes(value);
}
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 labelForPhase(phase: string): string {
return isPublicPhase(phase) ? (PHASE_LABELS[phase] ?? phase) : phase;
}
function activityFromReceipt(receipt: PersistedTurn["receipt"]): string[] {
if (!receipt) return [];
const phases = (receipt.phases ?? []).filter(isPublicPhase);
return phases.map(labelForPhase).slice(0, 12);
function activityFromReceipt(receipt: PersistedTurn["receipt"]): ReceiptActivity {
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]))],
};
}
export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
@@ -213,13 +233,14 @@ 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: [] },
{ role: "assistant", text: "", renderKey: assistantRenderKey, state: "thinking", receiptActivity: { steps: [], methods: [] } },
]);
setDraft("");
let raw = "";
let liveActivity: string[] = [];
const activitySet = new Set<string>();
let liveActivity: ReceiptActivity = { steps: [], methods: [] };
const activitySteps = new Set<string>();
const activityMethods = new Set<string>();
try {
const response = await fetch("/api/rectification/agent", {
method: "POST",
@@ -268,9 +289,9 @@ 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 };
let event: { type?: unknown; text?: unknown; message?: unknown; tool?: unknown; methods?: unknown };
try {
event = JSON.parse(line) as { type?: unknown; text?: unknown; message?: unknown };
event = JSON.parse(line) as { type?: unknown; text?: unknown; message?: unknown; tool?: unknown; methods?: unknown };
} catch {
continue;
}
@@ -289,10 +310,16 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
setError(typeof event.message === "string" ? event.message : "生时校正暂时不可用,请稍后再试。");
} else if (event.type === "run.completed") {
completed = true;
} else if (isPublicPhase(event.type)) {
if (!activitySet.has(event.type)) {
activitySet.add(event.type);
liveActivity = [...liveActivity, labelForPhase(event.type)];
} else {
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] };
setMessages((current) => current.map((message) => message.renderKey === assistantRenderKey
? { ...message, receiptActivity: liveActivity }
: message));
@@ -386,15 +413,23 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
)}
{messages.map((message) => (
<div key={message.renderKey} className="rectification-message-wrap">
<ChatMessageRow message={message} />
{message.receiptActivity && message.receiptActivity.length > 0 && (
{message.receiptActivity && (message.receiptActivity.steps.length > 0 || message.receiptActivity.methods.length > 0) && (
<details className="rectification-activity">
<summary></summary>
<ol>
{message.receiptActivity.map((step) => <li key={step}>{step}</li>)}
</ol>
{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} />
</div>
))}
{candidateResult?.selectionAllowed && candidateResult.candidates.length > 0 && (