base_parts_ai 1.0.44 → 1.0.45
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/lib/setapi_cx.js +116 -149
- package/package.json +4 -3
- package/test_home/.claude/settings.json +1 -1
- package/test_home/.codex/AGENTS.md +1 -1
- package/test_home/.codex/config.toml +1 -1
- package/test_jcc.js +1 -1
- package/test_jcx.js +17 -1
package/lib/setapi_cx.js
CHANGED
|
@@ -10,6 +10,7 @@ var fs = require('fs');
|
|
|
10
10
|
var inquirer = require('inquirer');
|
|
11
11
|
var utils = require('./ai_utils');
|
|
12
12
|
var childProcess = require('child_process');
|
|
13
|
+
var TOML = require('smol-toml');
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* 读取文本文件,文件不存在时返回空字符串
|
|
@@ -47,27 +48,63 @@ function getEnvString(channel, key) {
|
|
|
47
48
|
return typeof value === 'string' ? value.trim() : '';
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
/**
|
|
52
|
+
* 解析 Codex TOML 配置,格式错误时阻止继续覆盖用户配置
|
|
53
|
+
* @param {string} toml TOML 内容
|
|
54
|
+
* @returns {object}
|
|
55
|
+
*/
|
|
56
|
+
function parseTomlConfig(toml) {
|
|
57
|
+
if (!toml || !String(toml).trim()) {
|
|
58
|
+
return {};
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
return TOML.parse(toml);
|
|
62
|
+
} catch (e) {
|
|
63
|
+
throw new Error('Codex config.toml 格式错误,已停止写入以避免破坏配置: ' + e.message);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 将 Codex TOML 配置对象序列化为文本
|
|
69
|
+
* @param {object} config TOML 配置对象
|
|
70
|
+
* @returns {string}
|
|
71
|
+
*/
|
|
72
|
+
function stringifyTomlConfig(config) {
|
|
73
|
+
var content = TOML.stringify(config || {});
|
|
74
|
+
return content ? content.replace(/\s+$/, '') + '\n' : '';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 确保对象字段是可写 TOML table
|
|
79
|
+
* @param {object} parent 父级对象
|
|
80
|
+
* @param {string} key 字段名
|
|
81
|
+
* @returns {object}
|
|
82
|
+
*/
|
|
83
|
+
function ensureTable(parent, key) {
|
|
84
|
+
if (!parent[key] || typeof parent[key] !== 'object' || Array.isArray(parent[key])) {
|
|
85
|
+
// 字段不是 table 时用对象替换,避免生成非法的嵌套配置
|
|
86
|
+
parent[key] = {};
|
|
87
|
+
}
|
|
88
|
+
return parent[key];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 判断对象是否没有自有字段
|
|
93
|
+
* @param {object} value 待检查对象
|
|
94
|
+
* @returns {boolean}
|
|
95
|
+
*/
|
|
96
|
+
function isEmptyObject(value) {
|
|
97
|
+
return value && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 0;
|
|
98
|
+
}
|
|
99
|
+
|
|
50
100
|
/**
|
|
51
101
|
* 从 TOML 文本中提取顶层 model_provider
|
|
52
102
|
* @param {string} toml TOML 内容
|
|
53
103
|
* @returns {string}
|
|
54
104
|
*/
|
|
55
105
|
function getTopLevelModelProvider(toml) {
|
|
56
|
-
var
|
|
57
|
-
|
|
58
|
-
for (var i = 0; i < lines.length; i++) {
|
|
59
|
-
var line = lines[i];
|
|
60
|
-
if (/^\s*\[/.test(line)) {
|
|
61
|
-
inSection = true;
|
|
62
|
-
}
|
|
63
|
-
if (!inSection) {
|
|
64
|
-
var match = line.match(/^\s*model_provider\s*=\s*["']([^"']+)["']/);
|
|
65
|
-
if (match) {
|
|
66
|
-
return match[1];
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return '';
|
|
106
|
+
var config = parseTomlConfig(toml);
|
|
107
|
+
return typeof config.model_provider === 'string' ? config.model_provider : '';
|
|
71
108
|
}
|
|
72
109
|
|
|
73
110
|
/**
|
|
@@ -76,26 +113,13 @@ function getTopLevelModelProvider(toml) {
|
|
|
76
113
|
* @returns {string}
|
|
77
114
|
*/
|
|
78
115
|
function getJcodexBaseUrl(toml) {
|
|
79
|
-
var
|
|
80
|
-
var
|
|
81
|
-
|
|
82
|
-
var line = lines[i];
|
|
83
|
-
if (/^\s*\[/.test(line)) {
|
|
84
|
-
inJcodex = /^\s*\[model_providers\.jcodex\]\s*$/.test(line);
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
if (inJcodex) {
|
|
88
|
-
var match = line.match(/^\s*base_url\s*=\s*["']([^"']*)["']/);
|
|
89
|
-
if (match) {
|
|
90
|
-
return match[1];
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
return '';
|
|
116
|
+
var config = parseTomlConfig(toml);
|
|
117
|
+
var provider = config.model_providers && config.model_providers.jcodex;
|
|
118
|
+
return provider && typeof provider.base_url === 'string' ? provider.base_url : '';
|
|
95
119
|
}
|
|
96
120
|
|
|
97
121
|
/**
|
|
98
|
-
* 转义 TOML
|
|
122
|
+
* 转义 TOML 字符串值,保留给测试和旧调用方使用
|
|
99
123
|
* @param {string} value 原始字符串
|
|
100
124
|
* @returns {string}
|
|
101
125
|
*/
|
|
@@ -103,25 +127,39 @@ function escapeTomlString(value) {
|
|
|
103
127
|
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
104
128
|
}
|
|
105
129
|
|
|
130
|
+
/**
|
|
131
|
+
* 判断 TOML 顶层是否已经存在指定配置项
|
|
132
|
+
* @param {string} toml TOML 内容
|
|
133
|
+
* @param {string} key 顶层配置项名称
|
|
134
|
+
* @returns {boolean}
|
|
135
|
+
*/
|
|
136
|
+
function hasTopLevelKey(toml, key) {
|
|
137
|
+
var config = parseTomlConfig(toml);
|
|
138
|
+
return Object.prototype.hasOwnProperty.call(config, key);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 确保 Codex 配置包含默认 sandbox_mode,缺失时写入顶层配置
|
|
143
|
+
* @param {string} toml TOML 内容
|
|
144
|
+
* @returns {string}
|
|
145
|
+
*/
|
|
146
|
+
function ensureSandboxMode(toml) {
|
|
147
|
+
var config = parseTomlConfig(toml);
|
|
148
|
+
if (!Object.prototype.hasOwnProperty.call(config, 'sandbox_mode')) {
|
|
149
|
+
config.sandbox_mode = 'danger-full-access';
|
|
150
|
+
}
|
|
151
|
+
return stringifyTomlConfig(config);
|
|
152
|
+
}
|
|
153
|
+
|
|
106
154
|
/**
|
|
107
155
|
* 删除顶层 model_provider 字段,保留其它配置段落
|
|
108
156
|
* @param {string} toml TOML 内容
|
|
109
157
|
* @returns {string}
|
|
110
158
|
*/
|
|
111
159
|
function removeTopLevelModelProvider(toml) {
|
|
112
|
-
var
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
lines.forEach(function (line) {
|
|
116
|
-
if (/^\s*\[/.test(line)) {
|
|
117
|
-
inSection = true;
|
|
118
|
-
}
|
|
119
|
-
if (!inSection && /^\s*model_provider\s*=/.test(line)) {
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
kept.push(line);
|
|
123
|
-
});
|
|
124
|
-
return kept.join('\n').replace(/\n{3,}/g, '\n\n').replace(/\s+$/, '') + '\n';
|
|
160
|
+
var config = parseTomlConfig(toml);
|
|
161
|
+
delete config.model_provider;
|
|
162
|
+
return stringifyTomlConfig(config);
|
|
125
163
|
}
|
|
126
164
|
|
|
127
165
|
/**
|
|
@@ -130,22 +168,14 @@ function removeTopLevelModelProvider(toml) {
|
|
|
130
168
|
* @returns {string}
|
|
131
169
|
*/
|
|
132
170
|
function removeJcodexProviderBlock(toml) {
|
|
133
|
-
var
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
skipping = true;
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
if (skipping && /^\s*\[/.test(line)) {
|
|
142
|
-
skipping = false;
|
|
171
|
+
var config = parseTomlConfig(toml);
|
|
172
|
+
if (config.model_providers && typeof config.model_providers === 'object' && !Array.isArray(config.model_providers)) {
|
|
173
|
+
delete config.model_providers.jcodex;
|
|
174
|
+
if (isEmptyObject(config.model_providers)) {
|
|
175
|
+
delete config.model_providers;
|
|
143
176
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
return kept.join('\n').replace(/\n{3,}/g, '\n\n').replace(/\s+$/, '');
|
|
177
|
+
}
|
|
178
|
+
return stringifyTomlConfig(config);
|
|
149
179
|
}
|
|
150
180
|
|
|
151
181
|
/**
|
|
@@ -156,44 +186,18 @@ function removeJcodexProviderBlock(toml) {
|
|
|
156
186
|
* @returns {string}
|
|
157
187
|
*/
|
|
158
188
|
function setJcodexProvider(toml, baseUrl, wireApi) {
|
|
159
|
-
var
|
|
160
|
-
var
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
result.push('model_provider = "jcodex"');
|
|
167
|
-
inserted = true;
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
if (!inserted && /^\s*\[/.test(line)) {
|
|
171
|
-
result.push('model_provider = "jcodex"');
|
|
172
|
-
result.push('');
|
|
173
|
-
inserted = true;
|
|
174
|
-
}
|
|
175
|
-
result.push(line);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
if (!inserted) {
|
|
179
|
-
if (result.length && result[result.length - 1].trim() !== '') {
|
|
180
|
-
result.push('');
|
|
181
|
-
}
|
|
182
|
-
result.unshift('model_provider = "jcodex"');
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
var body = result.join('\n').replace(/\n{3,}/g, '\n\n').replace(/\s+$/, '');
|
|
186
|
-
if (body) {
|
|
187
|
-
body += '\n\n';
|
|
188
|
-
}
|
|
189
|
-
body += '[model_providers.jcodex]\n';
|
|
190
|
-
body += 'name = "jcodex"\n';
|
|
191
|
-
body += 'base_url = "' + escapeTomlString(baseUrl) + '"\n';
|
|
189
|
+
var config = parseTomlConfig(toml);
|
|
190
|
+
var providers = ensureTable(config, 'model_providers');
|
|
191
|
+
config.model_provider = 'jcodex';
|
|
192
|
+
providers.jcodex = {
|
|
193
|
+
name: 'jcodex',
|
|
194
|
+
base_url: baseUrl,
|
|
195
|
+
};
|
|
192
196
|
// wire_api 是 Codex provider 私有字段,非空时强制写入到 model_providers.jcodex 下
|
|
193
197
|
if (wireApi) {
|
|
194
|
-
|
|
198
|
+
providers.jcodex.wire_api = wireApi;
|
|
195
199
|
}
|
|
196
|
-
return
|
|
200
|
+
return stringifyTomlConfig(config);
|
|
197
201
|
}
|
|
198
202
|
|
|
199
203
|
/**
|
|
@@ -202,20 +206,13 @@ function setJcodexProvider(toml, baseUrl, wireApi) {
|
|
|
202
206
|
* @returns {string}
|
|
203
207
|
*/
|
|
204
208
|
function removeJcodexWireApi(toml) {
|
|
205
|
-
var
|
|
206
|
-
var
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
// wire_api 为空时只删除 jcodex provider 下的私有字段,不影响其它段落
|
|
213
|
-
if (inJcodex && /^\s*wire_api\s*=/.test(line)) {
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
kept.push(line);
|
|
217
|
-
});
|
|
218
|
-
return kept.join('\n').replace(/\n{3,}/g, '\n\n').replace(/\s+$/, '') + '\n';
|
|
209
|
+
var config = parseTomlConfig(toml);
|
|
210
|
+
var provider = config.model_providers && config.model_providers.jcodex;
|
|
211
|
+
// wire_api 为空时只删除 jcodex provider 下的私有字段,不影响其它段落
|
|
212
|
+
if (provider && typeof provider === 'object' && !Array.isArray(provider)) {
|
|
213
|
+
delete provider.wire_api;
|
|
214
|
+
}
|
|
215
|
+
return stringifyTomlConfig(config);
|
|
219
216
|
}
|
|
220
217
|
|
|
221
218
|
/**
|
|
@@ -225,47 +222,14 @@ function removeJcodexWireApi(toml) {
|
|
|
225
222
|
* @returns {string}
|
|
226
223
|
*/
|
|
227
224
|
function setJcodexWireApi(toml, wireApi) {
|
|
228
|
-
var
|
|
229
|
-
var
|
|
230
|
-
var
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
function appendWireApiLine() {
|
|
234
|
-
// 写入前去掉段尾空行,确保 wire_api 留在 jcodex 段内部
|
|
235
|
-
while (kept.length && kept[kept.length - 1].trim() === '') {
|
|
236
|
-
kept.pop();
|
|
237
|
-
}
|
|
238
|
-
kept.push('wire_api = "' + escapeTomlString(wireApi) + '"');
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
lines.forEach(function (line) {
|
|
242
|
-
if (/^\s*\[/.test(line)) {
|
|
243
|
-
if (inJcodex && !inserted) {
|
|
244
|
-
appendWireApiLine();
|
|
245
|
-
kept.push('');
|
|
246
|
-
inserted = true;
|
|
247
|
-
}
|
|
248
|
-
inJcodex = /^\s*\[model_providers\.jcodex\]\s*$/.test(line);
|
|
249
|
-
foundJcodex = foundJcodex || inJcodex;
|
|
250
|
-
}
|
|
251
|
-
kept.push(line);
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
if (foundJcodex && !inserted) {
|
|
255
|
-
appendWireApiLine();
|
|
256
|
-
inserted = true;
|
|
257
|
-
}
|
|
258
|
-
if (!foundJcodex) {
|
|
259
|
-
var body = kept.join('\n').replace(/\n{3,}/g, '\n\n').replace(/\s+$/, '');
|
|
260
|
-
if (body) {
|
|
261
|
-
body += '\n\n';
|
|
262
|
-
}
|
|
263
|
-
body += '[model_providers.jcodex]\n';
|
|
264
|
-
body += 'name = "jcodex"\n';
|
|
265
|
-
body += 'wire_api = "' + escapeTomlString(wireApi) + '"\n';
|
|
266
|
-
return body;
|
|
225
|
+
var config = parseTomlConfig(toml);
|
|
226
|
+
var providers = ensureTable(config, 'model_providers');
|
|
227
|
+
var provider = ensureTable(providers, 'jcodex');
|
|
228
|
+
if (!provider.name) {
|
|
229
|
+
provider.name = 'jcodex';
|
|
267
230
|
}
|
|
268
|
-
|
|
231
|
+
provider.wire_api = wireApi;
|
|
232
|
+
return stringifyTomlConfig(config);
|
|
269
233
|
}
|
|
270
234
|
|
|
271
235
|
/**
|
|
@@ -453,6 +417,7 @@ async function setapiCx(cmd, buildCfg) {
|
|
|
453
417
|
toml = removeTopLevelModelProvider(toml);
|
|
454
418
|
toml = wireApi ? setJcodexWireApi(toml, wireApi) : removeJcodexWireApi(toml);
|
|
455
419
|
}
|
|
420
|
+
toml = ensureSandboxMode(toml);
|
|
456
421
|
writeTextFile(buildCfg.codexConfigPath, toml);
|
|
457
422
|
|
|
458
423
|
await refreshAgentsFile(buildCfg.codexAgentsPath);
|
|
@@ -470,6 +435,8 @@ setapiCx._private = {
|
|
|
470
435
|
getTopLevelModelProvider: getTopLevelModelProvider,
|
|
471
436
|
getJcodexBaseUrl: getJcodexBaseUrl,
|
|
472
437
|
escapeTomlString: escapeTomlString,
|
|
438
|
+
hasTopLevelKey: hasTopLevelKey,
|
|
439
|
+
ensureSandboxMode: ensureSandboxMode,
|
|
473
440
|
removeTopLevelModelProvider: removeTopLevelModelProvider,
|
|
474
441
|
removeJcodexProviderBlock: removeJcodexProviderBlock,
|
|
475
442
|
removeJcodexWireApi: removeJcodexWireApi,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "base_parts_ai",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.45",
|
|
4
4
|
"description": "jaskle base_parts_ai",
|
|
5
5
|
"main": "./main.js",
|
|
6
6
|
"registry": true,
|
|
@@ -16,12 +16,13 @@
|
|
|
16
16
|
"jcx": "./bin/jcx.js"
|
|
17
17
|
},
|
|
18
18
|
"engines": {
|
|
19
|
-
"node": ">=
|
|
19
|
+
"node": ">=18"
|
|
20
20
|
},
|
|
21
21
|
"author": "jaskle",
|
|
22
22
|
"license": "ISC",
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"commander": "^14.0.3",
|
|
25
|
-
"inquirer": "^8.2.7"
|
|
25
|
+
"inquirer": "^8.2.7",
|
|
26
|
+
"smol-toml": "^1.6.1"
|
|
26
27
|
}
|
|
27
28
|
}
|
package/test_jcc.js
CHANGED
|
@@ -206,7 +206,7 @@ console.log('\n======= Test 3: setapi 写入渠道配置(envObj 合并)=====
|
|
|
206
206
|
ok = ok && (s.language === '简体中文');
|
|
207
207
|
ok = ok && (s.autoUpdatesChannel === 'stable');
|
|
208
208
|
ok = ok && (typeof s.hooks === 'object');
|
|
209
|
-
ok = ok && (aiTipCommand.indexOf('
|
|
209
|
+
ok = ok && (aiTipCommand.indexOf('cli=cc-' + utils.getAiTipOsParam()) !== -1);
|
|
210
210
|
ok = ok && (aiTipCommand.indexOf('%OS%') === -1);
|
|
211
211
|
ok = ok && (j.keys[selected.id] === newKey);
|
|
212
212
|
ok = ok && (j.keys.__lastManualAuthToken === newKey);
|
package/test_jcx.js
CHANGED
|
@@ -157,6 +157,21 @@ async function runTests() {
|
|
|
157
157
|
if (!ok) { process.exit(1); }
|
|
158
158
|
})();
|
|
159
159
|
|
|
160
|
+
// ============================================================
|
|
161
|
+
// 测试 2.1:缺失 sandbox_mode 时写入默认顶层配置
|
|
162
|
+
// ============================================================
|
|
163
|
+
console.log('\n======= jcx Test 2.1: 缺失 sandbox_mode 自动补齐 =======');
|
|
164
|
+
(function () {
|
|
165
|
+
var original = 'model_provider = "jcodex"\n\n[tui]\ntheme = "dark"\n';
|
|
166
|
+
var updated = privateApi.ensureSandboxMode(original);
|
|
167
|
+
var ok = true;
|
|
168
|
+
ok = ok && updated.indexOf('sandbox_mode = "danger-full-access"') !== -1;
|
|
169
|
+
ok = ok && updated.indexOf('sandbox_mode = "danger-full-access"') < updated.indexOf('[tui]');
|
|
170
|
+
ok = ok && privateApi.ensureSandboxMode(updated).match(/sandbox_mode\s*=/g).length === 1;
|
|
171
|
+
console.log('✅ Test 2.1 通过:', ok);
|
|
172
|
+
if (!ok) { process.exit(1); }
|
|
173
|
+
})();
|
|
174
|
+
|
|
160
175
|
// ============================================================
|
|
161
176
|
// 测试 3:空 base_url 删除顶层 model_provider,保留 provider 段
|
|
162
177
|
// ============================================================
|
|
@@ -200,10 +215,11 @@ async function runTests() {
|
|
|
200
215
|
ok = ok && auth.__jid === 'forced_channel';
|
|
201
216
|
ok = ok && auth.OPENAI_API_KEY === 'sk-forced-codex-key';
|
|
202
217
|
ok = ok && toml.indexOf('model_provider = "jcodex"') !== -1;
|
|
218
|
+
ok = ok && toml.indexOf('sandbox_mode = "danger-full-access"') !== -1;
|
|
203
219
|
ok = ok && toml.indexOf('base_url = "https://forced.example.com/v1"') !== -1;
|
|
204
220
|
ok = ok && toml.indexOf('wire_api = "wire-forced-provider-key"') !== -1;
|
|
205
221
|
ok = ok && agents.indexOf('cli=cx') !== -1;
|
|
206
|
-
ok = ok && agents.indexOf('
|
|
222
|
+
ok = ok && agents.indexOf('cli=cx-' + utils.getAiTipOsParam()) !== -1;
|
|
207
223
|
console.log('✅ Test 4 通过:', ok);
|
|
208
224
|
if (!ok) { process.exit(1); }
|
|
209
225
|
});
|