base_parts_ai 1.0.56 → 1.0.58
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 +8 -4
- package/bin/jgrok.js +47 -0
- package/bin/postinstall.js +1 -0
- package/bin/ucx.js +1 -1
- package/lib/ai_utils.js +2 -1
- package/lib/jgrok.js +202 -0
- package/lib/ucx.js +523 -30
- package/main.js +13 -11
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
npx base_parts_ai
|
|
8
8
|
```
|
|
9
9
|
|
|
10
|
-
无参数时会让用户选择 `jcc`、`jcx`、`weds` 或 `
|
|
10
|
+
无参数时会让用户选择 `jcc`、`jcx`、`weds`、`ucx` 或 `jgrok`,再继续执行对应流程。
|
|
11
11
|
|
|
12
12
|
也可以明确指定工具,适合脚本或需要带参数的场景:
|
|
13
13
|
|
|
@@ -16,6 +16,7 @@ npx base_parts_ai jcc
|
|
|
16
16
|
npx base_parts_ai jcx
|
|
17
17
|
npx base_parts_ai weds
|
|
18
18
|
npx base_parts_ai ucx
|
|
19
|
+
npx base_parts_ai jgrok
|
|
19
20
|
npx base_parts_ai jcc setapi
|
|
20
21
|
npx base_parts_ai jcx setapi
|
|
21
22
|
npx base_parts_ai weds setapi
|
|
@@ -28,18 +29,21 @@ npx base_parts_ai --help
|
|
|
28
29
|
npx base_parts_ai --version
|
|
29
30
|
```
|
|
30
31
|
|
|
31
|
-
通过 `base_parts_ai` 入口执行时,会屏蔽 `base_parts_ai` 自身自动升级;`jcc`、`jcx`、`weds`、`ucx` 单独运行行为保持不变。
|
|
32
|
+
通过 `base_parts_ai` 入口执行时,会屏蔽 `base_parts_ai` 自身自动升级;`jcc`、`jcx`、`weds`、`ucx`、`jgrok` 单独运行行为保持不变。
|
|
32
33
|
|
|
33
34
|
全局安装后也可以直接运行:
|
|
34
35
|
|
|
35
36
|
```powershell
|
|
36
37
|
ucx
|
|
38
|
+
jgrok
|
|
37
39
|
```
|
|
38
40
|
|
|
39
|
-
`ucx` 会扫描用户主目录下一层的 `~/.codex` 与 `~/.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_*` 账号目录。
|
|
42
|
+
|
|
43
|
+
`jgrok` 若 `~/.grok/.env` 不存在则写入代理示例并打印绝对路径,请编辑后再运行。已存在则把该文件注入环境变量:本机没有 `grok` 时通过腾讯云 npm 源安装 `@xai-official/grok`(安装不注入环境变量),然后启动 `grok`,用户参数原样透传。
|
|
40
44
|
|
|
41
45
|
## 下载发布的包
|
|
42
|
-
https://registry.npmjs.org/base_parts_ai/-/base_parts_ai-1.0.
|
|
46
|
+
https://registry.npmjs.org/base_parts_ai/-/base_parts_ai-1.0.57.tgz
|
|
43
47
|
|
|
44
48
|
## 然后放在jdwfiles目录下
|
|
45
49
|
https://jdwfiles.oss-cn-hangzhou.aliyuncs.com/npm_pkg/base_parts_ai-1.0.34.tgz
|
package/bin/jgrok.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
var os = require('os');
|
|
6
|
+
var path = require('path');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 构建 jgrok 运行配置,只解析主目录
|
|
10
|
+
* @returns {{ homeDir: string }}
|
|
11
|
+
*/
|
|
12
|
+
function getBuildCfg() {
|
|
13
|
+
var homeDir = process.env.JCC_TEST_HOME || os.homedir();
|
|
14
|
+
return { homeDir: homeDir };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 动态加载 lib 下的命令模块,便于本地开发时读取最新文件
|
|
19
|
+
* @param {string} libName lib 目录下的文件名(不含 .js)
|
|
20
|
+
* @returns {Function}
|
|
21
|
+
*/
|
|
22
|
+
function loadLib(libName) {
|
|
23
|
+
var libPath = path.resolve(__dirname, '../lib/' + libName + '.js');
|
|
24
|
+
delete require.cache[require.resolve(libPath)];
|
|
25
|
+
return require(libPath);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 运行 jgrok CLI,参数全部透传给 grok,可供命令行直接执行或被 base_parts_ai require 调用
|
|
30
|
+
* @param {Array<string>} args 用户参数,不包含 node 和脚本路径
|
|
31
|
+
* @returns {Promise<number>}
|
|
32
|
+
*/
|
|
33
|
+
async function runCli(args) {
|
|
34
|
+
var buildCfg = getBuildCfg();
|
|
35
|
+
return await loadLib('jgrok')(args || [], buildCfg);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (require.main === module) {
|
|
39
|
+
runCli(process.argv.slice(2)).then(function (code) {
|
|
40
|
+
process.exit(typeof code === 'number' ? code : 0);
|
|
41
|
+
}).catch(function (err) {
|
|
42
|
+
console.error('jgrok 执行失败:', err.message);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = runCli;
|
package/bin/postinstall.js
CHANGED
|
@@ -17,5 +17,6 @@ console.log(c.cyan + ' ║ ' + c.yellow + '安装完成!输入以下命令
|
|
|
17
17
|
console.log(c.cyan + ' ║ ' + c.green + 'jcc' + c.white + ' 配置或安装 ClaudeCode' + c.reset);
|
|
18
18
|
console.log(c.cyan + ' ║ ' + c.green + 'weds' + c.white + ' 配置或安装 weds ClaudeCode' + c.reset);
|
|
19
19
|
console.log(c.cyan + ' ║ ' + c.green + 'ucx' + c.white + ' 切换 Codex 账号' + c.reset);
|
|
20
|
+
console.log(c.cyan + ' ║ ' + c.green + 'jgrok' + c.white + ' 带代理环境启动 grok' + c.reset);
|
|
20
21
|
console.log(c.cyan + ' ╚══════════════════════════════════════════╝' + c.reset);
|
|
21
22
|
console.log('');
|
package/bin/ucx.js
CHANGED
package/lib/ai_utils.js
CHANGED
|
@@ -51,7 +51,7 @@ async function fetchRemoteText(url, timeoutMs) {
|
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
53
|
* 从当前入口文件名推断正在运行的 CLI 命令名
|
|
54
|
-
* @returns {string} jcc、jcx、weds、ucx 或空字符串
|
|
54
|
+
* @returns {string} jcc、jcx、weds、ucx、jgrok 或空字符串
|
|
55
55
|
*/
|
|
56
56
|
function getCurrentCliName() {
|
|
57
57
|
var entryName = path.basename(process.argv[1] || '').toLowerCase();
|
|
@@ -59,6 +59,7 @@ function getCurrentCliName() {
|
|
|
59
59
|
if (entryName === 'jcc' || entryName === 'jcc.js') return 'jcc';
|
|
60
60
|
if (entryName === 'weds' || entryName === 'weds.js') return 'weds';
|
|
61
61
|
if (entryName === 'ucx' || entryName === 'ucx.js') return 'ucx';
|
|
62
|
+
if (entryName === 'jgrok' || entryName === 'jgrok.js') return 'jgrok';
|
|
62
63
|
return '';
|
|
63
64
|
}
|
|
64
65
|
|
package/lib/jgrok.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* jgrok 命令:检测并安装 grok,注入 ~/.grok/.env 后启动官方 grok
|
|
5
|
+
* 首次创建 .env 只提示编辑,不启动 grok
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
var fs = require('fs');
|
|
9
|
+
var os = require('os');
|
|
10
|
+
var path = require('path');
|
|
11
|
+
var childProcess = require('child_process');
|
|
12
|
+
|
|
13
|
+
/** ~/.grok/.env 自定义代理示例内容 */
|
|
14
|
+
var GROK_ENV_DEMO = [
|
|
15
|
+
'HTTP_PROXY=http://127.0.0.1:11811',
|
|
16
|
+
'HTTPS_PROXY=http://127.0.0.1:11811',
|
|
17
|
+
'ALL_PROXY=http://127.0.0.1:11811',
|
|
18
|
+
'NO_PROXY=localhost,127.0.0.1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16',
|
|
19
|
+
'',
|
|
20
|
+
].join('\n');
|
|
21
|
+
|
|
22
|
+
/** 缺失 grok 时走腾讯云 npm 源安装,不加 --force,安装时不注入环境变量 */
|
|
23
|
+
var GROK_INSTALL_CMD = 'npm install -g @xai-official/grok --registry=https://mirrors.cloud.tencent.com/npm/';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 解析用户主目录:优先 buildCfg.homeDir,再 JCC_TEST_HOME,最后 os.homedir()
|
|
27
|
+
* @param {object} [buildCfg] 入口传入的配置
|
|
28
|
+
* @returns {string}
|
|
29
|
+
*/
|
|
30
|
+
function getHomeDir(buildCfg) {
|
|
31
|
+
if (buildCfg && buildCfg.homeDir) {
|
|
32
|
+
return buildCfg.homeDir;
|
|
33
|
+
}
|
|
34
|
+
return process.env.JCC_TEST_HOME || os.homedir();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 获取 ~/.grok 目录路径
|
|
39
|
+
* @param {string} homeDir 用户主目录
|
|
40
|
+
* @returns {string}
|
|
41
|
+
*/
|
|
42
|
+
function getGrokDir(homeDir) {
|
|
43
|
+
return path.join(homeDir, '.grok');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 获取 ~/.grok/.env 绝对路径
|
|
48
|
+
* @param {string} homeDir 用户主目录
|
|
49
|
+
* @returns {string}
|
|
50
|
+
*/
|
|
51
|
+
function getGrokEnvPath(homeDir) {
|
|
52
|
+
return path.join(getGrokDir(homeDir), '.env');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 解析 KEY=VALUE 文本,跳过空行和 # 注释,去掉值两端成对引号
|
|
57
|
+
* @param {string} content .env 文件内容
|
|
58
|
+
* @returns {object}
|
|
59
|
+
*/
|
|
60
|
+
function parseEnvFile(content) {
|
|
61
|
+
var env = {};
|
|
62
|
+
String(content || '').replace(/^\uFEFF/, '').split(/\r?\n/).forEach(function (line) {
|
|
63
|
+
var trimmed = line.trim();
|
|
64
|
+
if (!trimmed || trimmed.charAt(0) === '#') {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
var eq = trimmed.indexOf('=');
|
|
68
|
+
if (eq <= 0) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
var key = trimmed.slice(0, eq).trim();
|
|
72
|
+
var value = trimmed.slice(eq + 1).trim();
|
|
73
|
+
var quote = value.charAt(0);
|
|
74
|
+
if ((quote === '"' || quote === "'") && value.length >= 2 && value.charAt(value.length - 1) === quote) {
|
|
75
|
+
value = value.slice(1, -1);
|
|
76
|
+
}
|
|
77
|
+
env[key] = value;
|
|
78
|
+
});
|
|
79
|
+
return env;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 检测本机是否存在指定命令:Windows 用 where,其它用 which
|
|
84
|
+
* @param {string} name 命令名
|
|
85
|
+
* @param {object} [options] 可选配置,execImpl 用于注入 execSync
|
|
86
|
+
* @returns {boolean}
|
|
87
|
+
*/
|
|
88
|
+
function commandExists(name, options) {
|
|
89
|
+
options = options || {};
|
|
90
|
+
var exec = options.execImpl || childProcess.execSync;
|
|
91
|
+
var cmd = process.platform === 'win32' ? ('where ' + name) : ('which ' + name);
|
|
92
|
+
try {
|
|
93
|
+
exec(cmd, { stdio: 'ignore' });
|
|
94
|
+
return true;
|
|
95
|
+
} catch (e) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 本机没有 grok 时通过腾讯云 npm 源全局安装,安装失败向上抛错
|
|
102
|
+
* 安装不注入 ~/.grok/.env,直接走镜像源
|
|
103
|
+
* @param {object} [options] 可选配置,execImpl 用于注入 execSync
|
|
104
|
+
* @returns {void}
|
|
105
|
+
*/
|
|
106
|
+
function ensureGrokInstalled(options) {
|
|
107
|
+
if (commandExists('grok', options)) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
options = options || {};
|
|
111
|
+
var exec = options.execImpl || childProcess.execSync;
|
|
112
|
+
console.log('未检测到 grok,正在从腾讯云 npm 源安装 @xai-official/grok ...');
|
|
113
|
+
exec(GROK_INSTALL_CMD, { stdio: 'inherit' });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 确保 ~/.grok/.env 存在;不存在则写入代理示例,已存在则不覆盖
|
|
118
|
+
* @param {string} homeDir 用户主目录
|
|
119
|
+
* @returns {{ created: boolean, envPath: string }}
|
|
120
|
+
*/
|
|
121
|
+
function ensureGrokEnvFile(homeDir) {
|
|
122
|
+
var grokDir = getGrokDir(homeDir);
|
|
123
|
+
var envPath = getGrokEnvPath(homeDir);
|
|
124
|
+
if (!fs.existsSync(grokDir)) {
|
|
125
|
+
fs.mkdirSync(grokDir, { recursive: true });
|
|
126
|
+
}
|
|
127
|
+
if (fs.existsSync(envPath)) {
|
|
128
|
+
return { created: false, envPath: envPath };
|
|
129
|
+
}
|
|
130
|
+
fs.writeFileSync(envPath, GROK_ENV_DEMO, 'utf8');
|
|
131
|
+
return { created: true, envPath: envPath };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 将 .env 解析结果覆盖到当前进程环境副本上,供子进程使用
|
|
136
|
+
* @param {string} content .env 文件内容
|
|
137
|
+
* @returns {object}
|
|
138
|
+
*/
|
|
139
|
+
function buildChildEnv(content) {
|
|
140
|
+
return Object.assign({}, process.env, parseEnvFile(content));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 启动官方 grok,Windows 走 shell 以便找到 grok.cmd
|
|
145
|
+
* @param {Array<string>} args 透传给 grok 的参数
|
|
146
|
+
* @param {object} childEnv 注入后的环境变量
|
|
147
|
+
* @param {object} [options] 可选配置,spawnImpl 用于注入 spawnSync
|
|
148
|
+
* @returns {number} grok 退出码
|
|
149
|
+
*/
|
|
150
|
+
function spawnGrok(args, childEnv, options) {
|
|
151
|
+
options = options || {};
|
|
152
|
+
var spawn = options.spawnImpl || childProcess.spawnSync;
|
|
153
|
+
var result = spawn('grok', args || [], {
|
|
154
|
+
stdio: 'inherit',
|
|
155
|
+
shell: process.platform === 'win32',
|
|
156
|
+
env: childEnv,
|
|
157
|
+
}) || {};
|
|
158
|
+
if (result.error) {
|
|
159
|
+
throw result.error;
|
|
160
|
+
}
|
|
161
|
+
if (result.signal) {
|
|
162
|
+
return 1;
|
|
163
|
+
}
|
|
164
|
+
return typeof result.status === 'number' ? result.status : 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* 执行 jgrok:先处理 .env,首次创建则提示编辑;否则注入环境后安装并启动 grok
|
|
169
|
+
* @param {Array<string>} args 透传给 grok 的参数
|
|
170
|
+
* @param {object} buildCfg 含 homeDir 的配置
|
|
171
|
+
* @param {object} [options] 可选配置,execImpl/spawnImpl 用于单测注入
|
|
172
|
+
* @returns {Promise<number>} 退出码
|
|
173
|
+
*/
|
|
174
|
+
async function runJgrok(args, buildCfg, options) {
|
|
175
|
+
options = options || {};
|
|
176
|
+
var homeDir = getHomeDir(buildCfg);
|
|
177
|
+
var envResult = ensureGrokEnvFile(homeDir);
|
|
178
|
+
if (envResult.created) {
|
|
179
|
+
console.log('已首次创建 grok 环境文件,请编辑后再运行 jgrok:');
|
|
180
|
+
console.log(envResult.envPath);
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
183
|
+
var childEnv = buildChildEnv(fs.readFileSync(envResult.envPath, 'utf8'));
|
|
184
|
+
ensureGrokInstalled(options);
|
|
185
|
+
return spawnGrok(args || [], childEnv, options);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
runJgrok._private = {
|
|
189
|
+
GROK_ENV_DEMO: GROK_ENV_DEMO,
|
|
190
|
+
GROK_INSTALL_CMD: GROK_INSTALL_CMD,
|
|
191
|
+
getHomeDir: getHomeDir,
|
|
192
|
+
getGrokDir: getGrokDir,
|
|
193
|
+
getGrokEnvPath: getGrokEnvPath,
|
|
194
|
+
parseEnvFile: parseEnvFile,
|
|
195
|
+
commandExists: commandExists,
|
|
196
|
+
ensureGrokInstalled: ensureGrokInstalled,
|
|
197
|
+
ensureGrokEnvFile: ensureGrokEnvFile,
|
|
198
|
+
buildChildEnv: buildChildEnv,
|
|
199
|
+
spawnGrok: spawnGrok,
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
module.exports = runJgrok;
|
package/lib/ucx.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* ucx 命令:通过重命名 ~/.codex 与 ~/.codex_<账号名> 切换 Codex 账号
|
|
5
|
-
*
|
|
5
|
+
* 账号名由配置名、脱敏 Key、JWT email 组合,缺段去掉;配置名来自 config.toml 的 model_provider
|
|
6
6
|
* 不创建空的 ~/.codex,避免把空目录当成当前账号
|
|
7
|
+
* 选中当前 ~/.codex 进入二级菜单,只在该目录内用 juser_* 切换子账户,不碰 ~/.codex_*
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
var fs = require('fs');
|
|
@@ -11,6 +12,7 @@ var os = require('os');
|
|
|
11
12
|
var path = require('path');
|
|
12
13
|
var childProcess = require('child_process');
|
|
13
14
|
var inquirer = require('inquirer');
|
|
15
|
+
var TOML = require('@ltd/j-toml');
|
|
14
16
|
var utils = require('./ai_utils');
|
|
15
17
|
|
|
16
18
|
/** 无法解析账号名时的展示名 */
|
|
@@ -19,6 +21,18 @@ var UNKNOWN_EMAIL_LABEL = '未知邮箱';
|
|
|
19
21
|
/** 列表末尾固定项:移除当前账号,以便登录下一个账号 */
|
|
20
22
|
var REMOVE_CURRENT_ACCOUNT = '__remove_current_codex__';
|
|
21
23
|
|
|
24
|
+
/** 二级菜单:移除当前子账户(只停放 4 个配置文件) */
|
|
25
|
+
var REMOVE_CURRENT_SUB = '__remove_current_sub__';
|
|
26
|
+
|
|
27
|
+
/** 二级菜单:返回一级账号列表 */
|
|
28
|
+
var BACK_TO_PARENT = '__back_to_parent__';
|
|
29
|
+
|
|
30
|
+
/** 子账户目录前缀,只出现在当前 ~/.codex 内 */
|
|
31
|
+
var SUB_DIR_PREFIX = 'juser_';
|
|
32
|
+
|
|
33
|
+
/** 子账户只交换这 4 个文件,其它文件留在 ~/.codex */
|
|
34
|
+
var SUB_FILE_NAMES = ['config.toml', 'auth.json', '.env', 'models.json'];
|
|
35
|
+
|
|
22
36
|
/** ~/.codex/.env 自定义代理示例内容 */
|
|
23
37
|
var CODEX_ENV_DEMO = [
|
|
24
38
|
'HTTP_PROXY=http://127.0.0.1:11811',
|
|
@@ -51,7 +65,7 @@ function isCodexAccountDirName(name) {
|
|
|
51
65
|
|
|
52
66
|
/**
|
|
53
67
|
* 将账号名中 Windows 非法文件名字符替换为下划线,保留 @.+-
|
|
54
|
-
* @param {string} accountName
|
|
68
|
+
* @param {string} accountName 组合展示名(配置名_Key_Email)
|
|
55
69
|
* @returns {string}
|
|
56
70
|
*/
|
|
57
71
|
function sanitizeEmailForDir(accountName) {
|
|
@@ -123,20 +137,69 @@ function getOpenAiApiKey(auth) {
|
|
|
123
137
|
}
|
|
124
138
|
|
|
125
139
|
/**
|
|
126
|
-
*
|
|
127
|
-
*
|
|
140
|
+
* 读取账号目录 config.toml 顶层 model_provider 作为配置名
|
|
141
|
+
* 文件缺失、解析失败、字段为空或非字符串时返回空串,避免阻断账号列表
|
|
142
|
+
* @param {string} configTomlPath config.toml 路径
|
|
128
143
|
* @returns {string}
|
|
129
144
|
*/
|
|
130
|
-
function
|
|
131
|
-
var
|
|
132
|
-
|
|
145
|
+
function readModelProvider(configTomlPath) {
|
|
146
|
+
var text;
|
|
147
|
+
try {
|
|
148
|
+
text = fs.readFileSync(configTomlPath, 'utf8');
|
|
149
|
+
} catch (e) {
|
|
133
150
|
return '';
|
|
134
151
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
152
|
+
if (!text || !String(text).trim()) {
|
|
153
|
+
return '';
|
|
154
|
+
}
|
|
155
|
+
var config;
|
|
156
|
+
try {
|
|
157
|
+
config = TOML.parse(text);
|
|
158
|
+
} catch (e) {
|
|
159
|
+
return '';
|
|
160
|
+
}
|
|
161
|
+
if (!config || typeof config.model_provider !== 'string') {
|
|
162
|
+
return '';
|
|
163
|
+
}
|
|
164
|
+
return config.model_provider.trim();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* 按 配置名_Key_Email 顺序拼接展示名,空段去掉
|
|
169
|
+
* @param {string} configName config.toml 的 model_provider
|
|
170
|
+
* @param {string} maskedKey 脱敏后的 OPENAI_API_KEY
|
|
171
|
+
* @param {string} email JWT 中的邮箱
|
|
172
|
+
* @returns {string}
|
|
173
|
+
*/
|
|
174
|
+
function composeAccountName(configName, maskedKey, email) {
|
|
175
|
+
var parts = [];
|
|
176
|
+
[configName, maskedKey, email].forEach(function (part) {
|
|
177
|
+
if (part) {
|
|
178
|
+
parts.push(String(part));
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
return parts.join('_');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* 读取账号目录的展示/停放名:配置名、脱敏 Key、JWT email 全部读出后组合
|
|
186
|
+
* @param {string} authJsonPath auth.json 路径,同级目录读取 config.toml
|
|
187
|
+
* @returns {string}
|
|
188
|
+
*/
|
|
189
|
+
function readAccountName(authJsonPath) {
|
|
190
|
+
var dirPath = path.dirname(authJsonPath);
|
|
191
|
+
var configName = readModelProvider(path.join(dirPath, 'config.toml'));
|
|
192
|
+
var auth = utils.readJsonFile(authJsonPath, null);
|
|
193
|
+
var maskedKey = '';
|
|
194
|
+
var email = '';
|
|
195
|
+
if (auth) {
|
|
196
|
+
var apiKey = getOpenAiApiKey(auth);
|
|
197
|
+
if (apiKey) {
|
|
198
|
+
maskedKey = utils.maskKey(apiKey);
|
|
199
|
+
}
|
|
200
|
+
email = extractEmailFromTokens(auth.tokens);
|
|
138
201
|
}
|
|
139
|
-
return
|
|
202
|
+
return composeAccountName(configName, maskedKey, email);
|
|
140
203
|
}
|
|
141
204
|
|
|
142
205
|
/**
|
|
@@ -225,7 +288,7 @@ function buildAccountChoices(records) {
|
|
|
225
288
|
var suffix = ' (~/' + record.dirName + ')';
|
|
226
289
|
var name = record.displayEmail + suffix;
|
|
227
290
|
if (record.isCurrent) {
|
|
228
|
-
name += ' 当前';
|
|
291
|
+
name += ' 当前(点击配置子账号)';
|
|
229
292
|
}
|
|
230
293
|
return { name: name, value: record.dirPath };
|
|
231
294
|
});
|
|
@@ -281,7 +344,7 @@ async function maybeCreateCodexEnv(homeDir) {
|
|
|
281
344
|
}
|
|
282
345
|
|
|
283
346
|
/**
|
|
284
|
-
*
|
|
347
|
+
* 移除当前账号:有登录信息则停放改名(不删除);三无目录与切换时相同,询问删除或手工命名
|
|
285
348
|
* @param {string} homeDir 用户主目录
|
|
286
349
|
* @param {object|null} currentRecord 当前 ~/.codex 记录
|
|
287
350
|
* @param {object} [options] 可选配置,execImpl 用于注入杀进程实现
|
|
@@ -292,7 +355,7 @@ async function removeCurrentAccount(homeDir, currentRecord, options) {
|
|
|
292
355
|
var confirmAnswer = await inquirer.prompt([{
|
|
293
356
|
type: 'confirm',
|
|
294
357
|
name: 'confirmed',
|
|
295
|
-
message: '
|
|
358
|
+
message: '确认把当前账号从 ~/.codex 停放出来,以便登录新账号吗?(不会删除,会改名为 ~/.codex_xxx)',
|
|
296
359
|
default: false,
|
|
297
360
|
}]);
|
|
298
361
|
if (!confirmAnswer.confirmed) {
|
|
@@ -347,7 +410,7 @@ function killCodexProcesses(execImpl) {
|
|
|
347
410
|
/**
|
|
348
411
|
* 为当前账号分配不冲突的停放目录名;同名已存在时追加 _2、_3,不覆盖
|
|
349
412
|
* @param {string} homeDir 用户主目录
|
|
350
|
-
* @param {string} accountName
|
|
413
|
+
* @param {string} accountName 组合展示名(配置名_Key_Email)
|
|
351
414
|
* @returns {string} 可用的目录名,例如 .codex_a 或 .codex_a_2
|
|
352
415
|
*/
|
|
353
416
|
function allocateParkedDirName(homeDir, accountName) {
|
|
@@ -436,7 +499,7 @@ function assertCanParkCurrent(homeDir, currentRecord) {
|
|
|
436
499
|
|
|
437
500
|
/**
|
|
438
501
|
* 执行账号目录切换:停放当前 .codex,再把选中目录改名为 .codex
|
|
439
|
-
*
|
|
502
|
+
* 当前为三无目录(无配置名、无 Key、无登录信息)时先询问删除或手工命名,名称不写回 auth.json
|
|
440
503
|
* @param {string} homeDir 用户主目录
|
|
441
504
|
* @param {object} selected 选中的账号记录
|
|
442
505
|
* @param {Array<object>} records 全部账号记录
|
|
@@ -492,6 +555,406 @@ async function switchCodexAccount(homeDir, selected, records, options) {
|
|
|
492
555
|
return { skipped: false, email: selected.displayEmail };
|
|
493
556
|
}
|
|
494
557
|
|
|
558
|
+
/**
|
|
559
|
+
* 判断目录名是否为当前空间内的子账户目录(仅 juser_*)
|
|
560
|
+
* @param {string} name 目录名
|
|
561
|
+
* @returns {boolean}
|
|
562
|
+
*/
|
|
563
|
+
function isSubAccountDirName(name) {
|
|
564
|
+
return String(name || '').indexOf(SUB_DIR_PREFIX) === 0;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* 解析后的路径必须落在当前 ~/.codex 内,防止子账户操作逃到一级账号目录
|
|
569
|
+
* @param {string} codexDir 当前 ~/.codex
|
|
570
|
+
* @param {string} targetPath 待检查路径
|
|
571
|
+
* @returns {string} 解析后的绝对路径
|
|
572
|
+
*/
|
|
573
|
+
function resolvePathInsideCodexDir(codexDir, targetPath) {
|
|
574
|
+
var root = path.resolve(codexDir);
|
|
575
|
+
var resolved = path.resolve(targetPath);
|
|
576
|
+
var prefix = root.endsWith(path.sep) ? root : root + path.sep;
|
|
577
|
+
if (resolved !== root && resolved.indexOf(prefix) !== 0) {
|
|
578
|
+
throw new Error('子账户路径必须位于当前 ~/.codex 内');
|
|
579
|
+
}
|
|
580
|
+
return resolved;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* 扫描当前 ~/.codex 下一层 juser_* 目录,不递归、不看 ~/.codex_*
|
|
585
|
+
* @param {string} codexDir 当前 ~/.codex
|
|
586
|
+
* @returns {Array<string>} 目录名列表
|
|
587
|
+
*/
|
|
588
|
+
function listSubAccountDirs(codexDir) {
|
|
589
|
+
var entries;
|
|
590
|
+
try {
|
|
591
|
+
entries = fs.readdirSync(codexDir, { withFileTypes: true });
|
|
592
|
+
} catch (e) {
|
|
593
|
+
return [];
|
|
594
|
+
}
|
|
595
|
+
var names = [];
|
|
596
|
+
entries.forEach(function (ent) {
|
|
597
|
+
if (!ent.isDirectory()) {
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
if (isSubAccountDirName(ent.name)) {
|
|
601
|
+
names.push(ent.name);
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
return names;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* 列出目录中实际存在的子账户配置文件
|
|
609
|
+
* @param {string} dirPath 目录路径
|
|
610
|
+
* @returns {Array<string>}
|
|
611
|
+
*/
|
|
612
|
+
function listPresentSubFiles(dirPath) {
|
|
613
|
+
var present = [];
|
|
614
|
+
SUB_FILE_NAMES.forEach(function (fileName) {
|
|
615
|
+
var filePath = path.join(dirPath, fileName);
|
|
616
|
+
try {
|
|
617
|
+
if (fs.statSync(filePath).isFile()) {
|
|
618
|
+
present.push(fileName);
|
|
619
|
+
}
|
|
620
|
+
} catch (e) {
|
|
621
|
+
// 文件不存在则跳过,保证缺文件时不会把旧文件留下来
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
return present;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* 把源目录中存在的 4 个配置文件移动到目标目录;目标已有同名文件则先删
|
|
629
|
+
* 源和目标都必须在同一个 ~/.codex 内
|
|
630
|
+
* @param {string} codexDir 当前 ~/.codex
|
|
631
|
+
* @param {string} srcDir 源目录
|
|
632
|
+
* @param {string} destDir 目标目录
|
|
633
|
+
* @returns {void}
|
|
634
|
+
*/
|
|
635
|
+
function moveSubFiles(codexDir, srcDir, destDir) {
|
|
636
|
+
srcDir = resolvePathInsideCodexDir(codexDir, srcDir);
|
|
637
|
+
destDir = resolvePathInsideCodexDir(codexDir, destDir);
|
|
638
|
+
if (!fs.existsSync(destDir)) {
|
|
639
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
640
|
+
}
|
|
641
|
+
listPresentSubFiles(srcDir).forEach(function (fileName) {
|
|
642
|
+
var destPath = path.join(destDir, fileName);
|
|
643
|
+
resolvePathInsideCodexDir(codexDir, destPath);
|
|
644
|
+
if (fs.existsSync(destPath)) {
|
|
645
|
+
fs.rmSync(destPath, { force: true });
|
|
646
|
+
}
|
|
647
|
+
fs.renameSync(path.join(srcDir, fileName), destPath);
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* 删除目录中存在的 4 个配置文件,不删目录本身
|
|
653
|
+
* @param {string} dirPath 目录路径
|
|
654
|
+
* @returns {void}
|
|
655
|
+
*/
|
|
656
|
+
function removePresentSubFiles(dirPath) {
|
|
657
|
+
listPresentSubFiles(dirPath).forEach(function (fileName) {
|
|
658
|
+
fs.rmSync(path.join(dirPath, fileName), { force: true });
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* 子账户目录搬空后删除;还有其它文件则保留,避免把无关内容拷到 ~/.codex 根
|
|
664
|
+
* @param {string} dirPath 目录路径
|
|
665
|
+
* @returns {void}
|
|
666
|
+
*/
|
|
667
|
+
function removeDirIfEmpty(dirPath) {
|
|
668
|
+
var entries;
|
|
669
|
+
try {
|
|
670
|
+
entries = fs.readdirSync(dirPath);
|
|
671
|
+
} catch (e) {
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
if (entries.length === 0) {
|
|
675
|
+
fs.rmdirSync(dirPath);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* 为当前子账户分配不冲突的 juser_* 目录名,只在 ~/.codex 内查找
|
|
681
|
+
* @param {string} codexDir 当前 ~/.codex
|
|
682
|
+
* @param {string} accountName 组合展示名
|
|
683
|
+
* @returns {string} 目录名,例如 juser_a 或 juser_a_2
|
|
684
|
+
*/
|
|
685
|
+
function allocateParkedSubDirName(codexDir, accountName) {
|
|
686
|
+
var base = SUB_DIR_PREFIX + sanitizeEmailForDir(accountName);
|
|
687
|
+
var name = base;
|
|
688
|
+
var index = 2;
|
|
689
|
+
while (fs.existsSync(path.join(codexDir, name))) {
|
|
690
|
+
name = base + '_' + index;
|
|
691
|
+
index += 1;
|
|
692
|
+
}
|
|
693
|
+
return name;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* 把当前 ~/.codex 或某个 juser_* 转成子账户记录
|
|
698
|
+
* @param {string} codexDir 当前 ~/.codex
|
|
699
|
+
* @param {string} dirName 当前为 .codex,停放为 juser_*
|
|
700
|
+
* @param {boolean} isCurrent 是否为根上正在生效的 4 个文件
|
|
701
|
+
* @returns {object}
|
|
702
|
+
*/
|
|
703
|
+
function buildSubAccountRecord(codexDir, dirName, isCurrent) {
|
|
704
|
+
var dirPath = isCurrent ? codexDir : path.join(codexDir, dirName);
|
|
705
|
+
var accountName = readAccountName(path.join(dirPath, 'auth.json'));
|
|
706
|
+
var parkedName = accountName ? (SUB_DIR_PREFIX + sanitizeEmailForDir(accountName)) : '';
|
|
707
|
+
return {
|
|
708
|
+
dirName: dirName,
|
|
709
|
+
dirPath: dirPath,
|
|
710
|
+
accountName: accountName,
|
|
711
|
+
displayEmail: accountName || UNKNOWN_EMAIL_LABEL,
|
|
712
|
+
parkedName: parkedName,
|
|
713
|
+
isCurrent: !!isCurrent,
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* 读取当前空间子账户记录:根上 4 个文件为当前,juser_* 为停放
|
|
719
|
+
* @param {string} codexDir 当前 ~/.codex
|
|
720
|
+
* @returns {Array<object>}
|
|
721
|
+
*/
|
|
722
|
+
function buildSubAccountRecords(codexDir) {
|
|
723
|
+
var records = [buildSubAccountRecord(codexDir, '.codex', true)];
|
|
724
|
+
listSubAccountDirs(codexDir).forEach(function (dirName) {
|
|
725
|
+
records.push(buildSubAccountRecord(codexDir, dirName, false));
|
|
726
|
+
});
|
|
727
|
+
records.sort(function (a, b) {
|
|
728
|
+
if (a.isCurrent !== b.isCurrent) {
|
|
729
|
+
return a.isCurrent ? -1 : 1;
|
|
730
|
+
}
|
|
731
|
+
var emailCmp = String(a.displayEmail).localeCompare(String(b.displayEmail));
|
|
732
|
+
if (emailCmp !== 0) {
|
|
733
|
+
return emailCmp;
|
|
734
|
+
}
|
|
735
|
+
return String(a.dirName).localeCompare(String(b.dirName));
|
|
736
|
+
});
|
|
737
|
+
return records;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* 生成二级菜单子账户列表项
|
|
742
|
+
* @param {Array<object>} records 子账户记录
|
|
743
|
+
* @returns {Array<{name: string, value: string}>}
|
|
744
|
+
*/
|
|
745
|
+
function buildSubAccountChoices(records) {
|
|
746
|
+
return records.map(function (record) {
|
|
747
|
+
var suffix = record.isCurrent
|
|
748
|
+
? ' (~/.codex)'
|
|
749
|
+
: ' (~/.codex/' + record.dirName + ')';
|
|
750
|
+
var name = record.displayEmail + suffix;
|
|
751
|
+
if (record.isCurrent) {
|
|
752
|
+
name += ' 当前';
|
|
753
|
+
}
|
|
754
|
+
return { name: name, value: record.dirPath };
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* 二级完整选项:子账户列表 + 移除当前子账户 + 返回上级
|
|
760
|
+
* @param {Array<object>} records 子账户记录
|
|
761
|
+
* @returns {Array<{name: string, value: string}>}
|
|
762
|
+
*/
|
|
763
|
+
function buildUcxSubChoices(records) {
|
|
764
|
+
var choices = buildSubAccountChoices(records);
|
|
765
|
+
choices.push({
|
|
766
|
+
name: '*移除当前子账户*',
|
|
767
|
+
value: REMOVE_CURRENT_SUB,
|
|
768
|
+
});
|
|
769
|
+
choices.push({
|
|
770
|
+
name: '*返回上级*',
|
|
771
|
+
value: BACK_TO_PARENT,
|
|
772
|
+
});
|
|
773
|
+
return choices;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* 三无当前子账户:询问是否删除这 4 个配置文件,否则手工命名停放到 juser_*
|
|
778
|
+
* @returns {Promise<{ deleteCurrent: boolean, parkName?: string }>}
|
|
779
|
+
*/
|
|
780
|
+
async function promptUnnamedSubAction() {
|
|
781
|
+
var deleteAnswer = await inquirer.prompt([{
|
|
782
|
+
type: 'confirm',
|
|
783
|
+
name: 'confirmed',
|
|
784
|
+
message: '当前没有可识别的子账户配置,是否删除这 4 个配置文件?',
|
|
785
|
+
default: false,
|
|
786
|
+
}]);
|
|
787
|
+
if (deleteAnswer.confirmed) {
|
|
788
|
+
return { deleteCurrent: true };
|
|
789
|
+
}
|
|
790
|
+
var nameAnswer = await inquirer.prompt([{
|
|
791
|
+
type: 'input',
|
|
792
|
+
name: 'accountName',
|
|
793
|
+
message: '请输入用于停放当前子账户的名称(不会写回 auth.json)',
|
|
794
|
+
validate: validateParkName,
|
|
795
|
+
}]);
|
|
796
|
+
return { deleteCurrent: false, parkName: String(nameAnswer.accountName || '').trim() };
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* 按三无子账户的选择删除 4 个文件或停放到 ~/.codex/juser_*,不删 ~/.codex
|
|
801
|
+
* @param {string} codexDir 当前 ~/.codex
|
|
802
|
+
* @param {{ deleteCurrent: boolean, parkName?: string }} action 用户选择
|
|
803
|
+
* @returns {string|null} 停放后的路径,删除时为 null
|
|
804
|
+
*/
|
|
805
|
+
function applyUnnamedSubAction(codexDir, action) {
|
|
806
|
+
if (action.deleteCurrent) {
|
|
807
|
+
removePresentSubFiles(codexDir);
|
|
808
|
+
console.log('已删除当前子账户的 4 个配置文件。');
|
|
809
|
+
return null;
|
|
810
|
+
}
|
|
811
|
+
var parkedName = allocateParkedSubDirName(codexDir, action.parkName);
|
|
812
|
+
var parkedPath = path.join(codexDir, parkedName);
|
|
813
|
+
moveSubFiles(codexDir, codexDir, parkedPath);
|
|
814
|
+
console.log('已将当前子账户停放到 ' + parkedPath);
|
|
815
|
+
return parkedPath;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* 在当前 ~/.codex 内切换子账户:只 move 4 个文件,不碰 ~/.codex_*
|
|
820
|
+
* @param {string} codexDir 当前 ~/.codex
|
|
821
|
+
* @param {object} selected 选中的子账户记录
|
|
822
|
+
* @param {Array<object>} records 全部子账户记录
|
|
823
|
+
* @param {object} [options] 可选配置,execImpl 用于注入杀进程实现
|
|
824
|
+
* @returns {Promise<{ skipped: boolean, email?: string }>}
|
|
825
|
+
*/
|
|
826
|
+
async function switchSubAccount(codexDir, selected, records, options) {
|
|
827
|
+
options = options || {};
|
|
828
|
+
if (!selected) {
|
|
829
|
+
throw new Error('未找到要切换的子账户');
|
|
830
|
+
}
|
|
831
|
+
if (selected.isCurrent) {
|
|
832
|
+
console.log('当前已是 ' + selected.displayEmail + ',无需切换');
|
|
833
|
+
return { skipped: true };
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
var currentRecord = null;
|
|
837
|
+
records.forEach(function (record) {
|
|
838
|
+
if (record.isCurrent) {
|
|
839
|
+
currentRecord = record;
|
|
840
|
+
}
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
var presentFiles = listPresentSubFiles(codexDir);
|
|
844
|
+
var unnamedAction = null;
|
|
845
|
+
if (presentFiles.length > 0 && currentRecord && !currentRecord.accountName) {
|
|
846
|
+
unnamedAction = await promptUnnamedSubAction();
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
killCodexProcesses(options.execImpl);
|
|
850
|
+
|
|
851
|
+
var parkedPath = null;
|
|
852
|
+
if (presentFiles.length > 0) {
|
|
853
|
+
if (unnamedAction) {
|
|
854
|
+
parkedPath = applyUnnamedSubAction(codexDir, unnamedAction);
|
|
855
|
+
} else {
|
|
856
|
+
var parkedName = allocateParkedSubDirName(codexDir, currentRecord.accountName);
|
|
857
|
+
parkedPath = path.join(codexDir, parkedName);
|
|
858
|
+
moveSubFiles(codexDir, codexDir, parkedPath);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
try {
|
|
863
|
+
moveSubFiles(codexDir, selected.dirPath, codexDir);
|
|
864
|
+
removeDirIfEmpty(selected.dirPath);
|
|
865
|
+
} catch (e) {
|
|
866
|
+
if (parkedPath) {
|
|
867
|
+
console.error('已将原子账户停放到 ' + parkedPath + ',但启用新子账户失败,请手动恢复');
|
|
868
|
+
}
|
|
869
|
+
throw e;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
console.log('已切换到 ' + selected.displayEmail + ',请重新启动 Codex');
|
|
873
|
+
return { skipped: false, email: selected.displayEmail };
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* 移除当前子账户:只把 4 个文件停放到 ~/.codex/juser_*,不删 ~/.codex,不改其它账号目录
|
|
878
|
+
* @param {string} codexDir 当前 ~/.codex
|
|
879
|
+
* @param {object|null} currentRecord 当前子账户记录
|
|
880
|
+
* @param {object} [options] 可选配置
|
|
881
|
+
* @returns {Promise<{ cancelled: boolean }>}
|
|
882
|
+
*/
|
|
883
|
+
async function removeCurrentSubAccount(codexDir, currentRecord, options) {
|
|
884
|
+
options = options || {};
|
|
885
|
+
var confirmAnswer = await inquirer.prompt([{
|
|
886
|
+
type: 'confirm',
|
|
887
|
+
name: 'confirmed',
|
|
888
|
+
message: '确认把当前子账户停放到 ~/.codex/juser_* 吗?',
|
|
889
|
+
default: false,
|
|
890
|
+
}]);
|
|
891
|
+
if (!confirmAnswer.confirmed) {
|
|
892
|
+
console.log('已取消移除当前子账户。');
|
|
893
|
+
return { cancelled: true };
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
var presentFiles = listPresentSubFiles(codexDir);
|
|
897
|
+
if (presentFiles.length <= 0) {
|
|
898
|
+
console.log('当前没有可移除的子账户配置。');
|
|
899
|
+
await maybeCreateCodexEnv(path.dirname(codexDir));
|
|
900
|
+
return { cancelled: false };
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
var unnamedAction = null;
|
|
904
|
+
if (!(currentRecord && currentRecord.accountName)) {
|
|
905
|
+
unnamedAction = await promptUnnamedSubAction();
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
killCodexProcesses(options.execImpl);
|
|
909
|
+
if (unnamedAction) {
|
|
910
|
+
applyUnnamedSubAction(codexDir, unnamedAction);
|
|
911
|
+
} else {
|
|
912
|
+
var parkedName = allocateParkedSubDirName(codexDir, currentRecord.accountName);
|
|
913
|
+
var parkedPath = path.join(codexDir, parkedName);
|
|
914
|
+
moveSubFiles(codexDir, codexDir, parkedPath);
|
|
915
|
+
console.log('已移除当前子账户,已停放到 ' + parkedPath);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
await maybeCreateCodexEnv(path.dirname(codexDir));
|
|
919
|
+
return { cancelled: false };
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/**
|
|
923
|
+
* 当前空间的子账户二级菜单;所有改动限制在 codexDir 内
|
|
924
|
+
* @param {string} codexDir 当前 ~/.codex
|
|
925
|
+
* @param {object} [options] 可选配置
|
|
926
|
+
* @returns {Promise<{ back: boolean }>}
|
|
927
|
+
*/
|
|
928
|
+
async function runSubAccountMenu(codexDir, options) {
|
|
929
|
+
options = options || {};
|
|
930
|
+
var records = buildSubAccountRecords(codexDir);
|
|
931
|
+
var current = null;
|
|
932
|
+
records.forEach(function (record) {
|
|
933
|
+
if (record.isCurrent) {
|
|
934
|
+
current = record;
|
|
935
|
+
}
|
|
936
|
+
});
|
|
937
|
+
var answer = await inquirer.prompt([{
|
|
938
|
+
type: 'list',
|
|
939
|
+
name: 'dirPath',
|
|
940
|
+
message: '请选择要切换的子账户',
|
|
941
|
+
choices: buildUcxSubChoices(records),
|
|
942
|
+
default: current ? current.dirPath : undefined,
|
|
943
|
+
}]);
|
|
944
|
+
|
|
945
|
+
if (answer.dirPath === BACK_TO_PARENT) {
|
|
946
|
+
return { back: true };
|
|
947
|
+
}
|
|
948
|
+
if (answer.dirPath === REMOVE_CURRENT_SUB) {
|
|
949
|
+
await removeCurrentSubAccount(codexDir, current || null, options);
|
|
950
|
+
return { back: false };
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
var selected = findRecordByDirPath(records, answer.dirPath);
|
|
954
|
+
await switchSubAccount(codexDir, selected, records, options);
|
|
955
|
+
return { back: false };
|
|
956
|
+
}
|
|
957
|
+
|
|
495
958
|
/**
|
|
496
959
|
* 按路径从记录中查找账号
|
|
497
960
|
* @param {Array<object>} records 账号记录
|
|
@@ -510,40 +973,57 @@ function findRecordByDirPath(records, dirPath) {
|
|
|
510
973
|
|
|
511
974
|
/**
|
|
512
975
|
* 执行 ucx 交互切换
|
|
976
|
+
* 选中当前 ~/.codex 进入子账户二级菜单;返回上级时重新弹出一级列表
|
|
513
977
|
* @param {object} cmd commander 选项(当前无业务字段)
|
|
514
978
|
* @param {object} buildCfg 含 homeDir 的配置
|
|
515
979
|
* @returns {Promise<void>}
|
|
516
980
|
*/
|
|
517
981
|
async function runUcx(cmd, buildCfg) {
|
|
518
982
|
var homeDir = getHomeDir(buildCfg);
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
983
|
+
while (true) {
|
|
984
|
+
var records = buildAccountRecords(homeDir);
|
|
985
|
+
var current = findRecordByDirPath(records, path.join(homeDir, '.codex'));
|
|
986
|
+
var answer = await inquirer.prompt([{
|
|
987
|
+
type: 'list',
|
|
988
|
+
name: 'dirPath',
|
|
989
|
+
message: '请选择要切换的 Codex 账号',
|
|
990
|
+
choices: buildUcxChoices(records),
|
|
991
|
+
default: current ? current.dirPath : undefined,
|
|
992
|
+
}]);
|
|
993
|
+
|
|
994
|
+
if (answer.dirPath === REMOVE_CURRENT_ACCOUNT) {
|
|
995
|
+
await removeCurrentAccount(homeDir, current || null, {});
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
528
998
|
|
|
529
|
-
|
|
530
|
-
|
|
999
|
+
var selected = findRecordByDirPath(records, answer.dirPath);
|
|
1000
|
+
if (selected && selected.isCurrent) {
|
|
1001
|
+
var subResult = await runSubAccountMenu(selected.dirPath, {});
|
|
1002
|
+
if (subResult && subResult.back) {
|
|
1003
|
+
continue;
|
|
1004
|
+
}
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
await switchCodexAccount(homeDir, selected, records, {});
|
|
531
1008
|
return;
|
|
532
1009
|
}
|
|
533
|
-
|
|
534
|
-
var selected = findRecordByDirPath(records, answer.dirPath);
|
|
535
|
-
await switchCodexAccount(homeDir, selected, records, {});
|
|
536
1010
|
}
|
|
537
1011
|
|
|
538
1012
|
runUcx._private = {
|
|
539
1013
|
UNKNOWN_EMAIL_LABEL: UNKNOWN_EMAIL_LABEL,
|
|
540
1014
|
REMOVE_CURRENT_ACCOUNT: REMOVE_CURRENT_ACCOUNT,
|
|
1015
|
+
REMOVE_CURRENT_SUB: REMOVE_CURRENT_SUB,
|
|
1016
|
+
BACK_TO_PARENT: BACK_TO_PARENT,
|
|
1017
|
+
SUB_DIR_PREFIX: SUB_DIR_PREFIX,
|
|
1018
|
+
SUB_FILE_NAMES: SUB_FILE_NAMES,
|
|
541
1019
|
CODEX_ENV_DEMO: CODEX_ENV_DEMO,
|
|
542
1020
|
getHomeDir: getHomeDir,
|
|
543
1021
|
sanitizeEmailForDir: sanitizeEmailForDir,
|
|
544
1022
|
decodeJwtPayload: decodeJwtPayload,
|
|
545
1023
|
extractEmailFromTokens: extractEmailFromTokens,
|
|
546
1024
|
getOpenAiApiKey: getOpenAiApiKey,
|
|
1025
|
+
readModelProvider: readModelProvider,
|
|
1026
|
+
composeAccountName: composeAccountName,
|
|
547
1027
|
readAccountName: readAccountName,
|
|
548
1028
|
readAccountEmail: readAccountEmail,
|
|
549
1029
|
listCodexAccountDirs: listCodexAccountDirs,
|
|
@@ -557,6 +1037,19 @@ runUcx._private = {
|
|
|
557
1037
|
assertCanParkCurrent: assertCanParkCurrent,
|
|
558
1038
|
switchCodexAccount: switchCodexAccount,
|
|
559
1039
|
removeCurrentAccount: removeCurrentAccount,
|
|
1040
|
+
isSubAccountDirName: isSubAccountDirName,
|
|
1041
|
+
resolvePathInsideCodexDir: resolvePathInsideCodexDir,
|
|
1042
|
+
listSubAccountDirs: listSubAccountDirs,
|
|
1043
|
+
listPresentSubFiles: listPresentSubFiles,
|
|
1044
|
+
moveSubFiles: moveSubFiles,
|
|
1045
|
+
removePresentSubFiles: removePresentSubFiles,
|
|
1046
|
+
allocateParkedSubDirName: allocateParkedSubDirName,
|
|
1047
|
+
buildSubAccountRecords: buildSubAccountRecords,
|
|
1048
|
+
buildSubAccountChoices: buildSubAccountChoices,
|
|
1049
|
+
buildUcxSubChoices: buildUcxSubChoices,
|
|
1050
|
+
switchSubAccount: switchSubAccount,
|
|
1051
|
+
removeCurrentSubAccount: removeCurrentSubAccount,
|
|
1052
|
+
runSubAccountMenu: runSubAccountMenu,
|
|
560
1053
|
};
|
|
561
1054
|
|
|
562
1055
|
module.exports = runUcx;
|
package/main.js
CHANGED
|
@@ -6,7 +6,7 @@ var path = require('path');
|
|
|
6
6
|
var inquirer = require('inquirer');
|
|
7
7
|
var packageJson = require('./package.json');
|
|
8
8
|
|
|
9
|
-
var VALID_TARGETS = ['jcc', 'jcx', 'weds', 'ucx'];
|
|
9
|
+
var VALID_TARGETS = ['jcc', 'jcx', 'weds', 'ucx', 'jgrok'];
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* 打印 base_parts_ai 入口命令的中文帮助信息
|
|
@@ -17,11 +17,12 @@ function printUsage() {
|
|
|
17
17
|
'base_parts_ai 入口工具',
|
|
18
18
|
'',
|
|
19
19
|
'用法:',
|
|
20
|
-
' base_parts_ai 选择 jcc、jcx、weds 或
|
|
20
|
+
' base_parts_ai 选择 jcc、jcx、weds、ucx 或 jgrok 后继续执行',
|
|
21
21
|
' base_parts_ai jcc [参数...] 执行 Claude Code 配置工具',
|
|
22
22
|
' base_parts_ai jcx [参数...] 执行 Codex 配置工具',
|
|
23
23
|
' base_parts_ai weds [参数...] 执行 weds Claude Code 配置工具',
|
|
24
24
|
' base_parts_ai ucx [参数...] 通过目录改名切换 Codex 账号',
|
|
25
|
+
' base_parts_ai jgrok [参数...] 注入 ~/.grok/.env 后启动 grok',
|
|
25
26
|
' base_parts_ai --help 显示入口帮助',
|
|
26
27
|
' base_parts_ai --version 显示当前版本',
|
|
27
28
|
'',
|
|
@@ -57,13 +58,13 @@ function resolveTarget(args) {
|
|
|
57
58
|
|
|
58
59
|
return {
|
|
59
60
|
type: 'error',
|
|
60
|
-
message: '参数无效:有参数时必须先明确指定 jcc、jcx、weds 或
|
|
61
|
+
message: '参数无效:有参数时必须先明确指定 jcc、jcx、weds、ucx 或 jgrok。',
|
|
61
62
|
};
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
/**
|
|
65
66
|
* 让用户选择要继续执行的配置工具
|
|
66
|
-
* @returns {Promise<string>} jcc、jcx、weds 或
|
|
67
|
+
* @returns {Promise<string>} jcc、jcx、weds、ucx 或 jgrok
|
|
67
68
|
*/
|
|
68
69
|
async function selectTarget() {
|
|
69
70
|
var answer = await inquirer.prompt([{
|
|
@@ -75,6 +76,7 @@ async function selectTarget() {
|
|
|
75
76
|
{ name: 'jcx - Codex 配置工具', value: 'jcx' },
|
|
76
77
|
{ name: 'weds - weds Claude Code 配置工具', value: 'weds' },
|
|
77
78
|
{ name: 'ucx - 切换 Codex 账号', value: 'ucx' },
|
|
79
|
+
{ name: 'jgrok - 带代理环境启动 grok', value: 'jgrok' },
|
|
78
80
|
],
|
|
79
81
|
}]);
|
|
80
82
|
return answer.target;
|
|
@@ -82,7 +84,7 @@ async function selectTarget() {
|
|
|
82
84
|
|
|
83
85
|
/**
|
|
84
86
|
* 获取当前包内目标 CLI 脚本路径
|
|
85
|
-
* @param {string} target jcc、jcx、weds 或
|
|
87
|
+
* @param {string} target jcc、jcx、weds、ucx 或 jgrok
|
|
86
88
|
* @returns {string}
|
|
87
89
|
*/
|
|
88
90
|
function getTargetScriptPath(target) {
|
|
@@ -90,8 +92,8 @@ function getTargetScriptPath(target) {
|
|
|
90
92
|
}
|
|
91
93
|
|
|
92
94
|
/**
|
|
93
|
-
* 加载当前包内的 jcc/jcx/weds/ucx 入口函数
|
|
94
|
-
* @param {string} target jcc、jcx、weds 或
|
|
95
|
+
* 加载当前包内的 jcc/jcx/weds/ucx/jgrok 入口函数
|
|
96
|
+
* @param {string} target jcc、jcx、weds、ucx 或 jgrok
|
|
95
97
|
* @returns {Function}
|
|
96
98
|
*/
|
|
97
99
|
function loadTargetCli(target) {
|
|
@@ -99,8 +101,8 @@ function loadTargetCli(target) {
|
|
|
99
101
|
}
|
|
100
102
|
|
|
101
103
|
/**
|
|
102
|
-
* 同进程执行当前包内的 jcc/jcx/weds/ucx 入口,并临时注入跳过自身升级的环境变量
|
|
103
|
-
* @param {string} target jcc、jcx、weds 或
|
|
104
|
+
* 同进程执行当前包内的 jcc/jcx/weds/ucx/jgrok 入口,并临时注入跳过自身升级的环境变量
|
|
105
|
+
* @param {string} target jcc、jcx、weds、ucx 或 jgrok
|
|
104
106
|
* @param {Array<string>} args 透传给目标入口的参数
|
|
105
107
|
* @param {Function} loader 测试用入口加载函数,默认 loadTargetCli
|
|
106
108
|
* @returns {Promise<number>} 退出码
|
|
@@ -113,8 +115,8 @@ async function runTarget(target, args, loader) {
|
|
|
113
115
|
if (typeof runCli !== 'function') {
|
|
114
116
|
throw new Error(target + ' 入口没有导出可调用函数');
|
|
115
117
|
}
|
|
116
|
-
await runCli(args || []);
|
|
117
|
-
return 0;
|
|
118
|
+
var result = await runCli(args || []);
|
|
119
|
+
return typeof result === 'number' ? result : 0;
|
|
118
120
|
} finally {
|
|
119
121
|
if (typeof oldSkipSelfUpdate === 'undefined') {
|
|
120
122
|
delete process.env.BASE_PARTS_AI_SKIP_SELF_UPDATE;
|
package/package.json
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "base_parts_ai",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.58",
|
|
4
4
|
"description": "jaskle base_parts_ai",
|
|
5
5
|
"main": "./main.js",
|
|
6
6
|
"registry": true,
|
|
7
7
|
"directories": {},
|
|
8
8
|
"scripts": {
|
|
9
|
-
"test": "node ./test_project_md_rename.js && node ./test_main.js && node ./test_jcc.js && node ./test_jcx.js && node ./test_ucx.js",
|
|
9
|
+
"test": "node ./test_project_md_rename.js && node ./test_main.js && node ./test_jcc.js && node ./test_jcx.js && node ./test_ucx.js && node ./test_jgrok.js",
|
|
10
10
|
"jcc": "node ./bin/jcc.js",
|
|
11
11
|
"jcx": "node ./bin/jcx.js",
|
|
12
12
|
"weds": "node ./bin/weds.js",
|
|
13
13
|
"ucx": "node ./bin/ucx.js",
|
|
14
|
+
"jgrok": "node ./bin/jgrok.js",
|
|
14
15
|
"base_parts_ai": "node ./main.js",
|
|
15
16
|
"__postinstall": "node ./bin/postinstall.js"
|
|
16
17
|
},
|
|
@@ -18,8 +19,8 @@
|
|
|
18
19
|
"base_parts_ai": "./main.js",
|
|
19
20
|
"jcc": "./bin/jcc.js",
|
|
20
21
|
"jcx": "./bin/jcx.js",
|
|
21
|
-
"
|
|
22
|
-
"
|
|
22
|
+
"ucx": "./bin/ucx.js",
|
|
23
|
+
"jgrok": "./bin/jgrok.js"
|
|
23
24
|
},
|
|
24
25
|
"files": [
|
|
25
26
|
"main.js",
|