mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-14 03:30:19 +00:00
✨ feat(bot): add iMessage Desktop setup and bridge (#15228)
✨ feat(bot): add iMessage Desktop bridge with Labs gate Desktop-side BlueBubbles bridge for the iMessage channel: - Bridge runtime (ImessageBridgeCtr/Srv) + gateway message_api_request routing; chat-adapter-imessage api lists all webhooks instead of the 500-prone url filter (first-time save no longer fails). - iMessage channel UI: desktopDeviceId + webhookSecret are auto-filled/generated (not user fields); a single "Save Configuration" persists both the cloud provider and the local bridge via a post-save extension point — no separate "Save Bridge" button. - Gated behind the `enableImessage` Labs preference (off → "Coming Soon"). - Group local-testing bot skills into per-channel folders + add iMessage bridge/outbound regression scripts. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -397,35 +397,60 @@ The pattern is the same for every platform:
|
||||
|
||||
Pick the file for your target platform — each contains activation, navigation, send-message, and verification snippets specific to that app:
|
||||
|
||||
| Platform | Reference | Quick switcher |
|
||||
| ------------- | -------------------------------------------------- | -------------- |
|
||||
| Discord | [references/discord.md](./references/discord.md) | `Cmd+K` |
|
||||
| Slack | [references/slack.md](./references/slack.md) | `Cmd+K` |
|
||||
| Telegram | [references/telegram.md](./references/telegram.md) | `Cmd+F` |
|
||||
| WeChat / 微信 | [references/wechat.md](./references/wechat.md) | `Cmd+F` |
|
||||
| Lark / 飞书 | [references/lark.md](./references/lark.md) | `Cmd+K` |
|
||||
| QQ | [references/qq.md](./references/qq.md) | `Cmd+F` |
|
||||
Each channel has its own folder under `bot/<channel>/` containing an `index.md`
|
||||
(activation, navigation, send-message, and verification snippets specific to
|
||||
that app) and its test script:
|
||||
|
||||
For **shared osascript patterns** (activate, type, paste, screenshot, read accessibility, common workflow template, gotchas), see [references/osascript-common.md](./references/osascript-common.md). Read this first if you're new to osascript automation.
|
||||
| Platform | Reference | Quick switcher |
|
||||
| ------------- | ------------------------------------------------ | -------------- |
|
||||
| Discord | [bot/discord/index.md](./bot/discord/index.md) | `Cmd+K` |
|
||||
| Slack | [bot/slack/index.md](./bot/slack/index.md) | `Cmd+K` |
|
||||
| Telegram | [bot/telegram/index.md](./bot/telegram/index.md) | `Cmd+F` |
|
||||
| WeChat / 微信 | [bot/wechat/index.md](./bot/wechat/index.md) | `Cmd+F` |
|
||||
| Lark / 飞书 | [bot/lark/index.md](./bot/lark/index.md) | `Cmd+K` |
|
||||
| QQ | [bot/qq/index.md](./bot/qq/index.md) | `Cmd+F` |
|
||||
|
||||
For **shared osascript patterns** (activate, type, paste, screenshot, read accessibility, common workflow template, gotchas), see [bot/osascript-common.md](./bot/osascript-common.md). Read this first if you're new to osascript automation.
|
||||
|
||||
## Bridge-based channels (no native app)
|
||||
|
||||
Some channels have no native app to drive with osascript — they connect through
|
||||
a local bridge inside the Desktop app. These are tested with agent-browser
|
||||
(IPC + UI) plus the bridge's own HTTP/REST endpoints, not osascript:
|
||||
|
||||
| Channel | Reference | What it drives |
|
||||
| -------- | ------------------------------------------------ | -------------------------------------------------------- |
|
||||
| iMessage | [bot/imessage/index.md](./bot/imessage/index.md) | `imessageBridge.*` IPC + local bridge + BlueBubbles REST |
|
||||
|
||||
For iMessage there is a one-shot regression script — see `test-imessage-bridge.sh` below.
|
||||
|
||||
---
|
||||
|
||||
# Scripts
|
||||
|
||||
Ready-to-use scripts in `.agents/skills/local-testing/scripts/`:
|
||||
**App / recording scripts** in `.agents/skills/local-testing/scripts/`:
|
||||
|
||||
| Script | Usage |
|
||||
| ------------------------- | --------------------------------------------------- |
|
||||
| `electron-dev.sh` | Manage Electron dev env (start/stop/status/restart) |
|
||||
| `capture-app-window.sh` | Capture screenshot of a specific app window |
|
||||
| `record-electron-demo.sh` | Record Electron app demo with ffmpeg |
|
||||
| `record-app-screen.sh` | Record app screen (video + screenshots, start/stop) |
|
||||
| `test-discord-bot.sh` | Send message to Discord bot via osascript |
|
||||
| `test-slack-bot.sh` | Send message to Slack bot via osascript |
|
||||
| `test-telegram-bot.sh` | Send message to Telegram bot via osascript |
|
||||
| `test-wechat-bot.sh` | Send message to WeChat bot via osascript |
|
||||
| `test-lark-bot.sh` | Send message to Lark / 飞书 bot via osascript |
|
||||
| `test-qq-bot.sh` | Send message to QQ bot via osascript |
|
||||
|
||||
**Bot scripts** live under `.agents/skills/local-testing/bot/`, one folder per
|
||||
channel (alongside that channel's `index.md`). The shared
|
||||
`capture-app-window.sh` sits at the `bot/` root:
|
||||
|
||||
| Script | Usage |
|
||||
| ---------------------------------- | ------------------------------------------------------------------- |
|
||||
| `capture-app-window.sh` | Capture screenshot of a specific app window (used by bot tests) |
|
||||
| `discord/test-discord-bot.sh` | Send message to Discord bot via osascript |
|
||||
| `slack/test-slack-bot.sh` | Send message to Slack bot via osascript |
|
||||
| `telegram/test-telegram-bot.sh` | Send message to Telegram bot via osascript |
|
||||
| `wechat/test-wechat-bot.sh` | Send message to WeChat bot via osascript |
|
||||
| `lark/test-lark-bot.sh` | Send message to Lark / 飞书 bot via osascript |
|
||||
| `qq/test-qq-bot.sh` | Send message to QQ bot via osascript |
|
||||
| `imessage/test-imessage-bridge.sh` | Regression-test the iMessage BlueBubbles bridge (IPC + HTTP) |
|
||||
| `imessage/send-imessage-test.sh` | Send one real iMessage (desktop → BB → iMessage) and verify it sent |
|
||||
|
||||
### Window Screenshot Utility
|
||||
|
||||
@@ -433,9 +458,9 @@ Ready-to-use scripts in `.agents/skills/local-testing/scripts/`:
|
||||
|
||||
```bash
|
||||
# Standalone usage
|
||||
./.agents/skills/local-testing/scripts/capture-app-window.sh "Discord" /tmp/discord.png
|
||||
./.agents/skills/local-testing/scripts/capture-app-window.sh "Slack" /tmp/slack.png
|
||||
./.agents/skills/local-testing/scripts/capture-app-window.sh "WeChat" /tmp/wechat.png
|
||||
./.agents/skills/local-testing/bot/capture-app-window.sh "Discord" /tmp/discord.png
|
||||
./.agents/skills/local-testing/bot/capture-app-window.sh "Slack" /tmp/slack.png
|
||||
./.agents/skills/local-testing/bot/capture-app-window.sh "WeChat" /tmp/wechat.png
|
||||
```
|
||||
|
||||
All bot test scripts use this utility automatically for their screenshots.
|
||||
@@ -452,32 +477,48 @@ Examples:
|
||||
|
||||
```bash
|
||||
# Discord — test a bot in #bot-testing channel
|
||||
./.agents/skills/local-testing/scripts/test-discord-bot.sh "bot-testing" "!ping"
|
||||
./.agents/skills/local-testing/scripts/test-discord-bot.sh "bot-testing" "/ask Tell me a joke" 30
|
||||
./.agents/skills/local-testing/bot/discord/test-discord-bot.sh "bot-testing" "!ping"
|
||||
./.agents/skills/local-testing/bot/discord/test-discord-bot.sh "bot-testing" "/ask Tell me a joke" 30
|
||||
|
||||
# Slack — test a bot in #bot-testing channel
|
||||
./.agents/skills/local-testing/scripts/test-slack-bot.sh "bot-testing" "@mybot hello"
|
||||
./.agents/skills/local-testing/scripts/test-slack-bot.sh "bot-testing" "/ask What is 2+2?" 20
|
||||
./.agents/skills/local-testing/bot/slack/test-slack-bot.sh "bot-testing" "@mybot hello"
|
||||
./.agents/skills/local-testing/bot/slack/test-slack-bot.sh "bot-testing" "/ask What is 2+2?" 20
|
||||
|
||||
# Telegram — test a bot by username
|
||||
./.agents/skills/local-testing/scripts/test-telegram-bot.sh "MyTestBot" "/start"
|
||||
./.agents/skills/local-testing/scripts/test-telegram-bot.sh "GPTBot" "Hello" 60
|
||||
./.agents/skills/local-testing/bot/telegram/test-telegram-bot.sh "MyTestBot" "/start"
|
||||
./.agents/skills/local-testing/bot/telegram/test-telegram-bot.sh "GPTBot" "Hello" 60
|
||||
|
||||
# WeChat — test a bot or send to a contact
|
||||
./.agents/skills/local-testing/scripts/test-wechat-bot.sh "文件传输助手" "test message" 5
|
||||
./.agents/skills/local-testing/scripts/test-wechat-bot.sh "MyBot" "Tell me a joke" 30
|
||||
./.agents/skills/local-testing/bot/wechat/test-wechat-bot.sh "文件传输助手" "test message" 5
|
||||
./.agents/skills/local-testing/bot/wechat/test-wechat-bot.sh "MyBot" "Tell me a joke" 30
|
||||
|
||||
# Lark/飞书 — test a bot in a group chat
|
||||
./.agents/skills/local-testing/scripts/test-lark-bot.sh "bot-testing" "@MyBot hello"
|
||||
./.agents/skills/local-testing/scripts/test-lark-bot.sh "bot-testing" "Help me with this" 30
|
||||
./.agents/skills/local-testing/bot/lark/test-lark-bot.sh "bot-testing" "@MyBot hello"
|
||||
./.agents/skills/local-testing/bot/lark/test-lark-bot.sh "bot-testing" "Help me with this" 30
|
||||
|
||||
# QQ — test a bot in a group or direct chat
|
||||
./.agents/skills/local-testing/scripts/test-qq-bot.sh "bot-testing" "Hello bot" 15
|
||||
./.agents/skills/local-testing/scripts/test-qq-bot.sh "MyBot" "/help" 10
|
||||
./.agents/skills/local-testing/bot/qq/test-qq-bot.sh "bot-testing" "Hello bot" 15
|
||||
./.agents/skills/local-testing/bot/qq/test-qq-bot.sh "MyBot" "/help" 10
|
||||
```
|
||||
|
||||
Each script: activates the app, navigates to the channel/contact, pastes the message via clipboard, sends, waits, and takes a screenshot. Use the `Read` tool on the screenshot for visual verification.
|
||||
|
||||
### iMessage bridge regression script
|
||||
|
||||
`test-imessage-bridge.sh` does **not** follow the osascript bot interface — it
|
||||
drives the Desktop bridge's IPC + HTTP layers and asserts the result, then
|
||||
self-cleans. Needs BlueBubbles running and Electron up with CDP.
|
||||
|
||||
```bash
|
||||
./.agents/skills/local-testing/bot/imessage/test-imessage-bridge.sh '<bluebubbles_password>' [bb_url] [cdp_port]
|
||||
# defaults: bb_url=http://127.0.0.1:1234 cdp_port=9222 — exit 0 = all green
|
||||
```
|
||||
|
||||
It guards the connect/configure flow (testConfig happy + reject paths, first-time
|
||||
`upsertConfig` save, bridge running + webhook registered, local-server secret
|
||||
enforcement). See [bot/imessage/index.md](./bot/imessage/index.md)
|
||||
for the full manual UI flow and known bugs.
|
||||
|
||||
---
|
||||
|
||||
# Screen Recording
|
||||
@@ -517,4 +558,4 @@ Outputs to `.records/` directory (gitignored): `<name>.mp4` (video) + `<name>/`
|
||||
|
||||
### osascript
|
||||
|
||||
See [references/osascript-common.md](./references/osascript-common.md#gotchas) for the full osascript gotchas list (accessibility permissions, `keystroke` non-ASCII issues, locale-specific app names, rate limiting, etc.).
|
||||
See [bot/osascript-common.md](./bot/osascript-common.md#gotchas) for the full osascript gotchas list (accessibility permissions, `keystroke` non-ASCII issues, locale-specific app names, rate limiting, etc.).
|
||||
|
||||
+3
-3
@@ -2,7 +2,7 @@
|
||||
|
||||
**App name:** `Discord` | **Process name:** `Discord`
|
||||
|
||||
See [osascript-common.md](./osascript-common.md) for shared patterns.
|
||||
See [osascript-common.md](../osascript-common.md) for shared patterns.
|
||||
|
||||
## Activate & Navigate
|
||||
|
||||
@@ -92,6 +92,6 @@ echo "Screenshot saved to /tmp/discord-test-result.png"
|
||||
## Script
|
||||
|
||||
```bash
|
||||
./.agents/skills/local-testing/scripts/test-discord-bot.sh "bot-testing" "!ping"
|
||||
./.agents/skills/local-testing/scripts/test-discord-bot.sh "bot-testing" "/ask Tell me a joke" 30
|
||||
./.agents/skills/local-testing/bot/discord/test-discord-bot.sh "bot-testing" "!ping"
|
||||
./.agents/skills/local-testing/bot/discord/test-discord-bot.sh "bot-testing" "/ask Tell me a joke" 30
|
||||
```
|
||||
+1
-1
@@ -60,5 +60,5 @@ echo "[$APP] Waiting ${WAIT}s for bot response..."
|
||||
sleep "$WAIT"
|
||||
|
||||
echo "[$APP] Capturing screenshot..."
|
||||
"$SCRIPT_DIR/capture-app-window.sh" "$APP" "$SCREENSHOT"
|
||||
"$SCRIPT_DIR/../capture-app-window.sh" "$APP" "$SCREENSHOT"
|
||||
echo "[$APP] Done! Screenshot saved to $SCREENSHOT"
|
||||
@@ -0,0 +1,232 @@
|
||||
# iMessage Desktop bridge regression test
|
||||
|
||||
The iMessage channel is different from the other bot platforms: there is **no
|
||||
native app to drive with osascript**. Instead the Desktop app runs a local
|
||||
**BlueBubbles bridge** — a small HTTP server in the Electron main process that
|
||||
registers a webhook on a local [BlueBubbles](https://bluebubbles.app/) server,
|
||||
receives iMessage events, and forwards them to LobeHub Cloud.
|
||||
|
||||
So the test surface is three layers:
|
||||
|
||||
1. **Electron main IPC** — `imessageBridge.*` handlers (`getStatus`,
|
||||
`testConfig`, `upsertConfig`, `removeConfig`, `start`, `stop`)
|
||||
2. **Local bridge HTTP server** — `http://127.0.0.1:<port>/webhooks/bluebubbles/<appId>?secret=<secret>`
|
||||
3. **BlueBubbles REST API** — `http://127.0.0.1:1234/api/v1/*` (webhook + server/info)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A running **BlueBubbles server** (macOS, default `http://127.0.0.1:1234`) with
|
||||
a known password. Sanity check:
|
||||
```bash
|
||||
curl -sS -m4 -o /dev/null -w '%{http_code}\n' \
|
||||
"http://127.0.0.1:1234/api/v1/server/info?password=<PW>" # expect 200
|
||||
```
|
||||
- **Electron dev running with CDP**: `./.agents/skills/local-testing/scripts/electron-dev.sh start`
|
||||
- The **iMessage Desktop branch** checked out (the `imessageBridge` IPC group
|
||||
and `@lobechat/chat-adapter-imessage` must be compiled into the main bundle).
|
||||
Run `pnpm install --ignore-scripts` at the repo root **and** in `apps/desktop/`
|
||||
after switching branches — the new workspace package must be linked or the
|
||||
main build fails to resolve `@lobechat/chat-adapter-imessage`.
|
||||
|
||||
## Fast path: automated script
|
||||
|
||||
```bash
|
||||
./.agents/skills/local-testing/bot/imessage/test-imessage-bridge.sh '<bluebubbles_password>' [bb_url] [cdp_port]
|
||||
```
|
||||
|
||||
Asserts the whole flow and self-cleans (unique `applicationId` per run, removes
|
||||
its bridge config + BlueBubbles webhook on exit). Exit 0 = all green. It covers:
|
||||
|
||||
- BlueBubbles reachable + password valid; Electron CDP reachable; IPC available
|
||||
- `testConfig` happy path → success
|
||||
- `testConfig` wrong password → rejected; unreachable URL → rejected
|
||||
- `upsertConfig` **first-time save → success** (Bug #1 regression guard, below)
|
||||
- `getStatus` → `running:true`, config persisted, password redacted (`blueBubblesPasswordSet`)
|
||||
- BlueBubbles webhook actually registered for the appId
|
||||
- Local bridge HTTP server: wrong secret → 401; valid secret → past auth
|
||||
|
||||
The password is passed as argv (visible in `ps`) — local dev only, don't use a
|
||||
real secret on a shared machine.
|
||||
|
||||
## Layer 1 — IPC probes (no UI)
|
||||
|
||||
The renderer exposes the main-process handlers via `window.electronAPI.invoke`.
|
||||
This is the quickest way to exercise the bridge without clicking:
|
||||
|
||||
```bash
|
||||
# baseline
|
||||
agent-browser --cdp 9222 eval \
|
||||
"(async()=>JSON.stringify(await window.electronAPI.invoke('imessageBridge.getStatus',{})))()"
|
||||
|
||||
# test a connection (note: password as a JS string)
|
||||
agent-browser --cdp 9222 eval --stdin << 'EVALEOF'
|
||||
(async function () {
|
||||
try {
|
||||
var r = await window.electronAPI.invoke('imessageBridge.testConfig', {
|
||||
applicationId: 'probe',
|
||||
blueBubblesServerUrl: 'http://127.0.0.1:1234',
|
||||
blueBubblesPassword: 'PASTE_PW',
|
||||
enabled: true,
|
||||
webhookSecret: 'probe-secret',
|
||||
});
|
||||
return JSON.stringify(r); // { success: true }
|
||||
} catch (e) { return 'ERR: ' + (e.message || e); }
|
||||
})()
|
||||
EVALEOF
|
||||
```
|
||||
|
||||
`upsertConfig` persists to the Electron store, starts the local HTTP server, and
|
||||
registers the BlueBubbles webhook. `removeConfig` + `stop` reverse it.
|
||||
|
||||
## Layer 2 — full UI flow (agent-browser)
|
||||
|
||||
The bridge settings only render in Desktop (`isDesktop` guard) under the agent's
|
||||
**Channel → iMessage** screen. The platform tile only appears as a real (non
|
||||
"Coming Soon") entry once the server registers `imessage` **and** the frontend
|
||||
drops it from `COMING_SOON_PLATFORMS` (`src/routes/(main)/agent/channel/const.ts`).
|
||||
|
||||
```bash
|
||||
agent-browser --cdp 9222 open "http://localhost:5173/agent/<aid>/channel"
|
||||
agent-browser --cdp 9222 wait --load networkidle && agent-browser --cdp 9222 wait 1500
|
||||
|
||||
# confirm the remote backend lists imessage (it must be registered + deployed)
|
||||
agent-browser --cdp 9222 eval --stdin << 'EVALEOF'
|
||||
(async function(){
|
||||
var url='lobe-backend://lobe/trpc/lambda/agentBotProvider.listPlatforms?input='+encodeURIComponent('{"json":null,"meta":{"values":["undefined"],"v":1}}');
|
||||
var d=await (await fetch(url,{credentials:'include'})).json();
|
||||
var p=d.result?.data?.json||d;
|
||||
return JSON.stringify(p.map(function(x){return x.id;}));
|
||||
})()
|
||||
EVALEOF
|
||||
|
||||
# click the iMessage tile, then fill the form by ref
|
||||
agent-browser --cdp 9222 eval "(()=>{var b=[...document.querySelectorAll('aside button')].find(x=>/imessage/i.test(x.textContent));b&&b.click();})()"
|
||||
agent-browser --cdp 9222 wait 1500
|
||||
agent-browser --cdp 9222 snapshot -i | grep -iE "127.0.0.1:1234|Application ID|Webhook Secret|Test BlueBubbles|Save Bridge"
|
||||
```
|
||||
|
||||
Field refs (from the snapshot): Application ID, Webhook Secret, BlueBubbles
|
||||
Server URL (`placeholder="http://127.0.0.1:1234"`), and a **nested** textbox right
|
||||
under the URL one is the BlueBubbles Password. Fill with `fill` (real input
|
||||
events — `eval`-setting React inputs won't fire onChange), click **Test
|
||||
BlueBubbles**, then **Save Bridge**. Read the antd toast immediately (it
|
||||
auto-dismisses):
|
||||
|
||||
```bash
|
||||
agent-browser --cdp 9222 eval \
|
||||
"JSON.stringify([...new Set([...document.querySelectorAll('.ant-message-custom-content')].map(n=>n.textContent.trim()))])"
|
||||
# Test → "BlueBubbles connection passed"
|
||||
# Save → "iMessage Desktop bridge saved"
|
||||
```
|
||||
|
||||
Verify the end state via BlueBubbles + IPC:
|
||||
|
||||
```bash
|
||||
curl -sS "http://127.0.0.1:1234/api/v1/webhook?password=<PW>" # webhook for the appId present
|
||||
agent-browser --cdp 9222 eval "(async()=>JSON.stringify(await window.electronAPI.invoke('imessageBridge.getStatus',{})))()"
|
||||
# running:true, serverUrl: http://127.0.0.1:33270, configs[].blueBubblesPasswordSet:true
|
||||
```
|
||||
|
||||
Cleanup: `removeConfig` + `stop` via IPC, then `DELETE /api/v1/webhook/<id>` on
|
||||
BlueBubbles.
|
||||
|
||||
## Outbound send test (desktop → BlueBubbles → iMessage)
|
||||
|
||||
Verifies the leg the bridge uses to _reply_: `BlueBubblesApiClient.sendText`
|
||||
→ `POST /api/v1/message/text`. Run the helper against your own number:
|
||||
|
||||
```bash
|
||||
./.agents/skills/local-testing/bot/imessage/send-imessage-test.sh '<bb_password>' '+<E164>' # e.g. +15551234567
|
||||
```
|
||||
|
||||
**Gotcha that bites everyone:** with `method=apple-script` and a _new_
|
||||
conversation, the HTTP POST often **times out** even though the message is
|
||||
sent. Never judge success by the HTTP response. Instead poll
|
||||
`POST /api/v1/message/query` and read the matching `isFromMe:true` row's
|
||||
`error` field:
|
||||
|
||||
- `error: 0` (or null) → sent OK
|
||||
- non-zero `error` → real send failure
|
||||
|
||||
The script does exactly this: fires the send, ignores the timeout, then matches
|
||||
its marker text in the message store and asserts `error == 0`.
|
||||
|
||||
Two more notes:
|
||||
|
||||
- Use a full E.164 handle (`iMessage;-;+<countrycode><number>`) or an Apple ID
|
||||
email. Looking the chat up by guid afterwards may 404 if BB filed the message
|
||||
under a differently-formatted guid — that's a lookup quirk, not a send failure.
|
||||
- Sending to _your own_ number round-trips: BB records both the outgoing
|
||||
(`fromMe:true`) and an incoming copy (`fromMe:false`).
|
||||
|
||||
## Inbound e2e test (iMessage → cloud agent → reply)
|
||||
|
||||
Full inbound chain: a message arrives → BlueBubbles fires its `new-message`
|
||||
webhook → local bridge (`:33270`) → `forwardWebhook` POSTs to
|
||||
`<remote>/api/agent/webhooks/imessage/<appId>?secret=…` → cloud agent → reply
|
||||
flows back via Device Gateway → BB `sendText`.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- A cloud bot provider for the same `applicationId` exists and is **connected**
|
||||
(Save Configuration + the device gateway connected — a _disconnected_ gateway
|
||||
yields `DEVICE_NOT_FOUND` on connect and blocks the reply leg).
|
||||
- The `imessage` Labs toggle is on (otherwise the channel is gated to "Coming
|
||||
Soon"), and `webhookSecret` matches on both ends (auto-generated on save).
|
||||
|
||||
Two ways to drive it:
|
||||
|
||||
1. **Second device / Apple ID (recommended).** Have _another_ Apple ID message
|
||||
the BB-hosted number (e.g. "please reply pong"). The bot replies; you see it
|
||||
on the other device. **No loop risk** — the reply goes to the other party,
|
||||
not back to itself.
|
||||
2. **Send to your own number (quick, loop-aware).** `sendText` to the hosted
|
||||
number; the loopback _incoming_ copy (`isFromMe:false`) triggers the bot.
|
||||
Watch the reply land in `message/query` as a `fromMe:true` row.
|
||||
|
||||
**Loop guard — why a self-send doesn't spin forever:** the Chat SDK adapter
|
||||
drops any `isFromMe` message before dispatch
|
||||
(`packages/chat-adapter-imessage/src/adapter.ts`: `if (message.isFromMe) return`).
|
||||
The bot's own reply (`isFromMe:true`) is never re-processed, so in the normal
|
||||
case (someone else → bot → reply to them) there is no loop. The self-send case
|
||||
is a **test-only edge**: the bot's reply also round-trips to your number, and
|
||||
only the adapter's `isFromMe` check stops a second pass. Keep the prompt
|
||||
conversational (so the bot doesn't keep finding something to answer), and
|
||||
**turn the `imessage` lab off / remove the config when done** — never leave a
|
||||
self-send bot running unattended.
|
||||
|
||||
Watch the chain live:
|
||||
|
||||
```bash
|
||||
tail -f /tmp/electron-dev.log | grep -iE "imessage|bridge|forward|Message API"
|
||||
# the agent reply shows up as a fromMe:true row with the bot's text:
|
||||
curl -sS -X POST "http://127.0.0.1:1234/api/v1/message/query?password=<PW>" \
|
||||
-H 'Content-Type: application/json' -d '{"limit":5,"sort":"DESC"}'
|
||||
```
|
||||
|
||||
`startTyping` will log a Private-API error unless BlueBubbles has the Private
|
||||
API helper set up (needs a jailbroken / SIP-disabled Mac) — it's logged and
|
||||
ignored; text replies still work.
|
||||
|
||||
## Known bugs / gotchas
|
||||
|
||||
- **Bug #1 — first-time save (fixed; guarded by the script).** BlueBubbles'
|
||||
`GET /api/v1/webhook?url=<unregistered>` returns **HTTP 500**
|
||||
(`Cannot read properties of null (reading 'events')`). The bridge must list
|
||||
**all** webhooks and match client-side, never pass the `?url=` filter. If you
|
||||
see `upsertConfig` fail with "An unhandled error has occurred!" originating in
|
||||
`listWebhooks`, this regressed.
|
||||
- **Save leaves a half-state on webhook failure.** `upsertConfig` writes the
|
||||
config + starts the HTTP server _before_ registering the webhook, so a webhook
|
||||
failure still reports `running:true` with the config persisted but no
|
||||
BlueBubbles webhook. Always assert the BlueBubbles webhook list, not just IPC
|
||||
status.
|
||||
- **Unknown appId / forward failure → 500.** Posting to the local bridge for an
|
||||
unknown appId, or when no cloud bot is bound, returns 500 (BlueBubbles retries
|
||||
on 5xx). Auth (wrong secret → 401) is enforced before that.
|
||||
- **Backend deploy lag.** Desktop dev proxies tRPC through `lobe-backend://` to
|
||||
the _remote_ server. iMessage only appears in `listPlatforms` once the server
|
||||
registration is deployed there, regardless of local branch.
|
||||
- **Restart to load main-process fixes.** Editing `imessageBridgeSrv.ts` /
|
||||
`@lobechat/chat-adapter-imessage` needs `electron-dev.sh restart` — main isn't
|
||||
hot-replaced. On restart, enabled configs auto-register their webhook again.
|
||||
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# send-imessage-test.sh — Verify the outbound leg: desktop → BlueBubbles → iMessage
|
||||
#
|
||||
# Sends one real iMessage via the same REST call the Desktop bridge uses
|
||||
# (`POST /api/v1/message/text`, which BlueBubblesApiClient.sendText wraps) and
|
||||
# confirms it actually went out.
|
||||
#
|
||||
# KEY GOTCHA: with method=apple-script and a NEW conversation, the HTTP request
|
||||
# often TIMES OUT even though the message is sent. Do NOT treat the timeout as a
|
||||
# failure — instead poll `POST /api/v1/message/query` and check the message's
|
||||
# `error` field (0 = sent OK). This script does that for you.
|
||||
#
|
||||
# This sends a REAL message, so it has side effects. Target your own number.
|
||||
#
|
||||
# Usage:
|
||||
# ./send-imessage-test.sh <bb_password> <target_e164> [message] [bb_url]
|
||||
#
|
||||
# Example (send to your own phone, E.164 with country code):
|
||||
# ./send-imessage-test.sh 'my-bb-pass' '+15551234567'
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
BB_PASS="${1:?Usage: $0 <bb_password> <target_e164(+countrycode)> [message] [bb_url]}"
|
||||
TARGET="${2:?Need a target handle in E.164, e.g. +15551234567 (or an Apple ID email)}"
|
||||
MARKER="lobe-imsg-test-$(date +%s)"
|
||||
MESSAGE="${3:-[${MARKER}] desktop bridge → BlueBubbles → iMessage outbound check}"
|
||||
BB_URL="${4:-http://127.0.0.1:1234}"
|
||||
|
||||
CHAT_GUID="iMessage;-;${TARGET}"
|
||||
|
||||
echo "[send-test] target=${TARGET} marker=${MARKER}"
|
||||
|
||||
# 1) Fire the send. apple-script on a new chat may hang the HTTP response, so we
|
||||
# cap it short and ignore a timeout — step 2 is the source of truth.
|
||||
python3 - "$BB_PASS" "$BB_URL" "$CHAT_GUID" "$MESSAGE" <<'PY' || true
|
||||
import json,sys,urllib.request,urllib.parse,uuid
|
||||
pw,base,guid,msg=sys.argv[1:5]
|
||||
url=base+"/api/v1/message/text?password="+urllib.parse.quote(pw)
|
||||
body={"chatGuid":guid,"message":msg,"method":"apple-script","tempGuid":str(uuid.uuid4())}
|
||||
req=urllib.request.Request(url,data=json.dumps(body).encode("utf-8"),
|
||||
headers={"Content-Type":"application/json"},method="POST")
|
||||
try:
|
||||
r=urllib.request.urlopen(req,timeout=8)
|
||||
print("[send-test] HTTP",r.status,"(immediate response)")
|
||||
except urllib.error.HTTPError as e:
|
||||
print("[send-test] HTTP",e.code,e.read().decode()[:200])
|
||||
except Exception as e:
|
||||
print("[send-test] HTTP request returned no body (likely apple-script delay):",type(e).__name__)
|
||||
PY
|
||||
|
||||
# 2) Source of truth: find our marker in the message store and read its error.
|
||||
echo "[send-test] verifying via message/query (the HTTP timeout above is expected)…"
|
||||
sleep 3
|
||||
python3 - "$BB_PASS" "$BB_URL" "$MARKER" <<'PY'
|
||||
import json,sys,time,urllib.request,urllib.parse
|
||||
pw,base,marker=sys.argv[1:4]
|
||||
url=base+"/api/v1/message/query?password="+urllib.parse.quote(pw)
|
||||
def query():
|
||||
body={"limit":15,"offset":0,"with":["chats"],"sort":"DESC"}
|
||||
req=urllib.request.Request(url,data=json.dumps(body).encode(),
|
||||
headers={"Content-Type":"application/json"},method="POST")
|
||||
return json.load(urllib.request.urlopen(req,timeout=12)).get("data") or []
|
||||
hit=None
|
||||
for _ in range(5):
|
||||
for m in query():
|
||||
if marker in (m.get("text") or "") and m.get("isFromMe"):
|
||||
hit=m; break
|
||||
if hit: break
|
||||
time.sleep(2)
|
||||
if not hit:
|
||||
print("[send-test] ✗ outbound message not found in BB store — send likely failed")
|
||||
sys.exit(1)
|
||||
err=hit.get("error")
|
||||
if err in (0,None):
|
||||
print("[send-test] ✓ outbound message sent (fromMe=True, error=%s)"%err)
|
||||
print("[send-test] → confirm it arrived in the Messages app on the target device")
|
||||
else:
|
||||
print("[send-test] ✗ BlueBubbles reported send error=%s"%err)
|
||||
sys.exit(1)
|
||||
PY
|
||||
@@ -0,0 +1,187 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# test-imessage-bridge.sh — Regression test for the iMessage Desktop bridge
|
||||
#
|
||||
# Drives the Electron main-process `imessageBridge.*` IPC handlers plus the
|
||||
# local bridge HTTP server and the BlueBubbles server, asserting the full
|
||||
# connect/configure flow. Use it to regression-test PR work on the iMessage
|
||||
# channel (BlueBubbles bridge) without clicking through the UI every time.
|
||||
#
|
||||
# Prerequisites:
|
||||
# 1. BlueBubbles server running and reachable (default http://127.0.0.1:1234)
|
||||
# 2. Electron dev running with CDP — `electron-dev.sh start`
|
||||
# 3. `agent-browser` on PATH, connected to the same CDP port
|
||||
#
|
||||
# Usage:
|
||||
# ./test-imessage-bridge.sh <bluebubbles_password> [bb_url] [cdp_port]
|
||||
#
|
||||
# Example:
|
||||
# ./test-imessage-bridge.sh 'my-bb-password'
|
||||
# ./test-imessage-bridge.sh 'my-bb-password' http://127.0.0.1:1234 9222
|
||||
#
|
||||
# Notes:
|
||||
# - The password is passed as an argv, so it is visible in `ps`. This is a
|
||||
# local dev tool; do not run it on shared machines with a real secret.
|
||||
# - It uses a unique applicationId per run (imsg-regression-$$) and cleans up
|
||||
# its own bridge config + BlueBubbles webhook on exit, so it is safe to
|
||||
# re-run and does not disturb real configs.
|
||||
set -euo pipefail
|
||||
|
||||
BB_PASS="${1:?Usage: $0 <bluebubbles_password> [bb_url] [cdp_port]}"
|
||||
BB_URL="${2:-http://127.0.0.1:1234}"
|
||||
CDP_PORT="${3:-9222}"
|
||||
|
||||
APP_ID="imsg-regression-$$"
|
||||
SECRET="regression-secret-$$"
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
# ── Output helpers ───────────────────────────────────────────────────
|
||||
ok() { echo " ✓ $1"; PASS=$((PASS + 1)); }
|
||||
bad() { echo " ✗ $1 — $2"; FAIL=$((FAIL + 1)); }
|
||||
note() { echo "[imsg-test] $1"; }
|
||||
|
||||
# ── BlueBubbles REST helpers ─────────────────────────────────────────
|
||||
bb_get_webhooks() {
|
||||
curl -sS -m 8 "${BB_URL}/api/v1/webhook?password=${BB_PASS}"
|
||||
}
|
||||
|
||||
# Delete every webhook whose URL mentions our APP_ID (cleanup is idempotent).
|
||||
bb_cleanup_webhooks() {
|
||||
local ids
|
||||
ids=$(bb_get_webhooks | python3 -c '
|
||||
import json,sys
|
||||
try: d=json.load(sys.stdin)
|
||||
except Exception: sys.exit(0)
|
||||
for w in (d.get("data") or []):
|
||||
if "'"$APP_ID"'" in (w.get("url") or ""): print(w["id"])
|
||||
' 2>/dev/null || true)
|
||||
for id in $ids; do
|
||||
curl -sS -m 8 -X DELETE "${BB_URL}/api/v1/webhook/${id}?password=${BB_PASS}" >/dev/null 2>&1 || true
|
||||
done
|
||||
}
|
||||
|
||||
# ── IPC helper (drives the Electron renderer's electronAPI bridge) ───
|
||||
# Runs a JS snippet that returns a string token; prints the raw token.
|
||||
# The BlueBubbles password is base64-injected (atob) so special chars in the
|
||||
# secret never need shell/JS quoting.
|
||||
ipc_eval() {
|
||||
local js="$1"
|
||||
agent-browser --cdp "$CDP_PORT" eval -b "$(printf '%s' "$js" | base64)" 2>/dev/null
|
||||
}
|
||||
|
||||
PASS_B64=$(printf '%s' "$BB_PASS" | base64)
|
||||
|
||||
# Emit an inline JS object literal for the bridge config. $1 overrides the
|
||||
# password expression (defaults to atob of the real password); pass a JS string
|
||||
# literal like "'wrong'" to test the rejection path.
|
||||
ipc_config_js() {
|
||||
local pwexpr="${1:-atob('${PASS_B64}')}"
|
||||
printf "{applicationId:'%s',blueBubblesServerUrl:'%s',blueBubblesPassword:%s,enabled:true,webhookSecret:'%s'}" \
|
||||
"$APP_ID" "$BB_URL" "$pwexpr" "$SECRET"
|
||||
}
|
||||
|
||||
# ── Preflight ────────────────────────────────────────────────────────
|
||||
note "BlueBubbles: ${BB_URL} CDP: ${CDP_PORT} appId: ${APP_ID}"
|
||||
|
||||
code=$(curl -sS -m 6 -o /dev/null -w '%{http_code}' \
|
||||
"${BB_URL}/api/v1/server/info?password=${BB_PASS}" || echo 000)
|
||||
if [ "$code" = "200" ]; then ok "BlueBubbles reachable + password valid"; else
|
||||
bad "BlueBubbles preflight" "HTTP $code (is BlueBubbles running on ${BB_URL}?)"
|
||||
echo "Aborting — fix BlueBubbles first."; exit 1
|
||||
fi
|
||||
|
||||
if ! curl -sf --max-time 3 "http://localhost:${CDP_PORT}/json/version" >/dev/null 2>&1; then
|
||||
bad "Electron CDP preflight" "CDP ${CDP_PORT} unreachable — run electron-dev.sh start"
|
||||
echo "Aborting."; exit 1
|
||||
fi
|
||||
ok "Electron CDP reachable"
|
||||
|
||||
# Bridge must expose the IPC group (built from this branch's code).
|
||||
probe=$(ipc_eval "(async()=>{try{var s=await window.electronAPI.invoke('imessageBridge.getStatus',{});return 'OK:'+JSON.stringify(s);}catch(e){return 'ERR:'+(e.message||e);}})()")
|
||||
case "$probe" in
|
||||
*OK:*) ok "imessageBridge IPC available" ;;
|
||||
*) bad "imessageBridge IPC" "got: $probe (is the iMessage Desktop branch checked out?)"; echo "Aborting."; exit 1 ;;
|
||||
esac
|
||||
|
||||
# Start clean: remove any leftover config for this appId + BB webhooks.
|
||||
ipc_eval "(async()=>{try{await window.electronAPI.invoke('imessageBridge.removeConfig',{applicationId:'${APP_ID}'});}catch(e){}return 'done';})()" >/dev/null
|
||||
bb_cleanup_webhooks
|
||||
|
||||
# ── testConfig: happy path ───────────────────────────────────────────
|
||||
r=$(ipc_eval "(async()=>{try{var c=$(ipc_config_js);var x=await window.electronAPI.invoke('imessageBridge.testConfig',c);return 'OK:'+JSON.stringify(x);}catch(e){return 'ERR:'+(e.message||e);}})()")
|
||||
case "$r" in
|
||||
*OK:*success*true*) ok "testConfig with valid password → success" ;;
|
||||
*) bad "testConfig (valid)" "got: $r" ;;
|
||||
esac
|
||||
|
||||
# ── testConfig: wrong password rejects ───────────────────────────────
|
||||
r=$(ipc_eval "(async()=>{try{var c=$(ipc_config_js "'definitely-wrong-password'");var x=await window.electronAPI.invoke('imessageBridge.testConfig',c);return 'OK:'+JSON.stringify(x);}catch(e){return 'ERR:'+(e.message||e);}})()")
|
||||
case "$r" in
|
||||
*ERR:*) ok "testConfig with wrong password → rejected" ;;
|
||||
*) bad "testConfig (wrong password)" "expected rejection, got: $r" ;;
|
||||
esac
|
||||
|
||||
# ── testConfig: unreachable URL rejects ──────────────────────────────
|
||||
r=$(ipc_eval "(async()=>{try{var x=await window.electronAPI.invoke('imessageBridge.testConfig',{applicationId:'${APP_ID}',blueBubblesServerUrl:'http://127.0.0.1:65530',blueBubblesPassword:atob('${PASS_B64}'),enabled:true,webhookSecret:'${SECRET}'});return 'OK:'+JSON.stringify(x);}catch(e){return 'ERR:'+(e.message||e);}})()")
|
||||
case "$r" in
|
||||
*ERR:*) ok "testConfig with unreachable URL → rejected" ;;
|
||||
*) bad "testConfig (unreachable)" "expected rejection, got: $r" ;;
|
||||
esac
|
||||
|
||||
# ── upsertConfig: FIRST-TIME registration (Bug #1 regression guard) ──
|
||||
# BlueBubbles' GET /webhook?url=<unregistered> returns HTTP 500. The bridge
|
||||
# must list ALL webhooks and match client-side, otherwise this first save
|
||||
# fails. This assertion guards that fix.
|
||||
r=$(ipc_eval "(async()=>{try{var c=$(ipc_config_js);var x=await window.electronAPI.invoke('imessageBridge.upsertConfig',c);return 'OK:'+JSON.stringify(x);}catch(e){return 'ERR:'+(e.message||e);}})()")
|
||||
case "$r" in
|
||||
*OK:*success*true*) ok "upsertConfig first-time save → success (Bug #1 guard)" ;;
|
||||
*) bad "upsertConfig (first-time)" "got: $r" ;;
|
||||
esac
|
||||
|
||||
# ── getStatus: bridge running + config persisted ─────────────────────
|
||||
# Return a quote-free token so grep isn't tripped up by agent-browser's
|
||||
# JSON-string escaping of the eval result.
|
||||
r=$(ipc_eval "(async()=>{var s=await window.electronAPI.invoke('imessageBridge.getStatus',{});var c=(s.configs||[]).find(function(x){return x.applicationId==='${APP_ID}';});return 'RUN='+(s.running?'Y':'N')+' CFG='+(c?'Y':'N')+' PW='+((c&&c.blueBubblesPasswordSet)?'Y':'N');})()")
|
||||
echo "$r" | grep -q 'RUN=Y' && ok "bridge running" || bad "bridge running" "got: $r"
|
||||
echo "$r" | grep -q 'CFG=Y' && ok "config persisted" || bad "config persisted" "got: $r"
|
||||
echo "$r" | grep -q 'PW=Y' && ok "password stored (redacted in status)" || bad "password stored" "got: $r"
|
||||
|
||||
# ── BlueBubbles webhook actually registered ──────────────────────────
|
||||
if bb_get_webhooks | grep -q "${APP_ID}"; then
|
||||
ok "BlueBubbles webhook registered for appId"
|
||||
else
|
||||
bad "BlueBubbles webhook" "no webhook URL containing ${APP_ID}"
|
||||
fi
|
||||
|
||||
# ── Local bridge HTTP server: secret enforcement ─────────────────────
|
||||
BRIDGE_URL=$(ipc_eval "(async()=>{var s=await window.electronAPI.invoke('imessageBridge.getStatus',{});return s.serverUrl||'';})()" | tr -d '"')
|
||||
if [ -n "$BRIDGE_URL" ]; then
|
||||
# wrong secret → 401
|
||||
code=$(curl -sS -m 6 -o /dev/null -w '%{http_code}' -X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
"${BRIDGE_URL}/webhooks/bluebubbles/${APP_ID}?secret=WRONG" \
|
||||
-d '{"type":"new-message","data":{"guid":"x"}}' || echo 000)
|
||||
[ "$code" = "401" ] && ok "local bridge rejects wrong secret (401)" || bad "local bridge wrong secret" "expected 401, got $code"
|
||||
|
||||
# right secret → passes auth (reaches forward; without a bound cloud bot it
|
||||
# returns 5xx — that's fine, we're only asserting auth + routing here)
|
||||
code=$(curl -sS -m 6 -o /dev/null -w '%{http_code}' -X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
"${BRIDGE_URL}/webhooks/bluebubbles/${APP_ID}?secret=${SECRET}" \
|
||||
-d '{"type":"new-message","data":{"guid":"x","text":"hi"}}' || echo 000)
|
||||
[ "$code" != "401" ] && ok "local bridge accepts valid secret (HTTP $code, past auth)" || bad "local bridge valid secret" "got 401 with correct secret"
|
||||
else
|
||||
bad "local bridge URL" "getStatus returned no serverUrl"
|
||||
fi
|
||||
|
||||
# ── Cleanup ──────────────────────────────────────────────────────────
|
||||
ipc_eval "(async()=>{try{await window.electronAPI.invoke('imessageBridge.removeConfig',{applicationId:'${APP_ID}'});await window.electronAPI.invoke('imessageBridge.stop',{});}catch(e){}return 'cleaned';})()" >/dev/null
|
||||
bb_cleanup_webhooks
|
||||
note "cleaned up config + BlueBubbles webhook for ${APP_ID}"
|
||||
|
||||
# ── Summary ──────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "[imsg-test] PASS=${PASS} FAIL=${FAIL}"
|
||||
[ "$FAIL" -eq 0 ] || exit 1
|
||||
+3
-3
@@ -2,7 +2,7 @@
|
||||
|
||||
**App name:** `Lark` or `飞书` | **Process name:** `Lark` or `飞书`
|
||||
|
||||
See [osascript-common.md](./osascript-common.md) for shared patterns.
|
||||
See [osascript-common.md](../osascript-common.md) for shared patterns.
|
||||
|
||||
## Activate & Navigate
|
||||
|
||||
@@ -56,6 +56,6 @@ screencapture /tmp/lark-bot-response.png
|
||||
## Script
|
||||
|
||||
```bash
|
||||
./.agents/skills/local-testing/scripts/test-lark-bot.sh "bot-testing" "@MyBot hello"
|
||||
./.agents/skills/local-testing/scripts/test-lark-bot.sh "bot-testing" "Help me with this" 30
|
||||
./.agents/skills/local-testing/bot/lark/test-lark-bot.sh "bot-testing" "@MyBot hello"
|
||||
./.agents/skills/local-testing/bot/lark/test-lark-bot.sh "bot-testing" "Help me with this" 30
|
||||
```
|
||||
+1
-1
@@ -80,5 +80,5 @@ echo "[$APP] Waiting ${WAIT}s for bot response..."
|
||||
sleep "$WAIT"
|
||||
|
||||
echo "[$APP] Capturing screenshot..."
|
||||
"$SCRIPT_DIR/capture-app-window.sh" "$APP" "$SCREENSHOT"
|
||||
"$SCRIPT_DIR/../capture-app-window.sh" "$APP" "$SCREENSHOT"
|
||||
echo "[$APP] Done! Screenshot saved to $SCREENSHOT"
|
||||
+3
-3
@@ -2,7 +2,7 @@
|
||||
|
||||
**App name:** `QQ` | **Process name:** `QQ`
|
||||
|
||||
See [osascript-common.md](./osascript-common.md) for shared patterns.
|
||||
See [osascript-common.md](../osascript-common.md) for shared patterns.
|
||||
|
||||
## Activate & Navigate
|
||||
|
||||
@@ -57,6 +57,6 @@ screencapture /tmp/qq-bot-response.png
|
||||
## Script
|
||||
|
||||
```bash
|
||||
./.agents/skills/local-testing/scripts/test-qq-bot.sh "bot-testing" "Hello bot" 15
|
||||
./.agents/skills/local-testing/scripts/test-qq-bot.sh "MyBot" "/help" 10
|
||||
./.agents/skills/local-testing/bot/qq/test-qq-bot.sh "bot-testing" "Hello bot" 15
|
||||
./.agents/skills/local-testing/bot/qq/test-qq-bot.sh "MyBot" "/help" 10
|
||||
```
|
||||
+1
-1
@@ -72,5 +72,5 @@ echo "[$APP] Waiting ${WAIT}s for bot response..."
|
||||
sleep "$WAIT"
|
||||
|
||||
echo "[$APP] Capturing screenshot..."
|
||||
"$SCRIPT_DIR/capture-app-window.sh" "$APP" "$SCREENSHOT"
|
||||
"$SCRIPT_DIR/../capture-app-window.sh" "$APP" "$SCREENSHOT"
|
||||
echo "[$APP] Done! Screenshot saved to $SCREENSHOT"
|
||||
+3
-3
@@ -2,7 +2,7 @@
|
||||
|
||||
**App name:** `Slack` | **Process name:** `Slack`
|
||||
|
||||
See [osascript-common.md](./osascript-common.md) for shared patterns.
|
||||
See [osascript-common.md](../osascript-common.md) for shared patterns.
|
||||
|
||||
## Activate & Navigate
|
||||
|
||||
@@ -68,6 +68,6 @@ screencapture /tmp/slack-bot-response.png
|
||||
## Script
|
||||
|
||||
```bash
|
||||
./.agents/skills/local-testing/scripts/test-slack-bot.sh "bot-testing" "@mybot hello"
|
||||
./.agents/skills/local-testing/scripts/test-slack-bot.sh "bot-testing" "/ask What is 2+2?" 20
|
||||
./.agents/skills/local-testing/bot/slack/test-slack-bot.sh "bot-testing" "@mybot hello"
|
||||
./.agents/skills/local-testing/bot/slack/test-slack-bot.sh "bot-testing" "/ask What is 2+2?" 20
|
||||
```
|
||||
+1
-1
@@ -60,5 +60,5 @@ echo "[$APP] Waiting ${WAIT}s for bot response..."
|
||||
sleep "$WAIT"
|
||||
|
||||
echo "[$APP] Capturing screenshot..."
|
||||
"$SCRIPT_DIR/capture-app-window.sh" "$APP" "$SCREENSHOT"
|
||||
"$SCRIPT_DIR/../capture-app-window.sh" "$APP" "$SCREENSHOT"
|
||||
echo "[$APP] Done! Screenshot saved to $SCREENSHOT"
|
||||
+3
-3
@@ -2,7 +2,7 @@
|
||||
|
||||
**App name:** `Telegram` | **Process name:** `Telegram`
|
||||
|
||||
See [osascript-common.md](./osascript-common.md) for shared patterns.
|
||||
See [osascript-common.md](../osascript-common.md) for shared patterns.
|
||||
|
||||
## Activate & Navigate
|
||||
|
||||
@@ -75,6 +75,6 @@ curl -s "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/getUpdates?limit=5" | j
|
||||
## Script
|
||||
|
||||
```bash
|
||||
./.agents/skills/local-testing/scripts/test-telegram-bot.sh "MyTestBot" "/start"
|
||||
./.agents/skills/local-testing/scripts/test-telegram-bot.sh "GPTBot" "Hello" 60
|
||||
./.agents/skills/local-testing/bot/telegram/test-telegram-bot.sh "MyTestBot" "/start"
|
||||
./.agents/skills/local-testing/bot/telegram/test-telegram-bot.sh "GPTBot" "Hello" 60
|
||||
```
|
||||
+1
-1
@@ -75,5 +75,5 @@ echo "[$APP] Waiting ${WAIT}s for bot response..."
|
||||
sleep "$WAIT"
|
||||
|
||||
echo "[$APP] Capturing screenshot..."
|
||||
"$SCRIPT_DIR/capture-app-window.sh" "$APP" "$SCREENSHOT"
|
||||
"$SCRIPT_DIR/../capture-app-window.sh" "$APP" "$SCREENSHOT"
|
||||
echo "[$APP] Done! Screenshot saved to $SCREENSHOT"
|
||||
+3
-3
@@ -2,7 +2,7 @@
|
||||
|
||||
**App name:** `微信` or `WeChat` | **Process name:** `WeChat`
|
||||
|
||||
See [osascript-common.md](./osascript-common.md) for shared patterns.
|
||||
See [osascript-common.md](../osascript-common.md) for shared patterns.
|
||||
|
||||
## Activate & Navigate
|
||||
|
||||
@@ -76,6 +76,6 @@ screencapture /tmp/wechat-bot-response.png
|
||||
## Script
|
||||
|
||||
```bash
|
||||
./.agents/skills/local-testing/scripts/test-wechat-bot.sh "文件传输助手" "test message" 5
|
||||
./.agents/skills/local-testing/scripts/test-wechat-bot.sh "MyBot" "Tell me a joke" 30
|
||||
./.agents/skills/local-testing/bot/wechat/test-wechat-bot.sh "文件传输助手" "test message" 5
|
||||
./.agents/skills/local-testing/bot/wechat/test-wechat-bot.sh "MyBot" "Tell me a joke" 30
|
||||
```
|
||||
+1
-1
@@ -81,5 +81,5 @@ echo "[$APP] Waiting ${WAIT}s for bot response..."
|
||||
sleep "$WAIT"
|
||||
|
||||
echo "[$APP] Capturing screenshot..."
|
||||
"$SCRIPT_DIR/capture-app-window.sh" "$APP" "$SCREENSHOT"
|
||||
"$SCRIPT_DIR/../capture-app-window.sh" "$APP" "$SCREENSHOT"
|
||||
echo "[$APP] Done! Screenshot saved to $SCREENSHOT"
|
||||
Reference in New Issue
Block a user