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.
@@ -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
+ }