功能修改
This commit is contained in:
@@ -26,6 +26,9 @@ import type {
|
||||
RemoteAdapterDeclarationResponse
|
||||
} from "../api/types";
|
||||
import { ConfirmDialog, DiffView, UsageMeter } from "../components/OperationControls";
|
||||
import { ClientManagerLifecyclePanel } from "../components/ClientManagerLifecyclePanel";
|
||||
import { ProductionGovernancePanel } from "../components/ProductionGovernancePanel";
|
||||
import { PluginLifecycleWorkbench } from "../components/PluginLifecycleWorkbench";
|
||||
import {
|
||||
RuntimeTaskProgressDialog,
|
||||
runtimeBuildStages,
|
||||
@@ -61,7 +64,7 @@ import {
|
||||
serverLifecycleCommandRequest,
|
||||
serverMetadataUpdateRequestFromForm
|
||||
} from "../schemas/serverManagement";
|
||||
import { buildConfigDiff, diffHasChanges } from "../utils/diff";
|
||||
import { diffHasChanges } from "../utils/diff";
|
||||
import { createPluginBridgeDispatcher, createPluginBridgeHostContext, parsePluginArtifactReference } from "../utils/pluginBridgeHost";
|
||||
import { downloadArtifactReference, safeArtifactError, safeArtifactFilename } from "../utils/artifactTransfer";
|
||||
import { cx } from "../utils/classes";
|
||||
@@ -300,6 +303,9 @@ export function ServerDetailPage({ session, params, operations, onNavigate }: Pa
|
||||
onChanged={() => void refresh()}
|
||||
/>
|
||||
)}
|
||||
{section === "overview" && (
|
||||
<ClientManagerLifecyclePanel serverId={instance.data.id} serverName={instance.data.name} session={session} operations={operations} />
|
||||
)}
|
||||
{section === "overview" && (
|
||||
<ServerMetadataSection
|
||||
instance={instance.data}
|
||||
@@ -312,7 +318,7 @@ export function ServerDetailPage({ session, params, operations, onNavigate }: Pa
|
||||
{section === "overview" && <ServerAdministratorsSection instance={instance.data} session={session} onChanged={(next) => setInstance({ status: "ready", data: next })} />}
|
||||
{section === "logs" && <LogsSection serverId={serverId} />}
|
||||
{section === "config" && <ConfigSection serverId={serverId} instance={instance.data} session={session} operations={operations} />}
|
||||
{section === "plugins" && <PluginControlsSection serverId={serverId} instance={instance.data} plugins={plugins} artifacts={artifacts} session={session} operations={operations} />}
|
||||
{section === "plugins" && <PluginControlsSection serverId={serverId} instance={instance.data} plugins={plugins} artifacts={artifacts} session={session} operations={operations} onNavigate={onNavigate} />}
|
||||
{section === "llm" && <LlmSection serverId={serverId} instance={instance.data} session={session} operations={operations} />}
|
||||
{section === "history" && <HistorySection serverId={serverId} serverOperations={serverOperations} jobs={jobs} artifacts={artifacts} metricHistory={metricHistory} backups={backups} remoteAdapters={remoteAdapters} />}
|
||||
</>
|
||||
@@ -778,11 +784,25 @@ function RuntimeDistributionSection({ instance, runtimeActions, session, operati
|
||||
const [runtimeTaskActions, setRuntimeTaskActions] = useState<RuntimeTaskDialogAction[]>([]);
|
||||
|
||||
const refreshRuntimeProjections = useCallback(async () => {
|
||||
const dependencyActions = runtimeActions.status === "ready"
|
||||
? runtimeActions.data.actions.filter((action) => action.key === "dependencies-check" || action.key === "dependencies-install")
|
||||
: [];
|
||||
const dependencyActionReason = dependencyActions.find((action) => action.reason)?.reason ?? "依赖操作未开放";
|
||||
const catalogRequest: Promise<LoadState<DependencyCatalogResponse>> =
|
||||
runtimeActions.status === "ready" && dependencyActions.some((action) => action.available)
|
||||
? platformApiClient
|
||||
.getDependencyCatalog(instance.id)
|
||||
.then((data): LoadState<DependencyCatalogResponse> => ({ status: "ready", data }))
|
||||
.catch((error): LoadState<DependencyCatalogResponse> => ({ status: "error", reason: error instanceof Error ? error.message : "依赖目录加载失败" }))
|
||||
: Promise.resolve(
|
||||
runtimeActions.status === "error"
|
||||
? { status: "error", reason: runtimeActions.reason }
|
||||
: runtimeActions.status === "ready"
|
||||
? { status: "error", reason: dependencyActionReason }
|
||||
: { status: "loading" }
|
||||
);
|
||||
const [catalog, updates] = await Promise.all([
|
||||
platformApiClient
|
||||
.getDependencyCatalog(instance.id)
|
||||
.then((data): LoadState<DependencyCatalogResponse> => ({ status: "ready", data }))
|
||||
.catch((error): LoadState<DependencyCatalogResponse> => ({ status: "error", reason: error instanceof Error ? error.message : "依赖目录加载失败" })),
|
||||
catalogRequest,
|
||||
platformApiClient
|
||||
.listRunUpdates(instance.id)
|
||||
.then((data): LoadState<RunUpdateJobResponse[]> => ({ status: "ready", data: data.items }))
|
||||
@@ -790,7 +810,7 @@ function RuntimeDistributionSection({ instance, runtimeActions, session, operati
|
||||
]);
|
||||
setDependencyCatalog(catalog);
|
||||
setRunUpdates(updates);
|
||||
}, [instance.id]);
|
||||
}, [instance.id, runtimeActions]);
|
||||
|
||||
useEffect(() => {
|
||||
void refreshRuntimeProjections();
|
||||
@@ -1134,7 +1154,7 @@ function RuntimeDistributionSection({ instance, runtimeActions, session, operati
|
||||
setLastClient(distribution);
|
||||
return distribution;
|
||||
},
|
||||
(distribution) => `客户端管理器二进制已构建,artifact ${distribution.artifactId},secret ref ${safeRuntimeRef(distribution.secretRef)}`,
|
||||
(distribution) => `客户端管理器二进制已构建,artifact ${distribution.artifactId},组件密钥仅由 Platform/Run 受控使用`,
|
||||
{
|
||||
description: `按 ${profileKey} profile 拉取客户端代码、安装环境、编译并生成可下载 artifact。`,
|
||||
stages: runtimeBuildStages,
|
||||
@@ -1341,13 +1361,6 @@ function runUpdatePhaseLabel(phase: RunUpdateJobResponse["phase"]): string {
|
||||
}
|
||||
}
|
||||
|
||||
function safeRuntimeRef(ref: string): string {
|
||||
if (ref.startsWith("secret://runtime-keys/") || ref.startsWith("artifact://")) {
|
||||
return ref;
|
||||
}
|
||||
return "[redacted-ref]";
|
||||
}
|
||||
|
||||
interface LogsSectionProps {
|
||||
serverId: string;
|
||||
}
|
||||
@@ -1680,6 +1693,7 @@ interface PluginControlsSectionProps {
|
||||
artifacts: ArtifactResponse[];
|
||||
session: PageComponentProps["session"];
|
||||
operations: PageComponentProps["operations"];
|
||||
onNavigate: PageComponentProps["onNavigate"];
|
||||
}
|
||||
|
||||
function controlsForPlugin(plugin: GamePluginResponse): PluginControlDescriptor[] {
|
||||
@@ -1746,7 +1760,7 @@ function lifecycleControlLabel(action: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
function PluginControlsSection({ serverId, instance, plugins, artifacts, session, operations }: PluginControlsSectionProps) {
|
||||
function PluginControlsSection({ serverId, instance, plugins, artifacts, session, operations, onNavigate }: PluginControlsSectionProps) {
|
||||
const [collapsed, setCollapsed] = useState<Set<string>>(new Set());
|
||||
const [confirmControl, setConfirmControl] = useState<null | { plugin: PluginControlGroupView; control: PluginControlDescriptor }>(null);
|
||||
const [confirmBusy, setConfirmBusy] = useState(false);
|
||||
@@ -1829,12 +1843,16 @@ function PluginControlsSection({ serverId, instance, plugins, artifacts, session
|
||||
{!isCollapsed && (
|
||||
<div className="plugin-group-body">
|
||||
{plugins.find((plugin) => plugin.id === group.pluginId) && (
|
||||
<PluginBridgeExecutionPanel
|
||||
plugin={plugins.find((plugin) => plugin.id === group.pluginId)!}
|
||||
serverId={serverId}
|
||||
serverInstance={instance}
|
||||
artifacts={artifacts}
|
||||
/>
|
||||
<>
|
||||
<PluginLifecycleWorkbench pluginId={group.pluginId} pluginName={group.pluginName} operations={plugins.find((plugin) => plugin.id === group.pluginId)?.productionLifecycle?.operations} serverId={serverId} />
|
||||
<PluginBridgeExecutionPanel
|
||||
plugin={plugins.find((plugin) => plugin.id === group.pluginId)!}
|
||||
serverId={serverId}
|
||||
serverInstance={instance}
|
||||
artifacts={artifacts}
|
||||
onNavigate={onNavigate}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{group.controls.length === 0 && <span className="provider-id">该插件未声明可用控制项。</span>}
|
||||
{group.controls.map((control) => {
|
||||
@@ -1904,12 +1922,14 @@ interface PluginBridgeExecutionPanelProps {
|
||||
serverId: string;
|
||||
serverInstance: ServerInstanceResponse;
|
||||
artifacts: ArtifactResponse[];
|
||||
onNavigate: PageComponentProps["onNavigate"];
|
||||
}
|
||||
|
||||
function PluginBridgeExecutionPanel({ plugin, serverId, serverInstance, artifacts }: PluginBridgeExecutionPanelProps) {
|
||||
function PluginBridgeExecutionPanel({ plugin, serverId, serverInstance, artifacts, onNavigate }: PluginBridgeExecutionPanelProps) {
|
||||
const [pendingAction, setPendingAction] = useState<PluginBridgeAction | null>(null);
|
||||
const [result, setResult] = useState<{ status: "succeeded" | "failed" | "pending"; label: string } | null>(null);
|
||||
const page = plugin.pages[0];
|
||||
const declaredPageKey = plugin.gameClientBridge?.pages?.[0]?.pageKey;
|
||||
const page = plugin.pages.find((candidate) => candidate.key === declaredPageKey) ?? plugin.pages[0];
|
||||
if (!page || plugin.bridgeActions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
@@ -1962,6 +1982,15 @@ function PluginBridgeExecutionPanel({ plugin, serverId, serverInstance, artifact
|
||||
{result && <ResultBadge status={result.status} label={result.label} />}
|
||||
</span>
|
||||
<div className="action-strip">
|
||||
<button
|
||||
type="button"
|
||||
className="icon-command"
|
||||
onClick={() => onNavigate("pluginPage", { pluginId: plugin.id, routeKey: page.key, serverId })}
|
||||
title={`打开 ${page.title}`}
|
||||
>
|
||||
<PackageOpen size={14} />
|
||||
<span>打开页面</span>
|
||||
</button>
|
||||
{executableActions.slice(0, 3).map((action) => (
|
||||
<button
|
||||
key={action}
|
||||
@@ -2027,27 +2056,11 @@ interface LlmSectionProps {
|
||||
|
||||
function LlmSection({ serverId, instance, session, operations }: LlmSectionProps) {
|
||||
const [prompt, setPrompt] = useState("");
|
||||
const [currentConfig, setCurrentConfig] = useState<string>("");
|
||||
const [suggestion, setSuggestion] = useState<LlmSuggestionView | null>(null);
|
||||
const [confirming, setConfirming] = useState(false);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
void platformApiClient
|
||||
.getServerConfig(serverId)
|
||||
.then((response) => {
|
||||
if (!cancelled) {
|
||||
setCurrentConfig(response.content);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setCurrentConfig("");
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [serverId]);
|
||||
const [approvalBusy, setApprovalBusy] = useState(false);
|
||||
const [suggestionError, setSuggestionError] = useState("");
|
||||
|
||||
async function requestSuggestion(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
@@ -2056,50 +2069,53 @@ function LlmSection({ serverId, instance, session, operations }: LlmSectionProps
|
||||
}
|
||||
setBusy(true);
|
||||
setSuggestion(null);
|
||||
setSuggestionError("");
|
||||
try {
|
||||
const response = await platformApiClient.suggestServerConfig({ serverInstanceId: serverId, prompt: prompt.trim(), currentConfig });
|
||||
const preview = response.suggestedConfig
|
||||
const response = await platformApiClient.invokeAI({ requestId: `web:ai.config:${serverId}:${Date.now()}`, serverInstanceId: serverId, purpose: "config.suggest", prompt: prompt.trim() });
|
||||
if (response.status !== "ok") {
|
||||
throw new Error(response.error?.message ?? "AI 提供商未返回可用建议");
|
||||
}
|
||||
const recommendation = response.configRecommendation;
|
||||
const preview = recommendation?.suggestedConfig
|
||||
? await platformApiClient.previewServerConfigDiff(serverId, {
|
||||
expectedConfigVersion: instance.configVersion,
|
||||
expectedChecksum: instance.configChecksum,
|
||||
key: defaultConfigKey,
|
||||
proposedContent: response.suggestedConfig
|
||||
key: recommendation.key,
|
||||
proposedContent: recommendation.suggestedConfig
|
||||
})
|
||||
: undefined;
|
||||
setSuggestion({
|
||||
serverInstanceId: serverId,
|
||||
source: "api",
|
||||
recommendation: response.recommendation,
|
||||
recommendation: response.recommendation ?? "Platform 已返回配置建议。",
|
||||
diffId: recommendation?.diffId,
|
||||
expiresAt: recommendation?.expiresAt,
|
||||
diff: preview ? configDiffViewFromPreview(preview) : undefined
|
||||
});
|
||||
} catch {
|
||||
setSuggestion(buildLocalSuggestion(serverId, prompt.trim(), currentConfig));
|
||||
} catch (caught) {
|
||||
setSuggestionError(caught instanceof Error ? caught.message : "AI 建议请求失败");
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function applySuggestion() {
|
||||
if (!suggestion?.diff) {
|
||||
if (!suggestion?.diff || !suggestion.diffId || approvalBusy) {
|
||||
return;
|
||||
}
|
||||
const operationId = operations.begin({ intent: "应用 AI 配置建议", targetKind: "llm", targetId: serverId, requester: session.displayName });
|
||||
setApprovalBusy(true);
|
||||
try {
|
||||
const dispatch = await platformApiClient.approveServerConfigWrite(serverId, {
|
||||
expectedConfigVersion: suggestion.diff.configVersion ?? instance.configVersion,
|
||||
expectedChecksum: suggestion.diff.checksum ?? instance.configChecksum,
|
||||
key: suggestion.diff.key ?? defaultConfigKey,
|
||||
proposedContent: suggestion.diff.nextContent,
|
||||
proposedContentInputRef: suggestion.diff.proposedContentInputRef,
|
||||
idempotencyKey: `web:config.write.llm:${serverId}:${Date.now()}`
|
||||
});
|
||||
const job = dispatch.job;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2130,6 +2146,7 @@ function LlmSection({ serverId, instance, session, operations }: LlmSectionProps
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{suggestionError && <ErrorState title="AI 建议不可用" reason={suggestionError} diagnosticId={`ai-config:${serverId}`} onRetry={() => setSuggestionError("")} compact />}
|
||||
<form className="provider-form" style={{ border: 0, padding: 0 }} onSubmit={(event) => void requestSuggestion(event)}>
|
||||
<label>
|
||||
想让 AI 帮你调整什么?
|
||||
@@ -2149,7 +2166,7 @@ function LlmSection({ serverId, instance, session, operations }: LlmSectionProps
|
||||
<div style={{ display: "grid", gap: 12, marginTop: 14 }}>
|
||||
<div className="panel-header" style={{ marginBottom: 0 }}>
|
||||
<h3>AI 建议</h3>
|
||||
<span className="page-status">{suggestion.source === "api" ? "平台 LLM" : "本地建议(LLM 接口未提供)"}</span>
|
||||
<span className="page-status">平台 AI Provider</span>
|
||||
</div>
|
||||
<p style={{ margin: 0, color: "var(--ink-soft)", fontSize: 14 }}>{suggestion.recommendation}</p>
|
||||
{suggestion.diff ? (
|
||||
@@ -2175,7 +2192,7 @@ function LlmSection({ serverId, instance, session, operations }: LlmSectionProps
|
||||
title="确认应用 AI 配置建议"
|
||||
description={`即将向服务器 ${instance.name}(${serverId})派发配置写入任务。写入内容以上方差异为准。`}
|
||||
confirmLabel="确认写入"
|
||||
busy={llmOperation?.status === "pending"}
|
||||
busy={approvalBusy || llmOperation?.status === "pending"}
|
||||
onCancel={() => setConfirming(false)}
|
||||
onConfirm={() => void applySuggestion()}
|
||||
/>
|
||||
@@ -2183,48 +2200,6 @@ function LlmSection({ serverId, instance, session, operations }: LlmSectionProps
|
||||
);
|
||||
}
|
||||
|
||||
function buildLocalSuggestion(serverId: string, prompt: string, currentConfig: string): LlmSuggestionView {
|
||||
const lines = currentConfig.split("\n");
|
||||
const next = [...lines];
|
||||
const changed: string[] = [];
|
||||
const playerMatch = prompt.match(/(\d+)\s*(?:人|名玩家|players?)/i) ?? prompt.match(/玩家[^\d]*(\d+)/);
|
||||
if (playerMatch) {
|
||||
const index = next.findIndex((line) => line.startsWith("max-players="));
|
||||
if (index >= 0) {
|
||||
next[index] = `max-players=${playerMatch[1]}`;
|
||||
changed.push(`max-players 调整为 ${playerMatch[1]}`);
|
||||
}
|
||||
}
|
||||
if (/关闭\s*pvp|禁用\s*pvp|pvp.*(off|false|关)/i.test(prompt)) {
|
||||
const index = next.findIndex((line) => line.startsWith("pvp="));
|
||||
if (index >= 0) {
|
||||
next[index] = "pvp=false";
|
||||
changed.push("pvp 关闭");
|
||||
}
|
||||
}
|
||||
if (/开启\s*pvp|pvp.*(on|true|开)/i.test(prompt)) {
|
||||
const index = next.findIndex((line) => line.startsWith("pvp="));
|
||||
if (index >= 0) {
|
||||
next[index] = "pvp=true";
|
||||
changed.push("pvp 开启");
|
||||
}
|
||||
}
|
||||
const nextContent = next.join("\n");
|
||||
if (changed.length === 0) {
|
||||
return {
|
||||
serverInstanceId: serverId,
|
||||
source: "local",
|
||||
recommendation: `暂时无法为“${prompt}”生成配置差异。平台 LLM 建议接口尚未提供;本地建议引擎只支持常见字段(如 max-players、pvp)。`
|
||||
};
|
||||
}
|
||||
return {
|
||||
serverInstanceId: serverId,
|
||||
source: "local",
|
||||
recommendation: `根据请求“${prompt}”,建议:${changed.join(";")}。请确认差异后再写入。`,
|
||||
diff: buildConfigDiff(serverId, currentConfig, nextContent)
|
||||
};
|
||||
}
|
||||
|
||||
export function configDiffViewFromPreview(preview: ServerConfigDiffPreviewResponse): ConfigDiffView {
|
||||
const lines = preview.diff.map(configDiffLineFromPreviewLine);
|
||||
const added = lines.filter((line) => line.kind === "added").length;
|
||||
@@ -2261,6 +2236,8 @@ interface HistorySectionProps {
|
||||
|
||||
function HistorySection({ serverId, serverOperations, jobs, artifacts, metricHistory, backups, remoteAdapters }: HistorySectionProps) {
|
||||
return (
|
||||
<>
|
||||
<ProductionGovernancePanel compact title={`服务器 ${serverId} 的容量与告警`} />
|
||||
<div className="overview-two-col" aria-label="operation history">
|
||||
<article className="console-panel">
|
||||
<div className="panel-header">
|
||||
@@ -2370,6 +2347,7 @@ function HistorySection({ serverId, serverOperations, jobs, artifacts, metricHis
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user