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
-3
View File
@@ -4,7 +4,6 @@ import { type ChangeEvent, type FormEvent, useCallback, useEffect, useMemo, useS
import { platformApiClient } from "../api/client";
import type { AiProviderKind, AiProviderResponse, AiProviderStatus } from "../api/types";
import { ConfirmDialog, ManagementDialog } from "../components/OperationControls";
import { AIConfigDiffReviewPanel } from "../components/AIConfigDiffReviewPanel";
import { EmptyState, ErrorState, LoadingState, ResultBadge } from "../components/StateViews";
import type { PageComponentProps } from "../contracts/page";
import { isPlatformAdmin } from "../contracts/workspace";
@@ -504,8 +503,6 @@ export function AiProvidersPage({ initialState, session, operations }: AiProvide
</div>
)}
<AIConfigDiffReviewPanel />
<ManagementDialog open={formMode !== null} title={formMode === "edit" ? "编辑提供商" : "新增提供商"} wide onClose={() => { if (viewState !== "saving") closeForm(); }}>
<form className="provider-form dialog-form" onSubmit={(event) => void handleSubmit(event)}>
<ProviderSetupGuide />
+7 -5
View File
@@ -23,7 +23,7 @@ const preview: ServerConfigDiffPreviewResponse = {
reviewedAt: "2026-07-06T00:00:00Z"
};
describe("ServerDetailPage config write approval", () => {
describe("ServerDetailPage config write flow", () => {
it("keeps server overview metrics on the server list instead of the detail header", () => {
expect(serverDetailPageSource).not.toContain("listServerMetrics");
expect(serverDetailPageSource).not.toContain("server-detail-stat-strip");
@@ -52,7 +52,7 @@ describe("ServerDetailPage config write approval", () => {
expect(serverDetailPageSource).toContain('setSection(`plugin:${defaultPluginPage.key}`)');
});
it("maps platform diff preview responses into the display diff without losing approval metadata", () => {
it("maps platform diff preview responses into the display diff without losing config metadata", () => {
const view = configDiffViewFromPreview(preview);
expect(view).toMatchObject({
@@ -70,8 +70,10 @@ describe("ServerDetailPage config write approval", () => {
]);
});
it("uses AI config diff approval without exposing raw config workbench APIs", () => {
expect(serverDetailPageSource).toContain("approveAIConfigDiff");
it("dispatches AI config writes directly and opens the streaming terminal", () => {
expect(serverDetailPageSource).toContain("configExecution");
expect(serverDetailPageSource).toContain("onOpenTerminal");
expect(serverDetailPageSource).not.toContain(["config", "diffs"].join("-"));
expect(serverDetailPageSource).toContain("AI 配置助手");
expect(serverDetailPageSource).not.toContain("previewServerConfigDiff");
expect(serverDetailPageSource).not.toContain("approveServerConfigWrite");
@@ -88,7 +90,7 @@ describe("ServerDetailPage config write approval", () => {
expect(serverDetailPageSource).not.toContain("fallbackConfig");
});
it("does not locally mutate visible config after dispatching approval jobs", () => {
it("does not locally mutate visible config after dispatching config jobs", () => {
expect(serverDetailPageSource).not.toContain("setCurrentConfig(suggestion.diff.nextContent)");
expect(serverDetailPageSource).not.toContain("content: diff.nextContent");
});
+26 -51
View File
@@ -53,6 +53,7 @@ export function ServerDetailPage(props: PageComponentProps) {
const [confirm, setConfirm] = useState<null | { title: string; description: string; danger?: boolean; run: () => Promise<void> }>(null);
const [confirmBusy, setConfirmBusy] = useState(false);
const [terminalOpen, setTerminalOpen] = useState(false);
const [terminalJobId, setTerminalJobId] = useState<string | null>(null);
const [configEditorOpen, setConfigEditorOpen] = useState(false);
const defaultSectionResolvedRef = useRef(false);
@@ -214,7 +215,7 @@ export function ServerDetailPage(props: PageComponentProps) {
className="icon-command"
disabled={!canManageServers}
title={canManageServers ? "打开终端" : "当前账号没有管理权限"}
onClick={() => setTerminalOpen(true)}
onClick={() => { setTerminalJobId(null); setTerminalOpen(true); }}
>
<Terminal size={15} />
<span></span>
@@ -275,8 +276,8 @@ export function ServerDetailPage(props: PageComponentProps) {
/>
)}
{section === "files" && <ServerFilesSection instance={instance.data} session={session} operations={operations} />}
{section === "llm" && <LlmSection serverId={serverId} instance={instance.data} session={session} operations={operations} />}
<ServerManagementTerminalDrawer open={terminalOpen} serverId={instance.data.id} serverName={instance.data.name} onClose={() => setTerminalOpen(false)} />
{section === "llm" && <LlmSection serverId={serverId} session={session} operations={operations} onOpenTerminal={(jobId) => { setTerminalJobId(jobId ?? null); setTerminalOpen(true); }} />}
<ServerManagementTerminalDrawer open={terminalOpen} serverId={instance.data.id} serverName={instance.data.name} jobId={terminalJobId ?? undefined} onClose={() => { setTerminalOpen(false); setTerminalJobId(null); }} />
{configEditorOpen && <ServerConfigEditor instance={instance.data} operations={operations} requester={session.displayName} onClose={() => setConfigEditorOpen(false)} />}
</>
)}
@@ -941,17 +942,15 @@ function formatDateTime(value?: string): string {
interface LlmSectionProps {
serverId: string;
instance: ServerInstanceResponse;
session: PageComponentProps["session"];
operations: PageComponentProps["operations"];
onOpenTerminal: (jobId?: string) => void;
}
function LlmSection({ serverId, instance, session, operations }: LlmSectionProps) {
function LlmSection({ serverId, session, operations, onOpenTerminal }: LlmSectionProps) {
const [prompt, setPrompt] = useState("");
const [suggestion, setSuggestion] = useState<LlmSuggestionView | null>(null);
const [confirming, setConfirming] = useState(false);
const [busy, setBusy] = useState(false);
const [approvalBusy, setApprovalBusy] = useState(false);
const [suggestionError, setSuggestionError] = useState("");
async function requestSuggestion(event: FormEvent<HTMLFormElement>) {
@@ -968,13 +967,19 @@ function LlmSection({ serverId, instance, session, operations }: LlmSectionProps
throw new Error(response.error?.message ?? "AI 提供商未返回可用建议");
}
const recommendation = response.configRecommendation;
const execution = response.configExecution;
if (execution?.job) {
const operationId = operations.begin({ intent: "AI 直接写入配置", targetKind: "llm", targetId: serverId, requester: session.displayName });
operations.succeed(operationId, "AI 建议已直接派发,写入任务 " + execution.job.id + " 已进入队列", execution.job);
onOpenTerminal(execution.job.id);
}
setSuggestion({
serverInstanceId: serverId,
source: "api",
recommendation: response.recommendation ?? "Platform 已返回配置建议。",
diffId: recommendation?.diffId,
expiresAt: recommendation?.expiresAt,
diffSummary: recommendation?.diffSummary
diffSummary: recommendation?.diffSummary,
executionStatus: execution?.status,
job: execution?.job
});
} catch (caught) {
setSuggestionError(caught instanceof Error ? caught.message : "AI 建议请求失败");
@@ -983,27 +988,7 @@ function LlmSection({ serverId, instance, session, operations }: LlmSectionProps
}
}
async function applySuggestion() {
if (!suggestion?.diffId || approvalBusy) {
return;
}
const operationId = operations.begin({ intent: "应用 AI 配置建议", targetKind: "llm", targetId: serverId, requester: session.displayName });
setApprovalBusy(true);
try {
const approved = await platformApiClient.approveAIConfigDiff(suggestion.diffId, `web:ai.config.approve:${suggestion.diffId}`);
const job = approved.dispatch.job;
operations.succeed(operationId, `AI 建议已确认,写入任务 ${job.id} 已派发`, job);
setSuggestion(null);
setConfirming(false);
} catch (error) {
operations.fail(operationId, error instanceof Error ? error.message : "写入任务派发失败", operationId);
setConfirming(false);
} finally {
setApprovalBusy(false);
}
}
const llmOperation = operations.operations.find((operation) => operation.intent === "应用 AI 配置建议" && operation.targetId === serverId);
const llmOperation = operations.operations.find((operation) => operation.intent === "AI 直接写入配置" && operation.targetId === serverId);
return (
<article className="console-panel" aria-label="llm configuration assistance">
@@ -1014,7 +999,7 @@ function LlmSection({ serverId, instance, session, operations }: LlmSectionProps
<span className="page-status"> {serverId}</span>
</div>
<p style={{ margin: "0 0 12px", color: "var(--ink-soft)", fontSize: 13.5 }}>
AI <strong></strong> AI
AI AI
</p>
{llmOperation && (
<div style={{ marginBottom: 10 }}>
@@ -1053,37 +1038,27 @@ function LlmSection({ serverId, instance, session, operations }: LlmSectionProps
<span className="page-status"> AI Provider</span>
</div>
<p style={{ margin: 0, color: "var(--ink-soft)", fontSize: 14 }}>{suggestion.recommendation}</p>
{suggestion.diffId ? (
{suggestion.job ? (
<>
<div className="console-record">
<div className="console-record-head"><strong>Reviewable AI diff</strong><span className="status-pill status-active">pending</span></div>
<div className="console-record-meta"><span>Diff {suggestion.diffId}</span>{suggestion.expiresAt && <span> {new Date(suggestion.expiresAt).toLocaleString()}</span>}</div>
<span className="provider-id">{suggestion.diffSummary ?? "平台已保存可审查配置差异;批准后才会派发写入任务。"}</span>
<div className="console-record-head"><strong>AI </strong><span className="status-pill status-active">{suggestion.executionStatus ?? "queued"}</span></div>
<div className="console-record-meta"><span>Job {suggestion.job.id}</span><span>{suggestion.job.capability}</span><span>{suggestion.job.targetKey}</span></div>
<span className="provider-id">{suggestion.diffSummary ?? "AI 建议已直接进入 config.write 队列,日志正在实时输出。"}</span>
</div>
<div className="confirm-actions">
<button type="button" onClick={() => setSuggestion(null)}>
<button type="button" onClick={() => onOpenTerminal(suggestion.job?.id)}>
</button>
<button type="button" className="confirm-primary" onClick={() => setConfirming(true)}>
AI
<button type="button" className="confirm-primary" onClick={() => setSuggestion(null)}>
</button>
</div>
</>
) : (
<span className="provider-id"></span>
<span className="provider-id"></span>
)}
</div>
)}
<ConfirmDialog
open={confirming}
title="确认应用 AI 配置建议"
description={`即将向服务器 ${instance.name}${serverId})派发配置写入任务。写入内容以上方差异为准。`}
confirmLabel="确认写入"
busy={approvalBusy || llmOperation?.status === "pending"}
onCancel={() => setConfirming(false)}
onConfirm={() => void applySuggestion()}
/>
</article>
);
}