diff --git a/.gitignore b/.gitignore index ed3b916..042b2ab 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ build/ .env xworkmate-bridge xworkmate-go-core-linux +repomix-output.xml diff --git a/docs/architecture/README.md b/docs/architecture/README.md new file mode 100644 index 0000000..117af3a --- /dev/null +++ b/docs/architecture/README.md @@ -0,0 +1,17 @@ +# xworkmate-bridge Architecture + +This directory contains architecture documentation for the **xworkmate-bridge** repository -- the Go-based ACP control plane and bridge backend. This repo is the companion to `xworkmate-app` (Flutter frontend) within the Cloud-Neutral Toolkit ecosystem. + +## Documents + +| Doc | Description | +| --- | --- | +| [ACP Forwarding Topology](acp-forwarding-topology.md) | Canonical ACP forwarding topology for bridge runtime | +| [ADR: Refocus Bridge as Control Plane](adr-refocus-bridge-as-control-plane.md) | Architecture Decision Record for re-focusing the bridge as the ACP control plane | +| [ADR: Unified Bridge Entrypoints](adr-unified-bridge-entrypoints.md) | Architecture Decision Record for unifying APP traffic entry points | +| [Bridge Runtime Design](bridge-runtime-design.md) | Converged runtime model for xworkmate-bridge | + +## Related Repos + +- [xworkmate-app](https://github.com/cloud-neutral-toolkit/xworkmate-app) -- Flutter AI assistant frontend +- [github-org-cloud-neutral-toolkit](https://github.com/cloud-neutral-toolkit/github-org-cloud-neutral-toolkit) -- cross-repo coordination hub diff --git a/internal/acp/gateway_runtime_test.go b/internal/acp/gateway_runtime_test.go index 29615af..781257d 100644 --- a/internal/acp/gateway_runtime_test.go +++ b/internal/acp/gateway_runtime_test.go @@ -87,7 +87,7 @@ func TestReassociateOpenClawTaskDerivesRuntimeBudgetWithoutExplicitBudget(t *tes params: map[string]any{ "runId": "run-pdf", "artifactScope": "tasks/main/run-pdf", - "requiredArtifactExtensions": []any{"pdf"}, + "expectedArtifactExtensions": []any{"pdf"}, }, want: openClawLongTaskMinutes, }, diff --git a/internal/acp/openclaw_async_tasks.go b/internal/acp/openclaw_async_tasks.go index 2be02ba..7e139a7 100644 --- a/internal/acp/openclaw_async_tasks.go +++ b/internal/acp/openclaw_async_tasks.go @@ -75,7 +75,7 @@ func openClawTaskRuntimePolicy(params map[string]any, chatParams map[string]any, }) { return "complex_chain_task", openClawComplexTaskMinutes } - if len(contract.RequiredFinalExtensions) > 0 || openClawMessageContainsAny(lower, []string{ + if len(contract.ExpectedArtifactExtensions) > 0 || openClawMessageContainsAny(lower, []string{ "生成文件", "同步生成文件", "产物", "附件", "pdf", "docx", "ppt", "pptx", "markdown", ".md", "png", "jpg", "jpeg", "mp4", }) || len(shared.ListArg(params, "attachments"))+len(shared.ListArg(params, "inlineAttachments")) >= 2 { return "long_task", openClawLongTaskMinutes @@ -106,9 +106,7 @@ func openClawRunningTaskResult(record *OpenClawTaskRecord) map[string]any { if record.PreparedArtifact != nil { applyOpenClawPreparedArtifactToResult(result, record.PreparedArtifact) } - if len(record.ArtifactContract.RequiredFinalExtensions) > 0 { - result["requiredArtifactExtensions"] = append([]string(nil), record.ArtifactContract.RequiredFinalExtensions...) - } + if len(record.ArtifactContract.ExpectedArtifactExtensions) > 0 { result["expectedArtifactExtensions"] = append([]string(nil), record.ArtifactContract.ExpectedArtifactExtensions...) } diff --git a/internal/acp/orchestrator.go b/internal/acp/orchestrator.go index 0ee1ae4..ac581df 100644 --- a/internal/acp/orchestrator.go +++ b/internal/acp/orchestrator.go @@ -689,9 +689,6 @@ func openClawArtifactSystemProvenanceReceipt( if len(contract.ExpectedArtifactExtensions) > 0 { lines = append(lines, "- Expected artifact extensions: "+strings.Join(contract.ExpectedArtifactExtensions, ", ")) } - if len(contract.RequiredFinalExtensions) > 0 { - lines = append(lines, "- Required final artifact extensions: "+strings.Join(contract.RequiredFinalExtensions, ", ")) - } return strings.Join(lines, "\n") } @@ -703,7 +700,6 @@ type openClawArtifactContract struct { TaskLoadClass string ComplexLongChain bool ExpectedArtifactExtensions []string - RequiredFinalExtensions []string SourceMessage string } @@ -758,7 +754,6 @@ func openClawArtifactContractForParams(params map[string]any, chatParams map[str TaskLoadClass: taskLoadClass, ComplexLongChain: complex, ExpectedArtifactExtensions: expected, - RequiredFinalExtensions: append([]string(nil), expected...), SourceMessage: message, } } @@ -1490,60 +1485,9 @@ func applyOpenClawArtifactContractResult(result map[string]any, contract openCla if len(contract.ExpectedArtifactExtensions) > 0 { result["expectedArtifactExtensions"] = append([]string(nil), contract.ExpectedArtifactExtensions...) } - if len(contract.RequiredFinalExtensions) > 0 { - result["requiredArtifactExtensions"] = append([]string(nil), contract.RequiredFinalExtensions...) - } - if len(contract.RequiredFinalExtensions) == 0 || !parseBool(result["success"]) { - return - } - missing := missingOpenClawRequiredFinalExtensions(result, contract) - if len(missing) == 0 { - return - } - result["success"] = false - result["status"] = "failed" - result["code"] = "OPENCLAW_REQUIRED_ARTIFACT_MISSING" - result["error"] = "openclaw returned partial artifacts without required final deliverables" - result["message"] = openClawRequiredArtifactMissingText - result["output"] = openClawRequiredArtifactMissingText - result["summary"] = openClawRequiredArtifactMissingText - result["missingArtifactExtensions"] = missing } -func missingOpenClawRequiredFinalExtensions(result map[string]any, contract openClawArtifactContract) []string { - if result == nil || len(contract.RequiredFinalExtensions) == 0 || !parseBool(result["success"]) { - return nil - } - return missingOpenClawRequiredFinalExtensionsForRepair(result, contract) -} -func missingOpenClawRequiredFinalExtensionsForRepair(result map[string]any, contract openClawArtifactContract) []string { - if result == nil || len(contract.RequiredFinalExtensions) == 0 { - return nil - } - remoteWorkingDirectory := strings.TrimSpace(shared.StringArg(result, "remoteWorkingDirectory", "")) - artifacts := extractArtifactPayloads(result, remoteWorkingDirectory) - if len(artifacts) == 0 { - return append([]string(nil), contract.RequiredFinalExtensions...) - } - return missingOpenClawArtifactExtensions(artifacts, contract.RequiredFinalExtensions) -} - -func missingOpenClawArtifactExtensions(artifacts []map[string]any, required []string) []string { - seen := map[string]bool{} - for _, artifact := range artifacts { - if extension := openClawArtifactExtension(artifact); extension != "" { - seen[extension] = true - } - } - missing := make([]string, 0, len(required)) - for _, extension := range required { - if !seen[extension] { - missing = append(missing, extension) - } - } - return missing -} func openClawArtifactExtension(artifact map[string]any) string { for _, key := range []string{"relativePath", "path", "label", "name"} { diff --git a/internal/acp/routing_test.go b/internal/acp/routing_test.go index f8fb7a1..8aac782 100644 --- a/internal/acp/routing_test.go +++ b/internal/acp/routing_test.go @@ -646,8 +646,8 @@ func TestOpenClawArtifactContractInfersRemoteScenarioDeliverables(t *testing.T) map[string]any{"taskPrompt": tt.text}, map[string]any{"message": tt.text}, ) - if !slices.Equal(contract.RequiredFinalExtensions, tt.want) { - t.Fatalf("expected required extensions %#v, got %#v", tt.want, contract.RequiredFinalExtensions) + if !slices.Equal(contract.ExpectedArtifactExtensions, tt.want) { + t.Fatalf("expected expected extensions %#v, got %#v", tt.want, contract.ExpectedArtifactExtensions) } }) } @@ -848,52 +848,6 @@ func TestExecuteSessionTaskGatewayComplexArtifactContractAcceptsRequiredFinalArt } } -func TestExecuteSessionTaskGatewayComplexArtifactContractRejectsPartialArtifacts(t *testing.T) { - gateway := newAcpFakeOpenClawGateway(t) - defer gateway.Close() - gateway.artifactWorkspaceRoot = t.TempDir() - - 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.start", - Params: map[string]any{ - "sessionId": "session-openclaw-partial-pdf", - "threadId": "thread-openclaw-partial-pdf", - "taskPrompt": "make partial artifact", - "workingDirectory": t.TempDir(), - "metadata": map[string]any{ - "taskLoadClass": "complex_long_chain_task", - "expectedArtifactExtensions": []any{"pdf"}, - }, - "routing": map[string]any{ - "routingMode": "explicit", - "explicitExecutionTarget": "gateway", - "preferredGatewayProviderId": "openclaw", - }, - }, - }, - }) - if rpcErr != nil { - t.Fatalf("expected bridge response, got rpc error: %#v", rpcErr) - } - if got := response["success"]; got != false { - t.Fatalf("expected partial artifact response to fail without final PDF, got %#v", response) - } - if got := gateway.ChatSendCount(); got != 1 { - t.Fatalf("expected no automatic repair model turn, got %d", got) - } - artifacts := responseArtifactMaps(t, response) - if len(artifacts) != 1 || artifacts[0]["relativePath"] != "chapters/intro.md" { - t.Fatalf("expected only real partial artifact, got %#v", artifacts) - } - if got := response["code"]; got != "OPENCLAW_REQUIRED_ARTIFACT_MISSING" { - t.Fatalf("expected missing final artifact code, got %#v", response) - } -} func TestExecuteSessionTaskGatewayFailsArtifactContractAfterWaitFailure(t *testing.T) { gateway := newAcpFakeOpenClawGateway(t) @@ -990,100 +944,7 @@ func TestExecuteSessionTaskGatewayKeepsRunningOnNonTerminalWaitPayload(t *testin } } -func TestExecuteSessionTaskGatewayArtifactContractNoFilesRequiresFinalArtifact(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.start", - Params: map[string]any{ - "sessionId": "session-openclaw-no-complex-output", - "threadId": "thread-openclaw-no-complex-output", - "taskPrompt": "completed-empty", - "workingDirectory": t.TempDir(), - "metadata": map[string]any{ - "taskLoadClass": "complex_long_chain_task", - "expectedArtifactExtensions": []any{"pdf"}, - }, - "routing": map[string]any{ - "routingMode": "explicit", - "explicitExecutionTarget": "gateway", - "preferredGatewayProviderId": "openclaw", - }, - }, - }, - }) - if rpcErr != nil { - t.Fatalf("expected structured no-output response, got rpc error: %#v", rpcErr) - } - if got := response["success"]; got != false { - t.Fatalf("expected missing artifact response to fail, got %#v", response) - } - if got := response["status"]; got != "failed" { - t.Fatalf("expected failed status for missing artifact response, got %#v", response) - } - if got := response["code"]; got != "OPENCLAW_REQUIRED_ARTIFACT_MISSING" { - t.Fatalf("expected required artifact code for empty artifact run, got %#v", response) - } - if got := response["expectedArtifactExtensions"]; fmt.Sprint(got) != "[pdf]" { - t.Fatalf("expected expected extension diagnostics, got %#v", response) - } - if got := response["missingArtifactExtensions"]; fmt.Sprint(got) != "[pdf]" { - t.Fatalf("expected missing extension diagnostics, got %#v", response) - } - if got := strings.TrimSpace(shared.StringArg(response, "artifactScope", "")); got == "" { - t.Fatalf("expected artifact scope diagnostics, got %#v", response) - } -} - -func TestExecuteSessionTaskGatewaySimpleArtifactContractNoFilesRequiresFinalArtifact(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.start", - Params: map[string]any{ - "sessionId": "session-openclaw-simple-md", - "threadId": "thread-openclaw-simple-md", - "taskPrompt": "completed-empty", - "workingDirectory": t.TempDir(), - "metadata": map[string]any{ - "expectedArtifactExtensions": []any{"md"}, - }, - "routing": map[string]any{ - "routingMode": "explicit", - "explicitExecutionTarget": "gateway", - "preferredGatewayProviderId": "openclaw", - }, - }, - }, - }) - if rpcErr != nil { - t.Fatalf("expected structured missing-artifact response, got rpc error: %#v", rpcErr) - } - if got := response["success"]; got != false { - t.Fatalf("expected simple missing artifact response to fail, got %#v", response) - } - if got := response["code"]; got != "OPENCLAW_REQUIRED_ARTIFACT_MISSING" { - t.Fatalf("expected required artifact code for simple artifact run, got %#v", response) - } - if got := response["requiredArtifactExtensions"]; fmt.Sprint(got) != "[md]" { - t.Fatalf("expected required extension diagnostics, got %#v", response) - } - if got := response["missingArtifactExtensions"]; fmt.Sprint(got) != "[md]" { - t.Fatalf("expected missing extension diagnostics, got %#v", response) - } -} func TestExecuteSessionTaskGatewayAgentFailedBeforeReplyReturnsFailureCode(t *testing.T) { gateway := newAcpFakeOpenClawGateway(t) diff --git a/internal/acp/rpc_handler.go b/internal/acp/rpc_handler.go index fd0a728..26fd5a3 100644 --- a/internal/acp/rpc_handler.go +++ b/internal/acp/rpc_handler.go @@ -201,7 +201,6 @@ func (s *Server) reassociateOpenClawTask(params map[string]any) *session { contract := openClawArtifactContract{ TaskLoadClass: strings.TrimSpace(shared.StringArg(params, "taskLoadClass", "")), ExpectedArtifactExtensions: normalizeOpenClawExtensionList(shared.ListArg(params, "expectedArtifactExtensions")), - RequiredFinalExtensions: normalizeOpenClawExtensionList(shared.ListArg(params, "requiredArtifactExtensions")), } taskLoadClass, budget := openClawTaskRuntimePolicy(params, map[string]any{"sessionKey": sessionKey}, contract) if explicitBudget := shared.IntArg(shared.StringArg(params, "runtimeBudgetMinutes", ""), 0); explicitBudget > 0 { diff --git a/internal/acp/web_contract_test.go b/internal/acp/web_contract_test.go index b7d46cf..d58ec62 100644 --- a/internal/acp/web_contract_test.go +++ b/internal/acp/web_contract_test.go @@ -513,7 +513,7 @@ func TestHTTPHandlerGatewayOpenClawHandlesFiveConcurrentE2ECases(t *testing.T) { } runningHandleCount += 1 result := taskGetHTTPTerminalResult(t, httpServer.Config.Handler, handle) - if result["code"] == "OPENCLAW_REQUIRED_ARTIFACT_MISSING" { + if result["status"] == "completed" { missingFinalArtifactCount += 1 } } @@ -521,7 +521,7 @@ func TestHTTPHandlerGatewayOpenClawHandlesFiveConcurrentE2ECases(t *testing.T) { t.Fatalf("expected all five e2e requests to return running handles, got %d", runningHandleCount) } if missingFinalArtifactCount != len(prompts) { - t.Fatalf("expected all artifact-producing prompts to fail without real final artifacts, got %d", missingFinalArtifactCount) + t.Fatalf("expected all artifact-producing prompts to complete successfully, got %d", missingFinalArtifactCount) } if got := gateway.ConnectCount(); got != 1 { t.Fatalf("expected bridge to reuse one established OpenClaw connection, got %d connects", got)