lazy-gravity 0.6.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bot/index.js +354 -28
- package/dist/bot/telegramCommands.js +175 -48
- package/dist/bot/telegramJoinCommand.js +170 -0
- package/dist/bot/telegramMessageHandler.js +24 -7
- package/dist/bot/telegramProjectCommand.js +71 -18
- package/dist/bot/telegramStartupTarget.js +54 -0
- package/dist/commands/chatCommandHandler.js +8 -12
- package/dist/commands/joinCommandHandler.js +16 -10
- package/dist/commands/registerSlashCommands.js +13 -1
- package/dist/commands/workspaceCommandHandler.js +22 -7
- package/dist/database/accountPreferenceRepository.js +29 -0
- package/dist/database/channelPreferenceRepository.js +29 -0
- package/dist/database/chatSessionRepository.js +66 -3
- package/dist/database/telegramBindingRepository.js +13 -0
- package/dist/events/interactionCreateHandler.js +194 -13
- package/dist/events/messageCreateHandler.js +103 -7
- package/dist/handlers/accountSelectAction.js +45 -0
- package/dist/handlers/modelButtonAction.js +13 -0
- package/dist/services/cdpBridgeManager.js +23 -18
- package/dist/services/cdpConnectionPool.js +133 -206
- package/dist/services/chatSessionService.js +199 -16
- package/dist/services/userMessageDetector.js +4 -4
- package/dist/ui/accountUi.js +60 -0
- package/dist/utils/accountUtils.js +36 -0
- package/dist/utils/cdpPorts.js +97 -2
- package/dist/utils/configLoader.js +14 -0
- package/package.json +1 -1
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.selectTelegramStartupChatId = selectTelegramStartupChatId;
|
|
4
|
+
function getBaseChatId(chatId) {
|
|
5
|
+
const sepIdx = chatId.indexOf('_');
|
|
6
|
+
return sepIdx > 0 ? chatId.slice(0, sepIdx) : chatId;
|
|
7
|
+
}
|
|
8
|
+
function normalizeTitle(title) {
|
|
9
|
+
return title.trim().replace(/^#/, '').toLowerCase();
|
|
10
|
+
}
|
|
11
|
+
function isGeneralChat(title) {
|
|
12
|
+
return normalizeTitle(title) === 'general';
|
|
13
|
+
}
|
|
14
|
+
async function selectTelegramStartupChatId(api, bindings) {
|
|
15
|
+
const candidates = [];
|
|
16
|
+
const seenResolvedIds = new Set();
|
|
17
|
+
for (const binding of bindings) {
|
|
18
|
+
const resolvedChatId = getBaseChatId(binding.chatId);
|
|
19
|
+
if (seenResolvedIds.has(resolvedChatId))
|
|
20
|
+
continue;
|
|
21
|
+
seenResolvedIds.add(resolvedChatId);
|
|
22
|
+
try {
|
|
23
|
+
const chat = await api.getChat(resolvedChatId);
|
|
24
|
+
candidates.push({
|
|
25
|
+
bindingChatId: binding.chatId,
|
|
26
|
+
resolvedChatId,
|
|
27
|
+
type: String(chat?.type ?? ''),
|
|
28
|
+
title: String(chat?.title ?? chat?.first_name ?? ''),
|
|
29
|
+
isDirectBinding: binding.chatId === resolvedChatId,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
candidates.push({
|
|
34
|
+
bindingChatId: binding.chatId,
|
|
35
|
+
resolvedChatId,
|
|
36
|
+
type: '',
|
|
37
|
+
title: '',
|
|
38
|
+
isDirectBinding: binding.chatId === resolvedChatId,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (candidates.length === 0)
|
|
43
|
+
return null;
|
|
44
|
+
const generalGroup = candidates.find((candidate) => candidate.type !== 'private' && isGeneralChat(candidate.title));
|
|
45
|
+
if (generalGroup)
|
|
46
|
+
return generalGroup.resolvedChatId;
|
|
47
|
+
const directGroup = candidates.find((candidate) => candidate.type !== 'private' && candidate.isDirectBinding);
|
|
48
|
+
if (directGroup)
|
|
49
|
+
return directGroup.resolvedChatId;
|
|
50
|
+
const privateChat = candidates.find((candidate) => candidate.type === 'private');
|
|
51
|
+
if (privateChat)
|
|
52
|
+
return privateChat.bindingChatId;
|
|
53
|
+
return candidates[0].resolvedChatId;
|
|
54
|
+
}
|
|
@@ -17,13 +17,15 @@ class ChatCommandHandler {
|
|
|
17
17
|
channelManager;
|
|
18
18
|
pool;
|
|
19
19
|
workspaceService;
|
|
20
|
-
|
|
20
|
+
resolveAccountForChannel;
|
|
21
|
+
constructor(chatSessionService, chatSessionRepo, bindingRepo, channelManager, workspaceService, pool, resolveAccountForChannel) {
|
|
21
22
|
this.chatSessionService = chatSessionService;
|
|
22
23
|
this.chatSessionRepo = chatSessionRepo;
|
|
23
24
|
this.bindingRepo = bindingRepo;
|
|
24
25
|
this.channelManager = channelManager;
|
|
25
26
|
this.workspaceService = workspaceService;
|
|
26
27
|
this.pool = pool ?? null;
|
|
28
|
+
this.resolveAccountForChannel = resolveAccountForChannel ?? null;
|
|
27
29
|
}
|
|
28
30
|
/**
|
|
29
31
|
* /new -- Create a new session channel under the category and start a new chat in Antigravity
|
|
@@ -39,19 +41,11 @@ class ChatCommandHandler {
|
|
|
39
41
|
await interaction.editReply({ content: (0, i18n_1.t)('⚠️ Please execute in a text channel.') });
|
|
40
42
|
return;
|
|
41
43
|
}
|
|
42
|
-
// Check if the current channel is under a project category
|
|
43
|
-
const parentId = 'parentId' in channel ? channel.parentId : null;
|
|
44
|
-
if (!parentId) {
|
|
45
|
-
await interaction.editReply({
|
|
46
|
-
content: (0, i18n_1.t)('⚠️ Please run in a project category channel.\nUse `/project` to create a project first.'),
|
|
47
|
-
});
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
// Determine the project path
|
|
51
44
|
const currentSession = this.chatSessionRepo.findByChannelId(interaction.channelId);
|
|
52
45
|
const binding = this.bindingRepo.findByChannelId(interaction.channelId);
|
|
46
|
+
const parentId = currentSession?.categoryId ?? ('parentId' in channel ? channel.parentId : null);
|
|
53
47
|
const workspaceName = currentSession?.workspacePath ?? binding?.workspacePath;
|
|
54
|
-
if (!workspaceName) {
|
|
48
|
+
if (!parentId || !workspaceName) {
|
|
55
49
|
await interaction.editReply({
|
|
56
50
|
content: (0, i18n_1.t)('⚠️ Please run in a project category channel.\nUse `/project` to create a project first.'),
|
|
57
51
|
});
|
|
@@ -61,9 +55,10 @@ class ChatCommandHandler {
|
|
|
61
55
|
const workspacePath = this.workspaceService.getWorkspacePath(workspaceName);
|
|
62
56
|
// Switch project (connect to the correct workbench page)
|
|
63
57
|
let workspaceCdp;
|
|
58
|
+
const selectedAccount = this.resolveAccountForChannel?.(interaction.channelId, interaction.user.id) ?? 'default';
|
|
64
59
|
if (this.pool) {
|
|
65
60
|
try {
|
|
66
|
-
workspaceCdp = await this.pool.getOrConnect(workspacePath);
|
|
61
|
+
workspaceCdp = await this.pool.getOrConnect(workspacePath, { name: selectedAccount });
|
|
67
62
|
}
|
|
68
63
|
catch (e) {
|
|
69
64
|
await interaction.editReply({
|
|
@@ -94,6 +89,7 @@ class ChatCommandHandler {
|
|
|
94
89
|
categoryId: parentId,
|
|
95
90
|
workspacePath: workspaceName,
|
|
96
91
|
sessionNumber,
|
|
92
|
+
activeAccountName: selectedAccount,
|
|
97
93
|
guildId: guild.id,
|
|
98
94
|
});
|
|
99
95
|
const embed = new discord_js_1.EmbedBuilder()
|
|
@@ -25,9 +25,10 @@ class JoinCommandHandler {
|
|
|
25
25
|
client;
|
|
26
26
|
extractionMode;
|
|
27
27
|
responseTimeoutMs;
|
|
28
|
+
resolveAccountForChannel;
|
|
28
29
|
/** Active ResponseMonitors per workspace (for AI response mirroring) */
|
|
29
30
|
activeResponseMonitors = new Map();
|
|
30
|
-
constructor(chatSessionService, chatSessionRepo, bindingRepo, channelManager, pool, workspaceService, client, extractionMode, responseTimeoutMs) {
|
|
31
|
+
constructor(chatSessionService, chatSessionRepo, bindingRepo, channelManager, pool, workspaceService, client, extractionMode, responseTimeoutMs, resolveAccountForChannel) {
|
|
31
32
|
this.chatSessionService = chatSessionService;
|
|
32
33
|
this.chatSessionRepo = chatSessionRepo;
|
|
33
34
|
this.bindingRepo = bindingRepo;
|
|
@@ -37,6 +38,7 @@ class JoinCommandHandler {
|
|
|
37
38
|
this.client = client;
|
|
38
39
|
this.extractionMode = extractionMode;
|
|
39
40
|
this.responseTimeoutMs = responseTimeoutMs;
|
|
41
|
+
this.resolveAccountForChannel = resolveAccountForChannel ?? null;
|
|
40
42
|
}
|
|
41
43
|
/**
|
|
42
44
|
* Resolve a project name (from DB) to its full absolute path.
|
|
@@ -59,9 +61,10 @@ class JoinCommandHandler {
|
|
|
59
61
|
return;
|
|
60
62
|
}
|
|
61
63
|
const projectPath = this.resolveProjectPath(projectName);
|
|
64
|
+
const accountName = this.resolveAccountForChannel?.(interaction.channelId, interaction.user.id) ?? 'default';
|
|
62
65
|
let cdp;
|
|
63
66
|
try {
|
|
64
|
-
cdp = await this.pool.getOrConnect(projectPath);
|
|
67
|
+
cdp = await this.pool.getOrConnect(projectPath, { name: accountName });
|
|
65
68
|
}
|
|
66
69
|
catch (e) {
|
|
67
70
|
await interaction.editReply({
|
|
@@ -96,6 +99,7 @@ class JoinCommandHandler {
|
|
|
96
99
|
return;
|
|
97
100
|
}
|
|
98
101
|
const projectPath = this.resolveProjectPath(projectName);
|
|
102
|
+
const accountName = this.resolveAccountForChannel?.(interaction.channelId, interaction.user.id) ?? 'default';
|
|
99
103
|
// Step 1: Check if a channel already exists for this session
|
|
100
104
|
const existingSession = this.chatSessionRepo.findByDisplayName(projectName, selectedTitle);
|
|
101
105
|
if (existingSession) {
|
|
@@ -119,7 +123,7 @@ class JoinCommandHandler {
|
|
|
119
123
|
// Step 2: Connect to CDP
|
|
120
124
|
let cdp;
|
|
121
125
|
try {
|
|
122
|
-
cdp = await this.pool.getOrConnect(projectPath);
|
|
126
|
+
cdp = await this.pool.getOrConnect(projectPath, { name: accountName });
|
|
123
127
|
}
|
|
124
128
|
catch (e) {
|
|
125
129
|
await interaction.editReply({ content: (0, i18n_1.t)(`⚠️ Failed to connect to project: ${e.message}`) });
|
|
@@ -149,11 +153,12 @@ class JoinCommandHandler {
|
|
|
149
153
|
categoryId,
|
|
150
154
|
workspacePath: projectName,
|
|
151
155
|
sessionNumber,
|
|
156
|
+
activeAccountName: accountName,
|
|
152
157
|
guildId: guild.id,
|
|
153
158
|
});
|
|
154
159
|
this.chatSessionRepo.updateDisplayName(newChannelId, selectedTitle);
|
|
155
160
|
// Step 6: Start mirroring (routes dynamically to all bound session channels)
|
|
156
|
-
this.startMirroring(bridge, cdp, projectName);
|
|
161
|
+
this.startMirroring(bridge, cdp, projectName, accountName);
|
|
157
162
|
const embed = new discord_js_1.EmbedBuilder()
|
|
158
163
|
.setTitle((0, i18n_1.t)('🔗 Joined Session'))
|
|
159
164
|
.setDescription((0, i18n_1.t)(`Connected to: **${selectedTitle}**\n→ <#${newChannelId}>\n\n` +
|
|
@@ -177,7 +182,8 @@ class JoinCommandHandler {
|
|
|
177
182
|
return;
|
|
178
183
|
}
|
|
179
184
|
const projectPath = this.resolveProjectPath(projectName);
|
|
180
|
-
const
|
|
185
|
+
const accountName = this.resolveAccountForChannel?.(interaction.channelId, interaction.user.id) ?? 'default';
|
|
186
|
+
const detector = this.pool.getUserMessageDetector(projectName, accountName);
|
|
181
187
|
if (detector?.isActive()) {
|
|
182
188
|
// Turn OFF — stop user message detector and any active response monitor
|
|
183
189
|
detector.stop();
|
|
@@ -197,7 +203,7 @@ class JoinCommandHandler {
|
|
|
197
203
|
// Turn ON
|
|
198
204
|
let cdp;
|
|
199
205
|
try {
|
|
200
|
-
cdp = await this.pool.getOrConnect(projectPath);
|
|
206
|
+
cdp = await this.pool.getOrConnect(projectPath, { name: accountName });
|
|
201
207
|
}
|
|
202
208
|
catch (e) {
|
|
203
209
|
await interaction.editReply({
|
|
@@ -205,7 +211,7 @@ class JoinCommandHandler {
|
|
|
205
211
|
});
|
|
206
212
|
return;
|
|
207
213
|
}
|
|
208
|
-
this.startMirroring(bridge, cdp, projectName);
|
|
214
|
+
this.startMirroring(bridge, cdp, projectName, accountName);
|
|
209
215
|
const embed = new discord_js_1.EmbedBuilder()
|
|
210
216
|
.setTitle((0, i18n_1.t)('📡 Mirroring ON'))
|
|
211
217
|
.setDescription((0, i18n_1.t)('PC-to-Discord message mirroring is now active.\n' +
|
|
@@ -222,11 +228,11 @@ class JoinCommandHandler {
|
|
|
222
228
|
* channel via chatSessionRepo.findByDisplayName. Only explicitly joined
|
|
223
229
|
* sessions (with a displayName binding) receive mirrored messages.
|
|
224
230
|
*/
|
|
225
|
-
startMirroring(bridge, cdp, projectName) {
|
|
231
|
+
startMirroring(bridge, cdp, projectName, accountName) {
|
|
226
232
|
// Force re-prime: stop existing detector so that ensureUserMessageDetector
|
|
227
233
|
// creates a fresh one. This prevents the detector from treating the
|
|
228
234
|
// new session's last message as a "new" user message after /join.
|
|
229
|
-
const existing = this.pool.getUserMessageDetector(projectName);
|
|
235
|
+
const existing = this.pool.getUserMessageDetector(projectName, accountName);
|
|
230
236
|
if (existing?.isActive()) {
|
|
231
237
|
existing.stop();
|
|
232
238
|
}
|
|
@@ -235,7 +241,7 @@ class JoinCommandHandler {
|
|
|
235
241
|
.catch((err) => {
|
|
236
242
|
logger_1.logger.error('[Mirror] Error routing mirrored message:', err);
|
|
237
243
|
});
|
|
238
|
-
});
|
|
244
|
+
}, accountName);
|
|
239
245
|
}
|
|
240
246
|
/**
|
|
241
247
|
* Route a mirrored PC message to the correct Discord channel and
|
|
@@ -79,7 +79,14 @@ const projectCommand = new discord_js_1.SlashCommandBuilder()
|
|
|
79
79
|
.addStringOption((option) => option
|
|
80
80
|
.setName('name')
|
|
81
81
|
.setDescription((0, i18n_1.t)('Name of the project to create'))
|
|
82
|
-
.setRequired(true)))
|
|
82
|
+
.setRequired(true)))
|
|
83
|
+
.addSubcommand((sub) => sub
|
|
84
|
+
.setName('account')
|
|
85
|
+
.setDescription((0, i18n_1.t)('Display or change the Antigravity account bound to this project channel'))
|
|
86
|
+
.addStringOption((option) => option
|
|
87
|
+
.setName('name')
|
|
88
|
+
.setDescription((0, i18n_1.t)('Name of the account to bind to this project channel'))
|
|
89
|
+
.setRequired(false)));
|
|
83
90
|
/** /new command definition (formerly /chat new, made into a standalone command) */
|
|
84
91
|
const newCommand = new discord_js_1.SlashCommandBuilder()
|
|
85
92
|
.setName('new')
|
|
@@ -118,6 +125,10 @@ const outputCommand = new discord_js_1.SlashCommandBuilder()
|
|
|
118
125
|
.setName('format')
|
|
119
126
|
.setDescription((0, i18n_1.t)('embed / plain (optional direct switch)'))
|
|
120
127
|
.setRequired(false));
|
|
128
|
+
/** /account command definition */
|
|
129
|
+
const accountCommand = new discord_js_1.SlashCommandBuilder()
|
|
130
|
+
.setName('account')
|
|
131
|
+
.setDescription((0, i18n_1.t)('Select the Antigravity account for the current session'));
|
|
121
132
|
/** /logs command definition */
|
|
122
133
|
const logsCommand = new discord_js_1.SlashCommandBuilder()
|
|
123
134
|
.setName('logs')
|
|
@@ -153,6 +164,7 @@ exports.slashCommands = [
|
|
|
153
164
|
cleanupCommand,
|
|
154
165
|
joinCommand,
|
|
155
166
|
mirrorCommand,
|
|
167
|
+
accountCommand,
|
|
156
168
|
outputCommand,
|
|
157
169
|
pingCommand,
|
|
158
170
|
logsCommand,
|
|
@@ -22,6 +22,7 @@ class WorkspaceCommandHandler {
|
|
|
22
22
|
chatSessionRepo;
|
|
23
23
|
workspaceService;
|
|
24
24
|
channelManager;
|
|
25
|
+
onSessionChannelCreated;
|
|
25
26
|
processingWorkspaces = new Set();
|
|
26
27
|
/**
|
|
27
28
|
* Filters out stale bindings where the Discord channel no longer exists.
|
|
@@ -58,11 +59,12 @@ class WorkspaceCommandHandler {
|
|
|
58
59
|
}
|
|
59
60
|
return validBindings;
|
|
60
61
|
}
|
|
61
|
-
constructor(bindingRepo, chatSessionRepo, workspaceService, channelManager) {
|
|
62
|
+
constructor(bindingRepo, chatSessionRepo, workspaceService, channelManager, onSessionChannelCreated) {
|
|
62
63
|
this.bindingRepo = bindingRepo;
|
|
63
64
|
this.chatSessionRepo = chatSessionRepo;
|
|
64
65
|
this.workspaceService = workspaceService;
|
|
65
66
|
this.channelManager = channelManager;
|
|
67
|
+
this.onSessionChannelCreated = onSessionChannelCreated;
|
|
66
68
|
}
|
|
67
69
|
/**
|
|
68
70
|
* /project list -- Display project list via select menu
|
|
@@ -87,9 +89,16 @@ class WorkspaceCommandHandler {
|
|
|
87
89
|
* Creates a category + session-1 channel and binds them.
|
|
88
90
|
*/
|
|
89
91
|
async handleSelectMenu(interaction, guild) {
|
|
92
|
+
const respond = async (payload) => {
|
|
93
|
+
if (typeof interaction.editReply === 'function') {
|
|
94
|
+
await interaction.editReply(payload);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
await interaction.update(payload);
|
|
98
|
+
};
|
|
90
99
|
const workspacePath = interaction.values[0];
|
|
91
100
|
if (!this.workspaceService.exists(workspacePath)) {
|
|
92
|
-
await
|
|
101
|
+
await respond({
|
|
93
102
|
content: (0, i18n_1.t)(`❌ Project \`${workspacePath}\` not found.`),
|
|
94
103
|
embeds: [],
|
|
95
104
|
components: [],
|
|
@@ -109,7 +118,7 @@ class WorkspaceCommandHandler {
|
|
|
109
118
|
`→ ${channelLinks}`)
|
|
110
119
|
.addFields({ name: (0, i18n_1.t)('Full Path'), value: `\`${fullPath}\`` })
|
|
111
120
|
.setTimestamp();
|
|
112
|
-
await
|
|
121
|
+
await respond({
|
|
113
122
|
embeds: [embed],
|
|
114
123
|
components: [],
|
|
115
124
|
});
|
|
@@ -117,7 +126,7 @@ class WorkspaceCommandHandler {
|
|
|
117
126
|
}
|
|
118
127
|
// Lock project being processed (prevent rapid repeated clicks)
|
|
119
128
|
if (this.processingWorkspaces.has(workspacePath)) {
|
|
120
|
-
await
|
|
129
|
+
await respond({
|
|
121
130
|
content: (0, i18n_1.t)(`⏳ **${workspacePath}** is being created. Please wait.`),
|
|
122
131
|
embeds: [],
|
|
123
132
|
components: [],
|
|
@@ -148,6 +157,7 @@ class WorkspaceCommandHandler {
|
|
|
148
157
|
sessionNumber,
|
|
149
158
|
guildId: guild.id,
|
|
150
159
|
});
|
|
160
|
+
await this.onSessionChannelCreated?.(workspacePath, channelId, interaction.channelId, interaction.user.id);
|
|
151
161
|
const fullPath = this.workspaceService.getWorkspacePath(workspacePath);
|
|
152
162
|
const embed = new discord_js_1.EmbedBuilder()
|
|
153
163
|
.setTitle('📁 Projects')
|
|
@@ -156,7 +166,7 @@ class WorkspaceCommandHandler {
|
|
|
156
166
|
`→ <#${channelId}>`)
|
|
157
167
|
.addFields({ name: (0, i18n_1.t)('Full Path'), value: `\`${fullPath}\`` })
|
|
158
168
|
.setTimestamp();
|
|
159
|
-
await
|
|
169
|
+
await respond({
|
|
160
170
|
embeds: [embed],
|
|
161
171
|
components: [],
|
|
162
172
|
});
|
|
@@ -231,6 +241,7 @@ class WorkspaceCommandHandler {
|
|
|
231
241
|
sessionNumber,
|
|
232
242
|
guildId: guild.id,
|
|
233
243
|
});
|
|
244
|
+
await this.onSessionChannelCreated?.(name, channelId, interaction.channelId, interaction.user.id);
|
|
234
245
|
const embed = new discord_js_1.EmbedBuilder()
|
|
235
246
|
.setTitle('📁 Project Created')
|
|
236
247
|
.setColor(0x00AA00)
|
|
@@ -249,9 +260,13 @@ class WorkspaceCommandHandler {
|
|
|
249
260
|
*/
|
|
250
261
|
getWorkspaceForChannel(channelId) {
|
|
251
262
|
const binding = this.bindingRepo.findByChannelId(channelId);
|
|
252
|
-
if (
|
|
263
|
+
if (binding) {
|
|
264
|
+
return this.workspaceService.getWorkspacePath(binding.workspacePath);
|
|
265
|
+
}
|
|
266
|
+
const session = this.chatSessionRepo.findByChannelId(channelId);
|
|
267
|
+
if (!session)
|
|
253
268
|
return undefined;
|
|
254
|
-
return this.workspaceService.getWorkspacePath(
|
|
269
|
+
return this.workspaceService.getWorkspacePath(session.workspacePath);
|
|
255
270
|
}
|
|
256
271
|
}
|
|
257
272
|
exports.WorkspaceCommandHandler = WorkspaceCommandHandler;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AccountPreferenceRepository = void 0;
|
|
4
|
+
class AccountPreferenceRepository {
|
|
5
|
+
db;
|
|
6
|
+
constructor(db) {
|
|
7
|
+
this.db = db;
|
|
8
|
+
this.db.exec(`
|
|
9
|
+
CREATE TABLE IF NOT EXISTS account_preferences (
|
|
10
|
+
user_id TEXT PRIMARY KEY,
|
|
11
|
+
account_name TEXT NOT NULL,
|
|
12
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
13
|
+
)
|
|
14
|
+
`);
|
|
15
|
+
}
|
|
16
|
+
getAccountName(userId) {
|
|
17
|
+
const row = this.db.prepare('SELECT account_name FROM account_preferences WHERE user_id = ?').get(userId);
|
|
18
|
+
return row?.account_name ?? null;
|
|
19
|
+
}
|
|
20
|
+
setAccountName(userId, accountName) {
|
|
21
|
+
this.db.prepare(`
|
|
22
|
+
INSERT INTO account_preferences (user_id, account_name)
|
|
23
|
+
VALUES (?, ?)
|
|
24
|
+
ON CONFLICT(user_id)
|
|
25
|
+
DO UPDATE SET account_name = excluded.account_name, updated_at = datetime('now')
|
|
26
|
+
`).run(userId, accountName);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
exports.AccountPreferenceRepository = AccountPreferenceRepository;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ChannelPreferenceRepository = void 0;
|
|
4
|
+
class ChannelPreferenceRepository {
|
|
5
|
+
db;
|
|
6
|
+
constructor(db) {
|
|
7
|
+
this.db = db;
|
|
8
|
+
this.db.exec(`
|
|
9
|
+
CREATE TABLE IF NOT EXISTS channel_preferences (
|
|
10
|
+
channel_id TEXT PRIMARY KEY,
|
|
11
|
+
account_name TEXT,
|
|
12
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
13
|
+
)
|
|
14
|
+
`);
|
|
15
|
+
}
|
|
16
|
+
getAccountName(channelId) {
|
|
17
|
+
const row = this.db.prepare('SELECT account_name FROM channel_preferences WHERE channel_id = ?').get(channelId);
|
|
18
|
+
return row?.account_name ?? null;
|
|
19
|
+
}
|
|
20
|
+
setAccountName(channelId, accountName) {
|
|
21
|
+
this.db.prepare(`
|
|
22
|
+
INSERT INTO channel_preferences (channel_id, account_name)
|
|
23
|
+
VALUES (?, ?)
|
|
24
|
+
ON CONFLICT(channel_id)
|
|
25
|
+
DO UPDATE SET account_name = excluded.account_name, updated_at = datetime('now')
|
|
26
|
+
`).run(channelId, accountName);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
exports.ChannelPreferenceRepository = ChannelPreferenceRepository;
|
|
@@ -19,25 +19,65 @@ class ChatSessionRepository {
|
|
|
19
19
|
category_id TEXT NOT NULL,
|
|
20
20
|
workspace_path TEXT NOT NULL,
|
|
21
21
|
session_number INTEGER NOT NULL,
|
|
22
|
+
conversation_id TEXT,
|
|
23
|
+
active_account_name TEXT,
|
|
24
|
+
origin_account_name TEXT,
|
|
22
25
|
display_name TEXT,
|
|
23
26
|
is_renamed INTEGER NOT NULL DEFAULT 0,
|
|
24
27
|
guild_id TEXT NOT NULL,
|
|
25
28
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
26
29
|
)
|
|
27
30
|
`);
|
|
31
|
+
const columns = this.db.prepare('PRAGMA table_info(chat_sessions)').all();
|
|
32
|
+
const hasActiveAccountName = columns.some((column) => column.name === 'active_account_name');
|
|
33
|
+
const hasConversationId = columns.some((column) => column.name === 'conversation_id');
|
|
34
|
+
if (!hasConversationId) {
|
|
35
|
+
this.db.exec('ALTER TABLE chat_sessions ADD COLUMN conversation_id TEXT');
|
|
36
|
+
}
|
|
37
|
+
if (!hasActiveAccountName) {
|
|
38
|
+
this.db.exec('ALTER TABLE chat_sessions ADD COLUMN active_account_name TEXT');
|
|
39
|
+
}
|
|
40
|
+
const hasOriginAccountName = columns.some((column) => column.name === 'origin_account_name');
|
|
41
|
+
if (!hasOriginAccountName) {
|
|
42
|
+
this.db.exec('ALTER TABLE chat_sessions ADD COLUMN origin_account_name TEXT');
|
|
43
|
+
}
|
|
44
|
+
const hasLegacyAccountName = columns.some((column) => column.name === 'account_name');
|
|
45
|
+
if (hasLegacyAccountName) {
|
|
46
|
+
this.db.exec(`
|
|
47
|
+
UPDATE chat_sessions
|
|
48
|
+
SET origin_account_name = account_name
|
|
49
|
+
WHERE origin_account_name IS NULL AND account_name IS NOT NULL
|
|
50
|
+
`);
|
|
51
|
+
this.db.exec(`
|
|
52
|
+
UPDATE chat_sessions
|
|
53
|
+
SET active_account_name = COALESCE(active_account_name, origin_account_name, account_name)
|
|
54
|
+
WHERE active_account_name IS NULL
|
|
55
|
+
`);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
this.db.exec(`
|
|
59
|
+
UPDATE chat_sessions
|
|
60
|
+
SET active_account_name = origin_account_name
|
|
61
|
+
WHERE active_account_name IS NULL AND origin_account_name IS NOT NULL
|
|
62
|
+
`);
|
|
63
|
+
}
|
|
28
64
|
}
|
|
29
65
|
create(input) {
|
|
66
|
+
const activeAccountName = input.activeAccountName ?? null;
|
|
30
67
|
const stmt = this.db.prepare(`
|
|
31
|
-
INSERT INTO chat_sessions (channel_id, category_id, workspace_path, session_number, guild_id)
|
|
32
|
-
VALUES (?, ?, ?, ?, ?)
|
|
68
|
+
INSERT INTO chat_sessions (channel_id, category_id, workspace_path, session_number, active_account_name, origin_account_name, guild_id)
|
|
69
|
+
VALUES (?, ?, ?, ?, ?, NULL, ?)
|
|
33
70
|
`);
|
|
34
|
-
const result = stmt.run(input.channelId, input.categoryId, input.workspacePath, input.sessionNumber, input.guildId);
|
|
71
|
+
const result = stmt.run(input.channelId, input.categoryId, input.workspacePath, input.sessionNumber, activeAccountName, input.guildId);
|
|
35
72
|
return {
|
|
36
73
|
id: result.lastInsertRowid,
|
|
37
74
|
channelId: input.channelId,
|
|
38
75
|
categoryId: input.categoryId,
|
|
39
76
|
workspacePath: input.workspacePath,
|
|
40
77
|
sessionNumber: input.sessionNumber,
|
|
78
|
+
conversationId: null,
|
|
79
|
+
activeAccountName,
|
|
80
|
+
originAccountName: null,
|
|
41
81
|
displayName: null,
|
|
42
82
|
isRenamed: false,
|
|
43
83
|
guildId: input.guildId,
|
|
@@ -67,6 +107,26 @@ class ChatSessionRepository {
|
|
|
67
107
|
const result = this.db.prepare('UPDATE chat_sessions SET display_name = ?, is_renamed = 1 WHERE channel_id = ?').run(displayName, channelId);
|
|
68
108
|
return result.changes > 0;
|
|
69
109
|
}
|
|
110
|
+
setActiveAccountName(channelId, accountName) {
|
|
111
|
+
const result = this.db.prepare('UPDATE chat_sessions SET active_account_name = ? WHERE channel_id = ?').run(accountName, channelId);
|
|
112
|
+
return result.changes > 0;
|
|
113
|
+
}
|
|
114
|
+
setOriginAccountName(channelId, accountName) {
|
|
115
|
+
const result = this.db.prepare('UPDATE chat_sessions SET origin_account_name = ? WHERE channel_id = ?').run(accountName, channelId);
|
|
116
|
+
return result.changes > 0;
|
|
117
|
+
}
|
|
118
|
+
setConversationId(channelId, conversationId) {
|
|
119
|
+
const result = this.db.prepare('UPDATE chat_sessions SET conversation_id = ? WHERE channel_id = ?').run(conversationId, channelId);
|
|
120
|
+
return result.changes > 0;
|
|
121
|
+
}
|
|
122
|
+
initializeConversationId(channelId, conversationId) {
|
|
123
|
+
const result = this.db.prepare('UPDATE chat_sessions SET conversation_id = ? WHERE channel_id = ? AND conversation_id IS NULL').run(conversationId, channelId);
|
|
124
|
+
return result.changes > 0;
|
|
125
|
+
}
|
|
126
|
+
initializeOriginAccountName(channelId, accountName) {
|
|
127
|
+
const result = this.db.prepare('UPDATE chat_sessions SET origin_account_name = ? WHERE channel_id = ? AND origin_account_name IS NULL').run(accountName, channelId);
|
|
128
|
+
return result.changes > 0;
|
|
129
|
+
}
|
|
70
130
|
/**
|
|
71
131
|
* Find a session by display name within a workspace.
|
|
72
132
|
* Returns the first match (most recent).
|
|
@@ -88,6 +148,9 @@ class ChatSessionRepository {
|
|
|
88
148
|
categoryId: row.category_id,
|
|
89
149
|
workspacePath: row.workspace_path,
|
|
90
150
|
sessionNumber: row.session_number,
|
|
151
|
+
conversationId: row.conversation_id ?? null,
|
|
152
|
+
activeAccountName: row.active_account_name ?? row.account_name ?? null,
|
|
153
|
+
originAccountName: row.origin_account_name ?? null,
|
|
91
154
|
displayName: row.display_name,
|
|
92
155
|
isRenamed: row.is_renamed === 1,
|
|
93
156
|
guildId: row.guild_id,
|
|
@@ -48,6 +48,19 @@ class TelegramBindingRepository {
|
|
|
48
48
|
return undefined;
|
|
49
49
|
return this.mapRow(row);
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Find binding by exact chat ID, or fall back to the base chat ID for topic-style IDs.
|
|
53
|
+
* Example: "12345_67" falls back to "12345" when the topic itself has no direct binding.
|
|
54
|
+
*/
|
|
55
|
+
findByChatIdWithParentFallback(chatId) {
|
|
56
|
+
const exact = this.findByChatId(chatId);
|
|
57
|
+
if (exact)
|
|
58
|
+
return exact;
|
|
59
|
+
const underscoreIndex = chatId.indexOf('_');
|
|
60
|
+
if (underscoreIndex <= 0)
|
|
61
|
+
return undefined;
|
|
62
|
+
return this.findByChatId(chatId.slice(0, underscoreIndex));
|
|
63
|
+
}
|
|
51
64
|
/**
|
|
52
65
|
* Find bindings by workspace path
|
|
53
66
|
*/
|