codex-slot 0.1.19 → 0.1.21

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.
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.loadSchedulerStatsSnapshot = loadSchedulerStatsSnapshot;
4
+ exports.recordAccountScheduleSuccess = recordAccountScheduleSuccess;
5
+ const state_1 = require("./state");
6
+ /**
7
+ * 读取全部账号的调度统计快照。
8
+ *
9
+ * 业务含义:
10
+ * 1. 调度器一次调度只读取一次统计,避免评分过程中反复触碰 state 文件。
11
+ * 2. 返回值是普通对象快照,调用方不得把它当作可自动持久化的引用。
12
+ *
13
+ * @returns 账号调度统计表;key 为账号 id。
14
+ * @throws 当 state 文件读取或 JSON 解析失败时透传底层异常。
15
+ */
16
+ function loadSchedulerStatsSnapshot() {
17
+ return (0, state_1.loadState)().scheduler_stats;
18
+ }
19
+ /**
20
+ * 记录指定账号完成一次成功代理请求。
21
+ *
22
+ * 业务含义:
23
+ * 1. 该统计只服务于调度均匀分摊,不参与额度判断。
24
+ * 2. 每次成功上游响应后递增成功次数并刷新最近成功时间。
25
+ *
26
+ * @param accountId 账号标识;必须是当前配置中的受管账号 id。
27
+ * @returns 无返回值。
28
+ * @throws 当 state 文件写入失败时透传底层异常。
29
+ */
30
+ function recordAccountScheduleSuccess(accountId) {
31
+ (0, state_1.updateState)((state) => {
32
+ const current = state.scheduler_stats[accountId] ?? {
33
+ success_count: 0,
34
+ last_success_at: null
35
+ };
36
+ state.scheduler_stats[accountId] = {
37
+ success_count: current.success_count + 1,
38
+ last_success_at: new Date().toISOString()
39
+ };
40
+ });
41
+ }
package/dist/state.js CHANGED
@@ -5,7 +5,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.loadState = loadState;
7
7
  exports.saveState = saveState;
8
+ exports.updateState = updateState;
8
9
  exports.setAccountBlock = setAccountBlock;
10
+ exports.clearAccountBlock = clearAccountBlock;
9
11
  exports.pruneExpiredBlocks = pruneExpiredBlocks;
10
12
  exports.getAccountBlock = getAccountBlock;
11
13
  exports.setUsageCache = setUsageCache;
@@ -13,8 +15,6 @@ exports.getUsageCache = getUsageCache;
13
15
  exports.setUsageRefreshError = setUsageRefreshError;
14
16
  exports.clearUsageRefreshError = clearUsageRefreshError;
15
17
  exports.getUsageRefreshError = getUsageRefreshError;
16
- exports.getSchedulerStats = getSchedulerStats;
17
- exports.recordAccountScheduleSuccess = recordAccountScheduleSuccess;
18
18
  exports.getManagedCodexConfigState = getManagedCodexConfigState;
19
19
  exports.getManagedCodexAuthState = getManagedCodexAuthState;
20
20
  exports.setManagedCodexConfigState = setManagedCodexConfigState;
@@ -24,9 +24,50 @@ exports.clearManagedCodexAuthState = clearManagedCodexAuthState;
24
24
  const node_fs_1 = __importDefault(require("node:fs"));
25
25
  const node_path_1 = __importDefault(require("node:path"));
26
26
  const config_1 = require("./config");
27
+ const STATE_SCHEMA_VERSION = 1;
27
28
  function getStatePath() {
28
29
  return node_path_1.default.join((0, config_1.getCslotHome)(), "state.json");
29
30
  }
