Merge pull request #8 from ai-workspace-lab/release/v1.1.4
Release/v1.1.4
This commit is contained in:
commit
e6ffdf3177
101
.github/workflows/runtime-release.yml
vendored
Normal file
101
.github/workflows/runtime-release.yml
vendored
Normal 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
|
||||
@ -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", "")),
|
||||
})
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user