@gloablehive/ipad-wechat-plugin 1.0.0 → 1.0.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.
- package/dist/index.js +40 -0
- package/dist/openclaw.plugin.json +98 -0
- package/dist/src/channel.js +229 -0
- package/dist/src/client.js +603 -0
- package/index.ts +7 -2
- package/openclaw.plugin.json +12 -5
- package/package.json +3 -2
- package/src/channel.ts +73 -27
- package/src/client.ts +660 -155
- package/test-ipad-real.ts +77 -0
- package/test-ipad.ts +150 -0
- package/tsconfig.json +8 -2
package/dist/index.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
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
|
+
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
|
|
8
|
+
import { ipadWeChatPlugin, handleInboundMessage } from "./src/channel.js";
|
|
9
|
+
export default defineChannelPluginEntry({
|
|
10
|
+
id: "ipad-wechat",
|
|
11
|
+
name: "iPad WeChat",
|
|
12
|
+
description: "Connect OpenClaw to iPad WeChat protocol for sending and receiving WeChat messages",
|
|
13
|
+
plugin: ipadWeChatPlugin,
|
|
14
|
+
registerFull(api) {
|
|
15
|
+
// Register webhook endpoint for receiving messages
|
|
16
|
+
api.registerHttpRoute({
|
|
17
|
+
path: "/ipad-wechat/webhook",
|
|
18
|
+
auth: "plugin",
|
|
19
|
+
handler: async (req, res) => {
|
|
20
|
+
try {
|
|
21
|
+
const payload = req.body;
|
|
22
|
+
// Handle inbound message
|
|
23
|
+
console.log("[iPad WeChat] Received webhook:", payload);
|
|
24
|
+
if (payload) {
|
|
25
|
+
await handleInboundMessage(api, payload);
|
|
26
|
+
}
|
|
27
|
+
res.statusCode = 200;
|
|
28
|
+
res.end("ok");
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
console.error("[iPad WeChat] Webhook error:", error);
|
|
33
|
+
res.statusCode = 500;
|
|
34
|
+
res.end("Internal error");
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
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
|
+
"appKey": {
|
|
43
|
+
"type": "string",
|
|
44
|
+
"description": "JuHeBot App Key (聚合聊天开放平台)",
|
|
45
|
+
"secret": true,
|
|
46
|
+
"required": true
|
|
47
|
+
},
|
|
48
|
+
"appSecret": {
|
|
49
|
+
"type": "string",
|
|
50
|
+
"description": "JuHeBot App Secret",
|
|
51
|
+
"secret": true,
|
|
52
|
+
"required": true
|
|
53
|
+
},
|
|
54
|
+
"guid": {
|
|
55
|
+
"type": "string",
|
|
56
|
+
"description": "实例/设备 ID (Guid)",
|
|
57
|
+
"required": true
|
|
58
|
+
},
|
|
59
|
+
"accountId": {
|
|
60
|
+
"type": "string",
|
|
61
|
+
"description": "Internal account ID for routing"
|
|
62
|
+
},
|
|
63
|
+
"wechatAccountId": {
|
|
64
|
+
"type": "string",
|
|
65
|
+
"description": "WeChat account ID in the system"
|
|
66
|
+
},
|
|
67
|
+
"wechatId": {
|
|
68
|
+
"type": "string",
|
|
69
|
+
"description": "WeChat ID (wxid_xxx)"
|
|
70
|
+
},
|
|
71
|
+
"nickName": {
|
|
72
|
+
"type": "string",
|
|
73
|
+
"description": "Display name for this WeChat account"
|
|
74
|
+
},
|
|
75
|
+
"allowFrom": {
|
|
76
|
+
"type": "array",
|
|
77
|
+
"items": { "type": "string" },
|
|
78
|
+
"description": "List of WeChat IDs that can DM the agent",
|
|
79
|
+
"default": []
|
|
80
|
+
},
|
|
81
|
+
"dmSecurity": {
|
|
82
|
+
"type": "string",
|
|
83
|
+
"enum": ["allowlist", "blocklist", "allowall"],
|
|
84
|
+
"description": "DM security policy",
|
|
85
|
+
"default": "allowlist"
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
"required": ["appKey", "appSecret", "guid"]
|
|
89
|
+
},
|
|
90
|
+
"permissions": {
|
|
91
|
+
"channels": ["ipad-wechat"],
|
|
92
|
+
"http": {
|
|
93
|
+
"outbound": ["*"],
|
|
94
|
+
"inbound": ["/ipad-wechat/webhook"]
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
"apiVersion": "1.0.0"
|
|
98
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
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
|
+
import { createChatChannelPlugin, createChannelPluginBase, } from "openclaw/plugin-sdk/core";
|
|
17
|
+
import { createIPadClient } from "./client.js";
|
|
18
|
+
// Import cache modules from shared package
|
|
19
|
+
import { createCacheManager, } from "@gloablehive/wechat-cache";
|
|
20
|
+
// Cache manager instance (lazy initialized)
|
|
21
|
+
let cacheManager = null;
|
|
22
|
+
/**
|
|
23
|
+
* Get or create cache manager
|
|
24
|
+
*/
|
|
25
|
+
function getCacheManager(cfg) {
|
|
26
|
+
if (cacheManager)
|
|
27
|
+
return cacheManager;
|
|
28
|
+
const section = cfg.channels?.["ipad-wechat"];
|
|
29
|
+
const accounts = (section?.accounts || []);
|
|
30
|
+
// If no accounts configured, create default from main config
|
|
31
|
+
if (accounts.length === 0 && section?.wechatAccountId) {
|
|
32
|
+
accounts.push({
|
|
33
|
+
accountId: section.accountId || "default",
|
|
34
|
+
wechatAccountId: section.wechatAccountId,
|
|
35
|
+
wechatId: section.wechatId || "",
|
|
36
|
+
nickName: section.nickName || "WeChat User",
|
|
37
|
+
enabled: true,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
const basePath = section?.cachePath || "./cache/wechat-ipad";
|
|
41
|
+
cacheManager = createCacheManager({
|
|
42
|
+
basePath,
|
|
43
|
+
accounts,
|
|
44
|
+
});
|
|
45
|
+
return cacheManager;
|
|
46
|
+
}
|
|
47
|
+
function resolveAccount(cfg, accountId) {
|
|
48
|
+
const section = cfg.channels?.["ipad-wechat"];
|
|
49
|
+
const appKey = section?.appKey;
|
|
50
|
+
const appSecret = section?.appSecret;
|
|
51
|
+
const guid = section?.guid;
|
|
52
|
+
if (!appKey || !appSecret || !guid) {
|
|
53
|
+
throw new Error("ipad-wechat: appKey, appSecret, and guid are required");
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
accountId: accountId ?? null,
|
|
57
|
+
appKey,
|
|
58
|
+
appSecret,
|
|
59
|
+
guid,
|
|
60
|
+
wechatAccountId: section?.wechatAccountId || "",
|
|
61
|
+
wechatId: section?.wechatId || "",
|
|
62
|
+
nickName: section?.nickName || "WeChat User",
|
|
63
|
+
allowFrom: section?.allowFrom ?? [],
|
|
64
|
+
dmPolicy: section?.dmSecurity,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export const ipadWeChatPlugin = createChatChannelPlugin({
|
|
68
|
+
base: createChannelPluginBase({
|
|
69
|
+
id: "ipad-wechat",
|
|
70
|
+
setup: {
|
|
71
|
+
resolveAccount,
|
|
72
|
+
inspectAccount(cfg, accountId) {
|
|
73
|
+
const section = cfg.channels?.["ipad-wechat"];
|
|
74
|
+
const hasCredentials = Boolean(section?.appKey && section?.appSecret && section?.guid);
|
|
75
|
+
return {
|
|
76
|
+
enabled: hasCredentials,
|
|
77
|
+
configured: hasCredentials,
|
|
78
|
+
tokenStatus: hasCredentials ? "available" : "missing",
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
}),
|
|
83
|
+
security: {
|
|
84
|
+
dm: {
|
|
85
|
+
channelKey: "ipad-wechat",
|
|
86
|
+
resolvePolicy: (account) => account.dmPolicy,
|
|
87
|
+
resolveAllowFrom: (account) => account.allowFrom,
|
|
88
|
+
defaultPolicy: "allowlist",
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
threading: {
|
|
92
|
+
topLevelReplyToMode: "reply",
|
|
93
|
+
},
|
|
94
|
+
outbound: {
|
|
95
|
+
attachedResults: {
|
|
96
|
+
sendText: async (params) => {
|
|
97
|
+
// Get the resolved account from params
|
|
98
|
+
const cfg = params.cfg;
|
|
99
|
+
const section = cfg.channels?.["ipad-wechat"];
|
|
100
|
+
const client = createIPadClient({
|
|
101
|
+
appKey: section?.appKey,
|
|
102
|
+
appSecret: section?.appSecret,
|
|
103
|
+
guid: section?.guid,
|
|
104
|
+
});
|
|
105
|
+
// Check if DM or group
|
|
106
|
+
const isChatroom = params.to?.includes("@chatroom");
|
|
107
|
+
if (isChatroom) {
|
|
108
|
+
const result = await client.sendRoomMessage({
|
|
109
|
+
roomId: params.to,
|
|
110
|
+
content: params.text,
|
|
111
|
+
});
|
|
112
|
+
return { messageId: result.messageId };
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
const result = await client.sendFriendMessage({
|
|
116
|
+
friendWechatId: params.to,
|
|
117
|
+
content: params.text,
|
|
118
|
+
});
|
|
119
|
+
return { messageId: result.messageId };
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
sendMedia: async (params) => {
|
|
123
|
+
const cfg = params.cfg;
|
|
124
|
+
const section = cfg.channels?.["ipad-wechat"];
|
|
125
|
+
const client = createIPadClient({
|
|
126
|
+
appKey: section?.appKey,
|
|
127
|
+
appSecret: section?.appSecret,
|
|
128
|
+
guid: section?.guid,
|
|
129
|
+
});
|
|
130
|
+
const isChatroom = params.to?.includes("@chatroom");
|
|
131
|
+
// Get file path from mediaUrl or mediaReadFile
|
|
132
|
+
let filePath = "";
|
|
133
|
+
if (params.mediaUrl) {
|
|
134
|
+
filePath = params.mediaUrl;
|
|
135
|
+
}
|
|
136
|
+
else if (params.mediaReadFile) {
|
|
137
|
+
// Read file from local roots
|
|
138
|
+
const roots = params.mediaLocalRoots || ["./"];
|
|
139
|
+
for (const root of roots) {
|
|
140
|
+
try {
|
|
141
|
+
const fullPath = `${root}/${filePath}`;
|
|
142
|
+
const buffer = await params.mediaReadFile(fullPath);
|
|
143
|
+
// In a real implementation, we'd upload the file first
|
|
144
|
+
filePath = fullPath;
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// Try next root
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (isChatroom) {
|
|
153
|
+
const result = await client.sendRoomMedia({
|
|
154
|
+
roomId: params.to,
|
|
155
|
+
filePath: filePath,
|
|
156
|
+
});
|
|
157
|
+
return { messageId: result.messageId };
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
const result = await client.sendFriendMedia({
|
|
161
|
+
friendWechatId: params.to,
|
|
162
|
+
filePath: filePath,
|
|
163
|
+
});
|
|
164
|
+
return { messageId: result.messageId };
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
capabilities: {
|
|
170
|
+
supportedMessageTypes: ["text", "image", "video", "file", "link", "location", "contact"],
|
|
171
|
+
maxAttachmentSize: 25 * 1024 * 1024,
|
|
172
|
+
supportsMarkdown: false,
|
|
173
|
+
supportsHtml: false,
|
|
174
|
+
supportsEmoji: true,
|
|
175
|
+
supportsReactions: false,
|
|
176
|
+
supportsThreads: true,
|
|
177
|
+
supportsEditing: false,
|
|
178
|
+
supportsDeleting: false,
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
export async function handleInboundMessage(api, payload, cfg) {
|
|
182
|
+
const { event, message } = payload;
|
|
183
|
+
if (event === "message" && message) {
|
|
184
|
+
const isChatroom = !!message.roomId;
|
|
185
|
+
const conversationId = isChatroom
|
|
186
|
+
? message.roomId
|
|
187
|
+
: message.fromUser || message.toUser || "";
|
|
188
|
+
const openclawMessage = {
|
|
189
|
+
id: message.messageId,
|
|
190
|
+
conversation: {
|
|
191
|
+
type: isChatroom ? "group" : "dm",
|
|
192
|
+
id: conversationId,
|
|
193
|
+
},
|
|
194
|
+
sender: {
|
|
195
|
+
id: message.fromUser || "",
|
|
196
|
+
},
|
|
197
|
+
content: {
|
|
198
|
+
type: "text",
|
|
199
|
+
text: message.content,
|
|
200
|
+
},
|
|
201
|
+
timestamp: new Date(message.timestamp || Date.now()),
|
|
202
|
+
isSelf: message.isSelf || false,
|
|
203
|
+
};
|
|
204
|
+
// Write to cache
|
|
205
|
+
if (cfg) {
|
|
206
|
+
try {
|
|
207
|
+
const cache = getCacheManager(cfg);
|
|
208
|
+
const wechatMessage = {
|
|
209
|
+
messageId: message.messageId,
|
|
210
|
+
accountId: cfg.channels?.["ipad-wechat"]?.accountId || "default",
|
|
211
|
+
conversationType: isChatroom ? "chatroom" : "friend",
|
|
212
|
+
conversationId,
|
|
213
|
+
senderId: message.fromUser || "",
|
|
214
|
+
content: message.content,
|
|
215
|
+
messageType: message.type || 1,
|
|
216
|
+
timestamp: message.timestamp || Date.now(),
|
|
217
|
+
isSelf: message.isSelf || false,
|
|
218
|
+
direction: "inbound",
|
|
219
|
+
};
|
|
220
|
+
await cache.onMessage(wechatMessage);
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
console.error("[iPad WeChat] Cache error:", error);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
await api.inbound.dispatchMessage(openclawMessage);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
export default ipadWeChatPlugin;
|