--- title: testzerogpu emoji: 🖥️ colorFrom: gray colorTo: blue sdk: gradio sdk_version: 6.25.0 python_version: 3.12 app_file: app.py pinned: false license: apache-2.0 short_description: ZeroGPU web terminal with OpenCode --- ZeroGPU web terminal. UI 與 `HelloSun/CPU_TERMINAL_00` 一致(多頁終端機、檔案瀏覽器、 下載/上傳、複製/貼上、狀態列)。 OpenCode 在啟動時安裝,並重設使用者層級設定為預設值。 ## 檔案 - `app.py` — FastAPI + xterm.js 終端機、檔案瀏覽器、`/ws` session 管理與固定 LXDE 桌面 - `packages.txt` — 系統套件(`tmux`、`xvfb`、`x11vnc`、`lxde-core`、`xterm`、`novnc`、 `x11-utils`,以及 Playwright Chromium 所需的系統函式庫與字型) - `requirements.txt` — Python 相依套件(含 `playwright`) ## 終端機分頁(全部共用、全部長存) - **每個分頁 = 一個 tmux session**:真正共用的 shell 在 tmux 裡,每個瀏覽器分頁只是 一條 tmux client attach。因此不同裝置可以有不同終端機大小,tmux 會針對每個 client 重畫畫面: - window 大小由 tmux 依**所有目前附掛的 client 自動計算(`window-size smallest`)**, 取最小的那台,並自動扣掉狀態列。window 一定放得進每台裝置的可視區域,因此不會有 錯位、殘影或亂碼。較大的裝置會看到 tmux 以 `·` 填滿、外框標出 window 範圍。 - 輸入、重連都**不會**改動 window 大小(舊版每次按鍵都會切換尺寸,多裝置會互相拉扯)。 手機/小視窗離線後,window 會自動放大回剩下那台的尺寸。 - 每條連線在 URL 帶上自己的 `cols`/`rows`,第一次重畫就是正確大小。 - 前端只有**可見的分頁**會保持連線;切走的分頁立即 detach,不以錯誤尺寸拖累其他裝置。 - 每次(重新)附掛,前端都會 `term.reset()` 清空本機畫面後再接收 tmux 的完整重畫, 重連或重新整理不會把新舊畫面疊在一起。 - 多位元組字元(中文)用增量 UTF-8 解碼,不會在切斷處產生亂碼。 - 斷線只會 detach 該 client,`opencode` 與所有程序都留在 tmux session 裡。 - **分頁清單由伺服器統一管理**:`GET /api/tabs` 回傳目前所有共用分頁,所有裝置看到的 分頁完全相同。按 `+` 開的新分頁(`POST /api/tabs`)會出現在每一台裝置上。 - **只有明確關閉分頁**(`DELETE /api/tabs/{id}`,前端會先確認)才會 kill 該 tmux session,而且所有裝置會一起關閉該分頁;至少會保留一頁。 - 分頁清單會落地到 `~/.webterm_tabs.json`;只要容器還活著,tmux session 也能跨 app 行程重啟接回。Space 重建(整個容器重來)時仍會失去程序,這是平台限制。 - 分頁數量上限可用 Space 變數 `WEBTERM_MAX_TABS` 覆寫(預設 12)。 - 除錯用端點:`GET /api/tabs`、`GET /api/sessions`(含 tmux 是否可用、每個 attach 的 尺寸與訂閱數)。 `installpi.sh` 已從 Space 流程中移除(不再自動執行),檔案本身保留在 repo 作為紀錄, 需要時手動執行。 ## 固定桌面分頁(Xvfb + LXDE + x11vnc + noVNC) 終端機分頁之外,最左側有一個 **固定** 的「桌面 1(LXDE,共用)」分頁: - 伺服器永遠維持 **恰好一個** `Xvfb`(`:99`)+ 一個 LXDE session + 一個 `x11vnc` (只監聽 `127.0.0.1:5900`)。看門狗每 3 秒檢查,任何一個程序死掉會自動補回, 重複的程序會被清掉。 - 桌面裡永遠有一個標題為「共用終端機」的 xterm;把它關掉後看門狗會自動再開一個, 所以固定分頁裡永遠有 1 個可用的圖形終端機視窗(使用者自己另開的终端機不受影響)。 - 容器內沒有 systemd-logind session,因此會自動停用 `lxpolkit` 與 `light-locker` autostart,避免桌面啟動時跳出「No session for pid …」的 Error 對話框。 - **剪貼簿同步(固定開啟)**:桌面裡的 CLIPBOARD/PRIMARY 永遠與瀏覽器所在系統的 剪貼簿雙向同步;右上角「剪貼簿同步」按鈕可隨時手動再同步一次(沒有關閉選項)。 在桌面內按 Ctrl+V (或 Cmd+V)會先把本機剪貼簿送進 X11 再貼上。底層由 `autocutsel` 將 x11vnc 使用的 CUT_BUFFER0 與 X11 selection 對齊,xterm 也設定為選取即同步 CLIPBOARD。 瀏覽器基於安全限制,頁面需要保持在前景;若系統阻擋寫入,點一下桌面會重試。 - 瀏覽器是透過 `/vnc` WebSocket 橋接到同一個桌面,所有裝置看到完全相同、可同時操作的 LXDE 畫面;關掉瀏覽器分頁不會終止桌面,重新整理或重新連線都會接回原畫面。 - 這個分頁是固定的:按它的 `×` 不會刪除分頁,而是重新啟動一份新的桌面 (`POST /api/desktop/restart`),永遠維持一個。 - 終端機分頁也支援系統剪貼簿:選取後按 **Ctrl+Shift+C**(或 Ctrl+Insert)複製, **Ctrl+V**、**Ctrl+Shift+V**(或 Shift+Insert)直接貼上。貼上走 xterm.js 的 bracketed paste,與一般終端機一致。 - 檔案瀏覽器可瀏覽/下載伺服器上的**任何路徑**(含 `/tmp`、`/`),不再有白名單限制; 選取資料夾後下載會即時串流打包成 `.tar.gz`,不會一次載入記憶體。 - 桌面分頁的「上傳」會把檔案存到 `~/Desktop`(同名檔案自動加序號),上傳後可直接在 LXDE 桌面看到。 - 除錯端點:`GET /api/desktop/status`、`POST /api/desktop/restart`(程序 pid、VNC port、 健康狀態);桌面 log 在 `/tmp/webterm-desktop.log`。 - 可用 Space 變數覆寫:`DESKTOP_DISPLAY`(預設 `:99`)、`DESKTOP_GEOMETRY` (預設 `1440x900x24`)、`DESKTOP_VNC_PORT`(預設 `5900`)、`DESKTOP_NOVNC_ROOT` (預設 `/usr/share/novnc`)。noVNC 靜態檔由 `/novnc/*` 提供,缺少時前端自動改用 CDN。 ## Playwright + Chromium(桌面內使用) Space 啟動後會在背景自動安裝 Playwright 的 Chromium(下載到 `~/.cache/ms-playwright`,系統依賴已由 `packages.txt` 預裝),完成後: - 共用 LXDE 桌面會出現 **Playwright Chromium** 圖示,點兩下即可在桌面內開瀏覽器 (以 root 執行,已自動加上 `--no-sandbox --disable-dev-shm-usage`)。 - 也可直接執行啟動器:`~/.local/bin/playwright-chromium`。 - 在終端機用 Python 操作時,記得 Chromium 需要無沙箱參數: ```python from playwright.sync_api import sync_playwright with sync_playwright() as p: browser = p.chromium.launch(args=["--no-sandbox", "--disable-dev-shm-usage"]) page = browser.new_page() page.goto("https://example.com") print(page.title()) browser.close() ``` - 若要開在共用桌面(看的到視窗),把 `headless` 設為 `False` 並使用 `DISPLAY=:99`; Playwright 會自動沿用 `PLAYWRIGHT_BROWSERS_PATH`。 - 除錯端點:`GET /api/playwright/status`(是否就緒、Chromium 路徑、最後錯誤)、 `POST /api/playwright/install`(手動觸發重裝)。安裝 log 在 `/tmp/webterm-playwright.log`。 - 手動更新 Chromium:`python3 -m playwright install chromium`。 ## 記憶體守衛(memguard) 每次 Space 啟動時,`app.py` 的 `install_memguard()` 會自動: - 把 repo 的 `memguard.py` 複製到 `~/memkill.py` 並啟動它 - `opencode` 本體 + 它啟動的所有子行程合計 RSS > **80 GiB**(單一行程 > **64 GiB**)→ 對記憶體最肥的那個子行程樹送 `SIGKILL` - **只砍子行程,opencode 本體永不砍** - 被砍的指令與峰值寫入 `/tmp/memkill.last.json`、歷史 `/tmp/memkill.jsonl`、log `/var/log/memkill.log` - 把 repo 的 `AGENTS.md` 複製到 `~/.config/opencode/AGENTS.md` - opencode 每輪開工都會讀:被砍後**不准用同一種做法重跑**,必須改用串流/分頁/分段落盤/降批次等低記憶體做法,改不過來就停下來問使用者 - 在 `~/.bashrc` 加冪等自動啟動,新開的終端機也一定有守衛 上限可用 Space 變數覆寫:`MEMGUARD_TOTAL_GIB`、`MEMGUARD_PER_PROC_GIB`。 使用者在終端機檢查: ```bash pgrep -af 'memkill[.]py' # 守衛是否活著 tail -f /var/log/memkill.log # 每 2 秒的用量 cat /tmp/memkill.last.json # 上次被砍的是哪條指令、峰值幾 GiB ```