@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.
- package/.env.example +8 -3
- package/README.md +17 -7
- package/README_EN.md +17 -7
- package/dist/auth/mirror.d.ts +38 -0
- package/dist/auth/mirror.js +259 -0
- package/dist/codex_app/client.d.ts +4 -1
- package/dist/codex_app/client.js +23 -4
- package/dist/config.d.ts +7 -0
- package/dist/config.js +18 -1
- package/dist/controller/controller.d.ts +24 -2
- package/dist/controller/controller.js +161 -35
- package/dist/core/bridge_scope.d.ts +5 -2
- package/dist/core/bridge_scope.js +7 -3
- package/dist/i18n.d.ts +20 -2
- package/dist/i18n.js +20 -2
- package/dist/main.js +198 -10
- package/dist/store/database.d.ts +4 -2
- package/dist/store/database.js +42 -6
- package/dist/telegram/addressing.d.ts +1 -0
- package/dist/telegram/addressing.js +3 -0
- package/dist/telegram/gateway.d.ts +4 -1
- package/dist/telegram/gateway.js +23 -5
- package/dist/types.d.ts +26 -0
- package/dist/update.d.ts +5 -0
- package/dist/update.js +93 -5
- package/docs/agent-assisted-install.md +7 -6
- package/docs/install-for-beginners.md +12 -4
- package/docs/troubleshooting.md +13 -1
- package/docs/user-manual.md +12 -12
- package/docs/zh/agent-assisted-install.md +7 -6
- package/docs/zh/foxclaw-skill.md +4 -2
- package/docs/zh/install-for-beginners.md +12 -4
- package/docs/zh/troubleshooting.md +13 -1
- package/docs/zh/user-manual.md +12 -12
- package/package.json +1 -1
- package/skills/foxclaw/SKILL.md +28 -20
- package/skills/foxclaw/references/telegram-setup.md +9 -6
- package/skills/foxclaw/scripts/bootstrap_host.py +11 -8
- 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
|
|
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: '
|
|
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
|
|
268
|
-
if (
|
|
269
|
-
updates.
|
|
270
|
-
if (!/^\d+:[A-Za-z0-9_-]+$/.test(token)) {
|
|
271
|
-
warnings.push('
|
|
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('
|
|
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') {
|
package/dist/store/database.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/store/database.js
CHANGED
|
@@ -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;
|
|
@@ -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<{
|
package/dist/telegram/gateway.js
CHANGED
|
@@ -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
|
-
|
|
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;
|