openclaw-quiubo 2.6.16 → 2.6.18
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 +126 -199
- package/dist/index.js +51 -6
- package/dist/index.js.map +2 -2
- package/dist/src/channel.d.ts.map +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,34 +10,26 @@ npm install openclaw-quiubo
|
|
|
10
10
|
|
|
11
11
|
## Setup
|
|
12
12
|
|
|
13
|
-
Quiubo is configured with:
|
|
14
|
-
|
|
15
13
|
```bash
|
|
16
14
|
openclaw channels add --channel quiubo
|
|
17
15
|
```
|
|
18
16
|
|
|
19
|
-
|
|
17
|
+
The interactive wizard will:
|
|
20
18
|
|
|
21
19
|
1. Prompt for your **SDK API Key** (starts with `qub_`)
|
|
22
20
|
2. Authenticate against the Quiubo API
|
|
23
|
-
3. List
|
|
21
|
+
3. List existing bot identities or create a new one
|
|
24
22
|
4. Save the configuration
|
|
25
23
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
You need a Quiubo SDK app with an API key. Get one from the Developer section in the Quiubo app settings.
|
|
29
|
-
|
|
30
|
-
### What you'll need
|
|
24
|
+
### Prerequisites
|
|
31
25
|
|
|
32
26
|
| Item | Where to get it |
|
|
33
27
|
|------|----------------|
|
|
34
|
-
| SDK API Key | Quiubo app
|
|
28
|
+
| SDK API Key | Quiubo app > Settings > Developer > API Keys |
|
|
35
29
|
| Bot Identity | Created during setup wizard, or pre-create in Developer console |
|
|
36
30
|
|
|
37
31
|
## Configuration
|
|
38
32
|
|
|
39
|
-
After running `channels add`, your OpenClaw config will contain:
|
|
40
|
-
|
|
41
33
|
```yaml
|
|
42
34
|
channels:
|
|
43
35
|
quiubo:
|
|
@@ -50,8 +42,6 @@ channels:
|
|
|
50
42
|
pollIntervalMs: 5000
|
|
51
43
|
```
|
|
52
44
|
|
|
53
|
-
### Config fields
|
|
54
|
-
|
|
55
45
|
| Field | Required | Default | Description |
|
|
56
46
|
|-------|----------|---------|-------------|
|
|
57
47
|
| `apiKey` | Yes | — | SDK API key (starts with `qub_`) |
|
|
@@ -62,21 +52,96 @@ channels:
|
|
|
62
52
|
|
|
63
53
|
### Multiple accounts
|
|
64
54
|
|
|
65
|
-
|
|
55
|
+
Run the setup wizard again and enter a different Account ID when prompted (e.g., `support-bot`, `sales-bot`). Each account gets its own gateway instance, cursor file, and bot config cache.
|
|
66
56
|
|
|
67
|
-
|
|
68
|
-
|
|
57
|
+
## Architecture
|
|
58
|
+
|
|
59
|
+
### Message delivery
|
|
60
|
+
|
|
61
|
+
The plugin uses a dual-mode real-time gateway:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
Pusher WebSocket (primary) ─┐
|
|
65
|
+
├──→ dedup ──→ onMessage() ──→ OpenClaw agent pipeline
|
|
66
|
+
Polling fallback (safety) ─┘
|
|
69
67
|
```
|
|
70
68
|
|
|
71
|
-
|
|
69
|
+
- **Pusher WebSocket**: Real-time push delivery via `private-user-{botIdentityId}` channel
|
|
70
|
+
- **Polling**: Runs concurrently as a safety net (catches messages Pusher may silently miss)
|
|
71
|
+
- **Dedup**: Both paths pass through cursor + in-memory dedup — no double-processing
|
|
72
|
+
|
|
73
|
+
Outbound messages are sent via the Quiubo SDK REST API.
|
|
74
|
+
|
|
75
|
+
### Replay prevention
|
|
76
|
+
|
|
77
|
+
Four layers prevent old messages from being reprocessed on restart:
|
|
78
|
+
|
|
79
|
+
1. **Cursor persistence**: Per-account cursor files at `~/.openclaw/cron/quiubo-cursors-{accountId}.json` survive restarts
|
|
80
|
+
2. **seekToLatest()**: On cold start (no cursor for a group), paginates to the latest message without processing — only subsequent messages are treated as new
|
|
81
|
+
3. **FIFO dedup set**: In-memory `dispatched` set (capped at 500 entries with FIFO eviction) catches duplicates across Pusher and poll paths within a session
|
|
82
|
+
4. **Dedup guard on all delivery paths**: Pusher plaintext, Pusher E2EE, and poll paths all check the dedup set before dispatching
|
|
83
|
+
|
|
84
|
+
### Resilience
|
|
85
|
+
|
|
86
|
+
- **Exponential backoff**: Poll failures increase delay (5s > 10s > 20s > 40s > 60s cap), resets on success
|
|
87
|
+
- **Poll concurrency limiter**: Groups polled in batches of 5 via `Promise.allSettled` to avoid API burst
|
|
88
|
+
- **Gateway lifecycle safety**: `startAccount()` stops any existing gateway before creating a new one (prevents orphaned timers on config reload)
|
|
89
|
+
- **Pusher timeout management**: 15s connection timeout is properly cancelled on stop/restart
|
|
90
|
+
|
|
91
|
+
### End-to-end encryption (E2EE)
|
|
92
|
+
|
|
93
|
+
The plugin supports Quiubo's E2EE protocol for groups that require it:
|
|
94
|
+
|
|
95
|
+
- **Key generation**: Deterministic Ed25519 + X25519 keypairs derived from a 32-byte seed (persisted in config)
|
|
96
|
+
- **Auto-enrollment**: On first startup, generates keypair and enrolls via challenge-response (`requestKeyChallenge` > `signChallenge` > `verifyKeyChallenge`)
|
|
97
|
+
- **Inbound decryption**: GroupEnvelopeV2 messages decrypted using XChaCha20-Poly1305 with epoch keys
|
|
98
|
+
- **Outbound encryption**: Plaintext encrypted before sending when E2EE is granted for the group
|
|
99
|
+
- **Epoch key management**: Dual-layer cache (active key + historical keys per epoch) with 30-min TTL and automatic refresh
|
|
100
|
+
- **Pusher events**: Handles `e2ee-granted`, `e2ee-revoked`, `epoch-rotated` for real-time key lifecycle
|
|
101
|
+
|
|
102
|
+
### Directory agents
|
|
103
|
+
|
|
104
|
+
Agents can be listed in Quiubo's public agent directory, allowing any group to add them:
|
|
105
|
+
|
|
106
|
+
- **Auto-registration**: If no agent record exists for the bot identity, one is created automatically on startup
|
|
107
|
+
- **Directory groups**: Discovered via `listAgentGroups()` (refreshed every 60s) and polled alongside partner groups
|
|
108
|
+
- **Mention filtering**: In directory groups, polled messages are only processed if they contain an `@mention` of the agent (Pusher path is pre-filtered server-side by `triggerMode`)
|
|
109
|
+
- **Scope enforcement**: Outbound messages check `grantedScopes` — agent won't attempt to send if `send_messages` isn't granted
|
|
110
|
+
- **Real-time membership**: Pusher events `agent:group-added` and `agent:group-removed` update the cache immediately
|
|
111
|
+
|
|
112
|
+
### Bot-enabled gating
|
|
113
|
+
|
|
114
|
+
Every incoming message passes through a multi-step gating pipeline:
|
|
115
|
+
|
|
116
|
+
1. **Self-echo skip** — Bot's own messages ignored
|
|
117
|
+
2. **Agent channel bypass** — `agent_channel` groups always process (1:1 bot channels)
|
|
118
|
+
3. **Cache miss resolution** — Unknown groups fetched via API with 5s timeout (fail-closed)
|
|
119
|
+
4. **Bot-enabled check** — `settings.bot.enabled` must be `true`
|
|
120
|
+
5. **Security mode check** — `PLAINTEXT_SDK` or E2EE-granted groups only
|
|
121
|
+
6. **Owner takeover** — Owner sends a message > bot suppressed for `suppressionMinutes`
|
|
122
|
+
7. **Suppression check** — Skipped while suppressed (stale entries evicted after 1 hour)
|
|
123
|
+
|
|
124
|
+
### Inbound routing
|
|
125
|
+
|
|
126
|
+
Messages are routed through OpenClaw's agent pipeline:
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
finalizeInboundContext() → dispatchReplyWithBufferedBlockDispatcher()
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Session keys: `quiubo:{groupId}` for the main agent, `agent:{agentId}:quiubo:{groupId}` for non-main agents.
|
|
133
|
+
|
|
134
|
+
### Auto-provisioning
|
|
135
|
+
|
|
136
|
+
On first gateway startup, if the bot has no groups, the plugin automatically creates a welcome `agent_channel` group, adds the bot as a member, and sends a welcome message.
|
|
72
137
|
|
|
73
138
|
## Multi-Agent Setup
|
|
74
139
|
|
|
75
|
-
Run multiple AI agents on a single OpenClaw gateway — each with their own Quiubo chat, workspace, model, and cron jobs.
|
|
140
|
+
Run multiple AI agents on a single OpenClaw gateway — each with their own Quiubo chat, workspace, model, and cron jobs.
|
|
76
141
|
|
|
77
142
|
### Step 1: Add the agent
|
|
78
143
|
|
|
79
|
-
In `~/.openclaw/openclaw.json
|
|
144
|
+
In `~/.openclaw/openclaw.json`:
|
|
80
145
|
|
|
81
146
|
```json
|
|
82
147
|
{
|
|
@@ -93,11 +158,9 @@ In `~/.openclaw/openclaw.json`, add a new agent to the list:
|
|
|
93
158
|
}
|
|
94
159
|
```
|
|
95
160
|
|
|
96
|
-
Each agent can use a different model — useful for cost control when running autonomous agents.
|
|
97
|
-
|
|
98
161
|
### Step 2: Add a Quiubo account for the agent
|
|
99
162
|
|
|
100
|
-
|
|
163
|
+
Each account needs its own bot identity (can share the same API key):
|
|
101
164
|
|
|
102
165
|
```json
|
|
103
166
|
{
|
|
@@ -118,12 +181,8 @@ Add a second account under the Quiubo plugin config. You can reuse the same SDK
|
|
|
118
181
|
}
|
|
119
182
|
```
|
|
120
183
|
|
|
121
|
-
Create additional bot identities through the Quiubo developer console or the setup wizard.
|
|
122
|
-
|
|
123
184
|
### Step 3: Bind accounts to agents
|
|
124
185
|
|
|
125
|
-
Bindings tell OpenClaw which Quiubo account routes to which agent:
|
|
126
|
-
|
|
127
186
|
```json
|
|
128
187
|
{
|
|
129
188
|
"bindings": [
|
|
@@ -139,7 +198,7 @@ Bindings tell OpenClaw which Quiubo account routes to which agent:
|
|
|
139
198
|
}
|
|
140
199
|
```
|
|
141
200
|
|
|
142
|
-
Without bindings, all messages route to the first agent.
|
|
201
|
+
Without bindings, all messages route to the first agent.
|
|
143
202
|
|
|
144
203
|
### Step 4: Create the agent's workspace
|
|
145
204
|
|
|
@@ -147,31 +206,14 @@ Without bindings, all messages route to the first agent. This is the most common
|
|
|
147
206
|
mkdir -p ~/.openclaw/workspace-project-manager/memory
|
|
148
207
|
```
|
|
149
208
|
|
|
150
|
-
Seed it with
|
|
151
|
-
|
|
152
|
-
- `SOUL.md` — personality and purpose
|
|
153
|
-
- `AGENTS.md` — operating instructions
|
|
154
|
-
- `USER.md` — info about the human
|
|
155
|
-
- `IDENTITY.md` — name, emoji, avatar
|
|
156
|
-
- `MEMORY.md` — long-term memory (starts empty)
|
|
157
|
-
|
|
158
|
-
### Step 5: Copy auth profiles (optional)
|
|
159
|
-
|
|
160
|
-
If the new agent needs access to the same services (web search, APIs, etc.):
|
|
161
|
-
|
|
162
|
-
```bash
|
|
163
|
-
mkdir -p ~/.openclaw/agents/project-manager/agent/
|
|
164
|
-
cp ~/.openclaw/agents/main/agent/auth-profiles.json ~/.openclaw/agents/project-manager/agent/
|
|
165
|
-
```
|
|
209
|
+
Seed it with: `SOUL.md`, `AGENTS.md`, `USER.md`, `IDENTITY.md`, `MEMORY.md`.
|
|
166
210
|
|
|
167
|
-
### Step
|
|
211
|
+
### Step 5: Restart
|
|
168
212
|
|
|
169
213
|
```bash
|
|
170
214
|
openclaw gateway restart
|
|
171
215
|
```
|
|
172
216
|
|
|
173
|
-
The new agent is live. Messages to the PM bot identity route to `project-manager`, use Sonnet, and read from its own workspace.
|
|
174
|
-
|
|
175
217
|
### How routing works
|
|
176
218
|
|
|
177
219
|
```
|
|
@@ -179,72 +221,21 @@ Quiubo Chat (You + Main Bot) → account: default → binding → agent: main
|
|
|
179
221
|
Quiubo Chat (You + PM Bot) → account: pm → binding → agent: project-manager
|
|
180
222
|
```
|
|
181
223
|
|
|
182
|
-
- **Messages
|
|
183
|
-
- **Cron jobs
|
|
184
|
-
- **Sessions
|
|
185
|
-
- **Workspaces
|
|
186
|
-
|
|
187
|
-
### Full config example
|
|
188
|
-
|
|
189
|
-
```json
|
|
190
|
-
{
|
|
191
|
-
"agents": {
|
|
192
|
-
"list": [
|
|
193
|
-
{ "id": "main" },
|
|
194
|
-
{
|
|
195
|
-
"id": "project-manager",
|
|
196
|
-
"model": "anthropic/claude-sonnet-4-5",
|
|
197
|
-
"workspace": "~/.openclaw/workspace-project-manager"
|
|
198
|
-
}
|
|
199
|
-
]
|
|
200
|
-
},
|
|
201
|
-
"bindings": [
|
|
202
|
-
{
|
|
203
|
-
"match": { "channel": "quiubo", "accountId": "default" },
|
|
204
|
-
"agentId": "main"
|
|
205
|
-
},
|
|
206
|
-
{
|
|
207
|
-
"match": { "channel": "quiubo", "accountId": "pm" },
|
|
208
|
-
"agentId": "project-manager"
|
|
209
|
-
}
|
|
210
|
-
],
|
|
211
|
-
"plugins": {
|
|
212
|
-
"quiubo": {
|
|
213
|
-
"accounts": {
|
|
214
|
-
"default": {
|
|
215
|
-
"sdkApiKey": "qub_...",
|
|
216
|
-
"botIdentityId": "<main-bot-uuid>"
|
|
217
|
-
},
|
|
218
|
-
"pm": {
|
|
219
|
-
"sdkApiKey": "qub_...",
|
|
220
|
-
"botIdentityId": "<pm-bot-uuid>"
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
### Tips
|
|
229
|
-
|
|
230
|
-
- **Cost control:** Use cheaper models for autonomous agents that run frequently
|
|
231
|
-
- **Workspace isolation:** Agents should never edit each other's files — enforce this in their `AGENTS.md`
|
|
232
|
-
- **Permissions:** Add rules about what agents can/can't do (e.g., "ask before spending money")
|
|
233
|
-
- **Scaling:** Same pattern for any number of agents — new ID, new bot identity, new binding, new workspace
|
|
224
|
+
- **Messages**: AccountId resolves the agent via bindings
|
|
225
|
+
- **Cron jobs**: Filtered by agentId — each agent's chat shows only its own
|
|
226
|
+
- **Sessions**: Isolated session namespaces per agent, no cross-talk
|
|
227
|
+
- **Workspaces**: Fully isolated — agents can't see each other's files
|
|
228
|
+
- **Cursors**: Each account has its own cursor file — no clobbering
|
|
234
229
|
|
|
235
230
|
## Markdown Attachments
|
|
236
231
|
|
|
237
|
-
Agents can send `.md` file attachments alongside
|
|
238
|
-
|
|
239
|
-
### How it works
|
|
232
|
+
Agents can send `.md` file attachments alongside messages. Attachments appear as tappable cards in the Quiubo app with a full markdown viewer.
|
|
240
233
|
|
|
241
|
-
OpenClaw
|
|
234
|
+
OpenClaw's `MEDIA:` token protocol is used: when an agent writes a file and includes `MEDIA: /path/to/file.md` in its response, the plugin reads the file and sends it as a structured attachment.
|
|
242
235
|
|
|
243
|
-
**Supported:** `.md` files only, max 1MB each
|
|
236
|
+
**Supported:** `.md` files only, max 1MB each. Source tracking distinguishes `agent` vs `subagent` attachments.
|
|
244
237
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
Add this to your agent's `AGENTS.md` so it knows how to send attachments:
|
|
238
|
+
Add this to your agent's `AGENTS.md`:
|
|
248
239
|
|
|
249
240
|
```markdown
|
|
250
241
|
## Sending File Attachments
|
|
@@ -262,133 +253,69 @@ The file will be delivered as a tappable attachment card in the chat.
|
|
|
262
253
|
Only `.md` files are supported. Files over 1MB are skipped.
|
|
263
254
|
```
|
|
264
255
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
Cron jobs with `delivery.channel` already target the correct group. To send attachments from a cron job, the cron agent just needs to follow the same `MEDIA:` token protocol — write the `.md` file, then include the `MEDIA:` line in the output.
|
|
268
|
-
|
|
269
|
-
### What the user sees
|
|
256
|
+
## Operational Notes
|
|
270
257
|
|
|
271
|
-
|
|
272
|
-
- **On tap:** Full-screen markdown viewer with rendered content
|
|
273
|
-
- **In group details:** A "Documents" tab listing all attachments in the group, filterable by source
|
|
258
|
+
### Cursor files
|
|
274
259
|
|
|
275
|
-
|
|
260
|
+
Cursors are persisted at:
|
|
276
261
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
The plugin uses a dual-mode gateway for receiving messages:
|
|
280
|
-
|
|
281
|
-
- **Primary:** Pusher WebSocket — real-time push delivery
|
|
282
|
-
- **Fallback:** Polling — automatic fallback when Pusher is unavailable
|
|
283
|
-
|
|
284
|
-
Outbound messages are sent via the Quiubo SDK REST API.
|
|
285
|
-
|
|
286
|
-
### Auto-provisioning
|
|
287
|
-
|
|
288
|
-
On first gateway startup, if the bot has no groups, the plugin automatically:
|
|
289
|
-
|
|
290
|
-
1. Creates a welcome group (type: `agent_channel`)
|
|
291
|
-
2. Adds the bot as a member
|
|
292
|
-
3. Sends a welcome message
|
|
293
|
-
|
|
294
|
-
This means users can start chatting immediately after setup — no manual group creation needed.
|
|
295
|
-
|
|
296
|
-
### Inbound routing
|
|
297
|
-
|
|
298
|
-
Messages are routed through OpenClaw's agent pipeline using the standard `finalizeInboundContext()` → `dispatchReplyWithBufferedBlockDispatcher()` pattern (same as openclaw-mqtt).
|
|
299
|
-
|
|
300
|
-
Session keys use the format `quiubo:<groupId>`. The `AccountId` field in the context payload tells OpenClaw which agent to route to via the bindings config.
|
|
301
|
-
|
|
302
|
-
## Bot-Enabled Gating
|
|
303
|
-
|
|
304
|
-
The extension implements a multi-step gating pipeline to determine whether to respond to a message. This runs for every incoming message (both Pusher and polling paths).
|
|
305
|
-
|
|
306
|
-
### Gating Pipeline
|
|
307
|
-
|
|
308
|
-
1. **Self-echo skip** — Messages from the bot's own identity are ignored
|
|
309
|
-
2. **Agent channel bypass** — `agent_channel` groups always process messages (1:1 bot channels)
|
|
310
|
-
3. **Security mode check** — Only `PLAINTEXT_SDK` groups are processed; E2EE groups are skipped
|
|
311
|
-
4. **Bot-enabled check** — `settings.bot.enabled` must be `true` (with cache-miss fallback)
|
|
312
|
-
5. **Owner takeover** — If the sender is the group owner, suppress bot for `suppressionMinutes`
|
|
313
|
-
6. **Suppression check** — If currently suppressed, skip the message
|
|
314
|
-
|
|
315
|
-
### Bot Config Cache
|
|
316
|
-
|
|
317
|
-
The extension maintains an in-memory cache of bot configuration per group:
|
|
318
|
-
|
|
319
|
-
```typescript
|
|
320
|
-
interface BotConfig {
|
|
321
|
-
enabled: boolean;
|
|
322
|
-
suppressionMinutes: number;
|
|
323
|
-
ownerIdentityId: string;
|
|
324
|
-
groupType: string; // 'standard' | 'agent_channel'
|
|
325
|
-
}
|
|
262
|
+
```
|
|
263
|
+
~/.openclaw/cron/quiubo-cursors-{accountId}.json
|
|
326
264
|
```
|
|
327
265
|
|
|
328
|
-
-
|
|
329
|
-
- **Updated on cache miss** via `onCacheMiss` callback (5s timeout, fail-closed)
|
|
330
|
-
- **Hydrated from polls** when Pusher is unavailable
|
|
331
|
-
|
|
332
|
-
### Suppression Map
|
|
333
|
-
|
|
334
|
-
Owner takeover suppression uses a `Map<groupId, suppressedUntil>`:
|
|
335
|
-
- When the group owner sends a message, `suppressedUntil` is set to `now + suppressionMinutes * 60_000`
|
|
336
|
-
- Entries older than 1 hour are periodically evicted via `clearStaleSuppression()`
|
|
266
|
+
Each account writes to its own file to avoid clobbering in multi-account setups. Safe to delete — the gateway will `seekToLatest()` on next start (no replay).
|
|
337
267
|
|
|
338
|
-
###
|
|
268
|
+
### Heartbeat
|
|
339
269
|
|
|
340
|
-
|
|
341
|
-
- Extension calls `onCacheMiss(groupId)` which fetches group details + members from the API
|
|
342
|
-
- 5-second timeout — if the fetch fails or times out, the message is skipped (fail-closed)
|
|
343
|
-
- This is safer than fail-open: better to miss a message than respond incorrectly
|
|
270
|
+
If an agent is registered, the gateway sends a heartbeat every 60s to report `connectionStatus: online`. Heartbeat stops on Pusher disconnect and resumes on reconnect.
|
|
344
271
|
|
|
345
|
-
|
|
272
|
+
### Typing indicators
|
|
346
273
|
|
|
347
|
-
|
|
274
|
+
A typing indicator is sent immediately when processing begins, then repeated every 4s until the response is delivered.
|
|
348
275
|
|
|
349
|
-
|
|
276
|
+
## Managing Accounts
|
|
350
277
|
|
|
351
278
|
```bash
|
|
352
|
-
openclaw channels disable --channel quiubo
|
|
279
|
+
openclaw channels disable --channel quiubo # Disable
|
|
280
|
+
openclaw channels add --channel quiubo # Re-configure
|
|
353
281
|
```
|
|
354
282
|
|
|
355
|
-
### Remove an account
|
|
356
|
-
|
|
357
|
-
Delete the account entry from your OpenClaw config, or use the config editor.
|
|
358
|
-
|
|
359
283
|
## Development
|
|
360
284
|
|
|
361
285
|
```bash
|
|
362
286
|
npm run build # Compile TypeScript
|
|
363
287
|
npm run typecheck # Type-check without emitting
|
|
288
|
+
npm run release # Bump version + publish
|
|
364
289
|
```
|
|
365
290
|
|
|
366
291
|
### Project structure
|
|
367
292
|
|
|
368
293
|
```
|
|
294
|
+
index.ts # Entry point + exports
|
|
369
295
|
src/
|
|
370
|
-
api.ts
|
|
371
|
-
channel.ts
|
|
372
|
-
realtime-gateway.ts # Pusher WebSocket + polling fallback
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
runtime.ts
|
|
376
|
-
types.ts
|
|
296
|
+
api.ts # REST API client (QuiuboApiClient)
|
|
297
|
+
channel.ts # Channel plugin (config, setup, onboarding, outbound, gateway)
|
|
298
|
+
realtime-gateway.ts # Pusher WebSocket + polling fallback + replay prevention
|
|
299
|
+
crypto.ts # E2EE: key generation, envelope decrypt/encrypt, Ed25519 signing
|
|
300
|
+
key-manager.ts # Dual-layer epoch key cache with auto-fetch
|
|
301
|
+
runtime.ts # Plugin runtime singleton
|
|
302
|
+
types.ts # TypeScript type definitions
|
|
377
303
|
```
|
|
378
304
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
The `QuiuboApiClient` wraps these SDK endpoints:
|
|
305
|
+
### API coverage
|
|
382
306
|
|
|
383
307
|
| Category | Methods |
|
|
384
308
|
|----------|---------|
|
|
385
309
|
| Auth | `authenticate()` |
|
|
386
|
-
| Messages | `sendMessage()`, `listMessages()` |
|
|
310
|
+
| Messages | `sendMessage()`, `listMessages()`, `sendTypingIndicator()` |
|
|
387
311
|
| Groups | `createGroup()`, `listGroups()`, `getGroup()`, `addMembers()`, `removeMembers()`, `listGroupMembers()`, `updateGroupSettings()` |
|
|
388
312
|
| Identities | `createIdentity()`, `listIdentities()`, `deleteIdentity()` |
|
|
389
313
|
| Join Tokens | `createJoinToken()`, `listJoinTokens()`, `deleteJoinToken()` |
|
|
390
|
-
|
|
|
314
|
+
| Agents | `listAgents()`, `createAgent()`, `updateAgent()`, `sendHeartbeat()`, `listAgentGroups()`, `getGroupAgent()` |
|
|
315
|
+
| E2EE | `requestKeyChallenge()`, `verifyKeyChallenge()`, `getEpochKey()`, `getEpochKeys()` |
|
|
391
316
|
| Pusher | `pusherAuth()` |
|
|
317
|
+
| OpenClaw | `sendOpenclawResponse()` |
|
|
318
|
+
| Webhooks | `configureWebhook()` |
|
|
392
319
|
|
|
393
320
|
## License
|
|
394
321
|
|
package/dist/index.js
CHANGED
|
@@ -13289,6 +13289,7 @@ async function readMdAttachments(mediaUrls, source, log, accountId) {
|
|
|
13289
13289
|
var gateways = /* @__PURE__ */ new Map();
|
|
13290
13290
|
var clients = /* @__PURE__ */ new Map();
|
|
13291
13291
|
var accounts = /* @__PURE__ */ new Map();
|
|
13292
|
+
var loggers = /* @__PURE__ */ new Map();
|
|
13292
13293
|
var CHANNEL_ID = "quiubo";
|
|
13293
13294
|
var DEFAULT_API_URL = "https://api.quiubo.io";
|
|
13294
13295
|
var DEFAULT_ACCOUNT_ID = "default";
|
|
@@ -13585,11 +13586,13 @@ var quiuboPlugin = {
|
|
|
13585
13586
|
async sendText(ctx) {
|
|
13586
13587
|
const { text, cfg } = ctx;
|
|
13587
13588
|
const accountId = ctx.accountId ?? DEFAULT_ACCOUNT_ID;
|
|
13589
|
+
const log = loggers.get(accountId);
|
|
13588
13590
|
let client = clients.get(accountId);
|
|
13589
13591
|
let account = accounts.get(accountId);
|
|
13590
13592
|
if (!client || !account) {
|
|
13591
13593
|
const acct = getChannelConfig(cfg)?.accounts?.[accountId];
|
|
13592
13594
|
if (!acct?.apiKey) {
|
|
13595
|
+
log?.warn?.(`[${accountId}] [outbound:sendText] no account config found`);
|
|
13593
13596
|
return { ok: false, error: "No account config found" };
|
|
13594
13597
|
}
|
|
13595
13598
|
const apiUrl = acct.apiUrl ?? DEFAULT_API_URL;
|
|
@@ -13598,8 +13601,13 @@ var quiuboPlugin = {
|
|
|
13598
13601
|
clients.set(accountId, client);
|
|
13599
13602
|
accounts.set(accountId, account);
|
|
13600
13603
|
}
|
|
13601
|
-
|
|
13604
|
+
let groupId = resolveOutboundGroupId(ctx);
|
|
13602
13605
|
if (!groupId) {
|
|
13606
|
+
groupId = await resolveAnnounceGroupId(accountId, log);
|
|
13607
|
+
}
|
|
13608
|
+
log?.info?.(`[${accountId}] [outbound:sendText] groupId=${groupId}, text=${text?.length ?? 0} chars, ctx.to=${ctx.to}, ConversationLabel=${ctx.target?.raw?.ConversationLabel ?? ctx.ConversationLabel}, SessionKey=${ctx.target?.raw?.SessionKey ?? ctx.SessionKey}`);
|
|
13609
|
+
if (!groupId) {
|
|
13610
|
+
log?.error?.(`[${accountId}] [outbound:sendText] no groupId \u2014 ctx keys=${Object.keys(ctx).join(",")}, target=${ctx.target ? JSON.stringify(Object.keys(ctx.target)) : "none"}, raw=${ctx.target?.raw ? JSON.stringify(Object.keys(ctx.target.raw)) : "none"}`);
|
|
13603
13611
|
return { ok: false, error: "No groupId in outbound context" };
|
|
13604
13612
|
}
|
|
13605
13613
|
const senderId = account.botIdentityId;
|
|
@@ -13607,14 +13615,16 @@ var quiuboPlugin = {
|
|
|
13607
13615
|
return { ok: false, error: "No botIdentityId configured" };
|
|
13608
13616
|
}
|
|
13609
13617
|
try {
|
|
13610
|
-
await client.sendMessage(groupId, {
|
|
13618
|
+
const apiResp = await client.sendMessage(groupId, {
|
|
13611
13619
|
senderIdentityId: senderId,
|
|
13612
13620
|
plaintext: text,
|
|
13613
13621
|
metadata: { format: "markdown" }
|
|
13614
13622
|
});
|
|
13623
|
+
log?.info?.(`[${accountId}] [outbound:sendText] sent to group ${groupId} (realtime=${apiResp?.realtimeDelivered})`);
|
|
13615
13624
|
return { ok: true };
|
|
13616
13625
|
} catch (error) {
|
|
13617
13626
|
const msg = error instanceof Error ? error.message : String(error);
|
|
13627
|
+
log?.error?.(`[${accountId}] [outbound:sendText] failed: ${msg}`);
|
|
13618
13628
|
return { ok: false, error: msg };
|
|
13619
13629
|
}
|
|
13620
13630
|
},
|
|
@@ -13624,14 +13634,16 @@ var quiuboPlugin = {
|
|
|
13624
13634
|
const urls = [];
|
|
13625
13635
|
if (ctx.mediaUrl) urls.push(ctx.mediaUrl);
|
|
13626
13636
|
if (Array.isArray(ctx.mediaUrls)) urls.push(...ctx.mediaUrls);
|
|
13627
|
-
const mdAttachments = await readMdAttachments(urls, "agent", void 0, "sendMedia");
|
|
13628
|
-
const plaintext = text || (mdAttachments.length === 0 ? "[Media attachment]" : "");
|
|
13629
13637
|
const accountId = ctx.accountId ?? DEFAULT_ACCOUNT_ID;
|
|
13638
|
+
const log = loggers.get(accountId);
|
|
13639
|
+
const mdAttachments = await readMdAttachments(urls, "agent", log, accountId);
|
|
13640
|
+
const plaintext = text || (mdAttachments.length === 0 ? "[Media attachment]" : "");
|
|
13630
13641
|
let client = clients.get(accountId);
|
|
13631
13642
|
let account = accounts.get(accountId);
|
|
13632
13643
|
if (!client || !account) {
|
|
13633
13644
|
const acct = getChannelConfig(ctx.cfg)?.accounts?.[accountId];
|
|
13634
13645
|
if (!acct?.apiKey) {
|
|
13646
|
+
log?.warn?.(`[${accountId}] [outbound:sendMedia] no account config found`);
|
|
13635
13647
|
return { ok: false, error: "No account config found" };
|
|
13636
13648
|
}
|
|
13637
13649
|
const apiUrl = acct.apiUrl ?? DEFAULT_API_URL;
|
|
@@ -13640,8 +13652,13 @@ var quiuboPlugin = {
|
|
|
13640
13652
|
clients.set(accountId, client);
|
|
13641
13653
|
accounts.set(accountId, account);
|
|
13642
13654
|
}
|
|
13643
|
-
|
|
13655
|
+
let groupId = resolveOutboundGroupId(ctx);
|
|
13656
|
+
if (!groupId) {
|
|
13657
|
+
groupId = await resolveAnnounceGroupId(accountId, log);
|
|
13658
|
+
}
|
|
13659
|
+
log?.info?.(`[${accountId}] [outbound:sendMedia] groupId=${groupId}, urls=${urls.length}, attachments=${mdAttachments.length}`);
|
|
13644
13660
|
if (!groupId) {
|
|
13661
|
+
log?.error?.(`[${accountId}] [outbound:sendMedia] no groupId \u2014 ctx keys=${Object.keys(ctx).join(",")}`);
|
|
13645
13662
|
return { ok: false, error: "No groupId in outbound context" };
|
|
13646
13663
|
}
|
|
13647
13664
|
const senderId = account.botIdentityId;
|
|
@@ -13649,15 +13666,17 @@ var quiuboPlugin = {
|
|
|
13649
13666
|
return { ok: false, error: "No botIdentityId configured" };
|
|
13650
13667
|
}
|
|
13651
13668
|
try {
|
|
13652
|
-
await client.sendMessage(groupId, {
|
|
13669
|
+
const apiResp = await client.sendMessage(groupId, {
|
|
13653
13670
|
senderIdentityId: senderId,
|
|
13654
13671
|
plaintext,
|
|
13655
13672
|
metadata: { format: "markdown" },
|
|
13656
13673
|
...mdAttachments.length > 0 ? { attachments: mdAttachments } : {}
|
|
13657
13674
|
});
|
|
13675
|
+
log?.info?.(`[${accountId}] [outbound:sendMedia] sent to group ${groupId} (realtime=${apiResp?.realtimeDelivered})`);
|
|
13658
13676
|
return { ok: true };
|
|
13659
13677
|
} catch (error) {
|
|
13660
13678
|
const msg = error instanceof Error ? error.message : String(error);
|
|
13679
|
+
log?.error?.(`[${accountId}] [outbound:sendMedia] failed: ${msg}`);
|
|
13661
13680
|
return { ok: false, error: msg };
|
|
13662
13681
|
}
|
|
13663
13682
|
}
|
|
@@ -13693,6 +13712,7 @@ var quiuboPlugin = {
|
|
|
13693
13712
|
const client = new QuiuboApiClient(apiUrl, apiKey);
|
|
13694
13713
|
clients.set(accountId, client);
|
|
13695
13714
|
accounts.set(accountId, { ...quiuboConfig, accountId });
|
|
13715
|
+
loggers.set(accountId, log);
|
|
13696
13716
|
let pusherConfig;
|
|
13697
13717
|
let auth;
|
|
13698
13718
|
try {
|
|
@@ -14024,6 +14044,7 @@ var quiuboPlugin = {
|
|
|
14024
14044
|
gateways.delete(accountId);
|
|
14025
14045
|
clients.delete(accountId);
|
|
14026
14046
|
accounts.delete(accountId);
|
|
14047
|
+
loggers.delete(accountId);
|
|
14027
14048
|
resolve();
|
|
14028
14049
|
};
|
|
14029
14050
|
if (abortSignal) {
|
|
@@ -14049,6 +14070,30 @@ function resolveOutboundGroupId(ctx) {
|
|
|
14049
14070
|
if (targetGroupId) return targetGroupId;
|
|
14050
14071
|
return void 0;
|
|
14051
14072
|
}
|
|
14073
|
+
async function resolveAnnounceGroupId(accountId, log) {
|
|
14074
|
+
try {
|
|
14075
|
+
const { readFile: readFile3 } = await import("node:fs/promises");
|
|
14076
|
+
const { join: join2 } = await import("node:path");
|
|
14077
|
+
const homeDir = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
14078
|
+
const cronPath = join2(homeDir, ".openclaw", "cron", "jobs.json");
|
|
14079
|
+
const raw = await readFile3(cronPath, "utf-8");
|
|
14080
|
+
const parsed = JSON.parse(raw);
|
|
14081
|
+
const jobs = parsed?.jobs ?? [];
|
|
14082
|
+
for (const job of jobs) {
|
|
14083
|
+
if (job.enabled === false) continue;
|
|
14084
|
+
const delivery = job.delivery;
|
|
14085
|
+
if (!delivery) continue;
|
|
14086
|
+
if (delivery.channel !== "quiubo") continue;
|
|
14087
|
+
if (delivery.to) {
|
|
14088
|
+
log?.info?.(`[${accountId}] [resolveAnnounceGroupId] found cron job ${job.id} \u2192 delivery.to=${delivery.to}`);
|
|
14089
|
+
return delivery.to;
|
|
14090
|
+
}
|
|
14091
|
+
}
|
|
14092
|
+
} catch (err) {
|
|
14093
|
+
log?.warn?.(`[${accountId}] [resolveAnnounceGroupId] failed to read cron jobs: ${err}`);
|
|
14094
|
+
}
|
|
14095
|
+
return void 0;
|
|
14096
|
+
}
|
|
14052
14097
|
async function getActivityData(runtime2, log, agentId) {
|
|
14053
14098
|
try {
|
|
14054
14099
|
const { readFile: readFile3 } = await import("node:fs/promises");
|