codex-slot 0.1.8 → 0.1.16
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/dist/codex-config.js +288 -65
- package/dist/state.js +41 -0
- package/dist/status-command.js +82 -2
- package/dist/status.js +192 -49
- package/dist/usage-sync.js +28 -2
- package/package.json +1 -1
package/dist/codex-config.js
CHANGED
|
@@ -103,29 +103,22 @@ function findMarkedBlockRange(content, startMarker, endMarker) {
|
|
|
103
103
|
return { start, end };
|
|
104
104
|
}
|
|
105
105
|
/**
|
|
106
|
-
*
|
|
106
|
+
* 反复移除文本中所有带指定标记的受管块,避免异常退出后残留旧块导致后续写入出现重复或串位。
|
|
107
107
|
*
|
|
108
108
|
* @param content 当前 `config.toml` 内容。
|
|
109
|
-
* @param
|
|
110
|
-
* @
|
|
109
|
+
* @param startMarker 块起始标记。
|
|
110
|
+
* @param endMarker 块结束标记。
|
|
111
|
+
* @returns 清理后的文本内容。
|
|
111
112
|
*/
|
|
112
|
-
function
|
|
113
|
-
let
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
const modelProviderRange = findMarkedBlockRange(restored, MODEL_PROVIDER_START_MARKER, MODEL_PROVIDER_END_MARKER);
|
|
122
|
-
if (modelProviderRange) {
|
|
123
|
-
restored =
|
|
124
|
-
restored.slice(0, modelProviderRange.start) +
|
|
125
|
-
(managedState.original_model_provider_block ?? "") +
|
|
126
|
-
restored.slice(modelProviderRange.end);
|
|
113
|
+
function stripMarkedBlocks(content, startMarker, endMarker) {
|
|
114
|
+
let stripped = content;
|
|
115
|
+
while (true) {
|
|
116
|
+
const range = findMarkedBlockRange(stripped, startMarker, endMarker);
|
|
117
|
+
if (!range) {
|
|
118
|
+
return stripped;
|
|
119
|
+
}
|
|
120
|
+
stripped = stripped.slice(0, range.start) + stripped.slice(range.end);
|
|
127
121
|
}
|
|
128
|
-
return restored;
|
|
129
122
|
}
|
|
130
123
|
/**
|
|
131
124
|
* 查找首个 `model_provider` 配置块,兼容已启用与注释掉的场景。
|
|
@@ -183,12 +176,13 @@ function findModelProviderLine(content) {
|
|
|
183
176
|
return null;
|
|
184
177
|
}
|
|
185
178
|
/**
|
|
186
|
-
*
|
|
179
|
+
* 查找指定表块的文本范围。
|
|
187
180
|
*
|
|
188
181
|
* @param content 当前 `config.toml` 内容。
|
|
182
|
+
* @param header 目标表头,例如 `[model_providers.cslot]`。
|
|
189
183
|
* @returns 命中时返回完整表块范围;未命中返回 `null`。
|
|
190
184
|
*/
|
|
191
|
-
function
|
|
185
|
+
function findTableSectionRange(content, header) {
|
|
192
186
|
const lines = content.split(/\r?\n/);
|
|
193
187
|
let offset = 0;
|
|
194
188
|
let startLineIndex = -1;
|
|
@@ -196,7 +190,7 @@ function findProviderSectionRange(content) {
|
|
|
196
190
|
for (let i = 0; i < lines.length; i += 1) {
|
|
197
191
|
const line = lines[i];
|
|
198
192
|
const lineEnd = offset + line.length;
|
|
199
|
-
if (line.trim() ===
|
|
193
|
+
if (line.trim() === header) {
|
|
200
194
|
startLineIndex = i;
|
|
201
195
|
startOffset = offset;
|
|
202
196
|
break;
|
|
@@ -234,6 +228,164 @@ function findProviderSectionRange(content) {
|
|
|
234
228
|
value: content.slice(startOffset, endOffset)
|
|
235
229
|
};
|
|
236
230
|
}
|
|
231
|
+
/**
|
|
232
|
+
* 查找 `[model_providers.cslot]` 表块的文本范围。
|
|
233
|
+
*
|
|
234
|
+
* @param content 当前 `config.toml` 内容。
|
|
235
|
+
* @returns 命中时返回完整表块范围;未命中返回 `null`。
|
|
236
|
+
*/
|
|
237
|
+
function findProviderSectionRange(content) {
|
|
238
|
+
return findTableSectionRange(content, "[model_providers.cslot]");
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* 查找指定表头所在的行起始偏移。
|
|
242
|
+
*
|
|
243
|
+
* @param content 当前 `config.toml` 内容。
|
|
244
|
+
* @param header 目标表头。
|
|
245
|
+
* @returns 命中时返回表头行起始偏移;未命中返回 `null`。
|
|
246
|
+
*/
|
|
247
|
+
function findTableHeaderOffset(content, header) {
|
|
248
|
+
const lines = content.split(/\r?\n/);
|
|
249
|
+
let offset = 0;
|
|
250
|
+
for (const line of lines) {
|
|
251
|
+
const lineEnd = offset + line.length;
|
|
252
|
+
if (line.trim() === header) {
|
|
253
|
+
return offset;
|
|
254
|
+
}
|
|
255
|
+
offset = lineEnd + (content.slice(lineEnd, lineEnd + 2) === "\r\n" ? 2 : 1);
|
|
256
|
+
}
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* 查找指定偏移之前最近的表头,供恢复原有表块位置时作为后备锚点。
|
|
261
|
+
*
|
|
262
|
+
* @param content 当前 `config.toml` 内容。
|
|
263
|
+
* @param offset 截止偏移。
|
|
264
|
+
* @returns 最近的表头文本;未命中返回 `null`。
|
|
265
|
+
*/
|
|
266
|
+
function findPreviousTableHeaderBeforeOffset(content, offset) {
|
|
267
|
+
const lines = content.split(/\r?\n/);
|
|
268
|
+
let currentOffset = 0;
|
|
269
|
+
let previousHeader = null;
|
|
270
|
+
for (const line of lines) {
|
|
271
|
+
const lineEnd = currentOffset + line.length;
|
|
272
|
+
const trimmed = line.trim();
|
|
273
|
+
if (currentOffset >= offset) {
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
if (trimmed.startsWith("[") && !trimmed.startsWith("[[") && !trimmed.startsWith("#")) {
|
|
277
|
+
previousHeader = trimmed;
|
|
278
|
+
}
|
|
279
|
+
currentOffset = lineEnd + (content.slice(lineEnd, lineEnd + 2) === "\r\n" ? 2 : 1);
|
|
280
|
+
}
|
|
281
|
+
return previousHeader;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* 查找指定偏移之后的首个表头,供恢复原有表块位置时作为优先锚点。
|
|
285
|
+
*
|
|
286
|
+
* @param content 当前 `config.toml` 内容。
|
|
287
|
+
* @param offset 起始偏移。
|
|
288
|
+
* @returns 首个后续表头文本;未命中返回 `null`。
|
|
289
|
+
*/
|
|
290
|
+
function findNextTableHeaderAfterOffset(content, offset) {
|
|
291
|
+
const lines = content.split(/\r?\n/);
|
|
292
|
+
let currentOffset = 0;
|
|
293
|
+
for (const line of lines) {
|
|
294
|
+
const lineEnd = currentOffset + line.length;
|
|
295
|
+
const trimmed = line.trim();
|
|
296
|
+
if (currentOffset >= offset &&
|
|
297
|
+
trimmed.startsWith("[") &&
|
|
298
|
+
!trimmed.startsWith("[[") &&
|
|
299
|
+
!trimmed.startsWith("#")) {
|
|
300
|
+
return trimmed;
|
|
301
|
+
}
|
|
302
|
+
currentOffset = lineEnd + (content.slice(lineEnd, lineEnd + 2) === "\r\n" ? 2 : 1);
|
|
303
|
+
}
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* 清理文本中的所有 `model_provider` 配置块,确保每次接管都以单一稳定块重新写入。
|
|
308
|
+
*
|
|
309
|
+
* @param content 当前 `config.toml` 内容。
|
|
310
|
+
* @returns 移除后的文本内容。
|
|
311
|
+
*/
|
|
312
|
+
function removeAllModelProviderLines(content) {
|
|
313
|
+
let nextContent = content;
|
|
314
|
+
while (true) {
|
|
315
|
+
const range = findModelProviderLine(nextContent);
|
|
316
|
+
if (!range) {
|
|
317
|
+
return nextContent;
|
|
318
|
+
}
|
|
319
|
+
nextContent = nextContent.slice(0, range.start) + nextContent.slice(range.end);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* 清理文本中的所有 `[model_providers.cslot]` 表块,避免残留旧块影响下一段配置。
|
|
324
|
+
*
|
|
325
|
+
* @param content 当前 `config.toml` 内容。
|
|
326
|
+
* @returns 移除后的文本内容。
|
|
327
|
+
*/
|
|
328
|
+
function removeAllProviderSections(content) {
|
|
329
|
+
let nextContent = content;
|
|
330
|
+
while (true) {
|
|
331
|
+
const range = findProviderSectionRange(nextContent);
|
|
332
|
+
if (!range) {
|
|
333
|
+
return nextContent;
|
|
334
|
+
}
|
|
335
|
+
nextContent = nextContent.slice(0, range.start) + nextContent.slice(range.end);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* 移除所有 cslot 受管标记块,得到不包含历史残留接管片段的基线内容。
|
|
340
|
+
*
|
|
341
|
+
* @param content 当前 `config.toml` 内容。
|
|
342
|
+
* @returns 清理后的基线内容。
|
|
343
|
+
*/
|
|
344
|
+
function stripAllManagedBlocks(content) {
|
|
345
|
+
const withoutProviderBlock = stripMarkedBlocks(content, PROVIDER_BLOCK_START_MARKER, PROVIDER_BLOCK_END_MARKER);
|
|
346
|
+
return stripMarkedBlocks(withoutProviderBlock, MODEL_PROVIDER_START_MARKER, MODEL_PROVIDER_END_MARKER);
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* 查找根级配置区的尾部插入点。
|
|
350
|
+
*
|
|
351
|
+
* 业务规则:
|
|
352
|
+
* 1. 若文件中存在 table header,则插入到首个 table 之前,保证 `model_provider` 仍处于根级作用域。
|
|
353
|
+
* 2. 若文件不存在任何 table,则允许直接追加到文件尾部。
|
|
354
|
+
*
|
|
355
|
+
* @param content 当前 `config.toml` 内容。
|
|
356
|
+
* @returns 可用于插入根级配置块的偏移位置。
|
|
357
|
+
*/
|
|
358
|
+
function findRootSectionInsertOffset(content) {
|
|
359
|
+
const lines = content.split(/\r?\n/);
|
|
360
|
+
let offset = 0;
|
|
361
|
+
for (const line of lines) {
|
|
362
|
+
const lineEnd = offset + line.length;
|
|
363
|
+
const trimmed = line.trim();
|
|
364
|
+
if (trimmed.startsWith("[") && !trimmed.startsWith("#")) {
|
|
365
|
+
return offset;
|
|
366
|
+
}
|
|
367
|
+
offset = lineEnd + (content.slice(lineEnd, lineEnd + 2) === "\r\n" ? 2 : 1);
|
|
368
|
+
}
|
|
369
|
+
return content.length;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* 将根级配置块插回根级区域。
|
|
373
|
+
*
|
|
374
|
+
* 若能命中原始记录的后续表头,则优先插回该表头前;否则回退到首个表头前。
|
|
375
|
+
*
|
|
376
|
+
* @param content 当前 `config.toml` 内容。
|
|
377
|
+
* @param block 待插入的根级配置块。
|
|
378
|
+
* @param eol 目标换行符。
|
|
379
|
+
* @param preferredNextTableHeader 原始记录的后续表头锚点。
|
|
380
|
+
* @returns 插入后的完整文本。
|
|
381
|
+
*/
|
|
382
|
+
function insertRootBlock(content, block, eol, preferredNextTableHeader) {
|
|
383
|
+
const preferredOffset = preferredNextTableHeader
|
|
384
|
+
? findTableHeaderOffset(content, preferredNextTableHeader)
|
|
385
|
+
: null;
|
|
386
|
+
const insertOffset = preferredOffset ?? findRootSectionInsertOffset(content);
|
|
387
|
+
return insertBlockBetween(content.slice(0, insertOffset), block, content.slice(insertOffset), eol);
|
|
388
|
+
}
|
|
237
389
|
/**
|
|
238
390
|
* 将指定文本规范为单个块插入形式,避免在块两侧不断叠加多余空行。
|
|
239
391
|
*
|
|
@@ -248,6 +400,103 @@ function insertBlockBetween(before, block, after, eol) {
|
|
|
248
400
|
const normalizedAfter = after.startsWith(eol) || after.length === 0 ? after : `${eol}${after}`;
|
|
249
401
|
return `${normalizedBefore}${block}${normalizedAfter}`;
|
|
250
402
|
}
|
|
403
|
+
/**
|
|
404
|
+
* 将配置块稳定追加到文件尾部,统一清理尾部多余空行,避免多次接管后空行不断累积。
|
|
405
|
+
*
|
|
406
|
+
* @param content 当前 `config.toml` 内容。
|
|
407
|
+
* @param block 待追加配置块。
|
|
408
|
+
* @param eol 目标换行符。
|
|
409
|
+
* @returns 追加后的完整文本。
|
|
410
|
+
*/
|
|
411
|
+
function appendBlockToEnd(content, block, eol) {
|
|
412
|
+
let trimmed = content;
|
|
413
|
+
while (trimmed.endsWith(eol)) {
|
|
414
|
+
trimmed = trimmed.slice(0, -eol.length);
|
|
415
|
+
}
|
|
416
|
+
if (trimmed.length === 0) {
|
|
417
|
+
return `${block}${eol}`;
|
|
418
|
+
}
|
|
419
|
+
return `${trimmed}${eol}${eol}${block}${eol}`;
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* 将表块尽量插回原有相邻表头附近;若锚点已不存在,则退回文件尾部追加。
|
|
423
|
+
*
|
|
424
|
+
* @param content 当前 `config.toml` 内容。
|
|
425
|
+
* @param block 待插入的表块。
|
|
426
|
+
* @param eol 目标换行符。
|
|
427
|
+
* @param preferredNextTableHeader 原始后续表头锚点,命中时优先插到该表之前。
|
|
428
|
+
* @param preferredPreviousTableHeader 原始前驱表头锚点,当前者失效时插到该表之后。
|
|
429
|
+
* @returns 插入后的完整文本。
|
|
430
|
+
*/
|
|
431
|
+
function insertTableBlock(content, block, eol, preferredNextTableHeader, preferredPreviousTableHeader) {
|
|
432
|
+
if (preferredNextTableHeader) {
|
|
433
|
+
const nextOffset = findTableHeaderOffset(content, preferredNextTableHeader);
|
|
434
|
+
if (nextOffset !== null) {
|
|
435
|
+
return insertBlockBetween(content.slice(0, nextOffset), block, content.slice(nextOffset), eol);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
if (preferredPreviousTableHeader) {
|
|
439
|
+
const previousRange = findTableSectionRange(content, preferredPreviousTableHeader);
|
|
440
|
+
if (previousRange) {
|
|
441
|
+
return insertBlockBetween(content.slice(0, previousRange.end), block, content.slice(previousRange.end), eol);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return appendBlockToEnd(content, block, eol);
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* 解析当前目标文件对应的上一轮接管快照。
|
|
448
|
+
*
|
|
449
|
+
* @param targetFile 当前准备接管或恢复的 `config.toml` 路径。
|
|
450
|
+
* @returns 命中同一目标文件时返回上一轮快照;否则返回 `null`。
|
|
451
|
+
*/
|
|
452
|
+
function resolveManagedStateForTarget(targetFile) {
|
|
453
|
+
const managedState = (0, state_1.getManagedCodexConfigState)();
|
|
454
|
+
if (!managedState || managedState.target_file !== targetFile) {
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
return managedState;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* 基于当前未受管的配置文本与上一轮快照,生成本轮接管所需的最小恢复快照。
|
|
461
|
+
*
|
|
462
|
+
* 业务规则:
|
|
463
|
+
* 1. 优先记录当前文件里实际存在的原始 `model_provider` 与 `[model_providers.cslot]`。
|
|
464
|
+
* 2. 若当前文件只剩残留受管块,允许继承上一轮快照中的原始片段。
|
|
465
|
+
* 3. 仅保存 cslot 自己声明所有权的两块配置及其锚点,不保存整文件内容。
|
|
466
|
+
*
|
|
467
|
+
* @param targetFile 当前准备接管的 `config.toml` 路径。
|
|
468
|
+
* @param strippedCurrent 已移除受管标记块后的配置文本。
|
|
469
|
+
* @param previousManagedState 同一目标文件的上一轮快照;不存在时传 `null`。
|
|
470
|
+
* @returns 本轮接管后用于 stop 恢复的快照。
|
|
471
|
+
*/
|
|
472
|
+
function buildManagedSnapshot(targetFile, strippedCurrent, previousManagedState) {
|
|
473
|
+
const originalModelProviderLine = findModelProviderLine(strippedCurrent);
|
|
474
|
+
const originalProviderSection = findProviderSectionRange(strippedCurrent);
|
|
475
|
+
return {
|
|
476
|
+
target_file: targetFile,
|
|
477
|
+
original_model_provider_block: originalModelProviderLine?.value ??
|
|
478
|
+
previousManagedState?.original_model_provider_block ??
|
|
479
|
+
null,
|
|
480
|
+
original_model_provider_next_table_header: (originalModelProviderLine
|
|
481
|
+
? findNextTableHeaderAfterOffset(strippedCurrent, originalModelProviderLine.end)
|
|
482
|
+
: null) ??
|
|
483
|
+
previousManagedState?.original_model_provider_next_table_header ??
|
|
484
|
+
null,
|
|
485
|
+
original_cslot_provider_block: originalProviderSection?.value ??
|
|
486
|
+
previousManagedState?.original_cslot_provider_block ??
|
|
487
|
+
null,
|
|
488
|
+
original_cslot_provider_previous_table_header: (originalProviderSection
|
|
489
|
+
? findPreviousTableHeaderBeforeOffset(strippedCurrent, originalProviderSection.start)
|
|
490
|
+
: null) ??
|
|
491
|
+
previousManagedState?.original_cslot_provider_previous_table_header ??
|
|
492
|
+
null,
|
|
493
|
+
original_cslot_provider_next_table_header: (originalProviderSection
|
|
494
|
+
? findNextTableHeaderAfterOffset(strippedCurrent, originalProviderSection.end)
|
|
495
|
+
: null) ??
|
|
496
|
+
previousManagedState?.original_cslot_provider_next_table_header ??
|
|
497
|
+
null
|
|
498
|
+
};
|
|
499
|
+
}
|
|
251
500
|
/**
|
|
252
501
|
* 将 cslot 需要的 provider 配置写入指定 `config.toml`,并保存恢复快照。
|
|
253
502
|
*
|
|
@@ -260,51 +509,16 @@ function applyManagedCodexConfig(targetPathOrDir, options) {
|
|
|
260
509
|
const rawTarget = targetPathOrDir ? (0, config_1.expandHome)(targetPathOrDir) : getDefaultCodexConfigPath();
|
|
261
510
|
const targetFile = rawTarget.endsWith(".toml") ? rawTarget : node_path_1.default.join(rawTarget, "config.toml");
|
|
262
511
|
const current = node_fs_1.default.existsSync(targetFile) ? node_fs_1.default.readFileSync(targetFile, "utf8") : "";
|
|
263
|
-
const previousManagedState = (
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const eol = detectEol(baseContent);
|
|
268
|
-
const originalModelProviderLine = findModelProviderLine(baseContent);
|
|
269
|
-
const originalProviderSection = findProviderSectionRange(baseContent);
|
|
270
|
-
const snapshot = {
|
|
271
|
-
target_file: targetFile,
|
|
272
|
-
original_model_provider_block: originalModelProviderLine?.value ?? null,
|
|
273
|
-
original_cslot_provider_block: originalProviderSection?.value ?? null
|
|
274
|
-
};
|
|
512
|
+
const previousManagedState = resolveManagedStateForTarget(targetFile);
|
|
513
|
+
const strippedCurrent = stripAllManagedBlocks(current);
|
|
514
|
+
const eol = detectEol(strippedCurrent);
|
|
515
|
+
const snapshot = buildManagedSnapshot(targetFile, strippedCurrent, previousManagedState);
|
|
275
516
|
const config = options?.config ?? (0, config_1.loadConfig)();
|
|
276
|
-
let nextContent = baseContent;
|
|
277
517
|
const managedModelProviderBlock = buildManagedModelProviderBlock(eol);
|
|
278
518
|
const managedProviderBlock = buildManagedProviderBlock(eol, config);
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
nextContent.slice(0, originalProviderSection.start) +
|
|
283
|
-
managedProviderBlock +
|
|
284
|
-
nextContent.slice(originalProviderSection.end);
|
|
285
|
-
}
|
|
286
|
-
else if (nextContent.length > 0) {
|
|
287
|
-
nextContent = insertBlockBetween(nextContent, managedProviderBlock, "", eol);
|
|
288
|
-
}
|
|
289
|
-
else {
|
|
290
|
-
nextContent = `${managedProviderBlock}${eol}`;
|
|
291
|
-
}
|
|
292
|
-
const modelProviderLine = findModelProviderLine(nextContent);
|
|
293
|
-
if (modelProviderLine) {
|
|
294
|
-
nextContent =
|
|
295
|
-
nextContent.slice(0, modelProviderLine.start) +
|
|
296
|
-
managedModelProviderBlock +
|
|
297
|
-
nextContent.slice(modelProviderLine.end);
|
|
298
|
-
}
|
|
299
|
-
else {
|
|
300
|
-
const firstNonWhitespaceMatch = nextContent.match(/\S/);
|
|
301
|
-
if (firstNonWhitespaceMatch && firstNonWhitespaceMatch.index !== undefined) {
|
|
302
|
-
nextContent = insertBlockBetween(nextContent.slice(0, firstNonWhitespaceMatch.index), managedModelProviderBlock, nextContent.slice(firstNonWhitespaceMatch.index), eol);
|
|
303
|
-
}
|
|
304
|
-
else {
|
|
305
|
-
nextContent = `${managedModelProviderBlock}${eol}`;
|
|
306
|
-
}
|
|
307
|
-
}
|
|
519
|
+
const cleanedBaseContent = removeAllProviderSections(removeAllModelProviderLines(strippedCurrent));
|
|
520
|
+
let nextContent = insertRootBlock(cleanedBaseContent, managedModelProviderBlock, eol, snapshot.original_model_provider_next_table_header);
|
|
521
|
+
nextContent = appendBlockToEnd(nextContent, managedProviderBlock, eol);
|
|
308
522
|
if (!nextContent.endsWith(eol)) {
|
|
309
523
|
nextContent = `${nextContent}${eol}`;
|
|
310
524
|
}
|
|
@@ -335,7 +549,16 @@ function deactivateManagedCodexConfig() {
|
|
|
335
549
|
return null;
|
|
336
550
|
}
|
|
337
551
|
const current = node_fs_1.default.readFileSync(targetFile, "utf8");
|
|
338
|
-
const
|
|
552
|
+
const eol = detectEol(current);
|
|
553
|
+
let restored = stripAllManagedBlocks(current);
|
|
554
|
+
const existingModelProviderLine = findModelProviderLine(restored);
|
|
555
|
+
if (!existingModelProviderLine && managedState.original_model_provider_block) {
|
|
556
|
+
restored = insertRootBlock(restored, managedState.original_model_provider_block, eol, managedState.original_model_provider_next_table_header);
|
|
557
|
+
}
|
|
558
|
+
const existingProviderSection = findProviderSectionRange(restored);
|
|
559
|
+
if (!existingProviderSection && managedState.original_cslot_provider_block) {
|
|
560
|
+
restored = insertTableBlock(restored, managedState.original_cslot_provider_block, eol, managedState.original_cslot_provider_next_table_header, managedState.original_cslot_provider_previous_table_header);
|
|
561
|
+
}
|
|
339
562
|
writeFileAtomic(targetFile, restored);
|
|
340
563
|
(0, state_1.clearManagedCodexConfigState)();
|
|
341
564
|
return targetFile;
|
package/dist/state.js
CHANGED
|
@@ -10,6 +10,9 @@ exports.pruneExpiredBlocks = pruneExpiredBlocks;
|
|
|
10
10
|
exports.getAccountBlock = getAccountBlock;
|
|
11
11
|
exports.setUsageCache = setUsageCache;
|
|
12
12
|
exports.getUsageCache = getUsageCache;
|
|
13
|
+
exports.setUsageRefreshError = setUsageRefreshError;
|
|
14
|
+
exports.clearUsageRefreshError = clearUsageRefreshError;
|
|
15
|
+
exports.getUsageRefreshError = getUsageRefreshError;
|
|
13
16
|
exports.getManagedCodexConfigState = getManagedCodexConfigState;
|
|
14
17
|
exports.setManagedCodexConfigState = setManagedCodexConfigState;
|
|
15
18
|
exports.clearManagedCodexConfigState = clearManagedCodexConfigState;
|
|
@@ -30,6 +33,7 @@ function loadState() {
|
|
|
30
33
|
return {
|
|
31
34
|
account_blocks: {},
|
|
32
35
|
usage_cache: {},
|
|
36
|
+
usage_refresh_errors: {},
|
|
33
37
|
managed_codex_config: null
|
|
34
38
|
};
|
|
35
39
|
}
|
|
@@ -39,11 +43,13 @@ function loadState() {
|
|
|
39
43
|
: {
|
|
40
44
|
account_blocks: {},
|
|
41
45
|
usage_cache: {},
|
|
46
|
+
usage_refresh_errors: {},
|
|
42
47
|
managed_codex_config: null
|
|
43
48
|
};
|
|
44
49
|
return {
|
|
45
50
|
account_blocks: parsed.account_blocks ?? {},
|
|
46
51
|
usage_cache: parsed.usage_cache ?? {},
|
|
52
|
+
usage_refresh_errors: parsed.usage_refresh_errors ?? {},
|
|
47
53
|
managed_codex_config: parsed.managed_codex_config ?? null
|
|
48
54
|
};
|
|
49
55
|
}
|
|
@@ -125,6 +131,41 @@ function getUsageCache(accountId) {
|
|
|
125
131
|
const state = loadState();
|
|
126
132
|
return state.usage_cache[accountId] ?? null;
|
|
127
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* 记录指定账号最近一次额度刷新失败的状态,供 `status` 命令渲染为账号状态而不是直接打印异常。
|
|
136
|
+
*
|
|
137
|
+
* @param usageError 刷新失败信息,包含账号、状态码与原始错误摘要。
|
|
138
|
+
* @returns 无返回值。
|
|
139
|
+
*/
|
|
140
|
+
function setUsageRefreshError(usageError) {
|
|
141
|
+
const state = loadState();
|
|
142
|
+
state.usage_refresh_errors[usageError.accountId] = usageError;
|
|
143
|
+
saveState(state);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* 清理指定账号最近一次记录的额度刷新失败状态,避免后续成功刷新后继续展示旧错误。
|
|
147
|
+
*
|
|
148
|
+
* @param accountId 账号标识。
|
|
149
|
+
* @returns 无返回值。
|
|
150
|
+
*/
|
|
151
|
+
function clearUsageRefreshError(accountId) {
|
|
152
|
+
const state = loadState();
|
|
153
|
+
if (!(accountId in state.usage_refresh_errors)) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
delete state.usage_refresh_errors[accountId];
|
|
157
|
+
saveState(state);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* 读取指定账号最近一次记录的额度刷新失败状态。
|
|
161
|
+
*
|
|
162
|
+
* @param accountId 账号标识。
|
|
163
|
+
* @returns 刷新失败信息;若不存在则返回 `null`。
|
|
164
|
+
*/
|
|
165
|
+
function getUsageRefreshError(accountId) {
|
|
166
|
+
const state = loadState();
|
|
167
|
+
return state.usage_refresh_errors[accountId] ?? null;
|
|
168
|
+
}
|
|
128
169
|
/**
|
|
129
170
|
* 读取当前记录的 Codex `config.toml` 接管快照。
|
|
130
171
|
*
|
package/dist/status-command.js
CHANGED
|
@@ -10,6 +10,75 @@ const status_service_1 = require("./app/status-service");
|
|
|
10
10
|
const scheduler_1 = require("./scheduler");
|
|
11
11
|
const status_1 = require("./status");
|
|
12
12
|
const text_1 = require("./text");
|
|
13
|
+
const ANSI = {
|
|
14
|
+
reset: "\x1b[0m",
|
|
15
|
+
bold: "\x1b[1m",
|
|
16
|
+
dim: "\x1b[2m",
|
|
17
|
+
cyan: "\x1b[36m",
|
|
18
|
+
green: "\x1b[32m",
|
|
19
|
+
yellow: "\x1b[33m"
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* 判断当前终端是否适合启用 ANSI 样式,避免在 dumb/no-color 环境输出控制字符。
|
|
23
|
+
*
|
|
24
|
+
* @returns 可安全启用样式时返回 `true`,否则返回 `false`。
|
|
25
|
+
* @throws 无显式抛出。
|
|
26
|
+
*/
|
|
27
|
+
function shouldUseAnsiStyle() {
|
|
28
|
+
return Boolean(process.stdout.isTTY) && process.env.NO_COLOR === undefined && process.env.TERM !== "dumb";
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 对文本应用 ANSI 样式;当样式关闭时原样返回。
|
|
32
|
+
*
|
|
33
|
+
* @param text 原始文本。
|
|
34
|
+
* @param color ANSI 颜色码。
|
|
35
|
+
* @param enabled 是否启用 ANSI 样式。
|
|
36
|
+
* @returns 样式化后的文本或原文。
|
|
37
|
+
* @throws 无显式抛出。
|
|
38
|
+
*/
|
|
39
|
+
function paint(text, color, enabled) {
|
|
40
|
+
if (!enabled) {
|
|
41
|
+
return text;
|
|
42
|
+
}
|
|
43
|
+
return `${color}${text}${ANSI.reset}`;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 渲染分区标题行,兼容窄终端与普通宽度终端。
|
|
47
|
+
*
|
|
48
|
+
* @param title 分区标题文本。
|
|
49
|
+
* @param width 当前终端宽度。
|
|
50
|
+
* @param styled 是否启用 ANSI 样式。
|
|
51
|
+
* @returns 可直接打印的单行分区标题。
|
|
52
|
+
* @throws 无显式抛出。
|
|
53
|
+
*/
|
|
54
|
+
function renderSectionHeader(title, width, styled) {
|
|
55
|
+
if (width < 44) {
|
|
56
|
+
return paint(`[ ${title} ]`, ANSI.cyan, styled);
|
|
57
|
+
}
|
|
58
|
+
const plainLabel = ` ${title} `;
|
|
59
|
+
const targetWidth = Math.max(plainLabel.length + 2, Math.min(width, 96));
|
|
60
|
+
const side = Math.max(1, Math.floor((targetWidth - plainLabel.length) / 2));
|
|
61
|
+
const line = `${"-".repeat(side)}${plainLabel}${"-".repeat(side)}`;
|
|
62
|
+
return paint(line.slice(0, targetWidth), ANSI.cyan, styled);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 渲染摘要区可读性更高的计数文本,并对关键指标做轻量着色。
|
|
66
|
+
*
|
|
67
|
+
* @param summary 状态摘要计数。
|
|
68
|
+
* @param narrowScreen 是否窄屏布局。
|
|
69
|
+
* @param styled 是否启用 ANSI 样式。
|
|
70
|
+
* @returns 摘要展示文本。
|
|
71
|
+
* @throws 无显式抛出。
|
|
72
|
+
*/
|
|
73
|
+
function renderSummaryLine(summary, narrowScreen, styled) {
|
|
74
|
+
const available = paint(String(summary.available), ANSI.green, styled);
|
|
75
|
+
const fiveHourLimited = paint(String(summary.fiveHourLimited), ANSI.yellow, styled);
|
|
76
|
+
const weeklyLimited = paint(String(summary.weeklyLimited), ANSI.yellow, styled);
|
|
77
|
+
if (narrowScreen) {
|
|
78
|
+
return `ok=${available} 5h=${fiveHourLimited} wk=${weeklyLimited}`;
|
|
79
|
+
}
|
|
80
|
+
return `available=${available} 5h_limited=${fiveHourLimited} weekly_limited=${weeklyLimited}`;
|
|
81
|
+
}
|
|
13
82
|
/**
|
|
14
83
|
* 进入交互式全屏缓冲区,并隐藏光标,确保后续重绘始终基于固定画布。
|
|
15
84
|
*
|
|
@@ -103,6 +172,9 @@ async function handleInteractiveToggle(initialStatuses) {
|
|
|
103
172
|
return await new Promise((resolve) => {
|
|
104
173
|
let closed = false;
|
|
105
174
|
const render = () => {
|
|
175
|
+
const screenWidth = process.stdout.columns ?? 80;
|
|
176
|
+
const narrowScreen = screenWidth < 72;
|
|
177
|
+
const styled = shouldUseAnsiStyle();
|
|
106
178
|
const latestSnapshot = (0, status_service_1.getStatusSnapshot)();
|
|
107
179
|
const statusSource = changed ? latestSnapshot.statuses : (initialStatuses ?? latestSnapshot.statuses);
|
|
108
180
|
const statusById = new Map(statusSource.map((item) => [item.id, item]));
|
|
@@ -120,18 +192,26 @@ async function handleInteractiveToggle(initialStatuses) {
|
|
|
120
192
|
};
|
|
121
193
|
})
|
|
122
194
|
.filter((item) => item !== null);
|
|
195
|
+
const currentItem = displayStatuses.find((item) => item.id === accounts[cursor]?.id) ?? null;
|
|
123
196
|
renderInteractiveScreen([
|
|
197
|
+
renderSectionHeader("accounts", screenWidth, styled),
|
|
124
198
|
(0, status_1.renderStatusTable)(displayStatuses, {
|
|
199
|
+
compact: true,
|
|
200
|
+
maxWidth: screenWidth,
|
|
125
201
|
selectorColumn: {
|
|
126
202
|
enabledById: Object.fromEntries(accounts.map((account) => [account.id, account.enabled])),
|
|
127
203
|
cursorAccountId: accounts[cursor]?.id ?? null
|
|
128
204
|
}
|
|
129
205
|
}),
|
|
130
206
|
"",
|
|
131
|
-
|
|
207
|
+
(0, status_1.renderStatusDetails)(currentItem, { maxWidth: screenWidth }),
|
|
208
|
+
"",
|
|
209
|
+
renderSectionHeader("summary", screenWidth, styled),
|
|
210
|
+
renderSummaryLine(summary, narrowScreen, styled),
|
|
132
211
|
`selected=${latestSnapshot.selectedName ?? "none"}`,
|
|
133
212
|
"",
|
|
134
|
-
(
|
|
213
|
+
renderSectionHeader("help", screenWidth, styled),
|
|
214
|
+
(0, text_1.bi)(narrowScreen ? "Space 切换,Enter / q 退出。" : "Space 切换启用状态,Enter / q 退出。", narrowScreen ? "Space toggles, Enter or q exits." : "Space toggles enabled state, Enter or q exits.")
|
|
135
215
|
]);
|
|
136
216
|
};
|
|
137
217
|
const applyChanges = () => {
|
package/dist/status.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.collectAccountStatuses = collectAccountStatuses;
|
|
4
4
|
exports.summarizeAccountStatuses = summarizeAccountStatuses;
|
|
5
5
|
exports.renderStatusTable = renderStatusTable;
|
|
6
|
+
exports.renderStatusDetails = renderStatusDetails;
|
|
6
7
|
const config_1 = require("./config");
|
|
7
8
|
const account_store_1 = require("./account-store");
|
|
8
9
|
const state_1 = require("./state");
|
|
@@ -28,6 +29,69 @@ function formatPercent(value) {
|
|
|
28
29
|
function formatReset(unixSeconds) {
|
|
29
30
|
return (0, text_1.formatLocalDateTime)(unixSeconds);
|
|
30
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* 按给定最大宽度截断单元格文本,优先保证表格整体不换行。
|
|
34
|
+
*
|
|
35
|
+
* @param value 原始文本。
|
|
36
|
+
* @param maxWidth 最大宽度。
|
|
37
|
+
* @returns 截断后的文本;宽度过小时退化为最短可读形式。
|
|
38
|
+
*/
|
|
39
|
+
function truncateCell(value, maxWidth) {
|
|
40
|
+
if (maxWidth <= 0) {
|
|
41
|
+
return "";
|
|
42
|
+
}
|
|
43
|
+
if (value.length <= maxWidth) {
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
if (maxWidth <= 2) {
|
|
47
|
+
return value.slice(0, maxWidth);
|
|
48
|
+
}
|
|
49
|
+
return `${value.slice(0, maxWidth - 1)}…`;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 生成固定标签宽度的详情行,超出终端宽度时自动截断值部分。
|
|
53
|
+
*
|
|
54
|
+
* @param label 字段标签。
|
|
55
|
+
* @param value 字段值。
|
|
56
|
+
* @param maxWidth 当前可用最大宽度。
|
|
57
|
+
* @returns 单行详情文本。
|
|
58
|
+
*/
|
|
59
|
+
function formatDetailLine(label, value, maxWidth) {
|
|
60
|
+
const prefix = `${label.padEnd(6)} `;
|
|
61
|
+
const safeWidth = Number.isFinite(maxWidth) ? maxWidth : prefix.length + value.length;
|
|
62
|
+
const valueWidth = Math.max(8, safeWidth - prefix.length);
|
|
63
|
+
return `${prefix}${truncateCell(value, valueWidth)}`;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* 将状态对象归一化为单个紧凑状态标签,供表格与详情面板复用。
|
|
67
|
+
*
|
|
68
|
+
* @param item 单个账号状态。
|
|
69
|
+
* @returns 适合在终端展示的状态标签。
|
|
70
|
+
*/
|
|
71
|
+
function resolveStatusLabel(item) {
|
|
72
|
+
if (item.refreshErrorCode) {
|
|
73
|
+
return item.refreshErrorCode;
|
|
74
|
+
}
|
|
75
|
+
if (!item.exists) {
|
|
76
|
+
return "missing";
|
|
77
|
+
}
|
|
78
|
+
if (!item.enabled) {
|
|
79
|
+
return "disabled";
|
|
80
|
+
}
|
|
81
|
+
if (item.localBlockUntil && item.localBlockUntil * 1000 > Date.now()) {
|
|
82
|
+
return formatBlockedStatus(item.localBlockReason, item.localBlockUntil);
|
|
83
|
+
}
|
|
84
|
+
if (item.isWeeklyLimited) {
|
|
85
|
+
return formatLimitStatus("weekly_limited", item.weeklyResetsAt);
|
|
86
|
+
}
|
|
87
|
+
if (item.isFiveHourLimited) {
|
|
88
|
+
return formatLimitStatus("5h_limited", item.fiveHourResetsAt);
|
|
89
|
+
}
|
|
90
|
+
if (item.isAvailable) {
|
|
91
|
+
return "available";
|
|
92
|
+
}
|
|
93
|
+
return "unknown";
|
|
94
|
+
}
|
|
31
95
|
function formatLimitStatus(label, resetAt) {
|
|
32
96
|
const remaining = formatRemainingDuration(resetAt);
|
|
33
97
|
if (!remaining) {
|
|
@@ -44,6 +108,46 @@ function normalizeBlockReason(reason) {
|
|
|
44
108
|
}
|
|
45
109
|
return reason;
|
|
46
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* 将账号工作空间读取异常统一归类为状态码,避免状态汇总阶段直接抛出异常中断整个命令。
|
|
113
|
+
*
|
|
114
|
+
* @param error 工作空间读取过程中抛出的异常。
|
|
115
|
+
* @returns 归一化后的状态码与错误摘要。
|
|
116
|
+
*/
|
|
117
|
+
function classifyWorkspaceStatusError(error) {
|
|
118
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
119
|
+
return {
|
|
120
|
+
code: "workspace_invalid",
|
|
121
|
+
message
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* 读取账号工作空间的本地登录态摘要;若目录损坏或 JSON 非法,则降级为工作空间不可用状态。
|
|
126
|
+
*
|
|
127
|
+
* @param codexHome 账号隔离 HOME 目录。
|
|
128
|
+
* @returns 是否存在完整登录态、主账号信息以及可选的工作空间错误状态。
|
|
129
|
+
*/
|
|
130
|
+
function readWorkspaceSnapshot(codexHome) {
|
|
131
|
+
try {
|
|
132
|
+
const exists = (0, account_store_1.hasCompleteCodexAuthState)(codexHome);
|
|
133
|
+
const primary = exists ? (0, account_store_1.resolvePrimaryRegistryAccount)(codexHome) : null;
|
|
134
|
+
return {
|
|
135
|
+
exists,
|
|
136
|
+
primary,
|
|
137
|
+
workspaceErrorCode: null,
|
|
138
|
+
workspaceErrorMessage: null
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
const workspaceError = classifyWorkspaceStatusError(error);
|
|
143
|
+
return {
|
|
144
|
+
exists: false,
|
|
145
|
+
primary: null,
|
|
146
|
+
workspaceErrorCode: workspaceError.code,
|
|
147
|
+
workspaceErrorMessage: workspaceError.message
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
47
151
|
/**
|
|
48
152
|
* 将剩余秒数格式化为紧凑的人类可读文本,便于在状态列中展示熔断剩余时间。
|
|
49
153
|
*
|
|
@@ -92,10 +196,10 @@ function formatBlockedStatus(reason, until) {
|
|
|
92
196
|
function collectAccountStatuses() {
|
|
93
197
|
const config = (0, config_1.loadConfig)();
|
|
94
198
|
return config.accounts.map((account) => {
|
|
95
|
-
const
|
|
96
|
-
const primary = exists ? (0, account_store_1.resolvePrimaryRegistryAccount)(account.codex_home) : null;
|
|
199
|
+
const workspace = readWorkspaceSnapshot(account.codex_home);
|
|
97
200
|
const usageCache = (0, state_1.getUsageCache)(account.id);
|
|
98
|
-
const
|
|
201
|
+
const refreshError = (0, state_1.getUsageRefreshError)(account.id);
|
|
202
|
+
const activeEmail = usageCache?.email ?? workspace.primary?.email ?? account.email;
|
|
99
203
|
const fiveHourUsed = usageCache?.fiveHourUsedPercent ?? null;
|
|
100
204
|
const fiveHourReset = usageCache?.fiveHourResetAt ?? null;
|
|
101
205
|
const weeklyUsed = usageCache?.weeklyUsedPercent ?? null;
|
|
@@ -106,13 +210,15 @@ function collectAccountStatuses() {
|
|
|
106
210
|
const isWeeklyLimited = isLimited(weeklyUsed, weeklyReset);
|
|
107
211
|
const localBlock = (0, state_1.getAccountBlock)(account.id);
|
|
108
212
|
const localBlocked = localBlock?.until != null ? localBlock.until * 1000 > Date.now() : false;
|
|
213
|
+
const refreshErrorCode = workspace.workspaceErrorCode ?? refreshError?.code ?? null;
|
|
214
|
+
const refreshErrorMessage = workspace.workspaceErrorMessage ?? refreshError?.message ?? null;
|
|
109
215
|
return {
|
|
110
216
|
id: account.id,
|
|
111
217
|
name: account.name,
|
|
112
218
|
email: activeEmail,
|
|
113
219
|
enabled: account.enabled,
|
|
114
|
-
exists,
|
|
115
|
-
plan: usageCache?.plan ?? primary?.plan ?? "-",
|
|
220
|
+
exists: workspace.exists,
|
|
221
|
+
plan: usageCache?.plan ?? workspace.primary?.plan ?? "-",
|
|
116
222
|
fiveHourLeftPercent,
|
|
117
223
|
fiveHourResetsAt: fiveHourReset,
|
|
118
224
|
weeklyLeftPercent,
|
|
@@ -121,8 +227,11 @@ function collectAccountStatuses() {
|
|
|
121
227
|
isWeeklyLimited,
|
|
122
228
|
localBlockReason: localBlock?.reason,
|
|
123
229
|
localBlockUntil: localBlock?.until ?? null,
|
|
230
|
+
refreshErrorCode,
|
|
231
|
+
refreshErrorMessage,
|
|
124
232
|
isAvailable: account.enabled &&
|
|
125
|
-
exists &&
|
|
233
|
+
workspace.exists &&
|
|
234
|
+
!refreshErrorCode &&
|
|
126
235
|
!isFiveHourLimited &&
|
|
127
236
|
!isWeeklyLimited &&
|
|
128
237
|
!localBlocked,
|
|
@@ -152,58 +261,92 @@ function summarizeAccountStatuses(statuses) {
|
|
|
152
261
|
*/
|
|
153
262
|
function renderStatusTable(statuses, options) {
|
|
154
263
|
const selectorColumn = options?.selectorColumn;
|
|
264
|
+
const compact = options?.compact ?? false;
|
|
265
|
+
const maxWidth = options?.maxWidth ?? Number.POSITIVE_INFINITY;
|
|
266
|
+
const compactHeader = maxWidth < 68;
|
|
267
|
+
const compactSlotWidth = maxWidth < 56 ? 8 : 12;
|
|
268
|
+
const compactPlanWidth = maxWidth < 56 ? 4 : 6;
|
|
269
|
+
const compactStatusWidth = maxWidth < 56 ? 12 : 18;
|
|
155
270
|
const rows = [
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
271
|
+
compact
|
|
272
|
+
? [
|
|
273
|
+
...(selectorColumn ? [" "] : []),
|
|
274
|
+
compactHeader ? "ID" : "SLOT",
|
|
275
|
+
compactHeader ? "P" : "PLAN",
|
|
276
|
+
"5H",
|
|
277
|
+
compactHeader ? "WK" : "WEEK",
|
|
278
|
+
compactHeader ? "ST" : "STATUS"
|
|
279
|
+
]
|
|
280
|
+
: [
|
|
281
|
+
...(selectorColumn ? [" "] : []),
|
|
282
|
+
"NAME",
|
|
283
|
+
"EMAIL",
|
|
284
|
+
"PLAN",
|
|
285
|
+
"5H_LEFT",
|
|
286
|
+
"5H_RESET",
|
|
287
|
+
"WEEK_LEFT",
|
|
288
|
+
"WEEK_RESET",
|
|
289
|
+
"STATUS"
|
|
290
|
+
]
|
|
167
291
|
];
|
|
168
292
|
for (const item of statuses) {
|
|
169
|
-
|
|
170
|
-
if (item.exists) {
|
|
171
|
-
if (!item.enabled) {
|
|
172
|
-
status = "disabled";
|
|
173
|
-
}
|
|
174
|
-
else if (item.localBlockUntil && item.localBlockUntil * 1000 > Date.now()) {
|
|
175
|
-
status = formatBlockedStatus(item.localBlockReason, item.localBlockUntil);
|
|
176
|
-
}
|
|
177
|
-
else if (item.isWeeklyLimited) {
|
|
178
|
-
status = formatLimitStatus("weekly_limited", item.weeklyResetsAt);
|
|
179
|
-
}
|
|
180
|
-
else if (item.isFiveHourLimited) {
|
|
181
|
-
status = formatLimitStatus("5h_limited", item.fiveHourResetsAt);
|
|
182
|
-
}
|
|
183
|
-
else if (item.isAvailable) {
|
|
184
|
-
status = "available";
|
|
185
|
-
}
|
|
186
|
-
else {
|
|
187
|
-
status = "unknown";
|
|
188
|
-
}
|
|
189
|
-
}
|
|
293
|
+
const status = resolveStatusLabel(item);
|
|
190
294
|
const selectorCell = selectorColumn
|
|
191
295
|
? `${selectorColumn.cursorAccountId === item.id ? ">" : " "}[${selectorColumn.enabledById[item.id] ? "x" : " "}]`
|
|
192
296
|
: null;
|
|
193
|
-
rows.push(
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
297
|
+
rows.push(compact
|
|
298
|
+
? [
|
|
299
|
+
...(selectorCell ? [selectorCell] : []),
|
|
300
|
+
truncateCell(item.name, compactSlotWidth),
|
|
301
|
+
truncateCell(item.plan, compactPlanWidth),
|
|
302
|
+
formatPercent(item.fiveHourLeftPercent),
|
|
303
|
+
formatPercent(item.weeklyLeftPercent),
|
|
304
|
+
truncateCell(status, compactStatusWidth)
|
|
305
|
+
]
|
|
306
|
+
: [
|
|
307
|
+
...(selectorCell ? [selectorCell] : []),
|
|
308
|
+
item.name,
|
|
309
|
+
item.email ?? "-",
|
|
310
|
+
item.plan,
|
|
311
|
+
formatPercent(item.fiveHourLeftPercent),
|
|
312
|
+
formatReset(item.fiveHourResetsAt),
|
|
313
|
+
formatPercent(item.weeklyLeftPercent),
|
|
314
|
+
formatReset(item.weeklyResetsAt),
|
|
315
|
+
status
|
|
316
|
+
]);
|
|
204
317
|
}
|
|
205
318
|
const widths = rows[0].map((_, columnIndex) => Math.max(...rows.map((row) => row[columnIndex].length)));
|
|
206
319
|
return rows
|
|
207
320
|
.map((row) => row.map((cell, index) => cell.padEnd(widths[index])).join(" "))
|
|
208
321
|
.join("\n");
|
|
209
322
|
}
|
|
323
|
+
/**
|
|
324
|
+
* 将当前选中账号渲染为紧凑详情区,补充主表中省略的邮箱、重置时间与错误摘要。
|
|
325
|
+
*
|
|
326
|
+
* @param item 当前选中的账号状态;为空时返回占位提示。
|
|
327
|
+
* @param options 详情区渲染选项。
|
|
328
|
+
* @returns 适合直接打印的详情区文本。
|
|
329
|
+
*/
|
|
330
|
+
function renderStatusDetails(item, options) {
|
|
331
|
+
if (!item) {
|
|
332
|
+
return ["[ current ]", "slot -"].join("\n");
|
|
333
|
+
}
|
|
334
|
+
const maxWidth = options?.maxWidth ?? Number.POSITIVE_INFINITY;
|
|
335
|
+
const narrow = maxWidth < 72;
|
|
336
|
+
const lines = [
|
|
337
|
+
"[ current ]",
|
|
338
|
+
formatDetailLine("slot", `${item.name} plan=${item.plan}`, maxWidth),
|
|
339
|
+
formatDetailLine("email", item.email ?? "-", maxWidth),
|
|
340
|
+
formatDetailLine("status", resolveStatusLabel(item), maxWidth),
|
|
341
|
+
narrow
|
|
342
|
+
? formatDetailLine("5h", `${formatPercent(item.fiveHourLeftPercent)} reset=${formatReset(item.fiveHourResetsAt)}`, maxWidth)
|
|
343
|
+
: formatDetailLine("5h", `${formatPercent(item.fiveHourLeftPercent)} reset=${formatReset(item.fiveHourResetsAt)}`, maxWidth),
|
|
344
|
+
narrow
|
|
345
|
+
? formatDetailLine("week", `${formatPercent(item.weeklyLeftPercent)} reset=${formatReset(item.weeklyResetsAt)}`, maxWidth)
|
|
346
|
+
: formatDetailLine("week", `${formatPercent(item.weeklyLeftPercent)} reset=${formatReset(item.weeklyResetsAt)}`, maxWidth)
|
|
347
|
+
];
|
|
348
|
+
if (item.refreshErrorMessage) {
|
|
349
|
+
lines.push(formatDetailLine("error", item.refreshErrorMessage, narrow ? Math.max(28, maxWidth) : Math.min(maxWidth, 96)));
|
|
350
|
+
}
|
|
351
|
+
return lines.join("\n");
|
|
352
|
+
}
|
package/dist/usage-sync.js
CHANGED
|
@@ -21,6 +21,32 @@ function normalizeResetAt(value, resetAfterSeconds) {
|
|
|
21
21
|
}
|
|
22
22
|
return null;
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* 将额度刷新异常归类为可直接展示在 `status` 表格中的状态码。
|
|
26
|
+
*
|
|
27
|
+
* @param accountId 刷新失败的账号标识。
|
|
28
|
+
* @param error 刷新流程抛出的原始异常。
|
|
29
|
+
* @returns 归一化后的刷新失败状态。
|
|
30
|
+
*/
|
|
31
|
+
function classifyUsageRefreshError(accountId, error) {
|
|
32
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
33
|
+
const workspaceInvalidPatterns = [
|
|
34
|
+
"未找到账号",
|
|
35
|
+
"缺少 access_token",
|
|
36
|
+
"缺少 refresh_token",
|
|
37
|
+
"Unexpected end of JSON input",
|
|
38
|
+
"Unexpected token"
|
|
39
|
+
];
|
|
40
|
+
const code = workspaceInvalidPatterns.some((pattern) => message.includes(pattern))
|
|
41
|
+
? "workspace_invalid"
|
|
42
|
+
: "refresh_failed";
|
|
43
|
+
return {
|
|
44
|
+
accountId,
|
|
45
|
+
code,
|
|
46
|
+
message,
|
|
47
|
+
updatedAt: new Date().toISOString()
|
|
48
|
+
};
|
|
49
|
+
}
|
|
24
50
|
/**
|
|
25
51
|
* 使用 refresh token 刷新指定账号的 access token,并回写到账号目录。
|
|
26
52
|
*
|
|
@@ -122,6 +148,7 @@ async function refreshAccountUsage(accountId) {
|
|
|
122
148
|
refreshedAt: new Date().toISOString()
|
|
123
149
|
};
|
|
124
150
|
(0, state_1.setUsageCache)(result);
|
|
151
|
+
(0, state_1.clearUsageRefreshError)(accountId);
|
|
125
152
|
return result;
|
|
126
153
|
}
|
|
127
154
|
/**
|
|
@@ -182,8 +209,7 @@ async function refreshAllAccountUsage() {
|
|
|
182
209
|
results.push(result);
|
|
183
210
|
}
|
|
184
211
|
catch (error) {
|
|
185
|
-
|
|
186
|
-
console.error((0, text_1.bi)(`[refresh] ${account.id} 失败: ${message}`, `[refresh] ${account.id} failed: ${message}`));
|
|
212
|
+
(0, state_1.setUsageRefreshError)(classifyUsageRefreshError(account.id, error));
|
|
187
213
|
}
|
|
188
214
|
}
|
|
189
215
|
return results;
|