opencode-claude-code-ua 0.0.0-stage → 0.3.6

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,97 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { createUATestRunner } from './ua-tests.mjs'
3
+ import { validateTemplate } from './ua-transform.mjs'
4
+ import { lookupUAModels, readModelCatalog } from './ua-runtime.mjs'
5
+ import { createUAModelCatalog } from './ua-model-catalog.mjs'
6
+
7
+ const digest = value => createHash('sha256').update(JSON.stringify(value)).digest('hex')
8
+ const failure = (message, status = 400) => Object.assign(new Error(message), { status })
9
+
10
+ export function createUAService({ store, legacyStore, readAuth, fetchImpl, modelFetchFor, modelCatalog = createUAModelCatalog(store.path), onConnectionSaved, runtime = () => ({ mode: 'standalone', observed: [] }) }) {
11
+ const test = createUATestRunner({ store, readAuth, fetchImpl, modelFetchFor })
12
+ const initialized = () => store.initialize()
13
+ return {
14
+ async state() {
15
+ await initialized()
16
+ const snapshot = await store.snapshot({ secrets: true })
17
+ const modelCatalogs = {}
18
+ for (const connection of snapshot.connections) {
19
+ const entry = await readModelCatalog(connection, { catalog: modelCatalog, readAuth })
20
+ if (entry) modelCatalogs[connection.id] = entry
21
+ connection.auth.key = ''
22
+ connection.auth.value = ''
23
+ }
24
+ return { ...snapshot, modelCatalogs, runtime: runtime() }
25
+ },
26
+ async execute(action, data, { signal } = {}) {
27
+ await initialized()
28
+ if (action === 'templates/validate') return { template: validateTemplate(data.template) }
29
+ if (action === 'templates/save') return store.saveTemplate(data)
30
+ if (action === 'templates/archive') return store.archiveTemplate(data)
31
+ if (action === 'templates/remove') return store.removeTemplate(data)
32
+ if (action === 'connections/models') {
33
+ if (data.confirmNetwork !== true) throw failure('获取模型需要明确确认')
34
+ const connection = await store.prepareConnection(data)
35
+ const timeout = AbortSignal.timeout(15000)
36
+ const combined = signal ? AbortSignal.any([signal, timeout]) : timeout
37
+ try {
38
+ const result = await lookupUAModels(connection, { readAuth, fetchImpl, modelFetchFor, signal: combined, catalog: modelCatalog, source: 'management', allowCached: true })
39
+ signal?.throwIfAborted()
40
+ if (!result.cached) combined.throwIfAborted()
41
+ return { ...result, models: result.models.map(({ id, name }) => ({ id, name })), message: result.cached ? '刷新失败,显示此前发现的模型;连接尚未保存' : '模型列表已读取,连接尚未保存;未发送生成请求' }
42
+ } catch (error) {
43
+ if (combined.aborted) throw Object.assign(failure('获取模型已取消或超时,手填内容保留', 408), { code: signal?.aborted ? 'cancelled' : 'timeout' })
44
+ if (error.code === 'upstream-http') throw Object.assign(failure(`${error.message},请检查地址、协议和凭据`), { code: error.code })
45
+ if ([400, 409].includes(error.status)) {
46
+ if (['dns', 'connection-refused', 'connection-reset', 'timeout', 'tls', 'network'].includes(error.code)) {
47
+ const route = runtime().mode === 'host' ? '当前管理会话的代理可能与宿主账号代理不同。' : '独立工具不继承 Magpie 的账号代理;可从 Magpie 打开 UA PRO 管理后核对。'
48
+ error.message += `。${route}`
49
+ }
50
+ throw error
51
+ }
52
+ throw failure('模型列表读取未完成,请重新打开管理页后重试', 400)
53
+ }
54
+ }
55
+ if (action === 'connections/save') {
56
+ const result = await store.saveConnection(data)
57
+ if (onConnectionSaved) {
58
+ try { await onConnectionSaved(result.connection); return { ...result, registration: 'registered', activation: 'pending-runtime-observation' } }
59
+ catch { return { ...result, registration: 'pending-reload', activation: 'pending-reload', message: '连接已保存,宿主注册未确认,请重载插件后核对' } }
60
+ }
61
+ return { ...result, registration: 'pending-host-load', activation: 'pending-runtime-observation' }
62
+ }
63
+ if (action === 'connections/remove') return store.removeConnection(data)
64
+ if (action === 'bindings/apply') return { ...await store.applyBinding(data), activation: 'pending-runtime-observation' }
65
+ if (action === 'routes/apply') return { ...await store.applyModelRoute(data), activation: 'pending-reload', message: '模型上游协议和模板已保存;请在 Magpie 重新获取模型或重载插件后使用' }
66
+ if (action === 'test') return { result: await test(data, { signal }) }
67
+ if (action === 'migration/preview') {
68
+ const profile = await legacyStore?.load()
69
+ if (!profile) return { available: false, message: '没有可迁移的独立 Claude 配置;原生账号继续使用兼容入口' }
70
+ 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 新连接,不修改原配置或停用原供应商' }
71
+ }
72
+ if (action === 'migration/apply') {
73
+ if (typeof data.fingerprint !== 'string') throw failure('请先预览迁移')
74
+ const profile = await legacyStore?.load()
75
+ if (!profile || digest(profile) !== data.fingerprint) throw failure('旧配置已改变,请重新预览', 409)
76
+ const snapshot = await store.snapshot()
77
+ const id = `migration.${digest({ base: profile.upstream.baseUrl, key: profile.upstream.apiKey }).slice(0, 16)}`
78
+ if (snapshot.connections.some(connection => connection.id === id)) throw failure('此配置已经迁移,请在连接列表中核对', 409)
79
+ const baseline = structuredClone(snapshot.templates.find(item => item.id === 'custom.claude-vscode-2316' && item.revision === 1))
80
+ delete baseline.id
81
+ baseline.name = '原 Claude 配置副本'
82
+ baseline.kind = 'custom'
83
+ baseline.basedOn = { id: 'custom.claude-vscode-2316', revision: 1 }
84
+ baseline.body.parameters = structuredClone(profile.parameters)
85
+ const map = { userAgent: 'user-agent', anthropicVersion: 'anthropic-version', anthropicBeta: 'anthropic-beta', contentType: 'content-type' }
86
+ 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' })
87
+ 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' })
88
+ const saved = await store.saveTemplate({ template: baseline })
89
+ return this.execute('connections/save', { connection: { id, name: '原 Claude 连接', enabled: true, baseURL: profile.upstream.baseUrl,
90
+ protocol: 'anthropic', models: profile.models, apiPaths: {}, modelProtocols: {}, siteId: '', independent: false,
91
+ auth: { source: 'stored', mode: profile.upstream.authorizationMode, key: profile.upstream.apiKey, value: profile.upstream.authorizationValue },
92
+ binding: { mode: 'template', id: saved.template.id, revision: saved.template.revision }, modelBindings: {} } })
93
+ }
94
+ throw failure('未知的 UA PRO 操作', 404)
95
+ },
96
+ }
97
+ }