@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.
@@ -4,11 +4,27 @@ import { apiKeysDb, telegramLinksDb } from '../../database/db.js';
4
4
  import { getProjects } from '../../projects.js';
5
5
  import { getStaticProviderModels } from '../model-registry.js';
6
6
 
7
+ import {
8
+ TELEGRAM_CONTROL_SCOPES,
9
+ TELEGRAM_PROVIDERS,
10
+ buildTelegramAgentPrompt,
11
+ classifyTelegramIntent,
12
+ clearTelegramConfirmation,
13
+ consumeTelegramConfirmation,
14
+ createTelegramConfirmation,
15
+ enqueueTelegramJob,
16
+ resolveTelegramModel,
17
+ resolveTelegramProvider,
18
+ retryWithBackoff,
19
+ runTelegramTool,
20
+ splitTelegramText,
21
+ } from './telegram-gateway.js';
7
22
  import { SUPPORTED_LANGUAGES, t } from './translations.js';
8
23
 
9
- const PROVIDERS = ['claude', 'cursor', 'codex', 'gemini', 'qwen', 'opencode'];
24
+ const PROVIDERS = TELEGRAM_PROVIDERS;
10
25
  const TERMINAL_RUN_STATES = new Set(['completed', 'failed', 'canceled']);
11
26
  const CALLBACK_TTL_MS = 10 * 60 * 1000;
27
+ const MAX_CALLBACK_ACTIONS = 1000;
12
28
  const MAX_TELEGRAM_TEXT = 3600;
13
29
  const callbackActions = new Map();
14
30
  const runMonitors = new Map();
@@ -94,7 +110,23 @@ export function updateTelegramControlState(userId, patch) {
94
110
  return telegramLinksDb.updateControlState(userId, patch);
95
111
  }
96
112
 
