dsh-bundle-antigravity-host 0.1.30 → 0.1.35

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.
Files changed (82) hide show
  1. package/README.md +13 -7
  2. package/cordis.patch.yml +7 -2
  3. package/dist/adapter/antigravity-adapter.js +23 -5
  4. package/dist/adapter/antigravity-adapter.js.map +1 -1
  5. package/dist/bridge/mcp-bridge.d.ts +9 -0
  6. package/dist/bridge/mcp-bridge.js +19 -2
  7. package/dist/bridge/mcp-bridge.js.map +1 -1
  8. package/dist/client.js +435 -143
  9. package/dist/index.js +126 -10
  10. package/dist/index.js.map +1 -1
  11. package/dist/pool/pooled-adapter.js +6 -5
  12. package/dist/pool/pooled-adapter.js.map +1 -1
  13. package/dist/protocol/agent-link.d.ts +13 -2
  14. package/dist/protocol/agent-link.js +59 -5
  15. package/dist/protocol/agent-link.js.map +1 -1
  16. package/dist/proxy/account-remote.d.ts +34 -0
  17. package/dist/proxy/account-remote.js +92 -0
  18. package/dist/proxy/account-remote.js.map +1 -0
  19. package/dist/proxy/account-service.d.ts +78 -0
  20. package/dist/proxy/account-service.js +359 -0
  21. package/dist/proxy/account-service.js.map +1 -0
  22. package/dist/proxy/account-store.d.ts +56 -0
  23. package/dist/proxy/account-store.js +182 -0
  24. package/dist/proxy/account-store.js.map +1 -0
  25. package/dist/proxy/adapter.d.ts +72 -0
  26. package/dist/proxy/adapter.js +812 -0
  27. package/dist/proxy/adapter.js.map +1 -0
  28. package/dist/proxy/image.d.ts +27 -0
  29. package/dist/proxy/image.js +82 -0
  30. package/dist/proxy/image.js.map +1 -0
  31. package/dist/proxy/index.d.ts +19 -0
  32. package/dist/proxy/index.js +118 -0
  33. package/dist/proxy/index.js.map +1 -0
  34. package/dist/proxy/mode-bus.d.ts +50 -0
  35. package/dist/proxy/mode-bus.js +38 -0
  36. package/dist/proxy/mode-bus.js.map +1 -0
  37. package/dist/proxy/oauth.d.ts +87 -0
  38. package/dist/proxy/oauth.js +430 -0
  39. package/dist/proxy/oauth.js.map +1 -0
  40. package/dist/proxy/payload.d.ts +67 -0
  41. package/dist/proxy/payload.js +540 -0
  42. package/dist/proxy/payload.js.map +1 -0
  43. package/dist/proxy/quota-source.d.ts +15 -0
  44. package/dist/proxy/quota-source.js +114 -0
  45. package/dist/proxy/quota-source.js.map +1 -0
  46. package/dist/proxy/replay.d.ts +34 -0
  47. package/dist/proxy/replay.js +238 -0
  48. package/dist/proxy/replay.js.map +1 -0
  49. package/dist/proxy/transport.d.ts +55 -0
  50. package/dist/proxy/transport.js +723 -0
  51. package/dist/proxy/transport.js.map +1 -0
  52. package/dist/proxy/wire-identity.d.ts +25 -0
  53. package/dist/proxy/wire-identity.js +124 -0
  54. package/dist/proxy/wire-identity.js.map +1 -0
  55. package/dist/quota/control.d.ts +10 -0
  56. package/dist/quota/control.js +17 -2
  57. package/dist/quota/control.js.map +1 -1
  58. package/dist/quota/quota-remote.d.ts +18 -0
  59. package/dist/quota/quota-remote.js +50 -9
  60. package/dist/quota/quota-remote.js.map +1 -1
  61. package/dist/runtime/binary.d.ts +20 -1
  62. package/dist/runtime/binary.js +58 -6
  63. package/dist/runtime/binary.js.map +1 -1
  64. package/dist/runtime/proxied-fetch.js +9 -1
  65. package/dist/runtime/proxied-fetch.js.map +1 -1
  66. package/dist/runtime/proxy-env.d.ts +2 -0
  67. package/dist/runtime/proxy-env.js +8 -0
  68. package/dist/runtime/proxy-env.js.map +1 -1
  69. package/dist/runtime/quota.d.ts +19 -1
  70. package/dist/runtime/quota.js +97 -22
  71. package/dist/runtime/quota.js.map +1 -1
  72. package/dist/runtime/session-map.d.ts +7 -0
  73. package/dist/runtime/session-map.js +11 -0
  74. package/dist/runtime/session-map.js.map +1 -1
  75. package/dist/settings.d.ts +11 -1
  76. package/dist/settings.js +3 -1
  77. package/dist/settings.js.map +1 -1
  78. package/docs/changelog.md +61 -1
  79. package/docs/configuration.md +24 -6
  80. package/docs/development.md +21 -9
  81. package/docs/reference.md +34 -7
  82. package/package.json +4 -2
