diff --git a/README.md b/README.md index 78a3ed9..5a1aa59 100644 --- a/README.md +++ b/README.md @@ -134,270 +134,85 @@ Blobatar 色塊+眼睛。啟動、喚醒、連線、換手時,預覽左上 --- -## 系統怎麼疊起來 +## 系統怎麼轉起來 -```text -瀏覽器 - ├─ React UI(apps/web) - └─ /view/{bot} → API 畫面代理(cookie)→ 容器 websockify → x11vnc → Xvfb - │ - ▼ - lazyboy-api - ├─ Postgres + pgvector - ├─ runs worker(最多 16 條並行,每個 bot 同時一個) - ├─ computer idle_loop(熱機/凍結/停放) - ├─ schedules tick - ├─ MCP hub - └─ harness:選模型、接金鑰 - │ - ▼ HTTP(SANDBOX_SUPERVISOR_TOKEN) - lazyboy-supervisor :7091 - │ Docker socket - ▼ - lazyboy/computer 容器 - ├─ start.sh → Xvfb :1 + XFCE + x11vnc + websockify - ├─ lazyboy-screen 額外 Team 螢幕 :2…:8 - └─ lazyboy-controld :7070 容器內觀察/動作 - data/homes/ bind 到 /home/lazyboy -``` +![其實只有三個角色:你、LazyBoy、它的電腦](./docs/diagrams/map.png) -Compose 裡 supervisor **不**對主機開埠。API 在容器網路連 `supervisor:7091`。 +Compose 裡 supervisor **不**對主機開埠。API 在容器網路連 `supervisor:7091`。家目錄 `data/homes/` bind 進容器的 `/home/lazyboy`。 --- -## 時序:你送一則訊息 +## 你送一則訊息 -```mermaid -sequenceDiagram - participant U as 瀏覽器 - participant A as API - participant DB as Postgres - participant W as run worker - participant H as harness - participant M as 模型 - participant S as supervisor - participant C as 桌面容器 +![它先決定:聊就好,還是要動手](./docs/diagrams/chat.png) - U->>A: POST /api/sessions/{id}/messages - A->>DB: 寫 user 訊息(clientNonce 去重) - A->>DB: INSERT run status=queued - A-->>U: 202 + 輪詢 messages / computer status +同一 bot 已有進行中的工作時,新訊息會排隊(`queuedBehindActive`)。人正在接管時,後面的話只排隊,思考轉圈不會假裝它還在動。問候路徑會把工具表清空,從源頭避免「哈囉」去開電腦。 - loop 每 200ms - W->>DB: 租下一筆 queued(同 bot 沒有別人在跑) - end - W->>H: resolve_backend(bot → 工作區 → 環境變數) - H-->>W: provider + model + key - W->>M: 系統提示 + 歷史 + 記憶 + 工具定義 - - alt 純聊天(問候、閒聊、知識問答) - M-->>W: 純文字 - W->>DB: 寫 assistant 訊息,run=completed - else 要用電腦 - M-->>W: tool_call(browser / computer_act / shell…) - W->>DB: busy_step(電腦啟動中/喚醒中/實際動作) - W->>A: boot() 若尚未 running - A->>S: provision 或 unpause - S->>C: 等 /tmp/lazyboy/ready - W->>C: 執行工具 - C-->>W: 截圖/DOM/stdout - W->>M: tool result(畫面變了才帶圖) - M-->>W: 下一動或結束文字 - end - W->>DB: completed,放開螢幕租約 - U->>A: GET status/messages(2 秒一次) -``` - -同一 bot 已有 `leased`/`running`/`waiting_takeover` 時,新訊息會 `queuedBehindActive`。人正在接管時,後面的話只排隊,思考轉圈不會假裝它還在動。 +`execute_run` 每一輪:續租約 → 寫步驟文字 → 問模型 → 沒有工具就結束(技能沒過會再把畫面塞回去)→ 有工具且需要沙盒才 boot → 畫面沒變就不重複塞圖。回合上限:聊天 4、一般 40、技能 80。 --- -## 時序:電腦從開機到你看到畫面 +## 電腦的作息 -```mermaid -sequenceDiagram - participant U as 瀏覽器 - participant A as API - participant S as supervisor - participant D as Docker - participant C as 容器 PID 1 - - U->>A: POST /api/computer/{bot}/boot - alt 已是 running 且容器還在 - A-->>U: state=running - else 休眠中(docker pause) - A->>S: POST /computers/{id}/unpause - S->>D: unpause - S->>C: 確認 /tmp/lazyboy/ready(約一秒內) - A-->>U: state=running - else 已關或沒有容器 - A->>DB: state=booting - A->>S: POST /computers - S->>D: 找到可重用的就 start,否則 create - D->>C: start.sh - C->>C: Xvfb :1、XFCE、x11vnc、websockify、controld - C->>C: touch /tmp/lazyboy/ready - A->>S: ensure_screen(Team 再掛 :2…) - A-->>U: state=running - end - U->>A: GET /api/computer/{bot}/screen - A-->>U: /view/{bot}/vnc.html - U->>A: WebSocket /view/{bot}/websockify - A->>C: 轉到該 slot 的 6080+N - Note over U,C: 分頁每 2 秒 heartbeat,更新 computers.updated_at -``` +![開著、小睡、關機](./docs/diagrams/sleep.png) 閒置(`crates/api/src/computer.rs` `idle_loop`): -1. 執行中、超過 10 分鐘沒人看、也沒有進行中的 run/示範 → `docker pause`,狀態 `suspended` -2. 休眠超過 6 小時 → `docker stop`,狀態 `stopped` +1. 執行中、超過 10 分鐘沒人看、也沒有進行中的 run/示範 → `docker pause`,狀態 `suspended` +2. 休眠超過 6 小時 → `docker stop`,狀態 `stopped` 3. 分頁還在就心跳,不會進 1 ---- - -## Harness 流程 - -`crates/harness` 不管滑鼠,只負責**這次 run 要用哪一家模型**。真正的 agent 迴圈在 `crates/api/src/runs.rs`。 - -```mermaid -flowchart TD - A[execute_run] --> B[讀 bot 與工作區設定] - B --> C[CredentialChain] - C --> C1[bot 自己存的 key] - C --> C2[工作區設定的 key] - C --> C3[環境變數 XAI_API_KEY 等] - C1 --> D[resolve_backend] - C2 --> D - C3 --> D - D --> E{provider} - E -->|xai| F[rig xAI CompletionModel] - E -->|opencode-go 且 gpt/grok/muse| G[OpenAI Responses API] - E -->|其他 OpenAI 相容| H[OpenAI Chat Completions] - F --> I[connect_model] - G --> I - H --> I - I --> J[帶工具定義進 complete_once] - J --> K{回傳} - K -->|純文字| L[聊天結束或 skill 檢查沒過就 nudge] - K -->|tool_calls| M[dispatch] - M --> N[結果寫回 history] - N --> J -``` - -金鑰優先順序:**bot → 工作區 → 環境變數**。API 跑在 Docker 時,迴圈位址 `127.0.0.1` 會被改成 `host.docker.internal`,才能打到你本機的相容端點。 - -`execute_run` 每一輪: - -1. 續租約(5 分鐘),否則 halt -2. 寫 `busy_step`(思考中/電腦啟動中/browser click…) -3. `complete_once`:system + 記憶 + 技能目錄 + 歷史 -4. 沒有 tool call:問候就結束;技能 run 若檢查沒過,把現在畫面塞回去再逼一次(最多數次) -5. 有 tool call:需要沙盒才 `prepare_run_computer`(boot/解凍、拿螢幕執行租約、瀏覽器 profile lock) -6. `dispatch` 跑工具,畫面沒變就不重複塞圖 -7. 直到文字結束、halt(停止/接管)、或達到回合上限(聊天 4、一般 40、技能 80) - -問候路徑會把工具表清空,從源頭避免「哈囉」去 `ls` 家目錄。 +開機/喚醒:已在跑就直接回;凍結中就 `unpause`(約一秒);沒有容器才 `provision`,等 `/tmp/lazyboy/ready`。畫面走 `/view/{bot}/vnc.html`,已登入的 cookie 轉到 websockify。 --- -## 控制電腦的原理 +## 它怎麼看、怎麼點 -模型**從不**直接連 VNC。它只打 API 工具;工具經 sandbox HTTP 進 supervisor,再 `docker exec` 或打容器內 `controld`。 +![你看乾淨的,它看有編號的](./docs/diagrams/look.png) -```mermaid -flowchart LR - subgraph 模型側 - T1[browser] - T2[computer_observe / computer_act] - T3[shell / files] - end - subgraph 容器內 - CDP[Chromium CDP] - ATSPI[AT-SPI] - XD[xdotool / Xvfb] - VNC[x11vnc] - end - T1 --> CDP - T2 --> ATSPI - T2 --> XD - T3 --> XD - U[你的瀏覽器] --> VNC -``` +![能認控制項,就不要猜座標](./docs/diagrams/click.png) -**三層找得到什麼、點得了什麼** +模型從不直接連 VNC。它只打 API 工具;工具經 sandbox HTTP 進 supervisor,再 `docker exec` 或打容器內 `controld`。 -1. **CDP(網頁)** - Chromium 開著時,`browser` 拿 DOM:可點的控制項編成 1…N,截圖上蓋黃字。`click {element:N}` 會捲到視窗外的節點。這是訓練系統、信箱、後台的主路徑。`computer_act` 點在瀏覽器視窗上會被拒,避免用像素點網頁。 +編號只畫在給模型的 JPEG 上。VNC 是乾淨桌面。每次 navigation/snapshot 會重編號,舊 id 作廢。`computer_act` 點在瀏覽器視窗上會被拒,避免用像素點網頁。解析度契約是 **1280×800**。 -2. **AT-SPI(原生 GUI)** - 沒有 DOM 時(檔案選取、XFCE 對話框),`computer_observe` 走無障礙樹。編號是控制項,不是視窗外框。 +每個 bot 一個 `computer_screens` 列:slot、DISPLAY、執行租約、控制租約。人接管寫 `control_holder=user`,worker 在回合邊界停,不跟你搶滑鼠。`view_only` 用 postMessage 切,不重掛 iframe,所以換手時預覽不會黑掉。 -3. **座標(最後)** - 畫布、無樹的 widget 才用 `computer_act` 的 x,y。解析度契約是 **1280×800**。 - -**編號從哪來** -`overlay_elements` 只畫在給模型的 JPEG 上。VNC 是乾淨桌面。每次 navigation/snapshot 會重編號,舊 id 作廢。 - -**誰可以動滑鼠** -每個 bot 一個 `computer_screens` 列:slot、DISPLAY、執行租約 `execution_run_id`、控制租約 `control_holder`。run 要 GUI 時 `take_screen_execution` 把 fence +1,較新的 fence 贏。人接管寫 `control_holder=user`,worker 在回合邊界停,不跟你搶滑鼠。 - -**你看到的畫面** -`screen_proxy` 只接受已登入的 GET/WebSocket。頁面本體是 `apps/web/vnc.html`。`view_only` 用 postMessage 切,不重掛 iframe,所以換手時預覽不會黑掉。 - -**容器內 controld** -`lazyboy-controld` 聽 `127.0.0.1:7070`,要 `LAZYBOY_CONTROL_TOKEN`。supervisor 的觀察/動作能打通就走它,否則退回 `import`/`xdotool` 指令。 - -**Team 多螢幕** -slot 0 = `:1` / VNC 5900 / 畫面 6080。slot N = `:N+1` / 5900+N / 6080+N。`lazyboy-screen ensure` 在同一個容器裡再長一組 Xvfb。瀏覽器設定檔預設 per-bot;兩個 bot 搶同一份 shared profile 會拿到「profile locked」,檔案與 shell 仍可用。 +`lazyboy-controld` 聽 `127.0.0.1:7070`。Team 多螢幕:slot 0 = `:1`,slot N = `:N+1`。`lazyboy-screen ensure` 在同一個容器裡再長一組 Xvfb。 --- -## 教技能怎麼做 +## 模型金鑰從哪來 -```mermaid -sequenceDiagram - participant H as 你 - participant A as API - participant C as 桌面 - participant M as 模型 +![由近到遠,找到第一把就用](./docs/diagrams/keys.png) - H->>A: POST /skills/start {goal} - A->>C: 開電腦、把控制權給人 - A->>C: 啟動 CDP recorder - loop 約 1.5s - A->>C: 視窗標題 + 粗略畫面簽名 - Note over A: 簽名沒變就不存,避免閒置把 60 幀用完 - end - H->>C: 正常操作(密碼欄不錄) - H->>A: POST /skills/stop - A->>C: 停 recorder - A->>M: 目標 + 語意事件 + 最多 8 張關鍵畫面 - M-->>A: playbook(意圖、輸入、步驟、怎麼驗收) - A-->>H: 草稿,可改名、試跑、匯出 JSON -``` +`crates/harness` 不管滑鼠,只決定這次 run 要用哪一家模型。真正的 agent 迴圈在 `crates/api/src/runs.rs`。 + +金鑰:**這個機器人 → 工作區設定 → 環境變數**。API 跑在 Docker 時,迴圈位址 `127.0.0.1` 會被改成 `host.docker.internal`,才能打到你本機的相容端點。 + +--- + +## 教會它 + +![你做一次,它記住為什麼](./docs/diagrams/teach.png) 之後 run 若 prompt 對得上技能名,會把完整 playbook 塞進當則,並清掉舊聊天以免模型複誦上次的「還在倒數」。執行仍用 `browser`/`computer_act`,在**現在**的畫面上找「Next」,不是記像素。 --- -## 排程怎麼進 run +## 排程怎麼進工作 -```mermaid -flowchart LR - A[對話 create_schedule 或側欄新增] --> B[(schedules 表)] - B --> C[tick_loop] - C -->|next_run_at 到了| D[INSERT runs queued] - D --> E[同一個 worker_loop] - E --> F[普通 execute_run,prompt 是 instructions] -``` +![到點以後,跟你傳訊息同一條路](./docs/diagrams/schedule.png) -Cron 五欄。時區寫在列上,預設台北。`run now` 只是立刻插一筆 run,不改下一拍時間。 +Cron 五欄。時區寫在列上,預設台北。`立刻跑` 只是立刻插一筆 run,不改下一拍時間。 --- ## 專案目錄(二次開發從這裡找) -Cargo workspace。契約在 `contracts`,畫面邏輯在 `control`,HTTP 與 agent 迴圈在 `api`,Docker 生命週期在 `supervisor`。前端是獨立的 Vite app,由 API 把 `apps/web/dist`(或開發時的 `apps/web`)端出去。 +![把它當成幾間房間,不是分層蛋糕](./docs/diagrams/folders.png) + +Cargo workspace。畫面在 `apps/web`,對話與工作在 `api`,怎麼點在 `control`,開機在 `supervisor` + `image/computer`,問哪一家模型在 `harness`。前端是獨立的 Vite app,由 API 把 `apps/web/dist`(或開發時的 `apps/web`)端出去。 ```text LazyBoy/ @@ -449,17 +264,6 @@ LazyBoy/ └── reference/rakazo/ 上游參考實作,不要當 runtime 依賴 ``` -crate 依賴方向(不要倒過來 import): - -```text -api → harness, control, sandbox, contracts -sandbox → control, contracts -supervisor → control, contracts -controld → control, contracts -harness → contracts -control → contracts -``` - ### 想改什麼,開哪個檔 | 你要做的事 | 先開 | diff --git a/apps/web/package-lock.json b/apps/web/package-lock.json index c71e1cb..372c30b 100644 --- a/apps/web/package-lock.json +++ b/apps/web/package-lock.json @@ -14,7 +14,10 @@ "blobatar": "^2.7.0", "react": "^19.0.0", "react-dom": "^19.0.0", - "react-useanimations": "^2.10.0" + "react-markdown": "^10.1.0", + "react-useanimations": "^2.10.0", + "remark-breaks": "^4.0.0", + "remark-gfm": "^4.0.1" }, "devDependencies": { "@types/react": "^19.0.0", @@ -1284,18 +1287,58 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", "license": "MIT" }, "node_modules/@types/react": { "version": "19.2.18", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", - "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -1311,6 +1354,18 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.4.0.tgz", + "integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==", + "license": "ISC" + }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", @@ -1332,6 +1387,16 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/baseline-browser-mapping": { "version": "2.11.20", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.20.tgz", @@ -1418,6 +1483,66 @@ ], "license": "CC-BY-4.0" }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -1429,14 +1554,12 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, "license": "MIT" }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1450,6 +1573,41 @@ } } }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.420", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.420.tgz", @@ -1509,6 +1667,34 @@ "node": ">=6" } }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -1552,6 +1738,118 @@ "node": ">=6.9.0" } }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -1585,6 +1883,16 @@ "node": ">=6" } }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/lottie-web": { "version": "5.13.0", "resolved": "https://registry.npmjs.org/lottie-web/-/lottie-web-5.13.0.tgz", @@ -1601,11 +1909,867 @@ "yallist": "^3.0.2" } }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-newline-to-break": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-newline-to-break/-/mdast-util-newline-to-break-2.0.0.tgz", + "integrity": "sha512-MbgeFca0hLYIEx/2zGsszCSEJJ1JSCdiY5xQxRcLDDGa8EPvlLPupJ4DSajbMPAnC0je8jfb9TiUATnxxrHUog==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-find-and-replace": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -1637,6 +2801,31 @@ "node": ">=18" } }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -1686,6 +2875,16 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/property-information": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/react": { "version": "19.2.8", "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", @@ -1707,6 +2906,33 @@ "react": "^19.2.8" } }, + "node_modules/react-markdown": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -1730,6 +2956,87 @@ "react-dom": ">=16" } }, + "node_modules/remark-breaks": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-breaks/-/remark-breaks-4.0.0.tgz", + "integrity": "sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-newline-to-break": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/rollup": { "version": "4.63.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.1.tgz", @@ -1802,6 +3109,48 @@ "node": ">=0.10.0" } }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -1819,6 +3168,26 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -1833,6 +3202,93 @@ "node": ">=14.17" } }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/update-browserslist-db": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", @@ -1864,6 +3320,34 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/vite": { "version": "6.4.3", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", @@ -1945,6 +3429,16 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, "license": "ISC" + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/apps/web/package.json b/apps/web/package.json index 555fd9e..b531d08 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -15,7 +15,10 @@ "blobatar": "^2.7.0", "react": "^19.0.0", "react-dom": "^19.0.0", - "react-useanimations": "^2.10.0" + "react-markdown": "^10.1.0", + "react-useanimations": "^2.10.0", + "remark-breaks": "^4.0.0", + "remark-gfm": "^4.0.1" }, "devDependencies": { "@types/react": "^19.0.0", diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 8dc8b91..531c0c9 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -18,6 +18,7 @@ import { api, ApiError } from "./api"; import { Avatar, AvatarLookProvider, AvatarStack, BLOBATAR_BACKGROUNDS, BLOBATAR_EXPRESSIONS, BLOBATAR_SHAPES, DEFAULT_LOOK, persistBlobatarShape, readAvatarLooks, resolveBlobatarShape, writeAvatarLook, type AvatarBackground, type AvatarExpression, type AvatarLook } from "./avatar"; import { t, type MessageKey } from "./i18n"; import type { AvatarShape, Bot, ComputerMode, ComputerStatus, McpCatalogEntry, McpServer, McpTransport, MemoryItem, Message, MessageFile, ModelProviderId, Playbook, PlaybookInput, PlaybookStep, Room, RoomMember, Session, TaughtSkill, WorkspaceSettings } from "./types"; +import { ChatMarkdown, CopyMessageButton } from "./markdown"; import { ScheduleEditor, ScheduleList, cronFromPreset, defaultCronPreset, presetFromCron, type CronPreset, type ScheduleItem } from "./schedule"; const blankComputer:ComputerStatus={botId:"",mode:"team",state:"stopped",controlHolder:"none",takeoverRequested:false,busyBotName:null,busySessionId:null,busyRunId:null,busyStep:null,usingComputer:false,waitingRunId:null,waitingSessionId:null,queuedRuns:0,display:null,profileMode:"per-bot",screenAvailable:false}; @@ -297,7 +298,7 @@ export function App(){
{activeRoom?<>member.id)}/>{activeRoom.name}{setSessionMenuOpen(false);setSessionToDelete(id)}} onClear={()=>{setSessionMenuOpen(false);setClearOpen(true)}}/>{topTools}:active?<>{active.name}{setSessionMenuOpen(false);setSessionToDelete(id)}} onClear={()=>{setSessionMenuOpen(false);setClearOpen(true)}}/>{topTools}:<>{t("chooseBot")}{topTools}}
-
{(activeRoom||active)&&messages.length===0?
{activeRoom?:}

{activeRoom?t("startRoomDiscussion",{name:activeRoom.name}):t("startBotWork",{name:active!.name})}

{activeRoom?t("roomWillReply",{names:activeRoom.members.map(member=>member.name).join("、")}):active!.description||t("botWelcome")}

:messages.map(message=>{const spoken=message.role!=="user"&&Boolean(activeRoom);const speakerName=message.speakerName||(spoken?paneBot?.name:undefined);const speakerShape=(message.speakerShape||paneBot?.avatarShape||"blob") as AvatarShape;const files=messageFiles(message.blocks);const chips=chipBlocks(message.blocks);const hideBody=isAutoAttachCaption(message.body,files);return
{spoken&&}{spoken&&{speakerName}}{files.length>0&&
{files.map(file=>)}
}{chips.map((chip,index)=>chip.kind==="login"?
{t("loginNeedsYou")}
{chip.site||message.body}
{chip.why?
{t("loginWhy",{why:chip.why})}
:null}
:chip.kind==="schedule"?
{t("scheduleChip")}
{chip.name}{chip.human}
:
{t("scheduleRunChip")}
{chip.name}{chip.human}
)}{!hideBody&&{message.body}}{!hideBody&&message.body.trim()&&}
})}{pausedForUser&&paneBot&&
{computer.controlHolder==="user"?t("pausedUserControl",{name:paneBot.name}):t("pausedNeedsUser",{name:paneBot.name})}{(computer.queuedRuns||0)>0&&<> {t("queuedMessages",{count:computer.queuedRuns||0})}}{computer.controlHolder==="user"?:}
}{teaching&&active&&
{t("teachingLive",{goal:teaching.goal})}{t("teachingHint")}{teaching.eventCount>0&&<> · {t("teachingCaptured",{count:teaching.eventCount})}}
}{drafting&&
{t("distilling",{goal:drafting.goal})}
}{skillDraft&&active&&void saveSkill(skillDraft,name,playbook)} onTest={(name,playbook)=>void testSkill(skillDraft,name,playbook)} onDiscard={()=>void discardSkill(skillDraft)} onEdit={()=>setEditingSkillId(skillDraft.id)} onExport={(name,playbook)=>downloadSkill(name,skillDraft.goal,playbook)}/>} +
{(activeRoom||active)&&messages.length===0?
{activeRoom?:}

{activeRoom?t("startRoomDiscussion",{name:activeRoom.name}):t("startBotWork",{name:active!.name})}

{activeRoom?t("roomWillReply",{names:activeRoom.members.map(member=>member.name).join("、")}):active!.description||t("botWelcome")}

:messages.map(message=>{const spoken=message.role!=="user"&&Boolean(activeRoom);const speakerName=message.speakerName||(spoken?paneBot?.name:undefined);const speakerShape=(message.speakerShape||paneBot?.avatarShape||"blob") as AvatarShape;const files=messageFiles(message.blocks);const chips=chipBlocks(message.blocks);const hideBody=isAutoAttachCaption(message.body,files);return
{spoken&&}{spoken&&{speakerName}}{files.length>0&&
{files.map(file=>)}
}{chips.map((chip,index)=>chip.kind==="login"?
{t("loginNeedsYou")}
{chip.site||message.body}
{chip.why?
{t("loginWhy",{why:chip.why})}
:null}
:chip.kind==="schedule"?
{t("scheduleChip")}
{chip.name}{chip.human}
:
{t("scheduleRunChip")}
{chip.name}{chip.human}
)}{!hideBody&&(message.role==="assistant"?
{message.body}
{message.body.trim()?:null}
:{message.body})}{!hideBody&&message.body.trim()&&}
})}{pausedForUser&&paneBot&&
{computer.controlHolder==="user"?t("pausedUserControl",{name:paneBot.name}):t("pausedNeedsUser",{name:paneBot.name})}{(computer.queuedRuns||0)>0&&<> {t("queuedMessages",{count:computer.queuedRuns||0})}}{computer.controlHolder==="user"?:}
}{teaching&&active&&
{t("teachingLive",{goal:teaching.goal})}{t("teachingHint")}{teaching.eventCount>0&&<> · {t("teachingCaptured",{count:teaching.eventCount})}}
}{drafting&&
{t("distilling",{goal:drafting.goal})}
}{skillDraft&&active&&void saveSkill(skillDraft,name,playbook)} onTest={(name,playbook)=>void testSkill(skillDraft,name,playbook)} onDiscard={()=>void discardSkill(skillDraft)} onEdit={()=>setEditingSkillId(skillDraft.id)} onExport={(name,playbook)=>downloadSkill(name,skillDraft.goal,playbook)}/>} {error&&
{error}
} {otherSessionBusy&&
{t("anotherConversationQueued")}
}
@@ -411,16 +412,20 @@ function VaultPane({bot}:{bot:Bot}){ const load=useCallback(()=>api(`/api/bots/${bot.id}/accounts`).then(setItems),[bot.id]); useEffect(()=>{load().catch(e=>setError(e instanceof Error?e.message:t("loadFailed")))},[load]); async function run(work:()=>Promise){setBusy(true);setError("");try{await work();await load()}catch(e){setError(e instanceof Error?e.message:t("operationFailed"))}finally{setBusy(false)}} - return
+ function setField(key:K,value:string){setForm(current=>({...current,[key]:value}))} + return

{t("accountsHelp")}

-
{e.preventDefault();if(!form.site.trim()||!form.username.trim()||!form.password)return;void run(async()=>{await api(`/api/bots/${bot.id}/accounts`,{method:"POST",body:JSON.stringify(form)});setForm({site:"",host:"",username:"",password:"",notes:""})})}}> - - - - + {e.preventDefault();if(!form.site.trim()||!form.username.trim()||!form.password)return;void run(async()=>{await api(`/api/bots/${bot.id}/accounts`,{method:"POST",body:JSON.stringify(form)});setForm({site:"",host:"",username:"",password:"",notes:""})})}}> +
+ + + + +
+
-
{items.length===0?

{t("noAccounts")}

:items.map(item=>
{item.site}{item.username}{item.host?` · ${item.host}`:""}
)}
+
{items.length===0?

{t("noAccounts")}

:items.map(item=>
{item.site}{item.username}{item.host?` · ${item.host}`:""}{item.notes?{item.notes}:null}
)}
{error&&
{error}
}
} diff --git a/apps/web/src/chat.css b/apps/web/src/chat.css index b774ae0..1ff036c 100644 --- a/apps/web/src/chat.css +++ b/apps/web/src/chat.css @@ -1,9 +1,16 @@ .messages{flex:1;overflow:auto;padding:34px max(34px,7vw) 150px;display:flex;flex-direction:column;gap:18px} .message{display:flex} -.message>.message-body{max-width:76%;padding:13px 17px;border-radius:22px;white-space:pre-wrap;line-height:1.5} +.message>.message-body,.message-stack>.message-body{max-width:76%;padding:13px 17px;border-radius:22px;white-space:pre-wrap;line-height:1.5} +.message>.message-body.md,.message-stack>.message-body.md{min-width:0;white-space:normal} .message.user{justify-content:flex-end} .message.user>.message-body{background:var(--cream);color:#1a1a1a} -.message.assistant>.message-body{background:#19191c} +.message.assistant>.message-body,.message-stack>.message-body{background:#19191c} +.message-stack{display:flex;flex-direction:column;align-items:flex-start;max-width:76%;min-width:0} +.message-stack>.message-body{max-width:100%} +.copy-msg{display:inline-flex;align-items:center;gap:6px;height:28px;margin-top:6px;padding:0 8px 0 6px;border:0;border-radius:8px;background:transparent;color:var(--muted);font:inherit;font-size:12px;cursor:pointer} +.copy-msg svg{flex:0 0 14px} +.copy-msg:hover{background:rgba(255,255,255,.07);color:var(--ink)} +.copy-msg.copied,.copy-msg.copied:hover{color:var(--accent);background:transparent;cursor:default} .composer{position:absolute;left:max(34px,5vw);right:max(34px,5vw);bottom:22px;display:flex;align-items:flex-end;gap:8px;border:1px solid var(--border);background:#121214;border-radius:26px;padding:8px;box-shadow:0 16px 50px rgba(0,0,0,.22)} .composer textarea{flex:1;min-height:42px;max-height:140px;resize:none;border:0;outline:0;background:transparent;color:var(--ink);padding:11px 4px} .composer-plus{background:transparent;color:var(--muted)} @@ -35,7 +42,7 @@ .file-card-remove svg{width:14px;height:14px} .message.with-files{align-items:flex-end;flex-direction:column;gap:8px} .message.with-files .msg-attachments{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:8px;max-width:min(420px,82%)} -.message.with-files .message-body{max-width:82%} +.message.with-files .message-body,.message.with-files .message-stack{max-width:82%} .composer-plus,.composer .send{box-sizing:border-box;flex:0 0 42px;width:42px;height:42px;margin:0;align-self:center} .stop-send{background:#f2f2f2;color:#151515} .stop-send svg{width:14px;height:14px;fill:currentColor} @@ -55,5 +62,6 @@ .message.assistant.spoken{column-gap:11px;row-gap:6px} .message.assistant.spoken>.msg-avatar{max-width:none;padding:0;border-radius:0;background:transparent} .message.assistant.spoken .speaker{padding:0 2px;letter-spacing:.01em} -.message.assistant.spoken>.message-body{grid-column:2;max-width:82%;line-height:1.52;border-radius:6px 18px 18px 18px} +.message.assistant.spoken>.message-body,.message.assistant.spoken>.message-stack{grid-column:2;max-width:82%;min-width:0} +.message.assistant.spoken .message-stack>.message-body{max-width:100%;line-height:1.52;border-radius:6px 18px 18px 18px} .composer-plus.open{color:var(--ink);background:rgba(255,255,255,.08)} diff --git a/apps/web/src/locales/zh-TW.ts b/apps/web/src/locales/zh-TW.ts index b15c22d..11fc72b 100644 --- a/apps/web/src/locales/zh-TW.ts +++ b/apps/web/src/locales/zh-TW.ts @@ -13,6 +13,7 @@ export const zhTW = { openOnPhone: "在手機開啟", settings: "設定", about: "關於", helpCenter: "說明中心", sendFeedback: "傳送意見回饋", logout: "登出", workspaceMenu: "工作區選單", chooseBot: "選擇一個機器人", startRoomDiscussion: "和 {name} 開始討論", startBotWork: "和 {name} 開始工作", roomWillReply: "{names} 會一起回覆。", botWelcome: "傳送訊息,讓它在自己的電腦上完成任務。", remembered: "已記住", remember: "記住", working: "{name} 正在工作…", + copyCode: "複製程式碼", copiedCode: "已複製", copyMessage: "複製", copiedMessage: "已複製", anotherConversationQueued: "另一則對話正在執行,這則會排隊。", workingStep: "{name} 正在工作… {step}", queuedMessages: "還有 {count} 則訊息排隊中", pausedUserControl: "你正在操控畫面,{name} 已暫停。完成後按「釋放控制」,它會從目前畫面接著做(排隊中的訊息也會一起處理)。", @@ -99,7 +100,8 @@ export const zhTW = { urlPlaceholder: "https://mcp.example.com/mcp", headersPlaceholder: "Authorization=Bearer …", back: "返回", accounts: "帳號", accountsHelp: "存在這台機器人自己的保險箱。密碼只在這裡填,不會進聊天、也不會給模型看。排程碰到登入牆時會自動填表。", accountSite: "站名", accountSitePlaceholder: "例如:Gmail", accountHost: "網址或網域", accountHostPlaceholder: "mail.google.com", - accountUsername: "帳號", accountPassword: "密碼", accountPasswordKeep: "留空表示不改密碼", accountNotes: "備註", + accountUsername: "帳號", accountPassword: "密碼", accountPasswordKeep: "留空表示不改密碼", + accountNotes: "備註", accountNotesPlaceholder: "選填,例如:公司信箱", addAccount: "新增帳號", noAccounts: "還沒有已存帳號。排程若要自動登入,先在這裡加一筆。", loginNeedsYou: "需要你在鍵盤上登入", loginOpenScreen: "打開它的畫面", loginWhy: "登入之後:{why}", scheduleChip: "已排程", scheduleRunChip: "排程執行", diff --git a/apps/web/src/markdown.css b/apps/web/src/markdown.css new file mode 100644 index 0000000..ca8150a --- /dev/null +++ b/apps/web/src/markdown.css @@ -0,0 +1,56 @@ +.md-body{min-width:0;overflow-wrap:anywhere} +.md-body>:first-child,.md-body>:first-child>:first-child{margin-top:0} +.md-body>:last-child,.md-body>:last-child>:last-child{margin-bottom:0} +.md-body p,.md-body blockquote,.md-body ul,.md-body ol,.md-body table{margin:.65em 0} +.md-body h1,.md-body h2,.md-body h3,.md-body h4,.md-body h5,.md-body h6{ + margin:.85em 0 .3em;color:var(--ink);font-weight:400;line-height:1.3;letter-spacing:.01em +} +.md-body h1{font-size:1.22em} +.md-body h2{font-size:1.12em} +.md-body h3,.md-body h4,.md-body h5,.md-body h6{font-size:1.04em} +.md-body ul,.md-body ol{padding-left:1.4em} +.md-body ul{list-style:disc} +.md-body ol{list-style:decimal} +.md-body li+li{margin-top:.28em} +.md-body li>p{margin:0} +.md-body .contains-task-list,.md-body .task-list-item{list-style:none} +.md-body .contains-task-list{padding-left:0} +.md-body a{ + color:var(--accent);text-decoration:underline; + text-decoration-color:color-mix(in srgb,var(--accent) 55%,transparent); + text-underline-offset:.16em +} +.md-body a:hover{color:#7ee0c8} +.md-body code{ + border:1px solid var(--border);border-radius:.35em;background:#101012; + padding:.08em .32em;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; + font-size:.88em;direction:ltr;unicode-bidi:isolate +} +.md-body pre{ + max-width:100%;overflow-x:auto;margin:0;border:1px solid var(--border); + border-radius:12px;background:#0c0c0d;padding:.75em .9em;direction:ltr;unicode-bidi:isolate +} +.md-body pre code{border:0;background:transparent;padding:0;font-size:.84em;white-space:pre} +.md-pre-wrap{position:relative;margin:.65em 0} +.md-copy{ + position:absolute;top:.45em;right:.45em;display:grid;place-items:center; + width:28px;height:28px;border:1px solid var(--border);border-radius:8px; + background:#17171a;color:var(--muted);opacity:0;cursor:pointer +} +.md-pre-wrap:hover .md-copy,.md-copy:focus-visible{opacity:1} +.md-copy:hover{color:var(--ink);background:#202024} +.md-body blockquote{ + margin:.65em 0;border-left:3px solid var(--accent);padding-left:.85em;color:var(--muted) +} +.md-body table{display:block;max-width:100%;overflow-x:auto;border-collapse:collapse;font-size:.92em} +.md-body th,.md-body td{ + border:1px solid var(--border);padding:.4em .6em;text-align:left;overflow-wrap:break-word +} +.md-body th{background:#1a1a1d;color:var(--ink);font-weight:400} +.md-body hr{margin:.9em 0;border:0;border-top:1px solid var(--border)} +.md-body img{display:block;max-width:100%;height:auto;margin:.65em 0;border-radius:12px} +.md-body input[type="checkbox"]{margin-right:.4em;accent-color:var(--accent)} +.message-body.md{white-space:normal} +.user .md-body code,.user .md-body pre,.user .md-body th{background:#e8e8e6;border-color:#d4d4d2;color:#1a1a1a} +.user .md-body a{color:#0f766e} +.user .md-copy{background:#ececea;color:#555} diff --git a/apps/web/src/markdown.tsx b/apps/web/src/markdown.tsx new file mode 100644 index 0000000..3d0270c --- /dev/null +++ b/apps/web/src/markdown.tsx @@ -0,0 +1,110 @@ +import { memo, useCallback, useRef, useState, type ComponentPropsWithoutRef } from "react"; +import ReactMarkdown, { type Components } from "react-markdown"; +import remarkBreaks from "remark-breaks"; +import remarkGfm from "remark-gfm"; +import { t } from "./i18n"; +import "./markdown.css"; + +const protocolPattern = /^([a-z][a-z\d+.-]*):/i; +const safeProtocols = new Set(["http", "https", "mailto", "tel"]); + +export function sanitizeMarkdownUrl(url: string): string | undefined { + const value = url.trim(); + const protocol = value.match(protocolPattern)?.[1]?.toLowerCase(); + if (protocol) return safeProtocols.has(protocol) ? value : undefined; + if (value.startsWith("#") && !value.toLowerCase().startsWith("#javascript")) return value; + return undefined; +} + +function CopyIcon() { + return ( + + ); +} + +function CheckIcon() { + return ( + + ); +} + +function useCopiedFlag() { + const [copied, setCopied] = useState(false); + const timerRef = useRef(undefined); + const markCopied = useCallback(() => { + setCopied(true); + window.clearTimeout(timerRef.current); + timerRef.current = window.setTimeout(() => setCopied(false), 1500); + }, []); + return {copied, markCopied}; +} + +function copyText(text: string): Promise { + if (!navigator.clipboard) return Promise.resolve(false); + return navigator.clipboard.writeText(text).then(() => true).catch(() => false); +} + +function CodeBlock(props: ComponentPropsWithoutRef<"pre">) { + const preRef = useRef(null); + const {copied, markCopied} = useCopiedFlag(); + const handleCopy = useCallback(() => { + void copyText(preRef.current?.textContent ?? "").then((ok) => { if (ok) markCopied(); }); + }, [markCopied]); + return ( +
+
+      
+    
+ ); +} + +const components: Components = { + a({node: _node, ...props}) { + return ; + }, + img({node: _node, ...props}) { + return {props.alt; + }, + pre({node: _node, ...props}) { + return ; + }, +}; + +export function CopyMessageButton({text}:{text:string}) { + const {copied, markCopied} = useCopiedFlag(); + if (!text.trim()) return null; + return ( + + ); +} + +export const ChatMarkdown = memo(function ChatMarkdown({children}:{children:string}) { + return ( +
+ sanitizeMarkdownUrl(url) ?? ""} + > + {children} + +
+ ); +}); diff --git a/apps/web/src/refinements.css b/apps/web/src/refinements.css index 803b03e..921a248 100644 --- a/apps/web/src/refinements.css +++ b/apps/web/src/refinements.css @@ -545,13 +545,13 @@ .side-card-body{display:flex;flex-direction:column;min-height:0;flex:1;padding:14px 16px 16px;overflow:hidden} -.pane-form,.memory-pane{display:grid;gap:14px;min-height:0;overflow:auto;padding-bottom:8px} +.pane-form,.memory-pane,.vault-pane{display:grid;gap:14px;min-height:0;overflow:auto;padding-bottom:8px;align-content:start} -.pane-form label,.memory-pane label,.mcp-add label{display:grid;gap:8px;color:var(--muted);font-size:13px} +.pane-form label,.memory-pane label,.mcp-add label,.vault-pane label{display:grid;gap:8px;color:var(--muted);font-size:13px;min-width:0} -.pane-form input:not([type="checkbox"]):not([type="color"]),.memory-pane input:not([type="checkbox"]),.mcp-add input{height:40px;border:1px solid var(--border);border-radius:10px;background:#0c0c0d;color:var(--ink);padding:0 12px;outline:0} +.pane-form input:not([type="checkbox"]):not([type="color"]),.memory-pane input:not([type="checkbox"]),.mcp-add input,.vault-pane input{width:100%;min-width:0;height:40px;border:1px solid var(--border);border-radius:10px;background:#0c0c0d;color:var(--ink);padding:0 12px;outline:0} .pane-form textarea,.memory-pane textarea,.mcp-add textarea{box-sizing:border-box;width:100%;resize:vertical;border:1px solid var(--border);border-radius:10px;background:#0c0c0d;color:var(--ink);padding:11px;font:inherit;outline:none} @@ -860,16 +860,35 @@ .dialog.skill-edit .dialog-actions .outline svg{fill:none;stroke:currentColor;width:14px;height:14px} -.account-list{display:grid;gap:10px} +.vault-pane{flex:1;container-type:inline-size} +.vault-form{display:grid;gap:12px} -.account-row{display:grid;gap:4px;padding:10px;border:1px solid var(--border);border-radius:12px} +.vault-fields{display:grid;grid-template-columns:1fr 1fr;gap:10px 12px} +@container (max-width:310px){ + .vault-fields{grid-template-columns:1fr} +} -.account-row strong{font-size:14px} +.vault-form>.primary{justify-self:start} +.account-list{display:grid;gap:8px;margin-top:2px;padding-top:16px;border-top:1px solid var(--line)} -.account-row small{color:var(--muted)} +.vault-empty{margin:0;color:var(--faint);font-size:13px;line-height:1.5} + +.account-row{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:8px;min-width:0;padding:10px 6px 10px 12px;border:1px solid var(--border);border-radius:12px;background:#101012} + +.account-row-main{display:grid;gap:3px;min-width:0} + +.account-row-main strong{overflow:hidden;font-size:14px;font-weight:400;text-overflow:ellipsis;white-space:nowrap} + +.account-row-main small{overflow:hidden;color:var(--muted);font-size:12px;text-overflow:ellipsis;white-space:nowrap} + +.account-row-main .account-notes{white-space:normal;overflow-wrap:anywhere} + +.account-row .icon-button{color:var(--muted)} + +.account-row .icon-button:hover:not(:disabled){color:var(--danger)} @keyframes monitor-breathe{50%{transform:translateY(-4px);box-shadow:0 0 28px #65dcb325}} diff --git a/apps/web/src/responsive.css b/apps/web/src/responsive.css index 9e9fb5e..f8716e6 100644 --- a/apps/web/src/responsive.css +++ b/apps/web/src/responsive.css @@ -2,7 +2,7 @@ @media(max-width:700px){.app-shell{display:block}.sidebar{position:fixed;z-index:40;inset:0 auto 0 0;width:min(300px,88vw);transform:translateX(-105%);transition:.2s;box-shadow:20px 0 60px #000}.sidebar.open{transform:none}.chat-panel{height:100%}.mobile-menu{display:inline-flex}.topbar{padding:0 12px}.messages{padding:24px 18px 130px}.composer{left:12px;right:12px}.computer-overlay>header{height:auto;min-height:64px;flex-wrap:wrap;padding:10px}.computer-overlay>header>div:last-child{flex-wrap:wrap;justify-content:flex-end}.overlay-screen{padding:8px}.mode-grid{grid-template-columns:1fr}} @media(max-width:1200px){.app-shell{grid-template-columns:230px minmax(0,1fr) 330px}} @media(max-width:1050px){.app-shell{grid-template-columns:250px minmax(0,1fr)}} -@media(max-width:700px){.chat-panel{--chat-gutter:18px}.app-shell{display:block}.message{width:100%}.message>span{max-width:90%}.composer-dock{padding:20px var(--chat-gutter) 16px}.composer{width:var(--chat-col);min-height:60px}.composer-files{padding:2px 4px 8px 8px}.file-card{max-width:100%;height:52px}.file-card-remove{flex-basis:32px;width:32px;height:32px}.message.with-files .msg-attachments{max-width:100%}.messages{padding-bottom:118px}.chat-panel:has(.composer-dock.has-status) .messages{padding-bottom:178px}} +@media(max-width:700px){.chat-panel{--chat-gutter:18px}.app-shell{display:block}.message{width:100%}.message>span,.message>.message-body,.message-stack{max-width:90%}.composer-dock{padding:20px var(--chat-gutter) 16px}.composer{width:var(--chat-col);min-height:60px}.composer-files{padding:2px 4px 8px 8px}.file-card{max-width:100%;height:52px}.file-card-remove{flex-basis:32px;width:32px;height:32px}.message.with-files .msg-attachments{max-width:100%}.messages{padding-bottom:118px}.chat-panel:has(.composer-dock.has-status) .messages{padding-bottom:178px}} @media(prefers-reduced-motion:reduce){.avatar.blobatar.thinking:before,.avatar.blobatar.thinking:after,.thinking-dots i{animation:none!important}} @media(max-width:1200px){.app-shell.right-open{grid-template-columns:230px minmax(0,1fr) 340px}.app-shell.right-collapsed{grid-template-columns:230px minmax(0,1fr) 52px}} @media(max-width:1050px){ diff --git a/docs/bug-improvement-plan.md b/docs/bug-improvement-plan.md new file mode 100644 index 0000000..b9631dc --- /dev/null +++ b/docs/bug-improvement-plan.md @@ -0,0 +1,30 @@ +# 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 提醒仍存在;未量測首屏時間。 diff --git a/docs/diagrams.html b/docs/diagrams.html new file mode 100644 index 0000000..e966325 --- /dev/null +++ b/docs/diagrams.html @@ -0,0 +1,925 @@ + + + + + LazyBoy diagrams + + + + +
+
LAZYBOY · 怎麼轉起來
+

其實只有三個角色

+

不是一疊伺服器名詞。你說話、中間有人幫忙想、右邊那台電腦去按。

+
+
+
+
+
把這堂課看完
+
好,我去開電腦點 Next。
+
右側還能看見它的桌面
+
+
+
+
+ +

瀏覽器
+
+

聊天、看桌面、必要時接手滑鼠。分頁開著,電腦就不會睡著。

+
+
+
+
+
+
+ +
+ 記憶技能要不要開機 +
+
+
+
+
+

LazyBoy

想、記、叫人做事
+
+

把話留給模型、把檔案留給磁碟。問候不會無故開機。

+
+
+
+
+
+
+
+
Chromium
+
star.example / course / 12
+
+

Workplace Safety

+

單元 12/18

+
+
+ +
+
+
+
+
+
+
+ +

它的電腦

真的 Linux 桌面
+
+

Debian、視窗、瀏覽器。家目錄在你硬碟上,不在別人雲裡。

+
+
+
+
+ +
+
傳一句話之後
+

它先決定:聊就好,還是要動手

+

同一條路走完。差別只在第四步要不要打開那台電腦。

+
+
+
+
+
哈囉
+
把課看完
+
+
+
1

你送出

訊息進這則對話。同一句話重送不會變兩則。

+
+
+
+
+
現在這則
+
下一則等著
+
再下一則
+
+
+
2

排隊

這個 Agent 一次只做一件事。忙完才輪到下一則。

+
+
+
+
+ +
記憶 · 技能 · 你剛說的
+
+
+
3

問模型

帶上記憶、技能、你剛說的話。金鑰用你自己的。

+
+
+
+
+
哈囉 → 直接回你
+
「把課看完」→ 開電腦再點
+
+
+
4

兩條岔路

問候不開機。真的要動手,才叫醒那台 Debian。

+
+
+
+
+
+
Chromium
+
star.example / course / 13
+
+

Quiz

+

右側畫面跟著動

+
+
+
+
+
+
+
+
5

寫回來

回覆出現在聊天裡。右側畫面跟著動。

+
+
+
+ +
+
電腦的作息
+

開著、小睡、關機

+

分頁還在看,它就醒著。沒人看才睡,醒來幾乎不用等。

+
+
+
+
+
+
Chromium
+
你還在這個分頁
+
+

熱機中

+

每兩秒打一次招呼

+
+
+
+
+
+
+
+
+
1
+

開著

+

記憶體佔著,畫面隨時能進。

+
心跳執行中
+
+
+
+
+
+
+
Chromium
+
離開約 10 分鐘
+
+

凍結

+

行程還在記憶體裡

+
+
+
+
+
+
Zzz
+
+
+
2
+

小睡

+

像筆電合蓋。下次用,大約一秒內醒來。

+
凍結不要重開桌面
+
+
+
+
+
+
+ +
家目錄還在
+
+
+
+
+
+
3
+

關機

+

約六小時後才真正停掉,把那 2 GB 還你。

+
停機家目錄還在
+
+
+
+
+ +
+
它怎麼「看」螢幕
+

你看乾淨的,它看有編號的

+

模型從來不連你的 VNC。它拿一張蓋了黃字的截圖,點編號,不是猜像素。

+
+
+

你看到的

右側預覽,就是那台 Linux 桌面。沒有黃框、沒有編號。

+
+
+
+
Chromium
+
star.example / course / 12
+
+

Workplace Safety

+

單元 12/18 · 看完按 Next

+
+
    +
  • 11. 防護用具
  • +
  • 12. 現場巡視
  • +
  • 13. 測驗
  • +
+
+ +
+
+
+
+
+
+
+

模型看到的

同一瞬間多一層黃字。它說「點 3」,就是點那個按鈕。

+
+
+
+
Chromium
+
star.example / course / 12
+
+

Workplace Safety

+

單元 12/18 · 看完按 Next

+
+
    +
  • 11. 防護用具
  • +
  • 12. 現場巡視
  • +
  • 13. 測驗
  • +
+
+ +
+
+
+
+
+
+
+
+ +
+
動手的三種方式
+

能認控制項,就不要猜座標

+

網頁走瀏覽器、對話框走無障礙樹,實在沒編號才點像素。

+
+
+
+
+
+
Chromium
+
mail.example / inbox
+
+

收件匣

+
+
訓練系統 · 請完成單元 12
+
HR · 本週排班
+
IT · 密碼即將到期
+
+
+ + +
+
+
+
+
+
+
+
1
+

網頁

+

Chromium 裡的按鈕、輸入框。點 id,會自己捲到看不見的地方。

+
優先
+
+
+
+
+
+

開啟檔案

+
report.pdf
+
syllabus.pdf
+
notes.txt
+
開啟4
+
+
+
+
2
+

桌面視窗

+

檔案選取、系統對話框。用無障礙名稱,不是用視窗外框。

+
原生 GUI
+
+
+
+
+
+
+
640, 400
+
+
+
+
3
+

座標

+

畫布、沒有名字的控制項。最後才用。畫面契約是 1280×800。

+
不得已
+
+
+
+
+ +
+
教會它
+

你做一次,它記住「為什麼」

+

不是錄巨集。它記你點了哪個控制項、填了什麼、去了哪一頁,下次在活的畫面上自己找。

+
+
+
+
+
+
你在操作
+
star.example / course / 12
+
+

Workplace Safety

+

你正在示範

+
+ +
+
+
+
+
+
1

你示範

畫面交給你,用平常的方式做完。密碼欄不會被記。

+
+
+
+
+
click Next
+
fill 姓名
+
goto /quiz
+
+
+
2

它在旁邊看

記語意事件。畫面沒變就不存,避免一堆廢幀。

+
+
+
+
+ 技能草稿 + 目標:看完課並交測驗
+ 輸入:學員姓名
+ 步驟:找到 Next → 填表 → 送出
+ 完成:出現分數 +
可改名 · 試跑 · 匯出 JSON
+
+
+
3

整理成技能

目標、可變輸入、步驟、怎麼算做完。你可以改名字再存。

+
+
+
+
+
+
下次自己做
+
star.example / course / 3
+
+

另一堂課

+

在現在的畫面上找 Next

+
+ +
+
+
+
+
+
4

下次自己做

找「Next」,不是重播上次的座標。

+
+
+
+ +
+
排程
+

到點以後,跟你傳訊息同一條路

+

不是另一套引擎。鬧鐘響了,就塞進同一個工作隊列。

+
+
+
+
+
每天九點交報告
+
對話裡講,或側欄自己建
+
+
+
1

你說「每天九點」

五欄時間,預設台北。

+
+
+
+
+
+ + + + + + +
每天 09:00
+
+
+
2

鬧鐘記住

到點前它只是一列時間。按「立刻跑」也不會把鬧鐘撥亂。

+
+
+
+
+
+
09:00 交報告
+
你剛傳的那則
+
下一則等著
+
+
+
3

變成普通工作

跟你剛傳的那則訊息一樣:排隊、問模型、必要時開電腦。

+
+
+
+ +
+
模型金鑰
+

由近到遠,找到第一把就用

+

不管滑鼠。只決定這次要問哪一家模型。

+
+
+
+
+ +
xai-••••••••••••用這把
+
+
+
1

這個機器人

它自己若存了 key,用它的。適合不同 Agent 接不同家。

+
+
+
+
+ +
尚未填寫
+
+
+
2

工作區設定

本機工作區裡填的。畫面上存的會蓋過環境變數。

+
+
+
+
+ +
XAI_API_KEY=…
OPENAI_API_KEY=
+
+
+
3

環境變數

什麼都沒填才落到這裡。

+
+
+
+ +
+
想改程式時
+

把它當成幾間房間,不是分層蛋糕

+

從你看得見的畫面往裡走。不必先背 crate 依賴圖。

+
+
+
apps/web
+

畫面

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 聊天、電腦預覽、頭像、排程表單。中文在 locales。VNC 在 vnc.html。 +
+
+
+
crates/api
+

對話與工作

+
+ 送訊息、工具、開機、技能、排程、保險箱。加工具從 tools.rs 開始。 +
+
+
+
crates/control
+

怎麼點

+
+ 編號、CDP、無障礙樹、滑鼠。桌面上的手。 +
+
+
+
supervisor + image/computer
+

怎麼開機

+
+ 容器的生老病死,以及 XFCE/Chromium 長什麼樣。 +
+
+
+
crates/harness
+

問哪一家模型

+
+ 金鑰從哪來、打哪一個網址。不管滑鼠。 +
+
+
+ + + + diff --git a/docs/diagrams/chat.png b/docs/diagrams/chat.png new file mode 100644 index 0000000..0b1adf5 Binary files /dev/null and b/docs/diagrams/chat.png differ diff --git a/docs/diagrams/click.png b/docs/diagrams/click.png new file mode 100644 index 0000000..abd9b47 Binary files /dev/null and b/docs/diagrams/click.png differ diff --git a/docs/diagrams/folders.png b/docs/diagrams/folders.png new file mode 100644 index 0000000..9c2fe51 Binary files /dev/null and b/docs/diagrams/folders.png differ diff --git a/docs/diagrams/keys.png b/docs/diagrams/keys.png new file mode 100644 index 0000000..63af9d8 Binary files /dev/null and b/docs/diagrams/keys.png differ diff --git a/docs/diagrams/look.png b/docs/diagrams/look.png new file mode 100644 index 0000000..f9b2398 Binary files /dev/null and b/docs/diagrams/look.png differ diff --git a/docs/diagrams/map.png b/docs/diagrams/map.png new file mode 100644 index 0000000..7c6928b Binary files /dev/null and b/docs/diagrams/map.png differ diff --git a/docs/diagrams/schedule.png b/docs/diagrams/schedule.png new file mode 100644 index 0000000..27c229a Binary files /dev/null and b/docs/diagrams/schedule.png differ diff --git a/docs/diagrams/sleep.png b/docs/diagrams/sleep.png new file mode 100644 index 0000000..06c267c Binary files /dev/null and b/docs/diagrams/sleep.png differ diff --git a/docs/diagrams/teach.png b/docs/diagrams/teach.png new file mode 100644 index 0000000..d58a836 Binary files /dev/null and b/docs/diagrams/teach.png differ diff --git a/docs/security-and-harness-review.md b/docs/security-and-harness-review.md new file mode 100644 index 0000000..58572b3 --- /dev/null +++ b/docs/security-and-harness-review.md @@ -0,0 +1,62 @@ +# 安全、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) diff --git a/scripts/render-readme-diagrams.sh b/scripts/render-readme-diagrams.sh new file mode 100755 index 0000000..17c0e6b --- /dev/null +++ b/scripts/render-readme-diagrams.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Render docs/diagrams.html slides to docs/diagrams/*.png +set -euo pipefail +root="$(cd "$(dirname "$0")/.." && pwd)" +html="$root/docs/diagrams.html" +out="$root/docs/diagrams" +brave="/Applications/Brave Browser.app/Contents/MacOS/Brave Browser" +mkdir -p "$out" +if [[ ! -x "$brave" ]]; then + echo "Need Brave at $brave" >&2 + exit 1 +fi +for p in map chat sleep look click teach schedule keys folders; do + "$brave" \ + --headless=new \ + --disable-gpu \ + --hide-scrollbars \ + --force-device-scale-factor=2 \ + --window-size=1280,720 \ + --screenshot="$out/$p.png" \ + "file://$html?p=$p" + echo "wrote $out/$p.png" +done diff --git a/tests/frontend.test.mjs b/tests/frontend.test.mjs index 51c2aa7..efee616 100644 --- a/tests/frontend.test.mjs +++ b/tests/frontend.test.mjs @@ -31,3 +31,23 @@ test('saved login rejects HTTP, lookalike hosts, and missing host before touchin assert.equal(evaluate({username:'u',password:'secret',expectedHost}).ok,false); } }); + +const mdJs=ts.transpileModule(fs.readFileSync('apps/web/src/markdown.tsx','utf8'),{compilerOptions:{module:ts.ModuleKind.CommonJS,jsx:ts.JsxEmit.ReactJSX}}).outputText; +const mdBox={exports:{},require:(name)=>{ + if(name==='react')return{useCallback:fn=>fn,useRef:()=>({current:null}),useState:()=>[false,()=>{}],memo:fn=>fn}; + if(name==='react/jsx-runtime')return{jsx:()=>null,jsxs:()=>null,Fragment:'Fragment'}; + if(name==='react-markdown'||name==='remark-gfm'||name==='remark-breaks')return{default:()=>null}; + if(name==='./i18n')return{t:key=>key}; + if(name.endsWith('.css'))return{}; + throw new Error('unexpected import '+name); +}}; +vm.runInNewContext(mdJs,mdBox); +const {sanitizeMarkdownUrl}=mdBox.exports; +test('markdown links only keep http(s), mailto, tel, and in-page hashes',()=>{ + assert.equal(sanitizeMarkdownUrl('https://example.com/docs'),'https://example.com/docs'); + assert.equal(sanitizeMarkdownUrl('mailto:hi@example.com'),'mailto:hi@example.com'); + assert.equal(sanitizeMarkdownUrl('#section'),'#section'); + assert.equal(sanitizeMarkdownUrl('javascript:alert(1)'),undefined); + assert.equal(sanitizeMarkdownUrl('data:text/html,'),undefined); + assert.equal(sanitizeMarkdownUrl('/relative'),undefined); +});