@dsh-overdrive/gateway 0.3.0 → 0.3.1
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/package.json +9 -4
- package/src/adapter.ts +0 -42
- package/src/adapters/cli.ts +0 -37
- package/src/adapters/dingtalk.ts +0 -206
- package/src/adapters/discord.ts +0 -127
- package/src/adapters/feishu.ts +0 -224
- package/src/adapters/slack.ts +0 -123
- package/src/adapters/telegram.ts +0 -142
- package/src/adapters/wechat.ts +0 -247
- package/src/adapters/wecom.ts +0 -218
- package/src/adapters/whatsapp.ts +0 -249
- package/src/asr.ts +0 -83
- package/src/commands.ts +0 -98
- package/src/config.ts +0 -104
- package/src/feed.ts +0 -190
- package/src/index.ts +0 -510
- package/src/memory.ts +0 -176
- package/src/mention.ts +0 -51
- package/src/pending-buttons.ts +0 -65
- package/src/session.ts +0 -23
- package/src/setup.ts +0 -252
- package/src/status.ts +0 -63
- package/src/text.ts +0 -32
- package/src/trajectory.ts +0 -45
- package/test/adapters.dingtalk.test.ts +0 -64
- package/test/adapters.discord.test.ts +0 -41
- package/test/adapters.feishu.test.ts +0 -66
- package/test/adapters.slack.test.ts +0 -45
- package/test/adapters.telegram.test.ts +0 -37
- package/test/adapters.wechat.test.ts +0 -78
- package/test/adapters.wecom.test.ts +0 -62
- package/test/adapters.whatsapp.test.ts +0 -138
- package/test/asr.test.ts +0 -77
- package/test/commands.test.ts +0 -53
- package/test/config.test.ts +0 -30
- package/test/feed.test.ts +0 -111
- package/test/memory.test.ts +0 -79
- package/test/mention.test.ts +0 -54
- package/test/multi.test.ts +0 -284
- package/test/outbound.test.ts +0 -29
- package/test/pending-buttons.test.ts +0 -100
- package/test/session.test.ts +0 -26
- package/test/status.test.ts +0 -41
- package/test/streaming.test.ts +0 -162
- package/test/text.test.ts +0 -20
- package/test/trajectory.test.ts +0 -58
- package/tsconfig.json +0 -5
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
buildApprovalCard, buildNumberedText, cardActionToButtonId, parseFeishuTextMessage,
|
|
4
|
-
} from '../src/adapters/feishu.js';
|
|
5
|
-
|
|
6
|
-
describe('parseFeishuTextMessage(im.message.receive_v1 载荷 → NormalizedMessage)', () => {
|
|
7
|
-
it('文本私聊消息', () => {
|
|
8
|
-
const data = {
|
|
9
|
-
event: {
|
|
10
|
-
message: { message_id: 'om_1', chat_id: 'oc_1', message_type: 'text', content: JSON.stringify({ text: 'hello' }) },
|
|
11
|
-
sender: { sender_id: { open_id: 'ou_1' } },
|
|
12
|
-
},
|
|
13
|
-
};
|
|
14
|
-
const out = parseFeishuTextMessage(data);
|
|
15
|
-
expect(out).toMatchObject({ chatId: 'oc_1', userId: 'ou_1', text: 'hello' });
|
|
16
|
-
});
|
|
17
|
-
it('非文本消息返回 null', () => {
|
|
18
|
-
const data = { event: { message: { message_type: 'image', content: '{}' }, sender: { sender_id: { open_id: 'ou_1' } } } };
|
|
19
|
-
expect(parseFeishuTextMessage(data)).toBeNull();
|
|
20
|
-
});
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
describe('buildNumberedText(审批编号回复)', () => {
|
|
24
|
-
it('生成 1/2 选项文本', () => {
|
|
25
|
-
const text = buildNumberedText('需要批准', [
|
|
26
|
-
{ id: 'approve:r1', label: '✅ 同意' },
|
|
27
|
-
{ id: 'reject:r1', label: '🚫 拒绝' },
|
|
28
|
-
]);
|
|
29
|
-
expect(text).toContain('1) ✅ 同意');
|
|
30
|
-
expect(text).toContain('2) 🚫 拒绝');
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
describe('buildApprovalCard(飞书原生交互卡片)', () => {
|
|
35
|
-
it('生成 interactive 卡片 JSON:header + 文本 + action 按钮', () => {
|
|
36
|
-
const content = buildApprovalCard('需要批准:执行危险操作', [
|
|
37
|
-
{ id: 'approve:r1', label: '✅ 同意' },
|
|
38
|
-
{ id: 'reject:r1', label: '🚫 拒绝' },
|
|
39
|
-
]);
|
|
40
|
-
const card = JSON.parse(content);
|
|
41
|
-
expect(card.config.wide_screen_mode).toBe(true);
|
|
42
|
-
expect(card.header.title.content).toContain('需要批准');
|
|
43
|
-
const actions = card.elements.find((e: { tag: string }) => e.tag === 'action').actions;
|
|
44
|
-
expect(actions).toHaveLength(2);
|
|
45
|
-
expect(actions[0]).toMatchObject({
|
|
46
|
-
tag: 'button',
|
|
47
|
-
type: 'primary', // approve 主按钮
|
|
48
|
-
value: { action: 'approve', reqId: 'r1' },
|
|
49
|
-
});
|
|
50
|
-
expect(actions[1].value).toEqual({ action: 'reject', reqId: 'r1' });
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
describe('cardActionToButtonId(卡片回调 → 按钮 id)', () => {
|
|
55
|
-
it('approve/reject 值还原为按钮 id', () => {
|
|
56
|
-
expect(cardActionToButtonId({ action: 'approve', reqId: 'r1' })).toBe('approve:r1');
|
|
57
|
-
expect(cardActionToButtonId({ action: 'reject', reqId: 'r9' })).toBe('reject:r9');
|
|
58
|
-
});
|
|
59
|
-
it('非法值返回 null', () => {
|
|
60
|
-
expect(cardActionToButtonId(null)).toBeNull();
|
|
61
|
-
expect(cardActionToButtonId({})).toBeNull();
|
|
62
|
-
expect(cardActionToButtonId({ action: 'other', reqId: 'r1' })).toBeNull();
|
|
63
|
-
expect(cardActionToButtonId({ action: 'approve' })).toBeNull();
|
|
64
|
-
expect(cardActionToButtonId('str')).toBeNull();
|
|
65
|
-
});
|
|
66
|
-
});
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { normalizeSlackMessage, slackBlocks, slackFileUrl } from '../src/adapters/slack.js';
|
|
3
|
-
|
|
4
|
-
describe('normalizeSlackMessage', () => {
|
|
5
|
-
it('文本消息 → NormalizedMessage', () => {
|
|
6
|
-
const raw = { channel: 'C123', user: 'U456', text: 'hello', subtype: undefined };
|
|
7
|
-
const out = normalizeSlackMessage(raw);
|
|
8
|
-
expect(out).toMatchObject({ chatId: 'C123', userId: 'U456', text: 'hello' });
|
|
9
|
-
});
|
|
10
|
-
it('bot 自己的消息(subtype=bot_message)返回 null', () => {
|
|
11
|
-
expect(normalizeSlackMessage({ channel: 'C1', user: 'U2', text: 'x', subtype: 'bot_message' })).toBeNull();
|
|
12
|
-
});
|
|
13
|
-
it('含文件 → media: { kind: "image", url }(纯函数 slackFileUrl 取 files[0].url_private)', () => {
|
|
14
|
-
const raw = {
|
|
15
|
-
channel: 'C1',
|
|
16
|
-
user: 'U2',
|
|
17
|
-
text: '',
|
|
18
|
-
files: [{ url_private: 'https://files.slack.com/files/x.png', mimetype: 'image/png' }],
|
|
19
|
-
};
|
|
20
|
-
expect(slackFileUrl(raw)).toBe('https://files.slack.com/files/x.png');
|
|
21
|
-
expect(normalizeSlackMessage(raw)).toMatchObject({
|
|
22
|
-
chatId: 'C1', userId: 'U2', text: '', media: { kind: 'image', url: 'https://files.slack.com/files/x.png' },
|
|
23
|
-
});
|
|
24
|
-
});
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
describe('slackBlocks', () => {
|
|
28
|
-
it('纯文本 → 一个 section', () => {
|
|
29
|
-
const blocks = slackBlocks('hi', []);
|
|
30
|
-
expect(blocks).toEqual([{ type: 'section', text: { type: 'mrkdwn', text: 'hi' } }]);
|
|
31
|
-
});
|
|
32
|
-
it('带按钮 → section + actions', () => {
|
|
33
|
-
const blocks = slackBlocks('需要批准', [
|
|
34
|
-
{ id: 'approve:r1', label: '✅ 同意' },
|
|
35
|
-
{ id: 'reject:r1', label: '🚫 拒绝' },
|
|
36
|
-
]);
|
|
37
|
-
expect(blocks[1]).toMatchObject({
|
|
38
|
-
type: 'actions',
|
|
39
|
-
elements: [
|
|
40
|
-
{ type: 'button', value: 'approve:r1', text: { type: 'plain_text', text: '✅ 同意' } },
|
|
41
|
-
{ type: 'button', value: 'reject:r1', text: { type: 'plain_text', text: '🚫 拒绝' } },
|
|
42
|
-
],
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
});
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { buttonRows, normalizeTelegramMessage, telegramImageUrl, telegramPhotoFileId } from '../src/adapters/telegram.js';
|
|
3
|
-
|
|
4
|
-
describe('normalizeTelegramMessage', () => {
|
|
5
|
-
it('文本消息 → NormalizedMessage(chatId/userId 字符串化)', () => {
|
|
6
|
-
const ctx = { chat: { id: 12345 }, from: { id: 678 }, message: { text: 'hello' } };
|
|
7
|
-
const out = normalizeTelegramMessage(ctx as never);
|
|
8
|
-
expect(out).toMatchObject({ chatId: '12345', userId: '678', text: 'hello' });
|
|
9
|
-
});
|
|
10
|
-
it('无文本返回 null', () => {
|
|
11
|
-
expect(normalizeTelegramMessage({ chat: { id: 1 }, from: { id: 2 }, message: { photo: [] } } as never)).toBeNull();
|
|
12
|
-
});
|
|
13
|
-
it('含 photo 的消息 → media: { kind: "image" },file_id → 下载 URL 模板(真实 getFile 在 adapter)', () => {
|
|
14
|
-
const ctx = {
|
|
15
|
-
chat: { id: 1 },
|
|
16
|
-
from: { id: 2 },
|
|
17
|
-
message: { photo: [{ file_id: 'small' }, { file_id: 'large' }] },
|
|
18
|
-
};
|
|
19
|
-
expect(normalizeTelegramMessage(ctx as never)).toMatchObject({
|
|
20
|
-
chatId: '1', userId: '2', text: '', media: { kind: 'image' },
|
|
21
|
-
});
|
|
22
|
-
expect(telegramPhotoFileId(ctx.message.photo)).toBe('large');
|
|
23
|
-
expect(telegramImageUrl('SECRET', 'photos/file_10.jpg')).toBe('https://api.telegram.org/file/botSECRET/photos/file_10.jpg');
|
|
24
|
-
});
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
describe('buttonRows(InlineKeyboard 数据)', () => {
|
|
28
|
-
it('按钮 → [label, id] 行', () => {
|
|
29
|
-
expect(buttonRows([
|
|
30
|
-
{ id: 'approve:r1', label: '✅ 同意' },
|
|
31
|
-
{ id: 'reject:r1', label: '🚫 拒绝' },
|
|
32
|
-
])).toEqual([
|
|
33
|
-
['✅ 同意', 'approve:r1'],
|
|
34
|
-
['🚫 拒绝', 'reject:r1'],
|
|
35
|
-
]);
|
|
36
|
-
});
|
|
37
|
-
});
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
buildGetUpdatesBody, buildNumberedReplyText, buildSendMessageBody, chunkText, extractWeChatText, parseWeChatUpdate,
|
|
4
|
-
} from '../src/adapters/wechat.js';
|
|
5
|
-
|
|
6
|
-
describe('extractWeChatText', () => {
|
|
7
|
-
it('提取 text_item 文本', () => {
|
|
8
|
-
expect(extractWeChatText({ from_user_id: 'u1', text_item: { text: 'hello' } })).toBe('hello');
|
|
9
|
-
});
|
|
10
|
-
it('提取 item_list 中 type===1 的文本', () => {
|
|
11
|
-
expect(extractWeChatText({ item_list: [{ type: 2, text_item: { text: 'img' } }, { type: 1, text_item: { text: 'text' } }] })).toBe('text');
|
|
12
|
-
});
|
|
13
|
-
it('无文本返回 null', () => {
|
|
14
|
-
expect(extractWeChatText({ from_user_id: 'u1' })).toBeNull();
|
|
15
|
-
});
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
describe('parseWeChatUpdate', () => {
|
|
19
|
-
it('文本消息 → NormalizedMessage(chatId=userId=from_user_id)', () => {
|
|
20
|
-
expect(parseWeChatUpdate({ from_user_id: 'wx-1', context_token: 'tok', text_item: { text: 'hi' } }))
|
|
21
|
-
.toEqual({ chatId: 'wx-1', userId: 'wx-1', text: 'hi' });
|
|
22
|
-
});
|
|
23
|
-
it('非文本或缺发送者返回 null', () => {
|
|
24
|
-
expect(parseWeChatUpdate({ from_user_id: 'wx-1', text_item: { text: '' } })).toBeNull();
|
|
25
|
-
expect(parseWeChatUpdate({ text_item: { text: 'x' } })).toBeNull();
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
describe('buildGetUpdatesBody', () => {
|
|
30
|
-
it('带同步游标与 longpolling,且必须带 base_info.channel_version', () => {
|
|
31
|
-
const body = buildGetUpdatesBody('buf-1');
|
|
32
|
-
expect(body.get_updates_buf).toBe('buf-1');
|
|
33
|
-
expect(body.longpolling_timeout).toBe(35000);
|
|
34
|
-
expect(body.base_info).toEqual({ channel_version: '1.0.2' });
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
describe('buildSendMessageBody', () => {
|
|
39
|
-
it('msg 包裹 + text_item,带回话 context_token', () => {
|
|
40
|
-
const body = buildSendMessageBody('wx-1', '回复', 'tok-1', 'client-1');
|
|
41
|
-
expect(body.base_info).toEqual({ channel_version: '1.0.2' });
|
|
42
|
-
expect(body.msg).toMatchObject({
|
|
43
|
-
from_user_id: '',
|
|
44
|
-
to_user_id: 'wx-1',
|
|
45
|
-
client_id: 'client-1',
|
|
46
|
-
message_type: 2,
|
|
47
|
-
message_state: 2,
|
|
48
|
-
context_token: 'tok-1',
|
|
49
|
-
item_list: [{ type: 1, text_item: { text: '回复' } }],
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
describe('chunkText', () => {
|
|
55
|
-
it('长文本按上限分段', () => {
|
|
56
|
-
const chunks = chunkText('a'.repeat(1700), 800);
|
|
57
|
-
expect(chunks).toHaveLength(3);
|
|
58
|
-
expect(chunks[0].length).toBe(800);
|
|
59
|
-
expect(chunks[2].length).toBe(100);
|
|
60
|
-
});
|
|
61
|
-
it('短文本单段', () => {
|
|
62
|
-
expect(chunkText('hi', 800)).toEqual(['hi']);
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
describe('buildNumberedReplyText', () => {
|
|
67
|
-
it('生成 1/2 选项文本', () => {
|
|
68
|
-
const text = buildNumberedReplyText('需要批准', [
|
|
69
|
-
{ id: 'approve:r1', label: '✅ 同意' },
|
|
70
|
-
{ id: 'reject:r1', label: '🚫 拒绝' },
|
|
71
|
-
]);
|
|
72
|
-
expect(text).toContain('1) ✅ 同意');
|
|
73
|
-
expect(text).toContain('回复数字选择');
|
|
74
|
-
});
|
|
75
|
-
it('无按钮时原样返回', () => {
|
|
76
|
-
expect(buildNumberedReplyText('hi', [])).toBe('hi');
|
|
77
|
-
});
|
|
78
|
-
});
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
buildNumberedText,
|
|
4
|
-
decryptWeComPayload,
|
|
5
|
-
encryptWeComPayload,
|
|
6
|
-
matchNumberedButton,
|
|
7
|
-
parseWeComXmlMessage,
|
|
8
|
-
} from '../src/adapters/wecom.js';
|
|
9
|
-
|
|
10
|
-
const KEY = 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG'; // 43 位 EncodingAESKey
|
|
11
|
-
|
|
12
|
-
describe('企业微信 AES 加解密(往返)', () => {
|
|
13
|
-
it('encrypt → decrypt 还原原文(含 receiveId)', () => {
|
|
14
|
-
const { encrypted } = encryptWeComPayload(KEY, 'hello', 'corpid123');
|
|
15
|
-
const out = decryptWeComPayload(KEY, encrypted);
|
|
16
|
-
expect(out.message).toBe('hello');
|
|
17
|
-
expect(out.receiveId).toBe('corpid123');
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it('中文消息往返', () => {
|
|
21
|
-
const { encrypted } = encryptWeComPayload(KEY, '你好,世界', 'ww1234567890');
|
|
22
|
-
const out = decryptWeComPayload(KEY, encrypted);
|
|
23
|
-
expect(out.message).toBe('你好,世界');
|
|
24
|
-
expect(out.receiveId).toBe('ww1234567890');
|
|
25
|
-
});
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
describe('parseWeComXmlMessage(回调 XML → NormalizedMessage)', () => {
|
|
29
|
-
it('文本消息', () => {
|
|
30
|
-
const xml = `<xml><ToUserName><![CDATA[ww1]]></ToUserName><FromUserName><![CDATA[user1]]></FromUserName><MsgType><![CDATA[text]]></MsgType><Content><![CDATA[你好]]></Content></xml>`;
|
|
31
|
-
const out = parseWeComXmlMessage(xml);
|
|
32
|
-
expect(out).toMatchObject({ chatId: 'user1', userId: 'user1', text: '你好' });
|
|
33
|
-
});
|
|
34
|
-
it('非文本返回 null', () => {
|
|
35
|
-
const xml = `<xml><FromUserName><![CDATA[u]]></FromUserName><MsgType><![CDATA[image]]></MsgType></xml>`;
|
|
36
|
-
expect(parseWeComXmlMessage(xml)).toBeNull();
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
describe('buildNumberedText(审批编号回复)', () => {
|
|
41
|
-
it('生成 1/2 选项文本', () => {
|
|
42
|
-
const text = buildNumberedText('需要批准', [
|
|
43
|
-
{ id: 'approve:r1', label: '同意' },
|
|
44
|
-
{ id: 'reject:r1', label: '拒绝' },
|
|
45
|
-
]);
|
|
46
|
-
expect(text).toContain('1) 同意');
|
|
47
|
-
expect(text).toContain('2) 拒绝');
|
|
48
|
-
expect(text).toContain('回复数字选择');
|
|
49
|
-
});
|
|
50
|
-
it('无按钮时原样返回', () => {
|
|
51
|
-
expect(buildNumberedText('plain', [])).toBe('plain');
|
|
52
|
-
});
|
|
53
|
-
it('matchNumberedButton 命中选项', () => {
|
|
54
|
-
const buttons = [
|
|
55
|
-
{ id: 'approve:r1', label: '同意' },
|
|
56
|
-
{ id: 'reject:r1', label: '拒绝' },
|
|
57
|
-
];
|
|
58
|
-
expect(matchNumberedButton('2', buttons)).toEqual(buttons[1]);
|
|
59
|
-
expect(matchNumberedButton('3', buttons)).toBeUndefined();
|
|
60
|
-
expect(matchNumberedButton('abc', buttons)).toBeUndefined();
|
|
61
|
-
});
|
|
62
|
-
});
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
buildNativeFlowButtons,
|
|
4
|
-
buildNumberedReply,
|
|
5
|
-
matchNumberedReply,
|
|
6
|
-
normalizeWhatsAppMessage,
|
|
7
|
-
parseNativeButtonResponse,
|
|
8
|
-
whatsappImageUrl,
|
|
9
|
-
type RawWhatsAppMessage,
|
|
10
|
-
} from '../src/adapters/whatsapp.js';
|
|
11
|
-
|
|
12
|
-
describe('normalizeWhatsAppMessage', () => {
|
|
13
|
-
it('文本消息 → NormalizedMessage(chatId=JID, userId=发送者)', () => {
|
|
14
|
-
const raw = {
|
|
15
|
-
key: { remoteJid: '60123@s.whatsapp.net', participant: undefined },
|
|
16
|
-
message: { conversation: 'hello' },
|
|
17
|
-
messageType: 'conversation',
|
|
18
|
-
};
|
|
19
|
-
const out = normalizeWhatsAppMessage(raw);
|
|
20
|
-
expect(out?.kind).toBe('message');
|
|
21
|
-
expect(out?.msg).toMatchObject({ chatId: '60123@s.whatsapp.net', userId: '60123@s.whatsapp.net', text: 'hello' });
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it('群聊用 participant 作 userId', () => {
|
|
25
|
-
const raw = {
|
|
26
|
-
key: { remoteJid: 'group@g.us', participant: '60123@s.whatsapp.net' },
|
|
27
|
-
message: { extendedTextMessage: { text: 'hi' } },
|
|
28
|
-
messageType: 'extendedTextMessage',
|
|
29
|
-
};
|
|
30
|
-
const out = normalizeWhatsAppMessage(raw);
|
|
31
|
-
expect(out?.msg.userId).toBe('60123@s.whatsapp.net');
|
|
32
|
-
expect(out?.msg.chatId).toBe('group@g.us');
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('非文本消息返回 null', () => {
|
|
36
|
-
expect(normalizeWhatsAppMessage({ key: { remoteJid: 'x' }, message: { imageMessage: {} } })).toBeNull();
|
|
37
|
-
expect(normalizeWhatsAppMessage({ key: { remoteJid: 'x' }, message: {} })).toBeNull();
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
describe('whatsappImageUrl / 媒体消息捕获', () => {
|
|
42
|
-
it('imageMessage url + caption → media: { kind: "image", url };无 url 退 directPath', () => {
|
|
43
|
-
const raw = {
|
|
44
|
-
key: { remoteJid: '60123@s.whatsapp.net' },
|
|
45
|
-
message: { imageMessage: { url: 'https://mmg.whatsapp.net/f/x.jpg', caption: '看图' } },
|
|
46
|
-
messageType: 'imageMessage',
|
|
47
|
-
};
|
|
48
|
-
expect(whatsappImageUrl(raw)).toBe('https://mmg.whatsapp.net/f/x.jpg');
|
|
49
|
-
expect(normalizeWhatsAppMessage(raw)).toMatchObject({
|
|
50
|
-
kind: 'message',
|
|
51
|
-
msg: {
|
|
52
|
-
chatId: '60123@s.whatsapp.net',
|
|
53
|
-
userId: '60123@s.whatsapp.net',
|
|
54
|
-
text: '看图',
|
|
55
|
-
media: { kind: 'image', url: 'https://mmg.whatsapp.net/f/x.jpg' },
|
|
56
|
-
},
|
|
57
|
-
});
|
|
58
|
-
// directPath 兜底(M4 简化:URL 直接透传,认证头后续再补)
|
|
59
|
-
expect(whatsappImageUrl({ message: { imageMessage: { directPath: '/d/p.jpg' } } })).toBe('/d/p.jpg');
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
describe('buildNumberedReply / matchNumberedReply(审批按钮的编号文本方案)', () => {
|
|
64
|
-
it('生成 1/2 编号选项', () => {
|
|
65
|
-
const text = buildNumberedReply('⚠️ 需要批准:删除文件', [
|
|
66
|
-
{ id: 'approve:r1', label: '✅ 同意' },
|
|
67
|
-
{ id: 'reject:r1', label: '🚫 拒绝' },
|
|
68
|
-
]);
|
|
69
|
-
expect(text).toContain('1) ✅ 同意');
|
|
70
|
-
expect(text).toContain('2) 🚫 拒绝');
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('回复数字能匹配回按钮 id', () => {
|
|
74
|
-
const buttons = [
|
|
75
|
-
{ id: 'approve:r1', label: '✅ 同意' },
|
|
76
|
-
{ id: 'reject:r1', label: '🚫 拒绝' },
|
|
77
|
-
];
|
|
78
|
-
expect(matchNumberedReply('1', buttons)?.id).toBe('approve:r1');
|
|
79
|
-
expect(matchNumberedReply('2', buttons)?.id).toBe('reject:r1');
|
|
80
|
-
expect(matchNumberedReply('9', buttons)).toBeUndefined();
|
|
81
|
-
expect(matchNumberedReply('同意', buttons)).toBeUndefined();
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
describe('buildNativeFlowButtons(WhatsApp 原生交互按钮)', () => {
|
|
86
|
-
it('按钮 → nativeFlowMessage buttons 数组', () => {
|
|
87
|
-
const buttons = buildNativeFlowButtons([
|
|
88
|
-
{ id: 'approve:r1', label: '✅ 同意' },
|
|
89
|
-
{ id: 'reject:r1', label: '🚫 拒绝' },
|
|
90
|
-
]);
|
|
91
|
-
expect(buttons).toHaveLength(2);
|
|
92
|
-
expect(buttons[0]).toMatchObject({
|
|
93
|
-
name: 'quick_reply',
|
|
94
|
-
buttonParamsJson: JSON.stringify({ id: 'approve:r1', display_text: '✅ 同意' }),
|
|
95
|
-
});
|
|
96
|
-
expect(buttons[1]).toMatchObject({
|
|
97
|
-
name: 'quick_reply',
|
|
98
|
-
buttonParamsJson: JSON.stringify({ id: 'reject:r1', display_text: '🚫 拒绝' }),
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('空数组 → 空 buttons', () => {
|
|
103
|
-
expect(buildNativeFlowButtons([])).toEqual([]);
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
describe('parseNativeButtonResponse(交互按钮响应解析)', () => {
|
|
108
|
-
it('解析 paramsJson 中的 id', () => {
|
|
109
|
-
const raw: RawWhatsAppMessage = {
|
|
110
|
-
key: { remoteJid: '60123@s.whatsapp.net' },
|
|
111
|
-
message: {
|
|
112
|
-
interactiveResponseMessage: {
|
|
113
|
-
nativeFlowResponseMessage: { paramsJson: JSON.stringify({ id: 'approve:r1' }) },
|
|
114
|
-
},
|
|
115
|
-
},
|
|
116
|
-
messageType: 'interactiveResponseMessage',
|
|
117
|
-
};
|
|
118
|
-
expect(parseNativeButtonResponse(raw)).toBe('approve:r1');
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it('无交互响应 / 非法 JSON / 缺 id 均返回 null', () => {
|
|
122
|
-
expect(parseNativeButtonResponse({ key: { remoteJid: 'x' }, message: { conversation: 'hi' } })).toBeNull();
|
|
123
|
-
expect(parseNativeButtonResponse({ key: { remoteJid: 'x' }, message: {} })).toBeNull();
|
|
124
|
-
expect(parseNativeButtonResponse({ key: { remoteJid: 'x' } })).toBeNull();
|
|
125
|
-
expect(
|
|
126
|
-
parseNativeButtonResponse({
|
|
127
|
-
key: { remoteJid: 'x' },
|
|
128
|
-
message: { interactiveResponseMessage: { nativeFlowResponseMessage: { paramsJson: 'not-json' } } },
|
|
129
|
-
}),
|
|
130
|
-
).toBeNull();
|
|
131
|
-
expect(
|
|
132
|
-
parseNativeButtonResponse({
|
|
133
|
-
key: { remoteJid: 'x' },
|
|
134
|
-
message: { interactiveResponseMessage: { nativeFlowResponseMessage: { paramsJson: JSON.stringify({ foo: 'bar' }) } } },
|
|
135
|
-
}),
|
|
136
|
-
).toBeNull();
|
|
137
|
-
});
|
|
138
|
-
});
|
package/test/asr.test.ts
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { createTranscriber, extensionForMime } from '../src/asr.js';
|
|
3
|
-
|
|
4
|
-
describe('extensionForMime', () => {
|
|
5
|
-
it('maps known audio mimes', () => {
|
|
6
|
-
expect(extensionForMime('audio/ogg')).toBe('ogg');
|
|
7
|
-
expect(extensionForMime('audio/mpeg')).toBe('mp3');
|
|
8
|
-
expect(extensionForMime('audio/mp4')).toBe('m4a');
|
|
9
|
-
expect(extensionForMime('audio/wav')).toBe('wav');
|
|
10
|
-
expect(extensionForMime('audio/webm')).toBe('webm');
|
|
11
|
-
});
|
|
12
|
-
it('ignores parameters and case', () => {
|
|
13
|
-
expect(extensionForMime('audio/OGG; codecs=opus')).toBe('ogg');
|
|
14
|
-
});
|
|
15
|
-
it('falls back for unknown or missing mime', () => {
|
|
16
|
-
expect(extensionForMime('application/x-foo')).toBe('oga');
|
|
17
|
-
expect(extensionForMime(undefined)).toBe('oga');
|
|
18
|
-
expect(extensionForMime('')).toBe('oga');
|
|
19
|
-
});
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
describe('createTranscriber.transcribe', () => {
|
|
23
|
-
afterEach(() => vi.unstubAllGlobals());
|
|
24
|
-
|
|
25
|
-
it('未配置 API key 时禁用且不发起请求', async () => {
|
|
26
|
-
const fetchMock = vi.fn()
|
|
27
|
-
vi.stubGlobal('fetch', fetchMock)
|
|
28
|
-
const asr = createTranscriber({})
|
|
29
|
-
expect(asr.enabled).toBe(false)
|
|
30
|
-
expect(await asr.transcribe({ url: 'https://x/v.ogg' })).toBeNull()
|
|
31
|
-
expect(fetchMock).not.toHaveBeenCalled()
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
it('下载音频并调用 OpenAI 兼容转写端点,返回文本', async () => {
|
|
35
|
-
const fetchMock = vi.fn()
|
|
36
|
-
.mockResolvedValueOnce({ ok: true, blob: async () => new Blob(['fake-audio']) })
|
|
37
|
-
.mockResolvedValueOnce({ ok: true, json: async () => ({ text: ' 你好世界 ' }) })
|
|
38
|
-
vi.stubGlobal('fetch', fetchMock)
|
|
39
|
-
const asr = createTranscriber({ apiKey: 'sk-test', baseUrl: 'https://api.siliconflow.cn/v1', model: 'whisper-1' })
|
|
40
|
-
expect(asr.enabled).toBe(true)
|
|
41
|
-
const text = await asr.transcribe({ url: 'https://x/v.ogg', mime: 'audio/ogg' })
|
|
42
|
-
expect(text).toBe('你好世界')
|
|
43
|
-
|
|
44
|
-
// 第一次 fetch:下载音频;第二次:转写端点
|
|
45
|
-
expect(fetchMock.mock.calls[0][0]).toBe('https://x/v.ogg')
|
|
46
|
-
const [url, init] = fetchMock.mock.calls[1]
|
|
47
|
-
expect(String(url)).toBe('https://api.siliconflow.cn/v1/audio/transcriptions')
|
|
48
|
-
expect(init.method).toBe('POST')
|
|
49
|
-
expect(init.headers.authorization).toBe('Bearer sk-test')
|
|
50
|
-
expect(init.body).toBeInstanceOf(FormData)
|
|
51
|
-
const filename = (init.body as FormData).get('file') as File
|
|
52
|
-
expect(filename.name).toBe('voice.ogg')
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
it('下载失败返回 null(不抛错)', async () => {
|
|
56
|
-
vi.stubGlobal('fetch', vi.fn().mockResolvedValueOnce({ ok: false, status: 403 }))
|
|
57
|
-
const asr = createTranscriber({ apiKey: 'sk-test' })
|
|
58
|
-
expect(await asr.transcribe({ url: 'https://x/v.ogg' })).toBeNull()
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
it('转写端点失败返回 null', async () => {
|
|
62
|
-
const fetchMock = vi.fn()
|
|
63
|
-
.mockResolvedValueOnce({ ok: true, blob: async () => new Blob(['x']) })
|
|
64
|
-
.mockResolvedValueOnce({ ok: false, status: 500, text: async () => 'server error' })
|
|
65
|
-
vi.stubGlobal('fetch', fetchMock)
|
|
66
|
-
const asr = createTranscriber({ apiKey: 'sk-test' })
|
|
67
|
-
expect(await asr.transcribe({ url: 'https://x/v.ogg' })).toBeNull()
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
it('无 url 返回 null', async () => {
|
|
71
|
-
const fetchMock = vi.fn()
|
|
72
|
-
vi.stubGlobal('fetch', fetchMock)
|
|
73
|
-
const asr = createTranscriber({ apiKey: 'sk-test' })
|
|
74
|
-
expect(await asr.transcribe({ mime: 'audio/ogg' })).toBeNull()
|
|
75
|
-
expect(fetchMock).not.toHaveBeenCalled()
|
|
76
|
-
})
|
|
77
|
-
})
|
package/test/commands.test.ts
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { parseCommand, type ParsedCommand } from '../src/commands.js';
|
|
3
|
-
|
|
4
|
-
describe('parseCommand', () => {
|
|
5
|
-
it('识别 /trace、/task、/cron、/agents、/new、/help', () => {
|
|
6
|
-
expect(parseCommand('/trace')).toEqual({ kind: 'trace' });
|
|
7
|
-
expect(parseCommand('/new')).toEqual({ kind: 'new' });
|
|
8
|
-
expect(parseCommand('/agents')).toEqual({ kind: 'agents' });
|
|
9
|
-
expect(parseCommand('/help')).toEqual({ kind: 'help' });
|
|
10
|
-
expect(parseCommand('/task 调研竞品')).toEqual({ kind: 'task', prompt: '调研竞品' });
|
|
11
|
-
expect(parseCommand('/cron 0 8 * * * 每日汇报')).toEqual({ kind: 'cron', schedule: '0 8 * * *', prompt: '每日汇报' });
|
|
12
|
-
});
|
|
13
|
-
it('识别 /crons 与 /cronrm', () => {
|
|
14
|
-
expect(parseCommand('/crons')).toEqual({ kind: 'crons' });
|
|
15
|
-
expect(parseCommand('/cronrm cron-1')).toEqual({ kind: 'cronrm', taskId: 'cron-1' });
|
|
16
|
-
expect(parseCommand('/cronrm cron-123-abc')).toEqual({ kind: 'cronrm', taskId: 'cron-123-abc' });
|
|
17
|
-
});
|
|
18
|
-
it('非命令返回 null', () => {
|
|
19
|
-
expect(parseCommand('hello')).toBeNull();
|
|
20
|
-
expect(parseCommand('/unknown')).toBeNull();
|
|
21
|
-
expect(parseCommand('/task')).toBeNull(); // 缺参数
|
|
22
|
-
expect(parseCommand('/cronrm')).toBeNull(); // 缺任务 id
|
|
23
|
-
});
|
|
24
|
-
it('识别 /remember /recall /forget', () => {
|
|
25
|
-
expect(parseCommand('/remember 用户喜欢美式咖啡')).toEqual({ kind: 'remember', text: '用户喜欢美式咖啡' });
|
|
26
|
-
expect(parseCommand('/recall 咖啡')).toEqual({ kind: 'recall', query: '咖啡' });
|
|
27
|
-
expect(parseCommand('/recall')).toEqual({ kind: 'recall', query: '' });
|
|
28
|
-
expect(parseCommand('/forget abc123')).toEqual({ kind: 'forget', memoryId: 'abc123' });
|
|
29
|
-
expect(parseCommand('/remember')).toBeNull(); // 缺内容
|
|
30
|
-
});
|
|
31
|
-
it('识别 /remind(相对时间与定点时间)', () => {
|
|
32
|
-
expect(parseCommand('/remind in 10 分钟 喝水')).toEqual({ kind: 'remind', text: '喝水', inMinutes: 10, atTime: null });
|
|
33
|
-
expect(parseCommand('/remind in 2 小时 开会')).toEqual({ kind: 'remind', text: '开会', inMinutes: 120, atTime: null });
|
|
34
|
-
expect(parseCommand('/remind in 30 minutes 散步')).toEqual({ kind: 'remind', text: '散步', inMinutes: 30, atTime: null });
|
|
35
|
-
expect(parseCommand('/remind in 1 day 汇报')).toEqual({ kind: 'remind', text: '汇报', inMinutes: 1440, atTime: null });
|
|
36
|
-
expect(parseCommand('/remind at 14:30 开会')).toEqual({ kind: 'remind', text: '开会', inMinutes: null, atTime: '14:30' });
|
|
37
|
-
});
|
|
38
|
-
it('识别 /send 与 /status', () => {
|
|
39
|
-
expect(parseCommand('/send /tmp/report.png')).toEqual({ kind: 'send', path: '/tmp/report.png' });
|
|
40
|
-
expect(parseCommand('/status')).toEqual({ kind: 'status' });
|
|
41
|
-
expect(parseCommand('/send')).toBeNull(); // 缺路径
|
|
42
|
-
});
|
|
43
|
-
it('识别 /cron --tz 时区', () => {
|
|
44
|
-
expect(parseCommand('/cron 0 8 * * * 每日汇报 --tz Asia/Shanghai'))
|
|
45
|
-
.toEqual({ kind: 'cron', schedule: '0 8 * * *', prompt: '每日汇报', timeZone: 'Asia/Shanghai' });
|
|
46
|
-
expect(parseCommand('/cron 0 8 * * * 每日汇报')).toEqual({ kind: 'cron', schedule: '0 8 * * *', prompt: '每日汇报', timeZone: undefined });
|
|
47
|
-
});
|
|
48
|
-
it('识别 /context', () => {
|
|
49
|
-
expect(parseCommand('/context 项目重构')).toEqual({ kind: 'context', action: 'set', topic: '项目重构' });
|
|
50
|
-
expect(parseCommand('/context off')).toEqual({ kind: 'context', action: 'clear' });
|
|
51
|
-
expect(parseCommand('/context')).toEqual({ kind: 'context', action: 'show' });
|
|
52
|
-
});
|
|
53
|
-
});
|
package/test/config.test.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { createAdapter, parseAdapterIds, type AdapterEnv } from '../src/config.js';
|
|
3
|
-
|
|
4
|
-
describe('parseAdapterIds', () => {
|
|
5
|
-
it('逗号分隔 + 去空格 + 去空项', () => {
|
|
6
|
-
expect(parseAdapterIds('cli, whatsapp, telegram,')).toEqual(['cli', 'whatsapp', 'telegram']);
|
|
7
|
-
});
|
|
8
|
-
it('缺省为 cli', () => {
|
|
9
|
-
expect(parseAdapterIds('')).toEqual(['cli']);
|
|
10
|
-
});
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
describe('createAdapter 注册表', () => {
|
|
14
|
-
it('cli 恒可用', () => {
|
|
15
|
-
const a = createAdapter('cli', {});
|
|
16
|
-
expect(a.id).toBe('cli');
|
|
17
|
-
});
|
|
18
|
-
it('未知适配器抛错', () => {
|
|
19
|
-
expect(() => createAdapter('nope', {})).toThrow(/unknown adapter/);
|
|
20
|
-
});
|
|
21
|
-
it('feishu 缺凭据抛错', () => {
|
|
22
|
-
expect(() => createAdapter('feishu', {})).toThrow(/FEISHU_APP_ID/);
|
|
23
|
-
});
|
|
24
|
-
it('dingtalk 缺凭据抛错', () => {
|
|
25
|
-
expect(() => createAdapter('dingtalk', {})).toThrow(/DINGTALK_CLIENT_ID/);
|
|
26
|
-
});
|
|
27
|
-
it('wecom 缺凭据抛错', () => {
|
|
28
|
-
expect(() => createAdapter('wecom', {})).toThrow(/WECOM_CORP_ID/);
|
|
29
|
-
});
|
|
30
|
-
});
|