@yanhaidao/wecom 2.3.13 → 2.3.14
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 +148 -0
- package/changelog/v2.3.14.md +48 -0
- package/index.ts +4 -0
- package/package.json +3 -2
- package/src/agent/handler.event-filter.test.ts +15 -5
- package/src/agent/handler.ts +192 -48
- package/src/app/account-runtime.ts +1 -1
- package/src/app/index.ts +4 -0
- package/src/capability/agent/delivery-service.ts +16 -8
- package/src/capability/bot/fallback-delivery.ts +1 -1
- package/src/capability/bot/stream-orchestrator.ts +10 -10
- package/src/capability/doc/client.ts +910 -0
- package/src/capability/doc/schema.ts +1404 -0
- package/src/capability/doc/tool.ts +1165 -0
- package/src/capability/doc/types.ts +408 -0
- package/src/channel.ts +1 -1
- package/src/outbound.ts +5 -5
- package/src/runtime/session-manager.ts +4 -4
- package/src/target.ts +3 -0
- package/src/transport/bot-webhook/active-reply.ts +4 -1
- package/src/transport/bot-ws/inbound.ts +2 -2
- package/src/transport/bot-ws/reply.test.ts +124 -56
- package/src/transport/bot-ws/reply.ts +121 -13
- package/src/transport/bot-ws/sdk-adapter.ts +1 -0
- package/src/types/runtime.ts +3 -0
|
@@ -1,31 +1,52 @@
|
|
|
1
|
-
import { describe, expect, it, vi, afterEach } from "vitest";
|
|
1
|
+
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
|
|
3
|
+
import type { WSClient } from "@wecom/aibot-node-sdk";
|
|
2
4
|
|
|
3
5
|
import { createBotWsReplyHandle } from "./reply.js";
|
|
4
6
|
|
|
5
7
|
type ReplyHandleParams = Parameters<typeof createBotWsReplyHandle>[0];
|
|
6
8
|
|
|
7
9
|
describe("createBotWsReplyHandle", () => {
|
|
10
|
+
let mockClient: import("vitest").Mocked<WSClient>;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
vi.useFakeTimers();
|
|
14
|
+
mockClient = {
|
|
15
|
+
replyStream: vi.fn(),
|
|
16
|
+
sendMessage: vi.fn(),
|
|
17
|
+
replyWelcome: vi.fn(),
|
|
18
|
+
} as unknown as import("vitest").Mocked<WSClient>;
|
|
19
|
+
mockClient.replyStream.mockResolvedValue({} as any);
|
|
20
|
+
mockClient.sendMessage.mockResolvedValue({} as any);
|
|
21
|
+
mockClient.replyWelcome.mockResolvedValue({} as any);
|
|
22
|
+
});
|
|
23
|
+
|
|
8
24
|
afterEach(() => {
|
|
25
|
+
vi.clearAllTimers();
|
|
9
26
|
vi.useRealTimers();
|
|
27
|
+
vi.restoreAllMocks();
|
|
10
28
|
});
|
|
11
29
|
|
|
12
30
|
it("uses configured placeholder content for immediate ws ack", async () => {
|
|
13
|
-
const replyStream = vi.fn().mockResolvedValue(undefined);
|
|
14
31
|
createBotWsReplyHandle({
|
|
15
|
-
client:
|
|
16
|
-
replyStream,
|
|
17
|
-
} as unknown as ReplyHandleParams["client"],
|
|
32
|
+
client: mockClient,
|
|
18
33
|
frame: {
|
|
19
34
|
headers: { req_id: "req-1" },
|
|
20
|
-
body: {},
|
|
35
|
+
body: { chatid: "123", chattype: "group" },
|
|
36
|
+
cmd: "aibot_msg_callback",
|
|
21
37
|
} as unknown as ReplyHandleParams["frame"],
|
|
22
38
|
accountId: "default",
|
|
39
|
+
inboundKind: "text",
|
|
23
40
|
placeholderContent: "正在思考...",
|
|
24
41
|
});
|
|
25
42
|
|
|
26
|
-
|
|
43
|
+
vi.advanceTimersByTime(3000);
|
|
44
|
+
// Let promises flush
|
|
45
|
+
await Promise.resolve();
|
|
46
|
+
|
|
47
|
+
expect(mockClient.replyStream).toHaveBeenCalledWith(
|
|
27
48
|
expect.objectContaining({
|
|
28
|
-
headers: { req_id: "req-1" }
|
|
49
|
+
headers: { req_id: "req-1" }
|
|
29
50
|
}),
|
|
30
51
|
expect.any(String),
|
|
31
52
|
"正在思考...",
|
|
@@ -34,29 +55,30 @@ describe("createBotWsReplyHandle", () => {
|
|
|
34
55
|
});
|
|
35
56
|
|
|
36
57
|
it("keeps placeholder alive until the first real ws chunk arrives", async () => {
|
|
37
|
-
vi.useFakeTimers();
|
|
38
|
-
|
|
39
|
-
const replyStream = vi.fn().mockResolvedValue(undefined);
|
|
40
58
|
const handle = createBotWsReplyHandle({
|
|
41
|
-
client:
|
|
42
|
-
replyStream,
|
|
43
|
-
} as unknown as ReplyHandleParams["client"],
|
|
59
|
+
client: mockClient,
|
|
44
60
|
frame: {
|
|
45
61
|
headers: { req_id: "req-keepalive" },
|
|
46
62
|
body: {},
|
|
47
63
|
} as unknown as ReplyHandleParams["frame"],
|
|
48
64
|
accountId: "default",
|
|
65
|
+
inboundKind: "text",
|
|
49
66
|
placeholderContent: "正在思考...",
|
|
50
67
|
});
|
|
51
68
|
|
|
52
|
-
|
|
53
|
-
|
|
69
|
+
vi.advanceTimersByTime(3000);
|
|
70
|
+
// Flush the microtasks so `placeholderInFlight` becomes false
|
|
71
|
+
for (let i = 0; i < 10; i++) await Promise.resolve();
|
|
72
|
+
|
|
73
|
+
// Now trigger the next timer
|
|
74
|
+
vi.advanceTimersByTime(3000);
|
|
75
|
+
for (let i = 0; i < 10; i++) await Promise.resolve();
|
|
76
|
+
expect(mockClient.replyStream).toHaveBeenCalledTimes(2);
|
|
54
77
|
|
|
55
|
-
|
|
56
|
-
await
|
|
78
|
+
handle.deliver({ text: "最终回复", isReasoning: false }, { kind: "final" });
|
|
79
|
+
await Promise.resolve();
|
|
57
80
|
|
|
58
|
-
expect(replyStream).
|
|
59
|
-
expect(replyStream).toHaveBeenLastCalledWith(
|
|
81
|
+
expect(mockClient.replyStream).toHaveBeenCalledWith(
|
|
60
82
|
expect.objectContaining({
|
|
61
83
|
headers: { req_id: "req-keepalive" },
|
|
62
84
|
}),
|
|
@@ -64,66 +86,63 @@ describe("createBotWsReplyHandle", () => {
|
|
|
64
86
|
"最终回复",
|
|
65
87
|
true,
|
|
66
88
|
);
|
|
89
|
+
|
|
90
|
+
// Ensure interval is cleared
|
|
91
|
+
vi.advanceTimersByTime(6000);
|
|
92
|
+
await Promise.resolve();
|
|
93
|
+
expect(mockClient.replyStream).toHaveBeenCalledTimes(3);
|
|
67
94
|
});
|
|
68
95
|
|
|
69
|
-
it("does not auto-send placeholder when disabled", () => {
|
|
70
|
-
const replyStream = vi.fn().mockResolvedValue(undefined);
|
|
96
|
+
it("does not auto-send placeholder when disabled", async () => {
|
|
71
97
|
createBotWsReplyHandle({
|
|
72
|
-
client:
|
|
73
|
-
replyStream,
|
|
74
|
-
} as unknown as ReplyHandleParams["client"],
|
|
98
|
+
client: mockClient,
|
|
75
99
|
frame: {
|
|
76
100
|
headers: { req_id: "req-2" },
|
|
77
101
|
body: {},
|
|
78
102
|
} as unknown as ReplyHandleParams["frame"],
|
|
79
103
|
accountId: "default",
|
|
104
|
+
inboundKind: "text",
|
|
80
105
|
autoSendPlaceholder: false,
|
|
81
106
|
});
|
|
82
107
|
|
|
83
|
-
|
|
108
|
+
vi.advanceTimersByTime(3000);
|
|
109
|
+
await Promise.resolve();
|
|
110
|
+
expect(mockClient.replyStream).not.toHaveBeenCalled();
|
|
84
111
|
});
|
|
85
112
|
|
|
86
113
|
it("sends cumulative content for block streaming updates", async () => {
|
|
87
|
-
const replyStream = vi.fn().mockResolvedValue(undefined);
|
|
88
114
|
const handle = createBotWsReplyHandle({
|
|
89
|
-
client:
|
|
90
|
-
replyStream,
|
|
91
|
-
} as unknown as ReplyHandleParams["client"],
|
|
115
|
+
client: mockClient,
|
|
92
116
|
frame: {
|
|
93
117
|
headers: { req_id: "req-blocks" },
|
|
94
118
|
body: {},
|
|
95
119
|
} as unknown as ReplyHandleParams["frame"],
|
|
96
120
|
accountId: "default",
|
|
121
|
+
inboundKind: "text",
|
|
97
122
|
autoSendPlaceholder: false,
|
|
98
123
|
});
|
|
99
124
|
|
|
100
|
-
await handle.deliver({ text: "第一段" }, { kind: "block" });
|
|
101
|
-
await handle.deliver({ text: "第二段" }, { kind: "block" });
|
|
102
|
-
await handle.deliver({ text: "收尾" }, { kind: "final" });
|
|
125
|
+
await handle.deliver({ text: "第一段", isReasoning: false }, { kind: "block" });
|
|
126
|
+
await handle.deliver({ text: "第二段", isReasoning: false }, { kind: "block" });
|
|
127
|
+
await handle.deliver({ text: "收尾", isReasoning: false }, { kind: "final" });
|
|
103
128
|
|
|
104
|
-
expect(replyStream).toHaveBeenNthCalledWith(
|
|
129
|
+
expect(mockClient.replyStream).toHaveBeenNthCalledWith(
|
|
105
130
|
1,
|
|
106
|
-
expect.objectContaining({
|
|
107
|
-
headers: { req_id: "req-blocks" },
|
|
108
|
-
}),
|
|
131
|
+
expect.objectContaining({ headers: { req_id: "req-blocks" } }),
|
|
109
132
|
expect.any(String),
|
|
110
133
|
"第一段",
|
|
111
134
|
false,
|
|
112
135
|
);
|
|
113
|
-
expect(replyStream).toHaveBeenNthCalledWith(
|
|
136
|
+
expect(mockClient.replyStream).toHaveBeenNthCalledWith(
|
|
114
137
|
2,
|
|
115
|
-
expect.objectContaining({
|
|
116
|
-
headers: { req_id: "req-blocks" },
|
|
117
|
-
}),
|
|
138
|
+
expect.objectContaining({ headers: { req_id: "req-blocks" } }),
|
|
118
139
|
expect.any(String),
|
|
119
140
|
"第一段\n第二段",
|
|
120
141
|
false,
|
|
121
142
|
);
|
|
122
|
-
expect(replyStream).toHaveBeenNthCalledWith(
|
|
143
|
+
expect(mockClient.replyStream).toHaveBeenNthCalledWith(
|
|
123
144
|
3,
|
|
124
|
-
expect.objectContaining({
|
|
125
|
-
headers: { req_id: "req-blocks" },
|
|
126
|
-
}),
|
|
145
|
+
expect.objectContaining({ headers: { req_id: "req-blocks" } }),
|
|
127
146
|
expect.any(String),
|
|
128
147
|
"第一段\n第二段\n收尾",
|
|
129
148
|
true,
|
|
@@ -136,24 +155,24 @@ describe("createBotWsReplyHandle", () => {
|
|
|
136
155
|
errcode: 846608,
|
|
137
156
|
errmsg: "stream message update expired (>6 minutes), cannot update",
|
|
138
157
|
};
|
|
139
|
-
|
|
158
|
+
mockClient.replyStream.mockRejectedValueOnce(expiredError);
|
|
140
159
|
const onFail = vi.fn();
|
|
160
|
+
|
|
141
161
|
const handle = createBotWsReplyHandle({
|
|
142
|
-
client:
|
|
143
|
-
replyStream,
|
|
144
|
-
} as unknown as ReplyHandleParams["client"],
|
|
162
|
+
client: mockClient,
|
|
145
163
|
frame: {
|
|
146
164
|
headers: { req_id: "req-expired" },
|
|
147
165
|
body: {},
|
|
148
166
|
} as unknown as ReplyHandleParams["frame"],
|
|
149
167
|
accountId: "default",
|
|
168
|
+
inboundKind: "text",
|
|
150
169
|
autoSendPlaceholder: false,
|
|
151
170
|
onFail,
|
|
152
171
|
});
|
|
153
172
|
|
|
154
|
-
await
|
|
173
|
+
await handle.deliver({ text: "最终回复", isReasoning: false }, { kind: "final" });
|
|
155
174
|
|
|
156
|
-
expect(replyStream).toHaveBeenCalledTimes(1);
|
|
175
|
+
expect(mockClient.replyStream).toHaveBeenCalledTimes(1);
|
|
157
176
|
expect(onFail).toHaveBeenCalledWith(expiredError);
|
|
158
177
|
});
|
|
159
178
|
|
|
@@ -161,24 +180,73 @@ describe("createBotWsReplyHandle", () => {
|
|
|
161
180
|
[{ headers: { req_id: "req-invalid" }, errcode: 846605, errmsg: "invalid req_id" }],
|
|
162
181
|
[{ headers: { req_id: "req-expired" }, errcode: 846608, errmsg: "stream message update expired (>6 minutes), cannot update" }],
|
|
163
182
|
])("does not retry error reply when the ws reply window is already closed", async (error) => {
|
|
164
|
-
const replyStream = vi.fn().mockResolvedValue(undefined);
|
|
165
183
|
const onFail = vi.fn();
|
|
166
184
|
const handle = createBotWsReplyHandle({
|
|
167
|
-
client:
|
|
168
|
-
replyStream,
|
|
169
|
-
} as unknown as ReplyHandleParams["client"],
|
|
185
|
+
client: mockClient,
|
|
170
186
|
frame: {
|
|
171
187
|
headers: { req_id: String(error.headers.req_id) },
|
|
172
188
|
body: {},
|
|
173
189
|
} as unknown as ReplyHandleParams["frame"],
|
|
174
190
|
accountId: "default",
|
|
191
|
+
inboundKind: "text",
|
|
175
192
|
autoSendPlaceholder: false,
|
|
176
193
|
onFail,
|
|
177
194
|
});
|
|
178
195
|
|
|
179
196
|
await handle.fail?.(error);
|
|
180
197
|
|
|
181
|
-
expect(replyStream).not.toHaveBeenCalled();
|
|
198
|
+
expect(mockClient.replyStream).not.toHaveBeenCalled();
|
|
182
199
|
expect(onFail).toHaveBeenCalledTimes(1);
|
|
183
200
|
});
|
|
201
|
+
|
|
202
|
+
it("sends simple fallback message for ordinary events without placeholders", async () => {
|
|
203
|
+
const handle = createBotWsReplyHandle({
|
|
204
|
+
client: mockClient,
|
|
205
|
+
frame: {
|
|
206
|
+
headers: { req_id: "event_req" },
|
|
207
|
+
body: { chattype: "single", from: { userid: "alice" } },
|
|
208
|
+
} as unknown as ReplyHandleParams["frame"],
|
|
209
|
+
accountId: "default",
|
|
210
|
+
inboundKind: "event",
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
vi.advanceTimersByTime(3000);
|
|
214
|
+
await Promise.resolve();
|
|
215
|
+
// Events should not send stream placeholders
|
|
216
|
+
expect(mockClient.replyStream).not.toHaveBeenCalled();
|
|
217
|
+
|
|
218
|
+
handle.deliver({ text: "Event Reply", isReasoning: false }, { kind: "final" });
|
|
219
|
+
await Promise.resolve();
|
|
220
|
+
|
|
221
|
+
expect(mockClient.sendMessage).toHaveBeenCalledWith(
|
|
222
|
+
"alice",
|
|
223
|
+
{
|
|
224
|
+
msgtype: "markdown",
|
|
225
|
+
markdown: { content: "Event Reply" },
|
|
226
|
+
}
|
|
227
|
+
);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("sends replyWelcome for welcome events", async () => {
|
|
231
|
+
const handle = createBotWsReplyHandle({
|
|
232
|
+
client: mockClient,
|
|
233
|
+
frame: {
|
|
234
|
+
headers: { req_id: "welcome_req" },
|
|
235
|
+
body: { chattype: "single", from: { userid: "bob" } },
|
|
236
|
+
} as unknown as ReplyHandleParams["frame"],
|
|
237
|
+
accountId: "default",
|
|
238
|
+
inboundKind: "welcome",
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
handle.deliver({ text: "Hello Bob", isReasoning: false }, { kind: "final" });
|
|
242
|
+
await Promise.resolve();
|
|
243
|
+
|
|
244
|
+
expect(mockClient.replyWelcome).toHaveBeenCalledWith(
|
|
245
|
+
expect.objectContaining({ headers: { req_id: "welcome_req" } }),
|
|
246
|
+
{
|
|
247
|
+
msgtype: "text",
|
|
248
|
+
text: { content: "Hello Bob" },
|
|
249
|
+
}
|
|
250
|
+
);
|
|
251
|
+
});
|
|
184
252
|
});
|
|
@@ -4,6 +4,7 @@ import { generateReqId, type WsFrame, type BaseMessage, type EventMessage, type
|
|
|
4
4
|
import type { ReplyHandle, ReplyPayload } from "../../types/index.js";
|
|
5
5
|
|
|
6
6
|
const PLACEHOLDER_KEEPALIVE_MS = 3000;
|
|
7
|
+
const MAX_KEEPALIVE_MS = 120 * 1000; // Force stop keepalive after 120s if ignored
|
|
7
8
|
|
|
8
9
|
function isInvalidReqIdError(error: unknown): boolean {
|
|
9
10
|
if (!error || typeof error !== "object") {
|
|
@@ -34,10 +35,18 @@ function isTerminalReplyError(error: unknown): boolean {
|
|
|
34
35
|
return isInvalidReqIdError(error) || isExpiredStreamUpdateError(error) || isAckTimeoutError(error);
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
// Global registry to track active keepalives by peerId
|
|
39
|
+
interface ActiveKeepalive {
|
|
40
|
+
reqId: string;
|
|
41
|
+
stop: () => void;
|
|
42
|
+
}
|
|
43
|
+
const activeKeepalivesByPeer = new Map<string, Set<ActiveKeepalive>>();
|
|
44
|
+
|
|
37
45
|
export function createBotWsReplyHandle(params: {
|
|
38
46
|
client: WSClient;
|
|
39
47
|
frame: WsFrame<BaseMessage | EventMessage>;
|
|
40
48
|
accountId: string;
|
|
49
|
+
inboundKind: string;
|
|
41
50
|
placeholderContent?: string;
|
|
42
51
|
autoSendPlaceholder?: boolean;
|
|
43
52
|
onDeliver?: () => void;
|
|
@@ -54,20 +63,49 @@ export function createBotWsReplyHandle(params: {
|
|
|
54
63
|
let streamSettled = false;
|
|
55
64
|
let placeholderInFlight = false;
|
|
56
65
|
let placeholderKeepalive: ReturnType<typeof setInterval> | undefined;
|
|
66
|
+
let placeholderTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
67
|
+
|
|
68
|
+
// Extract peerId for clustering handles
|
|
69
|
+
const body = params.frame.body as any;
|
|
70
|
+
const peerId = String(
|
|
71
|
+
(body?.chattype === "group" ? body?.chatid || body?.from?.userid : body?.from?.userid) || "unknown"
|
|
72
|
+
);
|
|
73
|
+
const reqId = params.frame.headers.req_id || "unknown";
|
|
74
|
+
|
|
75
|
+
const isEvent = params.inboundKind === "welcome" || params.inboundKind === "event" || params.inboundKind === "template-card-event";
|
|
57
76
|
|
|
58
77
|
const stopPlaceholderKeepalive = () => {
|
|
59
|
-
if (
|
|
60
|
-
|
|
61
|
-
|
|
78
|
+
if (placeholderKeepalive) {
|
|
79
|
+
clearInterval(placeholderKeepalive);
|
|
80
|
+
placeholderKeepalive = undefined;
|
|
81
|
+
}
|
|
82
|
+
if (placeholderTimeout) {
|
|
83
|
+
clearTimeout(placeholderTimeout);
|
|
84
|
+
placeholderTimeout = undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Remove from registry
|
|
88
|
+
const keepalives = activeKeepalivesByPeer.get(peerId);
|
|
89
|
+
if (keepalives) {
|
|
90
|
+
for (const ka of keepalives) {
|
|
91
|
+
if (ka.reqId === reqId) {
|
|
92
|
+
keepalives.delete(ka);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (keepalives.size === 0) {
|
|
96
|
+
activeKeepalivesByPeer.delete(peerId);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
62
99
|
};
|
|
63
100
|
|
|
64
101
|
const settleStream = () => {
|
|
102
|
+
if (streamSettled) return;
|
|
65
103
|
streamSettled = true;
|
|
66
104
|
stopPlaceholderKeepalive();
|
|
67
105
|
};
|
|
68
106
|
|
|
69
107
|
const sendPlaceholder = () => {
|
|
70
|
-
if (streamSettled || placeholderInFlight) return;
|
|
108
|
+
if (streamSettled || placeholderInFlight || isEvent) return;
|
|
71
109
|
placeholderInFlight = true;
|
|
72
110
|
params.client.replyStream(params.frame, resolveStreamId(), placeholderText, false)
|
|
73
111
|
.catch((error) => {
|
|
@@ -82,11 +120,39 @@ export function createBotWsReplyHandle(params: {
|
|
|
82
120
|
});
|
|
83
121
|
};
|
|
84
122
|
|
|
85
|
-
|
|
123
|
+
const notifyPeerActive = () => {
|
|
124
|
+
// A genuine reply or reasoning is happening on THIS handle.
|
|
125
|
+
// It means the core SDK has chosen this handle to deliver the response.
|
|
126
|
+
// We can safely terminate all other orphaned keepalives for this peer to prevent infinite loops.
|
|
127
|
+
const keepalives = activeKeepalivesByPeer.get(peerId);
|
|
128
|
+
if (keepalives) {
|
|
129
|
+
for (const ka of keepalives) {
|
|
130
|
+
if (ka.reqId !== reqId) {
|
|
131
|
+
ka.stop();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
if (params.autoSendPlaceholder !== false && !isEvent) {
|
|
86
138
|
sendPlaceholder();
|
|
87
139
|
placeholderKeepalive = setInterval(() => {
|
|
88
140
|
sendPlaceholder();
|
|
89
141
|
}, PLACEHOLDER_KEEPALIVE_MS);
|
|
142
|
+
|
|
143
|
+
// Safety net: force stop keepalive after MAX_KEEPALIVE_MS
|
|
144
|
+
// in case the message is completely ignored by the core and never triggers deliver/fail
|
|
145
|
+
placeholderTimeout = setTimeout(() => {
|
|
146
|
+
stopPlaceholderKeepalive();
|
|
147
|
+
}, MAX_KEEPALIVE_MS);
|
|
148
|
+
|
|
149
|
+
// Register keepalive
|
|
150
|
+
let keepalives = activeKeepalivesByPeer.get(peerId);
|
|
151
|
+
if (!keepalives) {
|
|
152
|
+
keepalives = new Set();
|
|
153
|
+
activeKeepalivesByPeer.set(peerId, keepalives);
|
|
154
|
+
}
|
|
155
|
+
keepalives.add({ reqId, stop: stopPlaceholderKeepalive });
|
|
90
156
|
}
|
|
91
157
|
|
|
92
158
|
return {
|
|
@@ -103,7 +169,19 @@ export function createBotWsReplyHandle(params: {
|
|
|
103
169
|
},
|
|
104
170
|
},
|
|
105
171
|
deliver: async (payload: ReplyPayload, info) => {
|
|
106
|
-
|
|
172
|
+
// Mark this chat as active on this handle
|
|
173
|
+
notifyPeerActive();
|
|
174
|
+
|
|
175
|
+
if (payload.isReasoning) {
|
|
176
|
+
// We reset the safety timeout if reasoning is actively streaming
|
|
177
|
+
if (placeholderTimeout && !isEvent) {
|
|
178
|
+
clearTimeout(placeholderTimeout);
|
|
179
|
+
placeholderTimeout = setTimeout(() => {
|
|
180
|
+
stopPlaceholderKeepalive();
|
|
181
|
+
}, MAX_KEEPALIVE_MS);
|
|
182
|
+
}
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
107
185
|
|
|
108
186
|
const text = payload.text?.trim();
|
|
109
187
|
if (!text) return;
|
|
@@ -121,14 +199,32 @@ export function createBotWsReplyHandle(params: {
|
|
|
121
199
|
: text
|
|
122
200
|
: accumulatedText || text;
|
|
123
201
|
|
|
202
|
+
// Event frames do not support streaming chunks
|
|
203
|
+
if (isEvent && info.kind !== "final") {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
124
207
|
settleStream();
|
|
125
208
|
try {
|
|
126
|
-
|
|
127
|
-
params.frame,
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
)
|
|
209
|
+
if (params.inboundKind === "welcome") {
|
|
210
|
+
await params.client.replyWelcome(params.frame, {
|
|
211
|
+
msgtype: "text",
|
|
212
|
+
text: { content: outboundText },
|
|
213
|
+
});
|
|
214
|
+
} else if (isEvent) {
|
|
215
|
+
// Send push message for other events
|
|
216
|
+
await params.client.sendMessage(peerId, {
|
|
217
|
+
msgtype: "markdown",
|
|
218
|
+
markdown: { content: outboundText },
|
|
219
|
+
});
|
|
220
|
+
} else {
|
|
221
|
+
await params.client.replyStream(
|
|
222
|
+
params.frame,
|
|
223
|
+
resolveStreamId(),
|
|
224
|
+
outboundText,
|
|
225
|
+
info.kind === "final",
|
|
226
|
+
);
|
|
227
|
+
}
|
|
132
228
|
} catch (error) {
|
|
133
229
|
if (isTerminalReplyError(error)) {
|
|
134
230
|
params.onFail?.(error);
|
|
@@ -139,14 +235,26 @@ export function createBotWsReplyHandle(params: {
|
|
|
139
235
|
params.onDeliver?.();
|
|
140
236
|
},
|
|
141
237
|
fail: async (error: unknown) => {
|
|
238
|
+
notifyPeerActive();
|
|
142
239
|
settleStream();
|
|
143
240
|
if (isTerminalReplyError(error)) {
|
|
144
241
|
params.onFail?.(error);
|
|
145
242
|
return;
|
|
146
243
|
}
|
|
147
244
|
const message = formatErrorMessage(error);
|
|
245
|
+
const text = `WeCom WS reply failed: ${message}`;
|
|
246
|
+
|
|
148
247
|
try {
|
|
149
|
-
|
|
248
|
+
if (params.inboundKind === "welcome") {
|
|
249
|
+
await params.client.replyWelcome(params.frame, { msgtype: "text", text: { content: text }});
|
|
250
|
+
} else if (isEvent) {
|
|
251
|
+
await params.client.sendMessage(peerId, {
|
|
252
|
+
msgtype: "markdown",
|
|
253
|
+
markdown: { content: text },
|
|
254
|
+
});
|
|
255
|
+
} else {
|
|
256
|
+
await params.client.replyStream(params.frame, resolveStreamId(), text, true);
|
|
257
|
+
}
|
|
150
258
|
} catch (sendError) {
|
|
151
259
|
params.onFail?.(sendError);
|
|
152
260
|
return;
|
|
@@ -149,6 +149,7 @@ export class BotWsSdkAdapter {
|
|
|
149
149
|
client,
|
|
150
150
|
frame,
|
|
151
151
|
accountId: this.runtime.account.accountId,
|
|
152
|
+
inboundKind: event.inboundKind,
|
|
152
153
|
placeholderContent: botAccount.config.streamPlaceholderContent,
|
|
153
154
|
autoSendPlaceholder:
|
|
154
155
|
event.inboundKind === "text" ||
|