@foxden-app/foxclaw 0.11.1 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/.env.example +18 -1
  2. package/CHANGELOG.md +36 -0
  3. package/README.md +4 -3
  4. package/README_EN.md +4 -3
  5. package/dist/antigravity/account_ui.d.ts +34 -0
  6. package/dist/antigravity/account_ui.js +354 -0
  7. package/dist/antigravity/adapter.js +1 -1
  8. package/dist/antigravity/auth.d.ts +4 -1
  9. package/dist/antigravity/auth.js +17 -5
  10. package/dist/antigravity/controller.d.ts +3 -106
  11. package/dist/antigravity/controller.js +2 -2083
  12. package/dist/antigravity/conversation_ui.d.ts +56 -0
  13. package/dist/antigravity/conversation_ui.js +509 -0
  14. package/dist/antigravity/ui_callbacks.d.ts +6 -0
  15. package/dist/antigravity/ui_callbacks.js +6 -0
  16. package/dist/bridge/unified_bridge.d.ts +114 -0
  17. package/dist/bridge/unified_bridge.js +1343 -0
  18. package/dist/channels/bridge_messaging_router.d.ts +8 -4
  19. package/dist/channels/bridge_messaging_router.js +15 -4
  20. package/dist/channels/telegram/telegram_messaging_port.d.ts +16 -0
  21. package/dist/channels/telegram/telegram_messaging_port.js +22 -2
  22. package/dist/channels/text_actions.d.ts +14 -0
  23. package/dist/channels/text_actions.js +48 -0
  24. package/dist/channels/weixin/weixin_channel_adapter.d.ts +3 -4
  25. package/dist/channels/weixin/weixin_channel_adapter.js +2 -2
  26. package/dist/channels/weixin/weixin_messaging_port.d.ts +16 -1
  27. package/dist/channels/weixin/weixin_messaging_port.js +34 -7
  28. package/dist/codex_app/activity.d.ts +62 -0
  29. package/dist/codex_app/activity.js +418 -0
  30. package/dist/codex_app/adapter.d.ts +2 -2
  31. package/dist/codex_app/adapter.js +55 -25
  32. package/dist/codex_app/backend_ui.d.ts +4 -0
  33. package/dist/codex_app/backend_ui.js +48 -0
  34. package/dist/codex_app/client.js +0 -12
  35. package/dist/config.d.ts +4 -0
  36. package/dist/config.js +41 -5
  37. package/dist/controller/access.d.ts +1 -11
  38. package/dist/controller/access.js +1 -33
  39. package/dist/controller/activity.d.ts +1 -62
  40. package/dist/controller/activity.js +2 -418
  41. package/dist/controller/auth_errors.d.ts +11 -0
  42. package/dist/controller/auth_errors.js +124 -0
  43. package/dist/controller/auth_files.d.ts +13 -0
  44. package/dist/controller/auth_files.js +156 -0
  45. package/dist/controller/auth_presentation.d.ts +77 -0
  46. package/dist/controller/auth_presentation.js +697 -0
  47. package/dist/controller/auth_quota_service.d.ts +27 -0
  48. package/dist/controller/auth_quota_service.js +151 -0
  49. package/dist/controller/controller.d.ts +30 -56
  50. package/dist/controller/controller.js +181 -3497
  51. package/dist/controller/interactions.d.ts +74 -0
  52. package/dist/controller/interactions.js +560 -0
  53. package/dist/controller/local_usage_service.d.ts +19 -0
  54. package/dist/controller/local_usage_service.js +62 -0
  55. package/dist/controller/native_panel_service.d.ts +55 -0
  56. package/dist/controller/native_panel_service.js +214 -0
  57. package/dist/controller/native_panels.d.ts +61 -0
  58. package/dist/controller/native_panels.js +784 -0
  59. package/dist/controller/shared_helpers.d.ts +53 -0
  60. package/dist/controller/shared_helpers.js +399 -0
  61. package/dist/controller/state_types.d.ts +390 -0
  62. package/dist/controller/state_types.js +82 -0
  63. package/dist/controller/turn_rendering.d.ts +22 -0
  64. package/dist/controller/turn_rendering.js +288 -0
  65. package/dist/core/access.d.ts +11 -0
  66. package/dist/core/access.js +33 -0
  67. package/dist/core/attachment_files.d.ts +13 -0
  68. package/dist/core/attachment_files.js +180 -0
  69. package/dist/core/attachment_types.d.ts +22 -0
  70. package/dist/core/attachment_types.js +1 -0
  71. package/dist/core/attachments.d.ts +5 -3
  72. package/dist/core/attachments.js +2 -2
  73. package/dist/core/backend_registry.d.ts +22 -0
  74. package/dist/core/backend_registry.js +50 -0
  75. package/dist/core/backend_ui.d.ts +28 -0
  76. package/dist/core/backend_ui.js +1 -0
  77. package/dist/core/channel_events.d.ts +48 -0
  78. package/dist/core/channel_events.js +1 -0
  79. package/dist/core/channel_port.d.ts +52 -8
  80. package/dist/core/engine_spi.d.ts +20 -5
  81. package/dist/core/listener_scope.d.ts +11 -0
  82. package/dist/core/listener_scope.js +10 -0
  83. package/dist/core/message_text.d.ts +2 -0
  84. package/dist/core/message_text.js +28 -0
  85. package/dist/core/network_errors.d.ts +1 -0
  86. package/dist/core/network_errors.js +23 -0
  87. package/dist/core/orchestrator.d.ts +57 -22
  88. package/dist/core/orchestrator.js +877 -477
  89. package/dist/core/queued_engine_input.d.ts +12 -0
  90. package/dist/core/queued_engine_input.js +16 -0
  91. package/dist/core/runtime_supervisor.d.ts +19 -0
  92. package/dist/core/runtime_supervisor.js +45 -0
  93. package/dist/core/scope_operations.d.ts +7 -0
  94. package/dist/core/scope_operations.js +19 -0
  95. package/dist/core/stream_preview.js +21 -9
  96. package/dist/dsh/adapter.d.ts +48 -0
  97. package/dist/dsh/adapter.js +205 -0
  98. package/dist/dsh/backend.d.ts +7 -0
  99. package/dist/dsh/backend.js +21 -0
  100. package/dist/dsh/client.d.ts +46 -0
  101. package/dist/dsh/client.js +184 -0
  102. package/dist/dsh/permissions_plugin.d.ts +30 -0
  103. package/dist/dsh/permissions_plugin.js +21 -0
  104. package/dist/dsh/runtime.d.ts +20 -0
  105. package/dist/dsh/runtime.js +36 -0
  106. package/dist/dsh/ui.d.ts +38 -0
  107. package/dist/dsh/ui.js +342 -0
  108. package/dist/i18n.d.ts +4 -0
  109. package/dist/i18n.js +12 -0
  110. package/dist/main.js +144 -72
  111. package/dist/opencode/adapter.js +28 -9
  112. package/dist/opencode/controller.js +1 -1
  113. package/dist/store/channel_inbox.d.ts +14 -0
  114. package/dist/store/channel_inbox.js +21 -0
  115. package/dist/store/database.d.ts +10 -4
  116. package/dist/store/database.js +13 -3
  117. package/dist/store/task_journal.d.ts +32 -0
  118. package/dist/store/task_journal.js +70 -0
  119. package/dist/telegram/api.d.ts +2 -2
  120. package/dist/telegram/api.js +10 -28
  121. package/dist/telegram/gateway.d.ts +13 -27
  122. package/dist/telegram/gateway.js +86 -43
  123. package/dist/telegram/media.d.ts +2 -34
  124. package/dist/telegram/media.js +1 -180
  125. package/dist/telegram/task_preview.d.ts +13 -0
  126. package/dist/telegram/task_preview.js +92 -0
  127. package/dist/telegram/task_result.d.ts +2 -0
  128. package/dist/telegram/task_result.js +12 -0
  129. package/dist/telegram/text.d.ts +1 -1
  130. package/dist/telegram/text.js +2 -27
  131. package/dist/types.d.ts +2 -2
  132. package/docs/dsh.md +83 -0
  133. package/docs/release.md +13 -11
  134. package/docs/zh/architecture-review.md +106 -0
  135. package/docs/zh/dsh.md +83 -0
  136. package/docs/zh/release.md +13 -11
  137. package/docs/zh/telegram-experience.md +40 -0
  138. package/package.json +7 -5
@@ -1,115 +1,41 @@
1
- import crypto from 'node:crypto';
2
- import fs from 'node:fs/promises';
3
- import os from 'node:os';
4
- import path from 'node:path';
1
+ import { ListenerScope } from '../core/listener_scope.js';
2
+ import { CodexLocalUsageService } from './local_usage_service.js';
3
+ import { CodexAuthQuotaService } from './auth_quota_service.js';
4
+ import { CodexNativePanelService } from './native_panel_service.js';
5
5
  import { externalWriterControl } from '../codex_app/force_takeover.js';
6
- import { TELEGRAM_VOICE_MAX_BYTES, TELEGRAM_VOICE_SUPPORTED_EXTENSIONS, telegramVoiceContentType, } from '../voice/files.js';
7
- import { normalizeLocale, t } from '../i18n.js';
8
- import { chatGptAuthMetadataMatchesCandidateName, parseChatGptAuthMetadata, readChatGptAuthRecord, readChatGptAuthMetadata, } from '../auth/mirror.js';
9
- import { readAccessTokenExpiresAtMs } from '../auth/cross_node_sync.js';
10
- import { parseCommand } from './commands.js';
11
- import { shouldShowRuntimeLastUpdate } from '../update_status.js';
12
6
  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';
13
- import { clampServiceTierToModel, resolveFastTierForModel } from './service_tier.js';
7
+ import { parseCommand } from './commands.js';
8
+ import path from 'node:path';
9
+ import os from 'node:os';
10
+ import { BRIDGE_SCOPE_WEIXIN_PREFIX, parseTelegramTargetFromBridgeScope, parseWeixinBridgeScope } from '../core/bridge_scope.js';
14
11
  import { TELEGRAM_BOT_API_DOWNLOAD_LIMIT_BYTES, buildAttachmentPrompt, isNativeImageAttachment, planAttachmentStoragePath, summarizeTelegramInput, } from '../telegram/media.js';
15
- import { TELEGRAM_MESSAGE_LIMIT, chunkTelegramMessage, chunkTelegramStreamMessage, clipTelegramDraftMessage, } from '../telegram/text.js';
16
- import { escapeTelegramHtml, telegramBold, telegramDetails, telegramExpandableBlockquote, telegramPre, telegramPreCode, } from '../telegram/html.js';
17
- import { TELEGRAM_RICH_MESSAGE_TEXT_LIMIT } from '../telegram/rich.js';
18
- import { renderTelegramMarkdownRichHtml } from '../telegram/rich_markdown.js';
12
+ import { normalizeLocale, t } from '../i18n.js';
19
13
  import { isDefaultTelegramScope, resolveTelegramAddressing } from '../telegram/addressing.js';
20
- import { BRIDGE_SCOPE_WEIXIN_PREFIX, parseTelegramTargetFromBridgeScope, parseWeixinBridgeScope } from '../core/bridge_scope.js';
21
- import { resolveTelegramRenderRoute } from '../telegram/rendering.js';
22
- import { normalizeVoiceText, synthesizeTelegramVoice } from '../voice/tts.js';
23
- import { readCodexLocalUsageSnapshot, readCodexLocalUsageStats, writeCodexLocalUsageSnapshot, } from '../codex_app/local_usage.js';
24
- import { normalizeTurnActivityEvent, } from './activity.js';
14
+ import { shouldShowRuntimeLastUpdate } from '../update_status.js';
25
15
  import { normalizeAccessPreset, resolveAccessMode } from './access.js';
16
+ import { normalizeTurnActivityEvent } from './activity.js';
17
+ import fs from 'node:fs/promises';
18
+ import { chatGptAuthMetadataMatchesCandidateName, parseChatGptAuthMetadata, readChatGptAuthRecord, readChatGptAuthMetadata, } from '../auth/mirror.js';
19
+ import crypto from 'node:crypto';
20
+ import { clampServiceTierToModel, resolveFastTierForModel } from './service_tier.js';
21
+ import { resolveTelegramRenderRoute } from '../telegram/rendering.js';
22
+ import { TELEGRAM_MESSAGE_LIMIT, chunkTelegramMessage, chunkTelegramStreamMessage, clipTelegramDraftMessage } from '../telegram/text.js';
23
+ import { writeRuntimeStatus } from '../runtime.js';
24
+ import { renderTelegramMarkdownRichHtml } from '../telegram/rich_markdown.js';
25
+ import { escapeTelegramHtml, telegramBold, telegramDetails, telegramPre } from '../telegram/html.js';
26
26
  import { diffObservedTurn, findLatestTurn, findLiveTurn } from './observer.js';
27
- import { applySessionLog, bootstrapSessionLog, splitJsonlChunk, } from './session_observer.js';
27
+ import { applySessionLog, bootstrapSessionLog, splitJsonlChunk } from './session_observer.js';
28
+ import { TELEGRAM_VOICE_MAX_BYTES, TELEGRAM_VOICE_SUPPORTED_EXTENSIONS, telegramVoiceContentType } from '../voice/files.js';
29
+ import { normalizeVoiceText, synthesizeTelegramVoice } from '../voice/tts.js';
28
30
  import { renderActiveTurnStatus } from './status.js';
