從 local-tool 複製出獨立的「visionA Agent」桌面應用(A3 純橋樑: tunnel client + 配對 UI + 設定,不開 HTTP port、不做本機裝置/推論 UI)。 Bundle ID 與 local-tool 不同(com.innovedus.visiona-agent vs visiona-local), 雙 app 可共存。fork 後不主動 sync,需要時手動 cherry-pick。 Backend / Wails Go(AB1-AB13): - internal/tunnel:6 狀態機(Idle/Connecting/Connected/Reconnecting/Failed/Stopped) + Pair/Unpair/Reconnect/Disconnect binding + ClientHooks event - internal/auth:encrypted file token store(AES-GCM + scrypt + machineID fallback salt + 13 tests) - internal/config:YAML validation + atomic write + 11 tests - internal/log:ring buffer + ExportLog 升級 zip - visionA-backend /api/pairing/exchange:SessionTokenStore + 17 new tests - 三平台 build 驗證(macOS DMG 160 MB / Windows EXE / Linux AppImage) - end-to-end 5 milestone 全綠(pairing → tunnel → forward → reuse 防護 → tunnel drop failover) Frontend / Next.js(AF1-AF7,沿用 visionA-frontend 基礎): - AppShell + Header + TabNav(StatusView / PairView / SettingsView 三 tab) - ConnectionStatusBadge 5 種狀態 - TokenInput regex 驗證 + 7 種錯誤 + 0.5s auto-switch 到狀態頁 - 設定頁 4 區塊(含重新配對 AlertDialog) - agent-api.ts 封裝 Wails bindings(mock/real 雙實作)+ 90 tests Phase 0.7 review-driven fix(Round 2): - A1 Session fixation 防護(RotateSessionID) - A3 mock pairing 預設改 false(必須明確 opt-in)+ startup log - A4 Pair 失敗後 state 清理矩陣(exchange/Save/Start fail 各自終態) - A5 Pair/Unpair/Reconnect lifecycleMu + 50 goroutine race test - F1 重新配對次按鈕 / F2 PairView Esc cancel / F3 Wails BrowserOpenURL / F4 Settings draft 持久 + 未儲存 badge 驗證:agent backend go test -race -count=3 ./... 4 packages 全綠 / agent frontend pnpm test 119 tests 全綠 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
86 lines
2.6 KiB
Python
86 lines
2.6 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
生成 DMG 美化背景圖(640×400),對齊 Wails 控制台深色 splash 風格。
|
||
|
||
用法:
|
||
python3 installer/macos/make-dmg-background.py
|
||
|
||
輸出:
|
||
installer/macos/background.png (1x, 640×400)
|
||
installer/macos/background@2x.png (2x, 1280×800 Retina)
|
||
|
||
create-dmg 會自動挑 @2x 版本用於 Retina 螢幕。
|
||
"""
|
||
from pathlib import Path
|
||
|
||
from PIL import Image, ImageDraw, ImageFont
|
||
|
||
OUT_DIR = Path(__file__).resolve().parent
|
||
W, H = 640, 400
|
||
|
||
BG_TOP = (17, 24, 39)
|
||
BG_BOTTOM = (11, 15, 25)
|
||
TEXT = (229, 231, 235)
|
||
MUTED = (148, 163, 184)
|
||
ACCENT = (56, 189, 248)
|
||
|
||
|
||
def make(scale: int, out_path: Path) -> None:
|
||
w, h = W * scale, H * scale
|
||
img = Image.new("RGB", (w, h), BG_TOP)
|
||
px = img.load()
|
||
for y in range(h):
|
||
t = y / (h - 1)
|
||
r = int(BG_TOP[0] + (BG_BOTTOM[0] - BG_TOP[0]) * t)
|
||
g = int(BG_TOP[1] + (BG_BOTTOM[1] - BG_TOP[1]) * t)
|
||
b = int(BG_TOP[2] + (BG_BOTTOM[2] - BG_TOP[2]) * t)
|
||
for x in range(w):
|
||
px[x, y] = (r, g, b)
|
||
|
||
draw = ImageDraw.Draw(img)
|
||
|
||
try:
|
||
font_title = ImageFont.truetype("/System/Library/Fonts/Helvetica.ttc", 22 * scale)
|
||
font_hint = ImageFont.truetype("/System/Library/Fonts/Helvetica.ttc", 14 * scale)
|
||
except OSError:
|
||
font_title = ImageFont.load_default()
|
||
font_hint = ImageFont.load_default()
|
||
|
||
title = "Drag visionA Agent to Applications"
|
||
tw = draw.textlength(title, font=font_title)
|
||
draw.text(((w - tw) / 2, 28 * scale), title, fill=TEXT, font=font_title)
|
||
|
||
hint = "拖曳圖示到右邊的 Applications 即可安裝"
|
||
hw = draw.textlength(hint, font=font_hint)
|
||
draw.text(((w - hw) / 2, 60 * scale), hint, fill=MUTED, font=font_hint)
|
||
|
||
# 箭頭位置:左右兩個 icon 約在 y=230,中心。icon 本身 128×128,由 create-dmg 擺。
|
||
# create-dmg 預設 app icon x=180, Applications x=460(y=200)。箭頭畫在中間 240-400 區段。
|
||
arrow_y = 200 * scale
|
||
arrow_x1 = 260 * scale
|
||
arrow_x2 = 380 * scale
|
||
line_w = 3 * scale
|
||
draw.line([(arrow_x1, arrow_y), (arrow_x2, arrow_y)], fill=ACCENT, width=line_w)
|
||
# 箭頭頭
|
||
head = 12 * scale
|
||
draw.polygon(
|
||
[
|
||
(arrow_x2, arrow_y),
|
||
(arrow_x2 - head, arrow_y - head // 2 - 2 * scale),
|
||
(arrow_x2 - head, arrow_y + head // 2 + 2 * scale),
|
||
],
|
||
fill=ACCENT,
|
||
)
|
||
|
||
img.save(out_path, "PNG", optimize=True)
|
||
print(f"wrote {out_path} ({out_path.stat().st_size // 1024} KB)")
|
||
|
||
|
||
def main() -> None:
|
||
make(1, OUT_DIR / "background.png")
|
||
make(2, OUT_DIR / "background@2x.png")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|