功能修改

This commit is contained in:
npc0-hue
2026-07-20 16:42:33 +08:00
parent 48b8ad8d6c
commit a0e69417db
224 changed files with 22015 additions and 884 deletions
+81 -103
View File
@@ -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>
</>
);
}