@foxden-app/foxclaw 0.3.17 → 0.3.19
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 +5 -2
- package/README.md +14 -4
- package/README_EN.md +14 -4
- package/dist/auth/mirror.d.ts +25 -0
- package/dist/auth/mirror.js +199 -0
- package/dist/codex_app/client.d.ts +2 -1
- package/dist/codex_app/client.js +10 -2
- package/dist/codex_app/local_usage.d.ts +7 -6
- package/dist/codex_app/local_usage.js +55 -66
- package/dist/config.d.ts +7 -0
- package/dist/config.js +18 -1
- package/dist/controller/controller.d.ts +14 -2
- package/dist/controller/controller.js +110 -34
- package/dist/core/bridge_scope.d.ts +5 -2
- package/dist/core/bridge_scope.js +7 -3
- package/dist/i18n.d.ts +4 -4
- package/dist/i18n.js +4 -4
- package/dist/main.js +144 -9
- 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 +7 -0
- package/dist/update.d.ts +5 -0
- package/dist/update.js +93 -5
- package/docs/agent-assisted-install.md +4 -4
- package/docs/install-for-beginners.md +3 -3
- package/docs/troubleshooting.md +1 -1
- package/docs/user-manual.md +11 -11
- package/docs/zh/agent-assisted-install.md +4 -4
- package/docs/zh/foxclaw-skill.md +1 -1
- package/docs/zh/install-for-beginners.md +3 -3
- package/docs/zh/troubleshooting.md +1 -1
- package/docs/zh/user-manual.md +11 -11
- package/package.json +1 -1
- package/skills/foxclaw/SKILL.md +26 -19
- package/skills/foxclaw/references/telegram-setup.md +5 -4
- package/skills/foxclaw/scripts/bootstrap_host.py +11 -8
- package/skills/foxclaw/scripts/bootstrap_remote.py +8 -4
- package/skills/npm-publish/SKILL.md +3 -0
package/dist/main.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
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';
|
|
@@ -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,132 @@ async function runServeCli() {
|
|
|
157
162
|
const processLock = acquireProcessLock(config.lockPath);
|
|
158
163
|
let store = null;
|
|
159
164
|
let weixinAdapter = null;
|
|
165
|
+
let activeTelegramAdapters = [];
|
|
166
|
+
let managedApps = [];
|
|
167
|
+
let activeAuthMirror = null;
|
|
160
168
|
try {
|
|
161
169
|
store = new BridgeStore(config.storePath);
|
|
170
|
+
if (config.tgMultiBotMode) {
|
|
171
|
+
const seeds = [];
|
|
172
|
+
for (const token of config.tgBotTokens) {
|
|
173
|
+
const bot = new TelegramGateway(token, config.tgAllowedUserId, config.tgAllowedChatId, config.telegramPollIntervalMs, store, logger, true);
|
|
174
|
+
const id = await bot.initializeIdentity();
|
|
175
|
+
if (seeds.some((runtime) => runtime.id === id)) {
|
|
176
|
+
throw new Error(`TG_BOT_TOKENS contains duplicate Telegram bot identity: ${id}`);
|
|
177
|
+
}
|
|
178
|
+
const home = path.join(DEFAULT_CODEX_TELEGRAM_HOME, id, 'home');
|
|
179
|
+
fs.mkdirSync(home, { recursive: true, mode: 0o700 });
|
|
180
|
+
const runtimeConfig = {
|
|
181
|
+
...config,
|
|
182
|
+
tgBotToken: token,
|
|
183
|
+
tgBotTokens: [token],
|
|
184
|
+
tgScopeBotId: id,
|
|
185
|
+
codexAuthDir: home,
|
|
186
|
+
codexHome: home,
|
|
187
|
+
codexAppServerStatePath: path.join(APP_HOME, 'runtime', `codex-app-server-${id}.json`),
|
|
188
|
+
codexAppServerLogPath: path.join(APP_HOME, 'logs', `codex-app-server-${id}.log`),
|
|
189
|
+
};
|
|
190
|
+
const app = new CodexAppClient(runtimeConfig.codexCliBin, runtimeConfig.codexAppLaunchCmd, runtimeConfig.codexAppAutolaunch, runtimeConfig.codexAppServerStatePath, runtimeConfig.codexAppServerLogPath, logger, { CODEX_HOME: home });
|
|
191
|
+
seeds.push({ id, home, config: runtimeConfig, bot, app });
|
|
192
|
+
}
|
|
193
|
+
const canonicalAuthDir = config.codexAuthDir ?? config.codexHome ?? path.join(os.homedir(), '.codex');
|
|
194
|
+
const mirror = new AuthCandidateMirror(canonicalAuthDir, seeds.map((runtime) => ({
|
|
195
|
+
id: runtime.id,
|
|
196
|
+
authDir: runtime.home,
|
|
197
|
+
notify: async (message) => {
|
|
198
|
+
const chatId = store.getTelegramPrivateChatId(runtime.id);
|
|
199
|
+
if (chatId) {
|
|
200
|
+
await runtime.bot.sendMessage(chatId, message);
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
})), logger);
|
|
204
|
+
await mirror.initialize();
|
|
205
|
+
mirror.start();
|
|
206
|
+
activeAuthMirror = mirror;
|
|
207
|
+
managedApps = seeds.map((runtime) => runtime.app);
|
|
208
|
+
const selfUpdater = createSelfUpdateRuntime({
|
|
209
|
+
entryPoint,
|
|
210
|
+
nodePath: process.execPath,
|
|
211
|
+
version: readPackageVersion(),
|
|
212
|
+
statusPath: config.statusPath,
|
|
213
|
+
logPath: path.join(APP_HOME, 'logs', 'update.log'),
|
|
214
|
+
codexCliBin: config.codexCliBin,
|
|
215
|
+
});
|
|
216
|
+
const runtimes = [];
|
|
217
|
+
const writeAggregateStatus = (running = true) => {
|
|
218
|
+
const statuses = runtimes.map((runtime) => runtime.core.getRuntimeStatus());
|
|
219
|
+
const first = statuses[0] ?? null;
|
|
220
|
+
writeRuntimeStatus(config.statusPath, {
|
|
221
|
+
running,
|
|
222
|
+
connected: running && statuses.every((status) => status.connected),
|
|
223
|
+
userAgent: first?.userAgent ?? null,
|
|
224
|
+
...(first?.codexAppServer ? { codexAppServer: first.codexAppServer } : {}),
|
|
225
|
+
botUsername: first?.botUsername ?? null,
|
|
226
|
+
currentBindings: store.countBindings(),
|
|
227
|
+
pendingApprovals: store.countPendingApprovals(),
|
|
228
|
+
pendingUserInputs: store.countPendingUserInputs(),
|
|
229
|
+
activeTurns: statuses.reduce((sum, status) => sum + status.activeTurns, 0),
|
|
230
|
+
lastError: statuses.find((status) => status.lastError)?.lastError ?? null,
|
|
231
|
+
updatedAt: new Date().toISOString(),
|
|
232
|
+
channels: { telegram: running, weixin: running && config.wxEnabled },
|
|
233
|
+
bots: runtimes.map((runtime, index) => ({
|
|
234
|
+
id: runtime.id,
|
|
235
|
+
username: statuses[index]?.botUsername ?? runtime.bot.username,
|
|
236
|
+
connected: running && Boolean(statuses[index]?.connected),
|
|
237
|
+
activeTurns: running ? (statuses[index]?.activeTurns ?? 0) : 0,
|
|
238
|
+
...(statuses[index]?.codexAppServer ? { codexAppServer: statuses[index].codexAppServer } : {}),
|
|
239
|
+
})),
|
|
240
|
+
});
|
|
241
|
+
};
|
|
242
|
+
const coordinator = {
|
|
243
|
+
canSelfUpdate: () => runtimes.every((runtime) => runtime.core.isIdleForServiceUpdate()),
|
|
244
|
+
authCandidateUpdated: (runtimeId, candidateName) => mirror.syncRuntimeCandidate(runtimeId, candidateName).then(() => undefined),
|
|
245
|
+
statusUpdated: () => writeAggregateStatus(),
|
|
246
|
+
};
|
|
247
|
+
for (const [index, seed] of seeds.entries()) {
|
|
248
|
+
const telegramMessaging = new TelegramMessagingPort(seed.bot);
|
|
249
|
+
const weixinMessaging = index === 0 && config.wxEnabled
|
|
250
|
+
? new WeixinMessagingPort(store, (id) => loadWeixinAccount(config.weixinAccountsDir, id))
|
|
251
|
+
: null;
|
|
252
|
+
const outbound = new BridgeMessagingRouter(telegramMessaging, weixinMessaging);
|
|
253
|
+
const core = new BridgeSessionCore(seed.config, store, logger, seed.bot, seed.app, outbound, selfUpdater, coordinator);
|
|
254
|
+
runtimes.push({ ...seed, core, telegram: new TelegramChannelAdapter(core) });
|
|
255
|
+
}
|
|
256
|
+
if (config.wxEnabled) {
|
|
257
|
+
weixinAdapter = new WeixinChannelAdapter(runtimes[0].core, store, runtimes[0].config, logger);
|
|
258
|
+
}
|
|
259
|
+
activeTelegramAdapters = runtimes.map((runtime) => runtime.telegram);
|
|
260
|
+
process.on('unhandledRejection', (error) => {
|
|
261
|
+
logger.error('process.unhandled_rejection', { error: serializeError(error) });
|
|
262
|
+
});
|
|
263
|
+
process.on('uncaughtException', (error) => {
|
|
264
|
+
logger.error('process.uncaught_exception', { error: serializeError(error) });
|
|
265
|
+
});
|
|
266
|
+
for (const runtime of runtimes) {
|
|
267
|
+
await runtime.telegram.start();
|
|
268
|
+
}
|
|
269
|
+
if (weixinAdapter) {
|
|
270
|
+
await weixinAdapter.start();
|
|
271
|
+
}
|
|
272
|
+
writeAggregateStatus();
|
|
273
|
+
logger.info('bridge.started', { bots: runtimes.map((runtime) => runtime.id) });
|
|
274
|
+
const shutdown = async (signal) => {
|
|
275
|
+
logger.info('bridge.shutting_down', { signal });
|
|
276
|
+
mirror.stop();
|
|
277
|
+
await weixinAdapter?.stop();
|
|
278
|
+
await Promise.all(runtimes.map((runtime) => runtime.telegram.stop()));
|
|
279
|
+
writeAggregateStatus(false);
|
|
280
|
+
await Promise.all(runtimes.map((runtime) => runtime.app.stop({ terminateServer: true }).catch((error) => {
|
|
281
|
+
logger.warn('codex.app-server.stop_failed', { runtimeId: runtime.id, error: serializeError(error) });
|
|
282
|
+
})));
|
|
283
|
+
store?.close();
|
|
284
|
+
processLock.release();
|
|
285
|
+
process.exit(0);
|
|
286
|
+
};
|
|
287
|
+
process.on('SIGINT', () => void shutdown('SIGINT'));
|
|
288
|
+
process.on('SIGTERM', () => void shutdown('SIGTERM'));
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
162
291
|
const bot = new TelegramGateway(config.tgBotToken, config.tgAllowedUserId, config.tgAllowedChatId, config.telegramPollIntervalMs, store, logger);
|
|
163
292
|
const app = new CodexAppClient(config.codexCliBin, config.codexAppLaunchCmd, config.codexAppAutolaunch, config.codexAppServerStatePath, config.codexAppServerLogPath, logger);
|
|
164
293
|
const telegramMessaging = new TelegramMessagingPort(bot);
|
|
@@ -172,9 +301,12 @@ async function runServeCli() {
|
|
|
172
301
|
version: readPackageVersion(),
|
|
173
302
|
statusPath: config.statusPath,
|
|
174
303
|
logPath: path.join(APP_HOME, 'logs', 'update.log'),
|
|
304
|
+
codexCliBin: config.codexCliBin,
|
|
175
305
|
});
|
|
176
306
|
const core = new BridgeSessionCore(config, store, logger, bot, app, outbound, selfUpdater);
|
|
177
307
|
const telegram = new TelegramChannelAdapter(core);
|
|
308
|
+
managedApps = [app];
|
|
309
|
+
activeTelegramAdapters = [telegram];
|
|
178
310
|
if (config.wxEnabled) {
|
|
179
311
|
weixinAdapter = new WeixinChannelAdapter(core, store, config, logger);
|
|
180
312
|
}
|
|
@@ -218,7 +350,10 @@ async function runServeCli() {
|
|
|
218
350
|
process.on('SIGTERM', () => void shutdown('SIGTERM'));
|
|
219
351
|
}
|
|
220
352
|
catch (error) {
|
|
353
|
+
activeAuthMirror?.stop();
|
|
221
354
|
await weixinAdapter?.stop().catch(() => { });
|
|
355
|
+
await Promise.allSettled(activeTelegramAdapters.map((adapter) => adapter.stop()));
|
|
356
|
+
await Promise.allSettled(managedApps.map((app) => app.stop({ terminateServer: true })));
|
|
222
357
|
store?.close();
|
|
223
358
|
processLock.release();
|
|
224
359
|
throw error;
|
|
@@ -264,15 +399,15 @@ async function configureEnvInteractively(envPath, existed) {
|
|
|
264
399
|
const skipped = [];
|
|
265
400
|
const warnings = [];
|
|
266
401
|
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('
|
|
402
|
+
const tokens = sanitizeEnvInput(await rl.question('Telegram bot token(s), comma-separated (TG_BOT_TOKENS): '));
|
|
403
|
+
if (tokens) {
|
|
404
|
+
updates.TG_BOT_TOKENS = tokens;
|
|
405
|
+
if (tokens.split(',').map((token) => token.trim()).some((token) => !/^\d+:[A-Za-z0-9_-]+$/.test(token))) {
|
|
406
|
+
warnings.push('One or more TG_BOT_TOKENS values do not look like standard Telegram bot tokens.');
|
|
272
407
|
}
|
|
273
408
|
}
|
|
274
409
|
else {
|
|
275
|
-
skipped.push('
|
|
410
|
+
skipped.push('TG_BOT_TOKENS');
|
|
276
411
|
}
|
|
277
412
|
const userId = sanitizeEnvInput(await rl.question('Telegram numeric user ID (TG_ALLOWED_USER_ID): '));
|
|
278
413
|
if (userId) {
|
|
@@ -486,7 +621,7 @@ function runDoctorChecks() {
|
|
|
486
621
|
const checks = [
|
|
487
622
|
['node >= 24', Number(process.versions.node.split('.')[0]) >= 24],
|
|
488
623
|
['codex cli available', hasConfiguredCodexBin(configuredCodexBin) || hasCommand('codex')],
|
|
489
|
-
['telegram bot token configured', Boolean(process.env.TG_BOT_TOKEN)],
|
|
624
|
+
['telegram bot token(s) configured', Boolean(process.env.TG_BOT_TOKENS?.trim() || process.env.TG_BOT_TOKEN?.trim())],
|
|
490
625
|
['telegram allowed user configured', Boolean(process.env.TG_ALLOWED_USER_ID)],
|
|
491
626
|
];
|
|
492
627
|
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,11 @@ 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
|
+
codexAppServer?: RuntimeStatus['codexAppServer'];
|
|
349
|
+
}>;
|
|
343
350
|
}
|
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;
|