Stabilize OpenClaw artifact finality
This commit is contained in:
parent
169ec72783
commit
8fc9a614f2
@ -29,6 +29,8 @@ type OpenClawTaskRecord struct {
|
||||
ProgressMessage string
|
||||
PreparedArtifact *openClawPreparedArtifactScope
|
||||
RequiresArtifactExport bool
|
||||
ExpectedArtifactDirs []string
|
||||
RequiredArtifactExts []string
|
||||
ResolvedModel string
|
||||
ResolvedSkills []string
|
||||
}
|
||||
@ -93,6 +95,12 @@ func openClawRunningTaskResult(record *OpenClawTaskRecord) map[string]any {
|
||||
if record.PreparedArtifact != nil {
|
||||
applyOpenClawPreparedArtifactToResult(result, record.PreparedArtifact)
|
||||
}
|
||||
if len(record.ExpectedArtifactDirs) > 0 {
|
||||
result["expectedArtifactDirs"] = append([]string(nil), record.ExpectedArtifactDirs...)
|
||||
}
|
||||
if len(record.RequiredArtifactExts) > 0 {
|
||||
result["requiredArtifactExtensions"] = append([]string(nil), record.RequiredArtifactExts...)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
@ -398,6 +398,8 @@ func (o *SessionOrchestrator) startOpenClawGatewayTask(
|
||||
ProgressMessage: "OpenClaw task accepted",
|
||||
PreparedArtifact: preparedArtifact,
|
||||
RequiresArtifactExport: artifactContract.RequiresArtifactExport,
|
||||
ExpectedArtifactDirs: append([]string(nil), artifactContract.ExpectedArtifactDirs...),
|
||||
RequiredArtifactExts: append([]string(nil), artifactContract.RequiredArtifactExts...),
|
||||
ResolvedModel: routing.Model,
|
||||
ResolvedSkills: append([]string(nil), routing.Skills...),
|
||||
}
|
||||
@ -621,6 +623,9 @@ func openClawSessionPrepareParams(params map[string]any, openClawSessionKey stri
|
||||
if artifactContract.RequiresArtifactExport {
|
||||
result["requiresArtifactExport"] = true
|
||||
}
|
||||
if len(artifactContract.RequiredArtifactExts) > 0 {
|
||||
result["requiredArtifactExtensions"] = append([]string(nil), artifactContract.RequiredArtifactExts...)
|
||||
}
|
||||
if workspaceDir := openClawArtifactWorkspaceDir(params); workspaceDir != "" {
|
||||
result["workspaceDir"] = workspaceDir
|
||||
}
|
||||
@ -778,6 +783,7 @@ type openClawArtifactContract struct {
|
||||
ComplexLongChain bool
|
||||
RequiresArtifactExport bool
|
||||
ExpectedArtifactDirs []string
|
||||
RequiredArtifactExts []string
|
||||
SourceMessage string
|
||||
}
|
||||
|
||||
@ -793,11 +799,19 @@ func openClawArtifactContractForParams(params map[string]any, chatParams map[str
|
||||
expectedDirs := normalizeOpenClawDirList(shared.ListArg(contract, "expectedArtifactDirs"))
|
||||
requiresExport := parseBool(contract["requiresExportBeforeFinalResponse"]) || len(expectedDirs) > 0
|
||||
complex := taskLoadClass == "complex_long_chain_task" || isOpenClawLongArtifactTask(lowerMessage)
|
||||
requiredExts := normalizeOpenClawArtifactExtList(shared.ListArg(metadata, "requiredArtifactExtensions"))
|
||||
if len(requiredExts) == 0 {
|
||||
requiredExts = normalizeOpenClawArtifactExtList(shared.ListArg(metadata, "expectedArtifactExtensions"))
|
||||
}
|
||||
if len(requiredExts) == 0 {
|
||||
requiredExts = inferOpenClawRequiredArtifactExts(lowerMessage)
|
||||
}
|
||||
return openClawArtifactContract{
|
||||
TaskLoadClass: taskLoadClass,
|
||||
ComplexLongChain: complex,
|
||||
RequiresArtifactExport: requiresExport,
|
||||
ExpectedArtifactDirs: expectedDirs,
|
||||
RequiredArtifactExts: requiredExts,
|
||||
SourceMessage: message,
|
||||
}
|
||||
}
|
||||
@ -819,6 +833,39 @@ func normalizeOpenClawDirList(values []any) []string {
|
||||
return result
|
||||
}
|
||||
|
||||
func normalizeOpenClawArtifactExtList(values []any) []string {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
result := make([]string, 0, len(values))
|
||||
seen := map[string]bool{}
|
||||
for _, value := range values {
|
||||
ext := strings.ToLower(strings.TrimSpace(fmt.Sprint(value)))
|
||||
ext = strings.TrimPrefix(ext, ".")
|
||||
if ext == "" || strings.Contains(ext, "/") || strings.Contains(ext, "\\") || seen[ext] {
|
||||
continue
|
||||
}
|
||||
seen[ext] = true
|
||||
result = append(result, ext)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func inferOpenClawRequiredArtifactExts(lowerMessage string) []string {
|
||||
switch {
|
||||
case openClawMessageContainsAny(lowerMessage, []string{"pdf", "输出 pdf", "生成 pdf"}):
|
||||
return []string{"pdf"}
|
||||
case openClawMessageContainsAny(lowerMessage, []string{"视频", "video", "mp4", "渲染"}):
|
||||
return []string{"mp4"}
|
||||
case openClawMessageContainsAny(lowerMessage, []string{"图片", "图像", "png", "jpg", "jpeg", "webp", "生成图"}):
|
||||
return []string{"png", "jpg", "jpeg", "webp"}
|
||||
case openClawMessageContainsAny(lowerMessage, []string{"markdown", "md文件", ".md", "文案", "资讯"}):
|
||||
return []string{"md"}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func openClawChatSendParams(
|
||||
params map[string]any,
|
||||
turnID string,
|
||||
|
||||
@ -1925,6 +1925,56 @@ func TestExecuteSessionMessageGatewayDoesNotRewriteClaimedArtifactsWithoutGatewa
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteSessionMessageGatewayVerifiesClaimedArtifactsWhenExportRequired(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.message",
|
||||
Params: map[string]any{
|
||||
"sessionId": "session-openclaw-claimed-required-artifact",
|
||||
"threadId": "thread-openclaw-claimed-required-artifact",
|
||||
"taskPrompt": "fallback artifact hallucinate-files",
|
||||
"workingDirectory": t.TempDir(),
|
||||
"metadata": map[string]any{
|
||||
"xworkmateTaskArtifactContract": map[string]any{
|
||||
"requiresExportBeforeFinalResponse": true,
|
||||
"expectedArtifactDirs": []any{"exports/"},
|
||||
},
|
||||
},
|
||||
"routing": map[string]any{
|
||||
"routingMode": "explicit",
|
||||
"explicitExecutionTarget": "gateway",
|
||||
"preferredGatewayProviderId": "openclaw",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if rpcErr != nil {
|
||||
t.Fatalf("expected gateway response, got rpc error: %#v", rpcErr)
|
||||
}
|
||||
if got := response["status"]; got != string(TaskStateRunning) {
|
||||
t.Fatalf("expected required but unverified artifact claim to stay syncing, got %#v", response)
|
||||
}
|
||||
if artifacts := extractArtifactPayloads(response, shared.StringArg(response, "remoteWorkingDirectory", "")); len(artifacts) != 0 {
|
||||
t.Fatalf("expected bridge to remove unverified native artifact claims, got %#v", artifacts)
|
||||
}
|
||||
if got := shared.StringArg(shared.AsMap(response["progress"]), "stage", ""); got != "syncing-artifacts" {
|
||||
t.Fatalf("expected syncing-artifacts progress, got %#v", response)
|
||||
}
|
||||
if gateway.ArtifactExportCount() != 1 {
|
||||
t.Fatalf("expected Bridge artifact export verification, got %d", gateway.ArtifactExportCount())
|
||||
}
|
||||
if got := gateway.Methods(); !sameMethods(got, []string{"connect", "xworkmate.session.prepare", "chat.send", "xworkmate.tasks.get", "xworkmate.artifacts.export"}) {
|
||||
t.Fatalf("expected connect, prepare, chat.send, task lookup, then artifact export, got %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteSessionMessageGatewayExportsArtifactsWithoutPromptHeuristic(t *testing.T) {
|
||||
gateway := newAcpFakeOpenClawGateway(t)
|
||||
defer gateway.Close()
|
||||
|
||||
@ -169,6 +169,12 @@ func (s *Server) taskGetParamsWithSessionScope(params map[string]any) map[string
|
||||
if _, ok := next["requiresArtifactExport"]; !ok && sess.openClaw != nil && sess.openClaw.RequiresArtifactExport {
|
||||
next["requiresArtifactExport"] = true
|
||||
}
|
||||
if _, ok := next["expectedArtifactDirs"]; !ok && sess.openClaw != nil && len(sess.openClaw.ExpectedArtifactDirs) > 0 {
|
||||
next["expectedArtifactDirs"] = append([]string(nil), sess.openClaw.ExpectedArtifactDirs...)
|
||||
}
|
||||
if _, ok := next["requiredArtifactExtensions"]; !ok && sess.openClaw != nil && len(sess.openClaw.RequiredArtifactExts) > 0 {
|
||||
next["requiredArtifactExtensions"] = append([]string(nil), sess.openClaw.RequiredArtifactExts...)
|
||||
}
|
||||
if strings.TrimSpace(shared.StringArg(next, "openclawSessionKey", "")) == "" {
|
||||
next["openclawSessionKey"] = sess.task.SessionKey
|
||||
}
|
||||
@ -197,9 +203,6 @@ func (s *Server) mergeOpenClawTaskGetArtifactExport(payload map[string]any, para
|
||||
return
|
||||
}
|
||||
remoteWorkingDirectory := strings.TrimSpace(shared.StringArg(payload, "remoteWorkingDirectory", ""))
|
||||
if len(extractArtifactPayloads(payload, remoteWorkingDirectory)) > 0 {
|
||||
return
|
||||
}
|
||||
sessionKey := firstNonEmptyString(payload, "openclawSessionKey", "sessionKey")
|
||||
if sessionKey == "" {
|
||||
sessionKey = strings.TrimSpace(shared.StringArg(params, "openclawSessionKey", ""))
|
||||
@ -235,10 +238,15 @@ func (s *Server) mergeOpenClawTaskGetArtifactExport(payload map[string]any, para
|
||||
"maxInlineBytes": 0,
|
||||
"includeContent": false,
|
||||
}
|
||||
if expectedDirs := shared.ListArg(params, "expectedArtifactDirs"); len(expectedDirs) > 0 {
|
||||
if expectedDirs := openClawTaskGetExpectedArtifactDirs(params, payload); len(expectedDirs) > 0 {
|
||||
exportParams["expectedArtifactDirs"] = expectedDirs
|
||||
}
|
||||
mergeOpenClawArtifactPayload(payload, s.orchestrator.openClawArtifactExportRequest(gatewayProvider, exportParams, notify))
|
||||
exportPayload := s.orchestrator.openClawArtifactExportRequest(gatewayProvider, exportParams, notify)
|
||||
if openClawArtifactExportPayloadAuthoritative(exportPayload) {
|
||||
replaceOpenClawArtifactPayload(payload, exportPayload)
|
||||
} else {
|
||||
mergeOpenClawArtifactPayload(payload, exportPayload)
|
||||
}
|
||||
applyOpenClawPreparedArtifactToResult(payload, prepared)
|
||||
s.decorateOpenClawArtifactDownloadURLs(payload, sessionKey, runID)
|
||||
stripOpenClawArtifactInlineContent(payload)
|
||||
@ -260,7 +268,9 @@ func normalizeOpenClawTaskGetResult(params map[string]any, payload map[string]an
|
||||
return payload
|
||||
}
|
||||
remoteWorkingDirectory := strings.TrimSpace(shared.StringArg(payload, "remoteWorkingDirectory", ""))
|
||||
if len(extractArtifactPayloads(payload, remoteWorkingDirectory)) > 0 {
|
||||
artifacts := extractArtifactPayloads(payload, remoteWorkingDirectory)
|
||||
requiredExts := openClawTaskGetRequiredArtifactExtensions(params, payload)
|
||||
if len(artifacts) > 0 && openClawArtifactsSatisfyRequiredExtensions(artifacts, requiredExts) {
|
||||
return payload
|
||||
}
|
||||
status := strings.ToLower(strings.TrimSpace(shared.StringArg(payload, "status", "")))
|
||||
@ -296,6 +306,9 @@ func normalizeOpenClawTaskGetResult(params map[string]any, payload map[string]an
|
||||
}
|
||||
payload["artifactScope"] = artifactScope
|
||||
payload["artifactDirectory"] = artifactDirectory
|
||||
if len(requiredExts) > 0 {
|
||||
payload["requiredArtifactExtensions"] = append([]string(nil), requiredExts...)
|
||||
}
|
||||
if strings.TrimSpace(shared.StringArg(payload, "resolvedGatewayProviderId", "")) == "" {
|
||||
payload["resolvedGatewayProviderId"] = gatewayProvider
|
||||
}
|
||||
@ -315,7 +328,93 @@ func openClawTaskGetRequiresArtifactExport(params map[string]any, payload map[st
|
||||
return true
|
||||
}
|
||||
return len(shared.ListArg(params, "expectedArtifactDirs")) > 0 ||
|
||||
len(shared.ListArg(payload, "expectedArtifactDirs")) > 0
|
||||
len(shared.ListArg(payload, "expectedArtifactDirs")) > 0 ||
|
||||
len(shared.ListArg(params, "requiredArtifactExtensions")) > 0 ||
|
||||
len(shared.ListArg(payload, "requiredArtifactExtensions")) > 0
|
||||
}
|
||||
|
||||
func openClawTaskGetExpectedArtifactDirs(params map[string]any, payload map[string]any) []any {
|
||||
seen := map[string]bool{}
|
||||
result := []any{}
|
||||
for _, values := range [][]any{
|
||||
shared.ListArg(params, "expectedArtifactDirs"),
|
||||
shared.ListArg(payload, "expectedArtifactDirs"),
|
||||
} {
|
||||
for _, value := range values {
|
||||
item := strings.TrimSpace(fmt.Sprint(value))
|
||||
if item == "" || seen[item] {
|
||||
continue
|
||||
}
|
||||
seen[item] = true
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func openClawArtifactExportPayloadAuthoritative(payload map[string]any) bool {
|
||||
if len(payload) == 0 {
|
||||
return false
|
||||
}
|
||||
if strings.TrimSpace(shared.StringArg(payload, "remoteWorkingDirectory", "")) != "" {
|
||||
return true
|
||||
}
|
||||
if strings.TrimSpace(shared.StringArg(payload, "artifactScope", "")) != "" {
|
||||
return true
|
||||
}
|
||||
_, hasArtifacts := payload["artifacts"]
|
||||
_, hasFiles := payload["files"]
|
||||
_, hasAttachments := payload["attachments"]
|
||||
return hasArtifacts || hasFiles || hasAttachments
|
||||
}
|
||||
|
||||
func replaceOpenClawArtifactPayload(result map[string]any, source map[string]any) {
|
||||
if result == nil {
|
||||
return
|
||||
}
|
||||
for _, key := range []string{"artifacts", "files", "attachments"} {
|
||||
delete(result, key)
|
||||
}
|
||||
mergeOpenClawArtifactPayload(result, source)
|
||||
}
|
||||
|
||||
func openClawTaskGetRequiredArtifactExtensions(params map[string]any, payload map[string]any) []string {
|
||||
return normalizeOpenClawArtifactExtList(openClawTaskGetMergedList(params, payload, "requiredArtifactExtensions"))
|
||||
}
|
||||
|
||||
func openClawTaskGetMergedList(params map[string]any, payload map[string]any, key string) []any {
|
||||
seen := map[string]bool{}
|
||||
result := []any{}
|
||||
for _, values := range [][]any{
|
||||
shared.ListArg(params, key),
|
||||
shared.ListArg(payload, key),
|
||||
} {
|
||||
for _, value := range values {
|
||||
item := strings.TrimSpace(fmt.Sprint(value))
|
||||
if item == "" || seen[item] {
|
||||
continue
|
||||
}
|
||||
seen[item] = true
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func openClawArtifactsSatisfyRequiredExtensions(artifacts []map[string]any, requiredExts []string) bool {
|
||||
if len(requiredExts) == 0 {
|
||||
return true
|
||||
}
|
||||
for _, artifact := range artifacts {
|
||||
relativePath := strings.ToLower(strings.TrimSpace(shared.StringArg(artifact, "relativePath", "")))
|
||||
for _, ext := range requiredExts {
|
||||
normalized := strings.TrimPrefix(strings.ToLower(strings.TrimSpace(ext)), ".")
|
||||
if normalized != "" && strings.HasSuffix(relativePath, "."+normalized) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Server) handleTaskCancel(ctx context.Context, params map[string]any, notify func(map[string]any)) map[string]any {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user