codex-slot 0.1.1 → 0.1.3
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 +31 -11
- package/dist/account-commands.js +91 -0
- package/dist/account-store.js +3 -2
- package/dist/app/account-service.js +121 -0
- package/dist/app/service-lifecycle-service.js +116 -0
- package/dist/app/status-service.js +51 -0
- package/dist/cli-helpers.js +44 -0
- package/dist/cli.js +92 -575
- package/dist/codex-config.js +354 -0
- package/dist/config.js +22 -23
- package/dist/login.js +3 -2
- package/dist/serve.js +2 -1
- package/dist/server.js +6 -5
- package/dist/service-control.js +43 -0
- package/dist/state.js +40 -4
- package/dist/status-command.js +216 -0
- package/dist/status.js +16 -6
- package/dist/text.js +33 -0
- package/dist/usage-sync.js +8 -7
- package/package.json +2 -2
|
@@ -0,0 +1,354 @@
|
|
|
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.getDefaultCodexConfigPath = getDefaultCodexConfigPath;
|
|
7
|
+
exports.generateServerApiKey = generateServerApiKey;
|
|
8
|
+
exports.applyManagedCodexConfig = applyManagedCodexConfig;
|
|
9
|
+
exports.deactivateManagedCodexConfig = deactivateManagedCodexConfig;
|
|
10
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
11
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
12
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
13
|
+
const config_1 = require("./config");
|
|
14
|
+
const state_1 = require("./state");
|
|
15
|
+
const text_1 = require("./text");
|
|
16
|
+
const MODEL_PROVIDER_START_MARKER = "# >>> cslot model_provider >>>";
|
|
17
|
+
const MODEL_PROVIDER_END_MARKER = "# <<< cslot model_provider <<<";
|
|
18
|
+
const PROVIDER_BLOCK_START_MARKER = "# >>> cslot provider:cslot >>>";
|
|
19
|
+
const PROVIDER_BLOCK_END_MARKER = "# <<< cslot provider:cslot <<<";
|
|
20
|
+
/**
|
|
21
|
+
* 返回默认的 `codex config.toml` 路径。
|
|
22
|
+
*
|
|
23
|
+
* @returns 默认 `config.toml` 绝对路径。
|
|
24
|
+
*/
|
|
25
|
+
function getDefaultCodexConfigPath() {
|
|
26
|
+
return node_path_1.default.join(process.env.HOME ?? "", ".codex", "config.toml");
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 生成随机本地 API Key,避免继续使用固定默认值。
|
|
30
|
+
*
|
|
31
|
+
* @returns 新的 API Key 字符串,仅包含十六进制字符。
|
|
32
|
+
*/
|
|
33
|
+
function generateServerApiKey() {
|
|
34
|
+
return `cslot-${node_crypto_1.default.randomBytes(18).toString("hex")}`;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 原子方式写入目标文件,避免写入过程中留下半截配置。
|
|
38
|
+
*
|
|
39
|
+
* @param targetFile 目标文件绝对路径。
|
|
40
|
+
* @param content 完整文件内容。
|
|
41
|
+
* @returns 无返回值。
|
|
42
|
+
* @throws 当目录创建、临时文件写入或重命名失败时抛出文件系统错误。
|
|
43
|
+
*/
|
|
44
|
+
function writeFileAtomic(targetFile, content) {
|
|
45
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(targetFile), { recursive: true });
|
|
46
|
+
const tmpFile = `${targetFile}.tmp-${process.pid}-${Date.now()}`;
|
|
47
|
+
node_fs_1.default.writeFileSync(tmpFile, content, "utf8");
|
|
48
|
+
node_fs_1.default.renameSync(tmpFile, targetFile);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* 根据当前文件内容推断换行符风格,尽量保持用户原文件格式不变。
|
|
52
|
+
*
|
|
53
|
+
* @param content 原始文件内容。
|
|
54
|
+
* @returns 当前文件使用的换行符;未命中时默认返回 `\n`。
|
|
55
|
+
*/
|
|
56
|
+
function detectEol(content) {
|
|
57
|
+
return content.includes("\r\n") ? "\r\n" : "\n";
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* 生成受 cslot 接管的 `model_provider` 配置块。
|
|
61
|
+
*
|
|
62
|
+
* @param eol 目标文件当前使用的换行符。
|
|
63
|
+
* @returns 带标记的配置块文本。
|
|
64
|
+
*/
|
|
65
|
+
function buildManagedModelProviderBlock(eol) {
|
|
66
|
+
return [
|
|
67
|
+
MODEL_PROVIDER_START_MARKER,
|
|
68
|
+
'model_provider = "cslot"',
|
|
69
|
+
MODEL_PROVIDER_END_MARKER
|
|
70
|
+
].join(eol);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* 生成受 cslot 接管的 provider 配置块。
|
|
74
|
+
*
|
|
75
|
+
* @param eol 目标文件当前使用的换行符。
|
|
76
|
+
* @returns 带标记的 provider 配置块文本。
|
|
77
|
+
*/
|
|
78
|
+
function buildManagedProviderBlock(eol) {
|
|
79
|
+
const config = (0, config_1.loadConfig)();
|
|
80
|
+
return [
|
|
81
|
+
PROVIDER_BLOCK_START_MARKER,
|
|
82
|
+
"[model_providers.cslot]",
|
|
83
|
+
'name = "cslot"',
|
|
84
|
+
`base_url = "http://${config.server.host}:${config.server.port}/v1"`,
|
|
85
|
+
`http_headers = { Authorization = "Bearer ${config.server.api_key}" }`,
|
|
86
|
+
'wire_api = "responses"',
|
|
87
|
+
PROVIDER_BLOCK_END_MARKER
|
|
88
|
+
].join(eol);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* 在文本中定位受 cslot 接管的块范围。
|
|
92
|
+
*
|
|
93
|
+
* @param content 原始文件内容。
|
|
94
|
+
* @param startMarker 块起始标记。
|
|
95
|
+
* @param endMarker 块结束标记。
|
|
96
|
+
* @returns 命中时返回起止偏移;未命中返回 `null`。
|
|
97
|
+
*/
|
|
98
|
+
function findMarkedBlockRange(content, startMarker, endMarker) {
|
|
99
|
+
const start = content.indexOf(startMarker);
|
|
100
|
+
if (start < 0) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
const endMarkerIndex = content.indexOf(endMarker, start);
|
|
104
|
+
if (endMarkerIndex < 0) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
let end = endMarkerIndex + endMarker.length;
|
|
108
|
+
if (content.slice(end, end + 2) === "\r\n") {
|
|
109
|
+
end += 2;
|
|
110
|
+
}
|
|
111
|
+
else if (content.slice(end, end + 1) === "\n") {
|
|
112
|
+
end += 1;
|
|
113
|
+
}
|
|
114
|
+
return { start, end };
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* 恢复文本中由 cslot 管理的配置块,得到接管前的原始内容基线。
|
|
118
|
+
*
|
|
119
|
+
* @param content 当前 `config.toml` 内容。
|
|
120
|
+
* @param managedState 上一次接管时保存的原始片段快照。
|
|
121
|
+
* @returns 恢复后的文本内容。
|
|
122
|
+
*/
|
|
123
|
+
function restoreManagedContent(content, managedState) {
|
|
124
|
+
let restored = content;
|
|
125
|
+
const providerRange = findMarkedBlockRange(restored, PROVIDER_BLOCK_START_MARKER, PROVIDER_BLOCK_END_MARKER);
|
|
126
|
+
if (providerRange) {
|
|
127
|
+
restored =
|
|
128
|
+
restored.slice(0, providerRange.start) +
|
|
129
|
+
(managedState.original_cslot_provider_block ?? "") +
|
|
130
|
+
restored.slice(providerRange.end);
|
|
131
|
+
}
|
|
132
|
+
const modelProviderRange = findMarkedBlockRange(restored, MODEL_PROVIDER_START_MARKER, MODEL_PROVIDER_END_MARKER);
|
|
133
|
+
if (modelProviderRange) {
|
|
134
|
+
restored =
|
|
135
|
+
restored.slice(0, modelProviderRange.start) +
|
|
136
|
+
(managedState.original_model_provider_block ?? "") +
|
|
137
|
+
restored.slice(modelProviderRange.end);
|
|
138
|
+
}
|
|
139
|
+
return restored;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* 查找首个 `model_provider` 配置块,兼容已启用与注释掉的场景。
|
|
143
|
+
*
|
|
144
|
+
* @param content 当前 `config.toml` 内容。
|
|
145
|
+
* @returns 命中时返回完整块及其偏移;未命中返回 `null`。
|
|
146
|
+
*/
|
|
147
|
+
function findModelProviderLine(content) {
|
|
148
|
+
const lines = content.split(/\r?\n/);
|
|
149
|
+
let offset = 0;
|
|
150
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
151
|
+
const line = lines[i];
|
|
152
|
+
const trimmed = line.trim();
|
|
153
|
+
const lineStart = offset;
|
|
154
|
+
const lineEnd = offset + line.length;
|
|
155
|
+
if (/^model_provider\s*=/.test(trimmed) ||
|
|
156
|
+
/^#\s*model_provider\s*=/.test(trimmed)) {
|
|
157
|
+
let blockEnd = lineEnd;
|
|
158
|
+
let nextOffset = lineEnd;
|
|
159
|
+
if (content.slice(lineEnd, lineEnd + 2) === "\r\n") {
|
|
160
|
+
blockEnd += 2;
|
|
161
|
+
nextOffset += 2;
|
|
162
|
+
}
|
|
163
|
+
else if (content.slice(lineEnd, lineEnd + 1) === "\n") {
|
|
164
|
+
blockEnd += 1;
|
|
165
|
+
nextOffset += 1;
|
|
166
|
+
}
|
|
167
|
+
for (let j = i + 1; j < lines.length; j += 1) {
|
|
168
|
+
const nextLine = lines[j];
|
|
169
|
+
const nextLineEnd = nextOffset + nextLine.length;
|
|
170
|
+
if (nextLine.trim() !== "") {
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
blockEnd = nextLineEnd;
|
|
174
|
+
if (content.slice(nextLineEnd, nextLineEnd + 2) === "\r\n") {
|
|
175
|
+
blockEnd += 2;
|
|
176
|
+
nextOffset = nextLineEnd + 2;
|
|
177
|
+
}
|
|
178
|
+
else if (content.slice(nextLineEnd, nextLineEnd + 1) === "\n") {
|
|
179
|
+
blockEnd += 1;
|
|
180
|
+
nextOffset = nextLineEnd + 1;
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
nextOffset = nextLineEnd;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
start: lineStart,
|
|
188
|
+
end: blockEnd,
|
|
189
|
+
value: content.slice(lineStart, blockEnd)
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
offset = lineEnd + (content.slice(lineEnd, lineEnd + 2) === "\r\n" ? 2 : 1);
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* 查找 `[model_providers.cslot]` 表块的文本范围。
|
|
198
|
+
*
|
|
199
|
+
* @param content 当前 `config.toml` 内容。
|
|
200
|
+
* @returns 命中时返回完整表块范围;未命中返回 `null`。
|
|
201
|
+
*/
|
|
202
|
+
function findProviderSectionRange(content) {
|
|
203
|
+
const lines = content.split(/\r?\n/);
|
|
204
|
+
let offset = 0;
|
|
205
|
+
let startLineIndex = -1;
|
|
206
|
+
let startOffset = -1;
|
|
207
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
208
|
+
const line = lines[i];
|
|
209
|
+
const lineEnd = offset + line.length;
|
|
210
|
+
if (line.trim() === "[model_providers.cslot]") {
|
|
211
|
+
startLineIndex = i;
|
|
212
|
+
startOffset = offset;
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
offset = lineEnd + (content.slice(lineEnd, lineEnd + 2) === "\r\n" ? 2 : 1);
|
|
216
|
+
}
|
|
217
|
+
if (startLineIndex < 0 || startOffset < 0) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
let endOffset = startOffset;
|
|
221
|
+
offset = startOffset;
|
|
222
|
+
for (let i = startLineIndex; i < lines.length; i += 1) {
|
|
223
|
+
const line = lines[i];
|
|
224
|
+
const lineEnd = offset + line.length;
|
|
225
|
+
const trimmed = line.trim();
|
|
226
|
+
if (i > startLineIndex && trimmed.startsWith("[") && !trimmed.startsWith("[[")) {
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
endOffset = lineEnd;
|
|
230
|
+
if (content.slice(lineEnd, lineEnd + 2) === "\r\n") {
|
|
231
|
+
endOffset += 2;
|
|
232
|
+
offset = lineEnd + 2;
|
|
233
|
+
}
|
|
234
|
+
else if (content.slice(lineEnd, lineEnd + 1) === "\n") {
|
|
235
|
+
endOffset += 1;
|
|
236
|
+
offset = lineEnd + 1;
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
offset = lineEnd + 1;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
start: startOffset,
|
|
244
|
+
end: endOffset,
|
|
245
|
+
value: content.slice(startOffset, endOffset)
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* 将指定文本规范为单个块插入形式,避免在块两侧不断叠加多余空行。
|
|
250
|
+
*
|
|
251
|
+
* @param before 插入点前的文本。
|
|
252
|
+
* @param block 待插入块。
|
|
253
|
+
* @param after 插入点后的文本。
|
|
254
|
+
* @param eol 目标换行符。
|
|
255
|
+
* @returns 插入后的完整文本。
|
|
256
|
+
*/
|
|
257
|
+
function insertBlockBetween(before, block, after, eol) {
|
|
258
|
+
const normalizedBefore = before.endsWith(eol) || before.length === 0 ? before : `${before}${eol}`;
|
|
259
|
+
const normalizedAfter = after.startsWith(eol) || after.length === 0 ? after : `${eol}${after}`;
|
|
260
|
+
return `${normalizedBefore}${block}${normalizedAfter}`;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* 将 cslot 需要的 provider 配置写入指定 `config.toml`,并保存恢复快照。
|
|
264
|
+
*
|
|
265
|
+
* @param targetPathOrDir 可选的 codex 配置目录或 `config.toml` 文件路径。
|
|
266
|
+
* @param options 可选控制项;`silent=true` 时不输出终端提示。
|
|
267
|
+
* @returns 实际写入的 `config.toml` 文件路径。
|
|
268
|
+
* @throws 当目标文件无法读取、写入或恢复快照保存失败时抛出异常。
|
|
269
|
+
*/
|
|
270
|
+
function applyManagedCodexConfig(targetPathOrDir, options) {
|
|
271
|
+
const rawTarget = targetPathOrDir ? (0, config_1.expandHome)(targetPathOrDir) : getDefaultCodexConfigPath();
|
|
272
|
+
const targetFile = rawTarget.endsWith(".toml") ? rawTarget : node_path_1.default.join(rawTarget, "config.toml");
|
|
273
|
+
const current = node_fs_1.default.existsSync(targetFile) ? node_fs_1.default.readFileSync(targetFile, "utf8") : "";
|
|
274
|
+
const previousManagedState = (0, state_1.getManagedCodexConfigState)();
|
|
275
|
+
const baseContent = previousManagedState && previousManagedState.target_file === targetFile
|
|
276
|
+
? restoreManagedContent(current, previousManagedState)
|
|
277
|
+
: current;
|
|
278
|
+
const eol = detectEol(baseContent);
|
|
279
|
+
const originalModelProviderLine = findModelProviderLine(baseContent);
|
|
280
|
+
const originalProviderSection = findProviderSectionRange(baseContent);
|
|
281
|
+
const snapshot = {
|
|
282
|
+
target_file: targetFile,
|
|
283
|
+
original_model_provider_block: originalModelProviderLine?.value ?? null,
|
|
284
|
+
original_cslot_provider_block: originalProviderSection?.value ?? null
|
|
285
|
+
};
|
|
286
|
+
let nextContent = baseContent;
|
|
287
|
+
const managedModelProviderBlock = buildManagedModelProviderBlock(eol);
|
|
288
|
+
const managedProviderBlock = buildManagedProviderBlock(eol);
|
|
289
|
+
// 先处理 provider 表块,再处理 model_provider 行,避免前面的插入导致后续偏移失效。
|
|
290
|
+
if (originalProviderSection) {
|
|
291
|
+
nextContent =
|
|
292
|
+
nextContent.slice(0, originalProviderSection.start) +
|
|
293
|
+
managedProviderBlock +
|
|
294
|
+
nextContent.slice(originalProviderSection.end);
|
|
295
|
+
}
|
|
296
|
+
else if (nextContent.length > 0) {
|
|
297
|
+
nextContent = insertBlockBetween(nextContent, managedProviderBlock, "", eol);
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
nextContent = `${managedProviderBlock}${eol}`;
|
|
301
|
+
}
|
|
302
|
+
const modelProviderLine = findModelProviderLine(nextContent);
|
|
303
|
+
if (modelProviderLine) {
|
|
304
|
+
nextContent =
|
|
305
|
+
nextContent.slice(0, modelProviderLine.start) +
|
|
306
|
+
managedModelProviderBlock +
|
|
307
|
+
nextContent.slice(modelProviderLine.end);
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
const firstNonWhitespaceMatch = nextContent.match(/\S/);
|
|
311
|
+
if (firstNonWhitespaceMatch && firstNonWhitespaceMatch.index !== undefined) {
|
|
312
|
+
nextContent = insertBlockBetween(nextContent.slice(0, firstNonWhitespaceMatch.index), managedModelProviderBlock, nextContent.slice(firstNonWhitespaceMatch.index), eol);
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
nextContent = `${managedModelProviderBlock}${eol}`;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (!nextContent.endsWith(eol)) {
|
|
319
|
+
nextContent = `${nextContent}${eol}`;
|
|
320
|
+
}
|
|
321
|
+
writeFileAtomic(targetFile, nextContent);
|
|
322
|
+
(0, state_1.setManagedCodexConfigState)(snapshot);
|
|
323
|
+
if (!options?.silent) {
|
|
324
|
+
const config = (0, config_1.loadConfig)();
|
|
325
|
+
console.log((0, text_1.bi)(`已写入: ${targetFile}`, `Written to: ${targetFile}`));
|
|
326
|
+
console.log(`base_url=http://${config.server.host}:${config.server.port}/v1`);
|
|
327
|
+
console.log(`api_key=${config.server.api_key}`);
|
|
328
|
+
console.log((0, text_1.bi)("提示: start 会自动接管 codex provider,stop 会精确恢复接管前内容。", "Note: start will manage the Codex provider automatically, and stop will restore the exact previous content."));
|
|
329
|
+
}
|
|
330
|
+
return targetFile;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* 解除 cslot 对 `config.toml` 的接管,并恢复接管前的原始片段。
|
|
334
|
+
*
|
|
335
|
+
* @returns 实际恢复的 `config.toml` 文件路径;若当前没有接管快照则返回 `null`。
|
|
336
|
+
* @throws 当目标文件读取或写入失败时抛出异常。
|
|
337
|
+
*/
|
|
338
|
+
function deactivateManagedCodexConfig() {
|
|
339
|
+
const managedState = (0, state_1.getManagedCodexConfigState)();
|
|
340
|
+
if (!managedState) {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
const targetFile = managedState.target_file;
|
|
344
|
+
if (!node_fs_1.default.existsSync(targetFile)) {
|
|
345
|
+
(0, state_1.clearManagedCodexConfigState)();
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
const current = node_fs_1.default.readFileSync(targetFile, "utf8");
|
|
349
|
+
const restored = restoreManagedContent(current, managedState);
|
|
350
|
+
writeFileAtomic(targetFile, restored);
|
|
351
|
+
(0, state_1.clearManagedCodexConfigState)();
|
|
352
|
+
console.log((0, text_1.bi)(`已恢复: ${targetFile}`, `Restored: ${targetFile}`));
|
|
353
|
+
return targetFile;
|
|
354
|
+
}
|
package/dist/config.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
6
|
+
exports.getCslotHome = getCslotHome;
|
|
7
7
|
exports.getConfigPath = getConfigPath;
|
|
8
8
|
exports.getPidPath = getPidPath;
|
|
9
9
|
exports.getServiceLogPath = getServiceLogPath;
|
|
@@ -13,6 +13,7 @@ exports.saveConfig = saveConfig;
|
|
|
13
13
|
exports.getManagedHome = getManagedHome;
|
|
14
14
|
exports.upsertAccount = upsertAccount;
|
|
15
15
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
16
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
16
17
|
const node_os_1 = __importDefault(require("node:os"));
|
|
17
18
|
const node_path_1 = __importDefault(require("node:path"));
|
|
18
19
|
const yaml_1 = __importDefault(require("yaml"));
|
|
@@ -53,18 +54,22 @@ const configSchema = zod_1.z.object({
|
|
|
53
54
|
}),
|
|
54
55
|
accounts: zod_1.z.array(managedAccountSchema).default([])
|
|
55
56
|
});
|
|
57
|
+
/**
|
|
58
|
+
* 生成默认的本地 API Key,用于首次初始化配置时避免使用固定常量。
|
|
59
|
+
*
|
|
60
|
+
* @returns 随机生成的本地 API Key。
|
|
61
|
+
*/
|
|
62
|
+
function generateDefaultLocalApiKey() {
|
|
63
|
+
return `cslot-${node_crypto_1.default.randomBytes(18).toString("hex")}`;
|
|
64
|
+
}
|
|
56
65
|
/**
|
|
57
66
|
* 返回 cslot 的根目录,并确保基础目录结构存在。
|
|
58
67
|
*
|
|
59
68
|
* @returns cslot 根目录绝对路径。
|
|
60
69
|
* @throws 当目录无法创建时抛出文件系统错误。
|
|
61
70
|
*/
|
|
62
|
-
function
|
|
71
|
+
function getCslotHome() {
|
|
63
72
|
const home = node_path_1.default.join(node_os_1.default.homedir(), ".cslot");
|
|
64
|
-
const legacyHome = node_path_1.default.join(node_os_1.default.homedir(), ".codexsw");
|
|
65
|
-
if (!node_fs_1.default.existsSync(home) && node_fs_1.default.existsSync(legacyHome)) {
|
|
66
|
-
node_fs_1.default.cpSync(legacyHome, home, { recursive: true });
|
|
67
|
-
}
|
|
68
73
|
// 先创建 cslot 根目录,后续命令统一基于该目录读写状态。
|
|
69
74
|
node_fs_1.default.mkdirSync(home, { recursive: true });
|
|
70
75
|
node_fs_1.default.mkdirSync(node_path_1.default.join(home, "homes"), { recursive: true });
|
|
@@ -77,7 +82,7 @@ function getCodexSwHome() {
|
|
|
77
82
|
* @returns 配置文件绝对路径。
|
|
78
83
|
*/
|
|
79
84
|
function getConfigPath() {
|
|
80
|
-
return node_path_1.default.join(
|
|
85
|
+
return node_path_1.default.join(getCslotHome(), "config.yaml");
|
|
81
86
|
}
|
|
82
87
|
/**
|
|
83
88
|
* 返回后台服务 PID 文件路径。
|
|
@@ -85,7 +90,7 @@ function getConfigPath() {
|
|
|
85
90
|
* @returns PID 文件绝对路径。
|
|
86
91
|
*/
|
|
87
92
|
function getPidPath() {
|
|
88
|
-
return node_path_1.default.join(
|
|
93
|
+
return node_path_1.default.join(getCslotHome(), "cslot.pid");
|
|
89
94
|
}
|
|
90
95
|
/**
|
|
91
96
|
* 返回后台服务日志文件路径。
|
|
@@ -93,7 +98,7 @@ function getPidPath() {
|
|
|
93
98
|
* @returns 日志文件绝对路径。
|
|
94
99
|
*/
|
|
95
100
|
function getServiceLogPath() {
|
|
96
|
-
return node_path_1.default.join(
|
|
101
|
+
return node_path_1.default.join(getCslotHome(), "logs", "service.log");
|
|
97
102
|
}
|
|
98
103
|
/**
|
|
99
104
|
* 将路径中的 `~` 展开为当前用户家目录。
|
|
@@ -118,14 +123,14 @@ function expandHome(input) {
|
|
|
118
123
|
*/
|
|
119
124
|
function loadConfig() {
|
|
120
125
|
const configPath = getConfigPath();
|
|
121
|
-
const legacyConfigPath = node_path_1.default.join(node_os_1.default.homedir(), ".codexsw", "config.yaml");
|
|
122
126
|
if (!node_fs_1.default.existsSync(configPath)) {
|
|
127
|
+
const defaultApiKey = generateDefaultLocalApiKey();
|
|
123
128
|
const defaultConfig = {
|
|
124
129
|
version: 1,
|
|
125
130
|
server: {
|
|
126
131
|
host: "127.0.0.1",
|
|
127
132
|
port: 4389,
|
|
128
|
-
api_key:
|
|
133
|
+
api_key: defaultApiKey,
|
|
129
134
|
body_limit_mb: 512
|
|
130
135
|
},
|
|
131
136
|
upstream: {
|
|
@@ -142,19 +147,13 @@ function loadConfig() {
|
|
|
142
147
|
const parsed = raw.trim() ? yaml_1.default.parse(raw) : {};
|
|
143
148
|
const normalized = configSchema.parse(parsed);
|
|
144
149
|
let changed = JSON.stringify(parsed) !== JSON.stringify(normalized);
|
|
145
|
-
if (
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const legacyConfig = configSchema.parse(legacyParsed);
|
|
150
|
-
if (legacyConfig.accounts.length > 0) {
|
|
151
|
-
normalized.accounts = legacyConfig.accounts;
|
|
152
|
-
changed = true;
|
|
153
|
-
}
|
|
150
|
+
if ((!parsed || typeof parsed !== "object" || !("server" in parsed)) ||
|
|
151
|
+
!(parsed.server && typeof parsed.server === "object" && "api_key" in parsed.server)) {
|
|
152
|
+
normalized.server.api_key = generateDefaultLocalApiKey();
|
|
153
|
+
changed = true;
|
|
154
154
|
}
|
|
155
155
|
// 兼容历史默认值,统一迁移到新的简短本地 key。
|
|
156
|
-
if (normalized.server.api_key === "local-only-key"
|
|
157
|
-
normalized.server.api_key === "codexsw-defaultkey") {
|
|
156
|
+
if (normalized.server.api_key === "local-only-key") {
|
|
158
157
|
normalized.server.api_key = "cslot-defaultkey";
|
|
159
158
|
changed = true;
|
|
160
159
|
}
|
|
@@ -183,7 +182,7 @@ function saveConfig(config) {
|
|
|
183
182
|
* @returns 该账号对应的 HOME 目录绝对路径。
|
|
184
183
|
*/
|
|
185
184
|
function getManagedHome(accountId) {
|
|
186
|
-
return node_path_1.default.join(
|
|
185
|
+
return node_path_1.default.join(getCslotHome(), "homes", accountId);
|
|
187
186
|
}
|
|
188
187
|
/**
|
|
189
188
|
* 将账号追加到配置中;若已存在相同 id 则覆盖更新。
|
package/dist/login.js
CHANGED
|
@@ -8,6 +8,7 @@ const node_child_process_1 = require("node:child_process");
|
|
|
8
8
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
9
|
const account_store_1 = require("./account-store");
|
|
10
10
|
const config_1 = require("./config");
|
|
11
|
+
const text_1 = require("./text");
|
|
11
12
|
/**
|
|
12
13
|
* 使用独立 HOME 目录拉起官方 `codex login`,完成单账号录入。
|
|
13
14
|
*
|
|
@@ -29,14 +30,14 @@ async function loginManagedAccount(accountId) {
|
|
|
29
30
|
child.on("exit", (code) => {
|
|
30
31
|
if (code === 0) {
|
|
31
32
|
if (!(0, account_store_1.hasCompleteCodexAuthState)(managedHome)) {
|
|
32
|
-
reject(new Error("codex login 已退出,但未检测到完整登录态,请重新登录"));
|
|
33
|
+
reject(new Error((0, text_1.bi)("codex login 已退出,但未检测到完整登录态,请重新登录", "codex login exited, but no complete auth state was detected. Please log in again.")));
|
|
33
34
|
return;
|
|
34
35
|
}
|
|
35
36
|
(0, account_store_1.registerManagedAccount)(accountId, managedHome);
|
|
36
37
|
resolve(managedHome);
|
|
37
38
|
return;
|
|
38
39
|
}
|
|
39
|
-
reject(new Error(`codex login 失败,退出码: ${code ?? "unknown"}`));
|
|
40
|
+
reject(new Error((0, text_1.bi)(`codex login 失败,退出码: ${code ?? "unknown"}`, `codex login failed with exit code: ${code ?? "unknown"}`)));
|
|
40
41
|
});
|
|
41
42
|
child.on("error", (error) => {
|
|
42
43
|
reject(error);
|
package/dist/serve.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
const config_1 = require("./config");
|
|
5
5
|
const server_1 = require("./server");
|
|
6
|
+
const text_1 = require("./text");
|
|
6
7
|
/**
|
|
7
8
|
* 后台服务进程入口。
|
|
8
9
|
*
|
|
@@ -19,6 +20,6 @@ async function main() {
|
|
|
19
20
|
}
|
|
20
21
|
void main().catch((error) => {
|
|
21
22
|
const message = error instanceof Error ? error.message : String(error);
|
|
22
|
-
console.error(`cslot service 启动失败: ${message}`);
|
|
23
|
+
console.error((0, text_1.bi)(`cslot service 启动失败: ${message}`, `cslot service failed to start: ${message}`));
|
|
23
24
|
process.exit(1);
|
|
24
25
|
});
|
package/dist/server.js
CHANGED
|
@@ -11,6 +11,7 @@ const config_1 = require("./config");
|
|
|
11
11
|
const status_1 = require("./status");
|
|
12
12
|
const scheduler_1 = require("./scheduler");
|
|
13
13
|
const state_1 = require("./state");
|
|
14
|
+
const text_1 = require("./text");
|
|
14
15
|
const usage_sync_1 = require("./usage-sync");
|
|
15
16
|
function getBearerToken(headerValue) {
|
|
16
17
|
if (!headerValue) {
|
|
@@ -117,7 +118,7 @@ function buildNetworkUnavailablePayload(accountId, error) {
|
|
|
117
118
|
const message = error instanceof Error ? error.message : String(error);
|
|
118
119
|
return {
|
|
119
120
|
error: {
|
|
120
|
-
message: `网络不可用,账号 ${accountId} 无法连接上游: ${message}`,
|
|
121
|
+
message: (0, text_1.bi)(`网络不可用,账号 ${accountId} 无法连接上游: ${message}`, `Network unavailable. Account ${accountId} cannot reach upstream: ${message}`),
|
|
121
122
|
type: "network_unavailable"
|
|
122
123
|
}
|
|
123
124
|
};
|
|
@@ -199,7 +200,7 @@ async function startServer(port) {
|
|
|
199
200
|
const bearer = getBearerToken(request.headers.authorization);
|
|
200
201
|
if (bearer !== config.server.api_key) {
|
|
201
202
|
reply.code(401);
|
|
202
|
-
throw new Error("
|
|
203
|
+
throw new Error((0, text_1.bi)("本地 API Key 无效", "Invalid local API key"));
|
|
203
204
|
}
|
|
204
205
|
});
|
|
205
206
|
app.get("/health", async () => {
|
|
@@ -222,7 +223,7 @@ async function startServer(port) {
|
|
|
222
223
|
reply.code(503);
|
|
223
224
|
reply.send({
|
|
224
225
|
error: {
|
|
225
|
-
message: "当前没有可用账号",
|
|
226
|
+
message: (0, text_1.bi)("当前没有可用账号", "No available account"),
|
|
226
227
|
type: "no_available_account"
|
|
227
228
|
}
|
|
228
229
|
});
|
|
@@ -230,7 +231,7 @@ async function startServer(port) {
|
|
|
230
231
|
}
|
|
231
232
|
let lastErrorPayload = {
|
|
232
233
|
error: {
|
|
233
|
-
message: "所有账号都请求失败",
|
|
234
|
+
message: (0, text_1.bi)("所有账号都请求失败", "All accounts failed"),
|
|
234
235
|
type: "all_accounts_failed"
|
|
235
236
|
}
|
|
236
237
|
};
|
|
@@ -244,7 +245,7 @@ async function startServer(port) {
|
|
|
244
245
|
markAccountFailure(picked.account.id, "invalid_account_auth", 10 * 60);
|
|
245
246
|
lastErrorPayload = {
|
|
246
247
|
error: {
|
|
247
|
-
message: `账号 ${picked.account.id} 缺少 access_token`,
|
|
248
|
+
message: (0, text_1.bi)(`账号 ${picked.account.id} 缺少 access_token`, `Account ${picked.account.id} is missing access_token`),
|
|
248
249
|
type: "invalid_account_auth"
|
|
249
250
|
}
|
|
250
251
|
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handleStart = handleStart;
|
|
4
|
+
exports.handleStop = handleStop;
|
|
5
|
+
const service_lifecycle_service_1 = require("./app/service-lifecycle-service");
|
|
6
|
+
const config_1 = require("./config");
|
|
7
|
+
const text_1 = require("./text");
|
|
8
|
+
/**
|
|
9
|
+
* 后台启动 cslot 服务并写入 PID 文件。
|
|
10
|
+
*
|
|
11
|
+
* @param portOverride 可选的端口文本;传入时会先校验并落盘到本地配置。
|
|
12
|
+
* @returns Promise,无返回值。
|
|
13
|
+
* @throws 当服务已在运行、端口非法或子进程启动失败时抛出异常。
|
|
14
|
+
*/
|
|
15
|
+
async function handleStart(portOverride) {
|
|
16
|
+
const config = (0, config_1.loadConfig)();
|
|
17
|
+
const result = (0, service_lifecycle_service_1.startManagedService)(portOverride);
|
|
18
|
+
if (result.alreadyRunning) {
|
|
19
|
+
console.log((0, text_1.bi)(`服务已在运行,PID=${result.pid}`, `Service is already running. PID=${result.pid}`));
|
|
20
|
+
if (portOverride) {
|
|
21
|
+
console.log((0, text_1.bi)(`已将新端口写入配置: ${result.port}`, `The new port has been saved to config: ${result.port}`));
|
|
22
|
+
console.log((0, text_1.bi)("请先执行 cslot stop,再执行 cslot start 使新端口生效。", "Run cslot stop first, then cslot start to apply the new port."));
|
|
23
|
+
}
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
console.log((0, text_1.bi)(`服务已启动: http://${config.server.host}:${result.port}`, `Service started: http://${config.server.host}:${result.port}`));
|
|
27
|
+
console.log(`PID: ${result.pid}`);
|
|
28
|
+
console.log((0, text_1.bi)(`日志: ${result.logPath}`, `Log: ${result.logPath}`));
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 停止后台运行的 cslot 服务,并恢复被接管的 Codex 配置。
|
|
32
|
+
*
|
|
33
|
+
* @returns 无返回值。
|
|
34
|
+
* @throws 当进程终止失败时透传底层异常。
|
|
35
|
+
*/
|
|
36
|
+
function handleStop() {
|
|
37
|
+
const result = (0, service_lifecycle_service_1.stopManagedService)();
|
|
38
|
+
if (!result.stoppedPid) {
|
|
39
|
+
console.log((0, text_1.bi)("服务未运行", "Service is not running."));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
console.log((0, text_1.bi)(`服务已停止,PID=${result.stoppedPid}`, `Service stopped. PID=${result.stoppedPid}`));
|
|
43
|
+
}
|