113
+ function pruneCallbackActions() {
114
+ const now = Date.now();
115
+ for (const [id, entry] of callbackActions.entries()) {
116
+ if (entry.expiresAt < now) {
117
+ callbackActions.delete(id);
118
+ }
119
+ }
120
+
121
+ while (callbackActions.size > MAX_CALLBACK_ACTIONS) {
122
+ const oldestId = callbackActions.keys().next().value;
123
+ if (!oldestId) break;
124
+ callbackActions.delete(oldestId);
125
+ }
126
+ }
127
+
97
128
  function registerAction(action, payload = {}) {
129
+ pruneCallbackActions();
98
130
  const id = crypto.randomBytes(8).toString('hex');
99
131
  callbackActions.set(id, {
100
132
  action,
@@ -113,7 +145,11 @@ function readAction(data) {
113
145
  callbackActions.delete(id);
114
146
  return null;
115
147
  }
116
- return entry;
148
+ return { id, ...entry };
149
+ }
150
+
151
+ function forgetAction(id) {
152
+ if (id) callbackActions.delete(id);
117
153
  }
118
154
 
119
155
  function button(text, action, payload = {}) {
@@ -133,27 +169,44 @@ async function send(bot, chatId, text, options = {}) {
133
169
  disable_web_page_preview: true,
134
170
  ...telegramOptions,
135
171
  };
136
- const messageText = truncate(text);
172
+ const chunks = splitTelegramText(text, MAX_TELEGRAM_TEXT);
173
+ const [firstChunk = ''] = chunks;
174
+ let startIndex = 0;
137
175
  if (editMessageId && typeof bot.editMessageText === 'function') {
138
176
  try {
139
- return await bot.editMessageText(messageText, {
177
+ const edited = await bot.editMessageText(firstChunk, {
140
178
  chat_id: chatId,
141
179
  message_id: editMessageId,
142
180
  ...extra,
143
181
  });
182
+ startIndex = 1;
183
+ for (const chunk of chunks.slice(startIndex)) {
184
+ await send(bot, chatId, chunk, telegramOptions);
185
+ }
186
+ return edited;
144
187
  } catch (err) {
145
188
  const description = err?.response?.body?.description || err?.message || '';
146
- if (/message is not modified/i.test(description)) return null;
189
+ if (/message is not modified/i.test(description)) {
190
+ startIndex = 1;
191
+ for (const chunk of chunks.slice(startIndex)) {
192
+ await send(bot, chatId, chunk, telegramOptions);
193
+ }
194
+ return null;
195
+ }
147
196
  console.warn('[telegram-control] editMessageText failed:', description || err);
148
197
  }
149
198
  }
150
- try {
151
- return await bot.sendMessage(chatId, messageText, extra);
152
- } catch {
153
- const fallback = { ...extra };
154
- delete fallback.parse_mode;
155
- return bot.sendMessage(chatId, messageText, fallback);
199
+ let result = null;
200
+ for (const chunk of chunks.slice(startIndex)) {
201
+ try {
202
+ result = await bot.sendMessage(chatId, chunk, extra);
203
+ } catch {
204
+ const fallback = { ...extra };
205
+ delete fallback.parse_mode;
206
+ result = await bot.sendMessage(chatId, chunk, fallback);
207
+ }
156
208
  }
209
+ return result;
157
210
  }
158
211
 
159
212
  function localApiBase() {
@@ -165,19 +218,35 @@ function getOrCreateTelegramApiKey(userId) {
165
218
  const existing = apiKeysDb
166
219
  .getApiKeys(userId)
167
220
  .find((key) => key.key_name === 'Telegram Control' && Boolean(key.is_active));
168
- if (existing?.api_key) return existing.api_key;
169
- return apiKeysDb.createApiKey(userId, 'Telegram Control').apiKey;
221
+ if (existing?.api_key) {
222
+ const existingScopes = apiKeysDb.normalizeScopes(existing.scopes || []);
223
+ const nextScopes = apiKeysDb.normalizeScopes([...existingScopes, ...TELEGRAM_CONTROL_SCOPES]);
224
+ const hasAllScopes = TELEGRAM_CONTROL_SCOPES.every((scope) => existingScopes.includes(scope));
225
+ if (!hasAllScopes || nextScopes.length !== existingScopes.length) {
226
+ apiKeysDb.updateApiKeyScopes(userId, existing.id, nextScopes);
227
+ }
228
+ return existing.api_key;
229
+ }
230
+ return apiKeysDb.createApiKey(userId, 'Telegram Control', TELEGRAM_CONTROL_SCOPES).apiKey;
170
231
  }
171
232
 
172
233
  async function localApi(userId, path, { method = 'GET', body } = {}) {
173
234
  const apiKey = getOrCreateTelegramApiKey(userId);
174
- const response = await fetch(`${localApiBase()}${path}`, {
175
- method,
176
- headers: {
177
- 'Content-Type': 'application/json',
178
- 'X-API-Key': apiKey,
179
- },
180
- body: body === undefined ? undefined : JSON.stringify(body),
235
+ const response = await retryWithBackoff(async () => {
236
+ const res = await fetch(`${localApiBase()}${path}`, {
237
+ method,
238
+ headers: {
239
+ 'Content-Type': 'application/json',
240
+ 'X-API-Key': apiKey,
241
+ },
242
+ body: body === undefined ? undefined : JSON.stringify(body),
243
+ });
244
+ if ([408, 429, 500, 502, 503, 504].includes(res.status)) {
245
+ const error = new Error(`HTTP ${res.status}`);
246
+ error.status = res.status;
247
+ throw error;
248
+ }
249
+ return res;
181
250
  });
182
251
  const data = await response.json().catch(() => ({}));
183
252
  if (!response.ok) {
@@ -470,6 +539,39 @@ function extractAssistantText(response) {
470
539
  return chunks.join('\n\n').trim();
471
540
  }
472
541
 
542
+ function confirmationLabel(lang, action, payload = {}) {
543
+ if (action === 'install_provider') {
544
+ return t(lang, 'control.confirmInstall', { provider: payload.provider || '' });
545
+ }
546
+ if (action === 'run_cancel') {
547
+ return t(lang, 'control.confirmCancelRun', { runId: payload.runId || '' });
548
+ }
549
+ return t(lang, 'control.confirmationRequired');
550
+ }
551
+
552
+ async function requestConfirmation({ bot, chatId, link, action, payload = {}, editMessageId }) {
553
+ const lang = languageFor(link);
554
+ const pending = createTelegramConfirmation(link.user_id, action, payload);
555
+ await send(bot, chatId, confirmationLabel(lang, action, payload), {
556
+ editMessageId,
557
+ reply_markup: {
558
+ inline_keyboard: [[
559
+ button(t(lang, 'control.button.confirm'), 'confirm_action', { id: pending.id }),
560
+ button(t(lang, 'control.button.cancel'), 'cancel_confirmation', { id: pending.id }),
561
+ ]],
562
+ },
563
+ });
564
+ return { ok: false, requiresConfirmation: true };
565
+ }
566
+
567
+ async function sendToolFailure({ bot, chatId, link, result, editMessageId }) {
568
+ const lang = languageFor(link);
569
+ const message = result?.message === 'REMOTE_CONTROL_DISABLED'
570
+ ? t(lang, 'control.disabled')
571
+ : (result?.message || t(lang, 'error.generic'));
572
+ await send(bot, chatId, message, { editMessageId });
573
+ }
574
+
473
575
  async function runAgent({ bot, chatId, link, prompt }) {
474
576
  const lang = languageFor(link);
475
577
  const state = getState(link.user_id);
@@ -483,28 +585,41 @@ async function runAgent({ bot, chatId, link, prompt }) {
483
585
  return;
484
586
  }
485
587
 
588
+ const provider = resolveTelegramProvider(state);
589
+ const model = resolveTelegramModel(state);
486
590
  if (state.progressMode !== 'final') {
487
591
  await send(bot, chatId, t(lang, 'control.agentStarted', {
488
- provider: state.selectedProvider,
592
+ provider,
489
593
  project: state.selectedProjectName || state.selectedProjectPath,
490
594
  }));
491
595
  }
492
596
 
493
- const response = await localApi(link.user_id, '/api/agent', {
494
- method: 'POST',
495
- body: {
496
- projectPath: state.selectedProjectPath,
497
- provider: state.selectedProvider,
498
- model: state.selectedModel || undefined,
499
- message: prompt,
500
- cleanup: false,
501
- stream: false,
502
- },
597
+ const result = await runTelegramTool({
598
+ userId: link.user_id,
599
+ action: 'run_agent',
600
+ retries: 0,
601
+ execute: () => localApi(link.user_id, '/api/agent', {
602
+ method: 'POST',
603
+ body: {
604
+ projectPath: state.selectedProjectPath,
605
+ provider,
606
+ model: model || undefined,
607
+ message: buildTelegramAgentPrompt(prompt, state),
608
+ cleanup: false,
609
+ stream: false,
610
+ },
611
+ }),
503
612
  });
613
+ if (!result.ok) {
614
+ await sendToolFailure({ bot, chatId, link, result });
615
+ return;
616
+ }
617
+
618
+ const response = result.data;
504
619
  const assistantText = extractAssistantText(response);
505
620
  const statusLine = response.success === false
506
621
  ? t(lang, 'control.agentFailed', { error: response.error || 'provider returned no answer' })
507
- : t(lang, 'control.agentDone', { provider: state.selectedProvider, session: response.sessionId ? ` (${response.sessionId})` : '' });
622
+ : t(lang, 'control.agentDone', { provider, session: response.sessionId ? ` (${response.sessionId})` : '' });
508
623
  await send(bot, chatId, `${statusLine}\n\n${assistantText || response.error || t(lang, 'control.noAssistantText')}`);
509
624
  }
510
625
 
@@ -512,6 +627,10 @@ export async function runWorkflow({ bot, chatId, link, input }) {
512
627
  const lang = languageFor(link);
513
628
  const state = getState(link.user_id);
514
629
  const workflowId = state.selectedWorkflowId;
630
+ if (!state.remoteControlEnabled) {
631
+ await send(bot, chatId, t(lang, 'control.disabled'));
632
+ return;
633
+ }
515
634
  if (!workflowId) {
516
635
  await send(bot, chatId, t(lang, 'control.selectWorkflowFirst'));
517
636
  await showWorkflowMenu({ bot, chatId, link });
@@ -523,27 +642,178 @@ export async function runWorkflow({ bot, chatId, link, input }) {
523
642
  return;
524
643
  }
525
644
 
526
- const run = await localApi(link.user_id, `/api/orchestration/workflows/${workflowId}/runs`, {
527
- method: 'POST',
528
- body: {
529
- input,
530
- metadata: {
531
- projectId: state.selectedProjectName,
532
- projectName: state.selectedProjectName,
533
- projectPath: state.selectedProjectPath,
534
- workspaceTarget: 'selected_project',
535
- telegram: true,
536
- preferredProvider: state.selectedProvider,
537
- preferredModel: state.selectedModel,
645
+ const provider = resolveTelegramProvider(state);
646
+ const model = resolveTelegramModel(state);
647
+ const result = await runTelegramTool({
648
+ userId: link.user_id,
649
+ action: 'run_workflow',
650
+ execute: () => localApi(link.user_id, `/api/orchestration/workflows/${workflowId}/runs`, {
651
+ method: 'POST',
652
+ body: {
653
+ input,
654
+ metadata: {
655
+ projectId: state.selectedProjectName,
656
+ projectName: state.selectedProjectName,
657
+ projectPath: state.selectedProjectPath,
658
+ workspaceTarget: 'selected_project',
659
+ telegram: true,
660
+ preferredProvider: provider,
661
+ preferredModel: model,
662
+ },
538
663
  },
539
- },
664
+ }),
540
665
  });
666
+ if (!result.ok) {
667
+ await sendToolFailure({ bot, chatId, link, result });
668
+ return;
669
+ }
670
+ const run = result.data;
541
671
  await send(bot, chatId, t(lang, 'control.workflowStarted', { runId: run.id, workflowId }));
542
672
  monitorWorkflowRun({ bot, chatId, link, runId: run.id }).catch((error) => {
543
673
  console.warn('[telegram-control] workflow monitor failed:', error?.message || error);
544
674
  });
545
675
  }
546
676
 
677
+ async function cancelRun({ bot, chatId, link, runId, editMessageId, confirmed = false }) {
678
+ const lang = languageFor(link);
679
+ if (!runId) {
680
+ await send(bot, chatId, t(lang, 'control.cancelUsage'), { editMessageId });
681
+ return;
682
+ }
683
+ const state = getState(link.user_id);
684
+ if (state.remoteControlEnabled === false) {
685
+ await send(bot, chatId, t(lang, 'control.disabled'), { editMessageId });
686
+ return;
687
+ }
688
+ if (state.confirmationPolicy === 'strict' && !confirmed) {
689
+ await requestConfirmation({
690
+ bot,
691
+ chatId,
692
+ link,
693
+ action: 'run_cancel',
694
+ payload: { runId },
695
+ editMessageId,
696
+ });
697
+ return;
698
+ }
699
+
700
+ const result = await runTelegramTool({
701
+ userId: link.user_id,
702
+ action: 'run_cancel',
703
+ execute: () => localApi(link.user_id, `/api/orchestration/workflows/runs/${encodeURIComponent(runId)}/cancel`, {
704
+ method: 'POST',
705
+ }),
706
+ });
707
+ if (!result.ok) {
708
+ await sendToolFailure({ bot, chatId, link, result, editMessageId });
709
+ return;
710
+ }
711
+ const run = result.data;
712
+ await send(bot, chatId, t(lang, 'control.runStatus', { runId: run.id, status: run.status }), { editMessageId });
713
+ }
714
+
715
+ async function executeConfirmedAction({ bot, chatId, link, confirmation, editMessageId }) {
716
+ if (confirmation.action === 'install_provider') {
717
+ await startCliInstall({
718
+ bot,
719
+ chatId,
720
+ link,
721
+ provider: confirmation.payload?.provider,
722
+ editMessageId,
723
+ confirmed: true,
724
+ });
725
+ return true;
726
+ }
727
+ if (confirmation.action === 'run_cancel') {
728
+ await cancelRun({
729
+ bot,
730
+ chatId,
731
+ link,
732
+ runId: confirmation.payload?.runId,
733
+ editMessageId,
734
+ confirmed: true,
735
+ });
736
+ return true;
737
+ }
738
+ return false;
739
+ }
740
+
741
+ async function findProjectByQuery(query) {
742
+ const projects = await listProjects();
743
+ const needle = String(query || '').trim().toLocaleLowerCase('tr');
744
+ if (!needle) return null;
745
+ return projects.find((project) => {
746
+ const candidates = [project.name, project.displayName, project.fullPath, project.path]
747
+ .filter(Boolean)
748
+ .map((value) => String(value).toLocaleLowerCase('tr'));
749
+ return candidates.some((candidate) => candidate === needle || candidate.includes(needle));
750
+ }) || null;
751
+ }
752
+
753
+ async function handleRoutedIntent({ bot, chatId, link, text }) {
754
+ const state = getState(link.user_id);
755
+ const intent = classifyTelegramIntent(text, state);
756
+ if (!intent) return false;
757
+
758
+ if (intent.action === 'projects') {
759
+ await showProjectMenu({ bot, chatId, link });
760
+ return true;
761
+ }
762
+ if (intent.action === 'project_select_query') {
763
+ const project = await findProjectByQuery(intent.query);
764
+ const lang = languageFor(link);
765
+ if (!project) {
766
+ await send(bot, chatId, t(lang, 'control.projectNotFound', { query: intent.query }));
767
+ await showProjectMenu({ bot, chatId, link });
768
+ return true;
769
+ }
770
+ updateTelegramControlState(link.user_id, {
771
+ selectedProjectName: project.name,
772
+ selectedProjectPath: project.fullPath || project.path,
773
+ });
774
+ await showMainMenu({
775
+ bot,
776
+ chatId,
777
+ link,
778
+ notice: t(lang, 'control.projectSelected', {
779
+ project: project.displayName || project.name,
780
+ path: project.fullPath || project.path,
781
+ }),
782
+ });
783
+ return true;
784
+ }
785
+ if (intent.action === 'provider_menu') {
786
+ await showProviderMenu({ bot, chatId, link });
787
+ return true;
788
+ }
789
+ if (intent.action === 'provider_select') {
790
+ updateTelegramControlState(link.user_id, { selectedProvider: intent.provider, selectedModel: null });
791
+ await showModelMenu({ bot, chatId, link });
792
+ return true;
793
+ }
794
+ if (intent.action === 'model_menu') {
795
+ await showModelMenu({ bot, chatId, link });
796
+ return true;
797
+ }
798
+ if (intent.action === 'runs') {
799
+ await showRuns({ bot, chatId, link });
800
+ return true;
801
+ }
802
+ if (intent.action === 'approvals') {
803
+ await showApprovalQueue({ bot, chatId, link });
804
+ return true;
805
+ }
806
+ if (intent.action === 'workflows') {
807
+ await showWorkflowMenu({ bot, chatId, link });
808
+ return true;
809
+ }
810
+ if (intent.action === 'new_chat') {
811
+ await startNewChat({ bot, chatId, link });
812
+ return true;
813
+ }
814
+ return false;
815
+ }
816
+
547
817
  async function fetchRun(userId, runId) {
548
818
  return localApi(userId, `/api/orchestration/workflows/runs/${runId}`);
549
819
  }
@@ -608,18 +878,49 @@ async function monitorWorkflowRun({ bot, chatId, link, runId }) {
608
878
  }
609
879
  }
610
880
 
611
- export async function startCliInstall({ bot, chatId, link, provider }) {
881
+ export async function startCliInstall({ bot, chatId, link, provider, editMessageId, confirmed = false }) {
612
882
  const lang = languageFor(link);
613
- const data = await localApi(link.user_id, `/api/providers/${provider}/install`, { method: 'POST' });
883
+ if (!PROVIDERS.includes(provider)) {
884
+ await send(bot, chatId, t(lang, 'control.providerAuthFallback'), { editMessageId });
885
+ return;
886
+ }
887
+ const state = getState(link.user_id);
888
+ if (state.remoteControlEnabled === false) {
889
+ await send(bot, chatId, t(lang, 'control.disabled'), { editMessageId });
890
+ return;
891
+ }
892
+ if (state.confirmationPolicy === 'strict' && !confirmed) {
893
+ await requestConfirmation({
894
+ bot,
895
+ chatId,
896
+ link,
897
+ action: 'install_provider',
898
+ payload: { provider },
899
+ editMessageId,
900
+ });
901
+ return;
902
+ }
903
+
904
+ const result = await runTelegramTool({
905
+ userId: link.user_id,
906
+ action: 'install_provider',
907
+ execute: () => localApi(link.user_id, `/api/providers/${provider}/install`, { method: 'POST' }),
908
+ });
909
+ if (!result.ok) {
910
+ await sendToolFailure({ bot, chatId, link, result, editMessageId });
911
+ return;
912
+ }
913
+
914
+ const data = result.data;
614
915
  if (data?.manual) {
615
- await send(bot, chatId, t(lang, 'control.manualInstall', { provider, manual: data.manual }));
916
+ await send(bot, chatId, t(lang, 'control.manualInstall', { provider, manual: data.manual }), { editMessageId });
616
917
  return;
617
918
  }
618
919
  await send(bot, chatId, t(lang, 'control.installStarted', {
619
920
  provider,
620
921
  jobId: data.jobId,
621
922
  command: data.installCmd || 'internal installer',
622
- }));
923
+ }), { editMessageId });
623
924
  }
624
925
 
625
926
  async function showInstallMenu({ bot, chatId, link, editMessageId }) {
@@ -801,16 +1102,13 @@ async function handleCommand({ bot, chatId, link, text }) {
801
1102
  await send(bot, chatId, t(lang, 'control.cancelUsage'));
802
1103
  return true;
803
1104
  }
804
- const run = await localApi(link.user_id, `/api/orchestration/workflows/runs/${encodeURIComponent(argText)}/cancel`, {
805
- method: 'POST',
806
- });
807
- await send(bot, chatId, t(lang, 'control.runStatus', { runId: run.id, status: run.status }));
1105
+ await cancelRun({ bot, chatId, link, runId: argText });
808
1106
  return true;
809
1107
  }
810
1108
  return false;
811
1109
  }
812
1110
 
813
- export async function handleTelegramControlMessage({ bot, msg, link }) {
1111
+ async function handleTelegramControlMessageInternal({ bot, msg, link }) {
814
1112
  const chatId = msg.chat.id;
815
1113
  const text = String(msg.text || '').trim();
816
1114
  if (!text) return false;
@@ -826,8 +1124,10 @@ export async function handleTelegramControlMessage({ bot, msg, link }) {
826
1124
 
827
1125
  if (await handleAwaitingInput({ bot, chatId, link, text })) return true;
828
1126
  if (await handleCommand({ bot, chatId, link, text })) return true;
1127
+ if (await handleRoutedIntent({ bot, chatId, link, text })) return true;
829
1128
 
830
1129
  const state = getState(link.user_id);
1130
+ if (state.routerEnabled === false) return false;
831
1131
  if (!state.remoteControlEnabled) {
832
1132
  await send(bot, chatId, t(languageFor(link), 'control.disabled'));
833
1133
  return true;
@@ -837,6 +1137,20 @@ export async function handleTelegramControlMessage({ bot, msg, link }) {
837
1137
  return true;
838
1138
  }
839
1139
 
1140
+ export async function handleTelegramControlMessage(args) {
1141
+ const chatId = args?.msg?.chat?.id;
1142
+ if (!chatId) return false;
1143
+ return enqueueTelegramJob(chatId, async () => {
1144
+ try {
1145
+ return await handleTelegramControlMessageInternal(args);
1146
+ } catch (error) {
1147
+ console.error('[telegram-control] message handler failed:', error);
1148
+ await send(args.bot, chatId, t(languageFor(args.link), 'error.generic')).catch(() => {});
1149
+ return true;
1150
+ }
1151
+ });
1152
+ }
1153
+
840
1154
  async function showRunDetail({ bot, chatId, link, runId, editMessageId }) {
841
1155
  const lang = languageFor(link);
842
1156
  const run = await fetchRun(link.user_id, runId);
@@ -851,7 +1165,7 @@ async function showRunDetail({ bot, chatId, link, runId, editMessageId }) {
851
1165
  });
852
1166
  }
853
1167
 
854
- export async function handleTelegramControlCallback({ bot, query, link }) {
1168
+ async function handleTelegramControlCallbackInternal({ bot, query, link }) {
855
1169
  const chatId = query.message?.chat?.id;
856
1170
  if (!chatId) return;
857
1171
  const editMessageId = query.message?.message_id;
@@ -865,6 +1179,32 @@ export async function handleTelegramControlCallback({ bot, query, link }) {
865
1179
  await bot.answerCallbackQuery(query.id).catch(() => {});
866
1180
 
867
1181
  const { action, payload } = entry;
1182
+ if (action === 'confirm_action') {
1183
+ forgetAction(entry.id);
1184
+ const lang = languageFor(link);
1185
+ const result = consumeTelegramConfirmation(link.user_id, payload.id);
1186
+ if (!result.ok) {
1187
+ await send(bot, chatId, t(lang, result.reason === 'expired'
1188
+ ? 'control.confirmationExpired'
1189
+ : 'control.confirmationMissing'), { editMessageId });
1190
+ return;
1191
+ }
1192
+ if (await executeConfirmedAction({
1193
+ bot,
1194
+ chatId,
1195
+ link,
1196
+ confirmation: result.confirmation,
1197
+ editMessageId,
1198
+ })) return;
1199
+ await send(bot, chatId, t(lang, 'error.generic'), { editMessageId });
1200
+ return;
1201
+ }
1202
+ if (action === 'cancel_confirmation') {
1203
+ forgetAction(entry.id);
1204
+ clearTelegramConfirmation(link.user_id, payload.id);
1205
+ await send(bot, chatId, t(languageFor(link), 'control.confirmationCanceled'), { editMessageId });
1206
+ return;
1207
+ }
868
1208
  if (action === 'menu') return showMainMenu({ bot, chatId, link, editMessageId });
869
1209
  if (action === 'projects') return showProjectMenu({ bot, chatId, link, editMessageId });
870
1210
  if (action === 'providers') return showProviderMenu({ bot, chatId, link, editMessageId });
@@ -930,20 +1270,26 @@ export async function handleTelegramControlCallback({ bot, query, link }) {
930
1270
  }
931
1271
  if (action === 'run_detail') return showRunDetail({ bot, chatId, link, runId: payload.runId, editMessageId });
932
1272
  if (action === 'run_cancel') {
933
- const run = await localApi(link.user_id, `/api/orchestration/workflows/runs/${encodeURIComponent(payload.runId)}/cancel`, {
934
- method: 'POST',
935
- });
936
- await send(bot, chatId, t(languageFor(link), 'control.runStatus', { runId: run.id, status: run.status }), { editMessageId });
1273
+ await cancelRun({ bot, chatId, link, runId: payload.runId, editMessageId });
937
1274
  return;
938
1275
  }
939
1276
  if (action === 'approval_decide') {
940
- const result = await localApi(link.user_id, `/api/orchestration/workflows/approvals/${encodeURIComponent(payload.approvalId)}`, {
941
- method: 'POST',
942
- body: {
943
- allow: payload.allow === true,
944
- source: 'telegram',
945
- },
1277
+ const toolResult = await runTelegramTool({
1278
+ userId: link.user_id,
1279
+ action: 'approval_decide',
1280
+ execute: () => localApi(link.user_id, `/api/orchestration/workflows/approvals/${encodeURIComponent(payload.approvalId)}`, {
1281
+ method: 'POST',
1282
+ body: {
1283
+ allow: payload.allow === true,
1284
+ source: 'telegram',
1285
+ },
1286
+ }),
946
1287
  });
1288
+ if (!toolResult.ok) {
1289
+ await sendToolFailure({ bot, chatId, link, result: toolResult, editMessageId });
1290
+ return;
1291
+ }
1292
+ const result = toolResult.data;
947
1293
  const lang = languageFor(link);
948
1294
  await send(bot, chatId, t(lang, 'control.approvalDecided', {
949
1295
  approvalId: payload.approvalId,
@@ -952,7 +1298,7 @@ export async function handleTelegramControlCallback({ bot, query, link }) {
952
1298
  }), { editMessageId });
953
1299
  return showApprovalQueue({ bot, chatId, link });
954
1300
  }
955
- if (action === 'install_provider') return startCliInstall({ bot, chatId, link, provider: payload.provider });
1301
+ if (action === 'install_provider') return startCliInstall({ bot, chatId, link, provider: payload.provider, editMessageId });
956
1302
  if (action === 'auth_provider') {
957
1303
  await send(bot, chatId, `${payload.provider} login:\n${AUTH_HELP[payload.provider] || t(languageFor(link), 'control.providerAuthFallback')}`, { editMessageId });
958
1304
  return;
@@ -977,3 +1323,18 @@ export async function handleTelegramControlCallback({ bot, query, link }) {
977
1323
  });
978
1324
  }
979
1325
  }
1326
+
1327
+ export async function handleTelegramControlCallback(args) {
1328
+ const chatId = args?.query?.message?.chat?.id;
1329
+ if (!chatId) return;
1330
+ return enqueueTelegramJob(chatId, async () => {
1331
+ try {
1332
+ await handleTelegramControlCallbackInternal(args);
1333
+ } catch (error) {
1334
+ console.error('[telegram-control] callback failed:', error);
1335
+ const lang = languageFor(args.link);
1336
+ await args.bot?.answerCallbackQuery(args.query?.id, { text: t(lang, 'error.generic') }).catch(() => {});
1337
+ await send(args.bot, chatId, t(lang, 'error.generic')).catch(() => {});
1338
+ }
1339
+ });
1340
+ }