接入后台架构标签与端到端验证
This commit is contained in:
+27
-2
@@ -7,6 +7,7 @@ import {
|
||||
ArticleInputForm,
|
||||
type ArticleInputPayload,
|
||||
} from "../components/article-input-form";
|
||||
import { ArchitectureObserverPanel } from "../components/architecture/architecture-observer-panel";
|
||||
import { FactCardEditor } from "../components/fact-card-editor";
|
||||
import { OptimizedPreview } from "../components/optimized-preview";
|
||||
import { PerformanceCalibrationPanel } from "../components/performance-calibration-panel";
|
||||
@@ -47,7 +48,7 @@ interface TimingSummary {
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
const [activeTab, setActiveTab] = useState<"geo" | "copy">("geo");
|
||||
const [activeTab, setActiveTab] = useState<"geo" | "copy" | "architecture">("geo");
|
||||
const [input, setInput] = useState(initialInput);
|
||||
const [jobId, setJobId] = useState<string | null>(null);
|
||||
const [factCard, setFactCard] = useState<OptimizationFactCard | null>(null);
|
||||
@@ -61,6 +62,7 @@ export default function Home() {
|
||||
const [elapsedSeconds, setElapsedSeconds] = useState(0);
|
||||
const [lastTiming, setLastTiming] = useState<TimingSummary | null>(null);
|
||||
const [streamActivity, setStreamActivity] = useState("");
|
||||
const [architectureEvents, setArchitectureEvents] = useState<OptimizationStreamEvent[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!busyAction) return;
|
||||
@@ -84,6 +86,7 @@ export default function Home() {
|
||||
setOptimizedArticle(null);
|
||||
setQaReport(null);
|
||||
setStreamActivity("正在创建任务");
|
||||
setArchitectureEvents([]);
|
||||
|
||||
try {
|
||||
const payload: ArticleInputPayload & {
|
||||
@@ -116,6 +119,9 @@ export default function Home() {
|
||||
}
|
||||
|
||||
function handleStreamEvent(event: OptimizationStreamEvent) {
|
||||
setArchitectureEvents((current) => event.type === "job_created"
|
||||
? [event]
|
||||
: [...current, event]);
|
||||
switch (event.type) {
|
||||
case "job_created":
|
||||
setJobId(event.job.id);
|
||||
@@ -157,6 +163,12 @@ export default function Home() {
|
||||
: "优化完成。",
|
||||
);
|
||||
break;
|
||||
case "llm_call_started":
|
||||
case "llm_call_responded":
|
||||
case "llm_call_validated":
|
||||
case "llm_call_failed":
|
||||
case "trace_warning":
|
||||
break;
|
||||
case "failed":
|
||||
setStreamActivity("优化失败");
|
||||
throw new Error(event.error);
|
||||
@@ -200,6 +212,13 @@ export default function Home() {
|
||||
>
|
||||
普通文案优化
|
||||
</button>
|
||||
<button
|
||||
className={activeTab === "architecture" ? "active-tab" : undefined}
|
||||
onClick={() => setActiveTab("architecture")}
|
||||
type="button"
|
||||
>
|
||||
后台架构
|
||||
</button>
|
||||
</nav>
|
||||
{activeTab === "geo" ? (
|
||||
<>
|
||||
@@ -237,10 +256,16 @@ export default function Home() {
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
) : activeTab === "copy" ? (
|
||||
<RenweiCopyOptimizerPanel
|
||||
apiAccessKey={apiAccessKey}
|
||||
/>
|
||||
) : (
|
||||
<ArchitectureObserverPanel
|
||||
apiAccessKey={apiAccessKey}
|
||||
currentJobId={jobId}
|
||||
liveEvents={architectureEvents}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
LlmTraceRun,
|
||||
} from "../../../lib/llm/trace-types";
|
||||
import {
|
||||
applyLiveOptimizationEvent,
|
||||
applyLiveTraceEvent,
|
||||
deriveArchitectureNodes,
|
||||
formatCallLabel,
|
||||
@@ -144,4 +145,49 @@ describe("architecture trace state", () => {
|
||||
});
|
||||
expect(manifest.calls[0].status).toBe("validated");
|
||||
});
|
||||
|
||||
it("uses workflow events to finish the run and retain QA business status", () => {
|
||||
const withQa = applyLiveOptimizationEvent(
|
||||
{ run, calls: [call({ business_status: null })] },
|
||||
{
|
||||
type: "qa_ready",
|
||||
job_id: "job_1",
|
||||
qa_report: { overall_status: "fail", checks: [] } as never,
|
||||
},
|
||||
);
|
||||
const completed = applyLiveOptimizationEvent(withQa, {
|
||||
type: "final_ready",
|
||||
job_id: "job_1",
|
||||
optimized_article: {} as never,
|
||||
qa_report: { overall_status: "fail", checks: [] } as never,
|
||||
export_paths: {},
|
||||
});
|
||||
|
||||
expect(withQa.calls[0].business_status).toBe("fail");
|
||||
expect(completed.run).toMatchObject({
|
||||
status: "completed",
|
||||
current_stage: "final",
|
||||
});
|
||||
expect(deriveArchitectureNodes(completed.run, completed.calls).final.status)
|
||||
.toBe("completed");
|
||||
});
|
||||
|
||||
it("uses workflow failure events to mark the matching architecture stage", () => {
|
||||
const failed = applyLiveOptimizationEvent(
|
||||
{ run: { ...run, current_stage: "draft" }, calls: [] },
|
||||
{
|
||||
type: "failed",
|
||||
job_id: "job_1",
|
||||
stage: "qa",
|
||||
error: "质量检查失败",
|
||||
},
|
||||
);
|
||||
|
||||
expect(failed.run).toMatchObject({
|
||||
status: "failed",
|
||||
current_stage: "qa",
|
||||
error_stage: "qa",
|
||||
error_summary: "质量检查失败",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,13 +4,13 @@ import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import type {
|
||||
LlmTraceManifest,
|
||||
LlmTraceStreamEvent,
|
||||
} from "../../lib/llm/trace-types";
|
||||
import type { OptimizationStreamEvent } from "../../lib/workflow/stream-events";
|
||||
import { getJobTrace, getLatestTrace } from "./api-client";
|
||||
import { ArchitectureFlow } from "./architecture-flow";
|
||||
import { LlmCallDetail } from "./llm-call-detail";
|
||||
import {
|
||||
applyLiveTraceEvent,
|
||||
applyLiveOptimizationEvent,
|
||||
deriveArchitectureNodes,
|
||||
formatCallLabel,
|
||||
formatNodeStatus,
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
interface ArchitectureObserverPanelProps {
|
||||
apiAccessKey: string;
|
||||
currentJobId: string | null;
|
||||
liveEvents: LlmTraceStreamEvent[];
|
||||
liveEvents: OptimizationStreamEvent[];
|
||||
}
|
||||
|
||||
interface ManifestLoadState {
|
||||
@@ -90,7 +90,7 @@ export function ArchitectureObserverPanel({
|
||||
|
||||
const manifest = useMemo(() => {
|
||||
if (loadState?.key !== loadKey || !loadState.manifest) return null;
|
||||
return liveEvents.reduce(applyLiveTraceEvent, loadState.manifest);
|
||||
return liveEvents.reduce(applyLiveOptimizationEvent, loadState.manifest);
|
||||
}, [liveEvents, loadKey, loadState]);
|
||||
|
||||
if (loadState?.key !== loadKey) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
LlmTraceStreamEvent,
|
||||
LlmTraceWorkflowStage,
|
||||
} from "../../lib/llm/trace-types";
|
||||
import type { OptimizationStreamEvent } from "../../lib/workflow/stream-events";
|
||||
|
||||
export type ArchitectureNodeStatus =
|
||||
| "waiting"
|
||||
@@ -183,6 +184,87 @@ export function applyLiveTraceEvent(
|
||||
};
|
||||
}
|
||||
|
||||
function isTraceEvent(
|
||||
event: OptimizationStreamEvent,
|
||||
): event is LlmTraceStreamEvent {
|
||||
return event.type === "llm_call_started"
|
||||
|| event.type === "llm_call_responded"
|
||||
|| event.type === "llm_call_validated"
|
||||
|| event.type === "llm_call_failed"
|
||||
|| event.type === "trace_warning";
|
||||
}
|
||||
|
||||
function workflowStageForEvent(
|
||||
event: OptimizationStreamEvent,
|
||||
): LlmTraceWorkflowStage | null {
|
||||
if (event.type === "fact_card_ready") return "fact_card";
|
||||
if (event.type === "draft_started" || event.type === "draft_ready") return "draft";
|
||||
if (event.type === "qa_started" || event.type === "qa_ready") return "qa";
|
||||
if (event.type === "rewrite_started" || event.type === "rewrite_ready") return "rewrite";
|
||||
if (event.type === "final_ready") return "final";
|
||||
if (event.type === "failed") {
|
||||
return event.stage === "job" ? "input" : event.stage;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function applyLiveOptimizationEvent(
|
||||
state: LlmTraceManifest,
|
||||
event: OptimizationStreamEvent,
|
||||
): LlmTraceManifest {
|
||||
if (isTraceEvent(event)) return applyLiveTraceEvent(state, event);
|
||||
if (event.type === "job_created") return state;
|
||||
if (event.job_id !== state.run.job_id) return state;
|
||||
|
||||
const now = new Date().toISOString();
|
||||
if (event.type === "qa_ready") {
|
||||
const qaCalls = state.calls.filter((call) => call.task === "quality_inspector");
|
||||
const latestQaCall = qaCalls.at(-1);
|
||||
return {
|
||||
run: { ...state.run, current_stage: "qa", updated_at: now },
|
||||
calls: latestQaCall
|
||||
? updateCall(state.calls, latestQaCall.call_id, (call) => ({
|
||||
...call,
|
||||
business_status: event.qa_report.overall_status,
|
||||
}))
|
||||
: state.calls,
|
||||
};
|
||||
}
|
||||
|
||||
const stage = workflowStageForEvent(event);
|
||||
if (!stage) return state;
|
||||
if (event.type === "final_ready") {
|
||||
return {
|
||||
...state,
|
||||
run: {
|
||||
...state.run,
|
||||
status: "completed",
|
||||
current_stage: "final",
|
||||
finished_at: now,
|
||||
updated_at: now,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (event.type === "failed") {
|
||||
return {
|
||||
...state,
|
||||
run: {
|
||||
...state.run,
|
||||
status: "failed",
|
||||
current_stage: stage,
|
||||
error_stage: event.stage,
|
||||
error_summary: event.error,
|
||||
finished_at: now,
|
||||
updated_at: now,
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
run: { ...state.run, current_stage: stage, updated_at: now },
|
||||
};
|
||||
}
|
||||
|
||||
function callDetail(call: LlmTraceCallPublic) {
|
||||
if (call.status === "failed" || call.schema_valid === false) {
|
||||
if (call.error_type === "provider") return "模型服务调用失败";
|
||||
|
||||
Reference in New Issue
Block a user