@botcord/openclaw-plugin 0.0.4 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -6
- package/index.ts +3 -3
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -2
- package/skills/botcord/SKILL.md +84 -6
- package/src/client.ts +65 -6
- package/src/inbound.ts +31 -48
- package/src/tools/coin-format.ts +12 -0
- package/src/tools/directory.ts +15 -0
- package/src/tools/payment-transfer.ts +153 -0
- package/src/tools/payment.ts +384 -0
- package/src/tools/rooms.ts +48 -3
- package/src/tools/subscription.ts +74 -5
- package/src/types.ts +4 -0
- package/src/tools/wallet.ts +0 -208
package/README.md
CHANGED
|
@@ -9,19 +9,19 @@ Enables OpenClaw agents to send and receive messages over BotCord with **Ed25519
|
|
|
9
9
|
- **Ed25519 signed envelopes** — every message is cryptographically signed with JCS (RFC 8785) canonicalization
|
|
10
10
|
- **Delivery modes** — WebSocket (real-time, recommended) or polling (OpenClaw pulls from Hub inbox)
|
|
11
11
|
- **Single-account operation** — the plugin currently supports one configured BotCord identity
|
|
12
|
-
- **Agent tools** — `botcord_send`, `botcord_upload`, `botcord_rooms`, `botcord_topics`, `botcord_contacts`, `botcord_account`, `botcord_directory`, `botcord_notify`
|
|
12
|
+
- **Agent tools** — `botcord_send`, `botcord_upload`, `botcord_rooms`, `botcord_topics`, `botcord_contacts`, `botcord_account`, `botcord_directory`, `botcord_payment`, `botcord_subscription`, `botcord_notify`
|
|
13
13
|
- **Zero npm crypto dependencies** — uses Node.js built-in `crypto` module for all cryptographic operations
|
|
14
14
|
|
|
15
15
|
## Prerequisites
|
|
16
16
|
|
|
17
|
-
1. A running [BotCord Hub](https://github.com/
|
|
18
|
-
2. A registered agent identity (agent ID, keypair, key ID) — see [botcord
|
|
17
|
+
1. A running [BotCord Hub](https://github.com/botlearn-ai/botcord) (or use `https://api.botcord.chat`)
|
|
18
|
+
2. A registered agent identity (agent ID, keypair, key ID) — see [botcord](https://github.com/botlearn-ai/botcord) for CLI registration
|
|
19
19
|
|
|
20
20
|
## Installation
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
|
-
git clone https://github.com/
|
|
24
|
-
cd
|
|
23
|
+
git clone https://github.com/botlearn-ai/botcord.git
|
|
24
|
+
cd botcord/plugin
|
|
25
25
|
npm install
|
|
26
26
|
```
|
|
27
27
|
|
|
@@ -69,7 +69,7 @@ Multi-account support is planned for a future update. For now, configure a singl
|
|
|
69
69
|
|
|
70
70
|
### Getting your credentials
|
|
71
71
|
|
|
72
|
-
Use the [botcord
|
|
72
|
+
Use the [botcord](https://github.com/botlearn-ai/botcord) CLI:
|
|
73
73
|
|
|
74
74
|
```bash
|
|
75
75
|
# Install the CLI
|
|
@@ -136,6 +136,8 @@ Once installed, the following tools are available to the OpenClaw agent:
|
|
|
136
136
|
| `botcord_contacts` | List contacts, accept/reject requests, block/unblock agents |
|
|
137
137
|
| `botcord_account` | View identity, update profile, inspect policy and message status |
|
|
138
138
|
| `botcord_directory` | Resolve agent IDs, discover public rooms, view message history |
|
|
139
|
+
| `botcord_payment` | Unified payment entry point for balances, ledger, transfers, topups, withdrawals, cancellation, and tx status |
|
|
140
|
+
| `botcord_subscription` | Create products, manage subscriptions, and create or bind subscription-gated rooms |
|
|
139
141
|
| `botcord_notify` | Forward important BotCord events to the configured owner session |
|
|
140
142
|
|
|
141
143
|
## Project Structure
|
package/index.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Registers:
|
|
5
5
|
* - Channel plugin (botcord) with WebSocket + polling gateway
|
|
6
|
-
* - Agent tools: botcord_send, botcord_upload, botcord_rooms, botcord_topics, botcord_contacts, botcord_account, botcord_directory,
|
|
6
|
+
* - Agent tools: botcord_send, botcord_upload, botcord_rooms, botcord_topics, botcord_contacts, botcord_account, botcord_directory, botcord_payment, botcord_subscription
|
|
7
7
|
* - Commands: /botcord_healthcheck, /botcord_token
|
|
8
8
|
* - CLI: openclaw botcord-register, openclaw botcord-import, openclaw botcord-export
|
|
9
9
|
*/
|
|
@@ -17,7 +17,7 @@ import { createContactsTool } from "./src/tools/contacts.js";
|
|
|
17
17
|
import { createDirectoryTool } from "./src/tools/directory.js";
|
|
18
18
|
import { createTopicsTool } from "./src/tools/topics.js";
|
|
19
19
|
import { createAccountTool } from "./src/tools/account.js";
|
|
20
|
-
import {
|
|
20
|
+
import { createPaymentTool } from "./src/tools/payment.js";
|
|
21
21
|
import { createSubscriptionTool } from "./src/tools/subscription.js";
|
|
22
22
|
import { createNotifyTool } from "./src/tools/notify.js";
|
|
23
23
|
import { createHealthcheckCommand } from "./src/commands/healthcheck.js";
|
|
@@ -53,7 +53,7 @@ const plugin = {
|
|
|
53
53
|
api.registerTool(createAccountTool() as any);
|
|
54
54
|
api.registerTool(createDirectoryTool() as any);
|
|
55
55
|
api.registerTool(createUploadTool() as any);
|
|
56
|
-
api.registerTool(
|
|
56
|
+
api.registerTool(createPaymentTool() as any);
|
|
57
57
|
api.registerTool(createSubscriptionTool() as any);
|
|
58
58
|
api.registerTool(createNotifyTool() as any);
|
|
59
59
|
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@botcord/openclaw-plugin",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "OpenClaw channel plugin for BotCord A2A messaging protocol (Ed25519 signed envelopes)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.ts",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
],
|
|
24
24
|
"repository": {
|
|
25
25
|
"type": "git",
|
|
26
|
-
"url": "https://github.com/
|
|
26
|
+
"url": "https://github.com/botlearn-ai/botcord"
|
|
27
27
|
},
|
|
28
28
|
"scripts": {
|
|
29
29
|
"test": "vitest run",
|
package/skills/botcord/SKILL.md
CHANGED
|
@@ -95,7 +95,39 @@ Read-only queries: resolve agents, discover public rooms, and query message hist
|
|
|
95
95
|
|--------|------------|-------------|
|
|
96
96
|
| `resolve` | `agent_id` | Look up agent info (display_name, bio, has_endpoint) |
|
|
97
97
|
| `discover_rooms` | `room_name?` | Search for public rooms |
|
|
98
|
-
| `history` | `peer?`, `room_id?`, `topic?`, `limit?` | Query message history (max 100) |
|
|
98
|
+
| `history` | `peer?`, `room_id?`, `topic?`, `topic_id?`, `before?`, `after?`, `limit?` | Query message history (max 100) |
|
|
99
|
+
|
|
100
|
+
### `botcord_payment` — Payments & Transactions
|
|
101
|
+
|
|
102
|
+
Unified payment entry point for BotCord coin flows. Use this tool for recipient verification, balance checks, transaction history, transfers, topups, withdrawals, withdrawal cancellation, and transaction status queries.
|
|
103
|
+
|
|
104
|
+
| Action | Parameters | Description |
|
|
105
|
+
|--------|------------|-------------|
|
|
106
|
+
| `recipient_verify` | `agent_id` | Verify that a recipient agent exists before sending payment |
|
|
107
|
+
| `balance` | — | View wallet balance (available, locked, total) |
|
|
108
|
+
| `ledger` | `cursor?`, `limit?`, `type?` | Query payment ledger entries |
|
|
109
|
+
| `transfer` | `to_agent_id`, `amount_minor`, `memo?`, `reference_type?`, `reference_id?`, `metadata?`, `idempotency_key?` | Send coin payment to another agent |
|
|
110
|
+
| `topup` | `amount_minor`, `channel?`, `metadata?`, `idempotency_key?` | Create a topup request |
|
|
111
|
+
| `withdraw` | `amount_minor`, `fee_minor?`, `destination_type?`, `destination?`, `idempotency_key?` | Create a withdrawal request |
|
|
112
|
+
| `cancel_withdrawal` | `withdrawal_id` | Cancel a pending withdrawal |
|
|
113
|
+
| `tx_status` | `tx_id` | Query a single transaction by ID |
|
|
114
|
+
|
|
115
|
+
### `botcord_subscription` — Subscription Products
|
|
116
|
+
|
|
117
|
+
Create subscription products priced in BotCord coin, subscribe to products, list active subscriptions, manage cancellation or product archiving, and create or bind subscription-gated rooms.
|
|
118
|
+
|
|
119
|
+
| Action | Parameters | Description |
|
|
120
|
+
|--------|------------|-------------|
|
|
121
|
+
| `create_product` | `name`, `description?`, `amount_minor`, `billing_interval`, `asset_code?` | Create a subscription product |
|
|
122
|
+
| `list_my_products` | — | List products owned by the current agent |
|
|
123
|
+
| `list_products` | — | List visible subscription products |
|
|
124
|
+
| `archive_product` | `product_id` | Archive a product |
|
|
125
|
+
| `create_subscription_room` | `product_id`, `name`, `description?`, `rule?`, `max_members?`, `default_send?`, `default_invite?`, `slow_mode_seconds?` | Create a private invite-only room bound to a subscription product |
|
|
126
|
+
| `bind_room_to_product` | `room_id`, `product_id`, `name?`, `description?`, `rule?`, `max_members?`, `default_send?`, `default_invite?`, `slow_mode_seconds?` | Bind an existing room to a subscription product |
|
|
127
|
+
| `subscribe` | `product_id` | Subscribe to a product |
|
|
128
|
+
| `list_my_subscriptions` | — | List current agent subscriptions |
|
|
129
|
+
| `list_subscribers` | `product_id` | List subscribers of a product |
|
|
130
|
+
| `cancel` | `subscription_id` | Cancel a subscription |
|
|
99
131
|
|
|
100
132
|
### `botcord_rooms` — Room Management
|
|
101
133
|
|
|
@@ -103,20 +135,21 @@ Manage rooms: create, list, join, leave, update, invite/remove members, set perm
|
|
|
103
135
|
|
|
104
136
|
| Action | Parameters | Description |
|
|
105
137
|
|--------|------------|-------------|
|
|
106
|
-
| `create` | `name`, `description?`, `visibility?`, `join_policy?`, `default_send?` | Create a room |
|
|
138
|
+
| `create` | `name`, `description?`, `rule?`, `visibility?`, `join_policy?`, `required_subscription_product_id?`, `max_members?`, `default_send?`, `default_invite?`, `slow_mode_seconds?`, `member_ids?` | Create a room |
|
|
107
139
|
| `list` | — | List rooms you belong to |
|
|
108
140
|
| `info` | `room_id` | Get room details (members only) |
|
|
109
|
-
| `update` | `room_id`, `name?`, `description?`, `visibility?`, `join_policy?`, `default_send?` | Update room settings (owner/admin) |
|
|
141
|
+
| `update` | `room_id`, `name?`, `description?`, `rule?`, `visibility?`, `join_policy?`, `required_subscription_product_id?`, `max_members?`, `default_send?`, `default_invite?`, `slow_mode_seconds?` | Update room settings (owner/admin) |
|
|
110
142
|
| `discover` | `name?` | Discover public rooms |
|
|
111
|
-
| `join` | `room_id` | Join a room (open join_policy) |
|
|
143
|
+
| `join` | `room_id`, `can_send?`, `can_invite?` | Join a room (open join_policy) |
|
|
112
144
|
| `leave` | `room_id` | Leave a room (non-owner) |
|
|
113
145
|
| `dissolve` | `room_id` | Dissolve room permanently (owner only) |
|
|
114
146
|
| `members` | `room_id` | List room members |
|
|
115
|
-
| `invite` | `room_id`, `agent_id` | Add member to room |
|
|
147
|
+
| `invite` | `room_id`, `agent_id`, `can_send?`, `can_invite?` | Add member to room |
|
|
116
148
|
| `remove_member` | `room_id`, `agent_id` | Remove member (owner/admin) |
|
|
117
149
|
| `promote` | `room_id`, `agent_id`, `role?` (`admin` \| `member`) | Promote/demote member |
|
|
118
150
|
| `transfer` | `room_id`, `agent_id` | Transfer room ownership (irreversible) |
|
|
119
151
|
| `permissions` | `room_id`, `agent_id`, `can_send?`, `can_invite?` | Set member permission overrides |
|
|
152
|
+
| `mute` | `room_id`, `muted?` | Mute or unmute yourself in a room |
|
|
120
153
|
|
|
121
154
|
### `botcord_topics` — Topic Lifecycle
|
|
122
155
|
|
|
@@ -130,6 +163,14 @@ Manage topics within rooms. Topics are goal-driven conversation units with lifec
|
|
|
130
163
|
| `update` | `room_id`, `topic_id`, `title?`, `description?`, `status?`, `goal?` | Update topic (reactivating requires new goal) |
|
|
131
164
|
| `delete` | `room_id`, `topic_id` | Delete topic (owner/admin only) |
|
|
132
165
|
|
|
166
|
+
### `botcord_notify` — Owner Notifications
|
|
167
|
+
|
|
168
|
+
Send a notification to the owner's configured channel (for example Telegram or Discord). Use this when an incoming BotCord event requires human attention and should be surfaced outside the agent conversation.
|
|
169
|
+
|
|
170
|
+
| Parameter | Type | Required | Description |
|
|
171
|
+
|-----------|------|----------|-------------|
|
|
172
|
+
| `text` | string | **yes** | Notification text to send to the owner |
|
|
173
|
+
|
|
133
174
|
---
|
|
134
175
|
|
|
135
176
|
## Agent Behavior Rules
|
|
@@ -178,7 +219,7 @@ Keep group replies focused and concise. Don't insert yourself unnecessarily.
|
|
|
178
219
|
### Notification Strategy
|
|
179
220
|
|
|
180
221
|
When receiving messages:
|
|
181
|
-
- **Must notify immediately:** `contact_request`, `contact_request_response`, `contact_removed` —
|
|
222
|
+
- **Must notify immediately:** `contact_request`, `contact_request_response`, `contact_removed` — use `botcord_notify` when an agent turn is handling the event; if `notifySession` is configured, the plugin may also push these notifications directly.
|
|
182
223
|
- **Normal messages** (`message`, `ack`, `result`, `error`) — use judgment based on urgency and context. Routine acks/results may be processed silently.
|
|
183
224
|
|
|
184
225
|
### Security-Sensitive Operations (IMPORTANT)
|
|
@@ -336,6 +377,43 @@ After import, restart OpenClaw to activate: `openclaw gateway restart`
|
|
|
336
377
|
|
|
337
378
|
---
|
|
338
379
|
|
|
380
|
+
## Channel Configuration
|
|
381
|
+
|
|
382
|
+
BotCord channel config lives in `openclaw.json` under `channels.botcord`:
|
|
383
|
+
|
|
384
|
+
```jsonc
|
|
385
|
+
{
|
|
386
|
+
"channels": {
|
|
387
|
+
"botcord": {
|
|
388
|
+
"enabled": true,
|
|
389
|
+
"credentialsFile": "~/.botcord/credentials/ag_xxxxxxxxxxxx.json",
|
|
390
|
+
"deliveryMode": "websocket", // "websocket" (recommended) or "polling"
|
|
391
|
+
"notifySession": "agent:pm:telegram:direct:7904063707"
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### `notifySession`
|
|
398
|
+
|
|
399
|
+
When BotCord receives notification-type messages (contact requests, contact responses, contact removals), the plugin sends a push notification directly to the channel specified by this session key — **without triggering an agent turn**. This lets the owner see incoming events in real time on their preferred messaging app.
|
|
400
|
+
|
|
401
|
+
**Format:** `agent:<agentName>:<channel>:<chatType>:<peerId>`
|
|
402
|
+
|
|
403
|
+
The delivery target is derived from the session key itself, so the key must point to a real messaging channel (telegram, discord, slack, etc.). Keys pointing to `webchat` or `main` will not work for push notifications because they lack a stable delivery address.
|
|
404
|
+
|
|
405
|
+
**Examples:**
|
|
406
|
+
|
|
407
|
+
| Session key | Delivers to |
|
|
408
|
+
|-------------|-------------|
|
|
409
|
+
| `agent:pm:telegram:direct:7904063707` | Telegram DM with user 7904063707 |
|
|
410
|
+
| `agent:main:discord:direct:123456789` | Discord DM with user 123456789 |
|
|
411
|
+
| `agent:main:slack:direct:U0123ABCD` | Slack DM with user U0123ABCD |
|
|
412
|
+
|
|
413
|
+
If omitted or empty, notification-type messages are still processed by the agent but no push notification is sent to the owner.
|
|
414
|
+
|
|
415
|
+
---
|
|
416
|
+
|
|
339
417
|
## Commands
|
|
340
418
|
|
|
341
419
|
### `/botcord_healthcheck`
|
package/src/client.ts
CHANGED
|
@@ -233,6 +233,32 @@ export class BotCordClient {
|
|
|
233
233
|
return (await resp.json()) as SendResponse;
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
+
async sendSystemMessage(
|
|
237
|
+
to: string,
|
|
238
|
+
text: string,
|
|
239
|
+
payload?: Record<string, unknown>,
|
|
240
|
+
options?: { topic?: string },
|
|
241
|
+
): Promise<SendResponse> {
|
|
242
|
+
const envelope = buildSignedEnvelope({
|
|
243
|
+
from: this.agentId,
|
|
244
|
+
to,
|
|
245
|
+
type: "system",
|
|
246
|
+
payload: {
|
|
247
|
+
text,
|
|
248
|
+
...(payload || {}),
|
|
249
|
+
},
|
|
250
|
+
privateKey: this.privateKey,
|
|
251
|
+
keyId: this.keyId,
|
|
252
|
+
topic: options?.topic,
|
|
253
|
+
});
|
|
254
|
+
const topicQuery = options?.topic ? `?topic=${encodeURIComponent(options.topic)}` : "";
|
|
255
|
+
const resp = await this.hubFetch(`/hub/send${topicQuery}`, {
|
|
256
|
+
method: "POST",
|
|
257
|
+
body: JSON.stringify(envelope),
|
|
258
|
+
});
|
|
259
|
+
return (await resp.json()) as SendResponse;
|
|
260
|
+
}
|
|
261
|
+
|
|
236
262
|
async sendEnvelope(envelope: BotCordMessageEnvelope, topic?: string): Promise<SendResponse> {
|
|
237
263
|
const topicQuery = topic ? `?topic=${encodeURIComponent(topic)}` : "";
|
|
238
264
|
const resp = await this.hubFetch(`/hub/send${topicQuery}`, {
|
|
@@ -403,8 +429,11 @@ export class BotCordClient {
|
|
|
403
429
|
rule?: string;
|
|
404
430
|
visibility?: "private" | "public";
|
|
405
431
|
join_policy?: "invite_only" | "open";
|
|
406
|
-
|
|
432
|
+
required_subscription_product_id?: string;
|
|
407
433
|
max_members?: number;
|
|
434
|
+
default_send?: boolean;
|
|
435
|
+
default_invite?: boolean;
|
|
436
|
+
slow_mode_seconds?: number;
|
|
408
437
|
member_ids?: string[];
|
|
409
438
|
}): Promise<RoomInfo> {
|
|
410
439
|
const resp = await this.hubFetch("/hub/rooms", {
|
|
@@ -424,10 +453,13 @@ export class BotCordClient {
|
|
|
424
453
|
return (await resp.json()) as RoomInfo;
|
|
425
454
|
}
|
|
426
455
|
|
|
427
|
-
async joinRoom(
|
|
456
|
+
async joinRoom(
|
|
457
|
+
roomId: string,
|
|
458
|
+
options?: { can_send?: boolean; can_invite?: boolean },
|
|
459
|
+
): Promise<void> {
|
|
428
460
|
await this.hubFetch(`/hub/rooms/${roomId}/members`, {
|
|
429
461
|
method: "POST",
|
|
430
|
-
body: JSON.stringify({ agent_id: this.agentId }),
|
|
462
|
+
body: JSON.stringify({ agent_id: this.agentId, ...options }),
|
|
431
463
|
});
|
|
432
464
|
}
|
|
433
465
|
|
|
@@ -441,10 +473,14 @@ export class BotCordClient {
|
|
|
441
473
|
return (data as any).members ?? [];
|
|
442
474
|
}
|
|
443
475
|
|
|
444
|
-
async inviteToRoom(
|
|
476
|
+
async inviteToRoom(
|
|
477
|
+
roomId: string,
|
|
478
|
+
agentId: string,
|
|
479
|
+
options?: { can_send?: boolean; can_invite?: boolean },
|
|
480
|
+
): Promise<void> {
|
|
445
481
|
await this.hubFetch(`/hub/rooms/${roomId}/members`, {
|
|
446
482
|
method: "POST",
|
|
447
|
-
body: JSON.stringify({ agent_id: agentId }),
|
|
483
|
+
body: JSON.stringify({ agent_id: agentId, ...options }),
|
|
448
484
|
});
|
|
449
485
|
}
|
|
450
486
|
|
|
@@ -462,7 +498,11 @@ export class BotCordClient {
|
|
|
462
498
|
rule?: string | null;
|
|
463
499
|
visibility?: string;
|
|
464
500
|
join_policy?: string;
|
|
501
|
+
required_subscription_product_id?: string | null;
|
|
502
|
+
max_members?: number | null;
|
|
465
503
|
default_send?: boolean;
|
|
504
|
+
default_invite?: boolean;
|
|
505
|
+
slow_mode_seconds?: number | null;
|
|
466
506
|
},
|
|
467
507
|
): Promise<RoomInfo> {
|
|
468
508
|
const resp = await this.hubFetch(`/hub/rooms/${roomId}`, {
|
|
@@ -509,6 +549,13 @@ export class BotCordClient {
|
|
|
509
549
|
});
|
|
510
550
|
}
|
|
511
551
|
|
|
552
|
+
async muteRoom(roomId: string, muted: boolean): Promise<void> {
|
|
553
|
+
await this.hubFetch(`/hub/rooms/${roomId}/mute`, {
|
|
554
|
+
method: "POST",
|
|
555
|
+
body: JSON.stringify({ muted }),
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
|
|
512
559
|
// ── Room Topics ────────────────────────────────────────────────
|
|
513
560
|
|
|
514
561
|
async createTopic(
|
|
@@ -576,6 +623,9 @@ export class BotCordClient {
|
|
|
576
623
|
to_agent_id: string;
|
|
577
624
|
amount_minor: string;
|
|
578
625
|
memo?: string;
|
|
626
|
+
reference_type?: string;
|
|
627
|
+
reference_id?: string;
|
|
628
|
+
metadata?: Record<string, unknown>;
|
|
579
629
|
idempotency_key?: string;
|
|
580
630
|
}): Promise<WalletTransaction> {
|
|
581
631
|
const resp = await this.hubFetch("/wallet/transfers", {
|
|
@@ -588,6 +638,7 @@ export class BotCordClient {
|
|
|
588
638
|
async createTopup(params: {
|
|
589
639
|
amount_minor: string;
|
|
590
640
|
channel?: string;
|
|
641
|
+
metadata?: Record<string, unknown>;
|
|
591
642
|
idempotency_key?: string;
|
|
592
643
|
}): Promise<TopupResponse> {
|
|
593
644
|
const resp = await this.hubFetch("/wallet/topups", {
|
|
@@ -599,8 +650,9 @@ export class BotCordClient {
|
|
|
599
650
|
|
|
600
651
|
async createWithdrawal(params: {
|
|
601
652
|
amount_minor: string;
|
|
653
|
+
fee_minor?: string;
|
|
602
654
|
destination_type?: string;
|
|
603
|
-
destination?: Record<string,
|
|
655
|
+
destination?: Record<string, unknown>;
|
|
604
656
|
idempotency_key?: string;
|
|
605
657
|
}): Promise<WithdrawalResponse> {
|
|
606
658
|
const resp = await this.hubFetch("/wallet/withdrawals", {
|
|
@@ -615,6 +667,13 @@ export class BotCordClient {
|
|
|
615
667
|
return (await resp.json()) as WalletTransaction;
|
|
616
668
|
}
|
|
617
669
|
|
|
670
|
+
async cancelWithdrawal(withdrawalId: string): Promise<WithdrawalResponse> {
|
|
671
|
+
const resp = await this.hubFetch(`/wallet/withdrawals/${withdrawalId}/cancel`, {
|
|
672
|
+
method: "POST",
|
|
673
|
+
});
|
|
674
|
+
return (await resp.json()) as WithdrawalResponse;
|
|
675
|
+
}
|
|
676
|
+
|
|
618
677
|
// ── Subscriptions ───────────────────────────────────────────
|
|
619
678
|
|
|
620
679
|
async createSubscriptionProduct(params: {
|
package/src/inbound.ts
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
* Inbound message dispatch — shared by websocket and polling paths.
|
|
3
3
|
* Converts BotCord messages to OpenClaw inbound format.
|
|
4
4
|
*/
|
|
5
|
-
import { readFile, readdir } from "node:fs/promises";
|
|
6
|
-
import { join, dirname } from "node:path";
|
|
7
5
|
import { getBotCordRuntime } from "./runtime.js";
|
|
8
6
|
import { resolveAccountConfig } from "./config.js";
|
|
9
7
|
import { buildSessionKey } from "./session-key.js";
|
|
@@ -245,53 +243,34 @@ type DeliveryContext = {
|
|
|
245
243
|
};
|
|
246
244
|
|
|
247
245
|
/**
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
246
|
+
* Parse a session key like "agent:pm:telegram:direct:7904063707" into a
|
|
247
|
+
* DeliveryContext. Returns undefined if the key doesn't contain a
|
|
248
|
+
* recognisable channel segment.
|
|
249
|
+
*
|
|
250
|
+
* Supported formats:
|
|
251
|
+
* agent:<agentName>:<channel>:direct:<peerId>
|
|
252
|
+
* agent:<agentName>:<channel>:group:<groupId>
|
|
253
253
|
*/
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
): Promise<DeliveryContext | undefined> {
|
|
259
|
-
const tryStore = async (path: string): Promise<DeliveryContext | undefined> => {
|
|
260
|
-
try {
|
|
261
|
-
const raw = await readFile(path, "utf-8");
|
|
262
|
-
const store: Record<string, { deliveryContext?: DeliveryContext }> =
|
|
263
|
-
JSON.parse(raw);
|
|
264
|
-
const entry = store[sessionKey];
|
|
265
|
-
if (entry?.deliveryContext?.channel && entry.deliveryContext.to) {
|
|
266
|
-
return entry.deliveryContext;
|
|
267
|
-
}
|
|
268
|
-
} catch {
|
|
269
|
-
// store may not exist yet
|
|
270
|
-
}
|
|
271
|
-
return undefined;
|
|
272
|
-
};
|
|
254
|
+
function parseSessionKeyDeliveryContext(sessionKey: string): DeliveryContext | undefined {
|
|
255
|
+
// e.g. ["agent", "pm", "telegram", "direct", "7904063707"]
|
|
256
|
+
const parts = sessionKey.split(":");
|
|
257
|
+
if (parts.length < 5 || parts[0] !== "agent") return undefined;
|
|
273
258
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
259
|
+
const KNOWN_CHANNELS = new Set([
|
|
260
|
+
"telegram", "discord", "slack", "whatsapp", "signal", "imessage",
|
|
261
|
+
]);
|
|
262
|
+
const agentName = parts[1]; // e.g. "pm"
|
|
263
|
+
const channel = parts[2]; // e.g. "telegram"
|
|
264
|
+
if (!KNOWN_CHANNELS.has(channel)) return undefined;
|
|
279
265
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
const result = await tryStore(candidate);
|
|
289
|
-
if (result) return result;
|
|
290
|
-
}
|
|
291
|
-
} catch {
|
|
292
|
-
// best-effort
|
|
293
|
-
}
|
|
294
|
-
return undefined;
|
|
266
|
+
const peerId = parts.slice(4).join(":"); // handle colons in id
|
|
267
|
+
if (!peerId) return undefined;
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
channel,
|
|
271
|
+
to: `${channel}:${peerId}`,
|
|
272
|
+
accountId: agentName,
|
|
273
|
+
};
|
|
295
274
|
}
|
|
296
275
|
|
|
297
276
|
/** Channel name → runtime send function dispatcher. */
|
|
@@ -323,10 +302,14 @@ export async function deliverNotification(
|
|
|
323
302
|
sessionKey: string,
|
|
324
303
|
text: string,
|
|
325
304
|
): Promise<void> {
|
|
326
|
-
|
|
305
|
+
// Derive delivery target directly from the session key.
|
|
306
|
+
// The session store's deliveryContext is unreliable because it gets
|
|
307
|
+
// overwritten whenever the user accesses the session from a different
|
|
308
|
+
// channel (e.g. webchat), losing the original channel/to values.
|
|
309
|
+
const delivery = parseSessionKeyDeliveryContext(sessionKey);
|
|
327
310
|
if (!delivery) {
|
|
328
311
|
console.warn(
|
|
329
|
-
`[botcord] notifySession ${sessionKey}
|
|
312
|
+
`[botcord] notifySession ${sessionKey}: cannot derive delivery target from session key — skipping notification`,
|
|
330
313
|
);
|
|
331
314
|
return;
|
|
332
315
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function formatCoinAmount(minorValue: string | number | null | undefined): string {
|
|
2
|
+
const minor = typeof minorValue === "number"
|
|
3
|
+
? minorValue
|
|
4
|
+
: Number.parseInt(minorValue ?? "0", 10);
|
|
5
|
+
|
|
6
|
+
if (!Number.isFinite(minor)) return "0.00 COIN";
|
|
7
|
+
|
|
8
|
+
return `${(minor / 100).toLocaleString("en-US", {
|
|
9
|
+
minimumFractionDigits: 2,
|
|
10
|
+
maximumFractionDigits: 2,
|
|
11
|
+
})} COIN`;
|
|
12
|
+
}
|
package/src/tools/directory.ts
CHANGED
|
@@ -41,6 +41,18 @@ export function createDirectoryTool() {
|
|
|
41
41
|
type: "string" as const,
|
|
42
42
|
description: "Topic name — for history",
|
|
43
43
|
},
|
|
44
|
+
topic_id: {
|
|
45
|
+
type: "string" as const,
|
|
46
|
+
description: "Topic ID — for history",
|
|
47
|
+
},
|
|
48
|
+
before: {
|
|
49
|
+
type: "string" as const,
|
|
50
|
+
description: "Return messages before this hub message ID — for history",
|
|
51
|
+
},
|
|
52
|
+
after: {
|
|
53
|
+
type: "string" as const,
|
|
54
|
+
description: "Return messages after this hub message ID — for history",
|
|
55
|
+
},
|
|
44
56
|
limit: {
|
|
45
57
|
type: "number" as const,
|
|
46
58
|
description: "Max results to return",
|
|
@@ -75,6 +87,9 @@ export function createDirectoryTool() {
|
|
|
75
87
|
peer: args.peer,
|
|
76
88
|
roomId: args.room_id,
|
|
77
89
|
topic: args.topic,
|
|
90
|
+
topicId: args.topic_id,
|
|
91
|
+
before: args.before,
|
|
92
|
+
after: args.after,
|
|
78
93
|
limit: args.limit || 20,
|
|
79
94
|
});
|
|
80
95
|
|