@yanhaidao/wecom 2.3.10 → 2.3.12
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 +54 -23
- package/changelog/v2.3.11.md +19 -0
- package/changelog/v2.3.12.md +23 -0
- package/package.json +1 -1
- package/src/app/account-runtime.ts +5 -4
- package/src/app/index.ts +19 -0
- package/src/onboarding.test.ts +50 -0
- package/src/onboarding.ts +4 -1
- package/src/outbound.test.ts +153 -1
- package/src/outbound.ts +105 -10
- package/src/runtime.ts +3 -0
- package/src/transport/bot-ws/reply.test.ts +137 -0
- package/src/transport/bot-ws/reply.ts +93 -18
- package/src/transport/bot-ws/sdk-adapter.test.ts +124 -0
- package/src/transport/bot-ws/sdk-adapter.ts +58 -2
- package/CLAUDE.md +0 -238
package/src/runtime.ts
CHANGED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { describe, expect, it, vi, afterEach } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { createBotWsReplyHandle } from "./reply.js";
|
|
4
|
+
|
|
5
|
+
type ReplyHandleParams = Parameters<typeof createBotWsReplyHandle>[0];
|
|
6
|
+
|
|
7
|
+
describe("createBotWsReplyHandle", () => {
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
vi.useRealTimers();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("uses configured placeholder content for immediate ws ack", async () => {
|
|
13
|
+
const replyStream = vi.fn().mockResolvedValue(undefined);
|
|
14
|
+
createBotWsReplyHandle({
|
|
15
|
+
client: {
|
|
16
|
+
replyStream,
|
|
17
|
+
} as unknown as ReplyHandleParams["client"],
|
|
18
|
+
frame: {
|
|
19
|
+
headers: { req_id: "req-1" },
|
|
20
|
+
body: {},
|
|
21
|
+
} as unknown as ReplyHandleParams["frame"],
|
|
22
|
+
accountId: "default",
|
|
23
|
+
placeholderContent: "正在思考...",
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
expect(replyStream).toHaveBeenCalledWith(
|
|
27
|
+
expect.objectContaining({
|
|
28
|
+
headers: { req_id: "req-1" },
|
|
29
|
+
}),
|
|
30
|
+
expect.any(String),
|
|
31
|
+
"正在思考...",
|
|
32
|
+
false,
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("keeps placeholder alive until the first real ws chunk arrives", async () => {
|
|
37
|
+
vi.useFakeTimers();
|
|
38
|
+
|
|
39
|
+
const replyStream = vi.fn().mockResolvedValue(undefined);
|
|
40
|
+
const handle = createBotWsReplyHandle({
|
|
41
|
+
client: {
|
|
42
|
+
replyStream,
|
|
43
|
+
} as unknown as ReplyHandleParams["client"],
|
|
44
|
+
frame: {
|
|
45
|
+
headers: { req_id: "req-keepalive" },
|
|
46
|
+
body: {},
|
|
47
|
+
} as unknown as ReplyHandleParams["frame"],
|
|
48
|
+
accountId: "default",
|
|
49
|
+
placeholderContent: "正在思考...",
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
await vi.advanceTimersByTimeAsync(3000);
|
|
53
|
+
expect(replyStream).toHaveBeenCalledTimes(2);
|
|
54
|
+
|
|
55
|
+
await handle.deliver({ text: "最终回复" }, { kind: "final" });
|
|
56
|
+
await vi.advanceTimersByTimeAsync(6000);
|
|
57
|
+
|
|
58
|
+
expect(replyStream).toHaveBeenCalledTimes(3);
|
|
59
|
+
expect(replyStream).toHaveBeenLastCalledWith(
|
|
60
|
+
expect.objectContaining({
|
|
61
|
+
headers: { req_id: "req-keepalive" },
|
|
62
|
+
}),
|
|
63
|
+
expect.any(String),
|
|
64
|
+
"最终回复",
|
|
65
|
+
true,
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("does not auto-send placeholder when disabled", () => {
|
|
70
|
+
const replyStream = vi.fn().mockResolvedValue(undefined);
|
|
71
|
+
createBotWsReplyHandle({
|
|
72
|
+
client: {
|
|
73
|
+
replyStream,
|
|
74
|
+
} as unknown as ReplyHandleParams["client"],
|
|
75
|
+
frame: {
|
|
76
|
+
headers: { req_id: "req-2" },
|
|
77
|
+
body: {},
|
|
78
|
+
} as unknown as ReplyHandleParams["frame"],
|
|
79
|
+
accountId: "default",
|
|
80
|
+
autoSendPlaceholder: false,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
expect(replyStream).not.toHaveBeenCalled();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("swallows expired stream update errors during delivery", async () => {
|
|
87
|
+
const expiredError = {
|
|
88
|
+
headers: { req_id: "req-expired" },
|
|
89
|
+
errcode: 846608,
|
|
90
|
+
errmsg: "stream message update expired (>6 minutes), cannot update",
|
|
91
|
+
};
|
|
92
|
+
const replyStream = vi.fn().mockRejectedValue(expiredError);
|
|
93
|
+
const onFail = vi.fn();
|
|
94
|
+
const handle = createBotWsReplyHandle({
|
|
95
|
+
client: {
|
|
96
|
+
replyStream,
|
|
97
|
+
} as unknown as ReplyHandleParams["client"],
|
|
98
|
+
frame: {
|
|
99
|
+
headers: { req_id: "req-expired" },
|
|
100
|
+
body: {},
|
|
101
|
+
} as unknown as ReplyHandleParams["frame"],
|
|
102
|
+
accountId: "default",
|
|
103
|
+
autoSendPlaceholder: false,
|
|
104
|
+
onFail,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
await expect(handle.deliver({ text: "最终回复" }, { kind: "final" })).resolves.toBeUndefined();
|
|
108
|
+
|
|
109
|
+
expect(replyStream).toHaveBeenCalledTimes(1);
|
|
110
|
+
expect(onFail).toHaveBeenCalledWith(expiredError);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it.each([
|
|
114
|
+
[{ headers: { req_id: "req-invalid" }, errcode: 846605, errmsg: "invalid req_id" }],
|
|
115
|
+
[{ headers: { req_id: "req-expired" }, errcode: 846608, errmsg: "stream message update expired (>6 minutes), cannot update" }],
|
|
116
|
+
])("does not retry error reply when the ws reply window is already closed", async (error) => {
|
|
117
|
+
const replyStream = vi.fn().mockResolvedValue(undefined);
|
|
118
|
+
const onFail = vi.fn();
|
|
119
|
+
const handle = createBotWsReplyHandle({
|
|
120
|
+
client: {
|
|
121
|
+
replyStream,
|
|
122
|
+
} as unknown as ReplyHandleParams["client"],
|
|
123
|
+
frame: {
|
|
124
|
+
headers: { req_id: String(error.headers.req_id) },
|
|
125
|
+
body: {},
|
|
126
|
+
} as unknown as ReplyHandleParams["frame"],
|
|
127
|
+
accountId: "default",
|
|
128
|
+
autoSendPlaceholder: false,
|
|
129
|
+
onFail,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
await handle.fail?.(error);
|
|
133
|
+
|
|
134
|
+
expect(replyStream).not.toHaveBeenCalled();
|
|
135
|
+
expect(onFail).toHaveBeenCalledTimes(1);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -1,11 +1,45 @@
|
|
|
1
|
+
import { formatErrorMessage } from "openclaw/plugin-sdk";
|
|
1
2
|
import { generateReqId, type WsFrame, type BaseMessage, type EventMessage, type WSClient } from "@wecom/aibot-node-sdk";
|
|
2
3
|
|
|
3
4
|
import type { ReplyHandle, ReplyPayload } from "../../types/index.js";
|
|
4
5
|
|
|
6
|
+
const PLACEHOLDER_KEEPALIVE_MS = 3000;
|
|
7
|
+
|
|
8
|
+
function isInvalidReqIdError(error: unknown): boolean {
|
|
9
|
+
if (!error || typeof error !== "object") {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
const errcode = "errcode" in error ? Number(error.errcode) : undefined;
|
|
13
|
+
const errmsg = "errmsg" in error ? String(error.errmsg ?? "") : "";
|
|
14
|
+
return errcode === 846605 || errmsg.includes("invalid req_id");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isExpiredStreamUpdateError(error: unknown): boolean {
|
|
18
|
+
if (!error || typeof error !== "object") {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
const errcode = "errcode" in error ? Number(error.errcode) : undefined;
|
|
22
|
+
const errmsg = "errmsg" in error ? String(error.errmsg ?? "").toLowerCase() : "";
|
|
23
|
+
return errcode === 846608 || errmsg.includes("stream message update expired");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** SDK rejects with a plain Error whose message contains "ack timeout" when
|
|
27
|
+
* the WeCom server does not acknowledge a reply within 5 s. Once timed out
|
|
28
|
+
* the reqId slot is released; further replies on the same reqId will fail. */
|
|
29
|
+
function isAckTimeoutError(error: unknown): boolean {
|
|
30
|
+
return error instanceof Error && error.message.includes("ack timeout");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isTerminalReplyError(error: unknown): boolean {
|
|
34
|
+
return isInvalidReqIdError(error) || isExpiredStreamUpdateError(error) || isAckTimeoutError(error);
|
|
35
|
+
}
|
|
36
|
+
|
|
5
37
|
export function createBotWsReplyHandle(params: {
|
|
6
38
|
client: WSClient;
|
|
7
39
|
frame: WsFrame<BaseMessage | EventMessage>;
|
|
8
40
|
accountId: string;
|
|
41
|
+
placeholderContent?: string;
|
|
42
|
+
autoSendPlaceholder?: boolean;
|
|
9
43
|
onDeliver?: () => void;
|
|
10
44
|
onFail?: (error: unknown) => void;
|
|
11
45
|
}): ReplyHandle {
|
|
@@ -15,15 +49,44 @@ export function createBotWsReplyHandle(params: {
|
|
|
15
49
|
return streamId;
|
|
16
50
|
};
|
|
17
51
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
params.client.replyStream(params.frame, resolveStreamId(), "⏳ 正在思考中...\n\n", false)
|
|
23
|
-
.catch(() => { /* ignore */ });
|
|
24
|
-
}, 4000);
|
|
52
|
+
const placeholderText = params.placeholderContent?.trim() || "⏳ 正在思考中...\n\n";
|
|
53
|
+
let streamSettled = false;
|
|
54
|
+
let placeholderInFlight = false;
|
|
55
|
+
let placeholderKeepalive: ReturnType<typeof setInterval> | undefined;
|
|
25
56
|
|
|
26
|
-
const
|
|
57
|
+
const stopPlaceholderKeepalive = () => {
|
|
58
|
+
if (!placeholderKeepalive) return;
|
|
59
|
+
clearInterval(placeholderKeepalive);
|
|
60
|
+
placeholderKeepalive = undefined;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const settleStream = () => {
|
|
64
|
+
streamSettled = true;
|
|
65
|
+
stopPlaceholderKeepalive();
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const sendPlaceholder = () => {
|
|
69
|
+
if (streamSettled || placeholderInFlight) return;
|
|
70
|
+
placeholderInFlight = true;
|
|
71
|
+
params.client.replyStream(params.frame, resolveStreamId(), placeholderText, false)
|
|
72
|
+
.catch((error) => {
|
|
73
|
+
if (!isTerminalReplyError(error)) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
settleStream();
|
|
77
|
+
params.onFail?.(error);
|
|
78
|
+
})
|
|
79
|
+
.finally(() => {
|
|
80
|
+
placeholderInFlight = false;
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
if (params.autoSendPlaceholder !== false) {
|
|
85
|
+
sendPlaceholder();
|
|
86
|
+
placeholderKeepalive = setInterval(() => {
|
|
87
|
+
sendPlaceholder();
|
|
88
|
+
}, PLACEHOLDER_KEEPALIVE_MS);
|
|
89
|
+
}
|
|
27
90
|
|
|
28
91
|
return {
|
|
29
92
|
context: {
|
|
@@ -40,23 +103,35 @@ export function createBotWsReplyHandle(params: {
|
|
|
40
103
|
},
|
|
41
104
|
deliver: async (payload: ReplyPayload, info) => {
|
|
42
105
|
if (payload.isReasoning) return;
|
|
43
|
-
|
|
106
|
+
|
|
44
107
|
const text = payload.text?.trim();
|
|
45
108
|
if (!text) return;
|
|
46
109
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
110
|
+
settleStream();
|
|
111
|
+
try {
|
|
112
|
+
await params.client.replyStream(params.frame, resolveStreamId(), text, info.kind === "final");
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (isTerminalReplyError(error)) {
|
|
115
|
+
params.onFail?.(error);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
throw error;
|
|
50
119
|
}
|
|
51
|
-
|
|
52
|
-
await params.client.replyStream(params.frame, resolveStreamId(), text, info.kind === "final");
|
|
53
120
|
params.onDeliver?.();
|
|
54
121
|
},
|
|
55
122
|
fail: async (error: unknown) => {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
123
|
+
settleStream();
|
|
124
|
+
if (isTerminalReplyError(error)) {
|
|
125
|
+
params.onFail?.(error);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const message = formatErrorMessage(error);
|
|
129
|
+
try {
|
|
130
|
+
await params.client.replyStream(params.frame, resolveStreamId(), `WeCom WS reply failed: ${message}`, true);
|
|
131
|
+
} catch (sendError) {
|
|
132
|
+
params.onFail?.(sendError);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
60
135
|
params.onFail?.(error);
|
|
61
136
|
},
|
|
62
137
|
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const sdkMockState = vi.hoisted(() => {
|
|
4
|
+
class MockWSClient {
|
|
5
|
+
readonly handlers = new Map<string, Array<(payload: any) => void>>();
|
|
6
|
+
readonly isConnected = true;
|
|
7
|
+
readonly replyStream = vi.fn().mockResolvedValue(undefined);
|
|
8
|
+
|
|
9
|
+
constructor(_options: unknown) {
|
|
10
|
+
sdkMockState.client = this;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
on(event: string, handler: (payload: any) => void): void {
|
|
14
|
+
const current = this.handlers.get(event) ?? [];
|
|
15
|
+
current.push(handler);
|
|
16
|
+
this.handlers.set(event, current);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
emit(event: string, payload: any): void {
|
|
20
|
+
for (const handler of this.handlers.get(event) ?? []) {
|
|
21
|
+
handler(payload);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
connect(): void {}
|
|
26
|
+
|
|
27
|
+
disconnect(): void {}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
client: null as InstanceType<typeof MockWSClient> | null,
|
|
32
|
+
MockWSClient,
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
vi.mock("@wecom/aibot-node-sdk", () => ({
|
|
37
|
+
default: {
|
|
38
|
+
WSClient: sdkMockState.MockWSClient,
|
|
39
|
+
},
|
|
40
|
+
WSClient: sdkMockState.MockWSClient,
|
|
41
|
+
generateReqId: (prefix: string) => `${prefix}-1`,
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
import { BotWsSdkAdapter } from "./sdk-adapter.js";
|
|
45
|
+
|
|
46
|
+
const waitForAsyncCallbacks = async () => {
|
|
47
|
+
await Promise.resolve();
|
|
48
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
describe("BotWsSdkAdapter", () => {
|
|
52
|
+
const unhandledRejections: unknown[] = [];
|
|
53
|
+
const onUnhandledRejection = (reason: unknown) => {
|
|
54
|
+
unhandledRejections.push(reason);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
process.off("unhandledRejection", onUnhandledRejection);
|
|
59
|
+
unhandledRejections.length = 0;
|
|
60
|
+
sdkMockState.client = null;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("contains frame handler rejections instead of leaking unhandled rejections", async () => {
|
|
64
|
+
process.on("unhandledRejection", onUnhandledRejection);
|
|
65
|
+
|
|
66
|
+
const runtime = {
|
|
67
|
+
account: {
|
|
68
|
+
accountId: "acc-1",
|
|
69
|
+
bot: {
|
|
70
|
+
wsConfigured: true,
|
|
71
|
+
ws: {
|
|
72
|
+
botId: "bot-1",
|
|
73
|
+
secret: "secret-1",
|
|
74
|
+
},
|
|
75
|
+
config: {},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
handleEvent: vi.fn().mockRejectedValue(new Error("frame exploded")),
|
|
79
|
+
updateTransportSession: vi.fn(),
|
|
80
|
+
touchTransportSession: vi.fn(),
|
|
81
|
+
recordOperationalIssue: vi.fn(),
|
|
82
|
+
};
|
|
83
|
+
const log = {
|
|
84
|
+
info: vi.fn(),
|
|
85
|
+
warn: vi.fn(),
|
|
86
|
+
error: vi.fn(),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
new BotWsSdkAdapter(runtime as any, log as any).start();
|
|
90
|
+
|
|
91
|
+
sdkMockState.client?.emit("message", {
|
|
92
|
+
cmd: "aibot_msg_callback",
|
|
93
|
+
headers: { req_id: "req-1" },
|
|
94
|
+
body: {
|
|
95
|
+
msgid: "msg-1",
|
|
96
|
+
msgtype: "text",
|
|
97
|
+
from: { userid: "user-1" },
|
|
98
|
+
text: { content: "hello" },
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
await waitForAsyncCallbacks();
|
|
103
|
+
|
|
104
|
+
expect(runtime.handleEvent).toHaveBeenCalledTimes(1);
|
|
105
|
+
expect(runtime.recordOperationalIssue).toHaveBeenCalledWith(
|
|
106
|
+
expect.objectContaining({
|
|
107
|
+
transport: "bot-ws",
|
|
108
|
+
category: "runtime-error",
|
|
109
|
+
messageId: "msg-1",
|
|
110
|
+
error: "frame exploded",
|
|
111
|
+
}),
|
|
112
|
+
);
|
|
113
|
+
expect(runtime.touchTransportSession).toHaveBeenCalledWith(
|
|
114
|
+
"bot-ws",
|
|
115
|
+
expect.objectContaining({
|
|
116
|
+
lastError: "frame exploded",
|
|
117
|
+
}),
|
|
118
|
+
);
|
|
119
|
+
expect(log.error).toHaveBeenCalledWith(
|
|
120
|
+
expect.stringContaining("frame handler failed account=acc-1 reqId=req-1 message=frame exploded"),
|
|
121
|
+
);
|
|
122
|
+
expect(unhandledRejections).toHaveLength(0);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -3,6 +3,7 @@ import crypto from "node:crypto";
|
|
|
3
3
|
import AiBot, { type BaseMessage, type EventMessage, type WsFrame } from "@wecom/aibot-node-sdk";
|
|
4
4
|
|
|
5
5
|
import type { RuntimeLogSink } from "../../types/index.js";
|
|
6
|
+
import { registerBotWsPushHandle, unregisterBotWsPushHandle } from "../../app/index.js";
|
|
6
7
|
import { mapBotWsFrameToInboundEvent } from "./inbound.js";
|
|
7
8
|
import { createBotWsReplyHandle } from "./reply.js";
|
|
8
9
|
import { createBotWsSessionSnapshot } from "./session.js";
|
|
@@ -38,6 +39,23 @@ export class BotWsSdkAdapter {
|
|
|
38
39
|
},
|
|
39
40
|
});
|
|
40
41
|
this.client = client;
|
|
42
|
+
registerBotWsPushHandle(this.runtime.account.accountId, {
|
|
43
|
+
isConnected: () => client.isConnected,
|
|
44
|
+
sendMarkdown: async (chatId, content) => {
|
|
45
|
+
await client.sendMessage(chatId, {
|
|
46
|
+
msgtype: "markdown",
|
|
47
|
+
markdown: { content },
|
|
48
|
+
});
|
|
49
|
+
this.runtime.touchTransportSession("bot-ws", {
|
|
50
|
+
ownerId: this.ownerId,
|
|
51
|
+
running: true,
|
|
52
|
+
connected: client.isConnected,
|
|
53
|
+
authenticated: client.isConnected,
|
|
54
|
+
lastOutboundAt: Date.now(),
|
|
55
|
+
lastError: undefined,
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
});
|
|
41
59
|
|
|
42
60
|
client.on("connected", () => {
|
|
43
61
|
this.log.info?.(`[wecom-ws] connected account=${this.runtime.account.accountId}`);
|
|
@@ -131,6 +149,13 @@ export class BotWsSdkAdapter {
|
|
|
131
149
|
client,
|
|
132
150
|
frame,
|
|
133
151
|
accountId: this.runtime.account.accountId,
|
|
152
|
+
placeholderContent: botAccount.config.streamPlaceholderContent,
|
|
153
|
+
autoSendPlaceholder:
|
|
154
|
+
event.inboundKind === "text" ||
|
|
155
|
+
event.inboundKind === "image" ||
|
|
156
|
+
event.inboundKind === "file" ||
|
|
157
|
+
event.inboundKind === "voice" ||
|
|
158
|
+
event.inboundKind === "mixed",
|
|
134
159
|
onDeliver: () => {
|
|
135
160
|
this.runtime.touchTransportSession("bot-ws", {
|
|
136
161
|
ownerId: this.ownerId,
|
|
@@ -153,11 +178,41 @@ export class BotWsSdkAdapter {
|
|
|
153
178
|
await this.runtime.handleEvent(event, replyHandle);
|
|
154
179
|
};
|
|
155
180
|
|
|
181
|
+
const runHandleFrame = (frame: WsFrame<BaseMessage | EventMessage>) => {
|
|
182
|
+
void handleFrame(frame).catch((error) => {
|
|
183
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
184
|
+
this.log.error?.(
|
|
185
|
+
`[wecom-ws] frame handler failed account=${this.runtime.account.accountId} reqId=${frame.headers?.req_id ?? "n/a"} message=${message}`,
|
|
186
|
+
);
|
|
187
|
+
this.runtime.recordOperationalIssue({
|
|
188
|
+
transport: "bot-ws",
|
|
189
|
+
category: "runtime-error",
|
|
190
|
+
messageId: frame.body?.msgid,
|
|
191
|
+
raw: {
|
|
192
|
+
transport: "bot-ws",
|
|
193
|
+
command: frame.cmd,
|
|
194
|
+
headers: frame.headers,
|
|
195
|
+
body: frame.body,
|
|
196
|
+
envelopeType: "ws",
|
|
197
|
+
},
|
|
198
|
+
summary: `bot-ws frame handler crashed reqId=${frame.headers?.req_id ?? "n/a"}`,
|
|
199
|
+
error: message,
|
|
200
|
+
});
|
|
201
|
+
this.runtime.touchTransportSession("bot-ws", {
|
|
202
|
+
ownerId: this.ownerId,
|
|
203
|
+
running: client.isConnected,
|
|
204
|
+
connected: client.isConnected,
|
|
205
|
+
authenticated: client.isConnected,
|
|
206
|
+
lastError: message,
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
};
|
|
210
|
+
|
|
156
211
|
client.on("message", (frame) => {
|
|
157
|
-
|
|
212
|
+
runHandleFrame(frame);
|
|
158
213
|
});
|
|
159
214
|
client.on("event", (frame) => {
|
|
160
|
-
|
|
215
|
+
runHandleFrame(frame);
|
|
161
216
|
});
|
|
162
217
|
|
|
163
218
|
client.connect();
|
|
@@ -165,6 +220,7 @@ export class BotWsSdkAdapter {
|
|
|
165
220
|
|
|
166
221
|
stop(): void {
|
|
167
222
|
this.log.info?.(`[wecom-ws] stop account=${this.runtime.account.accountId}`);
|
|
223
|
+
unregisterBotWsPushHandle(this.runtime.account.accountId);
|
|
168
224
|
this.runtime.updateTransportSession(
|
|
169
225
|
createBotWsSessionSnapshot({
|
|
170
226
|
accountId: this.runtime.account.accountId,
|