@foxden-app/foxclaw 0.5.34 → 0.5.36

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 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.5.36 - 2026-06-17
6
+
7
+ ### 中文
8
+ - `/auth` 的 Telegram RichMessage 候选区从旧的竖线分隔文本整理为横向表格,拆出额度窗口、auth 名称、当前标记、套餐、健康状态、最近刷新、过期时间和风险提示列。
9
+ - ChatGPT auth 候选会从 access token `exp` 读取过期时间,并以 UTC `YYYY-MM-DD HH:mmZ` 展示;拿不到过期字段时保持 `-`,原始文本 fallback 仍可展开查看。
10
+
11
+ ### English
12
+ - Refined the Telegram RichMessage `/auth` candidate area from pipe-delimited text into a horizontally scrollable table with quota windows, auth name, current marker, plan, health, last refresh, expiry, and risk columns.
13
+ - ChatGPT auth candidates now read the access token `exp` value and display expiry as UTC `YYYY-MM-DD HH:mmZ`; candidates without an expiry field show `-`, with the expandable plain-text fallback preserved.
14
+
15
+ ## 0.5.35 - 2026-06-17
16
+
17
+ ### 中文
18
+ - `/status`、`/auth`、`/auth sync status/events/trace` 在 Telegram 上优先使用 RichMessage 展示,内部状态会自动整理成标题、表格、列表和可展开原文。
19
+ - 保留微信和 RichMessage 失败 fallback:微信继续走原文本通道,Telegram rich 发送失败时退回原 HTML/plain 内容,不影响按钮和回调。
20
+
21
+ ### English
22
+ - `/status`, `/auth`, and `/auth sync status/events/trace` now prefer RichMessage on Telegram, rendering internal diagnostics as headings, tables, lists, and expandable plain-text details.
23
+ - Weixin and RichMessage failure fallbacks are preserved: Weixin keeps the previous text path, and Telegram falls back without breaking buttons or callbacks.
24
+
5
25
  ## 0.5.34 - 2026-06-17
6
26
 
7
27
  ### 中文
@@ -229,6 +229,7 @@ export declare class BridgeSessionCore {
229
229
  private sendHtmlMessage;
230
230
  private sendRichHtmlMessage;
231
231
  private sendRichMarkdownMessage;
232
+ private sendRichInternalMessage;
232
233
  private editMessage;
233
234
  private editHtmlMessage;
234
235
  private editRichHtmlMessage;
@@ -3,7 +3,8 @@ import fs from 'node:fs/promises';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { normalizeLocale, t } from '../i18n.js';
6
- import { chatGptAuthMetadataMatchesCandidateName, parseChatGptAuthMetadata, readChatGptAuthMetadata, } from '../auth/mirror.js';
6
+ import { chatGptAuthMetadataMatchesCandidateName, parseChatGptAuthMetadata, readChatGptAuthRecord, readChatGptAuthMetadata, } from '../auth/mirror.js';
7
+ import { readAccessTokenExpiresAtMs } from '../auth/cross_node_sync.js';
7
8
  import { parseCommand } from './commands.js';
8
9
  import { buildAccessSettingsKeyboard, buildModelSettingsKeyboard, buildSetupPanelKeyboard, buildThreadListKeyboard, buildThreadsKeyboard, clampEffortToModel, formatAccessPresetLabel, formatActiveTurnMessageModeLabel, formatAccessSettingsMessage, formatApprovalPolicyLabel, formatCollaborationModeLabel, formatModelSettingsMessage, formatSandboxModeLabel, formatServiceTierStatusLabel, formatSetupPanelMessage, formatThreadContextSummary, formatThreadsMessage, formatWeixinAccessCopyPaste, formatWeixinModelCopyPaste, formatWeixinThreadsCopyPaste, formatWeixinWhereNavCopyPaste, formatWhereMessage, normalizeRequestedEffort, resolveCurrentModel, resolveActiveTurnMessageMode, resolveRequestedModel, } from './presentation.js';
9
10
  import { clampServiceTierToModel, resolveFastTierForModel } from './service_tier.js';
@@ -508,7 +509,7 @@ export class BridgeSessionCore {
508
509
  }
509
510
  lines.push(...codexUsageLines);
510
511
  lines.push(...codexLocalUsageLines);
511
- await this.sendMessage(scopeId, lines.join('\n'));
512
+ await this.sendRichInternalMessage(scopeId, '/status', lines.join('\n'));
512
513
  return;
513
514
  }