31
+ /**
32
+ * 构造当前版本的默认本地状态对象。
33
+ *
34
+ * 业务含义:
35
+ * 1. 所有缺失或空 state 文件统一走这里补齐字段。
36
+ * 2. 新增状态字段时只需要在默认状态与归一化逻辑中集中维护。
37
+ *
38
+ * @returns 当前 schema 版本的默认状态。
39
+ * @throws 无显式抛出。
40
+ */
41
+ function createDefaultState() {
42
+ return {
43
+ state_version: STATE_SCHEMA_VERSION,
44
+ account_blocks: {},
45
+ usage_cache: {},
46
+ usage_refresh_errors: {},
47
+ scheduler_stats: {},
48
+ managed_codex_auth: null,
49
+ managed_codex_config: null
50
+ };
51
+ }
52
+ /**
53
+ * 将历史版本或字段缺失的 state 归一化为当前 schema。
54
+ *
55
+ * @param parsed 从 state 文件解析出的原始对象。
56
+ * @returns 补齐默认字段后的当前版本状态。
57
+ * @throws 无显式抛出。
58
+ */
59
+ function normalizeState(parsed) {
60
+ const defaults = createDefaultState();
61
+ return {
62
+ state_version: STATE_SCHEMA_VERSION,
63
+ account_blocks: parsed?.account_blocks ?? defaults.account_blocks,
64
+ usage_cache: parsed?.usage_cache ?? defaults.usage_cache,
65
+ usage_refresh_errors: parsed?.usage_refresh_errors ?? defaults.usage_refresh_errors,
66
+ scheduler_stats: parsed?.scheduler_stats ?? defaults.scheduler_stats,
67
+ managed_codex_auth: parsed?.managed_codex_auth ?? defaults.managed_codex_auth,
68
+ managed_codex_config: parsed?.managed_codex_config ?? defaults.managed_codex_config
69
+ };
70
+ }
30
71
  /**
31
72
  * 读取 cslot 的本地运行状态;文件不存在时返回默认空状态。
32
73
  *
@@ -35,34 +76,10 @@ function getStatePath() {
35
76
  function loadState() {
36
77
  const statePath = getStatePath();
37
78
  if (!node_fs_1.default.existsSync(statePath)) {
38
- return {
39
- account_blocks: {},
40
- usage_cache: {},
41
- usage_refresh_errors: {},
42
- scheduler_stats: {},
43
- managed_codex_auth: null,
44
- managed_codex_config: null
45
- };
79
+ return createDefaultState();
46
80
  }
47
81
  const raw = node_fs_1.default.readFileSync(statePath, "utf8");
48
- const parsed = raw.trim()
49
- ? JSON.parse(raw)
50
- : {
51
- account_blocks: {},
52
- usage_cache: {},
53
- usage_refresh_errors: {},
54
- scheduler_stats: {},
55
- managed_codex_auth: null,
56
- managed_codex_config: null
57
- };
58
- return {
59
- account_blocks: parsed.account_blocks ?? {},
60
- usage_cache: parsed.usage_cache ?? {},
61
- usage_refresh_errors: parsed.usage_refresh_errors ?? {},
62
- scheduler_stats: parsed.scheduler_stats ?? {},
63
- managed_codex_auth: parsed.managed_codex_auth ?? null,
64
- managed_codex_config: parsed.managed_codex_config ?? null
65
- };
82
+ return normalizeState(raw.trim() ? JSON.parse(raw) : null);
66
83
  }
67
84
  /**
68
85
  * 持久化 cslot 的本地运行状态。
@@ -72,7 +89,27 @@ function loadState() {
72
89
  */
