ai-control-center 1.15.2
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/LICENSE +21 -0
- package/README.md +584 -0
- package/bin/aicc.js +772 -0
- package/lib/actions/approve.js +71 -0
- package/lib/actions/assign-project.js +132 -0
- package/lib/actions/browser-test.js +64 -0
- package/lib/actions/cleanup.js +174 -0
- package/lib/actions/debug.js +298 -0
- package/lib/actions/deploy.js +1229 -0
- package/lib/actions/fix-bug.js +134 -0
- package/lib/actions/new-feature.js +255 -0
- package/lib/actions/reject.js +307 -0
- package/lib/actions/review.js +706 -0
- package/lib/actions/status.js +47 -0
- package/lib/agents/browser-qa-agent.js +611 -0
- package/lib/agents/payment-agent.js +116 -0
- package/lib/agents/suggestion-agent.js +88 -0
- package/lib/cli.js +303 -0
- package/lib/config.js +243 -0
- package/lib/hub/hub-server.js +440 -0
- package/lib/hub/project-poller.js +75 -0
- package/lib/hub/skill-registry.js +89 -0
- package/lib/hub/state-aggregator.js +204 -0
- package/lib/index.js +471 -0
- package/lib/init/doctor.js +523 -0
- package/lib/init/presets.js +222 -0
- package/lib/init/skill-fetcher.js +77 -0
- package/lib/init/wizard.js +973 -0
- package/lib/integrations/codex-runner.js +128 -0
- package/lib/integrations/github-actions.js +248 -0
- package/lib/integrations/github-reporter.js +229 -0
- package/lib/integrations/screenshot-store.js +102 -0
- package/lib/openclaw/bridge.js +650 -0
- package/lib/openclaw/generate-skill.js +235 -0
- package/lib/openclaw/openclaw.json +64 -0
- package/lib/orchestrator/autonomous-loop.js +429 -0
- package/lib/orchestrator/thread-triggers.js +63 -0
- package/lib/roleplay/agent-messenger.js +75 -0
- package/lib/roleplay/discussion-threads.js +303 -0
- package/lib/roleplay/health-monitor.js +121 -0
- package/lib/roleplay/pm-agent.js +513 -0
- package/lib/roleplay/roleplay-config.js +25 -0
- package/lib/roleplay/room.js +164 -0
- package/lib/shared/action-runner.js +2330 -0
- package/lib/shared/event-bus.js +185 -0
- package/lib/slack/bot.js +378 -0
- package/lib/telegram/bot.js +416 -0
- package/lib/telegram/commands.js +1267 -0
- package/lib/telegram/keyboards.js +113 -0
- package/lib/telegram/notifications.js +247 -0
- package/lib/twitch/bot.js +354 -0
- package/lib/twitch/commands.js +302 -0
- package/lib/twitch/notifications.js +63 -0
- package/lib/utils/achievements.js +191 -0
- package/lib/utils/activity-log.js +182 -0
- package/lib/utils/agent-leaderboard.js +119 -0
- package/lib/utils/audit-logger.js +232 -0
- package/lib/utils/codebase-context.js +288 -0
- package/lib/utils/codebase-indexer.js +381 -0
- package/lib/utils/config-schema.js +230 -0
- package/lib/utils/context-compressor.js +172 -0
- package/lib/utils/correlation.js +63 -0
- package/lib/utils/cost-tracker.js +423 -0
- package/lib/utils/cron-scheduler.js +53 -0
- package/lib/utils/db-adapter.js +293 -0
- package/lib/utils/display.js +272 -0
- package/lib/utils/errors.js +116 -0
- package/lib/utils/format.js +134 -0
- package/lib/utils/intent-engine.js +464 -0
- package/lib/utils/mcp-client.js +238 -0
- package/lib/utils/model-ab-test.js +164 -0
- package/lib/utils/notify.js +122 -0
- package/lib/utils/persona-loader.js +80 -0
- package/lib/utils/pipeline-lock.js +73 -0
- package/lib/utils/pipeline.js +214 -0
- package/lib/utils/plugin-runner.js +234 -0
- package/lib/utils/rate-limiter.js +84 -0
- package/lib/utils/rbac.js +74 -0
- package/lib/utils/runner.js +1809 -0
- package/lib/utils/security.js +191 -0
- package/lib/utils/self-healer.js +144 -0
- package/lib/utils/skill-loader.js +255 -0
- package/lib/utils/spinner.js +132 -0
- package/lib/utils/stage-queue.js +50 -0
- package/lib/utils/state-machine.js +89 -0
- package/lib/utils/status-bar.js +327 -0
- package/lib/utils/token-estimator.js +101 -0
- package/lib/utils/ux-analyzer.js +101 -0
- package/lib/utils/webhook-emitter.js +83 -0
- package/lib/web/public/css/styles.css +417 -0
- package/lib/web/public/dark-mode.js +44 -0
- package/lib/web/public/hub/kanban.html +206 -0
- package/lib/web/public/index.html +45 -0
- package/lib/web/public/js/app.js +71 -0
- package/lib/web/public/js/ask.js +110 -0
- package/lib/web/public/js/dashboard.js +165 -0
- package/lib/web/public/js/deploy.js +72 -0
- package/lib/web/public/js/feature.js +79 -0
- package/lib/web/public/js/health.js +65 -0
- package/lib/web/public/js/logs.js +93 -0
- package/lib/web/public/js/review.js +123 -0
- package/lib/web/public/js/ws-client.js +82 -0
- package/lib/web/public/office/css/office.css +678 -0
- package/lib/web/public/office/index.html +148 -0
- package/lib/web/public/office/js/achievements-ui.js +117 -0
- package/lib/web/public/office/js/character.js +1056 -0
- package/lib/web/public/office/js/chat-bubbles.js +177 -0
- package/lib/web/public/office/js/cost-overlay.js +123 -0
- package/lib/web/public/office/js/day-night.js +68 -0
- package/lib/web/public/office/js/effects.js +632 -0
- package/lib/web/public/office/js/engine.js +146 -0
- package/lib/web/public/office/js/feature-ticket.js +216 -0
- package/lib/web/public/office/js/hub-client.js +60 -0
- package/lib/web/public/office/js/main.js +1757 -0
- package/lib/web/public/office/js/office-layout.js +1524 -0
- package/lib/web/public/office/js/pathfinding.js +144 -0
- package/lib/web/public/office/js/pixel-sprites.js +1454 -0
- package/lib/web/public/office/js/progress-bars.js +117 -0
- package/lib/web/public/office/js/replay.js +191 -0
- package/lib/web/public/office/js/sound-effects.js +91 -0
- package/lib/web/public/office/js/sprite-renderer.js +211 -0
- package/lib/web/public/office/js/stamina-system.js +89 -0
- package/lib/web/public/office/js/ui.js +107 -0
- package/lib/web/public/onboarding/index.html +243 -0
- package/lib/web/public/timeline/index.html +195 -0
- package/lib/web/routes/api.js +499 -0
- package/lib/web/routes/logs.js +20 -0
- package/lib/web/routes/metrics.js +99 -0
- package/lib/web/server.js +183 -0
- package/lib/web/ws/handler.js +65 -0
- package/package.json +67 -0
- package/templates/agent-architect.md +69 -0
- package/templates/agent-gemini-pm.md +49 -0
- package/templates/agent-gemini-reviewer.md +52 -0
- package/templates/copilot-instructions.md +36 -0
- package/templates/pipelines/mobile.json +27 -0
- package/templates/pipelines/nodejs-api.json +27 -0
- package/templates/pipelines/python.json +27 -0
- package/templates/pipelines/react.json +27 -0
- package/templates/pipelines/salesforce.json +27 -0
- package/templates/role-gemini.md +97 -0
- package/templates/skill-architect.md +114 -0
- package/templates/skill-browser-qa.md +50 -0
- package/templates/skill-bug-from-qa.md +58 -0
- package/templates/skill-chatbot.md +93 -0
- package/templates/skill-implement.md +78 -0
- package/templates/skill-openclaw.md +174 -0
- package/templates/skill-payment.md +110 -0
- package/templates/skill-pm-spec.md +77 -0
- package/templates/skill-requirement-capture.md +97 -0
- package/templates/skill-review.md +108 -0
- package/templates/skill-reviewer-qa.md +44 -0
- package/templates/skill-suggestion.md +45 -0
- package/templates/skill-template.md +142 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Control Center Telegram Bot — remote pipeline control + notifications.
|
|
3
|
+
*
|
|
4
|
+
* v1.6.67 — Resilient polling architecture inspired by OpenClaw's monitorTelegramProvider.
|
|
5
|
+
*
|
|
6
|
+
* Key improvements over v1.6.66:
|
|
7
|
+
* 1. Fresh Bot instance per restart cycle — no stale HTTP/polling state
|
|
8
|
+
* 2. Exponential backoff with jitter (2s → 30s cap) — prevents API hammering
|
|
9
|
+
* 3. Error classification — recoverable network errors vs fatal bugs
|
|
10
|
+
* 4. Standalone API for notifications — survives bot restarts
|
|
11
|
+
*
|
|
12
|
+
* Setup:
|
|
13
|
+
* 1. Talk to @BotFather on Telegram → /newbot → get token
|
|
14
|
+
* 2. Set env vars (prefix with your project prefix from aicc.config.js):
|
|
15
|
+
* export {PREFIX}_TELEGRAM_TOKEN=your_bot_token
|
|
16
|
+
* export {PREFIX}_TELEGRAM_CHAT_ID=your_chat_id (for push notifications)
|
|
17
|
+
* export {PREFIX}_TELEGRAM_ALLOWED_IDS=id1,id2 (whitelist — comma-separated user/chat IDs)
|
|
18
|
+
* 3. Run: aicc telegram
|
|
19
|
+
*
|
|
20
|
+
* Security:
|
|
21
|
+
* - TELEGRAM_ALLOWED_IDS restricts who can use the bot.
|
|
22
|
+
* - If not set, only TELEGRAM_CHAT_ID is allowed.
|
|
23
|
+
* - If neither is set, the bot runs in "discovery mode" — it only responds
|
|
24
|
+
* to /start (to show the chat ID) and ignores everything else.
|
|
25
|
+
* - Unauthorized users get a single rejection message.
|
|
26
|
+
*
|
|
27
|
+
* To find your chat ID: send /start to the bot, check the logs.
|
|
28
|
+
*/
|
|
29
|
+
import { Api, Bot } from 'grammy';
|
|
30
|
+
import { env, getConfig, loadConfig } from '../config.js';
|
|
31
|
+
import { autoResumePipeline } from '../shared/action-runner.js';
|
|
32
|
+
import { bus } from '../shared/event-bus.js';
|
|
33
|
+
import { registerCommands } from './commands.js';
|
|
34
|
+
import { setupNotifications } from './notifications.js';
|
|
35
|
+
|
|
36
|
+
// ─── Resilience utilities (ported from OpenClaw's monitorTelegramProvider) ─────
|
|
37
|
+
|
|
38
|
+
/** Backoff policy — starts at 2s, caps at 30s, with 25% random jitter */
|
|
39
|
+
const RESTART_POLICY = { initialMs: 2000, maxMs: 30000, factor: 1.8, jitter: 0.25 };
|
|
40
|
+
|
|
41
|
+
/** Exponential backoff with jitter — prevents thundering herd on API recovery */
|
|
42
|
+
function computeBackoff(attempt, policy = RESTART_POLICY) {
|
|
43
|
+
const base = Math.min(policy.initialMs * Math.pow(policy.factor, attempt), policy.maxMs);
|
|
44
|
+
const jitter = base * policy.jitter * (Math.random() * 2 - 1); // ±25%
|
|
45
|
+
return Math.max(500, Math.round(base + jitter));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Classify errors as recoverable (network/transient) vs fatal (code bugs, auth) */
|
|
49
|
+
function isRecoverableError(err) {
|
|
50
|
+
if (!err) return false;
|
|
51
|
+
const msg = String(err?.message || err).toLowerCase();
|
|
52
|
+
const code = err?.code || '';
|
|
53
|
+
|
|
54
|
+
// Recoverable system error codes (network layer)
|
|
55
|
+
const RECOVERABLE_CODES = [
|
|
56
|
+
'ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'ENOTFOUND', 'EAI_AGAIN',
|
|
57
|
+
'EPIPE', 'EHOSTUNREACH', 'ENETUNREACH', 'ERR_SOCKET_CONNECTION_TIMEOUT',
|
|
58
|
+
];
|
|
59
|
+
if (RECOVERABLE_CODES.includes(code)) return true;
|
|
60
|
+
|
|
61
|
+
// Telegram API 409 conflict is always recoverable
|
|
62
|
+
if (err?.error_code === 409) return true;
|
|
63
|
+
|
|
64
|
+
// Recoverable message patterns (network issues, rate limits, server errors)
|
|
65
|
+
const RECOVERABLE_PATTERNS = [
|
|
66
|
+
'network', 'timeout', 'econnreset', 'econnrefused', 'etimedout',
|
|
67
|
+
'socket', 'dns', 'fetch failed', 'abort', 'terminated', 'getaddrinfo',
|
|
68
|
+
'connection', '502 ', '503 ', '429 ', 'too many requests',
|
|
69
|
+
'internal server error', 'bad gateway', 'service unavailable',
|
|
70
|
+
];
|
|
71
|
+
return RECOVERABLE_PATTERNS.some(p => msg.includes(p));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
|
75
|
+
|
|
76
|
+
// ─── Exported for testing ──────────────────────────────────────────────────────
|
|
77
|
+
export { computeBackoff, isRecoverableError, RESTART_POLICY };
|
|
78
|
+
|
|
79
|
+
// ─── Main bot startup ──────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
export async function startBot() {
|
|
82
|
+
// Guard against double execution (ESM can re-evaluate modules)
|
|
83
|
+
const GUARD_KEY = '__aicc_telegram_bot_started__';
|
|
84
|
+
if (globalThis[GUARD_KEY]) return;
|
|
85
|
+
globalThis[GUARD_KEY] = true;
|
|
86
|
+
|
|
87
|
+
// Ensure config + .env are loaded before reading env vars
|
|
88
|
+
await loadConfig().catch(() => {});
|
|
89
|
+
|
|
90
|
+
const BOT_TOKEN = env('TELEGRAM_TOKEN');
|
|
91
|
+
const CHAT_ID = env('TELEGRAM_CHAT_ID');
|
|
92
|
+
const ALLOWED_RAW = env('TELEGRAM_ALLOWED_IDS');
|
|
93
|
+
|
|
94
|
+
if (!BOT_TOKEN) {
|
|
95
|
+
const prefix = (() => { try { return getConfig().envPrefix; } catch { return 'AICC'; } })();
|
|
96
|
+
console.error(`\n Missing ${prefix}_TELEGRAM_TOKEN environment variable.`);
|
|
97
|
+
console.error(' Get one from @BotFather on Telegram.\n');
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Deprecation notice: recommend OpenClaw bridge for Telegram
|
|
102
|
+
try {
|
|
103
|
+
const { existsSync: _exists } = await import('fs');
|
|
104
|
+
const { resolve: _resolve } = await import('path');
|
|
105
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
106
|
+
if (_exists(_resolve(homeDir, '.openclaw', 'openclaw.json'))) {
|
|
107
|
+
console.log(' ⚠️ OpenClaw detected. Consider using the OpenClaw bridge instead of the built-in Telegram bot.');
|
|
108
|
+
console.log(' Set roleplay.openclawBridge: true in aicc.config.js and run "aicc start".');
|
|
109
|
+
console.log(' The bridge provides multi-channel support (Telegram + WhatsApp + Slack + Discord).\n');
|
|
110
|
+
}
|
|
111
|
+
} catch { /* ok */ }
|
|
112
|
+
|
|
113
|
+
// ─── Build whitelist ──────────────────────────────────────────────────────────
|
|
114
|
+
// Accepts user IDs and/or chat IDs (both are numbers in Telegram).
|
|
115
|
+
// Sources: TELEGRAM_ALLOWED_IDS (comma-separated) + TELEGRAM_CHAT_ID
|
|
116
|
+
const allowedIds = new Set();
|
|
117
|
+
|
|
118
|
+
if (ALLOWED_RAW) {
|
|
119
|
+
ALLOWED_RAW.split(',').map(s => s.trim()).filter(Boolean).forEach(id => allowedIds.add(id));
|
|
120
|
+
}
|
|
121
|
+
if (CHAT_ID) {
|
|
122
|
+
allowedIds.add(CHAT_ID);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const isWhitelistConfigured = allowedIds.size > 0;
|
|
126
|
+
|
|
127
|
+
// ─── Standalone API for notifications — survives bot restarts ─────────────────
|
|
128
|
+
// This never gets stale because it's not tied to any polling lifecycle.
|
|
129
|
+
const notifyApi = new Api(BOT_TOKEN);
|
|
130
|
+
|
|
131
|
+
/** Send a notification to the chat — fire-and-forget, never throws */
|
|
132
|
+
function notifyChat(text, opts = {}) {
|
|
133
|
+
if (!CHAT_ID) return;
|
|
134
|
+
notifyApi.sendMessage(CHAT_ID, text, { parse_mode: 'HTML', ...opts }).catch(() => {});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ─── Auth middleware (reusable across fresh bot instances) ─────────────────────
|
|
138
|
+
const authMiddleware = async (ctx, next) => {
|
|
139
|
+
const chatId = String(ctx.chat?.id ?? '');
|
|
140
|
+
const userId = String(ctx.from?.id ?? '');
|
|
141
|
+
const username = ctx.from?.username || 'unknown';
|
|
142
|
+
|
|
143
|
+
// Always allow /start so new users can discover their chat ID
|
|
144
|
+
const isStart = ctx.message?.text?.startsWith('/start') || false;
|
|
145
|
+
|
|
146
|
+
if (!isWhitelistConfigured) {
|
|
147
|
+
// Discovery mode — only /start works
|
|
148
|
+
if (isStart) {
|
|
149
|
+
console.log(` [AUTH] Discovery mode — /start from ${username} (user: ${userId}, chat: ${chatId})`);
|
|
150
|
+
console.log(` [AUTH] To whitelist, set: ${getConfig().envPrefix}_TELEGRAM_ALLOWED_IDS=${userId}`);
|
|
151
|
+
await ctx.reply(
|
|
152
|
+
`Your user ID: \`${userId}\`\nYour chat ID: \`${chatId}\`\n\n` +
|
|
153
|
+
`Add one of these to your env to enable the bot:\n` +
|
|
154
|
+
`\`${getConfig().envPrefix}_TELEGRAM_ALLOWED_IDS=${userId}\``,
|
|
155
|
+
{ parse_mode: 'Markdown' }
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
return; // Block all commands in discovery mode
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Whitelist check — match against user ID or chat ID
|
|
162
|
+
if (allowedIds.has(userId) || allowedIds.has(chatId)) {
|
|
163
|
+
return next(); // Authorized — proceed to command handlers
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Unauthorized
|
|
167
|
+
console.log(` [AUTH] Blocked: ${username} (user: ${userId}, chat: ${chatId})`);
|
|
168
|
+
|
|
169
|
+
if (isStart) {
|
|
170
|
+
await ctx.reply(
|
|
171
|
+
`Access denied.\n\nYour user ID: \`${userId}\`\nContact the bot owner to request access.`,
|
|
172
|
+
{ parse_mode: 'Markdown' }
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
// Silently ignore all other messages from unauthorized users
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// ─── Notifications — set up ONCE using standalone API ─────────────────────────
|
|
179
|
+
// Pass { api: notifyApi } as a proxy — setupNotifications only uses bot.api.*
|
|
180
|
+
// This way notifications survive bot restarts without re-registering bus listeners.
|
|
181
|
+
setupNotifications({ api: notifyApi }, bus, CHAT_ID);
|
|
182
|
+
|
|
183
|
+
// Start watching status.json
|
|
184
|
+
bus.startWatching();
|
|
185
|
+
|
|
186
|
+
// Initialize roleplay room + PM agent (non-fatal)
|
|
187
|
+
try {
|
|
188
|
+
const { isRoleplayEnabled } = await import('../roleplay/roleplay-config.js');
|
|
189
|
+
if (isRoleplayEnabled()) {
|
|
190
|
+
const { initRoom } = await import('../roleplay/room.js');
|
|
191
|
+
const { startPMAgent } = await import('../roleplay/pm-agent.js');
|
|
192
|
+
const room = initRoom(bus);
|
|
193
|
+
startPMAgent(bus, room);
|
|
194
|
+
}
|
|
195
|
+
} catch { /* roleplay optional — non-fatal */ }
|
|
196
|
+
|
|
197
|
+
// ─── Resilient polling loop ──────────────────────────────────────────────────
|
|
198
|
+
let _crashCount = 0;
|
|
199
|
+
let _attempt = 0;
|
|
200
|
+
let _currentBot = null;
|
|
201
|
+
let _triggerRestart = null; // crash signal resolver — accessible by global handlers
|
|
202
|
+
let _running = true;
|
|
203
|
+
let _lastRecoveryNotifyTime = 0; // suppress repeated "back online" messages
|
|
204
|
+
const RECOVERY_NOTIFY_COOLDOWN = 5 * 60 * 1000; // 5 minutes between recovery notifications
|
|
205
|
+
|
|
206
|
+
/** Create a FRESH Bot instance with auth middleware + commands registered.
|
|
207
|
+
* This is the #1 fix — a fresh instance has no stale HTTP state, no broken
|
|
208
|
+
* keep-alive connections, no corrupted polling offset. OpenClaw does this
|
|
209
|
+
* every cycle via createPollingBot(). */
|
|
210
|
+
/** Telegram command menu — shown when user types "/" in the chat */
|
|
211
|
+
const BOT_COMMANDS = [
|
|
212
|
+
{ command: 'feature', description: '✨ Submit a new feature' },
|
|
213
|
+
{ command: 'bug', description: '🐛 Report and fix a bug' },
|
|
214
|
+
{ command: 'review', description: '🔍 Trigger code review' },
|
|
215
|
+
{ command: 'approve', description: '✅ Approve current feature' },
|
|
216
|
+
{ command: 'reject', description: '❌ Reject with reason' },
|
|
217
|
+
{ command: 'implement', description: '⚡ Trigger Coder implementation' },
|
|
218
|
+
{ command: 'deploy', description: '🚀 Deploy to environment' },
|
|
219
|
+
{ command: 'status', description: '📊 Pipeline status' },
|
|
220
|
+
{ command: 'health', description: '🏥 System health check' },
|
|
221
|
+
{ command: 'logs', description: '📋 View session logs' },
|
|
222
|
+
{ command: 'docs', description: '📂 Browse pipeline documents' },
|
|
223
|
+
{ command: 'costs', description: '💰 AI usage & cost summary' },
|
|
224
|
+
{ command: 'leaderboard', description: '🏆 AI model performance' },
|
|
225
|
+
{ command: 'audit', description: '📋 Recent audit log' },
|
|
226
|
+
{ command: 'autopilot', description: '⚡ Toggle auto-pilot mode' },
|
|
227
|
+
{ command: 'reset', description: '🔄 Reset / Abandon feature' },
|
|
228
|
+
{ command: 'cleanup', description: '🧹 Clean up workspace' },
|
|
229
|
+
{ command: 'retry', description: '🔁 Retry from checkpoint' },
|
|
230
|
+
{ command: 'ask', description: '🤖 Ask AI a question' },
|
|
231
|
+
{ command: 'aimode', description: '🧠 View/toggle AI mode' },
|
|
232
|
+
{ command: 'dryrun', description: '🧪 Pipeline dry run' },
|
|
233
|
+
{ command: 'assign', description: '🎯 Assign project URL + goal to AI IT dept' },
|
|
234
|
+
{ command: 'qa', description: '🔬 Run browser QA tests on target website' },
|
|
235
|
+
{ command: 'suggest', description: '💡 AI suggests features based on QA data' },
|
|
236
|
+
{ command: 'threads', description: '💬 View open discussion threads' },
|
|
237
|
+
{ command: 'menu', description: '📱 Show main menu buttons' },
|
|
238
|
+
];
|
|
239
|
+
|
|
240
|
+
function createFreshBot() {
|
|
241
|
+
const bot = new Bot(BOT_TOKEN);
|
|
242
|
+
bot.use(authMiddleware);
|
|
243
|
+
registerCommands(bot);
|
|
244
|
+
// Register command menu with Telegram — shows suggestions when user types "/"
|
|
245
|
+
bot.api.setMyCommands(BOT_COMMANDS).catch(err =>
|
|
246
|
+
console.error(` [Bot] Failed to set command menu: ${err.message}`)
|
|
247
|
+
);
|
|
248
|
+
return bot;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function resilientLoop() {
|
|
252
|
+
while (_running) {
|
|
253
|
+
// ── Create FRESH bot each cycle — no stale HTTP/polling state ──────────
|
|
254
|
+
const bot = createFreshBot();
|
|
255
|
+
_currentBot = bot;
|
|
256
|
+
|
|
257
|
+
// Crash signal — resolved when bot.catch() or global error handlers fire.
|
|
258
|
+
// The Promise.race below picks up either clean-stop or crash.
|
|
259
|
+
const crashSignal = new Promise(resolve => { _triggerRestart = resolve; });
|
|
260
|
+
|
|
261
|
+
bot.catch((err) => {
|
|
262
|
+
const e = err.error || err;
|
|
263
|
+
_crashCount++;
|
|
264
|
+
console.error(` [Bot] Polling/middleware error: ${e.message || e}`);
|
|
265
|
+
try { bot.stop(); } catch {}
|
|
266
|
+
if (_triggerRestart) _triggerRestart(e);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Clear any stale webhook before starting — prevents 409 conflicts
|
|
270
|
+
try { await bot.api.deleteWebhook({ drop_pending_updates: false }); } catch {}
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
const result = await Promise.race([
|
|
274
|
+
bot.start({
|
|
275
|
+
onStart: (info) => {
|
|
276
|
+
const wasRestarted = _crashCount > 0;
|
|
277
|
+
if (wasRestarted) _attempt = 0; // reset backoff on successful reconnect
|
|
278
|
+
_crashCount = 0;
|
|
279
|
+
|
|
280
|
+
console.log(`\n ${getConfig().name} Telegram Bot`);
|
|
281
|
+
console.log(` @${info.username}`);
|
|
282
|
+
if (isWhitelistConfigured) {
|
|
283
|
+
console.log(` Whitelist: ${[...allowedIds].join(', ')}`);
|
|
284
|
+
} else {
|
|
285
|
+
console.log(` WARNING: No whitelist configured — discovery mode only`);
|
|
286
|
+
console.log(` Set ${getConfig().envPrefix}_TELEGRAM_ALLOWED_IDS to enable commands`);
|
|
287
|
+
}
|
|
288
|
+
if (CHAT_ID) console.log(` Push notifications → chat ${CHAT_ID}`);
|
|
289
|
+
console.log('');
|
|
290
|
+
|
|
291
|
+
// Notify user when bot recovers from a crash (with cooldown to prevent spam)
|
|
292
|
+
if (wasRestarted && Date.now() - _lastRecoveryNotifyTime > RECOVERY_NOTIFY_COOLDOWN) {
|
|
293
|
+
_lastRecoveryNotifyTime = Date.now();
|
|
294
|
+
const s = bus.getStatus();
|
|
295
|
+
const pipelineInfo = s?.current_feature
|
|
296
|
+
? `\nPipeline: <code>${s.current_feature}</code> at stage <b>${s.stage}</b>`
|
|
297
|
+
: '';
|
|
298
|
+
notifyChat(`🟢 Bot is back online after crash recovery.${pipelineInfo}`);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Resume any auto pipeline that was interrupted by a restart
|
|
302
|
+
setTimeout(() => autoResumePipeline().catch(err =>
|
|
303
|
+
console.error('[Pipeline] Resume error:', err.message)), 3000);
|
|
304
|
+
},
|
|
305
|
+
}).then(() => ({ type: 'clean-stop' })),
|
|
306
|
+
crashSignal.then(e => ({ type: 'crash', error: e })),
|
|
307
|
+
]);
|
|
308
|
+
|
|
309
|
+
if (result.type === 'clean-stop') {
|
|
310
|
+
// Graceful stop (SIGINT) — exit loop
|
|
311
|
+
console.log(' [Bot] Graceful stop.');
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ── Handle crash — compute backoff and retry ────────────────────────
|
|
316
|
+
const err = result.error;
|
|
317
|
+
_attempt++;
|
|
318
|
+
|
|
319
|
+
// 409 = another instance polling — must wait for Telegram's 30s timeout
|
|
320
|
+
if (err?.error_code === 409) {
|
|
321
|
+
const delay = 35000;
|
|
322
|
+
_crashCount = 0; // 409 is not a real crash — reset so recovery notification doesn't fire
|
|
323
|
+
console.log(` [Bot] Conflict (409) — waiting 35s for Telegram to release... (attempt ${_attempt})`);
|
|
324
|
+
await sleep(delay);
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Classify error and compute appropriate delay
|
|
329
|
+
const recoverable = isRecoverableError(err);
|
|
330
|
+
const delay = recoverable ? computeBackoff(_attempt) : 30000;
|
|
331
|
+
const label = recoverable ? 'Recoverable' : 'Fatal';
|
|
332
|
+
console.log(` [Bot] ${label} error — restarting with fresh instance in ${(delay / 1000).toFixed(1)}s (attempt ${_attempt})...`);
|
|
333
|
+
// Only notify on first error or after cooldown — prevent spam during crash loops
|
|
334
|
+
if (_attempt <= 1 || Date.now() - _lastRecoveryNotifyTime > RECOVERY_NOTIFY_COOLDOWN) {
|
|
335
|
+
notifyChat(`${recoverable ? '⚠️' : '🚨'} Bot ${label.toLowerCase()} error: ${err?.message || 'unknown'}\nRestarting in ${(delay / 1000).toFixed(1)}s (attempt ${_attempt})...`);
|
|
336
|
+
}
|
|
337
|
+
await sleep(delay);
|
|
338
|
+
|
|
339
|
+
} catch (err) {
|
|
340
|
+
// Startup-phase error (before polling begins)
|
|
341
|
+
_crashCount++;
|
|
342
|
+
_attempt++;
|
|
343
|
+
|
|
344
|
+
if (err.error_code === 409) {
|
|
345
|
+
_crashCount = 0; // 409 is not a real crash — reset so recovery notification doesn't fire
|
|
346
|
+
console.log(` [Bot] Conflict (409) at startup — waiting 35s...`);
|
|
347
|
+
await sleep(35000);
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const recoverable = isRecoverableError(err);
|
|
352
|
+
const delay = recoverable ? computeBackoff(_attempt) : 30000;
|
|
353
|
+
console.error(` [Bot] Startup error: ${err.message}`);
|
|
354
|
+
console.log(` [Bot] Restarting in ${(delay / 1000).toFixed(1)}s (attempt ${_attempt})...`);
|
|
355
|
+
notifyChat(`🚨 Bot startup error: ${err.message}\nRestarting in ${(delay / 1000).toFixed(1)}s...`);
|
|
356
|
+
await sleep(delay);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ─── Global error handlers — force restart via crash signal ──────────────────
|
|
362
|
+
// These catch errors that escape grammy's error boundary (unhandled promises,
|
|
363
|
+
// uncaught exceptions from callbacks, etc.) and feed them into the resilient
|
|
364
|
+
// loop instead of letting the process die silently.
|
|
365
|
+
|
|
366
|
+
process.on('uncaughtException', (err) => {
|
|
367
|
+
console.error(' [Bot] Uncaught exception:', err.message);
|
|
368
|
+
_crashCount++;
|
|
369
|
+
notifyChat(`🚨 Uncaught exception: ${err.message}\nForcing restart with fresh bot instance...`);
|
|
370
|
+
if (_currentBot) try { _currentBot.stop(); } catch {}
|
|
371
|
+
if (_triggerRestart) _triggerRestart(err);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
process.on('unhandledRejection', (err) => {
|
|
375
|
+
const msg = err?.message || String(err);
|
|
376
|
+
if (isRecoverableError(err)) {
|
|
377
|
+
// Network/transient error — restart with backoff
|
|
378
|
+
console.error(' [Bot] Unhandled rejection (recoverable):', msg);
|
|
379
|
+
_crashCount++;
|
|
380
|
+
if (_currentBot) try { _currentBot.stop(); } catch {}
|
|
381
|
+
if (_triggerRestart) _triggerRestart(err);
|
|
382
|
+
} else {
|
|
383
|
+
// Non-network rejection — log but don't restart (might be a code bug
|
|
384
|
+
// that would cause infinite restart loops)
|
|
385
|
+
console.error(' [Bot] Unhandled rejection (non-recoverable, not restarting):', msg);
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// ─── Signal handling ──────────────────────────────────────────────────────────
|
|
390
|
+
|
|
391
|
+
// Ignore SIGHUP — prevents instant death when terminal disconnects
|
|
392
|
+
// (SSH timeout, terminal tab close, macOS sleep, etc.)
|
|
393
|
+
process.on('SIGHUP', () => {
|
|
394
|
+
console.log(' [Bot] SIGHUP received — ignoring (terminal disconnect).');
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// Cleanup on explicit interrupt
|
|
398
|
+
process.on('SIGINT', () => {
|
|
399
|
+
_running = false;
|
|
400
|
+
bus.stopWatching();
|
|
401
|
+
if (_currentBot) _currentBot.stop();
|
|
402
|
+
process.exit(0);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
// ─── Start the resilient loop ─────────────────────────────────────────────────
|
|
406
|
+
resilientLoop().catch(err => {
|
|
407
|
+
console.error(' [Bot] Resilience loop fatal crash:', err.message);
|
|
408
|
+
notifyChat(`💀 Bot resilience loop crashed: ${err.message}`);
|
|
409
|
+
process.exit(1);
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Auto-start when run directly (node bot.js), not when imported via aicc CLI
|
|
414
|
+
if (process.argv[1]?.endsWith('bot.js')) {
|
|
415
|
+
startBot();
|
|
416
|
+
}
|