514
515
  case 'account': {
@@ -3377,6 +3378,12 @@ export class BridgeSessionCore {
3377
3378
  async sendRichMarkdownMessage(scopeId, text, inlineKeyboard) {
3378
3379
  return this.sendRichHtmlMessage(scopeId, renderTelegramMarkdownRichHtml(text), escapeTelegramHtml(text), inlineKeyboard);
3379
3380
  }
3381
+ async sendRichInternalMessage(scopeId, title, text, inlineKeyboard) {
3382
+ if (scopeId.startsWith(BRIDGE_SCOPE_WEIXIN_PREFIX)) {
3383
+ return this.sendMessage(scopeId, text, inlineKeyboard);
3384
+ }
3385
+ return this.sendRichHtmlMessage(scopeId, formatRichInternalMessage(title, text), escapeTelegramHtml(text), inlineKeyboard);
3386
+ }
3380
3387
  async editMessage(scopeId, messageId, text, inlineKeyboard) {
3381
3388
  await this.messaging.editPlain(scopeId, messageId, text, inlineKeyboard);
3382
3389
  }
@@ -4572,17 +4579,20 @@ export class BridgeSessionCore {
4572
4579
  await this.refreshCurrentCodexAuthQuota(state);
4573
4580
  const record = createPendingAuthChoiceList(scopeId, state.candidates, listRequest);
4574
4581
  this.pendingAuthChoiceLists.set(record.localId, record);
4575
- const messageId = await this.sendMessage(scopeId, renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(scopeId) !== null, record), authChoiceKeyboard(locale, record));
4582
+ const authListMessage = renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(scopeId) !== null, record);
4583
+ const messageId = await this.sendRichInternalMessage(scopeId, '/auth', authListMessage, authChoiceKeyboard(locale, record));
4576
4584
  record.messageId = messageId;
4577
4585
  }
