@pixelbyte-software/pixcode 1.53.4 → 1.53.5
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/assets/{index-DlwfTw9d.js → index-CyeODv14.js} +155 -155
- package/dist/index.html +1 -1
- package/dist-server/server/database/db.js +28 -0
- package/dist-server/server/database/db.js.map +1 -1
- package/dist-server/server/routes/telegram.js +16 -2
- package/dist-server/server/routes/telegram.js.map +1 -1
- package/dist-server/server/services/telegram/bot.js +9 -4
- package/dist-server/server/services/telegram/bot.js.map +1 -1
- package/dist-server/server/services/telegram/control-center.js +390 -67
- package/dist-server/server/services/telegram/control-center.js.map +1 -1
- package/dist-server/server/services/telegram/telegram-gateway.js +220 -0
- package/dist-server/server/services/telegram/telegram-gateway.js.map +1 -0
- package/dist-server/server/services/telegram/telegram-http-client.js +10 -8
- package/dist-server/server/services/telegram/telegram-http-client.js.map +1 -1
- package/dist-server/server/services/telegram/translations.js +18 -0
- package/dist-server/server/services/telegram/translations.js.map +1 -1
- package/package.json +1 -1
- package/scripts/smoke/telegram-control.mjs +10 -1
- package/server/database/db.js +28 -0
- package/server/routes/telegram.js +25 -2
- package/server/services/telegram/bot.js +8 -4
- package/server/services/telegram/control-center.js +410 -66
- package/server/services/telegram/telegram-gateway.js +250 -0
- package/server/services/telegram/telegram-http-client.js +7 -5
- package/server/services/telegram/translations.js +18 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import { telegramLinksDb } from '../../database/db.js';
|
|
4
|
+
|
|
5
|
+
export const TELEGRAM_CONTROL_SCOPES = [
|
|
6
|
+
'admin',
|
|
7
|
+
'telegram:read',
|
|
8
|
+
'telegram:write',
|
|
9
|
+
'providers:read',
|
|
10
|
+
'providers:write',
|
|
11
|
+
'orchestration:read',
|
|
12
|
+
'orchestration:write',
|
|
13
|
+
'projects:read',
|
|
14
|
+
'sessions:read',
|
|
15
|
+
'webhooks:read',
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
export const TELEGRAM_PROVIDERS = ['claude', 'cursor', 'codex', 'gemini', 'qwen', 'opencode'];
|
|
19
|
+
|
|
20
|
+
export const TELEGRAM_PROGRESS_MODES = ['final', 'steps', 'all', 'errors'];
|
|
21
|
+
|
|
22
|
+
const MAX_TELEGRAM_TEXT = 3600;
|
|
23
|
+
const CONFIRMATION_TTL_MS = 5 * 60 * 1000;
|
|
24
|
+
const DEFAULT_ACTION_TIMEOUT_MS = 10 * 60 * 1000;
|
|
25
|
+
const chatQueues = new Map();
|
|
26
|
+
|
|
27
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
28
|
+
|
|
29
|
+
export function splitTelegramText(text, max = MAX_TELEGRAM_TEXT) {
|
|
30
|
+
const value = String(text || '').trim();
|
|
31
|
+
if (!value) return [''];
|
|
32
|
+
if (value.length <= max) return [value];
|
|
33
|
+
|
|
34
|
+
const chunks = [];
|
|
35
|
+
let remaining = value;
|
|
36
|
+
while (remaining.length > max) {
|
|
37
|
+
let cut = remaining.lastIndexOf('\n', max);
|
|
38
|
+
if (cut < Math.floor(max * 0.55)) cut = remaining.lastIndexOf(' ', max);
|
|
39
|
+
if (cut < Math.floor(max * 0.55)) cut = max;
|
|
40
|
+
chunks.push(remaining.slice(0, cut).trim());
|
|
41
|
+
remaining = remaining.slice(cut).trim();
|
|
42
|
+
}
|
|
43
|
+
if (remaining) chunks.push(remaining);
|
|
44
|
+
return chunks;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function normalizeTelegramToolResult(result = {}) {
|
|
48
|
+
if (result?.ok === false) {
|
|
49
|
+
return {
|
|
50
|
+
ok: false,
|
|
51
|
+
message: String(result.message || result.error || 'Action failed.'),
|
|
52
|
+
data: result.data ?? null,
|
|
53
|
+
requiresConfirmation: Boolean(result.requiresConfirmation),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
ok: true,
|
|
58
|
+
message: String(result.message || 'OK'),
|
|
59
|
+
data: result.data ?? result,
|
|
60
|
+
requiresConfirmation: Boolean(result.requiresConfirmation),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function withTimeout(promiseFactory, timeoutMs = DEFAULT_ACTION_TIMEOUT_MS) {
|
|
65
|
+
let timeoutId;
|
|
66
|
+
const timeout = new Promise((_, reject) => {
|
|
67
|
+
timeoutId = setTimeout(() => {
|
|
68
|
+
const error = new Error(`Telegram action timed out after ${timeoutMs}ms`);
|
|
69
|
+
error.code = 'TELEGRAM_ACTION_TIMEOUT';
|
|
70
|
+
reject(error);
|
|
71
|
+
}, timeoutMs);
|
|
72
|
+
});
|
|
73
|
+
return Promise.race([Promise.resolve().then(promiseFactory), timeout])
|
|
74
|
+
.finally(() => clearTimeout(timeoutId));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isRetryableError(error) {
|
|
78
|
+
const status = error?.status || error?.statusCode || error?.response?.status || error?.response?.statusCode;
|
|
79
|
+
if ([408, 409, 425, 429, 500, 502, 503, 504].includes(Number(status))) return true;
|
|
80
|
+
const message = String(error?.message || '');
|
|
81
|
+
return /ECONNRESET|ETIMEDOUT|EAI_AGAIN|fetch failed|network/i.test(message);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function retryWithBackoff(action, {
|
|
85
|
+
retries = 2,
|
|
86
|
+
baseDelayMs = 500,
|
|
87
|
+
maxDelayMs = 4000,
|
|
88
|
+
shouldRetry = isRetryableError,
|
|
89
|
+
} = {}) {
|
|
90
|
+
let lastError;
|
|
91
|
+
for (let attempt = 0; attempt <= retries; attempt += 1) {
|
|
92
|
+
try {
|
|
93
|
+
return await action(attempt);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
lastError = error;
|
|
96
|
+
if (attempt >= retries || !shouldRetry(error)) break;
|
|
97
|
+
const delay = Math.min(maxDelayMs, baseDelayMs * 2 ** attempt);
|
|
98
|
+
await sleep(delay);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
throw lastError;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function enqueueTelegramJob(chatId, job, { timeoutMs = DEFAULT_ACTION_TIMEOUT_MS } = {}) {
|
|
105
|
+
const key = String(chatId);
|
|
106
|
+
const previous = chatQueues.get(key) || Promise.resolve();
|
|
107
|
+
const current = previous
|
|
108
|
+
.catch(() => {})
|
|
109
|
+
.then(() => withTimeout(job, timeoutMs));
|
|
110
|
+
let queued;
|
|
111
|
+
queued = current.finally(() => {
|
|
112
|
+
if (chatQueues.get(key) === queued) chatQueues.delete(key);
|
|
113
|
+
}).catch(() => {});
|
|
114
|
+
chatQueues.set(key, queued);
|
|
115
|
+
return current;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function runTelegramTool({
|
|
119
|
+
userId,
|
|
120
|
+
action,
|
|
121
|
+
execute,
|
|
122
|
+
remoteControlRequired = true,
|
|
123
|
+
timeoutMs = DEFAULT_ACTION_TIMEOUT_MS,
|
|
124
|
+
retries = 1,
|
|
125
|
+
}) {
|
|
126
|
+
const state = telegramLinksDb.getControlState(userId);
|
|
127
|
+
if (remoteControlRequired && state.remoteControlEnabled === false) {
|
|
128
|
+
return normalizeTelegramToolResult({
|
|
129
|
+
ok: false,
|
|
130
|
+
message: 'REMOTE_CONTROL_DISABLED',
|
|
131
|
+
data: { action },
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const data = await retryWithBackoff(
|
|
136
|
+
() => withTimeout(execute, timeoutMs),
|
|
137
|
+
{ retries },
|
|
138
|
+
);
|
|
139
|
+
return normalizeTelegramToolResult({ ok: true, message: 'OK', data });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function createTelegramConfirmation(userId, action, payload = {}) {
|
|
143
|
+
const pending = {
|
|
144
|
+
id: crypto.randomBytes(8).toString('hex'),
|
|
145
|
+
action,
|
|
146
|
+
payload,
|
|
147
|
+
expiresAt: new Date(Date.now() + CONFIRMATION_TTL_MS).toISOString(),
|
|
148
|
+
};
|
|
149
|
+
telegramLinksDb.updateControlState(userId, { pendingConfirmation: pending });
|
|
150
|
+
return pending;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function consumeTelegramConfirmation(userId, id) {
|
|
154
|
+
const state = telegramLinksDb.getControlState(userId);
|
|
155
|
+
const pending = state.pendingConfirmation;
|
|
156
|
+
if (!pending || pending.id !== id) return { ok: false, reason: 'missing' };
|
|
157
|
+
if (Date.parse(pending.expiresAt) <= Date.now()) {
|
|
158
|
+
telegramLinksDb.updateControlState(userId, { pendingConfirmation: null });
|
|
159
|
+
return { ok: false, reason: 'expired' };
|
|
160
|
+
}
|
|
161
|
+
telegramLinksDb.updateControlState(userId, { pendingConfirmation: null });
|
|
162
|
+
return { ok: true, confirmation: pending };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function clearTelegramConfirmation(userId, id = null) {
|
|
166
|
+
const state = telegramLinksDb.getControlState(userId);
|
|
167
|
+
if (id && state.pendingConfirmation?.id !== id) return false;
|
|
168
|
+
telegramLinksDb.updateControlState(userId, { pendingConfirmation: null });
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function normalizedText(text) {
|
|
173
|
+
return String(text || '')
|
|
174
|
+
.trim()
|
|
175
|
+
.toLocaleLowerCase('tr')
|
|
176
|
+
.replace(/\s+/g, ' ');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function classifyTelegramIntent(text, state = {}) {
|
|
180
|
+
if (state.routerEnabled === false || state.routerMode !== 'hybrid') return null;
|
|
181
|
+
|
|
182
|
+
const value = normalizedText(text);
|
|
183
|
+
if (!value) return null;
|
|
184
|
+
|
|
185
|
+
for (const provider of TELEGRAM_PROVIDERS) {
|
|
186
|
+
const providerRe = new RegExp(`(^|\\s)${provider}(\\s|$)`);
|
|
187
|
+
if (
|
|
188
|
+
providerRe.test(value)
|
|
189
|
+
&& /(kullan|seç|sec|switch|use|select|provider|sağlayıcı|saglayici)/i.test(value)
|
|
190
|
+
) {
|
|
191
|
+
return { action: 'provider_select', provider };
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const projectPick = value.match(/^(?:proje(?:yi)?\s+seç|proje(?:yi)?\s+sec|select project|pick project)\s+(.+)$/i);
|
|
196
|
+
if (projectPick?.[1]) return { action: 'project_select_query', query: projectPick[1].trim() };
|
|
197
|
+
|
|
198
|
+
if (/^(?:projeler|projeleri göster|projeleri goster|proje seç|proje sec|projects?|show projects|select project|pick project)$/.test(value)) {
|
|
199
|
+
return { action: 'projects' };
|
|
200
|
+
}
|
|
201
|
+
if (/^(?:sağlayıcı|saglayici|provider|providers|sağlayıcı seç|saglayici sec|change provider|select provider)$/.test(value)) {
|
|
202
|
+
return { action: 'provider_menu' };
|
|
203
|
+
}
|
|
204
|
+
if (/^(?:model|models|model seç|model sec|change model|select model)$/.test(value)) {
|
|
205
|
+
return { action: 'model_menu' };
|
|
206
|
+
}
|
|
207
|
+
if (/(?:son|last|recent).*(?:run|çalışma|calisma|durum|status)|^(?:runs?|çalışmalar|calismalar)$/.test(value)) {
|
|
208
|
+
return { action: 'runs' };
|
|
209
|
+
}
|
|
210
|
+
if (/^(?:approvals?|onaylar|bekleyen onaylar|izinler)$/.test(value)) {
|
|
211
|
+
return { action: 'approvals' };
|
|
212
|
+
}
|
|
213
|
+
if (/^(?:workflow|workflows|iş akışı|is akisi|orkestrasyon|orchestration|workflow seç|workflow sec)$/.test(value)) {
|
|
214
|
+
return { action: 'workflows' };
|
|
215
|
+
}
|
|
216
|
+
if (/^(?:new chat|yeni sohbet|yeni chat)$/.test(value)) {
|
|
217
|
+
return { action: 'new_chat' };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function resolveTelegramProvider(state = {}) {
|
|
224
|
+
const provider = TELEGRAM_PROVIDERS.includes(state.routerProvider)
|
|
225
|
+
? state.routerProvider
|
|
226
|
+
: state.selectedProvider;
|
|
227
|
+
return TELEGRAM_PROVIDERS.includes(provider) ? provider : 'opencode';
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function resolveTelegramModel(state = {}) {
|
|
231
|
+
return typeof state.routerModel === 'string' && state.routerModel.trim()
|
|
232
|
+
? state.routerModel.trim()
|
|
233
|
+
: state.selectedModel;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function buildTelegramAgentPrompt(prompt, state = {}) {
|
|
237
|
+
const provider = resolveTelegramProvider(state);
|
|
238
|
+
const model = resolveTelegramModel(state);
|
|
239
|
+
const project = state.selectedProjectName || state.selectedProjectPath || 'not selected';
|
|
240
|
+
return [
|
|
241
|
+
'Source: Telegram.',
|
|
242
|
+
`Selected project: ${project}.`,
|
|
243
|
+
`Selected provider: ${provider}${model ? ` / ${model}` : ''}.`,
|
|
244
|
+
'Return a Telegram-friendly final summary with status, relevant IDs, concise output, and any diff/test summary.',
|
|
245
|
+
'Keep the final answer brief unless the user explicitly asks for detail.',
|
|
246
|
+
'',
|
|
247
|
+
'User request:',
|
|
248
|
+
String(prompt || '').trim(),
|
|
249
|
+
].join('\n');
|
|
250
|
+
}
|
|
@@ -64,6 +64,7 @@ export class TelegramHttpBot extends EventEmitter {
|
|
|
64
64
|
this._polling = false;
|
|
65
65
|
this._abortController = null;
|
|
66
66
|
this._pollingLoop = null;
|
|
67
|
+
this._pollingErrorCount = 0;
|
|
67
68
|
if (polling) this.startPolling();
|
|
68
69
|
}
|
|
69
70
|
|
|
@@ -184,15 +185,16 @@ export class TelegramHttpBot extends EventEmitter {
|
|
|
184
185
|
await this._emitSerial('callback_query', update.callback_query);
|
|
185
186
|
}
|
|
186
187
|
}
|
|
188
|
+
this._pollingErrorCount = 0;
|
|
187
189
|
} catch (err) {
|
|
188
190
|
// AbortError is the expected path when stopPolling() is called.
|
|
189
191
|
if (err?.name === 'AbortError' || !this._polling) break;
|
|
190
192
|
this.emit('polling_error', err);
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
await new Promise((r) => setTimeout(r,
|
|
193
|
+
const status = err?.response?.statusCode || err?.code;
|
|
194
|
+
const baseDelay = status === 409 ? 2000 : 1000;
|
|
195
|
+
const delay = Math.min(60_000, baseDelay * 2 ** this._pollingErrorCount);
|
|
196
|
+
this._pollingErrorCount = Math.min(this._pollingErrorCount + 1, 6);
|
|
197
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
196
198
|
}
|
|
197
199
|
}
|
|
198
200
|
}
|
|
@@ -55,8 +55,11 @@ const EN = {
|
|
|
55
55
|
'control.button.refresh': 'Refresh',
|
|
56
56
|
'control.button.approve': 'Approve',
|
|
57
57
|
'control.button.deny': 'Deny',
|
|
58
|
+
'control.button.confirm': 'Confirm',
|
|
59
|
+
'control.button.cancel': 'Cancel',
|
|
58
60
|
'control.noProjects': 'No Pixcode projects were found yet. Add/open a project in Pixcode first.',
|
|
59
61
|
'control.pickProject': 'Pick the project Telegram should control:',
|
|
62
|
+
'control.projectNotFound': 'No project matched "{{query}}". Pick one from the list.',
|
|
60
63
|
'control.pickProvider': 'Pick the CLI provider Telegram should use:',
|
|
61
64
|
'control.modelsFor': 'Models for {{provider}}:',
|
|
62
65
|
'control.pickWorkflow': 'Pick an orchestration workflow:',
|
|
@@ -83,6 +86,12 @@ const EN = {
|
|
|
83
86
|
'control.authMenu': 'Pick a provider for OAuth/login instructions:',
|
|
84
87
|
'control.manualInstall': 'Manual install for {{provider}}:\n{{manual}}',
|
|
85
88
|
'control.installStarted': 'Install started for {{provider}}.\nJob: {{jobId}}\nCommand: {{command}}',
|
|
89
|
+
'control.confirmationRequired': 'This action needs confirmation before it can run.',
|
|
90
|
+
'control.confirmInstall': 'Confirm CLI install/repair for {{provider}}?',
|
|
91
|
+
'control.confirmCancelRun': 'Confirm cancel for workflow run {{runId}}?',
|
|
92
|
+
'control.confirmationExpired': 'That confirmation expired. Start the action again.',
|
|
93
|
+
'control.confirmationMissing': 'No matching confirmation is pending. Start the action again.',
|
|
94
|
+
'control.confirmationCanceled': 'Action canceled.',
|
|
86
95
|
'control.settingsTitle': 'Telegram settings',
|
|
87
96
|
'control.pickLanguage': 'Pick Telegram language:',
|
|
88
97
|
'control.menuExpired': 'This menu expired. Send /menu.',
|
|
@@ -157,8 +166,11 @@ const TR = {
|
|
|
157
166
|
'control.button.refresh': 'Yenile',
|
|
158
167
|
'control.button.approve': 'Onayla',
|
|
159
168
|
'control.button.deny': 'Reddet',
|
|
169
|
+
'control.button.confirm': 'Onayla',
|
|
170
|
+
'control.button.cancel': 'Vazgeç',
|
|
160
171
|
'control.noProjects': 'Henüz Pixcode projesi bulunamadı. Önce Pixcode içinde proje ekle/aç.',
|
|
161
172
|
'control.pickProject': 'Telegramın kontrol edeceği projeyi seç:',
|
|
173
|
+
'control.projectNotFound': '"{{query}}" ile eşleşen proje bulunamadı. Listeden birini seç.',
|
|
162
174
|
'control.pickProvider': 'Telegramın kullanacağı CLI sağlayıcısını seç:',
|
|
163
175
|
'control.modelsFor': '{{provider}} modelleri:',
|
|
164
176
|
'control.pickWorkflow': 'Bir orkestrasyon iş akışı seç:',
|
|
@@ -185,6 +197,12 @@ const TR = {
|
|
|
185
197
|
'control.authMenu': 'OAuth/giriş yönergeleri için sağlayıcı seç:',
|
|
186
198
|
'control.manualInstall': '{{provider}} manuel kurulum:\n{{manual}}',
|
|
187
199
|
'control.installStarted': '{{provider}} kurulumu başladı.\nİş: {{jobId}}\nKomut: {{command}}',
|
|
200
|
+
'control.confirmationRequired': 'Bu işlem çalışmadan önce onay gerektiriyor.',
|
|
201
|
+
'control.confirmInstall': '{{provider}} için CLI kurulum/onarım işlemini onaylıyor musun?',
|
|
202
|
+
'control.confirmCancelRun': '{{runId}} workflow çalışmasını iptal etmeyi onaylıyor musun?',
|
|
203
|
+
'control.confirmationExpired': 'Bu onayın süresi doldu. İşlemi yeniden başlat.',
|
|
204
|
+
'control.confirmationMissing': 'Bekleyen eşleşen bir onay yok. İşlemi yeniden başlat.',
|
|
205
|
+
'control.confirmationCanceled': 'İşlem iptal edildi.',
|
|
188
206
|
'control.settingsTitle': 'Telegram ayarları',
|
|
189
207
|
'control.pickLanguage': 'Telegram dilini seç:',
|
|
190
208
|
'control.menuExpired': 'Bu menünün süresi doldu. /menu gönder.',
|