@openxiaobu/codexl 0.1.0
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/LICENSE +21 -0
- package/README.md +164 -0
- package/dist/account-store.js +131 -0
- package/dist/cli.js +304 -0
- package/dist/config.js +202 -0
- package/dist/login.js +37 -0
- package/dist/scheduler.js +62 -0
- package/dist/serve.js +24 -0
- package/dist/server.js +235 -0
- package/dist/state.js +121 -0
- package/dist/status.js +134 -0
- package/dist/types.js +2 -0
- package/dist/usage-sync.js +143 -0
- package/package.json +47 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 bk
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# codexl
|
|
2
|
+
|
|
3
|
+
Local multi-account / multi-workspace switcher for Codex.
|
|
4
|
+
|
|
5
|
+
`codexl` 是一个本地 `Codex` 多账号 / 多工作空间切换器。
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
English:
|
|
10
|
+
|
|
11
|
+
- Reuse the official `~/.codex` login state
|
|
12
|
+
- Manage multiple accounts or workspaces as separate slots
|
|
13
|
+
- Fetch the latest usage from the official usage endpoint
|
|
14
|
+
- Expose a local provider endpoint for Codex
|
|
15
|
+
- Apply local cooldown rules for temporary, 5-hour, and weekly limits
|
|
16
|
+
|
|
17
|
+
中文:
|
|
18
|
+
|
|
19
|
+
- 复用官方 `~/.codex` 登录态
|
|
20
|
+
- 将多个账号或工作空间作为独立槽位管理
|
|
21
|
+
- 直接调用官方 usage 接口获取最新额度
|
|
22
|
+
- 暴露本地 provider 给 `Codex` 使用
|
|
23
|
+
- 对临时限流、5 小时限制、周限制做本地熔断
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm i -g @openxiaobu/codexl
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Verify:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
codexl --help
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Quick Start
|
|
38
|
+
|
|
39
|
+
1. Import your current Codex login state
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
codexl import current ~
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
2. Check latest usage
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
codexl status
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
3. Start the local proxy
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
codexl start
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Custom port:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
codexl start --port 4399
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
4. Show current local endpoint and key
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
codexl get
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
5. Write provider config into `~/.codex/config.toml`
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
codexl config
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Commands
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
codexl add <name>
|
|
79
|
+
codexl del <name>
|
|
80
|
+
codexl import <name> [HOME]
|
|
81
|
+
codexl status
|
|
82
|
+
codexl start [--port <port>]
|
|
83
|
+
codexl stop
|
|
84
|
+
codexl get
|
|
85
|
+
codexl config [codexPath]
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
More details: [HELP.md](./HELP.md)
|
|
89
|
+
|
|
90
|
+
## How `status` Works
|
|
91
|
+
|
|
92
|
+
English:
|
|
93
|
+
|
|
94
|
+
1. Read `access_token` / `refresh_token` / `account_id` from the official Codex login state
|
|
95
|
+
2. Request `https://chatgpt.com/backend-api/wham/usage`
|
|
96
|
+
3. Store the latest result in `~/.codexl/state.json`
|
|
97
|
+
4. Render the latest local cache
|
|
98
|
+
|
|
99
|
+
中文:
|
|
100
|
+
|
|
101
|
+
1. 从官方登录态中读取 `access_token` / `refresh_token` / `account_id`
|
|
102
|
+
2. 请求 `https://chatgpt.com/backend-api/wham/usage`
|
|
103
|
+
3. 将最新结果写入 `~/.codexl/state.json`
|
|
104
|
+
4. 最后读取本地最新缓存进行展示
|
|
105
|
+
|
|
106
|
+
## Generated Codex Config
|
|
107
|
+
|
|
108
|
+
`codexl config` writes a managed provider block like this:
|
|
109
|
+
|
|
110
|
+
```toml
|
|
111
|
+
# >>> codexl managed start >>>
|
|
112
|
+
[model_providers.codexl]
|
|
113
|
+
name = "codexl"
|
|
114
|
+
base_url = "http://127.0.0.1:4389/v1"
|
|
115
|
+
http_headers = { Authorization = "Bearer codexl-defaultkey" }
|
|
116
|
+
wire_api = "responses"
|
|
117
|
+
# <<< codexl managed end <<<
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Rules:
|
|
121
|
+
|
|
122
|
+
- If `[model_providers.codexl]` already exists, it is replaced
|
|
123
|
+
- If global `model_provider` exists, it is changed to `codexl`
|
|
124
|
+
- If commented `# model_provider = ...` exists, it is reopened as `model_provider = "codexl"`
|
|
125
|
+
- Global `model` is kept unchanged
|
|
126
|
+
- If you start with `--port`, the port is saved to `~/.codexl/config.yaml`, and later `get` / `config` will use that port
|
|
127
|
+
|
|
128
|
+
## Data Directory
|
|
129
|
+
|
|
130
|
+
`codexl` uses:
|
|
131
|
+
|
|
132
|
+
- `~/.codexl/config.yaml`
|
|
133
|
+
- `~/.codexl/state.json`
|
|
134
|
+
- `~/.codexl/codexl.pid`
|
|
135
|
+
- `~/.codexl/logs/service.log`
|
|
136
|
+
|
|
137
|
+
If you previously used `~/.codexsw`, it will be migrated automatically.
|
|
138
|
+
|
|
139
|
+
## Limit Handling
|
|
140
|
+
|
|
141
|
+
English:
|
|
142
|
+
|
|
143
|
+
- Weekly limit: blocked until weekly reset time
|
|
144
|
+
- 5-hour limit: blocked until 5-hour reset time
|
|
145
|
+
- Temporary limit: blocked for 5 minutes
|
|
146
|
+
|
|
147
|
+
中文:
|
|
148
|
+
|
|
149
|
+
- 周限制:禁用到周窗口重置时间
|
|
150
|
+
- 5 小时限制:禁用到 5 小时窗口重置时间
|
|
151
|
+
- 临时限流:先禁用 5 分钟
|
|
152
|
+
|
|
153
|
+
## Repository
|
|
154
|
+
|
|
155
|
+
- GitHub: https://github.com/openxiaobu/codexl
|
|
156
|
+
- Issues: https://github.com/openxiaobu/codexl/issues
|
|
157
|
+
|
|
158
|
+
## Development
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
npm install
|
|
162
|
+
npm run build
|
|
163
|
+
npm run check
|
|
164
|
+
```
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getCodexDataDir = getCodexDataDir;
|
|
7
|
+
exports.readRegistry = readRegistry;
|
|
8
|
+
exports.readAuthFile = readAuthFile;
|
|
9
|
+
exports.writeAuthFile = writeAuthFile;
|
|
10
|
+
exports.resolvePrimaryRegistryAccount = resolvePrimaryRegistryAccount;
|
|
11
|
+
exports.registerManagedAccount = registerManagedAccount;
|
|
12
|
+
exports.removeManagedAccount = removeManagedAccount;
|
|
13
|
+
exports.findManagedAccount = findManagedAccount;
|
|
14
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
15
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
16
|
+
const config_1 = require("./config");
|
|
17
|
+
/**
|
|
18
|
+
* 读取指定账号 HOME 下的 `.codex` 目录。
|
|
19
|
+
*
|
|
20
|
+
* @param codexHome 账号独立 HOME 目录。
|
|
21
|
+
* @returns `.codex` 目录绝对路径。
|
|
22
|
+
*/
|
|
23
|
+
function getCodexDataDir(codexHome) {
|
|
24
|
+
return node_path_1.default.join((0, config_1.expandHome)(codexHome), ".codex");
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* 读取某账号对应的 `registry.json`。
|
|
28
|
+
*
|
|
29
|
+
* @param codexHome 账号独立 HOME 目录。
|
|
30
|
+
* @returns 解析后的 registry;不存在时返回 `null`。
|
|
31
|
+
*/
|
|
32
|
+
function readRegistry(codexHome) {
|
|
33
|
+
const registryPath = node_path_1.default.join(getCodexDataDir(codexHome), "accounts", "registry.json");
|
|
34
|
+
if (!node_fs_1.default.existsSync(registryPath)) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
return JSON.parse(node_fs_1.default.readFileSync(registryPath, "utf8"));
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 读取账号目录下当前激活凭据文件。
|
|
41
|
+
*
|
|
42
|
+
* @param codexHome 账号独立 HOME 目录。
|
|
43
|
+
* @returns 解析后的 auth.json;不存在时返回 `null`。
|
|
44
|
+
*/
|
|
45
|
+
function readAuthFile(codexHome) {
|
|
46
|
+
const authPath = node_path_1.default.join(getCodexDataDir(codexHome), "auth.json");
|
|
47
|
+
if (!node_fs_1.default.existsSync(authPath)) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
return JSON.parse(node_fs_1.default.readFileSync(authPath, "utf8"));
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 将最新认证信息回写到指定账号的 `auth.json`。
|
|
54
|
+
*
|
|
55
|
+
* @param codexHome 账号独立 HOME 目录。
|
|
56
|
+
* @param auth 最新认证信息。
|
|
57
|
+
* @returns 无返回值。
|
|
58
|
+
*/
|
|
59
|
+
function writeAuthFile(codexHome, auth) {
|
|
60
|
+
const authPath = node_path_1.default.join(getCodexDataDir(codexHome), "auth.json");
|
|
61
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(authPath), { recursive: true });
|
|
62
|
+
node_fs_1.default.writeFileSync(authPath, `${JSON.stringify(auth, null, 2)}\n`, "utf8");
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 根据当前账号目录中的 registry 推断主账号信息。
|
|
66
|
+
*
|
|
67
|
+
* @param codexHome 账号独立 HOME 目录。
|
|
68
|
+
* @returns 当前活跃账号元数据;无可用账号时返回 `null`。
|
|
69
|
+
*/
|
|
70
|
+
function resolvePrimaryRegistryAccount(codexHome) {
|
|
71
|
+
const registry = readRegistry(codexHome);
|
|
72
|
+
if (!registry || registry.accounts.length === 0) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
if (registry.active_email) {
|
|
76
|
+
const active = registry.accounts.find((item) => item.email === registry.active_email);
|
|
77
|
+
if (active) {
|
|
78
|
+
return active;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return registry.accounts[0] ?? null;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* 将账号注册到 codexl 配置中,并为其准备独立 HOME 目录。
|
|
85
|
+
*
|
|
86
|
+
* @param accountId 本地账号标识。
|
|
87
|
+
* @param codexHome 可选的自定义 HOME 目录;未提供时使用默认路径。
|
|
88
|
+
* @returns 写入后的账号配置。
|
|
89
|
+
*/
|
|
90
|
+
function registerManagedAccount(accountId, codexHome) {
|
|
91
|
+
const home = codexHome ? (0, config_1.expandHome)(codexHome) : (0, config_1.getManagedHome)(accountId);
|
|
92
|
+
// 预先创建账号隔离目录,方便后续直接执行 codex login。
|
|
93
|
+
node_fs_1.default.mkdirSync(home, { recursive: true });
|
|
94
|
+
const primary = resolvePrimaryRegistryAccount(home);
|
|
95
|
+
const account = {
|
|
96
|
+
id: accountId,
|
|
97
|
+
name: accountId,
|
|
98
|
+
codex_home: home,
|
|
99
|
+
email: primary?.email,
|
|
100
|
+
enabled: true,
|
|
101
|
+
imported_at: new Date().toISOString()
|
|
102
|
+
};
|
|
103
|
+
(0, config_1.upsertAccount)(account);
|
|
104
|
+
return account;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* 从配置中删除指定账号;默认仅删除配置项,不主动删除本地 HOME 目录。
|
|
108
|
+
*
|
|
109
|
+
* @param accountId 本地账号标识。
|
|
110
|
+
* @returns 被删除的账号配置;未命中时返回 `null`。
|
|
111
|
+
*/
|
|
112
|
+
function removeManagedAccount(accountId) {
|
|
113
|
+
const config = (0, config_1.loadConfig)();
|
|
114
|
+
const index = config.accounts.findIndex((item) => item.id === accountId);
|
|
115
|
+
if (index < 0) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
const [removed] = config.accounts.splice(index, 1);
|
|
119
|
+
(0, config_1.saveConfig)(config);
|
|
120
|
+
return removed ?? null;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* 根据账号标识读取配置中的账号项。
|
|
124
|
+
*
|
|
125
|
+
* @param accountId 本地账号标识。
|
|
126
|
+
* @returns 命中的账号配置;未命中时返回 `null`。
|
|
127
|
+
*/
|
|
128
|
+
function findManagedAccount(accountId) {
|
|
129
|
+
const config = (0, config_1.loadConfig)();
|
|
130
|
+
return config.accounts.find((item) => item.id === accountId) ?? null;
|
|
131
|
+
}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const node_child_process_1 = require("node:child_process");
|
|
10
|
+
const commander_1 = require("commander");
|
|
11
|
+
const account_store_1 = require("./account-store");
|
|
12
|
+
const config_1 = require("./config");
|
|
13
|
+
const login_1 = require("./login");
|
|
14
|
+
const status_1 = require("./status");
|
|
15
|
+
const usage_sync_1 = require("./usage-sync");
|
|
16
|
+
/**
|
|
17
|
+
* 刷新所有已录入账号的远端额度,并输出最新状态表格。
|
|
18
|
+
*
|
|
19
|
+
* @returns Promise,无返回值。
|
|
20
|
+
*/
|
|
21
|
+
async function handleStatus() {
|
|
22
|
+
await (0, usage_sync_1.refreshAllAccountUsage)();
|
|
23
|
+
const statuses = (0, status_1.collectAccountStatuses)();
|
|
24
|
+
console.log((0, status_1.renderStatusTable)(statuses));
|
|
25
|
+
const available = statuses.filter((item) => item.isAvailable).length;
|
|
26
|
+
const cooldown = statuses.filter((item) => item.isFiveHourLimited && !item.isWeeklyLimited).length;
|
|
27
|
+
const weeklyLimited = statuses.filter((item) => item.isWeeklyLimited).length;
|
|
28
|
+
console.log("");
|
|
29
|
+
console.log(`available=${available} cooldown=${cooldown} weekly_limited=${weeklyLimited}`);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 将已有的 Codex HOME 目录纳入 codexl 管理。
|
|
33
|
+
*
|
|
34
|
+
* @param accountId 本地账号标识。
|
|
35
|
+
* @param codexHome 现有 HOME 目录;若未传则默认使用当前用户 HOME。
|
|
36
|
+
* @returns 无返回值。
|
|
37
|
+
*/
|
|
38
|
+
function handleAccountImport(accountId, codexHome) {
|
|
39
|
+
const home = codexHome ? (0, config_1.expandHome)(codexHome) : process.env.HOME ?? "";
|
|
40
|
+
const account = (0, account_store_1.registerManagedAccount)(accountId, home);
|
|
41
|
+
console.log(`账号已导入: ${account.id}`);
|
|
42
|
+
console.log(`来源 HOME: ${account.codex_home}`);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 执行隔离登录流程,将账号录入到 codexl 管理目录。
|
|
46
|
+
*
|
|
47
|
+
* @param accountId 本地账号标识。
|
|
48
|
+
* @returns Promise,无返回值。
|
|
49
|
+
*/
|
|
50
|
+
async function handleAccountLogin(accountId) {
|
|
51
|
+
const home = await (0, login_1.loginManagedAccount)(accountId);
|
|
52
|
+
console.log(`登录完成,账号目录: ${home}`);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 删除配置中的账号项。
|
|
56
|
+
*
|
|
57
|
+
* @param accountId 本地账号标识。
|
|
58
|
+
* @returns 无返回值。
|
|
59
|
+
* @throws 当账号不存在时抛出错误。
|
|
60
|
+
*/
|
|
61
|
+
function handleAccountRemove(accountId) {
|
|
62
|
+
const removed = (0, account_store_1.removeManagedAccount)(accountId);
|
|
63
|
+
if (!removed) {
|
|
64
|
+
throw new Error(`未找到账号 ${accountId}`);
|
|
65
|
+
}
|
|
66
|
+
console.log(`已删除账号配置: ${removed.id}`);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* 判断后台服务当前是否在运行。
|
|
70
|
+
*
|
|
71
|
+
* @returns 运行中的 PID;未运行时返回 `null`。
|
|
72
|
+
*/
|
|
73
|
+
function getRunningPid() {
|
|
74
|
+
const pidPath = (0, config_1.getPidPath)();
|
|
75
|
+
if (!node_fs_1.default.existsSync(pidPath)) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
const raw = node_fs_1.default.readFileSync(pidPath, "utf8").trim();
|
|
79
|
+
const pid = Number(raw);
|
|
80
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
81
|
+
node_fs_1.default.rmSync(pidPath, { force: true });
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
process.kill(pid, 0);
|
|
86
|
+
return pid;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
node_fs_1.default.rmSync(pidPath, { force: true });
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* 后台启动 codexl 服务并写入 PID 文件。
|
|
95
|
+
*
|
|
96
|
+
* @returns Promise,无返回值。
|
|
97
|
+
* @throws 当服务已在运行或子进程启动失败时抛出异常。
|
|
98
|
+
*/
|
|
99
|
+
async function handleStart(portOverride) {
|
|
100
|
+
const config = (0, config_1.loadConfig)();
|
|
101
|
+
const port = portOverride ? Number(portOverride) : config.server.port;
|
|
102
|
+
if (portOverride) {
|
|
103
|
+
config.server.port = port;
|
|
104
|
+
(0, config_1.saveConfig)(config);
|
|
105
|
+
}
|
|
106
|
+
const runningPid = getRunningPid();
|
|
107
|
+
if (runningPid) {
|
|
108
|
+
console.log(`服务已在运行,PID=${runningPid}`);
|
|
109
|
+
if (portOverride) {
|
|
110
|
+
console.log(`已将新端口写入配置: ${port}`);
|
|
111
|
+
console.log("请先执行 codexl stop,再执行 codexl start 使新端口生效。");
|
|
112
|
+
}
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const logPath = (0, config_1.getServiceLogPath)();
|
|
116
|
+
const logFd = node_fs_1.default.openSync(logPath, "a");
|
|
117
|
+
const child = (0, node_child_process_1.spawn)(process.execPath, [__filename.replace(/cli\.js$/, "serve.js"), "--port", String(port)], {
|
|
118
|
+
detached: true,
|
|
119
|
+
stdio: ["ignore", logFd, logFd]
|
|
120
|
+
});
|
|
121
|
+
child.unref();
|
|
122
|
+
node_fs_1.default.writeFileSync((0, config_1.getPidPath)(), `${child.pid}\n`, "utf8");
|
|
123
|
+
console.log(`服务已启动: http://${config.server.host}:${port}`);
|
|
124
|
+
console.log(`PID: ${child.pid}`);
|
|
125
|
+
console.log(`日志: ${logPath}`);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* 停止后台运行的 codexl 服务。
|
|
129
|
+
*
|
|
130
|
+
* @returns 无返回值。
|
|
131
|
+
*/
|
|
132
|
+
function handleStop() {
|
|
133
|
+
const pid = getRunningPid();
|
|
134
|
+
if (!pid) {
|
|
135
|
+
console.log("服务未运行");
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
process.kill(pid, "SIGTERM");
|
|
139
|
+
node_fs_1.default.rmSync((0, config_1.getPidPath)(), { force: true });
|
|
140
|
+
console.log(`服务已停止,PID=${pid}`);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* 输出当前 `codex` 需要的 provider 配置与本地 key 信息。
|
|
144
|
+
*
|
|
145
|
+
* @returns 无返回值。
|
|
146
|
+
*/
|
|
147
|
+
function handleGetConfig() {
|
|
148
|
+
const config = (0, config_1.loadConfig)();
|
|
149
|
+
console.log(`base_url=${`http://${config.server.host}:${config.server.port}/v1`}`);
|
|
150
|
+
console.log(`api_key=${config.server.api_key}`);
|
|
151
|
+
}
|
|
152
|
+
function escapeRegExp(input) {
|
|
153
|
+
return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* 将 codexl provider 配置写入指定的 codex config.toml。
|
|
157
|
+
*
|
|
158
|
+
* @param targetPathOrDir 可选的 codex 配置目录或 config.toml 文件路径。
|
|
159
|
+
* @returns 无返回值。
|
|
160
|
+
*/
|
|
161
|
+
function handleConfig(targetPathOrDir) {
|
|
162
|
+
const config = (0, config_1.loadConfig)();
|
|
163
|
+
const rawTarget = targetPathOrDir ? (0, config_1.expandHome)(targetPathOrDir) : node_path_1.default.join(process.env.HOME ?? "", ".codex", "config.toml");
|
|
164
|
+
const targetFile = rawTarget.endsWith(".toml") ? rawTarget : node_path_1.default.join(rawTarget, "config.toml");
|
|
165
|
+
const startMarker = "# >>> codexl managed start >>>";
|
|
166
|
+
const endMarker = "# <<< codexl managed end <<<";
|
|
167
|
+
const block = [
|
|
168
|
+
startMarker,
|
|
169
|
+
"[model_providers.codexl]",
|
|
170
|
+
'name = "codexl"',
|
|
171
|
+
`base_url = "http://${config.server.host}:${config.server.port}/v1"`,
|
|
172
|
+
`http_headers = { Authorization = "Bearer ${config.server.api_key}" }`,
|
|
173
|
+
'wire_api = "responses"',
|
|
174
|
+
endMarker
|
|
175
|
+
].join("\n");
|
|
176
|
+
let original = "";
|
|
177
|
+
if (node_fs_1.default.existsSync(targetFile)) {
|
|
178
|
+
original = node_fs_1.default.readFileSync(targetFile, "utf8");
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(targetFile), { recursive: true });
|
|
182
|
+
}
|
|
183
|
+
const managedBlockPattern = new RegExp(`${escapeRegExp(startMarker)}[\\s\\S]*?${escapeRegExp(endMarker)}\\n?`, "g");
|
|
184
|
+
const lines = original.replace(managedBlockPattern, "").split(/\r?\n/);
|
|
185
|
+
let insertAfterIndex = -1;
|
|
186
|
+
let hasGlobalModelProvider = false;
|
|
187
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
188
|
+
const line = lines[i];
|
|
189
|
+
const trimmed = line.trim();
|
|
190
|
+
if (/^#\s*model_provider\s*=/.test(trimmed)) {
|
|
191
|
+
lines[i] = 'model_provider = "codexl"';
|
|
192
|
+
hasGlobalModelProvider = true;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (trimmed.startsWith("#")) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (/^model_provider\s*=/.test(trimmed)) {
|
|
199
|
+
lines[i] = 'model_provider = "codexl"';
|
|
200
|
+
hasGlobalModelProvider = true;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (trimmed === "[model_providers.codexl]") {
|
|
204
|
+
let j = i;
|
|
205
|
+
while (j < lines.length) {
|
|
206
|
+
const current = lines[j];
|
|
207
|
+
const currentTrimmed = current.trim();
|
|
208
|
+
if (j > i && currentTrimmed.startsWith("[") && !currentTrimmed.startsWith("[[")) {
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
insertAfterIndex = j;
|
|
212
|
+
j += 1;
|
|
213
|
+
}
|
|
214
|
+
lines.splice(i, j - i);
|
|
215
|
+
insertAfterIndex = i - 1;
|
|
216
|
+
i = j - 1;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const blockLines = block.split("\n");
|
|
220
|
+
if (insertAfterIndex >= 0) {
|
|
221
|
+
lines.splice(insertAfterIndex + 1, 0, "", ...blockLines);
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
if (!hasGlobalModelProvider) {
|
|
225
|
+
const firstNonEmptyIndex = lines.findIndex((line) => line.trim() !== "");
|
|
226
|
+
if (firstNonEmptyIndex >= 0) {
|
|
227
|
+
lines.splice(firstNonEmptyIndex, 0, 'model_provider = "codexl"', "");
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
lines.push('model_provider = "codexl"', "");
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (lines.length > 0 && lines[lines.length - 1].trim() !== "") {
|
|
234
|
+
lines.push("");
|
|
235
|
+
}
|
|
236
|
+
lines.push(...blockLines);
|
|
237
|
+
}
|
|
238
|
+
const nextContent = `${lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd()}\n`;
|
|
239
|
+
node_fs_1.default.writeFileSync(targetFile, nextContent, "utf8");
|
|
240
|
+
console.log(`已写入: ${targetFile}`);
|
|
241
|
+
console.log(`base_url=http://${config.server.host}:${config.server.port}/v1`);
|
|
242
|
+
console.log(`api_key=${config.server.api_key}`);
|
|
243
|
+
console.log('提示: 已写入 codexl provider;如果原来存在 model_provider,则已切换为 codexl;model 保持不变。');
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* CLI 主入口,负责命令注册与执行。
|
|
247
|
+
*
|
|
248
|
+
* @returns Promise,无返回值。
|
|
249
|
+
* @throws 当命令执行失败时向上抛出异常。
|
|
250
|
+
*/
|
|
251
|
+
async function main() {
|
|
252
|
+
const program = new commander_1.Command();
|
|
253
|
+
(0, config_1.getCodexSwHome)();
|
|
254
|
+
(0, config_1.loadConfig)();
|
|
255
|
+
program
|
|
256
|
+
.name("codexsw")
|
|
257
|
+
.name("codexl")
|
|
258
|
+
.description("本地 Codex 多账号切换与状态管理工具")
|
|
259
|
+
.version("0.1.0");
|
|
260
|
+
program
|
|
261
|
+
.command("add")
|
|
262
|
+
.description("登录并新增一个账号或工作空间")
|
|
263
|
+
.argument("<accountId>", "账号标识")
|
|
264
|
+
.action(async (accountId) => {
|
|
265
|
+
await handleAccountLogin(accountId);
|
|
266
|
+
});
|
|
267
|
+
program
|
|
268
|
+
.command("del")
|
|
269
|
+
.description("删除一个已录入账号")
|
|
270
|
+
.argument("<accountId>", "账号标识")
|
|
271
|
+
.action(handleAccountRemove);
|
|
272
|
+
program
|
|
273
|
+
.command("import")
|
|
274
|
+
.description("导入当前或指定 HOME 下的官方 codex 登录态")
|
|
275
|
+
.argument("<accountId>", "账号标识")
|
|
276
|
+
.argument("[codexHome]", "已有 HOME 目录,默认当前用户 HOME")
|
|
277
|
+
.action(handleAccountImport);
|
|
278
|
+
program
|
|
279
|
+
.command("status")
|
|
280
|
+
.description("刷新并查看所有已录入账号或工作空间的最新额度")
|
|
281
|
+
.action(async () => {
|
|
282
|
+
await handleStatus();
|
|
283
|
+
});
|
|
284
|
+
program
|
|
285
|
+
.command("start")
|
|
286
|
+
.description("后台启动本地代理服务")
|
|
287
|
+
.option("--port <port>", "监听端口")
|
|
288
|
+
.action(async (options) => {
|
|
289
|
+
await handleStart(options.port);
|
|
290
|
+
});
|
|
291
|
+
program.command("stop").description("停止后台代理服务").action(handleStop);
|
|
292
|
+
program.command("get").description("输出当前 base_url 和 api_key").action(handleGetConfig);
|
|
293
|
+
program
|
|
294
|
+
.command("config")
|
|
295
|
+
.description("自动写入 codex 的 config.toml,默认 ~/.codex/config.toml")
|
|
296
|
+
.argument("[codexPath]", "codex 配置目录或 config.toml 文件路径")
|
|
297
|
+
.action(handleConfig);
|
|
298
|
+
await program.parseAsync(process.argv);
|
|
299
|
+
}
|
|
300
|
+
void main().catch((error) => {
|
|
301
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
302
|
+
console.error(`codexl 执行失败: ${message}`);
|
|
303
|
+
process.exit(1);
|
|
304
|
+
});
|