Fix OpenClaw artifact intent for follow-up turns
This commit is contained in:
parent
b39c9ec084
commit
ee1207ebfc
@ -243,6 +243,7 @@ func (o *SessionOrchestrator) runOpenClawGatewayChat(
|
||||
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
|
||||
@ -256,6 +257,7 @@ func (o *SessionOrchestrator) runOpenClawGatewayChat(
|
||||
return nil, rpcErr
|
||||
}
|
||||
}
|
||||
logOpenClawArtifactIntent(gatewayProvider, sessionKey, artifactRunID, "prepare", artifactDeliveryRequired, preparedArtifact != nil, false, false)
|
||||
chatParams, rpcErr := openClawChatSendParams(params, turnID, preparedArtifact)
|
||||
if rpcErr != nil {
|
||||
return nil, rpcErr
|
||||
@ -335,13 +337,16 @@ func (o *SessionOrchestrator) runOpenClawGatewayChat(
|
||||
artifactRunID,
|
||||
artifactSinceUnixMs,
|
||||
preparedArtifact,
|
||||
artifactDeliveryRequired || artifactDeliveryClaimed || preparedArtifact != nil,
|
||||
artifactDeliveryRequired || preparedArtifact != nil,
|
||||
notifyWithCollection,
|
||||
)
|
||||
if artifactDeliveryClaimed {
|
||||
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)
|
||||
o.server.decorateOpenClawArtifactDownloadURLs(result, shared.StringArg(chatParams, "sessionKey", ""), artifactRunID)
|
||||
stripOpenClawArtifactInlineContent(result)
|
||||
guardOpenClawArtifactResult(result, artifactDeliveryRequired || artifactDeliveryClaimed)
|
||||
@ -368,6 +373,37 @@ func logOpenClawGatewayTiming(
|
||||
)
|
||||
}
|
||||
|
||||
func logOpenClawArtifactIntent(
|
||||
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",
|
||||
gatewayProvider,
|
||||
sessionKey,
|
||||
runID,
|
||||
stage,
|
||||
required,
|
||||
prepared,
|
||||
exported,
|
||||
empty,
|
||||
)
|
||||
}
|
||||
|
||||
func openClawArtifactPayloadCount(payload map[string]any) int {
|
||||
if payload == nil {
|
||||
return 0
|
||||
}
|
||||
remoteWorkingDirectory := strings.TrimSpace(shared.StringArg(payload, "remoteWorkingDirectory", ""))
|
||||
return len(extractArtifactPayloads(payload, remoteWorkingDirectory))
|
||||
}
|
||||
|
||||
func (o *SessionOrchestrator) openClawArtifactExportForDelivery(
|
||||
gatewayProvider string,
|
||||
chatParams map[string]any,
|
||||
@ -428,7 +464,7 @@ func openClawChatSendParams(
|
||||
turnID string,
|
||||
preparedArtifact *openClawPreparedArtifactScope,
|
||||
) (map[string]any, *shared.RPCError) {
|
||||
message := firstNonEmptyString(params, "taskPrompt", "prompt", "message")
|
||||
message := openClawCurrentTurnMessage(params)
|
||||
if message == "" {
|
||||
return nil, &shared.RPCError{Code: -32602, Message: "OPENCLAW_TASK_PROMPT_REQUIRED"}
|
||||
}
|
||||
@ -526,30 +562,121 @@ func openClawArtifactDeliveryText(raw any) []string {
|
||||
}
|
||||
case map[string]any:
|
||||
texts := make([]string, 0, len(value))
|
||||
for key, item := range value {
|
||||
switch strings.TrimSpace(key) {
|
||||
case "taskPrompt", "prompt", "message":
|
||||
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)...)
|
||||
default:
|
||||
if _, ok := item.(map[string]any); ok {
|
||||
texts = append(texts, openClawArtifactDeliveryText(item)...)
|
||||
}
|
||||
if _, ok := item.([]any); ok {
|
||||
texts = append(texts, openClawArtifactDeliveryText(item)...)
|
||||
}
|
||||
}
|
||||
}
|
||||
return texts
|
||||
return compactOpenClawTexts(texts)
|
||||
case []any:
|
||||
texts := make([]string, 0, len(value))
|
||||
for _, item := range value {
|
||||
texts = append(texts, openClawArtifactDeliveryText(item)...)
|
||||
}
|
||||
return texts
|
||||
return compactOpenClawTexts(texts)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func openClawCurrentTurnMessage(params map[string]any) string {
|
||||
if params == nil {
|
||||
return ""
|
||||
}
|
||||
for _, key := range []string{"taskPrompt", "prompt", "message"} {
|
||||
if text := strings.TrimSpace(strings.Join(openClawTextFragments(params[key]), "\n")); text != "" {
|
||||
return text
|
||||
}
|
||||
}
|
||||
if text := strings.TrimSpace(strings.Join(openClawLatestUserMessageText(params["messages"]), "\n")); text != "" {
|
||||
return text
|
||||
}
|
||||
for _, key := range []string{"input", "content"} {
|
||||
if text := strings.TrimSpace(strings.Join(openClawTextFragments(params[key]), "\n")); text != "" {
|
||||
return text
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func openClawLatestUserMessageText(raw any) []string {
|
||||
messages, ok := raw.([]any)
|
||||
if !ok || len(messages) == 0 {
|
||||
return nil
|
||||
}
|
||||
var fallback []string
|
||||
for index := len(messages) - 1; index >= 0; index-- {
|
||||
message := shared.AsMap(messages[index])
|
||||
if len(message) == 0 {
|
||||
continue
|
||||
}
|
||||
text := compactOpenClawTexts(openClawMessageText(message))
|
||||
if len(text) == 0 {
|
||||
continue
|
||||
}
|
||||
if fallback == nil {
|
||||
fallback = text
|
||||
}
|
||||
role := strings.ToLower(strings.TrimSpace(shared.StringArg(message, "role", "")))
|
||||
switch role {
|
||||
case "user", "human", "client":
|
||||
return text
|
||||
}
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func openClawMessageText(message map[string]any) []string {
|
||||
if len(message) == 0 {
|
||||
return nil
|
||||
}
|
||||
texts := make([]string, 0, 4)
|
||||
for _, key := range []string{"content", "parts", "text", "message"} {
|
||||
texts = append(texts, openClawTextFragments(message[key])...)
|
||||
}
|
||||
return texts
|
||||
}
|
||||
|
||||
func openClawTextFragments(raw any) []string {
|
||||
switch value := raw.(type) {
|
||||
case nil:
|
||||
return nil
|
||||
case string:
|
||||
if text := strings.TrimSpace(value); text != "" {
|
||||
return []string{text}
|
||||
}
|
||||
case []any:
|
||||
texts := make([]string, 0, len(value))
|
||||
for _, item := range value {
|
||||
texts = append(texts, openClawTextFragments(item)...)
|
||||
}
|
||||
return compactOpenClawTexts(texts)
|
||||
case map[string]any:
|
||||
texts := make([]string, 0, len(value))
|
||||
for _, key := range []string{"text", "content", "message", "value"} {
|
||||
texts = append(texts, openClawTextFragments(value[key])...)
|
||||
}
|
||||
return compactOpenClawTexts(texts)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func compactOpenClawTexts(texts []string) []string {
|
||||
if len(texts) == 0 {
|
||||
return nil
|
||||
}
|
||||
result := make([]string, 0, len(texts))
|
||||
for _, text := range texts {
|
||||
if trimmed := strings.TrimSpace(text); trimmed != "" {
|
||||
result = append(result, trimmed)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func withOpenClawArtifactDeliveryInstructions(
|
||||
message string,
|
||||
preparedArtifact *openClawPreparedArtifactScope,
|
||||
|
||||
@ -934,18 +934,61 @@ func TestExecuteSessionMessageGatewayRejectsClaimedArtifactsWithoutScopedFiles(t
|
||||
if got := response["status"]; got != "artifact_missing" {
|
||||
t.Fatalf("expected artifact_missing status, got %#v", response)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteSessionMessageGatewayPreparesArtifactsFromMessagesPrompt(t *testing.T) {
|
||||
gateway := newAcpFakeOpenClawGateway(t)
|
||||
defer gateway.Close()
|
||||
|
||||
t.Setenv("GATEWAY_RPC_URL", gateway.URL())
|
||||
t.Setenv("BRIDGE_AUTH_TOKEN", "bridge-token")
|
||||
|
||||
server := NewServer()
|
||||
response, rpcErr := server.executeSessionTask(task{
|
||||
req: shared.RPCRequest{
|
||||
Method: "session.message",
|
||||
Params: map[string]any{
|
||||
"sessionId": "session-openclaw-message-artifact",
|
||||
"threadId": "thread-openclaw-message-artifact",
|
||||
"workingDirectory": t.TempDir(),
|
||||
"messages": []any{
|
||||
map[string]any{
|
||||
"role": "assistant",
|
||||
"content": "上一轮只是分析。",
|
||||
},
|
||||
map[string]any{
|
||||
"role": "user",
|
||||
"content": []any{
|
||||
map[string]any{"type": "text", "text": "继续,把这些内容 make artifact 输出为 Markdown 文件。"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"routing": map[string]any{
|
||||
"routingMode": "explicit",
|
||||
"explicitExecutionTarget": "gateway",
|
||||
"preferredGatewayProviderId": "openclaw",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if rpcErr != nil {
|
||||
t.Fatalf("expected message artifact response, got rpc error: %#v", rpcErr)
|
||||
}
|
||||
if got := response["success"]; got != true {
|
||||
t.Fatalf("expected artifact response success, got %#v", response)
|
||||
}
|
||||
exportParams := gateway.LastArtifactExportParams()
|
||||
if _, ok := exportParams["latestIfEmpty"]; ok {
|
||||
t.Fatalf("expected no latestIfEmpty fallback export param, got %#v", exportParams)
|
||||
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 _, ok := exportParams["latestTaskScopeIfEmpty"]; ok {
|
||||
t.Fatalf("expected no latestTaskScopeIfEmpty fallback export param, got %#v", exportParams)
|
||||
}
|
||||
if _, ok := exportParams["artifactScope"]; ok {
|
||||
t.Fatalf("expected no new prepared artifact scope for claimed follow-up, got %#v", exportParams)
|
||||
}
|
||||
if got := gateway.Methods(); !sameMethods(got, []string{"connect", "chat.send", "agent.wait", "xworkmate.artifacts.export"}) {
|
||||
t.Fatalf("expected connect, chat.send, agent.wait, then artifact export, got %#v", got)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1401,6 +1444,23 @@ func TestOpenClawArtifactDeliveryRequiredScansNestedParams(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
|
||||
@ -224,7 +224,7 @@ func TestHTTPHandlerGatewayOpenClawSSEKeepaliveBeforeFinalEnvelopeAndDone(t *tes
|
||||
if err != nil {
|
||||
t.Fatalf("send request: %v", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
defer func() { _ = response.Body.Close() }()
|
||||
body, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("read response: %v", err)
|
||||
@ -322,7 +322,7 @@ func TestHTTPHandlerGatewayOpenClawAdmissionQueuesExcessConcurrentSSE(t *testing
|
||||
results <- result{err: err}
|
||||
return
|
||||
}
|
||||
defer response.Body.Close()
|
||||
defer func() { _ = response.Body.Close() }()
|
||||
body, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
results <- result{err: err}
|
||||
@ -401,7 +401,7 @@ func TestHTTPHandlerGatewayOpenClawAdmissionRejectsWhenQueueFull(t *testing.T) {
|
||||
firstDone <- err
|
||||
return
|
||||
}
|
||||
defer response.Body.Close()
|
||||
defer func() { _ = response.Body.Close() }()
|
||||
_, err = io.ReadAll(response.Body)
|
||||
firstDone <- err
|
||||
}()
|
||||
@ -422,7 +422,7 @@ func TestHTTPHandlerGatewayOpenClawAdmissionRejectsWhenQueueFull(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("send second request: %v", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
defer func() { _ = response.Body.Close() }()
|
||||
body, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("read second response: %v", err)
|
||||
@ -471,7 +471,7 @@ func TestHTTPHandlerGatewayOpenClawFiltersRawGatewayEventsAndKeepsFinalResult(t
|
||||
if err != nil {
|
||||
t.Fatalf("send request: %v", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
defer func() { _ = response.Body.Close() }()
|
||||
body, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("read response: %v", err)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user