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.
- package/README.md +13 -7
- package/cordis.patch.yml +7 -2
- package/dist/adapter/antigravity-adapter.js +23 -5
- package/dist/adapter/antigravity-adapter.js.map +1 -1
- package/dist/bridge/mcp-bridge.d.ts +9 -0
- package/dist/bridge/mcp-bridge.js +19 -2
- package/dist/bridge/mcp-bridge.js.map +1 -1
- package/dist/client.js +435 -143
- package/dist/index.js +126 -10
- package/dist/index.js.map +1 -1
- package/dist/pool/pooled-adapter.js +6 -5
- package/dist/pool/pooled-adapter.js.map +1 -1
- package/dist/protocol/agent-link.d.ts +13 -2
- package/dist/protocol/agent-link.js +59 -5
- package/dist/protocol/agent-link.js.map +1 -1
- package/dist/proxy/account-remote.d.ts +34 -0
- package/dist/proxy/account-remote.js +92 -0
- package/dist/proxy/account-remote.js.map +1 -0
- package/dist/proxy/account-service.d.ts +78 -0
- package/dist/proxy/account-service.js +359 -0
- package/dist/proxy/account-service.js.map +1 -0
- package/dist/proxy/account-store.d.ts +56 -0
- package/dist/proxy/account-store.js +182 -0
- package/dist/proxy/account-store.js.map +1 -0
- package/dist/proxy/adapter.d.ts +72 -0
- package/dist/proxy/adapter.js +812 -0
- package/dist/proxy/adapter.js.map +1 -0
- package/dist/proxy/image.d.ts +27 -0
- package/dist/proxy/image.js +82 -0
- package/dist/proxy/image.js.map +1 -0
- package/dist/proxy/index.d.ts +19 -0
- package/dist/proxy/index.js +118 -0
- package/dist/proxy/index.js.map +1 -0
- package/dist/proxy/mode-bus.d.ts +50 -0
- package/dist/proxy/mode-bus.js +38 -0
- package/dist/proxy/mode-bus.js.map +1 -0
- package/dist/proxy/oauth.d.ts +87 -0
- package/dist/proxy/oauth.js +430 -0
- package/dist/proxy/oauth.js.map +1 -0
- package/dist/proxy/payload.d.ts +67 -0
- package/dist/proxy/payload.js +540 -0
- package/dist/proxy/payload.js.map +1 -0
- package/dist/proxy/quota-source.d.ts +15 -0
- package/dist/proxy/quota-source.js +114 -0
- package/dist/proxy/quota-source.js.map +1 -0
- package/dist/proxy/replay.d.ts +34 -0
- package/dist/proxy/replay.js +238 -0
- package/dist/proxy/replay.js.map +1 -0
- package/dist/proxy/transport.d.ts +55 -0
- package/dist/proxy/transport.js +723 -0
- package/dist/proxy/transport.js.map +1 -0
- package/dist/proxy/wire-identity.d.ts +25 -0
- package/dist/proxy/wire-identity.js +124 -0
- package/dist/proxy/wire-identity.js.map +1 -0
- package/dist/quota/control.d.ts +10 -0
- package/dist/quota/control.js +17 -2
- package/dist/quota/control.js.map +1 -1
- package/dist/quota/quota-remote.d.ts +18 -0
- package/dist/quota/quota-remote.js +50 -9
- package/dist/quota/quota-remote.js.map +1 -1
- package/dist/runtime/binary.d.ts +20 -1
- package/dist/runtime/binary.js +58 -6
- package/dist/runtime/binary.js.map +1 -1
- package/dist/runtime/proxied-fetch.js +9 -1
- package/dist/runtime/proxied-fetch.js.map +1 -1
- package/dist/runtime/proxy-env.d.ts +2 -0
- package/dist/runtime/proxy-env.js +8 -0
- package/dist/runtime/proxy-env.js.map +1 -1
- package/dist/runtime/quota.d.ts +19 -1
- package/dist/runtime/quota.js +97 -22
- package/dist/runtime/quota.js.map +1 -1
- package/dist/runtime/session-map.d.ts +7 -0
- package/dist/runtime/session-map.js +11 -0
- package/dist/runtime/session-map.js.map +1 -1
- package/dist/settings.d.ts +11 -1
- package/dist/settings.js +3 -1
- package/dist/settings.js.map +1 -1
- package/docs/changelog.md +61 -1
- package/docs/configuration.md +24 -6
- package/docs/development.md +21 -9
- package/docs/reference.md +34 -7
- 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: '
|
|
31
|
-
{ key: '
|
|
32
|
-
{ key: '
|
|
33
|
-
{ key: '
|
|
34
|
-
{ key: 'agyCliPath', group: 'network', label: '官方 CLI 路径(可选)', placeholder: '留空自动查找 agy', hint: '
|
|
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',
|
|
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 ? '目录已更新;不会自动替换当前会话的已选模型。' : '
|
|
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
|
|
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 ===
|
|
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) =>
|
|
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
|
-
|
|
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:
|
|
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 } }, '
|
|
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('
|
|
515
|
-
h('div', { style: { flex:
|
|
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: '
|
|
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(
|
|
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': '
|
|
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
|
-
|
|
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 ? '
|
|
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) => {
|
|
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:
|
|
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',
|
|
675
|
-
h('strong', { style: { fontSize: valid ? '
|
|
676
|
-
credits?.planName ? h('div', { style: {
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
credits?.
|
|
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
|
|
707
|
-
const [
|
|
708
|
-
const [
|
|
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
|
|
712
|
-
const
|
|
713
|
-
|
|
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
|
-
|
|
716
|
-
if (action === 'warmup') setPending(true)
|
|
879
|
+
setRefreshing(true)
|
|
717
880
|
try {
|
|
718
|
-
const value = unwrap(await
|
|
719
|
-
if (
|
|
881
|
+
const value = unwrap(await readQuota(true, source))
|
|
882
|
+
if (current === version.current) setData(value)
|
|
720
883
|
} catch (error) {
|
|
721
|
-
if (
|
|
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
|
-
|
|
724
|
-
if (alive.current && action === 'warmup') setPending(false)
|
|
890
|
+
if (current === version.current) { pending.current = false; setRefreshing(false) }
|
|
725
891
|
}
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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: '
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
1290
|
+
proxyControl,
|
|
999
1291
|
}),
|
|
1000
1292
|
},
|
|
1001
1293
|
AgYQuotaPill,
|