feat: 完整游戏运维功能

This commit is contained in:
npc0-hue
2026-07-18 09:04:01 +08:00
parent f3b14b7945
commit 48b8ad8d6c
187 changed files with 16607 additions and 1140 deletions
+61 -10
View File
@@ -22,6 +22,7 @@ import {
endpointLabel,
pendingJobsForServer,
pluginLabel,
runtimeBindingFields,
type ServerCreateFormState
} from "../contracts/serverManagement";
import { filterServerCards, serverIsOnline, type ServerCardView, type ServerStatusFilter } from "../contracts/workspace";
@@ -75,13 +76,21 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
setEndpoints(endpointResponse.items);
setInstances(instanceResponse.items);
setJobs(jobResponse.items);
setForm((current) => ({
...current,
pluginId: pluginResponse.items.some((plugin) => plugin.id === current.pluginId) ? current.pluginId : pluginResponse.items[0]?.id || "",
runEndpointId: endpointResponse.items.some((endpoint) => endpoint.id === current.runEndpointId)
? current.runEndpointId
: endpointResponse.items[0]?.id || ""
}));
setForm((current) => {
const plugin = pluginResponse.items.find((item) => item.id === current.pluginId) ?? pluginResponse.items[0];
const profileKey = plugin?.runtimeProfiles?.lifecycleProfiles?.some((profile) => profile.key === current.profileKey)
? current.profileKey
: plugin?.runtimeProfiles?.lifecycleProfiles?.[0]?.key ?? "";
return {
...current,
pluginId: plugin?.id ?? "",
profileKey,
bindings: plugin?.id === current.pluginId && profileKey === current.profileKey ? current.bindings : {},
runEndpointId: endpointResponse.items.some((endpoint) => endpoint.id === current.runEndpointId)
? current.runEndpointId
: endpointResponse.items[0]?.id || ""
};
});
setListState("ready");
setListError("");
} catch (error) {
@@ -115,10 +124,25 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
const visibleCards = useMemo(() => filterServerCards(cards, keyword, statusFilter), [cards, keyword, statusFilter]);
const createPending = operations.isPending("platform", "创建服务器");
const selectedCreatePlugin = plugins.find((plugin) => plugin.id === form.pluginId);
const createBindingFields = runtimeBindingFields(selectedCreatePlugin, form.profileKey);
function updateForm(event: ChangeEvent<HTMLInputElement | HTMLSelectElement>) {
const { name, value } = event.target;
setForm((current) => ({ ...current, [name]: value }));
setForm((current) => {
if (name === "pluginId") {
const plugin = plugins.find((item) => item.id === value);
return { ...current, pluginId: value, profileKey: plugin?.runtimeProfiles?.lifecycleProfiles?.[0]?.key ?? "", bindings: {} };
}
if (name === "profileKey") {
return { ...current, profileKey: value, bindings: {} };
}
return { ...current, [name]: value };
});
}
function updateBinding(key: string, value: string) {
setForm((current) => ({ ...current, bindings: { ...current.bindings, [key]: value } }));
}
async function handleCreate(event: FormEvent<HTMLFormElement>) {
@@ -203,7 +227,11 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
return `依赖检查任务已排队,job ${job.id}`;
}
if (action === "dependencies-install") {
const job = await platformApiClient.installDependencies(instance.id, dependencyJobRequest(instance.id, defaults.probeKey, defaults.installPlanKey));
const catalog = await platformApiClient.getDependencyCatalog(instance.id);
const plan = catalog.plans.find((candidate) => candidate.key === defaults.installPlanKey);
const probe = catalog.probes.find((candidate) => candidate.key === defaults.probeKey);
if (!plan || probe?.installPlanKey !== plan.key) throw new Error("Platform 未返回与当前 probe 匹配的审核安装计划");
const job = await platformApiClient.installDependencies(instance.id, dependencyJobRequest(instance.id, probe.key, plan.key, plan.digest));
return `依赖安装任务已排队,job ${job.id}`;
}
if (action === "live-logs") {
@@ -384,8 +412,31 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
))}
</select>
</label>
<label>
<select name="profileKey" value={form.profileKey} onChange={updateForm} required>
{(selectedCreatePlugin?.runtimeProfiles?.lifecycleProfiles ?? []).map((profile) => (
<option key={profile.key} value={profile.key}>
{profile.key} · {profile.mode}
</option>
))}
</select>
</label>
{createBindingFields.map((field) => (
<label key={field.key}>
{field.key}{field.required ? "(必填)" : ""}
<input
type={field.sensitive ? "password" : "text"}
autoComplete="off"
value={form.bindings[field.key] ?? ""}
onChange={(event) => updateBinding(field.key, event.target.value)}
placeholder={field.sensitive ? "托管凭据引用" : "安全逻辑值"}
required={field.required}
/>
</label>
))}
</div>
<button type="submit" className="primary-command" disabled={createPending} title="创建服务器">
<button type="submit" className="primary-command" disabled={createPending || !form.profileKey} title="创建服务器">
<Sparkles size={16} />
<span>{createPending ? "创建中…" : "创建并安装"}</span>
</button>