@pixelbyte-software/pixcode 1.53.3 → 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.
@@ -2,10 +2,12 @@ import crypto from 'node:crypto';
2
2
  import { apiKeysDb, telegramLinksDb } from '../../database/db.js';
3
3
  import { getProjects } from '../../projects.js';
4
4
  import { getStaticProviderModels } from '../model-registry.js';
5
+ import { TELEGRAM_CONTROL_SCOPES, TELEGRAM_PROVIDERS, buildTelegramAgentPrompt, classifyTelegramIntent, clearTelegramConfirmation, consumeTelegramConfirmation, createTelegramConfirmation, enqueueTelegramJob, resolveTelegramModel, resolveTelegramProvider, retryWithBackoff, runTelegramTool, splitTelegramText, } from './telegram-gateway.js';
5
6
  import { SUPPORTED_LANGUAGES, t } from './translations.js';
6
- const PROVIDERS = ['claude', 'cursor', 'codex', 'gemini', 'qwen', 'opencode'];
7
+ const PROVIDERS = TELEGRAM_PROVIDERS;
7
8
  const TERMINAL_RUN_STATES = new Set(['completed', 'failed', 'canceled']);
8
9
  const CALLBACK_TTL_MS = 10 * 60 * 1000;
10
+ const MAX_CALLBACK_ACTIONS = 1000;
9
11
  const MAX_TELEGRAM_TEXT = 3600;
10
12
  const callbackActions = new Map();
11
13
  const runMonitors = new Map();
