openclaw-quiubo 2.6.28 → 2.6.29
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 +50 -20
- package/dist/index.js +120 -37
- package/dist/index.js.map +3 -3
- package/dist/src/api.d.ts +21 -2
- package/dist/src/api.d.ts.map +1 -1
- package/dist/src/channel.d.ts.map +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -145,28 +145,25 @@ On first gateway startup, if the bot has no groups, the plugin automatically cre
|
|
|
145
145
|
|
|
146
146
|
Run multiple AI agents on a single OpenClaw gateway — each with their own Quiubo chat, workspace, model, and cron jobs.
|
|
147
147
|
|
|
148
|
-
|
|
148
|
+
Each agent needs two things: a **bot identity** in Quiubo (so it has its own chat presence) and an **agent entry** in OpenClaw (so it has its own workspace and model). They can share the same SDK API key.
|
|
149
149
|
|
|
150
|
-
|
|
150
|
+
### Step 1: Create a bot identity in Quiubo
|
|
151
151
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
{ "id": "main" },
|
|
157
|
-
{
|
|
158
|
-
"id": "project-manager",
|
|
159
|
-
"model": "anthropic/claude-sonnet-4-5",
|
|
160
|
-
"workspace": "~/.openclaw/workspace-project-manager"
|
|
161
|
-
}
|
|
162
|
-
]
|
|
163
|
-
}
|
|
164
|
-
}
|
|
152
|
+
Each agent needs its own bot identity. You create one by running the setup wizard with a new account ID:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
openclaw channels add --channel quiubo
|
|
165
156
|
```
|
|
166
157
|
|
|
167
|
-
|
|
158
|
+
The wizard will:
|
|
159
|
+
|
|
160
|
+
1. Ask for an **Account ID** — enter a short name for this agent (e.g. `pm`, `support`, `research`). Your first/default agent uses `default`.
|
|
161
|
+
2. Ask for your **SDK API Key** — you can reuse the same `qub_...` key across all agents.
|
|
162
|
+
3. **Create or select a bot identity** — choose "Create new bot identity" to give this agent its own username and display name in Quiubo.
|
|
168
163
|
|
|
169
|
-
|
|
164
|
+
> **Where to get an SDK API Key:** In the Quiubo app, go to **Settings > Developer > API Keys**. One key works for all agents under the same app.
|
|
165
|
+
|
|
166
|
+
After the wizard completes, the new account is saved to `~/.openclaw/openclaw.json` automatically:
|
|
170
167
|
|
|
171
168
|
```json
|
|
172
169
|
{
|
|
@@ -174,11 +171,11 @@ Each account needs its own bot identity (can share the same API key):
|
|
|
174
171
|
"quiubo": {
|
|
175
172
|
"accounts": {
|
|
176
173
|
"default": {
|
|
177
|
-
"
|
|
174
|
+
"apiKey": "qub_...",
|
|
178
175
|
"botIdentityId": "<main-bot-uuid>"
|
|
179
176
|
},
|
|
180
177
|
"pm": {
|
|
181
|
-
"
|
|
178
|
+
"apiKey": "qub_...",
|
|
182
179
|
"botIdentityId": "<pm-bot-uuid>"
|
|
183
180
|
}
|
|
184
181
|
}
|
|
@@ -187,7 +184,30 @@ Each account needs its own bot identity (can share the same API key):
|
|
|
187
184
|
}
|
|
188
185
|
```
|
|
189
186
|
|
|
190
|
-
### Step
|
|
187
|
+
### Step 2: Add the agent to OpenClaw
|
|
188
|
+
|
|
189
|
+
In `~/.openclaw/openclaw.json`, add an entry to the agents list:
|
|
190
|
+
|
|
191
|
+
```json
|
|
192
|
+
{
|
|
193
|
+
"agents": {
|
|
194
|
+
"list": [
|
|
195
|
+
{ "id": "main" },
|
|
196
|
+
{
|
|
197
|
+
"id": "project-manager",
|
|
198
|
+
"model": "anthropic/claude-sonnet-4-5",
|
|
199
|
+
"workspace": "~/.openclaw/workspace-project-manager"
|
|
200
|
+
}
|
|
201
|
+
]
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Each agent can have its own model, workspace, and system prompt.
|
|
207
|
+
|
|
208
|
+
### Step 3: Bind the Quiubo account to the agent
|
|
209
|
+
|
|
210
|
+
Tell OpenClaw which Quiubo account maps to which agent:
|
|
191
211
|
|
|
192
212
|
```json
|
|
193
213
|
{
|
|
@@ -233,6 +253,16 @@ Quiubo Chat (You + PM Bot) → account: pm → binding → agent: projec
|
|
|
233
253
|
- **Workspaces**: Fully isolated — agents can't see each other's files
|
|
234
254
|
- **Cursors**: Each account has its own cursor file — no clobbering
|
|
235
255
|
|
|
256
|
+
### Adding more agents
|
|
257
|
+
|
|
258
|
+
Repeat steps 1–5 for each new agent. The key command is:
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
openclaw channels add --channel quiubo
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Enter a unique Account ID each time (e.g. `support`, `research`). The wizard handles identity creation and config — then add the agent, binding, and workspace as shown above.
|
|
265
|
+
|
|
236
266
|
## Markdown Attachments
|
|
237
267
|
|
|
238
268
|
Agents can send `.md` file attachments alongside messages. Attachments appear as tappable cards in the Quiubo app with a full markdown viewer.
|
package/dist/index.js
CHANGED
|
@@ -9233,6 +9233,31 @@ var QuiuboApiClient = class {
|
|
|
9233
9233
|
identity_id: identityId
|
|
9234
9234
|
});
|
|
9235
9235
|
}
|
|
9236
|
+
// ========================================================================
|
|
9237
|
+
// Attachment Upload
|
|
9238
|
+
// ========================================================================
|
|
9239
|
+
/**
|
|
9240
|
+
* Get a presigned S3 URL for uploading an image attachment
|
|
9241
|
+
*/
|
|
9242
|
+
async getAttachmentPresignUrl(groupId, opts) {
|
|
9243
|
+
return this.request("POST", `/groups/${groupId}/attachments/presign`, opts);
|
|
9244
|
+
}
|
|
9245
|
+
/**
|
|
9246
|
+
* Upload a file buffer to a presigned S3 URL
|
|
9247
|
+
*/
|
|
9248
|
+
async uploadToPresignedUrl(uploadUrl, buffer, contentType) {
|
|
9249
|
+
const response = await fetch(uploadUrl, {
|
|
9250
|
+
method: "PUT",
|
|
9251
|
+
headers: {
|
|
9252
|
+
"Content-Type": contentType,
|
|
9253
|
+
"Content-Length": String(buffer.length)
|
|
9254
|
+
},
|
|
9255
|
+
body: buffer
|
|
9256
|
+
});
|
|
9257
|
+
if (!response.ok) {
|
|
9258
|
+
throw new Error(`S3 upload failed: ${response.status} ${response.statusText}`);
|
|
9259
|
+
}
|
|
9260
|
+
}
|
|
9236
9261
|
};
|
|
9237
9262
|
|
|
9238
9263
|
// src/realtime-gateway.ts
|
|
@@ -13300,23 +13325,57 @@ async function persistSeed(seedFile, seedB64) {
|
|
|
13300
13325
|
} catch {
|
|
13301
13326
|
}
|
|
13302
13327
|
}
|
|
13303
|
-
|
|
13328
|
+
var IMAGE_MIME_TYPES = {
|
|
13329
|
+
".jpg": "image/jpeg",
|
|
13330
|
+
".jpeg": "image/jpeg",
|
|
13331
|
+
".png": "image/png",
|
|
13332
|
+
".webp": "image/webp"
|
|
13333
|
+
};
|
|
13334
|
+
var MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
13335
|
+
async function readOutboundAttachments(mediaUrls, source, client, groupId, log, accountId) {
|
|
13304
13336
|
const attachments = [];
|
|
13305
13337
|
const seen = /* @__PURE__ */ new Set();
|
|
13306
13338
|
for (const url of mediaUrls) {
|
|
13307
13339
|
if (seen.has(url)) continue;
|
|
13308
13340
|
seen.add(url);
|
|
13309
13341
|
const filename = basename(url);
|
|
13310
|
-
|
|
13311
|
-
|
|
13312
|
-
|
|
13313
|
-
|
|
13314
|
-
|
|
13315
|
-
|
|
13342
|
+
const ext = filename.substring(filename.lastIndexOf(".")).toLowerCase();
|
|
13343
|
+
if (ext === ".md") {
|
|
13344
|
+
try {
|
|
13345
|
+
const content = await readFile2(url, "utf-8");
|
|
13346
|
+
if (content.length > 1024 * 1024) {
|
|
13347
|
+
log?.warn?.(`[${accountId}] Quiubo: skipping ${filename} \u2014 exceeds 1MB`);
|
|
13348
|
+
continue;
|
|
13349
|
+
}
|
|
13350
|
+
attachments.push({ filename, mimeType: "text/markdown", content, source });
|
|
13351
|
+
} catch (err) {
|
|
13352
|
+
log?.warn?.(`[${accountId}] Quiubo: failed to read ${url}: ${err}`);
|
|
13353
|
+
}
|
|
13354
|
+
} else if (ext in IMAGE_MIME_TYPES) {
|
|
13355
|
+
const mimeType = IMAGE_MIME_TYPES[ext];
|
|
13356
|
+
try {
|
|
13357
|
+
const buffer = await readFile2(url);
|
|
13358
|
+
if (buffer.length > MAX_IMAGE_BYTES) {
|
|
13359
|
+
log?.warn?.(`[${accountId}] Quiubo: skipping ${filename} \u2014 exceeds 5MB (${buffer.length} bytes)`);
|
|
13360
|
+
continue;
|
|
13361
|
+
}
|
|
13362
|
+
const presign = await client.getAttachmentPresignUrl(groupId, {
|
|
13363
|
+
contentType: mimeType,
|
|
13364
|
+
contentLength: buffer.length,
|
|
13365
|
+
filename
|
|
13366
|
+
});
|
|
13367
|
+
await client.uploadToPresignedUrl(presign.uploadUrl, buffer, mimeType);
|
|
13368
|
+
attachments.push({
|
|
13369
|
+
filename,
|
|
13370
|
+
mimeType,
|
|
13371
|
+
storageUrl: presign.storageUrl,
|
|
13372
|
+
sizeBytes: buffer.length,
|
|
13373
|
+
source
|
|
13374
|
+
});
|
|
13375
|
+
log?.info?.(`[${accountId}] Quiubo: uploaded image ${filename} (${buffer.length} bytes) \u2192 ${presign.storageUrl}`);
|
|
13376
|
+
} catch (err) {
|
|
13377
|
+
log?.warn?.(`[${accountId}] Quiubo: failed to upload image ${url}: ${err}`);
|
|
13316
13378
|
}
|
|
13317
|
-
attachments.push({ filename, mimeType: "text/markdown", content, source });
|
|
13318
|
-
} catch (err) {
|
|
13319
|
-
log?.warn?.(`[${accountId}] Quiubo: failed to read ${url}: ${err}`);
|
|
13320
13379
|
}
|
|
13321
13380
|
}
|
|
13322
13381
|
return attachments;
|
|
@@ -13675,8 +13734,6 @@ var quiuboPlugin = {
|
|
|
13675
13734
|
if (Array.isArray(ctx.mediaUrls)) urls.push(...ctx.mediaUrls);
|
|
13676
13735
|
const accountId = ctx.accountId ?? DEFAULT_ACCOUNT_ID;
|
|
13677
13736
|
const log = loggers.get(accountId);
|
|
13678
|
-
const mdAttachments = await readMdAttachments(urls, "agent", log, accountId);
|
|
13679
|
-
const plaintext = text || (mdAttachments.length === 0 ? "[Media attachment]" : "");
|
|
13680
13737
|
let client = clients.get(accountId);
|
|
13681
13738
|
let account = accounts.get(accountId);
|
|
13682
13739
|
if (!client || !account) {
|
|
@@ -13695,23 +13752,37 @@ var quiuboPlugin = {
|
|
|
13695
13752
|
if (!groupId) {
|
|
13696
13753
|
groupId = await resolveAnnounceGroupId(accountId, log);
|
|
13697
13754
|
}
|
|
13698
|
-
log?.info?.(`[${accountId}] [outbound:sendMedia] groupId=${groupId}, urls=${urls.length}, attachments=${mdAttachments.length}`);
|
|
13699
13755
|
if (!groupId) {
|
|
13700
13756
|
log?.error?.(`[${accountId}] [outbound:sendMedia] no groupId \u2014 ctx keys=${Object.keys(ctx).join(",")}`);
|
|
13701
13757
|
return { ok: false, error: "No groupId in outbound context" };
|
|
13702
13758
|
}
|
|
13759
|
+
const outboundAttachments = await readOutboundAttachments(urls, "agent", client, groupId, log, accountId);
|
|
13760
|
+
const imageAttachments = outboundAttachments.filter((a) => a.mimeType.startsWith("image/"));
|
|
13761
|
+
const nonImageAttachments = outboundAttachments.filter((a) => !a.mimeType.startsWith("image/"));
|
|
13762
|
+
const plaintext = text || (outboundAttachments.length === 0 ? "[Media attachment]" : "");
|
|
13763
|
+
log?.info?.(`[${accountId}] [outbound:sendMedia] groupId=${groupId}, urls=${urls.length}, attachments=${outboundAttachments.length} (${imageAttachments.length} images)`);
|
|
13703
13764
|
const senderId = account.botIdentityId;
|
|
13704
13765
|
if (!senderId) {
|
|
13705
13766
|
return { ok: false, error: "No botIdentityId configured" };
|
|
13706
13767
|
}
|
|
13707
13768
|
try {
|
|
13708
|
-
|
|
13709
|
-
|
|
13710
|
-
|
|
13711
|
-
|
|
13712
|
-
|
|
13713
|
-
|
|
13714
|
-
|
|
13769
|
+
if (plaintext || nonImageAttachments.length > 0) {
|
|
13770
|
+
const apiResp = await client.sendMessage(groupId, {
|
|
13771
|
+
senderIdentityId: senderId,
|
|
13772
|
+
plaintext: plaintext || " ",
|
|
13773
|
+
metadata: { format: "markdown" },
|
|
13774
|
+
...nonImageAttachments.length > 0 ? { attachments: nonImageAttachments } : {}
|
|
13775
|
+
});
|
|
13776
|
+
log?.info?.(`[${accountId}] [outbound:sendMedia] sent to group ${groupId} (realtime=${apiResp?.realtimeDelivered})`);
|
|
13777
|
+
}
|
|
13778
|
+
for (const img of imageAttachments) {
|
|
13779
|
+
const imgResp = await client.sendMessage(groupId, {
|
|
13780
|
+
senderIdentityId: senderId,
|
|
13781
|
+
plaintext: " ",
|
|
13782
|
+
attachments: [img]
|
|
13783
|
+
});
|
|
13784
|
+
log?.info?.(`[${accountId}] [outbound:sendMedia] image ${img.filename} sent to group ${groupId} (realtime=${imgResp?.realtimeDelivered})`);
|
|
13785
|
+
}
|
|
13715
13786
|
return { ok: true };
|
|
13716
13787
|
} catch (error) {
|
|
13717
13788
|
if (error instanceof QuiuboApiError) {
|
|
@@ -14321,13 +14392,13 @@ async function routeInboundMessage(opts) {
|
|
|
14321
14392
|
const fileUrls = [];
|
|
14322
14393
|
if (payload.mediaUrl) fileUrls.push(payload.mediaUrl);
|
|
14323
14394
|
if (Array.isArray(payload.mediaUrls)) fileUrls.push(...payload.mediaUrls);
|
|
14324
|
-
const
|
|
14395
|
+
const outboundAttachments = await readOutboundAttachments(fileUrls, agentSource, client, groupId, log, accountId);
|
|
14325
14396
|
if (payload.media?.length) {
|
|
14326
|
-
const existingFilenames = new Set(
|
|
14397
|
+
const existingFilenames = new Set(outboundAttachments.map((a) => a.filename));
|
|
14327
14398
|
for (const m of payload.media) {
|
|
14328
14399
|
const filename = m.filename || m.name || "";
|
|
14329
14400
|
if (filename.endsWith(".md") && typeof m.content === "string" && !existingFilenames.has(filename)) {
|
|
14330
|
-
|
|
14401
|
+
outboundAttachments.push({
|
|
14331
14402
|
filename,
|
|
14332
14403
|
mimeType: "text/markdown",
|
|
14333
14404
|
content: m.content,
|
|
@@ -14336,8 +14407,8 @@ async function routeInboundMessage(opts) {
|
|
|
14336
14407
|
}
|
|
14337
14408
|
}
|
|
14338
14409
|
}
|
|
14339
|
-
log?.info?.(`[${accountId}] Quiubo: after dedup: ${
|
|
14340
|
-
if (!payload.text &&
|
|
14410
|
+
log?.info?.(`[${accountId}] Quiubo: after dedup: ${outboundAttachments.length} attachments [${outboundAttachments.map((a) => a.filename).join(", ")}]`);
|
|
14411
|
+
if (!payload.text && outboundAttachments.length === 0) {
|
|
14341
14412
|
log?.debug?.(`[${accountId}] Quiubo: skipping empty ${info.kind} reply`);
|
|
14342
14413
|
return;
|
|
14343
14414
|
}
|
|
@@ -14350,11 +14421,13 @@ async function routeInboundMessage(opts) {
|
|
|
14350
14421
|
if (payload.text) {
|
|
14351
14422
|
payload.text = payload.text.replace(/\n*Reasoning:\n_[^_]*_\n*/g, "").trim();
|
|
14352
14423
|
}
|
|
14353
|
-
if (!payload.text &&
|
|
14424
|
+
if (!payload.text && outboundAttachments.length === 0) {
|
|
14354
14425
|
log?.debug?.(`[${accountId}] Quiubo: skipping empty reply after reasoning strip`);
|
|
14355
14426
|
return;
|
|
14356
14427
|
}
|
|
14357
|
-
|
|
14428
|
+
const imageAttachments2 = outboundAttachments.filter((a) => a.mimeType.startsWith("image/"));
|
|
14429
|
+
const nonImageAttachments = outboundAttachments.filter((a) => !a.mimeType.startsWith("image/"));
|
|
14430
|
+
log?.info?.(`[${accountId}] Quiubo: delivering ${info.kind} reply [${payload.text?.length ?? 0} chars, ${nonImageAttachments.length} non-image attachments, ${imageAttachments2.length} images]`);
|
|
14358
14431
|
try {
|
|
14359
14432
|
let ciphertextOut;
|
|
14360
14433
|
if (e2eeGrantedGroups?.has(groupId) && keyManager && payload.text) {
|
|
@@ -14371,16 +14444,26 @@ async function routeInboundMessage(opts) {
|
|
|
14371
14444
|
log?.warn?.(`[${accountId}] Quiubo: no epoch key for E2EE group ${groupId}, sending plaintext`);
|
|
14372
14445
|
}
|
|
14373
14446
|
}
|
|
14374
|
-
|
|
14375
|
-
|
|
14376
|
-
|
|
14377
|
-
|
|
14378
|
-
|
|
14379
|
-
|
|
14380
|
-
|
|
14381
|
-
|
|
14382
|
-
|
|
14383
|
-
|
|
14447
|
+
if (payload.text || nonImageAttachments.length > 0) {
|
|
14448
|
+
const apiResp = await client.sendMessage(groupId, {
|
|
14449
|
+
senderIdentityId: botIdentityId,
|
|
14450
|
+
...ciphertextOut ? { ciphertext: ciphertextOut } : { plaintext: payload.text || " " },
|
|
14451
|
+
metadata: { format: "markdown" },
|
|
14452
|
+
...nonImageAttachments.length > 0 ? { attachments: nonImageAttachments } : {}
|
|
14453
|
+
});
|
|
14454
|
+
if (apiResp?.realtimeDelivered === false) {
|
|
14455
|
+
log?.warn?.(`[${accountId}] Quiubo: reply persisted but Pusher delivery failed for group ${groupId} \u2014 clients will see on next poll`);
|
|
14456
|
+
} else {
|
|
14457
|
+
log?.info?.(`[${accountId}] Quiubo: reply sent to group ${groupId} (realtime=${apiResp?.realtimeDelivered})`);
|
|
14458
|
+
}
|
|
14459
|
+
}
|
|
14460
|
+
for (const img of imageAttachments2) {
|
|
14461
|
+
const imgResp = await client.sendMessage(groupId, {
|
|
14462
|
+
senderIdentityId: botIdentityId,
|
|
14463
|
+
plaintext: " ",
|
|
14464
|
+
attachments: [img]
|
|
14465
|
+
});
|
|
14466
|
+
log?.info?.(`[${accountId}] Quiubo: image ${img.filename} sent to group ${groupId} (realtime=${imgResp?.realtimeDelivered})`);
|
|
14384
14467
|
}
|
|
14385
14468
|
} catch (error) {
|
|
14386
14469
|
if (error instanceof QuiuboApiError) {
|