@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
package/src/config.ts DELETED
@@ -1,104 +0,0 @@
1
- import type { Adapter } from './adapter.js';
2
- import { CliAdapter } from './adapters/cli.js';
3
- import { WhatsAppAdapter } from './adapters/whatsapp.js';
4
- import { TelegramAdapter } from './adapters/telegram.js';
5
- import { DiscordAdapter } from './adapters/discord.js';
6
- import { SlackAdapter } from './adapters/slack.js';
7
- import { FeishuAdapter } from './adapters/feishu.js';
8
- import { DingTalkAdapter } from './adapters/dingtalk.js';
9
- import { WeComAdapter } from './adapters/wecom.js';
10
- import { WeChatAdapter } from './adapters/wechat.js';
11
-
12
- /** 平台适配器需要的全部环境变量(缺省为 undefined = 不启用该平台)。 */
13
- export interface AdapterEnv {
14
- whatsappDataDir?: string;
15
- telegramBotToken?: string;
16
- discordBotToken?: string;
17
- slackBotToken?: string;
18
- slackAppToken?: string;
19
- feishuAppId?: string;
20
- feishuAppSecret?: string;
21
- dingtalkClientId?: string;
22
- dingtalkClientSecret?: string;
23
- wecomCorpId?: string;
24
- wecomSecret?: string;
25
- wecomAgentId?: string;
26
- wecomToken?: string;
27
- wecomEncodingAESKey?: string;
28
- wecomCallbackPort?: string;
29
- wechatToken?: string;
30
- wechatStateDir?: string;
31
- asrApiKey?: string;
32
- asrBaseUrl?: string;
33
- asrModel?: string;
34
- }
35
-
36
- export function parseAdapterIds(raw: string): string[] {
37
- const ids = raw.split(',').map((s) => s.trim()).filter(Boolean);
38
- return ids.length > 0 ? ids : ['cli'];
39
- }
40
-
41
- /** 按 id 创建适配器实例;依赖注入 env 便于测试。 */
42
- export function createAdapter(id: string, env: AdapterEnv): Adapter {
43
- switch (id) {
44
- case 'cli':
45
- return new CliAdapter();
46
- case 'whatsapp':
47
- return new WhatsAppAdapter({ authDir: env.whatsappDataDir ?? 'data/whatsapp' });
48
- case 'telegram':
49
- if (!env.telegramBotToken) throw new Error('telegram 适配器需要 TELEGRAM_BOT_TOKEN');
50
- return new TelegramAdapter({ token: env.telegramBotToken });
51
- case 'discord':
52
- if (!env.discordBotToken) throw new Error('discord 适配器需要 DISCORD_BOT_TOKEN');
53
- return new DiscordAdapter({ token: env.discordBotToken });
54
- case 'slack':
55
- if (!env.slackBotToken || !env.slackAppToken) throw new Error('slack 适配器需要 SLACK_BOT_TOKEN 与 SLACK_APP_TOKEN');
56
- return new SlackAdapter({ botToken: env.slackBotToken, appToken: env.slackAppToken });
57
- case 'feishu':
58
- if (!env.feishuAppId || !env.feishuAppSecret) throw new Error('feishu 适配器需要 FEISHU_APP_ID / FEISHU_APP_SECRET');
59
- return new FeishuAdapter({ appId: env.feishuAppId, appSecret: env.feishuAppSecret });
60
- case 'dingtalk':
61
- if (!env.dingtalkClientId || !env.dingtalkClientSecret) throw new Error('dingtalk 适配器需要 DINGTALK_CLIENT_ID / DINGTALK_CLIENT_SECRET');
62
- return new DingTalkAdapter({ clientId: env.dingtalkClientId, clientSecret: env.dingtalkClientSecret });
63
- case 'wecom':
64
- if (!env.wecomCorpId || !env.wecomSecret || !env.wecomAgentId || !env.wecomToken || !env.wecomEncodingAESKey) {
65
- throw new Error('wecom 适配器需要 WECOM_CORP_ID / WECOM_SECRET / WECOM_AGENT_ID / WECOM_TOKEN / WECOM_ENCODING_AES_KEY');
66
- }
67
- return new WeComAdapter({
68
- corpId: env.wecomCorpId, secret: env.wecomSecret, agentId: env.wecomAgentId,
69
- token: env.wecomToken, encodingAESKey: env.wecomEncodingAESKey,
70
- callbackPort: Number(env.wecomCallbackPort ?? 3193),
71
- });
72
- case 'wechat':
73
- // 实验性(v0.2b):token 可缺省——未配置时启动扫码登录(iLink/ClawBot)
74
- return new WeChatAdapter({ token: env.wechatToken, stateDir: env.wechatStateDir ?? 'data/wechat' });
75
- default:
76
- throw new Error(`unknown adapter: ${id}`);
77
- }
78
- }
79
-
80
- /** 从 process.env 读适配器配置。 */
81
- export function adapterEnvFromProcess(env: NodeJS.ProcessEnv = process.env): AdapterEnv {
82
- return {
83
- whatsappDataDir: env.WHATSAPP_DATA_DIR,
84
- telegramBotToken: env.TELEGRAM_BOT_TOKEN,
85
- discordBotToken: env.DISCORD_BOT_TOKEN,
86
- slackBotToken: env.SLACK_BOT_TOKEN,
87
- slackAppToken: env.SLACK_APP_TOKEN,
88
- feishuAppId: env.FEISHU_APP_ID,
89
- feishuAppSecret: env.FEISHU_APP_SECRET,
90
- dingtalkClientId: env.DINGTALK_CLIENT_ID,
91
- dingtalkClientSecret: env.DINGTALK_CLIENT_SECRET,
92
- wecomCorpId: env.WECOM_CORP_ID,
93
- wecomSecret: env.WECOM_SECRET,
94
- wecomAgentId: env.WECOM_AGENT_ID,
95
- wecomToken: env.WECOM_TOKEN,
96
- wecomEncodingAESKey: env.WECOM_ENCODING_AES_KEY,
97
- wecomCallbackPort: env.WECOM_CALLBACK_PORT,
98
- wechatToken: env.WECHAT_TOKEN,
99
- wechatStateDir: env.WECHAT_STATE_DIR,
100
- asrApiKey: env.ASR_API_KEY,
101
- asrBaseUrl: env.ASR_BASE_URL,
102
- asrModel: env.ASR_MODEL,
103
- };
104
- }
package/src/feed.ts DELETED
@@ -1,190 +0,0 @@
1
- // RSS 订阅推送(零依赖):/feed add/list/rm + 定时轮询新条目推送到聊天。
2
- // 解析器为纯函数(RSS 2.0 子集 + 基本实体解码),可单测。
3
-
4
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
5
- import { dirname } from 'node:path';
6
- import { randomUUID } from 'node:crypto';
7
- import type { Adapter } from './adapter.js';
8
-
9
- export interface RssItem {
10
- title: string;
11
- link: string;
12
- guid: string;
13
- pubDate?: string;
14
- }
15
-
16
- function decodeEntities(text: string): string {
17
- return text
18
- .replace(/&lt;/g, '<')
19
- .replace(/&gt;/g, '>')
20
- .replace(/&quot;/g, '"')
21
- .replace(/&apos;/g, "'")
22
- .replace(/&amp;/g, '&')
23
- .replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, '$1')
24
- .trim();
25
- }
26
-
27
- /** 纯函数:RSS 2.0 XML → 条目列表(title/link/guid/pubDate)。 */
28
- export function parseRss(xml: string): RssItem[] {
29
- const items: RssItem[] = [];
30
- const itemRe = /<item>([\s\S]*?)<\/item>/gi;
31
- let m: RegExpExecArray | null;
32
- while ((m = itemRe.exec(xml)) !== null) {
33
- const block = m[1];
34
- const grab = (tag: string): string => {
35
- const r = new RegExp(`<${tag}(?:[^>]*)>([\\s\\S]*?)</${tag}>`, 'i');
36
- const mm = r.exec(block);
37
- return mm ? decodeEntities(mm[1]) : '';
38
- };
39
- const title = grab('title');
40
- const link = grab('link');
41
- if (!title && !link) continue;
42
- items.push({
43
- title,
44
- link,
45
- guid: grab('guid') || link || title,
46
- pubDate: grab('pubDate') || undefined,
47
- });
48
- }
49
- return items;
50
- }
51
-
52
- /** 纯函数:新条目摘要文本(推送用)。 */
53
- export function formatFeedItem(item: RssItem): string {
54
- const title = item.title || '(无标题)';
55
- return item.link ? `📰 ${title}\n${item.link}` : `📰 ${title}`;
56
- }
57
-
58
- export interface Feed {
59
- id: string;
60
- adapterId: string;
61
- chatId: string;
62
- url: string;
63
- /** 已推送过的最大条目 guid(增量推送游标) */
64
- lastGuid: string;
65
- addedAt: string;
66
- }
67
-
68
- /** JSON 持久化的订阅存储;file 缺省时仅内存(测试用)。 */
69
- export class FeedStore {
70
- private readonly feeds: Feed[] = [];
71
- private readonly file?: string;
72
-
73
- constructor(file?: string) {
74
- this.file = file;
75
- if (file && existsSync(file)) {
76
- try {
77
- const parsed = JSON.parse(readFileSync(file, 'utf8')) as Feed[];
78
- if (Array.isArray(parsed)) this.feeds.push(...parsed);
79
- } catch {
80
- /* 损坏则从空开始 */
81
- }
82
- }
83
- }
84
-
85
- private persist(): void {
86
- if (!this.file) return;
87
- try {
88
- mkdirSync(dirname(this.file), { recursive: true });
89
- writeFileSync(this.file, JSON.stringify(this.feeds, null, 2), 'utf8');
90
- } catch {
91
- /* 持久化失败不阻断 */
92
- }
93
- }
94
-
95
- add(adapterId: string, chatId: string, url: string): Feed {
96
- const feed: Feed = {
97
- id: randomUUID().slice(0, 8),
98
- adapterId,
99
- chatId,
100
- url,
101
- lastGuid: '',
102
- addedAt: new Date().toISOString(),
103
- };
104
- this.feeds.push(feed);
105
- this.persist();
106
- return feed;
107
- }
108
-
109
- list(): Feed[] {
110
- return [...this.feeds];
111
- }
112
-
113
- remove(id: string): boolean {
114
- const idx = this.feeds.findIndex((f) => f.id === id);
115
- if (idx < 0) return false;
116
- this.feeds.splice(idx, 1);
117
- this.persist();
118
- return true;
119
- }
120
-
121
- updateLastGuid(id: string, guid: string): void {
122
- const feed = this.feeds.find((f) => f.id === id);
123
- if (!feed) return;
124
- // 游标无条件推进:迭代顺序由轮询器控制(文档序),避免非单调 GUID 比较出错
125
- feed.lastGuid = guid;
126
- this.persist();
127
- }
128
- }
129
-
130
- export interface FeedPollerOptions {
131
- intervalMs?: number;
132
- fetchImpl?: typeof fetch;
133
- }
134
-
135
- /** 轮询器:定时抓取每个订阅源,把新条目推送到对应聊天(假定文档序最新在前)。 */
136
- export class FeedPoller {
137
- private timer?: ReturnType<typeof setInterval>;
138
- private readonly intervalMs: number;
139
- private readonly fetchImpl: typeof fetch;
140
-
141
- constructor(
142
- private readonly store: FeedStore,
143
- private readonly adapters: Map<string, Adapter>,
144
- opts: FeedPollerOptions = {},
145
- ) {
146
- this.intervalMs = opts.intervalMs ?? 5 * 60_000;
147
- this.fetchImpl = opts.fetchImpl ?? fetch;
148
- }
149
-
150
- start(): void {
151
- if (this.timer) return;
152
- this.timer = setInterval(() => void this.pollOnce(), this.intervalMs);
153
- this.timer.unref?.();
154
- }
155
-
156
- stop(): void {
157
- if (this.timer) clearInterval(this.timer);
158
- this.timer = undefined;
159
- }
160
-
161
- /** 抓一轮:每个订阅源 fetch → 解析 → 增量推送新条目。返回推送条数。 */
162
- async pollOnce(): Promise<number> {
163
- let pushed = 0;
164
- for (const feed of this.store.list()) {
165
- const adapter = this.adapters.get(feed.adapterId);
166
- if (!adapter) continue;
167
- try {
168
- const res = await this.fetchImpl(feed.url, { signal: AbortSignal.timeout(15000) });
169
- if (!res.ok) continue;
170
- const items = parseRss(await res.text());
171
- if (items.length === 0) continue;
172
- if (!feed.lastGuid) {
173
- // 首次订阅:只建立游标(最新一条),不推送历史,避免刷屏
174
- this.store.updateLastGuid(feed.id, items[0].guid);
175
- continue;
176
- }
177
- const cursor = feed.lastGuid; // 捕获轮询开始时的游标,迭代中不随推进而变化
178
- for (const item of items) {
179
- if (item.guid === cursor) break; // 游标之前的都是新条目
180
- this.store.updateLastGuid(feed.id, item.guid);
181
- await adapter.send(feed.chatId, { text: formatFeedItem(item) });
182
- pushed += 1;
183
- }
184
- } catch {
185
- /* 单源失败不影响其他源 */
186
- }
187
- }
188
- return pushed;
189
- }
190
- }