新增LLM后台架构观测界面
This commit is contained in:
+283
-1
@@ -222,6 +222,278 @@ h3 {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.architecture-observer {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.architecture-task-strip {
|
||||
align-items: center;
|
||||
background: #172033;
|
||||
border-radius: 8px;
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: space-between;
|
||||
padding: 0.85rem 1rem;
|
||||
}
|
||||
|
||||
.architecture-task-strip > div:first-child {
|
||||
display: grid;
|
||||
gap: 0.2rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.architecture-task-strip strong,
|
||||
.architecture-task-strip small {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.architecture-task-strip small,
|
||||
.architecture-eyebrow {
|
||||
color: #cbd3df;
|
||||
}
|
||||
|
||||
.architecture-eyebrow {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.architecture-task-meta {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.architecture-task-meta > span {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
border-radius: 999px;
|
||||
font-size: 0.78rem;
|
||||
padding: 0.35rem 0.6rem;
|
||||
}
|
||||
|
||||
.technical-detail-toggle {
|
||||
align-items: center;
|
||||
background: #ffffff;
|
||||
border-radius: 6px;
|
||||
color: #172033;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
gap: 0.45rem;
|
||||
padding: 0.4rem 0.6rem;
|
||||
}
|
||||
|
||||
.technical-detail-toggle input {
|
||||
accent-color: #2f6fdd;
|
||||
height: 1rem;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 1rem;
|
||||
}
|
||||
|
||||
.technical-detail-toggle span {
|
||||
color: inherit;
|
||||
font-size: 0.76rem;
|
||||
}
|
||||
|
||||
.architecture-warning {
|
||||
background: #fff7e6;
|
||||
border: 1px solid #f2c879;
|
||||
border-radius: 8px;
|
||||
color: #78520a;
|
||||
margin: 0;
|
||||
padding: 0.75rem 0.9rem;
|
||||
}
|
||||
|
||||
.architecture-flow-panel {
|
||||
background: #ffffff;
|
||||
border: 1px solid #dce2eb;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.architecture-flow {
|
||||
align-items: stretch;
|
||||
display: flex;
|
||||
min-width: 62rem;
|
||||
}
|
||||
|
||||
.architecture-flow-step {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex: 1 0 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.architecture-node {
|
||||
border: 1px solid #cbd3df;
|
||||
border-radius: 8px;
|
||||
display: grid;
|
||||
flex: 1 0 0;
|
||||
gap: 0.3rem;
|
||||
min-height: 8.5rem;
|
||||
min-width: 8.5rem;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.architecture-node > span:not(.architecture-node-index),
|
||||
.architecture-node small {
|
||||
color: #586174;
|
||||
font-size: 0.76rem;
|
||||
}
|
||||
|
||||
.architecture-node-index {
|
||||
align-items: center;
|
||||
background: #eef1f6;
|
||||
border-radius: 999px;
|
||||
display: flex;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 800;
|
||||
height: 1.5rem;
|
||||
justify-content: center;
|
||||
width: 1.5rem;
|
||||
}
|
||||
|
||||
.architecture-node-running {
|
||||
background: #eef5ff;
|
||||
border-color: #2f6fdd;
|
||||
box-shadow: 0 0 0 2px rgba(47, 111, 221, 0.1);
|
||||
}
|
||||
|
||||
.architecture-node-completed {
|
||||
background: #effaf4;
|
||||
border-color: #55a47a;
|
||||
}
|
||||
|
||||
.architecture-node-failed {
|
||||
background: #fff1f1;
|
||||
border-color: #d05858;
|
||||
}
|
||||
|
||||
.architecture-node-skipped {
|
||||
background: #f6f7f9;
|
||||
border-style: dashed;
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.architecture-arrow {
|
||||
color: #8992a3;
|
||||
flex: 0 0 1.4rem;
|
||||
font-size: 1.1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.architecture-loop-label {
|
||||
color: #586174;
|
||||
font-size: 0.78rem;
|
||||
margin: 0.75rem 0 0;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.architecture-workspace {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
grid-template-columns: minmax(16rem, 0.7fr) minmax(0, 1.3fr);
|
||||
}
|
||||
|
||||
.llm-call-list,
|
||||
.llm-call-detail {
|
||||
min-height: 26rem;
|
||||
}
|
||||
|
||||
.llm-call-list {
|
||||
align-content: start;
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.llm-call-list .panel-heading {
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.llm-call-list button {
|
||||
background: #ffffff;
|
||||
border-color: #dce2eb;
|
||||
color: #172033;
|
||||
display: grid;
|
||||
gap: 0.25rem;
|
||||
min-width: 0;
|
||||
padding: 0.7rem;
|
||||
text-align: left;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.llm-call-list button.selected {
|
||||
border-color: #2f6fdd;
|
||||
box-shadow: 0 0 0 2px rgba(47, 111, 221, 0.1);
|
||||
}
|
||||
|
||||
.llm-call-list code,
|
||||
.llm-call-list small {
|
||||
color: #586174;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.llm-call-detail {
|
||||
display: grid;
|
||||
grid-template-rows: auto auto minmax(18rem, 1fr);
|
||||
}
|
||||
|
||||
.llm-detail-heading,
|
||||
.llm-detail-tabs {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.llm-detail-heading {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.llm-detail-heading > div {
|
||||
display: grid;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.llm-detail-heading small {
|
||||
color: #586174;
|
||||
}
|
||||
|
||||
.llm-detail-tabs {
|
||||
border-bottom: 1px solid #dce2eb;
|
||||
margin-top: 0.8rem;
|
||||
}
|
||||
|
||||
.llm-detail-tabs button {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-bottom: 2px solid transparent;
|
||||
border-radius: 0;
|
||||
color: #586174;
|
||||
}
|
||||
|
||||
.llm-detail-tabs button.active-tab {
|
||||
border-bottom-color: #2f6fdd;
|
||||
color: #172033;
|
||||
}
|
||||
|
||||
.llm-json-view {
|
||||
background: #111827;
|
||||
border-radius: 6px;
|
||||
color: #dbeafe;
|
||||
font-size: 0.78rem;
|
||||
margin: 0.8rem 0 0;
|
||||
overflow: auto;
|
||||
padding: 1rem;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.panel-actions {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
@@ -769,10 +1041,20 @@ h3 {
|
||||
.calibration-metrics,
|
||||
.case-toolbar,
|
||||
.case-detail-grid,
|
||||
.case-meta-grid {
|
||||
.case-meta-grid,
|
||||
.architecture-workspace {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.architecture-task-strip {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.architecture-task-meta {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.case-row,
|
||||
.case-row-heading {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
@@ -8,6 +8,8 @@ import type {
|
||||
import {
|
||||
applyLiveTraceEvent,
|
||||
deriveArchitectureNodes,
|
||||
formatCallLabel,
|
||||
formatNodeStatus,
|
||||
} from "../trace-state";
|
||||
|
||||
const run: LlmTraceRun = {
|
||||
@@ -55,6 +57,17 @@ function call(
|
||||
}
|
||||
|
||||
describe("architecture trace state", () => {
|
||||
it("formats every architecture node and call label in Chinese", () => {
|
||||
expect(formatNodeStatus("running")).toBe("运行中");
|
||||
expect(formatNodeStatus("failed")).toBe("失败");
|
||||
expect(formatCallLabel(call({
|
||||
sequence: 4,
|
||||
task: "targeted_rewriter",
|
||||
rewrite_round: 1,
|
||||
duration_ms: 22600,
|
||||
}))).toBe("4 · 定向修复第 1 轮 · 22.6 秒");
|
||||
});
|
||||
|
||||
it("distinguishes QA business failure from schema failure", () => {
|
||||
const nodes = deriveArchitectureNodes(run, [
|
||||
call({ schema_valid: true, business_status: "fail" }),
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
formatNodeStatus,
|
||||
type ArchitectureNodeView,
|
||||
} from "./trace-state";
|
||||
|
||||
export function ArchitectureFlow({
|
||||
nodes,
|
||||
}: {
|
||||
nodes: Record<ArchitectureNodeView["id"], ArchitectureNodeView>;
|
||||
}) {
|
||||
const order: ArchitectureNodeView["id"][] = [
|
||||
"input",
|
||||
"fact_card",
|
||||
"draft",
|
||||
"qa",
|
||||
"rewrite",
|
||||
"final",
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="architecture-flow-panel" aria-label="文章优化后台架构">
|
||||
<div className="architecture-flow">
|
||||
{order.map((id, index) => {
|
||||
const node = nodes[id];
|
||||
return (
|
||||
<div className="architecture-flow-step" key={node.id}>
|
||||
<article
|
||||
className={`architecture-node architecture-node-${node.status}`}
|
||||
data-node={node.id}
|
||||
>
|
||||
<span className="architecture-node-index">{index + 1}</span>
|
||||
<strong>{node.label}</strong>
|
||||
<span>{formatNodeStatus(node.status)}</span>
|
||||
<small>{node.detail}</small>
|
||||
</article>
|
||||
{index < order.length - 1 && (
|
||||
<span aria-hidden="true" className="architecture-arrow">→</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p className="architecture-loop-label">
|
||||
质量检查未通过时,定向修复后返回复检,最多两轮。
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import type {
|
||||
LlmTraceManifest,
|
||||
LlmTraceStreamEvent,
|
||||
} from "../../lib/llm/trace-types";
|
||||
import { getJobTrace, getLatestTrace } from "./api-client";
|
||||
import { ArchitectureFlow } from "./architecture-flow";
|
||||
import { LlmCallDetail } from "./llm-call-detail";
|
||||
import {
|
||||
applyLiveTraceEvent,
|
||||
deriveArchitectureNodes,
|
||||
formatCallLabel,
|
||||
formatNodeStatus,
|
||||
} from "./trace-state";
|
||||
|
||||
interface ArchitectureObserverPanelProps {
|
||||
apiAccessKey: string;
|
||||
currentJobId: string | null;
|
||||
liveEvents: LlmTraceStreamEvent[];
|
||||
}
|
||||
|
||||
interface ManifestLoadState {
|
||||
key: string;
|
||||
status: "loaded" | "empty" | "error";
|
||||
manifest: LlmTraceManifest | null;
|
||||
error: string;
|
||||
}
|
||||
|
||||
function loadError(error: unknown) {
|
||||
return error instanceof Error ? error.message : "后台追踪加载失败";
|
||||
}
|
||||
|
||||
function runStatusLabel(status: LlmTraceManifest["run"]["status"]) {
|
||||
if (status === "running") return "运行中";
|
||||
if (status === "completed") return "已完成";
|
||||
if (status === "interrupted") return "已中断";
|
||||
return "失败";
|
||||
}
|
||||
|
||||
export function ArchitectureObserverPanel({
|
||||
apiAccessKey,
|
||||
currentJobId,
|
||||
liveEvents,
|
||||
}: ArchitectureObserverPanelProps) {
|
||||
const [loadState, setLoadState] = useState<ManifestLoadState | null>(null);
|
||||
const [selectedCallId, setSelectedCallId] = useState<string | null>(null);
|
||||
const [technicalDetailsEnabled, setTechnicalDetailsEnabled] = useState(false);
|
||||
const loadKey = `${currentJobId ?? "latest"}:${apiAccessKey}`;
|
||||
|
||||
useEffect(() => {
|
||||
let current = true;
|
||||
const request = currentJobId
|
||||
? getJobTrace(currentJobId, apiAccessKey)
|
||||
: getLatestTrace(apiAccessKey);
|
||||
request
|
||||
.then((result) => {
|
||||
if (!current) return;
|
||||
if (result.run === null) {
|
||||
setLoadState({
|
||||
key: loadKey,
|
||||
status: "empty",
|
||||
manifest: null,
|
||||
error: "",
|
||||
});
|
||||
return;
|
||||
}
|
||||
setLoadState({
|
||||
key: loadKey,
|
||||
status: "loaded",
|
||||
manifest: result,
|
||||
error: "",
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
if (!current) return;
|
||||
setLoadState({
|
||||
key: loadKey,
|
||||
status: "error",
|
||||
manifest: null,
|
||||
error: loadError(error),
|
||||
});
|
||||
});
|
||||
return () => {
|
||||
current = false;
|
||||
};
|
||||
}, [apiAccessKey, currentJobId, loadKey]);
|
||||
|
||||
const manifest = useMemo(() => {
|
||||
if (loadState?.key !== loadKey || !loadState.manifest) return null;
|
||||
return liveEvents.reduce(applyLiveTraceEvent, loadState.manifest);
|
||||
}, [liveEvents, loadKey, loadState]);
|
||||
|
||||
if (loadState?.key !== loadKey) {
|
||||
return <section className="panel empty-panel">正在读取后台架构…</section>;
|
||||
}
|
||||
if (loadState.status === "error") {
|
||||
return (
|
||||
<section className="panel empty-panel">
|
||||
<strong>无法读取后台追踪</strong>
|
||||
<p>{loadState.error}</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
if (loadState.status === "empty" || !manifest) {
|
||||
return (
|
||||
<section className="panel empty-panel">
|
||||
暂无后台追踪。请先在“GEO 文章优化”标签运行一次优化。
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
const nodes = deriveArchitectureNodes(manifest.run, manifest.calls);
|
||||
const selectedCall = manifest.calls.find(
|
||||
(call) => call.call_id === selectedCallId,
|
||||
) ?? manifest.calls[0] ?? null;
|
||||
const latestCall = manifest.calls.at(-1) ?? null;
|
||||
const isCurrentTask = currentJobId === manifest.run.job_id;
|
||||
|
||||
return (
|
||||
<section className="architecture-observer stack">
|
||||
<header className="architecture-task-strip">
|
||||
<div>
|
||||
<span className="architecture-eyebrow">
|
||||
{isCurrentTask ? "当前任务" : "最近一次任务"}
|
||||
</span>
|
||||
<strong>{manifest.run.job_id}</strong>
|
||||
<small>
|
||||
{latestCall
|
||||
? `${latestCall.provider} · ${latestCall.model}`
|
||||
: "尚未发起 LLM 调用"}
|
||||
</small>
|
||||
</div>
|
||||
<div className="architecture-task-meta">
|
||||
<span>{runStatusLabel(manifest.run.status)}</span>
|
||||
<span>{formatNodeStatus(nodes[manifest.run.current_stage === "unknown" ? "input" : manifest.run.current_stage].status)}</span>
|
||||
<label className="technical-detail-toggle">
|
||||
<input
|
||||
checked={technicalDetailsEnabled}
|
||||
onChange={(event) => setTechnicalDetailsEnabled(event.target.checked)}
|
||||
type="checkbox"
|
||||
/>
|
||||
<span>技术详情</span>
|
||||
</label>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{manifest.run.trace_completeness === "incomplete" && (
|
||||
<p className="architecture-warning" role="status">
|
||||
追踪记录不完整:已保存的数据仍可查看,但部分阶段或正文可能缺失。
|
||||
</p>
|
||||
)}
|
||||
|
||||
<ArchitectureFlow nodes={nodes} />
|
||||
|
||||
<div className="architecture-workspace">
|
||||
<aside className="panel llm-call-list">
|
||||
<div className="panel-heading">LLM 调用轨迹</div>
|
||||
{manifest.calls.length === 0 ? (
|
||||
<p className="empty-panel">等待第一次 LLM 调用。</p>
|
||||
) : manifest.calls.map((call) => (
|
||||
<button
|
||||
className={selectedCall?.call_id === call.call_id ? "selected" : undefined}
|
||||
key={call.call_id}
|
||||
onClick={() => setSelectedCallId(call.call_id)}
|
||||
type="button"
|
||||
>
|
||||
<strong>{formatCallLabel(call)}</strong>
|
||||
<code>{call.task}</code>
|
||||
<small>{call.schema_valid === false ? "Schema 失败" : call.status}</small>
|
||||
</button>
|
||||
))}
|
||||
</aside>
|
||||
|
||||
{selectedCall ? (
|
||||
<LlmCallDetail
|
||||
apiAccessKey={apiAccessKey}
|
||||
call={selectedCall}
|
||||
jobId={manifest.run.job_id}
|
||||
key={selectedCall.call_id}
|
||||
technicalDetailsEnabled={technicalDetailsEnabled}
|
||||
/>
|
||||
) : (
|
||||
<section className="panel empty-panel">请选择一次 LLM 调用。</section>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import type { LlmTraceCallPublic } from "../../lib/llm/trace-types";
|
||||
import { getTracePayload } from "./api-client";
|
||||
|
||||
type DetailTab = "request" | "response" | "validation";
|
||||
|
||||
interface LoadedPayload {
|
||||
key: string;
|
||||
value: unknown;
|
||||
}
|
||||
|
||||
function detailTabLabel(tab: DetailTab) {
|
||||
if (tab === "request") return "请求";
|
||||
if (tab === "response") return "响应";
|
||||
return "校验";
|
||||
}
|
||||
|
||||
function errorMessage(error: unknown) {
|
||||
return error instanceof Error ? error.message : "追踪正文加载失败";
|
||||
}
|
||||
|
||||
function validationView(call: LlmTraceCallPublic) {
|
||||
return {
|
||||
schema_name: call.schema_name,
|
||||
schema_valid: call.schema_valid,
|
||||
validation_issues: call.validation_issues,
|
||||
business_status: call.business_status,
|
||||
error_type: call.error_type,
|
||||
error_summary: call.error_summary,
|
||||
duration_ms: call.duration_ms,
|
||||
token_usage: call.token_usage,
|
||||
};
|
||||
}
|
||||
|
||||
export function LlmCallDetail({
|
||||
jobId,
|
||||
call,
|
||||
apiAccessKey,
|
||||
technicalDetailsEnabled,
|
||||
}: {
|
||||
jobId: string;
|
||||
call: LlmTraceCallPublic;
|
||||
apiAccessKey: string;
|
||||
technicalDetailsEnabled: boolean;
|
||||
}) {
|
||||
const [tab, setTab] = useState<DetailTab>("validation");
|
||||
const [loadedPayload, setLoadedPayload] = useState<LoadedPayload | null>(null);
|
||||
const payloadKey = `${call.call_id}:${tab}`;
|
||||
|
||||
useEffect(() => {
|
||||
if (!technicalDetailsEnabled || tab === "validation") return;
|
||||
let current = true;
|
||||
getTracePayload(jobId, call.call_id, tab, apiAccessKey)
|
||||
.then((value) => {
|
||||
if (current) setLoadedPayload({ key: payloadKey, value });
|
||||
})
|
||||
.catch((error) => {
|
||||
if (current) {
|
||||
setLoadedPayload({
|
||||
key: payloadKey,
|
||||
value: { error: errorMessage(error) },
|
||||
});
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
current = false;
|
||||
};
|
||||
}, [apiAccessKey, call.call_id, jobId, payloadKey, tab, technicalDetailsEnabled]);
|
||||
|
||||
const displayValue = tab === "validation"
|
||||
? validationView(call)
|
||||
: !technicalDetailsEnabled
|
||||
? { state: "locked", message: "开启技术详情后按需读取完整正文。" }
|
||||
: loadedPayload?.key === payloadKey
|
||||
? loadedPayload.value
|
||||
: { state: "loading", message: "正在读取完整 JSON…" };
|
||||
|
||||
return (
|
||||
<section className="panel llm-call-detail" aria-live="polite">
|
||||
<div className="llm-detail-heading">
|
||||
<div>
|
||||
<strong>{call.task}</strong>
|
||||
<small>{call.provider} · {call.model}</small>
|
||||
</div>
|
||||
<span className={`status-pill ${call.status === "failed" ? "fail" : "pass"}`}>
|
||||
{call.status}
|
||||
</span>
|
||||
</div>
|
||||
<div className="llm-detail-tabs" role="tablist" aria-label="LLM 调用详情">
|
||||
{(["request", "response", "validation"] as const).map((value) => (
|
||||
<button
|
||||
aria-selected={tab === value}
|
||||
className={tab === value ? "active-tab" : undefined}
|
||||
key={value}
|
||||
onClick={() => setTab(value)}
|
||||
role="tab"
|
||||
type="button"
|
||||
>
|
||||
{detailTabLabel(value)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<pre className="llm-json-view">{JSON.stringify(displayValue, null, 2)}</pre>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -19,6 +19,37 @@ export interface ArchitectureNodeView {
|
||||
detail: string;
|
||||
}
|
||||
|
||||
export function formatNodeStatus(status: ArchitectureNodeStatus) {
|
||||
return {
|
||||
waiting: "等待中",
|
||||
running: "运行中",
|
||||
completed: "已完成",
|
||||
failed: "失败",
|
||||
}[status];
|
||||
}
|
||||
|
||||
export function formatTaskName(call: LlmTraceCallPublic) {
|
||||
if (call.task === "fact_extractor") return "事实提取";
|
||||
if (call.task === "article_optimizer") return "生成优化稿";
|
||||
if (call.task === "quality_inspector") {
|
||||
return call.rewrite_round && call.rewrite_round > 0
|
||||
? `质量复检第 ${call.rewrite_round} 轮`
|
||||
: "质量检查";
|
||||
}
|
||||
if (call.task === "targeted_rewriter") {
|
||||
return `定向修复第 ${call.rewrite_round ?? 1} 轮`;
|
||||
}
|
||||
if (call.task === "renwei_copy_optimizer") return "普通文案优化";
|
||||
return "未知调用";
|
||||
}
|
||||
|
||||
export function formatCallLabel(call: LlmTraceCallPublic) {
|
||||
const duration = call.duration_ms == null
|
||||
? "进行中"
|
||||
: `${(call.duration_ms / 1000).toFixed(1)} 秒`;
|
||||
return `${call.sequence} · ${formatTaskName(call)} · ${duration}`;
|
||||
}
|
||||
|
||||
const nodeDefinitions: Array<Pick<ArchitectureNodeView, "id" | "label">> = [
|
||||
{ id: "input", label: "输入归一化" },
|
||||
{ id: "fact_card", label: "事实提取" },
|
||||
|
||||
Reference in New Issue
Block a user