@xmanrui/dsh-im 0.10.0 → 0.12.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.en.md +36 -6
- package/README.md +36 -6
- package/lib/client.js +377 -27
- package/lib/index.js +145 -135
- package/package.json +31 -2
- package/plugin-src/client/channels/dingtalk/api.js +19 -0
- package/plugin-src/client/channels/dingtalk/index.js +67 -10
- package/plugin-src/client/channels/feishu/index.js +35 -5
- package/plugin-src/client/channels/qq/api.js +21 -0
- package/plugin-src/client/channels/qq/index.js +39 -1
- package/plugin-src/client/channels/shared/token-channel.js +29 -3
- package/plugin-src/client/channels/wecom/api.js +11 -0
- package/plugin-src/client/channels/wecom/index.js +71 -1
- package/plugin-src/client/channels/weixin/api.js +11 -0
- package/plugin-src/client/channels/weixin/index.js +40 -4
- package/plugin-src/client/channels/whatsapp/index.js +41 -2
- package/plugin-src/client/i18n.js +13 -0
- package/plugin-src/host/channels/dingtalk/production.mjs +8 -1
- package/plugin-src/host/channels/dingtalk/rpc.mjs +29 -3
- package/plugin-src/host/channels/feishu/production.mjs +8 -1
- package/plugin-src/host/channels/feishu/rpc.mjs +34 -3
- package/plugin-src/host/channels/qq/production.mjs +8 -1
- package/plugin-src/host/channels/qq/rpc.mjs +35 -2
- package/plugin-src/host/channels/shared/production.mjs +8 -1
- package/plugin-src/host/channels/shared/rpc.mjs +25 -1
- package/plugin-src/host/channels/slack/production.mjs +8 -1
- package/plugin-src/host/channels/slack/rpc.mjs +28 -2
- package/plugin-src/host/channels/wecom/production.mjs +8 -1
- package/plugin-src/host/channels/wecom/rpc.mjs +31 -2
- package/plugin-src/host/channels/weixin/production.mjs +8 -1
- package/plugin-src/host/channels/weixin/rpc.mjs +29 -3
- package/plugin-src/host/channels/whatsapp/production.mjs +8 -1
- package/plugin-src/host/channels/whatsapp/rpc.mjs +31 -3
- package/plugin-src/host/harness-session-coordinator.mjs +134 -0
- package/src/channels/dingtalk/dingtalk-bridge.mjs +85 -0
- package/src/channels/dingtalk/dingtalk-controller.mjs +18 -0
- package/src/channels/dingtalk/dingtalk-runtime.mjs +21 -0
- package/src/channels/discord/discord-api.mjs +1 -1
- package/src/channels/discord/discord-runtime.mjs +10 -0
- package/src/channels/feishu/bridge.mjs +87 -0
- package/src/channels/feishu/feishu-runtime.mjs +45 -0
- package/src/channels/feishu/message-utils.mjs +94 -7
- package/src/channels/feishu/multi-bot-controller.mjs +21 -0
- package/src/channels/qq/qq-bridge.mjs +91 -0
- package/src/channels/qq/qq-controller.mjs +18 -0
- package/src/channels/qq/qq-runtime.mjs +26 -0
- package/src/channels/shared/bot-workspace-store.mjs +47 -1
- package/src/channels/shared/connection-test.mjs +54 -0
- package/src/channels/shared/control-command.mjs +84 -0
- package/src/channels/shared/harness-approval.mjs +4 -0
- package/src/channels/shared/harness-client.mjs +342 -35
- package/src/channels/shared/model-command.mjs +314 -0
- package/src/channels/shared/session-binding-lock.mjs +40 -0
- package/src/channels/shared/text-harness-bridge.mjs +100 -0
- package/src/channels/shared/token-bot-controller.mjs +21 -0
- package/src/channels/shared/workspace-session.mjs +21 -9
- package/src/channels/slack/slack-controller.mjs +19 -0
- package/src/channels/slack/slack-runtime.mjs +10 -0
- package/src/channels/telegram/telegram-runtime.mjs +10 -0
- package/src/channels/wecom/wecom-bridge.mjs +79 -0
- package/src/channels/wecom/wecom-controller.mjs +18 -0
- package/src/channels/wecom/wecom-runtime.mjs +18 -0
- package/src/channels/weixin/weixin-api.mjs +1 -1
- package/src/channels/weixin/weixin-bridge.mjs +85 -0
- package/src/channels/weixin/weixin-controller.mjs +18 -0
- package/src/channels/weixin/weixin-runtime.mjs +25 -0
- package/src/channels/whatsapp/whatsapp-controller.mjs +20 -0
- package/src/channels/whatsapp/whatsapp-runtime.mjs +16 -0
|
@@ -6,6 +6,14 @@ import {
|
|
|
6
6
|
} from '../shared/harness-question.mjs';
|
|
7
7
|
import { HarnessApprovalQueue } from '../shared/harness-approval.mjs';
|
|
8
8
|
import { runCompactCommand } from '../shared/compact-command.mjs';
|
|
9
|
+
import {
|
|
10
|
+
isControlCommand,
|
|
11
|
+
runControlCommand,
|
|
12
|
+
} from '../shared/control-command.mjs';
|
|
13
|
+
import {
|
|
14
|
+
isModelCommand,
|
|
15
|
+
runModelCommand,
|
|
16
|
+
} from '../shared/model-command.mjs';
|
|
9
17
|
import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
|
|
10
18
|
import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
|
|
11
19
|
import {
|
|
@@ -14,6 +22,7 @@ import {
|
|
|
14
22
|
imagePromptUserMessage,
|
|
15
23
|
promptContentForMessage,
|
|
16
24
|
} from '../shared/image-prompt.mjs';
|
|
25
|
+
import { rememberConnectionTestTarget } from '../shared/connection-test.mjs';
|
|
17
26
|
|
|
18
27
|
const HELP_TEXT = [
|
|
19
28
|
'企业微信机器人已连接 DeepSeek Harness。',
|
|
@@ -25,6 +34,10 @@ const HELP_TEXT = [
|
|
|
25
34
|
'/workspacelist 列出工作区绝对路径',
|
|
26
35
|
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
|
|
27
36
|
'/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
|
|
37
|
+
'/models 列出所有可用模型',
|
|
38
|
+
'/model [模型ID] 查看或切换当前会话模型',
|
|
39
|
+
'/stop 停止当前任务',
|
|
40
|
+
'/steer 补充指令 纠偏当前任务',
|
|
28
41
|
'/status 检查连接状态',
|
|
29
42
|
'/help 显示本帮助',
|
|
30
43
|
].join('\n');
|
|
@@ -222,6 +235,7 @@ export class WecomHarnessBridge {
|
|
|
222
235
|
#interactionKeys = new Map();
|
|
223
236
|
#acceptedMessageIds = new Set();
|
|
224
237
|
#approvalTasks = new Set();
|
|
238
|
+
#commandTasks = new Set();
|
|
225
239
|
#approvals;
|
|
226
240
|
#prefetchedImageCount = 0;
|
|
227
241
|
|
|
@@ -269,7 +283,37 @@ export class WecomHarnessBridge {
|
|
|
269
283
|
|
|
270
284
|
const key = conversationKey(frame);
|
|
271
285
|
this.#acceptedMessageIds.add(messageId);
|
|
286
|
+
if (body.chattype === 'single') {
|
|
287
|
+
rememberConnectionTestTarget(this.#state, { chatId });
|
|
288
|
+
}
|
|
272
289
|
const pending = this.#pendingInteractions.get(key);
|
|
290
|
+
const commandMessage = wecomInboundMessage(frame, this.#client);
|
|
291
|
+
const commandText = nonEmptyString(commandMessage.content) ?? '';
|
|
292
|
+
const commandRunner = isControlCommand(commandText)
|
|
293
|
+
? runControlCommand
|
|
294
|
+
: (isModelCommand(commandText) ? runModelCommand : null);
|
|
295
|
+
if (commandRunner) {
|
|
296
|
+
let task;
|
|
297
|
+
task = this.#processFastCommand(
|
|
298
|
+
frame,
|
|
299
|
+
messageId,
|
|
300
|
+
chatId,
|
|
301
|
+
key,
|
|
302
|
+
commandMessage,
|
|
303
|
+
commandRunner,
|
|
304
|
+
).catch((error) => {
|
|
305
|
+
if (error?.code === 'turn-stopped' || this.#signal?.aborted) return;
|
|
306
|
+
this.#status.lastError = error?.message ?? String(error);
|
|
307
|
+
this.#logger.error?.('[dsh-im:wecom] failed to process a command');
|
|
308
|
+
return this.#sendImmediate(frame, chatId, '消息处理失败,请稍后重试。')
|
|
309
|
+
.catch(() => undefined);
|
|
310
|
+
}).finally(() => {
|
|
311
|
+
this.#acceptedMessageIds.delete(messageId);
|
|
312
|
+
this.#commandTasks.delete(task);
|
|
313
|
+
});
|
|
314
|
+
this.#commandTasks.add(task);
|
|
315
|
+
return task;
|
|
316
|
+
}
|
|
273
317
|
const approval = this.#approvals.claimReply({
|
|
274
318
|
key,
|
|
275
319
|
actor: senderId,
|
|
@@ -373,9 +417,35 @@ export class WecomHarnessBridge {
|
|
|
373
417
|
pending.queue ? [pending.queue] : []
|
|
374
418
|
)),
|
|
375
419
|
...this.#approvalTasks,
|
|
420
|
+
...this.#commandTasks,
|
|
376
421
|
]);
|
|
377
422
|
}
|
|
378
423
|
|
|
424
|
+
async #processFastCommand(frame, messageId, chatId, key, message, runner) {
|
|
425
|
+
this.#signal?.throwIfAborted();
|
|
426
|
+
if (this.#state.hasSeen(messageId)) return;
|
|
427
|
+
await this.#state.markSeen(messageId);
|
|
428
|
+
this.#status.messagesReceived += 1;
|
|
429
|
+
this.#status.lastMessageAt = new Date().toISOString();
|
|
430
|
+
const result = await runner(message.content, this.#harness, this.#state, key, {
|
|
431
|
+
signal: this.#signal,
|
|
432
|
+
hasImages: hasInboundImages(message),
|
|
433
|
+
pendingInteraction: this.#pendingInteractions.has(key)
|
|
434
|
+
|| this.#approvals.hasPending(key),
|
|
435
|
+
control: { owner: this, key },
|
|
436
|
+
});
|
|
437
|
+
if (result?.stopped) {
|
|
438
|
+
await Promise.allSettled([
|
|
439
|
+
this.#cancelPendingInteraction(key),
|
|
440
|
+
this.#approvals.closeRoute(key),
|
|
441
|
+
]);
|
|
442
|
+
}
|
|
443
|
+
for (const reply of result?.messages ?? [result?.message]) {
|
|
444
|
+
if (reply) await this.#sendImmediate(frame, chatId, reply);
|
|
445
|
+
}
|
|
446
|
+
this.#status.lastError = null;
|
|
447
|
+
}
|
|
448
|
+
|
|
379
449
|
async #sendActive(chatId, text) {
|
|
380
450
|
for (const chunk of splitUtf8(text)) {
|
|
381
451
|
this.#signal?.throwIfAborted();
|
|
@@ -486,6 +556,7 @@ export class WecomHarnessBridge {
|
|
|
486
556
|
askOptions: {
|
|
487
557
|
timeoutMs: this.#replyTimeoutMs,
|
|
488
558
|
signal: this.#signal,
|
|
559
|
+
control: { owner: this, key },
|
|
489
560
|
onUpdate: streamStarted && typeof this.#client.replyStreamNonBlocking === 'function'
|
|
490
561
|
? async (update) => {
|
|
491
562
|
const progress = splitUtf8(progressText(update))[0];
|
|
@@ -521,6 +592,14 @@ export class WecomHarnessBridge {
|
|
|
521
592
|
this.#status.lastReplyAt = new Date().toISOString();
|
|
522
593
|
this.#status.lastError = null;
|
|
523
594
|
} catch (error) {
|
|
595
|
+
if (error?.code === 'turn-stopped') {
|
|
596
|
+
if (streamStarted && streamId) {
|
|
597
|
+
await this.#client.replyStream(frame, streamId, '已停止。', true)
|
|
598
|
+
.catch(() => undefined);
|
|
599
|
+
}
|
|
600
|
+
await this.#state.markSeen(messageId);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
524
603
|
if (this.#signal?.aborted) return;
|
|
525
604
|
this.#status.lastError = error?.message ?? String(error);
|
|
526
605
|
this.#logger.error?.('[dsh-im:wecom] failed to process an inbound message');
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
|
|
3
3
|
import { deriveWecomBotIdentity, maskWecomBotId } from './config-store.mjs';
|
|
4
|
+
import {
|
|
5
|
+
connectionTestMessage,
|
|
6
|
+
connectionTestTargetUnavailable,
|
|
7
|
+
} from '../shared/connection-test.mjs';
|
|
4
8
|
|
|
5
9
|
const ACTIVE_ATTEMPT_STATES = new Set(['pending', 'connecting']);
|
|
6
10
|
const TERMINAL_ATTEMPT_STATES = new Set(['connected', 'failed', 'cancelled', 'expired']);
|
|
@@ -237,6 +241,20 @@ export class WecomController {
|
|
|
237
241
|
return this.status();
|
|
238
242
|
}
|
|
239
243
|
|
|
244
|
+
async sendConnectionTest(botId) {
|
|
245
|
+
const config = this.#configStore.get(botId);
|
|
246
|
+
if (!config) throw new Error('Unknown Enterprise WeChat bot');
|
|
247
|
+
return this.#withBotTransition(botId, async () => {
|
|
248
|
+
const runtime = this.#runtimes.get(botId);
|
|
249
|
+
if (!runtime?.status?.ready || typeof runtime.sendConnectionTest !== 'function') {
|
|
250
|
+
throw connectionTestTargetUnavailable('企业微信机器人');
|
|
251
|
+
}
|
|
252
|
+
return runtime.sendConnectionTest(connectionTestMessage(
|
|
253
|
+
`企业微信机器人(${maskWecomBotId(config.remoteBotId)})`,
|
|
254
|
+
));
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
240
258
|
async deleteBot(botId) {
|
|
241
259
|
const config = this.#configStore.get(botId);
|
|
242
260
|
if (!config) throw new Error('Unknown Enterprise WeChat bot');
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { WSAuthFailureError, WSClient, WSReconnectExhaustedError } from '@wecom/aibot-node-sdk';
|
|
2
2
|
|
|
3
3
|
import { createWecomBridgeStatus, WecomHarnessBridge } from './wecom-bridge.mjs';
|
|
4
|
+
import { sendRememberedConnectionTest } from '../shared/connection-test.mjs';
|
|
4
5
|
|
|
5
6
|
function timeoutError() {
|
|
6
7
|
const error = new Error('Enterprise WeChat WebSocket authentication timed out');
|
|
@@ -197,6 +198,23 @@ export class WecomRuntime {
|
|
|
197
198
|
return this.status;
|
|
198
199
|
}
|
|
199
200
|
|
|
201
|
+
async sendConnectionTest(text) {
|
|
202
|
+
return sendRememberedConnectionTest({
|
|
203
|
+
state: this.#state,
|
|
204
|
+
text,
|
|
205
|
+
channelLabel: '企业微信机器人',
|
|
206
|
+
send: async ({ chatId }, content) => {
|
|
207
|
+
if (!this.#status.ready || !this.#client) {
|
|
208
|
+
throw new Error('Enterprise WeChat runtime is not connected');
|
|
209
|
+
}
|
|
210
|
+
await this.#client.sendMessage(chatId, {
|
|
211
|
+
msgtype: 'markdown',
|
|
212
|
+
markdown: { content },
|
|
213
|
+
});
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
200
218
|
async #stopActive() {
|
|
201
219
|
const client = this.#client;
|
|
202
220
|
const bridge = this.#bridge;
|
|
@@ -11,6 +11,14 @@ import {
|
|
|
11
11
|
} from '../shared/harness-question.mjs';
|
|
12
12
|
import { HarnessApprovalQueue } from '../shared/harness-approval.mjs';
|
|
13
13
|
import { runCompactCommand } from '../shared/compact-command.mjs';
|
|
14
|
+
import {
|
|
15
|
+
isControlCommand,
|
|
16
|
+
runControlCommand,
|
|
17
|
+
} from '../shared/control-command.mjs';
|
|
18
|
+
import {
|
|
19
|
+
isModelCommand,
|
|
20
|
+
runModelCommand,
|
|
21
|
+
} from '../shared/model-command.mjs';
|
|
14
22
|
import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
|
|
15
23
|
import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
|
|
16
24
|
import {
|
|
@@ -18,6 +26,7 @@ import {
|
|
|
18
26
|
imagePromptUserMessage,
|
|
19
27
|
promptContentForMessage,
|
|
20
28
|
} from '../shared/image-prompt.mjs';
|
|
29
|
+
import { rememberConnectionTestTarget } from '../shared/connection-test.mjs';
|
|
21
30
|
|
|
22
31
|
const INTERACTION_RESOLVED_TEXT = '这个问题已在其他客户端处理,无需再次回答。';
|
|
23
32
|
|
|
@@ -31,6 +40,10 @@ const HELP_TEXT = [
|
|
|
31
40
|
'/workspacelist 列出工作区绝对路径',
|
|
32
41
|
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
|
|
33
42
|
'/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
|
|
43
|
+
'/models 列出所有可用模型',
|
|
44
|
+
'/model [模型ID] 查看或切换当前会话模型',
|
|
45
|
+
'/stop 停止当前任务',
|
|
46
|
+
'/steer 补充指令 纠偏当前任务',
|
|
34
47
|
'/status 检查连接状态',
|
|
35
48
|
'/help 显示本帮助',
|
|
36
49
|
].join('\n');
|
|
@@ -84,6 +97,7 @@ export class WeixinHarnessBridge {
|
|
|
84
97
|
#interactionKeys = new Map();
|
|
85
98
|
#acceptedMessageIds = new Set();
|
|
86
99
|
#approvalTasks = new Set();
|
|
100
|
+
#commandTasks = new Set();
|
|
87
101
|
#approvals;
|
|
88
102
|
|
|
89
103
|
constructor({
|
|
@@ -128,10 +142,41 @@ export class WeixinHarnessBridge {
|
|
|
128
142
|
if (!messageId || !sender || this.#state.hasSeen(messageId)
|
|
129
143
|
|| this.#acceptedMessageIds.has(messageId)) return Promise.resolve();
|
|
130
144
|
this.#acceptedMessageIds.add(messageId);
|
|
145
|
+
if (sender === this.#ownerUserId) {
|
|
146
|
+
rememberConnectionTestTarget(this.#state, { toUserId: sender });
|
|
147
|
+
}
|
|
131
148
|
const key = conversationKey(sender);
|
|
132
149
|
const contextToken = nonEmptyString(message?.context_token) ?? undefined;
|
|
133
150
|
const runId = nonEmptyString(message?.run_id) ?? undefined;
|
|
134
151
|
const pending = this.#pendingInteractions.get(key);
|
|
152
|
+
const commandText = nonEmptyString(extractWeixinText(message)) ?? '';
|
|
153
|
+
const commandRunner = isControlCommand(commandText)
|
|
154
|
+
? runControlCommand
|
|
155
|
+
: (isModelCommand(commandText) ? runModelCommand : null);
|
|
156
|
+
if (commandRunner && sender === this.#ownerUserId) {
|
|
157
|
+
let task;
|
|
158
|
+
task = this.#processFastCommand(
|
|
159
|
+
message,
|
|
160
|
+
messageId,
|
|
161
|
+
key,
|
|
162
|
+
sender,
|
|
163
|
+
contextToken,
|
|
164
|
+
runId,
|
|
165
|
+
commandText,
|
|
166
|
+
commandRunner,
|
|
167
|
+
).catch((error) => {
|
|
168
|
+
if (error?.code === 'turn-stopped' || this.#signal?.aborted) return;
|
|
169
|
+
this.#status.lastError = error?.message ?? String(error);
|
|
170
|
+
this.#logger.error?.('[dsh-weixin] failed to process a command:', error);
|
|
171
|
+
return this.#send(sender, '消息处理失败,请稍后重试。', contextToken, runId)
|
|
172
|
+
.catch(() => undefined);
|
|
173
|
+
}).finally(() => {
|
|
174
|
+
this.#acceptedMessageIds.delete(messageId);
|
|
175
|
+
this.#commandTasks.delete(task);
|
|
176
|
+
});
|
|
177
|
+
this.#commandTasks.add(task);
|
|
178
|
+
return task;
|
|
179
|
+
}
|
|
135
180
|
const approval = this.#approvals.claimReply({
|
|
136
181
|
key,
|
|
137
182
|
actor: sender,
|
|
@@ -207,9 +252,44 @@ export class WeixinHarnessBridge {
|
|
|
207
252
|
pending.queue ? [pending.queue] : []
|
|
208
253
|
)),
|
|
209
254
|
...this.#approvalTasks,
|
|
255
|
+
...this.#commandTasks,
|
|
210
256
|
]);
|
|
211
257
|
}
|
|
212
258
|
|
|
259
|
+
async #processFastCommand(
|
|
260
|
+
message,
|
|
261
|
+
messageId,
|
|
262
|
+
key,
|
|
263
|
+
sender,
|
|
264
|
+
contextToken,
|
|
265
|
+
runId,
|
|
266
|
+
text,
|
|
267
|
+
runner,
|
|
268
|
+
) {
|
|
269
|
+
this.#signal?.throwIfAborted();
|
|
270
|
+
if (this.#state.hasSeen(messageId)) return;
|
|
271
|
+
await this.#state.markSeen(messageId);
|
|
272
|
+
this.#status.messagesReceived += 1;
|
|
273
|
+
this.#status.lastMessageAt = new Date().toISOString();
|
|
274
|
+
const result = await runner(text, this.#harness, this.#state, key, {
|
|
275
|
+
signal: this.#signal,
|
|
276
|
+
hasImages: hasWeixinImageItems(message),
|
|
277
|
+
pendingInteraction: this.#pendingInteractions.has(key)
|
|
278
|
+
|| this.#approvals.hasPending(key),
|
|
279
|
+
control: { owner: this, key },
|
|
280
|
+
});
|
|
281
|
+
if (result?.stopped) {
|
|
282
|
+
await Promise.allSettled([
|
|
283
|
+
this.#cancelPendingInteraction(key),
|
|
284
|
+
this.#approvals.closeRoute(key),
|
|
285
|
+
]);
|
|
286
|
+
}
|
|
287
|
+
for (const reply of result?.messages ?? [result?.message]) {
|
|
288
|
+
if (reply) await this.#send(sender, reply, contextToken, runId);
|
|
289
|
+
}
|
|
290
|
+
this.#status.lastError = null;
|
|
291
|
+
}
|
|
292
|
+
|
|
213
293
|
async #process(message, key, { alreadyRecorded = false } = {}) {
|
|
214
294
|
this.#signal?.throwIfAborted();
|
|
215
295
|
const messageId = weixinMessageId(message);
|
|
@@ -299,6 +379,7 @@ export class WeixinHarnessBridge {
|
|
|
299
379
|
askOptions: {
|
|
300
380
|
timeoutMs: this.#replyTimeoutMs,
|
|
301
381
|
signal: this.#signal,
|
|
382
|
+
control: { owner: this, key },
|
|
302
383
|
onInteraction: (interaction) => this.#handleInteraction(interaction, {
|
|
303
384
|
key,
|
|
304
385
|
actor: sender,
|
|
@@ -320,6 +401,10 @@ export class WeixinHarnessBridge {
|
|
|
320
401
|
this.#status.lastReplyAt = new Date().toISOString();
|
|
321
402
|
this.#status.lastError = null;
|
|
322
403
|
} catch (error) {
|
|
404
|
+
if (error?.code === 'turn-stopped') {
|
|
405
|
+
await this.#state.markSeen(messageId);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
323
408
|
if (this.#signal?.aborted) return;
|
|
324
409
|
this.#status.lastError = error?.message ?? String(error);
|
|
325
410
|
this.#logger.error?.('[dsh-weixin] failed to process an inbound message:', error);
|
|
@@ -6,6 +6,10 @@ import {
|
|
|
6
6
|
WeixinApiError,
|
|
7
7
|
} from './weixin-api.mjs';
|
|
8
8
|
import { deriveWeixinBotIdentity, maskWeixinAccountId } from './config-store.mjs';
|
|
9
|
+
import {
|
|
10
|
+
connectionTestMessage,
|
|
11
|
+
connectionTestTargetUnavailable,
|
|
12
|
+
} from '../shared/connection-test.mjs';
|
|
9
13
|
|
|
10
14
|
const ACTIVE_ATTEMPT_STATES = new Set([
|
|
11
15
|
'starting',
|
|
@@ -240,6 +244,20 @@ export class WeixinController {
|
|
|
240
244
|
return this.status();
|
|
241
245
|
}
|
|
242
246
|
|
|
247
|
+
async sendConnectionTest(botId) {
|
|
248
|
+
const config = this.#configStore.get(botId);
|
|
249
|
+
if (!config) throw new Error('Unknown Weixin account');
|
|
250
|
+
return this.#withBotTransition(botId, async () => {
|
|
251
|
+
const runtime = this.#runtimes.get(botId);
|
|
252
|
+
if (!runtime?.status?.ready || typeof runtime.sendConnectionTest !== 'function') {
|
|
253
|
+
throw connectionTestTargetUnavailable('微信机器人');
|
|
254
|
+
}
|
|
255
|
+
return runtime.sendConnectionTest(connectionTestMessage(
|
|
256
|
+
`微信机器人(${maskWeixinAccountId(config.accountId)})`,
|
|
257
|
+
));
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
243
261
|
async deleteBot(botId) {
|
|
244
262
|
const config = this.#configStore.get(botId);
|
|
245
263
|
if (!config) throw new Error('Unknown Weixin account');
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { WeixinApiError } from './weixin-api.mjs';
|
|
2
2
|
import { createWeixinBridgeStatus, WeixinHarnessBridge } from './weixin-bridge.mjs';
|
|
3
|
+
import {
|
|
4
|
+
connectionTestTarget,
|
|
5
|
+
connectionTestTargetUnavailable,
|
|
6
|
+
} from '../shared/connection-test.mjs';
|
|
3
7
|
|
|
4
8
|
const DEFAULT_START_RETRY_DELAYS_MS = Object.freeze([250, 1_000, 3_000]);
|
|
5
9
|
|
|
@@ -250,4 +254,25 @@ export class WeixinRuntime {
|
|
|
250
254
|
this.#status.weixinConnectionState = 'idle';
|
|
251
255
|
return this.status;
|
|
252
256
|
}
|
|
257
|
+
|
|
258
|
+
async sendConnectionTest(text) {
|
|
259
|
+
const remembered = connectionTestTarget(this.#state);
|
|
260
|
+
const toUserId = typeof remembered?.toUserId === 'string' && remembered.toUserId.trim()
|
|
261
|
+
? remembered.toUserId.trim()
|
|
262
|
+
: typeof this.#config.ownerUserId === 'string' && this.#config.ownerUserId.trim()
|
|
263
|
+
? this.#config.ownerUserId.trim()
|
|
264
|
+
: null;
|
|
265
|
+
if (!toUserId) throw connectionTestTargetUnavailable('微信机器人');
|
|
266
|
+
if (!this.#status.ready || !this.#abortController) {
|
|
267
|
+
throw new Error('Weixin runtime is not connected');
|
|
268
|
+
}
|
|
269
|
+
await this.#api.sendText({
|
|
270
|
+
baseUrl: this.#config.baseUrl,
|
|
271
|
+
token: this.#token,
|
|
272
|
+
toUserId,
|
|
273
|
+
text,
|
|
274
|
+
signal: this.#abortController.signal,
|
|
275
|
+
});
|
|
276
|
+
return { sent: true };
|
|
277
|
+
}
|
|
253
278
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
|
|
3
|
+
import { connectionTestMessage } from '../shared/connection-test.mjs';
|
|
3
4
|
import { deriveWhatsappBotId, maskWhatsappAccount } from './config-store.mjs';
|
|
4
5
|
|
|
5
6
|
const ACTIVE_ATTEMPT_STATES = new Set(['starting', 'pending', 'connecting']);
|
|
@@ -198,6 +199,25 @@ export class WhatsappController {
|
|
|
198
199
|
return this.status();
|
|
199
200
|
}
|
|
200
201
|
|
|
202
|
+
async sendConnectionTest(botId) {
|
|
203
|
+
const config = this.#configStore.get(botId);
|
|
204
|
+
if (!config) throw new Error('Unknown WhatsApp bot');
|
|
205
|
+
return this.#withBotTransition(botId, async () => {
|
|
206
|
+
const runtime = this.#runtimes.get(botId);
|
|
207
|
+
if (!runtime?.status?.ready || typeof runtime.sendConnectionTest !== 'function') {
|
|
208
|
+
const error = new Error('WhatsApp机器人尚未连接');
|
|
209
|
+
error.code = 'test-target-unavailable';
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
return runtime.sendConnectionTest(
|
|
213
|
+
connectionTestMessage(
|
|
214
|
+
`${config.name}(${maskWhatsappAccount(config.accountJid)})`,
|
|
215
|
+
'WhatsApp机器人',
|
|
216
|
+
),
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
201
221
|
async deleteBot(botId) {
|
|
202
222
|
const config = this.#configStore.get(botId);
|
|
203
223
|
if (!config) throw new Error('Unknown WhatsApp bot');
|
|
@@ -421,6 +421,22 @@ export class WhatsappRuntime {
|
|
|
421
421
|
return this.stop();
|
|
422
422
|
}
|
|
423
423
|
|
|
424
|
+
async sendConnectionTest(text) {
|
|
425
|
+
if (!this.#status.ready || !this.#client) {
|
|
426
|
+
const error = new Error('WhatsApp机器人尚未连接');
|
|
427
|
+
error.code = 'test-target-unavailable';
|
|
428
|
+
throw error;
|
|
429
|
+
}
|
|
430
|
+
if (typeof text !== 'string' || !text.trim()) {
|
|
431
|
+
throw new TypeError('WhatsApp connection test text is required');
|
|
432
|
+
}
|
|
433
|
+
await this.#client.sendText({
|
|
434
|
+
jid: this.#config.accountJid,
|
|
435
|
+
selfChat: true,
|
|
436
|
+
}, text);
|
|
437
|
+
return { sent: true };
|
|
438
|
+
}
|
|
439
|
+
|
|
424
440
|
async stop() {
|
|
425
441
|
const session = this.#session;
|
|
426
442
|
const client = this.#client;
|