73
90
  function saveState(state) {
74
91
  const statePath = getStatePath();
75
- node_fs_1.default.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
92
+ const normalizedState = normalizeState(state);
93
+ const tempPath = `${statePath}.${process.pid}.${Date.now()}.tmp`;
94
+ node_fs_1.default.writeFileSync(tempPath, `${JSON.stringify(normalizedState, null, 2)}\n`, "utf8");
95
+ node_fs_1.default.renameSync(tempPath, statePath);
96
+ }
97
+ /**
98
+ * 在单一边界内读取、修改并保存本地状态。
99
+ *
100
+ * 业务含义:
101
+ * 1. 所有状态写入都统一经过当前函数,避免各模块散落 load/mutate/save 流程。
102
+ * 2. 保存阶段复用原子替换写入,降低半写入状态文件风险。
103
+ *
104
+ * @param mutator 状态修改函数;接收当前状态对象并可原地修改。
105
+ * @returns 修改后已保存的状态对象。
106
+ * @throws 当读取、修改或写入失败时透传底层异常。
107
+ */
108
+ function updateState(mutator) {
109
+ const state = loadState();
110
+ mutator(state);
111
+ saveState(state);
112
+ return state;
76
113
  }
77
114
  /**
78
115
  * 为指定账号设置本地禁用窗口,用于临时熔断或周限制冷却。
@@ -83,13 +120,24 @@ function saveState(state) {
83
120
  * @returns 无返回值。
84
121
  */
85
122
  function setAccountBlock(accountId, until, reason) {
86
- const state = loadState();
87
- state.account_blocks[accountId] = {
88
- until,
89
- reason,
90
- updated_at: new Date().toISOString()
91
- };
92
- saveState(state);
123
+ updateState((state) => {
124
+ state.account_blocks[accountId] = {
125
+ until,
126
+ reason,
127
+ updated_at: new Date().toISOString()
128
+ };
129
+ });
130
+ }
131
+ /**
132
+ * 清理指定账号当前记录的本地禁用状态。
133
+ *
134
+ * @param accountId 账号标识。
135
+ * @returns 无返回值。
136
+ */
137
+ function clearAccountBlock(accountId) {
138
+ updateState((state) => {
139
+ delete state.account_blocks[accountId];
140
+ });
93
141
  }
94
142
  /**
95
143
  * 清理已过期的账号禁用记录,并返回最新状态。
@@ -107,7 +155,16 @@ function pruneExpiredBlocks() {
107
155
  }
108
156
  }
109
157
  if (changed) {
110
- saveState(state);
158
+ updateState((latest) => {
159
+ for (const accountId of Object.keys(state.account_blocks)) {
160
+ latest.account_blocks[accountId] = state.account_blocks[accountId];
161
+ }
162
+ for (const accountId of Object.keys(latest.account_blocks)) {
163
+ if (!(accountId in state.account_blocks)) {
164
+ delete latest.account_blocks[accountId];
165
+ }
166
+ }
167
+ });
111
168
  }
112
169
  return state;
113
170
  }
@@ -128,9 +185,9 @@ function getAccountBlock(accountId) {
128
185
  * @returns 无返回值。
129
186
  */
130
187
  function setUsageCache(usage) {
131
- const state = loadState();
132
- state.usage_cache[usage.accountId] = usage;
133
- saveState(state);
188
+ updateState((state) => {
189
+ state.usage_cache[usage.accountId] = usage;
190
+ });
134
191
  }
135
192
  /**
136
193
  * 读取指定账号最近一次成功刷新的额度缓存。
@@ -149,9 +206,9 @@ function getUsageCache(accountId) {
149
206
  * @returns 无返回值。
150
207
  */
151
208
  function setUsageRefreshError(usageError) {
152
- const state = loadState();
153
- state.usage_refresh_errors[usageError.accountId] = usageError;
154
- saveState(state);
209
+ updateState((state) => {
210
+ state.usage_refresh_errors[usageError.accountId] = usageError;
211
+ });
155
212
  }
156
213
  /**
157
214
  * 清理指定账号最近一次记录的额度刷新失败状态,避免后续成功刷新后继续展示旧错误。
@@ -160,12 +217,9 @@ function setUsageRefreshError(usageError) {
160
217
  * @returns 无返回值。
161
218
  */
