base_parts_ai 1.0.46 → 1.0.48

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/ai_utils.js CHANGED
@@ -8,7 +8,7 @@
8
8
  var fs = require('fs');
9
9
  var path = require('path');
10
10
  var http = require('http');
11
- var { execSync } = require('child_process');
11
+ var { execSync, spawnSync } = require('child_process');
12
12
 
13
13
  // ============================================================
14
14
  // AI 渠道列表(运行时由 fetchConfig 从服务端拉取填充)
@@ -24,6 +24,51 @@ var noticeList = [];
24
24
  // jcc.json 中保存最后一次手动输入 Token 的内部字段名
25
25
  var LAST_MANUAL_AUTH_TOKEN_KEY = '__lastManualAuthToken';
26
26
 
27
+ /**
28
+ * 从当前入口文件名推断正在运行的 CLI 命令名
29
+ * @returns {string} jcc、jcx 或空字符串
30
+ */
31
+ function getCurrentCliName() {
32
+ var entryName = path.basename(process.argv[1] || '').toLowerCase();
33
+ if (entryName === 'jcx' || entryName === 'jcx.js') return 'jcx';
34
+ if (entryName === 'jcc' || entryName === 'jcc.js') return 'jcc';
35
+ return '';
36
+ }
37
+
38
+ /**
39
+ * 升级完成后重新运行当前 CLI,并保留本次传入的全部参数
40
+ * @returns {void}
41
+ */
42
+ function rerunCurrentCli() {
43
+ var cliName = getCurrentCliName();
44
+ if (!cliName) {
45
+ console.log('[fetchConfig] 升级完成,未识别当前 CLI,请重新运行');
46
+ process.exit(0);
47
+ }
48
+
49
+ // 传递临时环境变量,避免新进程在版本缓存异常时反复触发升级。
50
+ var childEnv = Object.assign({}, process.env, {
51
+ BASE_PARTS_AI_SKIP_SELF_UPDATE: '1',
52
+ });
53
+ var args = process.argv.slice(2);
54
+ console.log('[fetchConfig] 升级完成,正在重新运行: ' + cliName + (args.length ? ' ' + args.join(' ') : ''));
55
+ var result = spawnSync(cliName, args, {
56
+ stdio: 'inherit',
57
+ shell: process.platform === 'win32',
58
+ env: childEnv,
59
+ });
60
+
61
+ if (result.error) {
62
+ console.warn('[fetchConfig] 重新运行失败:', result.error.message);
63
+ process.exit(1);
64
+ }
65
+ if (result.signal) {
66
+ console.warn('[fetchConfig] 重新运行被信号中断:', result.signal);
67
+ process.exit(1);
68
+ }
69
+ process.exit(typeof result.status === 'number' ? result.status : 0);
70
+ }
71
+
27
72
  /**
28
73
  * 从服务端拉取渠道配置,并检查 jcc 自身是否需要升级
29
74
  * @param {object} buildCfg getBuildCfg 返回的配置对象
@@ -76,7 +121,7 @@ function fetchConfig(buildCfg) {
76
121
  }
77
122
  return false;
78
123
  }
79
- if (newVer && semverGt(newVer, currentVer)) {
124
+ if (newVer && process.env.BASE_PARTS_AI_SKIP_SELF_UPDATE !== '1' && semverGt(newVer, currentVer)) {
80
125
  console.log('[fetchConfig] 检测到新版本 jcc: ' + newVer + '(当前 ' + currentVer + ')');
81
126
  var confirmed = "yes"
82
127
  // confirmed = await select({
@@ -93,8 +138,7 @@ function fetchConfig(buildCfg) {
93
138
  try {
94
139
  var tgzUrl = 'https://jdwfiles.oss-cn-hangzhou.aliyuncs.com/npm_pkg/base_parts_ai-' + newVer + '.tgz';
95
140
  execSync('npm install -g ' + tgzUrl + ' --force --registry=https://registry.npmmirror.com', { stdio: 'inherit' });
96
- console.log('[fetchConfig] 升级完成,请重新运行 jcc');
97
- process.exit(0);
141
+ rerunCurrentCli();
98
142
  } catch (e) {
99
143
  console.warn('[fetchConfig] 自动升级失败:', e.message);
100
144
  }
@@ -262,7 +306,7 @@ function getLastManualAuthToken(jccJson) {
262
306
 
263
307
  /**
264
308
  * 计算需要提示用户输入 Token 时的默认值
265
- * 优先使用当前渠道缓存,其次当前 settings,最后使用最后一次手动输入的 Token
309
+ * 优先使用当前渠道缓存;只有该渠道无缓存时,才使用当前渠道配置或最后一次手动 Token 兜底
266
310
  * @param {object} jccJson jcc.json 对象
267
311
  * @param {string} channelId 当前渠道 id
268
312
  * @param {object} settings Claude settings.json 对象
@@ -271,7 +315,8 @@ function getLastManualAuthToken(jccJson) {
271
315
  function getManualAuthTokenDefault(jccJson, channelId, settings) {
272
316
  var cachedToken = (jccJson.keys && jccJson.keys[channelId]) || '';
273
317
  var currentToken = (settings.env && settings.env.ANTHROPIC_AUTH_TOKEN) || '';
274
- return cachedToken || currentToken || getLastManualAuthToken(jccJson);
318
+ var currentChannelToken = (!settings.__jid || settings.__jid === channelId) ? currentToken : '';
319
+ return cachedToken || currentChannelToken || getLastManualAuthToken(jccJson) || currentToken;
275
320
  }
276
321
 
277
322
  /**
package/lib/setapi_cx.js CHANGED
@@ -152,16 +152,29 @@ function ensureSandboxMode(toml) {
152
152
  }
153
153
 
154
154
  /**
155
- * 强制写入 Codex TUI 编辑器换行快捷键,已有值会被覆盖为 alt-enter
155
+ * 强制删除 Codex TUI 编辑器 insert_newline 配置,交给官方默认行为处理
156
156
  * @param {string} toml TOML 内容
157
157
  * @returns {string}
158
158
  */
