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,232 @@
1
+ import { SDK_BY_PROTOCOL, protocolForModel, apiBase, operationURL } from './ua-protocol.mjs'
2
+
3
+ const OPERATIONS = {
4
+ anthropic: ['messages', 'count_tokens'],
5
+ responses: ['responses'],
6
+ 'chat-completions': ['chat/completions'],
7
+ gemini: ['generateContent', 'streamGenerateContent', 'countTokens'],
8
+ }
9
+ const BUILTIN_REFS = {
10
+ responses: { id: 'builtin.codex', revision: 1 },
11
+ anthropic: { id: 'builtin.claude-code', revision: 1 },
12
+ }
13
+ const CONTROL = /[\u0000-\u001f\u007f-\u009f]/
14
+ const isObject = value => value !== null && typeof value === 'object' && !Array.isArray(value)
15
+ const isCount = operation => operation === 'count_tokens' || operation === 'countTokens'
16
+ const label = template => template ? `${template.name || template.id}@${template.revision}` : '普通转发'
17
+ const fail = (message, field) => Object.assign(new Error(message), {
18
+ status: 400, code: 'test-plan-invalid', fields: { [field]: message },
19
+ })
20
+
21
+ function checked(field, action) {
22
+ try { return action() } catch (error) {
23
+ if (error?.code === 'test-plan-invalid') throw error
24
+ throw fail(error?.message || '测试计划无效', field)
25
+ }
26
+ }
27
+
28
+ function validateProtocol(protocol) {
29
+ if (!Object.hasOwn(SDK_BY_PROTOCOL, protocol)) throw fail('不支持的上游协议', 'protocol')
30
+ }
31
+
32
+ function validateModel(model) {
33
+ if (typeof model !== 'string' || !model.trim() || CONTROL.test(model)) throw fail('请选择有效的测试模型,模型名不能为空或包含控制字符', 'model')
34
+ }
35
+
36
+ function chooseOperation(protocol, operation, stream) {
37
+ validateProtocol(protocol)
38
+ if (typeof stream !== 'boolean') throw fail('测试 stream 必须是布尔值', 'stream')
39
+ const selected = operation ?? (protocol === 'gemini' && stream ? 'streamGenerateContent' : OPERATIONS[protocol][0])
40
+ if (!OPERATIONS[protocol].includes(selected)) throw fail('操作与上游协议不匹配', 'operation')
41
+ if (isCount(selected) && stream) throw fail('计数操作不支持流式测试', 'stream')
42
+ if (protocol === 'gemini' && !isCount(selected) && (selected === 'streamGenerateContent') !== stream) {
43
+ throw fail('Gemini 操作与测试 stream 不一致', 'stream')
44
+ }
45
+ return selected
46
+ }
47
+
48
+ function validateRef(ref, field) {
49
+ if (!isObject(ref) || typeof ref.id !== 'string' || !ref.id.trim() || CONTROL.test(ref.id) || !Number.isSafeInteger(ref.revision) || ref.revision < 1) {
50
+ throw fail('模板引用必须指定有效 id 和精确 revision', field)
51
+ }
52
+ }
53
+
54
+ function validateTemplateShape(template, field) {
55
+ validateRef(template, field)
56
+ if (template.schemaVersion !== 1 || typeof template.name !== 'string' || !template.name.trim() ||
57
+ !['builtin', 'custom'].includes(template.kind) || !Array.isArray(template.headers) || !isObject(template.body) ||
58
+ !Array.isArray(template.protocols) || !template.protocols.length ||
59
+ template.protocols.some(protocol => !Object.hasOwn(OPERATIONS, protocol)) ||
60
+ !Array.isArray(template.operations) || !template.operations.length ||
61
+ template.operations.some(operation => !template.protocols.some(protocol => OPERATIONS[protocol].includes(operation)))) {
62
+ throw fail('模板数据损坏,不能为测试自动回退', field)
63
+ }
64
+ return template
65
+ }
66
+
67
+ function findTemplate(snapshot, ref, field = 'templateRef', allowArchived = true) {
68
+ validateRef(ref, field)
69
+ if (!isObject(snapshot) || !Array.isArray(snapshot.templates)) throw fail('状态快照无效', 'snapshot')
70
+ const matches = snapshot.templates.filter(template => isObject(template) && template.id === ref.id && template.revision === ref.revision)
71
+ if (matches.length !== 1) throw fail(matches.length ? '模板版本重复,状态快照损坏' : '指定的模板 revision 不存在', field)
72
+ const template = validateTemplateShape(matches[0], field)
73
+ if (!allowArchived && template.archived) throw fail('本次自动兼容所需的固定内置模板已归档', field)
74
+ return template
75
+ }
76
+
77
+ function assertCompatible(template, protocol, operation) {
78
+ if (template && (!template.protocols.includes(protocol) || !template.operations.includes(operation))) {
79
+ throw fail('选择的模板不支持当前协议或操作,请明确选择兼容模板', 'template')
80
+ }
81
+ }
82
+
83
+ function autoTemplate(snapshot, binding, protocol) {
84
+ const versions = binding.resolvedVersions
85
+ if (versions !== undefined && !isObject(versions)) throw fail('auto 绑定的固定版本数据损坏', 'binding')
86
+ if (versions && Object.hasOwn(versions, protocol)) return findTemplate(snapshot, versions[protocol], 'binding')
87
+ if (protocol === 'chat-completions' || protocol === 'gemini') return null
88
+ throw fail('auto 绑定缺少当前协议的固定模板版本', 'binding')
89
+ }
90
+
91
+ function effectiveBinding(connection, model) {
92
+ if (connection.modelBindings !== undefined && !isObject(connection.modelBindings)) throw fail('模型模板绑定数据损坏', 'binding')
93
+ const binding = model && connection.modelBindings && Object.hasOwn(connection.modelBindings, model)
94
+ ? connection.modelBindings[model] : connection.binding
95
+ if (!isObject(binding)) throw fail('连接缺少模板绑定', 'binding')
96
+ return binding
97
+ }
98
+
99
+ function compatibleTemplate(snapshot, connection, binding, protocol) {
100
+ // An explicit configured auto reference is still pinned; invalid references never fall back.
101
+ const configured = binding.mode === 'auto' ? binding : connection.binding?.mode === 'auto' ? connection.binding : null
102
+ if (configured) return autoTemplate(snapshot, configured, protocol)
103
+ const ref = BUILTIN_REFS[protocol]
104
+ return ref ? findTemplate(snapshot, ref, 'binding', false) : null
105
+ }
106
+
107
+ function resolveSelection({ snapshot, connection, model, protocol, operation, templateMode, template, templateRef }) {
108
+ if (template === null) return { template: null, resolution: { mode: 'passthrough', message: '本次测试使用普通转发,不修改连接绑定' }, warnings: [] }
109
+ if (template !== undefined || templateRef !== undefined) {
110
+ const selected = template !== undefined ? validateTemplateShape(template, 'template') : findTemplate(snapshot, templateRef)
111
+ assertCompatible(selected, protocol, operation)
112
+ return { template: selected, resolution: { mode: 'explicit', message: `本次测试使用${label(selected)},不修改连接绑定` }, warnings: [] }
113
+ }
114
+ const binding = effectiveBinding(connection, model)
115
+ let selected
116
+ if (binding.mode === 'passthrough') selected = null
117
+ else if (binding.mode === 'auto') selected = autoTemplate(snapshot, binding, protocol)
118
+ else if (binding.mode === 'template') selected = findTemplate(snapshot, binding, 'binding')
119
+ else throw fail('模板绑定模式无效', 'binding')
120
+
121
+ const protocolMatches = selected === null || selected.protocols.includes(protocol)
122
+ const operationMatches = selected === null || selected.operations.includes(operation)
123
+ if (!protocolMatches || !operationMatches) {
124
+ // Counting must not hide a same-protocol template's deliberately unsupported operation.
125
+ if (templateMode !== 'auto' || (protocolMatches && isCount(operation))) assertCompatible(selected, protocol, operation)
126
+ const compatible = compatibleTemplate(snapshot, connection, binding, protocol)
127
+ assertCompatible(compatible, protocol, operation)
128
+ return {
129
+ template: compatible,
130
+ resolution: { mode: 'compatible', message: `本次测试改用${label(compatible)};连接仍绑定${label(selected)}` },
131
+ warnings: ['仅用于本次测试,不修改连接或模型的模板绑定。'],
132
+ }
133
+ }
134
+ return {
135
+ template: selected,
136
+ resolution: { mode: selected ? 'binding' : 'passthrough', message: selected ? `使用绑定模板${label(selected)}` : '使用绑定的普通转发策略' },
137
+ warnings: [],
138
+ }
139
+ }
140
+
141
+ /** Build an immutable-input, credential-free plan. The caller owns transport and authentication. */
142
+ export function createTestPlan({ snapshot, connection, kind = 'infer', protocol, operation, model = '', stream = false, templateMode = 'binding', template, templateRef, apiPath } = {}) {
143
+ if (!['preview', 'check', 'infer'].includes(kind)) throw fail('测试类型无效', 'kind')
144
+ if (!['binding', 'auto'].includes(templateMode)) throw fail('测试模板模式无效', 'templateMode')
145
+ if (typeof stream !== 'boolean') throw fail('测试 stream 必须是布尔值', 'stream')
146
+ if (!isObject(connection)) {
147
+ if (kind !== 'preview' || connection != null) throw fail('请选择测试连接', 'connection')
148
+ validateProtocol(protocol)
149
+ connection = { baseURL: 'https://preview.invalid', protocol, binding: { mode: 'auto', resolvedVersions: BUILTIN_REFS } }
150
+ }
151
+ if (kind === 'check') {
152
+ protocol = checked('protocol', () => protocolForModel(connection, ''))
153
+ model = ''
154
+ operation = 'models'
155
+ stream = false
156
+ } else {
157
+ validateModel(model)
158
+ protocol = protocol ?? checked('protocol', () => protocolForModel(connection, model))
159
+ operation = chooseOperation(protocol, operation, stream)
160
+ }
161
+ validateProtocol(protocol)
162
+ // Never spread the connection: doing so could read/copy a credential getter or secret value.
163
+ const target = { baseURL: connection.baseURL, apiPaths: connection.apiPaths }
164
+ if (apiPath !== undefined) {
165
+ if (typeof apiPath !== 'string' || !/^\/[A-Za-z0-9/_-]*$/.test(apiPath) || apiPath.includes('//')) {
166
+ throw fail('本次协议根路径必须以 / 开头,且不能包含 URL、重复斜杠、遍历、查询或片段', 'apiPath')
167
+ }
168
+ if (/\/(?:messages(?:\/count_tokens)?|responses|chat\/completions)$/.test(apiPath)) throw fail('请填写协议根路径,不要填写完整的模型操作路径', 'apiPath')
169
+ target.apiPaths = { [protocol]: apiPath }
170
+ }
171
+ const url = checked('connection.baseURL', () => kind === 'check' ? `${apiBase(target, protocol)}/models` : operationURL(target, protocol, operation, model))
172
+ const selection = kind === 'check'
173
+ ? { template: null, resolution: { mode: 'passthrough', message: '连接检查仅查询模型列表,不应用模板' }, warnings: [] }
174
+ : resolveSelection({ snapshot, connection, model, protocol, operation, templateMode, template, templateRef })
175
+ const selected = selection.template === null ? null : checked('template', () => structuredClone(selection.template))
176
+ if (selected) delete selected.archived
177
+ return {
178
+ kind,
179
+ connectionId: connection.id ?? null,
180
+ connectionName: connection.name ?? '',
181
+ model, protocol, operation, stream,
182
+ method: kind === 'check' ? 'GET' : 'POST',
183
+ url,
184
+ template: selected,
185
+ templateId: selected?.id ?? null,
186
+ revision: selected?.revision ?? null,
187
+ templateMode,
188
+ resolution: selection.resolution,
189
+ warnings: selection.warnings,
190
+ }
191
+ }
192
+
193
+ /** Generate a fresh sample for the exact protocol/model/stream/operation selection. */
194
+ export function createTestSample(protocol, model, stream = false, operation) {
195
+ validateModel(model)
196
+ operation = chooseOperation(protocol, operation, stream)
197
+ const text = 'Reply with OK.'
198
+ if (protocol === 'gemini') {
199
+ const contents = [{ role: 'user', parts: [{ text }] }]
200
+ return operation === 'countTokens' ? { contents } : { contents, generationConfig: { maxOutputTokens: 128 } }
201
+ }
202
+ const messages = [{ role: 'user', content: text }]
203
+ if (protocol === 'anthropic' && operation === 'count_tokens') return { model, messages }
204
+ if (protocol === 'responses') return {
205
+ model, input: [{ role: 'user', content: [{ type: 'input_text', text }] }],
206
+ max_output_tokens: 128, store: false, prompt_cache_key: 'ua-pro-test-session', stream,
207
+ }
208
+ return { model, max_tokens: 128, messages, stream }
209
+ }
210
+
211
+ /** Return the original body after structural checks; never rewrite caller business fields. */
212
+ export function validateTestSample(body, plan) {
213
+ if (!isObject(body)) throw fail('测试请求正文必须是 JSON 对象,不能是数组', 'request.body')
214
+ if (!isObject(plan)) throw fail('测试计划无效', 'plan')
215
+ validateProtocol(plan.protocol)
216
+ if (typeof body.model === 'string' && body.model !== plan.model) {
217
+ throw fail('请求正文模型与测试计划模型不一致,请重新生成样例或修改正文', 'request.body.model')
218
+ }
219
+ if (Object.hasOwn(body, 'stream')) {
220
+ if (typeof body.stream !== 'boolean') throw fail('请求正文 stream 必须是布尔值', 'request.body.stream')
221
+ if (body.stream !== plan.stream) throw fail('请求正文 stream 与测试计划不一致,请重新生成样例或修改正文', 'request.body.stream')
222
+ }
223
+ if (plan.operation === 'models') return body
224
+ if (['anthropic', 'chat-completions'].includes(plan.protocol) && !Array.isArray(body.messages)) {
225
+ throw fail('当前协议的测试正文需要 messages 数组', 'request.body.messages')
226
+ }
227
+ if (plan.protocol === 'responses' && typeof body.input !== 'string' && !Array.isArray(body.input)) {
228
+ throw fail('Responses 测试正文需要字符串或数组 input', 'request.body.input')
229
+ }
230
+ if (plan.protocol === 'gemini' && !Array.isArray(body.contents)) throw fail('Gemini 测试正文需要 contents 数组', 'request.body.contents')
231
+ return body
232
+ }
package/package.json CHANGED
@@ -1,6 +1,82 @@
1
1
  {
2
2
  "name": "opencode-claude-code-ua",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.3.6",
4
+ "publishConfig": {
5
+ "access": "public",
6
+ "registry": "https://registry.npmjs.org/"
7
+ },
8
+ "description": "UA PRO: versioned Codex and Claude compatibility templates, multi-protocol connections, previews and explicit upstream tests",
9
+ "license": "MIT",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/1466109846/ua-pro.git"
13
+ },
14
+ "homepage": "https://github.com/1466109846/ua-pro#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/1466109846/ua-pro/issues"
17
+ },
18
+ "type": "module",
19
+ "main": "./index.mjs",
20
+ "exports": {
21
+ ".": "./index.mjs",
22
+ "./server": "./index.mjs"
23
+ },
24
+ "files": [
25
+ "index.mjs",
26
+ "setup.mjs",
27
+ "lib",
28
+ "src",
29
+ "ui",
30
+ "assets",
31
+ "README.md",
32
+ "NOTICE.md",
33
+ "LICENSE"
34
+ ],
35
+ "scripts": {
36
+ "check": "node --check index.mjs && node --check src/plugin.mjs && node --check lib/parameters.mjs",
37
+ "setup": "node setup.mjs"
38
+ },
39
+ "engines": {
40
+ "node": ">=22"
41
+ },
42
+ "keywords": [
43
+ "opencode",
44
+ "opencode-plugin",
45
+ "magpie",
46
+ "anthropic",
47
+ "claude-code",
48
+ "user-agent"
49
+ ],
50
+ "magpie": {
51
+ "name": "UA PRO",
52
+ "icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJNYWdwaWUgQ2xhdWRlIENvZGUgVUEiPgogIDxyZWN0IHdpZHRoPSI2NCIgaGVpZ2h0PSI2NCIgcng9IjE0IiBmaWxsPSIjM2IyYjI0Ii8+CiAgPGcgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZjBiNzliIiBzdHJva2Utd2lkdGg9IjMuMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIj4KICAgIDxwYXRoIGQ9Ik0zMiAxMS41djkuMk0zMiA0My4zdjkuMk0xMS41IDMyaDkuMk00My4zIDMyaDkuMiIvPgogICAgPHBhdGggZD0iTTE3LjQgMTcuNGw2LjUgNi41TTQwLjEgNDAuMWw2LjUgNi41TTQ2LjYgMTcuNGwtNi41IDYuNU0yMy45IDQwLjFsLTYuNSA2LjUiLz4KICA8L2c+CiAgPGNpcmNsZSBjeD0iMzIiIGN5PSIzMiIgcj0iNyIgZmlsbD0iI2U2YTE4NCIvPgogIDxjaXJjbGUgY3g9IjMyIiBjeT0iMzIiIHI9IjIuNCIgZmlsbD0iIzNiMmIyNCIvPgo8L3N2Zz4=",
53
+ "summary": {
54
+ "zh": "Codex 与 Claude 客户端模板、版本化应用回退、四协议连接与转换测试。",
55
+ "en": "Versioned Codex and Claude templates, four-protocol connections, previews and explicit tests."
56
+ },
57
+ "maxConcurrency": 4,
58
+ "options": {
59
+ "userAgent": "claude-cli/2.1.316 (external, claude-vscode, agent-sdk/0.3.316)",
60
+ "headers": {
61
+ "accept": "application/json",
62
+ "anthropic-beta": "claude-code-20250219,context-1m-2025-08-07,interleaved-thinking-2025-05-14,thinking-token-count-2026-05-13,context-management-2025-06-27,prompt-caching-scope-2026-01-05,per-turn-control-2026-07-01,mid-conversation-tool-changes-2026-07-01,advanced-tool-use-2025-11-20,effort-2025-11-24,dangerous-tool-use-2026-09-03,afk-mode-2026-01-31",
63
+ "anthropic-dangerous-direct-browser-access": "true",
64
+ "anthropic-version": "2023-06-01",
65
+ "x-app": "cli",
66
+ "x-stainless-arch": "x64",
67
+ "x-stainless-lang": "js",
68
+ "x-stainless-os": "Windows",
69
+ "x-stainless-package-version": "0.128.0",
70
+ "x-stainless-retry-count": "0",
71
+ "x-stainless-runtime": "node",
72
+ "x-stainless-runtime-version": "v26.12.1",
73
+ "x-stainless-timeout": "300"
74
+ },
75
+ "parameters": {
76
+ "defaults": {},
77
+ "overrides": {},
78
+ "remove": []
79
+ }
80
+ }
81
+ }
82
+ }
package/setup.mjs ADDED
@@ -0,0 +1,39 @@
1
+ import { createProfileStore } from './src/store.mjs';
2
+ import { startSetupServer } from './src/setup-server.mjs';
3
+ import { resolveProfilePath, parseSetupArgs, openBrowser } from './src/runtime.mjs';
4
+ import { createAccountManager } from './src/accounts.mjs';
5
+ import { dirname, join } from 'node:path';
6
+ import { createUAStore } from './src/ua-store.mjs';
7
+ import { createUAService } from './src/ua-service.mjs';
8
+
9
+ async function main() {
10
+ const args = parseSetupArgs(process.argv.slice(2));
11
+ if (args.help) {
12
+ console.log('UA PRO 设置\n\nnode setup.mjs [--config <绝对路径>] [--port <端口>] [--no-open]\n\n默认只监听 127.0.0.1,端口随机。MAGPIE_CLAUDE_PROFILE_FILE 可指定配置文件。\n远程宿主请使用 SSH 隧道。启动和保存配置不会访问上游;点击“检查连接”会读取模型列表,不发送聊天请求;账号页的刷新与签到会请求你填写的站点。');
13
+ return;
14
+ }
15
+ const store = createProfileStore(resolveProfilePath({ configPath: args.configPath }));
16
+ const accountManager = createAccountManager({ path: join(dirname(store.path), 'accounts.json') });
17
+ const uaStore = createUAStore(process.env.UA_PRO_FILE || join(dirname(store.path), 'ua-pro.json'));
18
+ const uaService = createUAService({ store: uaStore, legacyStore: store });
19
+ const session = await startSetupServer({ store, accountManager, uaService, port: args.port, finishOnSave: false });
20
+ console.log(`配置页面:${session.url}\n账号管理:${new URL('accounts', session.url).href}\n配置文件:${store.path}\n页面链接仅本次有效,请勿分享。`);
21
+ if (args.open && !await openBrowser(session.url)) console.log('未找到可用的浏览器,请手动打开上面的本地链接。');
22
+ const stop = () => { void session.close(); };
23
+ process.once('SIGINT', stop);
24
+ process.once('SIGTERM', stop);
25
+ const result = await session.finished;
26
+ process.removeListener('SIGINT', stop);
27
+ process.removeListener('SIGTERM', stop);
28
+ if (result.status === 'saved') console.log('配置已保存。上游地址、协议或模型列表改变后,请在 Magpie 重载插件。');
29
+ else if (result.status === 'managed') console.log('账号管理已完成,账号与额度记录已保存。');
30
+ else if (result.status === 'timeout') console.log('配置页面已超时;此前已保存的配置与账号记录保留。');
31
+ else console.log('配置已关闭;未提交的更改不保存,已保存的配置与账号记录保留。');
32
+ await new Promise((resolve) => setTimeout(resolve, 500));
33
+ await session.close();
34
+ }
35
+
36
+ main().catch(() => {
37
+ console.error('无法启动配置页面,请检查参数、配置文件格式与目录权限。运行 --help 查看用法。');
38
+ process.exitCode = 1;
39
+ });
@@ -0,0 +1,73 @@
1
+ import { isDeepStrictEqual } from 'node:util';
2
+ import { resolve } from 'node:path';
3
+ const nativeWrites = new Map();
4
+ const unscopedQueue = Symbol('native-account-writes');
5
+ import { AUTH_MARKER, connectionProfile } from './connection.mjs';
6
+ import { createDefaultProfile, mergeProfileSecrets, ProfileValidationError, redactProfile, VARIABLE_DEFINITIONS } from './profile.mjs';
7
+ function requireNativeAuth(auth) {
8
+ if (auth?.type !== 'api' || typeof auth.key !== 'string' || !auth.key.trim() || auth.key.trim() === AUTH_MARKER) throw new ProfileValidationError({ 'upstream.apiKey': '请先选择已配置密钥的原生 API 账号' });
9
+ }
10
+ /** Session-only drafts; writes are confined to the injected native account slot. */
11
+ export function createAccountProfileStore({ auth, savedProfile = null, provider = {}, defaults, path, readAuth, writeAuth }) {
12
+ requireNativeAuth(auth);
13
+ let currentAuth = structuredClone(auth);
14
+ let profile = connectionProfile(savedProfile, currentAuth, provider, defaults);
15
+ const queueKey = typeof path === 'string' ? (process.platform === 'win32' ? resolve(path).toLowerCase() : resolve(path)) : unscopedQueue;
16
+ async function load() { return structuredClone(profile); }
17
+ function getAuth() { return structuredClone(currentAuth); }
18
+ async function verifyCurrentAuth() {
19
+ if (!readAuth) return;
20
+ let latest;
21
+ try { latest = await readAuth(); }
22
+ catch { throw new Error('无法读取当前账号,请重新打开配置后重试'); }
23
+ if (!isDeepStrictEqual(latest, currentAuth)) throw new Error('当前账号已发生变化,请重新打开配置后重试');
24
+ }
25
+ async function prepareFromUi(raw, clearSecrets = []) {
26
+ await verifyCurrentAuth();
27
+ const draft = mergeProfileSecrets(raw, profile, clearSecrets);
28
+ requireNativeAuth({ type: 'api', key: draft.upstream.apiKey });
29
+ if (draft.upstream.apiKey !== profile.upstream.apiKey) {
30
+ // Changing the host key starts a new credential scope; old endpoint-specific extras cannot follow silently.
31
+ const old = profile.headers.extra.filter(item => item.enabled !== false);
32
+ const retained = draft.headers.extra.some(item => item.enabled !== false && old.some(entry => entry.name.toLowerCase() === item.name.toLowerCase() && entry.value === item.value));
33
+ if (retained) throw new ProfileValidationError({ 'headers.extra': 'API key 已改变,请移除旧凭据的附加请求头' });
34
+ const baseline = createDefaultProfile().headers;
35
+ const fields = {};
36
+ for (const [field, value] of Object.entries(draft.headers)) {
37
+ if (typeof value === 'string' && value.trim() && value !== baseline[field] && value === profile.headers[field]) {
38
+ fields[`headers.${field}`] = 'API key 已改变,请移除旧凭据的专用请求头';
39
+ }
40
+ }
41
+ if (Object.keys(fields).length) throw new ProfileValidationError(fields);
42
+ if (draft.upstream.authorizationValue && draft.upstream.authorizationValue === profile.upstream.authorizationValue) {
43
+ throw new ProfileValidationError({ 'upstream.authorizationValue': 'API key 已改变,请重新填写对应的 Authorization 或切换授权模式' });
44
+ }
45
+ }
46
+ return draft;
47
+ }
48
+ async function saveFromUi(raw, clearSecrets = []) {
49
+ const input = structuredClone(raw); const clears = structuredClone(clearSecrets);
50
+ const result = (nativeWrites.get(queueKey) ?? Promise.resolve()).then(async () => {
51
+ const nextProfile = await prepareFromUi(input, clears);
52
+ const nextAuth = structuredClone(currentAuth);
53
+ nextAuth.key = nextProfile.upstream.apiKey;
54
+ nextAuth.metadata = { ...nextAuth.metadata, baseURL: nextProfile.upstream.baseUrl, endpoint: nextProfile.upstream.baseUrl, claudeProfile: structuredClone(nextProfile) };
55
+ nextAuth.metadata.claudeProfile.upstream.apiKey = '';
56
+ await verifyCurrentAuth();
57
+ try { await writeAuth(structuredClone(nextAuth)); }
58
+ catch { throw new Error('账号配置保存失败,请重试;当前会话配置保留'); }
59
+ currentAuth = nextAuth; profile = nextProfile;
60
+ return structuredClone(profile);
61
+ });
62
+ const next = result.catch(() => {});
63
+ nativeWrites.set(queueKey, next);
64
+ next.finally(() => { if (nativeWrites.get(queueKey) === next) nativeWrites.delete(queueKey); });
65
+ return result;
66
+ }
67
+ async function publicInfo() {
68
+ return { profile: redactProfile(profile), configured: true,
69
+ secrets: { apiKey: Boolean(profile.upstream.apiKey), authorizationValue: Boolean(profile.upstream.authorizationValue) },
70
+ variables: VARIABLE_DEFINITIONS, storagePath: path };
71
+ }
72
+ return { path, load, prepareFromUi, saveFromUi, publicInfo, getAuth };
73
+ }