refactor: require target for message actions
This commit is contained in:
parent
87cecd0268
commit
6e4d86f426
@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
### Breaking
|
### Breaking
|
||||||
- **BREAKING:** Channel auth now prefers config over env for Discord/Telegram/Matrix (env is fallback only). (#1040) — thanks @thewilloftheshadow.
|
- **BREAKING:** Channel auth now prefers config over env for Discord/Telegram/Matrix (env is fallback only). (#1040) — thanks @thewilloftheshadow.
|
||||||
|
- **BREAKING:** `clawdbot message` and message tool now require `target` (dropping `to`/`channelId` for destinations).
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
- Tools: improve `web_fetch` extraction using Readability (with fallback).
|
- Tools: improve `web_fetch` extraction using Readability (with fallback).
|
||||||
|
|||||||
@ -16,19 +16,19 @@ read_when:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# WhatsApp
|
# WhatsApp
|
||||||
clawdbot message poll --to +15555550123 \
|
clawdbot message poll --target +15555550123 \
|
||||||
--poll-question "Lunch today?" --poll-option "Yes" --poll-option "No" --poll-option "Maybe"
|
--poll-question "Lunch today?" --poll-option "Yes" --poll-option "No" --poll-option "Maybe"
|
||||||
clawdbot message poll --to 123456789@g.us \
|
clawdbot message poll --target 123456789@g.us \
|
||||||
--poll-question "Meeting time?" --poll-option "10am" --poll-option "2pm" --poll-option "4pm" --poll-multi
|
--poll-question "Meeting time?" --poll-option "10am" --poll-option "2pm" --poll-option "4pm" --poll-multi
|
||||||
|
|
||||||
# Discord
|
# Discord
|
||||||
clawdbot message poll --channel discord --to channel:123456789 \
|
clawdbot message poll --channel discord --target channel:123456789 \
|
||||||
--poll-question "Snack?" --poll-option "Pizza" --poll-option "Sushi"
|
--poll-question "Snack?" --poll-option "Pizza" --poll-option "Sushi"
|
||||||
clawdbot message poll --channel discord --to channel:123456789 \
|
clawdbot message poll --channel discord --target channel:123456789 \
|
||||||
--poll-question "Plan?" --poll-option "A" --poll-option "B" --poll-duration-hours 48
|
--poll-question "Plan?" --poll-option "A" --poll-option "B" --poll-duration-hours 48
|
||||||
|
|
||||||
# MS Teams
|
# MS Teams
|
||||||
clawdbot message poll --channel msteams --to conversation:19:abc@thread.tacv2 \
|
clawdbot message poll --channel msteams --target conversation:19:abc@thread.tacv2 \
|
||||||
--poll-question "Lunch?" --poll-option "Pizza" --poll-option "Sushi"
|
--poll-question "Lunch?" --poll-option "Pizza" --poll-option "Sushi"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@ -447,7 +447,7 @@ By default, Clawdbot only downloads media from Microsoft/Teams hostnames. Overri
|
|||||||
## Polls (Adaptive Cards)
|
## Polls (Adaptive Cards)
|
||||||
Clawdbot sends Teams polls as Adaptive Cards (there is no native Teams poll API).
|
Clawdbot sends Teams polls as Adaptive Cards (there is no native Teams poll API).
|
||||||
|
|
||||||
- CLI: `clawdbot message poll --channel msteams --to conversation:<id> ...`
|
- CLI: `clawdbot message poll --channel msteams --target conversation:<id> ...`
|
||||||
- Votes are recorded by the gateway in `~/.clawdbot/msteams-polls.json`.
|
- Votes are recorded by the gateway in `~/.clawdbot/msteams-polls.json`.
|
||||||
- The gateway must stay online to record votes.
|
- The gateway must stay online to record votes.
|
||||||
- Polls do not auto-post result summaries yet (inspect the store file if needed).
|
- Polls do not auto-post result summaries yet (inspect the store file if needed).
|
||||||
|
|||||||
@ -444,7 +444,7 @@ The agent sees reactions as **system notifications** in the conversation history
|
|||||||
|
|
||||||
## Delivery targets (CLI/cron)
|
## Delivery targets (CLI/cron)
|
||||||
- Use a chat id (`123456789`) or a username (`@name`) as the target.
|
- Use a chat id (`123456789`) or a username (`@name`) as the target.
|
||||||
- Example: `clawdbot message send --channel telegram --to 123456789 --message "hi"`.
|
- Example: `clawdbot message send --channel telegram --target 123456789 --message "hi"`.
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
|
|||||||
@ -124,7 +124,7 @@ Multi-account support: use `channels.zalo.accounts` with per-account tokens and
|
|||||||
|
|
||||||
## Delivery targets (CLI/cron)
|
## Delivery targets (CLI/cron)
|
||||||
- Use a chat id as the target.
|
- Use a chat id as the target.
|
||||||
- Example: `clawdbot message send --channel zalo --to 123456789 --message "hi"`.
|
- Example: `clawdbot message send --channel zalo --target 123456789 --message "hi"`.
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,7 @@ Directory lookups for channels that support it (contacts/peers, groups, and “m
|
|||||||
- `--json`: output JSON
|
- `--json`: output JSON
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
- `directory` is meant to help you find IDs you can paste into other commands (especially `clawdbot message send --to ...`).
|
- `directory` is meant to help you find IDs you can paste into other commands (especially `clawdbot message send --target ...`).
|
||||||
- For many channels, results are config-backed (allowlists / configured groups) rather than a live provider directory.
|
- For many channels, results are config-backed (allowlists / configured groups) rather than a live provider directory.
|
||||||
- Default output is `id` (and sometimes `name`) separated by a tab; use `--json` for scripting.
|
- Default output is `id` (and sometimes `name`) separated by a tab; use `--json` for scripting.
|
||||||
|
|
||||||
@ -23,7 +23,7 @@ Directory lookups for channels that support it (contacts/peers, groups, and “m
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
clawdbot directory peers list --channel slack --query "U0"
|
clawdbot directory peers list --channel slack --query "U0"
|
||||||
clawdbot message send --channel slack --to user:U012ABCDEF --message "hello"
|
clawdbot message send --channel slack --target user:U012ABCDEF --message "hello"
|
||||||
```
|
```
|
||||||
|
|
||||||
## ID formats (by channel)
|
## ID formats (by channel)
|
||||||
|
|||||||
@ -446,8 +446,8 @@ Subcommands:
|
|||||||
- `message event <list|create>`
|
- `message event <list|create>`
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
- `clawdbot message send --to +15555550123 --message "Hi"`
|
- `clawdbot message send --target +15555550123 --message "Hi"`
|
||||||
- `clawdbot message poll --channel discord --to channel:123 --poll-question "Snack?" --poll-option Pizza --poll-option Sushi`
|
- `clawdbot message poll --channel discord --target channel:123 --poll-question "Snack?" --poll-option Pizza --poll-option Sushi`
|
||||||
|
|
||||||
### `agent`
|
### `agent`
|
||||||
Run one agent turn via the Gateway (or `--local` embedded).
|
Run one agent turn via the Gateway (or `--local` embedded).
|
||||||
|
|||||||
@ -21,7 +21,7 @@ Channel selection:
|
|||||||
- If exactly one channel is configured, it becomes the default.
|
- If exactly one channel is configured, it becomes the default.
|
||||||
- Values: `whatsapp|telegram|discord|slack|signal|imessage|msteams`
|
- Values: `whatsapp|telegram|discord|slack|signal|imessage|msteams`
|
||||||
|
|
||||||
Target formats (`--to`):
|
Target formats (`--target`):
|
||||||
- WhatsApp: E.164 or group JID
|
- WhatsApp: E.164 or group JID
|
||||||
- Telegram: chat id or `@username`
|
- Telegram: chat id or `@username`
|
||||||
- Discord: `channel:<id>` or `user:<id>` (or `<@id>` mention; raw numeric ids are treated as channels)
|
- Discord: `channel:<id>` or `user:<id>` (or `<@id>` mention; raw numeric ids are treated as channels)
|
||||||
@ -38,6 +38,7 @@ Name lookup:
|
|||||||
|
|
||||||
- `--channel <name>`
|
- `--channel <name>`
|
||||||
- `--account <id>`
|
- `--account <id>`
|
||||||
|
- `--target <dest>` (target channel or user for send/poll/read/etc)
|
||||||
- `--targets <name>` (repeat; broadcast only)
|
- `--targets <name>` (repeat; broadcast only)
|
||||||
- `--json`
|
- `--json`
|
||||||
- `--dry-run`
|
- `--dry-run`
|
||||||
@ -49,7 +50,7 @@ Name lookup:
|
|||||||
|
|
||||||
- `send`
|
- `send`
|
||||||
- Channels: WhatsApp/Telegram/Discord/Slack/Signal/iMessage/MS Teams
|
- Channels: WhatsApp/Telegram/Discord/Slack/Signal/iMessage/MS Teams
|
||||||
- Required: `--to`, plus `--message` or `--media`
|
- Required: `--target`, plus `--message` or `--media`
|
||||||
- Optional: `--media`, `--reply-to`, `--thread-id`, `--gif-playback`
|
- Optional: `--media`, `--reply-to`, `--thread-id`, `--gif-playback`
|
||||||
- Telegram only: `--buttons` (requires `channels.telegram.capabilities.inlineButtons` to allow it)
|
- Telegram only: `--buttons` (requires `channels.telegram.capabilities.inlineButtons` to allow it)
|
||||||
- Telegram only: `--thread-id` (forum topic id)
|
- Telegram only: `--thread-id` (forum topic id)
|
||||||
@ -58,52 +59,47 @@ Name lookup:
|
|||||||
|
|
||||||
- `poll`
|
- `poll`
|
||||||
- Channels: WhatsApp/Discord/MS Teams
|
- Channels: WhatsApp/Discord/MS Teams
|
||||||
- Required: `--to`, `--poll-question`, `--poll-option` (repeat)
|
- Required: `--target`, `--poll-question`, `--poll-option` (repeat)
|
||||||
- Optional: `--poll-multi`
|
- Optional: `--poll-multi`
|
||||||
- Discord only: `--poll-duration-hours`, `--message`
|
- Discord only: `--poll-duration-hours`, `--message`
|
||||||
|
|
||||||
- `react`
|
- `react`
|
||||||
- Channels: Discord/Slack/Telegram/WhatsApp
|
- Channels: Discord/Slack/Telegram/WhatsApp
|
||||||
- Required: `--message-id`, `--to` or `--channel-id`
|
- Required: `--message-id`, `--target`
|
||||||
- Optional: `--emoji`, `--remove`, `--participant`, `--from-me`, `--channel-id`
|
- Optional: `--emoji`, `--remove`, `--participant`, `--from-me`
|
||||||
- Note: `--remove` requires `--emoji` (omit `--emoji` to clear own reactions where supported; see /tools/reactions)
|
- Note: `--remove` requires `--emoji` (omit `--emoji` to clear own reactions where supported; see /tools/reactions)
|
||||||
- WhatsApp only: `--participant`, `--from-me`
|
- WhatsApp only: `--participant`, `--from-me`
|
||||||
|
|
||||||
- `reactions`
|
- `reactions`
|
||||||
- Channels: Discord/Slack
|
- Channels: Discord/Slack
|
||||||
- Required: `--message-id`, `--to` or `--channel-id`
|
- Required: `--message-id`, `--target`
|
||||||
- Optional: `--limit`, `--channel-id`
|
- Optional: `--limit`
|
||||||
|
|
||||||
- `read`
|
- `read`
|
||||||
- Channels: Discord/Slack
|
- Channels: Discord/Slack
|
||||||
- Required: `--to` or `--channel-id`
|
- Required: `--target`
|
||||||
- Optional: `--limit`, `--before`, `--after`, `--channel-id`
|
- Optional: `--limit`, `--before`, `--after`
|
||||||
- Discord only: `--around`
|
- Discord only: `--around`
|
||||||
|
|
||||||
- `edit`
|
- `edit`
|
||||||
- Channels: Discord/Slack
|
- Channels: Discord/Slack
|
||||||
- Required: `--message-id`, `--message`, `--to` or `--channel-id`
|
- Required: `--message-id`, `--message`, `--target`
|
||||||
- Optional: `--channel-id`
|
|
||||||
|
|
||||||
- `delete`
|
- `delete`
|
||||||
- Channels: Discord/Slack/Telegram
|
- Channels: Discord/Slack/Telegram
|
||||||
- Required: `--message-id`, `--to` or `--channel-id`
|
- Required: `--message-id`, `--target`
|
||||||
- Optional: `--channel-id`
|
|
||||||
|
|
||||||
- `pin` / `unpin`
|
- `pin` / `unpin`
|
||||||
- Channels: Discord/Slack
|
- Channels: Discord/Slack
|
||||||
- Required: `--message-id`, `--to` or `--channel-id`
|
- Required: `--message-id`, `--target`
|
||||||
- Optional: `--channel-id`
|
|
||||||
|
|
||||||
- `pins` (list)
|
- `pins` (list)
|
||||||
- Channels: Discord/Slack
|
- Channels: Discord/Slack
|
||||||
- Required: `--to` or `--channel-id`
|
- Required: `--target`
|
||||||
- Optional: `--channel-id`
|
|
||||||
|
|
||||||
- `permissions`
|
- `permissions`
|
||||||
- Channels: Discord
|
- Channels: Discord
|
||||||
- Required: `--to` or `--channel-id`
|
- Required: `--target`
|
||||||
- Optional: `--channel-id`
|
|
||||||
|
|
||||||
- `search`
|
- `search`
|
||||||
- Channels: Discord
|
- Channels: Discord
|
||||||
@ -114,7 +110,7 @@ Name lookup:
|
|||||||
|
|
||||||
- `thread create`
|
- `thread create`
|
||||||
- Channels: Discord
|
- Channels: Discord
|
||||||
- Required: `--thread-name`, `--to` (channel id) or `--channel-id`
|
- Required: `--thread-name`, `--target` (channel id)
|
||||||
- Optional: `--message-id`, `--auto-archive-min`
|
- Optional: `--message-id`, `--auto-archive-min`
|
||||||
|
|
||||||
- `thread list`
|
- `thread list`
|
||||||
@ -124,7 +120,7 @@ Name lookup:
|
|||||||
|
|
||||||
- `thread reply`
|
- `thread reply`
|
||||||
- Channels: Discord
|
- Channels: Discord
|
||||||
- Required: `--to` (thread id), `--message`
|
- Required: `--target` (thread id), `--message`
|
||||||
- Optional: `--media`, `--reply-to`
|
- Optional: `--media`, `--reply-to`
|
||||||
|
|
||||||
### Emojis
|
### Emojis
|
||||||
@ -142,7 +138,7 @@ Name lookup:
|
|||||||
|
|
||||||
- `sticker send`
|
- `sticker send`
|
||||||
- Channels: Discord
|
- Channels: Discord
|
||||||
- Required: `--to`, `--sticker-id` (repeat)
|
- Required: `--target`, `--sticker-id` (repeat)
|
||||||
- Optional: `--message`
|
- Optional: `--message`
|
||||||
|
|
||||||
- `sticker upload`
|
- `sticker upload`
|
||||||
@ -153,7 +149,7 @@ Name lookup:
|
|||||||
|
|
||||||
- `role info` (Discord): `--guild-id`
|
- `role info` (Discord): `--guild-id`
|
||||||
- `role add` / `role remove` (Discord): `--guild-id`, `--user-id`, `--role-id`
|
- `role add` / `role remove` (Discord): `--guild-id`, `--user-id`, `--role-id`
|
||||||
- `channel info` (Discord): `--channel-id`
|
- `channel info` (Discord): `--target`
|
||||||
- `channel list` (Discord): `--guild-id`
|
- `channel list` (Discord): `--guild-id`
|
||||||
- `member info` (Discord/Slack): `--user-id` (+ `--guild-id` for Discord)
|
- `member info` (Discord/Slack): `--user-id` (+ `--guild-id` for Discord)
|
||||||
- `voice status` (Discord): `--guild-id`, `--user-id`
|
- `voice status` (Discord): `--guild-id`, `--user-id`
|
||||||
@ -183,13 +179,13 @@ Name lookup:
|
|||||||
Send a Discord reply:
|
Send a Discord reply:
|
||||||
```
|
```
|
||||||
clawdbot message send --channel discord \
|
clawdbot message send --channel discord \
|
||||||
--to channel:123 --message "hi" --reply-to 456
|
--target channel:123 --message "hi" --reply-to 456
|
||||||
```
|
```
|
||||||
|
|
||||||
Create a Discord poll:
|
Create a Discord poll:
|
||||||
```
|
```
|
||||||
clawdbot message poll --channel discord \
|
clawdbot message poll --channel discord \
|
||||||
--to channel:123 \
|
--target channel:123 \
|
||||||
--poll-question "Snack?" \
|
--poll-question "Snack?" \
|
||||||
--poll-option Pizza --poll-option Sushi \
|
--poll-option Pizza --poll-option Sushi \
|
||||||
--poll-multi --poll-duration-hours 48
|
--poll-multi --poll-duration-hours 48
|
||||||
@ -198,13 +194,13 @@ clawdbot message poll --channel discord \
|
|||||||
Send a Teams proactive message:
|
Send a Teams proactive message:
|
||||||
```
|
```
|
||||||
clawdbot message send --channel msteams \
|
clawdbot message send --channel msteams \
|
||||||
--to conversation:19:abc@thread.tacv2 --message "hi"
|
--target conversation:19:abc@thread.tacv2 --message "hi"
|
||||||
```
|
```
|
||||||
|
|
||||||
Create a Teams poll:
|
Create a Teams poll:
|
||||||
```
|
```
|
||||||
clawdbot message poll --channel msteams \
|
clawdbot message poll --channel msteams \
|
||||||
--to conversation:19:abc@thread.tacv2 \
|
--target conversation:19:abc@thread.tacv2 \
|
||||||
--poll-question "Lunch?" \
|
--poll-question "Lunch?" \
|
||||||
--poll-option Pizza --poll-option Sushi
|
--poll-option Pizza --poll-option Sushi
|
||||||
```
|
```
|
||||||
@ -212,11 +208,11 @@ clawdbot message poll --channel msteams \
|
|||||||
React in Slack:
|
React in Slack:
|
||||||
```
|
```
|
||||||
clawdbot message react --channel slack \
|
clawdbot message react --channel slack \
|
||||||
--to C123 --message-id 456 --emoji "✅"
|
--target C123 --message-id 456 --emoji "✅"
|
||||||
```
|
```
|
||||||
|
|
||||||
Send Telegram inline buttons:
|
Send Telegram inline buttons:
|
||||||
```
|
```
|
||||||
clawdbot message send --channel telegram --to @mychat --message "Choose:" \
|
clawdbot message send --channel telegram --target @mychat --message "Choose:" \
|
||||||
--buttons '[ [{"text":"Yes","callback_data":"cmd:yes"}], [{"text":"No","callback_data":"cmd:no"}] ]'
|
--buttons '[ [{"text":"Yes","callback_data":"cmd:yes"}], [{"text":"No","callback_data":"cmd:no"}] ]'
|
||||||
```
|
```
|
||||||
|
|||||||
@ -281,7 +281,7 @@ Windows installs should use **WSL2** and follow the Linux systemd section above.
|
|||||||
|
|
||||||
## CLI helpers
|
## CLI helpers
|
||||||
- `clawdbot gateway health|status` — request health/status over the Gateway WS.
|
- `clawdbot gateway health|status` — request health/status over the Gateway WS.
|
||||||
- `clawdbot message send --to <num> --message "hi" [--media ...]` — send via Gateway (idempotent for WhatsApp).
|
- `clawdbot message send --target <num> --message "hi" [--media ...]` — send via Gateway (idempotent for WhatsApp).
|
||||||
- `clawdbot agent --message "hi" --to <num>` — run an agent turn (waits for final by default).
|
- `clawdbot agent --message "hi" --to <num>` — run an agent turn (waits for final by default).
|
||||||
- `clawdbot gateway call <method> --params '{"k":"v"}'` — raw method invoker for debugging.
|
- `clawdbot gateway call <method> --params '{"k":"v"}'` — raw method invoker for debugging.
|
||||||
- `clawdbot daemon stop|restart` — stop/restart the supervised gateway service (launchd/systemd).
|
- `clawdbot daemon stop|restart` — stop/restart the supervised gateway service (launchd/systemd).
|
||||||
|
|||||||
@ -137,7 +137,7 @@ clawdbot gateway --port 19001
|
|||||||
Send a test message (requires a running Gateway):
|
Send a test message (requires a running Gateway):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
clawdbot message send --to +15555550123 --message "Hello from Clawdbot"
|
clawdbot message send --target +15555550123 --message "Hello from Clawdbot"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration (optional)
|
## Configuration (optional)
|
||||||
|
|||||||
@ -65,7 +65,7 @@ Channel config lives under `channels.zalouser` (not `plugins.entries.*`):
|
|||||||
clawdbot channels login --channel zalouser
|
clawdbot channels login --channel zalouser
|
||||||
clawdbot channels logout --channel zalouser
|
clawdbot channels logout --channel zalouser
|
||||||
clawdbot channels status --probe
|
clawdbot channels status --probe
|
||||||
clawdbot message send --channel zalouser --to <threadId> --message "Hello from Clawdbot"
|
clawdbot message send --channel zalouser --target <threadId> --message "Hello from Clawdbot"
|
||||||
clawdbot directory peers list --channel zalouser --query "name"
|
clawdbot directory peers list --channel zalouser --query "name"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@ -1495,7 +1495,7 @@ Outbound attachments from the agent must include a `MEDIA:<path-or-url>` line (o
|
|||||||
CLI sending:
|
CLI sending:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
clawdbot message send --to +15555550123 --message "Here you go" --media /path/to/file.png
|
clawdbot message send --target +15555550123 --message "Here you go" --media /path/to/file.png
|
||||||
```
|
```
|
||||||
|
|
||||||
Also check:
|
Also check:
|
||||||
|
|||||||
@ -174,7 +174,7 @@ In a new terminal:
|
|||||||
```bash
|
```bash
|
||||||
clawdbot status
|
clawdbot status
|
||||||
clawdbot health
|
clawdbot health
|
||||||
clawdbot message send --to +15555550123 --message "Hello from Clawdbot"
|
clawdbot message send --target +15555550123 --message "Hello from Clawdbot"
|
||||||
```
|
```
|
||||||
|
|
||||||
If `health` shows “no auth configured”, go back to the wizard and set OAuth/key auth — the agent won’t be able to respond without it.
|
If `health` shows “no auth configured”, go back to the wizard and set OAuth/key auth — the agent won’t be able to respond without it.
|
||||||
|
|||||||
@ -20,7 +20,7 @@ runtime on the current machine.
|
|||||||
- Output:
|
- Output:
|
||||||
- default: prints reply text (plus `MEDIA:<url>` lines)
|
- default: prints reply text (plus `MEDIA:<url>` lines)
|
||||||
- `--json`: prints structured payload + metadata
|
- `--json`: prints structured payload + metadata
|
||||||
- Optional delivery back to a channel with `--deliver` + `--channel` (target formats match `clawdbot message --to`).
|
- Optional delivery back to a channel with `--deliver` + `--channel` (target formats match `clawdbot message --target`).
|
||||||
|
|
||||||
If the Gateway is unreachable, the CLI **falls back** to the embedded local run.
|
If the Gateway is unreachable, the CLI **falls back** to the embedded local run.
|
||||||
|
|
||||||
|
|||||||
@ -47,7 +47,7 @@ describe("message tool mirroring", () => {
|
|||||||
|
|
||||||
await tool.execute("1", {
|
await tool.execute("1", {
|
||||||
action: "send",
|
action: "send",
|
||||||
to: "telegram:123",
|
target: "telegram:123",
|
||||||
message: "",
|
message: "",
|
||||||
media: "https://example.com/files/report.pdf?sig=1",
|
media: "https://example.com/files/report.pdf?sig=1",
|
||||||
});
|
});
|
||||||
@ -75,7 +75,7 @@ describe("message tool mirroring", () => {
|
|||||||
|
|
||||||
await tool.execute("1", {
|
await tool.execute("1", {
|
||||||
action: "send",
|
action: "send",
|
||||||
to: "telegram:123",
|
target: "telegram:123",
|
||||||
message: "hi",
|
message: "hi",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -26,7 +26,9 @@ const AllMessageActions = CHANNEL_MESSAGE_ACTION_NAMES;
|
|||||||
function buildRoutingSchema() {
|
function buildRoutingSchema() {
|
||||||
return {
|
return {
|
||||||
channel: Type.Optional(Type.String()),
|
channel: Type.Optional(Type.String()),
|
||||||
to: Type.Optional(channelTargetSchema()),
|
target: Type.Optional(
|
||||||
|
channelTargetSchema({ description: "Target channel/user id or name." }),
|
||||||
|
),
|
||||||
targets: Type.Optional(channelTargetsSchema()),
|
targets: Type.Optional(channelTargetsSchema()),
|
||||||
accountId: Type.Optional(Type.String()),
|
accountId: Type.Optional(Type.String()),
|
||||||
dryRun: Type.Optional(Type.Boolean()),
|
dryRun: Type.Optional(Type.Boolean()),
|
||||||
@ -89,8 +91,12 @@ function buildPollSchema() {
|
|||||||
|
|
||||||
function buildChannelTargetSchema() {
|
function buildChannelTargetSchema() {
|
||||||
return {
|
return {
|
||||||
channelId: Type.Optional(channelTargetSchema()),
|
channelId: Type.Optional(
|
||||||
channelIds: Type.Optional(channelTargetsSchema()),
|
Type.String({ description: "Channel id filter (search/thread list/event create)." }),
|
||||||
|
),
|
||||||
|
channelIds: Type.Optional(
|
||||||
|
Type.Array(Type.String({ description: "Channel id filter (repeatable)." })),
|
||||||
|
),
|
||||||
guildId: Type.Optional(Type.String()),
|
guildId: Type.Optional(Type.String()),
|
||||||
userId: Type.Optional(Type.String()),
|
userId: Type.Optional(Type.String()),
|
||||||
authorId: Type.Optional(Type.String()),
|
authorId: Type.Optional(Type.String()),
|
||||||
@ -182,6 +188,7 @@ function buildMessageToolSchemaProps(options: { includeButtons: boolean }) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function buildMessageToolSchemaFromActions(
|
function buildMessageToolSchemaFromActions(
|
||||||
actions: readonly string[],
|
actions: readonly string[],
|
||||||
options: { includeButtons: boolean },
|
options: { includeButtons: boolean },
|
||||||
|
|||||||
@ -56,7 +56,7 @@ describe("cli program (smoke)", () => {
|
|||||||
|
|
||||||
it("runs message with required options", async () => {
|
it("runs message with required options", async () => {
|
||||||
const program = buildProgram();
|
const program = buildProgram();
|
||||||
await program.parseAsync(["message", "send", "--to", "+1", "--message", "hi"], {
|
await program.parseAsync(["message", "send", "--target", "+1", "--message", "hi"], {
|
||||||
from: "user",
|
from: "user",
|
||||||
});
|
});
|
||||||
expect(messageCommand).toHaveBeenCalled();
|
expect(messageCommand).toHaveBeenCalled();
|
||||||
|
|||||||
@ -10,7 +10,7 @@ const EXAMPLES = [
|
|||||||
"Link personal WhatsApp Web and show QR + connection logs.",
|
"Link personal WhatsApp Web and show QR + connection logs.",
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'clawdbot message send --to +15555550123 --message "Hi" --json',
|
'clawdbot message send --target +15555550123 --message "Hi" --json',
|
||||||
"Send via your web session and print JSON result.",
|
"Send via your web session and print JSON result.",
|
||||||
],
|
],
|
||||||
["clawdbot gateway --port 18789", "Run the WebSocket Gateway locally."],
|
["clawdbot gateway --port 18789", "Run the WebSocket Gateway locally."],
|
||||||
@ -22,7 +22,7 @@ const EXAMPLES = [
|
|||||||
"Talk directly to the agent using the Gateway; optionally send the WhatsApp reply.",
|
"Talk directly to the agent using the Gateway; optionally send the WhatsApp reply.",
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'clawdbot message send --channel telegram --to @mychat --message "Hi"',
|
'clawdbot message send --channel telegram --target @mychat --message "Hi"',
|
||||||
"Send via your Telegram bot.",
|
"Send via your Telegram bot.",
|
||||||
],
|
],
|
||||||
] as const;
|
] as const;
|
||||||
|
|||||||
@ -25,9 +25,9 @@ export function createMessageCliHelpers(
|
|||||||
.option("--verbose", "Verbose logging", false);
|
.option("--verbose", "Verbose logging", false);
|
||||||
|
|
||||||
const withMessageTarget = (command: Command) =>
|
const withMessageTarget = (command: Command) =>
|
||||||
command.option("-t, --to <dest>", CHANNEL_TARGET_DESCRIPTION);
|
command.option("-t, --target <dest>", CHANNEL_TARGET_DESCRIPTION);
|
||||||
const withRequiredMessageTarget = (command: Command) =>
|
const withRequiredMessageTarget = (command: Command) =>
|
||||||
command.requiredOption("-t, --to <dest>", CHANNEL_TARGET_DESCRIPTION);
|
command.requiredOption("-t, --target <dest>", CHANNEL_TARGET_DESCRIPTION);
|
||||||
|
|
||||||
const runMessageAction = async (action: string, opts: Record<string, unknown>) => {
|
const runMessageAction = async (action: string, opts: Record<string, unknown>) => {
|
||||||
setVerbose(Boolean(opts.verbose));
|
setVerbose(Boolean(opts.verbose));
|
||||||
|
|||||||
@ -40,10 +40,9 @@ export function registerMessageDiscordAdminCommands(message: Command, helpers: M
|
|||||||
const channel = message.command("channel").description("Channel actions");
|
const channel = message.command("channel").description("Channel actions");
|
||||||
helpers
|
helpers
|
||||||
.withMessageBase(
|
.withMessageBase(
|
||||||
channel
|
helpers.withRequiredMessageTarget(
|
||||||
.command("info")
|
channel.command("info").description("Fetch channel info"),
|
||||||
.description("Fetch channel info")
|
),
|
||||||
.requiredOption("--channel-id <id>", "Channel id"),
|
|
||||||
)
|
)
|
||||||
.action(async (opts) => {
|
.action(async (opts) => {
|
||||||
await helpers.runMessageAction("channel-info", opts);
|
await helpers.runMessageAction("channel-info", opts);
|
||||||
|
|||||||
@ -5,11 +5,10 @@ import type { MessageCliHelpers } from "./helpers.js";
|
|||||||
export function registerMessagePermissionsCommand(message: Command, helpers: MessageCliHelpers) {
|
export function registerMessagePermissionsCommand(message: Command, helpers: MessageCliHelpers) {
|
||||||
helpers
|
helpers
|
||||||
.withMessageBase(
|
.withMessageBase(
|
||||||
helpers.withMessageTarget(
|
helpers.withRequiredMessageTarget(
|
||||||
message.command("permissions").description("Fetch channel permissions"),
|
message.command("permissions").description("Fetch channel permissions"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.option("--channel-id <id>", "Channel id (defaults to --to)")
|
|
||||||
.action(async (opts) => {
|
.action(async (opts) => {
|
||||||
await helpers.runMessageAction("permissions", opts);
|
await helpers.runMessageAction("permissions", opts);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -2,15 +2,10 @@ import type { Command } from "commander";
|
|||||||
import type { MessageCliHelpers } from "./helpers.js";
|
import type { MessageCliHelpers } from "./helpers.js";
|
||||||
|
|
||||||
export function registerMessagePinCommands(message: Command, helpers: MessageCliHelpers) {
|
export function registerMessagePinCommands(message: Command, helpers: MessageCliHelpers) {
|
||||||
const withPinsTarget = (command: Command) =>
|
|
||||||
command.option("--channel-id <id>", "Channel id (defaults to --to; required for WhatsApp)");
|
|
||||||
|
|
||||||
const pins = [
|
const pins = [
|
||||||
helpers
|
helpers
|
||||||
.withMessageBase(
|
.withMessageBase(
|
||||||
withPinsTarget(
|
helpers.withRequiredMessageTarget(message.command("pin").description("Pin a message")),
|
||||||
helpers.withMessageTarget(message.command("pin").description("Pin a message")),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.requiredOption("--message-id <id>", "Message id")
|
.requiredOption("--message-id <id>", "Message id")
|
||||||
.action(async (opts) => {
|
.action(async (opts) => {
|
||||||
@ -18,9 +13,7 @@ export function registerMessagePinCommands(message: Command, helpers: MessageCli
|
|||||||
}),
|
}),
|
||||||
helpers
|
helpers
|
||||||
.withMessageBase(
|
.withMessageBase(
|
||||||
withPinsTarget(
|
helpers.withRequiredMessageTarget(message.command("unpin").description("Unpin a message")),
|
||||||
helpers.withMessageTarget(message.command("unpin").description("Unpin a message")),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.requiredOption("--message-id <id>", "Message id")
|
.requiredOption("--message-id <id>", "Message id")
|
||||||
.action(async (opts) => {
|
.action(async (opts) => {
|
||||||
@ -28,9 +21,8 @@ export function registerMessagePinCommands(message: Command, helpers: MessageCli
|
|||||||
}),
|
}),
|
||||||
helpers
|
helpers
|
||||||
.withMessageBase(
|
.withMessageBase(
|
||||||
helpers.withMessageTarget(message.command("pins").description("List pinned messages")),
|
helpers.withRequiredMessageTarget(message.command("pins").description("List pinned messages")),
|
||||||
)
|
)
|
||||||
.option("--channel-id <id>", "Channel id (defaults to --to)")
|
|
||||||
.option("--limit <n>", "Result limit")
|
.option("--limit <n>", "Result limit")
|
||||||
.action(async (opts) => {
|
.action(async (opts) => {
|
||||||
await helpers.runMessageAction("list-pins", opts);
|
await helpers.runMessageAction("list-pins", opts);
|
||||||
|
|||||||
@ -4,27 +4,27 @@ import type { MessageCliHelpers } from "./helpers.js";
|
|||||||
export function registerMessageReactionsCommands(message: Command, helpers: MessageCliHelpers) {
|
export function registerMessageReactionsCommands(message: Command, helpers: MessageCliHelpers) {
|
||||||
helpers
|
helpers
|
||||||
.withMessageBase(
|
.withMessageBase(
|
||||||
helpers.withMessageTarget(message.command("react").description("Add or remove a reaction")),
|
helpers.withRequiredMessageTarget(
|
||||||
|
message.command("react").description("Add or remove a reaction"),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.requiredOption("--message-id <id>", "Message id")
|
.requiredOption("--message-id <id>", "Message id")
|
||||||
.option("--emoji <emoji>", "Emoji for reactions")
|
.option("--emoji <emoji>", "Emoji for reactions")
|
||||||
.option("--remove", "Remove reaction", false)
|
.option("--remove", "Remove reaction", false)
|
||||||
.option("--participant <id>", "WhatsApp reaction participant")
|
.option("--participant <id>", "WhatsApp reaction participant")
|
||||||
.option("--from-me", "WhatsApp reaction fromMe", false)
|
.option("--from-me", "WhatsApp reaction fromMe", false)
|
||||||
.option("--channel-id <id>", "Channel id (defaults to --to)")
|
|
||||||
.action(async (opts) => {
|
.action(async (opts) => {
|
||||||
await helpers.runMessageAction("react", opts);
|
await helpers.runMessageAction("react", opts);
|
||||||
});
|
});
|
||||||
|
|
||||||
helpers
|
helpers
|
||||||
.withMessageBase(
|
.withMessageBase(
|
||||||
helpers.withMessageTarget(
|
helpers.withRequiredMessageTarget(
|
||||||
message.command("reactions").description("List reactions on a message"),
|
message.command("reactions").description("List reactions on a message"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.requiredOption("--message-id <id>", "Message id")
|
.requiredOption("--message-id <id>", "Message id")
|
||||||
.option("--limit <n>", "Result limit")
|
.option("--limit <n>", "Result limit")
|
||||||
.option("--channel-id <id>", "Channel id (defaults to --to)")
|
|
||||||
.action(async (opts) => {
|
.action(async (opts) => {
|
||||||
await helpers.runMessageAction("reactions", opts);
|
await helpers.runMessageAction("reactions", opts);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -7,13 +7,12 @@ export function registerMessageReadEditDeleteCommands(
|
|||||||
) {
|
) {
|
||||||
helpers
|
helpers
|
||||||
.withMessageBase(
|
.withMessageBase(
|
||||||
helpers.withMessageTarget(message.command("read").description("Read recent messages")),
|
helpers.withRequiredMessageTarget(message.command("read").description("Read recent messages")),
|
||||||
)
|
)
|
||||||
.option("--limit <n>", "Result limit")
|
.option("--limit <n>", "Result limit")
|
||||||
.option("--before <id>", "Read/search before id")
|
.option("--before <id>", "Read/search before id")
|
||||||
.option("--after <id>", "Read/search after id")
|
.option("--after <id>", "Read/search after id")
|
||||||
.option("--around <id>", "Read around id")
|
.option("--around <id>", "Read around id")
|
||||||
.option("--channel-id <id>", "Channel id (defaults to --to)")
|
|
||||||
.option("--include-thread", "Include thread replies (Discord)", false)
|
.option("--include-thread", "Include thread replies (Discord)", false)
|
||||||
.action(async (opts) => {
|
.action(async (opts) => {
|
||||||
await helpers.runMessageAction("read", opts);
|
await helpers.runMessageAction("read", opts);
|
||||||
@ -21,7 +20,7 @@ export function registerMessageReadEditDeleteCommands(
|
|||||||
|
|
||||||
helpers
|
helpers
|
||||||
.withMessageBase(
|
.withMessageBase(
|
||||||
helpers.withMessageTarget(
|
helpers.withRequiredMessageTarget(
|
||||||
message
|
message
|
||||||
.command("edit")
|
.command("edit")
|
||||||
.description("Edit a message")
|
.description("Edit a message")
|
||||||
@ -29,7 +28,6 @@ export function registerMessageReadEditDeleteCommands(
|
|||||||
.requiredOption("-m, --message <text>", "Message body"),
|
.requiredOption("-m, --message <text>", "Message body"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.option("--channel-id <id>", "Channel id (defaults to --to)")
|
|
||||||
.option("--thread-id <id>", "Thread id (Telegram forum thread)")
|
.option("--thread-id <id>", "Thread id (Telegram forum thread)")
|
||||||
.action(async (opts) => {
|
.action(async (opts) => {
|
||||||
await helpers.runMessageAction("edit", opts);
|
await helpers.runMessageAction("edit", opts);
|
||||||
@ -37,14 +35,13 @@ export function registerMessageReadEditDeleteCommands(
|
|||||||
|
|
||||||
helpers
|
helpers
|
||||||
.withMessageBase(
|
.withMessageBase(
|
||||||
helpers.withMessageTarget(
|
helpers.withRequiredMessageTarget(
|
||||||
message
|
message
|
||||||
.command("delete")
|
.command("delete")
|
||||||
.description("Delete a message")
|
.description("Delete a message")
|
||||||
.requiredOption("--message-id <id>", "Message id"),
|
.requiredOption("--message-id <id>", "Message id"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.option("--channel-id <id>", "Channel id (defaults to --to)")
|
|
||||||
.action(async (opts) => {
|
.action(async (opts) => {
|
||||||
await helpers.runMessageAction("delete", opts);
|
await helpers.runMessageAction("delete", opts);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -6,14 +6,13 @@ export function registerMessageThreadCommands(message: Command, helpers: Message
|
|||||||
|
|
||||||
helpers
|
helpers
|
||||||
.withMessageBase(
|
.withMessageBase(
|
||||||
helpers.withMessageTarget(
|
helpers.withRequiredMessageTarget(
|
||||||
thread
|
thread
|
||||||
.command("create")
|
.command("create")
|
||||||
.description("Create a thread")
|
.description("Create a thread")
|
||||||
.requiredOption("--thread-name <name>", "Thread name"),
|
.requiredOption("--thread-name <name>", "Thread name"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.option("--channel-id <id>", "Channel id (defaults to --to)")
|
|
||||||
.option("--message-id <id>", "Message id (optional)")
|
.option("--message-id <id>", "Message id (optional)")
|
||||||
.option("--auto-archive-min <n>", "Thread auto-archive minutes")
|
.option("--auto-archive-min <n>", "Thread auto-archive minutes")
|
||||||
.action(async (opts) => {
|
.action(async (opts) => {
|
||||||
|
|||||||
@ -29,10 +29,10 @@ export function registerMessageCommands(program: Command, ctx: ProgramContext) {
|
|||||||
() =>
|
() =>
|
||||||
`
|
`
|
||||||
Examples:
|
Examples:
|
||||||
clawdbot message send --to +15555550123 --message "Hi"
|
clawdbot message send --target +15555550123 --message "Hi"
|
||||||
clawdbot message send --to +15555550123 --message "Hi" --media photo.jpg
|
clawdbot message send --target +15555550123 --message "Hi" --media photo.jpg
|
||||||
clawdbot message poll --channel discord --to channel:123 --poll-question "Snack?" --poll-option Pizza --poll-option Sushi
|
clawdbot message poll --channel discord --target channel:123 --poll-question "Snack?" --poll-option Pizza --poll-option Sushi
|
||||||
clawdbot message react --channel discord --to 123 --message-id 456 --emoji "✅"
|
clawdbot message react --channel discord --target 123 --message-id 456 --emoji "✅"
|
||||||
|
|
||||||
${theme.muted("Docs:")} ${formatDocsLink("/cli/message", "docs.clawd.bot/cli/message")}`,
|
${theme.muted("Docs:")} ${formatDocsLink("/cli/message", "docs.clawd.bot/cli/message")}`,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -88,7 +88,7 @@ describe("messageCommand", () => {
|
|||||||
const deps = makeDeps();
|
const deps = makeDeps();
|
||||||
await messageCommand(
|
await messageCommand(
|
||||||
{
|
{
|
||||||
to: "123",
|
target: "123",
|
||||||
message: "hi",
|
message: "hi",
|
||||||
},
|
},
|
||||||
deps,
|
deps,
|
||||||
@ -104,7 +104,7 @@ describe("messageCommand", () => {
|
|||||||
await expect(
|
await expect(
|
||||||
messageCommand(
|
messageCommand(
|
||||||
{
|
{
|
||||||
to: "123",
|
target: "123",
|
||||||
message: "hi",
|
message: "hi",
|
||||||
},
|
},
|
||||||
deps,
|
deps,
|
||||||
@ -120,7 +120,7 @@ describe("messageCommand", () => {
|
|||||||
{
|
{
|
||||||
action: "send",
|
action: "send",
|
||||||
channel: "whatsapp",
|
channel: "whatsapp",
|
||||||
to: "+15551234567",
|
target: "+15551234567",
|
||||||
message: "hi",
|
message: "hi",
|
||||||
},
|
},
|
||||||
deps,
|
deps,
|
||||||
@ -135,7 +135,7 @@ describe("messageCommand", () => {
|
|||||||
{
|
{
|
||||||
action: "poll",
|
action: "poll",
|
||||||
channel: "discord",
|
channel: "discord",
|
||||||
to: "channel:123456789",
|
target: "channel:123456789",
|
||||||
pollQuestion: "Snack?",
|
pollQuestion: "Snack?",
|
||||||
pollOption: ["Pizza", "Sushi"],
|
pollOption: ["Pizza", "Sushi"],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { type ChannelId, getChannelPlugin, listChannelPlugins } from "../channel
|
|||||||
import type { ChannelAccountSnapshot } from "../channels/plugins/types.js";
|
import type { ChannelAccountSnapshot } from "../channels/plugins/types.js";
|
||||||
import type { ClawdbotConfig } from "../config/config.js";
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
import { formatErrorMessage } from "../infra/errors.js";
|
import { formatErrorMessage } from "../infra/errors.js";
|
||||||
|
import { resetDirectoryCache } from "../infra/outbound/target-resolver.js";
|
||||||
import type { createSubsystemLogger } from "../logging.js";
|
import type { createSubsystemLogger } from "../logging.js";
|
||||||
import { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js";
|
import { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js";
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
@ -93,6 +94,7 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage
|
|||||||
const startAccount = plugin?.gateway?.startAccount;
|
const startAccount = plugin?.gateway?.startAccount;
|
||||||
if (!startAccount) return;
|
if (!startAccount) return;
|
||||||
const cfg = loadConfig();
|
const cfg = loadConfig();
|
||||||
|
resetDirectoryCache({ channel: channelId, accountId });
|
||||||
const store = getStore(channelId);
|
const store = getStore(channelId);
|
||||||
const accountIds = accountId ? [accountId] : plugin.config.listAccountIds(cfg);
|
const accountIds = accountId ? [accountId] : plugin.config.listAccountIds(cfg);
|
||||||
if (accountIds.length === 0) return;
|
if (accountIds.length === 0) return;
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import type { CliDeps } from "../cli/deps.js";
|
|||||||
import type { loadConfig } from "../config/config.js";
|
import type { loadConfig } from "../config/config.js";
|
||||||
import { startGmailWatcher, stopGmailWatcher } from "../hooks/gmail-watcher.js";
|
import { startGmailWatcher, stopGmailWatcher } from "../hooks/gmail-watcher.js";
|
||||||
import { startHeartbeatRunner } from "../infra/heartbeat-runner.js";
|
import { startHeartbeatRunner } from "../infra/heartbeat-runner.js";
|
||||||
|
import { resetDirectoryCache } from "../infra/outbound/target-resolver.js";
|
||||||
import { setCommandLaneConcurrency } from "../process/command-queue.js";
|
import { setCommandLaneConcurrency } from "../process/command-queue.js";
|
||||||
import type { ChannelKind, GatewayReloadPlan } from "./config-reload.js";
|
import type { ChannelKind, GatewayReloadPlan } from "./config-reload.js";
|
||||||
import { resolveHooksConfig } from "./hooks.js";
|
import { resolveHooksConfig } from "./hooks.js";
|
||||||
@ -52,6 +53,8 @@ export function createGatewayReloadHandlers(params: {
|
|||||||
nextState.heartbeatRunner = startHeartbeatRunner({ cfg: nextConfig });
|
nextState.heartbeatRunner = startHeartbeatRunner({ cfg: nextConfig });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resetDirectoryCache();
|
||||||
|
|
||||||
if (plan.restartCron) {
|
if (plan.restartCron) {
|
||||||
state.cronState.cron.stop();
|
state.cronState.cron.stop();
|
||||||
nextState.cronState = buildGatewayCronService({
|
nextState.cronState = buildGatewayCronService({
|
||||||
|
|||||||
@ -1,9 +1,41 @@
|
|||||||
|
import { MESSAGE_ACTION_TARGET_MODE } from "./message-action-spec.js";
|
||||||
|
|
||||||
export const CHANNEL_TARGET_DESCRIPTION =
|
export const CHANNEL_TARGET_DESCRIPTION =
|
||||||
"Recipient/channel: E.164 for WhatsApp/Signal, Telegram chat id/@username, Discord/Slack channel/user, or iMessage handle/chat_id";
|
"Recipient/channel: E.164 for WhatsApp/Signal, Telegram chat id/@username, Discord/Slack channel/user, or iMessage handle/chat_id";
|
||||||
|
|
||||||
export const CHANNEL_TARGETS_DESCRIPTION =
|
export const CHANNEL_TARGETS_DESCRIPTION =
|
||||||
"Recipient/channel targets (same format as --to); accepts ids or names when the directory is available.";
|
"Recipient/channel targets (same format as --target); accepts ids or names when the directory is available.";
|
||||||
|
|
||||||
export function normalizeChannelTargetInput(raw: string): string {
|
export function normalizeChannelTargetInput(raw: string): string {
|
||||||
return raw.trim();
|
return raw.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function applyTargetToParams(params: {
|
||||||
|
action: string;
|
||||||
|
args: Record<string, unknown>;
|
||||||
|
}): void {
|
||||||
|
const target = typeof params.args.target === "string" ? params.args.target.trim() : "";
|
||||||
|
const hasLegacyTo = typeof params.args.to === "string";
|
||||||
|
const hasLegacyChannelId = typeof params.args.channelId === "string";
|
||||||
|
const mode =
|
||||||
|
MESSAGE_ACTION_TARGET_MODE[params.action as keyof typeof MESSAGE_ACTION_TARGET_MODE] ?? "none";
|
||||||
|
|
||||||
|
if (mode !== "none") {
|
||||||
|
if (hasLegacyTo || hasLegacyChannelId) {
|
||||||
|
throw new Error("Use `target` instead of `to`/`channelId`.");
|
||||||
|
}
|
||||||
|
} else if (hasLegacyTo) {
|
||||||
|
throw new Error("Use `target` for actions that accept a destination.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!target) return;
|
||||||
|
if (mode === "channelId") {
|
||||||
|
params.args.channelId = target;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mode === "to") {
|
||||||
|
params.args.to = target;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new Error(`Action ${params.action} does not accept a target.`);
|
||||||
|
}
|
||||||
|
|||||||
@ -39,6 +39,12 @@ export class DirectoryCache<T> {
|
|||||||
this.cache.set(key, { value, fetchedAt: Date.now() });
|
this.cache.set(key, { value, fetchedAt: Date.now() });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearMatching(match: (key: string) => boolean): void {
|
||||||
|
for (const key of this.cache.keys()) {
|
||||||
|
if (match(key)) this.cache.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
clear(cfg?: ClawdbotConfig): void {
|
clear(cfg?: ClawdbotConfig): void {
|
||||||
this.cache.clear();
|
this.cache.clear();
|
||||||
if (cfg) this.lastConfigRef = cfg;
|
if (cfg) this.lastConfigRef = cfg;
|
||||||
|
|||||||
@ -27,7 +27,7 @@ describe("runMessageAction context isolation", () => {
|
|||||||
action: "send",
|
action: "send",
|
||||||
params: {
|
params: {
|
||||||
channel: "slack",
|
channel: "slack",
|
||||||
to: "#C12345678",
|
target: "#C12345678",
|
||||||
message: "hi",
|
message: "hi",
|
||||||
},
|
},
|
||||||
toolContext: { currentChannelId: "C12345678" },
|
toolContext: { currentChannelId: "C12345678" },
|
||||||
@ -43,7 +43,7 @@ describe("runMessageAction context isolation", () => {
|
|||||||
action: "send",
|
action: "send",
|
||||||
params: {
|
params: {
|
||||||
channel: "slack",
|
channel: "slack",
|
||||||
to: "#C12345678",
|
target: "#C12345678",
|
||||||
media: "https://example.com/note.ogg",
|
media: "https://example.com/note.ogg",
|
||||||
},
|
},
|
||||||
toolContext: { currentChannelId: "C12345678" },
|
toolContext: { currentChannelId: "C12345678" },
|
||||||
@ -60,7 +60,7 @@ describe("runMessageAction context isolation", () => {
|
|||||||
action: "send",
|
action: "send",
|
||||||
params: {
|
params: {
|
||||||
channel: "slack",
|
channel: "slack",
|
||||||
to: "#C12345678",
|
target: "#C12345678",
|
||||||
},
|
},
|
||||||
toolContext: { currentChannelId: "C12345678" },
|
toolContext: { currentChannelId: "C12345678" },
|
||||||
dryRun: true,
|
dryRun: true,
|
||||||
@ -74,7 +74,7 @@ describe("runMessageAction context isolation", () => {
|
|||||||
action: "send",
|
action: "send",
|
||||||
params: {
|
params: {
|
||||||
channel: "slack",
|
channel: "slack",
|
||||||
to: "channel:C99999999",
|
target: "channel:C99999999",
|
||||||
message: "hi",
|
message: "hi",
|
||||||
},
|
},
|
||||||
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" },
|
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" },
|
||||||
@ -90,7 +90,7 @@ describe("runMessageAction context isolation", () => {
|
|||||||
action: "thread-reply",
|
action: "thread-reply",
|
||||||
params: {
|
params: {
|
||||||
channel: "slack",
|
channel: "slack",
|
||||||
channelId: "C99999999",
|
target: "C99999999",
|
||||||
message: "hi",
|
message: "hi",
|
||||||
},
|
},
|
||||||
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" },
|
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" },
|
||||||
@ -106,7 +106,7 @@ describe("runMessageAction context isolation", () => {
|
|||||||
action: "send",
|
action: "send",
|
||||||
params: {
|
params: {
|
||||||
channel: "whatsapp",
|
channel: "whatsapp",
|
||||||
to: "group:123@g.us",
|
target: "group:123@g.us",
|
||||||
message: "hi",
|
message: "hi",
|
||||||
},
|
},
|
||||||
toolContext: { currentChannelId: "123@g.us" },
|
toolContext: { currentChannelId: "123@g.us" },
|
||||||
@ -122,7 +122,7 @@ describe("runMessageAction context isolation", () => {
|
|||||||
action: "send",
|
action: "send",
|
||||||
params: {
|
params: {
|
||||||
channel: "whatsapp",
|
channel: "whatsapp",
|
||||||
to: "456@g.us",
|
target: "456@g.us",
|
||||||
message: "hi",
|
message: "hi",
|
||||||
},
|
},
|
||||||
toolContext: { currentChannelId: "123@g.us", currentChannelProvider: "whatsapp" },
|
toolContext: { currentChannelId: "123@g.us", currentChannelProvider: "whatsapp" },
|
||||||
@ -138,7 +138,7 @@ describe("runMessageAction context isolation", () => {
|
|||||||
action: "send",
|
action: "send",
|
||||||
params: {
|
params: {
|
||||||
channel: "imessage",
|
channel: "imessage",
|
||||||
to: "imessage:+15551234567",
|
target: "imessage:+15551234567",
|
||||||
message: "hi",
|
message: "hi",
|
||||||
},
|
},
|
||||||
toolContext: { currentChannelId: "imessage:+15551234567" },
|
toolContext: { currentChannelId: "imessage:+15551234567" },
|
||||||
@ -154,7 +154,7 @@ describe("runMessageAction context isolation", () => {
|
|||||||
action: "send",
|
action: "send",
|
||||||
params: {
|
params: {
|
||||||
channel: "imessage",
|
channel: "imessage",
|
||||||
to: "imessage:+15551230000",
|
target: "imessage:+15551230000",
|
||||||
message: "hi",
|
message: "hi",
|
||||||
},
|
},
|
||||||
toolContext: {
|
toolContext: {
|
||||||
@ -174,7 +174,7 @@ describe("runMessageAction context isolation", () => {
|
|||||||
action: "send",
|
action: "send",
|
||||||
params: {
|
params: {
|
||||||
channel: "telegram",
|
channel: "telegram",
|
||||||
to: "telegram:@ops",
|
target: "telegram:@ops",
|
||||||
message: "hi",
|
message: "hi",
|
||||||
},
|
},
|
||||||
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" },
|
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" },
|
||||||
@ -201,7 +201,7 @@ describe("runMessageAction context isolation", () => {
|
|||||||
action: "send",
|
action: "send",
|
||||||
params: {
|
params: {
|
||||||
channel: "slack",
|
channel: "slack",
|
||||||
to: "channel:C99999999",
|
target: "channel:C99999999",
|
||||||
message: "hi",
|
message: "hi",
|
||||||
},
|
},
|
||||||
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" },
|
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" },
|
||||||
|
|||||||
@ -13,13 +13,10 @@ import type {
|
|||||||
} from "../../channels/plugins/types.js";
|
} from "../../channels/plugins/types.js";
|
||||||
import type { ClawdbotConfig } from "../../config/config.js";
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
import type { GatewayClientMode, GatewayClientName } from "../../utils/message-channel.js";
|
import type { GatewayClientMode, GatewayClientName } from "../../utils/message-channel.js";
|
||||||
import {
|
import { listConfiguredMessageChannels, resolveMessageChannelSelection } from "./channel-selection.js";
|
||||||
listConfiguredMessageChannels,
|
import { applyTargetToParams } from "./channel-target.js";
|
||||||
resolveMessageChannelSelection,
|
|
||||||
} from "./channel-selection.js";
|
|
||||||
import type { OutboundSendDeps } from "./deliver.js";
|
import type { OutboundSendDeps } from "./deliver.js";
|
||||||
import type { MessagePollResult, MessageSendResult } from "./message.js";
|
import type { MessagePollResult, MessageSendResult } from "./message.js";
|
||||||
import { sendMessage, sendPoll } from "./message.js";
|
|
||||||
import {
|
import {
|
||||||
applyCrossContextDecoration,
|
applyCrossContextDecoration,
|
||||||
buildCrossContextDecoration,
|
buildCrossContextDecoration,
|
||||||
@ -27,7 +24,9 @@ import {
|
|||||||
enforceCrossContextPolicy,
|
enforceCrossContextPolicy,
|
||||||
shouldApplyCrossContextMarker,
|
shouldApplyCrossContextMarker,
|
||||||
} from "./outbound-policy.js";
|
} from "./outbound-policy.js";
|
||||||
import { resolveMessagingTarget } from "./target-resolver.js";
|
import { executePollAction, executeSendAction } from "./outbound-send-service.js";
|
||||||
|
import { actionRequiresTarget } from "./message-action-spec.js";
|
||||||
|
import { resolveChannelTarget } from "./target-resolver.js";
|
||||||
|
|
||||||
export type MessageActionRunnerGateway = {
|
export type MessageActionRunnerGateway = {
|
||||||
url?: string;
|
url?: string;
|
||||||
@ -195,7 +194,7 @@ async function resolveActionTarget(params: {
|
|||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const toRaw = typeof params.args.to === "string" ? params.args.to.trim() : "";
|
const toRaw = typeof params.args.to === "string" ? params.args.to.trim() : "";
|
||||||
if (toRaw) {
|
if (toRaw) {
|
||||||
const resolved = await resolveMessagingTarget({
|
const resolved = await resolveChannelTarget({
|
||||||
cfg: params.cfg,
|
cfg: params.cfg,
|
||||||
channel: params.channel,
|
channel: params.channel,
|
||||||
input: toRaw,
|
input: toRaw,
|
||||||
@ -210,7 +209,7 @@ async function resolveActionTarget(params: {
|
|||||||
const channelIdRaw =
|
const channelIdRaw =
|
||||||
typeof params.args.channelId === "string" ? params.args.channelId.trim() : "";
|
typeof params.args.channelId === "string" ? params.args.channelId.trim() : "";
|
||||||
if (channelIdRaw) {
|
if (channelIdRaw) {
|
||||||
const resolved = await resolveMessagingTarget({
|
const resolved = await resolveChannelTarget({
|
||||||
cfg: params.cfg,
|
cfg: params.cfg,
|
||||||
channel: params.channel,
|
channel: params.channel,
|
||||||
input: channelIdRaw,
|
input: channelIdRaw,
|
||||||
@ -237,7 +236,6 @@ type ResolvedActionContext = {
|
|||||||
gateway?: MessageActionRunnerGateway;
|
gateway?: MessageActionRunnerGateway;
|
||||||
input: RunMessageActionParams;
|
input: RunMessageActionParams;
|
||||||
};
|
};
|
||||||
|
|
||||||
function resolveGateway(input: RunMessageActionParams): MessageActionRunnerGateway | undefined {
|
function resolveGateway(input: RunMessageActionParams): MessageActionRunnerGateway | undefined {
|
||||||
if (!input.gateway) return undefined;
|
if (!input.gateway) return undefined;
|
||||||
return {
|
return {
|
||||||
@ -281,7 +279,7 @@ async function handleBroadcastAction(
|
|||||||
for (const targetChannel of targetChannels) {
|
for (const targetChannel of targetChannels) {
|
||||||
for (const target of rawTargets) {
|
for (const target of rawTargets) {
|
||||||
try {
|
try {
|
||||||
const resolved = await resolveMessagingTarget({
|
const resolved = await resolveChannelTarget({
|
||||||
cfg: input.cfg,
|
cfg: input.cfg,
|
||||||
channel: targetChannel,
|
channel: targetChannel,
|
||||||
input: target,
|
input: target,
|
||||||
@ -293,7 +291,7 @@ async function handleBroadcastAction(
|
|||||||
params: {
|
params: {
|
||||||
...params,
|
...params,
|
||||||
channel: targetChannel,
|
channel: targetChannel,
|
||||||
to: resolved.target.to,
|
target: resolved.target.to,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
results.push({
|
results.push({
|
||||||
@ -326,11 +324,10 @@ async function handleSendAction(ctx: ResolvedActionContext): Promise<MessageActi
|
|||||||
const { cfg, params, channel, accountId, dryRun, gateway, input } = ctx;
|
const { cfg, params, channel, accountId, dryRun, gateway, input } = ctx;
|
||||||
const action: ChannelMessageActionName = "send";
|
const action: ChannelMessageActionName = "send";
|
||||||
const to = readStringParam(params, "to", { required: true });
|
const to = readStringParam(params, "to", { required: true });
|
||||||
// Allow message to be omitted when sending media-only (e.g., voice notes)
|
|
||||||
const mediaHint = readStringParam(params, "media", { trim: false });
|
const mediaHint = readStringParam(params, "media", { trim: false });
|
||||||
let message =
|
let message =
|
||||||
readStringParam(params, "message", {
|
readStringParam(params, "message", {
|
||||||
required: !mediaHint, // Only require message if no media hint
|
required: !mediaHint,
|
||||||
allowEmpty: true,
|
allowEmpty: true,
|
||||||
}) ?? "";
|
}) ?? "";
|
||||||
|
|
||||||
@ -364,50 +361,29 @@ async function handleSendAction(ctx: ResolvedActionContext): Promise<MessageActi
|
|||||||
const mediaUrl = readStringParam(params, "media", { trim: false });
|
const mediaUrl = readStringParam(params, "media", { trim: false });
|
||||||
const gifPlayback = readBooleanParam(params, "gifPlayback") ?? false;
|
const gifPlayback = readBooleanParam(params, "gifPlayback") ?? false;
|
||||||
const bestEffort = readBooleanParam(params, "bestEffort");
|
const bestEffort = readBooleanParam(params, "bestEffort");
|
||||||
if (!dryRun) {
|
const send = await executeSendAction({
|
||||||
const handled = await dispatchChannelMessageAction({
|
ctx: {
|
||||||
channel,
|
|
||||||
action,
|
|
||||||
cfg,
|
cfg,
|
||||||
|
channel,
|
||||||
params,
|
params,
|
||||||
accountId: accountId ?? undefined,
|
accountId: accountId ?? undefined,
|
||||||
gateway,
|
gateway,
|
||||||
toolContext: input.toolContext,
|
toolContext: input.toolContext,
|
||||||
|
deps: input.deps,
|
||||||
dryRun,
|
dryRun,
|
||||||
});
|
mirror:
|
||||||
if (handled) {
|
input.sessionKey && !dryRun
|
||||||
return {
|
? {
|
||||||
kind: "send",
|
sessionKey: input.sessionKey,
|
||||||
channel,
|
agentId: input.agentId,
|
||||||
action,
|
}
|
||||||
to,
|
: undefined,
|
||||||
handledBy: "plugin",
|
},
|
||||||
payload: extractToolPayload(handled),
|
|
||||||
toolResult: handled,
|
|
||||||
dryRun,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const result: MessageSendResult = await sendMessage({
|
|
||||||
cfg,
|
|
||||||
to,
|
to,
|
||||||
content: message,
|
message,
|
||||||
mediaUrl: mediaUrl || undefined,
|
mediaUrl: mediaUrl || undefined,
|
||||||
channel: channel || undefined,
|
|
||||||
accountId: accountId ?? undefined,
|
|
||||||
gifPlayback,
|
gifPlayback,
|
||||||
dryRun,
|
|
||||||
bestEffort: bestEffort ?? undefined,
|
bestEffort: bestEffort ?? undefined,
|
||||||
deps: input.deps,
|
|
||||||
gateway,
|
|
||||||
mirror:
|
|
||||||
input.sessionKey && !dryRun
|
|
||||||
? {
|
|
||||||
sessionKey: input.sessionKey,
|
|
||||||
agentId: input.agentId,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -415,9 +391,10 @@ async function handleSendAction(ctx: ResolvedActionContext): Promise<MessageActi
|
|||||||
channel,
|
channel,
|
||||||
action,
|
action,
|
||||||
to,
|
to,
|
||||||
handledBy: "core",
|
handledBy: send.handledBy,
|
||||||
payload: result,
|
payload: send.payload,
|
||||||
sendResult: result,
|
toolResult: send.toolResult,
|
||||||
|
sendResult: send.sendResult,
|
||||||
dryRun,
|
dryRun,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -458,41 +435,21 @@ async function handlePollAction(ctx: ResolvedActionContext): Promise<MessageActi
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!dryRun) {
|
const poll = await executePollAction({
|
||||||
const handled = await dispatchChannelMessageAction({
|
ctx: {
|
||||||
channel,
|
|
||||||
action,
|
|
||||||
cfg,
|
cfg,
|
||||||
|
channel,
|
||||||
params,
|
params,
|
||||||
accountId: accountId ?? undefined,
|
accountId: accountId ?? undefined,
|
||||||
gateway,
|
gateway,
|
||||||
toolContext: input.toolContext,
|
toolContext: input.toolContext,
|
||||||
dryRun,
|
dryRun,
|
||||||
});
|
},
|
||||||
if (handled) {
|
|
||||||
return {
|
|
||||||
kind: "poll",
|
|
||||||
channel,
|
|
||||||
action,
|
|
||||||
to,
|
|
||||||
handledBy: "plugin",
|
|
||||||
payload: extractToolPayload(handled),
|
|
||||||
toolResult: handled,
|
|
||||||
dryRun,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const result: MessagePollResult = await sendPoll({
|
|
||||||
cfg,
|
|
||||||
to,
|
to,
|
||||||
question,
|
question,
|
||||||
options,
|
options,
|
||||||
maxSelections,
|
maxSelections,
|
||||||
durationHours: durationHours ?? undefined,
|
durationHours: durationHours ?? undefined,
|
||||||
channel,
|
|
||||||
dryRun,
|
|
||||||
gateway,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -500,9 +457,10 @@ async function handlePollAction(ctx: ResolvedActionContext): Promise<MessageActi
|
|||||||
channel,
|
channel,
|
||||||
action,
|
action,
|
||||||
to,
|
to,
|
||||||
handledBy: "core",
|
handledBy: poll.handledBy,
|
||||||
payload: result,
|
payload: poll.payload,
|
||||||
pollResult: result,
|
toolResult: poll.toolResult,
|
||||||
|
pollResult: poll.pollResult,
|
||||||
dryRun,
|
dryRun,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -560,6 +518,16 @@ export async function runMessageAction(
|
|||||||
return handleBroadcastAction(input, params);
|
return handleBroadcastAction(input, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applyTargetToParams({ action, args: params });
|
||||||
|
if (actionRequiresTarget(action)) {
|
||||||
|
const hasTarget =
|
||||||
|
(typeof params.to === "string" && params.to.trim()) ||
|
||||||
|
(typeof params.channelId === "string" && params.channelId.trim());
|
||||||
|
if (!hasTarget) {
|
||||||
|
throw new Error(`Action ${action} requires a target.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const channel = await resolveChannel(cfg, params);
|
const channel = await resolveChannel(cfg, params);
|
||||||
const accountId = readStringParam(params, "accountId") ?? input.defaultAccountId;
|
const accountId = readStringParam(params, "accountId") ?? input.defaultAccountId;
|
||||||
const dryRun = Boolean(input.dryRun ?? readBooleanParam(params, "dryRun"));
|
const dryRun = Boolean(input.dryRun ?? readBooleanParam(params, "dryRun"));
|
||||||
|
|||||||
50
src/infra/outbound/message-action-spec.ts
Normal file
50
src/infra/outbound/message-action-spec.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import type { ChannelMessageActionName } from "../../channels/plugins/types.js";
|
||||||
|
|
||||||
|
export type MessageActionTargetMode = "to" | "channelId" | "none";
|
||||||
|
|
||||||
|
export const MESSAGE_ACTION_TARGET_MODE: Record<ChannelMessageActionName, MessageActionTargetMode> =
|
||||||
|
{
|
||||||
|
send: "to",
|
||||||
|
broadcast: "none",
|
||||||
|
poll: "to",
|
||||||
|
react: "to",
|
||||||
|
reactions: "to",
|
||||||
|
read: "to",
|
||||||
|
edit: "to",
|
||||||
|
delete: "to",
|
||||||
|
pin: "to",
|
||||||
|
unpin: "to",
|
||||||
|
"list-pins": "to",
|
||||||
|
permissions: "to",
|
||||||
|
"thread-create": "to",
|
||||||
|
"thread-list": "none",
|
||||||
|
"thread-reply": "to",
|
||||||
|
search: "none",
|
||||||
|
sticker: "to",
|
||||||
|
"member-info": "none",
|
||||||
|
"role-info": "none",
|
||||||
|
"emoji-list": "none",
|
||||||
|
"emoji-upload": "none",
|
||||||
|
"sticker-upload": "none",
|
||||||
|
"role-add": "none",
|
||||||
|
"role-remove": "none",
|
||||||
|
"channel-info": "channelId",
|
||||||
|
"channel-list": "none",
|
||||||
|
"channel-create": "none",
|
||||||
|
"channel-edit": "channelId",
|
||||||
|
"channel-delete": "channelId",
|
||||||
|
"channel-move": "channelId",
|
||||||
|
"category-create": "none",
|
||||||
|
"category-edit": "none",
|
||||||
|
"category-delete": "none",
|
||||||
|
"voice-status": "none",
|
||||||
|
"event-list": "none",
|
||||||
|
"event-create": "none",
|
||||||
|
timeout: "none",
|
||||||
|
kick: "none",
|
||||||
|
ban: "none",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function actionRequiresTarget(action: ChannelMessageActionName): boolean {
|
||||||
|
return MESSAGE_ACTION_TARGET_MODE[action] !== "none";
|
||||||
|
}
|
||||||
93
src/infra/outbound/outbound-policy.test.ts
Normal file
93
src/infra/outbound/outbound-policy.test.ts
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
|
import {
|
||||||
|
applyCrossContextDecoration,
|
||||||
|
buildCrossContextDecoration,
|
||||||
|
enforceCrossContextPolicy,
|
||||||
|
} from "./outbound-policy.js";
|
||||||
|
|
||||||
|
const slackConfig = {
|
||||||
|
channels: {
|
||||||
|
slack: {
|
||||||
|
botToken: "xoxb-test",
|
||||||
|
appToken: "xapp-test",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as ClawdbotConfig;
|
||||||
|
|
||||||
|
const discordConfig = {
|
||||||
|
channels: {
|
||||||
|
discord: {},
|
||||||
|
},
|
||||||
|
} as ClawdbotConfig;
|
||||||
|
|
||||||
|
describe("outbound policy", () => {
|
||||||
|
it("blocks cross-provider sends by default", () => {
|
||||||
|
expect(() =>
|
||||||
|
enforceCrossContextPolicy({
|
||||||
|
cfg: slackConfig,
|
||||||
|
channel: "telegram",
|
||||||
|
action: "send",
|
||||||
|
args: { to: "telegram:@ops" },
|
||||||
|
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" },
|
||||||
|
}),
|
||||||
|
).toThrow(/Cross-context messaging denied/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows cross-provider sends when enabled", () => {
|
||||||
|
const cfg = {
|
||||||
|
...slackConfig,
|
||||||
|
tools: {
|
||||||
|
message: { crossContext: { allowAcrossProviders: true } },
|
||||||
|
},
|
||||||
|
} as ClawdbotConfig;
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
enforceCrossContextPolicy({
|
||||||
|
cfg,
|
||||||
|
channel: "telegram",
|
||||||
|
action: "send",
|
||||||
|
args: { to: "telegram:@ops" },
|
||||||
|
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" },
|
||||||
|
}),
|
||||||
|
).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("blocks same-provider cross-context when disabled", () => {
|
||||||
|
const cfg = {
|
||||||
|
...slackConfig,
|
||||||
|
tools: { message: { crossContext: { allowWithinProvider: false } } },
|
||||||
|
} as ClawdbotConfig;
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
enforceCrossContextPolicy({
|
||||||
|
cfg,
|
||||||
|
channel: "slack",
|
||||||
|
action: "send",
|
||||||
|
args: { to: "C99999999" },
|
||||||
|
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" },
|
||||||
|
}),
|
||||||
|
).toThrow(/Cross-context messaging denied/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses embeds when available and preferred", async () => {
|
||||||
|
const decoration = await buildCrossContextDecoration({
|
||||||
|
cfg: discordConfig,
|
||||||
|
channel: "discord",
|
||||||
|
target: "123",
|
||||||
|
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "discord" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(decoration).not.toBeNull();
|
||||||
|
const applied = applyCrossContextDecoration({
|
||||||
|
message: "hello",
|
||||||
|
decoration: decoration!,
|
||||||
|
preferEmbeds: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(applied.usedEmbeds).toBe(true);
|
||||||
|
expect(applied.embeds?.length).toBeGreaterThan(0);
|
||||||
|
expect(applied.message).toBe("hello");
|
||||||
|
});
|
||||||
|
});
|
||||||
164
src/infra/outbound/outbound-send-service.ts
Normal file
164
src/infra/outbound/outbound-send-service.ts
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||||
|
import { dispatchChannelMessageAction } from "../../channels/plugins/message-actions.js";
|
||||||
|
import type {
|
||||||
|
ChannelId,
|
||||||
|
ChannelThreadingToolContext,
|
||||||
|
} from "../../channels/plugins/types.js";
|
||||||
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
|
import type { GatewayClientMode, GatewayClientName } from "../../utils/message-channel.js";
|
||||||
|
import type { OutboundSendDeps } from "./deliver.js";
|
||||||
|
import type { MessagePollResult, MessageSendResult } from "./message.js";
|
||||||
|
import { sendMessage, sendPoll } from "./message.js";
|
||||||
|
|
||||||
|
export type OutboundGatewayContext = {
|
||||||
|
url?: string;
|
||||||
|
token?: string;
|
||||||
|
timeoutMs?: number;
|
||||||
|
clientName: GatewayClientName;
|
||||||
|
clientDisplayName?: string;
|
||||||
|
mode: GatewayClientMode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OutboundSendContext = {
|
||||||
|
cfg: ClawdbotConfig;
|
||||||
|
channel: ChannelId;
|
||||||
|
params: Record<string, unknown>;
|
||||||
|
accountId?: string | null;
|
||||||
|
gateway?: OutboundGatewayContext;
|
||||||
|
toolContext?: ChannelThreadingToolContext;
|
||||||
|
deps?: OutboundSendDeps;
|
||||||
|
dryRun: boolean;
|
||||||
|
mirror?: {
|
||||||
|
sessionKey: string;
|
||||||
|
agentId?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function extractToolPayload(result: AgentToolResult<unknown>): unknown {
|
||||||
|
if (result.details !== undefined) return result.details;
|
||||||
|
const textBlock = Array.isArray(result.content)
|
||||||
|
? result.content.find(
|
||||||
|
(block) =>
|
||||||
|
block &&
|
||||||
|
typeof block === "object" &&
|
||||||
|
(block as { type?: unknown }).type === "text" &&
|
||||||
|
typeof (block as { text?: unknown }).text === "string",
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
|
const text = (textBlock as { text?: string } | undefined)?.text;
|
||||||
|
if (text) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(text);
|
||||||
|
} catch {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.content ?? result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function executeSendAction(params: {
|
||||||
|
ctx: OutboundSendContext;
|
||||||
|
to: string;
|
||||||
|
message: string;
|
||||||
|
mediaUrl?: string;
|
||||||
|
gifPlayback?: boolean;
|
||||||
|
bestEffort?: boolean;
|
||||||
|
}): Promise<{
|
||||||
|
handledBy: "plugin" | "core";
|
||||||
|
payload: unknown;
|
||||||
|
toolResult?: AgentToolResult<unknown>;
|
||||||
|
sendResult?: MessageSendResult;
|
||||||
|
}> {
|
||||||
|
if (!params.ctx.dryRun) {
|
||||||
|
const handled = await dispatchChannelMessageAction({
|
||||||
|
channel: params.ctx.channel,
|
||||||
|
action: "send",
|
||||||
|
cfg: params.ctx.cfg,
|
||||||
|
params: params.ctx.params,
|
||||||
|
accountId: params.ctx.accountId ?? undefined,
|
||||||
|
gateway: params.ctx.gateway,
|
||||||
|
toolContext: params.ctx.toolContext,
|
||||||
|
dryRun: params.ctx.dryRun,
|
||||||
|
});
|
||||||
|
if (handled) {
|
||||||
|
return {
|
||||||
|
handledBy: "plugin",
|
||||||
|
payload: extractToolPayload(handled),
|
||||||
|
toolResult: handled,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: MessageSendResult = await sendMessage({
|
||||||
|
cfg: params.ctx.cfg,
|
||||||
|
to: params.to,
|
||||||
|
content: params.message,
|
||||||
|
mediaUrl: params.mediaUrl || undefined,
|
||||||
|
channel: params.ctx.channel || undefined,
|
||||||
|
accountId: params.ctx.accountId ?? undefined,
|
||||||
|
gifPlayback: params.gifPlayback,
|
||||||
|
dryRun: params.ctx.dryRun,
|
||||||
|
bestEffort: params.bestEffort ?? undefined,
|
||||||
|
deps: params.ctx.deps,
|
||||||
|
gateway: params.ctx.gateway,
|
||||||
|
mirror: params.ctx.mirror,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
handledBy: "core",
|
||||||
|
payload: result,
|
||||||
|
sendResult: result,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function executePollAction(params: {
|
||||||
|
ctx: OutboundSendContext;
|
||||||
|
to: string;
|
||||||
|
question: string;
|
||||||
|
options: string[];
|
||||||
|
maxSelections: number;
|
||||||
|
durationHours?: number;
|
||||||
|
}): Promise<{
|
||||||
|
handledBy: "plugin" | "core";
|
||||||
|
payload: unknown;
|
||||||
|
toolResult?: AgentToolResult<unknown>;
|
||||||
|
pollResult?: MessagePollResult;
|
||||||
|
}> {
|
||||||
|
if (!params.ctx.dryRun) {
|
||||||
|
const handled = await dispatchChannelMessageAction({
|
||||||
|
channel: params.ctx.channel,
|
||||||
|
action: "poll",
|
||||||
|
cfg: params.ctx.cfg,
|
||||||
|
params: params.ctx.params,
|
||||||
|
accountId: params.ctx.accountId ?? undefined,
|
||||||
|
gateway: params.ctx.gateway,
|
||||||
|
toolContext: params.ctx.toolContext,
|
||||||
|
dryRun: params.ctx.dryRun,
|
||||||
|
});
|
||||||
|
if (handled) {
|
||||||
|
return {
|
||||||
|
handledBy: "plugin",
|
||||||
|
payload: extractToolPayload(handled),
|
||||||
|
toolResult: handled,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: MessagePollResult = await sendPoll({
|
||||||
|
cfg: params.ctx.cfg,
|
||||||
|
to: params.to,
|
||||||
|
question: params.question,
|
||||||
|
options: params.options,
|
||||||
|
maxSelections: params.maxSelections,
|
||||||
|
durationHours: params.durationHours ?? undefined,
|
||||||
|
channel: params.ctx.channel,
|
||||||
|
dryRun: params.ctx.dryRun,
|
||||||
|
gateway: params.ctx.gateway,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
handledBy: "core",
|
||||||
|
payload: result,
|
||||||
|
pollResult: result,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -23,9 +23,34 @@ export type ResolveMessagingTargetResult =
|
|||||||
| { ok: true; target: ResolvedMessagingTarget }
|
| { ok: true; target: ResolvedMessagingTarget }
|
||||||
| { ok: false; error: Error; candidates?: ChannelDirectoryEntry[] };
|
| { ok: false; error: Error; candidates?: ChannelDirectoryEntry[] };
|
||||||
|
|
||||||
|
export async function resolveChannelTarget(params: {
|
||||||
|
cfg: ClawdbotConfig;
|
||||||
|
channel: ChannelId;
|
||||||
|
input: string;
|
||||||
|
accountId?: string | null;
|
||||||
|
preferredKind?: TargetResolveKind;
|
||||||
|
runtime?: RuntimeEnv;
|
||||||
|
}): Promise<ResolveMessagingTargetResult> {
|
||||||
|
return resolveMessagingTarget(params);
|
||||||
|
}
|
||||||
|
|
||||||
const CACHE_TTL_MS = 30 * 60 * 1000;
|
const CACHE_TTL_MS = 30 * 60 * 1000;
|
||||||
const directoryCache = new DirectoryCache<ChannelDirectoryEntry[]>(CACHE_TTL_MS);
|
const directoryCache = new DirectoryCache<ChannelDirectoryEntry[]>(CACHE_TTL_MS);
|
||||||
|
|
||||||
|
export function resetDirectoryCache(params?: { channel?: ChannelId; accountId?: string | null }) {
|
||||||
|
if (!params?.channel) {
|
||||||
|
directoryCache.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const channelKey = params.channel;
|
||||||
|
const accountKey = params.accountId ?? "default";
|
||||||
|
directoryCache.clearMatching((key) => {
|
||||||
|
if (!key.startsWith(`${channelKey}:`)) return false;
|
||||||
|
if (!params.accountId) return true;
|
||||||
|
return key.startsWith(`${channelKey}:${accountKey}:`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeQuery(value: string): string {
|
function normalizeQuery(value: string): string {
|
||||||
return value.trim().toLowerCase();
|
return value.trim().toLowerCase();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -73,7 +73,7 @@ export function resolveOutboundTarget(params: {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
error: new Error(`Delivering to ${plugin.meta.label} requires --to`),
|
error: new Error(`Delivering to ${plugin.meta.label} requires a destination`),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user