4578
4586
  async handleAuthSyncCommand(scopeId, locale, args) {
4579
4587
  const action = args[0]?.toLowerCase() ?? 'status';
4580
4588
  if (action === 'status') {
4581
- await this.sendMessage(scopeId, formatAuthSyncStatus(locale, this.coordinator?.getAuthSyncStatus?.() ?? null, this.getRuntimeStatus().authProactiveRefresh ?? null));
4589
+ const message = formatAuthSyncStatus(locale, this.coordinator?.getAuthSyncStatus?.() ?? null, this.getRuntimeStatus().authProactiveRefresh ?? null);
4590
+ await this.sendRichInternalMessage(scopeId, '/auth sync status', message);
4582
4591
  return;
4583
4592
  }
4584
4593
  if (action === 'events') {
4585
- await this.sendMessage(scopeId, formatAuthSyncEvents(locale, this.coordinator?.getAuthSyncStatus?.() ?? null, args.slice(1).join(' ').trim() || null));
4594
+ const message = formatAuthSyncEvents(locale, this.coordinator?.getAuthSyncStatus?.() ?? null, args.slice(1).join(' ').trim() || null);
4595
+ await this.sendRichInternalMessage(scopeId, '/auth sync events', message);
4586
4596
  return;
4587
4597
  }
4588
4598
  if (action === 'trace') {
@@ -4591,7 +4601,7 @@ export class BridgeSessionCore {
4591
4601
  await this.sendMessage(scopeId, t(locale, 'auth_sync_trace_missing'));
4592
4602
  return;
4593
4603
  }
4594
- await this.sendMessage(scopeId, formatAuthSyncTrace(locale, this.coordinator?.getAuthSyncStatus?.() ?? null, requestId));
4604
+ await this.sendRichInternalMessage(scopeId, '/auth sync trace', formatAuthSyncTrace(locale, this.coordinator?.getAuthSyncStatus?.() ?? null, requestId));
4595
4605
  return;
4596
4606
  }
4597
4607
  if (action === 'test') {
@@ -6225,19 +6235,20 @@ export class BridgeSessionCore {
6225
6235
  }
6226
6236
  async readCodexAuthCandidateQuotaIdentities(candidates) {
6227
6237
  const entries = await Promise.all(candidates.map(async (candidate) => {
6228
- const metadata = await readChatGptAuthMetadata(candidate.path);
6229
- const metadataMatchesName = metadata
6230
- ? chatGptAuthMetadataMatchesCandidateName(candidate.name, metadata)
6238
+ const record = await readChatGptAuthRecord(candidate.path);
6239
+ const metadataMatchesName = record
6240
+ ? chatGptAuthMetadataMatchesCandidateName(candidate.name, record)
6231
6241
  : false;
6232
- candidate.credentialKind = metadata && metadataMatchesName
6242
+ candidate.credentialKind = record && metadataMatchesName
6233
6243
  ? 'chatgpt'
6234
6244
  : await isCodexApiKeyAuthCandidate(candidate.path)
6235
6245
  ? 'api-key'
6236
6246
  : 'invalid';
6237
- candidate.credentialLastRefreshMs = metadata && metadataMatchesName ? metadata.lastRefreshMs : null;
6238
- return [candidate.name, metadata && metadataMatchesName ? {
6239
- accountId: metadata.accountId,
6240
- quotaIdentityId: metadata.quotaIdentityId,
6247
+ candidate.credentialLastRefreshMs = record && metadataMatchesName ? record.lastRefreshMs : null;
6248
+ candidate.credentialExpiresAtMs = record && metadataMatchesName ? readAccessTokenExpiresAtMs(record.raw) : null;
6249
+ return [candidate.name, record && metadataMatchesName ? {
6250
+ accountId: record.accountId,
6251
+ quotaIdentityId: record.quotaIdentityId,
6241
6252
  } : null];
6242
6253
  }));
6243
6254
  const quotaIdentities = new Map();
@@ -8440,6 +8451,183 @@ function formatRichDemoFallbackMessage(locale) {
8440
8451
  telegramPreCode('sendRichMessage({ rich_message: { html } })', 'typescript'),
8441
8452
  ].join('\n');
8442
8453
  }
8454
+ function formatRichInternalMessage(title, text) {
8455
+ const sections = splitRichInternalSections(text);
8456
+ const body = sections.map(formatRichInternalSection).filter(Boolean).join('\n');
8457
+ return [
8458
+ `<h3>${escapeTelegramHtml(title)}</h3>`,
8459
+ body || `<p>${escapeTelegramHtml(text)}</p>`,
8460
+ telegramDetails('Plain text', telegramPreCode(text, 'text')),
8461
+ '<footer>FoxClaw · RichMessage</footer>',
8462
+ ].join('\n');
8463
+ }
8464
+ function splitRichInternalSections(text) {
8465
+ const sections = [];
8466
+ let current = [];
8467
+ for (const rawLine of text.split('\n')) {
8468
+ const line = rawLine.trimEnd();
8469
+ if (!line.trim()) {
8470
+ if (current.length > 0) {
8471
+ sections.push(current);
8472
+ current = [];
8473
+ }
8474
+ continue;
8475
+ }
8476
+ current.push(line);
8477
+ }
8478
+ if (current.length > 0) {
8479
+ sections.push(current);
8480
+ }
8481
+ return sections;
8482
+ }
8483
+ function formatRichInternalSection(lines) {
8484
+ const candidateRows = lines
8485
+ .map(parseRichAuthCandidateRow)
8486
+ .filter((row) => row !== null);
8487
+ if (candidateRows.length > 0) {
8488
+ const nonCandidateLines = lines.filter(line => parseRichAuthCandidateRow(line) === null);
8489
+ return [
8490
+ nonCandidateLines.length > 0 ? formatRichInternalSectionWithoutCandidates(nonCandidateLines) : '',
8491
+ formatRichAuthCandidateTable(candidateRows),
8492
+ ].filter(Boolean).join('\n');
8493
+ }
8494
+ return formatRichInternalSectionWithoutCandidates(lines);
8495
+ }
8496
+ function formatRichInternalSectionWithoutCandidates(lines) {
8497
+ const rows = lines
8498
+ .map(parseRichInternalKeyValue)
8499
+ .filter((row) => row !== null);
8500
+ if (rows.length >= Math.max(2, Math.floor(lines.length * 0.6))) {
8501
+ return [
8502
+ '<table bordered striped>',
8503
+ ...rows.map(([key, value]) => (`<tr><td>${escapeTelegramHtml(key)}</td><td>${formatRichInternalValue(value)}</td></tr>`)),
8504
+ '</table>',
8505
+ ].join('\n');
8506
+ }
8507
+ return [
8508
+ '<ul>',
8509
+ ...lines.map(line => `<li>${formatRichInternalValue(line.replace(/^\s*[-*]\s+/, '').replace(/^\s*\d+[.)]\s+/, ''))}</li>`),
8510
+ '</ul>',
8511
+ ].join('\n');
8512
+ }
8513
+ function parseRichAuthCandidateRow(line) {
8514
+ const match = line.match(/^\s*(\d+)\.\s+(.+)$/);
8515
+ if (!match) {
8516
+ return null;
8517
+ }
8518
+ let body = match[2].trim();
8519
+ let status = '';
8520
+ const statusMatch = body.match(/^(.*?)\s+(\[[^\]]+\])$/);
8521
+ if (statusMatch) {
8522
+ body = statusMatch[1].trim();
8523
+ status = statusMatch[2].replace(/^\[|\]$/g, '');
8524
+ }
8525
+ const current = body.endsWith(' *');
8526
+ if (current) {
8527
+ body = body.slice(0, -2).trimEnd();
8528
+ }
8529
+ const parts = body.split('|');
8530
+ const name = parts.pop()?.trim() ?? '';
8531
+ if (!name) {
8532
+ return null;
8533
+ }
8534
+ const quotas = parts.map(formatRichAuthQuotaCell);
8535
+ const statusParts = splitRichAuthStatus(status);
8536
+ return {
8537
+ index: match[1],
8538
+ quotaA: quotas[0] ?? '-',
8539
+ quotaB: quotas[1] ?? '-',
8540
+ name,
8541
+ current,
8542
+ ...statusParts,
8543
+ };
8544
+ }
8545
+ function formatRichAuthCandidateTable(rows) {
8546
+ return [
8547
+ '<table bordered striped>',
8548
+ '<tr><th>#</th><th>Quota A</th><th>Quota B</th><th>Auth</th><th>Current</th><th>Plan</th><th>Health</th><th>Last refresh</th><th>Expiry</th><th>Risk</th></tr>',
8549
+ ...rows.map(row => [
8550
+ '<tr>',
8551
+ `<td>${escapeTelegramHtml(row.index)}</td>`,
8552
+ `<td>${escapeTelegramHtml(row.quotaA)}</td>`,
8553
+ `<td>${escapeTelegramHtml(row.quotaB)}</td>`,
8554
+ `<td>${escapeTelegramHtml(row.name)}</td>`,
8555
+ `<td>${row.current ? 'yes' : '-'}</td>`,
8556
+ `<td>${escapeTelegramHtml(row.plan)}</td>`,
8557
+ `<td>${escapeTelegramHtml(row.health)}</td>`,
8558
+ `<td>${escapeTelegramHtml(row.refresh)}</td>`,
8559
+ `<td>${escapeTelegramHtml(row.expiry)}</td>`,
8560
+ `<td>${escapeTelegramHtml(row.risk)}</td>`,
8561
+ '</tr>',
8562
+ ].join('')),
8563
+ '</table>',
8564
+ ].join('\n');
8565
+ }
8566
+ function splitRichAuthStatus(status) {
8567
+ const parts = status.split(' · ').map(part => part.trim()).filter(Boolean);
8568
+ const refreshIndex = parts.findIndex(part => /^(refreshed|刷新于)\b/i.test(part));
8569
+ const refresh = refreshIndex >= 0 ? parts.splice(refreshIndex, 1)[0] : '-';
8570
+ const expiryIndex = parts.findIndex(part => /^(expires|过期于)\b/i.test(part));
8571
+ const expiry = expiryIndex >= 0 ? parts.splice(expiryIndex, 1)[0] : '-';
8572
+ const plan = parts.length > 1 ? parts.shift() : '-';
8573
+ const health = parts.shift() ?? '-';
8574
+ return {
8575
+ plan,
8576
+ health,
8577
+ refresh,
8578
+ expiry,
8579
+ risk: formatRichAuthRisk(health),
8580
+ };
8581
+ }
8582
+ function formatRichAuthRisk(health) {
8583
+ if (/not recently refreshed|长期未刷新/i.test(health)) {
8584
+ return `stale >${CODEX_AUTH_STALE_CREDENTIAL_DAYS}d`;
8585
+ }
8586
+ if (/needs login repair|需要登录修复/i.test(health)) {
8587
+ return 'repair';
8588
+ }
8589
+ if (/quota exhausted|额度耗尽/i.test(health)) {
8590
+ return 'quota exhausted';
8591
+ }
8592
+ if (/invalid|无效/i.test(health)) {
8593
+ return 'invalid';
8594
+ }
8595
+ if (/\blow\b|偏低|低/i.test(health)) {
8596
+ return 'low quota';
8597
+ }
8598
+ if (/unknown|未知/i.test(health)) {
8599
+ return 'unknown';
8600
+ }
8601
+ return '-';
8602
+ }
8603
+ function formatRichAuthQuotaCell(value) {
8604
+ const trimmed = value.trim();
8605
+ if (!trimmed || trimmed === '--' || trimmed === '—') {
8606
+ return '-';
8607
+ }
8608
+ const [windowLabel, percent] = trimmed.split(':');
8609
+ if (!windowLabel || percent === undefined) {
8610
+ return trimmed;
8611
+ }
8612
+ return `${windowLabel} ${percent}%`;
8613
+ }
8614
+ function parseRichInternalKeyValue(line) {
8615
+ const separator = line.includes(':') ? ':' : ':';
8616
+ const index = line.indexOf(separator);
8617
+ if (index <= 0) {
8618
+ return null;
8619
+ }
8620
+ const key = line.slice(0, index).trim();
8621
+ const value = line.slice(index + separator.length).trim();
8622
+ if (!key || !value || key.length > 80) {
8623
+ return null;
8624
+ }
8625
+ return [key, value];
8626
+ }
8627
+ function formatRichInternalValue(value) {
8628
+ const escaped = escapeTelegramHtml(value);
8629
+ return escaped.replace(/`([^`]+)`/g, '<code>$1</code>');
8630
+ }
8443
8631
  function clipRichMessageText(value, limit) {
8444
8632
  if (value.length <= limit) {
8445
8633
  return value;
@@ -9197,6 +9385,7 @@ async function listCodexAuthState(disabledNames = new Set(), candidateStates = n
9197
9385
  mtimeMs: stat.mtimeMs,
9198
9386
  credentialKind: 'invalid',
9199
9387
  credentialLastRefreshMs: null,
9388
+ credentialExpiresAtMs: null,
9200
9389
  quota: null,
9201
9390
  });
9202
9391
  }
@@ -9541,6 +9730,11 @@ function formatCodexAuthCandidateStatus(locale, candidate) {
9541
9730
  value: formatCompactAge(locale, Date.now() - candidate.credentialLastRefreshMs),
9542
9731
  }));
9543
9732
  }
9733
+ if (candidate.credentialExpiresAtMs !== null) {
9734
+ details.push(t(locale, 'auth_candidate_expires_at', {
9735
+ value: formatUtcDateTime(candidate.credentialExpiresAtMs),
9736
+ }));
9737
+ }
9544
9738
  return `[${details.join(' · ')}]`;
9545
9739
  }
9546
9740
  function formatCodexAuthFilter(locale, filter) {
@@ -10387,6 +10581,12 @@ function formatCompactAge(locale, ageMs) {
10387
10581
  const roundedMinutes = Math.floor(minutes);
10388
10582
  return locale === 'zh' ? `${roundedMinutes}分钟前` : `${roundedMinutes}m ago`;
10389
10583
  }
10584
+ function formatUtcDateTime(timestampMs) {
10585
+ if (!Number.isFinite(timestampMs)) {
10586
+ return '-';
10587
+ }
10588
+ return `${new Date(timestampMs).toISOString().slice(0, 16).replace('T', ' ')}Z`;
10589
+ }
10390
10590
  function formatUsagePercent(value) {
10391
10591
  if (!Number.isFinite(value)) {
10392
10592
  return '?';
package/dist/i18n.d.ts CHANGED
@@ -173,6 +173,7 @@ declare const MESSAGES: {
173
173
  readonly 'auth_candidate_health_api-key': "API key";
174
174
  readonly auth_candidate_health_invalid: "invalid auth file";
175
175
  readonly auth_candidate_last_refresh: "refreshed {value}";
176
+ readonly auth_candidate_expires_at: "expires {value}";
176
177
  readonly auth_candidate_enabled: "Enabled auth candidate for auto-rotation: {value}";
177
178
  readonly auth_candidate_disabled: "Disabled auth candidate for auto-rotation: {value}";
178
179
  readonly auth_candidate_enabled_short: "Auth enabled";
@@ -871,6 +872,7 @@ declare const MESSAGES: {
871
872
  readonly 'auth_candidate_health_api-key': "API key";
872
873
  readonly auth_candidate_health_invalid: "auth 文件无效";
873
874
  readonly auth_candidate_last_refresh: "刷新于 {value}";
875
+ readonly auth_candidate_expires_at: "过期于 {value}";
874
876
  readonly auth_candidate_enabled: "已启用 auth 候选自动轮换:{value}";
875
877
  readonly auth_candidate_disabled: "已禁用 auth 候选自动轮换:{value}";
876
878
  readonly auth_candidate_enabled_short: "auth 已启用";
package/dist/i18n.js CHANGED
@@ -171,6 +171,7 @@ const MESSAGES = {
171
171
  'auth_candidate_health_api-key': 'API key',
172
172
  auth_candidate_health_invalid: 'invalid auth file',
173
173
  auth_candidate_last_refresh: 'refreshed {value}',
174
+ auth_candidate_expires_at: 'expires {value}',
174
175
  auth_candidate_enabled: 'Enabled auth candidate for auto-rotation: {value}',
175
176
  auth_candidate_disabled: 'Disabled auth candidate for auto-rotation: {value}',
176
177
  auth_candidate_enabled_short: 'Auth enabled',
@@ -869,6 +870,7 @@ const MESSAGES = {
869
870
  'auth_candidate_health_api-key': 'API key',
870
871
  auth_candidate_health_invalid: 'auth 文件无效',
871
872
  auth_candidate_last_refresh: '刷新于 {value}',
873
+ auth_candidate_expires_at: '过期于 {value}',
872
874
  auth_candidate_enabled: '已启用 auth 候选自动轮换:{value}',
873
875
  auth_candidate_disabled: '已禁用 auth 候选自动轮换:{value}',
874
876
  auth_candidate_enabled_short: 'auth 已启用',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foxden-app/foxclaw",
3
- "version": "0.5.34",
3
+ "version": "0.5.36",
4
4
  "description": "Foxden local execution claw for controlling Codex from trusted chat interfaces.",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",