add voice
535
README.md
|
|
@ -1,339 +1,334 @@
|
|||
<div align="center">
|
||||
|
||||
# LazyBoy
|
||||
|
||||

|
||||
### 給 Agent 一台真的電腦
|
||||
|
||||
在瀏覽器裡開 Agent。每個都有自己的 Linux 桌面:開網頁、敲指令、學你示範過的流程。金鑰、模型、檔案都留在你這台機器上。
|
||||
在瀏覽器裡指派工作,讓 AI Agent 在隔離的 Linux 桌面中開網頁、操作應用程式、整理檔案,並在你需要時把控制權交回來。
|
||||
|
||||
---
|
||||
[](#快速開始)
|
||||
[](./Cargo.toml)
|
||||
[](./apps/web/package.json)
|
||||
[](./docker-compose.yml)
|
||||
|
||||
## 需要的硬體
|
||||
[快速開始](#快速開始) · [功能](#核心能力) · [運作流程](#一個任務如何完成) · [系統架構](#系統架構) · [二次開發](#二次開發) · [安全](#安全模型)
|
||||
|
||||
LazyBoy 是本機系統,不是雲端沙盒。機器要跑四件事:**Postgres**、**API(含網頁)**、**supervisor**、以及每個 Agent 的 **Debian 桌面容器**。
|
||||
</div>
|
||||
|
||||
| 項目 | 最低能跑 | 建議(一台桌面常開) |
|
||||
| --- | --- | --- |
|
||||
| 作業系統 | macOS / Linux,已裝 Docker Engine + Compose | 同上,磁碟給 Docker 至少 30 GB |
|
||||
| CPU | 4 核 | 8 核以上。每個桌面預設吃 2 核(`LAZYBOY_COMPUTER_CPUS`) |
|
||||
| 記憶體 | 8 GB(只能開一台、還會卡) | **16 GB**。每個桌面預設 2 GB(`LAZYBOY_COMPUTER_MEMORY_MB`)再加上 API、Postgres、嵌入模型 |
|
||||
| 磁碟 | 約 15 GB(桌面映像 + Postgres) | 家目錄 `data/homes/` 會隨瀏覽器設定檔長大 |
|
||||
| GPU | 不需要 | 模型走網路 API,畫面是 CPU 上的 Xvfb |
|
||||
| 網路 | 第一次建映像、拉套件需要 | 之後離線也能開 UI;聊天要模型金鑰能連外 |
|
||||

|
||||
|
||||
每個 Agent 桌面都會套用 Docker 的 CPU、記憶體與 PID 上限,預設是 2 CPU、2 GB、2048 個 PID。可在 `.env` 調整 `LAZYBOY_COMPUTER_CPUS`、`LAZYBOY_COMPUTER_MEMORY_MB`、`LAZYBOY_COMPUTER_PIDS`。Linux 若安裝 LXCFS,將 `LAZYBOY_LXCFS_ROOT` 指到它的 `/var/lib/lxcfs`,容器內的 `htop`、`free` 等也會顯示 cgroup 配額;macOS 仍會套用配額,但 Docker Desktop 不提供這個 `/proc` 虛擬化。桌面容器不掛主機 Docker socket,也不使用 `privileged`;需要一般管理命令時才在 `.env` 設定 `LAZYBOY_COMPUTER_SUDO=true`,權限只在該 Agent 容器內生效。
|
||||
LazyBoy 是一套本機優先(local-first)的多 Agent 工作空間。每個 Agent 都能使用瀏覽器、終端、檔案系統與桌面應用程式;執行畫面可即時觀看、可以人工接管,也能把示範整理成技能,之後透過排程重複執行。
|
||||
|
||||
預設一個 Team 電腦容器可同時掛最多 **8** 個螢幕(`TEAM_SCREEN_LIMIT`)。再開私人電腦就是再一個容器、再 2 GB。分頁開著時心跳會讓桌面保持熱機;關掉分頁約 10 分鐘後凍結(記憶體還在),約 6 小時後才真正停機。
|
||||
> [!IMPORTANT]
|
||||
> LazyBoy 會把對話、記憶、憑證與瀏覽器設定檔持久化在自己的主機,但使用外部模型時,完成任務所需的提示詞、工具結果或畫面仍可能傳送給你設定的模型供應商。請依資料敏感度選擇供應商與部署方式。
|
||||
|
||||
---
|
||||
## 為什麼是 LazyBoy?
|
||||
|
||||
## 怎麼快速啟動
|
||||
| 一般聊天機器人 | LazyBoy |
|
||||
| --- | --- |
|
||||
| 告訴你怎麼做 | 在隔離桌面中實際操作 |
|
||||
| 看不到執行過程 | 即時 noVNC 畫面與步驟狀態 |
|
||||
| 人與 Agent 搶控制權 | 接管/釋放租約,安全交接 |
|
||||
| 每次重新說明流程 | 示範一次,儲存為可攜技能 |
|
||||
| 一次性對話 | Session、長期記憶、排程與群組 |
|
||||
| 固定模型與工具 | xAI、OpenCode Go、OpenAI 相容端點與 MCP |
|
||||
|
||||
要有 Docker(含 Compose 外掛)和 Make。第一次會編 `lazyboy/computer:local`(Debian + XFCE + Chromium),會比較久。
|
||||
## 快速開始
|
||||
|
||||
### 需求
|
||||
|
||||
- macOS 或 Linux
|
||||
- Docker Engine / Docker Desktop(含 Compose)
|
||||
- Make、Git
|
||||
- 至少 4 核 CPU、8 GB RAM;建議 8 核、16 GB RAM
|
||||
- 一組支援的模型 API 金鑰
|
||||
|
||||
第一次啟動會建置 Debian、XFCE、Chromium 桌面映像,因此會比後續啟動久。
|
||||
|
||||
```bash
|
||||
git clone <your-repository-url>
|
||||
cd LazyBoy
|
||||
|
||||
make env
|
||||
# 在 .env 填 XAI_API_KEY
|
||||
# (也可以之後在「本機工作區 → 設定」接 xAI / OpenCode Go / OpenAI 相容端點)
|
||||
# 編輯 .env,至少填入 XAI_API_KEY、OPENCODE_GO_API_KEY
|
||||
# 或 OPENAI_API_KEY 其中之一
|
||||
|
||||
make up
|
||||
make health
|
||||
```
|
||||
|
||||
打開 [http://127.0.0.1:3101](http://127.0.0.1:3101),用 `.env` 裡的 `LAZYBOY_APP_TOKEN` 登入,建一個 Agent,傳一句話。
|
||||
開啟 [http://127.0.0.1:3101](http://127.0.0.1:3101),使用 `.env` 中的 `LAZYBOY_APP_TOKEN` 登入,建立第一個 Agent,然後直接描述目標。
|
||||
|
||||
沒有 Make:
|
||||
```text
|
||||
打開指定的訓練網站,完成還沒看完的章節,最後整理進度。
|
||||
```
|
||||
|
||||
常用指令:
|
||||
|
||||
| 指令 | 用途 |
|
||||
| --- | --- |
|
||||
| `make up` | 建置並啟動完整堆疊 |
|
||||
| `make health` | 檢查 API 健康狀態 |
|
||||
| `make logs` | 持續查看服務日誌 |
|
||||
| `make ps` | 查看容器狀態 |
|
||||
| `make down` | 停止服務,保留 PostgreSQL 資料 |
|
||||
| `make purge` | 停止服務並刪除 PostgreSQL volume |
|
||||
|
||||
沒有 Make 時,可以從 `.env.example` 建立設定後執行:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# openssl rand -hex 32 → LAZYBOY_APP_TOKEN
|
||||
# openssl rand -hex 32 → SANDBOX_SUPERVISOR_TOKEN
|
||||
# openssl rand -hex 32 → LAZYBOY_VAULT_KEY
|
||||
# 為 LAZYBOY_APP_TOKEN、SANDBOX_SUPERVISOR_TOKEN、
|
||||
# LAZYBOY_VAULT_KEY 各自執行一次 openssl rand -hex 32
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
前端熱重載:`apps/web` 裡 `npm install && npm run dev`,開 [http://127.0.0.1:5173](http://127.0.0.1:5173)。Vite 把 `/api` 和 `/view` 轉到 3101。
|
||||
## 核心能力
|
||||
|
||||
本機 Rust 開發(Postgres 仍在 Docker):
|
||||
| 能力 | 實際行為 |
|
||||
| --- | --- |
|
||||
| 可觀察的電腦操作 | 右側 noVNC 顯示 Agent 的 1280×800 桌面;模型觀察與人類畫面分流 |
|
||||
| 瀏覽器與原生桌面控制 | 網頁透過 CDP 元素操作;原生 UI 使用 AT-SPI,必要時才退回座標 |
|
||||
| 人工接管 | 登入、2FA、驗證碼或敏感步驟可暫停 Agent,由使用者接管後續跑 |
|
||||
| 示範教學 | 記錄語意操作事件並整理成 playbook,不依賴固定像素重播 |
|
||||
| 長期記憶 | PostgreSQL + pgvector + MiniLM;只保存明確要求記住的內容 |
|
||||
| 安全登入 | 每個 Agent 的 AES-256-GCM 憑證庫;只對相符的 HTTPS 網域填入 |
|
||||
| 排程 | 五欄 cron、時區支援、立即試跑;到期任務進入一般 run 佇列 |
|
||||
| 多 Agent 與群組 | Team 共用電腦可分配不同 DISPLAY;私人模式使用獨立容器 |
|
||||
| MCP 與檔案技能 | 支援 stdio、HTTP、SSE MCP,以及 `data/skills/*/SKILL.md` |
|
||||
| 語音通話 | 前端提供可設定的即時語音工作階段 |
|
||||
| 中英介面 | `zh-TW` 與 English 文案共用完整 key 契約 |
|
||||
|
||||
```bash
|
||||
make dev # 準備 .env、Postgres、桌面映像
|
||||
make dev-supervisor # 終端 1
|
||||
make dev-api # 終端 2
|
||||
## 一個任務如何完成
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
U["你<br/>文字・附件・語音"] --> W["React 工作空間<br/>對話・電腦・記憶"]
|
||||
W -->|HTTP / WebSocket| A["Rust API + Agent<br/>模型・工具・技能"]
|
||||
A -->|受控工具呼叫| S["Supervisor<br/>資源與生命週期"]
|
||||
S -->|啟動 / 暫停 / 恢復| C["隔離 Linux 電腦<br/>Chromium・XFCE・檔案"]
|
||||
C -->|畫面與事件| W
|
||||
|
||||
Q["排程"] -. Cron .-> A
|
||||
A <--> M[("PostgreSQL<br/>Session・記憶・事件")]
|
||||
V["加密憑證庫"] -. 僅相符 HTTPS 網域 .-> C
|
||||
|
||||
classDef ui fill:#151517,stroke:#3ec5a8,color:#f1f1f2
|
||||
classDef core fill:#101012,stroke:#85858a,color:#f1f1f2
|
||||
classDef secure fill:#221c0e,stroke:#fcd68a,color:#f1f1f2
|
||||
class U,W ui
|
||||
class A,M,Q core
|
||||
class S,C,V secure
|
||||
```
|
||||
|
||||
| 指令 | 做什麼 |
|
||||
<div align="center">
|
||||
|
||||
**[開啟可縮放、可搜尋、支援深色模式的互動流程圖 →](./docs/workflow.html)**
|
||||
|
||||
</div>
|
||||
|
||||
主流程之外還有兩個重要迴圈:
|
||||
|
||||
1. **接管迴圈**:使用者接管時,進行中的 run 進入等待;釋放後從目前畫面重新排隊執行。
|
||||
2. **技能迴圈**:示範期間記錄控制項與頁面情境,模型整理成 playbook;往後仍在當下畫面重新尋找元素,不重播舊座標。
|
||||
|
||||
## 系統架構
|
||||
|
||||
LazyBoy 的公開入口只有 API。Supervisor 位於 Compose 內部 control network,不直接對主機開埠;Agent 桌面也不掛載主機 Docker socket。
|
||||
|
||||
```text
|
||||
Browser
|
||||
│ HTTP / WebSocket / authenticated screen proxy
|
||||
▼
|
||||
lazyboy-api (:3101)
|
||||
├── React 靜態前端
|
||||
├── Session / Run / Memory / Schedule / Vault
|
||||
├── Model provider / MCP client
|
||||
└── PostgreSQL + pgvector
|
||||
│
|
||||
│ authenticated internal control API
|
||||
▼
|
||||
lazyboy-supervisor (:7091, internal only)
|
||||
│
|
||||
├── provision / pause / resume / stop
|
||||
├── CPU / memory / PID limits
|
||||
└── isolated computer containers
|
||||
├── Chromium + CDP
|
||||
├── XFCE + AT-SPI
|
||||
├── Xvfb + x11vnc + websockify
|
||||
└── per-computer persisted home
|
||||
```
|
||||
|
||||
### 主要元件
|
||||
|
||||
| 元件 | 職責 |
|
||||
| --- | --- |
|
||||
| `make up` / `make down` / `make purge` | 啟動/停止(留資料)/連 Postgres 一起清 |
|
||||
| `make logs` `make ps` `make health` | 看狀態 |
|
||||
| `make computer` | 只重建桌面映像 |
|
||||
| `make postgres` | 只開資料庫 `127.0.0.1:5434` |
|
||||
| `apps/web` | Vite + React 19 三欄工作空間、noVNC、語音與雙語 UI |
|
||||
| `crates/api` | 對外 Axum API、Agent run、Session、排程、記憶、MCP、保險箱 |
|
||||
| `crates/harness` | 模型供應商、憑證解析與語音契約 |
|
||||
| `crates/supervisor` | Docker 電腦生命週期、隔離與資源上限 |
|
||||
| `crates/control` | CDP、AT-SPI、X11 與畫面觀察操作 |
|
||||
| `crates/controld` | 電腦容器內部的 localhost 控制服務 |
|
||||
| `crates/contracts` | 跨 crate 的 Bot、Run、Computer、Voice 資料契約 |
|
||||
| `PostgreSQL` | 對話、run、記憶、排程、憑證與保留政策 |
|
||||
|
||||
---
|
||||
### 電腦生命週期
|
||||
|
||||
## 跟 Grok Bot 比,好在哪
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Stopped
|
||||
Stopped --> Booting: 第一個需要 GUI 的工具
|
||||
Booting --> Running: ready
|
||||
Running --> WaitingTakeover: 使用者接管 / 2FA
|
||||
WaitingTakeover --> Running: 釋放控制權
|
||||
Running --> Suspended: 閒置 10 分鐘
|
||||
Suspended --> Running: 新任務或畫面心跳
|
||||
Suspended --> Stopped: 休眠約 6 小時
|
||||
Running --> Stopped: 手動停止
|
||||
```
|
||||
|
||||
[Grok Bot](https://grok.com) 是 xAI 的雲端隊友:對話、電腦、排程都在他們的機器上。LazyBoy 走同一類產品(本機開源實作對齊 Rakazo 那條線),差在**誰擁有執行環境**。
|
||||
## 安全模型
|
||||
|
||||
| | Grok Bot | LazyBoy |
|
||||
目前程式碼包含下列防護:
|
||||
|
||||
- API session token 與 Supervisor token 分離。
|
||||
- Supervisor 只在內部 Compose network,並使用 `no-new-privileges`、唯讀 root filesystem 與 capability drop。
|
||||
- 每台電腦有 CPU、RAM、PID 上限;預設 2 CPU、2 GB、2048 PID。
|
||||
- API 預設只綁定 `127.0.0.1:3101`。
|
||||
- 憑證以獨立 `LAZYBOY_VAULT_KEY` 加密,輪替登入 token 時不應更換此 key。
|
||||
- 已保存登入只接受 HTTPS、精確或合法子網域匹配,不對相似惡意網域填入。
|
||||
- Markdown 連結限制為 HTTP(S)、`mailto:`、`tel:` 與頁內錨點。
|
||||
- 日誌有大小與檔案數上限;診斷資料有可設定的保留週期。
|
||||
|
||||
部署注意事項:
|
||||
|
||||
1. 對區網或網際網路開放前,先放在 HTTPS reverse proxy 後方,並設定 `LAZYBOY_SECURE_COOKIE=true`。
|
||||
2. 不要把 Supervisor `:7091` 對外發布,也不要將 Docker socket 掛進 Agent 電腦。
|
||||
3. `LAZYBOY_APP_TOKEN`、`SANDBOX_SUPERVISOR_TOKEN`、`LAZYBOY_VAULT_KEY` 必須使用不同的高熵值。
|
||||
4. 模型仍可能看見任務所需的網頁內容與截圖;密碼、token 與高敏感資料不要放進提示詞。
|
||||
5. CAPTCHA、2FA 與不確定的敏感操作應由使用者接管。
|
||||
|
||||
## 硬體與資源
|
||||
|
||||
| 項目 | 最低可執行 | 建議 |
|
||||
| --- | --- | --- |
|
||||
| 跑在哪 | xAI 雲端 | 你的 Docker |
|
||||
| 模型 | Grok | 你帶金鑰:xAI、OpenCode Go、或任何 OpenAI 相容端點 |
|
||||
| 電腦 | 廠商提供的桌面 | 你映像裡的 Debian/XFCE/Chromium,家目錄在 `data/homes/` |
|
||||
| 資料 | 在服務端 | 對話、記憶、保險箱、瀏覽器設定檔都在本機 Postgres + 磁碟 |
|
||||
| 登入帳號 | 跟雲端工作流程走 | 每個 Agent 自己的保險箱(AES-256-GCM),模型只看到帳號 id |
|
||||
| 客製 | 封閉 | 開源。工具、MCP、技能 JSON 可改可搬 |
|
||||
| 費用形態 | 訂閱/用量 | 電費與硬體;模型金鑰另計 |
|
||||
| 多 Agent 同桌 | 產品內建 | Team 電腦一個容器最多 8 螢幕;私人電腦一人一容器 |
|
||||
| 教會它 | 看產品當下提供什麼 | 你示範一次,CDP 記語意事件,模型整理成技能 |
|
||||
| CPU | 4 核 | 8 核以上 |
|
||||
| RAM | 8 GB,單台電腦 | 16 GB 以上 |
|
||||
| 磁碟 | 約 15 GB | Docker 至少保留 30 GB |
|
||||
| GPU | 不需要 | 模型預設走外部 API |
|
||||
| 作業系統 | macOS / Linux | Linux 可選配 LXCFS 顯示容器內 cgroup 配額 |
|
||||
|
||||
適合 LazyBoy 的情況:資料不能出門、要自己選模型、要看它點了哪個控制項、或想把「看完訓練影片交測驗」這種流程做成可匯出的技能。
|
||||
每台電腦的預設限制可由 `LAZYBOY_COMPUTER_CPUS`、`LAZYBOY_COMPUTER_MEMORY_MB`、`LAZYBOY_COMPUTER_PIDS` 調整。瀏覽器分頁的心跳會維持熱機;沒有工作且約 10 分鐘無人觀看時暫停,持續休眠約 6 小時後停止。
|
||||
|
||||
Grok Bot 適合的情況:不想養 Docker、要官方託管、機器不夠力。
|
||||
## 設定
|
||||
|
||||
---
|
||||
`make env` 會以 `.env.example` 為基礎建立 `.env`,並保留既有金鑰。
|
||||
|
||||
## 每個功能簡介
|
||||
| 變數 | 用途 | 預設 |
|
||||
| --- | --- | --- |
|
||||
| `XAI_API_KEY` | xAI 模型 | 空 |
|
||||
| `OPENCODE_GO_API_KEY` | OpenCode Go 模型 | 空 |
|
||||
| `OPENAI_API_KEY` | OpenAI 相容端點 | 空 |
|
||||
| `LAZYBOY_APP_TOKEN` | Web 登入 token | 必填 |
|
||||
| `SANDBOX_SUPERVISOR_TOKEN` | API ↔ Supervisor 驗證 | 必填 |
|
||||
| `LAZYBOY_VAULT_KEY` | 憑證庫加密 key | 必填且必須保持穩定 |
|
||||
| `LAZYBOY_BIND_IP` | 主機監聽位址 | `127.0.0.1` |
|
||||
| `LAZYBOY_SECURE_COOKIE` | HTTPS-only cookie | `false` |
|
||||
| `LAZYBOY_COMPUTER_CPUS` | 每台電腦 CPU | `2` |
|
||||
| `LAZYBOY_COMPUTER_MEMORY_MB` | 每台電腦記憶體 | `2048` |
|
||||
| `LAZYBOY_COMPUTER_PIDS` | 每台電腦 PID 上限 | `2048` |
|
||||
| `LAZYBOY_MEMORY_ENABLED` | 長期記憶 | `true` |
|
||||
|
||||
**對話與 Session**
|
||||
每個 Agent 多則對話。訊息進 Postgres,同一則用 `clientNonce` 去重。問候、閒聊走純文字,**不會**為了「看一下螢幕」去開 Docker。
|
||||
完整清單與保留政策請見 [`.env.example`](./.env.example)。
|
||||
|
||||
**Team / 私人電腦**
|
||||
Team:工作區共用一個家目錄,每個 bot 有自己的 `DISPLAY`(`:1`、`:2`…)和瀏覽器設定檔。私人:這個 bot 獨佔一個容器。
|
||||
## 二次開發
|
||||
|
||||
**即時畫面**
|
||||
右側預覽是 noVNC。瀏覽器連 `/view/{botId}/vnc.html`,API 用已登入的 cookie 轉到容器裡的 websockify。模型看到的截圖另走 `computer_observe`,上面會蓋黃字編號;你盯著的 VNC **沒有**那些編號。
|
||||
### 本機開發
|
||||
|
||||
**接管 / 釋放**
|
||||
人按接管就拿到控制租約(預設 15 分鐘,心跳續約)。進行中的 run 會進 `waiting_takeover`,放開後從目前畫面接著做。模型遇到登入牆、2FA、驗證碼會呼叫 `request_takeover`。
|
||||
```bash
|
||||
make dev # 準備 .env、PostgreSQL、電腦映像
|
||||
make dev-supervisor # 終端 1:Supervisor :7091
|
||||
make dev-api # 終端 2:API :3101
|
||||
```
|
||||
|
||||
**觀察與操作**
|
||||
- Chromium 網頁:`browser`(CDP,點 element id)
|
||||
- 原生視窗(對話框、檔案管理員、XFCE):`computer_act`(AT-SPI id,不行再退 xdotool 座標)
|
||||
- 檔案與指令:`list_files` / `read_file` / `write_file` / `shell`
|
||||
點到 `[disabled]` 的控制項會最多等 45 秒等它亮。模型用文字回「我在等」會結束整段 run,所以等待必須是 `wait` 工具。
|
||||
前端熱更新:
|
||||
|
||||
**教技能**
|
||||
你示範,容器內 CDP 錄「點了哪個控制項、填了什麼、去了哪一頁」,再抽幾個關鍵畫面。停下來後模型整理成意圖級 playbook,之後用普通工具在**當下畫面**找控制項,不是重播座標。密碼欄不錄。技能可匯出 JSON。
|
||||
```bash
|
||||
cd apps/web
|
||||
npm install
|
||||
npm run dev # http://127.0.0.1:5173
|
||||
```
|
||||
|
||||
**Slash 指令與長目標**
|
||||
在 `data/skills/<name>/SKILL.md` 放工作區共用的唯讀技能,就能在輸入框打 `/name 參數` 執行;輸入 `/` 會顯示可用技能。`/goal` 是 harness 的持續執行模式,和錄製示範產生的 playbook 分開。選單支援方向鍵、Enter/Tab 選取、Esc 關閉。`/goal 目標` 會先規劃、執行並檢查結果,直到模型回報已驗證完成;只有需要登入、驗證碼、接管畫面或缺少必要資訊時才會停下來請你處理。缺少必要資訊時會記為等待輸入,補充訊息後接續原目標;停止按鈕可以取消。一般訊息仍維持 40 回合上限,教學技能 80 回合。
|
||||
### 檢查與測試
|
||||
|
||||
**記憶**
|
||||
`pgvector` + MiniLM(384 維)。只有你叫它記住、或它呼叫 `remember` 的內容會進長期記憶。密碼與 token 會被拒。清除對話不會清記憶。
|
||||
```bash
|
||||
make fmt
|
||||
make clippy
|
||||
make test
|
||||
|
||||
**保險箱**
|
||||
每個 bot 自己的站名/帳號/密碼。模型用 `list_accounts` 只看到站與使用者名稱,`use_saved_login` 在 Chromium 登入表單填入。金鑰用 `LAZYBOY_VAULT_KEY` 加密。
|
||||
cd apps/web
|
||||
npm run typecheck
|
||||
npm run build
|
||||
|
||||
**排程**
|
||||
五欄 cron,預設 `Asia/Taipei`。對話裡講「以後每天九點」或側欄新增。tick 迴圈把到期列變成普通 queued run。
|
||||
cd ../..
|
||||
node --test tests/frontend.test.mjs
|
||||
python3 tests/control.test.py
|
||||
python3 tests/log-rotation.test.py
|
||||
|
||||
**MCP**
|
||||
工作區級外掛。市集或自訂 stdio/HTTP/SSE。stdio 跑在 API 容器裡。
|
||||
# 需要已啟動的 PostgreSQL Compose service
|
||||
docker compose up -d postgres
|
||||
python3 tests/retention.test.py
|
||||
```
|
||||
|
||||
**群組**
|
||||
多個 Agent 同一個 thread。Team 電腦上各用各的螢幕。同一 bot 同時只跑一個 run,後面的訊息排隊。
|
||||
|
||||
**附件**
|
||||
圖片給當則模型看,不進歷史二進位。要讓電腦開原檔會放 `inbox/`,兩小時後刪。
|
||||
|
||||
**頭像與狀態**
|
||||
Blobatar 色塊+眼睛。啟動、喚醒、連線、換手時,預覽左上角與思考列會顯示對應文字。分頁開著時心跳保住容器,換手不拆 VNC。
|
||||
|
||||
---
|
||||
|
||||
## 系統怎麼轉起來
|
||||
|
||||

|
||||
|
||||
Compose 裡 supervisor **不**對主機開埠。API 在容器網路連 `supervisor:7091`。家目錄 `data/homes/<homeKey>` bind 進容器的 `/home/lazyboy`。
|
||||
|
||||
---
|
||||
|
||||
## 你送一則訊息
|
||||
|
||||

|
||||
|
||||
同一 bot 已有進行中的工作時,新訊息會排隊(`queuedBehindActive`)。人正在接管時,後面的話只排隊,思考轉圈不會假裝它還在動。問候路徑會把工具表清空,從源頭避免「哈囉」去開電腦。
|
||||
|
||||
`execute_run` 每一輪:續租約 → 寫步驟文字 → 問模型 → 沒有工具就結束(技能沒過會再把畫面塞回去)→ 有工具且需要沙盒才 boot → 畫面沒變就不重複塞圖。回合上限:聊天 4、一般 40、教學技能 80;`/goal` 會持續到完成或明確需要人介入。目標執行期間,同一對話送進來的新訊息會作為下一輪的補充指示。
|
||||
|
||||
---
|
||||
|
||||
## 電腦的作息
|
||||
|
||||

|
||||
|
||||
閒置(`crates/api/src/computer.rs` `idle_loop`):
|
||||
|
||||
1. 執行中、超過 10 分鐘沒人看、也沒有進行中的 run/示範 → `docker pause`,狀態 `suspended`
|
||||
2. 休眠超過 6 小時 → `docker stop`,狀態 `stopped`
|
||||
3. 分頁還在就心跳,不會進 1
|
||||
|
||||
開機/喚醒:已在跑就直接回;凍結中就 `unpause`(約一秒);沒有容器才 `provision`,等 `/tmp/lazyboy/ready`。畫面走 `/view/{bot}/vnc.html`,已登入的 cookie 轉到 websockify。
|
||||
|
||||
---
|
||||
|
||||
## 它怎麼看、怎麼點
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
模型從不直接連 VNC。它只打 API 工具;工具經 sandbox HTTP 進 supervisor,再 `docker exec` 或打容器內 `controld`。
|
||||
|
||||
編號只畫在給模型的 JPEG 上。VNC 是乾淨桌面。每次 navigation/snapshot 會重編號,舊 id 作廢。`computer_act` 點在瀏覽器視窗上會被拒,避免用像素點網頁。解析度契約是 **1280×800**。
|
||||
|
||||
每個 bot 一個 `computer_screens` 列:slot、DISPLAY、執行租約、控制租約。人接管寫 `control_holder=user`,worker 在回合邊界停,不跟你搶滑鼠。`view_only` 用 postMessage 切,不重掛 iframe,所以換手時預覽不會黑掉。
|
||||
|
||||
`lazyboy-controld` 聽 `127.0.0.1:7070`。Team 多螢幕:slot 0 = `:1`,slot N = `:N+1`。`lazyboy-screen ensure` 在同一個容器裡再長一組 Xvfb。
|
||||
|
||||
---
|
||||
|
||||
## 模型金鑰從哪來
|
||||
|
||||

|
||||
|
||||
`crates/harness` 不管滑鼠,只決定這次 run 要用哪一家模型。真正的 agent 迴圈在 `crates/api/src/runs.rs`。
|
||||
|
||||
金鑰:**這個機器人 → 工作區設定 → 環境變數**。API 跑在 Docker 時,迴圈位址 `127.0.0.1` 會被改成 `host.docker.internal`,才能打到你本機的相容端點。
|
||||
|
||||
---
|
||||
|
||||
## 教會它
|
||||
|
||||

|
||||
|
||||
之後 run 若 prompt 對得上技能名,會把完整 playbook 塞進當則,並清掉舊聊天以免模型複誦上次的「還在倒數」。執行仍用 `browser`/`computer_act`,在**現在**的畫面上找「Next」,不是記像素。
|
||||
|
||||
---
|
||||
|
||||
## 排程怎麼進工作
|
||||
|
||||

|
||||
|
||||
Cron 五欄。時區寫在列上,預設台北。`立刻跑` 只是立刻插一筆 run,不改下一拍時間。
|
||||
|
||||
---
|
||||
|
||||
## 專案目錄(二次開發從這裡找)
|
||||
|
||||

|
||||
|
||||
Cargo workspace。畫面在 `apps/web`,對話與工作在 `api`,怎麼點在 `control`,開機在 `supervisor` + `image/computer`,問哪一家模型在 `harness`。前端是獨立的 Vite app,由 API 把 `apps/web/dist`(或開發時的 `apps/web`)端出去。
|
||||
### 專案結構
|
||||
|
||||
```text
|
||||
LazyBoy/
|
||||
├── apps/web/ 瀏覽器 UI(Vite + React)
|
||||
│ ├── src/App.tsx 幾乎全部畫面:側欄、聊天、電腦、設定
|
||||
│ ├── src/schedule.tsx 排程面板
|
||||
│ ├── src/avatar.tsx Blobatar 頭像
|
||||
│ ├── src/api.ts fetch 包裝、401
|
||||
│ ├── src/types.ts 跟 API JSON 對齊的型別
|
||||
│ ├── src/locales/zh-TW.ts 所有使用者看得到的字
|
||||
│ ├── src/*.css 樣式(styles / chat / computer / refinements…)
|
||||
│ └── vnc.html 內嵌桌面(noVNC);API 的 /view 會讀這一檔
|
||||
├── apps/web/ React 前端與 noVNC 頁面
|
||||
├── crates/
|
||||
│ ├── contracts/ 跨 crate 的型別:Bot、Run、ComputerState、動作 JSON
|
||||
│ ├── harness/ 模型後端:CredentialChain、resolve_backend、connect_model
|
||||
│ ├── control/ 桌面契約:螢幕 slot、租約、CDP/AT-SPI/xdotool、overlay
|
||||
│ │ 含 a11y.py / cdp.py(容器裡被 exec 的腳本)
|
||||
│ ├── sandbox/ API 打 supervisor 的 HTTP 客戶端;fake 給測試
|
||||
│ ├── supervisor/ Docker:provision / pause / unpause / exec / observe / act
|
||||
│ ├── controld/ 打進容器的小 HTTP(127.0.0.1:7070)
|
||||
│ └── api/ 唯一對外程序:路由、worker、idle、排程 tick、靜態網頁
|
||||
│ └── src/
|
||||
│ ├── main.rs 啟動、三條背景迴圈
|
||||
│ ├── routes.rs 組 router;bot / computer HTTP 也在這
|
||||
│ ├── runs.rs agent 迴圈(租約、complete_once、nudge)
|
||||
│ ├── tools.rs tool_definitions + dispatch(加工具從這裡)
|
||||
│ ├── computer.rs boot / 凍結 / 心跳 / 螢幕租約
|
||||
│ ├── sessions.rs 對話 CRUD、送訊息、SSE
|
||||
│ ├── skills.rs 示範錄製與蒸馏
|
||||
│ ├── file_skills.rs 讀取 data/skills 下的唯讀 SKILL.md
|
||||
│ ├── schedules.rs cron
|
||||
│ ├── vault.rs 登入保險箱
|
||||
│ ├── memory.rs pgvector 記憶
|
||||
│ ├── mcp.rs MCP 連線
|
||||
│ ├── screen_proxy.rs /view 反代
|
||||
│ └── db.rs SQL 與列定義
|
||||
├── image/
|
||||
│ ├── api/Dockerfile
|
||||
│ ├── supervisor/Dockerfile
|
||||
│ └── computer/ 桌面映像
|
||||
│ ├── Dockerfile
|
||||
│ ├── start.sh PID 1:controld + Xvfb/XFCE/VNC
|
||||
│ └── lazyboy-screen Team 額外 DISPLAY
|
||||
├── migrations/ sqlx,檔名流水號;API 啟動時自動 migrate
|
||||
├── data/homes/ 每個電腦的家目錄(bind 進容器 /home/lazyboy)
|
||||
├── data/skills/<name>/SKILL.md 工作區共用的 slash 技能(執行時掛載)
|
||||
├── tests/ 跨語言的小測試(node:test、Python)
|
||||
├── scripts/ init-env、build-computer-image、dev
|
||||
├── docker-compose.yml 正式堆疊(Postgres + supervisor + API)
|
||||
├── Makefile make up / dev / test
|
||||
└── reference/rakazo/ 上游參考實作,不要當 runtime 依賴
|
||||
│ ├── api/ 對外 API 與 Agent 執行迴圈
|
||||
│ ├── contracts/ 跨元件資料契約
|
||||
│ ├── control/ 瀏覽器與桌面控制
|
||||
│ ├── controld/ 容器內控制服務
|
||||
│ ├── harness/ 模型與語音後端
|
||||
│ ├── sandbox/ API 到 Supervisor 的抽象
|
||||
│ └── supervisor/ Docker 生命週期管理
|
||||
├── image/ API、Supervisor、電腦映像
|
||||
├── migrations/ SQLx PostgreSQL migrations
|
||||
├── tests/ Rust 以外的契約與回歸測試
|
||||
├── scripts/ 環境初始化與執行工具
|
||||
├── docs/hero.html README 首頁視覺原稿
|
||||
├── docs/workflow.html 可互動產品流程圖
|
||||
├── docker-compose.yml 正式堆疊
|
||||
└── Makefile 常用開發與部署指令
|
||||
```
|
||||
|
||||
### 想改什麼,開哪個檔
|
||||
## 徽章與開源認證
|
||||
|
||||
| 你要做的事 | 先開 |
|
||||
| --- | --- |
|
||||
| 加一個模型工具(例如 `screenshot_region`) | `crates/api/src/tools.rs`(`tool_definitions` + `dispatch`);若要 GUI,`runs.rs` 的 `tool_needs_sandbox` / `tool_needs_gui` |
|
||||
| 工具對應的滑鼠/鍵盤/CDP | `crates/control/src/{actions,x11,cdp,a11y}.rs` 與同目錄 `.py` |
|
||||
| 新的 HTTP 端點 | 功能模組自己的 `router()`(如 `schedules.rs`),在 `routes.rs` `.merge(...)`;電腦/bot 則直接寫在 `routes.rs` |
|
||||
| 新狀態、動作 JSON、Run 狀態機 | `crates/contracts/src/`(改完 `api` / 前端 `types.ts` 一起對) |
|
||||
| 換模型供應商或金鑰解析 | `crates/harness/src/resolve.rs`、`crates/contracts/src/model.rs`、`workspace.rs` |
|
||||
| 容器怎麼開、凍結、等 ready | `crates/supervisor/src/docker.rs`、`crates/api/src/computer.rs` |
|
||||
| 桌面裡多裝套件、改 XFCE、開機腳本 | `image/computer/`,然後 `make computer` |
|
||||
| 對話 UI、電腦預覽、頭像小卡 | `apps/web/src/App.tsx` + 對應 css |
|
||||
| 畫面上的中文 | `apps/web/src/locales/zh-TW.ts`(key 加了 `tsc` 才會過) |
|
||||
| 內嵌 VNC 行為(貼上、唯讀) | `apps/web/vnc.html` |
|
||||
| 新資料表 | `migrations/0xx_....sql`;列定義補 `crates/api/src/db.rs` |
|
||||
| 排程 UI | `apps/web/src/schedule.tsx` |
|
||||
| MCP 市集清單 | `crates/api/src/mcp_catalog.rs` |
|
||||
README 頂端目前只顯示可以直接從原始碼驗證的資訊徽章,不把「尚未執行的檢查」包裝成通過。
|
||||
|
||||
### 加一支工具的最短路徑
|
||||
若要取得可公開查驗的安全與品質徽章,建議依序完成:
|
||||
|
||||
1. `tools.rs` 的 `tool_definitions` 加 `ToolDefinition`(名稱、說明、JSON Schema)。說明是寫給模型看的。
|
||||
2. 同一個檔的 `dispatch` 加 match arm,回 `ToolOutcome { text, image, pause, blocks }`。
|
||||
3. 若會動到桌面:`runs.rs` 裡 `tool_needs_sandbox` / `tool_needs_gui` 把名字加進去,否則不會 boot、也拿不到螢幕租約。
|
||||
4. 需要新的容器指令就放 `control`(Rust 組 argv,Python 做 CDP/AT-SPI),supervisor 的 `exec` 已經會把 `DISPLAY` 帶進去。
|
||||
5. 前端若要顯示步驟文字,`runs.rs` 的 `describe_step` 加一列。
|
||||
6. `SANDBOX_PROVIDER=fake cargo test -p lazyboy-api` 先過,再對真容器看。
|
||||
- [ ] 在公開 GitHub repository 建立正式 mirror;OpenSSF 的公開查驗以 GitHub 為主要整合目標。
|
||||
- [ ] 加入完整 `LICENSE`、`SECURITY.md`、`CONTRIBUTING.md` 與行為準則。
|
||||
- [ ] 建立 CI:Rust format / Clippy / tests、前端 typecheck / build / tests。
|
||||
- [ ] 啟用 Dependabot 或 Renovate、CodeQL、secret scanning 與 branch protection。
|
||||
- [ ] 執行並發布 [OpenSSF Scorecard](https://scorecard.dev/) 結果後,再加入 Scorecard 徽章。
|
||||
- [ ] 在 [OpenSSF Best Practices](https://www.bestpractices.dev/) 登記專案、誠實完成 Passing 問卷後,再加入認證徽章。
|
||||
- [ ] 若提供容器映像,再加入 SBOM、簽章與可重現版本發布流程。
|
||||
|
||||
### 本機二次開發迴圈
|
||||
> 不建議現在顯示 CI passing、coverage、OpenSSF 或 Best Practices 徽章:目前 repository 沒有對應的公開結果,徽章會失真或直接顯示 unknown。
|
||||
|
||||
```bash
|
||||
make env && make postgres # 資料庫
|
||||
make computer # 桌面映像有改才需要
|
||||
make dev-supervisor # 終端 1,:7091
|
||||
make dev-api # 終端 2,:3101,會自動跑 migrations
|
||||
# 前端另開:
|
||||
cd apps/web && npm install && npm run dev # :5173
|
||||
```
|
||||
## 專案狀態
|
||||
|
||||
- 改 `apps/web/src`:Vite 熱更新。
|
||||
- 改 `crates/api`:停掉 `dev-api` 再 `make dev-api`。
|
||||
- 改 `crates/supervisor`:同樣重跑 supervisor。
|
||||
- 改 `crates/control` 的 `.py`:映像沒重建的話,執行中的容器還是舊腳本;要嘛 `make computer` 後重開電腦,要嘛確認 supervisor exec 讀的是映像內檔案。
|
||||
- 改 `image/computer`:一定 `make computer`,再在 UI 重啟該台電腦。
|
||||
- 契約改了:同時改 `contracts`、呼叫端、`apps/web/src/types.ts`。
|
||||
LazyBoy 目前版本為 `0.1.0`,仍屬早期階段。建議先在本機或受信任網路中使用;對外部署前請完成威脅模型、權限檢查、備份與還原演練。
|
||||
|
||||
檢查:
|
||||
|
||||
```bash
|
||||
make fmt && make clippy && make test
|
||||
node --test tests/frontend.test.mjs # 排程 cron、VNC 貼上、登入填表防護
|
||||
```
|
||||
|
||||
`SANDBOX_PROVIDER=fake` 時 API 不碰 Docker,適合先測 run/工具契約。
|
||||
|
||||
`reference/rakazo/` 是對齊用的上游,不要在 LazyBoy runtime import 它。
|
||||
Cargo workspace 的授權中繼資料宣告為 MIT。若要正式公開散布,應先在 repository 根目錄補上完整 MIT `LICENSE` 文字,再把上方授權徽章改為連到該檔案。
|
||||
|
||||
---
|
||||
|
||||
## 安全(操作時要記得)
|
||||
<div align="center">
|
||||
|
||||
- Supervisor 只在 Compose 內網,不要對 LAN 開埠
|
||||
- 畫面走已登入 API,VNC 密碼不進瀏覽器 URL
|
||||
- 區網請走 HTTPS;終端是 HTTPS 時設 `LAZYBOY_SECURE_COOKIE=true`
|
||||
- API 綁非本機時 `LAZYBOY_APP_TOKEN` 至少 32 字;supervisor 拒絕空白、過短、`dev-token`
|
||||
- 保險箱用 `LAZYBOY_VAULT_KEY`;換登入 token 時這把 key 要留著
|
||||
- 模型看不到密碼本文;2FA/CAPTCHA 一定要人在**它的**畫面上處理
|
||||
用自然語言交代工作,保留看得見、接得回來的控制權。
|
||||
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,30 +0,0 @@
|
|||
# Bug 檢查與改善計劃
|
||||
|
||||
檢查日期:2026-09-05。範圍為目前工作目錄的前端、排程與 VNC 剪貼簿整合;不是全專案完整稽核。保留原有未提交修改。
|
||||
|
||||
## 本次已修正
|
||||
|
||||
- 頭像:縮小聊天氣泡 span 選擇器範圍,固定頭像尺寸,群聊頂欄改以實際 32px 計算堆疊位置,移除額外縮放。
|
||||
- 綠點:恢復頭像本身的 presence,取消側欄額外偽元素,限制綠點大小;未讀仍以右上藍點區別。
|
||||
- 排程:補齊表單、checkbox、換行及按鈕配置;由整個電腦側欄負責捲動,避免列表與編輯器壓縮預覽。聊天排程卡片與文字採上下排列。
|
||||
- 等待動畫:電腦採獨立薄荷色機器人、呼吸光環與琥珀軌道;聊天只依工作中的對話顯示思考,不再因電腦啟動/連線而出現。排除 null session ID 互相比較產生假忙碌。
|
||||
- 剪貼簿:在 noVNC 接收前攔截 Ctrl/Cmd+V,讀取本機文字後只送出一次遠端貼上;權限不足時開啟手動貼上框。延遲貼上期間換連線或變唯讀就取消。父子訊息驗證來源視窗,避免舊 iframe 更新目前狀態。
|
||||
|
||||
## 計劃項目完成狀態
|
||||
|
||||
| 優先 | 項目 | 實作與驗證 |
|
||||
| --- | --- | --- |
|
||||
| P1 | 固定間隔排程 | 改用 `@every Nm/Nh/Nd`,一天固定 24 小時;跨月、DST、漏跑相位測試通過。舊 cron 保留原文 |
|
||||
| P1 | Cron 編輯不失真 | 僅五欄及合法範圍轉換預設;未知格式保留 Advanced,空值禁止儲存;修正 Unix/Rust 星期編號差異 |
|
||||
| P1 | 電腦啟動失敗恢復 | 失敗立即刷新狀態,刷新失敗回復前值;回應只更新原本的 bot |
|
||||
| P1 | 遠端貼上同步 | 後端確認 X11 剪貼簿內容後才貼上;終端使用 Ctrl+Shift+V,連續貼上依序處理 |
|
||||
| P2 | 複製與權限提示 | 顯示同步結果、權限失敗退路;macOS Cmd+C 交由後端判斷終端快捷鍵 |
|
||||
| P2 | CSS 拆分 | Avatar、Chat、Computer、Schedule 分檔,響應式規則集中最後載入 |
|
||||
| P2 | 主 JS 過大 | 動畫改為 lazy import;主檔約 446 kB,動畫 chunk 約 318 kB,消除 500 kB chunk 警告 |
|
||||
|
||||
## 驗證與限制
|
||||
|
||||
- 前端 TypeScript、正式打包、5 個 Node 回歸測試、3 個 Python 控制測試通過;Rust 驗證詳見[安全與 Harness 檢查](security-and-harness-review.md)。
|
||||
- 瀏覽器工具回報沒有可用瀏覽器,尚未完成視覺截圖與真實 VNC 端到端驗收。這些項目已完成程式修正,不能等同所有畫面已實機驗收。
|
||||
- 待實機驗收:1440×900、1280×720、390×844;群聊、長排程、多筆排程、開機與聊天各自獨立、權限拒絕、中文/emoji/多行與重連。
|
||||
- 剪貼簿支援純文字;圖片、檔案不在此次實作範圍。Lottie 上游 eval 提醒仍存在;未量測首屏時間。
|
||||
|
|
@ -1,925 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="zh-Hant">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<title>LazyBoy diagrams</title>
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: Huninn;
|
||||
src: url("../apps/web/node_modules/@fontsource/huninn/files/huninn-latin-400-normal.woff2") format("woff2");
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F;
|
||||
}
|
||||
@font-face {
|
||||
font-family: Huninn;
|
||||
src: url("../apps/web/node_modules/@fontsource/huninn/files/huninn-chinese-traditional-400-normal.woff2") format("woff2");
|
||||
unicode-range: U+4E00-9FFF, U+3400-4DBF, U+F900-FAFF, U+3000-303F, U+FF00-FFEF;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html, body {
|
||||
margin: 0;
|
||||
width: 1280px;
|
||||
height: 720px;
|
||||
overflow: hidden;
|
||||
background: #f3f3f5;
|
||||
color: #161618;
|
||||
font-family: Huninn, "PingFang TC", sans-serif;
|
||||
}
|
||||
.slide {
|
||||
width: 1280px;
|
||||
height: 720px;
|
||||
padding: 32px 48px 30px;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
}
|
||||
.slide.on { display: flex; }
|
||||
.kicker {
|
||||
font-size: 12px;
|
||||
letter-spacing: .16em;
|
||||
color: #0f766e;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 30px;
|
||||
font-weight: 400;
|
||||
letter-spacing: .01em;
|
||||
}
|
||||
.sub {
|
||||
margin: 6px 0 20px;
|
||||
color: #6b6b73;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.row { display: flex; gap: 16px; align-items: stretch; flex: 1; min-height: 0; }
|
||||
.col3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px; flex: 1; min-height: 0; }
|
||||
.col4 { display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap: 14px; flex: 1; min-height: 0; }
|
||||
.col5 { display: grid; grid-template-columns: repeat(5, 1fr); gap: 12px; flex: 1; min-height: 0; }
|
||||
.split { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; flex: 1; min-height: 0; }
|
||||
.rooms {
|
||||
display: grid;
|
||||
grid-template-columns: 1.15fr 1fr 1fr;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
gap: 14px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 22px;
|
||||
border: 1px solid #ececef;
|
||||
box-shadow: 0 16px 40px rgba(20,20,24,.06);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.card.dark { background: #111113; color: #f1f1f2; border-color: #1c1c20; }
|
||||
.viz {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.copy { padding: 14px 16px 16px; }
|
||||
.copy h2 { margin: 0 0 4px; font-size: 17px; font-weight: 400; }
|
||||
.copy p { margin: 0; color: #6b6b73; font-size: 13px; line-height: 1.45; }
|
||||
.dark .copy p { color: #9a9aa2; }
|
||||
.n {
|
||||
width: 24px; height: 24px; border-radius: 50%;
|
||||
display: grid; place-items: center;
|
||||
background: #3ec5a8; color: #083f34; font-size: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.pill {
|
||||
display: inline-flex; align-items: center;
|
||||
height: 22px; padding: 0 9px; border-radius: 999px;
|
||||
background: #eef8f5; color: #0f766e; font-size: 11px;
|
||||
}
|
||||
.dark .pill { background: #1a2e2a; color: #7ee0c8; }
|
||||
.face { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; }
|
||||
.blob { width: 36px; height: 36px; flex: 0 0 36px; }
|
||||
.chev {
|
||||
width: 22px; flex: 0 0 22px;
|
||||
display: grid; place-items: center;
|
||||
color: #3ec5a8; font-size: 22px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
/* mini chat */
|
||||
.chat-mini {
|
||||
position: absolute; inset: 16px 14px 12px;
|
||||
display: flex; flex-direction: column; gap: 8px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.b {
|
||||
max-width: 86%;
|
||||
padding: 8px 12px;
|
||||
border-radius: 14px;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.me { align-self: flex-end; background: #f1f1ef; color: #1a1a1a; }
|
||||
.them { align-self: flex-start; background: #19191c; color: #eee; }
|
||||
.think-row {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
color: #85858a; font-size: 11px;
|
||||
}
|
||||
|
||||
/* xfce-ish desktop */
|
||||
.desk {
|
||||
position: absolute; inset: 0;
|
||||
background:
|
||||
radial-gradient(circle at 18% 0%, #1a2430 0%, transparent 46%),
|
||||
linear-gradient(180deg, #15202b, #0d1218);
|
||||
}
|
||||
.desk.dim { filter: saturate(.4) brightness(.55); }
|
||||
.desk.off { background: #09090b; }
|
||||
.win {
|
||||
position: absolute;
|
||||
left: 16px; top: 14px; right: 16px; bottom: 26px;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 10px 24px rgba(0,0,0,.32);
|
||||
display: grid;
|
||||
grid-template-rows: 22px 26px 1fr;
|
||||
}
|
||||
.win-bar {
|
||||
background: #e8e8ea;
|
||||
display: flex; align-items: center; gap: 5px;
|
||||
padding: 0 8px;
|
||||
color: #666; font-size: 10px;
|
||||
}
|
||||
.win-bar i { width: 7px; height: 7px; border-radius: 50%; background: #c4c4c8; font-style: normal; }
|
||||
.addr {
|
||||
background: #f4f4f6;
|
||||
display: flex; align-items: center;
|
||||
padding: 0 10px;
|
||||
color: #666; font-size: 10px;
|
||||
border-bottom: 1px solid #ececf0;
|
||||
}
|
||||
.page-body {
|
||||
padding: 12px 14px 14px;
|
||||
color: #222;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
.page-body h3 { margin: 0 0 4px; font-size: 14px; font-weight: 400; }
|
||||
.page-body p { margin: 0; color: #666; font-size: 11px; line-height: 1.4; }
|
||||
.video {
|
||||
flex: 1;
|
||||
min-height: 40px;
|
||||
margin: 10px 0 8px;
|
||||
border-radius: 8px;
|
||||
background:
|
||||
linear-gradient(160deg, #1c2838 0%, #1a2c2a 55%, #2a8f7a 140%);
|
||||
}
|
||||
.lessons { margin: 8px 0 0; padding: 0; list-style: none; font-size: 11px; color: #555; }
|
||||
.lessons li {
|
||||
display: flex; justify-content: space-between;
|
||||
padding: 5px 0;
|
||||
border-bottom: 1px solid #f0f0f2;
|
||||
}
|
||||
.lessons li.on { color: #0f766e; }
|
||||
.mail { margin-top: 8px; font-size: 11px; color: #444; }
|
||||
.mail .m { padding: 7px 8px; border-radius: 8px; }
|
||||
.mail .m.on { background: #eef8f5; }
|
||||
.progress {
|
||||
margin-top: 8px; height: 6px; border-radius: 99px; background: #ececf0; overflow: hidden;
|
||||
}
|
||||
.progress b { display: block; width: 62%; height: 100%; background: #3ec5a8; }
|
||||
.next {
|
||||
margin-top: 10px; height: 26px; width: 68px;
|
||||
border-radius: 6px; background: #1a1a1c; color: #fff;
|
||||
display: grid; place-items: center; font-size: 11px;
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.btns { display: flex; gap: 8px; margin-top: auto; padding-top: 10px; }
|
||||
.tag {
|
||||
position: absolute;
|
||||
right: -9px; top: -9px;
|
||||
width: 20px; height: 20px; border-radius: 5px;
|
||||
background: #ffe66d; color: #111;
|
||||
display: grid; place-items: center; font-size: 11px;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,.18);
|
||||
}
|
||||
.panelbar {
|
||||
position: absolute; left: 0; right: 0; bottom: 0; height: 22px;
|
||||
background: #1b242e; border-top: 1px solid #2a3540;
|
||||
}
|
||||
.zzz {
|
||||
position: absolute; right: 18px; top: 14px;
|
||||
color: #cdeae3; font-size: 22px; letter-spacing: 2px;
|
||||
}
|
||||
.house {
|
||||
position: absolute; inset: 0;
|
||||
display: grid; place-items: center;
|
||||
color: #3ec5a8; font-size: 13px; text-align: center; line-height: 1.5;
|
||||
}
|
||||
|
||||
/* queue / chips */
|
||||
.stack {
|
||||
position: absolute; inset: 18px 16px;
|
||||
display: flex; flex-direction: column; gap: 8px; justify-content: center;
|
||||
}
|
||||
.job {
|
||||
height: 36px; border-radius: 10px;
|
||||
display: flex; align-items: center; padding: 0 12px;
|
||||
font-size: 12px; gap: 8px;
|
||||
}
|
||||
.job.on { background: #111113; color: #fff; }
|
||||
.job.wait { background: #f1f1ef; color: #666; }
|
||||
.job .dot { width: 7px; height: 7px; border-radius: 50%; background: #3ec5a8; }
|
||||
|
||||
.fork {
|
||||
position: absolute; inset: 16px 12px;
|
||||
display: flex; flex-direction: column; gap: 10px; justify-content: center;
|
||||
}
|
||||
.lane {
|
||||
border-radius: 12px; padding: 10px 12px;
|
||||
font-size: 12px; line-height: 1.4;
|
||||
}
|
||||
.lane.a { background: #eef8f5; color: #0f766e; }
|
||||
.lane.b { background: #111113; color: #f1f1f2; }
|
||||
|
||||
.keys-viz {
|
||||
position: absolute; inset: 0;
|
||||
display: grid; place-items: center;
|
||||
}
|
||||
.key {
|
||||
width: 88px; height: 88px;
|
||||
}
|
||||
.clock {
|
||||
width: 120px; height: 120px;
|
||||
}
|
||||
.playbook {
|
||||
position: absolute; inset: 18px 16px;
|
||||
background: #fff; border-radius: 10px;
|
||||
padding: 12px; box-shadow: 0 8px 20px rgba(0,0,0,.08);
|
||||
font-size: 11px; color: #333; line-height: 1.55;
|
||||
}
|
||||
.playbook b { display: block; font-weight: 400; font-size: 13px; margin-bottom: 6px; }
|
||||
.events {
|
||||
position: absolute; inset: 16px 14px;
|
||||
display: flex; flex-direction: column; gap: 7px; justify-content: center;
|
||||
}
|
||||
.ev {
|
||||
background: #fff; border-radius: 10px; padding: 8px 10px;
|
||||
font-size: 11px; color: #333;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,.06);
|
||||
}
|
||||
.ev em { color: #0f766e; font-style: normal; }
|
||||
|
||||
.dialog {
|
||||
position: absolute; left: 18px; right: 18px; top: 28px; bottom: 28px;
|
||||
background: #f6f6f4; border-radius: 10px;
|
||||
box-shadow: 0 12px 28px rgba(0,0,0,.2);
|
||||
padding: 12px;
|
||||
color: #222; font-size: 12px;
|
||||
}
|
||||
.dialog h4 { margin: 0 0 10px; font-weight: 400; font-size: 13px; }
|
||||
.file { padding: 6px 8px; border-radius: 6px; }
|
||||
.file.on { background: #d7efe8; }
|
||||
.dlg-btn {
|
||||
position: absolute; right: 12px; bottom: 12px;
|
||||
height: 26px; padding: 0 12px; border-radius: 6px;
|
||||
background: #1a1a1c; color: #fff; display: grid; place-items: center; font-size: 11px;
|
||||
}
|
||||
|
||||
.canvas {
|
||||
position: absolute; inset: 16px;
|
||||
background:
|
||||
linear-gradient(#ececf0 1px, transparent 1px) 0 0 / 24px 24px,
|
||||
linear-gradient(90deg, #ececf0 1px, transparent 1px) 0 0 / 24px 24px,
|
||||
#fafafa;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.cross {
|
||||
position: absolute; left: 52%; top: 46%;
|
||||
width: 18px; height: 18px;
|
||||
transform: translate(-50%,-50%);
|
||||
}
|
||||
.cross:before, .cross:after {
|
||||
content: ""; position: absolute; background: #d23b3b;
|
||||
}
|
||||
.cross:before { left: 8px; top: 0; width: 2px; height: 18px; }
|
||||
.cross:after { left: 0; top: 8px; width: 18px; height: 2px; }
|
||||
.xy {
|
||||
position: absolute; left: 54%; top: 52%;
|
||||
font-size: 11px; color: #d23b3b;
|
||||
}
|
||||
|
||||
.alarm {
|
||||
position: absolute; inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.cron {
|
||||
margin-top: 8px; font-size: 12px; color: #0f766e;
|
||||
background: #eef8f5; padding: 4px 10px; border-radius: 999px;
|
||||
}
|
||||
|
||||
.room {
|
||||
border-radius: 20px;
|
||||
padding: 18px 18px 16px;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
min-height: 0;
|
||||
}
|
||||
.room small { opacity: .82; font-size: 13px; line-height: 1.45; }
|
||||
.room h2 { margin: 0 0 4px; font-size: 20px; font-weight: 400; }
|
||||
.room .path { font-size: 11px; opacity: .7; letter-spacing: .02em; }
|
||||
.tiny-ui {
|
||||
margin: 14px 0 10px;
|
||||
background: #1a1a1d;
|
||||
border-radius: 12px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
display: grid;
|
||||
grid-template-columns: 54px 1fr 78px;
|
||||
}
|
||||
.tiny-ui > div { border-right: 1px solid #2a2a2e; }
|
||||
.tiny-ui > div:last-child { border: 0; background: #121214; }
|
||||
.form {
|
||||
position: absolute; inset: 22px 18px;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 18px;
|
||||
box-shadow: 0 8px 20px rgba(0,0,0,.06);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.form.dark {
|
||||
background: #19191c;
|
||||
color: #eee;
|
||||
}
|
||||
.form label { display: block; font-size: 11px; color: #888; margin-bottom: 8px; }
|
||||
.field {
|
||||
height: 36px; border-radius: 10px;
|
||||
display: flex; align-items: center; padding: 0 12px;
|
||||
font-size: 13px; gap: 8px;
|
||||
background: #f4f4f6;
|
||||
}
|
||||
.form.dark .field { background: #111113; }
|
||||
.ok { margin-left: auto; color: #3ec5a8; font-size: 12px; }
|
||||
.env-lines {
|
||||
font-size: 12px; line-height: 1.8; color: #444;
|
||||
font-family: ui-monospace, Menlo, monospace;
|
||||
}
|
||||
|
||||
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
|
||||
.chip {
|
||||
background: #f1f1ef; border-radius: 999px;
|
||||
padding: 4px 10px; font-size: 11px; color: #333;
|
||||
}
|
||||
.dark .chip { background: #1c1c20; color: #ddd; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<section class="slide" id="map">
|
||||
<div class="kicker">LAZYBOY · 怎麼轉起來</div>
|
||||
<h1>其實只有三個角色</h1>
|
||||
<p class="sub">不是一疊伺服器名詞。你說話、中間有人幫忙想、右邊那台電腦去按。</p>
|
||||
<div class="row">
|
||||
<article class="card">
|
||||
<div class="viz" style="background:#0d0d0e">
|
||||
<div class="chat-mini">
|
||||
<div class="b me">把這堂課看完</div>
|
||||
<div class="b them">好,我去開電腦點 Next。</div>
|
||||
<div class="think-row">右側還能看見它的桌面</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="copy">
|
||||
<div class="face">
|
||||
<svg class="blob" viewBox="0 0 64 64"><circle cx="32" cy="32" r="28" fill="#f1f1ef"/><circle cx="32" cy="26" r="8" fill="#c8c8cc"/><path d="M18 46c3-8 9-12 14-12s11 4 14 12" fill="#c8c8cc"/></svg>
|
||||
<div><h2>你</h2><span class="pill">瀏覽器</span></div>
|
||||
</div>
|
||||
<p>聊天、看桌面、必要時接手滑鼠。分頁開著,電腦就不會睡著。</p>
|
||||
</div>
|
||||
</article>
|
||||
<div class="chev">→</div>
|
||||
<article class="card dark">
|
||||
<div class="viz" style="background:#0a0a0c;display:grid;place-items:center">
|
||||
<div style="text-align:center">
|
||||
<svg width="72" height="72" viewBox="0 0 64 64"><path fill="#3ec5a8" d="M33 5c11.5 1 24 11 23 26-1 14-11 27-25 28S6 47 7 31 21 4 33 5z"/><ellipse cx="25.5" cy="29" rx="4.2" ry="5" fill="#1b1b22"/><ellipse cx="40.5" cy="29" rx="4.2" ry="5" fill="#1b1b22"/><circle cx="26.8" cy="27.2" r="1.3" fill="#f3f3f6"/><circle cx="41.8" cy="27.2" r="1.3" fill="#f3f3f6"/></svg>
|
||||
<div class="chips" style="justify-content:center">
|
||||
<span class="chip">記憶</span><span class="chip">技能</span><span class="chip">要不要開機</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="copy">
|
||||
<div class="face">
|
||||
<div><h2>LazyBoy</h2><span class="pill">想、記、叫人做事</span></div>
|
||||
</div>
|
||||
<p>把話留給模型、把檔案留給磁碟。問候不會無故開機。</p>
|
||||
</div>
|
||||
</article>
|
||||
<div class="chev">→</div>
|
||||
<article class="card">
|
||||
<div class="viz">
|
||||
<div class="desk">
|
||||
<div class="win">
|
||||
<div class="win-bar"><i></i><i></i><i></i> Chromium</div>
|
||||
<div class="addr">star.example / course / 12</div>
|
||||
<div class="page-body">
|
||||
<h3>Workplace Safety</h3>
|
||||
<p>單元 12/18</p>
|
||||
<div class="video"></div>
|
||||
<div class="progress"><b></b></div>
|
||||
<div class="next">Next</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panelbar"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="copy">
|
||||
<div class="face">
|
||||
<svg class="blob" viewBox="0 0 64 64"><rect x="8" y="12" width="48" height="36" rx="6" fill="#111113"/><rect x="14" y="18" width="36" height="20" fill="#3ec5a8" opacity=".35"/><rect x="20" y="50" width="24" height="4" rx="2" fill="#ccc"/></svg>
|
||||
<div><h2>它的電腦</h2><span class="pill">真的 Linux 桌面</span></div>
|
||||
</div>
|
||||
<p>Debian、視窗、瀏覽器。家目錄在你硬碟上,不在別人雲裡。</p>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="slide" id="chat">
|
||||
<div class="kicker">傳一句話之後</div>
|
||||
<h1>它先決定:聊就好,還是要動手</h1>
|
||||
<p class="sub">同一條路走完。差別只在第四步要不要打開那台電腦。</p>
|
||||
<div class="col5">
|
||||
<article class="card">
|
||||
<div class="viz" style="background:#0d0d0e">
|
||||
<div class="chat-mini">
|
||||
<div class="b me">哈囉</div>
|
||||
<div class="b me" style="opacity:.45">把課看完</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="copy"><div class="n">1</div><h2>你送出</h2><p>訊息進這則對話。同一句話重送不會變兩則。</p></div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<div class="viz" style="background:#f7f7f8">
|
||||
<div class="stack">
|
||||
<div class="job on"><span class="dot"></span>現在這則</div>
|
||||
<div class="job wait">下一則等著</div>
|
||||
<div class="job wait">再下一則</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="copy"><div class="n">2</div><h2>排隊</h2><p>這個 Agent 一次只做一件事。忙完才輪到下一則。</p></div>
|
||||
</article>
|
||||
<article class="card dark">
|
||||
<div class="viz" style="background:#0a0a0c;display:grid;place-items:center">
|
||||
<div style="text-align:center;color:#9a9aa2;font-size:12px;line-height:1.6">
|
||||
<svg width="56" height="56" viewBox="0 0 64 64"><path fill="#3ec5a8" d="M33 5c11.5 1 24 11 23 26-1 14-11 27-25 28S6 47 7 31 21 4 33 5z"/><ellipse cx="25.5" cy="29" rx="4.2" ry="5" fill="#1b1b22"/><ellipse cx="40.5" cy="29" rx="4.2" ry="5" fill="#1b1b22"/><circle cx="26.8" cy="27.2" r="1.3" fill="#f3f3f6"/><circle cx="41.8" cy="27.2" r="1.3" fill="#f3f3f6"/></svg>
|
||||
<div>記憶 · 技能 · 你剛說的</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="copy"><div class="n">3</div><h2>問模型</h2><p>帶上記憶、技能、你剛說的話。金鑰用你自己的。</p></div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<div class="viz" style="background:#f7f7f8">
|
||||
<div class="fork">
|
||||
<div class="lane a">哈囉 → 直接回你</div>
|
||||
<div class="lane b">「把課看完」→ 開電腦再點</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="copy"><div class="n">4</div><h2>兩條岔路</h2><p>問候不開機。真的要動手,才叫醒那台 Debian。</p></div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<div class="viz">
|
||||
<div class="desk">
|
||||
<div class="win">
|
||||
<div class="win-bar"><i></i><i></i><i></i> Chromium</div>
|
||||
<div class="addr">star.example / course / 13</div>
|
||||
<div class="page-body">
|
||||
<h3>Quiz</h3>
|
||||
<p>右側畫面跟著動</p>
|
||||
<div class="video"></div>
|
||||
<div class="progress"><b style="width:78%"></b></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panelbar"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="copy"><div class="n">5</div><h2>寫回來</h2><p>回覆出現在聊天裡。右側畫面跟著動。</p></div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="slide" id="sleep">
|
||||
<div class="kicker">電腦的作息</div>
|
||||
<h1>開著、小睡、關機</h1>
|
||||
<p class="sub">分頁還在看,它就醒著。沒人看才睡,醒來幾乎不用等。</p>
|
||||
<div class="col3">
|
||||
<article class="card dark">
|
||||
<div class="viz">
|
||||
<div class="desk">
|
||||
<div class="win">
|
||||
<div class="win-bar"><i></i><i></i><i></i> Chromium</div>
|
||||
<div class="addr">你還在這個分頁</div>
|
||||
<div class="page-body">
|
||||
<h3>熱機中</h3>
|
||||
<p>每兩秒打一次招呼</p>
|
||||
<div class="video"></div>
|
||||
<div class="progress"><b></b></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panelbar"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="copy">
|
||||
<div class="n">1</div>
|
||||
<h2>開著</h2>
|
||||
<p>記憶體佔著,畫面隨時能進。</p>
|
||||
<div class="chips"><span class="chip">心跳</span><span class="chip">執行中</span></div>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<div class="viz">
|
||||
<div class="desk dim">
|
||||
<div class="win">
|
||||
<div class="win-bar"><i></i><i></i><i></i> Chromium</div>
|
||||
<div class="addr">離開約 10 分鐘</div>
|
||||
<div class="page-body">
|
||||
<h3>凍結</h3>
|
||||
<p>行程還在記憶體裡</p>
|
||||
<div class="video"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panelbar"></div>
|
||||
</div>
|
||||
<div class="zzz">Zzz</div>
|
||||
</div>
|
||||
<div class="copy">
|
||||
<div class="n">2</div>
|
||||
<h2>小睡</h2>
|
||||
<p>像筆電合蓋。下次用,大約一秒內醒來。</p>
|
||||
<div class="chips"><span class="chip">凍結</span><span class="chip">不要重開桌面</span></div>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<div class="viz">
|
||||
<div class="desk off">
|
||||
<div class="house">
|
||||
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="#3ec5a8" stroke-width="1.6"><path d="M4 11.5 12 5l8 6.5V20H4z"/><path d="M9 20v-6h6v6"/></svg>
|
||||
<div>家目錄還在</div>
|
||||
</div>
|
||||
<div class="panelbar"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="copy">
|
||||
<div class="n">3</div>
|
||||
<h2>關機</h2>
|
||||
<p>約六小時後才真正停掉,把那 2 GB 還你。</p>
|
||||
<div class="chips"><span class="chip">停機</span><span class="chip">家目錄還在</span></div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="slide" id="look">
|
||||
<div class="kicker">它怎麼「看」螢幕</div>
|
||||
<h1>你看乾淨的,它看有編號的</h1>
|
||||
<p class="sub">模型從來不連你的 VNC。它拿一張蓋了黃字的截圖,點編號,不是猜像素。</p>
|
||||
<div class="split">
|
||||
<article class="card">
|
||||
<div class="copy" style="padding-bottom:8px"><h2>你看到的</h2><p>右側預覽,就是那台 Linux 桌面。沒有黃框、沒有編號。</p></div>
|
||||
<div class="viz" style="min-height:360px">
|
||||
<div class="desk">
|
||||
<div class="win">
|
||||
<div class="win-bar"><i></i><i></i><i></i> Chromium</div>
|
||||
<div class="addr">star.example / course / 12</div>
|
||||
<div class="page-body">
|
||||
<h3>Workplace Safety</h3>
|
||||
<p>單元 12/18 · 看完按 Next</p>
|
||||
<div class="video"></div>
|
||||
<ul class="lessons">
|
||||
<li>11. 防護用具</li>
|
||||
<li class="on">12. 現場巡視</li>
|
||||
<li>13. 測驗</li>
|
||||
</ul>
|
||||
<div class="progress"><b></b></div>
|
||||
<div class="next">Next</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panelbar"></div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card dark">
|
||||
<div class="copy" style="padding-bottom:8px"><h2>模型看到的</h2><p>同一瞬間多一層黃字。它說「點 3」,就是點那個按鈕。</p></div>
|
||||
<div class="viz" style="min-height:360px">
|
||||
<div class="desk">
|
||||
<div class="win">
|
||||
<div class="win-bar"><i></i><i></i><i></i> Chromium</div>
|
||||
<div class="addr">star.example / course / 12</div>
|
||||
<div class="page-body">
|
||||
<h3>Workplace Safety</h3>
|
||||
<p>單元 12/18 · 看完按 Next</p>
|
||||
<div class="video"></div>
|
||||
<ul class="lessons">
|
||||
<li>11. 防護用具</li>
|
||||
<li class="on">12. 現場巡視</li>
|
||||
<li>13. 測驗</li>
|
||||
</ul>
|
||||
<div class="progress"><b></b></div>
|
||||
<div class="next">Next<span class="tag">3</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panelbar"></div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="slide" id="click">
|
||||
<div class="kicker">動手的三種方式</div>
|
||||
<h1>能認控制項,就不要猜座標</h1>
|
||||
<p class="sub">網頁走瀏覽器、對話框走無障礙樹,實在沒編號才點像素。</p>
|
||||
<div class="col3">
|
||||
<article class="card dark">
|
||||
<div class="viz">
|
||||
<div class="desk">
|
||||
<div class="win">
|
||||
<div class="win-bar"><i></i><i></i><i></i> Chromium</div>
|
||||
<div class="addr">mail.example / inbox</div>
|
||||
<div class="page-body">
|
||||
<h3>收件匣</h3>
|
||||
<div class="mail">
|
||||
<div class="m on">訓練系統 · 請完成單元 12</div>
|
||||
<div class="m">HR · 本週排班</div>
|
||||
<div class="m">IT · 密碼即將到期</div>
|
||||
</div>
|
||||
<div class="btns">
|
||||
<div class="next" style="width:78px">Compose<span class="tag">1</span></div>
|
||||
<div class="next" style="width:64px;background:#444">Reply<span class="tag">2</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panelbar"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="copy">
|
||||
<div class="n">1</div>
|
||||
<h2>網頁</h2>
|
||||
<p>Chromium 裡的按鈕、輸入框。點 id,會自己捲到看不見的地方。</p>
|
||||
<div class="chips"><span class="chip">優先</span></div>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<div class="viz" style="background:#2a3340">
|
||||
<div class="dialog">
|
||||
<h4>開啟檔案</h4>
|
||||
<div class="file">report.pdf</div>
|
||||
<div class="file on">syllabus.pdf</div>
|
||||
<div class="file">notes.txt</div>
|
||||
<div class="dlg-btn">開啟<span class="tag">4</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="copy">
|
||||
<div class="n">2</div>
|
||||
<h2>桌面視窗</h2>
|
||||
<p>檔案選取、系統對話框。用無障礙名稱,不是用視窗外框。</p>
|
||||
<div class="chips"><span class="chip">原生 GUI</span></div>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<div class="viz" style="background:#fff">
|
||||
<div class="canvas">
|
||||
<div class="cross"></div>
|
||||
<div class="xy">640, 400</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="copy">
|
||||
<div class="n">3</div>
|
||||
<h2>座標</h2>
|
||||
<p>畫布、沒有名字的控制項。最後才用。畫面契約是 1280×800。</p>
|
||||
<div class="chips"><span class="chip">不得已</span></div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="slide" id="teach">
|
||||
<div class="kicker">教會它</div>
|
||||
<h1>你做一次,它記住「為什麼」</h1>
|
||||
<p class="sub">不是錄巨集。它記你點了哪個控制項、填了什麼、去了哪一頁,下次在活的畫面上自己找。</p>
|
||||
<div class="col4">
|
||||
<article class="card">
|
||||
<div class="viz">
|
||||
<div class="desk">
|
||||
<div class="win">
|
||||
<div class="win-bar"><i></i><i></i><i></i> 你在操作</div>
|
||||
<div class="addr">star.example / course / 12</div>
|
||||
<div class="page-body">
|
||||
<h3>Workplace Safety</h3>
|
||||
<p>你正在示範</p>
|
||||
<div class="video"></div>
|
||||
<div class="next">Next<span class="tag">3</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panelbar"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="copy"><div class="n">1</div><h2>你示範</h2><p>畫面交給你,用平常的方式做完。密碼欄不會被記。</p></div>
|
||||
</article>
|
||||
<article class="card dark">
|
||||
<div class="viz" style="background:#0a0a0c">
|
||||
<div class="events">
|
||||
<div class="ev">click <em>Next</em></div>
|
||||
<div class="ev">fill <em>姓名</em></div>
|
||||
<div class="ev">goto <em>/quiz</em></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="copy"><div class="n">2</div><h2>它在旁邊看</h2><p>記語意事件。畫面沒變就不存,避免一堆廢幀。</p></div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<div class="viz" style="background:#eef2ee">
|
||||
<div class="playbook">
|
||||
<b>技能草稿</b>
|
||||
目標:看完課並交測驗<br/>
|
||||
輸入:學員姓名<br/>
|
||||
步驟:找到 Next → 填表 → 送出<br/>
|
||||
完成:出現分數
|
||||
<div style="margin-top:12px;color:#0f766e;font-size:11px">可改名 · 試跑 · 匯出 JSON</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="copy"><div class="n">3</div><h2>整理成技能</h2><p>目標、可變輸入、步驟、怎麼算做完。你可以改名字再存。</p></div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<div class="viz">
|
||||
<div class="desk">
|
||||
<div class="win">
|
||||
<div class="win-bar"><i></i><i></i><i></i> 下次自己做</div>
|
||||
<div class="addr">star.example / course / 3</div>
|
||||
<div class="page-body">
|
||||
<h3>另一堂課</h3>
|
||||
<p>在現在的畫面上找 Next</p>
|
||||
<div class="video"></div>
|
||||
<div class="next">Next<span class="tag">2</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panelbar"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="copy"><div class="n">4</div><h2>下次自己做</h2><p>找「Next」,不是重播上次的座標。</p></div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="slide" id="schedule">
|
||||
<div class="kicker">排程</div>
|
||||
<h1>到點以後,跟你傳訊息同一條路</h1>
|
||||
<p class="sub">不是另一套引擎。鬧鐘響了,就塞進同一個工作隊列。</p>
|
||||
<div class="row">
|
||||
<article class="card">
|
||||
<div class="viz" style="background:#0d0d0e">
|
||||
<div class="chat-mini">
|
||||
<div class="b me">每天九點交報告</div>
|
||||
<div class="think-row">對話裡講,或側欄自己建</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="copy"><div class="n">1</div><h2>你說「每天九點」</h2><p>五欄時間,預設台北。</p></div>
|
||||
</article>
|
||||
<div class="chev">→</div>
|
||||
<article class="card">
|
||||
<div class="viz" style="background:#f7f7f8">
|
||||
<div class="alarm">
|
||||
<svg class="clock" viewBox="0 0 120 120">
|
||||
<circle cx="60" cy="60" r="52" fill="#fff" stroke="#ececef" stroke-width="4"/>
|
||||
<circle cx="60" cy="60" r="4" fill="#111"/>
|
||||
<line x1="60" y1="60" x2="60" y2="28" stroke="#111" stroke-width="4" stroke-linecap="round"/>
|
||||
<line x1="60" y1="60" x2="88" y2="60" stroke="#3ec5a8" stroke-width="4" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<div class="cron">每天 09:00</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="copy"><div class="n">2</div><h2>鬧鐘記住</h2><p>到點前它只是一列時間。按「立刻跑」也不會把鬧鐘撥亂。</p></div>
|
||||
</article>
|
||||
<div class="chev">→</div>
|
||||
<article class="card dark">
|
||||
<div class="viz" style="background:#0a0a0c">
|
||||
<div class="stack">
|
||||
<div class="job on"><span class="dot"></span>09:00 交報告</div>
|
||||
<div class="job wait">你剛傳的那則</div>
|
||||
<div class="job wait">下一則等著</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="copy"><div class="n">3</div><h2>變成普通工作</h2><p>跟你剛傳的那則訊息一樣:排隊、問模型、必要時開電腦。</p></div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="slide" id="keys">
|
||||
<div class="kicker">模型金鑰</div>
|
||||
<h1>由近到遠,找到第一把就用</h1>
|
||||
<p class="sub">不管滑鼠。只決定這次要問哪一家模型。</p>
|
||||
<div class="col3">
|
||||
<article class="card dark">
|
||||
<div class="viz" style="background:#0a0a0c">
|
||||
<div class="form dark">
|
||||
<label>這個機器人 · 模型金鑰</label>
|
||||
<div class="field">xai-••••••••••••<span class="ok">用這把</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="copy"><div class="n">1</div><h2>這個機器人</h2><p>它自己若存了 key,用它的。適合不同 Agent 接不同家。</p></div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<div class="viz" style="background:#f7f7f8">
|
||||
<div class="form">
|
||||
<label>本機工作區 · 設定</label>
|
||||
<div class="field" style="color:#999">尚未填寫</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="copy"><div class="n">2</div><h2>工作區設定</h2><p>本機工作區裡填的。畫面上存的會蓋過環境變數。</p></div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<div class="viz" style="background:#f7f7f8">
|
||||
<div class="form">
|
||||
<label>.env</label>
|
||||
<div class="env-lines">XAI_API_KEY=…<br/>OPENAI_API_KEY=</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="copy"><div class="n">3</div><h2>環境變數</h2><p>什麼都沒填才落到這裡。</p></div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="slide" id="folders">
|
||||
<div class="kicker">想改程式時</div>
|
||||
<h1>把它當成幾間房間,不是分層蛋糕</h1>
|
||||
<p class="sub">從你看得見的畫面往裡走。不必先背 crate 依賴圖。</p>
|
||||
<div class="rooms">
|
||||
<div class="room" style="background:#111113;grid-row:1 / span 2">
|
||||
<div class="path">apps/web</div>
|
||||
<h2>畫面</h2>
|
||||
<div class="tiny-ui">
|
||||
<div style="padding:10px 8px">
|
||||
<div style="height:8px;width:72%;background:#2a2a2e;border-radius:4px;margin-bottom:10px"></div>
|
||||
<div style="height:28px;background:#222;border-radius:8px;margin-bottom:8px"></div>
|
||||
<div style="height:28px;background:#1a1a1d;border-radius:8px"></div>
|
||||
</div>
|
||||
<div style="padding:12px 10px;display:flex;flex-direction:column;gap:8px">
|
||||
<div style="height:8px;width:36%;background:#2a2a2e;border-radius:4px"></div>
|
||||
<div style="height:26px;width:72%;background:#f1f1ef;border-radius:10px;margin-left:auto"></div>
|
||||
<div style="height:26px;width:58%;background:#19191c;border-radius:10px"></div>
|
||||
<div style="height:26px;width:64%;background:#f1f1ef;border-radius:10px;margin-left:auto"></div>
|
||||
<div style="flex:1"></div>
|
||||
<div style="height:28px;border:1px solid #2a2a2e;border-radius:14px"></div>
|
||||
</div>
|
||||
<div style="background:#101012"></div>
|
||||
</div>
|
||||
<small>聊天、電腦預覽、頭像、排程表單。中文在 locales。VNC 在 vnc.html。</small>
|
||||
</div>
|
||||
<div class="room" style="background:#0f766e">
|
||||
<div>
|
||||
<div class="path">crates/api</div>
|
||||
<h2>對話與工作</h2>
|
||||
</div>
|
||||
<small>送訊息、工具、開機、技能、排程、保險箱。加工具從 tools.rs 開始。</small>
|
||||
</div>
|
||||
<div class="room" style="background:#6a6bf5">
|
||||
<div>
|
||||
<div class="path">crates/control</div>
|
||||
<h2>怎麼點</h2>
|
||||
</div>
|
||||
<small>編號、CDP、無障礙樹、滑鼠。桌面上的手。</small>
|
||||
</div>
|
||||
<div class="room" style="background:#f5a03c;color:#1b1206">
|
||||
<div>
|
||||
<div class="path">supervisor + image/computer</div>
|
||||
<h2>怎麼開機</h2>
|
||||
</div>
|
||||
<small>容器的生老病死,以及 XFCE/Chromium 長什麼樣。</small>
|
||||
</div>
|
||||
<div class="room" style="background:#d9508a">
|
||||
<div>
|
||||
<div class="path">crates/harness</div>
|
||||
<h2>問哪一家模型</h2>
|
||||
</div>
|
||||
<small>金鑰從哪來、打哪一個網址。不管滑鼠。</small>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
const id = new URLSearchParams(location.search).get("p") || "map";
|
||||
document.querySelectorAll(".slide").forEach((el) => {
|
||||
el.classList.toggle("on", el.id === id);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
Before Width: | Height: | Size: 315 KiB |
|
Before Width: | Height: | Size: 225 KiB |
|
Before Width: | Height: | Size: 180 KiB |
|
Before Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 453 KiB |
|
Before Width: | Height: | Size: 340 KiB |
|
Before Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 427 KiB |
|
Before Width: | Height: | Size: 455 KiB |
|
|
@ -360,11 +360,11 @@
|
|||
</svg>
|
||||
LazyBoy
|
||||
</div>
|
||||
<div class="pill"><b>本機開源</b> 每個 Agent 有自己的 Linux 桌面</div>
|
||||
<div class="pill"><b>本機自託管</b> 每個 Agent 有自己的 Linux 桌面</div>
|
||||
</div>
|
||||
<div class="copy">
|
||||
<h1>給 Agent 一台真的電腦</h1>
|
||||
<p class="lead">在瀏覽器裡開機器人。它會自己開網頁、敲指令、學你示範過的流程。<br/>金鑰、模型、桌面都在你這台機器上。</p>
|
||||
<p class="lead">在瀏覽器裡開機器人。它會自己開網頁、敲指令、學你示範過的流程。<br/>對話與桌面資料留在你的主機,模型連線由你選擇。</p>
|
||||
</div>
|
||||
<div class="window">
|
||||
<div class="chrome"><i class="dot r"></i><i class="dot y"></i><i class="dot g"></i></div>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 263 KiB After Width: | Height: | Size: 268 KiB |
|
|
@ -1,62 +0,0 @@
|
|||
# 安全、Harness 與 Docker 檢查
|
||||
|
||||
日期:2026-09-05。檢查第一方 Rust workspace、前端、控制腳本及容器設定;保留工作目錄原有修改。這是程式與依賴檢查,不是完整滲透測試;reference 第三方專案未逐行稽核。
|
||||
|
||||
## 已修正的安全問題
|
||||
|
||||
| 風險 | 問題與修正 | 主要位置 |
|
||||
| --- | --- | --- |
|
||||
| 高 | 桌面原本持有 supervisor 主 token;改為依 home key 衍生的 HMAC token,管理路由驗證容器歸屬 | crates/supervisor/src/docker.rs、main.rs |
|
||||
| 高 | 可提供任意 home 路徑及透過 symlink 越界;限定 DATA_DIR/homes/key,附件改用 cap-std 目錄能力讀寫 | supervisor/docker.rs、api/attachments.rs |
|
||||
| 高 | 已儲存密碼可能填入不相符網站;現在強制指定 hostname 與 HTTPS,不自動送出;DOM 觀察遮蔽密碼類欄位 | api/tools.rs、control/cdp.py |
|
||||
| 高 | 桌面提供的 JavaScript 可能在 API 同源執行;noVNC 靜態程式改由 API 可信映像提供 | api/screen_proxy.rs、image/api/Dockerfile |
|
||||
| 中 | 固定登入 cookie 無法個別撤銷;改為隨機 session、伺服器期限及登出撤銷,加入 Origin/Fetch Metadata 與本機 Host 檢查 | api/auth.rs |
|
||||
| 中 | MCP 子程序繼承服務秘密;改為環境白名單及明確設定 | api/mcp.rs |
|
||||
| 中 | 工具輸出可能進入記錄;移除輸出摘錄;附件隨機儲存名稱避免覆寫 | api/runs.rs、attachments.rs |
|
||||
|
||||
Session 目前放在單一 API 記憶體,重啟失效;不適用多副本共用登入。已建立的 WebSocket 不會因 cookie 撤銷立即關閉。MCP 仍是同 UID 的可信子程序,環境白名單不等於作業系統沙箱。
|
||||
|
||||
## Harness 與電腦控制
|
||||
|
||||
- 修正 observe 對 POST-only 端點誤用 GET,消除正常路徑每次失敗再 fallback。
|
||||
- 移除有副作用操作失敗後的自動重播;執行前落盤 toolsStarted,完成批次保存上下文 checkpoint。worker 在不確定操作是否執行的狀態中斷,會標記失敗而非重做。
|
||||
- 模型暫時失敗採有限次重試與退避;各工具及程序有期限與輸出上限。Checkpoint 移除截圖並限制 1 MiB;超限保留不確定狀態以避免盲目重播。這不是 exactly-once 保證。
|
||||
- 使用 websocket-client 處理 CDP framing、控制訊框與 timeout;限制 Chromium 除錯介面為 loopback,移除 wildcard Origin。
|
||||
- DOM snapshot 使用每次唯一 selector,降低舊觀察誤點新元素風險;點擊前檢查可見、啟用、遮擋,動作後等待有限畫面更新。
|
||||
- 指標/視窗觀察並行;終端剪貼簿使用正確快捷鍵,確認 X11 文字一致後再貼上。
|
||||
- 排程以交易與 SKIP LOCKED 避免多 worker 重複派發,入列與下次時間一併提交。Unix 星期轉換已有回歸測試。DOM 與 DOW 同時受限的 cron 明確拒絕,避免不同 cron 引擎的 OR/AND 語意差異。
|
||||
|
||||
這些改動減少多餘往返與重播風險,尚未進行真實桌面延遲 benchmark,沒有速度倍數保證。DOM 優先、需要時使用桌面操作仍是本專案適合的路徑;沒有為了換框架重寫整個控制層。
|
||||
|
||||
## Docker 改善與升級
|
||||
|
||||
已加入 cargo/npm 建置 cache、cargo --locked、npm ci、直接 MCP 套件版本固定、API 非 root、capabilities 限縮、健康檢查、restart/init、程序與日誌上限。資料庫/控制網路獨立;預設只在 127.0.0.1:3101 開放 API,Postgres 不對主機映射。開發資料庫使用 make postgres 的額外 Compose 設定。
|
||||
|
||||
新安裝執行 make env,產生四組獨立秘密與隨機資料庫密碼,檔案權限 0600。既有 .env 不會被覆寫。
|
||||
|
||||
既有安裝更新時:
|
||||
|
||||
1. 備份資料庫、data 與 .env。此次僅建置映像,沒有重啟你的正式服務、旋轉秘密或刪除既有桌面。
|
||||
2. 保留原本 vault key。若之前未設定 LAZYBOY_VAULT_KEY,先把原本 LAZYBOY_APP_TOKEN 的值保存為 LAZYBOY_VAULT_KEY,才能旋轉 app token;否則舊密碼可能無法解密。
|
||||
3. 舊桌面曾取得 supervisor 主 token,更新時應更換 LAZYBOY_SUPERVISOR_TOKEN 並重建所有舊桌面容器(保留 home 資料)。新版 controlVersion 會使舊容器在重新 provision 時重建。
|
||||
4. API 改為 UID 1000。檢查既有 API 資料與快取目錄是否可由 UID 1000 存取;只調整確定需要的目錄,勿遞迴改動所有使用者 home。HOST_DATA_DIR 必須對應正確的主機資料目錄。
|
||||
5. 既有 Postgres volume 不會因改 .env 自動換密碼;密碼輪替須同時更新資料庫角色與連線設定。API 重啟後重新登入。
|
||||
|
||||
Supervisor 仍掌握 Docker socket,可控制 Docker 主機;cap_drop 不能消除此權限。較強隔離方案是專用 Docker daemon/VM。桌面 Chromium 既有 --no-sandbox 與可信 MCP 的執行邊界仍需納入威脅模型。基底映像尚未固定 digest,未做完整 OS image CVE 掃描;直接套件固定版本不代表所有下載資產都可完全重現。
|
||||
|
||||
## 驗證與剩餘告警
|
||||
|
||||
- Rust workspace:114 個測試通過,含隔離 PostgreSQL 測試;前端 Node 5 個、Python 3 個測試通過。
|
||||
- 前端 typecheck、production build、程式碼 diff 空白檢查(不含 README 的 Markdown 換行空白)、Compose config 驗證通過;三個 Docker 映像實際建置成功。
|
||||
- 隔離 API smoke test:未登入拒絕、跨站登入拒絕、合法登入可存取 API、可信 noVNC asset 可用、登出後舊 cookie 被拒絕;均使用最後建置的映像驗證。
|
||||
- npm audit:0。cargo-audit:RUSTSEC-2023-0071(rsa 0.9.10,無修補版本)仍在 lockfile,但目前啟用依賴樹 cargo tree -i rsa 無結果;不能把未使用的 lockfile 告警說成已移除。
|
||||
- paste 1.0.15 有停止維護告警 RUSTSEC-2024-0436,由 fastembed/影像相關上游引入,仍需追蹤替代版本。
|
||||
- 未完成瀏覽器視覺驗收、真實 VNC 剪貼簿端到端、故障注入/負載測試。Lottie eval 提醒仍存在。
|
||||
|
||||
## 方法參考
|
||||
|
||||
- [Docker 安全與 daemon 信任邊界](https://docs.docker.com/engine/security/)、[Docker socket 保護](https://docs.docker.com/engine/security/protect-access/)、[建置快取](https://docs.docker.com/build/cache/optimize/)
|
||||
- [Playwright actionability](https://playwright.dev/docs/actionability):參考動作前狀態檢查原則,並未導入 Playwright runtime。
|
||||
- [長時間 agent harness](https://www.anthropic.com/engineering/effective-harnesses-for-long-running-agents):持久化進度與恢復的設計參考。
|
||||
- [CDP 協定](https://chromedevtools.github.io/devtools-protocol/)、[websocket-client 用法](https://websocket-client.readthedocs.io/en/latest/examples.html)
|
||||
- [RSA advisory](https://rustsec.org/advisories/RUSTSEC-2023-0071.html)、[paste advisory](https://rustsec.org/advisories/RUSTSEC-2024-0436.html)
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
{
|
||||
"schema_version": 2,
|
||||
"diagram_type": "workflow",
|
||||
"meta": {
|
||||
"title": "LazyBoy:從一句話到完成任務",
|
||||
"quality_profile": "showcase",
|
||||
"views": [
|
||||
{
|
||||
"id": "task-flow",
|
||||
"label": "任務主流程",
|
||||
"focus": ["workspace", "api", "supervisor", "desktop", "result"],
|
||||
"note": "沿著主路徑查看指令如何成為可觀察、可接管的桌面操作。"
|
||||
},
|
||||
{
|
||||
"id": "memory-schedule",
|
||||
"label": "記憶與排程",
|
||||
"focus": ["api", "memory", "schedule"],
|
||||
"note": "長期記憶與排程會在需要時補充情境並重新啟動任務。"
|
||||
},
|
||||
{
|
||||
"id": "safety",
|
||||
"label": "隔離與憑證",
|
||||
"focus": ["vault", "supervisor", "desktop"],
|
||||
"note": "登入資料經加密保存,桌面工作負載在獨立容器中執行。"
|
||||
}
|
||||
]
|
||||
},
|
||||
"lanes": [
|
||||
{ "id": "flow", "label": "任務主流程" },
|
||||
{ "id": "automation", "label": "持續自動化" },
|
||||
{ "id": "data", "label": "資料與安全", "variant": "exception" }
|
||||
],
|
||||
"phases": [
|
||||
{ "id": "intent", "label": "說明目標", "fromCol": 0, "toCol": 1 },
|
||||
{ "id": "orchestrate", "label": "理解與編排", "fromCol": 2, "toCol": 3, "variant": "emphasis" },
|
||||
{ "id": "operate", "label": "執行與回傳", "fromCol": 4, "toCol": 5, "variant": "dashed" }
|
||||
],
|
||||
"groups": [
|
||||
{ "id": "agent_core", "label": "Rust Agent Runtime", "lane": "flow", "fromCol": 1, "toCol": 2, "variant": "emphasis" },
|
||||
{ "id": "sandbox_boundary", "label": "Docker 隔離邊界", "lane": "flow", "fromCol": 2, "toCol": 3, "variant": "security" },
|
||||
{ "id": "durable_context", "label": "排程、記憶與登入資料", "lane": "data", "fromCol": 1, "toCol": 3, "variant": "dashed" }
|
||||
],
|
||||
"mainPath": ["workspace", "api", "supervisor", "desktop", "result"],
|
||||
"semanticChecks": {
|
||||
"allowedRoots": ["workspace", "schedule", "vault"],
|
||||
"allowedTerminals": ["result", "memory"],
|
||||
"requiredPaths": [
|
||||
{ "from": "workspace", "to": "result" },
|
||||
{ "from": "schedule", "to": "desktop" },
|
||||
{ "from": "vault", "to": "desktop" }
|
||||
]
|
||||
},
|
||||
"nodes": [
|
||||
{ "id": "workspace", "lane": "flow", "col": 0, "type": "frontend", "label": "React 工作空間", "sublabel": "對話・電腦・記憶", "tag": "zh-TW / EN", "width": 150 },
|
||||
{ "id": "api", "lane": "flow", "col": 1, "type": "backend", "label": "API + Agent", "sublabel": "模型、工具與技能", "tag": "Rust / Axum", "width": 146 },
|
||||
{ "id": "supervisor", "lane": "flow", "col": 2, "type": "security", "label": "Supervisor", "sublabel": "資源限制與生命週期", "width": 146 },
|
||||
{ "id": "desktop", "lane": "flow", "col": 3, "type": "cloud", "label": "隔離電腦", "sublabel": "瀏覽器、檔案與輸入", "tag": "noVNC", "width": 146 },
|
||||
{ "id": "result", "lane": "flow", "col": 4, "type": "frontend", "label": "即時結果", "sublabel": "畫面、訊息與狀態", "width": 142 },
|
||||
{ "id": "schedule", "lane": "automation", "col": 1, "type": "messagebus", "label": "排程", "sublabel": "到點自動執行", "width": 130 },
|
||||
{ "id": "memory", "lane": "data", "col": 1, "type": "database", "label": "記憶與事件", "sublabel": "PostgreSQL + pgvector", "width": 154 },
|
||||
{ "id": "vault", "lane": "data", "col": 3, "type": "security", "label": "加密憑證庫", "sublabel": "網站登入資料", "width": 142 }
|
||||
],
|
||||
"edges": [
|
||||
{ "id": "workspace-api", "from": "workspace", "to": "api", "label": "HTTP / WebSocket", "variant": "emphasis", "role": "main" },
|
||||
{ "id": "api-supervisor", "from": "api", "to": "supervisor", "label": "受控工具呼叫", "variant": "security", "role": "main" },
|
||||
{ "id": "supervisor-desktop", "from": "supervisor", "to": "desktop", "label": "啟動・暫停・恢復", "variant": "security", "role": "main" },
|
||||
{ "id": "desktop-result", "from": "desktop", "to": "result", "label": "螢幕與事件", "variant": "emphasis", "role": "return" },
|
||||
{ "id": "schedule-api", "from": "schedule", "to": "api", "label": "Cron 觸發", "variant": "dashed", "role": "async" },
|
||||
{ "id": "api-memory", "from": "api", "to": "memory", "label": "檢索與寫入", "variant": "dashed", "role": "branch" },
|
||||
{ "id": "vault-desktop", "from": "vault", "to": "desktop", "label": "僅匹配 HTTPS 網域", "variant": "security", "role": "branch" }
|
||||
],
|
||||
"cards": [
|
||||
{
|
||||
"dot": "emerald",
|
||||
"title": "看得見,也接得回來",
|
||||
"items": [
|
||||
"即時觀看 Agent 桌面與目前步驟",
|
||||
"需要時由使用者接管,再交還給 Agent"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dot": "violet",
|
||||
"title": "從一次任務到持續自動化",
|
||||
"items": [
|
||||
"示範操作可整理成可重用技能",
|
||||
"排程、長期記憶與 MCP 延伸工作能力"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 0f5c4cefd59cdbe440deb7e05fd3f503164a6068
|
||||