@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.
Files changed (47) hide show
  1. package/package.json +9 -4
  2. package/src/adapter.ts +0 -42
  3. package/src/adapters/cli.ts +0 -37
  4. package/src/adapters/dingtalk.ts +0 -206
  5. package/src/adapters/discord.ts +0 -127
  6. package/src/adapters/feishu.ts +0 -224
  7. package/src/adapters/slack.ts +0 -123
  8. package/src/adapters/telegram.ts +0 -142
  9. package/src/adapters/wechat.ts +0 -247
  10. package/src/adapters/wecom.ts +0 -218
  11. package/src/adapters/whatsapp.ts +0 -249
  12. package/src/asr.ts +0 -83
  13. package/src/commands.ts +0 -98
  14. package/src/config.ts +0 -104
  15. package/src/feed.ts +0 -190
  16. package/src/index.ts +0 -510
  17. package/src/memory.ts +0 -176
  18. package/src/mention.ts +0 -51
  19. package/src/pending-buttons.ts +0 -65
  20. package/src/session.ts +0 -23
  21. package/src/setup.ts +0 -252
  22. package/src/status.ts +0 -63
  23. package/src/text.ts +0 -32
  24. package/src/trajectory.ts +0 -45
  25. package/test/adapters.dingtalk.test.ts +0 -64
  26. package/test/adapters.discord.test.ts +0 -41
  27. package/test/adapters.feishu.test.ts +0 -66
  28. package/test/adapters.slack.test.ts +0 -45
  29. package/test/adapters.telegram.test.ts +0 -37
  30. package/test/adapters.wechat.test.ts +0 -78
  31. package/test/adapters.wecom.test.ts +0 -62
  32. package/test/adapters.whatsapp.test.ts +0 -138
  33. package/test/asr.test.ts +0 -77
  34. package/test/commands.test.ts +0 -53
  35. package/test/config.test.ts +0 -30
  36. package/test/feed.test.ts +0 -111
  37. package/test/memory.test.ts +0 -79
  38. package/test/mention.test.ts +0 -54
  39. package/test/multi.test.ts +0 -284
  40. package/test/outbound.test.ts +0 -29
  41. package/test/pending-buttons.test.ts +0 -100
  42. package/test/session.test.ts +0 -26
  43. package/test/status.test.ts +0 -41
  44. package/test/streaming.test.ts +0 -162
  45. package/test/text.test.ts +0 -20
  46. package/test/trajectory.test.ts +0 -58
  47. 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
- })
@@ -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
- });
@@ -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
- });