feat/flow #10
41
README.md
|
|
@ -111,7 +111,7 @@ Open [http://127.0.0.1:5173](http://127.0.0.1:5173). Tests and the tree layout a
|
||||||
The guides below are currently in Traditional Chinese.
|
The guides below are currently in Traditional Chinese.
|
||||||
|
|
||||||
| Doc | Contents |
|
| Doc | Contents |
|
||||||
| --- | --- |
|
| -------------------------------------------------------- | ---------------------------------------------------- |
|
||||||
| [Architecture](./docs/architecture.md) | Task flow, system architecture, computer lifecycle |
|
| [Architecture](./docs/architecture.md) | Task flow, system architecture, computer lifecycle |
|
||||||
| [Interactive diagram](./docs/workflow.html) | Zoomable, searchable HTML chart; download and open |
|
| [Interactive diagram](./docs/workflow.html) | Zoomable, searchable HTML chart; download and open |
|
||||||
| [Operations](./docs/operations.md) | Resources, env vars, security, site checks, sudo |
|
| [Operations](./docs/operations.md) | Resources, env vars, security, site checks, sudo |
|
||||||
|
|
@ -124,16 +124,45 @@ The guides below are currently in Traditional Chinese.
|
||||||
|
|
||||||
Chats, memory, browser profiles, and encrypted credentials stay on your host. When you use an external model, the prompts, tool results, and screenshots the task needs may still be sent to that provider.
|
Chats, memory, browser profiles, and encrypted credentials stay on your host. When you use an external model, the prompts, tool results, and screenshots the task needs may still be sent to that provider.
|
||||||
|
|
||||||
|
## Acknowledgements
|
||||||
|
|
||||||
|
LazyBoy is mostly other people's software, carefully assembled. The projects we lean on hardest:
|
||||||
|
|
||||||
|
**Special thanks**
|
||||||
|
|
||||||
|
- **[Cua](https://github.com/trycua/cua)** — the Linux desktop driver behind every click, keystroke, and screenshot. We rebuild `cua-driver-rs` v0.23.2 from source with two small patches kept in this repo: one lets the agent cursor wear the bot's own colour, the other swaps the embedded Latin-only badge font for [jf open Huninn](https://github.com/justfont/open-huninn-font) so Chinese renders. Neither patch touches input handling or permissions.
|
||||||
|
- **[hermes-agent](https://github.com/NousResearch/hermes-agent)** — not a dependency, but the reference we kept returning to while tuning how smooth an agent's desktop should feel. The comparison is written up in the [hermes-agent review](./docs/hermes-agent-cua-review.md).
|
||||||
|
|
||||||
|
**Agents and retrieval** — [rig](https://github.com/0xPlaygrounds/rig) · [rmcp](https://github.com/modelcontextprotocol/rust-sdk) · [fastembed-rs](https://github.com/Anush008/fastembed-rs) and [ONNX Runtime](https://github.com/microsoft/onnxruntime), running [paraphrase-multilingual-MiniLM-L12-v2](https://huggingface.co/sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2)
|
||||||
|
|
||||||
|
**Rust** — Tokio · Axum · tower-http · SQLx · reqwest · rustls · Bollard · tracing · aes-gcm and hmac from [RustCrypto](https://github.com/RustCrypto) · cap-std · cron · chrono · uuid · thiserror · dotenvy
|
||||||
|
|
||||||
|
**Web** — React · Vite · TypeScript · [noVNC](https://github.com/novnc/noVNC) · react-markdown with [remark-gfm](https://github.com/remarkjs/remark-gfm) and [remark-breaks](https://github.com/remarkjs/remark-breaks) · [Blobatar](https://github.com/Alain00/blobatar) avatars · [react-useanimations](https://github.com/useAnimations/react-useanimations) icons
|
||||||
|
|
||||||
|
**Data** — [PostgreSQL](https://www.postgresql.org) · [pgvector](https://github.com/pgvector/pgvector)
|
||||||
|
|
||||||
|
**The desktop inside each container** — [Docker](https://www.docker.com) · [Debian](https://www.debian.org) · [XFCE](https://www.xfce.org) · [Chromium](https://www.chromium.org) · [Xvfb](https://www.x.org) · Thunar · [x11vnc](https://github.com/LibVNC/x11vnc) · [websockify](https://github.com/novnc/websockify) · [AT-SPI2](https://gitlab.gnome.org/GNOME/at-spi2-core) · [gosu](https://github.com/tianon/gosu) · [LXCFS](https://github.com/lxc/lxcfs) · git · zsh with [Powerlevel10k](https://github.com/romkatv/powerlevel10k) · htop
|
||||||
|
|
||||||
|
**Type and theme** — [jf open Huninn](https://github.com/justfont/open-huninn-font) (SIL OFL) in the UI and on the agent badge · [Noto CJK](https://github.com/notofonts/noto-cjk), DejaVu, and Liberation as fallbacks · [MesloLGS NF](https://github.com/romkatv/powerlevel10k-media) in the terminal · [Arc Dark](https://github.com/horst3180/arc-theme) and [Papirus](https://github.com/PapirusDevelopmentTeam/papirus-icon-theme) for the look of the room
|
||||||
|
|
||||||
|
Every version above is pinned in `Cargo.lock`, `apps/web/package.json`, and `image/computer/Dockerfile`. LazyBoy is Apache-2.0; each project keeps its own license. Thanks also to the maintainers whose names never make it into a README, and to everyone who files a good bug report.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
|
<img src="./apps/web/public/lazyboy-icon.png" width="72" height="72" alt="LazyBoy" />
|
||||||
|
|
||||||
|
**LazyBoy is free and open source.** If it hands you back an afternoon, a coffee is the nicest way to say so.
|
||||||
|
|
||||||
|
<a href="https://www.buymeacoffee.com/daniel.wang.1993"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me a Coffee" height="48" /></a>
|
||||||
|
|
||||||
|
<img src="https://img.shields.io/badge/license-Apache--2.0-2f9e8f?style=for-the-badge&labelColor=1b1b22" alt="Apache License 2.0" />
|
||||||
|
<img src="https://img.shields.io/badge/release-0.1.0-2f9e8f?style=for-the-badge&labelColor=1b1b22" alt="Release 0.1.0" />
|
||||||
|
<img src="https://img.shields.io/badge/Rust%20%2B%20React-6f9c96?style=for-the-badge&labelColor=1b1b22" alt="Rust and React" />
|
||||||
|
|
||||||
**Daniel Wang** <img src="https://flagcdn.com/w20/tw.png" width="20" alt="Taiwan" />
|
**Daniel Wang** <img src="https://flagcdn.com/w20/tw.png" width="20" alt="Taiwan" />
|
||||||
|
|
||||||
[igs170911@gmail.com](mailto:igs170911@gmail.com)
|
[igs170911@gmail.com](mailto:igs170911@gmail.com) · [Apache License 2.0](./LICENSE)
|
||||||
|
|
||||||
<a href="https://www.buymeacoffee.com/daniel.wang.1993"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me a Coffee" height="50" /></a>
|
|
||||||
|
|
||||||
[Apache License 2.0](./LICENSE)
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
LazyBoy 讓 Agent 在 Docker 裡使用自己的 Linux 桌面,操作瀏覽器、終端與檔案。你可以建立不同的 Agent、在群組中協作,把示範整理成技能,再安排定時執行。
|
LazyBoy 讓 Agent 在 Docker 裡使用自己的 Linux 桌面,操作瀏覽器、終端與檔案。你可以建立不同的 Agent、在群組中協作,把示範整理成技能,再安排定時執行。
|
||||||
|
|
||||||
|
|
@ -122,16 +122,45 @@ npm run dev
|
||||||
|
|
||||||
對話、記憶、瀏覽器設定檔與加密憑證保存在自架主機。使用外部模型時,任務所需的提示詞、工具結果與截圖仍可能傳送給該模型供應商。
|
對話、記憶、瀏覽器設定檔與加密憑證保存在自架主機。使用外部模型時,任務所需的提示詞、工具結果與截圖仍可能傳送給該模型供應商。
|
||||||
|
|
||||||
|
## 特別感謝
|
||||||
|
|
||||||
|
LazyBoy 站在許多開源專案的肩上,下面是我們用到最多、也最想致謝的專案。
|
||||||
|
|
||||||
|
**首要致謝**
|
||||||
|
|
||||||
|
- **[Cua](https://github.com/trycua/cua)**:每一次點擊、鍵盤輸入與截圖,都由它的 Linux 桌面驅動完成。我們從原始碼重建 `cua-driver-rs` v0.23.2,並在本專案內維護兩處小修補:一處讓 Agent 游標套用機器人自己的顏色,另一處把內嵌的純英文徽章字型換成 [jf open Huninn](https://github.com/justfont/open-huninn-font),讓中文能正確顯示。兩處修補都不碰輸入處理與權限邏輯。
|
||||||
|
- **[hermes-agent](https://github.com/NousResearch/hermes-agent)**:它不是相依套件,卻是我們調整操作流暢度時一再回頭參照的對象。對照分析見 [hermes-agent 比較](./docs/hermes-agent-cua-review.md)。
|
||||||
|
|
||||||
|
**Agent 與記憶檢索**:[rig](https://github.com/0xPlaygrounds/rig) · [rmcp](https://github.com/modelcontextprotocol/rust-sdk) · [fastembed-rs](https://github.com/Anush008/fastembed-rs) 與 [ONNX Runtime](https://github.com/microsoft/onnxruntime),嵌入模型為 [paraphrase-multilingual-MiniLM-L12-v2](https://huggingface.co/sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2)
|
||||||
|
|
||||||
|
**Rust**:Tokio · Axum · tower-http · SQLx · reqwest · rustls · Bollard · tracing · [RustCrypto](https://github.com/RustCrypto) 的 aes-gcm 與 hmac · cap-std · cron · chrono · uuid · thiserror · dotenvy
|
||||||
|
|
||||||
|
**前端**:React · Vite · TypeScript · [noVNC](https://github.com/novnc/noVNC) · react-markdown 搭配 [remark-gfm](https://github.com/remarkjs/remark-gfm) 與 [remark-breaks](https://github.com/remarkjs/remark-breaks) · [Blobatar](https://github.com/Alain00/blobatar) 頭像 · [react-useanimations](https://github.com/useAnimations/react-useanimations) 圖示
|
||||||
|
|
||||||
|
**資料**:[PostgreSQL](https://www.postgresql.org) · [pgvector](https://github.com/pgvector/pgvector)
|
||||||
|
|
||||||
|
**容器裡的桌面**:[Docker](https://www.docker.com) · [Debian](https://www.debian.org) · [XFCE](https://www.xfce.org) · [Chromium](https://www.chromium.org) · [Xvfb](https://www.x.org) · Thunar · [x11vnc](https://github.com/LibVNC/x11vnc) · [websockify](https://github.com/novnc/websockify) · [AT-SPI2](https://gitlab.gnome.org/GNOME/at-spi2-core) · [gosu](https://github.com/tianon/gosu) · [LXCFS](https://github.com/lxc/lxcfs) · git · 搭配 [Powerlevel10k](https://github.com/romkatv/powerlevel10k) 的 zsh · htop
|
||||||
|
|
||||||
|
**字型與佈景主題**:介面與 Agent 徽章使用 [jf open Huninn](https://github.com/justfont/open-huninn-font)(SIL OFL)· 備援字型為 [Noto CJK](https://github.com/notofonts/noto-cjk)、DejaVu 與 Liberation · 終端機使用 [MesloLGS NF](https://github.com/romkatv/powerlevel10k-media) · 外觀採用 [Arc Dark](https://github.com/horst3180/arc-theme) 與 [Papirus](https://github.com/PapirusDevelopmentTeam/papirus-icon-theme)
|
||||||
|
|
||||||
|
以上版本都釘選在 `Cargo.lock`、`apps/web/package.json` 與 `image/computer/Dockerfile`。LazyBoy 以 Apache-2.0 釋出,上述每個專案保留各自的授權條款。也謝謝那些名字從不出現在 README 裡的維護者,以及每一位認真回報問題的使用者。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
|
<img src="./apps/web/public/lazyboy-icon.png" width="72" height="72" alt="LazyBoy" />
|
||||||
|
|
||||||
|
**LazyBoy 是免費的開源專案。** 如果它幫你省下一個下午,一杯咖啡是最好的回禮。
|
||||||
|
|
||||||
|
<a href="https://www.buymeacoffee.com/daniel.wang.1993"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="請我喝杯咖啡" height="48" /></a>
|
||||||
|
|
||||||
|
<img src="https://img.shields.io/badge/license-Apache--2.0-2f9e8f?style=for-the-badge&labelColor=1b1b22" alt="Apache License 2.0" />
|
||||||
|
<img src="https://img.shields.io/badge/release-0.1.0-2f9e8f?style=for-the-badge&labelColor=1b1b22" alt="Release 0.1.0" />
|
||||||
|
<img src="https://img.shields.io/badge/Rust%20%2B%20React-6f9c96?style=for-the-badge&labelColor=1b1b22" alt="Rust and React" />
|
||||||
|
|
||||||
**danielwang** <img src="https://flagcdn.com/w20/tw.png" width="20" alt="Taiwan" />
|
**danielwang** <img src="https://flagcdn.com/w20/tw.png" width="20" alt="Taiwan" />
|
||||||
|
|
||||||
[igs170911@gmail.com](mailto:igs170911@gmail.com)
|
[igs170911@gmail.com](mailto:igs170911@gmail.com) · [Apache License 2.0](./LICENSE)
|
||||||
|
|
||||||
<a href="https://www.buymeacoffee.com/daniel.wang.1993"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="請我喝杯咖啡" height="50" /></a>
|
|
||||||
|
|
||||||
[Apache License 2.0](./LICENSE)
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -77,14 +77,11 @@
|
||||||
</svg>
|
</svg>
|
||||||
LazyBoy
|
LazyBoy
|
||||||
</div>
|
</div>
|
||||||
<div class="pill"><b>Self-hosted</b> a Linux desktop per agent</div>
|
<div class="pill"><b id="pill-strong"></b> <span id="pill-rest"></span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="copy">
|
<div class="copy">
|
||||||
<h1>Give an agent a real computer</h1>
|
<h1 id="title"></h1>
|
||||||
<p class="lead">
|
<p class="lead" id="lead"></p>
|
||||||
Assign work by text, voice, or a quick demo, then watch its own Linux desktop do it.<br/>
|
|
||||||
Chats, memory, and desktops stay on your host. You choose the model.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="window">
|
<div class="window">
|
||||||
<div class="chrome"><i class="dot r"></i><i class="dot y"></i><i class="dot g"></i></div>
|
<div class="chrome"><i class="dot r"></i><i class="dot y"></i><i class="dot g"></i></div>
|
||||||
|
|
@ -92,32 +89,63 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
// Every layer is a real capture from scripts/capture-hero.mjs, so the banner
|
// One banner per README language: the frame copy and the screenshot are
|
||||||
// never shows an invented UI. The group view is two layers on purpose:
|
// captured in the same language, so the English README never shows a
|
||||||
// room.png is captured with `--hide .side-card`, which blanks the desktop
|
// Chinese screen and vice versa. ?lang=zh-TW|en.
|
||||||
// panel (an agent's browser holds whatever its owner left open), and
|
const pages = {
|
||||||
// desktop.png drops a neutral one back in at the anchor the same capture
|
en: {
|
||||||
// prints. Captures are 2x shots of a 1480px viewport, so a capture-space
|
pillStrong: 'Self-hosted',
|
||||||
// number becomes hero-space by multiplying by 1184 / 1480 = 0.8.
|
pillRest: 'a Linux desktop per agent',
|
||||||
const layers = {
|
title: 'Give an agent a real computer',
|
||||||
room: [
|
lead: [
|
||||||
{ src: './hero/room.png', x: 0, y: 0, w: 1184, h: 520 },
|
'Assign work by text, voice, or a quick demo, then watch its own Linux desktop do it.',
|
||||||
{ src: './hero/desktop.png', x: 832, y: 0, w: 352, h: 520 },
|
'Chats, memory, and desktops stay on your host. You choose the model.',
|
||||||
],
|
],
|
||||||
agent: [{ src: './hero/agent.png', x: 0, y: 0, w: 1184, h: 520 }],
|
alt: 'LazyBoy group chat routing one message to a single agent, with that agent\'s live desktop on the right',
|
||||||
|
},
|
||||||
|
'zh-TW': {
|
||||||
|
pillStrong: '自架部署',
|
||||||
|
pillRest: '每個 Agent 一台 Linux 桌面',
|
||||||
|
title: '給 Agent 一台真正的電腦',
|
||||||
|
lead: [
|
||||||
|
'用文字、語音或一段示範交代任務,接著看它自己的 Linux 桌面動手做完。',
|
||||||
|
'對話、記憶與桌面都留在你的主機上,模型由你挑選。',
|
||||||
|
],
|
||||||
|
alt: 'LazyBoy 群組對話:訊息由其中一個 Agent 接手回覆,右側是它自己的即時桌面',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
const shot = new URLSearchParams(location.search).get('shot') || 'room';
|
// Real captures only, from scripts/capture-hero.mjs. The group view is two
|
||||||
const host = document.querySelector('.shot');
|
// layers: room-<lang>.png is shot with `--hide .side-card`, which blanks the
|
||||||
(layers[shot] || layers.room).forEach((layer) => {
|
// desktop panel (an agent's browser holds whatever its owner left open), and
|
||||||
|
// desktop-<lang>.png drops a neutral one back in at the anchor that same
|
||||||
|
// capture prints. Captures are 2x shots of a 1480px viewport, so a
|
||||||
|
// capture-space number becomes hero-space by multiplying by 1184/1480 = 0.8.
|
||||||
|
const lang = new URLSearchParams(location.search).get('lang') || 'en';
|
||||||
|
const page = pages[lang] || pages.en;
|
||||||
|
const capture = lang === 'en' ? 'en' : 'zh';
|
||||||
|
const layers = [
|
||||||
|
{ src: `./hero/room-${capture}.png`, x: 0, y: 0, w: 1184, h: 520 },
|
||||||
|
{ src: `./hero/desktop-${capture}.png`, x: 832, y: 0, w: 352, h: 520 },
|
||||||
|
];
|
||||||
|
|
||||||
|
document.documentElement.lang = lang;
|
||||||
|
document.title = `LazyBoy hero (${lang})`;
|
||||||
|
document.getElementById('pill-strong').textContent = page.pillStrong;
|
||||||
|
document.getElementById('pill-rest').textContent = page.pillRest;
|
||||||
|
document.getElementById('title').textContent = page.title;
|
||||||
|
document.getElementById('lead').innerHTML = page.lead.join('<br/>');
|
||||||
|
document.querySelector('.shot').append(
|
||||||
|
...layers.map((layer) => {
|
||||||
const img = document.createElement('img');
|
const img = document.createElement('img');
|
||||||
img.src = layer.src;
|
img.src = layer.src;
|
||||||
img.alt = 'LazyBoy group chat routing one message to an agent on its live desktop';
|
img.alt = page.alt;
|
||||||
img.style.left = `${layer.x}px`;
|
img.style.left = `${layer.x}px`;
|
||||||
img.style.top = `${layer.y}px`;
|
img.style.top = `${layer.y}px`;
|
||||||
img.style.width = `${layer.w}px`;
|
img.style.width = `${layer.w}px`;
|
||||||
img.style.height = `${layer.h}px`;
|
img.style.height = `${layer.h}px`;
|
||||||
host.appendChild(img);
|
return img;
|
||||||
});
|
}),
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 177 KiB |
|
After Width: | Height: | Size: 181 KiB |
|
Before Width: | Height: | Size: 242 KiB After Width: | Height: | Size: 308 KiB |
|
After Width: | Height: | Size: 324 KiB |
|
|
@ -9,6 +9,11 @@
|
||||||
// node scripts/capture-hero.mjs --out docs/hero/room.png --pick 測試聊天
|
// node scripts/capture-hero.mjs --out docs/hero/room.png --pick 測試聊天
|
||||||
// node scripts/capture-hero.mjs --out docs/hero/desktop.png --selector .side-card
|
// node scripts/capture-hero.mjs --out docs/hero/desktop.png --selector .side-card
|
||||||
//
|
//
|
||||||
|
// `--lang` pins the UI language so each README gets a banner and a screenshot
|
||||||
|
// in the same language; the app itself defaults to zh-TW.
|
||||||
|
//
|
||||||
|
// node scripts/capture-hero.mjs --lang en --out docs/hero/room-en.png --pick 測試聊天
|
||||||
|
//
|
||||||
// A capture that goes into the repository must not photograph whatever the
|
// A capture that goes into the repository must not photograph whatever the
|
||||||
// agent's browser happens to have open. `--hide` blanks a region (its layout
|
// agent's browser happens to have open. `--hide` blanks a region (its layout
|
||||||
// box stays, so nothing reflows) and `--anchor` prints where that region sits
|
// box stays, so nothing reflows) and `--anchor` prints where that region sits
|
||||||
|
|
@ -40,6 +45,7 @@ const config = {
|
||||||
out: option('out', 'docs/hero/app.png'),
|
out: option('out', 'docs/hero/app.png'),
|
||||||
pick: option('pick', ''),
|
pick: option('pick', ''),
|
||||||
selector: option('selector', '.app-shell'),
|
selector: option('selector', '.app-shell'),
|
||||||
|
lang: option('lang', 'zh-TW'),
|
||||||
hide: option('hide', ''),
|
hide: option('hide', ''),
|
||||||
anchor: option('anchor', ''),
|
anchor: option('anchor', ''),
|
||||||
width: Number(option('width', 1480)),
|
width: Number(option('width', 1480)),
|
||||||
|
|
@ -49,6 +55,10 @@ const config = {
|
||||||
settle: Number(option('settle', 6000)),
|
settle: Number(option('settle', 6000)),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!['zh-TW', 'en'].includes(config.lang)) {
|
||||||
|
throw new Error(`--lang must be zh-TW or en, got "${config.lang}"`);
|
||||||
|
}
|
||||||
|
|
||||||
function findChrome() {
|
function findChrome() {
|
||||||
const candidates = [
|
const candidates = [
|
||||||
process.env.CHROME,
|
process.env.CHROME,
|
||||||
|
|
@ -204,6 +214,13 @@ try {
|
||||||
sessionId,
|
sessionId,
|
||||||
).then((result) => result.result?.value);
|
).then((result) => result.result?.value);
|
||||||
if (status !== 200) throw new Error(`login failed with HTTP ${status}`);
|
if (status !== 200) throw new Error(`login failed with HTTP ${status}`);
|
||||||
|
// The app reads its locale once, at module init, so pin it before the reload
|
||||||
|
// that mounts the real view.
|
||||||
|
await connection.send(
|
||||||
|
'Runtime.evaluate',
|
||||||
|
{ expression: `localStorage.setItem('lazyboy.locale', ${JSON.stringify(config.lang)})` },
|
||||||
|
sessionId,
|
||||||
|
);
|
||||||
await goto(`${config.base}/`);
|
await goto(`${config.base}/`);
|
||||||
|
|
||||||
if (config.pick) {
|
if (config.pick) {
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,23 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Render docs/hero.html to docs/readme-hero.png
|
# Render docs/hero.html into both README banners
|
||||||
#
|
#
|
||||||
# The banner only frames a real capture (see scripts/capture-hero.mjs), so this
|
# The banner only frames a real capture (see scripts/capture-hero.mjs), so this
|
||||||
# step needs any headless Chromium: CHROME=/path/to/chrome, a browser on PATH,
|
# step needs any headless Chromium: CHROME=/path/to/chrome, a browser on PATH,
|
||||||
# the macOS Brave/Chrome install, or a Playwright download.
|
# the macOS Brave/Chrome install, or a Playwright download.
|
||||||
#
|
#
|
||||||
# scripts/render-readme-hero.sh # the group shot (docs/hero/room.png)
|
# Each language gets its own frame copy and its own screenshot, so the English
|
||||||
# SHOT=agent scripts/render-readme-hero.sh # the single-agent shot
|
# README never shows a Chinese screen and the Chinese README never shows English.
|
||||||
|
#
|
||||||
|
# scripts/render-readme-hero.sh
|
||||||
|
# -> docs/readme-hero.png, docs/readme-hero.zh-TW.png
|
||||||
|
# HERO_LANG=en scripts/render-readme-hero.sh # one language only
|
||||||
|
# HERO_LANG=en OUT=/tmp/preview.png scripts/render-readme-hero.sh
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
root="$(cd "$(dirname "$0")/.." && pwd)"
|
root="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
html="$root/docs/hero.html"
|
html="$root/docs/hero.html"
|
||||||
out="${OUT:-$root/docs/readme-hero.png}"
|
|
||||||
shot="${SHOT:-room}"
|
|
||||||
size="${SIZE:-1280,760}"
|
size="${SIZE:-1280,760}"
|
||||||
|
# Deliberately not LANG, which is the system locale and already set.
|
||||||
|
langs="${HERO_LANG:-en zh-TW}"
|
||||||
|
|
||||||
browser=""
|
browser=""
|
||||||
for candidate in "${CHROME:-}" \
|
for candidate in "${CHROME:-}" \
|
||||||
|
|
@ -34,13 +39,22 @@ if [ -z "$browser" ]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
args=(--headless=new --disable-gpu --hide-scrollbars --force-device-scale-factor=2
|
for lang in $langs; do
|
||||||
|
case "$lang" in
|
||||||
|
zh-TW) name="readme-hero.zh-TW" ;;
|
||||||
|
en) name="readme-hero" ;;
|
||||||
|
*) echo "unknown HERO_LANG: $lang (expected: en, zh-TW)" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
out="${OUT:-$root/docs/$name.png}"
|
||||||
|
args=(--headless=new --disable-gpu --hide-scrollbars --force-device-scale-factor=2
|
||||||
--window-size="$size" --screenshot="$out")
|
--window-size="$size" --screenshot="$out")
|
||||||
# Root needs the sandbox off; nowhere else does.
|
# Root needs the sandbox off; nowhere else does.
|
||||||
[ "$(id -u)" = "0" ] && args+=(--no-sandbox)
|
[ "$(id -u)" = "0" ] && args+=(--no-sandbox)
|
||||||
"$browser" "${args[@]}" "file://$html?shot=$shot"
|
if ! noise="$("$browser" "${args[@]}" "file://$html?lang=$lang" 2>&1)"; then
|
||||||
|
echo "$noise" >&2
|
||||||
python3 - "$out" <<'PY'
|
exit 1
|
||||||
|
fi
|
||||||
|
python3 - "$out" <<'PY'
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import struct, sys
|
import struct, sys
|
||||||
p = Path(sys.argv[1])
|
p = Path(sys.argv[1])
|
||||||
|
|
@ -48,3 +62,4 @@ data = p.read_bytes()
|
||||||
w, h = struct.unpack(">II", data[16:24])
|
w, h = struct.unpack(">II", data[16:24])
|
||||||
print(f"wrote {p} ({w}x{h}, {p.stat().st_size} bytes)")
|
print(f"wrote {p} ({w}x{h}, {p.stat().st_size} bytes)")
|
||||||
PY
|
PY
|
||||||
|
done
|
||||||
|
|
|
||||||