@mrtrinhvn/ag-kit 1.5.2 → 1.6.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/package.json +1 -1
- package/template/.agent/scripts/receptionist_up.sh +3 -2
- package/template/.agent/skills/ag-kit-core/SKILL.md +8 -1
- package/template/.agent/skills/bash-linux/SKILL.md +5 -0
- package/template/.agent/skills/behavioral-modes/SKILL.md +5 -0
- package/template/.agent/skills/clean-code/SKILL.md +3 -1
- package/template/.env.example +2 -18
- package/template/AGENTS.md +36 -0
- package/template/GEMINI.md +30 -3
- package/template/package.json +25 -0
- package/template/scripts/ag_hud.js +39 -41
- package/template/src/bot/TelegramBot.ts +83 -0
- package/template/src/index.ts +46 -0
- package/template/src/services/CdpService.ts +84 -0
- package/template/src/services/ModelService.ts +119 -0
- package/template/src/services/OllamaService.ts +91 -0
- package/template/src/services/SessionService.ts +188 -0
- package/template/src/services/TopicManager.ts +53 -0
- package/template/.agent/knowledge/artifacts/claude-code-system-prompt.md +0 -1490
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { CdpService } from './CdpService.js';
|
|
2
|
+
import { OllamaService } from './OllamaService.js';
|
|
3
|
+
|
|
4
|
+
export interface ModelInfo {
|
|
5
|
+
name: string;
|
|
6
|
+
refreshText: string;
|
|
7
|
+
isActive: boolean;
|
|
8
|
+
provider: 'ide' | 'ollama';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* ModelService - Bộ não điều phối danh sách Model danh cho ag-kit v1.6.0.
|
|
13
|
+
* Hỗ trợ Hybrid: Kết hợp Native IDE Cloud và Local Ollama.
|
|
14
|
+
*/
|
|
15
|
+
export class ModelService {
|
|
16
|
+
private ollama = new OllamaService();
|
|
17
|
+
private manualSelectedModel: string | null = null;
|
|
18
|
+
|
|
19
|
+
constructor(private cdp: CdpService) {}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Liệt kê các model khả dụng.
|
|
23
|
+
* "Tinh hoa": Tôn trọng sự lựa chọn thủ công của người dùng trên HUD.
|
|
24
|
+
*/
|
|
25
|
+
async listModels(): Promise<ModelInfo[]> {
|
|
26
|
+
const ideModels = await this.listIdeModels();
|
|
27
|
+
const ollamaModels = await this.ollama.listModels();
|
|
28
|
+
|
|
29
|
+
// 1. Gộp danh sách model
|
|
30
|
+
const merged: ModelInfo[] = [
|
|
31
|
+
...ideModels.map(m => ({ ...m, provider: 'ide' as const })),
|
|
32
|
+
...ollamaModels.map(m => ({
|
|
33
|
+
name: `[Ollama] ${m.name}`,
|
|
34
|
+
refreshText: m.size ? `${(m.size / (1024 * 1024 * 1024)).toFixed(1)}GB` : 'Local',
|
|
35
|
+
isActive: false,
|
|
36
|
+
provider: 'ollama' as const
|
|
37
|
+
}))
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
// 2. Logic "Manual Lock": Nếu chưa chọn, ưu tiên lấy model đang Active của IDE
|
|
41
|
+
if (!this.manualSelectedModel) {
|
|
42
|
+
const activeIde = merged.find(m => m.provider === 'ide' && m.isActive);
|
|
43
|
+
if (activeIde) {
|
|
44
|
+
this.manualSelectedModel = activeIde.name;
|
|
45
|
+
} else if (merged.length > 0) {
|
|
46
|
+
this.manualSelectedModel = merged[0].name;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 3. Buộc đồng bộ hóa trạng thái Active với sự lựa chọn của Sếp
|
|
51
|
+
if (this.manualSelectedModel) {
|
|
52
|
+
let foundMatch = false;
|
|
53
|
+
merged.forEach(m => {
|
|
54
|
+
if (m.name === this.manualSelectedModel) {
|
|
55
|
+
m.isActive = true;
|
|
56
|
+
foundMatch = true;
|
|
57
|
+
} else {
|
|
58
|
+
m.isActive = false;
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Nếu model cũ không còn (bị xóa khỏi Ollama), quay về bản IDE mặc định
|
|
63
|
+
if (!foundMatch && merged.length > 0) {
|
|
64
|
+
this.manualSelectedModel = merged.find(m => m.provider === 'ide')?.name || merged[0].name;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return merged;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Scraper Shadow DOM cho các IDE dựa trên VS Code (Antigravity, Cursor).
|
|
73
|
+
*/
|
|
74
|
+
private async listIdeModels(): Promise<Omit<ModelInfo, 'provider'>[]> {
|
|
75
|
+
const script = `(() => {
|
|
76
|
+
const findInShadow = (root, patterns) => {
|
|
77
|
+
let found = [];
|
|
78
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
|
79
|
+
let node = walker.nextNode();
|
|
80
|
+
while (node) {
|
|
81
|
+
const el = node;
|
|
82
|
+
const text = el.innerText || "";
|
|
83
|
+
if (patterns.some(p => text.includes(p)) && el.offsetParent !== null) {
|
|
84
|
+
// Nhận diện Menu Item trong IDE
|
|
85
|
+
if (el.classList && (el.classList.contains('monaco-list-row') || el.classList.contains('menu-item'))) {
|
|
86
|
+
found.push({
|
|
87
|
+
name: text.split('\\n')[0].trim(),
|
|
88
|
+
refreshText: 'Ready',
|
|
89
|
+
isActive: el.classList.contains('focused') || el.classList.contains('active') || el.getAttribute('aria-selected') === 'true'
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (el.shadowRoot) found = found.concat(findInShadow(el.shadowRoot, patterns));
|
|
94
|
+
node = walker.nextNode();
|
|
95
|
+
}
|
|
96
|
+
return found;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const patterns = ['Gemini', 'Claude', 'GPT', 'Llama', 'DeepSeek', 'o1'];
|
|
100
|
+
return findInShadow(document.body, patterns);
|
|
101
|
+
})()`;
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const raw = await this.cdp.evaluate(script) as any[];
|
|
105
|
+
// Deduplicate by name
|
|
106
|
+
return Array.from(new Map(raw.map((item: any) => [item['name'], item])).values());
|
|
107
|
+
} catch {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
public async setModel(name: string) {
|
|
113
|
+
this.manualSelectedModel = name;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
public getSelectedModel() {
|
|
117
|
+
return this.manualSelectedModel;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
|
|
3
|
+
export interface OllamaModel {
|
|
4
|
+
name: string;
|
|
5
|
+
model: string;
|
|
6
|
+
modified_at: string;
|
|
7
|
+
size: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class OllamaService {
|
|
11
|
+
private baseUrl = 'http://localhost:11434';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* List available Ollama models
|
|
15
|
+
*/
|
|
16
|
+
async listModels(): Promise<OllamaModel[]> {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
const req = http.get(`${this.baseUrl}/api/tags`, (res) => {
|
|
19
|
+
let data = '';
|
|
20
|
+
res.on('data', chunk => data += chunk);
|
|
21
|
+
res.on('end', () => {
|
|
22
|
+
try {
|
|
23
|
+
const parsed = JSON.parse(data);
|
|
24
|
+
const validModels = (parsed.models || []).filter((m: any) => !m.name.includes('nomic-embed-text'));
|
|
25
|
+
resolve(validModels);
|
|
26
|
+
} catch (e) {
|
|
27
|
+
reject(new Error('Failed to parse Ollama response'));
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
req.on('error', (err) => {
|
|
32
|
+
// Silently fail if Ollama is not running, return empty list
|
|
33
|
+
resolve([]);
|
|
34
|
+
});
|
|
35
|
+
req.setTimeout(2000, () => {
|
|
36
|
+
req.destroy();
|
|
37
|
+
resolve([]);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Generate response from an Ollama model (Streaming)
|
|
44
|
+
*/
|
|
45
|
+
async generateResponse(model: string, prompt: string, onProgress: (chunk: string) => void): Promise<string> {
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
const postData = JSON.stringify({
|
|
48
|
+
model: model,
|
|
49
|
+
prompt: prompt,
|
|
50
|
+
stream: true
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const options = {
|
|
54
|
+
hostname: 'localhost',
|
|
55
|
+
port: 11434,
|
|
56
|
+
path: '/api/generate',
|
|
57
|
+
method: 'POST',
|
|
58
|
+
headers: {
|
|
59
|
+
'Content-Type': 'application/json',
|
|
60
|
+
'Content-Length': Buffer.byteLength(postData)
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
let fullResponse = '';
|
|
65
|
+
const req = http.request(options, (res) => {
|
|
66
|
+
res.on('data', (chunk) => {
|
|
67
|
+
try {
|
|
68
|
+
const lines = chunk.toString().split('\n');
|
|
69
|
+
for (const line of lines) {
|
|
70
|
+
if (!line.trim()) continue;
|
|
71
|
+
const json = JSON.parse(line);
|
|
72
|
+
if (json.response) {
|
|
73
|
+
fullResponse += json.response;
|
|
74
|
+
onProgress(json.response);
|
|
75
|
+
}
|
|
76
|
+
if (json.done) {
|
|
77
|
+
resolve(fullResponse);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
} catch (e) {
|
|
81
|
+
// Ignore partial parse errors during streaming
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
req.on('error', reject);
|
|
87
|
+
req.write(postData);
|
|
88
|
+
req.end();
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { CdpService } from './CdpService.js';
|
|
2
|
+
|
|
3
|
+
export interface SessionListItem {
|
|
4
|
+
title: string;
|
|
5
|
+
isActive: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const SELECTORS = {
|
|
9
|
+
NEW_CHAT_BTN: '[data-tooltip-id="new-conversation-tooltip"]',
|
|
10
|
+
PAST_CONVERSATIONS_BTN: '[data-tooltip-id*="history"], [data-tooltip-id*="past-conversations"], [class*="lucide-history"]',
|
|
11
|
+
SESSION_ROWS: 'div[class*="cursor-pointer"]',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export class SessionService {
|
|
15
|
+
private cdp: CdpService;
|
|
16
|
+
|
|
17
|
+
constructor(cdp: CdpService) {
|
|
18
|
+
this.cdp = cdp;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Click the "New Chat" (+) button in the IDE.
|
|
23
|
+
*/
|
|
24
|
+
async startNewChat(): Promise<boolean> {
|
|
25
|
+
const script = `(() => {
|
|
26
|
+
const selectors = [
|
|
27
|
+
'${SELECTORS.NEW_CHAT_BTN}',
|
|
28
|
+
'[data-tooltip-id*="new-conversation"]',
|
|
29
|
+
'[data-tooltip-id*="new-chat"]',
|
|
30
|
+
'[aria-label*="New Conversation"]',
|
|
31
|
+
'[aria-label*="New Chat"]'
|
|
32
|
+
];
|
|
33
|
+
let btn = null;
|
|
34
|
+
for (const s of selectors) {
|
|
35
|
+
btn = document.querySelector(s);
|
|
36
|
+
if (btn) break;
|
|
37
|
+
}
|
|
38
|
+
if (!btn) return null;
|
|
39
|
+
const rect = btn.getBoundingClientRect();
|
|
40
|
+
return { x: Math.round(rect.x + rect.width / 2), y: Math.round(rect.y + rect.height / 2) };
|
|
41
|
+
})()`;
|
|
42
|
+
|
|
43
|
+
const coords = await this.evaluateOnAnyContext(script);
|
|
44
|
+
if (!coords) return false;
|
|
45
|
+
|
|
46
|
+
await this.cdpClick(coords.x, coords.y);
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Scrape session list from the "Past Conversations" panel.
|
|
52
|
+
*/
|
|
53
|
+
async listSessions(): Promise<SessionListItem[]> {
|
|
54
|
+
// 1. Open Past Conversations panel
|
|
55
|
+
const btnScript = `(() => {
|
|
56
|
+
const btn = document.querySelector('${SELECTORS.PAST_CONVERSATIONS_BTN}');
|
|
57
|
+
if (!btn) return null;
|
|
58
|
+
const rect = btn.getBoundingClientRect();
|
|
59
|
+
return { x: Math.round(rect.x + rect.width / 2), y: Math.round(rect.y + rect.height / 2) };
|
|
60
|
+
})()`;
|
|
61
|
+
|
|
62
|
+
const btnCoords = await this.evaluateOnAnyContext(btnScript);
|
|
63
|
+
if (!btnCoords) return [];
|
|
64
|
+
|
|
65
|
+
await this.cdpClick(btnCoords.x, btnCoords.y);
|
|
66
|
+
await this.sleep(800); // Wait for panel
|
|
67
|
+
|
|
68
|
+
// 2. Scrape sessions
|
|
69
|
+
const scrapeScript = `(() => {
|
|
70
|
+
const rows = Array.from(document.querySelectorAll('div[class*="cursor-pointer"]'));
|
|
71
|
+
const items = [];
|
|
72
|
+
for (const row of rows) {
|
|
73
|
+
const text = (row.textContent || '').trim();
|
|
74
|
+
if (text.length < 2 || text.length > 100) continue;
|
|
75
|
+
if (/history|new|past/i.test(text)) continue;
|
|
76
|
+
const isActive = /focusBackground/i.test(row.className || '');
|
|
77
|
+
items.push({ title: text, isActive });
|
|
78
|
+
}
|
|
79
|
+
return items;
|
|
80
|
+
})()`;
|
|
81
|
+
|
|
82
|
+
const sessions = await this.evaluateOnAnyContext(scrapeScript);
|
|
83
|
+
|
|
84
|
+
// 3. Close panel (Escape)
|
|
85
|
+
await this.cdp.call('Input.dispatchKeyEvent', { type: 'keyDown', key: 'Escape', code: 'Escape', windowsVirtualKeyCode: 27 });
|
|
86
|
+
await this.cdp.call('Input.dispatchKeyEvent', { type: 'keyUp', key: 'Escape', code: 'Escape', windowsVirtualKeyCode: 27 });
|
|
87
|
+
|
|
88
|
+
return sessions || [];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Switch to a session by clicking its title in the panel.
|
|
93
|
+
*/
|
|
94
|
+
async activateSession(title: string): Promise<boolean> {
|
|
95
|
+
const script = `(() => {
|
|
96
|
+
const rows = Array.from(document.querySelectorAll('div[class*="cursor-pointer"]'));
|
|
97
|
+
const target = rows.find(r => (r.textContent || '').trim() === ${JSON.stringify(title)});
|
|
98
|
+
if (!target) return null;
|
|
99
|
+
const rect = target.getBoundingClientRect();
|
|
100
|
+
return { x: Math.round(rect.x + rect.width / 2), y: Math.round(rect.y + rect.height / 2) };
|
|
101
|
+
})()`;
|
|
102
|
+
|
|
103
|
+
// 1. Open panel first
|
|
104
|
+
await this.listSessions();
|
|
105
|
+
await this.sleep(500);
|
|
106
|
+
|
|
107
|
+
const coords = await this.evaluateOnAnyContext(script);
|
|
108
|
+
if (!coords) return false;
|
|
109
|
+
|
|
110
|
+
await this.cdpClick(coords.x, coords.y);
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Find a session that starts with [TG-Topic-ID] or [TG-Chat-ID]
|
|
116
|
+
*/
|
|
117
|
+
async findSessionByTopic(id: number, isPrivate: boolean): Promise<string | null> {
|
|
118
|
+
const prefix = isPrivate ? `[TG-Chat-${id}]` : `[TG-Topic-${id}]`;
|
|
119
|
+
const sessions = await this.listSessions();
|
|
120
|
+
const found = sessions.find(s => s.title.startsWith(prefix));
|
|
121
|
+
return found ? found.title : null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Create a new chat and immediately rename it (or just return the initial title)
|
|
126
|
+
* Note: Renaming in IDE is tricky via CDP, so we just use the default new chat
|
|
127
|
+
* and the first message will set the title.
|
|
128
|
+
*/
|
|
129
|
+
async startNewChatWithTopic(id: number, isPrivate: boolean): Promise<boolean> {
|
|
130
|
+
const ok = await this.startNewChat();
|
|
131
|
+
// We can't easily rename via CDP without more complex DOM interaction.
|
|
132
|
+
// However, the IDE usually titles by the first prompt.
|
|
133
|
+
// For now, we'll just return 'true' and the bot will inject a hidden prefix if possible,
|
|
134
|
+
// or we just rely on the fact that we can't perfectly name it but we can *find* it
|
|
135
|
+
// if we manage to inject the prefix in the first message.
|
|
136
|
+
return ok;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Scrape the last 40 lines of the currently active terminal in the IDE.
|
|
141
|
+
*/
|
|
142
|
+
async getTerminalOutput(): Promise<string> {
|
|
143
|
+
const script = `(() => {
|
|
144
|
+
// 1. Try to find accessibility tree (contains text nodes even if canvas renderer is used)
|
|
145
|
+
const ariaRows = Array.from(document.querySelectorAll('.xterm-accessibility-tree .xterm-rows > div, .xterm-accessibility .live-region'));
|
|
146
|
+
|
|
147
|
+
// 2. Fallback to normal rows if DOM renderer is used
|
|
148
|
+
const normalRows = Array.from(document.querySelectorAll('.xterm-rows > div'));
|
|
149
|
+
|
|
150
|
+
// 3. Fallback to aria-live region which contains the terminal stream
|
|
151
|
+
const liveRegion = document.querySelector('.xterm-accessibility-tree [aria-live], .live-region[aria-live="polite"]');
|
|
152
|
+
|
|
153
|
+
let lines = [];
|
|
154
|
+
if (ariaRows.length > 0) {
|
|
155
|
+
lines = ariaRows.map(r => r.textContent || '');
|
|
156
|
+
} else if (normalRows.length > 0) {
|
|
157
|
+
lines = normalRows.map(r => r.textContent || '');
|
|
158
|
+
} else if (liveRegion && liveRegion.textContent) {
|
|
159
|
+
lines = liveRegion.textContent.split('\\n');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
lines = lines.filter(t => t.trim().length > 0);
|
|
163
|
+
if (lines.length === 0) return null;
|
|
164
|
+
return lines.slice(-40).join('\\n');
|
|
165
|
+
})()`;
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
const text = await this.evaluateOnAnyContext(script);
|
|
169
|
+
return text || '📭 [DOM Trống: IDE đang ẩn hoặc chưa bật Tab Terminal. Sếp hãy mở/focus Panel Terminal trên màn hình IDE trước nhé do cơ chế tiết kiệm RAM của VS Code!]';
|
|
170
|
+
} catch(e) {
|
|
171
|
+
return '❌ Lỗi đọc Terminal từ IDE';
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private async evaluateOnAnyContext(expression: string): Promise<any> {
|
|
176
|
+
return this.cdp.evaluateOnAnyContext(expression);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private async cdpClick(x: number, y: number): Promise<void> {
|
|
180
|
+
await this.cdp.call('Input.dispatchMouseEvent', { type: 'mouseMoved', x, y });
|
|
181
|
+
await this.cdp.call('Input.dispatchMouseEvent', { type: 'mousePressed', x, y, button: 'left', clickCount: 1 });
|
|
182
|
+
await this.cdp.call('Input.dispatchMouseEvent', { type: 'mouseReleased', x, y, button: 'left', clickCount: 1 });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private sleep(ms: number): Promise<void> {
|
|
186
|
+
return new Promise(r => setTimeout(r, ms));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
|
|
4
|
+
export interface SessionState {
|
|
5
|
+
topicId: string;
|
|
6
|
+
metadata: Record<string, any>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* TopicManager - Quản lý phiên hội thoại (Sessions) dựa trên chủ đề.
|
|
11
|
+
* Giúp đồng bộ hóa ngữ cảnh giữa các Agent.
|
|
12
|
+
*/
|
|
13
|
+
export class TopicManager {
|
|
14
|
+
private stateFile: string;
|
|
15
|
+
private chatToSession: Map<number, SessionState> = new Map();
|
|
16
|
+
|
|
17
|
+
constructor(storagePath: string = '.topic_state.json') {
|
|
18
|
+
this.stateFile = path.resolve(storagePath);
|
|
19
|
+
this.loadState();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
private loadState() {
|
|
23
|
+
try {
|
|
24
|
+
if (fs.existsSync(this.stateFile)) {
|
|
25
|
+
const raw = fs.readFileSync(this.stateFile, 'utf8');
|
|
26
|
+
const data = JSON.parse(raw);
|
|
27
|
+
for (const [id, state] of Object.entries(data)) {
|
|
28
|
+
this.chatToSession.set(Number(id), state as SessionState);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
} catch (e) {
|
|
32
|
+
console.error('[TopicManager] Error loading state:', e);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private saveState() {
|
|
37
|
+
try {
|
|
38
|
+
const data = Object.fromEntries(this.chatToSession);
|
|
39
|
+
fs.writeFileSync(this.stateFile, JSON.stringify(data, null, 2));
|
|
40
|
+
} catch (e) {
|
|
41
|
+
console.error('[TopicManager] Error saving state:', e);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public setSession(chatId: number, state: SessionState) {
|
|
46
|
+
this.chatToSession.set(chatId, state);
|
|
47
|
+
this.saveState();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public getSession(chatId: number): SessionState | undefined {
|
|
51
|
+
return this.chatToSession.get(chatId);
|
|
52
|
+
}
|
|
53
|
+
}
|