codex-slot 0.1.5 → 0.1.7
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 +2 -2
- package/dist/app/service-lifecycle-service.js +110 -22
- package/dist/codex-config.js +1 -3
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -71,7 +71,7 @@ codex-slot start --port 4399
|
|
|
71
71
|
```
|
|
72
72
|
|
|
73
73
|
`start` will automatically write the required provider config into `~/.codex/config.toml`.
|
|
74
|
-
It prefers port `4399` by default and will switch to the next available port automatically when `4399` is busy:
|
|
74
|
+
It prefers port `4399` by default and will switch to the next available port automatically when `4399` is busy, then sync that actual port into config:
|
|
75
75
|
Each start also generates a fresh local `api_key` and syncs it into the managed provider config.
|
|
76
76
|
|
|
77
77
|
```bash
|
|
@@ -140,7 +140,7 @@ Behavior:
|
|
|
140
140
|
- On `cslot stop`, the original `model_provider` line and original `[model_providers.cslot]` block are restored from the saved snapshot
|
|
141
141
|
- Other providers and settings in `config.toml` are left untouched
|
|
142
142
|
- If you start with `--port`, the port is saved to `~/.cslot/config.yaml`
|
|
143
|
-
- If you start without `--port`, `4399` is preferred first and the next free port is chosen automatically on conflict
|
|
143
|
+
- If you start without `--port`, `4399` is preferred first and the next free port is chosen automatically on conflict, and the actual chosen port is written back to `~/.cslot/config.yaml` and the managed provider block
|
|
144
144
|
- Every `start` rotates the local `api_key`, and the new value is written to both `~/.cslot/config.yaml` and the managed provider block
|
|
145
145
|
|
|
146
146
|
## Data Directory
|
|
@@ -10,9 +10,22 @@ const node_fs_1 = __importDefault(require("node:fs"));
|
|
|
10
10
|
const node_net_1 = __importDefault(require("node:net"));
|
|
11
11
|
const node_path_1 = __importDefault(require("node:path"));
|
|
12
12
|
const node_child_process_1 = require("node:child_process");
|
|
13
|
+
const undici_1 = require("undici");
|
|
13
14
|
const codex_config_1 = require("../codex-config");
|
|
14
15
|
const cli_helpers_1 = require("../cli-helpers");
|
|
15
16
|
const config_1 = require("../config");
|
|
17
|
+
const STARTUP_POLL_INTERVAL_MS = 100;
|
|
18
|
+
const STARTUP_TIMEOUT_MS = 5000;
|
|
19
|
+
/**
|
|
20
|
+
* 休眠指定毫秒数,供启动轮询流程复用。
|
|
21
|
+
*
|
|
22
|
+
* @param delayMs 等待时长,单位毫秒。
|
|
23
|
+
* @returns Promise,等待结束后返回。
|
|
24
|
+
* @throws 无显式抛出。
|
|
25
|
+
*/
|
|
26
|
+
function sleep(delayMs) {
|
|
27
|
+
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
28
|
+
}
|
|
16
29
|
/**
|
|
17
30
|
* 判断后台服务当前是否在运行。
|
|
18
31
|
*
|
|
@@ -78,21 +91,93 @@ function isPortAvailable(host, port) {
|
|
|
78
91
|
server.listen(port, host);
|
|
79
92
|
});
|
|
80
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* 通过健康检查探测后台服务是否已经完成启动。
|
|
96
|
+
*
|
|
97
|
+
* @param host 本地监听地址。
|
|
98
|
+
* @param port 期望监听的端口。
|
|
99
|
+
* @returns Promise,健康检查通过时返回 `true`,否则返回 `false`。
|
|
100
|
+
* @throws 无显式抛出。
|
|
101
|
+
*/
|
|
102
|
+
async function isManagedServiceHealthy(host, port) {
|
|
103
|
+
try {
|
|
104
|
+
const response = await (0, undici_1.request)(`http://${host}:${port}/health`, {
|
|
105
|
+
method: "GET",
|
|
106
|
+
headersTimeout: 500,
|
|
107
|
+
bodyTimeout: 500
|
|
108
|
+
});
|
|
109
|
+
if (response.statusCode !== 200) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
const payload = (await response.body.json());
|
|
113
|
+
return payload.ok === true;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* 等待后台服务真正进入可用状态,避免“配置已写入但服务未成功启动”的假成功状态。
|
|
121
|
+
*
|
|
122
|
+
* @param host 本地监听地址。
|
|
123
|
+
* @param port 期望监听的端口。
|
|
124
|
+
* @param pid 子进程 PID。
|
|
125
|
+
* @param timeoutMs 等待超时时间,单位毫秒。
|
|
126
|
+
* @returns Promise,健康检查通过时正常返回。
|
|
127
|
+
* @throws 当子进程提前退出、超时或服务始终未就绪时抛出异常。
|
|
128
|
+
*/
|
|
129
|
+
async function waitForManagedServiceReady(host, port, pid, timeoutMs = STARTUP_TIMEOUT_MS) {
|
|
130
|
+
const startedAt = Date.now();
|
|
131
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
132
|
+
try {
|
|
133
|
+
process.kill(pid, 0);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
throw new Error(`后台服务启动失败,进程已退出,PID=${pid}`);
|
|
137
|
+
}
|
|
138
|
+
// 只有健康检查通过,才认为本地代理已经可安全对外服务。
|
|
139
|
+
if (await isManagedServiceHealthy(host, port)) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
await sleep(STARTUP_POLL_INTERVAL_MS);
|
|
143
|
+
}
|
|
144
|
+
throw new Error(`后台服务启动超时,${host}:${port} 未在 ${timeoutMs}ms 内通过健康检查`);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* 在启动失败时终止残留子进程,并恢复启动前的本地配置与 Codex 接管状态。
|
|
148
|
+
*
|
|
149
|
+
* @param pid 可能已创建的子进程 PID。
|
|
150
|
+
* @param previousConfig 启动前的原始配置快照。
|
|
151
|
+
* @returns 无返回值。
|
|
152
|
+
* @throws 无显式抛出。
|
|
153
|
+
*/
|
|
154
|
+
function rollbackFailedStart(pid, previousConfig) {
|
|
155
|
+
if (pid && Number.isInteger(pid) && pid > 0) {
|
|
156
|
+
try {
|
|
157
|
+
process.kill(pid, "SIGTERM");
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
// 子进程可能已经自行退出,此处按幂等清理处理。
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
node_fs_1.default.rmSync((0, config_1.getPidPath)(), { force: true });
|
|
164
|
+
(0, config_1.saveConfig)(previousConfig);
|
|
165
|
+
(0, codex_config_1.deactivateManagedCodexConfig)();
|
|
166
|
+
}
|
|
81
167
|
/**
|
|
82
168
|
* 为后台服务挑选最终启动端口。
|
|
83
169
|
*
|
|
84
170
|
* 规则:
|
|
85
171
|
* 1. 若用户显式指定 `--port`,则严格使用该端口,冲突时直接报错。
|
|
86
172
|
* 2. 若未显式指定端口,则优先使用 4399。
|
|
87
|
-
* 3.
|
|
173
|
+
* 3. 若默认候选端口冲突,则从 4399 开始向上查找下一个可用端口。
|
|
88
174
|
*
|
|
89
175
|
* @param host 监听地址。
|
|
90
|
-
* @param currentPort 当前配置中的端口。
|
|
91
176
|
* @param portOverride 用户显式指定的端口文本。
|
|
92
177
|
* @returns Promise,成功时返回最终端口与是否发生自动切换。
|
|
93
178
|
* @throws 当显式指定端口冲突或找不到可用端口时抛出异常。
|
|
94
179
|
*/
|
|
95
|
-
async function resolveStartPort(host,
|
|
180
|
+
async function resolveStartPort(host, portOverride) {
|
|
96
181
|
if (portOverride) {
|
|
97
182
|
const port = (0, cli_helpers_1.parsePort)(portOverride);
|
|
98
183
|
if (!(await isPortAvailable(host, port))) {
|
|
@@ -100,7 +185,7 @@ async function resolveStartPort(host, currentPort, portOverride) {
|
|
|
100
185
|
}
|
|
101
186
|
return { port, autoSwitched: false };
|
|
102
187
|
}
|
|
103
|
-
const preferredPort =
|
|
188
|
+
const preferredPort = 4399;
|
|
104
189
|
for (let candidate = preferredPort; candidate < preferredPort + 50; candidate += 1) {
|
|
105
190
|
if (await isPortAvailable(host, candidate)) {
|
|
106
191
|
return {
|
|
@@ -120,37 +205,26 @@ async function resolveStartPort(host, currentPort, portOverride) {
|
|
|
120
205
|
*/
|
|
121
206
|
async function startManagedService(portOverride) {
|
|
122
207
|
const config = (0, config_1.loadConfig)();
|
|
123
|
-
const
|
|
124
|
-
const
|
|
208
|
+
const previousConfig = structuredClone(config);
|
|
209
|
+
const { port, autoSwitched } = await resolveStartPort(config.server.host, portOverride);
|
|
125
210
|
const runningPid = getRunningPid();
|
|
126
|
-
if (runningPid && hasExplicitPortOverride && config.server.port !== port) {
|
|
127
|
-
config.server.port = port;
|
|
128
|
-
(0, config_1.saveConfig)(config);
|
|
129
|
-
}
|
|
130
211
|
if (runningPid) {
|
|
131
212
|
return {
|
|
132
213
|
alreadyRunning: true,
|
|
133
214
|
pid: runningPid,
|
|
134
|
-
port,
|
|
215
|
+
port: config.server.port,
|
|
135
216
|
logPath: (0, config_1.getServiceLogPath)(),
|
|
136
217
|
autoSwitched: false,
|
|
137
218
|
apiKeyRotated: false
|
|
138
219
|
};
|
|
139
220
|
}
|
|
140
|
-
if (
|
|
221
|
+
if (config.server.port !== port) {
|
|
141
222
|
config.server.port = port;
|
|
142
223
|
(0, config_1.saveConfig)(config);
|
|
143
224
|
}
|
|
144
225
|
// 每次真正启动服务前都轮换一次本地 api_key,并让受管 config.toml 使用同一新值。
|
|
145
226
|
const persistedConfig = (0, config_1.rotateServerApiKey)(config);
|
|
146
|
-
|
|
147
|
-
...persistedConfig,
|
|
148
|
-
server: {
|
|
149
|
-
...persistedConfig.server,
|
|
150
|
-
port
|
|
151
|
-
}
|
|
152
|
-
};
|
|
153
|
-
(0, codex_config_1.applyManagedCodexConfig)(undefined, { config: runtimeConfig });
|
|
227
|
+
(0, codex_config_1.applyManagedCodexConfig)(undefined, { config: persistedConfig });
|
|
154
228
|
const logPath = (0, config_1.getServiceLogPath)();
|
|
155
229
|
const logFd = node_fs_1.default.openSync(logPath, "a");
|
|
156
230
|
const serveEntrypoint = resolveServeEntrypoint();
|
|
@@ -158,11 +232,25 @@ async function startManagedService(portOverride) {
|
|
|
158
232
|
detached: true,
|
|
159
233
|
stdio: ["ignore", logFd, logFd]
|
|
160
234
|
});
|
|
235
|
+
const childPid = child.pid ?? null;
|
|
161
236
|
child.unref();
|
|
162
|
-
|
|
237
|
+
if (!childPid) {
|
|
238
|
+
node_fs_1.default.closeSync(logFd);
|
|
239
|
+
rollbackFailedStart(null, previousConfig);
|
|
240
|
+
throw new Error("后台服务启动失败,未获取到有效子进程 PID");
|
|
241
|
+
}
|
|
242
|
+
node_fs_1.default.writeFileSync((0, config_1.getPidPath)(), `${childPid}\n`, "utf8");
|
|
243
|
+
node_fs_1.default.closeSync(logFd);
|
|
244
|
+
try {
|
|
245
|
+
await waitForManagedServiceReady(config.server.host, port, childPid);
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
rollbackFailedStart(childPid, previousConfig);
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
163
251
|
return {
|
|
164
252
|
alreadyRunning: false,
|
|
165
|
-
pid:
|
|
253
|
+
pid: childPid,
|
|
166
254
|
port,
|
|
167
255
|
logPath,
|
|
168
256
|
autoSwitched,
|
package/dist/codex-config.js
CHANGED
|
@@ -72,9 +72,7 @@ function buildManagedProviderBlock(eol, config) {
|
|
|
72
72
|
'name = "cslot"',
|
|
73
73
|
`base_url = "http://${config.server.host}:${config.server.port}/v1"`,
|
|
74
74
|
'wire_api = "responses"',
|
|
75
|
-
""
|
|
76
|
-
"[model_providers.cslot.http_headers]",
|
|
77
|
-
`Authorization = "Bearer ${config.server.api_key}"`,
|
|
75
|
+
`http_headers = { Authorization = "Bearer ${config.server.api_key}" }`,
|
|
78
76
|
PROVIDER_BLOCK_END_MARKER
|
|
79
77
|
].join(eol);
|
|
80
78
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-slot",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "本地 Codex 多账号切换与状态管理工具",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "dist/cli.js",
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"build": "npm run clean && tsc -p tsconfig.json && chmod +x dist/cli.js dist/serve.js",
|
|
17
17
|
"prepublishOnly": "npm run build",
|
|
18
18
|
"dev": "tsx src/cli.ts",
|
|
19
|
-
"check": "tsc --noEmit -p tsconfig.json"
|
|
19
|
+
"check": "tsc --noEmit -p tsconfig.json",
|
|
20
|
+
"test": "npm run build && node --test test/*.test.js"
|
|
20
21
|
},
|
|
21
22
|
"keywords": [
|
|
22
23
|
"codex",
|