openclaw-quiubo 2.6.35 → 2.6.37

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/POSTS.md ADDED
@@ -0,0 +1,122 @@
1
+ # Agent Posts
2
+
3
+ Let agents create posts in Quiubo groups — image + optional caption, visible in the group's post feed, auto-expires after 24 hours.
4
+
5
+ ## Why This Exists
6
+
7
+ Without this tool, an agent asked to "create a post" will generate an image and send it as a chat message via `sendMedia`. The agent has no way to distinguish "send a message with an image" from "create a post" — both route through the same `deliver-callback` pipeline.
8
+
9
+ The `quiubo_create_post` tool gives agents an explicit action for posts. The agent calls it by name, the image goes through the presigned upload flow, and the result appears in the post feed — not the chat.
10
+
11
+ ## Requirements
12
+
13
+ - A configured Quiubo account with `apiKey` and `botIdentityId` in `channels.quiubo.accounts`
14
+ - The agent must have an image file on disk (`.jpg`, `.jpeg`, `.png`, or `.webp`, max 5MB)
15
+
16
+ If the Quiubo channel isn't configured, the tool silently doesn't register — agents won't see it.
17
+
18
+ ## How It Works
19
+
20
+ ```
21
+ Agent decides to create a post
22
+
23
+
24
+ quiubo_create_post(image_path, caption?, group_id?)
25
+
26
+ ├─ Reads image from disk
27
+ ├─ Validates format + size
28
+ ├─ POST /v1/sdk/posts/presign → gets upload URL + post record
29
+ ├─ PUT to presigned S3 URL → uploads image bytes
30
+
31
+
32
+ Post appears in group feed (not chat)
33
+ ```
34
+
35
+ The tool creates its own API client from the account config — no shared state with the channel gateway.
36
+
37
+ ## Tool Parameters
38
+
39
+ | Parameter | Type | Required | Description |
40
+ |-----------|------|----------|-------------|
41
+ | `image_path` | string | Yes | Absolute path to image file on disk |
42
+ | `caption` | string | No | Text caption (max 500 chars) |
43
+ | `group_id` | string | No | Target group UUID. Defaults to the current conversation group. |
44
+
45
+ ## Tool Response
46
+
47
+ **Success:**
48
+ ```json
49
+ {
50
+ "ok": true,
51
+ "postId": "post-uuid",
52
+ "imageUrl": "https://...",
53
+ "groupId": "group-uuid"
54
+ }
55
+ ```
56
+
57
+ **Error:**
58
+ ```json
59
+ {
60
+ "ok": false,
61
+ "error": "Unsupported image format: .gif. Use .jpg, .png, or .webp"
62
+ }
63
+ ```
64
+
65
+ ## Teaching Your Agent
66
+
67
+ Add something like this to the agent's identity/instructions file (`SOUL.md`, `IDENTITY.md`, or system prompt):
68
+
69
+ ```markdown
70
+ ## Posts
71
+
72
+ You have a `quiubo_create_post` tool. Use it when asked to create a post, share a photo to the feed, or post an update.
73
+
74
+ Posts are different from messages:
75
+ - Posts appear in the group's **post feed**, not the chat
76
+ - Posts require an **image** (messages don't)
77
+ - Posts auto-expire after **24 hours**
78
+
79
+ Workflow:
80
+ 1. Generate or locate the image file
81
+ 2. Call `quiubo_create_post` with `image_path` and optional `caption`
82
+ 3. The post appears in the group feed automatically
83
+
84
+ Do NOT use sendMessage/sendMedia for posts — that sends a chat message, not a post.
85
+ ```
86
+
87
+ ## Group Targeting
88
+
89
+ The tool resolves the target group in this order:
90
+
91
+ 1. **`group_id` parameter** — if the agent passes it explicitly
92
+ 2. **Session key** — extracted from the OpenClaw session key (`agent:<name>:quiubo:<uuid>`)
93
+
94
+ For most conversational use ("create a post in this group"), the session key auto-resolves. The `group_id` parameter is for cross-posting to a different group.
95
+
96
+ ## Example Agent Interaction
97
+
98
+ ```
99
+ User: Create a post with today's weather forecast
100
+
101
+ Agent: (generates weather image → saves to /tmp/weather-2026-02-28.png)
102
+ Agent: (calls quiubo_create_post)
103
+ image_path: /tmp/weather-2026-02-28.png
104
+ caption: "☀️ Today's forecast: Clear skies, 22°C"
105
+
106
+ Tool result: { ok: true, postId: "abc-123", groupId: "def-456" }
107
+
108
+ Agent: Done — the weather forecast is now in the group's post feed.
109
+ ```
110
+
111
+ ## Troubleshooting
112
+
113
+ | Symptom | Fix |
114
+ |---------|-----|
115
+ | Agent sends image as chat message instead of post | Agent doesn't know about the tool. Add instructions to its identity file (see [Teaching Your Agent](#teaching-your-agent)). |
116
+ | Tool doesn't appear for the agent | Check that `channels.quiubo.accounts.default` has both `apiKey` and `botIdentityId`. The factory returns null without them. |
117
+ | "No group_id provided and could not resolve from session context" | The session key doesn't contain a group UUID. Pass `group_id` explicitly. |
118
+ | "No botIdentityId configured" | Add `botIdentityId` to the account config in `channels.quiubo.accounts`. |
119
+ | "Unsupported image format" | Only `.jpg`, `.jpeg`, `.png`, `.webp` are supported. Convert the image first. |
120
+ | "Image exceeds 5MB limit" | Compress or resize the image before calling the tool. |
121
+ | "Cannot read image" | The `image_path` doesn't exist or isn't readable. Check the path is absolute and the file was saved. |
122
+ | Post created but not visible | Check the group UUID is correct. Posts are scoped to a group and expire after 24 hours. |
package/dist/index.d.ts CHANGED
@@ -15,6 +15,9 @@ interface OpenClawPluginApi {
15
15
  registerChannel: (opts: {
16
16
  plugin: any;
17
17
  }) => void;
18
+ registerTool: (tool: any, opts?: {
19
+ name?: string;
20
+ }) => void;
18
21
  }