159
- function ensureEditorInsertNewlineKeymap(toml) {
159
+ function removeEditorInsertNewlineKeymap(toml) {
160
160
  var config = parseTomlConfig(toml);
161
- var tui = ensureTable(config, 'tui');
162
- var keymap = ensureTable(tui, 'keymap');
163
- var editor = ensureTable(keymap, 'editor');
164
- editor.insert_newline = 'alt-enter';
161
+ var tui = config.tui;
162
+ var keymap = tui && typeof tui === 'object' && !Array.isArray(tui) ? tui.keymap : null;
163
+ var editor = keymap && typeof keymap === 'object' && !Array.isArray(keymap) ? keymap.editor : null;
164
+
165
+ // 只删除官方已支持的 insert_newline,保留用户其它 TUI 快捷键配置。
166
+ if (editor && typeof editor === 'object' && !Array.isArray(editor)) {
167
+ delete editor.insert_newline;
168
+ if (isEmptyObject(editor)) {
169
+ delete keymap.editor;
170
+ }
171
+ if (isEmptyObject(keymap)) {
172
+ delete tui.keymap;
173
+ }
174
+ if (isEmptyObject(tui)) {
175
+ delete config.tui;
176
+ }
177
+ }
165
178
  return stringifyTomlConfig(config);
166
179
  }
167
180
 
@@ -356,6 +369,48 @@ function getCurrentCodexChannelId(auth, toml) {
356
369
  return channel ? channel.id : '';
357
370
  }
358
371
 
372
+ /**
373
+ * 记录最后一次由用户手动输入的 Codex Key
374
+ * 该值仅作为新渠道无专属 Key 时的默认输入提示
375
+ * @param {object} auth auth.json 对象
376
+ * @param {string} key 用户手动输入的 OPENAI_API_KEY
377
+ */
378
+ function saveLastManualOpenAiKey(auth, key) {
379
+ if (key) {
380
+ auth.__lastManualOpenAiKey = key;
381
+ }
382
+ }
383
+
384
+ /**
385
+ * 获取 Codex 手动 Key 输入框默认值
386
+ * 优先使用当前渠道专属缓存;没有缓存时才用当前渠道 Key 或最后手动 Key 兜底
387
+ * @param {object} auth auth.json 对象
388
+ * @param {string} channelId 当前选择的渠道 id
389
+ * @returns {string}
390
+ */
391
+ function getManualOpenAiKeyDefault(auth, channelId) {
392
+ var cachedKey = (auth.__keys && auth.__keys[channelId]) || '';
393
+ var currentKey = auth.OPENAI_API_KEY || '';
394
+ var currentChannelKey = (!auth.__jid || auth.__jid === channelId) ? currentKey : '';
395
+ return cachedKey || currentChannelKey || auth.__lastManualOpenAiKey || currentKey;
396
+ }
397
+
398
+ /**
399
+ * 保存或删除 Codex 渠道专属 Key 缓存
400
+ * @param {object} auth auth.json 对象
401
+ * @param {string} channelId 当前选择的渠道 id
402
+ * @param {string} key 用户手动输入的 OPENAI_API_KEY,空值表示删除该渠道缓存
403
+ */
404
+ function saveManualOpenAiKey(auth, channelId, key) {
405
+ if (!auth.__keys) { auth.__keys = {}; }
406
+ if (key) {
407
+ auth.__keys[channelId] = key;
408
+ saveLastManualOpenAiKey(auth, key);
409
+ } else {
410
+ delete auth.__keys[channelId];
411
+ }
412
+ }
413
+
359
414
  /**
360
415
  * 执行 Codex setapi 交互配置
361
416
  * @param {object} cmd 命令参数
@@ -407,9 +462,10 @@ async function setapiCx(cmd, buildCfg) {
407
462
  type: 'input',
408
463
  name: 'value',
409
464
  message: '请输入 OPENAI_API_KEY(输入空格后回车删除该配置):',
410
- default: auth.OPENAI_API_KEY || '',
465
+ default: getManualOpenAiKeyDefault(auth, selected.id),
411
466
  }]);
412
467
  forcedKey = keyAnswer.value.trim();
468
+ saveManualOpenAiKey(auth, selected.id, forcedKey);
413
469
  } else {
414
470
  console.log('已使用渠道配置中的 OPENAI_API_KEY,跳过 Key 输入。');
415
471
  }
@@ -432,7 +488,7 @@ async function setapiCx(cmd, buildCfg) {
432
488
  toml = wireApi ? setJcodexWireApi(toml, wireApi) : removeJcodexWireApi(toml);
433
489
  }
434
490
  toml = ensureSandboxMode(toml);
435
- toml = ensureEditorInsertNewlineKeymap(toml);
491
+ toml = removeEditorInsertNewlineKeymap(toml);
436
492
  writeTextFile(buildCfg.codexConfigPath, toml);
437
493
 
438
494
  await refreshAgentsFile(buildCfg.codexAgentsPath);
@@ -452,13 +508,16 @@ setapiCx._private = {
452
508
  escapeTomlString: escapeTomlString,
453
509
  hasTopLevelKey: hasTopLevelKey,
454
510
  ensureSandboxMode: ensureSandboxMode,
455
- ensureEditorInsertNewlineKeymap: ensureEditorInsertNewlineKeymap,
511
+ removeEditorInsertNewlineKeymap: removeEditorInsertNewlineKeymap,
456
512
  removeTopLevelModelProvider: removeTopLevelModelProvider,
457
513
  removeJcodexProviderBlock: removeJcodexProviderBlock,
458
514
  removeJcodexWireApi: removeJcodexWireApi,
459
515
  setJcodexWireApi: setJcodexWireApi,
460
516
  setJcodexProvider: setJcodexProvider,
461
517
  getCurrentCodexChannelId: getCurrentCodexChannelId,
518
+ saveLastManualOpenAiKey: saveLastManualOpenAiKey,
519
+ getManualOpenAiKeyDefault: getManualOpenAiKeyDefault,
520
+ saveManualOpenAiKey: saveManualOpenAiKey,
462
521
  resolveRequiredCodexVersion: resolveRequiredCodexVersion,
463
522
  ensureCodexVersion: ensureCodexVersion,
464
523
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "base_parts_ai",
3
- "version": "1.0.46",
3
+ "version": "1.0.48",
4
4
  "description": "jaskle base_parts_ai",
5
5
  "main": "./main.js",
6
6
  "registry": true,
@@ -1,4 +1,8 @@
1
1
  {
2
2
  "__jid": "version_channel",
3
- "OPENAI_API_KEY": "sk-version-openai-key"
3
+ "OPENAI_API_KEY": "sk-version-openai-key",
4
+ "__lastManualOpenAiKey": "sk-channel-b-new",
5
+ "__keys": {
6
+ "channel_b": "sk-channel-b-new"
7
+ }
4
8
  }
@@ -4,9 +4,6 @@ model_provider = "jcodex"
4
4
  [tui]
5
5
  theme = "dark"
6
6
 
7
- [tui.keymap.editor]
8
- insert_newline = "alt-enter"
9
-
10
7
  [model_providers.jcodex]
11
8
  name = "jcodex"
12
9
  base_url = "https://version.example.com/v1"
package/test_jcc.js CHANGED
@@ -378,12 +378,15 @@ console.log('\n======= Test 10: Token 默认值与强制配置 =======');
378
378
 
379
379
  settings.env.ANTHROPIC_AUTH_TOKEN = 'sk-current-settings';
380
380
  var currentDefault = utils.getManualAuthTokenDefault({ keys: {} }, 'channel_c', settings);
381
+ settings.__jid = 'other_channel';
382
+ var otherChannelDefault = utils.getManualAuthTokenDefault(jccJson, 'channel_b', settings);
381
383
 
382
384
  console.log('强制 Token:', forcedToken);
383
385
  console.log('空强制 Token:', emptyForcedToken || '(空)');
384
386
  console.log('渠道缓存默认值:', cachedDefault);
385
387
  console.log('最后手动默认值:', lastManualDefault);
386
388
  console.log('当前 settings 默认值:', currentDefault);
389
+ console.log('其它渠道 settings 兜底默认值:', otherChannelDefault);
387
390
 
388
391
  var ok = true;
389
392
  ok = ok && (forcedToken === 'sk-forced-token');
@@ -391,6 +394,7 @@ console.log('\n======= Test 10: Token 默认值与强制配置 =======');
391
394
  ok = ok && (cachedDefault === 'sk-channel-cache');
392
395
  ok = ok && (lastManualDefault === 'sk-last-manual');
393
396
  ok = ok && (currentDefault === 'sk-current-settings');
397
+ ok = ok && (otherChannelDefault === 'sk-last-manual');
394
398
 
395
399
  console.log('✅ Test 10 通过:', ok);
396
400
  if (!ok) { process.exit(1); }
package/test_jcx.js CHANGED
@@ -173,18 +173,18 @@ async function runTests() {
173
173
  })();
174
174
 
175
175
  // ============================================================
176
- // 测试 2.2:强制写入 TUI 编辑器换行快捷键
176
+ // 测试 2.2:强制删除 TUI 编辑器换行快捷键
177
177
  // ============================================================
178
- console.log('\n======= jcx Test 2.2: 强制写入 TUI keymap =======');
178
+ console.log('\n======= jcx Test 2.2: 强制删除 TUI keymap insert_newline =======');
179
179
  (function () {
180
- var original = '[tui]\ntheme = "dark"\n\n[tui.keymap.editor]\ninsert_newline = "enter"\n';
181
- var updated = privateApi.ensureEditorInsertNewlineKeymap(original);
180
+ var original = '[tui]\ntheme = "dark"\n\n[tui.keymap.editor]\ninsert_newline = "enter"\naccept_completion = "tab"\n';
181
+ var updated = privateApi.removeEditorInsertNewlineKeymap(original);
182
182
  var ok = true;
183
183
  ok = ok && updated.indexOf('[tui]') !== -1;
184
184
  ok = ok && updated.indexOf('theme = "dark"') !== -1;
185
185
  ok = ok && updated.indexOf('[tui.keymap.editor]') !== -1;
186
- ok = ok && updated.indexOf('insert_newline = "alt-enter"') !== -1;
187
- ok = ok && updated.indexOf('insert_newline = "enter"') === -1;
186
+ ok = ok && updated.indexOf('accept_completion = "tab"') !== -1;
187
+ ok = ok && updated.indexOf('insert_newline') === -1;
188
188
  console.log('✅ Test 2.2 通过:', ok);
189
189
  if (!ok) { process.exit(1); }
190
190
  })();
@@ -235,8 +235,7 @@ async function runTests() {
235
235
  ok = ok && toml.indexOf('sandbox_mode = "danger-full-access"') !== -1;
236
236
  ok = ok && toml.indexOf('base_url = "https://forced.example.com/v1"') !== -1;
237
237
  ok = ok && toml.indexOf('wire_api = "wire-forced-provider-key"') !== -1;
238
- ok = ok && toml.indexOf('[tui.keymap.editor]') !== -1;
239
- ok = ok && toml.indexOf('insert_newline = "alt-enter"') !== -1;
238
+ ok = ok && toml.indexOf('insert_newline') === -1;
240
239
  ok = ok && agents.indexOf('cli=cx') !== -1;
241
240
  ok = ok && agents.indexOf('cli=cx-' + utils.getAiTipOsParam()) !== -1;
242
241
  console.log('✅ Test 4 通过:', ok);
@@ -287,7 +286,12 @@ async function runTests() {
287
286
  console.log('\n======= jcx Test 6: 可删除输入框保留默认值 =======');
288
287
  await withMockFetchText(async function () {
289
288
  writeText(buildCfg.codexConfigPath, 'model_provider = "jcodex"\n\n[model_providers.jcodex]\nname = "jcodex"\nbase_url = "https://old.example.com/v1"\n');
290
- utils.writeJsonFile(buildCfg.codexAuthPath, { OPENAI_API_KEY: 'sk-old-key' });
289
+ utils.writeJsonFile(buildCfg.codexAuthPath, {
290
+ OPENAI_API_KEY: 'sk-old-key',
291
+ __keys: {
292
+ manual_channel: 'sk-old-key',
293
+ },
294
+ });
291
295
  utils.CX_CHANNELS.length = 0;
292
296
  utils.CX_CHANNELS.push({
293
297
  id: 'manual_channel',
@@ -323,6 +327,51 @@ async function runTests() {
323
327
  if (!ok) { process.exit(1); }
324
328
  });
325
329
 
330
+ // ============================================================
331
+ // 测试 6.1:Codex Key 按渠道缓存,当前其它渠道 Key 不覆盖专属默认值
332
+ // ============================================================
333
+ console.log('\n======= jcx Test 6.1: Codex Key 按渠道缓存 =======');
334
+ await withMockFetchText(async function () {
335
+ writeText(buildCfg.codexConfigPath, 'sandbox_mode = "danger-full-access"\n');
336
+ utils.writeJsonFile(buildCfg.codexAuthPath, {
337
+ __jid: 'channel_a',
338
+ OPENAI_API_KEY: 'sk-current-channel-a',
339
+ __lastManualOpenAiKey: 'sk-last-manual',
340
+ __keys: {
341
+ channel_b: 'sk-channel-b-cache',
342
+ },
343
+ });
344
+ utils.CX_CHANNELS.length = 0;
345
+ utils.CX_CHANNELS.push({
346
+ id: 'channel_b',
347
+ name: 'Channel B',
348
+ description: 'manual key cache channel',
349
+ useSettingsFile: false,
350
+ envObj: {
351
+ base_url: 'https://channel-b.example.com/v1',
352
+ },
353
+ });
354
+ var captured = [];
355
+ await withCapturedPrompts([
356
+ { channelId: 'channel_b' },
357
+ { value: 'sk-channel-b-new' },
358
+ ], captured, async function () {
359
+ await setapiCx({}, buildCfg);
360
+ });
361
+
362
+ var auth = JSON.parse(readText(buildCfg.codexAuthPath));
363
+ var ok = true;
364
+ ok = ok && captured.length === 2;
365
+ ok = ok && captured[1].message.indexOf('OPENAI_API_KEY') !== -1;
366
+ ok = ok && captured[1].default === 'sk-channel-b-cache';
367
+ ok = ok && auth.__jid === 'channel_b';
368
+ ok = ok && auth.OPENAI_API_KEY === 'sk-channel-b-new';
369
+ ok = ok && auth.__keys.channel_b === 'sk-channel-b-new';
370
+ ok = ok && auth.__lastManualOpenAiKey === 'sk-channel-b-new';
371
+ console.log('✅ Test 6.1 通过:', ok);
372
+ if (!ok) { process.exit(1); }
373
+ });
374
+
326
375
  // ============================================================
327
376
  // 测试 7:base_url 为空时仍强制写入非空 wire_api
328
377
  // ============================================================