feat: sync OpenClaw artifacts by scoped export
This commit is contained in:
parent
8fbc0e51be
commit
f3f2e7464c
@ -46,6 +46,8 @@ func (s *Server) HandleOpenClawArtifactDownload(w http.ResponseWriter, r *http.R
|
||||
query := r.URL.Query()
|
||||
sessionKey := strings.TrimSpace(query.Get("sessionKey"))
|
||||
runID := strings.TrimSpace(query.Get("runId"))
|
||||
rawArtifactScope := strings.TrimSpace(query.Get("artifactScope"))
|
||||
artifactScope := safeOpenClawArtifactDownloadArtifactScope(rawArtifactScope)
|
||||
relativePath := safeOpenClawArtifactDownloadRelativePath(query.Get("relativePath"))
|
||||
expires := strings.TrimSpace(query.Get("expires"))
|
||||
signature := strings.TrimSpace(query.Get("sig"))
|
||||
@ -53,6 +55,10 @@ func (s *Server) HandleOpenClawArtifactDownload(w http.ResponseWriter, r *http.R
|
||||
shared.WriteJSONError(w, nil, http.StatusBadRequest, -32602, "missing artifact download parameters")
|
||||
return
|
||||
}
|
||||
if rawArtifactScope != "" && artifactScope == "" {
|
||||
shared.WriteJSONError(w, nil, http.StatusBadRequest, -32602, "invalid artifact scope")
|
||||
return
|
||||
}
|
||||
expiresUnix, err := strconv.ParseInt(expires, 10, 64)
|
||||
if err != nil || expiresUnix <= 0 {
|
||||
shared.WriteJSONError(w, nil, http.StatusBadRequest, -32602, "invalid artifact download expiry")
|
||||
@ -62,7 +68,7 @@ func (s *Server) HandleOpenClawArtifactDownload(w http.ResponseWriter, r *http.R
|
||||
shared.WriteJSONError(w, nil, http.StatusGone, -32041, "artifact download link expired")
|
||||
return
|
||||
}
|
||||
if !validOpenClawArtifactDownloadSignature(sessionKey, runID, relativePath, expires, signature) {
|
||||
if !validOpenClawArtifactDownloadSignature(sessionKey, runID, artifactScope, relativePath, expires, signature) {
|
||||
shared.WriteJSONError(w, nil, http.StatusForbidden, -32042, "invalid artifact download signature")
|
||||
return
|
||||
}
|
||||
@ -71,15 +77,19 @@ func (s *Server) HandleOpenClawArtifactDownload(w http.ResponseWriter, r *http.R
|
||||
shared.WriteJSONError(w, nil, http.StatusBadGateway, rpcErr.Code, rpcErr.Message)
|
||||
return
|
||||
}
|
||||
readParams := map[string]any{
|
||||
"sessionKey": sessionKey,
|
||||
"runId": runID,
|
||||
"relativePath": relativePath,
|
||||
"maxInlineBytes": openClawArtifactDownloadMaxBytes,
|
||||
}
|
||||
if artifactScope != "" {
|
||||
readParams["artifactScope"] = artifactScope
|
||||
}
|
||||
readResult := s.gateway.RequestByMode(
|
||||
"openclaw",
|
||||
"xworkmate.artifacts.read",
|
||||
map[string]any{
|
||||
"sessionKey": sessionKey,
|
||||
"runId": runID,
|
||||
"relativePath": relativePath,
|
||||
"maxInlineBytes": openClawArtifactDownloadMaxBytes,
|
||||
},
|
||||
readParams,
|
||||
time.Minute,
|
||||
nil,
|
||||
)
|
||||
@ -95,7 +105,7 @@ func (s *Server) HandleOpenClawArtifactDownload(w http.ResponseWriter, r *http.R
|
||||
|
||||
payload := shared.AsMap(readResult.Payload)
|
||||
remoteWorkingDirectory := strings.TrimSpace(shared.StringArg(payload, "remoteWorkingDirectory", ""))
|
||||
artifact, ok := firstOpenClawArtifactForPath(payload, remoteWorkingDirectory, relativePath)
|
||||
artifact, ok := firstOpenClawArtifactForPath(payload, remoteWorkingDirectory, artifactScope, relativePath)
|
||||
if !ok {
|
||||
shared.WriteJSONError(w, nil, http.StatusNotFound, -32044, "artifact_missing")
|
||||
return
|
||||
@ -144,6 +154,7 @@ func (s *Server) decorateOpenClawArtifactDownloadURLs(result map[string]any, ses
|
||||
if result == nil || strings.TrimSpace(sessionKey) == "" || strings.TrimSpace(runID) == "" {
|
||||
return
|
||||
}
|
||||
resultArtifactScope := safeOpenClawArtifactDownloadArtifactScope(shared.StringArg(result, "artifactScope", ""))
|
||||
for _, key := range []string{"artifacts", "files", "attachments"} {
|
||||
switch items := result[key].(type) {
|
||||
case []any:
|
||||
@ -152,19 +163,24 @@ func (s *Server) decorateOpenClawArtifactDownloadURLs(result map[string]any, ses
|
||||
if len(mapped) == 0 {
|
||||
continue
|
||||
}
|
||||
s.decorateOpenClawArtifactDownloadURL(mapped, sessionKey, runID)
|
||||
s.decorateOpenClawArtifactDownloadURL(mapped, sessionKey, runID, resultArtifactScope)
|
||||
items[index] = mapped
|
||||
}
|
||||
result[key] = items
|
||||
case []map[string]any:
|
||||
for _, mapped := range items {
|
||||
s.decorateOpenClawArtifactDownloadURL(mapped, sessionKey, runID)
|
||||
s.decorateOpenClawArtifactDownloadURL(mapped, sessionKey, runID, resultArtifactScope)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) decorateOpenClawArtifactDownloadURL(artifact map[string]any, sessionKey string, runID string) {
|
||||
func (s *Server) decorateOpenClawArtifactDownloadURL(
|
||||
artifact map[string]any,
|
||||
sessionKey string,
|
||||
runID string,
|
||||
resultArtifactScope string,
|
||||
) {
|
||||
if artifact == nil {
|
||||
return
|
||||
}
|
||||
@ -179,20 +195,38 @@ func (s *Server) decorateOpenClawArtifactDownloadURL(artifact map[string]any, se
|
||||
if relativePath == "" {
|
||||
return
|
||||
}
|
||||
downloadURL := s.openClawArtifactDownloadURL(sessionKey, runID, relativePath, time.Now())
|
||||
artifactScope := safeOpenClawArtifactDownloadArtifactScope(shared.StringArg(artifact, "artifactScope", ""))
|
||||
if artifactScope == "" {
|
||||
artifactScope = resultArtifactScope
|
||||
}
|
||||
downloadURL := s.openClawArtifactDownloadURL(sessionKey, runID, artifactScope, relativePath, time.Now())
|
||||
if downloadURL == "" {
|
||||
return
|
||||
}
|
||||
artifact["relativePath"] = relativePath
|
||||
if artifactScope != "" {
|
||||
artifact["artifactScope"] = artifactScope
|
||||
}
|
||||
artifact["downloadUrl"] = downloadURL
|
||||
delete(artifact, "downloadURL")
|
||||
delete(artifact, "download_url")
|
||||
}
|
||||
|
||||
func (s *Server) openClawArtifactDownloadURL(sessionKey string, runID string, relativePath string, now time.Time) string {
|
||||
func (s *Server) openClawArtifactDownloadURL(
|
||||
sessionKey string,
|
||||
runID string,
|
||||
artifactScope string,
|
||||
relativePath string,
|
||||
now time.Time,
|
||||
) string {
|
||||
sessionKey = strings.TrimSpace(sessionKey)
|
||||
runID = strings.TrimSpace(runID)
|
||||
rawArtifactScope := strings.TrimSpace(artifactScope)
|
||||
artifactScope = safeOpenClawArtifactDownloadArtifactScope(artifactScope)
|
||||
relativePath = safeOpenClawArtifactDownloadRelativePath(relativePath)
|
||||
if rawArtifactScope != "" && artifactScope == "" {
|
||||
return ""
|
||||
}
|
||||
if sessionKey == "" || runID == "" || relativePath == "" || openClawArtifactSigningSecret() == "" {
|
||||
return ""
|
||||
}
|
||||
@ -216,9 +250,10 @@ func (s *Server) openClawArtifactDownloadURL(sessionKey string, runID string, re
|
||||
query := parsed.Query()
|
||||
query.Set("sessionKey", sessionKey)
|
||||
query.Set("runId", runID)
|
||||
query.Set("artifactScope", artifactScope)
|
||||
query.Set("relativePath", relativePath)
|
||||
query.Set("expires", expires)
|
||||
query.Set("sig", signOpenClawArtifactDownload(sessionKey, runID, relativePath, expires))
|
||||
query.Set("sig", signOpenClawArtifactDownload(sessionKey, runID, artifactScope, relativePath, expires))
|
||||
parsed.RawQuery = query.Encode()
|
||||
return parsed.String()
|
||||
}
|
||||
@ -226,10 +261,15 @@ func (s *Server) openClawArtifactDownloadURL(sessionKey string, runID string, re
|
||||
func firstOpenClawArtifactForPath(
|
||||
payload map[string]any,
|
||||
remoteWorkingDirectory string,
|
||||
artifactScope string,
|
||||
relativePath string,
|
||||
) (map[string]any, bool) {
|
||||
artifacts := extractArtifactPayloads(payload, remoteWorkingDirectory)
|
||||
for _, artifact := range artifacts {
|
||||
if artifactScope != "" &&
|
||||
safeOpenClawArtifactDownloadArtifactScope(shared.StringArg(artifact, "artifactScope", "")) != artifactScope {
|
||||
continue
|
||||
}
|
||||
if safeOpenClawArtifactDownloadRelativePath(shared.StringArg(artifact, "relativePath", "")) == relativePath {
|
||||
return artifact, true
|
||||
}
|
||||
@ -258,14 +298,26 @@ func safeOpenClawArtifactDownloadRelativePath(rawPath string) string {
|
||||
return cleaned
|
||||
}
|
||||
|
||||
func safeOpenClawArtifactDownloadArtifactScope(rawScope string) string {
|
||||
scope := safeOpenClawArtifactDownloadRelativePath(rawScope)
|
||||
if scope == "" {
|
||||
return ""
|
||||
}
|
||||
if !strings.HasPrefix(scope, ".xworkmate/artifacts/tasks/") {
|
||||
return ""
|
||||
}
|
||||
return scope
|
||||
}
|
||||
|
||||
func validOpenClawArtifactDownloadSignature(
|
||||
sessionKey string,
|
||||
runID string,
|
||||
artifactScope string,
|
||||
relativePath string,
|
||||
expires string,
|
||||
signature string,
|
||||
) bool {
|
||||
expected := signOpenClawArtifactDownload(sessionKey, runID, relativePath, expires)
|
||||
expected := signOpenClawArtifactDownload(sessionKey, runID, artifactScope, relativePath, expires)
|
||||
if expected == "" || signature == "" {
|
||||
return false
|
||||
}
|
||||
@ -280,15 +332,23 @@ func validOpenClawArtifactDownloadSignature(
|
||||
return hmac.Equal(expectedBytes, actualBytes)
|
||||
}
|
||||
|
||||
func signOpenClawArtifactDownload(sessionKey string, runID string, relativePath string, expires string) string {
|
||||
func signOpenClawArtifactDownload(
|
||||
sessionKey string,
|
||||
runID string,
|
||||
artifactScope string,
|
||||
relativePath string,
|
||||
expires string,
|
||||
) string {
|
||||
secret := openClawArtifactSigningSecret()
|
||||
if secret == "" {
|
||||
return ""
|
||||
}
|
||||
mac := hmac.New(sha256.New, []byte(secret))
|
||||
artifactScope = safeOpenClawArtifactDownloadArtifactScope(artifactScope)
|
||||
_, _ = mac.Write([]byte(strings.Join([]string{
|
||||
strings.TrimSpace(sessionKey),
|
||||
strings.TrimSpace(runID),
|
||||
artifactScope,
|
||||
safeOpenClawArtifactDownloadRelativePath(relativePath),
|
||||
strings.TrimSpace(expires),
|
||||
}, "\n")))
|
||||
|
||||
@ -180,11 +180,26 @@ func (o *SessionOrchestrator) runOpenClawGatewayChat(
|
||||
notify(message)
|
||||
}
|
||||
}
|
||||
chatParams, rpcErr := openClawChatSendParams(params, turnID)
|
||||
artifactDeliveryRequired := openClawArtifactDeliveryRequired(params)
|
||||
sessionKey := openClawSessionKey(params, turnID)
|
||||
artifactRunID := turnID
|
||||
var preparedArtifact *openClawPreparedArtifactScope
|
||||
if artifactDeliveryRequired {
|
||||
var rpcErr *shared.RPCError
|
||||
preparedArtifact, rpcErr = o.openClawArtifactPrepare(
|
||||
gatewayProvider,
|
||||
sessionKey,
|
||||
artifactRunID,
|
||||
notifyWithCollection,
|
||||
)
|
||||
if rpcErr != nil {
|
||||
return nil, rpcErr
|
||||
}
|
||||
}
|
||||
chatParams, rpcErr := openClawChatSendParams(params, turnID, preparedArtifact)
|
||||
if rpcErr != nil {
|
||||
return nil, rpcErr
|
||||
}
|
||||
artifactDeliveryRequired := openClawArtifactDeliveryRequired(params)
|
||||
artifactSinceUnixMs := time.Now().Add(-1 * time.Second).UnixMilli()
|
||||
sendResult := o.server.gateway.RequestByMode(
|
||||
gatewayProvider,
|
||||
@ -234,11 +249,13 @@ func (o *SessionOrchestrator) runOpenClawGatewayChat(
|
||||
mergeOpenClawArtifactPayload(result, o.openClawArtifactExport(
|
||||
gatewayProvider,
|
||||
chatParams,
|
||||
runID,
|
||||
artifactRunID,
|
||||
artifactSinceUnixMs,
|
||||
preparedArtifact,
|
||||
artifactDeliveryRequired,
|
||||
notifyWithCollection,
|
||||
))
|
||||
o.server.decorateOpenClawArtifactDownloadURLs(result, shared.StringArg(chatParams, "sessionKey", ""), runID)
|
||||
o.server.decorateOpenClawArtifactDownloadURLs(result, shared.StringArg(chatParams, "sessionKey", ""), artifactRunID)
|
||||
guardOpenClawArtifactResult(result, artifactDeliveryRequired)
|
||||
return result, nil
|
||||
}
|
||||
@ -252,13 +269,23 @@ func isSessionTaskMethod(method string) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func openClawChatSendParams(params map[string]any, turnID string) (map[string]any, *shared.RPCError) {
|
||||
type openClawPreparedArtifactScope struct {
|
||||
ArtifactScope string
|
||||
ArtifactDirectory string
|
||||
ScopeKind string
|
||||
}
|
||||
|
||||
func openClawChatSendParams(
|
||||
params map[string]any,
|
||||
turnID string,
|
||||
preparedArtifact *openClawPreparedArtifactScope,
|
||||
) (map[string]any, *shared.RPCError) {
|
||||
message := firstNonEmptyString(params, "taskPrompt", "prompt", "message")
|
||||
if message == "" {
|
||||
return nil, &shared.RPCError{Code: -32602, Message: "OPENCLAW_TASK_PROMPT_REQUIRED"}
|
||||
}
|
||||
if openClawArtifactDeliveryRequired(params) {
|
||||
message = withOpenClawArtifactDeliveryInstructions(message)
|
||||
message = withOpenClawArtifactDeliveryInstructions(message, preparedArtifact)
|
||||
}
|
||||
sessionKey := openClawSessionKey(params, turnID)
|
||||
chatParams := map[string]any{
|
||||
@ -290,7 +317,7 @@ func openClawArtifactDeliveryRequired(params map[string]any) bool {
|
||||
"视频", "音频", "压缩包", "数据集", "文档", "报告", "演示", "幻灯片", "表格", "代码",
|
||||
}
|
||||
actionSignals := []string{
|
||||
"create", "generate", "build", "write", "export", "output", "deliver", "download",
|
||||
"create", "generate", "build", "make", "write", "export", "output", "deliver", "download",
|
||||
"save", "produce", "render", "attach", "return",
|
||||
"生成", "制作", "输出", "导出", "下载", "交付", "收取", "保存", "渲染", "返回", "提供",
|
||||
}
|
||||
@ -312,18 +339,31 @@ func openClawArtifactDeliveryRequired(params map[string]any) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func withOpenClawArtifactDeliveryInstructions(message string) string {
|
||||
func withOpenClawArtifactDeliveryInstructions(
|
||||
message string,
|
||||
preparedArtifact *openClawPreparedArtifactScope,
|
||||
) string {
|
||||
message = strings.TrimSpace(message)
|
||||
if message == "" {
|
||||
return message
|
||||
}
|
||||
return message + "\n\n" + strings.Join([]string{
|
||||
lines := []string{
|
||||
"XWorkmate artifact delivery requirements:",
|
||||
"- Create the requested files in the current OpenClaw workspace as real files before finishing.",
|
||||
"- 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.",
|
||||
}, "\n")
|
||||
)
|
||||
return message + "\n\n" + strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func openClawSessionKey(params map[string]any, turnID string) string {
|
||||
@ -338,27 +378,72 @@ 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.server.gateway.RequestByMode(
|
||||
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,
|
||||
runID string,
|
||||
sinceUnixMs int64,
|
||||
preparedArtifact *openClawPreparedArtifactScope,
|
||||
latestIfEmpty bool,
|
||||
notify func(map[string]any),
|
||||
) map[string]any {
|
||||
sessionKey := strings.TrimSpace(shared.StringArg(chatParams, "sessionKey", ""))
|
||||
if sessionKey == "" || strings.TrimSpace(runID) == "" {
|
||||
return nil
|
||||
}
|
||||
exportParams := map[string]any{
|
||||
"sessionKey": sessionKey,
|
||||
"runId": strings.TrimSpace(runID),
|
||||
"sinceUnixMs": sinceUnixMs,
|
||||
"maxFiles": 64,
|
||||
"maxInlineBytes": 10 * 1024 * 1024,
|
||||
}
|
||||
if preparedArtifact != nil && strings.TrimSpace(preparedArtifact.ArtifactScope) != "" {
|
||||
exportParams["artifactScope"] = strings.TrimSpace(preparedArtifact.ArtifactScope)
|
||||
}
|
||||
if latestIfEmpty {
|
||||
exportParams["latestIfEmpty"] = true
|
||||
}
|
||||
exportResult := o.server.gateway.RequestByMode(
|
||||
gatewayProvider,
|
||||
"xworkmate.artifacts.export",
|
||||
map[string]any{
|
||||
"sessionKey": sessionKey,
|
||||
"runId": strings.TrimSpace(runID),
|
||||
"sinceUnixMs": sinceUnixMs,
|
||||
"maxFiles": 64,
|
||||
"maxInlineBytes": 10 * 1024 * 1024,
|
||||
},
|
||||
exportParams,
|
||||
30*time.Second,
|
||||
notify,
|
||||
)
|
||||
@ -408,6 +493,16 @@ func mergeOpenClawArtifactPayload(result map[string]any, source map[string]any)
|
||||
result["remoteWorkspaceRefKind"] = remoteWorkspaceRefKind
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(shared.StringArg(result, "artifactScope", "")) == "" {
|
||||
if artifactScope := strings.TrimSpace(shared.StringArg(source, "artifactScope", "")); artifactScope != "" {
|
||||
result["artifactScope"] = artifactScope
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(shared.StringArg(result, "scopeKind", "")) == "" {
|
||||
if scopeKind := strings.TrimSpace(shared.StringArg(source, "scopeKind", "")); scopeKind != "" {
|
||||
result["scopeKind"] = scopeKind
|
||||
}
|
||||
}
|
||||
for _, key := range []string{"artifacts", "files", "attachments", "artifactWarnings", "warnings"} {
|
||||
merged := appendArtifactList(result[key], source[key])
|
||||
if len(merged) > 0 {
|
||||
|
||||
@ -697,11 +697,15 @@ 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, ".xworkmate/artifacts/tasks/") {
|
||||
t.Fatalf("expected artifact scope in downloadUrl, got %q", artifactScope)
|
||||
}
|
||||
if parsedDownloadURL.Query().Get("sig") == "" {
|
||||
t.Fatalf("expected signed downloadUrl, got %q", downloadURL)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -713,7 +717,13 @@ func TestHTTPHandlerOpenClawArtifactDownloadReadsViaGateway(t *testing.T) {
|
||||
t.Setenv("BRIDGE_AUTH_TOKEN", "bridge-token")
|
||||
|
||||
server := NewServer()
|
||||
downloadURL := server.openClawArtifactDownloadURL("thread-openclaw-artifact", "run-1", "reports/final.md", time.Now())
|
||||
downloadURL := server.openClawArtifactDownloadURL(
|
||||
"thread-openclaw-artifact",
|
||||
"run-1",
|
||||
".xworkmate/artifacts/tasks/thread-openclaw-artifact/run-1",
|
||||
"reports/final.md",
|
||||
time.Now(),
|
||||
)
|
||||
if downloadURL == "" {
|
||||
t.Fatal("expected signed download URL")
|
||||
}
|
||||
@ -747,7 +757,13 @@ func TestHTTPHandlerOpenClawArtifactDownloadReturnsArtifactMissing(t *testing.T)
|
||||
t.Setenv("BRIDGE_AUTH_TOKEN", "bridge-token")
|
||||
|
||||
server := NewServer()
|
||||
downloadURL := server.openClawArtifactDownloadURL("thread-openclaw-artifact", "run-1", "missing.txt", time.Now())
|
||||
downloadURL := server.openClawArtifactDownloadURL(
|
||||
"thread-openclaw-artifact",
|
||||
"run-1",
|
||||
".xworkmate/artifacts/tasks/thread-openclaw-artifact/run-1",
|
||||
"missing.txt",
|
||||
time.Now(),
|
||||
)
|
||||
if downloadURL == "" {
|
||||
t.Fatal("expected signed download URL")
|
||||
}
|
||||
@ -771,7 +787,13 @@ func TestHTTPHandlerOpenClawArtifactDownloadRequiresBearer(t *testing.T) {
|
||||
t.Setenv("BRIDGE_AUTH_TOKEN", "bridge-token")
|
||||
|
||||
server := NewServer()
|
||||
downloadURL := server.openClawArtifactDownloadURL("thread-openclaw-artifact", "run-1", "reports/final.md", time.Now())
|
||||
downloadURL := server.openClawArtifactDownloadURL(
|
||||
"thread-openclaw-artifact",
|
||||
"run-1",
|
||||
".xworkmate/artifacts/tasks/thread-openclaw-artifact/run-1",
|
||||
"reports/final.md",
|
||||
time.Now(),
|
||||
)
|
||||
recorder := httptest.NewRecorder()
|
||||
request := httptest.NewRequest(http.MethodGet, downloadURL, nil)
|
||||
server.Handler().ServeHTTP(recorder, request)
|
||||
@ -785,7 +807,13 @@ func TestHTTPHandlerOpenClawArtifactDownloadRejectsInvalidSignature(t *testing.T
|
||||
t.Setenv("BRIDGE_AUTH_TOKEN", "bridge-token")
|
||||
|
||||
server := NewServer()
|
||||
downloadURL := server.openClawArtifactDownloadURL("thread-openclaw-artifact", "run-1", "reports/final.md", time.Now())
|
||||
downloadURL := server.openClawArtifactDownloadURL(
|
||||
"thread-openclaw-artifact",
|
||||
"run-1",
|
||||
".xworkmate/artifacts/tasks/thread-openclaw-artifact/run-1",
|
||||
"reports/final.md",
|
||||
time.Now(),
|
||||
)
|
||||
parsed, err := url.Parse(downloadURL)
|
||||
if err != nil {
|
||||
t.Fatalf("parse downloadUrl: %v", err)
|
||||
@ -811,9 +839,16 @@ func TestHTTPHandlerOpenClawArtifactDownloadRejectsExpiredSignature(t *testing.T
|
||||
expires := fmt.Sprintf("%d", time.Now().Add(-time.Minute).Unix())
|
||||
values.Set("sessionKey", "thread-openclaw-artifact")
|
||||
values.Set("runId", "run-1")
|
||||
values.Set("artifactScope", ".xworkmate/artifacts/tasks/thread-openclaw-artifact/run-1")
|
||||
values.Set("relativePath", "reports/final.md")
|
||||
values.Set("expires", expires)
|
||||
values.Set("sig", signOpenClawArtifactDownload("thread-openclaw-artifact", "run-1", "reports/final.md", expires))
|
||||
values.Set("sig", signOpenClawArtifactDownload(
|
||||
"thread-openclaw-artifact",
|
||||
"run-1",
|
||||
".xworkmate/artifacts/tasks/thread-openclaw-artifact/run-1",
|
||||
"reports/final.md",
|
||||
expires,
|
||||
))
|
||||
|
||||
server := NewServer()
|
||||
recorder := httptest.NewRecorder()
|
||||
@ -847,6 +882,28 @@ func TestHTTPHandlerOpenClawArtifactDownloadRejectsTraversalPath(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPHandlerOpenClawArtifactDownloadRejectsInvalidArtifactScope(t *testing.T) {
|
||||
t.Setenv("BRIDGE_AUTH_TOKEN", "bridge-token")
|
||||
|
||||
values := url.Values{}
|
||||
values.Set("sessionKey", "thread-openclaw-artifact")
|
||||
values.Set("runId", "run-1")
|
||||
values.Set("artifactScope", "../outside")
|
||||
values.Set("relativePath", "reports/final.md")
|
||||
values.Set("expires", fmt.Sprintf("%d", time.Now().Add(time.Hour).Unix()))
|
||||
values.Set("sig", "irrelevant")
|
||||
|
||||
server := NewServer()
|
||||
recorder := httptest.NewRecorder()
|
||||
request := httptest.NewRequest(http.MethodGet, openClawArtifactDownloadPath+"?"+values.Encode(), nil)
|
||||
request.Header.Set("Authorization", "Bearer bridge-token")
|
||||
server.Handler().ServeHTTP(recorder, request)
|
||||
|
||||
if recorder.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected 400, got %d body=%q", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenClawChatSendParamsAddsArtifactDeliveryInstructions(t *testing.T) {
|
||||
for _, prompt := range []string{
|
||||
"输出 PPT PDF docx 文件",
|
||||
@ -858,7 +915,11 @@ func TestOpenClawChatSendParamsAddsArtifactDeliveryInstructions(t *testing.T) {
|
||||
chatParams, rpcErr := openClawChatSendParams(map[string]any{
|
||||
"threadId": "thread-artifact-instructions",
|
||||
"taskPrompt": prompt,
|
||||
}, "turn-artifact-instructions")
|
||||
}, "turn-artifact-instructions", &openClawPreparedArtifactScope{
|
||||
ArtifactScope: ".xworkmate/artifacts/tasks/thread-artifact-instructions/turn-artifact-instructions",
|
||||
ArtifactDirectory: "/remote/openclaw/workspace/.xworkmate/artifacts/tasks/thread-artifact-instructions/turn-artifact-instructions",
|
||||
ScopeKind: "task",
|
||||
})
|
||||
if rpcErr != nil {
|
||||
t.Fatalf("expected chat params, got rpc error: %#v", rpcErr)
|
||||
}
|
||||
@ -866,9 +927,12 @@ func TestOpenClawChatSendParamsAddsArtifactDeliveryInstructions(t *testing.T) {
|
||||
if !strings.Contains(message, prompt) {
|
||||
t.Fatalf("expected original prompt to be preserved, got %q", message)
|
||||
}
|
||||
if !strings.Contains(message, "Create the requested files in the current OpenClaw workspace as real files") {
|
||||
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/.xworkmate/artifacts/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)
|
||||
}
|
||||
@ -1232,6 +1296,27 @@ func newAcpFakeOpenClawGateway(t *testing.T) *acpFakeOpenClawGateway {
|
||||
"status": "started",
|
||||
},
|
||||
})
|
||||
case "xworkmate.artifacts.prepare":
|
||||
params := shared.AsMap(frame["params"])
|
||||
runID := strings.TrimSpace(shared.StringArg(params, "runId", "fake-run"))
|
||||
sessionKey := strings.TrimSpace(shared.StringArg(params, "sessionKey", "main"))
|
||||
artifactScope := ".xworkmate/artifacts/tasks/" + sessionKey + "/" + runID
|
||||
_ = conn.WriteJSON(map[string]any{
|
||||
"type": "res",
|
||||
"id": id,
|
||||
"ok": true,
|
||||
"payload": map[string]any{
|
||||
"runId": runID,
|
||||
"sessionKey": sessionKey,
|
||||
"remoteWorkingDirectory": "/remote/openclaw/workspace",
|
||||
"remoteWorkspaceRefKind": "remotePath",
|
||||
"artifactScope": artifactScope,
|
||||
"scopeKind": "task",
|
||||
"artifactDirectory": "/remote/openclaw/workspace/" + artifactScope,
|
||||
"relativeArtifactDirectory": artifactScope,
|
||||
"warnings": []any{},
|
||||
},
|
||||
})
|
||||
case "agent.wait":
|
||||
fake.agentWaitCount.Add(1)
|
||||
params := shared.AsMap(frame["params"])
|
||||
@ -1323,24 +1408,32 @@ func newAcpFakeOpenClawGateway(t *testing.T) *acpFakeOpenClawGateway {
|
||||
}
|
||||
params := shared.AsMap(frame["params"])
|
||||
runID := strings.TrimSpace(shared.StringArg(params, "runId", "fake-run"))
|
||||
artifactScope := strings.TrimSpace(shared.StringArg(params, "artifactScope", ""))
|
||||
payload := map[string]any{
|
||||
"runId": runID,
|
||||
"sessionKey": strings.TrimSpace(shared.StringArg(params, "sessionKey", "")),
|
||||
"remoteWorkingDirectory": "/remote/openclaw/workspace",
|
||||
"remoteWorkspaceRefKind": "remotePath",
|
||||
"scopeKind": "workspace",
|
||||
"artifacts": []any{},
|
||||
"warnings": []any{},
|
||||
}
|
||||
if artifactScope != "" {
|
||||
payload["artifactScope"] = artifactScope
|
||||
payload["scopeKind"] = "task"
|
||||
}
|
||||
if strings.Contains(fake.runMessage(runID), "make artifact") {
|
||||
payload["artifacts"] = []any{
|
||||
map[string]any{
|
||||
"relativePath": "reports/final.md",
|
||||
"label": "final.md",
|
||||
"contentType": "text/markdown",
|
||||
"sizeBytes": 12,
|
||||
"sha256": "fake-sha256",
|
||||
"encoding": "base64",
|
||||
"content": "ZmluYWwgcmVwb3J0",
|
||||
"relativePath": "reports/final.md",
|
||||
"label": "final.md",
|
||||
"contentType": "text/markdown",
|
||||
"sizeBytes": 12,
|
||||
"sha256": "fake-sha256",
|
||||
"artifactScope": artifactScope,
|
||||
"scopeKind": "task",
|
||||
"encoding": "base64",
|
||||
"content": "ZmluYWwgcmVwb3J0",
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -1354,6 +1447,7 @@ func newAcpFakeOpenClawGateway(t *testing.T) *acpFakeOpenClawGateway {
|
||||
fake.artifactReadCount.Add(1)
|
||||
params := shared.AsMap(frame["params"])
|
||||
relativePath := strings.TrimSpace(shared.StringArg(params, "relativePath", ""))
|
||||
artifactScope := strings.TrimSpace(shared.StringArg(params, "artifactScope", ""))
|
||||
if relativePath != "reports/final.md" {
|
||||
_ = conn.WriteJSON(map[string]any{
|
||||
"type": "res",
|
||||
@ -1377,15 +1471,19 @@ func newAcpFakeOpenClawGateway(t *testing.T) *acpFakeOpenClawGateway {
|
||||
"sessionKey": strings.TrimSpace(shared.StringArg(params, "sessionKey", "")),
|
||||
"remoteWorkingDirectory": "/remote/openclaw/workspace",
|
||||
"remoteWorkspaceRefKind": "remotePath",
|
||||
"artifactScope": artifactScope,
|
||||
"scopeKind": "task",
|
||||
"artifacts": []any{
|
||||
map[string]any{
|
||||
"relativePath": "reports/final.md",
|
||||
"label": "final.md",
|
||||
"contentType": "text/markdown",
|
||||
"sizeBytes": len(content),
|
||||
"sha256": hex.EncodeToString(sum[:]),
|
||||
"encoding": "base64",
|
||||
"content": base64.StdEncoding.EncodeToString(content),
|
||||
"relativePath": "reports/final.md",
|
||||
"label": "final.md",
|
||||
"contentType": "text/markdown",
|
||||
"sizeBytes": len(content),
|
||||
"sha256": hex.EncodeToString(sum[:]),
|
||||
"artifactScope": artifactScope,
|
||||
"scopeKind": "task",
|
||||
"encoding": "base64",
|
||||
"content": base64.StdEncoding.EncodeToString(content),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Loading…
Reference in New Issue
Block a user