fix: export claimed openclaw artifacts

This commit is contained in:
Haitao Pan 2026-05-07 10:10:01 +08:00
parent 3932c6bd8f
commit 2a86a19677
2 changed files with 179 additions and 13 deletions

View File

@ -6,6 +6,7 @@ import (
"fmt"
"net/url"
"os"
"path"
"path/filepath"
"strings"
"time"
@ -249,18 +250,25 @@ func (o *SessionOrchestrator) runOpenClawGatewayChat(
if preparedArtifact == nil {
preparedArtifact = openClawPreparedArtifactScopeFromPayload(result)
}
mergeOpenClawArtifactPayload(result, o.openClawArtifactExportForDelivery(
artifactDeliveryClaimed := !artifactDeliveryRequired &&
openClawArtifactDeliveryRequired(map[string]any{"message": output})
artifactPayload := o.openClawArtifactExportForDelivery(
gatewayProvider,
chatParams,
artifactRunID,
artifactSinceUnixMs,
preparedArtifact,
artifactDeliveryRequired || preparedArtifact != nil,
artifactDeliveryRequired || artifactDeliveryClaimed || preparedArtifact != nil,
artifactDeliveryClaimed,
notifyWithCollection,
))
)
if artifactDeliveryClaimed {
artifactPayload = filterOpenClawArtifactPayloadByOutput(output, artifactPayload)
}
mergeOpenClawArtifactPayload(result, artifactPayload)
o.server.decorateOpenClawArtifactDownloadURLs(result, shared.StringArg(chatParams, "sessionKey", ""), artifactRunID)
stripOpenClawArtifactInlineContent(result)
guardOpenClawArtifactResult(result, artifactDeliveryRequired)
guardOpenClawArtifactResult(result, artifactDeliveryRequired || artifactDeliveryClaimed)
return result, nil
}
@ -271,6 +279,7 @@ func (o *SessionOrchestrator) openClawArtifactExportForDelivery(
sinceUnixMs int64,
preparedArtifact *openClawPreparedArtifactScope,
artifactDeliveryRequired bool,
latestTaskScopeIfEmpty bool,
notify func(map[string]any),
) map[string]any {
if !artifactDeliveryRequired {
@ -281,6 +290,7 @@ func (o *SessionOrchestrator) openClawArtifactExportForDelivery(
sinceUnixMs,
preparedArtifact,
false,
false,
notify,
)
}
@ -294,6 +304,7 @@ func (o *SessionOrchestrator) openClawArtifactExportForDelivery(
sinceUnixMs,
preparedArtifact,
true,
latestTaskScopeIfEmpty,
notify,
)
remoteWorkingDirectory := strings.TrimSpace(shared.StringArg(payload, "remoteWorkingDirectory", ""))
@ -518,6 +529,7 @@ func (o *SessionOrchestrator) openClawArtifactExport(
sinceUnixMs int64,
preparedArtifact *openClawPreparedArtifactScope,
latestIfEmpty bool,
latestTaskScopeIfEmpty bool,
notify func(map[string]any),
) map[string]any {
sessionKey := strings.TrimSpace(shared.StringArg(chatParams, "sessionKey", ""))
@ -538,6 +550,9 @@ func (o *SessionOrchestrator) openClawArtifactExport(
if latestIfEmpty {
exportParams["latestIfEmpty"] = true
}
if latestTaskScopeIfEmpty {
exportParams["latestTaskScopeIfEmpty"] = true
}
exportResult := o.server.gateway.RequestByMode(
gatewayProvider,
"xworkmate.artifacts.export",
@ -577,6 +592,58 @@ func guardOpenClawArtifactResult(result map[string]any, artifactDeliveryRequired
)
}
func filterOpenClawArtifactPayloadByOutput(output string, payload map[string]any) map[string]any {
if payload == nil {
return nil
}
output = strings.ToLower(output)
if strings.TrimSpace(output) == "" {
return payload
}
filtered := map[string]any{}
for key, value := range payload {
filtered[key] = value
}
matchedAny := false
for _, key := range []string{"artifacts", "files", "attachments"} {
list := shared.ListArg(payload, key)
if len(list) == 0 {
continue
}
filteredList := make([]any, 0, len(list))
for _, item := range list {
artifact := shared.AsMap(item)
relativePath := strings.TrimSpace(shared.StringArg(artifact, "relativePath", ""))
if relativePath == "" {
relativePath = strings.TrimSpace(shared.StringArg(artifact, "path", ""))
}
if relativePath == "" {
relativePath = strings.TrimSpace(shared.StringArg(artifact, "name", ""))
}
if openClawOutputMentionsArtifactPath(output, relativePath) {
filteredList = append(filteredList, item)
matchedAny = true
}
}
filtered[key] = filteredList
}
if !matchedAny {
return payload
}
return filtered
}
func openClawOutputMentionsArtifactPath(output string, relativePath string) bool {
relativePath = strings.TrimSpace(strings.ReplaceAll(relativePath, "\\", "/"))
if relativePath == "" {
return false
}
normalizedPath := strings.ToLower(relativePath)
base := strings.ToLower(path.Base(normalizedPath))
return strings.Contains(output, normalizedPath) ||
(base != "." && base != "" && strings.Contains(output, base))
}
func mergeOpenClawArtifactPayload(result map[string]any, source map[string]any) {
if result == nil || len(source) == 0 {
return
@ -901,6 +968,7 @@ func (o *SessionOrchestrator) completeOpenClawScopedArtifactExport(
0,
preparedArtifact,
true,
false,
nil,
))
o.server.decorateOpenClawArtifactDownloadURLs(result, sessionKey, runID)

View File

@ -800,6 +800,97 @@ func TestExecuteSessionTaskGatewayExportsLatestWorkspaceArtifactsWhenScopedDirec
}
}
func TestExecuteSessionMessageGatewayExportsClaimedOpenClawArtifacts(t *testing.T) {
gateway := newAcpFakeOpenClawGateway(t)
gateway.artifactMode = "workspace-latest"
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.message",
Params: map[string]any{
"sessionId": "session-openclaw-claimed-artifact",
"threadId": "thread-openclaw-claimed-artifact",
"taskPrompt": "hi hallucinate-files",
"workingDirectory": t.TempDir(),
"routing": map[string]any{
"routingMode": "explicit",
"explicitExecutionTarget": "gateway",
"preferredGatewayProviderId": "openclaw",
},
},
},
})
if rpcErr != nil {
t.Fatalf("expected claimed artifact response, got rpc error: %#v", rpcErr)
}
if got := response["success"]; got != true {
t.Fatalf("expected successful claimed artifact response, got %#v", response)
}
artifacts, ok := response["artifacts"].([]map[string]any)
if !ok {
raw, ok := response["artifacts"].([]any)
if !ok {
t.Fatalf("expected artifacts payload, got %#v", response["artifacts"])
}
artifacts = make([]map[string]any, 0, len(raw))
for _, item := range raw {
artifacts = append(artifacts, shared.AsMap(item))
}
}
if len(artifacts) != 1 {
t.Fatalf("expected one claimed artifact, got %#v", artifacts)
}
if got := artifacts[0]["relativePath"]; got != "existing/report.pdf" {
t.Fatalf("expected claimed latest artifact relative path, got %#v", artifacts[0])
}
if got := strings.TrimSpace(shared.StringArg(artifacts[0], "downloadUrl", "")); got == "" {
t.Fatalf("expected bridge downloadUrl on claimed artifact, got %#v", artifacts[0])
}
exportParams := gateway.LastArtifactExportParams()
if got := shared.BoolArg(shared.StringArg(exportParams, "latestIfEmpty", ""), false); !got {
t.Fatalf("expected latestIfEmpty export param, got %#v", exportParams)
}
if got := shared.BoolArg(shared.StringArg(exportParams, "latestTaskScopeIfEmpty", ""), false); !got {
t.Fatalf("expected latestTaskScopeIfEmpty export param, got %#v", exportParams)
}
if _, ok := exportParams["artifactScope"]; ok {
t.Fatalf("expected no new prepared artifact scope for claimed follow-up, got %#v", exportParams)
}
if got := gateway.Methods(); !sameMethods(got, []string{"connect", "chat.send", "agent.wait", "xworkmate.artifacts.export"}) {
t.Fatalf("expected connect, chat.send, agent.wait, then artifact export, got %#v", got)
}
}
func TestFilterOpenClawArtifactPayloadByOutputKeepsMentionedFiles(t *testing.T) {
payload := map[string]any{
"remoteWorkingDirectory": "/remote/openclaw/workspace",
"artifacts": []any{
map[string]any{"relativePath": "k8s-networking.pdf"},
map[string]any{"relativePath": "k8s-networking.docx"},
map[string]any{"relativePath": "generate_all.py"},
},
}
filtered := filterOpenClawArtifactPayloadByOutput(
"文件已经生成好了k8s-networking.pdf, k8s-networking.docx",
payload,
)
artifacts := shared.ListArg(filtered, "artifacts")
if len(artifacts) != 2 {
t.Fatalf("expected only mentioned artifacts, got %#v", artifacts)
}
if got := shared.StringArg(shared.AsMap(artifacts[0]), "relativePath", ""); got != "k8s-networking.pdf" {
t.Fatalf("expected pdf artifact first, got %#v", artifacts)
}
if got := shared.StringArg(shared.AsMap(artifacts[1]), "relativePath", ""); got != "k8s-networking.docx" {
t.Fatalf("expected docx artifact second, got %#v", artifacts)
}
}
func TestNormalizeResultStripsOpenClawInlineArtifactsAfterRecordNormalization(t *testing.T) {
server := NewServer()
orchestrator := NewSessionOrchestrator(server)
@ -1729,19 +1820,26 @@ func newAcpFakeOpenClawGateway(t *testing.T) *acpFakeOpenClawGateway {
}
if fake.artifactMode == "workspace-latest" &&
shared.BoolArg(shared.StringArg(params, "latestIfEmpty", ""), false) &&
artifactScope != "" &&
len(payload["artifacts"].([]any)) == 0 {
if artifactScope == "" &&
!shared.BoolArg(shared.StringArg(params, "latestTaskScopeIfEmpty", ""), false) {
break
}
if artifactScope == "" {
artifactScope = "tasks/" + strings.TrimSpace(shared.StringArg(params, "sessionKey", "main")) + "/previous-run"
}
payload["scopeKind"] = "workspace-latest"
payload["artifacts"] = []any{
map[string]any{
"relativePath": "existing/report.pdf",
"label": "report.pdf",
"contentType": "application/pdf",
"sizeBytes": 3,
"sha256": "latest-sha256",
"scopeKind": "workspace-latest",
"encoding": "base64",
"content": "cGRm",
"relativePath": "existing/report.pdf",
"label": "report.pdf",
"contentType": "application/pdf",
"sizeBytes": 3,
"sha256": "latest-sha256",
"artifactScope": artifactScope,
"scopeKind": "workspace-latest",
"encoding": "base64",
"content": "cGRm",
},
}
}