File size: 8,486 Bytes
e30ad14
1949064
 
 
e30ad14
 
1949064
 
e30ad14
 
1949064
 
e30ad14
 
1949064
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
---
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
```