197 lines
8.1 KiB
Python
197 lines
8.1 KiB
Python
"""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()
|