@foxden-app/foxclaw 0.12.0 → 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.
- package/CHANGELOG.md +20 -0
- package/dist/antigravity/account_ui.d.ts +34 -0
- package/dist/antigravity/account_ui.js +354 -0
- package/dist/antigravity/adapter.js +1 -1
- package/dist/antigravity/auth.d.ts +4 -1
- package/dist/antigravity/auth.js +17 -5
- package/dist/antigravity/controller.d.ts +3 -108
- package/dist/antigravity/controller.js +2 -2129
- package/dist/antigravity/conversation_ui.d.ts +56 -0
- package/dist/antigravity/conversation_ui.js +509 -0
- package/dist/antigravity/ui_callbacks.d.ts +6 -0
- package/dist/antigravity/ui_callbacks.js +6 -0
- package/dist/bridge/unified_bridge.d.ts +114 -0
- package/dist/bridge/unified_bridge.js +1343 -0
- package/dist/channels/bridge_messaging_router.d.ts +8 -4
- package/dist/channels/bridge_messaging_router.js +15 -4
- package/dist/channels/telegram/telegram_messaging_port.d.ts +16 -0
- package/dist/channels/telegram/telegram_messaging_port.js +22 -2
- package/dist/channels/text_actions.d.ts +14 -0
- package/dist/channels/text_actions.js +48 -0
- package/dist/channels/weixin/weixin_channel_adapter.d.ts +3 -4
- package/dist/channels/weixin/weixin_channel_adapter.js +2 -2
- package/dist/channels/weixin/weixin_messaging_port.d.ts +16 -1
- package/dist/channels/weixin/weixin_messaging_port.js +34 -7
- package/dist/codex_app/activity.d.ts +62 -0
- package/dist/codex_app/activity.js +418 -0
- package/dist/codex_app/adapter.d.ts +2 -2
- package/dist/codex_app/adapter.js +55 -25
- package/dist/codex_app/backend_ui.d.ts +4 -0
- package/dist/codex_app/backend_ui.js +48 -0
- package/dist/codex_app/client.js +0 -12
- package/dist/controller/access.d.ts +1 -11
- package/dist/controller/access.js +1 -33
- package/dist/controller/activity.d.ts +1 -62
- package/dist/controller/activity.js +2 -418
- package/dist/controller/auth_errors.d.ts +11 -0
- package/dist/controller/auth_errors.js +124 -0
- package/dist/controller/auth_files.d.ts +13 -0
- package/dist/controller/auth_files.js +156 -0
- package/dist/controller/auth_presentation.d.ts +77 -0
- package/dist/controller/auth_presentation.js +697 -0
- package/dist/controller/auth_quota_service.d.ts +27 -0
- package/dist/controller/auth_quota_service.js +151 -0
- package/dist/controller/controller.d.ts +30 -56
- package/dist/controller/controller.js +181 -3497
- package/dist/controller/interactions.d.ts +74 -0
- package/dist/controller/interactions.js +560 -0
- package/dist/controller/local_usage_service.d.ts +19 -0
- package/dist/controller/local_usage_service.js +62 -0
- package/dist/controller/native_panel_service.d.ts +55 -0
- package/dist/controller/native_panel_service.js +214 -0
- package/dist/controller/native_panels.d.ts +61 -0
- package/dist/controller/native_panels.js +784 -0
- package/dist/controller/shared_helpers.d.ts +53 -0
- package/dist/controller/shared_helpers.js +399 -0
- package/dist/controller/state_types.d.ts +390 -0
- package/dist/controller/state_types.js +82 -0
- package/dist/controller/turn_rendering.d.ts +22 -0
- package/dist/controller/turn_rendering.js +288 -0
- package/dist/core/access.d.ts +11 -0
- package/dist/core/access.js +33 -0
- package/dist/core/attachment_files.d.ts +13 -0
- package/dist/core/attachment_files.js +180 -0
- package/dist/core/attachment_types.d.ts +22 -0
- package/dist/core/attachment_types.js +1 -0
- package/dist/core/attachments.d.ts +5 -3
- package/dist/core/attachments.js +2 -2
- package/dist/core/backend_registry.d.ts +22 -0
- package/dist/core/backend_registry.js +50 -0
- package/dist/core/backend_ui.d.ts +28 -0
- package/dist/core/backend_ui.js +1 -0
- package/dist/core/channel_events.d.ts +48 -0
- package/dist/core/channel_events.js +1 -0
- package/dist/core/channel_port.d.ts +52 -8
- package/dist/core/engine_spi.d.ts +19 -4
- package/dist/core/listener_scope.d.ts +11 -0
- package/dist/core/listener_scope.js +10 -0
- package/dist/core/message_text.d.ts +2 -0
- package/dist/core/message_text.js +28 -0
- package/dist/core/network_errors.d.ts +1 -0
- package/dist/core/network_errors.js +23 -0
- package/dist/core/orchestrator.d.ts +53 -24
- package/dist/core/orchestrator.js +855 -507
- package/dist/core/queued_engine_input.d.ts +12 -0
- package/dist/core/queued_engine_input.js +16 -0
- package/dist/core/runtime_supervisor.d.ts +19 -0
- package/dist/core/runtime_supervisor.js +45 -0
- package/dist/core/scope_operations.d.ts +7 -0
- package/dist/core/scope_operations.js +19 -0
- package/dist/core/stream_preview.js +21 -9
- package/dist/dsh/adapter.js +18 -5
- package/dist/dsh/backend.d.ts +7 -0
- package/dist/dsh/backend.js +21 -0
- package/dist/dsh/runtime.d.ts +0 -1
- package/dist/dsh/runtime.js +4 -13
- package/dist/dsh/ui.d.ts +8 -8
- package/dist/dsh/ui.js +1 -1
- package/dist/main.js +55 -43
- package/dist/opencode/adapter.js +28 -9
- package/dist/opencode/controller.js +1 -1
- package/dist/store/channel_inbox.d.ts +14 -0
- package/dist/store/channel_inbox.js +21 -0
- package/dist/store/database.d.ts +7 -3
- package/dist/store/database.js +7 -2
- package/dist/store/task_journal.d.ts +32 -0
- package/dist/store/task_journal.js +70 -0
- package/dist/telegram/api.d.ts +2 -2
- package/dist/telegram/api.js +10 -28
- package/dist/telegram/gateway.d.ts +13 -27
- package/dist/telegram/gateway.js +86 -43
- package/dist/telegram/media.d.ts +2 -34
- package/dist/telegram/media.js +1 -180
- package/dist/telegram/task_preview.d.ts +13 -0
- package/dist/telegram/task_preview.js +92 -0
- package/dist/telegram/task_result.d.ts +2 -0
- package/dist/telegram/task_result.js +12 -0
- package/dist/telegram/text.d.ts +1 -1
- package/dist/telegram/text.js +2 -27
- package/dist/types.d.ts +1 -1
- package/docs/release.md +13 -11
- package/docs/zh/architecture-review.md +106 -0
- package/docs/zh/release.md +13 -11
- package/docs/zh/telegram-experience.md +40 -0
- package/package.json +5 -4
|
@@ -1,115 +1,41 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
|
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 {
|
|
30
|
-
import {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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.
|
|
227
|
-
|
|
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.
|
|
232
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
this.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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 ??
|
|
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 ??
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6055
|
-
|
|
6056
|
-
}
|
|
6057
|
-
async
|
|
6058
|
-
|
|
6059
|
-
|
|
6060
|
-
|
|
6061
|
-
|
|
6062
|
-
}
|
|
6063
|
-
async
|
|
6064
|
-
|
|
6065
|
-
|
|
6066
|
-
|
|
6067
|
-
|
|
6068
|
-
|
|
6069
|
-
|
|
6070
|
-
|
|
6071
|
-
|
|
6072
|
-
|
|
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
|
-
|
|
7188
|
-
|
|
7189
|
-
|
|
7190
|
-
|
|
7191
|
-
|
|
7192
|
-
|
|
7193
|
-
}
|
|
7194
|
-
|
|
7195
|
-
|
|
7196
|
-
|
|
7197
|
-
|
|
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 ??
|
|
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 ??
|
|
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 ??
|
|
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 ??
|
|
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 };
|