@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
|
@@ -0,0 +1,1343 @@
|
|
|
1
|
+
import { AntigravityConversationUi } from '../antigravity/conversation_ui.js';
|
|
2
|
+
import { AntigravityAccountUi } from '../antigravity/account_ui.js';
|
|
3
|
+
import { createCodexBackendUi } from '../codex_app/backend_ui.js';
|
|
4
|
+
import fsPromises from 'node:fs/promises';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import { TelegramMessagingPort } from '../channels/telegram/telegram_messaging_port.js';
|
|
8
|
+
import { AntigravityAppClient } from '../antigravity/client.js';
|
|
9
|
+
import { AntigravityAuthManager } from '../antigravity/auth.js';
|
|
10
|
+
import { AntigravityConversationManager } from '../antigravity/conversations.js';
|
|
11
|
+
import { UnifiedChannelOrchestrator } from '../core/orchestrator.js';
|
|
12
|
+
import { AntigravityEngineAdapter } from '../antigravity/adapter.js';
|
|
13
|
+
import { CodexEngineAdapter } from '../codex_app/adapter.js';
|
|
14
|
+
import { OpencodeEngineAdapter } from '../opencode/adapter.js';
|
|
15
|
+
import { BridgeSessionCore } from '../controller/controller.js';
|
|
16
|
+
import { BridgeMessagingRouter } from '../channels/bridge_messaging_router.js';
|
|
17
|
+
import { BRIDGE_SCOPE_WEIXIN_PREFIX, BRIDGE_SCOPE_TELEGRAM_PREFIX, parseTelegramTargetFromBridgeScope } from '../core/bridge_scope.js';
|
|
18
|
+
import { syncCodexLocalUsageToStore } from '../store/token_usage.js';
|
|
19
|
+
import { getTelegramCommands, getAntigravityTelegramCommands } from '../i18n.js';
|
|
20
|
+
function formatCandidateButtonPrefix(c) {
|
|
21
|
+
const p5h = typeof c.quota?.fiveHourPercent === 'number' ? `${c.quota.fiveHourPercent}%` : '—';
|
|
22
|
+
const pw = typeof c.quota?.weeklyPercent === 'number' ? `${c.quota.weeklyPercent}%` : '—';
|
|
23
|
+
return `${p5h}|${pw}`;
|
|
24
|
+
}
|
|
25
|
+
function formatAccountExpiry(expiryMs, locale, isActive = false) {
|
|
26
|
+
if (!expiryMs)
|
|
27
|
+
return locale === 'zh' ? '未知' : 'unknown';
|
|
28
|
+
const now = Date.now();
|
|
29
|
+
if (expiryMs <= now) {
|
|
30
|
+
if (isActive) {
|
|
31
|
+
return locale === 'zh' ? '待命中 (发任务时即时刷新)' : 'standby (refreshes on task)';
|
|
32
|
+
}
|
|
33
|
+
return locale === 'zh' ? '待命中 (就绪 · 切号时即时激活)' : 'standby (ready · refreshes on switch)';
|
|
34
|
+
}
|
|
35
|
+
const remainingMins = Math.round((expiryMs - now) / 60000);
|
|
36
|
+
const timeStr = new Date(expiryMs).toTimeString().slice(0, 5);
|
|
37
|
+
return locale === 'zh'
|
|
38
|
+
? `${timeStr} (剩 ${remainingMins} 分钟)`
|
|
39
|
+
: `${timeStr} (${remainingMins}m left)`;
|
|
40
|
+
}
|
|
41
|
+
import { AGY_MODEL_CALLBACK_PREFIX, AGY_AUTH_CALLBACK_PREFIX, AGY_EFFORT_CALLBACK_PREFIX, AGY_OPEN_CALLBACK_PREFIX, AGY_SETUP_CALLBACK_PREFIX, AGY_WATCH_CALLBACK_PREFIX, } from '../antigravity/ui_callbacks.js';
|
|
42
|
+
export class UnifiedBridgeCore {
|
|
43
|
+
conversationUi;
|
|
44
|
+
accountUi;
|
|
45
|
+
config;
|
|
46
|
+
store;
|
|
47
|
+
logger;
|
|
48
|
+
bot;
|
|
49
|
+
app;
|
|
50
|
+
auth;
|
|
51
|
+
conversations;
|
|
52
|
+
messaging;
|
|
53
|
+
adapter;
|
|
54
|
+
codexAdapter;
|
|
55
|
+
codexApp;
|
|
56
|
+
codexCore;
|
|
57
|
+
opencodeAdapter;
|
|
58
|
+
defaultBackendId;
|
|
59
|
+
selfUpdater;
|
|
60
|
+
orchestrator;
|
|
61
|
+
background = new Set();
|
|
62
|
+
trackBackground(pending) { this.background.add(pending); void pending.finally(() => this.background.delete(pending)).catch(() => { }); }
|
|
63
|
+
pendingAgyRenames = new Map();
|
|
64
|
+
constructor(config, store, logger, bot, app, auth, messaging, options) {
|
|
65
|
+
this.config = config;
|
|
66
|
+
this.store = store;
|
|
67
|
+
this.logger = logger;
|
|
68
|
+
this.bot = bot;
|
|
69
|
+
this.auth = options?.antigravityAuth ?? auth ?? new AntigravityAuthManager(config.antigravityAuthDir, logger);
|
|
70
|
+
this.app = options?.antigravityApp ?? app ?? new AntigravityAppClient(config.antigravityCliBin, logger);
|
|
71
|
+
const effectiveAuthDir = this.auth.authDir || config.antigravityAuthDir;
|
|
72
|
+
this.conversations = new AntigravityConversationManager(effectiveAuthDir, logger);
|
|
73
|
+
this.messaging = messaging ?? new TelegramMessagingPort(bot);
|
|
74
|
+
this.codexApp = options?.codexApp;
|
|
75
|
+
this.defaultBackendId = options?.defaultBackendId ?? 'antigravity';
|
|
76
|
+
this.selfUpdater = options?.selfUpdater;
|
|
77
|
+
this.codexCore = options?.codexCore;
|
|
78
|
+
if (!this.codexCore && options?.codexApp) {
|
|
79
|
+
try {
|
|
80
|
+
this.codexCore = new BridgeSessionCore(this.config, this.store, this.logger, this.bot, options.codexApp, new BridgeMessagingRouter(this.messaging, null), null, null, false);
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
this.logger.warn('unified.codex_core_init_failed', { error: String(err) });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
this.adapter = options?.antigravityAdapter ?? new AntigravityEngineAdapter(this.app, config.antigravityDefaultModel, this.auth, logger);
|
|
87
|
+
if (options?.codexAdapter) {
|
|
88
|
+
this.codexAdapter = options.codexAdapter;
|
|
89
|
+
}
|
|
90
|
+
else if (options?.codexApp) {
|
|
91
|
+
this.codexAdapter = new CodexEngineAdapter(options.codexApp, {
|
|
92
|
+
defaultApprovalPolicy: config.defaultApprovalPolicy,
|
|
93
|
+
defaultSandboxMode: config.defaultSandboxMode,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
if (options?.opencodeAdapter) {
|
|
97
|
+
this.opencodeAdapter = options.opencodeAdapter;
|
|
98
|
+
}
|
|
99
|
+
else if (options?.opencodeApp) {
|
|
100
|
+
this.opencodeAdapter = new OpencodeEngineAdapter(options.opencodeApp);
|
|
101
|
+
}
|
|
102
|
+
const isCodexDefault = this.defaultBackendId === 'codex';
|
|
103
|
+
const isOpencodeDefault = this.defaultBackendId === 'opencode';
|
|
104
|
+
const primaryAdapter = isCodexDefault && this.codexAdapter
|
|
105
|
+
? this.codexAdapter
|
|
106
|
+
: isOpencodeDefault && this.opencodeAdapter
|
|
107
|
+
? this.opencodeAdapter
|
|
108
|
+
: this.adapter;
|
|
109
|
+
const initialBackends = [];
|
|
110
|
+
if (isCodexDefault && this.codexAdapter) {
|
|
111
|
+
initialBackends.push({
|
|
112
|
+
id: 'codex',
|
|
113
|
+
name: 'OpenAI Codex (App Server)',
|
|
114
|
+
engineType: 'codex',
|
|
115
|
+
adapter: this.codexAdapter,
|
|
116
|
+
isDefault: true,
|
|
117
|
+
});
|
|
118
|
+
initialBackends.push({
|
|
119
|
+
id: 'antigravity',
|
|
120
|
+
name: 'Google Antigravity (AGY)',
|
|
121
|
+
engineType: 'antigravity',
|
|
122
|
+
adapter: this.adapter,
|
|
123
|
+
isDefault: false,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
initialBackends.push({
|
|
128
|
+
id: 'antigravity',
|
|
129
|
+
name: 'Google Antigravity (AGY)',
|
|
130
|
+
engineType: 'antigravity',
|
|
131
|
+
adapter: this.adapter,
|
|
132
|
+
isDefault: !isOpencodeDefault,
|
|
133
|
+
});
|
|
134
|
+
if (this.codexAdapter) {
|
|
135
|
+
initialBackends.push({
|
|
136
|
+
id: 'codex',
|
|
137
|
+
name: 'OpenAI Codex (App Server)',
|
|
138
|
+
engineType: 'codex',
|
|
139
|
+
adapter: this.codexAdapter,
|
|
140
|
+
isDefault: false,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (this.opencodeAdapter) {
|
|
145
|
+
initialBackends.push({
|
|
146
|
+
id: 'opencode',
|
|
147
|
+
name: 'OpenCode (SDK)',
|
|
148
|
+
engineType: 'opencode',
|
|
149
|
+
adapter: this.opencodeAdapter,
|
|
150
|
+
isDefault: isOpencodeDefault,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
initialBackends.push(...(options?.backends ?? []));
|
|
154
|
+
for (const backend of initialBackends) {
|
|
155
|
+
backend.isDefault = backend.id === this.defaultBackendId;
|
|
156
|
+
if (backend.engineType === 'codex') {
|
|
157
|
+
backend.defaults = { reasoningEffort: 'medium', supportedReasoningEfforts: ['low', 'medium', 'high', 'xhigh', 'max'], boost: true, tokenUsage: true };
|
|
158
|
+
backend.commands = getTelegramCommands;
|
|
159
|
+
}
|
|
160
|
+
else if (backend.engineType === 'antigravity') {
|
|
161
|
+
backend.defaults = { reasoningEffort: 'high', supportedReasoningEfforts: ['low', 'medium', 'high'], boost: true, tokenUsage: true };
|
|
162
|
+
backend.commands = getAntigravityTelegramCommands;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
this.backendDefinitions = initialBackends;
|
|
166
|
+
const allAdapters = [this.adapter];
|
|
167
|
+
if (this.codexAdapter)
|
|
168
|
+
allAdapters.push(this.codexAdapter);
|
|
169
|
+
if (this.opencodeAdapter)
|
|
170
|
+
allAdapters.push(this.opencodeAdapter);
|
|
171
|
+
const legacyUi = {
|
|
172
|
+
renderCustomStatus: (scopeId, locale) => this.renderCustomStatus(scopeId, locale),
|
|
173
|
+
renderCustomSetupRows: (scopeId, locale) => this.renderCustomSetupRows(scopeId, locale),
|
|
174
|
+
handleCustomCallback: (scopeId, data, locale, messageId, event) => this.handleCustomCallback(scopeId, data, locale, messageId, event),
|
|
175
|
+
handleCustomCommand: (scopeId, command, args, locale, event) => this.handleCustomCommand(scopeId, command, args, locale, event),
|
|
176
|
+
handleCustomInbound: (event, locale) => this.handleCustomInbound(event, locale),
|
|
177
|
+
};
|
|
178
|
+
for (const backend of initialBackends) {
|
|
179
|
+
if (backend.engineType === 'codex' && this.codexCore)
|
|
180
|
+
backend.createUi = host => createCodexBackendUi(this.codexCore, host, legacyUi);
|
|
181
|
+
else if (backend.engineType === 'codex' || backend.engineType === 'antigravity')
|
|
182
|
+
backend.createUi = () => legacyUi;
|
|
183
|
+
}
|
|
184
|
+
this.orchestrator = new UnifiedChannelOrchestrator({
|
|
185
|
+
config,
|
|
186
|
+
store,
|
|
187
|
+
logger,
|
|
188
|
+
bot,
|
|
189
|
+
adapter: primaryAdapter,
|
|
190
|
+
adapters: allAdapters,
|
|
191
|
+
backends: initialBackends,
|
|
192
|
+
defaultBackendId: this.defaultBackendId,
|
|
193
|
+
ownsScope: options?.ownsScope ?? (scopeId => {
|
|
194
|
+
if (!scopeId.startsWith(BRIDGE_SCOPE_TELEGRAM_PREFIX))
|
|
195
|
+
return !config.tgMultiBotMode && !scopeId.startsWith(BRIDGE_SCOPE_WEIXIN_PREFIX);
|
|
196
|
+
const target = parseTelegramTargetFromBridgeScope(scopeId);
|
|
197
|
+
return target.botId ? target.botId === this.bot.identity : !config.tgMultiBotMode;
|
|
198
|
+
}),
|
|
199
|
+
backendProvider: () => this.getBackendDescriptors(),
|
|
200
|
+
messaging: this.messaging,
|
|
201
|
+
serviceUi: { handleCustomCommand: (scopeId, cmd, args, locale, event) => this.handleServiceCommand(scopeId, cmd, args, locale, event) },
|
|
202
|
+
});
|
|
203
|
+
const panels = { sendMessage: this.sendMessage.bind(this), editMessage: this.editMessage.bind(this), scheduleStalePanelDeletion: this.scheduleStalePanelDeletion.bind(this) };
|
|
204
|
+
this.conversationUi = new AntigravityConversationUi(config, store, logger, this.conversations, panels, this.orchestrator);
|
|
205
|
+
this.accountUi = new AntigravityAccountUi(config, store, logger, this.auth, panels, this.orchestrator);
|
|
206
|
+
}
|
|
207
|
+
backendDefinitions;
|
|
208
|
+
async getBackendDescriptors() {
|
|
209
|
+
const list = [];
|
|
210
|
+
list.push(...this.backendDefinitions.filter(backend => !['codex', 'antigravity', 'opencode'].includes(backend.id)));
|
|
211
|
+
const isCodexDefault = this.defaultBackendId === 'codex';
|
|
212
|
+
const active = await this.auth.getActiveAccount();
|
|
213
|
+
const activeQuota = active?.quota
|
|
214
|
+
? `${formatCandidateButtonPrefix(active)} · ${formatAccountExpiry(active.expiry, 'zh', true)}`
|
|
215
|
+
: undefined;
|
|
216
|
+
const agyPrimary = {
|
|
217
|
+
id: 'antigravity',
|
|
218
|
+
name: 'Google Antigravity (AGY)',
|
|
219
|
+
engineType: 'antigravity',
|
|
220
|
+
adapter: this.adapter,
|
|
221
|
+
account: active?.email || active?.name || 'default',
|
|
222
|
+
details: activeQuota,
|
|
223
|
+
isDefault: !isCodexDefault,
|
|
224
|
+
};
|
|
225
|
+
let codexPrimary = null;
|
|
226
|
+
if (this.codexAdapter) {
|
|
227
|
+
const codexHome = this.config.codexAuthDir ?? this.config.codexHome ?? path.join(os.homedir(), '.codex');
|
|
228
|
+
let currentCodexAccount = 'default';
|
|
229
|
+
try {
|
|
230
|
+
const authPath = path.join(codexHome, 'auth.json');
|
|
231
|
+
const stat = await fsPromises.lstat(authPath).catch(() => null);
|
|
232
|
+
if (stat?.isSymbolicLink()) {
|
|
233
|
+
const target = await fsPromises.readlink(authPath);
|
|
234
|
+
currentCodexAccount = path.basename(target).replace(/^auth\.json_/, '');
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
// ignore
|
|
239
|
+
}
|
|
240
|
+
codexPrimary = {
|
|
241
|
+
id: 'codex',
|
|
242
|
+
name: 'OpenAI Codex (App Server)',
|
|
243
|
+
engineType: 'codex',
|
|
244
|
+
adapter: this.codexAdapter,
|
|
245
|
+
account: currentCodexAccount,
|
|
246
|
+
details: 'Official Codex App runtime',
|
|
247
|
+
isDefault: isCodexDefault,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
let opencodePrimary = null;
|
|
251
|
+
if (this.opencodeAdapter) {
|
|
252
|
+
opencodePrimary = {
|
|
253
|
+
id: 'opencode',
|
|
254
|
+
name: 'OpenCode (SDK)',
|
|
255
|
+
engineType: 'opencode',
|
|
256
|
+
adapter: this.opencodeAdapter,
|
|
257
|
+
account: 'default',
|
|
258
|
+
details: 'OpenCode CLI / Server',
|
|
259
|
+
isDefault: false,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
if (isCodexDefault) {
|
|
263
|
+
if (codexPrimary)
|
|
264
|
+
list.push(codexPrimary);
|
|
265
|
+
list.push(agyPrimary);
|
|
266
|
+
if (opencodePrimary)
|
|
267
|
+
list.push(opencodePrimary);
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
list.push(agyPrimary);
|
|
271
|
+
if (codexPrimary)
|
|
272
|
+
list.push(codexPrimary);
|
|
273
|
+
if (opencodePrimary)
|
|
274
|
+
list.push(opencodePrimary);
|
|
275
|
+
}
|
|
276
|
+
return list.map(backend => ({ ...this.backendDefinitions.find(definition => definition.id === backend.id), ...backend, isDefault: backend.id === this.defaultBackendId }));
|
|
277
|
+
}
|
|
278
|
+
registerInboundHandlers() {
|
|
279
|
+
this.orchestrator.registerInboundHandlers();
|
|
280
|
+
}
|
|
281
|
+
registerTelegramInboundHandlers() {
|
|
282
|
+
this.registerInboundHandlers();
|
|
283
|
+
}
|
|
284
|
+
dispatchInboundLikeTelegramText(event) {
|
|
285
|
+
this.orchestrator.dispatchInboundLikeTelegramText(event);
|
|
286
|
+
}
|
|
287
|
+
async startCodexApp() {
|
|
288
|
+
if (this.codexCore) {
|
|
289
|
+
this.codexCore.attachExecutionHost({ ownsScope: scope => this.orchestrator.ownsScopeId(scope), hasExecutingTasks: () => this.orchestrator.hasExecutingTasks() });
|
|
290
|
+
await this.codexCore.startControlPlane();
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (this.codexApp && typeof this.codexApp.isConnected === 'function' && !this.codexApp.isConnected()) {
|
|
294
|
+
if (typeof this.codexApp.start === 'function') {
|
|
295
|
+
await this.codexApp.start().catch((err) => {
|
|
296
|
+
this.logger.warn('codex.app.start_failed', { error: String(err) });
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
async startTelegramPolling() {
|
|
302
|
+
await this.start();
|
|
303
|
+
}
|
|
304
|
+
async start() {
|
|
305
|
+
await this.startCodexApp();
|
|
306
|
+
await this.orchestrator.start();
|
|
307
|
+
this.auth.startKeepAlive();
|
|
308
|
+
this.logger.info('bridge.started', { defaultBackendId: this.defaultBackendId });
|
|
309
|
+
this.trackBackground(syncCodexLocalUsageToStore(this.store, this.config.codexHome ?? undefined));
|
|
310
|
+
// Restore persisted watchers on startup
|
|
311
|
+
try {
|
|
312
|
+
const persisted = this.store.listWatchedThreads();
|
|
313
|
+
for (const item of persisted) {
|
|
314
|
+
this.trackBackground(this.watchConversation(item.scopeId, item.threadId, 'zh', undefined, false).catch((err) => {
|
|
315
|
+
this.logger.warn('antigravity.restore_watcher_failed', { scopeId: item.scopeId, error: String(err) });
|
|
316
|
+
}));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
catch (err) {
|
|
320
|
+
this.logger.warn('antigravity.restore_watchers_error', { error: String(err) });
|
|
321
|
+
}
|
|
322
|
+
if (this.selfUpdater) {
|
|
323
|
+
this.scheduleSelfUpdateStatusPoll(500);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
async stop() {
|
|
327
|
+
this.clearSelfUpdateStatusPoll();
|
|
328
|
+
try {
|
|
329
|
+
await this.orchestrator.stop();
|
|
330
|
+
}
|
|
331
|
+
finally {
|
|
332
|
+
await this.auth.stopKeepAlive();
|
|
333
|
+
await this.conversationUi.stop();
|
|
334
|
+
await Promise.allSettled([...this.background]);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
selfUpdatePollTimer = null;
|
|
338
|
+
scheduleSelfUpdateStatusPoll(delay = 1000) {
|
|
339
|
+
if (!this.selfUpdater || this.selfUpdatePollTimer) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
this.selfUpdatePollTimer = setTimeout(() => {
|
|
343
|
+
this.selfUpdatePollTimer = null;
|
|
344
|
+
void this.pollSelfUpdateStatus().catch((error) => {
|
|
345
|
+
this.logger.error('antigravity.self_update_poll_failed', { error: String(error) });
|
|
346
|
+
this.scheduleSelfUpdateStatusPoll();
|
|
347
|
+
});
|
|
348
|
+
}, delay);
|
|
349
|
+
this.selfUpdatePollTimer.unref?.();
|
|
350
|
+
}
|
|
351
|
+
clearSelfUpdateStatusPoll() {
|
|
352
|
+
if (!this.selfUpdatePollTimer)
|
|
353
|
+
return;
|
|
354
|
+
clearTimeout(this.selfUpdatePollTimer);
|
|
355
|
+
this.selfUpdatePollTimer = null;
|
|
356
|
+
}
|
|
357
|
+
ownsScope(scopeId) {
|
|
358
|
+
if (this.bot.identity) {
|
|
359
|
+
return scopeId.includes(`:${this.bot.identity}:`);
|
|
360
|
+
}
|
|
361
|
+
return !scopeId.includes(':bot');
|
|
362
|
+
}
|
|
363
|
+
async pollSelfUpdateStatus() {
|
|
364
|
+
const status = await this.selfUpdater?.readStatus();
|
|
365
|
+
if (!status)
|
|
366
|
+
return;
|
|
367
|
+
if (status.state === 'pending') {
|
|
368
|
+
this.scheduleSelfUpdateStatusPoll();
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (status.scopeId.startsWith('cluster:')) {
|
|
372
|
+
await this.selfUpdater?.clearStatus();
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
if (!this.ownsScope(status.scopeId)) {
|
|
376
|
+
this.scheduleSelfUpdateStatusPoll();
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
const isZh = status.locale === 'zh';
|
|
380
|
+
if (status.state === 'succeeded') {
|
|
381
|
+
const notes = status.releaseNotes?.filter((n) => n.trim()) ?? [];
|
|
382
|
+
const title = isZh ? '🎉 FoxClaw 全链路升级完成' : '🎉 FoxClaw Update Completed';
|
|
383
|
+
const lines = [
|
|
384
|
+
`**${title}**`,
|
|
385
|
+
`• FoxClaw: \`${status.fromVersion}\` ➔ \`${status.toVersion ?? '0.11.0'}\``,
|
|
386
|
+
`• Codex CLI: \`${status.codexFromVersion ?? '0.157.1'}\` ➔ \`${status.codexToVersion ?? '0.159.2'}\``,
|
|
387
|
+
`• Antigravity CLI: \`${status.agyFromVersion ?? '1.2.14'}\` (最新)`,
|
|
388
|
+
];
|
|
389
|
+
if (notes.length > 0) {
|
|
390
|
+
lines.push('', isZh ? '**更新日志**:' : '**Release Notes**:');
|
|
391
|
+
for (const note of notes) {
|
|
392
|
+
lines.push(`• ${note}`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
lines.push('', isZh ? '服务已自动重载并恢复就绪。' : 'Service reloaded and ready.');
|
|
396
|
+
await this.sendMessage(status.scopeId, lines.join('\n')).catch(() => { });
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
const errorMsg = status.error ?? (isZh ? '未知错误' : 'Unknown error');
|
|
400
|
+
await this.sendMessage(status.scopeId, `❌ **FoxClaw 升级失败**: ${errorMsg}`).catch(() => { });
|
|
401
|
+
}
|
|
402
|
+
await this.selfUpdater?.clearStatus();
|
|
403
|
+
}
|
|
404
|
+
getRuntimeStatus() {
|
|
405
|
+
const isConnected = this.codexApp && typeof this.codexApp.isConnected === 'function'
|
|
406
|
+
? this.codexApp.isConnected()
|
|
407
|
+
: true;
|
|
408
|
+
return {
|
|
409
|
+
running: true,
|
|
410
|
+
connected: isConnected,
|
|
411
|
+
activeTurns: this.orchestrator.getActiveTurnsCount(),
|
|
412
|
+
pendingApprovals: this.orchestrator.getPendingApprovals(),
|
|
413
|
+
botUsername: this.bot.username,
|
|
414
|
+
codexHome: this.config.codexHome ?? this.config.codexAuthDir ?? path.join(os.homedir(), '.codex'),
|
|
415
|
+
...(this.codexApp && typeof this.codexApp.getServerStatus === 'function' && this.codexApp.getServerStatus()
|
|
416
|
+
? { codexAppServer: this.codexApp.getServerStatus() }
|
|
417
|
+
: {}),
|
|
418
|
+
userAgent: this.codexApp && typeof this.codexApp.getUserAgent === 'function'
|
|
419
|
+
? this.codexApp.getUserAgent()
|
|
420
|
+
: null,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
isIdleForServiceUpdate() {
|
|
424
|
+
return this.orchestrator.isIdleForServiceUpdate();
|
|
425
|
+
}
|
|
426
|
+
async getCurrentAuthLabel() {
|
|
427
|
+
const codexHome = this.config.codexAuthDir ?? this.config.codexHome ?? path.join(os.homedir(), '.codex');
|
|
428
|
+
try {
|
|
429
|
+
const authPath = path.join(codexHome, 'auth.json');
|
|
430
|
+
const stat = await fsPromises.lstat(authPath).catch(() => null);
|
|
431
|
+
if (stat?.isSymbolicLink()) {
|
|
432
|
+
const target = await fsPromises.readlink(authPath);
|
|
433
|
+
return path.basename(target).replace(/^auth\.json_/, '');
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
/* ignore */
|
|
438
|
+
}
|
|
439
|
+
return 'default';
|
|
440
|
+
}
|
|
441
|
+
async handleExternalCodexAuthCandidateDeleted(candidateName, _reason = null) {
|
|
442
|
+
void _reason;
|
|
443
|
+
this.store.deleteCodexAuthCandidate(candidateName);
|
|
444
|
+
if (this.codexApp) {
|
|
445
|
+
await this.codexApp.restart().catch(() => { });
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
async validateExternalCodexAuthCandidate(_candidateName, _rawAuth, _expectedAccountId) {
|
|
449
|
+
void _candidateName;
|
|
450
|
+
void _rawAuth;
|
|
451
|
+
void _expectedAccountId;
|
|
452
|
+
return { ok: true };
|
|
453
|
+
}
|
|
454
|
+
async sendMessage(scopeId, text, inlineKeyboard) {
|
|
455
|
+
try {
|
|
456
|
+
return await this.messaging.sendRichMarkdown(scopeId, text, inlineKeyboard);
|
|
457
|
+
}
|
|
458
|
+
catch {
|
|
459
|
+
return this.messaging.sendPlain(scopeId, text, inlineKeyboard);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
async editMessage(scopeId, messageId, text, inlineKeyboard) {
|
|
463
|
+
try {
|
|
464
|
+
await this.messaging.editRichMarkdown(scopeId, messageId, text, inlineKeyboard);
|
|
465
|
+
}
|
|
466
|
+
catch {
|
|
467
|
+
await this.messaging.editPlain(scopeId, messageId, text, inlineKeyboard);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
scheduleStalePanelDeletion(scopeId, messageId) {
|
|
471
|
+
this.orchestrator.scheduleStalePanelDeletion(scopeId, messageId);
|
|
472
|
+
}
|
|
473
|
+
async renderCustomStatus(scopeId, locale) {
|
|
474
|
+
const activeBackend = this.orchestrator.getBackendDescriptorForScope(scopeId);
|
|
475
|
+
if (activeBackend.engineType === 'codex') {
|
|
476
|
+
const codexHome = this.config.codexAuthDir ?? this.config.codexHome ?? path.join(os.homedir(), '.codex');
|
|
477
|
+
let currentCodexAccount = 'default';
|
|
478
|
+
try {
|
|
479
|
+
const authPath = path.join(codexHome, 'auth.json');
|
|
480
|
+
const stat = await fsPromises.lstat(authPath).catch(() => null);
|
|
481
|
+
if (stat?.isSymbolicLink()) {
|
|
482
|
+
const target = await fsPromises.readlink(authPath);
|
|
483
|
+
currentCodexAccount = path.basename(target).replace(/^auth\.json_/, '');
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
catch {
|
|
487
|
+
/* ignore */
|
|
488
|
+
}
|
|
489
|
+
const binding = this.store.getBinding(scopeId);
|
|
490
|
+
const threadLine = binding?.threadId
|
|
491
|
+
? (locale === 'zh' ? `\n• **当前会话**: \`${binding.threadId.slice(0, 16)}…\`` : `\n• **Thread**: \`${binding.threadId.slice(0, 16)}…\``)
|
|
492
|
+
: (locale === 'zh' ? `\n• **当前会话**: \`新会话就绪 (发送消息开启)\`` : `\n• **Thread**: \`Ready (new)\``);
|
|
493
|
+
const cwdLine = `\n• **工作目录**: \`${binding?.cwd || this.config.defaultCwd}\``;
|
|
494
|
+
return ((locale === 'zh'
|
|
495
|
+
? `• **Codex 账号**: \`${currentCodexAccount}\` (软链接管理)\n• **执行引擎**: \`OpenAI Codex App Server\``
|
|
496
|
+
: `• **Codex Account**: \`${currentCodexAccount}\`\n• **Engine**: \`OpenAI Codex App Server\``) +
|
|
497
|
+
threadLine +
|
|
498
|
+
cwdLine);
|
|
499
|
+
}
|
|
500
|
+
const activeAccount = await this.auth.getActiveAccount();
|
|
501
|
+
const candidates = await this.auth.listCandidates();
|
|
502
|
+
const currentWatcher = this.conversationUi.watcherFor(scopeId);
|
|
503
|
+
let watchLine = '';
|
|
504
|
+
if (currentWatcher && !currentWatcher.stopped) {
|
|
505
|
+
watchLine =
|
|
506
|
+
locale === 'zh'
|
|
507
|
+
? `\n• **观察状态**: 👁 正在观察 \`${currentWatcher.conversationId.slice(0, 8)}\``
|
|
508
|
+
: `\n• **Watching**: 👁 Observing \`${currentWatcher.conversationId.slice(0, 8)}\``;
|
|
509
|
+
}
|
|
510
|
+
let quotaLine = '';
|
|
511
|
+
if (activeAccount) {
|
|
512
|
+
const q = activeAccount.quota ?? (await this.auth.fetchQuotaForAccount(activeAccount.name));
|
|
513
|
+
if (q) {
|
|
514
|
+
const p5h = typeof q.fiveHourPercent === 'number' ? `${q.fiveHourPercent}%` : '—';
|
|
515
|
+
const pw = typeof q.weeklyPercent === 'number' ? `${q.weeklyPercent}%` : '—';
|
|
516
|
+
quotaLine =
|
|
517
|
+
locale === 'zh'
|
|
518
|
+
? `\n• **5h/7d 额度**: \`${p5h} | ${pw}\` (剩余百分比)`
|
|
519
|
+
: `\n• **5h/7d Quota**: \`${p5h} | ${pw}\` (remaining %)`;
|
|
520
|
+
}
|
|
521
|
+
else {
|
|
522
|
+
quotaLine =
|
|
523
|
+
locale === 'zh'
|
|
524
|
+
? `\n• **5h/7d 额度**: \`未获取到\` (在 /auth 中可实时刷新)`
|
|
525
|
+
: `\n• **5h/7d Quota**: \`Not available\` (refresh in /auth)`;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return ((locale === 'zh'
|
|
529
|
+
? `• **当前账号**: \`${activeAccount?.email ?? activeAccount?.name ?? '未知'}\` (共 ${candidates.length} 个账号)`
|
|
530
|
+
: `• **Account**: \`${activeAccount?.email ?? activeAccount?.name ?? 'unknown'}\` (${candidates.length} candidates)`) +
|
|
531
|
+
quotaLine +
|
|
532
|
+
watchLine);
|
|
533
|
+
}
|
|
534
|
+
async renderCustomSetupRows(scopeId, _locale) {
|
|
535
|
+
void _locale;
|
|
536
|
+
const activeBackend = this.orchestrator.getBackendDescriptorForScope(scopeId);
|
|
537
|
+
if (activeBackend.engineType === 'codex') {
|
|
538
|
+
const codexHome = this.config.codexAuthDir ?? this.config.codexHome ?? path.join(os.homedir(), '.codex');
|
|
539
|
+
let currentCodexAccount = 'default';
|
|
540
|
+
try {
|
|
541
|
+
const authPath = path.join(codexHome, 'auth.json');
|
|
542
|
+
const stat = await fsPromises.lstat(authPath).catch(() => null);
|
|
543
|
+
if (stat?.isSymbolicLink()) {
|
|
544
|
+
const target = await fsPromises.readlink(authPath);
|
|
545
|
+
currentCodexAccount = path.basename(target).replace(/^auth\.json_/, '');
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
catch {
|
|
549
|
+
/* ignore */
|
|
550
|
+
}
|
|
551
|
+
return [
|
|
552
|
+
[
|
|
553
|
+
{ text: `👤 Codex 账号 (${currentCodexAccount})`, callback_data: 'codex:setup:auth' },
|
|
554
|
+
{ text: '📁 Codex 会话历史', callback_data: 'codex:setup:threads' },
|
|
555
|
+
],
|
|
556
|
+
];
|
|
557
|
+
}
|
|
558
|
+
if (activeBackend.engineType === 'opencode') {
|
|
559
|
+
return [
|
|
560
|
+
[
|
|
561
|
+
{ text: '📁 OpenCode 会话历史', callback_data: 'opencode:setup:threads' },
|
|
562
|
+
],
|
|
563
|
+
];
|
|
564
|
+
}
|
|
565
|
+
const candidates = await this.auth.listCandidates();
|
|
566
|
+
const active = candidates.find((c) => c.isActive) || candidates[0];
|
|
567
|
+
let quotaBadge = '';
|
|
568
|
+
if (active?.quota) {
|
|
569
|
+
const p5h = typeof active.quota.fiveHourPercent === 'number' ? `${active.quota.fiveHourPercent}%` : '—';
|
|
570
|
+
const pw = typeof active.quota.weeklyPercent === 'number' ? `${active.quota.weeklyPercent}%` : '—';
|
|
571
|
+
quotaBadge = ` · ${p5h}|${pw}`;
|
|
572
|
+
}
|
|
573
|
+
return [
|
|
574
|
+
[
|
|
575
|
+
{ text: `👤 AGY 账号 (${candidates.length}${quotaBadge})`, callback_data: `${AGY_SETUP_CALLBACK_PREFIX}auth` },
|
|
576
|
+
{ text: '📁 AGY 会话历史', callback_data: `${AGY_SETUP_CALLBACK_PREFIX}threads` },
|
|
577
|
+
],
|
|
578
|
+
];
|
|
579
|
+
}
|
|
580
|
+
async handleCustomInbound(event, locale) {
|
|
581
|
+
const scopeId = event.scopeId;
|
|
582
|
+
// OAuth uploads belong to Antigravity; other backends receive their files intact.
|
|
583
|
+
if (this.orchestrator.getBackendDescriptorForScope(scopeId).engineType !== 'antigravity') {
|
|
584
|
+
if (this.orchestrator.getBackendDescriptorForScope(scopeId).engineType === 'codex' && this.codexCore?.hasPendingInteraction(scopeId)) {
|
|
585
|
+
this.codexCore.dispatchInboundLikeTelegramText(event);
|
|
586
|
+
return true;
|
|
587
|
+
}
|
|
588
|
+
return false;
|
|
589
|
+
}
|
|
590
|
+
// Check for JSON token upload / document attachment
|
|
591
|
+
const doc = event.attachments?.find((a) => a.kind === 'document');
|
|
592
|
+
if (doc && (doc.fileName?.endsWith('.json') || doc.mimeType?.includes('json'))) {
|
|
593
|
+
try {
|
|
594
|
+
const remoteFile = await this.bot.getFile(doc.fileId);
|
|
595
|
+
if (remoteFile.file_path) {
|
|
596
|
+
const tempDest = path.join(os.tmpdir(), `agy_upload_${Date.now()}_${doc.fileName || 'token.json'}`);
|
|
597
|
+
await this.bot.downloadResolvedFile(remoteFile.file_path, tempDest);
|
|
598
|
+
const content = await fsPromises.readFile(tempDest, 'utf8');
|
|
599
|
+
await fsPromises.unlink(tempDest).catch(() => { });
|
|
600
|
+
if (content.includes('refresh_token')) {
|
|
601
|
+
const importRes = await this.auth.importAccountFromJson(content);
|
|
602
|
+
if (importRes.success && importRes.account) {
|
|
603
|
+
const candidates = await this.auth.listCandidates();
|
|
604
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
605
|
+
? `🎉 **Antigravity 账号导入成功!**\n\n` +
|
|
606
|
+
`• **账号名称**: \`${importRes.account.name}\`\n` +
|
|
607
|
+
`• **绑定邮箱**: \`${importRes.account.email || '未知'}\`\n` +
|
|
608
|
+
`• **鉴权状态**: ✅ Google OAuth 鉴权校验通过并已刷新 Token\n` +
|
|
609
|
+
`• **账号池现存**: 共 ${candidates.length} 个账号\n\n` +
|
|
610
|
+
`已自动加入候选池与自动保活轮转!`
|
|
611
|
+
: `🎉 **Antigravity Account Imported!**\n\n` +
|
|
612
|
+
`• **Name**: \`${importRes.account.name}\`\n` +
|
|
613
|
+
`• **Email**: \`${importRes.account.email || 'unknown'}\`\n` +
|
|
614
|
+
`• **Status**: ✅ Google OAuth validated & refreshed\n` +
|
|
615
|
+
`• **Pool Size**: ${candidates.length} accounts\n\n` +
|
|
616
|
+
`Joined keep-alive and rotation pool!`);
|
|
617
|
+
return true;
|
|
618
|
+
}
|
|
619
|
+
else if (importRes.error) {
|
|
620
|
+
await this.sendMessage(scopeId, `❌ 凭据导入失败: ${importRes.error}`);
|
|
621
|
+
return true;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
catch (err) {
|
|
627
|
+
this.logger.warn('antigravity.doc_import_error', { error: String(err) });
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
// Check for inline JSON paste containing refresh_token
|
|
631
|
+
const trimmed = event.text.trim();
|
|
632
|
+
if (trimmed.startsWith('{') && trimmed.includes('refresh_token')) {
|
|
633
|
+
try {
|
|
634
|
+
const importRes = await this.auth.importAccountFromJson(trimmed);
|
|
635
|
+
if (importRes.success && importRes.account) {
|
|
636
|
+
const candidates = await this.auth.listCandidates();
|
|
637
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
638
|
+
? `🎉 **Antigravity 账号导入成功!**\n\n` +
|
|
639
|
+
`• **账号名称**: \`${importRes.account.name}\`\n` +
|
|
640
|
+
`• **绑定邮箱**: \`${importRes.account.email || '未知'}\`\n` +
|
|
641
|
+
`• **鉴权状态**: ✅ Google OAuth 鉴权校验通过并已刷新 Token\n` +
|
|
642
|
+
`• **账号池现存**: 共 ${candidates.length} 个账号\n\n` +
|
|
643
|
+
`已自动加入候选池与自动保活轮转!`
|
|
644
|
+
: `🎉 **Antigravity Account Imported!**\n\n` +
|
|
645
|
+
`• **Name**: \`${importRes.account.name}\`\n` +
|
|
646
|
+
`• **Email**: \`${importRes.account.email || 'unknown'}\`\n` +
|
|
647
|
+
`• **Status**: ✅ Google OAuth validated & refreshed\n` +
|
|
648
|
+
`• **Pool Size**: ${candidates.length} accounts\n\n` +
|
|
649
|
+
`Joined keep-alive and rotation pool!`);
|
|
650
|
+
return true;
|
|
651
|
+
}
|
|
652
|
+
else if (importRes.error) {
|
|
653
|
+
await this.sendMessage(scopeId, `❌ 凭据导入失败: ${importRes.error}`);
|
|
654
|
+
return true;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
catch (err) {
|
|
658
|
+
this.logger.warn('antigravity.text_import_error', { error: String(err) });
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
// Check if there is an active pending browser login session awaiting authorization code
|
|
662
|
+
if (this.auth.hasPendingLogin(scopeId)) {
|
|
663
|
+
const loginRes = await this.auth.completeBrowserLogin(scopeId, event.text);
|
|
664
|
+
if (loginRes.success && loginRes.account) {
|
|
665
|
+
const candidates = await this.auth.listCandidates();
|
|
666
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
667
|
+
? `🎉 **Google 账号登录成功!**\n\n` +
|
|
668
|
+
`• **账号名称**: \`${loginRes.account.name}\`\n` +
|
|
669
|
+
`• **绑定邮箱**: \`${loginRes.account.email || '未知'}\`\n` +
|
|
670
|
+
`• **鉴权状态**: ✅ 授权码兑换成功,Token 已保存\n` +
|
|
671
|
+
`• **账号池现存**: 共 ${candidates.length} 个账号\n\n` +
|
|
672
|
+
`已成功加入候选池,并已纳入后台自动静默保活!`
|
|
673
|
+
: `🎉 **Google Account Logged In!**\n\n` +
|
|
674
|
+
`• **Account**: \`${loginRes.account.name}\`\n` +
|
|
675
|
+
`• **Email**: \`${loginRes.account.email || 'unknown'}\`\n` +
|
|
676
|
+
`• **Status**: ✅ Code exchanged successfully\n` +
|
|
677
|
+
`• **Pool Size**: ${candidates.length} accounts\n\n` +
|
|
678
|
+
`Joined candidate pool with automatic keep-alive!`);
|
|
679
|
+
return true;
|
|
680
|
+
}
|
|
681
|
+
else {
|
|
682
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
683
|
+
? `❌ **登录授权失败**: ${loginRes.error || '未能识别或兑换授权码'}\n\n请检查是否复制了完整授权码,或发送 \`/login\` 重新发起登录,发送 \`/auth cancel\` 可取消。`
|
|
684
|
+
: `❌ **Login Failed**: ${loginRes.error || 'Failed to exchange code'}\nPlease check the code or send /login again. Send /auth cancel to abort.`);
|
|
685
|
+
return true;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
if (this.codexCore?.hasPendingInteraction(scopeId)) {
|
|
689
|
+
this.codexCore.dispatchInboundLikeTelegramText(event);
|
|
690
|
+
return true;
|
|
691
|
+
}
|
|
692
|
+
const rename = this.pendingAgyRenames.get(scopeId);
|
|
693
|
+
if (rename && event.text.trim()) {
|
|
694
|
+
this.pendingAgyRenames.delete(scopeId);
|
|
695
|
+
const newTitle = event.text.trim();
|
|
696
|
+
this.conversations.renameConversation(rename.conversationId, newTitle);
|
|
697
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
698
|
+
? `✅ 会话名称已修改为: **${newTitle}**`
|
|
699
|
+
: `✅ Conversation renamed to: **${newTitle}**`);
|
|
700
|
+
await this.sendThreadsPanel(scopeId, '', locale);
|
|
701
|
+
return true;
|
|
702
|
+
}
|
|
703
|
+
return false;
|
|
704
|
+
}
|
|
705
|
+
async handleServiceCommand(scopeId, cmd, _args, locale, _event) {
|
|
706
|
+
if (cmd.toLowerCase() === 'update') {
|
|
707
|
+
if (this.selfUpdater) {
|
|
708
|
+
const status = await this.selfUpdater.readStatus();
|
|
709
|
+
if (status?.state === 'pending') {
|
|
710
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
711
|
+
? '⏳ 升级任务已在后台执行中,请稍候…'
|
|
712
|
+
: '⏳ Self-update is already running, please wait…');
|
|
713
|
+
return true;
|
|
714
|
+
}
|
|
715
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
716
|
+
? '🚀 正在启动全链路自升级(FoxClaw + Codex + Antigravity CLI),服务将自动更新并重载…'
|
|
717
|
+
: '🚀 Starting full-stack self-update (FoxClaw + Codex + Antigravity CLI). Service will restart automatically…');
|
|
718
|
+
try {
|
|
719
|
+
await this.selfUpdater.launch(scopeId, locale);
|
|
720
|
+
}
|
|
721
|
+
catch (err) {
|
|
722
|
+
await this.sendMessage(scopeId, `❌ 自升级启动失败: ${String(err)}`);
|
|
723
|
+
}
|
|
724
|
+
return true;
|
|
725
|
+
}
|
|
726
|
+
if (this.codexCore) {
|
|
727
|
+
if (_event) {
|
|
728
|
+
this.codexCore.dispatchInboundLikeTelegramText(_event);
|
|
729
|
+
}
|
|
730
|
+
else {
|
|
731
|
+
this.codexCore.dispatchInboundLikeTelegramText({
|
|
732
|
+
scopeId,
|
|
733
|
+
text: '/update',
|
|
734
|
+
senderId: '0',
|
|
735
|
+
senderName: 'User',
|
|
736
|
+
timestamp: Date.now(),
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
return true;
|
|
740
|
+
}
|
|
741
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
742
|
+
? '⚠️ 当前运行环境下未配置自升级服务,请在终端执行 `foxclaw update`。'
|
|
743
|
+
: '⚠️ Self-updater is not available in current runtime. Run `foxclaw update` in terminal.');
|
|
744
|
+
return true;
|
|
745
|
+
}
|
|
746
|
+
return false;
|
|
747
|
+
}
|
|
748
|
+
async handleCustomCommand(scopeId, cmd, args, locale, _event) {
|
|
749
|
+
const activeBackend = this.orchestrator.getBackendDescriptorForScope(scopeId);
|
|
750
|
+
const isCodex = activeBackend.engineType === 'codex';
|
|
751
|
+
if (isCodex) {
|
|
752
|
+
if (this.codexCore) {
|
|
753
|
+
switch (cmd.toLowerCase()) {
|
|
754
|
+
case 'threads':
|
|
755
|
+
if (_event) {
|
|
756
|
+
this.codexCore.dispatchInboundLikeTelegramText(_event);
|
|
757
|
+
}
|
|
758
|
+
else {
|
|
759
|
+
await this.codexCore.showThreadsPanel(scopeId, undefined, args.trim() || null, locale);
|
|
760
|
+
}
|
|
761
|
+
return true;
|
|
762
|
+
case 'open':
|
|
763
|
+
if (_event) {
|
|
764
|
+
this.codexCore.dispatchInboundLikeTelegramText(_event);
|
|
765
|
+
}
|
|
766
|
+
else if (!args.trim()) {
|
|
767
|
+
await this.codexCore.showThreadsPanel(scopeId, undefined, null, locale);
|
|
768
|
+
}
|
|
769
|
+
return true;
|
|
770
|
+
case 'auth':
|
|
771
|
+
if (_event) {
|
|
772
|
+
this.codexCore.dispatchInboundLikeTelegramText(_event);
|
|
773
|
+
}
|
|
774
|
+
else {
|
|
775
|
+
await this.codexCore.handleAuthCommand(scopeId, locale, args ? args.split(/\s+/) : []);
|
|
776
|
+
}
|
|
777
|
+
return true;
|
|
778
|
+
case 'login_device':
|
|
779
|
+
if (_event) {
|
|
780
|
+
this.codexCore.dispatchInboundLikeTelegramText(_event);
|
|
781
|
+
return true;
|
|
782
|
+
}
|
|
783
|
+
break;
|
|
784
|
+
case 'login':
|
|
785
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
786
|
+
? `ℹ️ **Codex 账号登录**\n\n发送 \`/auth\` 可在账号池中一键切换,或点击【🔑 设备登录】使用浏览器授权。`
|
|
787
|
+
: `ℹ️ Use /auth to manage accounts or use Device Login.`);
|
|
788
|
+
return true;
|
|
789
|
+
case 'watch':
|
|
790
|
+
case 'unwatch':
|
|
791
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
792
|
+
? `ℹ️ 会话实时观察模式当前仅在 Google Antigravity (AGY) 引擎下可用。可使用 \`/backend antigravity\` 切换。`
|
|
793
|
+
: `ℹ️ Observer mode is only available on Google Antigravity (AGY) engine.`);
|
|
794
|
+
return true;
|
|
795
|
+
default:
|
|
796
|
+
return false;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
switch (cmd.toLowerCase()) {
|
|
800
|
+
case 'threads':
|
|
801
|
+
await this.sendCodexThreadsMenu(scopeId, locale);
|
|
802
|
+
return true;
|
|
803
|
+
case 'open':
|
|
804
|
+
if (!args.trim()) {
|
|
805
|
+
await this.sendCodexThreadsMenu(scopeId, locale);
|
|
806
|
+
}
|
|
807
|
+
else {
|
|
808
|
+
await this.openCodexThread(scopeId, args.trim(), locale);
|
|
809
|
+
}
|
|
810
|
+
return true;
|
|
811
|
+
case 'auth':
|
|
812
|
+
await this.sendCodexAuthMenu(scopeId, locale);
|
|
813
|
+
return true;
|
|
814
|
+
case 'login':
|
|
815
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
816
|
+
? `ℹ️ **Codex 账号管理说明**\n\nCodex 使用 \`~/.codex/auth.json\` 凭据。\n发送 \`/auth\` 可在多个已有账号候选之间一键热切换。\n若需添加新账号,可在终端运行 \`codex auth login\` 或将 \`auth.json_<name>\` 放入 \`~/.codex/\` 目录。`
|
|
817
|
+
: `ℹ️ **Codex Auth Note**\n\nUse /auth to switch between accounts in ~/.codex/. To log in, run \`codex auth login\` in terminal.`);
|
|
818
|
+
return true;
|
|
819
|
+
case 'watch':
|
|
820
|
+
case 'unwatch':
|
|
821
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
822
|
+
? `ℹ️ 会话实时观察模式当前仅在 Google Antigravity (AGY) 引擎下可用。可使用 \`/backend antigravity\` 切换。`
|
|
823
|
+
: `ℹ️ Observer mode is only available on Google Antigravity (AGY) engine.`);
|
|
824
|
+
return true;
|
|
825
|
+
default:
|
|
826
|
+
return false;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
switch (cmd.toLowerCase()) {
|
|
830
|
+
case 'threads':
|
|
831
|
+
await this.sendThreadsPanel(scopeId, args.trim(), locale);
|
|
832
|
+
return true;
|
|
833
|
+
case 'open':
|
|
834
|
+
await this.openThread(scopeId, args.trim(), locale);
|
|
835
|
+
return true;
|
|
836
|
+
case 'watch':
|
|
837
|
+
await this.watchConversation(scopeId, args.trim(), locale);
|
|
838
|
+
return true;
|
|
839
|
+
case 'unwatch':
|
|
840
|
+
await this.unwatchConversation(scopeId, locale);
|
|
841
|
+
return true;
|
|
842
|
+
case 'login':
|
|
843
|
+
await this.startLoginFlow(scopeId, locale);
|
|
844
|
+
return true;
|
|
845
|
+
case 'auth':
|
|
846
|
+
if (args.trim() === 'login') {
|
|
847
|
+
await this.startLoginFlow(scopeId, locale);
|
|
848
|
+
}
|
|
849
|
+
else if (args.trim() === 'cancel' || args.trim() === 'cancel_login') {
|
|
850
|
+
this.auth.cancelBrowserLogin(scopeId);
|
|
851
|
+
await this.sendMessage(scopeId, locale === 'zh' ? '已取消登录会话。' : 'Login session canceled.');
|
|
852
|
+
}
|
|
853
|
+
else {
|
|
854
|
+
await this.sendAuthMenu(scopeId, args.trim(), locale);
|
|
855
|
+
}
|
|
856
|
+
return true;
|
|
857
|
+
default:
|
|
858
|
+
return false;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
async handleCustomCallback(scopeId, data, locale, messageId, event) {
|
|
862
|
+
const engine = this.orchestrator.getBackendDescriptorForScope(scopeId).engineType;
|
|
863
|
+
const isCodexCallback = /^(codex:|thread:|auth:|settings:)/.test(data);
|
|
864
|
+
const isAntigravityCallback = data.startsWith('agy:');
|
|
865
|
+
if ((isCodexCallback && engine !== 'codex') || (isAntigravityCallback && engine !== 'antigravity')) {
|
|
866
|
+
if (event)
|
|
867
|
+
await this.messaging.answerCallback(event.callbackQueryId, locale === 'zh' ? '后端已切换,请重新打开设置面板。' : 'Backend changed; reopen the setup panel.');
|
|
868
|
+
return true;
|
|
869
|
+
}
|
|
870
|
+
if (this.codexCore && (data.startsWith('thread:') ||
|
|
871
|
+
data.startsWith('auth:') ||
|
|
872
|
+
data.startsWith('settings:access:') ||
|
|
873
|
+
data.startsWith('settings:permissions'))) {
|
|
874
|
+
if (event) {
|
|
875
|
+
await this.codexCore.handleCallback(event);
|
|
876
|
+
}
|
|
877
|
+
return true;
|
|
878
|
+
}
|
|
879
|
+
if (data.startsWith('codex:setup:auth')) {
|
|
880
|
+
await this.messaging.answerCallback(data, '');
|
|
881
|
+
if (this.codexCore) {
|
|
882
|
+
await this.codexCore.handleAuthCommand(scopeId, locale, []);
|
|
883
|
+
}
|
|
884
|
+
else {
|
|
885
|
+
await this.sendCodexAuthMenu(scopeId, locale, messageId);
|
|
886
|
+
}
|
|
887
|
+
return true;
|
|
888
|
+
}
|
|
889
|
+
if (data.startsWith('codex:setup:threads')) {
|
|
890
|
+
await this.messaging.answerCallback(data, '');
|
|
891
|
+
if (this.codexCore) {
|
|
892
|
+
await this.codexCore.showThreadsPanel(scopeId, messageId, undefined, locale);
|
|
893
|
+
}
|
|
894
|
+
else {
|
|
895
|
+
await this.sendCodexThreadsMenu(scopeId, locale, messageId);
|
|
896
|
+
}
|
|
897
|
+
return true;
|
|
898
|
+
}
|
|
899
|
+
if (data.startsWith('agy:rename:')) {
|
|
900
|
+
const convId = data.slice('agy:rename:'.length);
|
|
901
|
+
this.pendingAgyRenames.set(scopeId, { conversationId: convId });
|
|
902
|
+
await this.messaging.answerCallback(data, '请输入新名称');
|
|
903
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
904
|
+
? `✏️ 请直接回复发送该会话的新名称:`
|
|
905
|
+
: `✏️ Please reply with the new conversation name:`);
|
|
906
|
+
return true;
|
|
907
|
+
}
|
|
908
|
+
if (data.startsWith('agy:archive:')) {
|
|
909
|
+
const convId = data.slice('agy:archive:'.length);
|
|
910
|
+
this.conversations.archiveConversation(convId);
|
|
911
|
+
await this.messaging.answerCallback(data, locale === 'zh' ? '已归档' : 'Archived');
|
|
912
|
+
await this.sendThreadsPanel(scopeId, '', locale, messageId);
|
|
913
|
+
return true;
|
|
914
|
+
}
|
|
915
|
+
if (data.startsWith('agy:new:')) {
|
|
916
|
+
const convId = data.slice('agy:new:'.length);
|
|
917
|
+
const conv = this.conversations.getConversation(convId);
|
|
918
|
+
const cwd = conv?.workspaceDir || this.config.defaultCwd;
|
|
919
|
+
this.store.setBinding(scopeId, '', cwd);
|
|
920
|
+
this.orchestrator.syncCurrentBackendSettings(scopeId);
|
|
921
|
+
await this.messaging.answerCallback(data, locale === 'zh' ? '已创建新会话' : 'New conversation ready');
|
|
922
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
923
|
+
? `✨ **已在目录创建新会话**\n• 工作目录: \`${cwd}\`\n\n发送任意消息开启新任务。`
|
|
924
|
+
: `✨ **New conversation ready** in \`${cwd}\`.`);
|
|
925
|
+
return true;
|
|
926
|
+
}
|
|
927
|
+
if (data.startsWith('codex:auth:')) {
|
|
928
|
+
const cand = data.slice('codex:auth:'.length);
|
|
929
|
+
await this.switchCodexAccount(scopeId, cand, locale, messageId);
|
|
930
|
+
return true;
|
|
931
|
+
}
|
|
932
|
+
if (data.startsWith('codex:open:')) {
|
|
933
|
+
const threadId = data.slice('codex:open:'.length);
|
|
934
|
+
await this.openCodexThread(scopeId, threadId, locale, messageId);
|
|
935
|
+
return true;
|
|
936
|
+
}
|
|
937
|
+
if (data.startsWith(AGY_OPEN_CALLBACK_PREFIX)) {
|
|
938
|
+
const targetId = data.slice(AGY_OPEN_CALLBACK_PREFIX.length);
|
|
939
|
+
const conv = this.conversations.resolveConversation(targetId, scopeId, this.store);
|
|
940
|
+
if (conv) {
|
|
941
|
+
this.store.setBinding(scopeId, conv.conversationId, conv.workspaceDir || this.config.defaultCwd);
|
|
942
|
+
await this.messaging.answerCallback(data, `已切换到: ${conv.title}`);
|
|
943
|
+
await this.openThread(scopeId, conv.conversationId, locale, messageId);
|
|
944
|
+
}
|
|
945
|
+
else {
|
|
946
|
+
await this.messaging.answerCallback(data, '未找到会话');
|
|
947
|
+
}
|
|
948
|
+
return true;
|
|
949
|
+
}
|
|
950
|
+
if (data.startsWith(AGY_WATCH_CALLBACK_PREFIX)) {
|
|
951
|
+
const targetId = data.slice(AGY_WATCH_CALLBACK_PREFIX.length);
|
|
952
|
+
if (targetId === 'stop') {
|
|
953
|
+
await this.messaging.answerCallback(data, '已停止观察');
|
|
954
|
+
await this.unwatchConversation(scopeId, locale, true);
|
|
955
|
+
return true;
|
|
956
|
+
}
|
|
957
|
+
const existing = this.conversationUi.watcherFor(scopeId);
|
|
958
|
+
if (existing && !existing.stopped && existing.conversationId === targetId) {
|
|
959
|
+
await this.messaging.answerCallback(data, '已停止观察');
|
|
960
|
+
await this.unwatchConversation(scopeId, locale, true);
|
|
961
|
+
return true;
|
|
962
|
+
}
|
|
963
|
+
await this.messaging.answerCallback(data, '进入观察模式');
|
|
964
|
+
await this.watchConversation(scopeId, targetId, locale, messageId);
|
|
965
|
+
return true;
|
|
966
|
+
}
|
|
967
|
+
if (data.startsWith(AGY_SETUP_CALLBACK_PREFIX)) {
|
|
968
|
+
const sub = data.slice(AGY_SETUP_CALLBACK_PREFIX.length);
|
|
969
|
+
switch (sub) {
|
|
970
|
+
case 'auth':
|
|
971
|
+
await this.sendAuthMenu(scopeId, '', locale, messageId);
|
|
972
|
+
return true;
|
|
973
|
+
case 'threads':
|
|
974
|
+
await this.sendThreadsPanel(scopeId, '', locale, messageId);
|
|
975
|
+
return true;
|
|
976
|
+
case 'effort':
|
|
977
|
+
await this.sendEffortMenu(scopeId, '', locale, messageId);
|
|
978
|
+
return true;
|
|
979
|
+
default:
|
|
980
|
+
return false;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
if (data.startsWith(AGY_MODEL_CALLBACK_PREFIX)) {
|
|
984
|
+
const model = data.slice(AGY_MODEL_CALLBACK_PREFIX.length);
|
|
985
|
+
this.store.setChatModel(scopeId, model === 'default' ? null : model);
|
|
986
|
+
this.orchestrator.syncCurrentBackendSettings(scopeId);
|
|
987
|
+
await this.messaging.answerCallback(data, `Model: ${model}`);
|
|
988
|
+
await this.orchestrator.sendModelsMenu(scopeId, locale, messageId);
|
|
989
|
+
return true;
|
|
990
|
+
}
|
|
991
|
+
if (data.startsWith(AGY_EFFORT_CALLBACK_PREFIX)) {
|
|
992
|
+
const effort = data.slice(AGY_EFFORT_CALLBACK_PREFIX.length);
|
|
993
|
+
const settings = this.store.getChatSettings(scopeId);
|
|
994
|
+
if (effort !== 'high' && settings?.serviceTier === 'boost') {
|
|
995
|
+
this.store.setChatServiceTier(scopeId, null);
|
|
996
|
+
}
|
|
997
|
+
this.store.setChatEffort(scopeId, effort);
|
|
998
|
+
this.orchestrator.syncCurrentBackendSettings(scopeId);
|
|
999
|
+
await this.messaging.answerCallback(data, `Effort: ${effort}`);
|
|
1000
|
+
await this.sendEffortMenu(scopeId, '', locale, messageId);
|
|
1001
|
+
return true;
|
|
1002
|
+
}
|
|
1003
|
+
if (data.startsWith(AGY_AUTH_CALLBACK_PREFIX)) {
|
|
1004
|
+
const accountName = data.slice(AGY_AUTH_CALLBACK_PREFIX.length);
|
|
1005
|
+
if (accountName.startsWith('filter:')) {
|
|
1006
|
+
await this.sendAuthMenu(scopeId, accountName, locale, messageId);
|
|
1007
|
+
return true;
|
|
1008
|
+
}
|
|
1009
|
+
if (accountName === 'login') {
|
|
1010
|
+
await this.startLoginFlow(scopeId, locale, messageId);
|
|
1011
|
+
return true;
|
|
1012
|
+
}
|
|
1013
|
+
if (accountName === 'cancel_login') {
|
|
1014
|
+
this.auth.cancelBrowserLogin(scopeId);
|
|
1015
|
+
await this.messaging.answerCallback(data, '已取消登录');
|
|
1016
|
+
await this.sendAuthMenu(scopeId, '', locale, messageId);
|
|
1017
|
+
return true;
|
|
1018
|
+
}
|
|
1019
|
+
if (accountName === 'rotate') {
|
|
1020
|
+
try {
|
|
1021
|
+
const res = await this.auth.rotateNextCandidate();
|
|
1022
|
+
await this.messaging.answerCallback(data, `已轮转到: ${res.account.email || res.account.name}`);
|
|
1023
|
+
await this.sendAuthMenu(scopeId, '', locale, messageId);
|
|
1024
|
+
}
|
|
1025
|
+
catch (err) {
|
|
1026
|
+
await this.messaging.answerCallback(data, `轮转失败: ${String(err)}`);
|
|
1027
|
+
}
|
|
1028
|
+
return true;
|
|
1029
|
+
}
|
|
1030
|
+
if (accountName === 'refresh_current') {
|
|
1031
|
+
const active = await this.auth.getActiveAccount();
|
|
1032
|
+
if (!active) {
|
|
1033
|
+
await this.messaging.answerCallback(data, '无活跃账号');
|
|
1034
|
+
return true;
|
|
1035
|
+
}
|
|
1036
|
+
const res = await this.auth.refreshTokenForAccount(active.name);
|
|
1037
|
+
await this.messaging.answerCallback(data, res.success ? '⚡ 当前 Token 刷新成功!' : `刷新失败: ${res.error}`);
|
|
1038
|
+
await this.sendAuthMenu(scopeId, '', locale, messageId);
|
|
1039
|
+
return true;
|
|
1040
|
+
}
|
|
1041
|
+
if (accountName === 'refresh_all') {
|
|
1042
|
+
const res = await this.auth.refreshAllTokens();
|
|
1043
|
+
await this.messaging.answerCallback(data, `⚡ 刷新完成: ${res.refreshed}/${res.total} 成功`);
|
|
1044
|
+
await this.sendAuthMenu(scopeId, '', locale, messageId);
|
|
1045
|
+
return true;
|
|
1046
|
+
}
|
|
1047
|
+
if (accountName === 'import_help') {
|
|
1048
|
+
await this.messaging.answerCallback(data, '查看导入指南');
|
|
1049
|
+
const guideText = `📥 **Antigravity 账号导入指南**\n\n` +
|
|
1050
|
+
`你可以通过以下任意方式向机器人导入新的 Google / Antigravity 账号:\n\n` +
|
|
1051
|
+
`1️⃣ **直接发送 Token 文件**:\n` +
|
|
1052
|
+
` 将包含 OAuth 凭据的 \`.json\` 文件(例如从其他机器复制的 \`antigravity-oauth-token\`)作为文档直接发送给机器人。\n\n` +
|
|
1053
|
+
`2️⃣ **粘贴 Token JSON**:\n` +
|
|
1054
|
+
` 直接在会话中发送包含 \`refresh_token\` 的 JSON 文本。\n\n` +
|
|
1055
|
+
`3️⃣ **使用命令**:\n` +
|
|
1056
|
+
` 发送 \`/auth add <json_content>\` 即可导入。\n\n` +
|
|
1057
|
+
`机器人会自动校验并向 Google API 请求刷新测试,校验成功后立即加入候选池与自动保活轮转!`;
|
|
1058
|
+
await this.sendMessage(scopeId, guideText);
|
|
1059
|
+
return true;
|
|
1060
|
+
}
|
|
1061
|
+
if (accountName.startsWith('toggle_pause:')) {
|
|
1062
|
+
const target = accountName.slice('toggle_pause:'.length);
|
|
1063
|
+
const nowPaused = this.auth.togglePauseAccount(target);
|
|
1064
|
+
await this.messaging.answerCallback(data, nowPaused ? `⏸ 账号已暂停: ${target}` : `▶️ 账号已恢复启用: ${target}`);
|
|
1065
|
+
await this.sendAuthMenu(scopeId, '', locale, messageId);
|
|
1066
|
+
return true;
|
|
1067
|
+
}
|
|
1068
|
+
if (accountName.startsWith('repair:')) {
|
|
1069
|
+
const target = accountName.slice('repair:'.length);
|
|
1070
|
+
const res = await this.auth.diagnoseAndRepairAccount(target);
|
|
1071
|
+
await this.messaging.answerCallback(data, res.ok ? `🩺 修复成功: ${res.message}` : `❌ 修复失败: ${res.message}`);
|
|
1072
|
+
await this.sendAuthMenu(scopeId, '', locale, messageId);
|
|
1073
|
+
return true;
|
|
1074
|
+
}
|
|
1075
|
+
if (accountName === 'repair_all') {
|
|
1076
|
+
const res = await this.auth.diagnoseAndRepairAll();
|
|
1077
|
+
await this.messaging.answerCallback(data, `🩺 体检完成: 正常/已修复 ${res.healthy + res.repaired} / 需关注 ${res.failed}`);
|
|
1078
|
+
await this.sendAuthMenu(scopeId, '', locale, messageId);
|
|
1079
|
+
return true;
|
|
1080
|
+
}
|
|
1081
|
+
if (accountName.startsWith('switch:')) {
|
|
1082
|
+
const target = accountName.slice('switch:'.length);
|
|
1083
|
+
try {
|
|
1084
|
+
const res = await this.auth.switchAccount(target);
|
|
1085
|
+
await this.messaging.answerCallback(data, `已切换到: ${res.account.email || res.account.name}`);
|
|
1086
|
+
}
|
|
1087
|
+
catch (err) {
|
|
1088
|
+
await this.messaging.answerCallback(data, `切换失败: ${String(err)}`);
|
|
1089
|
+
}
|
|
1090
|
+
await this.sendAuthMenu(scopeId, '', locale, messageId);
|
|
1091
|
+
return true;
|
|
1092
|
+
}
|
|
1093
|
+
try {
|
|
1094
|
+
const res = await this.auth.switchAccount(accountName);
|
|
1095
|
+
await this.messaging.answerCallback(data, `已切换到: ${res.account.email || res.account.name}`);
|
|
1096
|
+
await this.sendAuthMenu(scopeId, '', locale, messageId);
|
|
1097
|
+
}
|
|
1098
|
+
catch (err) {
|
|
1099
|
+
await this.messaging.answerCallback(data, `Failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1100
|
+
}
|
|
1101
|
+
return true;
|
|
1102
|
+
}
|
|
1103
|
+
return false;
|
|
1104
|
+
}
|
|
1105
|
+
async sendThreadsPanel(scopeId, search, locale, editMessageId) { return this.conversationUi.sendThreadsPanel(scopeId, search, locale, editMessageId); }
|
|
1106
|
+
async openThread(scopeId, rawTarget, locale, editMessageId) { return this.conversationUi.openThread(scopeId, rawTarget, locale, editMessageId); }
|
|
1107
|
+
async sendCodexAuthMenu(scopeId, locale, editMessageId) {
|
|
1108
|
+
const codexHome = this.config.codexAuthDir ?? this.config.codexHome ?? path.join(os.homedir(), '.codex');
|
|
1109
|
+
let currentAccount = 'default';
|
|
1110
|
+
let candidates = [];
|
|
1111
|
+
try {
|
|
1112
|
+
const files = await fsPromises.readdir(codexHome);
|
|
1113
|
+
candidates = files
|
|
1114
|
+
.filter((f) => f.startsWith('auth.json_') && !f.endsWith('.bak') && !f.endsWith('.tmp'))
|
|
1115
|
+
.map((f) => f.replace(/^auth\.json_/, ''))
|
|
1116
|
+
.sort();
|
|
1117
|
+
const authPath = path.join(codexHome, 'auth.json');
|
|
1118
|
+
const stat = await fsPromises.lstat(authPath).catch(() => null);
|
|
1119
|
+
if (stat?.isSymbolicLink()) {
|
|
1120
|
+
const target = await fsPromises.readlink(authPath);
|
|
1121
|
+
currentAccount = path.basename(target).replace(/^auth\.json_/, '');
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
catch (err) {
|
|
1125
|
+
this.logger.warn('codex.list_auth_failed', { error: String(err) });
|
|
1126
|
+
}
|
|
1127
|
+
const text = locale === 'zh'
|
|
1128
|
+
? `👤 **OpenAI Codex 账号管理 (Account Pool)**\n\n` +
|
|
1129
|
+
`• **当前活跃账号**: ● **${currentAccount}**\n` +
|
|
1130
|
+
`• **账号候选池**: 共 ${candidates.length} 个账号\n` +
|
|
1131
|
+
`• **凭据路径**: \`${path.join(codexHome, 'auth.json')}\`\n\n` +
|
|
1132
|
+
`点击下方账号名称可直接热切换 Codex 登录凭据:`
|
|
1133
|
+
: `👤 **OpenAI Codex Account Management**\n\n` +
|
|
1134
|
+
`• **Active Account**: ● **${currentAccount}**\n` +
|
|
1135
|
+
`• **Candidates**: ${candidates.length} accounts\n` +
|
|
1136
|
+
`• **Path**: \`${path.join(codexHome, 'auth.json')}\`\n\n` +
|
|
1137
|
+
`Tap an account below to switch:`;
|
|
1138
|
+
const keyboard = [];
|
|
1139
|
+
for (let i = 0; i < candidates.length; i += 2) {
|
|
1140
|
+
const row = [];
|
|
1141
|
+
const c1 = candidates[i];
|
|
1142
|
+
const isC1 = c1 === currentAccount;
|
|
1143
|
+
row.push({
|
|
1144
|
+
text: `${isC1 ? '● ' : '○ '}${c1}`,
|
|
1145
|
+
callback_data: `codex:auth:${c1}`,
|
|
1146
|
+
});
|
|
1147
|
+
if (i + 1 < candidates.length) {
|
|
1148
|
+
const c2 = candidates[i + 1];
|
|
1149
|
+
const isC2 = c2 === currentAccount;
|
|
1150
|
+
row.push({
|
|
1151
|
+
text: `${isC2 ? '● ' : '○ '}${c2}`,
|
|
1152
|
+
callback_data: `codex:auth:${c2}`,
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
keyboard.push(row);
|
|
1156
|
+
}
|
|
1157
|
+
keyboard.push([
|
|
1158
|
+
{ text: '◀️ 返回控制面板', callback_data: 'engine:setup:main' },
|
|
1159
|
+
{ text: '🔄 刷新列表', callback_data: 'codex:setup:auth' },
|
|
1160
|
+
]);
|
|
1161
|
+
if (editMessageId) {
|
|
1162
|
+
await this.editMessage(scopeId, editMessageId, text, keyboard);
|
|
1163
|
+
this.scheduleStalePanelDeletion(scopeId, editMessageId);
|
|
1164
|
+
}
|
|
1165
|
+
else {
|
|
1166
|
+
const msgId = await this.sendMessage(scopeId, text, keyboard);
|
|
1167
|
+
this.scheduleStalePanelDeletion(scopeId, msgId);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
async switchCodexAccount(scopeId, candName, locale, messageId) {
|
|
1171
|
+
const codexHome = this.config.codexAuthDir ?? this.config.codexHome ?? path.join(os.homedir(), '.codex');
|
|
1172
|
+
const authPath = path.join(codexHome, 'auth.json');
|
|
1173
|
+
const targetPath = path.join(codexHome, `auth.json_${candName}`);
|
|
1174
|
+
try {
|
|
1175
|
+
await fsPromises.unlink(authPath).catch(() => { });
|
|
1176
|
+
await fsPromises.symlink(targetPath, authPath);
|
|
1177
|
+
this.logger.info('codex.auth_switched', { candidate: candName });
|
|
1178
|
+
await this.messaging.answerCallback(`codex:auth:${candName}`, locale === 'zh' ? `已切换至 Codex 账号: ${candName}` : `Switched to: ${candName}`);
|
|
1179
|
+
if (messageId) {
|
|
1180
|
+
await this.sendCodexAuthMenu(scopeId, locale, messageId);
|
|
1181
|
+
}
|
|
1182
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
1183
|
+
? `🔄 **Codex 账号已热切换至: \`${candName}\`**`
|
|
1184
|
+
: `🔄 **Codex account switched to: \`${candName}\`**`);
|
|
1185
|
+
}
|
|
1186
|
+
catch (err) {
|
|
1187
|
+
this.logger.error('codex.auth_switch_error', { candidate: candName, error: String(err) });
|
|
1188
|
+
await this.messaging.answerCallback(`codex:auth:${candName}`, `切换失败: ${String(err)}`);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
async sendCodexThreadsMenu(scopeId, locale, editMessageId) {
|
|
1192
|
+
const codexHome = this.config.codexAuthDir ?? this.config.codexHome ?? path.join(os.homedir(), '.codex');
|
|
1193
|
+
const currentBinding = this.store.getBinding(scopeId);
|
|
1194
|
+
const activeThreadId = currentBinding?.threadId;
|
|
1195
|
+
const items = [];
|
|
1196
|
+
try {
|
|
1197
|
+
const indexPath = path.join(codexHome, 'session_index.jsonl');
|
|
1198
|
+
const content = await fsPromises.readFile(indexPath, 'utf8').catch(() => '');
|
|
1199
|
+
const lines = content.trim().split('\n').filter(Boolean);
|
|
1200
|
+
const seenIds = new Set();
|
|
1201
|
+
for (let i = lines.length - 1; i >= 0 && items.length < 8; i--) {
|
|
1202
|
+
try {
|
|
1203
|
+
const parsed = JSON.parse(lines[i]);
|
|
1204
|
+
if (parsed.id && !seenIds.has(parsed.id)) {
|
|
1205
|
+
seenIds.add(parsed.id);
|
|
1206
|
+
items.push({
|
|
1207
|
+
id: parsed.id,
|
|
1208
|
+
title: parsed.thread_name || parsed.id.slice(0, 16),
|
|
1209
|
+
updatedAt: parsed.updated_at,
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
catch {
|
|
1214
|
+
/* ignore */
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
catch (err) {
|
|
1219
|
+
this.logger.warn('codex.list_threads_failed', { error: String(err) });
|
|
1220
|
+
}
|
|
1221
|
+
const listLines = items.map((item, idx) => {
|
|
1222
|
+
const num = idx + 1;
|
|
1223
|
+
const isActive = item.id === activeThreadId;
|
|
1224
|
+
return `${isActive ? '●' : '○'} **${num}.** ${item.title}${isActive ? ' `[当前会话]`' : ''}\n \`${item.id.slice(0, 16)}…\``;
|
|
1225
|
+
});
|
|
1226
|
+
const text = locale === 'zh'
|
|
1227
|
+
? `📁 **OpenAI Codex 会话历史**\n\n` +
|
|
1228
|
+
`• **当前会话**: ${activeThreadId ? `\`${activeThreadId.slice(0, 16)}…\`` : '`新会话就绪 (发送消息开启)`'}\n` +
|
|
1229
|
+
`• **工作目录**: \`${currentBinding?.cwd || this.config.defaultCwd}\`\n\n` +
|
|
1230
|
+
(items.length > 0
|
|
1231
|
+
? `**最近会话列表**:\n${listLines.join('\n')}\n\n点击下方按钮恢复会话,或使用 \`/open <编号>\`:`
|
|
1232
|
+
: `暂无历史会话记录。发送消息将开启新会话。`)
|
|
1233
|
+
: `📁 **OpenAI Codex Session History**\n\n` +
|
|
1234
|
+
`• **Current Thread**: ${activeThreadId ? `\`${activeThreadId.slice(0, 16)}…\`` : '`Ready (new)`'}\n` +
|
|
1235
|
+
`• **Directory**: \`${currentBinding?.cwd || this.config.defaultCwd}\`\n\n` +
|
|
1236
|
+
(items.length > 0 ? `${listLines.join('\n')}\n\nTap below to resume or use \`/open <num>\`:` : 'No sessions found.');
|
|
1237
|
+
const keyboard = [];
|
|
1238
|
+
for (let i = 0; i < items.length; i += 2) {
|
|
1239
|
+
const row = [];
|
|
1240
|
+
const item1 = items[i];
|
|
1241
|
+
const isA1 = item1.id === activeThreadId;
|
|
1242
|
+
const shortTitle1 = item1.title.length > 14 ? item1.title.slice(0, 13) + '…' : item1.title;
|
|
1243
|
+
row.push({
|
|
1244
|
+
text: `${isA1 ? '● ' : ''}${i + 1}. ${shortTitle1}`,
|
|
1245
|
+
callback_data: `codex:open:${item1.id}`,
|
|
1246
|
+
});
|
|
1247
|
+
if (i + 1 < items.length) {
|
|
1248
|
+
const item2 = items[i + 1];
|
|
1249
|
+
const isA2 = item2.id === activeThreadId;
|
|
1250
|
+
const shortTitle2 = item2.title.length > 14 ? item2.title.slice(0, 13) + '…' : item2.title;
|
|
1251
|
+
row.push({
|
|
1252
|
+
text: `${isA2 ? '● ' : ''}${i + 2}. ${shortTitle2}`,
|
|
1253
|
+
callback_data: `codex:open:${item2.id}`,
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
keyboard.push(row);
|
|
1257
|
+
}
|
|
1258
|
+
keyboard.push([
|
|
1259
|
+
{ text: '✨ 新建会话', callback_data: 'engine:setup:new' },
|
|
1260
|
+
{ text: '◀️ 返回控制面板', callback_data: 'engine:setup:main' },
|
|
1261
|
+
]);
|
|
1262
|
+
if (editMessageId) {
|
|
1263
|
+
await this.editMessage(scopeId, editMessageId, text, keyboard);
|
|
1264
|
+
this.scheduleStalePanelDeletion(scopeId, editMessageId);
|
|
1265
|
+
}
|
|
1266
|
+
else {
|
|
1267
|
+
const msgId = await this.sendMessage(scopeId, text, keyboard);
|
|
1268
|
+
this.scheduleStalePanelDeletion(scopeId, msgId);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
async openCodexThread(scopeId, rawTarget, locale, messageId) {
|
|
1272
|
+
const codexHome = this.config.codexAuthDir ?? this.config.codexHome ?? path.join(os.homedir(), '.codex');
|
|
1273
|
+
let targetThreadId = rawTarget.trim();
|
|
1274
|
+
if (/^\d+$/.test(targetThreadId)) {
|
|
1275
|
+
const idx = parseInt(targetThreadId, 10) - 1;
|
|
1276
|
+
try {
|
|
1277
|
+
const indexPath = path.join(codexHome, 'session_index.jsonl');
|
|
1278
|
+
const content = await fsPromises.readFile(indexPath, 'utf8').catch(() => '');
|
|
1279
|
+
const lines = content.trim().split('\n').filter(Boolean);
|
|
1280
|
+
const seenIds = new Set();
|
|
1281
|
+
const items = [];
|
|
1282
|
+
for (let i = lines.length - 1; i >= 0 && items.length < 20; i--) {
|
|
1283
|
+
try {
|
|
1284
|
+
const parsed = JSON.parse(lines[i]);
|
|
1285
|
+
if (parsed.id && !seenIds.has(parsed.id)) {
|
|
1286
|
+
seenIds.add(parsed.id);
|
|
1287
|
+
items.push(parsed.id);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
catch {
|
|
1291
|
+
/* ignore */
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
if (idx >= 0 && idx < items.length) {
|
|
1295
|
+
targetThreadId = items[idx];
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
catch {
|
|
1299
|
+
/* ignore */
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
const currentBinding = this.store.getBinding(scopeId);
|
|
1303
|
+
const targetCwd = currentBinding?.cwd || this.config.defaultCwd;
|
|
1304
|
+
this.store.setBinding(scopeId, targetThreadId, targetCwd);
|
|
1305
|
+
const activeBackend = this.orchestrator.getBackendDescriptorForScope(scopeId);
|
|
1306
|
+
this.store.setScopeBackendBinding(scopeId, activeBackend.id, targetThreadId, targetCwd);
|
|
1307
|
+
if (messageId) {
|
|
1308
|
+
await this.messaging.answerCallback(`codex:open:${targetThreadId}`, locale === 'zh' ? '已恢复会话' : 'Thread resumed');
|
|
1309
|
+
}
|
|
1310
|
+
const text = locale === 'zh'
|
|
1311
|
+
? `📂 **已切换绑定到既有 OpenAI Codex 会话**\n\n` +
|
|
1312
|
+
`• **会话 ID**: \`${targetThreadId}\`\n` +
|
|
1313
|
+
`• **工作目录**: \`${targetCwd}\`\n\n` +
|
|
1314
|
+
`后续消息将在此 Codex 会话继续执行。`
|
|
1315
|
+
: `📂 **Switched to Codex Session**\n\n` +
|
|
1316
|
+
`• **ID**: \`${targetThreadId}\`\n` +
|
|
1317
|
+
`• **Directory**: \`${targetCwd}\`\n\n` +
|
|
1318
|
+
`Next messages will continue in this thread.`;
|
|
1319
|
+
const keyboard = [
|
|
1320
|
+
[
|
|
1321
|
+
{ text: '📁 查看其他会话', callback_data: 'codex:setup:threads' },
|
|
1322
|
+
{ text: '⚙️ 控制面板', callback_data: 'engine:setup:main' },
|
|
1323
|
+
],
|
|
1324
|
+
];
|
|
1325
|
+
if (messageId) {
|
|
1326
|
+
await this.editMessage(scopeId, messageId, text, keyboard);
|
|
1327
|
+
}
|
|
1328
|
+
else {
|
|
1329
|
+
await this.sendMessage(scopeId, text, keyboard);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
async watchConversation(scopeId, rawTarget, locale, editMessageId, notify = true) { return this.conversationUi.watchConversation(scopeId, rawTarget, locale, editMessageId, notify); }
|
|
1333
|
+
async unwatchConversation(scopeId, locale, notify = true) { return this.conversationUi.unwatchConversation(scopeId, locale, notify); }
|
|
1334
|
+
scheduleWatcherPoll(watcher) { return this.conversationUi.scheduleWatcherPoll(watcher); }
|
|
1335
|
+
getScopeLocale(scopeId) { return this.conversationUi.getScopeLocale(scopeId); }
|
|
1336
|
+
async renderWatcherProgressUpdate(watcher) { return this.conversationUi.renderWatcherProgressUpdate(watcher); }
|
|
1337
|
+
async pollWatcher(watcher) { return this.conversationUi.pollWatcher(watcher); }
|
|
1338
|
+
async handleWatcherTranscriptEntry(watcher, entry) { return this.conversationUi.handleWatcherTranscriptEntry(watcher, entry); }
|
|
1339
|
+
async sendAuthMenu(scopeId, args, locale, editMessageId) { return this.accountUi.sendAuthMenu(scopeId, args, locale, editMessageId); }
|
|
1340
|
+
async startLoginFlow(scopeId, locale, editMessageId) { return this.accountUi.startLoginFlow(scopeId, locale, editMessageId); }
|
|
1341
|
+
async sendEffortMenu(scopeId, arg, locale, editMessageId) { return this.accountUi.sendEffortMenu(scopeId, arg, locale, editMessageId); }
|
|
1342
|
+
}
|
|
1343
|
+
export { UnifiedBridgeCore as AntigravityBridgeCore };
|