@openlife/cli 1.7.12 → 1.7.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +98 -2
- package/dist/orchestrator/Brain.js +40 -1
- package/dist/orchestrator/Gateway.js +49 -9
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2825,6 +2825,91 @@ profileCmd.command('import <file>').description('Import a profile from a JSON fi
|
|
|
2825
2825
|
process.exitCode = 1;
|
|
2826
2826
|
}
|
|
2827
2827
|
});
|
|
2828
|
+
// `openlife telegram` — manage Telegram delivery mode (polling vs webhook)
|
|
2829
|
+
const telegramCmd = program
|
|
2830
|
+
.command('telegram')
|
|
2831
|
+
.description('Gerencia o modo de entrega do Telegram (polling vs webhook)');
|
|
2832
|
+
telegramCmd
|
|
2833
|
+
.command('status')
|
|
2834
|
+
.description('Mostra modo atual + webhook info')
|
|
2835
|
+
.action(async () => {
|
|
2836
|
+
const token = (process.env.TELEGRAM_BOT_TOKEN || '').trim();
|
|
2837
|
+
if (!token) {
|
|
2838
|
+
console.log(JSON.stringify({ ok: false, error: 'TELEGRAM_BOT_TOKEN ausente' }, null, 2));
|
|
2839
|
+
return;
|
|
2840
|
+
}
|
|
2841
|
+
const { execFileSync } = require('child_process');
|
|
2842
|
+
try {
|
|
2843
|
+
const raw = String(execFileSync('curl', ['-sS', '--max-time', '8', `https://api.telegram.org/bot${token}/getWebhookInfo`], { encoding: 'utf-8' })).trim();
|
|
2844
|
+
const j = JSON.parse(raw);
|
|
2845
|
+
const info = (j && j.result) || {};
|
|
2846
|
+
const mode = info.url ? 'webhook' : 'polling';
|
|
2847
|
+
const onRailway = !!(process.env.RAILWAY_ENVIRONMENT || process.env.RAILWAY_PUBLIC_DOMAIN || process.env.RAILWAY_STATIC_URL);
|
|
2848
|
+
const configured = String(process.env.OPENLIFE_TELEGRAM_MODE || 'auto').toLowerCase();
|
|
2849
|
+
console.log(JSON.stringify({
|
|
2850
|
+
ok: true,
|
|
2851
|
+
mode_telegram_thinks: mode,
|
|
2852
|
+
mode_configured: configured,
|
|
2853
|
+
railway_detected: onRailway,
|
|
2854
|
+
webhook_url: info.url || null,
|
|
2855
|
+
pending_updates: info.pending_update_count || 0,
|
|
2856
|
+
last_error: info.last_error_message || null,
|
|
2857
|
+
}, null, 2));
|
|
2858
|
+
}
|
|
2859
|
+
catch (e) {
|
|
2860
|
+
console.log(JSON.stringify({ ok: false, error: 'getWebhookInfo_failed', detail: e.message }, null, 2));
|
|
2861
|
+
process.exitCode = 1;
|
|
2862
|
+
}
|
|
2863
|
+
});
|
|
2864
|
+
telegramCmd
|
|
2865
|
+
.command('webhook-set <domain>')
|
|
2866
|
+
.description('Configura webhook (uso típico Railway). Ex: openlife telegram webhook-set https://app.up.railway.app')
|
|
2867
|
+
.option('--path <path>', 'Caminho do webhook (default: /api/v1/telegram/webhook)', '/api/v1/telegram/webhook')
|
|
2868
|
+
.action(async (domain, opts) => {
|
|
2869
|
+
const token = (process.env.TELEGRAM_BOT_TOKEN || '').trim();
|
|
2870
|
+
if (!token) {
|
|
2871
|
+
console.error('TELEGRAM_BOT_TOKEN ausente em .env');
|
|
2872
|
+
process.exitCode = 1;
|
|
2873
|
+
return;
|
|
2874
|
+
}
|
|
2875
|
+
const fullDomain = domain.startsWith('http') ? domain : `https://${domain}`;
|
|
2876
|
+
const url = `${fullDomain.replace(/\/$/, '')}${opts.path}`;
|
|
2877
|
+
const { execFileSync } = require('child_process');
|
|
2878
|
+
try {
|
|
2879
|
+
const raw = String(execFileSync('curl', ['-sS', '--max-time', '8', '-X', 'POST', '-d', `url=${url}`, `https://api.telegram.org/bot${token}/setWebhook`], { encoding: 'utf-8' })).trim();
|
|
2880
|
+
const j = JSON.parse(raw);
|
|
2881
|
+
console.log(JSON.stringify({ ok: !!j.ok, url, telegram_response: j }, null, 2));
|
|
2882
|
+
if (!j.ok)
|
|
2883
|
+
process.exitCode = 1;
|
|
2884
|
+
}
|
|
2885
|
+
catch (e) {
|
|
2886
|
+
console.error('webhook-set falhou:', e.message);
|
|
2887
|
+
process.exitCode = 1;
|
|
2888
|
+
}
|
|
2889
|
+
});
|
|
2890
|
+
telegramCmd
|
|
2891
|
+
.command('webhook-clear')
|
|
2892
|
+
.description('Remove webhook (volta para polling)')
|
|
2893
|
+
.action(async () => {
|
|
2894
|
+
const token = (process.env.TELEGRAM_BOT_TOKEN || '').trim();
|
|
2895
|
+
if (!token) {
|
|
2896
|
+
console.error('TELEGRAM_BOT_TOKEN ausente');
|
|
2897
|
+
process.exitCode = 1;
|
|
2898
|
+
return;
|
|
2899
|
+
}
|
|
2900
|
+
const { execFileSync } = require('child_process');
|
|
2901
|
+
try {
|
|
2902
|
+
const raw = String(execFileSync('curl', ['-sS', '--max-time', '8', '-X', 'POST', `https://api.telegram.org/bot${token}/deleteWebhook`], { encoding: 'utf-8' })).trim();
|
|
2903
|
+
const j = JSON.parse(raw);
|
|
2904
|
+
console.log(JSON.stringify({ ok: !!j.ok, telegram_response: j }, null, 2));
|
|
2905
|
+
if (!j.ok)
|
|
2906
|
+
process.exitCode = 1;
|
|
2907
|
+
}
|
|
2908
|
+
catch (e) {
|
|
2909
|
+
console.error('webhook-clear falhou:', e.message);
|
|
2910
|
+
process.exitCode = 1;
|
|
2911
|
+
}
|
|
2912
|
+
});
|
|
2828
2913
|
// `openlife config` — interactive configuration TUI (provider, keys, telegram, voice, daemon)
|
|
2829
2914
|
program
|
|
2830
2915
|
.command('config')
|
|
@@ -2838,8 +2923,19 @@ program
|
|
|
2838
2923
|
program
|
|
2839
2924
|
.command('chat')
|
|
2840
2925
|
.description('Inicia o chat interativo no terminal (Matrix-themed REPL)')
|
|
2841
|
-
.
|
|
2842
|
-
|
|
2926
|
+
.option('--test', 'executa smoke não-interativo do chat e sai')
|
|
2927
|
+
.action(async (opts) => {
|
|
2928
|
+
const { runChat, runChatSmoke } = require('./cli/ChatTui');
|
|
2929
|
+
if (opts.test) {
|
|
2930
|
+
const result = runChatSmoke({ cwd: process.cwd() });
|
|
2931
|
+
console.log(`${result.marker}: ${result.detail}`);
|
|
2932
|
+
console.log(`sessionId=${result.sessionId}`);
|
|
2933
|
+
console.log(`model=${result.model}`);
|
|
2934
|
+
console.log(`cwd=${result.cwd}`);
|
|
2935
|
+
if (!result.ok)
|
|
2936
|
+
process.exitCode = 1;
|
|
2937
|
+
return;
|
|
2938
|
+
}
|
|
2843
2939
|
await runChat();
|
|
2844
2940
|
});
|
|
2845
2941
|
// Bare invocation (no subcommand + interactive TTY) → launch chat TUI.
|
|
@@ -288,7 +288,46 @@ class Brain {
|
|
|
288
288
|
const commandArgs = process.platform === 'win32'
|
|
289
289
|
? args
|
|
290
290
|
: ['-k', '2s', `${timeoutSeconds}s`, 'codex', ...args];
|
|
291
|
-
|
|
291
|
+
// Codex CLI reads stdin when invoked without a TTY. The promisified
|
|
292
|
+
// execFile doesn't expose stdio, so it keeps the child's stdin
|
|
293
|
+
// attached to our (open) pipe — codex then waits forever for input
|
|
294
|
+
// and our outer timeout fires. Use spawn with `stdio: ['ignore', ...]`
|
|
295
|
+
// to close stdin explicitly. Fixes ~30s phantom timeouts when the
|
|
296
|
+
// daemon (non-TTY) calls codex.
|
|
297
|
+
const stdout = await new Promise((resolve, reject) => {
|
|
298
|
+
const child = child_process.spawn(command, commandArgs, {
|
|
299
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
300
|
+
});
|
|
301
|
+
let stdoutBuf = '';
|
|
302
|
+
let stderrBuf = '';
|
|
303
|
+
let killed = false;
|
|
304
|
+
const watchdog = setTimeout(() => {
|
|
305
|
+
killed = true;
|
|
306
|
+
try {
|
|
307
|
+
child.kill('SIGKILL');
|
|
308
|
+
}
|
|
309
|
+
catch { /* ignore */ }
|
|
310
|
+
}, timeoutMs + 3000);
|
|
311
|
+
child.stdout.on('data', (d) => { stdoutBuf += String(d); });
|
|
312
|
+
child.stderr.on('data', (d) => { stderrBuf += String(d); });
|
|
313
|
+
child.on('error', (err) => { clearTimeout(watchdog); reject(err); });
|
|
314
|
+
child.on('close', (code, signal) => {
|
|
315
|
+
clearTimeout(watchdog);
|
|
316
|
+
if (killed || code === 124 || (signal && killed)) {
|
|
317
|
+
const e = new Error('ETIMEDOUT');
|
|
318
|
+
e.code = 'ETIMEDOUT';
|
|
319
|
+
reject(e);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (code !== 0) {
|
|
323
|
+
const e = new Error(`codex exited with code ${code}: ${stderrBuf.slice(-300)}`);
|
|
324
|
+
e.code = String(code);
|
|
325
|
+
reject(e);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
resolve(stdoutBuf);
|
|
329
|
+
});
|
|
330
|
+
});
|
|
292
331
|
const lastMessage = fs.existsSync(outputFile) ? fs.readFileSync(outputFile, 'utf-8').trim() : '';
|
|
293
332
|
try {
|
|
294
333
|
fs.unlinkSync(outputFile);
|
|
@@ -344,16 +344,56 @@ class Gateway {
|
|
|
344
344
|
await ctx.reply("Erro no módulo de visão ao baixar a imagem.");
|
|
345
345
|
}
|
|
346
346
|
});
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
347
|
+
// Decide Telegram delivery mode:
|
|
348
|
+
// - OPENLIFE_TELEGRAM_MODE=polling | webhook | auto (default: auto)
|
|
349
|
+
// - "auto" → webhook if a Railway env var is present, else polling.
|
|
350
|
+
// This is the simplest UX for the "Railway + local" topology: Railway
|
|
351
|
+
// deploys boot in webhook mode (no polling conflict with local), local
|
|
352
|
+
// dev boots in polling mode (no webhook URL needed). User changes
|
|
353
|
+
// nothing.
|
|
354
|
+
const mode = String(process.env.OPENLIFE_TELEGRAM_MODE || 'auto').toLowerCase();
|
|
355
|
+
const onRailway = !!(process.env.RAILWAY_ENVIRONMENT || process.env.RAILWAY_PUBLIC_DOMAIN || process.env.RAILWAY_STATIC_URL);
|
|
356
|
+
const useWebhook = mode === 'webhook' || (mode === 'auto' && onRailway);
|
|
357
|
+
if (useWebhook) {
|
|
358
|
+
const webhookPath = process.env.OPENLIFE_WEBHOOK_PATH || '/api/v1/telegram/webhook';
|
|
359
|
+
// Register webhook handler on the existing Express app — no separate
|
|
360
|
+
// HTTP server, no port conflict. Telegraf's webhookCallback returns
|
|
361
|
+
// an Express-compatible middleware that parses updates.
|
|
362
|
+
this.app.use(webhookPath, this.bot.webhookCallback(webhookPath));
|
|
363
|
+
const rawDomain = process.env.OPENLIFE_WEBHOOK_DOMAIN
|
|
364
|
+
|| process.env.RAILWAY_PUBLIC_DOMAIN
|
|
365
|
+
|| process.env.RAILWAY_STATIC_URL
|
|
366
|
+
|| '';
|
|
367
|
+
if (rawDomain) {
|
|
368
|
+
const domain = rawDomain.startsWith('http') ? rawDomain : `https://${rawDomain}`;
|
|
369
|
+
const fullUrl = `${domain.replace(/\/$/, '')}${webhookPath}`;
|
|
370
|
+
this.bot.telegram.setWebhook(fullUrl).then(() => {
|
|
371
|
+
console.log(`[GATEWAY] Telegram em modo WEBHOOK @ ${fullUrl}`);
|
|
372
|
+
}).catch((err) => {
|
|
373
|
+
console.error('[GATEWAY] setWebhook falhou:', err instanceof Error ? err.message : String(err));
|
|
374
|
+
});
|
|
352
375
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
376
|
+
else {
|
|
377
|
+
console.warn('[GATEWAY] Webhook mode ativado mas OPENLIFE_WEBHOOK_DOMAIN (ou RAILWAY_PUBLIC_DOMAIN) não está definido. Telegram não saberá onde entregar mensagens. Defina o domínio público antes de subir.');
|
|
378
|
+
}
|
|
379
|
+
console.log("[GATEWAY] Telegram Bot pronto (modo webhook — sem polling).");
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
// Default polling mode (local dev, single-instance bots).
|
|
383
|
+
// Clear any previously-registered webhook before polling, otherwise
|
|
384
|
+
// Telegram rejects getUpdates with 409. Best-effort, non-fatal.
|
|
385
|
+
this.bot.telegram.deleteWebhook({ drop_pending_updates: false }).catch(() => { });
|
|
386
|
+
this.bot.launch().catch((err) => {
|
|
387
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
388
|
+
if (msg.includes('409') || msg.includes('terminated by other getUpdates request')) {
|
|
389
|
+
console.error('[GATEWAY] Telegram 409 Conflict: outra instância está conectada com este bot. Mantendo processo vivo para healthcheck; Telegram polling fica desativado nesta instância.');
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
console.error('[GATEWAY] Falha ao iniciar Telegram bot:', err);
|
|
393
|
+
process.exit(1);
|
|
394
|
+
});
|
|
395
|
+
console.log("[GATEWAY] Telegram Bot escutando (modo polling) com suporte Multimodal (Voz/Visão).");
|
|
396
|
+
}
|
|
357
397
|
}
|
|
358
398
|
async shutdown(reason = 'manual') {
|
|
359
399
|
if (this.isShuttingDown)
|