base_parts_ai 1.0.60 → 1.0.61

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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/lib/ucx.js +98 -19
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -38,7 +38,7 @@ ucx
38
38
  jgrok
39
39
  ```
40
40
 
41
- `ucx` 会扫描用户主目录下一层的 `~/.codex` 与 `~/.codex_*`。账号名由 `config.toml` 顶层 `model_provider`、`auth.json` 中非空 `OPENAI_API_KEY` 的脱敏短名、JWT email 组合成 `配置名_Key_Email`,缺段去掉;不会自动创建空的 `~/.codex`。选中当前 `~/.codex` 会进入二级菜单,只在该目录内用 `juser_*` 切换 `config.toml`、`auth.json`、`.env`、`models.json`,不改其它 `~/.codex_*` 账号目录。二级选中子账户后(含选中当前无需切换)会进入下一级选择:还原最后一次会话并进入 codex(读 `~/.codex/session_index.jsonl` 取最后一次会话 id 后执行 `codex fork <会话id> --yolo`)、直接进入 codex(`codex --yolo`)、退出 ucx;启动 codex 时会等待其执行完成后一起退出并透传其退出码,`~/.codex/.env` 存在时会注入子进程环境。
41
+ `ucx` 会扫描用户主目录下一层的 `~/.codex` 与 `~/.codex_*`。账号名由 `config.toml` 顶层 `model_provider`、`auth.json` 中非空 `OPENAI_API_KEY` 的脱敏短名、JWT email 组合成 `配置名_Key_Email`,缺段去掉;不会自动创建空的 `~/.codex`。选中当前 `~/.codex` 会进入二级菜单,只在该目录内用 `juser_*` 切换 `config.toml`、`auth.json`、`.env`、`models.json`,不改其它 `~/.codex_*` 账号目录。二级选中子账户后(含选中当前无需切换)会进入下一级选择:选择历史会话并进入 codex(读 `~/.codex/session_index.jsonl` 倒序去重列出最近 8 条会话,id 在前、名字在后,最新在上并默认选中,选择后再执行 `codex fork <会话id> --yolo`)、直接进入 codex(`codex --yolo`)、退出 ucx;启动 codex 时会等待其执行完成后一起退出并透传其退出码,`~/.codex/.env` 存在时会注入子进程环境。
42
42
 
43
43
  `jgrok` 若 `~/.grok/.env` 不存在则写入代理示例并打印绝对路径,请编辑后再运行。已存在则把该文件注入环境变量:本机没有 `grok` 时通过腾讯云 npm 源安装 `@xai-official/grok`(安装不注入环境变量),然后启动 `grok`,用户参数原样透传。
44
44
 
package/lib/ucx.js CHANGED
@@ -27,7 +27,7 @@ var REMOVE_CURRENT_SUB = '__remove_current_sub__';
27
27
  /** 二级菜单:返回一级账号列表 */
28
28
  var BACK_TO_PARENT = '__back_to_parent__';
29
29
 
30
- /** 三级菜单:还原最后一次会话并进入 codex */
30
+ /** 三级菜单:选择历史会话并进入 codex */
31
31
  var POST_SWITCH_RESUME = '__post_switch_resume__';
32
32
 
33
33
  /** 三级菜单:直接进入 codex */
@@ -36,6 +36,12 @@ var POST_SWITCH_DIRECT = '__post_switch_direct__';
36
36
  /** 三级菜单:退出 ucx,不执行任何命令 */
37
37
  var POST_SWITCH_EXIT = '__post_switch_exit__';
38
38
 
39
+ /** 会话列表:返回三级菜单,不执行 fork */
40
+ var POST_SWITCH_RESUME_BACK = '__post_switch_resume_back__';
41
+
42
+ /** 还原会话时最多列出的最近有效会话数 */
43
+ var RESUME_SESSION_LIMIT = 8;
44
+
39
45
  /** 子账户目录前缀,只出现在当前 ~/.codex 内 */
40
46
  var SUB_DIR_PREFIX = 'juser_';
41
47
 
@@ -479,19 +485,23 @@ function codexCommandExists(execImpl) {
479
485
  }
480
486
 
481
487
  /**
482
- * 读取 ~/.codex/session_index.jsonl 的最后一次会话记录:倒序跳过坏行,取第一条含非空 id 的记录
488
+ * 读取 ~/.codex/session_index.jsonl 最近若干条有效会话:倒序跳过坏行,按 id 去重
483
489
  * @param {string} homeDir 用户主目录
484
- * @returns {{ id: string, threadName: string }|null} 无有效记录时返回 null
490
+ * @param {number} [limit] 最多返回条数,默认 RESUME_SESSION_LIMIT
491
+ * @returns {Array<{ id: string, threadName: string }>} 新到旧;无有效记录时为空数组
485
492
  */
486
- function readLastSessionRecord(homeDir) {
493
+ function readRecentSessionRecords(homeDir, limit) {
494
+ var max = typeof limit === 'number' && limit > 0 ? limit : RESUME_SESSION_LIMIT;
487
495
  var indexPath = path.join(homeDir, '.codex', 'session_index.jsonl');
488
496
  var content = '';
489
497
  try {
490
498
  content = fs.readFileSync(indexPath, 'utf8');
491
499
  } catch (e) {
492
- return null;
500
+ return [];
493
501
  }
494
502
  var lines = content.split(/\r?\n/);
503
+ var seen = {};
504
+ var records = [];
495
505
  for (var i = lines.length - 1; i >= 0; i -= 1) {
496
506
  var line = lines[i].trim();
497
507
  if (!line) {
@@ -500,16 +510,48 @@ function readLastSessionRecord(homeDir) {
500
510
  try {
501
511
  var record = JSON.parse(line);
502
512
  if (record && typeof record.id === 'string' && record.id.trim()) {
503
- return {
504
- id: record.id.trim(),
513
+ var id = record.id.trim();
514
+ if (seen[id]) {
515
+ continue;
516
+ }
517
+ seen[id] = true;
518
+ records.push({
519
+ id: id,
505
520
  threadName: typeof record.thread_name === 'string' ? record.thread_name : '',
506
- };
521
+ });
522
+ if (records.length >= max) {
523
+ break;
524
+ }
507
525
  }
508
526
  } catch (e) {
509
527
  // 半行写入等坏行跳过,继续向前找
510
528
  }
511
529
  }
512
- return null;
530
+ return records;
531
+ }
532
+
533
+ /**
534
+ * 读取 ~/.codex/session_index.jsonl 的最后一次会话记录
535
+ * @param {string} homeDir 用户主目录
536
+ * @returns {{ id: string, threadName: string }|null} 无有效记录时返回 null
537
+ */
538
+ function readLastSessionRecord(homeDir) {
539
+ var records = readRecentSessionRecords(homeDir, 1);
540
+ return records.length ? records[0] : null;
541
+ }
542
+
543
+ /**
544
+ * 会话选项展示名:有 thread_name 时为「id 名字」,否则只用 id
545
+ * @param {{ id: string, threadName: string }} session 会话记录
546
+ * @returns {string}
547
+ */
548
+ function formatSessionChoiceName(session) {
549
+ var name = session && typeof session.threadName === 'string' ? session.threadName.trim() : '';
550
+ var id = session && session.id ? session.id : '';
551
+ if (name) {
552
+ return id + ' ' + name;
553
+ }
554
+ return id;
513
555
  }
514
556
 
515
557
  /**
@@ -1025,17 +1067,30 @@ async function removeCurrentSubAccount(codexDir, currentRecord, options) {
1025
1067
  }
1026
1068
 
1027
1069
  /**
1028
- * 三级菜单选项:还原最后一次会话 / 直接进入 / 退出,固定顺序
1070
+ * 三级菜单选项:选择历史会话 / 直接进入 / 退出,固定顺序
1029
1071
  * @returns {Array<object>}
1030
1072
  */
1031
1073
  function buildPostSwitchChoices() {
1032
1074
  return [
1033
- { name: '还原最后一次会话并进入 codex', value: POST_SWITCH_RESUME },
1075
+ { name: '选择历史会话并进入 codex', value: POST_SWITCH_RESUME },
1034
1076
  { name: '直接进入 codex', value: POST_SWITCH_DIRECT },
1035
1077
  { name: '退出 ucx', value: POST_SWITCH_EXIT },
1036
1078
  ];
1037
1079
  }
1038
1080
 
1081
+ /**
1082
+ * 还原会话列表:最近若干条会话 + 返回上级,固定返回项在末尾
1083
+ * @param {Array<{ id: string, threadName: string }>} sessions 新到旧的会话
1084
+ * @returns {Array<object>}
1085
+ */
1086
+ function buildResumeSessionChoices(sessions) {
1087
+ var choices = (sessions || []).map(function (session) {
1088
+ return { name: formatSessionChoiceName(session), value: session.id };
1089
+ });
1090
+ choices.push({ name: '*返回上级*', value: POST_SWITCH_RESUME_BACK });
1091
+ return choices;
1092
+ }
1093
+
1039
1094
  /**
1040
1095
  * 前台启动 codex 并等待其执行完成,返回其退出码;codex 缺失时提示并返回 null,spawn 失败向上抛错
1041
1096
  * 关键点:inquirer 菜单用过 stdin 后,父进程的 stdin 上可能仍挂着操作系统层面的读取,
@@ -1095,8 +1150,8 @@ async function launchCodex(args, homeDir, options) {
1095
1150
  }
1096
1151
 
1097
1152
  /**
1098
- * 子账号切换后的三级菜单:还原最后一次会话 / 直接进入 codex / 退出 ucx
1099
- * 还原时读 ~/.codex/session_index.jsonl 取最后一次会话 id 后执行 codex fork <id> --yolo
1153
+ * 子账号切换后的三级菜单:选择历史会话 / 直接进入 codex / 退出 ucx
1154
+ * 还原时读 ~/.codex/session_index.jsonl 倒序去重列出最近 8 条,最新在上并默认选中,选中后再 fork
1100
1155
  * @param {string} homeDir 用户主目录
1101
1156
  * @param {object} [options] 可选配置,spawnImpl/execImpl 用于单测注入
1102
1157
  * @returns {Promise<number|undefined>} codex 退出码;未启动 codex 时为 undefined
@@ -1118,15 +1173,34 @@ async function runPostSwitchMenu(homeDir, options) {
1118
1173
  return launchCodex(['--yolo'], homeDir, options);
1119
1174
  }
1120
1175
 
1121
- // 还原最后一次会话:无会话记录时提示并重新选择,用户可改选直接进入
1122
- var session = readLastSessionRecord(homeDir);
1123
- if (!session) {
1176
+ // 还原会话:无记录时提示并重选;有记录则列出最近 8 条供选择,默认最新
1177
+ var sessions = readRecentSessionRecords(homeDir, RESUME_SESSION_LIMIT);
1178
+ if (!sessions.length) {
1124
1179
  console.log('未在 ~/.codex/session_index.jsonl 中找到会话记录,无法还原。');
1125
1180
  continue;
1126
1181
  }
1127
- var display = session.threadName || session.id;
1128
- console.log('正在还原最后一次会话:' + display);
1129
- return launchCodex(['fork', session.id, '--yolo'], homeDir, options);
1182
+ var sessionAnswer = await inquirer.prompt([{
1183
+ type: 'list',
1184
+ name: 'sessionId',
1185
+ message: '请选择要还原的会话',
1186
+ choices: buildResumeSessionChoices(sessions),
1187
+ default: sessions[0].id,
1188
+ }]);
1189
+ if (sessionAnswer.sessionId === POST_SWITCH_RESUME_BACK) {
1190
+ continue;
1191
+ }
1192
+ var selectedSession = null;
1193
+ sessions.forEach(function (item) {
1194
+ if (item.id === sessionAnswer.sessionId) {
1195
+ selectedSession = item;
1196
+ }
1197
+ });
1198
+ if (!selectedSession) {
1199
+ continue;
1200
+ }
1201
+ var display = selectedSession.threadName || selectedSession.id;
1202
+ console.log('正在还原会话:' + display);
1203
+ return launchCodex(['fork', selectedSession.id, '--yolo'], homeDir, options);
1130
1204
  }
1131
1205
  }
1132
1206
 
@@ -1232,6 +1306,8 @@ runUcx._private = {
1232
1306
  POST_SWITCH_RESUME: POST_SWITCH_RESUME,
1233
1307
  POST_SWITCH_DIRECT: POST_SWITCH_DIRECT,
1234
1308
  POST_SWITCH_EXIT: POST_SWITCH_EXIT,
1309
+ POST_SWITCH_RESUME_BACK: POST_SWITCH_RESUME_BACK,
1310
+ RESUME_SESSION_LIMIT: RESUME_SESSION_LIMIT,
1235
1311
  SUB_DIR_PREFIX: SUB_DIR_PREFIX,
1236
1312
  SUB_FILE_NAMES: SUB_FILE_NAMES,
1237
1313
  CODEX_ENV_DEMO: CODEX_ENV_DEMO,
@@ -1271,7 +1347,10 @@ runUcx._private = {
1271
1347
  parseCodexEnvContent: parseCodexEnvContent,
1272
1348
  buildCodexChildEnv: buildCodexChildEnv,
1273
1349
  codexCommandExists: codexCommandExists,
1350
+ readRecentSessionRecords: readRecentSessionRecords,
1274
1351
  readLastSessionRecord: readLastSessionRecord,
1352
+ formatSessionChoiceName: formatSessionChoiceName,
1353
+ buildResumeSessionChoices: buildResumeSessionChoices,
1275
1354
  buildPostSwitchChoices: buildPostSwitchChoices,
1276
1355
  launchCodex: launchCodex,
1277
1356
  runPostSwitchMenu: runPostSwitchMenu,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "base_parts_ai",
3
- "version": "1.0.60",
3
+ "version": "1.0.61",
4
4
  "description": "jaskle base_parts_ai",
5
5
  "main": "./main.js",
6
6
  "registry": true,