@dsh-overdrive/gateway 0.1.3 → 0.1.4

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 (46) hide show
  1. package/dist/adapters/dingtalk.d.ts +21 -0
  2. package/dist/adapters/dingtalk.js +85 -12
  3. package/dist/adapters/dingtalk.js.map +1 -1
  4. package/dist/adapters/feishu.d.ts +9 -0
  5. package/dist/adapters/feishu.js +65 -11
  6. package/dist/adapters/feishu.js.map +1 -1
  7. package/dist/adapters/telegram.d.ts +7 -1
  8. package/dist/adapters/telegram.js +17 -4
  9. package/dist/adapters/telegram.js.map +1 -1
  10. package/dist/adapters/wecom.d.ts +2 -0
  11. package/dist/adapters/wecom.js +12 -9
  12. package/dist/adapters/wecom.js.map +1 -1
  13. package/dist/adapters/whatsapp.d.ts +1 -1
  14. package/dist/adapters/whatsapp.js +9 -12
  15. package/dist/adapters/whatsapp.js.map +1 -1
  16. package/dist/asr.d.ts +19 -0
  17. package/dist/asr.js +63 -0
  18. package/dist/asr.js.map +1 -0
  19. package/dist/commands.d.ts +5 -0
  20. package/dist/commands.js +7 -0
  21. package/dist/commands.js.map +1 -1
  22. package/dist/config.d.ts +3 -0
  23. package/dist/config.js +3 -0
  24. package/dist/config.js.map +1 -1
  25. package/dist/index.d.ts +3 -0
  26. package/dist/index.js +33 -1
  27. package/dist/index.js.map +1 -1
  28. package/dist/pending-buttons.d.ts +19 -0
  29. package/dist/pending-buttons.js +40 -0
  30. package/dist/pending-buttons.js.map +1 -0
  31. package/package.json +2 -2
  32. package/src/adapters/dingtalk.ts +86 -11
  33. package/src/adapters/feishu.ts +64 -10
  34. package/src/adapters/telegram.ts +22 -5
  35. package/src/adapters/wecom.ts +11 -9
  36. package/src/adapters/whatsapp.ts +9 -12
  37. package/src/asr.ts +83 -0
  38. package/src/commands.ts +8 -1
  39. package/src/config.ts +6 -0
  40. package/src/index.ts +35 -1
  41. package/src/pending-buttons.ts +45 -0
  42. package/test/adapters.dingtalk.test.ts +34 -1
  43. package/test/adapters.feishu.test.ts +37 -1
  44. package/test/asr.test.ts +77 -0
  45. package/test/commands.test.ts +6 -0
  46. package/test/pending-buttons.test.ts +61 -0
@@ -0,0 +1,77 @@
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
+ })
@@ -10,9 +10,15 @@ describe('parseCommand', () => {
10
10
  expect(parseCommand('/task 调研竞品')).toEqual({ kind: 'task', prompt: '调研竞品' });
11
11
  expect(parseCommand('/cron 0 8 * * * 每日汇报')).toEqual({ kind: 'cron', schedule: '0 8 * * *', prompt: '每日汇报' });
12
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
+ });
13
18
  it('非命令返回 null', () => {
14
19
  expect(parseCommand('hello')).toBeNull();
15
20
  expect(parseCommand('/unknown')).toBeNull();
16
21
  expect(parseCommand('/task')).toBeNull(); // 缺参数
22
+ expect(parseCommand('/cronrm')).toBeNull(); // 缺任务 id
17
23
  });
18
24
  });
@@ -0,0 +1,61 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { PendingButtons, PENDING_BUTTONS_TTL_MS } from '../src/pending-buttons.js';
3
+
4
+ describe('PendingButtons', () => {
5
+ it('数字回复命中并消费', () => {
6
+ const pb = new PendingButtons();
7
+ pb.set('chat-1', [
8
+ { id: 'approve:1', label: '同意' },
9
+ { id: 'reject:1', label: '拒绝' },
10
+ ]);
11
+ expect(pb.match('chat-1', '2')).toEqual({ id: 'reject:1', label: '拒绝' });
12
+ expect(pb.match('chat-1', '1')).toBeUndefined(); // 已消费
13
+ });
14
+
15
+ it('非数字 / 越界不消费,按钮仍有效', () => {
16
+ const pb = new PendingButtons();
17
+ pb.set('chat-1', [{ id: 'a', label: '同意' }]);
18
+ expect(pb.match('chat-1', 'hello')).toBeUndefined();
19
+ expect(pb.match('chat-1', '5')).toBeUndefined();
20
+ expect(pb.match('chat-1', '1')).toEqual({ id: 'a', label: '同意' });
21
+ });
22
+
23
+ it('过期后自动失效(不吞后续数字消息)', () => {
24
+ vi.useFakeTimers();
25
+ try {
26
+ const pb = new PendingButtons(1000);
27
+ pb.set('chat-1', [{ id: 'a', label: '同意' }]);
28
+ vi.advanceTimersByTime(1001);
29
+ expect(pb.match('chat-1', '1')).toBeUndefined();
30
+ } finally {
31
+ vi.useRealTimers();
32
+ }
33
+ });
34
+
35
+ it('TTL 内不失效', () => {
36
+ vi.useFakeTimers();
37
+ try {
38
+ const pb = new PendingButtons(PENDING_BUTTONS_TTL_MS);
39
+ pb.set('chat-1', [{ id: 'a', label: '同意' }]);
40
+ vi.advanceTimersByTime(PENDING_BUTTONS_TTL_MS - 1);
41
+ expect(pb.match('chat-1', '1')).toEqual({ id: 'a', label: '同意' });
42
+ } finally {
43
+ vi.useRealTimers();
44
+ }
45
+ });
46
+
47
+ it('consume 删除 pending(原生按钮点击)', () => {
48
+ const pb = new PendingButtons();
49
+ pb.set('chat-1', [{ id: 'a', label: '同意' }]);
50
+ pb.consume('chat-1');
51
+ expect(pb.match('chat-1', '1')).toBeUndefined();
52
+ });
53
+
54
+ it('不同 chat 互不影响', () => {
55
+ const pb = new PendingButtons();
56
+ pb.set('chat-1', [{ id: 'a', label: '同意' }]);
57
+ pb.set('chat-2', [{ id: 'b', label: '拒绝' }]);
58
+ expect(pb.match('chat-2', '1')).toEqual({ id: 'b', label: '拒绝' });
59
+ expect(pb.match('chat-1', '1')).toEqual({ id: 'a', label: '同意' });
60
+ });
61
+ });