feat: confirm run deployment execution

This commit is contained in:
npc0-hue
2026-07-27 10:08:51 +08:00
parent 6c5b74b915
commit f2cc6b12e1
14 changed files with 241 additions and 18 deletions
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-07-27
@@ -0,0 +1,50 @@
## Context
Platform stores a deployment definition on the server instance and serializes it into a leased lifecycle assignment. The currently checked-out independent Run implementation has no matching `deployment` member in its protocol, so Go silently discards the incoming JSON field. Existing Platform dispatch evidence proves only that Platform queued a snapshot.
Raw host directories and command text are normally protected. The operator has explicitly authorized local Run startup diagnostics containing those values, but the repository's channel rules still prohibit sending them through Platform, plugins, artifacts, job results, audits, or browser logs.
## Goals / Non-Goals
**Goals:**
- Execute the same typed, frozen deployment revision that Platform dispatches.
- Provide an authenticated, non-sensitive execution receipt that ties a Run result to the deployment revision.
- Load authorized saved execution values directly into the edit dialog and make clearing optional values explicit.
- Emit approved raw diagnostics only to Run's local process logger for a start operation.
**Non-Goals:**
- No host browsing, direct Platform-to-host access, plugin access to execution inputs, credential logging, or raw values in any remotely transmitted channel.
- No re-addition of Run source to this repository.
- No implicit conversion of arbitrary command strings into shell execution; selected shell policy remains enforced by Run.
## Decisions
1. **Use a typed deployment envelope shared by copied contracts, with a required protocol version and definition revision.** Platform sends the immutable definition already stored with the job; Run rejects unsupported versions or invalid action/mode combinations. This is preferred to a separate mutable lookup because it preserves the job's execution fence.
2. **Add a compact execution receipt to the terminal result.** It contains only protocol version, deployment revision, lifecycle action, deployment mode, configured/effective-working-directory state, shell kind, and outcome. Platform validates receipt equality with the leased snapshot before projecting `Run confirmed rN`. A hash of raw values is deliberately excluded because low-entropy commands and paths are susceptible to guessing.
3. **Keep raw diagnostics local and opt-in.** A new Run configuration flag enables startup diagnostics. When enabled, the Run process logger prints server root, effective working directory, selected shell, and the exact command immediately before launch. This writer is distinct from the process log sink and cannot be passed into Run's log batch uploader. Credentials, runtime bindings, install/stop/status commands, and environment values remain excluded.
4. **Use explicit update operations for protected optional fields.** The deployment update DTO gains a `clearFields` list, while omitted values preserve the saved field and supplied non-empty values replace it. Shell is represented as an optional value so omission preserves it and an explicit empty shell selects direct argv mode. This prevents a normal edit from silently resetting shell configuration.
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.
## 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.
- [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.
- [Existing clients cannot express clearing] → The additive `clearFields` field preserves existing requests while enabling explicit removal.
## Migration Plan
1. Release Run support and advertise a new deployment-execution capability.
2. Release Platform support, requiring that capability before it dispatches a typed deployment job.
3. Enable local diagnostics only on Runs where the operator sets the explicit configuration flag.
4. Roll back by disabling the flag and stopping Platform dispatch to the new capability; saved deployment definitions remain intact.
## Open Questions
- None. The raw diagnostic allowance is constrained to the local Run process logger by the operator's instruction.
@@ -0,0 +1,31 @@
## Why
Platform persists a complete deployment definition, but the independent Run worker currently ignores the deployment body carried in its job assignment. Operators can inspect protected fields only through an extra action, and there is no trustworthy confirmation that a start job used the saved revision.
This change makes the editor load the authorized saved definition directly and makes Platform and Run execute and acknowledge the same frozen deployment revision. The user has explicitly approved printing the raw server directory and execution command in Run's local startup diagnostics only.
## What Changes
- Add a typed, versioned deployment execution body and execution receipt to the PlatformRun lifecycle contract. **BREAKING:** compatible Run binaries must be upgraded before Platform dispatches deployment-aware lifecycle jobs.
- Make Run validate and execute the frozen deployment body for install, start, stop, and status actions, including the configured working-directory and shell policy.
- Add opt-in local-only Run startup diagnostics that print the raw server directory, effective working directory, shell, and executable command. These diagnostics must not be uploaded, returned through the job API, stored as artifacts, or exposed to plugins or Platform Web.
- 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.
- Replace ambiguous empty-string update handling with explicit preserve/set/clear semantics and prevent an omitted shell from resetting the saved shell.
## Capabilities
### New Capabilities
- `run-deployment-execution-receipts`: Run consumes a frozen deployment definition and confirms the revision it executed without exposing protected values remotely.
- `local-run-startup-diagnostics`: opt-in local Run diagnostics print approved raw execution details while remaining outside all Platform channels.
### Modified Capabilities
- None.
## Impact
- 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.
- Operator workflow: opening an authorized stopped-server editor reads protected deployment values; normal list/detail/job/log/audit responses remain redacted.
@@ -0,0 +1,19 @@
## ADDED Requirements
### Requirement: Local raw startup diagnostics
When explicitly enabled in Run configuration, Run SHALL print the raw server root, effective working directory, shell, and exact start command to its local process logger immediately before executing a valid start job.
#### Scenario: Enabled diagnostics print local start context
- **WHEN** an enabled Run begins a valid custom-command start job
- **THEN** its local process logger SHALL contain the configured root, effective directory, shell, and command
#### Scenario: Disabled diagnostics remain silent
- **WHEN** local startup diagnostics are not enabled
- **THEN** Run SHALL not print raw deployment values
### Requirement: Raw diagnostics remain local
Run SHALL NOT include raw deployment values in log-batch uploads, job progress, terminal results, artifacts, audits, or plugin-facing payloads.
#### Scenario: Local diagnostics do not enter remote channels
- **WHEN** enabled Run diagnostics are emitted during a start job
- **THEN** the corresponding uploaded log events and terminal result SHALL contain no raw directory or command text
@@ -0,0 +1,34 @@
## ADDED Requirements
### Requirement: Frozen deployment execution
Platform SHALL send a typed, versioned deployment definition with every lifecycle job for a configured server, and Run SHALL reject a definition it cannot validate or execute for the requested lifecycle action.
#### Scenario: Start uses the leased revision
- **WHEN** Run receives a valid `process.start` job with deployment revision 7
- **THEN** it SHALL execute only the fields in that leased revision and SHALL not fetch a mutable replacement definition
#### Scenario: Incompatible Run is rejected
- **WHEN** a selected Run does not advertise deployment-execution support
- **THEN** Platform SHALL reject dispatch before assigning a lifecycle job
### Requirement: Execution revision receipt
Run SHALL include a non-sensitive deployment execution receipt in each terminal deployment-aware lifecycle result, and Platform SHALL validate the receipt against the job's leased definition before projecting confirmation.
#### Scenario: Matching receipt is projected
- **WHEN** Run succeeds a start job and returns the job's deployment revision and action
- **THEN** Platform SHALL show that Run confirmed execution of that revision
#### Scenario: Mismatched receipt is rejected
- **WHEN** Run reports a deployment revision or lifecycle action different from the lease
- **THEN** Platform SHALL reject the terminal result and SHALL not mark the job successful
### Requirement: Explicit deployment field clearing
Platform SHALL preserve a deployment value when an update omits it and SHALL clear an optional field only when the update explicitly names that field for clearing.
#### Scenario: Omitted shell remains unchanged
- **WHEN** an editor saves a deployment update without a shell value
- **THEN** Platform SHALL preserve the saved shell selection
#### Scenario: Optional command is cleared
- **WHEN** an editor submits `stopCommand` in its explicit clear list
- **THEN** Platform SHALL store no stop command for the next deployment revision
@@ -0,0 +1,29 @@
## 1. Task boundaries and contracts
- [x] 1.1 正向提示词:让授权操作者在编辑已停止服务器时读取完整已保存部署配置;让 Run 执行并确认同一冻结修订;开启本机诊断后可直接打印目录和启动命令,且所有成功路径都有自动化验证。
- [x] 1.2 方向提示词:在 `platform/``platform_web/` 沿用现有部署 DTO/生命周期/编辑工作流模式;在独立 `run.git` 的协议和生命周期执行器中实现匹配契约;验证 `go test ./...`、前端测试/类型检查、`scripts/check-structure.sh` 与严格 OpenSpec 校验。
- [x] 1.3 任务边界:不把 Run 源码移入 browser 仓库;不输出凭据、运行绑定或环境变量;不让原始目录/命令进入 Platform API、日志回传、任务结果、工件、审计或插件接口;不增加计费、云主机或无关 SaaS 能力。
## 2. Platform deployment contract
- [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.
- [ ] 2.3 Add explicit deployment-field clearing and preserve omitted shell values; cover service/API behavior with tests.
## 3. Platform editor workflow
- [x] 3.1 Automatically retrieve authorized saved inputs when the deployment editor opens and clear them on close.
- [x] 3.2 Display dispatch revision separately from Run-confirmed revision and update frontend tests/types.
## 4. Independent Run execution
- [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.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.
## 5. Verification and delivery
- [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.
- [ ] 5.3 Stage, commit, and push scoped changes on `main` in both repositories.
+20
View File
@@ -887,6 +887,16 @@ type ServerDeploymentDispatchEvidence struct {
JobState JobState
DeploymentRevision int
DeploymentDefinitionIncluded bool
RunConfirmed bool
}
type ServerDeploymentExecutionReceipt struct {
SchemaVersion string
Revision int
Action string
Mode ServerDeploymentMode
Shell ServerCommandShell
UsedServerRoot bool
}
type ConfigDiffLine struct {
@@ -1087,6 +1097,7 @@ type JobExecutionResult struct {
AuditSummary string
Content string
ServerDeploymentEvidence *ServerDeploymentEvidence
DeploymentReceipt *ServerDeploymentExecutionReceipt
}
type Job struct {
@@ -1798,6 +1809,14 @@ func CopyServerDeploymentEvidence(evidence *ServerDeploymentEvidence) *ServerDep
return &copy
}
func CopyServerDeploymentExecutionReceipt(receipt *ServerDeploymentExecutionReceipt) *ServerDeploymentExecutionReceipt {
if receipt == nil {
return nil
}
copy := *receipt
return &copy
}
func CopyServerDeploymentDefinition(definition ServerDeploymentDefinition) ServerDeploymentDefinition {
definition.RuntimeBindings = CopyStringMap(definition.RuntimeBindings)
definition.CreateInputs = CopyStringMap(definition.CreateInputs)
@@ -1877,6 +1896,7 @@ func CopyJob(job Job) Job {
job.ExecutionInput.SourceRCON = CopyRuntimeSourceRCONPlan(job.ExecutionInput.SourceRCON)
job.ExecutionInput.ServerDeploymentPlan = CopyServerDeploymentPlan(job.ExecutionInput.ServerDeploymentPlan)
job.ExecutionResult.ServerDeploymentEvidence = CopyServerDeploymentEvidence(job.ExecutionResult.ServerDeploymentEvidence)
job.ExecutionResult.DeploymentReceipt = CopyServerDeploymentExecutionReceipt(job.ExecutionResult.DeploymentReceipt)
if job.ExecutionInput.Deployment != nil {
copy := CopyServerDeploymentDefinition(*job.ExecutionInput.Deployment)
job.ExecutionInput.Deployment = &copy
+35 -12
View File
@@ -139,9 +139,19 @@ type ServerDeploymentEvidenceBody struct {
FailureCode string `json:"failureCode,omitempty"`
}
type ServerDeploymentExecutionReceiptBody struct {
SchemaVersion string `json:"schemaVersion"`
Revision int `json:"revision"`
Action string `json:"action"`
Mode domain.ServerDeploymentMode `json:"mode"`
Shell domain.ServerCommandShell `json:"shell,omitempty"`
UsedServerRoot bool `json:"usedServerRoot"`
}
// ServerDeploymentExecutionBody is included only in a leased Run assignment.
// It is intentionally absent from all public server and job response DTOs.
type ServerDeploymentExecutionBody struct {
SchemaVersion string `json:"schemaVersion"`
Mode domain.ServerDeploymentMode `json:"mode"`
ProfileKey string `json:"profileKey,omitempty"`
RuntimeBindings map[string]string `json:"runtimeBindings,omitempty"`
@@ -166,16 +176,17 @@ type RuntimeSourceRCONPlanBody struct {
}
type RunJobExecutionResultBody struct {
Kind string `json:"kind,omitempty"`
ProcessState string `json:"processState,omitempty"`
ExitClassification string `json:"exitClassification,omitempty"`
ExitCode int `json:"exitCode,omitempty"`
Version int `json:"version,omitempty"`
Checksum string `json:"checksum,omitempty"`
SizeBytes int64 `json:"sizeBytes,omitempty"`
AuditSummary string `json:"auditSummary,omitempty"`
Content string `json:"content,omitempty"`
ServerDeploymentEvidence *ServerDeploymentEvidenceBody `json:"serverDeploymentEvidence,omitempty"`
Kind string `json:"kind,omitempty"`
ProcessState string `json:"processState,omitempty"`
ExitClassification string `json:"exitClassification,omitempty"`
ExitCode int `json:"exitCode,omitempty"`
Version int `json:"version,omitempty"`
Checksum string `json:"checksum,omitempty"`
SizeBytes int64 `json:"sizeBytes,omitempty"`
AuditSummary string `json:"auditSummary,omitempty"`
Content string `json:"content,omitempty"`
ServerDeploymentEvidence *ServerDeploymentEvidenceBody `json:"serverDeploymentEvidence,omitempty"`
DeploymentReceipt *ServerDeploymentExecutionReceiptBody `json:"deploymentReceipt,omitempty"`
}
type RunJobResultResponse struct {
@@ -408,7 +419,7 @@ func (request RunJobResultRequest) ToDomain() domain.RunJobResult {
Message: request.Message,
ErrorCode: request.ErrorCode,
Retryable: request.Retryable,
ExecutionResult: domain.JobExecutionResult{Kind: request.ExecutionResult.Kind, ProcessState: request.ExecutionResult.ProcessState, ExitClassification: request.ExecutionResult.ExitClassification, ExitCode: request.ExecutionResult.ExitCode, Version: request.ExecutionResult.Version, Checksum: request.ExecutionResult.Checksum, SizeBytes: request.ExecutionResult.SizeBytes, AuditSummary: request.ExecutionResult.AuditSummary, Content: request.ExecutionResult.Content, ServerDeploymentEvidence: serverDeploymentEvidenceToDomain(request.ExecutionResult.ServerDeploymentEvidence)},
ExecutionResult: domain.JobExecutionResult{Kind: request.ExecutionResult.Kind, ProcessState: request.ExecutionResult.ProcessState, ExitClassification: request.ExecutionResult.ExitClassification, ExitCode: request.ExecutionResult.ExitCode, Version: request.ExecutionResult.Version, Checksum: request.ExecutionResult.Checksum, SizeBytes: request.ExecutionResult.SizeBytes, AuditSummary: request.ExecutionResult.AuditSummary, Content: request.ExecutionResult.Content, ServerDeploymentEvidence: serverDeploymentEvidenceToDomain(request.ExecutionResult.ServerDeploymentEvidence), DeploymentReceipt: deploymentReceiptToDomain(request.ExecutionResult.DeploymentReceipt)},
}
}
@@ -657,13 +668,25 @@ func serverDeploymentEvidenceFromDomain(evidence *domain.ServerDeploymentEvidenc
}
return &ServerDeploymentEvidenceBody{TemplateKey: evidence.TemplateKey, TemplateVersion: evidence.TemplateVersion, PreflightState: evidence.PreflightState, DiscoveryState: evidence.DiscoveryState, MappingState: evidence.MappingState, VerificationState: evidence.VerificationState, DiscoveredFacts: domain.CopyStringMap(evidence.DiscoveredFacts), MappingResults: domain.CopyStringMap(evidence.MappingResults), VerificationResults: domain.CopyStringMap(evidence.VerificationResults), FailureCode: evidence.FailureCode}
}
func deploymentReceiptToDomain(body *ServerDeploymentExecutionReceiptBody) *domain.ServerDeploymentExecutionReceipt {
if body == nil {
return nil
}
return &domain.ServerDeploymentExecutionReceipt{SchemaVersion: body.SchemaVersion, Revision: body.Revision, Action: body.Action, Mode: body.Mode, Shell: body.Shell, UsedServerRoot: body.UsedServerRoot}
}
func deploymentReceiptFromDomain(value *domain.ServerDeploymentExecutionReceipt) *ServerDeploymentExecutionReceiptBody {
if value == nil {
return nil
}
return &ServerDeploymentExecutionReceiptBody{SchemaVersion: value.SchemaVersion, Revision: value.Revision, Action: value.Action, Mode: value.Mode, Shell: value.Shell, UsedServerRoot: value.UsedServerRoot}
}
func deploymentExecutionFromDomain(definition *domain.ServerDeploymentDefinition) *ServerDeploymentExecutionBody {
if definition == nil {
return nil
}
copy := domain.CopyServerDeploymentDefinition(*definition)
return &ServerDeploymentExecutionBody{Mode: copy.Mode, ProfileKey: copy.ProfileKey, RuntimeBindings: copy.RuntimeBindings, CreateInputs: copy.CreateInputs, ServerRoot: copy.ServerRoot, WorkingDirectory: copy.WorkingDirectory, InstallCommand: copy.InstallCommand, StartCommand: copy.StartCommand, StopCommand: copy.StopCommand, StatusCommand: copy.StatusCommand, Shell: copy.Shell, Revision: copy.Revision}
return &ServerDeploymentExecutionBody{SchemaVersion: "1", Mode: copy.Mode, ProfileKey: copy.ProfileKey, RuntimeBindings: copy.RuntimeBindings, CreateInputs: copy.CreateInputs, ServerRoot: copy.ServerRoot, WorkingDirectory: copy.WorkingDirectory, InstallCommand: copy.InstallCommand, StartCommand: copy.StartCommand, StopCommand: copy.StopCommand, StatusCommand: copy.StatusCommand, Shell: copy.Shell, Revision: copy.Revision}
}
func runtimeSourceRCONPlanFromDomain(plan *domain.RuntimeSourceRCONPlan) *RuntimeSourceRCONPlanBody {
+2 -1
View File
@@ -54,6 +54,7 @@ type ServerDeploymentDispatchEvidenceBody struct {
JobState domain.JobState `json:"jobState"`
DeploymentRevision int `json:"deploymentRevision"`
DeploymentDefinitionIncluded bool `json:"deploymentDefinitionIncluded"`
RunConfirmed bool `json:"runConfirmed"`
}
type ServerDeploymentProjectionBody struct {
@@ -133,7 +134,7 @@ func deploymentDispatchEvidenceFromDomain(evidence *domain.ServerDeploymentDispa
if evidence == nil {
return nil
}
return &ServerDeploymentDispatchEvidenceBody{JobID: evidence.JobID, JobState: evidence.JobState, DeploymentRevision: evidence.DeploymentRevision, DeploymentDefinitionIncluded: evidence.DeploymentDefinitionIncluded}
return &ServerDeploymentDispatchEvidenceBody{JobID: evidence.JobID, JobState: evidence.JobState, DeploymentRevision: evidence.DeploymentRevision, DeploymentDefinitionIncluded: evidence.DeploymentDefinitionIncluded, RunConfirmed: evidence.RunConfirmed}
}
func deploymentProjectionFromDomain(projection domain.ServerDeploymentProjection) ServerDeploymentProjectionBody {
+6
View File
@@ -269,6 +269,12 @@ func (svc *CoreService) CompleteRunJob(result domain.RunJobResult) (domain.RunJo
}
func validateExecutionResultForJob(job domain.Job, result domain.RunJobResult) error {
if definition := job.ExecutionInput.Deployment; definition != nil {
receipt := result.ExecutionResult.DeploymentReceipt
if receipt == nil || receipt.SchemaVersion != "1" || receipt.Revision != definition.Revision || receipt.Action != job.ExecutionInput.LifecycleOperation || receipt.Mode != definition.Mode || receipt.Shell != definition.Shell {
return validationError("deployment execution receipt does not match leased definition")
}
}
if job.ExecutionInput.ServerDeploymentPlan != nil {
return validateSCUMDeploymentEvidence(job.ExecutionInput.ServerDeploymentPlan, result)
}
+3 -1
View File
@@ -242,7 +242,9 @@ func (svc *CoreService) deploymentView(instance domain.ServerInstance) (domain.S
if job.ExecutionInput.Deployment == nil || (view.LatestDispatch != nil && !job.CreatedAt.After(latestDispatchAt)) {
continue
}
view.LatestDispatch = &domain.ServerDeploymentDispatchEvidence{JobID: job.ID, JobState: job.State, DeploymentRevision: job.ExecutionInput.Deployment.Revision, DeploymentDefinitionIncluded: true}
receipt := job.ExecutionResult.DeploymentReceipt
confirmed := receipt != nil && receipt.Revision == job.ExecutionInput.Deployment.Revision && receipt.Action == job.ExecutionInput.LifecycleOperation
view.LatestDispatch = &domain.ServerDeploymentDispatchEvidence{JobID: job.ID, JobState: job.State, DeploymentRevision: job.ExecutionInput.Deployment.Revision, DeploymentDefinitionIncluded: true, RunConfirmed: confirmed}
latestDispatchAt = job.CreatedAt
}
return domain.CopyServerDeploymentView(view), nil
+2 -1
View File
@@ -546,7 +546,8 @@ export interface ServerDeploymentDispatchEvidenceResponse {
jobId: string;
jobState: JobState;
deploymentRevision: number;
deploymentDefinitionIncluded: boolean;
deploymentDefinitionIncluded: boolean;
runConfirmed: boolean;
}
export interface RuntimeBindingUpdateRequest {
@@ -54,6 +54,11 @@ export function ServerDeploymentWorkflow({ open, kind, plugins, endpoints, initi
setRevealError("");
}, [initialForm, kind, open]);
useEffect(() => {
if (!open || kind !== "edit" || !onReveal) return;
void revealSavedInputs();
}, [kind, onReveal, open]);
function updateForm(event: ChangeEvent<HTMLInputElement | HTMLSelectElement>) {
const { name, value } = event.target;
setForm((current) => {
@@ -111,7 +116,7 @@ export function ServerDeploymentWorkflow({ open, kind, plugins, endpoints, initi
const protectedState = (nextValue: string, configured: boolean) => nextValue.trim() ? "将替换" : configured ? "保持已配置" : "未配置";
const actionLabel = kind === "create" ? "保存草稿并准备专属 Run" : "保存部署设置";
return <ManagementDialog open={open} title={kind === "create" ? "创建服务器" : "编辑部署"} description={kind === "create" ? "按部署顺序完成设置;路径和命令始终受保护,不会在确认页或日志中回显。" : "仅停止中的服务器可以修改部署设置。受保护路径和命令留空会保持原值;可主动显示已保存配置。"} wide onClose={closeWorkflow}>
return <ManagementDialog open={open} title={kind === "create" ? "创建服务器" : "编辑部署"} description={kind === "create" ? "按部署顺序完成设置;路径和命令始终受保护,不会在确认页或日志中回显。" : "仅停止中的服务器可以修改部署设置。已保存的受保护路径和命令仅在本窗口内读取,关闭后清除。"} wide onClose={closeWorkflow}>
<form className="provider-form dialog-form server-deployment-workflow" onSubmit={(event) => void submit(event)} aria-label={kind === "create" ? "创建服务器部署向导" : "编辑服务器部署向导"}>
<ol className="deployment-workflow-steps" style={{ gridTemplateColumns: `repeat(${workflowSteps.length}, minmax(0, 1fr))` }} aria-label="部署步骤">{workflowSteps.map((item, index) => { const Icon = item.icon; return <li key={item.label} className={cx(index === step && "deployment-workflow-step-active", index < step && "deployment-workflow-step-complete")}><span>{index < step ? <CheckCircle2 size={15} /> : <Icon size={15} />}</span><strong>{index + 1}. {item.label}</strong></li>; })}</ol>
{step === targetStep && <div className="deployment-workflow-body">
@@ -124,7 +129,7 @@ export function ServerDeploymentWorkflow({ open, kind, plugins, endpoints, initi
<ModeOption active={form.deploymentMode === "existing-server"} title="接管已有服务器" copy="预检指定目录并接入已有实例;不会把它当作一次新安装。" onClick={() => setForm((current) => ({ ...current, deploymentMode: "existing-server" }))} />
<ModeOption active={form.deploymentMode === "custom-command"} title="自定义启动方式" copy="用于非标准启动器或脚本;需由节点策略允许。" onClick={() => setForm((current) => ({ ...current, deploymentMode: "custom-command" }))} />
</div></div>}
{step === configurationStep && <div className="deployment-workflow-body">{kind === "edit" && onReveal && <div className="form-guidance"><strong></strong><span></span><button type="button" className="primary-command" disabled={busy || revealBusy} onClick={() => void revealSavedInputs()}>{revealBusy ? "读取中…" : "显示已保存配置"}</button>{revealError && <span className="field-help">{revealError}</span>}</div>}<div className="form-grid">
{step === configurationStep && <div className="deployment-workflow-body">{kind === "edit" && onReveal && <div className="form-guidance"><strong></strong><span>{revealBusy ? "正在读取已保存的目录和命令…" : "这些值只保留在当前编辑窗口,关闭后会清除。"}</span>{revealError && <><span className="field-help">{revealError}</span><button type="button" className="primary-command" disabled={busy || revealBusy} onClick={() => void revealSavedInputs()}></button></>}</div>}<div className="form-grid">
{kind === "create" && <label><input name="name" value={form.name} onChange={updateForm} placeholder="Example Survival #3" required /></label>}
{kind === "create" && <label><select name="profileKey" value={form.profileKey} onChange={updateForm}><option value="">使</option>{profileOptions.map((profile) => <option key={profile.key} value={profile.key}>{profile.key} · {profile.mode}</option>)}</select><small className="field-help"></small></label>}
{kind === "edit" && <label><select name="deploymentMode" value={form.deploymentMode} onChange={updateForm}><option value="guided-install"></option><option value="existing-server"></option><option value="custom-command"></option></select><small className="field-help"></small></label>}
+1 -1
View File
@@ -466,7 +466,7 @@ function ServerDeploymentSection({ instance, deployment, onEdit }: ServerDeploym
return <article className="console-panel" aria-label="server deployment">
<div className="panel-header"><h2><PackageOpen size={16} style={{ verticalAlign: "-2px" }} /> </h2><span className="page-status">{view.mode || "未配置"} · {view.revision}</span></div>
<p className="section-copy"></p>
<div className="console-row-list"><div className="console-row"><span></span><strong>{view.serverRootConfigured ? "已配置" : "未配置"}</strong></div><div className="console-row"><span></span><strong>{view.workingDirectoryConfigured ? "已配置" : "使用服务器目录"}</strong></div><div className="console-row"><span></span><strong>{view.startCommandConfigured ? "已配置" : view.mode === "custom-command" ? "未配置" : "插件引导"}</strong></div>{view.latestDispatch && <div className="console-row"><span> Run </span><strong>{view.latestDispatch.deploymentDefinitionIncluded ? `部署定义已随任务发送 · r${view.latestDispatch.deploymentRevision} · ${view.latestDispatch.jobState}` : "未携带部署定义"}</strong></div>}</div>
<div className="console-row-list"><div className="console-row"><span></span><strong>{view.serverRootConfigured ? "已配置" : "未配置"}</strong></div><div className="console-row"><span></span><strong>{view.workingDirectoryConfigured ? "已配置" : "使用服务器目录"}</strong></div><div className="console-row"><span></span><strong>{view.startCommandConfigured ? "已配置" : view.mode === "custom-command" ? "未配置" : "插件引导"}</strong></div>{view.latestDispatch && <div className="console-row"><span> Run </span><strong>{view.latestDispatch.deploymentDefinitionIncluded ? `部署定义已随任务发送 · r${view.latestDispatch.deploymentRevision} · ${view.latestDispatch.jobState}` : "未携带部署定义"}</strong></div>}{view.latestDispatch?.runConfirmed && <div className="console-row"><span>Run </span><strong> r{view.latestDispatch.deploymentRevision} </strong></div>}</div>
{isScumTemplate && <div className="console-row-list" style={{ marginTop: 12 }}><div className="console-row"><span>SCUM </span><strong>{projection?.templateVersion ? `${projection.templateKey ?? "已选择"} · v${projection.templateVersion}` : "等待 Run 预检"}</strong></div><div className="console-row"><span> / </span><strong>{deploymentProjectionLabel(projection?.preflightState)} / {deploymentProjectionLabel(projection?.discoveryState)}</strong></div><div className="console-row"><span> / </span><strong>{deploymentProjectionLabel(projection?.mappingState)} / {deploymentProjectionLabel(projection?.verificationState)}</strong></div>{projection?.failureCode && <div className="console-row"><span></span><strong>{projection.failureCode}</strong></div>}</div>}
<div className="action-strip" style={{ marginTop: 12 }}><button type="button" className="primary-command" disabled={instance.state === "running" || instance.state === "installing"} onClick={onEdit}><Pencil size={14} /><span></span></button>{(instance.state === "draft" || instance.state === "failed") && <span className="field-help"></span>}</div>
</article>;