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/README.md +94 -90
- package/index.mjs +2 -6
- package/lib/ua-protocol.mjs +8 -0
- package/package.json +1 -1
- package/setup.mjs +1 -1
- package/src/account-usage.mjs +33 -0
- package/src/accounts.mjs +96 -23
- package/src/key-usage.mjs +73 -0
- package/src/newapi.mjs +106 -13
- package/src/plugin.mjs +4 -34
- package/src/scheduler.mjs +2 -2
- package/src/setup-server.mjs +16 -3
- package/src/ua-plugin.mjs +57 -14
- package/src/ua-runtime.mjs +1 -1
- package/src/ua-service.mjs +38 -6
- package/src/ua-store.mjs +4 -1
- package/src/ua-usage.mjs +64 -0
- package/ui/accounts.css +1 -1
- package/ui/accounts.html +21 -11
- package/ui/accounts.js +230 -26
- package/ui/pro.css +1 -0
- package/ui/pro.html +10 -11
- package/ui/pro.js +82 -12
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
|
|
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
|
-
$('
|
|
240
|
-
$('
|
|
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
|
|
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('
|
|
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
|
}
|