Remove AI config approval flow
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user