From a5207472cf9c584e7f437393a41cda81d1995ea2 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Sat, 6 Jun 2026 23:08:39 +0800 Subject: [PATCH] fix: decorate fallback openclaw artifact downloads --- internal/acp/routing_test.go | 84 ++++++++++++++++++++++++++++++++++++ internal/acp/rpc_handler.go | 13 +++++- 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/internal/acp/routing_test.go b/internal/acp/routing_test.go index 24c858b..805ce96 100644 --- a/internal/acp/routing_test.go +++ b/internal/acp/routing_test.go @@ -1600,6 +1600,78 @@ func TestExecuteSessionTaskGatewayExportsOpenClawArtifacts(t *testing.T) { } } +func TestExecuteSessionTaskGatewayDecoratesFallbackArtifacts(t *testing.T) { + gateway := newAcpFakeOpenClawGateway(t) + defer gateway.Close() + + t.Setenv("GATEWAY_RPC_URL", gateway.URL()) + t.Setenv("BRIDGE_AUTH_TOKEN", "bridge-token") + + server := NewServer() + response, rpcErr := server.executeSessionTask(task{ + req: shared.RPCRequest{ + Method: "session.start", + Params: map[string]any{ + "sessionId": "session-openclaw-fallback-artifact", + "threadId": "thread-openclaw-fallback-artifact", + "taskPrompt": "fallback artifact", + "workingDirectory": t.TempDir(), + "routing": map[string]any{ + "routingMode": "explicit", + "explicitExecutionTarget": "gateway", + "preferredGatewayProviderId": "openclaw", + }, + }, + }, + }) + if rpcErr != nil { + t.Fatalf("expected gateway response, got rpc error: %#v", rpcErr) + } + artifacts := []map[string]any{} + rawArtifacts, ok := response["artifacts"].([]any) + if !ok { + t.Fatalf("expected artifacts payload, got %#v", response["artifacts"]) + } + for _, item := range rawArtifacts { + artifacts = append(artifacts, shared.AsMap(item)) + } + if len(artifacts) != 1 { + t.Fatalf("expected one fallback artifact, got %#v", response["artifacts"]) + } + if got := shared.StringArg(artifacts[0], "relativePath", ""); got != "ai-news-2026-06.md" { + t.Fatalf("expected fallback artifact relative path, got %#v", artifacts[0]) + } + downloadURL := strings.TrimSpace(shared.StringArg(artifacts[0], "downloadUrl", "")) + if downloadURL == "" { + t.Fatalf("expected bridge to decorate fallback artifact downloadUrl, got %#v", artifacts[0]) + } + parsedDownloadURL, err := url.Parse(downloadURL) + if err != nil { + t.Fatalf("parse downloadUrl: %v", err) + } + if got := parsedDownloadURL.Path; got != openClawArtifactDownloadPath { + t.Fatalf("expected bridge artifact download path, got %q from %q", got, downloadURL) + } + if got := parsedDownloadURL.Query().Get("sessionKey"); got != shared.StringArg(response, "openclawSessionKey", "") { + t.Fatalf("expected mapped sessionKey in downloadUrl, got %q", got) + } + if got := parsedDownloadURL.Query().Get("runId"); got != response["runId"].(string) { + t.Fatalf("expected runId in downloadUrl, got %q", got) + } + if got := parsedDownloadURL.Query().Get("relativePath"); got != "ai-news-2026-06.md" { + t.Fatalf("expected artifact relativePath in downloadUrl, got %q", got) + } + if parsedDownloadURL.Query().Get("sig") == "" { + t.Fatalf("expected signed downloadUrl, got %q", downloadURL) + } + if _, ok := artifacts[0]["content"]; ok { + t.Fatalf("expected OpenClaw task response to omit inline artifact content, got %#v", artifacts[0]) + } + if got := gateway.Methods(); !sameMethods(got, []string{"connect", "xworkmate.session.prepare", "chat.send", "xworkmate.tasks.get"}) { + t.Fatalf("expected connect, prepare, chat.send, then native task lookup, got %#v", got) + } +} + func TestExecuteSessionTaskGatewayDoesNotTreatPromptTextAsArtifactContract(t *testing.T) { gateway := newAcpFakeOpenClawGateway(t) defer gateway.Close() @@ -3203,6 +3275,18 @@ func newAcpFakeOpenClawGateway(t *testing.T) *acpFakeOpenClawGateway { "downloadUrl": downloadURL("reports/final.md"), }) } + if strings.Contains(runMessage, "fallback artifact") { + artifacts = append(artifacts, map[string]any{ + "relativePath": "ai-news-2026-06.md", + "label": "ai-news-2026-06.md", + "contentType": "text/markdown", + "sizeBytes": 5089, + "sha256": "fake-sha256", + "artifactScope": artifactScope, + "scopeKind": "task", + "artifactRef": "tasks://fallback/ai-news-2026-06.md", + }) + } if strings.Contains(runMessage, "make pdf artifact") { artifacts = append(artifacts, map[string]any{ "relativePath": "exports/final.pdf", diff --git a/internal/acp/rpc_handler.go b/internal/acp/rpc_handler.go index 4a03180..d53f3c2 100644 --- a/internal/acp/rpc_handler.go +++ b/internal/acp/rpc_handler.go @@ -117,7 +117,18 @@ func (s *Server) handleTaskGet(ctx context.Context, params map[string]any, notif if result.OK { payload := shared.AsMap(result.Payload) s.mergeOpenClawTaskGetArtifactExport(payload, params, gatewayProvider, notify) - return normalizeOpenClawTaskGetResult(params, payload, gatewayProvider) + payload = normalizeOpenClawTaskGetResult(params, payload, gatewayProvider) + sessionKey := firstNonEmptyString(payload, "openclawSessionKey", "sessionKey") + if sessionKey == "" { + sessionKey = strings.TrimSpace(shared.StringArg(params, "openclawSessionKey", "")) + } + runID := firstNonEmptyString(payload, "runId", "taskId") + if runID == "" { + runID = strings.TrimSpace(shared.StringArg(params, "runId", "")) + } + s.decorateOpenClawArtifactDownloadURLs(payload, sessionKey, runID) + stripOpenClawArtifactInlineContent(payload) + return payload } message := strings.TrimSpace(shared.StringArg(result.Error, "message", "openclaw native task lookup failed")) code := strings.TrimSpace(shared.StringArg(result.Error, "code", "TASK_LOOKUP_FAILED"))