opencode-claude-code-ua 0.3.6 → 0.3.9

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/ui/pro.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createTestPlan, createTestSample, validateTestSample } from '../lib/ua-test-plan.mjs'
2
- import { protocolForModel } from '../lib/ua-protocol.mjs'
2
+ import { protocolForModel, matchesAccountSite } from '../lib/ua-protocol.mjs'
3
3
 
4
4
  export const PROTOCOLS = ['anthropic', 'responses', 'chat-completions', 'gemini']
5
5
  export const OPERATIONS = { anthropic: ['messages', 'count_tokens'], responses: ['responses'], 'chat-completions': ['chat/completions'], gemini: ['generateContent', 'streamGenerateContent', 'countTokens'] }
@@ -30,6 +30,41 @@ export function jsonObject(text, label) {
30
30
  return value
31
31
  }
32
32
  export function lines(text) { return [...new Set(String(text).split(/\r?\n/).map(value => value.trim()).filter(Boolean))] }
33
+ export const ACCOUNT_BINDING_EMPTY = '不绑定签到账号(仅查询 Key 用量)'
34
+ /** Same-site comparison for the optional balance/check-in account binding. */
35
+ export function siteRoot(value) {
36
+ const text = String(value ?? '').trim().replace(/\/+$/, '')
37
+ if (!text) return ''
38
+ return text.replace(/\/v1(?:beta)?$/i, '').replace(/\/+$/, '')
39
+ }
40
+ export function connectionAccountChoices(accounts = [], baseURL = '', selectedId = '') {
41
+ const list = (Array.isArray(accounts) ? accounts : []).filter(item => item && item.id)
42
+ const root = siteRoot(baseURL)
43
+ const matches = item => { try { return matchesAccountSite(baseURL, item) } catch { return false } }
44
+ const same = root ? list.filter(matches) : []
45
+ const pool = same.length ? same : list
46
+ const selected = selectedId === null || selectedId === undefined ? '' : String(selectedId)
47
+ const selectedAccount = list.find(item => String(item.id) === selected)
48
+ const choices = pool.map(item => ({
49
+ value: String(item.id),
50
+ label: `${item.name ?? item.id} · ${item.siteUrl ?? ''}${item.enabled === false ? '(已停用)' : ''}`,
51
+ }))
52
+ const invalid = Boolean(selected && (!selectedAccount || (root !== '' && !matches(selectedAccount))))
53
+ if (invalid) {
54
+ const label = selectedAccount
55
+ ? `当前绑定:${selectedAccount.name} · ${selectedAccount.siteUrl}(属于其他站点或已失效,请重新选择)`
56
+ : '当前绑定(已失效或属于其他站点,请重新选择)'
57
+ const existing = choices.findIndex(item => item.value === selected)
58
+ if (existing >= 0) choices.splice(existing, 1)
59
+ choices.unshift({ value: selected, label, invalid: true })
60
+ }
61
+ return { mode: same.length ? 'same-site' : 'all', choices, selected, invalid }
62
+ }
63
+ export function connectionAccountBindingLabel(accountId, accounts = []) {
64
+ if (!accountId) return '余额与签到:仅查询 Key 用量(未绑定站点账号)'
65
+ const account = (Array.isArray(accounts) ? accounts : []).find(item => String(item.id) === String(accountId))
66
+ return account ? `余额与签到:${account.name} · ${account.siteUrl}` : '余额与签到:绑定账号已失效,请编辑连接重新选择'
67
+ }
33
68
  export function fillSelectedModels(current, selected, replace = false) {
34
69
  const ids = selected.filter(id => typeof id === 'string' && id.trim()).map(id => id.trim())
35
70
  return [...new Set([...(replace ? [] : lines(current)), ...ids])].join('\n')
@@ -100,6 +135,22 @@ export function testModelOptions(connection, catalog) {
100
135
  export function selectTestModel(models, current = '') {
101
136
  return models.some(model => model.id === current) ? current : models[0]?.id ?? ''
102
137
  }
138
+ export function connectionHostView(state = {}, connection = {}) {
139
+ const provider = connection.independent && connection.siteId ? `ua-pro-${connection.siteId}` : 'ua-pro'
140
+ const host = state.runtime?.connections?.find(item => item.connectionId === connection.id)
141
+ if (connection.enabled === false) return { label: '连接已停用', detail: `${provider} · 此连接不参与模型请求` }
142
+ if (state.runtime?.mode !== 'host') return { label: '仅本地保存,未同步到宿主', detail: `在 Magpie 打开 UA PRO 管理并同步后,连接位于 ${provider} 的账号列表` }
143
+ if (host?.status === 'registered') return { label: '已注册到宿主', detail: `Magpie 供应商 ${host.provider || provider} · 账号 ${connection.name || host.account};请刷新宿主模型列表` }
144
+ if (host?.status === 'reload-required') return { label: '需重载插件', detail: `新增供应商 ${host.provider || provider} 尚未载入,请在 Magpie 重载 UA PRO` }
145
+ return { label: host?.status === 'conflict' ? '账号冲突,未覆盖' : '尚未同步到宿主', detail: host?.message || `点击“同步到宿主”注册到 ${provider} 的账号列表` }
146
+ }
147
+ export function connectionSaveMessage(result = {}) {
148
+ if (result.message) return result.message
149
+ if (result.registration === 'registered') return `连接已保存并注册到宿主 ${result.host?.provider || 'UA PRO'};在供应商的账号列表中查看并刷新模型`
150
+ if (result.registration === 'pending-reload') return '连接已保存,宿主尚未确认;请同步到宿主或重载 UA PRO 插件'
151
+ return '连接已保存到本地;测试成功不代表已同步,请在 Magpie 打开 UA PRO 管理并同步到宿主'
152
+ }
153
+
103
154
  export function gatewayModelReference(connection, model) {
104
155
  const provider = connection?.independent && connection.siteId ? `ua-pro-${connection.siteId}` : 'ua-pro'
105
156
  return model ? `${provider}/${model}` : ''
@@ -131,10 +182,10 @@ export async function requestOperation(fetcher, action, payload, operationId) {
131
182
  if (typeof document !== 'undefined') void mount()
132
183
  async function mount() {
133
184
  const $ = id => document.getElementById(id)
134
- let state = { templates: [], connections: [], generation: 0, runtime: {} }
185
+ let state = { templates: [], connections: [], siteAccounts: [], generation: 0, runtime: {} }
135
186
  let view = 'overview', loading = false, pending = null, operation = null
136
187
  let connection = null, connectionBaseline = '', template = null, templateBaseline = '', templateOrigin = null
137
- let migration = null, dirtyConnection = false, dirtyTemplate = false
188
+ let dirtyConnection = false, dirtyTemplate = false
138
189
  let modelQuery = null, modelQueryReady = false, modelResultShown = false
139
190
  let testModelQuery = null, testModels = [], testCatalog = null, testConnectionID = '', testConnectionRevision, testModelManual = false, testSampleBaseline = '', testProtocolExplicit = false
140
191
  let testSelectionInitialized = false
@@ -196,7 +247,7 @@ async function mount() {
196
247
  if (!result.ok) { showError(operationErrorText(result)); status('操作未完成,草稿保留'); return }
197
248
  await refresh()
198
249
  if (complete) await complete(result.data)
199
- status(action === 'test' ? result.data.result?.status === 'passed' ? '测试完成:通过' : '测试完成:未通过,请查看本次原因' : action === 'routes/apply' ? '模型路由已保存,请在 Magpie 重新获取模型或重载插件' : result.data.activation ? '已保存绑定;等待宿主后续请求确认使用版本' : '操作完成')
250
+ status(action === 'test' ? result.data.result?.status === 'passed' ? '测试完成:通过' : '测试完成:未通过,请查看本次原因' : action === 'connections/save' ? connectionSaveMessage(result.data) : action === 'connections/sync' ? result.data.message || '宿主同步完成,请刷新 Magpie 供应商和模型列表' : action === 'routes/apply' ? '模型路由已保存,请在 Magpie 重新获取模型或重载插件' : result.data.activation ? '已保存绑定;等待宿主后续请求确认使用版本' : '操作完成')
200
251
  }
201
252
  async function recover() {
202
253
  if (!pending || loading) return
@@ -229,6 +280,7 @@ async function mount() {
229
280
  }
230
281
  function renderState() {
231
282
  text('pro-generation', state.generation)
283
+ text('pro-version', state.application?.version || '版本未知,请重新打开插件管理')
232
284
  text('pro-runtime', state.runtime?.mode === 'host' ? 'Magpie 插件宿主' : '独立配置工具')
233
285
  $('overview-metrics').replaceChildren(...[
234
286
  [state.connections.filter(item => item.enabled).length, '启用连接'],
@@ -236,8 +288,15 @@ async function mount() {
236
288
  [state.runtime?.observed?.length ?? 0, '宿主已观察连接'],
237
289
  ].map(([count, label]) => { const item = node('div', undefined, 'metric'); item.append(node('span', label), node('strong', count)); return item }))
238
290
  $('overview-empty').hidden = state.connections.length > 0
239
- $('overview-connections').replaceChildren(...state.connections.map(item => { const row = node('div', undefined, 'overview-item'); const info = node('div'); info.append(node('strong', item.name), node('small', item.baseURL), node('small', bindingLabel(item.binding))); const seen = state.runtime?.observed?.find(entry => entry.connectionId === item.id); row.append(info, tag(seen ? `观察 generation ${seen.generation} · ${seen.templateId ?? '普通转发'} @ ${seen.revision ?? '—'}` : '等待宿主观察')); return row }))
240
- $('connection-list').replaceChildren(...state.connections.map(item => { const card = node('article', undefined, 'conn-card'); card.append(node('h3', item.name), tag(item.enabled ? '启用' : '停用'), node('p', `${item.baseURL}\n${item.protocol} · ${item.auth.source === 'host' ? '宿主凭据' : '本地连接凭据'}`), node('p', bindingLabel(item.binding))); const actions = node('div', undefined, 'card-actions'); actions.append(button('编辑', () => editConnection(item)), button('应用模板', () => openBinding([item.id])), button('测试', () => openTest(item.id))); card.append(actions); return card }))
291
+ $('host-sync').hidden = state.runtime?.mode !== 'host'
292
+ $('overview-connections').replaceChildren(...state.connections.map(item => {
293
+ const row = node('div', undefined, 'overview-item'), info = node('div'), host = connectionHostView(state, item)
294
+ info.append(node('strong', item.name), node('small', item.baseURL), node('small', bindingLabel(item.binding)), node('small', host.detail))
295
+ const seen = state.runtime?.observed?.find(entry => entry.connectionId === item.id)
296
+ row.append(info, tag(seen ? `已用于请求 · generation ${seen.generation}` : host.label))
297
+ return row
298
+ }))
299
+ $('connection-list').replaceChildren(...state.connections.map(item => { const card = node('article', undefined, 'conn-card'); card.append(node('h3', item.name), tag(item.enabled ? '启用' : '停用'), node('p', `${item.baseURL}\n${item.protocol} · ${item.auth.source === 'host' ? '宿主凭据' : '本地连接凭据'}`), node('p', bindingLabel(item.binding)), node('p', connectionAccountBindingLabel(item.newApiAccountId, state.siteAccounts), 'hint'), (() => { const link = node('a', '管理签到账号 / 补 Cookie 或登录凭据'); link.href = './accounts'; link.addEventListener('click', event => { if (!allowLeave()) event.preventDefault() }); return link })()); const actions = node('div', undefined, 'card-actions'); actions.append(button('编辑', () => editConnection(item)), button('应用模板', () => openBinding([item.id])), button('测试', () => openTest(item.id))); card.append(actions); return card }))
241
300
  renderTemplates()
242
301
  renderConnectionSelectors()
243
302
  $('session-info').replaceChildren(...Object.entries(state.session ?? {}).map(([key, value]) => { const item = node('div'); item.append(node('dt', key), node('dd', value)); return item }))
@@ -275,7 +334,7 @@ async function mount() {
275
334
  function connectionDraft() {
276
335
  const auth = { ...(connection?.auth ?? { source: 'stored' }), mode: $('conn-auth-mode').value, key: $('conn-key').value, value: $('conn-value').value }
277
336
  const result = { ...(connection ? clone(connection) : {}), name: $('conn-name').value.trim(), baseURL: $('conn-base').value.trim(), protocol: $('conn-protocol').value,
278
- auth, models: lines($('conn-models').value), modelProtocols: modelProtocolLines($('conn-model-protocols').value), apiPaths: {}, enabled: $('conn-enabled').checked, independent: $('conn-independent').checked, siteId: $('conn-site').value.trim() }
337
+ auth, models: lines($('conn-models').value), modelProtocols: modelProtocolLines($('conn-model-protocols').value), apiPaths: {}, enabled: $('conn-enabled').checked, independent: $('conn-independent').checked, siteId: $('conn-site').value.trim(), newApiAccountId: $('conn-site-account').value }
279
338
  delete result.secrets
280
339
  for (const control of document.querySelectorAll('[data-api-path]')) if (control.value.trim()) result.apiPaths[control.dataset.apiPath] = control.value.trim()
281
340
  if (!connection) {
@@ -290,7 +349,7 @@ async function mount() {
290
349
  $('conn-value-field').hidden = host || mode !== 'custom'
291
350
  $('conn-host-note').hidden = !host
292
351
  $('conn-auth-mode').disabled = Boolean(host)
293
- text('conn-secret-state', host ? '凭据由 Magpie 保存,不会复制到模板。' : connection?.secrets.key || connection?.secrets.value ? '已有凭据;同一目标留空保留。换站需输入新凭据。' : '输入此连接自己的模型凭据。')
352
+ text('conn-secret-state', host ? '凭据由 Magpie 保存,不会复制到模板。' : connection?.secrets?.key || connection?.secrets?.value ? '已有凭据;同一目标留空保留。换站需输入新凭据。' : '输入此连接自己的模型凭据。')
294
353
  $('conn-clear-field').hidden = !connection || host
295
354
  }
296
355
  function resetModelQuery(message = '使用上方地址、认证方式和密钥读取模型,无需先保存;不会发送生成请求。') {
@@ -323,7 +382,7 @@ async function mount() {
323
382
  if (!draft.baseURL) { text('conn-model-status', '请先填写上方站点地址。'); $('conn-base').focus(); return }
324
383
  if (draft.auth.source !== 'host' && draft.auth.mode !== 'none') {
325
384
  const field = draft.auth.mode === 'custom' ? 'value' : 'key'
326
- const saved = connection?.secrets[field] && connection.baseURL.replace(/\/+$/, '') === draft.baseURL.replace(/\/+$/, '') && !$('conn-clear-secret').checked
385
+ const saved = connection?.secrets?.[field] && connection.baseURL.replace(/\/+$/, '') === draft.baseURL.replace(/\/+$/, '') && !$('conn-clear-secret').checked
327
386
  if (!draft.auth[field] && !saved) { text('conn-model-status', '请先填写上方当前站点的密钥或完整 Authorization。'); return }
328
387
  }
329
388
  resetModelQuery('正在读取模型列表…')
@@ -373,6 +432,16 @@ async function mount() {
373
432
  $('conn-models').dispatchEvent(new Event('input', { bubbles: true }))
374
433
  text('conn-model-status', `已${replace ? '替换为' : '追加'}选中的 ${selected.length} 个模型;点击“保存连接”才会保存。`)
375
434
  }
435
+ function renderConnectionAccountOptions(selectedId = $('conn-site-account').value) {
436
+ const result = connectionAccountChoices(state.siteAccounts ?? [], $('conn-base').value, selectedId)
437
+ options($('conn-site-account'), [['', ACCOUNT_BINDING_EMPTY], ...result.choices.map(item => [item.value, item.label])], result.selected)
438
+ text('conn-site-account-hint', result.invalid
439
+ ? '当前绑定已失效或属于其他站点,已保留选项等待重新选择;保存时后端会拒绝无效绑定。'
440
+ : result.mode === 'same-site'
441
+ ? '仅列出与该连接站点根地址相同的账号;只影响余额与签到,不改变模型请求和凭据。'
442
+ : '未找到同站点账号,暂显示全部;保存时后端仍会校验站点根地址一致。')
443
+ return result
444
+ }
376
445
  function editConnection(item = null) {
377
446
  if (!allowLeave()) return
378
447
  resetModelQuery()
@@ -382,6 +451,7 @@ async function mount() {
382
451
  text('connection-form-title', item ? '编辑连接' : '添加连接')
383
452
  for (const [id, value] of Object.entries({ 'conn-name': item?.name ?? '', 'conn-base': item?.baseURL ?? '', 'conn-protocol': item?.protocol ?? 'auto', 'conn-auth-mode': item?.auth.mode ?? 'api-key', 'conn-key': '', 'conn-value': '', 'conn-models': (item?.models ?? []).join('\n'), 'conn-model-protocols': Object.entries(item?.modelProtocols ?? {}).map(([key, value]) => `${key} = ${value}`).join('\n'), 'conn-site': item?.siteId ?? '' })) $(id).value = value
384
453
  $('conn-enabled').checked = item?.enabled !== false; $('conn-independent').checked = Boolean(item?.independent); $('conn-clear-secret').checked = false
454
+ renderConnectionAccountOptions(item?.newApiAccountId ?? '')
385
455
  for (const control of document.querySelectorAll('[data-api-path]')) control.value = item?.apiPaths?.[control.dataset.apiPath] ?? ''
386
456
  $('connection-remove').hidden = !item; $('conn-binding-field').hidden = Boolean(item); $('conn-binding-note').hidden = !item
387
457
  $('conn-binding').value = 'auto'; text('connection-form-error', ''); updateAuthFields()
@@ -720,11 +790,13 @@ async function mount() {
720
790
  for (const link of document.querySelectorAll('[data-view-link]')) link.addEventListener('click', event => { event.preventDefault(); navigate(link.dataset.viewLink) })
721
791
  for (const link of document.querySelectorAll('[data-leave]')) link.addEventListener('click',event => { if (!allowLeave()) event.preventDefault() })
722
792
  for (const control of document.querySelectorAll('[data-action="connection-new"]')) control.addEventListener('click', () => editConnection())
793
+ $('host-sync').addEventListener('click', () => { if (allowLeave()) void run('connections/sync', {}) })
723
794
  $('pro-reload').addEventListener('click', async () => { if (!allowLeave()) return; try { await refresh(false); status('已重新读取') } catch (error) { showError(error) } })
724
795
  $('pro-recover').addEventListener('click', recover)
725
796
  $('pro-close').addEventListener('click', async () => { if (!allowLeave()) return; await fetch('./api/cancel', { method:'POST',headers:{'content-type':'application/json'},body:'{}' }); document.body.replaceChildren(node('main','配置已关闭。此前保存的更改已保留。','empty')) })
726
797
  $('connection-form').addEventListener('input', () => { try { dirtyConnection = JSON.stringify(connectionDraft()) !== connectionBaseline } catch { dirtyConnection = true } text('connection-form-state',dirtyConnection ? '未保存' : '无修改') })
727
798
  $('conn-auth-mode').addEventListener('change',updateAuthFields)
799
+ $('conn-base').addEventListener('input',()=>{if(!$('connection-form').hidden)renderConnectionAccountOptions()})
728
800
  $('conn-model-fetch').addEventListener('click', () => { void fetchConnectionModels() })
729
801
  $('conn-model-cancel').addEventListener('click', () => resetModelQuery('已取消获取,手填内容保留。'))
730
802
  $('conn-model-all').addEventListener('click', () => { for (const option of $('conn-model-options').options) option.selected = true })
@@ -735,7 +807,7 @@ async function mount() {
735
807
  })
736
808
  $('connection-form').addEventListener('submit', async event => { event.preventDefault(); try { const current = connectionDraft(); if (!current.name || !current.baseURL) throw new Error('请填写连接名称和地址'); await run('connections/save',{connection:current,expectedRevision:connection?.revision,clearSecrets:$('conn-clear-secret').checked ? ['key','value'] : []},data=>{dirtyConnection=false;editConnection(data.connection)}) } catch(error){text('connection-form-error',error.message)} })
737
809
  $('connection-cancel').addEventListener('click',()=>{if(allowLeave()){dirtyConnection=false;$('connection-panel').hidden=true}})
738
- $('connection-remove').addEventListener('click',()=>{if(connection && window.confirm('删除这个连接?不会删除模板或修改旧插件。'))void run('connections/remove',{id:connection.id,expectedRevision:connection.revision},()=>{dirtyConnection=false;$('connection-panel').hidden=true})})
810
+ $('connection-remove').addEventListener('click',()=>{if(connection && window.confirm('删除这个连接?不会删除模板或站点账号。'))void run('connections/remove',{id:connection.id,expectedRevision:connection.revision},()=>{dirtyConnection=false;$('connection-panel').hidden=true})})
739
811
  $('binding-open').addEventListener('click',()=>openBinding())
740
812
  $('binding-template').addEventListener('change',()=>renderRevisions('binding'))
741
813
  $('binding-mode').addEventListener('change',bindingFields); $('binding-scope').addEventListener('change',bindingFields)
@@ -823,8 +895,6 @@ async function mount() {
823
895
  catch{showError('未收到取消确认,请查询原操作,不要重复发送')}
824
896
  finally{if(!loading){$('test-cancel').disabled=false;text('test-cancel','取消测试')}}
825
897
  })
826
- $('migration-preview').addEventListener('click',()=>run('migration/preview',{},data=>{migration=data;$('migration-result').replaceChildren(node('pre',JSON.stringify(data,null,2),'code-block'));$('migration-apply').hidden=!data.available}))
827
- $('migration-apply').addEventListener('click',()=>{if(migration?.available&&window.confirm('复制旧 Claude 配置为 UA PRO 新连接?原配置不变。'))void run('migration/apply',{fingerprint:migration.fingerprint},()=>{$('migration-apply').hidden=true})})
828
898
  window.addEventListener('beforeunload',event=>{if(isDirty()||locked()){event.preventDefault();event.returnValue=''}})
829
899
  try { await refresh();navigate(location.hash.slice(1)||'overview',true);testFields();resetSample();$('pro-shell').dataset.ready='true';status('配置已读取。保存版本与应用连接分别执行。') } catch(error){showError(error);status('无法读取 UA PRO 配置')}
830
900
  }