@mininglamp-oss/cc-channel-octo 1.0.1-dev.0ac574a
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/CHANGELOG.md +361 -0
- package/LICENSE +191 -0
- package/README.md +577 -0
- package/config.bot.example.json +15 -0
- package/config.example.json +33 -0
- package/dist/agent-bridge.d.ts +91 -0
- package/dist/agent-bridge.js +397 -0
- package/dist/agent-bridge.js.map +1 -0
- package/dist/cli.d.ts +109 -0
- package/dist/cli.js +467 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands.d.ts +57 -0
- package/dist/commands.js +121 -0
- package/dist/commands.js.map +1 -0
- package/dist/config.d.ts +294 -0
- package/dist/config.js +344 -0
- package/dist/config.js.map +1 -0
- package/dist/configure.d.ts +11 -0
- package/dist/configure.js +106 -0
- package/dist/configure.js.map +1 -0
- package/dist/cron-evaluator.d.ts +53 -0
- package/dist/cron-evaluator.js +191 -0
- package/dist/cron-evaluator.js.map +1 -0
- package/dist/cron-fire-marker.d.ts +24 -0
- package/dist/cron-fire-marker.js +25 -0
- package/dist/cron-fire-marker.js.map +1 -0
- package/dist/cron-scheduler.d.ts +46 -0
- package/dist/cron-scheduler.js +114 -0
- package/dist/cron-scheduler.js.map +1 -0
- package/dist/cron-store.d.ts +62 -0
- package/dist/cron-store.js +63 -0
- package/dist/cron-store.js.map +1 -0
- package/dist/cron-tool.d.ts +44 -0
- package/dist/cron-tool.js +151 -0
- package/dist/cron-tool.js.map +1 -0
- package/dist/cwd-resolver.d.ts +72 -0
- package/dist/cwd-resolver.js +166 -0
- package/dist/cwd-resolver.js.map +1 -0
- package/dist/db-adapter.d.ts +21 -0
- package/dist/db-adapter.js +64 -0
- package/dist/db-adapter.js.map +1 -0
- package/dist/file-inline-wrap.d.ts +94 -0
- package/dist/file-inline-wrap.js +243 -0
- package/dist/file-inline-wrap.js.map +1 -0
- package/dist/gateway.d.ts +105 -0
- package/dist/gateway.js +425 -0
- package/dist/gateway.js.map +1 -0
- package/dist/group-config.d.ts +41 -0
- package/dist/group-config.js +104 -0
- package/dist/group-config.js.map +1 -0
- package/dist/group-context.d.ts +81 -0
- package/dist/group-context.js +466 -0
- package/dist/group-context.js.map +1 -0
- package/dist/inbound.d.ts +136 -0
- package/dist/inbound.js +667 -0
- package/dist/inbound.js.map +1 -0
- package/dist/index.d.ts +65 -0
- package/dist/index.js +1026 -0
- package/dist/index.js.map +1 -0
- package/dist/media-inbound.d.ts +38 -0
- package/dist/media-inbound.js +131 -0
- package/dist/media-inbound.js.map +1 -0
- package/dist/mention-utils.d.ts +108 -0
- package/dist/mention-utils.js +199 -0
- package/dist/mention-utils.js.map +1 -0
- package/dist/octo/api.d.ts +148 -0
- package/dist/octo/api.js +320 -0
- package/dist/octo/api.js.map +1 -0
- package/dist/octo/socket.d.ts +102 -0
- package/dist/octo/socket.js +793 -0
- package/dist/octo/socket.js.map +1 -0
- package/dist/octo/types.d.ts +126 -0
- package/dist/octo/types.js +35 -0
- package/dist/octo/types.js.map +1 -0
- package/dist/prompt-safety.d.ts +78 -0
- package/dist/prompt-safety.js +148 -0
- package/dist/prompt-safety.js.map +1 -0
- package/dist/session-router.d.ts +144 -0
- package/dist/session-router.js +490 -0
- package/dist/session-router.js.map +1 -0
- package/dist/session-store.d.ts +89 -0
- package/dist/session-store.js +297 -0
- package/dist/session-store.js.map +1 -0
- package/dist/skill-linker.d.ts +31 -0
- package/dist/skill-linker.js +160 -0
- package/dist/skill-linker.js.map +1 -0
- package/dist/stream-relay.d.ts +42 -0
- package/dist/stream-relay.js +243 -0
- package/dist/stream-relay.js.map +1 -0
- package/dist/url-policy.d.ts +103 -0
- package/dist/url-policy.js +290 -0
- package/dist/url-policy.js.map +1 -0
- package/package.json +79 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Group Context — group chat message cache + maxContextChars budget + mention mapping.
|
|
3
|
+
*/
|
|
4
|
+
import type { DbAdapter } from './db-adapter.js';
|
|
5
|
+
export declare class GroupContext {
|
|
6
|
+
private readonly messageCache;
|
|
7
|
+
private readonly memberMapByChannel;
|
|
8
|
+
private readonly nameToUidByChannel;
|
|
9
|
+
private readonly robotFlags;
|
|
10
|
+
private readonly lastRefresh;
|
|
11
|
+
private readonly adapter;
|
|
12
|
+
private readonly maxContextChars;
|
|
13
|
+
private readonly maxWindowSize;
|
|
14
|
+
private upsertMember;
|
|
15
|
+
private deleteMember;
|
|
16
|
+
private selectMembers;
|
|
17
|
+
private insertMessage;
|
|
18
|
+
private selectRecentMessages;
|
|
19
|
+
private deleteOldMessages;
|
|
20
|
+
private selectMessagesSince;
|
|
21
|
+
private selectMaxId;
|
|
22
|
+
private upsertCursor;
|
|
23
|
+
private selectCursor;
|
|
24
|
+
constructor(adapter: DbAdapter, maxContextChars: number);
|
|
25
|
+
private initStatements;
|
|
26
|
+
private getMemberMap;
|
|
27
|
+
private getNameToUid;
|
|
28
|
+
pushMessage(channelId: string, fromUid: string, fromName: string, content: string, timestamp: number): void;
|
|
29
|
+
learnMember(channelId: string, uid: string, name: string): void;
|
|
30
|
+
refreshMembers(channelId: string, apiUrl: string, botToken: string): Promise<void>;
|
|
31
|
+
fetchAndLearnUser(uid: string, channelId: string, apiUrl: string, botToken: string): Promise<string | undefined>;
|
|
32
|
+
buildContext(channelId: string): string;
|
|
33
|
+
/** Read the per-channel consumption cursor (highest already-injected id), or 0. */
|
|
34
|
+
getContextCursor(channelId: string): number;
|
|
35
|
+
/** Advance the per-channel cursor to `lastId` (monotonic — never moves backward). */
|
|
36
|
+
setContextCursor(channelId: string, lastId: number): void;
|
|
37
|
+
/** The highest group_messages.id for a channel (for cursor priming), or 0. */
|
|
38
|
+
getMaxMessageId(channelId: string): number;
|
|
39
|
+
/**
|
|
40
|
+
* Build a group-context block from messages NEWER than `sinceId` (the delta the
|
|
41
|
+
* model hasn't seen yet), within `maxContextChars`. Returns the block text (same
|
|
42
|
+
* `[Recent group messages]` format as buildContext) and the highest id that
|
|
43
|
+
* EXISTS in the channel above `sinceId` (so the caller advances the cursor past
|
|
44
|
+
* the whole delta, including any oldest lines the char budget dropped — those
|
|
45
|
+
* are the least-relevant and are intentionally not re-shown). Empty text + the
|
|
46
|
+
* unchanged cursor when there's nothing new.
|
|
47
|
+
*
|
|
48
|
+
* Rows are fetched newest-first (so a backlog larger than the budget keeps the
|
|
49
|
+
* most-recent messages); the selected slice is reversed back to chronological
|
|
50
|
+
* order for display. Unlike buildContext (in-memory rolling window), this reads
|
|
51
|
+
* from the DB so the cursor delta is exact even across restarts / window eviction.
|
|
52
|
+
*/
|
|
53
|
+
buildContextSince(channelId: string, sinceId: number): {
|
|
54
|
+
text: string;
|
|
55
|
+
lastId: number;
|
|
56
|
+
};
|
|
57
|
+
resolveMentions(text: string, channelId: string): string[];
|
|
58
|
+
getName(uid: string, channelId: string): string | undefined;
|
|
59
|
+
/** G23: Check the server-authoritative robot flag for a group member. */
|
|
60
|
+
isRobot(channelId: string, uid: string): boolean | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* A8 (#143): true iff `uid` is a current member of `channelId` per the cached
|
|
63
|
+
* member list. The list is kept authoritative by refreshMembers (it prunes
|
|
64
|
+
* departed members, not just upserts) — but it is only best-effort fresh:
|
|
65
|
+
* refresh is throttled to REFRESH_INTERVAL_MS and seeded from DB on restart,
|
|
66
|
+
* so a membership change inside that window may not be reflected yet. Used by
|
|
67
|
+
* the outbound mention guard as defense-in-depth (the server still enforces
|
|
68
|
+
* real permissions), NOT as a real-time authority.
|
|
69
|
+
*/
|
|
70
|
+
isMember(channelId: string, uid: string): boolean;
|
|
71
|
+
/**
|
|
72
|
+
* A8 (#143): the channel's displayName→uid map, for v1 `@name` outbound
|
|
73
|
+
* resolution in StreamRelay.deliver. Returns the live map (empty if the
|
|
74
|
+
* channel has no cached members yet). Read-only use by callers.
|
|
75
|
+
*/
|
|
76
|
+
getNameToUidMap(channelId: string): Map<string, string>;
|
|
77
|
+
loadMembersFromDb(channelId: string): void;
|
|
78
|
+
loadMessagesFromDb(channelId: string): void;
|
|
79
|
+
/** Load all persisted members and messages from DB (call once at startup). */
|
|
80
|
+
loadAllFromDb(): void;
|
|
81
|
+
}
|
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Group Context — group chat message cache + maxContextChars budget + mention mapping.
|
|
3
|
+
*/
|
|
4
|
+
import { getGroupMembers, fetchUserInfo } from './octo/api.js';
|
|
5
|
+
import { sanitizeDisplayName } from './prompt-safety.js';
|
|
6
|
+
const REFRESH_INTERVAL_MS = 60 * 60 * 1000;
|
|
7
|
+
export class GroupContext {
|
|
8
|
+
messageCache = new Map();
|
|
9
|
+
// Per-channel member maps to avoid cross-group name collisions
|
|
10
|
+
memberMapByChannel = new Map(); // channelId → uid → name
|
|
11
|
+
nameToUidByChannel = new Map(); // channelId → name → uid
|
|
12
|
+
// G23: per-channel robot flag map (channelId → uid → isRobot).
|
|
13
|
+
// Populated from refreshMembers; stored for future routing integrations
|
|
14
|
+
// (e.g. 免@ gate that should treat bot members differently).
|
|
15
|
+
robotFlags = new Map();
|
|
16
|
+
lastRefresh = new Map();
|
|
17
|
+
adapter;
|
|
18
|
+
maxContextChars;
|
|
19
|
+
maxWindowSize;
|
|
20
|
+
upsertMember;
|
|
21
|
+
deleteMember;
|
|
22
|
+
selectMembers;
|
|
23
|
+
insertMessage;
|
|
24
|
+
selectRecentMessages;
|
|
25
|
+
deleteOldMessages;
|
|
26
|
+
selectMessagesSince;
|
|
27
|
+
selectMaxId;
|
|
28
|
+
upsertCursor;
|
|
29
|
+
selectCursor;
|
|
30
|
+
constructor(adapter, maxContextChars) {
|
|
31
|
+
this.adapter = adapter;
|
|
32
|
+
this.maxContextChars = maxContextChars;
|
|
33
|
+
this.maxWindowSize = 100;
|
|
34
|
+
this.initStatements();
|
|
35
|
+
}
|
|
36
|
+
initStatements() {
|
|
37
|
+
this.adapter.exec(`
|
|
38
|
+
CREATE TABLE IF NOT EXISTS group_messages (
|
|
39
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
40
|
+
channel_id TEXT NOT NULL,
|
|
41
|
+
from_uid TEXT NOT NULL,
|
|
42
|
+
from_name TEXT NOT NULL,
|
|
43
|
+
content TEXT NOT NULL,
|
|
44
|
+
timestamp INTEGER NOT NULL
|
|
45
|
+
)
|
|
46
|
+
`);
|
|
47
|
+
this.adapter.exec(`
|
|
48
|
+
CREATE INDEX IF NOT EXISTS idx_group_messages_channel
|
|
49
|
+
ON group_messages (channel_id, id DESC)
|
|
50
|
+
`);
|
|
51
|
+
// Per-channel consumption cursor: the highest group_messages.id that has
|
|
52
|
+
// already been injected into a turn for this channel. Only group messages
|
|
53
|
+
// NEWER than this are injected next turn (the bot's standing context lives in
|
|
54
|
+
// the SDK session, so re-injecting the whole window every turn would be both
|
|
55
|
+
// redundant and a frozen-prompt violation). Mirrors the reset_barriers /
|
|
56
|
+
// sdk_sessions single-row-per-key pattern.
|
|
57
|
+
this.adapter.exec(`
|
|
58
|
+
CREATE TABLE IF NOT EXISTS group_context_cursors (
|
|
59
|
+
channel_id TEXT PRIMARY KEY,
|
|
60
|
+
last_id INTEGER NOT NULL,
|
|
61
|
+
updated_at INTEGER NOT NULL
|
|
62
|
+
)
|
|
63
|
+
`);
|
|
64
|
+
this.upsertMember = this.adapter.prepare('INSERT INTO group_members (group_id, uid, name, updated_at) VALUES (?, ?, ?, ?) ON CONFLICT(group_id, uid) DO UPDATE SET name = excluded.name, updated_at = excluded.updated_at');
|
|
65
|
+
this.deleteMember = this.adapter.prepare('DELETE FROM group_members WHERE group_id = ? AND uid = ?');
|
|
66
|
+
this.selectMembers = this.adapter.prepare('SELECT uid, name FROM group_members WHERE group_id = ?');
|
|
67
|
+
this.insertMessage = this.adapter.prepare('INSERT INTO group_messages (channel_id, from_uid, from_name, content, timestamp) VALUES (?, ?, ?, ?, ?)');
|
|
68
|
+
this.selectRecentMessages = this.adapter.prepare('SELECT from_uid, from_name, content, timestamp FROM group_messages WHERE channel_id = ? ORDER BY id DESC LIMIT ?');
|
|
69
|
+
this.deleteOldMessages = this.adapter.prepare('DELETE FROM group_messages WHERE channel_id = ? AND id NOT IN (SELECT id FROM group_messages WHERE channel_id = ? ORDER BY id DESC LIMIT ?)');
|
|
70
|
+
// Cursor delta: messages strictly newer than the cursor, NEWEST-first so a
|
|
71
|
+
// backlog larger than the fetch limit keeps the most-recent messages (the
|
|
72
|
+
// relevant ones) rather than ancient ones. LIMIT (maxWindowSize=100) is <=
|
|
73
|
+
// deleteOldMessages' retained 200, so every fetchable row survives trimming.
|
|
74
|
+
// buildContextSince re-sorts the budget-selected slice into chronological
|
|
75
|
+
// order for display. Mirrors the in-memory buildContext rolling-window.
|
|
76
|
+
this.selectMessagesSince = this.adapter.prepare('SELECT id, from_name, content FROM group_messages WHERE channel_id = ? AND id > ? ORDER BY id DESC LIMIT ?');
|
|
77
|
+
this.selectMaxId = this.adapter.prepare('SELECT MAX(id) AS maxId FROM group_messages WHERE channel_id = ?');
|
|
78
|
+
this.upsertCursor = this.adapter.prepare('INSERT INTO group_context_cursors (channel_id, last_id, updated_at) VALUES (?, ?, ?) ' +
|
|
79
|
+
'ON CONFLICT(channel_id) DO UPDATE SET last_id = excluded.last_id, updated_at = excluded.updated_at ' +
|
|
80
|
+
'WHERE excluded.last_id > group_context_cursors.last_id');
|
|
81
|
+
this.selectCursor = this.adapter.prepare('SELECT last_id FROM group_context_cursors WHERE channel_id = ?');
|
|
82
|
+
}
|
|
83
|
+
getMemberMap(channelId) {
|
|
84
|
+
let m = this.memberMapByChannel.get(channelId);
|
|
85
|
+
if (!m) {
|
|
86
|
+
m = new Map();
|
|
87
|
+
this.memberMapByChannel.set(channelId, m);
|
|
88
|
+
}
|
|
89
|
+
return m;
|
|
90
|
+
}
|
|
91
|
+
getNameToUid(channelId) {
|
|
92
|
+
let m = this.nameToUidByChannel.get(channelId);
|
|
93
|
+
if (!m) {
|
|
94
|
+
m = new Map();
|
|
95
|
+
this.nameToUidByChannel.set(channelId, m);
|
|
96
|
+
}
|
|
97
|
+
return m;
|
|
98
|
+
}
|
|
99
|
+
pushMessage(channelId, fromUid, fromName, content, timestamp) {
|
|
100
|
+
// SECURITY: fromName is the user-controlled IM display name and is rendered
|
|
101
|
+
// into the [Group context] block as `<name>:<content>`. Bound + strip it at
|
|
102
|
+
// the boundary (shared choke point) so it can't forge a label/line. fromUid
|
|
103
|
+
// is ALSO user-controlled, and sanitizeDisplayName returns its fallback
|
|
104
|
+
// verbatim — so sanitize the uid fallback too rather than passing it raw
|
|
105
|
+
// (PR #128 review: raw-uid-as-fallback re-introduces the injection).
|
|
106
|
+
const safeName = sanitizeDisplayName(fromName) || sanitizeDisplayName(fromUid) || 'unknown';
|
|
107
|
+
let window = this.messageCache.get(channelId);
|
|
108
|
+
if (!window) {
|
|
109
|
+
window = [];
|
|
110
|
+
this.messageCache.set(channelId, window);
|
|
111
|
+
}
|
|
112
|
+
window.push({ fromUid, fromName: safeName, content, timestamp });
|
|
113
|
+
while (window.length > this.maxWindowSize) {
|
|
114
|
+
window.shift();
|
|
115
|
+
}
|
|
116
|
+
this.learnMember(channelId, fromUid, safeName);
|
|
117
|
+
try {
|
|
118
|
+
this.insertMessage.run(channelId, fromUid, safeName, content, timestamp);
|
|
119
|
+
// Trim old messages to keep DB bounded (keep 2x window for safety)
|
|
120
|
+
this.deleteOldMessages.run(channelId, channelId, this.maxWindowSize * 2);
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
console.error(`group-context: insert message failed: ${String(err)}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
learnMember(channelId, uid, name) {
|
|
127
|
+
if (!uid || !name)
|
|
128
|
+
return;
|
|
129
|
+
const memberMap = this.getMemberMap(channelId);
|
|
130
|
+
const nameMap = this.getNameToUid(channelId);
|
|
131
|
+
const existing = memberMap.get(uid);
|
|
132
|
+
if (existing !== name) {
|
|
133
|
+
// Remove old reverse mapping only if it still points to THIS uid.
|
|
134
|
+
// If another user already claimed the same display name, don't clobber.
|
|
135
|
+
if (existing && nameMap.get(existing) === uid) {
|
|
136
|
+
nameMap.delete(existing);
|
|
137
|
+
}
|
|
138
|
+
memberMap.set(uid, name);
|
|
139
|
+
nameMap.set(name, uid);
|
|
140
|
+
try {
|
|
141
|
+
this.upsertMember.run(channelId, uid, name, Date.now());
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
console.error(`group-context: upsert member failed: ${String(err)}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async refreshMembers(channelId, apiUrl, botToken) {
|
|
149
|
+
const now = Date.now();
|
|
150
|
+
const last = this.lastRefresh.get(channelId) ?? 0;
|
|
151
|
+
if (now - last < REFRESH_INTERVAL_MS)
|
|
152
|
+
return;
|
|
153
|
+
// Don't set lastRefresh here — only on success
|
|
154
|
+
try {
|
|
155
|
+
const members = await getGroupMembers({ apiUrl, botToken, groupNo: channelId });
|
|
156
|
+
this.lastRefresh.set(channelId, now); // Record only on success
|
|
157
|
+
const memberMap = this.getMemberMap(channelId);
|
|
158
|
+
const nameMap = this.getNameToUid(channelId);
|
|
159
|
+
// A8 (#143, take 2): the server response is AUTHORITATIVE — it is the full
|
|
160
|
+
// current roster, not a delta. Upserting returned members WITHOUT pruning
|
|
161
|
+
// departed ones (the original #144 bug, Jerry-Xin's 🔴) left a user who
|
|
162
|
+
// left the group cached forever, so isMember() kept accepting them and the
|
|
163
|
+
// outbound mention guard let a stale @uid through. Track who the server
|
|
164
|
+
// returned, then drop anyone cached/persisted who is no longer present.
|
|
165
|
+
//
|
|
166
|
+
// Best-effort caveat: this is only as fresh as the last successful refresh,
|
|
167
|
+
// which is throttled to REFRESH_INTERVAL_MS (1h) and seeded from DB on
|
|
168
|
+
// restart. A membership change inside that window is not reflected until
|
|
169
|
+
// the next refresh — the outbound guard is defense-in-depth, not a
|
|
170
|
+
// real-time authority. The server still enforces real permissions.
|
|
171
|
+
const present = new Set();
|
|
172
|
+
for (const m of members) {
|
|
173
|
+
if (!m.uid || !m.name)
|
|
174
|
+
continue;
|
|
175
|
+
present.add(m.uid);
|
|
176
|
+
const oldName = memberMap.get(m.uid);
|
|
177
|
+
if (oldName && oldName !== m.name && nameMap.get(oldName) === m.uid) {
|
|
178
|
+
nameMap.delete(oldName);
|
|
179
|
+
}
|
|
180
|
+
memberMap.set(m.uid, m.name);
|
|
181
|
+
nameMap.set(m.name, m.uid);
|
|
182
|
+
// G23: Track server-authoritative robot flag for future 免@ gate.
|
|
183
|
+
if (m.robot !== undefined) {
|
|
184
|
+
let rfMap = this.robotFlags.get(channelId);
|
|
185
|
+
if (!rfMap) {
|
|
186
|
+
rfMap = new Map();
|
|
187
|
+
this.robotFlags.set(channelId, rfMap);
|
|
188
|
+
}
|
|
189
|
+
rfMap.set(m.uid, m.robot === 1);
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
this.upsertMember.run(channelId, m.uid, m.name, now);
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
console.error(`group-context: upsert member failed: ${String(err)}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// Prune members no longer in the authoritative roster (memory + DB + robot
|
|
199
|
+
// flags). Iterate a snapshot of uids since we mutate the map in the loop.
|
|
200
|
+
//
|
|
201
|
+
// Guard: skip pruning when the roster came back EMPTY. getGroupMembers
|
|
202
|
+
// returns [] not only for a genuinely empty group but also for a
|
|
203
|
+
// malformed/unexpected-shape 200 response (data.members not an array →
|
|
204
|
+
// silently []). A group the bot is in always has ≥1 member, so an empty
|
|
205
|
+
// roster is far more likely a transient quirk than "everyone left".
|
|
206
|
+
// Mass-pruning on it would wipe the whole roster — and since lastRefresh
|
|
207
|
+
// was already set on this "success", it wouldn't re-fetch for an hour,
|
|
208
|
+
// downgrading every mention to plain text in that window. Keep the prior
|
|
209
|
+
// snapshot instead; a real emptying still prunes member-by-member as the
|
|
210
|
+
// roster shrinks across non-empty responses.
|
|
211
|
+
if (present.size > 0) {
|
|
212
|
+
const rfMap = this.robotFlags.get(channelId);
|
|
213
|
+
for (const uid of [...memberMap.keys()]) {
|
|
214
|
+
if (present.has(uid))
|
|
215
|
+
continue;
|
|
216
|
+
const staleName = memberMap.get(uid);
|
|
217
|
+
memberMap.delete(uid);
|
|
218
|
+
// Only remove the reverse entry if it still points at THIS uid (a rename
|
|
219
|
+
// may have already re-pointed the name to someone else).
|
|
220
|
+
if (staleName !== undefined && nameMap.get(staleName) === uid) {
|
|
221
|
+
nameMap.delete(staleName);
|
|
222
|
+
}
|
|
223
|
+
rfMap?.delete(uid);
|
|
224
|
+
try {
|
|
225
|
+
this.deleteMember.run(channelId, uid);
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
console.error(`group-context: delete stale member failed: ${String(err)}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
console.error(`group-context: refreshMembers(${channelId}) failed: ${String(err)}`);
|
|
235
|
+
// Don't update lastRefresh on failure — allow retry
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
async fetchAndLearnUser(uid, channelId, apiUrl, botToken) {
|
|
239
|
+
const memberMap = this.getMemberMap(channelId);
|
|
240
|
+
const cached = memberMap.get(uid);
|
|
241
|
+
if (cached)
|
|
242
|
+
return cached;
|
|
243
|
+
try {
|
|
244
|
+
const info = await fetchUserInfo({ apiUrl, botToken, uid });
|
|
245
|
+
if (info?.name) {
|
|
246
|
+
this.learnMember(channelId, uid, info.name);
|
|
247
|
+
return info.name;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
catch (err) {
|
|
251
|
+
console.error(`group-context: fetchUserInfo(${uid}) failed: ${String(err)}`);
|
|
252
|
+
}
|
|
253
|
+
return undefined;
|
|
254
|
+
}
|
|
255
|
+
buildContext(channelId) {
|
|
256
|
+
const window = this.messageCache.get(channelId);
|
|
257
|
+
if (!window || window.length === 0)
|
|
258
|
+
return '';
|
|
259
|
+
const header = '[Recent group messages]\n';
|
|
260
|
+
const trailer = '\n';
|
|
261
|
+
const budget = this.maxContextChars - header.length - trailer.length;
|
|
262
|
+
if (budget <= 0)
|
|
263
|
+
return '';
|
|
264
|
+
const selected = [];
|
|
265
|
+
let used = 0;
|
|
266
|
+
for (let i = window.length - 1; i >= 0; i--) {
|
|
267
|
+
const m = window[i];
|
|
268
|
+
const line = `${m.fromName}:${m.content}`;
|
|
269
|
+
const cost = line.length + (selected.length > 0 ? 1 : 0);
|
|
270
|
+
if (used + cost > budget)
|
|
271
|
+
break;
|
|
272
|
+
selected.push(line);
|
|
273
|
+
used += cost;
|
|
274
|
+
}
|
|
275
|
+
if (selected.length === 0)
|
|
276
|
+
return '';
|
|
277
|
+
selected.reverse();
|
|
278
|
+
return `${header}${selected.join('\n')}${trailer}`;
|
|
279
|
+
}
|
|
280
|
+
/** Read the per-channel consumption cursor (highest already-injected id), or 0. */
|
|
281
|
+
getContextCursor(channelId) {
|
|
282
|
+
try {
|
|
283
|
+
const row = this.selectCursor.get(channelId);
|
|
284
|
+
return row?.last_id ?? 0;
|
|
285
|
+
}
|
|
286
|
+
catch (err) {
|
|
287
|
+
console.error(`group-context: getContextCursor(${channelId}) failed: ${String(err)}`);
|
|
288
|
+
return 0;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
/** Advance the per-channel cursor to `lastId` (monotonic — never moves backward). */
|
|
292
|
+
setContextCursor(channelId, lastId) {
|
|
293
|
+
try {
|
|
294
|
+
this.upsertCursor.run(channelId, lastId, Date.now());
|
|
295
|
+
}
|
|
296
|
+
catch (err) {
|
|
297
|
+
console.error(`group-context: setContextCursor(${channelId}) failed: ${String(err)}`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/** The highest group_messages.id for a channel (for cursor priming), or 0. */
|
|
301
|
+
getMaxMessageId(channelId) {
|
|
302
|
+
try {
|
|
303
|
+
const row = this.selectMaxId.get(channelId);
|
|
304
|
+
return row?.maxId ?? 0;
|
|
305
|
+
}
|
|
306
|
+
catch (err) {
|
|
307
|
+
console.error(`group-context: getMaxMessageId(${channelId}) failed: ${String(err)}`);
|
|
308
|
+
return 0;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Build a group-context block from messages NEWER than `sinceId` (the delta the
|
|
313
|
+
* model hasn't seen yet), within `maxContextChars`. Returns the block text (same
|
|
314
|
+
* `[Recent group messages]` format as buildContext) and the highest id that
|
|
315
|
+
* EXISTS in the channel above `sinceId` (so the caller advances the cursor past
|
|
316
|
+
* the whole delta, including any oldest lines the char budget dropped — those
|
|
317
|
+
* are the least-relevant and are intentionally not re-shown). Empty text + the
|
|
318
|
+
* unchanged cursor when there's nothing new.
|
|
319
|
+
*
|
|
320
|
+
* Rows are fetched newest-first (so a backlog larger than the budget keeps the
|
|
321
|
+
* most-recent messages); the selected slice is reversed back to chronological
|
|
322
|
+
* order for display. Unlike buildContext (in-memory rolling window), this reads
|
|
323
|
+
* from the DB so the cursor delta is exact even across restarts / window eviction.
|
|
324
|
+
*/
|
|
325
|
+
buildContextSince(channelId, sinceId) {
|
|
326
|
+
let rows;
|
|
327
|
+
try {
|
|
328
|
+
rows = this.selectMessagesSince.all(channelId, sinceId, this.maxWindowSize);
|
|
329
|
+
}
|
|
330
|
+
catch (err) {
|
|
331
|
+
console.error(`group-context: buildContextSince(${channelId}) failed: ${String(err)}`);
|
|
332
|
+
return { text: '', lastId: sinceId };
|
|
333
|
+
}
|
|
334
|
+
if (rows.length === 0)
|
|
335
|
+
return { text: '', lastId: sinceId };
|
|
336
|
+
// rows are newest-first; the highest id is the first row. Advance the cursor to
|
|
337
|
+
// it regardless of what the budget keeps, so we never re-show a line.
|
|
338
|
+
const lastId = rows[0].id;
|
|
339
|
+
const header = '[Recent group messages]\n';
|
|
340
|
+
const trailer = '\n';
|
|
341
|
+
const budget = this.maxContextChars - header.length - trailer.length;
|
|
342
|
+
if (budget <= 0)
|
|
343
|
+
return { text: '', lastId };
|
|
344
|
+
// Walk newest→oldest (rows[0] is newest) keeping lines within budget.
|
|
345
|
+
const selected = [];
|
|
346
|
+
let used = 0;
|
|
347
|
+
for (let i = 0; i < rows.length; i++) {
|
|
348
|
+
const line = `${rows[i].from_name}:${rows[i].content}`;
|
|
349
|
+
const cost = line.length + (selected.length > 0 ? 1 : 0);
|
|
350
|
+
if (used + cost > budget)
|
|
351
|
+
break;
|
|
352
|
+
selected.push(line);
|
|
353
|
+
used += cost;
|
|
354
|
+
}
|
|
355
|
+
if (selected.length === 0)
|
|
356
|
+
return { text: '', lastId };
|
|
357
|
+
selected.reverse(); // chronological order for display
|
|
358
|
+
return { text: `${header}${selected.join('\n')}${trailer}`, lastId };
|
|
359
|
+
}
|
|
360
|
+
resolveMentions(text, channelId) {
|
|
361
|
+
const uids = [];
|
|
362
|
+
const seen = new Set();
|
|
363
|
+
const nameMap = this.nameToUidByChannel.get(channelId);
|
|
364
|
+
if (!nameMap)
|
|
365
|
+
return uids;
|
|
366
|
+
// Match @name where name is a run of word-like characters (letters, digits, underscores,
|
|
367
|
+
// CJK ideographs, Hangul, Kana, etc.) — stops at punctuation and whitespace.
|
|
368
|
+
const regex = /@([\w\u4e00-\u9fff\u3400-\u4dbf\uac00-\ud7af\u3040-\u309f\u30a0-\u30ff]+)/g;
|
|
369
|
+
let match;
|
|
370
|
+
while ((match = regex.exec(text)) !== null) {
|
|
371
|
+
// Strip known trailing punctuation that might stick to names
|
|
372
|
+
let name = match[1];
|
|
373
|
+
name = name.replace(/[,.!?;:,。!?;:、)\]]+$/, '');
|
|
374
|
+
if (!name)
|
|
375
|
+
continue;
|
|
376
|
+
const uid = nameMap.get(name);
|
|
377
|
+
if (uid && !seen.has(uid)) {
|
|
378
|
+
seen.add(uid);
|
|
379
|
+
uids.push(uid);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return uids;
|
|
383
|
+
}
|
|
384
|
+
getName(uid, channelId) {
|
|
385
|
+
return this.getMemberMap(channelId).get(uid);
|
|
386
|
+
}
|
|
387
|
+
/** G23: Check the server-authoritative robot flag for a group member. */
|
|
388
|
+
isRobot(channelId, uid) {
|
|
389
|
+
return this.robotFlags.get(channelId)?.get(uid);
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* A8 (#143): true iff `uid` is a current member of `channelId` per the cached
|
|
393
|
+
* member list. The list is kept authoritative by refreshMembers (it prunes
|
|
394
|
+
* departed members, not just upserts) — but it is only best-effort fresh:
|
|
395
|
+
* refresh is throttled to REFRESH_INTERVAL_MS and seeded from DB on restart,
|
|
396
|
+
* so a membership change inside that window may not be reflected yet. Used by
|
|
397
|
+
* the outbound mention guard as defense-in-depth (the server still enforces
|
|
398
|
+
* real permissions), NOT as a real-time authority.
|
|
399
|
+
*/
|
|
400
|
+
isMember(channelId, uid) {
|
|
401
|
+
return this.memberMapByChannel.get(channelId)?.has(uid) ?? false;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* A8 (#143): the channel's displayName→uid map, for v1 `@name` outbound
|
|
405
|
+
* resolution in StreamRelay.deliver. Returns the live map (empty if the
|
|
406
|
+
* channel has no cached members yet). Read-only use by callers.
|
|
407
|
+
*/
|
|
408
|
+
getNameToUidMap(channelId) {
|
|
409
|
+
return this.getNameToUid(channelId);
|
|
410
|
+
}
|
|
411
|
+
loadMembersFromDb(channelId) {
|
|
412
|
+
try {
|
|
413
|
+
const rows = this.selectMembers.all(channelId);
|
|
414
|
+
const memberMap = this.getMemberMap(channelId);
|
|
415
|
+
const nameMap = this.getNameToUid(channelId);
|
|
416
|
+
for (const r of rows) {
|
|
417
|
+
if (!r.uid || !r.name)
|
|
418
|
+
continue;
|
|
419
|
+
memberMap.set(r.uid, r.name);
|
|
420
|
+
nameMap.set(r.name, r.uid);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
catch (err) {
|
|
424
|
+
console.error(`group-context: loadMembersFromDb(${channelId}) failed: ${String(err)}`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
loadMessagesFromDb(channelId) {
|
|
428
|
+
try {
|
|
429
|
+
const rows = this.selectRecentMessages.all(channelId, this.maxWindowSize);
|
|
430
|
+
if (rows.length === 0)
|
|
431
|
+
return;
|
|
432
|
+
// Rows come in DESC order, reverse to chronological
|
|
433
|
+
rows.reverse();
|
|
434
|
+
const existing = this.messageCache.get(channelId);
|
|
435
|
+
if (existing && existing.length > 0)
|
|
436
|
+
return; // Don't overwrite live data
|
|
437
|
+
// Map snake_case DB columns to camelCase GroupMessage
|
|
438
|
+
this.messageCache.set(channelId, rows.map(r => ({
|
|
439
|
+
fromUid: r.from_uid,
|
|
440
|
+
fromName: r.from_name,
|
|
441
|
+
content: r.content,
|
|
442
|
+
timestamp: r.timestamp,
|
|
443
|
+
})));
|
|
444
|
+
}
|
|
445
|
+
catch (err) {
|
|
446
|
+
console.error(`group-context: loadMessagesFromDb(${channelId}) failed: ${String(err)}`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
/** Load all persisted members and messages from DB (call once at startup). */
|
|
450
|
+
loadAllFromDb() {
|
|
451
|
+
try {
|
|
452
|
+
const rows = this.adapter.prepare('SELECT DISTINCT group_id FROM group_members').all();
|
|
453
|
+
for (const row of rows) {
|
|
454
|
+
this.loadMembersFromDb(row.group_id);
|
|
455
|
+
this.loadMessagesFromDb(row.group_id);
|
|
456
|
+
}
|
|
457
|
+
if (rows.length > 0) {
|
|
458
|
+
console.log(`[group-context] Loaded members + messages for ${rows.length} group(s) from DB`);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
catch (err) {
|
|
462
|
+
console.error(`group-context: loadAllFromDb failed: ${String(err)}`);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
//# sourceMappingURL=group-context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"group-context.js","sourceRoot":"","sources":["../src/group-context.ts"],"names":[],"mappings":"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"}
|