dsh-agy 0.1.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/LICENSE +21 -0
- package/README.md +174 -0
- package/bin/dsh-agy.js +6 -0
- package/cordis.patch.yml +13 -0
- package/lib/blob-D1e7_uT1.mjs +63 -0
- package/lib/blob-D1e7_uT1.mjs.map +1 -0
- package/lib/cli/index.d.mts +10 -0
- package/lib/cli/index.d.mts.map +1 -0
- package/lib/cli/index.mjs +317 -0
- package/lib/cli/index.mjs.map +1 -0
- package/lib/exchange-MhFUiBAm.mjs +240 -0
- package/lib/exchange-MhFUiBAm.mjs.map +1 -0
- package/lib/import-DnKzgr-0.mjs +187 -0
- package/lib/import-DnKzgr-0.mjs.map +1 -0
- package/lib/index.d.mts +8 -0
- package/lib/index.d.mts.map +1 -0
- package/lib/index.mjs +15 -0
- package/lib/index.mjs.map +1 -0
- package/lib/models-Cu1g7AXV.mjs +222 -0
- package/lib/models-Cu1g7AXV.mjs.map +1 -0
- package/lib/parse-CiEzD9d_.mjs +174 -0
- package/lib/parse-CiEzD9d_.mjs.map +1 -0
- package/lib/plugin-common-I56kmGjr.mjs +270 -0
- package/lib/plugin-common-I56kmGjr.mjs.map +1 -0
- package/lib/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/lib/session-tawi2_uh.mjs +1221 -0
- package/lib/session-tawi2_uh.mjs.map +1 -0
- package/lib/translate-h2bjH7ub.mjs +148 -0
- package/lib/translate-h2bjH7ub.mjs.map +1 -0
- package/lib/web/plugin.d.mts +8 -0
- package/lib/web/plugin.d.mts.map +1 -0
- package/lib/web/plugin.mjs +1618 -0
- package/lib/web/plugin.mjs.map +1 -0
- package/package.json +81 -0
|
@@ -0,0 +1,1618 @@
|
|
|
1
|
+
import { m as resolveAntigravityVersionBounded, n as generateFingerprint, r as recordFingerprintVersion } from "../session-tawi2_uh.mjs";
|
|
2
|
+
import { t as createAgyRuntime } from "../plugin-common-I56kmGjr.mjs";
|
|
3
|
+
import { t as exchangeAntigravity } from "../exchange-MhFUiBAm.mjs";
|
|
4
|
+
import { n as upsertImportedAccount, r as authorizeAntigravity, t as parseImportSource } from "../import-DnKzgr-0.mjs";
|
|
5
|
+
//#region src/web/i18n.ts
|
|
6
|
+
const I18N_DICT = {
|
|
7
|
+
en: {
|
|
8
|
+
title: "Antigravity Account Pool",
|
|
9
|
+
subtitle: "Manage multi-account rotation, quota monitoring, and credentials.",
|
|
10
|
+
refresh: "Refresh",
|
|
11
|
+
loginWithGoogle: "Login with Google",
|
|
12
|
+
switchLang: "中文",
|
|
13
|
+
accountsList: "Accounts",
|
|
14
|
+
noAccounts: "No accounts configured",
|
|
15
|
+
noAccountsHint: "Click \"Login with Google\" or import credentials below to get started.",
|
|
16
|
+
activeBadge: "Active",
|
|
17
|
+
stateActive: "Healthy",
|
|
18
|
+
stateCooling: "Cooling Down",
|
|
19
|
+
stateRateLimited: "Rate Limited",
|
|
20
|
+
stateVerificationRequired: "Verification Required",
|
|
21
|
+
stateDisabled: "Disabled",
|
|
22
|
+
project: "Project",
|
|
23
|
+
defaultProject: "default",
|
|
24
|
+
activate: "Set Active",
|
|
25
|
+
verify: "Verify",
|
|
26
|
+
verifyCta: "Verify Now",
|
|
27
|
+
testAll: "Test All Models",
|
|
28
|
+
exportBlob: "Export Blob",
|
|
29
|
+
fingerprint: "Fingerprint",
|
|
30
|
+
deleteAccount: "Delete Account",
|
|
31
|
+
confirmDelete: "Are you sure you want to delete this account? This action cannot be undone.",
|
|
32
|
+
confirmTestAll: "Test All will execute real API calls on all models, consuming actual quota. Continue?",
|
|
33
|
+
confirmRegenerateFp: "Generate a fresh device fingerprint and user agent for this account?",
|
|
34
|
+
quotaTitle: "Model Quotas",
|
|
35
|
+
quotaUnavailable: "Quota information unavailable for this account",
|
|
36
|
+
noQuotaReported: "No quota reported by upstream",
|
|
37
|
+
testModel: "Test",
|
|
38
|
+
testing: "Testing...",
|
|
39
|
+
copied: "Copied to clipboard!",
|
|
40
|
+
importTitle: "Import Credentials",
|
|
41
|
+
importDesc: "Paste an agy auth.json token document, or a credential blob (from dsh-agy login --blob):",
|
|
42
|
+
importPlaceholder: "{\"token\":{\"access_token\":\"...\",\"refresh_token\":\"...\"}} or dsh-agy-cred-v1....",
|
|
43
|
+
importJson: "Import JSON",
|
|
44
|
+
importBlobBtn: "Import Blob",
|
|
45
|
+
importSuccess: "Successfully imported account: ",
|
|
46
|
+
fingerprintTitle: "Device Fingerprint",
|
|
47
|
+
fingerprintDevice: "Device ID",
|
|
48
|
+
fingerprintUserAgent: "User Agent",
|
|
49
|
+
fingerprintCreated: "Generated At",
|
|
50
|
+
fingerprintHistory: "Fingerprint Versions",
|
|
51
|
+
regenerateFp: "Regenerate Fingerprint",
|
|
52
|
+
copyBlobMsg: "Credential blob exported. Copied to clipboard!",
|
|
53
|
+
copyFpMsg: "Device fingerprint details loaded.",
|
|
54
|
+
allModelsOk: "All model tests passed!",
|
|
55
|
+
modelsTestedSummary: "Completed testing {total} models: {ok} passed, {failed} failed.",
|
|
56
|
+
loginSuccessTitle: "Sign-in Successful",
|
|
57
|
+
loginSuccessDesc: "Your Antigravity account has been authorized and saved.",
|
|
58
|
+
loginFailedTitle: "Sign-in Failed",
|
|
59
|
+
loginFailedDesc: "Unable to complete authorization. Error details:",
|
|
60
|
+
windowClosing: "This window will close automatically in a moment...",
|
|
61
|
+
closeWindow: "Close Window",
|
|
62
|
+
retryLogin: "Return to Dashboard",
|
|
63
|
+
resetIn: "Resets in ",
|
|
64
|
+
resetAt: "Resets at "
|
|
65
|
+
},
|
|
66
|
+
zh: {
|
|
67
|
+
title: "Antigravity 账号池管理",
|
|
68
|
+
subtitle: "多账号智能轮换、配额实时监控与凭据指纹管理。",
|
|
69
|
+
refresh: "刷新",
|
|
70
|
+
loginWithGoogle: "Google 账号登录",
|
|
71
|
+
switchLang: "EN",
|
|
72
|
+
accountsList: "账号列表",
|
|
73
|
+
noAccounts: "暂无配置账号",
|
|
74
|
+
noAccountsHint: "点击上方“Google 账号登录”或在下方粘贴凭据以添加账号。",
|
|
75
|
+
activeBadge: "当前使用",
|
|
76
|
+
stateActive: "正常",
|
|
77
|
+
stateCooling: "冷却中",
|
|
78
|
+
stateRateLimited: "已限流",
|
|
79
|
+
stateVerificationRequired: "需要验证",
|
|
80
|
+
stateDisabled: "已禁用",
|
|
81
|
+
project: "项目 ID",
|
|
82
|
+
defaultProject: "默认项目",
|
|
83
|
+
activate: "设为默认",
|
|
84
|
+
verify: "验证账号",
|
|
85
|
+
verifyCta: "立即验证",
|
|
86
|
+
testAll: "测试全部模型",
|
|
87
|
+
exportBlob: "导出凭据 Blob",
|
|
88
|
+
fingerprint: "设备指纹",
|
|
89
|
+
deleteAccount: "删除账号",
|
|
90
|
+
confirmDelete: "确定要删除此账号吗?该操作不可撤销。",
|
|
91
|
+
confirmTestAll: "批量测试将对所有模型发起真实调用并消耗 API 配额,确认继续?",
|
|
92
|
+
confirmRegenerateFp: "确定要为该账号重新生成全新的设备指纹与 User-Agent 吗?",
|
|
93
|
+
quotaTitle: "模型配额池",
|
|
94
|
+
quotaUnavailable: "无法获取该账号的模型配额信息",
|
|
95
|
+
noQuotaReported: "上游未返回配额信息",
|
|
96
|
+
testModel: "测试",
|
|
97
|
+
testing: "测试中...",
|
|
98
|
+
copied: "已复制到剪贴板!",
|
|
99
|
+
importTitle: "导入凭据",
|
|
100
|
+
importDesc: "粘贴 agy auth.json 文件内容,或输入凭据 blob(来自 dsh-agy login --blob):",
|
|
101
|
+
importPlaceholder: "{\"token\":{\"access_token\":\"...\",\"refresh_token\":\"...\"}} 或 dsh-agy-cred-v1....",
|
|
102
|
+
importJson: "导入 JSON",
|
|
103
|
+
importBlobBtn: "导入 Blob",
|
|
104
|
+
importSuccess: "成功导入账号:",
|
|
105
|
+
fingerprintTitle: "设备指纹信息",
|
|
106
|
+
fingerprintDevice: "设备 ID (DeviceId)",
|
|
107
|
+
fingerprintUserAgent: "用户代理 (User-Agent)",
|
|
108
|
+
fingerprintCreated: "生成时间",
|
|
109
|
+
fingerprintHistory: "历史版本数",
|
|
110
|
+
regenerateFp: "重新生成指纹",
|
|
111
|
+
copyBlobMsg: "凭据 Blob 已成功导出并复制到剪贴板!",
|
|
112
|
+
copyFpMsg: "设备指纹信息已加载。",
|
|
113
|
+
allModelsOk: "所有模型可用性测试通过!",
|
|
114
|
+
modelsTestedSummary: "完成 {total} 个模型测试:{ok} 个可用,{failed} 个失败。",
|
|
115
|
+
loginSuccessTitle: "授权登录成功",
|
|
116
|
+
loginSuccessDesc: "Antigravity 账号已成功接入并保存。",
|
|
117
|
+
loginFailedTitle: "授权登录失败",
|
|
118
|
+
loginFailedDesc: "无法完成 OAuth 授权,错误详情:",
|
|
119
|
+
windowClosing: "此窗口即将在 2 秒内自动关闭...",
|
|
120
|
+
closeWindow: "立即关闭窗口",
|
|
121
|
+
retryLogin: "返回控制面板",
|
|
122
|
+
resetIn: "将在 ",
|
|
123
|
+
resetAt: "重置时间:"
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
//#endregion
|
|
127
|
+
//#region src/web/page.ts
|
|
128
|
+
/**
|
|
129
|
+
* Web dashboard HTML/CSS/JS generator for dsh-agy.
|
|
130
|
+
* Follows DeepSeek Harness (DSH) design system tokens and UX conventions.
|
|
131
|
+
*/
|
|
132
|
+
function renderDashboardHtml() {
|
|
133
|
+
return `<!doctype html>
|
|
134
|
+
<html lang="en">
|
|
135
|
+
<head>
|
|
136
|
+
<meta charset="utf-8">
|
|
137
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
138
|
+
<title>agy — Antigravity Accounts</title>
|
|
139
|
+
<style>
|
|
140
|
+
/* DSH Design System Tokens snapshot (ui-theme / design-platform) */
|
|
141
|
+
:root {
|
|
142
|
+
color-scheme: light dark;
|
|
143
|
+
--dsw-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
|
144
|
+
--ds-font-family-code: 'SF Mono', 'JetBrains Mono', 'Fira Code', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
|
|
145
|
+
--ds-ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
|
|
146
|
+
--ds-transition-fast: 0.12s var(--ds-ease-in-out);
|
|
147
|
+
--ds-transition-normal: 0.2s var(--ds-ease-in-out);
|
|
148
|
+
|
|
149
|
+
/* Light Theme Defaults */
|
|
150
|
+
--bg-page: #f8fafc;
|
|
151
|
+
--bg-surface: #ffffff;
|
|
152
|
+
--bg-surface-elevated: #f1f5f9;
|
|
153
|
+
--bg-surface-hover: #f1f5f9;
|
|
154
|
+
--bg-input: #ffffff;
|
|
155
|
+
--border-l1: rgba(0, 0, 0, 0.06);
|
|
156
|
+
--border-l2: rgba(0, 0, 0, 0.12);
|
|
157
|
+
--border-l3: rgba(0, 0, 0, 0.2);
|
|
158
|
+
--text-primary: #0f172a;
|
|
159
|
+
--text-secondary: #475569;
|
|
160
|
+
--text-tertiary: #94a3b8;
|
|
161
|
+
--text-dimmed: #cbd5e1;
|
|
162
|
+
|
|
163
|
+
--brand-primary: #4176e6;
|
|
164
|
+
--brand-hover: #3362c4;
|
|
165
|
+
--brand-surface: rgba(65, 118, 230, 0.1);
|
|
166
|
+
|
|
167
|
+
--state-success: #22c55e;
|
|
168
|
+
--state-success-bg: rgba(34, 197, 94, 0.12);
|
|
169
|
+
--state-warn: #f59e0b;
|
|
170
|
+
--state-warn-bg: rgba(245, 158, 11, 0.12);
|
|
171
|
+
--state-error: #ef4444;
|
|
172
|
+
--state-error-bg: rgba(239, 68, 68, 0.12);
|
|
173
|
+
|
|
174
|
+
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
|
175
|
+
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
|
|
176
|
+
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
@media (prefers-color-scheme: dark) {
|
|
180
|
+
:root {
|
|
181
|
+
--bg-page: #0f1115;
|
|
182
|
+
--bg-surface: #171a21;
|
|
183
|
+
--bg-surface-elevated: #20242e;
|
|
184
|
+
--bg-surface-hover: #262b37;
|
|
185
|
+
--bg-input: #12141a;
|
|
186
|
+
--border-l1: rgba(255, 255, 255, 0.06);
|
|
187
|
+
--border-l2: rgba(255, 255, 255, 0.12);
|
|
188
|
+
--border-l3: rgba(255, 255, 255, 0.22);
|
|
189
|
+
--text-primary: #f8fafc;
|
|
190
|
+
--text-secondary: #94a3b8;
|
|
191
|
+
--text-tertiary: #64748b;
|
|
192
|
+
--text-dimmed: #475569;
|
|
193
|
+
|
|
194
|
+
--brand-primary: #5686fe;
|
|
195
|
+
--brand-hover: #4176e6;
|
|
196
|
+
--brand-surface: rgba(86, 134, 254, 0.15);
|
|
197
|
+
|
|
198
|
+
--state-success: #34d399;
|
|
199
|
+
--state-success-bg: rgba(52, 211, 153, 0.15);
|
|
200
|
+
--state-warn: #fbbf24;
|
|
201
|
+
--state-warn-bg: rgba(251, 191, 36, 0.15);
|
|
202
|
+
--state-error: #f87171;
|
|
203
|
+
--state-error-bg: rgba(248, 113, 113, 0.15);
|
|
204
|
+
|
|
205
|
+
--shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.4);
|
|
206
|
+
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.5);
|
|
207
|
+
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.6);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
212
|
+
body {
|
|
213
|
+
font-family: var(--dsw-font-family);
|
|
214
|
+
background-color: var(--bg-page);
|
|
215
|
+
color: var(--text-primary);
|
|
216
|
+
line-height: 1.5;
|
|
217
|
+
font-size: 14px;
|
|
218
|
+
padding: 24px 16px;
|
|
219
|
+
-webkit-font-smoothing: antialiased;
|
|
220
|
+
}
|
|
221
|
+
.container {
|
|
222
|
+
max-width: 1080px;
|
|
223
|
+
margin: 0 auto;
|
|
224
|
+
display: flex;
|
|
225
|
+
flex-direction: column;
|
|
226
|
+
gap: 20px;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/* Header */
|
|
230
|
+
.header {
|
|
231
|
+
display: flex;
|
|
232
|
+
justify-content: space-between;
|
|
233
|
+
align-items: center;
|
|
234
|
+
flex-wrap: wrap;
|
|
235
|
+
gap: 12px;
|
|
236
|
+
padding-bottom: 16px;
|
|
237
|
+
border-bottom: 1px solid var(--border-l1);
|
|
238
|
+
}
|
|
239
|
+
.header-title-group h1 {
|
|
240
|
+
font-size: 20px;
|
|
241
|
+
font-weight: 600;
|
|
242
|
+
letter-spacing: -0.01em;
|
|
243
|
+
color: var(--text-primary);
|
|
244
|
+
display: flex;
|
|
245
|
+
align-items: center;
|
|
246
|
+
gap: 8px;
|
|
247
|
+
}
|
|
248
|
+
.header-title-group p {
|
|
249
|
+
font-size: 13px;
|
|
250
|
+
color: var(--text-secondary);
|
|
251
|
+
margin-top: 2px;
|
|
252
|
+
}
|
|
253
|
+
.header-actions {
|
|
254
|
+
display: flex;
|
|
255
|
+
align-items: center;
|
|
256
|
+
gap: 8px;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/* Buttons (DSH Capsule Geometry) */
|
|
260
|
+
.btn {
|
|
261
|
+
display: inline-flex;
|
|
262
|
+
align-items: center;
|
|
263
|
+
justify-content: center;
|
|
264
|
+
gap: 6px;
|
|
265
|
+
font-family: inherit;
|
|
266
|
+
font-size: 13px;
|
|
267
|
+
font-weight: 500;
|
|
268
|
+
line-height: 20px;
|
|
269
|
+
height: 32px;
|
|
270
|
+
padding: 0 12px;
|
|
271
|
+
border-radius: 16px;
|
|
272
|
+
border: 1px solid transparent;
|
|
273
|
+
cursor: pointer;
|
|
274
|
+
transition: background-color var(--ds-transition-fast), border-color var(--ds-transition-fast), color var(--ds-transition-fast), opacity var(--ds-transition-fast);
|
|
275
|
+
white-space: nowrap;
|
|
276
|
+
user-select: none;
|
|
277
|
+
}
|
|
278
|
+
.btn:disabled {
|
|
279
|
+
cursor: not-allowed;
|
|
280
|
+
opacity: 0.45 !important;
|
|
281
|
+
}
|
|
282
|
+
.btn-sm {
|
|
283
|
+
height: 26px;
|
|
284
|
+
font-size: 12px;
|
|
285
|
+
padding: 0 9px;
|
|
286
|
+
border-radius: 13px;
|
|
287
|
+
}
|
|
288
|
+
.btn-primary {
|
|
289
|
+
background: var(--brand-primary);
|
|
290
|
+
color: #ffffff;
|
|
291
|
+
}
|
|
292
|
+
.btn-primary:hover:not(:disabled) {
|
|
293
|
+
background: var(--brand-hover);
|
|
294
|
+
}
|
|
295
|
+
.btn-secondary {
|
|
296
|
+
background: transparent;
|
|
297
|
+
border-color: var(--border-l2);
|
|
298
|
+
color: var(--text-primary);
|
|
299
|
+
}
|
|
300
|
+
.btn-secondary:hover:not(:disabled) {
|
|
301
|
+
background: var(--bg-surface-hover);
|
|
302
|
+
border-color: var(--border-l3);
|
|
303
|
+
}
|
|
304
|
+
.btn-cta {
|
|
305
|
+
background: var(--state-warn-bg);
|
|
306
|
+
border-color: var(--state-warn);
|
|
307
|
+
color: var(--state-warn);
|
|
308
|
+
font-weight: 600;
|
|
309
|
+
animation: pulse-border 2s infinite;
|
|
310
|
+
}
|
|
311
|
+
.btn-cta:hover:not(:disabled) {
|
|
312
|
+
background: var(--state-warn);
|
|
313
|
+
color: #ffffff;
|
|
314
|
+
}
|
|
315
|
+
@keyframes pulse-border {
|
|
316
|
+
0%, 100% { box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.4); }
|
|
317
|
+
50% { box-shadow: 0 0 0 4px rgba(245, 158, 11, 0.15); }
|
|
318
|
+
}
|
|
319
|
+
.btn-danger {
|
|
320
|
+
background: transparent;
|
|
321
|
+
border-color: var(--border-l2);
|
|
322
|
+
color: var(--state-error);
|
|
323
|
+
}
|
|
324
|
+
.btn-danger:hover:not(:disabled) {
|
|
325
|
+
background: var(--state-error-bg);
|
|
326
|
+
border-color: var(--state-error);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/* Master-Detail Layout */
|
|
330
|
+
.main-grid {
|
|
331
|
+
display: grid;
|
|
332
|
+
grid-template-columns: 290px 1fr;
|
|
333
|
+
gap: 20px;
|
|
334
|
+
align-items: start;
|
|
335
|
+
}
|
|
336
|
+
@media (max-width: 780px) {
|
|
337
|
+
.main-grid {
|
|
338
|
+
grid-template-columns: 1fr;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/* Cards */
|
|
343
|
+
.card {
|
|
344
|
+
background: var(--bg-surface);
|
|
345
|
+
border: 1px solid var(--border-l2);
|
|
346
|
+
border-radius: 12px;
|
|
347
|
+
padding: 16px;
|
|
348
|
+
box-shadow: var(--shadow-sm);
|
|
349
|
+
}
|
|
350
|
+
.card-title {
|
|
351
|
+
font-size: 14px;
|
|
352
|
+
font-weight: 600;
|
|
353
|
+
color: var(--text-primary);
|
|
354
|
+
margin-bottom: 12px;
|
|
355
|
+
display: flex;
|
|
356
|
+
align-items: center;
|
|
357
|
+
justify-content: space-between;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/* Account List (Master) */
|
|
361
|
+
.account-list {
|
|
362
|
+
display: flex;
|
|
363
|
+
flex-direction: column;
|
|
364
|
+
gap: 8px;
|
|
365
|
+
list-style: none;
|
|
366
|
+
}
|
|
367
|
+
.account-item {
|
|
368
|
+
padding: 10px 12px;
|
|
369
|
+
border-radius: 8px;
|
|
370
|
+
border: 1px solid var(--border-l1);
|
|
371
|
+
background: var(--bg-surface);
|
|
372
|
+
cursor: pointer;
|
|
373
|
+
transition: all var(--ds-transition-fast);
|
|
374
|
+
display: flex;
|
|
375
|
+
flex-direction: column;
|
|
376
|
+
gap: 6px;
|
|
377
|
+
}
|
|
378
|
+
.account-item:hover {
|
|
379
|
+
background: var(--bg-surface-hover);
|
|
380
|
+
border-color: var(--border-l2);
|
|
381
|
+
}
|
|
382
|
+
.account-item.active-item {
|
|
383
|
+
background: var(--brand-surface);
|
|
384
|
+
border-color: var(--brand-primary);
|
|
385
|
+
}
|
|
386
|
+
.account-item-top {
|
|
387
|
+
display: flex;
|
|
388
|
+
align-items: center;
|
|
389
|
+
justify-content: space-between;
|
|
390
|
+
gap: 6px;
|
|
391
|
+
}
|
|
392
|
+
.account-email {
|
|
393
|
+
font-weight: 500;
|
|
394
|
+
font-size: 13px;
|
|
395
|
+
color: var(--text-primary);
|
|
396
|
+
overflow: hidden;
|
|
397
|
+
text-overflow: ellipsis;
|
|
398
|
+
white-space: nowrap;
|
|
399
|
+
}
|
|
400
|
+
.account-item-bottom {
|
|
401
|
+
display: flex;
|
|
402
|
+
align-items: center;
|
|
403
|
+
gap: 6px;
|
|
404
|
+
flex-wrap: wrap;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/* Badges & Pills */
|
|
408
|
+
.badge {
|
|
409
|
+
display: inline-flex;
|
|
410
|
+
align-items: center;
|
|
411
|
+
gap: 4px;
|
|
412
|
+
font-size: 11px;
|
|
413
|
+
font-weight: 500;
|
|
414
|
+
line-height: 16px;
|
|
415
|
+
padding: 1px 7px;
|
|
416
|
+
border-radius: 10px;
|
|
417
|
+
}
|
|
418
|
+
.badge-primary { background: var(--brand-surface); color: var(--brand-primary); }
|
|
419
|
+
.badge-success { background: var(--state-success-bg); color: var(--state-success); }
|
|
420
|
+
.badge-warn { background: var(--state-warn-bg); color: var(--state-warn); }
|
|
421
|
+
.badge-error { background: var(--state-error-bg); color: var(--state-error); }
|
|
422
|
+
.badge-neutral { background: var(--bg-surface-elevated); color: var(--text-secondary); }
|
|
423
|
+
|
|
424
|
+
.dot {
|
|
425
|
+
width: 7px;
|
|
426
|
+
height: 7px;
|
|
427
|
+
border-radius: 50%;
|
|
428
|
+
display: inline-block;
|
|
429
|
+
flex-shrink: 0;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/* Account Detail View */
|
|
433
|
+
.detail-head {
|
|
434
|
+
display: flex;
|
|
435
|
+
justify-content: space-between;
|
|
436
|
+
align-items: flex-start;
|
|
437
|
+
gap: 12px;
|
|
438
|
+
flex-wrap: wrap;
|
|
439
|
+
padding-bottom: 14px;
|
|
440
|
+
margin-bottom: 14px;
|
|
441
|
+
border-bottom: 1px solid var(--border-l1);
|
|
442
|
+
}
|
|
443
|
+
.detail-meta h2 {
|
|
444
|
+
font-size: 16px;
|
|
445
|
+
font-weight: 600;
|
|
446
|
+
display: flex;
|
|
447
|
+
align-items: center;
|
|
448
|
+
gap: 8px;
|
|
449
|
+
}
|
|
450
|
+
.detail-meta p {
|
|
451
|
+
font-size: 12px;
|
|
452
|
+
color: var(--text-secondary);
|
|
453
|
+
margin-top: 4px;
|
|
454
|
+
}
|
|
455
|
+
.detail-actions {
|
|
456
|
+
display: flex;
|
|
457
|
+
gap: 6px;
|
|
458
|
+
flex-wrap: wrap;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/* Quota Section */
|
|
462
|
+
.quota-table-wrap {
|
|
463
|
+
overflow-x: auto;
|
|
464
|
+
margin-top: 8px;
|
|
465
|
+
}
|
|
466
|
+
.quota-table {
|
|
467
|
+
width: 100%;
|
|
468
|
+
border-collapse: collapse;
|
|
469
|
+
font-size: 13px;
|
|
470
|
+
}
|
|
471
|
+
.quota-table th, .quota-table td {
|
|
472
|
+
padding: 8px 10px;
|
|
473
|
+
text-align: left;
|
|
474
|
+
border-bottom: 1px solid var(--border-l1);
|
|
475
|
+
}
|
|
476
|
+
.quota-table th {
|
|
477
|
+
font-size: 12px;
|
|
478
|
+
font-weight: 500;
|
|
479
|
+
color: var(--text-tertiary);
|
|
480
|
+
text-transform: uppercase;
|
|
481
|
+
letter-spacing: 0.03em;
|
|
482
|
+
}
|
|
483
|
+
.model-name {
|
|
484
|
+
font-family: var(--ds-font-family-code);
|
|
485
|
+
font-weight: 500;
|
|
486
|
+
font-size: 12.5px;
|
|
487
|
+
color: var(--text-primary);
|
|
488
|
+
cursor: pointer;
|
|
489
|
+
display: inline-flex;
|
|
490
|
+
align-items: center;
|
|
491
|
+
gap: 4px;
|
|
492
|
+
}
|
|
493
|
+
.model-name:hover {
|
|
494
|
+
color: var(--brand-primary);
|
|
495
|
+
text-decoration: underline;
|
|
496
|
+
}
|
|
497
|
+
.progress-bar {
|
|
498
|
+
background: var(--bg-surface-elevated);
|
|
499
|
+
border-radius: 4px;
|
|
500
|
+
height: 8px;
|
|
501
|
+
width: 110px;
|
|
502
|
+
overflow: hidden;
|
|
503
|
+
display: inline-block;
|
|
504
|
+
vertical-align: middle;
|
|
505
|
+
margin-right: 8px;
|
|
506
|
+
}
|
|
507
|
+
.progress-fill {
|
|
508
|
+
height: 100%;
|
|
509
|
+
border-radius: 4px;
|
|
510
|
+
transition: width var(--ds-transition-normal);
|
|
511
|
+
}
|
|
512
|
+
.reset-time {
|
|
513
|
+
font-size: 12px;
|
|
514
|
+
color: var(--text-tertiary);
|
|
515
|
+
white-space: nowrap;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/* Collapsible diagnostics */
|
|
519
|
+
details.diagnostics {
|
|
520
|
+
margin-top: 14px;
|
|
521
|
+
border: 1px solid var(--border-l1);
|
|
522
|
+
border-radius: 8px;
|
|
523
|
+
background: var(--bg-surface-elevated);
|
|
524
|
+
overflow: hidden;
|
|
525
|
+
}
|
|
526
|
+
details.diagnostics summary {
|
|
527
|
+
padding: 8px 12px;
|
|
528
|
+
font-size: 12.5px;
|
|
529
|
+
font-weight: 500;
|
|
530
|
+
color: var(--text-secondary);
|
|
531
|
+
cursor: pointer;
|
|
532
|
+
user-select: none;
|
|
533
|
+
display: flex;
|
|
534
|
+
align-items: center;
|
|
535
|
+
justify-content: space-between;
|
|
536
|
+
}
|
|
537
|
+
details.diagnostics summary:hover {
|
|
538
|
+
color: var(--text-primary);
|
|
539
|
+
}
|
|
540
|
+
.diagnostics-body {
|
|
541
|
+
padding: 12px;
|
|
542
|
+
border-top: 1px solid var(--border-l1);
|
|
543
|
+
display: flex;
|
|
544
|
+
flex-direction: column;
|
|
545
|
+
gap: 8px;
|
|
546
|
+
}
|
|
547
|
+
pre.code-view {
|
|
548
|
+
font-family: var(--ds-font-family-code);
|
|
549
|
+
font-size: 12px;
|
|
550
|
+
background: var(--bg-input);
|
|
551
|
+
border: 1px solid var(--border-l1);
|
|
552
|
+
border-radius: 6px;
|
|
553
|
+
padding: 10px;
|
|
554
|
+
overflow-x: auto;
|
|
555
|
+
word-break: break-all;
|
|
556
|
+
white-space: pre-wrap;
|
|
557
|
+
color: var(--text-secondary);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/* Import Form */
|
|
561
|
+
.import-section {
|
|
562
|
+
margin-top: 4px;
|
|
563
|
+
}
|
|
564
|
+
.import-textarea {
|
|
565
|
+
width: 100%;
|
|
566
|
+
font-family: var(--ds-font-family-code);
|
|
567
|
+
font-size: 12.5px;
|
|
568
|
+
padding: 10px 12px;
|
|
569
|
+
background: var(--bg-input);
|
|
570
|
+
border: 1px solid var(--border-l2);
|
|
571
|
+
border-radius: 8px;
|
|
572
|
+
color: var(--text-primary);
|
|
573
|
+
resize: vertical;
|
|
574
|
+
min-height: 70px;
|
|
575
|
+
outline: none;
|
|
576
|
+
transition: border-color var(--ds-transition-fast);
|
|
577
|
+
margin-bottom: 8px;
|
|
578
|
+
}
|
|
579
|
+
.import-textarea:focus {
|
|
580
|
+
border-color: var(--brand-primary);
|
|
581
|
+
box-shadow: 0 0 0 2px var(--brand-surface);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/* Toast Notification System */
|
|
585
|
+
#toast-container {
|
|
586
|
+
position: fixed;
|
|
587
|
+
bottom: 20px;
|
|
588
|
+
right: 20px;
|
|
589
|
+
z-index: 1000;
|
|
590
|
+
display: flex;
|
|
591
|
+
flex-direction: column;
|
|
592
|
+
gap: 8px;
|
|
593
|
+
max-width: 380px;
|
|
594
|
+
pointer-events: none;
|
|
595
|
+
}
|
|
596
|
+
.toast {
|
|
597
|
+
background: var(--bg-surface);
|
|
598
|
+
color: var(--text-primary);
|
|
599
|
+
border: 1px solid var(--border-l2);
|
|
600
|
+
border-radius: 8px;
|
|
601
|
+
padding: 10px 14px;
|
|
602
|
+
font-size: 13px;
|
|
603
|
+
box-shadow: var(--shadow-lg);
|
|
604
|
+
display: flex;
|
|
605
|
+
align-items: center;
|
|
606
|
+
justify-content: space-between;
|
|
607
|
+
gap: 10px;
|
|
608
|
+
pointer-events: auto;
|
|
609
|
+
animation: toast-in 0.2s var(--ds-ease-in-out);
|
|
610
|
+
transition: opacity 0.2s, transform 0.2s;
|
|
611
|
+
}
|
|
612
|
+
.toast-success { border-left: 4px solid var(--state-success); }
|
|
613
|
+
.toast-error { border-left: 4px solid var(--state-error); }
|
|
614
|
+
.toast-info { border-left: 4px solid var(--brand-primary); }
|
|
615
|
+
@keyframes toast-in {
|
|
616
|
+
from { transform: translateY(12px); opacity: 0; }
|
|
617
|
+
to { transform: translateY(0); opacity: 1; }
|
|
618
|
+
}
|
|
619
|
+
</style>
|
|
620
|
+
</head>
|
|
621
|
+
<body>
|
|
622
|
+
<div class="container">
|
|
623
|
+
<!-- Header -->
|
|
624
|
+
<header class="header">
|
|
625
|
+
<div class="header-title-group">
|
|
626
|
+
<h1>
|
|
627
|
+
<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>
|
|
628
|
+
<span id="txt-title">Antigravity Account Pool</span>
|
|
629
|
+
</h1>
|
|
630
|
+
<p id="txt-subtitle">Manage multi-account rotation, quota monitoring, and credentials.</p>
|
|
631
|
+
</div>
|
|
632
|
+
<div class="header-actions">
|
|
633
|
+
<button id="btn-lang" class="btn btn-secondary btn-sm" title="Toggle Language">中文</button>
|
|
634
|
+
<button id="btn-refresh" class="btn btn-secondary" title="Refresh accounts">
|
|
635
|
+
<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>
|
|
636
|
+
<span id="txt-refresh">Refresh</span>
|
|
637
|
+
</button>
|
|
638
|
+
<button id="btn-login" class="btn btn-primary">
|
|
639
|
+
<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>
|
|
640
|
+
<span id="txt-login">Login with Google</span>
|
|
641
|
+
</button>
|
|
642
|
+
</div>
|
|
643
|
+
</header>
|
|
644
|
+
|
|
645
|
+
<!-- Main Workspace -->
|
|
646
|
+
<main class="main-grid">
|
|
647
|
+
<!-- Master: Account List -->
|
|
648
|
+
<section class="card">
|
|
649
|
+
<div class="card-title">
|
|
650
|
+
<span id="txt-accounts-list">Accounts</span>
|
|
651
|
+
<span id="account-total-badge" class="badge badge-neutral">0</span>
|
|
652
|
+
</div>
|
|
653
|
+
<ul id="account-list" class="account-list">
|
|
654
|
+
<li style="color:var(--text-tertiary);font-size:13px;padding:8px 0;" id="txt-loading">Loading accounts...</li>
|
|
655
|
+
</ul>
|
|
656
|
+
</section>
|
|
657
|
+
|
|
658
|
+
<!-- Detail: Selected Account Details -->
|
|
659
|
+
<section class="card" id="detail-card">
|
|
660
|
+
<div id="detail-content">
|
|
661
|
+
<p style="color:var(--text-tertiary);" id="txt-no-accounts-hint">Select or add an account to view details.</p>
|
|
662
|
+
</div>
|
|
663
|
+
</section>
|
|
664
|
+
</main>
|
|
665
|
+
|
|
666
|
+
<!-- Bottom: Import Section -->
|
|
667
|
+
<section class="card import-section">
|
|
668
|
+
<div class="card-title">
|
|
669
|
+
<span id="txt-import-title">Import Credentials</span>
|
|
670
|
+
</div>
|
|
671
|
+
<p style="font-size:12.5px;color:var(--text-secondary);margin-bottom:8px;" id="txt-import-desc">
|
|
672
|
+
Paste an agy auth.json token document, or a credential blob (from dsh-agy login --blob):
|
|
673
|
+
</p>
|
|
674
|
+
<textarea id="input-import" class="import-textarea" placeholder='{"token":{"access_token":"...","refresh_token":"..."}} or dsh-agy-cred-v1....'></textarea>
|
|
675
|
+
<div style="display:flex;gap:8px;">
|
|
676
|
+
<button id="btn-import-json" class="btn btn-secondary">
|
|
677
|
+
<span id="txt-import-json">Import JSON</span>
|
|
678
|
+
</button>
|
|
679
|
+
<button id="btn-import-blob" class="btn btn-secondary">
|
|
680
|
+
<span id="txt-import-blob">Import Blob</span>
|
|
681
|
+
</button>
|
|
682
|
+
</div>
|
|
683
|
+
</section>
|
|
684
|
+
</div>
|
|
685
|
+
|
|
686
|
+
<div id="toast-container"></div>
|
|
687
|
+
|
|
688
|
+
<script>
|
|
689
|
+
const I18N = ${JSON.stringify(I18N_DICT)};
|
|
690
|
+
let currentLang = localStorage.getItem('agy_lang') || (navigator.language.startsWith('zh') ? 'zh' : 'en');
|
|
691
|
+
let accountData = [];
|
|
692
|
+
let selectedIndex = 0;
|
|
693
|
+
let isBusy = false;
|
|
694
|
+
|
|
695
|
+
function t(key) {
|
|
696
|
+
const dict = I18N[currentLang] || I18N.en;
|
|
697
|
+
return dict[key] || I18N.en[key] || key;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const $ = (id) => document.getElementById(id);
|
|
701
|
+
|
|
702
|
+
function esc(s) {
|
|
703
|
+
if (s == null) return '';
|
|
704
|
+
return String(s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function toast(text, type = 'info') {
|
|
708
|
+
const container = $('toast-container');
|
|
709
|
+
const el = document.createElement('div');
|
|
710
|
+
el.className = 'toast toast-' + type;
|
|
711
|
+
el.innerHTML = '<span>' + esc(text) + '</span><span style="cursor:pointer;opacity:0.6;font-weight:600;" onclick="this.parentElement.remove()">✕</span>';
|
|
712
|
+
container.appendChild(el);
|
|
713
|
+
setTimeout(() => {
|
|
714
|
+
el.style.opacity = '0';
|
|
715
|
+
el.style.transform = 'translateY(8px)';
|
|
716
|
+
setTimeout(() => el.remove(), 200);
|
|
717
|
+
}, 3500);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function copyToClipboard(text, msg) {
|
|
721
|
+
if (navigator.clipboard && window.isSecureContext) {
|
|
722
|
+
navigator.clipboard.writeText(text).then(() => toast(msg || t('copied'), 'success')).catch(() => fallbackCopy(text, msg));
|
|
723
|
+
} else {
|
|
724
|
+
fallbackCopy(text, msg);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function fallbackCopy(text, msg) {
|
|
729
|
+
const ta = document.createElement('textarea');
|
|
730
|
+
ta.value = text;
|
|
731
|
+
ta.style.position = 'fixed';
|
|
732
|
+
ta.style.left = '-9999px';
|
|
733
|
+
document.body.appendChild(ta);
|
|
734
|
+
ta.select();
|
|
735
|
+
try {
|
|
736
|
+
document.execCommand('copy');
|
|
737
|
+
toast(msg || t('copied'), 'success');
|
|
738
|
+
} catch {
|
|
739
|
+
toast('Failed to copy', 'error');
|
|
740
|
+
}
|
|
741
|
+
document.body.removeChild(ta);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function formatRelativeTime(dateStr) {
|
|
745
|
+
if (!dateStr) return '';
|
|
746
|
+
const target = new Date(dateStr).getTime();
|
|
747
|
+
const diffMs = target - Date.now();
|
|
748
|
+
if (diffMs <= 0) return '';
|
|
749
|
+
const diffMin = Math.round(diffMs / 60000);
|
|
750
|
+
if (diffMin < 60) return t('resetIn') + diffMin + 'm';
|
|
751
|
+
const diffHours = (diffMs / 3600000).toFixed(1);
|
|
752
|
+
if (diffHours < 24) return t('resetIn') + diffHours + 'h';
|
|
753
|
+
return t('resetAt') + new Date(target).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function quotaColor(fraction) {
|
|
757
|
+
if (fraction == null) return 'var(--text-dimmed)';
|
|
758
|
+
return fraction > 0.7 ? 'var(--state-success)' : fraction >= 0.3 ? 'var(--state-warn)' : 'var(--state-error)';
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function getWorstQuotaFraction(acc) {
|
|
762
|
+
const models = (acc.quota && acc.quota.models) || [];
|
|
763
|
+
let worst = 1;
|
|
764
|
+
let hasQuota = false;
|
|
765
|
+
for (const m of models) {
|
|
766
|
+
if (m.remainingFraction != null) {
|
|
767
|
+
worst = Math.min(worst, m.remainingFraction);
|
|
768
|
+
hasQuota = true;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
return hasQuota ? worst : null;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function applyI18nLabels() {
|
|
775
|
+
$('txt-title').textContent = t('title');
|
|
776
|
+
$('txt-subtitle').textContent = t('subtitle');
|
|
777
|
+
$('txt-refresh').textContent = t('refresh');
|
|
778
|
+
$('txt-login').textContent = t('loginWithGoogle');
|
|
779
|
+
$('btn-lang').textContent = t('switchLang');
|
|
780
|
+
$('txt-accounts-list').textContent = t('accountsList');
|
|
781
|
+
$('txt-import-title').textContent = t('importTitle');
|
|
782
|
+
$('txt-import-desc').textContent = t('importDesc');
|
|
783
|
+
$('txt-import-json').textContent = t('importJson');
|
|
784
|
+
$('txt-import-blob').textContent = t('importBlobBtn');
|
|
785
|
+
$('input-import').placeholder = t('importPlaceholder');
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
async function api(path, init) {
|
|
789
|
+
const res = await fetch('/agy/api' + path, {
|
|
790
|
+
headers: { 'content-type': 'application/json' },
|
|
791
|
+
...init
|
|
792
|
+
});
|
|
793
|
+
const data = await res.json().catch(() => ({}));
|
|
794
|
+
if (!res.ok) throw new Error(data.error || res.statusText || 'Request failed');
|
|
795
|
+
return data;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function renderMasterList() {
|
|
799
|
+
const listEl = $('account-list');
|
|
800
|
+
$('account-total-badge').textContent = String(accountData.length);
|
|
801
|
+
|
|
802
|
+
if (accountData.length === 0) {
|
|
803
|
+
listEl.innerHTML = '<li style="color:var(--text-tertiary);font-size:13px;padding:12px 0;">' + esc(t('noAccounts')) + '</li>';
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
listEl.innerHTML = accountData.map((a, i) => {
|
|
808
|
+
const isSelected = i === selectedIndex;
|
|
809
|
+
const worstFrac = getWorstQuotaFraction(a);
|
|
810
|
+
const dotColor = quotaColor(worstFrac);
|
|
811
|
+
|
|
812
|
+
let stateBadge = '';
|
|
813
|
+
if (a.state === 'disabled') {
|
|
814
|
+
stateBadge = '<span class="badge badge-error">' + esc(t('stateDisabled')) + '</span>';
|
|
815
|
+
} else if (a.state === 'verification-required') {
|
|
816
|
+
stateBadge = '<span class="badge badge-warn">' + esc(t('stateVerificationRequired')) + '</span>';
|
|
817
|
+
} else if (a.state === 'cooling') {
|
|
818
|
+
stateBadge = '<span class="badge badge-warn">' + esc(t('stateCooling')) + '</span>';
|
|
819
|
+
} else if (a.rateLimits && Object.values(a.rateLimits).some((t) => t > Date.now())) {
|
|
820
|
+
stateBadge = '<span class="badge badge-warn">' + esc(t('stateRateLimited')) + '</span>';
|
|
821
|
+
} else {
|
|
822
|
+
stateBadge = '<span class="badge badge-success">' + esc(t('stateActive')) + '</span>';
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
const activeMark = a.active ? '<span class="badge badge-primary">★ ' + esc(t('activeBadge')) + '</span>' : '';
|
|
826
|
+
|
|
827
|
+
return '<li class="account-item ' + (isSelected ? 'active-item' : '') + '" onclick="selectAccount(' + i + ')">' +
|
|
828
|
+
'<div class="account-item-top">' +
|
|
829
|
+
'<span class="account-email" title="' + esc(a.email || '(no email)') + '">' + esc(a.email || '(no email)') + '</span>' +
|
|
830
|
+
'<span class="dot" style="background:' + dotColor + '" title="Quota Health"></span>' +
|
|
831
|
+
'</div>' +
|
|
832
|
+
'<div class="account-item-bottom">' + activeMark + stateBadge + '</div>' +
|
|
833
|
+
'</li>';
|
|
834
|
+
}).join('');
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function renderDetailView() {
|
|
838
|
+
const container = $('detail-content');
|
|
839
|
+
if (accountData.length === 0) {
|
|
840
|
+
container.innerHTML = '<div style="text-align:center;padding:32px 12px;color:var(--text-tertiary);">' +
|
|
841
|
+
'<p style="font-size:15px;font-weight:500;margin-bottom:4px;">' + esc(t('noAccounts')) + '</p>' +
|
|
842
|
+
'<p style="font-size:13px;">' + esc(t('noAccountsHint')) + '</p>' +
|
|
843
|
+
'</div>';
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
const a = accountData[selectedIndex] || accountData[0];
|
|
848
|
+
const isVerificationRequired = a.state === 'verification-required';
|
|
849
|
+
const isCooling = a.state === 'cooling' && a.cooldownUntil;
|
|
850
|
+
const coolInfo = isCooling ? ' (' + formatRelativeTime(a.cooldownUntil) + ')' : '';
|
|
851
|
+
|
|
852
|
+
const models = (a.quota && a.quota.models) || [];
|
|
853
|
+
const quotaRows = models.map((m) => {
|
|
854
|
+
const pct = m.remainingFraction != null ? Math.round(m.remainingFraction * 100) : null;
|
|
855
|
+
const color = quotaColor(m.remainingFraction);
|
|
856
|
+
const resetText = m.resetTime ? formatRelativeTime(m.resetTime) : '';
|
|
857
|
+
const exactReset = m.resetTime ? new Date(m.resetTime).toLocaleString() : '';
|
|
858
|
+
|
|
859
|
+
return '<tr>' +
|
|
860
|
+
'<td><span class="model-name" onclick="copyToClipboard(\\'' + esc(m.id) + '\\')" title="Click to copy ID">' + esc(m.id) + '</span></td>' +
|
|
861
|
+
'<td>' +
|
|
862
|
+
(pct != null
|
|
863
|
+
? '<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>'
|
|
864
|
+
: '<span style="color:var(--text-tertiary);font-size:12px;">' + esc(t('noQuotaReported')) + '</span>') +
|
|
865
|
+
'</td>' +
|
|
866
|
+
'<td><span class="reset-time" title="' + esc(exactReset) + '">' + esc(resetText) + '</span></td>' +
|
|
867
|
+
'<td style="text-align:right;">' +
|
|
868
|
+
'<button class="btn btn-secondary btn-sm" data-test-model="' + esc(m.id) + '">' + esc(t('testModel')) + '</button>' +
|
|
869
|
+
'</td>' +
|
|
870
|
+
'</tr>';
|
|
871
|
+
}).join('');
|
|
872
|
+
|
|
873
|
+
container.innerHTML =
|
|
874
|
+
'<div class="detail-head">' +
|
|
875
|
+
'<div class="detail-meta">' +
|
|
876
|
+
'<h2>' + esc(a.email || '(no email)') + (a.active ? ' <span class="badge badge-primary">★ ' + esc(t('activeBadge')) + '</span>' : '') + '</h2>' +
|
|
877
|
+
'<p>' + esc(t('project')) + ': <code>' + esc(a.projectId || t('defaultProject')) + '</code>' + esc(coolInfo) + '</p>' +
|
|
878
|
+
'</div>' +
|
|
879
|
+
'<div class="detail-actions">' +
|
|
880
|
+
(isVerificationRequired ? '<button class="btn btn-cta btn-sm" id="btn-action-verify">⚡ ' + esc(t('verifyCta')) + '</button>' : '') +
|
|
881
|
+
(!a.active ? '<button class="btn btn-secondary btn-sm" id="btn-action-activate">' + esc(t('activate')) + '</button>' : '') +
|
|
882
|
+
(!isVerificationRequired ? '<button class="btn btn-secondary btn-sm" id="btn-action-verify">' + esc(t('verify')) + '</button>' : '') +
|
|
883
|
+
'<button class="btn btn-secondary btn-sm" id="btn-action-export">' + esc(t('exportBlob')) + '</button>' +
|
|
884
|
+
'<button class="btn btn-secondary btn-sm" id="btn-action-fp">' + esc(t('fingerprint')) + '</button>' +
|
|
885
|
+
'<button class="btn btn-danger btn-sm" id="btn-action-delete">' + esc(t('deleteAccount')) + '</button>' +
|
|
886
|
+
'</div>' +
|
|
887
|
+
'</div>' +
|
|
888
|
+
|
|
889
|
+
'<div style="margin-top:16px;">' +
|
|
890
|
+
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">' +
|
|
891
|
+
'<h3 style="font-size:13.5px;font-weight:600;">' + esc(t('quotaTitle')) + ' (' + models.length + ')</h3>' +
|
|
892
|
+
(models.length > 0 ? '<button class="btn btn-secondary btn-sm" id="btn-action-test-all">⚡ ' + esc(t('testAll')) + '</button>' : '') +
|
|
893
|
+
'</div>' +
|
|
894
|
+
'<div class="quota-table-wrap">' +
|
|
895
|
+
'<table class="quota-table">' +
|
|
896
|
+
'<thead><tr><th>Model</th><th>Remaining</th><th>Reset</th><th style="text-align:right;">Action</th></tr></thead>' +
|
|
897
|
+
'<tbody>' + (quotaRows || '<tr><td colspan="4" style="color:var(--text-tertiary);text-align:center;padding:16px 0;">' + esc(t('quotaUnavailable')) + '</td></tr>') + '</tbody>' +
|
|
898
|
+
'</table>' +
|
|
899
|
+
'</div>' +
|
|
900
|
+
'</div>' +
|
|
901
|
+
|
|
902
|
+
'<details class="diagnostics" id="diagnostics-panel">' +
|
|
903
|
+
'<summary><span>' + esc(t('fingerprintTitle')) + ' & Output</span><span>▼</span></summary>' +
|
|
904
|
+
'<div class="diagnostics-body">' +
|
|
905
|
+
'<div style="display:flex;justify-content:space-between;align-items:center;">' +
|
|
906
|
+
'<span style="font-size:12px;color:var(--text-secondary);">' + esc(t('fingerprintHistory')) + ': ' + (a.fingerprintHistory || 0) + '</span>' +
|
|
907
|
+
'<button class="btn btn-secondary btn-sm" id="btn-action-regen-fp">' + esc(t('regenerateFp')) + '</button>' +
|
|
908
|
+
'</div>' +
|
|
909
|
+
'<pre class="code-view" id="diagnostics-output">' + (a.fingerprint ? esc(JSON.stringify(a.fingerprint, null, 2)) : 'No diagnostics generated yet.') + '</pre>' +
|
|
910
|
+
'</div>' +
|
|
911
|
+
'</details>';
|
|
912
|
+
|
|
913
|
+
bindDetailActions(selectedIndex);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function selectAccount(index) {
|
|
917
|
+
selectedIndex = index;
|
|
918
|
+
renderMasterList();
|
|
919
|
+
renderDetailView();
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
function bindDetailActions(index) {
|
|
923
|
+
const btnActivate = $('btn-action-activate');
|
|
924
|
+
if (btnActivate) btnActivate.onclick = async () => {
|
|
925
|
+
try {
|
|
926
|
+
await api('/activate', { method: 'POST', body: JSON.stringify({ index }) });
|
|
927
|
+
toast(t('activeBadge') + ' -> #' + (index + 1), 'success');
|
|
928
|
+
render();
|
|
929
|
+
} catch (e) { toast(String(e), 'error'); }
|
|
930
|
+
};
|
|
931
|
+
|
|
932
|
+
const btnVerify = $('btn-action-verify');
|
|
933
|
+
if (btnVerify) btnVerify.onclick = async () => {
|
|
934
|
+
btnVerify.disabled = true;
|
|
935
|
+
try {
|
|
936
|
+
await api('/verify', { method: 'POST', body: JSON.stringify({ index }) });
|
|
937
|
+
toast('Account #' + (index + 1) + ' verified successfully', 'success');
|
|
938
|
+
render();
|
|
939
|
+
} catch (e) { toast(String(e), 'error'); }
|
|
940
|
+
finally { btnVerify.disabled = false; }
|
|
941
|
+
};
|
|
942
|
+
|
|
943
|
+
const btnDelete = $('btn-action-delete');
|
|
944
|
+
if (btnDelete) btnDelete.onclick = async () => {
|
|
945
|
+
if (!confirm(t('confirmDelete'))) return;
|
|
946
|
+
try {
|
|
947
|
+
await api('/delete', { method: 'POST', body: JSON.stringify({ index }) });
|
|
948
|
+
toast('Account deleted', 'success');
|
|
949
|
+
if (selectedIndex >= accountData.length - 1) selectedIndex = Math.max(0, accountData.length - 2);
|
|
950
|
+
render();
|
|
951
|
+
} catch (e) { toast(String(e), 'error'); }
|
|
952
|
+
};
|
|
953
|
+
|
|
954
|
+
const btnExport = $('btn-action-export');
|
|
955
|
+
if (btnExport) btnExport.onclick = async () => {
|
|
956
|
+
try {
|
|
957
|
+
const r = await api('/export', { method: 'POST', body: JSON.stringify({ index }) });
|
|
958
|
+
copyToClipboard(r.blob, t('copyBlobMsg'));
|
|
959
|
+
$('diagnostics-panel').open = true;
|
|
960
|
+
$('diagnostics-output').textContent = r.blob;
|
|
961
|
+
} catch (e) { toast(String(e), 'error'); }
|
|
962
|
+
};
|
|
963
|
+
|
|
964
|
+
const btnFp = $('btn-action-fp');
|
|
965
|
+
if (btnFp) btnFp.onclick = async () => {
|
|
966
|
+
try {
|
|
967
|
+
const r = await api('/fingerprint', { method: 'POST', body: JSON.stringify({ index }) });
|
|
968
|
+
$('diagnostics-panel').open = true;
|
|
969
|
+
$('diagnostics-output').textContent = JSON.stringify(r.fingerprint, null, 2);
|
|
970
|
+
toast(t('copyFpMsg'), 'info');
|
|
971
|
+
} catch (e) { toast(String(e), 'error'); }
|
|
972
|
+
};
|
|
973
|
+
|
|
974
|
+
const btnRegenFp = $('btn-action-regen-fp');
|
|
975
|
+
if (btnRegenFp) btnRegenFp.onclick = async () => {
|
|
976
|
+
if (!confirm(t('confirmRegenerateFp'))) return;
|
|
977
|
+
try {
|
|
978
|
+
const r = await api('/fingerprint', { method: 'POST', body: JSON.stringify({ index, action: 'regenerate' }) });
|
|
979
|
+
$('diagnostics-panel').open = true;
|
|
980
|
+
$('diagnostics-output').textContent = JSON.stringify(r.fingerprint, null, 2);
|
|
981
|
+
toast('Fingerprint regenerated', 'success');
|
|
982
|
+
render();
|
|
983
|
+
} catch (e) { toast(String(e), 'error'); }
|
|
984
|
+
};
|
|
985
|
+
|
|
986
|
+
const btnTestAll = $('btn-action-test-all');
|
|
987
|
+
if (btnTestAll) btnTestAll.onclick = async () => {
|
|
988
|
+
if (!confirm(t('confirmTestAll'))) return;
|
|
989
|
+
btnTestAll.disabled = true;
|
|
990
|
+
const a = accountData[index];
|
|
991
|
+
const models = ((a.quota && a.quota.models) || []).map((m) => m.id);
|
|
992
|
+
toast(t('testing') + ' (' + models.length + ')', 'info');
|
|
993
|
+
let okCount = 0;
|
|
994
|
+
let failCount = 0;
|
|
995
|
+
const lines = [];
|
|
996
|
+
|
|
997
|
+
for (const model of models) {
|
|
998
|
+
try {
|
|
999
|
+
const r = await api('/test', { method: 'POST', body: JSON.stringify({ model }) });
|
|
1000
|
+
if (r.ok) {
|
|
1001
|
+
okCount++;
|
|
1002
|
+
lines.push('OK ' + model);
|
|
1003
|
+
} else {
|
|
1004
|
+
failCount++;
|
|
1005
|
+
lines.push('FAIL ' + model + ' - ' + r.error);
|
|
1006
|
+
}
|
|
1007
|
+
} catch (err) {
|
|
1008
|
+
failCount++;
|
|
1009
|
+
lines.push('ERR ' + model + ' - ' + String(err));
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
$('diagnostics-panel').open = true;
|
|
1013
|
+
$('diagnostics-output').textContent = lines.join('\\n');
|
|
1014
|
+
toast(t('modelsTestedSummary').replace('{total}', String(models.length)).replace('{ok}', String(okCount)).replace('{failed}', String(failCount)), failCount === 0 ? 'success' : 'warn');
|
|
1015
|
+
btnTestAll.disabled = false;
|
|
1016
|
+
};
|
|
1017
|
+
|
|
1018
|
+
// Single model tests
|
|
1019
|
+
document.querySelectorAll('[data-test-model]').forEach((btn) => {
|
|
1020
|
+
btn.onclick = async () => {
|
|
1021
|
+
const model = btn.getAttribute('data-test-model');
|
|
1022
|
+
btn.disabled = true;
|
|
1023
|
+
btn.textContent = '...';
|
|
1024
|
+
try {
|
|
1025
|
+
const r = await api('/test', { method: 'POST', body: JSON.stringify({ model }) });
|
|
1026
|
+
if (r.ok) {
|
|
1027
|
+
toast('OK: ' + model, 'success');
|
|
1028
|
+
} else {
|
|
1029
|
+
toast('FAIL: ' + model + ' - ' + r.error, 'error');
|
|
1030
|
+
}
|
|
1031
|
+
} catch (err) {
|
|
1032
|
+
toast('ERR: ' + model + ' - ' + String(err), 'error');
|
|
1033
|
+
} finally {
|
|
1034
|
+
btn.disabled = false;
|
|
1035
|
+
btn.textContent = t('testModel');
|
|
1036
|
+
}
|
|
1037
|
+
};
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
async function render() {
|
|
1042
|
+
if (isBusy) return;
|
|
1043
|
+
isBusy = true;
|
|
1044
|
+
try {
|
|
1045
|
+
const data = await api('/accounts');
|
|
1046
|
+
accountData = data.accounts || [];
|
|
1047
|
+
if (selectedIndex >= accountData.length) selectedIndex = Math.max(0, accountData.length - 1);
|
|
1048
|
+
renderMasterList();
|
|
1049
|
+
renderDetailView();
|
|
1050
|
+
} catch (e) {
|
|
1051
|
+
toast(String(e), 'error');
|
|
1052
|
+
} finally {
|
|
1053
|
+
isBusy = false;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// Event Handlers
|
|
1058
|
+
$('btn-refresh').onclick = render;
|
|
1059
|
+
|
|
1060
|
+
$('btn-lang').onclick = () => {
|
|
1061
|
+
currentLang = currentLang === 'zh' ? 'en' : 'zh';
|
|
1062
|
+
localStorage.setItem('agy_lang', currentLang);
|
|
1063
|
+
applyI18nLabels();
|
|
1064
|
+
renderMasterList();
|
|
1065
|
+
renderDetailView();
|
|
1066
|
+
};
|
|
1067
|
+
|
|
1068
|
+
$('btn-login').onclick = async () => {
|
|
1069
|
+
try {
|
|
1070
|
+
const { url } = await api('/auth-url', { method: 'POST' });
|
|
1071
|
+
window.open(url, '_blank');
|
|
1072
|
+
toast(t('windowClosing'), 'info');
|
|
1073
|
+
} catch (e) { toast(String(e), 'error'); }
|
|
1074
|
+
};
|
|
1075
|
+
|
|
1076
|
+
$('btn-import-json').onclick = async () => {
|
|
1077
|
+
const source = $('input-import').value.trim();
|
|
1078
|
+
if (!source) return;
|
|
1079
|
+
try {
|
|
1080
|
+
const r = await api('/import', { method: 'POST', body: JSON.stringify({ kind: 'json', source }) });
|
|
1081
|
+
toast(t('importSuccess') + (r.email || '(no email)'), 'success');
|
|
1082
|
+
$('input-import').value = '';
|
|
1083
|
+
render();
|
|
1084
|
+
} catch (e) { toast(String(e), 'error'); }
|
|
1085
|
+
};
|
|
1086
|
+
|
|
1087
|
+
$('btn-import-blob').onclick = async () => {
|
|
1088
|
+
const source = $('input-import').value.trim();
|
|
1089
|
+
if (!source) return;
|
|
1090
|
+
try {
|
|
1091
|
+
const r = await api('/import', { method: 'POST', body: JSON.stringify({ kind: 'blob', source }) });
|
|
1092
|
+
toast(t('importSuccess') + (r.email || '(no email)'), 'success');
|
|
1093
|
+
$('input-import').value = '';
|
|
1094
|
+
render();
|
|
1095
|
+
} catch (e) { toast(String(e), 'error'); }
|
|
1096
|
+
};
|
|
1097
|
+
|
|
1098
|
+
// Auto-refresh when tab receives focus or receives postMessage from login popup
|
|
1099
|
+
window.addEventListener('focus', render);
|
|
1100
|
+
window.addEventListener('message', (ev) => {
|
|
1101
|
+
if (ev.data && ev.data.type === 'agy_login_success') {
|
|
1102
|
+
toast(t('loginSuccessTitle'), 'success');
|
|
1103
|
+
render();
|
|
1104
|
+
}
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
// Init
|
|
1108
|
+
applyI18nLabels();
|
|
1109
|
+
render();
|
|
1110
|
+
<\/script>
|
|
1111
|
+
</body>
|
|
1112
|
+
</html>`;
|
|
1113
|
+
}
|
|
1114
|
+
function renderCallbackHtml(options) {
|
|
1115
|
+
const { ok, error, email, baseUrl } = options;
|
|
1116
|
+
const i18nJson = JSON.stringify(I18N_DICT);
|
|
1117
|
+
return `<!doctype html>
|
|
1118
|
+
<html lang="en">
|
|
1119
|
+
<head>
|
|
1120
|
+
<meta charset="utf-8">
|
|
1121
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1122
|
+
<title>Antigravity Sign-in</title>
|
|
1123
|
+
<style>
|
|
1124
|
+
:root {
|
|
1125
|
+
color-scheme: light dark;
|
|
1126
|
+
--dsw-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
|
1127
|
+
--bg-page: #0f1115;
|
|
1128
|
+
--bg-surface: #171a21;
|
|
1129
|
+
--border-l2: rgba(255, 255, 255, 0.12);
|
|
1130
|
+
--text-primary: #f8fafc;
|
|
1131
|
+
--text-secondary: #94a3b8;
|
|
1132
|
+
--brand-primary: #5686fe;
|
|
1133
|
+
--state-success: #34d399;
|
|
1134
|
+
--state-error: #f87171;
|
|
1135
|
+
}
|
|
1136
|
+
@media (prefers-color-scheme: light) {
|
|
1137
|
+
:root {
|
|
1138
|
+
--bg-page: #f8fafc;
|
|
1139
|
+
--bg-surface: #ffffff;
|
|
1140
|
+
--border-l2: rgba(0, 0, 0, 0.12);
|
|
1141
|
+
--text-primary: #0f172a;
|
|
1142
|
+
--text-secondary: #475569;
|
|
1143
|
+
--brand-primary: #4176e6;
|
|
1144
|
+
--state-success: #22c55e;
|
|
1145
|
+
--state-error: #ef4444;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
1149
|
+
body {
|
|
1150
|
+
font-family: var(--dsw-font-family);
|
|
1151
|
+
background-color: var(--bg-page);
|
|
1152
|
+
color: var(--text-primary);
|
|
1153
|
+
display: flex;
|
|
1154
|
+
align-items: center;
|
|
1155
|
+
justify-content: center;
|
|
1156
|
+
min-height: 100vh;
|
|
1157
|
+
padding: 16px;
|
|
1158
|
+
}
|
|
1159
|
+
.card {
|
|
1160
|
+
background: var(--bg-surface);
|
|
1161
|
+
border: 1px solid var(--border-l2);
|
|
1162
|
+
border-radius: 12px;
|
|
1163
|
+
padding: 24px;
|
|
1164
|
+
max-width: 440px;
|
|
1165
|
+
width: 100%;
|
|
1166
|
+
text-align: center;
|
|
1167
|
+
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3);
|
|
1168
|
+
}
|
|
1169
|
+
.icon-wrap {
|
|
1170
|
+
width: 48px;
|
|
1171
|
+
height: 48px;
|
|
1172
|
+
border-radius: 50%;
|
|
1173
|
+
display: flex;
|
|
1174
|
+
align-items: center;
|
|
1175
|
+
justify-content: center;
|
|
1176
|
+
margin: 0 auto 16px;
|
|
1177
|
+
font-size: 24px;
|
|
1178
|
+
}
|
|
1179
|
+
.icon-success { background: rgba(52, 211, 153, 0.15); color: var(--state-success); }
|
|
1180
|
+
.icon-error { background: rgba(248, 113, 113, 0.15); color: var(--state-error); }
|
|
1181
|
+
h1 { font-size: 18px; font-weight: 600; margin-bottom: 8px; }
|
|
1182
|
+
p { font-size: 13.5px; color: var(--text-secondary); margin-bottom: 16px; line-height: 1.5; }
|
|
1183
|
+
.btn {
|
|
1184
|
+
display: inline-flex;
|
|
1185
|
+
align-items: center;
|
|
1186
|
+
justify-content: center;
|
|
1187
|
+
font-family: inherit;
|
|
1188
|
+
font-size: 13px;
|
|
1189
|
+
font-weight: 500;
|
|
1190
|
+
height: 34px;
|
|
1191
|
+
padding: 0 16px;
|
|
1192
|
+
border-radius: 17px;
|
|
1193
|
+
border: 1px solid var(--border-l2);
|
|
1194
|
+
background: transparent;
|
|
1195
|
+
color: var(--text-primary);
|
|
1196
|
+
cursor: pointer;
|
|
1197
|
+
text-decoration: none;
|
|
1198
|
+
}
|
|
1199
|
+
.btn:hover { background: rgba(128, 128, 128, 0.1); }
|
|
1200
|
+
.btn-primary { background: var(--brand-primary); color: #ffffff; border-color: transparent; }
|
|
1201
|
+
</style>
|
|
1202
|
+
</head>
|
|
1203
|
+
<body>
|
|
1204
|
+
<div class="card">
|
|
1205
|
+
${ok ? `
|
|
1206
|
+
<div class="icon-wrap icon-success">✓</div>
|
|
1207
|
+
<h1 id="title">Sign-in Successful</h1>
|
|
1208
|
+
<p id="desc">Your Antigravity account ${email ? "<strong>" + email + "</strong> " : ""}has been authorized and saved.</p>
|
|
1209
|
+
<p style="font-size:12px;opacity:0.8;" id="closing">This window will close automatically...</p>
|
|
1210
|
+
<button class="btn" onclick="window.close()">Close Window</button>
|
|
1211
|
+
` : `
|
|
1212
|
+
<div class="icon-wrap icon-error">✕</div>
|
|
1213
|
+
<h1 id="title">Sign-in Failed</h1>
|
|
1214
|
+
<p id="desc">Error details: ${error || "Unknown error"}</p>
|
|
1215
|
+
<a class="btn btn-primary" href="${baseUrl}/agy">Return to Dashboard</a>
|
|
1216
|
+
`}
|
|
1217
|
+
</div>
|
|
1218
|
+
<script>
|
|
1219
|
+
const I18N = ${i18nJson};
|
|
1220
|
+
const lang = localStorage.getItem('agy_lang') || (navigator.language.startsWith('zh') ? 'zh' : 'en');
|
|
1221
|
+
const dict = I18N[lang] || I18N.en;
|
|
1222
|
+
|
|
1223
|
+
if (${ok ? "true" : "false"}) {
|
|
1224
|
+
document.getElementById('title').textContent = dict.loginSuccessTitle;
|
|
1225
|
+
document.getElementById('desc').innerHTML = dict.loginSuccessDesc + (${JSON.stringify(email ? " (" + email + ")" : "")});
|
|
1226
|
+
document.getElementById('closing').textContent = dict.windowClosing;
|
|
1227
|
+
|
|
1228
|
+
// Notify parent window & close
|
|
1229
|
+
try {
|
|
1230
|
+
if (window.opener) {
|
|
1231
|
+
window.opener.postMessage({ type: 'agy_login_success' }, '*');
|
|
1232
|
+
}
|
|
1233
|
+
} catch (e) {}
|
|
1234
|
+
setTimeout(() => {
|
|
1235
|
+
window.close();
|
|
1236
|
+
}, 1800);
|
|
1237
|
+
} else {
|
|
1238
|
+
document.getElementById('title').textContent = dict.loginFailedTitle;
|
|
1239
|
+
}
|
|
1240
|
+
<\/script>
|
|
1241
|
+
</body>
|
|
1242
|
+
</html>`;
|
|
1243
|
+
}
|
|
1244
|
+
//#endregion
|
|
1245
|
+
//#region src/web/routes.ts
|
|
1246
|
+
function sendJson(res, status, data) {
|
|
1247
|
+
res.writeHead(status, { "content-type": "application/json" });
|
|
1248
|
+
res.end(JSON.stringify(data));
|
|
1249
|
+
}
|
|
1250
|
+
async function readJson(req) {
|
|
1251
|
+
const chunks = [];
|
|
1252
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
1253
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
1254
|
+
return text ? JSON.parse(text) : {};
|
|
1255
|
+
}
|
|
1256
|
+
function createAgyWebRoutes(options) {
|
|
1257
|
+
const { store, sessions, baseUrl } = options;
|
|
1258
|
+
const pendingAuth = /* @__PURE__ */ new Map();
|
|
1259
|
+
const PENDING_AUTH_TTL_MS = 6e5;
|
|
1260
|
+
const prunePendingAuth = (now = Date.now()) => {
|
|
1261
|
+
for (const [key, entry] of pendingAuth) if (entry.expiresAt <= now) pendingAuth.delete(key);
|
|
1262
|
+
while (pendingAuth.size > 100) {
|
|
1263
|
+
let oldestKey = null;
|
|
1264
|
+
let oldestExpiry = Infinity;
|
|
1265
|
+
for (const [key, entry] of pendingAuth) if (entry.expiresAt < oldestExpiry) {
|
|
1266
|
+
oldestExpiry = entry.expiresAt;
|
|
1267
|
+
oldestKey = key;
|
|
1268
|
+
}
|
|
1269
|
+
if (oldestKey) pendingAuth.delete(oldestKey);
|
|
1270
|
+
else break;
|
|
1271
|
+
}
|
|
1272
|
+
};
|
|
1273
|
+
const accountsList = async () => {
|
|
1274
|
+
const storage = await store.load();
|
|
1275
|
+
const now = Date.now();
|
|
1276
|
+
const list = [];
|
|
1277
|
+
for (const [index, account] of storage.accounts.entries()) {
|
|
1278
|
+
const entry = {
|
|
1279
|
+
index,
|
|
1280
|
+
email: account.email ?? null,
|
|
1281
|
+
projectId: account.projectId ?? null,
|
|
1282
|
+
active: index === storage.activeIndex && account.enabled !== false,
|
|
1283
|
+
state: account.enabled === false ? "disabled" : account.verificationRequired ? "verification-required" : account.coolingDownUntil && account.coolingDownUntil > now ? "cooling" : "active",
|
|
1284
|
+
cooldownUntil: account.coolingDownUntil && account.coolingDownUntil > now ? new Date(account.coolingDownUntil).toISOString() : null,
|
|
1285
|
+
cooldownReason: account.cooldownReason ?? null,
|
|
1286
|
+
rateLimits: account.rateLimitResetTimes ?? null,
|
|
1287
|
+
fingerprint: account.fingerprint ? {
|
|
1288
|
+
userAgent: account.fingerprint.userAgent,
|
|
1289
|
+
deviceId: account.fingerprint.deviceId,
|
|
1290
|
+
createdAt: account.fingerprint.createdAt
|
|
1291
|
+
} : null,
|
|
1292
|
+
fingerprintHistory: (account.fingerprintHistory ?? []).length,
|
|
1293
|
+
quota: null
|
|
1294
|
+
};
|
|
1295
|
+
list.push(entry);
|
|
1296
|
+
}
|
|
1297
|
+
for (const entry of list) {
|
|
1298
|
+
const session = await sessions.getSession().catch(() => void 0);
|
|
1299
|
+
if (!session || session.index !== entry.index) continue;
|
|
1300
|
+
try {
|
|
1301
|
+
const { fetchAvailableModels } = await import("../models-Cu1g7AXV.mjs").then((n) => n.n);
|
|
1302
|
+
const discovered = await fetchAvailableModels(session.auth.access, session.account.projectId);
|
|
1303
|
+
const models = Object.entries(discovered.models ?? {});
|
|
1304
|
+
if (models.length > 0) {
|
|
1305
|
+
const quotaRows = models.map(([id, m]) => ({
|
|
1306
|
+
id,
|
|
1307
|
+
remainingFraction: typeof m.quotaInfo?.remainingFraction === "number" ? Math.max(0, Math.min(1, m.quotaInfo.remainingFraction)) : null,
|
|
1308
|
+
resetTime: m.quotaInfo?.resetTime ?? null
|
|
1309
|
+
})).sort((a, b) => (a.remainingFraction ?? -1) - (b.remainingFraction ?? -1));
|
|
1310
|
+
entry.quota = {
|
|
1311
|
+
modelCount: models.length,
|
|
1312
|
+
models: quotaRows
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
} catch {}
|
|
1316
|
+
}
|
|
1317
|
+
return list;
|
|
1318
|
+
};
|
|
1319
|
+
const handleAuthUrl = async (res) => {
|
|
1320
|
+
const redirectUri = `${baseUrl}/agy/oauth-callback`;
|
|
1321
|
+
const authorization = await authorizeAntigravity(redirectUri);
|
|
1322
|
+
prunePendingAuth();
|
|
1323
|
+
pendingAuth.set(authorization.state, {
|
|
1324
|
+
verifier: authorization.verifier,
|
|
1325
|
+
expiresAt: Date.now() + PENDING_AUTH_TTL_MS
|
|
1326
|
+
});
|
|
1327
|
+
sendJson(res, 200, { url: authorization.url });
|
|
1328
|
+
};
|
|
1329
|
+
const handleCallback = async (req, res) => {
|
|
1330
|
+
const url = new URL(req.url ?? "/", baseUrl);
|
|
1331
|
+
const code = url.searchParams.get("code");
|
|
1332
|
+
const state = url.searchParams.get("state");
|
|
1333
|
+
if (!code || !state) {
|
|
1334
|
+
res.writeHead(400, { "content-type": "text/html; charset=utf-8" });
|
|
1335
|
+
res.end(renderCallbackHtml({
|
|
1336
|
+
ok: false,
|
|
1337
|
+
error: "Missing code or state parameter",
|
|
1338
|
+
baseUrl
|
|
1339
|
+
}));
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
const expected = pendingAuth.get(state);
|
|
1343
|
+
if (!expected) {
|
|
1344
|
+
res.writeHead(400, { "content-type": "text/html; charset=utf-8" });
|
|
1345
|
+
res.end(renderCallbackHtml({
|
|
1346
|
+
ok: false,
|
|
1347
|
+
error: "Unknown or expired authorization — please start a new login.",
|
|
1348
|
+
baseUrl
|
|
1349
|
+
}));
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
pendingAuth.delete(state);
|
|
1353
|
+
const redirectUri = `${baseUrl}/agy/oauth-callback`;
|
|
1354
|
+
const result = await exchangeAntigravity(code, state, redirectUri, expected.verifier);
|
|
1355
|
+
if (result.type === "failed") {
|
|
1356
|
+
res.writeHead(400, { "content-type": "text/html; charset=utf-8" });
|
|
1357
|
+
res.end(renderCallbackHtml({
|
|
1358
|
+
ok: false,
|
|
1359
|
+
error: result.error,
|
|
1360
|
+
baseUrl
|
|
1361
|
+
}));
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
await upsertImportedAccount(store, {
|
|
1365
|
+
accessToken: result.access,
|
|
1366
|
+
refreshToken: result.refresh.split("|")[0],
|
|
1367
|
+
tokenType: "Bearer",
|
|
1368
|
+
expiresAt: new Date(result.expires).toISOString(),
|
|
1369
|
+
authMethod: "oauth",
|
|
1370
|
+
email: result.email ?? null,
|
|
1371
|
+
projectId: result.projectId || null
|
|
1372
|
+
}, { overwriteExisting: true });
|
|
1373
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
1374
|
+
res.end(renderCallbackHtml({
|
|
1375
|
+
ok: true,
|
|
1376
|
+
email: result.email ?? null,
|
|
1377
|
+
baseUrl
|
|
1378
|
+
}));
|
|
1379
|
+
};
|
|
1380
|
+
const handleImport = async (req, res) => {
|
|
1381
|
+
try {
|
|
1382
|
+
const body = await readJson(req);
|
|
1383
|
+
const kind = body.kind === "blob" ? "blob" : "json";
|
|
1384
|
+
const enriched = await parseImportSource(body.source, kind);
|
|
1385
|
+
const { account } = await upsertImportedAccount(store, enriched, { overwriteExisting: true });
|
|
1386
|
+
sendJson(res, 200, {
|
|
1387
|
+
email: account.email ?? null,
|
|
1388
|
+
projectId: enriched.projectId
|
|
1389
|
+
});
|
|
1390
|
+
} catch (error) {
|
|
1391
|
+
sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
1392
|
+
}
|
|
1393
|
+
};
|
|
1394
|
+
const handleVerify = async (req, res) => {
|
|
1395
|
+
try {
|
|
1396
|
+
const body = await readJson(req);
|
|
1397
|
+
const index = Number(body.index);
|
|
1398
|
+
const result = await sessions.verifyAccount(index);
|
|
1399
|
+
sendJson(res, result.ok ? 200 : 400, result);
|
|
1400
|
+
} catch (error) {
|
|
1401
|
+
sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
1402
|
+
}
|
|
1403
|
+
};
|
|
1404
|
+
const handleDelete = async (req, res) => {
|
|
1405
|
+
try {
|
|
1406
|
+
const body = await readJson(req);
|
|
1407
|
+
const index = Number(body.index);
|
|
1408
|
+
await store.mutate((storage) => {
|
|
1409
|
+
if (index < 0 || index >= storage.accounts.length) throw new Error("account not found");
|
|
1410
|
+
storage.accounts.splice(index, 1);
|
|
1411
|
+
if (storage.activeIndex >= storage.accounts.length) storage.activeIndex = 0;
|
|
1412
|
+
});
|
|
1413
|
+
sendJson(res, 200, { ok: true });
|
|
1414
|
+
} catch (error) {
|
|
1415
|
+
sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
1416
|
+
}
|
|
1417
|
+
};
|
|
1418
|
+
const handleActivate = async (req, res) => {
|
|
1419
|
+
try {
|
|
1420
|
+
const body = await readJson(req);
|
|
1421
|
+
const index = Number(body.index);
|
|
1422
|
+
await store.mutate((storage) => {
|
|
1423
|
+
if (index < 0 || index >= storage.accounts.length) throw new Error("account not found");
|
|
1424
|
+
storage.activeIndex = index;
|
|
1425
|
+
});
|
|
1426
|
+
sendJson(res, 200, {
|
|
1427
|
+
ok: true,
|
|
1428
|
+
index
|
|
1429
|
+
});
|
|
1430
|
+
} catch (error) {
|
|
1431
|
+
sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
1432
|
+
}
|
|
1433
|
+
};
|
|
1434
|
+
const handleModels = async (_req, res) => {
|
|
1435
|
+
const session = await sessions.getSession().catch(() => void 0);
|
|
1436
|
+
if (!session) {
|
|
1437
|
+
sendJson(res, 400, { error: "No agy account configured — run `dsh-agy login` first." });
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
try {
|
|
1441
|
+
const { listAgyModels } = await import("../models-Cu1g7AXV.mjs").then((n) => n.n);
|
|
1442
|
+
const models = await listAgyModels(session.auth.access, session.account.projectId);
|
|
1443
|
+
sendJson(res, 200, {
|
|
1444
|
+
account: session.account.email ?? null,
|
|
1445
|
+
models: models.map((m) => ({
|
|
1446
|
+
id: m.id,
|
|
1447
|
+
name: m.name
|
|
1448
|
+
}))
|
|
1449
|
+
});
|
|
1450
|
+
} catch (error) {
|
|
1451
|
+
sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
1452
|
+
}
|
|
1453
|
+
};
|
|
1454
|
+
const handleTest = async (req, res) => {
|
|
1455
|
+
try {
|
|
1456
|
+
const body = await readJson(req);
|
|
1457
|
+
const model = typeof body.model === "string" ? body.model : "";
|
|
1458
|
+
if (!model) {
|
|
1459
|
+
sendJson(res, 400, { error: "model is required" });
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
const result = await sessions.testCall(model);
|
|
1463
|
+
sendJson(res, result.ok ? 200 : 400, result);
|
|
1464
|
+
} catch (error) {
|
|
1465
|
+
sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
1466
|
+
}
|
|
1467
|
+
};
|
|
1468
|
+
const handleExport = async (req, res) => {
|
|
1469
|
+
try {
|
|
1470
|
+
const body = await readJson(req);
|
|
1471
|
+
const index = Number(body.index);
|
|
1472
|
+
const result = await sessions.exportBlob(index);
|
|
1473
|
+
if (result.error) {
|
|
1474
|
+
sendJson(res, 400, result);
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
sendJson(res, 200, { blob: result.blob });
|
|
1478
|
+
} catch (error) {
|
|
1479
|
+
sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
1480
|
+
}
|
|
1481
|
+
};
|
|
1482
|
+
const handleFingerprint = async (req, res) => {
|
|
1483
|
+
try {
|
|
1484
|
+
const body = await readJson(req);
|
|
1485
|
+
const index = Number(body.index);
|
|
1486
|
+
const action = body.action === "regenerate" ? "regenerate" : "show";
|
|
1487
|
+
sendJson(res, 200, await store.mutate(async (storage) => {
|
|
1488
|
+
const account = storage.accounts[index];
|
|
1489
|
+
if (!account) throw new Error("account not found");
|
|
1490
|
+
if (action === "regenerate") {
|
|
1491
|
+
const fresh = generateFingerprint(void 0, await resolveAntigravityVersionBounded());
|
|
1492
|
+
account.fingerprint = fresh;
|
|
1493
|
+
account.fingerprintHistory = recordFingerprintVersion(account.fingerprintHistory, fresh, "regenerated");
|
|
1494
|
+
}
|
|
1495
|
+
return {
|
|
1496
|
+
action,
|
|
1497
|
+
fingerprint: account.fingerprint ? {
|
|
1498
|
+
userAgent: account.fingerprint.userAgent,
|
|
1499
|
+
deviceId: account.fingerprint.deviceId,
|
|
1500
|
+
createdAt: account.fingerprint.createdAt
|
|
1501
|
+
} : null,
|
|
1502
|
+
history: account.fingerprintHistory?.length ?? 0
|
|
1503
|
+
};
|
|
1504
|
+
}));
|
|
1505
|
+
} catch (error) {
|
|
1506
|
+
sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
1507
|
+
}
|
|
1508
|
+
};
|
|
1509
|
+
return [
|
|
1510
|
+
{
|
|
1511
|
+
kind: "exact",
|
|
1512
|
+
path: "/agy",
|
|
1513
|
+
handler: (req, res) => {
|
|
1514
|
+
if (req.method === "GET") {
|
|
1515
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
1516
|
+
res.end(renderDashboardHtml());
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
res.writeHead(405);
|
|
1520
|
+
res.end();
|
|
1521
|
+
}
|
|
1522
|
+
},
|
|
1523
|
+
{
|
|
1524
|
+
kind: "exact",
|
|
1525
|
+
path: "/agy/oauth-callback",
|
|
1526
|
+
handler: handleCallback
|
|
1527
|
+
},
|
|
1528
|
+
{
|
|
1529
|
+
kind: "exact",
|
|
1530
|
+
path: "/agy/api/accounts",
|
|
1531
|
+
handler: async (_req, res) => {
|
|
1532
|
+
sendJson(res, 200, { accounts: await accountsList() });
|
|
1533
|
+
}
|
|
1534
|
+
},
|
|
1535
|
+
{
|
|
1536
|
+
kind: "exact",
|
|
1537
|
+
path: "/agy/api/auth-url",
|
|
1538
|
+
handler: async (_req, res) => {
|
|
1539
|
+
await handleAuthUrl(res);
|
|
1540
|
+
}
|
|
1541
|
+
},
|
|
1542
|
+
{
|
|
1543
|
+
kind: "exact",
|
|
1544
|
+
path: "/agy/api/import",
|
|
1545
|
+
handler: handleImport
|
|
1546
|
+
},
|
|
1547
|
+
{
|
|
1548
|
+
kind: "exact",
|
|
1549
|
+
path: "/agy/api/verify",
|
|
1550
|
+
handler: handleVerify
|
|
1551
|
+
},
|
|
1552
|
+
{
|
|
1553
|
+
kind: "exact",
|
|
1554
|
+
path: "/agy/api/delete",
|
|
1555
|
+
handler: handleDelete
|
|
1556
|
+
},
|
|
1557
|
+
{
|
|
1558
|
+
kind: "exact",
|
|
1559
|
+
path: "/agy/api/activate",
|
|
1560
|
+
handler: handleActivate
|
|
1561
|
+
},
|
|
1562
|
+
{
|
|
1563
|
+
kind: "exact",
|
|
1564
|
+
path: "/agy/api/models",
|
|
1565
|
+
handler: handleModels
|
|
1566
|
+
},
|
|
1567
|
+
{
|
|
1568
|
+
kind: "exact",
|
|
1569
|
+
path: "/agy/api/test",
|
|
1570
|
+
handler: handleTest
|
|
1571
|
+
},
|
|
1572
|
+
{
|
|
1573
|
+
kind: "exact",
|
|
1574
|
+
path: "/agy/api/export",
|
|
1575
|
+
handler: handleExport
|
|
1576
|
+
},
|
|
1577
|
+
{
|
|
1578
|
+
kind: "exact",
|
|
1579
|
+
path: "/agy/api/fingerprint",
|
|
1580
|
+
handler: handleFingerprint
|
|
1581
|
+
}
|
|
1582
|
+
];
|
|
1583
|
+
}
|
|
1584
|
+
//#endregion
|
|
1585
|
+
//#region src/web/plugin.ts
|
|
1586
|
+
const name = "dsh-agy-web";
|
|
1587
|
+
const inject = ["llm", "webServer"];
|
|
1588
|
+
function apply(ctx) {
|
|
1589
|
+
ctx.effect(async () => {
|
|
1590
|
+
const webServer = ctx.get("webServer");
|
|
1591
|
+
if (!webServer) return () => {};
|
|
1592
|
+
const webStartup = ctx.get("webStartup");
|
|
1593
|
+
const host = webStartup?.host ?? "127.0.0.1";
|
|
1594
|
+
const port = webStartup?.port ?? 3080;
|
|
1595
|
+
const bindHost = webServer.host ?? host;
|
|
1596
|
+
if (![
|
|
1597
|
+
"127.0.0.1",
|
|
1598
|
+
"localhost",
|
|
1599
|
+
"::1"
|
|
1600
|
+
].includes(bindHost)) {
|
|
1601
|
+
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.");
|
|
1602
|
+
return () => {};
|
|
1603
|
+
}
|
|
1604
|
+
const { store, sessions } = await createAgyRuntime(ctx);
|
|
1605
|
+
const disposers = createAgyWebRoutes({
|
|
1606
|
+
store,
|
|
1607
|
+
sessions,
|
|
1608
|
+
baseUrl: `http://${host}:${port}`
|
|
1609
|
+
}).map((route) => webServer.register(route));
|
|
1610
|
+
return () => {
|
|
1611
|
+
for (const dispose of disposers) dispose();
|
|
1612
|
+
};
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1615
|
+
//#endregion
|
|
1616
|
+
export { apply, inject, name };
|
|
1617
|
+
|
|
1618
|
+
//# sourceMappingURL=plugin.mjs.map
|