@foxden-app/foxclaw 0.6.3 → 0.6.5
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/CHANGELOG.md +20 -0
- package/dist/controller/controller.js +28 -0
- package/dist/telegram/gateway.d.ts +1 -0
- package/dist/telegram/gateway.js +19 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
All notable FoxClaw changes are listed here. Each release note is bilingual so GitHub Releases and the npm package are useful to both Chinese and English readers.
|
|
4
4
|
|
|
5
|
+
## 0.6.5 - 2026-08-10
|
|
6
|
+
|
|
7
|
+
### 中文
|
|
8
|
+
- Telegram bot 身份现在直接从标准 token 的数字前缀获得,FoxClaw 启动不再依赖远端 `getMe` 成功;断网重启时服务会保持运行,也不会因 systemd 连续重试而进入 `Start request repeated too quickly`。
|
|
9
|
+
- `getMe`、命令菜单注册和消息轮询统一在后台恢复循环中执行。Telegram 恢复后会自动完成初始化并继续收发消息,无需人工重启;Telegram 可用而 Codex 上游不可用时,既有桥接错误仍会正常发回聊天。
|
|
10
|
+
|
|
11
|
+
### English
|
|
12
|
+
- Telegram bot identity is now derived from the numeric prefix of a standard token, so FoxClaw startup no longer depends on a successful remote `getMe` call. The service stays alive across offline restarts instead of exhausting systemd retries with `Start request repeated too quickly`.
|
|
13
|
+
- `getMe`, command registration, and update polling now recover through the same background loop. Telegram automatically resumes without a manual restart, while existing bridge errors remain chat-visible when Telegram is reachable but the Codex upstream is unavailable.
|
|
14
|
+
|
|
15
|
+
## 0.6.4 - 2026-08-09
|
|
16
|
+
|
|
17
|
+
### 中文
|
|
18
|
+
- 将 Codex 上游 `willRetry: true` 的 `responseStreamDisconnected` 识别为自动恢复中的传输事件,不再把 `Reconnecting... n/5` 写入 Telegram 最终回复或桥接错误状态。
|
|
19
|
+
- WebSocket 重试耗尽后自动切换 HTTPS 的 `Falling back from WebSockets to HTTPS transport` 提示改为仅记录内部 info 日志,不再单独打扰聊天;若 HTTPS 最终仍失败且 `willRetry: false`,真实错误继续正常展示。
|
|
20
|
+
|
|
21
|
+
### English
|
|
22
|
+
- Treats Codex `responseStreamDisconnected` events with `willRetry: true` as in-progress transport recovery, so `Reconnecting... n/5` no longer replaces Telegram output or becomes the bridge's last error.
|
|
23
|
+
- Records the automatic `Falling back from WebSockets to HTTPS transport` notice as internal info instead of a separate chat warning. A final HTTPS failure with `willRetry: false` remains user-visible.
|
|
24
|
+
|
|
5
25
|
## 0.6.3 - 2026-08-09
|
|
6
26
|
|
|
7
27
|
### 中文
|
|
@@ -1425,6 +1425,14 @@ export class BridgeSessionCore {
|
|
|
1425
1425
|
this.updateStatus();
|
|
1426
1426
|
}
|
|
1427
1427
|
async handleCodexErrorNotification(params) {
|
|
1428
|
+
if (isRetryableCodexTransportError(params)) {
|
|
1429
|
+
this.logger.info('codex.transport.retrying', {
|
|
1430
|
+
message: stringOrNull(params?.error?.message),
|
|
1431
|
+
threadId: stringOrNull(params?.threadId),
|
|
1432
|
+
turnId: stringOrNull(params?.turnId),
|
|
1433
|
+
});
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1428
1436
|
const message = formatCodexNotificationError(params);
|
|
1429
1437
|
this.lastError = message;
|
|
1430
1438
|
this.logger.error('codex.notification.error', params);
|
|
@@ -1859,6 +1867,13 @@ export class BridgeSessionCore {
|
|
|
1859
1867
|
await this.notifyBoundScopes(message);
|
|
1860
1868
|
}
|
|
1861
1869
|
async handleBridgeWarningNotification(method, params) {
|
|
1870
|
+
if (isCodexTransportFallbackWarning(method, params)) {
|
|
1871
|
+
this.logger.info('codex.transport.fallback', {
|
|
1872
|
+
message: stringOrNull(params?.message),
|
|
1873
|
+
threadId: stringOrNull(params?.threadId),
|
|
1874
|
+
});
|
|
1875
|
+
return;
|
|
1876
|
+
}
|
|
1862
1877
|
const threadId = stringOrNull(params?.threadId);
|
|
1863
1878
|
const scopeId = threadId ? this.findChatByThread(threadId) : null;
|
|
1864
1879
|
const locale = scopeId ? this.localeForChat(scopeId) : 'en';
|
|
@@ -9856,6 +9871,12 @@ function formatWarningNotification(locale, method, params) {
|
|
|
9856
9871
|
}
|
|
9857
9872
|
return `${t(locale, 'warning_title')}\n${String(params?.message ?? t(locale, 'unknown'))}`;
|
|
9858
9873
|
}
|
|
9874
|
+
function isCodexTransportFallbackWarning(method, params) {
|
|
9875
|
+
if (method !== 'warning') {
|
|
9876
|
+
return false;
|
|
9877
|
+
}
|
|
9878
|
+
return /falling back from websockets? to https transport/i.test(String(params?.message ?? ''));
|
|
9879
|
+
}
|
|
9859
9880
|
function normalizeThreadStatusLabel(raw) {
|
|
9860
9881
|
if (typeof raw === 'string') {
|
|
9861
9882
|
return raw;
|
|
@@ -10933,6 +10954,13 @@ function formatCodexNotificationError(params) {
|
|
|
10933
10954
|
}
|
|
10934
10955
|
return clipUserFacingError(cleanUserFacingError(JSON.stringify(params?.error ?? params ?? {})));
|
|
10935
10956
|
}
|
|
10957
|
+
function isRetryableCodexTransportError(params) {
|
|
10958
|
+
if (params?.willRetry !== true) {
|
|
10959
|
+
return false;
|
|
10960
|
+
}
|
|
10961
|
+
const errorInfo = params?.error?.codexErrorInfo;
|
|
10962
|
+
return Boolean(errorInfo && typeof errorInfo === 'object' && 'responseStreamDisconnected' in errorInfo);
|
|
10963
|
+
}
|
|
10936
10964
|
function collectCodexErrorText(params) {
|
|
10937
10965
|
const parts = [
|
|
10938
10966
|
stringOrNull(params?.error?.message),
|
package/dist/telegram/gateway.js
CHANGED
|
@@ -27,7 +27,10 @@ export class TelegramGateway extends EventEmitter {
|
|
|
27
27
|
this.logger = logger;
|
|
28
28
|
this.namespacedScopes = namespacedScopes;
|
|
29
29
|
this.commandProvider = commandProvider;
|
|
30
|
-
|
|
30
|
+
const tokenHash = crypto.createHash('sha256').update(this.botToken).digest('hex').slice(0, 8);
|
|
31
|
+
const tokenBotId = parseTelegramBotId(this.botToken);
|
|
32
|
+
this.botUserId = tokenBotId;
|
|
33
|
+
this.botKey = tokenBotId === null ? `telegram:${tokenHash}` : `telegram:bot${tokenBotId}`;
|
|
31
34
|
}
|
|
32
35
|
get username() {
|
|
33
36
|
return this.botUsername;
|
|
@@ -36,6 +39,8 @@ export class TelegramGateway extends EventEmitter {
|
|
|
36
39
|
return this.botUserId === null ? null : `bot${this.botUserId}`;
|
|
37
40
|
}
|
|
38
41
|
async initializeIdentity() {
|
|
42
|
+
if (this.identity)
|
|
43
|
+
return this.identity;
|
|
39
44
|
await this.resolveBotIdentity(true);
|
|
40
45
|
return this.identity;
|
|
41
46
|
}
|
|
@@ -43,10 +48,6 @@ export class TelegramGateway extends EventEmitter {
|
|
|
43
48
|
if (this.running)
|
|
44
49
|
return;
|
|
45
50
|
this.running = true;
|
|
46
|
-
if (this.botUserId === null) {
|
|
47
|
-
await this.resolveBotIdentity(this.namespacedScopes);
|
|
48
|
-
}
|
|
49
|
-
await this.registerCommands();
|
|
50
51
|
void this.pollLoop();
|
|
51
52
|
}
|
|
52
53
|
stop() {
|
|
@@ -238,8 +239,14 @@ export class TelegramGateway extends EventEmitter {
|
|
|
238
239
|
});
|
|
239
240
|
}
|
|
240
241
|
async pollLoop() {
|
|
242
|
+
let remoteInitialized = false;
|
|
241
243
|
while (this.running) {
|
|
242
244
|
try {
|
|
245
|
+
if (!remoteInitialized) {
|
|
246
|
+
await this.resolveBotIdentity(true);
|
|
247
|
+
await this.registerCommands();
|
|
248
|
+
remoteInitialized = true;
|
|
249
|
+
}
|
|
243
250
|
const offset = this.store.getTelegramOffset(this.botKey) + 1;
|
|
244
251
|
const result = await callTelegramApi(this.botToken, 'getUpdates', {
|
|
245
252
|
timeout: Math.max(1, Math.floor(this.pollIntervalMs / 1000)),
|
|
@@ -340,6 +347,13 @@ export class TelegramGateway extends EventEmitter {
|
|
|
340
347
|
function sleep(ms) {
|
|
341
348
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
342
349
|
}
|
|
350
|
+
export function parseTelegramBotId(botToken) {
|
|
351
|
+
const match = /^(\d+):/.exec(botToken.trim());
|
|
352
|
+
if (!match)
|
|
353
|
+
return null;
|
|
354
|
+
const botId = Number(match[1]);
|
|
355
|
+
return Number.isSafeInteger(botId) && botId > 0 ? botId : null;
|
|
356
|
+
}
|
|
343
357
|
function extractAttachments(message) {
|
|
344
358
|
const attachments = [];
|
|
345
359
|
const largestPhoto = pickLargestPhoto(message.photo ?? []);
|