19
22
  declare const plugin: {
20
23
  id: string;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,KAAK,aAAa,EAAE,KAAK,sBAAsB,EAAE,KAAK,cAAc,EAAE,KAAK,cAAc,EAAE,KAAK,SAAS,EAAE,MAAM,2BAA2B,CAAC;AACvK,OAAO,EAAE,cAAc,EAAE,KAAK,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AACtF,OAAO,EAAE,kBAAkB,EAAE,KAAK,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAC1F,YAAY,EACV,mBAAmB,EACnB,WAAW,EACX,aAAa,EACb,cAAc,EACd,eAAe,EACf,kBAAkB,EAClB,cAAc,EACd,kBAAkB,EAClB,4BAA4B,GAC7B,MAAM,gBAAgB,CAAC;AAGxB,UAAU,iBAAiB;IACzB,OAAO,EAAE,OAAO,CAAC;IAEjB,eAAe,EAAE,CAAC,IAAI,EAAE;QAAE,MAAM,EAAE,GAAG,CAAA;KAAE,KAAK,IAAI,CAAC;CAClD;AAED,QAAA,MAAM,MAAM;;;;kBAII,iBAAiB;CAIhC,CAAC;AAEF,eAAe,MAAM,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAOH,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,KAAK,aAAa,EAAE,KAAK,sBAAsB,EAAE,KAAK,cAAc,EAAE,KAAK,cAAc,EAAE,KAAK,SAAS,EAAE,MAAM,2BAA2B,CAAC;AACvK,OAAO,EAAE,cAAc,EAAE,KAAK,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AACtF,OAAO,EAAE,kBAAkB,EAAE,KAAK,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAC1F,YAAY,EACV,mBAAmB,EACnB,WAAW,EACX,aAAa,EACb,cAAc,EACd,eAAe,EACf,kBAAkB,EAClB,cAAc,EACd,kBAAkB,EAClB,4BAA4B,GAC7B,MAAM,gBAAgB,CAAC;AAGxB,UAAU,iBAAiB;IACzB,OAAO,EAAE,OAAO,CAAC;IAEjB,eAAe,EAAE,CAAC,IAAI,EAAE;QAAE,MAAM,EAAE,GAAG,CAAA;KAAE,KAAK,IAAI,CAAC;IAEjD,YAAY,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,IAAI,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CAC7D;AAED,QAAA,MAAM,MAAM;;;;kBAII,iBAAiB;CAKhC,CAAC;AAEF,eAAe,MAAM,CAAC"}
package/dist/index.js CHANGED
@@ -9234,6 +9234,28 @@ var QuiuboApiClient = class {
9234
9234
  });
9235
9235
  }
9236
9236
  // ========================================================================
