base_parts_ai 1.0.58 → 1.0.59
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/ucx.js +13 -7
- package/lib/ucx.js +209 -9
- 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` 取最后一次会话 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/ucx.js
CHANGED
|
@@ -29,45 +29,51 @@ function loadLib(libName) {
|
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* 拉取远程配置(含自动升级)后执行账号切换
|
|
32
|
-
* @returns {Promise<
|
|
32
|
+
* @returns {Promise<number|undefined>} 子账户三级菜单启动过 codex 时返回其退出码
|
|
33
33
|
*/
|
|
34
34
|
async function runSwitch() {
|
|
35
35
|
var buildCfg = getBuildCfg();
|
|
36
36
|
var utils = loadLib('ai_utils');
|
|
37
37
|
await utils.fetchConfig(buildCfg);
|
|
38
38
|
await utils.showNotice();
|
|
39
|
-
|
|
39
|
+
return loadLib('ucx')({}, buildCfg);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
43
|
* 运行 ucx CLI,可供命令行直接执行或被 base_parts_ai require 调用
|
|
44
44
|
* @param {Array<string>} args 用户参数,不包含 node 和脚本路径
|
|
45
|
-
* @returns {Promise<
|
|
45
|
+
* @returns {Promise<number|undefined>} 透传 codex 退出码
|
|
46
46
|
*/
|
|
47
47
|
async function runCli(args) {
|
|
48
48
|
var cliArgs = (args || []).slice();
|
|
49
49
|
var program = new Command();
|
|
50
|
+
var exitCode;
|
|
50
51
|
|
|
51
52
|
program
|
|
52
53
|
.name('ucx')
|
|
53
54
|
.description('通过目录改名切换 Codex 账号,选中当前目录可切换其子账户')
|
|
54
55
|
.version(packageJson.version)
|
|
55
56
|
.action(async function () {
|
|
56
|
-
await runSwitch();
|
|
57
|
+
exitCode = await runSwitch();
|
|
57
58
|
});
|
|
58
59
|
|
|
59
60
|
// 无参数时直接进入切换,避免 commander 空参数不触发根 action
|
|
60
61
|
if (cliArgs.length <= 0) {
|
|
61
|
-
|
|
62
|
-
return;
|
|
62
|
+
return runSwitch();
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
// 使用 from:user 避免读取或修改全局 process.argv,便于被主入口 require 调用
|
|
66
66
|
await program.parseAsync(cliArgs, { from: 'user' });
|
|
67
|
+
return exitCode;
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
if (require.main === module) {
|
|
70
|
-
runCli(process.argv.slice(2)).
|
|
71
|
+
runCli(process.argv.slice(2)).then(function (code) {
|
|
72
|
+
// 启动过 codex 时与其共用退出码,便于脚本感知 codex 结果
|
|
73
|
+
if (typeof code === 'number') {
|
|
74
|
+
process.exit(code);
|
|
75
|
+
}
|
|
76
|
+
}).catch(function (err) {
|
|
71
77
|
console.error('ucx 执行失败:', err.message);
|
|
72
78
|
process.exit(1);
|
|
73
79
|
});
|
package/lib/ucx.js
CHANGED
|
@@ -27,6 +27,15 @@ var REMOVE_CURRENT_SUB = '__remove_current_sub__';
|
|
|
27
27
|
/** 二级菜单:返回一级账号列表 */
|
|
28
28
|
var BACK_TO_PARENT = '__back_to_parent__';
|
|
29
29
|
|
|
30
|
+
/** 三级菜单:还原最后一次会话并进入 codex */
|
|
31
|
+
var POST_SWITCH_RESUME = '__post_switch_resume__';
|
|
32
|
+
|
|
33
|
+
/** 三级菜单:直接进入 codex */
|
|
34
|
+
var POST_SWITCH_DIRECT = '__post_switch_direct__';
|
|
35
|
+
|
|
36
|
+
/** 三级菜单:退出 ucx,不执行任何命令 */
|
|
37
|
+
var POST_SWITCH_EXIT = '__post_switch_exit__';
|
|
38
|
+
|
|
30
39
|
/** 子账户目录前缀,只出现在当前 ~/.codex 内 */
|
|
31
40
|
var SUB_DIR_PREFIX = 'juser_';
|
|
32
41
|
|
|
@@ -407,6 +416,102 @@ function killCodexProcesses(execImpl) {
|
|
|
407
416
|
});
|
|
408
417
|
}
|
|
409
418
|
|
|
419
|
+
/**
|
|
420
|
+
* 解析 .env 文本为键值对象:跳过注释与空行,去掉成对引号
|
|
421
|
+
* @param {string} content .env 文件内容
|
|
422
|
+
* @returns {object}
|
|
423
|
+
*/
|
|
424
|
+
function parseCodexEnvContent(content) {
|
|
425
|
+
var result = {};
|
|
426
|
+
String(content || '').split(/\r?\n/).forEach(function (line) {
|
|
427
|
+
var trimmed = line.trim();
|
|
428
|
+
if (!trimmed || trimmed.indexOf('#') === 0) {
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
var eqIndex = trimmed.indexOf('=');
|
|
432
|
+
if (eqIndex <= 0) {
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
var key = trimmed.slice(0, eqIndex).trim();
|
|
436
|
+
var value = trimmed.slice(eqIndex + 1).trim();
|
|
437
|
+
if (value.length >= 2 && ((value[0] === '"' && value[value.length - 1] === '"') || (value[0] === '\'' && value[value.length - 1] === '\''))) {
|
|
438
|
+
value = value.slice(1, -1);
|
|
439
|
+
}
|
|
440
|
+
result[key] = value;
|
|
441
|
+
});
|
|
442
|
+
return result;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* 构建 codex 子进程环境:在当前进程环境之上合并 ~/.codex/.env,确保子账户切换后代理与 Key 立即生效
|
|
447
|
+
* @param {string} homeDir 用户主目录
|
|
448
|
+
* @returns {object}
|
|
449
|
+
*/
|
|
450
|
+
function buildCodexChildEnv(homeDir) {
|
|
451
|
+
var envPath = path.join(homeDir, '.codex', '.env');
|
|
452
|
+
var childEnv = Object.assign({}, process.env);
|
|
453
|
+
if (!fs.existsSync(envPath)) {
|
|
454
|
+
return childEnv;
|
|
455
|
+
}
|
|
456
|
+
try {
|
|
457
|
+
var parsed = parseCodexEnvContent(fs.readFileSync(envPath, 'utf8'));
|
|
458
|
+
return Object.assign(childEnv, parsed);
|
|
459
|
+
} catch (e) {
|
|
460
|
+
// .env 读取失败时退回当前进程环境,不阻断启动
|
|
461
|
+
return childEnv;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* 检测本机是否存在 codex 命令;测试环境或注入 exec 时避免触碰宿主机
|
|
467
|
+
* @param {Function} [execImpl] 可注入的 execSync,便于单测
|
|
468
|
+
* @returns {boolean}
|
|
469
|
+
*/
|
|
470
|
+
function codexCommandExists(execImpl) {
|
|
471
|
+
var exec = execImpl || childProcess.execSync;
|
|
472
|
+
var cmd = process.platform === 'win32' ? 'where codex' : 'which codex';
|
|
473
|
+
try {
|
|
474
|
+
exec(cmd, { stdio: 'ignore' });
|
|
475
|
+
return true;
|
|
476
|
+
} catch (e) {
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* 读取 ~/.codex/session_index.jsonl 的最后一次会话记录:倒序跳过坏行,取第一条含非空 id 的记录
|
|
483
|
+
* @param {string} homeDir 用户主目录
|
|
484
|
+
* @returns {{ id: string, threadName: string }|null} 无有效记录时返回 null
|
|
485
|
+
*/
|
|
486
|
+
function readLastSessionRecord(homeDir) {
|
|
487
|
+
var indexPath = path.join(homeDir, '.codex', 'session_index.jsonl');
|
|
488
|
+
var content = '';
|
|
489
|
+
try {
|
|
490
|
+
content = fs.readFileSync(indexPath, 'utf8');
|
|
491
|
+
} catch (e) {
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
var lines = content.split(/\r?\n/);
|
|
495
|
+
for (var i = lines.length - 1; i >= 0; i -= 1) {
|
|
496
|
+
var line = lines[i].trim();
|
|
497
|
+
if (!line) {
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
try {
|
|
501
|
+
var record = JSON.parse(line);
|
|
502
|
+
if (record && typeof record.id === 'string' && record.id.trim()) {
|
|
503
|
+
return {
|
|
504
|
+
id: record.id.trim(),
|
|
505
|
+
threadName: typeof record.thread_name === 'string' ? record.thread_name : '',
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
} catch (e) {
|
|
509
|
+
// 半行写入等坏行跳过,继续向前找
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
|
|
410
515
|
/**
|
|
411
516
|
* 为当前账号分配不冲突的停放目录名;同名已存在时追加 _2、_3,不覆盖
|
|
412
517
|
* @param {string} homeDir 用户主目录
|
|
@@ -919,11 +1024,92 @@ async function removeCurrentSubAccount(codexDir, currentRecord, options) {
|
|
|
919
1024
|
return { cancelled: false };
|
|
920
1025
|
}
|
|
921
1026
|
|
|
1027
|
+
/**
|
|
1028
|
+
* 三级菜单选项:还原最后一次会话 / 直接进入 / 退出,固定顺序
|
|
1029
|
+
* @returns {Array<object>}
|
|
1030
|
+
*/
|
|
1031
|
+
function buildPostSwitchChoices() {
|
|
1032
|
+
return [
|
|
1033
|
+
{ name: '还原最后一次会话并进入 codex', value: POST_SWITCH_RESUME },
|
|
1034
|
+
{ name: '直接进入 codex', value: POST_SWITCH_DIRECT },
|
|
1035
|
+
{ name: '退出 ucx', value: POST_SWITCH_EXIT },
|
|
1036
|
+
];
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
/**
|
|
1040
|
+
* 启动 codex 并等待其执行完成,返回子进程退出码;codex 缺失时提示并返回 null
|
|
1041
|
+
* @param {Array<string>} args 传给 codex 的参数
|
|
1042
|
+
* @param {string} homeDir 用户主目录
|
|
1043
|
+
* @param {object} [options] 可选配置,spawnImpl/execImpl 用于单测注入
|
|
1044
|
+
* @returns {number|null}
|
|
1045
|
+
*/
|
|
1046
|
+
function launchCodex(args, homeDir, options) {
|
|
1047
|
+
options = options || {};
|
|
1048
|
+
// 测试环境且未注入实现时跳过,避免真实启动 codex
|
|
1049
|
+
if (process.env.JCC_TEST_HOME && !options.spawnImpl) {
|
|
1050
|
+
console.log('测试环境跳过启动 codex。');
|
|
1051
|
+
return null;
|
|
1052
|
+
}
|
|
1053
|
+
if (!codexCommandExists(options.execImpl)) {
|
|
1054
|
+
console.error('未检测到 codex 命令,请先安装 Codex CLI 后重试。');
|
|
1055
|
+
return null;
|
|
1056
|
+
}
|
|
1057
|
+
var spawn = options.spawnImpl || childProcess.spawnSync;
|
|
1058
|
+
var result = spawn('codex', args || [], {
|
|
1059
|
+
stdio: 'inherit',
|
|
1060
|
+
shell: process.platform === 'win32',
|
|
1061
|
+
env: buildCodexChildEnv(homeDir),
|
|
1062
|
+
}) || {};
|
|
1063
|
+
if (result.error) {
|
|
1064
|
+
throw result.error;
|
|
1065
|
+
}
|
|
1066
|
+
if (result.signal) {
|
|
1067
|
+
return 1;
|
|
1068
|
+
}
|
|
1069
|
+
return typeof result.status === 'number' ? result.status : 0;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
/**
|
|
1073
|
+
* 子账号切换后的三级菜单:还原最后一次会话 / 直接进入 codex / 退出 ucx
|
|
1074
|
+
* 还原时读 ~/.codex/session_index.jsonl 取最后一次会话 id 后执行 codex fork <id> --yolo
|
|
1075
|
+
* @param {string} homeDir 用户主目录
|
|
1076
|
+
* @param {object} [options] 可选配置,spawnImpl/execImpl 用于单测注入
|
|
1077
|
+
* @returns {Promise<number|undefined>} codex 退出码;未启动 codex 时为 undefined
|
|
1078
|
+
*/
|
|
1079
|
+
async function runPostSwitchMenu(homeDir, options) {
|
|
1080
|
+
options = options || {};
|
|
1081
|
+
for (;;) {
|
|
1082
|
+
var answer = await inquirer.prompt([{
|
|
1083
|
+
type: 'list',
|
|
1084
|
+
name: 'action',
|
|
1085
|
+
message: '请选择接下来的操作',
|
|
1086
|
+
choices: buildPostSwitchChoices(),
|
|
1087
|
+
}]);
|
|
1088
|
+
|
|
1089
|
+
if (answer.action === POST_SWITCH_EXIT) {
|
|
1090
|
+
return undefined;
|
|
1091
|
+
}
|
|
1092
|
+
if (answer.action === POST_SWITCH_DIRECT) {
|
|
1093
|
+
return launchCodex(['--yolo'], homeDir, options);
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// 还原最后一次会话:无会话记录时提示并重新选择,用户可改选直接进入
|
|
1097
|
+
var session = readLastSessionRecord(homeDir);
|
|
1098
|
+
if (!session) {
|
|
1099
|
+
console.log('未在 ~/.codex/session_index.jsonl 中找到会话记录,无法还原。');
|
|
1100
|
+
continue;
|
|
1101
|
+
}
|
|
1102
|
+
var display = session.threadName || session.id;
|
|
1103
|
+
console.log('正在还原最后一次会话:' + display);
|
|
1104
|
+
return launchCodex(['fork', session.id, '--yolo'], homeDir, options);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
|
|
922
1108
|
/**
|
|
923
1109
|
* 当前空间的子账户二级菜单;所有改动限制在 codexDir 内
|
|
924
1110
|
* @param {string} codexDir 当前 ~/.codex
|
|
925
1111
|
* @param {object} [options] 可选配置
|
|
926
|
-
* @returns {Promise<{ back: boolean }>}
|
|
1112
|
+
* @returns {Promise<{ back: boolean, exitCode?: number }>}
|
|
927
1113
|
*/
|
|
928
1114
|
async function runSubAccountMenu(codexDir, options) {
|
|
929
1115
|
options = options || {};
|
|
@@ -952,7 +1138,9 @@ async function runSubAccountMenu(codexDir, options) {
|
|
|
952
1138
|
|
|
953
1139
|
var selected = findRecordByDirPath(records, answer.dirPath);
|
|
954
1140
|
await switchSubAccount(codexDir, selected, records, options);
|
|
955
|
-
|
|
1141
|
+
// 切换完成(含“已是当前子账户”的跳过场景)后进入三级菜单,可选择还原会话/直接进入 codex/退出
|
|
1142
|
+
var exitCode = await runPostSwitchMenu(path.dirname(codexDir), options);
|
|
1143
|
+
return { back: false, exitCode: exitCode };
|
|
956
1144
|
}
|
|
957
1145
|
|
|
958
1146
|
/**
|
|
@@ -976,11 +1164,13 @@ function findRecordByDirPath(records, dirPath) {
|
|
|
976
1164
|
* 选中当前 ~/.codex 进入子账户二级菜单;返回上级时重新弹出一级列表
|
|
977
1165
|
* @param {object} cmd commander 选项(当前无业务字段)
|
|
978
1166
|
* @param {object} buildCfg 含 homeDir 的配置
|
|
979
|
-
* @
|
|
1167
|
+
* @param {object} [options] 可选配置,spawnImpl/execImpl 用于单测注入
|
|
1168
|
+
* @returns {Promise<number|undefined>} 启动过 codex 时返回其退出码
|
|
980
1169
|
*/
|
|
981
|
-
async function runUcx(cmd, buildCfg) {
|
|
1170
|
+
async function runUcx(cmd, buildCfg, options) {
|
|
1171
|
+
options = options || {};
|
|
982
1172
|
var homeDir = getHomeDir(buildCfg);
|
|
983
|
-
|
|
1173
|
+
for (;;) {
|
|
984
1174
|
var records = buildAccountRecords(homeDir);
|
|
985
1175
|
var current = findRecordByDirPath(records, path.join(homeDir, '.codex'));
|
|
986
1176
|
var answer = await inquirer.prompt([{
|
|
@@ -992,19 +1182,19 @@ async function runUcx(cmd, buildCfg) {
|
|
|
992
1182
|
}]);
|
|
993
1183
|
|
|
994
1184
|
if (answer.dirPath === REMOVE_CURRENT_ACCOUNT) {
|
|
995
|
-
await removeCurrentAccount(homeDir, current || null,
|
|
1185
|
+
await removeCurrentAccount(homeDir, current || null, options);
|
|
996
1186
|
return;
|
|
997
1187
|
}
|
|
998
1188
|
|
|
999
1189
|
var selected = findRecordByDirPath(records, answer.dirPath);
|
|
1000
1190
|
if (selected && selected.isCurrent) {
|
|
1001
|
-
var subResult = await runSubAccountMenu(selected.dirPath,
|
|
1191
|
+
var subResult = await runSubAccountMenu(selected.dirPath, options);
|
|
1002
1192
|
if (subResult && subResult.back) {
|
|
1003
1193
|
continue;
|
|
1004
1194
|
}
|
|
1005
|
-
return;
|
|
1195
|
+
return subResult ? subResult.exitCode : undefined;
|
|
1006
1196
|
}
|
|
1007
|
-
await switchCodexAccount(homeDir, selected, records,
|
|
1197
|
+
await switchCodexAccount(homeDir, selected, records, options);
|
|
1008
1198
|
return;
|
|
1009
1199
|
}
|
|
1010
1200
|
}
|
|
@@ -1014,6 +1204,9 @@ runUcx._private = {
|
|
|
1014
1204
|
REMOVE_CURRENT_ACCOUNT: REMOVE_CURRENT_ACCOUNT,
|
|
1015
1205
|
REMOVE_CURRENT_SUB: REMOVE_CURRENT_SUB,
|
|
1016
1206
|
BACK_TO_PARENT: BACK_TO_PARENT,
|
|
1207
|
+
POST_SWITCH_RESUME: POST_SWITCH_RESUME,
|
|
1208
|
+
POST_SWITCH_DIRECT: POST_SWITCH_DIRECT,
|
|
1209
|
+
POST_SWITCH_EXIT: POST_SWITCH_EXIT,
|
|
1017
1210
|
SUB_DIR_PREFIX: SUB_DIR_PREFIX,
|
|
1018
1211
|
SUB_FILE_NAMES: SUB_FILE_NAMES,
|
|
1019
1212
|
CODEX_ENV_DEMO: CODEX_ENV_DEMO,
|
|
@@ -1050,6 +1243,13 @@ runUcx._private = {
|
|
|
1050
1243
|
switchSubAccount: switchSubAccount,
|
|
1051
1244
|
removeCurrentSubAccount: removeCurrentSubAccount,
|
|
1052
1245
|
runSubAccountMenu: runSubAccountMenu,
|
|
1246
|
+
parseCodexEnvContent: parseCodexEnvContent,
|
|
1247
|
+
buildCodexChildEnv: buildCodexChildEnv,
|
|
1248
|
+
codexCommandExists: codexCommandExists,
|
|
1249
|
+
readLastSessionRecord: readLastSessionRecord,
|
|
1250
|
+
buildPostSwitchChoices: buildPostSwitchChoices,
|
|
1251
|
+
launchCodex: launchCodex,
|
|
1252
|
+
runPostSwitchMenu: runPostSwitchMenu,
|
|
1053
1253
|
};
|
|
1054
1254
|
|
|
1055
1255
|
module.exports = runUcx;
|