package/dist/client.js CHANGED
@@ -23,19 +23,31 @@ window.__ModuleLoader__.load({
23
23
  const React = require('react')
24
24
  const h = React.createElement
25
25
  const PROVIDER = 'antigravity-acp'
26
+ const PROXY_PROVIDER = 'antigravity-proxy'
26
27
  /** Settings namespace projected from Config's volatile fields in src/settings.ts. */
27
28
  const ENTRY_ID = 'llm-antigravity-acp'
28
29
  /** Live fields owned by this card; ordinary fields stay in the host config editor. */
29
30
  const FIELDS = [
30
- { key: 'autoWarmup', group: 'general', checkbox: true, defaultChecked: true, label: '启动时自动预热', hint: '提前启动并保留连接,不发送模型消息。保存后下次启用插件生效;本次可点击“立即预热”。' },
31
- { key: 'reuseSessions', group: 'general', checkbox: true, defaultChecked: true, label: '复用 ACP 会话(增量发送)', hint: '校验历史与实际输出后只发新增消息;变化的上下文仍发送,不一致时自动重建。' },
32
- { key: 'proxyUrl', group: 'network', label: '出网代理', placeholder: 'http://127.0.0.1:7890', hint: '此处配置优先;留空则继承宿主代理环境变量,宿主也未配置时才直连。支持 HTTP、HTTPS 与 SOCKS5。' },
33
- { key: 'proxyBypass', group: 'network', label: '代理排除列表', placeholder: '*.internal,example.internal', hint: '逗号分隔,支持域名与通配后缀,不支持 CIDR。localhost/127.0.0.1/::1 始终自动排除(私有 MCP 桥在 loopback)。' },
34
- { key: 'agyCliPath', group: 'network', label: '官方 CLI 路径(可选)', placeholder: '留空自动查找 agy', hint: '用于模型目录、套餐额度和 AI Credits。自定义安装位置可填写完整可执行文件路径;CLI 登录与 ACP 登录独立。' },
35
- { key: 'resumeSessions', group: 'advanced', checkbox: true, label: '重启后恢复 ACP 会话', hint: '尝试加载本地映射,失败则重建;仅保存 ID 与指纹。需要同时开启会话复用。' },
36
- { key: 'keepProcessAfterCancel', group: 'advanced', checkbox: true, label: '取消后保留已停止的进程', hint: '仅在 agent 确认停止后保留;等待期间新轮排队,超时或通信失败回收。' },
37
- { key: 'cancelGraceMs', group: 'advanced', numeric: true, defaultValue: 30000, min: 0, max: 60000, label: '取消确认宽限(毫秒)', hint: '默认 30000;0 表示不等待并立即回收,不会复用未确认连接。仅开启取消后保留进程时生效。' },
38
- { key: 'idleRecycleMs', group: 'advanced', numeric: true, defaultValue: 300000, min: 0, max: 3600000, label: '进程空闲回收(毫秒)', hint: '默认 300000(5 分钟),0 禁用。聊天、预热、目录发现结束后统一计时;等待工具不算空闲。' },
31
+ { key: 'proxyMode', group: 'mode', checkbox: true, label: '使用代理模式', hint: '默认使用 ACP。勾选并保存后,通过代理账户直接调用上游模型;请启用插件页的 Antigravity Proxy Router 组件,并连接下方代理账户。' },
32
+ { key: 'autoWarmup', group: 'general', checkbox: true, defaultChecked: true, acpOnly: true, label: '启动时自动预热', hint: '提前启动并保留连接,不发送模型消息。保存后下次启用插件生效;本次可点击“立即预热”。代理模式下不预热 ACP。' },
33
+ { key: 'reuseSessions', group: 'general', checkbox: true, defaultChecked: true, acpOnly: true, label: '复用 ACP 会话(增量发送)', hint: '校验历史与实际输出后只发新增消息;变化的上下文仍发送,不一致时自动重建。' },
34
+ { key: 'proxyUrl', group: 'network', label: '出网代理', placeholder: 'http://127.0.0.1:7890', hint: '此处配置优先;留空则继承宿主代理环境变量。代理模式支持 HTTP、HTTPS CONNECT;ACP 模式还支持 SOCKS5。' },
35
+ { key: 'agyCliPath', group: 'network', acpOnly: true, label: '官方 CLI 路径(可选)', placeholder: '留空自动查找 agy', hint: 'ACP 模式可用 CLI 读取目录和额度;AI Credits 也来自 CLI。代理模式登录、目录、套餐额度和聊天不需要安装 CLI。' },
36
+ { key: 'resumeSessions', group: 'advanced', checkbox: true, acpOnly: true, label: '重启后恢复 ACP 会话', hint: '尝试加载本地映射,失败则重建;仅保存 ID 与指纹。需要同时开启会话复用。' },
37
+ { key: 'keepProcessAfterCancel', group: 'advanced', checkbox: true, acpOnly: true, label: '取消后保留已停止的进程', hint: '仅在 agent 确认停止后保留;等待期间新轮排队,超时或通信失败回收。' },
38
+ { key: 'cancelGraceMs', group: 'advanced', numeric: true, defaultValue: 30000, min: 0, max: 60000, acpOnly: true, label: '取消确认宽限(毫秒)', hint: '默认 30000;0 表示不等待并立即回收,不会复用未确认连接。仅开启取消后保留进程时生效。' },
39
+ { key: 'idleRecycleMs', group: 'advanced', numeric: true, defaultValue: 300000, min: 0, max: 3600000, acpOnly: true, label: '进程空闲回收(毫秒)', hint: '默认 300000(5 分钟),0 禁用。聊天、预热、目录发现结束后统一计时;等待工具不算空闲。' },
40
+ { key: 'binaryPath', group: 'launch', acpOnly: true, label: 'ACP Server 路径(可选)', placeholder: '留空自动解析或下载', hint: '显式指定官方 agy-acp-server;优先级高于环境变量与缓存。保存后按宿主配置生命周期生效。' },
41
+ { key: 'autoDownload', group: 'launch', checkbox: true, defaultChecked: true, acpOnly: true, label: '自动下载 ACP Server', hint: '本地没有可用二进制时,仅从官方 dl.google.com 下载。' },
42
+ { key: 'acpVersion', group: 'launch', acpOnly: true, label: 'ACP Server 版本', placeholder: '1.4.0', hint: '自动下载与缓存使用的官方版本;不是 ACP 协议版本。' },
43
+ { key: 'extraArgs', group: 'launch', lines: true, acpOnly: true, label: 'ACP 额外启动参数', placeholder: '每行一个参数', hint: '每行一个参数,按原顺序传给官方进程。不要用于绕过协议或权限边界。' },
44
+ { key: 'maxConcurrentSessions', group: 'pool', numeric: true, defaultValue: 2, min: 1, max: 4, acpOnly: true, label: '最大并发 ACP 连接数', hint: '1–4,按需启动;不同对话使用独立 worker。' },
45
+ { key: 'queueTimeoutMs', group: 'pool', numeric: true, defaultValue: 120000, min: 1000, acpOnly: true, label: '连接排队超时(毫秒)', hint: '默认 120000,最小 1000;等待空闲 worker 期间可取消。' },
46
+ { key: 'requestTimeoutMs', group: 'timeouts', numeric: true, defaultValue: 30000, min: 0, acpOnly: true, label: '普通 ACP 请求超时(毫秒)', hint: '默认 30000,覆盖单次协议请求。' },
47
+ { key: 'handshakeTimeoutMs', group: 'timeouts', numeric: true, defaultValue: 120000, min: 1000, acpOnly: true, label: 'ACP 握手超时(毫秒)', hint: '默认 120000,最小 1000;官方进程冷启动可能需要约 1 分钟。' },
48
+ { key: 'authTimeoutMs', group: 'timeouts', numeric: true, defaultValue: 300000, min: 0, acpOnly: true, label: '浏览器登录超时(毫秒)', hint: '默认 300000,仅用于需要交互授权的 ACP 登录。' },
49
+ { key: 'promptTimeoutMs', group: 'timeouts', numeric: true, defaultValue: 1800000, min: 0, acpOnly: true, label: '单轮生成超时(毫秒)', hint: '默认 1800000(30 分钟),覆盖 session/prompt。' },
50
+ { key: 'bridgeParkSettleMs', group: 'timeouts', numeric: true, defaultValue: 50, min: 0, acpOnly: true, label: '工具调用聚合窗口(毫秒)', hint: '默认 50;不是工具执行超时。' },
39
51
  ]
40
52
 
41
53
  /** 逐模型设置:键是 DSH 选择器里的模型 ID,值是开关、默认档位、显示名与官方 ID 映射。 */
@@ -146,7 +158,7 @@ window.__ModuleLoader__.load({
146
158
  function StatusNotice({ status }) {
147
159
  if (!status) return null
148
160
  const error = status.kind === 'error'
149
- return h('p', { role: error ? 'alert' : 'status', style: { margin: 0, padding: '10px 12px', borderRadius: '10px', fontSize: '12px', lineHeight: 1.6, overflowWrap: 'anywhere', color: error ? '#b42318' : status.kind === 'busy' ? '#946200' : '#177c55', background: error ? 'rgba(217,119,87,.10)' : status.kind === 'busy' ? 'rgba(217,155,40,.10)' : 'rgba(37,184,121,.10)' } }, status.text)
161
+ return h('p', { role: error ? 'alert' : 'status', style: { margin: 0, padding: '10px 12px', borderRadius: '10px', borderLeft: '3px solid ' + (error ? '#e5534b' : status.kind === 'busy' ? '#d99b28' : '#25b879'), fontSize: '12px', lineHeight: 1.6, overflowWrap: 'anywhere', color: 'inherit', background: error ? 'rgba(217,119,87,.10)' : status.kind === 'busy' ? 'rgba(217,155,40,.10)' : 'rgba(37,184,121,.10)' } }, status.text)
150
162
  }
151
163
  function RuntimeDetails({ runtime }) {
152
164
  if (!runtime) return null
@@ -181,9 +193,10 @@ window.__ModuleLoader__.load({
181
193
  const inputId = 'agy-setting-' + (React.useId?.() ?? field.key) + '-' + field.key
182
194
  return h('div', { style: { display: 'grid', gap: '6px' } },
183
195
  h('label', { htmlFor: inputId, style: { fontSize: '12px', fontWeight: 550 } }, field.label),
184
- h('input', {
196
+ h(field.lines ? 'textarea' : 'input', {
185
197
  id: inputId, name: field.key, type: field.numeric ? 'number' : 'text',
186
198
  ...(field.numeric ? { min: field.min, max: field.max, step: 1 } : {}),
199
+ ...(field.lines ? { rows: 3 } : {}),
187
200
  disabled, value: value ?? '', placeholder: field.placeholder,
188
201
  'aria-invalid': invalid || undefined, 'aria-describedby': inputId + '-hint',
189
202
  style: { ...ACTION, cursor: disabled ? 'not-allowed' : 'text', width: '100%', boxSizing: 'border-box', background: 'rgba(128,128,128,.035)', opacity: disabled ? .6 : 1 },
@@ -270,6 +283,88 @@ window.__ModuleLoader__.load({
270
283
  })))
271
284
  }
272
285
 
286
+ /**
287
+ * 代理账户面板:代理模式的独立登录态(与 ACP / 官方 CLI 登录相互独立)。
288
+ * 状态与授权 URL 经 Host 的 remote.agyProxyAccount 读取;授权 URL 只在
289
+ * 点击「连接代理账户」后返回一次,展示为可打开的链接。
290
+ */
291
+ function ProxyAccountPanel({ control, proxyMode, disabled, connectDisabled, onAccountChanged }) {
292
+ const [snapshot, setSnapshot] = React.useState(undefined)
293
+ const [authUrl, setAuthUrl] = React.useState(undefined)
294
+ const [message, setMessage] = React.useState('')
295
+ const [busy, setBusy] = React.useState(false)
296
+ const alive = React.useRef(true)
297
+ const version = React.useRef(0)
298
+ const polling = React.useRef(false)
299
+ const actionVersion = React.useRef(0)
300
+ const status = React.useCallback(async () => {
301
+ if (!control || polling.current) return
302
+ polling.current = true
303
+ const current = ++version.current
304
+ try {
305
+ const value = unwrap(await control('account', { action: 'status' }))
306
+ if (alive.current && current === version.current) setSnapshot(value.snapshot)
307
+ } catch (error) {
308
+ if (alive.current && current === version.current) setSnapshot({ phase: 'signed-out', message: error instanceof Error ? error.message : String(error), updatedAt: new Date().toISOString() })
309
+ } finally { polling.current = false }
310
+ }, [control])
311
+ React.useEffect(() => {
312
+ alive.current = true
313
+ void status()
314
+ const timer = setInterval(() => {
315
+ if (typeof document === 'undefined' || document.visibilityState !== 'hidden') void status()
316
+ }, 3000)
317
+ return () => { alive.current = false; ++version.current; clearInterval(timer) }
318
+ }, [status])
319
+ React.useEffect(() => {
320
+ if (!snapshot) return
321
+ setMessage('')
322
+ if (snapshot.phase !== 'pending-login') setAuthUrl(undefined)
323
+ onAccountChanged?.(snapshot)
324
+ }, [snapshot?.phase, snapshot?.connectionStage, snapshot?.message, snapshot?.revision, onAccountChanged])
325
+ const act = async (action) => {
326
+ if (!control || (busy && action === 'login')) return
327
+ const operation = ++actionVersion.current
328
+ ++version.current
329
+ setBusy(true)
330
+ setMessage(action === 'login' ? '正在准备授权…' : '')
331
+ try {
332
+ const value = unwrap(await control('account', { action }))
333
+ if (!alive.current || operation !== actionVersion.current) return
334
+ setSnapshot(value.snapshot)
335
+ setMessage(value.message ?? '')
336
+ if (value.authorizationUrl) setAuthUrl(value.authorizationUrl)
337
+ else if (action !== 'login') setAuthUrl(undefined)
338
+ } catch (error) {
339
+ if (alive.current && operation === actionVersion.current) setMessage(error instanceof Error ? error.message : String(error))
340
+ } finally { if (alive.current && operation === actionVersion.current) setBusy(false) }
341
+ }
342
+ const phase = snapshot?.phase
343
+ const stage = snapshot?.connectionStage
344
+ const connecting = busy || stage === 'preparing' || stage === 'finishing' || phase === 'pending-login'
345
+ const dotColor = phase === 'signed-in' ? '#25b879' : connecting ? '#d99b28' : phase === 'login-failed' ? '#e5534b' : '#808080'
346
+ const phaseLabel = phase === 'signed-in' ? '代理账户已连接' : stage === 'finishing' ? '正在完成登录…' : stage === 'preparing' ? '正在检查登录态…' : phase === 'pending-login' ? '等待浏览器授权…' : phase === 'login-failed' ? '登录失败' : busy ? '正在连接…' : '未连接'
347
+ return h('div', { style: { display: 'grid', gap: '10px', padding: '12px', borderRadius: '10px', background: 'rgba(128,128,128,.045)' } },
348
+ h('div', { style: { display: 'flex', alignItems: 'flex-start', gap: '10px' } },
349
+ h(StatusDot, { color: dotColor }),
350
+ h('div', { style: { minWidth: 0 } },
351
+ h('div', { style: { fontSize: '12px', fontWeight: 650 } }, phaseLabel + (snapshot?.email ? `(${snapshot.email})` : '') + (snapshot?.source === 'cli-import' ? ' · 导入自官方 CLI' : '')),
352
+ h('p', { role: 'status', style: { fontSize: '12px', lineHeight: 1.6, opacity: .7, margin: '3px 0 0', overflowWrap: 'anywhere' } }, message || snapshot?.message || ''))),
353
+ h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '8px' } },
354
+ h('button', { type: 'button', style: phase === 'signed-in' ? ACTION : PRIMARY, disabled: disabled || connectDisabled || connecting || phase === 'signed-in', onClick: () => act('login') }, connecting ? '连接中…' : '连接代理账户'),
355
+ connecting ? h('button', { type: 'button', style: ACTION, disabled, onClick: () => act('cancel') }, '取消登录') : null,
356
+ phase === 'signed-in' ? h('button', { type: 'button', style: ACTION, disabled: disabled || busy, onClick: () => act('logout') }, '退出登录') : null),
357
+ authUrl && stage !== 'finishing' ? h('div', { style: { display: 'grid', gap: '6px', justifyItems: 'start' } },
358
+ h('a', { href: authUrl, target: '_blank', rel: 'noopener noreferrer', style: { ...PRIMARY, textDecoration: 'none', display: 'inline-flex', alignItems: 'center' } }, '打开 Google 授权页'),
359
+ h('small', { style: { fontSize: '11px', opacity: .65, lineHeight: 1.6 } }, '在运行 DSH 的这台电脑上打开,完成后自动更新连接状态。授权有效期为 5 分钟。')) : null,
360
+ phase === 'pending-login' && stage !== 'finishing' && !authUrl && !busy ? h('p', { role: 'status', style: { fontSize: '11px', margin: 0, opacity: .7 } }, '授权链接在之前的页面中打开。若已找不到,请取消登录后重新连接。') : null,
361
+ h('p', { style: { fontSize: '11px', lineHeight: 1.6, margin: 0, opacity: .7 } }, '无需安装 agy CLI。没有可导入的登录态时,点击连接后使用 Google 浏览器授权。'),
362
+ h('p', { style: { opacity: .55, fontSize: '11px', lineHeight: 1.6, margin: 0 } },
363
+ proxyMode
364
+ ? '代理模式已开启:生成与额度都使用该账户;官方 ACP 路由已停用。'
365
+ : '代理模式关闭时该账户待命;连接时优先导入本机官方 CLI 登录态,缺失时走浏览器授权(回调端口 51121)。'))
366
+ }
367
+
273
368
  function AntigravitySettingsCard(props) {
274
369
  const remote = props.remote
275
370
  const [account, setAccount] = React.useState(undefined)
@@ -291,7 +386,7 @@ window.__ModuleLoader__.load({
291
386
  const value = unwrap(await props.control('models', force ? { force: true } : undefined))
292
387
  if (!alive.current || version !== modelVersion.current) return
293
388
  setModels(value.models ?? [])
294
- setModelMessage(value.status === 'error' ? value.message : (value.models?.length ? '目录已更新;不会自动替换当前会话的已选模型。' : '暂无模型目录。连接 / 登录 ACP 可验证会话并读取目录;也可从已登录的官方 CLI 拉取。'))
389
+ setModelMessage(value.status === 'error' ? value.message : (value.models?.length ? '目录已更新;不会自动替换当前会话的已选模型。' : '暂无模型目录。请先连接当前模式对应的账户,再刷新模型目录。'))
295
390
  } catch (error) { if (alive.current && version === modelVersion.current) setModelMessage(error instanceof Error ? error.message : String(error)) }
296
391
  finally {
297
392
  if (force) manualModelPull.current = false
@@ -333,6 +428,7 @@ window.__ModuleLoader__.load({
333
428
  const [saving, setSaving] = React.useState(false)
334
429
  const [dirtyKeys, setDirtyKeys] = React.useState([])
335
430
  const [conflicts, setConflicts] = React.useState([])
431
+ const [savedProxyMode, setSavedProxyMode] = React.useState(false)
336
432
  const settingsVersion = React.useRef(0)
337
433
  const savingRef = React.useRef(false)
338
434
  const refreshAfterSave = React.useRef(false)
@@ -340,6 +436,29 @@ window.__ModuleLoader__.load({
340
436
  const dirty = React.useRef(new Set())
341
437
  const draftRef = React.useRef(draft)
342
438
  draftRef.current = draft
439
+ const effectiveProxyMode = account?.generationMode ? account.generationMode.requested === 'proxy' : savedProxyMode
440
+ const activeModeRef = React.useRef(effectiveProxyMode)
441
+ activeModeRef.current = effectiveProxyMode
442
+ React.useEffect(() => { props.readQuota?.observeMode?.(effectiveProxyMode ? PROXY_PROVIDER : PROVIDER) }, [effectiveProxyMode, props.readQuota])
443
+ const proxyUnavailable = savedProxyMode && account?.generationMode?.proxyRouterReady === false
444
+ const previousProxyAccount = React.useRef(undefined)
445
+ const onProxyAccountChanged = React.useCallback((snapshot) => {
446
+ const previous = previousProxyAccount.current
447
+ previousProxyAccount.current = snapshot
448
+ const identityChanged = previous?.revision !== snapshot.revision
449
+ const connected = snapshot.phase === 'signed-in' && previous?.phase !== 'signed-in'
450
+ const signedOut = snapshot.phase === 'signed-out' && previous?.phase === 'signed-in'
451
+ props.readQuota?.observeProxyAccount?.(snapshot)
452
+ if (savedProxyMode && (connected || signedOut || (identityChanged && !snapshot.connectionStage))) void pullModels(true)
453
+ void accountAction('status')
454
+ }, [savedProxyMode, pullModels, accountAction, props.readQuota])
455
+ React.useEffect(() => {
456
+ let current = true
457
+ Promise.resolve().then(() => remote?.llm?.listProviders?.()).then(unwrap).then((providers) => {
458
+ if (current && alive.current && Array.isArray(providers)) setRegistered(providers.some((row) => row?.id === (effectiveProxyMode ? PROXY_PROVIDER : PROVIDER)))
459
+ }).catch(() => { if (current && alive.current) setRegistered(false) })
460
+ return () => { current = false }
461
+ }, [effectiveProxyMode, remote])
343
462
 
344
463
  const editField = React.useCallback((key, value) => {
345
464
  if (savingRef.current) return
@@ -397,7 +516,12 @@ window.__ModuleLoader__.load({
397
516
  const found = namespaces.find((item) => item?.ns === ENTRY_ID)
398
517
  setView(found ?? null)
399
518
  const current = found?.value && typeof found.value === 'object' ? found.value : {}
400
- const server = Object.fromEntries(FIELDS.map((field) => [field.key, field.checkbox ? (field.defaultChecked ? current[field.key] !== false : current[field.key] === true) : current[field.key] ?? field.defaultValue ?? '']))
519
+ const activeProvider = activeModeRef.current ? PROXY_PROVIDER : PROVIDER
520
+ setSavedProxyMode(current.proxyMode === true)
521
+ const server = Object.fromEntries(FIELDS.map((field) => [field.key, field.checkbox
522
+ ? (field.defaultChecked ? current[field.key] !== false : current[field.key] === true)
523
+ : field.lines ? (Array.isArray(current[field.key]) ? current[field.key].join('\n') : current[field.key] ?? '')
524
+ : current[field.key] ?? field.defaultValue ?? '']))
401
525
  // 整份 map 作为一个字段参与脏值比较;规范化后再比,避免键序或
402
526
  // 「显式写回默认值」被误判为未保存修改。
403
527
  server[OVERRIDES_KEY] = canonicalOverrides(current[OVERRIDES_KEY])
@@ -422,7 +546,7 @@ window.__ModuleLoader__.load({
422
546
  setDirtyKeys([...dirty.current])
423
547
  setConflicts((previous) => [...new Set([...previous.filter((key) => dirty.current.has(key)), ...changed])])
424
548
  setWritable(settings?.writable === true)
425
- setRegistered(rows.some((row) => row?.id === PROVIDER))
549
+ setRegistered(rows.some((row) => row?.id === activeProvider))
426
550
  if (!found) setStatus({ kind: 'error', text: '宿主尚未提供 Antigravity ACP 的可编辑设置。请确认 Host 与 Client 插件版本一致,并重新加载插件后刷新配置。' })
427
551
  else if (!preserveStatus) setStatus(null)
428
552
  } catch (cause) {
@@ -435,9 +559,9 @@ window.__ModuleLoader__.load({
435
559
  const save = React.useCallback(async () => {
436
560
  if (!view || !writable || loading || savingRef.current || dirty.current.size === 0) return
437
561
  const invalid = FIELDS.find((field) => field.numeric && dirty.current.has(field.key)
438
- && (!Number.isSafeInteger(draftRef.current[field.key]) || draftRef.current[field.key] < field.min || draftRef.current[field.key] > field.max))
562
+ && (!Number.isSafeInteger(draftRef.current[field.key]) || draftRef.current[field.key] < (field.min ?? 0) || (field.max !== undefined && draftRef.current[field.key] > field.max)))
439
563
  if (invalid) {
440
- setStatus({ kind: 'error', text: invalid.label + '必须是 ' + invalid.min + '–' + invalid.max + ' 的整数。' })
564
+ setStatus({ kind: 'error', text: invalid.label + '必须是' + (invalid.max === undefined ? '不小于 ' + (invalid.min ?? 0) : ' ' + invalid.min + '–' + invalid.max) + ' 的整数。' })
441
565
  return
442
566
  }
443
567
  savingRef.current = true
@@ -447,18 +571,28 @@ window.__ModuleLoader__.load({
447
571
  // modelOverrides 是整份 map 一个字段,按同样方式提交。
448
572
  const ops = [...FIELDS.map((field) => field.key), OVERRIDES_KEY]
449
573
  .filter((key) => dirty.current.has(key))
450
- .map((key) => ({ op: 'set', path: [key], value: draftRef.current[key] }))
574
+ .map((key) => {
575
+ const field = FIELDS.find((entry) => entry.key === key)
576
+ const value = field?.lines ? String(draftRef.current[key] ?? '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean) : draftRef.current[key]
577
+ return { op: 'set', path: [key], value }
578
+ })
451
579
  try {
452
580
  unwrap(await remote.settings.mutate(ENTRY_ID, ops, view.revision))
453
581
  if (!alive.current) return
454
582
  for (const operation of ops) {
455
583
  const key = operation.path[0]
456
- if (draftRef.current[key] === operation.value) dirty.current.delete(key)
584
+ const field = FIELDS.find((entry) => entry.key === key)
585
+ const currentValue = field?.lines ? String(draftRef.current[key] ?? '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean) : draftRef.current[key]
586
+ if (JSON.stringify(currentValue) === JSON.stringify(operation.value)) dirty.current.delete(key)
457
587
  }
458
588
  setDirtyKeys([...dirty.current])
459
589
  setConflicts([])
460
- setStatus({ kind: 'ok', text: '已保存。会话偏好从下一轮生效;代理用于后续请求及新启动的 ACP 进程,已有进程需重新加载插件才能切换代理。自动预热在下次启用插件时生效;当前已选模型保持不变。' })
590
+ setStatus({ kind: 'ok', text: ops.some((operation) => operation.path[0] === 'proxyMode')
591
+ ? '已保存运行模式,请在会话中选择该模式下的模型。会话偏好从下一轮生效。'
592
+ : '已保存。会话偏好从下一轮生效;代理用于后续请求及新启动的 ACP 进程,已有进程需重新加载插件才能切换代理。自动预热在下次启用插件时生效;当前已选模型保持不变。' })
461
593
  await reload(true)
594
+ void accountAction('status')
595
+ void pullModels(true)
462
596
  } catch (cause) {
463
597
  if (!alive.current) return
464
598
  setStatus({ kind: 'error', text: cause instanceof Error ? cause.message : String(cause) })
@@ -471,7 +605,7 @@ window.__ModuleLoader__.load({
471
605
  if (refreshAfterSave.current) { refreshAfterSave.current = false; void reload(true) }
472
606
  }
473
607
  }
474
- }, [loading, writable, remote, reload, view])
608
+ }, [loading, writable, remote, reload, view, accountAction, pullModels])
475
609
 
476
610
  React.useEffect(() => {
477
611
  reload()
@@ -490,6 +624,7 @@ window.__ModuleLoader__.load({
490
624
  // 目录条目 + 已配置但目录里暂时没有的 ID:正在编辑的映射不能因为目录刷新而消失。
491
625
  const rows = modelRows(models, draft[OVERRIDES_KEY])
492
626
  const editingDisabled = loading || saving || !view || !writable
627
+ const connectionSettingsDirty = dirtyKeys.some(key => key === 'proxyUrl' || key === 'proxyBypass')
493
628
  const discardDraft = () => {
494
629
  if (savingRef.current) return
495
630
  dirty.current.clear()
@@ -501,26 +636,42 @@ window.__ModuleLoader__.load({
501
636
  }
502
637
 
503
638
  const connection = connectionStatus(account, registered)
504
- const fieldsFor = (group) => FIELDS.filter((field) => field.group === group)
639
+ const fieldsFor = (group) => FIELDS.filter((field) => field.group === group && (!effectiveProxyMode || !field.acpOnly))
505
640
  const fieldProps = { draft, dirtyKeys, conflicts, disabled: editingDisabled, editField }
506
641
  const busy = ['connecting', 'cancelling'].includes(account?.state) || runtimeBusy(account?.runtime)
507
642
  const readonly = !loading && view && !writable
508
643
  const advancedSummary = h('summary', { style: SUMMARY },
509
644
  h('span', { style: { display: 'grid', gap: '4px' } },
510
645
  h('span', { style: { fontSize: '13px', fontWeight: 650 } }, '高级设置'),
511
- h('small', { style: { fontSize: '11px', fontWeight: 400, opacity: .6 } }, '网络、CLI、会话恢复与进程生命周期')))
646
+ h('small', { style: { fontSize: '11px', fontWeight: 400, opacity: .6 } }, effectiveProxyMode ? '代理网络设置' : '网络、启动、并发、超时与进程生命周期')))
512
647
 
513
- return h('div', { style: { padding: '20px', display: 'flex', flexDirection: 'column', gap: '14px', maxWidth: '720px', boxSizing: 'border-box', lineHeight: 1.5 } },
514
- h('header', { style: { display: 'flex', alignItems: 'flex-start', gap: '12px', padding: '0 2px 2px' } },
515
- h('div', { style: { flex: 1, minWidth: 0 } },
648
+ return h('div', { className: 'antigravity-settings-card', style: { padding: '20px', display: 'flex', flexDirection: 'column', gap: '14px', maxWidth: '720px', boxSizing: 'border-box', lineHeight: 1.5 } },
649
+ h('div', { role: 'region', 'aria-label': '设置保存操作', style: { position: 'sticky', top: 0, zIndex: 20, flexShrink: 0, background: 'var(--dsh-surface, Canvas)', color: 'var(--dsh-text, CanvasText)', padding: '12px', border: '1px solid rgba(128,128,128,.22)', borderRadius: '10px', boxShadow: '0 3px 12px rgba(0,0,0,.08)', display: 'grid', gap: '8px' } },
650
+ h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '8px', alignItems: 'center' } },
651
+ h('button', { type: 'button', onClick: save, disabled: editingDisabled || dirtyKeys.length === 0, style: PRIMARY }, saving ? '保存中…' : '保存'),
652
+ dirtyKeys.length ? h('button', { type: 'button', disabled: loading || saving, onClick: discardDraft, style: ACTION }, '撤销未保存修改') : null,
653
+ h('span', { role: 'status', 'aria-live': 'polite', style: { fontSize: '11px', opacity: .7 } }, dirtyKeys.length ? dirtyKeys.length + ' 项未保存' : loading ? '正在读取设置…' : readonly ? '只读设置' : '没有未保存的修改'),
654
+ h('button', { type: 'button', title: '重新读取宿主配置,保留未保存的输入', disabled: loading || saving, onClick: () => reload(), style: { ...ACTION, borderColor: 'transparent', background: 'transparent', opacity: .7, marginLeft: 'auto' } }, loading ? '刷新中…' : '刷新配置')),
655
+ conflicts.length ? h('p', { role: 'alert', style: { margin: 0, fontSize: '12px', lineHeight: 1.6 } }, '其他位置更新了以下设置,已保留你的输入:' + conflicts.map((key) => key === OVERRIDES_KEY ? OVERRIDES_LABEL : FIELDS.find((field) => field.key === key)?.label ?? key).join('、') + '。请核对后再保存。') : null,
656
+ h(StatusNotice, { status })),
657
+ h('header', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'flex-start', gap: '10px', padding: '0 2px 2px' } },
658
+ h('div', { style: { flex: '1 1 220px', minWidth: 0 } },
516
659
  h('div', { style: { fontSize: '10px', fontWeight: 650, letterSpacing: '.1em', opacity: .5, marginBottom: '5px' } }, 'MODEL PROVIDER'),
517
660
  h('h2', { style: { margin: 0, fontSize: '20px', lineHeight: 1.35, fontWeight: 650, letterSpacing: '-.015em' } }, 'Antigravity ACP'),
518
661
  h('p', { style: { margin: '6px 0 0', fontSize: '12px', opacity: .6 } }, '连接官方 Antigravity,模型与工具仍由 DSH 管理。')),
519
- h('span', { title: 'Provider 路由:' + PROVIDER, style: { flex: '0 0 auto', padding: '4px 9px', borderRadius: '99px', fontSize: '11px', color: registered === false ? '#b42318' : registered === true ? '#177c55' : 'inherit', background: registered === false ? 'rgba(217,119,87,.10)' : registered === true ? 'rgba(37,184,121,.10)' : TRACK } }, registered === false ? '未注册' : registered === true ? 'Provider 已注册' : '正在检查路由')),
662
+ h('span', { title: 'Provider 路由:' + (effectiveProxyMode ? PROXY_PROVIDER : PROVIDER), style: { flex: '0 0 auto', padding: '4px 9px', borderRadius: '99px', fontSize: '11px', color: registered === false ? '#b42318' : registered === true ? '#177c55' : 'inherit', background: registered === false ? 'rgba(217,119,87,.10)' : registered === true ? 'rgba(37,184,121,.10)' : TRACK } }, registered === false ? '未注册' : registered === true ? 'Provider 已注册' : '正在检查路由'),
663
+ h('span', { title: effectiveProxyMode ? '生成直连上游,官方 ACP 路由停用' : '生成走官方 ACP 进程', style: { flex: '0 0 auto', padding: '4px 9px', borderRadius: '99px', fontSize: '11px', color: effectiveProxyMode ? '#946200' : 'inherit', background: effectiveProxyMode ? 'rgba(217,159,4,.12)' : TRACK } }, effectiveProxyMode ? '代理模式' : 'ACP 模式')),
520
664
 
521
665
  readonly ? h(StatusNotice, { status: { kind: 'busy', text: '当前设置为只读。你仍可查看连接状态与诊断;请在有写入权限的宿主中修改配置。' } }) : null,
666
+ connectionSettingsDirty ? h(StatusNotice, { status: { kind: 'busy', text: '网络设置尚未保存。请先点击顶部「保存」,再连接或登录;取消和退出仍可使用。' } }) : null,
667
+
668
+ h('section', { style: PANEL, 'aria-label': '运行模式' },
669
+ h(SectionHeading, { title: '运行模式', description: '保存后切换生成方式,再连接对应账户并选择模型。' }),
670
+ h(SettingsFields, { ...fieldProps, fields: fieldsFor('mode') }),
671
+ dirtyKeys.includes('proxyMode') ? h('p', { role: 'status', style: { fontSize: '12px', color: '#946200' } }, '模式修改尚未保存,当前仍使用' + (effectiveProxyMode ? '代理模式。' : ' ACP 模式。')) : null,
672
+ proxyUnavailable ? h(StatusNotice, { status: { kind: 'error', text: '代理路由组件尚未就绪,ACP 功能保持停用。请在插件页启用 Antigravity Proxy Router 组件,再连接代理账户。' } }) : null),
522
673
 
523
- h('section', { style: PANEL, 'aria-label': '账户连接' },
674
+ !effectiveProxyMode ? h('section', { style: PANEL, 'aria-label': '账户连接' },
524
675
  h(SectionHeading, { eyebrow: '01 · 连接', title: '账户与连接', description: '先预热减少等待,或验证账户并读取模型目录。' }),
525
676
  h('div', { style: { display: 'flex', alignItems: 'flex-start', gap: '10px', padding: '12px', borderRadius: '10px', background: 'rgba(128,128,128,.045)', marginBottom: '14px' } },
526
677
  h(StatusDot, { color: connection.color }),
@@ -528,48 +679,57 @@ window.__ModuleLoader__.load({
528
679
  h('div', { style: { fontSize: '12px', fontWeight: 650, color: connection.color } }, connection.label),
529
680
  h('p', { role: 'status', style: { fontSize: '12px', lineHeight: 1.6, opacity: .7, overflowWrap: 'anywhere', margin: '3px 0 0' } }, connection.description))),
530
681
  h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '8px' } },
531
- h('button', { type: 'button', style: PRIMARY, disabled: !props.control || busy, onClick: () => accountAction('login') }, account?.state === 'connecting' ? '正在连接…' : '连接 / 登录 ACP'),
532
- h('button', { type: 'button', style: ACTION, disabled: !props.control || busy || connection.ready, onClick: () => accountAction('warmup') }, connection.ready ? '已预热' : '立即预热'),
682
+ h('button', { type: 'button', style: PRIMARY, disabled: !props.control || busy || connectionSettingsDirty, onClick: () => accountAction('login') }, account?.state === 'connecting' ? '正在连接…' : '连接 / 登录 ACP'),
683
+ h('button', { type: 'button', style: ACTION, disabled: !props.control || busy || connection.ready || connectionSettingsDirty, onClick: () => accountAction('warmup') }, connection.ready ? '已预热' : '立即预热'),
533
684
  ['connecting', 'cancelling'].includes(account?.state) ? h('button', { type: 'button', style: ACTION, disabled: account?.state === 'cancelling', onClick: () => accountAction('cancel') }, account?.state === 'cancelling' ? '正在取消…' : '取消连接') : null),
534
- h('p', { style: { opacity: .55, fontSize: '11px', lineHeight: 1.6, margin: '10px 0 0' } }, '预热不会发送聊天消息。仅在需要登录时打开官方授权页面;思考强度仍由会话菜单设置。')),
685
+ h('p', { style: { opacity: .55, fontSize: '11px', lineHeight: 1.6, margin: '10px 0 0' } }, '预热不会发送聊天消息。仅在需要登录时打开官方授权页面;思考强度仍由会话菜单设置。')) : null,
686
+
687
+ h('section', { style: PANEL, 'aria-label': '代理账户' },
688
+ h(SectionHeading, { eyebrow: '01b · 代理模式', title: '代理账户', description: '使用 Google 账户授权,不需要安装 agy CLI。' }),
689
+ h(ProxyAccountPanel, { control: props.proxyControl, proxyMode: effectiveProxyMode, onAccountChanged: onProxyAccountChanged, disabled: !props.proxyControl || !view || !writable, connectDisabled: editingDisabled || connectionSettingsDirty })),
535
690
 
536
691
  h('section', { style: PANEL, 'aria-label': '模型列表' },
537
692
  h(SectionHeading, { eyebrow: '02 · 模型', title: '模型列表', description: '目录中的模型是否出现在 DSH 选择器,以及各自的默认思考档位与官方 ID 映射。',
538
- action: h('button', { type: 'button', title: '从官方 CLI 或 ACP 会话重新读取模型目录', 'aria-busy': modelBusy || undefined, style: { ...ACTION, flex: '0 0 auto', whiteSpace: 'nowrap' }, disabled: modelBusy || !props.control, onClick: () => pullModels(true) }, modelBusy ? '正在刷新…' : '刷新模型目录') }),
693
+ action: h('button', { type: 'button', title: '按当前已保存的模式重新读取模型目录', 'aria-busy': modelBusy || undefined, style: { ...ACTION, flex: '0 0 auto', whiteSpace: 'nowrap' }, disabled: modelBusy || !props.control, onClick: () => pullModels(true) }, modelBusy ? '正在刷新…' : '刷新模型目录') }),
539
694
  h(ModelOverrideList, { rows: rows, overrides: draft[OVERRIDES_KEY], disabled: editingDisabled, onEdit: editOverride }),
540
695
  modelMessage ? h('p', { role: 'status', style: { fontSize: '11px', lineHeight: 1.6, opacity: .65, margin: '10px 0 0', overflowWrap: 'anywhere' } }, modelMessage) : null),
541
696
 
542
- h('section', { style: PANEL, 'aria-label': '常用设置' },
697
+ h(SettingsQuotaPanel, { readQuota: props.readQuota, source: effectiveProxyMode ? PROXY_PROVIDER : PROVIDER,
698
+ accountRevision: effectiveProxyMode ? previousProxyAccount.current?.revision : undefined }),
699
+
700
+ !effectiveProxyMode ? h('section', { style: PANEL, 'aria-label': '常用设置' },
543
701
  h(SectionHeading, { eyebrow: '03 · 偏好', title: '常用设置', description: '适合大多数使用场景;其它选项保持默认即可。' }),
544
- h(SettingsFields, { ...fieldProps, fields: fieldsFor('general') })),
702
+ h(SettingsFields, { ...fieldProps, fields: fieldsFor('general') })) : null,
545
703
 
546
704
  h('details', { style: PANEL, 'aria-label': '高级设置' }, advancedSummary,
547
705
  h('div', { style: { display: 'grid', gap: '24px', marginTop: '20px', paddingTop: '18px', borderTop: '1px solid rgba(128,128,128,.14)' } },
548
706
  h('section', { 'aria-label': '网络与 CLI 设置', style: { display: 'grid', gap: '16px' } },
549
- h('h4', { style: { margin: 0, fontSize: '12px', fontWeight: 650 } }, '网络与 CLI'),
707
+ h('h4', { style: { margin: 0, fontSize: '12px', fontWeight: 650 } }, effectiveProxyMode ? '代理网络' : '网络与 CLI'),
550
708
  h(SettingsFields, { ...fieldProps, fields: fieldsFor('network') })),
551
- h('section', { 'aria-label': '会话与进程设置', style: { display: 'grid', gap: '16px' } },
709
+ !effectiveProxyMode ? h('section', { 'aria-label': 'ACP 启动设置', style: { display: 'grid', gap: '16px' } },
710
+ h('h4', { style: { margin: 0, fontSize: '12px', fontWeight: 650 } }, 'ACP 启动'),
711
+ h(SettingsFields, { ...fieldProps, fields: fieldsFor('launch') })) : null,
712
+ !effectiveProxyMode ? h('section', { 'aria-label': 'ACP 连接池设置', style: { display: 'grid', gap: '16px' } },
713
+ h('h4', { style: { margin: 0, fontSize: '12px', fontWeight: 650 } }, '连接池'),
714
+ h(SettingsFields, { ...fieldProps, fields: fieldsFor('pool') })) : null,
715
+ !effectiveProxyMode ? h('section', { 'aria-label': 'ACP 超时设置', style: { display: 'grid', gap: '16px' } },
716
+ h('h4', { style: { margin: 0, fontSize: '12px', fontWeight: 650 } }, '请求与工具超时'),
717
+ h(SettingsFields, { ...fieldProps, fields: fieldsFor('timeouts') })) : null,
718
+ !effectiveProxyMode ? h('section', { 'aria-label': '会话与进程设置', style: { display: 'grid', gap: '16px' } },
552
719
  h('h4', { style: { margin: 0, fontSize: '12px', fontWeight: 650 } }, '会话与进程'),
553
- h(SettingsFields, { ...fieldProps, fields: fieldsFor('advanced') })))),
720
+ h(SettingsFields, { ...fieldProps, fields: fieldsFor('advanced') })) : null)),
554
721
 
555
- h('details', { style: PANEL, 'aria-label': '连接诊断' },
722
+ !effectiveProxyMode ? h('details', { style: PANEL, 'aria-label': '连接诊断' },
556
723
  h('summary', { style: SUMMARY },
557
724
  h('span', { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
558
725
  h(StatusDot, { color: connection.color }),
559
726
  h('span', { style: { fontSize: '13px', fontWeight: 650 } }, '连接诊断')),
560
727
  h('small', { style: { fontSize: '11px', opacity: .6 } }, '状态、并发与耗时')),
561
728
  h('div', { style: { marginTop: '18px', paddingTop: '16px', borderTop: '1px solid rgba(128,128,128,.14)' } },
562
- account?.runtime ? RuntimeDetails({ runtime: account.runtime }) : h('p', { style: { margin: 0, fontSize: '12px', opacity: .6 } }, '连接后显示生命周期、并发容量与分阶段耗时。'))),
729
+ account?.runtime ? RuntimeDetails({ runtime: account.runtime }) : h('p', { style: { margin: 0, fontSize: '12px', opacity: .6 } }, '连接后显示生命周期、并发容量与分阶段耗时。'))) : null,
563
730
 
564
731
  h('footer', { style: { display: 'grid', gap: '10px', padding: '4px 2px 0' } },
565
- conflicts.length ? h('p', { role: 'alert', style: { margin: 0, fontSize: '12px', lineHeight: 1.6, color: '#946200' } }, '其他位置更新了以下设置,已保留你的输入:' + conflicts.map((key) => key === OVERRIDES_KEY ? OVERRIDES_LABEL : FIELDS.find((field) => field.key === key)?.label ?? key).join('、') + '。请核对后再保存。') : null,
566
- h(StatusNotice, { status }),
567
- h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '8px', alignItems: 'center' } },
568
- h('button', { type: 'button', onClick: save, disabled: editingDisabled || dirtyKeys.length === 0, style: PRIMARY }, saving ? '保存中…' : '保存'),
569
- dirtyKeys.length ? h('button', { type: 'button', disabled: loading || saving, onClick: discardDraft, style: ACTION }, '撤销未保存修改') : null,
570
- h('button', { type: 'button', title: '重新读取宿主配置,保留未保存的输入', disabled: loading || saving, onClick: () => reload(), style: { ...ACTION, borderColor: 'transparent', background: 'transparent', opacity: .7, marginLeft: 'auto' } }, loading ? '刷新中…' : '刷新配置')),
571
- dirtyKeys.length ? h('p', { role: 'status', style: { margin: 0, fontSize: '11px', opacity: .65 } }, '有 ' + dirtyKeys.length + ' 项未保存的修改;刷新会保留这些输入。') : null,
572
- h('p', { style: { margin: '2px 0 0', paddingTop: '12px', borderTop: '1px solid rgba(128,128,128,.12)', fontSize: '11px', lineHeight: 1.6, opacity: .55 } }, '套餐额度与 AI Credits 请在对话输入框旁的额度药丸中查看,此处不再重复展示。')))
732
+ h('p', { style: { margin: '2px 0 0', paddingTop: '12px', borderTop: '1px solid rgba(128,128,128,.12)', fontSize: '11px', lineHeight: 1.6, opacity: .55 } }, '对话输入框旁的额度药丸也可查看明细。额度仅供参考,AI Credits 始终来自官方 CLI 登录账户。')))
573
733
  }
574
734
 
575
735
  /**
@@ -594,7 +754,7 @@ window.__ModuleLoader__.load({
594
754
  return undefined
595
755
  }
596
756
  function modelGroup(model) {
597
- const id = String(model ?? '').toLowerCase()
757
+ const id = String(model ?? '').toLowerCase().replace(/^antigravity-/, '')
598
758
  if (/^gemini-/.test(id) && /(?:^|-)argon(?:-|$)/.test(id)) return 'argon'
599
759
  if (/^gemini-/.test(id)) return 'gemini'
600
760
  if (/^(claude|gpt)-/.test(id)) return 'claude'
@@ -627,26 +787,27 @@ window.__ModuleLoader__.load({
627
787
  if (minutes >= 60) return Math.floor(minutes / 60) + '小时 ' + minutes % 60 + '分钟后重置'
628
788
  return minutes + '分钟后重置'
629
789
  }
630
- function remainingBar(bucket, compact, stale, label) {
790
+ function remainingBar(bucket, compact, stale, label, compactHeight = 4) {
631
791
  const pct = percentage(bucket)
632
792
  const unknown = pct === undefined || bucket.disabled
633
793
  return h('span', {
634
794
  role: 'progressbar', 'aria-label': label,
635
795
  'aria-valuemin': 0, 'aria-valuemax': 100,
636
796
  ...(unknown ? { 'aria-valuetext': bucket.disabled ? '已停用' : '额度未知' } : { 'aria-valuenow': pct }),
637
- style: { display: 'block', width: compact ? '30px' : '100%', height: compact ? '4px' : '6px', borderRadius: '99px', overflow: 'hidden', background: TRACK },
797
+ style: { display: 'block', width: compact ? '34px' : '100%', height: compact ? compactHeight + 'px' : '4px', borderRadius: '99px', overflow: 'hidden', background: TRACK },
638
798
  }, h('span', { style: { display: 'block', height: '100%', width: unknown ? '100%' : pct + '%', opacity: unknown ? .35 : 1, borderRadius: '99px', background: tone(bucket, stale) } }))
639
799
  }
640
- function useQuota(readQuota, enabled = true) {
800
+ function useQuota(readQuota, enabled = true, source) {
641
801
  const [data, setData] = React.useState(null)
642
802
  const [refreshing, setRefreshing] = React.useState(false)
643
803
  const version = React.useRef(0)
804
+ const currentSource = React.useRef(source)
644
805
  const update = React.useCallback((value) => setData(value), [])
645
806
  const load = React.useCallback((force = false) => {
646
807
  if (!enabled || !readQuota) return Promise.resolve()
647
808
  const current = ++version.current
648
809
  setRefreshing(true)
649
- return Promise.resolve().then(() => readQuota(force)).then(unwrap).then((value) => {
810
+ return Promise.resolve().then(() => readQuota(force, source)).then(unwrap).then((value) => {
650
811
  if (current === version.current) update(value)
651
812
  }).catch((error) => {
652
813
  if (current === version.current) {
@@ -656,95 +817,154 @@ window.__ModuleLoader__.load({
656
817
  : { status: 'error', groups: [], error: { message } })
657
818
  }
658
819
  }).finally(() => { if (current === version.current) setRefreshing(false) })
659
- }, [readQuota, enabled, update])
820
+ }, [readQuota, enabled, update, source])
660
821
  React.useEffect(() => {
822
+ currentSource.current = source
823
+ setData(null)
661
824
  if (!enabled || !readQuota) { setData(null); return undefined }
662
825
  void load(false)
663
- const off = readQuota.subscribe?.((value) => { ++version.current; update(value) })
826
+ const off = readQuota.subscribe?.((value, origin) => {
827
+ if (origin !== undefined && origin !== source) return
828
+ if (value === null) {
829
+ ++version.current; update(null); setRefreshing(false)
830
+ void load(true)
831
+ return
832
+ }
833
+ ++version.current; update(value); setRefreshing(false)
834
+ })
664
835
  const timer = setInterval(() => { if (typeof document === 'undefined' || document.visibilityState !== 'hidden') void load(false) }, 300_000)
665
836
  return () => { ++version.current; clearInterval(timer); off?.() }
666
- }, [load, enabled, readQuota, update])
667
- return { data, refreshing, load }
837
+ }, [load, enabled, readQuota, update, source])
838
+ return { data: currentSource.current === source ? data : null, refreshing, load }
668
839
  }
669
840
  function CreditsDetails({ credits, pending }) {
670
841
  const valid = credits?.status === 'ok' && Number.isSafeInteger(credits.remainingCredits) && credits.remainingCredits >= 0
671
842
  const label = valid ? credits.remainingCredits.toLocaleString() : pending ? '正在读取…' : credits?.status === 'error' ? '查询失败' : '未提供'
672
- return h('section', { style: CARD, 'aria-label': 'AI Credits 余额' },
843
+ return h('section', { style: { padding: '9px 0', borderTop: '1px solid rgba(128,128,128,.16)' }, 'aria-label': 'AI Credits 余额' },
673
844
  h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: '12px' } },
674
- h('strong', null, 'AI Credits 余额'),
675
- h('strong', { style: { fontSize: valid ? '20px' : '13px', fontVariantNumeric: 'tabular-nums', color: credits?.stale ? '#d99b28' : 'inherit' } }, label)),
676
- credits?.planName ? h('div', { style: { fontSize: '11px', opacity: .7, marginTop: '6px' } }, '套餐:' + credits.planName) : null,
677
- credits?.stale ? h('p', { role: 'status', style: { margin: '6px 0 0', fontSize: '11px', color: '#d99b28' } }, '余额缓存 · ' + (credits.staleReason ?? '余额刷新失败')) : null,
678
- credits?.error?.message ? h('p', { role: credits.status === 'error' ? 'alert' : 'status', style: { margin: '6px 0 0', fontSize: '11px', overflowWrap: 'anywhere' } }, credits.error.message) : null,
679
- credits?.updatedAt ? h('div', { style: { marginTop: '6px', fontSize: '10px', opacity: .55 } }, (valid ? '余额更新于 ' : '最近查询 ') + new Date(credits.updatedAt).toLocaleString()) : null)
680
- }
681
- function QuotaDetails({ data, refreshing, load, model }) {
682
- return h('div', { style: { display: 'grid', gap: '12px' } },
683
- h('div', { style: { display: 'flex', gap: '8px', alignItems: 'center' } },
684
- h('strong', { style: { fontSize: '14px', flex: 1 } }, 'Antigravity · 套餐额度'),
685
- data?.plan ? h('span', { style: { fontSize: '11px', padding: '2px 8px', background: TRACK, borderRadius: '99px' } }, data.plan) : null),
686
- data?.stale ? h('p', { role: 'status', style: { ...CARD, margin: 0, color: '#d99b28', fontSize: '12px' } }, '缓存数据 · ' + (data.staleReason ?? '刷新失败')) : null,
687
- data?.status === 'error' ? h('p', { role: 'alert', style: { margin: 0, color: '#ef4444', overflowWrap: 'anywhere' } }, data.error?.message ?? data.error?.text ?? '额度读取失败') : null,
688
- ...quotaGroups(data).map((group) => h('section', { key: group.key, style: CARD, 'aria-label': group.name + '额度' },
689
- h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '10px' } },
690
- h('strong', { style: { fontSize: '13px' } }, group.name),
691
- group.key === modelGroup(model) ? h('span', { style: { fontSize: '10px', padding: '2px 6px', borderRadius: '6px', color: '#25b879', background: 'rgba(37,184,121,.12)' } }, '当前模型') : null),
692
- group.buckets.length ? h('div', { style: { display: 'grid', gap: '12px' } }, ...group.buckets.map((bucket, index) => {
693
- const pct = percentage(bucket)
694
- return h('div', { key: index },
695
- h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: '12px', marginBottom: '6px' } },
696
- h('span', { style: { fontSize: '12px', opacity: .75 } }, windowName(bucket)),
697
- h('strong', { style: { fontSize: '16px', fontVariantNumeric: 'tabular-nums' } }, bucket.disabled ? '已停用' : pct === undefined ? '未知' : Math.round(pct) + '%', h('small', { style: { fontSize: '10px', fontWeight: 400, opacity: .6, marginLeft: '4px' } }, '剩余'))),
698
- remainingBar(bucket, false, data?.stale, group.name + ' ' + windowName(bucket) + '剩余额度'),
699
- h('div', { title: bucket.resetTime, style: { fontSize: '10px', opacity: .6, marginTop: '5px' } }, bucket.disabled ? bucket.description ?? '此窗口不可用' : resetCountdown(bucket.resetTime)))
700
- })) : h('span', { style: { opacity: .55, fontSize: '12px' } }, data ? '未提供额度数据' : '正在读取…'))),
701
- CreditsDetails({ credits: data?.credits, pending: !data }),
702
- h('div', { style: { display: 'flex', alignItems: 'center', gap: '10px' } },
703
- h('small', { style: { flex: 1, opacity: .55, minWidth: 0 } }, data?.updatedAt ? '更新于 ' + new Date(data.updatedAt).toLocaleString() : '尚无成功快照'),
704
- h('button', { type: 'button', disabled: refreshing || !load, onClick: () => load(true), style: ACTION }, refreshing ? '刷新中…' : '刷新额度')))
845
+ h('strong', { style: { fontSize: '12px' } }, 'AI Credits'),
846
+ h('strong', { style: { fontSize: valid ? '17px' : '12px', fontVariantNumeric: 'tabular-nums', color: credits?.stale ? '#d99b28' : 'inherit' } }, label)),
847
+ credits?.planName || credits?.updatedAt ? h('div', { style: { display: 'flex', flexWrap: 'wrap', justifyContent: 'space-between', gap: '3px 10px', fontSize: '10px', opacity: .55, marginTop: '3px' } },
848
+ credits?.planName ? h('span', null, credits.planName) : null,
849
+ credits?.updatedAt ? h('span', null, (valid ? '余额更新于 ' : '最近查询 ') + new Date(credits.updatedAt).toLocaleString()) : null) : null,
850
+ credits?.stale ? h('p', { role: 'status', style: { margin: '4px 0 0', fontSize: '10px', color: '#d99b28' } }, '余额缓存 · ' + (credits.staleReason ?? '余额刷新失败')) : null,
851
+ credits?.error?.message ? h('p', { role: credits.status === 'error' ? 'alert' : 'status', style: { margin: '4px 0 0', fontSize: '10px', overflowWrap: 'anywhere' } }, credits.error.message) : null)
705
852
  }
706
- function useAcpWarmup(control, enabled) {
707
- const [account, setAccount] = React.useState(undefined)
708
- const [pending, setPending] = React.useState(false)
709
- const alive = React.useRef(false)
853
+ function SettingsQuotaPanel({ readQuota, source, accountRevision }) {
854
+ const [data, setData] = React.useState(null)
855
+ const [refreshing, setRefreshing] = React.useState(false)
710
856
  const version = React.useRef(0)
711
- const polling = React.useRef(false)
712
- const run = React.useCallback(async (action = 'status') => {
713
- if (!control || !enabled || (action === 'status' && polling.current)) return
857
+ const requested = React.useRef(false)
858
+ const pending = React.useRef(false)
859
+ React.useEffect(() => {
860
+ ++version.current
861
+ requested.current = false
862
+ pending.current = false
863
+ setData(null)
864
+ setRefreshing(false)
865
+ const off = readQuota?.subscribe?.((value, origin) => {
866
+ if (origin !== source) return
867
+ if (value === null) {
868
+ ++version.current; requested.current = false; pending.current = false
869
+ setData(null); setRefreshing(false)
870
+ } else if (requested.current) setData(value)
871
+ })
872
+ return () => { ++version.current; off?.() }
873
+ }, [readQuota, source, accountRevision])
874
+ const load = async () => {
875
+ if (!readQuota || pending.current) return
876
+ pending.current = true
877
+ requested.current = true
714
878
  const current = ++version.current
715
- polling.current = true
716
- if (action === 'warmup') setPending(true)
879
+ setRefreshing(true)
717
880
  try {
718
- const value = unwrap(await control('account', { action }))
719
- if (alive.current && current === version.current) setAccount(value)
881
+ const value = unwrap(await readQuota(true, source))
882
+ if (current === version.current) setData(value)
720
883
  } catch (error) {
721
- if (alive.current && current === version.current) setAccount({ state: 'error', message: error instanceof Error ? error.message : String(error) })
884
+ if (current === version.current) {
885
+ const message = error instanceof Error ? error.message : String(error)
886
+ setData(previous => previous?.status === 'ok' ? { ...previous, stale: true, staleReason: message }
887
+ : { status: 'error', groups: [], error: { message } })
888
+ }
722
889
  } finally {
723
- polling.current = false
724
- if (alive.current && action === 'warmup') setPending(false)
890
+ if (current === version.current) { pending.current = false; setRefreshing(false) }
725
891
  }
726
- }, [control, enabled])
727
- React.useEffect(() => {
728
- alive.current = true
729
- if (!enabled) return () => { alive.current = false; ++version.current }
730
- void run()
731
- const timer = setInterval(() => {
732
- if (typeof document === 'undefined' || document.visibilityState !== 'hidden') void run()
733
- }, 2000)
734
- return () => { alive.current = false; ++version.current; clearInterval(timer) }
735
- }, [run, enabled])
736
- return { account, pending, run }
892
+ }
893
+ return h('section', { style: PANEL, 'aria-label': '套餐额度查询' },
894
+ h(SectionHeading, { title: '套餐额度', description: (source === PROXY_PROVIDER ? '代理账户' : 'ACP / 官方 CLI 账户') + ' · 点击拉取,查看当前模式的剩余额度。',
895
+ action: h('button', { type: 'button', style: ACTION, disabled: !readQuota || refreshing, 'aria-busy': refreshing || undefined, onClick: load }, refreshing ? '正在拉取…' : '拉取额度') }),
896
+ data ? QuotaDetails({ data, refreshing, source }) : h('p', { role: 'status', style: { margin: 0, fontSize: '12px', lineHeight: 1.6, opacity: .65 } }, refreshing ? '正在读取套餐额度…' : '尚未拉取。打开设置页不会自动查询额度。'))
737
897
  }
738
- function AgYQuotaPill({ store, readQuota, control }) {
898
+ function QuotaDetails({ data, refreshing, load, model, source, onClose }) {
899
+ return h('div', { style: { display: 'grid', gap: 0 } },
900
+ h('div', { style: { display: 'flex', gap: '8px', alignItems: 'center', paddingBottom: '9px', borderBottom: '1px solid rgba(128,128,128,.16)' } },
901
+ h('strong', { style: { fontSize: '14px', flex: 1 } }, 'Antigravity · 套餐额度'),
902
+ data?.plan ? h('span', { style: { fontSize: '11px', padding: '2px 8px', background: TRACK, borderRadius: '99px' } }, data.plan) : null,
903
+ onClose ? h('button', { type: 'button', 'aria-label': '关闭额度详情', onClick: onClose, style: { ...ACTION, padding: '2px 7px', minHeight: '26px', borderColor: 'transparent' } }, '×') : null),
904
+ data?.stale ? h('p', { role: 'status', style: { margin: '8px 0', padding: '6px 8px', borderRadius: '7px', background: 'rgba(217,155,40,.10)', color: '#d99b28', fontSize: '11px' } }, '缓存数据 · ' + (data.staleReason ?? '刷新失败')) : null,
905
+ data?.status === 'error' ? h('p', { role: 'alert', style: { margin: '8px 0', color: '#ef4444', fontSize: '11px', overflowWrap: 'anywhere' } }, data.error?.message ?? data.error?.text ?? '额度读取失败') : null,
906
+ ...quotaGroups(data).map((group) => h('section', { key: group.key, style: { padding: '9px 0', borderBottom: '1px solid rgba(128,128,128,.14)' }, 'aria-label': group.name + '额度' },
907
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: '7px', marginBottom: '7px' } },
908
+ h('strong', { style: { fontSize: '12px' } }, group.name),
909
+ group.key === modelGroup(model) ? h('span', { style: { fontSize: '9px', padding: '1px 5px', borderRadius: '5px', color: '#25b879', background: 'rgba(37,184,121,.12)' } }, '当前模型') : null),
910
+ group.buckets.length ? h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(120px, 1fr))', gap: '9px 12px' } }, ...group.buckets.map((bucket, index) => {
911
+ const pct = percentage(bucket)
912
+ return h('div', { key: index, style: { minWidth: 0 } },
913
+ h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: '8px', marginBottom: '4px' } },
914
+ h('span', { style: { fontSize: '11px', opacity: .7 } }, windowName(bucket)),
915
+ h('strong', { style: { fontSize: bucket.disabled ? '12px' : '15px', fontVariantNumeric: 'tabular-nums' } }, bucket.disabled ? '已停用' : pct === undefined ? '未知' : Math.round(pct) + '%')),
916
+ remainingBar(bucket, false, data?.stale, group.name + ' ' + windowName(bucket) + '剩余额度'),
917
+ h('div', { title: bucket.disabled ? bucket.description : bucket.resetTime, style: { display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden', fontSize: '10px', lineHeight: 1.35, opacity: .58, marginTop: '4px' } }, bucket.disabled ? bucket.description ?? '此窗口不可用' : resetCountdown(bucket.resetTime)))
918
+ })) : h('span', { style: { opacity: .55, fontSize: '11px' } }, data ? '未提供额度数据' : '正在读取…'))),
919
+ CreditsDetails({ credits: data?.credits, pending: !data }),
920
+ source === PROXY_PROVIDER ? h('small', { style: { opacity: .55, fontSize: '10px', lineHeight: 1.4, padding: '7px 0', borderBottom: '1px solid rgba(128,128,128,.14)' } }, '套餐额度属于代理账户;AI Credits 来自官方 CLI 账户,可能不是同一账户。') : null,
921
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px', paddingTop: '8px' } },
922
+ h('small', { style: { flex: 1, opacity: .5, minWidth: 0, fontSize: '10px' } }, data?.updatedAt ? '更新于 ' + new Date(data.updatedAt).toLocaleString() : '尚无成功快照'),
923
+ load ? h('button', { type: 'button', disabled: refreshing, onClick: () => load(true), style: { ...ACTION, minHeight: '27px', padding: '3px 9px', fontSize: '11px' } }, refreshing ? '刷新中…' : '刷新额度') : null))
924
+ }
925
+ /** 紧凑余量条与单词状态的交互参考 dsh-subusage;取数仍由本插件负责。 */
926
+ function quotaPillStatus(data, buckets) {
927
+ if (!data) return { short: '', icon: '…', color: '#8b909a' }
928
+ if (data.status === 'error') return { short: '失败', icon: '!', color: '#e5534b', tint: true }
929
+ if (data.stale) return { short: '缓存', icon: '!', color: '#d99b28', tint: true }
930
+ const usable = buckets.filter(bucket => !bucket.disabled && percentage(bucket) !== undefined)
931
+ if (!usable.length) return { short: buckets.some(bucket => bucket.disabled) ? '受限' : '未知', icon: '?', color: '#8b909a' }
932
+ const minimum = Math.min(...usable.map(percentage))
933
+ if (minimum === 0) return { short: '已用尽', color: '#e5534b', tint: true }
934
+ if (minimum <= 10) return { short: '紧张', color: '#e5534b', tint: true }
935
+ if (minimum <= 25) return { short: '偏少', color: '#d99b28', tint: true }
936
+ return { short: '', color: '#25b879' }
937
+ }
938
+ function AgYQuotaPill({ store, readQuota, proxyControl }) {
739
939
  const state = React.useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot)
740
- const enabled = state?.current?.provider === PROVIDER
940
+ const isAcp = state?.current?.provider === PROVIDER
941
+ const isProxy = state?.current?.provider === PROXY_PROVIDER
942
+ const enabled = isAcp || isProxy
741
943
  const model = state?.current?.model
742
- const quota = useQuota(readQuota, enabled)
944
+ const quota = useQuota(readQuota, enabled, state?.current?.provider)
743
945
  const [expanded, setExpanded] = React.useState(false)
744
946
  const [position, setPosition] = React.useState(null)
745
947
  const root = React.useRef(null)
746
948
  const button = React.useRef(null)
747
- const warmup = useAcpWarmup(control, enabled && Boolean(control))
949
+ React.useEffect(() => {
950
+ if (!isProxy || !proxyControl) return
951
+ let active = true
952
+ let pending = false
953
+ const check = async () => {
954
+ if (pending) return
955
+ pending = true
956
+ try {
957
+ const value = unwrap(await proxyControl('account', { action: 'status' }))
958
+ if (active) readQuota?.observeProxyAccount?.(value.snapshot)
959
+ } catch { /* 账户轮询失败不覆盖额度查询自身的结果。 */ }
960
+ finally { pending = false }
961
+ }
962
+ void check()
963
+ const timer = setInterval(() => {
964
+ if (typeof document === 'undefined' || document.visibilityState !== 'hidden') void check()
965
+ }, 3000)
966
+ return () => { active = false; clearInterval(timer) }
967
+ }, [isProxy, proxyControl, readQuota])
748
968
  React.useEffect(() => { setExpanded(false) }, [enabled])
749
969
  React.useEffect(() => {
750
970
  if (!expanded) { setPosition(null); return undefined }
@@ -780,30 +1000,20 @@ window.__ModuleLoader__.load({
780
1000
  const group = groups.find((g) => g.key === family && (family !== 'argon' || g.buckets.length > 0))
781
1001
  ?? (family === 'argon' ? groups.find((g) => g.key === 'gemini') : undefined)
782
1002
  const buckets = group?.buckets ?? []
1003
+ const pillStatus = quotaPillStatus(quota.data, buckets)
1004
+ const miniBuckets = buckets.slice(0, 5)
1005
+ const miniHeight = miniBuckets.length <= 1 ? 6 : miniBuckets.length === 2 ? 5 : miniBuckets.length === 3 ? 4 : miniBuckets.length === 4 ? 3 : 2
1006
+ const modeTag = isProxy ? '代理' : 'ACP'
783
1007
  const label = quota.data?.status === 'error' ? '额度读取失败' : !quota.data ? '正在读取额度' : !group ? '额度未匹配,请选择具体模型' : !buckets.length ? '额度未知' : buckets.map((b) => windowName(b) + ' ' + (b.disabled ? '已停用' : percentage(b) === undefined ? '额度未知' : Math.round(percentage(b)) + '% 剩余')).join(' · ')
784
- const title = String(model ?? '未选择模型') + ' · ' + label + (quota.data?.stale ? ' · 缓存数据' : '')
785
- const runtime = warmup.account?.runtime
786
- const warming = warmup.pending || ['connecting', 'cancelling'].includes(warmup.account?.state)
787
- || ['binary', 'credentials', 'spawn', 'initialize', 'authenticate'].includes(runtime?.phase)
788
- const working = !warming && runtimeBusy(runtime)
789
- const ready = runtime?.phase === 'ready'
790
- const failed = warmup.account?.state === 'error' || runtime?.phase === 'error'
791
- const warmupLabel = warming ? '预热中…' : runtime?.queue?.waiting > 0 ? '排队 ' + runtime.queue.waiting : working ? '运行中' : ready ? '已预热' : failed ? '重试预热' : '预热'
792
- const warmupTitle = runtime ? (RUNTIME_PHASES[runtime.phase] ?? runtime.phase) + ' · ' + duration(runtime.phaseElapsedMs)
793
- + (runtime.queue ? ' · 运行 ' + runtime.queue.active + ' · 等待工具 ' + runtime.queue.parked + ' · 排队 ' + runtime.queue.waiting : '')
794
- + (runtime.error ? ' · ' + runtime.error : '') : warmup.account?.message ?? '提前启动 ACP 连接,不发送模型消息'
795
- return h('span', { ref: root, style: { position: 'relative', display: 'inline-flex', alignItems: 'center', gap: '6px' } },
1008
+ const title = modeTag + ' · ' + String(model ?? '未选择模型') + ' · ' + label + (quota.data?.stale ? ' · 缓存数据' : '')
1009
+ return h('span', { ref: root, className: 'antigravity-usage-pill', 'data-expanded': String(expanded), style: { position: 'relative', display: 'inline-flex', alignItems: 'center' } },
796
1010
  h('button', { ref: button, type: 'button', title, 'aria-label': title, 'aria-expanded': expanded, 'aria-haspopup': 'dialog', onClick: () => setExpanded((value) => !value),
797
- style: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', padding: '6px 10px', minHeight: '26px', borderRadius: '99px', border: '1px solid rgba(128,128,128,.28)', background: 'transparent', color: 'inherit', cursor: 'pointer' } },
798
- buckets.length ? h('span', { style: { display: 'grid', gap: '3px' } }, ...buckets.map((b, i) => h('span', { key: i }, remainingBar(b, true, quota.data?.stale, group.name + ' ' + windowName(b) + '剩余额度')))) : h('span', { 'aria-hidden': true }, quota.data?.status === 'error' ? '!' : quota.data ? '?' : '…')),
799
- control ? h('button', {
800
- type: 'button', title: warmupTitle, 'aria-label': '预热 ACP', 'aria-busy': warming,
801
- disabled: Boolean(warming || working || ready), onClick: () => warmup.run('warmup'),
802
- style: { ...ACTION, padding: '4px 9px', minHeight: '26px', borderRadius: '99px', fontSize: '11px', whiteSpace: 'nowrap',
803
- color: ready ? '#25b879' : failed ? '#e5534b' : 'inherit', cursor: warming || working || ready ? 'default' : 'pointer' },
804
- }, warmupLabel) : null,
805
- expanded ? h('div', { role: 'dialog', 'aria-label': 'Antigravity 额度详情', style: { position: 'absolute', bottom: 'calc(100% + 8px)', right: 0, zIndex: 50, width: 'min(340px, calc(100vw - 48px))', maxHeight: 'min(75vh, calc(100vh - 72px))', overflowY: 'auto', boxSizing: 'border-box', overflowWrap: 'anywhere', padding: '16px', borderRadius: '16px', background: 'var(--dsh-surface, Canvas)', color: 'var(--dsh-text, CanvasText)', border: '1px solid rgba(128,128,128,.25)', boxShadow: '0 14px 40px rgba(0,0,0,.22)', fontSize: '12px', ...position } },
806
- QuotaDetails({ ...quota, model })) : null)
1011
+ style: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: '8px', padding: pillStatus.short ? '4px 11px 4px 9px' : '5px 10px', minHeight: '26px', boxSizing: 'border-box', borderRadius: '99px', border: '1px solid ' + (pillStatus.tint ? pillStatus.color + '73' : 'rgba(128,128,128,.3)'), background: pillStatus.tint ? pillStatus.color + '1a' : 'transparent', color: 'inherit', cursor: 'pointer', font: 'inherit', fontSize: '12px', fontWeight: 600, whiteSpace: 'nowrap', maxWidth: 'min(46vw, 240px)' } },
1012
+ miniBuckets.length ? h('span', { style: { display: 'grid', gap: miniBuckets.length <= 2 ? '3px' : '2px', opacity: quota.data?.stale ? .5 : 1 } }, ...miniBuckets.map((b, i) => h('span', { key: i }, remainingBar(b, true, quota.data?.stale, group.name + ' ' + windowName(b) + '剩余额度', miniHeight))))
1013
+ : h('span', { 'aria-hidden': true, style: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: '16px', height: '16px', borderRadius: '50%', background: pillStatus.color, color: '#fff', fontSize: '11px' } }, pillStatus.icon),
1014
+ pillStatus.short ? h('span', null, pillStatus.short) : null),
1015
+ expanded ? h('div', { role: 'dialog', 'aria-label': 'Antigravity 额度详情', style: { position: 'absolute', bottom: 'calc(100% + 8px)', right: 0, zIndex: 50, width: 'min(340px, calc(100vw - 24px))', maxHeight: 'min(70vh, calc(100vh - 72px))', overflowY: 'auto', boxSizing: 'border-box', overflowWrap: 'anywhere', padding: '12px', borderRadius: '12px', background: 'var(--dsh-surface, Canvas)', color: 'var(--dsh-text, CanvasText)', border: '1px solid rgba(128,128,128,.28)', boxShadow: '0 12px 32px rgba(0,0,0,.28)', fontSize: '12px', ...position } },
1016
+ QuotaDetails({ ...quota, model, source: state?.current?.provider, onClose: () => { setExpanded(false); button.current?.focus?.() } })) : null)
807
1017
  }
808
1018
 
809
1019
  /** 给 promise 加超时:Host 侧异常时界面也要落到可读错误,而不是永远 pending。 */
@@ -820,6 +1030,9 @@ window.__ModuleLoader__.load({
820
1030
  /** 组装额度读取器:remote.agyQuota 由 Host 注册后才出现,短轮询等待就绪。 */
821
1031
  function makeQuotaReader(scope) {
822
1032
  const listeners = new Set()
1033
+ const epochs = new Map()
1034
+ let proxyAccountKey
1035
+ let mode
823
1036
  const read = async (force) => {
824
1037
  const deadline = Date.now() + 20_000
825
1038
  for (;;) {
@@ -843,12 +1056,36 @@ window.__ModuleLoader__.load({
843
1056
  await new Promise((resolve) => setTimeout(resolve, 500))
844
1057
  }
845
1058
  }
846
- const reader = async (force) => {
1059
+ const reader = async (force, source) => {
1060
+ const epoch = epochs.get(source) ?? 0
847
1061
  const value = unwrap(await read(force))
848
- for (const listener of listeners) listener(value)
1062
+ if (epoch !== (epochs.get(source) ?? 0)) throw new Error('账户已变化,请重新拉取额度')
1063
+ for (const listener of listeners) {
1064
+ if (source === undefined) listener(value)
1065
+ else listener(value, source)
1066
+ }
849
1067
  return value
850
1068
  }
851
1069
  reader.subscribe = (listener) => { listeners.add(listener); return () => listeners.delete(listener) }
1070
+ reader.invalidate = (source) => {
1071
+ epochs.set(source, (epochs.get(source) ?? 0) + 1)
1072
+ for (const listener of listeners) listener(null, source)
1073
+ }
1074
+ reader.observeProxyAccount = (snapshot) => {
1075
+ if (!snapshot) return
1076
+ const key = JSON.stringify([snapshot.revision, snapshot.phase === 'signed-in', snapshot.email])
1077
+ if (proxyAccountKey !== undefined && key !== proxyAccountKey) {
1078
+ proxyAccountKey = key
1079
+ reader.invalidate(PROXY_PROVIDER)
1080
+ } else proxyAccountKey = key
1081
+ }
1082
+ reader.observeMode = (source) => {
1083
+ if (mode !== undefined && mode !== source) {
1084
+ mode = source
1085
+ reader.invalidate(PROVIDER)
1086
+ reader.invalidate(PROXY_PROVIDER)
1087
+ } else mode = source
1088
+ }
852
1089
  return reader
853
1090
  }
854
1091
 
@@ -869,6 +1106,22 @@ window.__ModuleLoader__.load({
869
1106
  }
870
1107
  }
871
1108
 
1109
+ /** 代理账户控制读取器:remote.agyProxyAccount 由 Host 的代理行注册。 */
1110
+ function makeProxyControlReader(scope) {
1111
+ return async (method, request) => {
1112
+ if (method !== 'account') throw new Error('不支持的代理账户操作')
1113
+ const deadline = Date.now() + 20_000
1114
+ for (;;) {
1115
+ const service = scope.get('remote.agyProxyAccount')
1116
+ if (typeof service?.account === 'function') {
1117
+ return await withTimeout(service.account(request), 60_000, '代理账户操作超时,请重试')
1118
+ }
1119
+ if (Date.now() >= deadline) throw new Error('代理账户服务未就绪:请确认已启用本插件的代理路由组件(antigravity-proxy-router)')
1120
+ await new Promise((resolve) => setTimeout(resolve, 500))
1121
+ }
1122
+ }
1123
+ }
1124
+
872
1125
  /**
873
1126
  * 客户端侧的额度 remote 声明。
874
1127
  *
@@ -882,12 +1135,25 @@ window.__ModuleLoader__.load({
882
1135
  */
883
1136
  const AGY_QUOTA_PACKAGE = 'dsh-bundle-antigravity-host'
884
1137
  const AGY_QUOTA_SERVICE = 'agyQuota'
1138
+ const AGY_PROXY_ACCOUNT_SERVICE = 'agyProxyAccount'
885
1139
 
886
1140
  function strictCodec(typeSymbol, parse) {
887
1141
  const schema = { parse }
888
1142
  return { mode: 'strict', typeSymbol, create: () => schema }
889
1143
  }
890
1144
 
1145
+ function parseProxyAccountRequest(value) {
1146
+ if (value === undefined || value === null) return {}
1147
+ if (typeof value !== 'object' || Array.isArray(value)) throw new TypeError('Invalid AgYProxyAccountRequest')
1148
+ if (value.action !== undefined && !['status', 'login', 'cancel', 'logout'].includes(value.action)) throw new TypeError('Invalid AgYProxyAccountRequest.action')
1149
+ return value
1150
+ }
1151
+
1152
+ function parseProxyAccountResult(value) {
1153
+ if (!value || typeof value !== 'object' || typeof value.message !== 'string' || !value.snapshot || typeof value.snapshot !== 'object') throw new TypeError('Invalid AgYProxyAccountResult')
1154
+ return value
1155
+ }
1156
+
891
1157
  function parseQuotaRequest(value) {
892
1158
  if (value === undefined || value === null) return undefined
893
1159
  if (typeof value !== 'object' || Array.isArray(value)) throw new TypeError('Invalid AgYQuotaRequest')
@@ -942,18 +1208,44 @@ window.__ModuleLoader__.load({
942
1208
  ],
943
1209
  }
944
1210
 
1211
+ const AGY_PROXY_REMOTE = {
1212
+ package: AGY_QUOTA_PACKAGE,
1213
+ descriptors: [
1214
+ {
1215
+ id: `${AGY_QUOTA_PACKAGE}#${AGY_PROXY_ACCOUNT_SERVICE}/account`,
1216
+ service: AGY_PROXY_ACCOUNT_SERVICE,
1217
+ namespace: AGY_PROXY_ACCOUNT_SERVICE,
1218
+ method: 'account',
1219
+ invocation: { kind: 'direct' },
1220
+ parameters: [{ name: 'request', wire: 'request', source: 'json', acceptsUndefined: true, codec: strictCodec(`${AGY_QUOTA_PACKAGE}#AgYProxyAccountRequest`, parseProxyAccountRequest) }],
1221
+ result: strictCodec(`${AGY_QUOTA_PACKAGE}#AgYProxyAccountResult`, parseProxyAccountResult),
1222
+ },
1223
+ ],
1224
+ }
1225
+
945
1226
  return {
946
1227
  name: 'dsh-bundle-antigravity-host',
947
1228
  // remote.settings / remote.llm 必须逐个声明:remote 各命名空间是独立服务,
948
1229
  // 漏声明会在读取属性时抛「without inject」并击穿 settings.section 卡片。
949
1230
  inject: ['slots', 'remote', 'remote.settings', 'remote.llm'],
950
1231
  apply(ctx) {
1232
+ ctx.effect(() => {
1233
+ if (typeof document === 'undefined' || !document.head || !document.createElement) return () => {}
1234
+ const style = document.createElement('style')
1235
+ style.textContent = '.antigravity-usage-pill>button:hover{filter:brightness(1.08)}.antigravity-usage-pill button:focus-visible,.antigravity-settings-card button:focus-visible,.antigravity-settings-card a:focus-visible{outline:2px solid #4a90e2;outline-offset:3px}.antigravity-settings-card button:disabled{cursor:not-allowed;opacity:.55}'
1236
+ document.head.appendChild(style)
1237
+ return () => style.remove()
1238
+ }, 'antigravity: scoped control styles')
951
1239
  const readQuota = makeQuotaReader(ctx)
952
1240
  const control = makeControlReader(ctx)
1241
+ const proxyControl = makeProxyControlReader(ctx)
953
1242
  // 挂载本包贡献的 remote(幂等,随 fiber 一起 dispose)。挂载失败不影响其它卡片:
954
1243
  // 药丸会自行超时并显示「额度服务未就绪」的原因。
955
1244
  try {
956
- const mounted = ctx.remote.$mount(AGY_QUOTA_REMOTE)
1245
+ const mounted = ctx.remote.$mount({
1246
+ package: AGY_QUOTA_PACKAGE,
1247
+ descriptors: [...AGY_QUOTA_REMOTE.descriptors, ...AGY_PROXY_REMOTE.descriptors],
1248
+ })
957
1249
  ctx.effect(() => mounted, 'antigravity-acp: mount quota remote')
958
1250
  } catch (error) {
959
1251
  console.error('[dsh-bundle-antigravity-host] 额度 remote 客户端挂载失败:', error)
@@ -967,7 +1259,7 @@ window.__ModuleLoader__.load({
967
1259
  id: 'antigravity-acp',
968
1260
  order: 42,
969
1261
  label: () => 'Antigravity ACP',
970
- inject: () => ({ remote: ctx.remote, control }),
1262
+ inject: () => ({ remote: ctx.remote, control, proxyControl, readQuota }),
971
1263
  },
972
1264
  AntigravitySettingsCard,
973
1265
  ),
@@ -995,7 +1287,7 @@ window.__ModuleLoader__.load({
995
1287
  inject: (sessionId) => ({
996
1288
  store: scope.modelDirectories.directoryFor(sessionId).store,
997
1289
  readQuota,
998
- control,
1290
+ proxyControl,
999
1291
  }),
1000
1292
  },
1001
1293
  AgYQuotaPill,