From 1b583f3ca67f591eba7dadb617953630a56efe19 Mon Sep 17 00:00:00 2001 From: npc0-hue Date: Mon, 7 Sep 2026 18:54:56 +0800 Subject: [PATCH] Fix server file list fallback --- platform/service/resources_test.go | 44 ++++++++++++++++++++++++++++++ platform/service/server_files.go | 24 +++++++++++++++- scripts/local-debug/smoke.sh | 17 ++++++++++++ 3 files changed, 84 insertions(+), 1 deletion(-) diff --git a/platform/service/resources_test.go b/platform/service/resources_test.go index 003efd8..0e3b574 100644 --- a/platform/service/resources_test.go +++ b/platform/service/resources_test.go @@ -1165,6 +1165,50 @@ func TestServerFileListFallsBackToPluginWorkspaceWithoutRunListCapability(t *tes } } +func TestServerFileListFallsBackToPluginWorkspaceWhenRunEndpointUnavailable(t *testing.T) { + svc := newTestCoreService() + plugin, endpoint := createPluginAndRunEndpoint(t, svc) + plugin.FileWorkspace = scumTestFileWorkspace() + if err := svc.store.GamePlugins().Update(plugin); err != nil { + t.Fatalf("update plugin workspace: %v", err) + } + ownerSession := createServiceUserAndLogin(t, svc, domain.User{ID: "user-file-list-run-missing", DisplayName: "File List Run Missing", Email: "file-list-run-missing@example.test", Roles: []string{"server-owner"}, PasswordHash: "secret-password"}) + instance, err := svc.CreateServerInstanceForSession(ownerSession, domain.ServerInstance{ID: "server-file-list-run-missing", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "File List Missing Run", State: domain.ServerInstanceStateRunning}) + if err != nil { + t.Fatalf("create server: %v", err) + } + if err := svc.store.RunEndpoints().Delete(endpoint.ID); err != nil { + t.Fatalf("delete run endpoint: %v", err) + } + + refresh, err := svc.RefreshServerFileListForSession(ownerSession, domain.ServerFileListRequest{ServerInstanceID: instance.ID, DirectoryKey: "scum-config", IdempotencyKey: "idem-file-list-run-missing"}) + if err != nil { + t.Fatalf("refresh file list without run endpoint: %v", err) + } + if refresh.State != "ready" || refresh.Job.ID != "" || len(refresh.Entries) != 2 || refresh.Entries[1].LogicalKey != "scum-server-settings" || !strings.Contains(refresh.Reason, "Run 注册记录不存在") { + t.Fatalf("expected declared workspace fallback without run endpoint, got %+v", refresh) + } + jobs, err := svc.store.Jobs().List(domain.JobFilter{ServerInstanceID: instance.ID}) + if err != nil { + t.Fatalf("list missing-run jobs: %v", err) + } + if len(jobs) != 0 { + t.Fatalf("missing Run endpoint must not create a failed refresh job, got %+v", jobs) + } + + draft, err := svc.CreateServerInstanceForSession(ownerSession, domain.ServerInstance{ID: "server-file-list-run-unbound", PluginID: plugin.ID, Name: "File List Unbound Run", State: domain.ServerInstanceStateDraft}) + if err != nil { + t.Fatalf("create unbound draft server: %v", err) + } + unbound, err := svc.RefreshServerFileListForSession(ownerSession, domain.ServerFileListRequest{ServerInstanceID: draft.ID, DirectoryKey: "scum-config", IdempotencyKey: "idem-file-list-run-unbound"}) + if err != nil { + t.Fatalf("refresh file list without run binding: %v", err) + } + if unbound.State != "ready" || unbound.Job.ID != "" || len(unbound.Entries) != 2 || unbound.Entries[1].LogicalKey != "scum-server-settings" || !strings.Contains(unbound.Reason, "尚未绑定 Run") { + t.Fatalf("expected declared workspace fallback without run binding, got %+v", unbound) + } +} + func TestDeclaredFileReadSnapshotProjectionStatesAndPassThroughContent(t *testing.T) { svc := newTestCoreService() plugin, endpoint := createPluginAndRunEndpoint(t, svc) diff --git a/platform/service/server_files.go b/platform/service/server_files.go index 0ad3a80..d4dec61 100644 --- a/platform/service/server_files.go +++ b/platform/service/server_files.go @@ -152,11 +152,33 @@ func (svc *CoreService) RefreshServerFileListForSession(sessionID string, reques if err != nil { return domain.ServerFileListResult{}, err } + entries := filterServerFileEntries(serverFileEntriesFromDeclaredWorkspace(ctx.Workspace, request.DirectoryKey), request.Query) + if strings.TrimSpace(ctx.Instance.RunEndpointID) == "" { + return domain.CopyServerFileListResult(domain.ServerFileListResult{ + ServerInstanceID: ctx.Instance.ID, + PluginID: ctx.Plugin.ID, + DirectoryKey: request.DirectoryKey, + Path: request.Path, + State: "ready", + Entries: entries, + Reason: "当前服务器尚未绑定 Run,已展示插件声明的逻辑文件。", + }), nil + } endpoint, err := svc.GetRunEndpoint(ctx.Instance.RunEndpointID) if err != nil { + if errors.Is(err, repo.ErrNotFound) { + return domain.CopyServerFileListResult(domain.ServerFileListResult{ + ServerInstanceID: ctx.Instance.ID, + PluginID: ctx.Plugin.ID, + DirectoryKey: request.DirectoryKey, + Path: request.Path, + State: "ready", + Entries: entries, + Reason: "当前 Run 注册记录不存在,已展示插件声明的逻辑文件。", + }), nil + } return domain.ServerFileListResult{}, err } - entries := filterServerFileEntries(serverFileEntriesFromDeclaredWorkspace(ctx.Workspace, request.DirectoryKey), request.Query) if !containsString(endpoint.Capabilities, domain.JobCapabilityFilesList) { return domain.CopyServerFileListResult(domain.ServerFileListResult{ ServerInstanceID: ctx.Instance.ID, diff --git a/scripts/local-debug/smoke.sh b/scripts/local-debug/smoke.sh index b996410..3bc8ffd 100755 --- a/scripts/local-debug/smoke.sh +++ b/scripts/local-debug/smoke.sh @@ -442,6 +442,9 @@ const bridgeActions = Array.isArray(plugin.bridgeActions) ? plugin.bridgeActions const lifecycleProfiles = plugin.runtimeProfiles?.lifecycleProfiles ?? []; const logSources = plugin.runtimeProfiles?.logSources ?? []; const pageKeys = (plugin.pages ?? []).map((page) => page.key); +const fileWorkspace = plugin.fileWorkspace ?? {}; +const fileWorkspaceDirectoryKeys = (fileWorkspace.directories ?? []).map((directory) => directory.key); +const fileWorkspaceFileKeys = (fileWorkspace.files ?? []).map((file) => file.key); if (plugin.version !== expectedVersion) { missing.push(`version ${expectedVersion}`); } @@ -473,6 +476,19 @@ if (!createFields.some((field) => field.key === "queryPort" && field.defaultValu if (!createFields.some((field) => field.key === "maxPlayers" && field.defaultValue === "128")) { missing.push("SCUM max players create field"); } +if (fileWorkspace.defaultDirectoryKey !== "scum-config") { + missing.push("SCUM file workspace default directory"); +} +for (const directoryKey of ["scum-config", "scum-logs"]) { + if (!fileWorkspaceDirectoryKeys.includes(directoryKey)) { + missing.push(`SCUM file workspace directory ${directoryKey}`); + } +} +for (const fileKey of ["scum-server-settings", "scum-chat-log"]) { + if (!fileWorkspaceFileKeys.includes(fileKey)) { + missing.push(`SCUM file workspace file ${fileKey}`); + } +} if (!plugin.gameClientBridge?.commands?.length) { missing.push("game client bridge declarations"); } @@ -1146,6 +1162,7 @@ const manifest = { ai: source.ai, productionLifecycle: source.productionLifecycle, remoteAccess: source.remoteAccess, + fileWorkspace: source.fileWorkspace, runtimeProfiles: localRuntimeProfiles, gameClientBridge: localGameClientBridge };