9237
+ // Posts
9238
+ // ========================================================================
9239
+ /**
9240
+ * Create a post as an SDK-owned identity. Returns presigned upload URL and post metadata.
9241
+ */
9242
+ async createPost(opts) {
9243
+ return this.request("POST", "/posts/presign", opts);
9244
+ }
9245
+ /**
9246
+ * List posts by SDK identities (optionally filter by identity)
9247
+ */
9248
+ async listPosts(identityId) {
9249
+ const query = identityId ? `?identity_id=${identityId}` : "";
9250
+ return this.request("GET", `/posts${query}`);
9251
+ }
9252
+ /**
9253
+ * Delete a post authored by an SDK identity
9254
+ */
9255
+ async deletePost(postId) {
9256
+ return this.request("DELETE", `/posts/${postId}`);
9257
+ }
9258
+ // ========================================================================
9237
9259
  // Attachment Upload
9238
9260
  // ========================================================================
9239
9261
  /**
@@ -13440,6 +13462,7 @@ var quiuboPlugin = {
13440
13462
  capabilities: {
13441
13463
  chatTypes: ["group"],
13442
13464
  supportsMedia: true,
13465
+ supportsPosts: true,
13443
13466
  supportsReactions: false,
13444
13467
  supportsThreads: false
13445
13468
  },
@@ -13821,6 +13844,86 @@ var quiuboPlugin = {
13821
13844
  log?.error?.(`[${accountId}] [outbound:sendMedia] SEND FAILED [network] group=${groupId} \u2014 ${msg}`);
13822
13845
  return { ok: false, error: msg };
13823
13846
  }
13847
+ },
13848
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
13849
+ async createPost(ctx) {
13850
+ const text = ctx.text || ctx.caption || "";
13851
+ const urls = [];
13852
+ if (ctx.mediaUrl) urls.push(ctx.mediaUrl);
13853
+ if (Array.isArray(ctx.mediaUrls)) urls.push(...ctx.mediaUrls);
13854
+ const accountId = ctx.accountId ?? DEFAULT_ACCOUNT_ID;
13855
+ const log = loggers.get(accountId);
13856
+ let client = clients.get(accountId);
13857
+ let account = accounts.get(accountId);
13858
+ if (!client || !account) {
13859
+ const acct = getChannelConfig(ctx.cfg)?.accounts?.[accountId];
13860
+ if (!acct?.apiKey) {
13861
+ log?.warn?.(`[${accountId}] [outbound:createPost] no account config found`);
13862
+ return { ok: false, error: "No account config found" };
13863
+ }
13864
+ const apiUrl = acct.apiUrl ?? DEFAULT_API_URL;
13865
+ client = new QuiuboApiClient(apiUrl, acct.apiKey);
13866
+ account = { ...acct, accountId };
13867
+ clients.set(accountId, client);
13868
+ accounts.set(accountId, account);
13869
+ }
13870
+ let groupId = resolveOutboundGroupId(ctx);
13871
+ if (!groupId) {
13872
+ groupId = await resolveAnnounceGroupId(accountId, log);
13873
+ }
13874
+ if (!groupId) {
13875
+ log?.error?.(`[${accountId}] [outbound:createPost] no groupId \u2014 ctx keys=${Object.keys(ctx).join(",")}`);
13876
+ return { ok: false, error: "No groupId in outbound context" };
13877
+ }
13878
+ const senderId = account.botIdentityId;
13879
+ if (!senderId) {
13880
+ return { ok: false, error: "No botIdentityId configured" };
13881
+ }
13882
+ let imageBuffer;
13883
+ let imageContentType;
13884
+ for (const url of urls) {
13885
+ const filename = basename(url);
13886
+ const ext = filename.substring(filename.lastIndexOf(".")).toLowerCase();
13887
+ if (ext in IMAGE_MIME_TYPES) {
13888
+ try {
13889
+ imageBuffer = await readFile2(url);
13890
+ imageContentType = IMAGE_MIME_TYPES[ext];
13891
+ if (imageBuffer.length > MAX_IMAGE_BYTES) {
13892
+ log?.warn?.(`[${accountId}] [outbound:createPost] skipping ${filename} \u2014 exceeds 5MB (${imageBuffer.length} bytes)`);
13893
+ imageBuffer = void 0;
13894
+ imageContentType = void 0;
13895
+ continue;
13896
+ }
13897
+ break;
13898
+ } catch (err) {
13899
+ log?.warn?.(`[${accountId}] [outbound:createPost] failed to read ${url}: ${err}`);
13900
+ }
13901
+ }
13902
+ }
13903
+ if (!imageBuffer || !imageContentType) {
13904
+ log?.error?.(`[${accountId}] [outbound:createPost] no valid image found in mediaUrls (posts require an image)`);
13905
+ return { ok: false, error: "Posts require an image \u2014 no valid image found in media URLs" };
13906
+ }
13907
+ log?.info?.(`[${accountId}] [outbound:createPost] groupId=${groupId}, text=${text?.length ?? 0} chars, image=${imageContentType} (${imageBuffer.length} bytes)`);
13908
+ try {
13909
+ const presign = await client.createPost({
13910
+ identityId: senderId,
13911
+ contentType: imageContentType,
13912
+ content: text || void 0,
13913
+ groupId
13914
+ });
13915
+ await client.uploadToPresignedUrl(presign.uploadUrl, imageBuffer, imageContentType);
13916
+ log?.info?.(`[${accountId}] [outbound:createPost] post ${presign.post.id} created in group ${groupId}, image uploaded to ${presign.imageUrl}`);
13917
+ return { ok: true };
13918
+ } catch (error) {
13919
+ if (error instanceof QuiuboApiError) {
13920
+ log?.error?.(`[${accountId}] [outbound:createPost] FAILED [${error.status}] group=${groupId} \u2014 ${error.body}`);
13921
+ return { ok: false, error: `${error.status}: ${error.body}` };
13922
+ }
13923
+ const msg = error instanceof Error ? error.message : String(error);
13924
+ log?.error?.(`[${accountId}] [outbound:createPost] FAILED [network] group=${groupId} \u2014 ${msg}`);
13925
+ return { ok: false, error: msg };
13926
+ }
13824
13927
  }
13825
13928
  },
13826
13929
  // ── gateway adapter ─────────────────────────────────────────────
@@ -14240,11 +14343,11 @@ function resolveOutboundGroupId(ctx) {
14240
14343
  }
14241
14344
  async function resolveAnnounceGroupId(accountId, log) {
14242
14345
  try {
14243
- const { readFile: readFile3 } = await import("node:fs/promises");
14346
+ const { readFile: readFile4 } = await import("node:fs/promises");
14244
14347
  const { join: join3 } = await import("node:path");
14245
14348
  const homeDir = process.env.HOME ?? process.env.USERPROFILE ?? "";
14246
14349
  const cronPath = join3(homeDir, ".openclaw", "cron", "jobs.json");
14247
- const raw = await readFile3(cronPath, "utf-8");
14350
+ const raw = await readFile4(cronPath, "utf-8");
14248
14351
  const parsed = JSON.parse(raw);
14249
14352
  const jobs = parsed?.jobs ?? [];
14250
14353
  for (const job of jobs) {
@@ -14264,12 +14367,12 @@ async function resolveAnnounceGroupId(accountId, log) {
14264
14367
  }
14265
14368
  async function getActivityData(runtime2, log, agentId) {
14266
14369
  try {
14267
- const { readFile: readFile3 } = await import("node:fs/promises");
14370
+ const { readFile: readFile4 } = await import("node:fs/promises");
14268
14371
  const { join: join3 } = await import("node:path");
14269
14372
  const homeDir = process.env.HOME ?? process.env.USERPROFILE ?? "";
14270
14373
  const cronPath = join3(homeDir, ".openclaw", "cron", "jobs.json");
14271
14374
  log?.info?.(`getActivityData: reading ${cronPath} (agentId=${agentId})`);
14272
- const raw = await readFile3(cronPath, "utf-8");
14375
+ const raw = await readFile4(cronPath, "utf-8");
14273
14376
  const parsed = JSON.parse(raw);
14274
14377
  const jobs = parsed?.jobs ?? [];
14275
14378
  const items = jobs.filter((j) => j.enabled !== false && (!agentId || j.agentId === agentId)).map((job) => ({
@@ -14520,6 +14623,116 @@ async function routeInboundMessage(opts) {
14520
14623
  log?.info?.(`[${accountId}] Quiubo: message processed from ${senderId}`);
14521
14624
  }
14522
14625
 
14626
+ // src/create-post-tool.ts
14627
+ import { readFile as readFile3 } from "fs/promises";
14628
+ var IMAGE_MIME_TYPES2 = {
14629
+ ".jpg": "image/jpeg",
14630
+ ".jpeg": "image/jpeg",
14631
+ ".png": "image/png",
14632
+ ".webp": "image/webp"
14633
+ };
14634
+ var MAX_IMAGE_BYTES2 = 5 * 1024 * 1024;
14635
+ var DEFAULT_API_URL2 = "https://api.quiubo.io";
14636
+ function extractGroupIdFromSessionKey(key) {
14637
+ if (!key) return void 0;
14638
+ const match = key.match(/quiubo:([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i);
14639
+ return match?.[1];
14640
+ }
14641
+ function jsonResult(payload) {
14642
+ return {
14643
+ content: [{ type: "text", text: JSON.stringify(payload) }],
14644
+ details: payload
14645
+ };
14646
+ }
14647
+ function errorResult(msg) {
14648
+ return {
14649
+ content: [{ type: "text", text: msg }],
14650
+ details: { ok: false, error: msg }
14651
+ };
14652
+ }
14653
+ function createQuiuboPostToolFactory(ctx) {
14654
+ const quiuboConfig = ctx.config?.channels?.quiubo;
14655
+ const accountCfg = quiuboConfig?.accounts?.default;
14656
+ if (!accountCfg?.apiKey) return null;
14657
+ const client = new QuiuboApiClient(
14658
+ accountCfg.apiUrl ?? DEFAULT_API_URL2,
14659
+ accountCfg.apiKey
14660
+ );
14661
+ const botIdentityId = accountCfg.botIdentityId;
14662
+ const defaultGroupId = extractGroupIdFromSessionKey(ctx.sessionKey);
14663
+ return {
14664
+ name: "quiubo_create_post",
14665
+ label: "Create Quiubo Post",
14666
+ description: "Create a post with an image in a Quiubo group. Posts appear in the group post feed and auto-expire after 24 hours. REQUIRES an image file path on disk (.jpg/.jpeg/.png/.webp, max 5MB). Optionally include a caption (max 500 chars).",
14667
+ parameters: {
14668
+ type: "object",
14669
+ properties: {
14670
+ image_path: {
14671
+ type: "string",
14672
+ description: "Absolute path to image file on disk"
14673
+ },
14674
+ caption: {
14675
+ type: "string",
14676
+ description: "Optional text caption (max 500 chars)"
14677
+ },
14678
+ group_id: {
14679
+ type: "string",
14680
+ description: "Target group UUID. Omit to use current conversation group."
14681
+ }
14682
+ },
14683
+ required: ["image_path"]
14684
+ },
14685
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
14686
+ execute: async (_toolCallId, params) => {
14687
+ const groupId = params.group_id || defaultGroupId;
14688
+ if (!groupId) {
14689
+ return errorResult("No group_id provided and could not resolve from session context");
14690
+ }
14691
+ if (!botIdentityId) {
14692
+ return errorResult("No botIdentityId configured");
14693
+ }
14694
+ const imagePath = params.image_path;
14695
+ const ext = imagePath.substring(imagePath.lastIndexOf(".")).toLowerCase();
14696
+ if (!(ext in IMAGE_MIME_TYPES2)) {
14697
+ return errorResult(`Unsupported image format: ${ext}. Use .jpg, .png, or .webp`);
14698
+ }
14699
+ let imageBuffer;
14700
+ try {
14701
+ imageBuffer = await readFile3(imagePath);
14702
+ } catch {
14703
+ return errorResult(`Cannot read image: ${imagePath}`);
14704
+ }
14705
+ if (imageBuffer.length > MAX_IMAGE_BYTES2) {
14706
+ return errorResult(
14707
+ `Image exceeds 5MB limit (${(imageBuffer.length / 1024 / 1024).toFixed(1)}MB)`
14708
+ );
14709
+ }
14710
+ const contentType = IMAGE_MIME_TYPES2[ext];
14711
+ try {
14712
+ const presign = await client.createPost({
14713
+ identityId: botIdentityId,
14714
+ contentType,
14715
+ content: params.caption || void 0,
14716
+ groupId
14717
+ });
14718
+ await client.uploadToPresignedUrl(presign.uploadUrl, imageBuffer, contentType);
14719
+ return jsonResult({
14720
+ ok: true,
14721
+ postId: presign.post.id,
14722
+ imageUrl: presign.imageUrl,
14723
+ groupId
14724
+ });
14725
+ } catch (error) {
14726
+ if (error instanceof QuiuboApiError) {
14727
+ return errorResult(`Quiubo API error: ${error.status} \u2014 ${error.body}`);
14728
+ }
14729
+ const msg = error instanceof Error ? error.message : String(error);
14730
+ return errorResult(`Post creation failed: ${msg}`);
14731
+ }
14732
+ }
14733
+ };
14734
+ }
14735
+
14523
14736
  // src/polling-gateway.ts
14524
14737
  var PollingGateway = class {
14525
14738
  client;
@@ -14667,6 +14880,7 @@ var plugin = {
14667
14880
  register(api) {
14668
14881
  setQuiuboRuntime(api.runtime);
14669
14882
  api.registerChannel({ plugin: quiuboPlugin });
14883
+ api.registerTool(createQuiuboPostToolFactory, { name: "quiubo_create_post" });
14670
14884
  }
14671
14885
  };
14672
14886
  var index_default = plugin;