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