@gloablehive/ipad-wechat-plugin 1.0.26 → 2.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/README.md ADDED
@@ -0,0 +1,135 @@
1
+ # @gloablehive/ipad-wechat-plugin
2
+
3
+ OpenClaw channel plugin for iPad WeChat protocol — enables sending and receiving WeChat messages through the JuHeBot API.
4
+
5
+ ## Features
6
+
7
+ - **Inbound**: Receive WeChat messages via JuHeBot webhook callbacks
8
+ - **Outbound**: Auto-reply via agent → JuHeBot `send_text` API
9
+ - **Multi-account**: Support multiple WeChat accounts via config
10
+ - **Deduplication**: Prevents duplicate message processing (msg_id TTL)
11
+ - **Chatroom support**: Direct messages and group chats
12
+ - **Message caching**: Integrated with `@gloablehive/wechat-cache`
13
+
14
+ ## Architecture
15
+
16
+ ```
17
+ WeChat User
18
+ ↓ (message)
19
+ JuHeBot Cloud (notify_type=1010)
20
+ ↓ (HTTP POST callback)
21
+ Cloudflare Tunnel / Public IP
22
+
23
+ Standalone HTTP Server (:18790)
24
+ ↓ transformPayload() + dedup
25
+ Gateway WebSocket (chat.send → chat.history polling)
26
+
27
+ OpenClaw Agent (processes message)
28
+ ↓ (agent reply detected via polling)
29
+ JuHeBot API (/msg/send_text, to_username)
30
+
31
+ WeChat User (receives reply)
32
+ ```
33
+
34
+ ## Setup
35
+
36
+ ### 1. Install
37
+
38
+ ```bash
39
+ npm install @gloablehive/ipad-wechat-plugin
40
+ ```
41
+
42
+ ### 2. Configure OpenClaw
43
+
44
+ Add to your `openclaw.json` under `plugins.entries`:
45
+
46
+ ```json
47
+ {
48
+ "ipad-wechat": {
49
+ "config": {
50
+ "appKey": "<your-juhebot-app-key>",
51
+ "appSecret": "<your-juhebot-app-secret>",
52
+ "guid": "<your-instance-guid>"
53
+ }
54
+ }
55
+ }
56
+ ```
57
+
58
+ ### 3. Set JuHeBot Notify URL
59
+
60
+ After the plugin starts (webhook server on port 18790), set the callback URL:
61
+
62
+ ```bash
63
+ curl -X POST "https://chat-api.juhebot.com/open/GuidRequest" \
64
+ -H "Content-Type: application/json" \
65
+ -d '{
66
+ "app_key": "<appKey>",
67
+ "app_secret": "<appSecret>",
68
+ "path": "/client/set_notify_url",
69
+ "data": {
70
+ "guid": "<guid>",
71
+ "notify_url": "https://<your-public-url>/"
72
+ }
73
+ }'
74
+ ```
75
+
76
+ > Use a Cloudflare tunnel (`cloudflared tunnel --url http://127.0.0.1:18790`) if you don't have a public IP.
77
+
78
+ ### 4. Environment Variables
79
+
80
+ | Variable | Default | Description |
81
+ |---|---|---|
82
+ | `IPAD_WECHAT_WEBHOOK_PORT` | `18790` | Standalone webhook server port |
83
+ | `OPENCLAW_GATEWAY_PORT` | `18789` | OpenClaw gateway WS port |
84
+
85
+ ## JuHeBot API Notes
86
+
87
+ All JuHeBot API parameters use **snake_case**:
88
+
89
+ | Method | Path | Key Params |
90
+ |---|---|---|
91
+ | Send text | `/msg/send_text` | `to_username`, `content` |
92
+ | Send room @ | `/msg/send_room_at` | `to_username`, `content`, `at_list` |
93
+ | Set notify URL | `/client/set_notify_url` | `notify_url` |
94
+
95
+ ### Callback Format (notify_type=1010)
96
+
97
+ ```json
98
+ {
99
+ "guid": "...",
100
+ "notify_type": 1010,
101
+ "data": {
102
+ "from_username": "wxid_xxx",
103
+ "to_username": "wxid_yyy",
104
+ "desc": "NickName : message content",
105
+ "msg_id": "123456",
106
+ "msg_type": 1,
107
+ "is_chatroom_msg": 0,
108
+ "chatroom": ""
109
+ }
110
+ }
111
+ ```
112
+
113
+ > Note: There is **no `content` field** — text is extracted from `desc` by stripping the `"NickName : "` prefix.
114
+
115
+ ## Version History
116
+
117
+ ### 2.0.0
118
+
119
+ - Full end-to-end inbound + outbound message flow
120
+ - Real JuHeBot callback format support (`desc` field parsing)
121
+ - Fixed all API params to snake_case (`to_username`, `notify_url`)
122
+ - Message deduplication by `msg_id`
123
+ - Chatroom field mapping (`is_chatroom_msg` + `chatroom`)
124
+ - Gateway WS polling strategy for agent reply detection
125
+ - Short-lived WS connections per RPC call
126
+
127
+ ### 1.x
128
+
129
+ - Initial implementation with webhook receiver
130
+ - Cache integration
131
+ - Multi-account scaffolding
132
+
133
+ ## License
134
+
135
+ MIT
package/dist/index.js CHANGED
@@ -21,6 +21,7 @@ import { randomUUID } from "crypto";
21
21
  import WebSocket from "ws";
22
22
  import { defineChannelPluginEntry, buildChannelOutboundSessionRoute } from "openclaw/plugin-sdk/core";
23
23
  import { ipadWeChatPlugin, handleInboundMessage, getCacheManagerReady } from "./src/channel.js";
24
+ import { getIPadClient } from "./src/client-pool.js";
24
25
  /** Read OpenClaw main config from disk so standalone server has full cfg */
25
26
  let _cfgCache = null;
