mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-10 18:03:18 +08:00
feat: send test messages during bot connection checks
This commit is contained in:
parent
94da24538f
commit
fb61c4235f
78 changed files with 3460 additions and 210 deletions
|
|
@ -17,6 +17,7 @@ import {
|
|||
imagePromptUserMessage,
|
||||
promptContentForMessage,
|
||||
} from '../shared/image-prompt.mjs';
|
||||
import { rememberConnectionTestTarget } from '../shared/connection-test.mjs';
|
||||
|
||||
const CARD_INITIAL_TEXT = '已连接 DeepSeek Harness,正在思考…';
|
||||
const CARD_ERROR_TEXT = '消息处理失败,请稍后重试。';
|
||||
|
|
@ -459,6 +460,9 @@ export class DingtalkHarnessBridge {
|
|||
if (isPlainText && !hasImages && command === '/status') {
|
||||
await this.#harness.ensureRunning({ signal: this.#signal });
|
||||
await this.#send(sessionWebhook, '钉钉机器人与 DeepSeek Harness 连接正常。');
|
||||
if (String(message.conversationType) !== '2') {
|
||||
rememberConnectionTestTarget(this.#state, { sessionWebhook });
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (isPlainText && !hasImages && command === '/new') {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,10 @@ import {
|
|||
maskDingtalkClientId,
|
||||
maskDingtalkSenderId,
|
||||
} from './config-store.mjs';
|
||||
import {
|
||||
connectionTestMessage,
|
||||
connectionTestTargetUnavailable,
|
||||
} from '../shared/connection-test.mjs';
|
||||
|
||||
const ACTIVE_ATTEMPT_STATES = new Set(['starting', 'pending', 'connecting']);
|
||||
const TERMINAL_ATTEMPT_STATES = new Set(['connected', 'expired', 'failed', 'cancelled']);
|
||||
|
|
@ -386,6 +390,20 @@ export class DingtalkController {
|
|||
return this.status();
|
||||
}
|
||||
|
||||
async sendConnectionTest(botId) {
|
||||
const config = this.#configStore.get(botId);
|
||||
if (!config) throw new Error('Unknown DingTalk bot');
|
||||
return this.#withBotTransition(botId, async () => {
|
||||
const runtime = this.#runtimes.get(botId);
|
||||
if (!runtime?.status?.ready || typeof runtime.sendConnectionTest !== 'function') {
|
||||
throw connectionTestTargetUnavailable('钉钉机器人');
|
||||
}
|
||||
return runtime.sendConnectionTest(connectionTestMessage(
|
||||
`钉钉机器人(${maskDingtalkClientId(config.clientId)})`,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
/** Removes one bot, its secret, runtime, and local conversation state. */
|
||||
async deleteBot(botId) {
|
||||
const config = this.#configStore.get(botId);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import {
|
|||
createDingtalkBridgeStatus,
|
||||
DingtalkHarnessBridge,
|
||||
} from './dingtalk-bridge.mjs';
|
||||
import { sendRememberedConnectionTest } from '../shared/connection-test.mjs';
|
||||
|
||||
function nonEmptyString(value) {
|
||||
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
||||
|
|
@ -347,6 +348,26 @@ export class DingtalkRuntime {
|
|||
return this.status;
|
||||
}
|
||||
|
||||
async sendConnectionTest(text) {
|
||||
return sendRememberedConnectionTest({
|
||||
state: this.#state,
|
||||
text,
|
||||
channelLabel: '钉钉机器人',
|
||||
send: async ({ sessionWebhook }, content) => {
|
||||
if (!this.#status.ready || !this.#abortController) {
|
||||
throw new Error('DingTalk runtime is not connected');
|
||||
}
|
||||
await this.#api.sendText({
|
||||
clientId: this.#config.clientId,
|
||||
clientSecret: this.#clientSecret,
|
||||
sessionWebhook,
|
||||
text: content,
|
||||
signal: this.#abortController.signal,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
#pendingSenders() {
|
||||
return typeof this.#state.pendingSenders === 'function'
|
||||
? this.#state.pendingSenders()
|
||||
|
|
|
|||
|
|
@ -108,6 +108,7 @@ export function normalizeDiscordMessage(message, botId, { fetchImpl = fetch } =
|
|||
channelId: String(message.channel_id),
|
||||
replyToMessageId: String(message.id),
|
||||
},
|
||||
connectionTestTarget: { channelId: String(message.channel_id) },
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -239,6 +240,15 @@ export class DiscordRuntime {
|
|||
return structuredClone(this.#status);
|
||||
}
|
||||
|
||||
async sendConnectionTest(text) {
|
||||
if (!this.#status.ready || !this.#bridge) {
|
||||
const error = new Error('Discord bot is not connected');
|
||||
error.code = 'test-target-unavailable';
|
||||
throw error;
|
||||
}
|
||||
return this.#bridge.sendConnectionTest(text);
|
||||
}
|
||||
|
||||
async start() {
|
||||
if (this.#status.ready && this.#socket) return this.status;
|
||||
if (this.#starting) return this.#starting;
|
||||
|
|
|
|||
|
|
@ -240,6 +240,35 @@ export class FeishuRuntime {
|
|||
}
|
||||
}
|
||||
|
||||
async sendConnectionTest(text) {
|
||||
if (!this.#status.ready || !this.#client) {
|
||||
const error = new Error('飞书机器人尚未连接');
|
||||
error.code = 'test-target-unavailable';
|
||||
throw error;
|
||||
}
|
||||
const ownerOpenId = this.#ownerOpenIds[0];
|
||||
if (!ownerOpenId || ownerOpenId === '*') {
|
||||
const error = new Error('飞书机器人没有可用的测试消息接收人');
|
||||
error.code = 'test-target-unavailable';
|
||||
throw error;
|
||||
}
|
||||
if (typeof text !== 'string' || !text.trim()) {
|
||||
throw new TypeError('Feishu connection test text is required');
|
||||
}
|
||||
const response = await this.#client.im.v1.message.create({
|
||||
params: { receive_id_type: 'open_id' },
|
||||
data: {
|
||||
receive_id: ownerOpenId,
|
||||
msg_type: 'text',
|
||||
content: JSON.stringify({ text }),
|
||||
},
|
||||
});
|
||||
if (response?.code && response.code !== 0) {
|
||||
throw new Error(`Feishu connection test failed: ${response.msg || response.code}`);
|
||||
}
|
||||
return { sent: true };
|
||||
}
|
||||
|
||||
async stop({ preserveError = false } = {}) {
|
||||
const error = preserveError ? this.#status.lastError : null;
|
||||
const abortController = this.#abortController;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { randomUUID } from 'node:crypto';
|
||||
import { connectionTestMessage } from '../shared/connection-test.mjs';
|
||||
import { RegistrationManager } from './registration-manager.mjs';
|
||||
import { REQUIRED_TENANT_SCOPES } from './plugin-controller.mjs';
|
||||
|
||||
|
|
@ -339,6 +340,26 @@ export class MultiBotDshFeishuController {
|
|||
});
|
||||
}
|
||||
|
||||
async sendConnectionTest(botId) {
|
||||
this.#assertOpen();
|
||||
return this.#withBotTransition(botId, async () => {
|
||||
const config = this.#requireBot(botId);
|
||||
const runtime = this.#runtimes.get(botId);
|
||||
if (!isConnected(connectionStatus(runtime))
|
||||
|| typeof runtime.sendConnectionTest !== 'function') {
|
||||
const error = new Error('飞书机器人尚未连接');
|
||||
error.code = 'test-target-unavailable';
|
||||
throw error;
|
||||
}
|
||||
return runtime.sendConnectionTest(
|
||||
connectionTestMessage(
|
||||
`${config.botName}(${maskedAppId(config.appId)})`,
|
||||
'飞书机器人',
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async disconnectBot(botId) {
|
||||
this.#assertOpen();
|
||||
// An operational pause only: credentials/config remain durable, so the
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
|
||||
import { runCompactCommand } from '../shared/compact-command.mjs';
|
||||
import { rememberConnectionTestTarget } from '../shared/connection-test.mjs';
|
||||
import {
|
||||
harnessAnswerForQuestion,
|
||||
harnessQuestionText,
|
||||
|
|
@ -295,6 +296,11 @@ export class QqHarnessBridge {
|
|||
}
|
||||
if (!hasImages && command === '/status') {
|
||||
await this.#harness.ensureRunning({ signal: this.#signal });
|
||||
if (message.kind === 'c2c'
|
||||
&& target?.scope === 'c2c'
|
||||
&& nonEmptyString(target.targetId) === sender) {
|
||||
rememberConnectionTestTarget(this.#state, target);
|
||||
}
|
||||
await this.#bot.sendText(target, 'QQ 机器人与 DeepSeek Harness 连接正常。');
|
||||
await this.#state.markSeen(messageId);
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import { connectionTestMessage } from '../shared/connection-test.mjs';
|
||||
import { deriveQqBotIdentity, maskQqAppId } from './config-store.mjs';
|
||||
|
||||
const ACTIVE_ATTEMPT_STATES = new Set(['starting', 'pending', 'refreshing', 'connecting']);
|
||||
|
|
@ -248,6 +249,23 @@ export class QqController {
|
|||
return this.status();
|
||||
}
|
||||
|
||||
async sendConnectionTest(botId) {
|
||||
const config = this.#configStore.get(botId);
|
||||
if (!config) throw new Error('Unknown QQ bot');
|
||||
return this.#withBotTransition(botId, async () => {
|
||||
const runtime = this.#runtimes.get(botId);
|
||||
if (!runtime?.status?.ready || typeof runtime.sendConnectionTest !== 'function') {
|
||||
const error = new Error('QQ机器人尚未连接');
|
||||
error.code = 'test-target-unavailable';
|
||||
throw error;
|
||||
}
|
||||
await runtime.sendConnectionTest(connectionTestMessage(
|
||||
`QQ 机器人(${maskQqAppId(config.appId)})`,
|
||||
));
|
||||
return { sent: true };
|
||||
});
|
||||
}
|
||||
|
||||
async deleteBot(botId) {
|
||||
const config = this.#configStore.get(botId);
|
||||
if (!config) throw new Error('Unknown QQ bot');
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
import { QQBot, typingIndicator } from '@tencent-connect/qqbot-nodejs';
|
||||
|
||||
import {
|
||||
connectionTestTarget,
|
||||
connectionTestTargetUnavailable,
|
||||
} from '../shared/connection-test.mjs';
|
||||
import { createQqBridgeStatus, QqHarnessBridge } from './qq-bridge.mjs';
|
||||
|
||||
function timeoutError() {
|
||||
|
|
@ -67,6 +71,28 @@ export class QqRuntime {
|
|||
return structuredClone(this.#status);
|
||||
}
|
||||
|
||||
async sendConnectionTest(text) {
|
||||
if (!this.#status.ready || !this.#bot) {
|
||||
throw connectionTestTargetUnavailable('QQ机器人');
|
||||
}
|
||||
const ownerUserOpenid = typeof this.#config.ownerUserOpenid === 'string'
|
||||
? this.#config.ownerUserOpenid.trim()
|
||||
: '';
|
||||
const remembered = connectionTestTarget(this.#state);
|
||||
const rememberedUserOpenid = remembered?.scope === 'c2c'
|
||||
&& typeof remembered.targetId === 'string'
|
||||
? remembered.targetId.trim()
|
||||
: '';
|
||||
const target = rememberedUserOpenid
|
||||
? { scope: 'c2c', targetId: rememberedUserOpenid }
|
||||
: (ownerUserOpenid && ownerUserOpenid !== '*'
|
||||
? { scope: 'c2c', targetId: ownerUserOpenid }
|
||||
: null);
|
||||
if (!target) throw connectionTestTargetUnavailable('QQ机器人');
|
||||
await this.#bot.sendText(target, text);
|
||||
return { sent: true };
|
||||
}
|
||||
|
||||
async start() {
|
||||
if (this.#status.ready && this.#bot) return this.status;
|
||||
if (this.#starting) return this.#starting;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
} from 'node:fs/promises';
|
||||
import { dirname, isAbsolute, resolve } from 'node:path';
|
||||
|
||||
import { CONNECTION_TEST_STATE_IDENTITY } from './connection-test.mjs';
|
||||
import { WORKSPACE_SESSION_STALE } from './workspace-session.mjs';
|
||||
|
||||
const EMPTY_DOCUMENT = Object.freeze({ version: 1, workspaces: Object.freeze({}) });
|
||||
|
|
@ -687,6 +688,7 @@ export function createBotWorkspaceScope(harness, { botId, workspaces, state }) {
|
|||
});
|
||||
const scopedState = new Proxy(state, {
|
||||
get(target, property) {
|
||||
if (property === CONNECTION_TEST_STATE_IDENTITY) return target;
|
||||
if (property === 'sessionFor') {
|
||||
return (key, ...args) => {
|
||||
if (!isCurrentScope()) return null;
|
||||
|
|
|
|||
54
src/channels/shared/connection-test.mjs
Normal file
54
src/channels/shared/connection-test.mjs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
const targets = new WeakMap();
|
||||
|
||||
export const CONNECTION_TEST_STATE_IDENTITY = Symbol('dsh-im.connection-test-state-identity');
|
||||
|
||||
function stateIdentity(state) {
|
||||
return state?.[CONNECTION_TEST_STATE_IDENTITY] ?? state;
|
||||
}
|
||||
|
||||
function cleanText(value) {
|
||||
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
||||
}
|
||||
|
||||
export function rememberConnectionTestTarget(state, target) {
|
||||
if (!state || !target || typeof target !== 'object') return false;
|
||||
try {
|
||||
targets.set(stateIdentity(state), structuredClone(target));
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function connectionTestTarget(state) {
|
||||
const target = state ? targets.get(stateIdentity(state)) : null;
|
||||
return target ? structuredClone(target) : null;
|
||||
}
|
||||
|
||||
export function connectionTestMessage(botName, channelLabel = '机器人') {
|
||||
const name = cleanText(botName) ?? channelLabel;
|
||||
return `✅ DeepSeek Harness 连接测试成功\n这条消息由插件页面中的“${name}”机器人卡片发出。`;
|
||||
}
|
||||
|
||||
export function connectionTestTargetUnavailable(channelLabel = '机器人') {
|
||||
const error = new Error(`请先私聊${channelLabel}发送 /status,再点击“检查连接”。`);
|
||||
error.code = 'test-target-unavailable';
|
||||
return error;
|
||||
}
|
||||
|
||||
export async function sendRememberedConnectionTest({ state, send, text, channelLabel }) {
|
||||
const target = connectionTestTarget(state);
|
||||
if (!target) throw connectionTestTargetUnavailable(channelLabel);
|
||||
await send(target, text);
|
||||
return { sent: true };
|
||||
}
|
||||
|
||||
export function publicConnectionTestResult(error) {
|
||||
if (!error) return Object.freeze({ sent: true });
|
||||
return Object.freeze({
|
||||
sent: false,
|
||||
code: error?.code === 'test-target-unavailable'
|
||||
? 'test-target-unavailable'
|
||||
: 'test-message-failed',
|
||||
});
|
||||
}
|
||||
|
|
@ -1,5 +1,9 @@
|
|||
import { runWorkspaceCommand } from './workspace-command.mjs';
|
||||
import { runCompactCommand } from './compact-command.mjs';
|
||||
import {
|
||||
rememberConnectionTestTarget,
|
||||
sendRememberedConnectionTest,
|
||||
} from './connection-test.mjs';
|
||||
import { askInWorkspaceSession } from './workspace-session.mjs';
|
||||
import { HarnessApprovalQueue } from './harness-approval.mjs';
|
||||
import {
|
||||
|
|
@ -194,6 +198,15 @@ export class TextHarnessBridge {
|
|||
]);
|
||||
}
|
||||
|
||||
sendConnectionTest(text) {
|
||||
return sendRememberedConnectionTest({
|
||||
state: this.#state,
|
||||
text,
|
||||
channelLabel: `${this.#descriptor.label}机器人`,
|
||||
send: (target, message) => this.#bot.sendText(target, message),
|
||||
});
|
||||
}
|
||||
|
||||
async #process(message, messageId, senderId, conversationKey, {
|
||||
alreadyRecorded = false,
|
||||
} = {}) {
|
||||
|
|
@ -239,6 +252,9 @@ export class TextHarnessBridge {
|
|||
if (!hasImages && command === '/status') {
|
||||
await this.#harness.ensureRunning({ signal: this.#signal });
|
||||
await this.#bot.sendText(target, `${this.#descriptor.label}机器人与 DeepSeek Harness 连接正常。`);
|
||||
if (message.kind === 'direct') {
|
||||
rememberConnectionTestTarget(this.#state, message.connectionTestTarget ?? target);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const workspaceCommand = !hasImages
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { connectionTestMessage } from './connection-test.mjs';
|
||||
|
||||
function cleanString(value) {
|
||||
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
||||
}
|
||||
|
|
@ -161,6 +163,25 @@ export class TokenBotController {
|
|||
return this.status();
|
||||
}
|
||||
|
||||
async sendConnectionTest(botId) {
|
||||
const config = this.#configStore.get(botId);
|
||||
if (!config) throw new Error(`Unknown ${this.#descriptor.label} bot`);
|
||||
return this.#withBotTransition(botId, async () => {
|
||||
const runtime = this.#runtimes.get(botId);
|
||||
if (!runtime?.status?.ready || typeof runtime.sendConnectionTest !== 'function') {
|
||||
const error = new Error(`${this.#descriptor.label}机器人尚未连接`);
|
||||
error.code = 'test-target-unavailable';
|
||||
throw error;
|
||||
}
|
||||
const cardLabel = `${config.name}(${this.#maskPlatformId(config.platformId)})`;
|
||||
await runtime.sendConnectionTest(connectionTestMessage(
|
||||
cardLabel,
|
||||
`${this.#descriptor.label}机器人`,
|
||||
));
|
||||
return { sent: true };
|
||||
});
|
||||
}
|
||||
|
||||
async deleteBot(botId) {
|
||||
const config = this.#configStore.get(botId);
|
||||
if (!config) throw new Error(`Unknown ${this.#descriptor.label} bot`);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { connectionTestMessage } from '../shared/connection-test.mjs';
|
||||
import { deriveSlackBotIdentity, maskSlackBotId } from './config-store.mjs';
|
||||
import { inspectSlackCredentials } from './slack-api.mjs';
|
||||
import { SLACK_DESCRIPTOR } from './slack-bridge.mjs';
|
||||
|
|
@ -168,6 +169,24 @@ export class SlackController {
|
|||
return this.status();
|
||||
}
|
||||
|
||||
async sendConnectionTest(botId) {
|
||||
const config = this.#configStore.get(botId);
|
||||
if (!config) throw new Error('Unknown Slack bot');
|
||||
return this.#withBotTransition(botId, async () => {
|
||||
const runtime = this.#runtimes.get(botId);
|
||||
if (!runtime?.status?.ready || typeof runtime.sendConnectionTest !== 'function') {
|
||||
const error = new Error('Slack机器人尚未连接');
|
||||
error.code = 'test-target-unavailable';
|
||||
throw error;
|
||||
}
|
||||
await runtime.sendConnectionTest(connectionTestMessage(
|
||||
`${config.name}(${maskSlackBotId(config.platformId)})`,
|
||||
'Slack机器人',
|
||||
));
|
||||
return { sent: true };
|
||||
});
|
||||
}
|
||||
|
||||
async deleteBot(botId) {
|
||||
const config = this.#configStore.get(botId);
|
||||
if (!config) throw new Error('Unknown Slack bot');
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ export function normalizeSlackEvent(payload, botUserId, { loadFile = async () =>
|
|||
recipientUserId: String(event.user),
|
||||
recipientTeamId: String(event.user_team ?? payload.team_id ?? ''),
|
||||
},
|
||||
connectionTestTarget: { channelId: String(event.channel) },
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -293,6 +294,15 @@ export class SlackRuntime {
|
|||
return structuredClone(this.#status);
|
||||
}
|
||||
|
||||
async sendConnectionTest(text) {
|
||||
if (!this.#status.ready || !this.#bridge) {
|
||||
const error = new Error('Slack bot is not connected');
|
||||
error.code = 'test-target-unavailable';
|
||||
throw error;
|
||||
}
|
||||
return this.#bridge.sendConnectionTest(text);
|
||||
}
|
||||
|
||||
async start() {
|
||||
if (this.#status.ready && this.#socket) return this.status;
|
||||
if (this.#starting) return this.#starting;
|
||||
|
|
|
|||
|
|
@ -111,6 +111,7 @@ export function normalizeTelegramUpdate(update, { botId, username, loadFile = as
|
|||
replyToMessageId: messageId,
|
||||
messageThreadId,
|
||||
},
|
||||
connectionTestTarget: { chatId, messageThreadId },
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -229,6 +230,15 @@ export class TelegramRuntime {
|
|||
return structuredClone(this.#status);
|
||||
}
|
||||
|
||||
async sendConnectionTest(text) {
|
||||
if (!this.#status.ready || !this.#bridge) {
|
||||
const error = new Error('Telegram bot is not connected');
|
||||
error.code = 'test-target-unavailable';
|
||||
throw error;
|
||||
}
|
||||
return this.#bridge.sendConnectionTest(text);
|
||||
}
|
||||
|
||||
async start() {
|
||||
if (this.#status.ready && this.#pollTask) return this.status;
|
||||
if (this.#starting) return this.#starting;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
imagePromptUserMessage,
|
||||
promptContentForMessage,
|
||||
} from '../shared/image-prompt.mjs';
|
||||
import { rememberConnectionTestTarget } from '../shared/connection-test.mjs';
|
||||
|
||||
const HELP_TEXT = [
|
||||
'企业微信机器人已连接 DeepSeek Harness。',
|
||||
|
|
@ -430,6 +431,9 @@ export class WecomHarnessBridge {
|
|||
if (!hasImages && command === '/status') {
|
||||
await this.#harness.ensureRunning({ signal: this.#signal });
|
||||
await this.#sendImmediate(frame, chatId, '企业微信机器人与 DeepSeek Harness 连接正常。');
|
||||
if (body.chattype === 'single') {
|
||||
rememberConnectionTestTarget(this.#state, { chatId });
|
||||
}
|
||||
await this.#state.markSeen(messageId);
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import { deriveWecomBotIdentity, maskWecomBotId } from './config-store.mjs';
|
||||
import {
|
||||
connectionTestMessage,
|
||||
connectionTestTargetUnavailable,
|
||||
} from '../shared/connection-test.mjs';
|
||||
|
||||
const ACTIVE_ATTEMPT_STATES = new Set(['pending', 'connecting']);
|
||||
const TERMINAL_ATTEMPT_STATES = new Set(['connected', 'failed', 'cancelled', 'expired']);
|
||||
|
|
@ -237,6 +241,20 @@ export class WecomController {
|
|||
return this.status();
|
||||
}
|
||||
|
||||
async sendConnectionTest(botId) {
|
||||
const config = this.#configStore.get(botId);
|
||||
if (!config) throw new Error('Unknown Enterprise WeChat bot');
|
||||
return this.#withBotTransition(botId, async () => {
|
||||
const runtime = this.#runtimes.get(botId);
|
||||
if (!runtime?.status?.ready || typeof runtime.sendConnectionTest !== 'function') {
|
||||
throw connectionTestTargetUnavailable('企业微信机器人');
|
||||
}
|
||||
return runtime.sendConnectionTest(connectionTestMessage(
|
||||
`企业微信机器人(${maskWecomBotId(config.remoteBotId)})`,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
async deleteBot(botId) {
|
||||
const config = this.#configStore.get(botId);
|
||||
if (!config) throw new Error('Unknown Enterprise WeChat bot');
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { WSAuthFailureError, WSClient, WSReconnectExhaustedError } from '@wecom/aibot-node-sdk';
|
||||
|
||||
import { createWecomBridgeStatus, WecomHarnessBridge } from './wecom-bridge.mjs';
|
||||
import { sendRememberedConnectionTest } from '../shared/connection-test.mjs';
|
||||
|
||||
function timeoutError() {
|
||||
const error = new Error('Enterprise WeChat WebSocket authentication timed out');
|
||||
|
|
@ -197,6 +198,23 @@ export class WecomRuntime {
|
|||
return this.status;
|
||||
}
|
||||
|
||||
async sendConnectionTest(text) {
|
||||
return sendRememberedConnectionTest({
|
||||
state: this.#state,
|
||||
text,
|
||||
channelLabel: '企业微信机器人',
|
||||
send: async ({ chatId }, content) => {
|
||||
if (!this.#status.ready || !this.#client) {
|
||||
throw new Error('Enterprise WeChat runtime is not connected');
|
||||
}
|
||||
await this.#client.sendMessage(chatId, {
|
||||
msgtype: 'markdown',
|
||||
markdown: { content },
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async #stopActive() {
|
||||
const client = this.#client;
|
||||
const bridge = this.#bridge;
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {
|
|||
imagePromptUserMessage,
|
||||
promptContentForMessage,
|
||||
} from '../shared/image-prompt.mjs';
|
||||
import { rememberConnectionTestTarget } from '../shared/connection-test.mjs';
|
||||
|
||||
const INTERACTION_RESOLVED_TEXT = '这个问题已在其他客户端处理,无需再次回答。';
|
||||
|
||||
|
|
@ -250,6 +251,7 @@ export class WeixinHarnessBridge {
|
|||
if (!hasImages && command === '/status') {
|
||||
await this.#harness.ensureRunning({ signal: this.#signal });
|
||||
await this.#send(sender, '微信与 DeepSeek Harness 连接正常。', contextToken, runId);
|
||||
rememberConnectionTestTarget(this.#state, { toUserId: sender });
|
||||
await this.#state.markSeen(messageId);
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,10 @@ import {
|
|||
WeixinApiError,
|
||||
} from './weixin-api.mjs';
|
||||
import { deriveWeixinBotIdentity, maskWeixinAccountId } from './config-store.mjs';
|
||||
import {
|
||||
connectionTestMessage,
|
||||
connectionTestTargetUnavailable,
|
||||
} from '../shared/connection-test.mjs';
|
||||
|
||||
const ACTIVE_ATTEMPT_STATES = new Set([
|
||||
'starting',
|
||||
|
|
@ -240,6 +244,20 @@ export class WeixinController {
|
|||
return this.status();
|
||||
}
|
||||
|
||||
async sendConnectionTest(botId) {
|
||||
const config = this.#configStore.get(botId);
|
||||
if (!config) throw new Error('Unknown Weixin account');
|
||||
return this.#withBotTransition(botId, async () => {
|
||||
const runtime = this.#runtimes.get(botId);
|
||||
if (!runtime?.status?.ready || typeof runtime.sendConnectionTest !== 'function') {
|
||||
throw connectionTestTargetUnavailable('微信机器人');
|
||||
}
|
||||
return runtime.sendConnectionTest(connectionTestMessage(
|
||||
`微信机器人(${maskWeixinAccountId(config.accountId)})`,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
async deleteBot(botId) {
|
||||
const config = this.#configStore.get(botId);
|
||||
if (!config) throw new Error('Unknown Weixin account');
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
import { WeixinApiError } from './weixin-api.mjs';
|
||||
import { createWeixinBridgeStatus, WeixinHarnessBridge } from './weixin-bridge.mjs';
|
||||
import {
|
||||
connectionTestTarget,
|
||||
connectionTestTargetUnavailable,
|
||||
} from '../shared/connection-test.mjs';
|
||||
|
||||
const DEFAULT_START_RETRY_DELAYS_MS = Object.freeze([250, 1_000, 3_000]);
|
||||
|
||||
|
|
@ -250,4 +254,25 @@ export class WeixinRuntime {
|
|||
this.#status.weixinConnectionState = 'idle';
|
||||
return this.status;
|
||||
}
|
||||
|
||||
async sendConnectionTest(text) {
|
||||
const remembered = connectionTestTarget(this.#state);
|
||||
const toUserId = typeof remembered?.toUserId === 'string' && remembered.toUserId.trim()
|
||||
? remembered.toUserId.trim()
|
||||
: typeof this.#config.ownerUserId === 'string' && this.#config.ownerUserId.trim()
|
||||
? this.#config.ownerUserId.trim()
|
||||
: null;
|
||||
if (!toUserId) throw connectionTestTargetUnavailable('微信机器人');
|
||||
if (!this.#status.ready || !this.#abortController) {
|
||||
throw new Error('Weixin runtime is not connected');
|
||||
}
|
||||
await this.#api.sendText({
|
||||
baseUrl: this.#config.baseUrl,
|
||||
token: this.#token,
|
||||
toUserId,
|
||||
text,
|
||||
signal: this.#abortController.signal,
|
||||
});
|
||||
return { sent: true };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import { connectionTestMessage } from '../shared/connection-test.mjs';
|
||||
import { deriveWhatsappBotId, maskWhatsappAccount } from './config-store.mjs';
|
||||
|
||||
const ACTIVE_ATTEMPT_STATES = new Set(['starting', 'pending', 'connecting']);
|
||||
|
|
@ -198,6 +199,25 @@ export class WhatsappController {
|
|||
return this.status();
|
||||
}
|
||||
|
||||
async sendConnectionTest(botId) {
|
||||
const config = this.#configStore.get(botId);
|
||||
if (!config) throw new Error('Unknown WhatsApp bot');
|
||||
return this.#withBotTransition(botId, async () => {
|
||||
const runtime = this.#runtimes.get(botId);
|
||||
if (!runtime?.status?.ready || typeof runtime.sendConnectionTest !== 'function') {
|
||||
const error = new Error('WhatsApp机器人尚未连接');
|
||||
error.code = 'test-target-unavailable';
|
||||
throw error;
|
||||
}
|
||||
return runtime.sendConnectionTest(
|
||||
connectionTestMessage(
|
||||
`${config.name}(${maskWhatsappAccount(config.accountJid)})`,
|
||||
'WhatsApp机器人',
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async deleteBot(botId) {
|
||||
const config = this.#configStore.get(botId);
|
||||
if (!config) throw new Error('Unknown WhatsApp bot');
|
||||
|
|
|
|||
|
|
@ -421,6 +421,22 @@ export class WhatsappRuntime {
|
|||
return this.stop();
|
||||
}
|
||||
|
||||
async sendConnectionTest(text) {
|
||||
if (!this.#status.ready || !this.#client) {
|
||||
const error = new Error('WhatsApp机器人尚未连接');
|
||||
error.code = 'test-target-unavailable';
|
||||
throw error;
|
||||
}
|
||||
if (typeof text !== 'string' || !text.trim()) {
|
||||
throw new TypeError('WhatsApp connection test text is required');
|
||||
}
|
||||
await this.#client.sendText({
|
||||
jid: this.#config.accountJid,
|
||||
selfChat: true,
|
||||
}, text);
|
||||
return { sent: true };
|
||||
}
|
||||
|
||||
async stop() {
|
||||
const session = this.#session;
|
||||
const client = this.#client;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue