@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,4 +1,5 @@
|
|
|
1
1
|
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
|
2
|
+
import { publicConnectionTestResult } from '../../../../src/channels/shared/connection-test.mjs';
|
|
2
3
|
import {
|
|
3
4
|
publicWorkspaceError,
|
|
4
5
|
SET_WORKSPACE_ENDPOINT,
|
|
@@ -53,7 +54,8 @@ function payloadFailure(endpoint, payload) {
|
|
|
53
54
|
? null : 'bot.bind-credentials requires xoxb Bot Token and xapp App Token.';
|
|
54
55
|
}
|
|
55
56
|
if (endpoint === SLACK_ENDPOINTS.reconnectBot) {
|
|
56
|
-
return exactKeys(payload, ['botId']) && validId(payload.botId)
|
|
57
|
+
return exactKeys(payload, ['botId', 'sendTest']) && validId(payload.botId)
|
|
58
|
+
&& (payload.sendTest === undefined || typeof payload.sendTest === 'boolean')
|
|
57
59
|
? null : 'bot.reconnect requires a botId.';
|
|
58
60
|
}
|
|
59
61
|
if (endpoint === SLACK_ENDPOINTS.deleteBot) {
|
|
@@ -114,7 +116,31 @@ export function createSlackRpcHandler(controller) {
|
|
|
114
116
|
let value;
|
|
115
117
|
if (endpoint === SLACK_ENDPOINTS.status) value = await controller.status();
|
|
116
118
|
else if (endpoint === SLACK_ENDPOINTS.bindCredentials) value = await controller.bindCredentials(payload);
|
|
117
|
-
else if (endpoint === SLACK_ENDPOINTS.reconnectBot)
|
|
119
|
+
else if (endpoint === SLACK_ENDPOINTS.reconnectBot) {
|
|
120
|
+
value = await controller.reconnectBot(payload.botId);
|
|
121
|
+
if (signal?.aborted) {
|
|
122
|
+
return { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } };
|
|
123
|
+
}
|
|
124
|
+
if (payload.sendTest === true) {
|
|
125
|
+
let testError = null;
|
|
126
|
+
try {
|
|
127
|
+
if (value?.bots?.find((bot) => bot?.botId === payload.botId)?.connected !== true) {
|
|
128
|
+
const unavailable = new Error('Bot is not connected');
|
|
129
|
+
unavailable.code = 'test-target-unavailable';
|
|
130
|
+
throw unavailable;
|
|
131
|
+
}
|
|
132
|
+
if (typeof controller.sendConnectionTest !== 'function') {
|
|
133
|
+
const unavailable = new Error('Connection test is unavailable');
|
|
134
|
+
unavailable.code = 'test-target-unavailable';
|
|
135
|
+
throw unavailable;
|
|
136
|
+
}
|
|
137
|
+
await controller.sendConnectionTest(payload.botId);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
testError = error;
|
|
140
|
+
}
|
|
141
|
+
value = { ...value, testMessage: publicConnectionTestResult(testError) };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
118
144
|
else if (endpoint === SLACK_ENDPOINTS.setWorkspace) {
|
|
119
145
|
if (typeof controller.updateWorkspace !== 'function') throw new Error('Workspace update is unavailable');
|
|
120
146
|
value = await controller.updateWorkspace(payload.botId, payload.workspace);
|
|
@@ -78,7 +78,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
78
78
|
const harness = new Harness({
|
|
79
79
|
baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
|
|
80
80
|
workspace: defaultWorkspace,
|
|
81
|
-
agentPreset: config.agentPreset
|
|
81
|
+
...(config.agentPreset == null ? {} : { agentPreset: config.agentPreset }),
|
|
82
82
|
autostart: false,
|
|
83
83
|
dshBin: config.dshBin ?? 'dsh',
|
|
84
84
|
...(commandExecutor ? { commandExecutor } : {}),
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import QRCode from 'qrcode';
|
|
2
2
|
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
|
3
3
|
import { publicWorkspaceError, SET_WORKSPACE_ENDPOINT, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
|
|
4
|
+
import {
|
|
5
|
+
connectionTestTargetUnavailable,
|
|
6
|
+
publicConnectionTestResult,
|
|
7
|
+
} from '../../../../src/channels/shared/connection-test.mjs';
|
|
4
8
|
|
|
5
9
|
export const WECOM_RPC_CHANNEL = '/wecom';
|
|
6
10
|
export const WECOM_ENDPOINTS = Object.freeze({
|
|
@@ -53,7 +57,10 @@ function payloadFailure(endpoint, payload) {
|
|
|
53
57
|
? null : 'bot.bind-credentials requires Bot ID and Secret.';
|
|
54
58
|
}
|
|
55
59
|
if (endpoint === WECOM_ENDPOINTS.reconnectBot) {
|
|
56
|
-
return exactKeys(payload, ['botId'
|
|
60
|
+
return exactKeys(payload, ['botId', 'sendTest'])
|
|
61
|
+
&& validId(payload.botId)
|
|
62
|
+
&& (payload.sendTest === undefined || payload.sendTest === true)
|
|
63
|
+
? null : 'bot.reconnect requires a botId and optional sendTest=true.';
|
|
57
64
|
}
|
|
58
65
|
if (endpoint === WECOM_ENDPOINTS.deleteBot) {
|
|
59
66
|
return exactKeys(payload, ['botId', 'confirm']) && validId(payload.botId) && payload.confirm === true
|
|
@@ -130,7 +137,29 @@ export function createWecomRpcHandler(controller, { encodeQr = qrDataUrl } = {})
|
|
|
130
137
|
} else if (endpoint === WECOM_ENDPOINTS.bindCredentials) {
|
|
131
138
|
value = await publicStatus(await controller.bindCredentials(payload), cachedEncode);
|
|
132
139
|
} else if (endpoint === WECOM_ENDPOINTS.reconnectBot) {
|
|
133
|
-
|
|
140
|
+
const snapshot = await controller.reconnectBot(payload.botId);
|
|
141
|
+
if (signal?.aborted) {
|
|
142
|
+
return { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } };
|
|
143
|
+
}
|
|
144
|
+
let testMessage;
|
|
145
|
+
if (payload.sendTest === true) {
|
|
146
|
+
const connected = snapshot?.bots?.some(
|
|
147
|
+
(bot) => bot?.botId === payload.botId && bot?.connected === true,
|
|
148
|
+
);
|
|
149
|
+
if (!connected || typeof controller.sendConnectionTest !== 'function') {
|
|
150
|
+
testMessage = publicConnectionTestResult(
|
|
151
|
+
connectionTestTargetUnavailable('企业微信机器人'),
|
|
152
|
+
);
|
|
153
|
+
} else {
|
|
154
|
+
try {
|
|
155
|
+
await controller.sendConnectionTest(payload.botId);
|
|
156
|
+
testMessage = publicConnectionTestResult();
|
|
157
|
+
} catch (error) {
|
|
158
|
+
testMessage = publicConnectionTestResult(error);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
value = await publicStatus({ ...snapshot, ...(testMessage ? { testMessage } : {}) }, cachedEncode);
|
|
134
163
|
} else if (endpoint === WECOM_ENDPOINTS.setWorkspace) {
|
|
135
164
|
if (typeof controller.updateWorkspace !== 'function') throw new Error('Workspace update is unavailable');
|
|
136
165
|
value = await publicStatus(
|
|
@@ -78,7 +78,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
78
78
|
const harness = new Harness({
|
|
79
79
|
baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
|
|
80
80
|
workspace: defaultWorkspace,
|
|
81
|
-
agentPreset: config.agentPreset
|
|
81
|
+
...(config.agentPreset == null ? {} : { agentPreset: config.agentPreset }),
|
|
82
82
|
autostart: false,
|
|
83
83
|
dshBin: config.dshBin ?? 'dsh',
|
|
84
84
|
...(commandExecutor ? { commandExecutor } : {}),
|
|
@@ -5,6 +5,10 @@ import {
|
|
|
5
5
|
SET_WORKSPACE_ENDPOINT,
|
|
6
6
|
validWorkspacePayload,
|
|
7
7
|
} from '../shared/workspace-rpc.mjs';
|
|
8
|
+
import {
|
|
9
|
+
connectionTestTargetUnavailable,
|
|
10
|
+
publicConnectionTestResult,
|
|
11
|
+
} from '../../../../src/channels/shared/connection-test.mjs';
|
|
8
12
|
|
|
9
13
|
export const WEIXIN_RPC_CHANNEL = '/weixin';
|
|
10
14
|
export const WEIXIN_ENDPOINTS = Object.freeze({
|
|
@@ -55,9 +59,11 @@ function payloadFailure(endpoint, payload) {
|
|
|
55
59
|
: 'provision.verify requires an attemptId and a 4-to-8-digit code.';
|
|
56
60
|
}
|
|
57
61
|
if (endpoint === WEIXIN_ENDPOINTS.reconnectBot) {
|
|
58
|
-
return exactKeys(payload, ['botId'])
|
|
62
|
+
return exactKeys(payload, ['botId', 'sendTest'])
|
|
63
|
+
&& validId(payload.botId)
|
|
64
|
+
&& (payload.sendTest === undefined || payload.sendTest === true)
|
|
59
65
|
? null
|
|
60
|
-
: 'bot.reconnect requires a botId.';
|
|
66
|
+
: 'bot.reconnect requires a botId and optional sendTest=true.';
|
|
61
67
|
}
|
|
62
68
|
if (endpoint === WEIXIN_ENDPOINTS.deleteBot) {
|
|
63
69
|
return exactKeys(payload, ['botId', 'confirm']) && validId(payload.botId) && payload.confirm === true
|
|
@@ -165,7 +171,27 @@ export function createWeixinRpcHandler(controller, { encodeQr = qrDataUrl } = {}
|
|
|
165
171
|
value = await controller.cancelProvisioning(payload.attemptId);
|
|
166
172
|
if (!value) return badRequest('The provisioning attempt no longer exists.');
|
|
167
173
|
} else if (endpoint === WEIXIN_ENDPOINTS.reconnectBot) {
|
|
168
|
-
|
|
174
|
+
const snapshot = await controller.reconnectBot(payload.botId);
|
|
175
|
+
if (signal?.aborted) return cancelled();
|
|
176
|
+
let testMessage;
|
|
177
|
+
if (payload.sendTest === true) {
|
|
178
|
+
const connected = snapshot?.bots?.some(
|
|
179
|
+
(bot) => bot?.botId === payload.botId && bot?.connected === true,
|
|
180
|
+
);
|
|
181
|
+
if (!connected || typeof controller.sendConnectionTest !== 'function') {
|
|
182
|
+
testMessage = publicConnectionTestResult(
|
|
183
|
+
connectionTestTargetUnavailable('微信机器人'),
|
|
184
|
+
);
|
|
185
|
+
} else {
|
|
186
|
+
try {
|
|
187
|
+
await controller.sendConnectionTest(payload.botId);
|
|
188
|
+
testMessage = publicConnectionTestResult();
|
|
189
|
+
} catch (error) {
|
|
190
|
+
testMessage = publicConnectionTestResult(error);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
value = await publicStatus({ ...snapshot, ...(testMessage ? { testMessage } : {}) }, cachedEncode);
|
|
169
195
|
} else if (endpoint === WEIXIN_ENDPOINTS.setWorkspace) {
|
|
170
196
|
if (typeof controller.updateWorkspace !== 'function') throw new Error('Workspace update is unavailable');
|
|
171
197
|
value = await publicStatus(
|
|
@@ -81,7 +81,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
81
81
|
const harness = new Harness({
|
|
82
82
|
baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
|
|
83
83
|
workspace: defaultWorkspace,
|
|
84
|
-
agentPreset: config.agentPreset
|
|
84
|
+
...(config.agentPreset == null ? {} : { agentPreset: config.agentPreset }),
|
|
85
85
|
autostart: false,
|
|
86
86
|
dshBin: config.dshBin ?? 'dsh',
|
|
87
87
|
...(commandExecutor ? { commandExecutor } : {}),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import QRCode from 'qrcode';
|
|
2
2
|
|
|
3
|
+
import { publicConnectionTestResult } from '../../../../src/channels/shared/connection-test.mjs';
|
|
3
4
|
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
|
4
5
|
import { publicWorkspaceError, SET_WORKSPACE_ENDPOINT, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
|
|
5
6
|
|
|
@@ -39,8 +40,9 @@ function payloadFailure(endpoint, payload) {
|
|
|
39
40
|
? null : `${endpoint} requires an attemptId.`;
|
|
40
41
|
}
|
|
41
42
|
if (endpoint === WHATSAPP_ENDPOINTS.reconnectBot) {
|
|
42
|
-
return exactKeys(payload, ['botId']) && validId(payload.botId)
|
|
43
|
-
|
|
43
|
+
return exactKeys(payload, ['botId', 'sendTest']) && validId(payload.botId)
|
|
44
|
+
&& (payload.sendTest === undefined || typeof payload.sendTest === 'boolean')
|
|
45
|
+
? null : 'bot.reconnect requires a botId and optional sendTest flag.';
|
|
44
46
|
}
|
|
45
47
|
if (endpoint === WHATSAPP_ENDPOINTS.deleteBot) {
|
|
46
48
|
return exactKeys(payload, ['botId', 'confirm']) && validId(payload.botId)
|
|
@@ -119,7 +121,33 @@ export function createWhatsappRpcHandler(controller, { encodeQr = qrDataUrl } =
|
|
|
119
121
|
} else if (endpoint === WHATSAPP_ENDPOINTS.cancelProvisioning) {
|
|
120
122
|
value = sanitizePublic(await controller.cancelProvisioning(payload.attemptId));
|
|
121
123
|
} else if (endpoint === WHATSAPP_ENDPOINTS.reconnectBot) {
|
|
122
|
-
|
|
124
|
+
const checked = await controller.reconnectBot(payload.botId);
|
|
125
|
+
if (signal?.aborted) {
|
|
126
|
+
return { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } };
|
|
127
|
+
}
|
|
128
|
+
value = await publicStatus(checked, cachedEncode);
|
|
129
|
+
if (payload.sendTest === true) {
|
|
130
|
+
let testError = null;
|
|
131
|
+
const connected = checked?.bots?.some(
|
|
132
|
+
(bot) => bot?.botId === payload.botId && bot.connected === true,
|
|
133
|
+
) === true;
|
|
134
|
+
if (!connected) {
|
|
135
|
+
testError = new Error('WhatsApp bot is not connected');
|
|
136
|
+
testError.code = 'test-target-unavailable';
|
|
137
|
+
} else {
|
|
138
|
+
try {
|
|
139
|
+
if (typeof controller.sendConnectionTest !== 'function') {
|
|
140
|
+
const unavailable = new Error('Connection test is unavailable');
|
|
141
|
+
unavailable.code = 'test-target-unavailable';
|
|
142
|
+
throw unavailable;
|
|
143
|
+
}
|
|
144
|
+
await controller.sendConnectionTest(payload.botId);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
testError = error;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
value = { ...value, testMessage: publicConnectionTestResult(testError) };
|
|
150
|
+
}
|
|
123
151
|
} else if (endpoint === WHATSAPP_ENDPOINTS.setWorkspace) {
|
|
124
152
|
if (typeof controller.updateWorkspace !== 'function') throw new Error('Workspace update is unavailable');
|
|
125
153
|
value = await publicStatus(
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
imagePromptUserMessage,
|
|
18
18
|
promptContentForMessage,
|
|
19
19
|
} from '../shared/image-prompt.mjs';
|
|
20
|
+
import { rememberConnectionTestTarget } from '../shared/connection-test.mjs';
|
|
20
21
|
|
|
21
22
|
const CARD_INITIAL_TEXT = '已连接 DeepSeek Harness,正在思考…';
|
|
22
23
|
const CARD_ERROR_TEXT = '消息处理失败,请稍后重试。';
|
|
@@ -309,6 +310,9 @@ export class DingtalkHarnessBridge {
|
|
|
309
310
|
} catch {
|
|
310
311
|
// An unsafe reply route must never be able to submit an approval.
|
|
311
312
|
}
|
|
313
|
+
if (sessionWebhook && String(message.conversationType) !== '2') {
|
|
314
|
+
rememberConnectionTestTarget(this.#state, { sessionWebhook });
|
|
315
|
+
}
|
|
312
316
|
const pending = this.#pendingInteractions.get(key);
|
|
313
317
|
const approvalReply = this.#approvals.claimReply({
|
|
314
318
|
key,
|
|
@@ -6,6 +6,10 @@ import {
|
|
|
6
6
|
maskDingtalkClientId,
|
|
7
7
|
maskDingtalkSenderId,
|
|
8
8
|
} 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(['starting', 'pending', 'connecting']);
|
|
11
15
|
const TERMINAL_ATTEMPT_STATES = new Set(['connected', 'expired', 'failed', 'cancelled']);
|
|
@@ -386,6 +390,20 @@ export class DingtalkController {
|
|
|
386
390
|
return this.status();
|
|
387
391
|
}
|
|
388
392
|
|
|
393
|
+
async sendConnectionTest(botId) {
|
|
394
|
+
const config = this.#configStore.get(botId);
|
|
395
|
+
if (!config) throw new Error('Unknown DingTalk bot');
|
|
396
|
+
return this.#withBotTransition(botId, async () => {
|
|
397
|
+
const runtime = this.#runtimes.get(botId);
|
|
398
|
+
if (!runtime?.status?.ready || typeof runtime.sendConnectionTest !== 'function') {
|
|
399
|
+
throw connectionTestTargetUnavailable('钉钉机器人');
|
|
400
|
+
}
|
|
401
|
+
return runtime.sendConnectionTest(connectionTestMessage(
|
|
402
|
+
`钉钉机器人(${maskDingtalkClientId(config.clientId)})`,
|
|
403
|
+
));
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
389
407
|
/** Removes one bot, its secret, runtime, and local conversation state. */
|
|
390
408
|
async deleteBot(botId) {
|
|
391
409
|
const config = this.#configStore.get(botId);
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
createDingtalkBridgeStatus,
|
|
4
4
|
DingtalkHarnessBridge,
|
|
5
5
|
} from './dingtalk-bridge.mjs';
|
|
6
|
+
import { sendRememberedConnectionTest } from '../shared/connection-test.mjs';
|
|
6
7
|
|
|
7
8
|
function nonEmptyString(value) {
|
|
8
9
|
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
@@ -347,6 +348,26 @@ export class DingtalkRuntime {
|
|
|
347
348
|
return this.status;
|
|
348
349
|
}
|
|
349
350
|
|
|
351
|
+
async sendConnectionTest(text) {
|
|
352
|
+
return sendRememberedConnectionTest({
|
|
353
|
+
state: this.#state,
|
|
354
|
+
text,
|
|
355
|
+
channelLabel: '钉钉机器人',
|
|
356
|
+
send: async ({ sessionWebhook }, content) => {
|
|
357
|
+
if (!this.#status.ready || !this.#abortController) {
|
|
358
|
+
throw new Error('DingTalk runtime is not connected');
|
|
359
|
+
}
|
|
360
|
+
await this.#api.sendText({
|
|
361
|
+
clientId: this.#config.clientId,
|
|
362
|
+
clientSecret: this.#clientSecret,
|
|
363
|
+
sessionWebhook,
|
|
364
|
+
text: content,
|
|
365
|
+
signal: this.#abortController.signal,
|
|
366
|
+
});
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
350
371
|
#pendingSenders() {
|
|
351
372
|
return typeof this.#state.pendingSenders === 'function'
|
|
352
373
|
? this.#state.pendingSenders()
|
|
@@ -108,7 +108,7 @@ export class DiscordApi {
|
|
|
108
108
|
headers: {
|
|
109
109
|
authorization: `Bot ${this.#token}`,
|
|
110
110
|
'content-type': 'application/json',
|
|
111
|
-
'user-agent': 'DeepSeek-Harness-dsh-im (https://github.com/xmanrui/dsh-im, 0.
|
|
111
|
+
'user-agent': 'DeepSeek-Harness-dsh-im (https://github.com/xmanrui/dsh-im, 0.11.0)',
|
|
112
112
|
},
|
|
113
113
|
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
114
114
|
signal: requestSignal(signal, timeoutMs),
|
|
@@ -108,6 +108,7 @@ export function normalizeDiscordMessage(message, botId, { fetchImpl = fetch } =
|
|
|
108
108
|
channelId: String(message.channel_id),
|
|
109
109
|
replyToMessageId: String(message.id),
|
|
110
110
|
},
|
|
111
|
+
connectionTestTarget: { channelId: String(message.channel_id) },
|
|
111
112
|
};
|
|
112
113
|
}
|
|
113
114
|
|
|
@@ -239,6 +240,15 @@ export class DiscordRuntime {
|
|
|
239
240
|
return structuredClone(this.#status);
|
|
240
241
|
}
|
|
241
242
|
|
|
243
|
+
async sendConnectionTest(text) {
|
|
244
|
+
if (!this.#status.ready || !this.#bridge) {
|
|
245
|
+
const error = new Error('Discord bot is not connected');
|
|
246
|
+
error.code = 'test-target-unavailable';
|
|
247
|
+
throw error;
|
|
248
|
+
}
|
|
249
|
+
return this.#bridge.sendConnectionTest(text);
|
|
250
|
+
}
|
|
251
|
+
|
|
242
252
|
async start() {
|
|
243
253
|
if (this.#status.ready && this.#socket) return this.status;
|
|
244
254
|
if (this.#starting) return this.#starting;
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from '../shared/harness-question.mjs';
|
|
19
19
|
import { HarnessApprovalQueue } from '../shared/harness-approval.mjs';
|
|
20
20
|
import { runCompactCommand } from '../shared/compact-command.mjs';
|
|
21
|
+
import { rememberConnectionTestTarget } from '../shared/connection-test.mjs';
|
|
21
22
|
import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
|
|
22
23
|
import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
|
|
23
24
|
|
|
@@ -133,6 +134,11 @@ export class FeishuHarnessBridge {
|
|
|
133
134
|
return Promise.resolve();
|
|
134
135
|
}
|
|
135
136
|
|
|
137
|
+
if (event.message.chat_type === 'p2p') {
|
|
138
|
+
const chatId = nonEmptyString(event.message.chat_id);
|
|
139
|
+
if (chatId) rememberConnectionTestTarget(this.#state, { chatId });
|
|
140
|
+
}
|
|
141
|
+
|
|
136
142
|
this.#acceptedMessageIds.add(messageId);
|
|
137
143
|
const processingReaction = this.#addReaction(messageId, 'OnIt');
|
|
138
144
|
if (this.#isResolvedQuestionReply(event, key)) {
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { FeishuHarnessBridge } from './bridge.mjs';
|
|
2
2
|
import { VerifiedFeishuChannel } from './feishu-channel.mjs';
|
|
3
|
+
import {
|
|
4
|
+
connectionTestTargetUnavailable,
|
|
5
|
+
sendRememberedConnectionTest,
|
|
6
|
+
} from '../shared/connection-test.mjs';
|
|
3
7
|
|
|
4
8
|
const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
|
|
5
9
|
|
|
@@ -240,6 +244,47 @@ export class FeishuRuntime {
|
|
|
240
244
|
}
|
|
241
245
|
}
|
|
242
246
|
|
|
247
|
+
async sendConnectionTest(text) {
|
|
248
|
+
if (!this.#status.ready || !this.#client) {
|
|
249
|
+
const error = new Error('飞书机器人尚未连接');
|
|
250
|
+
error.code = 'test-target-unavailable';
|
|
251
|
+
throw error;
|
|
252
|
+
}
|
|
253
|
+
if (typeof text !== 'string' || !text.trim()) {
|
|
254
|
+
throw new TypeError('Feishu connection test text is required');
|
|
255
|
+
}
|
|
256
|
+
const send = async (receiveIdType, receiveId, content) => {
|
|
257
|
+
const response = await this.#client.im.v1.message.create({
|
|
258
|
+
params: { receive_id_type: receiveIdType },
|
|
259
|
+
data: {
|
|
260
|
+
receive_id: receiveId,
|
|
261
|
+
msg_type: 'text',
|
|
262
|
+
content: JSON.stringify({ text: content }),
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
if (response?.code && response.code !== 0) {
|
|
266
|
+
throw new Error(`Feishu connection test failed: ${response.msg || response.code}`);
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const ownerOpenId = this.#ownerOpenIds.find((value) => value !== '*');
|
|
271
|
+
if (ownerOpenId) {
|
|
272
|
+
await send('open_id', ownerOpenId, text);
|
|
273
|
+
return { sent: true };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return sendRememberedConnectionTest({
|
|
277
|
+
state: this.#state,
|
|
278
|
+
text,
|
|
279
|
+
channelLabel: '飞书机器人',
|
|
280
|
+
send: async (target, content) => {
|
|
281
|
+
const chatId = typeof target?.chatId === 'string' ? target.chatId.trim() : '';
|
|
282
|
+
if (!chatId) throw connectionTestTargetUnavailable('飞书机器人');
|
|
283
|
+
await send('chat_id', chatId, content);
|
|
284
|
+
},
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
243
288
|
async stop({ preserveError = false } = {}) {
|
|
244
289
|
const error = preserveError ? this.#status.lastError : null;
|
|
245
290
|
const abortController = this.#abortController;
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { ImagePromptError } from '../shared/image-prompt.mjs';
|
|
2
2
|
|
|
3
|
+
const FEISHU_MISSING_MESSAGE_SCOPE_CODE = 99991672;
|
|
4
|
+
const FEISHU_ERROR_BODY_LIMIT = 64 * 1024;
|
|
5
|
+
const FEISHU_ERROR_BODY_TIMEOUT_MS = 1_000;
|
|
6
|
+
const FEISHU_IMAGE_PERMISSION_MESSAGE =
|
|
7
|
+
'飞书机器人缺少图片读取权限。请在飞书开放平台为该应用添加 im:message:readonly,发布新版本并完成必要的管理员审批后,再重新发送图片。';
|
|
8
|
+
|
|
3
9
|
export function conversationKey(event) {
|
|
4
10
|
const chatType = event?.message?.chat_type;
|
|
5
11
|
if (chatType === 'p2p') {
|
|
@@ -119,17 +125,98 @@ async function readBoundedStream(stream, { signal, maxBytes }) {
|
|
|
119
125
|
}
|
|
120
126
|
}
|
|
121
127
|
|
|
128
|
+
function providerCode(value) {
|
|
129
|
+
if (!value || typeof value !== 'object') return null;
|
|
130
|
+
const code = value.code ?? value.error?.code;
|
|
131
|
+
return Number.isSafeInteger(Number(code)) ? Number(code) : null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function readFeishuErrorBody(stream, signal) {
|
|
135
|
+
if (!stream || typeof stream[Symbol.asyncIterator] !== 'function') return null;
|
|
136
|
+
signal?.throwIfAborted();
|
|
137
|
+
const timeout = AbortSignal.timeout(FEISHU_ERROR_BODY_TIMEOUT_MS);
|
|
138
|
+
const readSignal = signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
139
|
+
const abort = () => stream.destroy?.(readSignal.reason);
|
|
140
|
+
readSignal.addEventListener('abort', abort, { once: true });
|
|
141
|
+
const chunks = [];
|
|
142
|
+
let size = 0;
|
|
143
|
+
try {
|
|
144
|
+
for await (const chunk of stream) {
|
|
145
|
+
const data = Buffer.from(chunk);
|
|
146
|
+
size += data.length;
|
|
147
|
+
if (size > FEISHU_ERROR_BODY_LIMIT) {
|
|
148
|
+
stream.destroy?.();
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
chunks.push(data);
|
|
152
|
+
}
|
|
153
|
+
return Buffer.concat(chunks, size).toString('utf8');
|
|
154
|
+
} catch {
|
|
155
|
+
signal?.throwIfAborted();
|
|
156
|
+
return null;
|
|
157
|
+
} finally {
|
|
158
|
+
readSignal.removeEventListener('abort', abort);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function feishuProviderCode(error, signal) {
|
|
163
|
+
const pending = [error];
|
|
164
|
+
const seen = new Set();
|
|
165
|
+
while (pending.length > 0 && seen.size < 8) {
|
|
166
|
+
const value = pending.shift();
|
|
167
|
+
if (!value || (typeof value !== 'object' && typeof value !== 'function') || seen.has(value)) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
seen.add(value);
|
|
171
|
+
const directCode = providerCode(value);
|
|
172
|
+
const data = value.response?.data ?? value.data;
|
|
173
|
+
if (directCode === FEISHU_MISSING_MESSAGE_SCOPE_CODE) {
|
|
174
|
+
data?.destroy?.();
|
|
175
|
+
return directCode;
|
|
176
|
+
}
|
|
177
|
+
if (data && typeof data[Symbol.asyncIterator] === 'function') {
|
|
178
|
+
const body = await readFeishuErrorBody(data, signal);
|
|
179
|
+
try {
|
|
180
|
+
const parsedCode = providerCode(JSON.parse(body));
|
|
181
|
+
if (parsedCode === FEISHU_MISSING_MESSAGE_SCOPE_CODE) return parsedCode;
|
|
182
|
+
} catch {
|
|
183
|
+
// Non-JSON provider failures keep the generic image download message.
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
const dataCode = providerCode(data);
|
|
187
|
+
if (dataCode === FEISHU_MISSING_MESSAGE_SCOPE_CODE) return dataCode;
|
|
188
|
+
}
|
|
189
|
+
pending.push(value.cause);
|
|
190
|
+
}
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function feishuImageDownloadError(error, signal) {
|
|
195
|
+
if (await feishuProviderCode(error, signal) !== FEISHU_MISSING_MESSAGE_SCOPE_CODE) return error;
|
|
196
|
+
return new ImagePromptError(
|
|
197
|
+
'feishu-image-permission-required',
|
|
198
|
+
'Feishu image download requires the im:message:readonly tenant scope',
|
|
199
|
+
FEISHU_IMAGE_PERMISSION_MESSAGE,
|
|
200
|
+
{ cause: error },
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
122
204
|
function feishuImageSource(event, client, key) {
|
|
123
205
|
return {
|
|
124
206
|
async load({ signal, maxBytes }) {
|
|
125
207
|
signal?.throwIfAborted();
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
208
|
+
let resource;
|
|
209
|
+
try {
|
|
210
|
+
resource = await client?.im?.v1?.messageResource?.get?.({
|
|
211
|
+
path: {
|
|
212
|
+
message_id: event.message.message_id,
|
|
213
|
+
file_key: key,
|
|
214
|
+
},
|
|
215
|
+
params: { type: 'image' },
|
|
216
|
+
});
|
|
217
|
+
} catch (error) {
|
|
218
|
+
throw await feishuImageDownloadError(error, signal);
|
|
219
|
+
}
|
|
133
220
|
signal?.throwIfAborted();
|
|
134
221
|
const size = declaredSize(resource?.headers);
|
|
135
222
|
if (size !== null && size > maxBytes) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { connectionTestMessage } from '../shared/connection-test.mjs';
|
|
2
3
|
import { RegistrationManager } from './registration-manager.mjs';
|
|
3
4
|
import { REQUIRED_TENANT_SCOPES } from './plugin-controller.mjs';
|
|
4
5
|
|
|
@@ -339,6 +340,26 @@ export class MultiBotDshFeishuController {
|
|
|
339
340
|
});
|
|
340
341
|
}
|
|
341
342
|
|
|
343
|
+
async sendConnectionTest(botId) {
|
|
344
|
+
this.#assertOpen();
|
|
345
|
+
return this.#withBotTransition(botId, async () => {
|
|
346
|
+
const config = this.#requireBot(botId);
|
|
347
|
+
const runtime = this.#runtimes.get(botId);
|
|
348
|
+
if (!isConnected(connectionStatus(runtime))
|
|
349
|
+
|| typeof runtime.sendConnectionTest !== 'function') {
|
|
350
|
+
const error = new Error('飞书机器人尚未连接');
|
|
351
|
+
error.code = 'test-target-unavailable';
|
|
352
|
+
throw error;
|
|
353
|
+
}
|
|
354
|
+
return runtime.sendConnectionTest(
|
|
355
|
+
connectionTestMessage(
|
|
356
|
+
`${config.botName}(${maskedAppId(config.appId)})`,
|
|
357
|
+
'飞书机器人',
|
|
358
|
+
),
|
|
359
|
+
);
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
342
363
|
async disconnectBot(botId) {
|
|
343
364
|
this.#assertOpen();
|
|
344
365
|
// An operational pause only: credentials/config remain durable, so the
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
|
|
2
2
|
import { runCompactCommand } from '../shared/compact-command.mjs';
|
|
3
|
+
import { rememberConnectionTestTarget } from '../shared/connection-test.mjs';
|
|
3
4
|
import {
|
|
4
5
|
harnessAnswerForQuestion,
|
|
5
6
|
harnessQuestionText,
|
|
@@ -176,6 +177,12 @@ export class QqHarnessBridge {
|
|
|
176
177
|
|| this.#acceptedMessageIds.has(messageId)) return Promise.resolve();
|
|
177
178
|
const key = conversationKey(message);
|
|
178
179
|
this.#acceptedMessageIds.add(messageId);
|
|
180
|
+
if (message.kind === 'c2c'
|
|
181
|
+
&& (this.#ownerUserOpenid === '*' || sender === this.#ownerUserOpenid)
|
|
182
|
+
&& message.replyTarget?.scope === 'c2c'
|
|
183
|
+
&& nonEmptyString(message.replyTarget.targetId) === sender) {
|
|
184
|
+
rememberConnectionTestTarget(this.#state, message.replyTarget);
|
|
185
|
+
}
|
|
179
186
|
const pending = this.#pendingInteractions.get(key);
|
|
180
187
|
const approval = this.#approvals.claimReply({
|
|
181
188
|
key,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
|
|
3
|
+
import { connectionTestMessage } from '../shared/connection-test.mjs';
|
|
3
4
|
import { deriveQqBotIdentity, maskQqAppId } from './config-store.mjs';
|
|
4
5
|
|
|
5
6
|
const ACTIVE_ATTEMPT_STATES = new Set(['starting', 'pending', 'refreshing', 'connecting']);
|
|
@@ -248,6 +249,23 @@ export class QqController {
|
|
|
248
249
|
return this.status();
|
|
249
250
|
}
|
|
250
251
|
|
|
252
|
+
async sendConnectionTest(botId) {
|
|
253
|
+
const config = this.#configStore.get(botId);
|
|
254
|
+
if (!config) throw new Error('Unknown QQ bot');
|
|
255
|
+
return this.#withBotTransition(botId, async () => {
|
|
256
|
+
const runtime = this.#runtimes.get(botId);
|
|
257
|
+
if (!runtime?.status?.ready || typeof runtime.sendConnectionTest !== 'function') {
|
|
258
|
+
const error = new Error('QQ机器人尚未连接');
|
|
259
|
+
error.code = 'test-target-unavailable';
|
|
260
|
+
throw error;
|
|
261
|
+
}
|
|
262
|
+
await runtime.sendConnectionTest(connectionTestMessage(
|
|
263
|
+
`QQ 机器人(${maskQqAppId(config.appId)})`,
|
|
264
|
+
));
|
|
265
|
+
return { sent: true };
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
251
269
|
async deleteBot(botId) {
|
|
252
270
|
const config = this.#configStore.get(botId);
|
|
253
271
|
if (!config) throw new Error('Unknown QQ bot');
|