diff --git a/internal/acp/orchestrator.go b/internal/acp/orchestrator.go index 3b72049..3d2601a 100644 --- a/internal/acp/orchestrator.go +++ b/internal/acp/orchestrator.go @@ -7,7 +7,6 @@ import ( "log" "net/url" "os" - "path" "path/filepath" "strings" "time" @@ -240,25 +239,10 @@ func (o *SessionOrchestrator) runOpenClawGatewayChat( notify(update) } } - artifactDeliveryRequired := openClawArtifactDeliveryRequired(params) sessionKey := openClawSessionKey(params, turnID) artifactRunID := turnID - logOpenClawArtifactIntent(gatewayProvider, sessionKey, artifactRunID, "intent", artifactDeliveryRequired, false, false, false) - var preparedArtifact *openClawPreparedArtifactScope - if artifactDeliveryRequired { - var rpcErr *shared.RPCError - preparedArtifact, rpcErr = o.openClawArtifactPrepare( - gatewayProvider, - sessionKey, - artifactRunID, - notifyWithCollection, - ) - if rpcErr != nil { - return nil, rpcErr - } - } - logOpenClawArtifactIntent(gatewayProvider, sessionKey, artifactRunID, "prepare", artifactDeliveryRequired, preparedArtifact != nil, false, false) - chatParams, rpcErr := openClawChatSendParams(params, turnID, preparedArtifact) + logOpenClawArtifactSync(gatewayProvider, sessionKey, artifactRunID, "intent", false, false, false) + chatParams, rpcErr := openClawChatSendParams(params, turnID) if rpcErr != nil { return nil, rpcErr } @@ -327,29 +311,20 @@ func (o *SessionOrchestrator) runOpenClawGatewayChat( } mergeOpenClawArtifactPayload(result, waitPayload) mergeOpenClawArtifactPayload(result, collector.artifactPayload()) - if preparedArtifact == nil { - preparedArtifact = openClawPreparedArtifactScopeFromPayload(result) - } - artifactDeliveryClaimed := !artifactDeliveryRequired && openClawArtifactDeliveryClaimedByOutput(output) - artifactPayload := o.openClawArtifactExportForDelivery( + preparedArtifact := openClawPreparedArtifactScopeFromPayload(result) + artifactPayload := o.openClawArtifactExport( gatewayProvider, chatParams, artifactRunID, artifactSinceUnixMs, preparedArtifact, - artifactDeliveryRequired || preparedArtifact != nil, notifyWithCollection, ) - if artifactDeliveryClaimed && preparedArtifact != nil { - artifactPayload = filterOpenClawArtifactPayloadByOutput(output, artifactPayload) - } mergeOpenClawArtifactPayload(result, artifactPayload) exportedCount := openClawArtifactPayloadCount(result) - artifactExpected := artifactDeliveryRequired || artifactDeliveryClaimed || preparedArtifact != nil - logOpenClawArtifactIntent(gatewayProvider, sessionKey, artifactRunID, "export", artifactDeliveryRequired, preparedArtifact != nil, exportedCount > 0, artifactExpected && exportedCount == 0) + logOpenClawArtifactSync(gatewayProvider, sessionKey, artifactRunID, "export", preparedArtifact != nil, exportedCount > 0, exportedCount == 0) o.server.decorateOpenClawArtifactDownloadURLs(result, shared.StringArg(chatParams, "sessionKey", ""), artifactRunID) stripOpenClawArtifactInlineContent(result) - guardOpenClawArtifactResult(result, artifactDeliveryRequired || artifactDeliveryClaimed) guardOpenClawNoDisplayableResult(result, noDisplayableOutput) if notify != nil { notify(shared.NotificationEnvelope("session.update", openClawGatewayCompletedResultUpdate(sessionID, threadID, turnID, result))) @@ -416,23 +391,21 @@ func logOpenClawGatewayTiming( ) } -func logOpenClawArtifactIntent( +func logOpenClawArtifactSync( gatewayProvider string, sessionKey string, runID string, stage string, - required bool, prepared bool, exported bool, empty bool, ) { log.Printf( - "level=info component=openclaw_gateway event=artifact_intent provider=%q sessionId=%q runId=%q stage=%q required=%t prepared=%t exported=%t empty=%t", + "level=info component=openclaw_gateway event=artifact_sync provider=%q sessionId=%q runId=%q stage=%q prepared=%t exported=%t empty=%t", gatewayProvider, sessionKey, runID, stage, - required, prepared, exported, empty, @@ -447,28 +420,6 @@ func openClawArtifactPayloadCount(payload map[string]any) int { return len(extractArtifactPayloads(payload, remoteWorkingDirectory)) } -func (o *SessionOrchestrator) openClawArtifactExportForDelivery( - gatewayProvider string, - chatParams map[string]any, - runID string, - sinceUnixMs int64, - preparedArtifact *openClawPreparedArtifactScope, - artifactDeliveryRequired bool, - notify func(map[string]any), -) map[string]any { - if !artifactDeliveryRequired { - return nil - } - return o.openClawArtifactExport( - gatewayProvider, - chatParams, - runID, - sinceUnixMs, - preparedArtifact, - notify, - ) -} - func isSessionTaskMethod(method string) bool { switch strings.TrimSpace(method) { case "session.start", "session.message": @@ -505,15 +456,11 @@ func openClawPreparedArtifactScopeFromPayload(payload map[string]any) *openClawP func openClawChatSendParams( params map[string]any, turnID string, - preparedArtifact *openClawPreparedArtifactScope, ) (map[string]any, *shared.RPCError) { message := openClawCurrentTurnMessage(params) if message == "" { return nil, &shared.RPCError{Code: -32602, Message: "OPENCLAW_TASK_PROMPT_REQUIRED"} } - if openClawArtifactDeliveryRequired(params) { - message = withOpenClawArtifactDeliveryInstructions(message, preparedArtifact) - } sessionKey := openClawSessionKey(params, turnID) chatParams := map[string]any{ "sessionKey": sessionKey, @@ -529,102 +476,6 @@ func openClawChatSendParams( return chatParams, nil } -func openClawArtifactDeliveryRequired(params map[string]any) bool { - text := strings.ToLower(strings.Join(openClawArtifactDeliveryText(params), "\n")) - if strings.TrimSpace(text) == "" { - return false - } - if openClawArtifactDeliverySuppressed(text) { - return false - } - fileSignals := []string{ - "ppt", "pptx", "powerpoint", "slide", "slides", - "pdf", "docx", "word", "xlsx", "excel", - "artifact", "artifacts", "file", "files", "download", "attachment", "asset", "output", - "image", "photo", "picture", "screenshot", "video", "audio", "csv", "json", "html", - "zip", "tar", "archive", "dataset", "report", "document", "markdown", "code", - "文件", "制品", "产物", "下载", "附件", "素材", "输出", "图片", "截图", "图像", - "视频", "音频", "压缩包", "数据集", "文档", "报告", "演示", "幻灯片", "表格", "代码", - } - actionSignals := []string{ - "create", "generate", "build", "make", "write", "export", "output", "deliver", "download", - "save", "produce", "render", "attach", "return", - "生成", "制作", "输出", "导出", "下载", "交付", "收取", "保存", "渲染", "返回", "提供", - } - hasFileSignal := false - for _, signal := range fileSignals { - if strings.Contains(text, signal) { - hasFileSignal = true - break - } - } - if !hasFileSignal { - return false - } - for _, signal := range actionSignals { - if strings.Contains(text, signal) { - return true - } - } - return false -} - -func openClawArtifactDeliverySuppressed(text string) bool { - suppressedSignals := []string{ - "do not create file", "do not create files", - "don't create file", "don't create files", - "do not generate file", "do not generate files", - "don't generate file", "don't generate files", - "do not write file", "do not write files", - "don't write file", "don't write files", - "no file", "no files", "no artifact", "no artifacts", - "without file", "without files", "without artifact", "without artifacts", - "不要创建文件", "不要生成文件", "不要写入文件", "不创建文件", "不生成文件", - "无需创建文件", "无需生成文件", "不需要文件", "不要产物", "无需产物", - } - for _, signal := range suppressedSignals { - if strings.Contains(text, signal) { - return true - } - } - return false -} - -func openClawArtifactDeliveryClaimedByOutput(output string) bool { - if strings.TrimSpace(output) == openClawNoDisplayableText { - return false - } - return openClawArtifactDeliveryRequired(map[string]any{"message": output}) -} - -func openClawArtifactDeliveryText(raw any) []string { - switch value := raw.(type) { - case string: - if text := strings.TrimSpace(value); text != "" { - return []string{text} - } - case map[string]any: - texts := make([]string, 0, len(value)) - for _, key := range []string{"taskPrompt", "prompt", "message", "text", "content", "input"} { - texts = append(texts, openClawTextFragments(value[key])...) - } - texts = append(texts, openClawLatestUserMessageText(value["messages"])...) - for _, key := range []string{"request", "params", "payload", "body"} { - if item, ok := value[key]; ok { - texts = append(texts, openClawArtifactDeliveryText(item)...) - } - } - return compactOpenClawTexts(texts) - case []any: - texts := make([]string, 0, len(value)) - for _, item := range value { - texts = append(texts, openClawArtifactDeliveryText(item)...) - } - return compactOpenClawTexts(texts) - } - return nil -} - func openClawCurrentTurnMessage(params map[string]any) string { if params == nil { return "" @@ -720,33 +571,6 @@ func compactOpenClawTexts(texts []string) []string { return result } -func withOpenClawArtifactDeliveryInstructions( - message string, - preparedArtifact *openClawPreparedArtifactScope, -) string { - message = strings.TrimSpace(message) - if message == "" { - return message - } - lines := []string{ - "XWorkmate artifact delivery requirements:", - "- Create the requested files as real files before finishing.", - } - if preparedArtifact != nil && strings.TrimSpace(preparedArtifact.ArtifactDirectory) != "" { - lines = append(lines, - "- Write every deliverable file into this exact directory:", - fmt.Sprintf(" `%s`", strings.TrimSpace(preparedArtifact.ArtifactDirectory)), - "- Do not write deliverable files outside that directory.", - ) - } - lines = append(lines, - "- If multiple formats are requested, write each requested format as a separate file with the correct extension.", - "- Do not claim that files are ready, downloadable, or clickable unless the files actually exist on disk.", - "- In the final response, list only the real file names you created. Do not invent download links.", - ) - return message + "\n\n" + strings.Join(lines, "\n") -} - func openClawSessionKey(params map[string]any, turnID string) string { for _, key := range []string{"threadId", "sessionId"} { if value := strings.TrimSpace(shared.StringArg(params, key, "")); value != "" { @@ -759,42 +583,6 @@ func openClawSessionKey(params map[string]any, turnID string) string { return "main" } -func (o *SessionOrchestrator) openClawArtifactPrepare( - gatewayProvider string, - sessionKey string, - runID string, - notify func(map[string]any), -) (*openClawPreparedArtifactScope, *shared.RPCError) { - sessionKey = strings.TrimSpace(sessionKey) - runID = strings.TrimSpace(runID) - if sessionKey == "" || runID == "" { - return nil, &shared.RPCError{Code: -32602, Message: "OPENCLAW_ARTIFACT_SCOPE_REQUIRED"} - } - prepareResult := o.openClawGatewayRequestWithRetry( - gatewayProvider, - "xworkmate.artifacts.prepare", - map[string]any{ - "sessionKey": sessionKey, - "runId": runID, - }, - 30*time.Second, - notify, - ) - if !prepareResult.OK { - return nil, gatewayRPCError(prepareResult.Error, "openclaw artifact prepare failed") - } - payload := shared.AsMap(prepareResult.Payload) - prepared := &openClawPreparedArtifactScope{ - ArtifactScope: strings.TrimSpace(shared.StringArg(payload, "artifactScope", "")), - ArtifactDirectory: strings.TrimSpace(shared.StringArg(payload, "artifactDirectory", "")), - ScopeKind: strings.TrimSpace(shared.StringArg(payload, "scopeKind", "")), - } - if prepared.ArtifactScope == "" || prepared.ArtifactDirectory == "" { - return nil, &shared.RPCError{Code: -32002, Message: "openclaw artifact prepare returned invalid scope"} - } - return prepared, nil -} - func (o *SessionOrchestrator) openClawArtifactExport( gatewayProvider string, chatParams map[string]any, @@ -837,28 +625,6 @@ func (o *SessionOrchestrator) openClawArtifactExport( } } -func guardOpenClawArtifactResult(result map[string]any, artifactDeliveryRequired bool) { - if !artifactDeliveryRequired || result == nil { - return - } - remoteWorkingDirectory := strings.TrimSpace(shared.StringArg(result, "remoteWorkingDirectory", "")) - if len(extractArtifactPayloads(result, remoteWorkingDirectory)) > 0 { - return - } - message := "未检测到 OpenClaw 本轮导出的实际文件。已阻止口头下载声明进入 artifacts 面板;请重新执行并要求 OpenClaw 在 workspace 中真实生成文件。" - result["success"] = false - result["status"] = "artifact_missing" - result["code"] = "OPENCLAW_ARTIFACT_MISSING" - result["error"] = "OpenClaw artifact export returned no files for this run." - result["output"] = message - result["message"] = message - result["summary"] = message - result["artifactWarnings"] = appendArtifactList( - result["artifactWarnings"], - []any{"OpenClaw artifact export returned no files for a file-delivery request."}, - ) -} - func guardOpenClawNoDisplayableResult(result map[string]any, noDisplayableOutput bool) { if !noDisplayableOutput || result == nil || !parseBool(result["success"]) { return @@ -876,58 +642,6 @@ func guardOpenClawNoDisplayableResult(result map[string]any, noDisplayableOutput result["summary"] = openClawNoDisplayableText } -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 @@ -1347,13 +1061,12 @@ func (o *SessionOrchestrator) completeOpenClawScopedArtifactExport( sessionKey := openClawSessionKey(params, turnID) runID := strings.TrimSpace(shared.StringArg(result, "runId", turnID)) chatParams := map[string]any{"sessionKey": sessionKey} - mergeOpenClawArtifactPayload(result, o.openClawArtifactExportForDelivery( + mergeOpenClawArtifactPayload(result, o.openClawArtifactExport( gatewayProvider, chatParams, runID, 0, preparedArtifact, - true, nil, )) o.server.decorateOpenClawArtifactDownloadURLs(result, sessionKey, runID) diff --git a/internal/acp/routing_test.go b/internal/acp/routing_test.go index 503aa9b..0555bb2 100644 --- a/internal/acp/routing_test.go +++ b/internal/acp/routing_test.go @@ -509,11 +509,11 @@ func TestExecuteSessionTaskGatewayAutoConnectsLocalOpenClaw(t *testing.T) { if got := int64(timeoutMs); got <= 120000 { t.Fatalf("expected OpenClaw agent.wait timeout to exceed the previous 120s cap, got %#v", waitParams) } - if gateway.ArtifactExportCount() != 0 { - t.Fatalf("expected no OpenClaw artifact export for text-only prompt, got %d", gateway.ArtifactExportCount()) + if gateway.ArtifactExportCount() != 1 { + t.Fatalf("expected one OpenClaw artifact export sync after run, got %d", gateway.ArtifactExportCount()) } - if got := gateway.Methods(); !sameMethods(got, []string{"connect", "chat.send", "agent.wait"}) { - t.Fatalf("expected connect, chat.send, then agent.wait, got %#v", got) + 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) } client := gateway.LastConnectClient() if got := client["id"]; got != "openclaw-macos" { @@ -563,8 +563,8 @@ func TestExecuteSessionTaskGatewayNoDisplayableOutputFails(t *testing.T) { if got := response["output"]; got != openClawNoDisplayableText { t.Fatalf("expected no-displayable output message, got %#v", response) } - if gateway.ArtifactExportCount() != 0 { - t.Fatalf("expected no artifact export for no-output text prompt, got %d", gateway.ArtifactExportCount()) + if gateway.ArtifactExportCount() != 1 { + t.Fatalf("expected one artifact export sync even when no displayable text is returned, got %d", gateway.ArtifactExportCount()) } } @@ -604,11 +604,11 @@ func TestExecuteSessionMessageGatewayUsesOpenClawChatSend(t *testing.T) { if gateway.AgentWaitCount() != 1 { t.Fatalf("expected one OpenClaw agent.wait request, got %d", gateway.AgentWaitCount()) } - if gateway.ArtifactExportCount() != 0 { - t.Fatalf("expected no OpenClaw artifact export for text-only prompt, got %d", gateway.ArtifactExportCount()) + if gateway.ArtifactExportCount() != 1 { + t.Fatalf("expected one OpenClaw artifact export sync after message run, got %d", gateway.ArtifactExportCount()) } - if got := gateway.Methods(); !sameMethods(got, []string{"connect", "chat.send", "agent.wait"}) { - t.Fatalf("expected connect, chat.send, then agent.wait, got %#v", got) + 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) } } @@ -834,9 +834,8 @@ func TestExecuteSessionTaskGatewayExportsOpenClawArtifacts(t *testing.T) { if got := parsedDownloadURL.Query().Get("relativePath"); got != "reports/final.md" { t.Fatalf("expected artifact relativePath in downloadUrl, got %q", got) } - artifactScope := parsedDownloadURL.Query().Get("artifactScope") - if !strings.HasPrefix(artifactScope, "tasks/") { - t.Fatalf("expected artifact scope in downloadUrl, got %q", artifactScope) + if artifactScope := parsedDownloadURL.Query().Get("artifactScope"); artifactScope != "" { + t.Fatalf("expected no bridge-imposed artifact scope in downloadUrl, got %q", artifactScope) } if parsedDownloadURL.Query().Get("sig") == "" { t.Fatalf("expected signed downloadUrl, got %q", downloadURL) @@ -848,12 +847,12 @@ func TestExecuteSessionTaskGatewayExportsOpenClawArtifacts(t *testing.T) { if got := shared.BoolArg(shared.StringArg(exportParams, "includeContent", ""), true); got { t.Fatalf("expected OpenClaw artifact export to omit content, got %#v", exportParams) } - if got := gateway.Methods(); !sameMethods(got, []string{"connect", "xworkmate.artifacts.prepare", "chat.send", "agent.wait", "xworkmate.artifacts.export"}) { - t.Fatalf("expected connect, artifact prepare, chat.send, agent.wait, then artifact export, got %#v", got) + 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 TestExecuteSessionTaskGatewayDoesNotExportStaleWorkspaceArtifactsWhenScopedDirectoryEmpty(t *testing.T) { +func TestExecuteSessionTaskGatewayDoesNotTreatPromptTextAsArtifactContract(t *testing.T) { gateway := newAcpFakeOpenClawGateway(t) defer gateway.Close() @@ -878,23 +877,17 @@ func TestExecuteSessionTaskGatewayDoesNotExportStaleWorkspaceArtifactsWhenScoped }, }) if rpcErr != nil { - t.Fatalf("expected artifact guard response, got rpc error: %#v", rpcErr) + t.Fatalf("expected gateway response, got rpc error: %#v", rpcErr) } - if got := response["success"]; got != false { - t.Fatalf("expected artifact_missing failure, got %#v", response) - } - if got := response["status"]; got != "artifact_missing" { - t.Fatalf("expected artifact_missing status, got %#v", response) - } - if got := response["code"]; got != "OPENCLAW_ARTIFACT_MISSING" { - t.Fatalf("expected structured artifact missing code, got %#v", response) + if got := response["success"]; got != true { + t.Fatalf("expected prompt text not to be converted into bridge artifact failure, got %#v", response) } if _, ok := response["artifacts"]; ok { - t.Fatalf("expected no stale artifacts when scoped directory is empty, got %#v", response["artifacts"]) + t.Fatalf("expected no stale artifacts when gateway exported none, got %#v", response["artifacts"]) } exportParams := gateway.LastArtifactExportParams() - if got := strings.TrimSpace(shared.StringArg(exportParams, "artifactScope", "")); !strings.HasPrefix(got, "tasks/thread-openclaw-latest-artifact/") { - t.Fatalf("expected scoped artifact export params, got %#v", exportParams) + if got := strings.TrimSpace(shared.StringArg(exportParams, "artifactScope", "")); got != "" { + t.Fatalf("expected bridge not to impose scoped artifact export params, got %#v", exportParams) } if _, ok := exportParams["latestIfEmpty"]; ok { t.Fatalf("expected no latestIfEmpty fallback export param, got %#v", exportParams) @@ -905,12 +898,12 @@ func TestExecuteSessionTaskGatewayDoesNotExportStaleWorkspaceArtifactsWhenScoped if got := shared.BoolArg(shared.StringArg(exportParams, "includeContent", ""), true); got { t.Fatalf("expected latest workspace export to omit content, got %#v", exportParams) } - if got := gateway.Methods(); !sameMethods(got, []string{"connect", "xworkmate.artifacts.prepare", "chat.send", "agent.wait", "xworkmate.artifacts.export"}) { - t.Fatalf("expected connect, artifact prepare, chat.send, agent.wait, then artifact export, got %#v", got) + 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 TestExecuteSessionMessageGatewayRejectsClaimedArtifactsWithoutScopedFiles(t *testing.T) { +func TestExecuteSessionMessageGatewayDoesNotRewriteClaimedArtifactsWithoutGatewayFiles(t *testing.T) { gateway := newAcpFakeOpenClawGateway(t) defer gateway.Close() @@ -935,26 +928,23 @@ func TestExecuteSessionMessageGatewayRejectsClaimedArtifactsWithoutScopedFiles(t }, }) if rpcErr != nil { - t.Fatalf("expected claimed artifact guard response, got rpc error: %#v", rpcErr) + t.Fatalf("expected gateway response, got rpc error: %#v", rpcErr) } - if got := response["success"]; got != false { - t.Fatalf("expected claimed artifact_missing failure, got %#v", response) + if got := response["success"]; got != true { + t.Fatalf("expected bridge to preserve gateway terminal state, got %#v", response) } - if got := response["status"]; got != "artifact_missing" { - t.Fatalf("expected artifact_missing status, got %#v", response) + if output := strings.TrimSpace(shared.StringArg(response, "output", "")); !strings.Contains(output, "文件已就绪") { + t.Fatalf("expected bridge not to rewrite gateway text output, got %q", output) } - if got := response["code"]; got != "OPENCLAW_ARTIFACT_MISSING" { - t.Fatalf("expected structured artifact missing code, got %#v", response) + if gateway.ArtifactExportCount() != 1 { + t.Fatalf("expected one post-run artifact export sync, got %d", gateway.ArtifactExportCount()) } - if gateway.ArtifactExportCount() != 0 { - t.Fatalf("expected no artifact export for unprepared claimed output, got %d", gateway.ArtifactExportCount()) - } - if got := gateway.Methods(); !sameMethods(got, []string{"connect", "chat.send", "agent.wait"}) { - t.Fatalf("expected connect, chat.send, then agent.wait, got %#v", got) + 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 TestExecuteSessionMessageGatewayPreparesArtifactsFromMessagesPrompt(t *testing.T) { +func TestExecuteSessionMessageGatewayExportsArtifactsWithoutPromptHeuristic(t *testing.T) { gateway := newAcpFakeOpenClawGateway(t) defer gateway.Close() @@ -996,36 +986,11 @@ func TestExecuteSessionMessageGatewayPreparesArtifactsFromMessagesPrompt(t *test t.Fatalf("expected artifact response success, got %#v", response) } exportParams := gateway.LastArtifactExportParams() - if got := strings.TrimSpace(shared.StringArg(exportParams, "artifactScope", "")); !strings.HasPrefix(got, "tasks/thread-openclaw-message-artifact/") { - t.Fatalf("expected scoped artifact export params for message prompt, got %#v", exportParams) + if got := strings.TrimSpace(shared.StringArg(exportParams, "artifactScope", "")); got != "" { + t.Fatalf("expected bridge not to impose artifact scope from message prompt, got %#v", exportParams) } - if got := gateway.Methods(); !sameMethods(got, []string{"connect", "xworkmate.artifacts.prepare", "chat.send", "agent.wait", "xworkmate.artifacts.export"}) { - t.Fatalf("expected connect, artifact prepare, 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) + 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) } } @@ -1407,7 +1372,7 @@ func TestHTTPHandlerOpenClawArtifactDownloadRejectsInvalidArtifactScope(t *testi } } -func TestOpenClawChatSendParamsAddsArtifactDeliveryInstructions(t *testing.T) { +func TestOpenClawChatSendParamsPreservesRawPrompt(t *testing.T) { for _, prompt := range []string{ "输出 PPT PDF docx 文件", "生成一张图片并返回制品", @@ -1418,61 +1383,18 @@ func TestOpenClawChatSendParamsAddsArtifactDeliveryInstructions(t *testing.T) { chatParams, rpcErr := openClawChatSendParams(map[string]any{ "threadId": "thread-artifact-instructions", "taskPrompt": prompt, - }, "turn-artifact-instructions", &openClawPreparedArtifactScope{ - ArtifactScope: "tasks/thread-artifact-instructions/turn-artifact-instructions", - ArtifactDirectory: "/remote/openclaw/workspace/tasks/thread-artifact-instructions/turn-artifact-instructions", - ScopeKind: "task", - }) + }, "turn-artifact-instructions") if rpcErr != nil { t.Fatalf("expected chat params, got rpc error: %#v", rpcErr) } message := strings.TrimSpace(shared.StringArg(chatParams, "message", "")) - if !strings.Contains(message, prompt) { - t.Fatalf("expected original prompt to be preserved, got %q", message) - } - if !strings.Contains(message, "Create the requested files as real files") { - t.Fatalf("expected artifact delivery instructions, got %q", message) - } - if !strings.Contains(message, "/remote/openclaw/workspace/tasks/thread-artifact-instructions/turn-artifact-instructions") { - t.Fatalf("expected scoped artifact directory instruction, got %q", message) - } - if !strings.Contains(message, "Do not claim that files are ready") { - t.Fatalf("expected anti-hallucination download instruction, got %q", message) + if message != prompt { + t.Fatalf("expected bridge to preserve raw prompt without artifact instructions, got %q", message) } }) } } -func TestOpenClawArtifactDeliveryRequiredScansNestedParams(t *testing.T) { - params := map[string]any{ - "request": map[string]any{ - "params": map[string]any{ - "taskPrompt": "请在当前任务制品目录中真实生成一个文件", - }, - }, - } - if !openClawArtifactDeliveryRequired(params) { - t.Fatal("expected nested artifact delivery prompt to be detected") - } -} - -func TestOpenClawArtifactDeliveryRequiredScansMessageContentParts(t *testing.T) { - params := map[string]any{ - "messages": []any{ - map[string]any{"role": "assistant", "content": "上一轮只是分析。"}, - map[string]any{ - "role": "user", - "content": []any{ - map[string]any{"type": "text", "text": "请输出 Markdown 文件并保存到 workspace。"}, - }, - }, - }, - } - if !openClawArtifactDeliveryRequired(params) { - t.Fatal("expected artifact delivery prompt in message content parts to be detected") - } -} - func TestExecuteSessionTaskGatewayCollectsOpenClawEventArtifacts(t *testing.T) { gateway := newAcpFakeOpenClawGateway(t) gateway.artifactMode = "unknown" @@ -1523,9 +1445,8 @@ func TestExecuteSessionTaskGatewayCollectsOpenClawEventArtifacts(t *testing.T) { } } -func TestExecuteSessionTaskGatewaySkipsArtifactExportForTextOnlyPrompt(t *testing.T) { +func TestExecuteSessionTaskGatewayAlwaysSyncsGatewayArtifactsAfterRun(t *testing.T) { gateway := newAcpFakeOpenClawGateway(t) - gateway.artifactMode = "unknown" defer gateway.Close() t.Setenv("GATEWAY_RPC_URL", gateway.URL()) @@ -1554,34 +1475,15 @@ func TestExecuteSessionTaskGatewaySkipsArtifactExportForTextOnlyPrompt(t *testin if got := response["output"]; got != "gateway pong" { t.Fatalf("expected gateway pong output, got %#v", response) } - if gateway.ArtifactExportCount() != 0 { - t.Fatalf("expected no OpenClaw artifact export request, got %d", gateway.ArtifactExportCount()) + if gateway.ArtifactExportCount() != 1 { + t.Fatalf("expected one OpenClaw artifact export sync, got %d", gateway.ArtifactExportCount()) } if warnings := shared.ListArg(response, "artifactWarnings"); len(warnings) != 0 { - t.Fatalf("expected no artifact warnings for text-only prompt, got %#v", warnings) + t.Fatalf("expected no artifact warnings when gateway export succeeds empty, got %#v", warnings) } } -func TestOpenClawNoDisplayableOutputDoesNotClaimArtifacts(t *testing.T) { - if openClawArtifactDeliveryClaimedByOutput(openClawNoDisplayableText) { - t.Fatalf("fallback no-output text must not trigger artifact delivery guard") - } -} - -func TestOpenClawArtifactDeliverySuppressionKeepsTextOnlyPrompt(t *testing.T) { - if openClawArtifactDeliveryRequired(map[string]any{ - "taskPrompt": "Reply exactly pong. Do not create files.", - }) { - t.Fatalf("negative file directive must not trigger artifact delivery") - } - if openClawArtifactDeliveryRequired(map[string]any{ - "taskPrompt": "只回答 pong,不要生成文件。", - }) { - t.Fatalf("Chinese negative file directive must not trigger artifact delivery") - } -} - -func TestExecuteSessionTaskGatewayRejectsMissingOpenClawFilesForDeliveryRequest(t *testing.T) { +func TestExecuteSessionTaskGatewayDoesNotFailMissingFilesFromPromptHeuristic(t *testing.T) { gateway := newAcpFakeOpenClawGateway(t) defer gateway.Close() @@ -1608,28 +1510,18 @@ func TestExecuteSessionTaskGatewayRejectsMissingOpenClawFilesForDeliveryRequest( if rpcErr != nil { t.Fatalf("expected bridge response, got rpc error: %#v", rpcErr) } - if success, _ := response["success"].(bool); success { - t.Fatalf("expected missing artifact delivery to be marked unsuccessful, got %#v", response) - } - if got := response["status"]; got != "artifact_missing" { - t.Fatalf("expected artifact_missing status, got %#v", response) - } - if got := response["code"]; got != "OPENCLAW_ARTIFACT_MISSING" { - t.Fatalf("expected structured artifact missing code, got %#v", response) + if success, _ := response["success"].(bool); !success { + t.Fatalf("expected bridge to preserve gateway success without prompt heuristic failure, got %#v", response) } output := strings.TrimSpace(shared.StringArg(response, "output", "")) - if strings.Contains(output, "点击直接下载") || strings.Contains(output, "文件已就绪") { - t.Fatalf("expected hallucinated download text to be replaced, got %q", output) - } - if !strings.Contains(output, "未检测到 OpenClaw 本轮导出的实际文件") { - t.Fatalf("expected explicit missing artifact message, got %q", output) + if !strings.Contains(output, "点击直接下载") || !strings.Contains(output, "文件已就绪") { + t.Fatalf("expected bridge to preserve gateway output, got %q", output) } if _, ok := response["artifacts"]; ok { t.Fatalf("expected no artifacts when export returned none, got %#v", response["artifacts"]) } - warnings := response["artifactWarnings"].([]any) - if len(warnings) != 1 || !strings.Contains(fmt.Sprint(warnings[0]), "returned no files") { - t.Fatalf("expected missing artifact warning, got %#v", response["artifactWarnings"]) + if warnings := shared.ListArg(response, "artifactWarnings"); len(warnings) != 0 { + t.Fatalf("expected no bridge artifact-missing warning, got %#v", warnings) } } diff --git a/internal/acp/web_contract_test.go b/internal/acp/web_contract_test.go index 127fd8d..7ddfd5a 100644 --- a/internal/acp/web_contract_test.go +++ b/internal/acp/web_contract_test.go @@ -561,7 +561,7 @@ func TestHTTPHandlerGatewayOpenClawFiltersRawGatewayEventsAndKeepsFinalResult(t if !sawFinal { t.Fatalf("expected final result envelope, got %q", bodyText) } - if got := gateway.Methods(); !sameMethods(got, []string{"connect", "xworkmate.artifacts.prepare", "chat.send", "agent.wait", "xworkmate.artifacts.export"}) { + if got := gateway.Methods(); !sameMethods(got, []string{"connect", "chat.send", "agent.wait", "xworkmate.artifacts.export"}) { t.Fatalf("expected artifact workflow methods to stay unchanged, got %#v", got) } }