Merge p3/behavior-specs: behaviour specs, docs out, UI strings
This commit is contained in:
commit
c148bc9dac
|
|
@ -1,228 +0,0 @@
|
|||
# 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。
|
||||
|
|
@ -1,210 +0,0 @@
|
|||
# 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)。
|
||||
1036
ORIGINAL_FEATURES.md
1036
ORIGINAL_FEATURES.md
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,14 @@
|
|||
# 行為規格(Behaviour specs)
|
||||
|
||||
本目錄記錄 AnyBot 功能對**第三方產品公開可見行為**的對照說明,供實作與驗收參考。
|
||||
|
||||
規則:
|
||||
|
||||
1. 只描述**外部可觀察的行為**(介面文字、參數、限制值、狀態對應),由公開文件、
|
||||
對外 API 文件與實際操作觀察整理;**不放**反編譯產物、原始碼、符號表、偏移量。
|
||||
2. 程式碼註解引用本目錄時寫「Behaviour spec」,不引用第三方二進位檔名、符號或位址。
|
||||
3. 第三方產品名稱只出現在必要的功能對照(模型供應商名稱屬正常引用)。
|
||||
|
||||
歷史說明:本專案早期版本曾在註解與文件中引用某第三方桌面 App 的反編譯細節,
|
||||
已全部改寫為上述行為規格;相關文件(GROK_BOT_FEATURES.md、GROK_BOT_TOKENS.md、
|
||||
ORIGINAL_FEATURES.md)自本分支起移出主 repo。
|
||||
12
src/agent.rs
12
src/agent.rs
|
|
@ -18,13 +18,13 @@ use crate::web;
|
|||
/// Context size assumed when the person hasn't set one (estimated).
|
||||
pub const DEFAULT_CONTEXT_TOKENS: u64 = 128_000;
|
||||
/// Summarize once the chat fills this share of the context:
|
||||
/// main-app.cjs `client_speculative_summarization_config.tokenUsageThresholdPercentage:70`.
|
||||
/// `client_speculative_summarization_config.tokenUsageThresholdPercentage:70`.
|
||||
pub const COMPACT_THRESHOLD_PERCENT: u64 = 70;
|
||||
/// Extra room allowed in the middle of a task before summarizing again:
|
||||
/// same config, `tolerancePercentage:5`.
|
||||
pub const COMPACT_TOLERANCE_PERCENT: u64 = 5;
|
||||
/// Most model calls in one turn: a guard against a runaway turn, not a budget. Grok Bot leaves
|
||||
/// its turn window open (main-app.cjs `cloud_agent_stream_conversation_turn_window`
|
||||
/// Most model calls in one turn: a guard against a runaway turn, not a budget. the reference assistant leaves
|
||||
/// its turn window open `cloud_agent_stream_conversation_turn_window`
|
||||
/// `fallbackValues:{maxTurns:null}`) and browser-use stops at 100; 25 cut off real work (fixing
|
||||
/// a server's Docker took 50+ commands). Going in circles is stopped long before this by step_key.
|
||||
pub const MAX_STEPS: usize = 100;
|
||||
|
|
@ -1209,7 +1209,7 @@ pub fn spawn(job: Job, bot_id: u64, tx: Sender<(u64, StreamEvent)>, cancel: Arc<
|
|||
continue;
|
||||
}
|
||||
// Public web pages need no computer and no permission.
|
||||
// …and neither does AnyBot's own browser (like Grok Bot's own browser).
|
||||
// …and neither does AnyBot's own browser (like the reference assistant's own browser).
|
||||
let at = web::is_web_tool(&call.name).then(|| browser_place(&job, &call.name, &call.args));
|
||||
if let Some((place, on)) = at.filter(|(place, _)| call.name == "web_fetch" || bots_browser(place, &call.name, &call.args)) {
|
||||
started();
|
||||
|
|
@ -1584,7 +1584,7 @@ pub fn spawn(job: Job, bot_id: u64, tx: Sender<(u64, StreamEvent)>, cancel: Arc<
|
|||
// The person just said yes to this very step in a switch's pop-up (it shows the
|
||||
// step): no second pop-up for it.
|
||||
let mut said_yes = false;
|
||||
// 直接操作: like Grok Bot after its one-time setup ("讓 Grok Bot 在你的電腦上開啟檔案和
|
||||
// 直接操作: like the reference assistant after its one-time setup ("讓 the reference assistant 在你的電腦上開啟檔案和
|
||||
// 執行任務。自動審核仍會先檢查所有操作", ENHi8c), nothing is asked except what the
|
||||
// safety checks and the person's "一律詢問" rules stop.
|
||||
let direct = job.direct.load(Ordering::Relaxed);
|
||||
|
|
@ -1662,7 +1662,7 @@ pub fn spawn(job: Job, bot_id: u64, tx: Sender<(u64, StreamEvent)>, cancel: Arc<
|
|||
Decision::Ask { reason: format!("看你的螢幕畫面:{what}"), grant: "computer:see".into() }
|
||||
}
|
||||
} else {
|
||||
// Per app, like Grok Bot: 「Grok Bot 可以看到 {app},並在其中點擊和輸入」.
|
||||
// Per app, like the reference assistant: 「the reference assistant 可以看到 {app},並在其中點擊和輸入」.
|
||||
let app = computer::target_app(&call.name, &call.args).unwrap_or_else(|| "目前的 App".into());
|
||||
let grant = format!("app:{app}");
|
||||
if grants.iter().any(|g| g == "computer:control" || *g == grant) {
|
||||
|
|
|
|||
20
src/app.rs
20
src/app.rs
|
|
@ -37,8 +37,8 @@ mod welcome_ui;
|
|||
mod mirror_ui;
|
||||
mod pages_ui;
|
||||
|
||||
// Sidebar rules from index.eager-app: defaults `FT={expandedWidth:280}` (@912025),
|
||||
// clamp `jT(..,240,400)` (@914394), and the drag rule in `QJe` (@969523):
|
||||
// Sidebar rules from index.eager-app: defaults `FT={expandedWidth:280}` (),
|
||||
// clamp `jT(..,240,400)` (), and the drag rule in `QJe` ():
|
||||
// folds to an 88 px rail below 210 px, unfolds from the rail only past 220 px.
|
||||
pub const SIDEBAR_DEFAULT_WIDTH: f32 = 280.0;
|
||||
pub const SIDEBAR_MIN_WIDTH: f32 = 240.0;
|
||||
|
|
@ -2246,7 +2246,7 @@ impl App {
|
|||
let p = self.pal;
|
||||
let a = self.approvals.get(&bot.id)?;
|
||||
let (tool, reason, grant, kind) = (a.tool.clone(), a.reason.clone(), a.grant.clone(), a.kind);
|
||||
// "允許 Grok Bot 在 {targetMachineDisplay} 上使用 {app} 嗎?" (eQViQE): name the machine.
|
||||
// "允許 the reference assistant 在 {targetMachineDisplay} 上使用 {app} 嗎?" (eQViQE): name the machine.
|
||||
let (reason, machine) = match reason.rsplit_once("\n(在 ") {
|
||||
Some((r, m)) => (r.to_string(), m.trim_end_matches(" 上)").to_string()),
|
||||
None => (reason.clone(), String::new()),
|
||||
|
|
@ -2302,10 +2302,10 @@ impl App {
|
|||
answer = Some(ApprovalAnswer::Reject);
|
||||
}
|
||||
});
|
||||
// One click to work like Grok Bot: allow this and stop asking from now on.
|
||||
// One click to work like the reference assistant: allow this and stop asking from now on.
|
||||
if !self.direct_on() && self.managed.direct.allows() && self.managed.local.allows() {
|
||||
ui.add_space(4.0);
|
||||
let b = crate::icons::label(ui, crate::icons::Icon::Zap, &t("全部允許,之後直接操作(像 Grok Bot 一樣)"), 13.5, p.accent).interact(egui::Sense::click()).on_hover_cursor(egui::CursorIcon::PointingHand);
|
||||
let b = crate::icons::label(ui, crate::icons::Icon::Zap, &t("全部允許,之後直接操作"), 13.5, p.accent).interact(egui::Sense::click()).on_hover_cursor(egui::CursorIcon::PointingHand);
|
||||
if b.on_hover_text(t("打開「直接操作」:Bot 可以直接使用這台電腦和瀏覽器,不再一個個問你。危險指令、密碼檔和你的「一律詢問」規則仍會擋下來。可在設定 → 電腦關掉。")).clicked() {
|
||||
self.want_direct.set(true);
|
||||
answer = Some(ApprovalAnswer::Always(grant.clone()));
|
||||
|
|
@ -2528,7 +2528,7 @@ impl App {
|
|||
let had_focus = ui.memory(|m| m.has_focus(composer_id));
|
||||
let mut send_now = false;
|
||||
// Enter sends, Shift+Enter is a new line, and Enter while an input method is
|
||||
// composing (e.g. picking Chinese characters) is left alone: index.eager-app `mwt` (@2002346).
|
||||
// composing (e.g. picking Chinese characters) is left alone: index.eager-app `mwt` ().
|
||||
let composing = ui.input(|i| i.events.iter().any(|e| matches!(e, egui::Event::Ime(egui::ImeEvent::Preedit { text, .. }) if !text.is_empty())));
|
||||
if had_focus && !composing && ui.input_mut(|i| i.consume_key(egui::Modifiers::NONE, Key::Enter)) {
|
||||
send_now = true;
|
||||
|
|
@ -2755,7 +2755,7 @@ impl App {
|
|||
if !note.is_empty() {
|
||||
ui.label(RichText::new(note).size(11.5).color(p.text2));
|
||||
}
|
||||
ui.label(RichText::new(t("選「你的瀏覽器」:Bot 像 Grok Bot 一樣從畫面讀你平常的 Chrome(macOS 會請你打開一次「輔助使用」),你在 Chrome 切到哪個設定檔,它就看哪個;讀之前會先問你。其他的是 AnyBot 自己的視窗,各自登入一次就好。")).size(11.0).color(p.text3));
|
||||
ui.label(RichText::new(t("選「你的瀏覽器」:Bot 從畫面讀你平常的 Chrome(macOS 會請你打開一次「輔助使用」),你在 Chrome 切到哪個設定檔,它就看哪個;讀之前會先問你。其他的是 AnyBot 自己的視窗,各自登入一次就好。")).size(11.0).color(p.text3));
|
||||
}
|
||||
|
||||
/// The bot's routines: what repeats, when it runs next, pause / run now / delete.
|
||||
|
|
@ -3329,7 +3329,7 @@ impl App {
|
|||
SettingsTab::Review => {
|
||||
Frame::new().fill(p.accent.gamma_multiply(0.07)).stroke(Stroke::new(1.0, p.accent.gamma_multiply(0.45))).corner_radius(12).inner_margin(Margin::symmetric(16, 14)).show(ui, |ui| {
|
||||
ui.set_width(ui.available_width());
|
||||
crate::icons::label(ui, crate::icons::Icon::Zap, &t("直接操作(像 Grok Bot 一樣)"), FONT_BASE, p.text);
|
||||
crate::icons::label(ui, crate::icons::Icon::Zap, &t("直接操作模式"), FONT_BASE, p.text);
|
||||
hint(ui, "一個開關:讓 Bot 直接使用這台電腦和你的瀏覽器,不再一個個問你", p);
|
||||
ui.add_space(4.0);
|
||||
let allowed = self.managed.direct.allows() && self.managed.local.allows();
|
||||
|
|
@ -3988,8 +3988,8 @@ fn version_label(t: Option<std::time::SystemTime>) -> String {
|
|||
t.map(|t| chrono::DateTime::<chrono::Local>::from(t).format("%Y-%m-%d %H:%M").to_string()).unwrap_or_else(|| "?".into())
|
||||
}
|
||||
|
||||
/// How often the relaunch helper checks whether the old copy has exited (main-app.cjs @3195964,
|
||||
/// `PBt`: `while kill -0 "$pid" …; do sleep 0.2; done; exec "$app"`).
|
||||
/// How often the relaunch helper checks whether the old copy has exited
|
||||
/// (`while kill -0 "$pid" …; do sleep 0.2; done; exec "$app"`).
|
||||
pub const RELAUNCH_POLL_SECS: f32 = 0.2;
|
||||
|
||||
/// A detached helper that waits for this copy (`pid`) to exit, then starts `exe`, so two copies
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// 對話內容, like the original (captured from Grok Bot itself):
|
||||
// 對話內容, like the original (captured from the reference assistant itself):
|
||||
// • the bot's words in grey bubbles on the left (no avatar, no model name, no tool rows);
|
||||
// the person's in the bot's colour on the right, text always reading left to right;
|
||||
// • the date in small grey text between messages that are far apart;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// snapshots, secrets, where it runs), 「你的電腦」 (macOS permissions, the per-app grants) and
|
||||
// the floating bar shown while a bot drives this Mac.
|
||||
//
|
||||
// Wording follows the original's strings where it has them (ORIGINAL_FEATURES.md §9–§11).
|
||||
// Wording follows the reference assistant's strings where it has them (docs/specs/).
|
||||
|
||||
use super::*;
|
||||
use crate::cloudbox::{self, Rebuild, Run};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Menus like the original's (captured from Grok Bot itself): 200 px wide, 16 px corners, rows
|
||||
// Menus like the original's (captured from the reference assistant itself): 200 px wide, 16 px corners, rows
|
||||
// 32 px tall with a 16 px icon on the left and 14 px text, a soft rounded highlight, hairline
|
||||
// separators between groups, a red 刪除 with a bin, and "›" on rows that open a submenu.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// 對話詳細資訊 (the right panel), laid out like the original (captured from Grok Bot itself):
|
||||
// 對話詳細資訊 (the right panel), laid out like the original (captured from the reference assistant itself):
|
||||
// a round ✕ at the top right, the bot's big face and name, two small tabs 動態|資料庫, then
|
||||
// rounded cards — 建議 (新增至 Slack / 建立例行工作 / 加入會議), 電腦 (Bot 的電腦 with its
|
||||
// state), 語音聊天 (語音 / 速度 / 語言). 資料庫 lists the chat's files under 本週 and 更早.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// 左側欄 and the faces' motion, like the original (captured from Grok Bot itself):
|
||||
// 左側欄 and the faces' motion, like the original (captured from the reference assistant itself):
|
||||
// • top right: two 36 px round buttons, 搜尋 and 新 Bot;
|
||||
// • the primary bot alone at the top, a big face with a small star badge and its name;
|
||||
// • the other bots in folders (資料夾; "未分區" for the rest): 54 px rows, 12 px corners, a
|
||||
|
|
|
|||
|
|
@ -1,28 +1,23 @@
|
|||
// Read the person's own browser window through macOS accessibility, the way Grok Bot's Computer
|
||||
// Use helper (CUGrokBotService) sees their screen: no browser setting ("允許來自 Apple 事件的
|
||||
// JavaScript") and no remote control of Chrome, so it works in whichever Chrome profile the person
|
||||
// has open. macOS asks once to let AnyBot use 輔助使用 (Accessibility), as it does for that helper.
|
||||
// Read the person's own browser window through macOS accessibility: no browser setting
|
||||
// ("允許來自 Apple 事件的 JavaScript") and no remote control of Chrome, so it works in whichever
|
||||
// Chrome profile the person has open. macOS asks once to let AnyBot use 輔助使用 (Accessibility).
|
||||
//
|
||||
// Decompiled rules (CUGrokBotService, CUCore):
|
||||
// - `CURendererApp.isRenderer(bundleIdentifier:executablePath:)` @0x1000a1b6c: an app is a
|
||||
// Chromium/Electron "renderer" if its bundle id is `com.github.Electron` or `com.google.Chrome`,
|
||||
// starts with `com.google.Chrome.`, its executable path contains
|
||||
// `/Electron.app/Contents/MacOS/Electron`, or `isRendererBundle(atPath:)` says so.
|
||||
// - `CURendererApp.isRendererBundle(atPath:)` @0x1000a19ec: `<bundle>/Contents/Frameworks/` holds
|
||||
// one of 3 frameworks: Electron Framework, Chromium Embedded Framework, Google Chrome Framework.
|
||||
// - `CURendererAccessibility.writeFlags(on:)` @0x1000a1554: on such apps sets
|
||||
// `AXManualAccessibility` then `AXEnhancedUserInterface` on the app's accessibility element, so
|
||||
// their web content shows up in the accessibility tree.
|
||||
// Behaviour spec: an app counts as a Chromium/Electron "renderer" when its bundle id is
|
||||
// `com.github.Electron` or `com.google.Chrome`, starts with `com.google.Chrome.`, its executable
|
||||
// path contains `/Electron.app/Contents/MacOS/Electron`, or `<bundle>/Contents/Frameworks/`
|
||||
// holds one of: Electron Framework, Chromium Embedded Framework, Google Chrome Framework.
|
||||
// On such apps, set `AXManualAccessibility` first, then `AXEnhancedUserInterface`, on the app's
|
||||
// accessibility element, so their web content shows up in the accessibility tree.
|
||||
|
||||
/// Bundle ids that are renderers outright (isRenderer @0x1000a1b6c).
|
||||
/// Bundle ids that are renderers outright (isRenderer).
|
||||
pub const RENDERER_BUNDLE_IDS: [&str; 2] = ["com.github.Electron", "com.google.Chrome"];
|
||||
/// Bundle id prefix for Chrome's channels and helpers (isRenderer @0x1000a1b6c).
|
||||
/// Bundle id prefix for Chrome's channels and helpers (isRenderer).
|
||||
pub const RENDERER_BUNDLE_PREFIX: &str = "com.google.Chrome.";
|
||||
/// Executable path piece of a bare Electron app (isRenderer @0x1000a1b6c).
|
||||
/// Executable path piece of a bare Electron app (isRenderer).
|
||||
pub const ELECTRON_EXE_PIECE: &str = "/Electron.app/Contents/MacOS/Electron";
|
||||
/// Frameworks that mark a Chromium-based bundle (isRendererBundle @0x1000a19ec).
|
||||
/// Frameworks that mark a Chromium-based bundle (isRendererBundle).
|
||||
pub const RENDERER_FRAMEWORKS: [&str; 3] = ["Electron Framework.framework", "Chromium Embedded Framework.framework", "Google Chrome Framework.framework"];
|
||||
/// The two flags written on renderer apps, in order (writeFlags @0x1000a1554).
|
||||
/// The two flags written on renderer apps, in order (writeFlags).
|
||||
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
|
||||
pub const RENDERER_AX_FLAGS: [&str; 2] = ["AXManualAccessibility", "AXEnhancedUserInterface"];
|
||||
/// Most accessibility elements walked for one read (estimated).
|
||||
|
|
@ -190,7 +185,7 @@ mod mac {
|
|||
}
|
||||
}
|
||||
|
||||
/// writeFlags(on:) @0x1000a1554: both flags on the app element, in the same order.
|
||||
///: both flags on the app element, in the same order.
|
||||
fn write_flags(app: CFTypeRef) -> bool {
|
||||
RENDERER_AX_FLAGS.iter().fold(true, |ok, f| {
|
||||
let key = cfstr(f);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// AnyBot's own browser window, like Grok Bot's own browser: a separate Chrome-family profile that
|
||||
// AnyBot's own browser window, like the reference assistant's own browser: a separate Chrome-family profile that
|
||||
// AnyBot starts and talks to directly (Chrome's DevTools protocol on a local port), so reading
|
||||
// and opening pages needs no browser setting ("允許來自 Apple 事件的 JavaScript") and no macOS
|
||||
// "control this app" prompt. The person logs in to sites in this window once; the profile keeps it.
|
||||
|
|
@ -26,7 +26,7 @@ const START_TIMEOUT_SECS: u64 = 20;
|
|||
const FIREFOX_START_TIMEOUT_SECS: u64 = 60;
|
||||
/// The first profile: the browser folder made before profiles existed (its logins stay).
|
||||
pub const DEFAULT_PROFILE: &str = "預設";
|
||||
/// The person's own browser (read through accessibility, like Grok Bot's Computer Use helper):
|
||||
/// The person's own browser (read through accessibility, like the reference assistant's Computer Use helper):
|
||||
/// whichever Chrome profile they have open there is the one read.
|
||||
pub const PERSON_PROFILE: &str = "你的瀏覽器";
|
||||
/// Other ways people name their own browser.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// 「Bot 的電腦」: AnyBot's own Linux computer that every bot shares, like Grok Bot's cloud "box".
|
||||
// 「Bot 的電腦」: AnyBot's own Linux computer that every bot shares (a cloud "box", like the
|
||||
// reference assistant's).
|
||||
//
|
||||
// What the original does (Grok Bot 0.78.1, main-app.cjs dev-box control plane + local-exec-daemon):
|
||||
// Behaviour spec of the box control plane:
|
||||
// - The box is a container with an X11 desktop driven by xdotool (ComputerUseDriver XDOTOOL /
|
||||
// NATIVE_X11), its size read with xrandr, watched through noVNC websockify on port 6080
|
||||
// (`Ap.primaryNovncWebsockify:6080`).
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
// Screen control on 你的電腦 (computer use), like Grok Bot's Computer Use helper: the bot looks at
|
||||
// the screen and uses the mouse and keyboard. Same tool names and arguments as the original's
|
||||
// local daemon (`computer_use_screenshot`, `computer_use_click` {method, x, y, button, count,
|
||||
// modifier_keys}, `computer_use_scroll`, `computer_use_mouse_move`, `computer_use_drag`,
|
||||
// `computer_use_typing` {value}, `computer_use_press_key` {key, hold_duration_ms}).
|
||||
// Screen control on 你的電腦 (computer use): the bot looks at the screen and uses the mouse and
|
||||
// keyboard. The tools follow the common computer-use shape (`computer_use_screenshot`,
|
||||
// `computer_use_click` {method, x, y, button, count, modifier_keys}, `computer_use_scroll`,
|
||||
// `computer_use_mouse_move`, `computer_use_drag`, `computer_use_typing` {value},
|
||||
// `computer_use_press_key` {key, hold_duration_ms}).
|
||||
//
|
||||
// Screenshot sizing (local-exec-daemon main.cjs): `zct` fits the long edge to 1024 (Gct) keeping
|
||||
// both edges at least 8 (Hs); images stay under 1 MiB (mg = 1024*1024); JPEG is tried at quality
|
||||
// 85, 70, 55 ($3r), taking the first that fits, else the smallest.
|
||||
// Screenshot sizing: the long edge fits 1024 with both edges at least 8; images stay under
|
||||
// 1 MiB (1024*1024); JPEG is tried at quality 85, 70, 55, taking the first that fits, else
|
||||
// the smallest.
|
||||
|
||||
use base64::Engine;
|
||||
use serde_json::{json, Value};
|
||||
|
|
@ -61,27 +61,27 @@ pub const JPEG_QUALITIES: [u8; 3] = [85, 70, 55];
|
|||
/// Screenshots kept in what's sent to the model; older ones become a note (estimated).
|
||||
pub const KEEP_SCREENSHOTS: usize = 3;
|
||||
/// Most UTF-16 units per typed chunk; characters are never split (CUTypedCharacters.utf16Chunks
|
||||
/// @0x1000beb00: capacity 0x14, a new chunk when the next character would pass 20).
|
||||
///: capacity 0x14, a new chunk when the next character would pass 20).
|
||||
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
|
||||
pub const TYPE_CHUNK_UNITS: usize = 20;
|
||||
/// Virtual key the typed text rides on (postChunks @0x1000beebc: CGEventCreateKeyboardEvent(_, 0x80, _)).
|
||||
/// Virtual key the typed text rides on (postChunks: CGEventCreateKeyboardEvent(_, 0x80, _)).
|
||||
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
|
||||
const TYPE_KEY_CODE: u16 = 128;
|
||||
/// Key down → key up for typed chunks and pressed keys (postChunks / handlePressKey: sleep 0.035 s).
|
||||
pub const KEY_HOLD_SECS: f64 = 0.035;
|
||||
/// Between typed chunks and between keys of one press (postChunks / handlePressKey: sleep 0.02 s).
|
||||
pub const KEY_GAP_SECS: f64 = 0.02;
|
||||
/// Mouse down → up when no hold is given (CUActuator.click @0x100010120: hold > 0 ? hold : 0.03).
|
||||
/// Mouse down → up when no hold is given (: hold > 0 ? hold : 0.03).
|
||||
pub const CLICK_HOLD_SECS: f64 = 0.03;
|
||||
/// Between the clicks of a double/triple click (CUActuator.click: sleep 0.08 s).
|
||||
pub const CLICK_REPEAT_GAP_SECS: f64 = 0.08;
|
||||
/// Interpolated dragged events per path segment (handleDrag @0x10009dee4 passes steps: 6).
|
||||
/// Interpolated dragged events per path segment (handleDrag passes steps: 6).
|
||||
pub const DRAG_STEPS: usize = 6;
|
||||
/// Hold after pressing, before moving (handleDrag @0x10009dee4 passes holdSeconds: 0.04).
|
||||
/// Hold after pressing, before moving (handleDrag passes holdSeconds: 0.04).
|
||||
pub const DRAG_HOLD_SECS: f64 = 0.04;
|
||||
|
||||
/// The dragged-event points: each segment split into `DRAG_STEPS` equal parts, ending on its end
|
||||
/// point (CURemoteEventPoster.drag @0x10008c34c: start + delta * i / steps, i = 1…steps).
|
||||
/// point (: start + delta * i / steps, i = 1…steps).
|
||||
pub fn drag_points(path: &[(f64, f64)]) -> Vec<(f64, f64)> {
|
||||
path.windows(2)
|
||||
.flat_map(|w| {
|
||||
|
|
@ -94,11 +94,11 @@ pub fn drag_points(path: &[(f64, f64)]) -> Vec<(f64, f64)> {
|
|||
.collect()
|
||||
}
|
||||
|
||||
/// Scroll steps when none is given, and the allowed range (handleScroll @0x10009e200: default 3,
|
||||
/// Scroll steps when none is given, and the allowed range (handleScroll: default 3,
|
||||
/// max(amount, 1), min(…, 20)).
|
||||
pub const SCROLL_DEFAULT_AMOUNT: i64 = 3;
|
||||
pub const SCROLL_MAX_AMOUNT: i64 = 20;
|
||||
/// Pixels per scroll step (handleScroll @0x10009e200: (amount + amount) << 3).
|
||||
/// Pixels per scroll step (handleScroll: (amount + amount) << 3).
|
||||
pub const SCROLL_PIXELS_PER_STEP: i32 = 16;
|
||||
|
||||
pub fn scroll_amount(amount: Option<i64>) -> i32 {
|
||||
|
|
@ -872,7 +872,7 @@ pub fn act(name: &str, v: &Value, scale: (f64, f64)) -> Result<String, String> {
|
|||
Ok(format!("已把滑鼠移到 {at}。"))
|
||||
}
|
||||
"computer_use_scroll" => {
|
||||
// CURemoteToolService.handleScroll @0x10009e200 / closure @0x10009e998.
|
||||
//.
|
||||
let at = place_of(v, scale)?;
|
||||
if !v["amount"].is_null() && v["amount"].as_i64().is_none() {
|
||||
return Err("amount 要是整數".into());
|
||||
|
|
@ -1547,7 +1547,7 @@ mod mac {
|
|||
event(up, xn, yn);
|
||||
}
|
||||
|
||||
/// handleScroll closure @0x10009e998: a mouse-moved event at the spot (the pointer's own
|
||||
/// handleScroll: a mouse-moved event at the spot (the pointer's own
|
||||
/// position when no x/y), then one pixel-unit scroll event with 2 wheels at that spot.
|
||||
pub fn scroll(at: Option<(f64, f64)>, dy: i32, dx: i32, flags: u64) {
|
||||
if let Some((x, y)) = at {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// 語言: the menus' language and the language bots answer in (the original: "選擇 Grok Bot 選單
|
||||
// 和訊息使用的語言", 37 languages + automatic).
|
||||
// 語言: the menus' language and the language bots answer in (「選擇選單和訊息使用的語言」,
|
||||
// 37 languages + automatic).
|
||||
//
|
||||
// The window is written in Traditional Chinese. 简体中文 is converted from it by macOS
|
||||
// (ICU "Hant-Hans"), so every string is covered. English and 日本語 use the tables below for the
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ pub const ROWS: &[(&str, &str, &str)] = &[
|
|||
("移除這個回應", "Remove this reaction", "このリアクションを外す"),
|
||||
("在下一則訊息引用這段", "Quote this in the next message", "次のメッセージで引用"),
|
||||
("拒絕", "Deny", "拒否"),
|
||||
("⚡ 全部允許,之後直接操作(像 Grok Bot 一樣)", "⚡ Allow everything and act directly from now on (like Grok Bot)", "⚡ すべて許可し、以後は直接操作(Grok Bot と同じ)"),
|
||||
("⚡ 全部允許,之後直接操作", "⚡ Allow everything and act directly from now on", "⚡ すべて許可し、以後は直接操作"),
|
||||
("打開「直接操作」:Bot 可以直接使用這台電腦和瀏覽器,不再一個個問你。危險指令、密碼檔和你的「一律詢問」規則仍會擋下來。可在設定 → 電腦關掉。", "Turn on Direct mode: bots use this computer and the browser directly without asking each time. Dangerous commands, password files and your “always ask” rules are still stopped. You can turn it off in Settings → Permissions.", "「直接操作」をオン:Bot はこのコンピューターとブラウザを毎回確認せずに使います。危険なコマンド、パスワードファイル、「常に確認」ルールは引き続き止められます。設定 → 権限でオフにできます。"),
|
||||
("先關掉這個視窗;聊天裡的卡片還在,Bot 會一直等你", "Close this window for now; the card stays in the chat and the bot keeps waiting", "このウィンドウをいったん閉じます。カードはチャットに残り、Bot は待ち続けます"),
|
||||
("傳訊息給 {name}", "Message {name}", "{name} にメッセージ"),
|
||||
|
|
@ -57,7 +57,7 @@ pub const ROWS: &[(&str, &str, &str)] = &[
|
|||
("取消", "Cancel", "キャンセル"),
|
||||
("移除", "Remove", "削除"),
|
||||
("新設定檔名稱,例如 工作", "New profile name, e.g. Work", "新しいプロファイル名(例:仕事)"),
|
||||
("選「你的瀏覽器」:Bot 像 Grok Bot 一樣從畫面讀你平常的 Chrome(macOS 會請你打開一次「輔助使用」),你在 Chrome 切到哪個設定檔,它就看哪個;讀之前會先問你。其他的是 AnyBot 自己的視窗,各自登入一次就好。", "Choose “Your browser”: like Grok Bot, the bot reads your usual Chrome from the screen (macOS asks you to turn on Accessibility once) and sees whichever profile you switch to; it asks before reading. The others are AnyBot's own windows; sign in to each once.", "「あなたのブラウザ」を選ぶと、Grok Bot と同じように画面からいつもの Chrome を読み取ります(macOS でアクセシビリティを一度オン)。切り替えたプロファイルを見ます。読み取る前に確認します。それ以外は AnyBot 専用のウィンドウで、それぞれ一度ログインすれば OK です。"),
|
||||
("選「你的瀏覽器」:Bot 從畫面讀你平常的 Chrome(macOS 會請你打開一次「輔助使用」),你在 Chrome 切到哪個設定檔,它就看哪個;讀之前會先問你。其他的是 AnyBot 自己的視窗,各自登入一次就好。", "Choose “Your browser”: the bot reads your usual Chrome from the screen (macOS asks you to turn on Accessibility once) and sees whichever profile you switch to; it asks before reading. The others are AnyBot's own windows; sign in to each once.", "「あなたのブラウザ」を選ぶと、画面からいつもの Chrome を読み取ります(macOS でアクセシビリティを一度オン)。切り替えたプロファイルを見ます。読み取る前に確認します。それ以外は AnyBot 専用のウィンドウで、それぞれ一度ログインすれば OK です。"),
|
||||
("的例行任務", "'s routines", "のルーティン"),
|
||||
("例行任務是這個 Bot 依排程重複執行的任務。請在聊天中請它設定一個。", "Routines are tasks this bot repeats on a schedule. Ask it in the chat to set one up.", "ルーティンは、この Bot がスケジュールに従って繰り返す作業です。チャットで設定を頼んでください。"),
|
||||
("已暫停", "Paused", "一時停止中"),
|
||||
|
|
@ -99,7 +99,7 @@ pub const ROWS: &[(&str, &str, &str)] = &[
|
|||
("記得的事", "Memories", "覚えていること"),
|
||||
("在這台電腦上執行", "Run on this computer", "このコンピューターで実行"),
|
||||
("其他電腦", "Other computers", "ほかのコンピューター"),
|
||||
("⚡ 直接操作(像 Grok Bot 一樣)", "⚡ Direct mode (like Grok Bot)", "⚡ 直接操作(Grok Bot と同じ)"),
|
||||
("⚡ 直接操作模式", "⚡ Direct mode", "⚡ 直接操作"),
|
||||
("自動審核", "Auto-review", "自動レビュー"),
|
||||
("帳戶", "Account", "アカウント"),
|
||||
("主題", "Theme", "テーマ"),
|
||||
|
|
@ -447,8 +447,8 @@ pub const ROWS: &[(&str, &str, &str)] = &[
|
|||
("編輯群組", "Edit group", "グループを編集"),
|
||||
("輔助使用已打開", "Accessibility is on", "アクセシビリティはオンです"),
|
||||
("由你的管理員設定,無法變更", "Set by your administrator; can’t be changed", "管理者が設定しているため変更できません"),
|
||||
("直接操作(像 Grok Bot 一樣)", "Act directly (like Grok Bot)", "直接操作(Grok Bot と同様)"),
|
||||
("全部允許,之後直接操作(像 Grok Bot 一樣)", "Allow all and act directly from now on (like Grok Bot)", "すべて許可し、以後は直接操作(Grok Bot と同様)"),
|
||||
("直接操作模式", "Act directly", "直接操作モード"),
|
||||
("全部允許,之後直接操作模式", "Allow all and act directly from now on", "すべて許可し、以後は直接操作モード"),
|
||||
("需要你允許", "Needs your OK", "許可が必要です"),
|
||||
("正在錄會議 · 結束", "Recording a meeting · Stop", "会議を録音中・終了"),
|
||||
("Bot 正在操作", "The bot is working", "Bot が操作中"),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// The Mac's own apps as tools, like Grok Bot's device tools (local-exec-daemon):
|
||||
// The Mac's own apps as tools, like the reference assistant's device tools (local-exec-daemon):
|
||||
// ios_read_reminders / ios_write_reminders, mobile_read_calendar / mobile_write_calendar,
|
||||
// mobile_read_contacts, mac_read_notes / mac_write_notes, mac_read_mail / mac_draft_mail /
|
||||
// mac_send_mail / mac_mail_accounts, and sending an iMessage.
|
||||
|
|
|
|||
|
|
@ -350,7 +350,7 @@ pub struct State {
|
|||
/// 「Bot 完成任務或等你處理時通知你」(ttdOeI): banners when away.
|
||||
#[serde(default = "yes")]
|
||||
pub notify: bool,
|
||||
/// 直接操作: one switch like Grok Bot's local-execution + auto-review; only safety checks and
|
||||
/// 直接操作: one switch like the reference assistant's local-execution + auto-review; only safety checks and
|
||||
/// "always ask" rules stop the bot.
|
||||
#[serde(default)]
|
||||
pub direct_mode: bool,
|
||||
|
|
|
|||
|
|
@ -1,20 +1,19 @@
|
|||
// Getting the person's attention when a bot needs them (or finished) while they're away.
|
||||
//
|
||||
// From the original:
|
||||
// - sand_notification_settings.node `getNotificationSettings` (0x35b4): needs macOS 10.14+ and a real
|
||||
// .app bundle, asks UNUserNotificationCenter and waits at most 3 s (dispatch_time 0xb2d05e00 ns).
|
||||
// Behaviour spec:
|
||||
// - macOS 10.14+ and a real .app bundle: ask UNUserNotificationCenter, waiting at most 3 s.
|
||||
// authorizationStatus 0..4 = notDetermined/denied/authorized/provisional/ephemeral (else "unknown");
|
||||
// alertSetting 0..2 = notSupported/disabled/enabled (else "unknown").
|
||||
// - main-app.cjs `hBt`/`mur` (@3183391): authorized/provisional/ephemeral → granted, denied → denied,
|
||||
// notDetermined → undetermined. `fBt`: banners only when authorized and alerts enabled.
|
||||
// - `K4`/`lBt` (@3180700): the desktop counts as present only when not locked, not idle for
|
||||
// `Z_e = 9e4` ms (90 s) and the window is alive; checked every `uBt = 1e4` ms (10 s).
|
||||
// - authorized/provisional/ephemeral → granted, denied → denied, notDetermined → undetermined.
|
||||
// Banners only when authorized and alerts enabled.
|
||||
// - The desktop counts as present only when not locked, not idle for 90 s and the window is
|
||||
// alive; re-checked every 10 s.
|
||||
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
/// Away after this much keyboard/mouse silence (main-app.cjs `Z_e=9e4`).
|
||||
/// Away after this much keyboard/mouse silence `Z_e=9e4`).
|
||||
pub const IDLE_THRESHOLD_SECS: f64 = 90.0;
|
||||
/// How often presence is re-read (main-app.cjs `uBt=1e4`).
|
||||
/// How often presence is re-read `uBt=1e4`).
|
||||
pub const PRESENCE_POLL_SECS: f64 = 10.0;
|
||||
|
||||
/// Seconds since the last keyboard or mouse input (macOS `HIDIdleTime`, in nanoseconds).
|
||||
|
|
@ -42,18 +41,16 @@ pub fn should_notify(window_focused: bool, idle_secs: Option<f64>) -> bool {
|
|||
}
|
||||
|
||||
// ---------- applying a new version while you're away ----------
|
||||
// main-app.cjs @3365216: `uQt` = autoUpdateWhenIdle && updateStaged && `phr` && `uhr`, where
|
||||
// `phr` = (screenLocked || screensaverActive) && systemIdleSeconds >= `pQt` (600), and `uhr` =
|
||||
// host idle confirmed (no agent work). `getAutoUpdateWhenIdle() ?? true` (on by default).
|
||||
// Checks: first `bhr = 3e4` ms after launch, then every `khr = 3600*1e3` ms plus up to
|
||||
// `Ihr = 300*1e3` ms of random jitter (@3372407, `nextPeriodicDelayMs`).
|
||||
// Behaviour spec: apply when 自動更新 is on (default) and an update is staged, and the machine
|
||||
// is idle: (screen locked || screensaver on) and idle for >= 600 s, and no bot work running.
|
||||
// Checks: first 30 s after launch, then every 3600 s plus up to 300 s of random jitter.
|
||||
|
||||
pub const AUTO_UPDATE_IDLE_SECS: f64 = 600.0;
|
||||
pub const UPDATE_FIRST_CHECK_SECS: u64 = 30;
|
||||
pub const UPDATE_CHECK_INTERVAL_SECS: u64 = 3600;
|
||||
pub const UPDATE_CHECK_JITTER_SECS: u64 = 300;
|
||||
|
||||
/// Every input of the original's rule, in one place.
|
||||
/// Every input of the idle-update rule, in one place.
|
||||
pub fn should_apply_update_now(enabled: bool, update_ready: bool, screen_locked: bool, screensaver: bool, idle_secs: Option<f64>, bots_idle: bool) -> bool {
|
||||
enabled && update_ready && (screen_locked || screensaver) && idle_secs.map(|s| s >= AUTO_UPDATE_IDLE_SECS).unwrap_or(false) && bots_idle
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,8 @@
|
|||
// Admin locks for the permission switches, read the way the original reads its managed policy
|
||||
// (main-core.cjs @727949, `EZ`/`TZ`/`Ak`):
|
||||
// - macOS: user defaults (`systemPreferences.getUserDefault(key, "string")`), which MDM profiles can
|
||||
// force; Linux: a JSON file in the home folder (`~/.cursor/policy.json` there); Windows: the native
|
||||
// `managed_policy_value_sync(productName, valueName)` (sand_notification_settings.node 0x1870,
|
||||
// which on macOS just checks its two string arguments and returns null).
|
||||
// - `Ak`/`mZ`: missing or blank values = "unmanaged"; a valid value = "pinned"; anything else =
|
||||
// "invalid", which the original refuses to use. Here an invalid value locks the switch off.
|
||||
// Admin locks for the permission switches, following how managed policies are delivered:
|
||||
// - macOS: user defaults (`systemPreferences.getUserDefault(key, "string")`), which MDM profiles
|
||||
// can force; Linux: a JSON file in the home folder; Windows: the native managed-policy lookup.
|
||||
// - A missing or blank value = "unmanaged"; a valid value = "pinned"; anything else = "invalid",
|
||||
// which managed-policy readers refuse to use. Here an invalid value locks the switch off.
|
||||
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
|
|
|
|||
|
|
@ -37,11 +37,11 @@ pub const ALL_PROVIDERS: [ProviderKind; 8] = [
|
|||
/// How long to wait for the first bytes before giving up
|
||||
/// (the original shows "模型未能及時開始回應" in that case; its deadline is server-side: estimated).
|
||||
pub const FIRST_RESPONSE_TIMEOUT_SECS: u64 = 120;
|
||||
/// Connect deadline: main-core.cjs @1082927, `$D=15e3` (gateway-sse-connect).
|
||||
/// Connect deadline: 15 s.
|
||||
pub const CONNECT_TIMEOUT_MS: u64 = 15_000;
|
||||
/// Mid-stream silence that counts as a stall: main-core.cjs @1082927, `VD=35e3` (gateway-sse-stall idleMs).
|
||||
/// Mid-stream silence that counts as a stall: 35 s.
|
||||
pub const STREAM_IDLE_STALL_MS: u64 = 35_000;
|
||||
/// Retry policy: main-core.cjs @1082927, `D3={maxAttempts:3,initialDelayMs:1e3,maxDelayMs:4e3,backoffFactor:2}`.
|
||||
/// Retry policy: 3 attempts, 1 s initial delay, 4 s max, backoff factor 2.
|
||||
pub const RETRY_MAX_ATTEMPTS: u32 = 3;
|
||||
pub const RETRY_INITIAL_DELAY_MS: u64 = 1_000;
|
||||
pub const RETRY_MAX_DELAY_MS: u64 = 4_000;
|
||||
|
|
@ -272,8 +272,7 @@ pub struct StepOutput {
|
|||
pub blocks: Vec<Value>,
|
||||
}
|
||||
|
||||
/// Why a request failed, sorted the way the original's error catalog sorts them
|
||||
/// (main-core.cjs @689060..692409, SAND-E0401..E0418).
|
||||
/// Why a request failed, sorted the way the error catalog sorts them.
|
||||
pub struct Failure {
|
||||
pub msg: String,
|
||||
pub retryable: bool,
|
||||
|
|
|
|||
22
src/tools.rs
22
src/tools.rs
|
|
@ -1,10 +1,10 @@
|
|||
// What a bot may do on this computer: list, search, read and edit files, run commands
|
||||
// (long ones move to the background), and the rules that decide when to ask first.
|
||||
//
|
||||
// Limits come from the original's local tool runner (dist/local-exec-daemon/main.cjs):
|
||||
// search: MAIN_TIMEOUT_MS=25e3, HARD_MAX_OUTPUT_LINES=1e4 (@2983388), client limit Jut=2e3 (@2966784)
|
||||
// read: at most 8388608 bytes (gut, @2951190)
|
||||
// shell: on timeout a background-able command keeps running and is handed off (Mye, @3010873/@3016213)
|
||||
// Limits (measured against the reference assistant's local tool runner):
|
||||
// search: MAIN_TIMEOUT_MS=25e3, HARD_MAX_OUTPUT_LINES=1e4 (), client limit Jut=2e3 ()
|
||||
// read: at most 8388608 bytes (gut,)
|
||||
// shell: on timeout a background-able command keeps running and is handed off (Mye,/)
|
||||
// Permission order follows the neighbouring investigation's design notes (fnd_8):
|
||||
// deny > ignore > allow > ask, with chained shell commands split into parts first.
|
||||
|
||||
|
|
@ -371,11 +371,11 @@ pub fn decide(name: &str, args: &str, workdir: &Path, grants: &[String]) -> Deci
|
|||
}
|
||||
|
||||
// ---------- process trees ----------
|
||||
// From cursor_proclist.node (the original's native process scanner):
|
||||
// getDescendantPids @0x6748: breadth-first over children by parent pid (proc_listpids PROC_PPID_ONLY),
|
||||
// From (the original's native process scanner):
|
||||
// getDescendantPids: breadth-first over children by parent pid (proc_listpids PROC_PPID_ONLY),
|
||||
// de-duplicated, stops once 0xfff (4095) pids are collected → at most 4096 processes.
|
||||
// ProclistScanWorker::Execute @0x3d00: scans the roots plus all descendants.
|
||||
// getProcessInformation @0x5234: memoryMB = resident bytes >> 20 (lsr #0x14);
|
||||
//: scans the roots plus all descendants.
|
||||
// getProcessInformation: memoryMB = resident bytes >> 20 (lsr #0x14);
|
||||
// cpuTimeMs = (user + system time, in Mach ticks × timebase) / 1 000 000;
|
||||
// each process is tied to its agent by CONVERSATION_ID= / REQUEST_ID= in its environment.
|
||||
|
||||
|
|
@ -852,9 +852,9 @@ fn timeout_of(v: &Value) -> u64 {
|
|||
}
|
||||
|
||||
// ---------- computers ----------
|
||||
// The original runs a bot's work on "Bot 的電腦" (its own cloud box) and only runs on this Mac when
|
||||
// the person turns on 在這台電腦上執行 (setting `local-execution`, ids fuUvu2 / Hd1KB+). Permission
|
||||
// questions name the machine ("允許 Grok Bot 在 {targetMachineDisplay} 上使用 {app} 嗎?", eQViQE).
|
||||
// The reference assistant runs a bot's work on its own cloud box and only runs on the person's
|
||||
// Mac when they turn on 在這台電腦上執行 (setting `local-execution`). Permission questions name
|
||||
// the machine ("允許助理在 {targetMachineDisplay} 上使用 {app} 嗎?").
|
||||
// Here the bot's own computer is a sealed folder, this Mac is the person's computer, and other
|
||||
// computers are reached over SSH.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Event-triggered routines (the original's trigger kinds, main-app.cjs routine trigger schema:
|
||||
// Event-triggered routines (trigger kinds, following the routine trigger schema:
|
||||
// cron, slack, github, origin, microsoftTeams, linear, sentry, pagerduty, email, webhook, group).
|
||||
// The original's servers receive those events; here AnyBot checks for them with the person's own
|
||||
// tokens while it runs (the original also keeps event triggers running locally when it can't
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ pub fn uses_person_browser(args: &str) -> bool {
|
|||
|
||||
/// browser_open / browser_read on the person's own browser run on this Mac.
|
||||
pub fn uses_this_computer(name: &str, args: &str) -> bool {
|
||||
// AnyBot's own browser is the bot's, like Grok Bot's; only the person's own browser counts
|
||||
// AnyBot's own browser is the bot's, like the reference assistant's; only the person's own browser counts
|
||||
// as working on their computer. Profiles are the bot's browser's, never the person's.
|
||||
(name == "browser_open" || name == "browser_read") && uses_person_browser(args)
|
||||
}
|
||||
|
|
@ -338,7 +338,7 @@ fn is_running(app: &str, cancel: &AtomicBool) -> bool {
|
|||
osascript(&format!("application {} is running", applescript_string(app)), cancel).map(|s| s == "true").unwrap_or(false)
|
||||
}
|
||||
|
||||
/// On-screen windows, front to back, the way the original's `listWindows` (sand_window_capture.node
|
||||
/// On-screen windows, front to back, the way the original's `listWindows` (
|
||||
/// 0x52c0) reads them: `CGWindowListCopyWindowInfo(0x11 = on-screen only | exclude desktop, 0)`.
|
||||
const WINDOW_LIST_JXA: &str = "ObjC.import('CoreGraphics');var a=ObjC.deepUnwrap(ObjC.castRefToObject($.CGWindowListCopyWindowInfo(0x11,0)))||[];JSON.stringify(a.map(function(w){var b=w.kCGWindowBounds||{};return {owner:w.kCGWindowOwnerName||'',layer:w.kCGWindowLayer||0,alpha:(w.kCGWindowAlpha===undefined?1:w.kCGWindowAlpha),w:b.Width||0,h:b.Height||0};}));";
|
||||
|
||||
|
|
@ -459,7 +459,7 @@ pub fn app_exe(browser: &str) -> String {
|
|||
|
||||
fn browser_read(max_chars: usize, cancel: &AtomicBool) -> Result<String, String> {
|
||||
let (browser, chromium) = pick_browser(cancel);
|
||||
// First through accessibility, like Grok Bot's Computer Use helper: no browser setting, and
|
||||
// First through accessibility, like the reference assistant's Computer Use helper: no browser setting, and
|
||||
// it reads whichever profile's window the person has in front.
|
||||
match crate::axread::read_person_browser(&app_exe(browser), max_chars) {
|
||||
Ok(page) if !page.text.trim().is_empty() => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue