feat: declare and execute SCUM guided installs
This commit is contained in:
@@ -31,12 +31,17 @@ Raw host directories and command text are normally protected. The operator has e
|
|||||||
|
|
||||||
5. **Auto-reveal only within the authorized editor route.** The editor calls the existing owner-scoped reveal endpoint when opened, stores values only in component state, signals that protected values are visible, and clears state on close. Ordinary deployment reads remain redacted.
|
5. **Auto-reveal only within the authorized editor route.** The editor calls the existing owner-scoped reveal endpoint when opened, stores values only in component state, signals that protected values are visible, and clears state on close. Ordinary deployment reads remain redacted.
|
||||||
|
|
||||||
|
6. **SCUM guided installation is a typed template, not a custom command.** The first-party SCUM template is the authority for SteamCMD App `3792580`, SCUM file/config markers, Microsoft Visual C++ prerequisites, DirectX runtime, and verification. Run receives the frozen template plus the protected root and game fields, creates a controlled SteamCMD argument vector including `+force_install_dir`, and never interprets an operator or plugin supplied shell snippet for the guided path. Microsoft prerequisite artifacts use official Microsoft endpoints and silent switches under a fixed Run catalog; an internal mirror is not selected unless it is explicitly checksum-equivalent to that catalog. Run checks runtime markers before each install and verifies the marker again afterward.
|
||||||
|
|
||||||
|
7. **Keep transport identifiers internal and present safe operation text.** `process.install` remains the Run capability because it is part of the leased job protocol. The Platform Web task card derives its title from the frozen plugin/action and deployment phase (for example, `安装 SCUM 服务端`), rather than rendering the protocol capability as user-facing copy.
|
||||||
|
|
||||||
## Risks / Trade-offs
|
## Risks / Trade-offs
|
||||||
|
|
||||||
- [Raw local diagnostics can be copied from the host console] → Diagnostics are opt-in, local-only, action-scoped, and clearly warn operators; no credentials or environment are included.
|
- [Raw local diagnostics can be copied from the host console] → Diagnostics are opt-in, local-only, action-scoped, and clearly warn operators; no credentials or environment are included.
|
||||||
- [Mixed Platform/Run releases can lose execution confirmation] → Version/capability negotiation rejects deployment-aware jobs until the Run supports the protocol, rather than silently ignoring input.
|
- [Mixed Platform/Run releases can lose execution confirmation] → Version/capability negotiation rejects deployment-aware jobs until the Run supports the protocol, rather than silently ignoring input.
|
||||||
- [An editor request can reveal operational values to an authorized browser] → It remains owner-authorized, uses no shared cache, and clears on dialog close.
|
- [An editor request can reveal operational values to an authorized browser] → It remains owner-authorized, uses no shared cache, and clears on dialog close.
|
||||||
- [Existing clients cannot express clearing] → The additive `clearFields` field preserves existing requests while enabling explicit removal.
|
- [Existing clients cannot express clearing] → The additive `clearFields` field preserves existing requests while enabling explicit removal.
|
||||||
|
- [A download mirror may be stale or tampered with] → Prefer the official Microsoft catalog and a fixed checksum/signature policy. Mirrors are acceleration only after equivalence verification; they are never the trust root.
|
||||||
|
|
||||||
## Migration Plan
|
## Migration Plan
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ This change makes the editor load the authorized saved definition directly and m
|
|||||||
- Return a non-sensitive receipt containing the deployment revision and execution mode so Platform can distinguish dispatch evidence from Run execution confirmation.
|
- Return a non-sensitive receipt containing the deployment revision and execution mode so Platform can distinguish dispatch evidence from Run execution confirmation.
|
||||||
- Automatically fetch and prefill the authorized saved deployment inputs when opening the edit workflow, then clear them when the dialog closes.
|
- Automatically fetch and prefill the authorized saved deployment inputs when opening the edit workflow, then clear them when the dialog closes.
|
||||||
- Replace ambiguous empty-string update handling with explicit preserve/set/clear semantics and prevent an omitted shell from resetting the saved shell.
|
- Replace ambiguous empty-string update handling with explicit preserve/set/clear semantics and prevent an omitted shell from resetting the saved shell.
|
||||||
|
- Make first-party SCUM guided installation fully plugin-owned: Run must consume the frozen SCUM template, ensure SteamCMD plus declared Microsoft prerequisites, install App `3792580` into the selected root, materialize configuration, and verify the resulting server. Operators supply the target directory and game settings, never lifecycle commands or prerequisite installers.
|
||||||
|
- Replace raw lifecycle capability text such as `process.install` in operator task surfaces with the plugin-aware operation name and phase, while retaining the capability as the internal routing identifier.
|
||||||
|
|
||||||
## Capabilities
|
## Capabilities
|
||||||
|
|
||||||
@@ -29,3 +31,4 @@ This change makes the editor load the authorized saved definition directly and m
|
|||||||
- Platform job-channel DTOs, domain contracts, validation, lifecycle dispatch/result projection, and deployment editor API/client/view code.
|
- Platform job-channel DTOs, domain contracts, validation, lifecycle dispatch/result projection, and deployment editor API/client/view code.
|
||||||
- Independent `run.git` protocol, lifecycle executor, worker diagnostics, tests, and release compatibility metadata. No Run source is added to this repository.
|
- Independent `run.git` protocol, lifecycle executor, worker diagnostics, tests, and release compatibility metadata. No Run source is added to this repository.
|
||||||
- Operator workflow: opening an authorized stopped-server editor reads protected deployment values; normal list/detail/job/log/audit responses remain redacted.
|
- Operator workflow: opening an authorized stopped-server editor reads protected deployment values; normal list/detail/job/log/audit responses remain redacted.
|
||||||
|
- SCUM guided deployment: first-party manifest declaration, Platform's frozen assignment, and the independent Run executor.
|
||||||
|
|||||||
+15
@@ -32,3 +32,18 @@ Platform SHALL preserve a deployment value when an update omits it and SHALL cle
|
|||||||
#### Scenario: Optional command is cleared
|
#### Scenario: Optional command is cleared
|
||||||
- **WHEN** an editor submits `stopCommand` in its explicit clear list
|
- **WHEN** an editor submits `stopCommand` in its explicit clear list
|
||||||
- **THEN** Platform SHALL store no stop command for the next deployment revision
|
- **THEN** Platform SHALL store no stop command for the next deployment revision
|
||||||
|
|
||||||
|
### Requirement: SCUM guided deployment is template-owned
|
||||||
|
For a frozen SCUM guided-install assignment, Run SHALL execute the declared SCUM installation template rather than requiring an operator-provided install or start command. The operator supplies only the protected target root and plugin create inputs.
|
||||||
|
|
||||||
|
#### Scenario: Missing runtime prerequisites are installed silently
|
||||||
|
- **WHEN** a Windows SCUM guided-install detects a required declared Microsoft runtime is absent
|
||||||
|
- **THEN** Run SHALL obtain the approved Microsoft artifact, invoke only its fixed silent installer arguments, verify the runtime marker, and continue only when verification passes
|
||||||
|
|
||||||
|
#### Scenario: SteamCMD installs into the selected root
|
||||||
|
- **WHEN** a valid SCUM guided-install is executed for root `C:\\scumserver`
|
||||||
|
- **THEN** Run SHALL invoke SteamCMD with `+force_install_dir C:\\scumserver`, anonymous login, App `3792580`, validation, and quit as separate argument values
|
||||||
|
|
||||||
|
#### Scenario: Guided execution does not disclose raw values remotely
|
||||||
|
- **WHEN** Run completes or fails a SCUM guided-install
|
||||||
|
- **THEN** its progress, result, evidence, artifact names, and uploaded logs SHALL contain no raw root, command line, credential, or installer URL
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||
|
|
||||||
- [x] 2.1 Add typed deployment execution version/receipt contracts and validation for frozen lifecycle jobs.
|
- [x] 2.1 Add typed deployment execution version/receipt contracts and validation for frozen lifecycle jobs.
|
||||||
- [x] 2.2 Require compatible Run capability before lifecycle dispatch and project validated Run confirmation in deployment views.
|
- [x] 2.2 Require compatible Run capability before lifecycle dispatch and project validated Run confirmation in deployment views.
|
||||||
- [ ] 2.3 Add explicit deployment-field clearing and preserve omitted shell values; cover service/API behavior with tests.
|
- [x] 2.3 Add explicit deployment-field clearing and preserve omitted shell values; cover service/API behavior with tests.
|
||||||
|
- [x] 2.4 Freeze SCUM prerequisite/template execution data into the leased deployment plan and validate it as a first-party Windows-only guided installation contract.
|
||||||
|
|
||||||
## 3. Platform editor workflow
|
## 3. Platform editor workflow
|
||||||
|
|
||||||
@@ -20,10 +21,12 @@
|
|||||||
- [x] 4.1 Extend independent `run.git` protocol and validation with the typed deployment envelope and safe execution receipt.
|
- [x] 4.1 Extend independent `run.git` protocol and validation with the typed deployment envelope and safe execution receipt.
|
||||||
- [x] 4.2 Execute deployment-aware lifecycle actions using the frozen root, working directory, shell, and commands.
|
- [x] 4.2 Execute deployment-aware lifecycle actions using the frozen root, working directory, shell, and commands.
|
||||||
- [x] 4.3 Add opt-in local-only raw startup diagnostics that bypass the upload log sink and exclude credentials/environment values.
|
- [x] 4.3 Add opt-in local-only raw startup diagnostics that bypass the upload log sink and exclude credentials/environment values.
|
||||||
- [ ] 4.4 Add Run protocol/runtime tests covering execution, receipt fences, enabled diagnostics, and remote-channel redaction.
|
- [x] 4.4 Add Run protocol/runtime tests covering execution, receipt fences, enabled diagnostics, and remote-channel redaction.
|
||||||
|
- [x] 4.5 Implement the SCUM guided executor: dependency probes and silent Microsoft prerequisite installation, SteamCMD bootstrap, `+force_install_dir` installation, configuration materialization, verification evidence, and local-only diagnostic output.
|
||||||
|
|
||||||
## 5. Verification and delivery
|
## 5. Verification and delivery
|
||||||
|
|
||||||
- [x] 5.1 Run browser Platform/backend/frontend validation and strict OpenSpec validation.
|
- [x] 5.1 Run browser Platform/backend/frontend validation and strict OpenSpec validation.
|
||||||
- [x] 5.2 Run independent Run test suite and inspect both worktrees for scoped changes only.
|
- [x] 5.2 Run independent Run test suite and inspect both worktrees for scoped changes only.
|
||||||
- [x] 5.3 Stage, commit, and push scoped changes on `main` in both repositories.
|
- [x] 5.3 Stage, commit, and push scoped changes on `main` in both repositories.
|
||||||
|
- [x] 5.4 Replace raw lifecycle capability labels in the server-management task views and run focused UI tests.
|
||||||
|
|||||||
@@ -545,6 +545,13 @@ type RuntimeServerVerificationCheck struct {
|
|||||||
Required bool
|
Required bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RuntimeServerPrerequisite is a plugin-declared named prerequisite resolved
|
||||||
|
// only through Run's fixed, platform-specific installer catalog.
|
||||||
|
type RuntimeServerPrerequisite struct {
|
||||||
|
Key string
|
||||||
|
Kind string
|
||||||
|
}
|
||||||
|
|
||||||
// RuntimeServerDeploymentProfile is a frozen, game-specific deployment
|
// RuntimeServerDeploymentProfile is a frozen, game-specific deployment
|
||||||
// template. It contains logical references only; Run resolves them locally.
|
// template. It contains logical references only; Run resolves them locally.
|
||||||
type RuntimeServerDeploymentProfile struct {
|
type RuntimeServerDeploymentProfile struct {
|
||||||
@@ -556,6 +563,7 @@ type RuntimeServerDeploymentProfile struct {
|
|||||||
InstallRootKey string
|
InstallRootKey string
|
||||||
ConfigKey string
|
ConfigKey string
|
||||||
ConfigFormat string
|
ConfigFormat string
|
||||||
|
Prerequisites []RuntimeServerPrerequisite
|
||||||
ConfigMappings []RuntimeServerConfigMapping
|
ConfigMappings []RuntimeServerConfigMapping
|
||||||
DiscoveryMarkers []RuntimeServerDiscoveryMarker
|
DiscoveryMarkers []RuntimeServerDiscoveryMarker
|
||||||
VerificationChecks []RuntimeServerVerificationCheck
|
VerificationChecks []RuntimeServerVerificationCheck
|
||||||
@@ -847,6 +855,8 @@ type ServerDeploymentUpdate struct {
|
|||||||
StopCommand string
|
StopCommand string
|
||||||
StatusCommand string
|
StatusCommand string
|
||||||
Shell ServerCommandShell
|
Shell ServerCommandShell
|
||||||
|
ShellSet bool
|
||||||
|
ClearFields []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServerDeploymentView is the intentionally redacted read model.
|
// ServerDeploymentView is the intentionally redacted read model.
|
||||||
@@ -1068,6 +1078,7 @@ type ServerDeploymentPlan struct {
|
|||||||
InstallRootKey string
|
InstallRootKey string
|
||||||
ConfigKey string
|
ConfigKey string
|
||||||
ConfigFormat string
|
ConfigFormat string
|
||||||
|
Prerequisites []RuntimeServerPrerequisite
|
||||||
ConfigMappings []RuntimeServerConfigMapping
|
ConfigMappings []RuntimeServerConfigMapping
|
||||||
DiscoveryMarkers []RuntimeServerDiscoveryMarker
|
DiscoveryMarkers []RuntimeServerDiscoveryMarker
|
||||||
VerificationChecks []RuntimeServerVerificationCheck
|
VerificationChecks []RuntimeServerVerificationCheck
|
||||||
@@ -1773,6 +1784,7 @@ func CopyServerInstance(instance ServerInstance) ServerInstance {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func CopyRuntimeServerDeploymentProfile(profile RuntimeServerDeploymentProfile) RuntimeServerDeploymentProfile {
|
func CopyRuntimeServerDeploymentProfile(profile RuntimeServerDeploymentProfile) RuntimeServerDeploymentProfile {
|
||||||
|
profile.Prerequisites = append([]RuntimeServerPrerequisite(nil), profile.Prerequisites...)
|
||||||
profile.SupportedTargets = append([]RuntimeTarget(nil), profile.SupportedTargets...)
|
profile.SupportedTargets = append([]RuntimeTarget(nil), profile.SupportedTargets...)
|
||||||
profile.ConfigMappings = append([]RuntimeServerConfigMapping(nil), profile.ConfigMappings...)
|
profile.ConfigMappings = append([]RuntimeServerConfigMapping(nil), profile.ConfigMappings...)
|
||||||
profile.DiscoveryMarkers = append([]RuntimeServerDiscoveryMarker(nil), profile.DiscoveryMarkers...)
|
profile.DiscoveryMarkers = append([]RuntimeServerDiscoveryMarker(nil), profile.DiscoveryMarkers...)
|
||||||
@@ -1792,6 +1804,7 @@ func CopyServerDeploymentPlan(plan *ServerDeploymentPlan) *ServerDeploymentPlan
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
copy := *plan
|
copy := *plan
|
||||||
|
copy.Prerequisites = append([]RuntimeServerPrerequisite(nil), plan.Prerequisites...)
|
||||||
copy.ConfigMappings = append([]RuntimeServerConfigMapping(nil), plan.ConfigMappings...)
|
copy.ConfigMappings = append([]RuntimeServerConfigMapping(nil), plan.ConfigMappings...)
|
||||||
copy.DiscoveryMarkers = append([]RuntimeServerDiscoveryMarker(nil), plan.DiscoveryMarkers...)
|
copy.DiscoveryMarkers = append([]RuntimeServerDiscoveryMarker(nil), plan.DiscoveryMarkers...)
|
||||||
copy.VerificationChecks = append([]RuntimeServerVerificationCheck(nil), plan.VerificationChecks...)
|
copy.VerificationChecks = append([]RuntimeServerVerificationCheck(nil), plan.VerificationChecks...)
|
||||||
@@ -1824,6 +1837,7 @@ func CopyServerDeploymentDefinition(definition ServerDeploymentDefinition) Serve
|
|||||||
}
|
}
|
||||||
|
|
||||||
func CopyServerDeploymentUpdate(update ServerDeploymentUpdate) ServerDeploymentUpdate {
|
func CopyServerDeploymentUpdate(update ServerDeploymentUpdate) ServerDeploymentUpdate {
|
||||||
|
update.ClearFields = CopyStringSlice(update.ClearFields)
|
||||||
update.RuntimeBindings = CopyStringMap(update.RuntimeBindings)
|
update.RuntimeBindings = CopyStringMap(update.RuntimeBindings)
|
||||||
update.CreateInputs = CopyStringMap(update.CreateInputs)
|
update.CreateInputs = CopyStringMap(update.CreateInputs)
|
||||||
return update
|
return update
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ type ServerDeploymentPlanBody struct {
|
|||||||
InstallRootKey string `json:"installRootKey"`
|
InstallRootKey string `json:"installRootKey"`
|
||||||
ConfigKey string `json:"configKey"`
|
ConfigKey string `json:"configKey"`
|
||||||
ConfigFormat string `json:"configFormat"`
|
ConfigFormat string `json:"configFormat"`
|
||||||
|
Prerequisites []RuntimeServerPrerequisiteBody `json:"prerequisites,omitempty"`
|
||||||
ConfigMappings []RuntimeServerConfigMappingBody `json:"configMappings"`
|
ConfigMappings []RuntimeServerConfigMappingBody `json:"configMappings"`
|
||||||
DiscoveryMarkers []RuntimeServerDiscoveryMarkerBody `json:"discoveryMarkers"`
|
DiscoveryMarkers []RuntimeServerDiscoveryMarkerBody `json:"discoveryMarkers"`
|
||||||
VerificationChecks []RuntimeServerVerificationCheckBody `json:"verificationChecks"`
|
VerificationChecks []RuntimeServerVerificationCheckBody `json:"verificationChecks"`
|
||||||
@@ -643,6 +644,9 @@ func serverDeploymentPlanFromDomain(plan *domain.ServerDeploymentPlan) *ServerDe
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
body := &ServerDeploymentPlanBody{SchemaVersion: plan.SchemaVersion, Operation: plan.Operation, PluginID: plan.PluginID, TemplateKey: plan.TemplateKey, TemplateVersion: plan.TemplateVersion, SteamAppID: plan.SteamAppID, ExecutableKey: plan.ExecutableKey, InstallRootKey: plan.InstallRootKey, ConfigKey: plan.ConfigKey, ConfigFormat: plan.ConfigFormat}
|
body := &ServerDeploymentPlanBody{SchemaVersion: plan.SchemaVersion, Operation: plan.Operation, PluginID: plan.PluginID, TemplateKey: plan.TemplateKey, TemplateVersion: plan.TemplateVersion, SteamAppID: plan.SteamAppID, ExecutableKey: plan.ExecutableKey, InstallRootKey: plan.InstallRootKey, ConfigKey: plan.ConfigKey, ConfigFormat: plan.ConfigFormat}
|
||||||
|
for _, prerequisite := range plan.Prerequisites {
|
||||||
|
body.Prerequisites = append(body.Prerequisites, RuntimeServerPrerequisiteBody{Key: prerequisite.Key, Kind: prerequisite.Kind})
|
||||||
|
}
|
||||||
for _, mapping := range plan.ConfigMappings {
|
for _, mapping := range plan.ConfigMappings {
|
||||||
body.ConfigMappings = append(body.ConfigMappings, RuntimeServerConfigMappingBody{FieldKey: mapping.FieldKey, ConfigKey: mapping.ConfigKey, ValueType: mapping.ValueType, Required: mapping.Required})
|
body.ConfigMappings = append(body.ConfigMappings, RuntimeServerConfigMappingBody{FieldKey: mapping.FieldKey, ConfigKey: mapping.ConfigKey, ValueType: mapping.ValueType, Required: mapping.Required})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,11 +89,17 @@ type RuntimeServerDeploymentProfileBody struct {
|
|||||||
InstallRootKey string `json:"installRootKey"`
|
InstallRootKey string `json:"installRootKey"`
|
||||||
ConfigKey string `json:"configKey"`
|
ConfigKey string `json:"configKey"`
|
||||||
ConfigFormat string `json:"configFormat"`
|
ConfigFormat string `json:"configFormat"`
|
||||||
|
Prerequisites []RuntimeServerPrerequisiteBody `json:"prerequisites,omitempty"`
|
||||||
ConfigMappings []RuntimeServerConfigMappingBody `json:"configMappings"`
|
ConfigMappings []RuntimeServerConfigMappingBody `json:"configMappings"`
|
||||||
DiscoveryMarkers []RuntimeServerDiscoveryMarkerBody `json:"discoveryMarkers"`
|
DiscoveryMarkers []RuntimeServerDiscoveryMarkerBody `json:"discoveryMarkers"`
|
||||||
VerificationChecks []RuntimeServerVerificationCheckBody `json:"verificationChecks"`
|
VerificationChecks []RuntimeServerVerificationCheckBody `json:"verificationChecks"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RuntimeServerPrerequisiteBody struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
}
|
||||||
|
|
||||||
type RuntimeLogSourceBody struct {
|
type RuntimeLogSourceBody struct {
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
Kind string `json:"kind"`
|
Kind string `json:"kind"`
|
||||||
@@ -297,6 +303,9 @@ func (body GamePluginRuntimeProfilesBody) ToDomain() domain.GamePluginRuntimePro
|
|||||||
}
|
}
|
||||||
for _, item := range body.ServerDeployments {
|
for _, item := range body.ServerDeployments {
|
||||||
profile := domain.RuntimeServerDeploymentProfile{Key: item.Key, Version: item.Version, SteamAppID: item.SteamAppID, ExecutableKey: item.ExecutableKey, InstallRootKey: item.InstallRootKey, ConfigKey: item.ConfigKey, ConfigFormat: item.ConfigFormat}
|
profile := domain.RuntimeServerDeploymentProfile{Key: item.Key, Version: item.Version, SteamAppID: item.SteamAppID, ExecutableKey: item.ExecutableKey, InstallRootKey: item.InstallRootKey, ConfigKey: item.ConfigKey, ConfigFormat: item.ConfigFormat}
|
||||||
|
for _, prerequisite := range item.Prerequisites {
|
||||||
|
profile.Prerequisites = append(profile.Prerequisites, domain.RuntimeServerPrerequisite{Key: prerequisite.Key, Kind: prerequisite.Kind})
|
||||||
|
}
|
||||||
for _, target := range item.SupportedTargets {
|
for _, target := range item.SupportedTargets {
|
||||||
profile.SupportedTargets = append(profile.SupportedTargets, domain.RuntimeTarget{OS: target.OS, Arch: target.Arch})
|
profile.SupportedTargets = append(profile.SupportedTargets, domain.RuntimeTarget{OS: target.OS, Arch: target.Arch})
|
||||||
}
|
}
|
||||||
@@ -370,6 +379,9 @@ func runtimeProfilesFromDomain(profiles domain.GamePluginRuntimeProfiles) GamePl
|
|||||||
}
|
}
|
||||||
for _, item := range profiles.ServerDeployments {
|
for _, item := range profiles.ServerDeployments {
|
||||||
bodyProfile := RuntimeServerDeploymentProfileBody{Key: item.Key, Version: item.Version, SteamAppID: item.SteamAppID, ExecutableKey: item.ExecutableKey, InstallRootKey: item.InstallRootKey, ConfigKey: item.ConfigKey, ConfigFormat: item.ConfigFormat}
|
bodyProfile := RuntimeServerDeploymentProfileBody{Key: item.Key, Version: item.Version, SteamAppID: item.SteamAppID, ExecutableKey: item.ExecutableKey, InstallRootKey: item.InstallRootKey, ConfigKey: item.ConfigKey, ConfigFormat: item.ConfigFormat}
|
||||||
|
for _, prerequisite := range item.Prerequisites {
|
||||||
|
bodyProfile.Prerequisites = append(bodyProfile.Prerequisites, RuntimeServerPrerequisiteBody{Key: prerequisite.Key, Kind: prerequisite.Kind})
|
||||||
|
}
|
||||||
for _, target := range item.SupportedTargets {
|
for _, target := range item.SupportedTargets {
|
||||||
bodyProfile.SupportedTargets = append(bodyProfile.SupportedTargets, RuntimeTargetBody{OS: target.OS, Arch: target.Arch})
|
bodyProfile.SupportedTargets = append(bodyProfile.SupportedTargets, RuntimeTargetBody{OS: target.OS, Arch: target.Arch})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ type ServerDeploymentRequest struct {
|
|||||||
StartCommand string `json:"startCommand,omitempty"`
|
StartCommand string `json:"startCommand,omitempty"`
|
||||||
StopCommand string `json:"stopCommand,omitempty"`
|
StopCommand string `json:"stopCommand,omitempty"`
|
||||||
StatusCommand string `json:"statusCommand,omitempty"`
|
StatusCommand string `json:"statusCommand,omitempty"`
|
||||||
Shell domain.ServerCommandShell `json:"shell,omitempty"`
|
Shell *domain.ServerCommandShell `json:"shell,omitempty"`
|
||||||
|
ClearFields []string `json:"clearFields,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServerDeploymentResponse struct {
|
type ServerDeploymentResponse struct {
|
||||||
@@ -114,7 +115,11 @@ func (request ServerLifecycleCreateRequest) ToDomain() domain.ServerLifecycleCre
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (request ServerDeploymentRequest) ToDomain() domain.ServerDeploymentUpdate {
|
func (request ServerDeploymentRequest) ToDomain() domain.ServerDeploymentUpdate {
|
||||||
return domain.ServerDeploymentUpdate{RunEndpointID: request.RunEndpointID, Mode: request.Mode, ProfileKey: request.ProfileKey, RuntimeBindings: domain.CopyStringMap(request.RuntimeBindings), CreateInputs: domain.CopyStringMap(request.CreateInputs), ServerRoot: request.ServerRoot, WorkingDirectory: request.WorkingDirectory, InstallCommand: request.InstallCommand, StartCommand: request.StartCommand, StopCommand: request.StopCommand, StatusCommand: request.StatusCommand, Shell: request.Shell}
|
update := domain.ServerDeploymentUpdate{RunEndpointID: request.RunEndpointID, Mode: request.Mode, ProfileKey: request.ProfileKey, RuntimeBindings: domain.CopyStringMap(request.RuntimeBindings), CreateInputs: domain.CopyStringMap(request.CreateInputs), ServerRoot: request.ServerRoot, WorkingDirectory: request.WorkingDirectory, InstallCommand: request.InstallCommand, StartCommand: request.StartCommand, StopCommand: request.StopCommand, StatusCommand: request.StatusCommand, ClearFields: domain.CopyStringSlice(request.ClearFields)}
|
||||||
|
if request.Shell != nil {
|
||||||
|
update.Shell, update.ShellSet = *request.Shell, true
|
||||||
|
}
|
||||||
|
return update
|
||||||
}
|
}
|
||||||
|
|
||||||
func (request ServerDeploymentRequest) deploymentDefinition() domain.ServerDeploymentDefinition {
|
func (request ServerDeploymentRequest) deploymentDefinition() domain.ServerDeploymentDefinition {
|
||||||
|
|||||||
@@ -48,6 +48,15 @@ resolved path or command text. Successful result kinds are
|
|||||||
`scum.install.completed` and `scum.adopt.completed`; failed result kinds are
|
`scum.install.completed` and `scum.adopt.completed`; failed result kinds are
|
||||||
`scum.install.failed` and `scum.adopt.failed`.
|
`scum.install.failed` and `scum.adopt.failed`.
|
||||||
|
|
||||||
|
The first-party Windows SCUM template declares `steamcmd`, Visual C++ 2012,
|
||||||
|
2013, and 2015-2022 (x86 and x64), plus DirectX runtime prerequisites. Run
|
||||||
|
checks their Windows markers before installation and uses only its fixed
|
||||||
|
Microsoft installer catalog with silent arguments when one is absent. The
|
||||||
|
template builds SteamCMD as separate arguments: `+force_install_dir`, the
|
||||||
|
protected root, anonymous login, App `3792580`, `validate`, and `+quit`. The
|
||||||
|
original root and constructed command are eligible only for opt-in local Run
|
||||||
|
diagnostics; they never enter progress or result payloads.
|
||||||
|
|
||||||
An `existing-server` plan may omit installation. A `custom-command` plan
|
An `existing-server` plan may omit installation. A `custom-command` plan
|
||||||
requires a start command. Guided templates remain plugin recommendations;
|
requires a start command. Guided templates remain plugin recommendations;
|
||||||
Run owns their local resolution and execution.
|
Run owns their local resolution and execution.
|
||||||
|
|||||||
@@ -42,6 +42,15 @@ func scumDeploymentPlan(plugin domain.GamePlugin, definition domain.ServerDeploy
|
|||||||
if strings.TrimSpace(profile.Key) == "" || strings.TrimSpace(profile.Version) == "" || profile.SteamAppID == "" {
|
if strings.TrimSpace(profile.Key) == "" || strings.TrimSpace(profile.Version) == "" || profile.SteamAppID == "" {
|
||||||
return nil, validationError("SCUM deployment template is incomplete")
|
return nil, validationError("SCUM deployment template is incomplete")
|
||||||
}
|
}
|
||||||
|
expectedPrerequisites := map[string]string{"steamcmd": "steamcmd", "vcredist-2012-x86": "windows-vcredist", "vcredist-2012-x64": "windows-vcredist", "vcredist-2013-x86": "windows-vcredist", "vcredist-2013-x64": "windows-vcredist", "vcredist-2015-2022-x86": "windows-vcredist", "vcredist-2015-2022-x64": "windows-vcredist", "directx-jun2010": "windows-directx"}
|
||||||
|
if len(profile.Prerequisites) != len(expectedPrerequisites) {
|
||||||
|
return nil, validationError("SCUM deployment template prerequisite list is incomplete")
|
||||||
|
}
|
||||||
|
for _, prerequisite := range profile.Prerequisites {
|
||||||
|
if expectedPrerequisites[prerequisite.Key] != prerequisite.Kind {
|
||||||
|
return nil, validationError("SCUM deployment template prerequisite is invalid")
|
||||||
|
}
|
||||||
|
}
|
||||||
fieldKeys := make(map[string]struct{}, len(plugin.CreateFields))
|
fieldKeys := make(map[string]struct{}, len(plugin.CreateFields))
|
||||||
for _, field := range plugin.CreateFields {
|
for _, field := range plugin.CreateFields {
|
||||||
fieldKeys[field.Key] = struct{}{}
|
fieldKeys[field.Key] = struct{}{}
|
||||||
@@ -70,6 +79,7 @@ func scumDeploymentPlan(plugin domain.GamePlugin, definition domain.ServerDeploy
|
|||||||
InstallRootKey: profile.InstallRootKey,
|
InstallRootKey: profile.InstallRootKey,
|
||||||
ConfigKey: profile.ConfigKey,
|
ConfigKey: profile.ConfigKey,
|
||||||
ConfigFormat: profile.ConfigFormat,
|
ConfigFormat: profile.ConfigFormat,
|
||||||
|
Prerequisites: append([]domain.RuntimeServerPrerequisite(nil), profile.Prerequisites...),
|
||||||
ConfigMappings: append([]domain.RuntimeServerConfigMapping(nil), profile.ConfigMappings...),
|
ConfigMappings: append([]domain.RuntimeServerConfigMapping(nil), profile.ConfigMappings...),
|
||||||
DiscoveryMarkers: append([]domain.RuntimeServerDiscoveryMarker(nil), profile.DiscoveryMarkers...),
|
DiscoveryMarkers: append([]domain.RuntimeServerDiscoveryMarker(nil), profile.DiscoveryMarkers...),
|
||||||
VerificationChecks: append([]domain.RuntimeServerVerificationCheck(nil), profile.VerificationChecks...),
|
VerificationChecks: append([]domain.RuntimeServerVerificationCheck(nil), profile.VerificationChecks...),
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ func scumDeploymentTestPlugin() domain.GamePlugin {
|
|||||||
},
|
},
|
||||||
RuntimeProfiles: domain.GamePluginRuntimeProfiles{ServerDeployments: []domain.RuntimeServerDeploymentProfile{{
|
RuntimeProfiles: domain.GamePluginRuntimeProfiles{ServerDeployments: []domain.RuntimeServerDeploymentProfile{{
|
||||||
Key: "scum-steamcmd-windows", Version: "1.0.0", SteamAppID: "3792580", ExecutableKey: "scum/server-executable", InstallRootKey: "server/install-root", ConfigKey: "scum/server-settings", ConfigFormat: "ini",
|
Key: "scum-steamcmd-windows", Version: "1.0.0", SteamAppID: "3792580", ExecutableKey: "scum/server-executable", InstallRootKey: "server/install-root", ConfigKey: "scum/server-settings", ConfigFormat: "ini",
|
||||||
|
Prerequisites: []domain.RuntimeServerPrerequisite{{Key: "steamcmd", Kind: "steamcmd"}, {Key: "vcredist-2012-x86", Kind: "windows-vcredist"}, {Key: "vcredist-2012-x64", Kind: "windows-vcredist"}, {Key: "vcredist-2013-x86", Kind: "windows-vcredist"}, {Key: "vcredist-2013-x64", Kind: "windows-vcredist"}, {Key: "vcredist-2015-2022-x86", Kind: "windows-vcredist"}, {Key: "vcredist-2015-2022-x64", Kind: "windows-vcredist"}, {Key: "directx-jun2010", Kind: "windows-directx"}},
|
||||||
ConfigMappings: []domain.RuntimeServerConfigMapping{{FieldKey: "serverName", ConfigKey: "server-settings.server-name", ValueType: "text", Required: true}, {FieldKey: "gamePort", ConfigKey: "server-settings.game-port", ValueType: "port", Required: true}, {FieldKey: "queryPort", ConfigKey: "server-settings.query-port", ValueType: "port", Required: true}, {FieldKey: "maxPlayers", ConfigKey: "server-settings.max-players", ValueType: "integer", Required: true}},
|
ConfigMappings: []domain.RuntimeServerConfigMapping{{FieldKey: "serverName", ConfigKey: "server-settings.server-name", ValueType: "text", Required: true}, {FieldKey: "gamePort", ConfigKey: "server-settings.game-port", ValueType: "port", Required: true}, {FieldKey: "queryPort", ConfigKey: "server-settings.query-port", ValueType: "port", Required: true}, {FieldKey: "maxPlayers", ConfigKey: "server-settings.max-players", ValueType: "integer", Required: true}},
|
||||||
VerificationChecks: []domain.RuntimeServerVerificationCheck{{Key: "executable", Kind: "executable.present", TargetKey: "scum/server-executable", Required: true}, {Key: "version", Kind: "version.matches", TargetKey: "scum/server-executable", Required: true}, {Key: "game-port", Kind: "port.bound", TargetKey: "game-port", Required: true}, {Key: "config", Kind: "config.readable", TargetKey: "scum/server-settings", Required: true}, {Key: "process", Kind: "process.healthy", TargetKey: "scum/server-executable", Required: true}},
|
VerificationChecks: []domain.RuntimeServerVerificationCheck{{Key: "executable", Kind: "executable.present", TargetKey: "scum/server-executable", Required: true}, {Key: "version", Kind: "version.matches", TargetKey: "scum/server-executable", Required: true}, {Key: "game-port", Kind: "port.bound", TargetKey: "game-port", Required: true}, {Key: "config", Kind: "config.readable", TargetKey: "scum/server-settings", Required: true}, {Key: "process", Kind: "process.healthy", TargetKey: "scum/server-executable", Required: true}},
|
||||||
}}},
|
}}},
|
||||||
|
|||||||
@@ -38,6 +38,13 @@ func (svc *CoreService) UpdateServerDeploymentForSession(sessionID, serverInstan
|
|||||||
if instance.State == domain.ServerInstanceStateInstalling || instance.State == domain.ServerInstanceStateRunning || instance.State == domain.ServerInstanceStateDeleted {
|
if instance.State == domain.ServerInstanceStateInstalling || instance.State == domain.ServerInstanceStateRunning || instance.State == domain.ServerInstanceStateDeleted {
|
||||||
return domain.ServerDeploymentView{}, validationError("deployment definition cannot be changed while the server is active")
|
return domain.ServerDeploymentView{}, validationError("deployment definition cannot be changed while the server is active")
|
||||||
}
|
}
|
||||||
|
seenClearFields := map[string]bool{}
|
||||||
|
for _, field := range update.ClearFields {
|
||||||
|
if seenClearFields[field] || field != "serverRoot" && field != "workingDirectory" && field != "installCommand" && field != "startCommand" && field != "stopCommand" && field != "statusCommand" && field != "shell" {
|
||||||
|
return domain.ServerDeploymentView{}, validationError("deployment clearFields contains an invalid field")
|
||||||
|
}
|
||||||
|
seenClearFields[field] = true
|
||||||
|
}
|
||||||
definition := mergeDeploymentDefinition(instance.Deployment, update)
|
definition := mergeDeploymentDefinition(instance.Deployment, update)
|
||||||
definition.Revision = instance.Deployment.Revision + 1
|
definition.Revision = instance.Deployment.Revision + 1
|
||||||
definition.UpdatedAt = svc.now()
|
definition.UpdatedAt = svc.now()
|
||||||
@@ -194,6 +201,24 @@ func deploymentShellCapability(shell domain.ServerCommandShell) string {
|
|||||||
|
|
||||||
func mergeDeploymentDefinition(current domain.ServerDeploymentDefinition, update domain.ServerDeploymentUpdate) domain.ServerDeploymentDefinition {
|
func mergeDeploymentDefinition(current domain.ServerDeploymentDefinition, update domain.ServerDeploymentUpdate) domain.ServerDeploymentDefinition {
|
||||||
definition := domain.CopyServerDeploymentDefinition(current)
|
definition := domain.CopyServerDeploymentDefinition(current)
|
||||||
|
for _, field := range update.ClearFields {
|
||||||
|
switch field {
|
||||||
|
case "serverRoot":
|
||||||
|
definition.ServerRoot = ""
|
||||||
|
case "workingDirectory":
|
||||||
|
definition.WorkingDirectory = ""
|
||||||
|
case "installCommand":
|
||||||
|
definition.InstallCommand = ""
|
||||||
|
case "startCommand":
|
||||||
|
definition.StartCommand = ""
|
||||||
|
case "stopCommand":
|
||||||
|
definition.StopCommand = ""
|
||||||
|
case "statusCommand":
|
||||||
|
definition.StatusCommand = ""
|
||||||
|
case "shell":
|
||||||
|
definition.Shell = domain.ServerCommandShellNone
|
||||||
|
}
|
||||||
|
}
|
||||||
definition.Mode = update.Mode
|
definition.Mode = update.Mode
|
||||||
if update.ProfileKey != "" {
|
if update.ProfileKey != "" {
|
||||||
definition.ProfileKey = update.ProfileKey
|
definition.ProfileKey = update.ProfileKey
|
||||||
@@ -222,7 +247,9 @@ func mergeDeploymentDefinition(current domain.ServerDeploymentDefinition, update
|
|||||||
if update.StatusCommand != "" {
|
if update.StatusCommand != "" {
|
||||||
definition.StatusCommand = update.StatusCommand
|
definition.StatusCommand = update.StatusCommand
|
||||||
}
|
}
|
||||||
definition.Shell = update.Shell
|
if update.ShellSet {
|
||||||
|
definition.Shell = update.Shell
|
||||||
|
}
|
||||||
return definition
|
return definition
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,3 +64,15 @@ func TestCoreServiceSavesDraftDeploymentRedactsReadsAndDispatchesOnlyToCompatibl
|
|||||||
t.Fatalf("expected safe dispatch evidence, view=%+v err=%v", view, err)
|
t.Fatalf("expected safe dispatch evidence, view=%+v err=%v", view, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMergeDeploymentPreservesOmittedShellAndClearsOnlyExplicitFields(t *testing.T) {
|
||||||
|
current := domain.ServerDeploymentDefinition{Mode: domain.ServerDeploymentModeCustom, ServerRoot: "/srv/game", StartCommand: "./start", StopCommand: "./stop", Shell: domain.ServerCommandShellCmd}
|
||||||
|
preserved := mergeDeploymentDefinition(current, domain.ServerDeploymentUpdate{Mode: domain.ServerDeploymentModeCustom})
|
||||||
|
if preserved.Shell != domain.ServerCommandShellCmd || preserved.StopCommand != "./stop" {
|
||||||
|
t.Fatalf("omitted protected fields must be preserved: %+v", preserved)
|
||||||
|
}
|
||||||
|
cleared := mergeDeploymentDefinition(current, domain.ServerDeploymentUpdate{Mode: domain.ServerDeploymentModeCustom, ClearFields: []string{"stopCommand", "shell"}})
|
||||||
|
if cleared.StopCommand != "" || cleared.Shell != domain.ServerCommandShellNone || cleared.StartCommand != "./start" {
|
||||||
|
t.Fatalf("only explicitly cleared fields may be removed: %+v", cleared)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -170,6 +170,18 @@ func ValidateGamePluginRuntimeProfiles(profiles domain.GamePluginRuntimeProfiles
|
|||||||
if len(profile.SupportedTargets) == 0 {
|
if len(profile.SupportedTargets) == 0 {
|
||||||
violations = append(violations, prefix+".supportedTargets must not be empty")
|
violations = append(violations, prefix+".supportedTargets must not be empty")
|
||||||
}
|
}
|
||||||
|
prerequisiteKeys := map[string]struct{}{}
|
||||||
|
for j, prerequisite := range profile.Prerequisites {
|
||||||
|
prerequisitePrefix := fmt.Sprintf("%s.prerequisites[%d]", prefix, j)
|
||||||
|
violations = append(violations, validateProfileKey(prerequisitePrefix+".key", prerequisite.Key)...)
|
||||||
|
if _, exists := prerequisiteKeys[prerequisite.Key]; exists {
|
||||||
|
violations = append(violations, prerequisitePrefix+".key duplicates another prerequisite")
|
||||||
|
}
|
||||||
|
prerequisiteKeys[prerequisite.Key] = struct{}{}
|
||||||
|
if !oneOf(prerequisite.Kind, "steamcmd", "windows-vcredist", "windows-directx") {
|
||||||
|
violations = append(violations, prerequisitePrefix+".kind is invalid")
|
||||||
|
}
|
||||||
|
}
|
||||||
for j, target := range profile.SupportedTargets {
|
for j, target := range profile.SupportedTargets {
|
||||||
if !validPluginSupportedOS(target.OS) || !oneOf(target.Arch, "amd64", "arm64") {
|
if !validPluginSupportedOS(target.OS) || !oneOf(target.Arch, "amd64", "arm64") {
|
||||||
violations = append(violations, fmt.Sprintf("%s.supportedTargets[%d] is invalid", prefix, j))
|
violations = append(violations, fmt.Sprintf("%s.supportedTargets[%d] is invalid", prefix, j))
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { jobCapabilityLabel } from "./jobPresentation";
|
||||||
|
|
||||||
|
describe("jobCapabilityLabel", () => {
|
||||||
|
it("keeps Run routing identifiers out of operator task titles", () => {
|
||||||
|
expect(jobCapabilityLabel("process.install")).toBe("安装服务端");
|
||||||
|
expect(jobCapabilityLabel("dependencies.install")).toBe("安装运行依赖");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export function jobCapabilityLabel(capability: string): string {
|
||||||
|
switch (capability) {
|
||||||
|
case "process.install": return "安装服务端";
|
||||||
|
case "process.start": return "启动服务器";
|
||||||
|
case "process.stop": return "停止服务器";
|
||||||
|
case "process.status": return "检查服务器状态";
|
||||||
|
case "dependencies.check": return "检查运行依赖";
|
||||||
|
case "dependencies.install": return "安装运行依赖";
|
||||||
|
default: return capability;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ import { UsageMeter } from "../components/OperationControls";
|
|||||||
import { ProductionGovernancePanel } from "../components/ProductionGovernancePanel";
|
import { ProductionGovernancePanel } from "../components/ProductionGovernancePanel";
|
||||||
import { EmptyState, ErrorState, LoadingState } from "../components/StateViews";
|
import { EmptyState, ErrorState, LoadingState } from "../components/StateViews";
|
||||||
import { jobBuckets, moduleFreshnessLabel, summarizeEndpointOperations, type OperationsModuleState } from "../contracts/operationsConsole";
|
import { jobBuckets, moduleFreshnessLabel, summarizeEndpointOperations, type OperationsModuleState } from "../contracts/operationsConsole";
|
||||||
|
import { jobCapabilityLabel } from "../contracts/jobPresentation";
|
||||||
import type { PageComponentProps } from "../contracts/page";
|
import type { PageComponentProps } from "../contracts/page";
|
||||||
import type { GameTypeDistributionEntry, PlatformOverviewSignal } from "../contracts/workspace";
|
import type { GameTypeDistributionEntry, PlatformOverviewSignal } from "../contracts/workspace";
|
||||||
import { serverIsOnline } from "../contracts/workspace";
|
import { serverIsOnline } from "../contracts/workspace";
|
||||||
@@ -249,7 +250,7 @@ export function HomePage({ session, onNavigate, initialState }: HomePageProps) {
|
|||||||
className={cx("console-row console-row-button", job.state === "failed" && "console-row-danger")}
|
className={cx("console-row console-row-button", job.state === "failed" && "console-row-danger")}
|
||||||
onClick={() => onNavigate(job.serverInstanceId ? "serverDetail" : "maintenance", job.serverInstanceId ? { serverId: job.serverInstanceId } : undefined)}
|
onClick={() => onNavigate(job.serverInstanceId ? "serverDetail" : "maintenance", job.serverInstanceId ? { serverId: job.serverInstanceId } : undefined)}
|
||||||
>
|
>
|
||||||
<span><strong>{job.capability}</strong><small>{job.serverInstanceId ? `服务器 ${job.serverInstanceId}` : "平台任务"}</small></span>
|
<span><strong>{jobCapabilityLabel(job.capability)}</strong><small>{job.serverInstanceId ? `服务器 ${job.serverInstanceId}` : "平台任务"}</small></span>
|
||||||
<span>{job.state === "retrying" ? `重试 ${job.attempt}/${job.retryPolicy.maxAttempts}` : jobStateLabel(job.state)}</span>
|
<span>{job.state === "retrying" ? `重试 ${job.attempt}/${job.retryPolicy.maxAttempts}` : jobStateLabel(job.state)}</span>
|
||||||
<span>{Math.round(job.progress.percent)}%</span>
|
<span>{Math.round(job.progress.percent)}%</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -382,7 +383,7 @@ function buildOverviewSignals(
|
|||||||
collected.push({ id: `fault-${instance.id}`, kind: "fault", summary: `服务器 ${instance.name} 处于异常状态`, detail: `实例 ${instance.id} 需要排查`, targetPage: "servers", targetId: instance.id, tone: "error", at: instance.updatedAt });
|
collected.push({ id: `fault-${instance.id}`, kind: "fault", summary: `服务器 ${instance.name} 处于异常状态`, detail: `实例 ${instance.id} 需要排查`, targetPage: "servers", targetId: instance.id, tone: "error", at: instance.updatedAt });
|
||||||
}
|
}
|
||||||
for (const job of jobBuckets(core.data.jobs).failed.slice(0, 5)) {
|
for (const job of jobBuckets(core.data.jobs).failed.slice(0, 5)) {
|
||||||
collected.push({ id: `job-${job.id}`, kind: "job", summary: `任务 ${job.capability} 失败`, detail: job.serverInstanceId ? `服务器 ${job.serverInstanceId} 的任务执行失败` : "平台任务执行失败", targetPage: job.serverInstanceId ? "servers" : "maintenance", targetId: job.serverInstanceId, tone: "error", at: job.updatedAt });
|
collected.push({ id: `job-${job.id}`, kind: "job", summary: `任务 ${jobCapabilityLabel(job.capability)} 失败`, detail: job.serverInstanceId ? `服务器 ${job.serverInstanceId} 的任务执行失败` : "平台任务执行失败", targetPage: job.serverInstanceId ? "servers" : "maintenance", targetId: job.serverInstanceId, tone: "error", at: job.updatedAt });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (providers.status === "ready") {
|
if (providers.status === "ready") {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { platformApiClient } from "../api/client";
|
|||||||
import type { AuditEventResponse, JobResponse, RunEndpointResponse, ServerInstanceResponse } from "../api/types";
|
import type { AuditEventResponse, JobResponse, RunEndpointResponse, ServerInstanceResponse } from "../api/types";
|
||||||
import { EmptyState, ErrorState, LoadingState, ResultBadge } from "../components/StateViews";
|
import { EmptyState, ErrorState, LoadingState, ResultBadge } from "../components/StateViews";
|
||||||
import { ProductionGovernancePanel } from "../components/ProductionGovernancePanel";
|
import { ProductionGovernancePanel } from "../components/ProductionGovernancePanel";
|
||||||
|
import { jobCapabilityLabel } from "../contracts/jobPresentation";
|
||||||
import type { PageComponentProps } from "../contracts/page";
|
import type { PageComponentProps } from "../contracts/page";
|
||||||
import { cx } from "../utils/classes";
|
import { cx } from "../utils/classes";
|
||||||
|
|
||||||
@@ -227,7 +228,7 @@ export function MaintenancePage({ session, operations, onNavigate }: PageCompone
|
|||||||
return (
|
return (
|
||||||
<div key={job.id} className="console-record">
|
<div key={job.id} className="console-record">
|
||||||
<div className="console-record-head">
|
<div className="console-record-head">
|
||||||
<strong>{job.capability}</strong>
|
<strong>{jobCapabilityLabel(job.capability)}</strong>
|
||||||
<span className="status-pill status-error">{jobStateLabel(job.state)}</span>
|
<span className="status-pill status-error">{jobStateLabel(job.state)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="console-record-meta">
|
<div className="console-record-meta">
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import {
|
|||||||
} from "../components/RuntimeTaskProgress";
|
} from "../components/RuntimeTaskProgress";
|
||||||
import { DiagnosticSummary, EmptyState, ErrorState, LoadingState, ResultBadge } from "../components/StateViews";
|
import { DiagnosticSummary, EmptyState, ErrorState, LoadingState, ResultBadge } from "../components/StateViews";
|
||||||
import type { PageComponentProps } from "../contracts/page";
|
import type { PageComponentProps } from "../contracts/page";
|
||||||
|
import { jobCapabilityLabel } from "../contracts/jobPresentation";
|
||||||
import type { PluginBridgeAction, PluginBridgeManifestContract } from "../contracts/pluginBridge";
|
import type { PluginBridgeAction, PluginBridgeManifestContract } from "../contracts/pluginBridge";
|
||||||
import { canStartServer, canStopServer, defaultServerCreateForm, endpointLabel, pluginCreateInputDefaults, pluginLabel, runtimeBindingFields, serverMetadataFormFromInstance, type ServerCreateFormState, type ServerMetadataFormState } from "../contracts/serverManagement";
|
import { canStartServer, canStopServer, defaultServerCreateForm, endpointLabel, pluginCreateInputDefaults, pluginLabel, runtimeBindingFields, serverMetadataFormFromInstance, type ServerCreateFormState, type ServerMetadataFormState } from "../contracts/serverManagement";
|
||||||
import {
|
import {
|
||||||
@@ -2348,7 +2349,7 @@ function HistorySection({ serverId, serverOperations, jobs, artifacts, metricHis
|
|||||||
{jobs.slice(0, 20).map((job) => (
|
{jobs.slice(0, 20).map((job) => (
|
||||||
<div key={job.id} className="console-record">
|
<div key={job.id} className="console-record">
|
||||||
<div className="console-record-head">
|
<div className="console-record-head">
|
||||||
<strong>{job.capability}</strong>
|
<strong>{jobCapabilityLabel(job.capability)}</strong>
|
||||||
<span className={cx("status-pill", job.state === "succeeded" ? "status-active" : job.state === "failed" ? "status-error" : "status-disabled")}>
|
<span className={cx("status-pill", job.state === "succeeded" ? "status-active" : job.state === "failed" ? "status-error" : "status-disabled")}>
|
||||||
{job.state}
|
{job.state}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -585,6 +585,16 @@
|
|||||||
"installRootKey": "server/install-root",
|
"installRootKey": "server/install-root",
|
||||||
"configKey": "scum/server-settings",
|
"configKey": "scum/server-settings",
|
||||||
"configFormat": "ini",
|
"configFormat": "ini",
|
||||||
|
"prerequisites": [
|
||||||
|
{ "key": "steamcmd", "kind": "steamcmd" },
|
||||||
|
{ "key": "vcredist-2012-x86", "kind": "windows-vcredist" },
|
||||||
|
{ "key": "vcredist-2012-x64", "kind": "windows-vcredist" },
|
||||||
|
{ "key": "vcredist-2013-x86", "kind": "windows-vcredist" },
|
||||||
|
{ "key": "vcredist-2013-x64", "kind": "windows-vcredist" },
|
||||||
|
{ "key": "vcredist-2015-2022-x86", "kind": "windows-vcredist" },
|
||||||
|
{ "key": "vcredist-2015-2022-x64", "kind": "windows-vcredist" },
|
||||||
|
{ "key": "directx-jun2010", "kind": "windows-directx" }
|
||||||
|
],
|
||||||
"configMappings": [
|
"configMappings": [
|
||||||
{ "fieldKey": "serverName", "configKey": "server-settings.server-name", "valueType": "text", "required": true },
|
{ "fieldKey": "serverName", "configKey": "server-settings.server-name", "valueType": "text", "required": true },
|
||||||
{ "fieldKey": "gamePort", "configKey": "server-settings.game-port", "valueType": "port", "required": true },
|
{ "fieldKey": "gamePort", "configKey": "server-settings.game-port", "valueType": "port", "required": true },
|
||||||
|
|||||||
@@ -573,11 +573,21 @@
|
|||||||
"installRootKey": { "$ref": "#/$defs/logicalKey" },
|
"installRootKey": { "$ref": "#/$defs/logicalKey" },
|
||||||
"configKey": { "$ref": "#/$defs/logicalKey" },
|
"configKey": { "$ref": "#/$defs/logicalKey" },
|
||||||
"configFormat": { "enum": ["ini", "json", "yaml", "properties"] },
|
"configFormat": { "enum": ["ini", "json", "yaml", "properties"] },
|
||||||
|
"prerequisites": { "type": "array", "items": { "$ref": "#/$defs/runtimeServerPrerequisite" }, "maxItems": 16 },
|
||||||
"configMappings": { "type": "array", "items": { "$ref": "#/$defs/runtimeServerConfigMapping" }, "minItems": 1, "maxItems": 32, "uniqueItems": true },
|
"configMappings": { "type": "array", "items": { "$ref": "#/$defs/runtimeServerConfigMapping" }, "minItems": 1, "maxItems": 32, "uniqueItems": true },
|
||||||
"discoveryMarkers": { "type": "array", "items": { "$ref": "#/$defs/runtimeServerDiscoveryMarker" }, "minItems": 1, "maxItems": 32, "uniqueItems": true },
|
"discoveryMarkers": { "type": "array", "items": { "$ref": "#/$defs/runtimeServerDiscoveryMarker" }, "minItems": 1, "maxItems": 32, "uniqueItems": true },
|
||||||
"verificationChecks": { "type": "array", "items": { "$ref": "#/$defs/runtimeServerVerificationCheck" }, "minItems": 5, "maxItems": 16, "uniqueItems": true }
|
"verificationChecks": { "type": "array", "items": { "$ref": "#/$defs/runtimeServerVerificationCheck" }, "minItems": 5, "maxItems": 16, "uniqueItems": true }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"runtimeServerPrerequisite": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["key", "kind"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"key": { "$ref": "#/$defs/logicalKey" },
|
||||||
|
"kind": { "enum": ["steamcmd", "windows-vcredist", "windows-directx"] }
|
||||||
|
}
|
||||||
|
},
|
||||||
"runtimeLogSource": {
|
"runtimeLogSource": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["key", "kind", "streamKey"],
|
"required": ["key", "kind", "streamKey"],
|
||||||
|
|||||||
Reference in New Issue
Block a user