@foxden-app/foxclaw 0.3.18 → 0.4.0

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.
Files changed (39) hide show
  1. package/.env.example +8 -3
  2. package/README.md +17 -7
  3. package/README_EN.md +17 -7
  4. package/dist/auth/mirror.d.ts +38 -0
  5. package/dist/auth/mirror.js +259 -0
  6. package/dist/codex_app/client.d.ts +4 -1
  7. package/dist/codex_app/client.js +23 -4
  8. package/dist/config.d.ts +7 -0
  9. package/dist/config.js +18 -1
  10. package/dist/controller/controller.d.ts +24 -2
  11. package/dist/controller/controller.js +161 -35
  12. package/dist/core/bridge_scope.d.ts +5 -2
  13. package/dist/core/bridge_scope.js +7 -3
  14. package/dist/i18n.d.ts +20 -2
  15. package/dist/i18n.js +20 -2
  16. package/dist/main.js +198 -10
  17. package/dist/store/database.d.ts +4 -2
  18. package/dist/store/database.js +42 -6
  19. package/dist/telegram/addressing.d.ts +1 -0
  20. package/dist/telegram/addressing.js +3 -0
  21. package/dist/telegram/gateway.d.ts +4 -1
  22. package/dist/telegram/gateway.js +23 -5
  23. package/dist/types.d.ts +26 -0
  24. package/dist/update.d.ts +5 -0
  25. package/dist/update.js +93 -5
  26. package/docs/agent-assisted-install.md +7 -6
  27. package/docs/install-for-beginners.md +12 -4
  28. package/docs/troubleshooting.md +13 -1
  29. package/docs/user-manual.md +12 -12
  30. package/docs/zh/agent-assisted-install.md +7 -6
  31. package/docs/zh/foxclaw-skill.md +4 -2
  32. package/docs/zh/install-for-beginners.md +12 -4
  33. package/docs/zh/troubleshooting.md +13 -1
  34. package/docs/zh/user-manual.md +12 -12
  35. package/package.json +1 -1
  36. package/skills/foxclaw/SKILL.md +28 -20
  37. package/skills/foxclaw/references/telegram-setup.md +9 -6
  38. package/skills/foxclaw/scripts/bootstrap_host.py +11 -8
  39. package/skills/foxclaw/scripts/bootstrap_remote.py +8 -4
