palz-connector 1.5.1 → 1.5.2

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "palz-connector",
3
3
  "name": "Palz Connector Channel",
4
- "version": "1.5.1",
4
+ "version": "1.5.2",
5
5
  "description": "Palz IM 接入 OpenClaw",
6
6
  "channels": [
7
7
  "palz-connector"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "palz-connector",
3
- "version": "1.5.1",
3
+ "version": "1.5.2",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "description": "Palz IM 接入 OpenClaw — 模块化架构,基于 OpenClaw Runtime 消息管道",
package/src/monitor.js CHANGED
@@ -8,6 +8,9 @@ import WebSocket from "ws";
8
8
  import { handlePalzMessage } from "./bot.js";
9
9
  import { reportPalzActivity } from "./activity.js";
10
10
  import { tracer, extractTraceparentContext, SpanStatusCode } from "./tracing.js";
11
+ const WS_CONNECT_TIMEOUT_MS = 15_000;
12
+ const WS_PING_INTERVAL_MS = 30_000;
13
+ const WS_PONG_TIMEOUT_MS = 70_000;
11
14
  export async function monitorPalzProvider(params) {
12
15
  const { cfg, config, runtime, abortSignal, accountId } = params;
13
16
  const log = typeof runtime?.log === "function" ? runtime.log : console.log;
@@ -24,6 +27,7 @@ export async function monitorPalzProvider(params) {
24
27
  let reconnectDelay = 1000;
25
28
  let reconnectTimer = null;
26
29
  let currentWs = null;
30
+ let disposeCurrentSocket = null;
27
31
  let consecutive4002 = 0;
28
32
  const MAX_CONSECUTIVE_4002 = 10;
29
33
  const MAX_RECONNECT_DELAY_MS = 16_000;
@@ -33,6 +37,10 @@ export async function monitorPalzProvider(params) {
33
37
  clearTimeout(reconnectTimer);
34
38
  reconnectTimer = null;
35
39
  }
40
+ if (disposeCurrentSocket) {
41
+ disposeCurrentSocket();
42
+ disposeCurrentSocket = null;
43
+ }
36
44
  if (currentWs) {
37
45
  currentWs.removeAllListeners();
38
46
  if (currentWs.readyState === WebSocket.OPEN || currentWs.readyState === WebSocket.CLOSING) {
@@ -91,15 +99,29 @@ export async function monitorPalzProvider(params) {
91
99
  currentWs = ws;
92
100
  let connectedAt = 0;
93
101
  let pingInterval = null;
102
+ let connectTimeout = null;
103
+ let lastPingAt = 0;
104
+ let lastPongAt = Date.now();
94
105
  let messageCount = 0;
95
106
  let ended = false;
107
+ const disposeSocketTimers = () => {
108
+ if (connectTimeout) {
109
+ clearTimeout(connectTimeout);
110
+ connectTimeout = null;
111
+ }
112
+ if (pingInterval) {
113
+ clearInterval(pingInterval);
114
+ pingInterval = null;
115
+ }
116
+ };
117
+ disposeCurrentSocket = disposeSocketTimers;
96
118
  const handleSocketEnd = (code, reasonStr) => {
97
119
  if (ended)
98
120
  return;
99
121
  ended = true;
100
- if (pingInterval) {
101
- clearInterval(pingInterval);
102
- pingInterval = null;
122
+ disposeSocketTimers();
123
+ if (disposeCurrentSocket === disposeSocketTimers) {
124
+ disposeCurrentSocket = null;
103
125
  }
104
126
  if (closed)
105
127
  return;
@@ -133,18 +155,47 @@ export async function monitorPalzProvider(params) {
133
155
  log(`palz[${accountId}]: disconnected (code=${code}, reason=${reasonStr}, uptime=${Math.round(stableMs / 1000)}s, msgs=${messageCount}), reconnecting in ${reconnectDelay}ms`);
134
156
  scheduleReconnect();
135
157
  };
158
+ connectTimeout = setTimeout(() => {
159
+ if (ws.readyState !== WebSocket.CONNECTING)
160
+ return;
161
+ error(`palz[${accountId}]: WebSocket connect timeout after ${WS_CONNECT_TIMEOUT_MS}ms`);
162
+ ws.terminate();
163
+ handleSocketEnd(1006, `connect timeout ${WS_CONNECT_TIMEOUT_MS}ms`);
164
+ }, WS_CONNECT_TIMEOUT_MS);
136
165
  ws.on("open", () => {
166
+ if (connectTimeout) {
167
+ clearTimeout(connectTimeout);
168
+ connectTimeout = null;
169
+ }
137
170
  connectedAt = Date.now();
171
+ lastPongAt = connectedAt;
138
172
  consecutive4002 = 0;
139
173
  log(`palz[${accountId}]: WebSocket connected, bot_id=${config.botId}`);
140
174
  pingInterval = setInterval(() => {
141
175
  if (ws.readyState === WebSocket.OPEN) {
176
+ const timeSinceLastPong = Date.now() - lastPongAt;
177
+ if (timeSinceLastPong > WS_PONG_TIMEOUT_MS) {
178
+ error(`palz[${accountId}]: WebSocket pong timeout after ${timeSinceLastPong}ms, terminating`);
179
+ ws.terminate();
180
+ handleSocketEnd(1006, `pong timeout ${timeSinceLastPong}ms`);
181
+ return;
182
+ }
142
183
  try {
184
+ lastPingAt = Date.now();
143
185
  ws.ping();
186
+ log(`palz[${accountId}]: [WS_HEARTBEAT] ping sent, last_pong_ago=${timeSinceLastPong}ms`);
187
+ }
188
+ catch (err) {
189
+ error(`palz[${accountId}]: [WS_HEARTBEAT] ping failed: ${err.message ?? err}`);
144
190
  }
145
- catch { }
146
191
  }
147
- }, 30_000);
192
+ }, WS_PING_INTERVAL_MS);
193
+ });
194
+ ws.on("pong", () => {
195
+ const now = Date.now();
196
+ const rttMs = lastPingAt > 0 ? now - lastPingAt : null;
197
+ lastPongAt = now;
198
+ log(`palz[${accountId}]: [WS_HEARTBEAT] pong received${rttMs === null ? "" : `, rtt=${rttMs}ms`}`);
148
199
  });
149
200
  ws.on("message", (data) => {
150
201
  const receivedAt = new Date();
package/src/send.js CHANGED
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { tracer, trace, SpanStatusCode, buildTraceparentHeader } from "./tracing.js";
8
8
  import { contentPartsToOpenAIContent } from "./content.js";
9
+ const SEND_TIMEOUT_MS = 10_000;
9
10
  let msgSeq = 0;
10
11
  function nextMsgId() {
11
12
  return `bot_reply_${Date.now()}_${++msgSeq}`;
@@ -83,13 +84,32 @@ async function _sendToPalzIMInner(params) {
83
84
  headers["Traceparent"] = traceparent;
84
85
  }
85
86
  const startMs = Date.now();
86
- const response = await fetch(url, {
87
- method: "POST",
88
- headers,
89
- body: reqBodyStr,
90
- });
91
- const elapsedMs = Date.now() - startMs;
87
+ const controller = new AbortController();
88
+ const timeout = setTimeout(() => controller.abort(), SEND_TIMEOUT_MS);
89
+ let response;
90
+ try {
91
+ response = await fetch(url, {
92
+ method: "POST",
93
+ headers,
94
+ body: reqBodyStr,
95
+ signal: controller.signal,
96
+ });
97
+ }
98
+ catch (err) {
99
+ const elapsedMs = Date.now() - startMs;
100
+ const reason = err?.name === "AbortError" ? `timeout ${SEND_TIMEOUT_MS}ms` : (err?.message ?? String(err));
101
+ const failLog = `[HTTP_RES] ERROR elapsed=${elapsedMs}ms reason=${reason}`;
102
+ console.error(`palz-send: ${failLog}`);
103
+ span?.addEvent(failLog);
104
+ throw err?.name === "AbortError"
105
+ ? new Error(`Palz send timeout after ${SEND_TIMEOUT_MS}ms`)
106
+ : err;
107
+ }
108
+ finally {
109
+ clearTimeout(timeout);
110
+ }
92
111
  const rawText = await response.text().catch(() => "");
112
+ const elapsedMs = Date.now() - startMs;
93
113
  let body = null;
94
114
  try {
95
115
  body = JSON.parse(rawText);