zapry-openclaw-plugin 0.1.0 → 0.1.1

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.
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Strip a single leading Zapry-side target prefix from a chat id.
3
+ *
4
+ * "chat:g_123" → "g_123"
5
+ * "zapry:g_123" → "g_123"
6
+ * "zapry:group:g_123" → "g_123"
7
+ * "zapry:group:group:g_123" → "g_123" (repeated `group:` segments are chained)
8
+ * "zapry:direct:123" → "direct:123" (any trailing kind is preserved after `zapry:` is stripped)
9
+ * "g_123" → "g_123" (no-op)
10
+ *
11
+ * Caveat: The regex matches once and is anchored at the start, so a
12
+ * double-wrapped id like `zapry:group:zapry:group:g_123` will only lose its
13
+ * outermost wrapper (→ `zapry:group:g_123`). Call again if a legacy producer
14
+ * is known to emit double wraps.
15
+ */
16
+ export declare function stripZapryTargetPrefix(value: string): string;
17
+ /**
18
+ * Extract the agent id from a structured sessionKey of the form
19
+ * `agent:{agentId}:...`
20
+ * Returns undefined if the key does not match the agent-scoped shape
21
+ * (e.g. legacy `"main"` or an empty string).
22
+ */
23
+ export declare function parseAgentIdFromSessionKey(sessionKey: string | undefined): string | undefined;
24
+ /**
25
+ * Structural identity check for two user id strings.
26
+ *
27
+ * Zapry user ids are typically numeric; the same underlying id may arrive as
28
+ * `"123"` or `"0123"`. We accept a numeric match when both sides parse to a
29
+ * finite number. Non-numeric ids fall back to strict equality.
30
+ */
31
+ export declare function sameUserIdentity(left: string, right: string): boolean;
32
+ /**
33
+ * Zapry bot tokens are `{ownerId}:{secret}`. Returns ownerId or empty string
34
+ * if the token is malformed (missing colon, leading colon, etc).
35
+ */
36
+ export declare function resolveOwnerIdFromBotToken(botToken: string): string;
37
+ /**
38
+ * Canonical session key for a (agent, zapry peer) pair.
39
+ *
40
+ * agent:{agentId}:zapry:{kind}:{peerId}
41
+ */
42
+ export declare function buildPeerSessionKey(agentId: string, peer: {
43
+ kind: "group" | "direct";
44
+ id: string;
45
+ }): string;
46
+ export type RouteLike = {
47
+ agentId: string;
48
+ accountId: string;
49
+ sessionKey: string;
50
+ };
51
+ /**
52
+ * Ensure a route emitted by the OpenClaw router has a peer-scoped sessionKey.
53
+ *
54
+ * Legacy routers sometimes return the agent's "main" session key (`"main"` or
55
+ * `agent:{agentId}:main`) which conflates every peer into a single thread.
56
+ * When that happens we rebuild the key using {@link buildPeerSessionKey}.
57
+ * Any other non-empty sessionKey is preserved verbatim.
58
+ */
59
+ export declare function normalizeRouteSessionKey(route: RouteLike, peer: {
60
+ kind: "group" | "direct";
61
+ id: string;
62
+ }): RouteLike;
63
+ /**
64
+ * Decide whether a tool invocation is coming from the bot's owner.
65
+ *
66
+ * Precedence:
67
+ * 1. Explicit trust from the caller (`senderIsOwner === true`) wins.
68
+ * 2. Missing sender id → fail closed (return false).
69
+ * 3. Otherwise compare the runtime sender id with the id embedded in the
70
+ * bot token via {@link sameUserIdentity}.
71
+ */
72
+ export declare function isOwnerInvocation(params: {
73
+ senderIsOwner?: boolean;
74
+ senderId: string;
75
+ botToken: string;
76
+ }): boolean;
@@ -0,0 +1,126 @@
1
+ // Pure helpers used across index.ts, src/actions.ts and src/inbound.ts.
2
+ // Extracted here so they are both tree-shakable and unit-testable without
3
+ // pulling in the OpenClaw runtime or the Zapry HTTP client.
4
+ //
5
+ // Rules:
6
+ // • No I/O, no process.env reads, no dynamic imports.
7
+ // • Deterministic: same input → same output.
8
+ // • Safe to import from any layer (channel / actions / inbound / tests).
9
+ /**
10
+ * Strip a single leading Zapry-side target prefix from a chat id.
11
+ *
12
+ * "chat:g_123" → "g_123"
13
+ * "zapry:g_123" → "g_123"
14
+ * "zapry:group:g_123" → "g_123"
15
+ * "zapry:group:group:g_123" → "g_123" (repeated `group:` segments are chained)
16
+ * "zapry:direct:123" → "direct:123" (any trailing kind is preserved after `zapry:` is stripped)
17
+ * "g_123" → "g_123" (no-op)
18
+ *
19
+ * Caveat: The regex matches once and is anchored at the start, so a
20
+ * double-wrapped id like `zapry:group:zapry:group:g_123` will only lose its
21
+ * outermost wrapper (→ `zapry:group:g_123`). Call again if a legacy producer
22
+ * is known to emit double wraps.
23
+ */
24
+ export function stripZapryTargetPrefix(value) {
25
+ return value.replace(/^(?:chat:|zapry:(?:group:)*)/i, "");
26
+ }
27
+ /**
28
+ * Extract the agent id from a structured sessionKey of the form
29
+ * `agent:{agentId}:...`
30
+ * Returns undefined if the key does not match the agent-scoped shape
31
+ * (e.g. legacy `"main"` or an empty string).
32
+ */
33
+ export function parseAgentIdFromSessionKey(sessionKey) {
34
+ const value = `${sessionKey || ""}`.trim();
35
+ const match = /^agent:([^:]+):/.exec(value);
36
+ return match?.[1]?.trim() || undefined;
37
+ }
38
+ /**
39
+ * Structural identity check for two user id strings.
40
+ *
41
+ * Zapry user ids are typically numeric; the same underlying id may arrive as
42
+ * `"123"` or `"0123"`. We accept a numeric match when both sides parse to a
43
+ * finite number. Non-numeric ids fall back to strict equality.
44
+ */
45
+ export function sameUserIdentity(left, right) {
46
+ const normalizedLeft = left.trim();
47
+ const normalizedRight = right.trim();
48
+ if (!normalizedLeft || !normalizedRight) {
49
+ return false;
50
+ }
51
+ if (normalizedLeft === normalizedRight) {
52
+ return true;
53
+ }
54
+ const leftNum = Number(normalizedLeft);
55
+ const rightNum = Number(normalizedRight);
56
+ return Number.isFinite(leftNum) && Number.isFinite(rightNum) && leftNum === rightNum;
57
+ }
58
+ /**
59
+ * Zapry bot tokens are `{ownerId}:{secret}`. Returns ownerId or empty string
60
+ * if the token is malformed (missing colon, leading colon, etc).
61
+ */
62
+ export function resolveOwnerIdFromBotToken(botToken) {
63
+ const trimmed = String(botToken ?? "").trim();
64
+ const separatorIdx = trimmed.indexOf(":");
65
+ if (separatorIdx <= 0) {
66
+ return "";
67
+ }
68
+ return trimmed.slice(0, separatorIdx).trim();
69
+ }
70
+ /**
71
+ * Canonical session key for a (agent, zapry peer) pair.
72
+ *
73
+ * agent:{agentId}:zapry:{kind}:{peerId}
74
+ */
75
+ export function buildPeerSessionKey(agentId, peer) {
76
+ return `agent:${agentId}:zapry:${peer.kind}:${peer.id}`;
77
+ }
78
+ /**
79
+ * Ensure a route emitted by the OpenClaw router has a peer-scoped sessionKey.
80
+ *
81
+ * Legacy routers sometimes return the agent's "main" session key (`"main"` or
82
+ * `agent:{agentId}:main`) which conflates every peer into a single thread.
83
+ * When that happens we rebuild the key using {@link buildPeerSessionKey}.
84
+ * Any other non-empty sessionKey is preserved verbatim.
85
+ */
86
+ export function normalizeRouteSessionKey(route, peer) {
87
+ const agentId = `${route.agentId || ""}`.trim() || "main";
88
+ const sessionKey = `${route.sessionKey || ""}`.trim();
89
+ if (!sessionKey) {
90
+ return {
91
+ ...route,
92
+ agentId,
93
+ sessionKey: buildPeerSessionKey(agentId, peer),
94
+ };
95
+ }
96
+ const defaultMainSessionKey = `agent:${agentId}:main`;
97
+ if (sessionKey === "main" || sessionKey === defaultMainSessionKey) {
98
+ return {
99
+ ...route,
100
+ agentId,
101
+ sessionKey: buildPeerSessionKey(agentId, peer),
102
+ };
103
+ }
104
+ return route;
105
+ }
106
+ /**
107
+ * Decide whether a tool invocation is coming from the bot's owner.
108
+ *
109
+ * Precedence:
110
+ * 1. Explicit trust from the caller (`senderIsOwner === true`) wins.
111
+ * 2. Missing sender id → fail closed (return false).
112
+ * 3. Otherwise compare the runtime sender id with the id embedded in the
113
+ * bot token via {@link sameUserIdentity}.
114
+ */
115
+ export function isOwnerInvocation(params) {
116
+ if (params.senderIsOwner === true) {
117
+ return true;
118
+ }
119
+ const senderId = `${params.senderId || ""}`.trim();
120
+ if (!senderId) {
121
+ return false;
122
+ }
123
+ const ownerId = resolveOwnerIdFromBotToken(params.botToken);
124
+ return sameUserIdentity(senderId, ownerId);
125
+ }
126
+ //# sourceMappingURL=internal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"internal.js","sourceRoot":"","sources":["../../src/internal.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zapry-openclaw-plugin",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "OpenClaw Zapry channel plugin — messaging, groups, feed, clubs, and bot self-management",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -41,10 +41,14 @@
41
41
  ],