package/dist/i18n.js CHANGED
@@ -116,18 +116,27 @@ const MESSAGES = {
116
116
  status_codex_local_usage_unavailable: 'Codex local history: unavailable ({error})',
117
117
  status_codex_credits: 'Codex credits: {value}',
118
118
  status_codex_limit_reached: 'Codex limit: {value}',
119
+ status_runtime_overview: 'Telegram bot runtimes:',
120
+ status_runtime_bot: '- {bot}: connected {connected}, auth {auth}, active turns {turns}',
121
+ status_runtime_weixin: '- Weixin default runtime: connected {connected}, active turns {turns}',
122
+ status_auth_mirror_none: 'Last auth mirror: none recorded',
123
+ status_auth_mirror_synced: 'Last auth mirror: {candidate} from {source} at {time}',
124
+ status_last_update_none: 'Last service update: none recorded',
125
+ status_last_update: 'Last service update: {from} -> {to} at {time}',
126
+ status_last_codex_update: 'Last Codex update: {value}',
119
127
  update_started: 'FoxClaw update started. I will report here after installation, checks, and service restart complete.',
120
128
  update_succeeded: 'FoxClaw updated and restarted: {from} -> {to}.',
121
129
  update_failed: 'FoxClaw update failed: {error}\nRun foxclaw update in a terminal for details.',
122
130
  update_unavailable: 'Self-update is unavailable in this runtime. Run foxclaw update in a terminal.',
123
131
  update_already_running: 'A FoxClaw update is already running. I will report here when it finishes.',
124
- update_blocked_active: 'Cannot update FoxClaw while a turn, approval, or question is active. Wait or use /interrupt first.',
132
+ update_blocked_active: 'Cannot update FoxClaw while a runtime activity or auth mirror write is active. Wait or use /interrupt first.',
125
133
  auth_reload_restarting: 'Restarting Codex app-server to reload auth...',
126
134
  auth_reload_done: 'Codex app-server restarted. Current auth has been reloaded.',
127
135
  auth_reload_blocked_active: 'Cannot reload Codex auth while a turn, approval, or question is active. Wait or use /interrupt first.',
128
136
  usage_auth: 'Usage: /auth [list|use <n>|enable <n>|disable <n>|reload|add <name>]',
129
137
  usage_auth_add: 'Usage: /auth add <name>. Use letters, numbers, dot, dash, or underscore.',
130
138
  auth_list_title: 'Codex auth files:',
139
+ auth_bot: 'Bot runtime: {value}',
131
140
  auth_current: 'Current auth: {value}',
132
141
  auth_dir: 'Auth dir: {value}',
133
142
  auth_candidate_count: 'Candidates: {value}',
@@ -680,18 +689,27 @@ const MESSAGES = {
680
689
  status_codex_local_usage_unavailable: 'Codex 本地历史:无法获取({error})',
681
690
  status_codex_credits: 'Codex 额度:{value}',
682
691
  status_codex_limit_reached: 'Codex 限制:{value}',
692
+ status_runtime_overview: 'Telegram bot 运行时:',
693
+ status_runtime_bot: '- {bot}:连接 {connected},auth {auth},进行中回复 {turns}',
694
+ status_runtime_weixin: '- 微信默认运行时:连接 {connected},进行中回复 {turns}',
695
+ status_auth_mirror_none: '最近 auth 镜像:暂无记录',
696
+ status_auth_mirror_synced: '最近 auth 镜像:{candidate} 由 {source} 于 {time} 同步',
697
+ status_last_update_none: '最近服务升级:暂无记录',
698
+ status_last_update: '最近服务升级:{from} -> {to}({time})',
699
+ status_last_codex_update: '最近 Codex 升级:{value}',
683
700
  update_started: '已开始升级 FoxClaw。安装、自检和服务重启完成后,我会在这里回报结果。',
684
701
  update_succeeded: 'FoxClaw 已升级并重启:{from} -> {to}。',
685
702
  update_failed: 'FoxClaw 升级失败:{error}\n请在终端运行 foxclaw update 查看详情。',
686
703
  update_unavailable: '当前运行方式不支持自升级,请在终端运行 foxclaw update。',
687
704
  update_already_running: 'FoxClaw 升级已经在进行中,结束后我会在这里回报结果。',
688
- update_blocked_active: '当前有回复、审批或问题在进行中,不能升级 FoxClaw。请先等待,或使用 /interrupt。',
705
+ update_blocked_active: '当前有 runtime 操作或 auth 镜像写入在进行中,不能升级 FoxClaw。请先等待,或使用 /interrupt。',
689
706
  auth_reload_restarting: '正在重启 Codex app-server 以重新读取 auth...',
690
707
  auth_reload_done: 'Codex app-server 已重启,当前 auth 已重新读取。',
691
708
  auth_reload_blocked_active: '当前有回复、审批或问题在进行中,不能重载 Codex auth。请先等待,或使用 /interrupt。',
692
709
  usage_auth: '用法:/auth [list|use <编号>|enable <编号>|disable <编号>|reload|add <名称>]',
693
710
  usage_auth_add: '用法:/auth add <名称>。名称只能包含字母、数字、点、短横线或下划线。',
694
711
  auth_list_title: 'Codex auth 文件:',
712
+ auth_bot: 'Bot runtime:{value}',
695
713
  auth_current: '当前 auth:{value}',
696
714
  auth_dir: 'Auth 目录:{value}',
697
715
  auth_candidate_count: '候选数量:{value}',
package/dist/main.js CHANGED
@@ -1,15 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from 'node:fs';
3
+ import os from 'node:os';
3
4
  import path from 'node:path';
4
5
  import process from 'node:process';
5
6
  import { createInterface } from 'node:readline/promises';
6
7
  import { spawnSync } from 'node:child_process';
7
8
  import { fileURLToPath } from 'node:url';
8
- import { APP_HOME, DEFAULT_ENV_PATH, DEFAULT_LOG_PATH, DEFAULT_STATUS_PATH, getLoadedEnvPath, loadConfig, loadEnv, } from './config.js';
9
+ import { APP_HOME, DEFAULT_CODEX_TELEGRAM_HOME, DEFAULT_ENV_PATH, DEFAULT_LOG_PATH, DEFAULT_STATUS_PATH, getLoadedEnvPath, loadConfig, loadEnv, } from './config.js';
9
10
  import { acquireProcessLock, LockHeldError } from './lock.js';
10
11
  import { readRuntimeStatus, writeRuntimeStatus } from './runtime.js';
11
12
  import { refreshFoxclawExecStartDropIns, removeFoxclawExecStartDropIns } from './systemd.js';
12
- import { createSelfUpdateRuntime, inferPnpmHomeFromEntryPoint, performSelfUpdate } from './update.js';
13
+ import { createSelfUpdateRuntime, inferPnpmHomeFromEntryPoint, performSelfUpdate, readSelfUpdateStatus, writeSelfUpdateStatus, } from './update.js';
13
14
  const rawCommand = process.argv[2];
14
15
  const command = rawCommand || 'serve';
15
16
  loadEnv();
@@ -61,6 +62,9 @@ async function main() {
61
62
  entryPoint,
62
63
  nodePath: process.execPath,
63
64
  version: readPackageVersion(),
65
+ ...(process.env.CODEX_CLI_BIN || resolveCommand('codex')
66
+ ? { codexCliBin: process.env.CODEX_CLI_BIN || resolveCommand('codex') }
67
+ : {}),
64
68
  ...(notificationFile ? { notificationFile } : {}),
65
69
  };
66
70
  const outcome = performSelfUpdate(options);
@@ -137,7 +141,7 @@ Usage:
137
141
  foxclaw --help`);
138
142
  }
139
143
  async function runServeCli() {
140
- const [{ BridgeMessagingRouter }, { TelegramMessagingPort }, { WeixinChannelAdapter }, { WeixinMessagingPort }, { attachIlinkRuntimeFromBridgeLogger }, { loadWeixinAccount }, { Logger }, { BridgeStore }, { TelegramGateway }, { CodexAppClient }, { BridgeSessionCore }, { TelegramChannelAdapter },] = await Promise.all([
144
+ const [{ BridgeMessagingRouter }, { TelegramMessagingPort }, { WeixinChannelAdapter }, { WeixinMessagingPort }, { attachIlinkRuntimeFromBridgeLogger }, { loadWeixinAccount }, { Logger }, { BridgeStore }, { TelegramGateway }, { CodexAppClient }, { BridgeSessionCore }, { TelegramChannelAdapter }, { AuthCandidateMirror },] = await Promise.all([
141
145
  import('./channels/bridge_messaging_router.js'),
142
146
  import('./channels/telegram/telegram_messaging_port.js'),
143
147
  import('./channels/weixin/weixin_channel_adapter.js'),
@@ -150,6 +154,7 @@ async function runServeCli() {
150
154
  import('./codex_app/client.js'),
151
155
  import('./controller/controller.js'),
152
156
  import('./channels/telegram/telegram_channel_adapter.js'),
157
+ import('./auth/mirror.js'),
153
158
  ]);
154
159
  const config = loadConfig();
155
160
  const logger = new Logger(config.logLevel, config.logPath);
@@ -157,8 +162,184 @@ async function runServeCli() {
157
162
  const processLock = acquireProcessLock(config.lockPath);
158
163
  let store = null;
159
164
  let weixinAdapter = null;
165
+ let activeWeixinCore = null;
166
+ let activeTelegramAdapters = [];
167
+ let managedApps = [];
168
+ let activeAuthMirror = null;
160
169
  try {
161
170
  store = new BridgeStore(config.storePath);
171
+ if (config.tgMultiBotMode) {
172
+ const seeds = [];
173
+ for (const token of config.tgBotTokens) {
174
+ const bot = new TelegramGateway(token, config.tgAllowedUserId, config.tgAllowedChatId, config.telegramPollIntervalMs, store, logger, true);
175
+ const id = await bot.initializeIdentity();
176
+ if (seeds.some((runtime) => runtime.id === id)) {
177
+ throw new Error(`TG_BOT_TOKENS contains duplicate Telegram bot identity: ${id}`);
178
+ }
179
+ const home = path.join(DEFAULT_CODEX_TELEGRAM_HOME, id, 'home');
180
+ fs.mkdirSync(home, { recursive: true, mode: 0o700 });
181
+ const runtimeConfig = {
182
+ ...config,
183
+ tgBotToken: token,
184
+ tgBotTokens: [token],
185
+ tgScopeBotId: id,
186
+ codexAuthDir: home,
187
+ codexHome: home,
188
+ codexAppAutolaunch: false,
189
+ codexAppServerStatePath: path.join(APP_HOME, 'runtime', `codex-app-server-${id}.json`),
190
+ codexAppServerLogPath: path.join(APP_HOME, 'logs', `codex-app-server-${id}.log`),
191
+ };
192
+ const app = new CodexAppClient(runtimeConfig.codexCliBin, runtimeConfig.codexAppLaunchCmd, runtimeConfig.codexAppAutolaunch, runtimeConfig.codexAppServerStatePath, runtimeConfig.codexAppServerLogPath, logger, { CODEX_HOME: home }, ['cli_auth_credentials_store="file"']);
193
+ seeds.push({ id, home, config: runtimeConfig, bot, app });
194
+ }
195
+ const canonicalAuthDir = config.codexAuthDir ?? config.codexHome ?? path.join(os.homedir(), '.codex');
196
+ const mirror = new AuthCandidateMirror(canonicalAuthDir, seeds.map((runtime) => ({
197
+ id: runtime.id,
198
+ label: runtime.bot.username ? `@${runtime.bot.username}` : runtime.id,
199
+ authDir: runtime.home,
200
+ notify: async (message) => {
201
+ const chatId = store.getTelegramPrivateChatId(runtime.id);
202
+ if (chatId) {
203
+ await runtime.bot.sendMessage(chatId, message);
204
+ }
205
+ },
206
+ })), logger, path.join(APP_HOME, 'runtime', 'auth-mirror.json'));
207
+ await mirror.initialize();
208
+ mirror.start();
209
+ activeAuthMirror = mirror;
210
+ managedApps = seeds.map((runtime) => runtime.app);
211
+ const selfUpdater = createSelfUpdateRuntime({
212
+ entryPoint,
213
+ nodePath: process.execPath,
214
+ version: readPackageVersion(),
215
+ statusPath: config.statusPath,
216
+ logPath: path.join(APP_HOME, 'logs', 'update.log'),
217
+ codexCliBin: config.codexCliBin,
218
+ });
219
+ const lastSelfUpdatePath = path.join(APP_HOME, 'runtime', 'last-self-update.json');
220
+ let lastSelfUpdate = readSelfUpdateStatus(lastSelfUpdatePath);
221
+ const runtimes = [];
222
+ const writeAggregateStatus = (running = true) => {
223
+ const statuses = runtimes.map((runtime) => runtime.core.getRuntimeStatus());
224
+ const weixinStatus = activeWeixinCore?.getRuntimeStatus() ?? null;
225
+ const first = statuses[0] ?? null;
226
+ writeRuntimeStatus(config.statusPath, {
227
+ running,
228
+ connected: running
229
+ && statuses.every((status) => status.connected)
230
+ && (!weixinStatus || weixinStatus.connected),
231
+ userAgent: first?.userAgent ?? null,
232
+ ...(first?.codexAppServer ? { codexAppServer: first.codexAppServer } : {}),
233
+ botUsername: first?.botUsername ?? null,
234
+ currentBindings: store.countBindings(),
235
+ pendingApprovals: store.countPendingApprovals(),
236
+ pendingUserInputs: store.countPendingUserInputs(),
237
+ activeTurns: statuses.reduce((sum, status) => sum + status.activeTurns, 0)
238
+ + (weixinStatus?.activeTurns ?? 0),
239
+ lastError: statuses.find((status) => status.lastError)?.lastError
240
+ ?? weixinStatus?.lastError
241
+ ?? null,
242
+ updatedAt: new Date().toISOString(),
243
+ channels: { telegram: running, weixin: running && config.wxEnabled },
244
+ bots: runtimes.map((runtime, index) => ({
245
+ id: runtime.id,
246
+ username: statuses[index]?.botUsername ?? runtime.bot.username,
247
+ connected: running && Boolean(statuses[index]?.connected),
248
+ activeTurns: running ? (statuses[index]?.activeTurns ?? 0) : 0,
249
+ ...(statuses[index]?.codexAppServer ? { codexAppServer: statuses[index].codexAppServer } : {}),
250
+ })),
251
+ ...(weixinStatus ? {
252
+ weixinRuntime: {
253
+ connected: running && weixinStatus.connected,
254
+ activeTurns: running ? weixinStatus.activeTurns : 0,
255
+ ...(weixinStatus.codexAppServer ? { codexAppServer: weixinStatus.codexAppServer } : {}),
256
+ },
257
+ } : {}),
258
+ authMirror: mirror.getStatus(),
259
+ lastUpdate: lastSelfUpdate,
260
+ });
261
+ };
262
+ const coordinator = {
263
+ canSelfUpdate: () => runtimes.every((runtime) => runtime.core.isIdleForServiceUpdate())
264
+ && (!activeWeixinCore || activeWeixinCore.isIdleForServiceUpdate())
265
+ && mirror.isIdle(),
266
+ authCandidateUpdated: (runtimeId, candidateName) => mirror.syncRuntimeCandidate(runtimeId, candidateName).then(() => undefined),
267
+ statusUpdated: () => writeAggregateStatus(),
268
+ getServiceStatus: async () => ({
269
+ bots: await Promise.all(runtimes.map(async (runtime) => {
270
+ const status = runtime.core.getRuntimeStatus();
271
+ return {
272
+ id: runtime.id,
273
+ username: status.botUsername ?? runtime.bot.username,
274
+ connected: status.connected,
275
+ activeTurns: status.activeTurns,
276
+ currentAuth: await runtime.core.getCurrentAuthLabel().catch(() => null),
277
+ ...(status.codexAppServer ? { codexAppServer: status.codexAppServer } : {}),
278
+ };
279
+ })),
280
+ ...(activeWeixinCore ? {
281
+ weixinRuntime: {
282
+ connected: activeWeixinCore.getRuntimeStatus().connected,
283
+ activeTurns: activeWeixinCore.getRuntimeStatus().activeTurns,
284
+ codexAppServer: activeWeixinCore.getRuntimeStatus().codexAppServer,
285
+ },
286
+ } : {}),
287
+ authMirror: mirror.getStatus(),
288
+ lastUpdate: lastSelfUpdate,
289
+ }),
290
+ selfUpdateCompleted: (status) => {
291
+ lastSelfUpdate = status;
292
+ writeSelfUpdateStatus(lastSelfUpdatePath, status);
293
+ writeAggregateStatus();
294
+ },
295
+ };
296
+ for (const seed of seeds) {
297
+ const telegramMessaging = new TelegramMessagingPort(seed.bot);
298
+ const outbound = new BridgeMessagingRouter(telegramMessaging, null);
299
+ const core = new BridgeSessionCore(seed.config, store, logger, seed.bot, seed.app, outbound, selfUpdater, coordinator);
300
+ runtimes.push({ ...seed, core, telegram: new TelegramChannelAdapter(core) });
301
+ }
302
+ if (config.wxEnabled) {
303
+ const weixinApp = new CodexAppClient(config.codexCliBin, config.codexAppLaunchCmd, config.codexAppAutolaunch, config.codexAppServerStatePath, config.codexAppServerLogPath, logger);
304
+ const outbound = new BridgeMessagingRouter(new TelegramMessagingPort(seeds[0].bot), new WeixinMessagingPort(store, (id) => loadWeixinAccount(config.weixinAccountsDir, id)));
305
+ activeWeixinCore = new BridgeSessionCore(config, store, logger, seeds[0].bot, weixinApp, outbound, selfUpdater, coordinator, false);
306
+ managedApps.push(weixinApp);
307
+ weixinAdapter = new WeixinChannelAdapter(activeWeixinCore, store, config, logger);
308
+ }
309
+ activeTelegramAdapters = runtimes.map((runtime) => runtime.telegram);
310
+ process.on('unhandledRejection', (error) => {
311
+ logger.error('process.unhandled_rejection', { error: serializeError(error) });
312
+ });
313
+ process.on('uncaughtException', (error) => {
314
+ logger.error('process.uncaught_exception', { error: serializeError(error) });
315
+ });
316
+ for (const runtime of runtimes) {
317
+ await runtime.telegram.start();
318
+ }
319
+ if (weixinAdapter) {
320
+ await activeWeixinCore.startCodexApp();
321
+ await weixinAdapter.start();
322
+ }
323
+ writeAggregateStatus();
324
+ logger.info('bridge.started', { bots: runtimes.map((runtime) => runtime.id) });
325
+ const shutdown = async (signal) => {
326
+ logger.info('bridge.shutting_down', { signal });
327
+ mirror.stop();
328
+ await weixinAdapter?.stop();
329
+ await activeWeixinCore?.stop();
330
+ await Promise.all(runtimes.map((runtime) => runtime.telegram.stop()));
331
+ writeAggregateStatus(false);
332
+ await Promise.all(managedApps.map((app) => app.stop({ terminateServer: true }).catch((error) => {
333
+ logger.warn('codex.app-server.stop_failed', { error: serializeError(error) });
334
+ })));
335
+ store?.close();
336
+ processLock.release();
337
+ process.exit(0);
338
+ };
339
+ process.on('SIGINT', () => void shutdown('SIGINT'));
340
+ process.on('SIGTERM', () => void shutdown('SIGTERM'));
341
+ return;
342
+ }
162
343
  const bot = new TelegramGateway(config.tgBotToken, config.tgAllowedUserId, config.tgAllowedChatId, config.telegramPollIntervalMs, store, logger);
163
344
  const app = new CodexAppClient(config.codexCliBin, config.codexAppLaunchCmd, config.codexAppAutolaunch, config.codexAppServerStatePath, config.codexAppServerLogPath, logger);
164
345
  const telegramMessaging = new TelegramMessagingPort(bot);
@@ -172,9 +353,12 @@ async function runServeCli() {
172
353
  version: readPackageVersion(),
173
354
  statusPath: config.statusPath,
174
355
  logPath: path.join(APP_HOME, 'logs', 'update.log'),
356
+ codexCliBin: config.codexCliBin,
175
357
  });
176
358
  const core = new BridgeSessionCore(config, store, logger, bot, app, outbound, selfUpdater);
177
359
  const telegram = new TelegramChannelAdapter(core);
360
+ managedApps = [app];
361
+ activeTelegramAdapters = [telegram];
178
362
  if (config.wxEnabled) {
179
363
  weixinAdapter = new WeixinChannelAdapter(core, store, config, logger);
180
364
  }
@@ -218,7 +402,11 @@ async function runServeCli() {
218
402
  process.on('SIGTERM', () => void shutdown('SIGTERM'));
219
403
  }
220
404
  catch (error) {
405
+ activeAuthMirror?.stop();
221
406
  await weixinAdapter?.stop().catch(() => { });
407
+ await activeWeixinCore?.stop().catch(() => { });
408
+ await Promise.allSettled(activeTelegramAdapters.map((adapter) => adapter.stop()));
409
+ await Promise.allSettled(managedApps.map((app) => app.stop({ terminateServer: true })));
222
410
  store?.close();
223
411
  processLock.release();
224
412
  throw error;
@@ -264,15 +452,15 @@ async function configureEnvInteractively(envPath, existed) {
264
452
  const skipped = [];
265
453
  const warnings = [];
266
454
  Object.assign(updates, await maybeSaveProxyEnvFromShell(rl, envPath));
267
- const token = sanitizeEnvInput(await rl.question('Telegram bot token (TG_BOT_TOKEN): '));
268
- if (token) {
269
- updates.TG_BOT_TOKEN = token;
270
- if (!/^\d+:[A-Za-z0-9_-]+$/.test(token)) {
271
- warnings.push('TG_BOT_TOKEN does not look like a standard Telegram bot token.');
455
+ const tokens = sanitizeEnvInput(await rl.question('Telegram bot token(s), comma-separated (TG_BOT_TOKENS): '));
456
+ if (tokens) {
457
+ updates.TG_BOT_TOKENS = tokens;
458
+ if (tokens.split(',').map((token) => token.trim()).some((token) => !/^\d+:[A-Za-z0-9_-]+$/.test(token))) {
459
+ warnings.push('One or more TG_BOT_TOKENS values do not look like standard Telegram bot tokens.');
272
460
  }
273
461
  }
274
462
  else {
275
- skipped.push('TG_BOT_TOKEN');
463
+ skipped.push('TG_BOT_TOKENS');
276
464
  }
277
465
  const userId = sanitizeEnvInput(await rl.question('Telegram numeric user ID (TG_ALLOWED_USER_ID): '));
278
466
  if (userId) {
@@ -486,7 +674,7 @@ function runDoctorChecks() {
486
674
  const checks = [
487
675
  ['node >= 24', Number(process.versions.node.split('.')[0]) >= 24],
488
676
  ['codex cli available', hasConfiguredCodexBin(configuredCodexBin) || hasCommand('codex')],
489
- ['telegram bot token configured', Boolean(process.env.TG_BOT_TOKEN)],
677
+ ['telegram bot token(s) configured', Boolean(process.env.TG_BOT_TOKENS?.trim() || process.env.TG_BOT_TOKEN?.trim())],
490
678
  ['telegram allowed user configured', Boolean(process.env.TG_ALLOWED_USER_ID)],
491
679
  ];
492
680
  if (process.env.WX_ENABLED === 'true' || process.env.WX_ENABLED === '1') {
@@ -29,6 +29,8 @@ export declare class BridgeStore {
29
29
  constructor(dbPath: string);
30
30
  getTelegramOffset(botKey: string): number;
31
31
  setTelegramOffset(botKey: string, updateId: number): void;
32
+ rememberTelegramPrivateScope(botId: string, scopeId: string, chatId: string): void;
33
+ getTelegramPrivateChatId(botId: string): string | null;
32
34
  getBinding(chatId: string): ThreadBinding | null;
33
35
  setBinding(chatId: string, threadId: string, cwd: string | null): void;
34
36
  clearBinding(chatId: string): void;
@@ -73,7 +75,7 @@ export declare class BridgeStore {
73
75
  private writeChatSettings;
74
76
  getWeixinContextToken(scopeId: string): string | null;
75
77
  setWeixinContextToken(scopeId: string, contextToken: string): void;
76
- listDisabledCodexAuthCandidateNames(): Set<string>;
77
- setCodexAuthCandidateDisabled(name: string, disabled: boolean): void;
78
+ listDisabledCodexAuthCandidateNames(runtimeId?: string): Set<string>;
79
+ setCodexAuthCandidateDisabled(name: string, disabled: boolean, runtimeId?: string): void;
78
80
  private ensureColumn;
79
81
  }
@@ -8,10 +8,16 @@ export class BridgeStore {
8
8
  fs.mkdirSync(path.dirname(dbPath), { recursive: true });
9
9
  this.db = new DatabaseSync(dbPath);
10
10
  this.db.exec(`
11
- CREATE TABLE IF NOT EXISTS telegram_offsets (
12
- bot_key TEXT PRIMARY KEY,
13
- update_id INTEGER NOT NULL
14
- );
11
+ CREATE TABLE IF NOT EXISTS telegram_offsets (
12
+ bot_key TEXT PRIMARY KEY,
13
+ update_id INTEGER NOT NULL
14
+ );
15
+ CREATE TABLE IF NOT EXISTS telegram_private_scopes (
16
+ bot_id TEXT PRIMARY KEY,
17
+ scope_id TEXT NOT NULL,
18
+ chat_id TEXT NOT NULL,
19
+ updated_at INTEGER NOT NULL
20
+ );
15
21
  CREATE TABLE IF NOT EXISTS chat_bindings (
16
22
  chat_id TEXT PRIMARY KEY,
17
23
  thread_id TEXT NOT NULL,
@@ -110,6 +116,13 @@ export class BridgeStore {
110
116
  disabled INTEGER NOT NULL DEFAULT 0,
111
117
  updated_at INTEGER NOT NULL
112
118
  );
119
+ CREATE TABLE IF NOT EXISTS codex_auth_candidate_runtime (
120
+ runtime_id TEXT NOT NULL,
121
+ name TEXT NOT NULL,
122
+ disabled INTEGER NOT NULL DEFAULT 0,
123
+ updated_at INTEGER NOT NULL,
124
+ PRIMARY KEY (runtime_id, name)
125
+ );
113
126
  `);
114
127
  this.ensureColumn('thread_cache', 'name', 'TEXT');
115
128
  this.ensureColumn('thread_cache', 'model_provider', 'TEXT');
@@ -136,6 +149,17 @@ export class BridgeStore {
136
149
  ON CONFLICT(bot_key) DO UPDATE SET update_id = excluded.update_id
137
150
  `).run(botKey, updateId);
138
151
  }
152
+ rememberTelegramPrivateScope(botId, scopeId, chatId) {
153
+ this.db.prepare(`
154
+ INSERT INTO telegram_private_scopes (bot_id, scope_id, chat_id, updated_at)
155
+ VALUES (?, ?, ?, ?)
156
+ ON CONFLICT(bot_id) DO UPDATE SET scope_id = excluded.scope_id, chat_id = excluded.chat_id, updated_at = excluded.updated_at
157
+ `).run(botId, scopeId, chatId, Date.now());
158
+ }
159
+ getTelegramPrivateChatId(botId) {
160
+ const row = this.db.prepare('SELECT chat_id FROM telegram_private_scopes WHERE bot_id = ?').get(botId);
161
+ return row ? String(row.chat_id) : null;
162
+ }
139
163
  getBinding(chatId) {
140
164
  const row = this.db.prepare('SELECT chat_id, thread_id, cwd, updated_at FROM chat_bindings WHERE chat_id = ?').get(chatId);
141
165
  if (!row)
@@ -462,11 +486,23 @@ export class BridgeStore {
462
486
  ON CONFLICT(scope_id) DO UPDATE SET context_token = excluded.context_token, updated_at = excluded.updated_at
463
487
  `).run(scopeId, contextToken, Date.now());
464
488
  }
465
- listDisabledCodexAuthCandidateNames() {
489
+ listDisabledCodexAuthCandidateNames(runtimeId = 'default') {
490
+ if (runtimeId !== 'default') {
491
+ const runtimeRows = this.db.prepare('SELECT name FROM codex_auth_candidate_runtime WHERE runtime_id = ? AND disabled = 1').all(runtimeId);
492
+ return new Set(runtimeRows.map(row => String(row.name)));
493
+ }
466
494
  const rows = this.db.prepare('SELECT name FROM codex_auth_candidates WHERE disabled = 1').all();
467
495
  return new Set(rows.map(row => String(row.name)));
468
496
  }
469
- setCodexAuthCandidateDisabled(name, disabled) {
497
+ setCodexAuthCandidateDisabled(name, disabled, runtimeId = 'default') {
498
+ if (runtimeId !== 'default') {
499
+ this.db.prepare(`
500
+ INSERT INTO codex_auth_candidate_runtime (runtime_id, name, disabled, updated_at)
501
+ VALUES (?, ?, ?, ?)
502
+ ON CONFLICT(runtime_id, name) DO UPDATE SET disabled = excluded.disabled, updated_at = excluded.updated_at
503
+ `).run(runtimeId, name, disabled ? 1 : 0, Date.now());
504
+ return;
505
+ }
470
506
  this.db.prepare(`
471
507
  INSERT INTO codex_auth_candidates (name, disabled, updated_at)
472
508
  VALUES (?, ?, ?)
@@ -27,6 +27,7 @@ interface DefaultScopeParams {
27
27
  allowedChatId: string | null;
28
28
  allowedTopicId: number | null;
29
29
  topicId: number | null;
30
+ requireExplicitGroupAddressing?: boolean;
30
31
  }
31
32
  export declare function resolveTelegramAddressing(params: ResolveTelegramAddressingParams): TelegramAddressingDecision;
32
33
  export declare function isDefaultTelegramScope(params: DefaultScopeParams): boolean;
@@ -21,6 +21,9 @@ export function isDefaultTelegramScope(params) {
21
21
  if (params.chatType === 'private') {
22
22
  return true;
23
23
  }
24
+ if (params.requireExplicitGroupAddressing) {
25
+ return false;
26
+ }
24
27
  if (params.allowedChatId === null) {
25
28
  return false;
26
29
  }
@@ -34,12 +34,15 @@ export declare class TelegramGateway extends EventEmitter {
34
34
  private readonly pollIntervalMs;
35
35
  private readonly store;
36
36
  private readonly logger;
37
+ private readonly namespacedScopes;
37
38
  private running;
38
39
  private botKey;
39
40
  private botUsername;
40
41
  private botUserId;
41
- constructor(botToken: string, allowedUserId: string, allowedChatId: string | null, pollIntervalMs: number, store: BridgeStore, logger: Logger);
42
+ constructor(botToken: string, allowedUserId: string, allowedChatId: string | null, pollIntervalMs: number, store: BridgeStore, logger: Logger, namespacedScopes?: boolean);
42
43
  get username(): string | null;
44
+ get identity(): string | null;
45
+ initializeIdentity(): Promise<string>;
43
46
  start(): Promise<void>;
44
47
  stop(): void;
45
48
  sendMessage(chatId: string, text: string, inlineKeyboard?: Array<Array<{
@@ -11,11 +11,12 @@ export class TelegramGateway extends EventEmitter {
11
11
  pollIntervalMs;
12
12
  store;
13
13
  logger;
14
+ namespacedScopes;
14
15
  running = false;
15
16
  botKey;
16
17
  botUsername = null;
17
18
  botUserId = null;
18
- constructor(botToken, allowedUserId, allowedChatId, pollIntervalMs, store, logger) {
19
+ constructor(botToken, allowedUserId, allowedChatId, pollIntervalMs, store, logger, namespacedScopes = false) {
19
20
  super();
20
21
  this.botToken = botToken;
21
22
  this.allowedUserId = allowedUserId;
@@ -23,16 +24,26 @@ export class TelegramGateway extends EventEmitter {
23
24
  this.pollIntervalMs = pollIntervalMs;
24
25
  this.store = store;
25
26
  this.logger = logger;
27
+ this.namespacedScopes = namespacedScopes;
26
28
  this.botKey = `telegram:${crypto.createHash('sha256').update(this.botToken).digest('hex').slice(0, 8)}`;
27
29
  }
28
30
  get username() {
29
31
  return this.botUsername;
30
32
  }
33
+ get identity() {
34
+ return this.botUserId === null ? null : `bot${this.botUserId}`;
35
+ }
36
+ async initializeIdentity() {
37
+ await this.resolveBotIdentity(true);
38
+ return this.identity;
39
+ }
31
40
  async start() {
32
41
  if (this.running)
33
42
  return;
34
43
  this.running = true;
35
- await this.resolveBotIdentity();
44
+ if (this.botUserId === null) {
45
+ await this.resolveBotIdentity(this.namespacedScopes);
46
+ }
36
47
  await this.registerCommands();
37
48
  void this.pollLoop();
38
49
  }
@@ -134,12 +145,16 @@ export class TelegramGateway extends EventEmitter {
134
145
  async downloadResolvedFile(remoteFilePath, destinationPath) {
135
146
  return downloadTelegramFile(this.botToken, remoteFilePath, destinationPath);
136
147
  }
137
- async resolveBotIdentity() {
148
+ async resolveBotIdentity(required = false) {
138
149
  const result = await callTelegramApi(this.botToken, 'getMe', {});
139
150
  if (result.ok && result.result) {
140
151
  this.botKey = `telegram:bot${result.result.id}`;
141
152
  this.botUserId = result.result.id;
142
153
  this.botUsername = result.result.username ?? null;
154
+ return;
155
+ }
156
+ if (required) {
157
+ throw new Error(result.description || 'Failed to resolve Telegram bot identity');
143
158
  }
144
159
  }
145
160
  async registerCommands() {
@@ -187,7 +202,7 @@ export class TelegramGateway extends EventEmitter {
187
202
  const attachments = extractAttachments(update.message);
188
203
  const text = update.message.text ?? update.message.caption ?? '';
189
204
  const topicId = update.message.message_thread_id ?? null;
190
- const scopeId = toTelegramBridgeScopeId(createTelegramScopeId(String(update.message.chat.id), topicId));
205
+ const scopeId = toTelegramBridgeScopeId(createTelegramScopeId(String(update.message.chat.id), topicId), this.namespacedScopes ? this.identity : null);
191
206
  const entities = update.message.text ? (update.message.entities ?? []) : (update.message.caption_entities ?? []);
192
207
  const replyToBot = this.botUserId !== null && update.message.reply_to_message?.from?.id === this.botUserId;
193
208
  if (text || attachments.length > 0) {
@@ -204,6 +219,9 @@ export class TelegramGateway extends EventEmitter {
204
219
  replyToBot,
205
220
  ...(update.message.from.language_code ? { languageCode: update.message.from.language_code } : {}),
206
221
  });
222
+ if (update.message.chat.type === 'private' && this.identity) {
223
+ this.store.rememberTelegramPrivateScope(this.identity, scopeId, String(update.message.chat.id));
224
+ }
207
225
  return;
208
226
  }
209
227
  }
@@ -216,7 +234,7 @@ export class TelegramGateway extends EventEmitter {
216
234
  this.emit('callback', {
217
235
  chatId: String(update.callback_query.message.chat.id),
218
236
  topicId,
219
- scopeId: toTelegramBridgeScopeId(createTelegramScopeId(String(update.callback_query.message.chat.id), topicId)),
237
+ scopeId: toTelegramBridgeScopeId(createTelegramScopeId(String(update.callback_query.message.chat.id), topicId), this.namespacedScopes ? this.identity : null),
220
238
  userId: String(update.callback_query.from.id),
221
239
  data: update.callback_query.data,
222
240
  callbackQueryId: update.callback_query.id,
package/dist/types.d.ts CHANGED
@@ -340,4 +340,30 @@ export interface RuntimeStatus {
340
340
  telegram: boolean;
341
341
  weixin: boolean;
342
342
  };
343
+ bots?: Array<{
344
+ id: string;
345
+ username: string | null;
346
+ connected: boolean;
347
+ activeTurns: number;
348
+ currentAuth?: string | null;
349
+ codexAppServer?: RuntimeStatus['codexAppServer'];
350
+ }>;
351
+ weixinRuntime?: {
352
+ connected: boolean;
353
+ activeTurns: number;
354
+ codexAppServer?: RuntimeStatus['codexAppServer'];
355
+ };
356
+ authMirror?: {
357
+ candidateName: string;
358
+ sourceRuntimeId: string;
359
+ sourceLabel: string;
360
+ syncedAt: string;
361
+ } | null;
362
+ lastUpdate?: {
363
+ state: string;
364
+ fromVersion: string;
365
+ toVersion: string | null;
366
+ codexUpdate?: string | null;
367
+ updatedAt: string;
368
+ } | null;
343
369
  }
package/dist/update.d.ts CHANGED
@@ -6,6 +6,7 @@ export interface SelfUpdateStatus {
6
6
  locale: AppLocale;
7
7
  fromVersion: string;
8
8
  toVersion: string | null;
9
+ codexUpdate?: string | null;
9
10
  error: string | null;
10
11
  updatedAt: string;
11
12
  }
@@ -19,6 +20,7 @@ export interface SelfUpdateInstaller {
19
20
  command: string;
20
21
  installArgs: string[];
21
22
  rootArgs: string[];
23
+ pnpmHome?: string;
22
24
  }
23
25
  interface CreateSelfUpdateRuntimeOptions {
24
26
  entryPoint: string;
@@ -26,12 +28,14 @@ interface CreateSelfUpdateRuntimeOptions {
26
28
  version: string;
27
29
  statusPath: string;
28
30
  logPath: string;
31
+ codexCliBin?: string;
29
32
  }
30
33
  interface PerformSelfUpdateOptions {
31
34
  entryPoint: string;
32
35
  nodePath: string;
33
36
  version: string;
34
37
  notificationFile?: string;
38
+ codexCliBin?: string;
35
39
  env?: NodeJS.ProcessEnv;
36
40
  }
37
41
  export interface SelfUpdateOutcome {
@@ -43,6 +47,7 @@ export interface SelfUpdateOutcome {
43
47
  export declare function selfUpdateStatusPath(statusPath: string): string;
44
48
  export declare function inferPnpmHomeFromEntryPoint(entryPoint: string): string | null;
45
49
  export declare function resolveSelfUpdateInstaller(entryPoint: string, nodePath?: string, exists?: (target: string) => boolean, env?: NodeJS.ProcessEnv): SelfUpdateInstaller;
50
+ export declare function resolveCodexUpdateInstaller(codexCliBin: string, nodePath?: string, exists?: (target: string) => boolean, env?: NodeJS.ProcessEnv, realpath?: (target: string) => string, readText?: (target: string) => string): SelfUpdateInstaller | null;
46
51
  export declare function readSelfUpdateStatus(statusFile: string): SelfUpdateStatus | null;
47
52
  export declare function writeSelfUpdateStatus(statusFile: string, status: SelfUpdateStatus): void;
48
53
  export declare function createSelfUpdateRuntime(options: CreateSelfUpdateRuntimeOptions): SelfUpdateRuntime;