using-grok-bot-app

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Using the Grok Bot app

使用Grok Bot应用

/Applications/Grok Bot.app
is an Electron app, so it is a Chromium renderer wearing a native window: everything CDP does to a web page works here. Drive it with
agent-browser
, never with Computer Use or synthetic clicks — those are slower, unreliable, and unnecessary when a real DOM is one port away.
Identity caveat. The bundle identifier is
com.anysphere.sand
(Anysphere, the Cursor vendor), not xAI. The name is a costume. Do not infer xAI API semantics, model names, or rate limits from it.
Name collision.
box@grok-bot
in this fleet is a Tailscale deploy host, a completely different thing. If the task is SSH, deployment, or port 5173, this skill is the wrong one.
/Applications/Grok Bot.app
是一款Electron应用,因此它是套着原生窗口的Chromium渲染器:CDP对网页能做的所有操作在这里都适用。请使用
agent-browser
来操控它,绝对不要用Computer Use或模拟点击——当真实DOM只差一个端口就能访问时,那些方法既慢又不可靠,而且完全没必要。
身份注意事项:包标识符为
com.anysphere.sand
(Anysphere是Cursor的开发商),并非xAI。这个名字只是个外壳。不要据此推断xAI API的语义、模型名称或速率限制。
名称冲突:本集群中的
box@grok-bot
是一台Tailscale部署主机,完全是两回事。如果任务涉及SSH、部署或5173端口,请勿使用此技能。

Connect

连接

The
--remote-debugging-port
flag only takes effect at launch, so an already-running instance has no port to attach to and must be restarted. That restart drops whatever is typed in the composer, which is why it needs the user's OK before you pull the trigger.
sh
pgrep -xl "Grok Bot"                                   # running?
lsof -nP -iTCP:9231 -sTCP:LISTEN                       # already debuggable?
pkill -x "Grok Bot"                                    # only with user approval
open -a "Grok Bot" --args --remote-debugging-port=9231
agent-browser connect 9231
agent-browser tab      # expect one target: file://…/app.asar/dist/renderer/index.html
If the port is already listening, skip straight to
connect
— no restart, no approval needed, nothing lost.
--remote-debugging-port
参数仅在应用启动时生效,因此已运行的实例没有可连接的调试端口,必须重启。重启会丢失输入框中已输入的内容,因此执行重启前必须征得用户同意。
sh
pgrep -xl "Grok Bot"                                   # running?
lsof -nP -iTCP:9231 -sTCP:LISTEN                       # already debuggable?
pkill -x "Grok Bot"                                    # only with user approval
open -a "Grok Bot" --args --remote-debugging-port=9231
agent-browser connect 9231
agent-browser tab      # expect one target: file://…/app.asar/dist/renderer/index.html
如果端口已经在监听,直接跳到
connect
步骤即可——无需重启,无需批准,也不会丢失任何内容。

Address bots by UUID, never by name

通过UUID而非名称定位机器人

Every sidebar entry is
button[data-agent-id="<uuid>"]
with the human-readable name in
aria-label
. Names repeat — several bots are literally called
座位
— so an
aria-label
selector silently picks whichever one the DOM happened to order first. The UUID is the only stable key, and it has survived app upgrades (observed across 0.29.0 → 0.39.0), so it is safe to remember one between sessions and resolve it back to a name at run time.
侧边栏的每个条目都是
button[data-agent-id="<uuid>"]
,其可读名称存储在
aria-label
属性中。名称可能重复——有好几个机器人就叫
座位
——因此使用
aria-label
选择器会悄无声息地选中DOM排序中第一个出现的条目。UUID是唯一稳定的标识,并且在应用升级中保持不变(已在0.29.0 → 0.39.0版本中验证),因此可以在会话间记住UUID,运行时再解析回名称。

Read

读取数据

