fix(openclaw): remove synthetic artifact completion
This commit is contained in:
parent
9b2276e895
commit
d2d32f554d
@ -1,282 +0,0 @@
|
|||||||
package acp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"image"
|
|
||||||
"image/color"
|
|
||||||
"image/draw"
|
|
||||||
"image/png"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func openClawShouldSynthesizeMissingArtifacts(contract openClawArtifactContract, missing []string) bool {
|
|
||||||
if len(missing) == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
message := strings.ToLower(strings.TrimSpace(contract.SourceMessage))
|
|
||||||
if contract.ComplexLongChain {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if openClawMessageContainsAny(message, []string{
|
|
||||||
"it-infra-continuous-png",
|
|
||||||
"it-infra-evolution-video",
|
|
||||||
"ai-tech-news-video",
|
|
||||||
"product-intro-video",
|
|
||||||
"wan-image-video",
|
|
||||||
"image-cog",
|
|
||||||
}) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if hasOpenClawRequiredExtension(missing, "mp4") &&
|
|
||||||
openClawMessageContainsAny(message, []string{"video", "mp4", "视频", "渲染"}) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if hasAnyOpenClawRequiredExtension(missing, []string{"png", "jpg", "jpeg", "webp"}) &&
|
|
||||||
openClawMessageContainsAny(message, []string{"image", "images", "图片", "生成图", "配图", "插图", "多图片"}) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if hasOpenClawRequiredExtension(missing, "md") &&
|
|
||||||
openClawMessageContainsAny(message, []string{"文案", "小红书", "微信文章", "头条号", "copywriting", "资讯", "新闻", "报告", "news"}) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func hasAnyOpenClawRequiredExtension(values []string, extensions []string) bool {
|
|
||||||
for _, extension := range extensions {
|
|
||||||
if hasOpenClawRequiredExtension(values, extension) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func hasOpenClawRequiredExtension(values []string, extension string) bool {
|
|
||||||
extension = normalizeOpenClawArtifactExtension(extension)
|
|
||||||
for _, value := range values {
|
|
||||||
if normalizeOpenClawArtifactExtension(value) == extension {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func openClawSynthesizedArtifactOutput(contract openClawArtifactContract) string {
|
|
||||||
extensions := append([]string(nil), contract.RequiredFinalExtensions...)
|
|
||||||
sort.Strings(extensions)
|
|
||||||
if len(extensions) == 0 {
|
|
||||||
return "OpenClaw final artifacts were written to the current task artifact scope."
|
|
||||||
}
|
|
||||||
return "OpenClaw final artifacts were written to the current task artifact scope: " + strings.Join(extensions, ", ") + "."
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeOpenClawRequiredFinalArtifacts(
|
|
||||||
prepared *openClawPreparedArtifactScope,
|
|
||||||
contract openClawArtifactContract,
|
|
||||||
missing []string,
|
|
||||||
) ([]string, error) {
|
|
||||||
artifactDirectory, err := writableOpenClawArtifactDirectory(prepared)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := os.MkdirAll(artifactDirectory, 0o755); err != nil {
|
|
||||||
return nil, fmt.Errorf("openclaw artifact recovery failed to create artifact directory: %w", err)
|
|
||||||
}
|
|
||||||
written := make([]string, 0, len(missing))
|
|
||||||
for _, extension := range missing {
|
|
||||||
normalized := normalizeOpenClawArtifactExtension(extension)
|
|
||||||
if normalized == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
relativePath := openClawRecoveredArtifactRelativePath(normalized)
|
|
||||||
absolutePath := filepath.Join(artifactDirectory, filepath.FromSlash(relativePath))
|
|
||||||
if err := os.MkdirAll(filepath.Dir(absolutePath), 0o755); err != nil {
|
|
||||||
return written, fmt.Errorf("openclaw artifact recovery failed to create %s: %w", relativePath, err)
|
|
||||||
}
|
|
||||||
if err := writeOpenClawRecoveredArtifact(absolutePath, normalized, contract); err != nil {
|
|
||||||
return written, fmt.Errorf("openclaw artifact recovery failed to write %s: %w", relativePath, err)
|
|
||||||
}
|
|
||||||
written = append(written, relativePath)
|
|
||||||
}
|
|
||||||
return written, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func writableOpenClawArtifactDirectory(prepared *openClawPreparedArtifactScope) (string, error) {
|
|
||||||
if prepared == nil {
|
|
||||||
return "", fmt.Errorf("openclaw artifact recovery skipped: missing prepared artifact scope")
|
|
||||||
}
|
|
||||||
artifactDirectory := filepath.Clean(strings.TrimSpace(prepared.ArtifactDirectory))
|
|
||||||
remoteWorkingDirectory := filepath.Clean(strings.TrimSpace(prepared.RemoteWorkingDirectory))
|
|
||||||
if artifactDirectory == "." || artifactDirectory == "" {
|
|
||||||
return "", fmt.Errorf("openclaw artifact recovery skipped: empty artifact directory")
|
|
||||||
}
|
|
||||||
if remoteWorkingDirectory == "." || remoteWorkingDirectory == "" {
|
|
||||||
return "", fmt.Errorf("openclaw artifact recovery skipped: empty remote workspace")
|
|
||||||
}
|
|
||||||
relative, err := filepath.Rel(remoteWorkingDirectory, artifactDirectory)
|
|
||||||
if err != nil || relative == "." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) || relative == ".." {
|
|
||||||
return "", fmt.Errorf("openclaw artifact recovery skipped: artifact directory is outside remote workspace")
|
|
||||||
}
|
|
||||||
return artifactDirectory, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func openClawRecoveredArtifactRelativePath(extension string) string {
|
|
||||||
switch extension {
|
|
||||||
case "md":
|
|
||||||
return "reports/final.md"
|
|
||||||
case "txt":
|
|
||||||
return "reports/final.txt"
|
|
||||||
case "html":
|
|
||||||
return "reports/final.html"
|
|
||||||
case "json":
|
|
||||||
return "reports/final.json"
|
|
||||||
case "csv":
|
|
||||||
return "reports/final.csv"
|
|
||||||
case "pdf":
|
|
||||||
return "exports/final.pdf"
|
|
||||||
case "png", "jpg", "jpeg", "webp":
|
|
||||||
return "assets/images/final." + extension
|
|
||||||
case "mp4", "mov", "webm":
|
|
||||||
return "renders/final." + extension
|
|
||||||
default:
|
|
||||||
return "exports/final." + extension
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeOpenClawRecoveredArtifact(path string, extension string, contract openClawArtifactContract) error {
|
|
||||||
switch extension {
|
|
||||||
case "md":
|
|
||||||
return os.WriteFile(path, []byte(openClawRecoveredMarkdown(contract)), 0o644)
|
|
||||||
case "txt":
|
|
||||||
return os.WriteFile(path, []byte(openClawRecoveredPlainText(contract)), 0o644)
|
|
||||||
case "html":
|
|
||||||
return os.WriteFile(path, []byte("<!doctype html><meta charset=\"utf-8\"><title>XWorkmate Artifact</title><pre>"+htmlEscape(openClawRecoveredPlainText(contract))+"</pre>\n"), 0o644)
|
|
||||||
case "json":
|
|
||||||
return os.WriteFile(path, []byte("{\n \"status\": \"artifact_recovered\",\n \"source\": \"xworkmate-bridge\"\n}\n"), 0o644)
|
|
||||||
case "csv":
|
|
||||||
return os.WriteFile(path, []byte("status,source\nartifact_recovered,xworkmate-bridge\n"), 0o644)
|
|
||||||
case "pdf":
|
|
||||||
return os.WriteFile(path, openClawRecoveredPDFBytes(contract), 0o644)
|
|
||||||
case "png", "jpg", "jpeg", "webp":
|
|
||||||
return writeOpenClawRecoveredPNG(path)
|
|
||||||
case "mp4", "mov", "webm":
|
|
||||||
return writeOpenClawRecoveredVideo(path)
|
|
||||||
default:
|
|
||||||
return os.WriteFile(path, []byte(openClawRecoveredPlainText(contract)), 0o644)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func openClawRecoveredMarkdown(contract openClawArtifactContract) string {
|
|
||||||
return "# XWorkmate Task Artifact\n\n" +
|
|
||||||
"The remote task artifact scope was finalized by the XWorkmate gateway because the OpenClaw run did not export every required final deliverable.\n\n" +
|
|
||||||
"## Required Extensions\n\n" +
|
|
||||||
"- " + strings.Join(contract.RequiredFinalExtensions, "\n- ") + "\n\n" +
|
|
||||||
"## Task Prompt\n\n" +
|
|
||||||
"```text\n" + truncateOpenClawArtifactText(contract.SourceMessage, 2000) + "\n```\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
func openClawRecoveredPlainText(contract openClawArtifactContract) string {
|
|
||||||
return "XWorkmate task artifact\n\n" +
|
|
||||||
"Required extensions: " + strings.Join(contract.RequiredFinalExtensions, ", ") + "\n\n" +
|
|
||||||
truncateOpenClawArtifactText(contract.SourceMessage, 2000) + "\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
func openClawRecoveredPDFBytes(contract openClawArtifactContract) []byte {
|
|
||||||
text := strings.NewReplacer("\\", "\\\\", "(", "\\(", ")", "\\)", "\r", " ", "\n", " ").Replace(
|
|
||||||
truncateOpenClawArtifactText(openClawRecoveredPlainText(contract), 700),
|
|
||||||
)
|
|
||||||
stream := "BT /F1 14 Tf 72 760 Td (XWorkmate Task Artifact) Tj 0 -28 Td /F1 10 Tf (" + text + ") Tj ET"
|
|
||||||
objects := []string{
|
|
||||||
"<< /Type /Catalog /Pages 2 0 R >>",
|
|
||||||
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
|
|
||||||
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>",
|
|
||||||
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>",
|
|
||||||
fmt.Sprintf("<< /Length %d >>\nstream\n%s\nendstream", len(stream), stream),
|
|
||||||
}
|
|
||||||
var buf bytes.Buffer
|
|
||||||
buf.WriteString("%PDF-1.4\n")
|
|
||||||
offsets := make([]int, 0, len(objects)+1)
|
|
||||||
offsets = append(offsets, 0)
|
|
||||||
for index, object := range objects {
|
|
||||||
offsets = append(offsets, buf.Len())
|
|
||||||
fmt.Fprintf(&buf, "%d 0 obj\n%s\nendobj\n", index+1, object)
|
|
||||||
}
|
|
||||||
xrefOffset := buf.Len()
|
|
||||||
fmt.Fprintf(&buf, "xref\n0 %d\n", len(objects)+1)
|
|
||||||
buf.WriteString("0000000000 65535 f \n")
|
|
||||||
for _, offset := range offsets[1:] {
|
|
||||||
fmt.Fprintf(&buf, "%010d 00000 n \n", offset)
|
|
||||||
}
|
|
||||||
fmt.Fprintf(&buf, "trailer\n<< /Size %d /Root 1 0 R >>\nstartxref\n%d\n%%%%EOF\n", len(objects)+1, xrefOffset)
|
|
||||||
return buf.Bytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeOpenClawRecoveredPNG(path string) error {
|
|
||||||
const width = 1280
|
|
||||||
const height = 720
|
|
||||||
img := image.NewRGBA(image.Rect(0, 0, width, height))
|
|
||||||
draw.Draw(img, img.Bounds(), &image.Uniform{C: color.RGBA{R: 250, G: 252, B: 255, A: 255}}, image.Point{}, draw.Src)
|
|
||||||
bands := []struct {
|
|
||||||
rect image.Rectangle
|
|
||||||
c color.RGBA
|
|
||||||
}{
|
|
||||||
{image.Rect(0, 0, width, 92), color.RGBA{R: 18, G: 92, B: 182, A: 255}},
|
|
||||||
{image.Rect(72, 180, width-72, 260), color.RGBA{R: 90, G: 196, B: 144, A: 255}},
|
|
||||||
{image.Rect(72, 310, width-220, 390), color.RGBA{R: 245, G: 180, B: 55, A: 255}},
|
|
||||||
{image.Rect(72, 440, width-360, 520), color.RGBA{R: 222, G: 86, B: 94, A: 255}},
|
|
||||||
}
|
|
||||||
for _, band := range bands {
|
|
||||||
draw.Draw(img, band.rect, &image.Uniform{C: band.c}, image.Point{}, draw.Src)
|
|
||||||
}
|
|
||||||
file, err := os.Create(path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer func() { _ = file.Close() }()
|
|
||||||
return png.Encode(file, img)
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeOpenClawRecoveredVideo(path string) error {
|
|
||||||
if ffmpegPath, err := exec.LookPath("ffmpeg"); err == nil {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
cmd := exec.CommandContext(
|
|
||||||
ctx,
|
|
||||||
ffmpegPath,
|
|
||||||
"-y",
|
|
||||||
"-f", "lavfi",
|
|
||||||
"-i", "color=c=0x125cb6:s=1280x720:d=1",
|
|
||||||
"-f", "lavfi",
|
|
||||||
"-i", "anullsrc=channel_layout=stereo:sample_rate=44100",
|
|
||||||
"-shortest",
|
|
||||||
"-pix_fmt", "yuv420p",
|
|
||||||
"-movflags", "+faststart",
|
|
||||||
path,
|
|
||||||
)
|
|
||||||
if err := cmd.Run(); err == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return os.WriteFile(path, []byte("XWorkmate task video artifact placeholder\n"), 0o644)
|
|
||||||
}
|
|
||||||
|
|
||||||
func truncateOpenClawArtifactText(value string, limit int) string {
|
|
||||||
value = strings.TrimSpace(value)
|
|
||||||
if limit <= 0 || len([]rune(value)) <= limit {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
runes := []rune(value)
|
|
||||||
return string(runes[:limit]) + "\n..."
|
|
||||||
}
|
|
||||||
|
|
||||||
func htmlEscape(value string) string {
|
|
||||||
replacer := strings.NewReplacer("&", "&", "<", "<", ">", ">", "\"", """, "'", "'")
|
|
||||||
return replacer.Replace(value)
|
|
||||||
}
|
|
||||||
@ -31,7 +31,6 @@ const (
|
|||||||
const (
|
const (
|
||||||
openClawAgentWaitDefaultTimeout = 6 * time.Minute
|
openClawAgentWaitDefaultTimeout = 6 * time.Minute
|
||||||
openClawAgentWaitMaxTimeout = time.Hour
|
openClawAgentWaitMaxTimeout = time.Hour
|
||||||
openClawRecoverableArtifactWaitLimit = 75 * time.Second
|
|
||||||
openClawAgentWaitHTTPMargin = time.Minute
|
openClawAgentWaitHTTPMargin = time.Minute
|
||||||
openClawNoDisplayableText = "OpenClaw completed without displayable output."
|
openClawNoDisplayableText = "OpenClaw completed without displayable output."
|
||||||
openClawRequiredArtifactMissingText = "OpenClaw completed without required final artifacts."
|
openClawRequiredArtifactMissingText = "OpenClaw completed without required final artifacts."
|
||||||
@ -376,10 +375,6 @@ func (o *SessionOrchestrator) runOpenClawGatewayChat(
|
|||||||
applyOpenClawPreparedArtifactToChatParams(chatParams, preparedArtifact, sessionKey, runID, artifactContract)
|
applyOpenClawPreparedArtifactToChatParams(chatParams, preparedArtifact, sessionKey, runID, artifactContract)
|
||||||
}
|
}
|
||||||
waitTimeout := openClawAgentWaitTimeout(params, chatParams)
|
waitTimeout := openClawAgentWaitTimeout(params, chatParams)
|
||||||
if openClawShouldSynthesizeMissingArtifacts(artifactContract, artifactContract.RequiredFinalExtensions) &&
|
|
||||||
waitTimeout > openClawRecoverableArtifactWaitLimit {
|
|
||||||
waitTimeout = openClawRecoverableArtifactWaitLimit
|
|
||||||
}
|
|
||||||
waitStarted := time.Now()
|
waitStarted := time.Now()
|
||||||
waitResult := o.openClawGatewayRequestWithRetry(
|
waitResult := o.openClawGatewayRequestWithRetry(
|
||||||
gatewayProvider,
|
gatewayProvider,
|
||||||
@ -400,42 +395,6 @@ func (o *SessionOrchestrator) runOpenClawGatewayChat(
|
|||||||
waitResult.OK,
|
waitResult.OK,
|
||||||
)
|
)
|
||||||
if !waitResult.OK {
|
if !waitResult.OK {
|
||||||
if openClawShouldSynthesizeMissingArtifacts(artifactContract, artifactContract.RequiredFinalExtensions) {
|
|
||||||
output := openClawSynthesizedArtifactOutput(artifactContract)
|
|
||||||
result := map[string]any{
|
|
||||||
"success": true,
|
|
||||||
"output": output,
|
|
||||||
"message": output,
|
|
||||||
"summary": output,
|
|
||||||
"turnId": turnID,
|
|
||||||
"runId": runID,
|
|
||||||
"mode": router.ExecutionTargetGatewayChat,
|
|
||||||
"resolvedGatewayProviderId": gatewayProvider,
|
|
||||||
"artifactWarnings": []any{strings.TrimSpace(shared.StringArg(waitResult.Error, "message", "openclaw agent.wait failed"))},
|
|
||||||
}
|
|
||||||
applyOpenClawPreparedArtifactToResult(result, preparedArtifact)
|
|
||||||
synthesizedPayload := o.openClawSynthesizeMissingArtifacts(
|
|
||||||
gatewayProvider,
|
|
||||||
chatParams,
|
|
||||||
runID,
|
|
||||||
artifactSinceUnixMs,
|
|
||||||
preparedArtifact,
|
|
||||||
artifactContract,
|
|
||||||
artifactContract.RequiredFinalExtensions,
|
|
||||||
notifyWithCollection,
|
|
||||||
)
|
|
||||||
mergeOpenClawArtifactPayload(result, synthesizedPayload)
|
|
||||||
result[openClawArtifactExportAttemptedField] = true
|
|
||||||
recoveredCount := openClawArtifactPayloadCount(result)
|
|
||||||
logOpenClawArtifactSync(gatewayProvider, sessionKey, runID, "recover", preparedArtifact != nil, recoveredCount > 0, recoveredCount == 0)
|
|
||||||
o.server.decorateOpenClawArtifactDownloadURLs(result, shared.StringArg(chatParams, "sessionKey", ""), runID)
|
|
||||||
stripOpenClawArtifactInlineContent(result)
|
|
||||||
applyOpenClawArtifactContractResult(result, artifactContract)
|
|
||||||
if notify != nil {
|
|
||||||
notify(shared.NotificationEnvelope("session.update", openClawGatewayCompletedResultUpdate(sessionID, threadID, turnID, result)))
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
return nil, gatewayRPCError(waitResult.Error, "openclaw agent.wait failed")
|
return nil, gatewayRPCError(waitResult.Error, "openclaw agent.wait failed")
|
||||||
}
|
}
|
||||||
waitPayload := shared.AsMap(waitResult.Payload)
|
waitPayload := shared.AsMap(waitResult.Payload)
|
||||||
@ -472,55 +431,6 @@ func (o *SessionOrchestrator) runOpenClawGatewayChat(
|
|||||||
result[openClawArtifactExportAttemptedField] = true
|
result[openClawArtifactExportAttemptedField] = true
|
||||||
exportedCount := openClawArtifactPayloadCount(result)
|
exportedCount := openClawArtifactPayloadCount(result)
|
||||||
logOpenClawArtifactSync(gatewayProvider, sessionKey, runID, "export", preparedArtifact != nil, exportedCount > 0, exportedCount == 0)
|
logOpenClawArtifactSync(gatewayProvider, sessionKey, runID, "export", preparedArtifact != nil, exportedCount > 0, exportedCount == 0)
|
||||||
if missing := missingOpenClawRequiredFinalExtensions(result, artifactContract); len(missing) > 0 {
|
|
||||||
if openClawShouldSynthesizeMissingArtifacts(artifactContract, missing) {
|
|
||||||
synthesizedPayload := o.openClawSynthesizeMissingArtifacts(
|
|
||||||
gatewayProvider,
|
|
||||||
chatParams,
|
|
||||||
runID,
|
|
||||||
artifactSinceUnixMs,
|
|
||||||
preparedArtifact,
|
|
||||||
artifactContract,
|
|
||||||
missing,
|
|
||||||
notifyWithCollection,
|
|
||||||
)
|
|
||||||
mergeOpenClawArtifactPayload(result, synthesizedPayload)
|
|
||||||
if openClawArtifactPayloadCount(synthesizedPayload) > 0 &&
|
|
||||||
len(missingOpenClawRequiredFinalExtensionsForRepair(result, artifactContract)) == 0 {
|
|
||||||
recoveredOutput := openClawSynthesizedArtifactOutput(artifactContract)
|
|
||||||
result["success"] = true
|
|
||||||
result["output"] = recoveredOutput
|
|
||||||
result["message"] = recoveredOutput
|
|
||||||
result["summary"] = recoveredOutput
|
|
||||||
delete(result, "status")
|
|
||||||
delete(result, "code")
|
|
||||||
delete(result, "error")
|
|
||||||
delete(result, "missingArtifactExtensions")
|
|
||||||
}
|
|
||||||
recoveredCount := openClawArtifactPayloadCount(result)
|
|
||||||
logOpenClawArtifactSync(gatewayProvider, sessionKey, runID, "recover", preparedArtifact != nil, recoveredCount > 0, recoveredCount == 0)
|
|
||||||
} else {
|
|
||||||
repairPayload := o.openClawFinalizeMissingArtifacts(
|
|
||||||
gatewayProvider,
|
|
||||||
chatParams,
|
|
||||||
sessionKey,
|
|
||||||
runID,
|
|
||||||
artifactSinceUnixMs,
|
|
||||||
preparedArtifact,
|
|
||||||
artifactContract,
|
|
||||||
missing,
|
|
||||||
notifyWithCollection,
|
|
||||||
)
|
|
||||||
mergeOpenClawArtifactPayload(result, repairPayload)
|
|
||||||
if repairedOutput := collector.output(); repairedOutput != "" {
|
|
||||||
result["output"] = repairedOutput
|
|
||||||
result["message"] = repairedOutput
|
|
||||||
result["summary"] = repairedOutput
|
|
||||||
}
|
|
||||||
repairedCount := openClawArtifactPayloadCount(result)
|
|
||||||
logOpenClawArtifactSync(gatewayProvider, sessionKey, runID, "finalize", preparedArtifact != nil, repairedCount > 0, repairedCount == 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
o.server.decorateOpenClawArtifactDownloadURLs(result, shared.StringArg(chatParams, "sessionKey", ""), runID)
|
o.server.decorateOpenClawArtifactDownloadURLs(result, shared.StringArg(chatParams, "sessionKey", ""), runID)
|
||||||
stripOpenClawArtifactInlineContent(result)
|
stripOpenClawArtifactInlineContent(result)
|
||||||
applyOpenClawArtifactContractResult(result, artifactContract)
|
applyOpenClawArtifactContractResult(result, artifactContract)
|
||||||
@ -1431,150 +1341,6 @@ func (o *SessionOrchestrator) openClawArtifactExport(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *SessionOrchestrator) openClawFinalizeMissingArtifacts(
|
|
||||||
gatewayProvider string,
|
|
||||||
chatParams map[string]any,
|
|
||||||
sessionKey string,
|
|
||||||
runID string,
|
|
||||||
sinceUnixMs int64,
|
|
||||||
preparedArtifact *openClawPreparedArtifactScope,
|
|
||||||
contract openClawArtifactContract,
|
|
||||||
missing []string,
|
|
||||||
notify func(map[string]any),
|
|
||||||
) map[string]any {
|
|
||||||
if len(missing) == 0 || preparedArtifact == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
artifactDirectory := strings.TrimSpace(preparedArtifact.ArtifactDirectory)
|
|
||||||
if artifactDirectory == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
finalizeRunID := strings.TrimSpace(runID) + "-finalize"
|
|
||||||
if finalizeRunID == "-finalize" {
|
|
||||||
finalizeRunID = fmt.Sprintf("finalize-%d", time.Now().UnixNano())
|
|
||||||
}
|
|
||||||
finalizeParams := map[string]any{
|
|
||||||
"sessionKey": strings.TrimSpace(sessionKey),
|
|
||||||
"idempotencyKey": finalizeRunID,
|
|
||||||
"message": strings.Join([]string{
|
|
||||||
"XWorkmate final deliverable repair:",
|
|
||||||
"The previous run produced partial artifacts but missed required final deliverables.",
|
|
||||||
"Missing required artifact extensions: " + strings.Join(missing, ", ") + ".",
|
|
||||||
"Continue the same task. Do not restart from scratch unless necessary.",
|
|
||||||
"Use the existing artifactDirectory and write the missing final deliverables directly there.",
|
|
||||||
"artifactDirectory: " + artifactDirectory,
|
|
||||||
"After writing the files, reply with the relative paths of the final deliverables.",
|
|
||||||
}, "\n"),
|
|
||||||
}
|
|
||||||
if thinking := strings.TrimSpace(shared.StringArg(chatParams, "thinking", "")); thinking != "" {
|
|
||||||
finalizeParams["thinking"] = thinking
|
|
||||||
}
|
|
||||||
applyOpenClawPreparedArtifactToChatParams(finalizeParams, preparedArtifact, sessionKey, runID, contract)
|
|
||||||
sendStarted := time.Now()
|
|
||||||
sendResult := o.openClawGatewayRequestWithRetry(
|
|
||||||
gatewayProvider,
|
|
||||||
"chat.send",
|
|
||||||
finalizeParams,
|
|
||||||
2*time.Minute,
|
|
||||||
notify,
|
|
||||||
)
|
|
||||||
logOpenClawGatewayTiming(
|
|
||||||
gatewayProvider,
|
|
||||||
"chat.send.finalize",
|
|
||||||
sessionKey,
|
|
||||||
finalizeRunID,
|
|
||||||
time.Since(sendStarted),
|
|
||||||
sendResult.OK,
|
|
||||||
)
|
|
||||||
if !sendResult.OK {
|
|
||||||
return openClawFinalizeWarningPayload(sendResult.Error, "openclaw final deliverable repair failed")
|
|
||||||
}
|
|
||||||
sendPayload := shared.AsMap(sendResult.Payload)
|
|
||||||
waitRunID := strings.TrimSpace(shared.StringArg(sendPayload, "runId", finalizeRunID))
|
|
||||||
waitStarted := time.Now()
|
|
||||||
waitResult := o.openClawGatewayRequestWithRetry(
|
|
||||||
gatewayProvider,
|
|
||||||
"agent.wait",
|
|
||||||
map[string]any{
|
|
||||||
"runId": waitRunID,
|
|
||||||
"timeoutMs": openClawAgentWaitDefaultTimeout.Milliseconds(),
|
|
||||||
},
|
|
||||||
openClawAgentWaitDefaultTimeout,
|
|
||||||
notify,
|
|
||||||
)
|
|
||||||
logOpenClawGatewayTiming(
|
|
||||||
gatewayProvider,
|
|
||||||
"agent.wait.finalize",
|
|
||||||
sessionKey,
|
|
||||||
waitRunID,
|
|
||||||
time.Since(waitStarted),
|
|
||||||
waitResult.OK,
|
|
||||||
)
|
|
||||||
if !waitResult.OK {
|
|
||||||
return openClawFinalizeWarningPayload(waitResult.Error, "openclaw final deliverable repair wait failed")
|
|
||||||
}
|
|
||||||
exportPayload := o.openClawArtifactExport(
|
|
||||||
gatewayProvider,
|
|
||||||
chatParams,
|
|
||||||
runID,
|
|
||||||
sinceUnixMs,
|
|
||||||
preparedArtifact,
|
|
||||||
notify,
|
|
||||||
)
|
|
||||||
if len(exportPayload) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
exportPayload["finalizeRunId"] = waitRunID
|
|
||||||
return exportPayload
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *SessionOrchestrator) openClawSynthesizeMissingArtifacts(
|
|
||||||
gatewayProvider string,
|
|
||||||
chatParams map[string]any,
|
|
||||||
runID string,
|
|
||||||
sinceUnixMs int64,
|
|
||||||
preparedArtifact *openClawPreparedArtifactScope,
|
|
||||||
contract openClawArtifactContract,
|
|
||||||
missing []string,
|
|
||||||
notify func(map[string]any),
|
|
||||||
) map[string]any {
|
|
||||||
if !openClawShouldSynthesizeMissingArtifacts(contract, missing) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
written, err := writeOpenClawRequiredFinalArtifacts(preparedArtifact, contract, missing)
|
|
||||||
if err != nil {
|
|
||||||
return map[string]any{
|
|
||||||
"artifactWarnings": []any{err.Error()},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(written) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
exportPayload := o.openClawArtifactExport(
|
|
||||||
gatewayProvider,
|
|
||||||
chatParams,
|
|
||||||
runID,
|
|
||||||
sinceUnixMs,
|
|
||||||
preparedArtifact,
|
|
||||||
notify,
|
|
||||||
)
|
|
||||||
if len(exportPayload) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
exportPayload["recoveredArtifactPaths"] = append([]string(nil), written...)
|
|
||||||
return exportPayload
|
|
||||||
}
|
|
||||||
|
|
||||||
func openClawFinalizeWarningPayload(errorPayload map[string]any, fallback string) map[string]any {
|
|
||||||
message := strings.TrimSpace(shared.StringArg(errorPayload, "message", ""))
|
|
||||||
if message == "" {
|
|
||||||
message = fallback
|
|
||||||
}
|
|
||||||
return map[string]any{
|
|
||||||
"artifactWarnings": []any{message},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func guardOpenClawNoDisplayableResult(result map[string]any, noDisplayableOutput bool) {
|
func guardOpenClawNoDisplayableResult(result map[string]any, noDisplayableOutput bool) {
|
||||||
if !noDisplayableOutput || result == nil || !parseBool(result["success"]) {
|
if !noDisplayableOutput || result == nil || !parseBool(result["success"]) {
|
||||||
return
|
return
|
||||||
|
|||||||
@ -636,42 +636,6 @@ func TestOpenClawArtifactContractInfersRemoteScenarioDeliverables(t *testing.T)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOpenClawArtifactFinalizerWritesCurrentScopeDeliverables(t *testing.T) {
|
|
||||||
workspace := t.TempDir()
|
|
||||||
artifactDirectory := filepath.Join(workspace, "tasks", "thread-main", "turn-1")
|
|
||||||
prepared := &openClawPreparedArtifactScope{
|
|
||||||
ArtifactScope: "tasks/thread-main/turn-1",
|
|
||||||
ArtifactDirectory: artifactDirectory,
|
|
||||||
RemoteWorkingDirectory: workspace,
|
|
||||||
}
|
|
||||||
contract := openClawArtifactContract{
|
|
||||||
RequiredFinalExtensions: []string{"md", "pdf", "png", "mp4"},
|
|
||||||
SourceMessage: "测试制作文案、PDF、图片和视频",
|
|
||||||
}
|
|
||||||
|
|
||||||
written, err := writeOpenClawRequiredFinalArtifacts(prepared, contract, contract.RequiredFinalExtensions)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("write final artifacts: %v", err)
|
|
||||||
}
|
|
||||||
if !slices.Equal(written, []string{
|
|
||||||
"reports/final.md",
|
|
||||||
"exports/final.pdf",
|
|
||||||
"assets/images/final.png",
|
|
||||||
"renders/final.mp4",
|
|
||||||
}) {
|
|
||||||
t.Fatalf("unexpected written paths: %#v", written)
|
|
||||||
}
|
|
||||||
for _, relativePath := range written {
|
|
||||||
info, err := os.Stat(filepath.Join(artifactDirectory, filepath.FromSlash(relativePath)))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("expected %s to exist: %v", relativePath, err)
|
|
||||||
}
|
|
||||||
if info.Size() == 0 {
|
|
||||||
t.Fatalf("expected %s to be non-empty", relativePath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGatewayRequestForwardsOpenClawSkillsStatus(t *testing.T) {
|
func TestGatewayRequestForwardsOpenClawSkillsStatus(t *testing.T) {
|
||||||
gateway := newAcpFakeOpenClawGateway(t)
|
gateway := newAcpFakeOpenClawGateway(t)
|
||||||
defer gateway.Close()
|
defer gateway.Close()
|
||||||
@ -825,7 +789,7 @@ func TestExecuteSessionTaskGatewayComplexArtifactContractAcceptsRequiredFinalArt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExecuteSessionTaskGatewayComplexArtifactContractRecoversPartialArtifacts(t *testing.T) {
|
func TestExecuteSessionTaskGatewayComplexArtifactContractRejectsPartialArtifacts(t *testing.T) {
|
||||||
gateway := newAcpFakeOpenClawGateway(t)
|
gateway := newAcpFakeOpenClawGateway(t)
|
||||||
defer gateway.Close()
|
defer gateway.Close()
|
||||||
gateway.artifactWorkspaceRoot = t.TempDir()
|
gateway.artifactWorkspaceRoot = t.TempDir()
|
||||||
@ -855,31 +819,24 @@ func TestExecuteSessionTaskGatewayComplexArtifactContractRecoversPartialArtifact
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
if rpcErr != nil {
|
if rpcErr != nil {
|
||||||
t.Fatalf("expected finalized partial-artifact response, got rpc error: %#v", rpcErr)
|
t.Fatalf("expected bridge response, got rpc error: %#v", rpcErr)
|
||||||
}
|
}
|
||||||
if got := response["success"]; got != true {
|
if got := response["success"]; got != false {
|
||||||
t.Fatalf("expected partial artifact response to be finalized, got %#v", response)
|
t.Fatalf("expected partial artifact response to fail without final PDF, got %#v", response)
|
||||||
}
|
}
|
||||||
if got := gateway.ChatSendCount(); got != 1 {
|
if got := gateway.ChatSendCount(); got != 1 {
|
||||||
t.Fatalf("expected Bridge to recover partial artifacts without another model turn, got %d", got)
|
t.Fatalf("expected no automatic repair model turn, got %d", got)
|
||||||
}
|
}
|
||||||
artifacts := responseArtifactMaps(t, response)
|
artifacts := responseArtifactMaps(t, response)
|
||||||
if len(artifacts) != 3 {
|
if len(artifacts) != 1 || artifacts[0]["relativePath"] != "chapters/intro.md" {
|
||||||
t.Fatalf("expected initial partial artifact plus finalized export artifacts, got %#v", artifacts)
|
t.Fatalf("expected only real partial artifact, got %#v", artifacts)
|
||||||
}
|
}
|
||||||
seen := map[string]bool{}
|
if got := response["code"]; got != "OPENCLAW_REQUIRED_ARTIFACT_MISSING" {
|
||||||
for _, artifact := range artifacts {
|
t.Fatalf("expected missing final artifact code, got %#v", response)
|
||||||
seen[fmt.Sprint(artifact["relativePath"])] = true
|
|
||||||
}
|
|
||||||
if !seen["chapters/intro.md"] || !seen["exports/final.pdf"] {
|
|
||||||
t.Fatalf("expected partial markdown and final PDF artifacts, got %#v", artifacts)
|
|
||||||
}
|
|
||||||
if _, ok := response["missingArtifactExtensions"]; ok {
|
|
||||||
t.Fatalf("expected finalize turn to clear missing artifact diagnostics, got %#v", response)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExecuteSessionTaskGatewayRecoversArtifactContractAfterWaitFailure(t *testing.T) {
|
func TestExecuteSessionTaskGatewayFailsArtifactContractAfterWaitFailure(t *testing.T) {
|
||||||
gateway := newAcpFakeOpenClawGateway(t)
|
gateway := newAcpFakeOpenClawGateway(t)
|
||||||
defer gateway.Close()
|
defer gateway.Close()
|
||||||
gateway.artifactWorkspaceRoot = t.TempDir()
|
gateway.artifactWorkspaceRoot = t.TempDir()
|
||||||
@ -908,21 +865,17 @@ func TestExecuteSessionTaskGatewayRecoversArtifactContractAfterWaitFailure(t *te
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if rpcErr != nil {
|
if rpcErr == nil {
|
||||||
t.Fatalf("expected recovered wait-timeout response, got rpc error: %#v", rpcErr)
|
t.Fatalf("expected wait-timeout rpc error, got response: %#v", response)
|
||||||
}
|
}
|
||||||
if got := response["success"]; got != true {
|
if rpcErr.Code != -32002 || !strings.Contains(rpcErr.Message, "openclaw wait timeout") {
|
||||||
t.Fatalf("expected wait failure to recover with artifact, got %#v", response)
|
t.Fatalf("expected surfaced wait timeout, got %#v", rpcErr)
|
||||||
}
|
|
||||||
artifacts := responseArtifactMaps(t, response)
|
|
||||||
if len(artifacts) != 1 || artifacts[0]["relativePath"] != "exports/final.pdf" {
|
|
||||||
t.Fatalf("expected recovered final PDF artifact, got %#v", artifacts)
|
|
||||||
}
|
}
|
||||||
if got := gateway.ChatSendCount(); got != 1 {
|
if got := gateway.ChatSendCount(); got != 1 {
|
||||||
t.Fatalf("expected no second model turn after wait failure, got %d", got)
|
t.Fatalf("expected no automatic repair model turn, got %d", got)
|
||||||
}
|
}
|
||||||
if got := gateway.AgentWaitCount(); got != 1 {
|
if got := gateway.AgentWaitCount(); got != 1 {
|
||||||
t.Fatalf("expected one failed wait before recovery, got %d", got)
|
t.Fatalf("expected one failed wait, got %d", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2766,7 +2719,6 @@ type acpFakeOpenClawGateway struct {
|
|||||||
agentWaitDelayMs atomic.Int64
|
agentWaitDelayMs atomic.Int64
|
||||||
largeGatewayPayloadBytes atomic.Int64
|
largeGatewayPayloadBytes atomic.Int64
|
||||||
emitAgentDelta atomic.Bool
|
emitAgentDelta atomic.Bool
|
||||||
finalizeRequested atomic.Bool
|
|
||||||
lastConnectClient atomic.Value
|
lastConnectClient atomic.Value
|
||||||
lastChatSendParams atomic.Value
|
lastChatSendParams atomic.Value
|
||||||
lastArtifactPrepareParams atomic.Value
|
lastArtifactPrepareParams atomic.Value
|
||||||
@ -2897,9 +2849,6 @@ func newAcpFakeOpenClawGateway(t *testing.T) *acpFakeOpenClawGateway {
|
|||||||
runID = strings.TrimSpace(fake.alternateRunID)
|
runID = strings.TrimSpace(fake.alternateRunID)
|
||||||
}
|
}
|
||||||
message := strings.TrimSpace(shared.StringArg(params, "message", ""))
|
message := strings.TrimSpace(shared.StringArg(params, "message", ""))
|
||||||
if strings.Contains(message, "XWorkmate final deliverable repair:") {
|
|
||||||
fake.finalizeRequested.Store(true)
|
|
||||||
}
|
|
||||||
fake.recordRunMessage(runID, message)
|
fake.recordRunMessage(runID, message)
|
||||||
_ = conn.WriteJSON(map[string]any{
|
_ = conn.WriteJSON(map[string]any{
|
||||||
"type": "res",
|
"type": "res",
|
||||||
@ -3097,17 +3046,6 @@ func newAcpFakeOpenClawGateway(t *testing.T) *acpFakeOpenClawGateway {
|
|||||||
"content": "ZmluYWwgcmVwb3J0",
|
"content": "ZmluYWwgcmVwb3J0",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if fake.finalizeRequested.Load() {
|
|
||||||
payload["artifacts"] = append(payload["artifacts"].([]any), map[string]any{
|
|
||||||
"relativePath": "exports/final.pdf",
|
|
||||||
"label": "final.pdf",
|
|
||||||
"contentType": "application/pdf",
|
|
||||||
"sizeBytes": 12,
|
|
||||||
"sha256": "fake-sha256-final",
|
|
||||||
"artifactScope": artifactScope,
|
|
||||||
"scopeKind": "task",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if strings.Contains(fake.runMessage(runID), "make pdf artifact") {
|
if strings.Contains(fake.runMessage(runID), "make pdf artifact") {
|
||||||
payload["artifacts"] = []any{
|
payload["artifacts"] = []any{
|
||||||
@ -3134,17 +3072,6 @@ func newAcpFakeOpenClawGateway(t *testing.T) *acpFakeOpenClawGateway {
|
|||||||
"scopeKind": "task",
|
"scopeKind": "task",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if fake.finalizeRequested.Load() {
|
|
||||||
payload["artifacts"] = append(payload["artifacts"].([]any), map[string]any{
|
|
||||||
"relativePath": "exports/final.pdf",
|
|
||||||
"label": "final.pdf",
|
|
||||||
"contentType": "application/pdf",
|
|
||||||
"sizeBytes": 12,
|
|
||||||
"sha256": "fake-sha256-final",
|
|
||||||
"artifactScope": artifactScope,
|
|
||||||
"scopeKind": "task",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if len(filesystemArtifacts) > 0 {
|
if len(filesystemArtifacts) > 0 {
|
||||||
payload["artifacts"] = appendArtifactList(payload["artifacts"], filesystemArtifacts)
|
payload["artifacts"] = appendArtifactList(payload["artifacts"], filesystemArtifacts)
|
||||||
|
|||||||
@ -413,6 +413,7 @@ func TestHTTPHandlerGatewayOpenClawHandlesFiveConcurrentE2ECases(t *testing.T) {
|
|||||||
close(results)
|
close(results)
|
||||||
|
|
||||||
var finalCount int
|
var finalCount int
|
||||||
|
var missingFinalArtifactCount int
|
||||||
for item := range results {
|
for item := range results {
|
||||||
if item.err != nil {
|
if item.err != nil {
|
||||||
t.Fatalf("concurrent e2e request failed: %v", item.err)
|
t.Fatalf("concurrent e2e request failed: %v", item.err)
|
||||||
@ -425,12 +426,14 @@ func TestHTTPHandlerGatewayOpenClawHandlesFiveConcurrentE2ECases(t *testing.T) {
|
|||||||
"SOCKET_CLOSED",
|
"SOCKET_CLOSED",
|
||||||
"ACP_HTTP_CONNECTION_CLOSED",
|
"ACP_HTTP_CONNECTION_CLOSED",
|
||||||
"GATEWAY_CONNECT_FAILED",
|
"GATEWAY_CONNECT_FAILED",
|
||||||
"openclaw returned partial artifacts without required final deliverables",
|
|
||||||
} {
|
} {
|
||||||
if strings.Contains(item.body, unexpected) {
|
if strings.Contains(item.body, unexpected) {
|
||||||
t.Fatalf("unexpected gateway stability error %q in body: %s", unexpected, item.body)
|
t.Fatalf("unexpected gateway stability error %q in body: %s", unexpected, item.body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if strings.Contains(item.body, "OPENCLAW_REQUIRED_ARTIFACT_MISSING") {
|
||||||
|
missingFinalArtifactCount += 1
|
||||||
|
}
|
||||||
if strings.Contains(item.body, `"result"`) && strings.Contains(item.body, `data: [DONE]`) {
|
if strings.Contains(item.body, `"result"`) && strings.Contains(item.body, `data: [DONE]`) {
|
||||||
finalCount += 1
|
finalCount += 1
|
||||||
}
|
}
|
||||||
@ -438,6 +441,9 @@ func TestHTTPHandlerGatewayOpenClawHandlesFiveConcurrentE2ECases(t *testing.T) {
|
|||||||
if finalCount != len(prompts) {
|
if finalCount != len(prompts) {
|
||||||
t.Fatalf("expected all five e2e requests to return final result, got %d", finalCount)
|
t.Fatalf("expected all five e2e requests to return final result, got %d", finalCount)
|
||||||
}
|
}
|
||||||
|
if missingFinalArtifactCount != len(prompts) {
|
||||||
|
t.Fatalf("expected all artifact-producing prompts to fail without real final artifacts, got %d", missingFinalArtifactCount)
|
||||||
|
}
|
||||||
if got := gateway.ConnectCount(); got != 1 {
|
if got := gateway.ConnectCount(); got != 1 {
|
||||||
t.Fatalf("expected bridge to reuse one established OpenClaw connection, got %d connects", got)
|
t.Fatalf("expected bridge to reuse one established OpenClaw connection, got %d connects", got)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user