first commit

This commit is contained in:
npc0-hue
2026-07-11 14:56:10 +08:00
commit 7e05d0a4e7
660 changed files with 78119 additions and 0 deletions
+233
View File
@@ -0,0 +1,233 @@
package service
import (
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"strings"
"browser.local/platform/domain"
"browser.local/platform/repo"
"browser.local/platform/validator"
)
func (svc *CoreService) CreateServerInstanceWorkflow(create domain.ServerLifecycleCreate) (domain.ServerLifecycleResult, error) {
create = domain.CopyServerLifecycleCreate(create)
if err := validator.ValidateServerLifecycleCreate(create); err != nil {
return domain.ServerLifecycleResult{}, err
}
plugin, endpoint, err := svc.lifecycleDependencies(create.PluginID, create.RunEndpointID)
if err != nil {
return domain.ServerLifecycleResult{}, err
}
if err := validateLifecycleActionRef(plugin, domain.ServerLifecycleActionCreate); err != nil {
return domain.ServerLifecycleResult{}, err
}
stamp := svc.now()
instance := domain.ServerInstance{
ID: create.ID,
PluginID: create.PluginID,
PluginVersion: plugin.Version,
RunEndpointID: create.RunEndpointID,
Name: create.Name,
OwnerUserID: create.OwnerUserID,
State: domain.ServerInstanceStateInstalling,
ConfigVersion: 1,
CreatedAt: stamp,
UpdatedAt: stamp,
}
if err := validator.ValidateServerInstance(instance); err != nil {
return domain.ServerLifecycleResult{}, err
}
if err := validator.ValidateServerInstanceDependencies(instance, plugin, endpoint); err != nil {
return domain.ServerLifecycleResult{}, err
}
if err := validateRunnableEndpoint(endpoint, domain.LifecycleCapabilityForAction(domain.ServerLifecycleActionCreate)); err != nil {
return domain.ServerLifecycleResult{}, err
}
if err := svc.validateLifecycleIdempotency(instance.RunEndpointID, create.IdempotencyKey, instance.ID, domain.LifecycleCapabilityForAction(domain.ServerLifecycleActionCreate)); err != nil {
return domain.ServerLifecycleResult{}, err
}
if err := svc.store.ServerInstances().Create(instance); err != nil {
return domain.ServerLifecycleResult{}, err
}
job, err := svc.dispatchLifecycleJob(instance, domain.ServerLifecycleActionCreate, create.IdempotencyKey)
if err != nil {
return domain.ServerLifecycleResult{}, err
}
return domain.CopyServerLifecycleResult(domain.ServerLifecycleResult{
Accepted: true,
Action: domain.ServerLifecycleActionCreate,
Instance: instance,
Job: job,
}), nil
}
func (svc *CoreService) CreateServerInstanceWorkflowForSession(sessionID string, create domain.ServerLifecycleCreate) (domain.ServerLifecycleResult, error) {
user, err := svc.GetCurrentUser(sessionID)
if err != nil {
return domain.ServerLifecycleResult{}, err
}
if strings.TrimSpace(create.OwnerUserID) == "" {
create.OwnerUserID = user.ID
}
if !isPlatformAdmin(user) && create.OwnerUserID != user.ID {
return domain.ServerLifecycleResult{}, ErrForbidden
}
return svc.CreateServerInstanceWorkflow(create)
}
func (svc *CoreService) StartServerInstance(command domain.ServerLifecycleCommand) (domain.ServerLifecycleResult, error) {
return svc.dispatchExistingServerLifecycle(command, domain.ServerLifecycleActionStart, []domain.ServerInstanceState{
domain.ServerInstanceStateReady,
domain.ServerInstanceStateStopped,
})
}
func (svc *CoreService) StartServerInstanceForSession(sessionID string, command domain.ServerLifecycleCommand) (domain.ServerLifecycleResult, error) {
if err := svc.authorizeServerLifecycle(sessionID, command.ServerInstanceID); err != nil {
return domain.ServerLifecycleResult{}, err
}
return svc.StartServerInstance(command)
}
func (svc *CoreService) StopServerInstance(command domain.ServerLifecycleCommand) (domain.ServerLifecycleResult, error) {
return svc.dispatchExistingServerLifecycle(command, domain.ServerLifecycleActionStop, []domain.ServerInstanceState{
domain.ServerInstanceStateRunning,
})
}
func (svc *CoreService) StopServerInstanceForSession(sessionID string, command domain.ServerLifecycleCommand) (domain.ServerLifecycleResult, error) {
if err := svc.authorizeServerLifecycle(sessionID, command.ServerInstanceID); err != nil {
return domain.ServerLifecycleResult{}, err
}
return svc.StopServerInstance(command)
}
func (svc *CoreService) dispatchExistingServerLifecycle(command domain.ServerLifecycleCommand, action domain.ServerLifecycleAction, allowedStates []domain.ServerInstanceState) (domain.ServerLifecycleResult, error) {
command = domain.CopyServerLifecycleCommand(command)
if err := validator.ValidateServerLifecycleCommand(command); err != nil {
return domain.ServerLifecycleResult{}, err
}
if err := validator.ValidateServerLifecycleAction(action); err != nil {
return domain.ServerLifecycleResult{}, err
}
instance, err := svc.store.ServerInstances().Get(command.ServerInstanceID)
if err != nil {
return domain.ServerLifecycleResult{}, err
}
if instance.ConfigVersion != command.ExpectedConfigVersion {
return domain.ServerLifecycleResult{}, validationError("expectedConfigVersion must match server instance")
}
if !serverStateAllowed(instance.State, allowedStates) {
return domain.ServerLifecycleResult{}, validationError(fmt.Sprintf("server instance state %q cannot %s", instance.State, action))
}
plugin, endpoint, err := svc.lifecycleDependencies(instance.PluginID, instance.RunEndpointID)
if err != nil {
return domain.ServerLifecycleResult{}, err
}
if err := validateLifecycleActionRef(plugin, action); err != nil {
return domain.ServerLifecycleResult{}, err
}
if err := validator.ValidateServerInstanceDependencies(instance, plugin, endpoint); err != nil {
return domain.ServerLifecycleResult{}, err
}
if err := validateRunnableEndpoint(endpoint, domain.LifecycleCapabilityForAction(action)); err != nil {
return domain.ServerLifecycleResult{}, err
}
job, err := svc.dispatchLifecycleJob(instance, action, command.IdempotencyKey)
if err != nil {
return domain.ServerLifecycleResult{}, err
}
return domain.CopyServerLifecycleResult(domain.ServerLifecycleResult{
Accepted: true,
Action: action,
Instance: instance,
Job: job,
}), nil
}
func (svc *CoreService) lifecycleDependencies(pluginID string, runEndpointID string) (domain.GamePlugin, domain.RunEndpoint, error) {
plugin, err := svc.store.GamePlugins().Get(pluginID)
if err != nil {
return domain.GamePlugin{}, domain.RunEndpoint{}, fmt.Errorf("get plugin dependency: %w", err)
}
endpoint, err := svc.store.RunEndpoints().Get(runEndpointID)
if err != nil {
return domain.GamePlugin{}, domain.RunEndpoint{}, fmt.Errorf("get run endpoint dependency: %w", err)
}
return plugin, endpoint, nil
}
func (svc *CoreService) dispatchLifecycleJob(instance domain.ServerInstance, action domain.ServerLifecycleAction, idempotencyKey string) (domain.Job, error) {
capability := domain.LifecycleCapabilityForAction(action)
job, err := svc.CreateJob(domain.Job{
ID: lifecycleJobID(instance.ID, action, idempotencyKey),
ServerInstanceID: instance.ID,
RunEndpointID: instance.RunEndpointID,
Capability: capability,
IdempotencyKey: idempotencyKey,
})
if err != nil {
return domain.Job{}, err
}
if job.ServerInstanceID != instance.ID || job.RunEndpointID != instance.RunEndpointID || job.Capability != capability {
return domain.Job{}, validationError("idempotencyKey is already used for a different lifecycle target")
}
return job, nil
}
func (svc *CoreService) validateLifecycleIdempotency(runEndpointID string, idempotencyKey string, serverInstanceID string, capability string) error {
existing, err := svc.store.Jobs().GetByIdempotency(runEndpointID, idempotencyKey)
if errors.Is(err, repo.ErrNotFound) {
return nil
}
if err != nil {
return err
}
if existing.ServerInstanceID == serverInstanceID && existing.Capability == capability {
return nil
}
return validationError("idempotencyKey is already used for a different lifecycle target")
}
func validateLifecycleActionRef(plugin domain.GamePlugin, action domain.ServerLifecycleAction) error {
if strings.TrimSpace(lifecycleActionRef(plugin, action)) == "" {
return validationError(fmt.Sprintf("plugin %s lifecycle action is required", action))
}
return nil
}
func lifecycleActionRef(plugin domain.GamePlugin, action domain.ServerLifecycleAction) string {
switch action {
case domain.ServerLifecycleActionCreate:
return plugin.LifecycleActions.Install
case domain.ServerLifecycleActionStart:
return plugin.LifecycleActions.Start
case domain.ServerLifecycleActionStop:
return plugin.LifecycleActions.Stop
default:
return ""
}
}
func serverStateAllowed(state domain.ServerInstanceState, allowed []domain.ServerInstanceState) bool {
for _, candidate := range allowed {
if state == candidate {
return true
}
}
return false
}
func lifecycleJobID(serverInstanceID string, action domain.ServerLifecycleAction, idempotencyKey string) string {
sum := sha256.Sum256([]byte(idempotencyKey))
return fmt.Sprintf("server-lifecycle:%s:%s:%s", serverInstanceID, action, hex.EncodeToString(sum[:8]))
}