@pixelbyte-software/pixcode 1.36.3 → 1.37.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/assets/index-CfHK8y_H.css +32 -0
- package/dist/assets/{index-Bp8mXdQd.js → index-D8uNxHf1.js} +165 -159
- package/dist/index.html +2 -2
- package/dist-server/server/daemon-manager.js +18 -12
- package/dist-server/server/daemon-manager.js.map +1 -1
- package/dist-server/server/database/db.js +53 -2
- package/dist-server/server/database/db.js.map +1 -1
- package/dist-server/server/index.js +11 -4
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/modules/orchestration/tasks/orchestration-task.routes.js +10 -1
- package/dist-server/server/modules/orchestration/tasks/orchestration-task.routes.js.map +1 -1
- package/dist-server/server/modules/orchestration/tasks/orchestration-task.service.js +7 -0
- package/dist-server/server/modules/orchestration/tasks/orchestration-task.service.js.map +1 -1
- package/dist-server/server/modules/orchestration/workflows/workflow-runner.js +143 -26
- package/dist-server/server/modules/orchestration/workflows/workflow-runner.js.map +1 -1
- package/dist-server/server/routes/taskmaster.js +194 -0
- package/dist-server/server/routes/taskmaster.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/install-jobs.js +1 -0
- package/dist-server/server/services/install-jobs.js.map +1 -1
- package/dist-server/server/services/notification-orchestrator.js +66 -9
- package/dist-server/server/services/notification-orchestrator.js.map +1 -1
- package/dist-server/server/services/telegram/bot.js +48 -6
- package/dist-server/server/services/telegram/bot.js.map +1 -1
- package/dist-server/server/services/telegram/control-center.js +903 -0
- package/dist-server/server/services/telegram/control-center.js.map +1 -0
- package/dist-server/server/services/telegram/telegram-http-client.js +26 -4
- package/dist-server/server/services/telegram/telegram-http-client.js.map +1 -1
- package/dist-server/server/services/telegram/translations.js +150 -2
- package/dist-server/server/services/telegram/translations.js.map +1 -1
- package/package.json +3 -1
- package/scripts/smoke/chat-realtime-hydration.mjs +44 -0
- package/scripts/smoke/daemon-entrypoint.mjs +20 -0
- package/scripts/smoke/multi-worker-slots.mjs +42 -0
- package/scripts/smoke/notification-center.mjs +63 -0
- package/scripts/smoke/orchestration-execution-dashboard.mjs +33 -0
- package/scripts/smoke/orchestration-user-facing-output.mjs +25 -0
- package/scripts/smoke/shell-manual-disconnect.mjs +30 -0
- package/scripts/smoke/side-panel-editor-layout.mjs +34 -0
- package/scripts/smoke/static-root-routing.mjs +21 -0
- package/scripts/smoke/strict-handoff-compact.mjs +60 -0
- package/scripts/smoke/taskmaster-execution-telegram.mjs +52 -0
- package/scripts/smoke/taskmaster-onboarding.mjs +52 -0
- package/scripts/smoke/telegram-control.mjs +242 -0
- package/scripts/smoke/update-issue-progress.mjs +69 -0
- package/scripts/smoke/version-modal-autoshow.mjs +29 -0
- package/server/daemon-manager.js +17 -12
- package/server/database/db.js +56 -2
- package/server/index.js +12 -5
- package/server/modules/orchestration/tasks/orchestration-task.routes.ts +10 -1
- package/server/modules/orchestration/tasks/orchestration-task.service.ts +7 -0
- package/server/modules/orchestration/tasks/orchestration-task.types.ts +3 -0
- package/server/modules/orchestration/workflows/workflow-runner.ts +149 -26
- package/server/modules/orchestration/workflows/workflow.types.ts +2 -0
- package/server/routes/taskmaster.js +201 -0
- package/server/routes/telegram.js +17 -2
- package/server/services/install-jobs.js +1 -0
- package/server/services/notification-orchestrator.js +76 -8
- package/server/services/telegram/bot.js +58 -6
- package/server/services/telegram/control-center.js +965 -0
- package/server/services/telegram/telegram-http-client.js +25 -4
- package/server/services/telegram/translations.js +150 -2
- package/dist/assets/index-Dx7QyTSN.css +0 -32
|
@@ -17,6 +17,12 @@ import express from 'express';
|
|
|
17
17
|
import { orchestrationTaskService } from '@/modules/orchestration/tasks/orchestration-task.service.js';
|
|
18
18
|
|
|
19
19
|
import { extractProjectDirectory } from '../projects.js';
|
|
20
|
+
import {
|
|
21
|
+
cancelInstallJob,
|
|
22
|
+
createInstallJob,
|
|
23
|
+
getInstallJob,
|
|
24
|
+
snapshotDonePayload
|
|
25
|
+
} from '../services/install-jobs.js';
|
|
20
26
|
import { broadcastTaskMasterProjectUpdate, broadcastTaskMasterTasksUpdate } from '../utils/taskmaster-websocket.js';
|
|
21
27
|
import { detectTaskMasterMCPServer } from '../utils/mcp-detector.js';
|
|
22
28
|
|
|
@@ -144,6 +150,17 @@ async function readTaskMasterTasks(projectName) {
|
|
|
144
150
|
return { projectPath, transformedTasks, currentTag };
|
|
145
151
|
}
|
|
146
152
|
|
|
153
|
+
function taskMasterExecutionDescription(task) {
|
|
154
|
+
return [
|
|
155
|
+
task.description ? `Description:\n${task.description}` : '',
|
|
156
|
+
task.details ? `Details:\n${task.details}` : '',
|
|
157
|
+
task.testStrategy ? `Test strategy:\n${task.testStrategy}` : '',
|
|
158
|
+
Array.isArray(task.dependencies) && task.dependencies.length
|
|
159
|
+
? `Dependencies: ${task.dependencies.join(', ')}`
|
|
160
|
+
: '',
|
|
161
|
+
].filter(Boolean).join('\n\n');
|
|
162
|
+
}
|
|
163
|
+
|
|
147
164
|
// API Routes
|
|
148
165
|
|
|
149
166
|
/**
|
|
@@ -181,6 +198,121 @@ router.get('/installation-status', async (req, res) => {
|
|
|
181
198
|
}
|
|
182
199
|
});
|
|
183
200
|
|
|
201
|
+
/**
|
|
202
|
+
* POST /api/taskmaster/install
|
|
203
|
+
* Install TaskMaster CLI into Pixcode's sandboxed CLI bin.
|
|
204
|
+
*/
|
|
205
|
+
router.post('/install', async (req, res) => {
|
|
206
|
+
try {
|
|
207
|
+
const job = createInstallJob({
|
|
208
|
+
provider: 'taskmaster',
|
|
209
|
+
installCmd: 'npm install -g task-master',
|
|
210
|
+
packageName: 'task-master'
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
res.json({
|
|
214
|
+
success: true,
|
|
215
|
+
jobId: job.id,
|
|
216
|
+
provider: 'taskmaster',
|
|
217
|
+
packageName: 'task-master',
|
|
218
|
+
startedAt: job.startedAt
|
|
219
|
+
});
|
|
220
|
+
} catch (error) {
|
|
221
|
+
console.error('TaskMaster install start error:', error);
|
|
222
|
+
res.status(500).json({
|
|
223
|
+
success: false,
|
|
224
|
+
error: 'Failed to start TaskMaster install',
|
|
225
|
+
message: error.message
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* GET /api/taskmaster/install/:jobId/stream
|
|
232
|
+
* Replay and stream TaskMaster install output.
|
|
233
|
+
*/
|
|
234
|
+
router.get('/install/:jobId/stream', async (req, res) => {
|
|
235
|
+
const job = getInstallJob(req.params.jobId);
|
|
236
|
+
if (!job || job.provider !== 'taskmaster') {
|
|
237
|
+
return res.status(404).json({
|
|
238
|
+
success: false,
|
|
239
|
+
error: 'Install job not found or already expired'
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
244
|
+
res.setHeader('Cache-Control', 'no-cache, no-transform');
|
|
245
|
+
res.setHeader('Connection', 'keep-alive');
|
|
246
|
+
res.setHeader('X-Accel-Buffering', 'no');
|
|
247
|
+
if (typeof res.flushHeaders === 'function') res.flushHeaders();
|
|
248
|
+
|
|
249
|
+
let closed = false;
|
|
250
|
+
const write = (event, payload) => {
|
|
251
|
+
if (closed) return;
|
|
252
|
+
try {
|
|
253
|
+
res.write(`event: ${event}\n`);
|
|
254
|
+
res.write(`data: ${JSON.stringify(payload)}\n\n`);
|
|
255
|
+
} catch {
|
|
256
|
+
// Socket is gone.
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
try { res.write(': start\n\n'); } catch { /* noop */ }
|
|
261
|
+
const heartbeat = setInterval(() => {
|
|
262
|
+
if (!closed) {
|
|
263
|
+
try { res.write(': ping\n\n'); } catch { /* noop */ }
|
|
264
|
+
}
|
|
265
|
+
}, 5000);
|
|
266
|
+
|
|
267
|
+
for (const entry of job.logs) {
|
|
268
|
+
write('log', { stream: entry.stream, chunk: entry.chunk });
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const cleanup = () => {
|
|
272
|
+
if (closed) return;
|
|
273
|
+
closed = true;
|
|
274
|
+
clearInterval(heartbeat);
|
|
275
|
+
job.emitter.off('log', onLog);
|
|
276
|
+
job.emitter.off('done', onDone);
|
|
277
|
+
};
|
|
278
|
+
const onLog = (entry) => {
|
|
279
|
+
write('log', { stream: entry.stream, chunk: entry.chunk });
|
|
280
|
+
};
|
|
281
|
+
const onDone = (payload) => {
|
|
282
|
+
write('done', payload);
|
|
283
|
+
cleanup();
|
|
284
|
+
try { res.end(); } catch { /* noop */ }
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
if (job.status !== 'running') {
|
|
288
|
+
write('done', snapshotDonePayload(job));
|
|
289
|
+
cleanup();
|
|
290
|
+
try { res.end(); } catch { /* noop */ }
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
job.emitter.on('log', onLog);
|
|
295
|
+
job.emitter.once('done', onDone);
|
|
296
|
+
|
|
297
|
+
req.on('close', cleanup);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* DELETE /api/taskmaster/install/:jobId
|
|
302
|
+
* Cancel a running TaskMaster install job.
|
|
303
|
+
*/
|
|
304
|
+
router.delete('/install/:jobId', async (req, res) => {
|
|
305
|
+
const job = getInstallJob(req.params.jobId);
|
|
306
|
+
if (!job || job.provider !== 'taskmaster') {
|
|
307
|
+
return res.status(404).json({
|
|
308
|
+
success: false,
|
|
309
|
+
error: 'Install job not found'
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
res.json({ success: true, cancelled: cancelInstallJob(req.params.jobId) });
|
|
314
|
+
});
|
|
315
|
+
|
|
184
316
|
/**
|
|
185
317
|
* GET /api/taskmaster/tasks/:projectName
|
|
186
318
|
* Load actual tasks from .taskmaster/tasks/tasks.json
|
|
@@ -230,6 +362,75 @@ router.get('/tasks/:projectName', async (req, res) => {
|
|
|
230
362
|
}
|
|
231
363
|
});
|
|
232
364
|
|
|
365
|
+
/**
|
|
366
|
+
* POST /api/taskmaster/execute/:projectName/:taskId
|
|
367
|
+
* Import a TaskMaster task into orchestration and dispatch it to a CLI agent.
|
|
368
|
+
*/
|
|
369
|
+
router.post('/execute/:projectName/:taskId', async (req, res) => {
|
|
370
|
+
try {
|
|
371
|
+
const { projectName, taskId } = req.params;
|
|
372
|
+
const adapterId = typeof req.body?.adapterId === 'string'
|
|
373
|
+
? req.body.adapterId
|
|
374
|
+
: typeof req.body?.provider === 'string'
|
|
375
|
+
? req.body.provider
|
|
376
|
+
: '';
|
|
377
|
+
const model = typeof req.body?.model === 'string' ? req.body.model : undefined;
|
|
378
|
+
const permissionMode = typeof req.body?.permissionMode === 'string' ? req.body.permissionMode : undefined;
|
|
379
|
+
const isolation = ['host', 'worktree', 'docker'].includes(req.body?.isolation)
|
|
380
|
+
? req.body.isolation
|
|
381
|
+
: 'worktree';
|
|
382
|
+
const projectId = typeof req.body?.projectId === 'string' ? req.body.projectId : projectName;
|
|
383
|
+
|
|
384
|
+
if (!adapterId) {
|
|
385
|
+
return res.status(400).json({
|
|
386
|
+
success: false,
|
|
387
|
+
error: 'Missing adapterId',
|
|
388
|
+
message: 'adapterId or provider is required'
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const { projectPath, transformedTasks } = await readTaskMasterTasks(projectName);
|
|
393
|
+
const task = transformedTasks.find((candidate) => String(candidate.id) === String(taskId));
|
|
394
|
+
if (!task) {
|
|
395
|
+
return res.status(404).json({
|
|
396
|
+
success: false,
|
|
397
|
+
error: 'TaskMaster task not found',
|
|
398
|
+
message: `Task "${taskId}" was not found in project "${projectName}"`
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const orchestrationTask = orchestrationTaskService.upsertFromTaskMaster({
|
|
403
|
+
projectId,
|
|
404
|
+
taskmasterId: String(task.id),
|
|
405
|
+
title: `TaskMaster #${task.id}: ${task.title}`,
|
|
406
|
+
description: taskMasterExecutionDescription(task)
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
const dispatchedTask = await orchestrationTaskService.dispatch(orchestrationTask.id, {
|
|
410
|
+
adapterId,
|
|
411
|
+
isolation,
|
|
412
|
+
projectPath,
|
|
413
|
+
model,
|
|
414
|
+
permissionMode
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
res.json({
|
|
418
|
+
success: true,
|
|
419
|
+
projectName,
|
|
420
|
+
projectPath,
|
|
421
|
+
taskmasterTask: task,
|
|
422
|
+
task: dispatchedTask
|
|
423
|
+
});
|
|
424
|
+
} catch (error) {
|
|
425
|
+
console.error('TaskMaster execute error:', error);
|
|
426
|
+
res.status(500).json({
|
|
427
|
+
success: false,
|
|
428
|
+
error: 'Failed to execute TaskMaster task',
|
|
429
|
+
message: error.message
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
|
|
233
434
|
/**
|
|
234
435
|
* POST /api/taskmaster/sync-orchestration/:projectName
|
|
235
436
|
* One-way sync: TaskMaster -> Orchestration tasks
|
|
@@ -33,6 +33,7 @@ router.get('/status', (req, res) => {
|
|
|
33
33
|
language: link.language,
|
|
34
34
|
notificationsEnabled: Boolean(link.notifications_enabled),
|
|
35
35
|
bridgeEnabled: Boolean(link.bridge_enabled),
|
|
36
|
+
control: telegramLinksDb.getControlState(req.user.id),
|
|
36
37
|
pairingCode: link.pairing_code,
|
|
37
38
|
pairingExpiresAt: link.pairing_code_expires_at,
|
|
38
39
|
verifiedAt: link.verified_at,
|
|
@@ -98,13 +99,27 @@ router.post('/pairing-code', (req, res) => {
|
|
|
98
99
|
// PATCH /api/telegram/link — update language / toggles on the user's link
|
|
99
100
|
router.patch('/link', (req, res) => {
|
|
100
101
|
try {
|
|
101
|
-
const { language, notificationsEnabled, bridgeEnabled } = req.body || {};
|
|
102
|
+
const { language, notificationsEnabled, bridgeEnabled, controlEnabled, progressMode } = req.body || {};
|
|
102
103
|
const payload = {};
|
|
103
104
|
if (language !== undefined) payload.language = sanitizeLanguage(language);
|
|
104
105
|
if (notificationsEnabled !== undefined) payload.notificationsEnabled = Boolean(notificationsEnabled);
|
|
105
106
|
if (bridgeEnabled !== undefined) payload.bridgeEnabled = Boolean(bridgeEnabled);
|
|
106
107
|
telegramLinksDb.updatePreferences(req.user.id, payload);
|
|
107
|
-
|
|
108
|
+
|
|
109
|
+
const controlPatch = {};
|
|
110
|
+
if (controlEnabled !== undefined) controlPatch.remoteControlEnabled = Boolean(controlEnabled);
|
|
111
|
+
if (progressMode !== undefined && ['final', 'steps', 'all'].includes(progressMode)) {
|
|
112
|
+
controlPatch.progressMode = progressMode;
|
|
113
|
+
}
|
|
114
|
+
if (Object.keys(controlPatch).length > 0) {
|
|
115
|
+
telegramLinksDb.updateControlState(req.user.id, controlPatch);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
res.json({
|
|
119
|
+
success: true,
|
|
120
|
+
link: telegramLinksDb.getByUserId(req.user.id),
|
|
121
|
+
control: telegramLinksDb.getControlState(req.user.id),
|
|
122
|
+
});
|
|
108
123
|
} catch (error) {
|
|
109
124
|
console.error('telegram/link patch failed:', error);
|
|
110
125
|
res.status(500).json({ error: 'Failed to update link' });
|
|
@@ -23,6 +23,11 @@ const PROVIDER_LABELS = {
|
|
|
23
23
|
|
|
24
24
|
const recentEventKeys = new Map();
|
|
25
25
|
const DEDUPE_WINDOW_MS = 20000;
|
|
26
|
+
let notificationWebSocketServer = null;
|
|
27
|
+
|
|
28
|
+
function setNotificationWebSocketServer(wss) {
|
|
29
|
+
notificationWebSocketServer = wss;
|
|
30
|
+
}
|
|
26
31
|
|
|
27
32
|
const cleanupOldEventKeys = () => {
|
|
28
33
|
const now = Date.now();
|
|
@@ -41,6 +46,22 @@ function shouldSendPush(preferences, event) {
|
|
|
41
46
|
return webPushEnabled && eventEnabled;
|
|
42
47
|
}
|
|
43
48
|
|
|
49
|
+
function shouldSendInApp(preferences, event) {
|
|
50
|
+
const inAppEnabled = preferences?.channels?.inApp !== false;
|
|
51
|
+
const prefEventKey = KIND_TO_PREF_KEY[event.kind];
|
|
52
|
+
const eventEnabled = prefEventKey ? preferences?.events?.[prefEventKey] !== false : true;
|
|
53
|
+
|
|
54
|
+
return inAppEnabled && eventEnabled;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function shouldSendTelegram(preferences, event) {
|
|
58
|
+
const telegramEnabled = preferences?.channels?.telegram !== false;
|
|
59
|
+
const prefEventKey = KIND_TO_PREF_KEY[event.kind];
|
|
60
|
+
const eventEnabled = prefEventKey ? preferences?.events?.[prefEventKey] !== false : true;
|
|
61
|
+
|
|
62
|
+
return telegramEnabled && eventEnabled;
|
|
63
|
+
}
|
|
64
|
+
|
|
44
65
|
function isDuplicate(event) {
|
|
45
66
|
cleanupOldEventKeys();
|
|
46
67
|
const key = event.dedupeKey || `${event.provider}:${event.kind || 'info'}:${event.code || 'generic'}:${event.sessionId || 'none'}`;
|
|
@@ -149,6 +170,41 @@ function buildPushBody(event) {
|
|
|
149
170
|
};
|
|
150
171
|
}
|
|
151
172
|
|
|
173
|
+
function buildNotificationPayload(event) {
|
|
174
|
+
const pushBody = buildPushBody(event);
|
|
175
|
+
return {
|
|
176
|
+
id: event.dedupeKey || `${event.provider || 'system'}:${event.kind || 'info'}:${event.code || 'generic'}:${event.sessionId || 'none'}:${event.createdAt}`,
|
|
177
|
+
title: pushBody.title,
|
|
178
|
+
body: pushBody.body,
|
|
179
|
+
kind: event.kind || 'info',
|
|
180
|
+
code: event.code || 'generic.info',
|
|
181
|
+
severity: event.severity || 'info',
|
|
182
|
+
provider: event.provider || null,
|
|
183
|
+
sessionId: event.sessionId || null,
|
|
184
|
+
createdAt: event.createdAt,
|
|
185
|
+
requiresUserAction: Boolean(event.requiresUserAction),
|
|
186
|
+
data: pushBody.data
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function broadcastInAppNotification(userId, event) {
|
|
191
|
+
if (!notificationWebSocketServer || !userId || !event) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const message = JSON.stringify({
|
|
196
|
+
type: 'notification:event',
|
|
197
|
+
notification: buildNotificationPayload(event)
|
|
198
|
+
});
|
|
199
|
+
const normalizedUserId = String(userId);
|
|
200
|
+
|
|
201
|
+
notificationWebSocketServer.clients.forEach((client) => {
|
|
202
|
+
if (client.readyState === 1 && String(client.userId || '') === normalizedUserId) {
|
|
203
|
+
client.send(message);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
152
208
|
async function sendWebPush(userId, event) {
|
|
153
209
|
const subscriptions = pushSubscriptionsDb.getSubscriptions(userId);
|
|
154
210
|
if (!subscriptions.length) return;
|
|
@@ -191,6 +247,14 @@ function notifyUserIfEnabled({ userId, event }) {
|
|
|
191
247
|
return;
|
|
192
248
|
}
|
|
193
249
|
|
|
250
|
+
if (shouldSendInApp(preferences, event)) {
|
|
251
|
+
try {
|
|
252
|
+
broadcastInAppNotification(userId, event);
|
|
253
|
+
} catch (err) {
|
|
254
|
+
console.error('In-app notification send error:', err);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
194
258
|
if (shouldSendPush(preferences, event)) {
|
|
195
259
|
sendWebPush(userId, event).catch((err) => {
|
|
196
260
|
console.error('Web push send error:', err);
|
|
@@ -203,14 +267,16 @@ function notifyUserIfEnabled({ userId, event }) {
|
|
|
203
267
|
const providerLabel = PROVIDER_LABELS[event.provider] || event.provider || 'Session';
|
|
204
268
|
const sessionTitle = event.meta?.sessionName || providerLabel;
|
|
205
269
|
const errorText = event.meta?.error || '';
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
270
|
+
if (shouldSendTelegram(preferences, event)) {
|
|
271
|
+
notifyTelegramUser({
|
|
272
|
+
userId,
|
|
273
|
+
kind: event.kind,
|
|
274
|
+
title: sessionTitle,
|
|
275
|
+
error: errorText,
|
|
276
|
+
}).catch((err) => {
|
|
277
|
+
console.warn('[telegram] notify failed:', err?.message || err);
|
|
278
|
+
});
|
|
279
|
+
}
|
|
214
280
|
}
|
|
215
281
|
|
|
216
282
|
function notifyRunStopped({ userId, provider, sessionId = null, stopReason = 'completed', sessionName = null }) {
|
|
@@ -247,6 +313,8 @@ function notifyRunFailed({ userId, provider, sessionId = null, error, sessionNam
|
|
|
247
313
|
|
|
248
314
|
export {
|
|
249
315
|
createNotificationEvent,
|
|
316
|
+
setNotificationWebSocketServer,
|
|
317
|
+
broadcastInAppNotification,
|
|
250
318
|
notifyUserIfEnabled,
|
|
251
319
|
notifyRunStopped,
|
|
252
320
|
notifyRunFailed
|
|
@@ -2,6 +2,13 @@ import { EventEmitter } from 'node:events';
|
|
|
2
2
|
|
|
3
3
|
import { telegramConfigDb, telegramLinksDb } from '../../database/db.js';
|
|
4
4
|
|
|
5
|
+
import {
|
|
6
|
+
getTelegramControlCommand,
|
|
7
|
+
handleTelegramControlCallback,
|
|
8
|
+
handleTelegramControlMessage,
|
|
9
|
+
isTelegramControlCommand,
|
|
10
|
+
showMainMenu,
|
|
11
|
+
} from './control-center.js';
|
|
5
12
|
import { t } from './translations.js';
|
|
6
13
|
// Swapped in v1.32: previously `node-telegram-bot-api` which carried the
|
|
7
14
|
// deprecated `request`/`har-validator`/`uuid@3` chain. TelegramHttpBot is
|
|
@@ -21,6 +28,10 @@ let bot = null;
|
|
|
21
28
|
let botInfo = null; // { id, username, first_name }
|
|
22
29
|
let lastError = null;
|
|
23
30
|
|
|
31
|
+
export const setTelegramBotForTesting = (nextBot) => {
|
|
32
|
+
bot = nextBot;
|
|
33
|
+
};
|
|
34
|
+
|
|
24
35
|
// Subscribers (notification-orchestrator, future session bridge) use this to
|
|
25
36
|
// react to events without importing the bot module directly.
|
|
26
37
|
export const telegramEvents = new EventEmitter();
|
|
@@ -64,14 +75,14 @@ const parseMaybeCode = (text) => {
|
|
|
64
75
|
return /^\d{6}$/.test(trimmed) ? trimmed : null;
|
|
65
76
|
};
|
|
66
77
|
|
|
67
|
-
const safeSend = async (chatId, text) => {
|
|
78
|
+
const safeSend = async (chatId, text, extra = {}) => {
|
|
68
79
|
if (!bot) return;
|
|
69
80
|
try {
|
|
70
|
-
await bot.sendMessage(chatId, text, { parse_mode: 'Markdown' });
|
|
81
|
+
await bot.sendMessage(chatId, text, { parse_mode: 'Markdown', ...extra });
|
|
71
82
|
} catch (err) {
|
|
72
83
|
// Markdown parse errors from user input are common; retry plaintext.
|
|
73
84
|
try {
|
|
74
|
-
await bot.sendMessage(chatId, text);
|
|
85
|
+
await bot.sendMessage(chatId, text, extra);
|
|
75
86
|
} catch (fallbackErr) {
|
|
76
87
|
console.warn('[telegram] sendMessage failed:', fallbackErr?.message || fallbackErr);
|
|
77
88
|
}
|
|
@@ -98,19 +109,21 @@ const handlePairing = async (msg, code) => {
|
|
|
98
109
|
telegramEvents.emit('paired', { userId: link.user_id, chatId: String(msg.chat.id), username: telegramUsername });
|
|
99
110
|
|
|
100
111
|
await safeSend(msg.chat.id, t(language, 'pairing.success'));
|
|
112
|
+
await safeSend(msg.chat.id, t(language, 'control.onboarding'));
|
|
113
|
+
await showMainMenu({ bot, chatId: msg.chat.id, link: telegramLinksDb.getByUserId(link.user_id) });
|
|
101
114
|
};
|
|
102
115
|
|
|
103
116
|
const handleBridgeMessage = async (msg, existing) => {
|
|
104
117
|
const language = existing.language || 'en';
|
|
105
118
|
|
|
119
|
+
const handled = await handleTelegramControlMessage({ bot, msg, link: existing, safeSend });
|
|
120
|
+
if (handled) return;
|
|
121
|
+
|
|
106
122
|
if (!existing.bridge_enabled) {
|
|
107
123
|
await safeSend(msg.chat.id, t(language, 'bridge.disabled'));
|
|
108
124
|
return;
|
|
109
125
|
}
|
|
110
126
|
|
|
111
|
-
// Fan out to subscribers (future: session-prompt bridge). We don't do the
|
|
112
|
-
// actual agent dispatch here to keep the bot service narrowly focused on
|
|
113
|
-
// Telegram I/O and let the rest of the server opt in.
|
|
114
127
|
telegramEvents.emit('prompt', {
|
|
115
128
|
userId: existing.user_id,
|
|
116
129
|
chatId: String(msg.chat.id),
|
|
@@ -122,11 +135,43 @@ const handleBridgeMessage = async (msg, existing) => {
|
|
|
122
135
|
await safeSend(msg.chat.id, t(language, 'bridge.queued'));
|
|
123
136
|
};
|
|
124
137
|
|
|
138
|
+
const handleCallbackQuery = async (query) => {
|
|
139
|
+
const chatId = query?.message?.chat?.id;
|
|
140
|
+
if (!chatId) return;
|
|
141
|
+
const existing = telegramLinksDb.getByChatId(String(chatId));
|
|
142
|
+
if (!existing) {
|
|
143
|
+
await bot?.answerCallbackQuery(query.id, { text: 'Pair Pixcode first.' }).catch(() => {});
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
await handleTelegramControlCallback({ bot, query, link: existing, safeSend });
|
|
147
|
+
};
|
|
148
|
+
|
|
125
149
|
const handleMessage = async (msg) => {
|
|
126
150
|
if (!msg?.chat?.id || !msg?.text) return;
|
|
127
151
|
|
|
128
152
|
const existing = telegramLinksDb.getByChatId(String(msg.chat.id));
|
|
129
153
|
if (existing) {
|
|
154
|
+
const language = existing.language || 'en';
|
|
155
|
+
const command = getTelegramControlCommand(msg.text);
|
|
156
|
+
if (command === '/start') {
|
|
157
|
+
await safeSend(msg.chat.id, t(language, 'control.onboarding'));
|
|
158
|
+
await showMainMenu({ bot, chatId: msg.chat.id, link: existing });
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (command === '/help') {
|
|
162
|
+
await safeSend(msg.chat.id, t(language, 'control.help'));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (command === '/menu' || command === 'menu') {
|
|
166
|
+
await showMainMenu({ bot, chatId: msg.chat.id, link: existing });
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (isTelegramControlCommand(msg.text)) {
|
|
171
|
+
await handleTelegramControlMessage({ bot, msg, link: existing });
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
130
175
|
// Paired user path: a 6-digit-only message is treated as noise (we keep
|
|
131
176
|
// the already-paired binding); anything else is bridge traffic.
|
|
132
177
|
const maybeCode = parseMaybeCode(msg.text);
|
|
@@ -157,6 +202,8 @@ const handleMessage = async (msg) => {
|
|
|
157
202
|
await safeSend(msg.chat.id, t('en', 'pairing.stillNeeded'));
|
|
158
203
|
};
|
|
159
204
|
|
|
205
|
+
export const handleIncomingTelegramMessage = handleMessage;
|
|
206
|
+
|
|
160
207
|
const wirePollingErrors = () => {
|
|
161
208
|
if (!bot) return;
|
|
162
209
|
bot.on('polling_error', (err) => {
|
|
@@ -213,6 +260,11 @@ export const startBot = async ({ token, persist = true } = {}) => {
|
|
|
213
260
|
console.error('[telegram] handleMessage crashed:', err);
|
|
214
261
|
});
|
|
215
262
|
});
|
|
263
|
+
bot.on('callback_query', (query) => {
|
|
264
|
+
handleCallbackQuery(query).catch((err) => {
|
|
265
|
+
console.error('[telegram] handleCallbackQuery crashed:', err);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
216
268
|
wirePollingErrors();
|
|
217
269
|
|
|
218
270
|
console.log(`[telegram] bot started as @${me.username}`);
|