162
219
  function clearUsageRefreshError(accountId) {
163
- const state = loadState();
164
- if (!(accountId in state.usage_refresh_errors)) {
165
- return;
166
- }
167
- delete state.usage_refresh_errors[accountId];
168
- saveState(state);
220
+ updateState((state) => {
221
+ delete state.usage_refresh_errors[accountId];
222
+ });
169
223
  }
170
224
  /**
171
225
  * 读取指定账号最近一次记录的额度刷新失败状态。
@@ -177,37 +231,6 @@ function getUsageRefreshError(accountId) {
177
231
  const state = loadState();
178
232
  return state.usage_refresh_errors[accountId] ?? null;
179
233
  }
180
- /**
181
- * 读取指定账号的调度使用统计,用于在多账号可用时做均匀分摊。
182
- *
183
- * @param accountId 账号标识。
184
- * @returns 调度统计;不存在时返回默认零值。
185
- */
186
- function getSchedulerStats(accountId) {
187
- const state = loadState();
188
- return state.scheduler_stats[accountId] ?? {
189
- success_count: 0,
190
- last_success_at: null
191
- };
192
- }
193
- /**
194
- * 记录指定账号完成一次成功代理请求,供后续调度降低连续命中同一账号的概率。
195
- *
196
- * @param accountId 账号标识。
197
- * @returns 无返回值。
198
- */
199
- function recordAccountScheduleSuccess(accountId) {
200
- const state = loadState();
201
- const current = state.scheduler_stats[accountId] ?? {
202
- success_count: 0,
203
- last_success_at: null
204
- };
205
- state.scheduler_stats[accountId] = {
206
- success_count: current.success_count + 1,
207
- last_success_at: new Date().toISOString()
208
- };
209
- saveState(state);
210
- }
211
234
  /**
212
235
  * 读取当前记录的 Codex `config.toml` 接管快照。
213
236
  *
@@ -233,9 +256,9 @@ function getManagedCodexAuthState() {
233
256
  * @returns 无返回值。
234
257
  */
235
258
  function setManagedCodexConfigState(managedState) {
236
- const state = loadState();
237
- state.managed_codex_config = managedState;
238
- saveState(state);
259
+ updateState((state) => {
260
+ state.managed_codex_config = managedState;
261
+ });
239
262
  }
240
263
  /**
241
264
  * 保存 Codex 主 HOME 登录态接管快照,用于 stop 时恢复原始登录态文件。
@@ -244,9 +267,9 @@ function setManagedCodexConfigState(managedState) {
244
267
  * @returns 无返回值。
245
268
  */
246
269
  function setManagedCodexAuthState(managedState) {
247
- const state = loadState();
248
- state.managed_codex_auth = managedState;
249
- saveState(state);
270
+ updateState((state) => {
271
+ state.managed_codex_auth = managedState;
272
+ });
250
273
  }
251
274
  /**
252
275
  * 清理 Codex `config.toml` 接管快照。
@@ -254,9 +277,9 @@ function setManagedCodexAuthState(managedState) {
254
277
  * @returns 无返回值。
255
278
  */
256
279
  function clearManagedCodexConfigState() {
257
- const state = loadState();
258
- state.managed_codex_config = null;
259
- saveState(state);
280
+ updateState((state) => {
281
+ state.managed_codex_config = null;
282
+ });
260
283
  }
261
284
  /**
262
285
  * 清理 Codex 主 HOME 登录态接管快照。
@@ -264,7 +287,7 @@ function clearManagedCodexConfigState() {
264
287
  * @returns 无返回值。
265
288
  */
266
289
  function clearManagedCodexAuthState() {
267
- const state = loadState();
268
- state.managed_codex_auth = null;
269
- saveState(state);
290
+ updateState((state) => {
291
+ state.managed_codex_auth = null;
292
+ });
270
293
  }
@@ -91,6 +91,84 @@ function renderSummaryLine(summary, narrowScreen, styled) {
91
91
  }
