⚠️ 🚨 CRITICAL BUTTON STRUCTURE WARNING 🚨
The buttons array from the script is a 2D array where each inner array = one row.
ALWAYS pass it directly to the message tool without modification.
- - In OpenClaw context: use
response['buttons'] directly - In Python scripts: use
payload['buttons'] from INLINECODE3
❌ NEVER flatten, restructure, re-group, or stringify rows.
Zero-Footgun Protocol
When sending any browser menu, follow the correct workflow for your context:
If in OpenClaw (using exec + message tools):
- 1. Run
browser_dispatcher.py via exec tool - Use
response['messageToolCall'] as the complete payload for the message tool - Don't touch the buttons — use them exactly as returned inside that payload
- If send succeeds and the response includes
postSend, then update liveMessageId and optionally delete the previous menu
If in standalone Python script:
- 1. Generate plan via
run_browser_action.py or import functions - Run
build_message_payload(plan) and require INLINECODE11 - Use
payload['message'] and payload['buttons'] from the validated result
Common mistakes to avoid:
- - ❌ Don't rewrite buttons manually
- ❌ Don't flatten buttons into one row
- ❌ Don't pass buttons as a JSON string
Telegram File Browser
Build Telegram file-browsing flows around a single live menu plus optional side messages for previews, paths, and downloads.
Invocation
When the user explicitly invokes telegram-file-browser or asks to use this skill:
- 1. Open an interactive browser immediately in the same turn.
- Default the root to
~/.openclaw/workspace unless the user gave a path. - Do not reply with a conversational clarification like “你想让我做什么?” when the user already asked to browse.
- Ask a follow-up only when the target path or safety boundary is genuinely ambiguous.
- If the input is
config, open the configuration flow instead of the browser tree.
Do not stop at describing the skill when you already have enough information to launch the UI.
Runtime Contexts
This skill can be used in two different contexts. Choose the one that matches your environment.
Context 1: OpenClaw Tool Calls (Most Common)
When you're running inside OpenClaw and using exec + message tools (like right now), use browser_dispatcher.py via exec, then call the message tool with the exact JSON from the script.
⚠️ The script already validates and returns the correct payload — use it directly, don't rewrite anything.
Step 1: Run the script via exec tool
CODEBLOCK0
Step 2: Call message tool with the script's output
CODEBLOCK1
Complete example:
CODEBLOCK2
Callback handling:
CODEBLOCK3
Important for current Telegram/OpenClaw routing:
If a button click arrives as a plain inbound text message like tfb_root_v12_w8 instead of a native callback event, treat that text as the callback_data and run the exact same callback flow above. Do not ignore it just because it came in as a message.
Recommended detection rule:
- - if inbound text matches INLINECODE23
- treat it as a telegram-file-browser callback
- run
browser_dispatcher.py handle-callback <that_text> immediately - do not ask the user what they mean
Context 2: Standalone Python Script
When you're writing a standalone Python script (not inside OpenClaw), use the import-based approach with build_message_payload.
CODEBLOCK4
Why build_message_payload is required in Python scripts:
- - Validates button structure (2D array, one row per item)
- Returns the exact payload to send
- Prevents common mistakes like flattening buttons
Why it's NOT needed in OpenClaw context:
- -
browser_dispatcher.py already returns a validated payload - Just use the JSON values directly
⚠️ For OpenClaw tool calls: Just use browser_dispatcher.py — see Context 1 above.
The examples below are for standalone Python scripts only.
Tool Plan Contract
In OpenClaw context (most common): Use browser_dispatcher.py — it already validates and returns the correct payload. Just use the JSON values directly.
In standalone Python scripts: Use run_browser_action.py + build_message_payload(plan) as the guardrail.
Supported toolAction values:
Plans that send buttons should also include viewType (for example directory or file-actions) so the validator can enforce row-shape rules.
- - INLINECODE36
- INLINECODE37
- INLINECODE38
- INLINECODE39
Map them to OpenClaw message actions like this:
-
In OpenClaw context: Use
browser_dispatcher.py — the response already contains validated
message and
buttons. Just pass them to the message tool:
message(action='send', message=response['message'], buttons=response['buttons'])
-
In Python scripts: Run
build_message_payload(plan) first, then use:
payload = wrapped["payload"]
message(action=payload["action"], message=payload["message"], buttons=payload["buttons"])
- Pass
buttons as a real 2D array (each inner array = one row)
- Do
not stringify or flatten buttons
⚠️ Common mistakes to avoid:
CODEBLOCK7
- use
message action=delete
- pass INLINECODE48
- use
message action=send
- pass
path or
filePath
- pass
caption
- if
replyTo is present, pass it as INLINECODE55
- do not send any user-visible message unless debugging is explicitly needed
Important runtime note:
- - With the current OpenClaw message tool, Telegram inline buttons are reliably attached on
send, not on edit. - Therefore navigation should be implemented as send a fresh menu, then optionally delete the previous one.
- Treat
edit-message mode as replace the prior menu with minimal chat noise, not as a literal button-preserving in-place edit.
Display Modes
Persist display mode in config.json.
edit-message
- - keep one logical live browser menu
- because current OpenClaw tooling does not reliably preserve Telegram buttons on
edit, implement this as:
1. send a fresh menu
2. update
liveMessageId
3. delete the previous menu after send succeeds
- - best for low-noise chat history
new-message
- - navigation sends a fresh browser menu
- old menus remain in history unless you intentionally delete them
- update
liveMessageId to the newly sent menu after success - previews, path replies, and downloads still reply to the current live menu
Core Interaction Rules
Keep callback payloads short
Never place full file paths in callback_data.
Use opaque ids and store the real mapping in state.
Split navigation from content
Use the menu for navigation only.
Use separate messages for:
- - file previews
- copyable path output
- file downloads
- error notices that should not replace the current menu
File click behavior
When a user clicks a file from a directory listing:
- 1. Open a file action menu.
- Do not immediately dump file contents.
- Offer at least
👁 预览, 📋 路径, ⬇️ 下载, ⬅️ 返回, ❌ 关闭. - Prefer one button per row here as well unless a later UX change explicitly says otherwise.
Pagination
Keep pagination inside the same state machine.
Required behavior:
- - page buttons stay versioned like other callbacks
- page changes rebuild the current directory view
- pagination preserves root boundary and current path
- item ids remain resolvable after changing pages
State Requirements
Persist at least:
- - INLINECODE72
- INLINECODE73
- INLINECODE74
- INLINECODE75
- INLINECODE76
- INLINECODE77
- the current view's page metadata
Recommended shape:
CODEBLOCK8
Callback Safety
Protect against replayed and stale callbacks.
Required behavior:
- - include menu version in interactive callbacks
- reject callbacks whose version does not match current state
- debounce duplicate callbacks received in a short window
- prefer silent
noop over noisy execution for stale callbacks
Failure Handling
Missing or stale target
If the resolved file or directory no longer exists:
- - keep the browser alive
- send a short failure message or rebuild the nearest valid view
- preserve a way back
Callback execution loop
When Telegram delivers a button click as a callback message such as callback_data: tfb_root_v2_w13, do this immediately:
- 1. run INLINECODE80
- inspect the returned tool plan
- if
toolAction == "send", run it through send_plan.py / build_message_payload(plan) first - execute the validated payload with the OpenClaw
message tool - if a fresh menu was sent successfully, write the returned
messageId back into state as INLINECODE86 - if the plan requests previous-menu cleanup, delete the previous menu only after the new menu send succeeds
Do not answer a callback message with a normal conversational reply when the callback belongs to the file browser.
Do not ask a follow-up unless the callback cannot be resolved safely.
Prefer silent noop over chatty recovery for stale callbacks.
Message edit failure
If an edit fails because the menu no longer exists or cannot be edited:
- - send a fresh menu
- update INLINECODE88
- preserve current path and stack when possible
Oversized or invalid callbacks
If Telegram rejects callback payloads:
- - shorten callback ids
- move real state into local storage
- do not encode raw paths in buttons
Bundled Scripts
- -
scripts/build_view.py — list a directory, sort entries, paginate, and emit view JSON - INLINECODE90 — initialize/load/save state and manage current path, back stack, menu version, and live message id
- INLINECODE91 — generate safe file previews
- INLINECODE92 — build Telegram button matrices for directory and file-action views
- INLINECODE93 — resolve callback payloads into browser actions
- INLINECODE94 — orchestrate open-root, open-dir, file actions, back, paging, and live-message state updates
- INLINECODE95 — load and persist display config
- INLINECODE96 — convert browser actions into concrete tool plans for messaging and file delivery
- INLINECODE97 — validate send plans and build the only approved
message(action="send", ...) payload - INLINECODE99 — CLI validator for plan JSON before sending
- INLINECODE100 — regression test for root/page/dir/file-action flows and flattened-button rejection
scripts/browser_dispatcher.py — ⭐ RECOMMENDED one-click wrapper: generates plan, validates, returns exact payload. Use this instead of hand-rolling the flow.
Runtime artifacts
Treat state/ as runtime-only scratch data. Do not commit it. Keep it ignored in git.
Pre-send Checklist
Before sending or replacing a browser menu, verify all of the following:
In OpenClaw context (using exec + message tools):
- - ✅ You used
browser_dispatcher.py to get the plan - ✅ You passed
response['buttons'] directly to the message tool — no manual rewriting - ✅
buttons is a real 2D array (each inner array = one row)
In standalone Python scripts:
- - ✅ You used
build_message_payload(plan) to validate - ✅ You used
payload['buttons'] from the validated result
General rules (always):
- - ⚠️ Never rewrite buttons yourself — use the script's output directly
- ⚠️ Never flatten buttons into one row — keep 2D structure
- ✅ Each directory/file item stays on its own row
- ✅ Callback payloads are short opaque ids, not raw paths
- ✅
replyTo is forwarded when the tool plan includes it - ✅ After a successful send,
liveMessageId is updated - ✅ When replacing a live menu, send first and delete old menu only after send succeeds
Hard rule
Treat any manual rewrite of plan["buttons"] as a bug, not an optimization.
Notes
Read references/interaction-patterns.md when you need concrete UX guidance for:
- - single-message navigation
- file action menus
- pagination behavior
- copyable path behavior
- menu recovery after edit failures
技能名称: telegram-file-browser
详细描述:
⚠️ 🚨 关键按钮结构警告 🚨
脚本中的 buttons 数组是一个二维数组,其中每个内部数组代表一行。
始终直接将其传递给消息工具,无需修改。
- - 在 OpenClaw 上下文中:直接使用 response[buttons]
- 在 Python 脚本中:使用来自 buildmessagepayload() 的 payload[buttons]
❌ 切勿展平、重组、重新分组或字符串化行。
零失误协议
发送任何浏览器菜单时,请根据您的上下文遵循正确的工作流程:
如果在 OpenClaw 中(使用 exec + message 工具):
- 1. 通过 exec 工具运行 browser_dispatcher.py
- 使用 response[messageToolCall] 作为 message 工具的完整负载
- 不要触碰按钮 — 完全按照该负载中返回的方式使用它们
- 如果发送成功且响应包含 postSend,则更新 liveMessageId 并可选择删除之前的菜单
如果在独立的 Python 脚本中:
- 1. 通过 runbrowseraction.py 或导入函数生成计划
- 运行 buildmessagepayload(plan) 并要求 ok: true
- 使用验证结果中的 payload[message] 和 payload[buttons]
要避免的常见错误:
- - ❌ 不要手动重写按钮
- ❌ 不要将按钮展平成一行
- ❌ 不要将按钮作为 JSON 字符串传递
Telegram 文件浏览器
围绕一个实时菜单构建 Telegram 文件浏览流程,并可选择添加用于预览、路径和下载的侧边消息。
调用
当用户明确调用 telegram-file-browser 或要求使用此技能时:
- 1. 在同一轮对话中立即打开一个交互式浏览器。
- 除非用户提供了路径,否则默认根目录为 ~/.openclaw/workspace。
- 当用户已经要求浏览时,不要用“你想让我做什么?”这样的对话式澄清来回复。
- 仅当目标路径或安全边界确实不明确时才提出后续问题。
- 如果输入是 config,则打开配置流程,而不是浏览器树。
当您已有足够信息启动 UI 时,不要停留在描述技能上。
运行时上下文
此技能可在两种不同的上下文中使用。选择与您的环境匹配的一种。
上下文 1:OpenClaw 工具调用(最常见)
当您在 OpenClaw 内部运行并使用 exec + message 工具时(就像现在这样),通过 exec 使用 browser_dispatcher.py,然后使用脚本中的精确 JSON 调用 message 工具。
⚠️ 脚本已经验证并返回正确的负载 — 直接使用它,不要重写任何内容。
步骤 1:通过 exec 工具运行脚本
python
运行调度器以获取最终的消息工具负载
result = exec(command=python3 ~/.openclaw/workspace/skills/telegram-file-browser/scripts/browser_dispatcher.py open-root)
response = json.loads(result) # response 包含 messageToolCall、postSend 和兼容性字段
步骤 2:使用脚本的输出调用 message 工具
python
✅ 正确:直接传递调度器负载
message(
response[messageToolCall])
❌ 错误:不要自己重写按钮
message(
action=send,
message=response[message],
buttons=[[{...}, {...}]]
)
❌ 错误:也不要字符串化负载
message(action=send, message=response[message], buttons=json.dumps(response[buttons]))
完整示例:
python
1. 获取完整的消息工具负载
result = exec(command=python3 ~/.openclaw/workspace/skills/telegram-file-browser/scripts/browser_dispatcher.py open-root)
response = json.loads(result.stdout)
if response[ok] and response.get(messageToolCall):
# 2. 精确发送调度器返回的内容
msg_result = message(response[messageToolCall])
# 3. 如果请求,更新 liveMessageId
post_send = response.get(postSend) or {}
if msgresult.get(messageId) and postsend.get(updateLiveMessageId):
exec(command=python3 ~/.openclaw/workspace/skills/telegram-file-browser/scripts/browserdispatcher.py update-message-id + str(msgresult[messageId]))
# 4. 仅在新菜单成功后才删除之前的实时菜单
previousid = postsend.get(previousMessageId)
if previousid and postsend.get(cleanupPreviousMessage):
message(action=delete, messageId=previous_id)
回调处理:
python
1. 处理回调
result = exec(command=python3 ~/.openclaw/workspace/skills/telegram-file-browser/scripts/browser
dispatcher.py handle-callback + callbackdata)
response = json.loads(result.stdout)
2. 执行脚本返回的精确工具负载
if response.get(messageToolCall):
msg_result = message(
response[messageToolCall])
post_send = response.get(postSend) or {}
if msgresult.get(messageId) and postsend.get(updateLiveMessageId):
exec(command=python3 ~/.openclaw/workspace/skills/telegram-file-browser/scripts/browserdispatcher.py update-message-id + str(msgresult[messageId]))
previousid = postsend.get(previousMessageId)
if previousid and postsend.get(cleanupPreviousMessage):
message(action=delete, messageId=previous_id)
当前 Telegram/OpenClaw 路由的重要说明:
如果按钮点击以纯文本入站消息的形式到达,例如 tfbrootv12w8,而不是原生回调事件,将该文本视为 callbackdata 并立即运行上述完全相同的回调流程。不要仅仅因为它作为消息到达就忽略它。
推荐的检测规则:
- - 如果入站文本匹配 ^tfb(root|dir|preview|path|download|back|close)
- 将其视为 telegram-file-browser 回调
- 立即运行 browserdispatcher.py handle-callback text>
- 不要询问用户他们的意图
上下文 2:独立 Python 脚本
当您编写独立的 Python 脚本时(不在 OpenClaw 内部),使用基于导入的方法配合 buildmessagepayload。
python
import sys
sys.path.insert(0, ~/.openclaw/workspace/skills/telegram-file-browser/scripts)
from runbrowseraction import openroot, handlecallback
from sendplan import buildmessage_payload
STATE = ~/.openclaw/workspace/.openclaw/telegram-file-browser/state.json
ROOT = ~/.openclaw/workspace
def sendbrowserplan(plan):
使用 OpenClaw 的消息工具或 Telegram API 发送消息。
if plan[toolAction] == noop:
return None
if plan[toolAction] == delete:
return {action: delete, messageId: plan[messageId]}
if plan[toolAction] == send-file:
return {action: send, path: plan[path], caption: plan.get(caption)}
# 必须通过 buildmessagepayload 验证
wrapped = buildmessagepayload(plan)
if not wrapped[ok]:
raise RuntimeError(wrapped[error])
payload = wrapped[payload]
return {
action: payload[action],
message: payload[message],
buttons: payload[buttons],
replyTo: payload.get(replyTo)
}
首次打开
send
browserplan(open_root(STATE, ROOT))
处理回调
send
browserplan(handle
callback(STATE, callbackdata))
为什么在 Python 脚本中需要 buildmessagepayload:
- - 验证按钮结构(二维数组,每项一行)
- 返回要发送的精确负载
- 防止常见错误,例如展平按钮
为什么在 OpenClaw 上下文中不需要:
- - browser_dispatcher.py 已经返回验证过的负载
- 直接使用 JSON 值即可
⚠️ 对于 OpenClaw 工具调用:只需使用 browser_dispatcher.py — 请参阅上面的上下文 1。
下面的示例仅适用于独立 Python 脚本。
工具计划契约
在 OpenClaw 上下文(最常见)中:使用 browser_dispatcher.py — 它已经验证并返回正确的负载。直接使用 JSON 值即可。
在 独立 Python 脚本中:使用 runbrowseraction.py +