29
- import { formatMetricTokenCount } from '../store/token_usage.js';
30
- import { writeRuntimeStatus } from '../runtime.js';
31
- const AUTH_DELETE_REASON_NEEDS_REPAIR = 'needs_repair';
32
- const RESTART_PREVIEW_RECOVERY_RETRY_DELAYS_MS = [3000, 7000, 15_000, 30_000, 45_000, 60_000, 60_000, 60_000];
33
- const TOOL_ARCHIVE_MAX_LINES = 12;
34
- const TOOL_ARCHIVE_LINE_LIMIT = 240;
35
- class UserFacingError extends Error {
36
- }
37
- const OBSERVED_THREAD_POLL_MS = 1500;
38
- const OBSERVED_CLI_USER_LABEL = 'codex-cli-user';
39
- const DEFAULT_COLLABORATION_MODE = 'default';
40
- const CODEX_LOCAL_USAGE_REFRESH_MS = 30 * 60_000;
41
- const CODEX_LOCAL_USAGE_SNAPSHOT_FILENAME = 'codex-local-usage.json';
42
- const CODEX_AUTH_QUOTA_SNAPSHOT_FILENAME = 'codex-auth-quota.json';
43
- const CODEX_AUTH_LIST_PAGE_SIZE = 8;
44
- const CODEX_AUTH_LOW_QUOTA_PERCENT = 10;
45
- const CODEX_AUTH_STALE_CREDENTIAL_DAYS = 8;
46
- const CODEX_AUTH_PROACTIVE_REFRESH_DAYS = 8;
47
- const CODEX_AUTH_PROACTIVE_REFRESH_INTERVAL_MS = 60 * 60_000;
48
- const CODEX_AUTH_PROACTIVE_REFRESH_INITIAL_DELAY_MS = 5 * 60_000;
49
- const USER_INPUT_SUBMITTED_NOTICE_MS = 90_000;
50
- const SELF_UPDATE_STATUS_POLL_MS = 1000;
51
- const ATTACHMENT_BATCH_MERGE_WINDOW_MS = 120_000;
52
- const PLAN_IMPLEMENTATION_CODING_MESSAGE = 'Implement the plan.';
53
- const PLAN_IMPLEMENTATION_CLEAR_CONTEXT_PREFIX = 'A previous agent produced the plan below to accomplish the user\'s task. Implement the plan in a fresh context. Treat the plan as the source of user intent, re-read files as needed, and carry the work through implementation and verification.';
54
- const PINNED_HELP_COMMANDS = [
55
- { key: 'help', line: '/help' },
56
- { key: 'setup', line: '/setup' },
57
- { key: 'status', line: '/status' },
58
- { key: 'threads', line: '/threads [query]' },
59
- { key: 'auth', line: '/auth' },
60
- ];
61
- const DYNAMIC_HELP_COMMANDS = [
62
- { key: 'fast', line: '/fast <on|off|toggle>' },
63
- { key: 'active', line: '/active <steer|queue>' },
64
- { key: 'rich', line: '/rich' },
65
- { key: 'account', line: '/account' },
66
- { key: 'quota', line: '/quota' },
67
- { key: 'update', line: '/update' },
68
- { key: 'login_device', line: '/login_device' },
69
- { key: 'login_cancel', line: '/login_cancel' },
70
- { key: 'cli', line: '/cli' },
71
- { key: 'threads_archived', line: '/threads archived [query]' },
72
- { key: 'open', line: '/open <n>' },
73
- { key: 'goal', line: '/goal [objective|pause|resume|done|budget <tokens|off>|clear confirm]' },
74
- { key: 'history', line: '/history [limit]' },
75
- { key: 'files', line: '/files <query>' },
76
- { key: 'remote', line: '/remote' },
77
- { key: 'watch', line: '/watch' },
78
- { key: 'unwatch', line: '/unwatch' },
79
- { key: 'steer', line: '/steer <message>' },
80
- { key: 'takeover', line: '/takeover <message>' },
81
- { key: 'queue', line: '/queue <message>' },
82
- { key: 'new', line: '/new [cwd]' },
83
- { key: 'mode', line: '/mode [default|plan]' },
84
- { key: 'plan', line: '/plan' },
85
- { key: 'agent', line: '/agent' },
86
- { key: 'auth_reload', line: '/auth_reload' },
87
- { key: 'logout', line: '/logout confirm' },
88
- { key: 'loaded', line: '/loaded' },
89
- { key: 'skills', line: '/skills [query]' },
90
- { key: 'skill', line: '/skill <name>' },
91
- { key: 'hooks', line: '/hooks' },
92
- { key: 'plugins', line: '/plugins [query]' },
93
- { key: 'plugin', line: '/plugin <name>' },
94
- { key: 'apps', line: '/apps' },
95
- { key: 'features', line: '/features' },
96
- { key: 'config', line: '/config' },
97
- { key: 'requirements', line: '/requirements' },
98
- { key: 'provider', line: '/provider' },
99
- { key: 'mcp', line: '/mcp' },
100
- { key: 'review', line: '/review' },
101
- { key: 'fork', line: '/fork [name]' },
102
- { key: 'undo', line: '/undo [n]' },
103
- { key: 'rename', line: '/rename <name>' },
104
- { key: 'compact', line: '/compact' },
105
- { key: 'archive', line: '/archive' },
106
- { key: 'models', line: '/models' },
107
- { key: 'permissions', line: '/permissions' },
108
- { key: 'permissions_arg', line: '/permissions <read-only|default|full-access>' },
109
- { key: 'reveal', line: '/reveal' },
110
- { key: 'where', line: '/where' },
111
- { key: 'interrupt', line: '/interrupt' },
112
- ];
31
+ import { CODEX_AUTH_PROACTIVE_REFRESH_INITIAL_DELAY_MS, DEFAULT_COLLABORATION_MODE, DYNAMIC_HELP_COMMANDS, PINNED_HELP_COMMANDS, PLAN_IMPLEMENTATION_CLEAR_CONTEXT_PREFIX, PLAN_IMPLEMENTATION_CODING_MESSAGE, UserFacingError, OBSERVED_CLI_USER_LABEL, USER_INPUT_SUBMITTED_NOTICE_MS, OBSERVED_THREAD_POLL_MS, SELF_UPDATE_STATUS_POLL_MS, CODEX_AUTH_PROACTIVE_REFRESH_INTERVAL_MS, CODEX_AUTH_PROACTIVE_REFRESH_DAYS, AUTH_DELETE_REASON_NEEDS_REPAIR, RESTART_PREVIEW_RECOVERY_RETRY_DELAYS_MS, } from './state_types.js';
32
+ import { toErrorMeta, formatUserError, isThreadNotFoundError, isThreadActiveWriterError, normalizeHelpUsageKey, normalizeApprovalTextAction, isTelegramMessageGone, stringOrNull, cloneAuthRetryContext, normalizeThreadStatusLabel, formatThreadTokenUsage, mapGoalNotification, formatRawLabel, attachedThreadKey, isFileMissingError, isCodexTransportFallbackWarning, formatWarningNotification, normalizePlanImplementationTextAction, normalizeMcpElicitationTextAction, resolveCollaborationMode, activeTurnKey, parseActiveTurnKey, isReadableSessionPath, isNoActiveTurnToSteerError, isThreadQueueUnsupportedError, normalizeRequestedCollaborationMode, normalizeRequestedActiveTurnMessageMode, parsePositiveInt, parseReviewTarget, formatShortStatusError, formatLocalTimestamp, formatTokenCount, formatCodexTokenCountWithMetric, formatCompactNumber, formatWatchCallbackText, isThreadNewCwdCreateConfirmation, isThreadNewCwdCancelConfirmation, turnHasRelayableOutcome, isTelegramMessageTooLong, firstLine, } from './shared_helpers.js';
33
+ import { formatCodexAuthPoolSummary, formatRemoteStatusMessage, isInvalidCodexAuthDeleteReason, formatRichInternalMessage, formatGoalMessage, formatHistoryMessage, formatFuzzyFilesMessage, formatSelfUpdateBroadcastLine, formatSelfUpdateVersionTransition, formatCodexUpdateState, renderTelegramTable, formatRichDemoMessage, formatRichDemoFallbackMessage, formatRichDiffMessage, formatDiffMessage, } from './native_panels.js';
34
+ import { formatAuthProactiveRefreshStatus, selectCodexRateLimitSnapshot, parseCodexAuthListRequest, createPendingAuthChoiceList, renderAuthListMessage, authChoiceKeyboard, formatAuthSyncStatus, formatAuthSyncEvents, formatAuthSyncTrace, formatAuthClusterAuditResult, authRefreshAllConfirmKeyboard, formatAuthRefreshAllResult, formatCodexAccountLabel, formatPlanTypeLabel, clampCodexAuthListOffset, formatCodexAuthCandidateDisplayName, authRepairKeyboard, chatGptAuthMetadataCompatible, formatRateLimitWindowLabel, formatRemainingUsagePercent, } from './auth_presentation.js';
35
+ import { mapApprovalDecision, parseStoredServerRequestId, renderApprovalMessage, approvalKeyboard, resolveScopeMessageTarget, parseServerRequestId, stringifyServerRequestId, sameServerRequestId, renderUserInputMessage, parseUserInputQuestions, serializePendingUserInput, userInputKeyboard, stringifyPendingUserInputAnswers, pendingUserInputCurrentQuestionIndex, parseStoredPendingUserInput, renderPlanImplementationPrompt, planImplementationKeyboard, renderMcpElicitationMessage, mcpElicitationKeyboard, findReusableStandaloneAttachmentBatch, parseStagedTelegramAttachments, mergeAttachmentBatchCaption, summarizeStagedAttachmentInput, shortId, renderAttachmentBatchMessage, attachmentBatchKeyboard, observerCursorFromActiveTurn, parseStoredTurnInput, threadNewCwdCreateKeyboard, whereKeyboard, restartPreviewRecoveryKey, seedObservedTurnCursor, activeTurnKeyboard, } from './interactions.js';
36
+ import { isRetryableCodexTransportError, formatCodexNotificationError, classifyCodexAuthRotationError, isCodexAuthInvalidError, isCodexQuotaLimitError, } from './auth_errors.js';
37
+ import { truncateInline, ensureTurnSegment, extractLatestPlanMarkdown, extractLatestProposedPlanMarkdown, renderCollapsedCommentary, formatToolBatchStatus, createToolBatchState, describeExecCommand, incrementToolBatchCount, renderArchivedToolBatchStatus, } from './turn_rendering.js';
38
+ import { pointCodexAuthAtTarget, restoreCodexAuthTarget, codexAuthCandidateNameFromAddName, authPathDisplayLabel, listCodexAuthState, switchCodexAuth, isCodexApiKeyAuthCandidate, } from './auth_files.js';
113
39
  export class BridgeSessionCore {
114
40
  config;
115
41
  store;
@@ -119,6 +45,14 @@ export class BridgeSessionCore {
119
45
  selfUpdater;
120
46
  coordinator;
121
47
  ownsTelegramRuntime;
48
+ listeners = new ListenerScope();
49
+ controlOperations = new Set();
50
+ controlPlaneStarted = false;
51
+ recoverExecution = true;
52
+ executionHost = null;
53
+ localUsage;
54
+ authQuota;
55
+ nativePanels;
122
56
  activeTurns = new Map();
123
57
  activeTurnsByTurnId = new Map();
124
58
  observedThreadWatchers = new Map();
@@ -142,11 +76,6 @@ export class BridgeSessionCore {
142
76
  externalAuthValidationInProgress = false;
143
77
  turnStartInProgress = 0;
144
78
  authRotationFailedTargets = new Set();
145
- localUsageCache = null;
146
- localUsageCacheLoaded = false;
147
- localUsageRefresh = null;
148
- authQuotaSnapshots = {};
149
- authQuotaSnapshotsLoaded = false;
150
79
  lastRemoteControlStatus = null;
151
80
  pendingThreadRenames = new Map();
152
81
  pendingThreadNewCwds = new Map();
@@ -182,7 +111,15 @@ export class BridgeSessionCore {
182
111
  this.coordinator = coordinator;
183
112
  this.ownsTelegramRuntime = ownsTelegramRuntime;
184
113
  this.messaging = outbound;
185
- }
114
+ this.localUsage = new CodexLocalUsageService(config, store);
115
+ this.authQuota = new CodexAuthQuotaService(config, store, logger, app, name => this.syncCodexAuthCandidate(name));
116
+ this.nativePanels = new CodexNativePanelService(config, store, logger, app, { sendMessage: (scope, text, keyboard) => this.sendMessage(scope, text, keyboard), editMessage: (scope, id, text, keyboard) => this.editMessage(scope, id, text, keyboard), scheduleStalePanelDeletion: (scope, id) => this.scheduleStalePanelDeletion(scope, id), answerCallback: (id, text) => this.messaging.answerCallback(id, text) });
117
+ }
118
+ attachExecutionHost(host) { this.executionHost = host; }
119
+ async startControlPlane() { await this.startCodexApp({ recoverExecution: false }); }
120
+ async stopControlPlane() { await this.stop({ keepTransports: true }); }
121
+ getPendingApprovalCount() { return this.pendingApprovalMessages.size; }
122
+ async dispatchBackendCommand(event) { await this.withLock(event.scopeId, () => this.handleText(event)); }
186
123
  /** Wire Telegram inbound events. Call before {@link startCodexApp}. */
187
124
  registerTelegramInboundHandlers() {
188
125
  this.bot.on('remoteReady', () => {
@@ -221,31 +158,33 @@ export class BridgeSessionCore {
221
158
  || this.pendingForceTakeovers.has(scopeId);
222
159
  }
223
160
  /** Start Codex app-server transport and attach RPC listeners. */
224
- async startCodexApp() {
161
+ async startCodexApp(options) {
162
+ if (this.controlPlaneStarted)
163
+ return;
164
+ this.controlPlaneStarted = true;
165
+ this.recoverExecution = options?.recoverExecution !== false;
225
166
  this.stopping = false;
226
- this.app.on('notification', (msg) => {
227
- void this.handleNotification(msg).catch((error) => {
228
- void this.handleAsyncError('codex.notification', error);
229
- });
167
+ this.listeners.listen(this.app, 'notification', (msg) => {
168
+ this.trackControlOperation(this.handleNotification(msg).catch(error => this.handleAsyncError('codex.notification', error)));
230
169
  });
231
- this.app.on('serverRequest', (msg) => {
232
- void this.handleServerRequest(msg).catch((error) => {
233
- void this.handleAsyncError('codex.server_request', error);
234
- });
170
+ this.listeners.listen(this.app, 'serverRequest', (msg) => {
171
+ this.trackControlOperation(this.handleServerRequest(msg).catch(error => this.handleAsyncError('codex.server_request', error)));
235
172
  });
236
- this.app.on('connected', () => {
173
+ this.listeners.listen(this.app, 'connected', () => {
237
174
  this.attachedThreads.clear();
238
175
  this.lastError = null;
239
176
  this.updateStatus();
240
177
  });
241
- this.app.on('ready', () => {
178
+ this.listeners.listen(this.app, 'ready', () => {
179
+ if (!this.recoverExecution)
180
+ return;
242
181
  if (!this.codexReconnectPending || this.stopping) {
243
182
  return;
244
183
  }
245
184
  this.codexReconnectPending = false;
246
185
  this.scheduleCodexReconnectRecovery();
247
186
  });
248
- this.app.on('disconnected', () => {
187
+ this.listeners.listen(this.app, 'disconnected', () => {
249
188
  this.attachedThreads.clear();
250
189
  this.threadTokenUsageAlerts.clear();
251
190
  if (!this.stopping) {
@@ -263,14 +202,17 @@ export class BridgeSessionCore {
263
202
  }
264
203
  this.updateStatus();
265
204
  });
266
- await this.app.start();
267
- this.store.requeueInterruptedQueuedTurnInputs();
268
- await this.restorePendingUserInputs();
269
- await this.restoreGuidedPlanSessions();
270
- await this.cleanupStaleTurnPreviews();
271
- void this.recoverQueuedTurns().catch((error) => {
272
- this.logger.warn('codex.queued_turn_recovery_failed', { error: toErrorMeta(error) });
273
- });
205
+ if (typeof this.app.isConnected !== 'function' || !this.app.isConnected())
206
+ await this.app.start();
207
+ if (options?.recoverExecution !== false) {
208
+ this.store.requeueInterruptedQueuedTurnInputs();
209
+ await this.restorePendingUserInputs();
210
+ await this.restoreGuidedPlanSessions();
211
+ await this.cleanupStaleTurnPreviews();
212
+ void this.recoverQueuedTurns().catch((error) => {
213
+ this.logger.warn('codex.queued_turn_recovery_failed', { error: toErrorMeta(error) });
214
+ });
215
+ }
274
216
  void this.refreshCodexLocalUsageIfNeeded().catch((error) => {
275
217
  this.logger.warn('codex.local_usage_background_refresh_failed', { error: formatUserError(error) });
276
218
  });
@@ -290,8 +232,16 @@ export class BridgeSessionCore {
290
232
  await this.startCodexApp();
291
233
  await this.startTelegramPolling();
292
234
  }
293
- async stop() {
235
+ async stop(options) {
294
236
  this.stopping = true;
237
+ this.controlPlaneStarted = false;
238
+ this.listeners.clear();
239
+ this.clearObservedThreadWatchers();
240
+ this.clearSelfUpdateStatusPoll();
241
+ this.clearProactiveAuthRefreshTimer();
242
+ while (this.controlOperations.size)
243
+ await Promise.allSettled([...this.controlOperations]);
244
+ await this.localUsage.stop();
295
245
  this.codexReconnectPending = false;
296
246
  this.pendingTurnErrors.clear();
297
247
  this.pendingUserInputs.clear();
@@ -315,7 +265,8 @@ export class BridgeSessionCore {
315
265
  this.turnStartInProgress = 0;
316
266
  this.clearObservedThreadWatchers();
317
267
  this.releaseActiveTurnsForBridgeShutdown();
318
- this.bot.stop();
268
+ if (!options?.keepTransports && this.ownsTelegramRuntime)
269
+ await this.bot.stop();
319
270
  for (const timer of this.approvalTimers.values()) {
320
271
  clearTimeout(timer);
321
272
  }
@@ -331,7 +282,8 @@ export class BridgeSessionCore {
331
282
  clearTimeout(timer);
332
283
  }
333
284
  this.stalePanelDeleteTimers.clear();
334
- await this.app.stop({ terminateServer: false });
285
+ if (!options?.keepTransports)
286
+ await this.app.stop({ terminateServer: false });
335
287
  this.updateStatus();
336
288
  }
337
289
  getRuntimeStatus() {
@@ -358,6 +310,11 @@ export class BridgeSessionCore {
358
310
  };
359
311
  }
360
312
  async handleText(event) {
313
+ const action = this.messaging.resolveAction(event);
314
+ if (action) {
315
+ await this.handleCallback(action);
316
+ return;
317
+ }
361
318
  const scopeId = event.scopeId;
362
319
  const locale = this.localeForChat(scopeId, event.languageCode);
363
320
  if (scopeId.startsWith(BRIDGE_SCOPE_WEIXIN_PREFIX)) {
@@ -1297,7 +1254,14 @@ export class BridgeSessionCore {
1297
1254
  await this.app.respond(parseStoredServerRequestId(approval.serverRequestId), result);
1298
1255
  await this.markApprovalResolvedForAllScopes(approval, action, scopeId);
1299
1256
  }
1257
+ trackControlOperation(operation) {
1258
+ this.controlOperations.add(operation);
1259
+ void operation.catch(error => this.logger.warn('codex.control_operation_failed', { error: toErrorMeta(error) }))
1260
+ .finally(() => this.controlOperations.delete(operation));
1261
+ }
1300
1262
  async handleNotification(notification) {
1263
+ if (this.stopping)
1264
+ return;
1301
1265
  const activity = normalizeTurnActivityEvent(notification);
1302
1266
  if (activity) {
1303
1267
  await this.handleTurnActivityEvent(activity);
@@ -1305,6 +1269,8 @@ export class BridgeSessionCore {
1305
1269
  }
1306
1270
  switch (notification.method) {
1307
1271
  case 'sessionConfigured': {
1272
+ if (!this.recoverExecution)
1273
+ return;
1308
1274
  const params = notification.params;
1309
1275
  const threadId = String(params.session_id || '');
1310
1276
  if (!threadId)
@@ -1421,6 +1387,11 @@ export class BridgeSessionCore {
1421
1387
  }
1422
1388
  }
1423
1389
  async handleServerRequest(request) {
1390
+ if (this.stopping)
1391
+ return;
1392
+ const threadId = request.params?.threadId;
1393
+ if (this.executionHost && typeof threadId === 'string' && !this.findChatByThread(threadId))
1394
+ return;
1424
1395
  switch (request.method) {
1425
1396
  case 'item/commandExecution/requestApproval': {
1426
1397
  const params = request.params;
@@ -1567,6 +1538,13 @@ export class BridgeSessionCore {
1567
1538
  if (!turnId || !threadId) {
1568
1539
  return;
1569
1540
  }
1541
+ if (!this.recoverExecution) {
1542
+ for (const watcher of this.observedThreadWatchers.values()) {
1543
+ if (!watcher.stopped && watcher.threadId === threadId)
1544
+ await this.ensureObservedActiveTurnState(watcher, turnId);
1545
+ }
1546
+ return;
1547
+ }
1570
1548
  for (const scopeId of this.findAllChatsByThread(threadId)) {
1571
1549
  if (this.getActiveTurn(scopeId, turnId) || this.findActiveTurn(scopeId)) {
1572
1550
  continue;
@@ -1900,15 +1878,17 @@ export class BridgeSessionCore {
1900
1878
  await this.restoreAuthAfterAddFailure(state.authDir, state.authPath, record.previousTargetPath);
1901
1879
  }
1902
1880
  scheduleAuthLoginRecovery(loginId) {
1881
+ if (this.stopping)
1882
+ return;
1903
1883
  const timer = setTimeout(() => {
1904
1884
  this.authLoginRecoveryTimers.delete(loginId);
1905
- void this.recoverAuthLogin(loginId).catch(error => {
1885
+ this.trackControlOperation(this.recoverAuthLogin(loginId).catch(error => {
1906
1886
  this.logger.warn('codex.auth_login_recovery_failed', { error: toErrorMeta(error) });
1907
1887
  }).finally(() => {
1908
1888
  if (!this.stopping && this.pendingAuthAddsByLoginId.has(loginId)) {
1909
1889
  this.scheduleAuthLoginRecovery(loginId);
1910
1890
  }
1911
- });
1891
+ }));
1912
1892
  }, 5_000);
1913
1893
  timer.unref();
1914
1894
  this.authLoginRecoveryTimers.set(loginId, timer);
@@ -1984,7 +1964,7 @@ export class BridgeSessionCore {
1984
1964
  }));
1985
1965
  }
1986
1966
  async handleRateLimitsUpdated() {
1987
- this.localUsageCache = null;
1967
+ this.localUsage.invalidate();
1988
1968
  }
1989
1969
  async handleSkillsChangedNotification() {
1990
1970
  this.logger.info('codex.skills_changed');
@@ -2758,7 +2738,7 @@ export class BridgeSessionCore {
2758
2738
  sandboxMode: access.sandboxMode,
2759
2739
  cwd,
2760
2740
  model: settings?.model ?? null,
2761
- effort: settings?.reasoningEffort ?? null,
2741
+ effort: normalizeRequestedEffort(settings?.reasoningEffort ?? ''),
2762
2742
  serviceTier,
2763
2743
  collaborationMode,
2764
2744
  });
@@ -2786,7 +2766,7 @@ export class BridgeSessionCore {
2786
2766
  sandboxMode: nextAccess.sandboxMode,
2787
2767
  cwd: replacementCwd,
2788
2768
  model: nextSettings?.model ?? null,
2789
- effort: nextSettings?.reasoningEffort ?? null,
2769
+ effort: normalizeRequestedEffort(nextSettings?.reasoningEffort ?? ''),
2790
2770
  serviceTier: replacementServiceTier,
2791
2771
  collaborationMode: replacementCollaborationMode,
2792
2772
  });
@@ -3232,7 +3212,7 @@ export class BridgeSessionCore {
3232
3212
  }
3233
3213
  }
3234
3214
  isIdleForServiceUpdate() {
3235
- return this.activeTurns.size === 0
3215
+ return !this.executionHost?.hasExecutingTasks() && this.activeTurns.size === 0
3236
3216
  && this.pendingApprovalMessages.size === 0
3237
3217
  && this.pendingUserInputs.size === 0
3238
3218
  && this.pendingMcpElicitations.size === 0
@@ -3254,6 +3234,8 @@ export class BridgeSessionCore {
3254
3234
  return this.botUsername ? `@${this.botUsername} (${this.config.tgScopeBotId})` : this.config.tgScopeBotId;
3255
3235
  }
3256
3236
  ownsScope(scopeId) {
3237
+ if (this.executionHost)
3238
+ return this.executionHost.ownsScope(scopeId);
3257
3239
  if (scopeId.startsWith(BRIDGE_SCOPE_WEIXIN_PREFIX)) {
3258
3240
  return this.messaging.hasWeixinTransport;
3259
3241
  }
@@ -3622,7 +3604,7 @@ export class BridgeSessionCore {
3622
3604
  this.scheduleStalePanelDeletion(scopeId, messageId);
3623
3605
  }
3624
3606
  scheduleStalePanelDeletion(scopeId, messageId) {
3625
- if (this.config.telegramPanelTtlMs <= 0 || parseWeixinBridgeScope(scopeId)) {
3607
+ if (this.stopping || this.config.telegramPanelTtlMs <= 0 || parseWeixinBridgeScope(scopeId)) {
3626
3608
  return;
3627
3609
  }
3628
3610
  const key = `${scopeId}:${messageId}`;
@@ -3632,9 +3614,9 @@ export class BridgeSessionCore {
3632
3614
  }
3633
3615
  const timer = setTimeout(() => {
3634
3616
  this.stalePanelDeleteTimers.delete(key);
3635
- void this.deleteMessage(scopeId, messageId).catch(error => {
3617
+ this.trackControlOperation(this.deleteMessage(scopeId, messageId).catch(error => {
3636
3618
  this.logger.warn('telegram.stale_panel_delete_failed', { scopeId, messageId, error: toErrorMeta(error) });
3637
- });
3619
+ }));
3638
3620
  }, this.config.telegramPanelTtlMs);
3639
3621
  timer.unref();
3640
3622
  this.stalePanelDeleteTimers.set(key, timer);
@@ -3794,9 +3776,11 @@ export class BridgeSessionCore {
3794
3776
  }
3795
3777
  }
3796
3778
  armApprovalTimer(localId) {
3779
+ if (this.stopping)
3780
+ return;
3797
3781
  this.clearApprovalTimer(localId);
3798
3782
  const timer = setTimeout(() => {
3799
- void this.expireApproval(localId);
3783
+ this.trackControlOperation(this.expireApproval(localId));
3800
3784
  }, 5 * 60 * 1000);
3801
3785
  this.approvalTimers.set(localId, timer);
3802
3786
  }
@@ -3808,14 +3792,16 @@ export class BridgeSessionCore {
3808
3792
  this.approvalTimers.delete(localId);
3809
3793
  }
3810
3794
  armSubmittedUserInputTimer(localId) {
3795
+ if (this.stopping)
3796
+ return;
3811
3797
  this.clearSubmittedUserInputTimer(localId);
3812
3798
  const timer = setTimeout(() => {
3813
- void this.notifySubmittedUserInputStillWaiting(localId).catch((error) => {
3799
+ this.trackControlOperation(this.notifySubmittedUserInputStillWaiting(localId).catch((error) => {
3814
3800
  this.logger.warn('telegram.user_input_waiting_notice_failed', {
3815
3801
  localId,
3816
3802
  error: toErrorMeta(error),
3817
3803
  });
3818
- });
3804
+ }));
3819
3805
  }, USER_INPUT_SUBMITTED_NOTICE_MS);
3820
3806
  timer.unref?.();
3821
3807
  this.submittedUserInputTimers.set(localId, timer);
@@ -3977,9 +3963,9 @@ export class BridgeSessionCore {
3977
3963
  }
3978
3964
  });
3979
3965
  this.codexReconnectRecovery = trackedRecovery;
3980
- void this.codexReconnectRecovery.catch((error) => {
3966
+ this.trackControlOperation(this.codexReconnectRecovery.catch((error) => {
3981
3967
  this.logger.error('codex.reconnect_recovery_failed', { error: toErrorMeta(error) });
3982
- });
3968
+ }));
3983
3969
  }
3984
3970
  async recoverAfterCodexReconnect() {
3985
3971
  const recoveredScopes = new Set();
@@ -4186,12 +4172,12 @@ export class BridgeSessionCore {
4186
4172
  return { mode, threadId };
4187
4173
  }
4188
4174
  scheduleObservedThreadPoll(watcher) {
4189
- if (watcher.stopped) {
4175
+ if (watcher.stopped || this.stopping) {
4190
4176
  return;
4191
4177
  }
4192
4178
  watcher.timer = setTimeout(() => {
4193
4179
  watcher.timer = null;
4194
- void this.pollObservedThread(watcher).catch((error) => {
4180
+ this.trackControlOperation(this.pollObservedThread(watcher).then(() => { }).catch((error) => {
4195
4181
  this.logger.error('codex.observe_thread_failed', {
4196
4182
  scopeId: watcher.scopeId,
4197
4183
  threadId: watcher.threadId,
@@ -4201,7 +4187,7 @@ export class BridgeSessionCore {
4201
4187
  if (!watcher.stopped && this.observedThreadWatchers.get(watcher.scopeId) === watcher) {
4202
4188
  this.scheduleObservedThreadPoll(watcher);
4203
4189
  }
4204
- });
4190
+ }));
4205
4191
  }, OBSERVED_THREAD_POLL_MS);
4206
4192
  }
4207
4193
  async pollObservedThread(watcher) {
@@ -5072,15 +5058,15 @@ export class BridgeSessionCore {
5072
5058
  }
5073
5059
  }
5074
5060
  scheduleSelfUpdateStatusPoll(delay = SELF_UPDATE_STATUS_POLL_MS) {
5075
- if (!this.selfUpdater || this.selfUpdatePollTimer) {
5061
+ if (this.stopping || !this.selfUpdater || this.selfUpdatePollTimer) {
5076
5062
  return;
5077
5063
  }
5078
5064
  this.selfUpdatePollTimer = setTimeout(() => {
5079
5065
  this.selfUpdatePollTimer = null;
5080
- void this.pollSelfUpdateStatus().catch((error) => {
5066
+ this.trackControlOperation(this.pollSelfUpdateStatus().catch((error) => {
5081
5067
  this.logger.error('self_update.poll_failed', { error: toErrorMeta(error) });
5082
5068
  this.scheduleSelfUpdateStatusPoll();
5083
- });
5069
+ }));
5084
5070
  }, delay);
5085
5071
  }
5086
5072
  clearSelfUpdateStatusPoll() {
@@ -5213,16 +5199,16 @@ export class BridgeSessionCore {
5213
5199
  return status.codexUpdate ?? null;
5214
5200
  }
5215
5201
  scheduleProactiveAuthRefresh(delayMs = CODEX_AUTH_PROACTIVE_REFRESH_INTERVAL_MS) {
5216
- if (this.proactiveAuthRefreshTimer) {
5202
+ if (this.stopping || this.proactiveAuthRefreshTimer) {
5217
5203
  return;
5218
5204
  }
5219
5205
  this.proactiveAuthRefreshTimer = setTimeout(() => {
5220
5206
  this.proactiveAuthRefreshTimer = null;
5221
- void this.runProactiveAuthRefresh().catch((error) => {
5207
+ this.trackControlOperation(this.runProactiveAuthRefresh().catch((error) => {
5222
5208
  this.logger.warn('codex.auth_proactive_refresh_failed', { error: toErrorMeta(error) });
5223
5209
  }).finally(() => {
5224
5210
  this.scheduleProactiveAuthRefresh();
5225
- });
5211
+ }));
5226
5212
  }, delayMs);
5227
5213
  this.proactiveAuthRefreshTimer.unref();
5228
5214
  }
@@ -6050,203 +6036,27 @@ export class BridgeSessionCore {
6050
6036
  }
6051
6037
  await this.sendRichHtmlMessage(scopeId, formatRichDiffMessage(locale, diff.diff), formatDiffMessage(locale, diff.diff));
6052
6038
  }
6053
- async handleLoadedCommand(scopeId, locale) {
6054
- const threadIds = await this.app.listLoadedThreads();
6055
- await this.sendMessage(scopeId, formatLoadedThreadsMessage(locale, threadIds));
6056
- }
6057
- async handleSkillsCommand(scopeId, locale, args) {
6058
- const forceReload = args[0]?.toLowerCase() === 'reload';
6059
- const query = (forceReload ? args.slice(1) : args).join(' ').trim();
6060
- const entries = await this.listSkillsForScope(scopeId, forceReload);
6061
- await this.sendMessage(scopeId, formatSkillsMessage(locale, entries, query || null, forceReload));
6062
- }
6063
- async handleSkillCommand(scopeId, locale, args) {
6064
- const name = args.join(' ').trim();
6065
- if (!name) {
6066
- await this.sendMessage(scopeId, t(locale, 'usage_skill'));
6067
- return;
6068
- }
6069
- const skill = findSkill(await this.listSkillsForScope(scopeId, false), name);
6070
- if (!skill) {
6071
- await this.sendMessage(scopeId, t(locale, 'skill_not_found', { name }));
6072
- return;
6073
- }
6074
- await this.sendMessage(scopeId, formatSkillDetailMessage(locale, skill));
6075
- }
6076
- async handleSkillConfigCommand(scopeId, locale, args, enabled) {
6077
- const name = args.join(' ').trim();
6078
- if (!name) {
6079
- await this.sendMessage(scopeId, enabled ? t(locale, 'usage_skill_enable') : t(locale, 'usage_skill_disable'));
6080
- return;
6081
- }
6082
- await this.app.writeSkillConfig({ name }, enabled);
6083
- await this.sendMessage(scopeId, t(locale, enabled ? 'skill_enabled' : 'skill_disabled', { name }));
6084
- }
6085
- async handleHooksCommand(scopeId, locale) {
6086
- const binding = this.store.getBinding(scopeId);
6087
- const entries = await this.app.listHooks(binding?.cwd ?? this.config.defaultCwd);
6088
- await this.sendMessage(scopeId, formatHooksMessage(locale, entries));
6089
- }
6090
- async handlePluginsCommand(scopeId, locale, args) {
6091
- const query = args.join(' ').trim() || null;
6092
- const binding = this.store.getBinding(scopeId);
6093
- const marketplaces = await this.app.listPlugins(binding?.cwd ?? this.config.defaultCwd);
6094
- await this.sendMessage(scopeId, formatPluginsMessage(locale, marketplaces, query));
6095
- }
6096
- async handlePluginCommand(scopeId, locale, args) {
6097
- const name = args.join(' ').trim();
6098
- if (!name) {
6099
- await this.sendMessage(scopeId, t(locale, 'usage_plugin'));
6100
- return;
6101
- }
6102
- const plugin = await this.app.readPlugin(name);
6103
- if (!plugin) {
6104
- await this.sendMessage(scopeId, t(locale, 'plugin_not_found', { name }));
6105
- return;
6106
- }
6107
- await this.sendMessage(scopeId, formatPluginDetailMessage(locale, plugin));
6108
- }
6109
- async handlePluginSkillCommand(scopeId, locale, args) {
6110
- const [marketplace, plugin, ...skillParts] = args;
6111
- const skill = skillParts.join(' ').trim();
6112
- if (!marketplace || !plugin || !skill) {
6113
- await this.sendMessage(scopeId, t(locale, 'usage_plugin_skill'));
6114
- return;
6115
- }
6116
- const contents = await this.app.readPluginSkill(marketplace, plugin, skill);
6117
- await this.sendMessage(scopeId, formatPluginSkillMessage(locale, marketplace, plugin, skill, contents));
6118
- }
6119
- async handleAppsCommand(scopeId, locale, args) {
6120
- const forceRefetch = args[0]?.toLowerCase() === 'reload';
6121
- const binding = this.store.getBinding(scopeId);
6122
- const apps = await this.app.listApps(binding?.threadId ?? null, forceRefetch);
6123
- await this.sendMessage(scopeId, formatAppsMessage(locale, apps, forceRefetch));
6124
- }
6125
- async handleFeaturesCommand(scopeId, locale) {
6126
- const features = await this.app.listExperimentalFeatures();
6127
- await this.sendMessage(scopeId, formatFeaturesMessage(locale, features));
6128
- }
6129
- async handleConfigCommand(scopeId, locale, args = []) {
6130
- const action = args[0]?.toLowerCase() ?? '';
6131
- if (['auth_auto_delete', 'auth-auto-delete', 'auto_delete_needs_repair', 'auto-delete-needs-repair'].includes(action)) {
6132
- const enabled = parseConfigBooleanArg(args[1]);
6133
- if (enabled === null) {
6134
- await this.sendMessage(scopeId, t(locale, 'config_auth_auto_delete_usage'));
6135
- return;
6136
- }
6137
- const update = await this.setFoxClawBooleanConfig('auth_auto_delete', enabled);
6138
- const binding = this.store.getBinding(scopeId);
6139
- const result = await this.app.readConfig(binding?.cwd ?? this.config.defaultCwd, true);
6140
- const sentMessageId = await this.sendMessage(scopeId, `${this.formatConfigToggleUpdate(locale, update)}\n\n${formatConfigMessage(locale, result, this.config, this.store.getCodexAuthPoolStats())}`, configKeyboard(locale, this.config));
6141
- this.scheduleStalePanelDeletion(scopeId, sentMessageId);
6142
- return;
6143
- }
6144
- if (['delete_tool_details', 'delete-tool-details', 'tool_details', 'tool-details'].includes(action)) {
6145
- const enabled = parseConfigBooleanArg(args[1]);
6146
- if (enabled === null) {
6147
- await this.sendMessage(scopeId, t(locale, 'config_delete_tool_details_usage'));
6148
- return;
6149
- }
6150
- const update = await this.setFoxClawBooleanConfig('delete_tool_details', enabled);
6151
- const binding = this.store.getBinding(scopeId);
6152
- const result = await this.app.readConfig(binding?.cwd ?? this.config.defaultCwd, true);
6153
- const sentMessageId = await this.sendMessage(scopeId, `${this.formatConfigToggleUpdate(locale, update)}\n\n${formatConfigMessage(locale, result, this.config, this.store.getCodexAuthPoolStats())}`, configKeyboard(locale, this.config));
6154
- this.scheduleStalePanelDeletion(scopeId, sentMessageId);
6155
- return;
6156
- }
6157
- const binding = this.store.getBinding(scopeId);
6158
- const result = await this.app.readConfig(binding?.cwd ?? this.config.defaultCwd, true);
6159
- const sentMessageId = await this.sendMessage(scopeId, formatConfigMessage(locale, result, this.config, this.store.getCodexAuthPoolStats()), configKeyboard(locale, this.config));
6160
- this.scheduleStalePanelDeletion(scopeId, sentMessageId);
6161
- }
6162
- async handleConfigToggleCallback(event, key, enabled, locale) {
6163
- const update = await this.setFoxClawBooleanConfig(key, enabled);
6164
- await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'decision_recorded'));
6165
- const binding = this.store.getBinding(event.scopeId);
6166
- const result = await this.app.readConfig(binding?.cwd ?? this.config.defaultCwd, true);
6167
- const message = `${this.formatConfigToggleUpdate(locale, update)}\n\n${formatConfigMessage(locale, result, this.config, this.store.getCodexAuthPoolStats())}`;
6168
- if (event.messageId !== null) {
6169
- await this.editMessage(event.scopeId, event.messageId, message, configKeyboard(locale, this.config));
6170
- this.scheduleStalePanelDeletion(event.scopeId, event.messageId);
6171
- }
6172
- else {
6173
- const sentMessageId = await this.sendMessage(event.scopeId, message, configKeyboard(locale, this.config));
6174
- this.scheduleStalePanelDeletion(event.scopeId, sentMessageId);
6175
- }
6176
- }
6177
- async setFoxClawBooleanConfig(key, enabled) {
6178
- const envKey = key === 'auth_auto_delete'
6179
- ? 'AUTH_AUTO_DELETE_NEEDS_REPAIR'
6180
- : 'TELEGRAM_DELETE_TOOL_DETAILS_AFTER_FINAL';
6181
- if (key === 'auth_auto_delete') {
6182
- this.config.authAutoDeleteNeedsRepair = enabled;
6183
- }
6184
- else {
6185
- this.config.telegramDeleteToolDetailsAfterFinal = enabled;
6186
- }
6187
- const envPath = this.config.envPath;
6188
- if (!envPath) {
6189
- return { key, enabled, envKey, envPath: null, envUpdated: false, envError: null };
6190
- }
6191
- try {
6192
- await writeEnvBoolean(envPath, envKey, enabled);
6193
- return { key, enabled, envKey, envPath, envUpdated: true, envError: null };
6194
- }
6195
- catch (error) {
6196
- this.logger.warn('config.env_update_failed', { key: envKey, envPath, error: toErrorMeta(error) });
6197
- return { key, enabled, envKey, envPath, envUpdated: false, envError: formatUserError(error) };
6198
- }
6199
- }
6200
- formatConfigToggleUpdate(locale, update) {
6201
- const lines = [t(locale, update.key === 'auth_auto_delete' ? 'config_auth_auto_delete_updated' : 'config_delete_tool_details_updated', {
6202
- value: t(locale, update.enabled ? 'yes' : 'no'),
6203
- })];
6204
- if (update.envError) {
6205
- lines.push(t(locale, 'config_env_update_failed', { value: update.envPath ?? t(locale, 'unknown'), error: update.envError }));
6206
- }
6207
- return lines.join('\n');
6208
- }
6209
- async handleRequirementsCommand(scopeId, locale) {
6210
- const requirements = await this.app.readConfigRequirements();
6211
- await this.sendMessage(scopeId, formatRequirementsMessage(locale, requirements));
6212
- }
6213
- async handleProviderCommand(scopeId, locale) {
6214
- const capabilities = await this.app.readModelProviderCapabilities();
6215
- await this.sendMessage(scopeId, formatProviderMessage(locale, capabilities));
6216
- }
6217
- async handleMcpCommand(scopeId, locale, args) {
6218
- const detail = args[0]?.toLowerCase() === 'brief' ? 'toolsAndAuthOnly' : 'full';
6219
- const statuses = await this.app.listMcpServerStatus(detail);
6220
- await this.sendMessage(scopeId, formatMcpStatusMessage(locale, statuses));
6221
- }
6222
- async handleMcpReloadCommand(scopeId, locale) {
6223
- await this.app.reloadMcpServers();
6224
- await this.sendMessage(scopeId, t(locale, 'mcp_reload_done'));
6225
- }
6226
- async handleMcpLoginCommand(scopeId, locale, args) {
6227
- const name = args.join(' ').trim();
6228
- if (!name) {
6229
- await this.sendMessage(scopeId, t(locale, 'usage_mcp_login'));
6230
- return;
6231
- }
6232
- const url = await this.app.loginMcpServer(name);
6233
- await this.sendMessage(scopeId, t(locale, 'mcp_login_started', { name, url: url || t(locale, 'unknown') }));
6234
- }
6235
- async handleMcpResourceCommand(scopeId, locale, args) {
6236
- const [server, ...uriParts] = args;
6237
- const uri = uriParts.join(' ').trim();
6238
- if (!server || !uri) {
6239
- await this.sendMessage(scopeId, t(locale, 'usage_mcp_resource'));
6240
- return;
6241
- }
6242
- const binding = this.store.getBinding(scopeId);
6243
- const contents = await this.app.readMcpResource(server, uri, binding?.threadId ?? null);
6244
- await this.sendMessage(scopeId, formatMcpResourceMessage(locale, server, uri, contents));
6245
- }
6246
- async listSkillsForScope(scopeId, forceReload) {
6247
- const binding = this.store.getBinding(scopeId);
6248
- return this.app.listSkills(binding?.cwd ?? this.config.defaultCwd, forceReload);
6249
- }
6039
+ async handleLoadedCommand(scopeId, locale) { return this.nativePanels.handleLoadedCommand(scopeId, locale); }
6040
+ async handleSkillsCommand(scopeId, locale, args) { return this.nativePanels.handleSkillsCommand(scopeId, locale, args); }
6041
+ async handleSkillCommand(scopeId, locale, args) { return this.nativePanels.handleSkillCommand(scopeId, locale, args); }
6042
+ async handleSkillConfigCommand(scopeId, locale, args, enabled) { return this.nativePanels.handleSkillConfigCommand(scopeId, locale, args, enabled); }
6043
+ async handleHooksCommand(scopeId, locale) { return this.nativePanels.handleHooksCommand(scopeId, locale); }
6044
+ async handlePluginsCommand(scopeId, locale, args) { return this.nativePanels.handlePluginsCommand(scopeId, locale, args); }
6045
+ async handlePluginCommand(scopeId, locale, args) { return this.nativePanels.handlePluginCommand(scopeId, locale, args); }
6046
+ async handlePluginSkillCommand(scopeId, locale, args) { return this.nativePanels.handlePluginSkillCommand(scopeId, locale, args); }
6047
+ async handleAppsCommand(scopeId, locale, args) { return this.nativePanels.handleAppsCommand(scopeId, locale, args); }
6048
+ async handleFeaturesCommand(scopeId, locale) { return this.nativePanels.handleFeaturesCommand(scopeId, locale); }
6049
+ async handleConfigCommand(scopeId, locale, args = []) { return this.nativePanels.handleConfigCommand(scopeId, locale, args); }
6050
+ async handleConfigToggleCallback(event, key, enabled, locale) { return this.nativePanels.handleConfigToggleCallback(event, key, enabled, locale); }
6051
+ async setFoxClawBooleanConfig(key, enabled) { return this.nativePanels.setFoxClawBooleanConfig(key, enabled); }
6052
+ formatConfigToggleUpdate(locale, update) { return this.nativePanels.formatConfigToggleUpdate(locale, update); }
6053
+ async handleRequirementsCommand(scopeId, locale) { return this.nativePanels.handleRequirementsCommand(scopeId, locale); }
6054
+ async handleProviderCommand(scopeId, locale) { return this.nativePanels.handleProviderCommand(scopeId, locale); }
6055
+ async handleMcpCommand(scopeId, locale, args) { return this.nativePanels.handleMcpCommand(scopeId, locale, args); }
6056
+ async handleMcpReloadCommand(scopeId, locale) { return this.nativePanels.handleMcpReloadCommand(scopeId, locale); }
6057
+ async handleMcpLoginCommand(scopeId, locale, args) { return this.nativePanels.handleMcpLoginCommand(scopeId, locale, args); }
6058
+ async handleMcpResourceCommand(scopeId, locale, args) { return this.nativePanels.handleMcpResourceCommand(scopeId, locale, args); }
6059
+ async listSkillsForScope(scopeId, forceReload) { return this.nativePanels.listSkillsForScope(scopeId, forceReload); }
6250
6060
  async requireReadyBinding(scopeId, locale) {
6251
6061
  const binding = this.store.getBinding(scopeId);
6252
6062
  if (!binding) {
@@ -7050,7 +6860,7 @@ export class BridgeSessionCore {
7050
6860
  mode,
7051
6861
  settings: {
7052
6862
  model,
7053
- reasoning_effort: reasoningEffort,
6863
+ reasoning_effort: normalizeRequestedEffort(reasoningEffort ?? ''),
7054
6864
  developer_instructions: config.developerInstructions,
7055
6865
  },
7056
6866
  };
@@ -7183,169 +6993,18 @@ export class BridgeSessionCore {
7183
6993
  return t(locale, 'unknown');
7184
6994
  }
7185
6995
  }
7186
- async readCachedCodexLocalUsageStats() {
7187
- if (this.localUsageCacheLoaded) {
7188
- return this.localUsageCache;
7189
- }
7190
- this.localUsageCache = await readCodexLocalUsageSnapshot(this.codexLocalUsageSnapshotPath());
7191
- this.localUsageCacheLoaded = true;
7192
- return this.localUsageCache;
7193
- }
7194
- async refreshCodexLocalUsageIfNeeded(snapshot) {
7195
- const current = snapshot === undefined ? await this.readCachedCodexLocalUsageStats() : snapshot;
7196
- if (current && Date.now() - current.computedAtMs < CODEX_LOCAL_USAGE_REFRESH_MS) {
7197
- return;
7198
- }
7199
- if (this.localUsageRefresh) {
7200
- return;
7201
- }
7202
- this.localUsageRefresh = this.refreshCodexLocalUsageStats().finally(() => {
7203
- this.localUsageRefresh = null;
7204
- });
7205
- await this.localUsageRefresh;
7206
- }
7207
- async refreshCodexLocalUsageStats() {
7208
- const stats = await readCodexLocalUsageStats(this.config.codexHome ?? undefined);
7209
- const snapshot = { computedAtMs: Date.now(), stats };
7210
- this.localUsageCache = snapshot;
7211
- this.localUsageCacheLoaded = true;
7212
- await writeCodexLocalUsageSnapshot(this.codexLocalUsageSnapshotPath(), snapshot);
7213
- this.store.setBackendCumulativeTokenUsage({
7214
- inputTokens: stats.totals.inputTokens,
7215
- outputTokens: stats.totals.outputTokens,
7216
- cachedTokens: stats.totals.cachedInputTokens,
7217
- totalTokens: stats.totals.totalTokens,
7218
- turnsCount: stats.turns,
7219
- }, 'codex');
7220
- }
7221
- codexLocalUsageSnapshotPath() {
7222
- return path.join(path.dirname(this.config.statusPath), this.runtimeSnapshotFilename(CODEX_LOCAL_USAGE_SNAPSHOT_FILENAME));
7223
- }
7224
- async refreshCurrentCodexAuthQuota(state) {
7225
- const candidate = state.candidates.find(entry => entry.isCurrent);
7226
- if (!candidate) {
7227
- return;
7228
- }
7229
- if (candidate.state === 'needs_repair') {
7230
- return;
7231
- }
7232
- try {
7233
- const metadata = await readChatGptAuthMetadata(candidate.path);
7234
- if (!metadata || !chatGptAuthMetadataMatchesCandidateName(candidate.name, metadata)) {
7235
- return;
7236
- }
7237
- const snapshot = selectCodexRateLimitSnapshot(await this.app.readAccountRateLimits());
7238
- if (!snapshot) {
7239
- return;
7240
- }
7241
- const quota = authQuotaSnapshotFromRateLimit(snapshot, metadata?.accountId ?? null, metadata?.quotaIdentityId ?? null);
7242
- candidate.quota = quota;
7243
- await this.recordCodexAuthQuotaSnapshot(candidate.name, metadata, snapshot);
7244
- await this.applySharedCodexAuthQuotaSnapshots(state);
7245
- await this.syncCodexAuthCandidate(candidate.name);
7246
- }
7247
- catch (error) {
7248
- this.logger.warn('codex.auth_quota_refresh_failed', { error: formatUserError(error) });
7249
- }
7250
- }
7251
- async applySharedCodexAuthQuotaSnapshots(state, candidateQuotaIdentities) {
7252
- const quotaIdentities = candidateQuotaIdentities ?? await this.readCodexAuthCandidateQuotaIdentities(state.candidates);
7253
- const uniqueQuotaIdentityIds = [...new Set([...quotaIdentities.values()].map(identity => identity.quotaIdentityId))];
7254
- if (uniqueQuotaIdentityIds.length === 0) {
7255
- return;
7256
- }
7257
- const snapshotsByIdentity = new Map();
7258
- for (const record of this.store.listCodexAuthQuotaSnapshots(uniqueQuotaIdentityIds)) {
7259
- if (!isFiniteCodexAuthQuotaSnapshotRecord(record)) {
7260
- continue;
7261
- }
7262
- snapshotsByIdentity.set(record.quotaIdentityId, mergeCodexAuthQuotaSnapshots(snapshotsByIdentity.get(record.quotaIdentityId) ?? null, codexAuthQuotaSnapshotFromRecord(record)));
7263
- }
7264
- for (const candidate of state.candidates) {
7265
- const quotaIdentity = quotaIdentities.get(candidate.name);
7266
- if (!quotaIdentity) {
7267
- continue;
7268
- }
7269
- candidate.quota = mergeCodexAuthQuotaSnapshots(candidate.quota, snapshotsByIdentity.get(quotaIdentity.quotaIdentityId) ?? null);
7270
- }
7271
- }
7272
- async recordCodexAuthQuotaSnapshot(candidateName, metadata, snapshot) {
7273
- const quota = authQuotaSnapshotFromRateLimit(snapshot, metadata?.accountId ?? null, metadata?.quotaIdentityId ?? null);
7274
- this.authQuotaSnapshots[candidateName] = quota;
7275
- if (metadata) {
7276
- this.store.setCodexAuthQuotaSnapshot(this.authRuntimeId(), candidateName, metadata.accountId, metadata.quotaIdentityId, quota);
7277
- }
7278
- await this.writeCodexAuthQuotaSnapshots();
7279
- }
7280
- async readCodexAuthCandidateQuotaIdentities(candidates) {
7281
- const entries = await Promise.all(candidates.map(async (candidate) => {
7282
- const record = await readChatGptAuthRecord(candidate.path);
7283
- const metadataMatchesName = record
7284
- ? chatGptAuthMetadataMatchesCandidateName(candidate.name, record)
7285
- : false;
7286
- candidate.credentialKind = record && metadataMatchesName
7287
- ? 'chatgpt'
7288
- : await isCodexApiKeyAuthCandidate(candidate.path)
7289
- ? 'api-key'
7290
- : 'invalid';
7291
- candidate.credentialLastRefreshMs = record && metadataMatchesName ? record.lastRefreshMs : null;
7292
- candidate.credentialExpiresAtMs = record && metadataMatchesName ? readAccessTokenExpiresAtMs(record.raw) : null;
7293
- return [candidate.name, record && metadataMatchesName ? {
7294
- accountId: record.accountId,
7295
- quotaIdentityId: record.quotaIdentityId,
7296
- } : null];
7297
- }));
7298
- const quotaIdentities = new Map();
7299
- for (const [name, quotaIdentity] of entries) {
7300
- if (quotaIdentity) {
7301
- quotaIdentities.set(name, quotaIdentity);
7302
- }
7303
- }
7304
- return quotaIdentities;
7305
- }
7306
- codexAuthQuotaSnapshotMatchesIdentity(snapshot, identity) {
7307
- if (!snapshot) {
7308
- return false;
7309
- }
7310
- const snapshotIdentityId = snapshot.quotaIdentityId ?? snapshot.accountId ?? null;
7311
- if (!snapshotIdentityId) {
7312
- return identity === null;
7313
- }
7314
- return identity !== null && snapshotIdentityId === identity.quotaIdentityId;
7315
- }
7316
- async readCodexAuthQuotaSnapshots() {
7317
- if (this.authQuotaSnapshotsLoaded) {
7318
- return this.authQuotaSnapshots;
7319
- }
7320
- this.authQuotaSnapshotsLoaded = true;
7321
- try {
7322
- const parsed = JSON.parse(await fs.readFile(this.codexAuthQuotaSnapshotPath(), 'utf8'));
7323
- if (parsed && typeof parsed === 'object') {
7324
- for (const [name, value] of Object.entries(parsed)) {
7325
- if (isCodexAuthQuotaSnapshot(value)) {
7326
- this.authQuotaSnapshots[name] = normalizeCodexAuthQuotaSnapshot(value);
7327
- }
7328
- }
7329
- }
7330
- }
7331
- catch {
7332
- // A missing or invalid historical cache should not block the auth panel.
7333
- }
7334
- return this.authQuotaSnapshots;
7335
- }
7336
- async writeCodexAuthQuotaSnapshots() {
7337
- const snapshotPath = this.codexAuthQuotaSnapshotPath();
7338
- await fs.mkdir(path.dirname(snapshotPath), { recursive: true });
7339
- const temporaryPath = `${snapshotPath}.${process.pid}.tmp`;
7340
- await fs.writeFile(temporaryPath, `${JSON.stringify(this.authQuotaSnapshots, null, 2)}\n`, {
7341
- encoding: 'utf8',
7342
- mode: 0o600,
7343
- });
7344
- await fs.rename(temporaryPath, snapshotPath);
7345
- }
7346
- codexAuthQuotaSnapshotPath() {
7347
- return path.join(path.dirname(this.config.statusPath), this.runtimeSnapshotFilename(CODEX_AUTH_QUOTA_SNAPSHOT_FILENAME));
7348
- }
6996
+ async readCachedCodexLocalUsageStats() { return this.localUsage.readCachedCodexLocalUsageStats(); }
6997
+ async refreshCodexLocalUsageIfNeeded(snapshot) { return this.localUsage.refreshCodexLocalUsageIfNeeded(snapshot); }
6998
+ async refreshCodexLocalUsageStats() { return this.localUsage.refreshCodexLocalUsageStats(); }
6999
+ codexLocalUsageSnapshotPath() { return this.localUsage.codexLocalUsageSnapshotPath(); }
7000
+ async refreshCurrentCodexAuthQuota(state) { return this.authQuota.refreshCurrentCodexAuthQuota(state); }
7001
+ async applySharedCodexAuthQuotaSnapshots(state, candidateQuotaIdentities) { return this.authQuota.applySharedCodexAuthQuotaSnapshots(state, candidateQuotaIdentities); }
7002
+ async recordCodexAuthQuotaSnapshot(candidateName, metadata, snapshot) { return this.authQuota.recordCodexAuthQuotaSnapshot(candidateName, metadata, snapshot); }
7003
+ async readCodexAuthCandidateQuotaIdentities(candidates) { return this.authQuota.readCodexAuthCandidateQuotaIdentities(candidates); }
7004
+ codexAuthQuotaSnapshotMatchesIdentity(snapshot, identity) { return this.authQuota.codexAuthQuotaSnapshotMatchesIdentity(snapshot, identity); }
7005
+ async readCodexAuthQuotaSnapshots() { return this.authQuota.readCodexAuthQuotaSnapshots(); }
7006
+ async writeCodexAuthQuotaSnapshots() { return this.authQuota.writeCodexAuthQuotaSnapshots(); }
7007
+ codexAuthQuotaSnapshotPath() { return this.authQuota.codexAuthQuotaSnapshotPath(); }
7349
7008
  runtimeSnapshotFilename(filename) {
7350
7009
  if (!this.config.tgScopeBotId) {
7351
7010
  return filename;
@@ -7383,7 +7042,7 @@ export class BridgeSessionCore {
7383
7042
  const models = await this.app.listModels();
7384
7043
  if (raw === '' || raw.toLowerCase() === 'default' || raw.toLowerCase() === 'reset') {
7385
7044
  const defaultModel = resolveCurrentModel(models, null);
7386
- const nextEffort = clampEffortToModel(defaultModel, settings?.reasoningEffort ?? null);
7045
+ const nextEffort = clampEffortToModel(defaultModel, normalizeRequestedEffort(settings?.reasoningEffort ?? ''));
7387
7046
  const nextTier = clampServiceTierToModel(defaultModel, settings?.serviceTier ?? null);
7388
7047
  this.store.setChatSettings(scopeId, null, nextEffort.effort);
7389
7048
  if (nextTier.adjusted) {
@@ -7410,7 +7069,7 @@ export class BridgeSessionCore {
7410
7069
  await this.sendMessage(scopeId, t(locale, 'unknown_model', { model: raw }));
7411
7070
  return;
7412
7071
  }
7413
- const nextEffort = clampEffortToModel(selected, settings?.reasoningEffort ?? null);
7072
+ const nextEffort = clampEffortToModel(selected, normalizeRequestedEffort(settings?.reasoningEffort ?? ''));
7414
7073
  const nextTier = clampServiceTierToModel(selected, settings?.serviceTier ?? null);
7415
7074
  this.store.setChatSettings(scopeId, selected.model, nextEffort.effort);
7416
7075
  if (nextTier.adjusted) {
@@ -8085,7 +7744,7 @@ export class BridgeSessionCore {
8085
7744
  if (kind === 'model') {
8086
7745
  if (value === 'default') {
8087
7746
  const defaultModel = resolveCurrentModel(models, null);
8088
- const nextEffort = clampEffortToModel(defaultModel, settings?.reasoningEffort ?? null);
7747
+ const nextEffort = clampEffortToModel(defaultModel, normalizeRequestedEffort(settings?.reasoningEffort ?? ''));
8089
7748
  const nextTier = clampServiceTierToModel(defaultModel, settings?.serviceTier ?? null);
8090
7749
  this.store.setChatSettings(scopeId, null, nextEffort.effort);
8091
7750
  if (nextTier.adjusted) {
@@ -8100,7 +7759,7 @@ export class BridgeSessionCore {
8100
7759
  await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'model_no_longer_available'));
8101
7760
  return;
8102
7761
  }
8103
- const nextEffort = clampEffortToModel(selected, settings?.reasoningEffort ?? null);
7762
+ const nextEffort = clampEffortToModel(selected, normalizeRequestedEffort(settings?.reasoningEffort ?? ''));
8104
7763
  const nextTier = clampServiceTierToModel(selected, settings?.serviceTier ?? null);
8105
7764
  this.store.setChatSettings(scopeId, selected.model, nextEffort.effort);
8106
7765
  if (nextTier.adjusted) {
@@ -9216,2979 +8875,4 @@ export class BridgeSessionCore {
9216
8875
  }
9217
8876
  }
9218
8877
  }
9219
- function ensureTurnSegment(active, itemId, phase, outputKind, isPlan) {
9220
- let segment = active.segments.find((entry) => entry.itemId === itemId);
9221
- if (segment) {
9222
- if (phase !== undefined) {
9223
- segment.phase = phase;
9224
- }
9225
- if (outputKind !== undefined) {
9226
- segment.outputKind = outputKind;
9227
- }
9228
- if (isPlan !== undefined) {
9229
- segment.isPlan = segment.isPlan || isPlan;
9230
- }
9231
- return segment;
9232
- }
9233
- segment = {
9234
- itemId,
9235
- phase: phase ?? null,
9236
- outputKind: outputKind ?? 'commentary',
9237
- isPlan: Boolean(isPlan),
9238
- text: '',
9239
- completed: false,
9240
- startedAtMs: Date.now(),
9241
- completedAtMs: null,
9242
- messages: [],
9243
- voiceSnippetId: null,
9244
- };
9245
- active.segments.push(segment);
9246
- return segment;
9247
- }
9248
- function renderCollapsedCommentary(locale, segments) {
9249
- const firstAt = segments[0]?.startedAtMs ?? Date.now();
9250
- const lastSegment = segments[segments.length - 1];
9251
- const lastAt = lastSegment?.completedAtMs ?? lastSegment?.startedAtMs ?? firstAt;
9252
- const range = firstAt === lastAt
9253
- ? formatClockTimestamp(firstAt)
9254
- : `${formatClockTimestamp(firstAt)} - ${formatClockTimestamp(lastAt)}`;
9255
- const summary = locale === 'zh'
9256
- ? `过程汇报 · ${segments.length} 条 · ${range}`
9257
- : `Progress · ${segments.length} updates · ${range}`;
9258
- const maxBodyLength = TELEGRAM_RICH_MESSAGE_TEXT_LIMIT - summary.length - 1024;
9259
- const blocks = [];
9260
- let bodyLength = 0;
9261
- let omitted = 0;
9262
- for (const segment of segments) {
9263
- const endAt = segment.completedAtMs ?? segment.startedAtMs;
9264
- const timestamp = endAt === segment.startedAtMs
9265
- ? formatClockTimestamp(segment.startedAtMs)
9266
- : `${formatClockTimestamp(segment.startedAtMs)} - ${formatClockTimestamp(endAt)}`;
9267
- const block = `<h4>${escapeTelegramHtml(timestamp)}</h4>\n${renderTelegramMarkdownRichHtml(segment.text)}`;
9268
- if (bodyLength + block.length > maxBodyLength) {
9269
- omitted += 1;
9270
- continue;
9271
- }
9272
- blocks.push(block);
9273
- bodyLength += block.length;
9274
- }
9275
- if (omitted > 0) {
9276
- blocks.push(`<p>${escapeTelegramHtml(locale === 'zh' ? `另有 ${omitted} 条过长内容未收入归档` : `${omitted} oversized updates omitted`)}</p>`);
9277
- }
9278
- return telegramDetails(summary, blocks.join('\n'));
9279
- }
9280
- function formatClockTimestamp(timestampMs) {
9281
- const date = new Date(timestampMs);
9282
- const pad = (value) => String(value).padStart(2, '0');
9283
- return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
9284
- }
9285
- function createToolBatchState() {
9286
- return {
9287
- openCallIds: new Set(),
9288
- actionKeys: new Set(),
9289
- actionLines: [],
9290
- counts: { files: 0, searches: 0, edits: 0, commands: 0 },
9291
- finalizeTimer: null,
9292
- };
9293
- }
9294
- function incrementToolBatchCount(counts, kind) {
9295
- counts[kind] += 1;
9296
- }
9297
- function formatToolBatchStatus(locale, counts, actionLines, inProgress) {
9298
- const heading = formatToolBatchHeading(locale, counts, inProgress);
9299
- const detailLines = actionLines.slice(0, 6);
9300
- if (detailLines.length === 0) {
9301
- return heading;
9302
- }
9303
- return [heading, ...detailLines].join('\n');
9304
- }
9305
- function renderArchivedToolBatchStatus(locale, counts, actionLines) {
9306
- const text = formatToolBatchStatus(locale, counts, actionLines, false);
9307
- if (actionLines.length === 0) {
9308
- return { text, html: null };
9309
- }
9310
- const heading = formatToolBatchHeading(locale, counts, false);
9311
- const detailLines = actionLines
9312
- .slice(0, TOOL_ARCHIVE_MAX_LINES)
9313
- .map(line => truncateInline(line, TOOL_ARCHIVE_LINE_LIMIT));
9314
- const html = [
9315
- telegramBold(heading),
9316
- telegramExpandableBlockquote(detailLines.join('\n')),
9317
- ].join('\n');
9318
- return { text, html };
9319
- }
9320
- function formatToolBatchHeading(locale, counts, inProgress) {
9321
- const parts = formatToolBatchCountParts(locale, counts);
9322
- const hasBrowse = counts.files > 0 || counts.searches > 0;
9323
- const hasEdit = counts.edits > 0;
9324
- const hasCommand = counts.commands > 0;
9325
- let verb;
9326
- if (hasEdit && !hasBrowse && !hasCommand) {
9327
- verb = locale === 'zh' ? (inProgress ? '正在编辑' : '已编辑') : (inProgress ? 'Editing' : 'Edited');
9328
- }
9329
- else if (hasBrowse && !hasEdit && !hasCommand) {
9330
- verb = locale === 'zh' ? (inProgress ? '正在浏览' : '已浏览') : (inProgress ? 'Browsing' : 'Browsed');
9331
- }
9332
- else if (hasCommand && !hasBrowse && !hasEdit) {
9333
- verb = locale === 'zh' ? (inProgress ? '正在运行' : '已运行') : (inProgress ? 'Running' : 'Ran');
9334
- }
9335
- else {
9336
- verb = locale === 'zh' ? (inProgress ? '正在处理' : '已处理') : (inProgress ? 'Processing' : 'Processed');
9337
- }
9338
- if (parts.length === 0) {
9339
- return locale === 'zh'
9340
- ? `${verb}操作...`
9341
- : `${verb} operations...`;
9342
- }
9343
- return locale === 'zh'
9344
- ? `${verb} ${parts.join(',')}`
9345
- : `${verb} ${parts.join(', ')}`;
9346
- }
9347
- function formatToolBatchCountParts(locale, counts) {
9348
- const parts = [];
9349
- if (counts.files > 0) {
9350
- parts.push(locale === 'zh' ? `${counts.files} 个文件` : pluralize(counts.files, 'file'));
9351
- }
9352
- if (counts.searches > 0) {
9353
- parts.push(locale === 'zh' ? `${counts.searches} 个搜索` : pluralize(counts.searches, 'search'));
9354
- }
9355
- if (counts.edits > 0) {
9356
- parts.push(locale === 'zh' ? `${counts.edits} 个编辑` : pluralize(counts.edits, 'edit'));
9357
- }
9358
- if (counts.commands > 0) {
9359
- parts.push(locale === 'zh' ? `${counts.commands} 个命令` : pluralize(counts.commands, 'command'));
9360
- }
9361
- return parts;
9362
- }
9363
- function pluralize(count, noun) {
9364
- if (count === 1) {
9365
- return `1 ${noun}`;
9366
- }
9367
- const plural = noun === 'search'
9368
- ? 'searches'
9369
- : noun === 'file'
9370
- ? 'files'
9371
- : `${noun}s`;
9372
- return `${count} ${plural}`;
9373
- }
9374
- function describeExecCommand(event) {
9375
- const descriptors = (event.parsedCmd ?? [])
9376
- .map((entry) => describeParsedCommand(entry))
9377
- .filter((entry) => entry !== null);
9378
- if (descriptors.length > 0) {
9379
- return descriptors;
9380
- }
9381
- const commandText = renderShellCommand(event.command);
9382
- return [{
9383
- kind: 'commands',
9384
- key: `command:${commandText}`,
9385
- line: `$ ${commandText}`,
9386
- }];
9387
- }
9388
- function describeParsedCommand(entry) {
9389
- const type = typeof entry?.type === 'string' ? entry.type : '';
9390
- const path = compactPath(entry?.path ?? entry?.name ?? null);
9391
- const query = typeof entry?.query === 'string' ? entry.query : null;
9392
- switch (type) {
9393
- case 'search':
9394
- return {
9395
- kind: 'searches',
9396
- key: `search:${path ?? '.'}:${query ?? ''}`,
9397
- line: path ? `Searched for ${truncateInline(query || '', 80)} in ${path}` : `Searched for ${truncateInline(query || '', 80)}`,
9398
- };
9399
- case 'read':
9400
- return {
9401
- kind: 'files',
9402
- key: `read:${path ?? 'unknown'}`,
9403
- line: `Read ${path ?? 'file'}`,
9404
- };
9405
- case 'list_files':
9406
- return {
9407
- kind: 'files',
9408
- key: `list:${path ?? 'workspace'}`,
9409
- line: path ? `Listed ${path}` : 'Listed files',
9410
- };
9411
- case 'write':
9412
- case 'edit':
9413
- case 'apply_patch':
9414
- return {
9415
- kind: 'edits',
9416
- key: `${type}:${path ?? 'workspace'}`,
9417
- line: `Edited ${path ?? 'files'}`,
9418
- };
9419
- case 'move':
9420
- return {
9421
- kind: 'edits',
9422
- key: `${type}:${path ?? 'workspace'}`,
9423
- line: `Moved ${path ?? 'files'}`,
9424
- };
9425
- case 'copy':
9426
- return {
9427
- kind: 'edits',
9428
- key: `${type}:${path ?? 'workspace'}`,
9429
- line: `Copied ${path ?? 'files'}`,
9430
- };
9431
- case 'delete':
9432
- return {
9433
- kind: 'edits',
9434
- key: `${type}:${path ?? 'workspace'}`,
9435
- line: `Deleted ${path ?? 'files'}`,
9436
- };
9437
- case 'mkdir':
9438
- return {
9439
- kind: 'edits',
9440
- key: `${type}:${path ?? 'workspace'}`,
9441
- line: `Created ${path ?? 'files'}`,
9442
- };
9443
- default:
9444
- return null;
9445
- }
9446
- }
9447
- function compactPath(value) {
9448
- if (typeof value !== 'string' || !value.trim()) {
9449
- return null;
9450
- }
9451
- return value.replace(/^\.\//, '');
9452
- }
9453
- function renderShellCommand(command) {
9454
- if (command.length >= 3 && (command[0] === '/bin/zsh' || command[0] === 'zsh') && command[1] === '-lc') {
9455
- return command[2] ?? command.join(' ');
9456
- }
9457
- return command.join(' ');
9458
- }
9459
- function truncateInline(value, limit) {
9460
- if (value.length <= limit) {
9461
- return value;
9462
- }
9463
- return `${value.slice(0, Math.max(0, limit - 1))}…`;
9464
- }
9465
- function firstLine(value) {
9466
- return value.split(/\r?\n/, 1)[0]?.trim() || value.trim();
9467
- }
9468
- function parseReviewTarget(args) {
9469
- if (args.length === 0) {
9470
- return { type: 'uncommittedChanges' };
9471
- }
9472
- const [kind, ...rest] = args;
9473
- if (kind === 'base') {
9474
- const branch = rest.join(' ').trim();
9475
- return branch ? { type: 'baseBranch', branch } : null;
9476
- }
9477
- if (kind === 'commit') {
9478
- const sha = rest[0]?.trim();
9479
- return sha ? { type: 'commit', sha, title: rest.slice(1).join(' ').trim() || null } : null;
9480
- }
9481
- if (kind === 'custom') {
9482
- const instructions = rest.join(' ').trim();
9483
- return instructions ? { type: 'custom', instructions } : null;
9484
- }
9485
- return { type: 'custom', instructions: args.join(' ').trim() };
9486
- }
9487
- function findSkill(entries, name) {
9488
- const normalized = name.trim().toLowerCase();
9489
- for (const entry of entries) {
9490
- const skill = entry.skills.find(candidate => candidate.name.toLowerCase() === normalized
9491
- || candidate.displayName?.toLowerCase() === normalized);
9492
- if (skill)
9493
- return skill;
9494
- }
9495
- return null;
9496
- }
9497
- function formatSkillsMessage(locale, entries, query, forceReload) {
9498
- const skills = entries.flatMap(entry => entry.skills.map(skill => ({ cwd: entry.cwd, skill })))
9499
- .filter(entry => !query
9500
- || entry.skill.name.toLowerCase().includes(query.toLowerCase())
9501
- || entry.skill.description.toLowerCase().includes(query.toLowerCase()));
9502
- const lines = [
9503
- t(locale, 'skills_title'),
9504
- forceReload ? t(locale, 'skills_reloaded') : null,
9505
- query ? t(locale, 'skills_filter', { value: query }) : null,
9506
- ].filter((line) => Boolean(line));
9507
- if (skills.length === 0) {
9508
- lines.push(t(locale, 'skills_empty'));
9509
- }
9510
- else {
9511
- for (const { skill } of skills.slice(0, 30)) {
9512
- const enabled = skill.enabled ? 'on' : 'off';
9513
- const label = skill.displayName || skill.name;
9514
- lines.push(`${skill.enabled ? '*' : '-'} ${label} (${enabled})`);
9515
- const desc = skill.shortDescription || skill.description;
9516
- if (desc) {
9517
- lines.push(` ${truncateInline(desc, 120)}`);
9518
- }
9519
- }
9520
- if (skills.length > 30) {
9521
- lines.push(t(locale, 'list_truncated', { count: skills.length - 30 }));
9522
- }
9523
- }
9524
- const errors = entries.flatMap(entry => entry.errors);
9525
- if (errors.length > 0) {
9526
- lines.push('', t(locale, 'skills_errors'));
9527
- lines.push(...errors.slice(0, 5).map(error => `- ${truncateInline(error, 160)}`));
9528
- }
9529
- return lines.join('\n');
9530
- }
9531
- function formatSkillDetailMessage(locale, skill) {
9532
- return [
9533
- t(locale, 'skill_title', { name: skill.displayName || skill.name }),
9534
- t(locale, 'skill_name', { value: skill.name }),
9535
- t(locale, 'skill_enabled_state', { value: skill.enabled ? t(locale, 'yes') : t(locale, 'no') }),
9536
- t(locale, 'skill_scope', { value: skill.scope || t(locale, 'unknown') }),
9537
- t(locale, 'skill_path', { value: skill.path || t(locale, 'unknown') }),
9538
- '',
9539
- skill.description || skill.shortDescription || t(locale, 'empty'),
9540
- skill.defaultPrompt ? `\n${t(locale, 'skill_default_prompt', { value: skill.defaultPrompt })}` : '',
9541
- ].filter(Boolean).join('\n');
9542
- }
9543
- function formatMcpStatusMessage(locale, statuses) {
9544
- const lines = [t(locale, 'mcp_title')];
9545
- if (statuses.length === 0) {
9546
- lines.push(t(locale, 'mcp_empty'));
9547
- return lines.join('\n');
9548
- }
9549
- for (const status of statuses) {
9550
- lines.push(`${status.name}: ${status.authStatus}`);
9551
- lines.push(` tools: ${status.toolNames.length ? status.toolNames.slice(0, 12).join(', ') : '-'}`);
9552
- if (status.resourceUris.length > 0) {
9553
- lines.push(` resources: ${status.resourceUris.slice(0, 5).join(', ')}`);
9554
- }
9555
- if (status.resourceTemplateUris.length > 0) {
9556
- lines.push(` templates: ${status.resourceTemplateUris.slice(0, 5).join(', ')}`);
9557
- }
9558
- }
9559
- return lines.join('\n');
9560
- }
9561
- function formatMcpResourceMessage(locale, server, uri, contents) {
9562
- const lines = [t(locale, 'mcp_resource_title', { server, uri })];
9563
- if (contents.length === 0) {
9564
- lines.push(t(locale, 'mcp_resource_empty'));
9565
- return lines.join('\n');
9566
- }
9567
- for (const content of contents.slice(0, 5)) {
9568
- lines.push(`- ${content.type}${content.mimeType ? ` (${content.mimeType})` : ''}${content.uri ? ` ${content.uri}` : ''}`);
9569
- if (content.text) {
9570
- lines.push(truncateInline(content.text, 1500));
9571
- }
9572
- else if (content.blob) {
9573
- lines.push(t(locale, 'mcp_resource_blob', { size: content.blob.length }));
9574
- }
9575
- }
9576
- return lines.join('\n');
9577
- }
9578
- function formatDiffMessage(locale, diff) {
9579
- const clipped = diff.length > 3500 ? `${diff.slice(0, 3500)}\n...` : diff;
9580
- return [
9581
- telegramBold(t(locale, 'diff_title')),
9582
- telegramExpandableBlockquote(clipped),
9583
- ].join('\n');
9584
- }
9585
- function formatRichDiffMessage(locale, diff) {
9586
- const clipped = clipRichMessageText(diff, Math.min(24_000, TELEGRAM_RICH_MESSAGE_TEXT_LIMIT - 1024));
9587
- const summary = locale === 'zh' ? '展开 diff' : 'Expand diff';
9588
- const footer = locale === 'zh'
9589
- ? 'FoxClaw · sendRichMessage · details/pre/code'
9590
- : 'FoxClaw · sendRichMessage · details/pre/code';
9591
- return [
9592
- `<h3>${escapeTelegramHtml(t(locale, 'diff_title'))}</h3>`,
9593
- telegramDetails(summary, telegramPreCode(clipped, 'diff')),
9594
- `<footer>${escapeTelegramHtml(footer)}</footer>`,
9595
- ].join('\n');
9596
- }
9597
- function formatRichDemoMessage(locale) {
9598
- const title = 'FoxClaw RichMessage';
9599
- const intro = locale === 'zh'
9600
- ? '这条消息通过 Telegram Bot API sendRichMessage 发送,用来验证 Rich Message 在真实客户端里的渲染。'
9601
- : 'This message is sent through Telegram Bot API sendRichMessage to verify Rich Message rendering in a real client.';
9602
- const detailsSummary = locale === 'zh' ? '展开 details + pre 示例' : 'Open details + pre sample';
9603
- const diffSample = [
9604
- 'diff --git a/src/telegram/rich.ts b/src/telegram/rich.ts',
9605
- '+ sendRichMessage({ rich_message: { html } })',
9606
- '+ <details><summary>Expandable</summary>...</details>',
9607
- '+ <table bordered striped>...</table>',
9608
- ].join('\n');
9609
- const tableCaption = locale === 'zh' ? 'FoxClaw 可用 rich 面' : 'FoxClaw rich surfaces';
9610
- const surfaceHeader = locale === 'zh' ? '功能面' : 'Surface';
9611
- const richHeader = locale === 'zh' ? 'Rich 用法' : 'Rich usage';
9612
- const tableRows = locale === 'zh'
9613
- ? [
9614
- ['`/diff`', 'details + pre/code'],
9615
- ['工具状态', 'details/list'],
9616
- ['`/status`', 'table'],
9617
- ]
9618
- : [
9619
- ['`/diff`', 'details + pre/code'],
9620
- ['Tool status', 'details/list'],
9621
- ['`/status`', 'table'],
9622
- ];
9623
- const listItems = locale === 'zh'
9624
- ? ['Gateway 已接入 sendRichMessage', '/diff 已优先使用 RichMessage', '失败会回退到 Telegram HTML']
9625
- : ['Gateway now supports sendRichMessage', '/diff prefers RichMessage', 'Failures fall back to Telegram HTML'];
9626
- const detailBody = [
9627
- `<p>${escapeTelegramHtml(locale === 'zh' ? '下面是 rich pre/code block,客户端支持时会按代码块渲染。' : 'This is a rich pre/code block rendered as code by supported clients.')}</p>`,
9628
- telegramPreCode(diffSample, 'diff'),
9629
- ].join('\n');
9630
- return [
9631
- `<h2>${escapeTelegramHtml(title)}</h2>`,
9632
- `<p><b>Bot API 10.1</b> ${escapeTelegramHtml(intro)}</p>`,
9633
- '<hr/>',
9634
- '<table bordered striped>',
9635
- `<caption>${escapeTelegramHtml(tableCaption)}</caption>`,
9636
- `<tr><th>${escapeTelegramHtml(surfaceHeader)}</th><th>${escapeTelegramHtml(richHeader)}</th></tr>`,
9637
- ...tableRows.map(([surface, usage]) => `<tr><td>${escapeTelegramHtml(surface)}</td><td>${escapeTelegramHtml(usage)}</td></tr>`),
9638
- '</table>',
9639
- telegramDetails(detailsSummary, detailBody, true),
9640
- '<ul>',
9641
- ...listItems.map(item => `<li>${escapeTelegramHtml(item)}</li>`),
9642
- '</ul>',
9643
- '<footer>FoxClaw · sendRichMessage</footer>',
9644
- ].join('\n');
9645
- }
9646
- function formatRichDemoFallbackMessage(locale) {
9647
- const lines = locale === 'zh'
9648
- ? [
9649
- 'RichMessage fallback 预览',
9650
- 'Gateway 已接入 sendRichMessage',
9651
- '/diff 已优先使用 RichMessage',
9652
- '如果你看到这条 HTML fallback,说明 Telegram rich API 返回了失败,正常功能仍可用。',
9653
- ]
9654
- : [
9655
- 'RichMessage fallback preview',
9656
- 'Gateway now supports sendRichMessage',
9657
- '/diff prefers RichMessage',
9658
- 'If you see this HTML fallback, Telegram rich API failed but normal messaging still works.',
9659
- ];
9660
- return [
9661
- telegramBold('FoxClaw RichMessage'),
9662
- telegramExpandableBlockquote(lines.join('\n')),
9663
- telegramPreCode('sendRichMessage({ rich_message: { html } })', 'typescript'),
9664
- ].join('\n');
9665
- }
9666
- function formatRichInternalMessage(title, text) {
9667
- const sections = splitRichInternalSections(text);
9668
- const body = sections.map(formatRichInternalSection).filter(Boolean).join('\n');
9669
- return [
9670
- `<h3>${escapeTelegramHtml(title)}</h3>`,
9671
- body || `<p>${escapeTelegramHtml(text)}</p>`,
9672
- telegramDetails('Plain text', telegramPreCode(text, 'text')),
9673
- '<footer>FoxClaw · RichMessage</footer>',
9674
- ].join('\n');
9675
- }
9676
- function splitRichInternalSections(text) {
9677
- const sections = [];
9678
- let current = [];
9679
- for (const rawLine of text.split('\n')) {
9680
- const line = rawLine.trimEnd();
9681
- if (!line.trim()) {
9682
- if (current.length > 0) {
9683
- sections.push(current);
9684
- current = [];
9685
- }
9686
- continue;
9687
- }
9688
- current.push(line);
9689
- }
9690
- if (current.length > 0) {
9691
- sections.push(current);
9692
- }
9693
- return sections;
9694
- }
9695
- function formatRichInternalSection(lines) {
9696
- const candidateRows = lines
9697
- .map(parseRichAuthCandidateRow)
9698
- .filter((row) => row !== null);
9699
- if (candidateRows.length > 0) {
9700
- const nonCandidateLines = lines.filter(line => parseRichAuthCandidateRow(line) === null);
9701
- return [
9702
- nonCandidateLines.length > 0 ? formatRichInternalSectionWithoutCandidates(nonCandidateLines) : '',
9703
- formatRichAuthCandidateTable(candidateRows),
9704
- ].filter(Boolean).join('\n');
9705
- }
9706
- return formatRichInternalSectionWithoutCandidates(lines);
9707
- }
9708
- function formatRichInternalSectionWithoutCandidates(lines) {
9709
- const rows = lines
9710
- .map(parseRichInternalKeyValue)
9711
- .filter((row) => row !== null);
9712
- if (rows.length >= Math.max(2, Math.floor(lines.length * 0.6))) {
9713
- return [
9714
- '<table bordered striped>',
9715
- ...rows.map(([key, value]) => (`<tr><td>${escapeTelegramHtml(key)}</td><td>${formatRichInternalValue(value)}</td></tr>`)),
9716
- '</table>',
9717
- ].join('\n');
9718
- }
9719
- return [
9720
- '<ul>',
9721
- ...lines.map(line => `<li>${formatRichInternalValue(line.replace(/^\s*[-*]\s+/, '').replace(/^\s*\d+[.)]\s+/, ''))}</li>`),
9722
- '</ul>',
9723
- ].join('\n');
9724
- }
9725
- function parseRichAuthCandidateRow(line) {
9726
- const match = line.match(/^\s*(\d+)\.\s+(.+)$/);
9727
- if (!match) {
9728
- return null;
9729
- }
9730
- let body = match[2].trim();
9731
- let status = '';
9732
- const statusMatch = body.match(/^(.*?)\s+(\[[^\]]+\])$/);
9733
- if (statusMatch) {
9734
- body = statusMatch[1].trim();
9735
- status = statusMatch[2].replace(/^\[|\]$/g, '');
9736
- }
9737
- const current = body.endsWith(' *');
9738
- if (current) {
9739
- body = body.slice(0, -2).trimEnd();
9740
- }
9741
- const parts = body.split('|');
9742
- const name = parts.pop()?.trim() ?? '';
9743
- if (!name) {
9744
- return null;
9745
- }
9746
- const quotas = parts.map(parseRichAuthQuotaCell);
9747
- const statusParts = splitRichAuthStatus(status);
9748
- return {
9749
- index: match[1],
9750
- quotaA: quotas[0]?.value ?? '-',
9751
- quotaAReset: quotas[0]?.reset ?? '-',
9752
- quotaB: quotas[1]?.value ?? '-',
9753
- quotaBReset: quotas[1]?.reset ?? '-',
9754
- name,
9755
- current,
9756
- enabled: statusParts.health === 'disabled' || statusParts.health === '已禁用'
9757
- ? false
9758
- : statusParts.health === '-' ? null : true,
9759
- ...statusParts,
9760
- };
9761
- }
9762
- function formatRichAuthCandidateTable(rows) {
9763
- return [
9764
- '<table bordered striped>',
9765
- '<tr><th>#</th><th>Quota A</th><th>A reset</th><th>Quota B</th><th>B reset</th><th>Auth</th><th>Current</th><th>Plan</th><th>Health</th><th>Last refresh</th><th>Expiry</th><th>Risk</th><th>Command</th></tr>',
9766
- ...rows.map(row => [
9767
- '<tr>',
9768
- `<td>${escapeTelegramHtml(row.index)}</td>`,
9769
- `<td>${escapeTelegramHtml(row.quotaA)}</td>`,
9770
- `<td>${escapeTelegramHtml(row.quotaAReset)}</td>`,
9771
- `<td>${escapeTelegramHtml(row.quotaB)}</td>`,
9772
- `<td>${escapeTelegramHtml(row.quotaBReset)}</td>`,
9773
- `<td>${formatRichAuthCommandLink(`/auth use ${row.index}`, row.name)}</td>`,
9774
- `<td>${row.current ? 'yes' : '-'}</td>`,
9775
- `<td>${escapeTelegramHtml(row.plan)}</td>`,
9776
- `<td>${escapeTelegramHtml(row.health)}</td>`,
9777
- `<td>${escapeTelegramHtml(row.refresh)}</td>`,
9778
- `<td>${escapeTelegramHtml(row.expiry)}</td>`,
9779
- `<td>${escapeTelegramHtml(row.risk)}</td>`,
9780
- `<td>${formatRichAuthCommandCell(row)}</td>`,
9781
- '</tr>',
9782
- ].join('')),
9783
- '</table>',
9784
- ].join('\n');
9785
- }
9786
- function splitRichAuthStatus(status) {
9787
- const parts = status.split(' · ').map(part => part.trim()).filter(Boolean);
9788
- const refreshIndex = parts.findIndex(part => /^(refreshed|刷新于)\b/i.test(part));
9789
- const refresh = refreshIndex >= 0 ? parts.splice(refreshIndex, 1)[0] : '-';
9790
- const expiryIndex = parts.findIndex(part => /^(expires|过期于)\b/i.test(part));
9791
- const expiry = expiryIndex >= 0 ? parts.splice(expiryIndex, 1)[0] : '-';
9792
- const plan = parts.length > 1 ? parts.shift() : '-';
9793
- const health = parts.shift() ?? '-';
9794
- return {
9795
- plan,
9796
- health,
9797
- refresh,
9798
- expiry,
9799
- risk: formatRichAuthRisk(health),
9800
- };
9801
- }
9802
- function formatRichAuthRisk(health) {
9803
- if (/not recently refreshed|长期未刷新/i.test(health)) {
9804
- return `stale >${CODEX_AUTH_STALE_CREDENTIAL_DAYS}d`;
9805
- }
9806
- if (/needs login repair|需要登录修复/i.test(health)) {
9807
- return 'repair';
9808
- }
9809
- if (/quota exhausted|额度耗尽/i.test(health)) {
9810
- return 'quota exhausted';
9811
- }
9812
- if (/invalid|无效/i.test(health)) {
9813
- return 'invalid';
9814
- }
9815
- if (/\blow\b|偏低|低/i.test(health)) {
9816
- return 'low quota';
9817
- }
9818
- if (/unknown|未知/i.test(health)) {
9819
- return 'unknown';
9820
- }
9821
- return '-';
9822
- }
9823
- function parseRichAuthQuotaCell(value) {
9824
- const trimmed = value.trim();
9825
- if (!trimmed || trimmed === '--' || trimmed === '—') {
9826
- return { value: '-', reset: '-' };
9827
- }
9828
- const [quotaPart, reset = '-'] = trimmed.split('@', 2);
9829
- const [windowLabel, percent] = quotaPart.split(':');
9830
- if (!windowLabel || percent === undefined) {
9831
- return { value: quotaPart, reset };
9832
- }
9833
- return { value: `${percent}%`, reset };
9834
- }
9835
- function formatRichAuthCommandCell(row) {
9836
- const commands = [formatRichAuthCommandLink(`/auth use ${row.index}`, 'use')];
9837
- if (row.enabled === true) {
9838
- commands.push(formatRichAuthCommandLink(`/auth disable ${row.index}`, 'disable'));
9839
- }
9840
- else if (row.enabled === false) {
9841
- commands.push(formatRichAuthCommandLink(`/auth enable ${row.index}`, 'enable'));
9842
- }
9843
- return commands.join(' ');
9844
- }
9845
- function formatRichAuthCommandLink(command, label) {
9846
- const url = `tg://msg?text=${encodeURIComponent(command)}`;
9847
- return `<a href="${escapeTelegramHtml(url)}">${escapeTelegramHtml(label)}</a>`;
9848
- }
9849
- function parseRichInternalKeyValue(line) {
9850
- const separator = line.includes(':') ? ':' : ':';
9851
- const index = line.indexOf(separator);
9852
- if (index <= 0) {
9853
- return null;
9854
- }
9855
- const key = line.slice(0, index).trim();
9856
- const value = line.slice(index + separator.length).trim();
9857
- if (!key || !value || key.length > 80) {
9858
- return null;
9859
- }
9860
- return [key, value];
9861
- }
9862
- function formatRichInternalValue(value) {
9863
- const escaped = escapeTelegramHtml(value);
9864
- return escaped.replace(/`([^`]+)`/g, '<code>$1</code>');
9865
- }
9866
- function renderTelegramTable(headers, rows) {
9867
- return [
9868
- '<table bordered striped>',
9869
- `<thead><tr>${headers.map(header => `<th>${escapeTelegramHtml(header)}</th>`).join('')}</tr></thead>`,
9870
- `<tbody>${rows.map(row => `<tr>${row.map(cell => `<td>${escapeTelegramHtml(cell)}</td>`).join('')}</tr>`).join('')}</tbody>`,
9871
- '</table>',
9872
- ].join('');
9873
- }
9874
- function formatSelfUpdateVersionTransition(fromVersion, toVersion, locale) {
9875
- if (!fromVersion && !toVersion) {
9876
- return locale === 'zh' ? '未检测' : 'Not detected';
9877
- }
9878
- return `${fromVersion ?? '?'} -> ${toVersion ?? '?'}`;
9879
- }
9880
- function formatCodexUpdateState(status) {
9881
- if (status.codexFromVersion && status.codexToVersion) {
9882
- if (status.codexFromVersion === status.codexToVersion) {
9883
- return status.locale === 'zh' ? '已是最新版本' : 'Already current';
9884
- }
9885
- return status.locale === 'zh' ? '升级完成' : 'Updated';
9886
- }
9887
- return status.codexUpdate ?? (status.locale === 'zh' ? '未执行升级' : 'Not updated');
9888
- }
9889
- function formatSelfUpdateBroadcastLine(locale, targetVersion, broadcast) {
9890
- if (broadcast.state === 'disabled') {
9891
- return locale === 'zh' ? '未启用跨节点同步' : 'Cross-node sync is disabled';
9892
- }
9893
- if (broadcast.state === 'pending') {
9894
- return locale === 'zh'
9895
- ? `目标 ${targetVersion ?? '?'},等待广播结果`
9896
- : `Target ${targetVersion ?? '?'}; waiting for broadcast result`;
9897
- }
9898
- if (broadcast.peers.length === 0) {
9899
- return locale === 'zh' ? '广播完成,无已配置 peer' : 'Broadcast completed; no configured peers';
9900
- }
9901
- return locale === 'zh'
9902
- ? `已发送 ${broadcast.sent} 个 peer:${broadcast.peers.join('、')}`
9903
- : `Sent to ${broadcast.sent} peers: ${broadcast.peers.join(', ')}`;
9904
- }
9905
- function clipRichMessageText(value, limit) {
9906
- if (value.length <= limit) {
9907
- return value;
9908
- }
9909
- return `${value.slice(0, Math.max(0, limit - 4))}\n...`;
9910
- }
9911
- function formatLoadedThreadsMessage(locale, threadIds) {
9912
- const lines = [t(locale, 'loaded_title')];
9913
- if (threadIds.length === 0) {
9914
- lines.push(t(locale, 'loaded_empty'));
9915
- return lines.join('\n');
9916
- }
9917
- for (const threadId of threadIds.slice(0, 30)) {
9918
- lines.push(`- ${threadId}`);
9919
- }
9920
- if (threadIds.length > 30) {
9921
- lines.push(t(locale, 'list_truncated', { count: threadIds.length - 30 }));
9922
- }
9923
- return lines.join('\n');
9924
- }
9925
- function formatHooksMessage(locale, entries) {
9926
- const hooks = entries.flatMap(entry => entry.hooks.map(hook => ({ cwd: entry.cwd, hook })));
9927
- const lines = [t(locale, 'hooks_title')];
9928
- if (hooks.length === 0) {
9929
- lines.push(t(locale, 'hooks_empty'));
9930
- }
9931
- else {
9932
- for (const { hook } of hooks.slice(0, 30)) {
9933
- lines.push(`${hook.enabled ? '*' : '-'} ${hook.key} (${hook.eventName}, ${hook.trustStatus})`);
9934
- const detail = [hook.pluginId ? `plugin=${hook.pluginId}` : null, hook.statusMessage, hook.command].filter(Boolean).join(' · ');
9935
- if (detail) {
9936
- lines.push(` ${truncateInline(detail, 140)}`);
9937
- }
9938
- }
9939
- if (hooks.length > 30) {
9940
- lines.push(t(locale, 'list_truncated', { count: hooks.length - 30 }));
9941
- }
9942
- }
9943
- const warnings = entries.flatMap(entry => entry.warnings);
9944
- const errors = entries.flatMap(entry => entry.errors);
9945
- if (warnings.length > 0) {
9946
- lines.push('', t(locale, 'hooks_warnings'));
9947
- lines.push(...warnings.slice(0, 5).map(warning => `- ${truncateInline(warning, 160)}`));
9948
- }
9949
- if (errors.length > 0) {
9950
- lines.push('', t(locale, 'hooks_errors'));
9951
- lines.push(...errors.slice(0, 5).map(error => `- ${truncateInline(`${error.path}: ${error.message}`, 180)}`));
9952
- }
9953
- return lines.join('\n');
9954
- }
9955
- function formatPluginsMessage(locale, marketplaces, query) {
9956
- const plugins = marketplaces.flatMap(marketplace => marketplace.plugins.map(plugin => ({ marketplace, plugin })))
9957
- .filter(entry => !query
9958
- || entry.plugin.name.toLowerCase().includes(query.toLowerCase())
9959
- || entry.plugin.id.toLowerCase().includes(query.toLowerCase()));
9960
- const lines = [t(locale, 'plugins_title')];
9961
- if (query)
9962
- lines.push(t(locale, 'plugins_filter', { value: query }));
9963
- if (plugins.length === 0) {
9964
- lines.push(t(locale, 'plugins_empty'));
9965
- }
9966
- else {
9967
- for (const { marketplace, plugin } of plugins.slice(0, 30)) {
9968
- const state = `${plugin.installed ? 'installed' : 'not-installed'}, ${plugin.enabled ? 'on' : 'off'}`;
9969
- lines.push(`${plugin.enabled ? '*' : '-'} ${plugin.name} (${state})`);
9970
- lines.push(` ${plugin.id} · ${marketplace.displayName || marketplace.name}`);
9971
- }
9972
- if (plugins.length > 30) {
9973
- lines.push(t(locale, 'list_truncated', { count: plugins.length - 30 }));
9974
- }
9975
- }
9976
- return lines.join('\n');
9977
- }
9978
- function formatPluginDetailMessage(locale, plugin) {
9979
- const lines = [
9980
- t(locale, 'plugin_title', { name: plugin.summary.name || plugin.summary.id }),
9981
- `id: ${plugin.summary.id}`,
9982
- `marketplace: ${plugin.marketplaceName}`,
9983
- `state: ${plugin.summary.installed ? 'installed' : 'not-installed'}, ${plugin.summary.enabled ? 'on' : 'off'}`,
9984
- plugin.description ? truncateInline(plugin.description, 400) : null,
9985
- ].filter((line) => Boolean(line));
9986
- if (plugin.skills.length > 0) {
9987
- lines.push('', `skills: ${plugin.skills.slice(0, 12).map(skill => skill.name).join(', ')}`);
9988
- }
9989
- if (plugin.hooks.length > 0) {
9990
- lines.push(`hooks: ${plugin.hooks.slice(0, 12).map(hook => `${hook.eventName}:${hook.key}`).join(', ')}`);
9991
- }
9992
- if (plugin.apps.length > 0) {
9993
- lines.push(`apps: ${plugin.apps.slice(0, 12).map(app => app.name || app.id).join(', ')}`);
9994
- }
9995
- if (plugin.mcpServers.length > 0) {
9996
- lines.push(`mcp: ${plugin.mcpServers.slice(0, 12).join(', ')}`);
9997
- }
9998
- return lines.join('\n');
9999
- }
10000
- function formatPluginSkillMessage(locale, marketplace, plugin, skill, contents) {
10001
- const lines = [t(locale, 'plugin_skill_title', { marketplace, plugin, skill })];
10002
- if (!contents) {
10003
- lines.push(t(locale, 'plugin_skill_empty'));
10004
- return lines.join('\n');
10005
- }
10006
- lines.push(truncateInline(contents, 3500));
10007
- return lines.join('\n');
10008
- }
10009
- function formatAppsMessage(locale, apps, forceRefetch) {
10010
- const lines = [t(locale, 'apps_title')];
10011
- if (forceRefetch)
10012
- lines.push(t(locale, 'apps_reloaded'));
10013
- if (apps.length === 0) {
10014
- lines.push(t(locale, 'apps_empty'));
10015
- }
10016
- else {
10017
- for (const app of apps.slice(0, 30)) {
10018
- const state = `${app.isEnabled ? 'on' : 'off'}, ${app.isAccessible ? 'accessible' : 'blocked'}`;
10019
- lines.push(`${app.isEnabled ? '*' : '-'} ${app.name} (${state})`);
10020
- if (app.description)
10021
- lines.push(` ${truncateInline(app.description, 120)}`);
10022
- }
10023
- if (apps.length > 30) {
10024
- lines.push(t(locale, 'list_truncated', { count: apps.length - 30 }));
10025
- }
10026
- }
10027
- return lines.join('\n');
10028
- }
10029
- function formatFeaturesMessage(locale, features) {
10030
- const lines = [t(locale, 'features_title')];
10031
- if (features.length === 0) {
10032
- lines.push(t(locale, 'features_empty'));
10033
- return lines.join('\n');
10034
- }
10035
- for (const feature of features.slice(0, 40)) {
10036
- lines.push(`${feature.enabled ? '*' : '-'} ${feature.displayName || feature.name} (${feature.stage})`);
10037
- if (feature.description)
10038
- lines.push(` ${truncateInline(feature.description, 120)}`);
10039
- }
10040
- if (features.length > 40) {
10041
- lines.push(t(locale, 'list_truncated', { count: features.length - 40 }));
10042
- }
10043
- return lines.join('\n');
10044
- }
10045
- function formatConfigMessage(locale, result, appConfig, authPoolStats) {
10046
- const config = result.config && typeof result.config === 'object' ? result.config : {};
10047
- const layers = Array.isArray(result.layers) ? result.layers : [];
10048
- const keys = ['model', 'model_provider', 'approval_policy', 'sandbox_mode', 'web_search', 'service_tier', 'profile', 'review_model'];
10049
- const lines = [t(locale, 'config_title')];
10050
- for (const key of keys) {
10051
- const value = config[key];
10052
- lines.push(`${key}: ${value === null || value === undefined ? '-' : formatConfigValue(value)}`);
10053
- }
10054
- lines.push(t(locale, 'config_layers', { count: layers.length }));
10055
- lines.push('');
10056
- lines.push(t(locale, 'config_foxclaw_title'));
10057
- lines.push(t(locale, 'config_auth_auto_delete_needs_repair', {
10058
- value: t(locale, appConfig.authAutoDeleteNeedsRepair ? 'yes' : 'no'),
10059
- }));
10060
- lines.push(`AUTH_AUTO_DELETE_NEEDS_REPAIR=${appConfig.authAutoDeleteNeedsRepair ? 'true' : 'false'}`);
10061
- lines.push(t(locale, 'config_delete_tool_details_after_final', {
10062
- value: t(locale, appConfig.telegramDeleteToolDetailsAfterFinal ? 'yes' : 'no'),
10063
- }));
10064
- lines.push(`TELEGRAM_DELETE_TOOL_DETAILS_AFTER_FINAL=${appConfig.telegramDeleteToolDetailsAfterFinal ? 'true' : 'false'}`);
10065
- lines.push(t(locale, 'config_panel_ttl', { value: formatDurationMs(locale, appConfig.telegramPanelTtlMs) }));
10066
- lines.push(`TELEGRAM_PANEL_TTL_MS=${appConfig.telegramPanelTtlMs}`);
10067
- lines.push(formatCodexAuthPoolSummary(locale, authPoolStats));
10068
- lines.push(...formatCodexApiProviderConfigLines(locale, appConfig));
10069
- return lines.join('\n');
10070
- }
10071
- function formatCodexApiProviderConfigLines(locale, appConfig) {
10072
- if (appConfig.codexApiProviders.length === 0) {
10073
- return [t(locale, 'config_api_providers_none')];
10074
- }
10075
- const lines = [t(locale, 'config_api_providers_title')];
10076
- lines.push(t(locale, 'config_api_default_provider', { value: appConfig.codexApiDefaultProvider ?? '-' }));
10077
- for (const provider of appConfig.codexApiProviders) {
10078
- lines.push(t(locale, 'config_api_provider_line', {
10079
- id: provider.id,
10080
- model: provider.model ?? '-',
10081
- base: provider.baseUrl,
10082
- env: provider.apiKeyEnv,
10083
- key: process.env[provider.apiKeyEnv]?.trim() ? t(locale, 'yes') : t(locale, 'no'),
10084
- }));
10085
- if (provider.chatCompletionsOnly) {
10086
- lines.push(t(locale, 'config_api_provider_chat_warning', { id: provider.id }));
10087
- }
10088
- }
10089
- return lines;
10090
- }
10091
- function formatCodexAuthPoolSummary(locale, stats) {
10092
- return t(locale, 'auth_pool_summary', {
10093
- total: stats.totalSeen,
10094
- alive: stats.alive,
10095
- deleted: stats.deletedInvalid,
10096
- });
10097
- }
10098
- function isInvalidCodexAuthDeleteReason(reason) {
10099
- return reason === AUTH_DELETE_REASON_NEEDS_REPAIR;
10100
- }
10101
- function configKeyboard(locale, appConfig) {
10102
- const authAutoDeleteEnabled = appConfig.authAutoDeleteNeedsRepair;
10103
- const deleteToolDetailsEnabled = appConfig.telegramDeleteToolDetailsAfterFinal;
10104
- return [
10105
- [{
10106
- text: t(locale, authAutoDeleteEnabled ? 'button_config_auth_auto_delete_off' : 'button_config_auth_auto_delete_on'),
10107
- callback_data: `config:auth_auto_delete:${authAutoDeleteEnabled ? 'off' : 'on'}`,
10108
- }],
10109
- [{
10110
- text: t(locale, deleteToolDetailsEnabled ? 'button_config_delete_tool_details_off' : 'button_config_delete_tool_details_on'),
10111
- callback_data: `config:delete_tool_details:${deleteToolDetailsEnabled ? 'off' : 'on'}`,
10112
- }],
10113
- ];
10114
- }
10115
- function formatDurationMs(locale, value) {
10116
- if (value <= 0) {
10117
- return locale === 'zh' ? '关闭' : 'disabled';
10118
- }
10119
- if (value % 60_000 === 0) {
10120
- const minutes = value / 60_000;
10121
- return locale === 'zh' ? `${minutes} 分钟` : `${minutes} min`;
10122
- }
10123
- if (value % 1000 === 0) {
10124
- const seconds = value / 1000;
10125
- return locale === 'zh' ? `${seconds} 秒` : `${seconds} sec`;
10126
- }
10127
- return `${value}ms`;
10128
- }
10129
- function parseConfigBooleanArg(value) {
10130
- const normalized = value?.trim().toLowerCase();
10131
- if (!normalized)
10132
- return null;
10133
- if (['1', 'true', 'yes', 'on', 'enable', 'enabled'].includes(normalized))
10134
- return true;
10135
- if (['0', 'false', 'no', 'off', 'disable', 'disabled'].includes(normalized))
10136
- return false;
10137
- return null;
10138
- }
10139
- async function writeEnvBoolean(envPath, key, enabled) {
10140
- await fs.mkdir(path.dirname(envPath), { recursive: true });
10141
- const nextLine = `${key}=${enabled ? 'true' : 'false'}`;
10142
- let contents = '';
10143
- try {
10144
- contents = await fs.readFile(envPath, 'utf8');
10145
- }
10146
- catch {
10147
- await fs.writeFile(envPath, `${nextLine}\n`, { encoding: 'utf8', mode: 0o600 });
10148
- return;
10149
- }
10150
- const pattern = new RegExp(`(^|\\n)[ \\t#]*${escapeRegExp(key)}\\s*=.*(?=\\r?\\n|$)`);
10151
- const nextContents = pattern.test(contents)
10152
- ? contents.replace(pattern, (_match, prefix) => `${prefix}${nextLine}`)
10153
- : `${contents}${contents.endsWith('\n') || contents.length === 0 ? '' : '\n'}${nextLine}\n`;
10154
- await fs.writeFile(envPath, nextContents, { encoding: 'utf8', mode: 0o600 });
10155
- }
10156
- function escapeRegExp(value) {
10157
- return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
10158
- }
10159
- function formatRequirementsMessage(locale, requirements) {
10160
- const lines = [t(locale, 'requirements_title')];
10161
- if (!requirements) {
10162
- lines.push(t(locale, 'requirements_empty'));
10163
- return lines.join('\n');
10164
- }
10165
- lines.push(`approval: ${requirements.allowedApprovalPolicies?.join(', ') ?? '-'}`);
10166
- lines.push(`sandbox: ${requirements.allowedSandboxModes?.join(', ') ?? '-'}`);
10167
- lines.push(`web_search: ${requirements.allowedWebSearchModes?.join(', ') ?? '-'}`);
10168
- lines.push(`residency: ${requirements.enforceResidency ?? '-'}`);
10169
- if (requirements.featureRequirements) {
10170
- const features = Object.entries(requirements.featureRequirements)
10171
- .map(([key, value]) => `${key}=${value ? 'on' : 'off'}`)
10172
- .join(', ');
10173
- lines.push(`features: ${truncateInline(features, 500)}`);
10174
- }
10175
- return lines.join('\n');
10176
- }
10177
- function formatProviderMessage(locale, capabilities) {
10178
- return [
10179
- t(locale, 'provider_title'),
10180
- `webSearch: ${t(locale, capabilities.webSearch ? 'yes' : 'no')}`,
10181
- `imageGeneration: ${t(locale, capabilities.imageGeneration ? 'yes' : 'no')}`,
10182
- `namespaceTools: ${t(locale, capabilities.namespaceTools ? 'yes' : 'no')}`,
10183
- ].join('\n');
10184
- }
10185
- function formatGoalMessage(locale, goal, prefix) {
10186
- const lines = [t(locale, 'goal_title')];
10187
- if (prefix) {
10188
- lines.push(prefix);
10189
- }
10190
- if (!goal) {
10191
- lines.push(t(locale, 'goal_empty'));
10192
- return lines.join('\n');
10193
- }
10194
- lines.push(t(locale, 'goal_status', { value: formatGoalStatus(locale, goal.status) }));
10195
- lines.push(t(locale, 'goal_objective', { value: goal.objective || t(locale, 'empty') }));
10196
- lines.push(t(locale, 'goal_budget', {
10197
- value: goal.tokenBudget === null ? t(locale, 'none') : t(locale, 'goal_tokens', { value: formatTokenCount(goal.tokenBudget) }),
10198
- }));
10199
- lines.push(t(locale, 'goal_usage', {
10200
- tokens: formatTokenCount(goal.tokensUsed),
10201
- seconds: formatCompactNumber(goal.timeUsedSeconds),
10202
- }));
10203
- if (goal.updatedAt > 0) {
10204
- lines.push(t(locale, 'goal_updated_at', { value: formatLocalTimestamp(goal.updatedAt) }));
10205
- }
10206
- return lines.join('\n');
10207
- }
10208
- function formatHistoryMessage(locale, threadId, turns) {
10209
- const lines = [t(locale, 'history_title', { threadId })];
10210
- if (turns.length === 0) {
10211
- lines.push(t(locale, 'history_empty'));
10212
- return lines.join('\n');
10213
- }
10214
- for (const turn of turns.slice(0, 30)) {
10215
- const time = turn.startedAt ? formatLocalTimestamp(turn.startedAt) : t(locale, 'unknown');
10216
- const itemSummary = summarizeTurnItems(turn.items);
10217
- const error = turn.error ? ` · ${truncateInline(turn.error, 80)}` : '';
10218
- lines.push(`- ${turn.turnId} · ${turn.status} · ${time}${error}`);
10219
- if (itemSummary) {
10220
- lines.push(` ${itemSummary}`);
10221
- }
10222
- }
10223
- return lines.join('\n');
10224
- }
10225
- function formatFuzzyFilesMessage(locale, query, root, files) {
10226
- const lines = [t(locale, 'files_title', { query, root })];
10227
- if (files.length === 0) {
10228
- lines.push(t(locale, 'files_empty'));
10229
- return lines.join('\n');
10230
- }
10231
- for (const file of files.slice(0, 25)) {
10232
- const displayPath = file.path || file.fileName || '(unknown)';
10233
- lines.push(`- ${displayPath}${file.matchType ? ` (${file.matchType})` : ''}`);
10234
- }
10235
- if (files.length > 25) {
10236
- lines.push(t(locale, 'list_truncated', { count: files.length - 25 }));
10237
- }
10238
- return lines.join('\n');
10239
- }
10240
- function formatRemoteStatusMessage(locale, status) {
10241
- const lines = [t(locale, 'remote_title')];
10242
- if (!status) {
10243
- lines.push(t(locale, 'remote_unknown'));
10244
- return lines.join('\n');
10245
- }
10246
- lines.push(t(locale, 'remote_status', { value: status.status }));
10247
- lines.push(t(locale, 'remote_environment', { value: status.environmentId ?? t(locale, 'none') }));
10248
- lines.push(t(locale, 'remote_installation', { value: status.installationId ?? t(locale, 'none') }));
10249
- return lines.join('\n');
10250
- }
10251
- function formatGoalStatus(locale, status) {
10252
- switch (status) {
10253
- case 'paused':
10254
- return t(locale, 'goal_status_paused');
10255
- case 'budgetLimited':
10256
- return t(locale, 'goal_status_budget_limited');
10257
- case 'complete':
10258
- return t(locale, 'goal_status_complete');
10259
- default:
10260
- return t(locale, 'goal_status_active');
10261
- }
10262
- }
10263
- function summarizeTurnItems(items) {
10264
- const counts = new Map();
10265
- for (const item of items) {
10266
- const type = item.type || 'item';
10267
- counts.set(type, (counts.get(type) ?? 0) + 1);
10268
- }
10269
- return [...counts.entries()]
10270
- .slice(0, 8)
10271
- .map(([type, count]) => `${type}=${count}`)
10272
- .join(', ');
10273
- }
10274
- function turnHasRelayableOutcome(turn) {
10275
- if (turn.error?.trim()) {
10276
- return true;
10277
- }
10278
- return turn.items.some((item) => {
10279
- const type = item.type.toLowerCase();
10280
- if (!item.text?.trim()) {
10281
- return false;
10282
- }
10283
- if (type === 'plan') {
10284
- return true;
10285
- }
10286
- if (type !== 'agentmessage' && type !== 'assistantmessage') {
10287
- return false;
10288
- }
10289
- const phase = item.phase?.toLowerCase() ?? null;
10290
- return phase === null || phase.startsWith('final');
10291
- });
10292
- }
10293
- function mapGoalNotification(raw) {
10294
- if (!raw || typeof raw !== 'object') {
10295
- return null;
10296
- }
10297
- const status = raw.status === 'paused' || raw.status === 'budgetLimited' || raw.status === 'complete'
10298
- ? raw.status
10299
- : 'active';
10300
- return {
10301
- threadId: String(raw.threadId ?? ''),
10302
- objective: String(raw.objective ?? ''),
10303
- status,
10304
- tokenBudget: numberOrNull(raw.tokenBudget),
10305
- tokensUsed: numberOrNull(raw.tokensUsed) ?? 0,
10306
- timeUsedSeconds: numberOrNull(raw.timeUsedSeconds) ?? 0,
10307
- createdAt: numberOrNull(raw.createdAt) ?? 0,
10308
- updatedAt: numberOrNull(raw.updatedAt) ?? 0,
10309
- };
10310
- }
10311
- function formatWarningNotification(locale, method, params) {
10312
- if (method === 'configWarning') {
10313
- return [
10314
- t(locale, 'warning_config_title'),
10315
- String(params?.summary ?? t(locale, 'unknown')),
10316
- params?.path ? `path: ${String(params.path)}` : null,
10317
- params?.details ? truncateInline(String(params.details), 600) : null,
10318
- ].filter(Boolean).join('\n');
10319
- }
10320
- if (method === 'deprecationNotice') {
10321
- return [
10322
- t(locale, 'warning_deprecation_title'),
10323
- String(params?.summary ?? t(locale, 'unknown')),
10324
- params?.details ? truncateInline(String(params.details), 600) : null,
10325
- ].filter(Boolean).join('\n');
10326
- }
10327
- if (method === 'guardianWarning') {
10328
- return `${t(locale, 'warning_guardian_title')}\n${String(params?.message ?? t(locale, 'unknown'))}`;
10329
- }
10330
- return `${t(locale, 'warning_title')}\n${String(params?.message ?? t(locale, 'unknown'))}`;
10331
- }
10332
- function isCodexTransportFallbackWarning(method, params) {
10333
- if (method !== 'warning') {
10334
- return false;
10335
- }
10336
- return /falling back from websockets? to https transport/i.test(String(params?.message ?? ''));
10337
- }
10338
- function normalizeThreadStatusLabel(raw) {
10339
- if (typeof raw === 'string') {
10340
- return raw;
10341
- }
10342
- if (typeof raw?.type === 'string') {
10343
- return raw.type;
10344
- }
10345
- return 'unknown';
10346
- }
10347
- function formatThreadTokenUsage(raw) {
10348
- const total = numberOrNull(raw?.last?.totalTokens ?? raw?.last?.total_tokens);
10349
- const limit = numberOrNull(raw?.modelContextWindow ?? raw?.model_context_window);
10350
- if (total === null || limit === null || total <= 0 || limit <= 0) {
10351
- return null;
10352
- }
10353
- const rawPercent = Math.round((total / limit) * 100);
10354
- const percent = Math.min(100, rawPercent);
10355
- return rawPercent >= 85 ? { percent, total, limit } : null;
10356
- }
10357
- function numberOrNull(value) {
10358
- if (typeof value === 'number') {
10359
- return Number.isFinite(value) ? value : null;
10360
- }
10361
- if (typeof value === 'string' && value.trim()) {
10362
- const parsed = Number(value);
10363
- return Number.isFinite(parsed) ? parsed : null;
10364
- }
10365
- return null;
10366
- }
10367
- function parsePositiveInt(value, fallback, min, max) {
10368
- if (value === undefined || value.trim() === '') {
10369
- return fallback;
10370
- }
10371
- const parsed = Number.parseInt(value, 10);
10372
- if (!Number.isFinite(parsed) || parsed < min || parsed > max) {
10373
- return null;
10374
- }
10375
- return parsed;
10376
- }
10377
- function formatConfigValue(value) {
10378
- if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
10379
- return String(value);
10380
- }
10381
- if (Array.isArray(value)) {
10382
- return value.map(formatConfigValue).join(', ');
10383
- }
10384
- if (value && typeof value === 'object') {
10385
- const keys = Object.keys(value);
10386
- if (keys.length === 1) {
10387
- return keys[0];
10388
- }
10389
- }
10390
- return truncateInline(JSON.stringify(value), 160);
10391
- }
10392
- function formatRawLabel(value) {
10393
- if (value === null || value === undefined) {
10394
- return 'unknown';
10395
- }
10396
- if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
10397
- return String(value);
10398
- }
10399
- if (typeof value === 'object') {
10400
- const keys = Object.keys(value);
10401
- if (keys.length === 1) {
10402
- return keys[0];
10403
- }
10404
- }
10405
- return truncateInline(JSON.stringify(value), 160);
10406
- }
10407
- function renderMcpElicitationMessage(locale, record, decision) {
10408
- const lines = [
10409
- t(locale, 'mcp_elicitation_requested'),
10410
- t(locale, 'mcp_server_name', { value: record.serverName }),
10411
- t(locale, 'line_thread', { value: record.threadId }),
10412
- ];
10413
- if (record.turnId)
10414
- lines.push(t(locale, 'line_turn', { value: record.turnId }));
10415
- lines.push(t(locale, 'mcp_elicitation_message', { value: record.message || t(locale, 'empty') }));
10416
- if (record.url) {
10417
- lines.push(t(locale, 'mcp_elicitation_url', { value: record.url }));
10418
- }
10419
- if (record.mode === 'form') {
10420
- lines.push(t(locale, 'mcp_elicitation_schema', { value: truncateInline(JSON.stringify(record.requestedSchema ?? {}), 1200) }));
10421
- lines.push(record.content === null
10422
- ? t(locale, 'mcp_elicitation_reply_json')
10423
- : t(locale, 'mcp_elicitation_json_ready'));
10424
- }
10425
- if (decision) {
10426
- lines.push(t(locale, 'line_decision', { value: decision }));
10427
- }
10428
- if (!decision && parseWeixinBridgeScope(record.chatId)) {
10429
- lines.push('', t(locale, 'weixin_copy_paste_divider'), t(locale, 'weixin_copy_mcp_title'));
10430
- if (record.mode === 'form') {
10431
- lines.push(t(locale, 'weixin_copy_mcp_json_hint'));
10432
- }
10433
- lines.push(`/mcpel ${record.localId} accept`, `/mcpel ${record.localId} decline`, `/mcpel ${record.localId} cancel`);
10434
- }
10435
- return lines.join('\n');
10436
- }
10437
- function mcpElicitationKeyboard(locale, record) {
10438
- return [[
10439
- { text: t(locale, 'button_accept'), callback_data: `mcpel:${record.localId}:accept` },
10440
- { text: t(locale, 'button_decline'), callback_data: `mcpel:${record.localId}:decline` },
10441
- { text: t(locale, 'button_cancel'), callback_data: `mcpel:${record.localId}:cancel` },
10442
- ]];
10443
- }
10444
- function formatPermissionRequestSummary(locale, permissions) {
10445
- const lines = [];
10446
- if (permissions?.network?.enabled !== undefined && permissions.network.enabled !== null) {
10447
- lines.push(t(locale, 'permission_network', { value: permissions.network.enabled ? t(locale, 'yes') : t(locale, 'no') }));
10448
- }
10449
- const fsPerms = permissions?.fileSystem;
10450
- if (fsPerms?.read?.length) {
10451
- lines.push(t(locale, 'permission_read_paths', { value: fsPerms.read.slice(0, 5).join(', ') }));
10452
- }
10453
- if (fsPerms?.write?.length) {
10454
- lines.push(t(locale, 'permission_write_paths', { value: fsPerms.write.slice(0, 5).join(', ') }));
10455
- }
10456
- if (Array.isArray(fsPerms?.entries) && fsPerms.entries.length > 0) {
10457
- lines.push(t(locale, 'permission_entries', { value: truncateInline(JSON.stringify(fsPerms.entries.slice(0, 5)), 500) }));
10458
- }
10459
- return lines.join('\n');
10460
- }
10461
- function resolveScopeMessageTarget(scopeId) {
10462
- if (scopeId.startsWith(BRIDGE_SCOPE_WEIXIN_PREFIX)) {
10463
- const parsed = parseWeixinBridgeScope(scopeId);
10464
- return parsed ? { chatId: parsed.fromUserId, chatType: 'private', topicId: null } : null;
10465
- }
10466
- try {
10467
- const parsed = parseTelegramTargetFromBridgeScope(scopeId);
10468
- return { chatId: parsed.chatId, chatType: parsed.topicId === null ? 'private' : 'supergroup', topicId: parsed.topicId };
10469
- }
10470
- catch {
10471
- return null;
10472
- }
10473
- }
10474
- function restartPreviewRecoveryKey(preview) {
10475
- return `${preview.scopeId}:${preview.turnId}:${preview.messageId}`;
10476
- }
10477
- function seedObservedTurnCursor(turn) {
10478
- const agentItems = turn.items.filter((item) => {
10479
- const type = item.type.toLowerCase();
10480
- return type === 'agentmessage' || type === 'assistantmessage' || type === 'plan';
10481
- });
10482
- const itemTexts = {};
10483
- for (const item of agentItems) {
10484
- itemTexts[item.itemId] = item.text ?? '';
10485
- }
10486
- return {
10487
- turnId: turn.turnId,
10488
- itemTexts,
10489
- completedItemIds: agentItems.map((item) => item.itemId),
10490
- };
10491
- }
10492
- function observerCursorFromActiveTurn(active) {
10493
- const itemTexts = {};
10494
- const completedItemIds = [];
10495
- for (const segment of active.segments) {
10496
- itemTexts[segment.itemId] = segment.text;
10497
- if (segment.completed) {
10498
- completedItemIds.push(segment.itemId);
10499
- }
10500
- }
10501
- return {
10502
- turnId: active.turnId,
10503
- itemTexts,
10504
- completedItemIds,
10505
- };
10506
- }
10507
- function approvalKeyboard(locale, localId) {
10508
- return [[
10509
- { text: t(locale, 'button_allow'), callback_data: `approval:${localId}:accept` },
10510
- { text: t(locale, 'button_allow_session'), callback_data: `approval:${localId}:session` },
10511
- { text: t(locale, 'button_deny'), callback_data: `approval:${localId}:deny` },
10512
- ]];
10513
- }
10514
- function activeTurnKeyboard(locale, turnId) {
10515
- return [[
10516
- { text: t(locale, 'button_interrupt'), callback_data: `turn:interrupt:${turnId}` },
10517
- ]];
10518
- }
10519
- function whereKeyboard(locale, hasBinding) {
10520
- const firstRow = [
10521
- { text: t(locale, 'button_permissions'), callback_data: 'nav:permissions' },
10522
- { text: t(locale, 'button_models'), callback_data: 'nav:models' },
10523
- ];
10524
- const secondRow = [{ text: t(locale, 'button_threads'), callback_data: 'nav:threads' }];
10525
- if (!hasBinding) {
10526
- return [firstRow, secondRow];
10527
- }
10528
- return [
10529
- [{ text: t(locale, 'button_reveal'), callback_data: 'nav:reveal' }, { text: t(locale, 'button_permissions'), callback_data: 'nav:permissions' }],
10530
- [{ text: t(locale, 'button_models'), callback_data: 'nav:models' }, { text: t(locale, 'button_threads'), callback_data: 'nav:threads' }],
10531
- ];
10532
- }
10533
- function renderApprovalMessage(locale, record, decision, renderScopeId = record.chatId) {
10534
- const lines = [
10535
- t(locale, 'approval_requested', {
10536
- kind: record.kind === 'fileChange'
10537
- ? t(locale, 'approval_kind_fileChange')
10538
- : record.kind === 'permissions'
10539
- ? t(locale, 'approval_kind_permissions')
10540
- : t(locale, 'approval_kind_command'),
10541
- }),
10542
- t(locale, 'line_thread', { value: record.threadId }),
10543
- t(locale, 'line_turn', { value: record.turnId }),
10544
- ];
10545
- if (record.command)
10546
- lines.push(t(locale, 'line_command', { value: record.command }));
10547
- if (record.cwd)
10548
- lines.push(t(locale, 'line_cwd', { value: record.cwd }));
10549
- if (record.reason)
10550
- lines.push(t(locale, 'line_reason', { value: record.reason }));
10551
- if (record.kind === 'permissions') {
10552
- const permissions = parseApprovalPayload(record.payloadJson)?.permissions ?? {};
10553
- const summary = formatPermissionRequestSummary(locale, permissions);
10554
- if (summary) {
10555
- lines.push(summary);
10556
- }
10557
- }
10558
- if (decision) {
10559
- const decisionKey = decision === 'accept'
10560
- ? 'approval_decision_accept'
10561
- : decision === 'session'
10562
- ? 'approval_decision_session'
10563
- : 'approval_decision_deny';
10564
- lines.push(t(locale, 'line_decision', { value: t(locale, decisionKey) }));
10565
- }
10566
- if (!decision && parseWeixinBridgeScope(renderScopeId)) {
10567
- lines.push('', t(locale, 'weixin_copy_paste_divider'), t(locale, 'weixin_copy_approval_title'), `/approve ${record.localId} allow`, `/approve ${record.localId} session`, `/approve ${record.localId} deny`);
10568
- }
10569
- return lines.join('\n');
10570
- }
10571
- function mapApprovalDecision(record, action) {
10572
- if (record.kind === 'permissions') {
10573
- const requested = parseApprovalPayload(record.payloadJson)?.permissions ?? {};
10574
- if (action === 'deny') {
10575
- return { permissions: {}, scope: 'turn' };
10576
- }
10577
- return {
10578
- permissions: grantedPermissionsFromRequest(requested),
10579
- scope: action === 'session' ? 'session' : 'turn',
10580
- };
10581
- }
10582
- const decision = action === 'accept'
10583
- ? 'accept'
10584
- : action === 'session'
10585
- ? 'acceptForSession'
10586
- : 'decline';
10587
- return { decision };
10588
- }
10589
- function parseApprovalPayload(payloadJson) {
10590
- if (!payloadJson)
10591
- return null;
10592
- try {
10593
- return JSON.parse(payloadJson);
10594
- }
10595
- catch {
10596
- return null;
10597
- }
10598
- }
10599
- function grantedPermissionsFromRequest(requested) {
10600
- const result = {};
10601
- if (requested?.network) {
10602
- result.network = requested.network;
10603
- }
10604
- if (requested?.fileSystem) {
10605
- result.fileSystem = requested.fileSystem;
10606
- }
10607
- return result;
10608
- }
10609
- function toErrorMeta(error) {
10610
- if (error instanceof Error) {
10611
- return { message: error.message, stack: error.stack };
10612
- }
10613
- return { error: String(error) };
10614
- }
10615
- function formatUserError(error) {
10616
- if (error instanceof Error) {
10617
- return error.message;
10618
- }
10619
- return String(error);
10620
- }
10621
- function normalizeRequestedCollaborationMode(value) {
10622
- const normalized = value.trim().toLowerCase();
10623
- if (normalized === 'plan' || normalized === '计划' || normalized === '规划') {
10624
- return 'plan';
10625
- }
10626
- if (normalized === 'default' || normalized === 'agent' || normalized === '默认') {
10627
- return 'default';
10628
- }
10629
- return null;
10630
- }
10631
- function normalizeRequestedActiveTurnMessageMode(value) {
10632
- const normalized = value.trim().toLowerCase();
10633
- if (normalized === 'steer' || normalized === 'guide' || normalized === '引导') {
10634
- return 'steer';
10635
- }
10636
- if (normalized === 'queue' || normalized === '排队') {
10637
- return 'queue';
10638
- }
10639
- return null;
10640
- }
10641
- function normalizeApprovalTextAction(value) {
10642
- const normalized = value.trim().toLowerCase();
10643
- if (normalized === 'allow' || normalized === 'accept' || normalized === 'yes' || normalized === '同意' || normalized === '允许') {
10644
- return 'accept';
10645
- }
10646
- if (normalized === 'session' || normalized === 'allow_session' || normalized === '本会话' || normalized === '本会话允许') {
10647
- return 'session';
10648
- }
10649
- if (normalized === 'deny' || normalized === 'decline' || normalized === 'no' || normalized === '拒绝') {
10650
- return 'deny';
10651
- }
10652
- return null;
10653
- }
10654
- function normalizePlanImplementationTextAction(value) {
10655
- const normalized = value.trim().toLowerCase();
10656
- if (normalized === 'run' || normalized === 'execute' || normalized === '执行') {
10657
- return 'run';
10658
- }
10659
- if (normalized === 'fresh' || normalized === 'clear' || normalized === '新上下文') {
10660
- return 'fresh';
10661
- }
10662
- if (normalized === 'stay' || normalized === 'plan' || normalized === '继续规划') {
10663
- return 'stay';
10664
- }
10665
- return null;
10666
- }
10667
- function normalizeMcpElicitationTextAction(value) {
10668
- const normalized = value.trim().toLowerCase();
10669
- if (normalized === 'accept' || normalized === 'allow' || normalized === '同意' || normalized === '接受') {
10670
- return 'accept';
10671
- }
10672
- if (normalized === 'decline' || normalized === 'deny' || normalized === '拒绝') {
10673
- return 'decline';
10674
- }
10675
- if (normalized === 'cancel' || normalized === '取消') {
10676
- return 'cancel';
10677
- }
10678
- return null;
10679
- }
10680
- function resolveCollaborationMode(mode) {
10681
- return mode ?? DEFAULT_COLLABORATION_MODE;
10682
- }
10683
- function attachedThreadKey(scopeId, threadId) {
10684
- return `${scopeId}:${threadId}`;
10685
- }
10686
- function activeTurnKey(scopeId, turnId) {
10687
- return `${encodeURIComponent(scopeId)}:${encodeURIComponent(turnId)}`;
10688
- }
10689
- function parseActiveTurnKey(key) {
10690
- const split = key.indexOf(':');
10691
- if (split === -1) {
10692
- return null;
10693
- }
10694
- return {
10695
- scopeId: decodeURIComponent(key.slice(0, split)),
10696
- turnId: decodeURIComponent(key.slice(split + 1)),
10697
- };
10698
- }
10699
- function codexAuthDir(explicitAuthDir = null) {
10700
- return explicitAuthDir || process.env.CODEX_AUTH_DIR || path.join(os.homedir(), '.codex');
10701
- }
10702
- async function listCodexAuthState(disabledNames = new Set(), candidateStates = new Map(), explicitAuthDir = null) {
10703
- const authDir = codexAuthDir(explicitAuthDir);
10704
- const authPath = path.join(authDir, 'auth.json');
10705
- const currentTargetPath = await resolveCurrentAuthTarget(authDir, authPath);
10706
- const candidates = [];
10707
- const entries = await fs.readdir(authDir, { withFileTypes: true }).catch((error) => {
10708
- if (isFileMissingError(error)) {
10709
- return [];
10710
- }
10711
- throw error;
10712
- });
10713
- for (const entry of entries) {
10714
- if (!isCodexAuthCandidateName(entry.name)) {
10715
- continue;
10716
- }
10717
- const candidatePath = path.join(authDir, entry.name);
10718
- const stat = await fs.stat(candidatePath).catch(() => null);
10719
- if (!stat?.isFile()) {
10720
- continue;
10721
- }
10722
- candidates.push({
10723
- name: entry.name,
10724
- path: candidatePath,
10725
- isCurrent: currentTargetPath === candidatePath,
10726
- disabled: disabledNames.has(entry.name),
10727
- state: candidateStates.get(entry.name) ?? 'active',
10728
- mtimeMs: stat.mtimeMs,
10729
- credentialKind: 'invalid',
10730
- credentialLastRefreshMs: null,
10731
- credentialExpiresAtMs: null,
10732
- quota: null,
10733
- });
10734
- }
10735
- candidates.sort((left, right) => left.name.localeCompare(right.name, undefined, { numeric: true }));
10736
- return {
10737
- authDir,
10738
- authPath,
10739
- currentTargetPath,
10740
- currentLabel: currentTargetPath ? await authPathDisplayLabel(currentTargetPath) : null,
10741
- candidates,
10742
- };
10743
- }
10744
- function isCodexAuthCandidateName(name) {
10745
- if (name === 'auth.json' || name.startsWith('.auth.json.')) {
10746
- return false;
10747
- }
10748
- return name.startsWith('auth.json_') || name.startsWith('auth.json.') || name.startsWith('auth.json-');
10749
- }
10750
- async function isCodexApiKeyAuthCandidate(candidatePath) {
10751
- try {
10752
- const parsed = JSON.parse(await fs.readFile(candidatePath, 'utf8'));
10753
- return typeof parsed.openai_api_key === 'string' && parsed.openai_api_key.trim().length > 0;
10754
- }
10755
- catch {
10756
- return false;
10757
- }
10758
- }
10759
- function codexAuthCandidateNameFromAddName(raw) {
10760
- const normalized = raw.trim();
10761
- if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(normalized)) {
10762
- return null;
10763
- }
10764
- const name = normalized.startsWith('auth.json_')
10765
- || normalized.startsWith('auth.json.')
10766
- || normalized.startsWith('auth.json-')
10767
- ? normalized
10768
- : `auth.json_${normalized}`;
10769
- return isCodexAuthCandidateName(name) ? name : null;
10770
- }
10771
- async function resolveCurrentAuthTarget(authDir, authPath) {
10772
- const stat = await fs.lstat(authPath).catch(() => null);
10773
- if (!stat) {
10774
- return null;
10775
- }
10776
- if (stat.isSymbolicLink()) {
10777
- const target = await fs.readlink(authPath);
10778
- return path.resolve(authDir, target);
10779
- }
10780
- return authPath;
10781
- }
10782
- async function resolveAuthFinalTargetPath(startPath) {
10783
- let currentPath = startPath;
10784
- const seen = new Set();
10785
- for (let depth = 0; depth < 20; depth += 1) {
10786
- const absolutePath = path.resolve(currentPath);
10787
- if (seen.has(absolutePath)) {
10788
- return currentPath;
10789
- }
10790
- seen.add(absolutePath);
10791
- const stat = await fs.lstat(currentPath).catch(() => null);
10792
- if (!stat?.isSymbolicLink()) {
10793
- return currentPath;
10794
- }
10795
- const target = await fs.readlink(currentPath);
10796
- currentPath = path.resolve(path.dirname(currentPath), target);
10797
- }
10798
- return currentPath;
10799
- }
10800
- async function authPathDisplayLabel(authPath) {
10801
- return path.basename(await resolveAuthFinalTargetPath(authPath));
10802
- }
10803
- async function pointCodexAuthAtTarget(authDir, authPath, targetPath) {
10804
- await fs.mkdir(authDir, { recursive: true });
10805
- const tempLink = path.join(authDir, `.auth.json.${process.pid}.${Date.now()}.tmp`);
10806
- try {
10807
- await fs.symlink(targetPath, tempLink);
10808
- await fs.rename(tempLink, authPath);
10809
- }
10810
- catch (error) {
10811
- await fs.unlink(tempLink).catch(() => { });
10812
- throw error;
10813
- }
10814
- }
10815
- async function restoreCodexAuthTarget(authDir, authPath, targetPath, regularAuthContents) {
10816
- if (regularAuthContents !== null) {
10817
- await fs.mkdir(authDir, { recursive: true });
10818
- const temporary = path.join(authDir, `.auth.json.${process.pid}.${Date.now()}.restore`);
10819
- try {
10820
- await fs.writeFile(temporary, regularAuthContents, { mode: 0o600 });
10821
- await fs.rename(temporary, authPath);
10822
- }
10823
- catch (error) {
10824
- await fs.unlink(temporary).catch(() => undefined);
10825
- throw error;
10826
- }
10827
- return;
10828
- }
10829
- if (targetPath) {
10830
- await pointCodexAuthAtTarget(authDir, authPath, targetPath);
10831
- return;
10832
- }
10833
- await fs.unlink(authPath).catch((error) => {
10834
- if (!isFileMissingError(error)) {
10835
- throw error;
10836
- }
10837
- });
10838
- }
10839
- async function switchCodexAuth(targetPath, explicitAuthDir = null) {
10840
- const state = await listCodexAuthState(new Set(), new Map(), explicitAuthDir);
10841
- const candidate = state.candidates.find(entry => entry.path === targetPath);
10842
- if (!candidate) {
10843
- throw new Error(`Auth candidate is no longer available: ${path.basename(targetPath)}`);
10844
- }
10845
- await pointCodexAuthAtTarget(state.authDir, state.authPath, candidate.path);
10846
- return {
10847
- fromLabel: state.currentLabel,
10848
- toLabel: await authPathDisplayLabel(candidate.path),
10849
- };
10850
- }
10851
- function parseCodexAuthListRequest(action, args) {
10852
- if (action === 'list') {
10853
- return { searchTerm: args.slice(1).join(' ').trim() || null };
10854
- }
10855
- if (action === 'filter') {
10856
- const filter = args[1]?.toLowerCase();
10857
- return filter === 'all' || filter === 'enabled' || filter === 'attention'
10858
- ? { filter }
10859
- : null;
10860
- }
10861
- if (action === 'page') {
10862
- const page = Number.parseInt(args[1] ?? '', 10);
10863
- return Number.isFinite(page) && page > 0
10864
- ? { offset: (page - 1) * CODEX_AUTH_LIST_PAGE_SIZE }
10865
- : null;
10866
- }
10867
- return null;
10868
- }
10869
- function createPendingAuthChoiceList(chatId, candidates, view = {}) {
10870
- const record = {
10871
- localId: crypto.randomBytes(8).toString('hex'),
10872
- chatId,
10873
- messageId: null,
10874
- candidates,
10875
- createdAt: Date.now(),
10876
- offset: Math.max(0, view.offset ?? 0),
10877
- pageSize: CODEX_AUTH_LIST_PAGE_SIZE,
10878
- filter: view.filter ?? 'all',
10879
- searchTerm: view.searchTerm?.trim() || null,
10880
- };
10881
- clampCodexAuthListOffset(record);
10882
- return record;
10883
- }
10884
- function clampCodexAuthListOffset(view) {
10885
- const filteredCount = filterCodexAuthCandidates(view.candidates, view).length;
10886
- const finalOffset = filteredCount > 0
10887
- ? Math.floor((filteredCount - 1) / view.pageSize) * view.pageSize
10888
- : 0;
10889
- view.offset = Math.min(Math.max(0, view.offset), finalOffset);
10890
- }
10891
- function codexAuthListPage(candidates, view) {
10892
- const pageSize = Math.max(1, view?.pageSize ?? CODEX_AUTH_LIST_PAGE_SIZE);
10893
- const filter = view?.filter ?? 'all';
10894
- const searchTerm = view?.searchTerm?.trim() || null;
10895
- const indexed = filterCodexAuthCandidates(candidates, { filter, searchTerm });
10896
- const finalOffset = indexed.length > 0
10897
- ? Math.floor((indexed.length - 1) / pageSize) * pageSize
10898
- : 0;
10899
- const offset = Math.min(Math.max(0, view?.offset ?? 0), finalOffset);
10900
- return {
10901
- visible: indexed.slice(offset, offset + pageSize),
10902
- filteredCount: indexed.length,
10903
- offset,
10904
- pageIndex: Math.floor(offset / pageSize),
10905
- pageCount: Math.max(1, Math.ceil(indexed.length / pageSize)),
10906
- filter,
10907
- searchTerm,
10908
- };
10909
- }
10910
- function filterCodexAuthCandidates(candidates, view) {
10911
- const searchTerm = view.searchTerm?.trim().toLowerCase() || null;
10912
- return candidates
10913
- .map((candidate, index) => ({ candidate, index }))
10914
- .filter(({ candidate }) => !searchTerm || candidate.name.toLowerCase().includes(searchTerm))
10915
- .filter(({ candidate }) => view.filter === 'all'
10916
- || (view.filter === 'enabled' && !candidate.disabled && candidate.state !== 'needs_repair')
10917
- || (view.filter === 'attention' && codexAuthCandidateNeedsAttention(candidate)));
10918
- }
10919
- function renderAuthListMessage(locale, state, botLabel = null, includeWeixinCopyPaste = false, view = null) {
10920
- const lines = [
10921
- t(locale, 'auth_list_title'),
10922
- ];
10923
- if (botLabel) {
10924
- lines.push(t(locale, 'auth_bot', { value: botLabel }));
10925
- }
10926
- lines.push(t(locale, 'auth_current', {
10927
- value: state.currentLabel
10928
- ? formatCodexAuthCandidateDisplayName(state.currentLabel)
10929
- : t(locale, 'none'),
10930
- }), t(locale, 'auth_dir', { value: state.authDir }));
10931
- if (state.candidates.length === 0) {
10932
- lines.push(t(locale, 'auth_no_candidates'));
10933
- if (includeWeixinCopyPaste) {
10934
- lines.push('', t(locale, 'weixin_copy_paste_divider'), t(locale, 'weixin_copy_auth_title'), '/login_device', '/auth sync safe', '/auth reload', '/permissions');
10935
- }
10936
- return lines.join('\n');
10937
- }
10938
- lines.push(t(locale, 'auth_candidate_count', { value: state.candidates.length }));
10939
- lines.push(t(locale, 'auth_quota_legend'));
10940
- const page = codexAuthListPage(state.candidates, view);
10941
- if (page.searchTerm) {
10942
- lines.push(t(locale, 'auth_search', { value: page.searchTerm }));
10943
- }
10944
- if (page.filter !== 'all') {
10945
- lines.push(t(locale, 'auth_filter', { value: formatCodexAuthFilter(locale, page.filter) }));
10946
- }
10947
- if (page.filteredCount !== state.candidates.length || page.pageCount > 1) {
10948
- lines.push(t(locale, 'auth_page', {
10949
- from: page.filteredCount === 0 ? 0 : page.offset + 1,
10950
- to: page.offset + page.visible.length,
10951
- filtered: page.filteredCount,
10952
- total: state.candidates.length,
10953
- page: page.pageIndex + 1,
10954
- pages: page.pageCount,
10955
- }));
10956
- }
10957
- if (page.visible.length === 0) {
10958
- lines.push(t(locale, 'auth_no_matches'));
10959
- }
10960
- page.visible.forEach(({ candidate, index }) => {
10961
- const marker = candidate.isCurrent ? ' *' : '';
10962
- lines.push(`${index + 1}. ${formatAuthQuotaPrefix(locale, candidate.quota)}|${formatCodexAuthCandidateDisplayName(candidate.name)}${marker} ${formatCodexAuthCandidateStatus(locale, candidate)}`);
10963
- });
10964
- if (includeWeixinCopyPaste) {
10965
- lines.push('', t(locale, 'weixin_copy_paste_divider'), t(locale, 'weixin_copy_auth_title'), ...page.visible.map(({ index }) => `/auth use ${index + 1}`), ...page.visible.map(({ candidate, index }) => candidate.disabled
10966
- ? `/auth enable ${index + 1}`
10967
- : `/auth disable ${index + 1}`), '/auth filter all', '/auth filter enabled', '/auth filter attention', '/auth list <keyword>', '/login_device', '/auth sync safe', '/auth reload', '/permissions');
10968
- }
10969
- return lines.join('\n');
10970
- }
10971
- function authChoiceKeyboard(locale, record) {
10972
- const page = codexAuthListPage(record.candidates, record);
10973
- const rows = page.visible.map(({ candidate, index }) => [
10974
- {
10975
- text: clipButtonText(`${candidate.state === 'needs_repair' ? '? ' : candidate.isCurrent ? '✅ ' : '🔐 '}${formatAuthQuotaButtonPrefix(candidate.quota)}|${formatCodexAuthCandidateDisplayName(candidate.name)}${candidate.disabled ? ' · off' : ''}`),
10976
- callback_data: candidate.state === 'needs_repair' ? `auth:${record.localId}:repair:${index}` : `auth:${record.localId}:${index}`,
10977
- },
10978
- {
10979
- text: candidate.state === 'needs_repair' ? '?' : t(locale, candidate.disabled ? 'button_auth_disable' : 'button_auth_enable'),
10980
- callback_data: candidate.state === 'needs_repair' ? `auth:${record.localId}:repair:${index}` : `auth:${record.localId}:toggle:${index}`,
10981
- },
10982
- ]);
10983
- const navigationRow = [];
10984
- if (page.offset > 0) {
10985
- navigationRow.push({ text: t(locale, 'button_prev_page'), callback_data: `auth:${record.localId}:page:prev` });
10986
- }
10987
- if (page.offset + page.visible.length < page.filteredCount) {
10988
- navigationRow.push({ text: t(locale, 'button_next_page'), callback_data: `auth:${record.localId}:page:next` });
10989
- }
10990
- if (navigationRow.length > 0) {
10991
- rows.push(navigationRow);
10992
- }
10993
- rows.push([
10994
- authFilterButton(locale, record, 'all'),
10995
- authFilterButton(locale, record, 'enabled'),
10996
- authFilterButton(locale, record, 'attention'),
10997
- ]);
10998
- if (record.searchTerm) {
10999
- rows.push([{ text: t(locale, 'button_clear_filter'), callback_data: `auth:${record.localId}:clear_search` }]);
11000
- }
11001
- rows.push([{ text: t(locale, 'button_login_device'), callback_data: `auth:${record.localId}:login_device` }]);
11002
- rows.push([{ text: t(locale, 'button_auth_safe_sync'), callback_data: `auth:${record.localId}:safe_sync` }]);
11003
- return rows;
11004
- }
11005
- function formatCodexAuthCandidateDisplayName(name) {
11006
- const prefix = 'auth.json_';
11007
- return name.length > prefix.length && name.startsWith(prefix)
11008
- ? name.slice(prefix.length)
11009
- : name;
11010
- }
11011
- function authFilterButton(locale, record, filter) {
11012
- const label = t(locale, `button_auth_filter_${filter}`);
11013
- return {
11014
- text: record.filter === filter ? `☑️ ${label}` : label,
11015
- callback_data: `auth:${record.localId}:filter:${filter}`,
11016
- };
11017
- }
11018
- function codexAuthCandidateHealth(candidate) {
11019
- if (candidate.state === 'needs_repair') {
11020
- return 'needs_repair';
11021
- }
11022
- if (candidate.disabled) {
11023
- return 'disabled';
11024
- }
11025
- if (candidate.credentialKind === 'api-key') {
11026
- return 'api-key';
11027
- }
11028
- if (candidate.credentialKind === 'invalid') {
11029
- return 'invalid';
11030
- }
11031
- const remaining = [
11032
- candidate.quota?.primaryRemainingPercent,
11033
- candidate.quota?.secondaryRemainingPercent,
11034
- ].filter((value) => typeof value === 'number' && Number.isFinite(value));
11035
- if (remaining.some(value => value <= 0)) {
11036
- return 'exhausted';
11037
- }
11038
- if (remaining.some(value => value <= CODEX_AUTH_LOW_QUOTA_PERCENT)) {
11039
- return 'low';
11040
- }
11041
- if (candidate.credentialLastRefreshMs !== null
11042
- && candidate.credentialLastRefreshMs < Date.now() - CODEX_AUTH_STALE_CREDENTIAL_DAYS * 24 * 60 * 60_000) {
11043
- return 'stale';
11044
- }
11045
- if (remaining.length === 0) {
11046
- return 'unknown';
11047
- }
11048
- return 'ready';
11049
- }
11050
- function codexAuthCandidateNeedsAttention(candidate) {
11051
- const health = codexAuthCandidateHealth(candidate);
11052
- return health === 'stale'
11053
- || health === 'unknown'
11054
- || health === 'low'
11055
- || health === 'exhausted'
11056
- || health === 'needs_repair'
11057
- || health === 'invalid';
11058
- }
11059
- function formatCodexAuthCandidateStatus(locale, candidate) {
11060
- const details = [];
11061
- if (candidate.quota?.planType) {
11062
- details.push(formatPlanTypeLabel(candidate.quota.planType));
11063
- }
11064
- details.push(t(locale, `auth_candidate_health_${codexAuthCandidateHealth(candidate)}`));
11065
- if (candidate.credentialLastRefreshMs !== null) {
11066
- details.push(t(locale, 'auth_candidate_last_refresh', {
11067
- value: formatCompactAge(locale, Date.now() - candidate.credentialLastRefreshMs),
11068
- }));
11069
- }
11070
- if (candidate.credentialExpiresAtMs !== null) {
11071
- details.push(t(locale, 'auth_candidate_expires_at', {
11072
- value: formatUtcDateTime(candidate.credentialExpiresAtMs),
11073
- }));
11074
- }
11075
- return `[${details.join(' · ')}]`;
11076
- }
11077
- function formatCodexAuthFilter(locale, filter) {
11078
- return t(locale, `auth_filter_${filter}`);
11079
- }
11080
- function authRefreshAllConfirmKeyboard(locale, record) {
11081
- return [
11082
- [{ text: t(locale, 'button_auth_refresh_all_confirm'), callback_data: `auth:${record.localId}:refresh_all_confirm` }],
11083
- [{ text: t(locale, 'button_cancel'), callback_data: `auth:${record.localId}:refresh_all_cancel` }],
11084
- ];
11085
- }
11086
- function authRepairKeyboard(locale, record, index) {
11087
- return [
11088
- [{ text: t(locale, 'button_auth_repair_login'), callback_data: `auth:${record.localId}:repair_login:${index}` }],
11089
- [{ text: t(locale, 'button_auth_delete'), callback_data: `auth:${record.localId}:repair_delete:${index}` }],
11090
- [{ text: t(locale, 'button_cancel'), callback_data: `auth:${record.localId}:repair_cancel:${index}` }],
11091
- ];
11092
- }
11093
- function formatAuthRefreshAllResult(locale, result, mode = 'manual') {
11094
- const lines = [t(locale, mode === 'proactive' ? 'auth_proactive_refresh_done' : 'auth_refresh_all_done', {
11095
- refreshed: String(result.refreshed.length),
11096
- skipped: String(result.skipped.length),
11097
- failed: String(result.failed.length),
11098
- })];
11099
- if (result.refreshed.length > 0) {
11100
- lines.push(t(locale, 'auth_refresh_all_refreshed', { value: result.refreshed.join(', ') }));
11101
- }
11102
- if (result.skipped.length > 0) {
11103
- lines.push(t(locale, 'auth_refresh_all_skipped', { value: result.skipped.join(', ') }));
11104
- }
11105
- if (result.failed.length > 0) {
11106
- const details = result.failed
11107
- .slice(0, 5)
11108
- .map((failure) => `${failure.name}: ${failure.error}`)
11109
- .join('; ');
11110
- lines.push(t(locale, 'auth_refresh_all_failed', { value: details }));
11111
- }
11112
- return lines.join('\n');
11113
- }
11114
- function formatAuthClusterAuditResult(locale, outcome) {
11115
- const { audit, refresh, push } = outcome;
11116
- const lines = [t(locale, 'auth_cluster_audit_summary', {
11117
- responded: audit.nodesResponded,
11118
- expected: audit.nodesExpected,
11119
- checked: audit.checkedCandidates,
11120
- valid: audit.validCandidates,
11121
- invalid: audit.invalidCandidates,
11122
- })];
11123
- if (audit.synchronizedCandidates.length > 0) {
11124
- lines.push(t(locale, 'auth_cluster_audit_synced', { value: audit.synchronizedCandidates.join(', ') }));
11125
- }
11126
- if (audit.consensusInvalidCandidates.length > 0) {
11127
- lines.push(t(locale, 'auth_cluster_audit_repair', { value: audit.consensusInvalidCandidates.join(', ') }));
11128
- }
11129
- if (audit.missingPeers.length > 0) {
11130
- lines.push(t(locale, 'auth_cluster_audit_missing', { value: audit.missingPeers.join(', ') }));
11131
- }
11132
- if (audit.busyNodes.length > 0) {
11133
- lines.push(t(locale, 'auth_cluster_audit_busy', { value: audit.busyNodes.join(', ') }));
11134
- }
11135
- if (audit.identityConflicts.length > 0) {
11136
- lines.push(t(locale, 'auth_cluster_audit_conflicts', { value: audit.identityConflicts.join(', ') }));
11137
- }
11138
- if (outcome.refreshSkippedReason) {
11139
- lines.push(t(locale, 'auth_cluster_audit_refresh_skipped'));
11140
- }
11141
- else {
11142
- lines.push(formatAuthRefreshAllResult(locale, refresh, 'proactive'));
11143
- }
11144
- lines.push(t(locale, 'auth_cluster_audit_push', { sent: push.sent, skipped: push.skipped }));
11145
- return lines.join('\n');
11146
- }
11147
- function formatAuthSyncStatus(locale, status, proactiveRefresh = null) {
11148
- if (!status?.enabled) {
11149
- return t(locale, 'auth_sync_disabled');
11150
- }
11151
- const lines = [
11152
- t(locale, 'auth_sync_status_title'),
11153
- t(locale, 'auth_sync_status_node', { value: status.nodeId ?? t(locale, 'unknown') }),
11154
- t(locale, 'auth_sync_status_transport', { value: status.transportLabel ?? t(locale, 'unknown') }),
11155
- t(locale, 'auth_sync_status_peers', { value: status.peers.length === 0 ? t(locale, 'none') : status.peers.join(', ') }),
11156
- t(locale, 'auth_sync_status_pending', { value: status.pendingImports }),
11157
- t(locale, 'auth_sync_status_sent', { value: status.lastSentAt ?? t(locale, 'none') }),
11158
- t(locale, 'auth_sync_status_received', { value: status.lastReceivedAt ?? t(locale, 'none') }),
11159
- t(locale, 'auth_sync_status_imported', {
11160
- value: status.lastImportedAt
11161
- ? `${status.lastImportCandidate ?? t(locale, 'unknown')} @ ${status.lastImportedAt}`
11162
- : t(locale, 'none'),
11163
- }),
11164
- t(locale, 'auth_sync_status_pull', {
11165
- value: status.lastPullAt
11166
- ? `${status.lastPullCandidate ?? t(locale, 'unknown')} @ ${status.lastPullAt}`
11167
- : t(locale, 'none'),
11168
- }),
11169
- ];
11170
- if (status.activeLeaseId) {
11171
- lines.push(t(locale, 'auth_sync_status_lease', { value: status.activeLeaseId }));
11172
- }
11173
- if (status.lastError) {
11174
- lines.push(t(locale, 'auth_sync_status_error', { value: status.lastError }));
11175
- }
11176
- if (proactiveRefresh) {
11177
- lines.push(t(locale, 'auth_sync_status_proactive_refresh', {
11178
- value: formatAuthProactiveRefreshStatus(locale, proactiveRefresh),
11179
- }));
11180
- }
11181
- if (status.peerStatuses?.length) {
11182
- lines.push(t(locale, 'auth_sync_status_peer_activity'));
11183
- for (const peer of status.peerStatuses.slice(0, 5)) {
11184
- lines.push(t(locale, 'auth_sync_status_peer_activity_item', {
11185
- peer: peer.peer,
11186
- time: peer.lastReceivedAt ?? t(locale, 'none'),
11187
- }));
11188
- }
11189
- }
11190
- if (status.candidateFailures?.length) {
11191
- lines.push(t(locale, 'auth_sync_status_candidate_failures'));
11192
- for (const failure of status.candidateFailures.slice(0, 5)) {
11193
- lines.push(t(locale, 'auth_sync_status_candidate_failure', {
11194
- candidate: failure.candidateName,
11195
- reason: failure.reason,
11196
- source: failure.sourceLabel ?? failure.sourceNodeId ?? t(locale, 'unknown'),
11197
- peer: failure.peer ?? t(locale, 'unknown'),
11198
- time: failure.updatedAt,
11199
- }));
11200
- }
11201
- }
11202
- const recentEvents = [...(status.recentEvents ?? [])].slice(-5);
11203
- if (recentEvents.length > 0) {
11204
- lines.push(t(locale, 'auth_sync_status_recent_events'));
11205
- for (const event of recentEvents) {
11206
- lines.push(formatAuthSyncEventLine(event));
11207
- }
11208
- }
11209
- return lines.join('\n');
11210
- }
11211
- function formatAuthProactiveRefreshStatus(locale, status) {
11212
- const time = status.finishedAt ?? status.startedAt;
11213
- const candidates = formatLimitedList(status.candidates, 3);
11214
- const state = formatAuthProactiveRefreshState(locale, status.state);
11215
- const result = locale === 'zh'
11216
- ? `已刷新 ${status.refreshed},已跳过 ${status.skipped},失败 ${status.failed}`
11217
- : `refreshed ${status.refreshed}, skipped ${status.skipped}, failed ${status.failed}`;
11218
- const parts = [
11219
- `${state} @ ${time}`,
11220
- locale === 'zh' ? `候选 ${candidates}` : `candidates ${candidates}`,
11221
- result,
11222
- ];
11223
- if (status.error) {
11224
- parts.push(locale === 'zh'
11225
- ? `原因 ${formatShortStatusError(status.error)}`
11226
- : `reason ${formatShortStatusError(status.error)}`);
11227
- }
11228
- if (status.details.length > 0) {
11229
- parts.push(locale === 'zh'
11230
- ? `明细 ${formatLimitedList(status.details.map(formatShortStatusError), 2)}`
11231
- : `details ${formatLimitedList(status.details.map(formatShortStatusError), 2)}`);
11232
- }
11233
- return parts.join('; ');
11234
- }
11235
- function formatAuthProactiveRefreshState(locale, state) {
11236
- if (locale === 'zh') {
11237
- switch (state) {
11238
- case 'running': return '进行中';
11239
- case 'completed': return '已完成';
11240
- case 'lease_failed': return '锁未授予';
11241
- case 'failed': return '失败';
11242
- }
11243
- }
11244
- switch (state) {
11245
- case 'running': return 'running';
11246
- case 'completed': return 'completed';
11247
- case 'lease_failed': return 'lease not granted';
11248
- case 'failed': return 'failed';
11249
- }
11250
- }
11251
- function formatLimitedList(values, limit) {
11252
- if (values.length === 0)
11253
- return '-';
11254
- const shown = values.slice(0, limit).join(', ');
11255
- return values.length > limit ? `${shown}, +${values.length - limit}` : shown;
11256
- }
11257
- function formatAuthSyncEvents(locale, status, filter) {
11258
- if (!status?.enabled) {
11259
- return t(locale, 'auth_sync_disabled');
11260
- }
11261
- const events = filterAuthSyncEvents(status.recentEvents ?? [], filter).slice(-15);
11262
- if (events.length === 0) {
11263
- return [
11264
- t(locale, 'auth_sync_events_title'),
11265
- t(locale, 'auth_sync_events_empty'),
11266
- ].join('\n');
11267
- }
11268
- return [
11269
- t(locale, 'auth_sync_events_title'),
11270
- ...events.map(formatAuthSyncEventLine),
11271
- ].join('\n');
11272
- }
11273
- function formatAuthSyncTrace(locale, status, requestId) {
11274
- if (!status?.enabled) {
11275
- return t(locale, 'auth_sync_disabled');
11276
- }
11277
- const events = (status.recentEvents ?? [])
11278
- .filter(event => event.requestId === requestId || event.id === requestId)
11279
- .slice(-25);
11280
- if (events.length === 0) {
11281
- return [
11282
- t(locale, 'auth_sync_trace_title', { value: requestId }),
11283
- t(locale, 'auth_sync_events_empty'),
11284
- ].join('\n');
11285
- }
11286
- return [
11287
- t(locale, 'auth_sync_trace_title', { value: requestId }),
11288
- ...events.map(formatAuthSyncEventLine),
11289
- ].join('\n');
11290
- }
11291
- function filterAuthSyncEvents(events, filter) {
11292
- const normalized = filter?.trim().toLowerCase() ?? '';
11293
- if (!normalized) {
11294
- return events;
11295
- }
11296
- return events.filter((event) => [
11297
- event.id,
11298
- event.requestId,
11299
- event.candidateName,
11300
- event.peer,
11301
- event.kind,
11302
- event.stage,
11303
- event.detail,
11304
- ].some(value => value?.toLowerCase().includes(normalized)));
11305
- }
11306
- function formatAuthSyncEventLine(event) {
11307
- const fields = [
11308
- event.createdAt,
11309
- event.direction,
11310
- event.kind,
11311
- event.stage,
11312
- event.peer ? `peer=${event.peer}` : null,
11313
- event.requestId ? `requestId=${event.requestId}` : null,
11314
- event.candidateName ? `candidate=${event.candidateName}` : null,
11315
- event.detail ? truncateInline(event.detail, 160) : null,
11316
- ].filter(Boolean);
11317
- return `- ${fields.join(' | ')}`;
11318
- }
11319
- function normalizeHelpUsageKey(name) {
11320
- const normalized = name.toLowerCase();
11321
- switch (normalized) {
11322
- case 'start':
11323
- return 'help';
11324
- case 'followup':
11325
- return 'active';
11326
- case 'login':
11327
- return 'login_device';
11328
- case 'codex_restart':
11329
- return 'auth_reload';
11330
- case 'file':
11331
- return 'files';
11332
- case 'rollback':
11333
- return 'undo';
11334
- case 'access':
11335
- return 'permissions';
11336
- case 'focus':
11337
- return 'reveal';
11338
- case 'model':
11339
- return 'models';
11340
- default:
11341
- if (PINNED_HELP_COMMANDS.some(entry => entry.key === normalized)) {
11342
- return normalized;
11343
- }
11344
- if (DYNAMIC_HELP_COMMANDS.some(entry => entry.key === normalized)) {
11345
- return normalized;
11346
- }
11347
- return null;
11348
- }
11349
- }
11350
- function formatWatchCallbackText(locale, mode, threadId) {
11351
- if (mode === 'already') {
11352
- return t(locale, 'watch_already_enabled', { threadId });
11353
- }
11354
- if (mode === 'active') {
11355
- return t(locale, 'watch_started_active', { threadId });
11356
- }
11357
- return t(locale, 'watch_started_idle', { threadId });
11358
- }
11359
- function cloneAuthRetryContext(context) {
11360
- return {
11361
- input: context.input,
11362
- threadId: context.threadId,
11363
- cwd: context.cwd,
11364
- chatId: context.chatId,
11365
- chatType: context.chatType,
11366
- topicId: context.topicId,
11367
- collaborationMode: context.collaborationMode,
11368
- failedAuthTargets: new Set(context.failedAuthTargets),
11369
- };
11370
- }
11371
- function classifyCodexAuthRotationError(params) {
11372
- const collected = collectCodexErrorText(params);
11373
- if (isChatGptBackendAccessBlocked(collected)) {
11374
- return null;
11375
- }
11376
- const code = stringOrNull(params?.error?.codexErrorInfo) ?? stringOrNull(params?.error?.code);
11377
- const message = stringOrNull(params?.error?.message) ?? '';
11378
- const text = `${code ?? ''}\n${message}\n${collected}`;
11379
- if (isCodexAuthInvalidError(text)) {
11380
- return 'auth_invalid';
11381
- }
11382
- if (isCodexQuotaLimitError(text)) {
11383
- return 'quota_limited';
11384
- }
11385
- if (code && /auth|unauthorized|forbidden|login/i.test(code)) {
11386
- return 'auth_invalid';
11387
- }
11388
- return /(not authenticated|unauthorized|forbidden|sign in|log in|login|auth)/i.test(message)
11389
- ? 'auth_invalid'
11390
- : null;
11391
- }
11392
- function isCodexAuthInvalidError(text) {
11393
- return /token[_\s-]?invalidated/i.test(text)
11394
- || /authentication token has been invalidated/i.test(text)
11395
- || /\b401\s+unauthorized\b/i.test(text)
11396
- || /\bunauthorized\b/i.test(text)
11397
- || /\bnot authenticated\b/i.test(text)
11398
- || /\bsign in again\b/i.test(text)
11399
- || /\blog in again\b/i.test(text);
11400
- }
11401
- function isCodexQuotaLimitError(text) {
11402
- return /usageLimitExceeded/i.test(text)
11403
- || /you['’]?ve hit your usage limit/i.test(text)
11404
- || /\busage limit(?:s)?\b/i.test(text)
11405
- || /\brate limit(?:ed|s)?\b/i.test(text)
11406
- || /\btoo many requests\b/i.test(text)
11407
- || /\binsufficient[_\s-]?quota\b/i.test(text)
11408
- || /\bquota exceeded\b/i.test(text)
11409
- || /\bbilling hard limit\b/i.test(text)
11410
- || /\bcredits? exhausted\b/i.test(text)
11411
- || /\bout of credits?\b/i.test(text)
11412
- || /\bcredits? limit\b/i.test(text);
11413
- }
11414
- function formatCodexNotificationError(params) {
11415
- const collected = collectCodexErrorText(params);
11416
- const known = formatKnownCodexAccessError(collected);
11417
- if (known) {
11418
- return known;
11419
- }
11420
- const message = stringOrNull(params?.error?.message);
11421
- if (message) {
11422
- return clipUserFacingError(cleanUserFacingError(message));
11423
- }
11424
- const code = stringOrNull(params?.error?.codexErrorInfo) ?? stringOrNull(params?.error?.code);
11425
- if (code) {
11426
- return clipUserFacingError(cleanUserFacingError(code));
11427
- }
11428
- return clipUserFacingError(cleanUserFacingError(JSON.stringify(params?.error ?? params ?? {})));
11429
- }
11430
- function isRetryableCodexTransportError(params) {
11431
- if (params?.willRetry !== true) {
11432
- return false;
11433
- }
11434
- const errorInfo = params?.error?.codexErrorInfo;
11435
- return Boolean(errorInfo && typeof errorInfo === 'object' && 'responseStreamDisconnected' in errorInfo);
11436
- }
11437
- function collectCodexErrorText(params) {
11438
- const parts = [
11439
- stringOrNull(params?.error?.message),
11440
- stringOrNull(params?.error?.additionalDetails),
11441
- stringOrNull(params?.additionalDetails),
11442
- stringOrNull(params?.error?.codexErrorInfo),
11443
- stringOrNull(params?.error?.code),
11444
- ].filter((part) => part !== null);
11445
- if (parts.length > 0) {
11446
- return parts.join(' ');
11447
- }
11448
- try {
11449
- return JSON.stringify(params?.error ?? params ?? {});
11450
- }
11451
- catch {
11452
- return String(params?.error ?? params ?? '');
11453
- }
11454
- }
11455
- function formatKnownCodexAccessError(raw) {
11456
- if (!isChatGptBackendAccessBlocked(raw)) {
11457
- return null;
11458
- }
11459
- const ray = raw.match(/\bcf-ray:\s*([A-Za-z0-9-]+)/i)?.[1]
11460
- ?? raw.match(/\bRay ID:\s*([A-Za-z0-9-]+)/i)?.[1]
11461
- ?? null;
11462
- return `ChatGPT backend 403 Forbidden: service network/proxy/IP is blocked, not necessarily auth.json invalid. Check HTTP_PROXY/HTTPS_PROXY in the FoxClaw env file and restart FoxClaw${ray ? ` (cf-ray: ${ray})` : ''}.`;
11463
- }
11464
- function isChatGptBackendAccessBlocked(raw) {
11465
- const value = raw.toLowerCase();
11466
- const targetsChatGptBackend = value.includes('chatgpt.com/backend-api')
11467
- || value.includes('wss://chatgpt.com/backend-api');
11468
- const isForbidden = value.includes('403 forbidden')
11469
- || value.includes('status 403')
11470
- || value.includes('httpstatuscode":403')
11471
- || value.includes('httpstatuscode:403');
11472
- const looksLikeHtmlBlock = value.includes('<html')
11473
- || value.includes('text/html')
11474
- || value.includes('cf-ray')
11475
- || value.includes('unable to load site')
11476
- || value.includes('if you are using a vpn');
11477
- return targetsChatGptBackend && isForbidden && looksLikeHtmlBlock;
11478
- }
11479
- function cleanUserFacingError(raw) {
11480
- return raw
11481
- .replace(/<script\b[\s\S]*?<\/script>/gi, ' ')
11482
- .replace(/<style\b[\s\S]*?<\/style>/gi, ' ')
11483
- .replace(/<[^>]+>/g, ' ')
11484
- .replace(/\s+/g, ' ')
11485
- .trim();
11486
- }
11487
- function clipUserFacingError(raw, limit = 900) {
11488
- const cleaned = raw.trim();
11489
- if (cleaned.length <= limit) {
11490
- return cleaned;
11491
- }
11492
- return `${cleaned.slice(0, limit - 3)}...`;
11493
- }
11494
- function parseUserInputQuestions(params) {
11495
- const rawQuestions = Array.isArray(params?.questions)
11496
- ? params.questions
11497
- : params?.question
11498
- ? [params.question]
11499
- : [];
11500
- const seenIds = new Set();
11501
- return rawQuestions
11502
- .map((raw, index) => {
11503
- const fallbackId = `q${index + 1}`;
11504
- const rawId = stringOrNull(raw?.id) ?? fallbackId;
11505
- const id = seenIds.has(rawId) ? `${rawId}_${index + 1}` : rawId;
11506
- seenIds.add(id);
11507
- const header = stringOrNull(raw?.header);
11508
- const question = stringOrNull(raw?.question) ?? stringOrNull(raw?.prompt) ?? stringOrNull(raw?.text) ?? '';
11509
- const isOther = raw?.isOther === true || raw?.is_other === true;
11510
- const isSecret = raw?.isSecret === true || raw?.is_secret === true;
11511
- const options = Array.isArray(raw?.options)
11512
- ? raw.options
11513
- .map((option) => {
11514
- const label = stringOrNull(option?.label) ?? stringOrNull(option?.value) ?? stringOrNull(option);
11515
- if (!label) {
11516
- return null;
11517
- }
11518
- return {
11519
- label,
11520
- description: stringOrNull(option?.description),
11521
- };
11522
- })
11523
- .filter((option) => option !== null)
11524
- : [];
11525
- if (!header && !question && options.length === 0) {
11526
- return null;
11527
- }
11528
- return { id, header, question, isOther, isSecret, options };
11529
- })
11530
- .filter((question) => question !== null);
11531
- }
11532
- function parseServerRequestId(raw) {
11533
- if (typeof raw === 'string' || typeof raw === 'number') {
11534
- return raw;
11535
- }
11536
- return null;
11537
- }
11538
- function stringifyServerRequestId(id) {
11539
- return String(id);
11540
- }
11541
- function sameServerRequestId(left, right) {
11542
- return stringifyServerRequestId(left) === stringifyServerRequestId(right);
11543
- }
11544
- function parseStoredServerRequestId(raw) {
11545
- if (/^(0|[1-9]\d*)$/.test(raw)) {
11546
- const value = Number(raw);
11547
- if (Number.isSafeInteger(value)) {
11548
- return value;
11549
- }
11550
- }
11551
- return raw;
11552
- }
11553
- function serializePendingUserInput(record) {
11554
- return {
11555
- localId: record.localId,
11556
- serverRequestId: stringifyServerRequestId(record.serverRequestId),
11557
- chatId: record.chatId,
11558
- threadId: record.threadId,
11559
- turnId: record.turnId,
11560
- itemId: record.itemId,
11561
- messageId: record.messageId,
11562
- questionsJson: JSON.stringify(record.questions),
11563
- answersJson: stringifyPendingUserInputAnswers(record.answers),
11564
- currentQuestionIndex: pendingUserInputCurrentQuestionIndex(record),
11565
- awaitingFreeText: false,
11566
- status: record.status,
11567
- createdAt: record.createdAt,
11568
- submittedAt: record.submittedAt,
11569
- resolvedAt: null,
11570
- };
11571
- }
11572
- function parseStoredPendingUserInput(record) {
11573
- const questions = parseStoredUserInputQuestions(record.questionsJson);
11574
- if (questions.length === 0) {
11575
- return null;
11576
- }
11577
- return {
11578
- localId: record.localId,
11579
- serverRequestId: parseStoredServerRequestId(record.serverRequestId),
11580
- chatId: record.chatId,
11581
- threadId: record.threadId,
11582
- turnId: record.turnId,
11583
- itemId: record.itemId,
11584
- questions,
11585
- answers: parseStoredUserInputAnswers(record.answersJson),
11586
- messageId: record.messageId,
11587
- status: normalizePendingUserInputStatus(record.status),
11588
- createdAt: record.createdAt,
11589
- submittedAt: record.submittedAt,
11590
- };
11591
- }
11592
- function normalizePendingUserInputStatus(raw) {
11593
- if (raw === 'submitted' || raw === 'resolved' || raw === 'interrupted') {
11594
- return raw;
11595
- }
11596
- return 'pending';
11597
- }
11598
- function parseStoredUserInputQuestions(rawJson) {
11599
- let parsed;
11600
- try {
11601
- parsed = JSON.parse(rawJson);
11602
- }
11603
- catch {
11604
- return [];
11605
- }
11606
- if (!Array.isArray(parsed)) {
11607
- return [];
11608
- }
11609
- return parsed
11610
- .map((raw, index) => {
11611
- const id = stringOrNull(raw?.id) ?? `q${index + 1}`;
11612
- const question = stringOrNull(raw?.question) ?? '';
11613
- const options = Array.isArray(raw?.options)
11614
- ? raw.options
11615
- .map((option) => {
11616
- const label = stringOrNull(option?.label);
11617
- if (!label) {
11618
- return null;
11619
- }
11620
- return {
11621
- label,
11622
- description: stringOrNull(option?.description),
11623
- };
11624
- })
11625
- .filter((option) => option !== null)
11626
- : [];
11627
- return {
11628
- id,
11629
- header: stringOrNull(raw?.header),
11630
- question,
11631
- isOther: raw?.isOther === true,
11632
- isSecret: raw?.isSecret === true,
11633
- options,
11634
- };
11635
- })
11636
- .filter((question) => question !== null);
11637
- }
11638
- function stringifyPendingUserInputAnswers(answers) {
11639
- return JSON.stringify(Object.fromEntries(answers.entries()));
11640
- }
11641
- function parseStoredUserInputAnswers(rawJson) {
11642
- const answers = new Map();
11643
- let parsed;
11644
- try {
11645
- parsed = JSON.parse(rawJson);
11646
- }
11647
- catch {
11648
- return answers;
11649
- }
11650
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
11651
- return answers;
11652
- }
11653
- for (const [id, rawAnswer] of Object.entries(parsed)) {
11654
- const answer = normalizeStoredUserInputAnswer(rawAnswer);
11655
- if (answer !== null) {
11656
- answers.set(id, answer);
11657
- }
11658
- }
11659
- return answers;
11660
- }
11661
- function normalizeStoredUserInputAnswer(rawAnswer) {
11662
- if (typeof rawAnswer === 'string') {
11663
- return rawAnswer;
11664
- }
11665
- if (Array.isArray(rawAnswer)) {
11666
- const first = rawAnswer.find((entry) => typeof entry === 'string');
11667
- return first ?? null;
11668
- }
11669
- if (rawAnswer && typeof rawAnswer === 'object') {
11670
- const nested = rawAnswer.answers;
11671
- return normalizeStoredUserInputAnswer(nested);
11672
- }
11673
- return null;
11674
- }
11675
- function pendingUserInputCurrentQuestionIndex(record) {
11676
- const index = record.questions.findIndex(question => !record.answers.has(question.id));
11677
- return index === -1 ? record.questions.length : index;
11678
- }
11679
- function renderUserInputMessage(locale, record) {
11680
- const lines = [
11681
- t(locale, 'user_input_requested'),
11682
- t(locale, 'line_thread', { value: record.threadId }),
11683
- ];
11684
- if (record.turnId) {
11685
- lines.push(t(locale, 'line_turn', { value: record.turnId }));
11686
- }
11687
- record.questions.forEach((question, index) => {
11688
- lines.push('');
11689
- const title = question.header || question.question || question.id;
11690
- if (question.header && question.question) {
11691
- lines.push(`${index + 1}. **${title}** ${question.question}`);
11692
- }
11693
- else {
11694
- lines.push(`${index + 1}. **${title}**`);
11695
- }
11696
- if (question.isOther) {
11697
- lines.push(` - ${t(locale, 'user_input_other_hint')}`);
11698
- }
11699
- if (question.isSecret) {
11700
- lines.push(` - ${t(locale, 'user_input_secret_warning')}`);
11701
- }
11702
- question.options.forEach((option, optionIndex) => {
11703
- const description = option.description ? ` - ${option.description}` : '';
11704
- lines.push(` ${optionIndex + 1}. ${option.label}${description}`);
11705
- });
11706
- const answer = record.answers.get(question.id);
11707
- if (answer) {
11708
- lines.push(` - ${t(locale, 'user_input_selected', { value: answer })}`);
11709
- }
11710
- });
11711
- lines.push('');
11712
- const statusKey = record.status === 'submitted'
11713
- ? 'user_input_submitted_waiting'
11714
- : record.status === 'interrupted'
11715
- ? 'user_input_interrupted'
11716
- : record.status === 'resolved'
11717
- ? 'user_input_submitted'
11718
- : 'user_input_reply_hint';
11719
- lines.push(t(locale, statusKey));
11720
- if (record.status === 'pending' && parseWeixinBridgeScope(record.chatId)) {
11721
- lines.push('', t(locale, 'weixin_copy_paste_divider'), t(locale, 'weixin_copy_answer_title'));
11722
- record.questions.forEach((question, questionIndex) => {
11723
- if (record.answers.has(question.id)) {
11724
- return;
11725
- }
11726
- if (question.options.length > 0) {
11727
- question.options.forEach((_option, optionIndex) => {
11728
- lines.push(`/answer ${record.localId} ${questionIndex + 1} ${optionIndex + 1}`);
11729
- });
11730
- }
11731
- lines.push(`/answer ${record.localId} ${questionIndex + 1} <text>`);
11732
- });
11733
- }
11734
- return lines.join('\n');
11735
- }
11736
- function userInputKeyboard(record) {
11737
- const rows = [];
11738
- record.questions.forEach((question, questionIndex) => {
11739
- if (record.answers.has(question.id) || question.options.length === 0) {
11740
- return;
11741
- }
11742
- rows.push(question.options.map((option, optionIndex) => ({
11743
- text: clipButtonText(record.questions.length > 1 ? `${questionIndex + 1}. ${option.label}` : option.label),
11744
- callback_data: `ui:${record.localId}:${questionIndex}:${optionIndex}`,
11745
- })));
11746
- });
11747
- return rows;
11748
- }
11749
- function parseStoredTurnInput(inputJson) {
11750
- const value = JSON.parse(inputJson);
11751
- if (!Array.isArray(value)) {
11752
- throw new Error('Queued turn input is not an array');
11753
- }
11754
- return value;
11755
- }
11756
- function parseStagedTelegramAttachments(attachmentsJson) {
11757
- const value = JSON.parse(attachmentsJson);
11758
- if (!Array.isArray(value)) {
11759
- throw new Error('Attachment batch payload is not an array');
11760
- }
11761
- return value.map((entry) => {
11762
- if (!entry || typeof entry !== 'object') {
11763
- throw new Error('Attachment batch contains an invalid entry');
11764
- }
11765
- const attachment = entry;
11766
- if (typeof attachment.localPath !== 'string' || typeof attachment.relativePath !== 'string') {
11767
- throw new Error('Attachment batch entry is missing a local path');
11768
- }
11769
- return attachment;
11770
- });
11771
- }
11772
- function findReusableStandaloneAttachmentBatch(batch, now) {
11773
- if (!batch || batch.mediaGroupId !== null) {
11774
- return null;
11775
- }
11776
- return now - batch.updatedAt <= ATTACHMENT_BATCH_MERGE_WINDOW_MS ? batch : null;
11777
- }
11778
- function mergeAttachmentBatchCaption(existing, next) {
11779
- const normalizedNext = next.trim();
11780
- if (!normalizedNext) {
11781
- return existing;
11782
- }
11783
- const normalizedExisting = existing.trim();
11784
- if (!normalizedExisting) {
11785
- return normalizedNext;
11786
- }
11787
- return normalizedExisting === normalizedNext ? normalizedExisting : `${normalizedExisting}\n\n${normalizedNext}`;
11788
- }
11789
- function summarizeStagedAttachmentInput(text, attachments) {
11790
- const lines = [text.trim() || '(no text)'];
11791
- if (attachments.length > 0) {
11792
- lines.push(`[attachments: ${attachments.map((attachment) => `${attachment.kind}:${attachment.fileName}`).join(', ')}]`);
11793
- }
11794
- return lines.join('\n');
11795
- }
11796
- function renderAttachmentBatchMessage(locale, record) {
11797
- const attachments = parseStagedTelegramAttachments(record.attachmentsJson);
11798
- const lines = [
11799
- t(locale, 'attachment_batch_staged', { count: attachments.length }),
11800
- ];
11801
- if (record.caption.trim()) {
11802
- lines.push('', t(locale, 'attachment_batch_caption', { value: truncateInline(record.caption.trim(), 600) }));
11803
- }
11804
- lines.push('');
11805
- for (const [index, attachment] of attachments.entries()) {
11806
- const name = attachment.fileName || attachment.fileUniqueId;
11807
- const size = attachment.fileSize === null ? '' : `, ${attachment.fileSize} bytes`;
11808
- lines.push(`${index + 1}. ${attachment.kind}: ${name}${size}`);
11809
- }
11810
- return lines.join('\n');
11811
- }
11812
- function attachmentBatchKeyboard(locale, batchId) {
11813
- return [[
11814
- { text: t(locale, 'attachment_batch_button_analyze'), callback_data: `attach:${batchId}:analyze` },
11815
- { text: t(locale, 'attachment_batch_button_clear'), callback_data: `attach:${batchId}:clear` },
11816
- ]];
11817
- }
11818
- function shortId(value) {
11819
- return value.slice(0, 8);
11820
- }
11821
- function renderPlanImplementationPrompt(locale, record) {
11822
- const lines = [
11823
- t(locale, 'plan_impl_title'),
11824
- t(locale, 'line_thread', { value: record.threadId }),
11825
- t(locale, 'line_turn', { value: record.turnId }),
11826
- '',
11827
- t(locale, 'plan_impl_prompt'),
11828
- ];
11829
- if (parseWeixinBridgeScope(record.scopeId)) {
11830
- lines.push('', t(locale, 'weixin_copy_paste_divider'), t(locale, 'weixin_copy_plan_title'), `/planimpl ${record.localId} run`, `/planimpl ${record.localId} fresh`, `/planimpl ${record.localId} stay`);
11831
- }
11832
- return lines.join('\n');
11833
- }
11834
- function planImplementationKeyboard(locale, localId) {
11835
- return [
11836
- [{ text: t(locale, 'plan_impl_button_run'), callback_data: `planimpl:${localId}:run` }],
11837
- [{ text: t(locale, 'plan_impl_button_fresh'), callback_data: `planimpl:${localId}:fresh` }],
11838
- [{ text: t(locale, 'plan_impl_button_stay'), callback_data: `planimpl:${localId}:stay` }],
11839
- ];
11840
- }
11841
- function threadNewCwdCreateKeyboard(locale) {
11842
- return [[
11843
- { text: t(locale, 'button_create_dir'), callback_data: 'thread:newcwd:create' },
11844
- { text: t(locale, 'button_cancel'), callback_data: 'thread:newcwd:cancel' },
11845
- ]];
11846
- }
11847
- function extractLatestPlanMarkdown(active) {
11848
- const segmentPlan = extractLatestPlanSegmentMarkdown(active);
11849
- if (segmentPlan) {
11850
- return segmentPlan;
11851
- }
11852
- return extractLatestProposedPlanMarkdown(active);
11853
- }
11854
- function extractLatestPlanSegmentMarkdown(active) {
11855
- for (let index = active.segments.length - 1; index >= 0; index -= 1) {
11856
- const segment = active.segments[index];
11857
- if (!segment.isPlan) {
11858
- continue;
11859
- }
11860
- const text = segment.text.trim();
11861
- if (text) {
11862
- return text;
11863
- }
11864
- }
11865
- return null;
11866
- }
11867
- function extractLatestProposedPlanMarkdown(active) {
11868
- return extractLatestProposedPlanBlock([
11869
- active.finalText,
11870
- active.buffer,
11871
- ...active.segments.map(segment => segment.text),
11872
- ].filter((text) => typeof text === 'string' && text.length > 0).join('\n'));
11873
- }
11874
- function extractLatestProposedPlanBlock(text) {
11875
- const pattern = /<proposed_plan\b[^>]*>([\s\S]*?)<\/proposed_plan>/gi;
11876
- let latest = null;
11877
- for (let match = pattern.exec(text); match !== null; match = pattern.exec(text)) {
11878
- const plan = match[1]?.trim();
11879
- if (plan) {
11880
- latest = plan;
11881
- }
11882
- }
11883
- return latest;
11884
- }
11885
- function clipButtonText(text) {
11886
- const trimmed = text.replace(/\s+/g, ' ').trim();
11887
- return trimmed.length > 48 ? `${trimmed.slice(0, 47)}...` : trimmed;
11888
- }
11889
- function stringOrNull(value) {
11890
- if (typeof value !== 'string') {
11891
- return null;
11892
- }
11893
- const trimmed = value.trim();
11894
- return trimmed ? trimmed : null;
11895
- }
11896
- function selectCodexRateLimitSnapshot(limits) {
11897
- return limits?.rateLimitsByLimitId?.codex
11898
- ?? Object.values(limits?.rateLimitsByLimitId ?? {})[0]
11899
- ?? limits?.rateLimits
11900
- ?? null;
11901
- }
11902
- function formatCodexAccountLabel(account) {
11903
- if (account.type === 'chatgpt') {
11904
- return 'ChatGPT';
11905
- }
11906
- if (account.type === 'apiKey') {
11907
- return 'API key';
11908
- }
11909
- if (account.type === 'amazonBedrock') {
11910
- return 'Amazon Bedrock';
11911
- }
11912
- return formatPlanTypeLabel(account.type || 'unknown');
11913
- }
11914
- function formatPlanTypeLabel(value) {
11915
- const words = value
11916
- .replace(/[_-]+/g, ' ')
11917
- .trim()
11918
- .split(/\s+/)
11919
- .filter(Boolean);
11920
- if (words.length === 0) {
11921
- return 'Unknown';
11922
- }
11923
- return words
11924
- .map((word) => word.toLowerCase() === 'api' ? 'API' : `${word[0].toUpperCase()}${word.slice(1)}`)
11925
- .join(' ');
11926
- }
11927
- function formatRateLimitWindowLabel(locale, window, fallback) {
11928
- const minutes = window.windowDurationMins;
11929
- if (!minutes || minutes <= 0) {
11930
- return fallback === 'primary'
11931
- ? (locale === 'zh' ? '短周期' : 'Primary window')
11932
- : (locale === 'zh' ? '长周期' : 'Secondary window');
11933
- }
11934
- if (minutes % 10080 === 0) {
11935
- const days = minutes / 1440;
11936
- return locale === 'zh' ? `${formatCompactNumber(days)}天` : `${formatCompactNumber(days)}d window`;
11937
- }
11938
- if (minutes % 1440 === 0) {
11939
- const days = minutes / 1440;
11940
- return locale === 'zh' ? `${formatCompactNumber(days)}天` : `${formatCompactNumber(days)}d window`;
11941
- }
11942
- if (minutes % 60 === 0) {
11943
- const hours = minutes / 60;
11944
- return locale === 'zh' ? `${formatCompactNumber(hours)}小时` : `${formatCompactNumber(hours)}h window`;
11945
- }
11946
- return locale === 'zh' ? `${formatCompactNumber(minutes)}分钟` : `${formatCompactNumber(minutes)}m window`;
11947
- }
11948
- function formatCompactRateLimitWindowLabel(locale, minutes, fallback) {
11949
- if (!minutes || minutes <= 0) {
11950
- return fallback === 'primary'
11951
- ? (locale === 'zh' ? '短' : 'P')
11952
- : (locale === 'zh' ? '长' : 'S');
11953
- }
11954
- if (minutes % 1440 === 0) {
11955
- return `${formatCompactNumber(minutes / 1440)}d`;
11956
- }
11957
- if (minutes % 60 === 0) {
11958
- return `${formatCompactNumber(minutes / 60)}h`;
11959
- }
11960
- return `${formatCompactNumber(minutes)}m`;
11961
- }
11962
- function formatCompactAge(locale, ageMs) {
11963
- const minutes = Math.max(0, ageMs) / 60_000;
11964
- if (minutes >= 1440) {
11965
- const days = Math.floor(minutes / 1440);
11966
- return locale === 'zh' ? `${days}天前` : `${days}d ago`;
11967
- }
11968
- if (minutes >= 60) {
11969
- const hours = Math.floor(minutes / 60);
11970
- return locale === 'zh' ? `${hours}小时前` : `${hours}h ago`;
11971
- }
11972
- const roundedMinutes = Math.floor(minutes);
11973
- return locale === 'zh' ? `${roundedMinutes}分钟前` : `${roundedMinutes}m ago`;
11974
- }
11975
- function formatUtcDateTime(timestampMs) {
11976
- if (!Number.isFinite(timestampMs)) {
11977
- return '-';
11978
- }
11979
- return `${new Date(timestampMs).toISOString().slice(0, 16).replace('T', ' ')}Z`;
11980
- }
11981
- function formatUsagePercent(value) {
11982
- if (!Number.isFinite(value)) {
11983
- return '?';
11984
- }
11985
- return formatCompactNumber(value);
11986
- }
11987
- function formatRemainingUsagePercent(usedPercent) {
11988
- const remainingPercent = remainingUsagePercent(usedPercent);
11989
- return remainingPercent === null ? '?' : formatUsagePercent(remainingPercent);
11990
- }
11991
- function authQuotaSnapshotFromRateLimit(snapshot, accountId = null, quotaIdentityId = null) {
11992
- return {
11993
- capturedAtMs: Date.now(),
11994
- accountId,
11995
- quotaIdentityId,
11996
- planType: snapshot.planType,
11997
- primaryWindowDurationMins: snapshot.primary?.windowDurationMins ?? null,
11998
- primaryRemainingPercent: snapshot.primary ? remainingUsagePercent(snapshot.primary.usedPercent) : null,
11999
- primaryResetsAt: snapshot.primary?.resetsAt ?? null,
12000
- secondaryWindowDurationMins: snapshot.secondary?.windowDurationMins ?? null,
12001
- secondaryRemainingPercent: snapshot.secondary ? remainingUsagePercent(snapshot.secondary.usedPercent) : null,
12002
- secondaryResetsAt: snapshot.secondary?.resetsAt ?? null,
12003
- };
12004
- }
12005
- function codexAuthQuotaSnapshotFromRecord(record) {
12006
- return {
12007
- capturedAtMs: record.capturedAtMs,
12008
- accountId: record.accountId,
12009
- quotaIdentityId: record.quotaIdentityId,
12010
- planType: record.planType,
12011
- primaryWindowDurationMins: record.primaryWindowDurationMins,
12012
- primaryRemainingPercent: record.primaryRemainingPercent,
12013
- primaryResetsAt: record.primaryResetsAt,
12014
- secondaryWindowDurationMins: record.secondaryWindowDurationMins,
12015
- secondaryRemainingPercent: record.secondaryRemainingPercent,
12016
- secondaryResetsAt: record.secondaryResetsAt,
12017
- };
12018
- }
12019
- function mergeCodexAuthQuotaSnapshots(current, incoming) {
12020
- if (!current) {
12021
- return incoming;
12022
- }
12023
- if (!incoming) {
12024
- return current;
12025
- }
12026
- const freshest = incoming.capturedAtMs >= current.capturedAtMs ? incoming : current;
12027
- const older = freshest === incoming ? current : incoming;
12028
- return {
12029
- ...freshest,
12030
- accountId: freshest.accountId ?? older.accountId ?? null,
12031
- quotaIdentityId: freshest.quotaIdentityId ?? older.quotaIdentityId ?? null,
12032
- };
12033
- }
12034
- function isFiniteCodexAuthQuotaSnapshotRecord(record) {
12035
- return Number.isFinite(record.capturedAtMs)
12036
- && isNullableString(record.planType)
12037
- && isNullableFiniteNumber(record.primaryWindowDurationMins)
12038
- && isNullableFiniteNumber(record.primaryRemainingPercent)
12039
- && isNullableFiniteNumber(record.primaryResetsAt)
12040
- && isNullableFiniteNumber(record.secondaryWindowDurationMins)
12041
- && isNullableFiniteNumber(record.secondaryRemainingPercent)
12042
- && isNullableFiniteNumber(record.secondaryResetsAt);
12043
- }
12044
- function remainingUsagePercent(usedPercent) {
12045
- if (!Number.isFinite(usedPercent)) {
12046
- return null;
12047
- }
12048
- return Math.max(0, Math.min(100, 100 - usedPercent));
12049
- }
12050
- function formatAuthQuotaPrefix(locale, snapshot) {
12051
- if (!snapshot) {
12052
- return '--';
12053
- }
12054
- const windows = [
12055
- [snapshot.primaryWindowDurationMins, snapshot.primaryRemainingPercent, snapshot.primaryResetsAt, 'primary'],
12056
- [snapshot.secondaryWindowDurationMins, snapshot.secondaryRemainingPercent, snapshot.secondaryResetsAt, 'secondary'],
12057
- ];
12058
- const values = windows
12059
- .filter(([duration, remaining, resetsAt]) => duration !== null || remaining !== null || resetsAt !== null)
12060
- .map(([duration, remaining, resetsAt, fallback]) => (`${formatCompactRateLimitWindowLabel(locale, duration, fallback)}:${remaining === null ? '--' : formatUsagePercent(remaining)}${resetsAt === null ? '' : `@${formatLocalTimestamp(resetsAt)}`}`));
12061
- return values.length > 0 ? values.join('|') : '--';
12062
- }
12063
- function formatAuthQuotaButtonPrefix(snapshot) {
12064
- return [
12065
- snapshot?.primaryRemainingPercent ?? null,
12066
- snapshot?.secondaryRemainingPercent ?? null,
12067
- ].map(formatAuthQuotaButtonValue).join('|');
12068
- }
12069
- function formatAuthQuotaButtonValue(value) {
12070
- return typeof value === 'number' && Number.isFinite(value)
12071
- ? formatUsagePercent(value)
12072
- : '—';
12073
- }
12074
- function isCodexAuthQuotaSnapshot(value) {
12075
- if (!value || typeof value !== 'object') {
12076
- return false;
12077
- }
12078
- const snapshot = value;
12079
- return typeof snapshot.capturedAtMs === 'number'
12080
- && Number.isFinite(snapshot.capturedAtMs)
12081
- && (snapshot.accountId === undefined || snapshot.accountId === null || typeof snapshot.accountId === 'string')
12082
- && (snapshot.quotaIdentityId === undefined || snapshot.quotaIdentityId === null || typeof snapshot.quotaIdentityId === 'string')
12083
- && (snapshot.planType === undefined || isNullableString(snapshot.planType))
12084
- && (snapshot.primaryWindowDurationMins === undefined || isNullableFiniteNumber(snapshot.primaryWindowDurationMins))
12085
- && isNullableFiniteNumber(snapshot.primaryRemainingPercent)
12086
- && (snapshot.primaryResetsAt === undefined || isNullableFiniteNumber(snapshot.primaryResetsAt))
12087
- && (snapshot.secondaryWindowDurationMins === undefined || isNullableFiniteNumber(snapshot.secondaryWindowDurationMins))
12088
- && isNullableFiniteNumber(snapshot.secondaryRemainingPercent)
12089
- && (snapshot.secondaryResetsAt === undefined || isNullableFiniteNumber(snapshot.secondaryResetsAt));
12090
- }
12091
- function normalizeCodexAuthQuotaSnapshot(snapshot) {
12092
- return {
12093
- capturedAtMs: snapshot.capturedAtMs,
12094
- accountId: snapshot.accountId ?? null,
12095
- quotaIdentityId: snapshot.quotaIdentityId ?? snapshot.accountId ?? null,
12096
- planType: snapshot.planType ?? null,
12097
- primaryWindowDurationMins: snapshot.primaryWindowDurationMins ?? null,
12098
- primaryRemainingPercent: snapshot.primaryRemainingPercent,
12099
- primaryResetsAt: snapshot.primaryResetsAt ?? null,
12100
- secondaryWindowDurationMins: snapshot.secondaryWindowDurationMins ?? null,
12101
- secondaryRemainingPercent: snapshot.secondaryRemainingPercent,
12102
- secondaryResetsAt: snapshot.secondaryResetsAt ?? null,
12103
- };
12104
- }
12105
- function chatGptAuthMetadataCompatible(left, right) {
12106
- if (left.accountId !== right.accountId) {
12107
- return false;
12108
- }
12109
- if (left.quotaIdentityId === left.accountId || right.quotaIdentityId === right.accountId) {
12110
- return true;
12111
- }
12112
- return left.quotaIdentityId === right.quotaIdentityId;
12113
- }
12114
- function isNullableFiniteNumber(value) {
12115
- return value === null || (typeof value === 'number' && Number.isFinite(value));
12116
- }
12117
- function isNullableString(value) {
12118
- return value === null || typeof value === 'string';
12119
- }
12120
- function formatCompactNumber(value) {
12121
- return Number.isInteger(value) ? String(value) : value.toFixed(1).replace(/\.0$/, '');
12122
- }
12123
- function formatTokenCount(value) {
12124
- if (!Number.isFinite(value)) {
12125
- return '?';
12126
- }
12127
- return Math.round(value).toLocaleString('en-US');
12128
- }
12129
- function formatCodexTokenCountWithMetric(value) {
12130
- if (value >= 1_000_000) {
12131
- return `${formatMetricTokenCount(value)} (${formatTokenCount(value)})`;
12132
- }
12133
- return formatTokenCount(value);
12134
- }
12135
- function formatLocalTimestamp(seconds) {
12136
- const date = new Date(seconds * 1000);
12137
- const pad = (value) => String(value).padStart(2, '0');
12138
- return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
12139
- }
12140
- function formatShortStatusError(error) {
12141
- const raw = formatUserError(error);
12142
- const known = formatKnownCodexAccessError(raw);
12143
- if (known) {
12144
- return known.length > 120 ? `${known.slice(0, 117)}...` : known;
12145
- }
12146
- const message = cleanUserFacingError(raw);
12147
- return message.length > 120 ? `${message.slice(0, 117)}...` : message;
12148
- }
12149
- function isThreadNotFoundError(error) {
12150
- return error instanceof Error && /(thread not found|no rollout found for thread id)/i.test(error.message);
12151
- }
12152
- function isThreadActiveWriterError(error) {
12153
- return error instanceof Error && /thread\s+\S+\s+already has an active writer/i.test(error.message);
12154
- }
12155
- function isNoActiveTurnToSteerError(error) {
12156
- return error instanceof Error && /no active turn to steer/i.test(error.message);
12157
- }
12158
- function isThreadQueueUnsupportedError(error) {
12159
- const message = formatUserError(error).toLowerCase();
12160
- return message.includes('method not found')
12161
- || message.includes('unknown method')
12162
- || message.includes('thread/queue/add') && message.includes('not supported');
12163
- }
12164
- function isThreadNewCwdCreateConfirmation(text) {
12165
- return /^(y|yes|ok|okay|confirm|create|mkdir|确定|确认|创建|新建|好)$/i.test(text.trim());
12166
- }
12167
- function isThreadNewCwdCancelConfirmation(text) {
12168
- return /^(n|no|cancel|stop|取消|不要|不用|算了)$/i.test(text.trim());
12169
- }
12170
- function isTelegramMessageGone(error) {
12171
- const message = formatUserError(error).toLowerCase();
12172
- return message.includes('message to delete not found')
12173
- || message.includes('message to edit not found')
12174
- || message.includes('message not found');
12175
- }
12176
- function isTelegramMessageTooLong(error) {
12177
- const message = formatUserError(error).toLowerCase();
12178
- return message.includes('message_too_long')
12179
- || message.includes('message is too long')
12180
- || message.includes('message too long');
12181
- }
12182
- function isFileMissingError(error) {
12183
- return error instanceof Error && /enoent|no such file or directory/i.test(error.message);
12184
- }
12185
- async function isReadableSessionPath(sessionPath) {
12186
- try {
12187
- const stats = await fs.stat(sessionPath);
12188
- return stats.isFile();
12189
- }
12190
- catch {
12191
- return false;
12192
- }
12193
- }
12194
8878
  export { BridgeSessionCore as BridgeController };