@gloablehive/ipad-wechat-plugin 1.0.0

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/index.ts ADDED
@@ -0,0 +1,39 @@
1
+ /**
2
+ * OpenClaw Channel Plugin Entry Point
3
+ *
4
+ * iPad WeChat Plugin - enables OpenClaw to send/receive WeChat messages
5
+ * through the iPad protocol (different from WorkPhone/phone protocol)
6
+ */
7
+
8
+ import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
9
+ import { ipadWeChatPlugin } from "./src/channel.js";
10
+
11
+ export default defineChannelPluginEntry({
12
+ id: "ipad-wechat",
13
+ name: "iPad WeChat",
14
+ description: "Connect OpenClaw to iPad WeChat protocol for sending and receiving WeChat messages",
15
+ plugin: ipadWeChatPlugin,
16
+
17
+ registerFull(api) {
18
+ // Register webhook endpoint for receiving messages
19
+ api.registerHttpRoute({
20
+ path: "/ipad-wechat/webhook",
21
+ auth: "plugin",
22
+ handler: async (req, res) => {
23
+ try {
24
+ const payload = req.body;
25
+ // Handle inbound message
26
+ console.log("[iPad WeChat] Received webhook:", payload);
27
+ res.statusCode = 200;
28
+ res.end("ok");
29
+ return true;
30
+ } catch (error) {
31
+ console.error("[iPad WeChat] Webhook error:", error);
32
+ res.statusCode = 500;
33
+ res.end("Internal error");
34
+ return true;
35
+ }
36
+ },
37
+ });
38
+ },
39
+ });
@@ -0,0 +1,91 @@
1
+ {
2
+ "$schema": "https://openclaw.ai/schema/plugin.json",
3
+ "id": "ipad-wechat",
4
+ "name": "iPad WeChat",
5
+ "version": "1.0.0",
6
+ "description": "Connect OpenClaw to iPad WeChat protocol for sending and receiving WeChat messages",
7
+ "author": {
8
+ "name": "gloablehive",
9
+ "url": "https://github.com/gloablehive"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/gloablehive/channels/ipad-wechat-plugin"
14
+ },
15
+ "keywords": ["openclaw", "channel", "wechat", "ipad", "messaging"],
16
+ "license": "MIT",
17
+ "type": "channel",
18
+ "capabilities": {
19
+ "inbound": {
20
+ "message": true,
21
+ "friend_request": true,
22
+ "group_invite": true
23
+ },
24
+ "outbound": {
25
+ "text": true,
26
+ "media": true,
27
+ "link": true,
28
+ "location": true,
29
+ "contact": true
30
+ },
31
+ "features": {
32
+ "pairing": false,
33
+ "reactions": false,
34
+ "editing": false,
35
+ "deleting": false,
36
+ "threads": true
37
+ }
38
+ },
39
+ "configSchema": {
40
+ "type": "object",
41
+ "properties": {
42
+ "apiKey": {
43
+ "type": "string",
44
+ "description": "iPad WeChat API key for authentication",
45
+ "secret": true,
46
+ "required": true
47
+ },
48
+ "baseUrl": {
49
+ "type": "string",
50
+ "description": "iPad WeChat API base URL"
51
+ },
52
+ "accountId": {
53
+ "type": "string",
54
+ "description": "Internal account ID for routing"
55
+ },
56
+ "wechatAccountId": {
57
+ "type": "string",
58
+ "description": "WeChat account ID in the system"
59
+ },
60
+ "wechatId": {
61
+ "type": "string",
62
+ "description": "WeChat ID (wxid_xxx)"
63
+ },
64
+ "nickName": {
65
+ "type": "string",
66
+ "description": "Display name for this WeChat account"
67
+ },
68
+ "allowFrom": {
69
+ "type": "array",
70
+ "items": { "type": "string" },
71
+ "description": "List of WeChat IDs that can DM the agent",
72
+ "default": []
73
+ },
74
+ "dmSecurity": {
75
+ "type": "string",
76
+ "enum": ["allowlist", "blocklist", "allowall"],
77
+ "description": "DM security policy",
78
+ "default": "allowlist"
79
+ }
80
+ },
81
+ "required": ["apiKey", "baseUrl"]
82
+ },
83
+ "permissions": {
84
+ "channels": ["ipad-wechat"],
85
+ "http": {
86
+ "outbound": ["*"],
87
+ "inbound": ["/ipad-wechat/webhook"]
88
+ }
89
+ },
90
+ "apiVersion": "1.0.0"
91
+ }
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@gloablehive/ipad-wechat-plugin",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "OpenClaw channel plugin for iPad WeChat protocol - enables sending/receiving WeChat messages through iPad protocol",
6
+ "main": "index.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "test": "npx tsx test-ipad.ts",
10
+ "dev": "tsx watch index.ts"
11
+ },
12
+ "openclaw": {
13
+ "extensions": [
14
+ "./index.ts"
15
+ ],
16
+ "channel": {
17
+ "id": "ipad-wechat",
18
+ "label": "iPad WeChat",
19
+ "blurb": "Connect OpenClaw to iPad WeChat protocol for sending and receiving WeChat messages"
20
+ }
21
+ },
22
+ "dependencies": {
23
+ "@gloablehive/wechat-cache": "^1.0.0",
24
+ "openclaw": ">=1.0.0"
25
+ },
26
+ "devDependencies": {
27
+ "@types/node": "^20.0.0",
28
+ "typescript": "^5.0.0"
29
+ }
30
+ }
package/src/channel.ts ADDED
@@ -0,0 +1,250 @@
1
+ /**
2
+ * iPad WeChat Channel Plugin for OpenClaw
3
+ *
4
+ * Uses iPad protocol (different from phone/WorkPhone protocol)
5
+ * API documentation: wechat/api.md
6
+ *
7
+ * Includes Local Cache (shared with celphone-wechat-plugin):
8
+ * - Per-account, per-user/conversation MD files
9
+ * - YAML frontmatter (aligned with Claude Code)
10
+ * - MEMORY.md indexing
11
+ * - 4-layer compression
12
+ * - AI summary extraction
13
+ * - SAAS connectivity + offline fallback
14
+ * - Cloud sync
15
+ */
16
+
17
+ import {
18
+ createChatChannelPlugin,
19
+ createChannelPluginBase,
20
+ } from "openclaw/plugin-sdk/core";
21
+ import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
22
+ import { createIPadClient, type WebhookPayload } from "./client.js";
23
+
24
+ // Import cache modules from shared package
25
+ import {
26
+ createCacheManager,
27
+ CacheManager,
28
+ WeChatAccount,
29
+ WeChatMessage,
30
+ } from "@gloablehive/wechat-cache";
31
+
32
+ // Cache manager instance (lazy initialized)
33
+ let cacheManager: CacheManager | null = null;
34
+
35
+ /**
36
+ * Get or create cache manager
37
+ */
38
+ function getCacheManager(cfg: OpenClawConfig): CacheManager {
39
+ if (cacheManager) return cacheManager;
40
+
41
+ const section = (cfg.channels as Record<string, any>)?.["ipad-wechat"];
42
+ const accounts = (section?.accounts || []) as WeChatAccount[];
43
+
44
+ // If no accounts configured, create default from main config
45
+ if (accounts.length === 0 && section?.wechatAccountId) {
46
+ accounts.push({
47
+ accountId: section.accountId || "default",
48
+ wechatAccountId: section.wechatAccountId,
49
+ wechatId: section.wechatId || "",
50
+ nickName: section.nickName || "WeChat User",
51
+ enabled: true,
52
+ });
53
+ }
54
+
55
+ const basePath = section?.cachePath || "./cache/wechat-ipad";
56
+
57
+ cacheManager = createCacheManager({
58
+ basePath,
59
+ accounts,
60
+ });
61
+
62
+ return cacheManager;
63
+ }
64
+
65
+ export interface IPadWeChatResolvedAccount {
66
+ accountId: string | null;
67
+ apiKey: string;
68
+ baseUrl: string;
69
+ wechatAccountId: string;
70
+ wechatId: string;
71
+ nickName: string;
72
+ allowFrom: string[];
73
+ dmPolicy: string | undefined;
74
+ }
75
+
76
+ function resolveAccount(
77
+ cfg: OpenClawConfig,
78
+ accountId?: string | null
79
+ ): IPadWeChatResolvedAccount {
80
+ const section = (cfg.channels as Record<string, any>)?.["ipad-wechat"];
81
+ const apiKey = section?.apiKey;
82
+ const baseUrl = section?.baseUrl || "https://api.example.com";
83
+
84
+ if (!apiKey) {
85
+ throw new Error("ipad-wechat: apiKey is required");
86
+ }
87
+
88
+ return {
89
+ accountId: accountId ?? null,
90
+ apiKey,
91
+ baseUrl,
92
+ wechatAccountId: section?.wechatAccountId || "",
93
+ wechatId: section?.wechatId || "",
94
+ nickName: section?.nickName || "WeChat User",
95
+ allowFrom: section?.allowFrom ?? [],
96
+ dmPolicy: section?.dmSecurity,
97
+ };
98
+ }
99
+
100
+ export const ipadWeChatPlugin = createChatChannelPlugin<IPadWeChatResolvedAccount>({
101
+ base: createChannelPluginBase({
102
+ id: "ipad-wechat",
103
+ setup: {
104
+ resolveAccount,
105
+ inspectAccount(cfg, accountId) {
106
+ const section = (cfg.channels as Record<string, any>)?.["ipad-wechat"];
107
+ const hasApiKey = Boolean(section?.apiKey);
108
+ return {
109
+ enabled: hasApiKey,
110
+ configured: hasApiKey,
111
+ tokenStatus: hasApiKey ? "available" : "missing",
112
+ };
113
+ },
114
+ },
115
+ }),
116
+
117
+ security: {
118
+ dm: {
119
+ channelKey: "ipad-wechat",
120
+ resolvePolicy: (account) => account.dmPolicy,
121
+ resolveAllowFrom: (account) => account.allowFrom,
122
+ defaultPolicy: "allowlist",
123
+ },
124
+ },
125
+
126
+ threading: {
127
+ topLevelReplyToMode: "reply",
128
+ },
129
+
130
+ outbound: {
131
+ attachedResults: {
132
+ sendText: async (params) => {
133
+ const client = createIPadClient({
134
+ baseUrl: params.account.baseUrl,
135
+ apiKey: params.account.apiKey,
136
+ });
137
+
138
+ // Check if DM or group
139
+ const isChatroom = params.to?.includes("@chatroom");
140
+
141
+ if (isChatroom) {
142
+ const result = await client.sendRoomMessage({
143
+ roomId: params.to,
144
+ content: params.text,
145
+ });
146
+ return { messageId: result.messageId };
147
+ } else {
148
+ const result = await client.sendFriendMessage({
149
+ friendWechatId: params.to,
150
+ content: params.text,
151
+ });
152
+ return { messageId: result.messageId };
153
+ }
154
+ },
155
+
156
+ sendMedia: async (params) => {
157
+ const client = createIPadClient({
158
+ baseUrl: params.account.baseUrl,
159
+ apiKey: params.account.apiKey,
160
+ });
161
+
162
+ const isChatroom = params.to?.includes("@chatroom");
163
+
164
+ if (isChatroom) {
165
+ const result = await client.sendRoomMedia({
166
+ roomId: params.to,
167
+ filePath: params.filePath || "",
168
+ });
169
+ return { messageId: result.messageId };
170
+ } else {
171
+ const result = await client.sendFriendMedia({
172
+ friendWechatId: params.to,
173
+ filePath: params.filePath || "",
174
+ });
175
+ return { messageId: result.messageId };
176
+ }
177
+ },
178
+ },
179
+ },
180
+
181
+ capabilities: {
182
+ supportedMessageTypes: ["text", "image", "video", "file", "link", "location", "contact"],
183
+ maxAttachmentSize: 25 * 1024 * 1024,
184
+ supportsMarkdown: false,
185
+ supportsHtml: false,
186
+ supportsEmoji: true,
187
+ supportsReactions: false,
188
+ supportsThreads: true,
189
+ supportsEditing: false,
190
+ supportsDeleting: false,
191
+ },
192
+ });
193
+
194
+ export async function handleInboundMessage(
195
+ api: any,
196
+ payload: WebhookPayload,
197
+ cfg?: OpenClawConfig
198
+ ): Promise<void> {
199
+ const { event, message } = payload;
200
+
201
+ if (event === "message" && message) {
202
+ const isChatroom = !!(message as any).roomId;
203
+ const conversationId = isChatroom
204
+ ? (message as any).roomId
205
+ : message.fromUser || message.toUser || "";
206
+
207
+ const openclawMessage = {
208
+ id: message.messageId,
209
+ conversation: {
210
+ type: isChatroom ? "group" as const : "dm" as const,
211
+ id: conversationId,
212
+ },
213
+ sender: {
214
+ id: message.fromUser || "",
215
+ },
216
+ content: {
217
+ type: "text",
218
+ text: message.content,
219
+ },
220
+ timestamp: new Date(message.timestamp || Date.now()),
221
+ isSelf: message.isSelf || false,
222
+ };
223
+
224
+ // Write to cache
225
+ if (cfg) {
226
+ try {
227
+ const cache = getCacheManager(cfg);
228
+ const wechatMessage: WeChatMessage = {
229
+ messageId: message.messageId,
230
+ accountId: (cfg.channels as Record<string, any>)?.["ipad-wechat"]?.accountId || "default",
231
+ conversationType: isChatroom ? "chatroom" : "friend",
232
+ conversationId,
233
+ senderId: message.fromUser || "",
234
+ content: message.content,
235
+ messageType: message.type || 1,
236
+ timestamp: message.timestamp || Date.now(),
237
+ isSelf: message.isSelf || false,
238
+ direction: "inbound",
239
+ };
240
+ await cache.onMessage(wechatMessage);
241
+ } catch (error) {
242
+ console.error("[iPad WeChat] Cache error:", error);
243
+ }
244
+ }
245
+
246
+ await api.inbound.dispatchMessage(openclawMessage);
247
+ }
248
+ }
249
+
250
+ export default ipadWeChatPlugin;
package/src/client.ts ADDED
@@ -0,0 +1,272 @@
1
+ /**
2
+ * iPad WeChat API Client
3
+ *
4
+ * Based on wechat/api.md documentation
5
+ * Different from WorkPhone - uses iPad protocol
6
+ */
7
+
8
+ export interface IPadClientConfig {
9
+ baseUrl: string;
10
+ apiKey: string;
11
+ }
12
+
13
+ export interface WebhookPayload {
14
+ event: string;
15
+ message?: {
16
+ messageId: string;
17
+ fromUser: string;
18
+ toUser: string;
19
+ content: string;
20
+ type: number;
21
+ timestamp: number;
22
+ isSelf: boolean;
23
+ roomId?: string;
24
+ };
25
+ }
26
+
27
+ export interface SendTextResponse {
28
+ messageId: string;
29
+ msgSvrId: string;
30
+ }
31
+
32
+ export interface SendMediaResponse {
33
+ messageId: string;
34
+ msgSvrId: string;
35
+ }
36
+
37
+ export interface Contact {
38
+ wechatId: string;
39
+ nickName: string;
40
+ remark: string;
41
+ avatar?: string;
42
+ }
43
+
44
+ export interface RoomInfo {
45
+ roomId: string;
46
+ name: string;
47
+ ownerId: string;
48
+ memberCount: number;
49
+ }
50
+
51
+ function getHeaders(apiKey: string) {
52
+ return {
53
+ "Content-Type": "application/json",
54
+ "Authorization": `Bearer ${apiKey}`,
55
+ };
56
+ }
57
+
58
+ export function createIPadClient(config: IPadClientConfig) {
59
+ const { baseUrl, apiKey } = config;
60
+
61
+ return {
62
+ // ========== 消息发送 ==========
63
+
64
+ async sendFriendMessage(params: {
65
+ friendWechatId: string;
66
+ content: string;
67
+ }): Promise<SendTextResponse> {
68
+ // API: 发送文本消息 - api-316658257
69
+ const response = await fetch(`${baseUrl}/api-316658257`, {
70
+ method: "POST",
71
+ headers: getHeaders(apiKey),
72
+ body: JSON.stringify({
73
+ toUser: params.friendWechatId,
74
+ content: params.content,
75
+ }),
76
+ });
77
+ const data = await response.json();
78
+ return {
79
+ messageId: data.data?.messageId || `msg_${Date.now()}`,
80
+ msgSvrId: data.data?.msgSvrId || "",
81
+ };
82
+ },
83
+
84
+ async sendRoomMessage(params: {
85
+ roomId: string;
86
+ content: string;
87
+ }): Promise<SendTextResponse> {
88
+ // API: 发送群@消息 - api-316658258
89
+ const response = await fetch(`${baseUrl}/api-316658258`, {
90
+ method: "POST",
91
+ headers: getHeaders(apiKey),
92
+ body: JSON.stringify({
93
+ roomId: params.roomId,
94
+ content: params.content,
95
+ }),
96
+ });
97
+ const data = await response.json();
98
+ return {
99
+ messageId: data.data?.messageId || `msg_${Date.now()}`,
100
+ msgSvrId: data.data?.msgSvrId || "",
101
+ };
102
+ },
103
+
104
+ async sendFriendMedia(params: {
105
+ friendWechatId: string;
106
+ filePath: string;
107
+ }): Promise<SendMediaResponse> {
108
+ // API: 发送图片 - api-316658262
109
+ const response = await fetch(`${baseUrl}/api-316658262`, {
110
+ method: "POST",
111
+ headers: getHeaders(apiKey),
112
+ body: JSON.stringify({
113
+ toUser: params.friendWechatId,
114
+ filePath: params.filePath,
115
+ }),
116
+ });
117
+ const data = await response.json();
118
+ return {
119
+ messageId: data.data?.messageId || `msg_${Date.now()}`,
120
+ msgSvrId: data.data?.msgSvrId || "",
121
+ };
122
+ },
123
+
124
+ async sendRoomMedia(params: {
125
+ roomId: string;
126
+ filePath: string;
127
+ }): Promise<SendMediaResponse> {
128
+ // API: 发送图片到群 - 复用图片接口
129
+ const response = await fetch(`${baseUrl}/api-316658262`, {
130
+ method: "POST",
131
+ headers: getHeaders(apiKey),
132
+ body: JSON.stringify({
133
+ roomId: params.roomId,
134
+ filePath: params.filePath,
135
+ }),
136
+ });
137
+ const data = await response.json();
138
+ return {
139
+ messageId: data.data?.messageId || `msg_${Date.now()}`,
140
+ msgSvrId: data.data?.msgSvrId || "",
141
+ };
142
+ },
143
+
144
+ // ========== 联系人管理 ==========
145
+
146
+ async syncContacts(): Promise<Contact[]> {
147
+ // API: 同步联系人 - api-316658212
148
+ const response = await fetch(`${baseUrl}/api-316658212`, {
149
+ method: "POST",
150
+ headers: getHeaders(apiKey),
151
+ body: JSON.stringify({}),
152
+ });
153
+ const data = await response.json();
154
+ return data.data?.contacts || [];
155
+ },
156
+
157
+ async getContactDetail(wechatId: string): Promise<Contact> {
158
+ // API: 获取联系人详细 - api-316658219
159
+ const response = await fetch(`${baseUrl}/api-316658219`, {
160
+ method: "POST",
161
+ headers: getHeaders(apiKey),
162
+ body: JSON.stringify({ userId: wechatId }),
163
+ });
164
+ const data = await response.json();
165
+ return data.data || {};
166
+ },
167
+
168
+ async updateFriendRemark(wechatId: string, remark: string): Promise<void> {
169
+ // API: 修改好友备注 - api-316658224
170
+ await fetch(`${baseUrl}/api-316658224`, {
171
+ method: "POST",
172
+ headers: getHeaders(apiKey),
173
+ body: JSON.stringify({
174
+ userId: wechatId,
175
+ remark,
176
+ }),
177
+ });
178
+ },
179
+
180
+ // ========== 群管理 ==========
181
+
182
+ async getRoomInfo(roomId: string): Promise<RoomInfo> {
183
+ // API: 获取群信息 - api-316658229
184
+ const response = await fetch(`${baseUrl}/api-316658229`, {
185
+ method: "POST",
186
+ headers: getHeaders(apiKey),
187
+ body: JSON.stringify({ roomId }),
188
+ });
189
+ const data = await response.json();
190
+ return data.data || {};
191
+ },
192
+
193
+ async getRoomMembers(roomId: string): Promise<string[]> {
194
+ // API: 获取群成员详细 - api-316658230
195
+ const response = await fetch(`${baseUrl}/api-316658230`, {
196
+ method: "POST",
197
+ headers: getHeaders(apiKey),
198
+ body: JSON.stringify({ roomId }),
199
+ });
200
+ const data = await response.json();
201
+ return data.data?.members || [];
202
+ },
203
+
204
+ async createRoom(memberIds: string[]): Promise<{ roomId: string }> {
205
+ // API: 创建群组 - api-316658231
206
+ const response = await fetch(`${baseUrl}/api-316658231`, {
207
+ method: "POST",
208
+ headers: getHeaders(apiKey),
209
+ body: JSON.stringify({ memberIds }),
210
+ });
211
+ const data = await response.json();
212
+ return { roomId: data.data?.roomId || "" };
213
+ },
214
+
215
+ async addRoomMember(roomId: string, memberId: string): Promise<void> {
216
+ // API: 添加群成员 - api-316658232
217
+ await fetch(`${baseUrl}/api-316658232`, {
218
+ method: "POST",
219
+ headers: getHeaders(apiKey),
220
+ body: JSON.stringify({ roomId, memberId }),
221
+ });
222
+ },
223
+
224
+ async removeRoomMember(roomId: string, memberId: string): Promise<void> {
225
+ // API: 移除群成员 - api-316658234
226
+ await fetch(`${baseUrl}/api-316658234`, {
227
+ method: "POST",
228
+ headers: getHeaders(apiKey),
229
+ body: JSON.stringify({ roomId, memberId }),
230
+ });
231
+ },
232
+
233
+ // ========== 账号 ==========
234
+
235
+ async getLoginAccountInfo(): Promise<{
236
+ wechatId: string;
237
+ nickName: string;
238
+ avatar: string;
239
+ }> {
240
+ // API: 获取登录账号信息 - api-316658209
241
+ const response = await fetch(`${baseUrl}/api-316658209`, {
242
+ method: "POST",
243
+ headers: getHeaders(apiKey),
244
+ body: JSON.stringify({}),
245
+ });
246
+ const data = await response.json();
247
+ return data.data || {};
248
+ },
249
+
250
+ // ========== 朋友圈 ==========
251
+
252
+ async getFriendMoments(wechatId: string, limit: number = 20): Promise<any[]> {
253
+ // API: 获取好友朋友圈动态 - api-316658314
254
+ const response = await fetch(`${baseUrl}/api-316658314`, {
255
+ method: "POST",
256
+ headers: getHeaders(apiKey),
257
+ body: JSON.stringify({ userId: wechatId, limit }),
258
+ });
259
+ const data = await response.json();
260
+ return data.data?.moments || [];
261
+ },
262
+
263
+ async publishMoment(content: string, images: string[]): Promise<void> {
264
+ // API: 发布朋友圈 - api-316658315
265
+ await fetch(`${baseUrl}/api-316658315`, {
266
+ method: "POST",
267
+ headers: getHeaders(apiKey),
268
+ body: JSON.stringify({ content, images }),
269
+ });
270
+ },
271
+ };
272
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["ES2022"],
7
+ "outDir": "./dist",
8
+ "strict": false,
9
+ "noImplicitAny": false,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true
13
+ },
14
+ "include": ["*.ts", "src/**/*.ts"],
15
+ "exclude": ["node_modules", "dist"]
16
+ }