openclaw-quiubo 2.6.36 → 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
@@ -14343,11 +14343,11 @@ function resolveOutboundGroupId(ctx) {
14343
14343
  }
14344
14344
  async function resolveAnnounceGroupId(accountId, log) {
14345
14345
  try {
14346
- const { readFile: readFile3 } = await import("node:fs/promises");
14346
+ const { readFile: readFile4 } = await import("node:fs/promises");
14347
14347
  const { join: join3 } = await import("node:path");
14348
14348
  const homeDir = process.env.HOME ?? process.env.USERPROFILE ?? "";
14349
14349
  const cronPath = join3(homeDir, ".openclaw", "cron", "jobs.json");
14350
- const raw = await readFile3(cronPath, "utf-8");
14350
+ const raw = await readFile4(cronPath, "utf-8");
14351
14351
  const parsed = JSON.parse(raw);
14352
14352
  const jobs = parsed?.jobs ?? [];
14353
14353
  for (const job of jobs) {
@@ -14367,12 +14367,12 @@ async function resolveAnnounceGroupId(accountId, log) {
14367
14367
  }
14368
14368
  async function getActivityData(runtime2, log, agentId) {
14369
14369
  try {
14370
- const { readFile: readFile3 } = await import("node:fs/promises");
14370
+ const { readFile: readFile4 } = await import("node:fs/promises");
14371
14371
  const { join: join3 } = await import("node:path");
14372
14372
  const homeDir = process.env.HOME ?? process.env.USERPROFILE ?? "";
14373
14373
  const cronPath = join3(homeDir, ".openclaw", "cron", "jobs.json");
14374
14374
  log?.info?.(`getActivityData: reading ${cronPath} (agentId=${agentId})`);
14375
- const raw = await readFile3(cronPath, "utf-8");
14375
+ const raw = await readFile4(cronPath, "utf-8");
14376
14376
  const parsed = JSON.parse(raw);
14377
14377
  const jobs = parsed?.jobs ?? [];
14378
14378
  const items = jobs.filter((j) => j.enabled !== false && (!agentId || j.agentId === agentId)).map((job) => ({
@@ -14623,6 +14623,116 @@ async function routeInboundMessage(opts) {
14623
14623
  log?.info?.(`[${accountId}] Quiubo: message processed from ${senderId}`);
14624
14624
  }
14625
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
+
14626
14736
  // src/polling-gateway.ts
14627
14737
  var PollingGateway = class {
14628
14738
  client;
@@ -14770,6 +14880,7 @@ var plugin = {
14770
14880
  register(api) {
14771
14881
  setQuiuboRuntime(api.runtime);
14772
14882
  api.registerChannel({ plugin: quiuboPlugin });
14883
+ api.registerTool(createQuiuboPostToolFactory, { name: "quiubo_create_post" });
14773
14884
  }
14774
14885
  };
14775
14886
  var index_default = plugin;