Merge pull request #8 from ai-workspace-lab/release/v1.1.4

Release/v1.1.4
This commit is contained in:
Haitao Pan 2026-06-15 22:03:40 +08:00 committed by GitHub
commit e6ffdf3177
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 300 additions and 20 deletions

101
.github/workflows/runtime-release.yml vendored Normal file
View File

@ -0,0 +1,101 @@
name: Build XWorkmate Bridge Runtime Release
on:
push:
branches: [main, release/**]
paths:
- "**/*.go"
- go.mod
- go.sum
- .github/workflows/runtime-release.yml
workflow_dispatch:
permissions:
contents: write
concurrency:
group: xworkmate-bridge-runtime-release-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
name: Build linux-${{ matrix.arch }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
arch: [amd64, arm64]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
cache: true
- name: Test
run: go test ./...
- name: Build runtime asset
env:
TARGET_ARCH: ${{ matrix.arch }}
run: |
set -euo pipefail
root="dist/runtime/xworkmate-bridge"
mkdir -p "${root}/bin" dist/assets
CGO_ENABLED=0 GOOS=linux GOARCH="${TARGET_ARCH}" \
go build -buildvcs=false -trimpath \
-ldflags "-X main.buildCommit=${GITHUB_SHA}" \
-o "${root}/bin/xworkmate-go-core" .
cat > "${root}/manifest.json" <<JSON
{
"component": "xworkmate-bridge",
"commit": "${GITHUB_SHA}",
"os": "linux",
"arch": "${TARGET_ARCH}",
"binary": "bin/xworkmate-go-core"
}
JSON
tar -czf "dist/assets/xworkmate-bridge-linux-${TARGET_ARCH}.tar.gz" \
-C dist/runtime xworkmate-bridge
(
cd dist/assets
sha256sum -- ./*.tar.gz | sed 's# \./# #' > "SHA256SUMS-${TARGET_ARCH}"
)
- uses: actions/upload-artifact@v4
with:
name: xworkmate-bridge-linux-${{ matrix.arch }}
path: |
dist/assets/*.tar.gz
dist/assets/SHA256SUMS-*
if-no-files-found: error
publish:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
pattern: xworkmate-bridge-linux-*
path: dist
merge-multiple: true
- name: Publish assets
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
tag="runtime-${GITHUB_SHA::12}"
cat dist/SHA256SUMS-* | sort -u > dist/SHA256SUMS
rm -f dist/SHA256SUMS-*
if gh release view "${tag}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then
gh release upload "${tag}" dist/*.tar.gz dist/SHA256SUMS \
--repo "${GITHUB_REPOSITORY}" --clobber
else
gh release create "${tag}" dist/*.tar.gz dist/SHA256SUMS \
--repo "${GITHUB_REPOSITORY}" \
--target "${GITHUB_SHA}" \
--title "XWorkmate Bridge runtime ${GITHUB_SHA::12}" \
--notes "Prebuilt Linux bridge binaries. No target-host Go build is required."
fi

View File

@ -894,7 +894,6 @@ func openClawChatSendParamsWithSessionKey(
}
attachments = append(attachments, inlineAttachments...)
if len(attachments) > 0 {
chatParams["attachments"] = attachments
chatParams["message"] = shared.AugmentPromptWithAttachments(
message,
map[string]any{"attachments": attachments},
@ -1026,17 +1025,28 @@ func openClawNonEmptyPathAttachments(params map[string]any) []any {
if len(rawAttachments) == 0 {
return nil
}
inlineAttachmentNames := map[string]bool{}
for _, raw := range shared.ListArg(params, "inlineAttachments") {
name := strings.TrimSpace(shared.StringArg(shared.AsMap(raw), "name", ""))
if name != "" {
inlineAttachmentNames[name] = true
}
}
attachments := make([]any, 0, len(rawAttachments))
for _, raw := range rawAttachments {
attachment := shared.AsMap(raw)
if len(attachment) == 0 {
continue
}
name := strings.TrimSpace(shared.StringArg(attachment, "name", "attachment"))
if inlineAttachmentNames[name] {
continue
}
if strings.TrimSpace(shared.StringArg(attachment, "path", "")) == "" {
continue
}
attachments = append(attachments, map[string]any{
"name": strings.TrimSpace(shared.StringArg(attachment, "name", "attachment")),
"name": name,
"description": strings.TrimSpace(shared.StringArg(attachment, "description", "")),
"path": strings.TrimSpace(shared.StringArg(attachment, "path", "")),
})

View File

@ -613,6 +613,75 @@ func TestExecuteSessionTaskGatewayAutoConnectsLocalOpenClaw(t *testing.T) {
}
}
func TestExecuteSessionTaskOpenClawVideoWithInlineImagesReturnsArtifact(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-video-attachments",
"threadId": "thread-video-attachments",
"taskPrompt": "制作视频:使用附件图片生成 final.mp4 视频制品",
"workingDirectory": t.TempDir(),
"attachments": []any{
map[string]any{"name": "01-frame.png", "description": "image/png", "path": ""},
map[string]any{"name": "02-frame.png", "description": "image/png", "path": ""},
},
"inlineAttachments": []any{
map[string]any{
"name": "01-frame.png",
"mimeType": "image/png",
"content": base64.StdEncoding.EncodeToString([]byte("image-1")),
},
map[string]any{
"name": "02-frame.png",
"mimeType": "image/png",
"content": base64.StdEncoding.EncodeToString([]byte("image-2")),
},
},
"routing": map[string]any{
"routingMode": "explicit",
"explicitExecutionTarget": "gateway",
"preferredGatewayProviderId": "openclaw",
},
},
},
})
if rpcErr != nil {
t.Fatalf("expected gateway response, got rpc error: %#v", rpcErr)
}
chatParams := gateway.LastChatSendParams()
if _, ok := chatParams["attachments"]; ok {
t.Fatalf("chat.send params must not forward attachments; use message paths instead, got %#v", chatParams)
}
if message := shared.StringArg(chatParams, "message", ""); !strings.Contains(message, "01-frame.png") || !strings.Contains(message, "02-frame.png") {
t.Fatalf("expected chat.send message to include materialized image paths, got %q", message)
}
artifacts := shared.ListArg(response, "artifacts")
if len(artifacts) == 0 {
t.Fatalf("expected video artifact output, got %#v", response)
}
foundVideo := false
for _, raw := range artifacts {
artifact := shared.AsMap(raw)
if shared.StringArg(artifact, "contentType", "") == "video/mp4" ||
strings.HasSuffix(strings.ToLower(shared.StringArg(artifact, "relativePath", "")), ".mp4") {
foundVideo = true
break
}
}
if !foundVideo {
t.Fatalf("expected mp4 artifact output, got %#v", artifacts)
}
}
func TestOpenClawAgentWaitTimeoutAdaptsToVideoWork(t *testing.T) {
base := openClawAgentWaitTimeout(
map[string]any{"taskPrompt": "say pong"},
@ -2454,18 +2523,11 @@ func TestOpenClawChatSendParamsMaterializesInlineAttachments(t *testing.T) {
t.Fatalf("expected chat params, got rpc error: %#v", rpcErr)
}
attachments := shared.ListArg(chatParams, "attachments")
if len(attachments) != 1 {
t.Fatalf("expected one materialized attachment, got %#v", attachments)
}
attachment := shared.AsMap(attachments[0])
if got := shared.StringArg(attachment, "name", ""); got != "prompt.png" {
t.Fatalf("expected materialized attachment name, got %#v", attachment)
}
path := shared.StringArg(attachment, "path", "")
if path == "" {
t.Fatalf("expected materialized attachment path, got %#v", attachment)
if _, ok := chatParams["attachments"]; ok {
t.Fatalf("chat.send params must not forward attachments; use message paths instead, got %#v", chatParams)
}
attachmentDirectory := filepath.Join(workspace, ".xworkmate", "attachments", "turn-inline-attachments")
path := filepath.Join(attachmentDirectory, "01-prompt.png")
content, err := os.ReadFile(path)
if err != nil {
t.Fatalf("expected materialized file to exist: %v", err)
@ -2481,6 +2543,74 @@ func TestOpenClawChatSendParamsMaterializesInlineAttachments(t *testing.T) {
}
}
func TestOpenClawChatSendParamsVideoInlineImagesUsePromptPathsOnly(t *testing.T) {
workspace := t.TempDir()
chatParams, rpcErr := openClawChatSendParams(map[string]any{
"threadId": "thread-video-attachments",
"taskPrompt": "制作视频",
"workingDirectory": workspace,
"attachments": []any{
map[string]any{
"name": "01-single-machine.png",
"description": "image/png",
"path": "/Users/shenlan/Pictures/01-single-machine.png",
},
},
"inlineAttachments": []any{
map[string]any{
"name": "01-single-machine.png",
"mimeType": "image/png",
"content": base64.StdEncoding.EncodeToString([]byte("image-1")),
},
map[string]any{
"name": "02-lan-era.png",
"mimeType": "image/png",
"content": base64.StdEncoding.EncodeToString([]byte("image-2")),
},
map[string]any{
"name": "03-web-era.png",
"mimeType": "image/png",
"content": base64.StdEncoding.EncodeToString([]byte("image-3")),
},
},
}, "turn-video-inline-images")
if rpcErr != nil {
t.Fatalf("expected chat params, got rpc error: %#v", rpcErr)
}
if _, ok := chatParams["attachments"]; ok {
t.Fatalf("chat.send params must not forward attachments; use message paths instead, got %#v", chatParams)
}
if _, ok := chatParams["inlineAttachments"]; ok {
t.Fatalf("chat.send params must not forward raw inlineAttachments, got %#v", chatParams)
}
message := shared.StringArg(chatParams, "message", "")
for index, name := range []string{"01-single-machine.png", "02-lan-era.png", "03-web-era.png"} {
path := filepath.Join(
workspace,
".xworkmate",
"attachments",
"turn-video-inline-images",
fmt.Sprintf("%02d-%s", index+1, name),
)
content, err := os.ReadFile(path)
if err != nil {
t.Fatalf("expected materialized image %s to exist: %v", path, err)
}
if got := string(content); got != fmt.Sprintf("image-%d", index+1) {
t.Fatalf("expected materialized image content, got %q", got)
}
if !strings.Contains(message, path) {
t.Fatalf("expected message to include materialized image path %q, got %q", path, message)
}
}
if !strings.Contains(message, "制作视频") {
t.Fatalf("expected message to preserve video prompt, got %q", message)
}
if strings.Contains(message, "/Users/shenlan/Pictures/01-single-machine.png") {
t.Fatalf("OpenClaw message must use materialized remote paths, got %q", message)
}
}
func TestOpenClawChatSendParamsMaterializesInlineAttachmentsInRemoteHint(t *testing.T) {
remoteWorkspace := t.TempDir()
chatParams, rpcErr := openClawChatSendParams(map[string]any{
@ -2500,11 +2630,10 @@ func TestOpenClawChatSendParamsMaterializesInlineAttachmentsInRemoteHint(t *test
t.Fatalf("expected chat params, got rpc error: %#v", rpcErr)
}
attachments := shared.ListArg(chatParams, "attachments")
if len(attachments) != 1 {
t.Fatalf("expected one materialized attachment, got %#v", attachments)
if _, ok := chatParams["attachments"]; ok {
t.Fatalf("chat.send params must not forward attachments; use message paths instead, got %#v", chatParams)
}
path := shared.StringArg(shared.AsMap(attachments[0]), "path", "")
path := filepath.Join(remoteWorkspace, ".xworkmate", "attachments", "turn-remote-attachments", "01-note.txt")
if !strings.HasPrefix(path, remoteWorkspace) {
t.Fatalf("expected attachment under remote workspace %q, got %q", remoteWorkspace, path)
}
@ -2557,7 +2686,10 @@ func TestOpenClawChatSendParamsMapsOwnerScopedWorkspaceToWritableRoot(t *testing
if !strings.Contains(message, writableWorkspace) {
t.Fatalf("message should reference writable workspace %q, got %q", writableWorkspace, message)
}
path := shared.StringArg(shared.AsMap(shared.ListArg(chatParams, "attachments")[0]), "path", "")
if _, ok := chatParams["attachments"]; ok {
t.Fatalf("chat.send params must not forward attachments; use message paths instead, got %#v", chatParams)
}
path := filepath.Join(writableWorkspace, ".xworkmate", "attachments", "turn-owner-workspace", "01-note.txt")
if !strings.HasPrefix(path, writableWorkspace) {
t.Fatalf("expected materialized attachment under writable workspace %q, got %q", writableWorkspace, path)
}

View File

@ -16,6 +16,7 @@ import (
const (
desktopReliableInputChannelLabel = "input"
desktopMoveInputChannelLabel = "input-move"
desktopICEGatheringTimeout = 10 * time.Second
)
type WebRTCServer struct {
@ -230,7 +231,9 @@ func (w *WebRTCServer) ProcessOffer(sdpOffer string) (string, error) {
return "", fmt.Errorf("failed to set local description: %w", err)
}
<-gatherComplete
if err := waitForICEGatheringComplete(gatherComplete, desktopICEGatheringTimeout); err != nil {
return "", err
}
localDesc := pc.LocalDescription()
if localDesc == nil {
@ -240,6 +243,15 @@ func (w *WebRTCServer) ProcessOffer(sdpOffer string) (string, error) {
return localDesc.SDP, nil
}
func waitForICEGatheringComplete(done <-chan struct{}, timeout time.Duration) error {
select {
case <-done:
return nil
case <-time.After(timeout):
return fmt.Errorf("timed out waiting for ICE gathering after %s", timeout)
}
}
// AddICECandidate adds a remote ICE candidate
func (w *WebRTCServer) AddICECandidate(candidate webrtc.ICECandidateInit) error {
w.mu.Lock()

View File

@ -1,6 +1,9 @@
package desktop
import "testing"
import (
"testing"
"time"
)
func TestIsDesktopInputDataChannelLabelAllowsReliableAndMoveChannels(t *testing.T) {
if !isDesktopInputDataChannelLabel(desktopReliableInputChannelLabel) {
@ -13,3 +16,25 @@ func TestIsDesktopInputDataChannelLabelAllowsReliableAndMoveChannels(t *testing.
t.Fatalf("expected unrelated data channel label to be ignored")
}
}
func TestWaitForICEGatheringCompleteReturnsWhenDoneCloses(t *testing.T) {
done := make(chan struct{})
close(done)
if err := waitForICEGatheringComplete(done, time.Second); err != nil {
t.Fatalf("expected closed gathering channel to succeed: %v", err)
}
}
func TestWaitForICEGatheringCompleteTimesOut(t *testing.T) {
done := make(chan struct{})
start := time.Now()
err := waitForICEGatheringComplete(done, 10*time.Millisecond)
if err == nil {
t.Fatalf("expected timeout error")
}
if elapsed := time.Since(start); elapsed > time.Second {
t.Fatalf("timeout helper waited too long: %s", elapsed)
}
}