Take one
snapshot -i
to see the shape of the tree, then switch to
eval
for everything after that. Snapshots pour the whole accessibility tree into context;
eval
returns exactly the JSON you asked for, which is the difference between a cheap session and an expensive one.
Bot roster and folder structure, no clicking required:
sh
agent-browser eval '(() => JSON.stringify({
  bots: [...document.querySelectorAll("button[data-agent-id]")]
    .map(b => ({id: b.getAttribute("data-agent-id").slice(0,8), name: b.getAttribute("aria-label")})),
  sidebar: document.querySelector("[aria-label=\"Bot list\"]")?.innerText.replace(/\n+/g,"|").slice(0,800)
}))()'
The sidebar text carries each bot's last message preview and timestamp, so "what did RULES last say" and "which bots moved today" are answerable without opening anything. Reach for the full transcript only when the preview is not enough.
Full transcript of one bot — this requires selecting it, which changes what the user sees on screen. The active bot is marked
aria-current="page"
, so capture it in the same call that navigates away, and you can restore it afterwards without guessing. Substitute the target UUID prefix for
<uuid8>
:
sh
agent-browser eval '(async () => {
  const prev = document.querySelector("button[data-agent-id][aria-current=\"page\"]")
    ?.getAttribute("data-agent-id");
  document.querySelector("button[data-agent-id^=\"<uuid8>\"]").click();
  await new Promise(r => setTimeout(r, 2500));
  const g = [...document.querySelectorAll("[role=group][aria-label$=\"message\"]")];
  return JSON.stringify({prev, count: g.length, msgs: g.slice(-5).map(m =>
    m.getAttribute("aria-label") + ": " + (m.innerText||"").replace(/\s+/g," ").slice(0,200))});
})()'
Messages live in
[role=group]
nodes whose
aria-label
is
"<bot> message"
or
"Your message"
, nested in
article
elements. Slice the text — a long transcript will otherwise dump tens of thousands of characters into context for no gain.
Put the screen back. Selecting a bot is a visible change to the user's app, so click the captured
prev
UUID when you are done, and say that you did. Restore by UUID rather than by name — names repeat, and there is no reliable heading to read the state back from (
main h2
came back empty in practice). Confirm with the whole panel's text instead:
sh
agent-browser eval '(async () => {
  document.querySelector("button[data-agent-id^=\"<prev8>\"]").click();
  await new Promise(r => setTimeout(r, 1500));
  return (document.querySelector("main")?.innerText || "").replace(/\s+/g," ").slice(0,80);
})()'
先执行一次
snapshot -i
查看DOM树结构,之后所有操作都切换到
eval
。快照会把整个无障碍树都塞进上下文;而
eval
只返回你明确请求的JSON,这就是低成本会话和高成本会话的区别。
无需点击即可获取机器人列表和文件夹结构:
sh
agent-browser eval '(() => JSON.stringify({
  bots: [...document.querySelectorAll("button[data-agent-id]")]
    .map(b => ({id: b.getAttribute("data-agent-id").slice(0,8), name: b.getAttribute("aria-label")})),
  sidebar: document.querySelector("[aria-label=\"Bot list\"]")?.innerText.replace(/\n+/g,"|").slice(0,800)
}))()'
侧边栏文本包含每个机器人的最后一条消息预览和时间戳,因此“RULES最后说了什么”和“哪些机器人今天有更新”这类问题不用打开任何对话就能回答。只有当预览信息不足时才需要获取完整对话记录。
获取单个机器人的完整对话记录——这需要选中该机器人,会改变用户屏幕上的应用显示内容。当前活动的机器人标记为
aria-current="page"
,因此在导航离开的同一个调用中捕获当前活动的机器人ID,之后就可以无需猜测地恢复原状。将目标UUID前缀替换为
<uuid8>
sh
agent-browser eval '(async () => {
  const prev = document.querySelector("button[data-agent-id][aria-current=\"page\"]")
    ?.getAttribute("data-agent-id");
  document.querySelector("button[data-agent-id^=\"<uuid8>\"]").click();
  await new Promise(r => setTimeout(r, 2500));
  const g = [...document.querySelectorAll("[role=group][aria-label$=\"message\"]")];
  return JSON.stringify({prev, count: g.length, msgs: g.slice(-5).map(m =>
    m.getAttribute("aria-label") + ": " + (m.innerText||"").replace(/\s+/g," ").slice(0,200))});
})()'
消息位于
[role=group]
节点中,其
aria-label
"<bot> message"
"Your message"
,嵌套在
article
元素内。请对文本做截断——否则很长的对话记录会无缘无故地将数万个字符塞入上下文。
恢复界面原状:选中机器人会对用户的应用界面造成可见更改,因此操作完成后请点击之前捕获的
prev
UUID,并告知用户已恢复界面。请通过UUID而非名称恢复——因为名称可能重复,而且没有可靠的标题可以回读状态(实际测试中
main h2
返回为空)。可以用整个面板的文本来确认恢复结果:
sh
agent-browser eval '(async () => {
  document.querySelector("button[data-agent-id^=\"<prev8>\"]").click();
  await new Promise(r => setTimeout(r, 1500));
  return (document.querySelector("main")?.innerText || "").replace(/\s+/g," ").slice(0,80);
})()'

