dsh-agy 0.2.11 → 0.3.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/README.md +67 -11
- package/cordis.patch.yml +4 -2
- package/lib/{accounts-C0Oj3tQA.mjs → accounts-CkaHhb5H.mjs} +1155 -646
- package/lib/accounts-CkaHhb5H.mjs.map +1 -0
- package/lib/{catalog-bfGIyoba.mjs → catalog-BgdDVJEB.mjs} +17 -2
- package/lib/{catalog-bfGIyoba.mjs.map → catalog-BgdDVJEB.mjs.map} +1 -1
- package/lib/cli/index.d.mts +10 -1
- package/lib/cli/index.d.mts.map +1 -1
- package/lib/cli/index.mjs +21 -8
- package/lib/cli/index.mjs.map +1 -1
- package/lib/client.js +2168 -14
- package/lib/{constants-BFKYjOmh.mjs → constants-Db4tmyfb.mjs} +79 -14
- package/lib/constants-Db4tmyfb.mjs.map +1 -0
- package/lib/{exchange-dQm2mjJ5.mjs → exchange-yZPIZumk.mjs} +27 -12
- package/lib/exchange-yZPIZumk.mjs.map +1 -0
- package/lib/identity-Ci8EGc1F.mjs +96 -0
- package/lib/identity-Ci8EGc1F.mjs.map +1 -0
- package/lib/{import-D7R0GpRr.mjs → import-BaFULQnH.mjs} +11 -7
- package/lib/import-BaFULQnH.mjs.map +1 -0
- package/lib/index.mjs +2 -2
- package/lib/{models-ClGTQBLY.mjs → models-BGuwl50d.mjs} +35 -22
- package/lib/models-BGuwl50d.mjs.map +1 -0
- package/lib/plugin-common-B1MG00JT.mjs +1313 -0
- package/lib/plugin-common-B1MG00JT.mjs.map +1 -0
- package/lib/{proxy-DLvX8yxv.mjs → proxy-DfaL73mL.mjs} +52 -3
- package/lib/proxy-DfaL73mL.mjs.map +1 -0
- package/lib/quota-summary-5CypVffd.mjs +127 -0
- package/lib/quota-summary-5CypVffd.mjs.map +1 -0
- package/lib/{translate-pdeVGFpT.mjs → translate-BQu8HLvB.mjs} +57 -26
- package/lib/translate-BQu8HLvB.mjs.map +1 -0
- package/lib/web/plugin.d.mts +1 -0
- package/lib/web/plugin.d.mts.map +1 -1
- package/lib/web/plugin.mjs +651 -1701
- package/lib/web/plugin.mjs.map +1 -1
- package/package.json +17 -4
- package/lib/accounts-C0Oj3tQA.mjs.map +0 -1
- package/lib/constants-BFKYjOmh.mjs.map +0 -1
- package/lib/exchange-dQm2mjJ5.mjs.map +0 -1
- package/lib/identity-DsftHTJC.mjs +0 -35
- package/lib/identity-DsftHTJC.mjs.map +0 -1
- package/lib/import-D7R0GpRr.mjs.map +0 -1
- package/lib/models-ClGTQBLY.mjs.map +0 -1
- package/lib/plugin-common-DtF_PyxH.mjs +0 -254
- package/lib/plugin-common-DtF_PyxH.mjs.map +0 -1
- package/lib/proxy-DLvX8yxv.mjs.map +0 -1
- package/lib/translate-pdeVGFpT.mjs.map +0 -1
package/lib/web/plugin.mjs
CHANGED
|
@@ -1,1201 +1,515 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import { n as
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
title: "Antigravity Account Pool",
|
|
10
|
-
subtitle: "Manage multi-account rotation, quota monitoring, and credentials.",
|
|
11
|
-
refresh: "Refresh",
|
|
12
|
-
loginWithGoogle: "Login with Google",
|
|
13
|
-
switchLang: "中文",
|
|
14
|
-
accountsList: "Accounts",
|
|
15
|
-
noAccounts: "No accounts configured",
|
|
16
|
-
noAccountsHint: "Click \"Login with Google\" or import credentials below to get started.",
|
|
17
|
-
activeBadge: "Active",
|
|
18
|
-
stateActive: "Healthy",
|
|
19
|
-
stateCooling: "Cooling Down",
|
|
20
|
-
stateRateLimited: "Rate Limited",
|
|
21
|
-
stateVerificationRequired: "Verification Required",
|
|
22
|
-
stateDisabled: "Disabled",
|
|
23
|
-
project: "Project",
|
|
24
|
-
defaultProject: "default",
|
|
25
|
-
activate: "Set Active",
|
|
26
|
-
verify: "Verify",
|
|
27
|
-
verifyCta: "Verify Now",
|
|
28
|
-
testAll: "Test All Models",
|
|
29
|
-
exportBlob: "Export Blob",
|
|
30
|
-
fingerprint: "Fingerprint",
|
|
31
|
-
deleteAccount: "Delete Account",
|
|
32
|
-
confirmDelete: "Are you sure you want to delete this account? This action cannot be undone.",
|
|
33
|
-
confirmTestAll: "Test All will execute real API calls on all models, consuming actual quota. Continue?",
|
|
34
|
-
confirmRegenerateFp: "Generate a fresh device fingerprint and user agent for this account?",
|
|
35
|
-
quotaTitle: "Model Quotas",
|
|
36
|
-
quotaUnavailable: "Quota information unavailable for this account",
|
|
37
|
-
noQuotaReported: "No quota reported by upstream",
|
|
38
|
-
testModel: "Test",
|
|
39
|
-
testing: "Testing...",
|
|
40
|
-
copied: "Copied to clipboard!",
|
|
41
|
-
importTitle: "Import Credentials",
|
|
42
|
-
importDesc: "Paste an agy auth.json token document, or a credential blob (from dsh-agy login --blob). Paste multiple blobs, one per line, for a batch import:",
|
|
43
|
-
importPlaceholder: "{\"token\":{\"access_token\":\"...\",\"refresh_token\":\"...\"}} or dsh-agy-cred-v1.... (one blob per line for batch)",
|
|
44
|
-
importJson: "Import JSON",
|
|
45
|
-
importBlobBtn: "Import Blob",
|
|
46
|
-
importSuccess: "Successfully imported account: ",
|
|
47
|
-
exportAll: "Export All",
|
|
48
|
-
importBatchResult: "Imported {imported}, replaced {replaced}",
|
|
49
|
-
copyAllMsg: "All credential blobs exported. Copied to clipboard!",
|
|
50
|
-
fingerprintTitle: "Device Fingerprint",
|
|
51
|
-
fingerprintDevice: "Device ID",
|
|
52
|
-
fingerprintUserAgent: "User Agent",
|
|
53
|
-
fingerprintCreated: "Generated At",
|
|
54
|
-
fingerprintHistory: "Fingerprint Versions",
|
|
55
|
-
regenerateFp: "Regenerate Fingerprint",
|
|
56
|
-
copyBlobMsg: "Credential blob exported. Copied to clipboard!",
|
|
57
|
-
copyFpMsg: "Device fingerprint details loaded.",
|
|
58
|
-
allModelsOk: "All model tests passed!",
|
|
59
|
-
modelsTestedSummary: "Completed testing {total} models: {ok} passed, {failed} failed.",
|
|
60
|
-
loginSuccessTitle: "Sign-in Successful",
|
|
61
|
-
loginSuccessDesc: "Your Antigravity account has been authorized and saved.",
|
|
62
|
-
loginFailedTitle: "Sign-in Failed",
|
|
63
|
-
loginFailedDesc: "Unable to complete authorization. Error details:",
|
|
64
|
-
windowClosing: "This window will close automatically in a moment...",
|
|
65
|
-
closeWindow: "Close Window",
|
|
66
|
-
retryLogin: "Return to Dashboard",
|
|
67
|
-
resetIn: "Resets in ",
|
|
68
|
-
resetAt: "Resets at "
|
|
69
|
-
},
|
|
70
|
-
zh: {
|
|
71
|
-
title: "Antigravity 账号池管理",
|
|
72
|
-
subtitle: "多账号智能轮换、配额实时监控与凭据指纹管理。",
|
|
73
|
-
refresh: "刷新",
|
|
74
|
-
loginWithGoogle: "Google 账号登录",
|
|
75
|
-
switchLang: "EN",
|
|
76
|
-
accountsList: "账号列表",
|
|
77
|
-
noAccounts: "暂无配置账号",
|
|
78
|
-
noAccountsHint: "点击上方“Google 账号登录”或在下方粘贴凭据以添加账号。",
|
|
79
|
-
activeBadge: "当前使用",
|
|
80
|
-
stateActive: "正常",
|
|
81
|
-
stateCooling: "冷却中",
|
|
82
|
-
stateRateLimited: "已限流",
|
|
83
|
-
stateVerificationRequired: "需要验证",
|
|
84
|
-
stateDisabled: "已禁用",
|
|
85
|
-
project: "项目 ID",
|
|
86
|
-
defaultProject: "默认项目",
|
|
87
|
-
activate: "设为默认",
|
|
88
|
-
verify: "验证账号",
|
|
89
|
-
verifyCta: "立即验证",
|
|
90
|
-
testAll: "测试全部模型",
|
|
91
|
-
exportBlob: "导出凭据 Blob",
|
|
92
|
-
fingerprint: "设备指纹",
|
|
93
|
-
deleteAccount: "删除账号",
|
|
94
|
-
confirmDelete: "确定要删除此账号吗?该操作不可撤销。",
|
|
95
|
-
confirmTestAll: "批量测试将对所有模型发起真实调用并消耗 API 配额,确认继续?",
|
|
96
|
-
confirmRegenerateFp: "确定要为该账号重新生成全新的设备指纹与 User-Agent 吗?",
|
|
97
|
-
quotaTitle: "模型配额池",
|
|
98
|
-
quotaUnavailable: "无法获取该账号的模型配额信息",
|
|
99
|
-
noQuotaReported: "上游未返回配额信息",
|
|
100
|
-
testModel: "测试",
|
|
101
|
-
testing: "测试中...",
|
|
102
|
-
copied: "已复制到剪贴板!",
|
|
103
|
-
importTitle: "导入凭据",
|
|
104
|
-
importDesc: "粘贴 agy auth.json 文件内容,或输入凭据 blob(来自 dsh-agy login --blob)。多个 blob 每行一个即可批量导入:",
|
|
105
|
-
importPlaceholder: "{\"token\":{\"access_token\":\"...\",\"refresh_token\":\"...\"}} 或 dsh-agy-cred-v1....(批量:每行一个 blob)",
|
|
106
|
-
importJson: "导入 JSON",
|
|
107
|
-
importBlobBtn: "导入 Blob",
|
|
108
|
-
importSuccess: "成功导入账号:",
|
|
109
|
-
exportAll: "导出全部",
|
|
110
|
-
importBatchResult: "导入 {imported} 个,替换 {replaced} 个",
|
|
111
|
-
copyAllMsg: "全部凭据 Blob 已导出并复制到剪贴板!",
|
|
112
|
-
fingerprintTitle: "设备指纹信息",
|
|
113
|
-
fingerprintDevice: "设备 ID (DeviceId)",
|
|
114
|
-
fingerprintUserAgent: "用户代理 (User-Agent)",
|
|
115
|
-
fingerprintCreated: "生成时间",
|
|
116
|
-
fingerprintHistory: "历史版本数",
|
|
117
|
-
regenerateFp: "重新生成指纹",
|
|
118
|
-
copyBlobMsg: "凭据 Blob 已成功导出并复制到剪贴板!",
|
|
119
|
-
copyFpMsg: "设备指纹信息已加载。",
|
|
120
|
-
allModelsOk: "所有模型可用性测试通过!",
|
|
121
|
-
modelsTestedSummary: "完成 {total} 个模型测试:{ok} 个可用,{failed} 个失败。",
|
|
122
|
-
loginSuccessTitle: "授权登录成功",
|
|
123
|
-
loginSuccessDesc: "Antigravity 账号已成功接入并保存。",
|
|
124
|
-
loginFailedTitle: "授权登录失败",
|
|
125
|
-
loginFailedDesc: "无法完成 OAuth 授权,错误详情:",
|
|
126
|
-
windowClosing: "此窗口即将在 2 秒内自动关闭...",
|
|
127
|
-
closeWindow: "立即关闭窗口",
|
|
128
|
-
retryLogin: "返回控制面板",
|
|
129
|
-
resetIn: "将在 ",
|
|
130
|
-
resetAt: "重置时间:"
|
|
131
|
-
}
|
|
132
|
-
};
|
|
133
|
-
//#endregion
|
|
134
|
-
//#region src/web/page.ts
|
|
1
|
+
import { p as currentAgyVersion } from "../constants-Db4tmyfb.mjs";
|
|
2
|
+
import { i as normalizeProxyUrl, n as isProxyReachable, s as proxyUrlForLogs } from "../proxy-DfaL73mL.mjs";
|
|
3
|
+
import { a as generateFingerprint, i as isAgyDisabled, l as clearExpiredState, n as maskProxyUrl, o as recordFingerprintVersion } from "../accounts-CkaHhb5H.mjs";
|
|
4
|
+
import "../models-BGuwl50d.mjs";
|
|
5
|
+
import { a as THINKING_BUDGET_MAX, i as CLAUDE_BUDGET_MIN, n as foldWindowBreakdown, r as CLAUDE_BUDGET_MAX, t as createAgyRuntime } from "../plugin-common-B1MG00JT.mjs";
|
|
6
|
+
import { t as exchangeAntigravity } from "../exchange-yZPIZumk.mjs";
|
|
7
|
+
import { n as upsertImportedAccount, r as authorizeAntigravity, t as importManySources } from "../import-BaFULQnH.mjs";
|
|
8
|
+
//#region src/web/management.ts
|
|
135
9
|
/**
|
|
136
|
-
*
|
|
137
|
-
*
|
|
10
|
+
* agy management API: one method dispatcher shared by the RPC transport.
|
|
11
|
+
*
|
|
12
|
+
* Every method is a pure `(payload) => result` function so the transport stays
|
|
13
|
+
* thin and the handlers stay testable without HTTP. Failures throw; the
|
|
14
|
+
* transport converts a thrown error into the RPC failure envelope.
|
|
15
|
+
*
|
|
16
|
+
* This module owns the OAuth pending-authorization state because `auth.url`
|
|
17
|
+
* issues it and the callback route consumes it — they must share one map, and
|
|
18
|
+
* splitting them would let a verifier and its callback drift apart.
|
|
138
19
|
*/
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
<body>
|
|
629
|
-
<div class="container">
|
|
630
|
-
<!-- Header -->
|
|
631
|
-
<header class="header">
|
|
632
|
-
<div class="header-title-group">
|
|
633
|
-
<h1>
|
|
634
|
-
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color:var(--brand-primary)"><circle cx="12" cy="12" r="10"></circle><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"></path><path d="M2 12h20"></path></svg>
|
|
635
|
-
<span id="txt-title">Antigravity Account Pool</span>
|
|
636
|
-
</h1>
|
|
637
|
-
<p id="txt-subtitle">Manage multi-account rotation, quota monitoring, and credentials.</p>
|
|
638
|
-
</div>
|
|
639
|
-
<div class="header-actions">
|
|
640
|
-
<button id="btn-lang" class="btn btn-secondary btn-sm" title="Toggle Language">中文</button>
|
|
641
|
-
<button id="btn-refresh" class="btn btn-secondary" title="Refresh accounts">
|
|
642
|
-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"></path><path d="M21 3v5h-5"></path><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"></path><path d="M8 16H3v5"></path></svg>
|
|
643
|
-
<span id="txt-refresh">Refresh</span>
|
|
644
|
-
</button>
|
|
645
|
-
<button id="btn-login" class="btn btn-primary">
|
|
646
|
-
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/><path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/><path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.06H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.94l2.85-2.22.81-.63z" fill="#FBBC05"/><path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.06l3.66 2.84c.87-2.6 3.3-4.52 6.16-4.52z" fill="#EA4335"/></svg>
|
|
647
|
-
<span id="txt-login">Login with Google</span>
|
|
648
|
-
</button>
|
|
649
|
-
</div>
|
|
650
|
-
</header>
|
|
651
|
-
|
|
652
|
-
<!-- Main Workspace -->
|
|
653
|
-
<main class="main-grid">
|
|
654
|
-
<!-- Master: Account List -->
|
|
655
|
-
<section class="card">
|
|
656
|
-
<div class="card-title">
|
|
657
|
-
<span id="txt-accounts-list">Accounts</span>
|
|
658
|
-
<span id="account-total-badge" class="badge badge-neutral">0</span>
|
|
659
|
-
</div>
|
|
660
|
-
<ul id="account-list" class="account-list">
|
|
661
|
-
<li style="color:var(--text-tertiary);font-size:13px;padding:8px 0;" id="txt-loading">Loading accounts...</li>
|
|
662
|
-
</ul>
|
|
663
|
-
</section>
|
|
664
|
-
|
|
665
|
-
<!-- Detail: Selected Account Details -->
|
|
666
|
-
<section class="card" id="detail-card">
|
|
667
|
-
<div id="detail-content">
|
|
668
|
-
<p style="color:var(--text-tertiary);" id="txt-no-accounts-hint">Select or add an account to view details.</p>
|
|
669
|
-
</div>
|
|
670
|
-
</section>
|
|
671
|
-
</main>
|
|
672
|
-
|
|
673
|
-
<!-- Bottom: Import Section -->
|
|
674
|
-
<section class="card import-section">
|
|
675
|
-
<div class="card-title">
|
|
676
|
-
<span id="txt-import-title">Import Credentials</span>
|
|
677
|
-
</div>
|
|
678
|
-
<p style="font-size:12.5px;color:var(--text-secondary);margin-bottom:8px;" id="txt-import-desc">
|
|
679
|
-
Paste an agy auth.json token document, or a credential blob (from dsh-agy login --blob). Paste multiple blobs, one per line, for a batch import:
|
|
680
|
-
</p>
|
|
681
|
-
<textarea id="input-import" class="import-textarea" placeholder='{"token":{"access_token":"...","refresh_token":"..."}} or dsh-agy-cred-v1.... (one blob per line for batch)'></textarea>
|
|
682
|
-
<div style="display:flex;gap:8px;">
|
|
683
|
-
<button id="btn-import-json" class="btn btn-secondary">
|
|
684
|
-
<span id="txt-import-json">Import JSON</span>
|
|
685
|
-
</button>
|
|
686
|
-
<button id="btn-import-blob" class="btn btn-secondary">
|
|
687
|
-
<span id="txt-import-blob">Import Blob</span>
|
|
688
|
-
</button>
|
|
689
|
-
<span style="flex:1"></span>
|
|
690
|
-
<button id="btn-export-all" class="btn btn-secondary">
|
|
691
|
-
<span id="txt-export-all">Export All</span>
|
|
692
|
-
</button>
|
|
693
|
-
</div>
|
|
694
|
-
</section>
|
|
695
|
-
</div>
|
|
696
|
-
|
|
697
|
-
<div id="toast-container"></div>
|
|
698
|
-
|
|
699
|
-
<script>
|
|
700
|
-
const I18N = ${JSON.stringify(I18N_DICT)};
|
|
701
|
-
let currentLang = localStorage.getItem('agy_lang') || (navigator.language.startsWith('zh') ? 'zh' : 'en');
|
|
702
|
-
let accountData = [];
|
|
703
|
-
let selectedIndex = 0;
|
|
704
|
-
let isBusy = false;
|
|
705
|
-
|
|
706
|
-
function t(key) {
|
|
707
|
-
const dict = I18N[currentLang] || I18N.en;
|
|
708
|
-
return dict[key] || I18N.en[key] || key;
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
const $ = (id) => document.getElementById(id);
|
|
712
|
-
|
|
713
|
-
function esc(s) {
|
|
714
|
-
if (s == null) return '';
|
|
715
|
-
return String(s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
function toast(text, type = 'info') {
|
|
719
|
-
const container = $('toast-container');
|
|
720
|
-
const el = document.createElement('div');
|
|
721
|
-
el.className = 'toast toast-' + type;
|
|
722
|
-
el.innerHTML = '<span>' + esc(text) + '</span><span style="cursor:pointer;opacity:0.6;font-weight:600;" onclick="this.parentElement.remove()">✕</span>';
|
|
723
|
-
container.appendChild(el);
|
|
724
|
-
setTimeout(() => {
|
|
725
|
-
el.style.opacity = '0';
|
|
726
|
-
el.style.transform = 'translateY(8px)';
|
|
727
|
-
setTimeout(() => el.remove(), 200);
|
|
728
|
-
}, 3500);
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
function copyToClipboard(text, msg) {
|
|
732
|
-
if (navigator.clipboard && window.isSecureContext) {
|
|
733
|
-
navigator.clipboard.writeText(text).then(() => toast(msg || t('copied'), 'success')).catch(() => fallbackCopy(text, msg));
|
|
734
|
-
} else {
|
|
735
|
-
fallbackCopy(text, msg);
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
function fallbackCopy(text, msg) {
|
|
740
|
-
const ta = document.createElement('textarea');
|
|
741
|
-
ta.value = text;
|
|
742
|
-
ta.style.position = 'fixed';
|
|
743
|
-
ta.style.left = '-9999px';
|
|
744
|
-
document.body.appendChild(ta);
|
|
745
|
-
ta.select();
|
|
746
|
-
try {
|
|
747
|
-
document.execCommand('copy');
|
|
748
|
-
toast(msg || t('copied'), 'success');
|
|
749
|
-
} catch {
|
|
750
|
-
toast('Failed to copy', 'error');
|
|
751
|
-
}
|
|
752
|
-
document.body.removeChild(ta);
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
function formatRelativeTime(dateStr) {
|
|
756
|
-
if (!dateStr) return '';
|
|
757
|
-
const target = new Date(dateStr).getTime();
|
|
758
|
-
const diffMs = target - Date.now();
|
|
759
|
-
if (diffMs <= 0) return '';
|
|
760
|
-
const diffMin = Math.round(diffMs / 60000);
|
|
761
|
-
if (diffMin < 60) return t('resetIn') + diffMin + 'm';
|
|
762
|
-
const diffHours = (diffMs / 3600000).toFixed(1);
|
|
763
|
-
if (diffHours < 24) return t('resetIn') + diffHours + 'h';
|
|
764
|
-
return t('resetAt') + new Date(target).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
function quotaColor(fraction) {
|
|
768
|
-
if (fraction == null) return 'var(--text-dimmed)';
|
|
769
|
-
return fraction > 0.7 ? 'var(--state-success)' : fraction >= 0.3 ? 'var(--state-warn)' : 'var(--state-error)';
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
function getWorstQuotaFraction(acc) {
|
|
773
|
-
const models = (acc.quota && acc.quota.models) || [];
|
|
774
|
-
let worst = 1;
|
|
775
|
-
let hasQuota = false;
|
|
776
|
-
for (const m of models) {
|
|
777
|
-
if (m.remainingFraction != null) {
|
|
778
|
-
worst = Math.min(worst, m.remainingFraction);
|
|
779
|
-
hasQuota = true;
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
return hasQuota ? worst : null;
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
function applyI18nLabels() {
|
|
786
|
-
$('txt-title').textContent = t('title');
|
|
787
|
-
$('txt-subtitle').textContent = t('subtitle');
|
|
788
|
-
$('txt-refresh').textContent = t('refresh');
|
|
789
|
-
$('txt-login').textContent = t('loginWithGoogle');
|
|
790
|
-
$('btn-lang').textContent = t('switchLang');
|
|
791
|
-
$('txt-accounts-list').textContent = t('accountsList');
|
|
792
|
-
$('txt-import-title').textContent = t('importTitle');
|
|
793
|
-
$('txt-import-desc').textContent = t('importDesc');
|
|
794
|
-
$('txt-import-json').textContent = t('importJson');
|
|
795
|
-
$('txt-import-blob').textContent = t('importBlobBtn');
|
|
796
|
-
$('txt-export-all').textContent = t('exportAll');
|
|
797
|
-
$('input-import').placeholder = t('importPlaceholder');
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
async function api(path, init) {
|
|
801
|
-
const res = await fetch('/agy/api' + path, {
|
|
802
|
-
headers: { 'content-type': 'application/json' },
|
|
803
|
-
...init
|
|
804
|
-
});
|
|
805
|
-
const data = await res.json().catch(() => ({}));
|
|
806
|
-
if (!res.ok) throw new Error(data.error || res.statusText || 'Request failed');
|
|
807
|
-
return data;
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
function renderMasterList() {
|
|
811
|
-
const listEl = $('account-list');
|
|
812
|
-
$('account-total-badge').textContent = String(accountData.length);
|
|
813
|
-
|
|
814
|
-
if (accountData.length === 0) {
|
|
815
|
-
listEl.innerHTML = '<li style="color:var(--text-tertiary);font-size:13px;padding:12px 0;">' + esc(t('noAccounts')) + '</li>';
|
|
816
|
-
return;
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
listEl.innerHTML = accountData.map((a, i) => {
|
|
820
|
-
const isSelected = i === selectedIndex;
|
|
821
|
-
const worstFrac = getWorstQuotaFraction(a);
|
|
822
|
-
const dotColor = quotaColor(worstFrac);
|
|
823
|
-
|
|
824
|
-
let stateBadge = '';
|
|
825
|
-
if (a.state === 'disabled') {
|
|
826
|
-
stateBadge = '<span class="badge badge-error">' + esc(t('stateDisabled')) + '</span>';
|
|
827
|
-
} else if (a.state === 'verification-required') {
|
|
828
|
-
stateBadge = '<span class="badge badge-warn">' + esc(t('stateVerificationRequired')) + '</span>';
|
|
829
|
-
} else if (a.state === 'cooling') {
|
|
830
|
-
stateBadge = '<span class="badge badge-warn">' + esc(t('stateCooling')) + '</span>';
|
|
831
|
-
} else if (a.rateLimits && Object.values(a.rateLimits).some((t) => t > Date.now())) {
|
|
832
|
-
stateBadge = '<span class="badge badge-warn">' + esc(t('stateRateLimited')) + '</span>';
|
|
833
|
-
} else {
|
|
834
|
-
stateBadge = '<span class="badge badge-success">' + esc(t('stateActive')) + '</span>';
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
const activeMark = a.active ? '<span class="badge badge-primary">★ ' + esc(t('activeBadge')) + '</span>' : '';
|
|
838
|
-
|
|
839
|
-
return '<li class="account-item ' + (isSelected ? 'active-item' : '') + '" onclick="selectAccount(' + i + ')">' +
|
|
840
|
-
'<div class="account-item-top">' +
|
|
841
|
-
'<span class="account-email" title="' + esc(a.email || '(no email)') + '">' + esc(a.email || '(no email)') + '</span>' +
|
|
842
|
-
'<span class="dot" style="background:' + dotColor + '" title="Quota Health"></span>' +
|
|
843
|
-
'</div>' +
|
|
844
|
-
'<div class="account-item-bottom">' + activeMark + stateBadge + '</div>' +
|
|
845
|
-
'</li>';
|
|
846
|
-
}).join('');
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
function renderDetailView() {
|
|
850
|
-
const container = $('detail-content');
|
|
851
|
-
if (accountData.length === 0) {
|
|
852
|
-
container.innerHTML = '<div style="text-align:center;padding:32px 12px;color:var(--text-tertiary);">' +
|
|
853
|
-
'<p style="font-size:15px;font-weight:500;margin-bottom:4px;">' + esc(t('noAccounts')) + '</p>' +
|
|
854
|
-
'<p style="font-size:13px;">' + esc(t('noAccountsHint')) + '</p>' +
|
|
855
|
-
'</div>';
|
|
856
|
-
return;
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
const a = accountData[selectedIndex] || accountData[0];
|
|
860
|
-
const isVerificationRequired = a.state === 'verification-required';
|
|
861
|
-
const isCooling = a.state === 'cooling' && a.cooldownUntil;
|
|
862
|
-
const coolInfo = isCooling ? ' (' + formatRelativeTime(a.cooldownUntil) + ')' : '';
|
|
863
|
-
|
|
864
|
-
const models = (a.quota && a.quota.models) || [];
|
|
865
|
-
const quotaRows = models.map((m) => {
|
|
866
|
-
const pct = m.remainingFraction != null ? Math.round(m.remainingFraction * 100) : null;
|
|
867
|
-
const color = quotaColor(m.remainingFraction);
|
|
868
|
-
const resetText = m.resetTime ? formatRelativeTime(m.resetTime) : '';
|
|
869
|
-
const exactReset = m.resetTime ? new Date(m.resetTime).toLocaleString() : '';
|
|
870
|
-
|
|
871
|
-
return '<tr>' +
|
|
872
|
-
'<td><span class="model-name" onclick="copyToClipboard(\\'' + esc(m.id) + '\\')" title="Click to copy ID">' + esc(m.id) + '</span></td>' +
|
|
873
|
-
'<td>' +
|
|
874
|
-
(pct != null
|
|
875
|
-
? '<div class="progress-bar"><div class="progress-fill" style="width:' + pct + '%;background:' + color + '"></div></div><span style="font-weight:500;font-size:12px;">' + pct + '%</span>'
|
|
876
|
-
: '<span style="color:var(--text-tertiary);font-size:12px;">' + esc(t('noQuotaReported')) + '</span>') +
|
|
877
|
-
'</td>' +
|
|
878
|
-
'<td><span class="reset-time" title="' + esc(exactReset) + '">' + esc(resetText) + '</span></td>' +
|
|
879
|
-
'<td style="text-align:right;">' +
|
|
880
|
-
'<button class="btn btn-secondary btn-sm" data-test-model="' + esc(m.id) + '">' + esc(t('testModel')) + '</button>' +
|
|
881
|
-
'</td>' +
|
|
882
|
-
'</tr>';
|
|
883
|
-
}).join('');
|
|
884
|
-
|
|
885
|
-
container.innerHTML =
|
|
886
|
-
'<div class="detail-head">' +
|
|
887
|
-
'<div class="detail-meta">' +
|
|
888
|
-
'<h2>' + esc(a.email || '(no email)') + (a.active ? ' <span class="badge badge-primary">★ ' + esc(t('activeBadge')) + '</span>' : '') + '</h2>' +
|
|
889
|
-
'<p>' + esc(t('project')) + ': <code>' + esc(a.projectId || t('defaultProject')) + '</code>' + esc(coolInfo) + '</p>' +
|
|
890
|
-
'</div>' +
|
|
891
|
-
'<div class="detail-actions">' +
|
|
892
|
-
(isVerificationRequired ? '<button class="btn btn-cta btn-sm" id="btn-action-verify">⚡ ' + esc(t('verifyCta')) + '</button>' : '') +
|
|
893
|
-
(!a.active ? '<button class="btn btn-secondary btn-sm" id="btn-action-activate">' + esc(t('activate')) + '</button>' : '') +
|
|
894
|
-
(!isVerificationRequired ? '<button class="btn btn-secondary btn-sm" id="btn-action-verify">' + esc(t('verify')) + '</button>' : '') +
|
|
895
|
-
'<button class="btn btn-secondary btn-sm" id="btn-action-export">' + esc(t('exportBlob')) + '</button>' +
|
|
896
|
-
'<button class="btn btn-secondary btn-sm" id="btn-action-fp">' + esc(t('fingerprint')) + '</button>' +
|
|
897
|
-
'<button class="btn btn-danger btn-sm" id="btn-action-delete">' + esc(t('deleteAccount')) + '</button>' +
|
|
898
|
-
'</div>' +
|
|
899
|
-
'</div>' +
|
|
900
|
-
|
|
901
|
-
'<div style="margin-top:14px;padding:10px 12px;border:1px solid var(--border-l1);border-radius:8px;background:var(--bg-surface-elevated);display:flex;flex-wrap:wrap;gap:8px;align-items:center;">' +
|
|
902
|
-
'<span style="font-size:13px;font-weight:500;white-space:nowrap;">Proxy:</span>' +
|
|
903
|
-
'<input id="proxy-input-' + selectedIndex + '" placeholder="socks5://user:pass@host:1080" style="flex:1;min-width:220px;font-family:var(--ds-font-family-code);font-size:12.5px;padding:6px 10px;background:var(--bg-input);border:1px solid var(--border-l2);border-radius:6px;color:var(--text-primary);outline:none;">' +
|
|
904
|
-
'<button class="btn btn-primary btn-sm" id="btn-proxy-save">Save</button>' +
|
|
905
|
-
'<button class="btn btn-secondary btn-sm" id="btn-proxy-clear">Clear</button>' +
|
|
906
|
-
'<button class="btn btn-secondary btn-sm" id="btn-proxy-test">Test</button>' +
|
|
907
|
-
'<span id="proxy-status" style="font-size:12px;color:var(--text-secondary);">' + ((a.proxy || a.proxyMasked) ? 'Current: ' + esc(a.proxy || a.proxyMasked) : 'No proxy (using env)') + '</span>' +
|
|
908
|
-
'</div>' +
|
|
909
|
-
|
|
910
|
-
'<div style="margin-top:16px;">' +
|
|
911
|
-
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">' +
|
|
912
|
-
'<h3 style="font-size:13.5px;font-weight:600;">' + esc(t('quotaTitle')) + ' (' + models.length + ')</h3>' +
|
|
913
|
-
(models.length > 0 ? '<button class="btn btn-secondary btn-sm" id="btn-action-test-all">⚡ ' + esc(t('testAll')) + '</button>' : '') +
|
|
914
|
-
'</div>' +
|
|
915
|
-
'<div class="quota-table-wrap">' +
|
|
916
|
-
'<table class="quota-table">' +
|
|
917
|
-
'<thead><tr><th>Model</th><th>Remaining</th><th>Reset</th><th style="text-align:right;">Action</th></tr></thead>' +
|
|
918
|
-
'<tbody>' + (quotaRows || '<tr><td colspan="4" style="color:var(--text-tertiary);text-align:center;padding:16px 0;">' + esc(t('quotaUnavailable')) + '</td></tr>') + '</tbody>' +
|
|
919
|
-
'</table>' +
|
|
920
|
-
'</div>' +
|
|
921
|
-
'</div>' +
|
|
922
|
-
|
|
923
|
-
'<details class="diagnostics" id="diagnostics-panel">' +
|
|
924
|
-
'<summary><span>' + esc(t('fingerprintTitle')) + ' & Output</span><span>▼</span></summary>' +
|
|
925
|
-
'<div class="diagnostics-body">' +
|
|
926
|
-
'<div style="display:flex;justify-content:space-between;align-items:center;">' +
|
|
927
|
-
'<span style="font-size:12px;color:var(--text-secondary);">' + esc(t('fingerprintHistory')) + ': ' + (a.fingerprintHistory || 0) + '</span>' +
|
|
928
|
-
'<button class="btn btn-secondary btn-sm" id="btn-action-regen-fp">' + esc(t('regenerateFp')) + '</button>' +
|
|
929
|
-
'</div>' +
|
|
930
|
-
'<pre class="code-view" id="diagnostics-output">' + (a.fingerprint ? esc(JSON.stringify(a.fingerprint, null, 2)) : 'No diagnostics generated yet.') + '</pre>' +
|
|
931
|
-
'</div>' +
|
|
932
|
-
'</details>';
|
|
933
|
-
|
|
934
|
-
bindDetailActions(selectedIndex);
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
function selectAccount(index) {
|
|
938
|
-
selectedIndex = index;
|
|
939
|
-
renderMasterList();
|
|
940
|
-
renderDetailView();
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
function bindDetailActions(index) {
|
|
944
|
-
const btnActivate = $('btn-action-activate');
|
|
945
|
-
if (btnActivate) btnActivate.onclick = async () => {
|
|
946
|
-
try {
|
|
947
|
-
await api('/activate', { method: 'POST', body: JSON.stringify({ index }) });
|
|
948
|
-
toast(t('activeBadge') + ' -> #' + (index + 1), 'success');
|
|
949
|
-
render();
|
|
950
|
-
} catch (e) { toast(String(e), 'error'); }
|
|
951
|
-
};
|
|
952
|
-
|
|
953
|
-
const btnVerify = $('btn-action-verify');
|
|
954
|
-
if (btnVerify) btnVerify.onclick = async () => {
|
|
955
|
-
btnVerify.disabled = true;
|
|
956
|
-
try {
|
|
957
|
-
await api('/verify', { method: 'POST', body: JSON.stringify({ index }) });
|
|
958
|
-
toast('Account #' + (index + 1) + ' verified successfully', 'success');
|
|
959
|
-
render();
|
|
960
|
-
} catch (e) { toast(String(e), 'error'); }
|
|
961
|
-
finally { btnVerify.disabled = false; }
|
|
962
|
-
};
|
|
963
|
-
|
|
964
|
-
const btnDelete = $('btn-action-delete');
|
|
965
|
-
if (btnDelete) btnDelete.onclick = async () => {
|
|
966
|
-
if (!confirm(t('confirmDelete'))) return;
|
|
967
|
-
try {
|
|
968
|
-
await api('/delete', { method: 'POST', body: JSON.stringify({ index }) });
|
|
969
|
-
toast('Account deleted', 'success');
|
|
970
|
-
if (selectedIndex >= accountData.length - 1) selectedIndex = Math.max(0, accountData.length - 2);
|
|
971
|
-
render();
|
|
972
|
-
} catch (e) { toast(String(e), 'error'); }
|
|
973
|
-
};
|
|
974
|
-
|
|
975
|
-
const btnExport = $('btn-action-export');
|
|
976
|
-
if (btnExport) btnExport.onclick = async () => {
|
|
977
|
-
try {
|
|
978
|
-
const r = await api('/export', { method: 'POST', body: JSON.stringify({ index }) });
|
|
979
|
-
copyToClipboard(r.blob, t('copyBlobMsg'));
|
|
980
|
-
$('diagnostics-panel').open = true;
|
|
981
|
-
$('diagnostics-output').textContent = r.blob;
|
|
982
|
-
} catch (e) { toast(String(e), 'error'); }
|
|
983
|
-
};
|
|
984
|
-
|
|
985
|
-
const btnFp = $('btn-action-fp');
|
|
986
|
-
if (btnFp) btnFp.onclick = async () => {
|
|
987
|
-
try {
|
|
988
|
-
const r = await api('/fingerprint', { method: 'POST', body: JSON.stringify({ index }) });
|
|
989
|
-
$('diagnostics-panel').open = true;
|
|
990
|
-
$('diagnostics-output').textContent = JSON.stringify(r.fingerprint, null, 2);
|
|
991
|
-
toast(t('copyFpMsg'), 'info');
|
|
992
|
-
} catch (e) { toast(String(e), 'error'); }
|
|
993
|
-
};
|
|
994
|
-
|
|
995
|
-
const btnRegenFp = $('btn-action-regen-fp');
|
|
996
|
-
if (btnRegenFp) btnRegenFp.onclick = async () => {
|
|
997
|
-
if (!confirm(t('confirmRegenerateFp'))) return;
|
|
998
|
-
try {
|
|
999
|
-
const r = await api('/fingerprint', { method: 'POST', body: JSON.stringify({ index, action: 'regenerate' }) });
|
|
1000
|
-
$('diagnostics-panel').open = true;
|
|
1001
|
-
$('diagnostics-output').textContent = JSON.stringify(r.fingerprint, null, 2);
|
|
1002
|
-
toast('Fingerprint regenerated', 'success');
|
|
1003
|
-
render();
|
|
1004
|
-
} catch (e) { toast(String(e), 'error'); }
|
|
1005
|
-
};
|
|
1006
|
-
|
|
1007
|
-
const btnTestAll = $('btn-action-test-all');
|
|
1008
|
-
if (btnTestAll) btnTestAll.onclick = async () => {
|
|
1009
|
-
if (!confirm(t('confirmTestAll'))) return;
|
|
1010
|
-
btnTestAll.disabled = true;
|
|
1011
|
-
const a = accountData[index];
|
|
1012
|
-
const models = ((a.quota && a.quota.models) || []).map((m) => m.id);
|
|
1013
|
-
toast(t('testing') + ' (' + models.length + ')', 'info');
|
|
1014
|
-
let okCount = 0;
|
|
1015
|
-
let failCount = 0;
|
|
1016
|
-
const lines = [];
|
|
1017
|
-
|
|
1018
|
-
for (const model of models) {
|
|
1019
|
-
try {
|
|
1020
|
-
const r = await api('/test', { method: 'POST', body: JSON.stringify({ model }) });
|
|
1021
|
-
if (r.ok) {
|
|
1022
|
-
okCount++;
|
|
1023
|
-
lines.push('OK ' + model);
|
|
1024
|
-
} else {
|
|
1025
|
-
failCount++;
|
|
1026
|
-
lines.push('FAIL ' + model + ' - ' + r.error);
|
|
1027
|
-
}
|
|
1028
|
-
} catch (err) {
|
|
1029
|
-
failCount++;
|
|
1030
|
-
lines.push('ERR ' + model + ' - ' + String(err));
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
|
-
$('diagnostics-panel').open = true;
|
|
1034
|
-
$('diagnostics-output').textContent = lines.join('\\n');
|
|
1035
|
-
toast(t('modelsTestedSummary').replace('{total}', String(models.length)).replace('{ok}', String(okCount)).replace('{failed}', String(failCount)), failCount === 0 ? 'success' : 'warn');
|
|
1036
|
-
btnTestAll.disabled = false;
|
|
1037
|
-
};
|
|
1038
|
-
|
|
1039
|
-
// Proxy actions
|
|
1040
|
-
const btnProxySave = $('btn-proxy-save');
|
|
1041
|
-
if (btnProxySave) btnProxySave.onclick = async () => {
|
|
1042
|
-
const input = $('proxy-input-' + index);
|
|
1043
|
-
const proxy = input ? input.value.trim() : '';
|
|
1044
|
-
btnProxySave.disabled = true;
|
|
1045
|
-
try {
|
|
1046
|
-
const r = await api('/proxy', { method: 'POST', body: JSON.stringify({ index, proxy }) });
|
|
1047
|
-
const masked = r.proxy || r.proxyMasked || '';
|
|
1048
|
-
const statusEl = $('proxy-status');
|
|
1049
|
-
if (statusEl) statusEl.textContent = masked ? 'Current: ' + masked : 'No proxy (using env)';
|
|
1050
|
-
toast(masked ? 'Proxy saved: ' + masked : 'Proxy cleared (using env)', 'success');
|
|
1051
|
-
render();
|
|
1052
|
-
} catch (e) { toast(String(e), 'error'); }
|
|
1053
|
-
finally { btnProxySave.disabled = false; }
|
|
1054
|
-
};
|
|
1055
|
-
const btnProxyClear = $('btn-proxy-clear');
|
|
1056
|
-
if (btnProxyClear) btnProxyClear.onclick = async () => {
|
|
1057
|
-
btnProxyClear.disabled = true;
|
|
1058
|
-
try {
|
|
1059
|
-
await api('/proxy', { method: 'POST', body: JSON.stringify({ index, proxy: '' }) });
|
|
1060
|
-
const statusEl = $('proxy-status');
|
|
1061
|
-
if (statusEl) statusEl.textContent = 'No proxy (using env)';
|
|
1062
|
-
const input = $('proxy-input-' + index);
|
|
1063
|
-
if (input) input.value = '';
|
|
1064
|
-
toast('Proxy cleared (using env)', 'success');
|
|
1065
|
-
render();
|
|
1066
|
-
} catch (e) { toast(String(e), 'error'); }
|
|
1067
|
-
finally { btnProxyClear.disabled = false; }
|
|
1068
|
-
};
|
|
1069
|
-
const btnProxyTest = $('btn-proxy-test');
|
|
1070
|
-
if (btnProxyTest) btnProxyTest.onclick = async () => {
|
|
1071
|
-
const input = $('proxy-input-' + index);
|
|
1072
|
-
const val = input ? input.value.trim() : '';
|
|
1073
|
-
const body = val ? { index, proxy: val } : { index };
|
|
1074
|
-
btnProxyTest.disabled = true;
|
|
1075
|
-
btnProxyTest.textContent = '...';
|
|
1076
|
-
try {
|
|
1077
|
-
const r = await api('/proxy/test', { method: 'POST', body: JSON.stringify(body) });
|
|
1078
|
-
const masked = r.masked ? esc(r.masked) : '';
|
|
1079
|
-
const statusEl = $('proxy-status');
|
|
1080
|
-
if (r.ok) {
|
|
1081
|
-
toast('Proxy reachable: ' + (r.masked || ''), 'success');
|
|
1082
|
-
if (statusEl) statusEl.textContent = 'Reachable: ' + (r.masked || '');
|
|
1083
|
-
} else {
|
|
1084
|
-
toast('Proxy unreachable: ' + (r.masked || '') + (r.error ? ' - ' + r.error : ''), 'error');
|
|
1085
|
-
if (statusEl) statusEl.textContent = 'Unreachable: ' + (r.masked || '') + (r.error ? ' (' + r.error + ')' : '');
|
|
1086
|
-
}
|
|
1087
|
-
} catch (e) { toast(String(e), 'error'); }
|
|
1088
|
-
finally { btnProxyTest.disabled = false; btnProxyTest.textContent = 'Test'; }
|
|
1089
|
-
};
|
|
1090
|
-
|
|
1091
|
-
// Single model tests
|
|
1092
|
-
document.querySelectorAll('[data-test-model]').forEach((btn) => {
|
|
1093
|
-
btn.onclick = async () => {
|
|
1094
|
-
const model = btn.getAttribute('data-test-model');
|
|
1095
|
-
btn.disabled = true;
|
|
1096
|
-
btn.textContent = '...';
|
|
1097
|
-
try {
|
|
1098
|
-
const r = await api('/test', { method: 'POST', body: JSON.stringify({ model }) });
|
|
1099
|
-
if (r.ok) {
|
|
1100
|
-
toast('OK: ' + model, 'success');
|
|
1101
|
-
} else {
|
|
1102
|
-
toast('FAIL: ' + model + ' - ' + r.error, 'error');
|
|
1103
|
-
}
|
|
1104
|
-
} catch (err) {
|
|
1105
|
-
toast('ERR: ' + model + ' - ' + String(err), 'error');
|
|
1106
|
-
} finally {
|
|
1107
|
-
btn.disabled = false;
|
|
1108
|
-
btn.textContent = t('testModel');
|
|
1109
|
-
}
|
|
1110
|
-
};
|
|
1111
|
-
});
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
async function render() {
|
|
1115
|
-
if (isBusy) return;
|
|
1116
|
-
isBusy = true;
|
|
1117
|
-
try {
|
|
1118
|
-
const data = await api('/accounts');
|
|
1119
|
-
accountData = data.accounts || [];
|
|
1120
|
-
if (selectedIndex >= accountData.length) selectedIndex = Math.max(0, accountData.length - 1);
|
|
1121
|
-
renderMasterList();
|
|
1122
|
-
renderDetailView();
|
|
1123
|
-
} catch (e) {
|
|
1124
|
-
toast(String(e), 'error');
|
|
1125
|
-
} finally {
|
|
1126
|
-
isBusy = false;
|
|
1127
|
-
}
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
// Event Handlers
|
|
1131
|
-
$('btn-refresh').onclick = render;
|
|
1132
|
-
|
|
1133
|
-
$('btn-lang').onclick = () => {
|
|
1134
|
-
currentLang = currentLang === 'zh' ? 'en' : 'zh';
|
|
1135
|
-
localStorage.setItem('agy_lang', currentLang);
|
|
1136
|
-
applyI18nLabels();
|
|
1137
|
-
renderMasterList();
|
|
1138
|
-
renderDetailView();
|
|
1139
|
-
};
|
|
1140
|
-
|
|
1141
|
-
$('btn-login').onclick = async () => {
|
|
1142
|
-
try {
|
|
1143
|
-
const { url } = await api('/auth-url', { method: 'POST' });
|
|
1144
|
-
window.open(url, '_blank');
|
|
1145
|
-
toast(t('windowClosing'), 'info');
|
|
1146
|
-
} catch (e) { toast(String(e), 'error'); }
|
|
1147
|
-
};
|
|
1148
|
-
|
|
1149
|
-
const importLines = () => $('input-import').value.split('\\n')
|
|
1150
|
-
.map((s) => s.trim())
|
|
1151
|
-
.filter(Boolean);
|
|
1152
|
-
|
|
1153
|
-
const doImport = async (kind) => {
|
|
1154
|
-
const sources = importLines();
|
|
1155
|
-
if (sources.length === 0) return;
|
|
1156
|
-
try {
|
|
1157
|
-
const r = await api('/import', { method: 'POST', body: JSON.stringify({ kind, sources }) });
|
|
1158
|
-
if (r.errors && r.errors.length > 0) toast(r.errors[0], 'error');
|
|
1159
|
-
toast(t('importBatchResult').replace('{imported}', r.imported).replace('{replaced}', r.replaced), 'success');
|
|
1160
|
-
$('input-import').value = '';
|
|
1161
|
-
render();
|
|
1162
|
-
} catch (e) { toast(String(e), 'error'); }
|
|
1163
|
-
};
|
|
1164
|
-
|
|
1165
|
-
$('btn-import-json').onclick = () => doImport('json');
|
|
1166
|
-
$('btn-import-blob').onclick = () => doImport('blob');
|
|
1167
|
-
|
|
1168
|
-
const btnExportAll = $('btn-export-all');
|
|
1169
|
-
if (btnExportAll) btnExportAll.onclick = async () => {
|
|
1170
|
-
try {
|
|
1171
|
-
const r = await api('/export-all', { method: 'POST' });
|
|
1172
|
-
if (!r.blobs || r.blobs.length === 0) { toast('No accounts to export', 'info'); return; }
|
|
1173
|
-
const text = r.blobs.map((b) => b.blob).join('\\n');
|
|
1174
|
-
copyToClipboard(text, t('copyAllMsg'));
|
|
1175
|
-
$('diagnostics-panel').open = true;
|
|
1176
|
-
$('diagnostics-output').textContent = text;
|
|
1177
|
-
} catch (e) { toast(String(e), 'error'); }
|
|
1178
|
-
};
|
|
1179
|
-
|
|
1180
|
-
// Auto-refresh when tab receives focus or receives postMessage from login popup
|
|
1181
|
-
window.addEventListener('focus', render);
|
|
1182
|
-
window.addEventListener('message', (ev) => {
|
|
1183
|
-
if (ev.data && ev.data.type === 'agy_login_success') {
|
|
1184
|
-
toast(t('loginSuccessTitle'), 'success');
|
|
1185
|
-
render();
|
|
1186
|
-
}
|
|
1187
|
-
});
|
|
1188
|
-
|
|
1189
|
-
// Init
|
|
1190
|
-
applyI18nLabels();
|
|
1191
|
-
render();
|
|
1192
|
-
<\/script>
|
|
1193
|
-
</body>
|
|
1194
|
-
</html>`;
|
|
20
|
+
/** Fail one RPC with a message the UI can show verbatim. */
|
|
21
|
+
function fail(message) {
|
|
22
|
+
throw new Error(message);
|
|
23
|
+
}
|
|
24
|
+
function asIndex(payload) {
|
|
25
|
+
const index = Number(payload?.index);
|
|
26
|
+
if (!Number.isInteger(index) || index < 0) fail("invalid index");
|
|
27
|
+
return index;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Flatten one account's ledger entry for transport.
|
|
31
|
+
*
|
|
32
|
+
* `models` and `lastUsedAt` are deliberately NOT carried: the per-model table
|
|
33
|
+
* that read `models` was removed (the cumulative metric strip states the same
|
|
34
|
+
* figures better), and the `lastUsedAt` ordering rule went with it. Sending
|
|
35
|
+
* fields no client reads is a wire surface with no consumer, which reads as
|
|
36
|
+
* intentional to the next person and invites a stale assumption.
|
|
37
|
+
*/
|
|
38
|
+
function toAccountUsageView(usage) {
|
|
39
|
+
if (usage === void 0) return null;
|
|
40
|
+
return {
|
|
41
|
+
totals: usage.totals,
|
|
42
|
+
sources: usage.sources
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function createAgyManagement(options) {
|
|
46
|
+
const { store, sessions, stats, modelVisibility, listAllModels, baseUrl, notifyModelsChanged } = options;
|
|
47
|
+
const thinkingBudget = options.thinkingBudget;
|
|
48
|
+
/** Authorizations issued by `auth.url`, keyed by raw state. */
|
|
49
|
+
const pendingAuth = /* @__PURE__ */ new Map();
|
|
50
|
+
const PENDING_AUTH_TTL_MS = 6e5;
|
|
51
|
+
const prunePendingAuth = (now = Date.now()) => {
|
|
52
|
+
for (const [key, entry] of pendingAuth) if (entry.expiresAt <= now) pendingAuth.delete(key);
|
|
53
|
+
while (pendingAuth.size > 100) {
|
|
54
|
+
let oldestKey = null;
|
|
55
|
+
let oldestExpiry = Infinity;
|
|
56
|
+
for (const [key, entry] of pendingAuth) if (entry.expiresAt < oldestExpiry) {
|
|
57
|
+
oldestExpiry = entry.expiresAt;
|
|
58
|
+
oldestKey = key;
|
|
59
|
+
}
|
|
60
|
+
if (oldestKey === null) break;
|
|
61
|
+
pendingAuth.delete(oldestKey);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
const listAccounts = async () => {
|
|
65
|
+
const storage = await store.load();
|
|
66
|
+
const now = Date.now();
|
|
67
|
+
for (const account of storage.accounts) clearExpiredState(account, now);
|
|
68
|
+
const ledger = stats.snapshot();
|
|
69
|
+
const rows = [];
|
|
70
|
+
for (const [index, account] of storage.accounts.entries()) {
|
|
71
|
+
const state = account.enabled === false ? "disabled" : account.verificationRequired === true ? "verification-required" : account.coolingDownUntil !== void 0 && account.coolingDownUntil > now ? "cooling" : "active";
|
|
72
|
+
const key = account.email ?? account.id;
|
|
73
|
+
rows.push({
|
|
74
|
+
index,
|
|
75
|
+
email: account.email ?? null,
|
|
76
|
+
projectId: account.projectId ?? null,
|
|
77
|
+
active: index === storage.activeIndex && account.enabled !== false,
|
|
78
|
+
state,
|
|
79
|
+
cooldownUntil: account.coolingDownUntil !== void 0 && account.coolingDownUntil > now ? new Date(account.coolingDownUntil).toISOString() : null,
|
|
80
|
+
cooldownReason: account.cooldownReason ?? null,
|
|
81
|
+
cooldownSetAt: account.cooldownSetAt === void 0 ? null : new Date(account.cooldownSetAt).toISOString(),
|
|
82
|
+
/**
|
|
83
|
+
* Appeal link from an upstream verification challenge. Surfaced, never
|
|
84
|
+
* followed automatically: only the account owner can complete it, and the
|
|
85
|
+
* account returns to service on its own once the wall clears.
|
|
86
|
+
*/
|
|
87
|
+
verificationUrl: account.verificationUrl ?? null,
|
|
88
|
+
verificationRequired: account.verificationRequired === true && account.enabled !== false,
|
|
89
|
+
rateLimits: account.rateLimitResetTimes ?? null,
|
|
90
|
+
fingerprint: account.fingerprint ? {
|
|
91
|
+
userAgent: account.fingerprint.userAgent,
|
|
92
|
+
deviceId: account.fingerprint.deviceId,
|
|
93
|
+
createdAt: account.fingerprint.createdAt
|
|
94
|
+
} : null,
|
|
95
|
+
fingerprintHistory: (account.fingerprintHistory ?? []).length,
|
|
96
|
+
proxy: account.proxy ? maskProxyUrl(account.proxy) : null,
|
|
97
|
+
limits: account.cachedLimits?.groups ?? null,
|
|
98
|
+
limitsUpdatedAt: account.cachedLimits?.updatedAt ?? null,
|
|
99
|
+
usage: key === void 0 ? null : toAccountUsageView(ledger.accounts[key])
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return rows;
|
|
103
|
+
};
|
|
104
|
+
/** Fold the ledger into the Usage tab's view. */
|
|
105
|
+
const statsView = () => {
|
|
106
|
+
const doc = stats.snapshot();
|
|
107
|
+
const now = Date.now();
|
|
108
|
+
const byModel = /* @__PURE__ */ new Map();
|
|
109
|
+
for (const usage of Object.values(doc.accounts)) for (const [model, counters] of Object.entries(usage.models)) {
|
|
110
|
+
const current = byModel.get(model);
|
|
111
|
+
if (current === void 0) byModel.set(model, { ...counters });
|
|
112
|
+
else for (const key of Object.keys(current)) current[key] += counters[key];
|
|
113
|
+
}
|
|
114
|
+
const allModels = [...byModel.entries()].map(([model, counters]) => ({
|
|
115
|
+
model,
|
|
116
|
+
counters
|
|
117
|
+
})).sort((a, b) => b.counters.requests - a.counters.requests);
|
|
118
|
+
const allAccounts = Object.entries(doc.accounts).map(([account, usage]) => ({
|
|
119
|
+
account,
|
|
120
|
+
counters: usage.totals
|
|
121
|
+
})).sort((a, b) => b.counters.requests - a.counters.requests);
|
|
122
|
+
const breakdown = (days) => {
|
|
123
|
+
const { totals, models, accounts } = foldWindowBreakdown(doc, days, now);
|
|
124
|
+
return {
|
|
125
|
+
counters: totals,
|
|
126
|
+
models,
|
|
127
|
+
accounts
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
return {
|
|
131
|
+
since: doc.totals.requests > 0 ? doc.since : null,
|
|
132
|
+
all: {
|
|
133
|
+
counters: doc.totals,
|
|
134
|
+
models: allModels,
|
|
135
|
+
accounts: allAccounts
|
|
136
|
+
},
|
|
137
|
+
today: breakdown(1),
|
|
138
|
+
week: breakdown(7),
|
|
139
|
+
month: breakdown(30)
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
const methods = {
|
|
143
|
+
"account.list": async () => ({ accounts: await listAccounts() }),
|
|
144
|
+
"account.activate": async (payload) => {
|
|
145
|
+
const index = asIndex(payload);
|
|
146
|
+
await store.mutate((storage) => {
|
|
147
|
+
if (index >= storage.accounts.length) fail("account not found");
|
|
148
|
+
storage.activeIndex = index;
|
|
149
|
+
});
|
|
150
|
+
return {
|
|
151
|
+
ok: true,
|
|
152
|
+
index
|
|
153
|
+
};
|
|
154
|
+
},
|
|
155
|
+
"account.delete": async (payload) => {
|
|
156
|
+
const index = asIndex(payload);
|
|
157
|
+
await store.mutate((storage) => {
|
|
158
|
+
if (index >= storage.accounts.length) fail("account not found");
|
|
159
|
+
storage.accounts.splice(index, 1);
|
|
160
|
+
if (storage.activeIndex >= storage.accounts.length) storage.activeIndex = 0;
|
|
161
|
+
});
|
|
162
|
+
return { ok: true };
|
|
163
|
+
},
|
|
164
|
+
"account.verify": async (payload) => sessions.verifyAccount(asIndex(payload)),
|
|
165
|
+
"account.health": async (payload) => {
|
|
166
|
+
const raw = payload?.indices;
|
|
167
|
+
const indices = Array.isArray(raw) ? raw.map((value) => Number(value)) : void 0;
|
|
168
|
+
return { results: await sessions.checkAccounts(indices) };
|
|
169
|
+
},
|
|
170
|
+
/**
|
|
171
|
+
* Refresh and return the 5h/weekly windows for every enabled account.
|
|
172
|
+
*
|
|
173
|
+
* A SEPARATE call rather than a field on `account.list`, because that reply
|
|
174
|
+
* is deliberately probe-free (see the note above its return): folding an
|
|
175
|
+
* upstream call into it made the whole accounts+usage view wait on the
|
|
176
|
+
* network and show "no accounts" behind a spinner. Here the client renders
|
|
177
|
+
* the list from `account.list` immediately and merges these in when they
|
|
178
|
+
* arrive, so a slow probe costs a placeholder, not the page.
|
|
179
|
+
*
|
|
180
|
+
* Server-side this is TTL-gated, so the upstream call happens at most once
|
|
181
|
+
* per window rather than once per view. `force` bypasses that gate for an
|
|
182
|
+
* EXPLICIT refresh — without it the toolbar's Refresh button could not
|
|
183
|
+
* deliver "the latest numbers now", which is the only reason to click it
|
|
184
|
+
* while the snapshot is still inside its TTL.
|
|
185
|
+
*/
|
|
186
|
+
"account.limits": async (payload) => {
|
|
187
|
+
const force = payload?.force === true;
|
|
188
|
+
const storage = await store.load();
|
|
189
|
+
const result = await sessions.refreshLimits(storage, { force }).catch(() => ({
|
|
190
|
+
measured: [],
|
|
191
|
+
failed: [],
|
|
192
|
+
skipped: 0
|
|
193
|
+
}));
|
|
194
|
+
return {
|
|
195
|
+
limits: (await store.load()).accounts.map((account, index) => ({
|
|
196
|
+
index,
|
|
197
|
+
groups: account.cachedLimits?.groups ?? null,
|
|
198
|
+
updatedAt: account.cachedLimits?.updatedAt ?? null
|
|
199
|
+
})),
|
|
200
|
+
measured: result.measured.length,
|
|
201
|
+
failed: result.failed.length,
|
|
202
|
+
skipped: result.skipped
|
|
203
|
+
};
|
|
204
|
+
},
|
|
205
|
+
"account.test": async (payload) => {
|
|
206
|
+
const model = payload?.model;
|
|
207
|
+
if (typeof model !== "string" || model === "") fail("model is required");
|
|
208
|
+
const rawIndex = payload?.index;
|
|
209
|
+
return sessions.testCall(model, { ...rawIndex === void 0 ? {} : { accountIndex: asIndex(payload) } });
|
|
210
|
+
},
|
|
211
|
+
"account.export": async (payload) => sessions.exportBlob(asIndex(payload)),
|
|
212
|
+
"account.exportAll": async () => {
|
|
213
|
+
const storage = await store.load();
|
|
214
|
+
const blobs = [];
|
|
215
|
+
for (let index = 0; index < storage.accounts.length; index++) {
|
|
216
|
+
const result = await sessions.exportBlob(index);
|
|
217
|
+
if (result.blob !== void 0) blobs.push({
|
|
218
|
+
index,
|
|
219
|
+
blob: result.blob
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
return { blobs };
|
|
223
|
+
},
|
|
224
|
+
"account.import": async (payload) => {
|
|
225
|
+
const body = payload;
|
|
226
|
+
const kind = body?.kind === "blob" ? "blob" : "json";
|
|
227
|
+
const sources = Array.isArray(body?.sources) ? body.sources : [];
|
|
228
|
+
if (sources.length === 0) fail("nothing to import");
|
|
229
|
+
const result = await importManySources(sources.map((source) => ({
|
|
230
|
+
source,
|
|
231
|
+
kind
|
|
232
|
+
})), store, { overwriteExisting: true });
|
|
233
|
+
return {
|
|
234
|
+
imported: result.imported,
|
|
235
|
+
replaced: result.replaced,
|
|
236
|
+
errors: result.errors.map((error) => String(error))
|
|
237
|
+
};
|
|
238
|
+
},
|
|
239
|
+
"account.fingerprint": async (payload) => {
|
|
240
|
+
const index = asIndex(payload);
|
|
241
|
+
const action = payload?.action === "regenerate" ? "regenerate" : "show";
|
|
242
|
+
return store.mutate(async (storage) => {
|
|
243
|
+
const account = storage.accounts[index];
|
|
244
|
+
if (!account) fail("account not found");
|
|
245
|
+
if (action === "regenerate") {
|
|
246
|
+
const fresh = generateFingerprint(void 0, currentAgyVersion());
|
|
247
|
+
account.fingerprint = fresh;
|
|
248
|
+
account.fingerprintHistory = recordFingerprintVersion(account.fingerprintHistory, fresh, "regenerated");
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
action,
|
|
252
|
+
fingerprint: account.fingerprint ? {
|
|
253
|
+
userAgent: account.fingerprint.userAgent,
|
|
254
|
+
deviceId: account.fingerprint.deviceId,
|
|
255
|
+
createdAt: account.fingerprint.createdAt
|
|
256
|
+
} : null,
|
|
257
|
+
history: account.fingerprintHistory?.length ?? 0
|
|
258
|
+
};
|
|
259
|
+
});
|
|
260
|
+
},
|
|
261
|
+
"account.proxy": async (payload) => {
|
|
262
|
+
const index = asIndex(payload);
|
|
263
|
+
const raw = payload?.proxy;
|
|
264
|
+
const proxy = typeof raw === "string" ? raw : "";
|
|
265
|
+
if (proxy.trim() === "") {
|
|
266
|
+
await store.mutate((storage) => {
|
|
267
|
+
const account = storage.accounts[index];
|
|
268
|
+
if (!account) fail("account not found");
|
|
269
|
+
delete account.proxy;
|
|
270
|
+
});
|
|
271
|
+
return {
|
|
272
|
+
ok: true,
|
|
273
|
+
proxy: null,
|
|
274
|
+
proxyMasked: null,
|
|
275
|
+
rawLogs: null
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
let normalized;
|
|
279
|
+
try {
|
|
280
|
+
normalized = normalizeProxyUrl(proxy);
|
|
281
|
+
} catch (error) {
|
|
282
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
283
|
+
}
|
|
284
|
+
await store.mutate((storage) => {
|
|
285
|
+
const account = storage.accounts[index];
|
|
286
|
+
if (!account) fail("account not found");
|
|
287
|
+
account.proxy = normalized;
|
|
288
|
+
});
|
|
289
|
+
return {
|
|
290
|
+
ok: true,
|
|
291
|
+
proxy: maskProxyUrl(normalized),
|
|
292
|
+
proxyMasked: maskProxyUrl(normalized),
|
|
293
|
+
rawLogs: proxyUrlForLogs(normalized)
|
|
294
|
+
};
|
|
295
|
+
},
|
|
296
|
+
"account.proxyTest": async (payload) => {
|
|
297
|
+
const body = payload;
|
|
298
|
+
const hasExplicit = typeof body?.proxy === "string" && body.proxy.trim() !== "";
|
|
299
|
+
let target;
|
|
300
|
+
if (hasExplicit) try {
|
|
301
|
+
target = normalizeProxyUrl(body.proxy);
|
|
302
|
+
} catch (error) {
|
|
303
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
const index = asIndex(payload);
|
|
307
|
+
const account = (await store.load()).accounts[index];
|
|
308
|
+
if (!account) fail("account not found");
|
|
309
|
+
if (!account.proxy) fail("no proxy configured for this account");
|
|
310
|
+
target = account.proxy;
|
|
311
|
+
}
|
|
312
|
+
const masked = maskProxyUrl(target) ?? proxyUrlForLogs(target);
|
|
313
|
+
try {
|
|
314
|
+
const ok = await isProxyReachable(target, 2e3);
|
|
315
|
+
return ok ? {
|
|
316
|
+
ok,
|
|
317
|
+
masked
|
|
318
|
+
} : {
|
|
319
|
+
ok,
|
|
320
|
+
masked,
|
|
321
|
+
error: `proxy unreachable: ${masked}`
|
|
322
|
+
};
|
|
323
|
+
} catch (error) {
|
|
324
|
+
return {
|
|
325
|
+
ok: false,
|
|
326
|
+
masked,
|
|
327
|
+
error: error instanceof Error ? error.message : String(error)
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
"auth.url": async () => {
|
|
332
|
+
const authorization = await authorizeAntigravity(`${baseUrl}/agy/oauth-callback`);
|
|
333
|
+
prunePendingAuth();
|
|
334
|
+
pendingAuth.set(authorization.state, {
|
|
335
|
+
verifier: authorization.verifier,
|
|
336
|
+
expiresAt: Date.now() + PENDING_AUTH_TTL_MS
|
|
337
|
+
});
|
|
338
|
+
return { url: authorization.url };
|
|
339
|
+
},
|
|
340
|
+
"model.list": async () => {
|
|
341
|
+
const session = await sessions.getSession().catch(() => void 0);
|
|
342
|
+
if (session === void 0) fail("No agy account configured — run `dsh-agy login` first.");
|
|
343
|
+
const models = await listAllModels();
|
|
344
|
+
const hidden = modelVisibility.disabledFor("agy");
|
|
345
|
+
const rows = models.map((model) => ({
|
|
346
|
+
id: model.id,
|
|
347
|
+
name: model.name,
|
|
348
|
+
disabled: hidden.has(model.id)
|
|
349
|
+
}));
|
|
350
|
+
return {
|
|
351
|
+
account: session.account.email ?? null,
|
|
352
|
+
models: rows
|
|
353
|
+
};
|
|
354
|
+
},
|
|
355
|
+
"model.setDisabled": async (payload) => {
|
|
356
|
+
const body = payload;
|
|
357
|
+
const modelId = body?.modelId;
|
|
358
|
+
if (typeof modelId !== "string" || modelId === "") fail("modelId is required");
|
|
359
|
+
const disabled = body?.disabled === true;
|
|
360
|
+
modelVisibility.setDisabled("agy", modelId, disabled);
|
|
361
|
+
notifyModelsChanged();
|
|
362
|
+
return {
|
|
363
|
+
modelId,
|
|
364
|
+
disabled
|
|
365
|
+
};
|
|
366
|
+
},
|
|
367
|
+
"thinking.get": async () => ({
|
|
368
|
+
budgets: thinkingBudget.all(),
|
|
369
|
+
min: -1,
|
|
370
|
+
max: THINKING_BUDGET_MAX,
|
|
371
|
+
tieredBudget: thinkingBudget.tiered() ?? null,
|
|
372
|
+
claudeBudget: thinkingBudget.claude() ?? null,
|
|
373
|
+
claudeMin: CLAUDE_BUDGET_MIN,
|
|
374
|
+
claudeMax: CLAUDE_BUDGET_MAX
|
|
375
|
+
}),
|
|
376
|
+
"thinking.setTiered": async (payload) => {
|
|
377
|
+
const raw = payload?.budget;
|
|
378
|
+
if (raw !== void 0 && raw !== null && typeof raw !== "number") fail("budget must be a number");
|
|
379
|
+
try {
|
|
380
|
+
return { tieredBudget: thinkingBudget.setTiered(raw === void 0 || raw === null ? void 0 : raw) ?? null };
|
|
381
|
+
} catch (error) {
|
|
382
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
383
|
+
}
|
|
384
|
+
},
|
|
385
|
+
"thinking.setClaude": async (payload) => {
|
|
386
|
+
const raw = payload?.budget;
|
|
387
|
+
if (raw !== void 0 && raw !== null && typeof raw !== "number") fail("budget must be a number");
|
|
388
|
+
try {
|
|
389
|
+
return { claudeBudget: thinkingBudget.setClaude(raw === void 0 || raw === null ? void 0 : raw) ?? null };
|
|
390
|
+
} catch (error) {
|
|
391
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
"thinking.set": async (payload) => {
|
|
395
|
+
const body = payload;
|
|
396
|
+
const level = body?.level;
|
|
397
|
+
if (typeof level !== "string" || level === "") fail("level is required");
|
|
398
|
+
const raw = body?.budget;
|
|
399
|
+
if (raw !== void 0 && raw !== null && typeof raw !== "number") fail("budget must be a number");
|
|
400
|
+
const value = raw === void 0 || raw === null ? void 0 : raw;
|
|
401
|
+
try {
|
|
402
|
+
return { budgets: thinkingBudget.set(level, value) };
|
|
403
|
+
} catch (error) {
|
|
404
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
"stats.get": async () => statsView()
|
|
408
|
+
};
|
|
409
|
+
return {
|
|
410
|
+
async call(method, payload) {
|
|
411
|
+
const handler = methods[method];
|
|
412
|
+
if (handler === void 0) fail(`unknown method: ${method}`);
|
|
413
|
+
return handler(payload);
|
|
414
|
+
},
|
|
415
|
+
async handleCallback(params) {
|
|
416
|
+
const code = params.get("code");
|
|
417
|
+
const state = params.get("state");
|
|
418
|
+
if (!code || !state) return {
|
|
419
|
+
ok: false,
|
|
420
|
+
error: "Missing code or state parameter"
|
|
421
|
+
};
|
|
422
|
+
const expected = pendingAuth.get(state);
|
|
423
|
+
if (expected === void 0) return {
|
|
424
|
+
ok: false,
|
|
425
|
+
error: "Unknown or expired authorization — please start a new login."
|
|
426
|
+
};
|
|
427
|
+
pendingAuth.delete(state);
|
|
428
|
+
const redirectUri = `${baseUrl}/agy/oauth-callback`;
|
|
429
|
+
const result = await exchangeAntigravity(code, state, redirectUri, expected.verifier);
|
|
430
|
+
if (result.type === "failed") return {
|
|
431
|
+
ok: false,
|
|
432
|
+
error: result.error
|
|
433
|
+
};
|
|
434
|
+
await upsertImportedAccount(store, {
|
|
435
|
+
accessToken: result.access,
|
|
436
|
+
refreshToken: result.refresh.split("|")[0],
|
|
437
|
+
tokenType: "Bearer",
|
|
438
|
+
expiresAt: new Date(result.expires).toISOString(),
|
|
439
|
+
authMethod: "oauth",
|
|
440
|
+
email: result.email ?? null,
|
|
441
|
+
projectId: result.projectId || null,
|
|
442
|
+
clientId: result.clientId || null
|
|
443
|
+
}, { overwriteExisting: true });
|
|
444
|
+
return {
|
|
445
|
+
ok: true,
|
|
446
|
+
email: result.email ?? null
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
//#endregion
|
|
452
|
+
//#region src/web/i18n.ts
|
|
453
|
+
const I18N_DICT = {
|
|
454
|
+
en: {
|
|
455
|
+
loginSuccessTitle: "Sign-in Successful",
|
|
456
|
+
loginSuccessDesc: "Your Antigravity account has been authorized and saved.",
|
|
457
|
+
loginFailedTitle: "Sign-in Failed",
|
|
458
|
+
windowClosing: "This window will close automatically in a moment..."
|
|
459
|
+
},
|
|
460
|
+
zh: {
|
|
461
|
+
loginSuccessTitle: "授权登录成功",
|
|
462
|
+
loginSuccessDesc: "Antigravity 账号已成功接入并保存。",
|
|
463
|
+
loginFailedTitle: "授权登录失败",
|
|
464
|
+
windowClosing: "此窗口即将在 2 秒内自动关闭..."
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
//#endregion
|
|
468
|
+
//#region src/web/page.ts
|
|
469
|
+
/**
|
|
470
|
+
* Browser-facing HTML for the agy OAuth callback.
|
|
471
|
+
*
|
|
472
|
+
* This is the only page agy serves. The account/statistics dashboard that used
|
|
473
|
+
* to live at `/agy` is gone: that surface is now the Settings section rendered
|
|
474
|
+
* by `src/client/` over the `/api/agy` RPC, so there is no longer a second UI to
|
|
475
|
+
* keep in step with it.
|
|
476
|
+
*
|
|
477
|
+
* The callback stays a real page because Google redirects the browser here.
|
|
478
|
+
*/
|
|
479
|
+
/**
|
|
480
|
+
* Escape text for an HTML text/attribute position.
|
|
481
|
+
*
|
|
482
|
+
* Every interpolation below except the static markup is attacker-influenced or
|
|
483
|
+
* upstream-influenced: `error` is the token endpoint's raw response body, and
|
|
484
|
+
* `email` comes from Google's userinfo. This page is served by the SAME web
|
|
485
|
+
* server, and therefore the same origin, as the DSH GUI — so injected script
|
|
486
|
+
* here would run with the GUI's session and could reach `/api/agy`
|
|
487
|
+
* (`account.exportAll` returns live credential blobs). That chain is why the
|
|
488
|
+
* escaping matters more than the narrow trigger suggests.
|
|
489
|
+
*
|
|
490
|
+
* Text in markup goes through {@link escapeHtml}; text inside the inline
|
|
491
|
+
* `<script>` goes through {@link jsonForInlineScript}. Neither is optional.
|
|
492
|
+
*/
|
|
493
|
+
function escapeHtml(value) {
|
|
494
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Serialize a value for interpolation into the inline `<script>`.
|
|
498
|
+
*
|
|
499
|
+
* `JSON.stringify` escapes for a JS string context only, NOT for the HTML script
|
|
500
|
+
* data state: it leaves `<` alone, so an email or error body containing
|
|
501
|
+
* `<\/script>` terminates the element early and everything after it is parsed as
|
|
502
|
+
* markup. Escaping the three HTML-significant characters as `\uXXXX` keeps the
|
|
503
|
+
* JSON value identical while making the byte sequence unrepresentable in the
|
|
504
|
+
* source. (The `JSON.stringify` output above is a valid JS string either way,
|
|
505
|
+
* because a `\u003c` escape and a literal `<` denote the same character.)
|
|
506
|
+
*/
|
|
507
|
+
function jsonForInlineScript(value) {
|
|
508
|
+
return JSON.stringify(value).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
|
|
1195
509
|
}
|
|
1196
510
|
function renderCallbackHtml(options) {
|
|
1197
511
|
const { ok, error, email, baseUrl } = options;
|
|
1198
|
-
const i18nJson =
|
|
512
|
+
const i18nJson = jsonForInlineScript(I18N_DICT);
|
|
1199
513
|
return `<!doctype html>
|
|
1200
514
|
<html lang="en">
|
|
1201
515
|
<head>
|
|
@@ -1287,14 +601,14 @@ function renderCallbackHtml(options) {
|
|
|
1287
601
|
${ok ? `
|
|
1288
602
|
<div class="icon-wrap icon-success">✓</div>
|
|
1289
603
|
<h1 id="title">Sign-in Successful</h1>
|
|
1290
|
-
<p id="desc">Your Antigravity account ${email ? "<strong>" + email + "</strong> " : ""}has been authorized and saved.</p>
|
|
604
|
+
<p id="desc">Your Antigravity account ${email ? "<strong>" + escapeHtml(email) + "</strong> " : ""}has been authorized and saved.</p>
|
|
1291
605
|
<p style="font-size:12px;opacity:0.8;" id="closing">This window will close automatically...</p>
|
|
1292
606
|
<button class="btn" onclick="window.close()">Close Window</button>
|
|
1293
607
|
` : `
|
|
1294
608
|
<div class="icon-wrap icon-error">✕</div>
|
|
1295
609
|
<h1 id="title">Sign-in Failed</h1>
|
|
1296
|
-
<p id="desc">Error details: ${error || "Unknown error"}</p>
|
|
1297
|
-
<a class="btn btn-primary" href="${baseUrl}/
|
|
610
|
+
<p id="desc">Error details: ${escapeHtml(error || "Unknown error")}</p>
|
|
611
|
+
<a class="btn btn-primary" href="${escapeHtml(baseUrl)}/">Return to Settings</a>
|
|
1298
612
|
`}
|
|
1299
613
|
</div>
|
|
1300
614
|
<script>
|
|
@@ -1304,13 +618,18 @@ function renderCallbackHtml(options) {
|
|
|
1304
618
|
|
|
1305
619
|
if (${ok ? "true" : "false"}) {
|
|
1306
620
|
document.getElementById('title').textContent = dict.loginSuccessTitle;
|
|
1307
|
-
|
|
621
|
+
// textContent, not innerHTML: an email containing markup must never be
|
|
622
|
+
// parsed as HTML. The string itself is serialized for the script data
|
|
623
|
+
// state by jsonForInlineScript, so it cannot terminate this element.
|
|
624
|
+
document.getElementById('desc').textContent = dict.loginSuccessDesc + (${jsonForInlineScript(email ? " (" + email + ")" : "")});
|
|
1308
625
|
document.getElementById('closing').textContent = dict.windowClosing;
|
|
1309
626
|
|
|
1310
627
|
// Notify parent window & close
|
|
1311
628
|
try {
|
|
1312
629
|
if (window.opener) {
|
|
1313
|
-
|
|
630
|
+
// Target this page's own origin rather than '*', so the message cannot
|
|
631
|
+
// be delivered to an unrelated opener.
|
|
632
|
+
window.opener.postMessage({ type: 'agy_login_success' }, window.location.origin);
|
|
1314
633
|
}
|
|
1315
634
|
} catch (e) {}
|
|
1316
635
|
setTimeout(() => {
|
|
@@ -1324,529 +643,160 @@ function renderCallbackHtml(options) {
|
|
|
1324
643
|
</html>`;
|
|
1325
644
|
}
|
|
1326
645
|
//#endregion
|
|
1327
|
-
//#region src/web/
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
646
|
+
//#region src/web/plugin.ts
|
|
647
|
+
const name = "dsh-agy-web";
|
|
648
|
+
/** The one service every composition provides; the rest are resolved lazily. */
|
|
649
|
+
const inject = ["llm"];
|
|
650
|
+
function apply(ctx) {
|
|
651
|
+
if (isAgyDisabled()) {
|
|
652
|
+
ctx.logger.warn("[dsh-agy] disabled by DSH_AGY_DISABLE=1 — skipping web registration");
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
ctx.inject(["webServer"], (webCtx) => {
|
|
656
|
+
const webServer = webCtx.get("webServer");
|
|
657
|
+
if (!webServer) return;
|
|
658
|
+
webCtx.effect(() => registerAgyWeb(webCtx, webServer));
|
|
659
|
+
});
|
|
1337
660
|
}
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
proxyMasked: account.proxy ? maskProxyUrl(account.proxy) : null
|
|
1378
|
-
};
|
|
1379
|
-
list.push(entry);
|
|
1380
|
-
}
|
|
1381
|
-
for (const entry of list) {
|
|
1382
|
-
const session = await sessions.getSession().catch(() => void 0);
|
|
1383
|
-
if (!session || session.index !== entry.index) continue;
|
|
1384
|
-
try {
|
|
1385
|
-
const { fetchAvailableModels } = await import("../models-ClGTQBLY.mjs").then((n) => n.r);
|
|
1386
|
-
const discovered = await fetchAvailableModels(session.auth.access, session.account.projectId, accountFetch({ proxyUrl: session.account.proxy }));
|
|
1387
|
-
const models = Object.entries(discovered.models ?? {});
|
|
1388
|
-
if (models.length > 0) {
|
|
1389
|
-
const quotaRows = models.map(([id, m]) => ({
|
|
1390
|
-
id,
|
|
1391
|
-
remainingFraction: typeof m.quotaInfo?.remainingFraction === "number" ? Math.max(0, Math.min(1, m.quotaInfo.remainingFraction)) : null,
|
|
1392
|
-
resetTime: m.quotaInfo?.resetTime ?? null
|
|
1393
|
-
})).sort((a, b) => (a.remainingFraction ?? -1) - (b.remainingFraction ?? -1));
|
|
1394
|
-
entry.quota = {
|
|
1395
|
-
modelCount: models.length,
|
|
1396
|
-
models: quotaRows
|
|
1397
|
-
};
|
|
1398
|
-
}
|
|
1399
|
-
} catch {}
|
|
661
|
+
/**
|
|
662
|
+
* Register the OAuth callback route, and the management RPC once `connection`
|
|
663
|
+
* appears.
|
|
664
|
+
* @param ctx - the web-server-bearing context (owns both registrations).
|
|
665
|
+
* @param webServer - the host web server.
|
|
666
|
+
* @returns disposer for every registration this function made.
|
|
667
|
+
*/
|
|
668
|
+
async function registerAgyWeb(ctx, webServer) {
|
|
669
|
+
const webStartup = ctx.get("webStartup");
|
|
670
|
+
const host = webStartup?.host ?? "127.0.0.1";
|
|
671
|
+
const port = webStartup?.port ?? 3080;
|
|
672
|
+
const bindHost = webServer.host ?? host;
|
|
673
|
+
if (![
|
|
674
|
+
"127.0.0.1",
|
|
675
|
+
"localhost",
|
|
676
|
+
"::1"
|
|
677
|
+
].includes(bindHost)) {
|
|
678
|
+
ctx.logger.warn("[dsh-agy] web server bound to \"" + bindHost + "\" (non-loopback): not registering the agy routes (they manage account credentials and must stay loopback-only). Bind the web server to 127.0.0.1 to enable them.");
|
|
679
|
+
return () => {};
|
|
680
|
+
}
|
|
681
|
+
const { store, sessions, adapter, stats, modelVisibility, thinkingBudget } = await createAgyRuntime(ctx);
|
|
682
|
+
const baseUrl = `http://${host}:${port}`;
|
|
683
|
+
const management = createAgyManagement({
|
|
684
|
+
store,
|
|
685
|
+
sessions,
|
|
686
|
+
stats,
|
|
687
|
+
modelVisibility,
|
|
688
|
+
thinkingBudget: {
|
|
689
|
+
all: () => thinkingBudget.all(),
|
|
690
|
+
set: (level, value) => thinkingBudget.setBudget(level, value),
|
|
691
|
+
claude: () => thinkingBudget.claudeBudget(),
|
|
692
|
+
setClaude: (value) => thinkingBudget.setClaudeBudget(value).claudeBudget,
|
|
693
|
+
tiered: () => thinkingBudget.tieredBudget(),
|
|
694
|
+
setTiered: (value) => thinkingBudget.setTieredBudget(value).tieredBudget
|
|
695
|
+
},
|
|
696
|
+
listAllModels: () => adapter.listAllModels(),
|
|
697
|
+
baseUrl,
|
|
698
|
+
notifyModelsChanged: () => {
|
|
699
|
+
ctx.emit("llm/adapters-updated");
|
|
1400
700
|
}
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
expiresAt: Date.now() + PENDING_AUTH_TTL_MS
|
|
1410
|
-
});
|
|
1411
|
-
sendJson(res, 200, { url: authorization.url });
|
|
1412
|
-
};
|
|
1413
|
-
const handleCallback = async (req, res) => {
|
|
1414
|
-
const url = new URL(req.url ?? "/", baseUrl);
|
|
1415
|
-
const code = url.searchParams.get("code");
|
|
1416
|
-
const state = url.searchParams.get("state");
|
|
1417
|
-
if (!code || !state) {
|
|
1418
|
-
res.writeHead(400, { "content-type": "text/html; charset=utf-8" });
|
|
1419
|
-
res.end(renderCallbackHtml({
|
|
701
|
+
});
|
|
702
|
+
const disposers = [];
|
|
703
|
+
disposers.push(webServer.register({
|
|
704
|
+
kind: "exact",
|
|
705
|
+
path: "/agy/oauth-callback",
|
|
706
|
+
handler: async (req, res) => {
|
|
707
|
+
const url = new URL(req.url ?? "/", baseUrl);
|
|
708
|
+
const result = await management.handleCallback(url.searchParams).catch((error) => ({
|
|
1420
709
|
ok: false,
|
|
1421
|
-
error:
|
|
1422
|
-
baseUrl
|
|
710
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1423
711
|
}));
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
res.writeHead(400, { "content-type": "text/html; charset=utf-8" });
|
|
1429
|
-
res.end(renderCallbackHtml({
|
|
1430
|
-
ok: false,
|
|
1431
|
-
error: "Unknown or expired authorization — please start a new login.",
|
|
712
|
+
res.writeHead(result.ok ? 200 : 400, { "content-type": "text/html; charset=utf-8" });
|
|
713
|
+
res.end(result.ok ? renderCallbackHtml({
|
|
714
|
+
ok: true,
|
|
715
|
+
email: result.email ?? null,
|
|
1432
716
|
baseUrl
|
|
1433
|
-
})
|
|
1434
|
-
return;
|
|
1435
|
-
}
|
|
1436
|
-
pendingAuth.delete(state);
|
|
1437
|
-
const redirectUri = `${baseUrl}/agy/oauth-callback`;
|
|
1438
|
-
const result = await exchangeAntigravity(code, state, redirectUri, expected.verifier);
|
|
1439
|
-
if (result.type === "failed") {
|
|
1440
|
-
res.writeHead(400, { "content-type": "text/html; charset=utf-8" });
|
|
1441
|
-
res.end(renderCallbackHtml({
|
|
717
|
+
}) : renderCallbackHtml({
|
|
1442
718
|
ok: false,
|
|
1443
|
-
error: result.error,
|
|
719
|
+
error: result.error ?? "Unknown error",
|
|
1444
720
|
baseUrl
|
|
1445
721
|
}));
|
|
1446
|
-
return;
|
|
1447
|
-
}
|
|
1448
|
-
await upsertImportedAccount(store, {
|
|
1449
|
-
accessToken: result.access,
|
|
1450
|
-
refreshToken: result.refresh.split("|")[0],
|
|
1451
|
-
tokenType: "Bearer",
|
|
1452
|
-
expiresAt: new Date(result.expires).toISOString(),
|
|
1453
|
-
authMethod: "oauth",
|
|
1454
|
-
email: result.email ?? null,
|
|
1455
|
-
projectId: result.projectId || null,
|
|
1456
|
-
clientId: result.clientId || null
|
|
1457
|
-
}, { overwriteExisting: true });
|
|
1458
|
-
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
1459
|
-
res.end(renderCallbackHtml({
|
|
1460
|
-
ok: true,
|
|
1461
|
-
email: result.email ?? null,
|
|
1462
|
-
baseUrl
|
|
1463
|
-
}));
|
|
1464
|
-
};
|
|
1465
|
-
const handleImport = async (req, res) => {
|
|
1466
|
-
try {
|
|
1467
|
-
const body = await readJson(req);
|
|
1468
|
-
const kind = body.kind === "blob" ? "blob" : "json";
|
|
1469
|
-
const sources = Array.isArray(body.sources) ? body.sources : body.source !== void 0 ? [body.source] : [];
|
|
1470
|
-
if (sources.length === 0) throw new Error("nothing to import");
|
|
1471
|
-
sendJson(res, 200, await importManySources(sources.map((source) => ({
|
|
1472
|
-
source,
|
|
1473
|
-
kind
|
|
1474
|
-
})), store, { overwriteExisting: true }));
|
|
1475
|
-
} catch (error) {
|
|
1476
|
-
sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
1477
|
-
}
|
|
1478
|
-
};
|
|
1479
|
-
const handleExportAll = async (_req, res) => {
|
|
1480
|
-
try {
|
|
1481
|
-
const storage = await store.load();
|
|
1482
|
-
const blobs = [];
|
|
1483
|
-
for (let index = 0; index < storage.accounts.length; index++) {
|
|
1484
|
-
const result = await sessions.exportBlob(index);
|
|
1485
|
-
if (result.blob) blobs.push({
|
|
1486
|
-
index,
|
|
1487
|
-
blob: result.blob
|
|
1488
|
-
});
|
|
1489
|
-
}
|
|
1490
|
-
sendJson(res, 200, { blobs });
|
|
1491
|
-
} catch (error) {
|
|
1492
|
-
sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
1493
|
-
}
|
|
1494
|
-
};
|
|
1495
|
-
const handleVerify = async (req, res) => {
|
|
1496
|
-
try {
|
|
1497
|
-
const body = await readJson(req);
|
|
1498
|
-
const index = Number(body.index);
|
|
1499
|
-
const result = await sessions.verifyAccount(index);
|
|
1500
|
-
sendJson(res, result.ok ? 200 : 400, result);
|
|
1501
|
-
} catch (error) {
|
|
1502
|
-
sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
1503
|
-
}
|
|
1504
|
-
};
|
|
1505
|
-
const handleHealth = async (req, res) => {
|
|
1506
|
-
try {
|
|
1507
|
-
const body = await readJson(req);
|
|
1508
|
-
const indices = Array.isArray(body.indices) ? body.indices.map((value) => Number(value)) : void 0;
|
|
1509
|
-
sendJson(res, 200, { results: await sessions.checkAccounts(indices) });
|
|
1510
|
-
} catch (error) {
|
|
1511
|
-
sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
1512
722
|
}
|
|
1513
|
-
};
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
await store.mutate((storage) => {
|
|
1519
|
-
if (index < 0 || index >= storage.accounts.length) throw new Error("account not found");
|
|
1520
|
-
storage.accounts.splice(index, 1);
|
|
1521
|
-
if (storage.activeIndex >= storage.accounts.length) storage.activeIndex = 0;
|
|
1522
|
-
});
|
|
1523
|
-
sendJson(res, 200, { ok: true });
|
|
1524
|
-
} catch (error) {
|
|
1525
|
-
sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
1526
|
-
}
|
|
1527
|
-
};
|
|
1528
|
-
const handleActivate = async (req, res) => {
|
|
1529
|
-
try {
|
|
1530
|
-
const body = await readJson(req);
|
|
1531
|
-
const index = Number(body.index);
|
|
1532
|
-
await store.mutate((storage) => {
|
|
1533
|
-
if (index < 0 || index >= storage.accounts.length) throw new Error("account not found");
|
|
1534
|
-
storage.activeIndex = index;
|
|
1535
|
-
});
|
|
1536
|
-
sendJson(res, 200, {
|
|
1537
|
-
ok: true,
|
|
1538
|
-
index
|
|
1539
|
-
});
|
|
1540
|
-
} catch (error) {
|
|
1541
|
-
sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
1542
|
-
}
|
|
1543
|
-
};
|
|
1544
|
-
const handleModels = async (_req, res) => {
|
|
1545
|
-
const session = await sessions.getSession().catch(() => void 0);
|
|
1546
|
-
if (!session) {
|
|
1547
|
-
sendJson(res, 400, { error: "No agy account configured — run `dsh-agy login` first." });
|
|
723
|
+
}));
|
|
724
|
+
ctx.inject(["connection"], (connectionCtx) => {
|
|
725
|
+
const connection = connectionCtx.get("connection");
|
|
726
|
+
if (!connection || typeof connection.fetch?.register !== "function") {
|
|
727
|
+
connectionCtx.logger.warn("[dsh-agy] connection.fetch unavailable — management RPC not registered");
|
|
1548
728
|
return;
|
|
1549
729
|
}
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
}
|
|
1563
|
-
};
|
|
1564
|
-
const handleTest = async (req, res) => {
|
|
1565
|
-
try {
|
|
1566
|
-
const body = await readJson(req);
|
|
1567
|
-
const model = typeof body.model === "string" ? body.model : "";
|
|
1568
|
-
if (!model) {
|
|
1569
|
-
sendJson(res, 400, { error: "model is required" });
|
|
1570
|
-
return;
|
|
1571
|
-
}
|
|
1572
|
-
const result = await sessions.testCall(model);
|
|
1573
|
-
sendJson(res, result.ok ? 200 : 400, result);
|
|
1574
|
-
} catch (error) {
|
|
1575
|
-
sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
1576
|
-
}
|
|
1577
|
-
};
|
|
1578
|
-
const handleExport = async (req, res) => {
|
|
1579
|
-
try {
|
|
1580
|
-
const body = await readJson(req);
|
|
1581
|
-
const index = Number(body.index);
|
|
1582
|
-
const result = await sessions.exportBlob(index);
|
|
1583
|
-
if (result.error) {
|
|
1584
|
-
sendJson(res, 400, result);
|
|
1585
|
-
return;
|
|
1586
|
-
}
|
|
1587
|
-
sendJson(res, 200, { blob: result.blob });
|
|
1588
|
-
} catch (error) {
|
|
1589
|
-
sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
1590
|
-
}
|
|
1591
|
-
};
|
|
1592
|
-
const handleFingerprint = async (req, res) => {
|
|
1593
|
-
try {
|
|
1594
|
-
const body = await readJson(req);
|
|
1595
|
-
const index = Number(body.index);
|
|
1596
|
-
const action = body.action === "regenerate" ? "regenerate" : "show";
|
|
1597
|
-
sendJson(res, 200, await store.mutate(async (storage) => {
|
|
1598
|
-
const account = storage.accounts[index];
|
|
1599
|
-
if (!account) throw new Error("account not found");
|
|
1600
|
-
if (action === "regenerate") {
|
|
1601
|
-
const fresh = generateFingerprint(void 0, await resolveAntigravityVersionBounded());
|
|
1602
|
-
account.fingerprint = fresh;
|
|
1603
|
-
account.fingerprintHistory = recordFingerprintVersion(account.fingerprintHistory, fresh, "regenerated");
|
|
730
|
+
connectionCtx.effect(() => connection.fetch.register({
|
|
731
|
+
path: "/api/agy",
|
|
732
|
+
methods: ["POST"],
|
|
733
|
+
requestBody: "buffered",
|
|
734
|
+
async fetch(request) {
|
|
735
|
+
if (request.method !== "POST") return new Response("method not allowed", { status: 405 });
|
|
736
|
+
if (request.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() !== "application/json") return new Response("content type must be application/json", { status: 415 });
|
|
737
|
+
let message;
|
|
738
|
+
try {
|
|
739
|
+
message = await request.json();
|
|
740
|
+
} catch {
|
|
741
|
+
return new Response("body is not JSON", { status: 400 });
|
|
1604
742
|
}
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
};
|
|
1614
|
-
}));
|
|
1615
|
-
} catch (error) {
|
|
1616
|
-
sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
1617
|
-
}
|
|
1618
|
-
};
|
|
1619
|
-
const handleProxy = async (req, res) => {
|
|
1620
|
-
try {
|
|
1621
|
-
const body = await readJson(req);
|
|
1622
|
-
const rawIdx = Number(body.index);
|
|
1623
|
-
if (!Number.isInteger(rawIdx) || !Number.isFinite(rawIdx) || rawIdx < 0) {
|
|
1624
|
-
sendJson(res, 400, { error: "invalid index" });
|
|
1625
|
-
return;
|
|
1626
|
-
}
|
|
1627
|
-
const index = rawIdx;
|
|
1628
|
-
const raw = typeof body.proxy === "string" ? body.proxy : "";
|
|
1629
|
-
if (!raw.trim()) {
|
|
1630
|
-
await store.mutate((storage) => {
|
|
1631
|
-
const account = storage.accounts[index];
|
|
1632
|
-
if (!account) throw new Error("account not found");
|
|
1633
|
-
delete account.proxy;
|
|
1634
|
-
});
|
|
1635
|
-
sendJson(res, 200, {
|
|
1636
|
-
ok: true,
|
|
1637
|
-
proxy: null,
|
|
1638
|
-
proxyMasked: null,
|
|
1639
|
-
rawLogs: null
|
|
743
|
+
const rpcId = typeof message.rpcId === "string" ? message.rpcId : "invalid-request";
|
|
744
|
+
const call = message.payload;
|
|
745
|
+
if (message.type !== "client-request" || typeof message.rpcId !== "string" || typeof call?.method !== "string") return reply(rpcId, {
|
|
746
|
+
ok: false,
|
|
747
|
+
error: {
|
|
748
|
+
code: "agy/bad-request",
|
|
749
|
+
message: "Invalid agy management request."
|
|
750
|
+
}
|
|
1640
751
|
});
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
ok: true,
|
|
1657
|
-
proxy: maskProxyUrl(normalized),
|
|
1658
|
-
proxyMasked: maskProxyUrl(normalized),
|
|
1659
|
-
rawLogs: proxyUrlForLogs(normalized)
|
|
1660
|
-
});
|
|
1661
|
-
} catch (error) {
|
|
1662
|
-
sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
1663
|
-
}
|
|
1664
|
-
};
|
|
1665
|
-
const handleProxyTest = async (req, res) => {
|
|
1666
|
-
try {
|
|
1667
|
-
const body = await readJson(req);
|
|
1668
|
-
const rawIdx2 = body.index !== void 0 ? Number(body.index) : NaN;
|
|
1669
|
-
const hasExplicitProxy = typeof body.proxy === "string" && body.proxy.trim() !== "";
|
|
1670
|
-
if (!hasExplicitProxy) {
|
|
1671
|
-
if (!Number.isInteger(rawIdx2) || !Number.isFinite(rawIdx2) || rawIdx2 < 0) {
|
|
1672
|
-
sendJson(res, 400, { error: "invalid index" });
|
|
1673
|
-
return;
|
|
1674
|
-
}
|
|
1675
|
-
} else if (body.index !== void 0) {
|
|
1676
|
-
if (!Number.isInteger(rawIdx2) || !Number.isFinite(rawIdx2) || rawIdx2 < 0) {
|
|
1677
|
-
sendJson(res, 400, { error: "invalid index" });
|
|
1678
|
-
return;
|
|
1679
|
-
}
|
|
1680
|
-
}
|
|
1681
|
-
const index = hasExplicitProxy && body.index === void 0 ? -1 : rawIdx2;
|
|
1682
|
-
let target;
|
|
1683
|
-
if (hasExplicitProxy) try {
|
|
1684
|
-
target = normalizeProxyUrl(body.proxy);
|
|
1685
|
-
} catch (e) {
|
|
1686
|
-
sendJson(res, 400, { error: e instanceof Error ? e.message : String(e) });
|
|
1687
|
-
return;
|
|
1688
|
-
}
|
|
1689
|
-
else {
|
|
1690
|
-
const account = (await store.load()).accounts[index];
|
|
1691
|
-
if (!account) {
|
|
1692
|
-
sendJson(res, 400, { error: "account not found" });
|
|
1693
|
-
return;
|
|
1694
|
-
}
|
|
1695
|
-
if (!account.proxy) {
|
|
1696
|
-
sendJson(res, 400, { error: "no proxy configured for this account" });
|
|
1697
|
-
return;
|
|
752
|
+
try {
|
|
753
|
+
return reply(rpcId, {
|
|
754
|
+
ok: true,
|
|
755
|
+
value: await management.call(call.method, call.payload)
|
|
756
|
+
});
|
|
757
|
+
} catch (error) {
|
|
758
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
759
|
+
connectionCtx.logger.warn(`[dsh-agy] ${call.method} failed: ${text}`);
|
|
760
|
+
return reply(rpcId, {
|
|
761
|
+
ok: false,
|
|
762
|
+
error: {
|
|
763
|
+
code: "agy/handler-failed",
|
|
764
|
+
message: text
|
|
765
|
+
}
|
|
766
|
+
});
|
|
1698
767
|
}
|
|
1699
|
-
target = account.proxy;
|
|
1700
|
-
}
|
|
1701
|
-
const masked = maskProxyUrl(target) ?? proxyUrlForLogs(target);
|
|
1702
|
-
let ok = false;
|
|
1703
|
-
let errMsg;
|
|
1704
|
-
try {
|
|
1705
|
-
ok = await isProxyReachable(target, 2e3);
|
|
1706
|
-
if (!ok) errMsg = "proxy unreachable: " + masked;
|
|
1707
|
-
} catch (e) {
|
|
1708
|
-
ok = false;
|
|
1709
|
-
errMsg = e instanceof Error ? e.message : String(e);
|
|
1710
768
|
}
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
});
|
|
1716
|
-
} catch (error) {
|
|
1717
|
-
sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
1718
|
-
}
|
|
769
|
+
}), "dsh-agy: /api/agy management RPC");
|
|
770
|
+
});
|
|
771
|
+
return () => {
|
|
772
|
+
for (const dispose of disposers) dispose();
|
|
1719
773
|
};
|
|
1720
|
-
return [
|
|
1721
|
-
{
|
|
1722
|
-
kind: "exact",
|
|
1723
|
-
path: "/agy",
|
|
1724
|
-
handler: (req, res) => {
|
|
1725
|
-
if (req.method === "GET") {
|
|
1726
|
-
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
1727
|
-
res.end(renderDashboardHtml());
|
|
1728
|
-
return;
|
|
1729
|
-
}
|
|
1730
|
-
res.writeHead(405);
|
|
1731
|
-
res.end();
|
|
1732
|
-
}
|
|
1733
|
-
},
|
|
1734
|
-
{
|
|
1735
|
-
kind: "exact",
|
|
1736
|
-
path: "/agy/oauth-callback",
|
|
1737
|
-
handler: handleCallback
|
|
1738
|
-
},
|
|
1739
|
-
{
|
|
1740
|
-
kind: "exact",
|
|
1741
|
-
path: "/agy/api/accounts",
|
|
1742
|
-
handler: async (_req, res) => {
|
|
1743
|
-
sendJson(res, 200, { accounts: await accountsList() });
|
|
1744
|
-
}
|
|
1745
|
-
},
|
|
1746
|
-
{
|
|
1747
|
-
kind: "exact",
|
|
1748
|
-
path: "/agy/api/auth-url",
|
|
1749
|
-
handler: async (_req, res) => {
|
|
1750
|
-
await handleAuthUrl(res);
|
|
1751
|
-
}
|
|
1752
|
-
},
|
|
1753
|
-
{
|
|
1754
|
-
kind: "exact",
|
|
1755
|
-
path: "/agy/api/import",
|
|
1756
|
-
handler: handleImport
|
|
1757
|
-
},
|
|
1758
|
-
{
|
|
1759
|
-
kind: "exact",
|
|
1760
|
-
path: "/agy/api/export-all",
|
|
1761
|
-
handler: handleExportAll
|
|
1762
|
-
},
|
|
1763
|
-
{
|
|
1764
|
-
kind: "exact",
|
|
1765
|
-
path: "/agy/api/verify",
|
|
1766
|
-
handler: handleVerify
|
|
1767
|
-
},
|
|
1768
|
-
{
|
|
1769
|
-
kind: "exact",
|
|
1770
|
-
path: "/agy/api/health",
|
|
1771
|
-
handler: handleHealth
|
|
1772
|
-
},
|
|
1773
|
-
{
|
|
1774
|
-
kind: "exact",
|
|
1775
|
-
path: "/agy/api/delete",
|
|
1776
|
-
handler: handleDelete
|
|
1777
|
-
},
|
|
1778
|
-
{
|
|
1779
|
-
kind: "exact",
|
|
1780
|
-
path: "/agy/api/activate",
|
|
1781
|
-
handler: handleActivate
|
|
1782
|
-
},
|
|
1783
|
-
{
|
|
1784
|
-
kind: "exact",
|
|
1785
|
-
path: "/agy/api/models",
|
|
1786
|
-
handler: handleModels
|
|
1787
|
-
},
|
|
1788
|
-
{
|
|
1789
|
-
kind: "exact",
|
|
1790
|
-
path: "/agy/api/test",
|
|
1791
|
-
handler: handleTest
|
|
1792
|
-
},
|
|
1793
|
-
{
|
|
1794
|
-
kind: "exact",
|
|
1795
|
-
path: "/agy/api/export",
|
|
1796
|
-
handler: handleExport
|
|
1797
|
-
},
|
|
1798
|
-
{
|
|
1799
|
-
kind: "exact",
|
|
1800
|
-
path: "/agy/api/fingerprint",
|
|
1801
|
-
handler: handleFingerprint
|
|
1802
|
-
},
|
|
1803
|
-
{
|
|
1804
|
-
kind: "exact",
|
|
1805
|
-
path: "/agy/api/proxy",
|
|
1806
|
-
handler: handleProxy
|
|
1807
|
-
},
|
|
1808
|
-
{
|
|
1809
|
-
kind: "exact",
|
|
1810
|
-
path: "/agy/api/proxy/test",
|
|
1811
|
-
handler: handleProxyTest
|
|
1812
|
-
}
|
|
1813
|
-
];
|
|
1814
774
|
}
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
const host = webStartup?.host ?? "127.0.0.1";
|
|
1829
|
-
const port = webStartup?.port ?? 3080;
|
|
1830
|
-
const bindHost = webServer.host ?? host;
|
|
1831
|
-
if (![
|
|
1832
|
-
"127.0.0.1",
|
|
1833
|
-
"localhost",
|
|
1834
|
-
"::1"
|
|
1835
|
-
].includes(bindHost)) {
|
|
1836
|
-
ctx.logger.warn("[dsh-agy] web server bound to \"" + bindHost + "\" (non-loopback): not registering the /agy routes (they manage account credentials and must stay loopback-only). Bind the web server to 127.0.0.1 to enable them.");
|
|
1837
|
-
return () => {};
|
|
775
|
+
/**
|
|
776
|
+
* Wrap one result in the Connection RPC response envelope.
|
|
777
|
+
*
|
|
778
|
+
* A failure MUST carry `error.details` as an object: the client's envelope
|
|
779
|
+
* parser rejects a failure without it as "invalid server-response", which hides
|
|
780
|
+
* the real message behind a transport-sounding error.
|
|
781
|
+
*/
|
|
782
|
+
function reply(rpcId, result) {
|
|
783
|
+
const normalized = isFailure(result) ? {
|
|
784
|
+
...result,
|
|
785
|
+
error: {
|
|
786
|
+
details: {},
|
|
787
|
+
...result.error
|
|
1838
788
|
}
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
}).map((route) => webServer.register(route));
|
|
1845
|
-
return () => {
|
|
1846
|
-
for (const dispose of disposers) dispose();
|
|
1847
|
-
};
|
|
789
|
+
} : result;
|
|
790
|
+
return Response.json({
|
|
791
|
+
type: "server-response",
|
|
792
|
+
rpcId,
|
|
793
|
+
result: normalized
|
|
1848
794
|
});
|
|
1849
795
|
}
|
|
796
|
+
/** Whether one result is an RPC failure envelope. */
|
|
797
|
+
function isFailure(value) {
|
|
798
|
+
return typeof value === "object" && value !== null && value.ok === false && typeof value.error === "object" && value.error !== null;
|
|
799
|
+
}
|
|
1850
800
|
//#endregion
|
|
1851
801
|
export { apply, inject, name };
|
|
1852
802
|
|