@xmanrui/dsh-im 0.10.0 → 0.11.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 +2 -0
- package/README.md +2 -0
- package/lib/client.js +377 -27
- package/lib/index.js +128 -127
- package/package.json +1 -1
- 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 +1 -1
- package/plugin-src/host/channels/dingtalk/rpc.mjs +29 -3
- package/plugin-src/host/channels/feishu/production.mjs +1 -1
- package/plugin-src/host/channels/feishu/rpc.mjs +34 -3
- package/plugin-src/host/channels/qq/production.mjs +1 -1
- package/plugin-src/host/channels/qq/rpc.mjs +35 -2
- package/plugin-src/host/channels/shared/production.mjs +1 -1
- package/plugin-src/host/channels/shared/rpc.mjs +25 -1
- package/plugin-src/host/channels/slack/production.mjs +1 -1
- package/plugin-src/host/channels/slack/rpc.mjs +28 -2
- package/plugin-src/host/channels/wecom/production.mjs +1 -1
- package/plugin-src/host/channels/wecom/rpc.mjs +31 -2
- package/plugin-src/host/channels/weixin/production.mjs +1 -1
- package/plugin-src/host/channels/weixin/rpc.mjs +29 -3
- package/plugin-src/host/channels/whatsapp/production.mjs +1 -1
- package/plugin-src/host/channels/whatsapp/rpc.mjs +31 -3
- package/src/channels/dingtalk/dingtalk-bridge.mjs +4 -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 +6 -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 +7 -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 +2 -0
- package/src/channels/shared/connection-test.mjs +54 -0
- package/src/channels/shared/harness-client.mjs +6 -6
- package/src/channels/shared/text-harness-bridge.mjs +19 -0
- package/src/channels/shared/token-bot-controller.mjs +21 -0
- 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 +4 -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 +4 -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
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { QQBot, typingIndicator } from '@tencent-connect/qqbot-nodejs';
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
connectionTestTarget,
|
|
5
|
+
connectionTestTargetUnavailable,
|
|
6
|
+
} from '../shared/connection-test.mjs';
|
|
3
7
|
import { createQqBridgeStatus, QqHarnessBridge } from './qq-bridge.mjs';
|
|
4
8
|
|
|
5
9
|
function timeoutError() {
|
|
@@ -67,6 +71,28 @@ export class QqRuntime {
|
|
|
67
71
|
return structuredClone(this.#status);
|
|
68
72
|
}
|
|
69
73
|
|
|
74
|
+
async sendConnectionTest(text) {
|
|
75
|
+
if (!this.#status.ready || !this.#bot) {
|
|
76
|
+
throw connectionTestTargetUnavailable('QQ机器人');
|
|
77
|
+
}
|
|
78
|
+
const ownerUserOpenid = typeof this.#config.ownerUserOpenid === 'string'
|
|
79
|
+
? this.#config.ownerUserOpenid.trim()
|
|
80
|
+
: '';
|
|
81
|
+
const remembered = connectionTestTarget(this.#state);
|
|
82
|
+
const rememberedUserOpenid = remembered?.scope === 'c2c'
|
|
83
|
+
&& typeof remembered.targetId === 'string'
|
|
84
|
+
? remembered.targetId.trim()
|
|
85
|
+
: '';
|
|
86
|
+
const target = rememberedUserOpenid
|
|
87
|
+
? { scope: 'c2c', targetId: rememberedUserOpenid }
|
|
88
|
+
: (ownerUserOpenid && ownerUserOpenid !== '*'
|
|
89
|
+
? { scope: 'c2c', targetId: ownerUserOpenid }
|
|
90
|
+
: null);
|
|
91
|
+
if (!target) throw connectionTestTargetUnavailable('QQ机器人');
|
|
92
|
+
await this.#bot.sendText(target, text);
|
|
93
|
+
return { sent: true };
|
|
94
|
+
}
|
|
95
|
+
|
|
70
96
|
async start() {
|
|
71
97
|
if (this.#status.ready && this.#bot) return this.status;
|
|
72
98
|
if (this.#starting) return this.#starting;
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from 'node:fs/promises';
|
|
10
10
|
import { dirname, isAbsolute, resolve } from 'node:path';
|
|
11
11
|
|
|
12
|
+
import { CONNECTION_TEST_STATE_IDENTITY } from './connection-test.mjs';
|
|
12
13
|
import { WORKSPACE_SESSION_STALE } from './workspace-session.mjs';
|
|
13
14
|
|
|
14
15
|
const EMPTY_DOCUMENT = Object.freeze({ version: 1, workspaces: Object.freeze({}) });
|
|
@@ -687,6 +688,7 @@ export function createBotWorkspaceScope(harness, { botId, workspaces, state }) {
|
|
|
687
688
|
});
|
|
688
689
|
const scopedState = new Proxy(state, {
|
|
689
690
|
get(target, property) {
|
|
691
|
+
if (property === CONNECTION_TEST_STATE_IDENTITY) return target;
|
|
690
692
|
if (property === 'sessionFor') {
|
|
691
693
|
return (key, ...args) => {
|
|
692
694
|
if (!isCurrentScope()) return null;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const targets = new WeakMap();
|
|
2
|
+
|
|
3
|
+
export const CONNECTION_TEST_STATE_IDENTITY = Symbol('dsh-im.connection-test-state-identity');
|
|
4
|
+
|
|
5
|
+
function stateIdentity(state) {
|
|
6
|
+
return state?.[CONNECTION_TEST_STATE_IDENTITY] ?? state;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function cleanText(value) {
|
|
10
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function rememberConnectionTestTarget(state, target) {
|
|
14
|
+
if (!state || !target || typeof target !== 'object') return false;
|
|
15
|
+
try {
|
|
16
|
+
targets.set(stateIdentity(state), structuredClone(target));
|
|
17
|
+
return true;
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function connectionTestTarget(state) {
|
|
24
|
+
const target = state ? targets.get(stateIdentity(state)) : null;
|
|
25
|
+
return target ? structuredClone(target) : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function connectionTestMessage(botName, channelLabel = '机器人') {
|
|
29
|
+
const name = cleanText(botName) ?? channelLabel;
|
|
30
|
+
return `✅ DeepSeek Harness 连接测试成功\n这条消息由插件页面中的“${name}”机器人卡片发出。`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function connectionTestTargetUnavailable(channelLabel = '机器人') {
|
|
34
|
+
const error = new Error(`${channelLabel}尚未收到可用于测试的私聊消息。`);
|
|
35
|
+
error.code = 'test-target-unavailable';
|
|
36
|
+
return error;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function sendRememberedConnectionTest({ state, send, text, channelLabel }) {
|
|
40
|
+
const target = connectionTestTarget(state);
|
|
41
|
+
if (!target) throw connectionTestTargetUnavailable(channelLabel);
|
|
42
|
+
await send(target, text);
|
|
43
|
+
return { sent: true };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function publicConnectionTestResult(error) {
|
|
47
|
+
if (!error) return Object.freeze({ sent: true });
|
|
48
|
+
return Object.freeze({
|
|
49
|
+
sent: false,
|
|
50
|
+
code: error?.code === 'test-target-unavailable'
|
|
51
|
+
? 'test-target-unavailable'
|
|
52
|
+
: 'test-message-failed',
|
|
53
|
+
});
|
|
54
|
+
}
|
|
@@ -327,7 +327,7 @@ export class HarnessClient {
|
|
|
327
327
|
constructor({
|
|
328
328
|
baseUrl,
|
|
329
329
|
workspace,
|
|
330
|
-
agentPreset
|
|
330
|
+
agentPreset,
|
|
331
331
|
autostart = false,
|
|
332
332
|
dshBin = 'dsh',
|
|
333
333
|
fetchImpl = fetch,
|
|
@@ -354,7 +354,8 @@ export class HarnessClient {
|
|
|
354
354
|
}
|
|
355
355
|
this.#baseUrl = new URL(baseUrl);
|
|
356
356
|
this.#workspace = workspace;
|
|
357
|
-
|
|
357
|
+
// Keep an omitted preset absent so session.create resolves the Host's current default.
|
|
358
|
+
this.#agentPreset = agentPreset ?? undefined;
|
|
358
359
|
this.#autostart = autostart;
|
|
359
360
|
this.#dshBin = dshBin;
|
|
360
361
|
this.#fetch = fetchImpl;
|
|
@@ -458,10 +459,9 @@ export class HarnessClient {
|
|
|
458
459
|
async createSession(options = {}) {
|
|
459
460
|
await this.ensureRunning(options);
|
|
460
461
|
const workspaceId = await this.workspaceId(options);
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
}, 30_000, options);
|
|
462
|
+
const payload = { workspaceId };
|
|
463
|
+
if (this.#agentPreset !== undefined) payload.agentPreset = this.#agentPreset;
|
|
464
|
+
const created = await this.rpc('session.create', payload, 30_000, options);
|
|
465
465
|
return created.sessionId;
|
|
466
466
|
}
|
|
467
467
|
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { runWorkspaceCommand } from './workspace-command.mjs';
|
|
2
2
|
import { runCompactCommand } from './compact-command.mjs';
|
|
3
|
+
import {
|
|
4
|
+
rememberConnectionTestTarget,
|
|
5
|
+
sendRememberedConnectionTest,
|
|
6
|
+
} from './connection-test.mjs';
|
|
3
7
|
import { askInWorkspaceSession } from './workspace-session.mjs';
|
|
4
8
|
import { HarnessApprovalQueue } from './harness-approval.mjs';
|
|
5
9
|
import {
|
|
@@ -97,6 +101,12 @@ export class TextHarnessBridge {
|
|
|
97
101
|
return Promise.resolve();
|
|
98
102
|
}
|
|
99
103
|
this.#acceptedMessageIds.add(messageId);
|
|
104
|
+
if (normalized.kind === 'direct') {
|
|
105
|
+
rememberConnectionTestTarget(
|
|
106
|
+
this.#state,
|
|
107
|
+
normalized.connectionTestTarget ?? normalized.replyTarget,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
100
110
|
|
|
101
111
|
const key = `${kind}:${conversationId}`;
|
|
102
112
|
const pending = this.#pendingInteractions.get(key);
|
|
@@ -194,6 +204,15 @@ export class TextHarnessBridge {
|
|
|
194
204
|
]);
|
|
195
205
|
}
|
|
196
206
|
|
|
207
|
+
sendConnectionTest(text) {
|
|
208
|
+
return sendRememberedConnectionTest({
|
|
209
|
+
state: this.#state,
|
|
210
|
+
text,
|
|
211
|
+
channelLabel: `${this.#descriptor.label}机器人`,
|
|
212
|
+
send: (target, message) => this.#bot.sendText(target, message),
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
197
216
|
async #process(message, messageId, senderId, conversationKey, {
|
|
198
217
|
alreadyRecorded = false,
|
|
199
218
|
} = {}) {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { connectionTestMessage } from './connection-test.mjs';
|
|
2
|
+
|
|
1
3
|
function cleanString(value) {
|
|
2
4
|
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
3
5
|
}
|
|
@@ -161,6 +163,25 @@ export class TokenBotController {
|
|
|
161
163
|
return this.status();
|
|
162
164
|
}
|
|
163
165
|
|
|
166
|
+
async sendConnectionTest(botId) {
|
|
167
|
+
const config = this.#configStore.get(botId);
|
|
168
|
+
if (!config) throw new Error(`Unknown ${this.#descriptor.label} bot`);
|
|
169
|
+
return this.#withBotTransition(botId, async () => {
|
|
170
|
+
const runtime = this.#runtimes.get(botId);
|
|
171
|
+
if (!runtime?.status?.ready || typeof runtime.sendConnectionTest !== 'function') {
|
|
172
|
+
const error = new Error(`${this.#descriptor.label}机器人尚未连接`);
|
|
173
|
+
error.code = 'test-target-unavailable';
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
const cardLabel = `${config.name}(${this.#maskPlatformId(config.platformId)})`;
|
|
177
|
+
await runtime.sendConnectionTest(connectionTestMessage(
|
|
178
|
+
cardLabel,
|
|
179
|
+
`${this.#descriptor.label}机器人`,
|
|
180
|
+
));
|
|
181
|
+
return { sent: true };
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
164
185
|
async deleteBot(botId) {
|
|
165
186
|
const config = this.#configStore.get(botId);
|
|
166
187
|
if (!config) throw new Error(`Unknown ${this.#descriptor.label} bot`);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { connectionTestMessage } from '../shared/connection-test.mjs';
|
|
1
2
|
import { deriveSlackBotIdentity, maskSlackBotId } from './config-store.mjs';
|
|
2
3
|
import { inspectSlackCredentials } from './slack-api.mjs';
|
|
3
4
|
import { SLACK_DESCRIPTOR } from './slack-bridge.mjs';
|
|
@@ -168,6 +169,24 @@ export class SlackController {
|
|
|
168
169
|
return this.status();
|
|
169
170
|
}
|
|
170
171
|
|
|
172
|
+
async sendConnectionTest(botId) {
|
|
173
|
+
const config = this.#configStore.get(botId);
|
|
174
|
+
if (!config) throw new Error('Unknown Slack bot');
|
|
175
|
+
return this.#withBotTransition(botId, async () => {
|
|
176
|
+
const runtime = this.#runtimes.get(botId);
|
|
177
|
+
if (!runtime?.status?.ready || typeof runtime.sendConnectionTest !== 'function') {
|
|
178
|
+
const error = new Error('Slack机器人尚未连接');
|
|
179
|
+
error.code = 'test-target-unavailable';
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
await runtime.sendConnectionTest(connectionTestMessage(
|
|
183
|
+
`${config.name}(${maskSlackBotId(config.platformId)})`,
|
|
184
|
+
'Slack机器人',
|
|
185
|
+
));
|
|
186
|
+
return { sent: true };
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
171
190
|
async deleteBot(botId) {
|
|
172
191
|
const config = this.#configStore.get(botId);
|
|
173
192
|
if (!config) throw new Error('Unknown Slack bot');
|
|
@@ -83,6 +83,7 @@ export function normalizeSlackEvent(payload, botUserId, { loadFile = async () =>
|
|
|
83
83
|
recipientUserId: String(event.user),
|
|
84
84
|
recipientTeamId: String(event.user_team ?? payload.team_id ?? ''),
|
|
85
85
|
},
|
|
86
|
+
connectionTestTarget: { channelId: String(event.channel) },
|
|
86
87
|
};
|
|
87
88
|
}
|
|
88
89
|
|
|
@@ -293,6 +294,15 @@ export class SlackRuntime {
|
|
|
293
294
|
return structuredClone(this.#status);
|
|
294
295
|
}
|
|
295
296
|
|
|
297
|
+
async sendConnectionTest(text) {
|
|
298
|
+
if (!this.#status.ready || !this.#bridge) {
|
|
299
|
+
const error = new Error('Slack bot is not connected');
|
|
300
|
+
error.code = 'test-target-unavailable';
|
|
301
|
+
throw error;
|
|
302
|
+
}
|
|
303
|
+
return this.#bridge.sendConnectionTest(text);
|
|
304
|
+
}
|
|
305
|
+
|
|
296
306
|
async start() {
|
|
297
307
|
if (this.#status.ready && this.#socket) return this.status;
|
|
298
308
|
if (this.#starting) return this.#starting;
|
|
@@ -111,6 +111,7 @@ export function normalizeTelegramUpdate(update, { botId, username, loadFile = as
|
|
|
111
111
|
replyToMessageId: messageId,
|
|
112
112
|
messageThreadId,
|
|
113
113
|
},
|
|
114
|
+
connectionTestTarget: { chatId, messageThreadId },
|
|
114
115
|
};
|
|
115
116
|
}
|
|
116
117
|
|
|
@@ -229,6 +230,15 @@ export class TelegramRuntime {
|
|
|
229
230
|
return structuredClone(this.#status);
|
|
230
231
|
}
|
|
231
232
|
|
|
233
|
+
async sendConnectionTest(text) {
|
|
234
|
+
if (!this.#status.ready || !this.#bridge) {
|
|
235
|
+
const error = new Error('Telegram bot is not connected');
|
|
236
|
+
error.code = 'test-target-unavailable';
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
239
|
+
return this.#bridge.sendConnectionTest(text);
|
|
240
|
+
}
|
|
241
|
+
|
|
232
242
|
async start() {
|
|
233
243
|
if (this.#status.ready && this.#pollTask) return this.status;
|
|
234
244
|
if (this.#starting) return this.#starting;
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
imagePromptUserMessage,
|
|
15
15
|
promptContentForMessage,
|
|
16
16
|
} from '../shared/image-prompt.mjs';
|
|
17
|
+
import { rememberConnectionTestTarget } from '../shared/connection-test.mjs';
|
|
17
18
|
|
|
18
19
|
const HELP_TEXT = [
|
|
19
20
|
'企业微信机器人已连接 DeepSeek Harness。',
|
|
@@ -269,6 +270,9 @@ export class WecomHarnessBridge {
|
|
|
269
270
|
|
|
270
271
|
const key = conversationKey(frame);
|
|
271
272
|
this.#acceptedMessageIds.add(messageId);
|
|
273
|
+
if (body.chattype === 'single') {
|
|
274
|
+
rememberConnectionTestTarget(this.#state, { chatId });
|
|
275
|
+
}
|
|
272
276
|
const pending = this.#pendingInteractions.get(key);
|
|
273
277
|
const approval = this.#approvals.claimReply({
|
|
274
278
|
key,
|
|
@@ -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;
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
imagePromptUserMessage,
|
|
19
19
|
promptContentForMessage,
|
|
20
20
|
} from '../shared/image-prompt.mjs';
|
|
21
|
+
import { rememberConnectionTestTarget } from '../shared/connection-test.mjs';
|
|
21
22
|
|
|
22
23
|
const INTERACTION_RESOLVED_TEXT = '这个问题已在其他客户端处理,无需再次回答。';
|
|
23
24
|
|
|
@@ -128,6 +129,9 @@ export class WeixinHarnessBridge {
|
|
|
128
129
|
if (!messageId || !sender || this.#state.hasSeen(messageId)
|
|
129
130
|
|| this.#acceptedMessageIds.has(messageId)) return Promise.resolve();
|
|
130
131
|
this.#acceptedMessageIds.add(messageId);
|
|
132
|
+
if (sender === this.#ownerUserId) {
|
|
133
|
+
rememberConnectionTestTarget(this.#state, { toUserId: sender });
|
|
134
|
+
}
|
|
131
135
|
const key = conversationKey(sender);
|
|
132
136
|
const contextToken = nonEmptyString(message?.context_token) ?? undefined;
|
|
133
137
|
const runId = nonEmptyString(message?.run_id) ?? undefined;
|
|
@@ -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;
|