Sending is gated

发送操作需受限

Typing into the composer works: it is a single
div[contenteditable=true]
with placeholder
Prompt
, and Enter submits. But a sent message is an irreversible external side effect — another agent reads it and acts. So draft the text, show it to the user verbatim, and send only after they approve that text.
在输入框中输入是可行的:它是单个
div[contenteditable=true]
元素,占位符为
Prompt
,按Enter键即可提交。但发送消息是不可逆的外部副作用——另一个代理会读取它并采取行动。因此请先起草文本,逐字展示给用户,只有在用户批准该文本后才能发送。

What will bite you

常见坑点

  • Element refs go stale.
    agent-browser click @e5
    fails with
    Could not locate element with role=button name=…
    once React re-renders, because
    @eN
    means "the Nth node of that particular snapshot". Re-snapshot immediately before interacting, or bypass refs entirely with
    eval
    and a DOM attribute selector — the latter is what the recipes above do.
  • No local storage to shortcut through.
    localStorage
    ,
    sessionStorage
    , and
    indexedDB.databases()
    are all empty. The DOM is the only source, so there is no "just query the database" path; a full export costs one click and one wait per bot.
  • The two message counts disagree. One conversation showed a header of "3 messages with 2 Bots" while the DOM held 7
    [role=group]
    nodes. Which one is authoritative, and why they differ, is
    UNCONFIRMED
    — the header may count threads rather than messages, or the DOM may hold rendered system entries. Neither number is a safe answer on its own: if a transcript looks short or the counts disagree, scroll the
    log "Conversation transcript"
    container, re-read, and report what you actually saw.
  • article
    counts lie.
    One
    eval
    returned a single
    article
    while the snapshot showed dozens. When dumping a whole transcript, prefer the union selector
    '[role=article],article,[role=group]'
    .
  • 一趟抓不全,而且捲到頂會刪資料。 對話串是虛擬化清單:
    scrollTop = 0
    一跳到頂,底部節點就被回收,等於邊讀邊刪(一次擷取因此掉了整天份的最新訊息)。 可靠做法是兩種獨立方法各跑一趟再取聯集:錨點法(抓最頂那則
    [role=group]
    scrollIntoView({block:"start"})
    ,直到頂端訊息連續數次不變 總數不再增長——兩個條件要同時成立,只看一個會把「捲不動」誤判成「到頂」) 與像素法
    [role=log][aria-label="Conversation transcript"]
    scrollTop
    由頂往下每次 0.8 屏)。去重鍵用
    aria-label + 前 120 字
    ,不要用 DOM 節點參照—— 同一則訊息被回收重建後是不同節點,內容才是穩定的身分。 兩法數字一致才算抓全:五個 bot 兩法各自給出 151/151、53/53、38/38、10/10、 7/7,那是可信的完整性證據;另一個 bot 兩法給 277 vs 132、聯集 372,就只能標
    UNCONFIRMED
  • 對話串短,先懷疑擷取方法,不要當成「這個 bot 沒在動」。 一個每天回報的 bot 曾被單趟擷取抓成 24 則、內容全是幾週前的設定過程,據此推論「它從沒回報過」—— 重抓後是 151 則,日報一直都在它自己的直接對話串上。另外,跨 bot 的往來還有獨立的 exchange 串(側欄與訊息裡的
    button[aria-label^="Open exchange with"]
    ), 那是另一個容器,不是日報的所在地;要讀跨 bot 對話才需要展開它。
  • 元素引用会失效:一旦React重新渲染,
    agent-browser click @e5
    会失败并报错
    Could not locate element with role=button name=…
    ,因为
    @eN
    指的是“特定快照中的第N个节点”。交互前请立即重新生成快照,或者完全绕过引用,使用
    eval
    和DOM属性选择器——上面的示例用的就是后者。
  • 没有本地存储可以走捷径
    localStorage
    sessionStorage
    indexedDB.databases()
    都是空的。DOM是唯一的数据源,因此没有“直接查数据库”的路径;完整导出每个机器人的数据需要一次点击和一次等待。
  • 两个消息计数不一致:有一个对话显示标题为“3 messages with 2 Bots”,但DOM中有7个
    [role=group]
    节点。哪个是权威数据,以及为什么会不一致,目前为
    UNCONFIRMED
    状态——标题可能统计的是线程而非消息,或者DOM中包含渲染的系统条目。两个数字本身都不能作为可靠答案:如果对话看起来很短或者计数不一致,请滚动
    log "Conversation transcript"
    容器,重新读取,并报告你实际看到的内容。
  • article
    计数不准
    :有一次
    eval
    只返回了一个
    article
    ,但快照显示有几十个。导出完整对话记录时,优先使用联合选择器
    '[role=article],article,[role=group]'
  • 一次抓不全,而且滚到顶部会删除数据。对话串是虚拟化列表:
    scrollTop = 0
    一跳到顶,底部节点就会被回收,等于边读边删(一次抓取因此丢失了一整天的最新消息)。可靠的做法是用两种独立方法各跑一遍再取并集:锚点法(抓取最顶部的那则
    [role=group]
    ,调用
    scrollIntoView({block:"start"})
    ,直到顶端消息连续数次不变总数不再增长——两个条件要同时满足,只看一个会把“滚不动”误判成“到顶了”)与像素法
    [role=log][aria-label="Conversation transcript"]
    scrollTop
    从顶部往下每次滚动0.8屏)。去重键使用
    aria-label + 前120字
    ,不要用DOM节点引用——同一则消息被回收重建后是不同节点,内容才是稳定的身份标识。两种方法的数字一致才算抓全:五个机器人两种方法各自给出151/151、53/53、38/38、10/10、7/7,这是可信的完整性证据;另一个机器人两种方法给出277 vs 132、并集372,就只能标记为
    UNCONFIRMED
  • 对话串短,先怀疑抓取方法,不要当成“这个机器人没在运行”。有一个每天上报的机器人曾被单趟抓取抓到24则消息,内容全是几周前的设置过程,据此推论“它从没上报过”——重新抓取后有151则,日报一直都在它自己的直接对话串上。另外,跨机器人的往来还有独立的exchange串(侧边栏和消息里的
    button[aria-label^="Open exchange with"]
    ),那是另一个容器,不是日报的所在地;只有需要读取跨机器人对话时才需要展开它。

Where this came from

内容来源

Everything above was executed against version
0.39.0
on macOS. The app ships an embedded
Grok Bot's Computer
panel (a bot can hand its screen over for interactive login and take it back) which is visible in the tree but unexplored — if a task needs it, expect to map it yourself and write down what you find.
以上所有内容均在macOS上针对
0.39.0
版本测试验证。该应用自带一个嵌入式的“Grok Bot's Computer”面板(机器人可以交出其屏幕用于交互式登录,之后再收回),该面板在DOM树中可见但尚未探索——如果任务需要用到它,你需要自行梳理其结构并记录发现。