@gloablehive/ipad-wechat-plugin 2.0.0 → 2.2.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 +6 -0
- package/dist/index.js +140 -18
- package/dist/src/channel.js +1 -1
- package/index.ts +136 -19
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/channel.ts +1 -1
package/README.md
CHANGED
|
@@ -114,6 +114,12 @@ All JuHeBot API parameters use **snake_case**:
|
|
|
114
114
|
|
|
115
115
|
## Version History
|
|
116
116
|
|
|
117
|
+
### 2.1.0
|
|
118
|
+
|
|
119
|
+
- **Outbound message caching**: Agent replies are written to `@gloablehive/wechat-cache` after successful JuHeBot send (direction: `outbound`, isSelf: `true`)
|
|
120
|
+
- **WS connection pool**: Reuse gateway WebSocket connections instead of opening a new one per RPC call. Pool of up to 3 connections with 30s idle timeout. Reduces connection overhead during `chat.history` polling from ~42 to ~3 connections per message.
|
|
121
|
+
- **Cloudflare named tunnel script**: `scripts/setup-cloudflare-tunnel.sh` for permanent tunnel URL that survives restarts. Auto-updates JuHeBot `notifyUrl` on start.
|
|
122
|
+
|
|
117
123
|
### 2.0.0
|
|
118
124
|
|
|
119
125
|
- Full end-to-end inbound + outbound message flow
|
package/dist/index.js
CHANGED
|
@@ -20,16 +20,19 @@ import { homedir } from "os";
|
|
|
20
20
|
import { randomUUID } from "crypto";
|
|
21
21
|
import WebSocket from "ws";
|
|
22
22
|
import { defineChannelPluginEntry, buildChannelOutboundSessionRoute } from "openclaw/plugin-sdk/core";
|
|
23
|
-
import { ipadWeChatPlugin, handleInboundMessage, getCacheManagerReady } from "./src/channel.js";
|
|
23
|
+
import { ipadWeChatPlugin, handleInboundMessage, getCacheManagerReady, getCacheManager } from "./src/channel.js";
|
|
24
24
|
import { getIPadClient } from "./src/client-pool.js";
|
|
25
25
|
/** Read OpenClaw main config from disk so standalone server has full cfg */
|
|
26
26
|
let _cfgCache = null;
|
|
27
|
+
let _cfgCacheTs = 0;
|
|
28
|
+
const CFG_CACHE_TTL_MS = 30_000; // re-read config every 30s to pick up binding changes
|
|
27
29
|
function loadOpenClawConfig() {
|
|
28
|
-
if (_cfgCache)
|
|
30
|
+
if (_cfgCache && (Date.now() - _cfgCacheTs) < CFG_CACHE_TTL_MS)
|
|
29
31
|
return _cfgCache;
|
|
30
32
|
try {
|
|
31
33
|
const p = join(homedir(), ".openclaw", "openclaw.json");
|
|
32
34
|
_cfgCache = JSON.parse(readFileSync(p, "utf-8"));
|
|
35
|
+
_cfgCacheTs = Date.now();
|
|
33
36
|
return _cfgCache;
|
|
34
37
|
}
|
|
35
38
|
catch (err) {
|
|
@@ -37,6 +40,24 @@ function loadOpenClawConfig() {
|
|
|
37
40
|
return null;
|
|
38
41
|
}
|
|
39
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Resolve agentId from config bindings for a given channel/accountId.
|
|
45
|
+
* Falls back to "main" if no binding matches.
|
|
46
|
+
*/
|
|
47
|
+
function resolveAgentFromBindings(cfg, channel, accountId) {
|
|
48
|
+
const bindings = cfg?.bindings ?? [];
|
|
49
|
+
// Try specific match first (channel + accountId)
|
|
50
|
+
if (accountId) {
|
|
51
|
+
const specific = bindings.find((b) => b.match?.channel === channel && b.match?.accountId === accountId);
|
|
52
|
+
if (specific?.agentId)
|
|
53
|
+
return specific.agentId;
|
|
54
|
+
}
|
|
55
|
+
// Then try channel-only match
|
|
56
|
+
const channelOnly = bindings.find((b) => b.match?.channel === channel && !b.match?.accountId);
|
|
57
|
+
if (channelOnly?.agentId)
|
|
58
|
+
return channelOnly.agentId;
|
|
59
|
+
return "main";
|
|
60
|
+
}
|
|
40
61
|
// ── Gateway WebSocket dispatch ──
|
|
41
62
|
const GATEWAY_PORT = parseInt(process.env.OPENCLAW_GATEWAY_PORT || "18789", 10);
|
|
42
63
|
let _gwReqId = 0;
|
|
@@ -84,25 +105,104 @@ function extractReplyText(message) {
|
|
|
84
105
|
* Open a short-lived WS to the gateway, run a single RPC call, and return the result.
|
|
85
106
|
*/
|
|
86
107
|
async function gwRpc(token, method, reqParams) {
|
|
108
|
+
const conn = await gwPoolAcquire(token);
|
|
109
|
+
try {
|
|
110
|
+
const result = await rpcCall(conn, method, reqParams);
|
|
111
|
+
gwPoolRelease(conn);
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
// On error, discard this connection from the pool
|
|
116
|
+
gwPoolDiscard(conn);
|
|
117
|
+
throw err;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// ── Gateway WS Connection Pool ──
|
|
121
|
+
const GW_POOL_MAX = 3;
|
|
122
|
+
const GW_POOL_IDLE_MS = 30_000; // close idle connections after 30s
|
|
123
|
+
const _gwPool = [];
|
|
124
|
+
let _gwPoolTimer = null;
|
|
125
|
+
function _gwPoolStartSweep() {
|
|
126
|
+
if (_gwPoolTimer)
|
|
127
|
+
return;
|
|
128
|
+
_gwPoolTimer = setInterval(() => {
|
|
129
|
+
const now = Date.now();
|
|
130
|
+
for (let i = _gwPool.length - 1; i >= 0; i--) {
|
|
131
|
+
const entry = _gwPool[i];
|
|
132
|
+
if (!entry.busy && now - entry.lastUsed > GW_POOL_IDLE_MS) {
|
|
133
|
+
_gwPool.splice(i, 1);
|
|
134
|
+
try {
|
|
135
|
+
entry.ws.close();
|
|
136
|
+
}
|
|
137
|
+
catch { }
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (_gwPool.length === 0 && _gwPoolTimer) {
|
|
141
|
+
clearInterval(_gwPoolTimer);
|
|
142
|
+
_gwPoolTimer = null;
|
|
143
|
+
}
|
|
144
|
+
}, 10_000);
|
|
145
|
+
}
|
|
146
|
+
async function gwPoolAcquire(token) {
|
|
147
|
+
// Try to reuse an idle connection with the same token
|
|
148
|
+
for (const entry of _gwPool) {
|
|
149
|
+
if (!entry.busy && entry.token === token && entry.ws.readyState === WebSocket.OPEN) {
|
|
150
|
+
entry.busy = true;
|
|
151
|
+
entry.lastUsed = Date.now();
|
|
152
|
+
return entry.ws;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// Create a new connection
|
|
87
156
|
const url = `ws://127.0.0.1:${GATEWAY_PORT}`;
|
|
88
157
|
const ws = new WebSocket(url, { headers: { Origin: `http://127.0.0.1:${GATEWAY_PORT}` } });
|
|
89
158
|
await new Promise((resolve, reject) => { ws.on("open", resolve); ws.on("error", reject); });
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
159
|
+
// Handshake
|
|
160
|
+
const connectRes = await rpcCall(ws, "connect", {
|
|
161
|
+
minProtocol: 3, maxProtocol: 3,
|
|
162
|
+
client: { id: "openclaw-control-ui", version: "1.0.0", mode: "webchat", platform: "node" },
|
|
163
|
+
scopes: ["operator.admin", "operator.read", "operator.write"],
|
|
164
|
+
...(token ? { auth: { token } } : {}),
|
|
165
|
+
});
|
|
166
|
+
if (!connectRes) {
|
|
167
|
+
ws.close();
|
|
168
|
+
throw new Error("Gateway connect failed");
|
|
169
|
+
}
|
|
170
|
+
// Evict oldest idle if pool is full
|
|
171
|
+
if (_gwPool.length >= GW_POOL_MAX) {
|
|
172
|
+
const idleIdx = _gwPool.findIndex(e => !e.busy);
|
|
173
|
+
if (idleIdx >= 0) {
|
|
174
|
+
const evicted = _gwPool.splice(idleIdx, 1)[0];
|
|
175
|
+
try {
|
|
176
|
+
evicted.ws.close();
|
|
177
|
+
}
|
|
178
|
+
catch { }
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const entry = { ws, token, busy: true, lastUsed: Date.now() };
|
|
182
|
+
_gwPool.push(entry);
|
|
183
|
+
// Auto-remove on close/error
|
|
184
|
+
ws.on("close", () => { const idx = _gwPool.indexOf(entry); if (idx >= 0)
|
|
185
|
+
_gwPool.splice(idx, 1); });
|
|
186
|
+
ws.on("error", () => { const idx = _gwPool.indexOf(entry); if (idx >= 0)
|
|
187
|
+
_gwPool.splice(idx, 1); });
|
|
188
|
+
_gwPoolStartSweep();
|
|
189
|
+
return ws;
|
|
190
|
+
}
|
|
191
|
+
function gwPoolRelease(ws) {
|
|
192
|
+
const entry = _gwPool.find(e => e.ws === ws);
|
|
193
|
+
if (entry) {
|
|
194
|
+
entry.busy = false;
|
|
195
|
+
entry.lastUsed = Date.now();
|
|
102
196
|
}
|
|
103
|
-
|
|
197
|
+
}
|
|
198
|
+
function gwPoolDiscard(ws) {
|
|
199
|
+
const idx = _gwPool.findIndex(e => e.ws === ws);
|
|
200
|
+
if (idx >= 0)
|
|
201
|
+
_gwPool.splice(idx, 1);
|
|
202
|
+
try {
|
|
104
203
|
ws.close();
|
|
105
204
|
}
|
|
205
|
+
catch { }
|
|
106
206
|
}
|
|
107
207
|
/**
|
|
108
208
|
* Send a single WS RPC request and wait for the response (with 30s timeout).
|
|
@@ -187,11 +287,31 @@ async function dispatchViaGateway(params) {
|
|
|
187
287
|
console.error(`[iPad WeChat] Poll ${i + 1} error:`, pollErr);
|
|
188
288
|
}
|
|
189
289
|
}
|
|
190
|
-
// Step 4: Send outbound via JuHeBot API
|
|
290
|
+
// Step 4: Send outbound via JuHeBot API + cache
|
|
191
291
|
if (replyText) {
|
|
192
292
|
try {
|
|
193
293
|
await sendToWeChat(cfg, params.from, replyText, params.accountId);
|
|
194
294
|
console.log(`[iPad WeChat] ✅ Outbound sent to ${params.from}`);
|
|
295
|
+
// Write outbound message to cache
|
|
296
|
+
try {
|
|
297
|
+
const cache = getCacheManager(cfg);
|
|
298
|
+
const isChatroom = params.from.includes("@chatroom");
|
|
299
|
+
await cache.onMessage({
|
|
300
|
+
messageId: `out_${Date.now()}`,
|
|
301
|
+
accountId: params.accountId || "default",
|
|
302
|
+
conversationType: isChatroom ? "chatroom" : "friend",
|
|
303
|
+
conversationId: params.from,
|
|
304
|
+
senderId: params.to || "self",
|
|
305
|
+
content: replyText,
|
|
306
|
+
messageType: 1,
|
|
307
|
+
timestamp: Date.now(),
|
|
308
|
+
isSelf: true,
|
|
309
|
+
direction: "outbound",
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
catch (cacheErr) {
|
|
313
|
+
console.error("[iPad WeChat] Outbound cache write failed:", cacheErr);
|
|
314
|
+
}
|
|
195
315
|
}
|
|
196
316
|
catch (sendErr) {
|
|
197
317
|
console.error(`[iPad WeChat] ❌ Outbound send failed:`, sendErr);
|
|
@@ -347,10 +467,12 @@ export default defineChannelPluginEntry({
|
|
|
347
467
|
const isChatroom = !!msg.roomId;
|
|
348
468
|
const peerId = isChatroom ? msg.roomId : (msg.fromUser || msg.toUser || "");
|
|
349
469
|
const peerKind = isChatroom ? "group" : "direct";
|
|
350
|
-
|
|
470
|
+
const resolvedAgent = resolveAgentFromBindings(cfg, "ipad-wechat");
|
|
471
|
+
console.log(`[iPad WeChat] Resolved agent: ${resolvedAgent} (from bindings)`);
|
|
472
|
+
let sessionKey = `agent:${resolvedAgent}:ipad-wechat:${peerKind}:${peerId}`;
|
|
351
473
|
try {
|
|
352
474
|
const route = buildChannelOutboundSessionRoute({
|
|
353
|
-
cfg, agentId:
|
|
475
|
+
cfg, agentId: resolvedAgent, channel: "ipad-wechat",
|
|
354
476
|
peer: { kind: peerKind, id: peerId },
|
|
355
477
|
chatType: peerKind,
|
|
356
478
|
from: msg.toUser || "", to: msg.fromUser || "",
|
package/dist/src/channel.js
CHANGED
package/index.ts
CHANGED
|
@@ -21,17 +21,20 @@ import { homedir } from "os";
|
|
|
21
21
|
import { randomUUID } from "crypto";
|
|
22
22
|
import WebSocket from "ws";
|
|
23
23
|
import { defineChannelPluginEntry, buildChannelOutboundSessionRoute } from "openclaw/plugin-sdk/core";
|
|
24
|
-
import { ipadWeChatPlugin, handleInboundMessage, getCacheManagerReady } from "./src/channel.js";
|
|
24
|
+
import { ipadWeChatPlugin, handleInboundMessage, getCacheManagerReady, getCacheManager } from "./src/channel.js";
|
|
25
25
|
import { getIPadClient } from "./src/client-pool.js";
|
|
26
26
|
import type { WebhookPayload } from "./src/client.js";
|
|
27
27
|
|
|
28
28
|
/** Read OpenClaw main config from disk so standalone server has full cfg */
|
|
29
29
|
let _cfgCache: any = null;
|
|
30
|
+
let _cfgCacheTs = 0;
|
|
31
|
+
const CFG_CACHE_TTL_MS = 30_000; // re-read config every 30s to pick up binding changes
|
|
30
32
|
function loadOpenClawConfig(): any {
|
|
31
|
-
if (_cfgCache) return _cfgCache;
|
|
33
|
+
if (_cfgCache && (Date.now() - _cfgCacheTs) < CFG_CACHE_TTL_MS) return _cfgCache;
|
|
32
34
|
try {
|
|
33
35
|
const p = join(homedir(), ".openclaw", "openclaw.json");
|
|
34
36
|
_cfgCache = JSON.parse(readFileSync(p, "utf-8"));
|
|
37
|
+
_cfgCacheTs = Date.now();
|
|
35
38
|
return _cfgCache;
|
|
36
39
|
} catch (err) {
|
|
37
40
|
console.error("[iPad WeChat] Failed to load openclaw.json:", err);
|
|
@@ -39,6 +42,27 @@ function loadOpenClawConfig(): any {
|
|
|
39
42
|
}
|
|
40
43
|
}
|
|
41
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Resolve agentId from config bindings for a given channel/accountId.
|
|
47
|
+
* Falls back to "main" if no binding matches.
|
|
48
|
+
*/
|
|
49
|
+
function resolveAgentFromBindings(cfg: any, channel: string, accountId?: string): string {
|
|
50
|
+
const bindings: any[] = cfg?.bindings ?? [];
|
|
51
|
+
// Try specific match first (channel + accountId)
|
|
52
|
+
if (accountId) {
|
|
53
|
+
const specific = bindings.find(
|
|
54
|
+
(b: any) => b.match?.channel === channel && b.match?.accountId === accountId
|
|
55
|
+
);
|
|
56
|
+
if (specific?.agentId) return specific.agentId;
|
|
57
|
+
}
|
|
58
|
+
// Then try channel-only match
|
|
59
|
+
const channelOnly = bindings.find(
|
|
60
|
+
(b: any) => b.match?.channel === channel && !b.match?.accountId
|
|
61
|
+
);
|
|
62
|
+
if (channelOnly?.agentId) return channelOnly.agentId;
|
|
63
|
+
return "main";
|
|
64
|
+
}
|
|
65
|
+
|
|
42
66
|
// ── Gateway WebSocket dispatch ──
|
|
43
67
|
const GATEWAY_PORT = parseInt(process.env.OPENCLAW_GATEWAY_PORT || "18789", 10);
|
|
44
68
|
let _gwReqId = 0;
|
|
@@ -90,25 +114,95 @@ function extractReplyText(message: any): string {
|
|
|
90
114
|
* Open a short-lived WS to the gateway, run a single RPC call, and return the result.
|
|
91
115
|
*/
|
|
92
116
|
async function gwRpc(token: string, method: string, reqParams: any): Promise<any> {
|
|
117
|
+
const conn = await gwPoolAcquire(token);
|
|
118
|
+
try {
|
|
119
|
+
const result = await rpcCall(conn, method, reqParams);
|
|
120
|
+
gwPoolRelease(conn);
|
|
121
|
+
return result;
|
|
122
|
+
} catch (err) {
|
|
123
|
+
// On error, discard this connection from the pool
|
|
124
|
+
gwPoolDiscard(conn);
|
|
125
|
+
throw err;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ── Gateway WS Connection Pool ──
|
|
130
|
+
const GW_POOL_MAX = 3;
|
|
131
|
+
const GW_POOL_IDLE_MS = 30_000; // close idle connections after 30s
|
|
132
|
+
const _gwPool: { ws: WebSocket; token: string; busy: boolean; lastUsed: number }[] = [];
|
|
133
|
+
let _gwPoolTimer: ReturnType<typeof setInterval> | null = null;
|
|
134
|
+
|
|
135
|
+
function _gwPoolStartSweep() {
|
|
136
|
+
if (_gwPoolTimer) return;
|
|
137
|
+
_gwPoolTimer = setInterval(() => {
|
|
138
|
+
const now = Date.now();
|
|
139
|
+
for (let i = _gwPool.length - 1; i >= 0; i--) {
|
|
140
|
+
const entry = _gwPool[i];
|
|
141
|
+
if (!entry.busy && now - entry.lastUsed > GW_POOL_IDLE_MS) {
|
|
142
|
+
_gwPool.splice(i, 1);
|
|
143
|
+
try { entry.ws.close(); } catch {}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (_gwPool.length === 0 && _gwPoolTimer) {
|
|
147
|
+
clearInterval(_gwPoolTimer);
|
|
148
|
+
_gwPoolTimer = null;
|
|
149
|
+
}
|
|
150
|
+
}, 10_000);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function gwPoolAcquire(token: string): Promise<WebSocket> {
|
|
154
|
+
// Try to reuse an idle connection with the same token
|
|
155
|
+
for (const entry of _gwPool) {
|
|
156
|
+
if (!entry.busy && entry.token === token && entry.ws.readyState === WebSocket.OPEN) {
|
|
157
|
+
entry.busy = true;
|
|
158
|
+
entry.lastUsed = Date.now();
|
|
159
|
+
return entry.ws;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Create a new connection
|
|
93
164
|
const url = `ws://127.0.0.1:${GATEWAY_PORT}`;
|
|
94
165
|
const ws = new WebSocket(url, { headers: { Origin: `http://127.0.0.1:${GATEWAY_PORT}` } });
|
|
95
166
|
await new Promise<void>((resolve, reject) => { ws.on("open", resolve); ws.on("error", reject); });
|
|
96
167
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
168
|
+
// Handshake
|
|
169
|
+
const connectRes = await rpcCall(ws, "connect", {
|
|
170
|
+
minProtocol: 3, maxProtocol: 3,
|
|
171
|
+
client: { id: "openclaw-control-ui", version: "1.0.0", mode: "webchat", platform: "node" },
|
|
172
|
+
scopes: ["operator.admin", "operator.read", "operator.write"],
|
|
173
|
+
...(token ? { auth: { token } } : {}),
|
|
174
|
+
});
|
|
175
|
+
if (!connectRes) { ws.close(); throw new Error("Gateway connect failed"); }
|
|
176
|
+
|
|
177
|
+
// Evict oldest idle if pool is full
|
|
178
|
+
if (_gwPool.length >= GW_POOL_MAX) {
|
|
179
|
+
const idleIdx = _gwPool.findIndex(e => !e.busy);
|
|
180
|
+
if (idleIdx >= 0) {
|
|
181
|
+
const evicted = _gwPool.splice(idleIdx, 1)[0];
|
|
182
|
+
try { evicted.ws.close(); } catch {}
|
|
183
|
+
}
|
|
111
184
|
}
|
|
185
|
+
|
|
186
|
+
const entry = { ws, token, busy: true, lastUsed: Date.now() };
|
|
187
|
+
_gwPool.push(entry);
|
|
188
|
+
|
|
189
|
+
// Auto-remove on close/error
|
|
190
|
+
ws.on("close", () => { const idx = _gwPool.indexOf(entry); if (idx >= 0) _gwPool.splice(idx, 1); });
|
|
191
|
+
ws.on("error", () => { const idx = _gwPool.indexOf(entry); if (idx >= 0) _gwPool.splice(idx, 1); });
|
|
192
|
+
|
|
193
|
+
_gwPoolStartSweep();
|
|
194
|
+
return ws;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function gwPoolRelease(ws: WebSocket) {
|
|
198
|
+
const entry = _gwPool.find(e => e.ws === ws);
|
|
199
|
+
if (entry) { entry.busy = false; entry.lastUsed = Date.now(); }
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function gwPoolDiscard(ws: WebSocket) {
|
|
203
|
+
const idx = _gwPool.findIndex(e => e.ws === ws);
|
|
204
|
+
if (idx >= 0) _gwPool.splice(idx, 1);
|
|
205
|
+
try { ws.close(); } catch {}
|
|
112
206
|
}
|
|
113
207
|
|
|
114
208
|
/**
|
|
@@ -207,11 +301,31 @@ async function dispatchViaGateway(params: {
|
|
|
207
301
|
}
|
|
208
302
|
}
|
|
209
303
|
|
|
210
|
-
// Step 4: Send outbound via JuHeBot API
|
|
304
|
+
// Step 4: Send outbound via JuHeBot API + cache
|
|
211
305
|
if (replyText) {
|
|
212
306
|
try {
|
|
213
307
|
await sendToWeChat(cfg, params.from, replyText, params.accountId);
|
|
214
308
|
console.log(`[iPad WeChat] ✅ Outbound sent to ${params.from}`);
|
|
309
|
+
|
|
310
|
+
// Write outbound message to cache
|
|
311
|
+
try {
|
|
312
|
+
const cache = getCacheManager(cfg);
|
|
313
|
+
const isChatroom = params.from.includes("@chatroom");
|
|
314
|
+
await cache.onMessage({
|
|
315
|
+
messageId: `out_${Date.now()}`,
|
|
316
|
+
accountId: params.accountId || "default",
|
|
317
|
+
conversationType: isChatroom ? "chatroom" : "friend",
|
|
318
|
+
conversationId: params.from,
|
|
319
|
+
senderId: params.to || "self",
|
|
320
|
+
content: replyText,
|
|
321
|
+
messageType: 1,
|
|
322
|
+
timestamp: Date.now(),
|
|
323
|
+
isSelf: true,
|
|
324
|
+
direction: "outbound",
|
|
325
|
+
});
|
|
326
|
+
} catch (cacheErr) {
|
|
327
|
+
console.error("[iPad WeChat] Outbound cache write failed:", cacheErr);
|
|
328
|
+
}
|
|
215
329
|
} catch (sendErr) {
|
|
216
330
|
console.error(`[iPad WeChat] ❌ Outbound send failed:`, sendErr);
|
|
217
331
|
}
|
|
@@ -373,10 +487,13 @@ export default defineChannelPluginEntry({
|
|
|
373
487
|
const peerId = isChatroom ? (msg as any).roomId : (msg.fromUser || msg.toUser || "");
|
|
374
488
|
const peerKind = isChatroom ? "group" : "direct";
|
|
375
489
|
|
|
376
|
-
|
|
490
|
+
const resolvedAgent = resolveAgentFromBindings(cfg, "ipad-wechat");
|
|
491
|
+
console.log(`[iPad WeChat] Resolved agent: ${resolvedAgent} (from bindings)`);
|
|
492
|
+
|
|
493
|
+
let sessionKey = `agent:${resolvedAgent}:ipad-wechat:${peerKind}:${peerId}`;
|
|
377
494
|
try {
|
|
378
495
|
const route = buildChannelOutboundSessionRoute({
|
|
379
|
-
cfg, agentId:
|
|
496
|
+
cfg, agentId: resolvedAgent, channel: "ipad-wechat",
|
|
380
497
|
peer: { kind: peerKind as any, id: peerId },
|
|
381
498
|
chatType: peerKind as any,
|
|
382
499
|
from: msg.toUser || "", to: msg.fromUser || "",
|
package/openclaw.plugin.json
CHANGED
|
@@ -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": "2.
|
|
5
|
+
"version": "2.1.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
package/src/channel.ts
CHANGED
|
@@ -83,7 +83,7 @@ function initializeRegistry(cfg: OpenClawConfig): void {
|
|
|
83
83
|
/**
|
|
84
84
|
* Get or create cache manager
|
|
85
85
|
*/
|
|
86
|
-
function getCacheManager(cfg: OpenClawConfig): CacheManager {
|
|
86
|
+
export function getCacheManager(cfg: OpenClawConfig): CacheManager {
|
|
87
87
|
if (cacheManager) return cacheManager;
|
|
88
88
|
|
|
89
89
|
const section = getConfigSection(cfg);
|