dsh-bundle-antigravity-host 0.1.31 → 0.1.36
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 +17 -9
- package/cordis.patch.yml +7 -2
- package/dist/adapter/antigravity-adapter.js +13 -3
- package/dist/adapter/antigravity-adapter.js.map +1 -1
- package/dist/adapter/session-reuse.d.ts +4 -1
- package/dist/adapter/session-reuse.js +3 -1
- package/dist/adapter/session-reuse.js.map +1 -1
- package/dist/client.js +563 -245
- package/dist/index.js +92 -7
- package/dist/index.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 +364 -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 +115 -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 +36 -3
- package/dist/quota/quota-remote.js +93 -12
- package/dist/quota/quota-remote.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 +3 -0
- package/dist/runtime/quota.js.map +1 -1
- package/dist/settings.d.ts +21 -1
- package/dist/settings.js +5 -1
- package/dist/settings.js.map +1 -1
- package/docs/changelog.md +53 -0
- package/docs/configuration.md +23 -6
- package/docs/development.md +19 -8
- package/docs/reference.md +33 -7
- package/package.json +4 -2
package/dist/client.js
CHANGED
|
@@ -23,19 +23,32 @@ 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: '
|
|
35
|
-
{ key: '
|
|
36
|
-
{ key: '
|
|
37
|
-
{ key: '
|
|
38
|
-
{ key: '
|
|
31
|
+
{ key: 'proxyMode', group: 'mode', checkbox: true, label: '使用代理模式', hint: '直连上游;需启用代理路由并连接代理账户。' },
|
|
32
|
+
{ key: 'cliQuotaEnabled', group: 'quota', checkbox: true, label: '使用 agy CLI 补充额度', hint: '默认不启动 agy;开启后才读取 AI Credits。' },
|
|
33
|
+
{ key: 'autoWarmup', group: 'general', checkbox: true, defaultChecked: true, acpOnly: true, label: '启动时自动预热', hint: '不发送消息;下次启用插件时生效。' },
|
|
34
|
+
{ key: 'reuseSessions', group: 'general', checkbox: true, defaultChecked: true, acpOnly: true, label: '复用 ACP 会话', hint: '只发送新增消息;上下文不一致时自动重建。' },
|
|
35
|
+
{ key: 'proxyUrl', group: 'network', label: '出网代理', placeholder: 'http://127.0.0.1:7890', hint: '留空继承宿主代理环境。' },
|
|
36
|
+
{ key: 'agyCliPath', group: 'network', label: '官方 CLI 路径', placeholder: '留空自动查找 agy', hint: '仅用于模型目录和可选的 CLI 额度查询。' },
|
|
37
|
+
{ key: 'resumeSessions', group: 'advanced', checkbox: true, acpOnly: true, label: '重启后恢复 ACP 会话', hint: '需开启会话复用;恢复失败时自动重建。' },
|
|
38
|
+
{ key: 'keepProcessAfterCancel', group: 'advanced', checkbox: true, acpOnly: true, label: '取消后保留进程', hint: '仅在官方进程确认停止后保留。' },
|
|
39
|
+
{ key: 'cancelGraceMs', group: 'advanced', numeric: true, defaultValue: 30000, min: 0, max: 60000, acpOnly: true, label: '取消确认宽限(毫秒)', hint: '0 表示立即回收。' },
|
|
40
|
+
{ key: 'idleRecycleMs', group: 'advanced', numeric: true, defaultValue: 300000, min: 0, max: 3600000, acpOnly: true, label: '进程空闲回收(毫秒)', hint: '默认 5 分钟;0 表示禁用。' },
|
|
41
|
+
{ key: 'binaryPath', group: 'launch', acpOnly: true, label: 'ACP Server 路径', placeholder: '留空自动解析或下载', hint: '优先于环境变量与缓存。' },
|
|
42
|
+
{ key: 'autoDownload', group: 'launch', checkbox: true, defaultChecked: true, acpOnly: true, label: '自动下载 ACP Server', hint: '仅从官方 dl.google.com 下载。' },
|
|
43
|
+
{ key: 'acpVersion', group: 'launch', acpOnly: true, label: 'ACP Server 版本', placeholder: '1.4.0', hint: '程序版本,不是 ACP 协议版本。' },
|
|
44
|
+
{ key: 'extraArgs', group: 'launch', lines: true, acpOnly: true, label: 'ACP 额外启动参数', placeholder: '每行一个参数', hint: '按行原样传给官方进程。' },
|
|
45
|
+
{ key: 'maxConcurrentSessions', group: 'pool', numeric: true, defaultValue: 2, min: 1, max: 4, acpOnly: true, label: '最大 ACP 连接数', hint: '范围 1–4。' },
|
|
46
|
+
{ key: 'queueTimeoutMs', group: 'pool', numeric: true, defaultValue: 120000, min: 1000, acpOnly: true, label: '连接排队超时(毫秒)', hint: '最小 1000。' },
|
|
47
|
+
{ key: 'requestTimeoutMs', group: 'timeouts', numeric: true, defaultValue: 30000, min: 0, acpOnly: true, label: '普通请求超时(毫秒)' },
|
|
48
|
+
{ key: 'handshakeTimeoutMs', group: 'timeouts', numeric: true, defaultValue: 120000, min: 1000, acpOnly: true, label: '握手超时(毫秒)', hint: '冷启动建议至少 120000。' },
|
|
49
|
+
{ key: 'authTimeoutMs', group: 'timeouts', numeric: true, defaultValue: 300000, min: 0, acpOnly: true, label: '浏览器登录超时(毫秒)' },
|
|
50
|
+
{ key: 'promptTimeoutMs', group: 'timeouts', numeric: true, defaultValue: 1800000, min: 0, acpOnly: true, label: '单轮生成超时(毫秒)', hint: '默认 30 分钟。' },
|
|
51
|
+
{ key: 'bridgeParkSettleMs', group: 'timeouts', numeric: true, defaultValue: 50, min: 0, acpOnly: true, label: '工具调用聚合窗口(毫秒)', hint: '不是工具执行超时。' },
|
|
39
52
|
]
|
|
40
53
|
|
|
41
54
|
/** 逐模型设置:键是 DSH 选择器里的模型 ID,值是开关、默认档位、显示名与官方 ID 映射。 */
|
|
@@ -137,7 +150,7 @@ window.__ModuleLoader__.load({
|
|
|
137
150
|
: account?.runtime?.phase === 'error'
|
|
138
151
|
? account.runtime.error ?? account?.message ?? 'ACP 连接异常,请重试。'
|
|
139
152
|
: registered === false ? '请确认插件已启用,Host 与 Client 均已加载。'
|
|
140
|
-
: startupHint(account) ?? account?.message ?? '
|
|
153
|
+
: startupHint(account) ?? account?.message ?? '连接后可读取模型目录,不发送聊天消息。'
|
|
141
154
|
return { label, description, ready, busy, error, color: error ? '#d97757' : ready ? '#25b879' : busy ? '#d99b28' : '#8b909a' }
|
|
142
155
|
}
|
|
143
156
|
function StatusDot({ color }) {
|
|
@@ -146,7 +159,7 @@ window.__ModuleLoader__.load({
|
|
|
146
159
|
function StatusNotice({ status }) {
|
|
147
160
|
if (!status) return null
|
|
148
161
|
const error = status.kind === 'error'
|
|
149
|
-
return h('p', { role: error ? 'alert' : 'status', style: { margin: 0, padding: '10px 12px', borderRadius: '10px',
|
|
162
|
+
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
163
|
}
|
|
151
164
|
function RuntimeDetails({ runtime }) {
|
|
152
165
|
if (!runtime) return null
|
|
@@ -170,104 +183,208 @@ window.__ModuleLoader__.load({
|
|
|
170
183
|
runtime.error ? h('p', { role: 'alert', style: { margin: '4px 0 0', color: '#b42318', overflowWrap: 'anywhere' } }, runtime.error) : null)
|
|
171
184
|
}
|
|
172
185
|
function SectionHeading({ eyebrow, title, description, action }) {
|
|
173
|
-
return h('div', { style: { display: 'flex', alignItems: '
|
|
186
|
+
return h('div', { style: { display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '12px' } },
|
|
174
187
|
h('div', { style: { flex: 1, minWidth: 0 } },
|
|
175
|
-
eyebrow ? h('div', { style: { fontSize: '10px', fontWeight: 650, letterSpacing: '.08em', opacity: .5, marginBottom: '
|
|
188
|
+
eyebrow ? h('div', { style: { fontSize: '10px', fontWeight: 650, letterSpacing: '.08em', opacity: .5, marginBottom: '3px' } }, eyebrow) : null,
|
|
176
189
|
h('h3', { style: { fontSize: '14px', lineHeight: 1.4, fontWeight: 650, margin: 0 } }, title),
|
|
177
|
-
description ? h('p', { style: { fontSize: '
|
|
190
|
+
description ? h('p', { style: { fontSize: '11px', lineHeight: 1.5, opacity: .6, margin: '3px 0 0' } }, description) : null),
|
|
178
191
|
action ?? null)
|
|
179
192
|
}
|
|
180
193
|
function TextField({ field, value, disabled, invalid, onChange }) {
|
|
181
194
|
const inputId = 'agy-setting-' + (React.useId?.() ?? field.key) + '-' + field.key
|
|
182
|
-
return h('div', { style: { display: 'grid', gap: '
|
|
195
|
+
return h('div', { style: { display: 'grid', gap: '4px', minWidth: 0 } },
|
|
183
196
|
h('label', { htmlFor: inputId, style: { fontSize: '12px', fontWeight: 550 } }, field.label),
|
|
184
|
-
h('input', {
|
|
197
|
+
h(field.lines ? 'textarea' : 'input', {
|
|
185
198
|
id: inputId, name: field.key, type: field.numeric ? 'number' : 'text',
|
|
186
199
|
...(field.numeric ? { min: field.min, max: field.max, step: 1 } : {}),
|
|
200
|
+
...(field.lines ? { rows: 2 } : {}),
|
|
187
201
|
disabled, value: value ?? '', placeholder: field.placeholder,
|
|
188
|
-
'aria-invalid': invalid || undefined, 'aria-describedby': inputId + '-hint',
|
|
202
|
+
'aria-invalid': invalid || undefined, 'aria-describedby': field.hint ? inputId + '-hint' : undefined,
|
|
189
203
|
style: { ...ACTION, cursor: disabled ? 'not-allowed' : 'text', width: '100%', boxSizing: 'border-box', background: 'rgba(128,128,128,.035)', opacity: disabled ? .6 : 1 },
|
|
190
204
|
onChange: (event) => onChange(field.numeric && event.target.value !== '' ? Number(event.target.value) : event.target.value),
|
|
191
205
|
}),
|
|
192
|
-
h('small', { id: inputId + '-hint', style: { fontSize: '
|
|
206
|
+
field.hint ? h('small', { id: inputId + '-hint', style: { fontSize: '10px', lineHeight: 1.45, opacity: .58 } }, field.hint) : null)
|
|
193
207
|
}
|
|
194
208
|
function SwitchField({ field, checked, disabled, onChange }) {
|
|
195
209
|
return h('label', { style: { display: 'flex', alignItems: 'flex-start', gap: '10px', cursor: disabled ? 'default' : 'pointer', padding: '2px 0', opacity: disabled ? .6 : 1 } },
|
|
196
|
-
h('input', { name: field.key, type: 'checkbox', checked: checked === true, disabled, onChange: (event) => onChange(event.target.checked), style: { width: '16px', height: '16px', flex: '0 0 auto', margin: '
|
|
197
|
-
h('span', { style: { display: 'grid', gap: '
|
|
210
|
+
h('input', { name: field.key, type: 'checkbox', checked: checked === true, disabled, onChange: (event) => onChange(event.target.checked), style: { width: '16px', height: '16px', flex: '0 0 auto', margin: '1px 0 0', accentColor: '#177c55', cursor: disabled ? 'not-allowed' : 'pointer' } }),
|
|
211
|
+
h('span', { style: { display: 'grid', gap: '2px', minWidth: 0 } },
|
|
198
212
|
h('span', { style: { fontSize: '12px', fontWeight: 550 } }, field.label),
|
|
199
|
-
h('small', { style: { fontSize: '
|
|
213
|
+
field.hint ? h('small', { style: { fontSize: '10px', lineHeight: 1.45, opacity: .6 } }, field.hint) : null))
|
|
200
214
|
}
|
|
201
215
|
function SettingsFields({ fields, draft, dirtyKeys, conflicts, disabled, editField }) {
|
|
202
|
-
|
|
216
|
+
const numericGrid = fields.length > 1 && fields.every((field) => field.numeric)
|
|
217
|
+
return h('div', { style: { display: 'grid', gap: '12px', gridTemplateColumns: numericGrid ? 'repeat(auto-fit, minmax(180px, 1fr))' : undefined } }, ...fields.map((field) => {
|
|
203
218
|
const onChange = (value) => editField(field.key, value)
|
|
204
219
|
return field.checkbox
|
|
205
220
|
? h(SwitchField, { key: field.key, field, checked: draft[field.key], disabled, onChange })
|
|
206
221
|
: h(TextField, { key: field.key, field, value: draft[field.key], disabled, invalid: dirtyKeys.includes(field.key) && field.numeric && !Number.isSafeInteger(draft[field.key]), onChange })
|
|
207
222
|
}))
|
|
208
223
|
}
|
|
224
|
+
function SettingsGroup({ title, fields, fieldProps, open = false }) {
|
|
225
|
+
return h('details', { style: PANEL, 'aria-label': title + '设置', open: open || undefined },
|
|
226
|
+
h('summary', { style: SUMMARY },
|
|
227
|
+
h('span', { style: { fontSize: '12px', fontWeight: 650 } }, title),
|
|
228
|
+
h('small', { style: { fontSize: '10px', opacity: .55 } }, fields.length + ' 项')),
|
|
229
|
+
h('div', { style: { marginTop: '14px', paddingTop: '12px', borderTop: '1px solid rgba(128,128,128,.12)' } },
|
|
230
|
+
h(SettingsFields, { ...fieldProps, fields })))
|
|
231
|
+
}
|
|
209
232
|
|
|
210
|
-
/**
|
|
233
|
+
/** 紧凑列表行;完整 ID、输入能力、改名和映射集中在单条详情里。 */
|
|
211
234
|
function ModelOverrideRow({ row, override, disabled, onEdit }) {
|
|
212
235
|
const enabled = override.enabled !== false
|
|
213
236
|
const inputId = 'agy-model-' + row.id.replace(/[^\w.-]/g, '_')
|
|
214
237
|
const labelStyle = { fontSize: '11px', opacity: .6 }
|
|
215
238
|
const inputStyle = { ...ACTION, width: '100%', minWidth: 0, boxSizing: 'border-box', background: 'rgba(128,128,128,.035)', cursor: disabled ? 'not-allowed' : 'text' }
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
239
|
+
const displayName = override.name?.trim() || row.name
|
|
240
|
+
return h('div', { role: 'listitem', style: { padding: '9px 0', borderBottom: '1px solid rgba(128,128,128,.12)', opacity: enabled ? 1 : .58 } },
|
|
241
|
+
h('div', { style: { display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) auto', alignItems: 'center', gap: '8px' } },
|
|
242
|
+
h('label', { title: displayName, style: { display: 'flex', alignItems: 'center', gap: '8px', minWidth: 0, cursor: disabled ? 'default' : 'pointer' } },
|
|
219
243
|
h('input', { name: 'model-enabled-' + row.id, type: 'checkbox', checked: enabled, disabled,
|
|
220
244
|
onChange: (event) => onEdit(row.id, { enabled: event.target.checked }),
|
|
221
|
-
style: { width: '15px', height: '15px', flex: '0 0 auto', margin:
|
|
222
|
-
h('span', { style: {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
row.inCatalog
|
|
226
|
-
? (Array.isArray(row.inputModalities) && row.inputModalities.length
|
|
227
|
-
? h('span', { style: { flex: '0 0 auto', fontSize: '10px', padding: '2px 7px', borderRadius: '99px', background: TRACK } }, row.inputModalities.map((modality) => MODALITY_LABELS[modality] ?? modality).join('、'))
|
|
228
|
-
: null)
|
|
229
|
-
: h('span', { title: '该 ID 来自已保存的设置,当前目录里没有;仍然可以修改或清除', style: { flex: '0 0 auto', fontSize: '10px', padding: '2px 7px', borderRadius: '99px', color: '#946200', background: 'rgba(217,155,40,.12)' } }, '不在当前目录')),
|
|
230
|
-
h('div', { style: { display: 'grid', gap: '6px', gridTemplateColumns: 'minmax(120px, 160px) minmax(0, 1fr)' } },
|
|
231
|
-
h('div', { style: { display: 'grid', gap: '4px', minWidth: 0 } },
|
|
232
|
-
h('label', { htmlFor: inputId + '-effort', style: labelStyle }, '默认思考档位'),
|
|
245
|
+
style: { width: '15px', height: '15px', flex: '0 0 auto', margin: 0, accentColor: '#177c55', cursor: disabled ? 'not-allowed' : 'pointer' } }),
|
|
246
|
+
h('span', { style: { minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontSize: '12px', fontWeight: 600 } }, displayName)),
|
|
247
|
+
h('label', { htmlFor: inputId + '-effort', style: { display: 'flex', alignItems: 'center', gap: '5px', fontSize: '11px' } },
|
|
248
|
+
h('span', { style: { opacity: .65 } }, '思考'),
|
|
233
249
|
h('select', { id: inputId + '-effort', name: 'model-effort-' + row.id, disabled, value: EFFORT_IDS.has(override.effort) ? override.effort : '',
|
|
250
|
+
'aria-label': displayName + ' 默认思考档位',
|
|
234
251
|
onChange: (event) => onEdit(row.id, { effort: event.target.value }),
|
|
235
|
-
style: { ...inputStyle, cursor: disabled ? 'not-allowed' : 'pointer' } },
|
|
236
|
-
...EFFORT_OPTIONS.map((option) => h('option', { key: option.value, value: option.value }, option.label)))),
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
252
|
+
style: { ...inputStyle, width: 'auto', minHeight: '29px', padding: '3px 7px', cursor: disabled ? 'not-allowed' : 'pointer' } },
|
|
253
|
+
...EFFORT_OPTIONS.map((option) => h('option', { key: option.value, value: option.value }, option.label))))),
|
|
254
|
+
h('details', { style: { marginLeft: '23px', marginTop: '5px' } },
|
|
255
|
+
h('summary', { style: { ...SUMMARY, justifyContent: 'flex-start', fontSize: '10px', opacity: .6 } }, '模型设置'),
|
|
256
|
+
h('div', { style: { display: 'grid', gap: '10px', marginTop: '10px', padding: '10px', borderRadius: '8px', background: 'rgba(128,128,128,.04)' } },
|
|
257
|
+
h('div', { style: { display: 'grid', gap: '3px' } },
|
|
258
|
+
h('span', { style: labelStyle }, '模型 ID'),
|
|
259
|
+
h('code', { style: { fontSize: '10px', overflowWrap: 'anywhere', opacity: .75 } }, row.id)),
|
|
260
|
+
h('div', { style: { display: 'grid', gap: '10px', gridTemplateColumns: 'repeat(auto-fit, minmax(170px, 1fr))' } },
|
|
261
|
+
h('div', { style: { display: 'grid', gap: '4px', minWidth: 0 } },
|
|
262
|
+
h('label', { htmlFor: inputId + '-name', style: labelStyle }, '显示名'),
|
|
263
|
+
h('input', { id: inputId + '-name', name: 'model-name-' + row.id, type: 'text', disabled, value: override.name ?? '',
|
|
264
|
+
placeholder: row.name, onChange: (event) => onEdit(row.id, { name: event.target.value }), style: inputStyle })),
|
|
265
|
+
h('div', { style: { display: 'grid', gap: '4px', minWidth: 0 } },
|
|
266
|
+
h('label', { htmlFor: inputId + '-target', style: labelStyle }, '官方模型 ID'),
|
|
267
|
+
h('input', { id: inputId + '-target', name: 'model-target-' + row.id, type: 'text', disabled, value: override.target ?? '',
|
|
268
|
+
placeholder: '留空自动路由', onChange: (event) => onEdit(row.id, { target: event.target.value }), style: inputStyle }))),
|
|
269
|
+
h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '4px 12px', fontSize: '10px', opacity: .6 } },
|
|
270
|
+
h('span', null, row.inCatalog ? '当前目录' : '不在当前目录'),
|
|
271
|
+
Array.isArray(row.inputModalities) && row.inputModalities.length
|
|
272
|
+
? h('span', null, row.inputModalities.map((modality) => MODALITY_LABELS[modality] ?? modality).join(' · ')) : null))))
|
|
245
273
|
}
|
|
246
274
|
|
|
247
|
-
/**
|
|
248
|
-
* 模型列表:一次列出目录中的每个模型,改动在保存时合并成一份
|
|
249
|
-
* `modelOverrides` 提交,空项自动清除,不留空壳配置。
|
|
250
|
-
*/
|
|
251
275
|
function ModelOverrideList({ rows, overrides, disabled, onEdit }) {
|
|
276
|
+
const [query, setQuery] = React.useState('')
|
|
277
|
+
const [filter, setFilter] = React.useState('all')
|
|
252
278
|
if (rows.length === 0) {
|
|
253
|
-
return h('p', { style: { margin: 0, fontSize: '12px', lineHeight: 1.
|
|
254
|
-
'还没有模型目录。连接 / 登录 ACP 或刷新模型目录后,可在此逐项设置显示开关、默认思考档位与映射。')
|
|
279
|
+
return h('p', { role: 'status', style: { margin: 0, fontSize: '12px', lineHeight: 1.5, opacity: .6 } }, '暂无模型。请先连接当前账户,再刷新目录。')
|
|
255
280
|
}
|
|
256
281
|
const hidden = rows.filter((row) => overrideFor(overrides, row.id).enabled === false).length
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
282
|
+
const keyword = query.trim().toLowerCase()
|
|
283
|
+
const visible = rows.filter((row) => {
|
|
284
|
+
const override = overrideFor(overrides, row.id)
|
|
285
|
+
if (filter === 'enabled' && override.enabled === false) return false
|
|
286
|
+
if (filter === 'hidden' && override.enabled !== false) return false
|
|
287
|
+
return !keyword || (row.name + ' ' + row.id + ' ' + (override.name ?? '')).toLowerCase().includes(keyword)
|
|
288
|
+
})
|
|
289
|
+
const setVisible = (enabled) => { for (const row of visible) onEdit(row.id, { enabled }) }
|
|
290
|
+
return h('div', { style: { display: 'grid', gap: '8px' } },
|
|
291
|
+
h('div', { style: { display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) auto', gap: '8px' } },
|
|
292
|
+
h('input', { type: 'search', value: query, onChange: (event) => setQuery(event.target.value), placeholder: '搜索模型', 'aria-label': '搜索模型',
|
|
293
|
+
style: { ...ACTION, width: '100%', minWidth: 0, boxSizing: 'border-box', cursor: 'text' } }),
|
|
294
|
+
h('select', { value: filter, onChange: (event) => setFilter(event.target.value), 'aria-label': '筛选模型', style: ACTION },
|
|
295
|
+
h('option', { value: 'all' }, '全部 ' + rows.length),
|
|
296
|
+
h('option', { value: 'enabled' }, '已启用 ' + (rows.length - hidden)),
|
|
297
|
+
h('option', { value: 'hidden' }, '已隐藏 ' + hidden))),
|
|
298
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px', fontSize: '10px', opacity: .7 } },
|
|
299
|
+
h('span', { style: { marginRight: 'auto' } }, '显示 ' + visible.length + ' / ' + rows.length),
|
|
300
|
+
h('button', { type: 'button', disabled: disabled || visible.length === 0, onClick: () => setVisible(true), style: { ...ACTION, minHeight: '26px', padding: '2px 7px', fontSize: '10px' } }, '全部启用'),
|
|
301
|
+
h('button', { type: 'button', disabled: disabled || visible.length === 0, onClick: () => setVisible(false), style: { ...ACTION, minHeight: '26px', padding: '2px 7px', fontSize: '10px' } }, '全部隐藏')),
|
|
263
302
|
hidden === rows.length
|
|
264
|
-
? h('p', { role: 'status', style: { margin: 0, fontSize: '11px', lineHeight: 1.
|
|
265
|
-
'所有模型都已隐藏:保存后 Antigravity 将不出现在 DSH 模型选择器中,重新启用任一项即可恢复。')
|
|
303
|
+
? h('p', { role: 'status', style: { margin: 0, fontSize: '11px', lineHeight: 1.5, color: '#946200' } }, '全部隐藏后,Antigravity 将从模型选择器消失。')
|
|
266
304
|
: null,
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
305
|
+
visible.length
|
|
306
|
+
? h('div', { role: 'list', style: { display: 'grid' } }, ...visible.map((row) => h(ModelOverrideRow, {
|
|
307
|
+
key: row.id, row, override: overrideFor(overrides, row.id), disabled, onEdit,
|
|
308
|
+
})))
|
|
309
|
+
: h('p', { role: 'status', style: { margin: 0, fontSize: '12px', opacity: .6 } }, '没有符合条件的模型。'))
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* 代理账户面板:代理模式的独立登录态(与 ACP / 官方 CLI 登录相互独立)。
|
|
314
|
+
* 状态与授权 URL 经 Host 的 remote.agyProxyAccount 读取;授权 URL 只在
|
|
315
|
+
* 点击「连接代理账户」后返回一次,展示为可打开的链接。
|
|
316
|
+
*/
|
|
317
|
+
function ProxyAccountPanel({ control, disabled, connectDisabled, onAccountChanged }) {
|
|
318
|
+
const [snapshot, setSnapshot] = React.useState(undefined)
|
|
319
|
+
const [authUrl, setAuthUrl] = React.useState(undefined)
|
|
320
|
+
const [message, setMessage] = React.useState('')
|
|
321
|
+
const [busy, setBusy] = React.useState(false)
|
|
322
|
+
const alive = React.useRef(true)
|
|
323
|
+
const version = React.useRef(0)
|
|
324
|
+
const polling = React.useRef(false)
|
|
325
|
+
const actionVersion = React.useRef(0)
|
|
326
|
+
const status = React.useCallback(async () => {
|
|
327
|
+
if (!control || polling.current) return
|
|
328
|
+
polling.current = true
|
|
329
|
+
const current = ++version.current
|
|
330
|
+
try {
|
|
331
|
+
const value = unwrap(await control('account', { action: 'status' }))
|
|
332
|
+
if (alive.current && current === version.current) setSnapshot(value.snapshot)
|
|
333
|
+
} catch (error) {
|
|
334
|
+
if (alive.current && current === version.current) setSnapshot({ phase: 'signed-out', message: error instanceof Error ? error.message : String(error), updatedAt: new Date().toISOString() })
|
|
335
|
+
} finally { polling.current = false }
|
|
336
|
+
}, [control])
|
|
337
|
+
React.useEffect(() => {
|
|
338
|
+
alive.current = true
|
|
339
|
+
void status()
|
|
340
|
+
const timer = setInterval(() => {
|
|
341
|
+
if (typeof document === 'undefined' || document.visibilityState !== 'hidden') void status()
|
|
342
|
+
}, 3000)
|
|
343
|
+
return () => { alive.current = false; ++version.current; clearInterval(timer) }
|
|
344
|
+
}, [status])
|
|
345
|
+
React.useEffect(() => {
|
|
346
|
+
if (!snapshot) return
|
|
347
|
+
setMessage('')
|
|
348
|
+
if (snapshot.phase !== 'pending-login') setAuthUrl(undefined)
|
|
349
|
+
onAccountChanged?.(snapshot)
|
|
350
|
+
}, [snapshot?.phase, snapshot?.connectionStage, snapshot?.message, snapshot?.revision, onAccountChanged])
|
|
351
|
+
const act = async (action) => {
|
|
352
|
+
if (!control || (busy && action === 'login')) return
|
|
353
|
+
const operation = ++actionVersion.current
|
|
354
|
+
++version.current
|
|
355
|
+
setBusy(true)
|
|
356
|
+
setMessage(action === 'login' ? '正在准备授权…' : '')
|
|
357
|
+
try {
|
|
358
|
+
const value = unwrap(await control('account', { action }))
|
|
359
|
+
if (!alive.current || operation !== actionVersion.current) return
|
|
360
|
+
setSnapshot(value.snapshot)
|
|
361
|
+
setMessage(value.message ?? '')
|
|
362
|
+
if (value.authorizationUrl) setAuthUrl(value.authorizationUrl)
|
|
363
|
+
else if (action !== 'login') setAuthUrl(undefined)
|
|
364
|
+
} catch (error) {
|
|
365
|
+
if (alive.current && operation === actionVersion.current) setMessage(error instanceof Error ? error.message : String(error))
|
|
366
|
+
} finally { if (alive.current && operation === actionVersion.current) setBusy(false) }
|
|
367
|
+
}
|
|
368
|
+
const phase = snapshot?.phase
|
|
369
|
+
const stage = snapshot?.connectionStage
|
|
370
|
+
const connecting = busy || stage === 'preparing' || stage === 'finishing' || phase === 'pending-login'
|
|
371
|
+
const dotColor = phase === 'signed-in' ? '#25b879' : connecting ? '#d99b28' : phase === 'login-failed' ? '#e5534b' : '#808080'
|
|
372
|
+
const phaseLabel = phase === 'signed-in' ? '代理账户已连接' : stage === 'finishing' ? '正在完成登录…' : stage === 'preparing' ? '正在检查登录态…' : phase === 'pending-login' ? '等待浏览器授权…' : phase === 'login-failed' ? '登录失败' : busy ? '正在连接…' : '未连接'
|
|
373
|
+
return h('div', { style: { display: 'grid', gap: '10px', padding: '12px', borderRadius: '10px', background: 'rgba(128,128,128,.045)' } },
|
|
374
|
+
h('div', { style: { display: 'flex', alignItems: 'flex-start', gap: '10px' } },
|
|
375
|
+
h(StatusDot, { color: dotColor }),
|
|
376
|
+
h('div', { style: { minWidth: 0 } },
|
|
377
|
+
h('div', { style: { fontSize: '12px', fontWeight: 650 } }, phaseLabel + (snapshot?.email ? `(${snapshot.email})` : '') + (snapshot?.source === 'cli-import' ? ' · 导入自官方 CLI' : '')),
|
|
378
|
+
h('p', { role: 'status', style: { fontSize: '12px', lineHeight: 1.6, opacity: .7, margin: '3px 0 0', overflowWrap: 'anywhere' } }, message || snapshot?.message || ''))),
|
|
379
|
+
h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '8px' } },
|
|
380
|
+
h('button', { type: 'button', style: phase === 'signed-in' ? ACTION : PRIMARY, disabled: disabled || connectDisabled || connecting || phase === 'signed-in', onClick: () => act('login') }, connecting ? '连接中…' : '连接代理账户'),
|
|
381
|
+
connecting ? h('button', { type: 'button', style: ACTION, disabled, onClick: () => act('cancel') }, '取消登录') : null,
|
|
382
|
+
phase === 'signed-in' ? h('button', { type: 'button', style: ACTION, disabled: disabled || busy, onClick: () => act('logout') }, '退出登录') : null),
|
|
383
|
+
authUrl && stage !== 'finishing' ? h('div', { style: { display: 'grid', gap: '6px', justifyItems: 'start' } },
|
|
384
|
+
h('a', { href: authUrl, target: '_blank', rel: 'noopener noreferrer', style: { ...PRIMARY, textDecoration: 'none', display: 'inline-flex', alignItems: 'center' } }, '打开 Google 授权页'),
|
|
385
|
+
h('small', { style: { fontSize: '10px', opacity: .65, lineHeight: 1.5 } }, '请在运行 DSH 的设备上完成授权(5 分钟内有效)。')) : null,
|
|
386
|
+
phase === 'pending-login' && stage !== 'finishing' && !authUrl && !busy ? h('p', { role: 'status', style: { fontSize: '11px', margin: 0, opacity: .7 } }, '授权页已打开;找不到时请取消后重试。') : null,
|
|
387
|
+
h('p', { style: { opacity: .55, fontSize: '10px', lineHeight: 1.5, margin: 0 } }, '生成与套餐额度使用此账户 · 无需 agy CLI · ACP 已停用'))
|
|
271
388
|
}
|
|
272
389
|
|
|
273
390
|
function AntigravitySettingsCard(props) {
|
|
@@ -291,7 +408,7 @@ window.__ModuleLoader__.load({
|
|
|
291
408
|
const value = unwrap(await props.control('models', force ? { force: true } : undefined))
|
|
292
409
|
if (!alive.current || version !== modelVersion.current) return
|
|
293
410
|
setModels(value.models ?? [])
|
|
294
|
-
setModelMessage(value.status === 'error' ? value.message :
|
|
411
|
+
setModelMessage(value.status === 'error' ? value.message : '')
|
|
295
412
|
} catch (error) { if (alive.current && version === modelVersion.current) setModelMessage(error instanceof Error ? error.message : String(error)) }
|
|
296
413
|
finally {
|
|
297
414
|
if (force) manualModelPull.current = false
|
|
@@ -333,6 +450,9 @@ window.__ModuleLoader__.load({
|
|
|
333
450
|
const [saving, setSaving] = React.useState(false)
|
|
334
451
|
const [dirtyKeys, setDirtyKeys] = React.useState([])
|
|
335
452
|
const [conflicts, setConflicts] = React.useState([])
|
|
453
|
+
const [savedProxyMode, setSavedProxyMode] = React.useState(false)
|
|
454
|
+
// 设置分页。非活动页只隐藏、不卸载:草稿、保存与控件覆盖测试都依赖字段始终挂载。
|
|
455
|
+
const [settingsPage, setSettingsPage] = React.useState('connection')
|
|
336
456
|
const settingsVersion = React.useRef(0)
|
|
337
457
|
const savingRef = React.useRef(false)
|
|
338
458
|
const refreshAfterSave = React.useRef(false)
|
|
@@ -340,6 +460,29 @@ window.__ModuleLoader__.load({
|
|
|
340
460
|
const dirty = React.useRef(new Set())
|
|
341
461
|
const draftRef = React.useRef(draft)
|
|
342
462
|
draftRef.current = draft
|
|
463
|
+
const effectiveProxyMode = account?.generationMode ? account.generationMode.requested === 'proxy' : savedProxyMode
|
|
464
|
+
const activeModeRef = React.useRef(effectiveProxyMode)
|
|
465
|
+
activeModeRef.current = effectiveProxyMode
|
|
466
|
+
React.useEffect(() => { props.readQuota?.observeMode?.(effectiveProxyMode ? PROXY_PROVIDER : PROVIDER) }, [effectiveProxyMode, props.readQuota])
|
|
467
|
+
const proxyUnavailable = savedProxyMode && account?.generationMode?.proxyRouterReady === false
|
|
468
|
+
const previousProxyAccount = React.useRef(undefined)
|
|
469
|
+
const onProxyAccountChanged = React.useCallback((snapshot) => {
|
|
470
|
+
const previous = previousProxyAccount.current
|
|
471
|
+
previousProxyAccount.current = snapshot
|
|
472
|
+
const identityChanged = previous?.revision !== snapshot.revision
|
|
473
|
+
const connected = snapshot.phase === 'signed-in' && previous?.phase !== 'signed-in'
|
|
474
|
+
const signedOut = snapshot.phase === 'signed-out' && previous?.phase === 'signed-in'
|
|
475
|
+
props.readQuota?.observeProxyAccount?.(snapshot)
|
|
476
|
+
if (savedProxyMode && (connected || signedOut || (identityChanged && !snapshot.connectionStage))) void pullModels(true)
|
|
477
|
+
void accountAction('status')
|
|
478
|
+
}, [savedProxyMode, pullModels, accountAction, props.readQuota])
|
|
479
|
+
React.useEffect(() => {
|
|
480
|
+
let current = true
|
|
481
|
+
Promise.resolve().then(() => remote?.llm?.listProviders?.()).then(unwrap).then((providers) => {
|
|
482
|
+
if (current && alive.current && Array.isArray(providers)) setRegistered(providers.some((row) => row?.id === (effectiveProxyMode ? PROXY_PROVIDER : PROVIDER)))
|
|
483
|
+
}).catch(() => { if (current && alive.current) setRegistered(false) })
|
|
484
|
+
return () => { current = false }
|
|
485
|
+
}, [effectiveProxyMode, remote])
|
|
343
486
|
|
|
344
487
|
const editField = React.useCallback((key, value) => {
|
|
345
488
|
if (savingRef.current) return
|
|
@@ -397,7 +540,12 @@ window.__ModuleLoader__.load({
|
|
|
397
540
|
const found = namespaces.find((item) => item?.ns === ENTRY_ID)
|
|
398
541
|
setView(found ?? null)
|
|
399
542
|
const current = found?.value && typeof found.value === 'object' ? found.value : {}
|
|
400
|
-
const
|
|
543
|
+
const activeProvider = activeModeRef.current ? PROXY_PROVIDER : PROVIDER
|
|
544
|
+
setSavedProxyMode(current.proxyMode === true)
|
|
545
|
+
const server = Object.fromEntries(FIELDS.map((field) => [field.key, field.checkbox
|
|
546
|
+
? (field.defaultChecked ? current[field.key] !== false : current[field.key] === true)
|
|
547
|
+
: field.lines ? (Array.isArray(current[field.key]) ? current[field.key].join('\n') : current[field.key] ?? '')
|
|
548
|
+
: current[field.key] ?? field.defaultValue ?? '']))
|
|
401
549
|
// 整份 map 作为一个字段参与脏值比较;规范化后再比,避免键序或
|
|
402
550
|
// 「显式写回默认值」被误判为未保存修改。
|
|
403
551
|
server[OVERRIDES_KEY] = canonicalOverrides(current[OVERRIDES_KEY])
|
|
@@ -422,7 +570,7 @@ window.__ModuleLoader__.load({
|
|
|
422
570
|
setDirtyKeys([...dirty.current])
|
|
423
571
|
setConflicts((previous) => [...new Set([...previous.filter((key) => dirty.current.has(key)), ...changed])])
|
|
424
572
|
setWritable(settings?.writable === true)
|
|
425
|
-
setRegistered(rows.some((row) => row?.id ===
|
|
573
|
+
setRegistered(rows.some((row) => row?.id === activeProvider))
|
|
426
574
|
if (!found) setStatus({ kind: 'error', text: '宿主尚未提供 Antigravity ACP 的可编辑设置。请确认 Host 与 Client 插件版本一致,并重新加载插件后刷新配置。' })
|
|
427
575
|
else if (!preserveStatus) setStatus(null)
|
|
428
576
|
} catch (cause) {
|
|
@@ -435,9 +583,9 @@ window.__ModuleLoader__.load({
|
|
|
435
583
|
const save = React.useCallback(async () => {
|
|
436
584
|
if (!view || !writable || loading || savingRef.current || dirty.current.size === 0) return
|
|
437
585
|
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))
|
|
586
|
+
&& (!Number.isSafeInteger(draftRef.current[field.key]) || draftRef.current[field.key] < (field.min ?? 0) || (field.max !== undefined && draftRef.current[field.key] > field.max)))
|
|
439
587
|
if (invalid) {
|
|
440
|
-
setStatus({ kind: 'error', text: invalid.label + '必须是 ' + invalid.min + '–' + invalid.max + ' 的整数。' })
|
|
588
|
+
setStatus({ kind: 'error', text: invalid.label + '必须是' + (invalid.max === undefined ? '不小于 ' + (invalid.min ?? 0) : ' ' + invalid.min + '–' + invalid.max) + ' 的整数。' })
|
|
441
589
|
return
|
|
442
590
|
}
|
|
443
591
|
savingRef.current = true
|
|
@@ -447,18 +595,28 @@ window.__ModuleLoader__.load({
|
|
|
447
595
|
// modelOverrides 是整份 map 一个字段,按同样方式提交。
|
|
448
596
|
const ops = [...FIELDS.map((field) => field.key), OVERRIDES_KEY]
|
|
449
597
|
.filter((key) => dirty.current.has(key))
|
|
450
|
-
.map((key) =>
|
|
598
|
+
.map((key) => {
|
|
599
|
+
const field = FIELDS.find((entry) => entry.key === key)
|
|
600
|
+
const value = field?.lines ? String(draftRef.current[key] ?? '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean) : draftRef.current[key]
|
|
601
|
+
return { op: 'set', path: [key], value }
|
|
602
|
+
})
|
|
451
603
|
try {
|
|
452
604
|
unwrap(await remote.settings.mutate(ENTRY_ID, ops, view.revision))
|
|
453
605
|
if (!alive.current) return
|
|
454
606
|
for (const operation of ops) {
|
|
455
607
|
const key = operation.path[0]
|
|
456
|
-
|
|
608
|
+
const field = FIELDS.find((entry) => entry.key === key)
|
|
609
|
+
const currentValue = field?.lines ? String(draftRef.current[key] ?? '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean) : draftRef.current[key]
|
|
610
|
+
if (JSON.stringify(currentValue) === JSON.stringify(operation.value)) dirty.current.delete(key)
|
|
457
611
|
}
|
|
458
612
|
setDirtyKeys([...dirty.current])
|
|
459
613
|
setConflicts([])
|
|
460
|
-
setStatus({ kind: 'ok', text:
|
|
614
|
+
setStatus({ kind: 'ok', text: ops.some((operation) => operation.path[0] === 'proxyMode')
|
|
615
|
+
? '已保存并切换模式,请在会话中选择对应模型。'
|
|
616
|
+
: '已保存;进行中的会话不会被中断。' })
|
|
461
617
|
await reload(true)
|
|
618
|
+
void accountAction('status')
|
|
619
|
+
void pullModels(true)
|
|
462
620
|
} catch (cause) {
|
|
463
621
|
if (!alive.current) return
|
|
464
622
|
setStatus({ kind: 'error', text: cause instanceof Error ? cause.message : String(cause) })
|
|
@@ -471,7 +629,7 @@ window.__ModuleLoader__.load({
|
|
|
471
629
|
if (refreshAfterSave.current) { refreshAfterSave.current = false; void reload(true) }
|
|
472
630
|
}
|
|
473
631
|
}
|
|
474
|
-
}, [loading, writable, remote, reload, view])
|
|
632
|
+
}, [loading, writable, remote, reload, view, accountAction, pullModels])
|
|
475
633
|
|
|
476
634
|
React.useEffect(() => {
|
|
477
635
|
reload()
|
|
@@ -490,6 +648,7 @@ window.__ModuleLoader__.load({
|
|
|
490
648
|
// 目录条目 + 已配置但目录里暂时没有的 ID:正在编辑的映射不能因为目录刷新而消失。
|
|
491
649
|
const rows = modelRows(models, draft[OVERRIDES_KEY])
|
|
492
650
|
const editingDisabled = loading || saving || !view || !writable
|
|
651
|
+
const connectionSettingsDirty = dirtyKeys.some(key => key === 'proxyUrl' || key === 'proxyBypass')
|
|
493
652
|
const discardDraft = () => {
|
|
494
653
|
if (savingRef.current) return
|
|
495
654
|
dirty.current.clear()
|
|
@@ -497,79 +656,94 @@ window.__ModuleLoader__.load({
|
|
|
497
656
|
setDraft(draftRef.current)
|
|
498
657
|
setDirtyKeys([])
|
|
499
658
|
setConflicts([])
|
|
500
|
-
setStatus({ kind: 'ok', text: '
|
|
659
|
+
setStatus({ kind: 'ok', text: '已撤销未保存修改。' })
|
|
501
660
|
}
|
|
502
661
|
|
|
503
662
|
const connection = connectionStatus(account, registered)
|
|
504
|
-
const fieldsFor = (group) => FIELDS.filter((field) => field.group === group)
|
|
663
|
+
const fieldsFor = (group) => FIELDS.filter((field) => field.group === group && (!effectiveProxyMode || !field.acpOnly))
|
|
505
664
|
const fieldProps = { draft, dirtyKeys, conflicts, disabled: editingDisabled, editField }
|
|
665
|
+
const pageStyle = (id) => ({ display: settingsPage === id ? 'flex' : 'none', flexDirection: 'column', gap: '14px' })
|
|
666
|
+
const pageButton = (id, label) => h('button', {
|
|
667
|
+
type: 'button', role: 'tab', 'aria-selected': settingsPage === id, 'aria-controls': 'agy-settings-page-' + id,
|
|
668
|
+
onClick: () => setSettingsPage(id),
|
|
669
|
+
style: { ...ACTION, minHeight: '32px', padding: '5px 12px', fontWeight: settingsPage === id ? 650 : 450, borderColor: settingsPage === id ? '#177c55' : 'rgba(128,128,128,.22)', background: settingsPage === id ? 'rgba(23,124,85,.12)' : 'transparent' },
|
|
670
|
+
}, label)
|
|
506
671
|
const busy = ['connecting', 'cancelling'].includes(account?.state) || runtimeBusy(account?.runtime)
|
|
507
672
|
const readonly = !loading && view && !writable
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
h('
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
h('
|
|
531
|
-
h(
|
|
532
|
-
h('
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
h(
|
|
552
|
-
h('
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
h('
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
h(
|
|
567
|
-
|
|
568
|
-
h(
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
h(
|
|
673
|
+
const saveVisible = dirtyKeys.length > 0 || loading || readonly || conflicts.length > 0 || Boolean(status)
|
|
674
|
+
|
|
675
|
+
return h('div', { className: 'antigravity-settings-card', style: { padding: '12px', display: 'flex', flexDirection: 'column', gap: '10px', maxWidth: '760px', boxSizing: 'border-box', lineHeight: 1.5 } },
|
|
676
|
+
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: '4px 0', display: saveVisible ? 'grid' : 'none', gap: '6px' } },
|
|
677
|
+
h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '7px', alignItems: 'center' } },
|
|
678
|
+
h('button', { type: 'button', onClick: save, disabled: editingDisabled || dirtyKeys.length === 0, style: { ...PRIMARY, minHeight: '30px', padding: '4px 10px' } }, saving ? '保存中…' : '保存'),
|
|
679
|
+
dirtyKeys.length ? h('button', { type: 'button', disabled: loading || saving, onClick: discardDraft, style: { ...ACTION, minHeight: '30px', padding: '4px 9px' } }, '撤销') : null,
|
|
680
|
+
h('span', { role: 'status', 'aria-live': 'polite', style: { fontSize: '11px', opacity: .7 } }, dirtyKeys.length ? dirtyKeys.length + ' 项未保存' : loading ? '正在读取…' : readonly ? '只读设置' : ''),
|
|
681
|
+
h('button', { type: 'button', title: '重新读取配置并保留草稿', disabled: loading || saving, onClick: () => reload(), style: { ...ACTION, minHeight: '30px', padding: '4px 8px', borderColor: 'transparent', background: 'transparent', opacity: .7, marginLeft: 'auto' } }, loading ? '刷新中…' : '刷新')),
|
|
682
|
+
conflicts.length ? h('p', { role: 'alert', style: { margin: 0, fontSize: '12px', lineHeight: 1.5 } }, '以下内容已在其他位置更新,已保留你的输入:' + conflicts.map((key) => key === OVERRIDES_KEY ? OVERRIDES_LABEL : FIELDS.find((field) => field.key === key)?.label ?? key).join('、')) : null,
|
|
683
|
+
h(StatusNotice, { status })),
|
|
684
|
+
|
|
685
|
+
connectionSettingsDirty ? h(StatusNotice, { status: { kind: 'busy', text: '请先保存网络设置,再连接或登录。' } }) : null,
|
|
686
|
+
proxyUnavailable ? h(StatusNotice, { status: { kind: 'error', text: '代理路由未就绪,ACP 保持停用。请启用 Antigravity Proxy Router 组件。' } }) : null,
|
|
687
|
+
h('div', { role: 'tablist', 'aria-label': '设置分页', style: { display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: '6px' } },
|
|
688
|
+
pageButton('connection', '连接'),
|
|
689
|
+
pageButton('models', '模型'),
|
|
690
|
+
pageButton('quota', '额度'),
|
|
691
|
+
pageButton('advanced', '高级'),
|
|
692
|
+
h('span', { style: { marginLeft: 'auto', padding: '3px 7px', borderRadius: '99px', fontSize: '10px', color: registered === false ? '#b42318' : effectiveProxyMode ? '#946200' : 'inherit', background: effectiveProxyMode ? 'rgba(217,159,4,.12)' : TRACK } }, registered === false ? '路由未注册' : effectiveProxyMode ? '代理模式' : 'ACP 模式')),
|
|
693
|
+
|
|
694
|
+
h('div', { id: 'agy-settings-page-connection', role: 'tabpanel', style: pageStyle('connection') },
|
|
695
|
+
h('section', { style: PANEL, 'aria-label': '运行模式' },
|
|
696
|
+
h(SettingsFields, { ...fieldProps, fields: fieldsFor('mode') }),
|
|
697
|
+
dirtyKeys.includes('proxyMode') ? h('p', { role: 'status', style: { margin: '8px 0 0', fontSize: '11px', color: '#946200' } }, '保存后切换到' + (draft.proxyMode ? '代理模式' : 'ACP 模式')) : null),
|
|
698
|
+
effectiveProxyMode
|
|
699
|
+
? h('section', { style: PANEL, 'aria-label': '代理账户' },
|
|
700
|
+
h(SectionHeading, { title: '代理账户' }),
|
|
701
|
+
h(ProxyAccountPanel, { control: props.proxyControl, onAccountChanged: onProxyAccountChanged, disabled: !props.proxyControl || !view || !writable, connectDisabled: editingDisabled || connectionSettingsDirty }))
|
|
702
|
+
: h('section', { style: PANEL, 'aria-label': '账户连接' },
|
|
703
|
+
h(SectionHeading, { title: 'ACP 账户与连接' }),
|
|
704
|
+
h('div', { style: { display: 'flex', alignItems: 'flex-start', gap: '10px', marginBottom: '12px' } },
|
|
705
|
+
h(StatusDot, { color: connection.color }),
|
|
706
|
+
h('div', { style: { minWidth: 0 } },
|
|
707
|
+
h('div', { style: { fontSize: '12px', fontWeight: 650, color: connection.color } }, connection.label),
|
|
708
|
+
h('p', { role: 'status', style: { fontSize: '11px', lineHeight: 1.5, opacity: .7, overflowWrap: 'anywhere', margin: '3px 0 0' } }, connection.description))),
|
|
709
|
+
h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '8px' } },
|
|
710
|
+
h('button', { type: 'button', style: PRIMARY, disabled: !props.control || busy || connectionSettingsDirty, onClick: () => accountAction('login') }, account?.state === 'connecting' ? '正在连接…' : '连接 / 登录 ACP'),
|
|
711
|
+
h('button', { type: 'button', style: ACTION, disabled: !props.control || busy || connection.ready || connectionSettingsDirty, onClick: () => accountAction('warmup') }, connection.ready ? '已预热' : '立即预热'),
|
|
712
|
+
['connecting', 'cancelling'].includes(account?.state) ? h('button', { type: 'button', style: ACTION, disabled: account?.state === 'cancelling', onClick: () => accountAction('cancel') }, account?.state === 'cancelling' ? '正在取消…' : '取消连接') : null))),
|
|
713
|
+
|
|
714
|
+
h('div', { id: 'agy-settings-page-models', role: 'tabpanel', style: pageStyle('models') },
|
|
715
|
+
h('section', { style: PANEL, 'aria-label': '模型列表' },
|
|
716
|
+
h(SectionHeading, { title: '模型',
|
|
717
|
+
action: h('button', { type: 'button', title: '刷新模型目录', 'aria-label': '刷新模型目录', 'aria-busy': modelBusy || undefined, style: { ...ACTION, flex: '0 0 auto', whiteSpace: 'nowrap' }, disabled: modelBusy || !props.control, onClick: () => pullModels(true) }, modelBusy ? '正在刷新…' : '刷新') }),
|
|
718
|
+
h(ModelOverrideList, { rows, overrides: draft[OVERRIDES_KEY], disabled: editingDisabled, onEdit: editOverride }),
|
|
719
|
+
modelMessage ? h('p', { role: 'status', style: { fontSize: '11px', lineHeight: 1.5, opacity: .65, margin: 0, overflowWrap: 'anywhere' } }, modelMessage) : null)),
|
|
720
|
+
|
|
721
|
+
h('div', { id: 'agy-settings-page-quota', role: 'tabpanel', style: pageStyle('quota') },
|
|
722
|
+
h(SettingsQuotaPanel, { readQuota: props.readQuota, source: effectiveProxyMode ? PROXY_PROVIDER : PROVIDER,
|
|
723
|
+
accountRevision: effectiveProxyMode ? previousProxyAccount.current?.revision : undefined,
|
|
724
|
+
cliQuotaOn: (dirtyKeys.includes('cliQuotaEnabled') ? baseline.current.cliQuotaEnabled : draft.cliQuotaEnabled) === true,
|
|
725
|
+
cliQuotaDirty: dirtyKeys.includes('cliQuotaEnabled') }),
|
|
726
|
+
h('section', { style: PANEL, 'aria-label': '额度取数方式' },
|
|
727
|
+
h(SectionHeading, { title: 'CLI 额度(可选)' }),
|
|
728
|
+
h(SettingsFields, { ...fieldProps, fields: fieldsFor('quota') }))),
|
|
729
|
+
|
|
730
|
+
h('div', { id: 'agy-settings-page-advanced', role: 'tabpanel', style: pageStyle('advanced') },
|
|
731
|
+
!effectiveProxyMode ? h('section', { style: PANEL, 'aria-label': '常用设置' },
|
|
732
|
+
h(SectionHeading, { title: '会话偏好' }),
|
|
733
|
+
h(SettingsFields, { ...fieldProps, fields: fieldsFor('general') })) : null,
|
|
734
|
+
h(SettingsGroup, { title: '网络与 CLI', fields: fieldsFor('network'), fieldProps, open: effectiveProxyMode }),
|
|
735
|
+
!effectiveProxyMode ? h(SettingsGroup, { title: 'ACP 启动', fields: fieldsFor('launch'), fieldProps }) : null,
|
|
736
|
+
!effectiveProxyMode ? h(SettingsGroup, { title: '连接池', fields: fieldsFor('pool'), fieldProps }) : null,
|
|
737
|
+
!effectiveProxyMode ? h(SettingsGroup, { title: '请求与工具超时', fields: fieldsFor('timeouts'), fieldProps }) : null,
|
|
738
|
+
!effectiveProxyMode ? h(SettingsGroup, { title: '会话与进程', fields: fieldsFor('advanced'), fieldProps }) : null,
|
|
739
|
+
!effectiveProxyMode ? h('details', { style: PANEL, 'aria-label': '连接诊断' },
|
|
740
|
+
h('summary', { style: SUMMARY },
|
|
741
|
+
h('span', { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
|
|
742
|
+
h(StatusDot, { color: connection.color }),
|
|
743
|
+
h('span', { style: { fontSize: '13px', fontWeight: 650 } }, '连接诊断')),
|
|
744
|
+
h('small', { style: { fontSize: '11px', opacity: .6 } }, '状态、并发与耗时')),
|
|
745
|
+
h('div', { style: { marginTop: '16px', paddingTop: '14px', borderTop: '1px solid rgba(128,128,128,.14)' } },
|
|
746
|
+
account?.runtime ? RuntimeDetails({ runtime: account.runtime }) : h('p', { style: { margin: 0, fontSize: '12px', opacity: .6 } }, '连接后显示运行详情。'))) : null))
|
|
573
747
|
}
|
|
574
748
|
|
|
575
749
|
/**
|
|
@@ -581,7 +755,7 @@ window.__ModuleLoader__.load({
|
|
|
581
755
|
|
|
582
756
|
const TRACK = 'rgba(128,128,128,.18)'
|
|
583
757
|
const CARD = { padding: '12px', borderRadius: '12px', border: '1px solid rgba(128,128,128,.18)', background: 'rgba(128,128,128,.05)' }
|
|
584
|
-
const PANEL = { padding: '
|
|
758
|
+
const PANEL = { padding: '14px', borderRadius: '12px', border: '1px solid rgba(128,128,128,.16)', background: 'rgba(128,128,128,.025)' }
|
|
585
759
|
const SUMMARY = { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '12px', cursor: 'pointer', listStyle: 'none' }
|
|
586
760
|
const ACTION = { padding: '7px 12px', minHeight: '34px', borderRadius: '8px', border: '1px solid rgba(128,128,128,.22)', background: 'rgba(128,128,128,.06)', color: 'inherit', font: 'inherit', fontSize: '12px', cursor: 'pointer' }
|
|
587
761
|
const PRIMARY = { ...ACTION, background: '#177c55', border: '1px solid #177c55', color: '#fff', fontWeight: 600 }
|
|
@@ -594,7 +768,7 @@ window.__ModuleLoader__.load({
|
|
|
594
768
|
return undefined
|
|
595
769
|
}
|
|
596
770
|
function modelGroup(model) {
|
|
597
|
-
const id = String(model ?? '').toLowerCase()
|
|
771
|
+
const id = String(model ?? '').toLowerCase().replace(/^antigravity-/, '')
|
|
598
772
|
if (/^gemini-/.test(id) && /(?:^|-)argon(?:-|$)/.test(id)) return 'argon'
|
|
599
773
|
if (/^gemini-/.test(id)) return 'gemini'
|
|
600
774
|
if (/^(claude|gpt)-/.test(id)) return 'claude'
|
|
@@ -627,26 +801,29 @@ window.__ModuleLoader__.load({
|
|
|
627
801
|
if (minutes >= 60) return Math.floor(minutes / 60) + '小时 ' + minutes % 60 + '分钟后重置'
|
|
628
802
|
return minutes + '分钟后重置'
|
|
629
803
|
}
|
|
630
|
-
function remainingBar(bucket, compact, stale, label) {
|
|
804
|
+
function remainingBar(bucket, compact, stale, label, compactHeight = 4) {
|
|
631
805
|
const pct = percentage(bucket)
|
|
632
806
|
const unknown = pct === undefined || bucket.disabled
|
|
633
807
|
return h('span', {
|
|
634
808
|
role: 'progressbar', 'aria-label': label,
|
|
635
809
|
'aria-valuemin': 0, 'aria-valuemax': 100,
|
|
636
810
|
...(unknown ? { 'aria-valuetext': bucket.disabled ? '已停用' : '额度未知' } : { 'aria-valuenow': pct }),
|
|
637
|
-
style: { display: 'block', width: compact ? '
|
|
811
|
+
style: { display: 'block', width: compact ? '34px' : '100%', height: compact ? compactHeight + 'px' : '4px', borderRadius: '99px', overflow: 'hidden', background: TRACK },
|
|
638
812
|
}, h('span', { style: { display: 'block', height: '100%', width: unknown ? '100%' : pct + '%', opacity: unknown ? .35 : 1, borderRadius: '99px', background: tone(bucket, stale) } }))
|
|
639
813
|
}
|
|
640
|
-
function useQuota(readQuota, enabled = true) {
|
|
814
|
+
function useQuota(readQuota, enabled = true, source, model) {
|
|
641
815
|
const [data, setData] = React.useState(null)
|
|
642
816
|
const [refreshing, setRefreshing] = React.useState(false)
|
|
643
817
|
const version = React.useRef(0)
|
|
818
|
+
const currentSource = React.useRef(source)
|
|
819
|
+
const currentModel = React.useRef(model)
|
|
820
|
+
currentModel.current = model
|
|
644
821
|
const update = React.useCallback((value) => setData(value), [])
|
|
645
822
|
const load = React.useCallback((force = false) => {
|
|
646
823
|
if (!enabled || !readQuota) return Promise.resolve()
|
|
647
824
|
const current = ++version.current
|
|
648
825
|
setRefreshing(true)
|
|
649
|
-
return Promise.resolve().then(() => readQuota(force)).then(unwrap).then((value) => {
|
|
826
|
+
return Promise.resolve().then(() => readQuota(force, source, model)).then(unwrap).then((value) => {
|
|
650
827
|
if (current === version.current) update(value)
|
|
651
828
|
}).catch((error) => {
|
|
652
829
|
if (current === version.current) {
|
|
@@ -656,95 +833,155 @@ window.__ModuleLoader__.load({
|
|
|
656
833
|
: { status: 'error', groups: [], error: { message } })
|
|
657
834
|
}
|
|
658
835
|
}).finally(() => { if (current === version.current) setRefreshing(false) })
|
|
659
|
-
}, [readQuota, enabled, update])
|
|
836
|
+
}, [readQuota, enabled, update, source, model])
|
|
660
837
|
React.useEffect(() => {
|
|
838
|
+
currentSource.current = source
|
|
839
|
+
setData(null)
|
|
661
840
|
if (!enabled || !readQuota) { setData(null); return undefined }
|
|
662
841
|
void load(false)
|
|
663
|
-
const off = readQuota.subscribe?.((value) => {
|
|
842
|
+
const off = readQuota.subscribe?.((value, origin) => {
|
|
843
|
+
if (origin !== undefined && origin !== source) return
|
|
844
|
+
if (value?.requestedModel !== undefined && value.requestedModel !== currentModel.current) return
|
|
845
|
+
if (value === null) {
|
|
846
|
+
++version.current; update(null); setRefreshing(false)
|
|
847
|
+
void load(true)
|
|
848
|
+
return
|
|
849
|
+
}
|
|
850
|
+
++version.current; update(value); setRefreshing(false)
|
|
851
|
+
})
|
|
664
852
|
const timer = setInterval(() => { if (typeof document === 'undefined' || document.visibilityState !== 'hidden') void load(false) }, 300_000)
|
|
665
853
|
return () => { ++version.current; clearInterval(timer); off?.() }
|
|
666
|
-
}, [load, enabled, readQuota, update])
|
|
667
|
-
return { data, refreshing, load }
|
|
854
|
+
}, [load, enabled, readQuota, update, source, model])
|
|
855
|
+
return { data: currentSource.current === source ? data : null, refreshing, load }
|
|
668
856
|
}
|
|
669
857
|
function CreditsDetails({ credits, pending }) {
|
|
670
858
|
const valid = credits?.status === 'ok' && Number.isSafeInteger(credits.remainingCredits) && credits.remainingCredits >= 0
|
|
671
|
-
const label = valid ? credits.remainingCredits.toLocaleString() : pending ? '正在读取…' : credits?.status === 'error' ? '查询失败' : '未提供'
|
|
672
|
-
return h('section', { style:
|
|
859
|
+
const label = valid ? credits.remainingCredits.toLocaleString() : pending ? '正在读取…' : credits?.status === 'error' ? '查询失败' : credits?.status === 'unavailable' ? '未启用' : '未提供'
|
|
860
|
+
return h('section', { style: { padding: '9px 0', borderTop: '1px solid rgba(128,128,128,.16)' }, 'aria-label': 'AI Credits 余额' },
|
|
673
861
|
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 ? '刷新中…' : '刷新额度')))
|
|
862
|
+
h('strong', { style: { fontSize: '12px' } }, 'AI Credits'),
|
|
863
|
+
h('strong', { style: { fontSize: valid ? '17px' : '12px', fontVariantNumeric: 'tabular-nums', color: credits?.stale ? '#d99b28' : 'inherit' } }, label)),
|
|
864
|
+
credits?.planName || credits?.updatedAt ? h('div', { style: { display: 'flex', flexWrap: 'wrap', justifyContent: 'space-between', gap: '3px 10px', fontSize: '10px', opacity: .55, marginTop: '3px' } },
|
|
865
|
+
credits?.planName ? h('span', null, credits.planName) : null,
|
|
866
|
+
credits?.updatedAt ? h('span', null, (valid ? '余额更新于 ' : '最近查询 ') + new Date(credits.updatedAt).toLocaleString()) : null) : null,
|
|
867
|
+
credits?.stale ? h('p', { role: 'status', style: { margin: '4px 0 0', fontSize: '10px', color: '#d99b28' } }, '余额缓存 · ' + (credits.staleReason ?? '余额刷新失败')) : null,
|
|
868
|
+
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
869
|
}
|
|
706
|
-
function
|
|
707
|
-
const [
|
|
708
|
-
const [
|
|
709
|
-
const alive = React.useRef(false)
|
|
870
|
+
function SettingsQuotaPanel({ readQuota, source, accountRevision, cliQuotaOn, cliQuotaDirty }) {
|
|
871
|
+
const [data, setData] = React.useState(null)
|
|
872
|
+
const [refreshing, setRefreshing] = React.useState(false)
|
|
710
873
|
const version = React.useRef(0)
|
|
711
|
-
const
|
|
712
|
-
const
|
|
713
|
-
|
|
874
|
+
const requested = React.useRef(false)
|
|
875
|
+
const pending = React.useRef(false)
|
|
876
|
+
React.useEffect(() => {
|
|
877
|
+
++version.current
|
|
878
|
+
requested.current = false
|
|
879
|
+
pending.current = false
|
|
880
|
+
setData(null)
|
|
881
|
+
setRefreshing(false)
|
|
882
|
+
const off = readQuota?.subscribe?.((value, origin) => {
|
|
883
|
+
if (origin !== source) return
|
|
884
|
+
if (value === null) {
|
|
885
|
+
++version.current; requested.current = false; pending.current = false
|
|
886
|
+
setData(null); setRefreshing(false)
|
|
887
|
+
} else if (requested.current) setData(value)
|
|
888
|
+
})
|
|
889
|
+
return () => { ++version.current; off?.() }
|
|
890
|
+
}, [readQuota, source, accountRevision])
|
|
891
|
+
const load = async () => {
|
|
892
|
+
if (!readQuota || pending.current) return
|
|
893
|
+
pending.current = true
|
|
894
|
+
requested.current = true
|
|
714
895
|
const current = ++version.current
|
|
715
|
-
|
|
716
|
-
if (action === 'warmup') setPending(true)
|
|
896
|
+
setRefreshing(true)
|
|
717
897
|
try {
|
|
718
|
-
const value = unwrap(await
|
|
719
|
-
if (
|
|
898
|
+
const value = unwrap(await readQuota(true, source))
|
|
899
|
+
if (current === version.current) setData(value)
|
|
720
900
|
} catch (error) {
|
|
721
|
-
if (
|
|
901
|
+
if (current === version.current) {
|
|
902
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
903
|
+
setData(previous => previous?.status === 'ok' ? { ...previous, stale: true, staleReason: message }
|
|
904
|
+
: { status: 'error', groups: [], error: { message } })
|
|
905
|
+
}
|
|
722
906
|
} finally {
|
|
723
|
-
|
|
724
|
-
if (alive.current && action === 'warmup') setPending(false)
|
|
907
|
+
if (current === version.current) { pending.current = false; setRefreshing(false) }
|
|
725
908
|
}
|
|
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 }
|
|
909
|
+
}
|
|
910
|
+
return h('section', { style: PANEL, 'aria-label': '套餐额度查询' },
|
|
911
|
+
h(SectionHeading, { title: '套餐额度', description: (source === PROXY_PROVIDER ? '代理账户' : 'ACP 账户') + (cliQuotaDirty ? ' · CLI 开关未保存' : cliQuotaOn ? ' · CLI 补充取数' : ''),
|
|
912
|
+
action: h('button', { type: 'button', style: ACTION, disabled: !readQuota || refreshing, 'aria-busy': refreshing || undefined, onClick: load }, refreshing ? '正在拉取…' : '拉取额度') }),
|
|
913
|
+
data ? QuotaDetails({ data, refreshing, source }) : h('p', { role: 'status', style: { margin: 0, fontSize: '12px', lineHeight: 1.5, opacity: .65 } }, refreshing ? '正在读取…' : '尚未查询'))
|
|
737
914
|
}
|
|
738
|
-
function
|
|
915
|
+
function QuotaDetails({ data, refreshing, load, model, source, onClose }) {
|
|
916
|
+
return h('div', { style: { display: 'grid', gap: 0 } },
|
|
917
|
+
h('div', { style: { display: 'flex', gap: '8px', alignItems: 'center', paddingBottom: '9px', borderBottom: '1px solid rgba(128,128,128,.16)' } },
|
|
918
|
+
h('strong', { style: { fontSize: '14px', flex: 1 } }, 'Antigravity · 套餐额度'),
|
|
919
|
+
data?.plan ? h('span', { style: { fontSize: '11px', padding: '2px 8px', background: TRACK, borderRadius: '99px' } }, data.plan) : null,
|
|
920
|
+
onClose ? h('button', { type: 'button', 'aria-label': '关闭额度详情', onClick: onClose, style: { ...ACTION, padding: '2px 7px', minHeight: '26px', borderColor: 'transparent' } }, '×') : null),
|
|
921
|
+
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,
|
|
922
|
+
data?.status === 'error' ? h('p', { role: 'alert', style: { margin: '8px 0', color: '#ef4444', fontSize: '11px', overflowWrap: 'anywhere' } }, data.error?.message ?? data.error?.text ?? '额度读取失败') : null,
|
|
923
|
+
...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 + '额度' },
|
|
924
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: '7px', marginBottom: '7px' } },
|
|
925
|
+
h('strong', { style: { fontSize: '12px' } }, group.name),
|
|
926
|
+
group.key === modelGroup(model) ? h('span', { style: { fontSize: '9px', padding: '1px 5px', borderRadius: '5px', color: '#25b879', background: 'rgba(37,184,121,.12)' } }, '当前模型') : null),
|
|
927
|
+
group.buckets.length ? h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(120px, 1fr))', gap: '9px 12px' } }, ...group.buckets.map((bucket, index) => {
|
|
928
|
+
const pct = percentage(bucket)
|
|
929
|
+
return h('div', { key: index, style: { minWidth: 0 } },
|
|
930
|
+
h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: '8px', marginBottom: '4px' } },
|
|
931
|
+
h('span', { style: { fontSize: '11px', opacity: .7 } }, windowName(bucket)),
|
|
932
|
+
h('strong', { style: { fontSize: bucket.disabled ? '12px' : '15px', fontVariantNumeric: 'tabular-nums' } }, bucket.disabled ? '已停用' : pct === undefined ? '未知' : Math.round(pct) + '%')),
|
|
933
|
+
remainingBar(bucket, false, data?.stale, group.name + ' ' + windowName(bucket) + '剩余额度'),
|
|
934
|
+
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)))
|
|
935
|
+
})) : h('span', { style: { opacity: .55, fontSize: '11px' } }, data ? '未提供额度数据' : '正在读取…'))),
|
|
936
|
+
CreditsDetails({ credits: data?.credits, pending: !data }),
|
|
937
|
+
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 默认不查询;打开「用 agy CLI 查询额度」后才读取,且来自官方 CLI 账户,可能不是同一账户。') : null,
|
|
938
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px', paddingTop: '8px' } },
|
|
939
|
+
h('small', { style: { flex: 1, opacity: .5, minWidth: 0, fontSize: '10px' } }, data?.updatedAt ? '更新于 ' + new Date(data.updatedAt).toLocaleString() : '尚无成功快照'),
|
|
940
|
+
load ? h('button', { type: 'button', disabled: refreshing, onClick: () => load(true), style: { ...ACTION, minHeight: '27px', padding: '3px 9px', fontSize: '11px' } }, refreshing ? '刷新中…' : '刷新额度') : null))
|
|
941
|
+
}
|
|
942
|
+
/** 紧凑余量条与单词状态的交互参考 dsh-subusage;取数仍由本插件负责。 */
|
|
943
|
+
function quotaPillStatus(data, buckets) {
|
|
944
|
+
if (!data) return { short: '', icon: '…', color: '#8b909a' }
|
|
945
|
+
if (data.status === 'error') return { short: '失败', icon: '!', color: '#e5534b', tint: true }
|
|
946
|
+
if (data.stale) return { short: '缓存', icon: '!', color: '#d99b28', tint: true }
|
|
947
|
+
const usable = buckets.filter(bucket => !bucket.disabled && percentage(bucket) !== undefined)
|
|
948
|
+
if (!usable.length) return { short: buckets.some(bucket => bucket.disabled) ? '受限' : '未知', icon: '?', color: '#8b909a' }
|
|
949
|
+
const minimum = Math.min(...usable.map(percentage))
|
|
950
|
+
if (minimum === 0) return { short: '已用尽', color: '#e5534b', tint: true }
|
|
951
|
+
if (minimum <= 10) return { short: '紧张', color: '#e5534b', tint: true }
|
|
952
|
+
if (minimum <= 25) return { short: '偏少', color: '#d99b28', tint: true }
|
|
953
|
+
return { short: '', color: '#25b879' }
|
|
954
|
+
}
|
|
955
|
+
function AgYQuotaPill({ store, readQuota, proxyControl }) {
|
|
739
956
|
const state = React.useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot)
|
|
740
|
-
const
|
|
957
|
+
const isAcp = state?.current?.provider === PROVIDER
|
|
958
|
+
const isProxy = state?.current?.provider === PROXY_PROVIDER
|
|
959
|
+
const enabled = isAcp || isProxy
|
|
741
960
|
const model = state?.current?.model
|
|
742
|
-
const quota = useQuota(readQuota, enabled)
|
|
961
|
+
const quota = useQuota(readQuota, enabled, state?.current?.provider, model)
|
|
743
962
|
const [expanded, setExpanded] = React.useState(false)
|
|
744
963
|
const [position, setPosition] = React.useState(null)
|
|
745
964
|
const root = React.useRef(null)
|
|
746
965
|
const button = React.useRef(null)
|
|
747
|
-
|
|
966
|
+
React.useEffect(() => {
|
|
967
|
+
if (!isProxy || !proxyControl) return
|
|
968
|
+
let active = true
|
|
969
|
+
let pending = false
|
|
970
|
+
const check = async () => {
|
|
971
|
+
if (pending) return
|
|
972
|
+
pending = true
|
|
973
|
+
try {
|
|
974
|
+
const value = unwrap(await proxyControl('account', { action: 'status' }))
|
|
975
|
+
if (active) readQuota?.observeProxyAccount?.(value.snapshot)
|
|
976
|
+
} catch { /* 账户轮询失败不覆盖额度查询自身的结果。 */ }
|
|
977
|
+
finally { pending = false }
|
|
978
|
+
}
|
|
979
|
+
void check()
|
|
980
|
+
const timer = setInterval(() => {
|
|
981
|
+
if (typeof document === 'undefined' || document.visibilityState !== 'hidden') void check()
|
|
982
|
+
}, 3000)
|
|
983
|
+
return () => { active = false; clearInterval(timer) }
|
|
984
|
+
}, [isProxy, proxyControl, readQuota])
|
|
748
985
|
React.useEffect(() => { setExpanded(false) }, [enabled])
|
|
749
986
|
React.useEffect(() => {
|
|
750
987
|
if (!expanded) { setPosition(null); return undefined }
|
|
@@ -774,36 +1011,26 @@ window.__ModuleLoader__.load({
|
|
|
774
1011
|
}, [expanded])
|
|
775
1012
|
if (!enabled) return null
|
|
776
1013
|
const groups = quotaGroups(quota.data)
|
|
777
|
-
const family = modelGroup(model)
|
|
1014
|
+
const family = modelGroup(quota.data?.routeModel ?? model)
|
|
778
1015
|
// 官方 /usage 可能只报告 Gemini 共享额度;只有真实 Argon 分组存在时,
|
|
779
1016
|
// 才把 Argon 型号绑定到独立组,不能命中一个预建的空组而误报未知。
|
|
780
1017
|
const group = groups.find((g) => g.key === family && (family !== 'argon' || g.buckets.length > 0))
|
|
781
1018
|
?? (family === 'argon' ? groups.find((g) => g.key === 'gemini') : undefined)
|
|
782
1019
|
const buckets = group?.buckets ?? []
|
|
1020
|
+
const pillStatus = quotaPillStatus(quota.data, buckets)
|
|
1021
|
+
const miniBuckets = buckets.slice(0, 5)
|
|
1022
|
+
const miniHeight = miniBuckets.length <= 1 ? 6 : miniBuckets.length === 2 ? 5 : miniBuckets.length === 3 ? 4 : miniBuckets.length === 4 ? 3 : 2
|
|
1023
|
+
const modeTag = isProxy ? '代理' : 'ACP'
|
|
783
1024
|
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' } },
|
|
1025
|
+
const title = modeTag + ' · ' + String(model ?? '未选择模型') + ' · ' + label + (quota.data?.stale ? ' · 缓存数据' : '')
|
|
1026
|
+
return h('span', { ref: root, className: 'antigravity-usage-pill', 'data-expanded': String(expanded), style: { position: 'relative', display: 'inline-flex', alignItems: 'center' } },
|
|
796
1027
|
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)
|
|
1028
|
+
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)' } },
|
|
1029
|
+
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))))
|
|
1030
|
+
: 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),
|
|
1031
|
+
pillStatus.short ? h('span', null, pillStatus.short) : null),
|
|
1032
|
+
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 } },
|
|
1033
|
+
QuotaDetails({ ...quota, model, source: state?.current?.provider, onClose: () => { setExpanded(false); button.current?.focus?.() } })) : null)
|
|
807
1034
|
}
|
|
808
1035
|
|
|
809
1036
|
/** 给 promise 加超时:Host 侧异常时界面也要落到可读错误,而不是永远 pending。 */
|
|
@@ -820,7 +1047,11 @@ window.__ModuleLoader__.load({
|
|
|
820
1047
|
/** 组装额度读取器:remote.agyQuota 由 Host 注册后才出现,短轮询等待就绪。 */
|
|
821
1048
|
function makeQuotaReader(scope) {
|
|
822
1049
|
const listeners = new Set()
|
|
823
|
-
const
|
|
1050
|
+
const epochs = new Map()
|
|
1051
|
+
const modelsBySource = new Map()
|
|
1052
|
+
let proxyAccountKey
|
|
1053
|
+
let mode
|
|
1054
|
+
const read = async (force, source, model) => {
|
|
824
1055
|
const deadline = Date.now() + 20_000
|
|
825
1056
|
for (;;) {
|
|
826
1057
|
// **只能用 `get()`**:`scope.remote.agyQuota` 这种属性访问在未 inject 的
|
|
@@ -832,7 +1063,11 @@ window.__ModuleLoader__.load({
|
|
|
832
1063
|
// Host 侧会起官方 agy CLI 取数(其启动含登录刷新)。万一它挂在网络上,
|
|
833
1064
|
// 也要让药丸显示「读取失败 + 原因」,而不是一直停在「额度…」。
|
|
834
1065
|
return await withTimeout(
|
|
835
|
-
service.read(
|
|
1066
|
+
service.read({
|
|
1067
|
+
...(force ? { force: true } : {}),
|
|
1068
|
+
...(source === PROVIDER ? { source: 'acp' } : source === PROXY_PROVIDER ? { source: 'proxy' } : {}),
|
|
1069
|
+
...(model ? { model } : {}),
|
|
1070
|
+
}),
|
|
836
1071
|
60_000,
|
|
837
1072
|
'额度读取超时(Host 侧 agy CLI 60 秒内没有返回)',
|
|
838
1073
|
)
|
|
@@ -843,12 +1078,38 @@ window.__ModuleLoader__.load({
|
|
|
843
1078
|
await new Promise((resolve) => setTimeout(resolve, 500))
|
|
844
1079
|
}
|
|
845
1080
|
}
|
|
846
|
-
const reader = async (force) => {
|
|
847
|
-
|
|
848
|
-
|
|
1081
|
+
const reader = async (force, source, model) => {
|
|
1082
|
+
if (model) modelsBySource.set(source, model)
|
|
1083
|
+
const requestedModel = model ?? modelsBySource.get(source)
|
|
1084
|
+
const epoch = epochs.get(source) ?? 0
|
|
1085
|
+
const value = unwrap(await read(force, source, requestedModel))
|
|
1086
|
+
if (epoch !== (epochs.get(source) ?? 0)) throw new Error('账户已变化,请重新拉取额度')
|
|
1087
|
+
for (const listener of listeners) {
|
|
1088
|
+
if (source === undefined) listener(value)
|
|
1089
|
+
else listener(value, source)
|
|
1090
|
+
}
|
|
849
1091
|
return value
|
|
850
1092
|
}
|
|
851
1093
|
reader.subscribe = (listener) => { listeners.add(listener); return () => listeners.delete(listener) }
|
|
1094
|
+
reader.invalidate = (source) => {
|
|
1095
|
+
epochs.set(source, (epochs.get(source) ?? 0) + 1)
|
|
1096
|
+
for (const listener of listeners) listener(null, source)
|
|
1097
|
+
}
|
|
1098
|
+
reader.observeProxyAccount = (snapshot) => {
|
|
1099
|
+
if (!snapshot) return
|
|
1100
|
+
const key = JSON.stringify([snapshot.revision, snapshot.phase === 'signed-in', snapshot.email])
|
|
1101
|
+
if (proxyAccountKey !== undefined && key !== proxyAccountKey) {
|
|
1102
|
+
proxyAccountKey = key
|
|
1103
|
+
reader.invalidate(PROXY_PROVIDER)
|
|
1104
|
+
} else proxyAccountKey = key
|
|
1105
|
+
}
|
|
1106
|
+
reader.observeMode = (source) => {
|
|
1107
|
+
if (mode !== undefined && mode !== source) {
|
|
1108
|
+
mode = source
|
|
1109
|
+
reader.invalidate(PROVIDER)
|
|
1110
|
+
reader.invalidate(PROXY_PROVIDER)
|
|
1111
|
+
} else mode = source
|
|
1112
|
+
}
|
|
852
1113
|
return reader
|
|
853
1114
|
}
|
|
854
1115
|
|
|
@@ -869,6 +1130,22 @@ window.__ModuleLoader__.load({
|
|
|
869
1130
|
}
|
|
870
1131
|
}
|
|
871
1132
|
|
|
1133
|
+
/** 代理账户控制读取器:remote.agyProxyAccount 由 Host 的代理行注册。 */
|
|
1134
|
+
function makeProxyControlReader(scope) {
|
|
1135
|
+
return async (method, request) => {
|
|
1136
|
+
if (method !== 'account') throw new Error('不支持的代理账户操作')
|
|
1137
|
+
const deadline = Date.now() + 20_000
|
|
1138
|
+
for (;;) {
|
|
1139
|
+
const service = scope.get('remote.agyProxyAccount')
|
|
1140
|
+
if (typeof service?.account === 'function') {
|
|
1141
|
+
return await withTimeout(service.account(request), 60_000, '代理账户操作超时,请重试')
|
|
1142
|
+
}
|
|
1143
|
+
if (Date.now() >= deadline) throw new Error('代理账户服务未就绪:请确认已启用本插件的代理路由组件(antigravity-proxy-router)')
|
|
1144
|
+
await new Promise((resolve) => setTimeout(resolve, 500))
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
872
1149
|
/**
|
|
873
1150
|
* 客户端侧的额度 remote 声明。
|
|
874
1151
|
*
|
|
@@ -882,16 +1159,31 @@ window.__ModuleLoader__.load({
|
|
|
882
1159
|
*/
|
|
883
1160
|
const AGY_QUOTA_PACKAGE = 'dsh-bundle-antigravity-host'
|
|
884
1161
|
const AGY_QUOTA_SERVICE = 'agyQuota'
|
|
1162
|
+
const AGY_PROXY_ACCOUNT_SERVICE = 'agyProxyAccount'
|
|
885
1163
|
|
|
886
1164
|
function strictCodec(typeSymbol, parse) {
|
|
887
1165
|
const schema = { parse }
|
|
888
1166
|
return { mode: 'strict', typeSymbol, create: () => schema }
|
|
889
1167
|
}
|
|
890
1168
|
|
|
1169
|
+
function parseProxyAccountRequest(value) {
|
|
1170
|
+
if (value === undefined || value === null) return {}
|
|
1171
|
+
if (typeof value !== 'object' || Array.isArray(value)) throw new TypeError('Invalid AgYProxyAccountRequest')
|
|
1172
|
+
if (value.action !== undefined && !['status', 'login', 'cancel', 'logout'].includes(value.action)) throw new TypeError('Invalid AgYProxyAccountRequest.action')
|
|
1173
|
+
return value
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
function parseProxyAccountResult(value) {
|
|
1177
|
+
if (!value || typeof value !== 'object' || typeof value.message !== 'string' || !value.snapshot || typeof value.snapshot !== 'object') throw new TypeError('Invalid AgYProxyAccountResult')
|
|
1178
|
+
return value
|
|
1179
|
+
}
|
|
1180
|
+
|
|
891
1181
|
function parseQuotaRequest(value) {
|
|
892
1182
|
if (value === undefined || value === null) return undefined
|
|
893
1183
|
if (typeof value !== 'object' || Array.isArray(value)) throw new TypeError('Invalid AgYQuotaRequest')
|
|
894
1184
|
if (value.force !== undefined && typeof value.force !== 'boolean') throw new TypeError('Invalid AgYQuotaRequest.force')
|
|
1185
|
+
if (value.source !== undefined && !['acp', 'proxy'].includes(value.source)) throw new TypeError('Invalid AgYQuotaRequest.source')
|
|
1186
|
+
if (value.model !== undefined && (typeof value.model !== 'string' || !value.model.trim())) throw new TypeError('Invalid AgYQuotaRequest.model')
|
|
895
1187
|
return value
|
|
896
1188
|
}
|
|
897
1189
|
|
|
@@ -942,18 +1234,44 @@ window.__ModuleLoader__.load({
|
|
|
942
1234
|
],
|
|
943
1235
|
}
|
|
944
1236
|
|
|
1237
|
+
const AGY_PROXY_REMOTE = {
|
|
1238
|
+
package: AGY_QUOTA_PACKAGE,
|
|
1239
|
+
descriptors: [
|
|
1240
|
+
{
|
|
1241
|
+
id: `${AGY_QUOTA_PACKAGE}#${AGY_PROXY_ACCOUNT_SERVICE}/account`,
|
|
1242
|
+
service: AGY_PROXY_ACCOUNT_SERVICE,
|
|
1243
|
+
namespace: AGY_PROXY_ACCOUNT_SERVICE,
|
|
1244
|
+
method: 'account',
|
|
1245
|
+
invocation: { kind: 'direct' },
|
|
1246
|
+
parameters: [{ name: 'request', wire: 'request', source: 'json', acceptsUndefined: true, codec: strictCodec(`${AGY_QUOTA_PACKAGE}#AgYProxyAccountRequest`, parseProxyAccountRequest) }],
|
|
1247
|
+
result: strictCodec(`${AGY_QUOTA_PACKAGE}#AgYProxyAccountResult`, parseProxyAccountResult),
|
|
1248
|
+
},
|
|
1249
|
+
],
|
|
1250
|
+
}
|
|
1251
|
+
|
|
945
1252
|
return {
|
|
946
1253
|
name: 'dsh-bundle-antigravity-host',
|
|
947
1254
|
// remote.settings / remote.llm 必须逐个声明:remote 各命名空间是独立服务,
|
|
948
1255
|
// 漏声明会在读取属性时抛「without inject」并击穿 settings.section 卡片。
|
|
949
1256
|
inject: ['slots', 'remote', 'remote.settings', 'remote.llm'],
|
|
950
1257
|
apply(ctx) {
|
|
1258
|
+
ctx.effect(() => {
|
|
1259
|
+
if (typeof document === 'undefined' || !document.head || !document.createElement) return () => {}
|
|
1260
|
+
const style = document.createElement('style')
|
|
1261
|
+
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}'
|
|
1262
|
+
document.head.appendChild(style)
|
|
1263
|
+
return () => style.remove()
|
|
1264
|
+
}, 'antigravity: scoped control styles')
|
|
951
1265
|
const readQuota = makeQuotaReader(ctx)
|
|
952
1266
|
const control = makeControlReader(ctx)
|
|
1267
|
+
const proxyControl = makeProxyControlReader(ctx)
|
|
953
1268
|
// 挂载本包贡献的 remote(幂等,随 fiber 一起 dispose)。挂载失败不影响其它卡片:
|
|
954
1269
|
// 药丸会自行超时并显示「额度服务未就绪」的原因。
|
|
955
1270
|
try {
|
|
956
|
-
const mounted = ctx.remote.$mount(
|
|
1271
|
+
const mounted = ctx.remote.$mount({
|
|
1272
|
+
package: AGY_QUOTA_PACKAGE,
|
|
1273
|
+
descriptors: [...AGY_QUOTA_REMOTE.descriptors, ...AGY_PROXY_REMOTE.descriptors],
|
|
1274
|
+
})
|
|
957
1275
|
ctx.effect(() => mounted, 'antigravity-acp: mount quota remote')
|
|
958
1276
|
} catch (error) {
|
|
959
1277
|
console.error('[dsh-bundle-antigravity-host] 额度 remote 客户端挂载失败:', error)
|
|
@@ -967,7 +1285,7 @@ window.__ModuleLoader__.load({
|
|
|
967
1285
|
id: 'antigravity-acp',
|
|
968
1286
|
order: 42,
|
|
969
1287
|
label: () => 'Antigravity ACP',
|
|
970
|
-
inject: () => ({ remote: ctx.remote, control }),
|
|
1288
|
+
inject: () => ({ remote: ctx.remote, control, proxyControl, readQuota }),
|
|
971
1289
|
},
|
|
972
1290
|
AntigravitySettingsCard,
|
|
973
1291
|
),
|
|
@@ -995,7 +1313,7 @@ window.__ModuleLoader__.load({
|
|
|
995
1313
|
inject: (sessionId) => ({
|
|
996
1314
|
store: scope.modelDirectories.directoryFor(sessionId).store,
|
|
997
1315
|
readQuota,
|
|
998
|
-
|
|
1316
|
+
proxyControl,
|
|
999
1317
|
}),
|
|
1000
1318
|
},
|
|
1001
1319
|
AgYQuotaPill,
|