AnyBot/MECHANICS.md

592 lines
50 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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).