mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-13 07:50:45 +08:00
feat: add stable proactive delivery
This commit is contained in:
parent
410cb822d7
commit
31792d7bc8
88 changed files with 6427 additions and 488 deletions
|
|
@ -153,6 +153,70 @@ test('session text replies include populated mention targets and omit empty ment
|
|||
for (const body of sent) assert.equal(body.text.content, '回答');
|
||||
});
|
||||
|
||||
test('DingTalk sends proactive text through stable user and group robot endpoints', async () => {
|
||||
const calls = [];
|
||||
const fetchImpl = async (url, options) => {
|
||||
calls.push({ url: url.toString(), options });
|
||||
if (url.pathname.endsWith('/oauth2/accessToken')) {
|
||||
return jsonResponse({ accessToken: 'proactive-access-token', expireIn: 7_200 });
|
||||
}
|
||||
return jsonResponse({ processQueryKey: `query-${calls.length}` });
|
||||
};
|
||||
const api = createDingtalkApi({ fetchImpl });
|
||||
await api.sendRobotText({
|
||||
clientId: 'ding-proactive-client',
|
||||
clientSecret: 'host-only-secret',
|
||||
target: { type: 'user', robotCode: 'ding-proactive-client', userId: 'staff-one' },
|
||||
text: ' 用户主动消息\n',
|
||||
});
|
||||
await api.sendRobotText({
|
||||
clientId: 'ding-proactive-client',
|
||||
clientSecret: 'host-only-secret',
|
||||
target: {
|
||||
type: 'group',
|
||||
robotCode: 'ding-proactive-client',
|
||||
openConversationId: 'cid-one',
|
||||
},
|
||||
text: '群主动消息',
|
||||
});
|
||||
|
||||
assert.equal(calls[1].url, `${DINGTALK_API_BASE_URL}v1.0/robot/oToMessages/batchSend`);
|
||||
assert.equal(calls[2].url, `${DINGTALK_API_BASE_URL}v1.0/robot/groupMessages/send`);
|
||||
assert.equal(calls[1].options.headers['x-acs-dingtalk-access-token'], 'proactive-access-token');
|
||||
assert.equal(calls[2].options.headers['x-acs-dingtalk-access-token'], 'proactive-access-token');
|
||||
assert.deepEqual(JSON.parse(calls[1].options.body), {
|
||||
robotCode: 'ding-proactive-client',
|
||||
msgKey: 'sampleText',
|
||||
msgParam: JSON.stringify({ content: ' 用户主动消息\n' }),
|
||||
userIds: ['staff-one'],
|
||||
});
|
||||
assert.deepEqual(JSON.parse(calls[2].options.body), {
|
||||
robotCode: 'ding-proactive-client',
|
||||
msgKey: 'sampleText',
|
||||
msgParam: JSON.stringify({ content: '群主动消息' }),
|
||||
openConversationId: 'cid-one',
|
||||
});
|
||||
assert.doesNotMatch(JSON.stringify(calls.slice(1)), /sessionWebhook|sendBySession/);
|
||||
});
|
||||
|
||||
test('DingTalk exposes a stable rejection code for rejected proactive text', async () => {
|
||||
const api = createDingtalkApi({
|
||||
fetchImpl: async (url) => url.pathname.endsWith('/oauth2/accessToken')
|
||||
? jsonResponse({ accessToken: 'rejected-text-token', expireIn: 7_200 })
|
||||
: jsonResponse({ code: 'Forbidden.AccessDenied' }),
|
||||
});
|
||||
await assert.rejects(() => api.sendRobotText({
|
||||
clientId: 'ding-rejected-text-client',
|
||||
clientSecret: 'host-only-secret',
|
||||
target: { type: 'user', robotCode: 'ding-rejected-text-client', userId: 'staff-one' },
|
||||
text: '主动投递',
|
||||
}), (error) => {
|
||||
assert.equal(error.code, 'send-rejected');
|
||||
assert.equal(error.providerCode, 'Forbidden.AccessDenied');
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
test('DingTalk adds, recalls, and replaces its native status reactions', async () => {
|
||||
const calls = [];
|
||||
const fetchImpl = async (url, options) => {
|
||||
|
|
|
|||
|
|
@ -57,9 +57,11 @@ function configFixture(initial = [], events = []) {
|
|||
function runtimeFactory({ events = [], pendingByClient = new Map(), failStart = false } = {}) {
|
||||
const runtimes = [];
|
||||
const connectionTests = [];
|
||||
const proactiveSends = [];
|
||||
return {
|
||||
runtimes,
|
||||
connectionTests,
|
||||
proactiveSends,
|
||||
createRuntime: async ({ botId, config, clientSecret }) => {
|
||||
events.push(['create', botId]);
|
||||
let ready = false;
|
||||
|
|
@ -96,6 +98,10 @@ function runtimeFactory({ events = [], pendingByClient = new Map(), failStart =
|
|||
async sendConnectionTest(text) {
|
||||
connectionTests.push({ botId, text });
|
||||
},
|
||||
async sendProactiveText(...args) {
|
||||
proactiveSends.push({ botId, args });
|
||||
return { sent: true };
|
||||
},
|
||||
};
|
||||
runtimes.push(runtime);
|
||||
return runtime;
|
||||
|
|
@ -154,6 +160,14 @@ test('successful QR poll stores secret then config then starts runtime without p
|
|||
assert.equal(runtimes.connectionTests[0].botId, completed.botId);
|
||||
assert.match(runtimes.connectionTests[0].text, /DeepSeek Harness 连接测试成功/);
|
||||
assert.match(runtimes.connectionTests[0].text, /钉钉机器人(ding••••vate)/);
|
||||
const target = { kind: 'user', route: { userId: 'staff-one' } };
|
||||
assert.deepEqual(await controller.sendProactiveText(completed.botId, target, '主动投递'), {
|
||||
sent: true,
|
||||
});
|
||||
assert.deepEqual(runtimes.proactiveSends, [{
|
||||
botId: completed.botId,
|
||||
args: [target, '主动投递', {}],
|
||||
}]);
|
||||
await controller.close();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -48,6 +48,8 @@ function stateFixture() {
|
|||
test('runtime sends a DingTalk connection test only through the remembered private webhook', async () => {
|
||||
const state = stateFixture();
|
||||
const sends = [];
|
||||
const proactiveSends = [];
|
||||
let proactiveFailure = null;
|
||||
const client = {
|
||||
connected: true,
|
||||
socket: { readyState: 1 },
|
||||
|
|
@ -61,7 +63,13 @@ test('runtime sends a DingTalk connection test only through the remembered priva
|
|||
clientSecret: 'host-secret',
|
||||
harness: { ensureRunning: async () => true },
|
||||
state,
|
||||
api: { sendText: async (request) => sends.push(request) },
|
||||
api: {
|
||||
sendText: async (request) => sends.push(request),
|
||||
sendRobotText: async (request) => {
|
||||
proactiveSends.push(request);
|
||||
if (proactiveFailure) throw proactiveFailure;
|
||||
},
|
||||
},
|
||||
streamFactory: async () => ({ client, topic: 'robot-topic' }),
|
||||
});
|
||||
|
||||
|
|
@ -78,6 +86,23 @@ test('runtime sends a DingTalk connection test only through the remembered priva
|
|||
assert.equal(sends[0].clientSecret, 'host-secret');
|
||||
assert.equal(sends[0].sessionWebhook, 'https://oapi.dingtalk.com/robot/reply?ticket=inbound-private');
|
||||
assert.equal(sends[0].text, '连接测试');
|
||||
assert.deepEqual(await runtime.sendProactiveText({
|
||||
kind: 'group',
|
||||
route: { openConversationId: 'cid-proactive' },
|
||||
}, '主动投递'), { sent: true });
|
||||
assert.equal(proactiveSends.length, 1);
|
||||
assert.deepEqual(proactiveSends[0].target, {
|
||||
type: 'group',
|
||||
robotCode: 'ding-client',
|
||||
openConversationId: 'cid-proactive',
|
||||
});
|
||||
assert.equal(proactiveSends[0].text, '主动投递');
|
||||
assert.equal('sessionWebhook' in proactiveSends[0], false);
|
||||
proactiveFailure = Object.assign(new Error('provider detail'), { code: 'send-rejected' });
|
||||
await assert.rejects(() => runtime.sendProactiveText({
|
||||
kind: 'user',
|
||||
route: { userId: 'staff-one' },
|
||||
}, '失败投递'), { code: 'target-rejected' });
|
||||
await runtime.stop();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -392,6 +392,7 @@ test('Discord controller persists a credential reference and exposes only masked
|
|||
const configPath = join(directory, 'config.json');
|
||||
const configStore = await new DiscordConfigStore(configPath).load();
|
||||
const credentialStore = credentials();
|
||||
const proactiveSends = [];
|
||||
const controller = new DiscordController({
|
||||
credentials: credentialStore,
|
||||
configStore,
|
||||
|
|
@ -409,6 +410,10 @@ test('Discord controller persists a credential reference and exposes only masked
|
|||
},
|
||||
async start() {},
|
||||
async stop() {},
|
||||
async sendProactiveText(...args) {
|
||||
proactiveSends.push(args);
|
||||
return { sent: true };
|
||||
},
|
||||
}),
|
||||
});
|
||||
const status = await controller.bindCredentials({ token: TOKEN });
|
||||
|
|
@ -426,6 +431,11 @@ test('Discord controller persists a credential reference and exposes only masked
|
|||
const identity = deriveDiscordBotIdentity('1234567890123456789');
|
||||
assert.equal(credentialStore.values.get(identity.tokenRef), TOKEN);
|
||||
assert.doesNotMatch(await readFile(configPath, 'utf8'), new RegExp(TOKEN.replaceAll('.', '\\.')));
|
||||
const target = { kind: 'channel', route: { channelId: '222222222222222222' } };
|
||||
assert.deepEqual(await controller.sendProactiveText(identity.botId, target, 'proactive-test'), {
|
||||
sent: true,
|
||||
});
|
||||
assert.deepEqual(proactiveSends, [[target, 'proactive-test', {}]]);
|
||||
await controller.deleteBot(identity.botId);
|
||||
assert.equal(credentialStore.values.has(identity.tokenRef), false);
|
||||
});
|
||||
|
|
@ -1608,6 +1618,7 @@ test('Discord runtime identifies on Gateway v10 and becomes ready', async () =>
|
|||
const abortMark = deferred();
|
||||
let abortMarkStarted = false;
|
||||
const errors = [];
|
||||
const proactiveCalls = [];
|
||||
const runtime = new DiscordRuntime({
|
||||
config: {
|
||||
botId: 'discord_test',
|
||||
|
|
@ -1632,6 +1643,10 @@ test('Discord runtime identifies on Gateway v10 and becomes ready', async () =>
|
|||
createApi: () => ({
|
||||
getCurrentUser: async () => ({ id: '1234567890123456789', bot: true }),
|
||||
getGatewayBot: async () => ({ url: 'wss://gateway.discord.gg' }),
|
||||
createMessage: async (request) => {
|
||||
proactiveCalls.push(request);
|
||||
return { id: '111111111111111900' };
|
||||
},
|
||||
}),
|
||||
createWebSocket: () => {
|
||||
socket = new FakeSocket();
|
||||
|
|
@ -1648,6 +1663,14 @@ test('Discord runtime identifies on Gateway v10 and becomes ready', async () =>
|
|||
});
|
||||
await runtime.start();
|
||||
assert.equal(runtime.status.ready, true);
|
||||
assert.deepEqual(await runtime.sendProactiveText({
|
||||
kind: 'channel',
|
||||
route: { channelId: '222222222222222900' },
|
||||
}, 'proactive-test'), { providerMessageIds: ['111111111111111900'] });
|
||||
assert.equal(proactiveCalls.length, 1);
|
||||
assert.equal(proactiveCalls[0].channelId, '222222222222222900');
|
||||
assert.equal(proactiveCalls[0].content, 'proactive-test');
|
||||
assert.equal(proactiveCalls[0].replyToMessageId, undefined);
|
||||
const identify = socket.sent.find((packet) => packet.op === 2);
|
||||
assert.equal(identify.d.token, TOKEN);
|
||||
assert.equal(identify.d.intents, 37_377);
|
||||
|
|
|
|||
|
|
@ -176,6 +176,19 @@ test('FeishuRuntime becomes chat-ready only after Harness and Feishu are connect
|
|||
},
|
||||
}]);
|
||||
|
||||
assert.deepEqual(await runtime.sendProactiveText({
|
||||
kind: 'group',
|
||||
route: { chatId: 'oc_proactive_group' },
|
||||
}, '主动投递'), { sent: true });
|
||||
assert.deepEqual(FakeClient.sent[1], {
|
||||
params: { receive_id_type: 'chat_id' },
|
||||
data: {
|
||||
receive_id: 'oc_proactive_group',
|
||||
msg_type: 'text',
|
||||
content: JSON.stringify({ text: '主动投递' }),
|
||||
},
|
||||
});
|
||||
|
||||
const stopped = await runtime.stop();
|
||||
assert.equal(stopped.ready, false);
|
||||
assert.equal(stopped.feishuLongConnectionState, 'idle');
|
||||
|
|
|
|||
|
|
@ -107,6 +107,7 @@ function fixture({
|
|||
starts: 0,
|
||||
stops: 0,
|
||||
sentTests: [],
|
||||
proactiveSends: [],
|
||||
probes: [],
|
||||
responseModes: [],
|
||||
repair,
|
||||
|
|
@ -127,6 +128,10 @@ function fixture({
|
|||
runtime.sentTests.push(text);
|
||||
return { sent: true };
|
||||
},
|
||||
async sendProactiveText(...args) {
|
||||
runtime.proactiveSends.push(args);
|
||||
return { sent: true };
|
||||
},
|
||||
setGroupResponseMode(mode) {
|
||||
runtime.responseModes.push(mode);
|
||||
runtime.config.groupResponseMode = mode;
|
||||
|
|
@ -793,6 +798,11 @@ test('connection test uses the selected bot runtime and shared message copy', as
|
|||
assert.deepEqual(fx.runtimes.get(healthy.id)[0].sentTests, [
|
||||
'✅ DeepSeek Harness 连接测试成功\n这条消息由「IM机器人」设置页中的“机器人 healthy(cli_heal••••7890)”机器人卡片发出。',
|
||||
]);
|
||||
const target = { kind: 'group', route: { chatId: 'oc_target' } };
|
||||
assert.deepEqual(await fx.controller.sendProactiveText(healthy.id, target, '主动投递'), {
|
||||
sent: true,
|
||||
});
|
||||
assert.deepEqual(fx.runtimes.get(healthy.id)[0].proactiveSends, [[target, '主动投递', {}]]);
|
||||
await fx.controller.close();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ test('QQ controller delegates the shared connection-test message to the selected
|
|||
botId: 'qq_bot', appId: 'app-id', secretRef: 'qq.secret', ownerUserOpenid: 'owner-openid',
|
||||
};
|
||||
const sent = [];
|
||||
const proactiveSends = [];
|
||||
const controller = new QqController({
|
||||
qrAuth: { start() { return () => {}; } },
|
||||
credentials: {
|
||||
|
|
@ -116,6 +117,10 @@ test('QQ controller delegates the shared connection-test message to the selected
|
|||
start: async () => {},
|
||||
stop: async () => {},
|
||||
sendConnectionTest: async (text) => sent.push(text),
|
||||
sendProactiveText: async (...args) => {
|
||||
proactiveSends.push(args);
|
||||
return { sent: true };
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
@ -124,6 +129,11 @@ test('QQ controller delegates the shared connection-test message to the selected
|
|||
assert.deepEqual(sent, [
|
||||
connectionTestMessage(`QQ 机器人(${maskQqAppId(config.appId)})`),
|
||||
]);
|
||||
const target = { kind: 'user', route: { userOpenId: 'user-openid' } };
|
||||
assert.deepEqual(await controller.sendProactiveText(config.botId, target, '主动投递'), {
|
||||
sent: true,
|
||||
});
|
||||
assert.deepEqual(proactiveSends, [[target, '主动投递', {}]]);
|
||||
await controller.close();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -98,6 +98,14 @@ test('QQ runtime sends a proactive connection test to the explicit owner fallbac
|
|||
target: { scope: 'c2c', targetId: 'owner-openid' },
|
||||
text: 'connection-test',
|
||||
}]);
|
||||
await runtime.sendProactiveText({
|
||||
kind: 'group',
|
||||
route: { groupOpenId: 'group-openid' },
|
||||
}, 'proactive-test');
|
||||
assert.deepEqual(bot.sent[1], {
|
||||
target: { scope: 'group', targetId: 'group-openid' },
|
||||
text: 'proactive-test',
|
||||
});
|
||||
await runtime.stop();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -709,6 +709,7 @@ test('Slack controller stores two protected credential references and exposes ne
|
|||
const configStore = await new SlackConfigStore(configPath).load();
|
||||
const credentialStore = credentials();
|
||||
const connectionTests = [];
|
||||
const proactiveSends = [];
|
||||
const controller = new SlackController({
|
||||
credentials: credentialStore,
|
||||
configStore,
|
||||
|
|
@ -729,6 +730,10 @@ test('Slack controller stores two protected credential references and exposes ne
|
|||
async start() {},
|
||||
async stop() {},
|
||||
async sendConnectionTest(text) { connectionTests.push(text); },
|
||||
async sendProactiveText(...args) {
|
||||
proactiveSends.push(args);
|
||||
return { sent: true };
|
||||
},
|
||||
}),
|
||||
});
|
||||
const status = await controller.bindCredentials({ botToken: BOT_TOKEN, appToken: APP_TOKEN });
|
||||
|
|
@ -744,6 +749,11 @@ test('Slack controller stores two protected credential references and exposes ne
|
|||
await controller.sendConnectionTest(identity.botId);
|
||||
assert.match(connectionTests[0], /DeepSeek Harness/);
|
||||
assert.match(connectionTests[0], /T1234••• · U1234•••/);
|
||||
const target = { kind: 'conversation', route: { channelId: 'C12345678' } };
|
||||
assert.deepEqual(await controller.sendProactiveText(identity.botId, target, 'proactive-test'), {
|
||||
sent: true,
|
||||
});
|
||||
assert.deepEqual(proactiveSends, [[target, 'proactive-test', {}]]);
|
||||
await controller.deleteBot(identity.botId);
|
||||
assert.equal(credentialStore.values.has(identity.botTokenRef), false);
|
||||
assert.equal(credentialStore.values.has(identity.appTokenRef), false);
|
||||
|
|
@ -1048,6 +1058,7 @@ test('Slack runtime opens Socket Mode, acknowledges envelopes, and becomes ready
|
|||
const abortMark = deferred();
|
||||
let abortMarkStarted = false;
|
||||
const errors = [];
|
||||
const proactiveCalls = [];
|
||||
const runtime = new SlackRuntime({
|
||||
config: {
|
||||
botId: 'slack_test',
|
||||
|
|
@ -1073,6 +1084,10 @@ test('Slack runtime opens Socket Mode, acknowledges envelopes, and becomes ready
|
|||
createApi: () => ({
|
||||
authTest: async () => ({ team_id: 'T12345678', user_id: 'U12345678' }),
|
||||
openConnection: async () => ({ url: 'wss://wss-primary.slack.com/link/?ticket=test' }),
|
||||
postMessage: async (request) => {
|
||||
proactiveCalls.push(request);
|
||||
return { ts: '1700000000.900' };
|
||||
},
|
||||
}),
|
||||
createWebSocket: () => {
|
||||
socket = new FakeSocket();
|
||||
|
|
@ -1091,6 +1106,14 @@ test('Slack runtime opens Socket Mode, acknowledges envelopes, and becomes ready
|
|||
});
|
||||
await runtime.start();
|
||||
assert.equal(runtime.status.ready, true);
|
||||
assert.deepEqual(await runtime.sendProactiveText({
|
||||
kind: 'thread',
|
||||
route: { channelId: 'C12345678', threadTs: '1700000000.100' },
|
||||
}, 'proactive-test'), { providerMessageIds: ['1700000000.900'] });
|
||||
assert.equal(proactiveCalls.length, 1);
|
||||
assert.equal(proactiveCalls[0].channelId, 'C12345678');
|
||||
assert.equal(proactiveCalls[0].threadTs, '1700000000.100');
|
||||
assert.equal(proactiveCalls[0].text, 'proactive-test');
|
||||
socket.emit('message', {
|
||||
data: JSON.stringify({
|
||||
envelope_id: 'env-1',
|
||||
|
|
|
|||
|
|
@ -630,6 +630,7 @@ test('Telegram config and controller store only a credential reference in bot da
|
|||
const credentialStore = credentials();
|
||||
const runtimes = [];
|
||||
const connectionTests = [];
|
||||
const proactiveSends = [];
|
||||
const controller = new TelegramController({
|
||||
credentials: credentialStore,
|
||||
configStore,
|
||||
|
|
@ -647,6 +648,10 @@ test('Telegram config and controller store only a credential reference in bot da
|
|||
async start() {},
|
||||
async stop() {},
|
||||
async sendConnectionTest(text) { connectionTests.push(text); },
|
||||
async sendProactiveText(...args) {
|
||||
proactiveSends.push(args);
|
||||
return { sent: true };
|
||||
},
|
||||
};
|
||||
runtimes.push(runtime);
|
||||
return runtime;
|
||||
|
|
@ -673,6 +678,11 @@ test('Telegram config and controller store only a credential reference in bot da
|
|||
await controller.sendConnectionTest(identity.botId);
|
||||
assert.match(connectionTests[0], /Harness Telegram/);
|
||||
assert.match(connectionTests[0], /123•••/);
|
||||
const target = { kind: 'chat', route: { chatId: '123456' } };
|
||||
assert.deepEqual(await controller.sendProactiveText(identity.botId, target, 'proactive-test'), {
|
||||
sent: true,
|
||||
});
|
||||
assert.deepEqual(proactiveSends, [[target, 'proactive-test', {}]]);
|
||||
await controller.deleteBot(identity.botId);
|
||||
assert.equal(credentialStore.values.has(identity.tokenRef), false);
|
||||
assert.equal(controller.status().totals.configured, 0);
|
||||
|
|
@ -1352,6 +1362,10 @@ test('Telegram runtime validates webhook state and starts a cancellable long pol
|
|||
reject(new DOMException('Aborted', 'AbortError'));
|
||||
}, { once: true }));
|
||||
},
|
||||
sendMessage: async (request) => {
|
||||
calls.push({ method: 'sendMessage', ...request });
|
||||
return { message_id: 900 };
|
||||
},
|
||||
};
|
||||
const runtime = new TelegramRuntime({
|
||||
config: {
|
||||
|
|
@ -1367,6 +1381,15 @@ test('Telegram runtime validates webhook state and starts a cancellable long pol
|
|||
await runtime.start();
|
||||
assert.equal(runtime.status.ready, true);
|
||||
assert.equal(runtime.status.connectionState, 'connected');
|
||||
assert.deepEqual(await runtime.sendProactiveText({
|
||||
kind: 'topic',
|
||||
route: { chatId: '-1001234567890', messageThreadId: 42 },
|
||||
}, 'proactive-test'), { providerMessageIds: ['900'] });
|
||||
const proactiveCall = calls.find(({ method }) => method === 'sendMessage');
|
||||
assert.equal(proactiveCall.chatId, -1001234567890);
|
||||
assert.equal(proactiveCall.messageThreadId, 42);
|
||||
assert.equal(proactiveCall.replyToMessageId, undefined);
|
||||
assert.equal(proactiveCall.text, 'proactive-test');
|
||||
await runtime.stop();
|
||||
assert.equal(runtime.status.ready, false);
|
||||
assert.deepEqual(calls[0], { method: 'setMyCommands', commands: TELEGRAM_COMMAND_MENU });
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ test('Enterprise WeChat Bot ID and Secret binding stores credentials and starts
|
|||
const configs = [];
|
||||
let runtimeArgs;
|
||||
let connectionTestText;
|
||||
const proactiveSends = [];
|
||||
const controller = new WecomController({
|
||||
qrAuth: { start: async () => ({}), poll: async () => ({ status: 'waiting' }) },
|
||||
credentials: {
|
||||
|
|
@ -77,6 +78,10 @@ test('Enterprise WeChat Bot ID and Secret binding stores credentials and starts
|
|||
status: { ready: true, wecomConnectionState: 'connected', harnessReachable: true },
|
||||
start: async () => {}, stop: async () => {},
|
||||
sendConnectionTest: async (text) => { connectionTestText = text; },
|
||||
sendProactiveText: async (...args) => {
|
||||
proactiveSends.push(args);
|
||||
return { sent: true };
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
@ -88,6 +93,11 @@ test('Enterprise WeChat Bot ID and Secret binding stores credentials and starts
|
|||
await controller.sendConnectionTest(status.bots[0].botId);
|
||||
assert.match(connectionTestText, /DeepSeek Harness 连接测试成功/);
|
||||
assert.match(connectionTestText, /企业微信机器人(remote••••nual)/);
|
||||
const target = { kind: 'group', route: { chatId: 'group-target' } };
|
||||
assert.deepEqual(await controller.sendProactiveText(status.bots[0].botId, target, '主动投递'), {
|
||||
sent: true,
|
||||
});
|
||||
assert.deepEqual(proactiveSends, [[target, '主动投递', {}]]);
|
||||
await controller.close();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,14 @@ test('Enterprise WeChat runtime sends a connection test only to the remembered p
|
|||
chatId: 'member-private',
|
||||
body: { msgtype: 'markdown', markdown: { content: '测试' } },
|
||||
}]);
|
||||
assert.deepEqual(await runtime.sendProactiveText({
|
||||
kind: 'group',
|
||||
route: { chatId: 'group-target' },
|
||||
}, '主动投递'), { sent: true });
|
||||
assert.deepEqual(client.sent[1], {
|
||||
chatId: 'group-target',
|
||||
body: { msgtype: 'markdown', markdown: { content: '主动投递' } },
|
||||
});
|
||||
await runtime.stop();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ function configFixture() {
|
|||
function runtimeFactory({ failStart = false, startError, lastMessageError = null } = {}) {
|
||||
const runtimes = [];
|
||||
const connectionTests = [];
|
||||
const proactiveSends = [];
|
||||
const createRuntime = async ({ config, token }) => {
|
||||
let ready = false;
|
||||
const runtime = {
|
||||
|
|
@ -71,11 +72,15 @@ function runtimeFactory({ failStart = false, startError, lastMessageError = null
|
|||
},
|
||||
async stop() { ready = false; },
|
||||
async sendConnectionTest(text) { connectionTests.push({ botId: config.botId, text }); },
|
||||
async sendProactiveText(target, text, options) {
|
||||
proactiveSends.push({ botId: config.botId, target, text, options });
|
||||
return { sent: true };
|
||||
},
|
||||
};
|
||||
runtimes.push(runtime);
|
||||
return runtime;
|
||||
};
|
||||
return { runtimes, connectionTests, createRuntime };
|
||||
return { runtimes, connectionTests, proactiveSends, createRuntime };
|
||||
}
|
||||
|
||||
test('confirmed QR login stores bot_token only in credentials and starts a redacted account', async () => {
|
||||
|
|
@ -139,6 +144,16 @@ test('confirmed QR login stores bot_token only in credentials and starts a redac
|
|||
assert.equal(runtimes.connectionTests[0].botId, completed.botId);
|
||||
assert.match(runtimes.connectionTests[0].text, /DeepSeek Harness 连接测试成功/);
|
||||
assert.match(runtimes.connectionTests[0].text, /微信机器人(accoun••••\.bot)/);
|
||||
const target = { kind: 'user', route: { toUserId: 'target-user' } };
|
||||
assert.deepEqual(await controller.sendProactiveText(completed.botId, target, '主动投递'), {
|
||||
sent: true,
|
||||
});
|
||||
assert.deepEqual(runtimes.proactiveSends[0], {
|
||||
botId: completed.botId,
|
||||
target,
|
||||
text: '主动投递',
|
||||
options: {},
|
||||
});
|
||||
|
||||
await controller.deleteBot(completed.botId);
|
||||
assert.equal(credentials.values.size, 0);
|
||||
|
|
|
|||
|
|
@ -165,6 +165,14 @@ test('runtime sends a connection test to the bound Weixin owner without reply co
|
|||
assert.equal(sends[0].text, '连接测试');
|
||||
assert.equal(sends[0].contextToken, undefined);
|
||||
assert.equal(sends[0].runId, undefined);
|
||||
assert.deepEqual(await runtime.sendProactiveText({
|
||||
kind: 'user',
|
||||
route: { toUserId: 'target-user' },
|
||||
}, '主动投递'), { sent: true });
|
||||
assert.equal(sends[1].toUserId, 'target-user');
|
||||
assert.equal(sends[1].text, '主动投递');
|
||||
assert.equal(sends[1].contextToken, undefined);
|
||||
assert.equal(sends[1].runId, undefined);
|
||||
await runtime.stop();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1341,7 +1341,14 @@ test('WhatsApp runtime sends a connection test to self and suppresses its outbou
|
|||
|
||||
await runtime.start();
|
||||
assert.deepEqual(await runtime.sendConnectionTest('连接测试'), { sent: true });
|
||||
assert.deepEqual(sent, [[ACCOUNT_JID, { text: '连接测试' }]]);
|
||||
assert.deepEqual(await runtime.sendProactiveText({
|
||||
kind: 'group',
|
||||
route: { jid: '120363000000000000@g.us' },
|
||||
}, '主动投递'), { providerMessageIds: ['connection-test-1'] });
|
||||
assert.deepEqual(sent, [
|
||||
[ACCOUNT_JID, { text: '连接测试' }],
|
||||
['120363000000000000@g.us', { text: '主动投递' }],
|
||||
]);
|
||||
await callbacks.onMessage({
|
||||
key: { remoteJid: ACCOUNT_JID, id: 'connection-test-1', fromMe: true },
|
||||
message: { conversation: '连接测试' },
|
||||
|
|
@ -1355,6 +1362,7 @@ test('WhatsApp controller delegates connection test copy to the current runtime'
|
|||
const configStore = await new WhatsappConfigStore(join(root, 'config.json')).load();
|
||||
const config = await configStore.save(linkedConfig());
|
||||
const sent = [];
|
||||
const proactiveSends = [];
|
||||
const controller = new WhatsappController({
|
||||
configStore,
|
||||
authPath: (name) => join(root, 'auth', name),
|
||||
|
|
@ -1371,6 +1379,10 @@ test('WhatsApp controller delegates connection test copy to the current runtime'
|
|||
sent.push(text);
|
||||
return { sent: true };
|
||||
},
|
||||
sendProactiveText: async (...args) => {
|
||||
proactiveSends.push(args);
|
||||
return { sent: true };
|
||||
},
|
||||
}),
|
||||
});
|
||||
t.after(() => controller.close());
|
||||
|
|
@ -1380,6 +1392,11 @@ test('WhatsApp controller delegates connection test copy to the current runtime'
|
|||
assert.deepEqual(sent, [
|
||||
'✅ DeepSeek Harness 连接测试成功\n这条消息由「IM机器人」设置页中的“Harness WhatsApp(1650••••0123)”机器人卡片发出。',
|
||||
]);
|
||||
const target = { kind: 'user', route: { jid: '16505550199@s.whatsapp.net' } };
|
||||
assert.deepEqual(await controller.sendProactiveText(config.botId, target, 'proactive-test'), {
|
||||
sent: true,
|
||||
});
|
||||
assert.deepEqual(proactiveSends, [[target, 'proactive-test', {}]]);
|
||||
});
|
||||
|
||||
test('WhatsApp reconnect RPC sends tests only for the connected target and keeps failures non-fatal', async () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue