@foxden-app/foxclaw 0.7.2 → 0.10.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/.env.example +14 -0
- package/CHANGELOG.md +129 -0
- package/README.md +22 -2
- package/README_EN.md +22 -2
- package/dist/antigravity/adapter.d.ts +17 -0
- package/dist/antigravity/adapter.js +256 -0
- package/dist/antigravity/auth.d.ts +121 -0
- package/dist/antigravity/auth.js +896 -0
- package/dist/antigravity/client.d.ts +32 -0
- package/dist/antigravity/client.js +238 -0
- package/dist/antigravity/controller.d.ts +96 -0
- package/dist/antigravity/controller.js +1922 -0
- package/dist/antigravity/conversations.d.ts +29 -0
- package/dist/antigravity/conversations.js +289 -0
- package/dist/antigravity/events.d.ts +101 -0
- package/dist/antigravity/events.js +114 -0
- package/dist/antigravity/runtime.d.ts +47 -0
- package/dist/antigravity/runtime.js +82 -0
- package/dist/auth/cross_node_sync.js +2 -2
- package/dist/channels/telegram/telegram_channel_adapter.d.ts +10 -3
- package/dist/channels/telegram/telegram_channel_adapter.js +2 -1
- package/dist/channels/weixin/weixin_channel_adapter.d.ts +5 -1
- package/dist/codex_app/adapter.d.ts +20 -0
- package/dist/codex_app/adapter.js +213 -0
- package/dist/codex_app/client.d.ts +2 -1
- package/dist/codex_app/client.js +47 -4
- package/dist/codex_app/force_takeover.d.ts +12 -0
- package/dist/codex_app/force_takeover.js +27 -0
- package/dist/config.d.ts +12 -0
- package/dist/config.js +42 -0
- package/dist/controller/controller.d.ts +15 -5
- package/dist/controller/controller.js +252 -20
- package/dist/core/attachments.d.ts +11 -0
- package/dist/core/attachments.js +56 -0
- package/dist/core/engine_spi.d.ts +76 -0
- package/dist/core/engine_spi.js +1 -0
- package/dist/core/orchestrator.d.ts +101 -0
- package/dist/core/orchestrator.js +1404 -0
- package/dist/core/stream_preview.d.ts +29 -0
- package/dist/core/stream_preview.js +146 -0
- package/dist/core/turn_queue.d.ts +19 -0
- package/dist/core/turn_queue.js +41 -0
- package/dist/i18n.d.ts +36 -6
- package/dist/i18n.js +81 -6
- package/dist/main.js +190 -20
- package/dist/opencode/adapter.d.ts +11 -0
- package/dist/opencode/adapter.js +168 -0
- package/dist/store/database.d.ts +41 -0
- package/dist/store/database.js +242 -43
- package/dist/store/token_usage.d.ts +23 -0
- package/dist/store/token_usage.js +61 -0
- package/dist/telegram/api.js +34 -1
- package/dist/telegram/bot_home.d.ts +3 -0
- package/dist/telegram/bot_home.js +107 -0
- package/dist/telegram/gateway.d.ts +5 -0
- package/dist/telegram/gateway.js +21 -0
- package/dist/types.d.ts +1 -0
- package/dist/update.d.ts +6 -0
- package/dist/update.js +78 -6
- package/dist/voice/target.js +2 -1
- package/dist/voice/tts.js +4 -2
- package/docs/devlogs//346/212/200/346/234/257/351/200/232/350/256/257/02./347/273/237/344/270/200/351/200/232/351/201/223/350/260/203/345/272/246/345/231/250/344/270/216/345/217/257/346/217/222/346/213/224/345/274/225/346/223/216SPI/357/274/232FoxClaw/344/273/216/345/215/225/344/275/223/350/265/260/345/220/221/345/244/232Agent/345/272/225/345/272/247.md +192 -0
- package/docs/devlogs//347/254/254/344/270/200/344/272/272/347/247/260/347/211/210/02./347/257/207/344/272/214_/345/275/223/344/270/200/344/270/252/346/241/245/346/216/245/345/231/250/345/274/200/345/247/213/345/255/246/344/274/232/345/220/254/346/207/202/346/211/200/346/234/211Agent.md +45 -0
- package/docs/user-manual.md +11 -1
- package/docs/zh/2026-09-05-bot-home-migration.md +26 -0
- package/docs/zh/2026-09-05-reliability-review.md +53 -0
- package/docs/zh/troubleshooting.md +12 -0
- package/docs/zh/user-manual.md +13 -1
- package/package.json +2 -2
- package/scripts/force-takeover.py +118 -0
- package/scripts/force-takeover.test.py +122 -0
|
@@ -0,0 +1,1922 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import fsPromises from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import { TelegramMessagingPort } from '../channels/telegram/telegram_messaging_port.js';
|
|
6
|
+
import { chunkTelegramMessage } from '../telegram/text.js';
|
|
7
|
+
import { escapeTelegramHtml } from '../telegram/html.js';
|
|
8
|
+
import { AntigravityAppClient } from './client.js';
|
|
9
|
+
import { formatQuotaResetTime, AntigravityAuthManager } from './auth.js';
|
|
10
|
+
import { AntigravityConversationManager, formatAge } from './conversations.js';
|
|
11
|
+
import { UnifiedChannelOrchestrator } from '../core/orchestrator.js';
|
|
12
|
+
import { AntigravityEngineAdapter } from './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 } from '../core/bridge_scope.js';
|
|
18
|
+
import { syncCodexLocalUsageToStore } from '../store/token_usage.js';
|
|
19
|
+
function formatCandidateButtonPrefix(c) {
|
|
20
|
+
const p5h = typeof c.quota?.fiveHourPercent === 'number' ? `${c.quota.fiveHourPercent}%` : '—';
|
|
21
|
+
const pw = typeof c.quota?.weeklyPercent === 'number' ? `${c.quota.weeklyPercent}%` : '—';
|
|
22
|
+
return `${p5h}|${pw}`;
|
|
23
|
+
}
|
|
24
|
+
function formatCandidateDisplayName(c) {
|
|
25
|
+
if (c.email) {
|
|
26
|
+
return c.email.replace(/@gmail\.com$/i, '');
|
|
27
|
+
}
|
|
28
|
+
return c.name.replace(/^antigravity-oauth-token_/, '');
|
|
29
|
+
}
|
|
30
|
+
function formatAccountExpiry(expiryMs, locale, isActive = false) {
|
|
31
|
+
if (!expiryMs)
|
|
32
|
+
return locale === 'zh' ? '未知' : 'unknown';
|
|
33
|
+
const now = Date.now();
|
|
34
|
+
if (expiryMs <= now) {
|
|
35
|
+
if (isActive) {
|
|
36
|
+
return locale === 'zh' ? '待命中 (发任务时即时刷新)' : 'standby (refreshes on task)';
|
|
37
|
+
}
|
|
38
|
+
return locale === 'zh' ? '待命中 (就绪 · 切号时即时激活)' : 'standby (ready · refreshes on switch)';
|
|
39
|
+
}
|
|
40
|
+
const remainingMins = Math.round((expiryMs - now) / 60000);
|
|
41
|
+
const timeStr = new Date(expiryMs).toTimeString().slice(0, 5);
|
|
42
|
+
return locale === 'zh'
|
|
43
|
+
? `${timeStr} (剩 ${remainingMins} 分钟)`
|
|
44
|
+
: `${timeStr} (${remainingMins}m left)`;
|
|
45
|
+
}
|
|
46
|
+
const AGY_MODEL_CALLBACK_PREFIX = 'agy:m:';
|
|
47
|
+
const AGY_AUTH_CALLBACK_PREFIX = 'agy:a:';
|
|
48
|
+
const AGY_EFFORT_CALLBACK_PREFIX = 'agy:e:';
|
|
49
|
+
const AGY_OPEN_CALLBACK_PREFIX = 'agy:open:';
|
|
50
|
+
const AGY_SETUP_CALLBACK_PREFIX = 'agy:setup:';
|
|
51
|
+
const AGY_WATCH_CALLBACK_PREFIX = 'agy:w:';
|
|
52
|
+
export class UnifiedBridgeCore {
|
|
53
|
+
config;
|
|
54
|
+
store;
|
|
55
|
+
logger;
|
|
56
|
+
bot;
|
|
57
|
+
app;
|
|
58
|
+
auth;
|
|
59
|
+
conversations;
|
|
60
|
+
messaging;
|
|
61
|
+
adapter;
|
|
62
|
+
codexAdapter;
|
|
63
|
+
codexApp;
|
|
64
|
+
codexCore;
|
|
65
|
+
opencodeAdapter;
|
|
66
|
+
defaultBackendId;
|
|
67
|
+
orchestrator;
|
|
68
|
+
watchers = new Map();
|
|
69
|
+
pendingAgyRenames = new Map();
|
|
70
|
+
stalePanelDeleteTimers = new Map();
|
|
71
|
+
constructor(config, store, logger, bot, app, auth, messaging, options) {
|
|
72
|
+
this.config = config;
|
|
73
|
+
this.store = store;
|
|
74
|
+
this.logger = logger;
|
|
75
|
+
this.bot = bot;
|
|
76
|
+
this.auth = options?.antigravityAuth ?? auth ?? new AntigravityAuthManager(config.antigravityAuthDir, logger);
|
|
77
|
+
this.app = options?.antigravityApp ?? app ?? new AntigravityAppClient(config.antigravityCliBin, logger);
|
|
78
|
+
const effectiveAuthDir = this.auth.authDir || config.antigravityAuthDir;
|
|
79
|
+
this.conversations = new AntigravityConversationManager(effectiveAuthDir, logger);
|
|
80
|
+
this.messaging = messaging ?? new TelegramMessagingPort(bot);
|
|
81
|
+
this.codexApp = options?.codexApp;
|
|
82
|
+
this.defaultBackendId = options?.defaultBackendId ?? 'antigravity';
|
|
83
|
+
this.codexCore = options?.codexCore;
|
|
84
|
+
if (!this.codexCore && options?.codexApp) {
|
|
85
|
+
try {
|
|
86
|
+
this.codexCore = new BridgeSessionCore(this.config, this.store, this.logger, this.bot, options.codexApp, new BridgeMessagingRouter(this.messaging, null), null, null, false);
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
this.logger.warn('unified.codex_core_init_failed', { error: String(err) });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
this.adapter = options?.antigravityAdapter ?? new AntigravityEngineAdapter(this.app, config.antigravityDefaultModel, this.auth, logger);
|
|
93
|
+
if (options?.codexAdapter) {
|
|
94
|
+
this.codexAdapter = options.codexAdapter;
|
|
95
|
+
}
|
|
96
|
+
else if (options?.codexApp) {
|
|
97
|
+
this.codexAdapter = new CodexEngineAdapter(options.codexApp, {
|
|
98
|
+
defaultModel: 'gpt-4o',
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
if (options?.opencodeAdapter) {
|
|
102
|
+
this.opencodeAdapter = options.opencodeAdapter;
|
|
103
|
+
}
|
|
104
|
+
else if (options?.opencodeApp) {
|
|
105
|
+
this.opencodeAdapter = new OpencodeEngineAdapter(options.opencodeApp);
|
|
106
|
+
}
|
|
107
|
+
const isCodexDefault = this.defaultBackendId === 'codex';
|
|
108
|
+
const isOpencodeDefault = this.defaultBackendId === 'opencode';
|
|
109
|
+
const primaryAdapter = isCodexDefault && this.codexAdapter
|
|
110
|
+
? this.codexAdapter
|
|
111
|
+
: isOpencodeDefault && this.opencodeAdapter
|
|
112
|
+
? this.opencodeAdapter
|
|
113
|
+
: this.adapter;
|
|
114
|
+
const initialBackends = [];
|
|
115
|
+
if (isCodexDefault && this.codexAdapter) {
|
|
116
|
+
initialBackends.push({
|
|
117
|
+
id: 'codex',
|
|
118
|
+
name: 'OpenAI Codex (App Server)',
|
|
119
|
+
engineType: 'codex',
|
|
120
|
+
adapter: this.codexAdapter,
|
|
121
|
+
isDefault: true,
|
|
122
|
+
});
|
|
123
|
+
initialBackends.push({
|
|
124
|
+
id: 'antigravity',
|
|
125
|
+
name: 'Google Antigravity (AGY)',
|
|
126
|
+
engineType: 'antigravity',
|
|
127
|
+
adapter: this.adapter,
|
|
128
|
+
isDefault: false,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
initialBackends.push({
|
|
133
|
+
id: 'antigravity',
|
|
134
|
+
name: 'Google Antigravity (AGY)',
|
|
135
|
+
engineType: 'antigravity',
|
|
136
|
+
adapter: this.adapter,
|
|
137
|
+
isDefault: !isOpencodeDefault,
|
|
138
|
+
});
|
|
139
|
+
if (this.codexAdapter) {
|
|
140
|
+
initialBackends.push({
|
|
141
|
+
id: 'codex',
|
|
142
|
+
name: 'OpenAI Codex (App Server)',
|
|
143
|
+
engineType: 'codex',
|
|
144
|
+
adapter: this.codexAdapter,
|
|
145
|
+
isDefault: false,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (this.opencodeAdapter) {
|
|
150
|
+
initialBackends.push({
|
|
151
|
+
id: 'opencode',
|
|
152
|
+
name: 'OpenCode (SDK)',
|
|
153
|
+
engineType: 'opencode',
|
|
154
|
+
adapter: this.opencodeAdapter,
|
|
155
|
+
isDefault: isOpencodeDefault,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
const allAdapters = [this.adapter];
|
|
159
|
+
if (this.codexAdapter)
|
|
160
|
+
allAdapters.push(this.codexAdapter);
|
|
161
|
+
if (this.opencodeAdapter)
|
|
162
|
+
allAdapters.push(this.opencodeAdapter);
|
|
163
|
+
this.orchestrator = new UnifiedChannelOrchestrator({
|
|
164
|
+
config,
|
|
165
|
+
store,
|
|
166
|
+
logger,
|
|
167
|
+
bot,
|
|
168
|
+
adapter: primaryAdapter,
|
|
169
|
+
adapters: allAdapters,
|
|
170
|
+
backends: initialBackends,
|
|
171
|
+
defaultBackendId: this.defaultBackendId,
|
|
172
|
+
backendProvider: () => this.getBackendDescriptors(),
|
|
173
|
+
messaging: this.messaging,
|
|
174
|
+
customUi: {
|
|
175
|
+
renderCustomStatus: (scopeId, locale) => this.renderCustomStatus(scopeId, locale),
|
|
176
|
+
renderCustomSetupRows: (scopeId, locale) => this.renderCustomSetupRows(scopeId, locale),
|
|
177
|
+
handleCustomCallback: (scopeId, data, locale, messageId, event) => this.handleCustomCallback(scopeId, data, locale, messageId, event),
|
|
178
|
+
handleCustomCommand: (scopeId, command, args, locale, event) => this.handleCustomCommand(scopeId, command, args, locale, event),
|
|
179
|
+
handleCustomInbound: (event, locale) => this.handleCustomInbound(event, locale),
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
async getBackendDescriptors() {
|
|
184
|
+
const list = [];
|
|
185
|
+
const isCodexDefault = this.defaultBackendId === 'codex';
|
|
186
|
+
const active = await this.auth.getActiveAccount();
|
|
187
|
+
const activeQuota = active?.quota
|
|
188
|
+
? `${formatCandidateButtonPrefix(active)} · ${formatAccountExpiry(active.expiry, 'zh', true)}`
|
|
189
|
+
: undefined;
|
|
190
|
+
const agyPrimary = {
|
|
191
|
+
id: 'antigravity',
|
|
192
|
+
name: 'Google Antigravity (AGY)',
|
|
193
|
+
engineType: 'antigravity',
|
|
194
|
+
adapter: this.adapter,
|
|
195
|
+
account: active?.email || active?.name || 'default',
|
|
196
|
+
details: activeQuota,
|
|
197
|
+
isDefault: !isCodexDefault,
|
|
198
|
+
};
|
|
199
|
+
let codexPrimary = null;
|
|
200
|
+
if (this.codexAdapter) {
|
|
201
|
+
const codexHome = this.config.codexAuthDir ?? this.config.codexHome ?? path.join(os.homedir(), '.codex');
|
|
202
|
+
let currentCodexAccount = 'default';
|
|
203
|
+
try {
|
|
204
|
+
const authPath = path.join(codexHome, 'auth.json');
|
|
205
|
+
const stat = await fsPromises.lstat(authPath).catch(() => null);
|
|
206
|
+
if (stat?.isSymbolicLink()) {
|
|
207
|
+
const target = await fsPromises.readlink(authPath);
|
|
208
|
+
currentCodexAccount = path.basename(target).replace(/^auth\.json_/, '');
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
// ignore
|
|
213
|
+
}
|
|
214
|
+
codexPrimary = {
|
|
215
|
+
id: 'codex',
|
|
216
|
+
name: 'OpenAI Codex (App Server)',
|
|
217
|
+
engineType: 'codex',
|
|
218
|
+
adapter: this.codexAdapter,
|
|
219
|
+
account: currentCodexAccount,
|
|
220
|
+
details: 'Official Codex App runtime',
|
|
221
|
+
isDefault: isCodexDefault,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
let opencodePrimary = null;
|
|
225
|
+
if (this.opencodeAdapter) {
|
|
226
|
+
opencodePrimary = {
|
|
227
|
+
id: 'opencode',
|
|
228
|
+
name: 'OpenCode (SDK)',
|
|
229
|
+
engineType: 'opencode',
|
|
230
|
+
adapter: this.opencodeAdapter,
|
|
231
|
+
account: 'default',
|
|
232
|
+
details: 'OpenCode CLI / Server',
|
|
233
|
+
isDefault: false,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
if (isCodexDefault) {
|
|
237
|
+
if (codexPrimary)
|
|
238
|
+
list.push(codexPrimary);
|
|
239
|
+
list.push(agyPrimary);
|
|
240
|
+
if (opencodePrimary)
|
|
241
|
+
list.push(opencodePrimary);
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
list.push(agyPrimary);
|
|
245
|
+
if (codexPrimary)
|
|
246
|
+
list.push(codexPrimary);
|
|
247
|
+
if (opencodePrimary)
|
|
248
|
+
list.push(opencodePrimary);
|
|
249
|
+
}
|
|
250
|
+
return list;
|
|
251
|
+
}
|
|
252
|
+
registerInboundHandlers() {
|
|
253
|
+
this.orchestrator.registerInboundHandlers();
|
|
254
|
+
}
|
|
255
|
+
registerTelegramInboundHandlers() {
|
|
256
|
+
this.registerInboundHandlers();
|
|
257
|
+
}
|
|
258
|
+
dispatchInboundLikeTelegramText(event) {
|
|
259
|
+
this.orchestrator.dispatchInboundLikeTelegramText(event);
|
|
260
|
+
}
|
|
261
|
+
async startCodexApp() {
|
|
262
|
+
if (this.codexApp && typeof this.codexApp.isConnected === 'function' && !this.codexApp.isConnected()) {
|
|
263
|
+
if (typeof this.codexApp.start === 'function') {
|
|
264
|
+
await this.codexApp.start().catch((err) => {
|
|
265
|
+
this.logger.warn('codex.app.start_failed', { error: String(err) });
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
async startTelegramPolling() {
|
|
271
|
+
await this.start();
|
|
272
|
+
}
|
|
273
|
+
async start() {
|
|
274
|
+
await this.startCodexApp();
|
|
275
|
+
await this.orchestrator.start();
|
|
276
|
+
this.auth.startKeepAlive();
|
|
277
|
+
this.logger.info('bridge.started', { defaultBackendId: this.defaultBackendId });
|
|
278
|
+
void syncCodexLocalUsageToStore(this.store, this.config.codexHome ?? undefined);
|
|
279
|
+
// Restore persisted watchers on startup
|
|
280
|
+
try {
|
|
281
|
+
const persisted = this.store.listWatchedThreads();
|
|
282
|
+
for (const item of persisted) {
|
|
283
|
+
this.watchConversation(item.scopeId, item.threadId, 'zh', undefined, false).catch((err) => {
|
|
284
|
+
this.logger.warn('antigravity.restore_watcher_failed', { scopeId: item.scopeId, error: String(err) });
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
catch (err) {
|
|
289
|
+
this.logger.warn('antigravity.restore_watchers_error', { error: String(err) });
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
async stop() {
|
|
293
|
+
await this.orchestrator.stop();
|
|
294
|
+
this.auth.stopKeepAlive();
|
|
295
|
+
for (const [scopeId, watcher] of this.watchers.entries()) {
|
|
296
|
+
watcher.stopped = true;
|
|
297
|
+
if (watcher.timer)
|
|
298
|
+
clearTimeout(watcher.timer);
|
|
299
|
+
this.watchers.delete(scopeId);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
getRuntimeStatus() {
|
|
303
|
+
const isConnected = this.codexApp && typeof this.codexApp.isConnected === 'function'
|
|
304
|
+
? this.codexApp.isConnected()
|
|
305
|
+
: true;
|
|
306
|
+
return {
|
|
307
|
+
running: true,
|
|
308
|
+
connected: isConnected,
|
|
309
|
+
activeTurns: this.orchestrator.getActiveTurnsCount(),
|
|
310
|
+
botUsername: this.bot.username,
|
|
311
|
+
codexHome: this.config.codexHome ?? this.config.codexAuthDir ?? path.join(os.homedir(), '.codex'),
|
|
312
|
+
...(this.codexApp && typeof this.codexApp.getServerStatus === 'function' && this.codexApp.getServerStatus()
|
|
313
|
+
? { codexAppServer: this.codexApp.getServerStatus() }
|
|
314
|
+
: {}),
|
|
315
|
+
userAgent: this.codexApp && typeof this.codexApp.getUserAgent === 'function'
|
|
316
|
+
? this.codexApp.getUserAgent()
|
|
317
|
+
: null,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
isIdleForServiceUpdate() {
|
|
321
|
+
return this.orchestrator.getActiveTurnsCount() === 0;
|
|
322
|
+
}
|
|
323
|
+
async getCurrentAuthLabel() {
|
|
324
|
+
const codexHome = this.config.codexAuthDir ?? this.config.codexHome ?? path.join(os.homedir(), '.codex');
|
|
325
|
+
try {
|
|
326
|
+
const authPath = path.join(codexHome, 'auth.json');
|
|
327
|
+
const stat = await fsPromises.lstat(authPath).catch(() => null);
|
|
328
|
+
if (stat?.isSymbolicLink()) {
|
|
329
|
+
const target = await fsPromises.readlink(authPath);
|
|
330
|
+
return path.basename(target).replace(/^auth\.json_/, '');
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
/* ignore */
|
|
335
|
+
}
|
|
336
|
+
return 'default';
|
|
337
|
+
}
|
|
338
|
+
async handleExternalCodexAuthCandidateDeleted(candidateName, _reason = null) {
|
|
339
|
+
void _reason;
|
|
340
|
+
this.store.deleteCodexAuthCandidate(candidateName);
|
|
341
|
+
if (this.codexApp) {
|
|
342
|
+
await this.codexApp.restart().catch(() => { });
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
async validateExternalCodexAuthCandidate(_candidateName, _rawAuth, _expectedAccountId) {
|
|
346
|
+
void _candidateName;
|
|
347
|
+
void _rawAuth;
|
|
348
|
+
void _expectedAccountId;
|
|
349
|
+
return { ok: true };
|
|
350
|
+
}
|
|
351
|
+
async sendMessage(scopeId, text, inlineKeyboard) {
|
|
352
|
+
try {
|
|
353
|
+
return await this.messaging.sendRichMarkdown(scopeId, text, inlineKeyboard);
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
return this.messaging.sendPlain(scopeId, text, inlineKeyboard);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
async editMessage(scopeId, messageId, text, inlineKeyboard) {
|
|
360
|
+
try {
|
|
361
|
+
await this.messaging.editRichMarkdown(scopeId, messageId, text, inlineKeyboard);
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
await this.messaging.editPlain(scopeId, messageId, text, inlineKeyboard);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
scheduleStalePanelDeletion(scopeId, messageId) {
|
|
368
|
+
if (this.config.telegramPanelTtlMs <= 0 || scopeId.startsWith(BRIDGE_SCOPE_WEIXIN_PREFIX)) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
const key = `${scopeId}:${messageId}`;
|
|
372
|
+
const existing = this.stalePanelDeleteTimers.get(key);
|
|
373
|
+
if (existing) {
|
|
374
|
+
clearTimeout(existing);
|
|
375
|
+
}
|
|
376
|
+
const timer = setTimeout(() => {
|
|
377
|
+
this.stalePanelDeleteTimers.delete(key);
|
|
378
|
+
if (typeof this.messaging?.deleteMessage === 'function') {
|
|
379
|
+
void this.messaging.deleteMessage(scopeId, messageId).catch((error) => {
|
|
380
|
+
this.logger.debug('telegram.stale_panel_delete_failed', { scopeId, messageId, error: String(error) });
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}, this.config.telegramPanelTtlMs);
|
|
384
|
+
timer.unref();
|
|
385
|
+
this.stalePanelDeleteTimers.set(key, timer);
|
|
386
|
+
}
|
|
387
|
+
async renderCustomStatus(scopeId, locale) {
|
|
388
|
+
const activeBackend = this.orchestrator.getBackendDescriptorForScope(scopeId);
|
|
389
|
+
if (activeBackend.engineType === 'codex') {
|
|
390
|
+
const codexHome = this.config.codexAuthDir ?? this.config.codexHome ?? path.join(os.homedir(), '.codex');
|
|
391
|
+
let currentCodexAccount = 'default';
|
|
392
|
+
try {
|
|
393
|
+
const authPath = path.join(codexHome, 'auth.json');
|
|
394
|
+
const stat = await fsPromises.lstat(authPath).catch(() => null);
|
|
395
|
+
if (stat?.isSymbolicLink()) {
|
|
396
|
+
const target = await fsPromises.readlink(authPath);
|
|
397
|
+
currentCodexAccount = path.basename(target).replace(/^auth\.json_/, '');
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
catch {
|
|
401
|
+
/* ignore */
|
|
402
|
+
}
|
|
403
|
+
const binding = this.store.getBinding(scopeId);
|
|
404
|
+
const threadLine = binding?.threadId
|
|
405
|
+
? (locale === 'zh' ? `\n• **当前会话**: \`${binding.threadId.slice(0, 16)}…\`` : `\n• **Thread**: \`${binding.threadId.slice(0, 16)}…\``)
|
|
406
|
+
: (locale === 'zh' ? `\n• **当前会话**: \`新会话就绪 (发送消息开启)\`` : `\n• **Thread**: \`Ready (new)\``);
|
|
407
|
+
const cwdLine = `\n• **工作目录**: \`${binding?.cwd || this.config.defaultCwd}\``;
|
|
408
|
+
return ((locale === 'zh'
|
|
409
|
+
? `• **Codex 账号**: \`${currentCodexAccount}\` (软链接管理)\n• **执行引擎**: \`OpenAI Codex App Server\``
|
|
410
|
+
: `• **Codex Account**: \`${currentCodexAccount}\`\n• **Engine**: \`OpenAI Codex App Server\``) +
|
|
411
|
+
threadLine +
|
|
412
|
+
cwdLine);
|
|
413
|
+
}
|
|
414
|
+
const activeAccount = await this.auth.getActiveAccount();
|
|
415
|
+
const candidates = await this.auth.listCandidates();
|
|
416
|
+
const currentWatcher = this.watchers.get(scopeId);
|
|
417
|
+
let watchLine = '';
|
|
418
|
+
if (currentWatcher && !currentWatcher.stopped) {
|
|
419
|
+
watchLine =
|
|
420
|
+
locale === 'zh'
|
|
421
|
+
? `\n• **观察状态**: 👁 正在观察 \`${currentWatcher.conversationId.slice(0, 8)}\``
|
|
422
|
+
: `\n• **Watching**: 👁 Observing \`${currentWatcher.conversationId.slice(0, 8)}\``;
|
|
423
|
+
}
|
|
424
|
+
let quotaLine = '';
|
|
425
|
+
if (activeAccount) {
|
|
426
|
+
const q = activeAccount.quota ?? (await this.auth.fetchQuotaForAccount(activeAccount.name));
|
|
427
|
+
if (q) {
|
|
428
|
+
const p5h = typeof q.fiveHourPercent === 'number' ? `${q.fiveHourPercent}%` : '—';
|
|
429
|
+
const pw = typeof q.weeklyPercent === 'number' ? `${q.weeklyPercent}%` : '—';
|
|
430
|
+
quotaLine =
|
|
431
|
+
locale === 'zh'
|
|
432
|
+
? `\n• **5h/7d 额度**: \`${p5h} | ${pw}\` (剩余百分比)`
|
|
433
|
+
: `\n• **5h/7d Quota**: \`${p5h} | ${pw}\` (remaining %)`;
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
quotaLine =
|
|
437
|
+
locale === 'zh'
|
|
438
|
+
? `\n• **5h/7d 额度**: \`未获取到\` (在 /auth 中可实时刷新)`
|
|
439
|
+
: `\n• **5h/7d Quota**: \`Not available\` (refresh in /auth)`;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return ((locale === 'zh'
|
|
443
|
+
? `• **当前账号**: \`${activeAccount?.email ?? activeAccount?.name ?? '未知'}\` (共 ${candidates.length} 个账号)`
|
|
444
|
+
: `• **Account**: \`${activeAccount?.email ?? activeAccount?.name ?? 'unknown'}\` (${candidates.length} candidates)`) +
|
|
445
|
+
quotaLine +
|
|
446
|
+
watchLine);
|
|
447
|
+
}
|
|
448
|
+
async renderCustomSetupRows(scopeId, _locale) {
|
|
449
|
+
void _locale;
|
|
450
|
+
const activeBackend = this.orchestrator.getBackendDescriptorForScope(scopeId);
|
|
451
|
+
if (activeBackend.engineType === 'codex') {
|
|
452
|
+
const codexHome = this.config.codexAuthDir ?? this.config.codexHome ?? path.join(os.homedir(), '.codex');
|
|
453
|
+
let currentCodexAccount = 'default';
|
|
454
|
+
try {
|
|
455
|
+
const authPath = path.join(codexHome, 'auth.json');
|
|
456
|
+
const stat = await fsPromises.lstat(authPath).catch(() => null);
|
|
457
|
+
if (stat?.isSymbolicLink()) {
|
|
458
|
+
const target = await fsPromises.readlink(authPath);
|
|
459
|
+
currentCodexAccount = path.basename(target).replace(/^auth\.json_/, '');
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
/* ignore */
|
|
464
|
+
}
|
|
465
|
+
return [
|
|
466
|
+
[
|
|
467
|
+
{ text: `👤 Codex 账号 (${currentCodexAccount})`, callback_data: 'codex:setup:auth' },
|
|
468
|
+
{ text: '📁 Codex 会话历史', callback_data: 'codex:setup:threads' },
|
|
469
|
+
],
|
|
470
|
+
];
|
|
471
|
+
}
|
|
472
|
+
if (activeBackend.engineType === 'opencode') {
|
|
473
|
+
return [
|
|
474
|
+
[
|
|
475
|
+
{ text: '📁 OpenCode 会话历史', callback_data: 'opencode:setup:threads' },
|
|
476
|
+
],
|
|
477
|
+
];
|
|
478
|
+
}
|
|
479
|
+
const candidates = await this.auth.listCandidates();
|
|
480
|
+
const active = candidates.find((c) => c.isActive) || candidates[0];
|
|
481
|
+
let quotaBadge = '';
|
|
482
|
+
if (active?.quota) {
|
|
483
|
+
const p5h = typeof active.quota.fiveHourPercent === 'number' ? `${active.quota.fiveHourPercent}%` : '—';
|
|
484
|
+
const pw = typeof active.quota.weeklyPercent === 'number' ? `${active.quota.weeklyPercent}%` : '—';
|
|
485
|
+
quotaBadge = ` · ${p5h}|${pw}`;
|
|
486
|
+
}
|
|
487
|
+
return [
|
|
488
|
+
[
|
|
489
|
+
{ text: `👤 AGY 账号 (${candidates.length}${quotaBadge})`, callback_data: `${AGY_SETUP_CALLBACK_PREFIX}auth` },
|
|
490
|
+
{ text: '📁 AGY 会话历史', callback_data: `${AGY_SETUP_CALLBACK_PREFIX}threads` },
|
|
491
|
+
],
|
|
492
|
+
];
|
|
493
|
+
}
|
|
494
|
+
async handleCustomInbound(event, locale) {
|
|
495
|
+
const scopeId = event.scopeId;
|
|
496
|
+
// Check for JSON token upload / document attachment
|
|
497
|
+
const doc = event.attachments?.find((a) => a.kind === 'document');
|
|
498
|
+
if (doc && (doc.fileName?.endsWith('.json') || doc.mimeType?.includes('json'))) {
|
|
499
|
+
try {
|
|
500
|
+
const remoteFile = await this.bot.getFile(doc.fileId);
|
|
501
|
+
if (remoteFile.file_path) {
|
|
502
|
+
const tempDest = path.join(os.tmpdir(), `agy_upload_${Date.now()}_${doc.fileName || 'token.json'}`);
|
|
503
|
+
await this.bot.downloadResolvedFile(remoteFile.file_path, tempDest);
|
|
504
|
+
const content = await fsPromises.readFile(tempDest, 'utf8');
|
|
505
|
+
await fsPromises.unlink(tempDest).catch(() => { });
|
|
506
|
+
if (content.includes('refresh_token')) {
|
|
507
|
+
const importRes = await this.auth.importAccountFromJson(content);
|
|
508
|
+
if (importRes.success && importRes.account) {
|
|
509
|
+
const candidates = await this.auth.listCandidates();
|
|
510
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
511
|
+
? `🎉 **Antigravity 账号导入成功!**\n\n` +
|
|
512
|
+
`• **账号名称**: \`${importRes.account.name}\`\n` +
|
|
513
|
+
`• **绑定邮箱**: \`${importRes.account.email || '未知'}\`\n` +
|
|
514
|
+
`• **鉴权状态**: ✅ Google OAuth 鉴权校验通过并已刷新 Token\n` +
|
|
515
|
+
`• **账号池现存**: 共 ${candidates.length} 个账号\n\n` +
|
|
516
|
+
`已自动加入候选池与自动保活轮转!`
|
|
517
|
+
: `🎉 **Antigravity Account Imported!**\n\n` +
|
|
518
|
+
`• **Name**: \`${importRes.account.name}\`\n` +
|
|
519
|
+
`• **Email**: \`${importRes.account.email || 'unknown'}\`\n` +
|
|
520
|
+
`• **Status**: ✅ Google OAuth validated & refreshed\n` +
|
|
521
|
+
`• **Pool Size**: ${candidates.length} accounts\n\n` +
|
|
522
|
+
`Joined keep-alive and rotation pool!`);
|
|
523
|
+
return true;
|
|
524
|
+
}
|
|
525
|
+
else if (importRes.error) {
|
|
526
|
+
await this.sendMessage(scopeId, `❌ 凭据导入失败: ${importRes.error}`);
|
|
527
|
+
return true;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
catch (err) {
|
|
533
|
+
this.logger.warn('antigravity.doc_import_error', { error: String(err) });
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
// Check for inline JSON paste containing refresh_token
|
|
537
|
+
const trimmed = event.text.trim();
|
|
538
|
+
if (trimmed.startsWith('{') && trimmed.includes('refresh_token')) {
|
|
539
|
+
try {
|
|
540
|
+
const importRes = await this.auth.importAccountFromJson(trimmed);
|
|
541
|
+
if (importRes.success && importRes.account) {
|
|
542
|
+
const candidates = await this.auth.listCandidates();
|
|
543
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
544
|
+
? `🎉 **Antigravity 账号导入成功!**\n\n` +
|
|
545
|
+
`• **账号名称**: \`${importRes.account.name}\`\n` +
|
|
546
|
+
`• **绑定邮箱**: \`${importRes.account.email || '未知'}\`\n` +
|
|
547
|
+
`• **鉴权状态**: ✅ Google OAuth 鉴权校验通过并已刷新 Token\n` +
|
|
548
|
+
`• **账号池现存**: 共 ${candidates.length} 个账号\n\n` +
|
|
549
|
+
`已自动加入候选池与自动保活轮转!`
|
|
550
|
+
: `🎉 **Antigravity Account Imported!**\n\n` +
|
|
551
|
+
`• **Name**: \`${importRes.account.name}\`\n` +
|
|
552
|
+
`• **Email**: \`${importRes.account.email || 'unknown'}\`\n` +
|
|
553
|
+
`• **Status**: ✅ Google OAuth validated & refreshed\n` +
|
|
554
|
+
`• **Pool Size**: ${candidates.length} accounts\n\n` +
|
|
555
|
+
`Joined keep-alive and rotation pool!`);
|
|
556
|
+
return true;
|
|
557
|
+
}
|
|
558
|
+
else if (importRes.error) {
|
|
559
|
+
await this.sendMessage(scopeId, `❌ 凭据导入失败: ${importRes.error}`);
|
|
560
|
+
return true;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
catch (err) {
|
|
564
|
+
this.logger.warn('antigravity.text_import_error', { error: String(err) });
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
// Check if there is an active pending browser login session awaiting authorization code
|
|
568
|
+
if (this.auth.hasPendingLogin(scopeId)) {
|
|
569
|
+
const loginRes = await this.auth.completeBrowserLogin(scopeId, event.text);
|
|
570
|
+
if (loginRes.success && loginRes.account) {
|
|
571
|
+
const candidates = await this.auth.listCandidates();
|
|
572
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
573
|
+
? `🎉 **Google 账号登录成功!**\n\n` +
|
|
574
|
+
`• **账号名称**: \`${loginRes.account.name}\`\n` +
|
|
575
|
+
`• **绑定邮箱**: \`${loginRes.account.email || '未知'}\`\n` +
|
|
576
|
+
`• **鉴权状态**: ✅ 授权码兑换成功,Token 已保存\n` +
|
|
577
|
+
`• **账号池现存**: 共 ${candidates.length} 个账号\n\n` +
|
|
578
|
+
`已成功加入候选池,并已纳入后台自动静默保活!`
|
|
579
|
+
: `🎉 **Google Account Logged In!**\n\n` +
|
|
580
|
+
`• **Account**: \`${loginRes.account.name}\`\n` +
|
|
581
|
+
`• **Email**: \`${loginRes.account.email || 'unknown'}\`\n` +
|
|
582
|
+
`• **Status**: ✅ Code exchanged successfully\n` +
|
|
583
|
+
`• **Pool Size**: ${candidates.length} accounts\n\n` +
|
|
584
|
+
`Joined candidate pool with automatic keep-alive!`);
|
|
585
|
+
return true;
|
|
586
|
+
}
|
|
587
|
+
else {
|
|
588
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
589
|
+
? `❌ **登录授权失败**: ${loginRes.error || '未能识别或兑换授权码'}\n\n请检查是否复制了完整授权码,或发送 \`/login\` 重新发起登录,发送 \`/auth cancel\` 可取消。`
|
|
590
|
+
: `❌ **Login Failed**: ${loginRes.error || 'Failed to exchange code'}\nPlease check the code or send /login again. Send /auth cancel to abort.`);
|
|
591
|
+
return true;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
if (this.codexCore?.hasPendingInteraction(scopeId)) {
|
|
595
|
+
this.codexCore.dispatchInboundLikeTelegramText(event);
|
|
596
|
+
return true;
|
|
597
|
+
}
|
|
598
|
+
const rename = this.pendingAgyRenames.get(scopeId);
|
|
599
|
+
if (rename && event.text.trim()) {
|
|
600
|
+
this.pendingAgyRenames.delete(scopeId);
|
|
601
|
+
const newTitle = event.text.trim();
|
|
602
|
+
this.conversations.renameConversation(rename.conversationId, newTitle);
|
|
603
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
604
|
+
? `✅ 会话名称已修改为: **${newTitle}**`
|
|
605
|
+
: `✅ Conversation renamed to: **${newTitle}**`);
|
|
606
|
+
await this.sendThreadsPanel(scopeId, '', locale);
|
|
607
|
+
return true;
|
|
608
|
+
}
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
611
|
+
async handleCustomCommand(scopeId, cmd, args, locale, _event) {
|
|
612
|
+
const activeBackend = this.orchestrator.getBackendDescriptorForScope(scopeId);
|
|
613
|
+
const isCodex = activeBackend.engineType === 'codex';
|
|
614
|
+
if (cmd.toLowerCase() === 'update') {
|
|
615
|
+
if (this.codexCore) {
|
|
616
|
+
if (_event) {
|
|
617
|
+
this.codexCore.dispatchInboundLikeTelegramText(_event);
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
this.codexCore.dispatchInboundLikeTelegramText({
|
|
621
|
+
scopeId,
|
|
622
|
+
text: '/update',
|
|
623
|
+
senderId: '0',
|
|
624
|
+
senderName: 'User',
|
|
625
|
+
timestamp: Date.now(),
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
return true;
|
|
629
|
+
}
|
|
630
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
631
|
+
? '⚠️ 当前运行环境下未配置自升级服务,请在终端执行 `foxclaw update`。'
|
|
632
|
+
: '⚠️ Self-updater is not available in current runtime. Run `foxclaw update` in terminal.');
|
|
633
|
+
return true;
|
|
634
|
+
}
|
|
635
|
+
if (isCodex) {
|
|
636
|
+
if (this.codexCore) {
|
|
637
|
+
switch (cmd.toLowerCase()) {
|
|
638
|
+
case 'threads':
|
|
639
|
+
if (_event) {
|
|
640
|
+
this.codexCore.dispatchInboundLikeTelegramText(_event);
|
|
641
|
+
}
|
|
642
|
+
else {
|
|
643
|
+
await this.codexCore.showThreadsPanel(scopeId, undefined, args.trim() || null, locale);
|
|
644
|
+
}
|
|
645
|
+
return true;
|
|
646
|
+
case 'open':
|
|
647
|
+
if (_event) {
|
|
648
|
+
this.codexCore.dispatchInboundLikeTelegramText(_event);
|
|
649
|
+
}
|
|
650
|
+
else if (!args.trim()) {
|
|
651
|
+
await this.codexCore.showThreadsPanel(scopeId, undefined, null, locale);
|
|
652
|
+
}
|
|
653
|
+
return true;
|
|
654
|
+
case 'auth':
|
|
655
|
+
if (_event) {
|
|
656
|
+
this.codexCore.dispatchInboundLikeTelegramText(_event);
|
|
657
|
+
}
|
|
658
|
+
else {
|
|
659
|
+
await this.codexCore.handleAuthCommand(scopeId, locale, args ? args.split(/\s+/) : []);
|
|
660
|
+
}
|
|
661
|
+
return true;
|
|
662
|
+
case 'login_device':
|
|
663
|
+
if (_event) {
|
|
664
|
+
this.codexCore.dispatchInboundLikeTelegramText(_event);
|
|
665
|
+
return true;
|
|
666
|
+
}
|
|
667
|
+
break;
|
|
668
|
+
case 'login':
|
|
669
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
670
|
+
? `ℹ️ **Codex 账号登录**\n\n发送 \`/auth\` 可在账号池中一键切换,或点击【🔑 设备登录】使用浏览器授权。`
|
|
671
|
+
: `ℹ️ Use /auth to manage accounts or use Device Login.`);
|
|
672
|
+
return true;
|
|
673
|
+
case 'watch':
|
|
674
|
+
case 'unwatch':
|
|
675
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
676
|
+
? `ℹ️ 会话实时观察模式当前仅在 Google Antigravity (AGY) 引擎下可用。可使用 \`/backend antigravity\` 切换。`
|
|
677
|
+
: `ℹ️ Observer mode is only available on Google Antigravity (AGY) engine.`);
|
|
678
|
+
return true;
|
|
679
|
+
default:
|
|
680
|
+
return false;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
switch (cmd.toLowerCase()) {
|
|
684
|
+
case 'threads':
|
|
685
|
+
await this.sendCodexThreadsMenu(scopeId, locale);
|
|
686
|
+
return true;
|
|
687
|
+
case 'open':
|
|
688
|
+
if (!args.trim()) {
|
|
689
|
+
await this.sendCodexThreadsMenu(scopeId, locale);
|
|
690
|
+
}
|
|
691
|
+
else {
|
|
692
|
+
await this.openCodexThread(scopeId, args.trim(), locale);
|
|
693
|
+
}
|
|
694
|
+
return true;
|
|
695
|
+
case 'auth':
|
|
696
|
+
await this.sendCodexAuthMenu(scopeId, locale);
|
|
697
|
+
return true;
|
|
698
|
+
case 'login':
|
|
699
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
700
|
+
? `ℹ️ **Codex 账号管理说明**\n\nCodex 使用 \`~/.codex/auth.json\` 凭据。\n发送 \`/auth\` 可在多个已有账号候选之间一键热切换。\n若需添加新账号,可在终端运行 \`codex auth login\` 或将 \`auth.json_<name>\` 放入 \`~/.codex/\` 目录。`
|
|
701
|
+
: `ℹ️ **Codex Auth Note**\n\nUse /auth to switch between accounts in ~/.codex/. To log in, run \`codex auth login\` in terminal.`);
|
|
702
|
+
return true;
|
|
703
|
+
case 'watch':
|
|
704
|
+
case 'unwatch':
|
|
705
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
706
|
+
? `ℹ️ 会话实时观察模式当前仅在 Google Antigravity (AGY) 引擎下可用。可使用 \`/backend antigravity\` 切换。`
|
|
707
|
+
: `ℹ️ Observer mode is only available on Google Antigravity (AGY) engine.`);
|
|
708
|
+
return true;
|
|
709
|
+
default:
|
|
710
|
+
return false;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
switch (cmd.toLowerCase()) {
|
|
714
|
+
case 'threads':
|
|
715
|
+
await this.sendThreadsPanel(scopeId, args.trim(), locale);
|
|
716
|
+
return true;
|
|
717
|
+
case 'open':
|
|
718
|
+
await this.openThread(scopeId, args.trim(), locale);
|
|
719
|
+
return true;
|
|
720
|
+
case 'watch':
|
|
721
|
+
await this.watchConversation(scopeId, args.trim(), locale);
|
|
722
|
+
return true;
|
|
723
|
+
case 'unwatch':
|
|
724
|
+
await this.unwatchConversation(scopeId, locale);
|
|
725
|
+
return true;
|
|
726
|
+
case 'login':
|
|
727
|
+
await this.startLoginFlow(scopeId, locale);
|
|
728
|
+
return true;
|
|
729
|
+
case 'auth':
|
|
730
|
+
if (args.trim() === 'login') {
|
|
731
|
+
await this.startLoginFlow(scopeId, locale);
|
|
732
|
+
}
|
|
733
|
+
else if (args.trim() === 'cancel' || args.trim() === 'cancel_login') {
|
|
734
|
+
this.auth.cancelBrowserLogin(scopeId);
|
|
735
|
+
await this.sendMessage(scopeId, locale === 'zh' ? '已取消登录会话。' : 'Login session canceled.');
|
|
736
|
+
}
|
|
737
|
+
else {
|
|
738
|
+
await this.sendAuthMenu(scopeId, args.trim(), locale);
|
|
739
|
+
}
|
|
740
|
+
return true;
|
|
741
|
+
default:
|
|
742
|
+
return false;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
async handleCustomCallback(scopeId, data, locale, messageId, event) {
|
|
746
|
+
if (this.codexCore && (data.startsWith('thread:') ||
|
|
747
|
+
data.startsWith('auth:') ||
|
|
748
|
+
data.startsWith('settings:access:') ||
|
|
749
|
+
data.startsWith('settings:permissions'))) {
|
|
750
|
+
if (event) {
|
|
751
|
+
await this.codexCore.handleCallback(event);
|
|
752
|
+
}
|
|
753
|
+
return true;
|
|
754
|
+
}
|
|
755
|
+
if (data.startsWith('codex:setup:auth')) {
|
|
756
|
+
await this.messaging.answerCallback(data, '');
|
|
757
|
+
if (this.codexCore) {
|
|
758
|
+
await this.codexCore.handleAuthCommand(scopeId, locale, []);
|
|
759
|
+
}
|
|
760
|
+
else {
|
|
761
|
+
await this.sendCodexAuthMenu(scopeId, locale, messageId);
|
|
762
|
+
}
|
|
763
|
+
return true;
|
|
764
|
+
}
|
|
765
|
+
if (data.startsWith('codex:setup:threads')) {
|
|
766
|
+
await this.messaging.answerCallback(data, '');
|
|
767
|
+
if (this.codexCore) {
|
|
768
|
+
await this.codexCore.showThreadsPanel(scopeId, messageId, undefined, locale);
|
|
769
|
+
}
|
|
770
|
+
else {
|
|
771
|
+
await this.sendCodexThreadsMenu(scopeId, locale, messageId);
|
|
772
|
+
}
|
|
773
|
+
return true;
|
|
774
|
+
}
|
|
775
|
+
if (data.startsWith('agy:rename:')) {
|
|
776
|
+
const convId = data.slice('agy:rename:'.length);
|
|
777
|
+
this.pendingAgyRenames.set(scopeId, { conversationId: convId });
|
|
778
|
+
await this.messaging.answerCallback(data, '请输入新名称');
|
|
779
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
780
|
+
? `✏️ 请直接回复发送该会话的新名称:`
|
|
781
|
+
: `✏️ Please reply with the new conversation name:`);
|
|
782
|
+
return true;
|
|
783
|
+
}
|
|
784
|
+
if (data.startsWith('agy:archive:')) {
|
|
785
|
+
const convId = data.slice('agy:archive:'.length);
|
|
786
|
+
this.conversations.archiveConversation(convId);
|
|
787
|
+
await this.messaging.answerCallback(data, locale === 'zh' ? '已归档' : 'Archived');
|
|
788
|
+
await this.sendThreadsPanel(scopeId, '', locale, messageId);
|
|
789
|
+
return true;
|
|
790
|
+
}
|
|
791
|
+
if (data.startsWith('agy:new:')) {
|
|
792
|
+
const convId = data.slice('agy:new:'.length);
|
|
793
|
+
const conv = this.conversations.getConversation(convId);
|
|
794
|
+
const cwd = conv?.workspaceDir || this.config.defaultCwd;
|
|
795
|
+
this.store.setBinding(scopeId, '', cwd);
|
|
796
|
+
this.orchestrator.syncCurrentBackendSettings(scopeId);
|
|
797
|
+
await this.messaging.answerCallback(data, locale === 'zh' ? '已创建新会话' : 'New conversation ready');
|
|
798
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
799
|
+
? `✨ **已在目录创建新会话**\n• 工作目录: \`${cwd}\`\n\n发送任意消息开启新任务。`
|
|
800
|
+
: `✨ **New conversation ready** in \`${cwd}\`.`);
|
|
801
|
+
return true;
|
|
802
|
+
}
|
|
803
|
+
if (data.startsWith('codex:auth:')) {
|
|
804
|
+
const cand = data.slice('codex:auth:'.length);
|
|
805
|
+
await this.switchCodexAccount(scopeId, cand, locale, messageId);
|
|
806
|
+
return true;
|
|
807
|
+
}
|
|
808
|
+
if (data.startsWith('codex:open:')) {
|
|
809
|
+
const threadId = data.slice('codex:open:'.length);
|
|
810
|
+
await this.openCodexThread(scopeId, threadId, locale, messageId);
|
|
811
|
+
return true;
|
|
812
|
+
}
|
|
813
|
+
if (data.startsWith(AGY_OPEN_CALLBACK_PREFIX)) {
|
|
814
|
+
const targetId = data.slice(AGY_OPEN_CALLBACK_PREFIX.length);
|
|
815
|
+
const conv = this.conversations.resolveConversation(targetId, scopeId, this.store);
|
|
816
|
+
if (conv) {
|
|
817
|
+
this.store.setBinding(scopeId, conv.conversationId, conv.workspaceDir || this.config.defaultCwd);
|
|
818
|
+
await this.messaging.answerCallback(data, `已切换到: ${conv.title}`);
|
|
819
|
+
await this.openThread(scopeId, conv.conversationId, locale, messageId);
|
|
820
|
+
}
|
|
821
|
+
else {
|
|
822
|
+
await this.messaging.answerCallback(data, '未找到会话');
|
|
823
|
+
}
|
|
824
|
+
return true;
|
|
825
|
+
}
|
|
826
|
+
if (data.startsWith(AGY_WATCH_CALLBACK_PREFIX)) {
|
|
827
|
+
const targetId = data.slice(AGY_WATCH_CALLBACK_PREFIX.length);
|
|
828
|
+
if (targetId === 'stop') {
|
|
829
|
+
await this.messaging.answerCallback(data, '已停止观察');
|
|
830
|
+
await this.unwatchConversation(scopeId, locale, true);
|
|
831
|
+
return true;
|
|
832
|
+
}
|
|
833
|
+
const existing = this.watchers.get(scopeId);
|
|
834
|
+
if (existing && !existing.stopped && existing.conversationId === targetId) {
|
|
835
|
+
await this.messaging.answerCallback(data, '已停止观察');
|
|
836
|
+
await this.unwatchConversation(scopeId, locale, true);
|
|
837
|
+
return true;
|
|
838
|
+
}
|
|
839
|
+
await this.messaging.answerCallback(data, '进入观察模式');
|
|
840
|
+
await this.watchConversation(scopeId, targetId, locale, messageId);
|
|
841
|
+
return true;
|
|
842
|
+
}
|
|
843
|
+
if (data.startsWith(AGY_SETUP_CALLBACK_PREFIX)) {
|
|
844
|
+
const sub = data.slice(AGY_SETUP_CALLBACK_PREFIX.length);
|
|
845
|
+
switch (sub) {
|
|
846
|
+
case 'auth':
|
|
847
|
+
await this.sendAuthMenu(scopeId, '', locale, messageId);
|
|
848
|
+
return true;
|
|
849
|
+
case 'threads':
|
|
850
|
+
await this.sendThreadsPanel(scopeId, '', locale, messageId);
|
|
851
|
+
return true;
|
|
852
|
+
case 'effort':
|
|
853
|
+
await this.sendEffortMenu(scopeId, '', locale, messageId);
|
|
854
|
+
return true;
|
|
855
|
+
default:
|
|
856
|
+
return false;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
if (data.startsWith(AGY_MODEL_CALLBACK_PREFIX)) {
|
|
860
|
+
const model = data.slice(AGY_MODEL_CALLBACK_PREFIX.length);
|
|
861
|
+
this.store.setChatSettings(scopeId, model, null);
|
|
862
|
+
this.orchestrator.syncCurrentBackendSettings(scopeId);
|
|
863
|
+
await this.messaging.answerCallback(data, `Model: ${model}`);
|
|
864
|
+
await this.orchestrator.sendModelsMenu(scopeId, locale, messageId);
|
|
865
|
+
return true;
|
|
866
|
+
}
|
|
867
|
+
if (data.startsWith(AGY_EFFORT_CALLBACK_PREFIX)) {
|
|
868
|
+
const effort = data.slice(AGY_EFFORT_CALLBACK_PREFIX.length);
|
|
869
|
+
const settings = this.store.getChatSettings(scopeId);
|
|
870
|
+
if (effort !== 'high' && settings?.serviceTier === 'boost') {
|
|
871
|
+
this.store.setChatServiceTier(scopeId, null);
|
|
872
|
+
}
|
|
873
|
+
this.store.setChatSettings(scopeId, null, effort);
|
|
874
|
+
this.orchestrator.syncCurrentBackendSettings(scopeId);
|
|
875
|
+
await this.messaging.answerCallback(data, `Effort: ${effort}`);
|
|
876
|
+
await this.sendEffortMenu(scopeId, '', locale, messageId);
|
|
877
|
+
return true;
|
|
878
|
+
}
|
|
879
|
+
if (data.startsWith(AGY_AUTH_CALLBACK_PREFIX)) {
|
|
880
|
+
const accountName = data.slice(AGY_AUTH_CALLBACK_PREFIX.length);
|
|
881
|
+
if (accountName.startsWith('filter:')) {
|
|
882
|
+
await this.sendAuthMenu(scopeId, accountName, locale, messageId);
|
|
883
|
+
return true;
|
|
884
|
+
}
|
|
885
|
+
if (accountName === 'login') {
|
|
886
|
+
await this.startLoginFlow(scopeId, locale, messageId);
|
|
887
|
+
return true;
|
|
888
|
+
}
|
|
889
|
+
if (accountName === 'cancel_login') {
|
|
890
|
+
this.auth.cancelBrowserLogin(scopeId);
|
|
891
|
+
await this.messaging.answerCallback(data, '已取消登录');
|
|
892
|
+
await this.sendAuthMenu(scopeId, '', locale, messageId);
|
|
893
|
+
return true;
|
|
894
|
+
}
|
|
895
|
+
if (accountName === 'rotate') {
|
|
896
|
+
try {
|
|
897
|
+
const res = await this.auth.rotateNextCandidate();
|
|
898
|
+
await this.messaging.answerCallback(data, `已轮转到: ${res.account.email || res.account.name}`);
|
|
899
|
+
await this.sendAuthMenu(scopeId, '', locale, messageId);
|
|
900
|
+
}
|
|
901
|
+
catch (err) {
|
|
902
|
+
await this.messaging.answerCallback(data, `轮转失败: ${String(err)}`);
|
|
903
|
+
}
|
|
904
|
+
return true;
|
|
905
|
+
}
|
|
906
|
+
if (accountName === 'refresh_current') {
|
|
907
|
+
const active = await this.auth.getActiveAccount();
|
|
908
|
+
if (!active) {
|
|
909
|
+
await this.messaging.answerCallback(data, '无活跃账号');
|
|
910
|
+
return true;
|
|
911
|
+
}
|
|
912
|
+
const res = await this.auth.refreshTokenForAccount(active.name);
|
|
913
|
+
await this.messaging.answerCallback(data, res.success ? '⚡ 当前 Token 刷新成功!' : `刷新失败: ${res.error}`);
|
|
914
|
+
await this.sendAuthMenu(scopeId, '', locale, messageId);
|
|
915
|
+
return true;
|
|
916
|
+
}
|
|
917
|
+
if (accountName === 'refresh_all') {
|
|
918
|
+
const res = await this.auth.refreshAllTokens();
|
|
919
|
+
await this.messaging.answerCallback(data, `⚡ 刷新完成: ${res.refreshed}/${res.total} 成功`);
|
|
920
|
+
await this.sendAuthMenu(scopeId, '', locale, messageId);
|
|
921
|
+
return true;
|
|
922
|
+
}
|
|
923
|
+
if (accountName === 'import_help') {
|
|
924
|
+
await this.messaging.answerCallback(data, '查看导入指南');
|
|
925
|
+
const guideText = `📥 **Antigravity 账号导入指南**\n\n` +
|
|
926
|
+
`你可以通过以下任意方式向机器人导入新的 Google / Antigravity 账号:\n\n` +
|
|
927
|
+
`1️⃣ **直接发送 Token 文件**:\n` +
|
|
928
|
+
` 将包含 OAuth 凭据的 \`.json\` 文件(例如从其他机器复制的 \`antigravity-oauth-token\`)作为文档直接发送给机器人。\n\n` +
|
|
929
|
+
`2️⃣ **粘贴 Token JSON**:\n` +
|
|
930
|
+
` 直接在会话中发送包含 \`refresh_token\` 的 JSON 文本。\n\n` +
|
|
931
|
+
`3️⃣ **使用命令**:\n` +
|
|
932
|
+
` 发送 \`/auth add <json_content>\` 即可导入。\n\n` +
|
|
933
|
+
`机器人会自动校验并向 Google API 请求刷新测试,校验成功后立即加入候选池与自动保活轮转!`;
|
|
934
|
+
await this.sendMessage(scopeId, guideText);
|
|
935
|
+
return true;
|
|
936
|
+
}
|
|
937
|
+
if (accountName.startsWith('toggle_pause:')) {
|
|
938
|
+
const target = accountName.slice('toggle_pause:'.length);
|
|
939
|
+
const nowPaused = this.auth.togglePauseAccount(target);
|
|
940
|
+
await this.messaging.answerCallback(data, nowPaused ? `⏸ 账号已暂停: ${target}` : `▶️ 账号已恢复启用: ${target}`);
|
|
941
|
+
await this.sendAuthMenu(scopeId, '', locale, messageId);
|
|
942
|
+
return true;
|
|
943
|
+
}
|
|
944
|
+
if (accountName.startsWith('repair:')) {
|
|
945
|
+
const target = accountName.slice('repair:'.length);
|
|
946
|
+
const res = await this.auth.diagnoseAndRepairAccount(target);
|
|
947
|
+
await this.messaging.answerCallback(data, res.ok ? `🩺 修复成功: ${res.message}` : `❌ 修复失败: ${res.message}`);
|
|
948
|
+
await this.sendAuthMenu(scopeId, '', locale, messageId);
|
|
949
|
+
return true;
|
|
950
|
+
}
|
|
951
|
+
if (accountName === 'repair_all') {
|
|
952
|
+
const res = await this.auth.diagnoseAndRepairAll();
|
|
953
|
+
await this.messaging.answerCallback(data, `🩺 体检完成: 正常/已修复 ${res.healthy + res.repaired} / 需关注 ${res.failed}`);
|
|
954
|
+
await this.sendAuthMenu(scopeId, '', locale, messageId);
|
|
955
|
+
return true;
|
|
956
|
+
}
|
|
957
|
+
if (accountName.startsWith('switch:')) {
|
|
958
|
+
const target = accountName.slice('switch:'.length);
|
|
959
|
+
try {
|
|
960
|
+
const res = await this.auth.switchAccount(target);
|
|
961
|
+
await this.messaging.answerCallback(data, `已切换到: ${res.account.email || res.account.name}`);
|
|
962
|
+
}
|
|
963
|
+
catch (err) {
|
|
964
|
+
await this.messaging.answerCallback(data, `切换失败: ${String(err)}`);
|
|
965
|
+
}
|
|
966
|
+
await this.sendAuthMenu(scopeId, '', locale, messageId);
|
|
967
|
+
return true;
|
|
968
|
+
}
|
|
969
|
+
try {
|
|
970
|
+
const res = await this.auth.switchAccount(accountName);
|
|
971
|
+
await this.messaging.answerCallback(data, `已切换到: ${res.account.email || res.account.name}`);
|
|
972
|
+
await this.sendAuthMenu(scopeId, '', locale, messageId);
|
|
973
|
+
}
|
|
974
|
+
catch (err) {
|
|
975
|
+
await this.messaging.answerCallback(data, `Failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
976
|
+
}
|
|
977
|
+
return true;
|
|
978
|
+
}
|
|
979
|
+
return false;
|
|
980
|
+
}
|
|
981
|
+
async sendThreadsPanel(scopeId, search, locale, editMessageId) {
|
|
982
|
+
const boundThreadId = this.store.getBinding(scopeId)?.threadId;
|
|
983
|
+
const list = this.conversations.listConversations(10, search);
|
|
984
|
+
if (list.length === 0) {
|
|
985
|
+
const emptyText = locale === 'zh'
|
|
986
|
+
? `📁 **Antigravity 会话列表**\n\n暂无历史会话。发送任意消息或执行 \`/new\` 即可创建。`
|
|
987
|
+
: `📁 **Antigravity Conversations**\n\nNo history found. Send a message or run \`/new\` to create one.`;
|
|
988
|
+
const emptyKb = [
|
|
989
|
+
[{ text: '✨ 新建会话', callback_data: 'engine:setup:new' }],
|
|
990
|
+
[{ text: '◀️ 返回控制面板', callback_data: 'engine:setup:main' }],
|
|
991
|
+
];
|
|
992
|
+
if (editMessageId) {
|
|
993
|
+
await this.editMessage(scopeId, editMessageId, emptyText, emptyKb);
|
|
994
|
+
}
|
|
995
|
+
else {
|
|
996
|
+
await this.sendMessage(scopeId, emptyText, emptyKb);
|
|
997
|
+
}
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
// Cache threads in database so `/open <idx>` works
|
|
1001
|
+
this.store.cacheThreadList(scopeId, list.map((c, i) => ({
|
|
1002
|
+
listIndex: i + 1,
|
|
1003
|
+
threadId: c.conversationId,
|
|
1004
|
+
name: c.title,
|
|
1005
|
+
preview: c.preview,
|
|
1006
|
+
cwd: c.workspaceDir ?? this.config.defaultCwd,
|
|
1007
|
+
modelProvider: 'antigravity',
|
|
1008
|
+
status: 'idle',
|
|
1009
|
+
updatedAt: c.updatedAt,
|
|
1010
|
+
})));
|
|
1011
|
+
const currentWatcher = this.watchers.get(scopeId);
|
|
1012
|
+
const watchingThreadId = currentWatcher && !currentWatcher.stopped ? currentWatcher.conversationId : null;
|
|
1013
|
+
const lines = [
|
|
1014
|
+
locale === 'zh' ? `📁 **Antigravity 会话列表**:` : `📁 **Antigravity Conversations**:`,
|
|
1015
|
+
'',
|
|
1016
|
+
];
|
|
1017
|
+
const keyboard = [];
|
|
1018
|
+
list.forEach((session, idx) => {
|
|
1019
|
+
const isCurrent = session.conversationId === boundThreadId;
|
|
1020
|
+
const isWatching = session.conversationId === watchingThreadId;
|
|
1021
|
+
const marker = isCurrent ? '●' : '○';
|
|
1022
|
+
const short = session.conversationId.slice(0, 8);
|
|
1023
|
+
const dir = session.workspaceDir ?? this.config.defaultCwd;
|
|
1024
|
+
const age = formatAge(session.updatedAt, locale);
|
|
1025
|
+
const watchBadge = isWatching ? ' [👁 观察中]' : '';
|
|
1026
|
+
lines.push(`${marker} **${idx + 1}.** ${session.title}${watchBadge}`);
|
|
1027
|
+
lines.push(` \`${short}\` · \`${dir}\` · ${age}`);
|
|
1028
|
+
if (idx < 8) {
|
|
1029
|
+
const dirName = path.basename(dir || this.config.defaultCwd);
|
|
1030
|
+
const titleSnippet = session.title ? session.title.replace(/\s+/g, ' ') : 'Untitled';
|
|
1031
|
+
const openText = `${isCurrent ? '✅ ' : ''}${idx + 1}. ${dirName}|${titleSnippet.length > 32 ? titleSnippet.slice(0, 31) + '…' : titleSnippet}`;
|
|
1032
|
+
keyboard.push([
|
|
1033
|
+
{
|
|
1034
|
+
text: openText,
|
|
1035
|
+
callback_data: `${AGY_OPEN_CALLBACK_PREFIX}${session.conversationId}`,
|
|
1036
|
+
},
|
|
1037
|
+
]);
|
|
1038
|
+
keyboard.push([
|
|
1039
|
+
{ text: '✏️', callback_data: `agy:rename:${session.conversationId}` },
|
|
1040
|
+
{ text: isWatching ? '👁 监视' : '👀', callback_data: `${AGY_WATCH_CALLBACK_PREFIX}${session.conversationId}` },
|
|
1041
|
+
{ text: '🗑️', callback_data: `agy:archive:${session.conversationId}` },
|
|
1042
|
+
{ text: '➕', callback_data: `agy:new:${session.conversationId}` },
|
|
1043
|
+
]);
|
|
1044
|
+
}
|
|
1045
|
+
});
|
|
1046
|
+
if (watchingThreadId) {
|
|
1047
|
+
keyboard.push([
|
|
1048
|
+
{
|
|
1049
|
+
text: locale === 'zh' ? '🛑 停止观察当前会话 (/unwatch)' : '🛑 Stop watching (/unwatch)',
|
|
1050
|
+
callback_data: `${AGY_WATCH_CALLBACK_PREFIX}stop`,
|
|
1051
|
+
},
|
|
1052
|
+
]);
|
|
1053
|
+
}
|
|
1054
|
+
lines.push('', locale === 'zh'
|
|
1055
|
+
? `• 点击名称切换会话;\n• 点击快捷按钮:\`[✏️ 重命名]\` \`[👀 观察]\` \`[🗑️ 归档]\` \`[➕ 新建分支]\`;\n• 命令行:\`/open <编号>\`,\`/watch <编号>\`,\`/unwatch\`,\`/new\`。`
|
|
1056
|
+
: `• Tap name to bind conversation;\n• Tap actions: \`[✏️ Rename]\` \`[👀 Watch]\` \`[🗑️ Archive]\` \`[➕ Fork]\`;\n• Commands: \`/open <num>\`, \`/watch <num>\`, \`/unwatch\`, \`/new\`.`);
|
|
1057
|
+
keyboard.push([
|
|
1058
|
+
{ text: '➕ 新建', callback_data: 'engine:setup:new' },
|
|
1059
|
+
{ text: '◀️ 返回控制面板', callback_data: 'engine:setup:main' },
|
|
1060
|
+
]);
|
|
1061
|
+
const content = lines.join('\n');
|
|
1062
|
+
if (editMessageId) {
|
|
1063
|
+
await this.editMessage(scopeId, editMessageId, content, keyboard);
|
|
1064
|
+
this.scheduleStalePanelDeletion(scopeId, editMessageId);
|
|
1065
|
+
}
|
|
1066
|
+
else {
|
|
1067
|
+
const msgId = await this.sendMessage(scopeId, content, keyboard);
|
|
1068
|
+
this.scheduleStalePanelDeletion(scopeId, msgId);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
async openThread(scopeId, rawTarget, locale, editMessageId) {
|
|
1072
|
+
if (!rawTarget) {
|
|
1073
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
1074
|
+
? '用法:`/open <编号|会话ID>`。例如 `/open 1`。可使用 `/threads` 查看有效编号。'
|
|
1075
|
+
: 'Usage: `/open <number|conversation-id>`. Use `/threads` to list.');
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
const conv = this.conversations.resolveConversation(rawTarget, scopeId, this.store);
|
|
1079
|
+
if (!conv) {
|
|
1080
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
1081
|
+
? `❌ 未找到会话 \`${rawTarget}\`。请使用 \`/threads\` 查看会话列表。`
|
|
1082
|
+
: `❌ Conversation \`${rawTarget}\` not found. Use \`/threads\` to list.`);
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
const targetCwd = conv.workspaceDir || this.config.defaultCwd;
|
|
1086
|
+
this.store.setBinding(scopeId, conv.conversationId, targetCwd);
|
|
1087
|
+
const currentWatcher = this.watchers.get(scopeId);
|
|
1088
|
+
let watchNoteZh = '';
|
|
1089
|
+
let watchNoteEn = '';
|
|
1090
|
+
if (currentWatcher && !currentWatcher.stopped) {
|
|
1091
|
+
watchNoteZh = `\n• **当前观察**: \`${currentWatcher.conversationId.slice(0, 8)}\``;
|
|
1092
|
+
watchNoteEn = `\n• **Watching**: \`${currentWatcher.conversationId.slice(0, 8)}\``;
|
|
1093
|
+
}
|
|
1094
|
+
const text = locale === 'zh'
|
|
1095
|
+
? `✅ **已切换绑定到既有 Antigravity 会话**\n\n` +
|
|
1096
|
+
`• **标题**: ${conv.title}\n` +
|
|
1097
|
+
`• **会话 ID**: \`${conv.conversationId}\`\n` +
|
|
1098
|
+
`• **工作目录**: \`${targetCwd}\`\n` +
|
|
1099
|
+
`• **更新时间**: ${formatAge(conv.updatedAt, 'zh')}` +
|
|
1100
|
+
watchNoteZh +
|
|
1101
|
+
`\n\n后续消息将在此会话继续执行。`
|
|
1102
|
+
: `✅ **Switched to Antigravity Conversation**\n\n` +
|
|
1103
|
+
`• **Title**: ${conv.title}\n` +
|
|
1104
|
+
`• **ID**: \`${conv.conversationId}\`\n` +
|
|
1105
|
+
`• **Directory**: \`${targetCwd}\`\n` +
|
|
1106
|
+
`• **Updated**: ${formatAge(conv.updatedAt, 'en')}` +
|
|
1107
|
+
watchNoteEn +
|
|
1108
|
+
`\n\nNext messages will continue in this conversation.`;
|
|
1109
|
+
const keyboard = [
|
|
1110
|
+
[{ text: '👁 开启实时观察 (/watch)', callback_data: `${AGY_WATCH_CALLBACK_PREFIX}${conv.conversationId}` }],
|
|
1111
|
+
[
|
|
1112
|
+
{ text: '📁 查看其他会话', callback_data: `${AGY_SETUP_CALLBACK_PREFIX}threads` },
|
|
1113
|
+
{ text: '⚙️ 控制面板', callback_data: 'engine:setup:main' },
|
|
1114
|
+
],
|
|
1115
|
+
];
|
|
1116
|
+
if (editMessageId) {
|
|
1117
|
+
await this.editMessage(scopeId, editMessageId, text, keyboard);
|
|
1118
|
+
}
|
|
1119
|
+
else {
|
|
1120
|
+
await this.sendMessage(scopeId, text, keyboard);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
async sendCodexAuthMenu(scopeId, locale, editMessageId) {
|
|
1124
|
+
const codexHome = this.config.codexAuthDir ?? this.config.codexHome ?? path.join(os.homedir(), '.codex');
|
|
1125
|
+
let currentAccount = 'default';
|
|
1126
|
+
let candidates = [];
|
|
1127
|
+
try {
|
|
1128
|
+
const files = await fsPromises.readdir(codexHome);
|
|
1129
|
+
candidates = files
|
|
1130
|
+
.filter((f) => f.startsWith('auth.json_') && !f.endsWith('.bak') && !f.endsWith('.tmp'))
|
|
1131
|
+
.map((f) => f.replace(/^auth\.json_/, ''))
|
|
1132
|
+
.sort();
|
|
1133
|
+
const authPath = path.join(codexHome, 'auth.json');
|
|
1134
|
+
const stat = await fsPromises.lstat(authPath).catch(() => null);
|
|
1135
|
+
if (stat?.isSymbolicLink()) {
|
|
1136
|
+
const target = await fsPromises.readlink(authPath);
|
|
1137
|
+
currentAccount = path.basename(target).replace(/^auth\.json_/, '');
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
catch (err) {
|
|
1141
|
+
this.logger.warn('codex.list_auth_failed', { error: String(err) });
|
|
1142
|
+
}
|
|
1143
|
+
const text = locale === 'zh'
|
|
1144
|
+
? `👤 **OpenAI Codex 账号管理 (Account Pool)**\n\n` +
|
|
1145
|
+
`• **当前活跃账号**: ● **${currentAccount}**\n` +
|
|
1146
|
+
`• **账号候选池**: 共 ${candidates.length} 个账号\n` +
|
|
1147
|
+
`• **凭据路径**: \`${path.join(codexHome, 'auth.json')}\`\n\n` +
|
|
1148
|
+
`点击下方账号名称可直接热切换 Codex 登录凭据:`
|
|
1149
|
+
: `👤 **OpenAI Codex Account Management**\n\n` +
|
|
1150
|
+
`• **Active Account**: ● **${currentAccount}**\n` +
|
|
1151
|
+
`• **Candidates**: ${candidates.length} accounts\n` +
|
|
1152
|
+
`• **Path**: \`${path.join(codexHome, 'auth.json')}\`\n\n` +
|
|
1153
|
+
`Tap an account below to switch:`;
|
|
1154
|
+
const keyboard = [];
|
|
1155
|
+
for (let i = 0; i < candidates.length; i += 2) {
|
|
1156
|
+
const row = [];
|
|
1157
|
+
const c1 = candidates[i];
|
|
1158
|
+
const isC1 = c1 === currentAccount;
|
|
1159
|
+
row.push({
|
|
1160
|
+
text: `${isC1 ? '● ' : '○ '}${c1}`,
|
|
1161
|
+
callback_data: `codex:auth:${c1}`,
|
|
1162
|
+
});
|
|
1163
|
+
if (i + 1 < candidates.length) {
|
|
1164
|
+
const c2 = candidates[i + 1];
|
|
1165
|
+
const isC2 = c2 === currentAccount;
|
|
1166
|
+
row.push({
|
|
1167
|
+
text: `${isC2 ? '● ' : '○ '}${c2}`,
|
|
1168
|
+
callback_data: `codex:auth:${c2}`,
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
keyboard.push(row);
|
|
1172
|
+
}
|
|
1173
|
+
keyboard.push([
|
|
1174
|
+
{ text: '◀️ 返回控制面板', callback_data: 'engine:setup:main' },
|
|
1175
|
+
{ text: '🔄 刷新列表', callback_data: 'codex:setup:auth' },
|
|
1176
|
+
]);
|
|
1177
|
+
if (editMessageId) {
|
|
1178
|
+
await this.editMessage(scopeId, editMessageId, text, keyboard);
|
|
1179
|
+
this.scheduleStalePanelDeletion(scopeId, editMessageId);
|
|
1180
|
+
}
|
|
1181
|
+
else {
|
|
1182
|
+
const msgId = await this.sendMessage(scopeId, text, keyboard);
|
|
1183
|
+
this.scheduleStalePanelDeletion(scopeId, msgId);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
async switchCodexAccount(scopeId, candName, locale, messageId) {
|
|
1187
|
+
const codexHome = this.config.codexAuthDir ?? this.config.codexHome ?? path.join(os.homedir(), '.codex');
|
|
1188
|
+
const authPath = path.join(codexHome, 'auth.json');
|
|
1189
|
+
const targetPath = path.join(codexHome, `auth.json_${candName}`);
|
|
1190
|
+
try {
|
|
1191
|
+
await fsPromises.unlink(authPath).catch(() => { });
|
|
1192
|
+
await fsPromises.symlink(targetPath, authPath);
|
|
1193
|
+
this.logger.info('codex.auth_switched', { candidate: candName });
|
|
1194
|
+
await this.messaging.answerCallback(`codex:auth:${candName}`, locale === 'zh' ? `已切换至 Codex 账号: ${candName}` : `Switched to: ${candName}`);
|
|
1195
|
+
if (messageId) {
|
|
1196
|
+
await this.sendCodexAuthMenu(scopeId, locale, messageId);
|
|
1197
|
+
}
|
|
1198
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
1199
|
+
? `🔄 **Codex 账号已热切换至: \`${candName}\`**`
|
|
1200
|
+
: `🔄 **Codex account switched to: \`${candName}\`**`);
|
|
1201
|
+
}
|
|
1202
|
+
catch (err) {
|
|
1203
|
+
this.logger.error('codex.auth_switch_error', { candidate: candName, error: String(err) });
|
|
1204
|
+
await this.messaging.answerCallback(`codex:auth:${candName}`, `切换失败: ${String(err)}`);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
async sendCodexThreadsMenu(scopeId, locale, editMessageId) {
|
|
1208
|
+
const codexHome = this.config.codexAuthDir ?? this.config.codexHome ?? path.join(os.homedir(), '.codex');
|
|
1209
|
+
const currentBinding = this.store.getBinding(scopeId);
|
|
1210
|
+
const activeThreadId = currentBinding?.threadId;
|
|
1211
|
+
const items = [];
|
|
1212
|
+
try {
|
|
1213
|
+
const indexPath = path.join(codexHome, 'session_index.jsonl');
|
|
1214
|
+
const content = await fsPromises.readFile(indexPath, 'utf8').catch(() => '');
|
|
1215
|
+
const lines = content.trim().split('\n').filter(Boolean);
|
|
1216
|
+
const seenIds = new Set();
|
|
1217
|
+
for (let i = lines.length - 1; i >= 0 && items.length < 8; i--) {
|
|
1218
|
+
try {
|
|
1219
|
+
const parsed = JSON.parse(lines[i]);
|
|
1220
|
+
if (parsed.id && !seenIds.has(parsed.id)) {
|
|
1221
|
+
seenIds.add(parsed.id);
|
|
1222
|
+
items.push({
|
|
1223
|
+
id: parsed.id,
|
|
1224
|
+
title: parsed.thread_name || parsed.id.slice(0, 16),
|
|
1225
|
+
updatedAt: parsed.updated_at,
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
catch {
|
|
1230
|
+
/* ignore */
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
catch (err) {
|
|
1235
|
+
this.logger.warn('codex.list_threads_failed', { error: String(err) });
|
|
1236
|
+
}
|
|
1237
|
+
const listLines = items.map((item, idx) => {
|
|
1238
|
+
const num = idx + 1;
|
|
1239
|
+
const isActive = item.id === activeThreadId;
|
|
1240
|
+
return `${isActive ? '●' : '○'} **${num}.** ${item.title}${isActive ? ' `[当前会话]`' : ''}\n \`${item.id.slice(0, 16)}…\``;
|
|
1241
|
+
});
|
|
1242
|
+
const text = locale === 'zh'
|
|
1243
|
+
? `📁 **OpenAI Codex 会话历史**\n\n` +
|
|
1244
|
+
`• **当前会话**: ${activeThreadId ? `\`${activeThreadId.slice(0, 16)}…\`` : '`新会话就绪 (发送消息开启)`'}\n` +
|
|
1245
|
+
`• **工作目录**: \`${currentBinding?.cwd || this.config.defaultCwd}\`\n\n` +
|
|
1246
|
+
(items.length > 0
|
|
1247
|
+
? `**最近会话列表**:\n${listLines.join('\n')}\n\n点击下方按钮恢复会话,或使用 \`/open <编号>\`:`
|
|
1248
|
+
: `暂无历史会话记录。发送消息将开启新会话。`)
|
|
1249
|
+
: `📁 **OpenAI Codex Session History**\n\n` +
|
|
1250
|
+
`• **Current Thread**: ${activeThreadId ? `\`${activeThreadId.slice(0, 16)}…\`` : '`Ready (new)`'}\n` +
|
|
1251
|
+
`• **Directory**: \`${currentBinding?.cwd || this.config.defaultCwd}\`\n\n` +
|
|
1252
|
+
(items.length > 0 ? `${listLines.join('\n')}\n\nTap below to resume or use \`/open <num>\`:` : 'No sessions found.');
|
|
1253
|
+
const keyboard = [];
|
|
1254
|
+
for (let i = 0; i < items.length; i += 2) {
|
|
1255
|
+
const row = [];
|
|
1256
|
+
const item1 = items[i];
|
|
1257
|
+
const isA1 = item1.id === activeThreadId;
|
|
1258
|
+
const shortTitle1 = item1.title.length > 14 ? item1.title.slice(0, 13) + '…' : item1.title;
|
|
1259
|
+
row.push({
|
|
1260
|
+
text: `${isA1 ? '● ' : ''}${i + 1}. ${shortTitle1}`,
|
|
1261
|
+
callback_data: `codex:open:${item1.id}`,
|
|
1262
|
+
});
|
|
1263
|
+
if (i + 1 < items.length) {
|
|
1264
|
+
const item2 = items[i + 1];
|
|
1265
|
+
const isA2 = item2.id === activeThreadId;
|
|
1266
|
+
const shortTitle2 = item2.title.length > 14 ? item2.title.slice(0, 13) + '…' : item2.title;
|
|
1267
|
+
row.push({
|
|
1268
|
+
text: `${isA2 ? '● ' : ''}${i + 2}. ${shortTitle2}`,
|
|
1269
|
+
callback_data: `codex:open:${item2.id}`,
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
keyboard.push(row);
|
|
1273
|
+
}
|
|
1274
|
+
keyboard.push([
|
|
1275
|
+
{ text: '✨ 新建会话', callback_data: 'engine:setup:new' },
|
|
1276
|
+
{ text: '◀️ 返回控制面板', callback_data: 'engine:setup:main' },
|
|
1277
|
+
]);
|
|
1278
|
+
if (editMessageId) {
|
|
1279
|
+
await this.editMessage(scopeId, editMessageId, text, keyboard);
|
|
1280
|
+
this.scheduleStalePanelDeletion(scopeId, editMessageId);
|
|
1281
|
+
}
|
|
1282
|
+
else {
|
|
1283
|
+
const msgId = await this.sendMessage(scopeId, text, keyboard);
|
|
1284
|
+
this.scheduleStalePanelDeletion(scopeId, msgId);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
async openCodexThread(scopeId, rawTarget, locale, messageId) {
|
|
1288
|
+
const codexHome = this.config.codexAuthDir ?? this.config.codexHome ?? path.join(os.homedir(), '.codex');
|
|
1289
|
+
let targetThreadId = rawTarget.trim();
|
|
1290
|
+
if (/^\d+$/.test(targetThreadId)) {
|
|
1291
|
+
const idx = parseInt(targetThreadId, 10) - 1;
|
|
1292
|
+
try {
|
|
1293
|
+
const indexPath = path.join(codexHome, 'session_index.jsonl');
|
|
1294
|
+
const content = await fsPromises.readFile(indexPath, 'utf8').catch(() => '');
|
|
1295
|
+
const lines = content.trim().split('\n').filter(Boolean);
|
|
1296
|
+
const seenIds = new Set();
|
|
1297
|
+
const items = [];
|
|
1298
|
+
for (let i = lines.length - 1; i >= 0 && items.length < 20; i--) {
|
|
1299
|
+
try {
|
|
1300
|
+
const parsed = JSON.parse(lines[i]);
|
|
1301
|
+
if (parsed.id && !seenIds.has(parsed.id)) {
|
|
1302
|
+
seenIds.add(parsed.id);
|
|
1303
|
+
items.push(parsed.id);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
catch {
|
|
1307
|
+
/* ignore */
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
if (idx >= 0 && idx < items.length) {
|
|
1311
|
+
targetThreadId = items[idx];
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
catch {
|
|
1315
|
+
/* ignore */
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
const currentBinding = this.store.getBinding(scopeId);
|
|
1319
|
+
const targetCwd = currentBinding?.cwd || this.config.defaultCwd;
|
|
1320
|
+
this.store.setBinding(scopeId, targetThreadId, targetCwd);
|
|
1321
|
+
const activeBackend = this.orchestrator.getBackendDescriptorForScope(scopeId);
|
|
1322
|
+
this.store.setScopeBackendBinding(scopeId, activeBackend.id, targetThreadId, targetCwd);
|
|
1323
|
+
if (messageId) {
|
|
1324
|
+
await this.messaging.answerCallback(`codex:open:${targetThreadId}`, locale === 'zh' ? '已恢复会话' : 'Thread resumed');
|
|
1325
|
+
}
|
|
1326
|
+
const text = locale === 'zh'
|
|
1327
|
+
? `📂 **已切换绑定到既有 OpenAI Codex 会话**\n\n` +
|
|
1328
|
+
`• **会话 ID**: \`${targetThreadId}\`\n` +
|
|
1329
|
+
`• **工作目录**: \`${targetCwd}\`\n\n` +
|
|
1330
|
+
`后续消息将在此 Codex 会话继续执行。`
|
|
1331
|
+
: `📂 **Switched to Codex Session**\n\n` +
|
|
1332
|
+
`• **ID**: \`${targetThreadId}\`\n` +
|
|
1333
|
+
`• **Directory**: \`${targetCwd}\`\n\n` +
|
|
1334
|
+
`Next messages will continue in this thread.`;
|
|
1335
|
+
const keyboard = [
|
|
1336
|
+
[
|
|
1337
|
+
{ text: '📁 查看其他会话', callback_data: 'codex:setup:threads' },
|
|
1338
|
+
{ text: '⚙️ 控制面板', callback_data: 'engine:setup:main' },
|
|
1339
|
+
],
|
|
1340
|
+
];
|
|
1341
|
+
if (messageId) {
|
|
1342
|
+
await this.editMessage(scopeId, messageId, text, keyboard);
|
|
1343
|
+
}
|
|
1344
|
+
else {
|
|
1345
|
+
await this.sendMessage(scopeId, text, keyboard);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
async watchConversation(scopeId, rawTarget, locale, editMessageId, notify = true) {
|
|
1349
|
+
let conv = null;
|
|
1350
|
+
if (rawTarget) {
|
|
1351
|
+
conv = this.conversations.resolveConversation(rawTarget, scopeId, this.store);
|
|
1352
|
+
}
|
|
1353
|
+
else {
|
|
1354
|
+
const boundThreadId = this.store.getBinding(scopeId)?.threadId;
|
|
1355
|
+
if (boundThreadId) {
|
|
1356
|
+
conv = this.conversations.getConversation(boundThreadId);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
if (!conv) {
|
|
1360
|
+
if (notify) {
|
|
1361
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
1362
|
+
? `❌ 未找到指定会话进行观察。用法:\`/watch <编号|会话ID>\`,或使用 \`/threads\` 选择。`
|
|
1363
|
+
: `❌ Conversation not found. Usage: \`/watch <num|id>\` or check \`/threads\`.`);
|
|
1364
|
+
}
|
|
1365
|
+
return;
|
|
1366
|
+
}
|
|
1367
|
+
const transcriptPath = this.conversations.getTranscriptPath(conv.conversationId);
|
|
1368
|
+
if (!transcriptPath) {
|
|
1369
|
+
if (notify) {
|
|
1370
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
1371
|
+
? `⚠️ 会话 \`${conv.conversationId.slice(0, 8)}\` 暂无本地日志文件,无法进入实时观察模式。`
|
|
1372
|
+
: `⚠️ No transcript log found for \`${conv.conversationId.slice(0, 8)}\`.`);
|
|
1373
|
+
}
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
// Stop existing watcher for this scope if any
|
|
1377
|
+
const existing = this.watchers.get(scopeId);
|
|
1378
|
+
if (existing) {
|
|
1379
|
+
existing.stopped = true;
|
|
1380
|
+
if (existing.timer)
|
|
1381
|
+
clearTimeout(existing.timer);
|
|
1382
|
+
}
|
|
1383
|
+
// Seek to end of file initially to only tail new entries
|
|
1384
|
+
let initialOffset = 0;
|
|
1385
|
+
try {
|
|
1386
|
+
if (fs.existsSync(transcriptPath)) {
|
|
1387
|
+
initialOffset = fs.statSync(transcriptPath).size;
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
catch {
|
|
1391
|
+
/* ignore */
|
|
1392
|
+
}
|
|
1393
|
+
const watcher = {
|
|
1394
|
+
scopeId,
|
|
1395
|
+
conversationId: conv.conversationId,
|
|
1396
|
+
transcriptPath,
|
|
1397
|
+
fileOffset: initialOffset,
|
|
1398
|
+
remainder: '',
|
|
1399
|
+
timer: null,
|
|
1400
|
+
stopped: false,
|
|
1401
|
+
messageId: null,
|
|
1402
|
+
currentToolLines: [],
|
|
1403
|
+
lastContentPreview: '',
|
|
1404
|
+
};
|
|
1405
|
+
this.watchers.set(scopeId, watcher);
|
|
1406
|
+
this.store.setWatchedThread(scopeId, conv.conversationId);
|
|
1407
|
+
if (notify) {
|
|
1408
|
+
const keyboard = [
|
|
1409
|
+
[{ text: '🛑 停止观察 (/unwatch)', callback_data: `${AGY_WATCH_CALLBACK_PREFIX}stop` }],
|
|
1410
|
+
[
|
|
1411
|
+
{ text: '📁 会话列表', callback_data: `${AGY_SETUP_CALLBACK_PREFIX}threads` },
|
|
1412
|
+
{ text: '⚙️ 控制面板', callback_data: 'engine:setup:main' },
|
|
1413
|
+
],
|
|
1414
|
+
];
|
|
1415
|
+
const text = locale === 'zh'
|
|
1416
|
+
? `👁 **已开启实时观察会话**\n\n` +
|
|
1417
|
+
`• **会话标题**: ${conv.title}\n` +
|
|
1418
|
+
`• **会话 ID**: \`${conv.conversationId}\`\n` +
|
|
1419
|
+
`• **工作目录**: \`${conv.workspaceDir || this.config.defaultCwd}\`\n` +
|
|
1420
|
+
`• **日志路径**: \`${transcriptPath}\`\n\n` +
|
|
1421
|
+
`现在开始,外部终端或其他客户端在此会话产生的步骤、工具调用和回复将实时同步至 Telegram。\n\n` +
|
|
1422
|
+
`发送 \`/unwatch\` 或点击下方按钮可随时退出观察。`
|
|
1423
|
+
: `👁 **Started Watching Conversation**\n\n` +
|
|
1424
|
+
`• **Title**: ${conv.title}\n` +
|
|
1425
|
+
`• **ID**: \`${conv.conversationId}\`\n` +
|
|
1426
|
+
`• **Directory**: \`${conv.workspaceDir || this.config.defaultCwd}\`\n` +
|
|
1427
|
+
`• **Log**: \`${transcriptPath}\`\n\n` +
|
|
1428
|
+
`Now monitoring new turns, tools, and responses in real-time.\n\n` +
|
|
1429
|
+
`Send \`/unwatch\` or tap below to stop.`;
|
|
1430
|
+
if (editMessageId) {
|
|
1431
|
+
await this.editMessage(scopeId, editMessageId, text, keyboard);
|
|
1432
|
+
}
|
|
1433
|
+
else {
|
|
1434
|
+
await this.sendMessage(scopeId, text, keyboard);
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
this.scheduleWatcherPoll(watcher);
|
|
1438
|
+
}
|
|
1439
|
+
async unwatchConversation(scopeId, locale, notify = true) {
|
|
1440
|
+
this.store.setWatchedThread(scopeId, null);
|
|
1441
|
+
const watcher = this.watchers.get(scopeId);
|
|
1442
|
+
if (!watcher) {
|
|
1443
|
+
if (notify) {
|
|
1444
|
+
await this.sendMessage(scopeId, locale === 'zh' ? 'ℹ️ 当前没有正在观察的会话。' : 'ℹ️ No conversation is currently being watched.');
|
|
1445
|
+
}
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
watcher.stopped = true;
|
|
1449
|
+
if (watcher.timer)
|
|
1450
|
+
clearTimeout(watcher.timer);
|
|
1451
|
+
this.watchers.delete(scopeId);
|
|
1452
|
+
if (notify) {
|
|
1453
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
1454
|
+
? `🛑 **已停止观察会话**\n\n已退出只读监控模式。`
|
|
1455
|
+
: `🛑 **Stopped Watching Conversation**\n\nExited read-only watch mode.`);
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
scheduleWatcherPoll(watcher) {
|
|
1459
|
+
if (watcher.stopped)
|
|
1460
|
+
return;
|
|
1461
|
+
watcher.timer = setTimeout(() => {
|
|
1462
|
+
watcher.timer = null;
|
|
1463
|
+
void this.pollWatcher(watcher)
|
|
1464
|
+
.catch((err) => {
|
|
1465
|
+
this.logger.debug('antigravity.watch_poll_error', { error: String(err) });
|
|
1466
|
+
})
|
|
1467
|
+
.finally(() => {
|
|
1468
|
+
if (!watcher.stopped && this.watchers.get(watcher.scopeId) === watcher) {
|
|
1469
|
+
this.scheduleWatcherPoll(watcher);
|
|
1470
|
+
}
|
|
1471
|
+
});
|
|
1472
|
+
}, 1500);
|
|
1473
|
+
watcher.timer?.unref?.();
|
|
1474
|
+
}
|
|
1475
|
+
async pollWatcher(watcher) {
|
|
1476
|
+
if (watcher.stopped)
|
|
1477
|
+
return;
|
|
1478
|
+
if (this.orchestrator.hasActiveTurn(watcher.scopeId)) {
|
|
1479
|
+
try {
|
|
1480
|
+
if (fs.existsSync(watcher.transcriptPath)) {
|
|
1481
|
+
watcher.fileOffset = fs.statSync(watcher.transcriptPath).size;
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
catch {
|
|
1485
|
+
/* ignore */
|
|
1486
|
+
}
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
try {
|
|
1490
|
+
if (!fs.existsSync(watcher.transcriptPath)) {
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
const stats = fs.statSync(watcher.transcriptPath);
|
|
1494
|
+
if (stats.size < watcher.fileOffset) {
|
|
1495
|
+
watcher.fileOffset = 0;
|
|
1496
|
+
watcher.remainder = '';
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
if (stats.size === watcher.fileOffset) {
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
const bytesToRead = stats.size - watcher.fileOffset;
|
|
1503
|
+
const buffer = Buffer.alloc(bytesToRead);
|
|
1504
|
+
const fd = fs.openSync(watcher.transcriptPath, 'r');
|
|
1505
|
+
try {
|
|
1506
|
+
fs.readSync(fd, buffer, 0, bytesToRead, watcher.fileOffset);
|
|
1507
|
+
}
|
|
1508
|
+
finally {
|
|
1509
|
+
fs.closeSync(fd);
|
|
1510
|
+
}
|
|
1511
|
+
watcher.fileOffset = stats.size;
|
|
1512
|
+
const chunk = watcher.remainder + buffer.toString('utf8');
|
|
1513
|
+
const lines = chunk.split('\n');
|
|
1514
|
+
watcher.remainder = lines.pop() ?? '';
|
|
1515
|
+
for (const line of lines) {
|
|
1516
|
+
const trimmed = line.trim();
|
|
1517
|
+
if (!trimmed)
|
|
1518
|
+
continue;
|
|
1519
|
+
let entry;
|
|
1520
|
+
try {
|
|
1521
|
+
entry = JSON.parse(trimmed);
|
|
1522
|
+
}
|
|
1523
|
+
catch {
|
|
1524
|
+
continue;
|
|
1525
|
+
}
|
|
1526
|
+
await this.handleWatcherTranscriptEntry(watcher, entry);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
catch (err) {
|
|
1530
|
+
this.logger.debug('antigravity.poll_watcher_error', { error: String(err) });
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
async handleWatcherTranscriptEntry(watcher, entry) {
|
|
1534
|
+
if (entry.type === 'USER_INPUT') {
|
|
1535
|
+
const text = typeof entry.content === 'string' ? entry.content : '';
|
|
1536
|
+
const preview = text.length > 300 ? text.slice(0, 300) + '…' : text;
|
|
1537
|
+
await this.sendMessage(watcher.scopeId, `👤 <b>外部用户输入</b>:\n${escapeTelegramHtml(preview)}`);
|
|
1538
|
+
watcher.messageId = null;
|
|
1539
|
+
watcher.currentToolLines = [];
|
|
1540
|
+
watcher.lastContentPreview = '';
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
if (entry.type === 'PLANNER_RESPONSE') {
|
|
1544
|
+
let hasUpdate = false;
|
|
1545
|
+
if (Array.isArray(entry.tool_calls) && entry.tool_calls.length > 0) {
|
|
1546
|
+
for (const tc of entry.tool_calls) {
|
|
1547
|
+
const name = tc.name || 'tool';
|
|
1548
|
+
let desc = '';
|
|
1549
|
+
if (tc.args) {
|
|
1550
|
+
const raw = tc.args.toolSummary || tc.args.toolAction || tc.args.CommandLine || tc.args.TargetFile || '';
|
|
1551
|
+
desc = typeof raw === 'string' ? raw.replace(/^"|"$/g, '').trim() : '';
|
|
1552
|
+
}
|
|
1553
|
+
const toolLine = `⚙️ <code>${escapeTelegramHtml(name)}</code>${desc ? ` · <i>${escapeTelegramHtml(desc.slice(0, 60))}</i>` : ''}`;
|
|
1554
|
+
if (!watcher.currentToolLines.includes(toolLine)) {
|
|
1555
|
+
watcher.currentToolLines.push(toolLine);
|
|
1556
|
+
hasUpdate = true;
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
const isFinalAnswer = entry.status === 'DONE' &&
|
|
1561
|
+
typeof entry.content === 'string' &&
|
|
1562
|
+
entry.content.trim().length > 0 &&
|
|
1563
|
+
(!entry.tool_calls || entry.tool_calls.length === 0);
|
|
1564
|
+
if (isFinalAnswer) {
|
|
1565
|
+
if (watcher.messageId) {
|
|
1566
|
+
let progressFinal = `✅ <b>[Antigravity 步骤已完成]</b>`;
|
|
1567
|
+
if (watcher.currentToolLines.length > 0) {
|
|
1568
|
+
const maxPreview = 3;
|
|
1569
|
+
const recent = watcher.currentToolLines.slice(-maxPreview);
|
|
1570
|
+
const hiddenCount = watcher.currentToolLines.length - recent.length;
|
|
1571
|
+
const hiddenNote = hiddenCount > 0
|
|
1572
|
+
? `\n<i>(其余 ${hiddenCount} 项历史工具调用已折叠收起)</i>`
|
|
1573
|
+
: '';
|
|
1574
|
+
progressFinal += `\n\n<blockquote expandable>🛠️ <b>已调用工具 (${watcher.currentToolLines.length} 项)</b>\n${recent.join('\n')}${hiddenNote}</blockquote>`;
|
|
1575
|
+
}
|
|
1576
|
+
await this.editMessage(watcher.scopeId, watcher.messageId, progressFinal).catch(() => { });
|
|
1577
|
+
}
|
|
1578
|
+
const chunks = chunkTelegramMessage(entry.content.trim(), 4000);
|
|
1579
|
+
for (const chunk of chunks) {
|
|
1580
|
+
await this.sendMessage(watcher.scopeId, chunk);
|
|
1581
|
+
}
|
|
1582
|
+
watcher.messageId = null;
|
|
1583
|
+
watcher.currentToolLines = [];
|
|
1584
|
+
watcher.lastContentPreview = '';
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1587
|
+
if (entry.content) {
|
|
1588
|
+
watcher.lastContentPreview = entry.content;
|
|
1589
|
+
hasUpdate = true;
|
|
1590
|
+
}
|
|
1591
|
+
if (hasUpdate) {
|
|
1592
|
+
let messageText = `👁 <b>[Antigravity 观察中]</b>\n\n`;
|
|
1593
|
+
if (watcher.currentToolLines.length > 0) {
|
|
1594
|
+
const recent = watcher.currentToolLines.slice(-8);
|
|
1595
|
+
messageText += `<blockquote expandable>🛠️ <b>已调用工具 (${watcher.currentToolLines.length} 项)</b>\n${recent.join('\n')}</blockquote>\n\n`;
|
|
1596
|
+
}
|
|
1597
|
+
if (watcher.lastContentPreview) {
|
|
1598
|
+
messageText += watcher.lastContentPreview.slice(-3000);
|
|
1599
|
+
}
|
|
1600
|
+
else {
|
|
1601
|
+
messageText += `⏳ 正在执行步骤 #${entry.step_index ?? '…'}…`;
|
|
1602
|
+
}
|
|
1603
|
+
if (watcher.messageId) {
|
|
1604
|
+
await this.editMessage(watcher.scopeId, watcher.messageId, messageText).catch(() => { });
|
|
1605
|
+
}
|
|
1606
|
+
else {
|
|
1607
|
+
watcher.messageId = await this.sendMessage(watcher.scopeId, messageText).catch(() => null);
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
async sendAuthMenu(scopeId, args, locale, editMessageId) {
|
|
1613
|
+
const trimmedArgs = (args || '').trim();
|
|
1614
|
+
let currentFilter = 'all';
|
|
1615
|
+
if (trimmedArgs.startsWith('filter:')) {
|
|
1616
|
+
const f = trimmedArgs.slice('filter:'.length).toLowerCase();
|
|
1617
|
+
if (f === 'enabled' || f === 'attention' || f === 'all') {
|
|
1618
|
+
currentFilter = f;
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
else if (trimmedArgs) {
|
|
1622
|
+
if (trimmedArgs.startsWith('pause ')) {
|
|
1623
|
+
const target = trimmedArgs.slice('pause '.length).trim();
|
|
1624
|
+
this.auth.pauseAccount(target);
|
|
1625
|
+
await this.sendMessage(scopeId, locale === 'zh' ? `⏸ 账号 \`${target}\` 已暂停使用。` : `⏸ Account \`${target}\` paused.`);
|
|
1626
|
+
return;
|
|
1627
|
+
}
|
|
1628
|
+
if (trimmedArgs.startsWith('resume ')) {
|
|
1629
|
+
const target = trimmedArgs.slice('resume '.length).trim();
|
|
1630
|
+
this.auth.resumeAccount(target);
|
|
1631
|
+
await this.sendMessage(scopeId, locale === 'zh' ? `▶️ 账号 \`${target}\` 已恢复启用。` : `▶️ Account \`${target}\` resumed.`);
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
if (trimmedArgs === 'repair_all' || trimmedArgs === 'repair all') {
|
|
1635
|
+
const res = await this.auth.diagnoseAndRepairAll();
|
|
1636
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
1637
|
+
? `🩺 **账号池体检与修复结果**\n\n• 总账号数: ${res.total}\n• 正常/已修复: ${res.healthy + res.repaired}\n• 需关注/失败: ${res.failed}\n\n${res.summary.join('\n')}`
|
|
1638
|
+
: `🩺 **Account Diagnostics & Repair**\n\n• Total: ${res.total}\n• Healthy/Repaired: ${res.healthy + res.repaired}\n• Failed: ${res.failed}\n\n${res.summary.join('\n')}`);
|
|
1639
|
+
return;
|
|
1640
|
+
}
|
|
1641
|
+
if (trimmedArgs.startsWith('repair ') || trimmedArgs === 'repair') {
|
|
1642
|
+
const target = trimmedArgs === 'repair' ? 'active' : trimmedArgs.slice('repair '.length).trim();
|
|
1643
|
+
const res = await this.auth.diagnoseAndRepairAccount(target);
|
|
1644
|
+
await this.sendMessage(scopeId, res.ok
|
|
1645
|
+
? (locale === 'zh' ? `🩺 账号 \`${res.email || res.accountName}\` 检查修复成功:${res.message}` : `🩺 Account \`${res.email || res.accountName}\` repaired: ${res.message}`)
|
|
1646
|
+
: (locale === 'zh' ? `❌ 检查修复失败:${res.message}` : `❌ Repair failed: ${res.message}`));
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1649
|
+
if (trimmedArgs === 'rotate') {
|
|
1650
|
+
try {
|
|
1651
|
+
const res = await this.auth.rotateNextCandidate();
|
|
1652
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
1653
|
+
? `🔄 已成功轮转到下一个账号: \`${res.account.email ?? res.account.name}\``
|
|
1654
|
+
: `🔄 Successfully rotated to next account: \`${res.account.email ?? res.account.name}\``);
|
|
1655
|
+
}
|
|
1656
|
+
catch (err) {
|
|
1657
|
+
await this.sendMessage(scopeId, `❌ ${err instanceof Error ? err.message : String(err)}`);
|
|
1658
|
+
}
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
if (trimmedArgs === 'refresh') {
|
|
1662
|
+
const active = await this.auth.getActiveAccount();
|
|
1663
|
+
if (!active) {
|
|
1664
|
+
await this.sendMessage(scopeId, locale === 'zh' ? '❌ 当前无活跃账号。' : '❌ No active account.');
|
|
1665
|
+
return;
|
|
1666
|
+
}
|
|
1667
|
+
const res = await this.auth.refreshTokenForAccount(active.name);
|
|
1668
|
+
if (res.success) {
|
|
1669
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
1670
|
+
? `⚡ 账号 \`${active.email || active.name}\` Token 刷新成功!`
|
|
1671
|
+
: `⚡ Account \`${active.email || active.name}\` refreshed successfully!`);
|
|
1672
|
+
}
|
|
1673
|
+
else {
|
|
1674
|
+
await this.sendMessage(scopeId, `❌ 刷新失败: ${res.error}`);
|
|
1675
|
+
}
|
|
1676
|
+
return;
|
|
1677
|
+
}
|
|
1678
|
+
if (trimmedArgs === 'refresh all' || trimmedArgs === 'refresh_all') {
|
|
1679
|
+
const res = await this.auth.refreshAllTokens();
|
|
1680
|
+
await this.auth.populateQuotas(await this.auth.listCandidates(false), true);
|
|
1681
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
1682
|
+
? `⚡ 全部 Token 与额度已刷新完成: 成功 ${res.refreshed} / 共 ${res.total} 个${res.failed > 0 ? ` (失败 ${res.failed})` : ''}`
|
|
1683
|
+
: `⚡ All tokens and quotas refreshed: ${res.refreshed} / ${res.total} succeeded`);
|
|
1684
|
+
return;
|
|
1685
|
+
}
|
|
1686
|
+
if (trimmedArgs.startsWith('add ') || trimmedArgs.startsWith('{')) {
|
|
1687
|
+
const jsonContent = trimmedArgs.startsWith('add ') ? trimmedArgs.slice('add '.length).trim() : trimmedArgs;
|
|
1688
|
+
const res = await this.auth.importAccountFromJson(jsonContent);
|
|
1689
|
+
if (res.success && res.account) {
|
|
1690
|
+
const candidates = await this.auth.listCandidates();
|
|
1691
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
1692
|
+
? `🎉 **Antigravity 账号导入成功!**\n• 账号名称: \`${res.account.name}\`\n• 绑定邮箱: \`${res.account.email || '未知'}\`\n• 候选池总数: 共 ${candidates.length} 个账号`
|
|
1693
|
+
: `🎉 **Account Imported!**\n• Name: \`${res.account.name}\`\n• Email: \`${res.account.email || 'unknown'}\`\n• Pool: ${candidates.length} accounts`);
|
|
1694
|
+
}
|
|
1695
|
+
else {
|
|
1696
|
+
await this.sendMessage(scopeId, `❌ 导入失败: ${res.error || '未知错误'}`);
|
|
1697
|
+
}
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
// Direct switch
|
|
1701
|
+
try {
|
|
1702
|
+
const res = await this.auth.switchAccount(trimmedArgs);
|
|
1703
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
1704
|
+
? `✅ 已成功切换到账号: \`${res.account.email ?? res.account.name}\``
|
|
1705
|
+
: `✅ Successfully switched to account: \`${res.account.email ?? res.account.name}\``);
|
|
1706
|
+
return;
|
|
1707
|
+
}
|
|
1708
|
+
catch (err) {
|
|
1709
|
+
await this.sendMessage(scopeId, `❌ ${err instanceof Error ? err.message : String(err)}`);
|
|
1710
|
+
return;
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
const candidates = await this.auth.listCandidates(true);
|
|
1714
|
+
const active = candidates.find((c) => c.isActive) || candidates[0];
|
|
1715
|
+
const filtered = candidates.filter((c) => {
|
|
1716
|
+
if (currentFilter === 'enabled') {
|
|
1717
|
+
return !c.isPaused && !c.isCooldown && c.hasRefreshToken;
|
|
1718
|
+
}
|
|
1719
|
+
if (currentFilter === 'attention') {
|
|
1720
|
+
return c.isPaused || c.isCooldown || !c.hasRefreshToken;
|
|
1721
|
+
}
|
|
1722
|
+
return true;
|
|
1723
|
+
});
|
|
1724
|
+
const keyboard = [];
|
|
1725
|
+
for (const c of filtered) {
|
|
1726
|
+
const prefix = formatCandidateButtonPrefix(c);
|
|
1727
|
+
let icon = '🔐 ';
|
|
1728
|
+
if (c.isPaused) {
|
|
1729
|
+
icon = '⏸ ';
|
|
1730
|
+
}
|
|
1731
|
+
else if (c.isActive) {
|
|
1732
|
+
icon = '🟢 ';
|
|
1733
|
+
}
|
|
1734
|
+
else if (c.isCooldown) {
|
|
1735
|
+
icon = '⏳ ';
|
|
1736
|
+
}
|
|
1737
|
+
const label = `${icon}${prefix}|${formatCandidateDisplayName(c)}${c.isPaused ? ' · off' : ''}`;
|
|
1738
|
+
let rightBtnText = '⏸';
|
|
1739
|
+
let rightBtnAction = `${AGY_AUTH_CALLBACK_PREFIX}toggle_pause:${c.name}`;
|
|
1740
|
+
if (!c.hasRefreshToken || c.isCooldown) {
|
|
1741
|
+
rightBtnText = '🩺';
|
|
1742
|
+
rightBtnAction = `${AGY_AUTH_CALLBACK_PREFIX}repair:${c.name}`;
|
|
1743
|
+
}
|
|
1744
|
+
else if (c.isPaused) {
|
|
1745
|
+
rightBtnText = '▶️';
|
|
1746
|
+
rightBtnAction = `${AGY_AUTH_CALLBACK_PREFIX}toggle_pause:${c.name}`;
|
|
1747
|
+
}
|
|
1748
|
+
else if (c.isActive) {
|
|
1749
|
+
rightBtnText = '✅';
|
|
1750
|
+
rightBtnAction = `${AGY_AUTH_CALLBACK_PREFIX}toggle_pause:${c.name}`;
|
|
1751
|
+
}
|
|
1752
|
+
keyboard.push([
|
|
1753
|
+
{
|
|
1754
|
+
text: label.length > 28 ? `${label.slice(0, 27)}…` : label,
|
|
1755
|
+
callback_data: `${AGY_AUTH_CALLBACK_PREFIX}switch:${c.name}`,
|
|
1756
|
+
},
|
|
1757
|
+
{
|
|
1758
|
+
text: rightBtnText,
|
|
1759
|
+
callback_data: rightBtnAction,
|
|
1760
|
+
},
|
|
1761
|
+
]);
|
|
1762
|
+
}
|
|
1763
|
+
// Filter tab row matching Codex
|
|
1764
|
+
keyboard.push([
|
|
1765
|
+
{
|
|
1766
|
+
text: currentFilter === 'all' ? '☑️ 全部' : '全部',
|
|
1767
|
+
callback_data: `${AGY_AUTH_CALLBACK_PREFIX}filter:all`,
|
|
1768
|
+
},
|
|
1769
|
+
{
|
|
1770
|
+
text: currentFilter === 'enabled' ? '☑️ 已启用' : '已启用',
|
|
1771
|
+
callback_data: `${AGY_AUTH_CALLBACK_PREFIX}filter:enabled`,
|
|
1772
|
+
},
|
|
1773
|
+
{
|
|
1774
|
+
text: currentFilter === 'attention' ? '☑️ 需关注' : '需关注',
|
|
1775
|
+
callback_data: `${AGY_AUTH_CALLBACK_PREFIX}filter:attention`,
|
|
1776
|
+
},
|
|
1777
|
+
]);
|
|
1778
|
+
keyboard.push([
|
|
1779
|
+
{ text: '🔑 设备登录', callback_data: `${AGY_AUTH_CALLBACK_PREFIX}login` },
|
|
1780
|
+
{ text: '🩺 账号体检与修复', callback_data: `${AGY_AUTH_CALLBACK_PREFIX}repair_all` },
|
|
1781
|
+
]);
|
|
1782
|
+
keyboard.push([
|
|
1783
|
+
{ text: '🔄 轮转切号', callback_data: `${AGY_AUTH_CALLBACK_PREFIX}rotate` },
|
|
1784
|
+
{ text: '⚡ 刷新额度与Token', callback_data: `${AGY_AUTH_CALLBACK_PREFIX}refresh_all` },
|
|
1785
|
+
]);
|
|
1786
|
+
keyboard.push([
|
|
1787
|
+
{ text: '📥 账号导入指南', callback_data: `${AGY_AUTH_CALLBACK_PREFIX}import_help` },
|
|
1788
|
+
{ text: '◀️ 返回控制面板', callback_data: 'engine:setup:main' },
|
|
1789
|
+
]);
|
|
1790
|
+
const activeExpiry = formatAccountExpiry(active?.expiry ?? null, locale, true);
|
|
1791
|
+
const activeLabel = active ? `${active.email || active.name}` : locale === 'zh' ? '无' : 'None';
|
|
1792
|
+
let quotaDetails = '';
|
|
1793
|
+
if (active?.quota) {
|
|
1794
|
+
const q = active.quota;
|
|
1795
|
+
const p5h = typeof q.fiveHourPercent === 'number' ? `${q.fiveHourPercent}%` : '未知';
|
|
1796
|
+
const pw = typeof q.weeklyPercent === 'number' ? `${q.weeklyPercent}%` : '未知';
|
|
1797
|
+
const r5h = q.resetTime5h ? formatQuotaResetTime(q.resetTime5h) : '—';
|
|
1798
|
+
const rw = q.resetTimeWeekly ? formatQuotaResetTime(q.resetTimeWeekly) : '—';
|
|
1799
|
+
quotaDetails =
|
|
1800
|
+
locale === 'zh'
|
|
1801
|
+
? `\n• **Gemini 5h 额度**: \`${p5h}\` (重置于 ${r5h})` +
|
|
1802
|
+
`\n• **Gemini 7d 额度**: \`${pw}\` (重置于 ${rw})`
|
|
1803
|
+
: `\n• **Gemini 5h Quota**: \`${p5h}\` (resets ${r5h})` +
|
|
1804
|
+
`\n• **Gemini 7d Quota**: \`${pw}\` (resets ${rw})`;
|
|
1805
|
+
if (typeof q.thirdPartyWeeklyPercent === 'number') {
|
|
1806
|
+
const p3pw = `${q.thirdPartyWeeklyPercent}%`;
|
|
1807
|
+
const r3pw = q.resetTimeWeekly ? formatQuotaResetTime(q.resetTimeWeekly) : '—';
|
|
1808
|
+
quotaDetails +=
|
|
1809
|
+
locale === 'zh'
|
|
1810
|
+
? `\n• **3P (Claude/GPT)**: \`${p3pw}\` (重置于 ${r3pw})`
|
|
1811
|
+
: `\n• **3P (Claude/GPT)**: \`${p3pw}\` (resets ${r3pw})`;
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
else {
|
|
1815
|
+
quotaDetails =
|
|
1816
|
+
locale === 'zh'
|
|
1817
|
+
? `\n• **5h/7d 额度**: \`未获取到\` (点击【⚡ 刷新额度与Token】实时获取)`
|
|
1818
|
+
: `\n• **5h/7d Quota**: \`Not available\` (tap [⚡ Refresh Quota & Token])`;
|
|
1819
|
+
}
|
|
1820
|
+
const text = locale === 'zh'
|
|
1821
|
+
? `👤 **Antigravity 账号管理池**\n\n` +
|
|
1822
|
+
`• **当前活跃账号**: \`${activeLabel}\`\n` +
|
|
1823
|
+
`• **活跃 Token 状态**: ${activeExpiry}` +
|
|
1824
|
+
quotaDetails +
|
|
1825
|
+
`\n• **账号池总数**: 共 ${candidates.length} 个账号\n` +
|
|
1826
|
+
`• **按键前缀说明**: \`5h剩余% | 7d剩余% | 账号别名\` (数值为剩余额度百分比,非时间)\n\n` +
|
|
1827
|
+
`点击下方账号名称可即时无缝热切换;点击【🔑 设备登录】可在 Telegram 内直接授权绑定新账号。`
|
|
1828
|
+
: `👤 **Antigravity Account Pool**\n\n` +
|
|
1829
|
+
`• **Active**: \`${activeLabel}\`\n` +
|
|
1830
|
+
`• **Status**: ${activeExpiry}` +
|
|
1831
|
+
quotaDetails +
|
|
1832
|
+
`\n• **Pool Size**: ${candidates.length} accounts\n` +
|
|
1833
|
+
`• **Prefix Meaning**: \`5h% | 7d% | account\` (values represent remaining quota %, not time)\n\n` +
|
|
1834
|
+
`Tap candidate below to switch seamlessly. Tap [🔑 Device Login] to sign in.`;
|
|
1835
|
+
if (editMessageId) {
|
|
1836
|
+
await this.editMessage(scopeId, editMessageId, text, keyboard);
|
|
1837
|
+
this.scheduleStalePanelDeletion(scopeId, editMessageId);
|
|
1838
|
+
}
|
|
1839
|
+
else {
|
|
1840
|
+
const msgId = await this.sendMessage(scopeId, text, keyboard);
|
|
1841
|
+
this.scheduleStalePanelDeletion(scopeId, msgId);
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
async startLoginFlow(scopeId, locale, editMessageId) {
|
|
1845
|
+
const session = this.auth.startBrowserLogin(scopeId);
|
|
1846
|
+
const keyboard = [
|
|
1847
|
+
[{ text: '❌ 取消登录会话', callback_data: `${AGY_AUTH_CALLBACK_PREFIX}cancel_login` }],
|
|
1848
|
+
[{ text: '◀️ 返回账号列表', callback_data: `${AGY_AUTH_CALLBACK_PREFIX}filter:all` }],
|
|
1849
|
+
];
|
|
1850
|
+
const text = locale === 'zh'
|
|
1851
|
+
? `🔑 **Google / Antigravity 设备授权登录**\n\n` +
|
|
1852
|
+
`1️⃣ **点击授权链接**(在浏览器中打开):\n` +
|
|
1853
|
+
`[👉 点击打开 Google 授权登录页面](${session.authUrl})\n\n` +
|
|
1854
|
+
`2️⃣ 在浏览器中完成账号选择与权限授予;\n` +
|
|
1855
|
+
`3️⃣ 授权完成后,浏览器页面可能提示复制授权码(Authorization Code),或重定向至空白页;\n` +
|
|
1856
|
+
`4️⃣ 直接将**授权码**或**重定向完整 URL**复制并作为消息发送给本机器人即可!\n\n` +
|
|
1857
|
+
`*临时会话有效期 10 分钟。随时发送 \`/auth cancel\` 可取消。*`
|
|
1858
|
+
: `🔑 **Google / Antigravity OAuth Login**\n\n` +
|
|
1859
|
+
`1️⃣ **Click authorization link**:\n` +
|
|
1860
|
+
`[👉 Open Google Authorization](${session.authUrl})\n\n` +
|
|
1861
|
+
`2️⃣ Approve requested permissions;\n` +
|
|
1862
|
+
`3️⃣ Paste the authorization code or redirect URL back into this chat!\n\n` +
|
|
1863
|
+
`*Valid for 10 minutes. Send /auth cancel to abort.*`;
|
|
1864
|
+
if (editMessageId) {
|
|
1865
|
+
await this.editMessage(scopeId, editMessageId, text, keyboard);
|
|
1866
|
+
}
|
|
1867
|
+
else {
|
|
1868
|
+
await this.sendMessage(scopeId, text, keyboard);
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
async sendEffortMenu(scopeId, arg, locale, editMessageId) {
|
|
1872
|
+
const settings = this.store.getChatSettings(scopeId);
|
|
1873
|
+
const isBoost = settings?.serviceTier === 'boost';
|
|
1874
|
+
if (arg) {
|
|
1875
|
+
const target = arg.toLowerCase();
|
|
1876
|
+
if (target === 'low' || target === 'medium' || target === 'high') {
|
|
1877
|
+
if (target !== 'high' && isBoost) {
|
|
1878
|
+
this.store.setChatServiceTier(scopeId, null);
|
|
1879
|
+
}
|
|
1880
|
+
this.store.setChatSettings(scopeId, null, target);
|
|
1881
|
+
this.orchestrator.syncCurrentBackendSettings(scopeId);
|
|
1882
|
+
await this.sendMessage(scopeId, locale === 'zh' ? `✅ 思考深度已切换为: \`${target}\`` : `✅ Effort switched to: \`${target}\``);
|
|
1883
|
+
return;
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
const current = isBoost ? 'high' : (settings?.reasoningEffort ?? 'high');
|
|
1887
|
+
const keyboard = [
|
|
1888
|
+
[
|
|
1889
|
+
{
|
|
1890
|
+
text: `${current === 'low' ? '✅ ' : ''}Low (快速)`,
|
|
1891
|
+
callback_data: `${AGY_EFFORT_CALLBACK_PREFIX}low`,
|
|
1892
|
+
},
|
|
1893
|
+
{
|
|
1894
|
+
text: `${current === 'medium' ? '✅ ' : ''}Medium (平衡)`,
|
|
1895
|
+
callback_data: `${AGY_EFFORT_CALLBACK_PREFIX}medium`,
|
|
1896
|
+
},
|
|
1897
|
+
{
|
|
1898
|
+
text: `${current === 'high' ? '✅ ' : ''}High (深度思考)`,
|
|
1899
|
+
callback_data: `${AGY_EFFORT_CALLBACK_PREFIX}high`,
|
|
1900
|
+
},
|
|
1901
|
+
],
|
|
1902
|
+
[{ text: '◀️ 返回控制面板', callback_data: 'engine:setup:main' }],
|
|
1903
|
+
];
|
|
1904
|
+
const boostHint = isBoost
|
|
1905
|
+
? locale === 'zh'
|
|
1906
|
+
? '\n\n*(当前处于 Boost 模式,深度锁定为 High)*'
|
|
1907
|
+
: '\n\n*(Locked to High in Boost Mode)*'
|
|
1908
|
+
: '';
|
|
1909
|
+
const text = locale === 'zh'
|
|
1910
|
+
? `⚡ **选择思考深度 (Reasoning Effort)**\n当前深度: \`${current}\`${boostHint}`
|
|
1911
|
+
: `⚡ **Select Reasoning Effort**\nCurrent: \`${current}\`${boostHint}`;
|
|
1912
|
+
if (editMessageId) {
|
|
1913
|
+
await this.editMessage(scopeId, editMessageId, text, keyboard);
|
|
1914
|
+
this.scheduleStalePanelDeletion(scopeId, editMessageId);
|
|
1915
|
+
}
|
|
1916
|
+
else {
|
|
1917
|
+
const msgId = await this.sendMessage(scopeId, text, keyboard);
|
|
1918
|
+
this.scheduleStalePanelDeletion(scopeId, msgId);
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
export { UnifiedBridgeCore as AntigravityBridgeCore };
|