@pixelbyte-software/pixcode 1.54.10 → 1.54.11
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/CODE_OF_CONDUCT.md +41 -41
- package/CONTRIBUTING.md +156 -156
- package/LICENSE +21 -718
- package/README.de.md +169 -169
- package/README.ja.md +167 -167
- package/README.ko.md +167 -167
- package/README.md +435 -418
- package/README.ru.md +169 -169
- package/README.tr.md +298 -298
- package/README.zh-CN.md +167 -167
- package/SECURITY.md +46 -46
- package/dist/api-automation.html +110 -110
- package/dist/api-docs.html +548 -548
- package/dist/assets/{index-DWOW_Jn0.js → index-Bcqs_1vc.js} +114 -114
- package/dist/clear-cache.html +85 -85
- package/dist/convert-icons.md +52 -52
- package/dist/docs.html +308 -308
- package/dist/features.html +133 -133
- package/dist/generate-icons.js +48 -48
- package/dist/humans.txt +15 -15
- package/dist/icons/codex-white.svg +3 -3
- package/dist/icons/codex.svg +3 -3
- package/dist/icons/cursor-white.svg +11 -11
- package/dist/icons/qwen-logo.svg +14 -14
- package/dist/index.html +58 -58
- package/dist/landing.html +268 -268
- package/dist/llms-full.txt +119 -119
- package/dist/llms.txt +53 -53
- package/dist/manifest.json +60 -60
- package/dist/openapi.yaml +1696 -1696
- package/dist/orchestration.html +125 -125
- package/dist/robots.txt +4 -4
- package/dist/site.css +692 -692
- package/dist/sitemap.xml +51 -51
- package/dist/sw.js +132 -132
- package/dist-server/server/cli.js +100 -100
- package/dist-server/server/daemon/manager.js +33 -33
- package/dist-server/server/daemon-manager.js +64 -64
- package/dist-server/server/routes/commands.js +25 -25
- package/dist-server/server/routes/git.js +17 -17
- package/dist-server/server/routes/live-view.js +46 -46
- package/dist-server/server/services/public-api-manifest.js +51 -51
- package/package.json +224 -224
- package/scripts/fix-node-pty.js +67 -67
- package/scripts/github/create-v1.38-issues.mjs +351 -351
- package/scripts/github/create-vscode-workbench-issues.mjs +121 -121
- package/scripts/smoke/backend-resource-bounds.mjs +152 -152
- package/scripts/smoke/changes-panel-layout.mjs +48 -48
- package/scripts/smoke/chat-composer-fixed-layout.mjs +55 -55
- package/scripts/smoke/chat-message-timeline-order.mjs +41 -41
- package/scripts/smoke/chat-realtime-hydration.mjs +44 -44
- package/scripts/smoke/chat-session-provider-pools.mjs +35 -35
- package/scripts/smoke/chat-session-state.mjs +19 -19
- package/scripts/smoke/code-editor-theme.mjs +55 -55
- package/scripts/smoke/code-editor-vscode-engine.mjs +91 -91
- package/scripts/smoke/command-center-agent-writes.mjs +79 -79
- package/scripts/smoke/command-center-non-git.mjs +46 -46
- package/scripts/smoke/context-packet.mjs +43 -43
- package/scripts/smoke/control-room-ux-redesign.mjs +91 -91
- package/scripts/smoke/daemon-entrypoint.mjs +20 -20
- package/scripts/smoke/default-landing-routing.mjs +33 -33
- package/scripts/smoke/desktop-native-notifications.mjs +30 -30
- package/scripts/smoke/desktop-tray-icon.mjs +33 -33
- package/scripts/smoke/discord-release-workflow.mjs +24 -24
- package/scripts/smoke/git-install-update.mjs +255 -255
- package/scripts/smoke/handoff-artifact-protocol.mjs +50 -50
- package/scripts/smoke/live-view-diagnostics.mjs +53 -53
- package/scripts/smoke/live-view-environment.mjs +92 -92
- package/scripts/smoke/live-view-integration.mjs +450 -450
- package/scripts/smoke/mac-desktop-runtime.mjs +37 -37
- package/scripts/smoke/mobile-tunnel-guidance.mjs +29 -29
- package/scripts/smoke/mobile-ux.mjs +76 -76
- package/scripts/smoke/model-registry.mjs +36 -36
- package/scripts/smoke/multi-project-ui.mjs +45 -45
- package/scripts/smoke/multi-worker-slots.mjs +42 -42
- package/scripts/smoke/notification-center.mjs +87 -87
- package/scripts/smoke/notification-inapp-preference.mjs +23 -23
- package/scripts/smoke/notification-taxonomy.mjs +58 -58
- package/scripts/smoke/orchestration-api.mjs +172 -172
- package/scripts/smoke/orchestration-execution-dashboard.mjs +33 -33
- package/scripts/smoke/orchestration-live-run.mjs +176 -176
- package/scripts/smoke/orchestration-mobile-scroll.mjs +29 -29
- package/scripts/smoke/orchestration-model-sync.mjs +30 -30
- package/scripts/smoke/orchestration-permission-fallback.mjs +34 -34
- package/scripts/smoke/orchestration-runtime-guards.mjs +48 -48
- package/scripts/smoke/orchestration-user-facing-output.mjs +25 -25
- package/scripts/smoke/permission-policy.mjs +50 -50
- package/scripts/smoke/pixcode-workbench-1-48.mjs +167 -167
- package/scripts/smoke/provider-models-opencode-live.mjs +66 -66
- package/scripts/smoke/provider-rest-api.mjs +124 -124
- package/scripts/smoke/provider-selection-status.mjs +52 -52
- package/scripts/smoke/run-state-refresh.mjs +52 -52
- package/scripts/smoke/runtime-manager.mjs +99 -99
- package/scripts/smoke/shell-manual-disconnect.mjs +30 -30
- package/scripts/smoke/side-panel-editor-layout.mjs +34 -34
- package/scripts/smoke/static-root-routing.mjs +21 -21
- package/scripts/smoke/strict-handoff-compact.mjs +60 -60
- package/scripts/smoke/taskmaster-config.mjs +24 -24
- package/scripts/smoke/taskmaster-execution-telegram.mjs +3 -3
- package/scripts/smoke/taskmaster-onboarding.mjs +3 -3
- package/scripts/smoke/taskmaster-run-graph.mjs +3 -3
- package/scripts/smoke/telegram-control.mjs +331 -331
- package/scripts/smoke/tunnel-persistence.mjs +56 -56
- package/scripts/smoke/update-issue-progress.mjs +69 -69
- package/scripts/smoke/update-ux.mjs +55 -55
- package/scripts/smoke/v138-completion.mjs +132 -132
- package/scripts/smoke/v138-desktop-release-hardening.mjs +69 -69
- package/scripts/smoke/v138-diagnostics.mjs +63 -63
- package/scripts/smoke/v138-issue-planner.mjs +33 -33
- package/scripts/smoke/v143-remote-control.mjs +76 -76
- package/scripts/smoke/v144-production-loop.mjs +47 -47
- package/scripts/smoke/v145-platformization.mjs +46 -46
- package/scripts/smoke/v146-control-room-ui.mjs +150 -150
- package/scripts/smoke/version-modal-autoshow.mjs +29 -29
- package/scripts/smoke/vscode-workbench-layout.mjs +63 -63
- package/scripts/smoke/vscode-workbench-polish.mjs +461 -461
- package/scripts/smoke/workflow-fallback-replay.mjs +56 -56
- package/scripts/smoke/workflow-templates.mjs +43 -43
- package/scripts/smoke/workflow-trace-timeline.mjs +46 -46
- package/scripts/update-git-install.mjs +298 -298
- package/server/claude-sdk.js +927 -927
- package/server/cli.js +1106 -1106
- package/server/constants/config.js +4 -4
- package/server/cursor-cli.js +344 -344
- package/server/daemon/manager.js +563 -563
- package/server/daemon-manager.js +964 -964
- package/server/database/db.js +988 -988
- package/server/database/json-store.js +197 -197
- package/server/gemini-cli.js +550 -550
- package/server/gemini-response-handler.js +79 -79
- package/server/index.js +5671 -5671
- package/server/load-env.js +35 -35
- package/server/middleware/account-lockout.js +112 -112
- package/server/middleware/auth.js +277 -277
- package/server/middleware/rate-limiter.js +87 -87
- package/server/modules/orchestration/a2a/adapter-registry.ts +108 -108
- package/server/modules/orchestration/a2a/adapters/abstract-a2a.adapter.ts +63 -63
- package/server/modules/orchestration/a2a/adapters/claude-code.adapter.ts +286 -286
- package/server/modules/orchestration/a2a/adapters/codex.adapter.ts +244 -244
- package/server/modules/orchestration/a2a/adapters/cursor.adapter.ts +249 -249
- package/server/modules/orchestration/a2a/adapters/gemini.adapter.ts +248 -248
- package/server/modules/orchestration/a2a/adapters/json-event.adapter.test.ts +60 -60
- package/server/modules/orchestration/a2a/adapters/json-event.adapter.ts +101 -101
- package/server/modules/orchestration/a2a/adapters/opencode.adapter.ts +248 -248
- package/server/modules/orchestration/a2a/adapters/qwen.adapter.ts +248 -248
- package/server/modules/orchestration/a2a/agent-card.ts +55 -55
- package/server/modules/orchestration/a2a/auth.middleware.ts +29 -29
- package/server/modules/orchestration/a2a/bus.ts +46 -46
- package/server/modules/orchestration/a2a/routes.ts +586 -586
- package/server/modules/orchestration/a2a/task-dispatcher.ts +345 -345
- package/server/modules/orchestration/a2a/task-store.ts +178 -178
- package/server/modules/orchestration/a2a/types.ts +126 -126
- package/server/modules/orchestration/a2a/validator.ts +113 -113
- package/server/modules/orchestration/index.ts +99 -99
- package/server/modules/orchestration/preview/port-watcher.ts +112 -112
- package/server/modules/orchestration/preview/preview-proxy.ts +60 -60
- package/server/modules/orchestration/preview/types.ts +19 -19
- package/server/modules/orchestration/security/permission-policy.ts +401 -401
- package/server/modules/orchestration/tasks/orchestration-task-store.ts +41 -41
- package/server/modules/orchestration/tasks/orchestration-task.routes.ts +81 -81
- package/server/modules/orchestration/tasks/orchestration-task.service.ts +201 -201
- package/server/modules/orchestration/tasks/orchestration-task.types.ts +40 -40
- package/server/modules/orchestration/tasks/task-run-graph.ts +155 -155
- package/server/modules/orchestration/workflows/approval-queue.ts +106 -106
- package/server/modules/orchestration/workflows/built-in-workflows.ts +127 -127
- package/server/modules/orchestration/workflows/context-packet.ts +186 -186
- package/server/modules/orchestration/workflows/handoff-artifact.ts +175 -175
- package/server/modules/orchestration/workflows/workflow-fallback-policy.ts +161 -161
- package/server/modules/orchestration/workflows/workflow-replay.ts +254 -254
- package/server/modules/orchestration/workflows/workflow-runner.ts +2062 -2062
- package/server/modules/orchestration/workflows/workflow-store.ts +97 -97
- package/server/modules/orchestration/workflows/workflow-templates.ts +272 -272
- package/server/modules/orchestration/workflows/workflow-trace.ts +424 -424
- package/server/modules/orchestration/workflows/workflow.routes.ts +586 -586
- package/server/modules/orchestration/workflows/workflow.types.ts +111 -111
- package/server/modules/orchestration/workflows/workspace-target.ts +122 -122
- package/server/modules/orchestration/workspace/docker-workspace.ts +136 -136
- package/server/modules/orchestration/workspace/path-safety.ts +55 -55
- package/server/modules/orchestration/workspace/types.ts +52 -52
- package/server/modules/orchestration/workspace/workspace-manager.ts +102 -102
- package/server/modules/orchestration/workspace/worktree-workspace.ts +126 -126
- package/server/modules/providers/index.ts +2 -2
- package/server/modules/providers/list/claude/claude-auth.provider.ts +146 -146
- package/server/modules/providers/list/claude/claude-mcp.provider.ts +135 -135
- package/server/modules/providers/list/claude/claude-sessions.provider.ts +306 -306
- package/server/modules/providers/list/claude/claude.provider.ts +15 -15
- package/server/modules/providers/list/codex/codex-auth.provider.ts +117 -117
- package/server/modules/providers/list/codex/codex-mcp.provider.ts +135 -135
- package/server/modules/providers/list/codex/codex-sessions.provider.ts +319 -319
- package/server/modules/providers/list/codex/codex.provider.ts +15 -15
- package/server/modules/providers/list/cursor/cursor-auth.provider.ts +147 -147
- package/server/modules/providers/list/cursor/cursor-mcp.provider.ts +108 -108
- package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +421 -421
- package/server/modules/providers/list/cursor/cursor.provider.ts +15 -15
- package/server/modules/providers/list/gemini/gemini-auth.provider.ts +173 -173
- package/server/modules/providers/list/gemini/gemini-mcp.provider.ts +110 -110
- package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +227 -227
- package/server/modules/providers/list/gemini/gemini.provider.ts +15 -15
- package/server/modules/providers/list/opencode/opencode-auth.provider.ts +131 -131
- package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +286 -286
- package/server/modules/providers/list/qwen/qwen-auth.provider.ts +146 -146
- package/server/modules/providers/list/qwen/qwen-sessions.provider.ts +265 -265
- package/server/modules/providers/provider.registry.ts +40 -40
- package/server/modules/providers/provider.routes.ts +982 -982
- package/server/modules/providers/services/mcp.service.ts +86 -86
- package/server/modules/providers/services/provider-auth.service.ts +26 -26
- package/server/modules/providers/services/sessions.service.ts +45 -45
- package/server/modules/providers/shared/base/abstract.provider.ts +20 -20
- package/server/modules/providers/shared/mcp/mcp.provider.ts +151 -151
- package/server/modules/providers/tests/mcp.test.ts +293 -293
- package/server/openai-codex.js +468 -468
- package/server/opencode-cli.js +491 -491
- package/server/opencode-response-handler.js +111 -111
- package/server/projects.js +3135 -3135
- package/server/qwen-code-cli.js +410 -410
- package/server/routes/agent.js +1462 -1462
- package/server/routes/auth.js +298 -298
- package/server/routes/codex.js +20 -20
- package/server/routes/commands.js +581 -581
- package/server/routes/cursor.js +61 -61
- package/server/routes/diagnostics.js +44 -44
- package/server/routes/gemini.js +25 -25
- package/server/routes/git.js +1822 -1822
- package/server/routes/live-view.js +434 -434
- package/server/routes/mcp-utils.js +13 -13
- package/server/routes/messages.js +97 -97
- package/server/routes/network.js +143 -143
- package/server/routes/platformization.js +231 -231
- package/server/routes/plugins.js +690 -690
- package/server/routes/production-agent-loop.js +90 -90
- package/server/routes/projects.js +918 -918
- package/server/routes/public-api.js +34 -34
- package/server/routes/qwen.js +27 -27
- package/server/routes/remote.js +56 -56
- package/server/routes/settings.js +321 -321
- package/server/routes/telegram.js +163 -163
- package/server/routes/user.js +125 -125
- package/server/routes/webhooks.js +63 -63
- package/server/services/control-room.js +102 -102
- package/server/services/diagnostics.js +165 -165
- package/server/services/external-access.js +403 -403
- package/server/services/install-jobs.js +715 -715
- package/server/services/live-view.js +956 -956
- package/server/services/managed-runtimes.js +493 -493
- package/server/services/model-registry.js +144 -144
- package/server/services/notification-orchestrator.js +365 -365
- package/server/services/notification-taxonomy.js +204 -204
- package/server/services/platformization.js +1052 -1052
- package/server/services/production-agent-loop.js +248 -248
- package/server/services/provider-cli-versions.js +149 -149
- package/server/services/provider-credentials.js +189 -189
- package/server/services/provider-models.js +396 -396
- package/server/services/public-api-manifest.js +181 -181
- package/server/services/qr-login.js +126 -126
- package/server/services/remote-connection.js +127 -127
- package/server/services/runtime-manager.js +323 -323
- package/server/services/startup-update.js +259 -259
- package/server/services/telegram/bot.js +393 -393
- package/server/services/telegram/control-center.js +2453 -2453
- package/server/services/telegram/telegram-gateway.js +314 -314
- package/server/services/telegram/telegram-http-client.js +201 -201
- package/server/services/telegram/translations.js +470 -470
- package/server/services/webhooks.js +216 -216
- package/server/sessionManager.js +225 -225
- package/server/setup-wizard.js +283 -283
- package/server/shared/interfaces.ts +54 -54
- package/server/shared/types.ts +172 -172
- package/server/shared/utils.ts +193 -193
- package/server/tsconfig.json +36 -36
- package/server/utils/colors.js +21 -21
- package/server/utils/commandParser.js +305 -305
- package/server/utils/frontmatter.js +18 -18
- package/server/utils/gitConfig.js +34 -34
- package/server/utils/plugin-loader.js +461 -461
- package/server/utils/plugin-process-manager.js +185 -185
- package/server/utils/port-access.js +227 -227
- package/server/utils/runtime-paths.js +37 -37
- package/server/utils/security-log.js +84 -84
- package/server/utils/url-detection.js +71 -71
- package/server/vite-daemon.js +79 -79
- package/shared/modelConstants.js +161 -161
- package/shared/networkHosts.js +22 -22
|
@@ -1,2453 +1,2453 @@
|
|
|
1
|
-
import crypto from 'node:crypto';
|
|
2
|
-
|
|
3
|
-
import { apiKeysDb, telegramLinksDb } from '../../database/db.js';
|
|
4
|
-
import { getProjects } from '../../projects.js';
|
|
5
|
-
import { getStaticProviderModels } from '../model-registry.js';
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
TELEGRAM_CONTROL_SCOPES,
|
|
9
|
-
TELEGRAM_PROVIDERS,
|
|
10
|
-
buildTelegramAgentPrompt,
|
|
11
|
-
buildTelegramIntentPrompt,
|
|
12
|
-
clearTelegramConfirmation,
|
|
13
|
-
consumeTelegramConfirmation,
|
|
14
|
-
createTelegramConfirmation,
|
|
15
|
-
enqueueTelegramJob,
|
|
16
|
-
parseTelegramAiIntentResponse,
|
|
17
|
-
resolveTelegramModel,
|
|
18
|
-
resolveTelegramProvider,
|
|
19
|
-
retryWithBackoff,
|
|
20
|
-
runTelegramTool,
|
|
21
|
-
splitTelegramText,
|
|
22
|
-
} from './telegram-gateway.js';
|
|
23
|
-
import { SUPPORTED_LANGUAGES, t } from './translations.js';
|
|
24
|
-
|
|
25
|
-
const PROVIDERS = TELEGRAM_PROVIDERS;
|
|
26
|
-
const TERMINAL_RUN_STATES = new Set(['completed', 'failed', 'canceled']);
|
|
27
|
-
const CALLBACK_TTL_MS = 10 * 60 * 1000;
|
|
28
|
-
const MAX_CALLBACK_ACTIONS = 1000;
|
|
29
|
-
const MAX_TELEGRAM_TEXT = 3600;
|
|
30
|
-
const MAX_ACTIVITY_OUTPUT_CHARS = 48_000;
|
|
31
|
-
const MAX_SSE_BUFFER_CHARS = 256_000;
|
|
32
|
-
const ACTIVITY_EDIT_THROTTLE_MS = 1200;
|
|
33
|
-
const ACTIVITY_HEARTBEAT_MS = 8000;
|
|
34
|
-
const INTENT_ROUTER_TIMEOUT_MS = 45_000;
|
|
35
|
-
const TERMINAL_BRIDGE_TIMEOUT_MS = 5 * 60 * 1000;
|
|
36
|
-
const TERMINAL_BRIDGE_POLL_MS = 1500;
|
|
37
|
-
const TERMINAL_BRIDGE_EDIT_THROTTLE_MS = 3500;
|
|
38
|
-
const TERMINAL_BRIDGE_SETTLE_MS = 6000;
|
|
39
|
-
const TERMINAL_BRIDGE_FINAL_OPEN = '<PIXCODE_TELEGRAM_FINAL>';
|
|
40
|
-
const TERMINAL_BRIDGE_FINAL_CLOSE = '</PIXCODE_TELEGRAM_FINAL>';
|
|
41
|
-
const callbackActions = new Map();
|
|
42
|
-
const runMonitors = new Map();
|
|
43
|
-
const activeLongTasks = new Map();
|
|
44
|
-
const terminalBridgeMonitors = new Map();
|
|
45
|
-
|
|
46
|
-
const MODEL_FALLBACKS = Object.fromEntries(
|
|
47
|
-
PROVIDERS.map((provider) => [provider, getStaticProviderModels(provider)]),
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
const AUTH_HELP = {
|
|
51
|
-
claude: '`claude login`',
|
|
52
|
-
cursor: '`cursor-agent login`',
|
|
53
|
-
codex: '`codex login`',
|
|
54
|
-
gemini: '`gemini auth login`',
|
|
55
|
-
qwen: '`qwen auth`',
|
|
56
|
-
opencode: '`opencode auth login`',
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
const CONTROL_COMMANDS = new Set([
|
|
60
|
-
'/menu',
|
|
61
|
-
'/start',
|
|
62
|
-
'/help',
|
|
63
|
-
'/projects',
|
|
64
|
-
'/provider',
|
|
65
|
-
'/model',
|
|
66
|
-
'/models',
|
|
67
|
-
'/workflows',
|
|
68
|
-
'/orchestration',
|
|
69
|
-
'/runs',
|
|
70
|
-
'/approvals',
|
|
71
|
-
'/controlroom',
|
|
72
|
-
'/control-room',
|
|
73
|
-
'/webhooks',
|
|
74
|
-
'/sessions',
|
|
75
|
-
'/newchat',
|
|
76
|
-
'/settings',
|
|
77
|
-
'/install',
|
|
78
|
-
'/auth',
|
|
79
|
-
'/control',
|
|
80
|
-
'/progress',
|
|
81
|
-
'/chat',
|
|
82
|
-
'/workflow',
|
|
83
|
-
'/orchestrate',
|
|
84
|
-
'/cancel',
|
|
85
|
-
'/terminal',
|
|
86
|
-
'/detach',
|
|
87
|
-
'menu',
|
|
88
|
-
]);
|
|
89
|
-
|
|
90
|
-
function compact(text, max = 80) {
|
|
91
|
-
const value = String(text || '').replace(/\s+/g, ' ').trim();
|
|
92
|
-
return value.length > max ? `${value.slice(0, max - 1)}…` : value;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function truncate(text, max = MAX_TELEGRAM_TEXT) {
|
|
96
|
-
const value = String(text || '').trim();
|
|
97
|
-
return value.length > max ? `${value.slice(0, max - 20)}\n\n…truncated…` : value;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function appendBoundedText(current, chunk, maxChars = MAX_ACTIVITY_OUTPUT_CHARS) {
|
|
101
|
-
const nextChunk = String(chunk || '');
|
|
102
|
-
if (!nextChunk) return { text: current || '', truncated: false };
|
|
103
|
-
const combined = `${current || ''}${nextChunk}`;
|
|
104
|
-
if (combined.length <= maxChars) return { text: combined, truncated: false };
|
|
105
|
-
return { text: combined.slice(-maxChars), truncated: true };
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function languageFor(link) {
|
|
109
|
-
return SUPPORTED_LANGUAGES.includes(link?.language) ? link.language : 'en';
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function normalizeCommandToken(commandRaw = '') {
|
|
113
|
-
return String(commandRaw || '')
|
|
114
|
-
.trim()
|
|
115
|
-
.toLowerCase()
|
|
116
|
-
.replace(/@[^@\s]+$/, '');
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export function getTelegramControlCommand(text = '') {
|
|
120
|
-
const [commandRaw = ''] = String(text || '').trim().split(/\s+/);
|
|
121
|
-
const command = normalizeCommandToken(commandRaw);
|
|
122
|
-
return CONTROL_COMMANDS.has(command) ? command : null;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export function isTelegramControlCommand(text = '') {
|
|
126
|
-
return Boolean(getTelegramControlCommand(text));
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function getState(userId) {
|
|
130
|
-
return telegramLinksDb.getControlState(userId);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export function updateTelegramControlState(userId, patch) {
|
|
134
|
-
return telegramLinksDb.updateControlState(userId, patch);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function pruneCallbackActions() {
|
|
138
|
-
const now = Date.now();
|
|
139
|
-
for (const [id, entry] of callbackActions.entries()) {
|
|
140
|
-
if (entry.expiresAt < now) {
|
|
141
|
-
callbackActions.delete(id);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
while (callbackActions.size > MAX_CALLBACK_ACTIONS) {
|
|
146
|
-
const oldestId = callbackActions.keys().next().value;
|
|
147
|
-
if (!oldestId) break;
|
|
148
|
-
callbackActions.delete(oldestId);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function registerAction(action, payload = {}) {
|
|
153
|
-
pruneCallbackActions();
|
|
154
|
-
const id = crypto.randomBytes(8).toString('hex');
|
|
155
|
-
callbackActions.set(id, {
|
|
156
|
-
action,
|
|
157
|
-
payload,
|
|
158
|
-
expiresAt: Date.now() + CALLBACK_TTL_MS,
|
|
159
|
-
});
|
|
160
|
-
return `tc:${id}`;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function readAction(data) {
|
|
164
|
-
if (typeof data !== 'string' || !data.startsWith('tc:')) return null;
|
|
165
|
-
const id = data.slice(3);
|
|
166
|
-
const entry = callbackActions.get(id);
|
|
167
|
-
if (!entry) return null;
|
|
168
|
-
if (entry.expiresAt < Date.now()) {
|
|
169
|
-
callbackActions.delete(id);
|
|
170
|
-
return null;
|
|
171
|
-
}
|
|
172
|
-
return { id, ...entry };
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function forgetAction(id) {
|
|
176
|
-
if (id) callbackActions.delete(id);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function button(text, action, payload = {}) {
|
|
180
|
-
return { text, callback_data: registerAction(action, payload) };
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function rows(buttons, width = 2) {
|
|
184
|
-
const out = [];
|
|
185
|
-
for (let i = 0; i < buttons.length; i += width) out.push(buttons.slice(i, i + width));
|
|
186
|
-
return out;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
async function send(bot, chatId, text, options = {}) {
|
|
190
|
-
const { editMessageId, ...telegramOptions } = options;
|
|
191
|
-
const extra = {
|
|
192
|
-
parse_mode: 'Markdown',
|
|
193
|
-
disable_web_page_preview: true,
|
|
194
|
-
...telegramOptions,
|
|
195
|
-
};
|
|
196
|
-
const chunks = splitTelegramText(text, MAX_TELEGRAM_TEXT);
|
|
197
|
-
const [firstChunk = ''] = chunks;
|
|
198
|
-
let startIndex = 0;
|
|
199
|
-
if (editMessageId && typeof bot.editMessageText === 'function') {
|
|
200
|
-
try {
|
|
201
|
-
const edited = await bot.editMessageText(firstChunk, {
|
|
202
|
-
chat_id: chatId,
|
|
203
|
-
message_id: editMessageId,
|
|
204
|
-
...extra,
|
|
205
|
-
});
|
|
206
|
-
startIndex = 1;
|
|
207
|
-
for (const chunk of chunks.slice(startIndex)) {
|
|
208
|
-
await send(bot, chatId, chunk, telegramOptions);
|
|
209
|
-
}
|
|
210
|
-
return edited;
|
|
211
|
-
} catch (err) {
|
|
212
|
-
const description = err?.response?.body?.description || err?.message || '';
|
|
213
|
-
if (/message is not modified/i.test(description)) {
|
|
214
|
-
startIndex = 1;
|
|
215
|
-
for (const chunk of chunks.slice(startIndex)) {
|
|
216
|
-
await send(bot, chatId, chunk, telegramOptions);
|
|
217
|
-
}
|
|
218
|
-
return null;
|
|
219
|
-
}
|
|
220
|
-
console.warn('[telegram-control] editMessageText failed:', description || err);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
let result = null;
|
|
224
|
-
for (const chunk of chunks.slice(startIndex)) {
|
|
225
|
-
try {
|
|
226
|
-
result = await bot.sendMessage(chatId, chunk, extra);
|
|
227
|
-
} catch {
|
|
228
|
-
const fallback = { ...extra };
|
|
229
|
-
delete fallback.parse_mode;
|
|
230
|
-
result = await bot.sendMessage(chatId, chunk, fallback);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
return result;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function localApiBase() {
|
|
237
|
-
const port = process.env.SERVER_PORT || process.env.PORT || '3001';
|
|
238
|
-
return `http://127.0.0.1:${port}`;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function getOrCreateTelegramApiKey(userId) {
|
|
242
|
-
const existing = apiKeysDb
|
|
243
|
-
.getApiKeys(userId)
|
|
244
|
-
.find((key) => key.key_name === 'Telegram Control' && Boolean(key.is_active));
|
|
245
|
-
if (existing?.api_key) {
|
|
246
|
-
const existingScopes = apiKeysDb.normalizeScopes(existing.scopes || []);
|
|
247
|
-
const nextScopes = apiKeysDb.normalizeScopes([...existingScopes, ...TELEGRAM_CONTROL_SCOPES]);
|
|
248
|
-
const hasAllScopes = TELEGRAM_CONTROL_SCOPES.every((scope) => existingScopes.includes(scope));
|
|
249
|
-
if (!hasAllScopes || nextScopes.length !== existingScopes.length) {
|
|
250
|
-
apiKeysDb.updateApiKeyScopes(userId, existing.id, nextScopes);
|
|
251
|
-
}
|
|
252
|
-
return existing.api_key;
|
|
253
|
-
}
|
|
254
|
-
return apiKeysDb.createApiKey(userId, 'Telegram Control', TELEGRAM_CONTROL_SCOPES).apiKey;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
async function localApi(userId, path, { method = 'GET', body, timeoutMs = 0 } = {}) {
|
|
258
|
-
const apiKey = getOrCreateTelegramApiKey(userId);
|
|
259
|
-
const response = await retryWithBackoff(async () => {
|
|
260
|
-
const controller = timeoutMs > 0 ? new AbortController() : null;
|
|
261
|
-
const timeoutId = controller
|
|
262
|
-
? setTimeout(() => controller.abort(new Error(`HTTP request timed out after ${timeoutMs}ms`)), timeoutMs)
|
|
263
|
-
: null;
|
|
264
|
-
const res = await fetch(`${localApiBase()}${path}`, {
|
|
265
|
-
method,
|
|
266
|
-
headers: {
|
|
267
|
-
'Content-Type': 'application/json',
|
|
268
|
-
'X-API-Key': apiKey,
|
|
269
|
-
},
|
|
270
|
-
signal: controller?.signal,
|
|
271
|
-
body: body === undefined ? undefined : JSON.stringify(body),
|
|
272
|
-
}).finally(() => {
|
|
273
|
-
if (timeoutId) clearTimeout(timeoutId);
|
|
274
|
-
});
|
|
275
|
-
if ([408, 429, 500, 502, 503, 504].includes(res.status)) {
|
|
276
|
-
const error = new Error(`HTTP ${res.status}`);
|
|
277
|
-
error.status = res.status;
|
|
278
|
-
throw error;
|
|
279
|
-
}
|
|
280
|
-
return res;
|
|
281
|
-
});
|
|
282
|
-
const data = await response.json().catch(() => ({}));
|
|
283
|
-
if (!response.ok) {
|
|
284
|
-
const error = data?.error?.message || data?.error || `HTTP ${response.status}`;
|
|
285
|
-
throw new Error(typeof error === 'string' ? error : JSON.stringify(error));
|
|
286
|
-
}
|
|
287
|
-
return data?.data ?? data;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
async function localAgentStream(userId, body, onEvent) {
|
|
291
|
-
const apiKey = getOrCreateTelegramApiKey(userId);
|
|
292
|
-
const response = await fetch(`${localApiBase()}/api/agent`, {
|
|
293
|
-
method: 'POST',
|
|
294
|
-
headers: {
|
|
295
|
-
'Content-Type': 'application/json',
|
|
296
|
-
'X-API-Key': apiKey,
|
|
297
|
-
},
|
|
298
|
-
body: JSON.stringify({
|
|
299
|
-
...body,
|
|
300
|
-
stream: true,
|
|
301
|
-
}),
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
if (!response.ok) {
|
|
305
|
-
const data = await response.json().catch(() => ({}));
|
|
306
|
-
const error = data?.error?.message || data?.error || `HTTP ${response.status}`;
|
|
307
|
-
throw new Error(typeof error === 'string' ? error : JSON.stringify(error));
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
if (!response.body) return;
|
|
311
|
-
|
|
312
|
-
const reader = response.body.getReader();
|
|
313
|
-
const decoder = new TextDecoder();
|
|
314
|
-
let buffer = '';
|
|
315
|
-
|
|
316
|
-
const consumeBlock = async (block) => {
|
|
317
|
-
const lines = String(block || '').split('\n');
|
|
318
|
-
for (const line of lines) {
|
|
319
|
-
if (!line.startsWith('data:')) continue;
|
|
320
|
-
const payload = line.slice(5).trim();
|
|
321
|
-
if (!payload) continue;
|
|
322
|
-
let event;
|
|
323
|
-
try {
|
|
324
|
-
event = JSON.parse(payload);
|
|
325
|
-
} catch {
|
|
326
|
-
continue;
|
|
327
|
-
}
|
|
328
|
-
await onEvent?.(event);
|
|
329
|
-
}
|
|
330
|
-
};
|
|
331
|
-
|
|
332
|
-
for (;;) {
|
|
333
|
-
const { done, value } = await reader.read();
|
|
334
|
-
if (done) break;
|
|
335
|
-
buffer += decoder.decode(value, { stream: true });
|
|
336
|
-
if (buffer.length > MAX_SSE_BUFFER_CHARS) {
|
|
337
|
-
buffer = buffer.slice(-MAX_SSE_BUFFER_CHARS);
|
|
338
|
-
}
|
|
339
|
-
let boundary = buffer.indexOf('\n\n');
|
|
340
|
-
while (boundary !== -1) {
|
|
341
|
-
const block = buffer.slice(0, boundary);
|
|
342
|
-
buffer = buffer.slice(boundary + 2);
|
|
343
|
-
await consumeBlock(block);
|
|
344
|
-
boundary = buffer.indexOf('\n\n');
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
const rest = `${buffer}${decoder.decode()}`;
|
|
349
|
-
if (rest.trim()) await consumeBlock(rest);
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
function checked(label) {
|
|
353
|
-
return `${label} ✓`;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
function formatElapsed(startedAt) {
|
|
357
|
-
const seconds = Math.max(0, Math.round((Date.now() - startedAt) / 1000));
|
|
358
|
-
if (seconds < 60) return `${seconds}s`;
|
|
359
|
-
const minutes = Math.floor(seconds / 60);
|
|
360
|
-
const rest = seconds % 60;
|
|
361
|
-
return `${minutes}m ${rest}s`;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
function projectLabel(state) {
|
|
365
|
-
return state.selectedProjectName || state.selectedProjectPath || '-';
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
function createActivityState({ lang, type = 'agent', provider, project, prompt, mode = 'final' }) {
|
|
369
|
-
return {
|
|
370
|
-
lang,
|
|
371
|
-
type,
|
|
372
|
-
status: 'starting',
|
|
373
|
-
phase: t(lang, 'control.activity.starting'),
|
|
374
|
-
provider,
|
|
375
|
-
project,
|
|
376
|
-
prompt: compact(prompt, 120),
|
|
377
|
-
mode,
|
|
378
|
-
startedAt: Date.now(),
|
|
379
|
-
lastEditAt: 0,
|
|
380
|
-
messageId: null,
|
|
381
|
-
sessionId: null,
|
|
382
|
-
runId: null,
|
|
383
|
-
workflowId: null,
|
|
384
|
-
events: [],
|
|
385
|
-
output: '',
|
|
386
|
-
outputTruncated: false,
|
|
387
|
-
error: null,
|
|
388
|
-
};
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
function pushActivityEvent(activity, text) {
|
|
392
|
-
const value = String(text || '').trim();
|
|
393
|
-
if (!value) return;
|
|
394
|
-
if (activity.events.at(-1) === value) return;
|
|
395
|
-
activity.events.push(value);
|
|
396
|
-
if (activity.events.length > 8) activity.events.splice(0, activity.events.length - 8);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
function activityTitle(activity) {
|
|
400
|
-
if (activity.type === 'workflow') return t(activity.lang, 'control.activity.workflowTitle');
|
|
401
|
-
if (activity.type === 'router') return t(activity.lang, 'control.activity.routerTitle');
|
|
402
|
-
return t(activity.lang, 'control.activity.agentTitle');
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
function trimTelegramOutput(text, max, suffix = '') {
|
|
406
|
-
const value = String(text || '').trim();
|
|
407
|
-
const ending = String(suffix || '').trim();
|
|
408
|
-
if (value.length <= max) return value;
|
|
409
|
-
const room = Math.max(300, max - ending.length - 4);
|
|
410
|
-
return `${value.slice(0, room).trim()}\n\n${ending}`;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
function appendActivityOutput(activity, chunk) {
|
|
414
|
-
if (!activity) return;
|
|
415
|
-
const next = appendBoundedText(activity.output, chunk);
|
|
416
|
-
activity.output = next.text;
|
|
417
|
-
activity.outputTruncated = Boolean(activity.outputTruncated || next.truncated);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
function renderActivity(activity, { finalText = null } = {}) {
|
|
421
|
-
const output = finalText || activity.output;
|
|
422
|
-
if (activity.type === 'agent' && output && !activity.error) {
|
|
423
|
-
if (activity.status === 'done') {
|
|
424
|
-
return trimTelegramOutput(
|
|
425
|
-
output,
|
|
426
|
-
3400,
|
|
427
|
-
activity.outputTruncated
|
|
428
|
-
? t(activity.lang, 'control.activity.outputHistoryTrimmed')
|
|
429
|
-
: t(activity.lang, 'control.activity.outputTooLong'),
|
|
430
|
-
);
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
const footer = `⏳ ${t(activity.lang, 'control.activity.liveFooter', { elapsed: formatElapsed(activity.startedAt) })}`;
|
|
434
|
-
const body = trimTelegramOutput(
|
|
435
|
-
output,
|
|
436
|
-
3200,
|
|
437
|
-
t(activity.lang, 'control.activity.outputShortened'),
|
|
438
|
-
);
|
|
439
|
-
return truncate(`${body}\n\n${footer}`, 3400);
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
const lines = [
|
|
443
|
-
`${activity.status === 'failed' ? '❌' : activity.status === 'done' ? '✅' : activity.status === 'running' ? '🔧' : '⏳'} ${activityTitle(activity)}`,
|
|
444
|
-
'',
|
|
445
|
-
`🤖 ${t(activity.lang, 'control.activity.provider')}: ${activity.provider || '-'}`,
|
|
446
|
-
`📁 ${t(activity.lang, 'control.activity.project')}: ${compact(activity.project, 90)}`,
|
|
447
|
-
];
|
|
448
|
-
|
|
449
|
-
if (activity.sessionId) lines.push(`🧵 ${t(activity.lang, 'control.activity.session')}: ${activity.sessionId}`);
|
|
450
|
-
if (activity.runId) lines.push(`🧭 ${t(activity.lang, 'control.activity.run')}: ${activity.runId}`);
|
|
451
|
-
if (activity.workflowId) lines.push(`🧩 ${t(activity.lang, 'control.activity.workflow')}: ${activity.workflowId}`);
|
|
452
|
-
lines.push(`⏱ ${t(activity.lang, 'control.activity.elapsed')}: ${formatElapsed(activity.startedAt)}`);
|
|
453
|
-
lines.push(`📌 ${t(activity.lang, 'control.activity.status')}: ${activity.phase}`);
|
|
454
|
-
|
|
455
|
-
const visibleEvents = activity.mode === 'final'
|
|
456
|
-
? activity.events.slice(-4)
|
|
457
|
-
: activity.events;
|
|
458
|
-
if (visibleEvents.length > 0) {
|
|
459
|
-
lines.push('', `🛠 ${t(activity.lang, 'control.activity.work')}:`);
|
|
460
|
-
for (const event of visibleEvents) lines.push(`• ${event}`);
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
if (activity.error) {
|
|
464
|
-
lines.push('', `⚠️ ${truncate(activity.error, 700)}`);
|
|
465
|
-
} else if (output) {
|
|
466
|
-
lines.push('', `💬 ${t(activity.lang, 'control.activity.output')}:`);
|
|
467
|
-
lines.push(truncate(output, 1800));
|
|
468
|
-
} else if (activity.prompt) {
|
|
469
|
-
lines.push('', `📝 ${compact(activity.prompt, 220)}`);
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
return truncate(lines.join('\n'), 3400);
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
async function createTelegramActivity({ bot, chatId, link, type = 'agent', prompt = '', phase = null }) {
|
|
476
|
-
const lang = languageFor(link);
|
|
477
|
-
const state = getState(link.user_id);
|
|
478
|
-
const activity = createActivityState({
|
|
479
|
-
lang,
|
|
480
|
-
type,
|
|
481
|
-
provider: resolveTelegramProvider(state),
|
|
482
|
-
project: projectLabel(state),
|
|
483
|
-
prompt,
|
|
484
|
-
mode: state.progressMode,
|
|
485
|
-
});
|
|
486
|
-
if (phase) activity.phase = phase;
|
|
487
|
-
const sent = await send(bot, chatId, renderActivity(activity), { parse_mode: undefined });
|
|
488
|
-
activity.messageId = sent?.message_id || sent?.message?.message_id || null;
|
|
489
|
-
activity.lastEditAt = Date.now();
|
|
490
|
-
return activity;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
async function editTelegramActivity({ bot, chatId, activity, force = false, reply_markup }) {
|
|
494
|
-
if (!activity) return null;
|
|
495
|
-
const now = Date.now();
|
|
496
|
-
if (!force && now - activity.lastEditAt < ACTIVITY_EDIT_THROTTLE_MS) return null;
|
|
497
|
-
const sent = await send(bot, chatId, renderActivity(activity), {
|
|
498
|
-
editMessageId: activity.messageId,
|
|
499
|
-
parse_mode: undefined,
|
|
500
|
-
reply_markup,
|
|
501
|
-
});
|
|
502
|
-
activity.messageId = sent?.message_id || sent?.message?.message_id || activity.messageId;
|
|
503
|
-
activity.lastEditAt = now;
|
|
504
|
-
return sent;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
function startActivityHeartbeat({ bot, chatId, activity }) {
|
|
508
|
-
return setInterval(() => {
|
|
509
|
-
if (!activity || activity.status === 'done' || activity.status === 'failed') return;
|
|
510
|
-
if (activity.status === 'starting') {
|
|
511
|
-
activity.status = 'running';
|
|
512
|
-
activity.phase = t(activity.lang, 'control.activity.thinking');
|
|
513
|
-
}
|
|
514
|
-
editTelegramActivity({ bot, chatId, activity }).catch((error) => {
|
|
515
|
-
console.warn('[telegram-control] activity heartbeat failed:', error?.message || error);
|
|
516
|
-
});
|
|
517
|
-
}, ACTIVITY_HEARTBEAT_MS);
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
function stateSummary(lang, state) {
|
|
521
|
-
const lines = [
|
|
522
|
-
`${t(lang, 'control.summary.project')}: ${state.selectedProjectName || t(lang, 'control.notSelected')}`,
|
|
523
|
-
`${t(lang, 'control.summary.provider')}: ${state.selectedProvider}${state.selectedModel ? ` / ${state.selectedModel}` : ''}`,
|
|
524
|
-
`${t(lang, 'control.summary.workflow')}: ${state.selectedWorkflowId || t(lang, 'control.notSelected')}`,
|
|
525
|
-
`${t(lang, 'control.summary.progress')}: ${state.progressMode}`,
|
|
526
|
-
];
|
|
527
|
-
if (state.activeTerminal) {
|
|
528
|
-
lines.push(`${t(lang, 'control.summary.terminal')}: ${state.activeTerminal.provider} / ${state.activeTerminal.projectLabel || state.activeTerminal.projectName || compact(state.activeTerminal.projectPath, 50)}`);
|
|
529
|
-
}
|
|
530
|
-
return lines.join('\n');
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
function terminalControlKeyboard(lang) {
|
|
534
|
-
return [
|
|
535
|
-
[button(t(lang, 'control.button.terminalRefresh'), 'terminal_status'), button(t(lang, 'control.button.detachTerminal'), 'detach_terminal')],
|
|
536
|
-
[button(t(lang, 'control.button.mainMenu'), 'menu')],
|
|
537
|
-
];
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
function mainMenuKeyboard(lang, state = null) {
|
|
541
|
-
const keyboard = [
|
|
542
|
-
[button(t(lang, 'control.button.projects'), 'projects'), button(t(lang, 'control.button.provider'), 'providers')],
|
|
543
|
-
[button(t(lang, 'control.button.models'), 'models'), button(t(lang, 'control.button.workflows'), 'workflows')],
|
|
544
|
-
[button(t(lang, 'control.button.runs'), 'runs'), button(t(lang, 'control.button.approvals'), 'approvals')],
|
|
545
|
-
[button(t(lang, 'control.button.controlRoom'), 'control_room'), button(t(lang, 'control.button.webhooks'), 'webhooks')],
|
|
546
|
-
[button(t(lang, 'control.button.sessions'), 'sessions'), button(t(lang, 'control.button.newChat'), 'new_chat')],
|
|
547
|
-
[button(t(lang, 'control.button.install'), 'install_menu'), button(t(lang, 'control.button.auth'), 'auth_menu')],
|
|
548
|
-
[button(t(lang, 'control.button.settings'), 'settings')],
|
|
549
|
-
];
|
|
550
|
-
if (state?.activeTerminal) {
|
|
551
|
-
keyboard.splice(1, 0, [button(t(lang, 'control.button.terminal'), 'terminal_status'), button(t(lang, 'control.button.detachTerminal'), 'detach_terminal')]);
|
|
552
|
-
}
|
|
553
|
-
return keyboard;
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
export async function showMainMenu({ bot, chatId, link, editMessageId, notice }) {
|
|
557
|
-
const lang = languageFor(link);
|
|
558
|
-
const state = getState(link.user_id);
|
|
559
|
-
const prefix = notice ? `${notice}\n\n` : '';
|
|
560
|
-
await send(bot, chatId, `${prefix}${t(lang, 'control.menu')}\n\n${stateSummary(lang, state)}`, {
|
|
561
|
-
editMessageId,
|
|
562
|
-
reply_markup: { inline_keyboard: mainMenuKeyboard(lang, state) },
|
|
563
|
-
});
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
async function showHelp({ bot, chatId, link }) {
|
|
567
|
-
const lang = languageFor(link);
|
|
568
|
-
await send(bot, chatId, t(lang, 'control.help'));
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
async function showCommandPalette({ bot, chatId, link, editMessageId, unknown = false }) {
|
|
572
|
-
const lang = languageFor(link);
|
|
573
|
-
const prefix = unknown ? `${t(lang, 'control.unknownCommand')}\n\n` : '';
|
|
574
|
-
await send(bot, chatId, `${prefix}${t(lang, 'control.help')}\n\n${t(lang, 'control.examples')}`, {
|
|
575
|
-
editMessageId,
|
|
576
|
-
reply_markup: { inline_keyboard: mainMenuKeyboard(lang, getState(link.user_id)) },
|
|
577
|
-
});
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
function getActiveTerminal(state) {
|
|
581
|
-
const terminal = state?.activeTerminal;
|
|
582
|
-
if (
|
|
583
|
-
!terminal ||
|
|
584
|
-
!PROVIDERS.includes(terminal.provider) ||
|
|
585
|
-
typeof terminal.projectPath !== 'string' ||
|
|
586
|
-
!terminal.projectPath.trim()
|
|
587
|
-
) {
|
|
588
|
-
return null;
|
|
589
|
-
}
|
|
590
|
-
return terminal;
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
function terminalProjectLabel(terminal) {
|
|
594
|
-
return terminal?.projectLabel || terminal?.projectName || terminal?.projectPath || '-';
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
function terminalOutputUrl(terminal, maxChars = 3200, sinceCursor = null) {
|
|
598
|
-
const params = new URLSearchParams({
|
|
599
|
-
provider: terminal.provider,
|
|
600
|
-
projectPath: terminal.projectPath,
|
|
601
|
-
maxChars: String(maxChars),
|
|
602
|
-
});
|
|
603
|
-
if (terminal.tabId) params.set('tabId', terminal.tabId);
|
|
604
|
-
if (terminal.sessionId) params.set('sessionId', terminal.sessionId);
|
|
605
|
-
if (Number.isFinite(sinceCursor)) params.set('sinceCursor', String(sinceCursor));
|
|
606
|
-
return `/api/shell/sessions/provider-output?${params.toString()}`;
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
function renderTerminalSnapshot(lang, terminal, data, { prefix = '', includeOutput = false } = {}) {
|
|
610
|
-
const active = data?.active !== false;
|
|
611
|
-
const lifecycle = data?.terminalState || data?.lifecycleState || (active ? 'running' : 'not running');
|
|
612
|
-
const output = String(data?.output || '').trim();
|
|
613
|
-
const lines = [
|
|
614
|
-
prefix || t(lang, active ? 'control.terminalAttached' : 'control.terminalNotRunning'),
|
|
615
|
-
'',
|
|
616
|
-
`🤖 ${t(lang, 'control.activity.provider')}: ${terminal.provider}`,
|
|
617
|
-
`📁 ${t(lang, 'control.activity.project')}: ${compact(terminalProjectLabel(terminal), 90)}`,
|
|
618
|
-
`📌 ${t(lang, 'control.activity.status')}: ${lifecycle}`,
|
|
619
|
-
];
|
|
620
|
-
if (terminal.sessionId || data?.sessionId) {
|
|
621
|
-
lines.push(`🧵 ${t(lang, 'control.activity.session')}: ${terminal.sessionId || data.sessionId}`);
|
|
622
|
-
}
|
|
623
|
-
if (includeOutput && output) {
|
|
624
|
-
lines.push('', `💬 ${t(lang, 'control.activity.output')}:`);
|
|
625
|
-
lines.push(truncate(output, 2400));
|
|
626
|
-
} else {
|
|
627
|
-
lines.push('', t(lang, 'control.terminalOutputHidden'));
|
|
628
|
-
}
|
|
629
|
-
return truncate(lines.join('\n'), 3400);
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
function terminalBridgeMonitorKey(chatId, terminal) {
|
|
633
|
-
return [
|
|
634
|
-
chatId,
|
|
635
|
-
terminal.provider,
|
|
636
|
-
terminal.projectPath,
|
|
637
|
-
terminal.tabId || '-',
|
|
638
|
-
terminal.sessionId || '-',
|
|
639
|
-
].join(':');
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
function escapeRegExp(value) {
|
|
643
|
-
return String(value || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
function terminalBridgeInput(text, lang) {
|
|
647
|
-
const prompt = String(text || '').trim();
|
|
648
|
-
const instruction = lang === 'tr'
|
|
649
|
-
? `Pixcode Telegram senkronu: Yanıtının sonunda Telegram için okunabilir ve eksiksiz final cevabını ${TERMINAL_BRIDGE_FINAL_OPEN} ve ${TERMINAL_BRIDGE_FINAL_CLOSE} etiketleri arasına yaz. Etiketlerin içine spinner, terminal ekranı veya tekrar eden durum satırı koyma.`
|
|
650
|
-
: `Pixcode Telegram sync: At the end of your response, write the readable complete final answer for Telegram between ${TERMINAL_BRIDGE_FINAL_OPEN} and ${TERMINAL_BRIDGE_FINAL_CLOSE}. Do not put spinners, terminal screen text, or repeated status lines inside the tags.`;
|
|
651
|
-
return `${prompt}\n\n${instruction}`;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
function normalizeBridgeLine(value) {
|
|
655
|
-
return String(value || '').replace(/\s+/g, ' ').trim();
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
function terminalBridgeLabels(terminal) {
|
|
659
|
-
const labels = new Set();
|
|
660
|
-
for (const value of [
|
|
661
|
-
terminal?.provider,
|
|
662
|
-
terminal?.projectName,
|
|
663
|
-
terminal?.projectLabel,
|
|
664
|
-
terminal?.projectPath,
|
|
665
|
-
]) {
|
|
666
|
-
const normalized = normalizeBridgeLine(value);
|
|
667
|
-
if (!normalized) continue;
|
|
668
|
-
labels.add(normalized);
|
|
669
|
-
const pathParts = normalized.split(/[\\/]/u).filter(Boolean);
|
|
670
|
-
const basename = pathParts.at(-1);
|
|
671
|
-
if (basename) labels.add(basename);
|
|
672
|
-
}
|
|
673
|
-
return [...labels]
|
|
674
|
-
.map((value) => value.trim())
|
|
675
|
-
.filter((value) => value.length >= 2)
|
|
676
|
-
.sort((a, b) => b.length - a.length);
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
function stripTerminalBridgeFragments(value, terminal) {
|
|
680
|
-
let text = String(value || '');
|
|
681
|
-
text = text.replace(/\[[0-?]{1,32}[ -/]*[@-~]/gu, ' ');
|
|
682
|
-
text = text.replace(/\b\d{1,3}(?:;\d{1,3}){1,8}[A-Za-z]/gu, ' ');
|
|
683
|
-
text = text.replace(/\b[012];[^\n\r]{0,100}[⠂⠐⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏✶✻✽✢✳][^\n\r]{0,100}/gu, ' ');
|
|
684
|
-
|
|
685
|
-
for (const label of terminalBridgeLabels(terminal)) {
|
|
686
|
-
const labelPattern = escapeRegExp(label).replace(/\s+/gu, '\\s+');
|
|
687
|
-
const titlePattern = new RegExp(
|
|
688
|
-
String.raw`\b(?:\d+[a-z])?[012];[⠂⠐⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏✶✻✽✢✳•*·\s-]*${labelPattern}(?:\d+;?)?`,
|
|
689
|
-
'giu',
|
|
690
|
-
);
|
|
691
|
-
text = text.replace(titlePattern, ' ');
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
return text
|
|
695
|
-
.replace(/[⠂⠐⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏✶✻✽✢✳]+/gu, ' ')
|
|
696
|
-
.replace(/\b(?:Working|Determining|Thinking|Running)(?:\s*•\s*(?:Working|Determining|Thinking|Running))+\b/giu, ' ');
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
function extractTerminalBridgeFinalBlock(text) {
|
|
700
|
-
const blocks = [];
|
|
701
|
-
const blockPattern = /(?:<|\[)\s*PIXCODE_TELEGRAM_FINAL\s*(?:>|\])([\s\S]*?)(?:<|\[)\s*\/\s*PIXCODE_TELEGRAM_FINAL\s*(?:>|\])/giu;
|
|
702
|
-
for (const match of text.matchAll(blockPattern)) {
|
|
703
|
-
const block = String(match[1] || '').trim();
|
|
704
|
-
if (block) blocks.push(block);
|
|
705
|
-
}
|
|
706
|
-
if (blocks.length > 0) return blocks.at(-1);
|
|
707
|
-
|
|
708
|
-
const openPattern = /(?:<|\[)\s*PIXCODE_TELEGRAM_FINAL\s*(?:>|\])/giu;
|
|
709
|
-
let lastOpen = null;
|
|
710
|
-
for (const match of text.matchAll(openPattern)) lastOpen = match;
|
|
711
|
-
if (!lastOpen) return '';
|
|
712
|
-
return text.slice((lastOpen.index || 0) + lastOpen[0].length).trim();
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
function isNoisyTerminalBridgeLine(line, prompt, terminal) {
|
|
716
|
-
const normalized = normalizeBridgeLine(line);
|
|
717
|
-
if (!normalized || normalized.length <= 1) return true;
|
|
718
|
-
if (/PIXCODE_TELEGRAM_FINAL/iu.test(normalized)) return true;
|
|
719
|
-
if (/Pixcode Telegram (?:sync|senkronu)/iu.test(normalized)) return true;
|
|
720
|
-
if (/^[╭╮╰╯│─═┌┐└┘├┤┬┴┼\s]+$/u.test(normalized)) return true;
|
|
721
|
-
if (/^[●✶✻✽✢✳⠂⠐⏵·\s0;:()\-|/\\]+$/u.test(normalized)) return true;
|
|
722
|
-
if (/^\[[0-?]{1,32}[ -/]*[@-~]/u.test(normalized)) return true;
|
|
723
|
-
if (/^(?:\d+[a-z])?[012];/iu.test(normalized) && /[⠂⠐⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏✶✻✽✢✳]/u.test(normalized)) return true;
|
|
724
|
-
if ((normalized.match(/[⠂⠐⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏✶✻✽✢✳]/gu) || []).length >= 2) return true;
|
|
725
|
-
|
|
726
|
-
const lower = normalized.toLowerCase();
|
|
727
|
-
const promptNeedle = normalizeBridgeLine(prompt).toLowerCase();
|
|
728
|
-
if (promptNeedle && lower.includes(promptNeedle.slice(0, 120))) return true;
|
|
729
|
-
if (/^[›❯>]\s*/u.test(normalized)) return true;
|
|
730
|
-
if (lower.includes('welcome back')) return true;
|
|
731
|
-
if (lower.includes('api usage billing')) return true;
|
|
732
|
-
if (lower.includes('bypass permissions')) return true;
|
|
733
|
-
if (lower.includes('try "')) return true;
|
|
734
|
-
if (lower.includes('esc to interrupt') || lower.includes('press esc')) return true;
|
|
735
|
-
if (lower.includes('/effort')) return true;
|
|
736
|
-
if (lower.includes('determining')) return true;
|
|
737
|
-
if (/^(?:claude code|codex)\b/i.test(normalized) && normalized.length < 80) return true;
|
|
738
|
-
if (/^\(?\d+s\s*·\s*[↓↑]?\d*\s*tokens?\)?$/iu.test(normalized)) return true;
|
|
739
|
-
for (const label of terminalBridgeLabels(terminal)) {
|
|
740
|
-
if (
|
|
741
|
-
lower.includes(label.toLowerCase())
|
|
742
|
-
&& /(?:^[012];|[⠂⠐⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏✶✻✽✢✳]|working|determining)/iu.test(normalized)
|
|
743
|
-
) {
|
|
744
|
-
return true;
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
return false;
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
export function cleanTerminalBridgeOutput(output, prompt, terminal = null) {
|
|
751
|
-
const text = stripTerminalBridgeFragments(String(output || ''), terminal)
|
|
752
|
-
.replace(/\r/g, '\n')
|
|
753
|
-
.replace(/[\x00-\x08\x0B-\x1F\x7F]/g, '')
|
|
754
|
-
.replace(/\u00a0/g, ' ');
|
|
755
|
-
const finalBlock = extractTerminalBridgeFinalBlock(text);
|
|
756
|
-
const source = finalBlock || text;
|
|
757
|
-
const lines = source
|
|
758
|
-
.split('\n')
|
|
759
|
-
.map((line) => line.replace(/[ \t]+/g, ' ').trim())
|
|
760
|
-
.filter((line) => !isNoisyTerminalBridgeLine(line, prompt, terminal));
|
|
761
|
-
const deduped = [];
|
|
762
|
-
for (const line of lines) {
|
|
763
|
-
if (deduped.at(-1) === line) continue;
|
|
764
|
-
deduped.push(line);
|
|
765
|
-
}
|
|
766
|
-
return deduped.join('\n').trim();
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
function renderTerminalBridgeProgress(lang, terminal, {
|
|
770
|
-
output = '',
|
|
771
|
-
statusKey = 'control.terminalWaiting',
|
|
772
|
-
startedAt = Date.now(),
|
|
773
|
-
final = false,
|
|
774
|
-
terminalState = null,
|
|
775
|
-
} = {}) {
|
|
776
|
-
const lines = [
|
|
777
|
-
`${final ? '✅' : '⏳'} ${t(lang, statusKey)}`,
|
|
778
|
-
'',
|
|
779
|
-
`🤖 ${t(lang, 'control.activity.provider')}: ${terminal.provider}`,
|
|
780
|
-
`📁 ${t(lang, 'control.activity.project')}: ${compact(terminalProjectLabel(terminal), 90)}`,
|
|
781
|
-
];
|
|
782
|
-
if (terminal.sessionId) {
|
|
783
|
-
lines.push(`🧵 ${t(lang, 'control.activity.session')}: ${terminal.sessionId}`);
|
|
784
|
-
}
|
|
785
|
-
if (terminalState && terminalState !== 'unknown') {
|
|
786
|
-
lines.push(`📌 ${t(lang, 'control.activity.status')}: ${terminalState}`);
|
|
787
|
-
}
|
|
788
|
-
if (output) {
|
|
789
|
-
lines.push('', `💬 ${t(lang, 'control.activity.output')}:`);
|
|
790
|
-
lines.push(truncate(output, final ? 3100 : 2200));
|
|
791
|
-
} else {
|
|
792
|
-
lines.push('', t(lang, 'control.terminalWaitingHint'));
|
|
793
|
-
}
|
|
794
|
-
if (!final) {
|
|
795
|
-
lines.push('', `⏱ ${t(lang, 'control.activity.elapsed')}: ${formatElapsed(startedAt)}`);
|
|
796
|
-
}
|
|
797
|
-
return truncate(lines.join('\n'), 3400);
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
async function monitorTerminalBridgeResponse({
|
|
801
|
-
bot,
|
|
802
|
-
chatId,
|
|
803
|
-
link,
|
|
804
|
-
terminal,
|
|
805
|
-
prompt,
|
|
806
|
-
sinceCursor,
|
|
807
|
-
editMessageId,
|
|
808
|
-
monitorKey,
|
|
809
|
-
monitorToken,
|
|
810
|
-
}) {
|
|
811
|
-
const lang = languageFor(link);
|
|
812
|
-
const startedAt = Date.now();
|
|
813
|
-
let lastCleanOutput = '';
|
|
814
|
-
let lastOutputChangeAt = startedAt;
|
|
815
|
-
let lastEditAt = 0;
|
|
816
|
-
|
|
817
|
-
const isCurrent = () => terminalBridgeMonitors.get(monitorKey) === monitorToken;
|
|
818
|
-
|
|
819
|
-
try {
|
|
820
|
-
while (isCurrent() && Date.now() - startedAt < TERMINAL_BRIDGE_TIMEOUT_MS) {
|
|
821
|
-
await new Promise((resolve) => setTimeout(resolve, TERMINAL_BRIDGE_POLL_MS));
|
|
822
|
-
if (!isCurrent()) return;
|
|
823
|
-
|
|
824
|
-
const data = await localApi(
|
|
825
|
-
link.user_id,
|
|
826
|
-
terminalOutputUrl(terminal, 12000, sinceCursor),
|
|
827
|
-
{ timeoutMs: 12_000 },
|
|
828
|
-
);
|
|
829
|
-
if (data?.active === false) {
|
|
830
|
-
await send(bot, chatId, renderTerminalBridgeProgress(lang, terminal, {
|
|
831
|
-
output: lastCleanOutput,
|
|
832
|
-
statusKey: 'control.terminalNotRunning',
|
|
833
|
-
startedAt,
|
|
834
|
-
final: true,
|
|
835
|
-
terminalState: 'not running',
|
|
836
|
-
}), {
|
|
837
|
-
editMessageId,
|
|
838
|
-
parse_mode: undefined,
|
|
839
|
-
reply_markup: { inline_keyboard: terminalControlKeyboard(lang) },
|
|
840
|
-
});
|
|
841
|
-
return;
|
|
842
|
-
}
|
|
843
|
-
const cleanOutput = cleanTerminalBridgeOutput(data?.output, prompt, terminal);
|
|
844
|
-
const terminalState = data?.terminalState || data?.lifecycleState || 'unknown';
|
|
845
|
-
const now = Date.now();
|
|
846
|
-
|
|
847
|
-
if (cleanOutput && cleanOutput !== lastCleanOutput) {
|
|
848
|
-
lastCleanOutput = cleanOutput;
|
|
849
|
-
lastOutputChangeAt = now;
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
const finishedByState = ['idle', 'completed', 'failed', 'exited'].includes(terminalState);
|
|
853
|
-
const finishedByQuietOutput = Boolean(lastCleanOutput) && now - lastOutputChangeAt >= TERMINAL_BRIDGE_SETTLE_MS;
|
|
854
|
-
const shouldFinish = Boolean(lastCleanOutput) && (finishedByState || finishedByQuietOutput);
|
|
855
|
-
|
|
856
|
-
if (shouldFinish) {
|
|
857
|
-
await send(bot, chatId, renderTerminalBridgeProgress(lang, terminal, {
|
|
858
|
-
output: lastCleanOutput,
|
|
859
|
-
statusKey: 'control.terminalResponseReady',
|
|
860
|
-
startedAt,
|
|
861
|
-
final: true,
|
|
862
|
-
terminalState,
|
|
863
|
-
}), {
|
|
864
|
-
editMessageId,
|
|
865
|
-
parse_mode: undefined,
|
|
866
|
-
reply_markup: { inline_keyboard: terminalControlKeyboard(lang) },
|
|
867
|
-
});
|
|
868
|
-
return;
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
if (now - lastEditAt >= TERMINAL_BRIDGE_EDIT_THROTTLE_MS) {
|
|
872
|
-
lastEditAt = now;
|
|
873
|
-
await send(bot, chatId, renderTerminalBridgeProgress(lang, terminal, {
|
|
874
|
-
output: lastCleanOutput,
|
|
875
|
-
statusKey: lastCleanOutput ? 'control.terminalResponding' : 'control.terminalWaiting',
|
|
876
|
-
startedAt,
|
|
877
|
-
terminalState,
|
|
878
|
-
}), {
|
|
879
|
-
editMessageId,
|
|
880
|
-
parse_mode: undefined,
|
|
881
|
-
reply_markup: { inline_keyboard: terminalControlKeyboard(lang) },
|
|
882
|
-
});
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
if (!isCurrent()) return;
|
|
887
|
-
await send(bot, chatId, renderTerminalBridgeProgress(lang, terminal, {
|
|
888
|
-
output: lastCleanOutput,
|
|
889
|
-
statusKey: lastCleanOutput ? 'control.terminalStillRunning' : 'control.terminalNoReadableOutput',
|
|
890
|
-
startedAt,
|
|
891
|
-
final: Boolean(lastCleanOutput),
|
|
892
|
-
terminalState: 'running',
|
|
893
|
-
}), {
|
|
894
|
-
editMessageId,
|
|
895
|
-
parse_mode: undefined,
|
|
896
|
-
reply_markup: { inline_keyboard: terminalControlKeyboard(lang) },
|
|
897
|
-
});
|
|
898
|
-
} finally {
|
|
899
|
-
if (isCurrent()) terminalBridgeMonitors.delete(monitorKey);
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
export async function sendActiveTerminalAttachedNotice({ bot, chatId, link, terminal }) {
|
|
904
|
-
const lang = languageFor(link);
|
|
905
|
-
const lines = [
|
|
906
|
-
t(lang, 'control.terminalAttached'),
|
|
907
|
-
'',
|
|
908
|
-
`🤖 ${t(lang, 'control.activity.provider')}: ${terminal.provider}`,
|
|
909
|
-
`📁 ${t(lang, 'control.activity.project')}: ${compact(terminalProjectLabel(terminal), 90)}`,
|
|
910
|
-
`📌 ${t(lang, 'control.activity.status')}: ${t(lang, 'control.terminalReadyStatus')}`,
|
|
911
|
-
];
|
|
912
|
-
if (terminal.sessionId) {
|
|
913
|
-
lines.push(`🧵 ${t(lang, 'control.activity.session')}: ${terminal.sessionId}`);
|
|
914
|
-
}
|
|
915
|
-
lines.push('', t(lang, 'control.terminalReadyPrompt'));
|
|
916
|
-
await send(bot, chatId, truncate(lines.join('\n'), 3400), {
|
|
917
|
-
parse_mode: undefined,
|
|
918
|
-
reply_markup: { inline_keyboard: terminalControlKeyboard(lang) },
|
|
919
|
-
});
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
async function showActiveTerminalStatus({ bot, chatId, link, editMessageId }) {
|
|
923
|
-
const lang = languageFor(link);
|
|
924
|
-
const state = getState(link.user_id);
|
|
925
|
-
const terminal = getActiveTerminal(state);
|
|
926
|
-
if (!terminal) {
|
|
927
|
-
await send(bot, chatId, t(lang, 'control.noActiveTerminal'), {
|
|
928
|
-
editMessageId,
|
|
929
|
-
reply_markup: { inline_keyboard: [[button(t(lang, 'control.button.mainMenu'), 'menu')]] },
|
|
930
|
-
});
|
|
931
|
-
return;
|
|
932
|
-
}
|
|
933
|
-
try {
|
|
934
|
-
const data = await localApi(link.user_id, terminalOutputUrl(terminal), { timeoutMs: 12_000 });
|
|
935
|
-
await send(bot, chatId, renderTerminalSnapshot(lang, terminal, data), {
|
|
936
|
-
editMessageId,
|
|
937
|
-
parse_mode: undefined,
|
|
938
|
-
reply_markup: { inline_keyboard: terminalControlKeyboard(lang) },
|
|
939
|
-
});
|
|
940
|
-
} catch (error) {
|
|
941
|
-
await send(bot, chatId, t(lang, 'control.terminalStatusFailed', { error: error?.message || String(error) }), {
|
|
942
|
-
editMessageId,
|
|
943
|
-
reply_markup: { inline_keyboard: terminalControlKeyboard(lang) },
|
|
944
|
-
});
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
async function detachActiveTerminal({ bot, chatId, link, editMessageId }) {
|
|
949
|
-
const lang = languageFor(link);
|
|
950
|
-
updateTelegramControlState(link.user_id, { activeTerminal: null });
|
|
951
|
-
await send(bot, chatId, t(lang, 'control.terminalDetached'), {
|
|
952
|
-
editMessageId,
|
|
953
|
-
reply_markup: { inline_keyboard: [[button(t(lang, 'control.button.mainMenu'), 'menu')]] },
|
|
954
|
-
});
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
async function sendToActiveTerminal({ bot, chatId, link, text }) {
|
|
958
|
-
const lang = languageFor(link);
|
|
959
|
-
const state = getState(link.user_id);
|
|
960
|
-
const terminal = getActiveTerminal(state);
|
|
961
|
-
if (!terminal) return false;
|
|
962
|
-
if (state.remoteControlEnabled === false) {
|
|
963
|
-
await send(bot, chatId, t(lang, 'control.disabled'));
|
|
964
|
-
return true;
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
const sent = await send(bot, chatId, t(lang, 'control.terminalSending', {
|
|
968
|
-
provider: terminal.provider,
|
|
969
|
-
project: terminalProjectLabel(terminal),
|
|
970
|
-
}), {
|
|
971
|
-
parse_mode: undefined,
|
|
972
|
-
reply_markup: { inline_keyboard: terminalControlKeyboard(lang) },
|
|
973
|
-
});
|
|
974
|
-
const editMessageId = sent?.message_id || sent?.message?.message_id || null;
|
|
975
|
-
|
|
976
|
-
try {
|
|
977
|
-
const terminalPrompt = terminalBridgeInput(text, lang);
|
|
978
|
-
const inputResult = await localApi(link.user_id, '/api/shell/sessions/provider-input', {
|
|
979
|
-
method: 'POST',
|
|
980
|
-
timeoutMs: 15_000,
|
|
981
|
-
body: {
|
|
982
|
-
provider: terminal.provider,
|
|
983
|
-
projectPath: terminal.projectPath,
|
|
984
|
-
tabId: terminal.tabId,
|
|
985
|
-
sessionId: terminal.sessionId,
|
|
986
|
-
input: terminalPrompt,
|
|
987
|
-
submit: true,
|
|
988
|
-
submitMode: 'deferred-enter',
|
|
989
|
-
},
|
|
990
|
-
});
|
|
991
|
-
const sinceCursor = Number.isFinite(inputResult?.outputCursorBefore)
|
|
992
|
-
? inputResult.outputCursorBefore
|
|
993
|
-
: null;
|
|
994
|
-
const monitorKey = terminalBridgeMonitorKey(chatId, terminal);
|
|
995
|
-
const monitorToken = crypto.randomUUID();
|
|
996
|
-
terminalBridgeMonitors.set(monitorKey, monitorToken);
|
|
997
|
-
|
|
998
|
-
await send(bot, chatId, renderTerminalBridgeProgress(lang, terminal, {
|
|
999
|
-
statusKey: 'control.terminalWaiting',
|
|
1000
|
-
startedAt: Date.now(),
|
|
1001
|
-
terminalState: 'running',
|
|
1002
|
-
}), {
|
|
1003
|
-
editMessageId,
|
|
1004
|
-
parse_mode: undefined,
|
|
1005
|
-
reply_markup: { inline_keyboard: terminalControlKeyboard(lang) },
|
|
1006
|
-
});
|
|
1007
|
-
monitorTerminalBridgeResponse({
|
|
1008
|
-
bot,
|
|
1009
|
-
chatId,
|
|
1010
|
-
link,
|
|
1011
|
-
terminal,
|
|
1012
|
-
prompt: terminalPrompt,
|
|
1013
|
-
sinceCursor,
|
|
1014
|
-
editMessageId,
|
|
1015
|
-
monitorKey,
|
|
1016
|
-
monitorToken,
|
|
1017
|
-
}).catch((error) => {
|
|
1018
|
-
console.warn('[telegram-control] terminal bridge monitor failed:', error?.message || error);
|
|
1019
|
-
});
|
|
1020
|
-
} catch (error) {
|
|
1021
|
-
await send(bot, chatId, t(lang, 'control.terminalSendFailed', {
|
|
1022
|
-
error: error?.message || String(error),
|
|
1023
|
-
}), {
|
|
1024
|
-
editMessageId,
|
|
1025
|
-
reply_markup: { inline_keyboard: terminalControlKeyboard(lang) },
|
|
1026
|
-
});
|
|
1027
|
-
}
|
|
1028
|
-
return true;
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
async function listProjects() {
|
|
1032
|
-
const projects = await getProjects();
|
|
1033
|
-
return projects.slice(0, 20);
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
async function showProjectMenu({ bot, chatId, link, editMessageId, notice }) {
|
|
1037
|
-
const lang = languageFor(link);
|
|
1038
|
-
const projects = await listProjects();
|
|
1039
|
-
if (projects.length === 0) {
|
|
1040
|
-
const prefix = notice ? `${notice}\n\n` : '';
|
|
1041
|
-
await send(bot, chatId, `${prefix}${t(lang, 'control.noProjects')}`, { editMessageId });
|
|
1042
|
-
return;
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
const buttons = projects.map((project, index) => button(
|
|
1046
|
-
`${index + 1}. ${compact(project.displayName || project.name, 34)}`,
|
|
1047
|
-
'project_select',
|
|
1048
|
-
{
|
|
1049
|
-
name: project.name,
|
|
1050
|
-
path: project.fullPath || project.path,
|
|
1051
|
-
displayName: project.displayName || project.name,
|
|
1052
|
-
},
|
|
1053
|
-
));
|
|
1054
|
-
const prefix = notice ? `${notice}\n\n` : '';
|
|
1055
|
-
await send(bot, chatId, `${prefix}${t(lang, 'control.pickProject')}`, {
|
|
1056
|
-
editMessageId,
|
|
1057
|
-
reply_markup: { inline_keyboard: rows(buttons, 1) },
|
|
1058
|
-
});
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
export async function showProviderMenu({ bot, chatId, link, editMessageId }) {
|
|
1062
|
-
const lang = languageFor(link);
|
|
1063
|
-
const state = getState(link.user_id);
|
|
1064
|
-
const buttons = PROVIDERS.map((provider) => button(
|
|
1065
|
-
provider === state.selectedProvider ? checked(provider) : provider,
|
|
1066
|
-
'provider_select',
|
|
1067
|
-
{ provider },
|
|
1068
|
-
));
|
|
1069
|
-
await send(bot, chatId, t(lang, 'control.pickProvider'), {
|
|
1070
|
-
editMessageId,
|
|
1071
|
-
reply_markup: { inline_keyboard: rows(buttons, 2) },
|
|
1072
|
-
});
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
async function readProviderModels(userId, provider, refresh = false) {
|
|
1076
|
-
try {
|
|
1077
|
-
const data = await localApi(userId, `/api/providers/${provider}/models${refresh ? '?refresh=1' : ''}`);
|
|
1078
|
-
const models = Array.isArray(data?.models) ? data.models : [];
|
|
1079
|
-
return models.length > 0 ? models : MODEL_FALLBACKS[provider] || [];
|
|
1080
|
-
} catch {
|
|
1081
|
-
return MODEL_FALLBACKS[provider] || [];
|
|
1082
|
-
}
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
export async function showModelMenu({ bot, chatId, link, refresh = false, editMessageId }) {
|
|
1086
|
-
const lang = languageFor(link);
|
|
1087
|
-
const state = getState(link.user_id);
|
|
1088
|
-
const models = await readProviderModels(link.user_id, state.selectedProvider, refresh);
|
|
1089
|
-
const modelButtons = models.slice(0, 18).map((model) => {
|
|
1090
|
-
const value = model.value || model.id || model.label;
|
|
1091
|
-
const label = compact(model.label || value, 38);
|
|
1092
|
-
return button(value === state.selectedModel ? checked(label) : label, 'model_select', { model: value });
|
|
1093
|
-
});
|
|
1094
|
-
modelButtons.push(button(t(lang, 'control.button.refreshModels'), 'models_refresh'));
|
|
1095
|
-
modelButtons.push(button(t(lang, 'control.button.clearModel'), 'model_select', { model: null }));
|
|
1096
|
-
await send(bot, chatId, t(lang, 'control.modelsFor', { provider: state.selectedProvider }), {
|
|
1097
|
-
editMessageId,
|
|
1098
|
-
reply_markup: { inline_keyboard: rows(modelButtons, 1) },
|
|
1099
|
-
});
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
export async function showWorkflowMenu({ bot, chatId, link, editMessageId }) {
|
|
1103
|
-
const lang = languageFor(link);
|
|
1104
|
-
const workflows = await localApi(link.user_id, '/api/orchestration/workflows');
|
|
1105
|
-
const list = Array.isArray(workflows?.workflows) ? workflows.workflows : [];
|
|
1106
|
-
const state = getState(link.user_id);
|
|
1107
|
-
const workflowButtons = list.map((workflow) => button(
|
|
1108
|
-
workflow.id === state.selectedWorkflowId ? checked(compact(workflow.name || workflow.id, 34)) : compact(workflow.name || workflow.id, 34),
|
|
1109
|
-
'workflow_select',
|
|
1110
|
-
{ workflowId: workflow.id, name: workflow.name || workflow.id },
|
|
1111
|
-
));
|
|
1112
|
-
workflowButtons.push(button(t(lang, 'control.button.runWorkflow'), 'workflow_prompt'));
|
|
1113
|
-
await send(bot, chatId, t(lang, 'control.pickWorkflow'), {
|
|
1114
|
-
editMessageId,
|
|
1115
|
-
reply_markup: { inline_keyboard: rows(workflowButtons, 1) },
|
|
1116
|
-
});
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
async function showRuns({ bot, chatId, link, editMessageId }) {
|
|
1120
|
-
const lang = languageFor(link);
|
|
1121
|
-
const data = await localApi(link.user_id, '/api/orchestration/workflows/runs?limit=10');
|
|
1122
|
-
const runs = Array.isArray(data?.runs) ? data.runs : [];
|
|
1123
|
-
if (runs.length === 0) {
|
|
1124
|
-
await send(bot, chatId, t(lang, 'control.noRuns'), { editMessageId });
|
|
1125
|
-
return;
|
|
1126
|
-
}
|
|
1127
|
-
const buttons = runs.map((run) => button(
|
|
1128
|
-
`${run.status} ${compact(run.workflowId || run.id, 28)}`,
|
|
1129
|
-
'run_detail',
|
|
1130
|
-
{ runId: run.id },
|
|
1131
|
-
));
|
|
1132
|
-
await send(bot, chatId, t(lang, 'control.recentRuns'), {
|
|
1133
|
-
editMessageId,
|
|
1134
|
-
reply_markup: { inline_keyboard: rows(buttons, 1) },
|
|
1135
|
-
});
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
|
-
async function showApprovalQueue({ bot, chatId, link, editMessageId }) {
|
|
1139
|
-
const lang = languageFor(link);
|
|
1140
|
-
const data = await localApi(link.user_id, '/api/orchestration/workflows/approvals');
|
|
1141
|
-
const approvals = Array.isArray(data?.pendingApprovals) ? data.pendingApprovals : [];
|
|
1142
|
-
if (approvals.length === 0) {
|
|
1143
|
-
await send(bot, chatId, t(lang, 'control.noApprovals'), { editMessageId });
|
|
1144
|
-
return;
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
const keyboard = [];
|
|
1148
|
-
const lines = approvals.slice(0, 8).map((approval, index) => {
|
|
1149
|
-
const label = compact(approval.summary || approval.reason || approval.id, 70);
|
|
1150
|
-
keyboard.push([
|
|
1151
|
-
button(t(lang, 'control.button.approve'), 'approval_decide', { approvalId: approval.id, allow: true }),
|
|
1152
|
-
button(t(lang, 'control.button.deny'), 'approval_decide', { approvalId: approval.id, allow: false }),
|
|
1153
|
-
]);
|
|
1154
|
-
return `${index + 1}. ${label}\nRun: ${approval.runId}`;
|
|
1155
|
-
});
|
|
1156
|
-
keyboard.push([button(t(lang, 'control.button.refresh'), 'approvals'), button(t(lang, 'control.button.mainMenu'), 'menu')]);
|
|
1157
|
-
await send(bot, chatId, `${t(lang, 'control.approvalQueue')}\n\n${lines.join('\n\n')}`, {
|
|
1158
|
-
editMessageId,
|
|
1159
|
-
reply_markup: { inline_keyboard: keyboard },
|
|
1160
|
-
});
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
async function showControlRoom({ bot, chatId, link, editMessageId }) {
|
|
1164
|
-
const lang = languageFor(link);
|
|
1165
|
-
const data = await localApi(link.user_id, '/api/remote/control-room');
|
|
1166
|
-
const snapshot = data?.controlRoom || data;
|
|
1167
|
-
const projects = Array.isArray(snapshot?.projects) ? snapshot.projects : [];
|
|
1168
|
-
const totals = snapshot?.totals || {};
|
|
1169
|
-
const lines = projects.map((project, index) => [
|
|
1170
|
-
`${index + 1}. ${compact(project.name || project.id, 44)}`,
|
|
1171
|
-
`Runs: ${project.activeRunCount || 0} active / ${project.failedRunCount || 0} failed`,
|
|
1172
|
-
`Approvals: ${project.pendingApprovalCount || 0}`,
|
|
1173
|
-
].join('\n'));
|
|
1174
|
-
await send(bot, chatId, [
|
|
1175
|
-
t(lang, 'control.controlRoomTitle'),
|
|
1176
|
-
'',
|
|
1177
|
-
`Projects: ${totals.projects || 0}`,
|
|
1178
|
-
`Active runs: ${totals.activeRuns || 0}`,
|
|
1179
|
-
`Pending approvals: ${totals.pendingApprovals || 0}`,
|
|
1180
|
-
'',
|
|
1181
|
-
lines.join('\n\n') || t(lang, 'control.noProjects'),
|
|
1182
|
-
].join('\n'), {
|
|
1183
|
-
editMessageId,
|
|
1184
|
-
reply_markup: {
|
|
1185
|
-
inline_keyboard: [
|
|
1186
|
-
[button(t(lang, 'control.button.approvals'), 'approvals'), button(t(lang, 'control.button.runs'), 'runs')],
|
|
1187
|
-
[button(t(lang, 'control.button.mainMenu'), 'menu')],
|
|
1188
|
-
],
|
|
1189
|
-
},
|
|
1190
|
-
});
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
async function showWebhookMenu({ bot, chatId, link, editMessageId }) {
|
|
1194
|
-
const lang = languageFor(link);
|
|
1195
|
-
const data = await localApi(link.user_id, '/api/webhooks');
|
|
1196
|
-
const webhooks = Array.isArray(data?.webhooks) ? data.webhooks : [];
|
|
1197
|
-
const lines = webhooks.slice(0, 10).map((webhook, index) => (
|
|
1198
|
-
`${index + 1}. ${webhook.enabled ? 'on' : 'off'} ${compact(webhook.name || webhook.url, 50)}\n${compact(webhook.events?.join(', ') || webhook.url, 90)}`
|
|
1199
|
-
));
|
|
1200
|
-
await send(bot, chatId, [
|
|
1201
|
-
t(lang, 'control.webhookTitle'),
|
|
1202
|
-
'',
|
|
1203
|
-
lines.join('\n\n') || t(lang, 'control.noWebhooks'),
|
|
1204
|
-
].join('\n'), {
|
|
1205
|
-
editMessageId,
|
|
1206
|
-
reply_markup: {
|
|
1207
|
-
inline_keyboard: [
|
|
1208
|
-
[button(t(lang, 'control.button.refresh'), 'webhooks'), button(t(lang, 'control.button.mainMenu'), 'menu')],
|
|
1209
|
-
],
|
|
1210
|
-
},
|
|
1211
|
-
});
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
async function showSessions({ bot, chatId, link, editMessageId }) {
|
|
1215
|
-
const lang = languageFor(link);
|
|
1216
|
-
const state = getState(link.user_id);
|
|
1217
|
-
if (!state.selectedProjectName) {
|
|
1218
|
-
await send(bot, chatId, t(lang, 'control.selectProjectFirst'), { editMessageId });
|
|
1219
|
-
return;
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
const data = await localApi(link.user_id, `/api/projects/${encodeURIComponent(state.selectedProjectName)}/sessions?limit=10&offset=0`);
|
|
1223
|
-
const sessions = Array.isArray(data?.sessions) ? data.sessions : [];
|
|
1224
|
-
if (sessions.length === 0) {
|
|
1225
|
-
await send(bot, chatId, t(lang, 'control.noSessions'), { editMessageId });
|
|
1226
|
-
return;
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
const lines = sessions.slice(0, 10).map((session, index) =>
|
|
1230
|
-
`${index + 1}. ${compact(session.summary || session.id || session.sessionId, 70)}`
|
|
1231
|
-
);
|
|
1232
|
-
await send(bot, chatId, `${t(lang, 'control.recentSessions')}\n\n${lines.join('\n')}`, {
|
|
1233
|
-
editMessageId,
|
|
1234
|
-
reply_markup: {
|
|
1235
|
-
inline_keyboard: [
|
|
1236
|
-
[button(t(lang, 'control.button.newChat'), 'new_chat')],
|
|
1237
|
-
[button(t(lang, 'control.button.mainMenu'), 'menu')],
|
|
1238
|
-
],
|
|
1239
|
-
},
|
|
1240
|
-
});
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
async function startNewChat({ bot, chatId, link, editMessageId }) {
|
|
1244
|
-
const lang = languageFor(link);
|
|
1245
|
-
updateTelegramControlState(link.user_id, { awaiting: { type: 'agent_prompt' } });
|
|
1246
|
-
await send(bot, chatId, t(lang, 'control.newChatReady'), {
|
|
1247
|
-
editMessageId,
|
|
1248
|
-
reply_markup: { inline_keyboard: [[button(t(lang, 'control.button.mainMenu'), 'menu')]] },
|
|
1249
|
-
});
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
|
-
function extractAssistantText(response) {
|
|
1253
|
-
const messages = Array.isArray(response?.messages) ? response.messages : [];
|
|
1254
|
-
const chunks = [];
|
|
1255
|
-
for (const message of messages) {
|
|
1256
|
-
if (typeof message?.content === 'string') chunks.push(message.content);
|
|
1257
|
-
const content = message?.message?.content;
|
|
1258
|
-
if (Array.isArray(content)) {
|
|
1259
|
-
for (const part of content) {
|
|
1260
|
-
if (part?.type === 'text' && typeof part.text === 'string') chunks.push(part.text);
|
|
1261
|
-
}
|
|
1262
|
-
}
|
|
1263
|
-
}
|
|
1264
|
-
return chunks.join('\n\n').trim();
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1267
|
-
function extractTextFromEvent(event) {
|
|
1268
|
-
if (!event || typeof event !== 'object') return '';
|
|
1269
|
-
if (typeof event.content === 'string') return event.content;
|
|
1270
|
-
if (Array.isArray(event.content)) {
|
|
1271
|
-
return event.content
|
|
1272
|
-
.map((part) => (typeof part === 'string' ? part : part?.text || ''))
|
|
1273
|
-
.join('');
|
|
1274
|
-
}
|
|
1275
|
-
const legacyContent = event.data?.message?.content || event.message?.content;
|
|
1276
|
-
if (Array.isArray(legacyContent)) {
|
|
1277
|
-
return legacyContent
|
|
1278
|
-
.map((part) => (typeof part === 'string' ? part : part?.text || ''))
|
|
1279
|
-
.join('');
|
|
1280
|
-
}
|
|
1281
|
-
if (typeof legacyContent === 'string') return legacyContent;
|
|
1282
|
-
return '';
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
|
-
function describeToolInput(toolName, input) {
|
|
1286
|
-
if (!input || typeof input !== 'object') return '';
|
|
1287
|
-
if (toolName === 'Bash' && typeof input.command === 'string') {
|
|
1288
|
-
return compact(input.command, 110);
|
|
1289
|
-
}
|
|
1290
|
-
if (toolName === 'WebSearch' && typeof input.query === 'string') {
|
|
1291
|
-
return compact(input.query, 110);
|
|
1292
|
-
}
|
|
1293
|
-
if (toolName === 'FileChanges') {
|
|
1294
|
-
return compact(JSON.stringify(input), 110);
|
|
1295
|
-
}
|
|
1296
|
-
const keys = Object.keys(input).slice(0, 3);
|
|
1297
|
-
if (keys.length === 0) return '';
|
|
1298
|
-
return compact(keys.map((key) => `${key}: ${String(input[key])}`).join(', '), 110);
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
|
-
function applyAgentStreamEvent(activity, event) {
|
|
1302
|
-
if (!event || typeof event !== 'object') return;
|
|
1303
|
-
|
|
1304
|
-
const sessionId = event.actualSessionId || event.newSessionId || event.sessionId || event.threadId;
|
|
1305
|
-
if (sessionId) activity.sessionId = sessionId;
|
|
1306
|
-
|
|
1307
|
-
if (event.type === 'status' && event.message) {
|
|
1308
|
-
activity.status = 'running';
|
|
1309
|
-
activity.phase = compact(event.message, 120);
|
|
1310
|
-
pushActivityEvent(activity, `⏳ ${compact(event.message, 120)}`);
|
|
1311
|
-
return;
|
|
1312
|
-
}
|
|
1313
|
-
if (event.type === 'done' || event.kind === 'complete' || event.kind === 'stream_end') {
|
|
1314
|
-
activity.status = 'done';
|
|
1315
|
-
activity.phase = t(activity.lang, 'control.activity.done');
|
|
1316
|
-
return;
|
|
1317
|
-
}
|
|
1318
|
-
if (event.type === 'error' || event.kind === 'error') {
|
|
1319
|
-
activity.status = 'failed';
|
|
1320
|
-
activity.phase = t(activity.lang, 'control.activity.failed');
|
|
1321
|
-
activity.error = event.error || event.message || event.content || t(activity.lang, 'error.generic');
|
|
1322
|
-
pushActivityEvent(activity, `❌ ${compact(activity.error, 120)}`);
|
|
1323
|
-
return;
|
|
1324
|
-
}
|
|
1325
|
-
if (event.kind === 'session_created') {
|
|
1326
|
-
activity.status = 'running';
|
|
1327
|
-
activity.phase = t(activity.lang, 'control.activity.sessionStarted');
|
|
1328
|
-
pushActivityEvent(activity, `🧵 ${t(activity.lang, 'control.activity.sessionStarted')}`);
|
|
1329
|
-
return;
|
|
1330
|
-
}
|
|
1331
|
-
if (event.kind === 'thinking') {
|
|
1332
|
-
activity.status = 'running';
|
|
1333
|
-
activity.phase = t(activity.lang, 'control.activity.thinking');
|
|
1334
|
-
const thought = extractTextFromEvent(event);
|
|
1335
|
-
if (thought) pushActivityEvent(activity, `🧠 ${compact(thought, 120)}`);
|
|
1336
|
-
return;
|
|
1337
|
-
}
|
|
1338
|
-
if (event.kind === 'stream_delta' || event.kind === 'text') {
|
|
1339
|
-
activity.status = 'running';
|
|
1340
|
-
activity.phase = t(activity.lang, 'control.activity.responding');
|
|
1341
|
-
appendActivityOutput(activity, extractTextFromEvent(event));
|
|
1342
|
-
return;
|
|
1343
|
-
}
|
|
1344
|
-
if (event.type === 'claude-response' && event.data?.type === 'assistant') {
|
|
1345
|
-
activity.status = 'running';
|
|
1346
|
-
activity.phase = t(activity.lang, 'control.activity.responding');
|
|
1347
|
-
appendActivityOutput(activity, extractTextFromEvent(event));
|
|
1348
|
-
return;
|
|
1349
|
-
}
|
|
1350
|
-
if (event.kind === 'tool_use') {
|
|
1351
|
-
activity.status = 'running';
|
|
1352
|
-
activity.phase = t(activity.lang, 'control.activity.working');
|
|
1353
|
-
const toolName = event.toolName || 'Tool';
|
|
1354
|
-
const input = describeToolInput(toolName, event.toolInput);
|
|
1355
|
-
const icon = toolName === 'Bash'
|
|
1356
|
-
? '💻'
|
|
1357
|
-
: toolName === 'FileChanges'
|
|
1358
|
-
? '📝'
|
|
1359
|
-
: toolName === 'WebSearch'
|
|
1360
|
-
? '🔎'
|
|
1361
|
-
: '🔧';
|
|
1362
|
-
pushActivityEvent(activity, `${icon} ${toolName}${input ? `: ${input}` : ''}`);
|
|
1363
|
-
return;
|
|
1364
|
-
}
|
|
1365
|
-
if (event.kind === 'tool_result') {
|
|
1366
|
-
activity.status = 'running';
|
|
1367
|
-
activity.phase = t(activity.lang, 'control.activity.working');
|
|
1368
|
-
const label = event.isError
|
|
1369
|
-
? t(activity.lang, 'control.activity.toolFailed')
|
|
1370
|
-
: t(activity.lang, 'control.activity.toolDone');
|
|
1371
|
-
pushActivityEvent(activity, `${event.isError ? '⚠️' : '✅'} ${label}`);
|
|
1372
|
-
return;
|
|
1373
|
-
}
|
|
1374
|
-
if (event.kind === 'status' && event.text === 'token_budget') {
|
|
1375
|
-
const used = event.tokenBudget?.used;
|
|
1376
|
-
if (used) pushActivityEvent(activity, `📊 ${t(activity.lang, 'control.activity.tokens')}: ${used}`);
|
|
1377
|
-
}
|
|
1378
|
-
}
|
|
1379
|
-
|
|
1380
|
-
function confirmationLabel(lang, action, payload = {}) {
|
|
1381
|
-
if (action === 'install_provider') {
|
|
1382
|
-
return t(lang, 'control.confirmInstall', { provider: payload.provider || '' });
|
|
1383
|
-
}
|
|
1384
|
-
if (action === 'run_cancel') {
|
|
1385
|
-
return t(lang, 'control.confirmCancelRun', { runId: payload.runId || '' });
|
|
1386
|
-
}
|
|
1387
|
-
return t(lang, 'control.confirmationRequired');
|
|
1388
|
-
}
|
|
1389
|
-
|
|
1390
|
-
async function requestConfirmation({ bot, chatId, link, action, payload = {}, editMessageId }) {
|
|
1391
|
-
const lang = languageFor(link);
|
|
1392
|
-
const pending = createTelegramConfirmation(link.user_id, action, payload);
|
|
1393
|
-
await send(bot, chatId, confirmationLabel(lang, action, payload), {
|
|
1394
|
-
editMessageId,
|
|
1395
|
-
reply_markup: {
|
|
1396
|
-
inline_keyboard: [[
|
|
1397
|
-
button(t(lang, 'control.button.confirm'), 'confirm_action', { id: pending.id }),
|
|
1398
|
-
button(t(lang, 'control.button.cancel'), 'cancel_confirmation', { id: pending.id }),
|
|
1399
|
-
]],
|
|
1400
|
-
},
|
|
1401
|
-
});
|
|
1402
|
-
return { ok: false, requiresConfirmation: true };
|
|
1403
|
-
}
|
|
1404
|
-
|
|
1405
|
-
async function sendToolFailure({ bot, chatId, link, result, editMessageId }) {
|
|
1406
|
-
const lang = languageFor(link);
|
|
1407
|
-
const message = result?.message === 'REMOTE_CONTROL_DISABLED'
|
|
1408
|
-
? t(lang, 'control.disabled')
|
|
1409
|
-
: (result?.message || t(lang, 'error.generic'));
|
|
1410
|
-
await send(bot, chatId, message, { editMessageId });
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
async function runAgent({ bot, chatId, link, prompt, activity = null }) {
|
|
1414
|
-
const lang = languageFor(link);
|
|
1415
|
-
const state = getState(link.user_id);
|
|
1416
|
-
if (!state.remoteControlEnabled) {
|
|
1417
|
-
await send(bot, chatId, t(lang, 'control.disabled'), { editMessageId: activity?.messageId });
|
|
1418
|
-
return;
|
|
1419
|
-
}
|
|
1420
|
-
if (!state.selectedProjectPath) {
|
|
1421
|
-
await showProjectMenu({ bot, chatId, link, editMessageId: activity?.messageId, notice: t(lang, 'control.selectProjectFirst') });
|
|
1422
|
-
return;
|
|
1423
|
-
}
|
|
1424
|
-
|
|
1425
|
-
const provider = resolveTelegramProvider(state);
|
|
1426
|
-
const model = resolveTelegramModel(state);
|
|
1427
|
-
const active = activity || await createTelegramActivity({
|
|
1428
|
-
bot,
|
|
1429
|
-
chatId,
|
|
1430
|
-
link,
|
|
1431
|
-
type: 'agent',
|
|
1432
|
-
prompt,
|
|
1433
|
-
phase: t(lang, 'control.activity.startingProvider', { provider }),
|
|
1434
|
-
});
|
|
1435
|
-
active.type = 'agent';
|
|
1436
|
-
active.provider = provider;
|
|
1437
|
-
active.project = projectLabel(state);
|
|
1438
|
-
active.status = 'running';
|
|
1439
|
-
active.phase = t(lang, 'control.activity.startingProvider', { provider });
|
|
1440
|
-
await editTelegramActivity({ bot, chatId, activity: active, force: true });
|
|
1441
|
-
|
|
1442
|
-
const heartbeat = startActivityHeartbeat({ bot, chatId, activity: active });
|
|
1443
|
-
let streamFailed = null;
|
|
1444
|
-
try {
|
|
1445
|
-
await localAgentStream(link.user_id, {
|
|
1446
|
-
projectPath: state.selectedProjectPath,
|
|
1447
|
-
provider,
|
|
1448
|
-
model: model || undefined,
|
|
1449
|
-
message: buildTelegramAgentPrompt(prompt, state),
|
|
1450
|
-
cleanup: false,
|
|
1451
|
-
permissionMode: 'default',
|
|
1452
|
-
suppressNotifications: true,
|
|
1453
|
-
}, async (event) => {
|
|
1454
|
-
applyAgentStreamEvent(active, event);
|
|
1455
|
-
await editTelegramActivity({ bot, chatId, activity: active });
|
|
1456
|
-
});
|
|
1457
|
-
} catch (error) {
|
|
1458
|
-
streamFailed = error;
|
|
1459
|
-
} finally {
|
|
1460
|
-
clearInterval(heartbeat);
|
|
1461
|
-
}
|
|
1462
|
-
|
|
1463
|
-
if (streamFailed) {
|
|
1464
|
-
active.status = 'failed';
|
|
1465
|
-
active.phase = t(lang, 'control.activity.failed');
|
|
1466
|
-
active.error = streamFailed.message || t(lang, 'error.generic');
|
|
1467
|
-
await editTelegramActivity({ bot, chatId, activity: active, force: true });
|
|
1468
|
-
return;
|
|
1469
|
-
}
|
|
1470
|
-
|
|
1471
|
-
active.status = active.error ? 'failed' : 'done';
|
|
1472
|
-
active.phase = active.error ? t(lang, 'control.activity.failed') : t(lang, 'control.activity.done');
|
|
1473
|
-
if (!active.output && !active.error) active.output = t(lang, 'control.noAssistantText');
|
|
1474
|
-
await editTelegramActivity({ bot, chatId, activity: active, force: true });
|
|
1475
|
-
}
|
|
1476
|
-
|
|
1477
|
-
function longTaskKey(chatId) {
|
|
1478
|
-
return String(chatId);
|
|
1479
|
-
}
|
|
1480
|
-
|
|
1481
|
-
async function launchLongTelegramTask({ bot, chatId, link, kind, activity = null, task }) {
|
|
1482
|
-
const key = longTaskKey(chatId);
|
|
1483
|
-
const existing = activeLongTasks.get(key);
|
|
1484
|
-
const lang = languageFor(link);
|
|
1485
|
-
if (existing) {
|
|
1486
|
-
await send(bot, chatId, t(lang, 'control.longTaskRunning', {
|
|
1487
|
-
kind: existing.kind,
|
|
1488
|
-
elapsed: formatElapsed(existing.startedAt),
|
|
1489
|
-
}), {
|
|
1490
|
-
editMessageId: activity?.messageId,
|
|
1491
|
-
parse_mode: undefined,
|
|
1492
|
-
});
|
|
1493
|
-
return true;
|
|
1494
|
-
}
|
|
1495
|
-
|
|
1496
|
-
const record = {
|
|
1497
|
-
kind,
|
|
1498
|
-
startedAt: Date.now(),
|
|
1499
|
-
};
|
|
1500
|
-
activeLongTasks.set(key, record);
|
|
1501
|
-
|
|
1502
|
-
Promise.resolve()
|
|
1503
|
-
.then(task)
|
|
1504
|
-
.catch(async (error) => {
|
|
1505
|
-
console.error('[telegram-control] long task failed:', error);
|
|
1506
|
-
await send(bot, chatId, t(lang, 'control.longTaskFailed'), {
|
|
1507
|
-
editMessageId: activity?.messageId,
|
|
1508
|
-
parse_mode: undefined,
|
|
1509
|
-
}).catch(() => {});
|
|
1510
|
-
})
|
|
1511
|
-
.finally(() => {
|
|
1512
|
-
if (activeLongTasks.get(key) === record) activeLongTasks.delete(key);
|
|
1513
|
-
});
|
|
1514
|
-
|
|
1515
|
-
return true;
|
|
1516
|
-
}
|
|
1517
|
-
|
|
1518
|
-
export async function runWorkflow({ bot, chatId, link, input, activity = null }) {
|
|
1519
|
-
const lang = languageFor(link);
|
|
1520
|
-
const state = getState(link.user_id);
|
|
1521
|
-
const workflowId = state.selectedWorkflowId;
|
|
1522
|
-
if (!state.remoteControlEnabled) {
|
|
1523
|
-
await send(bot, chatId, t(lang, 'control.disabled'), { editMessageId: activity?.messageId });
|
|
1524
|
-
return;
|
|
1525
|
-
}
|
|
1526
|
-
if (!workflowId) {
|
|
1527
|
-
await send(bot, chatId, t(lang, 'control.selectWorkflowFirst'), { editMessageId: activity?.messageId });
|
|
1528
|
-
await showWorkflowMenu({ bot, chatId, link, editMessageId: activity?.messageId });
|
|
1529
|
-
return;
|
|
1530
|
-
}
|
|
1531
|
-
if (!state.selectedProjectPath) {
|
|
1532
|
-
await showProjectMenu({ bot, chatId, link, editMessageId: activity?.messageId, notice: t(lang, 'control.selectProjectFirst') });
|
|
1533
|
-
return;
|
|
1534
|
-
}
|
|
1535
|
-
|
|
1536
|
-
const provider = resolveTelegramProvider(state);
|
|
1537
|
-
const model = resolveTelegramModel(state);
|
|
1538
|
-
const active = activity || await createTelegramActivity({
|
|
1539
|
-
bot,
|
|
1540
|
-
chatId,
|
|
1541
|
-
link,
|
|
1542
|
-
type: 'workflow',
|
|
1543
|
-
prompt: input,
|
|
1544
|
-
phase: t(lang, 'control.activity.startingWorkflow'),
|
|
1545
|
-
});
|
|
1546
|
-
active.type = 'workflow';
|
|
1547
|
-
active.provider = provider;
|
|
1548
|
-
active.project = projectLabel(state);
|
|
1549
|
-
active.workflowId = workflowId;
|
|
1550
|
-
active.status = 'running';
|
|
1551
|
-
active.phase = t(lang, 'control.activity.startingWorkflow');
|
|
1552
|
-
await editTelegramActivity({ bot, chatId, activity: active, force: true });
|
|
1553
|
-
|
|
1554
|
-
const result = await runTelegramTool({
|
|
1555
|
-
userId: link.user_id,
|
|
1556
|
-
action: 'run_workflow',
|
|
1557
|
-
execute: () => localApi(link.user_id, `/api/orchestration/workflows/${workflowId}/runs`, {
|
|
1558
|
-
method: 'POST',
|
|
1559
|
-
body: {
|
|
1560
|
-
input,
|
|
1561
|
-
metadata: {
|
|
1562
|
-
projectId: state.selectedProjectName,
|
|
1563
|
-
projectName: state.selectedProjectName,
|
|
1564
|
-
projectPath: state.selectedProjectPath,
|
|
1565
|
-
workspaceTarget: 'selected_project',
|
|
1566
|
-
telegram: true,
|
|
1567
|
-
preferredProvider: provider,
|
|
1568
|
-
preferredModel: model,
|
|
1569
|
-
},
|
|
1570
|
-
},
|
|
1571
|
-
}),
|
|
1572
|
-
});
|
|
1573
|
-
if (!result.ok) {
|
|
1574
|
-
await sendToolFailure({ bot, chatId, link, result, editMessageId: active.messageId });
|
|
1575
|
-
return;
|
|
1576
|
-
}
|
|
1577
|
-
const run = result.data;
|
|
1578
|
-
active.runId = run.id;
|
|
1579
|
-
active.phase = t(lang, 'control.activity.workflowRunning');
|
|
1580
|
-
pushActivityEvent(active, `🧭 ${t(lang, 'control.workflowStarted', { runId: run.id, workflowId }).replace('\n', ' ')}`);
|
|
1581
|
-
await editTelegramActivity({ bot, chatId, activity: active, force: true });
|
|
1582
|
-
monitorWorkflowRun({ bot, chatId, link, runId: run.id, activity: active }).catch((error) => {
|
|
1583
|
-
console.warn('[telegram-control] workflow monitor failed:', error?.message || error);
|
|
1584
|
-
});
|
|
1585
|
-
}
|
|
1586
|
-
|
|
1587
|
-
async function cancelRun({ bot, chatId, link, runId, editMessageId, confirmed = false }) {
|
|
1588
|
-
const lang = languageFor(link);
|
|
1589
|
-
if (!runId) {
|
|
1590
|
-
await send(bot, chatId, t(lang, 'control.cancelUsage'), { editMessageId });
|
|
1591
|
-
return;
|
|
1592
|
-
}
|
|
1593
|
-
const state = getState(link.user_id);
|
|
1594
|
-
if (state.remoteControlEnabled === false) {
|
|
1595
|
-
await send(bot, chatId, t(lang, 'control.disabled'), { editMessageId });
|
|
1596
|
-
return;
|
|
1597
|
-
}
|
|
1598
|
-
if (state.confirmationPolicy === 'strict' && !confirmed) {
|
|
1599
|
-
await requestConfirmation({
|
|
1600
|
-
bot,
|
|
1601
|
-
chatId,
|
|
1602
|
-
link,
|
|
1603
|
-
action: 'run_cancel',
|
|
1604
|
-
payload: { runId },
|
|
1605
|
-
editMessageId,
|
|
1606
|
-
});
|
|
1607
|
-
return;
|
|
1608
|
-
}
|
|
1609
|
-
|
|
1610
|
-
const result = await runTelegramTool({
|
|
1611
|
-
userId: link.user_id,
|
|
1612
|
-
action: 'run_cancel',
|
|
1613
|
-
execute: () => localApi(link.user_id, `/api/orchestration/workflows/runs/${encodeURIComponent(runId)}/cancel`, {
|
|
1614
|
-
method: 'POST',
|
|
1615
|
-
}),
|
|
1616
|
-
});
|
|
1617
|
-
if (!result.ok) {
|
|
1618
|
-
await sendToolFailure({ bot, chatId, link, result, editMessageId });
|
|
1619
|
-
return;
|
|
1620
|
-
}
|
|
1621
|
-
const run = result.data;
|
|
1622
|
-
await send(bot, chatId, t(lang, 'control.runStatus', { runId: run.id, status: run.status }), { editMessageId });
|
|
1623
|
-
}
|
|
1624
|
-
|
|
1625
|
-
async function executeConfirmedAction({ bot, chatId, link, confirmation, editMessageId }) {
|
|
1626
|
-
if (confirmation.action === 'install_provider') {
|
|
1627
|
-
await startCliInstall({
|
|
1628
|
-
bot,
|
|
1629
|
-
chatId,
|
|
1630
|
-
link,
|
|
1631
|
-
provider: confirmation.payload?.provider,
|
|
1632
|
-
editMessageId,
|
|
1633
|
-
confirmed: true,
|
|
1634
|
-
});
|
|
1635
|
-
return true;
|
|
1636
|
-
}
|
|
1637
|
-
if (confirmation.action === 'run_cancel') {
|
|
1638
|
-
await cancelRun({
|
|
1639
|
-
bot,
|
|
1640
|
-
chatId,
|
|
1641
|
-
link,
|
|
1642
|
-
runId: confirmation.payload?.runId,
|
|
1643
|
-
editMessageId,
|
|
1644
|
-
confirmed: true,
|
|
1645
|
-
});
|
|
1646
|
-
return true;
|
|
1647
|
-
}
|
|
1648
|
-
return false;
|
|
1649
|
-
}
|
|
1650
|
-
|
|
1651
|
-
async function findProjectByQuery(query) {
|
|
1652
|
-
const projects = await listProjects();
|
|
1653
|
-
const needle = String(query || '').trim().toLocaleLowerCase('tr');
|
|
1654
|
-
if (!needle) return null;
|
|
1655
|
-
return projects.find((project) => {
|
|
1656
|
-
const candidates = [project.name, project.displayName, project.fullPath, project.path]
|
|
1657
|
-
.filter(Boolean)
|
|
1658
|
-
.map((value) => String(value).toLocaleLowerCase('tr'));
|
|
1659
|
-
return candidates.some((candidate) => candidate === needle || candidate.includes(needle));
|
|
1660
|
-
}) || null;
|
|
1661
|
-
}
|
|
1662
|
-
|
|
1663
|
-
async function listWorkflowSummaries(userId) {
|
|
1664
|
-
try {
|
|
1665
|
-
const data = await localApi(userId, '/api/orchestration/workflows', { timeoutMs: 10_000 });
|
|
1666
|
-
if (Array.isArray(data)) return data;
|
|
1667
|
-
if (Array.isArray(data?.workflows)) return data.workflows;
|
|
1668
|
-
} catch {
|
|
1669
|
-
// Workflow context is helpful for routing but should never block chat input.
|
|
1670
|
-
}
|
|
1671
|
-
return [];
|
|
1672
|
-
}
|
|
1673
|
-
|
|
1674
|
-
async function resolveTelegramAiIntent({ bot, chatId, link, text, activity }) {
|
|
1675
|
-
const state = getState(link.user_id);
|
|
1676
|
-
if (state.routerEnabled === false || state.routerMode !== 'hybrid') {
|
|
1677
|
-
return { action: 'agent_prompt', prompt: text, confidence: 1 };
|
|
1678
|
-
}
|
|
1679
|
-
|
|
1680
|
-
const provider = resolveTelegramProvider(state);
|
|
1681
|
-
const model = resolveTelegramModel(state);
|
|
1682
|
-
if (activity) {
|
|
1683
|
-
activity.type = 'router';
|
|
1684
|
-
activity.provider = provider;
|
|
1685
|
-
activity.project = projectLabel(state);
|
|
1686
|
-
activity.status = 'running';
|
|
1687
|
-
activity.phase = t(activity.lang, 'control.activity.interpreting');
|
|
1688
|
-
pushActivityEvent(activity, `🧠 ${t(activity.lang, 'control.activity.interpreting')}`);
|
|
1689
|
-
await editTelegramActivity({ bot, chatId, activity, force: true });
|
|
1690
|
-
}
|
|
1691
|
-
|
|
1692
|
-
try {
|
|
1693
|
-
const projects = await listProjects().catch(() => []);
|
|
1694
|
-
const workflows = await listWorkflowSummaries(link.user_id);
|
|
1695
|
-
const response = await localApi(link.user_id, '/api/agent', {
|
|
1696
|
-
method: 'POST',
|
|
1697
|
-
timeoutMs: INTENT_ROUTER_TIMEOUT_MS,
|
|
1698
|
-
body: {
|
|
1699
|
-
projectPath: state.selectedProjectPath || process.cwd(),
|
|
1700
|
-
provider,
|
|
1701
|
-
model: model || undefined,
|
|
1702
|
-
message: buildTelegramIntentPrompt(text, state, { projects, workflows }),
|
|
1703
|
-
cleanup: false,
|
|
1704
|
-
stream: false,
|
|
1705
|
-
permissionMode: 'plan',
|
|
1706
|
-
suppressNotifications: true,
|
|
1707
|
-
},
|
|
1708
|
-
});
|
|
1709
|
-
const assistantText = extractAssistantText(response);
|
|
1710
|
-
const intent = parseTelegramAiIntentResponse(assistantText, text);
|
|
1711
|
-
if (activity) {
|
|
1712
|
-
pushActivityEvent(activity, `🧭 ${intent.action} (${Math.round(intent.confidence * 100)}%)`);
|
|
1713
|
-
await editTelegramActivity({ bot, chatId, activity });
|
|
1714
|
-
}
|
|
1715
|
-
return intent;
|
|
1716
|
-
} catch (error) {
|
|
1717
|
-
if (activity) {
|
|
1718
|
-
pushActivityEvent(activity, `⚠️ ${t(activity.lang, 'control.activity.routerFallback')}`);
|
|
1719
|
-
await editTelegramActivity({ bot, chatId, activity });
|
|
1720
|
-
}
|
|
1721
|
-
console.warn('[telegram-control] AI intent router fallback:', error?.message || error);
|
|
1722
|
-
return { action: 'agent_prompt', prompt: text, confidence: 0 };
|
|
1723
|
-
}
|
|
1724
|
-
}
|
|
1725
|
-
|
|
1726
|
-
async function handleRoutedIntent({ bot, chatId, link, text, activity }) {
|
|
1727
|
-
const intent = await resolveTelegramAiIntent({ bot, chatId, link, text, activity });
|
|
1728
|
-
const editMessageId = activity?.messageId;
|
|
1729
|
-
|
|
1730
|
-
if (intent.action === 'agent_prompt') {
|
|
1731
|
-
return launchLongTelegramTask({
|
|
1732
|
-
bot,
|
|
1733
|
-
chatId,
|
|
1734
|
-
link,
|
|
1735
|
-
kind: 'agent',
|
|
1736
|
-
activity,
|
|
1737
|
-
task: () => runAgent({ bot, chatId, link, prompt: intent.prompt || text, activity }),
|
|
1738
|
-
});
|
|
1739
|
-
}
|
|
1740
|
-
if (intent.action === 'show_menu') {
|
|
1741
|
-
await showMainMenu({ bot, chatId, link, editMessageId });
|
|
1742
|
-
return true;
|
|
1743
|
-
}
|
|
1744
|
-
if (intent.action === 'show_projects') {
|
|
1745
|
-
await showProjectMenu({ bot, chatId, link, editMessageId });
|
|
1746
|
-
return true;
|
|
1747
|
-
}
|
|
1748
|
-
if (intent.action === 'select_project') {
|
|
1749
|
-
const query = intent.projectQuery || intent.prompt || text;
|
|
1750
|
-
const project = await findProjectByQuery(query);
|
|
1751
|
-
const lang = languageFor(link);
|
|
1752
|
-
if (!project) {
|
|
1753
|
-
await showProjectMenu({
|
|
1754
|
-
bot,
|
|
1755
|
-
chatId,
|
|
1756
|
-
link,
|
|
1757
|
-
editMessageId,
|
|
1758
|
-
notice: t(lang, 'control.projectNotFound', { query }),
|
|
1759
|
-
});
|
|
1760
|
-
return true;
|
|
1761
|
-
}
|
|
1762
|
-
updateTelegramControlState(link.user_id, {
|
|
1763
|
-
selectedProjectName: project.name,
|
|
1764
|
-
selectedProjectPath: project.fullPath || project.path,
|
|
1765
|
-
});
|
|
1766
|
-
await showMainMenu({
|
|
1767
|
-
bot,
|
|
1768
|
-
chatId,
|
|
1769
|
-
link,
|
|
1770
|
-
editMessageId,
|
|
1771
|
-
notice: t(lang, 'control.projectSelected', {
|
|
1772
|
-
project: project.displayName || project.name,
|
|
1773
|
-
path: project.fullPath || project.path,
|
|
1774
|
-
}),
|
|
1775
|
-
});
|
|
1776
|
-
return true;
|
|
1777
|
-
}
|
|
1778
|
-
if (intent.action === 'show_provider_menu') {
|
|
1779
|
-
await showProviderMenu({ bot, chatId, link, editMessageId });
|
|
1780
|
-
return true;
|
|
1781
|
-
}
|
|
1782
|
-
if (intent.action === 'select_provider' && intent.provider) {
|
|
1783
|
-
updateTelegramControlState(link.user_id, { selectedProvider: intent.provider, selectedModel: null });
|
|
1784
|
-
await showModelMenu({ bot, chatId, link, editMessageId });
|
|
1785
|
-
return true;
|
|
1786
|
-
}
|
|
1787
|
-
if (intent.action === 'show_model_menu') {
|
|
1788
|
-
await showModelMenu({ bot, chatId, link, editMessageId });
|
|
1789
|
-
return true;
|
|
1790
|
-
}
|
|
1791
|
-
if (intent.action === 'select_model' && intent.model) {
|
|
1792
|
-
updateTelegramControlState(link.user_id, { selectedModel: intent.model });
|
|
1793
|
-
await showMainMenu({
|
|
1794
|
-
bot,
|
|
1795
|
-
chatId,
|
|
1796
|
-
link,
|
|
1797
|
-
editMessageId,
|
|
1798
|
-
notice: t(languageFor(link), 'control.modelSelected', { model: intent.model }),
|
|
1799
|
-
});
|
|
1800
|
-
return true;
|
|
1801
|
-
}
|
|
1802
|
-
if (intent.action === 'show_runs') {
|
|
1803
|
-
await showRuns({ bot, chatId, link, editMessageId });
|
|
1804
|
-
return true;
|
|
1805
|
-
}
|
|
1806
|
-
if (intent.action === 'show_approvals') {
|
|
1807
|
-
await showApprovalQueue({ bot, chatId, link, editMessageId });
|
|
1808
|
-
return true;
|
|
1809
|
-
}
|
|
1810
|
-
if (intent.action === 'show_workflows') {
|
|
1811
|
-
await showWorkflowMenu({ bot, chatId, link, editMessageId });
|
|
1812
|
-
return true;
|
|
1813
|
-
}
|
|
1814
|
-
if (intent.action === 'run_workflow') {
|
|
1815
|
-
return launchLongTelegramTask({
|
|
1816
|
-
bot,
|
|
1817
|
-
chatId,
|
|
1818
|
-
link,
|
|
1819
|
-
kind: 'workflow',
|
|
1820
|
-
activity,
|
|
1821
|
-
task: () => runWorkflow({ bot, chatId, link, input: intent.workflowInput || text, activity }),
|
|
1822
|
-
});
|
|
1823
|
-
}
|
|
1824
|
-
if (intent.action === 'show_sessions') {
|
|
1825
|
-
await showSessions({ bot, chatId, link, editMessageId });
|
|
1826
|
-
return true;
|
|
1827
|
-
}
|
|
1828
|
-
if (intent.action === 'new_chat') {
|
|
1829
|
-
await startNewChat({ bot, chatId, link, editMessageId });
|
|
1830
|
-
return true;
|
|
1831
|
-
}
|
|
1832
|
-
return launchLongTelegramTask({
|
|
1833
|
-
bot,
|
|
1834
|
-
chatId,
|
|
1835
|
-
link,
|
|
1836
|
-
kind: 'agent',
|
|
1837
|
-
activity,
|
|
1838
|
-
task: () => runAgent({ bot, chatId, link, prompt: text, activity }),
|
|
1839
|
-
});
|
|
1840
|
-
}
|
|
1841
|
-
|
|
1842
|
-
async function fetchRun(userId, runId) {
|
|
1843
|
-
return localApi(userId, `/api/orchestration/workflows/runs/${runId}`);
|
|
1844
|
-
}
|
|
1845
|
-
|
|
1846
|
-
function summarizeRun(run, mode) {
|
|
1847
|
-
const statusIcon = run.status === 'completed'
|
|
1848
|
-
? '✅'
|
|
1849
|
-
: run.status === 'failed'
|
|
1850
|
-
? '❌'
|
|
1851
|
-
: run.status === 'canceled'
|
|
1852
|
-
? '⏹'
|
|
1853
|
-
: '🔧';
|
|
1854
|
-
const lines = [
|
|
1855
|
-
`${statusIcon} Run ${run.id}`,
|
|
1856
|
-
`🧩 Workflow: ${run.workflowId}`,
|
|
1857
|
-
`📌 Status: ${run.status}`,
|
|
1858
|
-
];
|
|
1859
|
-
const nodeRuns = Array.isArray(run.nodeRuns) ? run.nodeRuns : [];
|
|
1860
|
-
if (mode !== 'final') {
|
|
1861
|
-
const visibleNodes = mode === 'errors'
|
|
1862
|
-
? nodeRuns.filter((node) => node.error || node.status === 'failed')
|
|
1863
|
-
: nodeRuns;
|
|
1864
|
-
for (const node of visibleNodes) {
|
|
1865
|
-
const nodeIcon = node.status === 'completed'
|
|
1866
|
-
? '✅'
|
|
1867
|
-
: node.status === 'failed'
|
|
1868
|
-
? '❌'
|
|
1869
|
-
: node.status === 'running'
|
|
1870
|
-
? '🔧'
|
|
1871
|
-
: '⏳';
|
|
1872
|
-
lines.push(`${nodeIcon} ${node.status}: ${node.agentLabel || node.nodeId}${node.error ? ` - ${node.error}` : ''}`);
|
|
1873
|
-
}
|
|
1874
|
-
}
|
|
1875
|
-
const outputs = nodeRuns
|
|
1876
|
-
.filter((node) => node.outputText)
|
|
1877
|
-
.slice(-3)
|
|
1878
|
-
.map((node) => `\n${node.agentLabel || node.nodeId}:\n${node.outputText}`);
|
|
1879
|
-
return truncate(`${lines.join('\n')}${outputs.join('\n')}`);
|
|
1880
|
-
}
|
|
1881
|
-
|
|
1882
|
-
async function monitorWorkflowRun({ bot, chatId, link, runId, activity = null }) {
|
|
1883
|
-
if (runMonitors.has(runId)) return;
|
|
1884
|
-
runMonitors.set(runId, true);
|
|
1885
|
-
const state = getState(link.user_id);
|
|
1886
|
-
const seenNodeStatus = new Map();
|
|
1887
|
-
try {
|
|
1888
|
-
for (;;) {
|
|
1889
|
-
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
1890
|
-
const run = await fetchRun(link.user_id, runId);
|
|
1891
|
-
const nodeRuns = Array.isArray(run.nodeRuns) ? run.nodeRuns : [];
|
|
1892
|
-
if (activity && !TERMINAL_RUN_STATES.has(run.status)) {
|
|
1893
|
-
activity.status = 'running';
|
|
1894
|
-
activity.phase = `${t(activity.lang, 'control.activity.workflowRunning')} (${run.status})`;
|
|
1895
|
-
await editTelegramActivity({ bot, chatId, activity });
|
|
1896
|
-
}
|
|
1897
|
-
if (state.progressMode === 'all') {
|
|
1898
|
-
for (const node of nodeRuns) {
|
|
1899
|
-
const key = `${node.nodeId}:${node.status}`;
|
|
1900
|
-
if (!seenNodeStatus.has(key)) {
|
|
1901
|
-
seenNodeStatus.set(key, true);
|
|
1902
|
-
if (activity) {
|
|
1903
|
-
pushActivityEvent(activity, `${node.status === 'completed' ? '✅' : node.status === 'failed' ? '❌' : '🔧'} ${node.agentLabel || node.nodeId}: ${node.status}${node.error ? ` - ${node.error}` : ''}`);
|
|
1904
|
-
activity.phase = t(activity.lang, 'control.activity.workflowRunning');
|
|
1905
|
-
await editTelegramActivity({ bot, chatId, activity });
|
|
1906
|
-
} else {
|
|
1907
|
-
await send(bot, chatId, `${node.agentLabel || node.nodeId}: ${node.status}${node.error ? `\n${node.error}` : ''}`);
|
|
1908
|
-
}
|
|
1909
|
-
}
|
|
1910
|
-
}
|
|
1911
|
-
}
|
|
1912
|
-
if (state.progressMode === 'errors') {
|
|
1913
|
-
for (const node of nodeRuns.filter((candidate) => candidate.error || candidate.status === 'failed')) {
|
|
1914
|
-
const key = `${node.nodeId}:${node.status}:${node.error || ''}`;
|
|
1915
|
-
if (!seenNodeStatus.has(key)) {
|
|
1916
|
-
seenNodeStatus.set(key, true);
|
|
1917
|
-
if (activity) {
|
|
1918
|
-
pushActivityEvent(activity, `❌ ${node.agentLabel || node.nodeId}: ${node.status}${node.error ? ` - ${node.error}` : ''}`);
|
|
1919
|
-
activity.phase = t(activity.lang, 'control.activity.workflowRunning');
|
|
1920
|
-
await editTelegramActivity({ bot, chatId, activity });
|
|
1921
|
-
} else {
|
|
1922
|
-
await send(bot, chatId, `${node.agentLabel || node.nodeId}: ${node.status}\n${node.error || ''}`);
|
|
1923
|
-
}
|
|
1924
|
-
}
|
|
1925
|
-
}
|
|
1926
|
-
}
|
|
1927
|
-
if (TERMINAL_RUN_STATES.has(run.status)) {
|
|
1928
|
-
if (activity) {
|
|
1929
|
-
activity.status = run.status === 'completed' ? 'done' : 'failed';
|
|
1930
|
-
activity.phase = run.status === 'completed'
|
|
1931
|
-
? t(activity.lang, 'control.activity.done')
|
|
1932
|
-
: t(activity.lang, 'control.activity.failed');
|
|
1933
|
-
activity.output = summarizeRun(run, state.progressMode);
|
|
1934
|
-
await editTelegramActivity({ bot, chatId, activity, force: true });
|
|
1935
|
-
} else {
|
|
1936
|
-
await send(bot, chatId, summarizeRun(run, state.progressMode));
|
|
1937
|
-
}
|
|
1938
|
-
return;
|
|
1939
|
-
}
|
|
1940
|
-
}
|
|
1941
|
-
} finally {
|
|
1942
|
-
runMonitors.delete(runId);
|
|
1943
|
-
}
|
|
1944
|
-
}
|
|
1945
|
-
|
|
1946
|
-
export async function startCliInstall({ bot, chatId, link, provider, editMessageId, confirmed = false }) {
|
|
1947
|
-
const lang = languageFor(link);
|
|
1948
|
-
if (!PROVIDERS.includes(provider)) {
|
|
1949
|
-
await send(bot, chatId, t(lang, 'control.providerAuthFallback'), { editMessageId });
|
|
1950
|
-
return;
|
|
1951
|
-
}
|
|
1952
|
-
const state = getState(link.user_id);
|
|
1953
|
-
if (state.remoteControlEnabled === false) {
|
|
1954
|
-
await send(bot, chatId, t(lang, 'control.disabled'), { editMessageId });
|
|
1955
|
-
return;
|
|
1956
|
-
}
|
|
1957
|
-
if (state.confirmationPolicy === 'strict' && !confirmed) {
|
|
1958
|
-
await requestConfirmation({
|
|
1959
|
-
bot,
|
|
1960
|
-
chatId,
|
|
1961
|
-
link,
|
|
1962
|
-
action: 'install_provider',
|
|
1963
|
-
payload: { provider },
|
|
1964
|
-
editMessageId,
|
|
1965
|
-
});
|
|
1966
|
-
return;
|
|
1967
|
-
}
|
|
1968
|
-
|
|
1969
|
-
const result = await runTelegramTool({
|
|
1970
|
-
userId: link.user_id,
|
|
1971
|
-
action: 'install_provider',
|
|
1972
|
-
execute: () => localApi(link.user_id, `/api/providers/${provider}/install`, { method: 'POST' }),
|
|
1973
|
-
});
|
|
1974
|
-
if (!result.ok) {
|
|
1975
|
-
await sendToolFailure({ bot, chatId, link, result, editMessageId });
|
|
1976
|
-
return;
|
|
1977
|
-
}
|
|
1978
|
-
|
|
1979
|
-
const data = result.data;
|
|
1980
|
-
if (data?.manual) {
|
|
1981
|
-
await send(bot, chatId, t(lang, 'control.manualInstall', { provider, manual: data.manual }), { editMessageId });
|
|
1982
|
-
return;
|
|
1983
|
-
}
|
|
1984
|
-
await send(bot, chatId, t(lang, 'control.installStarted', {
|
|
1985
|
-
provider,
|
|
1986
|
-
jobId: data.jobId,
|
|
1987
|
-
command: data.installCmd || 'internal installer',
|
|
1988
|
-
}), { editMessageId });
|
|
1989
|
-
}
|
|
1990
|
-
|
|
1991
|
-
async function showInstallMenu({ bot, chatId, link, editMessageId }) {
|
|
1992
|
-
const lang = languageFor(link);
|
|
1993
|
-
const buttons = PROVIDERS.map((provider) => button(provider, 'install_provider', { provider }));
|
|
1994
|
-
await send(bot, chatId, t(lang, 'control.installMenu'), {
|
|
1995
|
-
editMessageId,
|
|
1996
|
-
reply_markup: { inline_keyboard: rows(buttons, 2) },
|
|
1997
|
-
});
|
|
1998
|
-
}
|
|
1999
|
-
|
|
2000
|
-
async function showAuthMenu({ bot, chatId, link, editMessageId }) {
|
|
2001
|
-
const lang = languageFor(link);
|
|
2002
|
-
const buttons = PROVIDERS.map((provider) => button(provider, 'auth_provider', { provider }));
|
|
2003
|
-
await send(bot, chatId, t(lang, 'control.authMenu'), {
|
|
2004
|
-
editMessageId,
|
|
2005
|
-
reply_markup: { inline_keyboard: rows(buttons, 2) },
|
|
2006
|
-
});
|
|
2007
|
-
}
|
|
2008
|
-
|
|
2009
|
-
async function showSettings({ bot, chatId, link, editMessageId }) {
|
|
2010
|
-
const lang = languageFor(link);
|
|
2011
|
-
const state = getState(link.user_id);
|
|
2012
|
-
const keyboard = [
|
|
2013
|
-
[
|
|
2014
|
-
button(
|
|
2015
|
-
state.remoteControlEnabled
|
|
2016
|
-
? checked(t(lang, 'control.button.remoteOn'))
|
|
2017
|
-
: t(lang, 'control.button.remoteOff'),
|
|
2018
|
-
'toggle_control',
|
|
2019
|
-
),
|
|
2020
|
-
],
|
|
2021
|
-
[
|
|
2022
|
-
button(state.progressMode === 'final' ? checked(t(lang, 'control.button.progressFinal')) : t(lang, 'control.button.progressFinal'), 'progress_mode', { progressMode: 'final' }),
|
|
2023
|
-
button(state.progressMode === 'steps' ? checked(t(lang, 'control.button.progressSteps')) : t(lang, 'control.button.progressSteps'), 'progress_mode', { progressMode: 'steps' }),
|
|
2024
|
-
button(state.progressMode === 'errors' ? checked(t(lang, 'control.button.progressErrors')) : t(lang, 'control.button.progressErrors'), 'progress_mode', { progressMode: 'errors' }),
|
|
2025
|
-
button(state.progressMode === 'all' ? checked(t(lang, 'control.button.progressAll')) : t(lang, 'control.button.progressAll'), 'progress_mode', { progressMode: 'all' }),
|
|
2026
|
-
],
|
|
2027
|
-
[
|
|
2028
|
-
button(t(lang, 'control.button.language'), 'language_menu'),
|
|
2029
|
-
button(t(lang, 'control.button.mainMenu'), 'menu'),
|
|
2030
|
-
],
|
|
2031
|
-
];
|
|
2032
|
-
await send(bot, chatId, `${t(lang, 'control.settingsTitle')}\n\n${stateSummary(lang, state)}`, {
|
|
2033
|
-
editMessageId,
|
|
2034
|
-
reply_markup: { inline_keyboard: keyboard },
|
|
2035
|
-
});
|
|
2036
|
-
}
|
|
2037
|
-
|
|
2038
|
-
async function showLanguageMenu({ bot, chatId, link, editMessageId }) {
|
|
2039
|
-
const lang = languageFor(link);
|
|
2040
|
-
const buttons = SUPPORTED_LANGUAGES.map((language) => button(language, 'language_select', { language }));
|
|
2041
|
-
await send(bot, chatId, t(lang, 'control.pickLanguage'), {
|
|
2042
|
-
editMessageId,
|
|
2043
|
-
reply_markup: { inline_keyboard: rows(buttons, 3) },
|
|
2044
|
-
});
|
|
2045
|
-
}
|
|
2046
|
-
|
|
2047
|
-
async function handleAwaitingInput({ bot, chatId, link, text }) {
|
|
2048
|
-
const state = getState(link.user_id);
|
|
2049
|
-
const awaiting = state.awaiting;
|
|
2050
|
-
if (!awaiting?.type) return false;
|
|
2051
|
-
updateTelegramControlState(link.user_id, { awaiting: null });
|
|
2052
|
-
|
|
2053
|
-
if (awaiting.type === 'agent_prompt') {
|
|
2054
|
-
return launchLongTelegramTask({
|
|
2055
|
-
bot,
|
|
2056
|
-
chatId,
|
|
2057
|
-
link,
|
|
2058
|
-
kind: 'agent',
|
|
2059
|
-
task: () => runAgent({ bot, chatId, link, prompt: text }),
|
|
2060
|
-
});
|
|
2061
|
-
}
|
|
2062
|
-
if (awaiting.type === 'workflow_prompt') {
|
|
2063
|
-
return launchLongTelegramTask({
|
|
2064
|
-
bot,
|
|
2065
|
-
chatId,
|
|
2066
|
-
link,
|
|
2067
|
-
kind: 'workflow',
|
|
2068
|
-
task: () => runWorkflow({ bot, chatId, link, input: text }),
|
|
2069
|
-
});
|
|
2070
|
-
}
|
|
2071
|
-
return false;
|
|
2072
|
-
}
|
|
2073
|
-
|
|
2074
|
-
async function handleCommand({ bot, chatId, link, text }) {
|
|
2075
|
-
const [commandRaw, ...rest] = text.split(/\s+/);
|
|
2076
|
-
const command = normalizeCommandToken(commandRaw);
|
|
2077
|
-
const argText = rest.join(' ').trim();
|
|
2078
|
-
const lang = languageFor(link);
|
|
2079
|
-
|
|
2080
|
-
if (command === '/menu' || command === '/start' || command === 'menu') {
|
|
2081
|
-
await showMainMenu({ bot, chatId, link });
|
|
2082
|
-
return true;
|
|
2083
|
-
}
|
|
2084
|
-
if (command === '/help') {
|
|
2085
|
-
await showHelp({ bot, chatId, link });
|
|
2086
|
-
return true;
|
|
2087
|
-
}
|
|
2088
|
-
if (command === '/projects') {
|
|
2089
|
-
await showProjectMenu({ bot, chatId, link });
|
|
2090
|
-
return true;
|
|
2091
|
-
}
|
|
2092
|
-
if (command === '/provider') {
|
|
2093
|
-
await showProviderMenu({ bot, chatId, link });
|
|
2094
|
-
return true;
|
|
2095
|
-
}
|
|
2096
|
-
if (command === '/model' || command === '/models') {
|
|
2097
|
-
await showModelMenu({ bot, chatId, link, refresh: command === '/models' && argText === 'refresh' });
|
|
2098
|
-
return true;
|
|
2099
|
-
}
|
|
2100
|
-
if (command === '/workflows' || command === '/orchestration') {
|
|
2101
|
-
await showWorkflowMenu({ bot, chatId, link });
|
|
2102
|
-
return true;
|
|
2103
|
-
}
|
|
2104
|
-
if (command === '/runs') {
|
|
2105
|
-
await showRuns({ bot, chatId, link });
|
|
2106
|
-
return true;
|
|
2107
|
-
}
|
|
2108
|
-
if (command === '/approvals') {
|
|
2109
|
-
await showApprovalQueue({ bot, chatId, link });
|
|
2110
|
-
return true;
|
|
2111
|
-
}
|
|
2112
|
-
if (command === '/controlroom' || command === '/control-room') {
|
|
2113
|
-
await showControlRoom({ bot, chatId, link });
|
|
2114
|
-
return true;
|
|
2115
|
-
}
|
|
2116
|
-
if (command === '/webhooks') {
|
|
2117
|
-
await showWebhookMenu({ bot, chatId, link });
|
|
2118
|
-
return true;
|
|
2119
|
-
}
|
|
2120
|
-
if (command === '/sessions') {
|
|
2121
|
-
await showSessions({ bot, chatId, link });
|
|
2122
|
-
return true;
|
|
2123
|
-
}
|
|
2124
|
-
if (command === '/terminal') {
|
|
2125
|
-
await showActiveTerminalStatus({ bot, chatId, link });
|
|
2126
|
-
return true;
|
|
2127
|
-
}
|
|
2128
|
-
if (command === '/detach') {
|
|
2129
|
-
await detachActiveTerminal({ bot, chatId, link });
|
|
2130
|
-
return true;
|
|
2131
|
-
}
|
|
2132
|
-
if (command === '/newchat') {
|
|
2133
|
-
await startNewChat({ bot, chatId, link });
|
|
2134
|
-
return true;
|
|
2135
|
-
}
|
|
2136
|
-
if (command === '/settings') {
|
|
2137
|
-
await showSettings({ bot, chatId, link });
|
|
2138
|
-
return true;
|
|
2139
|
-
}
|
|
2140
|
-
if (command === '/install') {
|
|
2141
|
-
if (PROVIDERS.includes(argText)) await startCliInstall({ bot, chatId, link, provider: argText });
|
|
2142
|
-
else await showInstallMenu({ bot, chatId, link });
|
|
2143
|
-
return true;
|
|
2144
|
-
}
|
|
2145
|
-
if (command === '/auth') {
|
|
2146
|
-
if (PROVIDERS.includes(argText)) await send(bot, chatId, `${argText} login:\n${AUTH_HELP[argText]}`);
|
|
2147
|
-
else await showAuthMenu({ bot, chatId, link });
|
|
2148
|
-
return true;
|
|
2149
|
-
}
|
|
2150
|
-
if (command === '/control') {
|
|
2151
|
-
const enabled = argText !== 'off';
|
|
2152
|
-
updateTelegramControlState(link.user_id, { remoteControlEnabled: enabled });
|
|
2153
|
-
await send(bot, chatId, enabled ? t(lang, 'control.remoteEnabled') : t(lang, 'control.remoteDisabled'));
|
|
2154
|
-
return true;
|
|
2155
|
-
}
|
|
2156
|
-
if (command === '/progress') {
|
|
2157
|
-
if (!['final', 'steps', 'all', 'errors'].includes(argText)) {
|
|
2158
|
-
await send(bot, chatId, t(lang, 'control.progressUsage'));
|
|
2159
|
-
return true;
|
|
2160
|
-
}
|
|
2161
|
-
updateTelegramControlState(link.user_id, { progressMode: argText });
|
|
2162
|
-
await send(bot, chatId, t(lang, 'control.progressSet', { mode: argText }));
|
|
2163
|
-
return true;
|
|
2164
|
-
}
|
|
2165
|
-
if (command === '/chat') {
|
|
2166
|
-
if (!argText) {
|
|
2167
|
-
updateTelegramControlState(link.user_id, { awaiting: { type: 'agent_prompt' } });
|
|
2168
|
-
await send(bot, chatId, t(lang, 'control.sendAgentPrompt'));
|
|
2169
|
-
return true;
|
|
2170
|
-
}
|
|
2171
|
-
return launchLongTelegramTask({
|
|
2172
|
-
bot,
|
|
2173
|
-
chatId,
|
|
2174
|
-
link,
|
|
2175
|
-
kind: 'agent',
|
|
2176
|
-
task: () => runAgent({ bot, chatId, link, prompt: argText }),
|
|
2177
|
-
});
|
|
2178
|
-
}
|
|
2179
|
-
if (command === '/workflow' || command === '/orchestrate') {
|
|
2180
|
-
if (!argText) {
|
|
2181
|
-
updateTelegramControlState(link.user_id, { awaiting: { type: 'workflow_prompt' } });
|
|
2182
|
-
await send(bot, chatId, t(lang, 'control.sendWorkflowPrompt'));
|
|
2183
|
-
return true;
|
|
2184
|
-
}
|
|
2185
|
-
return launchLongTelegramTask({
|
|
2186
|
-
bot,
|
|
2187
|
-
chatId,
|
|
2188
|
-
link,
|
|
2189
|
-
kind: 'workflow',
|
|
2190
|
-
task: () => runWorkflow({ bot, chatId, link, input: argText }),
|
|
2191
|
-
});
|
|
2192
|
-
}
|
|
2193
|
-
if (command === '/cancel') {
|
|
2194
|
-
if (!argText) {
|
|
2195
|
-
await send(bot, chatId, t(lang, 'control.cancelUsage'));
|
|
2196
|
-
return true;
|
|
2197
|
-
}
|
|
2198
|
-
await cancelRun({ bot, chatId, link, runId: argText });
|
|
2199
|
-
return true;
|
|
2200
|
-
}
|
|
2201
|
-
return false;
|
|
2202
|
-
}
|
|
2203
|
-
|
|
2204
|
-
async function handleTelegramControlMessageInternal({ bot, msg, link }) {
|
|
2205
|
-
const chatId = msg.chat.id;
|
|
2206
|
-
const text = String(msg.text || '').trim();
|
|
2207
|
-
if (!text) return false;
|
|
2208
|
-
|
|
2209
|
-
if (text === '/') {
|
|
2210
|
-
await showCommandPalette({ bot, chatId, link });
|
|
2211
|
-
return true;
|
|
2212
|
-
}
|
|
2213
|
-
if (text.startsWith('/') && !getTelegramControlCommand(text)) {
|
|
2214
|
-
await showCommandPalette({ bot, chatId, link, unknown: true });
|
|
2215
|
-
return true;
|
|
2216
|
-
}
|
|
2217
|
-
|
|
2218
|
-
if (await handleAwaitingInput({ bot, chatId, link, text })) return true;
|
|
2219
|
-
if (await handleCommand({ bot, chatId, link, text })) return true;
|
|
2220
|
-
|
|
2221
|
-
const state = getState(link.user_id);
|
|
2222
|
-
if (getActiveTerminal(state)) {
|
|
2223
|
-
return sendToActiveTerminal({ bot, chatId, link, text });
|
|
2224
|
-
}
|
|
2225
|
-
if (state.routerEnabled === false) return false;
|
|
2226
|
-
if (!state.remoteControlEnabled) {
|
|
2227
|
-
await send(bot, chatId, t(languageFor(link), 'control.disabled'));
|
|
2228
|
-
return true;
|
|
2229
|
-
}
|
|
2230
|
-
|
|
2231
|
-
const activity = await createTelegramActivity({
|
|
2232
|
-
bot,
|
|
2233
|
-
chatId,
|
|
2234
|
-
link,
|
|
2235
|
-
type: 'router',
|
|
2236
|
-
prompt: text,
|
|
2237
|
-
phase: t(languageFor(link), 'control.activity.interpreting'),
|
|
2238
|
-
});
|
|
2239
|
-
return handleRoutedIntent({ bot, chatId, link, text, activity });
|
|
2240
|
-
}
|
|
2241
|
-
|
|
2242
|
-
export async function handleTelegramControlMessage(args) {
|
|
2243
|
-
const chatId = args?.msg?.chat?.id;
|
|
2244
|
-
if (!chatId) return false;
|
|
2245
|
-
return enqueueTelegramJob(chatId, async () => {
|
|
2246
|
-
try {
|
|
2247
|
-
return await handleTelegramControlMessageInternal(args);
|
|
2248
|
-
} catch (error) {
|
|
2249
|
-
console.error('[telegram-control] message handler failed:', error);
|
|
2250
|
-
await send(args.bot, chatId, t(languageFor(args.link), 'error.generic')).catch(() => {});
|
|
2251
|
-
return true;
|
|
2252
|
-
}
|
|
2253
|
-
}).catch(async (error) => {
|
|
2254
|
-
console.error('[telegram-control] message job failed:', error);
|
|
2255
|
-
await send(args.bot, chatId, t(languageFor(args.link), 'error.generic')).catch(() => {});
|
|
2256
|
-
return true;
|
|
2257
|
-
});
|
|
2258
|
-
}
|
|
2259
|
-
|
|
2260
|
-
async function showRunDetail({ bot, chatId, link, runId, editMessageId }) {
|
|
2261
|
-
const lang = languageFor(link);
|
|
2262
|
-
const run = await fetchRun(link.user_id, runId);
|
|
2263
|
-
await send(bot, chatId, summarizeRun(run, 'all'), {
|
|
2264
|
-
editMessageId,
|
|
2265
|
-
reply_markup: {
|
|
2266
|
-
inline_keyboard: [
|
|
2267
|
-
[button(t(lang, 'control.button.cancelRun'), 'run_cancel', { runId }), button(t(lang, 'control.button.refresh'), 'run_detail', { runId })],
|
|
2268
|
-
[button(t(lang, 'control.button.runs'), 'runs'), button(t(lang, 'control.button.mainMenu'), 'menu')],
|
|
2269
|
-
],
|
|
2270
|
-
},
|
|
2271
|
-
});
|
|
2272
|
-
}
|
|
2273
|
-
|
|
2274
|
-
async function handleTelegramControlCallbackInternal({ bot, query, link }) {
|
|
2275
|
-
const chatId = query.message?.chat?.id;
|
|
2276
|
-
if (!chatId) return;
|
|
2277
|
-
const editMessageId = query.message?.message_id;
|
|
2278
|
-
const entry = readAction(query.data);
|
|
2279
|
-
if (!entry) {
|
|
2280
|
-
const lang = languageFor(link);
|
|
2281
|
-
await bot.answerCallbackQuery(query.id, { text: t(lang, 'control.menuExpired') }).catch(() => {});
|
|
2282
|
-
await send(bot, chatId, t(lang, 'control.menuExpiredLong'));
|
|
2283
|
-
return;
|
|
2284
|
-
}
|
|
2285
|
-
await bot.answerCallbackQuery(query.id).catch(() => {});
|
|
2286
|
-
|
|
2287
|
-
const { action, payload } = entry;
|
|
2288
|
-
if (action === 'confirm_action') {
|
|
2289
|
-
forgetAction(entry.id);
|
|
2290
|
-
const lang = languageFor(link);
|
|
2291
|
-
const result = consumeTelegramConfirmation(link.user_id, payload.id);
|
|
2292
|
-
if (!result.ok) {
|
|
2293
|
-
await send(bot, chatId, t(lang, result.reason === 'expired'
|
|
2294
|
-
? 'control.confirmationExpired'
|
|
2295
|
-
: 'control.confirmationMissing'), { editMessageId });
|
|
2296
|
-
return;
|
|
2297
|
-
}
|
|
2298
|
-
if (await executeConfirmedAction({
|
|
2299
|
-
bot,
|
|
2300
|
-
chatId,
|
|
2301
|
-
link,
|
|
2302
|
-
confirmation: result.confirmation,
|
|
2303
|
-
editMessageId,
|
|
2304
|
-
})) return;
|
|
2305
|
-
await send(bot, chatId, t(lang, 'error.generic'), { editMessageId });
|
|
2306
|
-
return;
|
|
2307
|
-
}
|
|
2308
|
-
if (action === 'cancel_confirmation') {
|
|
2309
|
-
forgetAction(entry.id);
|
|
2310
|
-
clearTelegramConfirmation(link.user_id, payload.id);
|
|
2311
|
-
await send(bot, chatId, t(languageFor(link), 'control.confirmationCanceled'), { editMessageId });
|
|
2312
|
-
return;
|
|
2313
|
-
}
|
|
2314
|
-
if (action === 'menu') return showMainMenu({ bot, chatId, link, editMessageId });
|
|
2315
|
-
if (action === 'projects') return showProjectMenu({ bot, chatId, link, editMessageId });
|
|
2316
|
-
if (action === 'providers') return showProviderMenu({ bot, chatId, link, editMessageId });
|
|
2317
|
-
if (action === 'models') return showModelMenu({ bot, chatId, link, editMessageId });
|
|
2318
|
-
if (action === 'models_refresh') return showModelMenu({ bot, chatId, link, refresh: true, editMessageId });
|
|
2319
|
-
if (action === 'workflows') return showWorkflowMenu({ bot, chatId, link, editMessageId });
|
|
2320
|
-
if (action === 'runs') return showRuns({ bot, chatId, link, editMessageId });
|
|
2321
|
-
if (action === 'approvals') return showApprovalQueue({ bot, chatId, link, editMessageId });
|
|
2322
|
-
if (action === 'control_room') return showControlRoom({ bot, chatId, link, editMessageId });
|
|
2323
|
-
if (action === 'webhooks') return showWebhookMenu({ bot, chatId, link, editMessageId });
|
|
2324
|
-
if (action === 'sessions') return showSessions({ bot, chatId, link, editMessageId });
|
|
2325
|
-
if (action === 'terminal_status') return showActiveTerminalStatus({ bot, chatId, link, editMessageId });
|
|
2326
|
-
if (action === 'detach_terminal') return detachActiveTerminal({ bot, chatId, link, editMessageId });
|
|
2327
|
-
if (action === 'new_chat') return startNewChat({ bot, chatId, link, editMessageId });
|
|
2328
|
-
if (action === 'install_menu') return showInstallMenu({ bot, chatId, link, editMessageId });
|
|
2329
|
-
if (action === 'auth_menu') return showAuthMenu({ bot, chatId, link, editMessageId });
|
|
2330
|
-
if (action === 'settings') return showSettings({ bot, chatId, link, editMessageId });
|
|
2331
|
-
if (action === 'language_menu') return showLanguageMenu({ bot, chatId, link, editMessageId });
|
|
2332
|
-
if (action === 'project_select') {
|
|
2333
|
-
updateTelegramControlState(link.user_id, {
|
|
2334
|
-
selectedProjectName: payload.name,
|
|
2335
|
-
selectedProjectPath: payload.path,
|
|
2336
|
-
});
|
|
2337
|
-
const lang = languageFor(link);
|
|
2338
|
-
return showMainMenu({
|
|
2339
|
-
bot,
|
|
2340
|
-
chatId,
|
|
2341
|
-
link,
|
|
2342
|
-
editMessageId,
|
|
2343
|
-
notice: t(lang, 'control.projectSelected', { project: payload.displayName || payload.name, path: payload.path }),
|
|
2344
|
-
});
|
|
2345
|
-
}
|
|
2346
|
-
if (action === 'provider_select') {
|
|
2347
|
-
updateTelegramControlState(link.user_id, { selectedProvider: payload.provider, selectedModel: null });
|
|
2348
|
-
return showModelMenu({ bot, chatId, link, editMessageId });
|
|
2349
|
-
}
|
|
2350
|
-
if (action === 'model_select') {
|
|
2351
|
-
updateTelegramControlState(link.user_id, { selectedModel: payload.model || null });
|
|
2352
|
-
const lang = languageFor(link);
|
|
2353
|
-
return showMainMenu({
|
|
2354
|
-
bot,
|
|
2355
|
-
chatId,
|
|
2356
|
-
link,
|
|
2357
|
-
editMessageId,
|
|
2358
|
-
notice: payload.model
|
|
2359
|
-
? t(lang, 'control.modelSelected', { model: payload.model })
|
|
2360
|
-
: t(lang, 'control.modelCleared'),
|
|
2361
|
-
});
|
|
2362
|
-
}
|
|
2363
|
-
if (action === 'workflow_select') {
|
|
2364
|
-
updateTelegramControlState(link.user_id, { selectedWorkflowId: payload.workflowId });
|
|
2365
|
-
const lang = languageFor(link);
|
|
2366
|
-
return showMainMenu({
|
|
2367
|
-
bot,
|
|
2368
|
-
chatId,
|
|
2369
|
-
link,
|
|
2370
|
-
editMessageId,
|
|
2371
|
-
notice: t(lang, 'control.workflowSelected', { workflow: payload.name || payload.workflowId }),
|
|
2372
|
-
});
|
|
2373
|
-
}
|
|
2374
|
-
if (action === 'workflow_prompt') {
|
|
2375
|
-
updateTelegramControlState(link.user_id, { awaiting: { type: 'workflow_prompt' } });
|
|
2376
|
-
await send(bot, chatId, t(languageFor(link), 'control.sendWorkflowPrompt'), { editMessageId });
|
|
2377
|
-
return;
|
|
2378
|
-
}
|
|
2379
|
-
if (action === 'run_detail') return showRunDetail({ bot, chatId, link, runId: payload.runId, editMessageId });
|
|
2380
|
-
if (action === 'run_cancel') {
|
|
2381
|
-
await cancelRun({ bot, chatId, link, runId: payload.runId, editMessageId });
|
|
2382
|
-
return;
|
|
2383
|
-
}
|
|
2384
|
-
if (action === 'approval_decide') {
|
|
2385
|
-
const toolResult = await runTelegramTool({
|
|
2386
|
-
userId: link.user_id,
|
|
2387
|
-
action: 'approval_decide',
|
|
2388
|
-
execute: () => localApi(link.user_id, `/api/orchestration/workflows/approvals/${encodeURIComponent(payload.approvalId)}`, {
|
|
2389
|
-
method: 'POST',
|
|
2390
|
-
body: {
|
|
2391
|
-
allow: payload.allow === true,
|
|
2392
|
-
source: 'telegram',
|
|
2393
|
-
},
|
|
2394
|
-
}),
|
|
2395
|
-
});
|
|
2396
|
-
if (!toolResult.ok) {
|
|
2397
|
-
await sendToolFailure({ bot, chatId, link, result: toolResult, editMessageId });
|
|
2398
|
-
return;
|
|
2399
|
-
}
|
|
2400
|
-
const result = toolResult.data;
|
|
2401
|
-
const lang = languageFor(link);
|
|
2402
|
-
await send(bot, chatId, t(lang, 'control.approvalDecided', {
|
|
2403
|
-
approvalId: payload.approvalId,
|
|
2404
|
-
status: payload.allow === true ? 'approved' : 'denied',
|
|
2405
|
-
runId: result?.runId || '',
|
|
2406
|
-
}), { editMessageId });
|
|
2407
|
-
return showApprovalQueue({ bot, chatId, link });
|
|
2408
|
-
}
|
|
2409
|
-
if (action === 'install_provider') return startCliInstall({ bot, chatId, link, provider: payload.provider, editMessageId });
|
|
2410
|
-
if (action === 'auth_provider') {
|
|
2411
|
-
await send(bot, chatId, `${payload.provider} login:\n${AUTH_HELP[payload.provider] || t(languageFor(link), 'control.providerAuthFallback')}`, { editMessageId });
|
|
2412
|
-
return;
|
|
2413
|
-
}
|
|
2414
|
-
if (action === 'toggle_control') {
|
|
2415
|
-
const state = getState(link.user_id);
|
|
2416
|
-
updateTelegramControlState(link.user_id, { remoteControlEnabled: !state.remoteControlEnabled });
|
|
2417
|
-
return showSettings({ bot, chatId, link, editMessageId });
|
|
2418
|
-
}
|
|
2419
|
-
if (action === 'progress_mode') {
|
|
2420
|
-
updateTelegramControlState(link.user_id, { progressMode: payload.progressMode });
|
|
2421
|
-
return showSettings({ bot, chatId, link, editMessageId });
|
|
2422
|
-
}
|
|
2423
|
-
if (action === 'language_select') {
|
|
2424
|
-
telegramLinksDb.updatePreferences(link.user_id, { language: payload.language });
|
|
2425
|
-
return showMainMenu({
|
|
2426
|
-
bot,
|
|
2427
|
-
chatId,
|
|
2428
|
-
link: { ...link, language: payload.language },
|
|
2429
|
-
editMessageId,
|
|
2430
|
-
notice: t(payload.language, 'control.languageSet', { language: payload.language }),
|
|
2431
|
-
});
|
|
2432
|
-
}
|
|
2433
|
-
}
|
|
2434
|
-
|
|
2435
|
-
export async function handleTelegramControlCallback(args) {
|
|
2436
|
-
const chatId = args?.query?.message?.chat?.id;
|
|
2437
|
-
if (!chatId) return;
|
|
2438
|
-
return enqueueTelegramJob(chatId, async () => {
|
|
2439
|
-
try {
|
|
2440
|
-
await handleTelegramControlCallbackInternal(args);
|
|
2441
|
-
} catch (error) {
|
|
2442
|
-
console.error('[telegram-control] callback failed:', error);
|
|
2443
|
-
const lang = languageFor(args.link);
|
|
2444
|
-
await args.bot?.answerCallbackQuery(args.query?.id, { text: t(lang, 'error.generic') }).catch(() => {});
|
|
2445
|
-
await send(args.bot, chatId, t(lang, 'error.generic')).catch(() => {});
|
|
2446
|
-
}
|
|
2447
|
-
}).catch(async (error) => {
|
|
2448
|
-
console.error('[telegram-control] callback job failed:', error);
|
|
2449
|
-
const lang = languageFor(args.link);
|
|
2450
|
-
await args.bot?.answerCallbackQuery(args.query?.id, { text: t(lang, 'error.generic') }).catch(() => {});
|
|
2451
|
-
await send(args.bot, chatId, t(lang, 'error.generic')).catch(() => {});
|
|
2452
|
-
});
|
|
2453
|
-
}
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import { apiKeysDb, telegramLinksDb } from '../../database/db.js';
|
|
4
|
+
import { getProjects } from '../../projects.js';
|
|
5
|
+
import { getStaticProviderModels } from '../model-registry.js';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
TELEGRAM_CONTROL_SCOPES,
|
|
9
|
+
TELEGRAM_PROVIDERS,
|
|
10
|
+
buildTelegramAgentPrompt,
|
|
11
|
+
buildTelegramIntentPrompt,
|
|
12
|
+
clearTelegramConfirmation,
|
|
13
|
+
consumeTelegramConfirmation,
|
|
14
|
+
createTelegramConfirmation,
|
|
15
|
+
enqueueTelegramJob,
|
|
16
|
+
parseTelegramAiIntentResponse,
|
|
17
|
+
resolveTelegramModel,
|
|
18
|
+
resolveTelegramProvider,
|
|
19
|
+
retryWithBackoff,
|
|
20
|
+
runTelegramTool,
|
|
21
|
+
splitTelegramText,
|
|
22
|
+
} from './telegram-gateway.js';
|
|
23
|
+
import { SUPPORTED_LANGUAGES, t } from './translations.js';
|
|
24
|
+
|
|
25
|
+
const PROVIDERS = TELEGRAM_PROVIDERS;
|
|
26
|
+
const TERMINAL_RUN_STATES = new Set(['completed', 'failed', 'canceled']);
|
|
27
|
+
const CALLBACK_TTL_MS = 10 * 60 * 1000;
|
|
28
|
+
const MAX_CALLBACK_ACTIONS = 1000;
|
|
29
|
+
const MAX_TELEGRAM_TEXT = 3600;
|
|
30
|
+
const MAX_ACTIVITY_OUTPUT_CHARS = 48_000;
|
|
31
|
+
const MAX_SSE_BUFFER_CHARS = 256_000;
|
|
32
|
+
const ACTIVITY_EDIT_THROTTLE_MS = 1200;
|
|
33
|
+
const ACTIVITY_HEARTBEAT_MS = 8000;
|
|
34
|
+
const INTENT_ROUTER_TIMEOUT_MS = 45_000;
|
|
35
|
+
const TERMINAL_BRIDGE_TIMEOUT_MS = 5 * 60 * 1000;
|
|
36
|
+
const TERMINAL_BRIDGE_POLL_MS = 1500;
|
|
37
|
+
const TERMINAL_BRIDGE_EDIT_THROTTLE_MS = 3500;
|
|
38
|
+
const TERMINAL_BRIDGE_SETTLE_MS = 6000;
|
|
39
|
+
const TERMINAL_BRIDGE_FINAL_OPEN = '<PIXCODE_TELEGRAM_FINAL>';
|
|
40
|
+
const TERMINAL_BRIDGE_FINAL_CLOSE = '</PIXCODE_TELEGRAM_FINAL>';
|
|
41
|
+
const callbackActions = new Map();
|
|
42
|
+
const runMonitors = new Map();
|
|
43
|
+
const activeLongTasks = new Map();
|
|
44
|
+
const terminalBridgeMonitors = new Map();
|
|
45
|
+
|
|
46
|
+
const MODEL_FALLBACKS = Object.fromEntries(
|
|
47
|
+
PROVIDERS.map((provider) => [provider, getStaticProviderModels(provider)]),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const AUTH_HELP = {
|
|
51
|
+
claude: '`claude login`',
|
|
52
|
+
cursor: '`cursor-agent login`',
|
|
53
|
+
codex: '`codex login`',
|
|
54
|
+
gemini: '`gemini auth login`',
|
|
55
|
+
qwen: '`qwen auth`',
|
|
56
|
+
opencode: '`opencode auth login`',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const CONTROL_COMMANDS = new Set([
|
|
60
|
+
'/menu',
|
|
61
|
+
'/start',
|
|
62
|
+
'/help',
|
|
63
|
+
'/projects',
|
|
64
|
+
'/provider',
|
|
65
|
+
'/model',
|
|
66
|
+
'/models',
|
|
67
|
+
'/workflows',
|
|
68
|
+
'/orchestration',
|
|
69
|
+
'/runs',
|
|
70
|
+
'/approvals',
|
|
71
|
+
'/controlroom',
|
|
72
|
+
'/control-room',
|
|
73
|
+
'/webhooks',
|
|
74
|
+
'/sessions',
|
|
75
|
+
'/newchat',
|
|
76
|
+
'/settings',
|
|
77
|
+
'/install',
|
|
78
|
+
'/auth',
|
|
79
|
+
'/control',
|
|
80
|
+
'/progress',
|
|
81
|
+
'/chat',
|
|
82
|
+
'/workflow',
|
|
83
|
+
'/orchestrate',
|
|
84
|
+
'/cancel',
|
|
85
|
+
'/terminal',
|
|
86
|
+
'/detach',
|
|
87
|
+
'menu',
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
function compact(text, max = 80) {
|
|
91
|
+
const value = String(text || '').replace(/\s+/g, ' ').trim();
|
|
92
|
+
return value.length > max ? `${value.slice(0, max - 1)}…` : value;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function truncate(text, max = MAX_TELEGRAM_TEXT) {
|
|
96
|
+
const value = String(text || '').trim();
|
|
97
|
+
return value.length > max ? `${value.slice(0, max - 20)}\n\n…truncated…` : value;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function appendBoundedText(current, chunk, maxChars = MAX_ACTIVITY_OUTPUT_CHARS) {
|
|
101
|
+
const nextChunk = String(chunk || '');
|
|
102
|
+
if (!nextChunk) return { text: current || '', truncated: false };
|
|
103
|
+
const combined = `${current || ''}${nextChunk}`;
|
|
104
|
+
if (combined.length <= maxChars) return { text: combined, truncated: false };
|
|
105
|
+
return { text: combined.slice(-maxChars), truncated: true };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function languageFor(link) {
|
|
109
|
+
return SUPPORTED_LANGUAGES.includes(link?.language) ? link.language : 'en';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function normalizeCommandToken(commandRaw = '') {
|
|
113
|
+
return String(commandRaw || '')
|
|
114
|
+
.trim()
|
|
115
|
+
.toLowerCase()
|
|
116
|
+
.replace(/@[^@\s]+$/, '');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function getTelegramControlCommand(text = '') {
|
|
120
|
+
const [commandRaw = ''] = String(text || '').trim().split(/\s+/);
|
|
121
|
+
const command = normalizeCommandToken(commandRaw);
|
|
122
|
+
return CONTROL_COMMANDS.has(command) ? command : null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function isTelegramControlCommand(text = '') {
|
|
126
|
+
return Boolean(getTelegramControlCommand(text));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function getState(userId) {
|
|
130
|
+
return telegramLinksDb.getControlState(userId);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function updateTelegramControlState(userId, patch) {
|
|
134
|
+
return telegramLinksDb.updateControlState(userId, patch);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function pruneCallbackActions() {
|
|
138
|
+
const now = Date.now();
|
|
139
|
+
for (const [id, entry] of callbackActions.entries()) {
|
|
140
|
+
if (entry.expiresAt < now) {
|
|
141
|
+
callbackActions.delete(id);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
while (callbackActions.size > MAX_CALLBACK_ACTIONS) {
|
|
146
|
+
const oldestId = callbackActions.keys().next().value;
|
|
147
|
+
if (!oldestId) break;
|
|
148
|
+
callbackActions.delete(oldestId);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function registerAction(action, payload = {}) {
|
|
153
|
+
pruneCallbackActions();
|
|
154
|
+
const id = crypto.randomBytes(8).toString('hex');
|
|
155
|
+
callbackActions.set(id, {
|
|
156
|
+
action,
|
|
157
|
+
payload,
|
|
158
|
+
expiresAt: Date.now() + CALLBACK_TTL_MS,
|
|
159
|
+
});
|
|
160
|
+
return `tc:${id}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function readAction(data) {
|
|
164
|
+
if (typeof data !== 'string' || !data.startsWith('tc:')) return null;
|
|
165
|
+
const id = data.slice(3);
|
|
166
|
+
const entry = callbackActions.get(id);
|
|
167
|
+
if (!entry) return null;
|
|
168
|
+
if (entry.expiresAt < Date.now()) {
|
|
169
|
+
callbackActions.delete(id);
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
return { id, ...entry };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function forgetAction(id) {
|
|
176
|
+
if (id) callbackActions.delete(id);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function button(text, action, payload = {}) {
|
|
180
|
+
return { text, callback_data: registerAction(action, payload) };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function rows(buttons, width = 2) {
|
|
184
|
+
const out = [];
|
|
185
|
+
for (let i = 0; i < buttons.length; i += width) out.push(buttons.slice(i, i + width));
|
|
186
|
+
return out;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function send(bot, chatId, text, options = {}) {
|
|
190
|
+
const { editMessageId, ...telegramOptions } = options;
|
|
191
|
+
const extra = {
|
|
192
|
+
parse_mode: 'Markdown',
|
|
193
|
+
disable_web_page_preview: true,
|
|
194
|
+
...telegramOptions,
|
|
195
|
+
};
|
|
196
|
+
const chunks = splitTelegramText(text, MAX_TELEGRAM_TEXT);
|
|
197
|
+
const [firstChunk = ''] = chunks;
|
|
198
|
+
let startIndex = 0;
|
|
199
|
+
if (editMessageId && typeof bot.editMessageText === 'function') {
|
|
200
|
+
try {
|
|
201
|
+
const edited = await bot.editMessageText(firstChunk, {
|
|
202
|
+
chat_id: chatId,
|
|
203
|
+
message_id: editMessageId,
|
|
204
|
+
...extra,
|
|
205
|
+
});
|
|
206
|
+
startIndex = 1;
|
|
207
|
+
for (const chunk of chunks.slice(startIndex)) {
|
|
208
|
+
await send(bot, chatId, chunk, telegramOptions);
|
|
209
|
+
}
|
|
210
|
+
return edited;
|
|
211
|
+
} catch (err) {
|
|
212
|
+
const description = err?.response?.body?.description || err?.message || '';
|
|
213
|
+
if (/message is not modified/i.test(description)) {
|
|
214
|
+
startIndex = 1;
|
|
215
|
+
for (const chunk of chunks.slice(startIndex)) {
|
|
216
|
+
await send(bot, chatId, chunk, telegramOptions);
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
console.warn('[telegram-control] editMessageText failed:', description || err);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
let result = null;
|
|
224
|
+
for (const chunk of chunks.slice(startIndex)) {
|
|
225
|
+
try {
|
|
226
|
+
result = await bot.sendMessage(chatId, chunk, extra);
|
|
227
|
+
} catch {
|
|
228
|
+
const fallback = { ...extra };
|
|
229
|
+
delete fallback.parse_mode;
|
|
230
|
+
result = await bot.sendMessage(chatId, chunk, fallback);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return result;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function localApiBase() {
|
|
237
|
+
const port = process.env.SERVER_PORT || process.env.PORT || '3001';
|
|
238
|
+
return `http://127.0.0.1:${port}`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function getOrCreateTelegramApiKey(userId) {
|
|
242
|
+
const existing = apiKeysDb
|
|
243
|
+
.getApiKeys(userId)
|
|
244
|
+
.find((key) => key.key_name === 'Telegram Control' && Boolean(key.is_active));
|
|
245
|
+
if (existing?.api_key) {
|
|
246
|
+
const existingScopes = apiKeysDb.normalizeScopes(existing.scopes || []);
|
|
247
|
+
const nextScopes = apiKeysDb.normalizeScopes([...existingScopes, ...TELEGRAM_CONTROL_SCOPES]);
|
|
248
|
+
const hasAllScopes = TELEGRAM_CONTROL_SCOPES.every((scope) => existingScopes.includes(scope));
|
|
249
|
+
if (!hasAllScopes || nextScopes.length !== existingScopes.length) {
|
|
250
|
+
apiKeysDb.updateApiKeyScopes(userId, existing.id, nextScopes);
|
|
251
|
+
}
|
|
252
|
+
return existing.api_key;
|
|
253
|
+
}
|
|
254
|
+
return apiKeysDb.createApiKey(userId, 'Telegram Control', TELEGRAM_CONTROL_SCOPES).apiKey;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function localApi(userId, path, { method = 'GET', body, timeoutMs = 0 } = {}) {
|
|
258
|
+
const apiKey = getOrCreateTelegramApiKey(userId);
|
|
259
|
+
const response = await retryWithBackoff(async () => {
|
|
260
|
+
const controller = timeoutMs > 0 ? new AbortController() : null;
|
|
261
|
+
const timeoutId = controller
|
|
262
|
+
? setTimeout(() => controller.abort(new Error(`HTTP request timed out after ${timeoutMs}ms`)), timeoutMs)
|
|
263
|
+
: null;
|
|
264
|
+
const res = await fetch(`${localApiBase()}${path}`, {
|
|
265
|
+
method,
|
|
266
|
+
headers: {
|
|
267
|
+
'Content-Type': 'application/json',
|
|
268
|
+
'X-API-Key': apiKey,
|
|
269
|
+
},
|
|
270
|
+
signal: controller?.signal,
|
|
271
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
272
|
+
}).finally(() => {
|
|
273
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
274
|
+
});
|
|
275
|
+
if ([408, 429, 500, 502, 503, 504].includes(res.status)) {
|
|
276
|
+
const error = new Error(`HTTP ${res.status}`);
|
|
277
|
+
error.status = res.status;
|
|
278
|
+
throw error;
|
|
279
|
+
}
|
|
280
|
+
return res;
|
|
281
|
+
});
|
|
282
|
+
const data = await response.json().catch(() => ({}));
|
|
283
|
+
if (!response.ok) {
|
|
284
|
+
const error = data?.error?.message || data?.error || `HTTP ${response.status}`;
|
|
285
|
+
throw new Error(typeof error === 'string' ? error : JSON.stringify(error));
|
|
286
|
+
}
|
|
287
|
+
return data?.data ?? data;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function localAgentStream(userId, body, onEvent) {
|
|
291
|
+
const apiKey = getOrCreateTelegramApiKey(userId);
|
|
292
|
+
const response = await fetch(`${localApiBase()}/api/agent`, {
|
|
293
|
+
method: 'POST',
|
|
294
|
+
headers: {
|
|
295
|
+
'Content-Type': 'application/json',
|
|
296
|
+
'X-API-Key': apiKey,
|
|
297
|
+
},
|
|
298
|
+
body: JSON.stringify({
|
|
299
|
+
...body,
|
|
300
|
+
stream: true,
|
|
301
|
+
}),
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
if (!response.ok) {
|
|
305
|
+
const data = await response.json().catch(() => ({}));
|
|
306
|
+
const error = data?.error?.message || data?.error || `HTTP ${response.status}`;
|
|
307
|
+
throw new Error(typeof error === 'string' ? error : JSON.stringify(error));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (!response.body) return;
|
|
311
|
+
|
|
312
|
+
const reader = response.body.getReader();
|
|
313
|
+
const decoder = new TextDecoder();
|
|
314
|
+
let buffer = '';
|
|
315
|
+
|
|
316
|
+
const consumeBlock = async (block) => {
|
|
317
|
+
const lines = String(block || '').split('\n');
|
|
318
|
+
for (const line of lines) {
|
|
319
|
+
if (!line.startsWith('data:')) continue;
|
|
320
|
+
const payload = line.slice(5).trim();
|
|
321
|
+
if (!payload) continue;
|
|
322
|
+
let event;
|
|
323
|
+
try {
|
|
324
|
+
event = JSON.parse(payload);
|
|
325
|
+
} catch {
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
await onEvent?.(event);
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
for (;;) {
|
|
333
|
+
const { done, value } = await reader.read();
|
|
334
|
+
if (done) break;
|
|
335
|
+
buffer += decoder.decode(value, { stream: true });
|
|
336
|
+
if (buffer.length > MAX_SSE_BUFFER_CHARS) {
|
|
337
|
+
buffer = buffer.slice(-MAX_SSE_BUFFER_CHARS);
|
|
338
|
+
}
|
|
339
|
+
let boundary = buffer.indexOf('\n\n');
|
|
340
|
+
while (boundary !== -1) {
|
|
341
|
+
const block = buffer.slice(0, boundary);
|
|
342
|
+
buffer = buffer.slice(boundary + 2);
|
|
343
|
+
await consumeBlock(block);
|
|
344
|
+
boundary = buffer.indexOf('\n\n');
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const rest = `${buffer}${decoder.decode()}`;
|
|
349
|
+
if (rest.trim()) await consumeBlock(rest);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function checked(label) {
|
|
353
|
+
return `${label} ✓`;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function formatElapsed(startedAt) {
|
|
357
|
+
const seconds = Math.max(0, Math.round((Date.now() - startedAt) / 1000));
|
|
358
|
+
if (seconds < 60) return `${seconds}s`;
|
|
359
|
+
const minutes = Math.floor(seconds / 60);
|
|
360
|
+
const rest = seconds % 60;
|
|
361
|
+
return `${minutes}m ${rest}s`;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function projectLabel(state) {
|
|
365
|
+
return state.selectedProjectName || state.selectedProjectPath || '-';
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function createActivityState({ lang, type = 'agent', provider, project, prompt, mode = 'final' }) {
|
|
369
|
+
return {
|
|
370
|
+
lang,
|
|
371
|
+
type,
|
|
372
|
+
status: 'starting',
|
|
373
|
+
phase: t(lang, 'control.activity.starting'),
|
|
374
|
+
provider,
|
|
375
|
+
project,
|
|
376
|
+
prompt: compact(prompt, 120),
|
|
377
|
+
mode,
|
|
378
|
+
startedAt: Date.now(),
|
|
379
|
+
lastEditAt: 0,
|
|
380
|
+
messageId: null,
|
|
381
|
+
sessionId: null,
|
|
382
|
+
runId: null,
|
|
383
|
+
workflowId: null,
|
|
384
|
+
events: [],
|
|
385
|
+
output: '',
|
|
386
|
+
outputTruncated: false,
|
|
387
|
+
error: null,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function pushActivityEvent(activity, text) {
|
|
392
|
+
const value = String(text || '').trim();
|
|
393
|
+
if (!value) return;
|
|
394
|
+
if (activity.events.at(-1) === value) return;
|
|
395
|
+
activity.events.push(value);
|
|
396
|
+
if (activity.events.length > 8) activity.events.splice(0, activity.events.length - 8);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function activityTitle(activity) {
|
|
400
|
+
if (activity.type === 'workflow') return t(activity.lang, 'control.activity.workflowTitle');
|
|
401
|
+
if (activity.type === 'router') return t(activity.lang, 'control.activity.routerTitle');
|
|
402
|
+
return t(activity.lang, 'control.activity.agentTitle');
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function trimTelegramOutput(text, max, suffix = '') {
|
|
406
|
+
const value = String(text || '').trim();
|
|
407
|
+
const ending = String(suffix || '').trim();
|
|
408
|
+
if (value.length <= max) return value;
|
|
409
|
+
const room = Math.max(300, max - ending.length - 4);
|
|
410
|
+
return `${value.slice(0, room).trim()}\n\n${ending}`;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function appendActivityOutput(activity, chunk) {
|
|
414
|
+
if (!activity) return;
|
|
415
|
+
const next = appendBoundedText(activity.output, chunk);
|
|
416
|
+
activity.output = next.text;
|
|
417
|
+
activity.outputTruncated = Boolean(activity.outputTruncated || next.truncated);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function renderActivity(activity, { finalText = null } = {}) {
|
|
421
|
+
const output = finalText || activity.output;
|
|
422
|
+
if (activity.type === 'agent' && output && !activity.error) {
|
|
423
|
+
if (activity.status === 'done') {
|
|
424
|
+
return trimTelegramOutput(
|
|
425
|
+
output,
|
|
426
|
+
3400,
|
|
427
|
+
activity.outputTruncated
|
|
428
|
+
? t(activity.lang, 'control.activity.outputHistoryTrimmed')
|
|
429
|
+
: t(activity.lang, 'control.activity.outputTooLong'),
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const footer = `⏳ ${t(activity.lang, 'control.activity.liveFooter', { elapsed: formatElapsed(activity.startedAt) })}`;
|
|
434
|
+
const body = trimTelegramOutput(
|
|
435
|
+
output,
|
|
436
|
+
3200,
|
|
437
|
+
t(activity.lang, 'control.activity.outputShortened'),
|
|
438
|
+
);
|
|
439
|
+
return truncate(`${body}\n\n${footer}`, 3400);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const lines = [
|
|
443
|
+
`${activity.status === 'failed' ? '❌' : activity.status === 'done' ? '✅' : activity.status === 'running' ? '🔧' : '⏳'} ${activityTitle(activity)}`,
|
|
444
|
+
'',
|
|
445
|
+
`🤖 ${t(activity.lang, 'control.activity.provider')}: ${activity.provider || '-'}`,
|
|
446
|
+
`📁 ${t(activity.lang, 'control.activity.project')}: ${compact(activity.project, 90)}`,
|
|
447
|
+
];
|
|
448
|
+
|
|
449
|
+
if (activity.sessionId) lines.push(`🧵 ${t(activity.lang, 'control.activity.session')}: ${activity.sessionId}`);
|
|
450
|
+
if (activity.runId) lines.push(`🧭 ${t(activity.lang, 'control.activity.run')}: ${activity.runId}`);
|
|
451
|
+
if (activity.workflowId) lines.push(`🧩 ${t(activity.lang, 'control.activity.workflow')}: ${activity.workflowId}`);
|
|
452
|
+
lines.push(`⏱ ${t(activity.lang, 'control.activity.elapsed')}: ${formatElapsed(activity.startedAt)}`);
|
|
453
|
+
lines.push(`📌 ${t(activity.lang, 'control.activity.status')}: ${activity.phase}`);
|
|
454
|
+
|
|
455
|
+
const visibleEvents = activity.mode === 'final'
|
|
456
|
+
? activity.events.slice(-4)
|
|
457
|
+
: activity.events;
|
|
458
|
+
if (visibleEvents.length > 0) {
|
|
459
|
+
lines.push('', `🛠 ${t(activity.lang, 'control.activity.work')}:`);
|
|
460
|
+
for (const event of visibleEvents) lines.push(`• ${event}`);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (activity.error) {
|
|
464
|
+
lines.push('', `⚠️ ${truncate(activity.error, 700)}`);
|
|
465
|
+
} else if (output) {
|
|
466
|
+
lines.push('', `💬 ${t(activity.lang, 'control.activity.output')}:`);
|
|
467
|
+
lines.push(truncate(output, 1800));
|
|
468
|
+
} else if (activity.prompt) {
|
|
469
|
+
lines.push('', `📝 ${compact(activity.prompt, 220)}`);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return truncate(lines.join('\n'), 3400);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async function createTelegramActivity({ bot, chatId, link, type = 'agent', prompt = '', phase = null }) {
|
|
476
|
+
const lang = languageFor(link);
|
|
477
|
+
const state = getState(link.user_id);
|
|
478
|
+
const activity = createActivityState({
|
|
479
|
+
lang,
|
|
480
|
+
type,
|
|
481
|
+
provider: resolveTelegramProvider(state),
|
|
482
|
+
project: projectLabel(state),
|
|
483
|
+
prompt,
|
|
484
|
+
mode: state.progressMode,
|
|
485
|
+
});
|
|
486
|
+
if (phase) activity.phase = phase;
|
|
487
|
+
const sent = await send(bot, chatId, renderActivity(activity), { parse_mode: undefined });
|
|
488
|
+
activity.messageId = sent?.message_id || sent?.message?.message_id || null;
|
|
489
|
+
activity.lastEditAt = Date.now();
|
|
490
|
+
return activity;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
async function editTelegramActivity({ bot, chatId, activity, force = false, reply_markup }) {
|
|
494
|
+
if (!activity) return null;
|
|
495
|
+
const now = Date.now();
|
|
496
|
+
if (!force && now - activity.lastEditAt < ACTIVITY_EDIT_THROTTLE_MS) return null;
|
|
497
|
+
const sent = await send(bot, chatId, renderActivity(activity), {
|
|
498
|
+
editMessageId: activity.messageId,
|
|
499
|
+
parse_mode: undefined,
|
|
500
|
+
reply_markup,
|
|
501
|
+
});
|
|
502
|
+
activity.messageId = sent?.message_id || sent?.message?.message_id || activity.messageId;
|
|
503
|
+
activity.lastEditAt = now;
|
|
504
|
+
return sent;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function startActivityHeartbeat({ bot, chatId, activity }) {
|
|
508
|
+
return setInterval(() => {
|
|
509
|
+
if (!activity || activity.status === 'done' || activity.status === 'failed') return;
|
|
510
|
+
if (activity.status === 'starting') {
|
|
511
|
+
activity.status = 'running';
|
|
512
|
+
activity.phase = t(activity.lang, 'control.activity.thinking');
|
|
513
|
+
}
|
|
514
|
+
editTelegramActivity({ bot, chatId, activity }).catch((error) => {
|
|
515
|
+
console.warn('[telegram-control] activity heartbeat failed:', error?.message || error);
|
|
516
|
+
});
|
|
517
|
+
}, ACTIVITY_HEARTBEAT_MS);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function stateSummary(lang, state) {
|
|
521
|
+
const lines = [
|
|
522
|
+
`${t(lang, 'control.summary.project')}: ${state.selectedProjectName || t(lang, 'control.notSelected')}`,
|
|
523
|
+
`${t(lang, 'control.summary.provider')}: ${state.selectedProvider}${state.selectedModel ? ` / ${state.selectedModel}` : ''}`,
|
|
524
|
+
`${t(lang, 'control.summary.workflow')}: ${state.selectedWorkflowId || t(lang, 'control.notSelected')}`,
|
|
525
|
+
`${t(lang, 'control.summary.progress')}: ${state.progressMode}`,
|
|
526
|
+
];
|
|
527
|
+
if (state.activeTerminal) {
|
|
528
|
+
lines.push(`${t(lang, 'control.summary.terminal')}: ${state.activeTerminal.provider} / ${state.activeTerminal.projectLabel || state.activeTerminal.projectName || compact(state.activeTerminal.projectPath, 50)}`);
|
|
529
|
+
}
|
|
530
|
+
return lines.join('\n');
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function terminalControlKeyboard(lang) {
|
|
534
|
+
return [
|
|
535
|
+
[button(t(lang, 'control.button.terminalRefresh'), 'terminal_status'), button(t(lang, 'control.button.detachTerminal'), 'detach_terminal')],
|
|
536
|
+
[button(t(lang, 'control.button.mainMenu'), 'menu')],
|
|
537
|
+
];
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function mainMenuKeyboard(lang, state = null) {
|
|
541
|
+
const keyboard = [
|
|
542
|
+
[button(t(lang, 'control.button.projects'), 'projects'), button(t(lang, 'control.button.provider'), 'providers')],
|
|
543
|
+
[button(t(lang, 'control.button.models'), 'models'), button(t(lang, 'control.button.workflows'), 'workflows')],
|
|
544
|
+
[button(t(lang, 'control.button.runs'), 'runs'), button(t(lang, 'control.button.approvals'), 'approvals')],
|
|
545
|
+
[button(t(lang, 'control.button.controlRoom'), 'control_room'), button(t(lang, 'control.button.webhooks'), 'webhooks')],
|
|
546
|
+
[button(t(lang, 'control.button.sessions'), 'sessions'), button(t(lang, 'control.button.newChat'), 'new_chat')],
|
|
547
|
+
[button(t(lang, 'control.button.install'), 'install_menu'), button(t(lang, 'control.button.auth'), 'auth_menu')],
|
|
548
|
+
[button(t(lang, 'control.button.settings'), 'settings')],
|
|
549
|
+
];
|
|
550
|
+
if (state?.activeTerminal) {
|
|
551
|
+
keyboard.splice(1, 0, [button(t(lang, 'control.button.terminal'), 'terminal_status'), button(t(lang, 'control.button.detachTerminal'), 'detach_terminal')]);
|
|
552
|
+
}
|
|
553
|
+
return keyboard;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
export async function showMainMenu({ bot, chatId, link, editMessageId, notice }) {
|
|
557
|
+
const lang = languageFor(link);
|
|
558
|
+
const state = getState(link.user_id);
|
|
559
|
+
const prefix = notice ? `${notice}\n\n` : '';
|
|
560
|
+
await send(bot, chatId, `${prefix}${t(lang, 'control.menu')}\n\n${stateSummary(lang, state)}`, {
|
|
561
|
+
editMessageId,
|
|
562
|
+
reply_markup: { inline_keyboard: mainMenuKeyboard(lang, state) },
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
async function showHelp({ bot, chatId, link }) {
|
|
567
|
+
const lang = languageFor(link);
|
|
568
|
+
await send(bot, chatId, t(lang, 'control.help'));
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
async function showCommandPalette({ bot, chatId, link, editMessageId, unknown = false }) {
|
|
572
|
+
const lang = languageFor(link);
|
|
573
|
+
const prefix = unknown ? `${t(lang, 'control.unknownCommand')}\n\n` : '';
|
|
574
|
+
await send(bot, chatId, `${prefix}${t(lang, 'control.help')}\n\n${t(lang, 'control.examples')}`, {
|
|
575
|
+
editMessageId,
|
|
576
|
+
reply_markup: { inline_keyboard: mainMenuKeyboard(lang, getState(link.user_id)) },
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function getActiveTerminal(state) {
|
|
581
|
+
const terminal = state?.activeTerminal;
|
|
582
|
+
if (
|
|
583
|
+
!terminal ||
|
|
584
|
+
!PROVIDERS.includes(terminal.provider) ||
|
|
585
|
+
typeof terminal.projectPath !== 'string' ||
|
|
586
|
+
!terminal.projectPath.trim()
|
|
587
|
+
) {
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
590
|
+
return terminal;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function terminalProjectLabel(terminal) {
|
|
594
|
+
return terminal?.projectLabel || terminal?.projectName || terminal?.projectPath || '-';
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function terminalOutputUrl(terminal, maxChars = 3200, sinceCursor = null) {
|
|
598
|
+
const params = new URLSearchParams({
|
|
599
|
+
provider: terminal.provider,
|
|
600
|
+
projectPath: terminal.projectPath,
|
|
601
|
+
maxChars: String(maxChars),
|
|
602
|
+
});
|
|
603
|
+
if (terminal.tabId) params.set('tabId', terminal.tabId);
|
|
604
|
+
if (terminal.sessionId) params.set('sessionId', terminal.sessionId);
|
|
605
|
+
if (Number.isFinite(sinceCursor)) params.set('sinceCursor', String(sinceCursor));
|
|
606
|
+
return `/api/shell/sessions/provider-output?${params.toString()}`;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function renderTerminalSnapshot(lang, terminal, data, { prefix = '', includeOutput = false } = {}) {
|
|
610
|
+
const active = data?.active !== false;
|
|
611
|
+
const lifecycle = data?.terminalState || data?.lifecycleState || (active ? 'running' : 'not running');
|
|
612
|
+
const output = String(data?.output || '').trim();
|
|
613
|
+
const lines = [
|
|
614
|
+
prefix || t(lang, active ? 'control.terminalAttached' : 'control.terminalNotRunning'),
|
|
615
|
+
'',
|
|
616
|
+
`🤖 ${t(lang, 'control.activity.provider')}: ${terminal.provider}`,
|
|
617
|
+
`📁 ${t(lang, 'control.activity.project')}: ${compact(terminalProjectLabel(terminal), 90)}`,
|
|
618
|
+
`📌 ${t(lang, 'control.activity.status')}: ${lifecycle}`,
|
|
619
|
+
];
|
|
620
|
+
if (terminal.sessionId || data?.sessionId) {
|
|
621
|
+
lines.push(`🧵 ${t(lang, 'control.activity.session')}: ${terminal.sessionId || data.sessionId}`);
|
|
622
|
+
}
|
|
623
|
+
if (includeOutput && output) {
|
|
624
|
+
lines.push('', `💬 ${t(lang, 'control.activity.output')}:`);
|
|
625
|
+
lines.push(truncate(output, 2400));
|
|
626
|
+
} else {
|
|
627
|
+
lines.push('', t(lang, 'control.terminalOutputHidden'));
|
|
628
|
+
}
|
|
629
|
+
return truncate(lines.join('\n'), 3400);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function terminalBridgeMonitorKey(chatId, terminal) {
|
|
633
|
+
return [
|
|
634
|
+
chatId,
|
|
635
|
+
terminal.provider,
|
|
636
|
+
terminal.projectPath,
|
|
637
|
+
terminal.tabId || '-',
|
|
638
|
+
terminal.sessionId || '-',
|
|
639
|
+
].join(':');
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function escapeRegExp(value) {
|
|
643
|
+
return String(value || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function terminalBridgeInput(text, lang) {
|
|
647
|
+
const prompt = String(text || '').trim();
|
|
648
|
+
const instruction = lang === 'tr'
|
|
649
|
+
? `Pixcode Telegram senkronu: Yanıtının sonunda Telegram için okunabilir ve eksiksiz final cevabını ${TERMINAL_BRIDGE_FINAL_OPEN} ve ${TERMINAL_BRIDGE_FINAL_CLOSE} etiketleri arasına yaz. Etiketlerin içine spinner, terminal ekranı veya tekrar eden durum satırı koyma.`
|
|
650
|
+
: `Pixcode Telegram sync: At the end of your response, write the readable complete final answer for Telegram between ${TERMINAL_BRIDGE_FINAL_OPEN} and ${TERMINAL_BRIDGE_FINAL_CLOSE}. Do not put spinners, terminal screen text, or repeated status lines inside the tags.`;
|
|
651
|
+
return `${prompt}\n\n${instruction}`;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function normalizeBridgeLine(value) {
|
|
655
|
+
return String(value || '').replace(/\s+/g, ' ').trim();
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function terminalBridgeLabels(terminal) {
|
|
659
|
+
const labels = new Set();
|
|
660
|
+
for (const value of [
|
|
661
|
+
terminal?.provider,
|
|
662
|
+
terminal?.projectName,
|
|
663
|
+
terminal?.projectLabel,
|
|
664
|
+
terminal?.projectPath,
|
|
665
|
+
]) {
|
|
666
|
+
const normalized = normalizeBridgeLine(value);
|
|
667
|
+
if (!normalized) continue;
|
|
668
|
+
labels.add(normalized);
|
|
669
|
+
const pathParts = normalized.split(/[\\/]/u).filter(Boolean);
|
|
670
|
+
const basename = pathParts.at(-1);
|
|
671
|
+
if (basename) labels.add(basename);
|
|
672
|
+
}
|
|
673
|
+
return [...labels]
|
|
674
|
+
.map((value) => value.trim())
|
|
675
|
+
.filter((value) => value.length >= 2)
|
|
676
|
+
.sort((a, b) => b.length - a.length);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function stripTerminalBridgeFragments(value, terminal) {
|
|
680
|
+
let text = String(value || '');
|
|
681
|
+
text = text.replace(/\[[0-?]{1,32}[ -/]*[@-~]/gu, ' ');
|
|
682
|
+
text = text.replace(/\b\d{1,3}(?:;\d{1,3}){1,8}[A-Za-z]/gu, ' ');
|
|
683
|
+
text = text.replace(/\b[012];[^\n\r]{0,100}[⠂⠐⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏✶✻✽✢✳][^\n\r]{0,100}/gu, ' ');
|
|
684
|
+
|
|
685
|
+
for (const label of terminalBridgeLabels(terminal)) {
|
|
686
|
+
const labelPattern = escapeRegExp(label).replace(/\s+/gu, '\\s+');
|
|
687
|
+
const titlePattern = new RegExp(
|
|
688
|
+
String.raw`\b(?:\d+[a-z])?[012];[⠂⠐⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏✶✻✽✢✳•*·\s-]*${labelPattern}(?:\d+;?)?`,
|
|
689
|
+
'giu',
|
|
690
|
+
);
|
|
691
|
+
text = text.replace(titlePattern, ' ');
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
return text
|
|
695
|
+
.replace(/[⠂⠐⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏✶✻✽✢✳]+/gu, ' ')
|
|
696
|
+
.replace(/\b(?:Working|Determining|Thinking|Running)(?:\s*•\s*(?:Working|Determining|Thinking|Running))+\b/giu, ' ');
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function extractTerminalBridgeFinalBlock(text) {
|
|
700
|
+
const blocks = [];
|
|
701
|
+
const blockPattern = /(?:<|\[)\s*PIXCODE_TELEGRAM_FINAL\s*(?:>|\])([\s\S]*?)(?:<|\[)\s*\/\s*PIXCODE_TELEGRAM_FINAL\s*(?:>|\])/giu;
|
|
702
|
+
for (const match of text.matchAll(blockPattern)) {
|
|
703
|
+
const block = String(match[1] || '').trim();
|
|
704
|
+
if (block) blocks.push(block);
|
|
705
|
+
}
|
|
706
|
+
if (blocks.length > 0) return blocks.at(-1);
|
|
707
|
+
|
|
708
|
+
const openPattern = /(?:<|\[)\s*PIXCODE_TELEGRAM_FINAL\s*(?:>|\])/giu;
|
|
709
|
+
let lastOpen = null;
|
|
710
|
+
for (const match of text.matchAll(openPattern)) lastOpen = match;
|
|
711
|
+
if (!lastOpen) return '';
|
|
712
|
+
return text.slice((lastOpen.index || 0) + lastOpen[0].length).trim();
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function isNoisyTerminalBridgeLine(line, prompt, terminal) {
|
|
716
|
+
const normalized = normalizeBridgeLine(line);
|
|
717
|
+
if (!normalized || normalized.length <= 1) return true;
|
|
718
|
+
if (/PIXCODE_TELEGRAM_FINAL/iu.test(normalized)) return true;
|
|
719
|
+
if (/Pixcode Telegram (?:sync|senkronu)/iu.test(normalized)) return true;
|
|
720
|
+
if (/^[╭╮╰╯│─═┌┐└┘├┤┬┴┼\s]+$/u.test(normalized)) return true;
|
|
721
|
+
if (/^[●✶✻✽✢✳⠂⠐⏵·\s0;:()\-|/\\]+$/u.test(normalized)) return true;
|
|
722
|
+
if (/^\[[0-?]{1,32}[ -/]*[@-~]/u.test(normalized)) return true;
|
|
723
|
+
if (/^(?:\d+[a-z])?[012];/iu.test(normalized) && /[⠂⠐⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏✶✻✽✢✳]/u.test(normalized)) return true;
|
|
724
|
+
if ((normalized.match(/[⠂⠐⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏✶✻✽✢✳]/gu) || []).length >= 2) return true;
|
|
725
|
+
|
|
726
|
+
const lower = normalized.toLowerCase();
|
|
727
|
+
const promptNeedle = normalizeBridgeLine(prompt).toLowerCase();
|
|
728
|
+
if (promptNeedle && lower.includes(promptNeedle.slice(0, 120))) return true;
|
|
729
|
+
if (/^[›❯>]\s*/u.test(normalized)) return true;
|
|
730
|
+
if (lower.includes('welcome back')) return true;
|
|
731
|
+
if (lower.includes('api usage billing')) return true;
|
|
732
|
+
if (lower.includes('bypass permissions')) return true;
|
|
733
|
+
if (lower.includes('try "')) return true;
|
|
734
|
+
if (lower.includes('esc to interrupt') || lower.includes('press esc')) return true;
|
|
735
|
+
if (lower.includes('/effort')) return true;
|
|
736
|
+
if (lower.includes('determining')) return true;
|
|
737
|
+
if (/^(?:claude code|codex)\b/i.test(normalized) && normalized.length < 80) return true;
|
|
738
|
+
if (/^\(?\d+s\s*·\s*[↓↑]?\d*\s*tokens?\)?$/iu.test(normalized)) return true;
|
|
739
|
+
for (const label of terminalBridgeLabels(terminal)) {
|
|
740
|
+
if (
|
|
741
|
+
lower.includes(label.toLowerCase())
|
|
742
|
+
&& /(?:^[012];|[⠂⠐⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏✶✻✽✢✳]|working|determining)/iu.test(normalized)
|
|
743
|
+
) {
|
|
744
|
+
return true;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
return false;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
export function cleanTerminalBridgeOutput(output, prompt, terminal = null) {
|
|
751
|
+
const text = stripTerminalBridgeFragments(String(output || ''), terminal)
|
|
752
|
+
.replace(/\r/g, '\n')
|
|
753
|
+
.replace(/[\x00-\x08\x0B-\x1F\x7F]/g, '')
|
|
754
|
+
.replace(/\u00a0/g, ' ');
|
|
755
|
+
const finalBlock = extractTerminalBridgeFinalBlock(text);
|
|
756
|
+
const source = finalBlock || text;
|
|
757
|
+
const lines = source
|
|
758
|
+
.split('\n')
|
|
759
|
+
.map((line) => line.replace(/[ \t]+/g, ' ').trim())
|
|
760
|
+
.filter((line) => !isNoisyTerminalBridgeLine(line, prompt, terminal));
|
|
761
|
+
const deduped = [];
|
|
762
|
+
for (const line of lines) {
|
|
763
|
+
if (deduped.at(-1) === line) continue;
|
|
764
|
+
deduped.push(line);
|
|
765
|
+
}
|
|
766
|
+
return deduped.join('\n').trim();
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function renderTerminalBridgeProgress(lang, terminal, {
|
|
770
|
+
output = '',
|
|
771
|
+
statusKey = 'control.terminalWaiting',
|
|
772
|
+
startedAt = Date.now(),
|
|
773
|
+
final = false,
|
|
774
|
+
terminalState = null,
|
|
775
|
+
} = {}) {
|
|
776
|
+
const lines = [
|
|
777
|
+
`${final ? '✅' : '⏳'} ${t(lang, statusKey)}`,
|
|
778
|
+
'',
|
|
779
|
+
`🤖 ${t(lang, 'control.activity.provider')}: ${terminal.provider}`,
|
|
780
|
+
`📁 ${t(lang, 'control.activity.project')}: ${compact(terminalProjectLabel(terminal), 90)}`,
|
|
781
|
+
];
|
|
782
|
+
if (terminal.sessionId) {
|
|
783
|
+
lines.push(`🧵 ${t(lang, 'control.activity.session')}: ${terminal.sessionId}`);
|
|
784
|
+
}
|
|
785
|
+
if (terminalState && terminalState !== 'unknown') {
|
|
786
|
+
lines.push(`📌 ${t(lang, 'control.activity.status')}: ${terminalState}`);
|
|
787
|
+
}
|
|
788
|
+
if (output) {
|
|
789
|
+
lines.push('', `💬 ${t(lang, 'control.activity.output')}:`);
|
|
790
|
+
lines.push(truncate(output, final ? 3100 : 2200));
|
|
791
|
+
} else {
|
|
792
|
+
lines.push('', t(lang, 'control.terminalWaitingHint'));
|
|
793
|
+
}
|
|
794
|
+
if (!final) {
|
|
795
|
+
lines.push('', `⏱ ${t(lang, 'control.activity.elapsed')}: ${formatElapsed(startedAt)}`);
|
|
796
|
+
}
|
|
797
|
+
return truncate(lines.join('\n'), 3400);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
async function monitorTerminalBridgeResponse({
|
|
801
|
+
bot,
|
|
802
|
+
chatId,
|
|
803
|
+
link,
|
|
804
|
+
terminal,
|
|
805
|
+
prompt,
|
|
806
|
+
sinceCursor,
|
|
807
|
+
editMessageId,
|
|
808
|
+
monitorKey,
|
|
809
|
+
monitorToken,
|
|
810
|
+
}) {
|
|
811
|
+
const lang = languageFor(link);
|
|
812
|
+
const startedAt = Date.now();
|
|
813
|
+
let lastCleanOutput = '';
|
|
814
|
+
let lastOutputChangeAt = startedAt;
|
|
815
|
+
let lastEditAt = 0;
|
|
816
|
+
|
|
817
|
+
const isCurrent = () => terminalBridgeMonitors.get(monitorKey) === monitorToken;
|
|
818
|
+
|
|
819
|
+
try {
|
|
820
|
+
while (isCurrent() && Date.now() - startedAt < TERMINAL_BRIDGE_TIMEOUT_MS) {
|
|
821
|
+
await new Promise((resolve) => setTimeout(resolve, TERMINAL_BRIDGE_POLL_MS));
|
|
822
|
+
if (!isCurrent()) return;
|
|
823
|
+
|
|
824
|
+
const data = await localApi(
|
|
825
|
+
link.user_id,
|
|
826
|
+
terminalOutputUrl(terminal, 12000, sinceCursor),
|
|
827
|
+
{ timeoutMs: 12_000 },
|
|
828
|
+
);
|
|
829
|
+
if (data?.active === false) {
|
|
830
|
+
await send(bot, chatId, renderTerminalBridgeProgress(lang, terminal, {
|
|
831
|
+
output: lastCleanOutput,
|
|
832
|
+
statusKey: 'control.terminalNotRunning',
|
|
833
|
+
startedAt,
|
|
834
|
+
final: true,
|
|
835
|
+
terminalState: 'not running',
|
|
836
|
+
}), {
|
|
837
|
+
editMessageId,
|
|
838
|
+
parse_mode: undefined,
|
|
839
|
+
reply_markup: { inline_keyboard: terminalControlKeyboard(lang) },
|
|
840
|
+
});
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
const cleanOutput = cleanTerminalBridgeOutput(data?.output, prompt, terminal);
|
|
844
|
+
const terminalState = data?.terminalState || data?.lifecycleState || 'unknown';
|
|
845
|
+
const now = Date.now();
|
|
846
|
+
|
|
847
|
+
if (cleanOutput && cleanOutput !== lastCleanOutput) {
|
|
848
|
+
lastCleanOutput = cleanOutput;
|
|
849
|
+
lastOutputChangeAt = now;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
const finishedByState = ['idle', 'completed', 'failed', 'exited'].includes(terminalState);
|
|
853
|
+
const finishedByQuietOutput = Boolean(lastCleanOutput) && now - lastOutputChangeAt >= TERMINAL_BRIDGE_SETTLE_MS;
|
|
854
|
+
const shouldFinish = Boolean(lastCleanOutput) && (finishedByState || finishedByQuietOutput);
|
|
855
|
+
|
|
856
|
+
if (shouldFinish) {
|
|
857
|
+
await send(bot, chatId, renderTerminalBridgeProgress(lang, terminal, {
|
|
858
|
+
output: lastCleanOutput,
|
|
859
|
+
statusKey: 'control.terminalResponseReady',
|
|
860
|
+
startedAt,
|
|
861
|
+
final: true,
|
|
862
|
+
terminalState,
|
|
863
|
+
}), {
|
|
864
|
+
editMessageId,
|
|
865
|
+
parse_mode: undefined,
|
|
866
|
+
reply_markup: { inline_keyboard: terminalControlKeyboard(lang) },
|
|
867
|
+
});
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
if (now - lastEditAt >= TERMINAL_BRIDGE_EDIT_THROTTLE_MS) {
|
|
872
|
+
lastEditAt = now;
|
|
873
|
+
await send(bot, chatId, renderTerminalBridgeProgress(lang, terminal, {
|
|
874
|
+
output: lastCleanOutput,
|
|
875
|
+
statusKey: lastCleanOutput ? 'control.terminalResponding' : 'control.terminalWaiting',
|
|
876
|
+
startedAt,
|
|
877
|
+
terminalState,
|
|
878
|
+
}), {
|
|
879
|
+
editMessageId,
|
|
880
|
+
parse_mode: undefined,
|
|
881
|
+
reply_markup: { inline_keyboard: terminalControlKeyboard(lang) },
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if (!isCurrent()) return;
|
|
887
|
+
await send(bot, chatId, renderTerminalBridgeProgress(lang, terminal, {
|
|
888
|
+
output: lastCleanOutput,
|
|
889
|
+
statusKey: lastCleanOutput ? 'control.terminalStillRunning' : 'control.terminalNoReadableOutput',
|
|
890
|
+
startedAt,
|
|
891
|
+
final: Boolean(lastCleanOutput),
|
|
892
|
+
terminalState: 'running',
|
|
893
|
+
}), {
|
|
894
|
+
editMessageId,
|
|
895
|
+
parse_mode: undefined,
|
|
896
|
+
reply_markup: { inline_keyboard: terminalControlKeyboard(lang) },
|
|
897
|
+
});
|
|
898
|
+
} finally {
|
|
899
|
+
if (isCurrent()) terminalBridgeMonitors.delete(monitorKey);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
export async function sendActiveTerminalAttachedNotice({ bot, chatId, link, terminal }) {
|
|
904
|
+
const lang = languageFor(link);
|
|
905
|
+
const lines = [
|
|
906
|
+
t(lang, 'control.terminalAttached'),
|
|
907
|
+
'',
|
|
908
|
+
`🤖 ${t(lang, 'control.activity.provider')}: ${terminal.provider}`,
|
|
909
|
+
`📁 ${t(lang, 'control.activity.project')}: ${compact(terminalProjectLabel(terminal), 90)}`,
|
|
910
|
+
`📌 ${t(lang, 'control.activity.status')}: ${t(lang, 'control.terminalReadyStatus')}`,
|
|
911
|
+
];
|
|
912
|
+
if (terminal.sessionId) {
|
|
913
|
+
lines.push(`🧵 ${t(lang, 'control.activity.session')}: ${terminal.sessionId}`);
|
|
914
|
+
}
|
|
915
|
+
lines.push('', t(lang, 'control.terminalReadyPrompt'));
|
|
916
|
+
await send(bot, chatId, truncate(lines.join('\n'), 3400), {
|
|
917
|
+
parse_mode: undefined,
|
|
918
|
+
reply_markup: { inline_keyboard: terminalControlKeyboard(lang) },
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
async function showActiveTerminalStatus({ bot, chatId, link, editMessageId }) {
|
|
923
|
+
const lang = languageFor(link);
|
|
924
|
+
const state = getState(link.user_id);
|
|
925
|
+
const terminal = getActiveTerminal(state);
|
|
926
|
+
if (!terminal) {
|
|
927
|
+
await send(bot, chatId, t(lang, 'control.noActiveTerminal'), {
|
|
928
|
+
editMessageId,
|
|
929
|
+
reply_markup: { inline_keyboard: [[button(t(lang, 'control.button.mainMenu'), 'menu')]] },
|
|
930
|
+
});
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
try {
|
|
934
|
+
const data = await localApi(link.user_id, terminalOutputUrl(terminal), { timeoutMs: 12_000 });
|
|
935
|
+
await send(bot, chatId, renderTerminalSnapshot(lang, terminal, data), {
|
|
936
|
+
editMessageId,
|
|
937
|
+
parse_mode: undefined,
|
|
938
|
+
reply_markup: { inline_keyboard: terminalControlKeyboard(lang) },
|
|
939
|
+
});
|
|
940
|
+
} catch (error) {
|
|
941
|
+
await send(bot, chatId, t(lang, 'control.terminalStatusFailed', { error: error?.message || String(error) }), {
|
|
942
|
+
editMessageId,
|
|
943
|
+
reply_markup: { inline_keyboard: terminalControlKeyboard(lang) },
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
async function detachActiveTerminal({ bot, chatId, link, editMessageId }) {
|
|
949
|
+
const lang = languageFor(link);
|
|
950
|
+
updateTelegramControlState(link.user_id, { activeTerminal: null });
|
|
951
|
+
await send(bot, chatId, t(lang, 'control.terminalDetached'), {
|
|
952
|
+
editMessageId,
|
|
953
|
+
reply_markup: { inline_keyboard: [[button(t(lang, 'control.button.mainMenu'), 'menu')]] },
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
async function sendToActiveTerminal({ bot, chatId, link, text }) {
|
|
958
|
+
const lang = languageFor(link);
|
|
959
|
+
const state = getState(link.user_id);
|
|
960
|
+
const terminal = getActiveTerminal(state);
|
|
961
|
+
if (!terminal) return false;
|
|
962
|
+
if (state.remoteControlEnabled === false) {
|
|
963
|
+
await send(bot, chatId, t(lang, 'control.disabled'));
|
|
964
|
+
return true;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
const sent = await send(bot, chatId, t(lang, 'control.terminalSending', {
|
|
968
|
+
provider: terminal.provider,
|
|
969
|
+
project: terminalProjectLabel(terminal),
|
|
970
|
+
}), {
|
|
971
|
+
parse_mode: undefined,
|
|
972
|
+
reply_markup: { inline_keyboard: terminalControlKeyboard(lang) },
|
|
973
|
+
});
|
|
974
|
+
const editMessageId = sent?.message_id || sent?.message?.message_id || null;
|
|
975
|
+
|
|
976
|
+
try {
|
|
977
|
+
const terminalPrompt = terminalBridgeInput(text, lang);
|
|
978
|
+
const inputResult = await localApi(link.user_id, '/api/shell/sessions/provider-input', {
|
|
979
|
+
method: 'POST',
|
|
980
|
+
timeoutMs: 15_000,
|
|
981
|
+
body: {
|
|
982
|
+
provider: terminal.provider,
|
|
983
|
+
projectPath: terminal.projectPath,
|
|
984
|
+
tabId: terminal.tabId,
|
|
985
|
+
sessionId: terminal.sessionId,
|
|
986
|
+
input: terminalPrompt,
|
|
987
|
+
submit: true,
|
|
988
|
+
submitMode: 'deferred-enter',
|
|
989
|
+
},
|
|
990
|
+
});
|
|
991
|
+
const sinceCursor = Number.isFinite(inputResult?.outputCursorBefore)
|
|
992
|
+
? inputResult.outputCursorBefore
|
|
993
|
+
: null;
|
|
994
|
+
const monitorKey = terminalBridgeMonitorKey(chatId, terminal);
|
|
995
|
+
const monitorToken = crypto.randomUUID();
|
|
996
|
+
terminalBridgeMonitors.set(monitorKey, monitorToken);
|
|
997
|
+
|
|
998
|
+
await send(bot, chatId, renderTerminalBridgeProgress(lang, terminal, {
|
|
999
|
+
statusKey: 'control.terminalWaiting',
|
|
1000
|
+
startedAt: Date.now(),
|
|
1001
|
+
terminalState: 'running',
|
|
1002
|
+
}), {
|
|
1003
|
+
editMessageId,
|
|
1004
|
+
parse_mode: undefined,
|
|
1005
|
+
reply_markup: { inline_keyboard: terminalControlKeyboard(lang) },
|
|
1006
|
+
});
|
|
1007
|
+
monitorTerminalBridgeResponse({
|
|
1008
|
+
bot,
|
|
1009
|
+
chatId,
|
|
1010
|
+
link,
|
|
1011
|
+
terminal,
|
|
1012
|
+
prompt: terminalPrompt,
|
|
1013
|
+
sinceCursor,
|
|
1014
|
+
editMessageId,
|
|
1015
|
+
monitorKey,
|
|
1016
|
+
monitorToken,
|
|
1017
|
+
}).catch((error) => {
|
|
1018
|
+
console.warn('[telegram-control] terminal bridge monitor failed:', error?.message || error);
|
|
1019
|
+
});
|
|
1020
|
+
} catch (error) {
|
|
1021
|
+
await send(bot, chatId, t(lang, 'control.terminalSendFailed', {
|
|
1022
|
+
error: error?.message || String(error),
|
|
1023
|
+
}), {
|
|
1024
|
+
editMessageId,
|
|
1025
|
+
reply_markup: { inline_keyboard: terminalControlKeyboard(lang) },
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
return true;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
async function listProjects() {
|
|
1032
|
+
const projects = await getProjects();
|
|
1033
|
+
return projects.slice(0, 20);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
async function showProjectMenu({ bot, chatId, link, editMessageId, notice }) {
|
|
1037
|
+
const lang = languageFor(link);
|
|
1038
|
+
const projects = await listProjects();
|
|
1039
|
+
if (projects.length === 0) {
|
|
1040
|
+
const prefix = notice ? `${notice}\n\n` : '';
|
|
1041
|
+
await send(bot, chatId, `${prefix}${t(lang, 'control.noProjects')}`, { editMessageId });
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
const buttons = projects.map((project, index) => button(
|
|
1046
|
+
`${index + 1}. ${compact(project.displayName || project.name, 34)}`,
|
|
1047
|
+
'project_select',
|
|
1048
|
+
{
|
|
1049
|
+
name: project.name,
|
|
1050
|
+
path: project.fullPath || project.path,
|
|
1051
|
+
displayName: project.displayName || project.name,
|
|
1052
|
+
},
|
|
1053
|
+
));
|
|
1054
|
+
const prefix = notice ? `${notice}\n\n` : '';
|
|
1055
|
+
await send(bot, chatId, `${prefix}${t(lang, 'control.pickProject')}`, {
|
|
1056
|
+
editMessageId,
|
|
1057
|
+
reply_markup: { inline_keyboard: rows(buttons, 1) },
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
export async function showProviderMenu({ bot, chatId, link, editMessageId }) {
|
|
1062
|
+
const lang = languageFor(link);
|
|
1063
|
+
const state = getState(link.user_id);
|
|
1064
|
+
const buttons = PROVIDERS.map((provider) => button(
|
|
1065
|
+
provider === state.selectedProvider ? checked(provider) : provider,
|
|
1066
|
+
'provider_select',
|
|
1067
|
+
{ provider },
|
|
1068
|
+
));
|
|
1069
|
+
await send(bot, chatId, t(lang, 'control.pickProvider'), {
|
|
1070
|
+
editMessageId,
|
|
1071
|
+
reply_markup: { inline_keyboard: rows(buttons, 2) },
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
async function readProviderModels(userId, provider, refresh = false) {
|
|
1076
|
+
try {
|
|
1077
|
+
const data = await localApi(userId, `/api/providers/${provider}/models${refresh ? '?refresh=1' : ''}`);
|
|
1078
|
+
const models = Array.isArray(data?.models) ? data.models : [];
|
|
1079
|
+
return models.length > 0 ? models : MODEL_FALLBACKS[provider] || [];
|
|
1080
|
+
} catch {
|
|
1081
|
+
return MODEL_FALLBACKS[provider] || [];
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
export async function showModelMenu({ bot, chatId, link, refresh = false, editMessageId }) {
|
|
1086
|
+
const lang = languageFor(link);
|
|
1087
|
+
const state = getState(link.user_id);
|
|
1088
|
+
const models = await readProviderModels(link.user_id, state.selectedProvider, refresh);
|
|
1089
|
+
const modelButtons = models.slice(0, 18).map((model) => {
|
|
1090
|
+
const value = model.value || model.id || model.label;
|
|
1091
|
+
const label = compact(model.label || value, 38);
|
|
1092
|
+
return button(value === state.selectedModel ? checked(label) : label, 'model_select', { model: value });
|
|
1093
|
+
});
|
|
1094
|
+
modelButtons.push(button(t(lang, 'control.button.refreshModels'), 'models_refresh'));
|
|
1095
|
+
modelButtons.push(button(t(lang, 'control.button.clearModel'), 'model_select', { model: null }));
|
|
1096
|
+
await send(bot, chatId, t(lang, 'control.modelsFor', { provider: state.selectedProvider }), {
|
|
1097
|
+
editMessageId,
|
|
1098
|
+
reply_markup: { inline_keyboard: rows(modelButtons, 1) },
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
export async function showWorkflowMenu({ bot, chatId, link, editMessageId }) {
|
|
1103
|
+
const lang = languageFor(link);
|
|
1104
|
+
const workflows = await localApi(link.user_id, '/api/orchestration/workflows');
|
|
1105
|
+
const list = Array.isArray(workflows?.workflows) ? workflows.workflows : [];
|
|
1106
|
+
const state = getState(link.user_id);
|
|
1107
|
+
const workflowButtons = list.map((workflow) => button(
|
|
1108
|
+
workflow.id === state.selectedWorkflowId ? checked(compact(workflow.name || workflow.id, 34)) : compact(workflow.name || workflow.id, 34),
|
|
1109
|
+
'workflow_select',
|
|
1110
|
+
{ workflowId: workflow.id, name: workflow.name || workflow.id },
|
|
1111
|
+
));
|
|
1112
|
+
workflowButtons.push(button(t(lang, 'control.button.runWorkflow'), 'workflow_prompt'));
|
|
1113
|
+
await send(bot, chatId, t(lang, 'control.pickWorkflow'), {
|
|
1114
|
+
editMessageId,
|
|
1115
|
+
reply_markup: { inline_keyboard: rows(workflowButtons, 1) },
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
async function showRuns({ bot, chatId, link, editMessageId }) {
|
|
1120
|
+
const lang = languageFor(link);
|
|
1121
|
+
const data = await localApi(link.user_id, '/api/orchestration/workflows/runs?limit=10');
|
|
1122
|
+
const runs = Array.isArray(data?.runs) ? data.runs : [];
|
|
1123
|
+
if (runs.length === 0) {
|
|
1124
|
+
await send(bot, chatId, t(lang, 'control.noRuns'), { editMessageId });
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
const buttons = runs.map((run) => button(
|
|
1128
|
+
`${run.status} ${compact(run.workflowId || run.id, 28)}`,
|
|
1129
|
+
'run_detail',
|
|
1130
|
+
{ runId: run.id },
|
|
1131
|
+
));
|
|
1132
|
+
await send(bot, chatId, t(lang, 'control.recentRuns'), {
|
|
1133
|
+
editMessageId,
|
|
1134
|
+
reply_markup: { inline_keyboard: rows(buttons, 1) },
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
async function showApprovalQueue({ bot, chatId, link, editMessageId }) {
|
|
1139
|
+
const lang = languageFor(link);
|
|
1140
|
+
const data = await localApi(link.user_id, '/api/orchestration/workflows/approvals');
|
|
1141
|
+
const approvals = Array.isArray(data?.pendingApprovals) ? data.pendingApprovals : [];
|
|
1142
|
+
if (approvals.length === 0) {
|
|
1143
|
+
await send(bot, chatId, t(lang, 'control.noApprovals'), { editMessageId });
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
const keyboard = [];
|
|
1148
|
+
const lines = approvals.slice(0, 8).map((approval, index) => {
|
|
1149
|
+
const label = compact(approval.summary || approval.reason || approval.id, 70);
|
|
1150
|
+
keyboard.push([
|
|
1151
|
+
button(t(lang, 'control.button.approve'), 'approval_decide', { approvalId: approval.id, allow: true }),
|
|
1152
|
+
button(t(lang, 'control.button.deny'), 'approval_decide', { approvalId: approval.id, allow: false }),
|
|
1153
|
+
]);
|
|
1154
|
+
return `${index + 1}. ${label}\nRun: ${approval.runId}`;
|
|
1155
|
+
});
|
|
1156
|
+
keyboard.push([button(t(lang, 'control.button.refresh'), 'approvals'), button(t(lang, 'control.button.mainMenu'), 'menu')]);
|
|
1157
|
+
await send(bot, chatId, `${t(lang, 'control.approvalQueue')}\n\n${lines.join('\n\n')}`, {
|
|
1158
|
+
editMessageId,
|
|
1159
|
+
reply_markup: { inline_keyboard: keyboard },
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
async function showControlRoom({ bot, chatId, link, editMessageId }) {
|
|
1164
|
+
const lang = languageFor(link);
|
|
1165
|
+
const data = await localApi(link.user_id, '/api/remote/control-room');
|
|
1166
|
+
const snapshot = data?.controlRoom || data;
|
|
1167
|
+
const projects = Array.isArray(snapshot?.projects) ? snapshot.projects : [];
|
|
1168
|
+
const totals = snapshot?.totals || {};
|
|
1169
|
+
const lines = projects.map((project, index) => [
|
|
1170
|
+
`${index + 1}. ${compact(project.name || project.id, 44)}`,
|
|
1171
|
+
`Runs: ${project.activeRunCount || 0} active / ${project.failedRunCount || 0} failed`,
|
|
1172
|
+
`Approvals: ${project.pendingApprovalCount || 0}`,
|
|
1173
|
+
].join('\n'));
|
|
1174
|
+
await send(bot, chatId, [
|
|
1175
|
+
t(lang, 'control.controlRoomTitle'),
|
|
1176
|
+
'',
|
|
1177
|
+
`Projects: ${totals.projects || 0}`,
|
|
1178
|
+
`Active runs: ${totals.activeRuns || 0}`,
|
|
1179
|
+
`Pending approvals: ${totals.pendingApprovals || 0}`,
|
|
1180
|
+
'',
|
|
1181
|
+
lines.join('\n\n') || t(lang, 'control.noProjects'),
|
|
1182
|
+
].join('\n'), {
|
|
1183
|
+
editMessageId,
|
|
1184
|
+
reply_markup: {
|
|
1185
|
+
inline_keyboard: [
|
|
1186
|
+
[button(t(lang, 'control.button.approvals'), 'approvals'), button(t(lang, 'control.button.runs'), 'runs')],
|
|
1187
|
+
[button(t(lang, 'control.button.mainMenu'), 'menu')],
|
|
1188
|
+
],
|
|
1189
|
+
},
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
async function showWebhookMenu({ bot, chatId, link, editMessageId }) {
|
|
1194
|
+
const lang = languageFor(link);
|
|
1195
|
+
const data = await localApi(link.user_id, '/api/webhooks');
|
|
1196
|
+
const webhooks = Array.isArray(data?.webhooks) ? data.webhooks : [];
|
|
1197
|
+
const lines = webhooks.slice(0, 10).map((webhook, index) => (
|
|
1198
|
+
`${index + 1}. ${webhook.enabled ? 'on' : 'off'} ${compact(webhook.name || webhook.url, 50)}\n${compact(webhook.events?.join(', ') || webhook.url, 90)}`
|
|
1199
|
+
));
|
|
1200
|
+
await send(bot, chatId, [
|
|
1201
|
+
t(lang, 'control.webhookTitle'),
|
|
1202
|
+
'',
|
|
1203
|
+
lines.join('\n\n') || t(lang, 'control.noWebhooks'),
|
|
1204
|
+
].join('\n'), {
|
|
1205
|
+
editMessageId,
|
|
1206
|
+
reply_markup: {
|
|
1207
|
+
inline_keyboard: [
|
|
1208
|
+
[button(t(lang, 'control.button.refresh'), 'webhooks'), button(t(lang, 'control.button.mainMenu'), 'menu')],
|
|
1209
|
+
],
|
|
1210
|
+
},
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
async function showSessions({ bot, chatId, link, editMessageId }) {
|
|
1215
|
+
const lang = languageFor(link);
|
|
1216
|
+
const state = getState(link.user_id);
|
|
1217
|
+
if (!state.selectedProjectName) {
|
|
1218
|
+
await send(bot, chatId, t(lang, 'control.selectProjectFirst'), { editMessageId });
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
const data = await localApi(link.user_id, `/api/projects/${encodeURIComponent(state.selectedProjectName)}/sessions?limit=10&offset=0`);
|
|
1223
|
+
const sessions = Array.isArray(data?.sessions) ? data.sessions : [];
|
|
1224
|
+
if (sessions.length === 0) {
|
|
1225
|
+
await send(bot, chatId, t(lang, 'control.noSessions'), { editMessageId });
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
const lines = sessions.slice(0, 10).map((session, index) =>
|
|
1230
|
+
`${index + 1}. ${compact(session.summary || session.id || session.sessionId, 70)}`
|
|
1231
|
+
);
|
|
1232
|
+
await send(bot, chatId, `${t(lang, 'control.recentSessions')}\n\n${lines.join('\n')}`, {
|
|
1233
|
+
editMessageId,
|
|
1234
|
+
reply_markup: {
|
|
1235
|
+
inline_keyboard: [
|
|
1236
|
+
[button(t(lang, 'control.button.newChat'), 'new_chat')],
|
|
1237
|
+
[button(t(lang, 'control.button.mainMenu'), 'menu')],
|
|
1238
|
+
],
|
|
1239
|
+
},
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
async function startNewChat({ bot, chatId, link, editMessageId }) {
|
|
1244
|
+
const lang = languageFor(link);
|
|
1245
|
+
updateTelegramControlState(link.user_id, { awaiting: { type: 'agent_prompt' } });
|
|
1246
|
+
await send(bot, chatId, t(lang, 'control.newChatReady'), {
|
|
1247
|
+
editMessageId,
|
|
1248
|
+
reply_markup: { inline_keyboard: [[button(t(lang, 'control.button.mainMenu'), 'menu')]] },
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
function extractAssistantText(response) {
|
|
1253
|
+
const messages = Array.isArray(response?.messages) ? response.messages : [];
|
|
1254
|
+
const chunks = [];
|
|
1255
|
+
for (const message of messages) {
|
|
1256
|
+
if (typeof message?.content === 'string') chunks.push(message.content);
|
|
1257
|
+
const content = message?.message?.content;
|
|
1258
|
+
if (Array.isArray(content)) {
|
|
1259
|
+
for (const part of content) {
|
|
1260
|
+
if (part?.type === 'text' && typeof part.text === 'string') chunks.push(part.text);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
return chunks.join('\n\n').trim();
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
function extractTextFromEvent(event) {
|
|
1268
|
+
if (!event || typeof event !== 'object') return '';
|
|
1269
|
+
if (typeof event.content === 'string') return event.content;
|
|
1270
|
+
if (Array.isArray(event.content)) {
|
|
1271
|
+
return event.content
|
|
1272
|
+
.map((part) => (typeof part === 'string' ? part : part?.text || ''))
|
|
1273
|
+
.join('');
|
|
1274
|
+
}
|
|
1275
|
+
const legacyContent = event.data?.message?.content || event.message?.content;
|
|
1276
|
+
if (Array.isArray(legacyContent)) {
|
|
1277
|
+
return legacyContent
|
|
1278
|
+
.map((part) => (typeof part === 'string' ? part : part?.text || ''))
|
|
1279
|
+
.join('');
|
|
1280
|
+
}
|
|
1281
|
+
if (typeof legacyContent === 'string') return legacyContent;
|
|
1282
|
+
return '';
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
function describeToolInput(toolName, input) {
|
|
1286
|
+
if (!input || typeof input !== 'object') return '';
|
|
1287
|
+
if (toolName === 'Bash' && typeof input.command === 'string') {
|
|
1288
|
+
return compact(input.command, 110);
|
|
1289
|
+
}
|
|
1290
|
+
if (toolName === 'WebSearch' && typeof input.query === 'string') {
|
|
1291
|
+
return compact(input.query, 110);
|
|
1292
|
+
}
|
|
1293
|
+
if (toolName === 'FileChanges') {
|
|
1294
|
+
return compact(JSON.stringify(input), 110);
|
|
1295
|
+
}
|
|
1296
|
+
const keys = Object.keys(input).slice(0, 3);
|
|
1297
|
+
if (keys.length === 0) return '';
|
|
1298
|
+
return compact(keys.map((key) => `${key}: ${String(input[key])}`).join(', '), 110);
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
function applyAgentStreamEvent(activity, event) {
|
|
1302
|
+
if (!event || typeof event !== 'object') return;
|
|
1303
|
+
|
|
1304
|
+
const sessionId = event.actualSessionId || event.newSessionId || event.sessionId || event.threadId;
|
|
1305
|
+
if (sessionId) activity.sessionId = sessionId;
|
|
1306
|
+
|
|
1307
|
+
if (event.type === 'status' && event.message) {
|
|
1308
|
+
activity.status = 'running';
|
|
1309
|
+
activity.phase = compact(event.message, 120);
|
|
1310
|
+
pushActivityEvent(activity, `⏳ ${compact(event.message, 120)}`);
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
if (event.type === 'done' || event.kind === 'complete' || event.kind === 'stream_end') {
|
|
1314
|
+
activity.status = 'done';
|
|
1315
|
+
activity.phase = t(activity.lang, 'control.activity.done');
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
if (event.type === 'error' || event.kind === 'error') {
|
|
1319
|
+
activity.status = 'failed';
|
|
1320
|
+
activity.phase = t(activity.lang, 'control.activity.failed');
|
|
1321
|
+
activity.error = event.error || event.message || event.content || t(activity.lang, 'error.generic');
|
|
1322
|
+
pushActivityEvent(activity, `❌ ${compact(activity.error, 120)}`);
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
if (event.kind === 'session_created') {
|
|
1326
|
+
activity.status = 'running';
|
|
1327
|
+
activity.phase = t(activity.lang, 'control.activity.sessionStarted');
|
|
1328
|
+
pushActivityEvent(activity, `🧵 ${t(activity.lang, 'control.activity.sessionStarted')}`);
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
if (event.kind === 'thinking') {
|
|
1332
|
+
activity.status = 'running';
|
|
1333
|
+
activity.phase = t(activity.lang, 'control.activity.thinking');
|
|
1334
|
+
const thought = extractTextFromEvent(event);
|
|
1335
|
+
if (thought) pushActivityEvent(activity, `🧠 ${compact(thought, 120)}`);
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
if (event.kind === 'stream_delta' || event.kind === 'text') {
|
|
1339
|
+
activity.status = 'running';
|
|
1340
|
+
activity.phase = t(activity.lang, 'control.activity.responding');
|
|
1341
|
+
appendActivityOutput(activity, extractTextFromEvent(event));
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
if (event.type === 'claude-response' && event.data?.type === 'assistant') {
|
|
1345
|
+
activity.status = 'running';
|
|
1346
|
+
activity.phase = t(activity.lang, 'control.activity.responding');
|
|
1347
|
+
appendActivityOutput(activity, extractTextFromEvent(event));
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
if (event.kind === 'tool_use') {
|
|
1351
|
+
activity.status = 'running';
|
|
1352
|
+
activity.phase = t(activity.lang, 'control.activity.working');
|
|
1353
|
+
const toolName = event.toolName || 'Tool';
|
|
1354
|
+
const input = describeToolInput(toolName, event.toolInput);
|
|
1355
|
+
const icon = toolName === 'Bash'
|
|
1356
|
+
? '💻'
|
|
1357
|
+
: toolName === 'FileChanges'
|
|
1358
|
+
? '📝'
|
|
1359
|
+
: toolName === 'WebSearch'
|
|
1360
|
+
? '🔎'
|
|
1361
|
+
: '🔧';
|
|
1362
|
+
pushActivityEvent(activity, `${icon} ${toolName}${input ? `: ${input}` : ''}`);
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
if (event.kind === 'tool_result') {
|
|
1366
|
+
activity.status = 'running';
|
|
1367
|
+
activity.phase = t(activity.lang, 'control.activity.working');
|
|
1368
|
+
const label = event.isError
|
|
1369
|
+
? t(activity.lang, 'control.activity.toolFailed')
|
|
1370
|
+
: t(activity.lang, 'control.activity.toolDone');
|
|
1371
|
+
pushActivityEvent(activity, `${event.isError ? '⚠️' : '✅'} ${label}`);
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1374
|
+
if (event.kind === 'status' && event.text === 'token_budget') {
|
|
1375
|
+
const used = event.tokenBudget?.used;
|
|
1376
|
+
if (used) pushActivityEvent(activity, `📊 ${t(activity.lang, 'control.activity.tokens')}: ${used}`);
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
function confirmationLabel(lang, action, payload = {}) {
|
|
1381
|
+
if (action === 'install_provider') {
|
|
1382
|
+
return t(lang, 'control.confirmInstall', { provider: payload.provider || '' });
|
|
1383
|
+
}
|
|
1384
|
+
if (action === 'run_cancel') {
|
|
1385
|
+
return t(lang, 'control.confirmCancelRun', { runId: payload.runId || '' });
|
|
1386
|
+
}
|
|
1387
|
+
return t(lang, 'control.confirmationRequired');
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
async function requestConfirmation({ bot, chatId, link, action, payload = {}, editMessageId }) {
|
|
1391
|
+
const lang = languageFor(link);
|
|
1392
|
+
const pending = createTelegramConfirmation(link.user_id, action, payload);
|
|
1393
|
+
await send(bot, chatId, confirmationLabel(lang, action, payload), {
|
|
1394
|
+
editMessageId,
|
|
1395
|
+
reply_markup: {
|
|
1396
|
+
inline_keyboard: [[
|
|
1397
|
+
button(t(lang, 'control.button.confirm'), 'confirm_action', { id: pending.id }),
|
|
1398
|
+
button(t(lang, 'control.button.cancel'), 'cancel_confirmation', { id: pending.id }),
|
|
1399
|
+
]],
|
|
1400
|
+
},
|
|
1401
|
+
});
|
|
1402
|
+
return { ok: false, requiresConfirmation: true };
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
async function sendToolFailure({ bot, chatId, link, result, editMessageId }) {
|
|
1406
|
+
const lang = languageFor(link);
|
|
1407
|
+
const message = result?.message === 'REMOTE_CONTROL_DISABLED'
|
|
1408
|
+
? t(lang, 'control.disabled')
|
|
1409
|
+
: (result?.message || t(lang, 'error.generic'));
|
|
1410
|
+
await send(bot, chatId, message, { editMessageId });
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
async function runAgent({ bot, chatId, link, prompt, activity = null }) {
|
|
1414
|
+
const lang = languageFor(link);
|
|
1415
|
+
const state = getState(link.user_id);
|
|
1416
|
+
if (!state.remoteControlEnabled) {
|
|
1417
|
+
await send(bot, chatId, t(lang, 'control.disabled'), { editMessageId: activity?.messageId });
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
if (!state.selectedProjectPath) {
|
|
1421
|
+
await showProjectMenu({ bot, chatId, link, editMessageId: activity?.messageId, notice: t(lang, 'control.selectProjectFirst') });
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
const provider = resolveTelegramProvider(state);
|
|
1426
|
+
const model = resolveTelegramModel(state);
|
|
1427
|
+
const active = activity || await createTelegramActivity({
|
|
1428
|
+
bot,
|
|
1429
|
+
chatId,
|
|
1430
|
+
link,
|
|
1431
|
+
type: 'agent',
|
|
1432
|
+
prompt,
|
|
1433
|
+
phase: t(lang, 'control.activity.startingProvider', { provider }),
|
|
1434
|
+
});
|
|
1435
|
+
active.type = 'agent';
|
|
1436
|
+
active.provider = provider;
|
|
1437
|
+
active.project = projectLabel(state);
|
|
1438
|
+
active.status = 'running';
|
|
1439
|
+
active.phase = t(lang, 'control.activity.startingProvider', { provider });
|
|
1440
|
+
await editTelegramActivity({ bot, chatId, activity: active, force: true });
|
|
1441
|
+
|
|
1442
|
+
const heartbeat = startActivityHeartbeat({ bot, chatId, activity: active });
|
|
1443
|
+
let streamFailed = null;
|
|
1444
|
+
try {
|
|
1445
|
+
await localAgentStream(link.user_id, {
|
|
1446
|
+
projectPath: state.selectedProjectPath,
|
|
1447
|
+
provider,
|
|
1448
|
+
model: model || undefined,
|
|
1449
|
+
message: buildTelegramAgentPrompt(prompt, state),
|
|
1450
|
+
cleanup: false,
|
|
1451
|
+
permissionMode: 'default',
|
|
1452
|
+
suppressNotifications: true,
|
|
1453
|
+
}, async (event) => {
|
|
1454
|
+
applyAgentStreamEvent(active, event);
|
|
1455
|
+
await editTelegramActivity({ bot, chatId, activity: active });
|
|
1456
|
+
});
|
|
1457
|
+
} catch (error) {
|
|
1458
|
+
streamFailed = error;
|
|
1459
|
+
} finally {
|
|
1460
|
+
clearInterval(heartbeat);
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
if (streamFailed) {
|
|
1464
|
+
active.status = 'failed';
|
|
1465
|
+
active.phase = t(lang, 'control.activity.failed');
|
|
1466
|
+
active.error = streamFailed.message || t(lang, 'error.generic');
|
|
1467
|
+
await editTelegramActivity({ bot, chatId, activity: active, force: true });
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
active.status = active.error ? 'failed' : 'done';
|
|
1472
|
+
active.phase = active.error ? t(lang, 'control.activity.failed') : t(lang, 'control.activity.done');
|
|
1473
|
+
if (!active.output && !active.error) active.output = t(lang, 'control.noAssistantText');
|
|
1474
|
+
await editTelegramActivity({ bot, chatId, activity: active, force: true });
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
function longTaskKey(chatId) {
|
|
1478
|
+
return String(chatId);
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
async function launchLongTelegramTask({ bot, chatId, link, kind, activity = null, task }) {
|
|
1482
|
+
const key = longTaskKey(chatId);
|
|
1483
|
+
const existing = activeLongTasks.get(key);
|
|
1484
|
+
const lang = languageFor(link);
|
|
1485
|
+
if (existing) {
|
|
1486
|
+
await send(bot, chatId, t(lang, 'control.longTaskRunning', {
|
|
1487
|
+
kind: existing.kind,
|
|
1488
|
+
elapsed: formatElapsed(existing.startedAt),
|
|
1489
|
+
}), {
|
|
1490
|
+
editMessageId: activity?.messageId,
|
|
1491
|
+
parse_mode: undefined,
|
|
1492
|
+
});
|
|
1493
|
+
return true;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
const record = {
|
|
1497
|
+
kind,
|
|
1498
|
+
startedAt: Date.now(),
|
|
1499
|
+
};
|
|
1500
|
+
activeLongTasks.set(key, record);
|
|
1501
|
+
|
|
1502
|
+
Promise.resolve()
|
|
1503
|
+
.then(task)
|
|
1504
|
+
.catch(async (error) => {
|
|
1505
|
+
console.error('[telegram-control] long task failed:', error);
|
|
1506
|
+
await send(bot, chatId, t(lang, 'control.longTaskFailed'), {
|
|
1507
|
+
editMessageId: activity?.messageId,
|
|
1508
|
+
parse_mode: undefined,
|
|
1509
|
+
}).catch(() => {});
|
|
1510
|
+
})
|
|
1511
|
+
.finally(() => {
|
|
1512
|
+
if (activeLongTasks.get(key) === record) activeLongTasks.delete(key);
|
|
1513
|
+
});
|
|
1514
|
+
|
|
1515
|
+
return true;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
export async function runWorkflow({ bot, chatId, link, input, activity = null }) {
|
|
1519
|
+
const lang = languageFor(link);
|
|
1520
|
+
const state = getState(link.user_id);
|
|
1521
|
+
const workflowId = state.selectedWorkflowId;
|
|
1522
|
+
if (!state.remoteControlEnabled) {
|
|
1523
|
+
await send(bot, chatId, t(lang, 'control.disabled'), { editMessageId: activity?.messageId });
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
if (!workflowId) {
|
|
1527
|
+
await send(bot, chatId, t(lang, 'control.selectWorkflowFirst'), { editMessageId: activity?.messageId });
|
|
1528
|
+
await showWorkflowMenu({ bot, chatId, link, editMessageId: activity?.messageId });
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1531
|
+
if (!state.selectedProjectPath) {
|
|
1532
|
+
await showProjectMenu({ bot, chatId, link, editMessageId: activity?.messageId, notice: t(lang, 'control.selectProjectFirst') });
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
const provider = resolveTelegramProvider(state);
|
|
1537
|
+
const model = resolveTelegramModel(state);
|
|
1538
|
+
const active = activity || await createTelegramActivity({
|
|
1539
|
+
bot,
|
|
1540
|
+
chatId,
|
|
1541
|
+
link,
|
|
1542
|
+
type: 'workflow',
|
|
1543
|
+
prompt: input,
|
|
1544
|
+
phase: t(lang, 'control.activity.startingWorkflow'),
|
|
1545
|
+
});
|
|
1546
|
+
active.type = 'workflow';
|
|
1547
|
+
active.provider = provider;
|
|
1548
|
+
active.project = projectLabel(state);
|
|
1549
|
+
active.workflowId = workflowId;
|
|
1550
|
+
active.status = 'running';
|
|
1551
|
+
active.phase = t(lang, 'control.activity.startingWorkflow');
|
|
1552
|
+
await editTelegramActivity({ bot, chatId, activity: active, force: true });
|
|
1553
|
+
|
|
1554
|
+
const result = await runTelegramTool({
|
|
1555
|
+
userId: link.user_id,
|
|
1556
|
+
action: 'run_workflow',
|
|
1557
|
+
execute: () => localApi(link.user_id, `/api/orchestration/workflows/${workflowId}/runs`, {
|
|
1558
|
+
method: 'POST',
|
|
1559
|
+
body: {
|
|
1560
|
+
input,
|
|
1561
|
+
metadata: {
|
|
1562
|
+
projectId: state.selectedProjectName,
|
|
1563
|
+
projectName: state.selectedProjectName,
|
|
1564
|
+
projectPath: state.selectedProjectPath,
|
|
1565
|
+
workspaceTarget: 'selected_project',
|
|
1566
|
+
telegram: true,
|
|
1567
|
+
preferredProvider: provider,
|
|
1568
|
+
preferredModel: model,
|
|
1569
|
+
},
|
|
1570
|
+
},
|
|
1571
|
+
}),
|
|
1572
|
+
});
|
|
1573
|
+
if (!result.ok) {
|
|
1574
|
+
await sendToolFailure({ bot, chatId, link, result, editMessageId: active.messageId });
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1577
|
+
const run = result.data;
|
|
1578
|
+
active.runId = run.id;
|
|
1579
|
+
active.phase = t(lang, 'control.activity.workflowRunning');
|
|
1580
|
+
pushActivityEvent(active, `🧭 ${t(lang, 'control.workflowStarted', { runId: run.id, workflowId }).replace('\n', ' ')}`);
|
|
1581
|
+
await editTelegramActivity({ bot, chatId, activity: active, force: true });
|
|
1582
|
+
monitorWorkflowRun({ bot, chatId, link, runId: run.id, activity: active }).catch((error) => {
|
|
1583
|
+
console.warn('[telegram-control] workflow monitor failed:', error?.message || error);
|
|
1584
|
+
});
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
async function cancelRun({ bot, chatId, link, runId, editMessageId, confirmed = false }) {
|
|
1588
|
+
const lang = languageFor(link);
|
|
1589
|
+
if (!runId) {
|
|
1590
|
+
await send(bot, chatId, t(lang, 'control.cancelUsage'), { editMessageId });
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
const state = getState(link.user_id);
|
|
1594
|
+
if (state.remoteControlEnabled === false) {
|
|
1595
|
+
await send(bot, chatId, t(lang, 'control.disabled'), { editMessageId });
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
if (state.confirmationPolicy === 'strict' && !confirmed) {
|
|
1599
|
+
await requestConfirmation({
|
|
1600
|
+
bot,
|
|
1601
|
+
chatId,
|
|
1602
|
+
link,
|
|
1603
|
+
action: 'run_cancel',
|
|
1604
|
+
payload: { runId },
|
|
1605
|
+
editMessageId,
|
|
1606
|
+
});
|
|
1607
|
+
return;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
const result = await runTelegramTool({
|
|
1611
|
+
userId: link.user_id,
|
|
1612
|
+
action: 'run_cancel',
|
|
1613
|
+
execute: () => localApi(link.user_id, `/api/orchestration/workflows/runs/${encodeURIComponent(runId)}/cancel`, {
|
|
1614
|
+
method: 'POST',
|
|
1615
|
+
}),
|
|
1616
|
+
});
|
|
1617
|
+
if (!result.ok) {
|
|
1618
|
+
await sendToolFailure({ bot, chatId, link, result, editMessageId });
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1621
|
+
const run = result.data;
|
|
1622
|
+
await send(bot, chatId, t(lang, 'control.runStatus', { runId: run.id, status: run.status }), { editMessageId });
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
async function executeConfirmedAction({ bot, chatId, link, confirmation, editMessageId }) {
|
|
1626
|
+
if (confirmation.action === 'install_provider') {
|
|
1627
|
+
await startCliInstall({
|
|
1628
|
+
bot,
|
|
1629
|
+
chatId,
|
|
1630
|
+
link,
|
|
1631
|
+
provider: confirmation.payload?.provider,
|
|
1632
|
+
editMessageId,
|
|
1633
|
+
confirmed: true,
|
|
1634
|
+
});
|
|
1635
|
+
return true;
|
|
1636
|
+
}
|
|
1637
|
+
if (confirmation.action === 'run_cancel') {
|
|
1638
|
+
await cancelRun({
|
|
1639
|
+
bot,
|
|
1640
|
+
chatId,
|
|
1641
|
+
link,
|
|
1642
|
+
runId: confirmation.payload?.runId,
|
|
1643
|
+
editMessageId,
|
|
1644
|
+
confirmed: true,
|
|
1645
|
+
});
|
|
1646
|
+
return true;
|
|
1647
|
+
}
|
|
1648
|
+
return false;
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
async function findProjectByQuery(query) {
|
|
1652
|
+
const projects = await listProjects();
|
|
1653
|
+
const needle = String(query || '').trim().toLocaleLowerCase('tr');
|
|
1654
|
+
if (!needle) return null;
|
|
1655
|
+
return projects.find((project) => {
|
|
1656
|
+
const candidates = [project.name, project.displayName, project.fullPath, project.path]
|
|
1657
|
+
.filter(Boolean)
|
|
1658
|
+
.map((value) => String(value).toLocaleLowerCase('tr'));
|
|
1659
|
+
return candidates.some((candidate) => candidate === needle || candidate.includes(needle));
|
|
1660
|
+
}) || null;
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
async function listWorkflowSummaries(userId) {
|
|
1664
|
+
try {
|
|
1665
|
+
const data = await localApi(userId, '/api/orchestration/workflows', { timeoutMs: 10_000 });
|
|
1666
|
+
if (Array.isArray(data)) return data;
|
|
1667
|
+
if (Array.isArray(data?.workflows)) return data.workflows;
|
|
1668
|
+
} catch {
|
|
1669
|
+
// Workflow context is helpful for routing but should never block chat input.
|
|
1670
|
+
}
|
|
1671
|
+
return [];
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
async function resolveTelegramAiIntent({ bot, chatId, link, text, activity }) {
|
|
1675
|
+
const state = getState(link.user_id);
|
|
1676
|
+
if (state.routerEnabled === false || state.routerMode !== 'hybrid') {
|
|
1677
|
+
return { action: 'agent_prompt', prompt: text, confidence: 1 };
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
const provider = resolveTelegramProvider(state);
|
|
1681
|
+
const model = resolveTelegramModel(state);
|
|
1682
|
+
if (activity) {
|
|
1683
|
+
activity.type = 'router';
|
|
1684
|
+
activity.provider = provider;
|
|
1685
|
+
activity.project = projectLabel(state);
|
|
1686
|
+
activity.status = 'running';
|
|
1687
|
+
activity.phase = t(activity.lang, 'control.activity.interpreting');
|
|
1688
|
+
pushActivityEvent(activity, `🧠 ${t(activity.lang, 'control.activity.interpreting')}`);
|
|
1689
|
+
await editTelegramActivity({ bot, chatId, activity, force: true });
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
try {
|
|
1693
|
+
const projects = await listProjects().catch(() => []);
|
|
1694
|
+
const workflows = await listWorkflowSummaries(link.user_id);
|
|
1695
|
+
const response = await localApi(link.user_id, '/api/agent', {
|
|
1696
|
+
method: 'POST',
|
|
1697
|
+
timeoutMs: INTENT_ROUTER_TIMEOUT_MS,
|
|
1698
|
+
body: {
|
|
1699
|
+
projectPath: state.selectedProjectPath || process.cwd(),
|
|
1700
|
+
provider,
|
|
1701
|
+
model: model || undefined,
|
|
1702
|
+
message: buildTelegramIntentPrompt(text, state, { projects, workflows }),
|
|
1703
|
+
cleanup: false,
|
|
1704
|
+
stream: false,
|
|
1705
|
+
permissionMode: 'plan',
|
|
1706
|
+
suppressNotifications: true,
|
|
1707
|
+
},
|
|
1708
|
+
});
|
|
1709
|
+
const assistantText = extractAssistantText(response);
|
|
1710
|
+
const intent = parseTelegramAiIntentResponse(assistantText, text);
|
|
1711
|
+
if (activity) {
|
|
1712
|
+
pushActivityEvent(activity, `🧭 ${intent.action} (${Math.round(intent.confidence * 100)}%)`);
|
|
1713
|
+
await editTelegramActivity({ bot, chatId, activity });
|
|
1714
|
+
}
|
|
1715
|
+
return intent;
|
|
1716
|
+
} catch (error) {
|
|
1717
|
+
if (activity) {
|
|
1718
|
+
pushActivityEvent(activity, `⚠️ ${t(activity.lang, 'control.activity.routerFallback')}`);
|
|
1719
|
+
await editTelegramActivity({ bot, chatId, activity });
|
|
1720
|
+
}
|
|
1721
|
+
console.warn('[telegram-control] AI intent router fallback:', error?.message || error);
|
|
1722
|
+
return { action: 'agent_prompt', prompt: text, confidence: 0 };
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
async function handleRoutedIntent({ bot, chatId, link, text, activity }) {
|
|
1727
|
+
const intent = await resolveTelegramAiIntent({ bot, chatId, link, text, activity });
|
|
1728
|
+
const editMessageId = activity?.messageId;
|
|
1729
|
+
|
|
1730
|
+
if (intent.action === 'agent_prompt') {
|
|
1731
|
+
return launchLongTelegramTask({
|
|
1732
|
+
bot,
|
|
1733
|
+
chatId,
|
|
1734
|
+
link,
|
|
1735
|
+
kind: 'agent',
|
|
1736
|
+
activity,
|
|
1737
|
+
task: () => runAgent({ bot, chatId, link, prompt: intent.prompt || text, activity }),
|
|
1738
|
+
});
|
|
1739
|
+
}
|
|
1740
|
+
if (intent.action === 'show_menu') {
|
|
1741
|
+
await showMainMenu({ bot, chatId, link, editMessageId });
|
|
1742
|
+
return true;
|
|
1743
|
+
}
|
|
1744
|
+
if (intent.action === 'show_projects') {
|
|
1745
|
+
await showProjectMenu({ bot, chatId, link, editMessageId });
|
|
1746
|
+
return true;
|
|
1747
|
+
}
|
|
1748
|
+
if (intent.action === 'select_project') {
|
|
1749
|
+
const query = intent.projectQuery || intent.prompt || text;
|
|
1750
|
+
const project = await findProjectByQuery(query);
|
|
1751
|
+
const lang = languageFor(link);
|
|
1752
|
+
if (!project) {
|
|
1753
|
+
await showProjectMenu({
|
|
1754
|
+
bot,
|
|
1755
|
+
chatId,
|
|
1756
|
+
link,
|
|
1757
|
+
editMessageId,
|
|
1758
|
+
notice: t(lang, 'control.projectNotFound', { query }),
|
|
1759
|
+
});
|
|
1760
|
+
return true;
|
|
1761
|
+
}
|
|
1762
|
+
updateTelegramControlState(link.user_id, {
|
|
1763
|
+
selectedProjectName: project.name,
|
|
1764
|
+
selectedProjectPath: project.fullPath || project.path,
|
|
1765
|
+
});
|
|
1766
|
+
await showMainMenu({
|
|
1767
|
+
bot,
|
|
1768
|
+
chatId,
|
|
1769
|
+
link,
|
|
1770
|
+
editMessageId,
|
|
1771
|
+
notice: t(lang, 'control.projectSelected', {
|
|
1772
|
+
project: project.displayName || project.name,
|
|
1773
|
+
path: project.fullPath || project.path,
|
|
1774
|
+
}),
|
|
1775
|
+
});
|
|
1776
|
+
return true;
|
|
1777
|
+
}
|
|
1778
|
+
if (intent.action === 'show_provider_menu') {
|
|
1779
|
+
await showProviderMenu({ bot, chatId, link, editMessageId });
|
|
1780
|
+
return true;
|
|
1781
|
+
}
|
|
1782
|
+
if (intent.action === 'select_provider' && intent.provider) {
|
|
1783
|
+
updateTelegramControlState(link.user_id, { selectedProvider: intent.provider, selectedModel: null });
|
|
1784
|
+
await showModelMenu({ bot, chatId, link, editMessageId });
|
|
1785
|
+
return true;
|
|
1786
|
+
}
|
|
1787
|
+
if (intent.action === 'show_model_menu') {
|
|
1788
|
+
await showModelMenu({ bot, chatId, link, editMessageId });
|
|
1789
|
+
return true;
|
|
1790
|
+
}
|
|
1791
|
+
if (intent.action === 'select_model' && intent.model) {
|
|
1792
|
+
updateTelegramControlState(link.user_id, { selectedModel: intent.model });
|
|
1793
|
+
await showMainMenu({
|
|
1794
|
+
bot,
|
|
1795
|
+
chatId,
|
|
1796
|
+
link,
|
|
1797
|
+
editMessageId,
|
|
1798
|
+
notice: t(languageFor(link), 'control.modelSelected', { model: intent.model }),
|
|
1799
|
+
});
|
|
1800
|
+
return true;
|
|
1801
|
+
}
|
|
1802
|
+
if (intent.action === 'show_runs') {
|
|
1803
|
+
await showRuns({ bot, chatId, link, editMessageId });
|
|
1804
|
+
return true;
|
|
1805
|
+
}
|
|
1806
|
+
if (intent.action === 'show_approvals') {
|
|
1807
|
+
await showApprovalQueue({ bot, chatId, link, editMessageId });
|
|
1808
|
+
return true;
|
|
1809
|
+
}
|
|
1810
|
+
if (intent.action === 'show_workflows') {
|
|
1811
|
+
await showWorkflowMenu({ bot, chatId, link, editMessageId });
|
|
1812
|
+
return true;
|
|
1813
|
+
}
|
|
1814
|
+
if (intent.action === 'run_workflow') {
|
|
1815
|
+
return launchLongTelegramTask({
|
|
1816
|
+
bot,
|
|
1817
|
+
chatId,
|
|
1818
|
+
link,
|
|
1819
|
+
kind: 'workflow',
|
|
1820
|
+
activity,
|
|
1821
|
+
task: () => runWorkflow({ bot, chatId, link, input: intent.workflowInput || text, activity }),
|
|
1822
|
+
});
|
|
1823
|
+
}
|
|
1824
|
+
if (intent.action === 'show_sessions') {
|
|
1825
|
+
await showSessions({ bot, chatId, link, editMessageId });
|
|
1826
|
+
return true;
|
|
1827
|
+
}
|
|
1828
|
+
if (intent.action === 'new_chat') {
|
|
1829
|
+
await startNewChat({ bot, chatId, link, editMessageId });
|
|
1830
|
+
return true;
|
|
1831
|
+
}
|
|
1832
|
+
return launchLongTelegramTask({
|
|
1833
|
+
bot,
|
|
1834
|
+
chatId,
|
|
1835
|
+
link,
|
|
1836
|
+
kind: 'agent',
|
|
1837
|
+
activity,
|
|
1838
|
+
task: () => runAgent({ bot, chatId, link, prompt: text, activity }),
|
|
1839
|
+
});
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
async function fetchRun(userId, runId) {
|
|
1843
|
+
return localApi(userId, `/api/orchestration/workflows/runs/${runId}`);
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
function summarizeRun(run, mode) {
|
|
1847
|
+
const statusIcon = run.status === 'completed'
|
|
1848
|
+
? '✅'
|
|
1849
|
+
: run.status === 'failed'
|
|
1850
|
+
? '❌'
|
|
1851
|
+
: run.status === 'canceled'
|
|
1852
|
+
? '⏹'
|
|
1853
|
+
: '🔧';
|
|
1854
|
+
const lines = [
|
|
1855
|
+
`${statusIcon} Run ${run.id}`,
|
|
1856
|
+
`🧩 Workflow: ${run.workflowId}`,
|
|
1857
|
+
`📌 Status: ${run.status}`,
|
|
1858
|
+
];
|
|
1859
|
+
const nodeRuns = Array.isArray(run.nodeRuns) ? run.nodeRuns : [];
|
|
1860
|
+
if (mode !== 'final') {
|
|
1861
|
+
const visibleNodes = mode === 'errors'
|
|
1862
|
+
? nodeRuns.filter((node) => node.error || node.status === 'failed')
|
|
1863
|
+
: nodeRuns;
|
|
1864
|
+
for (const node of visibleNodes) {
|
|
1865
|
+
const nodeIcon = node.status === 'completed'
|
|
1866
|
+
? '✅'
|
|
1867
|
+
: node.status === 'failed'
|
|
1868
|
+
? '❌'
|
|
1869
|
+
: node.status === 'running'
|
|
1870
|
+
? '🔧'
|
|
1871
|
+
: '⏳';
|
|
1872
|
+
lines.push(`${nodeIcon} ${node.status}: ${node.agentLabel || node.nodeId}${node.error ? ` - ${node.error}` : ''}`);
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
const outputs = nodeRuns
|
|
1876
|
+
.filter((node) => node.outputText)
|
|
1877
|
+
.slice(-3)
|
|
1878
|
+
.map((node) => `\n${node.agentLabel || node.nodeId}:\n${node.outputText}`);
|
|
1879
|
+
return truncate(`${lines.join('\n')}${outputs.join('\n')}`);
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
async function monitorWorkflowRun({ bot, chatId, link, runId, activity = null }) {
|
|
1883
|
+
if (runMonitors.has(runId)) return;
|
|
1884
|
+
runMonitors.set(runId, true);
|
|
1885
|
+
const state = getState(link.user_id);
|
|
1886
|
+
const seenNodeStatus = new Map();
|
|
1887
|
+
try {
|
|
1888
|
+
for (;;) {
|
|
1889
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
1890
|
+
const run = await fetchRun(link.user_id, runId);
|
|
1891
|
+
const nodeRuns = Array.isArray(run.nodeRuns) ? run.nodeRuns : [];
|
|
1892
|
+
if (activity && !TERMINAL_RUN_STATES.has(run.status)) {
|
|
1893
|
+
activity.status = 'running';
|
|
1894
|
+
activity.phase = `${t(activity.lang, 'control.activity.workflowRunning')} (${run.status})`;
|
|
1895
|
+
await editTelegramActivity({ bot, chatId, activity });
|
|
1896
|
+
}
|
|
1897
|
+
if (state.progressMode === 'all') {
|
|
1898
|
+
for (const node of nodeRuns) {
|
|
1899
|
+
const key = `${node.nodeId}:${node.status}`;
|
|
1900
|
+
if (!seenNodeStatus.has(key)) {
|
|
1901
|
+
seenNodeStatus.set(key, true);
|
|
1902
|
+
if (activity) {
|
|
1903
|
+
pushActivityEvent(activity, `${node.status === 'completed' ? '✅' : node.status === 'failed' ? '❌' : '🔧'} ${node.agentLabel || node.nodeId}: ${node.status}${node.error ? ` - ${node.error}` : ''}`);
|
|
1904
|
+
activity.phase = t(activity.lang, 'control.activity.workflowRunning');
|
|
1905
|
+
await editTelegramActivity({ bot, chatId, activity });
|
|
1906
|
+
} else {
|
|
1907
|
+
await send(bot, chatId, `${node.agentLabel || node.nodeId}: ${node.status}${node.error ? `\n${node.error}` : ''}`);
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
if (state.progressMode === 'errors') {
|
|
1913
|
+
for (const node of nodeRuns.filter((candidate) => candidate.error || candidate.status === 'failed')) {
|
|
1914
|
+
const key = `${node.nodeId}:${node.status}:${node.error || ''}`;
|
|
1915
|
+
if (!seenNodeStatus.has(key)) {
|
|
1916
|
+
seenNodeStatus.set(key, true);
|
|
1917
|
+
if (activity) {
|
|
1918
|
+
pushActivityEvent(activity, `❌ ${node.agentLabel || node.nodeId}: ${node.status}${node.error ? ` - ${node.error}` : ''}`);
|
|
1919
|
+
activity.phase = t(activity.lang, 'control.activity.workflowRunning');
|
|
1920
|
+
await editTelegramActivity({ bot, chatId, activity });
|
|
1921
|
+
} else {
|
|
1922
|
+
await send(bot, chatId, `${node.agentLabel || node.nodeId}: ${node.status}\n${node.error || ''}`);
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
if (TERMINAL_RUN_STATES.has(run.status)) {
|
|
1928
|
+
if (activity) {
|
|
1929
|
+
activity.status = run.status === 'completed' ? 'done' : 'failed';
|
|
1930
|
+
activity.phase = run.status === 'completed'
|
|
1931
|
+
? t(activity.lang, 'control.activity.done')
|
|
1932
|
+
: t(activity.lang, 'control.activity.failed');
|
|
1933
|
+
activity.output = summarizeRun(run, state.progressMode);
|
|
1934
|
+
await editTelegramActivity({ bot, chatId, activity, force: true });
|
|
1935
|
+
} else {
|
|
1936
|
+
await send(bot, chatId, summarizeRun(run, state.progressMode));
|
|
1937
|
+
}
|
|
1938
|
+
return;
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
} finally {
|
|
1942
|
+
runMonitors.delete(runId);
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
export async function startCliInstall({ bot, chatId, link, provider, editMessageId, confirmed = false }) {
|
|
1947
|
+
const lang = languageFor(link);
|
|
1948
|
+
if (!PROVIDERS.includes(provider)) {
|
|
1949
|
+
await send(bot, chatId, t(lang, 'control.providerAuthFallback'), { editMessageId });
|
|
1950
|
+
return;
|
|
1951
|
+
}
|
|
1952
|
+
const state = getState(link.user_id);
|
|
1953
|
+
if (state.remoteControlEnabled === false) {
|
|
1954
|
+
await send(bot, chatId, t(lang, 'control.disabled'), { editMessageId });
|
|
1955
|
+
return;
|
|
1956
|
+
}
|
|
1957
|
+
if (state.confirmationPolicy === 'strict' && !confirmed) {
|
|
1958
|
+
await requestConfirmation({
|
|
1959
|
+
bot,
|
|
1960
|
+
chatId,
|
|
1961
|
+
link,
|
|
1962
|
+
action: 'install_provider',
|
|
1963
|
+
payload: { provider },
|
|
1964
|
+
editMessageId,
|
|
1965
|
+
});
|
|
1966
|
+
return;
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
const result = await runTelegramTool({
|
|
1970
|
+
userId: link.user_id,
|
|
1971
|
+
action: 'install_provider',
|
|
1972
|
+
execute: () => localApi(link.user_id, `/api/providers/${provider}/install`, { method: 'POST' }),
|
|
1973
|
+
});
|
|
1974
|
+
if (!result.ok) {
|
|
1975
|
+
await sendToolFailure({ bot, chatId, link, result, editMessageId });
|
|
1976
|
+
return;
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
const data = result.data;
|
|
1980
|
+
if (data?.manual) {
|
|
1981
|
+
await send(bot, chatId, t(lang, 'control.manualInstall', { provider, manual: data.manual }), { editMessageId });
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
await send(bot, chatId, t(lang, 'control.installStarted', {
|
|
1985
|
+
provider,
|
|
1986
|
+
jobId: data.jobId,
|
|
1987
|
+
command: data.installCmd || 'internal installer',
|
|
1988
|
+
}), { editMessageId });
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
async function showInstallMenu({ bot, chatId, link, editMessageId }) {
|
|
1992
|
+
const lang = languageFor(link);
|
|
1993
|
+
const buttons = PROVIDERS.map((provider) => button(provider, 'install_provider', { provider }));
|
|
1994
|
+
await send(bot, chatId, t(lang, 'control.installMenu'), {
|
|
1995
|
+
editMessageId,
|
|
1996
|
+
reply_markup: { inline_keyboard: rows(buttons, 2) },
|
|
1997
|
+
});
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
async function showAuthMenu({ bot, chatId, link, editMessageId }) {
|
|
2001
|
+
const lang = languageFor(link);
|
|
2002
|
+
const buttons = PROVIDERS.map((provider) => button(provider, 'auth_provider', { provider }));
|
|
2003
|
+
await send(bot, chatId, t(lang, 'control.authMenu'), {
|
|
2004
|
+
editMessageId,
|
|
2005
|
+
reply_markup: { inline_keyboard: rows(buttons, 2) },
|
|
2006
|
+
});
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
async function showSettings({ bot, chatId, link, editMessageId }) {
|
|
2010
|
+
const lang = languageFor(link);
|
|
2011
|
+
const state = getState(link.user_id);
|
|
2012
|
+
const keyboard = [
|
|
2013
|
+
[
|
|
2014
|
+
button(
|
|
2015
|
+
state.remoteControlEnabled
|
|
2016
|
+
? checked(t(lang, 'control.button.remoteOn'))
|
|
2017
|
+
: t(lang, 'control.button.remoteOff'),
|
|
2018
|
+
'toggle_control',
|
|
2019
|
+
),
|
|
2020
|
+
],
|
|
2021
|
+
[
|
|
2022
|
+
button(state.progressMode === 'final' ? checked(t(lang, 'control.button.progressFinal')) : t(lang, 'control.button.progressFinal'), 'progress_mode', { progressMode: 'final' }),
|
|
2023
|
+
button(state.progressMode === 'steps' ? checked(t(lang, 'control.button.progressSteps')) : t(lang, 'control.button.progressSteps'), 'progress_mode', { progressMode: 'steps' }),
|
|
2024
|
+
button(state.progressMode === 'errors' ? checked(t(lang, 'control.button.progressErrors')) : t(lang, 'control.button.progressErrors'), 'progress_mode', { progressMode: 'errors' }),
|
|
2025
|
+
button(state.progressMode === 'all' ? checked(t(lang, 'control.button.progressAll')) : t(lang, 'control.button.progressAll'), 'progress_mode', { progressMode: 'all' }),
|
|
2026
|
+
],
|
|
2027
|
+
[
|
|
2028
|
+
button(t(lang, 'control.button.language'), 'language_menu'),
|
|
2029
|
+
button(t(lang, 'control.button.mainMenu'), 'menu'),
|
|
2030
|
+
],
|
|
2031
|
+
];
|
|
2032
|
+
await send(bot, chatId, `${t(lang, 'control.settingsTitle')}\n\n${stateSummary(lang, state)}`, {
|
|
2033
|
+
editMessageId,
|
|
2034
|
+
reply_markup: { inline_keyboard: keyboard },
|
|
2035
|
+
});
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
async function showLanguageMenu({ bot, chatId, link, editMessageId }) {
|
|
2039
|
+
const lang = languageFor(link);
|
|
2040
|
+
const buttons = SUPPORTED_LANGUAGES.map((language) => button(language, 'language_select', { language }));
|
|
2041
|
+
await send(bot, chatId, t(lang, 'control.pickLanguage'), {
|
|
2042
|
+
editMessageId,
|
|
2043
|
+
reply_markup: { inline_keyboard: rows(buttons, 3) },
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
async function handleAwaitingInput({ bot, chatId, link, text }) {
|
|
2048
|
+
const state = getState(link.user_id);
|
|
2049
|
+
const awaiting = state.awaiting;
|
|
2050
|
+
if (!awaiting?.type) return false;
|
|
2051
|
+
updateTelegramControlState(link.user_id, { awaiting: null });
|
|
2052
|
+
|
|
2053
|
+
if (awaiting.type === 'agent_prompt') {
|
|
2054
|
+
return launchLongTelegramTask({
|
|
2055
|
+
bot,
|
|
2056
|
+
chatId,
|
|
2057
|
+
link,
|
|
2058
|
+
kind: 'agent',
|
|
2059
|
+
task: () => runAgent({ bot, chatId, link, prompt: text }),
|
|
2060
|
+
});
|
|
2061
|
+
}
|
|
2062
|
+
if (awaiting.type === 'workflow_prompt') {
|
|
2063
|
+
return launchLongTelegramTask({
|
|
2064
|
+
bot,
|
|
2065
|
+
chatId,
|
|
2066
|
+
link,
|
|
2067
|
+
kind: 'workflow',
|
|
2068
|
+
task: () => runWorkflow({ bot, chatId, link, input: text }),
|
|
2069
|
+
});
|
|
2070
|
+
}
|
|
2071
|
+
return false;
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
async function handleCommand({ bot, chatId, link, text }) {
|
|
2075
|
+
const [commandRaw, ...rest] = text.split(/\s+/);
|
|
2076
|
+
const command = normalizeCommandToken(commandRaw);
|
|
2077
|
+
const argText = rest.join(' ').trim();
|
|
2078
|
+
const lang = languageFor(link);
|
|
2079
|
+
|
|
2080
|
+
if (command === '/menu' || command === '/start' || command === 'menu') {
|
|
2081
|
+
await showMainMenu({ bot, chatId, link });
|
|
2082
|
+
return true;
|
|
2083
|
+
}
|
|
2084
|
+
if (command === '/help') {
|
|
2085
|
+
await showHelp({ bot, chatId, link });
|
|
2086
|
+
return true;
|
|
2087
|
+
}
|
|
2088
|
+
if (command === '/projects') {
|
|
2089
|
+
await showProjectMenu({ bot, chatId, link });
|
|
2090
|
+
return true;
|
|
2091
|
+
}
|
|
2092
|
+
if (command === '/provider') {
|
|
2093
|
+
await showProviderMenu({ bot, chatId, link });
|
|
2094
|
+
return true;
|
|
2095
|
+
}
|
|
2096
|
+
if (command === '/model' || command === '/models') {
|
|
2097
|
+
await showModelMenu({ bot, chatId, link, refresh: command === '/models' && argText === 'refresh' });
|
|
2098
|
+
return true;
|
|
2099
|
+
}
|
|
2100
|
+
if (command === '/workflows' || command === '/orchestration') {
|
|
2101
|
+
await showWorkflowMenu({ bot, chatId, link });
|
|
2102
|
+
return true;
|
|
2103
|
+
}
|
|
2104
|
+
if (command === '/runs') {
|
|
2105
|
+
await showRuns({ bot, chatId, link });
|
|
2106
|
+
return true;
|
|
2107
|
+
}
|
|
2108
|
+
if (command === '/approvals') {
|
|
2109
|
+
await showApprovalQueue({ bot, chatId, link });
|
|
2110
|
+
return true;
|
|
2111
|
+
}
|
|
2112
|
+
if (command === '/controlroom' || command === '/control-room') {
|
|
2113
|
+
await showControlRoom({ bot, chatId, link });
|
|
2114
|
+
return true;
|
|
2115
|
+
}
|
|
2116
|
+
if (command === '/webhooks') {
|
|
2117
|
+
await showWebhookMenu({ bot, chatId, link });
|
|
2118
|
+
return true;
|
|
2119
|
+
}
|
|
2120
|
+
if (command === '/sessions') {
|
|
2121
|
+
await showSessions({ bot, chatId, link });
|
|
2122
|
+
return true;
|
|
2123
|
+
}
|
|
2124
|
+
if (command === '/terminal') {
|
|
2125
|
+
await showActiveTerminalStatus({ bot, chatId, link });
|
|
2126
|
+
return true;
|
|
2127
|
+
}
|
|
2128
|
+
if (command === '/detach') {
|
|
2129
|
+
await detachActiveTerminal({ bot, chatId, link });
|
|
2130
|
+
return true;
|
|
2131
|
+
}
|
|
2132
|
+
if (command === '/newchat') {
|
|
2133
|
+
await startNewChat({ bot, chatId, link });
|
|
2134
|
+
return true;
|
|
2135
|
+
}
|
|
2136
|
+
if (command === '/settings') {
|
|
2137
|
+
await showSettings({ bot, chatId, link });
|
|
2138
|
+
return true;
|
|
2139
|
+
}
|
|
2140
|
+
if (command === '/install') {
|
|
2141
|
+
if (PROVIDERS.includes(argText)) await startCliInstall({ bot, chatId, link, provider: argText });
|
|
2142
|
+
else await showInstallMenu({ bot, chatId, link });
|
|
2143
|
+
return true;
|
|
2144
|
+
}
|
|
2145
|
+
if (command === '/auth') {
|
|
2146
|
+
if (PROVIDERS.includes(argText)) await send(bot, chatId, `${argText} login:\n${AUTH_HELP[argText]}`);
|
|
2147
|
+
else await showAuthMenu({ bot, chatId, link });
|
|
2148
|
+
return true;
|
|
2149
|
+
}
|
|
2150
|
+
if (command === '/control') {
|
|
2151
|
+
const enabled = argText !== 'off';
|
|
2152
|
+
updateTelegramControlState(link.user_id, { remoteControlEnabled: enabled });
|
|
2153
|
+
await send(bot, chatId, enabled ? t(lang, 'control.remoteEnabled') : t(lang, 'control.remoteDisabled'));
|
|
2154
|
+
return true;
|
|
2155
|
+
}
|
|
2156
|
+
if (command === '/progress') {
|
|
2157
|
+
if (!['final', 'steps', 'all', 'errors'].includes(argText)) {
|
|
2158
|
+
await send(bot, chatId, t(lang, 'control.progressUsage'));
|
|
2159
|
+
return true;
|
|
2160
|
+
}
|
|
2161
|
+
updateTelegramControlState(link.user_id, { progressMode: argText });
|
|
2162
|
+
await send(bot, chatId, t(lang, 'control.progressSet', { mode: argText }));
|
|
2163
|
+
return true;
|
|
2164
|
+
}
|
|
2165
|
+
if (command === '/chat') {
|
|
2166
|
+
if (!argText) {
|
|
2167
|
+
updateTelegramControlState(link.user_id, { awaiting: { type: 'agent_prompt' } });
|
|
2168
|
+
await send(bot, chatId, t(lang, 'control.sendAgentPrompt'));
|
|
2169
|
+
return true;
|
|
2170
|
+
}
|
|
2171
|
+
return launchLongTelegramTask({
|
|
2172
|
+
bot,
|
|
2173
|
+
chatId,
|
|
2174
|
+
link,
|
|
2175
|
+
kind: 'agent',
|
|
2176
|
+
task: () => runAgent({ bot, chatId, link, prompt: argText }),
|
|
2177
|
+
});
|
|
2178
|
+
}
|
|
2179
|
+
if (command === '/workflow' || command === '/orchestrate') {
|
|
2180
|
+
if (!argText) {
|
|
2181
|
+
updateTelegramControlState(link.user_id, { awaiting: { type: 'workflow_prompt' } });
|
|
2182
|
+
await send(bot, chatId, t(lang, 'control.sendWorkflowPrompt'));
|
|
2183
|
+
return true;
|
|
2184
|
+
}
|
|
2185
|
+
return launchLongTelegramTask({
|
|
2186
|
+
bot,
|
|
2187
|
+
chatId,
|
|
2188
|
+
link,
|
|
2189
|
+
kind: 'workflow',
|
|
2190
|
+
task: () => runWorkflow({ bot, chatId, link, input: argText }),
|
|
2191
|
+
});
|
|
2192
|
+
}
|
|
2193
|
+
if (command === '/cancel') {
|
|
2194
|
+
if (!argText) {
|
|
2195
|
+
await send(bot, chatId, t(lang, 'control.cancelUsage'));
|
|
2196
|
+
return true;
|
|
2197
|
+
}
|
|
2198
|
+
await cancelRun({ bot, chatId, link, runId: argText });
|
|
2199
|
+
return true;
|
|
2200
|
+
}
|
|
2201
|
+
return false;
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
async function handleTelegramControlMessageInternal({ bot, msg, link }) {
|
|
2205
|
+
const chatId = msg.chat.id;
|
|
2206
|
+
const text = String(msg.text || '').trim();
|
|
2207
|
+
if (!text) return false;
|
|
2208
|
+
|
|
2209
|
+
if (text === '/') {
|
|
2210
|
+
await showCommandPalette({ bot, chatId, link });
|
|
2211
|
+
return true;
|
|
2212
|
+
}
|
|
2213
|
+
if (text.startsWith('/') && !getTelegramControlCommand(text)) {
|
|
2214
|
+
await showCommandPalette({ bot, chatId, link, unknown: true });
|
|
2215
|
+
return true;
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
if (await handleAwaitingInput({ bot, chatId, link, text })) return true;
|
|
2219
|
+
if (await handleCommand({ bot, chatId, link, text })) return true;
|
|
2220
|
+
|
|
2221
|
+
const state = getState(link.user_id);
|
|
2222
|
+
if (getActiveTerminal(state)) {
|
|
2223
|
+
return sendToActiveTerminal({ bot, chatId, link, text });
|
|
2224
|
+
}
|
|
2225
|
+
if (state.routerEnabled === false) return false;
|
|
2226
|
+
if (!state.remoteControlEnabled) {
|
|
2227
|
+
await send(bot, chatId, t(languageFor(link), 'control.disabled'));
|
|
2228
|
+
return true;
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
const activity = await createTelegramActivity({
|
|
2232
|
+
bot,
|
|
2233
|
+
chatId,
|
|
2234
|
+
link,
|
|
2235
|
+
type: 'router',
|
|
2236
|
+
prompt: text,
|
|
2237
|
+
phase: t(languageFor(link), 'control.activity.interpreting'),
|
|
2238
|
+
});
|
|
2239
|
+
return handleRoutedIntent({ bot, chatId, link, text, activity });
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
export async function handleTelegramControlMessage(args) {
|
|
2243
|
+
const chatId = args?.msg?.chat?.id;
|
|
2244
|
+
if (!chatId) return false;
|
|
2245
|
+
return enqueueTelegramJob(chatId, async () => {
|
|
2246
|
+
try {
|
|
2247
|
+
return await handleTelegramControlMessageInternal(args);
|
|
2248
|
+
} catch (error) {
|
|
2249
|
+
console.error('[telegram-control] message handler failed:', error);
|
|
2250
|
+
await send(args.bot, chatId, t(languageFor(args.link), 'error.generic')).catch(() => {});
|
|
2251
|
+
return true;
|
|
2252
|
+
}
|
|
2253
|
+
}).catch(async (error) => {
|
|
2254
|
+
console.error('[telegram-control] message job failed:', error);
|
|
2255
|
+
await send(args.bot, chatId, t(languageFor(args.link), 'error.generic')).catch(() => {});
|
|
2256
|
+
return true;
|
|
2257
|
+
});
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
async function showRunDetail({ bot, chatId, link, runId, editMessageId }) {
|
|
2261
|
+
const lang = languageFor(link);
|
|
2262
|
+
const run = await fetchRun(link.user_id, runId);
|
|
2263
|
+
await send(bot, chatId, summarizeRun(run, 'all'), {
|
|
2264
|
+
editMessageId,
|
|
2265
|
+
reply_markup: {
|
|
2266
|
+
inline_keyboard: [
|
|
2267
|
+
[button(t(lang, 'control.button.cancelRun'), 'run_cancel', { runId }), button(t(lang, 'control.button.refresh'), 'run_detail', { runId })],
|
|
2268
|
+
[button(t(lang, 'control.button.runs'), 'runs'), button(t(lang, 'control.button.mainMenu'), 'menu')],
|
|
2269
|
+
],
|
|
2270
|
+
},
|
|
2271
|
+
});
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
async function handleTelegramControlCallbackInternal({ bot, query, link }) {
|
|
2275
|
+
const chatId = query.message?.chat?.id;
|
|
2276
|
+
if (!chatId) return;
|
|
2277
|
+
const editMessageId = query.message?.message_id;
|
|
2278
|
+
const entry = readAction(query.data);
|
|
2279
|
+
if (!entry) {
|
|
2280
|
+
const lang = languageFor(link);
|
|
2281
|
+
await bot.answerCallbackQuery(query.id, { text: t(lang, 'control.menuExpired') }).catch(() => {});
|
|
2282
|
+
await send(bot, chatId, t(lang, 'control.menuExpiredLong'));
|
|
2283
|
+
return;
|
|
2284
|
+
}
|
|
2285
|
+
await bot.answerCallbackQuery(query.id).catch(() => {});
|
|
2286
|
+
|
|
2287
|
+
const { action, payload } = entry;
|
|
2288
|
+
if (action === 'confirm_action') {
|
|
2289
|
+
forgetAction(entry.id);
|
|
2290
|
+
const lang = languageFor(link);
|
|
2291
|
+
const result = consumeTelegramConfirmation(link.user_id, payload.id);
|
|
2292
|
+
if (!result.ok) {
|
|
2293
|
+
await send(bot, chatId, t(lang, result.reason === 'expired'
|
|
2294
|
+
? 'control.confirmationExpired'
|
|
2295
|
+
: 'control.confirmationMissing'), { editMessageId });
|
|
2296
|
+
return;
|
|
2297
|
+
}
|
|
2298
|
+
if (await executeConfirmedAction({
|
|
2299
|
+
bot,
|
|
2300
|
+
chatId,
|
|
2301
|
+
link,
|
|
2302
|
+
confirmation: result.confirmation,
|
|
2303
|
+
editMessageId,
|
|
2304
|
+
})) return;
|
|
2305
|
+
await send(bot, chatId, t(lang, 'error.generic'), { editMessageId });
|
|
2306
|
+
return;
|
|
2307
|
+
}
|
|
2308
|
+
if (action === 'cancel_confirmation') {
|
|
2309
|
+
forgetAction(entry.id);
|
|
2310
|
+
clearTelegramConfirmation(link.user_id, payload.id);
|
|
2311
|
+
await send(bot, chatId, t(languageFor(link), 'control.confirmationCanceled'), { editMessageId });
|
|
2312
|
+
return;
|
|
2313
|
+
}
|
|
2314
|
+
if (action === 'menu') return showMainMenu({ bot, chatId, link, editMessageId });
|
|
2315
|
+
if (action === 'projects') return showProjectMenu({ bot, chatId, link, editMessageId });
|
|
2316
|
+
if (action === 'providers') return showProviderMenu({ bot, chatId, link, editMessageId });
|
|
2317
|
+
if (action === 'models') return showModelMenu({ bot, chatId, link, editMessageId });
|
|
2318
|
+
if (action === 'models_refresh') return showModelMenu({ bot, chatId, link, refresh: true, editMessageId });
|
|
2319
|
+
if (action === 'workflows') return showWorkflowMenu({ bot, chatId, link, editMessageId });
|
|
2320
|
+
if (action === 'runs') return showRuns({ bot, chatId, link, editMessageId });
|
|
2321
|
+
if (action === 'approvals') return showApprovalQueue({ bot, chatId, link, editMessageId });
|
|
2322
|
+
if (action === 'control_room') return showControlRoom({ bot, chatId, link, editMessageId });
|
|
2323
|
+
if (action === 'webhooks') return showWebhookMenu({ bot, chatId, link, editMessageId });
|
|
2324
|
+
if (action === 'sessions') return showSessions({ bot, chatId, link, editMessageId });
|
|
2325
|
+
if (action === 'terminal_status') return showActiveTerminalStatus({ bot, chatId, link, editMessageId });
|
|
2326
|
+
if (action === 'detach_terminal') return detachActiveTerminal({ bot, chatId, link, editMessageId });
|
|
2327
|
+
if (action === 'new_chat') return startNewChat({ bot, chatId, link, editMessageId });
|
|
2328
|
+
if (action === 'install_menu') return showInstallMenu({ bot, chatId, link, editMessageId });
|
|
2329
|
+
if (action === 'auth_menu') return showAuthMenu({ bot, chatId, link, editMessageId });
|
|
2330
|
+
if (action === 'settings') return showSettings({ bot, chatId, link, editMessageId });
|
|
2331
|
+
if (action === 'language_menu') return showLanguageMenu({ bot, chatId, link, editMessageId });
|
|
2332
|
+
if (action === 'project_select') {
|
|
2333
|
+
updateTelegramControlState(link.user_id, {
|
|
2334
|
+
selectedProjectName: payload.name,
|
|
2335
|
+
selectedProjectPath: payload.path,
|
|
2336
|
+
});
|
|
2337
|
+
const lang = languageFor(link);
|
|
2338
|
+
return showMainMenu({
|
|
2339
|
+
bot,
|
|
2340
|
+
chatId,
|
|
2341
|
+
link,
|
|
2342
|
+
editMessageId,
|
|
2343
|
+
notice: t(lang, 'control.projectSelected', { project: payload.displayName || payload.name, path: payload.path }),
|
|
2344
|
+
});
|
|
2345
|
+
}
|
|
2346
|
+
if (action === 'provider_select') {
|
|
2347
|
+
updateTelegramControlState(link.user_id, { selectedProvider: payload.provider, selectedModel: null });
|
|
2348
|
+
return showModelMenu({ bot, chatId, link, editMessageId });
|
|
2349
|
+
}
|
|
2350
|
+
if (action === 'model_select') {
|
|
2351
|
+
updateTelegramControlState(link.user_id, { selectedModel: payload.model || null });
|
|
2352
|
+
const lang = languageFor(link);
|
|
2353
|
+
return showMainMenu({
|
|
2354
|
+
bot,
|
|
2355
|
+
chatId,
|
|
2356
|
+
link,
|
|
2357
|
+
editMessageId,
|
|
2358
|
+
notice: payload.model
|
|
2359
|
+
? t(lang, 'control.modelSelected', { model: payload.model })
|
|
2360
|
+
: t(lang, 'control.modelCleared'),
|
|
2361
|
+
});
|
|
2362
|
+
}
|
|
2363
|
+
if (action === 'workflow_select') {
|
|
2364
|
+
updateTelegramControlState(link.user_id, { selectedWorkflowId: payload.workflowId });
|
|
2365
|
+
const lang = languageFor(link);
|
|
2366
|
+
return showMainMenu({
|
|
2367
|
+
bot,
|
|
2368
|
+
chatId,
|
|
2369
|
+
link,
|
|
2370
|
+
editMessageId,
|
|
2371
|
+
notice: t(lang, 'control.workflowSelected', { workflow: payload.name || payload.workflowId }),
|
|
2372
|
+
});
|
|
2373
|
+
}
|
|
2374
|
+
if (action === 'workflow_prompt') {
|
|
2375
|
+
updateTelegramControlState(link.user_id, { awaiting: { type: 'workflow_prompt' } });
|
|
2376
|
+
await send(bot, chatId, t(languageFor(link), 'control.sendWorkflowPrompt'), { editMessageId });
|
|
2377
|
+
return;
|
|
2378
|
+
}
|
|
2379
|
+
if (action === 'run_detail') return showRunDetail({ bot, chatId, link, runId: payload.runId, editMessageId });
|
|
2380
|
+
if (action === 'run_cancel') {
|
|
2381
|
+
await cancelRun({ bot, chatId, link, runId: payload.runId, editMessageId });
|
|
2382
|
+
return;
|
|
2383
|
+
}
|
|
2384
|
+
if (action === 'approval_decide') {
|
|
2385
|
+
const toolResult = await runTelegramTool({
|
|
2386
|
+
userId: link.user_id,
|
|
2387
|
+
action: 'approval_decide',
|
|
2388
|
+
execute: () => localApi(link.user_id, `/api/orchestration/workflows/approvals/${encodeURIComponent(payload.approvalId)}`, {
|
|
2389
|
+
method: 'POST',
|
|
2390
|
+
body: {
|
|
2391
|
+
allow: payload.allow === true,
|
|
2392
|
+
source: 'telegram',
|
|
2393
|
+
},
|
|
2394
|
+
}),
|
|
2395
|
+
});
|
|
2396
|
+
if (!toolResult.ok) {
|
|
2397
|
+
await sendToolFailure({ bot, chatId, link, result: toolResult, editMessageId });
|
|
2398
|
+
return;
|
|
2399
|
+
}
|
|
2400
|
+
const result = toolResult.data;
|
|
2401
|
+
const lang = languageFor(link);
|
|
2402
|
+
await send(bot, chatId, t(lang, 'control.approvalDecided', {
|
|
2403
|
+
approvalId: payload.approvalId,
|
|
2404
|
+
status: payload.allow === true ? 'approved' : 'denied',
|
|
2405
|
+
runId: result?.runId || '',
|
|
2406
|
+
}), { editMessageId });
|
|
2407
|
+
return showApprovalQueue({ bot, chatId, link });
|
|
2408
|
+
}
|
|
2409
|
+
if (action === 'install_provider') return startCliInstall({ bot, chatId, link, provider: payload.provider, editMessageId });
|
|
2410
|
+
if (action === 'auth_provider') {
|
|
2411
|
+
await send(bot, chatId, `${payload.provider} login:\n${AUTH_HELP[payload.provider] || t(languageFor(link), 'control.providerAuthFallback')}`, { editMessageId });
|
|
2412
|
+
return;
|
|
2413
|
+
}
|
|
2414
|
+
if (action === 'toggle_control') {
|
|
2415
|
+
const state = getState(link.user_id);
|
|
2416
|
+
updateTelegramControlState(link.user_id, { remoteControlEnabled: !state.remoteControlEnabled });
|
|
2417
|
+
return showSettings({ bot, chatId, link, editMessageId });
|
|
2418
|
+
}
|
|
2419
|
+
if (action === 'progress_mode') {
|
|
2420
|
+
updateTelegramControlState(link.user_id, { progressMode: payload.progressMode });
|
|
2421
|
+
return showSettings({ bot, chatId, link, editMessageId });
|
|
2422
|
+
}
|
|
2423
|
+
if (action === 'language_select') {
|
|
2424
|
+
telegramLinksDb.updatePreferences(link.user_id, { language: payload.language });
|
|
2425
|
+
return showMainMenu({
|
|
2426
|
+
bot,
|
|
2427
|
+
chatId,
|
|
2428
|
+
link: { ...link, language: payload.language },
|
|
2429
|
+
editMessageId,
|
|
2430
|
+
notice: t(payload.language, 'control.languageSet', { language: payload.language }),
|
|
2431
|
+
});
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
export async function handleTelegramControlCallback(args) {
|
|
2436
|
+
const chatId = args?.query?.message?.chat?.id;
|
|
2437
|
+
if (!chatId) return;
|
|
2438
|
+
return enqueueTelegramJob(chatId, async () => {
|
|
2439
|
+
try {
|
|
2440
|
+
await handleTelegramControlCallbackInternal(args);
|
|
2441
|
+
} catch (error) {
|
|
2442
|
+
console.error('[telegram-control] callback failed:', error);
|
|
2443
|
+
const lang = languageFor(args.link);
|
|
2444
|
+
await args.bot?.answerCallbackQuery(args.query?.id, { text: t(lang, 'error.generic') }).catch(() => {});
|
|
2445
|
+
await send(args.bot, chatId, t(lang, 'error.generic')).catch(() => {});
|
|
2446
|
+
}
|
|
2447
|
+
}).catch(async (error) => {
|
|
2448
|
+
console.error('[telegram-control] callback job failed:', error);
|
|
2449
|
+
const lang = languageFor(args.link);
|
|
2450
|
+
await args.bot?.answerCallbackQuery(args.query?.id, { text: t(lang, 'error.generic') }).catch(() => {});
|
|
2451
|
+
await send(args.bot, chatId, t(lang, 'error.generic')).catch(() => {});
|
|
2452
|
+
});
|
|
2453
|
+
}
|