opencode-claude-code-ua 0.0.0-stage → 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/LICENSE +23 -0
- package/NOTICE.md +9 -0
- package/README.md +200 -2
- package/assets/icon.svg +9 -0
- package/index.mjs +19 -0
- package/lib/parameters.mjs +108 -0
- package/lib/ua-protocol.mjs +51 -0
- package/lib/ua-test-plan.mjs +232 -0
- package/package.json +80 -4
- package/setup.mjs +39 -0
- package/src/account-profile.mjs +73 -0
- package/src/account-usage.mjs +33 -0
- package/src/accounts.mjs +354 -0
- package/src/connection.mjs +144 -0
- package/src/diagnostics.mjs +37 -0
- package/src/headers.mjs +60 -0
- package/src/key-usage.mjs +73 -0
- package/src/newapi.mjs +330 -0
- package/src/options.mjs +48 -0
- package/src/plugin.mjs +344 -0
- package/src/profile.mjs +217 -0
- package/src/runtime.mjs +39 -0
- package/src/schedule.mjs +88 -0
- package/src/scheduler.mjs +141 -0
- package/src/setup-server.mjs +478 -0
- package/src/store.mjs +58 -0
- package/src/transport.mjs +84 -0
- package/src/ua-model-catalog.mjs +218 -0
- package/src/ua-plugin.mjs +250 -0
- package/src/ua-runtime.mjs +229 -0
- package/src/ua-service.mjs +129 -0
- package/src/ua-store.mjs +660 -0
- package/src/ua-tests.mjs +153 -0
- package/src/ua-transform.mjs +478 -0
- package/src/ua-usage.mjs +64 -0
- package/ui/accounts.css +75 -0
- package/ui/accounts.html +67 -0
- package/ui/accounts.js +786 -0
- package/ui/pro.css +1683 -0
- package/ui/pro.html +347 -0
- package/ui/pro.js +900 -0
- package/ui/theme.js +39 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto'
|
|
2
|
+
import { transformRequest } from './ua-transform.mjs'
|
|
3
|
+
import { resolveTemplate } from './ua-store.mjs'
|
|
4
|
+
|
|
5
|
+
import { SDK_BY_PROTOCOL, PATHS, protocolForModel, siteRoot, apiBase, operationURL } from '../lib/ua-protocol.mjs'
|
|
6
|
+
export { SDK_BY_PROTOCOL, protocolForModel, siteRoot, apiBase, operationURL } from '../lib/ua-protocol.mjs'
|
|
7
|
+
const authHeaders = ['authorization', 'x-api-key', 'x-goog-api-key', 'api-key', 'cookie', 'proxy-authorization']
|
|
8
|
+
const err = (message, status = 400) => Object.assign(new Error(message), { status })
|
|
9
|
+
|
|
10
|
+
export function operationForURL(input) {
|
|
11
|
+
const path = new URL(input instanceof Request ? input.url : input).pathname
|
|
12
|
+
if (/\/messages\/count_tokens$/.test(path)) return { protocol: 'anthropic', operation: 'count_tokens' }
|
|
13
|
+
if (/\/messages$/.test(path)) return { protocol: 'anthropic', operation: 'messages' }
|
|
14
|
+
if (/\/responses$/.test(path)) return { protocol: 'responses', operation: 'responses' }
|
|
15
|
+
if (/\/chat\/completions$/.test(path)) return { protocol: 'chat-completions', operation: 'chat/completions' }
|
|
16
|
+
const gemini = /\/models\/(.+):(generateContent|streamGenerateContent|countTokens)$/.exec(path)
|
|
17
|
+
if (gemini) return { protocol: 'gemini', operation: gemini[2], model: decodeURIComponent(gemini[1]) }
|
|
18
|
+
throw err('请求路径不是支持的模型操作')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function credential(connection, readAuth) {
|
|
22
|
+
let auth = connection.auth ?? {}
|
|
23
|
+
if (auth.source === 'host') {
|
|
24
|
+
const current = await readAuth?.(auth.provider, auth.account)
|
|
25
|
+
if (current?.type !== 'api' || typeof current.key !== 'string' || !current.key.trim()) throw err('当前宿主账号缺少 API key,请重新登录', 401)
|
|
26
|
+
const endpoint = current.metadata?.baseURL ?? current.metadata?.endpoint
|
|
27
|
+
if (!endpoint || new URL(endpoint).href.replace(/\/+$/, '') !== new URL(connection.baseURL).href.replace(/\/+$/, '')) throw err('宿主账号地址已改变,此旧连接不能使用当前凭据', 409)
|
|
28
|
+
auth = { ...auth, key: current.key }
|
|
29
|
+
}
|
|
30
|
+
if (['api-key', 'bearer'].includes(auth.mode) && !auth.key?.trim()) throw err('此连接缺少 API key', 400)
|
|
31
|
+
if (auth.mode === 'custom' && !auth.value?.trim()) throw err('此连接缺少自定义 Authorization', 400)
|
|
32
|
+
return auth
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function addAuthentication(headers, auth, protocol) {
|
|
36
|
+
const output = new Headers(headers)
|
|
37
|
+
for (const name of authHeaders) output.delete(name)
|
|
38
|
+
for (const name of [...output.keys()]) if (name.startsWith('x-magpie-')) output.delete(name)
|
|
39
|
+
if (auth.mode === 'bearer') output.set('authorization', `Bearer ${auth.key}`)
|
|
40
|
+
else if (auth.mode === 'custom') output.set('authorization', auth.value)
|
|
41
|
+
else if (auth.mode === 'api-key') output.set(protocol === 'anthropic' ? 'x-api-key' : protocol === 'gemini' ? 'x-goog-api-key' : 'authorization', ['responses', 'chat-completions'].includes(protocol) ? `Bearer ${auth.key}` : auth.key)
|
|
42
|
+
return output
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function bodyModel(init) {
|
|
46
|
+
try {
|
|
47
|
+
const value = typeof init.body === 'string' ? JSON.parse(init.body) : ArrayBuffer.isView(init.body) && init.body.byteLength <= 2 * 1024 * 1024 ? JSON.parse(new TextDecoder().decode(init.body)) : undefined
|
|
48
|
+
return typeof value?.model === 'string' ? value.model : ''
|
|
49
|
+
} catch { return '' }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function prepareUpstream({ snapshot, connection, input, init = {}, context = {}, readAuth, template, templateRef, protocol: forcedProtocol, operation: forcedOperation, model: chosenModel }) {
|
|
53
|
+
if (!connection || connection.enabled === false) throw err('连接不存在或已停用', 404)
|
|
54
|
+
const incoming = new URL(input instanceof Request ? input.url : input)
|
|
55
|
+
const root = new URL(siteRoot(connection.baseURL))
|
|
56
|
+
const prefixes = [...new Set(Object.values(PATHS).concat(Object.values(connection.apiPaths ?? {})))].map(path => new URL(root.href.replace(/\/$/, '') + path.replace(/\/$/, '') + '/'))
|
|
57
|
+
if (incoming.origin !== root.origin || incoming.username || incoming.password || incoming.hash || !prefixes.some(prefix => incoming.pathname.startsWith(prefix.pathname))) throw err('拒绝发送凭据到此连接以外的路径')
|
|
58
|
+
const detected = operationForURL(input)
|
|
59
|
+
const protocol = forcedProtocol ?? detected.protocol
|
|
60
|
+
const operation = forcedOperation ?? detected.operation
|
|
61
|
+
if (detected.protocol !== protocol || detected.operation !== operation) throw err('请求协议与选定测试协议不匹配')
|
|
62
|
+
const model = chosenModel || context.model || bodyModel(init) || detected.model || ''
|
|
63
|
+
const target = new URL(operationURL(connection, protocol, operation, model || detected.model || ''))
|
|
64
|
+
target.search = incoming.search
|
|
65
|
+
if (protocol === 'gemini') target.searchParams.delete('key')
|
|
66
|
+
const selected = template !== undefined ? template : templateRef ? snapshot.templates.find(item => item.id === templateRef.id && item.revision === templateRef.revision) : resolveTemplate(snapshot, connection, protocol, model)
|
|
67
|
+
if (templateRef && !selected) throw err('指定模板版本不存在', 404)
|
|
68
|
+
const auth = await credential(connection, readAuth)
|
|
69
|
+
const scope = createHash('sha256').update(JSON.stringify([connection.baseURL, auth.mode, auth.key ?? '', auth.value ?? ''])).digest('hex')
|
|
70
|
+
const destination = target.href === incoming.href ? input : input instanceof Request ? new Request(target.href, input) : target.href
|
|
71
|
+
const transformed = await transformRequest({ template: selected, protocol, operation, input: destination, init,
|
|
72
|
+
context: { ...context, baseURL: connection.baseURL, credentialScope: scope, model: model || detected.model || '', requestId: context.requestId ?? randomUUID() } })
|
|
73
|
+
transformed.init = { ...transformed.init, headers: addAuthentication(transformed.init.headers, auth, protocol), redirect: 'manual' }
|
|
74
|
+
const result = { ...transformed, protocol, operation, model, generation: snapshot.generation, connectionId: connection.id }
|
|
75
|
+
Object.defineProperty(result, 'credentialValues', { value: [auth.key, auth.value].filter(Boolean), enumerable: false })
|
|
76
|
+
return result
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function createUATransport({ getSnapshot, getConnection, readAuth, fetchImpl = globalThis.fetch, getContext = () => ({}), observe = () => {} }) {
|
|
80
|
+
return async (input, init = {}) => {
|
|
81
|
+
const snapshot = await getSnapshot()
|
|
82
|
+
const connection = await getConnection(snapshot)
|
|
83
|
+
const prepared = await prepareUpstream({ snapshot, connection, input, init, context: await getContext(input, init), readAuth })
|
|
84
|
+
observe({ connectionId: connection.id, generation: snapshot.generation, templateId: prepared.summary?.templateId, revision: prepared.summary?.revision })
|
|
85
|
+
return fetchImpl(prepared.input, prepared.init)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function boundedResponse(response, { limit = 1024 * 1024, signal } = {}) {
|
|
90
|
+
if (Number(response.headers.get('content-length')) > limit) { await response.body?.cancel(); throw err('响应超过允许大小') }
|
|
91
|
+
const reader = response.body?.getReader()
|
|
92
|
+
if (!reader) return ''
|
|
93
|
+
let size = 0
|
|
94
|
+
const chunks = []
|
|
95
|
+
const stop = () => { void reader.cancel().catch(() => {}) }
|
|
96
|
+
signal?.addEventListener('abort', stop, { once: true })
|
|
97
|
+
try {
|
|
98
|
+
for (;;) {
|
|
99
|
+
signal?.throwIfAborted()
|
|
100
|
+
const { done, value } = await reader.read()
|
|
101
|
+
signal?.throwIfAborted()
|
|
102
|
+
if (done) break
|
|
103
|
+
size += value.byteLength
|
|
104
|
+
if (size > limit) throw err('响应超过允许大小')
|
|
105
|
+
chunks.push(Buffer.from(value))
|
|
106
|
+
}
|
|
107
|
+
return Buffer.concat(chunks).toString('utf8')
|
|
108
|
+
} finally {
|
|
109
|
+
signal?.removeEventListener('abort', stop)
|
|
110
|
+
stop()
|
|
111
|
+
reader.releaseLock()
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function discoverUA(connection, options = {}) {
|
|
116
|
+
if (connection.models?.length) return connection.models.map(id => ({ id, name: id }))
|
|
117
|
+
return queryUAModels(connection, options)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function modelQueryScope(connection, readAuth) {
|
|
121
|
+
return (await prepareModelQuery(connection, readAuth)).scope
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function prepareModelQuery(connection, readAuth) {
|
|
125
|
+
const auth = await credential(connection, readAuth)
|
|
126
|
+
const protocol = protocolForModel(connection)
|
|
127
|
+
const headers = addAuthentication({ accept: 'application/json', 'anthropic-version': '2023-06-01' }, auth, protocol)
|
|
128
|
+
const url = apiBase(connection, protocol) + '/models'
|
|
129
|
+
// Scope the discovery record to the actual request, including the credential.
|
|
130
|
+
// Responses and auto may share a list only when URL and authentication agree.
|
|
131
|
+
const scope = createHash('sha256').update(JSON.stringify([url, [...headers], auth.source, auth.provider ?? '', auth.account ?? ''])).digest('hex')
|
|
132
|
+
return { url, headers, protocol, scope, secrets: [auth.key, auth.value].filter(Boolean) }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function catalogResult(entry) {
|
|
136
|
+
return { models: entry.models, count: entry.models.length, checkedAt: entry.checkedAt, source: `${entry.source}-cache`, cached: true, stale: true }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function readModelCatalog(connection, { catalog, readAuth } = {}) {
|
|
140
|
+
if (!catalog) return null
|
|
141
|
+
try {
|
|
142
|
+
const query = await prepareModelQuery(connection, readAuth)
|
|
143
|
+
const entry = await catalog.read(query.scope)
|
|
144
|
+
return entry ? catalogResult(entry) : null
|
|
145
|
+
} catch { return null }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function modelNetworkError(cause) {
|
|
149
|
+
const codes = []
|
|
150
|
+
for (let current = cause, depth = 0; current && depth < 4; current = current.cause, depth++) {
|
|
151
|
+
if (typeof current.code === 'string') codes.push(current.code)
|
|
152
|
+
}
|
|
153
|
+
const known = [
|
|
154
|
+
['dns', ['ENOTFOUND', 'EAI_AGAIN', 'DNSLookupError'], '无法解析上游域名,请检查地址、DNS 或代理设置'],
|
|
155
|
+
['connection-refused', ['ECONNREFUSED', 'ConnectionRefused'], '上游或代理拒绝连接,请检查服务地址和代理设置'],
|
|
156
|
+
['connection-reset', ['ECONNRESET', 'EPIPE', 'ConnectionClosed', 'UND_ERR_SOCKET'], '上游或代理中断了连接,请检查网络和代理设置'],
|
|
157
|
+
['timeout', ['ETIMEDOUT', 'UND_ERR_CONNECT_TIMEOUT', 'UND_ERR_HEADERS_TIMEOUT', 'UND_ERR_BODY_TIMEOUT'], '连接上游超时,请检查网络和代理设置'],
|
|
158
|
+
['tls', ['CERT_HAS_EXPIRED', 'DEPTH_ZERO_SELF_SIGNED_CERT', 'UNABLE_TO_VERIFY_LEAF_SIGNATURE', 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY', 'ERR_TLS_CERT_ALTNAME_INVALID', 'CERT_SIGNATURE_FAILURE'], '上游 TLS 证书验证失败,请检查证书或代理配置'],
|
|
159
|
+
]
|
|
160
|
+
const [code, , message] = known.find(([, matches]) => codes.some(value => matches.includes(value))) ?? ['network', [], '无法连接上游,请检查网络或代理设置']
|
|
161
|
+
return Object.assign(err(message), { code, catalogFallback: true })
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function fetchModelList(query, fetchImpl, signal) {
|
|
165
|
+
let response
|
|
166
|
+
try { response = await fetchImpl(query.url, { method: 'GET', headers: query.headers, signal, redirect: 'manual' }) }
|
|
167
|
+
catch (error) { signal.throwIfAborted(); throw modelNetworkError(error) }
|
|
168
|
+
if (!response.ok) {
|
|
169
|
+
void response.body?.cancel().catch(() => {})
|
|
170
|
+
const error = Object.assign(err(`模型列表返回 HTTP ${response.status}`, response.status), { code: 'upstream-http', catalogFallback: response.status >= 500 })
|
|
171
|
+
if (response.status === 401) error.signIn = 'expired'
|
|
172
|
+
throw error
|
|
173
|
+
}
|
|
174
|
+
let text
|
|
175
|
+
try { text = await boundedResponse(response, { signal }) }
|
|
176
|
+
catch (error) { signal.throwIfAborted(); if (error.status) throw error; throw modelNetworkError(error) }
|
|
177
|
+
let data
|
|
178
|
+
try { data = JSON.parse(text) } catch { throw Object.assign(err('模型列表不是有效的 JSON'), { code: 'invalid-model-response' }) }
|
|
179
|
+
const list = query.protocol === 'gemini' && Array.isArray(data?.models)
|
|
180
|
+
? data.models.map(item => ({ id: typeof item?.name === 'string' ? item.name.replace(/^models\//, '') : '', display_name: item?.displayName, limit: { context: item?.inputTokenLimit, output: item?.outputTokenLimit } }))
|
|
181
|
+
: data?.data
|
|
182
|
+
if (!Array.isArray(list)) throw Object.assign(err('模型列表格式无效'), { code: 'invalid-model-response' })
|
|
183
|
+
const models = new Map()
|
|
184
|
+
const safeText = (value, limit = 512) => typeof value === 'string' && value.trim() && value.trim().length <= limit && !/[\x00-\x1f\x7f-\x9f]/.test(value) && !query.secrets.some(secret => value.includes(secret))
|
|
185
|
+
for (const item of list) {
|
|
186
|
+
if (!safeText(item?.id, 256)) continue
|
|
187
|
+
const id = item.id.trim()
|
|
188
|
+
if (models.has(id)) continue
|
|
189
|
+
const name = item.display_name ?? item.name
|
|
190
|
+
models.set(id, { id, name: safeText(name) ? name.trim() : id, ...(item.limit && typeof item.limit === 'object' ? { limit: item.limit } : {}) })
|
|
191
|
+
if (models.size > 2048) throw Object.assign(err('模型列表超过 2048 个,请使用手填模型缩小范围'), { code: 'invalid-model-response' })
|
|
192
|
+
}
|
|
193
|
+
return [...models.values()]
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export async function lookupUAModels(connection, { readAuth, fetchImpl = globalThis.fetch, modelFetchFor, signal = AbortSignal.timeout(15000), catalog, source = 'management', allowCached = false } = {}) {
|
|
197
|
+
signal.throwIfAborted()
|
|
198
|
+
const query = await prepareModelQuery(connection, readAuth)
|
|
199
|
+
let models
|
|
200
|
+
try {
|
|
201
|
+
const send = await modelFetchFor?.(query.scope) ?? fetchImpl
|
|
202
|
+
signal.throwIfAborted()
|
|
203
|
+
models = await fetchModelList(query, send, signal)
|
|
204
|
+
signal.throwIfAborted()
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
const timedOut = signal.aborted && signal.reason?.name === 'TimeoutError'
|
|
208
|
+
if (signal.aborted && !timedOut) signal.throwIfAborted()
|
|
209
|
+
if (timedOut) error = Object.assign(err('模型列表读取超时,请检查网络和代理设置', 408), { code: 'timeout', catalogFallback: true })
|
|
210
|
+
if (allowCached && error.catalogFallback && catalog) {
|
|
211
|
+
const entry = await catalog.read(query.scope).catch(() => null)
|
|
212
|
+
if (signal.aborted && !timedOut) signal.throwIfAborted()
|
|
213
|
+
if (entry) return { ...catalogResult(entry), refreshError: { code: error.code, message: error.message } }
|
|
214
|
+
}
|
|
215
|
+
throw error
|
|
216
|
+
}
|
|
217
|
+
const checkedAt = new Date().toISOString()
|
|
218
|
+
let catalogWarning
|
|
219
|
+
if (catalog) {
|
|
220
|
+
try { await catalog.write(query.scope, { models: models.map(({ id, name }) => ({ id, name })), checkedAt, source }) }
|
|
221
|
+
catch { catalogWarning = '模型目录未能保存,其他管理会话暂时无法共享本次结果;请检查目录权限或缓存文件' }
|
|
222
|
+
}
|
|
223
|
+
return { models, count: models.length, checkedAt, source: 'upstream', cached: false, stale: false, ...(catalogWarning ? { catalogWarning } : {}) }
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export async function queryUAModels(connection, options = {}) {
|
|
227
|
+
const result = await lookupUAModels(connection, { ...options, allowCached: false })
|
|
228
|
+
return result.models
|
|
229
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { createUATestRunner } from './ua-tests.mjs'
|
|
3
|
+
import { validateTemplate } from './ua-transform.mjs'
|
|
4
|
+
import { lookupUAModels, readModelCatalog, modelQueryScope } from './ua-runtime.mjs'
|
|
5
|
+
import { createUAModelCatalog } from './ua-model-catalog.mjs'
|
|
6
|
+
import { matchesAccountSite } from '../lib/ua-protocol.mjs'
|
|
7
|
+
import { createKeyUsage } from './key-usage.mjs'
|
|
8
|
+
|
|
9
|
+
const digest = value => createHash('sha256').update(JSON.stringify(value)).digest('hex')
|
|
10
|
+
const failure = (message, status = 400) => Object.assign(new Error(message), { status })
|
|
11
|
+
|
|
12
|
+
export function createUAService({ store, legacyStore, accountManager, readAuth, fetchImpl, modelFetchFor, modelCatalog = createUAModelCatalog(store.path), onConnectionSaved, syncConnections, runtime = () => ({ mode: 'standalone', observed: [] }) }) {
|
|
13
|
+
const test = createUATestRunner({ store, readAuth, fetchImpl, modelFetchFor })
|
|
14
|
+
const initialized = () => store.initialize()
|
|
15
|
+
return {
|
|
16
|
+
async state() {
|
|
17
|
+
await initialized()
|
|
18
|
+
const snapshot = await store.snapshot({ secrets: true })
|
|
19
|
+
const modelCatalogs = {}
|
|
20
|
+
for (const connection of snapshot.connections) {
|
|
21
|
+
const entry = await readModelCatalog(connection, { catalog: modelCatalog, readAuth })
|
|
22
|
+
if (entry) modelCatalogs[connection.id] = entry
|
|
23
|
+
connection.auth.key = ''
|
|
24
|
+
connection.auth.value = ''
|
|
25
|
+
}
|
|
26
|
+
const siteAccounts = accountManager ? (await accountManager.list()).accounts.map(account => ({ id: account.id, name: account.name,
|
|
27
|
+
siteUrl: account.siteUrl, siteType: account.siteType ?? 'newapi', userId: account.userId, enabled: account.enabled,
|
|
28
|
+
checkinEnabled: account.checkinEnabled, schedule: account.schedule })) : []
|
|
29
|
+
return { ...snapshot, modelCatalogs, siteAccounts, runtime: await runtime(snapshot) }
|
|
30
|
+
},
|
|
31
|
+
async accountConnections() {
|
|
32
|
+
await initialized()
|
|
33
|
+
return (await store.snapshot()).connections.map(connection => ({ id: connection.id, name: connection.name, baseURL: connection.baseURL,
|
|
34
|
+
revision: connection.revision, enabled: connection.enabled, newApiAccountId: connection.newApiAccountId ?? '' }))
|
|
35
|
+
},
|
|
36
|
+
async execute(action, data, { signal } = {}) {
|
|
37
|
+
await initialized()
|
|
38
|
+
if (action === 'templates/validate') return { template: validateTemplate(data.template) }
|
|
39
|
+
if (action === 'templates/save') return store.saveTemplate(data)
|
|
40
|
+
if (action === 'templates/archive') return store.archiveTemplate(data)
|
|
41
|
+
if (action === 'templates/remove') return store.removeTemplate(data)
|
|
42
|
+
if (action === 'connections/usage') {
|
|
43
|
+
if (data.confirmNetwork !== true) throw failure('读取连接余额需要明确确认')
|
|
44
|
+
const snapshot = await store.snapshot({ secrets: true })
|
|
45
|
+
const connection = snapshot.connections.find(item => item.id === data.connectionId)
|
|
46
|
+
if (!connection || !connection.enabled) throw failure('连接不存在或已停用', 404)
|
|
47
|
+
const usageFetch = modelFetchFor ? modelFetchFor(await modelQueryScope(connection, readAuth)) : fetchImpl
|
|
48
|
+
const usage = await createKeyUsage({ fetchImpl: usageFetch, readAuth })(connection)
|
|
49
|
+
return { connectionId: connection.id, revision: connection.revision, usage, at: new Date().toISOString() }
|
|
50
|
+
}
|
|
51
|
+
if (action === 'connections/models') {
|
|
52
|
+
if (data.confirmNetwork !== true) throw failure('获取模型需要明确确认')
|
|
53
|
+
const connection = await store.prepareConnection(data)
|
|
54
|
+
const timeout = AbortSignal.timeout(15000)
|
|
55
|
+
const combined = signal ? AbortSignal.any([signal, timeout]) : timeout
|
|
56
|
+
try {
|
|
57
|
+
const result = await lookupUAModels(connection, { readAuth, fetchImpl, modelFetchFor, signal: combined, catalog: modelCatalog, source: 'management', allowCached: true })
|
|
58
|
+
signal?.throwIfAborted()
|
|
59
|
+
if (!result.cached) combined.throwIfAborted()
|
|
60
|
+
return { ...result, models: result.models.map(({ id, name }) => ({ id, name })), message: result.cached ? '刷新失败,显示此前发现的模型;连接尚未保存' : '模型列表已读取,连接尚未保存;未发送生成请求' }
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (combined.aborted) throw Object.assign(failure('获取模型已取消或超时,手填内容保留', 408), { code: signal?.aborted ? 'cancelled' : 'timeout' })
|
|
63
|
+
if (error.code === 'upstream-http') throw Object.assign(failure(`${error.message},请检查地址、协议和凭据`), { code: error.code })
|
|
64
|
+
if ([400, 409].includes(error.status)) {
|
|
65
|
+
if (['dns', 'connection-refused', 'connection-reset', 'timeout', 'tls', 'network'].includes(error.code)) {
|
|
66
|
+
const route = (await runtime()).mode === 'host' ? '当前管理会话的代理可能与宿主账号代理不同。' : '独立工具不继承 Magpie 的账号代理;可从 Magpie 打开 UA PRO 管理后核对。'
|
|
67
|
+
error.message += `。${route}`
|
|
68
|
+
}
|
|
69
|
+
throw error
|
|
70
|
+
}
|
|
71
|
+
throw failure('模型列表读取未完成,请重新打开管理页后重试', 400)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (action === 'connections/sync') {
|
|
75
|
+
if (!syncConnections) throw failure('独立工具不能直接写入 Magpie;请从宿主打开 UA PRO 管理后同步', 409)
|
|
76
|
+
return syncConnections()
|
|
77
|
+
}
|
|
78
|
+
if (action === 'connections/save') {
|
|
79
|
+
if (data.connection?.newApiAccountId) {
|
|
80
|
+
if (!accountManager) throw failure('站点账号管理尚未初始化', 409)
|
|
81
|
+
const account = await accountManager.get(data.connection.newApiAccountId)
|
|
82
|
+
if (!matchesAccountSite(data.connection.baseURL, account)) throw Object.assign(failure('余额与签到只能绑定同一站点的账号'), { fields: { newApiAccountId: '所选账号属于其他站点' } })
|
|
83
|
+
}
|
|
84
|
+
const result = await store.saveConnection(data)
|
|
85
|
+
if (onConnectionSaved) {
|
|
86
|
+
try {
|
|
87
|
+
const host = await onConnectionSaved(result.connection)
|
|
88
|
+
if (host?.status === 'reload-required') return { ...result, registration: 'pending-reload', activation: 'pending-reload', host, message: host.message }
|
|
89
|
+
if (host && host.status !== 'registered') return { ...result, registration: 'pending-reload', activation: 'pending-reload', host, message: host.message || '连接已保存,宿主账号尚未确认,请同步或重载后核对' }
|
|
90
|
+
return { ...result, registration: 'registered', activation: 'pending-runtime-observation', ...(host ? { host } : {}) }
|
|
91
|
+
} catch { return { ...result, registration: 'pending-reload', activation: 'pending-reload', message: '连接已保存,宿主注册未确认,请重载插件后核对' } }
|
|
92
|
+
}
|
|
93
|
+
return { ...result, registration: 'pending-host-load', activation: 'pending-runtime-observation' }
|
|
94
|
+
}
|
|
95
|
+
if (action === 'connections/remove') return store.removeConnection(data)
|
|
96
|
+
if (action === 'bindings/apply') return { ...await store.applyBinding(data), activation: 'pending-runtime-observation' }
|
|
97
|
+
if (action === 'routes/apply') return { ...await store.applyModelRoute(data), activation: 'pending-reload', message: '模型上游协议和模板已保存;请在 Magpie 重新获取模型或重载插件后使用' }
|
|
98
|
+
if (action === 'test') return { result: await test(data, { signal }) }
|
|
99
|
+
if (action === 'migration/preview') {
|
|
100
|
+
const profile = await legacyStore?.load()
|
|
101
|
+
if (!profile) return { available: false, message: '没有可迁移的独立 Claude 配置;原生账号继续使用兼容入口' }
|
|
102
|
+
return { available: true, fingerprint: digest(profile), connection: { name: '原 Claude 连接', baseURL: profile.upstream.baseUrl, models: profile.models, secrets: { key: Boolean(profile.upstream.apiKey), value: Boolean(profile.upstream.authorizationValue) } }, template: { name: '原 Claude 配置副本', basedOn: 'custom.claude-vscode-2316', parameters: profile.parameters }, message: '只复制为 UA PRO 新连接,不修改原配置或停用原供应商' }
|
|
103
|
+
}
|
|
104
|
+
if (action === 'migration/apply') {
|
|
105
|
+
if (typeof data.fingerprint !== 'string') throw failure('请先预览迁移')
|
|
106
|
+
const profile = await legacyStore?.load()
|
|
107
|
+
if (!profile || digest(profile) !== data.fingerprint) throw failure('旧配置已改变,请重新预览', 409)
|
|
108
|
+
const snapshot = await store.snapshot()
|
|
109
|
+
const id = `migration.${digest({ base: profile.upstream.baseUrl, key: profile.upstream.apiKey }).slice(0, 16)}`
|
|
110
|
+
if (snapshot.connections.some(connection => connection.id === id)) throw failure('此配置已经迁移,请在连接列表中核对', 409)
|
|
111
|
+
const baseline = structuredClone(snapshot.templates.find(item => item.id === 'custom.claude-vscode-2316' && item.revision === 1))
|
|
112
|
+
delete baseline.id
|
|
113
|
+
baseline.name = '原 Claude 配置副本'
|
|
114
|
+
baseline.kind = 'custom'
|
|
115
|
+
baseline.basedOn = { id: 'custom.claude-vscode-2316', revision: 1 }
|
|
116
|
+
baseline.body.parameters = structuredClone(profile.parameters)
|
|
117
|
+
const map = { userAgent: 'user-agent', anthropicVersion: 'anthropic-version', anthropicBeta: 'anthropic-beta', contentType: 'content-type' }
|
|
118
|
+
for (const [field, name] of Object.entries(map)) if (profile.headers[field]) baseline.headers.push({ name, action: 'set', value: profile.headers[field], enabled: true, when: 'always' })
|
|
119
|
+
for (const extra of profile.headers.extra) baseline.headers.push({ name: extra.name, action: extra.value === null ? 'delete' : 'set', ...(extra.value === null ? {} : { value: extra.value }), enabled: extra.enabled, when: 'always' })
|
|
120
|
+
const saved = await store.saveTemplate({ template: baseline })
|
|
121
|
+
return this.execute('connections/save', { connection: { id, name: '原 Claude 连接', enabled: true, baseURL: profile.upstream.baseUrl,
|
|
122
|
+
protocol: 'anthropic', models: profile.models, apiPaths: {}, modelProtocols: {}, siteId: '', independent: false,
|
|
123
|
+
auth: { source: 'stored', mode: profile.upstream.authorizationMode, key: profile.upstream.apiKey, value: profile.upstream.authorizationValue },
|
|
124
|
+
binding: { mode: 'template', id: saved.template.id, revision: saved.template.revision }, modelBindings: {} } })
|
|
125
|
+
}
|
|
126
|
+
throw failure('未知的 UA PRO 操作', 404)
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
}
|