@@ -77,7 +79,22 @@ function getState(userId) {
77
79
  export function updateTelegramControlState(userId, patch) {
78
80
  return telegramLinksDb.updateControlState(userId, patch);
79
81
  }
82
+ function pruneCallbackActions() {
83
+ const now = Date.now();
84
+ for (const [id, entry] of callbackActions.entries()) {
85
+ if (entry.expiresAt < now) {
86
+ callbackActions.delete(id);
87
+ }
88
+ }
89
+ while (callbackActions.size > MAX_CALLBACK_ACTIONS) {
90
+ const oldestId = callbackActions.keys().next().value;
91
+ if (!oldestId)
92
+ break;
93
+ callbackActions.delete(oldestId);
94
+ }
95
+ }
80
96
  function registerAction(action, payload = {}) {
97
+ pruneCallbackActions();
81
98
  const id = crypto.randomBytes(8).toString('hex');
82
99
  callbackActions.set(id, {
83
100
  action,
@@ -97,7 +114,11 @@ function readAction(data) {
97
114
  callbackActions.delete(id);
98
115
  return null;
99
116
  }
100
- return entry;
117
+ return { id, ...entry };
118
+ }
119
+ function forgetAction(id) {
120
+ if (id)
121
+ callbackActions.delete(id);
101
122
  }
102
123
  function button(text, action, payload = {}) {
103
124
  return { text, callback_data: registerAction(action, payload) };
@@ -115,30 +136,46 @@ async function send(bot, chatId, text, options = {}) {
115
136
  disable_web_page_preview: true,
116
137
  ...telegramOptions,
117
138
  };
118
- const messageText = truncate(text);
139
+ const chunks = splitTelegramText(text, MAX_TELEGRAM_TEXT);
140
+ const [firstChunk = ''] = chunks;
141
+ let startIndex = 0;
119
142
  if (editMessageId && typeof bot.editMessageText === 'function') {
120
143
  try {
121
- return await bot.editMessageText(messageText, {
144
+ const edited = await bot.editMessageText(firstChunk, {
122
145
  chat_id: chatId,
123
146
  message_id: editMessageId,
124
147
  ...extra,
125
148
  });
149
+ startIndex = 1;
150
+ for (const chunk of chunks.slice(startIndex)) {
151
+ await send(bot, chatId, chunk, telegramOptions);
152
+ }
153
+ return edited;
126
154
  }
127
155
  catch (err) {
128
156
  const description = err?.response?.body?.description || err?.message || '';
129
- if (/message is not modified/i.test(description))
157
+ if (/message is not modified/i.test(description)) {
158
+ startIndex = 1;
159
+ for (const chunk of chunks.slice(startIndex)) {
160
+ await send(bot, chatId, chunk, telegramOptions);
161
+ }
130
162
  return null;
163
+ }
131
164
  console.warn('[telegram-control] editMessageText failed:', description || err);
132
165
  }
133
166
  }
134
- try {
135
- return await bot.sendMessage(chatId, messageText, extra);
136
- }
137
- catch {
138
- const fallback = { ...extra };
139
- delete fallback.parse_mode;
140
- return bot.sendMessage(chatId, messageText, fallback);
167
+ let result = null;
168
+ for (const chunk of chunks.slice(startIndex)) {
169
+ try {
170
+ result = await bot.sendMessage(chatId, chunk, extra);
171
+ }
172
+ catch {
173
+ const fallback = { ...extra };
174
+ delete fallback.parse_mode;
175
+ result = await bot.sendMessage(chatId, chunk, fallback);
176
+ }
141
177
  }
178
+ return result;
142
179
  }
143
180
  function localApiBase() {
144
181
  const port = process.env.SERVER_PORT || process.env.PORT || '3001';
@@ -148,19 +185,34 @@ function getOrCreateTelegramApiKey(userId) {
148
185
  const existing = apiKeysDb
149
186
  .getApiKeys(userId)
150
187
  .find((key) => key.key_name === 'Telegram Control' && Boolean(key.is_active));
151
- if (existing?.api_key)
188
+ if (existing?.api_key) {
189
+ const existingScopes = apiKeysDb.normalizeScopes(existing.scopes || []);
190
+ const nextScopes = apiKeysDb.normalizeScopes([...existingScopes, ...TELEGRAM_CONTROL_SCOPES]);
191
+ const hasAllScopes = TELEGRAM_CONTROL_SCOPES.every((scope) => existingScopes.includes(scope));
192
+ if (!hasAllScopes || nextScopes.length !== existingScopes.length) {
193
+ apiKeysDb.updateApiKeyScopes(userId, existing.id, nextScopes);
194
+ }
152
195
  return existing.api_key;
153
- return apiKeysDb.createApiKey(userId, 'Telegram Control').apiKey;
196
+ }
197
+ return apiKeysDb.createApiKey(userId, 'Telegram Control', TELEGRAM_CONTROL_SCOPES).apiKey;
154
198
  }
155
199
  async function localApi(userId, path, { method = 'GET', body } = {}) {
156
200
  const apiKey = getOrCreateTelegramApiKey(userId);
157
- const response = await fetch(`${localApiBase()}${path}`, {
158
- method,
159
- headers: {
160
- 'Content-Type': 'application/json',
161
- 'X-API-Key': apiKey,
162
- },
163
- body: body === undefined ? undefined : JSON.stringify(body),
201
+ const response = await retryWithBackoff(async () => {
202
+ const res = await fetch(`${localApiBase()}${path}`, {
203
+ method,
204
+ headers: {
205
+ 'Content-Type': 'application/json',
206
+ 'X-API-Key': apiKey,
207
+ },
208
+ body: body === undefined ? undefined : JSON.stringify(body),
209
+ });
210
+ if ([408, 429, 500, 502, 503, 504].includes(res.status)) {
211
+ const error = new Error(`HTTP ${res.status}`);
212
+ error.status = res.status;
213
+ throw error;
214
+ }
215
+ return res;
164
216
  });
165
217
  const data = await response.json().catch(() => ({}));
166
218
  if (!response.ok) {
@@ -412,6 +464,36 @@ function extractAssistantText(response) {
412
464
  }
413
465
  return chunks.join('\n\n').trim();
414
466
  }
467
+ function confirmationLabel(lang, action, payload = {}) {
468
+ if (action === 'install_provider') {
469
+ return t(lang, 'control.confirmInstall', { provider: payload.provider || '' });
470
+ }
471
+ if (action === 'run_cancel') {
472
+ return t(lang, 'control.confirmCancelRun', { runId: payload.runId || '' });
473
+ }
474
+ return t(lang, 'control.confirmationRequired');
475
+ }
476
+ async function requestConfirmation({ bot, chatId, link, action, payload = {}, editMessageId }) {
477
+ const lang = languageFor(link);
478
+ const pending = createTelegramConfirmation(link.user_id, action, payload);
479
+ await send(bot, chatId, confirmationLabel(lang, action, payload), {
480
+ editMessageId,
481
+ reply_markup: {
482
+ inline_keyboard: [[
483
+ button(t(lang, 'control.button.confirm'), 'confirm_action', { id: pending.id }),
484
+ button(t(lang, 'control.button.cancel'), 'cancel_confirmation', { id: pending.id }),
485
+ ]],
486
+ },
487
+ });
488
+ return { ok: false, requiresConfirmation: true };
489
+ }
490
+ async function sendToolFailure({ bot, chatId, link, result, editMessageId }) {
491
+ const lang = languageFor(link);
492
+ const message = result?.message === 'REMOTE_CONTROL_DISABLED'
493
+ ? t(lang, 'control.disabled')
494
+ : (result?.message || t(lang, 'error.generic'));
495
+ await send(bot, chatId, message, { editMessageId });
496
+ }
415
497
  async function runAgent({ bot, chatId, link, prompt }) {
416
498
  const lang = languageFor(link);
417
499
  const state = getState(link.user_id);
@@ -424,33 +506,49 @@ async function runAgent({ bot, chatId, link, prompt }) {
424
506
  await showProjectMenu({ bot, chatId, link });
425
507
  return;
426
508
  }
509
+ const provider = resolveTelegramProvider(state);
510
+ const model = resolveTelegramModel(state);
427
511
  if (state.progressMode !== 'final') {
428
512
  await send(bot, chatId, t(lang, 'control.agentStarted', {
429
- provider: state.selectedProvider,
513
+ provider,
430
514
  project: state.selectedProjectName || state.selectedProjectPath,
431
515
  }));
432
516
  }
433
- const response = await localApi(link.user_id, '/api/agent', {
434
- method: 'POST',
435
- body: {
436
- projectPath: state.selectedProjectPath,
437
- provider: state.selectedProvider,
438
- model: state.selectedModel || undefined,
439
- message: prompt,
440
- cleanup: false,
441
- stream: false,
442
- },
517
+ const result = await runTelegramTool({
518
+ userId: link.user_id,
519
+ action: 'run_agent',
520
+ retries: 0,
521
+ execute: () => localApi(link.user_id, '/api/agent', {
522
+ method: 'POST',
523
+ body: {
524
+ projectPath: state.selectedProjectPath,
525
+ provider,
526
+ model: model || undefined,
527
+ message: buildTelegramAgentPrompt(prompt, state),
528
+ cleanup: false,
529
+ stream: false,
530
+ },
531
+ }),
443
532
  });
533
+ if (!result.ok) {
534
+ await sendToolFailure({ bot, chatId, link, result });
535
+ return;
536
+ }
537
+ const response = result.data;
444
538
  const assistantText = extractAssistantText(response);
445
539
  const statusLine = response.success === false
446
540
  ? t(lang, 'control.agentFailed', { error: response.error || 'provider returned no answer' })
447
- : t(lang, 'control.agentDone', { provider: state.selectedProvider, session: response.sessionId ? ` (${response.sessionId})` : '' });
541
+ : t(lang, 'control.agentDone', { provider, session: response.sessionId ? ` (${response.sessionId})` : '' });
448
542
  await send(bot, chatId, `${statusLine}\n\n${assistantText || response.error || t(lang, 'control.noAssistantText')}`);
449
543
  }
450
544
  export async function runWorkflow({ bot, chatId, link, input }) {
451
545
  const lang = languageFor(link);
452
546
  const state = getState(link.user_id);
453
547
  const workflowId = state.selectedWorkflowId;
548
+ if (!state.remoteControlEnabled) {
549
+ await send(bot, chatId, t(lang, 'control.disabled'));
550
+ return;
551
+ }
454
552
  if (!workflowId) {
455
553
  await send(bot, chatId, t(lang, 'control.selectWorkflowFirst'));
456
554
  await showWorkflowMenu({ bot, chatId, link });
@@ -461,26 +559,173 @@ export async function runWorkflow({ bot, chatId, link, input }) {
461
559
  await showProjectMenu({ bot, chatId, link });
462
560
  return;
463
561
  }
464
- const run = await localApi(link.user_id, `/api/orchestration/workflows/${workflowId}/runs`, {
465
- method: 'POST',
466
- body: {
467
- input,
468
- metadata: {
469
- projectId: state.selectedProjectName,
470
- projectName: state.selectedProjectName,
471
- projectPath: state.selectedProjectPath,
472
- workspaceTarget: 'selected_project',
473
- telegram: true,
474
- preferredProvider: state.selectedProvider,
475
- preferredModel: state.selectedModel,
562
+ const provider = resolveTelegramProvider(state);
563
+ const model = resolveTelegramModel(state);
564
+ const result = await runTelegramTool({
565
+ userId: link.user_id,
566
+ action: 'run_workflow',
567
+ execute: () => localApi(link.user_id, `/api/orchestration/workflows/${workflowId}/runs`, {
568
+ method: 'POST',
569
+ body: {
570
+ input,
571
+ metadata: {
572
+ projectId: state.selectedProjectName,
573
+ projectName: state.selectedProjectName,
574
+ projectPath: state.selectedProjectPath,
575
+ workspaceTarget: 'selected_project',
576
+ telegram: true,
577
+ preferredProvider: provider,
578
+ preferredModel: model,
579
+ },
476
580
  },
477
- },
581
+ }),
478
582
  });
583
+ if (!result.ok) {
584
+ await sendToolFailure({ bot, chatId, link, result });
585
+ return;
586
+ }
587
+ const run = result.data;
479
588
  await send(bot, chatId, t(lang, 'control.workflowStarted', { runId: run.id, workflowId }));
480
589
  monitorWorkflowRun({ bot, chatId, link, runId: run.id }).catch((error) => {
481
590
  console.warn('[telegram-control] workflow monitor failed:', error?.message || error);
482
591
  });
483
592
  }
593
+ async function cancelRun({ bot, chatId, link, runId, editMessageId, confirmed = false }) {
594
+ const lang = languageFor(link);
595
+ if (!runId) {
596
+ await send(bot, chatId, t(lang, 'control.cancelUsage'), { editMessageId });
597
+ return;
598
+ }
599
+ const state = getState(link.user_id);
600
+ if (state.remoteControlEnabled === false) {
601
+ await send(bot, chatId, t(lang, 'control.disabled'), { editMessageId });
602
+ return;
603
+ }
604
+ if (state.confirmationPolicy === 'strict' && !confirmed) {
605
+ await requestConfirmation({
606
+ bot,
607
+ chatId,
608
+ link,
609
+ action: 'run_cancel',
610
+ payload: { runId },
611
+ editMessageId,
612
+ });
613
+ return;
614
+ }
615
+ const result = await runTelegramTool({
616
+ userId: link.user_id,
617
+ action: 'run_cancel',
618
+ execute: () => localApi(link.user_id, `/api/orchestration/workflows/runs/${encodeURIComponent(runId)}/cancel`, {
619
+ method: 'POST',
620
+ }),
621
+ });
622
+ if (!result.ok) {
623
+ await sendToolFailure({ bot, chatId, link, result, editMessageId });
624
+ return;
625
+ }
626
+ const run = result.data;
627
+ await send(bot, chatId, t(lang, 'control.runStatus', { runId: run.id, status: run.status }), { editMessageId });
628
+ }
629
+ async function executeConfirmedAction({ bot, chatId, link, confirmation, editMessageId }) {
630
+ if (confirmation.action === 'install_provider') {
631
+ await startCliInstall({
632
+ bot,
633
+ chatId,
634
+ link,
635
+ provider: confirmation.payload?.provider,
636
+ editMessageId,
637
+ confirmed: true,
638
+ });
639
+ return true;
640
+ }
641
+ if (confirmation.action === 'run_cancel') {
642
+ await cancelRun({
643
+ bot,
644
+ chatId,
645
+ link,
646
+ runId: confirmation.payload?.runId,
647
+ editMessageId,
648
+ confirmed: true,
649
+ });
650
+ return true;
651
+ }
652
+ return false;
653
+ }
654
+ async function findProjectByQuery(query) {
655
+ const projects = await listProjects();
656
+ const needle = String(query || '').trim().toLocaleLowerCase('tr');
657
+ if (!needle)
658
+ return null;
659
+ return projects.find((project) => {
660
+ const candidates = [project.name, project.displayName, project.fullPath, project.path]
661
+ .filter(Boolean)
662
+ .map((value) => String(value).toLocaleLowerCase('tr'));
663
+ return candidates.some((candidate) => candidate === needle || candidate.includes(needle));
664
+ }) || null;
665
+ }
666
+ async function handleRoutedIntent({ bot, chatId, link, text }) {
667
+ const state = getState(link.user_id);
668
+ const intent = classifyTelegramIntent(text, state);
669
+ if (!intent)
670
+ return false;
671
+ if (intent.action === 'projects') {
672
+ await showProjectMenu({ bot, chatId, link });
673
+ return true;
674
+ }
675
+ if (intent.action === 'project_select_query') {
676
+ const project = await findProjectByQuery(intent.query);
677
+ const lang = languageFor(link);
678
+ if (!project) {
679
+ await send(bot, chatId, t(lang, 'control.projectNotFound', { query: intent.query }));
680
+ await showProjectMenu({ bot, chatId, link });
681
+ return true;
682
+ }
683
+ updateTelegramControlState(link.user_id, {
684
+ selectedProjectName: project.name,
685
+ selectedProjectPath: project.fullPath || project.path,
686
+ });
687
+ await showMainMenu({
688
+ bot,
689
+ chatId,
690
+ link,
691
+ notice: t(lang, 'control.projectSelected', {
692
+ project: project.displayName || project.name,
693
+ path: project.fullPath || project.path,
694
+ }),
695
+ });
696
+ return true;
697
+ }
698
+ if (intent.action === 'provider_menu') {
699
+ await showProviderMenu({ bot, chatId, link });
700
+ return true;
701
+ }
702
+ if (intent.action === 'provider_select') {
703
+ updateTelegramControlState(link.user_id, { selectedProvider: intent.provider, selectedModel: null });
704
+ await showModelMenu({ bot, chatId, link });
705
+ return true;
706
+ }
707
+ if (intent.action === 'model_menu') {
708
+ await showModelMenu({ bot, chatId, link });
709
+ return true;
710
+ }
711
+ if (intent.action === 'runs') {
712
+ await showRuns({ bot, chatId, link });
713
+ return true;
714
+ }
715
+ if (intent.action === 'approvals') {
716
+ await showApprovalQueue({ bot, chatId, link });
717
+ return true;
718
+ }
719
+ if (intent.action === 'workflows') {
720
+ await showWorkflowMenu({ bot, chatId, link });
721
+ return true;
722
+ }
723
+ if (intent.action === 'new_chat') {
724
+ await startNewChat({ bot, chatId, link });
725
+ return true;
726
+ }
727
+ return false;
728
+ }
484
729
  async function fetchRun(userId, runId) {
485
730
  return localApi(userId, `/api/orchestration/workflows/runs/${runId}`);
486
731
  }
@@ -544,18 +789,47 @@ async function monitorWorkflowRun({ bot, chatId, link, runId }) {
544
789
  runMonitors.delete(runId);
545
790
  }
546
791
  }
547
- export async function startCliInstall({ bot, chatId, link, provider }) {
792
+ export async function startCliInstall({ bot, chatId, link, provider, editMessageId, confirmed = false }) {
548
793
  const lang = languageFor(link);
549
- const data = await localApi(link.user_id, `/api/providers/${provider}/install`, { method: 'POST' });
794
+ if (!PROVIDERS.includes(provider)) {
795
+ await send(bot, chatId, t(lang, 'control.providerAuthFallback'), { editMessageId });
796
+ return;
797
+ }
798
+ const state = getState(link.user_id);
799
+ if (state.remoteControlEnabled === false) {
800
+ await send(bot, chatId, t(lang, 'control.disabled'), { editMessageId });
801
+ return;
802
+ }
803
+ if (state.confirmationPolicy === 'strict' && !confirmed) {
804
+ await requestConfirmation({
805
+ bot,
806
+ chatId,
807
+ link,
808
+ action: 'install_provider',
809
+ payload: { provider },
810
+ editMessageId,
811
+ });
812
+ return;
813
+ }
814
+ const result = await runTelegramTool({
815
+ userId: link.user_id,
816
+ action: 'install_provider',
817
+ execute: () => localApi(link.user_id, `/api/providers/${provider}/install`, { method: 'POST' }),
818
+ });
819
+ if (!result.ok) {
820
+ await sendToolFailure({ bot, chatId, link, result, editMessageId });
821
+ return;
822
+ }
823
+ const data = result.data;
550
824
  if (data?.manual) {
551
- await send(bot, chatId, t(lang, 'control.manualInstall', { provider, manual: data.manual }));
825
+ await send(bot, chatId, t(lang, 'control.manualInstall', { provider, manual: data.manual }), { editMessageId });
552
826
  return;
553
827
  }
554
828
  await send(bot, chatId, t(lang, 'control.installStarted', {
555
829
  provider,
556
830
  jobId: data.jobId,
557
831
  command: data.installCmd || 'internal installer',
558
- }));
832
+ }), { editMessageId });
559
833
  }
560
834
  async function showInstallMenu({ bot, chatId, link, editMessageId }) {
561
835
  const lang = languageFor(link);
@@ -731,15 +1005,12 @@ async function handleCommand({ bot, chatId, link, text }) {
731
1005
  await send(bot, chatId, t(lang, 'control.cancelUsage'));
732
1006
  return true;
733
1007
  }
734
- const run = await localApi(link.user_id, `/api/orchestration/workflows/runs/${encodeURIComponent(argText)}/cancel`, {
735
- method: 'POST',
736
- });
737
- await send(bot, chatId, t(lang, 'control.runStatus', { runId: run.id, status: run.status }));
1008
+ await cancelRun({ bot, chatId, link, runId: argText });
738
1009
  return true;
739
1010
  }
740
1011
  return false;
741
1012
  }
742
- export async function handleTelegramControlMessage({ bot, msg, link }) {
1013
+ async function handleTelegramControlMessageInternal({ bot, msg, link }) {
743
1014
  const chatId = msg.chat.id;
744
1015
  const text = String(msg.text || '').trim();
745
1016
  if (!text)
@@ -756,7 +1027,11 @@ export async function handleTelegramControlMessage({ bot, msg, link }) {
756
1027
  return true;
757
1028
  if (await handleCommand({ bot, chatId, link, text }))
758
1029
  return true;
1030
+ if (await handleRoutedIntent({ bot, chatId, link, text }))
1031
+ return true;
759
1032
  const state = getState(link.user_id);
1033
+ if (state.routerEnabled === false)
1034
+ return false;
760
1035
  if (!state.remoteControlEnabled) {
761
1036
  await send(bot, chatId, t(languageFor(link), 'control.disabled'));
762
1037
  return true;
@@ -764,6 +1039,21 @@ export async function handleTelegramControlMessage({ bot, msg, link }) {
764
1039
  await runAgent({ bot, chatId, link, prompt: text });
765
1040
  return true;
766
1041
  }
1042
+ export async function handleTelegramControlMessage(args) {
1043
+ const chatId = args?.msg?.chat?.id;
1044
+ if (!chatId)
1045
+ return false;
1046
+ return enqueueTelegramJob(chatId, async () => {
1047
+ try {
1048
+ return await handleTelegramControlMessageInternal(args);
1049
+ }
1050
+ catch (error) {
1051
+ console.error('[telegram-control] message handler failed:', error);
1052
+ await send(args.bot, chatId, t(languageFor(args.link), 'error.generic')).catch(() => { });
1053
+ return true;
1054
+ }
1055
+ });
1056
+ }
767
1057
  async function showRunDetail({ bot, chatId, link, runId, editMessageId }) {
768
1058
  const lang = languageFor(link);
769
1059
  const run = await fetchRun(link.user_id, runId);
@@ -777,7 +1067,7 @@ async function showRunDetail({ bot, chatId, link, runId, editMessageId }) {
777
1067
  },
778
1068
  });
779
1069
  }
780
- export async function handleTelegramControlCallback({ bot, query, link }) {
1070
+ async function handleTelegramControlCallbackInternal({ bot, query, link }) {
781
1071
  const chatId = query.message?.chat?.id;
782
1072
  if (!chatId)
783
1073
  return;
@@ -791,6 +1081,33 @@ export async function handleTelegramControlCallback({ bot, query, link }) {
791
1081
  }
792
1082
  await bot.answerCallbackQuery(query.id).catch(() => { });
793
1083
  const { action, payload } = entry;
1084
+ if (action === 'confirm_action') {
1085
+ forgetAction(entry.id);
1086
+ const lang = languageFor(link);
1087
+ const result = consumeTelegramConfirmation(link.user_id, payload.id);
1088
+ if (!result.ok) {
1089
+ await send(bot, chatId, t(lang, result.reason === 'expired'
1090
+ ? 'control.confirmationExpired'
1091
+ : 'control.confirmationMissing'), { editMessageId });
1092
+ return;
1093
+ }
1094
+ if (await executeConfirmedAction({
1095
+ bot,
1096
+ chatId,
1097
+ link,
1098
+ confirmation: result.confirmation,
1099
+ editMessageId,
1100
+ }))
1101
+ return;
1102
+ await send(bot, chatId, t(lang, 'error.generic'), { editMessageId });
1103
+ return;
1104
+ }
1105
+ if (action === 'cancel_confirmation') {
1106
+ forgetAction(entry.id);
1107
+ clearTelegramConfirmation(link.user_id, payload.id);
1108
+ await send(bot, chatId, t(languageFor(link), 'control.confirmationCanceled'), { editMessageId });
1109
+ return;
1110
+ }
794
1111
  if (action === 'menu')
795
1112
  return showMainMenu({ bot, chatId, link, editMessageId });
796
1113
  if (action === 'projects')
@@ -873,20 +1190,26 @@ export async function handleTelegramControlCallback({ bot, query, link }) {
873
1190
  if (action === 'run_detail')
874
1191
  return showRunDetail({ bot, chatId, link, runId: payload.runId, editMessageId });
875
1192
  if (action === 'run_cancel') {
876
- const run = await localApi(link.user_id, `/api/orchestration/workflows/runs/${encodeURIComponent(payload.runId)}/cancel`, {
877
- method: 'POST',
878
- });
879
- await send(bot, chatId, t(languageFor(link), 'control.runStatus', { runId: run.id, status: run.status }), { editMessageId });
1193
+ await cancelRun({ bot, chatId, link, runId: payload.runId, editMessageId });
880
1194
  return;
881
1195
  }
882
1196
  if (action === 'approval_decide') {
883
- const result = await localApi(link.user_id, `/api/orchestration/workflows/approvals/${encodeURIComponent(payload.approvalId)}`, {
884
- method: 'POST',
885
- body: {
886
- allow: payload.allow === true,
887
- source: 'telegram',
888
- },
1197
+ const toolResult = await runTelegramTool({
1198
+ userId: link.user_id,
1199
+ action: 'approval_decide',
1200
+ execute: () => localApi(link.user_id, `/api/orchestration/workflows/approvals/${encodeURIComponent(payload.approvalId)}`, {
1201
+ method: 'POST',
1202
+ body: {
1203
+ allow: payload.allow === true,
1204
+ source: 'telegram',
1205
+ },
1206
+ }),
889
1207
  });
1208
+ if (!toolResult.ok) {
1209
+ await sendToolFailure({ bot, chatId, link, result: toolResult, editMessageId });
1210
+ return;
1211
+ }
1212
+ const result = toolResult.data;
890
1213
  const lang = languageFor(link);
891
1214
  await send(bot, chatId, t(lang, 'control.approvalDecided', {
892
1215
  approvalId: payload.approvalId,
@@ -896,7 +1219,7 @@ export async function handleTelegramControlCallback({ bot, query, link }) {
896
1219
  return showApprovalQueue({ bot, chatId, link });
897
1220
  }
898
1221
  if (action === 'install_provider')
899
- return startCliInstall({ bot, chatId, link, provider: payload.provider });
1222
+ return startCliInstall({ bot, chatId, link, provider: payload.provider, editMessageId });
900
1223
  if (action === 'auth_provider') {
901
1224
  await send(bot, chatId, `${payload.provider} login:\n${AUTH_HELP[payload.provider] || t(languageFor(link), 'control.providerAuthFallback')}`, { editMessageId });
902
1225
  return;
@@ -921,4 +1244,20 @@ export async function handleTelegramControlCallback({ bot, query, link }) {
921
1244
  });
922
1245
  }
923
1246
  }
1247
+ export async function handleTelegramControlCallback(args) {
1248
+ const chatId = args?.query?.message?.chat?.id;
1249
+ if (!chatId)
1250
+ return;
1251
+ return enqueueTelegramJob(chatId, async () => {
1252
+ try {
1253
+ await handleTelegramControlCallbackInternal(args);
1254
+ }
1255
+ catch (error) {
1256
+ console.error('[telegram-control] callback failed:', error);
1257
+ const lang = languageFor(args.link);
1258
+ await args.bot?.answerCallbackQuery(args.query?.id, { text: t(lang, 'error.generic') }).catch(() => { });
1259
+ await send(args.bot, chatId, t(lang, 'error.generic')).catch(() => { });
1260
+ }
1261
+ });
1262
+ }
924
1263
  //# sourceMappingURL=control-center.js.map