mirror of
https://github.com/lobehub/lobehub
synced 2026-04-21 09:37:28 +00:00
🔨 chore: optimize message tool (#13444)
* chore: adjust electron testing to local testing * chore: comprehence discord docs * chore: add common capture window * chore: default enable message tool in bot conversation * fix: discord readMessages error * chore: optimize readMessages prompt * chore: optimize limit description * chore: optimize limit size * chore: remove limit parameter for discord * chore: add threadRecover Patch * chore: optimize system role and bot context * fix: avoid overide user config message tool * chore: add default timeout
This commit is contained in:
parent
ee8cab8305
commit
32e36e330a
25 changed files with 1679 additions and 295 deletions
|
|
@ -1,270 +0,0 @@
|
|||
---
|
||||
name: electron-testing
|
||||
description: Electron desktop app automation testing using agent-browser CLI. Use when testing UI features in the running Electron app, verifying visual state, interacting with the desktop app, or running manual QA scenarios. Triggers on 'test in electron', 'test desktop', 'electron test', 'manual test', or UI verification tasks.
|
||||
---
|
||||
|
||||
# Electron Automation Testing with agent-browser
|
||||
|
||||
Use the `agent-browser` CLI to automate and test the LobeHub desktop Electron app.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- `agent-browser` CLI installed globally (`agent-browser --version`)
|
||||
- Working directory must be `apps/desktop/` when starting Electron
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# 1. Kill existing instances
|
||||
pkill -f "Electron" 2> /dev/null
|
||||
pkill -f "electron-vite" 2> /dev/null
|
||||
pkill -f "agent-browser" 2> /dev/null
|
||||
sleep 3
|
||||
|
||||
# 2. Start Electron with CDP (MUST cd to apps/desktop first)
|
||||
cd apps/desktop && ELECTRON_ENABLE_LOGGING=1 npx electron-vite dev -- --remote-debugging-port=9222 > /tmp/electron-dev.log 2>&1 &
|
||||
|
||||
# 3. Wait for startup (poll for "starting electron" in logs)
|
||||
for i in $(seq 1 12); do
|
||||
sleep 5
|
||||
if strings /tmp/electron-dev.log 2> /dev/null | grep -q "starting electron"; then
|
||||
echo "ready"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# 4. Wait for renderer to load, then connect
|
||||
sleep 15 && agent-browser --cdp 9222 wait 3000
|
||||
```
|
||||
|
||||
**Critical:** `npx electron-vite dev` MUST run from `apps/desktop/` directory, not project root. Running from root will fail silently (no `initUrl` in logs).
|
||||
|
||||
## Connecting to Electron
|
||||
|
||||
```bash
|
||||
agent-browser --cdp 9222 snapshot -i # Interactive elements only
|
||||
agent-browser --cdp 9222 snapshot -i -C # Include contenteditable elements
|
||||
```
|
||||
|
||||
Always use `--cdp 9222`. The `--auto-connect` flag is unreliable.
|
||||
|
||||
## Core Workflow
|
||||
|
||||
### 1. Snapshot → Find Elements
|
||||
|
||||
```bash
|
||||
agent-browser --cdp 9222 snapshot -i
|
||||
```
|
||||
|
||||
Returns element refs like `@e1`, `@e2`. **Refs are ephemeral** — re-snapshot after any page change (click, navigation, HMR).
|
||||
|
||||
### 2. Interact
|
||||
|
||||
```bash
|
||||
agent-browser --cdp 9222 click @e5
|
||||
agent-browser --cdp 9222 type @e3 "text" # Character by character (for contenteditable)
|
||||
agent-browser --cdp 9222 fill @e3 "text" # Bulk fill (for regular inputs)
|
||||
agent-browser --cdp 9222 press Enter
|
||||
agent-browser --cdp 9222 scroll down 500
|
||||
```
|
||||
|
||||
### 3. Wait
|
||||
|
||||
```bash
|
||||
agent-browser --cdp 9222 wait 2000 # Wait ms
|
||||
agent-browser --cdp 9222 wait --load networkidle # Wait for network
|
||||
```
|
||||
|
||||
Avoid `agent-browser wait` for long durations (>30s) — it blocks the daemon. Use `sleep N` in bash instead, then take a new snapshot/screenshot.
|
||||
|
||||
### 4. Screenshot & Verify
|
||||
|
||||
```bash
|
||||
agent-browser --cdp 9222 screenshot # Save to ~/.agent-browser/tmp/screenshots/
|
||||
agent-browser --cdp 9222 get text @e1 # Get element text
|
||||
agent-browser --cdp 9222 get url # Get current URL
|
||||
```
|
||||
|
||||
Read screenshots with the `Read` tool for visual verification.
|
||||
|
||||
### 5. Evaluate JavaScript
|
||||
|
||||
```bash
|
||||
agent-browser --cdp 9222 eval "document.title"
|
||||
```
|
||||
|
||||
For multi-line JS, use `--stdin`:
|
||||
|
||||
```bash
|
||||
agent-browser --cdp 9222 eval --stdin << 'EVALEOF'
|
||||
(function() {
|
||||
var chat = window.__LOBE_STORES.chat();
|
||||
return JSON.stringify({
|
||||
totalOps: Object.keys(chat.operations).length,
|
||||
queue: chat.queuedMessages,
|
||||
});
|
||||
})()
|
||||
EVALEOF
|
||||
```
|
||||
|
||||
## LobeHub-Specific Patterns
|
||||
|
||||
### Access Zustand Store State
|
||||
|
||||
The app exposes stores via `window.__LOBE_STORES` (dev mode only):
|
||||
|
||||
```bash
|
||||
agent-browser --cdp 9222 eval --stdin << 'EVALEOF'
|
||||
(function() {
|
||||
var chat = window.__LOBE_STORES.chat();
|
||||
var ops = Object.values(chat.operations);
|
||||
return JSON.stringify({
|
||||
ops: ops.map(function(o) { return { type: o.type, status: o.status }; }),
|
||||
activeAgent: chat.activeAgentId,
|
||||
activeTopic: chat.activeTopicId,
|
||||
});
|
||||
})()
|
||||
EVALEOF
|
||||
```
|
||||
|
||||
### Find the Chat Input
|
||||
|
||||
The chat input is a contenteditable div. Regular `snapshot -i` won't find it — use `-C`:
|
||||
|
||||
```bash
|
||||
agent-browser --cdp 9222 snapshot -i -C 2>&1 | grep "editable"
|
||||
# Output: - generic [ref=e48] editable [contenteditable]:
|
||||
```
|
||||
|
||||
### Navigate to an Agent
|
||||
|
||||
```bash
|
||||
# Snapshot to find agent links in sidebar
|
||||
agent-browser --cdp 9222 snapshot -i 2>&1 | grep -i "agent-name"
|
||||
# Click the agent link
|
||||
agent-browser --cdp 9222 click @e<ref>
|
||||
agent-browser --cdp 9222 wait 2000
|
||||
```
|
||||
|
||||
### Send a Chat Message
|
||||
|
||||
```bash
|
||||
# 1. Find contenteditable input
|
||||
agent-browser --cdp 9222 snapshot -i -C 2>&1 | grep "editable"
|
||||
# 2. Click, type, send
|
||||
agent-browser --cdp 9222 click @e<ref>
|
||||
agent-browser --cdp 9222 type @e<ref> "Hello world"
|
||||
agent-browser --cdp 9222 press Enter
|
||||
```
|
||||
|
||||
### Wait for Agent to Complete
|
||||
|
||||
Don't use `agent-browser wait` for long AI generation. Use `sleep` + screenshot:
|
||||
|
||||
```bash
|
||||
sleep 60 && agent-browser --cdp 9222 scroll down 5000 && agent-browser --cdp 9222 screenshot
|
||||
```
|
||||
|
||||
Or poll the store for operation status:
|
||||
|
||||
```bash
|
||||
agent-browser --cdp 9222 eval --stdin << 'EVALEOF'
|
||||
(function() {
|
||||
var chat = window.__LOBE_STORES.chat();
|
||||
var ops = Object.values(chat.operations);
|
||||
var running = ops.filter(function(o) { return o.status === 'running'; });
|
||||
return running.length === 0 ? 'done' : 'running: ' + running.length;
|
||||
})()
|
||||
EVALEOF
|
||||
```
|
||||
|
||||
### Install Error Interceptor
|
||||
|
||||
Capture `console.error` from the app for debugging:
|
||||
|
||||
```bash
|
||||
agent-browser --cdp 9222 eval --stdin << 'EVALEOF'
|
||||
(function() {
|
||||
window.__CAPTURED_ERRORS = [];
|
||||
var orig = console.error;
|
||||
console.error = function() {
|
||||
var msg = Array.from(arguments).map(function(a) {
|
||||
if (a instanceof Error) return a.message;
|
||||
return typeof a === 'object' ? JSON.stringify(a) : String(a);
|
||||
}).join(' ');
|
||||
window.__CAPTURED_ERRORS.push(msg);
|
||||
orig.apply(console, arguments);
|
||||
};
|
||||
return 'installed';
|
||||
})()
|
||||
EVALEOF
|
||||
|
||||
# Later, check captured errors:
|
||||
agent-browser --cdp 9222 eval "JSON.stringify(window.__CAPTURED_ERRORS)"
|
||||
```
|
||||
|
||||
## Screen Recording
|
||||
|
||||
Record automated demos by combining `ffmpeg` screen capture with `agent-browser` automation. The script `.agents/skills/electron-testing/record-electron-demo.sh` handles the full lifecycle.
|
||||
|
||||
### Usage
|
||||
|
||||
```bash
|
||||
# Run the built-in demo (queue-edit feature)
|
||||
./.agents/skills/electron-testing/record-electron-demo.sh
|
||||
|
||||
# Run a custom automation script
|
||||
./.agents/skills/electron-testing/record-electron-demo.sh ./my-demo.sh /tmp/my-demo.mp4
|
||||
```
|
||||
|
||||
The script automatically:
|
||||
|
||||
1. Starts Electron with CDP and waits for SPA to load
|
||||
2. Detects the window position, screen, and Retina scale via Swift/CGWindowList
|
||||
3. Records only the Electron window region using `ffmpeg -f avfoundation` with crop
|
||||
4. Runs the demo (built-in or custom script receiving CDP port as `$1`)
|
||||
5. Stops recording and cleans up
|
||||
|
||||
### Writing Custom Demo Scripts
|
||||
|
||||
Create a shell script that receives the CDP port as `$1`:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
# my-demo.sh — Custom demo script
|
||||
PORT=$1
|
||||
|
||||
# Navigate
|
||||
agent-browser --cdp "$PORT" snapshot -i 2>&1 | grep 'link "Lobe AI"'
|
||||
agent-browser --cdp "$PORT" click @e34
|
||||
sleep 3
|
||||
|
||||
# Find input and type
|
||||
INPUT=$(agent-browser --cdp "$PORT" snapshot -i -C 2>&1 \
|
||||
| grep "editable" | grep -oE 'ref=e[0-9]+' | head -1 | sed 's/ref=//')
|
||||
agent-browser --cdp "$PORT" click "@$INPUT"
|
||||
agent-browser --cdp "$PORT" type "@$INPUT" "Hello world"
|
||||
agent-browser --cdp "$PORT" press Enter
|
||||
sleep 5
|
||||
```
|
||||
|
||||
### Key Details
|
||||
|
||||
- **Multi-monitor support**: Uses Swift to find which screen the Electron window is on and calculates relative crop coordinates
|
||||
- **Retina aware**: Scales crop coordinates by the display's `backingScaleFactor`
|
||||
- **No window resize**: Records the window at its current position/size to avoid triggering SPA reload
|
||||
- **SPA load polling**: Waits for interactive elements to appear before starting the demo
|
||||
- **Prerequisites**: `ffmpeg` (`brew install ffmpeg`), `agent-browser`
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **`npx electron-vite dev` must run from `apps/desktop/`** — running from project root fails silently
|
||||
- **HMR invalidates everything** — after code changes, refs break, page may crash. Re-snapshot or restart Electron
|
||||
- **`agent-browser wait` blocks the daemon** — for waits >30s, use bash `sleep` instead
|
||||
- **Daemon can get stuck** — if commands hang, `pkill -f agent-browser` to reset the daemon
|
||||
- **`snapshot -i` doesn't find contenteditable** — always use `snapshot -i -C` to find rich text editors
|
||||
- **`fill` doesn't work on contenteditable** — use `type` for the chat input
|
||||
- **Screenshots go to `~/.agent-browser/tmp/screenshots/`** — read them with the `Read` tool
|
||||
- **Store is at `window.__LOBE_STORES`** not `window.__ZUSTAND_STORES__` — use `.chat()` to get current state
|
||||
- **Don't resize the Electron window after load** — resizing triggers a full SPA reload (splash screen), which can take 30+ seconds or get stuck. Record at the window's current size instead
|
||||
- **`screencapture -V -l<windowid>`** doesn't work reliably for video — use `ffmpeg -f avfoundation` with crop instead (see Screen Recording section)
|
||||
935
.agents/skills/local-testing/SKILL.md
Normal file
935
.agents/skills/local-testing/SKILL.md
Normal file
|
|
@ -0,0 +1,935 @@
|
|||
---
|
||||
name: local-testing
|
||||
description: >
|
||||
Local app and bot testing. Uses agent-browser CLI for Electron/web app UI testing,
|
||||
and osascript (AppleScript) for controlling native macOS apps (WeChat, Discord, Telegram, Slack, Lark/飞书, QQ)
|
||||
to test bots. Triggers on 'local test', 'test in electron', 'test desktop', 'test bot',
|
||||
'bot test', 'test in discord', 'test in telegram', 'test in slack', 'test in weixin',
|
||||
'test in wechat', 'test in lark', 'test in feishu', 'test in qq',
|
||||
'manual test', 'osascript', or UI/bot verification tasks.
|
||||
---
|
||||
|
||||
# Local App & Bot Testing
|
||||
|
||||
Two approaches for local testing on macOS:
|
||||
|
||||
| Approach | Tool | Best For |
|
||||
| --------------------------- | ------------------- | ---------------------------------------------------- |
|
||||
| **agent-browser + CDP** | `agent-browser` CLI | Electron apps, web apps (DOM access, JS eval) |
|
||||
| **osascript (AppleScript)** | `osascript -e` | Native macOS apps (WeChat, Discord, Telegram, Slack) |
|
||||
|
||||
---
|
||||
|
||||
# Part 1: agent-browser (Electron / Web Apps)
|
||||
|
||||
Use `agent-browser` to automate Chromium-based apps via Chrome DevTools Protocol.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- `agent-browser` CLI installed globally (`agent-browser --version`)
|
||||
|
||||
## Core Workflow
|
||||
|
||||
### 1. Snapshot → Find Elements
|
||||
|
||||
```bash
|
||||
agent-browser --cdp -i < PORT > snapshot # Interactive elements only
|
||||
agent-browser --cdp -i -C < PORT > snapshot # Include contenteditable elements
|
||||
```
|
||||
|
||||
Returns element refs like `@e1`, `@e2`. **Refs are ephemeral** — re-snapshot after any page change.
|
||||
|
||||
### 2. Interact
|
||||
|
||||
```bash
|
||||
agent-browser --cdp @e5 < PORT > click
|
||||
agent-browser --cdp @e3 "text" < PORT > type # Character by character (contenteditable)
|
||||
agent-browser --cdp @e3 "text" < PORT > fill # Bulk fill (regular inputs)
|
||||
agent-browser --cdp Enter < PORT > press
|
||||
agent-browser --cdp down 500 < PORT > scroll
|
||||
```
|
||||
|
||||
### 3. Wait
|
||||
|
||||
```bash
|
||||
agent-browser --cdp 2000 < PORT > wait # Wait ms
|
||||
agent-browser --cdp --load networkidle < PORT > wait # Wait for network
|
||||
```
|
||||
|
||||
For waits >30s, use `sleep N` in bash instead — `agent-browser wait` blocks the daemon.
|
||||
|
||||
### 4. Screenshot & Verify
|
||||
|
||||
```bash
|
||||
agent-browser --cdp < PORT > screenshot # Save to ~/.agent-browser/tmp/screenshots/
|
||||
agent-browser --cdp text @e1 < PORT > get # Get element text
|
||||
agent-browser --cdp url < PORT > get # Get current URL
|
||||
```
|
||||
|
||||
Read screenshots with the `Read` tool for visual verification.
|
||||
|
||||
### 5. Evaluate JavaScript
|
||||
|
||||
```bash
|
||||
agent-browser --cdp "document.title" < PORT > eval
|
||||
```
|
||||
|
||||
For multi-line JS, use `--stdin`:
|
||||
|
||||
```bash
|
||||
agent-browser --cdp --stdin < PORT > eval << 'EVALEOF'
|
||||
(function() {
|
||||
return JSON.stringify({ title: document.title, url: location.href });
|
||||
})()
|
||||
EVALEOF
|
||||
```
|
||||
|
||||
## Electron (LobeHub Desktop)
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
# 1. Kill existing instances
|
||||
pkill -f "Electron" 2> /dev/null
|
||||
pkill -f "electron-vite" 2> /dev/null
|
||||
pkill -f "agent-browser" 2> /dev/null
|
||||
sleep 3
|
||||
|
||||
# 2. Start Electron with CDP (MUST cd to apps/desktop first)
|
||||
cd apps/desktop && ELECTRON_ENABLE_LOGGING=1 npx electron-vite dev -- --remote-debugging-port=9222 > /tmp/electron-dev.log 2>&1 &
|
||||
|
||||
# 3. Wait for startup
|
||||
for i in $(seq 1 12); do
|
||||
sleep 5
|
||||
if strings /tmp/electron-dev.log 2> /dev/null | grep -q "starting electron"; then
|
||||
echo "ready"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# 4. Wait for renderer, then connect
|
||||
sleep 15 && agent-browser --cdp 9222 wait 3000
|
||||
```
|
||||
|
||||
**Critical:** `npx electron-vite dev` MUST run from `apps/desktop/` directory, not project root.
|
||||
|
||||
### LobeHub-Specific Patterns
|
||||
|
||||
#### Access Zustand Store State
|
||||
|
||||
```bash
|
||||
agent-browser --cdp 9222 eval --stdin << 'EVALEOF'
|
||||
(function() {
|
||||
var chat = window.__LOBE_STORES.chat();
|
||||
var ops = Object.values(chat.operations);
|
||||
return JSON.stringify({
|
||||
ops: ops.map(function(o) { return { type: o.type, status: o.status }; }),
|
||||
activeAgent: chat.activeAgentId,
|
||||
activeTopic: chat.activeTopicId,
|
||||
});
|
||||
})()
|
||||
EVALEOF
|
||||
```
|
||||
|
||||
#### Find and Use the Chat Input
|
||||
|
||||
```bash
|
||||
# The chat input is contenteditable — must use -C flag
|
||||
agent-browser --cdp 9222 snapshot -i -C 2>&1 | grep "editable"
|
||||
|
||||
agent-browser --cdp 9222 click @e48
|
||||
agent-browser --cdp 9222 type @e48 "Hello world"
|
||||
agent-browser --cdp 9222 press Enter
|
||||
```
|
||||
|
||||
#### Wait for Agent to Complete
|
||||
|
||||
```bash
|
||||
agent-browser --cdp 9222 eval --stdin << 'EVALEOF'
|
||||
(function() {
|
||||
var chat = window.__LOBE_STORES.chat();
|
||||
var ops = Object.values(chat.operations);
|
||||
var running = ops.filter(function(o) { return o.status === 'running'; });
|
||||
return running.length === 0 ? 'done' : 'running: ' + running.length;
|
||||
})()
|
||||
EVALEOF
|
||||
```
|
||||
|
||||
#### Install Error Interceptor
|
||||
|
||||
```bash
|
||||
agent-browser --cdp 9222 eval --stdin << 'EVALEOF'
|
||||
(function() {
|
||||
window.__CAPTURED_ERRORS = [];
|
||||
var orig = console.error;
|
||||
console.error = function() {
|
||||
var msg = Array.from(arguments).map(function(a) {
|
||||
if (a instanceof Error) return a.message;
|
||||
return typeof a === 'object' ? JSON.stringify(a) : String(a);
|
||||
}).join(' ');
|
||||
window.__CAPTURED_ERRORS.push(msg);
|
||||
orig.apply(console, arguments);
|
||||
};
|
||||
return 'installed';
|
||||
})()
|
||||
EVALEOF
|
||||
|
||||
# Later, check captured errors:
|
||||
agent-browser --cdp 9222 eval "JSON.stringify(window.__CAPTURED_ERRORS)"
|
||||
```
|
||||
|
||||
## Chrome / Web Apps
|
||||
|
||||
```bash
|
||||
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
|
||||
--remote-debugging-port=9222 \
|
||||
--user-data-dir=/tmp/chrome-test-profile \
|
||||
"<URL>" &
|
||||
sleep 5
|
||||
agent-browser --cdp 9222 snapshot -i
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Part 2: osascript (Native macOS App Bot Testing)
|
||||
|
||||
Use AppleScript via `osascript` to control native macOS desktop apps for bot testing. This works with any app that supports macOS Accessibility, without needing CDP or Chromium.
|
||||
|
||||
## Core osascript Patterns
|
||||
|
||||
### Activate an App
|
||||
|
||||
```bash
|
||||
osascript -e 'tell application "Discord" to activate'
|
||||
```
|
||||
|
||||
### Type Text
|
||||
|
||||
```bash
|
||||
# Type character by character (reliable, but slow for long text)
|
||||
osascript -e 'tell application "System Events" to keystroke "Hello world"'
|
||||
|
||||
# Press Enter
|
||||
osascript -e 'tell application "System Events" to key code 36'
|
||||
|
||||
# Press Tab
|
||||
osascript -e 'tell application "System Events" to key code 48'
|
||||
|
||||
# Press Escape
|
||||
osascript -e 'tell application "System Events" to key code 53'
|
||||
```
|
||||
|
||||
### Paste from Clipboard (fast, for long text)
|
||||
|
||||
```bash
|
||||
# Set clipboard and paste — much faster than keystroke for long messages
|
||||
osascript -e 'set the clipboard to "Your long message here"'
|
||||
osascript -e 'tell application "System Events" to keystroke "v" using command down'
|
||||
```
|
||||
|
||||
Or in one shot:
|
||||
|
||||
```bash
|
||||
osascript -e '
|
||||
set the clipboard to "Your long message here"
|
||||
tell application "System Events" to keystroke "v" using command down
|
||||
'
|
||||
```
|
||||
|
||||
### Keyboard Shortcuts
|
||||
|
||||
```bash
|
||||
# Cmd+K (quick switcher in Discord/Slack)
|
||||
osascript -e 'tell application "System Events" to keystroke "k" using command down'
|
||||
|
||||
# Cmd+F (search)
|
||||
osascript -e 'tell application "System Events" to keystroke "f" using command down'
|
||||
|
||||
# Cmd+N (new message/chat)
|
||||
osascript -e 'tell application "System Events" to keystroke "n" using command down'
|
||||
|
||||
# Cmd+Shift+K (example: multi-modifier)
|
||||
osascript -e 'tell application "System Events" to keystroke "k" using {command down, shift down}'
|
||||
```
|
||||
|
||||
### Click at Position
|
||||
|
||||
```bash
|
||||
# Click at absolute screen coordinates
|
||||
osascript -e '
|
||||
tell application "System Events"
|
||||
click at {500, 300}
|
||||
end tell
|
||||
'
|
||||
```
|
||||
|
||||
### Get Window Info
|
||||
|
||||
```bash
|
||||
# Get window position and size
|
||||
osascript -e '
|
||||
tell application "System Events"
|
||||
tell process "Discord"
|
||||
get {position, size} of window 1
|
||||
end tell
|
||||
end tell
|
||||
'
|
||||
```
|
||||
|
||||
### Screenshot
|
||||
|
||||
```bash
|
||||
# Full screen
|
||||
screencapture /tmp/screenshot.png
|
||||
|
||||
# Interactive region select
|
||||
screencapture -i /tmp/screenshot.png
|
||||
|
||||
# Specific window (by window ID from CGWindowList)
|
||||
screencapture -l < WINDOW_ID > /tmp/screenshot.png
|
||||
```
|
||||
|
||||
To get window ID for a specific app:
|
||||
|
||||
```bash
|
||||
osascript -e '
|
||||
tell application "System Events"
|
||||
tell process "Discord"
|
||||
get id of window 1
|
||||
end tell
|
||||
end tell
|
||||
'
|
||||
```
|
||||
|
||||
### Read Accessibility Elements
|
||||
|
||||
```bash
|
||||
# Get all UI elements of the frontmost window (can be slow/large)
|
||||
osascript -e '
|
||||
tell application "System Events"
|
||||
tell process "Discord"
|
||||
entire contents of window 1
|
||||
end tell
|
||||
end tell
|
||||
'
|
||||
|
||||
# Get a specific element's value
|
||||
osascript -e '
|
||||
tell application "System Events"
|
||||
tell process "Discord"
|
||||
get value of text field 1 of window 1
|
||||
end tell
|
||||
end tell
|
||||
'
|
||||
```
|
||||
|
||||
> **Warning:** `entire contents` can be extremely slow on complex UIs. Prefer screenshots + `Read` tool for visual verification.
|
||||
|
||||
### Read Screen Text via Clipboard
|
||||
|
||||
For reading the latest message or response from an app:
|
||||
|
||||
```bash
|
||||
# Select all text in the focused area and copy
|
||||
osascript -e '
|
||||
tell application "System Events"
|
||||
keystroke "a" using command down
|
||||
keystroke "c" using command down
|
||||
end tell
|
||||
'
|
||||
sleep 0.5
|
||||
# Read clipboard
|
||||
pbpaste
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Client: Discord
|
||||
|
||||
**App name:** `Discord` | **Process name:** `Discord`
|
||||
|
||||
### Activate & Navigate
|
||||
|
||||
```bash
|
||||
# Activate Discord
|
||||
osascript -e 'tell application "Discord" to activate'
|
||||
sleep 1
|
||||
|
||||
# Open Quick Switcher (Cmd+K) to navigate to a channel
|
||||
osascript -e 'tell application "System Events" to keystroke "k" using command down'
|
||||
sleep 0.5
|
||||
osascript -e 'tell application "System Events" to keystroke "bot-testing"'
|
||||
sleep 1
|
||||
osascript -e 'tell application "System Events" to key code 36' # Enter
|
||||
sleep 2
|
||||
```
|
||||
|
||||
### Send Message to Bot
|
||||
|
||||
```bash
|
||||
# The message input is focused after navigating to a channel
|
||||
# Type a message
|
||||
osascript -e 'tell application "System Events" to keystroke "/hello"'
|
||||
sleep 0.5
|
||||
osascript -e 'tell application "System Events" to key code 36' # Enter
|
||||
```
|
||||
|
||||
### Send Long Message (via clipboard)
|
||||
|
||||
```bash
|
||||
osascript -e '
|
||||
tell application "Discord" to activate
|
||||
delay 0.5
|
||||
set the clipboard to "Write a 3000 word essay about space exploration"
|
||||
tell application "System Events"
|
||||
keystroke "v" using command down
|
||||
delay 0.3
|
||||
key code 36 -- Enter
|
||||
end tell
|
||||
'
|
||||
```
|
||||
|
||||
### Verify Bot Response
|
||||
|
||||
```bash
|
||||
# Wait for bot to respond, then screenshot
|
||||
sleep 10
|
||||
screencapture /tmp/discord-bot-response.png
|
||||
# Read with the Read tool for visual verification
|
||||
```
|
||||
|
||||
### Full Bot Test Example
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
# test-discord-bot.sh — Send message and verify bot response
|
||||
|
||||
# 1. Activate Discord and navigate to channel
|
||||
osascript -e '
|
||||
tell application "Discord" to activate
|
||||
delay 1
|
||||
-- Quick Switcher
|
||||
tell application "System Events" to keystroke "k" using command down
|
||||
delay 0.5
|
||||
tell application "System Events" to keystroke "bot-testing"
|
||||
delay 1
|
||||
tell application "System Events" to key code 36
|
||||
delay 2
|
||||
'
|
||||
|
||||
# 2. Send test message
|
||||
osascript -e '
|
||||
set the clipboard to "!ping"
|
||||
tell application "System Events"
|
||||
keystroke "v" using command down
|
||||
delay 0.3
|
||||
key code 36
|
||||
end tell
|
||||
'
|
||||
|
||||
# 3. Wait for response and capture
|
||||
sleep 5
|
||||
screencapture /tmp/discord-test-result.png
|
||||
echo "Screenshot saved to /tmp/discord-test-result.png"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Client: Slack
|
||||
|
||||
**App name:** `Slack` | **Process name:** `Slack`
|
||||
|
||||
### Activate & Navigate
|
||||
|
||||
```bash
|
||||
# Activate Slack
|
||||
osascript -e 'tell application "Slack" to activate'
|
||||
sleep 1
|
||||
|
||||
# Quick Switcher (Cmd+K)
|
||||
osascript -e 'tell application "System Events" to keystroke "k" using command down'
|
||||
sleep 0.5
|
||||
osascript -e 'tell application "System Events" to keystroke "bot-testing"'
|
||||
sleep 1
|
||||
osascript -e 'tell application "System Events" to key code 36' # Enter
|
||||
sleep 2
|
||||
```
|
||||
|
||||
### Send Message to Bot
|
||||
|
||||
```bash
|
||||
# Direct message input (focused after channel nav)
|
||||
osascript -e 'tell application "System Events" to keystroke "@mybot hello"'
|
||||
sleep 0.3
|
||||
osascript -e 'tell application "System Events" to key code 36'
|
||||
```
|
||||
|
||||
### Send Long Message
|
||||
|
||||
```bash
|
||||
osascript -e '
|
||||
tell application "Slack" to activate
|
||||
delay 0.5
|
||||
set the clipboard to "A long test message for the bot..."
|
||||
tell application "System Events"
|
||||
keystroke "v" using command down
|
||||
delay 0.3
|
||||
key code 36
|
||||
end tell
|
||||
'
|
||||
```
|
||||
|
||||
### Slash Command Test
|
||||
|
||||
```bash
|
||||
osascript -e '
|
||||
tell application "Slack" to activate
|
||||
delay 0.5
|
||||
tell application "System Events"
|
||||
keystroke "/ask What is the meaning of life?"
|
||||
delay 0.5
|
||||
key code 36
|
||||
end tell
|
||||
'
|
||||
```
|
||||
|
||||
### Verify Response
|
||||
|
||||
```bash
|
||||
sleep 10
|
||||
screencapture /tmp/slack-bot-response.png
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Client: Telegram
|
||||
|
||||
**App name:** `Telegram` | **Process name:** `Telegram`
|
||||
|
||||
### Activate & Navigate
|
||||
|
||||
```bash
|
||||
# Activate Telegram
|
||||
osascript -e 'tell application "Telegram" to activate'
|
||||
sleep 1
|
||||
|
||||
# Search for a bot (Cmd+F or click search)
|
||||
osascript -e '
|
||||
tell application "System Events"
|
||||
keystroke "f" using command down
|
||||
delay 0.5
|
||||
keystroke "MyTestBot"
|
||||
delay 1
|
||||
key code 36 -- Enter to select
|
||||
end tell
|
||||
'
|
||||
sleep 2
|
||||
```
|
||||
|
||||
### Send Message to Bot
|
||||
|
||||
```bash
|
||||
# After navigating to bot chat, input is focused
|
||||
osascript -e '
|
||||
tell application "System Events"
|
||||
keystroke "/start"
|
||||
delay 0.3
|
||||
key code 36
|
||||
end tell
|
||||
'
|
||||
```
|
||||
|
||||
### Send Long Message
|
||||
|
||||
```bash
|
||||
osascript -e '
|
||||
tell application "Telegram" to activate
|
||||
delay 0.5
|
||||
set the clipboard to "Tell me about quantum computing in detail"
|
||||
tell application "System Events"
|
||||
keystroke "v" using command down
|
||||
delay 0.3
|
||||
key code 36
|
||||
end tell
|
||||
'
|
||||
```
|
||||
|
||||
### Verify Response
|
||||
|
||||
```bash
|
||||
sleep 10
|
||||
screencapture /tmp/telegram-bot-response.png
|
||||
```
|
||||
|
||||
### Telegram Bot API (programmatic alternative)
|
||||
|
||||
For sending messages directly to the bot's chat without UI:
|
||||
|
||||
```bash
|
||||
# Send message as the bot (for testing webhooks/responses)
|
||||
curl -s "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/sendMessage" \
|
||||
-d "chat_id=$CHAT_ID&text=test message"
|
||||
|
||||
# Get recent updates
|
||||
curl -s "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/getUpdates?limit=5" | jq .
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Client: WeChat / 微信
|
||||
|
||||
**App name:** `微信` or `WeChat` | **Process name:** `WeChat`
|
||||
|
||||
### Activate & Navigate
|
||||
|
||||
```bash
|
||||
# Activate WeChat
|
||||
osascript -e 'tell application "微信" to activate'
|
||||
sleep 1
|
||||
|
||||
# Search for a contact/bot (Cmd+F)
|
||||
osascript -e '
|
||||
tell application "System Events"
|
||||
keystroke "f" using command down
|
||||
delay 0.5
|
||||
keystroke "TestBot"
|
||||
delay 1
|
||||
key code 36 -- Enter to select
|
||||
end tell
|
||||
'
|
||||
sleep 2
|
||||
```
|
||||
|
||||
### Send Message
|
||||
|
||||
```bash
|
||||
# After navigating to a chat, the input is focused
|
||||
osascript -e '
|
||||
tell application "System Events"
|
||||
keystroke "Hello bot!"
|
||||
delay 0.3
|
||||
key code 36
|
||||
end tell
|
||||
'
|
||||
```
|
||||
|
||||
### Send Long Message (clipboard)
|
||||
|
||||
```bash
|
||||
osascript -e '
|
||||
tell application "微信" to activate
|
||||
delay 0.5
|
||||
set the clipboard to "Please help me with this task..."
|
||||
tell application "System Events"
|
||||
keystroke "v" using command down
|
||||
delay 0.3
|
||||
key code 36
|
||||
end tell
|
||||
'
|
||||
```
|
||||
|
||||
### Verify Response
|
||||
|
||||
```bash
|
||||
sleep 10
|
||||
screencapture /tmp/wechat-bot-response.png
|
||||
```
|
||||
|
||||
### WeChat-Specific Notes
|
||||
|
||||
- WeChat macOS app name can be `微信` or `WeChat` depending on system language. Try both:
|
||||
```bash
|
||||
osascript -e 'tell application "微信" to activate' 2> /dev/null \
|
||||
|| osascript -e 'tell application "WeChat" to activate'
|
||||
```
|
||||
- WeChat uses **Enter** to send (not Cmd+Enter by default, but configurable)
|
||||
- For multi-line messages without sending, use **Shift+Enter**:
|
||||
```bash
|
||||
osascript -e 'tell application "System Events" to key code 36 using shift down'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Client: Lark / 飞书
|
||||
|
||||
**App name:** `Lark` or `飞书` | **Process name:** `Lark` or `飞书`
|
||||
|
||||
### Activate & Navigate
|
||||
|
||||
```bash
|
||||
# Activate Lark (auto-detects Lark or 飞书)
|
||||
osascript -e 'tell application "Lark" to activate' 2> /dev/null \
|
||||
|| osascript -e 'tell application "飞书" to activate'
|
||||
sleep 1
|
||||
|
||||
# Quick Switcher / Search (Cmd+K)
|
||||
osascript -e 'tell application "System Events" to keystroke "k" using command down'
|
||||
sleep 0.5
|
||||
osascript -e '
|
||||
set the clipboard to "bot-testing"
|
||||
tell application "System Events"
|
||||
keystroke "v" using command down
|
||||
delay 1.5
|
||||
key code 36 -- Enter
|
||||
end tell
|
||||
'
|
||||
sleep 2
|
||||
```
|
||||
|
||||
### Send Message to Bot
|
||||
|
||||
```bash
|
||||
osascript -e '
|
||||
set the clipboard to "@MyBot help me with this task"
|
||||
tell application "System Events"
|
||||
keystroke "v" using command down
|
||||
delay 0.3
|
||||
key code 36 -- Enter
|
||||
end tell
|
||||
'
|
||||
```
|
||||
|
||||
### Verify Response
|
||||
|
||||
```bash
|
||||
sleep 10
|
||||
screencapture /tmp/lark-bot-response.png
|
||||
```
|
||||
|
||||
### Lark-Specific Notes
|
||||
|
||||
- App name varies: `Lark` (international) vs `飞书` (China mainland) — the script auto-detects
|
||||
- Uses `Cmd+K` for quick search (same as Discord/Slack)
|
||||
- Enter sends message by default
|
||||
|
||||
---
|
||||
|
||||
## Client: QQ
|
||||
|
||||
**App name:** `QQ` | **Process name:** `QQ`
|
||||
|
||||
### Activate & Navigate
|
||||
|
||||
```bash
|
||||
osascript -e 'tell application "QQ" to activate'
|
||||
sleep 1
|
||||
|
||||
# Search for contact/group (Cmd+F)
|
||||
osascript -e '
|
||||
tell application "System Events"
|
||||
keystroke "f" using command down
|
||||
delay 0.8
|
||||
end tell
|
||||
'
|
||||
osascript -e '
|
||||
set the clipboard to "bot-testing"
|
||||
tell application "System Events"
|
||||
keystroke "v" using command down
|
||||
delay 1.5
|
||||
key code 36 -- Enter
|
||||
end tell
|
||||
'
|
||||
sleep 2
|
||||
```
|
||||
|
||||
### Send Message to Bot
|
||||
|
||||
```bash
|
||||
osascript -e '
|
||||
set the clipboard to "Hello bot!"
|
||||
tell application "System Events"
|
||||
keystroke "v" using command down
|
||||
delay 0.3
|
||||
key code 36 -- Enter
|
||||
end tell
|
||||
'
|
||||
```
|
||||
|
||||
### Verify Response
|
||||
|
||||
```bash
|
||||
sleep 10
|
||||
screencapture /tmp/qq-bot-response.png
|
||||
```
|
||||
|
||||
### QQ-Specific Notes
|
||||
|
||||
- Enter sends message by default; Shift+Enter for newlines
|
||||
- Uses `Cmd+F` for search
|
||||
- Always use clipboard paste for CJK characters
|
||||
|
||||
---
|
||||
|
||||
## Common Bot Testing Workflow (osascript)
|
||||
|
||||
Regardless of platform, the pattern is:
|
||||
|
||||
```bash
|
||||
APP_NAME="Discord" # or "Slack", "Telegram", "微信"
|
||||
CHANNEL="bot-testing"
|
||||
MESSAGE="Hello bot!"
|
||||
WAIT_SECONDS=10
|
||||
|
||||
# 1. Activate
|
||||
osascript -e "tell application \"$APP_NAME\" to activate"
|
||||
sleep 1
|
||||
|
||||
# 2. Navigate to channel/chat (via Quick Switcher or Search)
|
||||
osascript -e 'tell application "System Events" to keystroke "k" using command down'
|
||||
sleep 0.5
|
||||
osascript -e "tell application \"System Events\" to keystroke \"$CHANNEL\""
|
||||
sleep 1
|
||||
osascript -e 'tell application "System Events" to key code 36'
|
||||
sleep 2
|
||||
|
||||
# 3. Send message
|
||||
osascript -e "set the clipboard to \"$MESSAGE\""
|
||||
osascript -e '
|
||||
tell application "System Events"
|
||||
keystroke "v" using command down
|
||||
delay 0.3
|
||||
key code 36
|
||||
end tell
|
||||
'
|
||||
|
||||
# 4. Wait for bot response
|
||||
sleep "$WAIT_SECONDS"
|
||||
|
||||
# 5. Screenshot for verification
|
||||
screencapture /tmp/"${APP_NAME,,}"-bot-test.png
|
||||
echo "Result saved to /tmp/${APP_NAME,,}-bot-test.png"
|
||||
```
|
||||
|
||||
### Tips
|
||||
|
||||
- **Use clipboard paste** (`Cmd+V`) for messages containing special characters or long text — `keystroke` can mangle non-ASCII
|
||||
- **Add `delay`** between actions — apps need time to process UI events
|
||||
- **Screenshot for verification** — use `screencapture` + `Read` tool for visual checks
|
||||
- **Use a dedicated test channel/chat** — avoid polluting real conversations
|
||||
- **Check app name** — some apps have different names in different locales (e.g., `微信` vs `WeChat`)
|
||||
- **Accessibility permissions required** — System Events automation requires granting Accessibility access in System Preferences > Privacy & Security > Accessibility
|
||||
|
||||
---
|
||||
|
||||
# Scripts
|
||||
|
||||
Ready-to-use scripts in `.agents/skills/local-testing/scripts/`:
|
||||
|
||||
| Script | Usage |
|
||||
| ------------------------- | --------------------------------------------- |
|
||||
| `capture-app-window.sh` | Capture screenshot of a specific app window |
|
||||
| `record-electron-demo.sh` | Record Electron app demo with ffmpeg |
|
||||
| `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 |
|
||||
|
||||
### Window Screenshot Utility
|
||||
|
||||
`capture-app-window.sh` captures a screenshot of a specific app window using `screencapture -l <windowID>`. It uses Swift + CGWindowList to find the window by process name, so screenshots work correctly even when the window is on an external monitor or behind other windows.
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
All bot test scripts use this utility automatically for their screenshots.
|
||||
|
||||
### Bot Test Scripts
|
||||
|
||||
All bot test scripts share the same interface:
|
||||
|
||||
```bash
|
||||
./scripts/test-<platform>-bot.sh <channel_or_contact> <message> [wait_seconds] [screenshot_path]
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
# Screen Recording
|
||||
|
||||
Record automated demos by combining `ffmpeg` screen capture with `agent-browser` automation. The script `.agents/skills/local-testing/scripts/record-electron-demo.sh` handles the full lifecycle for Electron.
|
||||
|
||||
### Usage
|
||||
|
||||
```bash
|
||||
# Run the built-in demo (queue-edit feature)
|
||||
./.agents/skills/local-testing/scripts/record-electron-demo.sh
|
||||
|
||||
# Run a custom automation script
|
||||
./.agents/skills/local-testing/scripts/record-electron-demo.sh ./my-demo.sh /tmp/my-demo.mp4
|
||||
```
|
||||
|
||||
The script automatically:
|
||||
|
||||
1. Starts Electron with CDP and waits for SPA to load
|
||||
2. Detects window position, screen, and Retina scale via Swift/CGWindowList
|
||||
3. Records only the Electron window region using `ffmpeg -f avfoundation` with crop
|
||||
4. Runs the demo (built-in or custom script receiving CDP port as `$1`)
|
||||
5. Stops recording and cleans up
|
||||
|
||||
---
|
||||
|
||||
# Gotchas
|
||||
|
||||
### agent-browser
|
||||
|
||||
- **Daemon can get stuck** — if commands hang, `pkill -f agent-browser` to reset
|
||||
- **`agent-browser wait` blocks the daemon** — for waits >30s, use bash `sleep`
|
||||
- **HMR invalidates everything** — after code changes, refs break. Re-snapshot or restart
|
||||
- **`snapshot -i` doesn't find contenteditable** — use `snapshot -i -C` for rich text editors
|
||||
- **`fill` doesn't work on contenteditable** — use `type` for chat inputs
|
||||
- **Screenshots go to `~/.agent-browser/tmp/screenshots/`** — read them with the `Read` tool
|
||||
|
||||
### Electron-specific
|
||||
|
||||
- **`npx electron-vite dev` must run from `apps/desktop/`** — running from project root fails silently
|
||||
- **Don't resize the Electron window after load** — resizing triggers full SPA reload
|
||||
- **Store is at `window.__LOBE_STORES`** not `window.__ZUSTAND_STORES__`
|
||||
|
||||
### osascript
|
||||
|
||||
- **Accessibility permission required** — first run will prompt for access; grant it in System Preferences > Privacy & Security > Accessibility for Terminal / iTerm / Claude Code
|
||||
- **`keystroke` is slow for long text** — always use clipboard paste (`Cmd+V`) for messages over \~20 characters
|
||||
- **`keystroke` can mangle non-ASCII** — use clipboard paste for Chinese, emoji, or special characters
|
||||
- **`key code 36` is Enter** — this is the hardware key code, works regardless of keyboard layout
|
||||
- **`entire contents` is extremely slow** — avoid for complex UIs; use screenshots instead
|
||||
- **App name varies by locale** — `微信` vs `WeChat`, `企业微信` vs `WeCom`; handle both
|
||||
- **WeChat Enter sends immediately** — use `Shift+Enter` for newlines within a message
|
||||
- **Rate limiting** — don't send messages too fast; platforms may throttle or flag automated input
|
||||
- **Lark / 飞书 app name varies** — `Lark` (international) vs `飞书` (China mainland); scripts auto-detect
|
||||
- **QQ uses `Cmd+F` for search** — not `Cmd+K` like Discord/Slack/Lark
|
||||
- **Bot response times vary** — AI-powered bots may take 10-60s; use generous sleep values
|
||||
54
.agents/skills/local-testing/scripts/capture-app-window.sh
Executable file
54
.agents/skills/local-testing/scripts/capture-app-window.sh
Executable file
|
|
@ -0,0 +1,54 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# capture-app-window.sh — Capture a screenshot of a specific app window
|
||||
#
|
||||
# Uses CGWindowList via Swift to find the window by process name, then
|
||||
# screencapture -l <windowID> to capture only that window.
|
||||
# Falls back to full-screen capture if the window is not found.
|
||||
#
|
||||
# Usage:
|
||||
# ./capture-app-window.sh <process_name> <output_path>
|
||||
#
|
||||
# Arguments:
|
||||
# process_name — The process/owner name as shown in Activity Monitor
|
||||
# (e.g., "Discord", "Slack", "Telegram", "WeChat", "QQ", "Lark")
|
||||
# output_path — Path to save the screenshot (e.g., /tmp/screenshot.png)
|
||||
#
|
||||
# Examples:
|
||||
# ./capture-app-window.sh "Discord" /tmp/discord.png
|
||||
# ./capture-app-window.sh "Slack" /tmp/slack.png
|
||||
# ./capture-app-window.sh "微信" /tmp/wechat.png
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
PROCESS="${1:?Usage: capture-app-window.sh <process_name> <output_path>}"
|
||||
OUTPUT="${2:?Usage: capture-app-window.sh <process_name> <output_path>}"
|
||||
|
||||
# Find the CGWindowID for the target process using Swift + CGWindowList
|
||||
# Pass process name via environment variable (swift -e doesn't support -- args)
|
||||
WINDOW_ID=$(TARGET_PROCESS="$PROCESS" swift -e '
|
||||
import Cocoa
|
||||
import Foundation
|
||||
let target = ProcessInfo.processInfo.environment["TARGET_PROCESS"] ?? ""
|
||||
let windowList = CGWindowListCopyWindowInfo([.optionAll], kCGNullWindowID) as! [[String: Any]]
|
||||
for w in windowList {
|
||||
let owner = w["kCGWindowOwnerName"] as? String ?? ""
|
||||
let layer = w["kCGWindowLayer"] as? Int ?? -1
|
||||
let bounds = w["kCGWindowBounds"] as? [String: Any] ?? [:]
|
||||
let ww = bounds["Width"] as? Double ?? 0
|
||||
let wh = bounds["Height"] as? Double ?? 0
|
||||
let wid = w["kCGWindowNumber"] as? Int ?? 0
|
||||
// Match process name, normal window layer (0), and reasonable size
|
||||
if owner == target && layer == 0 && ww > 200 && wh > 200 {
|
||||
print(wid)
|
||||
break
|
||||
}
|
||||
}
|
||||
' 2>/dev/null || true)
|
||||
|
||||
if [ -n "$WINDOW_ID" ]; then
|
||||
screencapture -l "$WINDOW_ID" -x "$OUTPUT"
|
||||
else
|
||||
echo "[capture] Warning: Could not find window for '$PROCESS', falling back to full screen"
|
||||
screencapture -x "$OUTPUT"
|
||||
fi
|
||||
64
.agents/skills/local-testing/scripts/test-discord-bot.sh
Executable file
64
.agents/skills/local-testing/scripts/test-discord-bot.sh
Executable file
|
|
@ -0,0 +1,64 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# test-discord-bot.sh — Send a message to a Discord bot and capture the response
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/test-discord-bot.sh <channel> <message> [wait_seconds] [screenshot_path]
|
||||
#
|
||||
# channel — Channel name to navigate to via Quick Switcher (Cmd+K)
|
||||
# message — Message to send to the bot
|
||||
# wait_seconds — Seconds to wait for bot response (default: 10)
|
||||
# screenshot_path — Output screenshot path (default: /tmp/discord-bot-test.png)
|
||||
#
|
||||
# Prerequisites:
|
||||
# - Discord desktop app installed and logged in
|
||||
# - Accessibility permission granted (System Preferences > Privacy > Accessibility)
|
||||
#
|
||||
# Examples:
|
||||
# ./scripts/test-discord-bot.sh "bot-testing" "!ping"
|
||||
# ./scripts/test-discord-bot.sh "bot-testing" "/ask Tell me a joke" 30
|
||||
# ./scripts/test-discord-bot.sh "general" "Hello bot" 15 /tmp/my-test.png
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
CHANNEL="${1:?Usage: test-discord-bot.sh <channel> <message> [wait_seconds] [screenshot_path]}"
|
||||
MESSAGE="${2:?Usage: test-discord-bot.sh <channel> <message> [wait_seconds] [screenshot_path]}"
|
||||
WAIT="${3:-10}"
|
||||
SCREENSHOT="${4:-/tmp/discord-bot-test.png}"
|
||||
|
||||
APP="Discord"
|
||||
|
||||
echo "[$APP] Activating..."
|
||||
osascript -e "tell application \"$APP\" to activate"
|
||||
sleep 1
|
||||
|
||||
echo "[$APP] Navigating to channel: $CHANNEL"
|
||||
osascript -e '
|
||||
tell application "System Events"
|
||||
-- Quick Switcher
|
||||
keystroke "k" using command down
|
||||
delay 0.8
|
||||
keystroke "'"$CHANNEL"'"
|
||||
delay 1.5
|
||||
key code 36 -- Enter
|
||||
end tell
|
||||
'
|
||||
sleep 2
|
||||
|
||||
echo "[$APP] Sending message: $MESSAGE"
|
||||
osascript -e '
|
||||
set the clipboard to "'"$MESSAGE"'"
|
||||
tell application "System Events"
|
||||
keystroke "v" using command down
|
||||
delay 0.3
|
||||
key code 36 -- Enter
|
||||
end tell
|
||||
'
|
||||
|
||||
echo "[$APP] Waiting ${WAIT}s for bot response..."
|
||||
sleep "$WAIT"
|
||||
|
||||
echo "[$APP] Capturing screenshot..."
|
||||
"$SCRIPT_DIR/capture-app-window.sh" "$APP" "$SCREENSHOT"
|
||||
echo "[$APP] Done! Screenshot saved to $SCREENSHOT"
|
||||
84
.agents/skills/local-testing/scripts/test-lark-bot.sh
Executable file
84
.agents/skills/local-testing/scripts/test-lark-bot.sh
Executable file
|
|
@ -0,0 +1,84 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# test-lark-bot.sh — Send a message to a Lark/Feishu bot and capture the response
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/test-lark-bot.sh <chat> <message> [wait_seconds] [screenshot_path]
|
||||
#
|
||||
# chat — Chat or contact name to search for
|
||||
# message — Message to send to the bot
|
||||
# wait_seconds — Seconds to wait for bot response (default: 10)
|
||||
# screenshot_path — Output screenshot path (default: /tmp/lark-bot-test.png)
|
||||
#
|
||||
# Prerequisites:
|
||||
# - Lark (飞书) desktop app installed and logged in
|
||||
# - Accessibility permission granted (System Preferences > Privacy > Accessibility)
|
||||
#
|
||||
# Notes:
|
||||
# - The app name may be "Lark" or "飞书" depending on version/locale
|
||||
# - Uses Cmd+K to open search/quick switcher
|
||||
# - Enter sends message by default
|
||||
#
|
||||
# Examples:
|
||||
# ./scripts/test-lark-bot.sh "TestBot" "Hello"
|
||||
# ./scripts/test-lark-bot.sh "bot-testing" "/ask Tell me a joke" 30
|
||||
# ./scripts/test-lark-bot.sh "MyBot" "Help me summarize this" 60 /tmp/my-test.png
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
CHAT="${1:?Usage: test-lark-bot.sh <chat> <message> [wait_seconds] [screenshot_path]}"
|
||||
MESSAGE="${2:?Usage: test-lark-bot.sh <chat> <message> [wait_seconds] [screenshot_path]}"
|
||||
WAIT="${3:-10}"
|
||||
SCREENSHOT="${4:-/tmp/lark-bot-test.png}"
|
||||
|
||||
# Detect app name — "Lark" or "飞书"
|
||||
APP=""
|
||||
if osascript -e 'tell application "Lark" to name' &>/dev/null; then
|
||||
APP="Lark"
|
||||
elif osascript -e 'tell application "飞书" to name' &>/dev/null; then
|
||||
APP="飞书"
|
||||
else
|
||||
echo "[error] Lark/飞书 app not found. Install Lark or 飞书."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[$APP] Activating..."
|
||||
osascript -e "tell application \"$APP\" to activate"
|
||||
sleep 1
|
||||
|
||||
echo "[$APP] Searching for chat: $CHAT"
|
||||
osascript -e '
|
||||
tell application "System Events"
|
||||
-- Quick Switcher / Search (Cmd+K)
|
||||
keystroke "k" using command down
|
||||
delay 0.8
|
||||
end tell
|
||||
'
|
||||
# Use clipboard for chat name (supports CJK characters)
|
||||
osascript -e '
|
||||
set the clipboard to "'"$CHAT"'"
|
||||
tell application "System Events"
|
||||
keystroke "v" using command down
|
||||
delay 1.5
|
||||
key code 36 -- Enter to select first result
|
||||
end tell
|
||||
'
|
||||
sleep 2
|
||||
|
||||
echo "[$APP] Sending message: $MESSAGE"
|
||||
osascript -e '
|
||||
set the clipboard to "'"$MESSAGE"'"
|
||||
tell application "System Events"
|
||||
keystroke "v" using command down
|
||||
delay 0.3
|
||||
key code 36 -- Enter to send
|
||||
end tell
|
||||
'
|
||||
|
||||
echo "[$APP] Waiting ${WAIT}s for bot response..."
|
||||
sleep "$WAIT"
|
||||
|
||||
echo "[$APP] Capturing screenshot..."
|
||||
"$SCRIPT_DIR/capture-app-window.sh" "$APP" "$SCREENSHOT"
|
||||
echo "[$APP] Done! Screenshot saved to $SCREENSHOT"
|
||||
76
.agents/skills/local-testing/scripts/test-qq-bot.sh
Executable file
76
.agents/skills/local-testing/scripts/test-qq-bot.sh
Executable file
|
|
@ -0,0 +1,76 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# test-qq-bot.sh — Send a message to a QQ bot and capture the response
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/test-qq-bot.sh <contact> <message> [wait_seconds] [screenshot_path]
|
||||
#
|
||||
# contact — Contact, group, or bot name to search for
|
||||
# message — Message to send
|
||||
# wait_seconds — Seconds to wait for bot response (default: 10)
|
||||
# screenshot_path — Output screenshot path (default: /tmp/qq-bot-test.png)
|
||||
#
|
||||
# Prerequisites:
|
||||
# - QQ desktop app installed and logged in
|
||||
# - Accessibility permission granted (System Preferences > Privacy > Accessibility)
|
||||
#
|
||||
# Notes:
|
||||
# - The app name is "QQ"
|
||||
# - Uses Cmd+F to open search
|
||||
# - Enter sends message by default; Shift+Enter for newlines
|
||||
# - Uses clipboard paste for CJK character support
|
||||
#
|
||||
# Examples:
|
||||
# ./scripts/test-qq-bot.sh "TestBot" "Hello"
|
||||
# ./scripts/test-qq-bot.sh "bot-testing" "Hello bot" 30
|
||||
# ./scripts/test-qq-bot.sh "MyBot" "/help" 15 /tmp/my-test.png
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
CONTACT="${1:?Usage: test-qq-bot.sh <contact> <message> [wait_seconds] [screenshot_path]}"
|
||||
MESSAGE="${2:?Usage: test-qq-bot.sh <contact> <message> [wait_seconds] [screenshot_path]}"
|
||||
WAIT="${3:-10}"
|
||||
SCREENSHOT="${4:-/tmp/qq-bot-test.png}"
|
||||
|
||||
APP="QQ"
|
||||
|
||||
echo "[$APP] Activating..."
|
||||
osascript -e "tell application \"$APP\" to activate"
|
||||
sleep 1
|
||||
|
||||
echo "[$APP] Searching for contact: $CONTACT"
|
||||
osascript -e '
|
||||
tell application "System Events"
|
||||
-- Search (Cmd+F)
|
||||
keystroke "f" using command down
|
||||
delay 0.8
|
||||
end tell
|
||||
'
|
||||
# Use clipboard for contact name (supports CJK characters)
|
||||
osascript -e '
|
||||
set the clipboard to "'"$CONTACT"'"
|
||||
tell application "System Events"
|
||||
keystroke "v" using command down
|
||||
delay 1.5
|
||||
key code 36 -- Enter to select first result
|
||||
end tell
|
||||
'
|
||||
sleep 2
|
||||
|
||||
echo "[$APP] Sending message: $MESSAGE"
|
||||
osascript -e '
|
||||
set the clipboard to "'"$MESSAGE"'"
|
||||
tell application "System Events"
|
||||
keystroke "v" using command down
|
||||
delay 0.3
|
||||
key code 36 -- Enter to send
|
||||
end tell
|
||||
'
|
||||
|
||||
echo "[$APP] Waiting ${WAIT}s for bot response..."
|
||||
sleep "$WAIT"
|
||||
|
||||
echo "[$APP] Capturing screenshot..."
|
||||
"$SCRIPT_DIR/capture-app-window.sh" "$APP" "$SCREENSHOT"
|
||||
echo "[$APP] Done! Screenshot saved to $SCREENSHOT"
|
||||
64
.agents/skills/local-testing/scripts/test-slack-bot.sh
Executable file
64
.agents/skills/local-testing/scripts/test-slack-bot.sh
Executable file
|
|
@ -0,0 +1,64 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# test-slack-bot.sh — Send a message to a Slack bot and capture the response
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/test-slack-bot.sh <channel> <message> [wait_seconds] [screenshot_path]
|
||||
#
|
||||
# channel — Channel name to navigate to via Quick Switcher (Cmd+K)
|
||||
# message — Message to send (e.g., "@mybot hello" or "/ask question")
|
||||
# wait_seconds — Seconds to wait for bot response (default: 10)
|
||||
# screenshot_path — Output screenshot path (default: /tmp/slack-bot-test.png)
|
||||
#
|
||||
# Prerequisites:
|
||||
# - Slack desktop app installed and logged in
|
||||
# - Accessibility permission granted (System Preferences > Privacy > Accessibility)
|
||||
#
|
||||
# Examples:
|
||||
# ./scripts/test-slack-bot.sh "bot-testing" "@mybot hello"
|
||||
# ./scripts/test-slack-bot.sh "bot-testing" "/ask What is 2+2?" 20
|
||||
# ./scripts/test-slack-bot.sh "general" "Hey bot" 15 /tmp/my-test.png
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
CHANNEL="${1:?Usage: test-slack-bot.sh <channel> <message> [wait_seconds] [screenshot_path]}"
|
||||
MESSAGE="${2:?Usage: test-slack-bot.sh <channel> <message> [wait_seconds] [screenshot_path]}"
|
||||
WAIT="${3:-10}"
|
||||
SCREENSHOT="${4:-/tmp/slack-bot-test.png}"
|
||||
|
||||
APP="Slack"
|
||||
|
||||
echo "[$APP] Activating..."
|
||||
osascript -e "tell application \"$APP\" to activate"
|
||||
sleep 1
|
||||
|
||||
echo "[$APP] Navigating to channel: $CHANNEL"
|
||||
osascript -e '
|
||||
tell application "System Events"
|
||||
-- Quick Switcher
|
||||
keystroke "k" using command down
|
||||
delay 0.8
|
||||
keystroke "'"$CHANNEL"'"
|
||||
delay 1.5
|
||||
key code 36 -- Enter
|
||||
end tell
|
||||
'
|
||||
sleep 2
|
||||
|
||||
echo "[$APP] Sending message: $MESSAGE"
|
||||
osascript -e '
|
||||
set the clipboard to "'"$MESSAGE"'"
|
||||
tell application "System Events"
|
||||
keystroke "v" using command down
|
||||
delay 0.3
|
||||
key code 36 -- Enter
|
||||
end tell
|
||||
'
|
||||
|
||||
echo "[$APP] Waiting ${WAIT}s for bot response..."
|
||||
sleep "$WAIT"
|
||||
|
||||
echo "[$APP] Capturing screenshot..."
|
||||
"$SCRIPT_DIR/capture-app-window.sh" "$APP" "$SCREENSHOT"
|
||||
echo "[$APP] Done! Screenshot saved to $SCREENSHOT"
|
||||
79
.agents/skills/local-testing/scripts/test-telegram-bot.sh
Executable file
79
.agents/skills/local-testing/scripts/test-telegram-bot.sh
Executable file
|
|
@ -0,0 +1,79 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# test-telegram-bot.sh — Send a message to a Telegram bot and capture the response
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/test-telegram-bot.sh <bot_or_chat> <message> [wait_seconds] [screenshot_path]
|
||||
#
|
||||
# bot_or_chat — Bot username or chat name to search for
|
||||
# message — Message to send to the bot
|
||||
# wait_seconds — Seconds to wait for bot response (default: 10)
|
||||
# screenshot_path — Output screenshot path (default: /tmp/telegram-bot-test.png)
|
||||
#
|
||||
# Prerequisites:
|
||||
# - Telegram desktop app installed and logged in
|
||||
# - Accessibility permission granted (System Preferences > Privacy > Accessibility)
|
||||
#
|
||||
# Notes:
|
||||
# - The app name may be "Telegram" or "Telegram Desktop" depending on installation
|
||||
# - Uses Cmd+F to search for the bot, then Enter to open the chat
|
||||
#
|
||||
# Examples:
|
||||
# ./scripts/test-telegram-bot.sh "MyTestBot" "/start"
|
||||
# ./scripts/test-telegram-bot.sh "MyTestBot" "Hello bot" 30
|
||||
# ./scripts/test-telegram-bot.sh "GPTBot" "/ask What is AI?" 60 /tmp/my-test.png
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
BOT="${1:?Usage: test-telegram-bot.sh <bot_or_chat> <message> [wait_seconds] [screenshot_path]}"
|
||||
MESSAGE="${2:?Usage: test-telegram-bot.sh <bot_or_chat> <message> [wait_seconds] [screenshot_path]}"
|
||||
WAIT="${3:-10}"
|
||||
SCREENSHOT="${4:-/tmp/telegram-bot-test.png}"
|
||||
|
||||
# Detect app name — "Telegram" or "Telegram Desktop"
|
||||
APP=""
|
||||
if osascript -e 'tell application "Telegram" to name' &>/dev/null; then
|
||||
APP="Telegram"
|
||||
elif osascript -e 'tell application "Telegram Desktop" to name' &>/dev/null; then
|
||||
APP="Telegram Desktop"
|
||||
else
|
||||
echo "[error] Telegram app not found. Install Telegram or Telegram Desktop."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[$APP] Activating..."
|
||||
osascript -e "tell application \"$APP\" to activate"
|
||||
sleep 1
|
||||
|
||||
echo "[$APP] Searching for: $BOT"
|
||||
osascript -e '
|
||||
tell application "System Events"
|
||||
-- Search (Escape first to clear any existing state)
|
||||
key code 53 -- Escape
|
||||
delay 0.3
|
||||
keystroke "f" using command down
|
||||
delay 0.8
|
||||
keystroke "'"$BOT"'"
|
||||
delay 2
|
||||
key code 36 -- Enter to select first result
|
||||
end tell
|
||||
'
|
||||
sleep 2
|
||||
|
||||
echo "[$APP] Sending message: $MESSAGE"
|
||||
osascript -e '
|
||||
set the clipboard to "'"$MESSAGE"'"
|
||||
tell application "System Events"
|
||||
keystroke "v" using command down
|
||||
delay 0.3
|
||||
key code 36 -- Enter
|
||||
end tell
|
||||
'
|
||||
|
||||
echo "[$APP] Waiting ${WAIT}s for bot response..."
|
||||
sleep "$WAIT"
|
||||
|
||||
echo "[$APP] Capturing screenshot..."
|
||||
"$SCRIPT_DIR/capture-app-window.sh" "$APP" "$SCREENSHOT"
|
||||
echo "[$APP] Done! Screenshot saved to $SCREENSHOT"
|
||||
85
.agents/skills/local-testing/scripts/test-wechat-bot.sh
Executable file
85
.agents/skills/local-testing/scripts/test-wechat-bot.sh
Executable file
|
|
@ -0,0 +1,85 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# test-wechat-bot.sh — Send a message to a WeChat bot and capture the response
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/test-wechat-bot.sh <contact> <message> [wait_seconds] [screenshot_path]
|
||||
#
|
||||
# contact — Contact or bot name to search for
|
||||
# message — Message to send
|
||||
# wait_seconds — Seconds to wait for bot response (default: 10)
|
||||
# screenshot_path — Output screenshot path (default: /tmp/wechat-bot-test.png)
|
||||
#
|
||||
# Prerequisites:
|
||||
# - WeChat (微信) desktop app installed and logged in
|
||||
# - Accessibility permission granted (System Preferences > Privacy > Accessibility)
|
||||
#
|
||||
# Notes:
|
||||
# - The app name may be "微信" or "WeChat" depending on system language
|
||||
# - WeChat sends on Enter by default; use Shift+Enter for newlines
|
||||
# - For Chinese text, always uses clipboard paste (keystroke can't handle CJK)
|
||||
#
|
||||
# Examples:
|
||||
# ./scripts/test-wechat-bot.sh "TestBot" "Hello"
|
||||
# ./scripts/test-wechat-bot.sh "文件传输助手" "test message" 5
|
||||
# ./scripts/test-wechat-bot.sh "MyBot" "Tell me a joke" 30 /tmp/my-test.png
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
CONTACT="${1:?Usage: test-wechat-bot.sh <contact> <message> [wait_seconds] [screenshot_path]}"
|
||||
MESSAGE="${2:?Usage: test-wechat-bot.sh <contact> <message> [wait_seconds] [screenshot_path]}"
|
||||
WAIT="${3:-10}"
|
||||
SCREENSHOT="${4:-/tmp/wechat-bot-test.png}"
|
||||
|
||||
# Detect app name — "微信" or "WeChat"
|
||||
APP=""
|
||||
if osascript -e 'tell application "微信" to name' &>/dev/null; then
|
||||
APP="微信"
|
||||
elif osascript -e 'tell application "WeChat" to name' &>/dev/null; then
|
||||
APP="WeChat"
|
||||
else
|
||||
echo "[error] WeChat app not found. Install 微信 (WeChat)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[$APP] Activating..."
|
||||
osascript -e "tell application \"$APP\" to activate"
|
||||
sleep 1
|
||||
|
||||
echo "[$APP] Searching for contact: $CONTACT"
|
||||
osascript -e '
|
||||
tell application "System Events"
|
||||
-- Search (Cmd+F)
|
||||
keystroke "f" using command down
|
||||
delay 0.8
|
||||
end tell
|
||||
'
|
||||
# Use clipboard for contact name (supports CJK characters)
|
||||
osascript -e '
|
||||
set the clipboard to "'"$CONTACT"'"
|
||||
tell application "System Events"
|
||||
keystroke "v" using command down
|
||||
delay 1.5
|
||||
key code 36 -- Enter to select first result
|
||||
end tell
|
||||
'
|
||||
sleep 2
|
||||
|
||||
echo "[$APP] Sending message: $MESSAGE"
|
||||
# Always use clipboard paste — keystroke can't handle CJK or special characters
|
||||
osascript -e '
|
||||
set the clipboard to "'"$MESSAGE"'"
|
||||
tell application "System Events"
|
||||
keystroke "v" using command down
|
||||
delay 0.3
|
||||
key code 36 -- Enter to send
|
||||
end tell
|
||||
'
|
||||
|
||||
echo "[$APP] Waiting ${WAIT}s for bot response..."
|
||||
sleep "$WAIT"
|
||||
|
||||
echo "[$APP] Capturing screenshot..."
|
||||
"$SCRIPT_DIR/capture-app-window.sh" "$APP" "$SCREENSHOT"
|
||||
echo "[$APP] Done! Screenshot saved to $SCREENSHOT"
|
||||
|
|
@ -24,6 +24,8 @@ tags:
|
|||
<Steps>
|
||||
### 访问 Discord 开发者门户
|
||||
|
||||

|
||||
|
||||
访问 [Discord 开发者门户](https://discord.com/developers/applications),点击 **新建应用程序**。为您的应用程序命名(例如,“LobeHub 助手”),然后点击 **创建**。
|
||||
|
||||
### 创建机器人
|
||||
|
|
@ -32,6 +34,8 @@ tags:
|
|||
|
||||
### 启用特权网关意图
|
||||
|
||||

|
||||
|
||||
在机器人设置页面,向下滚动到 **特权网关意图** 并启用以下选项:
|
||||
|
||||
- **消息内容意图** — 允许机器人读取消息内容(必需)
|
||||
|
|
@ -42,12 +46,16 @@ tags:
|
|||
|
||||
### 复制机器人令牌
|
||||
|
||||

|
||||
|
||||
在 **机器人** 页面,点击 **重置令牌** 以生成您的机器人令牌。复制并安全保存该令牌。
|
||||
|
||||
> **重要提示:** 请将您的机器人令牌视为密码。切勿公开分享或提交到版本控制系统。
|
||||
|
||||
### 复制应用程序 ID 和公钥
|
||||
|
||||

|
||||
|
||||
在左侧菜单中,转到 **常规信息**。复制并保存以下内容:
|
||||
|
||||
- **应用程序 ID**
|
||||
|
|
@ -65,6 +73,8 @@ tags:
|
|||
|
||||
### 填写凭据
|
||||
|
||||

|
||||
|
||||
输入以下字段:
|
||||
|
||||
- **应用程序 ID** — 来自 Discord 应用程序常规信息页面的应用程序 ID
|
||||
|
|
@ -83,6 +93,8 @@ tags:
|
|||
<Steps>
|
||||
### 生成邀请链接
|
||||
|
||||

|
||||
|
||||
在 Discord 开发者门户中,转到 **OAuth2** → **URL 生成器**。选择以下范围:
|
||||
|
||||
- `bot`
|
||||
|
|
@ -99,6 +111,8 @@ tags:
|
|||
|
||||
### 授权机器人
|
||||
|
||||

|
||||
|
||||
复制生成的链接,在浏览器中打开,选择您希望添加机器人的服务器,然后点击 **授权**。
|
||||
</Steps>
|
||||
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import { startWebServer, stopWebServer } from '../support/webServer';
|
|||
import type { CustomWorld } from '../support/world';
|
||||
|
||||
process.env['E2E'] = '1';
|
||||
// Set default timeout for all steps to 10 seconds
|
||||
setDefaultTimeout(10_000);
|
||||
// Set default timeout for all steps to 30 seconds
|
||||
setDefaultTimeout(30_000);
|
||||
|
||||
// Store base URL and cached session cookies
|
||||
let baseUrl: string;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,3 @@
|
|||
import {
|
||||
DEFAULT_BOT_HISTORY_LIMIT,
|
||||
MAX_BOT_HISTORY_LIMIT,
|
||||
MIN_BOT_HISTORY_LIMIT,
|
||||
} from '@lobechat/const';
|
||||
import type { BuiltinToolManifest } from '@lobechat/types';
|
||||
|
||||
import { systemPrompt } from './systemRole';
|
||||
|
|
@ -75,7 +70,7 @@ export const MessageManifest: BuiltinToolManifest = {
|
|||
},
|
||||
},
|
||||
{
|
||||
description: `Read recent messages from a channel or conversation. Returns messages in chronological order. Default limit is ${DEFAULT_BOT_HISTORY_LIMIT}. Only pass a different limit if the user explicitly asks for more or fewer messages.`,
|
||||
description: `Read recent messages from a channel or conversation. Returns messages in chronological order.`,
|
||||
name: MessageApiName.readMessages,
|
||||
parameters: {
|
||||
additionalProperties: false,
|
||||
|
|
@ -102,13 +97,6 @@ export const MessageManifest: BuiltinToolManifest = {
|
|||
'End time as Unix second timestamp. Used by Feishu/Lark to filter messages before this time.',
|
||||
type: 'string',
|
||||
},
|
||||
limit: {
|
||||
default: DEFAULT_BOT_HISTORY_LIMIT,
|
||||
description: `Maximum number of messages to fetch (default: ${DEFAULT_BOT_HISTORY_LIMIT}, max: ${MAX_BOT_HISTORY_LIMIT})`,
|
||||
maximum: MAX_BOT_HISTORY_LIMIT,
|
||||
minimum: MIN_BOT_HISTORY_LIMIT,
|
||||
type: 'integer',
|
||||
},
|
||||
platform: {
|
||||
description: 'Target messaging platform',
|
||||
enum: platformEnum,
|
||||
|
|
|
|||
|
|
@ -45,15 +45,18 @@ export const systemPrompt = `You have access to a Message tool that provides uni
|
|||
- When the user asks to "DM me" or "send me a private message", use \`sendDirectMessage\`. If \`userId\` is available from \`listBots\`, use it directly. If not, ask the user for their platform user ID.
|
||||
- **Never ask the user for channel IDs.** Use \`listChannels\` to discover channels yourself. If \`serverId\` is available from \`listBots\`, use it directly. If not, ask the user for the server/guild ID.
|
||||
- When the user references a channel by name (e.g. "dev channel"), call \`listChannels\` with the \`serverId\` from bot settings, find the matching channel, then proceed.
|
||||
- \`readMessages\` is designed for **small, targeted reads** (up to 100 messages per call). For quick context (e.g. "what was just discussed", "summarize the last few messages"), use \`readMessages\` with pagination via the \`before\`/\`after\` parameters.
|
||||
- \`readMessages\`: \`channelId\` and \`platform\` are **required**. All other parameters are **optional** — omit them when not needed. \`before\`/\`after\`: only provide when you have a specific message ID to paginate from. Do NOT pass empty strings — omit entirely. For quick context (e.g. "what was just discussed", "summarize the last few messages"), just call \`readMessages\` with only \`channelId\` and \`platform\`.
|
||||
- **For large-volume requests** (e.g. "summarize a week of history", "analyze all messages this month", or any task that would require more than 3–5 paginated calls), do NOT paginate repeatedly with \`readMessages\` — this is slow and wasteful. Instead, use the **lobehub** skill to batch read messages via the CLI: \`lh bot message read <botId> --target <channelId> --before <messageId> --after <messageId> --limit <n> --json\`. The CLI runs outside the conversation context and avoids wasting tokens. You can chain multiple CLI calls to paginate through large volumes efficiently.
|
||||
- Reactions use unicode emoji (👍) or platform-specific format (Discord custom emoji).
|
||||
</usage_guidelines>
|
||||
|
||||
<platform_notes>
|
||||
**Discord:**
|
||||
- Supports rich embeds, threads as sub-channels, polls, reactions, pins
|
||||
- Supports rich embeds, threads, polls, reactions, pins
|
||||
- serverId (guild ID) needed for listChannels and getMemberInfo
|
||||
- **Channel types:** Discord has text channels (type 0), voice channels (type 2), categories (type 4), forum channels (type 15), and threads (types 10/11/12). Threads are child channels — they have their own unique ID.
|
||||
- **channelId works for both channels and threads.** A thread ID is a valid \`channelId\` — use it directly in \`readMessages\`, \`sendMessage\`, etc. No special handling needed.
|
||||
- To discover channels: use \`listChannels\` (returns guild-level channels). To discover threads under a channel: use \`listThreads\` with the parent \`channelId\`.
|
||||
- Thread creation can be from a message or standalone
|
||||
|
||||
**Telegram:**
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ export interface ReadMessagesParams {
|
|||
cursor?: string;
|
||||
/** End time as Unix second timestamp (Feishu/Lark only) */
|
||||
endTime?: string;
|
||||
/** Max number of messages to fetch (default: 20, max: 100) */
|
||||
/** Max number of messages to fetch */
|
||||
limit?: number;
|
||||
/** Platform to read from */
|
||||
platform: MessagePlatformType;
|
||||
|
|
|
|||
|
|
@ -15,16 +15,27 @@ export const formatBotPlatformContext = ({
|
|||
}: BotPlatformInfo): string => {
|
||||
const lines = [
|
||||
`<bot_platform_context platform="${platformName}">`,
|
||||
`You are currently responding on the **${platformName}** platform.`,
|
||||
`Adapt your responses to this platform's conventions and capabilities.`,
|
||||
`You are a participant in a **${platformName}** conversation — not an external assistant being consulted.`,
|
||||
'',
|
||||
'<behavior>',
|
||||
'- Act like a knowledgeable group member: respond naturally, stay on topic, and match the conversational tone.',
|
||||
'- When the user\'s message references prior context you don\'t have (e.g. "what do you think?", "summarize this", "look at that"), use `readMessages` IMMEDIATELY to fetch recent chat history before responding. Never ask the user to repeat what was already said in the channel.',
|
||||
'- When you lack enough context to give a useful answer, silently read more history rather than asking clarifying questions — the answer is usually already in the chat.',
|
||||
'- Keep responses concise and conversational — IM platforms have character limits and small viewports. Avoid long preambles or formal structure unless the question demands it.',
|
||||
'- Do NOT reference UI elements from other environments (e.g. "check the sidebar", "click the button above").',
|
||||
'- Keep responses concise — IM platforms have character limits and small viewports.',
|
||||
'</behavior>',
|
||||
'',
|
||||
'<message_delivery>',
|
||||
'Your text response is AUTOMATICALLY delivered to the current conversation — the runtime pipeline handles it.',
|
||||
'Do NOT call `sendMessage` or `sendDirectMessage` to reply in the current channel. Just respond with text directly.',
|
||||
'`sendMessage` / `sendDirectMessage` should ONLY be used when the user explicitly asks you to send a message to a DIFFERENT channel or user.',
|
||||
'</message_delivery>',
|
||||
];
|
||||
|
||||
if (!supportsMarkdown) {
|
||||
lines.push(
|
||||
'',
|
||||
'<formatting>',
|
||||
'This platform does NOT support Markdown rendering.',
|
||||
'You MUST NOT use any Markdown formatting in your response, including:',
|
||||
'- **bold**, *italic*, ~~strikethrough~~',
|
||||
|
|
@ -34,6 +45,7 @@ export const formatBotPlatformContext = ({
|
|||
'- Tables, blockquotes, or HTML tags',
|
||||
'',
|
||||
'Use plain text only. Use line breaks, indentation, dashes, and numbering to structure your response for readability.',
|
||||
'</formatting>',
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { CloudSandboxManifest } from '@lobechat/builtin-tool-cloud-sandbox';
|
|||
import { KnowledgeBaseManifest } from '@lobechat/builtin-tool-knowledge-base';
|
||||
import { LocalSystemManifest } from '@lobechat/builtin-tool-local-system';
|
||||
import { MemoryManifest } from '@lobechat/builtin-tool-memory';
|
||||
import { MessageManifest } from '@lobechat/builtin-tool-message';
|
||||
import { RemoteDeviceManifest } from '@lobechat/builtin-tool-remote-device';
|
||||
import { WebBrowsingManifest } from '@lobechat/builtin-tool-web-browsing';
|
||||
import { alwaysOnToolIds, builtinTools, defaultToolIds } from '@lobechat/builtin-tools';
|
||||
|
|
@ -97,6 +98,7 @@ export const createServerAgentToolsEngine = (
|
|||
globalMemoryEnabled = false,
|
||||
hasAgentDocuments = false,
|
||||
hasEnabledKnowledgeBases = false,
|
||||
isBotConversation = false,
|
||||
model,
|
||||
provider,
|
||||
} = params;
|
||||
|
|
@ -140,6 +142,8 @@ export const createServerAgentToolsEngine = (
|
|||
!!deviceContext?.deviceOnline &&
|
||||
!!deviceContext?.autoActivated,
|
||||
[MemoryManifest.identifier]: globalMemoryEnabled,
|
||||
// Only auto-enable in bot conversations; otherwise let user's plugin selection take effect
|
||||
...(isBotConversation && { [MessageManifest.identifier]: true }),
|
||||
[RemoteDeviceManifest.identifier]:
|
||||
!!deviceContext?.gatewayConfigured && !deviceContext?.autoActivated,
|
||||
[AgentDocumentsManifest.identifier]: hasAgentDocuments,
|
||||
|
|
|
|||
|
|
@ -58,6 +58,8 @@ export interface ServerCreateAgentToolsEngineParams {
|
|||
hasAgentDocuments?: boolean;
|
||||
/** Whether agent has enabled knowledge bases */
|
||||
hasEnabledKnowledgeBases?: boolean;
|
||||
/** Whether the request originates from a bot conversation (auto-enables message tool) */
|
||||
isBotConversation?: boolean;
|
||||
/** Model name for function calling compatibility check */
|
||||
model: string;
|
||||
/** Provider name for function calling compatibility check */
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import type { AgentRuntimeContext, AgentState } from '@lobechat/agent-runtime';
|
|||
import { BUILTIN_AGENT_SLUGS, getAgentRuntimeConfig } from '@lobechat/builtin-agents';
|
||||
import { builtinSkills } from '@lobechat/builtin-skills';
|
||||
import { LocalSystemManifest } from '@lobechat/builtin-tool-local-system';
|
||||
import { MessageToolIdentifier } from '@lobechat/builtin-tool-message';
|
||||
import {
|
||||
type DeviceAttachment,
|
||||
generateSystemPrompt,
|
||||
|
|
@ -446,6 +447,10 @@ export class AiAgentService {
|
|||
// Agent documents check is non-critical
|
||||
}
|
||||
|
||||
// Auto-enable message tool when request comes from a bot conversation (e.g., Discord, Slack, Telegram)
|
||||
const isBotConversation = !!(botContext || discordContext);
|
||||
log('execAgent: isBotConversation=%s', isBotConversation);
|
||||
|
||||
// Build device context for ToolsEngine enableChecker
|
||||
const gatewayConfigured = deviceProxy.isConfigured;
|
||||
const boundDeviceId = agentConfig.agencyConfig?.boundDeviceId;
|
||||
|
|
@ -471,6 +476,7 @@ export class AiAgentService {
|
|||
...(agentConfig?.plugins ?? []),
|
||||
...(additionalPluginIds || []),
|
||||
...(hasTopicReference ? ['lobe-topic-reference'] : []),
|
||||
...(isBotConversation ? [MessageToolIdentifier] : []),
|
||||
];
|
||||
|
||||
// Derive activeDeviceId from device context:
|
||||
|
|
@ -501,6 +507,7 @@ export class AiAgentService {
|
|||
globalMemoryEnabled,
|
||||
hasAgentDocuments,
|
||||
hasEnabledKnowledgeBases,
|
||||
isBotConversation,
|
||||
model,
|
||||
provider,
|
||||
});
|
||||
|
|
@ -512,6 +519,7 @@ export class AiAgentService {
|
|||
...(additionalPluginIds || []),
|
||||
LocalSystemManifest.identifier,
|
||||
RemoteDeviceManifest.identifier,
|
||||
...(isBotConversation ? [MessageToolIdentifier] : []),
|
||||
];
|
||||
log('execAgent: agent configured plugins: %O', pluginIds);
|
||||
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ export class DiscordApi {
|
|||
return (await this.rest.get(Routes.channelMessages(channelId), {
|
||||
query: new URLSearchParams(
|
||||
Object.entries(query ?? {})
|
||||
.filter(([, v]) => v !== undefined)
|
||||
.filter(([, v]) => v !== undefined && v !== '')
|
||||
.map(([k, v]) => [k, String(v)]),
|
||||
),
|
||||
})) as RESTGetAPIChannelMessagesResult;
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import {
|
|||
} from '../types';
|
||||
import { formatUsageStats } from '../utils';
|
||||
import { DiscordApi } from './api';
|
||||
import { patchDiscordForwardedInteractions } from './patch';
|
||||
import { patchDiscordForwardedInteractions, patchDiscordThreadRecovery } from './patch';
|
||||
|
||||
const log = debug('bot-platform:discord:bot');
|
||||
|
||||
|
|
@ -179,6 +179,7 @@ class DiscordGatewayClient implements PlatformClient {
|
|||
|
||||
applyChatPatches(chatBot: ChatBot<any>): void {
|
||||
patchDiscordForwardedInteractions(chatBot);
|
||||
patchDiscordThreadRecovery(chatBot);
|
||||
}
|
||||
|
||||
createAdapter(): Record<string, any> {
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
export * from './forwardedInteractions';
|
||||
export * from './threadRecovery';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { patchDiscordThreadRecovery } from './threadRecovery';
|
||||
|
||||
describe('patchDiscordThreadRecovery', () => {
|
||||
it('should recover an existing thread when Discord reports it was already created', async () => {
|
||||
const createDiscordThread = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(
|
||||
new Error(
|
||||
'NetworkError: Discord API error: 400 {"message":"A thread has already been created for this message","code":160004}',
|
||||
),
|
||||
);
|
||||
const discordFetch = vi.fn().mockResolvedValue(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
id: 'message-1',
|
||||
thread: { id: 'thread-1' },
|
||||
}),
|
||||
{ status: 200 },
|
||||
),
|
||||
);
|
||||
|
||||
const adapter = {
|
||||
createDiscordThread,
|
||||
discordFetch,
|
||||
logger: { debug: vi.fn(), error: vi.fn(), warn: vi.fn() },
|
||||
};
|
||||
const chatBot = { adapters: new Map([['discord', adapter]]) } as any;
|
||||
|
||||
patchDiscordThreadRecovery(chatBot);
|
||||
|
||||
await expect(adapter.createDiscordThread('channel-1', 'message-1')).resolves.toEqual({
|
||||
id: 'thread-1',
|
||||
});
|
||||
expect(discordFetch).toHaveBeenCalledWith('/channels/channel-1/messages/message-1', 'GET');
|
||||
});
|
||||
|
||||
it('should rethrow non-recoverable thread creation errors', async () => {
|
||||
const originalError = new Error('Discord API error: 403 {"message":"Missing permissions"}');
|
||||
const createDiscordThread = vi.fn().mockRejectedValueOnce(originalError);
|
||||
const discordFetch = vi.fn();
|
||||
|
||||
const adapter = { createDiscordThread, discordFetch };
|
||||
const chatBot = { adapters: new Map([['discord', adapter]]) } as any;
|
||||
|
||||
patchDiscordThreadRecovery(chatBot);
|
||||
|
||||
await expect(adapter.createDiscordThread('channel-1', 'message-1')).rejects.toBe(originalError);
|
||||
expect(discordFetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should rethrow the original error when recovery cannot find a thread', async () => {
|
||||
const originalError = new Error(
|
||||
'NetworkError: Discord API error: 400 {"message":"A thread has already been created for this message","code":160004}',
|
||||
);
|
||||
const createDiscordThread = vi.fn().mockRejectedValueOnce(originalError);
|
||||
const discordFetch = vi.fn().mockResolvedValue(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
id: 'message-1',
|
||||
}),
|
||||
{ status: 200 },
|
||||
),
|
||||
);
|
||||
|
||||
const adapter = {
|
||||
createDiscordThread,
|
||||
discordFetch,
|
||||
logger: { debug: vi.fn(), error: vi.fn(), warn: vi.fn() },
|
||||
};
|
||||
const chatBot = { adapters: new Map([['discord', adapter]]) } as any;
|
||||
|
||||
patchDiscordThreadRecovery(chatBot);
|
||||
|
||||
await expect(adapter.createDiscordThread('channel-1', 'message-1')).rejects.toBe(originalError);
|
||||
expect(discordFetch).toHaveBeenCalledWith('/channels/channel-1/messages/message-1', 'GET');
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
import type { RESTGetAPIChannelMessageResult } from 'discord-api-types/v10';
|
||||
|
||||
const DISCORD_THREAD_ALREADY_CREATED_CODE = 160004;
|
||||
const PATCHED_FLAG = Symbol.for('lobe.discord.thread-recovery.patched');
|
||||
|
||||
interface RecoverableDiscordThreadAdapter {
|
||||
createDiscordThread: (channelId: string, messageId: string) => Promise<{ id: string }>;
|
||||
discordFetch: (path: string, method: string, body?: Record<string, unknown>) => Promise<Response>;
|
||||
logger?: {
|
||||
debug?: (message: string, metadata?: Record<string, unknown>) => void;
|
||||
error?: (message: string, metadata?: Record<string, unknown>) => void;
|
||||
warn?: (message: string, metadata?: Record<string, unknown>) => void;
|
||||
};
|
||||
[PATCHED_FLAG]?: boolean;
|
||||
}
|
||||
|
||||
const isRecoverableDiscordThreadAdapter = (
|
||||
adapter: unknown,
|
||||
): adapter is RecoverableDiscordThreadAdapter => {
|
||||
if (!adapter || typeof adapter !== 'object') return false;
|
||||
|
||||
return (
|
||||
typeof (adapter as RecoverableDiscordThreadAdapter).createDiscordThread === 'function' &&
|
||||
typeof (adapter as RecoverableDiscordThreadAdapter).discordFetch === 'function'
|
||||
);
|
||||
};
|
||||
|
||||
const getDiscordErrorCode = (error: unknown): number | undefined => {
|
||||
const rawMessage = error instanceof Error ? error.message : String(error);
|
||||
const match = rawMessage.match(/"code"\s*:\s*(\d+)/);
|
||||
|
||||
if (!match) return;
|
||||
|
||||
return Number(match[1]);
|
||||
};
|
||||
|
||||
const getExistingThread = async (
|
||||
adapter: RecoverableDiscordThreadAdapter,
|
||||
channelId: string,
|
||||
messageId: string,
|
||||
): Promise<{ id: string } | undefined> => {
|
||||
try {
|
||||
const response = await adapter.discordFetch(
|
||||
`/channels/${channelId}/messages/${messageId}`,
|
||||
'GET',
|
||||
);
|
||||
const message = (await response.json()) as RESTGetAPIChannelMessageResult;
|
||||
const threadId = message.thread?.id;
|
||||
|
||||
if (!threadId) {
|
||||
adapter.logger?.warn?.('Discord thread recovery could not find thread on starter message', {
|
||||
channelId,
|
||||
messageId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
adapter.logger?.debug?.('Recovered existing Discord thread for starter message', {
|
||||
channelId,
|
||||
messageId,
|
||||
threadId,
|
||||
});
|
||||
|
||||
return { id: threadId };
|
||||
} catch (recoveryError) {
|
||||
adapter.logger?.error?.('Failed to recover existing Discord thread', {
|
||||
channelId,
|
||||
error: String(recoveryError),
|
||||
messageId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
export const patchDiscordThreadRecovery = (chatBot: unknown) => {
|
||||
const adapter = (chatBot as { adapters?: Map<string, unknown> } | undefined)?.adapters?.get?.(
|
||||
'discord',
|
||||
);
|
||||
|
||||
if (!isRecoverableDiscordThreadAdapter(adapter) || adapter[PATCHED_FLAG]) return;
|
||||
|
||||
const originalCreateDiscordThread = adapter.createDiscordThread.bind(adapter);
|
||||
|
||||
adapter.createDiscordThread = async (channelId, messageId) => {
|
||||
try {
|
||||
return await originalCreateDiscordThread(channelId, messageId);
|
||||
} catch (error) {
|
||||
if (getDiscordErrorCode(error) !== DISCORD_THREAD_ALREADY_CREATED_CODE) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const existingThread = await getExistingThread(adapter, channelId, messageId);
|
||||
|
||||
if (existingThread) return existingThread;
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
adapter[PATCHED_FLAG] = true;
|
||||
};
|
||||
|
|
@ -77,8 +77,8 @@ export class DiscordMessageService implements MessageRuntimeService {
|
|||
|
||||
readMessages = async (params: ReadMessagesParams): Promise<ReadMessagesState> => {
|
||||
const messages = await this.api.getMessages(params.channelId, {
|
||||
after: params.after,
|
||||
before: params.before,
|
||||
after: params.after || undefined,
|
||||
before: params.before || undefined,
|
||||
limit: Math.min(params.limit ?? DEFAULT_BOT_HISTORY_LIMIT, MAX_DISCORD_HISTORY_LIMIT),
|
||||
});
|
||||
const items = messages.map(toMessageItem);
|
||||
|
|
|
|||
Loading…
Reference in a new issue