42
42
  "scripts": {
43
43
  "build": "tsc -p tsconfig.json",
44
- "prepack": "npm run build"
44
+ "prepack": "npm run build",
45
+ "test": "vitest run",
46
+ "test:watch": "vitest",
47
+ "typecheck": "tsc -p tsconfig.json --noEmit"
45
48
  },
46
49
  "devDependencies": {
47
50
  "@types/node": "^25.5.0",
48
- "typescript": "^5.9.3"
51
+ "typescript": "^5.9.3",
52
+ "vitest": "^4.1.5"
49
53
  }
50
54
  }
@@ -13,12 +13,12 @@ allowed-tools: ["message", "zapry_action", "zapry_post", "pdf"]
13
13
  tags: ["zapry", "messaging", "groups", "feed", "social", "openapi"]
14
14
  triggers_api:
15
15
  [
16
- "sendMessage", "sendPhoto", "sendVideo", "sendDocument", "sendAudio", "sendVoice", "sendAnimation",
16
+ "sendMessage", "sendLinkCard", "sendPhoto", "sendVideo", "sendDocument", "sendAudio", "sendVoice", "sendAnimation",
17
17
  "generateAudio",
18
18
  "deleteMessage", "answerCallbackQuery",
19
19
  "getFile",
20
20
  "getUpdates", "setWebhook", "getWebhookInfo", "deleteWebhook", "webhooks/:token",
21
- "muteChatMember", "kickChatMember", "setChatTitle", "setChatDescription",
21
+ "createGroupChat", "dismissGroupChat", "inviteChatMember", "muteChatMember", "kickChatMember", "setChatTitle", "setChatDescription",
22
22
  "getChatAdministrators", "getChatMember", "getChatMembers", "getChatMemberCount",
23
23
  "getMyGroups", "getMyChats", "getMyContacts", "setMyFriendVerify", "getMyFriendRequests",
24
24
  "acceptFriendRequest", "rejectFriendRequest", "addFriend", "deleteFriend",
@@ -34,7 +34,7 @@ triggers_api:
34
34
  执行 Zapry 动作时按以下路由:
35
35
 
36
36
  - `message`: 仅用于最简单的纯文本回复/发送,不承载任何 Zapry 平台 action
37
- - `zapry_action`: **唯一用于 Zapry 平台动作**,包括发文字(`send-message`)、图片、视频、音频、文件、文档,以及所有查询/管理能力。**支持群名自动解析**——直接传群名即可,无需手动查 ID。发送文件用 `action: "send-document"`
37
+ - `zapry_action`: **唯一用于 Zapry 平台动作**,包括发文字(`send-message`)、分享链接卡片(`send-link-card`)、图片、视频、音频、文件、文档,以及所有查询/管理能力。**支持群名自动解析**——直接传群名即可,无需手动查 ID。发送文件用 `action: "send-document"`
38
38
  - `zapry_post`: 发广场动态(create-post),传 `content`,可选 `images`
39
39
  - `pdf`: 创建 / 分析 PDF 文件。创建后用 `zapry_action send-document` 发送到聊天
40
40
 
@@ -42,6 +42,7 @@ triggers_api:
42
42
 
43
43
  - **发送任何内容(文字/图片/视频/文件/语音)到群聊或私聊**:**一律用 `zapry_action`**
44
44
  - 发文字用 `action: "send-message"`(支持群名自动解析)
45
+ - 主动分享 URL 卡片用 `action: "send-link-card"`,必填 `chat_id`、`url`、`title`
45
46
  - 发图片用 `action: "send-photo"`
46
47
  - 发视频/文件/音频分别用对应 action
47
48
  - **禁止用 `message` 工具向群聊发文字**——`message` 工具不支持群名自动解析,会导致发送失败
@@ -91,7 +92,7 @@ triggers_api:
91
92
  **owner-only action 清单(含只读查询):**
92
93
  - 好友/联系人:`get-my-contacts` / `get-my-friend-requests` / `accept-friend-request` / `reject-friend-request` / `add-friend` / `delete-friend`
93
94
  - Bot 资料/配置:`get-me` / `get-my-profile` / `get-my-soul` / `set-my-soul` / `get-my-skills` / `set-my-skills` / `set-my-name` / `set-my-description` / `set-my-wallet-address` / `set-my-friend-verify`
94
- - 群组/会话/历史:`get-my-groups` / `get-my-chats` / `get-chat-history` / `get-chat-member` / `get-chat-members` / `get-chat-member-count` / `get-chat-administrators` / `mute-chat-member` / `kick-chat-member` / `invite-chat-member` / `set-chat-title` / `set-chat-description`
95
+ - 群组/会话/历史:`get-my-groups` / `get-my-chats` / `get-chat-history` / `get-chat-member` / `get-chat-members` / `get-chat-member-count` / `get-chat-administrators` / `create-group-chat` / `dismiss-group-chat` / `invite-chat-member` / `mute-chat-member` / `kick-chat-member` / `set-chat-title` / `set-chat-description`
95
96
  - Feed / Club / Webhook:`zapry_post`、`create-post` / `delete-post` / `comment-post` / `like-post` / `share-post` / `get-trending-posts` / `get-latest-posts` / `get-my-posts` / `search-posts` / `get-my-clubs` / `create-club` / `update-club` / `set-webhook` / `get-webhook-info` / `delete-webhook` / `webhooks-token`
96
97
  - 其他平台动作:任何通过 `zapry_action` 发起的跨聊天发送、平台查询、平台管理动作,默认都按 owner-only 处理
97
98
 
@@ -114,7 +115,7 @@ triggers_api:
114
115
 
115
116
  - `chat_id`, `user_id`, `message_id`, `callback_query_id`, `file_id`, `dynamic_id`
116
117
  - `page`, `page_size`, `language_code`, `wallet_address`, `need_verify`, `pending_only`
117
- - `text`, `photo`, `video`, `document`, `audio`, `voice`, `animation`, `content`, `images`
118
+ - `text`, `url`, `title`, `content`, `icon_url`, `image_url`, `fallback_text`, `photo`, `video`, `document`, `audio`, `voice`, `animation`, `images`
118
119
  - `soulMd`, `skills`, `version`, `source`, `agentKey`
119
120
 
120
121
  兼容别名(仅兼容,不作为主写法):`chatId`、`userId`、`messageId`、`dynamicId`、`pageSize`、`languageCode`
@@ -202,6 +203,7 @@ triggers_api:
202
203
  ### Messaging
203
204
 
204
205
  - `send-message`:`chat_id`, `text`;可选 `reply_markup`, `reply_to_message_id`, `message_thread_id`
206
+ - `send-link-card`:`chat_id`, `url`, `title`;可选 `content`, `text`, `icon_url`, `image_url`, `source`, `open_mode`, `fallback_text`, `extra`, `reply_markup`, `reply_to_message_id`, `message_thread_id`
205
207
  - `send-photo`:`chat_id`;可选 `photo`(图片源)或 `prompt`(文字描述,自动生成图片)
206
208
  - `send-video`:`chat_id`, `video`
207
209
  - `send-document`:`chat_id`, `document`
@@ -238,6 +240,9 @@ triggers_api:
238
240
  - `get-chat-members`:`chat_id`;可选 `page`, `page_size`, `keyword`
239
241
  - `get-chat-member-count`:`chat_id`
240
242
  - `get-chat-administrators`:`chat_id`
243
+ - `create-group-chat`:`title`;可选 `description`, `avatar`, `user_ids`, `bot_ids`
244
+ - `dismiss-group-chat`:`chat_id`;可选 `reason`
245
+ - `invite-chat-member`:`chat_id`, `user_id`
241
246
  - `mute-chat-member`:`chat_id`, `user_id`, `mute`
242
247
  - `kick-chat-member`:`chat_id`, `user_id`
243
248
  - `set-chat-title`:`chat_id`, `title`