dsh-agy-link 0.3.4 → 0.4.5

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/dist/client.js CHANGED
@@ -5,81 +5,397 @@ window.__ModuleLoader__.load({
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  //#region src/client/index.ts
8
- const { createElement: h, useState, useEffect } = require("react");
8
+ const { createElement: h, useState, useEffect, useRef } = require("react");
9
+ const reactDom = require("react-dom");
9
10
  const win = globalThis;
11
+ const bodyEl = win.document?.body ?? null;
12
+ const portalToBody = (node) => {
13
+ if (bodyEl && reactDom && typeof reactDom.createPortal === "function") return reactDom.createPortal(node, bodyEl);
14
+ return node;
15
+ };
10
16
  const name = "dsh-agy-link-client";
11
17
  const inject = ["slots"];
12
18
  const base = win.location?.origin ?? "";
19
+ let statusCache = null;
13
20
  async function getStatus() {
14
21
  try {
15
22
  const res = await win.fetch?.(base + "/plugins/agy-link/status");
16
23
  if (!res || !res.ok) return null;
17
- return await res.json();
24
+ const payload = await res.json();
25
+ statusCache = payload;
26
+ return payload;
18
27
  } catch {
19
28
  return null;
20
29
  }
21
30
  }
22
31
  async function postJson(path, body) {
23
32
  try {
24
- return await win.fetch?.(base + path, {
33
+ const res = await win.fetch?.(base + path, {
25
34
  method: "POST",
26
35
  headers: { "Content-Type": "application/json" },
27
36
  body: JSON.stringify(body)
28
- }) ?? null;
37
+ });
38
+ if (!res) return null;
39
+ try {
40
+ return await res.json();
41
+ } catch {
42
+ return { ok: res.ok };
43
+ }
29
44
  } catch {
30
45
  return null;
31
46
  }
32
47
  }
48
+ /** Global modal state for opening Antigravity console dialog from footer shortcut or header status */
49
+ const agyModalStore = {
50
+ open: false,
51
+ listeners: /* @__PURE__ */ new Set(),
52
+ setOpen(v) {
53
+ agyModalStore.open = v;
54
+ agyModalStore.listeners.forEach((fn) => fn());
55
+ },
56
+ subscribe(fn) {
57
+ agyModalStore.listeners.add(fn);
58
+ return () => {
59
+ agyModalStore.listeners.delete(fn);
60
+ };
61
+ }
62
+ };
63
+ /** Smart parser for 5-hour rolling window vs weekly cap */
64
+ function formatQuotaWindow(resetTimeStr, remainingFraction) {
65
+ if (!resetTimeStr) return {
66
+ windowType: "5小时额度",
67
+ resetText: "",
68
+ countdownText: "",
69
+ isWeeklyLockout: false
70
+ };
71
+ try {
72
+ const d = new Date(resetTimeStr);
73
+ const diffMs = d.getTime() - Date.now();
74
+ const timeStr = `${d.getHours().toString().padStart(2, "0")}:${d.getMinutes().toString().padStart(2, "0")}`;
75
+ const isWeekly = diffMs > 216e5;
76
+ const windowType = isWeekly ? "周额度" : "5小时额度";
77
+ let countdown = "";
78
+ if (diffMs > 0) {
79
+ const totalMins = Math.ceil(diffMs / 6e4);
80
+ const hours = Math.floor(totalMins / 60);
81
+ const mins = totalMins % 60;
82
+ if (hours >= 24) countdown = `${Math.floor(hours / 24)}天${hours % 24}小时后`;
83
+ else if (hours > 0) countdown = `${hours}小时${mins}分后`;
84
+ else countdown = `${mins}分钟后`;
85
+ } else countdown = "即将重置";
86
+ let resetText = "";
87
+ if (isWeekly) resetText = `${(d.getMonth() + 1).toString().padStart(2, "0")}/${d.getDate().toString().padStart(2, "0")} ${timeStr} (${countdown})`;
88
+ else resetText = `${timeStr} (${countdown})`;
89
+ return {
90
+ windowType,
91
+ resetText,
92
+ countdownText: countdown,
93
+ isWeeklyLockout: isWeekly && (remainingFraction ?? 1) <= .05
94
+ };
95
+ } catch {
96
+ return {
97
+ windowType: "5小时额度",
98
+ resetText: "",
99
+ countdownText: "",
100
+ isWeeklyLockout: false
101
+ };
102
+ }
103
+ }
104
+ const GLOBAL_CSS = `
105
+ @keyframes agy-spin {
106
+ 0% { transform: rotate(0deg); }
107
+ 100% { transform: rotate(360deg); }
108
+ }
109
+ .agy-spinner {
110
+ display: inline-block;
111
+ width: 12px;
112
+ height: 12px;
113
+ border: 2px solid currentColor;
114
+ border-top-color: transparent;
115
+ border-radius: 50%;
116
+ animation: agy-spin 0.75s linear infinite;
117
+ vertical-align: -2px;
118
+ margin-right: 5px;
119
+ }
120
+ .agy-pulse-dot {
121
+ display: inline-block;
122
+ width: 8px;
123
+ height: 8px;
124
+ border-radius: 50%;
125
+ }
126
+ .agy-card-hover {
127
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
128
+ }
129
+ .agy-card-hover:hover {
130
+ border-color: rgba(128,128,128,0.35) !important;
131
+ }
132
+ .agy-btn {
133
+ transition: all 0.15s ease;
134
+ }
135
+ .agy-btn:hover:not(:disabled) {
136
+ filter: brightness(1.12);
137
+ transform: translateY(-0.5px);
138
+ }
139
+ .agy-btn:active:not(:disabled) {
140
+ transform: translateY(0.5px);
141
+ }
142
+ .agy-progress-fill {
143
+ transition: width 0.4s cubic-bezier(0.16, 1, 0.3, 1);
144
+ }
145
+ .agy-modal-backdrop {
146
+ position: fixed;
147
+ inset: 0;
148
+ z-index: 9999;
149
+ background: rgba(0, 0, 0, 0.55);
150
+ backdrop-filter: blur(4px);
151
+ display: flex;
152
+ align-items: center;
153
+ justify-content: center;
154
+ padding: 16px;
155
+ box-sizing: border-box;
156
+ }
157
+ .agy-modal-panel {
158
+ position: relative;
159
+ width: 100%;
160
+ max-width: 640px;
161
+ max-height: min(820px, calc(100vh - 48px));
162
+ background: var(--dsw-specific-menu, #1c1d22);
163
+ border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.25));
164
+ border-radius: 12px;
165
+ box-shadow: 0 20px 60px rgba(0,0,0,0.5);
166
+ display: flex;
167
+ flex-direction: column;
168
+ overflow: hidden;
169
+ color: inherit;
170
+ font-family: inherit;
171
+ }
172
+ .agy-submodel-row {
173
+ display: flex;
174
+ align-items: center;
175
+ justify-content: space-between;
176
+ padding: 3px 6px;
177
+ font-size: 10.5px;
178
+ border-radius: 4px;
179
+ background: rgba(128,128,128,0.06);
180
+ margin: 2px 0;
181
+ }
182
+ `;
33
183
  const S = {
34
- row: {
184
+ container: {
185
+ lineHeight: 1.5,
186
+ fontSize: "13px",
187
+ fontFamily: "inherit",
188
+ color: "inherit"
189
+ },
190
+ headerCard: {
35
191
  display: "flex",
36
192
  alignItems: "center",
37
- gap: "8px",
38
- marginBottom: "8px"
193
+ justifyContent: "space-between",
194
+ padding: "12px 14px",
195
+ background: "rgba(128,128,128,0.06)",
196
+ border: "1px solid rgba(128,128,128,0.18)",
197
+ borderRadius: "8px",
198
+ marginBottom: "12px"
199
+ },
200
+ badgePrimary: {
201
+ display: "inline-flex",
202
+ alignItems: "center",
203
+ gap: "3px",
204
+ background: "rgba(59,130,246,0.15)",
205
+ color: "#3b82f6",
206
+ border: "1px solid rgba(59,130,246,0.35)",
207
+ padding: "1px 7px",
208
+ borderRadius: "5px",
209
+ fontSize: "11px",
210
+ fontWeight: 600
211
+ },
212
+ badgeTag: {
213
+ display: "inline-flex",
214
+ alignItems: "center",
215
+ gap: "3px",
216
+ background: "rgba(128,128,128,0.12)",
217
+ color: "inherit",
218
+ border: "1px solid rgba(128,128,128,0.22)",
219
+ padding: "1px 6px",
220
+ borderRadius: "4px",
221
+ fontSize: "11px"
222
+ },
223
+ card: {
224
+ background: "rgba(128,128,128,0.04)",
225
+ border: "1px solid rgba(128,128,128,0.18)",
226
+ borderRadius: "8px",
227
+ padding: "12px 14px",
228
+ marginBottom: "10px"
229
+ },
230
+ cardPrimary: {
231
+ background: "rgba(59,130,246,0.03)",
232
+ border: "1px solid rgba(59,130,246,0.32)",
233
+ borderRadius: "8px",
234
+ padding: "12px 14px",
235
+ marginBottom: "10px"
236
+ },
237
+ quotaBox: {
238
+ background: "rgba(0,0,0,0.14)",
239
+ border: "1px solid rgba(128,128,128,0.14)",
240
+ borderRadius: "6px",
241
+ padding: "8px 12px",
242
+ marginTop: "8px"
243
+ },
244
+ progressBarBg: {
245
+ flex: "1",
246
+ height: "6px",
247
+ borderRadius: "3px",
248
+ background: "rgba(128,128,128,0.2)",
249
+ overflow: "hidden",
250
+ margin: "0 10px"
39
251
  },
40
252
  btn: {
253
+ display: "inline-flex",
254
+ alignItems: "center",
255
+ justifyContent: "center",
41
256
  padding: "4px 10px",
42
257
  borderRadius: "6px",
43
- border: "1px solid rgba(128,128,128,0.4)",
44
- background: "rgba(128,128,128,0.12)",
258
+ border: "1px solid rgba(128,128,128,0.3)",
259
+ background: "rgba(128,128,128,0.1)",
45
260
  color: "inherit",
46
261
  cursor: "pointer",
47
- fontSize: "12px"
262
+ fontSize: "12px",
263
+ fontWeight: 500
48
264
  },
49
265
  btnPrimary: {
50
- padding: "6px 12px",
266
+ display: "inline-flex",
267
+ alignItems: "center",
268
+ justifyContent: "center",
269
+ padding: "5px 12px",
51
270
  borderRadius: "6px",
52
- border: "1px solid rgba(64,140,255,0.6)",
53
- background: "rgba(64,140,255,0.25)",
271
+ border: "1px solid rgba(59,130,246,0.5)",
272
+ background: "rgba(59,130,246,0.22)",
273
+ color: "#3b82f6",
274
+ cursor: "pointer",
275
+ fontSize: "12px",
276
+ fontWeight: 600
277
+ },
278
+ btnDanger: {
279
+ display: "inline-flex",
280
+ alignItems: "center",
281
+ justifyContent: "center",
282
+ padding: "3px 8px",
283
+ borderRadius: "5px",
284
+ border: "1px solid rgba(239,68,68,0.35)",
285
+ background: "rgba(239,68,68,0.1)",
286
+ color: "#ef4444",
287
+ cursor: "pointer",
288
+ fontSize: "11px",
289
+ fontWeight: 500
290
+ },
291
+ btnSm: {
292
+ display: "inline-flex",
293
+ alignItems: "center",
294
+ justifyContent: "center",
295
+ padding: "3px 8px",
296
+ borderRadius: "5px",
297
+ border: "1px solid rgba(128,128,128,0.25)",
298
+ background: "rgba(128,128,128,0.08)",
54
299
  color: "inherit",
55
300
  cursor: "pointer",
56
- fontSize: "13px"
301
+ fontSize: "11px",
302
+ fontWeight: 500
303
+ },
304
+ btnSmPrimary: {
305
+ display: "inline-flex",
306
+ alignItems: "center",
307
+ justifyContent: "center",
308
+ padding: "3px 8px",
309
+ borderRadius: "5px",
310
+ border: "1px solid rgba(59,130,246,0.35)",
311
+ background: "rgba(59,130,246,0.15)",
312
+ color: "#3b82f6",
313
+ cursor: "pointer",
314
+ fontSize: "11px",
315
+ fontWeight: 600
316
+ },
317
+ segGroup: {
318
+ display: "inline-flex",
319
+ background: "rgba(128,128,128,0.12)",
320
+ borderRadius: "6px",
321
+ padding: "2px",
322
+ border: "1px solid rgba(128,128,128,0.18)"
323
+ },
324
+ segBtn: {
325
+ padding: "3px 9px",
326
+ borderRadius: "4px",
327
+ border: "none",
328
+ background: "transparent",
329
+ color: "inherit",
330
+ cursor: "pointer",
331
+ fontSize: "11px",
332
+ fontWeight: 500,
333
+ opacity: .75
334
+ },
335
+ segBtnActive: {
336
+ padding: "3px 9px",
337
+ borderRadius: "4px",
338
+ border: "none",
339
+ background: "rgba(59,130,246,0.28)",
340
+ color: "#3b82f6",
341
+ cursor: "pointer",
342
+ fontSize: "11px",
343
+ fontWeight: 600,
344
+ opacity: 1
57
345
  },
58
346
  input: {
59
347
  flex: "1",
60
- padding: "6px 8px",
348
+ padding: "6px 10px",
61
349
  borderRadius: "6px",
62
- border: "1px solid rgba(128,128,128,0.4)",
63
- background: "transparent",
350
+ border: "1px solid rgba(128,128,128,0.3)",
351
+ background: "rgba(0,0,0,0.1)",
64
352
  color: "inherit",
65
- fontSize: "12px"
353
+ fontSize: "12px",
354
+ outline: "none"
66
355
  },
67
356
  muted: {
68
357
  color: "#9aa0a6",
358
+ fontSize: "11px"
359
+ },
360
+ noticeBanner: {
361
+ display: "flex",
362
+ alignItems: "center",
363
+ justifyContent: "space-between",
364
+ padding: "8px 12px",
365
+ borderRadius: "6px",
366
+ marginBottom: "10px",
69
367
  fontSize: "12px"
368
+ },
369
+ authModal: {
370
+ background: "rgba(59,130,246,0.06)",
371
+ border: "1px solid rgba(59,130,246,0.3)",
372
+ borderRadius: "8px",
373
+ padding: "12px 14px",
374
+ marginBottom: "12px"
70
375
  }
71
376
  };
72
377
  function apply(ctx) {
73
- const AgySettingsSection = () => {
74
- const [status, setStatus] = useState(null);
75
- const [code, setCode] = useState("");
76
- const [busy, setBusy] = useState(false);
77
- const [notice, setNotice] = useState("");
378
+ const AgySettingsSection = (props) => {
379
+ const [status, setStatus] = useState(statusCache);
380
+ const [aliasInput, setAliasInput] = useState("");
381
+ const [proxyInputs, setProxyInputs] = useState({});
382
+ const [editingProxyId, setEditingProxyId] = useState(null);
383
+ const [addingAccount, setAddingAccount] = useState(false);
384
+ const [authCodeInput, setAuthCodeInput] = useState("");
385
+ const [loadingAction, setLoadingAction] = useState(null);
386
+ const [toast, setToast] = useState(null);
387
+ const [expandedModels, setExpandedModels] = useState({});
78
388
  useEffect(() => {
79
389
  let alive = true;
390
+ let lastJson = "";
80
391
  const tick = async () => {
81
392
  const st = await getStatus();
82
- if (alive) setStatus(st);
393
+ if (!alive || !st) return;
394
+ const json = JSON.stringify(st);
395
+ if (json !== lastJson) {
396
+ lastJson = json;
397
+ setStatus(st);
398
+ }
83
399
  };
84
400
  tick();
85
401
  const timer = setInterval(tick, 3e3);
@@ -88,138 +404,732 @@ window.__ModuleLoader__.load({
88
404
  clearInterval(timer);
89
405
  };
90
406
  }, []);
407
+ const flowStartedRef = useRef(false);
408
+ useEffect(() => {
409
+ if (!addingAccount || !flowStartedRef.current) return;
410
+ const pa = status?.poolAuth;
411
+ if (!pa) return;
412
+ if (pa.phase === "done") {
413
+ flowStartedRef.current = false;
414
+ setAddingAccount(false);
415
+ setAuthCodeInput("");
416
+ setAliasInput("");
417
+ showToast(`✅ ${pa.message || "账号已激活入池"}`, "success");
418
+ } else if (pa.phase === "failed") {
419
+ flowStartedRef.current = false;
420
+ showToast(`❌ ${pa.message || "授权失败"}`, "error");
421
+ }
422
+ }, [
423
+ addingAccount,
424
+ status?.poolAuth?.phase,
425
+ status?.poolAuth?.message
426
+ ]);
427
+ const showToast = (text, type = "info") => {
428
+ setToast({
429
+ text,
430
+ type,
431
+ id: Date.now()
432
+ });
433
+ setTimeout(() => {
434
+ setToast((prev) => prev?.text === text ? null : prev);
435
+ }, 6e3);
436
+ };
91
437
  const refresh = async () => {
92
438
  setStatus(await getStatus());
93
439
  };
94
- const startAuth = async () => {
95
- setBusy(true);
96
- setNotice("");
97
- await postJson("/plugins/agy-link/auth", {});
98
- await refresh();
99
- setBusy(false);
440
+ const handleBeginAddAccount = async () => {
441
+ setLoadingAction("pool:beginAdd");
442
+ const res = await postJson("/plugins/agy-link/pool/begin-add", { alias: aliasInput.trim() || `备用 Google 账号 ${(status?.pool?.accounts?.length ?? 1) + 1}` });
443
+ setLoadingAction(null);
444
+ if (res && res.ok) {
445
+ flowStartedRef.current = true;
446
+ await refresh();
447
+ if (res.browserOpened) showToast("🌐 浏览器已打开 Google 授权页,完成授权后这里会自动完成", "info");
448
+ else showToast("⚠️ 无法自动打开浏览器,请点击下方链接手动完成授权", "warn");
449
+ } else showToast(`启动 Google 授权失败: ${res?.message || "请检查网络或代理配置"}`, "error");
100
450
  };
101
- const submitCode = async () => {
102
- if (code.trim() === "") return;
103
- setBusy(true);
104
- await postJson("/plugins/agy-link/auth-code", { code: code.trim() });
105
- setCode("");
106
- const st = await getStatus();
107
- setStatus(st);
108
- setNotice(st?.auth?.message ?? "");
109
- setBusy(false);
451
+ const handleCompleteAddAccount = async () => {
452
+ if (!authCodeInput.trim()) return;
453
+ setLoadingAction("pool:completeAdd");
454
+ const res = await postJson("/plugins/agy-link/pool/complete-add", { code: authCodeInput.trim() });
455
+ setLoadingAction(null);
456
+ if (res && res.ok) {
457
+ flowStartedRef.current = false;
458
+ setAuthCodeInput("");
459
+ setAliasInput("");
460
+ setAddingAccount(false);
461
+ await refresh();
462
+ showToast(`✅ ${res.message || "成功添加并激活 Google 账号"}`, "success");
463
+ } else showToast(`❌ ${res?.message || res?.error || "授权码验证失败"}`, "error");
464
+ };
465
+ const handleCancelAddAccount = async () => {
466
+ flowStartedRef.current = false;
467
+ await postJson("/plugins/agy-link/pool/cancel-add", {});
468
+ setAuthCodeInput("");
469
+ setAliasInput("");
470
+ setAddingAccount(false);
471
+ await refresh();
110
472
  };
111
473
  const setCfg = async (key, value) => {
112
- setBusy(true);
474
+ setLoadingAction(`config:${key}`);
113
475
  await postJson("/plugins/agy-link/config", {
114
476
  key,
115
477
  value
116
478
  });
117
479
  await refresh();
118
- setBusy(false);
480
+ setLoadingAction(null);
481
+ };
482
+ const setPrimary = async (id) => {
483
+ setLoadingAction(`primary:${id}`);
484
+ await postJson("/plugins/agy-link/pool/primary", { id });
485
+ await refresh();
486
+ setLoadingAction(null);
487
+ showToast("⭐ 已设为主用账号", "success");
488
+ };
489
+ const removeAccount = async (id, alias) => {
490
+ setLoadingAction(`remove:${id}`);
491
+ await postJson("/plugins/agy-link/pool/remove", { id });
492
+ await refresh();
493
+ setLoadingAction(null);
494
+ showToast(`已移除账号: ${alias}`, "info");
495
+ };
496
+ const refreshQuota = async (id) => {
497
+ setLoadingAction(id ? `refresh:${id}` : "refresh:all");
498
+ await postJson("/plugins/agy-link/pool/refresh-quota", { id });
499
+ await refresh();
500
+ setLoadingAction(null);
501
+ showToast("✅ 额度已刷新", "success");
502
+ };
503
+ const saveProxy = async (id) => {
504
+ setLoadingAction(`proxy:${id}`);
505
+ const proxyUrl = proxyInputs[id];
506
+ await postJson("/plugins/agy-link/pool/proxy", {
507
+ id,
508
+ proxyUrl
509
+ });
510
+ setEditingProxyId(null);
511
+ await refresh();
512
+ setLoadingAction(null);
513
+ showToast("💾 代理已保存", "success");
119
514
  };
120
- const reachable = status !== null;
121
- const summary = !reachable ? "Status endpoint unreachable — the plugin host did not answer /plugins/agy-link/status. Restart the DSH web server and check its logs." : status.dormantReason ? "Not ready — " + status.dormantReason : status.auth?.phase === "ok" ? "Ready — agy is connected" : "Needs Google login — start the login flow below, or run /agy auth";
122
- const color = !reachable ? "#e5484d" : status.dormantReason ? "#f5a623" : status.auth?.phase === "ok" ? "#30a46c" : "#f5a623";
123
- const infoBlock = h("div", { style: {
515
+ const setMode = async (mode) => {
516
+ setLoadingAction(`mode:${mode}`);
517
+ await postJson("/plugins/agy-link/pool/mode", { mode });
518
+ await refresh();
519
+ setLoadingAction(null);
520
+ };
521
+ const clearCooldown = async (id) => {
522
+ setLoadingAction(id ? `clearCooldown:${id}` : "clearCooldown:all");
523
+ await postJson("/plugins/agy-link/pool/clear-cooldown", { id });
524
+ await refresh();
525
+ setLoadingAction(null);
526
+ showToast("⚡ 已清除冷却", "success");
527
+ };
528
+ const toggleExpand = (accId) => {
529
+ setExpandedModels((prev) => ({
530
+ ...prev,
531
+ [accId]: !prev[accId]
532
+ }));
533
+ };
534
+ const authPhase = status?.auth?.phase ?? "unknown";
535
+ const pool = status?.pool;
536
+ const accounts = pool?.accounts ?? [];
537
+ const isAuthed = authPhase === "ok" || accounts.length > 0;
538
+ const isBusy = loadingAction !== null;
539
+ const renderSpinner = () => h("span", { className: "agy-spinner" });
540
+ const renderToastBanner = () => {
541
+ if (!toast) return null;
542
+ const typeStyles = {
543
+ success: {
544
+ background: "rgba(16,185,129,0.15)",
545
+ border: "1px solid rgba(16,185,129,0.4)",
546
+ color: "#10b981"
547
+ },
548
+ info: {
549
+ background: "rgba(59,130,246,0.15)",
550
+ border: "1px solid rgba(59,130,246,0.4)",
551
+ color: "#3b82f6"
552
+ },
553
+ warn: {
554
+ background: "rgba(245,158,11,0.15)",
555
+ border: "1px solid rgba(245,158,11,0.4)",
556
+ color: "#f59e0b"
557
+ },
558
+ error: {
559
+ background: "rgba(239,68,68,0.15)",
560
+ border: "1px solid rgba(239,68,68,0.4)",
561
+ color: "#ef4444"
562
+ }
563
+ };
564
+ return h("div", { style: {
565
+ ...S.noticeBanner,
566
+ ...typeStyles[toast.type]
567
+ } }, h("span", { style: { fontWeight: 500 } }, toast.text), h("button", {
568
+ type: "button",
569
+ style: {
570
+ background: "transparent",
571
+ border: "none",
572
+ color: "inherit",
573
+ cursor: "pointer",
574
+ fontSize: "14px",
575
+ padding: "0 4px",
576
+ opacity: .8
577
+ },
578
+ onClick: () => setToast(null)
579
+ }, "✕"));
580
+ };
581
+ const renderQuotaBar = (label, icon, familyKey, acc) => {
582
+ const info = acc.quotas[familyKey];
583
+ const cd = acc.cooldowns[familyKey];
584
+ const inCooldown = cd && cd.cooldownUntil > Date.now();
585
+ const hasData = typeof info?.remainingFraction === "number" && Number.isFinite(info.remainingFraction);
586
+ let percent = hasData ? 100 : -1;
587
+ let statusLabel = hasData ? "充足" : "未知";
588
+ const windowInfo = formatQuotaWindow(info?.resetTime, info?.remainingFraction);
589
+ if (inCooldown) {
590
+ percent = 0;
591
+ statusLabel = "限流冷却中";
592
+ } else if (hasData) {
593
+ percent = Math.max(0, Math.min(100, Math.round((info?.remainingFraction ?? 0) * 100)));
594
+ statusLabel = percent <= 20 ? "紧张" : percent <= 50 ? "适中" : "充足";
595
+ }
596
+ let barGradient = "linear-gradient(90deg, #10b981, #059669)";
597
+ let textColor = "#10b981";
598
+ let badgeBg = "rgba(16,185,129,0.15)";
599
+ if (percent < 0) {
600
+ barGradient = "rgba(128,128,128,0.25)";
601
+ textColor = "#9aa0a6";
602
+ badgeBg = "rgba(128,128,128,0.12)";
603
+ } else if (percent <= 20 || inCooldown || windowInfo.isWeeklyLockout) {
604
+ barGradient = "linear-gradient(90deg, #ef4444, #dc2626)";
605
+ textColor = "#ef4444";
606
+ badgeBg = "rgba(239,68,68,0.15)";
607
+ } else if (percent <= 50) {
608
+ barGradient = "linear-gradient(90deg, #f59e0b, #d97706)";
609
+ textColor = "#f59e0b";
610
+ badgeBg = "rgba(245,158,11,0.15)";
611
+ }
612
+ return h("div", { style: {
613
+ display: "flex",
614
+ alignItems: "center",
615
+ fontSize: "11px",
616
+ margin: "6px 0",
617
+ flexWrap: "wrap",
618
+ gap: "4px 0"
619
+ } }, h("div", { style: {
620
+ width: "130px",
621
+ display: "inline-flex",
622
+ alignItems: "center",
623
+ gap: "4px",
624
+ fontWeight: 500
625
+ } }, h("span", null, icon), h("span", null, label), hasData ? h("span", { style: {
626
+ fontSize: "9.5px",
627
+ padding: "0 4px",
628
+ borderRadius: "3px",
629
+ background: windowInfo.isWeeklyLockout ? "rgba(239,68,68,0.2)" : "rgba(128,128,128,0.14)",
630
+ color: windowInfo.isWeeklyLockout ? "#ef4444" : "#9aa0a6",
631
+ border: "1px solid rgba(128,128,128,0.2)"
632
+ } }, windowInfo.windowType) : null), h("div", { style: S.progressBarBg }, h("div", {
633
+ className: "agy-progress-fill",
634
+ style: {
635
+ width: `${percent < 0 ? 100 : percent}%`,
636
+ height: "100%",
637
+ background: barGradient,
638
+ borderRadius: "3px"
639
+ }
640
+ })), h("div", { style: {
641
+ display: "inline-flex",
642
+ alignItems: "center",
643
+ gap: "6px",
644
+ minWidth: "150px",
645
+ justifyContent: "flex-end"
646
+ } }, h("span", { style: {
647
+ padding: "1px 6px",
648
+ borderRadius: "4px",
649
+ background: badgeBg,
650
+ color: textColor,
651
+ fontWeight: 600,
652
+ fontSize: "11px"
653
+ } }, percent < 0 ? `— ${statusLabel}` : `${percent}% ${statusLabel}`), windowInfo.resetText ? h("span", { style: {
654
+ ...S.muted,
655
+ fontSize: "10px"
656
+ } }, `(${windowInfo.resetText})`) : null));
657
+ };
658
+ const renderedAccountCards = accounts.map((acc) => {
659
+ const isPrimary = acc.id === pool?.primaryAccountId;
660
+ const hasCooldown = Object.entries(acc.cooldowns).some(([, cd]) => cd && cd.cooldownUntil > Date.now());
661
+ const dotColor = !acc.enabled ? "#9aa0a6" : hasCooldown ? "#f59e0b" : "#10b981";
662
+ const isEditingProxy = editingProxyId === acc.id;
663
+ const isExpanded = expandedModels[acc.id] ?? false;
664
+ const cardStyle = isPrimary ? { ...S.cardPrimary } : { ...S.card };
665
+ const googleModels = acc.quotas.google?.models ?? [];
666
+ const anthropicModels = acc.quotas.anthropic?.models ?? [];
667
+ const openaiModels = acc.quotas.openai?.models ?? [];
668
+ const allChildModels = [
669
+ ...googleModels.map((m) => ({
670
+ family: "Google",
671
+ model: m
672
+ })),
673
+ ...anthropicModels.map((m) => ({
674
+ family: "Anthropic",
675
+ model: m
676
+ })),
677
+ ...openaiModels.map((m) => ({
678
+ family: "OpenAI",
679
+ model: m
680
+ }))
681
+ ];
682
+ return h("div", {
683
+ key: acc.id,
684
+ className: "agy-card-hover",
685
+ style: cardStyle
686
+ }, h("div", { style: {
687
+ display: "flex",
688
+ alignItems: "center",
689
+ justifyContent: "space-between",
690
+ marginBottom: "6px"
691
+ } }, h("div", { style: {
692
+ display: "flex",
693
+ alignItems: "center",
694
+ gap: "8px",
695
+ flexWrap: "wrap"
696
+ } }, h("span", {
697
+ className: "agy-pulse-dot",
698
+ style: {
699
+ background: dotColor,
700
+ boxShadow: `0 0 8px ${dotColor}88`
701
+ }
702
+ }), h("span", { style: {
703
+ fontWeight: 600,
704
+ fontSize: "13px"
705
+ } }, acc.alias), acc.email ? h("span", { style: {
706
+ ...S.badgeTag,
707
+ color: "#3b82f6",
708
+ borderColor: "rgba(59,130,246,0.3)"
709
+ } }, `✉️ ${acc.email}`) : null, isPrimary ? h("span", { style: S.badgePrimary }, "⭐ 主用") : null, acc.proxyUrl ? h("span", { style: {
710
+ ...S.badgeTag,
711
+ color: "#10b981",
712
+ borderColor: "rgba(16,185,129,0.3)"
713
+ } }, "🌐 代理") : null), h("div", { style: {
714
+ display: "flex",
715
+ gap: "5px"
716
+ } }, allChildModels.length > 0 ? h("button", {
717
+ type: "button",
718
+ className: "agy-btn",
719
+ style: isExpanded ? { ...S.btnSmPrimary } : S.btnSm,
720
+ onClick: () => toggleExpand(acc.id)
721
+ }, isExpanded ? "收起细分" : "🔍 细分") : null, !isPrimary ? h("button", {
722
+ type: "button",
723
+ className: "agy-btn",
724
+ style: S.btnSm,
725
+ disabled: isBusy,
726
+ onClick: () => void setPrimary(acc.id)
727
+ }, loadingAction === `primary:${acc.id}` ? [renderSpinner(), "设置中"] : "设为主用") : null, h("button", {
728
+ type: "button",
729
+ className: "agy-btn",
730
+ style: isEditingProxy ? { ...S.btnSmPrimary } : S.btnSm,
731
+ disabled: isBusy,
732
+ onClick: () => setEditingProxyId(isEditingProxy ? null : acc.id)
733
+ }, "代理"), accounts.length > 1 ? h("button", {
734
+ type: "button",
735
+ className: "agy-btn",
736
+ style: S.btnDanger,
737
+ disabled: isBusy,
738
+ onClick: () => void removeAccount(acc.id, acc.alias)
739
+ }, loadingAction === `remove:${acc.id}` ? [renderSpinner(), ""] : "🗑️") : null)), h("div", { style: S.quotaBox }, renderQuotaBar("Gemini (Flash/Pro)", "✨", "google", acc), renderQuotaBar("Claude (Sonnet/Opus)", "🧠", "anthropic", acc), renderQuotaBar("GPT-OSS (120B)", "⚡", "openai", acc), isExpanded && allChildModels.length > 0 ? h("div", { style: {
740
+ marginTop: "8px",
741
+ paddingTop: "8px",
742
+ borderTop: "1px solid rgba(128,128,128,0.15)"
743
+ } }, h("div", { style: {
744
+ ...S.muted,
745
+ marginBottom: "4px",
746
+ fontWeight: 600
747
+ } }, "📊 单模型明细额度 (5小时/周限额状态):"), allChildModels.map(({ family, model }) => {
748
+ const frac = model.remainingFraction ?? 1;
749
+ const pct = Math.round(frac * 100);
750
+ const w = formatQuotaWindow(model.resetTime, frac);
751
+ const pColor = pct <= 20 ? "#ef4444" : pct <= 50 ? "#f59e0b" : "#10b981";
752
+ return h("div", {
753
+ key: model.modelId,
754
+ className: "agy-submodel-row"
755
+ }, h("span", { style: { fontWeight: 500 } }, `[${family}] ${model.displayName || model.modelId}`), h("div", { style: {
756
+ display: "flex",
757
+ alignItems: "center",
758
+ gap: "6px"
759
+ } }, h("span", { style: {
760
+ color: pColor,
761
+ fontWeight: 600
762
+ } }, `${pct}%`), w.resetText ? h("span", { style: S.muted }, w.resetText) : null));
763
+ })) : null), hasCooldown ? h("div", { style: {
764
+ background: "rgba(245,158,11,0.1)",
765
+ border: "1px solid rgba(245,158,11,0.3)",
766
+ borderRadius: "6px",
767
+ padding: "4px 8px",
768
+ color: "#f59e0b",
769
+ fontSize: "11px",
770
+ marginTop: "6px",
771
+ display: "flex",
772
+ alignItems: "center",
773
+ justifyContent: "space-between"
774
+ } }, h("span", null, "⚠️ 该账号部分模型处于限流冷却中 (已自动切换下个可用账号)"), h("button", {
775
+ type: "button",
776
+ className: "agy-btn",
777
+ style: {
778
+ ...S.btnSm,
779
+ color: "#f59e0b",
780
+ borderColor: "rgba(245,158,11,0.4)",
781
+ background: "rgba(245,158,11,0.15)"
782
+ },
783
+ disabled: isBusy,
784
+ onClick: () => void clearCooldown(acc.id)
785
+ }, loadingAction === `clearCooldown:${acc.id}` ? [renderSpinner(), ""] : "⚡ 清除冷却")) : null, isEditingProxy ? h("div", { style: {
786
+ marginTop: "6px",
787
+ padding: "8px 10px",
788
+ background: "rgba(128,128,128,0.06)",
789
+ borderRadius: "6px",
790
+ border: "1px solid rgba(128,128,128,0.15)"
791
+ } }, h("div", { style: {
792
+ display: "flex",
793
+ gap: "6px"
794
+ } }, h("input", {
795
+ style: S.input,
796
+ value: proxyInputs[acc.id] !== void 0 ? proxyInputs[acc.id] : acc.proxyUrl ?? "",
797
+ placeholder: "专属代理 URL (如: http://127.0.0.1:7890,留空则使用全局)",
798
+ onChange: (e) => setProxyInputs({
799
+ ...proxyInputs,
800
+ [acc.id]: e.target.value
801
+ })
802
+ }), h("button", {
803
+ type: "button",
804
+ className: "agy-btn",
805
+ style: S.btnPrimary,
806
+ disabled: isBusy,
807
+ onClick: () => void saveProxy(acc.id)
808
+ }, loadingAction === `proxy:${acc.id}` ? [renderSpinner(), "保存"] : "保存"), h("button", {
809
+ type: "button",
810
+ className: "agy-btn",
811
+ style: S.btn,
812
+ onClick: () => setEditingProxyId(null)
813
+ }, "取消"))) : null);
814
+ });
815
+ const poolAuth = status?.poolAuth;
816
+ const flowPhase = poolAuth?.phase ?? "idle";
817
+ const flowActive = flowPhase === "waiting" || flowPhase === "exchanging";
818
+ const addAccountSection = addingAccount ? h("div", { style: S.authModal }, h("div", { style: {
819
+ fontWeight: 600,
820
+ fontSize: "13px",
821
+ marginBottom: "8px"
822
+ } }, "➕ 添加 Google 账号"), !flowActive ? h("div", null, h("div", { style: {
823
+ display: "flex",
824
+ gap: "8px"
825
+ } }, h("input", {
826
+ style: S.input,
827
+ value: aliasInput,
828
+ placeholder: "账号别名 (例如: 备用账号 2)",
829
+ onChange: (e) => setAliasInput(e.target.value)
830
+ }), h("button", {
831
+ type: "button",
832
+ className: "agy-btn",
833
+ style: S.btnPrimary,
834
+ disabled: isBusy,
835
+ onClick: () => void handleBeginAddAccount()
836
+ }, loadingAction === "pool:beginAdd" ? [renderSpinner(), "正在打开浏览器..."] : "🚀 打开浏览器登录"), h("button", {
837
+ type: "button",
838
+ className: "agy-btn",
839
+ style: S.btn,
840
+ onClick: () => handleCancelAddAccount()
841
+ }, "取消")), flowPhase === "failed" && poolAuth?.message ? h("div", { style: {
124
842
  ...S.muted,
125
- lineHeight: 1.6,
126
- margin: "6px 0 12px"
127
- } }, h("div", null, "agy binary: ", status?.bin ?? "not found", status?.version ? " — v" + status.version : ""), h("div", null, "auth: ", status?.auth?.phase ?? "unknown"), h("div", null, "workspace: ", status?.workspaceRoot ? status.workspaceRoot : "(session cwd / process cwd)"), h("div", null, "models: ", String(status?.catalog?.count ?? 0), " — ", status?.catalog?.source ?? ""), h("div", null, "bindings: ", String(status?.bindings ?? 0)), h("div", null, "last run: ", status?.lastRun ? (status.lastRun.ok ? "ok" : status.lastRun.code) + " — " + status.lastRun.model : "none"));
128
- const modeRow = h("div", { style: {
129
- ...S.row,
843
+ color: "#ef4444",
130
844
  marginTop: "8px"
131
- } }, h("span", { style: S.muted }, "mode:"), h("button", {
845
+ } }, `❌ ${poolAuth.message}`) : null) : h("div", null, h("div", { style: {
846
+ display: "flex",
847
+ alignItems: "center",
848
+ ...S.muted,
849
+ marginBottom: "8px",
850
+ lineHeight: 1.5
851
+ } }, renderSpinner(), flowPhase === "exchanging" ? "正在验证授权并激活账号,请稍候..." : "等待浏览器中完成 Google 授权,成功后这里会自动完成。"), poolAuth?.url ? h("div", { style: { marginBottom: "8px" } }, h("a", {
852
+ href: poolAuth.url,
853
+ target: "_blank",
854
+ style: {
855
+ color: "#3b82f6",
856
+ textDecoration: "none",
857
+ fontSize: "12px",
858
+ fontWeight: 500
859
+ }
860
+ }, "👉 若浏览器未打开,请点击此处手动打开 Google 登录页")) : null, h("div", { style: {
861
+ ...S.muted,
862
+ marginBottom: "6px",
863
+ fontSize: "10px"
864
+ } }, "自动回调失败时,可粘贴授权码或浏览器地址栏中的完整回调 URL:"), h("div", { style: {
865
+ display: "flex",
866
+ gap: "8px"
867
+ } }, h("input", {
868
+ style: S.input,
869
+ value: authCodeInput,
870
+ placeholder: "授权码 或 http://localhost:51121/oauth-callback?code=... 完整链接",
871
+ onChange: (e) => setAuthCodeInput(e.target.value)
872
+ }), h("button", {
873
+ type: "button",
874
+ className: "agy-btn",
875
+ style: S.btnPrimary,
876
+ disabled: isBusy || !authCodeInput.trim(),
877
+ onClick: () => void handleCompleteAddAccount()
878
+ }, loadingAction === "pool:completeAdd" ? [renderSpinner(), "验证激活中..."] : "✅ 手动激活"), h("button", {
879
+ type: "button",
880
+ className: "agy-btn",
881
+ style: S.btn,
882
+ onClick: () => handleCancelAddAccount()
883
+ }, "取消")))) : null;
884
+ if (status === null) return h("div", { style: {
885
+ ...S.container,
886
+ minHeight: "120px",
887
+ display: "flex",
888
+ alignItems: "center",
889
+ justifyContent: "center"
890
+ } }, h("style", null, GLOBAL_CSS), h("span", { style: S.muted }, [renderSpinner(), "正在加载 Antigravity 状态..."]));
891
+ return h("div", { style: S.container }, h("style", null, GLOBAL_CSS), h("div", { style: S.headerCard }, h("div", { style: {
892
+ display: "flex",
893
+ alignItems: "center",
894
+ gap: "8px"
895
+ } }, h("span", {
896
+ className: "agy-pulse-dot",
897
+ style: {
898
+ background: isAuthed ? "#10b981" : "#f59e0b",
899
+ boxShadow: `0 0 8px ${isAuthed ? "#10b981" : "#f59e0b"}`
900
+ }
901
+ }), h("span", { style: {
902
+ fontWeight: 700,
903
+ fontSize: "14px"
904
+ } }, "Antigravity (agy CLI)"), h("span", { style: isAuthed ? {
905
+ ...S.badgePrimary,
906
+ color: "#10b981",
907
+ background: "rgba(16,185,129,0.12)",
908
+ borderColor: "rgba(16,185,129,0.3)"
909
+ } : S.badgePrimary }, isAuthed ? "就绪" : "待认证")), h("div", { style: {
910
+ display: "flex",
911
+ gap: "6px"
912
+ } }, !addingAccount ? h("button", {
913
+ type: "button",
914
+ className: "agy-btn",
915
+ style: S.btnPrimary,
916
+ onClick: () => setAddingAccount(true)
917
+ }, "➕ 添加账号") : null, h("button", {
132
918
  type: "button",
133
- style: status?.permissionMode === "plan" ? S.btnPrimary : S.btn,
919
+ className: "agy-btn",
920
+ style: S.btn,
921
+ disabled: isBusy,
922
+ onClick: () => void refreshQuota()
923
+ }, loadingAction === "refresh:all" ? [renderSpinner(), "刷新中"] : "🔄 刷新额度"))), renderToastBanner(), addAccountSection, renderedAccountCards, h("div", { style: {
924
+ background: "rgba(59,130,246,0.05)",
925
+ border: "1px solid rgba(59,130,246,0.18)",
926
+ borderRadius: "6px",
927
+ padding: "8px 10px",
928
+ marginBottom: "10px",
929
+ fontSize: "11px",
930
+ lineHeight: "1.6"
931
+ } }, h("div", { style: {
932
+ fontWeight: 600,
933
+ color: "#3b82f6",
934
+ marginBottom: "2px",
935
+ display: "flex",
936
+ alignItems: "center",
937
+ gap: "4px"
938
+ } }, "💡 Antigravity 额度机制说明 (5小时额度 & 周额度)"), h("div", { style: { opacity: .85 } }, h("div", null, "• 🕒 5小时短期额度:各模型族按 5 小时滚动窗口刷新配额,日常对话优先消耗此额度。"), h("div", null, "• 📅 周额度 / 总量上限:Google 账号设有周期总量保护;若高频重度使用触及周上限,重置时间将显示为数天后。"), h("div", null, "• 🔄 多账号号池调度:单账号 5 小时额度或周额度受限时,号池自动平滑接力至下一可用账号,无缝保障持续编码。"))), h("div", { style: {
939
+ marginTop: "14px",
940
+ paddingTop: "10px",
941
+ borderTop: "1px solid rgba(128,128,128,0.15)"
942
+ } }, h("div", { style: {
943
+ display: "flex",
944
+ flexDirection: "column",
945
+ gap: "8px"
946
+ } }, h("div", { style: {
947
+ display: "flex",
948
+ alignItems: "center",
949
+ justifyContent: "space-between"
950
+ } }, h("span", { style: S.muted }, "权限模式:"), h("div", { style: S.segGroup }, h("button", {
951
+ type: "button",
952
+ style: status?.permissionMode === "plan" ? S.segBtnActive : S.segBtn,
134
953
  onClick: () => void setCfg("permissionMode", "plan")
135
- }, "plan"), h("button", {
954
+ }, "plan (只读)"), h("button", {
136
955
  type: "button",
137
- style: status?.permissionMode === "accept-edits" ? S.btnPrimary : S.btn,
956
+ style: status?.permissionMode === "accept-edits" ? S.segBtnActive : S.segBtn,
138
957
  onClick: () => void setCfg("permissionMode", "accept-edits")
139
- }, "accept-edits"), h("button", {
958
+ }, "accept-edits (改代码)"), h("button", {
140
959
  type: "button",
141
960
  style: status?.permissionMode === "skip" ? {
142
- ...S.btn,
143
- color: "#e5484d",
144
- borderColor: "rgba(229,72,77,0.6)"
145
- } : S.btn,
961
+ ...S.segBtnActive,
962
+ color: "#ef4444",
963
+ background: "rgba(239,68,68,0.2)"
964
+ } : S.segBtn,
146
965
  onClick: () => void setCfg("permissionMode", "skip")
147
- }, "skip"));
148
- const effortRow = h("div", { style: S.row }, h("span", { style: S.muted }, "effort:"), h("button", {
966
+ }, "skip (全自动免确认)"))), h("div", { style: {
967
+ display: "flex",
968
+ alignItems: "center",
969
+ justifyContent: "space-between"
970
+ } }, h("span", { style: S.muted }, "思考强度:"), h("div", { style: S.segGroup }, h("button", {
149
971
  type: "button",
150
- style: status?.defaultEffort === "low" ? S.btnPrimary : S.btn,
972
+ style: status?.defaultEffort === "" ? S.segBtnActive : S.segBtn,
973
+ onClick: () => void setCfg("defaultEffort", "")
974
+ }, "auto"), h("button", {
975
+ type: "button",
976
+ style: status?.defaultEffort === "low" ? S.segBtnActive : S.segBtn,
151
977
  onClick: () => void setCfg("defaultEffort", "low")
152
978
  }, "low"), h("button", {
153
979
  type: "button",
154
- style: status?.defaultEffort === "medium" ? S.btnPrimary : S.btn,
980
+ style: status?.defaultEffort === "medium" ? S.segBtnActive : S.segBtn,
155
981
  onClick: () => void setCfg("defaultEffort", "medium")
156
982
  }, "medium"), h("button", {
157
983
  type: "button",
158
- style: status?.defaultEffort === "high" ? S.btnPrimary : S.btn,
984
+ style: status?.defaultEffort === "high" ? S.segBtnActive : S.segBtn,
159
985
  onClick: () => void setCfg("defaultEffort", "high")
160
- }, "high"), h("button", {
161
- type: "button",
162
- style: status?.defaultEffort === "" ? S.btnPrimary : S.btn,
163
- onClick: () => void setCfg("defaultEffort", "")
164
- }, "auto"));
165
- const skipWarn = h("div", { style: {
166
- ...S.muted,
167
- marginBottom: "8px"
168
- } }, "skip runs agy tools with --dangerously-skip-permissions (no approval prompts). plan is the safe read-only default.");
169
- const authBlock = h("div", null, h("div", { style: {
170
- fontWeight: 600,
171
- marginBottom: "4px"
172
- } }, "Google login required"), h("div", null, "agy is not signed in. Start the login flow, scan the QR or open the URL, then paste the authorization code below."), h("img", {
173
- src: base + "/plugins/agy-link/qr",
174
- alt: "auth QR",
175
- width: 200,
176
- height: 200,
177
- style: {
178
- display: "block",
179
- margin: "8px auto"
180
- }
181
- }), h("div", { style: { wordBreak: "break-all" } }, status?.auth?.url ?? ""), h("div", { style: S.row }, h("input", {
182
- style: S.input,
183
- value: code,
184
- placeholder: "authorization code",
185
- onChange: (e) => setCode(e.target.value)
186
- }), h("button", {
986
+ }, "high"))), h("div", { style: {
987
+ display: "flex",
988
+ alignItems: "center",
989
+ justifyContent: "space-between"
990
+ } }, h("span", { style: S.muted }, "号池调度:"), h("div", { style: S.segGroup }, h("button", {
187
991
  type: "button",
188
- style: S.btnPrimary,
189
- disabled: busy,
190
- onClick: () => void submitCode()
191
- }, "Submit code"), h("button", {
992
+ style: pool?.mode === "sequential" || !pool?.mode ? S.segBtnActive : S.segBtn,
993
+ onClick: () => void setMode("sequential")
994
+ }, "顺次耗尽"), h("button", {
192
995
  type: "button",
193
- style: S.btn,
194
- disabled: busy,
195
- onClick: () => void startAuth()
196
- }, "Restart login")), notice !== "" ? h("div", null, notice) : null);
197
- const needsAuth = reachable && !status.dormantReason && status.auth !== void 0 && status.auth.phase !== "ok";
198
- const controls = !reachable ? null : status.dormantReason ? h("div", { style: S.muted }, "Bridge dormant: ", status.dormantReason) : needsAuth ? authBlock : h("div", null, infoBlock, modeRow, effortRow, skipWarn);
199
- return h("div", { style: { lineHeight: 1.6 } }, h("div", { style: {
996
+ style: pool?.mode === "round-robin" ? S.segBtnActive : S.segBtn,
997
+ onClick: () => void setMode("round-robin")
998
+ }, "轮询均衡"))))));
999
+ };
1000
+ /** Modal dialog container rendered into document.body */
1001
+ const AgyModalDialog = () => {
1002
+ const [isOpen, setIsOpen] = useState(agyModalStore.open);
1003
+ useEffect(() => {
1004
+ return agyModalStore.subscribe(() => {
1005
+ setIsOpen(agyModalStore.open);
1006
+ });
1007
+ }, []);
1008
+ useEffect(() => {
1009
+ if (!isOpen) return;
1010
+ const onKey = (e) => {
1011
+ if (e.key === "Escape") {
1012
+ e.stopPropagation();
1013
+ agyModalStore.setOpen(false);
1014
+ }
1015
+ };
1016
+ win.addEventListener?.("keydown", onKey, true);
1017
+ return () => win.removeEventListener?.("keydown", onKey, true);
1018
+ }, [isOpen]);
1019
+ if (!isOpen) return null;
1020
+ return h("div", {
1021
+ className: "agy-modal-backdrop",
1022
+ onClick: (e) => {
1023
+ if (e.target === e.currentTarget) agyModalStore.setOpen(false);
1024
+ }
1025
+ }, h("div", {
1026
+ className: "agy-modal-panel",
1027
+ role: "dialog",
1028
+ "aria-modal": true
1029
+ }, h("div", { style: {
200
1030
  display: "flex",
201
1031
  alignItems: "center",
202
- gap: "8px",
203
- fontWeight: 600,
204
- fontSize: "14px"
205
- } }, h("span", { style: {
1032
+ justifyContent: "space-between",
1033
+ padding: "12px 16px",
1034
+ borderBottom: "1px solid rgba(128,128,128,0.2)",
1035
+ background: "rgba(128,128,128,0.05)"
1036
+ } }, h("div", { style: {
1037
+ display: "flex",
1038
+ alignItems: "center",
1039
+ gap: "8px"
1040
+ } }, h("span", { style: { fontSize: "16px" } }, "🪐"), h("strong", { style: { fontSize: "14px" } }, "Antigravity 管理控制台")), h("button", {
1041
+ type: "button",
1042
+ title: "关闭 (Esc)",
1043
+ style: {
1044
+ background: "transparent",
1045
+ border: "none",
1046
+ color: "#9aa0a6",
1047
+ cursor: "pointer",
1048
+ fontSize: "16px",
1049
+ lineHeight: 1,
1050
+ padding: "4px 6px",
1051
+ borderRadius: "4px"
1052
+ },
1053
+ onClick: () => agyModalStore.setOpen(false)
1054
+ }, "✕")), h("div", { style: {
1055
+ overflowY: "auto",
1056
+ padding: "16px",
1057
+ flex: "1"
1058
+ } }, h(AgySettingsSection, {
1059
+ isModal: true,
1060
+ onClose: () => agyModalStore.setOpen(false)
1061
+ }))));
1062
+ };
1063
+ /** Sidebar bottom-left action button + body portal modal */
1064
+ const AgySidebarFooterAction = (props) => {
1065
+ const [status, setStatus] = useState(statusCache);
1066
+ const isWide = props?.wide !== false;
1067
+ useEffect(() => {
1068
+ let alive = true;
1069
+ const tick = async () => {
1070
+ const st = await getStatus();
1071
+ if (!alive || !st) return;
1072
+ setStatus(st);
1073
+ };
1074
+ tick();
1075
+ const timer = setInterval(tick, 3e3);
1076
+ return () => {
1077
+ alive = false;
1078
+ clearInterval(timer);
1079
+ };
1080
+ }, []);
1081
+ const accounts = (status?.pool)?.accounts ?? [];
1082
+ const hasCooldown = accounts.some((a) => Object.entries(a.cooldowns).some(([, cd]) => cd && cd.cooldownUntil > Date.now()));
1083
+ const isAuthed = status?.auth?.phase === "ok" || accounts.length > 0;
1084
+ const dotColor = status === null ? "#9aa0a6" : status.dormantReason ? "#f59e0b" : hasCooldown ? "#f59e0b" : isAuthed ? "#10b981" : "#f59e0b";
1085
+ const button = h("button", {
1086
+ type: "button",
1087
+ title: `Antigravity: ${accounts.length} accounts (${isAuthed ? "就绪" : "待认证"}) · 点击打开管理控制台`,
1088
+ className: "agy-btn",
1089
+ style: {
1090
+ display: "inline-flex",
1091
+ alignItems: "center",
1092
+ gap: "6px",
1093
+ padding: isWide ? "5px 10px" : "6px",
1094
+ border: "1px solid rgba(128,128,128,0.22)",
1095
+ borderRadius: "8px",
1096
+ background: "rgba(128,128,128,0.08)",
1097
+ color: "inherit",
1098
+ cursor: "pointer",
1099
+ fontSize: "12px",
1100
+ lineHeight: 1.4
1101
+ },
1102
+ onClick: () => agyModalStore.setOpen(true)
1103
+ }, h("span", { style: { fontSize: "13px" } }, "🪐"), isWide ? h("span", { style: { fontWeight: 500 } }, "Antigravity") : null, h("span", { style: {
206
1104
  display: "inline-block",
207
- width: "10px",
208
- height: "10px",
1105
+ width: "7px",
1106
+ height: "7px",
209
1107
  borderRadius: "50%",
210
- background: color
211
- } }), "Antigravity (agy CLI)"), h("div", { style: {
212
- ...S.muted,
213
- margin: "6px 0 12px"
214
- } }, summary), controls, h("div", { style: S.muted }, "Run `/agy status` for full diagnostics, `/agy doctor` to export a report."));
1108
+ background: dotColor,
1109
+ boxShadow: `0 0 6px ${dotColor}`
1110
+ } }), isWide && accounts.length > 0 ? h("span", { style: {
1111
+ fontSize: "10px",
1112
+ opacity: .75,
1113
+ background: "rgba(128,128,128,0.18)",
1114
+ padding: "1px 5px",
1115
+ borderRadius: "10px"
1116
+ } }, `${accounts.length}`) : null);
1117
+ return h("div", { style: { display: "inline-block" } }, button, portalToBody(h(AgyModalDialog, null)));
215
1118
  };
1119
+ /** Session Header badge in chat toolbar */
216
1120
  const AgySessionStatus = () => {
217
- const [status, setStatus] = useState(null);
1121
+ const [status, setStatus] = useState(statusCache);
218
1122
  useEffect(() => {
219
1123
  let alive = true;
1124
+ let lastJson = "";
220
1125
  const tick = async () => {
221
1126
  const st = await getStatus();
222
- if (alive) setStatus(st);
1127
+ if (!alive || !st) return;
1128
+ const json = JSON.stringify(st);
1129
+ if (json !== lastJson) {
1130
+ lastJson = json;
1131
+ setStatus(st);
1132
+ }
223
1133
  };
224
1134
  tick();
225
1135
  const timer = setInterval(tick, 3e3);
@@ -228,16 +1138,20 @@ window.__ModuleLoader__.load({
228
1138
  clearInterval(timer);
229
1139
  };
230
1140
  }, []);
231
- const color = status === null ? "#9aa0a6" : status.dormantReason ? "#f5a623" : status.auth?.phase === "ok" ? "#30a46c" : "#f5a623";
232
- const title = status === null ? "Antigravity (agy) status…" : status.dormantReason ? "Antigravity not ready: " + status.dormantReason : status.auth?.phase === "ok" ? "Antigravity ready — agy is connected" : "Antigravity needs Google login";
1141
+ const accounts = (status?.pool)?.accounts ?? [];
1142
+ const hasCooldown = accounts.some((a) => Object.entries(a.cooldowns).some(([, cd]) => cd && cd.cooldownUntil > Date.now()));
1143
+ const isAuthed = status?.auth?.phase === "ok" || accounts.length > 0;
1144
+ const color = status === null ? "#9aa0a6" : status.dormantReason ? "#f59e0b" : hasCooldown ? "#f59e0b" : isAuthed ? "#10b981" : "#f59e0b";
233
1145
  return h("button", {
234
1146
  type: "button",
235
- title,
1147
+ title: `Antigravity: ${accounts.length} accounts ready · 点击打开控制台`,
1148
+ className: "agy-btn",
1149
+ onClick: () => agyModalStore.setOpen(true),
236
1150
  style: {
237
1151
  background: "transparent",
238
- border: "1px solid rgba(128,128,128,0.3)",
1152
+ border: "1px solid rgba(128,128,128,0.25)",
239
1153
  borderRadius: "999px",
240
- cursor: "default",
1154
+ cursor: "pointer",
241
1155
  padding: "2px 8px",
242
1156
  fontSize: "11px",
243
1157
  lineHeight: 1.5,
@@ -248,11 +1162,11 @@ window.__ModuleLoader__.load({
248
1162
  }
249
1163
  }, h("span", { style: {
250
1164
  display: "inline-block",
251
- width: "8px",
252
- height: "8px",
1165
+ width: "7px",
1166
+ height: "7px",
253
1167
  borderRadius: "50%",
254
1168
  background: color
255
- } }), "AGY");
1169
+ } }), `AGY (${accounts.length})`);
256
1170
  };
257
1171
  ctx.slots.inject("conversation.session.header.actions", () => {
258
1172
  return ctx.slots.register({
@@ -270,6 +1184,14 @@ window.__ModuleLoader__.load({
270
1184
  label: "Antigravity"
271
1185
  }, AgySettingsSection);
272
1186
  });
1187
+ ctx.slots.inject("sidebar.footer.action", () => {
1188
+ return ctx.slots.register({
1189
+ name: "sidebar.footer.action",
1190
+ id: "agy-link-sidebar-footer",
1191
+ order: 50,
1192
+ label: "Antigravity"
1193
+ }, AgySidebarFooterAction);
1194
+ });
273
1195
  }
274
1196
  //#endregion
275
1197
  exports.apply = apply;