Simplify OpenClaw artifact handoff
This commit is contained in:
parent
14d99646d9
commit
836b6bac44
@ -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)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user