feat: add Windows computer-use plugin adapter and validation tools
This commit is contained in:
1 parent
6431b68d5b
commit
254ce4ffb3
11 files changed
+700
-2
No files matched your search
@@ -0,0 +1,196 @@
|
||||
"""Build the pinned Windows plugin; never execute npm installation hooks."""
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import tarfile
|
||||
import tempfile
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
INTEGRITY = "KvrauHDzUMAaX/u735OnfQ15JTE38OjUxkt00ebRccKhY/ARkqJVCLUsJqZxxGsTBwjeldUn4VlOm4iPlFV/FA=="
|
||||
SOURCE_COMMIT = "7d8cb3fa5618d1ed8ec71773434ac3677f1922c9"
|
||||
RUNTIME_SHA256 = "e7c6f6cca76d7aa97dbd1c5f77f088a74f596e34cb2556d4270dd05febd20284"
|
||||
|
||||
|
||||
def build_windows(
|
||||
source: Path, go: str, adapter: Path, output: Path
|
||||
) -> dict[str, bytes]:
|
||||
revision = subprocess.check_output(
|
||||
["git", "rev-parse", "HEAD"], cwd=source, text=True
|
||||
).strip()
|
||||
if revision != SOURCE_COMMIT:
|
||||
raise ValueError("上游源码提交不匹配")
|
||||
folder = source / "apps" / "OpenComputerUseWindows"
|
||||
files = {
|
||||
name: (folder / name).read_bytes()
|
||||
for name in ("main.go", "main_test.go", "go.mod", "runtime.ps1")
|
||||
}
|
||||
for name, value in files.items():
|
||||
committed = subprocess.check_output(
|
||||
["git", "show", f"HEAD:apps/OpenComputerUseWindows/{name}"], cwd=source
|
||||
)
|
||||
if value.replace(b"\r\n", b"\n") != committed.replace(b"\r\n", b"\n"):
|
||||
raise ValueError("上游源码含未记录的修改")
|
||||
if hashlib.sha256(files["runtime.ps1"]).hexdigest() != RUNTIME_SHA256:
|
||||
# Accommodate Git's CRLF checkout without accepting a different source.
|
||||
normalized = files["runtime.ps1"].replace(b"\r\n", b"\n")
|
||||
if hashlib.sha256(normalized).hexdigest() != RUNTIME_SHA256:
|
||||
raise ValueError("窗口运行时源码校验失败")
|
||||
runtime = files["runtime.ps1"].decode("utf-8")
|
||||
start = runtime.index("function Capture-WindowPngBase64(")
|
||||
end = runtime.index("function Get-FocusedSummary", start)
|
||||
runtime = (
|
||||
runtime[:start]
|
||||
+ adapter.read_text(encoding="utf-8")
|
||||
+ "\n\n"
|
||||
+ adapter.with_name("window-input.ps1").read_text(encoding="utf-8")
|
||||
+ "\n\n"
|
||||
+ runtime[end:]
|
||||
)
|
||||
marker = "public static class OCUWin32 {"
|
||||
runtime = runtime.replace(
|
||||
marker,
|
||||
marker
|
||||
+ '\n [DllImport("user32.dll")]\n public static extern bool PrintWindow(IntPtr hwnd, IntPtr hdc, uint flags);\n [DllImport("user32.dll")]\n public static extern IntPtr SetThreadDpiAwarenessContext(IntPtr context);\n [DllImport("user32.dll")]\n public static extern IntPtr ChildWindowFromPointEx(IntPtr parent, POINT point, uint flags);',
|
||||
)
|
||||
runtime = runtime.replace(
|
||||
"$WM_SETTEXT = 0x000C",
|
||||
"[void][OCUWin32]::SetThreadDpiAwarenessContext([IntPtr](-4))\n\n$WM_SETTEXT = 0x000C",
|
||||
)
|
||||
mouse_signature = "function Send-MouseClick([IntPtr]$hwnd, [int]$screenX, [int]$screenY, [string]$button, [int]$count) {"
|
||||
runtime = runtime.replace(
|
||||
mouse_signature,
|
||||
mouse_signature + "\n $hwnd = Resolve-InputWindow $hwnd $screenX $screenY",
|
||||
)
|
||||
runtime = runtime.replace(
|
||||
"screenshotPngBase64 = Capture-WindowPngBase64 $bounds",
|
||||
"screenshotPngBase64 = Capture-WindowPngBase64 $bounds ([IntPtr]$process.MainWindowHandle)",
|
||||
)
|
||||
files["runtime.ps1"] = runtime.encode("utf-8")
|
||||
# Advertise only methods this Windows runtime actually implements.
|
||||
main = files["main.go"].decode("utf-8")
|
||||
main = main.replace(
|
||||
'Click implementation: auto (default), accessibility, app_post, sky_click, or global. Accessibility requires element_index. Windows supports app_post through HWND messages and does not currently support sky_click or global.", clickMethodValues)',
|
||||
'Click implementation: auto (default), accessibility or app_post. Accessibility requires element_index.", []string{"auto", "accessibility", "app_post"})',
|
||||
)
|
||||
files["main.go"] = main.encode("utf-8")
|
||||
files["main_test.go"] = files["main_test.go"].replace(
|
||||
b'strings.Join(values, ",") != "auto,accessibility,app_post,sky_click,global"',
|
||||
b'strings.Join(values, ",") != "auto,accessibility,app_post"',
|
||||
)
|
||||
with tempfile.TemporaryDirectory(prefix="artemis-ocu-build-") as temporary:
|
||||
build = Path(temporary)
|
||||
for name, value in files.items():
|
||||
(build / name).write_bytes(value)
|
||||
environment = {
|
||||
**os.environ,
|
||||
"CGO_ENABLED": "0",
|
||||
"GOOS": "windows",
|
||||
"GOARCH": "amd64",
|
||||
"GOTOOLCHAIN": "local",
|
||||
}
|
||||
for command in (
|
||||
[go, "test", "./..."],
|
||||
[
|
||||
go,
|
||||
"build",
|
||||
"-trimpath",
|
||||
"-buildvcs=false",
|
||||
"-ldflags=-s -w -X main.version=1.1.0-artemis.1",
|
||||
"-o",
|
||||
str(output),
|
||||
".",
|
||||
],
|
||||
):
|
||||
result = subprocess.run(
|
||||
command,
|
||||
cwd=build,
|
||||
env=environment,
|
||||
timeout=300,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
||||
)
|
||||
print(result.stdout, end="")
|
||||
if result.returncode:
|
||||
raise RuntimeError(result.stderr or result.stdout or "Go build failed")
|
||||
return files
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("npm_tarball", type=Path, help="open-computer-use-1.1.0.tgz")
|
||||
parser.add_argument(
|
||||
"--upstream-source",
|
||||
type=Path,
|
||||
required=True,
|
||||
help="Pinned upstream Git checkout",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--go", default="go", help="Go 1.22+ executable (build host only)"
|
||||
)
|
||||
parser.add_argument("--output", type=Path, default=ROOT / "artifacts")
|
||||
args = parser.parse_args()
|
||||
data = args.npm_tarball.read_bytes()
|
||||
if base64.b64encode(hashlib.sha512(data).digest()).decode() != INTEGRITY:
|
||||
raise ValueError("上游 npm 1.1.0 安装包完整性校验失败")
|
||||
source = ROOT / "plugins" / "open-computer-use"
|
||||
args.output.mkdir(parents=True, exist_ok=True)
|
||||
target = args.output / "open-computer-use-1.0.0-windows-amd64.zip"
|
||||
executable = args.output / "open-computer-use.exe"
|
||||
sources = build_windows(
|
||||
args.upstream_source, args.go, source / "window-capture.ps1", executable
|
||||
)
|
||||
with (
|
||||
tarfile.open(args.npm_tarball) as npm,
|
||||
zipfile.ZipFile(target, "w", zipfile.ZIP_DEFLATED) as archive,
|
||||
):
|
||||
for name in ("plugin.json", "SKILL.md"):
|
||||
archive.write(source / name, name)
|
||||
for original, destination in (
|
||||
("LICENSE", "LICENSE"),
|
||||
("THIRD_PARTY_NOTICES.md", "THIRD_PARTY_NOTICES.md"),
|
||||
):
|
||||
stream = npm.extractfile("package/" + original)
|
||||
if stream is None:
|
||||
raise ValueError("上游包缺少必需文件")
|
||||
archive.writestr(destination, stream.read())
|
||||
archive.write(executable, "bin/open-computer-use.exe")
|
||||
for name, value in sources.items():
|
||||
archive.writestr("source/" + name, value)
|
||||
archive.write(
|
||||
source / "window-capture.ps1", "source/artemis-window-capture.ps1"
|
||||
)
|
||||
archive.write(source / "window-input.ps1", "source/artemis-window-input.ps1")
|
||||
archive.writestr(
|
||||
"provenance.json",
|
||||
json.dumps(
|
||||
{
|
||||
"npm_package": "open-computer-use@1.1.0",
|
||||
"source": "https://github.com/iFurySt/open-codex-computer-use",
|
||||
"integrity": "sha512-" + INTEGRITY,
|
||||
"source_commit": SOURCE_COMMIT,
|
||||
"adapter": "ARTEMIS window-owned PrintWindow capture; no screen-region fallback",
|
||||
"binary_sha256": hashlib.sha256(
|
||||
executable.read_bytes()
|
||||
).hexdigest(),
|
||||
},
|
||||
indent=2,
|
||||
),
|
||||
)
|
||||
digest = hashlib.sha256(target.read_bytes()).hexdigest()
|
||||
target.with_suffix(".sha256").write_text(
|
||||
f"{digest} {target.name}\n", encoding="utf-8"
|
||||
)
|
||||
print(target)
|
||||
print(digest)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user