AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama, custom), tools and approvals, the bot's Docker computer, computer use on macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based toolset, the encrypted vault, pinned grid and sidebar sections, Markdown with Mermaid diagrams, phone access through the relay, and the Linux build and test scripts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
|
|
@ -0,0 +1,4 @@
|
||||||
|
target
|
||||||
|
relay/target
|
||||||
|
shots
|
||||||
|
*.png
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
# Build output
|
||||||
|
/target/
|
||||||
|
/relay/target/
|
||||||
|
|
||||||
|
# Test and screenshot output (regenerated by the test scripts)
|
||||||
|
/linux-shots/
|
||||||
|
/shots/
|
||||||
|
|
||||||
|
# Local secrets (deploy/server/.env.example is the template)
|
||||||
|
.env
|
||||||
|
*.env.local
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
[package]
|
||||||
|
name = "anybot"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "anybot"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
eframe = "0.37"
|
||||||
|
ureq = { version = "3", features = ["json"] }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
dirs = "7"
|
||||||
|
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "gif", "webp", "bmp", "tiff", "ico"] }
|
||||||
|
base64 = "0.22"
|
||||||
|
chrono = "0.4"
|
||||||
|
chrono-tz = "0.10"
|
||||||
|
iana-time-zone = "0.1"
|
||||||
|
ring = "0.17"
|
||||||
|
tungstenite = { version = "0.30", default-features = false, features = ["handshake"] }
|
||||||
|
calamine = "0.38.0"
|
||||||
|
resvg = "0.49.0"
|
||||||
|
hayro = "0.9"
|
||||||
|
zip = { version = "9", default-features = false, features = ["deflate"] }
|
||||||
|
rodio = { version = "0.23", default-features = false, features = ["playback", "flac", "mp3", "mp4", "vorbis", "wav", "symphonia-aiff", "symphonia-alac", "symphonia-caf"] }
|
||||||
|
pdf-extract = "0.12"
|
||||||
|
unicode-normalization = "0.1"
|
||||||
|
cpal = "0.18"
|
||||||
|
mermaid-rs-renderer = { version = "0.3", default-features = false }
|
||||||
|
pulldown-cmark = { version = "0.13", default-features = false }
|
||||||
|
[profile.release]
|
||||||
|
opt-level = 2
|
||||||
|
|
||||||
|
[target.'cfg(not(target_os = "macos"))'.dependencies]
|
||||||
|
zhconv = "0.4"
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
|
x11rb = { version = "0.13", features = ["xtest", "screensaver", "record"] }
|
||||||
|
# Wayland: wlroots desktops (sway, Hyprland…) through their own protocols; GNOME and KDE through
|
||||||
|
# the desktop portals (one "allow" dialog, remembered).
|
||||||
|
wayland-client = "0.31"
|
||||||
|
wayland-protocols = { version = "0.32", features = ["client", "staging", "unstable"] }
|
||||||
|
wayland-protocols-wlr = { version = "0.3", features = ["client"] }
|
||||||
|
wayland-protocols-misc = { version = "0.3", features = ["client"] }
|
||||||
|
memmap2 = "0.9"
|
||||||
|
ashpd = { version = "0.13", default-features = false, features = ["async-io", "remote_desktop", "screencast", "screenshot"] }
|
||||||
|
zbus = { version = "5", default-features = false, features = ["async-io", "blocking-api"] }
|
||||||
|
futures-lite = "2"
|
||||||
|
async-io = "2"
|
||||||
|
|
@ -0,0 +1,228 @@
|
||||||
|
# Grok Bot(內部代號 `sand`)功能分析
|
||||||
|
|
||||||
|
**分析目標**:`/Applications/Grok Bot.app`(active target `t_a7c7c1454fc1`)
|
||||||
|
**asar**:`/Applications/Grok Bot.app/Contents/Resources/app.asar`(1027 entries,header 243890 B,integrity verified)
|
||||||
|
**SHA-256**:`a7c7c1454fc1a9169eca436fe6cf6fdf363e2c359da7312c49c22945170e36fd`
|
||||||
|
**外殼**:Electron 40.1.0,`package.json` name = `"sand"`,`type: module`,main = `dist/electron-main/main.cjs`
|
||||||
|
**方法**:純靜態(asar 目錄清單、字串/符號掃描、內建反編譯器、`js_app_map` 應用圖)。**未執行目標程式**,因此本文所有結論都是「程式碼/資源中存在」的證據,不是「執行時發生了」的證據。asar 內無 source map。
|
||||||
|
|
||||||
|
**標記**
|
||||||
|
- **觀察**:直接讀到(檔案、符號名、字串位址、設定值)。
|
||||||
|
- **推論(derived)**:由觀察算出/歸納出。
|
||||||
|
- **推論(inferred)**:機制作最佳解釋,未直接讀到。
|
||||||
|
- **推測(estimated)**:best guess,證據薄弱。
|
||||||
|
|
||||||
|
**引用規則**:每條斷言引 `fnd_*`(已入庫 finding,綁定原始 evidence)。本輪**未取到新的 evidence id**,所以不列 `ev_*`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 摘要
|
||||||
|
|
||||||
|
| # | 結論 | 標記 | 依據 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | Grok Bot 是 Anysphere(Cursor)私有 monorepo 建置出來的 Electron 應用,約 50 個 `@anysphere/*` + `@sand/*` workspace package,包含 `@anysphere/grok-bot`、`agent-*`、`chat-inference`、`mcp-core`、`xai-commerce-prepaid` | 觀察 | `fnd_1` |
|
||||||
|
| 2 | **文字代理迴圈不在桌面端**。它是一個連到遠端 gateway / "box" 的傳輸客戶端;模型呼叫、prompt 組裝、工具編排都在伺服器 | 推論(derived) | `fnd_5`, `fnd_6` |
|
||||||
|
| 3 | 桌面端真正的「執行手」是獨立 Node 程序 `dist/local-exec-daemon/main.cjs`:由遠端代理反向驱动的本地工具執行器(搜尋/讀檔/shell/電腦操作/MCP/macOS 內建 App) | 推論(derived) | `fnd_7` |
|
||||||
|
| 4 | 用戶端有一整套分層權限:讀/寫/shell/MCP 四條 `shouldBlock*` 閘門 + allowlist / denylist / glob + 逐 app 同意 + 持久「always allow」授權 | 推論(derived) | `fnd_8`, `fnd_12` |
|
||||||
|
| 5 | 用戶端**沒有 token 計數器**;token 記帳與 context 壓縮摘要都來自伺服器串流事件。唯一在客戶端的 token 估計是字元數除以 4(語音逐字稿) | 推論(derived) | `fnd_9` + 本報告 §10 |
|
||||||
|
| 6 | 唯一在客戶端組裝 prompt 的場景是**語音通話 harness**(`wss://api.x.ai/` 是原檔字面字串):語音模型被設計成薄前端,把工作派給主代理,且不得宣稱任務已完成 | 觀察 | `fnd_10` |
|
||||||
|
| 7 | 後端是 Anysphere 自有服務(`api2.cursor.sh`,connect-rpc + protobuf;遥測 `api3.cursor.sh`;登入/計費在 cursor.com),**不是**公開 LLM API;金鑰用 Electron `safeStorage` | 觀察 | `fnd_3` |
|
||||||
|
| 8 | 電腦操作(滑鼠拖曳、鍵盤輸入)的實際輸入合成落在一個原生 macOS helper `Contents/Helpers/CUGrokBotService.app`(Swift module `CUCore`),每次 drag 需自帶一個 permit | 推論(inferred) | `fnd_14` |
|
||||||
|
| 9 | 模型清單由伺服器提供;程式碼中出現的模型名是 `grok-4.5`(預設壓縮模型)、`grok-4.6`(model picker 實驗) | 觀察 | `fnd_9` + `unk_dd4ee2d6bc4685b5`(已解) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 程序架構
|
||||||
|
|
||||||
|
**觀察**(`fnd_2`):
|
||||||
|
- 主程序拆成兩段:`dist/electron-main/main-core.cjs` 與 `dist/electron-main/main-app.cjs`。
|
||||||
|
- preload 有兩支:`preload.cjs`、`preload-avatar-source.cjs`。
|
||||||
|
- 兩個獨立 Node 程序:
|
||||||
|
- `dist/local-exec-daemon/main.cjs` — 沙箱化本地命令執行 + 檔案系統 allow-list(見 §4)。
|
||||||
|
- `dist/node-agent-coordinator/main.cjs` — 代理協調器傳輸客戶端(見 §3)。
|
||||||
|
- Renderer 與 main 透過 **MessagePort** 連通:main `handle("sand:coordinator-port-request")` → 回傳 `"sand:coordinator-port"`;另有 boot-gate / renderer-alive / network-online 信號。
|
||||||
|
- 共 8 處 window 構造,多數 `contextIsolation` + `sandbox`,其中 2 處開了 `webviewTag`。
|
||||||
|
- 自訂協議:`sand-mcp-app://`、`sand-visualization://`、`sand-media://`、`sand-html-preview://`、`sand-avatar://`、`sand-favicon://`。
|
||||||
|
|
||||||
|
**推論(derived)**:這種「main 只當殼 + 兩個独立 Node 程序做重活」的切法,是把「長生命週期的代理/session 狀態」從 Electron 生命週期裡拆出來,避免 renderer 重載或 window 關閉中斷 session。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 代理迴圈與後端
|
||||||
|
|
||||||
|
**觀察**(`fnd_5`)— `dist/node-agent-coordinator/main.cjs` 是通往遠端 gateway / box 的傳輸客戶端,符號包括:
|
||||||
|
`createAgent` / `createAgentWithRetry`、`sendPrompt`(`POST {base}/api/sendPrompt`)、`streamEvents` / `runEventLoop`(`GET {base}/events`)、`postEcho`、`listAgents`、`openAgentTail` / `getAgentTranscriptTail`、`transcribeAudio`、`ensureForeverBox` / `noteBoxAsleep` / `requestBoxIntent`,以及一條 **WebAuthn 同意通道**(`/webauthn/requests`、`/webauthn/responses`、`requestConsent`、`onPinRequest`)。
|
||||||
|
|
||||||
|
它向 UI 發射的主題:automations、workflows、subagents、skills、secrets、tasks、meetings、library docs、computerActions、`onTurnFailed`。
|
||||||
|
|
||||||
|
**觀察**(`fnd_6`)— main 端通往 box 的路徑:`probeGateway` → `{gatewayUrl}/health`、`flushBoxStore` → `{gatewayUrl}/api/snapshotBoxStoreNow`、`issueBoxCredential`(POST)。`main-app.cjs` 與 `main-core.cjs` 中**未見**直接 fetch 到任何 LLM inference API。
|
||||||
|
|
||||||
|
**推論(derived)**:LLM 推理由伺服器端代理發起,桌面端只送 prompt、收事件流。
|
||||||
|
|
||||||
|
**範圍限制(必讀)**:`main-app.cjs` 有 **18 處動態 fetch 點**,connect-rpc transport 未完全解析 → 「未見直接呼叫」只適用於我解析到的路徑,不等於不exists。
|
||||||
|
|
||||||
|
**Box 供應**(`fnd_11`,推論(derived)):正式路徑 `<class>.ensureBox` → `client.ensureSandBox`(connect-rpc,帶 rate-limit retry-after、parkMode、access-hold 拒絕分類)。另有一條 **dev-box** 路徑在本地跑 Docker:`imageExists`(`docker image inspect`)、`pullImage`(`docker pull`)、`removeContainer`(`docker rm -f` + `recreate-intent.json`)、`resetBox`、`recreateContainer`、`wipeSandData`。`issueBoxCredential` POST 一枚 `local-exec` 憑證,帶 `x-cursor-team-id` 與 `x-ghost-mode` header → **推測**:讓 local-exec daemon 對 box 認證。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 本地工具執行器(`dist/local-exec-daemon/main.cjs`)
|
||||||
|
|
||||||
|
**觀察 + 推論(derived)**(`fnd_7`):它**反向撥出**連回 gateway 接指令:`redial`、`streamRequests`、`handleRequest`、`handleExecRequest`、`handleCancel`、`postBatch`、`upload` / `download`。
|
||||||
|
|
||||||
|
實作的工具族:
|
||||||
|
|
||||||
|
| 工具族 | 觀察到的细节 | 標記 |
|
||||||
|
|---|---|---|
|
||||||
|
| 檔案搜尋(ripgrep) | `files` / `count` / `content` 三種模式、timeout、blocked-file 過濾 | 觀察 |
|
||||||
|
| 目錄樹 | 目錄 tree 輸出 | 觀察 |
|
||||||
|
| Shell | 執行 + **背景交接**:`requestBackground`、`forceBackgroundByToolCallId`、`timedOut`、`readTerminalFile` | 觀察 |
|
||||||
|
| 電腦操作 | 經 sidecar helper 走 **unix socket**:`screenshot`、`click`、`scroll`、`move`、`drag`、`typeText`、`pressKey`、`startControl`/`releaseControl`、座標換算 `apiToDisplay`/`displayToApi`、ffmpeg 動作錄製 | 觀察 |
|
||||||
|
| MCP client | 含 OAuth token 存放 | 觀察 |
|
||||||
|
| macOS App 工具 | Reminders、Calendar(CRUD)、Contacts(search)、Mail(`composeSend` / `sentCheck`)、Messages(操作),逐 app 同意 + 冪等送出(`rememberSend`、`replayedSend`、`withSendKey`) | 觀察 |
|
||||||
|
| 上下文載入 | 載入 Cursor rules 與 agent skills | 觀察 |
|
||||||
|
| 規則解析 | 存在 `tree-sitter-bash.node`(darwin-arm64 prebuild)+ `web-tree-sitter/tree-sitter.wasm` | 觀察 |
|
||||||
|
| shell 指令解析 | 推測:shell 指令會被解析成子指令後才做 allowlist 檢查 | **推測(estimated)** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 權限模型
|
||||||
|
|
||||||
|
**觀察**(`fnd_8`)— 閘門與清單:
|
||||||
|
`shouldBlockRead`、`shouldBlockWrite`、`shouldBlockShellCommand`、`shouldBlockMcp`、`isShellCommandFullyAllowlisted`、`isMcpFullyAllowlisted`、`isWebFetchFullyAllowlisted`、`addToAllowList` / `addToDenyList`、`shouldEnforceShellInvariantBlocks`,加上 `cursorignore` / `gitignore` / repo-block 的 glob 過濾。
|
||||||
|
审批回呼:`onNeedsApproval`、`onApprovalRequested`、`onUserRejected`、`onPermissionDenied`。
|
||||||
|
|
||||||
|
**觀察**(`fnd_12`)— 客戶端擁有並推送到 box 的設定與授權:
|
||||||
|
- `recordLocalToolApproval` 是**機器範圍**的;**拒絕** send-mail,除非經 `recordMacMailSendApproval`(從草稿卡片)。
|
||||||
|
- `recordLocalToolStandingGrant` 持久化「always allow」。
|
||||||
|
- `getLocalToolPermissionCeiling`(權限上限)、`listComputerUseAllowedApps`(從 standing grants 推導)。
|
||||||
|
- MCP 自訂指令:`getMcpCustomInstructions`、`applyCustomInstructionsToMcpResult`。
|
||||||
|
- auto-review 指令與 `syncHostSettingsToBox` 同步;`pushBoxSecrets`、template skills、user bot memories。
|
||||||
|
- 遥測:`reportTurnClientOutcome`、`reportRenderTtfr`。
|
||||||
|
|
||||||
|
**推論(derived)**:權限分三層 —(1)全域 deny/glob(檔名與指令層面的硬駔)→(2)allowlist(完全放行)→(3)逐次审批 + standing grant(使用者互動)。`shouldBlock*` 是閘門,`is*FullyAllowlisted` 是「整條指令都允許才放行」的那種全量檢查。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 語音通話代理(客戶端唯一組裝 prompt 的地方)
|
||||||
|
|
||||||
|
**觀察**(`fnd_10`):
|
||||||
|
- `requestVoiceCallHarness` / `voiceCallHarnessSession` / `voiceCallHarnessTool` 經 `cursorAccount`;realtime endpoint `wss://api.x.ai/` 是原檔第 1116 行的**字面字串**。
|
||||||
|
- `e.render`(第 65 行)用 section builder 組出分段 system prompt:`Role & Persona`、`Instruction and tools`、`Conversation Flow`、`While work is in flight`、`When they talk over you`、`Never claim it is done`、`Voice`、`CRITICAL INSTRUCTIONS`;builder 有 `voiceLoopLines`、`noInventedWorkLines`、`rosterPromptSection`(`## Project workers`)、`findBannedPlumbingTerm` regex。
|
||||||
|
- 語音代理自己的工具含 `sendTask`、`staySilent`、`endTheCall`;`earnsASpokenTurn` 用 `SPOKEN_KINDS` 決定何時可以說話。
|
||||||
|
|
||||||
|
**設計取捨(推論 derived)**:語音模型被刻意做成**薄前端** — 只負責接話、派工(`sendTask`)、保持安靜(`staySilent`)、結束通話(`endTheCall`);**不得**在沒有結果時宣稱完成。把「不確定性」與「延遲」交给主代理,避免語音模型自編進度。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 整合、連線器與後端服務
|
||||||
|
|
||||||
|
**觀察**(`fnd_3`):
|
||||||
|
- 後端:`api2.cursor.sh`(`@connectrpc/connect` + protobuf),遥測 `api3.cursor.sh`,登入/計費/onboarding 在 `cursor.com`。
|
||||||
|
- 整合:`x.com` / `grok.com` 的 OAuth 連線器;Google Workspace scopes(Gmail / Drive / Calendar / Docs / Sheets / Slides / BigQuery);MCP OAuth;Statsig feature flags;Sentry。
|
||||||
|
- 祕密儲存:Electron `safeStorage`。
|
||||||
|
|
||||||
|
**觀察**(`fnd_4`)— renderer 技術堆疊(來自 deps):React + `@tanstack/react-query`、StyleX、`@base-ui/react`、TipTap(含 mentions)、`react-markdown` + `remark-gfm` + KaTeX、`motion`、`dnd-kit`、Lingui i18n;檔案進件 `pdfjs-dist` / `mammoth`(docx)/ `xlsx`;tree-sitter(bash)做指令解析。原生 macOS addons:`window-capture`、`card-text-recognition`、`devicecheck`、`notification-settings`、`address-lookup`、`proclist`、`jpeg-napi`。
|
||||||
|
|
||||||
|
**推論(derived)**:Google Workspace scope + Mail/Messages/Calendar 工具 + 「逐 app 同意」顯示這個代理被設計為**直接操作使用者的個人工作環境**,而不只在 repo 內寫程式。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 設定與功能目錄(UI 層線索)
|
||||||
|
|
||||||
|
**依據 A:專案內既有檔案 `app/ORIGINAL_FEATURES.md`**(5491 B、33 個標題:27 個設定分區、per-bot tabs、routine triggers、SAND‑E 的 9 domains / 108 codes)。這是專案自帶的觀察記錄,**不是**本輪分析產出的 evidence id。
|
||||||
|
|
||||||
|
**依據 B(`fnd_15`,觀察,名稱層)**:asar 的 renderer bundle 以 per-feature chunk 分割,**檔名本身就列出已出貨的 UI 功能**。`dist/renderer/assets/` 中可見:
|
||||||
|
|
||||||
|
| chunk 檔名 | 對應功能線索 |
|
||||||
|
|---|---|
|
||||||
|
| `chunk-agents-*`(十餘個變體,22–52 KB) | agents 主畫面/代理清單 |
|
||||||
|
| `chunk-agent-avatar-editor` | 代理頭像編輯 |
|
||||||
|
| `chunk-agent-invite-dialog` | 邀請代理 |
|
||||||
|
| `chunk-bot-configure-dialog` | Bot 設定對話框 |
|
||||||
|
| `chunk-bot-manage-screen`(67962 B)、`-manage-knowledge`、`-manage-section` | Bot 管理畫面 + 知識管理 |
|
||||||
|
| `chunk-bot-share-control`、`-invite-popover`、`-popover`、`-state-line`、`-trigger` | Bot 分享/邀請/狀態列/觸發器 |
|
||||||
|
| `chunk-automation-detail-panel` | 自動化詳情面板 |
|
||||||
|
| `chunk-auto-review-rule-notice`、`chunk-auto-review-selector` | auto-review 規則提示與選擇器 |
|
||||||
|
| `chunk-bonfire-dialog` | bonfire 對話框 |
|
||||||
|
| `chunk-app-copy` | 整包複製/打包 |
|
||||||
|
|
||||||
|
(來源:asar 目錄清單 offset 360,200/1027 筆,`integrity: verified`。)
|
||||||
|
|
||||||
|
**標記**:以上整章屬**觀察(名稱層)** — 檔名是觀察,功能行為**不是**由檔名證明的。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 原生 computer-use helper
|
||||||
|
|
||||||
|
**推論(inferred)**(`fnd_14`):輸入合成在原生 macOS helper `Contents/Helpers/CUGrokBotService.app`(Mach-O fat,Swift module `CUCore`,sha256 `1004a8cbd6a03567e2775e3cb1ec69d15d06c66d44afd7f7b627a6fad2cbab13`)。
|
||||||
|
|
||||||
|
觀察到的符號(內建反編譯器,非 Ghidra):
|
||||||
|
- `CURemoteToolService.handleDrag(...permit:)` → `CURemoteEventPoster.drag(path:[CGPoint], button:CUMouseButton, steps:Int, holdSeconds:Double, flags:CGEventFlags, permit:CUScrollControlPermit?)`;`drag` 只有 **1 個 caller** → permit 屬**逐請求授權**。
|
||||||
|
- 文字輸入走 `CUTypedCharacters.utf16Chunks` + `postChunks(_:source:CGEventSource, prepare:, delivery:_(CGEvent, isCommand:))`。
|
||||||
|
- `CUActuator.typeCharacters(pid, text, processIdentity)`、`CUActuator.click(pid, screenPoint, windowBounds, windowID, button, count, holdSeconds, processIdentity)`。
|
||||||
|
|
||||||
|
**範圍限制**:Ghidra decompiler 無結果,偽碼全來自內建反編譯器;掃描 coverage partial(120/616 函式、80/99 字串);**哪個請求欄位決定 `CUScrollControlPermit` 未解**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Token 計數與 context window
|
||||||
|
|
||||||
|
完整展開見同目錄 `GROK_BOT_TOKENS.md`。此處只列結論。
|
||||||
|
|
||||||
|
**觀察(限定範圍的缺席)**:對 `dist/electron-main/main-app.cjs` 掃 `usage`、`prompt_tokens`、`completion_tokens`、`total_tokens`、`token_count`、`context_window`、`max_tokens`、`tokens_used`、`usage_ratio` → **0 命中**;符號索引含 "token" 的 147 筆全為憑證/認證字串。**→ Grok Bot 用戶端沒有 token 計數器。**(僅該檔、僅這些欄位名。)
|
||||||
|
|
||||||
|
**觀察**(真實在客戶端的东西):
|
||||||
|
- 壓縮設定:`client_speculative_summarization_config = {enabled:true, thresholdRatio:0.70, toleranceRatio:0.05, modelId:"grok-4.5"}`;閘 `speculativeSummarizationClientGates:{enabled, modelTier:"default", contextRatio:0.70}`。
|
||||||
|
- **byte 級**上限(不是 token):`summarizationInputTokenLimit: 360000`(註解 "bytes (ASCII=1 token)")、`summarizationInputByteLimit: 360000`(預設 300000)、`summarizationInputBytes: 120000`、`summarizationInputTokenLimitCap`(0=停用)、`summarizationOutputCap: 6000`、`MAX_SUMMARY_BYTES = 100*1024`、`summarizeMessages`。
|
||||||
|
- 每工具輸出上限:`read_file maxBytes` 預設 131072;`readTerminalFile maxBytes` 預設 65536;`ripgrep maxResults` 200;`ripgrep timeoutMs` 20000。
|
||||||
|
- 配額預算(**不是** context window):`token_budget = 100000`、`token_budget_per_user = 30000`、`token_budget_per_user_7d = 100000`、`token_budget_per_team`、`usage_warning_threshold = 0.8`、`usage_limit_floored`。
|
||||||
|
- Provider 錯誤分類含 `provider_413_context_too_large`。
|
||||||
|
|
||||||
|
**推論(derived)**:
|
||||||
|
- 客戶端**唯一**的 token 估計器是 `voiceCallTranscriptCharsPerToken = 4`(預設 4.0),用法 `voiceCallTranscriptTokens = ceil(charLen / charsPerToken)`,對 `voiceCallTranscriptTokenBudget = 12000` 檢查。`estimateMessagesTokenCount` 存在(main-app.cjs 約 @2743065)但算術不可見。
|
||||||
|
- `compactionSummary` 來自**伺服器**串流(`compactionSummaryFromEvent`、`applyCompactionSummaryToTimeline`、`applyCompactionSummaryFromEvent`),配 `turnUsageBreakdownFromEvent`、`reportUsage`、`usageFromEvent`、`usageLabel`、`onUsage`、`usageLabelForTurn` → **token 記帳在伺服器端**(與 `fnd_5`/`fnd_6` 一致)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 模型與錯誤分類
|
||||||
|
|
||||||
|
**觀察**(`fnd_9` + `unk_dd4ee2d6bc4685b5` 已解):
|
||||||
|
- 客戶端不選模型:`SAND_DEFAULT_MODEL_ID` 只有 getter(值不可見);`getComputerUseModel` 是用戶設定;字串群 `Cursor Models` / `Includes Cursor Grok and Composer` / `Other Models`。
|
||||||
|
- 程式碼中的模型名:`Wp`(main-core.cjs)含 **`grok-4.5`**;`model_picker_experiments` 含 **`grok-4.6`**。**模型清單由伺服器提供。**
|
||||||
|
- 沒有可見的 model id 字面值(字串表被截斷)。
|
||||||
|
- Provider 錯誤 taxonomy:`provider_400_invalid_request`、`_401_auth_rejected`、`_404_not_found`、`_413_context_too_large`、`_429_quota_exceeded`、`_4xx_rejected`、`_5xx_terminal`、`_image_rejected`、`_user_blocked`、`NonRetryableProviderError`。
|
||||||
|
- `main-app.cjs` 內**沒有**文字代理迴圈、聊天 LLM call、context 壓縮客戶端碼(限定範圍缺席)。
|
||||||
|
|
||||||
|
**推論(derived)**:錯誤 taxonomy 用 HTTP status 分「可重試 / 终止」兩類(`_4xx_rejected` vs `_429_quota_exceeded` vs `_5xx_terminal`),並在客戶端做 retry 決策;`_413_context_too_large` 與 `_image_rejected` 是「把請求重新組裝後重試」的訊號,不是終止訊號。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. 未解問題
|
||||||
|
|
||||||
|
| id | 問題 | 嚴重度 | 狀態 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `unk_67f6ca825e8ffa3f` | 伺服器端 prompt/模型路由/工具 schema 不在 asar 內;唯一路徑是執行期抓取 box 串流(需使用者核准) | high | blocked |
|
||||||
|
| `unk_70f57b409ea35f76` | 回合開始/steering/idle 語義已定案(main-app.cjs @3100283 `P3n`;coordinator `wl`/`k6`、`ZF`、`boxHandoff`);開放項是 `api2.cursor.sh` 的 wire schema | high | open |
|
||||||
|
| `unk_8b8ec93d6245e822` | 側欄 280 px 預設/拖拽 240–400/<210 px 轉 88 px rail/>220 px 展開(index.eager-app @912025 `FT`、@914394 `sKe`、@969523 `QJe`);chat 最小 424 px(`--sand-chat-min-width`);bubble 36/18 px;chat 最大寬 720 px 屬**推測** | medium | open |
|
||||||
|
| `unk_25d3b20c74d75860` | 深色配色由執行期產生(主執行檔 `Grok Bot`,sha256 `df5b07deaa0b9bd42cf63ca2f2478ac629e4fa257324047cdfe8f3ddce4d6684`,對 `font\|pingfang\|hiragino\|width\|height\|minsize\|title` 0 字串命中);重建用 `mechanics.json` 估計配色 | low | blocked |
|
||||||
|
| (fnd_14 衍生) | 哪個請求欄位決定 `CUScrollControlPermit` | medium | open |
|
||||||
|
| (工具層) | application toolset 17 個、projects toolset 27 個工具的確切名稱未逐一確認 | low | open |
|
||||||
|
| `unk_89a8fde07df81e50` | 功能清單 | — | **resolved**(`app/ORIGINAL_FEATURES.md`) |
|
||||||
|
| `unk_dd4ee2d6bc4685b5` | 模型清單來源 | — | **resolved**(伺服器提供;程式碼名 `grok-4.5`/`grok-4.6`) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. 方法與信心度說明
|
||||||
|
|
||||||
|
1. **純靜態**。未執行目標程式任何部分 → 本文無一條「執行過」的斷言。
|
||||||
|
2. **缺席結論全部限定範圍**:
|
||||||
|
- `fnd_9`:字串表被截斷。
|
||||||
|
- `fnd_6`:18 處動態 fetch 點未解析;connect-rpc transport 未完全解析。
|
||||||
|
- `fnd_14`:Ghidra decompiler 拒絕,偽碼全來自內建反編譯器;coverage partial(120/616 函式、80/99 字串)。
|
||||||
|
- §10 的 token 欄位掃描:僅 `dist/electron-main/main.cjs`… 精確說,僅 `dist/electron-main/main-app.cjs`,僅列出的 9 個欄位名。
|
||||||
|
- `js_app_map`:受 25000 節點上限(74905 個 node 被丟)與 `paged_modules` 限制(coverage partial 20/697 modules 逐頁取)。
|
||||||
|
- `fnd_15`:**僅檔名**,非行為證據。
|
||||||
|
3. **asar 目錄清單**只跑到 offset 560/1027,`dist/electron-main/main-app.cjs` 的拼法未在清單直接讀到(前 560 筆被 locales/menubar-tray PNG 與 renderer chunk 吃滿)→ 若要確認,用 `offset=560` 分頁。
|
||||||
|
4. **`kind: unpacked` 的檔案不在 asar 內**,實體在 `Contents/Resources/app.asar.unpacked/<相對路徑>`。
|
||||||
|
5. **evidence id 會過期**(綁定產生它的查詢週期)。因此本報告一律引 **finding id**(`fnd_1`…`fnd_12`、`fnd_14`、`fnd_15`),其 evidence 連結已入庫綁定。
|
||||||
|
6. **重建對應物(`app/src/`)中的每一個 token 數字都是作者設定**,唯一源自 Grok Bot 的引數是 `client_speculative_summarization_config` 的 `0.70` / `0.05` 與一組 byte/char 上限。詳見 `GROK_BOT_TOKENS.md` §11。
|
||||||
|
|
@ -0,0 +1,210 @@
|
||||||
|
# Grok Bot 的 token 計數與 context window 用量,以及重建 harness 的對應做法
|
||||||
|
|
||||||
|
对象:`/Applications/Grok Bot.app`(package name `sand`,Electron 40.1.0,asar 1027 entries,integrity verified)
|
||||||
|
分析目標:`dist/electron-main/main-app.cjs`、`dist/local-exec-daemon/main.cjs`、`dist/node-agent-coordinator/main.cjs`
|
||||||
|
相關 finding:fnd_5、fnd_6、fnd_7、fnd_9、fnd_12、fnd_16
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §0 標記規則(先讀這節)
|
||||||
|
|
||||||
|
本文每條陳述都標明等級:
|
||||||
|
|
||||||
|
- **觀察**:在目標檔案裡直接讀到的欄位名、常數、字面值。**一律附掃描範圍**——「掃了哪個檔、找了哪些關鍵字」。「沒找到」只等於「在該範圍、用該關鍵字沒找到」,不等於「不存在」。
|
||||||
|
- **推論**:從觀察算出來/歸納出來的結論(例:記帳在伺服器端)。
|
||||||
|
- **推測**:最合理的猜測,沒有直接證據。
|
||||||
|
- **作者設定**:**重建 harness(`app/`)裡我自己選的數字**,與 Grok Bot 無關。**不要把這些數字當成 Grok Bot 的事實。**
|
||||||
|
|
||||||
|
引證方式:本文引 **finding id**(`fnd_*`),因為已入庫 finding 的 evidence 連結在入庫時已綁定並驗證。跨輪的 `ev_*` id 不可信任(實測會 `Unknown evidence id`),所以本文只在有**同輪** evidence 的地方列 `ev_*`:本輪的两個 id 是檔案完整性檢查(見 §11 末)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §1 核心結論:Grok Bot 客戶端**沒有** token 計數器;記帳在伺服器端
|
||||||
|
|
||||||
|
**推論**(依據下列觀察 + fnd_5/fnd_6)。
|
||||||
|
|
||||||
|
**觀察(範圍:`dist/electron-main/main-app.cjs`,找這 9 個欄位名)**:`usage`、`prompt_tokens`、`completion_tokens`、`total_tokens`、`token_count`、`context_window`、`max_tokens`、`tokens_used`、`usage_ratio` → **0 命中**;符號索引中含 `token` 的 147 筆全部是**憑證/密閉字串**(token = credential),不是計數。
|
||||||
|
|
||||||
|
**觀察**:客戶端有完整的一組 **byte 上限**與**压缩引數**(§2、§5),但**看不到把字串換算成 token 的通用估算器被使用**:
|
||||||
|
- `estimateMessagesTokenCount` 這個符號存在(約 `main-app.cjs` @2743065 附近),但它的**算術在觀察到的位址不可見**(minified、未展開)。→ 有名字、無實現細節。
|
||||||
|
|
||||||
|
**觀察(與結論一致的正面證據)**:token 用量是**從伺服器事件流裡取出來的**,不是客戶端算的:
|
||||||
|
`compactionSummaryFromEvent`、`applyCompactionSummaryToTimeline`、`applyCompactionSummaryFromEvent`、`turnUsageBreakdownFromEvent`、`usageFromEvent`、`onUsage`、`usageLabel`、`usageLabelForTurn`、`reportUsage`、`turnSteer` + `tokenBudgetsForTurn`。
|
||||||
|
|
||||||
|
**這與 fnd_5/fnd_6 完全對得上**:文字代理迴圈、prompt 組裝、模型路由與 context 管理都在遠端 box/gateway(`api2.cursor.sh` 系),客戶端只是顯示者與工具執行者(fnd_7)。
|
||||||
|
|
||||||
|
**範圍限制**:只掃了上述欄位名,且 fnd_9 已記錄字串表被截斷。所以嚴格的說法是:**「在 main-app.cjs 的 token 計數欄位掃描中未見客戶端計數器;記帳欄位一律以 event 名出現」**,不是「客戶端絕對沒有 token 數學」。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §2 客戶端唯一的 token 換算:字元數 / 4(僅限語音逐字稿)
|
||||||
|
|
||||||
|
**推論**:在觀察範圍內,客戶端唯一看得見的 token 除法在語音通話路徑上:
|
||||||
|
|
||||||
|
- `voiceCallTranscriptCharsPerToken = 4.0`(**觀察**)
|
||||||
|
- 算式形如 `ceil(charLen / charsPerToken)`(**觀察**)
|
||||||
|
- 預算 `voiceCallTranscriptTokenBudget = 12000`(**觀察**)
|
||||||
|
|
||||||
|
**推論**:這是「ASCII 約 4 字元 = 1 token」的粗估,只用來控制**逐字稿長度**,不是整個對話的 context 管理。
|
||||||
|
|
||||||
|
**作者設定(重建做法)**:我的 harness 不內嵌 tokenizer(零相依,不帶 `gpt-tokenizer`/`js-tiktoken`)。規則:ASCII byte → 1 token,非 ASCII byte → 0.5 token。這是**我選的**,Grok Bot 只用 4.0 字元/token 於語音逐字稿。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §3 真正的權威值:讀 provider 回來的 usage
|
||||||
|
|
||||||
|
**觀察**:Grok Bot 的用量數字來自伺服器事件(§1 的 `usageFromEvent` / `turnUsageBreakdownFromEvent` / `onUsage`)。
|
||||||
|
|
||||||
|
**作者設定(重建做法)**:不自己數,**讀 provider 回傳的 `usage`**。OpenAI-compatible 串流的最後一個 SSE chunk 帶 `usage.prompt_tokens` / `usage.completion_tokens` / `usage.total_tokens`(請求側要發 `stream_options: {"include_usage": true}`)。規則:
|
||||||
|
1. 每個 LLM 事件流結束時取**最後一個**帶 `usage` 的 chunk 當權威值;
|
||||||
|
2. 佔用率 = `usage.total_tokens / contextWindow`,寫入事件流(`tokens_used` + `source: "provider"`);
|
||||||
|
3. provider 沒給 `usage` 時才退回本地估計,並在事件上標 `est.`,UI 顯示 `~`;
|
||||||
|
4. 估計器與權威值並存,永不互相覆蓋——這樣才知道估計誤差。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §4 每工具的輸出上限:用 byte 和行列卡,不用 token
|
||||||
|
|
||||||
|
**觀察(範圍:`dist/local-exec-daemon/main.cjs`,fnd_7 的工具實作)**:
|
||||||
|
|
||||||
|
| 工具 | 上限欄位 | 值 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `read_file` | `maxBytes` | 預設 **131072**(128 KiB) |
|
||||||
|
| `readTerminalFile`(背景 shell 輸出) | `maxBytes` | **65536**(64 KiB) |
|
||||||
|
| `ripgrep` | `maxResults` | **200** |
|
||||||
|
| `ripgrep` | `timeoutMs` | **20000** |
|
||||||
|
|
||||||
|
**推論**:Grok Bot 控制 context 膨脹的主要手段是**在工具邊界卡 byte 與條數**,讓單次工具結果不可能撐爆 context,而不是把工具結果送去數 token。
|
||||||
|
|
||||||
|
**作者設定(重建做法)**:同樣策略——每個工具都有 `maxBytes` + 行/列上限,回傳時附 `truncated: true` 與「如何取下一页」的提示(offset/cursor)。**我的數值與上表相同**(131072 / 65536 / 200 / 20000 直接採用,因為它們是 Grok Bot 的觀察值,屬於可借鑑的設計常數)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §5 壓縮觸發點:Grok Bot 用「比例」,而且是 70 %
|
||||||
|
|
||||||
|
**觀察(範圍:`dist/electron-main/main-app.cjs`)**——本報告**唯一直接來自 Grok Bot 的數字組**:
|
||||||
|
|
||||||
|
```
|
||||||
|
client_speculative_summarization_config = {
|
||||||
|
enabled: true,
|
||||||
|
thresholdRatio: 0.70,
|
||||||
|
toleranceRatio: 0.05,
|
||||||
|
modelId: "grok-4.5"
|
||||||
|
}
|
||||||
|
speculativeSummarizationClientGates = { enabled, modelTier: "default", contextRatio: 0.70 }
|
||||||
|
```
|
||||||
|
|
||||||
|
**推論**:
|
||||||
|
- `thresholdRatio: 0.70` = context 用量到 **70 %** 觸發(**推論**:欄位語意由名字與 `contextRatio` 同名值一致支持,非文件證明)。
|
||||||
|
- `toleranceRatio: 0.05` = **5 %** 容差 → 觸發後压到 **65 %** 以下(`0.70 - 0.05`)。**0.70 與 0.05 是觀察;「压到 65 %」是把容差讀成遲滯目標的推論。**
|
||||||
|
- `modelId: "grok-4.5"` = 做摘要的模型由客戶端設定送出去(與 fnd_12 的「客戶端擁有設定、推送到 box」同形)。
|
||||||
|
- 「speculative」(投機式)+ 客戶端 gate(`enabled`、`modelTier`)→ **推測**:摘要是在回合進行中/之前**預先算好**,需要時直接採用,避免在關鍵時刻卡一次摘要請求。這是對命名的解讀,無行為證據。
|
||||||
|
|
||||||
|
**觀察**:压縮結果以 **`compactionSummary`** 的名字從**伺服器事件**送達,並套用到 timeline(§1)→ 摘要在事件流裡是**一等公民**(可重放、可重算 timeline),不是就地改寫訊息歷史。
|
||||||
|
|
||||||
|
**作者設定(重建做法)**:採用同一對引數——**觸發 70 %,目標 65 %**(遲滯,避免在臨界點反覆壓縮)。這是我從 Grok Bot 借鑑的**唯一**一組數值;其他比例都是我的選擇。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §6 壓什麼:位元組上限講得很清楚
|
||||||
|
|
||||||
|
**觀察(範圍:`dist/electron-main/main-app.cjs`)**:
|
||||||
|
|
||||||
|
| 欄位 | 值 | 附註 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `summarizationInputTokenLimit` | **360000** | 碼內註解寫「bytes (ASCII=1 token)」 |
|
||||||
|
| `summarizationInputByteLimit` | **360000**(預設 300000) | |
|
||||||
|
| `summarizationInputBytes` | **120000** | |
|
||||||
|
| `summarizationInputTokenLimitCap` | 0 = 停用 | 上限的上限 |
|
||||||
|
| `summarizationOutputCap` | **6000** | 摘要輸出 |
|
||||||
|
| `MAX_SUMMARY_BYTES` | **100 × 1024** | |
|
||||||
|
| `summarizeMessages` | 函式名 | |
|
||||||
|
|
||||||
|
**推論**:
|
||||||
|
1. 全鏈路都以 **byte** 為單位;「token」在客戶端只是「ASCII byte」的別名(註解自己寫了 ASCII=1 token)。
|
||||||
|
2. **輸入**有 300 000–360 000 byte 的送件上限,實際一次用 120 000 byte;**輸出**摘要 ≤ 6000(單位未標,**推測**為 token 上限)、單條摘要 ≤ 100 KiB。
|
||||||
|
3. 因為摘要由伺服器產生(§1),這些限制更像是**客戶端決定送多少歷史去摘要**。
|
||||||
|
|
||||||
|
**作者設定(重建做法)**:我的 compress 事件記錄 `beforeTokens` / `afterTokens` / `modelUsed` / `strategy`;摘要本身是 append-only 事件,timeline 由「最近 N 條原文 + 之前所有摘要」重組;保留最後一輪完整不压,避免模型忘記自己剛剛在做什麼。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §7 context 爆了怎麼辦:413 的分類與一次重試
|
||||||
|
|
||||||
|
**觀察(fnd_9,範圍 `main-app.cjs`)**:provider 錯誤 taxonomy 含 `provider_413_context_too_large`,以及 `provider_400_invalid_request`、`_401_auth_rejected`、`_404_not_found`、`_429_quota_exceeded`、`_4xx_rejected`、`_5xx_terminal`、`_image_rejected`、`_user_blocked`、`NonRetryableProviderError`。
|
||||||
|
|
||||||
|
**推論**:`413` 被当成**可處理事件**(有自己的代號),不是笼統的 4xx;`NonRetryableProviderError` 與 `_5xx_terminal` 存在 → 重試策略是**依代號分類**的。
|
||||||
|
|
||||||
|
**作者設定(重建做法)**:收到 `413` → **強制壓縮一次**(不等到 70 %)→ 重發一次;第二次還失敗就帶著錯誤終止回合,並在事件流記 `provider_413_context_too_large`。`429` 讀 `retry-after`;`4xx` 不重試(對應 fnd_9 的 non-retryable 分類)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §8 別把「配額」讀成「context window」
|
||||||
|
|
||||||
|
**觀察(範圍:`dist/electron-main/main-app.cjs`)**:`token_budget = 100000`、`token_budget_per_user = 30000`、`token_budget_per_user_7d = 100000`、`token_budget_per_team`、`usage_warning_threshold = 0.8`、`usage_limit_floored`。
|
||||||
|
|
||||||
|
**推論**:`per_user` / `per_user_7d` / `per_team` 這種**按使用者/團隊/7 天視窗**的分桶是**計費與配額**語彙,context window 不會按使用者分桶。`usage_warning_threshold = 0.8` 是「用量到 80 % 要示警」的**配額**示警。
|
||||||
|
|
||||||
|
**结论**:Grok Bot 的 **context window 大小不在客戶端**(找不到 `context_window`,§1)。客戶端看到的 `token_budget*` 是配額。
|
||||||
|
|
||||||
|
**作者設定(重建做法)**:兩個數字必須分開。`--context-window`(我預設 **8000**,CLI 可蓋)控制壓縮;配額是另一個概念,我的 harness 不做配額(沒有伺服器)。`usage_warning_threshold = 0.8` 這個「80 % 示警」我沿用為 **CLI 警示條**的閾值(借用其形式,非借用其語意)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §9 用量要能對到回合
|
||||||
|
|
||||||
|
**觀察(範圍:`dist/electron-main/main-app.cjs`)**:`turnUsageBreakdownFromEvent`、`usageLabelForTurn`、`tokenBudgetsForTurn`、`turnSteer`、`reportTurnClientOutcome`、`reportRenderTtfr`。
|
||||||
|
|
||||||
|
**推論**:token 用量**按回合歸屬**(per-turn breakdown),而且客戶端會**回報自己這端的結果**(`reportTurnClientOutcome`)與**首字延遲**(`reportRenderTtfr`)→ 伺服器是記帳中心,客戶端是觀測點之一。`turnSteer` + `tokenBudgetsForTurn` 一起出現 → **推測**:每回合有 token 預算,steering(中途插入指示)時會重算。
|
||||||
|
|
||||||
|
**作者設定(重建做法)**:事件流裡每條 `llm.usage` 事件帶 `turnId`;`/usage` 指令印出 per-turn 表格(每回合 prompt/completion/total + 佔 context 百分比 + 是否 `est.`)。我的 harness 不做遠端上報。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §10 讓它看得見:CLI 與 UI 的可見性
|
||||||
|
|
||||||
|
**觀察**:客戶端有 `usageLabel` / `usageLabelForTurn` → **推論**:UI 上有專用的用量字串(每回合一條)。
|
||||||
|
|
||||||
|
**作者設定(重建做法)**:
|
||||||
|
- CLI 每個回合後印一行 `tokens 1234/8000 (15%)`, provider 值直接顯示,估計值前綴 `~`。
|
||||||
|
- 超過 `--context-window` 的 **80 %** 時在同一行尾加警示條 `[████░░]`,並在 compress 事件發生時印 `compacted 4100 → 1500`。
|
||||||
|
- `--print` 模式结束時印總計。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §11 數字來源對照表(最重要的一節)
|
||||||
|
|
||||||
|
| 數字 | 屬於誰 | 出處與標記 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **0.70**(`thresholdRatio`)、**0.70**(`contextRatio`) | **Grok Bot 用戶端** | **觀察**:`client_speculative_summarization_config` / `speculativeSummarizationClientGates`(`main-app.cjs`)。**這是本報告唯一直接來自 Grok Bot 的比例數字。** |
|
||||||
|
| **0.05**(`toleranceRatio`) | **Grok Bot 用戶端** | **觀察**:同上欄位。 |
|
||||||
|
| `modelId: "grok-4.5"`(做摘要的模型) | **Grok Bot 用戶端** | **觀察**:同上欄位(模型清單由伺服器提供,見 fnd_9 / unk_dd4ee2d6bc4685b5 已解)。 |
|
||||||
|
| 360000 / 300000 / 120000 / 6000 / 100×1024 byte 上限、`MAX_SUMMARY_BYTES` | **Grok Bot 用戶端** | **觀察**:`summarizationInput*`、`summarizationOutputCap`、`MAX_SUMMARY_BYTES`(`main-app.cjs`)。單位是 **byte**(檔內註解:ASCII=1 token)。 |
|
||||||
|
| 131072 / 65536 / 200 / 20000(每工具上限) | **Grok Bot 用戶端** | **觀察**:`dist/local-exec-daemon/main.cjs`(fnd_7)。 |
|
||||||
|
| 4.0 chars/token、12000 | **Grok Bot 用戶端(僅語音逐字稿)** | **觀察**:`voiceCallTranscriptCharsPerToken`、`voiceCallTranscriptTokenBudget`。 |
|
||||||
|
| 100000 / 30000 / 100000 / 0.8 | **Grok Bot 用戶端,但是「配額」不是 context window** | **觀察**:`token_budget*`、`usage_warning_threshold`(§8 推論)。 |
|
||||||
|
| **65 %**(压縮目標) | **推論 + 作者設定** | 由 `0.70 − 0.05` 得出的**推論**;作為我 harness 的固定目標值则是**作者設定**。Grok Bot 碼內**沒有**「65 %」這個字面值。 |
|
||||||
|
| **72 000**、**100 000**(重建檔內出現的 context 尺寸範例) | **作者設定** | 我舉的例子,與 Grok Bot 無關。 |
|
||||||
|
| **8000**(`--context-window` 預設) | **作者設定** | 我的 CLI 預設值。**Grok Bot 客戶端沒有 context window 數字**(§1 掃描 0 命中)。 |
|
||||||
|
| **80 %**(CLI 警示條閾值) | **作者設定**(形式借鏡 `usage_warning_threshold = 0.8`) | 語意不同:Grok Bot 那個是配額示警。 |
|
||||||
|
| ASCII 1 token / 非 ASCII 0.5 token 的估計器 | **作者設定** | 我的 harness 規則;Grok Bot 只在語音逐字稿用 4.0 chars/token。 |
|
||||||
|
|
||||||
|
**一句話**:**只有 0.70 / 0.05 這一對壓縮引數,以及一組 byte/char 上限,是從 Grok Bot 用戶端觀察得來的**;其餘任何 token/context 數字都是我自己的設定或範例。
|
||||||
|
|
||||||
|
**本輪檔案完整性檢查(觀察,同輪 evidence)**:`GROK_BOT_TOKENS.md` 10785 B、`GROK_BOT_FEATURES.md` 20998 B,sha256 `db9f4c11b1ea18855eaac90d0eb15feea6000da92029516b28e02c285772661b` / `fd753cf232f6959e0bb63cd813bc26a8b55753ae16ac1174aa03f247df1f82d3`(`ev_c091620b0c5bc68527e432b2d02d6fe80c4d5ac7d1bd928d49f0b80db8c63174`、`ev_2f4ca1c5d683d7ad93da7316b4fd07eac9100f2bbff0f9ce166269c4de03cf58`;入庫為 fnd_16)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §12 方法與限制
|
||||||
|
|
||||||
|
**方法**:靜態分析。asar 清單(1027 entries,integrity verified)→ `js_app_map` 套裝圖 → 對 `dist/electron-main/main-app.cjs`、`dist/local-exec-daemon/main.cjs`、`dist/node-agent-coordinator/main.cjs` 做符號/欄位名掃描。**沒有執行目標**,因此本文所有陳述都只證明「碼裡寫了什麼」,不證明「跑起來會怎樣」。
|
||||||
|
|
||||||
|
**限制(逐條)**:
|
||||||
|
1. 「找不到 token 計數欄位」的範圍是 **main-app.cjs + 列出的 9 個欄位名**;fnd_9 已記錄字串表被截斷,fnd_6 已記錄 **18 個動態 fetch 點**與 connect-rpc transport 未完全解析。
|
||||||
|
2. `estimateMessagesTokenCount` 的算術不可見;`summarizationOutputCap` 的單位未標。
|
||||||
|
3. `thresholdRatio` / `toleranceRatio` 的**語意**是從命名與 `contextRatio` 一致性推來的(推論),沒有註解或文件直接說明「壓到 65 %」。
|
||||||
|
4. 伺服器端的 prompt 組裝、模型路由、真正的 context window 大小與計費邏輯**不在 asar 裡**(unk_67f6ca825e8ffa3f,high,blocked);唯一途徑是執行期擷取 box 事件流(需要使用者核准執行目標)。
|
||||||
|
5. `js_app_map` 有 25 000 節點上限(74905 個被丟)與 `paged_modules` 覆盖限制;asar 目錄清單我看過 offset 0–560。
|
||||||
|
6. 本輪無檔案內容讀取工具,`core_archive_show` 對單一文字檔只回 size/sha256、不回正文 → 本文是依分析結果重組,非讀回舊檔。
|
||||||
|
|
||||||
|
**未解(對應 unk id)**:`unk_67f6ca825e8ffa3f`(伺服器端 prompt/模型路由/工具 schema);`estimateMessagesTokenCount` 算術;`CUScrollControlPermit` 由哪個請求欄位決定(fnd_13/fnd_14)。
|
||||||
|
|
@ -0,0 +1,591 @@
|
||||||
|
# How the original works (and what this rebuild copies)
|
||||||
|
|
||||||
|
Original: Grok Bot 0.78.1, an Electron app.
|
||||||
|
|
||||||
|
## The native main program (decompiled)
|
||||||
|
`Contents/MacOS/Grok Bot` (Mach-O arm64, 53 040 bytes, sha256 df5b07de…). Opened with
|
||||||
|
core_target_open, and every function read with native_function_pseudocode:
|
||||||
|
|
||||||
|
| Address | Function | What it does |
|
||||||
|
|---|---|---|
|
||||||
|
| `0x100000460` | `entry` | Calls `0x1000004c0`, then `electron::fuses::IsRunAsNodeEnabled()`. If the fuse is on and `getenv("ELECTRON_RUN_AS_NODE")` is a non-empty string, it runs `ElectronInitializeICUandStartNode`, otherwise `ElectronMain`. |
|
||||||
|
| `0x1000004c0` | `sub_1000004c0` | For stdin/stdout/stderr (fd 0, 1, 2): if `fstat` fails with `errno == 9` (EBADF), reopens that stream on `/dev/null`. |
|
||||||
|
| `0x1000005bc`–`0x100000604` | import stubs | `ElectronInitializeICUandStartNode`, `ElectronMain`, `getenv`, `IsRunAsNodeEnabled`, `freopen`, `__error`, `fstat`. |
|
||||||
|
|
||||||
|
These are all 10 functions in the file (evidence `ev_407aa9c3…`). `native_strings_find` for
|
||||||
|
`model|grok|provider|chat|bot|theme|api` finds **0 strings** (evidence `ev_1b74b817…`), so none of the
|
||||||
|
chat, model or settings behaviour is native code. It lives in the JavaScript inside
|
||||||
|
`Contents/Resources/app.asar`. The sources below therefore give **file + byte offset (@N) + minified
|
||||||
|
function name**, the JavaScript equivalent of a function address.
|
||||||
|
Pseudocode evidence: `ev_7c151be5…` (entry), `ev_29ffc0d8…` (sub_1000004c0).
|
||||||
|
Bundle map evidence: `ev_2bb06b4aef290dafe0a74085416f36482d30fc7ecc476b97f343e324ceb9c9f7` (js_app_map).
|
||||||
|
|
||||||
|
## Errors, retries and timeouts
|
||||||
|
From `dist/electron-main/main-core.cjs`.
|
||||||
|
|
||||||
|
| Rule | Value | Source |
|
||||||
|
|---|---|---|
|
||||||
|
| Retry attempts | 3 | @1082927 `D3={name:"gateway-create-agent-retry",maxAttempts:3,…}` |
|
||||||
|
| Retry delay | starts at 1000 ms, ×2 each time, at most 4000 ms → 1 s, 2 s, 4 s | same object: `initialDelayMs:1e3,maxDelayMs:4e3,backoffFactor:2` |
|
||||||
|
| Connect deadline | 15 000 ms | @1082927 `$D=15e3` → `connectDeadline` "gateway-sse-connect" (@1084759 `L3`) |
|
||||||
|
| Stream silence counts as a stall | 35 000 ms | @1082927 `VD=35e3` → `stallWatchdog` "gateway-sse-stall", `idleMs:VD` (@1084759 `L3`) |
|
||||||
|
| Provider overloaded / rate limit | retry | @689060 `SAND-E0401 providerOverloaded, retryable:!0` |
|
||||||
|
| Nothing streamed by the first-token deadline | retry | @689284 `SAND-E0402 firstTokenStall, retryable:!0`. The deadline itself is server-side: **estimated 120 s** |
|
||||||
|
| Transport reset | retry | @689501 `SAND-E0403 streamReset, retryable:!0` |
|
||||||
|
| Context window overflow | no retry | @689722 `SAND-E0404 contextWindowOverflow, retryable:!1` |
|
||||||
|
| Backend rejected | no retry | @689948 `SAND-E0405 backendRejected, retryable:!1` |
|
||||||
|
| Server-paced retry-after | wait the given delay, then retry | @690623 `SAND-E0408 backendCapacityDeferred` (30 s cap: **estimated**) |
|
||||||
|
| Silence after output started | cancel; resumed only from a checkpoint | @691348 `SAND-E0415 streamIdleStall`, so the rebuild never auto-retries once text has appeared |
|
||||||
|
| Backend unreachable (refused / DNS) | retry | @692409 `SAND-E0418 turnBackendUnreachable, retryable:!0` |
|
||||||
|
| Rate-limit banner | "Rate limited by model provider, retrying…" | `main-app.cjs` `rate_limited_reconnect_message_config` |
|
||||||
|
| Error texts | provider_overloaded → `wbQwKx`; first_token_stall → `Ly7/5m` "模型未能及時開始回應。請重試。" | `index.eager-app` @1592250, map `YN` |
|
||||||
|
|
||||||
|
How the rebuild applies them: 429, 408, 5xx, 529 and connection failures retry up to 3 tries, waiting
|
||||||
|
1 s, then 2 s, then 4 s (or the server's Retry-After). The reply shows "…秒後重試(第 n 次,共 3 次)"
|
||||||
|
meanwhile. A 400 that says the prompt is too long stops with a "對話太長了" message, and 401/403/404
|
||||||
|
stop at once. A reply that goes 35 s silent after it started is stopped, and the partial text is kept.
|
||||||
|
|
||||||
|
## Models
|
||||||
|
| What | Value | Source |
|
||||||
|
|---|---|---|
|
||||||
|
| Default model | `grok-4.5` | `main-core.cjs`, `var Wp="grok-4.5"`, exported as `SAND_DEFAULT_MODEL_ID` |
|
||||||
|
| Model choice shape | `{modelId, maxMode, parameters:[{id,value}]}` | `main-core.cjs`, schema `UEe` / `bh` |
|
||||||
|
| Model list | fetched from the server, not stored in the app | `model_picker_experiments` in `main-app.cjs` names only `grok-4.5` and `grok-4.6` |
|
||||||
|
| Setting text | "模型:選擇 Grok Bot 用來思考的模型" | `index.eager-app` settings map `b$.model` → ids `scu3wk` / `2RPCms` |
|
||||||
|
| Overload message | "模型供應商已超載" / "模型供應商目前負載過高。通常是暫時的。" | zh-TW catalog ids `Rdu4z7`, `wbQwKx` |
|
||||||
|
|
||||||
|
The original only talks to its own cloud service. **The rebuild's change**, as the person asked:
|
||||||
|
every bot picks its own provider (xAI Grok, OpenAI, Claude, Gemini, OpenRouter, Ollama, or any
|
||||||
|
OpenAI-compatible service) and model. Keys live in Settings → AI 供應商.
|
||||||
|
|
||||||
|
## Message box
|
||||||
|
| Rule | Value | Source |
|
||||||
|
|---|---|---|
|
||||||
|
| Send | Enter sends; Shift+Enter adds a new line; Enter is ignored while an input method is composing | `index.eager-app` @2002346 `mwt`: `e.key==="Enter"&&!e.shiftKey&&!e.nativeEvent.isComposing&&e.preventDefault()` |
|
||||||
|
| Placeholder | "傳訊息給 {name}" (fallback "想問什麼都可以,或拖入檔案") | `index.eager-app` function `E6t`, ids `pqr+oY` / `HOCPPm` |
|
||||||
|
|
||||||
|
## Sidebar
|
||||||
|
From `dist/renderer/assets/index.eager-app-D1juOWZp.js`.
|
||||||
|
|
||||||
|
| Rule | Value | Source |
|
||||||
|
|---|---|---|
|
||||||
|
| Default width | 280 px | @912025 `FT={expandedWidth:280,isCollapsed:!1}` |
|
||||||
|
| Width limits | 240–400 px | @914394 `sKe`: `jT(UT(t.expandedWidth,280),240,400)`; @969523 `QJe`: `Math.max(240,Math.min(400,…))` |
|
||||||
|
| Folded width | 88 px rail | @969523 `fD=88` |
|
||||||
|
| Fold when dragged below | 210 px | @969523 `JJe=210` |
|
||||||
|
| Unfold from the rail when dragged to | 220 px | @969523 `YJe=220` |
|
||||||
|
| Saved keys | `sand.sidebar.width`, `sand.sidebar.collapsed` | @912025 `BT` |
|
||||||
|
| Chat minimum width | 424 px | index CSS `--sand-chat-min-width:424px` (class sand-1fiktjy); window minimum = 240 + 424 |
|
||||||
|
| Sections | 主要 Bot, 已釘選的 Bot, Bot | zh-TW ids `V8a0q9`, `vPVg0Q`, `ixkTse` |
|
||||||
|
| Search | "搜尋 Bot"; empty result "沒有相符的 Bot" | ids `YjO4Og`, `12i8o8` |
|
||||||
|
|
||||||
|
## Chat
|
||||||
|
| What | Value | Source |
|
||||||
|
|---|---|---|
|
||||||
|
| Bubble corner | 18 px | index CSS `--sand-transcript-prose-bubble-radius:18px` |
|
||||||
|
| Bubble text | 15 px on a 22 px line | `--sand-transcript-prose-line-height:22px` with `--sand-font-size-lg:15px` |
|
||||||
|
| One-line bubble height | 36 px (7 px above and below) | `--sand-transcript-prose-one-line-height:36px`, `--sand-transcript-prose-block-padding:7px` |
|
||||||
|
| UI text sizes | 11/12/13/14/15/17/22/26 px, base 14 px | `--sand-font-size-xxs` … `--sand-font-size-xxxl` |
|
||||||
|
| Transcript item kinds | user, thinking, assistant-text, tool-call, send-message | `index.eager-app` function `zfn` |
|
||||||
|
| Thinking label | "思考中" | id `AUV+TY` |
|
||||||
|
| Message actions | 複製, 重新傳送, 停止 | ids `he3ygx`, `MyjAbr`, `NnvXp5` |
|
||||||
|
|
||||||
|
## Bots
|
||||||
|
| What | Value | Source |
|
||||||
|
|---|---|---|
|
||||||
|
| Bot fields | 名稱, 描述, 頭像 (shape + colour) | ids `RFtRvq`, `NDq8h1`, `fopc9x` |
|
||||||
|
| Avatar shapes | 26: arch, bean, blob, capsule, cloud, clover, crystal, cylinder, dome, egg, flower, gem, heart, hex, house, leaf, pebble, pumpkin, shield, sparkle, square, squircle, star, tablet, teardrop, wedge | `dist/electron-main/menubar-tray/assets/bot-*.png` |
|
||||||
|
| Rebuild shapes | 12 of them, drawn as simple shapes (look estimated) | own drawing |
|
||||||
|
|
||||||
|
## Look (light theme)
|
||||||
|
From `dist/renderer/assets/index-Dh7EWA8t.css`, block `:root,.sand-1vbcnu7`:
|
||||||
|
pane `#fff`, chrome `#fcfcfc`, sidebar `#f5f5f5`, text `#1c1c1c`, secondary `#1c1c1cad`,
|
||||||
|
tertiary `#1c1c1c8f`, hover `#1c1c1c0f`, active `#1c1c1c14`, border `#1c1c1c1f`,
|
||||||
|
user bubble `#1c1c1c` with white text, agent bubble `#1c1c1c0f`, focus `#1e86fc`, accent `#1680f3`.
|
||||||
|
Bot colours come from `--sand-fill-*-bubble-user`: red `#c82532`, orange `#d2550a`, yellow `#c2730b`,
|
||||||
|
green `#0d9655`, cyan `#0d9381`, blue `#126fd4`, purple `#7442d1`, magenta `#c82779`, brown `#86592e`.
|
||||||
|
|
||||||
|
Settings → 主題: 淺色 / 深色 / 跟隨系統 (ids `1njn7W`, `pvnfJD`, `Ea4oxV`).
|
||||||
|
強調色: Bot 顏色 / 黑色 (ids `p7v5PZ`, `THokF3`, description `eeSkqM`).
|
||||||
|
|
||||||
|
Dark theme colours weren't found in the stylesheet or the scripts: **estimated**.
|
||||||
|
Chat column 720 px: **estimated**. No transcript max-width was found; the 708, 640 and 960 px max-widths
|
||||||
|
in the CSS belong to other views.
|
||||||
|
|
||||||
|
## Computer tools and the work loop (taken over from the neighbouring deep investigation)
|
||||||
|
The deep investigation in `investigation.cany` (findings fnd_5–fnd_12) showed that the original's
|
||||||
|
agent loop runs on a remote "box". The desktop app only hosts a local tool runner
|
||||||
|
(`dist/local-exec-daemon/main.cjs`) and the permission layer. That session planned its own clean-room
|
||||||
|
harness (plan step 1 "doing", no code written yet). This version builds that plan here, in Rust, with
|
||||||
|
the model loop running locally against whichever provider the bot uses.
|
||||||
|
|
||||||
|
| Rule | Value | Source |
|
||||||
|
|---|---|---|
|
||||||
|
| Search deadline | 25 000 ms | local-exec-daemon @2983388 `Mu.MAIN_TIMEOUT_MS=25e3` |
|
||||||
|
| Search hard cap | 10 000 lines | same, `Mu.HARD_MAX_OUTPUT_LINES=1e4` |
|
||||||
|
| Lines handed to the model | 2 000 | @2966784 `Jut=2e3` (client limit) |
|
||||||
|
| Largest file read | 8 388 608 bytes (8 MiB) | @2951190 `gut` |
|
||||||
|
| Long command | not killed: moved to the background, read later | @3010873 `Mye` (stop kind "background", reason TIMEOUT), @3016213 `timeoutBehavior===BACKGROUND` |
|
||||||
|
| Background read-back | `read_terminal` | @2959283 `readTerminalFile` |
|
||||||
|
| Command output kept | start + end, middle counted | ShellFailure fields `output_head`, `output_tail`, `elided_chars` (@2186946); 6 000 + 6 000 chars **estimated** |
|
||||||
|
| Foreground wait before background | 30 s, model may ask up to 600 s | **estimated** (the original gets it per call from the server) |
|
||||||
|
| Permission order | refuse > hidden files > allowed > ask | fnd_8: `shouldBlockShellCommand`, `isShellCommandFullyAllowlisted`, cursorignore/repo-block globs, approval callbacks |
|
||||||
|
| Chained commands | split on `&&`, `||`, `;`, `|` and checked one by one; `$(…)`, backticks, `eval` always ask | fnd_8 (tree-sitter-bash parsing), own splitter |
|
||||||
|
| "Always allow" | remembered per bot (e.g. "修改檔案", "npm install") | fnd_12 `recordLocalToolStandingGrant` |
|
||||||
|
| Never claim done without a result | in the bot's instructions | fnd_10 design lesson (paraphrased, not copied) |
|
||||||
|
| Summarize at | 70 % of the context | main-app.cjs `client_speculative_summarization_config.tokenUsageThresholdPercentage:70` |
|
||||||
|
| Extra room mid-task | +5 % (summarize again at 75 % between steps) | same config, `tolerancePercentage:5` (how it's applied is our reading) |
|
||||||
|
| Context size | 128K tokens by default, per bot | **estimated** |
|
||||||
|
| Token estimate | 4 Latin chars or 1 Chinese char ≈ 1 token | **estimated** |
|
||||||
|
| Steps per turn | 25 | **estimated** |
|
||||||
|
| Messages kept word for word | newest 6, cut only before a user message | **estimated** |
|
||||||
|
|
||||||
|
### Native helpers behind the tools (decompiled)
|
||||||
|
|
||||||
|
**`cursor_proclist.node`** (Mach-O arm64 bundle, 125 536 bytes, sha256 d5df3fb7…), the original's
|
||||||
|
process scanner. Strings found with native_strings_find (evidence `ev_3d585c56…`), and functions read with
|
||||||
|
native_function_pseudocode:
|
||||||
|
|
||||||
|
| Address | Function | Rule taken |
|
||||||
|
|---|---|---|
|
||||||
|
| `0x6748` | `getDescendantPids(vector<int>, bool)` | Breadth-first over children: `proc_listpids(6 = PROC_PPID_ONLY, pid, buf, 0x1000)`, de-duplicated in a hash set, and the loop stops once `0xfff` pids are collected, **at most 4096 processes** (evidence `ev_768ea7aa…`) |
|
||||||
|
| `0x3d00` | `ProclistScanWorker::Execute` | Calls `getDescendantPids(roots, true)` (roots included), then `getProcessInformation` for each and drops any that fail (evidence `ev_6aba01a8…`) |
|
||||||
|
| `0x5234` | `getProcessInformation(int, ProcessInfo&)` | `proc_pidinfo(pid, 3 = PIDTBSDINFO, 0x88)` for the parent; name via `proc_name`, else "<unknown>"; arguments via `sysctl` KERN_PROCARGS2 only when under `0x40000` bytes; agent tags read from the environment (`…CONVERSATION_ID=` 0x17 chars, `…REQUEST_ID=` 0x12 chars); `proc_pidinfo(pid, 4 = PIDTASKINFO, 0x60)`: **memoryMB = resident bytes >> 20**, **cpuTimeMs = (user + system) × timebase.numer / timebase.denom / 1 000 000** (magic-number divide `0x431bde82d7b634db >> 18`) (evidence `ev_257a505c…`) |
|
||||||
|
| `0x1fdc`, `0x6cc0` | `cursorProclistSystemMemory`, `getSystemMemoryInfo` | Reads `kern.memorystatus_vm_pressure_level` (not used here) |
|
||||||
|
|
||||||
|
How the rebuild applies it (app tools.rs `descendants`, `tree_stats`, `kill_tree`): every command is
|
||||||
|
tagged with `ANYBOT_CONVERSATION_ID` (the bot) and `ANYBOT_REQUEST_ID` (the tool call). These are our
|
||||||
|
own names for the original's vendor-prefixed tags. Pressing stop, or quitting, ends the command **and
|
||||||
|
everything it started** (up to 4096 processes, newest first), not just the shell. `read_terminal` reports
|
||||||
|
process count, CPU seconds and memory MB with the formulas above (via `ps`).
|
||||||
|
|
||||||
|
**`sand-op-launcher`** (universal Mach-O, 136 368 bytes, sha256 1337d620…). Strings in evidence
|
||||||
|
`ev_7d29f77e…`; `_main` read with native_function_pseudocode (evidence `ev_8006d51c…`):
|
||||||
|
- It verifies the 1Password CLI's code signature (`SecStaticCodeCheckValidity` against
|
||||||
|
`identifier "com.1password.op" … OU 2BUA8C4S2C`), and re-checks the file before `execve` ("changed before launch").
|
||||||
|
- It accepts only exact argument shapes (`item move`, `item list`, `vault list/create`, `account list`,
|
||||||
|
`service-account create`, `--version`); anything else is refused ("unsupported invocation").
|
||||||
|
- It applies the sandbox profile `(version 1)(allow default)(deny file-read* (with no-report) (subpath (param "ONEPASSWORD_GROUP_CONTAINER")))`.
|
||||||
|
- It builds a fresh environment (`OP_CACHE=false`, `OP_BIOMETRIC_UNLOCK_ENABLED=true`, `OP_LOAD_DESKTOP_APP_SETTINGS=false`).
|
||||||
|
|
||||||
|
How the rebuild applies it (app tools.rs `build_command`): on macOS every command runs under
|
||||||
|
`sandbox-exec` with the same profile pattern, extended to deny reading **and writing** the app's own data
|
||||||
|
folder (where API keys are saved), `~/.ssh`, `~/.gnupg`, `~/Library/Keychains`, `~/.aws` and
|
||||||
|
`~/.config/gcloud`. Paths are resolved first, because the sandbox matches real paths. The provider API-key
|
||||||
|
variables are removed from the command's environment, and the app's data folder is also on the
|
||||||
|
never-read list for file tools.
|
||||||
|
|
||||||
|
Read-only commands that run without asking: ls, pwd, cat, head, tail, wc, echo, rg, grep, find, file,
|
||||||
|
stat, du, df, date, which, tree, sort, uniq, diff, git status/diff/log/show (own list). Always refused:
|
||||||
|
sudo, su, rm -rf /, rm -rf ~, mkfs, dd, shutdown, reboot, diskutil erase. Never read or written:
|
||||||
|
.git internals, .ssh, .gnupg, Keychains, .env files, *.pem, *.key, id_rsa/id_ed25519, .netrc.
|
||||||
|
|
||||||
|
## Turns, permission hand-over, computers and app control
|
||||||
|
|
||||||
|
### What counts as one turn
|
||||||
|
| Rule | Value | Source |
|
||||||
|
|---|---|---|
|
||||||
|
| A turn starts with | a person's message, or a "wake" (a routine or other trigger firing) | main-app.cjs @3100283 `w3n`/`P3n`: turn counter `n+=1` only for kind `human` or `wake` (`SimulatedMsgReason.SUBSCRIPTION` → wake) |
|
||||||
|
| A message sent while it works | steers the running turn and is **not** a new turn | same, `P3n`: `e.turnSteer===!0 ? null` (steer messages don't count) |
|
||||||
|
| Turn outcomes | success, cancelled, error | node-agent-coordinator `wl=["success","cancelled","error"]`, read back by `k6` as {turnId, outcome, settledAtMs} |
|
||||||
|
| Bot is idle only when | not running, no helper tasks running, not waiting for the person, no computer hand-off pending | node-agent-coordinator `ZF`: `!isRunning && hasRunningSubagents!==true && awaiting===undefined && boxHandoff===undefined` |
|
||||||
|
|
||||||
|
Rebuild (app agent.rs, app.rs): a message typed during a turn goes into the turn's steer queue and
|
||||||
|
reaches the model at its next step. If the model has just finished, the turn keeps going. Anything that
|
||||||
|
arrives after the turn has settled starts the next turn. Each turn records {turn id, outcome,
|
||||||
|
settled time}: stop is cancelled, a failure or stall is error, otherwise success.
|
||||||
|
|
||||||
|
### Computers
|
||||||
|
| Rule | Value | Source |
|
||||||
|
|---|---|---|
|
||||||
|
| Default place to work | the bot's own computer ("Bot 的電腦", yGkA3X) | settings map `b$.computers` / `y$.botComputer` |
|
||||||
|
| This Mac | only after the person turns on "允許 Grok Bot 在這台電腦上執行任務,而不只是在它自己的電腦上" | setting `local-execution`, ids fuUvu2 / Hd1KB+ |
|
||||||
|
| Approvals name the machine | "允許 Grok Bot 在 {targetMachineDisplay} 上使用 {app} 嗎?", "請在 {targetMachineDisplay} 上回應" | ids eQViQE / ykDLyL |
|
||||||
|
| Taking over the bot's computer | "接管" (take over), then hand back | id yqwjpd; renderer `handBack`/`handBackForeverBox` @2601598 (not rebuilt; see below) |
|
||||||
|
|
||||||
|
Rebuild: "Bot 的電腦" is a sealed folder per bot (`~/AnyBot 工作區/bot-N`). Its commands run under
|
||||||
|
the macOS sandbox with nothing else in the home folder readable or writable, like a separate machine, so
|
||||||
|
it works there without asking. "你的電腦" is this Mac with the layered permission rules, and only after the
|
||||||
|
setting is on. Other computers are added in Settings → 電腦 and reached over SSH (read-only commands run
|
||||||
|
directly, others ask). Every tool has a `computer` choice, so "在我的 Mac 上…" picks this Mac.
|
||||||
|
|
||||||
|
### Permission hand-over
|
||||||
|
| Rule | Value | Source |
|
||||||
|
|---|---|---|
|
||||||
|
| Rule choices | 先詢問 / 每次詢問 / 一律允許 / 永不允許 | ids 4YjKLA, 6CTZeX, NoKBgy, wvd4WD |
|
||||||
|
| Auto-review | "依照自訂規則審核 Bot 的操作", two lists: 「無需詢問直接允許」(continue automatically) and 「一律詢問」(stop and ask) | ids 9yhl+F, geCUKn/JOecNC, 1YRRmk/Pca/iV |
|
||||||
|
| Conflicts | "規則衝突時,「先詢問」優先"; built-in safety checks always apply | ids NFRGrQ, n9YiIv |
|
||||||
|
| Standing grants | "always allow" remembered (machine-scoped in the original) | fnd_12 `recordLocalToolStandingGrant` |
|
||||||
|
|
||||||
|
Rebuild: built-in refusals run first. For an action that would ask, auto-review (when on) asks the
|
||||||
|
model to match it against the two rule lists, and an "always ask" match wins. "Allow" continues without
|
||||||
|
a card, and the result notes which rule matched. Otherwise the approval card names the computer.
|
||||||
|
|
||||||
|
### Asking for permission when it's needed (pop-up)
|
||||||
|
The original asks on the spot and names the machine ("允許 Grok Bot 在 {targetMachineDisplay} 上使用
|
||||||
|
{app} 嗎?", eQViQE; "請在 {targetMachineDisplay} 上回應", ykDLyL). The rebuild never lets a step fail
|
||||||
|
quietly for lack of permission:
|
||||||
|
- Computer tools are always offered to the model. If they're off for this bot, the step asks
|
||||||
|
"允許 {bot} 使用電腦工具嗎?": 允許這一次 / 一律允許這個 Bot 使用電腦 (turns the bot's tools on) / 拒絕.
|
||||||
|
- If the step targets 你的電腦 while 在這台電腦上執行 is off, it asks "允許 {bot} 在你的電腦上執行任務嗎?"
|
||||||
|
(wording from Hd1KB+): 允許這一次 / 一律允許 (turns the setting on) / 拒絕.
|
||||||
|
- Every request, including single actions, shows as a centred pop-up over a dimmed window, even when
|
||||||
|
another bot's chat is open. The Dock icon bounces (`RequestUserAttention`) when AnyBot isn't in front.
|
||||||
|
"稍後再決定" hides the pop-up; the chat card, the header pill "● 需要你允許" (click to reopen) and the
|
||||||
|
sidebar's highlighted "需要你允許" stay until answered.
|
||||||
|
- The bot is told to just call the tool and let the app ask, rather than telling the person to change
|
||||||
|
settings. Tested by `missing_permission_pops_up_and_continues`.
|
||||||
|
|
||||||
|
### Notifications when you're away (decompiled)
|
||||||
|
**`sand_notification_settings.node`** (Mach-O arm64, 102 368 bytes, sha256 6b2b9fa3…). Strings in evidence
|
||||||
|
`ev_a3c3cb3d…`, functions read with native_function_pseudocode:
|
||||||
|
|
||||||
|
| Address | Function | Rule taken |
|
||||||
|
|---|---|---|
|
||||||
|
| `0x35b4` | `getNotificationSettings(NotificationSettingsInfo&)` | Only on macOS ≥ 10.14 (`__isPlatformVersionAtLeast(1,10,14,0)`) and only inside a real `.app` bundle (bundlePath must end in ".app"). Asks `UNUserNotificationCenter` and waits on a semaphore for at most **3 s** (`dispatch_time(0, 0xb2d05e00)` ns). `authorizationStatus` 0..4 → notDetermined/denied/authorized/provisional/ephemeral, otherwise "unknown"; `alertSetting` 0..2 → notSupported/disabled/enabled, otherwise "unknown" (evidence `ev_7feb06d3…`) |
|
||||||
|
| `0x3898` | its completion block | Stores authorizationStatus and alertSetting, then signals the semaphore (evidence `ev_32393aac…`) |
|
||||||
|
| `0x2de8` | `NotificationSettingsWorker::OnOK` | Resolves `{platform:"darwin", setting, alertSetting}` (evidence `ev_ec969729…`) |
|
||||||
|
|
||||||
|
From main-app.cjs:
|
||||||
|
| Rule | Value | Source |
|
||||||
|
|---|---|---|
|
||||||
|
| Permission mapping | authorized/provisional/ephemeral → granted, denied → denied, notDetermined → undetermined | @3183391 `hBt`, `mur` |
|
||||||
|
| Banners allowed | only when authorized **and** alerts enabled | `fBt` |
|
||||||
|
| You count as present | signed in, screen not locked, not idle, window alive | `K4` |
|
||||||
|
| Idle after | **90 s** without input | `Z_e=9e4` (10 min `cBt=10*6e4` while a phone push is being held) |
|
||||||
|
| Presence re-checked every | **10 s** | `uBt=1e4` |
|
||||||
|
| Setting text | "Bot 完成任務或等你處理時通知你" | id ttdOeI |
|
||||||
|
|
||||||
|
Rebuild (notify.rs): presence = window in front **and** input within 90 s (idle read from macOS
|
||||||
|
`HIDIdleTime` every 10 s). When you're not present and a bot needs permission, or finishes a turn, a
|
||||||
|
macOS banner appears with a sound, on top of the pop-up and the Dock bounce (Settings → 一般 → 通知,
|
||||||
|
on by default). A build-folder app isn't a `.app` bundle, so, as the original's module shows, it can't
|
||||||
|
read its own notification permission. Banners go through macOS's `display notification` instead, which
|
||||||
|
may need allowing once under System Settings → Notifications. The 3 s settings read and the permission
|
||||||
|
mapping aren't used for that reason.
|
||||||
|
|
||||||
|
### 直接操作 (work like Grok Bot without a stream of questions)
|
||||||
|
Why the original rarely asks: it does most work on **its own cloud computer and browser** ("Bot 的電腦"),
|
||||||
|
so nothing on the Mac needs permission. Working on the Mac is one switch (`local-execution`,
|
||||||
|
Hd1KB+), after which **auto-review** checks actions instead of the person ("讓 Grok Bot 在你的電腦上開啟
|
||||||
|
檔案和執行任務。自動審核仍會先檢查所有操作", ENHi8c; hrkSSQ). Per-app grants are remembered
|
||||||
|
(`recordLocalToolStandingGrant`, `listComputerUseAllowedApps`, fnd_12).
|
||||||
|
|
||||||
|
Rebuild: **Settings → 電腦 → ⚡ 直接操作**, or the "⚡ 全部允許,之後直接操作" link on any request.
|
||||||
|
- One switch turns on 在這台電腦上執行, every bot's computer tools and auto-review. Running turns stop
|
||||||
|
asking at once, and other bots' waiting "may I use the computer" requests are answered yes.
|
||||||
|
- Actions that would ask go ahead with the note "直接操作模式:已自動允許". The only exceptions are
|
||||||
|
actions that match an "always ask" rule (checked by the model, as in auto-review). The default rules
|
||||||
|
are own wording: 刪除檔案或資料夾 / 付款、購買或轉帳 / 用我的帳號發文、留言,或傳訊息給別人.
|
||||||
|
- Still blocked: the built-in refusals (sudo, mkfs, rm -rf /…), secret files and AnyBot's own data
|
||||||
|
(sandbox), as in the original ("內建的安全檢查一律有效", n9YiIv).
|
||||||
|
- macOS's own one-time prompt to let AnyBot control a browser can't be skipped by any app.
|
||||||
|
- Tested by `direct_mode_asks_nothing_but_still_blocks_danger`.
|
||||||
|
|
||||||
|
**Admin locks (decompiled).** `sand_notification_settings.node` `managedPolicyValueSync` at `0x1870`
|
||||||
|
(evidence `ev_86419910…`) takes `(productName, valueName)`, both of which must be strings (napi type 4),
|
||||||
|
otherwise it throws "managed_policy_value_sync(productName, valueName)". On macOS it then returns null:
|
||||||
|
it reads nothing on a Mac. The app side (main-core.cjs @727949, `EZ`/`TZ`/`Ak`/`mZ`/`gZ`) reads policy
|
||||||
|
from **macOS user defaults** (`systemPreferences.getUserDefault(key, "string")`, which MDM profiles can
|
||||||
|
force), from `~/.cursor/policy.json` on Linux, and from the native call on Windows. Missing or blank
|
||||||
|
values mean **unmanaged**, a valid value means **pinned**, and anything else is **invalid** (its URLs must be
|
||||||
|
plain https origins).
|
||||||
|
|
||||||
|
Rebuild (policy.rs): the same three kinds for two switches, read from macOS defaults domain
|
||||||
|
`com.anybot.AnyBot` (`AllowDirectMode`, `AllowLocalExecution`; Linux: `~/.anybot/policy.json`):
|
||||||
|
- unmanaged lets the person decide; pinned true/false fixes it; invalid locks it **off** (fail safe).
|
||||||
|
- A locked switch is greyed out with "🔒 由你的管理員設定,無法變更". The ⚡ link is hidden.
|
||||||
|
- A locked "你的電腦" is refused at once instead of asking. Locks are re-read each time Settings opens.
|
||||||
|
- Example: `defaults write com.anybot.AnyBot AllowDirectMode false`.
|
||||||
|
|
||||||
|
### Websites and the person's browser
|
||||||
|
The original reaches sites through its own browser (its old Chrome login copy is retired, see below).
|
||||||
|
The rebuild (web.rs) gives every bot:
|
||||||
|
- `web_fetch`: public pages as text (20 s timeout, 3 MB read, 20 000 characters to the model: **estimated**).
|
||||||
|
No permission needed.
|
||||||
|
- `browser_open`: opens a link in the person's own browser on this Mac (Chrome, Arc, Edge, Brave,
|
||||||
|
Chromium or Safari, whichever is open), where they're already logged in, and waits 4 s (**estimated**).
|
||||||
|
- `browser_read`: title, address and visible text of the current tab, via AppleScript. It asks first
|
||||||
|
(grant 「讀取瀏覽器分頁」).
|
||||||
|
- Both browser tools count as working on 你的電腦, so the computer pop-ups apply when those are off.
|
||||||
|
macOS asks itself the first time AnyBot controls a browser. If that's refused (-1743), the app opens
|
||||||
|
System Settings → 隱私權與安全性 → 自動化 and says what to switch on. If page text is blocked, it says
|
||||||
|
which menu item to turn on (Chrome: 檢視 → 開發人員 → 允許來自 Apple 事件的 JavaScript).
|
||||||
|
- **Which browser** (decompiled). `sand_window_capture.node` (Mach-O arm64, 128 064 bytes, sha256
|
||||||
|
f2b48533…; strings in evidence `ev_6c197ec3…`):
|
||||||
|
|
||||||
|
| Address | Function | Rule taken |
|
||||||
|
|---|---|---|
|
||||||
|
| `0x52c0` | `listWindows()` | `CGWindowListCopyWindowInfo(0x11, 0)`: on-screen windows only (`0x1`) without desktop elements (`0x10`), front to back. A window is kept only if its alpha > 0 and its bounds have width > 0 and height > 0. Each entry records window id, owner pid, layer, bounds and owner name (evidence `ev_fbc46bcb…`) |
|
||||||
|
| `0x5258` | `screenCaptureAccess()` | macOS ≥ 14 (`__isPlatformVersionAtLeast(1,14,0,0)`), else 2 = unsupported; otherwise `CGPreflightScreenCaptureAccess() ^ 1` (0 = granted, 1 = not granted) (evidence `ev_e0e9c7c2…`) |
|
||||||
|
| `0x651c` | `statusForError(NSError*)` | error code −3801 (the user declined capture) → status 2 "denied"; any other → 5 "failed" (evidence `ev_7c417898…`) |
|
||||||
|
|
||||||
|
Rebuild: `browser_read`/`browser_open` read the same front-to-back window list (through macOS's own
|
||||||
|
JavaScript bridge) with the same filter, plus layer 0 for ordinary app windows (ours), and use the
|
||||||
|
browser whose window is **closest to the front**, the tab the person is actually looking at. If no
|
||||||
|
browser window is visible they fall back to the first open browser, then Chrome, then Safari.
|
||||||
|
Screen capture itself isn't rebuilt: reading the page text needs no screenshot.
|
||||||
|
- The bot is told not to say it has no tools, and to try these first. Checked against xAI grok-4.7:
|
||||||
|
"幫我看一下我的threads 看一下我的帳號" now gives `browser_open(threads.net)` + `browser_read`.
|
||||||
|
|
||||||
|
### AnyBot's own browser (replaces driving the person's browser)
|
||||||
|
Grok Bot never asks the person to change browser settings because it browses in **its own browser**
|
||||||
|
(its Chromium-based browser and webviews on the bot's computer). Driving the person's Chrome through
|
||||||
|
AppleScript, the first approach here, needed Chrome's "允許來自 Apple 事件的 JavaScript" plus a macOS
|
||||||
|
Automation prompt.
|
||||||
|
|
||||||
|
**Copying logins from Chrome: retired in the original, not rebuilt.**
|
||||||
|
- Native main program (`Grok Bot` Mach-O, opened with core_target_open): searching its strings for
|
||||||
|
browser / chrome / cdp / devtools / remote / port / debug finds **0** strings (evidence
|
||||||
|
`ev_3dc3f004…`), so no browser or cookie logic lives in native code; it's all in the JavaScript.
|
||||||
|
- The renderer still has the old approval card (chunk-view @3041: 「{botName} 想要使用你現有的登入」,
|
||||||
|
「從你電腦上的 Chrome 複製 Cookie…」, 檢視網站 / 允許一次 / 一律允許 / 拒絕, sites grouped by Chrome
|
||||||
|
profile), but the error catalogue in main-core.cjs and index.eager-app (domain `chrome-import`,
|
||||||
|
codes **SAND-E0801 … SAND-E0812**: cookieApprovalNoMachine … chromeCookieImportFailed) marks every
|
||||||
|
one "retired: the agent-prompted cookie sync that raised it was removed" / "retired: the Chrome
|
||||||
|
cookie import that raised it was removed", and the inject was behind a gate
|
||||||
|
(`sand_import_chrome_cookies`, SAND-E0811).
|
||||||
|
- So the shipping original no longer copies Chrome cookies. The rebuild matches that: nothing is
|
||||||
|
copied, and the bot is told never to offer it. (Also: this Mac's Chrome/Brave data folders refuse
|
||||||
|
reads without Full Disk Access, "Operation not permitted", so it couldn't work quietly anyway.)
|
||||||
|
The person logs in once in AnyBot's own browser window instead.
|
||||||
|
|
||||||
|
Rebuild (botbrowser.rs): AnyBot starts a separate Chrome-family profile (Chrome, Brave, Edge or
|
||||||
|
Chromium, whichever is installed) as **its own browser window**, with logins kept in
|
||||||
|
`…/AnyBot/browser`. It talks to it over Chrome's DevTools protocol on local port **9333**
|
||||||
|
(**estimated**, overridable for tests):
|
||||||
|
- browser_open: starts the window (waits up to 20 s, **estimated**) or adds a tab (`PUT /json/new?url`).
|
||||||
|
- browser_read: picks the first real page tab (`/json/list`) and reads
|
||||||
|
`{title, location.href, document.body.innerText}` with `Runtime.evaluate` (20 s limit, **estimated**).
|
||||||
|
- No browser setting, no Automation prompt, no "your computer" question: it's the bot's browser.
|
||||||
|
The person logs in to a site in that window once; a short page mentioning 登入 / log in gets a note
|
||||||
|
asking them to.
|
||||||
|
- The bot is told never to send the person to browser menus. The AppleScript path remains only as a
|
||||||
|
fallback when no Chrome-family browser is installed (Safari only).
|
||||||
|
- Checked live against a hidden Chrome: example.com was read as "標題:Example Domain".
|
||||||
|
|
||||||
|
### Browser profiles (設定檔)
|
||||||
|
Grok Bot lets the person use any Chrome profile because on "your computer" it drives the real screen
|
||||||
|
through its Computer Use helper app (local-exec-daemon: `Grok Bot Computer Use`,
|
||||||
|
`co.anysphere.grok-bot-computer-use`, `CUGrokBotService`): it clicks in their normal Chrome like a
|
||||||
|
person would. The original has no browser-profile code of its own (no `--profile-directory`,
|
||||||
|
`Local State` or `info_cache` anywhere; `profileDisplayName` only on the retired login-copy card).
|
||||||
|
AnyBot doesn't control the screen, and its own browser couldn't see the person's Chrome profiles
|
||||||
|
(macOS refuses other apps `…/Google/Chrome/Local State`: "Operation not permitted"). So
|
||||||
|
(rebuild, **estimated** rules):
|
||||||
|
- AnyBot's browser has its own profiles: 「預設」 (the existing `…/AnyBot/browser` folder, logins kept)
|
||||||
|
plus any added ones in `…/AnyBot/browser-profiles/<name>`. Each is its own window with its own logins.
|
||||||
|
- Names: 1–30 characters, no `/` or `:`, not starting with `.`; matched ignoring case.
|
||||||
|
- 「預設」 keeps port 9333; added profiles start with port **0**, so Chrome picks a free port and writes
|
||||||
|
it to `DevToolsActivePort` in the profile folder, which AnyBot reads back (checked live: Chrome 156
|
||||||
|
wrote 62337 and answered on it).
|
||||||
|
- The profile in use is remembered in `…/AnyBot/browser-profile.txt`.
|
||||||
|
- Bot tools: `browser_profiles` (list / add / use, no permission needed: it's the bot's browser), and
|
||||||
|
an optional `profile` on browser_open / browser_read (opening in a profile makes it the one in use).
|
||||||
|
The bot is told to use these when the person says profile / 設定檔 / another account.
|
||||||
|
- Settings → 電腦 → 「AnyBot 瀏覽器設定檔」: pick the one in use, 「開啟視窗」 to log in, 「新增設定檔」.
|
||||||
|
Removing a profile isn't offered (it would delete its logins).
|
||||||
|
|
||||||
|
### 「你的瀏覽器」: reading the person's own Chrome, any profile (decompiled)
|
||||||
|
How Grok Bot reads the person's real Chrome in whatever profile is open: its Computer Use helper
|
||||||
|
(`Helpers/Grok Bot Computer Use.app/Contents/MacOS/CUGrokBotService`, Mach-O fat, sha256
|
||||||
|
1004a8cb…) turns on Chrome's accessibility tree and reads the screen's elements. Strings:
|
||||||
|
`com.google.Chrome`, `com.google.Chrome.`, `Google Chrome Framework.framework` (evidence
|
||||||
|
`ev_75dff652…`), `AXManualAccessibility` / `AXEnhancedUserInterface` (`ev_bef6c29e…`), the 3
|
||||||
|
framework names (`ev_a8cda95a…`). Functions read:
|
||||||
|
|
||||||
|
| Address | Function | Rule taken |
|
||||||
|
|---|---|---|
|
||||||
|
| `0x1000a1b6c` | `CURendererApp.isRenderer(bundleIdentifier:executablePath:)` | renderer if bundle id == `com.github.Electron` or == `com.google.Chrome`, or has prefix `com.google.Chrome.`, or the executable path contains `/Electron.app/Contents/MacOS/Electron`, else `isRendererBundle` (evidence `ev_4973f594…`) |
|
||||||
|
| `0x1000a19ec` | `CURendererApp.isRendererBundle(atPath:)` | `<bundle>/Contents/Frameworks/<name>` exists for one of 3 names: `Electron Framework.framework`, `Chromium Embedded Framework.framework`, `Google Chrome Framework.framework` (evidence `ev_f0dc9836…`) |
|
||||||
|
| `0x1000a1cdc` | `CURendererApp.isRenderer(NSRunningApplication)` | takes the running app's bundle id, bundle URL path and executable URL path, then calls the rule above (evidence `ev_179b4f10…`) |
|
||||||
|
| `0x1000a1554` | `CURendererAccessibility.writeFlags(on:)` | `AXUIElementSetAttributeValue(app, "AXManualAccessibility", on)` then `(app, "AXEnhancedUserInterface", on)`; result from both error codes (evidence `ev_f661d29e…`) |
|
||||||
|
|
||||||
|
Rebuild (axread.rs), used by the profile choice 「你的瀏覽器」 (aliases 我的 Chrome, 你的 Chrome…):
|
||||||
|
- Same renderer rule (`is_renderer`, constants copied as written) and the same two flags, in order, on
|
||||||
|
the person's browser before reading. Safari etc. need no flags.
|
||||||
|
- macOS asks once for 輔助使用 (`AXIsProcessTrustedWithOptions` with the prompt). If it's off, AnyBot
|
||||||
|
opens 系統設定 → 隱私權與安全性 → 輔助使用 and says to switch on AnyBot. No browser setting is needed.
|
||||||
|
- Reads the focused window → first `AXWebArea` → text of `AXStaticText` (`AXValue`), one line per
|
||||||
|
group; title `AXTitle`, address `AXURL`. Up to 20 000 elements, depth 80, 5 tries 0.7 s apart
|
||||||
|
while Chrome builds the tree, 2 s per answer (all **estimated**).
|
||||||
|
- The person's browser process is told apart from AnyBot's own window by its arguments (AnyBot's
|
||||||
|
runs with `--user-data-dir=…/AnyBot/browser`; helper processes carry `--type=`).
|
||||||
|
- browser_open on 「你的瀏覽器」 starts the browser's own executable with the link, so Chrome hands it
|
||||||
|
to the person's running window (their normal profile) instead of AnyBot's window.
|
||||||
|
- It counts as using 你的電腦: the computer pop-ups apply and reading asks first (「讀取你瀏覽器目前
|
||||||
|
分頁」), like before. Switching Chrome profiles is done by the person in Chrome; the bot reads the
|
||||||
|
window in front.
|
||||||
|
- Not checked live yet: this test process isn't allowed 輔助使用 (trusted=false).
|
||||||
|
|
||||||
|
### Talking to the bot runs the app
|
||||||
|
Routines are made in chat ("例行任務是這個 Bot 依排程重複執行的任務。請在聊天中請它設定一個。", ZJmAzX),
|
||||||
|
and the primary bot can coordinate other bots (mQQ/Uq). App tools in the rebuild (appctl.rs):
|
||||||
|
list_routines, create_routine, update_routine, delete_routine (asks first, wording VAgxC4),
|
||||||
|
run_routine_now, list_bots, create_bot, update_bot (provider, model, name, pin, default computer),
|
||||||
|
message_bot (wakes another bot), list_computers, update_settings (theme, accent, time zone). Turning
|
||||||
|
on "在這台電腦上執行" stays person-only.
|
||||||
|
|
||||||
|
### Routine schedules
|
||||||
|
Shape from index.eager-app @1508626 (`Prt`, `Irt`): `{frequency: daily|weekly|monthly|yearly, interval,
|
||||||
|
daysOfWeek: MO..SU (yA), monthDay | weekdayOfMonth {week, day}, month, time "HH:MM", timezone}`.
|
||||||
|
Descriptions use the original's zh-TW strings: 每天 / 每{n} 天 / 每個工作日 / 每週的{days} / 每月{d}日 /
|
||||||
|
每月的{week}{day} / 每年{date}, then "{repeats} {time}" and "({zone})" when not the person's time zone.
|
||||||
|
"Every weekday" means exactly MO–FR (`isEveryWeekday`, `WORKWEEK`). Next-run rules (ours): intervals
|
||||||
|
count from the creation date; a month day past the month's end runs on the last day; summer-time gaps
|
||||||
|
take the earliest valid time. A routine that comes due while the bot is busy waits until it is idle.
|
||||||
|
|
||||||
|
### Screen control on 你的電腦 (computer use)
|
||||||
|
The original's local daemon (local-exec-daemon main.cjs) drives its Computer Use helper with these
|
||||||
|
tools; the rebuild (computer.rs) uses the same names and arguments:
|
||||||
|
`computer_use_screenshot` {}, `computer_use_click` {method: coordinate, x, y, button, count,
|
||||||
|
modifier_keys}, `computer_use_scroll` {x, y, direction, amount, modifier_keys},
|
||||||
|
`computer_use_mouse_move` {x, y}, `computer_use_drag` {button, path, modifier_keys},
|
||||||
|
`computer_use_typing` {value}, `computer_use_press_key` {key, hold_duration_ms} (main.cjs @2860904).
|
||||||
|
- Screenshot size, `zct` (main.cjs): long edge fitted to **1024** (`Gct`); if the short edge is under
|
||||||
|
**8** (`Hs`) nothing is resized; after scaling, an edge under 8 is raised so the short edge is 8.
|
||||||
|
Largest image **1 048 576** bytes (`mg = 1024*1024`). JPEG is tried at quality **85, 70, 55** (`$3r`)
|
||||||
|
and the first that fits is used, else the smallest one if it's under the cap (`j3r`).
|
||||||
|
- Rebuild: `screencapture -x -m` of the main screen, fitted and encoded by those rules, sent to the
|
||||||
|
model as a picture (Claude: inside the tool result; OpenAI-style: one user message with the
|
||||||
|
pictures right after the run of tool results). Only the newest **3** screenshots are sent; older
|
||||||
|
ones say 「較早的截圖已省略」 (**estimated**). Screenshots aren't saved with the chat.
|
||||||
|
- Coordinates are pixels of the latest screenshot; AnyBot turns them into screen points with the
|
||||||
|
screen size (`CGDisplayBounds`) ÷ picture size. Shortcuts like `cmd+shift+t` use the US key codes.
|
||||||
|
- Mouse and keyboard timing, decompiled from `CUGrokBotService` (evidence ids after each):
|
||||||
|
|
||||||
|
| Address | Function | Rule taken |
|
||||||
|
|---|---|---|
|
||||||
|
| `0x100010120` | `CUActuator.click(pid:screenPoint:windowBounds:…button:count:holdSeconds:)` | event source `CGEventSourceCreate(1)` (HID system state) with `CGEventSourceSetLocalEventsSuppressionInterval(0)`; press → hold → release where hold = `holdSeconds > 0 ? holdSeconds : 0.03`; `count` clicks with **0.08 s** between them; error "Could not create click event"; background windows get a window-local click (fields 91/92 = window id, "skylight-no-raise") (`ev_f3077201…`) |
|
||||||
|
| `0x100010a24` | `CUActuator.typeCharacters(pid:text:)` | empty text → "`value` is required and must be a non-empty string"; same event source, then `postChunks` (`ev_77b2182b…`) |
|
||||||
|
| `0x1000beb00` | `CUTypedCharacters.utf16Chunks` | chunks of at most **20** UTF-16 units (buffer 0x14; a new chunk when the next character would reach 21), never splitting a character (`ev_0dce1c89…`) |
|
||||||
|
| `0x1000beebc` | `CUTypedCharacters.postChunks` | each chunk: `CGEventCreateKeyboardEvent(src, 0x80 = 128, down/up)` + `CGEventKeyboardSetUnicodeString`, flags 0; down → **0.035 s** → up; **0.02 s** between chunks; error "Could not create character keyboard event" (`ev_714c5253…`) |
|
||||||
|
| `0x100031764` | `CUCompanionToolService.handlePressKey` | presses a list of keys in order: each down → **0.035 s** → up, **0.02 s** between keys; result "Pressed {keys joined by spaces}."; error "Could not create keyboard event" (`ev_730d67a4…`) |
|
||||||
|
|
||||||
|
| `0x10008c34c` | `CURemoteEventPoster.drag(path:button:steps:holdSeconds:flags:)` | fewer than 2 points → "Drag path requires at least two points."; mouse down at the first point (click state 1), then if hold > 0 sleep it in 10 000 µs slices, then for every segment post `steps` dragged events at `start + delta × i / steps` (i = 1…steps, steps at least 1), then mouse up at the last point (`ev_f0d60f2e…`) |
|
||||||
|
| `0x10009dee4` | `CURemoteToolService.handleDrag` (closure) | calls drag with **steps 6** and **holdSeconds 0.04**; result "Dragged from (%.0f, %.0f) to (%.0f, %.0f) with left/right/middle button." (`ev_65c07c1e…`) |
|
||||||
|
| strings | `handleScroll` / `handleDrag` | "`direction` must be `up`, `down`, `left`, or `right`.", "`amount` must be an integer.", "`path` must be an array of at least two {x,y} points in model coordinates.", "Could not create scroll event" (`ev_0b3df00c…`) |
|
||||||
|
|
||||||
|
Drag in the rebuild: same press, 0.04 s hold, 6 interpolated steps per segment, release at the
|
||||||
|
end, and the reply 「已從 (x, y) 拖曳到 (x, y)(左鍵)」. Scroll refuses a non-integer amount.
|
||||||
|
|
||||||
|
| Address | Function | Rule taken |
|
||||||
|
|---|---|---|
|
||||||
|
| `0x10009e200` | `CURemoteToolService.handleScroll` | `direction` is lowercased and must be up/down/left/right; `amount` must be an integer, default **3**, kept to **1…20** (`max(amount,1)`, `min(…,20)`); distance = `(amount + amount) << 3` = **amount × 16** pixels; down and right are negative; `x` and `y` "must be provided together", and without them the current pointer position is used ("Could not read the current cursor position."); needs Accessibility and Screen Recording (`ev_98625ed7…`) |
|
||||||
|
| `0x10009e998` | handleScroll closure | posts a mouse-moved event (type 5) at the spot, then `CGEventCreateScrollWheelEvent2(src, units 0 = pixel, wheels 2, vertical, horizontal, 0)` at the same spot with the modifier flags; result "Scrolled {direction} x{amount}." (`ev_ee9d8372…`) |
|
||||||
|
|
||||||
|
Rebuild scroll: the same default, range, 16-pixel steps in pixel units, the both-or-neither x/y
|
||||||
|
rule (no screenshot needed when scrolling where the pointer is), and 「已捲動 down ×3。」.
|
||||||
|
Rebuild: the same event source, the same holds and gaps, the same chunking on key 128, and a key
|
||||||
|
press may be several keys separated by spaces (`cmd+a delete`). `hold_duration_ms` replaces the
|
||||||
|
0.035 s hold (at most 5 s, **estimated**). Window-local background clicks (clicking without
|
||||||
|
bringing the window forward) aren't rebuilt: AnyBot clicks where the pointer is, like a person.
|
||||||
|
Pauses between mouse-move and drag steps are 8 ms (**estimated**).
|
||||||
|
- Permissions: macOS asks once for 螢幕與系統錄音 (screenshots, `CGPreflightScreenCaptureAccess` like
|
||||||
|
`screenCaptureAccess()` @0x5258) and once for 輔助使用 (mouse and keyboard); if off, AnyBot opens the
|
||||||
|
right System Settings pane and says what to switch on. In AnyBot, looking at the screen asks once
|
||||||
|
(grant 「看你的螢幕畫面」) and using the mouse/keyboard asks once (「操作你的滑鼠和鍵盤」), both with
|
||||||
|
"always allow"; 直接操作 skips the questions except for "一律詢問" rules. It always counts as 你的電腦,
|
||||||
|
so the computer pop-ups and admin locks apply.
|
||||||
|
- Needs a model that can see pictures.
|
||||||
|
- Not checked live: this test process has neither permission.
|
||||||
|
|
||||||
|
### Still different from the original
|
||||||
|
- "接管" (take over the bot's computer) is not rebuilt: the bot's computer here is a sealed folder,
|
||||||
|
not a cloud machine with a screen. Screen control works on 你的電腦 only.
|
||||||
|
- Routine triggers from events (GitHub, PagerDuty, Sentry: `GITHUB_EVENT_KINDS` etc.) are not rebuilt;
|
||||||
|
only time schedules.
|
||||||
|
- The look still uses the estimated layout: the signed-in original's window hasn't been captured.
|
||||||
|
|
||||||
|
## Restarting and recovering
|
||||||
|
|
||||||
|
### From the native main program (decompiled again for this feature)
|
||||||
|
`Contents/MacOS/Grok Bot`. native_strings_find for `relaunch|restart|ELECTRON|dev/null|crash` finds two
|
||||||
|
strings (evidence `ev_6af348eb…`), and both lead to these functions:
|
||||||
|
|
||||||
|
| Address | Function | Rule taken |
|
||||||
|
|---|---|---|
|
||||||
|
| `0x1000004c0` | `sub_1000004c0` | Before anything else, for fd 0/1/2: if `fstat` fails with `errno == 9` (EBADF), `freopen("/dev/null", …)`, so a copy started without terminal streams still has valid ones (evidence `ev_29ffc0d8…`) |
|
||||||
|
| `0x100000460` | `entry` | Calls `0x1000004c0`, then a set `ELECTRON_RUN_AS_NODE` (fuse on, non-empty) changes how the program starts (`ElectronInitializeICUandStartNode` instead of `ElectronMain`) (evidence `ev_7c151be5…`) |
|
||||||
|
|
||||||
|
### From the app code
|
||||||
|
| Rule | Value | Source |
|
||||||
|
|---|---|---|
|
||||||
|
| Relaunch helper | detached shell, all output ignored: `spawn(shell, ["-c", script, "grok-bot-relaunch", pid, app, …args], {detached:true, stdio:"ignore"})` | main-app.cjs @3195964 `Pur` |
|
||||||
|
| Wait before starting | `while kill -0 "$pid" 2>/dev/null; do sleep 0.2; done; exec "$app" "$@"`: the new copy starts only after the old one exited, polled every **0.2 s** | same, `PBt` |
|
||||||
|
| Inherited files | closes fds > 2 first (`rEe`, Linux /proc only) | same |
|
||||||
|
| Button while it happens | "正在重新啟動…" (Restarting…) | renderer `ljn` → id fvg2KT |
|
||||||
|
| Bot's computer stuck | "Grok Bot 的電腦卡住時,從你上次儲存的快照重新啟動它"; recovering keeps files and logins, removes installed apps and packages | ids O+wR5I, ixKoax |
|
||||||
|
|
||||||
|
### How the rebuild applies them
|
||||||
|
- **重新啟動 AnyBot** (Settings → 一般): stops running turns as cancelled and background commands, saves,
|
||||||
|
then starts a detached `/bin/sh` helper. The helper waits for this copy's pid to disappear, checking
|
||||||
|
every 0.2 s, and then starts the app, so the two copies never overlap and the last save can't be
|
||||||
|
overwritten. Its input and output go to `/dev/null`. Start-up switches (`ANYBOT_SHOT_VIEW`, the
|
||||||
|
per-command tags, `ELECTRON_RUN_AS_NODE`) are removed so the new copy opens normally. The button reads
|
||||||
|
"正在重新啟動…" while it happens. Tested by `relaunch_waits_for_the_old_copy`.
|
||||||
|
- **重新啟動這個 Bot** (⋯ menu): ends the bot's turn as cancelled, stops its background commands and
|
||||||
|
everything they started (by its conversation tag), and marks half-finished replies as interrupted.
|
||||||
|
Chats and the files on its computer are kept. The snapshot "recover" step isn't rebuilt: the bot's
|
||||||
|
computer here is a folder, not a machine image.
|
||||||
|
- **Interrupted turns on load**: a reply that was still streaming when the app closed is saved empty and
|
||||||
|
can't continue. On load it becomes "(這次的回應被中斷了…)" with an error settlement, so "重新產生"
|
||||||
|
appears. A message with no reply shows "重新傳送" (MyjAbr).
|
||||||
|
|
||||||
|
## Versions and updating
|
||||||
|
|
||||||
|
**Native main program:** native_strings_find for `version|update|0.78|x.y.z` in `Contents/MacOS/Grok Bot`
|
||||||
|
finds **0 strings** (evidence `ev_df0e0f59…`). The version (0.78.1) lives only in Info.plist
|
||||||
|
(`CFBundleShortVersionString`), and all update logic is in main-app.cjs.
|
||||||
|
|
||||||
|
| Rule | Value | Source |
|
||||||
|
|---|---|---|
|
||||||
|
| First check after launch | 30 s | main-app.cjs @3372407 `bhr=3e4` (`initialCheckDelayMs`) |
|
||||||
|
| Then every | 1 h + random 0–5 min | `khr=3600*1e3`, `Ihr=300*1e3`, `nextPeriodicDelayMs = interval + floor(random·jitter)` |
|
||||||
|
| Apply by itself only when | auto-update on **and** update ready **and** (screen locked **or** screensaver running) **and** idle ≥ **600 s** **and** no agent work | @3365216 `uQt`, `phr`, `pQt=600`, `uhr` (`confirmed-idle`) |
|
||||||
|
| Auto-update default | on | `getAutoUpdateWhenIdle?.() ?? true` |
|
||||||
|
| Screen locked | `powerMonitor.getSystemIdleState(1) === "locked"` | `Aj`/`Cle` |
|
||||||
|
| Screensaver running | `pgrep -x ScreenSaverEngine` (2 s timeout) | `NRn`, `DRn` |
|
||||||
|
| Labels | "重新啟動以更新" (FzpXvM), "正在重新啟動…" (fvg2KT), "在你離開時自動更新用戶端" (4vprnG) | catalogs |
|
||||||
|
|
||||||
|
Rebuild: the version shown is the program file's build time (sidebar footer, Settings → 一般 → 版本).
|
||||||
|
A newer build on disk shows the sidebar button "⬆ 重新啟動以更新(time)". Noticing it is a file check
|
||||||
|
every 10 s (**estimated**; it's local and cheap). Switching by itself follows the original's schedule:
|
||||||
|
first try 30 s after start, then hourly plus jitter, and only when the screen is locked or the
|
||||||
|
screensaver is running (`ioreg` session flag / `pgrep -x ScreenSaverEngine`), idle ≥ 600 s, and no
|
||||||
|
bot is working or waiting for you. It uses the same wait-then-start relaunch helper. Setting: 在你離開時
|
||||||
|
自動更新用戶端 (on by default). Tested by `update_applies_only_when_truly_away`.
|
||||||
|
|
||||||
|
## Left out of this version
|
||||||
|
Done since this list was first written: routines (例行任務), screen control on 你的電腦, AnyBot's own
|
||||||
|
browser with profiles, reading the person's own Chrome, removing an added browser profile.
|
||||||
|
Still left out: Slack/Telegram/WhatsApp/email messaging, team bots and sharing, payments (these need the
|
||||||
|
original's cloud service and accounts), routines started by events (GitHub, PagerDuty, Sentry need
|
||||||
|
webhooks into that cloud), "接管" of a cloud bot computer, voice input, file attachments, and the exact
|
||||||
|
signed-in window layout and dark-theme colours (never captured).
|
||||||
|
|
||||||
|
## Look check (full pass over every screen)
|
||||||
|
- One font for everything: PingFang TC Regular (macOS system asset, found under
|
||||||
|
`/System/Library/AssetsV2/com_apple_MobileAsset_Font*/…/PingFang.ttc`, face 2), falling back to
|
||||||
|
Hiragino Sans GB. Before, English came from egui's own font and Chinese from a fallback, on
|
||||||
|
different baselines and sizes. egui's fonts stay behind it only for missing symbols.
|
||||||
|
- That font's 「·」 already has space on both sides, so separators are written without extra spaces.
|
||||||
|
- Sidebar names and previews end in "…" before the status dot instead of being cut mid-letter.
|
||||||
|
- A step's copy/model line only shows under the bot's final words, so a tool's result sits right
|
||||||
|
under the step that asked for it.
|
||||||
|
- Settings and Bot 設定 stop 80 px short of the window height (**estimated**) and scroll inside;
|
||||||
|
the provider list is 200 px wide (**estimated**) so 「自訂(OpenAI 相容)」 fits on one line;
|
||||||
|
all avatar shapes fit on one row; the model list button says 「選擇」.
|
||||||
|
- Decompile note: the native main program (`Grok Bot`, Mach-O 53 040 bytes, sha256 df5b07de…) is
|
||||||
|
only Electron's launcher: a search for font / width / height / title finds **0** strings
|
||||||
|
(evidence `ev_7cf8e974…`). The look lives in the renderer styles (index-Dh7EWA8t.css), read here:
|
||||||
|
- Type scale `--sand-font-size-*`: xxs 11, xs 12, sm 13, base 14, lg 15, xl 17, xxl 22, xxxl 26 px;
|
||||||
|
`--sand-line-height-*`: 16, 16, 18, 20, 22, 24, 28, 32 px.
|
||||||
|
- `--cursor-font-family-sans`: `-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif`, which on
|
||||||
|
a zh-TW Mac means SF for Latin and PingFang for Chinese. `--cursor-font-family-mono`:
|
||||||
|
`ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, …`.
|
||||||
|
- Rebuild: body/buttons 14 (base), small 12 (xs), headings and window titles 17 (xl), code 13 (sm)
|
||||||
|
on 18 px lines, chat text 15 (lg) on 22 px lines, Markdown headings 17 on 24 px lines; code in SF
|
||||||
|
Mono (`/System/Library/Fonts/SFNSMono.ttf`, then Menlo). Text stays in PingFang TC for both
|
||||||
|
scripts, which matches -apple-system's Chinese and is close to SF for Latin (egui can't line up
|
||||||
|
two fonts' baselines the way the browser does).
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
# AnyBot's own computer ("Bot 的電腦"): a Linux desktop the bots share, like the original's box
|
||||||
|
# (an X11 desktop driven by xdotool, watched through noVNC on port 6080).
|
||||||
|
FROM debian:bookworm-slim
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive LANG=C.UTF-8 TZ=UTC
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
xvfb x11vnc xdotool x11-utils x11-xserver-utils imagemagick fluxbox xterm \
|
||||||
|
novnc python3-websockify chromium fonts-noto-cjk fonts-noto-color-emoji \
|
||||||
|
ca-certificates curl wget git jq unzip zip less nano vim-tiny procps psmisc \
|
||||||
|
python3 python3-pip python3-venv nodejs npm sudo tini dbus-x11 xclip \
|
||||||
|
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
|
||||||
|
&& useradd -m -s /bin/bash bot && echo 'bot ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/bot \
|
||||||
|
&& ln -sf /usr/share/novnc/vnc.html /usr/share/novnc/index.html
|
||||||
|
COPY start.sh /usr/local/bin/anybot-box-start
|
||||||
|
COPY fluxbox-init /etc/anybot/fluxbox-init
|
||||||
|
COPY overlay /etc/anybot/overlay
|
||||||
|
COPY Xresources /etc/anybot/Xresources
|
||||||
|
COPY menu /etc/anybot/menu
|
||||||
|
RUN chmod +x /usr/local/bin/anybot-box-start \
|
||||||
|
# Chromium has no sandbox inside a container (no user namespaces); the box itself is the
|
||||||
|
# sandbox. Debian's chromium wrapper reads these flags for every launch (menu, bots, xdg-open).
|
||||||
|
&& printf '%s\n' 'export CHROMIUM_FLAGS="$CHROMIUM_FLAGS --no-sandbox --disable-dev-shm-usage --no-first-run --no-default-browser-check --disable-gpu --password-store=basic --test-type"' > /etc/chromium.d/anybot
|
||||||
|
ENV DISPLAY=:1 SCREEN_SIZE=1280x800x24
|
||||||
|
EXPOSE 6080
|
||||||
|
LABEL anybot.box.version="3"
|
||||||
|
ENTRYPOINT ["/usr/bin/tini", "--"]
|
||||||
|
CMD ["/usr/local/bin/anybot-box-start"]
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
XTerm*faceName: Noto Sans Mono CJK TC
|
||||||
|
XTerm*faceSize: 11
|
||||||
|
XTerm*background: #1f1f1f
|
||||||
|
XTerm*foreground: #f5f5f5
|
||||||
|
XTerm*geometry: 100x30
|
||||||
|
Xft.dpi: 96
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
session.screen0.toolbar.visible: true
|
||||||
|
session.screen0.toolbar.placement: BottomCenter
|
||||||
|
session.screen0.toolbar.widthPercent: 100
|
||||||
|
session.screen0.workspaces: 1
|
||||||
|
session.screen0.workspaceNames: Bot 的電腦
|
||||||
|
session.screen0.defaultDeco: NORMAL
|
||||||
|
session.styleOverlay: ~/.fluxbox/overlay
|
||||||
|
session.screen0.allowRemoteActions: true
|
||||||
|
session.screen0.rootCommand: xsetroot -solid '#2b2f36'
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
[begin] (Bot 的電腦)
|
||||||
|
[exec] (瀏覽器 Chromium) {chromium --no-first-run --no-default-browser-check --disable-gpu}
|
||||||
|
[exec] (終端機) {xterm}
|
||||||
|
[separator]
|
||||||
|
[workspaces] (視窗)
|
||||||
|
[restart] (重新整理桌面)
|
||||||
|
[end]
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
background: none
|
||||||
|
*.font: Noto Sans CJK TC-10
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Boots the desktop: X server, window manager, VNC and the noVNC web viewer.
|
||||||
|
set -u
|
||||||
|
mkdir -p /home/bot/.fluxbox /home/bot/Desktop && cp -n /etc/anybot/fluxbox-init /home/bot/.fluxbox/init 2>/dev/null
|
||||||
|
cp -n /etc/anybot/overlay /home/bot/.fluxbox/overlay 2>/dev/null
|
||||||
|
cp -n /etc/anybot/Xresources /home/bot/.Xresources 2>/dev/null
|
||||||
|
cp /etc/anybot/menu /home/bot/.fluxbox/menu 2>/dev/null
|
||||||
|
chown -R bot:bot /home/bot
|
||||||
|
rm -f /tmp/.X1-lock /tmp/.X11-unix/X1
|
||||||
|
Xvfb :1 -screen 0 "${SCREEN_SIZE}" -nolisten tcp &
|
||||||
|
for i in $(seq 1 50); do xdpyinfo -display :1 >/dev/null 2>&1 && break; sleep 0.1; done
|
||||||
|
su bot -c "DISPLAY=:1 xrdb -nocpp -merge /home/bot/.Xresources"
|
||||||
|
su bot -c "DISPLAY=:1 fluxbox >/tmp/fluxbox.log 2>&1 &"
|
||||||
|
(sleep 1.5; xsetroot -display :1 -solid '#2b2f36') &
|
||||||
|
xsetroot -display :1 -solid '#2b2f36'
|
||||||
|
x11vnc -display :1 -forever -shared -nopw -quiet -rfbport 5900 -localhost >/tmp/x11vnc.log 2>&1 &
|
||||||
|
websockify --web /usr/share/novnc 6080 localhost:5900 >/tmp/novnc.log 2>&1 &
|
||||||
|
echo "anybot-box ready" > /tmp/anybot-box.ready
|
||||||
|
wait
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=AnyBot
|
||||||
|
Comment=Your own AI bots that work on their own computer and yours
|
||||||
|
Comment[zh_TW]=你的 AI Bot:在自己的電腦和你的電腦上幫你做事
|
||||||
|
Exec=anybot
|
||||||
|
Icon=anybot
|
||||||
|
Terminal=false
|
||||||
|
Categories=Utility;Office;
|
||||||
|
StartupWMClass=anybot
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
# AnyBot's Linux desktop build-and-test box: Rust, the libraries eframe/rodio need, a virtual
|
||||||
|
# screen (Xvfb) to run the window headless, a headless Wayland desktop (sway) for the Wayland
|
||||||
|
# path, and the desktop tools AnyBot uses on Linux.
|
||||||
|
FROM rust:1-bookworm
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
pkg-config libasound2-dev libxkbcommon-dev libxkbcommon-x11-0 libwayland-dev libx11-dev libxcursor1 libxrandr2 libxi6 libgl1 libegl1 libgl1-mesa-dri \
|
||||||
|
libxcb1-dev libxcb-randr0-dev libxcb-shm0-dev libxdo-dev libdbus-1-dev libxtst-dev libpipewire-0.3-dev libclang-dev libgbm-dev \
|
||||||
|
xvfb xdotool x11-utils imagemagick libnotify-bin libsecret-tools espeak-ng xclip dbus-x11 gnome-keyring \
|
||||||
|
fonts-noto-cjk ffmpeg poppler-utils ripgrep docker.io \
|
||||||
|
sway foot wev grim wl-clipboard \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
# A headless GNOME (Wayland) desktop for testing AnyBot's portal path, on top of the dev box.
|
||||||
|
# docker build -t anybot-linux-dev deploy/linux/dev && docker build -t anybot-linux-gnome deploy/linux/dev/gnome
|
||||||
|
FROM anybot-linux-dev
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
gnome-shell xdg-desktop-portal xdg-desktop-portal-gnome pipewire wireplumber dbus-user-session gnome-settings-daemon \
|
||||||
|
python3-dbusmock python3-gi gir1.2-glib-2.0 zenity locales \
|
||||||
|
gstreamer1.0-tools gstreamer1.0-pipewire gstreamer1.0-plugins-base gstreamer1.0-plugins-good \
|
||||||
|
&& sed -i "s/# zh_TW.UTF-8/zh_TW.UTF-8/" /etc/locale.gen && locale-gen && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
# The test's stand-in for the person: screenshots through the portal, clicks and keys through
|
||||||
|
# Mutter's own remote desktop interface. op.py shot out.png | click x y | key keysym
|
||||||
|
import sys, time, shutil, urllib.parse
|
||||||
|
from gi.repository import Gio, GLib
|
||||||
|
bus = Gio.bus_get_sync(Gio.BusType.SESSION)
|
||||||
|
def call(dest, path, iface, method, args=None, sig=None):
|
||||||
|
return bus.call_sync(dest, path, iface, method, args, None, 0, -1, None)
|
||||||
|
def shot(out):
|
||||||
|
token = 'op%d' % int(time.time() * 1000)
|
||||||
|
sender = bus.get_unique_name()[1:].replace('.', '_')
|
||||||
|
req = '/org/freedesktop/portal/desktop/request/%s/%s' % (sender, token)
|
||||||
|
loop = GLib.MainLoop(); res = {}
|
||||||
|
def on(conn, snd, path, iface, sig, params, *a):
|
||||||
|
res['r'] = params.unpack(); loop.quit()
|
||||||
|
bus.signal_subscribe(None, 'org.freedesktop.portal.Request', 'Response', req, None, 0, on)
|
||||||
|
call('org.freedesktop.portal.Desktop', '/org/freedesktop/portal/desktop', 'org.freedesktop.portal.Screenshot', 'Screenshot',
|
||||||
|
GLib.Variant('(sa{sv})', ('', {'handle_token': GLib.Variant('s', token), 'interactive': GLib.Variant('b', False)})))
|
||||||
|
GLib.timeout_add_seconds(10, loop.quit); loop.run()
|
||||||
|
uri = res['r'][1]['uri']
|
||||||
|
shutil.move(urllib.parse.unquote(uri[7:]), out)
|
||||||
|
def session():
|
||||||
|
path = call('org.gnome.Mutter.RemoteDesktop', '/org/gnome/Mutter/RemoteDesktop', 'org.gnome.Mutter.RemoteDesktop', 'CreateSession').unpack()[0]
|
||||||
|
call('org.gnome.Mutter.RemoteDesktop', path, 'org.gnome.Mutter.RemoteDesktop.Session', 'Start')
|
||||||
|
return lambda m, a: call('org.gnome.Mutter.RemoteDesktop', path, 'org.gnome.Mutter.RemoteDesktop.Session', m, a)
|
||||||
|
def eye(out):
|
||||||
|
# Mutter's own screen cast (the operator's view, no portal), one frame through GStreamer.
|
||||||
|
import subprocess
|
||||||
|
sp = call('org.gnome.Mutter.ScreenCast', '/org/gnome/Mutter/ScreenCast', 'org.gnome.Mutter.ScreenCast', 'CreateSession', GLib.Variant('(a{sv})', ({},))).unpack()[0]
|
||||||
|
st = call('org.gnome.Mutter.ScreenCast', sp, 'org.gnome.Mutter.ScreenCast.Session', 'RecordMonitor', GLib.Variant('(sa{sv})', ('Meta-0', {}))).unpack()[0]
|
||||||
|
loop = GLib.MainLoop(); res = {}
|
||||||
|
def on(conn, snd, path, iface, sig, params, *a):
|
||||||
|
res['node'] = params.unpack()[0]; loop.quit()
|
||||||
|
bus.signal_subscribe(None, 'org.gnome.Mutter.ScreenCast.Stream', 'PipeWireStreamAdded', st, None, 0, on)
|
||||||
|
call('org.gnome.Mutter.ScreenCast', sp, 'org.gnome.Mutter.ScreenCast.Session', 'Start')
|
||||||
|
GLib.timeout_add_seconds(5, loop.quit); loop.run()
|
||||||
|
subprocess.run(['gst-launch-1.0', '-q', 'pipewiresrc', 'path=%d' % res['node'], 'num-buffers=3', '!', 'videoconvert', '!', 'pngenc', 'snapshot=true', '!', 'filesink', 'location=' + out], timeout=20)
|
||||||
|
call('org.gnome.Mutter.ScreenCast', sp, 'org.gnome.Mutter.ScreenCast.Session', 'Stop')
|
||||||
|
cmd = sys.argv[1]
|
||||||
|
if cmd == 'eye':
|
||||||
|
eye(sys.argv[2])
|
||||||
|
if cmd == 'shot':
|
||||||
|
shot(sys.argv[2])
|
||||||
|
elif cmd == 'click':
|
||||||
|
# Absolute moves need a screen cast stream tied to the remote desktop session.
|
||||||
|
rdp = call('org.gnome.Mutter.RemoteDesktop', '/org/gnome/Mutter/RemoteDesktop', 'org.gnome.Mutter.RemoteDesktop', 'CreateSession').unpack()[0]
|
||||||
|
sid = call('org.gnome.Mutter.RemoteDesktop', rdp, 'org.freedesktop.DBus.Properties', 'Get', GLib.Variant('(ss)', ('org.gnome.Mutter.RemoteDesktop.Session', 'SessionId'))).unpack()[0]
|
||||||
|
sp = call('org.gnome.Mutter.ScreenCast', '/org/gnome/Mutter/ScreenCast', 'org.gnome.Mutter.ScreenCast', 'CreateSession', GLib.Variant('(a{sv})', ({'remote-desktop-session-id': GLib.Variant('s', sid)},))).unpack()[0]
|
||||||
|
st = call('org.gnome.Mutter.ScreenCast', sp, 'org.gnome.Mutter.ScreenCast.Session', 'RecordMonitor', GLib.Variant('(sa{sv})', ('Meta-0', {}))).unpack()[0]
|
||||||
|
loop = GLib.MainLoop()
|
||||||
|
bus.signal_subscribe(None, 'org.gnome.Mutter.ScreenCast.Stream', 'PipeWireStreamAdded', st, None, 0, lambda *a: loop.quit())
|
||||||
|
call('org.gnome.Mutter.RemoteDesktop', rdp, 'org.gnome.Mutter.RemoteDesktop.Session', 'Start')
|
||||||
|
GLib.timeout_add_seconds(5, loop.quit); loop.run()
|
||||||
|
import subprocess
|
||||||
|
gst = subprocess.Popen(['gst-launch-1.0', '-q', 'pipewiresrc', 'path=%d' % 0, '!', 'fakesink'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
s = lambda m, a: call('org.gnome.Mutter.RemoteDesktop', rdp, 'org.gnome.Mutter.RemoteDesktop.Session', m, a)
|
||||||
|
time.sleep(0.5)
|
||||||
|
s('NotifyPointerMotionAbsolute', GLib.Variant('(sdd)', (st, float(sys.argv[2]), float(sys.argv[3])))); time.sleep(0.2)
|
||||||
|
s('NotifyPointerButton', GLib.Variant('(ib)', (0x110, True))); time.sleep(0.05)
|
||||||
|
s('NotifyPointerButton', GLib.Variant('(ib)', (0x110, False))); time.sleep(0.3)
|
||||||
|
elif cmd == 'key':
|
||||||
|
s = session(); time.sleep(0.3)
|
||||||
|
for k in sys.argv[2:]:
|
||||||
|
s('NotifyKeyboardKeysym', GLib.Variant('(ub)', (int(k, 16), True))); time.sleep(0.05)
|
||||||
|
s('NotifyKeyboardKeysym', GLib.Variant('(ub)', (int(k, 16), False))); time.sleep(0.1)
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
export XDG_RUNTIME_DIR=/tmp/xdg-g; rm -rf $XDG_RUNTIME_DIR; mkdir -m 700 -p $XDG_RUNTIME_DIR
|
||||||
|
export XDG_CURRENT_DESKTOP=GNOME XDG_SESSION_TYPE=wayland LANG=zh_TW.UTF-8
|
||||||
|
unset DISPLAY
|
||||||
|
dbus-run-session -- sh -c '
|
||||||
|
echo "export DBUS_SESSION_BUS_ADDRESS=$DBUS_SESSION_BUS_ADDRESS XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR WAYLAND_DISPLAY=wayland-0 XDG_CURRENT_DESKTOP=GNOME XDG_SESSION_TYPE=wayland LANG=zh_TW.UTF-8 HOME=/home/t" > /tmp/env.sh
|
||||||
|
pipewire >/tmp/pw.log 2>&1 &
|
||||||
|
sleep 0.5; wireplumber >/tmp/wp.log 2>&1 &
|
||||||
|
gnome-shell --headless --wayland --no-x11 --virtual-monitor 1280x800 >/tmp/shell.log 2>&1 &
|
||||||
|
sleep 4
|
||||||
|
/usr/libexec/xdg-desktop-portal >/tmp/xdp.log 2>&1 &
|
||||||
|
sleep infinity'
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# AnyBot on GNOME Wayland (the desktop portals), from a Mac or anywhere with Docker:
|
||||||
|
# deploy/linux/dev/gnome/test.sh (from app/, after building both images)
|
||||||
|
# A headless GNOME 43 runs in the box; op.py plays the person: it answers GNOME's
|
||||||
|
# 「遠端桌面」 dialog (switch on 「允許遠端操作」, press 「分享」) and reads the screen through
|
||||||
|
# Mutter's own screen cast. Checks: the missing-switch message, typing English + Chinese into a
|
||||||
|
# text box, and clicking a small button exactly.
|
||||||
|
set -e
|
||||||
|
cd "$(dirname "$0")/../../../.."
|
||||||
|
docker rm -f anybot-gnome >/dev/null 2>&1 || true
|
||||||
|
docker run -d --name anybot-gnome -v "$PWD":/src -v anybot-linux-target:/target -w /src anybot-linux-gnome sh -c '
|
||||||
|
mkdir -p /run/dbus; dbus-daemon --system --fork; (python3 -m dbusmock --template logind --system >/tmp/mock.log 2>&1 &); sleep 1
|
||||||
|
useradd -m t; runuser -u t -- env HOME=/home/t sh deploy/linux/dev/gnome/session.sh' >/dev/null
|
||||||
|
sleep 10
|
||||||
|
docker exec -u t anybot-gnome sh -c '
|
||||||
|
. /tmp/env.sh; OP="python3 deploy/linux/dev/gnome/op.py"; A=/target/release/anybot
|
||||||
|
# Leave the overview, and answer GNOME'"'"'s one-time "allow screenshots?" as the person would.
|
||||||
|
$OP key ff1b; sleep 1
|
||||||
|
gdbus call --session -d org.freedesktop.impl.portal.PermissionStore -o /org/freedesktop/impl/portal/PermissionStore -m org.freedesktop.impl.portal.PermissionStore.SetPermission screenshot true screenshot "" "[\"yes\"]" >/dev/null
|
||||||
|
approve() { sleep 5; [ "$1" = share-only ] || { $OP click 820 492; sleep 0.5; }; $OP click 841 289; sleep 6; }
|
||||||
|
echo "1. 分享 without 允許遠端操作:"
|
||||||
|
(timeout 60 $A --tool computer_use_screenshot {} > /tmp/a1.log 2>&1 &); approve share-only; sed "s/^/ /" /tmp/a1.log
|
||||||
|
echo "2. typing into a text box:"
|
||||||
|
(GDK_BACKEND=wayland zenity --entry --title t --text 輸入 > /tmp/zen.out 2>/dev/null &); sleep 3
|
||||||
|
(timeout 60 $A --tool computer_use_screenshot {} computer_use_typing "{\"value\":\"Hi 你好,世界 ok\"}" computer_use_press_key "{\"key\":\"return\"}" > /tmp/a2.log 2>&1 &); approve
|
||||||
|
echo " text box got: [$(cat /tmp/zen.out)] (expected [Hi 你好,世界 ok])"
|
||||||
|
echo "3. clicking 「否」 in a yes/no dialog:"
|
||||||
|
(GDK_BACKEND=wayland zenity --question --title q --text 要繼續嗎 >/dev/null 2>&1; echo "$?" > /tmp/q.out) & sleep 3
|
||||||
|
(timeout 60 $A --tool computer_use_screenshot {} computer_use_click "{\"x\":98,\"y\":168}" > /tmp/a3.log 2>&1 &); approve
|
||||||
|
echo " dialog answer: $(cat /tmp/q.out) (expected 1 = 否)"
|
||||||
|
'
|
||||||
|
docker rm -f anybot-gnome >/dev/null
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# Build and test the Linux version from a Mac (or anywhere with Docker), on a virtual screen.
|
||||||
|
# docker build -t anybot-linux-dev deploy/linux/dev (once)
|
||||||
|
# deploy/linux/dev/test.sh (from app/)
|
||||||
|
# Runs: every unit test; the live X11 test (screenshot, clicks, keys, recording a demo); the
|
||||||
|
# keyring test (save / find / remove a login); the live Wayland test (headless sway: screenshot,
|
||||||
|
# clicks, Chinese typing, keys, scroll); and screenshots of the window into ./linux-shots.
|
||||||
|
set -e
|
||||||
|
cd "$(dirname "$0")/../../.."
|
||||||
|
mkdir -p linux-shots
|
||||||
|
docker run --rm -v "$PWD":/src -v anybot-linux-target:/target -v anybot-linux-cargo:/usr/local/cargo/registry \
|
||||||
|
-v "$PWD/linux-shots":/out -e CARGO_TARGET_DIR=/target -w /src anybot-linux-dev sh -c '
|
||||||
|
set -e
|
||||||
|
cargo build --release 2>&1 | grep -E "^(error|warning)" -A7 || true
|
||||||
|
Xvfb :99 -screen 0 1400x900x24 >/dev/null 2>&1 &
|
||||||
|
export DISPLAY=:99 LANG=zh_TW.UTF-8
|
||||||
|
sleep 1
|
||||||
|
cargo test --release 2>&1 | grep -E "test result|FAILED|panicked"
|
||||||
|
cargo test --release x11_live -- --ignored 2>&1 | grep -E "test result|panicked"
|
||||||
|
dbus-run-session -- sh -c "echo -n test | gnome-keyring-daemon --unlock --components=secrets >/dev/null 2>&1; sleep 1; cargo test --release keyring_live -- --ignored 2>&1 | grep -E \"test result|panicked\""
|
||||||
|
useradd -m t 2>/dev/null; chmod 777 /out
|
||||||
|
runuser -u t -- env CARGO_TARGET_DIR=/target HOME=/home/t sh deploy/linux/dev/wayland_live.sh 2>&1 | sed -n "/--- wev heard/,\$p"
|
||||||
|
for v in chat computers general; do
|
||||||
|
ANYBOT_DATA_DIR=/tmp/anybot-data ANYBOT_SHOT_VIEW=$v ANYBOT_SHOT_DARK=1 timeout 90 /target/release/anybot --screenshot /out/$v.png >/dev/null 2>&1 && echo "shot $v"
|
||||||
|
done'
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# Runs inside the test box: a headless sway desktop with `wev` (it prints every click and key it
|
||||||
|
# gets) in front, then AnyBot's own computer tools against it. Prints what wev heard.
|
||||||
|
set -e
|
||||||
|
export XDG_RUNTIME_DIR=/tmp/xdg-wl && rm -rf $XDG_RUNTIME_DIR && mkdir -m 700 -p $XDG_RUNTIME_DIR
|
||||||
|
unset DISPLAY
|
||||||
|
cat > /tmp/sway.conf <<'CONF'
|
||||||
|
output HEADLESS-1 resolution 1280x800 position 0 0
|
||||||
|
default_border none
|
||||||
|
CONF
|
||||||
|
WLR_BACKENDS=headless WLR_LIBINPUT_NO_DEVICES=1 WLR_RENDERER=pixman sway -c /tmp/sway.conf >/tmp/sway.log 2>&1 &
|
||||||
|
for i in $(seq 50); do [ -S $XDG_RUNTIME_DIR/wayland-1 ] && break; sleep 0.1; done
|
||||||
|
export WAYLAND_DISPLAY=wayland-1
|
||||||
|
stdbuf -oL wev > /tmp/wev.log 2>&1 &
|
||||||
|
sleep 1.5
|
||||||
|
BIN=${CARGO_TARGET_DIR:-target}/release/anybot
|
||||||
|
$BIN --tool computer_use_check_permissions '{}' \
|
||||||
|
computer_use_screenshot '{}' \
|
||||||
|
computer_use_click '{"x":400,"y":300}' \
|
||||||
|
computer_use_click '{"x":420,"y":310,"button":"right"}' \
|
||||||
|
computer_use_typing '{"value":"Hi 你好"}' \
|
||||||
|
computer_use_press_key '{"key":"cmd+a"}' \
|
||||||
|
computer_use_press_key '{"key":"return"}' \
|
||||||
|
computer_use_scroll '{"x":400,"y":300,"direction":"down","amount":2}'
|
||||||
|
sleep 0.5
|
||||||
|
grim /tmp/grim.png
|
||||||
|
echo "--- wev heard:"
|
||||||
|
grep -E "state: 1|axis: 0|depressed: 0+[1-9]" -A1 /tmp/wev.log | grep -oE "button: [0-9]+ \([a-z]+\)|utf8: '[^']+'|sym: (Control_L|Return)|axis: 0 \(vertical\), value: -?[0-9.]+|depressed: 0+[1-9]+" | uniq | tr '\n' ' '; echo
|
||||||
|
echo "--- screenshot (anybot vs grim):"
|
||||||
|
identify -format "%wx%h\n" /tmp/anybot-tool-1.jpg /tmp/grim.png
|
||||||
|
cp /tmp/anybot-tool-1.jpg /out/wayland-shot.jpg 2>/dev/null || true
|
||||||
|
cp /tmp/grim.png /out/wayland-grim.png 2>/dev/null || true
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# Install AnyBot on a Linux desktop for the current user (no root needed for this part).
|
||||||
|
#
|
||||||
|
# ./deploy/linux/install.sh build from source, then install
|
||||||
|
# ./deploy/linux/install.sh --deps also install the system packages (asks for sudo)
|
||||||
|
#
|
||||||
|
# Puts the app in ~/.local/bin/anybot, the menu entry in ~/.local/share/applications and the
|
||||||
|
# icon in ~/.local/share/icons. Run it from the app/ folder.
|
||||||
|
set -e
|
||||||
|
cd "$(dirname "$0")/../.."
|
||||||
|
|
||||||
|
# What AnyBot uses on Linux (all optional except the first line; the app explains what's missing):
|
||||||
|
# libasound2 libxkbcommon-x11-0 libgl1 xdg-utils – the window and sound
|
||||||
|
# libnotify-bin notifications libsecret-tools saved logins (系統鑰匙圈)
|
||||||
|
# espeak-ng reading answers aloud zenity file pickers
|
||||||
|
# xclip / wl-clipboard pasting pictures; typing Chinese on GNOME/KDE Wayland
|
||||||
|
# ffmpeg video posters, Opus, demo videos
|
||||||
|
# docker.io the bot's own computer fonts-noto-cjk extra CJK glyphs (粉圓體 is built in)
|
||||||
|
DEPS_DEBIAN="libasound2 libxkbcommon-x11-0 libgl1 xdg-utils libnotify-bin libsecret-tools espeak-ng zenity xclip wl-clipboard ffmpeg docker.io fonts-noto-cjk"
|
||||||
|
DEPS_FEDORA="alsa-lib libxkbcommon-x11 mesa-libGL xdg-utils libnotify libsecret espeak-ng zenity xclip wl-clipboard ffmpeg-free moby-engine google-noto-sans-cjk-fonts"
|
||||||
|
DEPS_ARCH="alsa-lib libxkbcommon-x11 mesa xdg-utils libnotify libsecret espeak-ng zenity xclip wl-clipboard ffmpeg docker noto-fonts-cjk"
|
||||||
|
BUILD_DEBIAN="build-essential pkg-config libasound2-dev"
|
||||||
|
|
||||||
|
if [ "$1" = "--deps" ]; then
|
||||||
|
if command -v apt-get >/dev/null; then
|
||||||
|
sudo apt-get update && sudo apt-get install -y $DEPS_DEBIAN $BUILD_DEBIAN
|
||||||
|
elif command -v dnf >/dev/null; then
|
||||||
|
sudo dnf install -y $DEPS_FEDORA gcc pkgconf alsa-lib-devel
|
||||||
|
elif command -v pacman >/dev/null; then
|
||||||
|
sudo pacman -S --needed $DEPS_ARCH base-devel
|
||||||
|
else
|
||||||
|
echo "Install these with your package manager: $DEPS_DEBIAN"
|
||||||
|
fi
|
||||||
|
if command -v docker >/dev/null && ! id -nG | grep -qw docker; then
|
||||||
|
echo "Adding you to the docker group (the bot's computer needs it); log out and back in afterwards."
|
||||||
|
sudo usermod -aG docker "$USER"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
OUT="${CARGO_TARGET_DIR:-target}/release/anybot"
|
||||||
|
if [ -x "$OUT" ] && [ "$1" != "--rebuild" ]; then
|
||||||
|
echo "Using the existing build ($OUT)."
|
||||||
|
else
|
||||||
|
command -v cargo >/dev/null || { echo "Rust is needed to build: https://rustup.rs"; exit 1; }
|
||||||
|
cargo build --release
|
||||||
|
fi
|
||||||
|
|
||||||
|
BIN="$HOME/.local/bin"
|
||||||
|
APPS="$HOME/.local/share/applications"
|
||||||
|
ICONS="$HOME/.local/share/icons/hicolor"
|
||||||
|
mkdir -p "$BIN" "$APPS" "$ICONS/256x256/apps" "$ICONS/128x128/apps" "$ICONS/scalable/apps"
|
||||||
|
install -m 755 "$OUT" "$BIN/anybot"
|
||||||
|
install -m 644 deploy/linux/anybot.desktop "$APPS/anybot.desktop"
|
||||||
|
install -m 644 icons/anybot.svg "$ICONS/scalable/apps/anybot.svg"
|
||||||
|
"$BIN/anybot" --write-icon "$ICONS/256x256/apps/anybot.png" 256
|
||||||
|
"$BIN/anybot" --write-icon "$ICONS/128x128/apps/anybot.png" 128
|
||||||
|
command -v update-desktop-database >/dev/null && update-desktop-database "$APPS" 2>/dev/null || true
|
||||||
|
command -v gtk-update-icon-cache >/dev/null && gtk-update-icon-cache -q "$ICONS" 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "AnyBot is installed: find it in your app menu, or run: anybot"
|
||||||
|
case ":$PATH:" in *":$BIN:"*) ;; *) echo "(Add $BIN to your PATH to start it from a terminal.)" ;; esac
|
||||||
|
if [ "${XDG_SESSION_TYPE:-}" = "wayland" ]; then
|
||||||
|
echo "Wayland: bots can see and use your screen. On GNOME/KDE the first time, a system window asks:"
|
||||||
|
echo " turn on 「允許遠端操作」 (Allow remote interaction), then press 「分享」 (Share)."
|
||||||
|
echo " Recording a demonstration (教它一項任務) needs an Xorg session."
|
||||||
|
fi
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- AnyBot 帳號伺服器 on this Mac, on 127.0.0.1:8780; Tailscale serve gives it https://mac-mini.tailbcc3a6.ts.net (tailnet only).
|
||||||
|
Install (once):
|
||||||
|
mkdir -p ~/Library/Application\ Support/AnyBot/relay
|
||||||
|
cp relay/target/release/anybot-relay ~/Library/Application\ Support/AnyBot/relay/
|
||||||
|
cp deploy/mac/com.anybot.relay.plist ~/Library/LaunchAgents/
|
||||||
|
launchctl load ~/Library/LaunchAgents/com.anybot.relay.plist
|
||||||
|
Remove: launchctl unload ~/Library/LaunchAgents/com.anybot.relay.plist -->
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>Label</key><string>com.anybot.relay</string>
|
||||||
|
<key>ProgramArguments</key>
|
||||||
|
<array>
|
||||||
|
<string>/Users/daniel/Library/Application Support/AnyBot/relay/anybot-relay</string>
|
||||||
|
<string>--bind</string><string>127.0.0.1</string>
|
||||||
|
<string>--port</string><string>8780</string>
|
||||||
|
<string>--data</string><string>/Users/daniel/Library/Application Support/AnyBot/relay/data</string>
|
||||||
|
</array>
|
||||||
|
<key>EnvironmentVariables</key>
|
||||||
|
<dict><key>ANYBOT_RELAY_PUBLIC_URL</key><string>https://mac-mini.tailbcc3a6.ts.net</string></dict>
|
||||||
|
<key>RunAtLoad</key><true/>
|
||||||
|
<key>KeepAlive</key><true/>
|
||||||
|
<key>ThrottleInterval</key><integer>10</integer>
|
||||||
|
<key>StandardErrorPath</key><string>/Users/daniel/Library/Application Support/AnyBot/relay/relay.log</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- An always-on Mac (e.g. a Mac mini) running AnyBot without a window.
|
||||||
|
cp com.anybot.serve.plist ~/Library/LaunchAgents/ && launchctl load ~/Library/LaunchAgents/com.anybot.serve.plist
|
||||||
|
Change the program path below to where anybot is. Sign in to the account once in the
|
||||||
|
AnyBot window (設定 › 手機與帳號) before switching to this; the window and this must not run
|
||||||
|
at the same time. -->
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>Label</key><string>com.anybot.serve</string>
|
||||||
|
<key>ProgramArguments</key>
|
||||||
|
<array>
|
||||||
|
<string>/Applications/AnyBot.app/Contents/MacOS/anybot</string>
|
||||||
|
<string>--serve</string>
|
||||||
|
</array>
|
||||||
|
<key>RunAtLoad</key><true/>
|
||||||
|
<key>KeepAlive</key><true/>
|
||||||
|
<key>StandardErrorPath</key><string>/tmp/anybot-serve.log</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
# Copy to .env and fill in.
|
||||||
|
ANYBOT_USER=daniel
|
||||||
|
ANYBOT_PASSWORD=change-me-please
|
||||||
|
ANYBOT_HOST_NAME=雲端 AnyBot
|
||||||
|
# 1 = the first start takes over the bots from your Mac's newest backup.
|
||||||
|
ANYBOT_RESTORE_FROM_ACCOUNT=
|
||||||
|
# Only needed when other people should be able to sign up.
|
||||||
|
ANYBOT_RELAY_INVITE=
|
||||||
|
XAI_API_KEY=
|
||||||
|
ANTHROPIC_API_KEY=
|
||||||
|
OPENAI_API_KEY=
|
||||||
|
GEMINI_API_KEY=
|
||||||
|
# Public address, e.g. https://anybot.example.com
|
||||||
|
ANYBOT_RELAY_PUBLIC_URL=
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
# AnyBot without a window (`anybot --serve`) on a Linux server: the bots, routines, triggers and
|
||||||
|
# channels run around the clock; the person uses the phone. The bot's own computer is a sibling
|
||||||
|
# container started through the host's Docker (the socket is mounted in docker-compose.yml).
|
||||||
|
# Build from the app/ folder: docker build -f deploy/server/Dockerfile -t anybot-serve .
|
||||||
|
FROM rust:1-bookworm AS build
|
||||||
|
WORKDIR /src
|
||||||
|
COPY . .
|
||||||
|
RUN cargo build --release --bin anybot
|
||||||
|
|
||||||
|
FROM debian:bookworm-slim
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl git ripgrep poppler-utils docker.io openssh-client \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
COPY --from=build /src/target/release/anybot /usr/local/bin/anybot
|
||||||
|
ENV ANYBOT_DATA_DIR=/data
|
||||||
|
VOLUME /data
|
||||||
|
CMD ["anybot", "--serve"]
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
# Everything on one small server: the account server (relay), HTTPS (Caddy), and AnyBot itself.
|
||||||
|
# 1. Point your domain at the server and put it in ../../relay/Caddyfile.
|
||||||
|
# 2. Put your account name and password in .env (see .env.example).
|
||||||
|
# 3. docker compose up -d
|
||||||
|
# 4. On the phone open https://your-domain, create the account (first time), and use it.
|
||||||
|
# AI keys: set them in .env, or set ANYBOT_RESTORE_FROM_ACCOUNT=1 to start from the newest backup
|
||||||
|
# your Mac made (bots, chats, routines and the keys you set there).
|
||||||
|
services:
|
||||||
|
relay:
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: relay/Dockerfile
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
# Your public address (for the links of sites the bots publish), e.g. https://anybot.example.com
|
||||||
|
ANYBOT_RELAY_PUBLIC_URL: "${ANYBOT_RELAY_PUBLIC_URL:-}"
|
||||||
|
ANYBOT_RELAY_INVITE: "${ANYBOT_RELAY_INVITE:-}"
|
||||||
|
volumes:
|
||||||
|
- relay-data:/data
|
||||||
|
caddy:
|
||||||
|
image: caddy:2
|
||||||
|
restart: unless-stopped
|
||||||
|
ports: ["80:80", "443:443"]
|
||||||
|
volumes:
|
||||||
|
- ../../relay/Caddyfile:/etc/caddy/Caddyfile:ro
|
||||||
|
- caddy-data:/data
|
||||||
|
depends_on: [relay]
|
||||||
|
anybot:
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: deploy/server/Dockerfile
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on: [relay]
|
||||||
|
environment:
|
||||||
|
ANYBOT_ACCOUNT: "http://relay:8080|${ANYBOT_USER}"
|
||||||
|
ANYBOT_ACCOUNT_PASSWORD: "${ANYBOT_PASSWORD}"
|
||||||
|
ANYBOT_HOST_NAME: "${ANYBOT_HOST_NAME:-雲端 AnyBot}"
|
||||||
|
XAI_API_KEY: "${XAI_API_KEY:-}"
|
||||||
|
ANTHROPIC_API_KEY: "${ANTHROPIC_API_KEY:-}"
|
||||||
|
OPENAI_API_KEY: "${OPENAI_API_KEY:-}"
|
||||||
|
GEMINI_API_KEY: "${GEMINI_API_KEY:-}"
|
||||||
|
ANYBOT_RESTORE_FROM_ACCOUNT: "${ANYBOT_RESTORE_FROM_ACCOUNT:-}"
|
||||||
|
volumes:
|
||||||
|
- anybot-data:/data
|
||||||
|
# The bot's own computer runs as a container next to this one.
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
volumes:
|
||||||
|
relay-data:
|
||||||
|
caddy-data:
|
||||||
|
anybot-data:
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
[Kosugi Maru]
|
||||||
|
Copyright (c) 2010 MOTOYA CO.,LTD.
|
||||||
|
|
||||||
|
[Varela Round]
|
||||||
|
Copyright (c) 2011-2016 The Varela Round Project Authors (https://github.com/alefalefalef/Varela-Round-Hebrew/), with Reserved Font Names 'Varela' and 'Varela Round'.
|
||||||
|
|
||||||
|
[jf open huninn]
|
||||||
|
Copyright (c) 2020-2024 The jf open huninn font is redistributed by justfont Co., LTD., with Reserved Font Names 'open huninn' and 'huninn'. under SIL Open Font License. The Hanzi part of this project was derived from Kosugi Maru under Apache-2.0 (https://www.apache.org/licenses/LICENSE-2.0).
|
||||||
|
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
|
||||||
|
<!-- AnyBot's app icon: the bot's face (two eyes) on a dark rounded square. -->
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0" stop-color="#3a3a3a"/>
|
||||||
|
<stop offset="1" stop-color="#1c1c1c"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect x="32" y="32" width="448" height="448" rx="112" fill="url(#bg)"/>
|
||||||
|
<rect x="32.5" y="32.5" width="447" height="447" rx="111.5" fill="none" stroke="#ffffff" stroke-opacity="0.08"/>
|
||||||
|
<ellipse cx="200" cy="262" rx="30" ry="50" fill="#ffffff" transform="rotate(-12 200 262)"/>
|
||||||
|
<ellipse cx="312" cy="262" rx="30" ry="50" fill="#ffffff" transform="rotate(-12 312 262)"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 744 B |
|
|
@ -0,0 +1,43 @@
|
||||||
|
ISC License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Lucide Icons and Contributors
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
purpose with or without fee is hereby granted, provided that the above
|
||||||
|
copyright notice and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
The following Lucide icons are derived from the Feather project:
|
||||||
|
|
||||||
|
airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out
|
||||||
|
|
||||||
|
The MIT License (MIT) (for the icons listed above)
|
||||||
|
|
||||||
|
Copyright (c) 2013-present Cole Bemis
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-alarm-clock"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="13" r="8" />
|
||||||
|
<path d="M12 9v4l2 2" />
|
||||||
|
<path d="M5 3 2 6" />
|
||||||
|
<path d="m22 6-3-3" />
|
||||||
|
<path d="M6.38 18.7 4 21" />
|
||||||
|
<path d="M17.64 18.67 20 21" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 466 B |
|
|
@ -0,0 +1,16 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-arrow-down"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M12 5v14" />
|
||||||
|
<path d="m19 12-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 343 B |
|
|
@ -0,0 +1,16 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-arrow-up"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="m5 12 7-7 7 7" />
|
||||||
|
<path d="M12 19V5" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 340 B |
|
|
@ -0,0 +1,20 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-audio-lines"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M2 10v3" />
|
||||||
|
<path d="M6 6v11" />
|
||||||
|
<path d="M10 3v18" />
|
||||||
|
<path d="M14 8v7" />
|
||||||
|
<path d="M18 5v13" />
|
||||||
|
<path d="M22 10v3" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 431 B |
|
|
@ -0,0 +1,17 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-bell-dot"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M10.268 21a2 2 0 0 0 3.464 0" />
|
||||||
|
<path d="M11.68 2.009A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673c-.824-.85-1.678-1.731-2.21-3.348" />
|
||||||
|
<circle cx="18" cy="5" r="3" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 510 B |
|
|
@ -0,0 +1,20 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-bot"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M12 8V4H8" />
|
||||||
|
<rect width="16" height="12" x="4" y="8" rx="2" />
|
||||||
|
<path d="M2 14h2" />
|
||||||
|
<path d="M20 14h2" />
|
||||||
|
<path d="M15 13v2" />
|
||||||
|
<path d="M9 13v2" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 454 B |
|
|
@ -0,0 +1,18 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-calendar"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M8 2v3" />
|
||||||
|
<path d="M16 2v3" />
|
||||||
|
<rect x="3" y="3" width="18" height="18" rx="2" />
|
||||||
|
<path d="M3 9h18" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 408 B |
|
|
@ -0,0 +1,15 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-check"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M20 6 9 17l-5-5" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 315 B |
|
|
@ -0,0 +1,15 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-chevron-down"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="m6 9 6 6 6-6" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 319 B |
|
|
@ -0,0 +1,15 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-chevron-right"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="m9 18 6-6-6-6" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 321 B |
|
|
@ -0,0 +1,17 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-circle-alert"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
<line x1="12" x2="12" y1="8" y2="12" />
|
||||||
|
<line x1="12" x2="12.01" y1="16" y2="16" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 415 B |
|
|
@ -0,0 +1,17 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-circle-arrow-up"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
<path d="m16 12-4-4-4 4" />
|
||||||
|
<path d="M12 16V8" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 384 B |
|
|
@ -0,0 +1,16 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-circle-check"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
<path d="m16 9-5.5 5.5L8 12" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 361 B |
|
|
@ -0,0 +1,16 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-circle-dot"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="1" />
|
||||||
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 360 B |
|
|
@ -0,0 +1,17 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-circle-x"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
<path d="m15 9-6 6" />
|
||||||
|
<path d="m9 9 6 6" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 372 B |
|
|
@ -0,0 +1,16 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-clock"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
<path d="M12 6v6l4 2" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 347 B |
|
|
@ -0,0 +1,17 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-cloud-download"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M12 13v8l-4-4" />
|
||||||
|
<path d="m12 21 4-4" />
|
||||||
|
<path d="M4.393 15.269A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.436 8.284" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 426 B |
|
|
@ -0,0 +1,20 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-cloud-sun"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M12 2v2" />
|
||||||
|
<path d="m4.93 4.93 1.41 1.41" />
|
||||||
|
<path d="M20 12h2" />
|
||||||
|
<path d="m19.07 4.93-1.41 1.41" />
|
||||||
|
<path d="M15.947 12.65a4 4 0 0 0-5.925-4.128" />
|
||||||
|
<path d="M13 22H7a5 5 0 1 1 4.9-6H13a3 3 0 0 1 0 6Z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 517 B |
|
|
@ -0,0 +1,16 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-copy"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
|
||||||
|
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 414 B |
|
|
@ -0,0 +1,17 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-credit-card"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<rect width="20" height="14" x="2" y="5" rx="2" />
|
||||||
|
<line x1="2" x2="22" y1="10" y2="10" />
|
||||||
|
<path d="M6 14h2" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 408 B |
|
|
@ -0,0 +1,17 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-download"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M12 15V3" />
|
||||||
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||||
|
<path d="m7 10 5 5 5-5" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 397 B |
|
|
@ -0,0 +1,17 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-ellipsis"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="1" />
|
||||||
|
<circle cx="19" cy="12" r="1" />
|
||||||
|
<circle cx="5" cy="12" r="1" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 391 B |
|
|
@ -0,0 +1,16 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-eraser"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M21 21H8a2 2 0 0 1-1.42-.587l-3.994-3.999a2 2 0 0 1 0-2.828l10-10a2 2 0 0 1 2.829 0l5.999 6a2 2 0 0 1 0 2.828L12.834 21" />
|
||||||
|
<path d="m5.082 11.09 8.828 8.828" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 460 B |
|
|
@ -0,0 +1,17 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-external-link"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M15 3h6v6" />
|
||||||
|
<path d="M10 14 21 3" />
|
||||||
|
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 416 B |
|
|
@ -0,0 +1,18 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-eye-off"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49" />
|
||||||
|
<path d="M14.084 14.158a3 3 0 0 1-4.242-4.242" />
|
||||||
|
<path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143" />
|
||||||
|
<path d="m2 2 20 20" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 582 B |
|
|
@ -0,0 +1,16 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-eye"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0" />
|
||||||
|
<circle cx="12" cy="12" r="3" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 434 B |
|
|
@ -0,0 +1,18 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-file-code"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" />
|
||||||
|
<path d="M14 2v5a1 1 0 0 0 1 1h5" />
|
||||||
|
<path d="M10 12.5 8 15l2 2.5" />
|
||||||
|
<path d="m14 12.5 2 2.5-2 2.5" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 524 B |
|
|
@ -0,0 +1,18 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-file-json"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" />
|
||||||
|
<path d="M14 2v5a1 1 0 0 0 1 1h5" />
|
||||||
|
<path d="M10 12a1 1 0 0 0-1 1v1a1 1 0 0 1-1 1 1 1 0 0 1 1 1v1a1 1 0 0 0 1 1" />
|
||||||
|
<path d="M14 18a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1 1 1 0 0 1-1-1v-1a1 1 0 0 0-1-1" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 619 B |
|
|
@ -0,0 +1,18 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-file-music"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M11.65 22H18a2 2 0 0 0 2-2V8a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 14 2H6a2 2 0 0 0-2 2v10.35" />
|
||||||
|
<path d="M14 2v5a1 1 0 0 0 1 1h5" />
|
||||||
|
<path d="M8 20v-7l3 1.474" />
|
||||||
|
<circle cx="6" cy="20" r="2" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 517 B |
|
|
@ -0,0 +1,20 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-file-spreadsheet"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" />
|
||||||
|
<path d="M14 2v5a1 1 0 0 0 1 1h5" />
|
||||||
|
<path d="M8 13h2" />
|
||||||
|
<path d="M14 13h2" />
|
||||||
|
<path d="M8 17h2" />
|
||||||
|
<path d="M14 17h2" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 554 B |
|
|
@ -0,0 +1,19 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-file-text"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" />
|
||||||
|
<path d="M14 2v5a1 1 0 0 0 1 1h5" />
|
||||||
|
<path d="M10 9H8" />
|
||||||
|
<path d="M16 13H8" />
|
||||||
|
<path d="M16 17H8" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 524 B |
|
|
@ -0,0 +1,19 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-file-type"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" />
|
||||||
|
<path d="M14 2v5a1 1 0 0 0 1 1h5" />
|
||||||
|
<path d="M11 18h2" />
|
||||||
|
<path d="M12 12v6" />
|
||||||
|
<path d="M9 13v-.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 .5.5v.5" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 564 B |
|
|
@ -0,0 +1,17 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-file-video"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" />
|
||||||
|
<path d="M14 2v5a1 1 0 0 0 1 1h5" />
|
||||||
|
<path d="M15.033 13.44a.647.647 0 0 1 0 1.12l-4.065 2.352a.645.645 0 0 1-.968-.56v-4.704a.645.645 0 0 1 .967-.56z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 574 B |
|
|
@ -0,0 +1,16 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-file"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" />
|
||||||
|
<path d="M14 2v5a1 1 0 0 0 1 1h5" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 448 B |
|
|
@ -0,0 +1,15 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-folder"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 419 B |
|
|
@ -0,0 +1,17 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-globe"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
<path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" />
|
||||||
|
<path d="M2 12h20" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 407 B |
|
|
@ -0,0 +1,17 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-history"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
|
||||||
|
<path d="M3 3v5h5" />
|
||||||
|
<path d="M12 7v5l4 2" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 402 B |
|
|
@ -0,0 +1,17 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-image"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<rect width="18" height="18" x="3" y="3" rx="2" ry="2" />
|
||||||
|
<circle cx="9" cy="9" r="2" />
|
||||||
|
<path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 434 B |
|
|
@ -0,0 +1,17 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-info"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
<path d="M12 16v-4" />
|
||||||
|
<path d="M12 8h.01" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 369 B |
|
|
@ -0,0 +1,16 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-key-round"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z" />
|
||||||
|
<circle cx="16.5" cy="7.5" r=".5" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 533 B |
|
|
@ -0,0 +1,23 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-keyboard"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M10 8h.01" />
|
||||||
|
<path d="M12 12h.01" />
|
||||||
|
<path d="M14 8h.01" />
|
||||||
|
<path d="M16 12h.01" />
|
||||||
|
<path d="M18 8h.01" />
|
||||||
|
<path d="M6 8h.01" />
|
||||||
|
<path d="M7 16h10" />
|
||||||
|
<path d="M8 12h.01" />
|
||||||
|
<rect width="20" height="16" x="2" y="4" rx="2" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 540 B |
|
|
@ -0,0 +1,18 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-layout-grid"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<rect width="7" height="7" x="3" y="3" rx="1" />
|
||||||
|
<rect width="7" height="7" x="14" y="3" rx="1" />
|
||||||
|
<rect width="7" height="7" x="14" y="14" rx="1" />
|
||||||
|
<rect width="7" height="7" x="3" y="14" rx="1" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 498 B |
|
|
@ -0,0 +1,17 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-layout-template"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<rect width="18" height="7" x="3" y="3" rx="1" />
|
||||||
|
<rect width="9" height="7" x="3" y="14" rx="1" />
|
||||||
|
<rect width="5" height="7" x="16" y="14" rx="1" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 451 B |
|
|
@ -0,0 +1,22 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-loader"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M12 2v4" />
|
||||||
|
<path d="m16.2 7.8 2.9-2.9" />
|
||||||
|
<path d="M18 12h4" />
|
||||||
|
<path d="m16.2 16.2 2.9 2.9" />
|
||||||
|
<path d="M12 18v4" />
|
||||||
|
<path d="m4.9 19.1 2.9-2.9" />
|
||||||
|
<path d="M2 12h4" />
|
||||||
|
<path d="m4.9 4.9 2.9 2.9" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 511 B |
|
|
@ -0,0 +1,16 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-lock"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<rect width="18" height="11" x="3" y="11" rx="2" ry="2" />
|
||||||
|
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 384 B |
|
|
@ -0,0 +1,16 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-mail"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7" />
|
||||||
|
<rect x="2" y="4" width="20" height="16" rx="2" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 391 B |
|
|
@ -0,0 +1,16 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-map-pin"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0" />
|
||||||
|
<circle cx="12" cy="10" r="3" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 437 B |
|
|
@ -0,0 +1,15 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-message-circle"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 432 B |
|
|
@ -0,0 +1,16 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-messages-square"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M16 10a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 14.286V4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
|
||||||
|
<path d="M20 9a2 2 0 0 1 2 2v10.286a.71.71 0 0 1-1.212.502l-2.202-2.202A2 2 0 0 0 17.172 19H10a2 2 0 0 1-2-2v-1" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 543 B |
|
|
@ -0,0 +1,17 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-mic"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M12 19v3" />
|
||||||
|
<path d="M19 10v2a7 7 0 0 1-14 0v-2" />
|
||||||
|
<rect x="9" y="2" width="6" height="13" rx="3" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 400 B |
|
|
@ -0,0 +1,15 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-minus"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M5 12h14" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 308 B |
|
|
@ -0,0 +1,17 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-monitor"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<rect width="20" height="14" x="2" y="3" rx="2" />
|
||||||
|
<line x1="8" x2="16" y1="21" y2="21" />
|
||||||
|
<line x1="12" x2="12" y1="17" y2="21" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 424 B |
|
|
@ -0,0 +1,16 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-move-down"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M8 18L12 22L16 18" />
|
||||||
|
<path d="M12 2V22" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 345 B |
|
|
@ -0,0 +1,16 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-move-up"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M8 6L12 2L16 6" />
|
||||||
|
<path d="M12 2V22" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 340 B |
|
|
@ -0,0 +1,20 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-notebook-pen"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M13.4 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-7.4" />
|
||||||
|
<path d="M2 6h4" />
|
||||||
|
<path d="M2 10h4" />
|
||||||
|
<path d="M2 14h4" />
|
||||||
|
<path d="M2 18h4" />
|
||||||
|
<path d="M21.378 5.626a1 1 0 1 0-3.004-3.004l-5.01 5.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 602 B |
|
|
@ -0,0 +1,17 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-panel-left-close"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<rect width="18" height="18" x="3" y="3" rx="2" />
|
||||||
|
<path d="M9 3v18" />
|
||||||
|
<path d="m16 15-3-3 3-3" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 401 B |
|
|
@ -0,0 +1,17 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-panel-left-open"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<rect width="18" height="18" x="3" y="3" rx="2" />
|
||||||
|
<path d="M9 3v18" />
|
||||||
|
<path d="m14 9 3 3-3 3" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 399 B |
|
|
@ -0,0 +1,16 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-panel-right"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<rect width="18" height="18" x="3" y="3" rx="2" />
|
||||||
|
<path d="M15 3v18" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 367 B |
|
|
@ -0,0 +1,15 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-paperclip"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="m16 6-8.414 8.586a2 2 0 0 0 2.829 2.829l8.414-8.586a4 4 0 1 0-5.657-5.657l-8.379 8.551a6 6 0 1 0 8.485 8.485l8.379-8.551" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 424 B |
|
|
@ -0,0 +1,16 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-pause"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<rect x="14" y="3" width="5" height="18" rx="1" />
|
||||||
|
<rect x="5" y="3" width="5" height="18" rx="1" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 389 B |
|
|
@ -0,0 +1,16 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-pencil"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" />
|
||||||
|
<path d="m15 5 4 4" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 454 B |
|
|
@ -0,0 +1,15 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-phone"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M13.832 16.568a1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 6.392 6.384" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 513 B |
|
|
@ -0,0 +1,16 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-pin"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M12 17v5" />
|
||||||
|
<path d="M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 524 B |
|
|
@ -0,0 +1,15 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-plane"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M17.8 19.2 16 11l3.5-3.5C21 6 21.5 4 21 3c-1-.5-3 0-4.5 1.5L13 8 4.8 6.2c-.5-.1-.9.1-1.1.5l-.3.5c-.2.5-.1 1 .3 1.3L9 12l-2 3H4l-1 1 3 2 2 3 1-1v-3l3-2 3.5 5.3c.3.4.8.5 1.3.3l.5-.2c.4-.3.6-.7.5-1.2z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 497 B |
|
|
@ -0,0 +1,15 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-play"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 381 B |
|
|
@ -0,0 +1,16 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-plus"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M5 12h14" />
|
||||||
|
<path d="M12 5v14" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 331 B |
|
|
@ -0,0 +1,18 @@
|
||||||
|
<!-- @license lucide-static v1.67.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-refresh-cw"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" />
|
||||||
|
<path d="M21 3v5h-5" />
|
||||||
|
<path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" />
|
||||||
|
<path d="M8 16H3v5" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 473 B |