@pikecode/api-key-manager 1.1.0 → 1.1.2
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/package.json +1 -1
- package/src/commands/add.js +8 -12
- package/src/utils/codex-files.js +89 -109
package/package.json
CHANGED
package/src/commands/add.js
CHANGED
|
@@ -390,7 +390,7 @@ class ProviderAdder extends BaseCommand {
|
|
|
390
390
|
|
|
391
391
|
async importCodexConfig() {
|
|
392
392
|
try {
|
|
393
|
-
const { readCodexFiles } = require('../utils/codex-files');
|
|
393
|
+
const { readCodexFiles, extractBaseUrlFromConfigToml } = require('../utils/codex-files');
|
|
394
394
|
const codexFiles = await readCodexFiles();
|
|
395
395
|
|
|
396
396
|
if (!codexFiles.authJson) {
|
|
@@ -405,19 +405,15 @@ class ProviderAdder extends BaseCommand {
|
|
|
405
405
|
return null;
|
|
406
406
|
}
|
|
407
407
|
|
|
408
|
-
//
|
|
409
|
-
//
|
|
408
|
+
// 从 config.toml 中读取当前激活 provider 的 base_url
|
|
409
|
+
// 优先从 [model_providers.<key>] section 读取,兼容旧的顶层 api_base_url 格式
|
|
410
410
|
let baseUrl = null;
|
|
411
411
|
if (codexFiles.configToml) {
|
|
412
|
-
|
|
413
|
-
if (
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
const legacyMatch = codexFiles.configToml.match(/api_base\s*=\s*["']([^"']+)["']/);
|
|
418
|
-
if (legacyMatch) {
|
|
419
|
-
baseUrl = legacyMatch[1];
|
|
420
|
-
}
|
|
412
|
+
baseUrl = extractBaseUrlFromConfigToml(codexFiles.configToml);
|
|
413
|
+
if (!baseUrl) {
|
|
414
|
+
// 兼容旧格式(akm 之前错误写入的顶层字段)
|
|
415
|
+
const legacyMatch = codexFiles.configToml.match(/^api_base_url\s*=\s*["']([^"']+)["']/m);
|
|
416
|
+
if (legacyMatch) baseUrl = legacyMatch[1];
|
|
421
417
|
}
|
|
422
418
|
}
|
|
423
419
|
|
package/src/utils/codex-files.js
CHANGED
|
@@ -87,106 +87,83 @@ async function backupCodexFiles(codexHome = resolveCodexHome()) {
|
|
|
87
87
|
return backupDir;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
async function applyCodexProfile(profile, options = {}) {
|
|
91
|
-
if (!profile || (profile.configToml == null && profile.authJson == null)) {
|
|
92
|
-
throw new Error('Codex 配置为空,无法切换');
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const codexHome = await ensureCodexHome(profile.codexHome || options.codexHome);
|
|
96
|
-
const { configTomlPath, authJsonPath } = buildCodexPaths(codexHome);
|
|
97
|
-
|
|
98
|
-
const backupDir = await backupCodexFiles(codexHome);
|
|
99
|
-
|
|
100
|
-
if (profile.configToml != null) {
|
|
101
|
-
await fs.writeFile(configTomlPath, profile.configToml, 'utf8');
|
|
102
|
-
await setSecurePermissions(configTomlPath);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (profile.authJson != null) {
|
|
106
|
-
await fs.writeFile(authJsonPath, profile.authJson, 'utf8');
|
|
107
|
-
await setSecurePermissions(authJsonPath);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return { codexHome, backupDir };
|
|
111
|
-
}
|
|
112
|
-
|
|
113
90
|
/**
|
|
114
|
-
*
|
|
91
|
+
* 移除 config.toml 顶层(第一个 [section] 之前)的 api_base_url 字段
|
|
92
|
+
* 只清理顶层,不影响 [model_providers.xxx] section 内的 base_url
|
|
115
93
|
* @param {string} configToml - 现有的 config.toml 内容
|
|
116
94
|
* @returns {string} 更新后的 config.toml 内容
|
|
117
95
|
*/
|
|
118
|
-
function
|
|
119
|
-
if (!configToml)
|
|
120
|
-
// 如果没有 config.toml,创建一个最小配置
|
|
121
|
-
return 'preferred_auth_method = "apikey"\n';
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// 移除所有已存在的 preferred_auth_method 行(防止重复)
|
|
125
|
-
const authMethodRegex = /^preferred_auth_method\s*=\s*["']?[^"'\n]*["']?\s*\n?/gm;
|
|
126
|
-
let cleanedConfig = configToml.replace(authMethodRegex, '');
|
|
127
|
-
|
|
128
|
-
// 清理可能存在的损坏字符(如孤立的 { 或空行堆积)
|
|
129
|
-
cleanedConfig = cleanedConfig.replace(/^[{}]+\s*\n?/gm, '');
|
|
130
|
-
cleanedConfig = cleanedConfig.replace(/^\n{3,}/gm, '\n\n');
|
|
96
|
+
function removeTopLevelApiBaseUrl(configToml) {
|
|
97
|
+
if (!configToml) return configToml;
|
|
131
98
|
|
|
132
|
-
|
|
133
|
-
const
|
|
99
|
+
const sectionStart = configToml.search(/^\[/m);
|
|
100
|
+
const topLevel = sectionStart === -1 ? configToml : configToml.slice(0, sectionStart);
|
|
101
|
+
const rest = sectionStart === -1 ? '' : configToml.slice(sectionStart);
|
|
134
102
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
103
|
+
const cleaned = topLevel.replace(/^api_base_url\s*=\s*["']?[^"'\n]*["']?\s*\n?/m, '');
|
|
104
|
+
return cleaned + rest;
|
|
105
|
+
}
|
|
139
106
|
|
|
140
|
-
|
|
107
|
+
/**
|
|
108
|
+
* 从 config.toml 中提取当前激活的 model_provider 的 base_url
|
|
109
|
+
* 读取 model_provider 字段,再从对应的 [model_providers.<key>] section 中取 base_url
|
|
110
|
+
* @param {string} configToml - config.toml 内容
|
|
111
|
+
* @returns {string|null} base_url 或 null
|
|
112
|
+
*/
|
|
113
|
+
function extractBaseUrlFromConfigToml(configToml) {
|
|
114
|
+
if (!configToml) return null;
|
|
115
|
+
|
|
116
|
+
// 读取 model_provider = "xxx"
|
|
117
|
+
const providerMatch = configToml.match(/^model_provider\s*=\s*["']([^"']+)["']/m);
|
|
118
|
+
if (!providerMatch) return null;
|
|
119
|
+
|
|
120
|
+
const providerKey = providerMatch[1];
|
|
121
|
+
|
|
122
|
+
// 在对应 section 中找 base_url
|
|
123
|
+
const sectionRegex = new RegExp(
|
|
124
|
+
`\\[model_providers\\.${providerKey}\\][^\\[]*base_url\\s*=\\s*["']([^"']+)["']`,
|
|
125
|
+
's'
|
|
126
|
+
);
|
|
127
|
+
const urlMatch = configToml.match(sectionRegex);
|
|
128
|
+
return urlMatch ? urlMatch[1] : null;
|
|
141
129
|
}
|
|
142
130
|
|
|
143
131
|
/**
|
|
144
|
-
* 更新 config.toml 中的
|
|
132
|
+
* 更新 config.toml 中的 model_provider 和对应的 [model_providers.akm] section
|
|
133
|
+
* 使用固定 key "akm" 管理 akm 切换的供应商,不影响用户其他自定义配置
|
|
145
134
|
* @param {string} configToml - 现有的 config.toml 内容
|
|
146
|
-
* @param {string
|
|
135
|
+
* @param {string} baseUrl - 新供应商的 base_url
|
|
147
136
|
* @returns {string} 更新后的 config.toml 内容
|
|
148
137
|
*/
|
|
149
|
-
function
|
|
150
|
-
if (!configToml)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const sectionMatch = configToml.match(/^\[/m);
|
|
171
|
-
if (sectionMatch) {
|
|
172
|
-
const insertPos = configToml.indexOf(sectionMatch[0]);
|
|
173
|
-
const before = configToml.slice(0, insertPos);
|
|
174
|
-
const after = configToml.slice(insertPos);
|
|
175
|
-
// 确保前面有换行分隔
|
|
176
|
-
const separator = before.endsWith('\n') ? '' : '\n';
|
|
177
|
-
return before + separator + newLine + '\n\n' + after;
|
|
178
|
-
}
|
|
179
|
-
// 没有 section,在文件末尾添加
|
|
180
|
-
const separator = configToml.endsWith('\n') ? '' : '\n';
|
|
181
|
-
return configToml + separator + newLine + '\n';
|
|
138
|
+
function updateModelProvider(configToml, baseUrl) {
|
|
139
|
+
if (!configToml) configToml = '';
|
|
140
|
+
|
|
141
|
+
const providerKey = 'akm';
|
|
142
|
+
const newSection = [
|
|
143
|
+
`[model_providers.${providerKey}]`,
|
|
144
|
+
`name = "${providerKey}"`,
|
|
145
|
+
`base_url = "${baseUrl}"`,
|
|
146
|
+
'wire_api = "responses"',
|
|
147
|
+
'requires_openai_auth = true'
|
|
148
|
+
].join('\n');
|
|
149
|
+
|
|
150
|
+
// 更新顶层 model_provider 字段
|
|
151
|
+
let result = configToml.match(/^model_provider\s*=/m)
|
|
152
|
+
? configToml.replace(/^model_provider\s*=\s*["'][^"'\n]*["']/m, `model_provider = "${providerKey}"`)
|
|
153
|
+
: `model_provider = "${providerKey}"\n` + configToml;
|
|
154
|
+
|
|
155
|
+
// 替换或追加 [model_providers.akm] section
|
|
156
|
+
const sectionRegex = /\[model_providers\.akm\](?:\n(?!\[)[^\n]*)*\n?/;
|
|
157
|
+
if (result.match(sectionRegex)) {
|
|
158
|
+
result = result.replace(sectionRegex, newSection + '\n');
|
|
182
159
|
} else {
|
|
183
|
-
|
|
184
|
-
return configToml.replace(baseUrlRegex, '');
|
|
160
|
+
result = result.trimEnd() + '\n\n' + newSection + '\n';
|
|
185
161
|
}
|
|
162
|
+
|
|
163
|
+
return result;
|
|
186
164
|
}
|
|
187
165
|
|
|
188
166
|
/**
|
|
189
|
-
* 构建 auth.json 内容
|
|
190
167
|
* @param {string} apiKey - API Key
|
|
191
168
|
* @returns {string} auth.json 内容
|
|
192
169
|
*/
|
|
@@ -195,11 +172,14 @@ function buildAuthJson(apiKey) {
|
|
|
195
172
|
}
|
|
196
173
|
|
|
197
174
|
/**
|
|
198
|
-
* 应用 Codex 配置(写入 config.toml
|
|
199
|
-
*
|
|
175
|
+
* 应用 Codex 配置(写入 auth.json,更新 config.toml 中的 provider 路由)
|
|
176
|
+
* akm 负责:
|
|
177
|
+
* 1. 写入 auth.json(API Key)
|
|
178
|
+
* 2. 更新 config.toml 中的 model_provider 和 [model_providers.akm] section
|
|
179
|
+
* 3. 清理之前错误写入的顶层 api_base_url 字段
|
|
200
180
|
* @param {object} config - 供应商配置
|
|
201
181
|
* @param {object} options - 选项
|
|
202
|
-
* @returns {Promise<{codexHome: string
|
|
182
|
+
* @returns {Promise<{codexHome: string}>}
|
|
203
183
|
*/
|
|
204
184
|
async function applyCodexConfig(config, options = {}) {
|
|
205
185
|
if (!config || !config.authToken) {
|
|
@@ -209,41 +189,41 @@ async function applyCodexConfig(config, options = {}) {
|
|
|
209
189
|
const codexHome = await ensureCodexHome(options.codexHome);
|
|
210
190
|
const { configTomlPath, authJsonPath } = buildCodexPaths(codexHome);
|
|
211
191
|
|
|
212
|
-
//
|
|
213
|
-
const backupDir = await backupCodexFiles(codexHome);
|
|
214
|
-
|
|
215
|
-
// 读取现有 config.toml
|
|
216
|
-
let existingConfigToml = null;
|
|
217
|
-
if (await fs.pathExists(configTomlPath)) {
|
|
218
|
-
existingConfigToml = await fs.readFile(configTomlPath, 'utf8');
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// 确保设置了 preferred_auth_method = "apikey"
|
|
222
|
-
let updatedConfigToml = ensureApiKeyAuthMethod(existingConfigToml);
|
|
223
|
-
|
|
224
|
-
// 更新 api_base_url(如果有则设置,没有则移除)
|
|
225
|
-
updatedConfigToml = updateApiBaseUrl(updatedConfigToml, config.baseUrl || null);
|
|
226
|
-
|
|
227
|
-
await fs.writeFile(configTomlPath, updatedConfigToml, 'utf8');
|
|
228
|
-
await setSecurePermissions(configTomlPath);
|
|
229
|
-
|
|
230
|
-
// 写入 auth.json
|
|
192
|
+
// 写入 auth.json(API Key)
|
|
231
193
|
const authJsonContent = buildAuthJson(config.authToken);
|
|
232
194
|
await fs.writeFile(authJsonPath, authJsonContent, 'utf8');
|
|
233
195
|
await setSecurePermissions(authJsonPath);
|
|
234
196
|
|
|
235
|
-
|
|
197
|
+
// 更新 config.toml:设置 model_provider + [model_providers.akm],清理旧的顶层 api_base_url
|
|
198
|
+
if (config.baseUrl) {
|
|
199
|
+
const existingToml = await fs.pathExists(configTomlPath)
|
|
200
|
+
? await fs.readFile(configTomlPath, 'utf8')
|
|
201
|
+
: '';
|
|
202
|
+
let updatedToml = removeTopLevelApiBaseUrl(existingToml);
|
|
203
|
+
updatedToml = updateModelProvider(updatedToml, config.baseUrl);
|
|
204
|
+
await fs.writeFile(configTomlPath, updatedToml, 'utf8');
|
|
205
|
+
await setSecurePermissions(configTomlPath);
|
|
206
|
+
} else if (await fs.pathExists(configTomlPath)) {
|
|
207
|
+
// 没有 baseUrl,只清理旧的顶层 api_base_url
|
|
208
|
+
const existingToml = await fs.readFile(configTomlPath, 'utf8');
|
|
209
|
+
const cleanedToml = removeTopLevelApiBaseUrl(existingToml);
|
|
210
|
+
if (cleanedToml !== existingToml) {
|
|
211
|
+
await fs.writeFile(configTomlPath, cleanedToml, 'utf8');
|
|
212
|
+
await setSecurePermissions(configTomlPath);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return { codexHome };
|
|
236
217
|
}
|
|
237
218
|
|
|
238
219
|
module.exports = {
|
|
239
220
|
resolveCodexHome,
|
|
240
221
|
buildCodexPaths,
|
|
241
222
|
readCodexFiles,
|
|
242
|
-
applyCodexProfile,
|
|
243
223
|
applyCodexConfig,
|
|
244
224
|
backupCodexFiles,
|
|
245
|
-
|
|
246
|
-
|
|
225
|
+
removeTopLevelApiBaseUrl,
|
|
226
|
+
extractBaseUrlFromConfigToml,
|
|
227
|
+
updateModelProvider,
|
|
247
228
|
buildAuthJson
|
|
248
229
|
};
|
|
249
|
-
|