@dsh-overdrive/gateway 0.1.9 → 0.3.0
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/dist/commands.d.ts +18 -0
- package/dist/commands.js +31 -4
- package/dist/commands.js.map +1 -1
- package/dist/feed.d.ts +48 -0
- package/dist/feed.js +166 -0
- package/dist/feed.js.map +1 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.js +133 -10
- package/dist/index.js.map +1 -1
- package/dist/memory.d.ts +10 -0
- package/dist/memory.js +43 -0
- package/dist/memory.js.map +1 -1
- package/dist/mention.d.ts +13 -0
- package/dist/mention.js +43 -0
- package/dist/mention.js.map +1 -0
- package/dist/pending-buttons.d.ts +5 -1
- package/dist/pending-buttons.js +26 -8
- package/dist/pending-buttons.js.map +1 -1
- package/dist/text.d.ts +2 -0
- package/dist/text.js +24 -0
- package/dist/text.js.map +1 -0
- package/package.json +1 -1
- package/src/commands.ts +32 -6
- package/src/feed.ts +190 -0
- package/src/index.ts +137 -12
- package/src/memory.ts +43 -0
- package/src/mention.ts +51 -0
- package/src/pending-buttons.ts +26 -6
- package/src/text.ts +32 -0
- package/test/commands.test.ts +10 -0
- package/test/feed.test.ts +111 -0
- package/test/mention.test.ts +54 -0
- package/test/multi.test.ts +49 -3
- package/test/pending-buttons.test.ts +39 -0
- package/test/text.test.ts +20 -0
package/dist/commands.d.ts
CHANGED
|
@@ -13,11 +13,16 @@ export type ParsedCommand = {
|
|
|
13
13
|
kind: 'cron';
|
|
14
14
|
schedule: string;
|
|
15
15
|
prompt: string;
|
|
16
|
+
timeZone?: string;
|
|
16
17
|
} | {
|
|
17
18
|
kind: 'crons';
|
|
18
19
|
} | {
|
|
19
20
|
kind: 'cronrm';
|
|
20
21
|
taskId: string;
|
|
22
|
+
} | {
|
|
23
|
+
kind: 'context';
|
|
24
|
+
action: 'set' | 'clear' | 'show';
|
|
25
|
+
topic?: string;
|
|
21
26
|
} | {
|
|
22
27
|
kind: 'remember';
|
|
23
28
|
text: string;
|
|
@@ -37,6 +42,19 @@ export type ParsedCommand = {
|
|
|
37
42
|
path: string;
|
|
38
43
|
} | {
|
|
39
44
|
kind: 'status';
|
|
45
|
+
} | {
|
|
46
|
+
kind: 'feedadd';
|
|
47
|
+
url: string;
|
|
48
|
+
} | {
|
|
49
|
+
kind: 'feedlist';
|
|
50
|
+
} | {
|
|
51
|
+
kind: 'feedrm';
|
|
52
|
+
feedId: string;
|
|
53
|
+
} | {
|
|
54
|
+
kind: 'digest';
|
|
55
|
+
} | {
|
|
56
|
+
kind: 'digestdaily';
|
|
57
|
+
time: string;
|
|
40
58
|
};
|
|
41
59
|
export declare function parseCommand(text: string): ParsedCommand | null;
|
|
42
60
|
export declare const HELP_TEXT: string;
|
package/dist/commands.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// cron 语法:/cron <分 时 日 月 周>
|
|
2
|
-
const CRON_RE = /^\/cron\s+(\S+\s+\S+\s+\S+\s+\S+\s+\S+)\s+(
|
|
1
|
+
// cron 语法:/cron <分 时 日 月 周> <需求> [--tz <IANA时区>]
|
|
2
|
+
const CRON_RE = /^\/cron\s+(\S+\s+\S+\s+\S+\s+\S+\s+\S+)\s+(.+?)(?:\s+--tz\s+(\S+))?$/;
|
|
3
3
|
// /remind in N 分钟/小时/天 <text>(也支持 min/minutes/hour/hours/day/days);或 /remind at HH:MM <text>
|
|
4
4
|
const REMIND_IN_RE = /^\/remind\s+in\s+(\d+)\s+(\S+)\s+(.+)$/i;
|
|
5
5
|
const REMIND_AT_RE = /^\/remind\s+at\s+(\d{1,2}:\d{2})\s+(.+)$/i;
|
|
@@ -22,10 +22,19 @@ export function parseCommand(text) {
|
|
|
22
22
|
return { kind: 'task', prompt: task[1] };
|
|
23
23
|
const cron = trimmed.match(CRON_RE);
|
|
24
24
|
if (cron)
|
|
25
|
-
return { kind: 'cron', schedule: cron[1], prompt: cron[2] };
|
|
25
|
+
return { kind: 'cron', schedule: cron[1], prompt: cron[2].trim(), timeZone: cron[3] };
|
|
26
26
|
const cronrm = trimmed.match(/^\/cronrm\s+(\S+)$/);
|
|
27
27
|
if (cronrm)
|
|
28
28
|
return { kind: 'cronrm', taskId: cronrm[1] };
|
|
29
|
+
const context = trimmed.match(/^\/context\s+(.+)$/);
|
|
30
|
+
if (context) {
|
|
31
|
+
const topic = context[1].trim();
|
|
32
|
+
if (topic === 'off' || topic === '清除' || topic === 'clear')
|
|
33
|
+
return { kind: 'context', action: 'clear' };
|
|
34
|
+
return { kind: 'context', action: 'set', topic };
|
|
35
|
+
}
|
|
36
|
+
if (trimmed === '/context')
|
|
37
|
+
return { kind: 'context', action: 'show' };
|
|
29
38
|
const remember = trimmed.match(/^\/remember\s+(.+)$/);
|
|
30
39
|
if (remember)
|
|
31
40
|
return { kind: 'remember', text: remember[1] };
|
|
@@ -48,21 +57,39 @@ export function parseCommand(text) {
|
|
|
48
57
|
const send = trimmed.match(/^\/send\s+(.+)$/);
|
|
49
58
|
if (send)
|
|
50
59
|
return { kind: 'send', path: send[1].trim() };
|
|
60
|
+
if (trimmed === '/digest')
|
|
61
|
+
return { kind: 'digest' };
|
|
62
|
+
const digestDaily = trimmed.match(/^\/digest\s+daily\s+(\d{1,2}:\d{2})$/);
|
|
63
|
+
if (digestDaily)
|
|
64
|
+
return { kind: 'digestdaily', time: digestDaily[1] };
|
|
65
|
+
const feedAdd = trimmed.match(/^\/feed\s+add\s+(\S+)$/i);
|
|
66
|
+
if (feedAdd)
|
|
67
|
+
return { kind: 'feedadd', url: feedAdd[1] };
|
|
68
|
+
if (/^\/feed\s+list$/i.test(trimmed))
|
|
69
|
+
return { kind: 'feedlist' };
|
|
70
|
+
const feedRm = trimmed.match(/^\/feed\s+rm\s+(\S+)$/i);
|
|
71
|
+
if (feedRm)
|
|
72
|
+
return { kind: 'feedrm', feedId: feedRm[1] };
|
|
51
73
|
return null;
|
|
52
74
|
}
|
|
53
75
|
export const HELP_TEXT = [
|
|
54
76
|
'/help — 帮助',
|
|
55
77
|
'/trace — 查看最近一轮轨迹',
|
|
56
78
|
'/task <需求> — 派子任务',
|
|
57
|
-
'/cron <分 时 日 月 周> <需求> — 定时任务',
|
|
79
|
+
'/cron <分 时 日 月 周> <需求> [--tz 时区] — 定时任务',
|
|
58
80
|
'/crons — 查看定时任务列表',
|
|
59
81
|
'/cronrm <任务id> — 删除定时任务',
|
|
82
|
+
'/context <主题> — 绑定当前会话主题(off 清除)',
|
|
60
83
|
'/remind in 10 分钟 <提醒内容> — 一次性定时提醒(也支持 at HH:MM)',
|
|
61
84
|
'/remember <事实> — 记住关于我的事',
|
|
62
85
|
'/recall <关键词> — 回忆相关记忆',
|
|
63
86
|
'/forget <记忆id> — 删除一条记忆',
|
|
64
87
|
'/send <文件路径> — 把本地文件/图片发到当前聊天',
|
|
65
88
|
'/status — 查看运行状态',
|
|
89
|
+
'/digest — 立即生成今日摘要',
|
|
90
|
+
'/digest daily 09:00 — 每天定时生成摘要',
|
|
91
|
+
'/feed add <rss链接> — 订阅 RSS 推送',
|
|
92
|
+
'/feed list / rm <id> — 管理订阅',
|
|
66
93
|
'/agents — 查看子任务状态',
|
|
67
94
|
'/new — 重置会话',
|
|
68
95
|
].join('\n');
|
package/dist/commands.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commands.js","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"commands.js","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"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"}
|
package/dist/feed.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Adapter } from './adapter.js';
|
|
2
|
+
export interface RssItem {
|
|
3
|
+
title: string;
|
|
4
|
+
link: string;
|
|
5
|
+
guid: string;
|
|
6
|
+
pubDate?: string;
|
|
7
|
+
}
|
|
8
|
+
/** 纯函数:RSS 2.0 XML → 条目列表(title/link/guid/pubDate)。 */
|
|
9
|
+
export declare function parseRss(xml: string): RssItem[];
|
|
10
|
+
/** 纯函数:新条目摘要文本(推送用)。 */
|
|
11
|
+
export declare function formatFeedItem(item: RssItem): string;
|
|
12
|
+
export interface Feed {
|
|
13
|
+
id: string;
|
|
14
|
+
adapterId: string;
|
|
15
|
+
chatId: string;
|
|
16
|
+
url: string;
|
|
17
|
+
/** 已推送过的最大条目 guid(增量推送游标) */
|
|
18
|
+
lastGuid: string;
|
|
19
|
+
addedAt: string;
|
|
20
|
+
}
|
|
21
|
+
/** JSON 持久化的订阅存储;file 缺省时仅内存(测试用)。 */
|
|
22
|
+
export declare class FeedStore {
|
|
23
|
+
private readonly feeds;
|
|
24
|
+
private readonly file?;
|
|
25
|
+
constructor(file?: string);
|
|
26
|
+
private persist;
|
|
27
|
+
add(adapterId: string, chatId: string, url: string): Feed;
|
|
28
|
+
list(): Feed[];
|
|
29
|
+
remove(id: string): boolean;
|
|
30
|
+
updateLastGuid(id: string, guid: string): void;
|
|
31
|
+
}
|
|
32
|
+
export interface FeedPollerOptions {
|
|
33
|
+
intervalMs?: number;
|
|
34
|
+
fetchImpl?: typeof fetch;
|
|
35
|
+
}
|
|
36
|
+
/** 轮询器:定时抓取每个订阅源,把新条目推送到对应聊天(假定文档序最新在前)。 */
|
|
37
|
+
export declare class FeedPoller {
|
|
38
|
+
private readonly store;
|
|
39
|
+
private readonly adapters;
|
|
40
|
+
private timer?;
|
|
41
|
+
private readonly intervalMs;
|
|
42
|
+
private readonly fetchImpl;
|
|
43
|
+
constructor(store: FeedStore, adapters: Map<string, Adapter>, opts?: FeedPollerOptions);
|
|
44
|
+
start(): void;
|
|
45
|
+
stop(): void;
|
|
46
|
+
/** 抓一轮:每个订阅源 fetch → 解析 → 增量推送新条目。返回推送条数。 */
|
|
47
|
+
pollOnce(): Promise<number>;
|
|
48
|
+
}
|
package/dist/feed.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// RSS 订阅推送(零依赖):/feed add/list/rm + 定时轮询新条目推送到聊天。
|
|
2
|
+
// 解析器为纯函数(RSS 2.0 子集 + 基本实体解码),可单测。
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { dirname } from 'node:path';
|
|
5
|
+
import { randomUUID } from 'node:crypto';
|
|
6
|
+
function decodeEntities(text) {
|
|
7
|
+
return text
|
|
8
|
+
.replace(/</g, '<')
|
|
9
|
+
.replace(/>/g, '>')
|
|
10
|
+
.replace(/"/g, '"')
|
|
11
|
+
.replace(/'/g, "'")
|
|
12
|
+
.replace(/&/g, '&')
|
|
13
|
+
.replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, '$1')
|
|
14
|
+
.trim();
|
|
15
|
+
}
|
|
16
|
+
/** 纯函数:RSS 2.0 XML → 条目列表(title/link/guid/pubDate)。 */
|
|
17
|
+
export function parseRss(xml) {
|
|
18
|
+
const items = [];
|
|
19
|
+
const itemRe = /<item>([\s\S]*?)<\/item>/gi;
|
|
20
|
+
let m;
|
|
21
|
+
while ((m = itemRe.exec(xml)) !== null) {
|
|
22
|
+
const block = m[1];
|
|
23
|
+
const grab = (tag) => {
|
|
24
|
+
const r = new RegExp(`<${tag}(?:[^>]*)>([\\s\\S]*?)</${tag}>`, 'i');
|
|
25
|
+
const mm = r.exec(block);
|
|
26
|
+
return mm ? decodeEntities(mm[1]) : '';
|
|
27
|
+
};
|
|
28
|
+
const title = grab('title');
|
|
29
|
+
const link = grab('link');
|
|
30
|
+
if (!title && !link)
|
|
31
|
+
continue;
|
|
32
|
+
items.push({
|
|
33
|
+
title,
|
|
34
|
+
link,
|
|
35
|
+
guid: grab('guid') || link || title,
|
|
36
|
+
pubDate: grab('pubDate') || undefined,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return items;
|
|
40
|
+
}
|
|
41
|
+
/** 纯函数:新条目摘要文本(推送用)。 */
|
|
42
|
+
export function formatFeedItem(item) {
|
|
43
|
+
const title = item.title || '(无标题)';
|
|
44
|
+
return item.link ? `📰 ${title}\n${item.link}` : `📰 ${title}`;
|
|
45
|
+
}
|
|
46
|
+
/** JSON 持久化的订阅存储;file 缺省时仅内存(测试用)。 */
|
|
47
|
+
export class FeedStore {
|
|
48
|
+
feeds = [];
|
|
49
|
+
file;
|
|
50
|
+
constructor(file) {
|
|
51
|
+
this.file = file;
|
|
52
|
+
if (file && existsSync(file)) {
|
|
53
|
+
try {
|
|
54
|
+
const parsed = JSON.parse(readFileSync(file, 'utf8'));
|
|
55
|
+
if (Array.isArray(parsed))
|
|
56
|
+
this.feeds.push(...parsed);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
/* 损坏则从空开始 */
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
persist() {
|
|
64
|
+
if (!this.file)
|
|
65
|
+
return;
|
|
66
|
+
try {
|
|
67
|
+
mkdirSync(dirname(this.file), { recursive: true });
|
|
68
|
+
writeFileSync(this.file, JSON.stringify(this.feeds, null, 2), 'utf8');
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
/* 持久化失败不阻断 */
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
add(adapterId, chatId, url) {
|
|
75
|
+
const feed = {
|
|
76
|
+
id: randomUUID().slice(0, 8),
|
|
77
|
+
adapterId,
|
|
78
|
+
chatId,
|
|
79
|
+
url,
|
|
80
|
+
lastGuid: '',
|
|
81
|
+
addedAt: new Date().toISOString(),
|
|
82
|
+
};
|
|
83
|
+
this.feeds.push(feed);
|
|
84
|
+
this.persist();
|
|
85
|
+
return feed;
|
|
86
|
+
}
|
|
87
|
+
list() {
|
|
88
|
+
return [...this.feeds];
|
|
89
|
+
}
|
|
90
|
+
remove(id) {
|
|
91
|
+
const idx = this.feeds.findIndex((f) => f.id === id);
|
|
92
|
+
if (idx < 0)
|
|
93
|
+
return false;
|
|
94
|
+
this.feeds.splice(idx, 1);
|
|
95
|
+
this.persist();
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
updateLastGuid(id, guid) {
|
|
99
|
+
const feed = this.feeds.find((f) => f.id === id);
|
|
100
|
+
if (!feed)
|
|
101
|
+
return;
|
|
102
|
+
// 游标无条件推进:迭代顺序由轮询器控制(文档序),避免非单调 GUID 比较出错
|
|
103
|
+
feed.lastGuid = guid;
|
|
104
|
+
this.persist();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/** 轮询器:定时抓取每个订阅源,把新条目推送到对应聊天(假定文档序最新在前)。 */
|
|
108
|
+
export class FeedPoller {
|
|
109
|
+
store;
|
|
110
|
+
adapters;
|
|
111
|
+
timer;
|
|
112
|
+
intervalMs;
|
|
113
|
+
fetchImpl;
|
|
114
|
+
constructor(store, adapters, opts = {}) {
|
|
115
|
+
this.store = store;
|
|
116
|
+
this.adapters = adapters;
|
|
117
|
+
this.intervalMs = opts.intervalMs ?? 5 * 60_000;
|
|
118
|
+
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
119
|
+
}
|
|
120
|
+
start() {
|
|
121
|
+
if (this.timer)
|
|
122
|
+
return;
|
|
123
|
+
this.timer = setInterval(() => void this.pollOnce(), this.intervalMs);
|
|
124
|
+
this.timer.unref?.();
|
|
125
|
+
}
|
|
126
|
+
stop() {
|
|
127
|
+
if (this.timer)
|
|
128
|
+
clearInterval(this.timer);
|
|
129
|
+
this.timer = undefined;
|
|
130
|
+
}
|
|
131
|
+
/** 抓一轮:每个订阅源 fetch → 解析 → 增量推送新条目。返回推送条数。 */
|
|
132
|
+
async pollOnce() {
|
|
133
|
+
let pushed = 0;
|
|
134
|
+
for (const feed of this.store.list()) {
|
|
135
|
+
const adapter = this.adapters.get(feed.adapterId);
|
|
136
|
+
if (!adapter)
|
|
137
|
+
continue;
|
|
138
|
+
try {
|
|
139
|
+
const res = await this.fetchImpl(feed.url, { signal: AbortSignal.timeout(15000) });
|
|
140
|
+
if (!res.ok)
|
|
141
|
+
continue;
|
|
142
|
+
const items = parseRss(await res.text());
|
|
143
|
+
if (items.length === 0)
|
|
144
|
+
continue;
|
|
145
|
+
if (!feed.lastGuid) {
|
|
146
|
+
// 首次订阅:只建立游标(最新一条),不推送历史,避免刷屏
|
|
147
|
+
this.store.updateLastGuid(feed.id, items[0].guid);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const cursor = feed.lastGuid; // 捕获轮询开始时的游标,迭代中不随推进而变化
|
|
151
|
+
for (const item of items) {
|
|
152
|
+
if (item.guid === cursor)
|
|
153
|
+
break; // 游标之前的都是新条目
|
|
154
|
+
this.store.updateLastGuid(feed.id, item.guid);
|
|
155
|
+
await adapter.send(feed.chatId, { text: formatFeedItem(item) });
|
|
156
|
+
pushed += 1;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
/* 单源失败不影响其他源 */
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return pushed;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
//# sourceMappingURL=feed.js.map
|
package/dist/feed.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"feed.js","sourceRoot":"","sources":["../src/feed.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,8 @@ import { GatewayClient, type ServerEvent } from '@dsh-overdrive/sdk';
|
|
|
2
2
|
import type { Adapter, OutboundPayload } from './adapter.js';
|
|
3
3
|
import { CliAdapter } from './adapters/cli.js';
|
|
4
4
|
import { type AsrTranscriber } from './asr.js';
|
|
5
|
-
import { MemoryStore } from './memory.js';
|
|
5
|
+
import { MemoryStore, TopicStore } from './memory.js';
|
|
6
|
+
import { FeedStore } from './feed.js';
|
|
6
7
|
/**
|
|
7
8
|
* message.delta → 打字指示去重:同一 turn 内首个 delta 触发一次 typing,
|
|
8
9
|
* complete 后复位,下一 turn 可再次触发。纯逻辑,便于单测。
|
|
@@ -27,6 +28,14 @@ export interface WireOptions {
|
|
|
27
28
|
memory?: MemoryStore;
|
|
28
29
|
/** 人设(persona):每条用户消息前注入的固定上下文,如「你是一个毒舌但贴心的私人助理」。 */
|
|
29
30
|
persona?: string;
|
|
31
|
+
/** RSS 订阅存储(/feed 命令)。 */
|
|
32
|
+
feed?: FeedStore;
|
|
33
|
+
/** 群聊提及策略:true 时群聊中仅在被提及/回复时响应(私聊始终响应)。 */
|
|
34
|
+
requireMention?: boolean;
|
|
35
|
+
/** 机器人身份(telegram @用户名 / discord·slack 用户ID / whatsapp 号码)。 */
|
|
36
|
+
botIdentity?: string;
|
|
37
|
+
/** 会话主题存储(/context)。 */
|
|
38
|
+
topics?: TopicStore;
|
|
30
39
|
}
|
|
31
40
|
/** 纯函数:一次性提醒的时间 → cron 5 字段表达式(分钟精度)。 */
|
|
32
41
|
export declare function remindSchedule(minutes: number, atTime: string | null, now?: Date): string;
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
|
-
import { basename } from 'node:path';
|
|
1
|
+
import { existsSync, writeFileSync, rmSync } from 'node:fs';
|
|
2
|
+
import { basename, join } from 'node:path';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
3
4
|
import { GatewayClient } from '@dsh-overdrive/sdk';
|
|
4
5
|
import { Allowlist, buildSessionKey } from './session.js';
|
|
5
6
|
import { adapterEnvFromProcess, createAdapter, parseAdapterIds } from './config.js';
|
|
@@ -8,7 +9,10 @@ import { parseCommand, HELP_TEXT } from './commands.js';
|
|
|
8
9
|
import { TrajectoryAggregator, formatTrajectorySummary } from './trajectory.js';
|
|
9
10
|
import { createStatusServer } from './status.js';
|
|
10
11
|
import { createTranscriber } from './asr.js';
|
|
11
|
-
import { MemoryStore, memoryScope, formatMemories, extractAutoMemories } from './memory.js';
|
|
12
|
+
import { MemoryStore, TopicStore, memoryScope, formatMemories, extractAutoMemories } from './memory.js';
|
|
13
|
+
import { FeedStore, FeedPoller } from './feed.js';
|
|
14
|
+
import { shouldRespond } from './mention.js';
|
|
15
|
+
import { chunkLongText } from './text.js';
|
|
12
16
|
/**
|
|
13
17
|
* message.delta → 打字指示去重:同一 turn 内首个 delta 触发一次 typing,
|
|
14
18
|
* complete 后复位,下一 turn 可再次触发。纯逻辑,便于单测。
|
|
@@ -85,7 +89,7 @@ export function mediaKindFromPath(path) {
|
|
|
85
89
|
return 'file';
|
|
86
90
|
}
|
|
87
91
|
/** 命令面分发:/trace /new /task /cron /agents /help /remind /remember /recall /forget(M4 + v0.3)。 */
|
|
88
|
-
async function handleCommand(adapter, client, command, sessionId, chatId, aggregator, memory) {
|
|
92
|
+
async function handleCommand(adapter, client, command, sessionId, chatId, aggregator, memory, feed, topics) {
|
|
89
93
|
switch (command.kind) {
|
|
90
94
|
case 'trace': {
|
|
91
95
|
const summary = aggregator.recentSummary(sessionId);
|
|
@@ -103,8 +107,27 @@ async function handleCommand(adapter, client, command, sessionId, chatId, aggreg
|
|
|
103
107
|
return;
|
|
104
108
|
}
|
|
105
109
|
case 'cron': {
|
|
106
|
-
await client.createTask({ sessionId, kind: 'cron', prompt: command.prompt, schedule: command.schedule });
|
|
107
|
-
await adapter.send(chatId, { text:
|
|
110
|
+
await client.createTask({ sessionId, kind: 'cron', prompt: command.prompt, schedule: command.schedule, timeZone: command.timeZone });
|
|
111
|
+
await adapter.send(chatId, { text: `⏰ 定时任务已注册${command.timeZone ? `(时区 ${command.timeZone})` : ''}` });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
case 'context': {
|
|
115
|
+
if (!topics) {
|
|
116
|
+
await adapter.send(chatId, { text: '会话主题未启用。' });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (command.action === 'set' && command.topic) {
|
|
120
|
+
topics.set(sessionId, command.topic);
|
|
121
|
+
await adapter.send(chatId, { text: `📌 会话主题已绑定:${command.topic}` });
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (command.action === 'clear') {
|
|
125
|
+
const ok = topics.clear(sessionId);
|
|
126
|
+
await adapter.send(chatId, { text: ok ? '📌 会话主题已清除' : '当前没有会话主题。' });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const current = topics.get(sessionId);
|
|
130
|
+
await adapter.send(chatId, { text: current ? `📌 当前会话主题:${current}` : '当前没有会话主题。用 /context <主题> 绑定。' });
|
|
108
131
|
return;
|
|
109
132
|
}
|
|
110
133
|
case 'crons': {
|
|
@@ -203,6 +226,57 @@ async function handleCommand(adapter, client, command, sessionId, chatId, aggreg
|
|
|
203
226
|
});
|
|
204
227
|
return;
|
|
205
228
|
}
|
|
229
|
+
case 'digest': {
|
|
230
|
+
const scope = memoryScope(adapter.id, sessionId.split(':')[2] ?? '');
|
|
231
|
+
const mems = memory ? memory.list(scope) : [];
|
|
232
|
+
const crons = await client.listTasks();
|
|
233
|
+
await adapter.send(chatId, {
|
|
234
|
+
text: [
|
|
235
|
+
`📋 今日摘要`,
|
|
236
|
+
`- 你的记忆: ${mems.length} 条${mems.slice(0, 5).map((e) => `\n · ${e.text}`).join('')}`,
|
|
237
|
+
`- 定时任务: ${crons.tasks.length} 个`,
|
|
238
|
+
`- 订阅源: ${feed ? feed.list().filter((f) => f.chatId === chatId).length : 0} 个`,
|
|
239
|
+
].join('\n'),
|
|
240
|
+
});
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
case 'digestdaily': {
|
|
244
|
+
const schedule = remindSchedule(0, command.time);
|
|
245
|
+
await client.createTask({ sessionId, kind: 'cron', prompt: '⏰ 每日摘要:请基于今天的对话输出一份简短摘要。', schedule });
|
|
246
|
+
await adapter.send(chatId, { text: `📋 已设置每日摘要(${command.time} 触发)` });
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
case 'feedadd': {
|
|
250
|
+
if (!feed) {
|
|
251
|
+
await adapter.send(chatId, { text: 'RSS 订阅未启用。' });
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const entry = feed.add(adapter.id, chatId, command.url);
|
|
255
|
+
await adapter.send(chatId, { text: `✅ 已订阅 RSS(\`${entry.id}\`):${command.url}` });
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
case 'feedlist': {
|
|
259
|
+
if (!feed) {
|
|
260
|
+
await adapter.send(chatId, { text: 'RSS 订阅未启用。' });
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const mine = feed.list().filter((f) => f.chatId === chatId);
|
|
264
|
+
await adapter.send(chatId, {
|
|
265
|
+
text: mine.length
|
|
266
|
+
? `📡 订阅(${mine.length}):\n` + mine.map((f) => `- \`${f.id}\` ${f.url}`).join('\n')
|
|
267
|
+
: '暂无订阅。用 /feed add <rss链接> 添加。',
|
|
268
|
+
});
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
case 'feedrm': {
|
|
272
|
+
if (!feed) {
|
|
273
|
+
await adapter.send(chatId, { text: 'RSS 订阅未启用。' });
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const ok = feed.remove(command.feedId);
|
|
277
|
+
await adapter.send(chatId, { text: ok ? `🗑️ 已删除订阅 \`${command.feedId}\`` : `未找到订阅 \`${command.feedId}\`` });
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
206
280
|
case 'help': {
|
|
207
281
|
await adapter.send(chatId, { text: HELP_TEXT });
|
|
208
282
|
return;
|
|
@@ -224,12 +298,24 @@ export async function wireAdapter(adapter, client, opts) {
|
|
|
224
298
|
return;
|
|
225
299
|
}
|
|
226
300
|
chatIds.set(key, msg.chatId);
|
|
301
|
+
// 群聊提及策略(对齐竞品):群聊中仅被提及/回复时响应;私聊始终响应
|
|
302
|
+
if (opts.requireMention && !shouldRespond(adapter.id, msg, { requireMention: true, botIdentity: opts.botIdentity ?? '' })) {
|
|
303
|
+
console.log(`[gateway][${adapter.id}] 群聊未提及,跳过: chat=${msg.chatId}`);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
227
306
|
const command = parseCommand(msg.text);
|
|
228
307
|
if (command) {
|
|
229
308
|
console.log(`[gateway][${adapter.id}] 命令: ${JSON.stringify(command)}`);
|
|
230
|
-
await handleCommand(adapter, client, command, key, msg.chatId, aggregator, opts.memory);
|
|
309
|
+
await handleCommand(adapter, client, command, key, msg.chatId, aggregator, opts.memory, opts.feed, opts.topics);
|
|
231
310
|
return;
|
|
232
311
|
}
|
|
312
|
+
// 会话主题注入(/context)
|
|
313
|
+
if (opts.topics) {
|
|
314
|
+
const topic = opts.topics.get(key);
|
|
315
|
+
if (topic) {
|
|
316
|
+
msg.text = `【会话主题】${topic}\n${msg.text}`;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
233
319
|
// OpenClaw 式记忆注入:入站消息前检索相关记忆,拼到文本后让 agent「记得你」
|
|
234
320
|
if (opts.memory) {
|
|
235
321
|
const scope = memoryScope(adapter.id, msg.userId);
|
|
@@ -308,14 +394,37 @@ export async function wireAdapter(adapter, client, opts) {
|
|
|
308
394
|
else if (ev.type === 'message.complete' || ev.type === 'error') {
|
|
309
395
|
deltas.onComplete(ev.sessionId);
|
|
310
396
|
}
|
|
397
|
+
// 自动发送:agent 产出的文件(file.created,base64)→ 写临时文件 → 发到聊天 → 清理
|
|
398
|
+
if (ev.type === 'file.created') {
|
|
399
|
+
const chatId = chatIdFor(ev.sessionId);
|
|
400
|
+
const tmp = join(tmpdir(), `dsh-out-${Date.now()}-${Math.random().toString(36).slice(2, 6)}-${ev.name}`);
|
|
401
|
+
try {
|
|
402
|
+
writeFileSync(tmp, Buffer.from(ev.data, 'base64'));
|
|
403
|
+
}
|
|
404
|
+
catch (error) {
|
|
405
|
+
console.error(`[gateway][${adapter.id}] 写入自动发送临时文件失败: ${error instanceof Error ? error.message : String(error)}`);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
void adapter.send(chatId, { text: `📎 ${ev.name}`, media: { kind: ev.kind, path: tmp, caption: ev.name } })
|
|
409
|
+
.then(() => console.log(`[gateway][${adapter.id}] 已自动发送 ${ev.name} 到 ${chatId}`), (error) => console.error(`[gateway][${adapter.id}] 自动发送失败 ${ev.name}: ${error instanceof Error ? error.message : String(error)}`))
|
|
410
|
+
.finally(() => rmSync(tmp, { force: true }));
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
311
413
|
// 轨迹聚合:trajectory.step 攒批,idle 时产出 trajectory.summary(减少刷屏)
|
|
312
414
|
aggregator.onEvent(ev, (out) => {
|
|
313
415
|
const planned = planOutbound(out);
|
|
314
416
|
if (!planned)
|
|
315
417
|
return;
|
|
316
418
|
const chatId = chatIdFor(out.sessionId);
|
|
317
|
-
|
|
318
|
-
|
|
419
|
+
// 长回复智能分片(对齐竞品):>1500 字按换行/句号断行,带(i/n)序号
|
|
420
|
+
const chunks = chunkLongText(planned.payload.text);
|
|
421
|
+
chunks.forEach((chunk, i) => {
|
|
422
|
+
const payload = i === 0
|
|
423
|
+
? planned.payload
|
|
424
|
+
: { text: chunk };
|
|
425
|
+
console.log(`[gateway][${adapter.id}] 事件 ${out.type} -> 发送到 ${chatId}${chunks.length > 1 ? `(${i + 1}/${chunks.length})` : ''}`);
|
|
426
|
+
void adapter.send(chatId, payload).then(() => console.log(`[gateway][${adapter.id}] 已发送 ${out.type} 到 ${chatId}`), (error) => console.error(`[gateway][${adapter.id}] 发送失败 ${out.type}: ${error instanceof Error ? error.message : String(error)}`));
|
|
427
|
+
});
|
|
319
428
|
});
|
|
320
429
|
});
|
|
321
430
|
}
|
|
@@ -336,14 +445,28 @@ async function main() {
|
|
|
336
445
|
console.log('[gateway] ASR 语音转写已启用');
|
|
337
446
|
const memory = new MemoryStore(process.env.MEMORY_FILE ?? 'data/memory.json');
|
|
338
447
|
console.log(`[gateway] 记忆系统已启用(文件: ${process.env.MEMORY_FILE ?? 'data/memory.json'})`);
|
|
448
|
+
const feedStore = new FeedStore(process.env.FEED_FILE ?? 'data/feeds.json');
|
|
449
|
+
const topics = new TopicStore(process.env.TOPIC_FILE ?? 'data/topics.json');
|
|
450
|
+
const requireMention = process.env.GROUP_MENTION === '1';
|
|
451
|
+
if (requireMention)
|
|
452
|
+
console.log('[gateway] 群聊提及模式已启用(群聊仅在被提及/回复时响应)');
|
|
339
453
|
const client = new GatewayClient(dshBaseUrl, dshToken);
|
|
340
454
|
await client.health(); // 确认 DSH 侧(或 mock)活着
|
|
341
455
|
const adapters = adapterIds.map((id) => createAdapter(id, env));
|
|
456
|
+
const adaptersMap = new Map(adapters.map((a) => [a.id, a]));
|
|
457
|
+
const feedPoller = new FeedPoller(feedStore, adaptersMap);
|
|
458
|
+
feedPoller.start();
|
|
459
|
+
console.log('[gateway] RSS 订阅轮询已启动');
|
|
342
460
|
for (const adapter of adapters) {
|
|
343
461
|
await adapter.connect();
|
|
344
462
|
// 人设:PERSONA_<ADAPTER_ID>(如 PERSONA_TELEGRAM)优先,回退 PERSONA
|
|
345
463
|
const persona = process.env[`PERSONA_${adapter.id.toUpperCase()}`] ?? process.env.PERSONA;
|
|
346
|
-
|
|
464
|
+
// 机器人身份:BOT_IDENTITY_<ADAPTER_ID> 优先,回退 BOT_IDENTITY
|
|
465
|
+
const botIdentity = process.env[`BOT_IDENTITY_${adapter.id.toUpperCase()}`] ?? process.env.BOT_IDENTITY ?? '';
|
|
466
|
+
await wireAdapter(adapter, client, {
|
|
467
|
+
allowlist, allowAll, asr, memory, persona, feed: feedStore, topics,
|
|
468
|
+
requireMention, botIdentity,
|
|
469
|
+
});
|
|
347
470
|
console.log(`[gateway] ${adapter.id} 适配器已就绪${persona ? `(人设: ${persona.slice(0, 20)}…)` : ''}`);
|
|
348
471
|
}
|
|
349
472
|
const consolePort = Number(process.env.GATEWAY_CONSOLE_PORT ?? 3190);
|