LocalSend
Interactive file transfer between devices on the local network using real Telegram inline keyboard buttons. Works with any device running the LocalSend app (Android, iOS, Windows, macOS, Linux).
Install
The localsend-cli is a zero-dependency Python CLI. Install from GitHub:
CODEBLOCK0
Full docs: https://github.com/Chordlini/localsend-cli
Requires Python 3.8+ and openssl (for TLS).
Telegram Button Format
All menus MUST use OpenClaw's inline button format. Send buttons alongside your message using this structure:
CODEBLOCK1
- - Outer array = rows of buttons
- Inner array = buttons per row (max 3 per row for readability)
- Prefix all callback_data with
ls: to namespace this skill - When user taps a button, you receive: INLINECODE3
State Awareness (CRITICAL)
This skill uses conversational state. Track where you are in the flow:
| State | Meaning | Next user input should be treated as... |
|---|
| INLINECODE4 | No active flow | Normal message — respond normally |
| INLINECODE5 |
Asked user to drop/specify a file to send |
The file to send — do NOT comment on it, describe it, or react to it. Immediately use it as the send payload. |
|
awaiting_text | Asked user to type text to send |
The text payload — send it, don't discuss it |
|
awaiting_confirm | Waiting for send confirmation | Expect
ls:confirm-send or
ls:menu |
|
receiving | Receiver is active | Monitor for incoming files |
RULES:
- - When in
awaiting_file state and user sends an image/file/path → treat it as the file to send. Show confirmation buttons immediately. - When in
awaiting_text state and user types anything → treat it as the text to send. - NEVER comment on, describe, or react to a file/image when you're in
awaiting_file state. - State resets to
idle when user taps ls:menu or the flow completes.
On Trigger: Main Menu
When the user types /localsend or mentions sending/receiving files locally, send this message with real inline buttons:
Message:
CODEBLOCK2
Buttons:
CODEBLOCK3
Do NOT run any commands yet. Wait for the button tap.
Flow: Scan Devices
Trigger: callback_data: ls:devices or user says "scan", "discover", "find devices"
- 1. Run:
CODEBLOCK4
- 2. Devices found — create one button per device, plus Refresh and Back:
Message:
CODEBLOCK5
Buttons (one device per row):
CODEBLOCK6
- 3. No devices found:
Message:
CODEBLOCK7
Buttons:
CODEBLOCK8
- 4. User taps a device (
callback_data: ls:dev:DEVICENAME) — store it as the selected target. Show action menu:
Message:
CODEBLOCK9
Buttons:
buttons: [
[
{ "text": "📄 Send File", "callback_data": "ls:sendfile" },
{ "text": "📝 Send Text", "callback_data": "ls:sendtext" }
],
[
{ "text": "📦 Send Multiple", "callback_data": "ls:sendmulti" },
{ "text": "⬅️ Back", "callback_data": "ls:devices" }
]
]
Flow: Send
Trigger: INLINECODE19
Step 1 — Pick target device (if not already selected)
Run discover and show device picker (see Scan Devices flow above).
Step 2 — Choose what to send
Message:
CODEBLOCK11
Buttons:
CODEBLOCK12
Send File (callback_data: ls:sendfile)
- 1. Ask: INLINECODE21
- User provides file path or sends a file via chat
- Get file size with
stat or INLINECODE23 - Confirm with buttons:
Message:
CODEBLOCK13
Buttons:
CODEBLOCK14
- 5. On confirm, run:
CODEBLOCK15
- 6. Report result:
Message:
CODEBLOCK16
Buttons:
CODEBLOCK17
Send Text (callback_data: ls:sendtext)
- 1. Ask: INLINECODE25
- User types their message
- Write text to temp file, send:
echo "user's text" > /tmp/localsend-text.txt
localsend-cli send --to "Fast Potato" /tmp/localsend-text.txt
rm /tmp/localsend-text.txt
- 4. Confirm:
Message:
CODEBLOCK19
Buttons:
CODEBLOCK20
Send Multiple (callback_data: ls:sendmulti)
- 1. Ask: INLINECODE27
- User provides paths or pattern
- Expand glob, list files with sizes:
Message:
CODEBLOCK21
Buttons:
CODEBLOCK22
- 4. On confirm, run:
CODEBLOCK23
- 5. Report:
Message:
CODEBLOCK24
Buttons:
buttons: [
[
{ "text": "📤 Send More", "callback_data": "ls:send" },
{ "text": "⬅️ Menu", "callback_data": "ls:menu" }
]
]
Flow: Receive
Trigger: callback_data: ls:receive or user says "receive", "start receiving", "listen"
Step 1 — Snapshot current files
CODEBLOCK26
Step 2 — Start receiver in background
CODEBLOCK27
Run with run_in_background: true. Store the task ID.
CRITICAL: --alias MUST come BEFORE receive (global flag).
Step 3 — Confirm ready with buttons
Message:
CODEBLOCK28
Buttons:
CODEBLOCK29
Step 4 — Monitor for incoming files
Poll every 3 seconds for new files:
CODEBLOCK30
Step 5 — Post-receive confirmation (MANDATORY)
When file(s) arrive, immediately present in chat with inline buttons.
Single file:
Message:
CODEBLOCK31
Buttons (contextual by file type):
CODEBLOCK32
Image file — show inline preview:
Message:
✅ Received from Fast Potato:
🖼️ screenshot.png — 2.1 MB
Include
MEDIA:~/incoming/screenshot.png for inline preview.
Buttons:
CODEBLOCK34
Multiple files:
Message:
CODEBLOCK35
Buttons:
CODEBLOCK36
Contextual button rules by file type:
- -
.zip, .tar.gz → add 📂 Extract button - INLINECODE36 ,
.jpg, .gif, .webp → show MEDIA: inline + INLINECODE40 - INLINECODE41 → add
📱 Install button - INLINECODE43 ,
.js, .py → add 👁️ Preview button - website archives → add
🚀 Deploy button
Step 6 — Stop receiver
Trigger: INLINECODE48
- 1. Kill the background task using stored task ID
- Confirm:
Message:
CODEBLOCK37
Buttons:
buttons: [
[
{ "text": "📡 Restart", "callback_data": "ls:receive" },
{ "text": "⬅️ Menu", "callback_data": "ls:menu" }
]
]
Flow: Status Check
Trigger: INLINECODE49
Check if receiver is running and count new files:
CODEBLOCK39
Message:
CODEBLOCK40
Buttons:
buttons: [
[
{ "text": "🛑 Stop", "callback_data": "ls:stop" },
{ "text": "📂 Show Files", "callback_data": "ls:showall" }
]
]
Callback Data Reference
| callback_data | Action |
|---|
| INLINECODE50 | Show main menu |
| INLINECODE51 |
Start send flow |
|
ls:receive | Start receive flow |
|
ls:devices | Discover devices |
|
ls:dev:DEVICENAME | Select a specific device |
|
ls:sendfile | Send single file |
|
ls:sendtext | Send text message |
|
ls:sendmulti | Send multiple files |
|
ls:confirm-send | Confirm and execute send |
|
ls:stop | Stop receiver |
|
ls:status | Check receiver status |
|
ls:extract | Extract received archive |
|
ls:deploy | Deploy received website |
|
ls:openfolder | Open save directory |
|
ls:showall | List all received files |
CLI Reference
| Command | Usage |
|---|
| Discover | INLINECODE65 |
| Send |
localsend-cli send --to "DEVICE" file1 file2 ... |
| Receive |
localsend-cli --alias NAME receive --save-dir DIR -y |
| Flag | Scope | Description |
|---|
| INLINECODE68 | Global (before subcommand) | Device name to advertise |
| INLINECODE69 |
send | Target device (case-insensitive substring) |
|
-t N | discover | Scan duration in seconds (use 2 for speed) |
|
--json | discover | Machine-readable output |
|
--save-dir DIR | receive | Save location (default: ~/Downloads) |
|
-y | receive | Auto-accept transfers |
Troubleshooting
| Problem | Fix |
|---|
| INLINECODE74 | Move --alias BEFORE the subcommand |
| No devices found |
Open LocalSend on target, same WiFi, screen on |
| Port 53317 in use | Normal — CLI auto-falls back to 53318/53319 |
| Transfer declined (403) | Use
-y on receiver side |
| Transfer hangs | Large file on slow WiFi — be patient |
Reference
- - CLI repo & docs: https://github.com/Chordlini/localsend-cli
- LocalSend protocol:
references/protocol.md or https://github.com/localsend/protocol
LocalSend
使用真实的Telegram内联键盘按钮在本地网络上的设备之间进行交互式文件传输。适用于任何运行LocalSend应用的设备(Android、iOS、Windows、macOS、Linux)。
安装
localsend-cli是一个零依赖的Python CLI工具。从GitHub安装:
bash
curl -fsSL https://raw.githubusercontent.com/Chordlini/localsend-cli/master/localsend-cli -o ~/.local/bin/localsend-cli
chmod +x ~/.local/bin/localsend-cli
完整文档:https://github.com/Chordlini/localsend-cli
需要Python 3.8+和openssl(用于TLS)。
Telegram按钮格式
所有菜单必须使用OpenClaw的内联按钮格式。使用此结构将按钮与消息一起发送:
json
buttons: [
[{ text: 标签, callback_data: ls:action }],
[{ text: 第二行, callback_data: ls:other }]
]
- - 外层数组 = 按钮行
- 内层数组 = 每行的按钮(为保持可读性,每行最多3个)
- 所有callbackdata前缀为ls:以命名此技能
- 当用户点击按钮时,您将收到:callbackdata: ls:action
状态感知(关键)
此技能使用对话状态。跟踪您在流程中的位置:
| 状态 | 含义 | 下一个用户输入应被视为... |
|---|
| idle | 无活动流程 | 正常消息 — 正常回复 |
| awaiting_file |
已要求用户拖放/指定要发送的文件 |
要发送的文件 — 不要评论、描述或回应它。立即将其用作发送负载。 |
| awaiting_text | 已要求用户输入要发送的文本 |
文本负载 — 发送它,不要讨论它 |
| awaiting_confirm | 等待发送确认 | 期望ls:confirm-send或ls:menu |
| receiving | 接收端活跃 | 监控传入文件 |
规则:
- - 当处于awaitingfile状态且用户发送图片/文件/路径时 → 将其视为要发送的文件。立即显示确认按钮。
- 当处于awaitingtext状态且用户输入任何内容时 → 将其视为要发送的文本。
- 当处于awaiting_file状态时,永远不要评论、描述或回应文件/图片。
- 当用户点击ls:menu或流程完成时,状态重置为idle。
触发时:主菜单
当用户输入/localsend或提到本地发送/接收文件时,发送此消息并附带真实内联按钮:
消息:
📡 LocalSend — 文件传输
按钮:
json
buttons: [
[
{ text: 📤 发送, callback_data: ls:send },
{ text: 📥 接收, callback_data: ls:receive }
],
[
{ text: 🔍 扫描设备, callback_data: ls:devices }
]
]
暂时不要运行任何命令。等待按钮点击。
流程:扫描设备
触发: callback_data: ls:devices或用户说扫描、发现、查找设备
- 1. 运行:
bash
localsend-cli discover --json -t 2
- 2. 找到设备 — 为每个设备创建一个按钮,加上刷新和返回按钮:
消息:
📡 找到3个设备:
按钮(每行一个设备):
json
buttons: [
[{ text: 📱 Fast Potato — 192.168.0.148, callback_data: ls:dev:Fast Potato }],
[{ text: 💻 Rami-Desktop — 192.168.0.100, callback_data: ls:dev:Rami-Desktop }],
[{ text: 🖥️ Living Room PC — 192.168.0.105, callback_data: ls:dev:Living Room PC }],
[
{ text: 🔄 刷新, callback_data: ls:devices },
{ text: ⬅️ 返回, callback_data: ls:menu }
]
]
- 3. 未找到设备:
消息:
📡 未找到设备。
请确保LocalSend在另一台设备上打开,并且两台设备连接到同一个WiFi。
按钮:
json
buttons: [
[
{ text: 🔄 重试, callback_data: ls:devices },
{ text: ⬅️ 返回, callback_data: ls:menu }
]
]
- 4. 用户点击设备(callback_data: ls:dev:DEVICENAME)— 将其存储为选定的目标。显示操作菜单:
消息:
✅ 已选择:Fast Potato (192.168.0.148)
您想做什么?
按钮:
json
buttons: [
[
{ text: 📄 发送文件, callback_data: ls:sendfile },
{ text: 📝 发送文本, callback_data: ls:sendtext }
],
[
{ text: 📦 发送多个, callback_data: ls:sendmulti },
{ text: ⬅️ 返回, callback_data: ls:devices }
]
]
流程:发送
触发: callback_data: ls:send
第1步 — 选择目标设备(如果尚未选择)
运行发现并显示设备选择器(参见上面的扫描设备流程)。
第2步 — 选择要发送的内容
消息:
发送到 Fast Potato:
按钮:
json
buttons: [
[
{ text: 📄 发送文件, callback_data: ls:sendfile },
{ text: 📝 发送文本, callback_data: ls:sendtext }
],
[
{ text: 📦 发送多个, callback_data: ls:sendmulti },
{ text: ⬅️ 返回, callback_data: ls:menu }
]
]
发送文件(callback_data: ls:sendfile)
- 1. 询问:发送文件给我,拖放路径,或告诉我发送哪个文件
- 用户提供文件路径或通过聊天发送文件
- 使用stat或ls -lh获取文件大小
- 使用按钮确认:
消息:
📤 发送到 Fast Potato?
📄 project.zip — 4.2 MB
按钮:
json
buttons: [
[
{ text: ✅ 发送, callback_data: ls:confirm-send },
{ text: ❌ 取消, callback_data: ls:menu }
]
]
- 5. 确认后,运行:
bash
localsend-cli send --to Fast Potato /path/to/project.zip
- 6. 报告结果:
消息:
✅ 已将 project.zip (4.2 MB) 发送到 Fast Potato
按钮:
json
buttons: [
[
{ text: 📤 发送另一个, callback_data: ls:send },
{ text: ⬅️ 菜单, callback_data: ls:menu }
]
]
发送文本(callback_data: ls:sendtext)
- 1. 询问:输入您要发送的文本:
- 用户输入他们的消息
- 将文本写入临时文件,发送:
bash
echo 用户的文本 > /tmp/localsend-text.txt
localsend-cli send --to Fast Potato /tmp/localsend-text.txt
rm /tmp/localsend-text.txt
- 4. 确认:
消息:
✅ 文本已发送到 Fast Potato
按钮:
json
buttons: [
[
{ text: 📝 发送更多文本, callback_data: ls:sendtext },
{ text: 📤 发送文件, callback_data: ls:sendfile }
],
[{ text: ⬅️ 菜单, callback_data: ls:menu }]