@foxden-app/foxclaw 0.2.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/.env.example +36 -0
- package/LICENSE +22 -0
- package/README.md +244 -0
- package/README_EN.md +244 -0
- package/dist/channels/bridge_messaging_router.d.ts +27 -0
- package/dist/channels/bridge_messaging_router.js +85 -0
- package/dist/channels/telegram/telegram_channel_adapter.d.ts +12 -0
- package/dist/channels/telegram/telegram_channel_adapter.js +21 -0
- package/dist/channels/telegram/telegram_messaging_port.d.ts +25 -0
- package/dist/channels/telegram/telegram_messaging_port.js +51 -0
- package/dist/channels/weixin/account_store.d.ts +15 -0
- package/dist/channels/weixin/account_store.js +54 -0
- package/dist/channels/weixin/ilink/aes_ecb.d.ts +3 -0
- package/dist/channels/weixin/ilink/aes_ecb.js +12 -0
- package/dist/channels/weixin/ilink/api.d.ts +44 -0
- package/dist/channels/weixin/ilink/api.js +187 -0
- package/dist/channels/weixin/ilink/cdn_upload.d.ts +11 -0
- package/dist/channels/weixin/ilink/cdn_upload.js +60 -0
- package/dist/channels/weixin/ilink/cdn_url.d.ts +7 -0
- package/dist/channels/weixin/ilink/cdn_url.js +7 -0
- package/dist/channels/weixin/ilink/constants.d.ts +7 -0
- package/dist/channels/weixin/ilink/constants.js +27 -0
- package/dist/channels/weixin/ilink/context.d.ts +13 -0
- package/dist/channels/weixin/ilink/context.js +13 -0
- package/dist/channels/weixin/ilink/login_qr.d.ts +34 -0
- package/dist/channels/weixin/ilink/login_qr.js +233 -0
- package/dist/channels/weixin/ilink/media_image.d.ts +11 -0
- package/dist/channels/weixin/ilink/media_image.js +44 -0
- package/dist/channels/weixin/ilink/mime.d.ts +3 -0
- package/dist/channels/weixin/ilink/mime.js +36 -0
- package/dist/channels/weixin/ilink/pic_decrypt.d.ts +2 -0
- package/dist/channels/weixin/ilink/pic_decrypt.js +56 -0
- package/dist/channels/weixin/ilink/random.d.ts +2 -0
- package/dist/channels/weixin/ilink/random.js +7 -0
- package/dist/channels/weixin/ilink/redact.d.ts +4 -0
- package/dist/channels/weixin/ilink/redact.js +34 -0
- package/dist/channels/weixin/ilink/runtime_attach.d.ts +3 -0
- package/dist/channels/weixin/ilink/runtime_attach.js +13 -0
- package/dist/channels/weixin/ilink/send.d.ts +21 -0
- package/dist/channels/weixin/ilink/send.js +108 -0
- package/dist/channels/weixin/ilink/session_guard.d.ts +6 -0
- package/dist/channels/weixin/ilink/session_guard.js +39 -0
- package/dist/channels/weixin/ilink/types.d.ts +155 -0
- package/dist/channels/weixin/ilink/types.js +10 -0
- package/dist/channels/weixin/ilink/upload.d.ts +15 -0
- package/dist/channels/weixin/ilink/upload.js +75 -0
- package/dist/channels/weixin/sync_buf_store.d.ts +3 -0
- package/dist/channels/weixin/sync_buf_store.js +19 -0
- package/dist/channels/weixin/weixin_channel_adapter.d.ts +18 -0
- package/dist/channels/weixin/weixin_channel_adapter.js +273 -0
- package/dist/channels/weixin/weixin_messaging_port.d.ts +29 -0
- package/dist/channels/weixin/weixin_messaging_port.js +113 -0
- package/dist/codex_app/client.d.ts +176 -0
- package/dist/codex_app/client.js +1230 -0
- package/dist/codex_app/deeplink.d.ts +7 -0
- package/dist/codex_app/deeplink.js +29 -0
- package/dist/codex_app/local_usage.d.ts +16 -0
- package/dist/codex_app/local_usage.js +123 -0
- package/dist/config.d.ts +44 -0
- package/dist/config.js +131 -0
- package/dist/controller/access.d.ts +11 -0
- package/dist/controller/access.js +33 -0
- package/dist/controller/activity.d.ts +62 -0
- package/dist/controller/activity.js +330 -0
- package/dist/controller/commands.d.ts +6 -0
- package/dist/controller/commands.js +17 -0
- package/dist/controller/controller.d.ts +326 -0
- package/dist/controller/controller.js +7503 -0
- package/dist/controller/observer.d.ts +16 -0
- package/dist/controller/observer.js +98 -0
- package/dist/controller/presentation.d.ts +80 -0
- package/dist/controller/presentation.js +568 -0
- package/dist/controller/service_tier.d.ts +9 -0
- package/dist/controller/service_tier.js +32 -0
- package/dist/controller/session_observer.d.ts +22 -0
- package/dist/controller/session_observer.js +259 -0
- package/dist/controller/status.d.ts +10 -0
- package/dist/controller/status.js +28 -0
- package/dist/core/bridge_scope.d.ts +18 -0
- package/dist/core/bridge_scope.js +46 -0
- package/dist/core/channel_port.d.ts +15 -0
- package/dist/core/channel_port.js +1 -0
- package/dist/i18n.d.ts +1108 -0
- package/dist/i18n.js +1154 -0
- package/dist/lock.d.ts +7 -0
- package/dist/lock.js +80 -0
- package/dist/logger.d.ts +12 -0
- package/dist/logger.js +57 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +236 -0
- package/dist/runtime.d.ts +3 -0
- package/dist/runtime.js +14 -0
- package/dist/store/database.d.ts +79 -0
- package/dist/store/database.js +489 -0
- package/dist/store/migrate_bridge_scope.d.ts +6 -0
- package/dist/store/migrate_bridge_scope.js +59 -0
- package/dist/telegram/addressing.d.ts +33 -0
- package/dist/telegram/addressing.js +57 -0
- package/dist/telegram/api.d.ts +14 -0
- package/dist/telegram/api.js +89 -0
- package/dist/telegram/gateway.d.ts +76 -0
- package/dist/telegram/gateway.js +383 -0
- package/dist/telegram/media.d.ts +34 -0
- package/dist/telegram/media.js +180 -0
- package/dist/telegram/rendering.d.ts +10 -0
- package/dist/telegram/rendering.js +21 -0
- package/dist/telegram/scope.d.ts +6 -0
- package/dist/telegram/scope.js +24 -0
- package/dist/telegram/text.d.ts +7 -0
- package/dist/telegram/text.js +47 -0
- package/dist/types.d.ts +343 -0
- package/dist/types.js +1 -0
- package/docs/agent-assisted-install.md +84 -0
- package/docs/install-for-beginners.md +287 -0
- package/docs/troubleshooting.md +239 -0
- package/package.json +62 -0
- package/scripts/doctor.sh +3 -0
- package/scripts/launchd/install.sh +54 -0
- package/scripts/status.sh +3 -0
- package/scripts/systemd/install.sh +83 -0
- package/scripts/systemd/uninstall.sh +15 -0
- package/skills/foxclaw/SKILL.md +167 -0
- package/skills/foxclaw/agents/openai.yaml +4 -0
- package/skills/foxclaw/references/telegram-setup.md +93 -0
- package/skills/foxclaw/scripts/bootstrap_host.py +350 -0
- package/skills/foxclaw/scripts/bootstrap_remote.py +67 -0
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
4
|
+
import { migrateLegacyBridgeScopeIds } from './migrate_bridge_scope.js';
|
|
5
|
+
export class BridgeStore {
|
|
6
|
+
db;
|
|
7
|
+
constructor(dbPath) {
|
|
8
|
+
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
9
|
+
this.db = new DatabaseSync(dbPath);
|
|
10
|
+
this.db.exec(`
|
|
11
|
+
CREATE TABLE IF NOT EXISTS telegram_offsets (
|
|
12
|
+
bot_key TEXT PRIMARY KEY,
|
|
13
|
+
update_id INTEGER NOT NULL
|
|
14
|
+
);
|
|
15
|
+
CREATE TABLE IF NOT EXISTS chat_bindings (
|
|
16
|
+
chat_id TEXT PRIMARY KEY,
|
|
17
|
+
thread_id TEXT NOT NULL,
|
|
18
|
+
cwd TEXT,
|
|
19
|
+
updated_at INTEGER NOT NULL
|
|
20
|
+
);
|
|
21
|
+
CREATE TABLE IF NOT EXISTS chat_settings (
|
|
22
|
+
chat_id TEXT PRIMARY KEY,
|
|
23
|
+
model TEXT,
|
|
24
|
+
reasoning_effort TEXT,
|
|
25
|
+
locale TEXT,
|
|
26
|
+
access_preset TEXT,
|
|
27
|
+
collaboration_mode TEXT,
|
|
28
|
+
service_tier TEXT,
|
|
29
|
+
active_turn_message_mode TEXT,
|
|
30
|
+
updated_at INTEGER NOT NULL
|
|
31
|
+
);
|
|
32
|
+
CREATE TABLE IF NOT EXISTS thread_cache (
|
|
33
|
+
chat_id TEXT NOT NULL,
|
|
34
|
+
idx INTEGER NOT NULL,
|
|
35
|
+
thread_id TEXT NOT NULL,
|
|
36
|
+
name TEXT,
|
|
37
|
+
preview TEXT NOT NULL,
|
|
38
|
+
cwd TEXT,
|
|
39
|
+
model_provider TEXT,
|
|
40
|
+
status TEXT NOT NULL DEFAULT 'idle',
|
|
41
|
+
archived INTEGER NOT NULL DEFAULT 0,
|
|
42
|
+
updated_at INTEGER NOT NULL,
|
|
43
|
+
PRIMARY KEY (chat_id, idx)
|
|
44
|
+
);
|
|
45
|
+
CREATE TABLE IF NOT EXISTS pending_approvals (
|
|
46
|
+
local_id TEXT PRIMARY KEY,
|
|
47
|
+
server_request_id TEXT NOT NULL,
|
|
48
|
+
kind TEXT NOT NULL,
|
|
49
|
+
chat_id TEXT NOT NULL,
|
|
50
|
+
thread_id TEXT NOT NULL,
|
|
51
|
+
turn_id TEXT NOT NULL,
|
|
52
|
+
item_id TEXT NOT NULL,
|
|
53
|
+
approval_id TEXT,
|
|
54
|
+
reason TEXT,
|
|
55
|
+
command TEXT,
|
|
56
|
+
cwd TEXT,
|
|
57
|
+
payload_json TEXT,
|
|
58
|
+
message_id INTEGER,
|
|
59
|
+
created_at INTEGER NOT NULL,
|
|
60
|
+
resolved_at INTEGER
|
|
61
|
+
);
|
|
62
|
+
CREATE TABLE IF NOT EXISTS active_turn_previews (
|
|
63
|
+
turn_id TEXT PRIMARY KEY,
|
|
64
|
+
scope_id TEXT NOT NULL,
|
|
65
|
+
thread_id TEXT NOT NULL,
|
|
66
|
+
message_id INTEGER NOT NULL,
|
|
67
|
+
created_at INTEGER NOT NULL,
|
|
68
|
+
updated_at INTEGER NOT NULL
|
|
69
|
+
);
|
|
70
|
+
CREATE TABLE IF NOT EXISTS pending_user_inputs (
|
|
71
|
+
local_id TEXT PRIMARY KEY,
|
|
72
|
+
server_request_id TEXT NOT NULL,
|
|
73
|
+
chat_id TEXT NOT NULL,
|
|
74
|
+
thread_id TEXT NOT NULL,
|
|
75
|
+
turn_id TEXT,
|
|
76
|
+
item_id TEXT NOT NULL,
|
|
77
|
+
message_id INTEGER,
|
|
78
|
+
questions_json TEXT NOT NULL,
|
|
79
|
+
answers_json TEXT NOT NULL,
|
|
80
|
+
current_question_index INTEGER NOT NULL,
|
|
81
|
+
awaiting_free_text INTEGER NOT NULL DEFAULT 0,
|
|
82
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
83
|
+
created_at INTEGER NOT NULL,
|
|
84
|
+
submitted_at INTEGER,
|
|
85
|
+
resolved_at INTEGER
|
|
86
|
+
);
|
|
87
|
+
CREATE TABLE IF NOT EXISTS pending_user_input_messages (
|
|
88
|
+
input_local_id TEXT NOT NULL,
|
|
89
|
+
question_index INTEGER NOT NULL,
|
|
90
|
+
message_id INTEGER NOT NULL,
|
|
91
|
+
message_kind TEXT NOT NULL,
|
|
92
|
+
created_at INTEGER NOT NULL,
|
|
93
|
+
PRIMARY KEY (input_local_id, question_index, message_kind)
|
|
94
|
+
);
|
|
95
|
+
CREATE TABLE IF NOT EXISTS audit_logs (
|
|
96
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
97
|
+
direction TEXT NOT NULL,
|
|
98
|
+
chat_id TEXT NOT NULL,
|
|
99
|
+
event_type TEXT NOT NULL,
|
|
100
|
+
summary TEXT NOT NULL,
|
|
101
|
+
created_at INTEGER NOT NULL
|
|
102
|
+
);
|
|
103
|
+
CREATE TABLE IF NOT EXISTS weixin_context_tokens (
|
|
104
|
+
scope_id TEXT PRIMARY KEY,
|
|
105
|
+
context_token TEXT NOT NULL,
|
|
106
|
+
updated_at INTEGER NOT NULL
|
|
107
|
+
);
|
|
108
|
+
CREATE TABLE IF NOT EXISTS codex_auth_candidates (
|
|
109
|
+
name TEXT PRIMARY KEY,
|
|
110
|
+
disabled INTEGER NOT NULL DEFAULT 0,
|
|
111
|
+
updated_at INTEGER NOT NULL
|
|
112
|
+
);
|
|
113
|
+
`);
|
|
114
|
+
this.ensureColumn('thread_cache', 'name', 'TEXT');
|
|
115
|
+
this.ensureColumn('thread_cache', 'model_provider', 'TEXT');
|
|
116
|
+
this.ensureColumn('thread_cache', 'status', "TEXT NOT NULL DEFAULT 'idle'");
|
|
117
|
+
this.ensureColumn('thread_cache', 'archived', 'INTEGER NOT NULL DEFAULT 0');
|
|
118
|
+
this.ensureColumn('chat_settings', 'locale', 'TEXT');
|
|
119
|
+
this.ensureColumn('chat_settings', 'access_preset', 'TEXT');
|
|
120
|
+
this.ensureColumn('chat_settings', 'collaboration_mode', 'TEXT');
|
|
121
|
+
this.ensureColumn('chat_settings', 'service_tier', 'TEXT');
|
|
122
|
+
this.ensureColumn('chat_settings', 'active_turn_message_mode', 'TEXT');
|
|
123
|
+
this.ensureColumn('pending_approvals', 'payload_json', 'TEXT');
|
|
124
|
+
this.ensureColumn('pending_user_inputs', 'status', "TEXT NOT NULL DEFAULT 'pending'");
|
|
125
|
+
this.ensureColumn('pending_user_inputs', 'submitted_at', 'INTEGER');
|
|
126
|
+
migrateLegacyBridgeScopeIds(this.db);
|
|
127
|
+
}
|
|
128
|
+
getTelegramOffset(botKey) {
|
|
129
|
+
const row = this.db.prepare('SELECT update_id FROM telegram_offsets WHERE bot_key = ?').get(botKey);
|
|
130
|
+
return row?.update_id ?? 0;
|
|
131
|
+
}
|
|
132
|
+
setTelegramOffset(botKey, updateId) {
|
|
133
|
+
this.db.prepare(`
|
|
134
|
+
INSERT INTO telegram_offsets (bot_key, update_id)
|
|
135
|
+
VALUES (?, ?)
|
|
136
|
+
ON CONFLICT(bot_key) DO UPDATE SET update_id = excluded.update_id
|
|
137
|
+
`).run(botKey, updateId);
|
|
138
|
+
}
|
|
139
|
+
getBinding(chatId) {
|
|
140
|
+
const row = this.db.prepare('SELECT chat_id, thread_id, cwd, updated_at FROM chat_bindings WHERE chat_id = ?').get(chatId);
|
|
141
|
+
if (!row)
|
|
142
|
+
return null;
|
|
143
|
+
return {
|
|
144
|
+
chatId: String(row.chat_id),
|
|
145
|
+
threadId: String(row.thread_id),
|
|
146
|
+
cwd: row.cwd === null ? null : String(row.cwd),
|
|
147
|
+
updatedAt: Number(row.updated_at)
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
setBinding(chatId, threadId, cwd) {
|
|
151
|
+
this.db.prepare(`
|
|
152
|
+
INSERT INTO chat_bindings (chat_id, thread_id, cwd, updated_at)
|
|
153
|
+
VALUES (?, ?, ?, ?)
|
|
154
|
+
ON CONFLICT(chat_id) DO UPDATE SET thread_id = excluded.thread_id, cwd = excluded.cwd, updated_at = excluded.updated_at
|
|
155
|
+
`).run(chatId, threadId, cwd, Date.now());
|
|
156
|
+
}
|
|
157
|
+
clearBinding(chatId) {
|
|
158
|
+
this.db.prepare('DELETE FROM chat_bindings WHERE chat_id = ?').run(chatId);
|
|
159
|
+
}
|
|
160
|
+
getChatSettings(chatId) {
|
|
161
|
+
const row = this.db.prepare('SELECT chat_id, model, reasoning_effort, locale, access_preset, collaboration_mode, service_tier, active_turn_message_mode, updated_at FROM chat_settings WHERE chat_id = ?').get(chatId);
|
|
162
|
+
if (!row)
|
|
163
|
+
return null;
|
|
164
|
+
return {
|
|
165
|
+
chatId: String(row.chat_id),
|
|
166
|
+
model: row.model === null ? null : String(row.model),
|
|
167
|
+
reasoningEffort: row.reasoning_effort === null ? null : String(row.reasoning_effort),
|
|
168
|
+
locale: row.locale === null ? null : String(row.locale),
|
|
169
|
+
accessPreset: row.access_preset === null ? null : String(row.access_preset),
|
|
170
|
+
collaborationMode: normalizeCollaborationMode(row.collaboration_mode),
|
|
171
|
+
serviceTier: row.service_tier === null ? null : String(row.service_tier),
|
|
172
|
+
activeTurnMessageMode: normalizeActiveTurnMessageMode(row.active_turn_message_mode),
|
|
173
|
+
updatedAt: Number(row.updated_at),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
setChatSettings(chatId, model, reasoningEffort, locale) {
|
|
177
|
+
const current = this.getChatSettings(chatId);
|
|
178
|
+
const nextLocale = locale === undefined ? current?.locale ?? null : locale;
|
|
179
|
+
this.writeChatSettings(chatId, model, reasoningEffort, nextLocale, current?.accessPreset ?? null, current?.collaborationMode ?? null, current?.serviceTier ?? null, current?.activeTurnMessageMode ?? null);
|
|
180
|
+
}
|
|
181
|
+
setChatLocale(chatId, locale) {
|
|
182
|
+
const current = this.getChatSettings(chatId);
|
|
183
|
+
this.writeChatSettings(chatId, current?.model ?? null, current?.reasoningEffort ?? null, locale, current?.accessPreset ?? null, current?.collaborationMode ?? null, current?.serviceTier ?? null, current?.activeTurnMessageMode ?? null);
|
|
184
|
+
}
|
|
185
|
+
setChatAccessPreset(chatId, accessPreset) {
|
|
186
|
+
const current = this.getChatSettings(chatId);
|
|
187
|
+
this.writeChatSettings(chatId, current?.model ?? null, current?.reasoningEffort ?? null, current?.locale ?? null, accessPreset, current?.collaborationMode ?? null, current?.serviceTier ?? null, current?.activeTurnMessageMode ?? null);
|
|
188
|
+
}
|
|
189
|
+
setChatCollaborationMode(chatId, collaborationMode) {
|
|
190
|
+
const current = this.getChatSettings(chatId);
|
|
191
|
+
this.writeChatSettings(chatId, current?.model ?? null, current?.reasoningEffort ?? null, current?.locale ?? null, current?.accessPreset ?? null, collaborationMode, current?.serviceTier ?? null, current?.activeTurnMessageMode ?? null);
|
|
192
|
+
}
|
|
193
|
+
setChatServiceTier(chatId, serviceTier) {
|
|
194
|
+
const current = this.getChatSettings(chatId);
|
|
195
|
+
this.writeChatSettings(chatId, current?.model ?? null, current?.reasoningEffort ?? null, current?.locale ?? null, current?.accessPreset ?? null, current?.collaborationMode ?? null, serviceTier, current?.activeTurnMessageMode ?? null);
|
|
196
|
+
}
|
|
197
|
+
setChatActiveTurnMessageMode(chatId, activeTurnMessageMode) {
|
|
198
|
+
const current = this.getChatSettings(chatId);
|
|
199
|
+
this.writeChatSettings(chatId, current?.model ?? null, current?.reasoningEffort ?? null, current?.locale ?? null, current?.accessPreset ?? null, current?.collaborationMode ?? null, current?.serviceTier ?? null, activeTurnMessageMode);
|
|
200
|
+
}
|
|
201
|
+
findChatIdByThreadId(threadId) {
|
|
202
|
+
const row = this.db.prepare('SELECT chat_id FROM chat_bindings WHERE thread_id = ?').get(threadId);
|
|
203
|
+
return row ? String(row.chat_id) : null;
|
|
204
|
+
}
|
|
205
|
+
findAllChatIdsByThreadId(threadId) {
|
|
206
|
+
const rows = this.db.prepare('SELECT chat_id FROM chat_bindings WHERE thread_id = ? ORDER BY updated_at ASC').all(threadId);
|
|
207
|
+
return rows.map(row => String(row.chat_id));
|
|
208
|
+
}
|
|
209
|
+
countBindings() {
|
|
210
|
+
const row = this.db.prepare('SELECT COUNT(*) AS count FROM chat_bindings').get();
|
|
211
|
+
return Number(row.count);
|
|
212
|
+
}
|
|
213
|
+
cacheThreadList(chatId, threads) {
|
|
214
|
+
const deleteStmt = this.db.prepare('DELETE FROM thread_cache WHERE chat_id = ?');
|
|
215
|
+
const insertStmt = this.db.prepare(`
|
|
216
|
+
INSERT INTO thread_cache (chat_id, idx, thread_id, name, preview, cwd, model_provider, status, archived, updated_at)
|
|
217
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
218
|
+
`);
|
|
219
|
+
deleteStmt.run(chatId);
|
|
220
|
+
threads.forEach((thread, index) => {
|
|
221
|
+
const idx = typeof thread.listIndex === 'number' ? thread.listIndex : index + 1;
|
|
222
|
+
insertStmt.run(chatId, idx, thread.threadId, thread.name, thread.preview, thread.cwd, thread.modelProvider, thread.status, thread.archived ? 1 : 0, thread.updatedAt);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
getCachedThread(chatId, index) {
|
|
226
|
+
const row = this.db.prepare(`
|
|
227
|
+
SELECT idx, thread_id, name, preview, cwd, model_provider, status, archived, updated_at
|
|
228
|
+
FROM thread_cache
|
|
229
|
+
WHERE chat_id = ? AND idx = ?
|
|
230
|
+
`).get(chatId, index);
|
|
231
|
+
if (!row)
|
|
232
|
+
return null;
|
|
233
|
+
return {
|
|
234
|
+
index: Number(row.idx),
|
|
235
|
+
threadId: String(row.thread_id),
|
|
236
|
+
name: row.name === null ? null : String(row.name),
|
|
237
|
+
preview: String(row.preview),
|
|
238
|
+
cwd: row.cwd === null ? null : String(row.cwd),
|
|
239
|
+
modelProvider: row.model_provider === null ? null : String(row.model_provider),
|
|
240
|
+
status: String(row.status),
|
|
241
|
+
archived: Boolean(row.archived),
|
|
242
|
+
updatedAt: Number(row.updated_at),
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
listCachedThreads(chatId) {
|
|
246
|
+
const rows = this.db.prepare(`
|
|
247
|
+
SELECT idx, thread_id, name, preview, cwd, model_provider, status, archived, updated_at
|
|
248
|
+
FROM thread_cache
|
|
249
|
+
WHERE chat_id = ?
|
|
250
|
+
ORDER BY idx ASC
|
|
251
|
+
`).all(chatId);
|
|
252
|
+
return rows.map((row) => ({
|
|
253
|
+
index: Number(row.idx),
|
|
254
|
+
threadId: String(row.thread_id),
|
|
255
|
+
name: row.name === null ? null : String(row.name),
|
|
256
|
+
preview: String(row.preview),
|
|
257
|
+
cwd: row.cwd === null ? null : String(row.cwd),
|
|
258
|
+
modelProvider: row.model_provider === null ? null : String(row.model_provider),
|
|
259
|
+
status: String(row.status),
|
|
260
|
+
archived: Boolean(row.archived),
|
|
261
|
+
updatedAt: Number(row.updated_at),
|
|
262
|
+
}));
|
|
263
|
+
}
|
|
264
|
+
savePendingApproval(record) {
|
|
265
|
+
this.db.prepare(`
|
|
266
|
+
INSERT INTO pending_approvals (
|
|
267
|
+
local_id, server_request_id, kind, chat_id, thread_id, turn_id, item_id, approval_id, reason, command, cwd, payload_json, message_id, created_at, resolved_at
|
|
268
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
269
|
+
`).run(record.localId, record.serverRequestId, record.kind, record.chatId, record.threadId, record.turnId, record.itemId, record.approvalId, record.reason, record.command, record.cwd, record.payloadJson, record.messageId, record.createdAt, record.resolvedAt);
|
|
270
|
+
}
|
|
271
|
+
updatePendingApprovalMessage(localId, messageId) {
|
|
272
|
+
this.db.prepare('UPDATE pending_approvals SET message_id = ? WHERE local_id = ?').run(messageId, localId);
|
|
273
|
+
}
|
|
274
|
+
getPendingApproval(localId) {
|
|
275
|
+
const row = this.db.prepare('SELECT * FROM pending_approvals WHERE local_id = ?').get(localId);
|
|
276
|
+
if (!row)
|
|
277
|
+
return null;
|
|
278
|
+
return this.mapApproval(row);
|
|
279
|
+
}
|
|
280
|
+
getPendingApprovalByServerRequestId(serverRequestId) {
|
|
281
|
+
const row = this.db.prepare(`
|
|
282
|
+
SELECT * FROM pending_approvals
|
|
283
|
+
WHERE server_request_id = ? AND resolved_at IS NULL
|
|
284
|
+
ORDER BY created_at DESC
|
|
285
|
+
LIMIT 1
|
|
286
|
+
`).get(serverRequestId);
|
|
287
|
+
if (!row)
|
|
288
|
+
return null;
|
|
289
|
+
return this.mapApproval(row);
|
|
290
|
+
}
|
|
291
|
+
markApprovalResolved(localId) {
|
|
292
|
+
this.db.prepare('UPDATE pending_approvals SET resolved_at = ? WHERE local_id = ?').run(Date.now(), localId);
|
|
293
|
+
}
|
|
294
|
+
countPendingApprovals() {
|
|
295
|
+
const row = this.db.prepare('SELECT COUNT(*) AS count FROM pending_approvals WHERE resolved_at IS NULL').get();
|
|
296
|
+
return Number(row.count);
|
|
297
|
+
}
|
|
298
|
+
saveActiveTurnPreview(record) {
|
|
299
|
+
const now = Date.now();
|
|
300
|
+
this.db.prepare('DELETE FROM active_turn_previews WHERE turn_id = ? OR scope_id = ?').run(record.turnId, record.scopeId);
|
|
301
|
+
this.db.prepare(`
|
|
302
|
+
INSERT INTO active_turn_previews (turn_id, scope_id, thread_id, message_id, created_at, updated_at)
|
|
303
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
304
|
+
`).run(record.turnId, record.scopeId, record.threadId, record.messageId, now, now);
|
|
305
|
+
}
|
|
306
|
+
listActiveTurnPreviews() {
|
|
307
|
+
const rows = this.db.prepare(`
|
|
308
|
+
SELECT turn_id, scope_id, thread_id, message_id, created_at, updated_at
|
|
309
|
+
FROM active_turn_previews
|
|
310
|
+
ORDER BY created_at ASC
|
|
311
|
+
`).all();
|
|
312
|
+
return rows.map((row) => ({
|
|
313
|
+
turnId: String(row.turn_id),
|
|
314
|
+
scopeId: String(row.scope_id),
|
|
315
|
+
threadId: String(row.thread_id),
|
|
316
|
+
messageId: Number(row.message_id),
|
|
317
|
+
createdAt: Number(row.created_at),
|
|
318
|
+
updatedAt: Number(row.updated_at),
|
|
319
|
+
}));
|
|
320
|
+
}
|
|
321
|
+
removeActiveTurnPreview(turnId) {
|
|
322
|
+
this.db.prepare('DELETE FROM active_turn_previews WHERE turn_id = ?').run(turnId);
|
|
323
|
+
}
|
|
324
|
+
removeActiveTurnPreviewByMessage(scopeId, messageId) {
|
|
325
|
+
this.db.prepare('DELETE FROM active_turn_previews WHERE scope_id = ? AND message_id = ?').run(scopeId, messageId);
|
|
326
|
+
}
|
|
327
|
+
savePendingUserInput(record) {
|
|
328
|
+
this.db.prepare(`
|
|
329
|
+
INSERT INTO pending_user_inputs (
|
|
330
|
+
local_id, server_request_id, chat_id, thread_id, turn_id, item_id, message_id,
|
|
331
|
+
questions_json, answers_json, current_question_index, awaiting_free_text, status, created_at, submitted_at, resolved_at
|
|
332
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
333
|
+
ON CONFLICT(local_id) DO UPDATE SET
|
|
334
|
+
server_request_id = excluded.server_request_id,
|
|
335
|
+
chat_id = excluded.chat_id,
|
|
336
|
+
thread_id = excluded.thread_id,
|
|
337
|
+
turn_id = excluded.turn_id,
|
|
338
|
+
item_id = excluded.item_id,
|
|
339
|
+
message_id = excluded.message_id,
|
|
340
|
+
questions_json = excluded.questions_json,
|
|
341
|
+
answers_json = excluded.answers_json,
|
|
342
|
+
current_question_index = excluded.current_question_index,
|
|
343
|
+
awaiting_free_text = excluded.awaiting_free_text,
|
|
344
|
+
status = excluded.status,
|
|
345
|
+
created_at = excluded.created_at,
|
|
346
|
+
submitted_at = excluded.submitted_at,
|
|
347
|
+
resolved_at = excluded.resolved_at
|
|
348
|
+
`).run(record.localId, record.serverRequestId, record.chatId, record.threadId, record.turnId ?? '', record.itemId, record.messageId, record.questionsJson, record.answersJson, record.currentQuestionIndex, record.awaitingFreeText ? 1 : 0, record.status, record.createdAt, record.submittedAt, record.resolvedAt);
|
|
349
|
+
}
|
|
350
|
+
updatePendingUserInputMessage(localId, messageId) {
|
|
351
|
+
this.db.prepare('UPDATE pending_user_inputs SET message_id = ? WHERE local_id = ?').run(messageId, localId);
|
|
352
|
+
}
|
|
353
|
+
updatePendingUserInputAnswers(localId, answersJson, currentQuestionIndex, awaitingFreeText = false) {
|
|
354
|
+
this.db.prepare(`
|
|
355
|
+
UPDATE pending_user_inputs
|
|
356
|
+
SET answers_json = ?, current_question_index = ?, awaiting_free_text = ?
|
|
357
|
+
WHERE local_id = ?
|
|
358
|
+
`).run(answersJson, currentQuestionIndex, awaitingFreeText ? 1 : 0, localId);
|
|
359
|
+
}
|
|
360
|
+
markPendingUserInputSubmitted(localId) {
|
|
361
|
+
this.db.prepare(`
|
|
362
|
+
UPDATE pending_user_inputs
|
|
363
|
+
SET status = 'submitted', submitted_at = ?
|
|
364
|
+
WHERE local_id = ? AND resolved_at IS NULL
|
|
365
|
+
`).run(Date.now(), localId);
|
|
366
|
+
}
|
|
367
|
+
markPendingUserInputResolved(localId) {
|
|
368
|
+
this.db.prepare(`
|
|
369
|
+
UPDATE pending_user_inputs
|
|
370
|
+
SET status = 'resolved', resolved_at = ?
|
|
371
|
+
WHERE local_id = ?
|
|
372
|
+
`).run(Date.now(), localId);
|
|
373
|
+
}
|
|
374
|
+
markPendingUserInputInterrupted(localId) {
|
|
375
|
+
this.db.prepare(`
|
|
376
|
+
UPDATE pending_user_inputs
|
|
377
|
+
SET status = 'interrupted', resolved_at = ?
|
|
378
|
+
WHERE local_id = ?
|
|
379
|
+
`).run(Date.now(), localId);
|
|
380
|
+
}
|
|
381
|
+
listPendingUserInputs() {
|
|
382
|
+
const rows = this.db.prepare(`
|
|
383
|
+
SELECT local_id, server_request_id, chat_id, thread_id, turn_id, item_id, message_id,
|
|
384
|
+
questions_json, answers_json, current_question_index, awaiting_free_text, status, created_at, submitted_at, resolved_at
|
|
385
|
+
FROM pending_user_inputs
|
|
386
|
+
WHERE resolved_at IS NULL
|
|
387
|
+
ORDER BY created_at ASC
|
|
388
|
+
`).all();
|
|
389
|
+
return rows.map((row) => this.mapPendingUserInput(row));
|
|
390
|
+
}
|
|
391
|
+
countPendingUserInputs() {
|
|
392
|
+
const row = this.db.prepare('SELECT COUNT(*) AS count FROM pending_user_inputs WHERE resolved_at IS NULL').get();
|
|
393
|
+
return Number(row?.count ?? 0);
|
|
394
|
+
}
|
|
395
|
+
insertAudit(direction, chatId, eventType, summary) {
|
|
396
|
+
this.db.prepare('INSERT INTO audit_logs (direction, chat_id, event_type, summary, created_at) VALUES (?, ?, ?, ?, ?)').run(direction, chatId, eventType, summary, Date.now());
|
|
397
|
+
}
|
|
398
|
+
close() {
|
|
399
|
+
this.db.close();
|
|
400
|
+
}
|
|
401
|
+
mapApproval(row) {
|
|
402
|
+
return {
|
|
403
|
+
localId: String(row.local_id),
|
|
404
|
+
serverRequestId: String(row.server_request_id),
|
|
405
|
+
kind: row.kind === 'fileChange' ? 'fileChange' : row.kind === 'permissions' ? 'permissions' : 'command',
|
|
406
|
+
chatId: String(row.chat_id),
|
|
407
|
+
threadId: String(row.thread_id),
|
|
408
|
+
turnId: String(row.turn_id),
|
|
409
|
+
itemId: String(row.item_id),
|
|
410
|
+
approvalId: row.approval_id === null ? null : String(row.approval_id),
|
|
411
|
+
reason: row.reason === null ? null : String(row.reason),
|
|
412
|
+
command: row.command === null ? null : String(row.command),
|
|
413
|
+
cwd: row.cwd === null ? null : String(row.cwd),
|
|
414
|
+
payloadJson: row.payload_json === null ? null : String(row.payload_json),
|
|
415
|
+
messageId: row.message_id === null ? null : Number(row.message_id),
|
|
416
|
+
createdAt: Number(row.created_at),
|
|
417
|
+
resolvedAt: row.resolved_at === null ? null : Number(row.resolved_at)
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
mapPendingUserInput(row) {
|
|
421
|
+
return {
|
|
422
|
+
localId: String(row.local_id),
|
|
423
|
+
serverRequestId: String(row.server_request_id),
|
|
424
|
+
chatId: String(row.chat_id),
|
|
425
|
+
threadId: String(row.thread_id),
|
|
426
|
+
turnId: row.turn_id === null || String(row.turn_id) === '' ? null : String(row.turn_id),
|
|
427
|
+
itemId: String(row.item_id),
|
|
428
|
+
messageId: row.message_id === null ? null : Number(row.message_id),
|
|
429
|
+
questionsJson: String(row.questions_json),
|
|
430
|
+
answersJson: String(row.answers_json),
|
|
431
|
+
currentQuestionIndex: Number(row.current_question_index),
|
|
432
|
+
awaitingFreeText: Boolean(row.awaiting_free_text),
|
|
433
|
+
status: row.status === null ? 'pending' : String(row.status),
|
|
434
|
+
createdAt: Number(row.created_at),
|
|
435
|
+
submittedAt: row.submitted_at === null ? null : Number(row.submitted_at),
|
|
436
|
+
resolvedAt: row.resolved_at === null ? null : Number(row.resolved_at),
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
writeChatSettings(chatId, model, reasoningEffort, locale, accessPreset, collaborationMode, serviceTier, activeTurnMessageMode) {
|
|
440
|
+
this.db.prepare(`
|
|
441
|
+
INSERT INTO chat_settings (chat_id, model, reasoning_effort, locale, access_preset, collaboration_mode, service_tier, active_turn_message_mode, updated_at)
|
|
442
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
443
|
+
ON CONFLICT(chat_id) DO UPDATE SET
|
|
444
|
+
model = excluded.model,
|
|
445
|
+
reasoning_effort = excluded.reasoning_effort,
|
|
446
|
+
locale = excluded.locale,
|
|
447
|
+
access_preset = excluded.access_preset,
|
|
448
|
+
collaboration_mode = excluded.collaboration_mode,
|
|
449
|
+
service_tier = excluded.service_tier,
|
|
450
|
+
active_turn_message_mode = excluded.active_turn_message_mode,
|
|
451
|
+
updated_at = excluded.updated_at
|
|
452
|
+
`).run(chatId, model, reasoningEffort, locale, accessPreset, collaborationMode, serviceTier, activeTurnMessageMode, Date.now());
|
|
453
|
+
}
|
|
454
|
+
getWeixinContextToken(scopeId) {
|
|
455
|
+
const row = this.db.prepare('SELECT context_token FROM weixin_context_tokens WHERE scope_id = ?').get(scopeId);
|
|
456
|
+
return row ? String(row.context_token) : null;
|
|
457
|
+
}
|
|
458
|
+
setWeixinContextToken(scopeId, contextToken) {
|
|
459
|
+
this.db.prepare(`
|
|
460
|
+
INSERT INTO weixin_context_tokens (scope_id, context_token, updated_at)
|
|
461
|
+
VALUES (?, ?, ?)
|
|
462
|
+
ON CONFLICT(scope_id) DO UPDATE SET context_token = excluded.context_token, updated_at = excluded.updated_at
|
|
463
|
+
`).run(scopeId, contextToken, Date.now());
|
|
464
|
+
}
|
|
465
|
+
listDisabledCodexAuthCandidateNames() {
|
|
466
|
+
const rows = this.db.prepare('SELECT name FROM codex_auth_candidates WHERE disabled = 1').all();
|
|
467
|
+
return new Set(rows.map(row => String(row.name)));
|
|
468
|
+
}
|
|
469
|
+
setCodexAuthCandidateDisabled(name, disabled) {
|
|
470
|
+
this.db.prepare(`
|
|
471
|
+
INSERT INTO codex_auth_candidates (name, disabled, updated_at)
|
|
472
|
+
VALUES (?, ?, ?)
|
|
473
|
+
ON CONFLICT(name) DO UPDATE SET disabled = excluded.disabled, updated_at = excluded.updated_at
|
|
474
|
+
`).run(name, disabled ? 1 : 0, Date.now());
|
|
475
|
+
}
|
|
476
|
+
ensureColumn(table, column, definition) {
|
|
477
|
+
const columns = this.db.prepare(`PRAGMA table_info(${table})`).all();
|
|
478
|
+
if (columns.some(entry => entry.name === column)) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
function normalizeCollaborationMode(value) {
|
|
485
|
+
return value === 'default' || value === 'plan' ? value : null;
|
|
486
|
+
}
|
|
487
|
+
function normalizeActiveTurnMessageMode(value) {
|
|
488
|
+
return value === 'steer' || value === 'queue' ? value : null;
|
|
489
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
/**
|
|
3
|
+
* One-time migration: prefix legacy Telegram-only keys so future channels (e.g. weixin) cannot collide.
|
|
4
|
+
* Idempotent: rows already prefixed are skipped.
|
|
5
|
+
*/
|
|
6
|
+
export declare function migrateLegacyBridgeScopeIds(db: DatabaseSync): void;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { BRIDGE_SCOPE_TELEGRAM_PREFIX, isBridgeScopedKey } from '../core/bridge_scope.js';
|
|
2
|
+
/**
|
|
3
|
+
* One-time migration: prefix legacy Telegram-only keys so future channels (e.g. weixin) cannot collide.
|
|
4
|
+
* Idempotent: rows already prefixed are skipped.
|
|
5
|
+
*/
|
|
6
|
+
export function migrateLegacyBridgeScopeIds(db) {
|
|
7
|
+
const needsMigrate = (key) => !isBridgeScopedKey(key);
|
|
8
|
+
db.exec('BEGIN IMMEDIATE');
|
|
9
|
+
try {
|
|
10
|
+
const bindingRows = db.prepare('SELECT chat_id FROM chat_bindings').all();
|
|
11
|
+
for (const row of bindingRows) {
|
|
12
|
+
if (needsMigrate(row.chat_id)) {
|
|
13
|
+
db.prepare('UPDATE chat_bindings SET chat_id = ? WHERE chat_id = ?').run(`${BRIDGE_SCOPE_TELEGRAM_PREFIX}${row.chat_id}`, row.chat_id);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const settingsRows = db.prepare('SELECT chat_id FROM chat_settings').all();
|
|
17
|
+
for (const row of settingsRows) {
|
|
18
|
+
if (needsMigrate(row.chat_id)) {
|
|
19
|
+
db.prepare('UPDATE chat_settings SET chat_id = ? WHERE chat_id = ?').run(`${BRIDGE_SCOPE_TELEGRAM_PREFIX}${row.chat_id}`, row.chat_id);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const cacheRows = db.prepare('SELECT DISTINCT chat_id FROM thread_cache').all();
|
|
23
|
+
for (const row of cacheRows) {
|
|
24
|
+
if (needsMigrate(row.chat_id)) {
|
|
25
|
+
const next = `${BRIDGE_SCOPE_TELEGRAM_PREFIX}${row.chat_id}`;
|
|
26
|
+
db.prepare('UPDATE thread_cache SET chat_id = ? WHERE chat_id = ?').run(next, row.chat_id);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const approvalRows = db.prepare('SELECT local_id, chat_id FROM pending_approvals').all();
|
|
30
|
+
for (const row of approvalRows) {
|
|
31
|
+
if (needsMigrate(row.chat_id)) {
|
|
32
|
+
db.prepare('UPDATE pending_approvals SET chat_id = ? WHERE local_id = ?').run(`${BRIDGE_SCOPE_TELEGRAM_PREFIX}${row.chat_id}`, row.local_id);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const userInputRows = db.prepare('SELECT local_id, chat_id FROM pending_user_inputs').all();
|
|
36
|
+
for (const row of userInputRows) {
|
|
37
|
+
if (needsMigrate(row.chat_id)) {
|
|
38
|
+
db.prepare('UPDATE pending_user_inputs SET chat_id = ? WHERE local_id = ?').run(`${BRIDGE_SCOPE_TELEGRAM_PREFIX}${row.chat_id}`, row.local_id);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const auditRows = db.prepare('SELECT id, chat_id FROM audit_logs').all();
|
|
42
|
+
for (const row of auditRows) {
|
|
43
|
+
if (needsMigrate(row.chat_id)) {
|
|
44
|
+
db.prepare('UPDATE audit_logs SET chat_id = ? WHERE id = ?').run(`${BRIDGE_SCOPE_TELEGRAM_PREFIX}${row.chat_id}`, row.id);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const previewRows = db.prepare('SELECT turn_id, scope_id FROM active_turn_previews').all();
|
|
48
|
+
for (const row of previewRows) {
|
|
49
|
+
if (needsMigrate(row.scope_id)) {
|
|
50
|
+
db.prepare('UPDATE active_turn_previews SET scope_id = ? WHERE turn_id = ?').run(`${BRIDGE_SCOPE_TELEGRAM_PREFIX}${row.scope_id}`, row.turn_id);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
db.exec('COMMIT');
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
db.exec('ROLLBACK');
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ParsedCommand } from '../controller/commands.js';
|
|
2
|
+
export interface TelegramMessageEntity {
|
|
3
|
+
type: string;
|
|
4
|
+
offset: number;
|
|
5
|
+
length: number;
|
|
6
|
+
}
|
|
7
|
+
interface ResolveTelegramAddressingParams {
|
|
8
|
+
text: string;
|
|
9
|
+
attachmentsCount: number;
|
|
10
|
+
entities: readonly TelegramMessageEntity[];
|
|
11
|
+
command: ParsedCommand | null;
|
|
12
|
+
botUsername: string | null;
|
|
13
|
+
isDefaultTopic: boolean;
|
|
14
|
+
replyToBot: boolean;
|
|
15
|
+
}
|
|
16
|
+
export type TelegramAddressingDecision = {
|
|
17
|
+
kind: 'ignore';
|
|
18
|
+
} | {
|
|
19
|
+
kind: 'command';
|
|
20
|
+
command: ParsedCommand;
|
|
21
|
+
} | {
|
|
22
|
+
kind: 'prompt';
|
|
23
|
+
text: string;
|
|
24
|
+
};
|
|
25
|
+
interface DefaultScopeParams {
|
|
26
|
+
chatType: string;
|
|
27
|
+
allowedChatId: string | null;
|
|
28
|
+
allowedTopicId: number | null;
|
|
29
|
+
topicId: number | null;
|
|
30
|
+
}
|
|
31
|
+
export declare function resolveTelegramAddressing(params: ResolveTelegramAddressingParams): TelegramAddressingDecision;
|
|
32
|
+
export declare function isDefaultTelegramScope(params: DefaultScopeParams): boolean;
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export function resolveTelegramAddressing(params) {
|
|
2
|
+
const botUsername = normalizeBotUsername(params.botUsername);
|
|
3
|
+
if (params.command) {
|
|
4
|
+
if (isCommandAddressedToThisBot(params.command, botUsername, params.isDefaultTopic)) {
|
|
5
|
+
return { kind: 'command', command: params.command };
|
|
6
|
+
}
|
|
7
|
+
return { kind: 'ignore' };
|
|
8
|
+
}
|
|
9
|
+
const stripped = stripLeadingBotMention(params.text, params.entities, botUsername);
|
|
10
|
+
const explicitMention = stripped !== null;
|
|
11
|
+
const text = stripped ?? params.text;
|
|
12
|
+
const normalizedText = text.trim();
|
|
13
|
+
if (params.isDefaultTopic || params.replyToBot || explicitMention) {
|
|
14
|
+
if (normalizedText || params.attachmentsCount > 0) {
|
|
15
|
+
return { kind: 'prompt', text: normalizedText };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return { kind: 'ignore' };
|
|
19
|
+
}
|
|
20
|
+
export function isDefaultTelegramScope(params) {
|
|
21
|
+
if (params.chatType === 'private') {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
if (params.allowedChatId === null) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
if (params.allowedTopicId === null) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
return params.topicId === params.allowedTopicId;
|
|
31
|
+
}
|
|
32
|
+
function normalizeBotUsername(botUsername) {
|
|
33
|
+
if (!botUsername)
|
|
34
|
+
return null;
|
|
35
|
+
return botUsername.trim().replace(/^@+/, '').toLowerCase() || null;
|
|
36
|
+
}
|
|
37
|
+
function isCommandAddressedToThisBot(command, botUsername, isDefaultTopic) {
|
|
38
|
+
if (command.targetBot === null) {
|
|
39
|
+
return isDefaultTopic;
|
|
40
|
+
}
|
|
41
|
+
if (!botUsername) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
return command.targetBot === botUsername;
|
|
45
|
+
}
|
|
46
|
+
function stripLeadingBotMention(text, entities, botUsername) {
|
|
47
|
+
if (!botUsername)
|
|
48
|
+
return null;
|
|
49
|
+
const mentionEntity = entities.find(entity => entity.type === 'mention' && entity.offset === 0);
|
|
50
|
+
if (!mentionEntity)
|
|
51
|
+
return null;
|
|
52
|
+
const mentionText = text.slice(mentionEntity.offset, mentionEntity.offset + mentionEntity.length);
|
|
53
|
+
if (mentionText.toLowerCase() !== `@${botUsername}`) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
return text.slice(mentionEntity.length).replace(/^[\s,:;-]+/, '');
|
|
57
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface TelegramApiResult<T> {
|
|
2
|
+
ok: boolean;
|
|
3
|
+
result?: T;
|
|
4
|
+
description?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface TelegramRemoteFile {
|
|
7
|
+
file_id: string;
|
|
8
|
+
file_unique_id: string;
|
|
9
|
+
file_size?: number;
|
|
10
|
+
file_path?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function callTelegramApi<T>(botToken: string, method: string, body: Record<string, unknown>): Promise<TelegramApiResult<T>>;
|
|
13
|
+
export declare function getTelegramFile(botToken: string, fileId: string): Promise<TelegramRemoteFile>;
|
|
14
|
+
export declare function downloadTelegramFile(botToken: string, remoteFilePath: string, destinationPath: string): Promise<number>;
|