92
92
  return `available=${available} 5h_limited=${fiveHourLimited} weekly_limited=${weeklyLimited}`;
93
93
  }
94
+ /**
95
+ * 移除 ANSI 控制序列,避免布局计算把颜色码当成可见字符。
96
+ *
97
+ * @param value 可能包含 ANSI 样式的文本。
98
+ * @returns 去除 ANSI 控制符后的文本。
99
+ * @throws 无显式抛出。
100
+ */
101
+ function stripAnsi(value) {
102
+ return value.replace(/\x1b\[[0-9;]*m/g, "");
103
+ }
104
+ /**
105
+ * 判断字符是否应按双列宽展示。
106
+ *
107
+ * @param codePoint Unicode code point;必须来自单个字符迭代结果。
108
+ * @returns 中文、全角符号等宽字符返回 `true`,其他字符返回 `false`。
109
+ * @throws 无显式抛出。
110
+ */
111
+ function isWideCodePoint(codePoint) {
112
+ return (codePoint >= 0x1100 &&
113
+ (codePoint <= 0x115f ||
114
+ codePoint === 0x2329 ||
115
+ codePoint === 0x232a ||
116
+ (codePoint >= 0x2e80 && codePoint <= 0xa4cf && codePoint !== 0x303f) ||
117
+ (codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
118
+ (codePoint >= 0xf900 && codePoint <= 0xfaff) ||
119
+ (codePoint >= 0xfe10 && codePoint <= 0xfe19) ||
120
+ (codePoint >= 0xfe30 && codePoint <= 0xfe6f) ||
121
+ (codePoint >= 0xff00 && codePoint <= 0xff60) ||
122
+ (codePoint >= 0xffe0 && codePoint <= 0xffe6)));
123
+ }
124
+ /**
125
+ * 计算文本在常见等宽终端中的显示列宽。
126
+ *
127
+ * @param value 待计算的文本;允许包含 ANSI 样式。
128
+ * @returns 文本实际占用的显示列数。
129
+ * @throws 无显式抛出。
130
+ */
131
+ function getDisplayWidth(value) {
132
+ let width = 0;
133
+ for (const char of stripAnsi(value)) {
134
+ const codePoint = char.codePointAt(0) ?? 0;
135
+ width += isWideCodePoint(codePoint) ? 2 : 1;
136
+ }
137
+ return width;
138
+ }
139
+ /**
140
+ * 按显示宽度补齐右侧空格。
141
+ *
142
+ * @param value 原始文本;允许包含 ANSI 样式。
143
+ * @param width 目标显示列宽。
144
+ * @returns 右侧补齐后的文本。
145
+ * @throws 无显式抛出。
146
+ */
147
+ function padVisible(value, width) {
148
+ return `${value}${" ".repeat(Math.max(0, width - getDisplayWidth(value)))}`;
149
+ }
150
+ /**
151
+ * 将左右两组文本行渲染为双栏布局。
152
+ *
153
+ * 业务含义:
154
+ * 1. 宽屏状态页左侧展示账号列表,右侧展示当前账号与摘要。
155
+ * 2. 左栏高度通常更高,右栏缺失行需要自动补空,避免右侧内容把左表挤乱。
156
+ *
157
+ * @param leftLines 左栏文本行。
158
+ * @param rightLines 右栏文本行。
159
+ * @param gap 两栏之间的空格数量。
160
+ * @returns 合并后的双栏文本行。
161
+ * @throws 无显式抛出。
162
+ */
163
+ function renderColumns(leftLines, rightLines, gap) {
164
+ const leftWidth = Math.max(0, ...leftLines.map((line) => getDisplayWidth(line)));
165
+ const rowCount = Math.max(leftLines.length, rightLines.length);
166
+ const rows = [];
167
+ for (let index = 0; index < rowCount; index += 1) {
168
+ rows.push(`${padVisible(leftLines[index] ?? "", leftWidth)}${" ".repeat(gap)}${rightLines[index] ?? ""}`.trimEnd());
169
+ }
170
+ return rows;
171
+ }
94
172
  /**
95
173
  * 进入交互式全屏缓冲区,并隐藏光标,确保后续重绘始终基于固定画布。
96
174
  *
@@ -185,7 +263,6 @@ async function handleInteractiveToggle(initialStatuses) {
185
263
  let closed = false;
186
264
  const render = () => {
187
265
  const screenWidth = process.stdout.columns ?? 80;
188
- const narrowScreen = screenWidth < 72;
189
266
  const styled = shouldUseAnsiStyle();
190
267
  const latestSnapshot = (0, status_service_1.getStatusSnapshot)();
191
268
  const statusSource = changed ? latestSnapshot.statuses : (initialStatuses ?? latestSnapshot.statuses);
@@ -205,27 +282,41 @@ async function handleInteractiveToggle(initialStatuses) {
205
282
  })
206
283
  .filter((item) => item !== null);
207
284
  const currentItem = displayStatuses.find((item) => item.id === accounts[cursor]?.id) ?? null;
208
- renderInteractiveScreen([
209
- renderSectionHeader("accounts", screenWidth, styled),
210
- (0, status_1.renderStatusTable)(displayStatuses, {
285
+ const wideLayout = screenWidth >= 104;
286
+ const leftWidth = wideLayout ? Math.max(68, Math.floor(screenWidth * 0.64)) : screenWidth;
287
+ const rightWidth = wideLayout ? Math.max(28, screenWidth - leftWidth - 3) : screenWidth;
288
+ const accountLines = [
289
+ renderSectionHeader("accounts", leftWidth, styled),
290
+ ...(0, status_1.renderStatusTable)(displayStatuses, {
211
291
  compact: true,
212
- maxWidth: screenWidth,
292
+ maxWidth: leftWidth,
293
+ styled,
213
294
  selectorColumn: {
214
295
  enabledById: Object.fromEntries(accounts.map((account) => [account.id, account.enabled])),
215
296
  cursorAccountId: accounts[cursor]?.id ?? null
216
297
  }
217
- }),
298
+ }).split("\n")
299
+ ];
300
+ const sideLines = [
301
+ renderSectionHeader("current", rightWidth, styled),
302
+ ...(0, status_1.renderStatusDetails)(currentItem, { maxWidth: rightWidth, header: false }).split("\n"),
218
303
  "",
219
- renderDivider(screenWidth, styled),
220
- renderSectionHeader("current", screenWidth, styled),
221
- (0, status_1.renderStatusDetails)(currentItem, { maxWidth: screenWidth, header: false }),
222
- "",
223
- renderSectionHeader("summary", screenWidth, styled),
224
- renderSummaryLine(summary, narrowScreen, styled),
304
+ renderSectionHeader("summary", rightWidth, styled),
305
+ renderSummaryLine(summary, rightWidth < 42, styled),
225
306
  `selected=${latestSnapshot.selectedName ?? "none"}`,
226
307
  "",
227
- renderSectionHeader("help", screenWidth, styled),
228
- (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.")
308
+ renderSectionHeader("help", rightWidth, styled),
309
+ (0, text_1.bi)(rightWidth < 42 ? "Space 切换,Enter/q 退出。" : "Space 切换启用状态,Enter / q 退出。", rightWidth < 42 ? "Space toggles, Enter/q exits." : "Space toggles enabled state, Enter / q exits.")
310
+ ];
311
+ if (wideLayout) {
312
+ renderInteractiveScreen(renderColumns(accountLines, sideLines, 3));
313
+ return;
314
+ }
315
+ renderInteractiveScreen([
316
+ ...accountLines,
317
+ "",
318
+ renderDivider(screenWidth, styled),
319
+ ...sideLines
229
320
  ]);
230
321
  };
231
322
  const applyChanges = () => {