Synthesize default server file workspace

This commit is contained in:
npc0-hue
2026-08-24 14:35:10 +08:00
parent 3521a7f579
commit 49b90ebd29
2 changed files with 64 additions and 14 deletions
+23
View File
@@ -286,6 +286,29 @@ func TestCoreAPIServerFileWorkspaceRoutesAreScoped(t *testing.T) {
assertErrorResponse(t, requestWithAuth(t, router, http.MethodGet, "/api/v1/server-instances/"+instance.ID+"/files/workspace", "", otherSession), http.StatusForbidden, errorCodeForbidden) assertErrorResponse(t, requestWithAuth(t, router, http.MethodGet, "/api/v1/server-instances/"+instance.ID+"/files/workspace", "", otherSession), http.StatusForbidden, errorCodeForbidden)
} }
func TestCoreAPIServerFileWorkspaceSynthesizesDefaultDirectoryForLegacyPlugin(t *testing.T) {
router := newTestRouter()
adminSession := createAdminSession(t, router)
pluginRequest := validGamePluginRequest()
pluginRequest.RequiredRunCapabilities = append(pluginRequest.RequiredRunCapabilities, domain.JobCapabilityFilesList, domain.JobCapabilityFilesRead, domain.JobCapabilityFilesWrite)
pluginRequest.DeclaredPermissions = []string{"server.files.read", "server.files.write"}
pluginRequest.Permissions.Files = true
postJSON[dto.GamePluginResponse](t, router, "/api/v1/game-plugins", pluginRequest)
endpointRequest := validRunEndpointRequest()
endpointRequest.Capabilities = append(endpointRequest.Capabilities, domain.JobCapabilityFilesList, domain.JobCapabilityFilesRead, domain.JobCapabilityFilesWrite)
postJSON[dto.RunEndpointResponse](t, router, "/api/v1/run/endpoints", endpointRequest)
instance := postJSONWithAuth[dto.ServerInstanceResponse](t, router, "/api/v1/server-instances", dto.ServerInstanceCreateRequest{ID: "server-file-legacy-api", PluginID: "server.scum", RunEndpointID: "run-local", Name: "Legacy File API Server", State: domain.ServerInstanceStateRunning}, adminSession)
workspace := getJSONWithAuth[dto.ServerFileWorkspaceResponse](t, router, "/api/v1/server-instances/"+instance.ID+"/files/workspace", adminSession)
if workspace.DefaultDirectoryKey != "server-files" || workspace.DeclaredOnly || len(workspace.Directories) == 0 || workspace.Directories[0].Label != "服务器文件" || workspace.Directories == nil || workspace.Files == nil || workspace.ConfigFields == nil {
t.Fatalf("expected synthesized non-null workspace, got %+v", workspace)
}
list := getJSONWithAuth[dto.ServerFileListResponse](t, router, "/api/v1/server-instances/"+instance.ID+"/files/list", adminSession)
if list.DirectoryKey != "server-files" || list.Entries == nil || !strings.Contains(list.Reason, "通用服务器文件目录") {
t.Fatalf("expected default file list, got %+v", list)
}
}
func TestCoreAPIServerRuntimeDistributionAndJobWorkflows(t *testing.T) { func TestCoreAPIServerRuntimeDistributionAndJobWorkflows(t *testing.T) {
releaseBuilds := make(chan struct{}) releaseBuilds := make(chan struct{})
t.Cleanup(func() { close(releaseBuilds) }) t.Cleanup(func() { close(releaseBuilds) })
+41 -14
View File
@@ -16,8 +16,10 @@ import (
) )
const ( const (
serverFileTransferChannel = "run-file-transfer" serverFileTransferChannel = "run-file-transfer"
serverFileMaxInlineEditBytes = 64 * 1024 serverFileMaxInlineEditBytes = 64 * 1024
serverFileDefaultDirectoryKey = "server-files"
serverFileDefaultLogDirectoryKey = "server-logs"
) )
type serverFileContext struct { type serverFileContext struct {
@@ -25,6 +27,7 @@ type serverFileContext struct {
Instance domain.ServerInstance Instance domain.ServerInstance
Plugin domain.GamePlugin Plugin domain.GamePlugin
Directory domain.PluginLogicalDirectory Directory domain.PluginLogicalDirectory
Workspace domain.PluginFileWorkspace
Scope string Scope string
} }
@@ -54,10 +57,7 @@ func (svc *CoreService) GetServerFileWorkspaceForSession(sessionID string, serve
if err != nil { if err != nil {
return domain.ServerFileWorkspaceView{}, err return domain.ServerFileWorkspaceView{}, err
} }
workspace := domain.CopyPluginFileWorkspace(ctx.Plugin.FileWorkspace) workspace := domain.CopyPluginFileWorkspace(ctx.Workspace)
if workspace.DefaultDirectoryKey == "" && len(workspace.Directories) > 0 {
workspace.DefaultDirectoryKey = workspace.Directories[0].Key
}
view := domain.ServerFileWorkspaceView{ view := domain.ServerFileWorkspaceView{
ServerInstanceID: ctx.Instance.ID, ServerInstanceID: ctx.Instance.ID,
PluginID: ctx.Plugin.ID, PluginID: ctx.Plugin.ID,
@@ -65,7 +65,7 @@ func (svc *CoreService) GetServerFileWorkspaceForSession(sessionID string, serve
Directories: workspace.Directories, Directories: workspace.Directories,
Files: workspace.Files, Files: workspace.Files,
ConfigFields: workspace.ConfigFields, ConfigFields: workspace.ConfigFields,
DeclaredOnly: true, DeclaredOnly: serverFileWorkspaceIsPluginDeclared(ctx.Plugin.FileWorkspace),
RuntimeWorkspaceScope: svc.runtimeProfileScope(ctx.Instance.ID), RuntimeWorkspaceScope: svc.runtimeProfileScope(ctx.Instance.ID),
Transfer: domain.ServerFileTransferPolicy{ Transfer: domain.ServerFileTransferPolicy{
Channel: serverFileTransferChannel, Channel: serverFileTransferChannel,
@@ -107,7 +107,14 @@ func (svc *CoreService) ListServerFilesForSession(sessionID string, request doma
state = "pending" state = "pending"
reason = "Run 正在刷新目录。" reason = "Run 正在刷新目录。"
} }
entries := filterServerFileEntries(serverFileEntriesFromDeclaredWorkspace(ctx.Plugin.FileWorkspace, request.DirectoryKey), request.Query) entries := filterServerFileEntries(serverFileEntriesFromDeclaredWorkspace(ctx.Workspace, request.DirectoryKey), request.Query)
if state == "declared" {
reasonPrefix := "展示插件声明的逻辑文件"
if !serverFileWorkspaceIsPluginDeclared(ctx.Plugin.FileWorkspace) {
reasonPrefix = "展示通用服务器文件目录"
}
reason = reasonPrefix + ";点击刷新可请求 Run 返回实时目录。"
}
return domain.CopyServerFileListResult(domain.ServerFileListResult{ServerInstanceID: ctx.Instance.ID, PluginID: ctx.Plugin.ID, DirectoryKey: request.DirectoryKey, Path: request.Path, State: state, Entries: entries, Job: latest, RefreshedAt: latest.TerminalAt, Reason: reason}), nil return domain.CopyServerFileListResult(domain.ServerFileListResult{ServerInstanceID: ctx.Instance.ID, PluginID: ctx.Plugin.ID, DirectoryKey: request.DirectoryKey, Path: request.Path, State: state, Entries: entries, Job: latest, RefreshedAt: latest.TerminalAt, Reason: reason}), nil
} }
@@ -142,7 +149,7 @@ func (svc *CoreService) RefreshServerFileListForSession(sessionID string, reques
if err != nil { if err != nil {
return domain.ServerFileListResult{}, err return domain.ServerFileListResult{}, err
} }
entries := filterServerFileEntries(serverFileEntriesFromDeclaredWorkspace(ctx.Plugin.FileWorkspace, request.DirectoryKey), request.Query) entries := filterServerFileEntries(serverFileEntriesFromDeclaredWorkspace(ctx.Workspace, request.DirectoryKey), request.Query)
return domain.CopyServerFileListResult(domain.ServerFileListResult{ServerInstanceID: ctx.Instance.ID, PluginID: ctx.Plugin.ID, DirectoryKey: request.DirectoryKey, Path: request.Path, State: "pending", Entries: entries, Job: job, Reason: "目录刷新任务已派发到 Run。"}), nil return domain.CopyServerFileListResult(domain.ServerFileListResult{ServerInstanceID: ctx.Instance.ID, PluginID: ctx.Plugin.ID, DirectoryKey: request.DirectoryKey, Path: request.Path, State: "pending", Entries: entries, Job: job, Reason: "目录刷新任务已派发到 Run。"}), nil
} }
@@ -213,7 +220,7 @@ func (svc *CoreService) PrepareServerFileDownloadForSession(sessionID string, re
if err != nil { if err != nil {
return domain.ServerFileDownloadResult{}, err return domain.ServerFileDownloadResult{}, err
} }
filename := serverFileDisplayName(ctx.Plugin.FileWorkspace, request.Key) filename := serverFileDisplayName(ctx.Workspace, request.Key)
job, hasJob, err := svc.latestFileReadJob(ctx.Instance.ID, request.Key) job, hasJob, err := svc.latestFileReadJob(ctx.Instance.ID, request.Key)
if err != nil { if err != nil {
return domain.ServerFileDownloadResult{}, err return domain.ServerFileDownloadResult{}, err
@@ -323,21 +330,41 @@ func (svc *CoreService) serverFileContextForSession(sessionID string, serverInst
} else if !plugin.Permissions.Files && !containsString(plugin.DeclaredPermissions, "server.files.read") { } else if !plugin.Permissions.Files && !containsString(plugin.DeclaredPermissions, "server.files.read") {
return serverFileContext{}, ErrForbidden return serverFileContext{}, ErrForbidden
} }
workspace := effectiveServerFileWorkspace(plugin)
directory := domain.PluginLogicalDirectory{} directory := domain.PluginLogicalDirectory{}
if directoryKey != "" { if directoryKey != "" {
var found bool var found bool
for _, candidate := range plugin.FileWorkspace.Directories { for _, candidate := range workspace.Directories {
if candidate.Key == directoryKey { if candidate.Key == directoryKey {
directory = candidate directory = candidate
found = true found = true
break break
} }
} }
if !found && len(plugin.FileWorkspace.Directories) > 0 { if !found && len(workspace.Directories) > 0 {
return serverFileContext{}, validationError("directoryKey must reference a plugin-declared directory") return serverFileContext{}, validationError("directoryKey must reference an available logical directory")
} }
} }
return serverFileContext{User: user, Instance: instance, Plugin: plugin, Directory: directory, Scope: svc.runtimeProfileScope(instance.ID)}, nil return serverFileContext{User: user, Instance: instance, Plugin: plugin, Directory: directory, Workspace: workspace, Scope: svc.runtimeProfileScope(instance.ID)}, nil
}
func effectiveServerFileWorkspace(plugin domain.GamePlugin) domain.PluginFileWorkspace {
workspace := domain.CopyPluginFileWorkspace(plugin.FileWorkspace)
if len(workspace.Directories) == 0 && workspace.DefaultDirectoryKey == "" {
workspace.DefaultDirectoryKey = serverFileDefaultDirectoryKey
workspace.Directories = []domain.PluginLogicalDirectory{{Key: serverFileDefaultDirectoryKey, Label: "服务器文件", Scope: "config"}}
if plugin.Permissions.Logs || containsString(plugin.DeclaredPermissions, "server.logs.read") || len(plugin.RuntimeProfiles.LogSources) > 0 {
workspace.Directories = append(workspace.Directories, domain.PluginLogicalDirectory{Key: serverFileDefaultLogDirectoryKey, Label: "日志文件", Scope: "logs"})
}
}
if workspace.DefaultDirectoryKey == "" && len(workspace.Directories) > 0 {
workspace.DefaultDirectoryKey = workspace.Directories[0].Key
}
return workspace
}
func serverFileWorkspaceIsPluginDeclared(workspace domain.PluginFileWorkspace) bool {
return workspace.DefaultDirectoryKey != "" || len(workspace.Directories) > 0 || len(workspace.Files) > 0 || len(workspace.ConfigFields) > 0
} }
func normalizeServerFileListRequest(request domain.ServerFileListRequest) domain.ServerFileListRequest { func normalizeServerFileListRequest(request domain.ServerFileListRequest) domain.ServerFileListRequest {