base_parts_ai 1.0.59 → 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.
- package/README.md +1 -1
- package/bin/jgrok.js +1 -1
- package/lib/jgrok.js +40 -13
- package/lib/ucx.js +135 -31
- 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_*`
|
|
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/bin/jgrok.js
CHANGED
package/lib/jgrok.js
CHANGED
|
@@ -141,27 +141,54 @@ function buildChildEnv(content) {
|
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
/**
|
|
144
|
-
*
|
|
144
|
+
* 前台启动官方 grok 并等待其执行完成,返回其退出码;spawn 失败向上抛错
|
|
145
|
+
* 与 ucx 的 launchCodex 相同的健壮启动模式:异步 spawn + stdio:'inherit' 等待 close 事件。
|
|
146
|
+
* 当前 jgrok 没有 inquirer 交互不会抢输入,但启动前仍恢复 cooked 模式、真实启动后立即
|
|
147
|
+
* 销毁父进程 stdin,防止将来增加交互业务后出现“grok 界面起来但无法输入”的问题
|
|
148
|
+
* (inquirer 用过 stdin 后父进程挂起的读取会抢走子进程键盘输入)。
|
|
145
149
|
* @param {Array<string>} args 透传给 grok 的参数
|
|
146
150
|
* @param {object} childEnv 注入后的环境变量
|
|
147
|
-
* @param {object} [options] 可选配置,spawnImpl 用于注入
|
|
148
|
-
* @returns {number} grok 退出码
|
|
151
|
+
* @param {object} [options] 可选配置,spawnImpl 用于注入 spawn
|
|
152
|
+
* @returns {Promise<number>} grok 退出码
|
|
149
153
|
*/
|
|
150
|
-
function spawnGrok(args, childEnv, options) {
|
|
154
|
+
async function spawnGrok(args, childEnv, options) {
|
|
151
155
|
options = options || {};
|
|
152
|
-
|
|
153
|
-
|
|
156
|
+
// 恢复 cooked 模式,给子进程一个干净的终端基线(将来加入 inquirer 交互后尤为关键)
|
|
157
|
+
if (process.stdin && process.stdin.isTTY && typeof process.stdin.setRawMode === 'function') {
|
|
158
|
+
try {
|
|
159
|
+
process.stdin.setRawMode(false);
|
|
160
|
+
} catch (e) {
|
|
161
|
+
// 模式恢复失败不阻断启动
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
var spawn = options.spawnImpl || childProcess.spawn;
|
|
165
|
+
var child = spawn('grok', args || [], {
|
|
154
166
|
stdio: 'inherit',
|
|
155
167
|
shell: process.platform === 'win32',
|
|
156
168
|
env: childEnv,
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if (
|
|
162
|
-
|
|
169
|
+
});
|
|
170
|
+
// 真实启动时销毁父进程 stdin,取消可能挂起的读取,把键盘输入完全让给 grok;
|
|
171
|
+
// 句柄已在 spawn 时复制给子进程,销毁父进程 stdin 不影响 grok。
|
|
172
|
+
// 注入 spawnImpl 的单测不销毁,避免破坏测试进程自身的 stdin。
|
|
173
|
+
if (!options.spawnImpl && process.stdin && typeof process.stdin.destroy === 'function') {
|
|
174
|
+
if (typeof process.stdin.pause === 'function') {
|
|
175
|
+
process.stdin.pause();
|
|
176
|
+
}
|
|
177
|
+
process.stdin.destroy();
|
|
163
178
|
}
|
|
164
|
-
|
|
179
|
+
var exitCode = await new Promise(function (resolve, reject) {
|
|
180
|
+
child.on('error', function (err) {
|
|
181
|
+
reject(err);
|
|
182
|
+
});
|
|
183
|
+
child.on('close', function (code, signal) {
|
|
184
|
+
if (signal) {
|
|
185
|
+
resolve(1);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
resolve(typeof code === 'number' ? code : 0);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
return exitCode;
|
|
165
192
|
}
|
|
166
193
|
|
|
167
194
|
/**
|
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
|
-
/**
|
|
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
|
|
488
|
+
* 读取 ~/.codex/session_index.jsonl 最近若干条有效会话:倒序跳过坏行,按 id 去重
|
|
483
489
|
* @param {string} homeDir 用户主目录
|
|
484
|
-
* @
|
|
490
|
+
* @param {number} [limit] 最多返回条数,默认 RESUME_SESSION_LIMIT
|
|
491
|
+
* @returns {Array<{ id: string, threadName: string }>} 新到旧;无有效记录时为空数组
|
|
485
492
|
*/
|
|
486
|
-
function
|
|
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
|
|
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
|
-
|
|
504
|
-
|
|
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
|
|
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,25 +1067,41 @@ 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: '
|
|
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
|
|
|
1039
1081
|
/**
|
|
1040
|
-
*
|
|
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
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* 前台启动 codex 并等待其执行完成,返回其退出码;codex 缺失时提示并返回 null,spawn 失败向上抛错
|
|
1096
|
+
* 关键点:inquirer 菜单用过 stdin 后,父进程的 stdin 上可能仍挂着操作系统层面的读取,
|
|
1097
|
+
* 会抢走子进程的键盘输入(codex 界面起来但无法输入);jgrok 没这个问题是因为它从不碰 stdin。
|
|
1098
|
+
* 因此启动前恢复 cooked 模式,真实启动时 spawn 后立即销毁父进程 stdin,把输入完全让给 codex。
|
|
1041
1099
|
* @param {Array<string>} args 传给 codex 的参数
|
|
1042
1100
|
* @param {string} homeDir 用户主目录
|
|
1043
1101
|
* @param {object} [options] 可选配置,spawnImpl/execImpl 用于单测注入
|
|
1044
|
-
* @returns {number|null}
|
|
1102
|
+
* @returns {Promise<number|null>}
|
|
1045
1103
|
*/
|
|
1046
|
-
function launchCodex(args, homeDir, options) {
|
|
1104
|
+
async function launchCodex(args, homeDir, options) {
|
|
1047
1105
|
options = options || {};
|
|
1048
1106
|
// 测试环境且未注入实现时跳过,避免真实启动 codex
|
|
1049
1107
|
if (process.env.JCC_TEST_HOME && !options.spawnImpl) {
|
|
@@ -1054,24 +1112,46 @@ function launchCodex(args, homeDir, options) {
|
|
|
1054
1112
|
console.error('未检测到 codex 命令,请先安装 Codex CLI 后重试。');
|
|
1055
1113
|
return null;
|
|
1056
1114
|
}
|
|
1057
|
-
|
|
1058
|
-
|
|
1115
|
+
// 恢复 cooked 模式,给子进程一个与 jgrok 场景一致的干净终端基线
|
|
1116
|
+
if (process.stdin && process.stdin.isTTY && typeof process.stdin.setRawMode === 'function') {
|
|
1117
|
+
try {
|
|
1118
|
+
process.stdin.setRawMode(false);
|
|
1119
|
+
} catch (e) {
|
|
1120
|
+
// 模式恢复失败不阻断启动
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
var spawn = options.spawnImpl || childProcess.spawn;
|
|
1124
|
+
var child = spawn('codex', args || [], {
|
|
1059
1125
|
stdio: 'inherit',
|
|
1060
1126
|
shell: process.platform === 'win32',
|
|
1061
1127
|
env: buildCodexChildEnv(homeDir),
|
|
1062
|
-
})
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1128
|
+
});
|
|
1129
|
+
// 真实启动时销毁父进程 stdin,取消挂起读取;句柄已在 spawn 时复制给子进程,不受影响。
|
|
1130
|
+
// 注入 spawnImpl 的单测不销毁,避免破坏测试进程自身的 stdin。
|
|
1131
|
+
if (!options.spawnImpl && process.stdin && typeof process.stdin.destroy === 'function') {
|
|
1132
|
+
if (typeof process.stdin.pause === 'function') {
|
|
1133
|
+
process.stdin.pause();
|
|
1134
|
+
}
|
|
1135
|
+
process.stdin.destroy();
|
|
1068
1136
|
}
|
|
1069
|
-
|
|
1137
|
+
var exitCode = await new Promise(function (resolve, reject) {
|
|
1138
|
+
child.on('error', function (err) {
|
|
1139
|
+
reject(err);
|
|
1140
|
+
});
|
|
1141
|
+
child.on('close', function (code, signal) {
|
|
1142
|
+
if (signal) {
|
|
1143
|
+
resolve(1);
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
resolve(typeof code === 'number' ? code : 0);
|
|
1147
|
+
});
|
|
1148
|
+
});
|
|
1149
|
+
return exitCode;
|
|
1070
1150
|
}
|
|
1071
1151
|
|
|
1072
1152
|
/**
|
|
1073
|
-
*
|
|
1074
|
-
* 还原时读 ~/.codex/session_index.jsonl
|
|
1153
|
+
* 子账号切换后的三级菜单:选择历史会话 / 直接进入 codex / 退出 ucx
|
|
1154
|
+
* 还原时读 ~/.codex/session_index.jsonl 倒序去重列出最近 8 条,最新在上并默认选中,选中后再 fork
|
|
1075
1155
|
* @param {string} homeDir 用户主目录
|
|
1076
1156
|
* @param {object} [options] 可选配置,spawnImpl/execImpl 用于单测注入
|
|
1077
1157
|
* @returns {Promise<number|undefined>} codex 退出码;未启动 codex 时为 undefined
|
|
@@ -1093,15 +1173,34 @@ async function runPostSwitchMenu(homeDir, options) {
|
|
|
1093
1173
|
return launchCodex(['--yolo'], homeDir, options);
|
|
1094
1174
|
}
|
|
1095
1175
|
|
|
1096
|
-
//
|
|
1097
|
-
var
|
|
1098
|
-
if (!
|
|
1176
|
+
// 还原会话:无记录时提示并重选;有记录则列出最近 8 条供选择,默认最新
|
|
1177
|
+
var sessions = readRecentSessionRecords(homeDir, RESUME_SESSION_LIMIT);
|
|
1178
|
+
if (!sessions.length) {
|
|
1099
1179
|
console.log('未在 ~/.codex/session_index.jsonl 中找到会话记录,无法还原。');
|
|
1100
1180
|
continue;
|
|
1101
1181
|
}
|
|
1102
|
-
var
|
|
1103
|
-
|
|
1104
|
-
|
|
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);
|
|
1105
1204
|
}
|
|
1106
1205
|
}
|
|
1107
1206
|
|
|
@@ -1207,6 +1306,8 @@ runUcx._private = {
|
|
|
1207
1306
|
POST_SWITCH_RESUME: POST_SWITCH_RESUME,
|
|
1208
1307
|
POST_SWITCH_DIRECT: POST_SWITCH_DIRECT,
|
|
1209
1308
|
POST_SWITCH_EXIT: POST_SWITCH_EXIT,
|
|
1309
|
+
POST_SWITCH_RESUME_BACK: POST_SWITCH_RESUME_BACK,
|
|
1310
|
+
RESUME_SESSION_LIMIT: RESUME_SESSION_LIMIT,
|
|
1210
1311
|
SUB_DIR_PREFIX: SUB_DIR_PREFIX,
|
|
1211
1312
|
SUB_FILE_NAMES: SUB_FILE_NAMES,
|
|
1212
1313
|
CODEX_ENV_DEMO: CODEX_ENV_DEMO,
|
|
@@ -1246,7 +1347,10 @@ runUcx._private = {
|
|
|
1246
1347
|
parseCodexEnvContent: parseCodexEnvContent,
|
|
1247
1348
|
buildCodexChildEnv: buildCodexChildEnv,
|
|
1248
1349
|
codexCommandExists: codexCommandExists,
|
|
1350
|
+
readRecentSessionRecords: readRecentSessionRecords,
|
|
1249
1351
|
readLastSessionRecord: readLastSessionRecord,
|
|
1352
|
+
formatSessionChoiceName: formatSessionChoiceName,
|
|
1353
|
+
buildResumeSessionChoices: buildResumeSessionChoices,
|
|
1250
1354
|
buildPostSwitchChoices: buildPostSwitchChoices,
|
|
1251
1355
|
launchCodex: launchCodex,
|
|
1252
1356
|
runPostSwitchMenu: runPostSwitchMenu,
|