Remove AI config approval flow

This commit is contained in:
npc0-hue
2026-09-22 15:33:32 +08:00
parent 20008b4043
commit a8b5d483a3
54 changed files with 364 additions and 875 deletions
@@ -1,77 +0,0 @@
import { FileCheck2, RotateCw } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { platformApiClient } from "../api/client";
import type { AIConfigDiffPreviewResponse } from "../api/types";
import { ConfirmDialog } from "./OperationControls";
import { ErrorState, LoadingState, ResultBadge } from "./StateViews";
export function AIConfigDiffReviewPanel() {
const [items, setItems] = useState<AIConfigDiffPreviewResponse[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [selected, setSelected] = useState<AIConfigDiffPreviewResponse | null>(null);
const [busyId, setBusyId] = useState("");
const [result, setResult] = useState<{ status: "succeeded" | "failed"; label: string } | null>(null);
const refresh = useCallback(async () => {
setLoading(true);
setError("");
try {
const response = await platformApiClient.listAIConfigDiffs();
setItems(response.items);
} catch (caught) {
setError(caught instanceof Error ? caught.message : "AI 配置审查队列加载失败");
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
void refresh();
}, [refresh]);
async function approve() {
if (!selected || busyId) return;
setBusyId(selected.id);
setResult(null);
try {
const response = await platformApiClient.approveAIConfigDiff(selected.id, `web:ai.config.approve:${selected.id}`);
setResult({ status: "succeeded", label: `已审批 ${response.preview.id} · 写入任务 ${response.dispatch.job.id}` });
setSelected(null);
await refresh();
} catch (caught) {
setResult({ status: "failed", label: caught instanceof Error ? caught.message : "AI 配置审批失败" });
setSelected(null);
} finally {
setBusyId("");
}
}
return (
<section className="console-panel console-module ai-diff-review-panel" aria-label="AI config diff review">
<div className="panel-header">
<h2><FileCheck2 size={16} /> AI </h2>
<button type="button" className="icon-command" disabled={loading || Boolean(busyId)} onClick={() => void refresh()}><RotateCw size={14} /><span></span></button>
</div>
{result && <ResultBadge status={result.status} label={result.label} />}
{loading && <LoadingState label="正在同步 AI 配置差异…" compact />}
{!loading && error && <ErrorState title="AI 配置审查不可用" reason={error} diagnosticId="ai-config-diffs" onRetry={() => void refresh()} compact />}
{!loading && !error && (
<div className="console-record-list">
{items.length === 0 && <p className="console-empty-note"> AI </p>}
{items.slice(0, 12).map((item) => (
<div key={item.id} className="console-record">
<div className="console-record-head"><strong>{item.serverInstanceId} · {item.key}</strong><span className={`status-pill status-${item.state === "approved" ? "succeeded" : item.state === "pending" ? "warning" : "disabled"}`}>{item.state}</span></div>
<div className="console-record-meta"><span> {item.requestId}</span><span> {item.configVersion}</span><span>{item.model || "Platform model"}</span><span> {new Date(item.expiresAt).toLocaleString()}</span>{item.jobId && <span> {item.jobId}</span>}</div>
<p>{item.diffSummary}</p>
{item.proposedConfig && <pre className="log-view ai-config-proposal">{item.proposedConfig}</pre>}
{item.state === "pending" && <div className="row-actions console-row-actions"><button type="button" disabled={Boolean(busyId)} onClick={() => setSelected(item)}><FileCheck2 size={14} /><span></span></button></div>}
</div>
))}
</div>
)}
<ConfirmDialog open={selected !== null} title="批准 AI 配置差异" description={selected ? `服务器 ${selected.serverInstanceId},配置版本 ${selected.configVersion},差异 ${selected.id}` : "确认 AI 配置差异。"} confirmLabel="批准并派发" busy={Boolean(busyId)} onCancel={() => { if (!busyId) setSelected(null); }} onConfirm={() => void approve()} />
</section>
);
}
@@ -1,21 +1,15 @@
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vitest";
import { AIConfigDiffReviewPanel } from "./AIConfigDiffReviewPanel";
import { PluginLifecycleWorkbench } from "./PluginLifecycleWorkbench";
import lifecycleSource from "./PluginLifecycleWorkbench.tsx?raw";
import diffSource from "./AIConfigDiffReviewPanel.tsx?raw";
describe("plugin operations components", () => {
it("renders persisted loading states without optimistic terminal success", () => {
expect(renderToStaticMarkup(<PluginLifecycleWorkbench pluginId="game.example" pluginName="Example" />)).toContain("正在同步插件生命周期");
expect(renderToStaticMarkup(<AIConfigDiffReviewPanel />)).toContain("正在同步 AI 配置差异");
for (const source of [lifecycleSource, diffSource]) {
expect(source).not.toContain("setTimeout");
expect(source).not.toMatch(/apiKeyRef|rawApiKey|runSocket|providerBaseUrl|hostPath|directRun/i);
expect(source).toContain("disabled=");
}
expect(lifecycleSource).not.toContain("setTimeout");
expect(lifecycleSource).not.toMatch(/apiKeyRef|rawApiKey|runSocket|providerBaseUrl|hostPath|directRun/i);
expect(lifecycleSource).toContain("disabled=");
expect(lifecycleSource).toContain("if (!selectedServerId || busy) return");
expect(diffSource).toContain("if (!selected || busyId) return");
});
});
@@ -228,7 +228,7 @@ async function renderDrawer() {
await act(async () => {
root?.render(<ServerManagementTerminalDrawer open serverId="server-1" serverName="SCUM Alpha" onClose={() => undefined} />);
});
expect(apiMocks.openServerLogEvents).toHaveBeenCalledWith("server-1");
expect(apiMocks.openServerLogEvents).toHaveBeenCalledWith("server-1", { jobId: undefined });
}
async function emitSession(logSessionId?: string, serverTime = "2026-08-14T00:00:00Z") {
@@ -75,10 +75,11 @@ interface ServerManagementTerminalDrawerProps {
open: boolean;
serverId: string;
serverName: string;
jobId?: string;
onClose: () => void;
}
export function ServerManagementTerminalDrawer({ open, serverId, serverName, onClose }: ServerManagementTerminalDrawerProps) {
export function ServerManagementTerminalDrawer({ open, serverId, serverName, jobId, onClose }: ServerManagementTerminalDrawerProps) {
const [command, setCommand] = useState("");
const [pending, setPending] = useState(false);
const [result, setResult] = useState<{ status: "pending" | "succeeded" | "failed"; label: string } | null>(null);
@@ -140,7 +141,7 @@ export function ServerManagementTerminalDrawer({ open, serverId, serverName, onC
followLatestRef.current = true;
setFollowLatest(true);
setLines([terminalSystemLine("info", "正在连接当前受管进程输出。", "SYSTEM", undefined, serverTimeRef.current)]);
}, [open]);
}, [jobId, open]);
useEffect(() => {
if (!open || initialHistoryPendingRef.current || !followLatestRef.current) return undefined;
@@ -153,7 +154,7 @@ export function ServerManagementTerminalDrawer({ open, serverId, serverName, onC
const hydrateCurrentSessionHistory = useCallback((sessionId: string | null | undefined) => {
if (!sessionId) return;
const streamsToHydrate = liveStreamsRef.current.filter((stream) => eventBelongsToLiveSession(stream.logSessionId, sessionId) && stream.latestSeq > 0 && !hydratedLiveStreamKeysRef.current.has(liveHistoryStreamKey(sessionId, stream)));
const streamsToHydrate = liveStreamsRef.current.filter((stream) => (jobId ? stream.id.startsWith(`job.${jobId}.`) : eventBelongsToLiveSession(stream.logSessionId, sessionId)) && stream.latestSeq > 0 && !hydratedLiveStreamKeysRef.current.has(liveHistoryStreamKey(sessionId, stream)));
if (streamsToHydrate.length === 0) return;
const requestId = liveHistoryRequestRef.current + 1;
liveHistoryRequestRef.current = requestId;
@@ -172,12 +173,12 @@ export function ServerManagementTerminalDrawer({ open, serverId, serverName, onC
appendLines([terminalSystemLine("warn", "当前会话历史读取失败,继续等待实时输出。", "SYSTEM", `session-history-failed-${sessionId}`, serverTimeRef.current)]);
lockTerminalFollow();
});
}, [appendLines, lockTerminalFollow]);
}, [appendLines, jobId, lockTerminalFollow]);
useEffect(() => {
if (!open) return undefined;
let ready = false;
const events = platformApiClient.openServerLogEvents(serverId);
const events = platformApiClient.openServerLogEvents(serverId, { jobId });
events.addEventListener("session", (event) => {
const session = parseLogSessionEvent(event);
if (!session) return;
@@ -199,7 +200,7 @@ export function ServerManagementTerminalDrawer({ open, serverId, serverName, onC
});
events.addEventListener("stream", (event) => {
const stream = parseLogStreamEvent(event);
if (!stream || !eventBelongsToLiveSession(stream.logSessionId, liveSessionRef.current)) return;
if (!stream || (jobId ? !stream.id.startsWith(`job.${jobId}.`) : !eventBelongsToLiveSession(stream.logSessionId, liveSessionRef.current))) return;
ready = true;
liveStreamsRef.current = mergeLogStreams(liveStreamsRef.current, stream);
setStreams((current) => ({ status: "ready", data: mergeLogStreams(current.status === "ready" ? current.data : [], stream) }));
@@ -212,7 +213,7 @@ export function ServerManagementTerminalDrawer({ open, serverId, serverName, onC
});
events.addEventListener("log", (event) => {
const payload = parseServerLogEvent(event);
if (!payload || !eventBelongsToLiveSession(payload.logSessionId, liveSessionRef.current)) return;
if (!payload || (jobId ? !payload.streamId.startsWith(`job.${jobId}.`) : !eventBelongsToLiveSession(payload.logSessionId, liveSessionRef.current))) return;
ready = true;
const stream = streamFromServerLogEvent(payload);
liveStreamsRef.current = mergeLogStreams(liveStreamsRef.current, stream);
@@ -223,19 +224,20 @@ export function ServerManagementTerminalDrawer({ open, serverId, serverName, onC
if (!ready) setStreams({ status: "error", reason: "实时日志推送连接失败" });
};
return () => events.close();
}, [appendLines, hydrateCurrentSessionHistory, lockTerminalFollow, open, serverId]);
}, [appendLines, hydrateCurrentSessionHistory, jobId, lockTerminalFollow, open, serverId]);
useEffect(() => {
if (!open || !historyOpen) return;
let cancelled = false;
setHistoryStreams({ status: "loading" });
void platformApiClient.listLogStreams(serverId).then((response) => {
if (!cancelled) setHistoryStreams({ status: "ready", data: [...response.items].sort((left, right) => Date.parse(right.updatedAt) - Date.parse(left.updatedAt)) });
const items = jobId ? response.items.filter((stream) => stream.id.startsWith(`job.${jobId}.`)) : response.items;
if (!cancelled) setHistoryStreams({ status: "ready", data: [...items].sort((left, right) => Date.parse(right.updatedAt) - Date.parse(left.updatedAt)) });
}).catch((error) => {
if (!cancelled) setHistoryStreams({ status: "error", reason: error instanceof Error ? error.message : "历史日志列表加载失败" });
});
return () => { cancelled = true; };
}, [historyOpen, open, serverId]);
}, [historyOpen, jobId, open, serverId]);
async function selectHistoryStream(streamId: string) {
const stream = historyStreams.status === "ready" ? historyStreams.data.find((item) => item.id === streamId) : undefined;
@@ -312,7 +314,7 @@ export function ServerManagementTerminalDrawer({ open, serverId, serverName, onC
<div className="terminal-output-topbar">
<div>
<strong>{serverName}</strong>
<span>{historyOpen ? "历史日志(独立于实时终端)" : `当前受管进程会话${liveSessionId ? " · SSE 实时推送" : " · 等待 Run 输出"}`} · {streams.status === "ready" ? "已连接" : streams.status === "loading" ? "连接日志流" : "日志流异常"} · {followLatest ? "自动置底" : "已解锁滚动"}</span>
<span>{historyOpen ? "历史日志(独立于实时终端)" : jobId ? `AI 写入任务 ${jobId} · ${liveSessionId ? "SSE 实时推送" : "等待 Run 输出"}` : `当前受管进程会话${liveSessionId ? " · SSE 实时推送" : " · 等待 Run 输出"}`} · {streams.status === "ready" ? "已连接" : streams.status === "loading" ? "连接日志流" : "日志流异常"} · {followLatest ? "自动置底" : "已解锁滚动"}</span>
</div>
<div>
<button type="button" className="terminal-output-action" onClick={clearTerminalBuffer}><Trash2 size={14} /><span></span></button>