@workclaw/openclaw-workchat 1.0.0 → 1.0.4
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 +210 -210
- package/index.js +49568 -0
- package/index.ts +19 -19
- package/openclaw.plugin.json +25 -25
- package/package.json +64 -63
- package/src/api/api.ts +260 -260
- package/src/api/types.ts +56 -56
- package/src/auth/accounts.ts +279 -290
- package/src/auth/bind-agent.ts +52 -70
- package/src/auth/login-qr-terminal.ts +16 -16
- package/src/auth/login-qr.ts +477 -475
- package/src/auth/pairing.ts +133 -133
- package/src/channel.ts +282 -282
- package/src/compat.ts +75 -75
- package/src/config/config-schema.ts +29 -29
- package/src/messaging/debug-mode.ts +57 -57
- package/src/messaging/error-notice.ts +30 -30
- package/src/messaging/inbound.ts +174 -174
- package/src/messaging/markdown-filter.ts +361 -361
- package/src/messaging/outbound-hooks.ts +84 -84
- package/src/messaging/process-message.ts +530 -530
- package/src/messaging/send.ts +119 -119
- package/src/messaging/slash-commands.ts +105 -105
- package/src/monitor/monitor.ts +261 -261
- package/src/storage/state-dir.ts +11 -11
- package/src/util/logger.ts +147 -147
- package/src/util/random.ts +9 -9
- package/src/util/redact.ts +39 -39
- package/src/vendor.d.ts +10 -10
package/src/monitor/monitor.ts
CHANGED
|
@@ -1,261 +1,261 @@
|
|
|
1
|
-
import { Agent, WebSocket } from "undici";
|
|
2
|
-
|
|
3
|
-
import type { ChannelAccountSnapshot } from "openclaw/plugin-sdk/channel-contract";
|
|
4
|
-
import type { PluginRuntime } from "openclaw/plugin-sdk";
|
|
5
|
-
|
|
6
|
-
import { openConnection, clearWorkChatTokenCache } from "../api/api.js";
|
|
7
|
-
import type { WorkChatApiOptions } from "../api/api.js";
|
|
8
|
-
import { normalizeBaseUrl } from "../api/api.js";
|
|
9
|
-
import { buildPongFrame, parseWorkChatFrame } from "../messaging/inbound.js";
|
|
10
|
-
import { processOneFrame } from "../messaging/process-message.js";
|
|
11
|
-
import { logger } from "../util/logger.js";
|
|
12
|
-
import type { Logger } from "../util/logger.js";
|
|
13
|
-
|
|
14
|
-
const WS_CONNECT_TIMEOUT_MS = 30_000;
|
|
15
|
-
const RECONNECT_BACKOFF_BASE_MS = 5_000;
|
|
16
|
-
const RECONNECT_BACKOFF_MAX_MS = 5 * 60_000;
|
|
17
|
-
const RATE_LIMIT_BACKOFF_MS = 1_000;
|
|
18
|
-
|
|
19
|
-
export type MonitorWorkChatOpts = {
|
|
20
|
-
apiOpts: WorkChatApiOptions;
|
|
21
|
-
websocketUrl?: string;
|
|
22
|
-
localIp?: string;
|
|
23
|
-
accountId: string;
|
|
24
|
-
account: import("../auth/accounts.js").ResolvedWorkChatAccount;
|
|
25
|
-
config: import("openclaw/plugin-sdk").OpenClawConfig;
|
|
26
|
-
runtime?: { log?: (msg: string) => void; error?: (msg: string) => void };
|
|
27
|
-
/**
|
|
28
|
-
* Gateway-injected channel runtime surface (reply/routing/session/media/commands/...).
|
|
29
|
-
* Required for inbound message processing; provided by `ChannelGatewayContext.channelRuntime`.
|
|
30
|
-
*/
|
|
31
|
-
channelRuntime: PluginRuntime["channel"];
|
|
32
|
-
abortSignal?: AbortSignal;
|
|
33
|
-
/** Gateway status callback — called on connect and inbound messages. */
|
|
34
|
-
setStatus?: (next: ChannelAccountSnapshot) => void;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
function isAuthError(errorStr: string): boolean {
|
|
38
|
-
return (
|
|
39
|
-
errorStr.includes('"errCode":2001')
|
|
40
|
-
|| errorStr.includes('"code":2001')
|
|
41
|
-
|| errorStr.includes("鉴权失败")
|
|
42
|
-
|| errorStr.includes("invalid credentials")
|
|
43
|
-
|| errorStr.includes("unauthorized")
|
|
44
|
-
|| errorStr.includes("missing appKey/appSecret")
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function sleep(ms: number, abortSignal?: AbortSignal): Promise<void> {
|
|
49
|
-
return new Promise(resolve => {
|
|
50
|
-
const timer = setTimeout(() => {
|
|
51
|
-
abortSignal?.removeEventListener("abort", onAbort);
|
|
52
|
-
resolve();
|
|
53
|
-
}, ms);
|
|
54
|
-
function onAbort(): void {
|
|
55
|
-
clearTimeout(timer);
|
|
56
|
-
resolve();
|
|
57
|
-
}
|
|
58
|
-
if (abortSignal?.aborted) {
|
|
59
|
-
clearTimeout(timer);
|
|
60
|
-
resolve();
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* WebSocket monitor loop: open connection → connect → dispatch frames.
|
|
69
|
-
* Connection loss reconnects with exponential backoff; auth failures stop
|
|
70
|
-
* the loop; rate limiting retries with minimal backoff.
|
|
71
|
-
*/
|
|
72
|
-
export async function monitorWorkChatProvider(opts: MonitorWorkChatOpts): Promise<void> {
|
|
73
|
-
const { apiOpts, accountId, config, channelRuntime, abortSignal, setStatus } = opts;
|
|
74
|
-
const log = opts.runtime?.log ?? (() => {});
|
|
75
|
-
const errLog = opts.runtime?.error ?? ((m: string) => log(m));
|
|
76
|
-
const aLog: Logger = logger.withAccount(accountId);
|
|
77
|
-
|
|
78
|
-
if (!channelRuntime) {
|
|
79
|
-
const msg = "channelRuntime missing on monitor opts; gateway must inject ChannelGatewayContext.channelRuntime";
|
|
80
|
-
aLog.error(msg);
|
|
81
|
-
throw new Error(msg);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
log(`workchat monitor started (${normalizeBaseUrl(apiOpts.baseUrl)}, account=${accountId})`);
|
|
85
|
-
aLog.info(`Monitor started: baseUrl=${normalizeBaseUrl(apiOpts.baseUrl)}`);
|
|
86
|
-
|
|
87
|
-
let backoffMs = RECONNECT_BACKOFF_BASE_MS;
|
|
88
|
-
|
|
89
|
-
while (!abortSignal?.aborted) {
|
|
90
|
-
let ws: WebSocket | null = null;
|
|
91
|
-
try {
|
|
92
|
-
const tokenCacheKey = apiOpts.appKey || accountId;
|
|
93
|
-
clearWorkChatTokenCache(tokenCacheKey);
|
|
94
|
-
const { endpoint, ticket } = await openConnection({
|
|
95
|
-
...apiOpts,
|
|
96
|
-
websocketUrl: opts.websocketUrl,
|
|
97
|
-
localIp: opts.localIp,
|
|
98
|
-
});
|
|
99
|
-
const url = `${endpoint}?ticket=${encodeURIComponent(ticket)}`;
|
|
100
|
-
|
|
101
|
-
const dispatcher = apiOpts.allowInsecureTls
|
|
102
|
-
? new Agent({ connect: { rejectUnauthorized: false } })
|
|
103
|
-
: undefined;
|
|
104
|
-
ws = new WebSocket(url, dispatcher ? { dispatcher } : undefined);
|
|
105
|
-
|
|
106
|
-
await new Promise<void>((resolve, reject) => {
|
|
107
|
-
const timer = setTimeout(
|
|
108
|
-
() => reject(new Error("workchat websocket connection timeout")),
|
|
109
|
-
WS_CONNECT_TIMEOUT_MS,
|
|
110
|
-
);
|
|
111
|
-
ws!.onopen = () => {
|
|
112
|
-
clearTimeout(timer);
|
|
113
|
-
resolve();
|
|
114
|
-
};
|
|
115
|
-
ws!.onerror = (event: unknown) => {
|
|
116
|
-
clearTimeout(timer);
|
|
117
|
-
reject(new Error(String((event as any)?.message ?? event)));
|
|
118
|
-
};
|
|
119
|
-
ws!.onclose = (event: unknown) => {
|
|
120
|
-
clearTimeout(timer);
|
|
121
|
-
reject(new Error(`workchat websocket closed before ready code=${String((event as any)?.code ?? "")}`));
|
|
122
|
-
};
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
aLog.info("WebSocket connected");
|
|
126
|
-
setStatus?.({
|
|
127
|
-
accountId,
|
|
128
|
-
running: true,
|
|
129
|
-
lastStartAt: Date.now(),
|
|
130
|
-
lastEventAt: Date.now(),
|
|
131
|
-
});
|
|
132
|
-
backoffMs = RECONNECT_BACKOFF_BASE_MS;
|
|
133
|
-
|
|
134
|
-
ws.onmessage = (event: unknown) => {
|
|
135
|
-
const data = typeof (event as any).data === "string"
|
|
136
|
-
? (event as any).data
|
|
137
|
-
: String((event as any).data ?? "");
|
|
138
|
-
void handleFrame(data, ws);
|
|
139
|
-
};
|
|
140
|
-
let connectionClosed!: () => void;
|
|
141
|
-
const connectionClosedPromise = new Promise<void>(resolve => {
|
|
142
|
-
connectionClosed = resolve;
|
|
143
|
-
});
|
|
144
|
-
ws.onclose = (event: unknown) => {
|
|
145
|
-
aLog.warn(`WebSocket closed code=${String((event as any)?.code ?? "")} reason=${String((event as any)?.reason ?? "")}`);
|
|
146
|
-
scheduleReconnect();
|
|
147
|
-
connectionClosed();
|
|
148
|
-
};
|
|
149
|
-
ws.onerror = (event: unknown) => {
|
|
150
|
-
aLog.error(`WebSocket error: ${String((event as any)?.message ?? event)}`);
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
// Stay in this connection until it closes or the account is aborted;
|
|
154
|
-
// closing resolves the wait and lets the outer while-loop reconnect
|
|
155
|
-
// with backoff (the previous open-ended wait never resolved on close,
|
|
156
|
-
// so a mid-session 1005 close left the monitor stuck forever).
|
|
157
|
-
await new Promise<void>(resolve => {
|
|
158
|
-
const settle = (): void => {
|
|
159
|
-
abortSignal?.removeEventListener("abort", onAbort);
|
|
160
|
-
resolve();
|
|
161
|
-
};
|
|
162
|
-
const onAbort = (): void => {
|
|
163
|
-
settle();
|
|
164
|
-
try {
|
|
165
|
-
ws?.close();
|
|
166
|
-
} catch {
|
|
167
|
-
// ignore
|
|
168
|
-
}
|
|
169
|
-
};
|
|
170
|
-
if (abortSignal?.aborted) {
|
|
171
|
-
settle();
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
175
|
-
connectionClosedPromise.then(settle);
|
|
176
|
-
});
|
|
177
|
-
// Connection ended mid-session (not aborted): loop again to reconnect.
|
|
178
|
-
if (!abortSignal?.aborted) {
|
|
179
|
-
await sleep(backoffMs, abortSignal);
|
|
180
|
-
backoffMs = Math.min(backoffMs * 2, RECONNECT_BACKOFF_MAX_MS);
|
|
181
|
-
continue;
|
|
182
|
-
}
|
|
183
|
-
return;
|
|
184
|
-
} catch (err) {
|
|
185
|
-
const errorStr = String(err);
|
|
186
|
-
aLog.error(`connect failed: ${errorStr}`);
|
|
187
|
-
errLog(`workchat connect failed: ${errorStr}`);
|
|
188
|
-
|
|
189
|
-
if (isAuthError(errorStr)) {
|
|
190
|
-
aLog.error("authentication failed, not reconnecting");
|
|
191
|
-
setStatus?.({ accountId, running: false, lastError: errorStr });
|
|
192
|
-
throw err;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
if (errorStr.includes("访问过于频繁")) {
|
|
196
|
-
backoffMs = RATE_LIMIT_BACKOFF_MS;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
await sleep(backoffMs, abortSignal);
|
|
200
|
-
backoffMs = Math.min(backoffMs * 2, RECONNECT_BACKOFF_MAX_MS);
|
|
201
|
-
} finally {
|
|
202
|
-
try {
|
|
203
|
-
if (abortSignal?.aborted) ws?.close();
|
|
204
|
-
} catch {
|
|
205
|
-
// ignore
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function scheduleReconnect(): void {
|
|
211
|
-
// The connection-wait resolves on close so the outer while-loop re-runs
|
|
212
|
-
// and reconnects with backoff; this hook only logs the transition.
|
|
213
|
-
aLog.info(`reconnect scheduled via monitor loop (backoff=${backoffMs}ms)`);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
async function handleFrame(data: string, ws: WebSocket | null): Promise<void> {
|
|
217
|
-
aLog.debug(`frame received bytes=${data.length}`);
|
|
218
|
-
const result = parseWorkChatFrame(data);
|
|
219
|
-
if (!result) {
|
|
220
|
-
aLog.warn("failed to parse frame");
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
if (result.type === "ping") {
|
|
225
|
-
try {
|
|
226
|
-
ws?.send(buildPongFrame(result.pongData));
|
|
227
|
-
aLog.debug("pong sent");
|
|
228
|
-
// Ping/pong proves the WS is bidirectionally alive — surface it so
|
|
229
|
-
// connectivity diagnostics can distinguish live from zombie sockets.
|
|
230
|
-
setStatus?.({ accountId, running: true, lastEventAt: Date.now() });
|
|
231
|
-
} catch (err) {
|
|
232
|
-
aLog.error(`failed to send pong: ${String(err)}`);
|
|
233
|
-
}
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
if (result.type === "disconnect") {
|
|
237
|
-
aLog.info("disconnect command received, closing WebSocket");
|
|
238
|
-
try {
|
|
239
|
-
ws?.close();
|
|
240
|
-
} catch {
|
|
241
|
-
// ignore
|
|
242
|
-
}
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
setStatus?.({ accountId, running: true, lastEventAt: Date.now() });
|
|
247
|
-
try {
|
|
248
|
-
await processOneFrame(result, data, {
|
|
249
|
-
accountId,
|
|
250
|
-
account: opts.account,
|
|
251
|
-
config,
|
|
252
|
-
channelRuntime,
|
|
253
|
-
apiOpts,
|
|
254
|
-
log,
|
|
255
|
-
errLog,
|
|
256
|
-
});
|
|
257
|
-
} catch (err) {
|
|
258
|
-
aLog.error(`processOneFrame failed: ${String(err)}`);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
}
|
|
1
|
+
import { Agent, WebSocket } from "undici";
|
|
2
|
+
|
|
3
|
+
import type { ChannelAccountSnapshot } from "openclaw/plugin-sdk/channel-contract";
|
|
4
|
+
import type { PluginRuntime } from "openclaw/plugin-sdk";
|
|
5
|
+
|
|
6
|
+
import { openConnection, clearWorkChatTokenCache } from "../api/api.js";
|
|
7
|
+
import type { WorkChatApiOptions } from "../api/api.js";
|
|
8
|
+
import { normalizeBaseUrl } from "../api/api.js";
|
|
9
|
+
import { buildPongFrame, parseWorkChatFrame } from "../messaging/inbound.js";
|
|
10
|
+
import { processOneFrame } from "../messaging/process-message.js";
|
|
11
|
+
import { logger } from "../util/logger.js";
|
|
12
|
+
import type { Logger } from "../util/logger.js";
|
|
13
|
+
|
|
14
|
+
const WS_CONNECT_TIMEOUT_MS = 30_000;
|
|
15
|
+
const RECONNECT_BACKOFF_BASE_MS = 5_000;
|
|
16
|
+
const RECONNECT_BACKOFF_MAX_MS = 5 * 60_000;
|
|
17
|
+
const RATE_LIMIT_BACKOFF_MS = 1_000;
|
|
18
|
+
|
|
19
|
+
export type MonitorWorkChatOpts = {
|
|
20
|
+
apiOpts: WorkChatApiOptions;
|
|
21
|
+
websocketUrl?: string;
|
|
22
|
+
localIp?: string;
|
|
23
|
+
accountId: string;
|
|
24
|
+
account: import("../auth/accounts.js").ResolvedWorkChatAccount;
|
|
25
|
+
config: import("openclaw/plugin-sdk").OpenClawConfig;
|
|
26
|
+
runtime?: { log?: (msg: string) => void; error?: (msg: string) => void };
|
|
27
|
+
/**
|
|
28
|
+
* Gateway-injected channel runtime surface (reply/routing/session/media/commands/...).
|
|
29
|
+
* Required for inbound message processing; provided by `ChannelGatewayContext.channelRuntime`.
|
|
30
|
+
*/
|
|
31
|
+
channelRuntime: PluginRuntime["channel"];
|
|
32
|
+
abortSignal?: AbortSignal;
|
|
33
|
+
/** Gateway status callback — called on connect and inbound messages. */
|
|
34
|
+
setStatus?: (next: ChannelAccountSnapshot) => void;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function isAuthError(errorStr: string): boolean {
|
|
38
|
+
return (
|
|
39
|
+
errorStr.includes('"errCode":2001')
|
|
40
|
+
|| errorStr.includes('"code":2001')
|
|
41
|
+
|| errorStr.includes("鉴权失败")
|
|
42
|
+
|| errorStr.includes("invalid credentials")
|
|
43
|
+
|| errorStr.includes("unauthorized")
|
|
44
|
+
|| errorStr.includes("missing appKey/appSecret")
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function sleep(ms: number, abortSignal?: AbortSignal): Promise<void> {
|
|
49
|
+
return new Promise(resolve => {
|
|
50
|
+
const timer = setTimeout(() => {
|
|
51
|
+
abortSignal?.removeEventListener("abort", onAbort);
|
|
52
|
+
resolve();
|
|
53
|
+
}, ms);
|
|
54
|
+
function onAbort(): void {
|
|
55
|
+
clearTimeout(timer);
|
|
56
|
+
resolve();
|
|
57
|
+
}
|
|
58
|
+
if (abortSignal?.aborted) {
|
|
59
|
+
clearTimeout(timer);
|
|
60
|
+
resolve();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* WebSocket monitor loop: open connection → connect → dispatch frames.
|
|
69
|
+
* Connection loss reconnects with exponential backoff; auth failures stop
|
|
70
|
+
* the loop; rate limiting retries with minimal backoff.
|
|
71
|
+
*/
|
|
72
|
+
export async function monitorWorkChatProvider(opts: MonitorWorkChatOpts): Promise<void> {
|
|
73
|
+
const { apiOpts, accountId, config, channelRuntime, abortSignal, setStatus } = opts;
|
|
74
|
+
const log = opts.runtime?.log ?? (() => {});
|
|
75
|
+
const errLog = opts.runtime?.error ?? ((m: string) => log(m));
|
|
76
|
+
const aLog: Logger = logger.withAccount(accountId);
|
|
77
|
+
|
|
78
|
+
if (!channelRuntime) {
|
|
79
|
+
const msg = "channelRuntime missing on monitor opts; gateway must inject ChannelGatewayContext.channelRuntime";
|
|
80
|
+
aLog.error(msg);
|
|
81
|
+
throw new Error(msg);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
log(`workchat monitor started (${normalizeBaseUrl(apiOpts.baseUrl)}, account=${accountId})`);
|
|
85
|
+
aLog.info(`Monitor started: baseUrl=${normalizeBaseUrl(apiOpts.baseUrl)}`);
|
|
86
|
+
|
|
87
|
+
let backoffMs = RECONNECT_BACKOFF_BASE_MS;
|
|
88
|
+
|
|
89
|
+
while (!abortSignal?.aborted) {
|
|
90
|
+
let ws: WebSocket | null = null;
|
|
91
|
+
try {
|
|
92
|
+
const tokenCacheKey = apiOpts.appKey || accountId;
|
|
93
|
+
clearWorkChatTokenCache(tokenCacheKey);
|
|
94
|
+
const { endpoint, ticket } = await openConnection({
|
|
95
|
+
...apiOpts,
|
|
96
|
+
websocketUrl: opts.websocketUrl,
|
|
97
|
+
localIp: opts.localIp,
|
|
98
|
+
});
|
|
99
|
+
const url = `${endpoint}?ticket=${encodeURIComponent(ticket)}`;
|
|
100
|
+
|
|
101
|
+
const dispatcher = apiOpts.allowInsecureTls
|
|
102
|
+
? new Agent({ connect: { rejectUnauthorized: false } })
|
|
103
|
+
: undefined;
|
|
104
|
+
ws = new WebSocket(url, dispatcher ? { dispatcher } : undefined);
|
|
105
|
+
|
|
106
|
+
await new Promise<void>((resolve, reject) => {
|
|
107
|
+
const timer = setTimeout(
|
|
108
|
+
() => reject(new Error("workchat websocket connection timeout")),
|
|
109
|
+
WS_CONNECT_TIMEOUT_MS,
|
|
110
|
+
);
|
|
111
|
+
ws!.onopen = () => {
|
|
112
|
+
clearTimeout(timer);
|
|
113
|
+
resolve();
|
|
114
|
+
};
|
|
115
|
+
ws!.onerror = (event: unknown) => {
|
|
116
|
+
clearTimeout(timer);
|
|
117
|
+
reject(new Error(String((event as any)?.message ?? event)));
|
|
118
|
+
};
|
|
119
|
+
ws!.onclose = (event: unknown) => {
|
|
120
|
+
clearTimeout(timer);
|
|
121
|
+
reject(new Error(`workchat websocket closed before ready code=${String((event as any)?.code ?? "")}`));
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
aLog.info("WebSocket connected");
|
|
126
|
+
setStatus?.({
|
|
127
|
+
accountId,
|
|
128
|
+
running: true,
|
|
129
|
+
lastStartAt: Date.now(),
|
|
130
|
+
lastEventAt: Date.now(),
|
|
131
|
+
});
|
|
132
|
+
backoffMs = RECONNECT_BACKOFF_BASE_MS;
|
|
133
|
+
|
|
134
|
+
ws.onmessage = (event: unknown) => {
|
|
135
|
+
const data = typeof (event as any).data === "string"
|
|
136
|
+
? (event as any).data
|
|
137
|
+
: String((event as any).data ?? "");
|
|
138
|
+
void handleFrame(data, ws);
|
|
139
|
+
};
|
|
140
|
+
let connectionClosed!: () => void;
|
|
141
|
+
const connectionClosedPromise = new Promise<void>(resolve => {
|
|
142
|
+
connectionClosed = resolve;
|
|
143
|
+
});
|
|
144
|
+
ws.onclose = (event: unknown) => {
|
|
145
|
+
aLog.warn(`WebSocket closed code=${String((event as any)?.code ?? "")} reason=${String((event as any)?.reason ?? "")}`);
|
|
146
|
+
scheduleReconnect();
|
|
147
|
+
connectionClosed();
|
|
148
|
+
};
|
|
149
|
+
ws.onerror = (event: unknown) => {
|
|
150
|
+
aLog.error(`WebSocket error: ${String((event as any)?.message ?? event)}`);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// Stay in this connection until it closes or the account is aborted;
|
|
154
|
+
// closing resolves the wait and lets the outer while-loop reconnect
|
|
155
|
+
// with backoff (the previous open-ended wait never resolved on close,
|
|
156
|
+
// so a mid-session 1005 close left the monitor stuck forever).
|
|
157
|
+
await new Promise<void>(resolve => {
|
|
158
|
+
const settle = (): void => {
|
|
159
|
+
abortSignal?.removeEventListener("abort", onAbort);
|
|
160
|
+
resolve();
|
|
161
|
+
};
|
|
162
|
+
const onAbort = (): void => {
|
|
163
|
+
settle();
|
|
164
|
+
try {
|
|
165
|
+
ws?.close();
|
|
166
|
+
} catch {
|
|
167
|
+
// ignore
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
if (abortSignal?.aborted) {
|
|
171
|
+
settle();
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
175
|
+
connectionClosedPromise.then(settle);
|
|
176
|
+
});
|
|
177
|
+
// Connection ended mid-session (not aborted): loop again to reconnect.
|
|
178
|
+
if (!abortSignal?.aborted) {
|
|
179
|
+
await sleep(backoffMs, abortSignal);
|
|
180
|
+
backoffMs = Math.min(backoffMs * 2, RECONNECT_BACKOFF_MAX_MS);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
return;
|
|
184
|
+
} catch (err) {
|
|
185
|
+
const errorStr = String(err);
|
|
186
|
+
aLog.error(`connect failed: ${errorStr}`);
|
|
187
|
+
errLog(`workchat connect failed: ${errorStr}`);
|
|
188
|
+
|
|
189
|
+
if (isAuthError(errorStr)) {
|
|
190
|
+
aLog.error("authentication failed, not reconnecting");
|
|
191
|
+
setStatus?.({ accountId, running: false, lastError: errorStr });
|
|
192
|
+
throw err;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (errorStr.includes("访问过于频繁")) {
|
|
196
|
+
backoffMs = RATE_LIMIT_BACKOFF_MS;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
await sleep(backoffMs, abortSignal);
|
|
200
|
+
backoffMs = Math.min(backoffMs * 2, RECONNECT_BACKOFF_MAX_MS);
|
|
201
|
+
} finally {
|
|
202
|
+
try {
|
|
203
|
+
if (abortSignal?.aborted) ws?.close();
|
|
204
|
+
} catch {
|
|
205
|
+
// ignore
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function scheduleReconnect(): void {
|
|
211
|
+
// The connection-wait resolves on close so the outer while-loop re-runs
|
|
212
|
+
// and reconnects with backoff; this hook only logs the transition.
|
|
213
|
+
aLog.info(`reconnect scheduled via monitor loop (backoff=${backoffMs}ms)`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function handleFrame(data: string, ws: WebSocket | null): Promise<void> {
|
|
217
|
+
aLog.debug(`frame received bytes=${data.length}`);
|
|
218
|
+
const result = parseWorkChatFrame(data);
|
|
219
|
+
if (!result) {
|
|
220
|
+
aLog.warn("failed to parse frame");
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (result.type === "ping") {
|
|
225
|
+
try {
|
|
226
|
+
ws?.send(buildPongFrame(result.pongData));
|
|
227
|
+
aLog.debug("pong sent");
|
|
228
|
+
// Ping/pong proves the WS is bidirectionally alive — surface it so
|
|
229
|
+
// connectivity diagnostics can distinguish live from zombie sockets.
|
|
230
|
+
setStatus?.({ accountId, running: true, lastEventAt: Date.now() });
|
|
231
|
+
} catch (err) {
|
|
232
|
+
aLog.error(`failed to send pong: ${String(err)}`);
|
|
233
|
+
}
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (result.type === "disconnect") {
|
|
237
|
+
aLog.info("disconnect command received, closing WebSocket");
|
|
238
|
+
try {
|
|
239
|
+
ws?.close();
|
|
240
|
+
} catch {
|
|
241
|
+
// ignore
|
|
242
|
+
}
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
setStatus?.({ accountId, running: true, lastEventAt: Date.now() });
|
|
247
|
+
try {
|
|
248
|
+
await processOneFrame(result, data, {
|
|
249
|
+
accountId,
|
|
250
|
+
account: opts.account,
|
|
251
|
+
config,
|
|
252
|
+
channelRuntime,
|
|
253
|
+
apiOpts,
|
|
254
|
+
log,
|
|
255
|
+
errLog,
|
|
256
|
+
});
|
|
257
|
+
} catch (err) {
|
|
258
|
+
aLog.error(`processOneFrame failed: ${String(err)}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
package/src/storage/state-dir.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import os from "node:os";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
|
|
4
|
-
/** Resolve the OpenClaw state directory (mirrors core logic in src/infra). */
|
|
5
|
-
export function resolveStateDir(): string {
|
|
6
|
-
return (
|
|
7
|
-
process.env.OPENCLAW_STATE_DIR?.trim() ||
|
|
8
|
-
process.env.CLAWDBOT_STATE_DIR?.trim() ||
|
|
9
|
-
path.join(os.homedir(), ".openclaw")
|
|
10
|
-
);
|
|
11
|
-
}
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
/** Resolve the OpenClaw state directory (mirrors core logic in src/infra). */
|
|
5
|
+
export function resolveStateDir(): string {
|
|
6
|
+
return (
|
|
7
|
+
process.env.OPENCLAW_STATE_DIR?.trim() ||
|
|
8
|
+
process.env.CLAWDBOT_STATE_DIR?.trim() ||
|
|
9
|
+
path.join(os.homedir(), ".openclaw")
|
|
10
|
+
);
|
|
11
|
+
}
|