26
27
  function loadOpenClawConfig() {
@@ -39,73 +40,188 @@ function loadOpenClawConfig() {
39
40
  // ── Gateway WebSocket dispatch ──
40
41
  const GATEWAY_PORT = parseInt(process.env.OPENCLAW_GATEWAY_PORT || "18789", 10);
41
42
  let _gwReqId = 0;
42
- function dispatchViaGateway(params) {
43
- const cfg = loadOpenClawConfig();
44
- const token = cfg?.gateway?.controlUi?.auth?.token || cfg?.gateway?.auth?.token || "";
45
- return new Promise((resolve, reject) => {
46
- const url = `ws://127.0.0.1:${GATEWAY_PORT}`;
47
- const ws = new WebSocket(url, { headers: { Origin: `http://127.0.0.1:${GATEWAY_PORT}` } });
48
- const timeout = setTimeout(() => { ws.close(); reject(new Error("Gateway dispatch timeout")); }, 30000);
49
- let connected = false;
50
- let sendId = null;
51
- ws.on("open", () => {
52
- const connectId = `ipad-wechat-${++_gwReqId}`;
53
- ws.send(JSON.stringify({
54
- type: "req", id: connectId, method: "connect",
55
- params: {
56
- minProtocol: 3, maxProtocol: 3,
57
- client: { id: "openclaw-control-ui", version: "1.0.0", mode: "webchat", platform: "node" },
58
- scopes: ["operator.admin", "operator.read", "operator.write"],
59
- ...(token ? { auth: { token } } : {}),
60
- },
61
- }));
43
+ /**
44
+ * Resolve JuHeBot credentials from OpenClaw config and send a text message
45
+ * to a WeChat user/room via the JuHeBot API.
46
+ */
47
+ async function sendToWeChat(cfg, toUser, text, accountId) {
48
+ const entry = cfg?.plugins?.entries?.["ipad-wechat"] || {};
49
+ const section = entry.config || cfg?.channels?.["ipad-wechat"] || {};
50
+ const accounts = (section.accounts || []);
51
+ const account = accounts.find((a) => a.accountId === (accountId || "default")) || accounts[0] || {};
52
+ const appKey = account.appKey || section.appKey || "";
53
+ const appSecret = account.appSecret || section.appSecret || "";
54
+ const guid = account.guid || section.guid || "";
55
+ if (!appKey || !appSecret || !guid) {
56
+ throw new Error("Missing JuHeBot credentials (appKey/appSecret/guid)");
57
+ }
58
+ const client = getIPadClient(account.accountId || "default", { appKey, appSecret, guid });
59
+ const isChatroom = toUser.includes("@chatroom");
60
+ if (isChatroom) {
61
+ await client.sendRoomMessage({ roomId: toUser, content: text });
62
+ }
63
+ else {
64
+ await client.sendFriendMessage({ friendWechatId: toUser, content: text });
65
+ }
66
+ }
67
+ /**
68
+ * Extract text content from a gateway chat event message object.
69
+ */
70
+ function extractReplyText(message) {
71
+ if (!message?.content)
72
+ return "";
73
+ if (typeof message.content === "string")
74
+ return message.content;
75
+ if (Array.isArray(message.content)) {
76
+ return message.content
77
+ .filter((c) => c.type === "text")
78
+ .map((c) => c.text || "")
79
+ .join("\n");
80
+ }
81
+ return "";
82
+ }
83
+ /**
84
+ * Open a short-lived WS to the gateway, run a single RPC call, and return the result.
85
+ */
86
+ async function gwRpc(token, method, reqParams) {
87
+ const url = `ws://127.0.0.1:${GATEWAY_PORT}`;
88
+ const ws = new WebSocket(url, { headers: { Origin: `http://127.0.0.1:${GATEWAY_PORT}` } });
89
+ await new Promise((resolve, reject) => { ws.on("open", resolve); ws.on("error", reject); });
90
+ try {
91
+ // Handshake
92
+ const connectRes = await rpcCall(ws, "connect", {
93
+ minProtocol: 3, maxProtocol: 3,
94
+ client: { id: "openclaw-control-ui", version: "1.0.0", mode: "webchat", platform: "node" },
95
+ scopes: ["operator.admin", "operator.read", "operator.write"],
96
+ ...(token ? { auth: { token } } : {}),
62
97
  });
63
- ws.on("message", (data) => {
98
+ if (!connectRes)
99
+ throw new Error("Gateway connect failed");
100
+ // Execute the method
101
+ return await rpcCall(ws, method, reqParams);
102
+ }
103
+ finally {
104
+ ws.close();
105
+ }
106
+ }
107
+ /**
108
+ * Send a single WS RPC request and wait for the response (with 30s timeout).
109
+ */
110
+ function rpcCall(ws, method, reqParams) {
111
+ return new Promise((resolve, reject) => {
112
+ const id = `ipad-wechat-${++_gwReqId}`;
113
+ const timer = setTimeout(() => { ws.off("message", handler); reject(new Error(`${method} timed out`)); }, 30000);
114
+ const handler = (data) => {
64
115
  try {
65
116
  const frame = JSON.parse(data.toString());
66
- if (frame.type === "res" && !connected) {
67
- connected = true;
68
- if (!frame.ok) {
69
- clearTimeout(timeout);
70
- ws.close();
71
- reject(new Error("Gateway connect failed: " + (frame.error?.message || "unknown")));
72
- return;
73
- }
74
- sendId = `ipad-wechat-${++_gwReqId}`;
75
- ws.send(JSON.stringify({
76
- type: "req", id: sendId, method: "chat.send",
77
- params: {
78
- idempotencyKey: randomUUID(),
79
- sessionKey: params.sessionKey,
80
- message: params.message,
81
- deliver: true,
82
- originatingChannel: "ipad-wechat",
83
- originatingTo: params.from,
84
- originatingAccountId: params.accountId || "default",
85
- },
86
- }));
87
- }
88
- else if (frame.type === "res" && frame.id === sendId) {
89
- clearTimeout(timeout);
90
- ws.close();
91
- if (frame.ok) {
92
- resolve();
93
- }
94
- else {
95
- reject(new Error("chat.send failed: " + (frame.error?.message || "unknown")));
96
- }
117
+ if (frame.type === "res" && frame.id === id) {
118
+ clearTimeout(timer);
119
+ ws.off("message", handler);
120
+ if (frame.ok)
121
+ resolve(frame.payload ?? frame);
122
+ else
123
+ reject(new Error(`${method} failed: ${frame.error?.message || "unknown"}`));
97
124
  }
98
125
  }
99
- catch { /* ignore parse errors */ }
100
- });
101
- ws.on("error", (err) => { clearTimeout(timeout); reject(err); });
102
- ws.on("close", () => { clearTimeout(timeout); if (!connected)
103
- reject(new Error("Gateway closed before connect")); });
126
+ catch { /* ignore non-matching frames */ }
127
+ };
128
+ ws.on("message", handler);
129
+ ws.send(JSON.stringify({ type: "req", id, method, params: reqParams }));
104
130
  });
105
131
  }
132
+ /**
133
+ * Dispatch an inbound message to the agent via the Gateway WebSocket protocol.
134
+ *
135
+ * Strategy:
136
+ * 1. Fire chat.send via a short-lived WS connection (triggers agent)
137
+ * 2. Poll chat.history via separate WS connections to detect the reply
138
+ * 3. Extract reply text, strip NO_REPLY token
139
+ * 4. If there is content, call JuHeBot API to send to WeChat
140
+ */
141
+ async function dispatchViaGateway(params) {
142
+ const cfg = loadOpenClawConfig();
143
+ const token = cfg?.gateway?.controlUi?.auth?.token || cfg?.gateway?.auth?.token || "";
144
+ // Step 1: Get baseline message count
145
+ const baselineHistory = await gwRpc(token, "chat.history", {
146
+ sessionKey: params.sessionKey, limit: 50,
147
+ });
148
+ const baselineCount = Array.isArray(baselineHistory?.messages)
149
+ ? baselineHistory.messages.length : 0;
150
+ console.log(`[iPad WeChat] Baseline history: ${baselineCount} messages`);
151
+ // Step 2: Send user message (triggers agent processing)
152
+ const sendResult = await gwRpc(token, "chat.send", {
153
+ idempotencyKey: randomUUID(),
154
+ sessionKey: params.sessionKey,
155
+ message: params.message,
156
+ deliver: true,
157
+ originatingChannel: "ipad-wechat",
158
+ originatingTo: params.from,
159
+ originatingAccountId: params.accountId || "default",
160
+ });
161
+ console.log(`[iPad WeChat] chat.send accepted, runId=${sendResult?.runId}, polling for reply…`);
162
+ // Step 3: Poll chat.history via fresh connections
163
+ const POLL_INTERVAL = 3000;
164
+ const MAX_POLLS = 40; // 3s × 40 = 120s max
165
+ let replyText = "";
166
+ for (let i = 0; i < MAX_POLLS; i++) {
167
+ await new Promise(r => setTimeout(r, POLL_INTERVAL));
168
+ try {
169
+ const history = await gwRpc(token, "chat.history", {
170
+ sessionKey: params.sessionKey, limit: 50,
171
+ });
172
+ const messages = Array.isArray(history?.messages) ? history.messages : [];
173
+ if (messages.length <= baselineCount)
174
+ continue;
175
+ // Look for new assistant messages after the baseline
176
+ // chat.history returns messages with role/content at top level
177
+ const newMessages = messages.slice(baselineCount);
178
+ const assistantMsg = newMessages.find((m) => m?.role === "assistant" || m?.message?.role === "assistant");
179
+ if (assistantMsg) {
180
+ const rawText = extractReplyText(assistantMsg);
181
+ replyText = rawText.replace(/\s*NO_REPLY\s*/gi, "").trim();
182
+ console.log(`[iPad WeChat] Agent reply found on poll ${i + 1}: "${replyText.slice(0, 200)}"`);
183
+ break;
184
+ }
185
+ }
186
+ catch (pollErr) {
187
+ console.error(`[iPad WeChat] Poll ${i + 1} error:`, pollErr);
188
+ }
189
+ }
190
+ // Step 4: Send outbound via JuHeBot API
191
+ if (replyText) {
192
+ try {
193
+ await sendToWeChat(cfg, params.from, replyText, params.accountId);
194
+ console.log(`[iPad WeChat] ✅ Outbound sent to ${params.from}`);
195
+ }
196
+ catch (sendErr) {
197
+ console.error(`[iPad WeChat] ❌ Outbound send failed:`, sendErr);
198
+ }
199
+ }
200
+ else {
201
+ console.log("[iPad WeChat] No non-silent agent reply found after polling, skipping outbound");
202
+ }
203
+ }
106
204
  // JuHeBot notify_type constants (from doc-6966894.md)
107
205
  const NOTIFY_NEW_MSG = 1010;
108
206
  const NOTIFY_BATCH_NEW_MSG = 1011;
207
+ /**
208
+ * Extract message content from JuHeBot `desc` field.
209
+ * Real callback format: "NickName : actualContent"
210
+ * For chatroom: "NickName:\nactualContent"
211
+ */
212
+ function extractContentFromDesc(desc) {
213
+ if (!desc)
214
+ return "";
215
+ // Private chat: "NickName : content"
216
+ const colonIdx = desc.indexOf(" : ");
217
+ if (colonIdx >= 0)
218
+ return desc.slice(colonIdx + 3);
219
+ // Chatroom: "NickName:\ncontent"
220
+ const nlIdx = desc.indexOf(":\n");
221
+ if (nlIdx >= 0)
222
+ return desc.slice(nlIdx + 2);
223
+ return desc;
224
+ }
109
225
  /**
110
226
  * Transform JuHeBot callback payload → WebhookPayload
111
227
  * Handles both JuHeBot native format and our own test format.
@@ -130,11 +246,11 @@ function transformPayload(raw) {
130
246
  messageId: String(m.msg_id ?? m.msgId ?? m.new_msg_id ?? m.newMsgId ?? `msg_${Date.now()}`),
131
247
  fromUser: m.from_username ?? m.fromUsername ?? m.from_user ?? m.fromUser ?? "",
132
248
  toUser: m.to_username ?? m.toUsername ?? m.to_user ?? m.toUser ?? "",
133
- content: m.content ?? m.msg_content ?? m.msgContent ?? "",
249
+ content: m.content ?? m.msg_content ?? m.msgContent ?? extractContentFromDesc(m.desc) ?? "",
134
250
  type: m.msg_type ?? m.msgType ?? m.type ?? 1,
135
251
  timestamp: m.timestamp ?? m.create_time ?? m.createTime ?? Date.now(),
136
252
  isSelf: m.is_self === 1 || m.is_self === true || m.isSelf === true,
137
- roomId: m.room_username ?? m.roomUsername ?? m.roomId ?? undefined,
253
+ roomId: m.room_username ?? m.roomUsername ?? m.roomId ?? (m.is_chatroom_msg === 1 && m.chatroom ? m.chatroom : undefined),
138
254
  },
139
255
  };
140
256
  }
@@ -144,6 +260,22 @@ function transformPayload(raw) {
144
260
  }
145
261
  const WEBHOOK_PORT = parseInt(process.env.IPAD_WECHAT_WEBHOOK_PORT || "18790", 10);
146
262
  let _webhookServerStarted = false;
263
+ // Deduplication: track recently processed msg_ids (TTL 60s)
264
+ const _processedMsgIds = new Map();
265
+ function isDuplicate(msgId) {
266
+ const now = Date.now();
267
+ // Prune old entries every check
268
+ if (_processedMsgIds.size > 500) {
269
+ for (const [k, t] of _processedMsgIds) {
270
+ if (now - t > 60000)
271
+ _processedMsgIds.delete(k);
272
+ }
273
+ }
274
+ if (_processedMsgIds.has(msgId))
275
+ return true;
276
+ _processedMsgIds.set(msgId, now);
277
+ return false;
278
+ }
147
279
  export default defineChannelPluginEntry({
148
280
  id: "ipad-wechat",
149
281
  name: "iPad WeChat",
@@ -202,8 +334,16 @@ export default defineChannelPluginEntry({
202
334
  console.log("[iPad WeChat] Webhook received:", JSON.stringify(raw).slice(0, 500));
203
335
  const payload = transformPayload(raw);
204
336
  if (payload && payload.message) {
337
+ // Deduplicate by msg_id
338
+ if (isDuplicate(payload.message.messageId)) {
339
+ console.log(`[iPad WeChat] Duplicate msg_id=${payload.message.messageId}, skipping`);
340
+ res.writeHead(200, { "Content-Type": "text/plain" });
341
+ res.end("ok");
342
+ return;
343
+ }
205
344
  const cfg = loadOpenClawConfig();
206
345
  const msg = payload.message;
346
+ console.log(`[iPad WeChat] Parsed: from=${msg.fromUser}, content="${msg.content?.slice(0, 100)}", type=${msg.type}`);
207
347
  const isChatroom = !!msg.roomId;
208
348
  const peerId = isChatroom ? msg.roomId : (msg.fromUser || msg.toUser || "");
209
349
  const peerKind = isChatroom ? "group" : "direct";
@@ -64,10 +64,10 @@ export function createIPadClient(config) {
64
64
  },
65
65
  /**
66
66
  * 设置实例通知地址 (回调地址)
67
- * Path: /instance/set_notify_url
67
+ * Path: /client/set_notify_url
68
68
  */
69
69
  async setNotifyUrl(notifyUrl) {
70
- await callApi(config, "/instance/set_notify_url", { notifyUrl });
70
+ await callApi(config, "/client/set_notify_url", { notify_url: notifyUrl });
71
71
  },
72
72
  /**
73
73
  * 设置实例桥接ID
@@ -180,7 +180,7 @@ export function createIPadClient(config) {
180
180
  */
181
181
  async sendFriendMessage(params) {
182
182
  const res = await callApi(config, "/msg/send_text", {
183
- toUser: params.friendWechatId,
183
+ to_username: params.friendWechatId,
184
184
  content: params.content,
185
185
  });
186
186
  return {
@@ -194,7 +194,7 @@ export function createIPadClient(config) {
194
194
  */
195
195
  async sendRoomMessage(params) {
196
196
  const res = await callApi(config, "/msg/send_room_at", {
197
- roomId: params.roomId,
197
+ to_username: params.roomId,
198
198
  content: params.content,
199
199
  });
200
200
  return {
@@ -543,7 +543,7 @@ export const API_PATHS = {
543
543
  instanceGetStatus: "/instance/get_status",
544
544
  instanceRestore: "/instance/restore",
545
545
  instanceStop: "/instance/stop",
546
- instanceSetNotifyUrl: "/instance/set_notify_url",
546
+ instanceSetNotifyUrl: "/client/set_notify_url",
547
547
  instanceSetBridgeId: "/instance/set_bridge_id",
548
548
  // 用户
549
549
  userGetProfile: "/user/get_profile",
package/index.ts CHANGED
@@ -22,6 +22,7 @@ import { randomUUID } from "crypto";
22
22
  import WebSocket from "ws";
23
23
  import { defineChannelPluginEntry, buildChannelOutboundSessionRoute } from "openclaw/plugin-sdk/core";
24
24
  import { ipadWeChatPlugin, handleInboundMessage, getCacheManagerReady } from "./src/channel.js";
25
+ import { getIPadClient } from "./src/client-pool.js";
25
26
  import type { WebhookPayload } from "./src/client.js";
26
27
 
27
28
  /** Read OpenClaw main config from disk so standalone server has full cfg */
@@ -42,7 +43,107 @@ function loadOpenClawConfig(): any {
42
43
  const GATEWAY_PORT = parseInt(process.env.OPENCLAW_GATEWAY_PORT || "18789", 10);
43
44
  let _gwReqId = 0;
44
45
 
45
- function dispatchViaGateway(params: {
46
+ /**
47
+ * Resolve JuHeBot credentials from OpenClaw config and send a text message
48
+ * to a WeChat user/room via the JuHeBot API.
49
+ */
50
+ async function sendToWeChat(cfg: any, toUser: string, text: string, accountId?: string): Promise<void> {
51
+ const entry = cfg?.plugins?.entries?.["ipad-wechat"] || {};
52
+ const section = entry.config || cfg?.channels?.["ipad-wechat"] || {};
53
+ const accounts = (section.accounts || []) as any[];
54
+ const account = accounts.find((a: any) => a.accountId === (accountId || "default")) || accounts[0] || {};
55
+
56
+ const appKey = account.appKey || section.appKey || "";
57
+ const appSecret = account.appSecret || section.appSecret || "";
58
+ const guid = account.guid || section.guid || "";
59
+
60
+ if (!appKey || !appSecret || !guid) {
61
+ throw new Error("Missing JuHeBot credentials (appKey/appSecret/guid)");
62
+ }
63
+
64
+ const client = getIPadClient(account.accountId || "default", { appKey, appSecret, guid });
65
+ const isChatroom = toUser.includes("@chatroom");
66
+
67
+ if (isChatroom) {
68
+ await client.sendRoomMessage({ roomId: toUser, content: text });
69
+ } else {
70
+ await client.sendFriendMessage({ friendWechatId: toUser, content: text });
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Extract text content from a gateway chat event message object.
76
+ */
77
+ function extractReplyText(message: any): string {
78
+ if (!message?.content) return "";
79
+ if (typeof message.content === "string") return message.content;
80
+ if (Array.isArray(message.content)) {
81
+ return message.content
82
+ .filter((c: any) => c.type === "text")
83
+ .map((c: any) => c.text || "")
84
+ .join("\n");
85
+ }
86
+ return "";
87
+ }
88
+
89
+ /**
90
+ * Open a short-lived WS to the gateway, run a single RPC call, and return the result.
91
+ */
92
+ async function gwRpc(token: string, method: string, reqParams: any): Promise<any> {
93
+ const url = `ws://127.0.0.1:${GATEWAY_PORT}`;
94
+ const ws = new WebSocket(url, { headers: { Origin: `http://127.0.0.1:${GATEWAY_PORT}` } });
95
+ await new Promise<void>((resolve, reject) => { ws.on("open", resolve); ws.on("error", reject); });
96
+
97
+ try {
98
+ // Handshake
99
+ const connectRes = await rpcCall(ws, "connect", {
100
+ minProtocol: 3, maxProtocol: 3,
101
+ client: { id: "openclaw-control-ui", version: "1.0.0", mode: "webchat", platform: "node" },
102
+ scopes: ["operator.admin", "operator.read", "operator.write"],
103
+ ...(token ? { auth: { token } } : {}),
104
+ });
105
+ if (!connectRes) throw new Error("Gateway connect failed");
106
+
107
+ // Execute the method
108
+ return await rpcCall(ws, method, reqParams);
109
+ } finally {
110
+ ws.close();
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Send a single WS RPC request and wait for the response (with 30s timeout).
116
+ */
117
+ function rpcCall(ws: WebSocket, method: string, reqParams: any): Promise<any> {
118
+ return new Promise((resolve, reject) => {
119
+ const id = `ipad-wechat-${++_gwReqId}`;
120
+ const timer = setTimeout(() => { ws.off("message", handler); reject(new Error(`${method} timed out`)); }, 30000);
121
+ const handler = (data: Buffer) => {
122
+ try {
123
+ const frame = JSON.parse(data.toString());
124
+ if (frame.type === "res" && frame.id === id) {
125
+ clearTimeout(timer);
126
+ ws.off("message", handler);
127
+ if (frame.ok) resolve(frame.payload ?? frame);
128
+ else reject(new Error(`${method} failed: ${frame.error?.message || "unknown"}`));
129
+ }
130
+ } catch { /* ignore non-matching frames */ }
131
+ };
132
+ ws.on("message", handler);
133
+ ws.send(JSON.stringify({ type: "req", id, method, params: reqParams }));
134
+ });
135
+ }
136
+
137
+ /**
138
+ * Dispatch an inbound message to the agent via the Gateway WebSocket protocol.
139
+ *
140
+ * Strategy:
141
+ * 1. Fire chat.send via a short-lived WS connection (triggers agent)
142
+ * 2. Poll chat.history via separate WS connections to detect the reply
143
+ * 3. Extract reply text, strip NO_REPLY token
144
+ * 4. If there is content, call JuHeBot API to send to WeChat
145
+ */
146
+ async function dispatchViaGateway(params: {
46
147
  sessionKey: string;
47
148
  message: string;
48
149
  from: string;
@@ -52,63 +153,93 @@ function dispatchViaGateway(params: {
52
153
  const cfg = loadOpenClawConfig();
53
154
  const token = cfg?.gateway?.controlUi?.auth?.token || cfg?.gateway?.auth?.token || "";
54
155
 
55
- return new Promise((resolve, reject) => {
56
- const url = `ws://127.0.0.1:${GATEWAY_PORT}`;
57
- const ws = new WebSocket(url, { headers: { Origin: `http://127.0.0.1:${GATEWAY_PORT}` } });
58
- const timeout = setTimeout(() => { ws.close(); reject(new Error("Gateway dispatch timeout")); }, 30000);
59
-
60
- let connected = false;
61
- let sendId: string | null = null;
62
-
63
- ws.on("open", () => {
64
- const connectId = `ipad-wechat-${++_gwReqId}`;
65
- ws.send(JSON.stringify({
66
- type: "req", id: connectId, method: "connect",
67
- params: {
68
- minProtocol: 3, maxProtocol: 3,
69
- client: { id: "openclaw-control-ui", version: "1.0.0", mode: "webchat", platform: "node" },
70
- scopes: ["operator.admin", "operator.read", "operator.write"],
71
- ...(token ? { auth: { token } } : {}),
72
- },
73
- }));
74
- });
156
+ // Step 1: Get baseline message count
157
+ const baselineHistory = await gwRpc(token, "chat.history", {
158
+ sessionKey: params.sessionKey, limit: 50,
159
+ });
160
+ const baselineCount = Array.isArray(baselineHistory?.messages)
161
+ ? baselineHistory.messages.length : 0;
162
+ console.log(`[iPad WeChat] Baseline history: ${baselineCount} messages`);
163
+
164
+ // Step 2: Send user message (triggers agent processing)
165
+ const sendResult = await gwRpc(token, "chat.send", {
166
+ idempotencyKey: randomUUID(),
167
+ sessionKey: params.sessionKey,
168
+ message: params.message,
169
+ deliver: true,
170
+ originatingChannel: "ipad-wechat",
171
+ originatingTo: params.from,
172
+ originatingAccountId: params.accountId || "default",
173
+ });
174
+ console.log(`[iPad WeChat] chat.send accepted, runId=${sendResult?.runId}, polling for reply…`);
75
175
 
76
- ws.on("message", (data: Buffer) => {
77
- try {
78
- const frame = JSON.parse(data.toString());
79
- if (frame.type === "res" && !connected) {
80
- connected = true;
81
- if (!frame.ok) { clearTimeout(timeout); ws.close(); reject(new Error("Gateway connect failed: " + (frame.error?.message || "unknown"))); return; }
82
- sendId = `ipad-wechat-${++_gwReqId}`;
83
- ws.send(JSON.stringify({
84
- type: "req", id: sendId, method: "chat.send",
85
- params: {
86
- idempotencyKey: randomUUID(),
87
- sessionKey: params.sessionKey,
88
- message: params.message,
89
- deliver: true,
90
- originatingChannel: "ipad-wechat",
91
- originatingTo: params.from,
92
- originatingAccountId: params.accountId || "default",
93
- },
94
- }));
95
- } else if (frame.type === "res" && frame.id === sendId) {
96
- clearTimeout(timeout);
97
- ws.close();
98
- if (frame.ok) { resolve(); } else { reject(new Error("chat.send failed: " + (frame.error?.message || "unknown"))); }
99
- }
100
- } catch { /* ignore parse errors */ }
101
- });
176
+ // Step 3: Poll chat.history via fresh connections
177
+ const POLL_INTERVAL = 3000;
178
+ const MAX_POLLS = 40; // 3s × 40 = 120s max
179
+ let replyText = "";
102
180
 
103
- ws.on("error", (err) => { clearTimeout(timeout); reject(err); });
104
- ws.on("close", () => { clearTimeout(timeout); if (!connected) reject(new Error("Gateway closed before connect")); });
105
- });
181
+ for (let i = 0; i < MAX_POLLS; i++) {
182
+ await new Promise(r => setTimeout(r, POLL_INTERVAL));
183
+
184
+ try {
185
+ const history = await gwRpc(token, "chat.history", {
186
+ sessionKey: params.sessionKey, limit: 50,
187
+ });
188
+ const messages = Array.isArray(history?.messages) ? history.messages : [];
189
+
190
+ if (messages.length <= baselineCount) continue;
191
+
192
+ // Look for new assistant messages after the baseline
193
+ // chat.history returns messages with role/content at top level
194
+ const newMessages = messages.slice(baselineCount);
195
+ const assistantMsg = newMessages.find(
196
+ (m: any) => m?.role === "assistant" || m?.message?.role === "assistant"
197
+ );
198
+
199
+ if (assistantMsg) {
200
+ const rawText = extractReplyText(assistantMsg);
201
+ replyText = rawText.replace(/\s*NO_REPLY\s*/gi, "").trim();
202
+ console.log(`[iPad WeChat] Agent reply found on poll ${i + 1}: "${replyText.slice(0, 200)}"`);
203
+ break;
204
+ }
205
+ } catch (pollErr) {
206
+ console.error(`[iPad WeChat] Poll ${i + 1} error:`, pollErr);
207
+ }
208
+ }
209
+
210
+ // Step 4: Send outbound via JuHeBot API
211
+ if (replyText) {
212
+ try {
213
+ await sendToWeChat(cfg, params.from, replyText, params.accountId);
214
+ console.log(`[iPad WeChat] ✅ Outbound sent to ${params.from}`);
215
+ } catch (sendErr) {
216
+ console.error(`[iPad WeChat] ❌ Outbound send failed:`, sendErr);
217
+ }
218
+ } else {
219
+ console.log("[iPad WeChat] No non-silent agent reply found after polling, skipping outbound");
220
+ }
106
221
  }
107
222
 
108
223
  // JuHeBot notify_type constants (from doc-6966894.md)
109
224
  const NOTIFY_NEW_MSG = 1010;
110
225
  const NOTIFY_BATCH_NEW_MSG = 1011;
111
226
 
227
+ /**
228
+ * Extract message content from JuHeBot `desc` field.
229
+ * Real callback format: "NickName : actualContent"
230
+ * For chatroom: "NickName:\nactualContent"
231
+ */
232
+ function extractContentFromDesc(desc: string | undefined): string {
233
+ if (!desc) return "";
234
+ // Private chat: "NickName : content"
235
+ const colonIdx = desc.indexOf(" : ");
236
+ if (colonIdx >= 0) return desc.slice(colonIdx + 3);
237
+ // Chatroom: "NickName:\ncontent"
238
+ const nlIdx = desc.indexOf(":\n");
239
+ if (nlIdx >= 0) return desc.slice(nlIdx + 2);
240
+ return desc;
241
+ }
242
+
112
243
  /**
113
244
  * Transform JuHeBot callback payload → WebhookPayload
114
245
  * Handles both JuHeBot native format and our own test format.
@@ -136,11 +267,11 @@ function transformPayload(raw: any): WebhookPayload | null {
136
267
  messageId: String(m.msg_id ?? m.msgId ?? m.new_msg_id ?? m.newMsgId ?? `msg_${Date.now()}`),
137
268
  fromUser: m.from_username ?? m.fromUsername ?? m.from_user ?? m.fromUser ?? "",
138
269
  toUser: m.to_username ?? m.toUsername ?? m.to_user ?? m.toUser ?? "",
139
- content: m.content ?? m.msg_content ?? m.msgContent ?? "",
270
+ content: m.content ?? m.msg_content ?? m.msgContent ?? extractContentFromDesc(m.desc) ?? "",
140
271
  type: m.msg_type ?? m.msgType ?? m.type ?? 1,
141
272
  timestamp: m.timestamp ?? m.create_time ?? m.createTime ?? Date.now(),
142
273
  isSelf: m.is_self === 1 || m.is_self === true || m.isSelf === true,
143
- roomId: m.room_username ?? m.roomUsername ?? m.roomId ?? undefined,
274
+ roomId: m.room_username ?? m.roomUsername ?? m.roomId ?? (m.is_chatroom_msg === 1 && m.chatroom ? m.chatroom : undefined),
144
275
  },
145
276
  };
146
277
  }
@@ -153,6 +284,21 @@ function transformPayload(raw: any): WebhookPayload | null {
153
284
  const WEBHOOK_PORT = parseInt(process.env.IPAD_WECHAT_WEBHOOK_PORT || "18790", 10);
154
285
  let _webhookServerStarted = false;
155
286
 
287
+ // Deduplication: track recently processed msg_ids (TTL 60s)
288
+ const _processedMsgIds = new Map<string, number>();
289
+ function isDuplicate(msgId: string): boolean {
290
+ const now = Date.now();
291
+ // Prune old entries every check
292
+ if (_processedMsgIds.size > 500) {
293
+ for (const [k, t] of _processedMsgIds) {
294
+ if (now - t > 60000) _processedMsgIds.delete(k);
295
+ }
296
+ }
297
+ if (_processedMsgIds.has(msgId)) return true;
298
+ _processedMsgIds.set(msgId, now);
299
+ return false;
300
+ }
301
+
156
302
  export default defineChannelPluginEntry({
157
303
  id: "ipad-wechat",
158
304
  name: "iPad WeChat",
@@ -212,8 +358,17 @@ export default defineChannelPluginEntry({
212
358
 
213
359
  const payload = transformPayload(raw);
214
360
  if (payload && payload.message) {
361
+ // Deduplicate by msg_id
362
+ if (isDuplicate(payload.message.messageId)) {
363
+ console.log(`[iPad WeChat] Duplicate msg_id=${payload.message.messageId}, skipping`);
364
+ res.writeHead(200, { "Content-Type": "text/plain" });
365
+ res.end("ok");
366
+ return;
367
+ }
368
+
215
369
  const cfg = loadOpenClawConfig();
216
370
  const msg = payload.message;
371
+ console.log(`[iPad WeChat] Parsed: from=${msg.fromUser}, content="${msg.content?.slice(0, 100)}", type=${msg.type}`);
217
372
  const isChatroom = !!(msg as any).roomId;
218
373
  const peerId = isChatroom ? (msg as any).roomId : (msg.fromUser || msg.toUser || "");
219
374
  const peerKind = isChatroom ? "group" : "direct";
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://openclaw.ai/schema/plugin.json",
3
3
  "id": "ipad-wechat",
4
4
  "name": "iPad WeChat",
5
- "version": "1.0.14",
5
+ "version": "2.0.0",
6
6
  "description": "Connect OpenClaw to iPad WeChat protocol for sending and receiving WeChat messages",
7
7
  "author": {
8
8
  "name": "gloablehive",
package/package.json CHANGED
@@ -1,11 +1,19 @@
1
1
  {
2
2
  "name": "@gloablehive/ipad-wechat-plugin",
3
- "version": "1.0.26",
3
+ "version": "2.0.0",
4
4
  "type": "module",
5
- "description": "OpenClaw channel plugin for iPad WeChat protocol - enables sending/receiving WeChat messages through iPad protocol",
5
+ "description": "OpenClaw channel plugin for iPad WeChat protocol - enables sending/receiving WeChat messages through JuHeBot API",
6
6
  "main": "index.ts",
7
+ "files": [
8
+ "index.ts",
9
+ "src/",
10
+ "dist/",
11
+ "openclaw.plugin.json",
12
+ "README.md"
13
+ ],
7
14
  "scripts": {
8
15
  "build": "tsc",
16
+ "prepublishOnly": "npm run build",
9
17
  "test": "npx tsx test-ipad.ts",
10
18
  "dev": "tsx watch index.ts"
11
19
  },
@@ -19,6 +27,13 @@
19
27
  "blurb": "Connect OpenClaw to iPad WeChat protocol for sending and receiving WeChat messages"
20
28
  }
21
29
  },
30
+ "keywords": ["openclaw", "wechat", "ipad", "channel", "plugin", "juhebot", "messaging"],
31
+ "license": "MIT",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/tbuijibing/gloablehive",
35
+ "directory": "channels/ipad-wechat-plugin"
36
+ },
22
37
  "dependencies": {
23
38
  "@gloablehive/wechat-cache": "^1.2.0",
24
39
  "openclaw": ">=1.0.0"
package/src/client.ts CHANGED
@@ -140,10 +140,10 @@ export function createIPadClient(config: JuHeBotConfig) {
140
140
 
141
141
  /**
142
142
  * 设置实例通知地址 (回调地址)
143
- * Path: /instance/set_notify_url
143
+ * Path: /client/set_notify_url
144
144
  */
145
145
  async setNotifyUrl(notifyUrl: string): Promise<void> {
146
- await callApi(config, "/instance/set_notify_url", { notifyUrl });
146
+ await callApi(config, "/client/set_notify_url", { notify_url: notifyUrl });
147
147
  },
148
148
 
149
149
  /**
@@ -280,7 +280,7 @@ export function createIPadClient(config: JuHeBotConfig) {
280
280
  content: string;
281
281
  }): Promise<SendTextResponse> {
282
282
  const res = await callApi(config, "/msg/send_text", {
283
- toUser: params.friendWechatId,
283
+ to_username: params.friendWechatId,
284
284
  content: params.content,
285
285
  });
286
286
 
@@ -299,7 +299,7 @@ export function createIPadClient(config: JuHeBotConfig) {
299
299
  content: string;
300
300
  }): Promise<SendTextResponse> {
301
301
  const res = await callApi(config, "/msg/send_room_at", {
302
- roomId: params.roomId,
302
+ to_username: params.roomId,
303
303
  content: params.content,
304
304
  });
305
305
 
@@ -704,7 +704,7 @@ export const API_PATHS = {
704
704
  instanceGetStatus: "/instance/get_status",
705
705
  instanceRestore: "/instance/restore",
706
706
  instanceStop: "/instance/stop",
707
- instanceSetNotifyUrl: "/instance/set_notify_url",
707
+ instanceSetNotifyUrl: "/client/set_notify_url",
708
708
  instanceSetBridgeId: "/instance/set_bridge_id",
709
709
 
710
710
  // 用户
package/test-ipad-real.ts DELETED
@@ -1,77 +0,0 @@
1
- /**
2
- * Test Script for iPad WeChat Plugin - Real API Test
3
- * Run: npx tsx test-ipad-real.ts
4
- */
5
-
6
- import { createIPadClient } from './src/client.js';
7
-
8
- const CONFIG = {
9
- appKey: 'app84l4hKvqmUphNX1H',
10
- appSecret: 'CztidspJSiunhw7BVWnKiTgwVFV55nbaPVcBMRa34IT7hRvyxtJLsgMM0C3XMfbD',
11
- guid: '0e7d1810-2b76-3c2b-8cab-74d94a951af9',
12
- };
13
-
14
- async function test1_GetLoginAccountInfo() {
15
- console.log('\n📋 Test 1: Get Login Account Info');
16
-
17
- const client = createIPadClient(CONFIG);
18
-
19
- try {
20
- const info = await client.getLoginAccountInfo();
21
- console.log('✅ Account Info:');
22
- console.log(' WeChat ID:', (info as any).userName?.string);
23
- console.log(' NickName:', (info as any).nickName?.string);
24
- console.log(' Mobile:', (info as any).bindMobile?.string);
25
- console.log(' Signature:', (info as any).signature?.string);
26
- return info;
27
- } catch (error: any) {
28
- console.log('❌ Error:', error.message);
29
- return null;
30
- }
31
- }
32
-
33
- async function test2_SyncContacts() {
34
- console.log('\n📋 Test 2: Sync Contacts');
35
-
36
- const client = createIPadClient(CONFIG);
37
-
38
- try {
39
- const contacts = await client.syncContacts();
40
- console.log('✅ Contacts count:', contacts.length);
41
- if (contacts.length > 0) {
42
- console.log('Sample contact:', contacts[0]);
43
- }
44
- return contacts;
45
- } catch (error: any) {
46
- console.log('❌ Error:', error.message);
47
- return [];
48
- }
49
- }
50
-
51
- async function test3_GetRoomList() {
52
- console.log('\n📋 Test 3: Get Room Info (sample room ID needed)');
53
- console.log('⚠️ Skipping - requires a valid room ID');
54
- return null;
55
- }
56
-
57
- async function test4_SendTextMessage() {
58
- console.log('\n📋 Test 4: Send Text Message');
59
- console.log('⚠️ Skipping - requires a valid friend wechat ID');
60
- return null;
61
- }
62
-
63
- async function main() {
64
- console.log('🚀 iPad WeChat Plugin - Real API Test');
65
- console.log('=======================================');
66
- console.log('Config:', { appKey: CONFIG.appKey, guid: CONFIG.guid });
67
-
68
- await test1_GetLoginAccountInfo();
69
- await test2_SyncContacts();
70
- await test3_GetRoomList();
71
- await test4_SendTextMessage();
72
-
73
- console.log('\n=======================================');
74
- console.log('✅ Tests completed!');
75
- }
76
-
77
- main();
package/test-ipad.ts DELETED
@@ -1,150 +0,0 @@
1
- /**
2
- * Test Script for iPad WeChat Plugin
3
- * Run: npx tsx test-ipad.ts
4
- */
5
-
6
- import * as fs from 'fs/promises';
7
- import * as path from 'path';
8
- import {
9
- createCacheManager,
10
- CacheManager,
11
- WeChatAccount,
12
- WeChatMessage,
13
- } from "@gloablehive/wechat-cache";
14
- import { createIPadClient, type WebhookPayload } from './src/client.js';
15
-
16
- const TEST_CACHE_PATH = '/tmp/wechat-cache-ipad-test';
17
-
18
- async function cleanup() {
19
- try {
20
- await fs.rm(TEST_CACHE_PATH, { recursive: true, force: true });
21
- } catch {}
22
- }
23
-
24
- async function test1_CacheSystem() {
25
- console.log('\n📋 Test 1: iPad WeChat Cache System');
26
-
27
- const accounts: WeChatAccount[] = [
28
- {
29
- accountId: 'ipad-account-001',
30
- wechatAccountId: 'ipad-wechat-001',
31
- wechatId: 'wxid_ipad001',
32
- nickName: 'iPad客服',
33
- enabled: true,
34
- },
35
- ];
36
-
37
- const manager = createCacheManager({
38
- basePath: TEST_CACHE_PATH,
39
- accounts,
40
- });
41
-
42
- await manager.init();
43
- console.log('✅ Cache manager initialized');
44
-
45
- // Test message
46
- const message: WeChatMessage = {
47
- messageId: 'ipad-msg-001',
48
- accountId: 'ipad-account-001',
49
- conversationType: 'friend',
50
- conversationId: 'wxid_friend001',
51
- senderId: 'wxid_friend001',
52
- content: '你好,这是iPad协议测试消息',
53
- messageType: 1,
54
- timestamp: Date.now(),
55
- isSelf: false,
56
- direction: 'inbound',
57
- };
58
-
59
- await manager.onMessage(message);
60
- console.log('✅ Message cached');
61
-
62
- // Verify file exists
63
- const friendPath = path.join(TEST_CACHE_PATH, 'accounts', 'ipad-account-001', 'friends', 'wxid_friend001');
64
- const files = await fs.readdir(friendPath);
65
- console.log('✅ Cached files:', files);
66
-
67
- return manager;
68
- }
69
-
70
- async function test2_WebhookPayload() {
71
- console.log('\n📋 Test 2: Webhook Payload Parsing');
72
-
73
- // Simulate iPad webhook payload
74
- const payload: WebhookPayload = {
75
- event: 'message',
76
- message: {
77
- messageId: 'ipad-msg-002',
78
- fromUser: 'wxid_testfriend',
79
- toUser: 'wxid_ipad001',
80
- content: '收到一条测试消息',
81
- type: 1,
82
- timestamp: Date.now(),
83
- isSelf: false,
84
- },
85
- };
86
-
87
- console.log('✅ Payload:', JSON.stringify(payload, null, 2));
88
- console.log('✅ Webhook payload structure valid');
89
- }
90
-
91
- async function test3_ClientCreation() {
92
- console.log('\n📋 Test 3: iPad Client Creation');
93
-
94
- const client = createIPadClient({
95
- baseUrl: 'https://api.example.com',
96
- apiKey: 'test-api-key',
97
- });
98
-
99
- console.log('✅ Client created with methods:', Object.keys(client));
100
-
101
- // Check all methods exist
102
- const expectedMethods = [
103
- 'sendFriendMessage',
104
- 'sendRoomMessage',
105
- 'sendFriendMedia',
106
- 'sendRoomMedia',
107
- 'syncContacts',
108
- 'getContactDetail',
109
- 'updateFriendRemark',
110
- 'getRoomInfo',
111
- 'getRoomMembers',
112
- 'createRoom',
113
- 'addRoomMember',
114
- 'removeRoomMember',
115
- 'getLoginAccountInfo',
116
- 'getFriendMoments',
117
- 'publishMoment',
118
- ];
119
-
120
- for (const method of expectedMethods) {
121
- if (typeof (client as any)[method] === 'function') {
122
- console.log(` ✅ ${method}`);
123
- } else {
124
- console.log(` ❌ ${method} - missing`);
125
- }
126
- }
127
- }
128
-
129
- async function main() {
130
- console.log('🚀 iPad WeChat Plugin Tests');
131
- console.log('============================');
132
-
133
- await cleanup();
134
-
135
- try {
136
- await test1_CacheSystem();
137
- await test2_WebhookPayload();
138
- await test3_ClientCreation();
139
-
140
- console.log('\n============================');
141
- console.log('✅ All tests passed!');
142
- } catch (error) {
143
- console.error('\n❌ Test failed:', error);
144
- process.exit(1);
145
- } finally {
146
- await cleanup();
147
- }
148
- }
149
-
150
- main();
package/tsconfig.json DELETED
@@ -1,22 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "ESNext",
5
- "moduleResolution": "bundler",
6
- "lib": ["ES2022"],
7
- "outDir": "./dist",
8
- "rootDir": ".",
9
- "declaration": false,
10
- "strict": false,
11
- "noImplicitAny": false,
12
- "esModuleInterop": true,
13
- "skipLibCheck": true,
14
- "forceConsistentCasingInFileNames": true,
15
- "resolveJsonModule": true,
16
- "allowSyntheticDefaultImports": true,
17
- "noEmit": false,
18
- "noEmitOnError": false
19
- },
20
- "include": ["*.ts", "src/**/*.ts"],
21
- "exclude": ["node_modules", "dist", "test*.ts"]
22
- }