@aliwey/bmo 2.1.0 â 2.1.2
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/bin/bmo.js +27 -13
- package/config/__pycache__/settings.cpython-313.pyc +0 -0
- package/core/__pycache__/bmo_engine.cpython-313.pyc +0 -0
- package/core/__pycache__/bot_client.cpython-313.pyc +0 -0
- package/core/bmo_engine.py +2 -1
- package/package.json +1 -1
- package/scripts/bmo_init.js +46 -4
- package/scripts/postinstall.js +13 -11
- package/scripts/web_cmd.js +34 -10
- package/storage/__pycache__/sqlite_storage.cpython-313.pyc +0 -0
- package/storage/sqlite_storage.py +64 -13
package/bin/bmo.js
CHANGED
|
@@ -163,20 +163,34 @@ Data lives in: ${BMO_HOME_DISPLAY}
|
|
|
163
163
|
// Ignore failures
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
console.
|
|
177
|
-
process.exit(
|
|
166
|
+
if (os.platform() === 'win32') {
|
|
167
|
+
console.log('Launching updater in a new window to release folder/file locks...');
|
|
168
|
+
|
|
169
|
+
const cmdStr = `timeout /t 2 /nobreak >nul && echo [BMO Updater] Upgrading to @aliwey/bmo${ver}... && npm install -g @aliwey/bmo${ver} && echo [OK] BMO updated successfully! Press any key to close. && pause`;
|
|
170
|
+
|
|
171
|
+
spawn('cmd.exe', ['/c', 'start', 'cmd.exe', '/c', cmdStr], {
|
|
172
|
+
detached: true,
|
|
173
|
+
stdio: 'ignore'
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
console.log('Updater launched. This window will now close to release file locks.');
|
|
177
|
+
process.exit(0);
|
|
178
|
+
} else {
|
|
179
|
+
console.log(`Uninstalling old version of @aliwey/bmo...`);
|
|
180
|
+
try {
|
|
181
|
+
execSync('npm uninstall -g @aliwey/bmo', { stdio: 'inherit' });
|
|
182
|
+
} catch (e) {
|
|
183
|
+
// Ignore uninstall failures if it wasn't already installed
|
|
184
|
+
}
|
|
185
|
+
console.log(`Installing @aliwey/bmo${ver}...`);
|
|
186
|
+
try {
|
|
187
|
+
execSync(`npm install -g @aliwey/bmo${ver}`, { stdio: 'inherit' });
|
|
188
|
+
} catch {
|
|
189
|
+
console.error('Update failed. Try manually: npm install -g @aliwey/bmo');
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
process.exit(0);
|
|
178
193
|
}
|
|
179
|
-
process.exit(0);
|
|
180
194
|
}
|
|
181
195
|
|
|
182
196
|
// ââ bmo init ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/core/bmo_engine.py
CHANGED
|
@@ -12,7 +12,7 @@ from uuid import uuid4
|
|
|
12
12
|
from datetime import datetime
|
|
13
13
|
from typing import Optional, List, Dict, AsyncGenerator
|
|
14
14
|
|
|
15
|
-
from config.settings import OPENCODE_BASE_URL, BFP_RELAY_URL, BFP_TRANSPORT_PORT, BFP_A2A_PORT
|
|
15
|
+
from config.settings import OPENCODE_BASE_URL, BFP_RELAY_URL, BFP_TRANSPORT_PORT, BFP_A2A_PORT, OWNER_ID, ALLOWED_USER_IDS
|
|
16
16
|
from core.bot_client import OpenCodeBotClient
|
|
17
17
|
from core.worker_manager import WorkerManager
|
|
18
18
|
from models.chat_models import ChatSession, ChatMessage
|
|
@@ -38,6 +38,7 @@ class BMOEngine:
|
|
|
38
38
|
self.storage = SQLiteStorage(db_path=db_path)
|
|
39
39
|
else:
|
|
40
40
|
self.storage = get_storage()
|
|
41
|
+
self.storage.sync_session_groups(OWNER_ID, ALLOWED_USER_IDS)
|
|
41
42
|
self.client = OpenCodeBotClient()
|
|
42
43
|
self.worker_manager = WorkerManager(backend=worker_backend)
|
|
43
44
|
self.client.set_worker_manager(self.worker_manager)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aliwey/bmo",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.2",
|
|
4
4
|
"description": "BMO â AI coding assistant with Telegram, CLI & Web sync. One command, all frontends.",
|
|
5
5
|
"keywords": ["ai", "coding-assistant", "telegram-bot", "cli", "opencode", "bfp"],
|
|
6
6
|
"homepage": "https://github.com/aliwey/bmo",
|
package/scripts/bmo_init.js
CHANGED
|
@@ -13,6 +13,7 @@ const os = require('os');
|
|
|
13
13
|
|
|
14
14
|
const BMO_HOME = process.env.BMO_HOME || path.join(os.homedir(), '.bmo');
|
|
15
15
|
const ENV_PATH = path.join(BMO_HOME, '.env');
|
|
16
|
+
const PKG_DIR = path.join(__dirname, '..');
|
|
16
17
|
|
|
17
18
|
function mkdirp(p) { fs.mkdirSync(p, { recursive: true }); }
|
|
18
19
|
|
|
@@ -26,9 +27,9 @@ function ask(rl, question, defaultValue) {
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
(async () => {
|
|
29
|
-
console.log('\n
|
|
30
|
+
console.log('\nâââââââââââââââââââââââââââââââââââââââââââââŽ');
|
|
30
31
|
console.log('â BMO Setup Wizard â');
|
|
31
|
-
console.log('
|
|
32
|
+
console.log('â°ââââââââââââââââââââââââââââââââââââââââââââ¯\n');
|
|
32
33
|
|
|
33
34
|
// Load existing .env values as defaults
|
|
34
35
|
let existing = {};
|
|
@@ -38,7 +39,24 @@ function ask(rl, question, defaultValue) {
|
|
|
38
39
|
const m = line.match(/^([^#=]+)=(.*)$/);
|
|
39
40
|
if (m) existing[m[1].trim()] = m[2].trim();
|
|
40
41
|
}
|
|
41
|
-
console.log(`
|
|
42
|
+
console.log(` [Info] Existing config found at ${ENV_PATH} â press Enter to keep values.\n`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Load existing memory.md values if available
|
|
46
|
+
let existing_name = '';
|
|
47
|
+
let existing_role = '';
|
|
48
|
+
let existing_preference = '';
|
|
49
|
+
const userMemoryPath = path.join(BMO_HOME, 'data', 'memory.md');
|
|
50
|
+
if (fs.existsSync(userMemoryPath)) {
|
|
51
|
+
try {
|
|
52
|
+
const memoryContent = fs.readFileSync(userMemoryPath, 'utf8');
|
|
53
|
+
const nameMatch = memoryContent.match(/-\s*Name:\s*(.*)/i);
|
|
54
|
+
const roleMatch = memoryContent.match(/-\s*Role:\s*(.*)/i);
|
|
55
|
+
const prefMatch = memoryContent.match(/-\s*Preference:\s*(.*)/i);
|
|
56
|
+
if (nameMatch && nameMatch[1].trim() !== '[Enter Name]') existing_name = nameMatch[1].trim();
|
|
57
|
+
if (roleMatch && roleMatch[1].trim() !== '[Enter Role]') existing_role = roleMatch[1].trim();
|
|
58
|
+
if (prefMatch && prefMatch[1].trim() !== '[Enter Preference]') existing_preference = prefMatch[1].trim();
|
|
59
|
+
} catch (e) {}
|
|
42
60
|
}
|
|
43
61
|
|
|
44
62
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -73,6 +91,13 @@ function ask(rl, question, defaultValue) {
|
|
|
73
91
|
existing.BFP_REGISTRY_URL || 'https://bfp-registry.aliwey.workers.dev'
|
|
74
92
|
);
|
|
75
93
|
|
|
94
|
+
// ââ User Profile ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
95
|
+
console.log('\nââ User Profile âââââââââââââââââââââââââââââââââââââ');
|
|
96
|
+
console.log(' Provide details to personalize your BMO companion.');
|
|
97
|
+
const userName = await ask(rl, 'Your Name', existing_name || 'Developer');
|
|
98
|
+
const userRole = await ask(rl, 'Your Knowledge / Background Description', existing_role || 'Python/Web Developer');
|
|
99
|
+
const userPref = await ask(rl, 'What should BMO do / Where should BMO focus', existing_preference || 'Assist with software development, debugging, and task automation');
|
|
100
|
+
|
|
76
101
|
rl.close();
|
|
77
102
|
|
|
78
103
|
// ââ Write .env ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
@@ -106,8 +131,25 @@ function ask(rl, question, defaultValue) {
|
|
|
106
131
|
|
|
107
132
|
fs.writeFileSync(ENV_PATH, envContent, 'utf8');
|
|
108
133
|
|
|
134
|
+
// ââ Update memory.md ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
135
|
+
const packageMemoryPath = path.join(PKG_DIR, 'memory.md');
|
|
136
|
+
|
|
137
|
+
let memoryContent = '';
|
|
138
|
+
if (fs.existsSync(userMemoryPath)) {
|
|
139
|
+
memoryContent = fs.readFileSync(userMemoryPath, 'utf8');
|
|
140
|
+
} else if (fs.existsSync(packageMemoryPath)) {
|
|
141
|
+
memoryContent = fs.readFileSync(packageMemoryPath, 'utf8');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (memoryContent) {
|
|
145
|
+
memoryContent = memoryContent.replace(/-\s*Name:\s*[^\r\n]*/i, `- Name: ${userName}`);
|
|
146
|
+
memoryContent = memoryContent.replace(/-\s*Role:\s*[^\r\n]*/i, `- Role: ${userRole}`);
|
|
147
|
+
memoryContent = memoryContent.replace(/-\s*Preference:\s*[^\r\n]*/i, `- Preference: ${userPref}`);
|
|
148
|
+
fs.writeFileSync(userMemoryPath, memoryContent, 'utf8');
|
|
149
|
+
}
|
|
150
|
+
|
|
109
151
|
console.log('\nâââââââââââââââââââââââââââââââââââââââââââââââââââââŽ');
|
|
110
|
-
console.log(
|
|
152
|
+
console.log('â' + ` [OK] Config saved to ${ENV_PATH}`.padEnd(51) + 'â');
|
|
111
153
|
console.log('â â');
|
|
112
154
|
console.log('â Run: bmo â start BMO â');
|
|
113
155
|
console.log('â bmo relay â go online for BFP discovery â');
|
package/scripts/postinstall.js
CHANGED
|
@@ -53,9 +53,9 @@ const CF_URLS = {
|
|
|
53
53
|
// ââ Helpers ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
54
54
|
|
|
55
55
|
const step = (n, msg) => console.log(`\n[${n}/5] ${msg}`);
|
|
56
|
-
const ok = msg => console.log(`
|
|
57
|
-
const warn = msg => console.log(`
|
|
58
|
-
const err = msg => console.error(`
|
|
56
|
+
const ok = msg => console.log(` [OK] ${msg}`);
|
|
57
|
+
const warn = msg => console.log(` [Warning] ${msg}`);
|
|
58
|
+
const err = msg => console.error(` [Error] ${msg}`);
|
|
59
59
|
|
|
60
60
|
function mkdirp(p) { fs.mkdirSync(p, { recursive: true }); }
|
|
61
61
|
|
|
@@ -250,16 +250,18 @@ function installWebchatDeps() {
|
|
|
250
250
|
await installCloudflared();
|
|
251
251
|
installWebchatDeps();
|
|
252
252
|
|
|
253
|
-
console.log('\n
|
|
254
|
-
console.log('â
|
|
255
|
-
console.log('â
|
|
256
|
-
console.log('â
|
|
257
|
-
console.log('â bmo init
|
|
258
|
-
console.log('â
|
|
259
|
-
console.log('
|
|
253
|
+
console.log('\nâââââââââââââââââââââââââââââââââââââââââââââââââââââŽ');
|
|
254
|
+
console.log('â [OK] BMO installed successfully! â');
|
|
255
|
+
console.log('â â');
|
|
256
|
+
console.log('â IMPORTANT: You must run the setup wizard first: â');
|
|
257
|
+
console.log('â bmo --init â');
|
|
258
|
+
console.log('â â');
|
|
259
|
+
console.log('â Then start BMO: â');
|
|
260
|
+
console.log('â bmo â');
|
|
261
|
+
console.log('â°ââââââââââââââââââââââââââââââââââââââââââââââââââââ¯\n');
|
|
260
262
|
} catch (e) {
|
|
261
263
|
err(`Installation failed: ${e.message}`);
|
|
262
|
-
console.log('\nRun `bmo init` to retry configuration, or check the docs.');
|
|
264
|
+
console.log('\nRun `bmo --init` to retry configuration, or check the docs.');
|
|
263
265
|
process.exit(1);
|
|
264
266
|
}
|
|
265
267
|
})();
|
package/scripts/web_cmd.js
CHANGED
|
@@ -37,7 +37,7 @@ function getCloudflaredExe() {
|
|
|
37
37
|
: path.join(BMO_BIN, 'cloudflared');
|
|
38
38
|
if (fs.existsSync(embedded)) return embedded;
|
|
39
39
|
try { execSync('cloudflared --version', { stdio: 'ignore' }); return 'cloudflared'; } catch {}
|
|
40
|
-
console.error('
|
|
40
|
+
console.error('[Error] cloudflared not found. Run: bmo init');
|
|
41
41
|
process.exit(1);
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -70,7 +70,7 @@ function waitForTunnelUrl(proc) {
|
|
|
70
70
|
const tasks = loadTasks();
|
|
71
71
|
const existing = tasks.webchat_tunnel_url;
|
|
72
72
|
if (existing) {
|
|
73
|
-
console.log(`\n
|
|
73
|
+
console.log(`\n[Info] Webchat already running!`);
|
|
74
74
|
console.log(` Local: http://127.0.0.1:${WEBCHAT_PORT}`);
|
|
75
75
|
console.log(` Public: ${existing}\n`);
|
|
76
76
|
process.exit(0);
|
|
@@ -78,11 +78,18 @@ function waitForTunnelUrl(proc) {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
// Start webchat server
|
|
81
|
-
console.log('
|
|
81
|
+
console.log('Starting webchat server...');
|
|
82
82
|
const webchatDir = path.join(PKG_DIR, 'webchat');
|
|
83
|
+
|
|
84
|
+
// Ensure logs directory exists
|
|
85
|
+
const logDir = path.join(BMO_HOME, 'logs');
|
|
86
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
87
|
+
const webchatLogPath = path.join(logDir, 'webchat.log');
|
|
88
|
+
const logStream = fs.openSync(webchatLogPath, 'a');
|
|
89
|
+
|
|
83
90
|
const webchat = spawn('node', ['server.js'], {
|
|
84
91
|
cwd: webchatDir,
|
|
85
|
-
stdio: 'ignore',
|
|
92
|
+
stdio: ['ignore', logStream, logStream],
|
|
86
93
|
detached: true,
|
|
87
94
|
env: {
|
|
88
95
|
...process.env,
|
|
@@ -94,15 +101,32 @@ function waitForTunnelUrl(proc) {
|
|
|
94
101
|
webchat.unref();
|
|
95
102
|
|
|
96
103
|
// Wait for webchat to be ready
|
|
104
|
+
let started = false;
|
|
97
105
|
for (let i = 0; i < 20; i++) {
|
|
98
106
|
await sleep(500);
|
|
99
|
-
if (await isPortOpen(WEBCHAT_PORT))
|
|
100
|
-
|
|
107
|
+
if (await isPortOpen(WEBCHAT_PORT)) {
|
|
108
|
+
started = true;
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!started) {
|
|
114
|
+
console.error('[Error] Webchat failed to start');
|
|
115
|
+
try {
|
|
116
|
+
const logContent = fs.readFileSync(webchatLogPath, 'utf8').trim();
|
|
117
|
+
const lines = logContent.split('\n');
|
|
118
|
+
const lastLines = lines.slice(-15).join('\n');
|
|
119
|
+
console.error('\nLast log output:');
|
|
120
|
+
console.error(lastLines);
|
|
121
|
+
} catch (e) {
|
|
122
|
+
console.error('Could not read webchat log file.');
|
|
123
|
+
}
|
|
124
|
+
process.exit(1);
|
|
101
125
|
}
|
|
102
|
-
console.log('
|
|
126
|
+
console.log('[OK] Webchat server running on port', WEBCHAT_PORT);
|
|
103
127
|
|
|
104
128
|
// Start cloudflared tunnel
|
|
105
|
-
console.log('
|
|
129
|
+
console.log('Starting cloudflared tunnel...');
|
|
106
130
|
const cfExe = getCloudflaredExe();
|
|
107
131
|
const cf = spawn(cfExe, ['tunnel', '--url', `http://localhost:${WEBCHAT_PORT}`], {
|
|
108
132
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -114,7 +138,7 @@ function waitForTunnelUrl(proc) {
|
|
|
114
138
|
try {
|
|
115
139
|
tunnelUrl = await waitForTunnelUrl(cf);
|
|
116
140
|
} catch {
|
|
117
|
-
console.error('
|
|
141
|
+
console.error('[Error] Could not get tunnel URL from cloudflared');
|
|
118
142
|
process.exit(1);
|
|
119
143
|
}
|
|
120
144
|
|
|
@@ -127,7 +151,7 @@ function waitForTunnelUrl(proc) {
|
|
|
127
151
|
saveTasks(tasks);
|
|
128
152
|
|
|
129
153
|
console.log('\nââââââââââââââââââââââââââââââââââââââââââââââŽ');
|
|
130
|
-
console.log('â
|
|
154
|
+
console.log('â [OK] BMO Webchat is live! â');
|
|
131
155
|
console.log(`â Local: http://127.0.0.1:${WEBCHAT_PORT} â`);
|
|
132
156
|
console.log(`â Public: ${tunnelUrl.padEnd(34)} â`);
|
|
133
157
|
console.log('â°âââââââââââââââââââââââââââââââââââââââââââââ¯\n');
|
|
Binary file
|
|
@@ -117,6 +117,11 @@ class SQLiteStorage:
|
|
|
117
117
|
created_at REAL NOT NULL,
|
|
118
118
|
UNIQUE(user_id, scope)
|
|
119
119
|
);
|
|
120
|
+
|
|
121
|
+
CREATE TABLE IF NOT EXISTS session_groups (
|
|
122
|
+
chat_id INTEGER PRIMARY KEY,
|
|
123
|
+
group_id INTEGER NOT NULL
|
|
124
|
+
);
|
|
120
125
|
""")
|
|
121
126
|
conn.commit()
|
|
122
127
|
|
|
@@ -254,6 +259,22 @@ class SQLiteStorage:
|
|
|
254
259
|
except Exception as e:
|
|
255
260
|
print(f"_fix_active_sessions error: {e}")
|
|
256
261
|
|
|
262
|
+
# ââ Session Groups âââââââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
263
|
+
|
|
264
|
+
def sync_session_groups(self, owner_id: int, allowed_ids: set[int]):
|
|
265
|
+
"""Populate session_groups table from OWNER_ID + ALLOWED_USER_IDS config."""
|
|
266
|
+
if not owner_id and not allowed_ids:
|
|
267
|
+
return
|
|
268
|
+
group_id = owner_id or next(iter(allowed_ids))
|
|
269
|
+
ids_to_map = {owner_id} if owner_id else set()
|
|
270
|
+
ids_to_map.update(allowed_ids)
|
|
271
|
+
for cid in ids_to_map:
|
|
272
|
+
self._exec(
|
|
273
|
+
"INSERT OR REPLACE INTO session_groups (chat_id, group_id) VALUES (?, ?)",
|
|
274
|
+
(cid, group_id),
|
|
275
|
+
)
|
|
276
|
+
self._conn().commit()
|
|
277
|
+
|
|
257
278
|
# ââ Session CRUD ââââââââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
258
279
|
|
|
259
280
|
def save_session(self, session: ChatSession) -> bool:
|
|
@@ -292,16 +313,17 @@ class SQLiteStorage:
|
|
|
292
313
|
),
|
|
293
314
|
)
|
|
294
315
|
|
|
295
|
-
# Only set active session pointer if none exists yet for this
|
|
316
|
+
# Only set active session pointer if none exists yet for this group.
|
|
296
317
|
# This prevents save_session from overwriting a manual session switch.
|
|
318
|
+
group_id = self._resolve_group(session.chat_id)
|
|
297
319
|
existing_active = self._fetchone(
|
|
298
320
|
"SELECT session_id FROM active_sessions WHERE chat_id = ?",
|
|
299
|
-
(
|
|
321
|
+
(group_id,),
|
|
300
322
|
)
|
|
301
323
|
if not existing_active:
|
|
302
324
|
self._exec(
|
|
303
325
|
"INSERT OR REPLACE INTO active_sessions (chat_id, session_id) VALUES (?, ?)",
|
|
304
|
-
(
|
|
326
|
+
(group_id, session.session_id),
|
|
305
327
|
)
|
|
306
328
|
|
|
307
329
|
self._conn().commit()
|
|
@@ -312,9 +334,10 @@ class SQLiteStorage:
|
|
|
312
334
|
|
|
313
335
|
def load_session(self, chat_id: int) -> Optional[ChatSession]:
|
|
314
336
|
try:
|
|
337
|
+
group_id = self._resolve_group(chat_id)
|
|
315
338
|
active = self._fetchone(
|
|
316
339
|
"SELECT session_id FROM active_sessions WHERE chat_id = ?",
|
|
317
|
-
(
|
|
340
|
+
(group_id,),
|
|
318
341
|
)
|
|
319
342
|
session_id = active["session_id"] if active else None
|
|
320
343
|
|
|
@@ -326,14 +349,19 @@ class SQLiteStorage:
|
|
|
326
349
|
return self._row_to_session(row)
|
|
327
350
|
|
|
328
351
|
candidates = self._fetchall(
|
|
329
|
-
"SELECT
|
|
330
|
-
(
|
|
352
|
+
"SELECT s.* FROM sessions s JOIN session_groups sg ON s.chat_id = sg.chat_id WHERE sg.group_id = ? ORDER BY s.updated_at DESC LIMIT 1",
|
|
353
|
+
(group_id,),
|
|
331
354
|
)
|
|
355
|
+
if not candidates:
|
|
356
|
+
candidates = self._fetchall(
|
|
357
|
+
"SELECT * FROM sessions WHERE chat_id = ? ORDER BY updated_at DESC LIMIT 1",
|
|
358
|
+
(chat_id,),
|
|
359
|
+
)
|
|
332
360
|
if candidates:
|
|
333
361
|
row = candidates[0]
|
|
334
362
|
self._exec(
|
|
335
363
|
"INSERT OR REPLACE INTO active_sessions (chat_id, session_id) VALUES (?, ?)",
|
|
336
|
-
(
|
|
364
|
+
(group_id, dict(row)["id"]),
|
|
337
365
|
)
|
|
338
366
|
self._conn().commit()
|
|
339
367
|
return self._row_to_session(row)
|
|
@@ -346,9 +374,10 @@ class SQLiteStorage:
|
|
|
346
374
|
def set_active_session(self, chat_id: int, session_id: str) -> bool:
|
|
347
375
|
"""Explicitly switch the active session pointer for a chat."""
|
|
348
376
|
try:
|
|
377
|
+
group_id = self._resolve_group(chat_id)
|
|
349
378
|
self._exec(
|
|
350
379
|
"INSERT OR REPLACE INTO active_sessions (chat_id, session_id) VALUES (?, ?)",
|
|
351
|
-
(
|
|
380
|
+
(group_id, session_id),
|
|
352
381
|
)
|
|
353
382
|
self._conn().commit()
|
|
354
383
|
return True
|
|
@@ -358,9 +387,10 @@ class SQLiteStorage:
|
|
|
358
387
|
|
|
359
388
|
def delete_session(self, chat_id: int) -> bool:
|
|
360
389
|
try:
|
|
390
|
+
group_id = self._resolve_group(chat_id)
|
|
361
391
|
active = self._fetchone(
|
|
362
392
|
"SELECT session_id FROM active_sessions WHERE chat_id = ?",
|
|
363
|
-
(
|
|
393
|
+
(group_id,),
|
|
364
394
|
)
|
|
365
395
|
if active:
|
|
366
396
|
self._exec("DELETE FROM messages WHERE session_id = ?", (active["session_id"],))
|
|
@@ -375,9 +405,10 @@ class SQLiteStorage:
|
|
|
375
405
|
def add_message(self, chat_id: int, sender: str, content: str, interface: str = 'telegram') -> bool:
|
|
376
406
|
"""Insert a single message directly into DB without rewriting the whole session."""
|
|
377
407
|
try:
|
|
408
|
+
group_id = self._resolve_group(chat_id)
|
|
378
409
|
active = self._fetchone(
|
|
379
410
|
"SELECT session_id FROM active_sessions WHERE chat_id = ?",
|
|
380
|
-
(
|
|
411
|
+
(group_id,),
|
|
381
412
|
)
|
|
382
413
|
if not active:
|
|
383
414
|
return False
|
|
@@ -415,14 +446,26 @@ class SQLiteStorage:
|
|
|
415
446
|
return [dict(r) for r in reversed(rows)]
|
|
416
447
|
|
|
417
448
|
def list_chat_sessions(self, chat_id: int) -> List[dict]:
|
|
449
|
+
group_id = self._resolve_group(chat_id)
|
|
418
450
|
rows = self._fetchall(
|
|
419
451
|
"""SELECT id, title, summary,
|
|
420
452
|
(SELECT COUNT(*) FROM messages WHERE session_id = sessions.id) as msg_count,
|
|
421
453
|
updated_at,
|
|
422
454
|
(SELECT session_id FROM active_sessions WHERE chat_id = ?) as active_id
|
|
423
|
-
FROM sessions
|
|
424
|
-
|
|
455
|
+
FROM sessions
|
|
456
|
+
WHERE chat_id IN (SELECT chat_id FROM session_groups WHERE group_id = ?)
|
|
457
|
+
ORDER BY updated_at DESC""",
|
|
458
|
+
(group_id, group_id),
|
|
425
459
|
)
|
|
460
|
+
if not rows:
|
|
461
|
+
rows = self._fetchall(
|
|
462
|
+
"""SELECT id, title, summary,
|
|
463
|
+
(SELECT COUNT(*) FROM messages WHERE session_id = sessions.id) as msg_count,
|
|
464
|
+
updated_at,
|
|
465
|
+
(SELECT session_id FROM active_sessions WHERE chat_id = ?) as active_id
|
|
466
|
+
FROM sessions WHERE chat_id = ? ORDER BY updated_at DESC""",
|
|
467
|
+
(group_id, chat_id),
|
|
468
|
+
)
|
|
426
469
|
return [
|
|
427
470
|
{
|
|
428
471
|
"session_id": r["id"],
|
|
@@ -441,9 +484,10 @@ class SQLiteStorage:
|
|
|
441
484
|
)
|
|
442
485
|
if not exists:
|
|
443
486
|
return False
|
|
487
|
+
group_id = self._resolve_group(chat_id)
|
|
444
488
|
self._exec(
|
|
445
489
|
"INSERT OR REPLACE INTO active_sessions (chat_id, session_id) VALUES (?, ?)",
|
|
446
|
-
(
|
|
490
|
+
(group_id, session_id),
|
|
447
491
|
)
|
|
448
492
|
self._conn().commit()
|
|
449
493
|
return True
|
|
@@ -546,6 +590,13 @@ class SQLiteStorage:
|
|
|
546
590
|
session_id=row_dict["id"],
|
|
547
591
|
)
|
|
548
592
|
|
|
593
|
+
def _resolve_group(self, chat_id: int) -> int:
|
|
594
|
+
"""Return the group_id for a chat_id, or chat_id itself if not in a group."""
|
|
595
|
+
row = self._fetchone(
|
|
596
|
+
"SELECT group_id FROM session_groups WHERE chat_id = ?", (chat_id,)
|
|
597
|
+
)
|
|
598
|
+
return row["group_id"] if row else chat_id
|
|
599
|
+
|
|
549
600
|
def get_stats(self) -> dict:
|
|
550
601
|
"""Returns global statistics for the admin."""
|
|
551
602
|
user_count = self._fetchone("SELECT COUNT(DISTINCT chat_id) FROM sessions")[0]
|