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/dist/state.js CHANGED
@@ -10,11 +10,14 @@ exports.pruneExpiredBlocks = pruneExpiredBlocks;
10
10
  exports.getAccountBlock = getAccountBlock;
11
11
  exports.setUsageCache = setUsageCache;
12
12
  exports.getUsageCache = getUsageCache;
13
+ exports.getManagedCodexConfigState = getManagedCodexConfigState;
14
+ exports.setManagedCodexConfigState = setManagedCodexConfigState;
15
+ exports.clearManagedCodexConfigState = clearManagedCodexConfigState;
13
16
  const node_fs_1 = __importDefault(require("node:fs"));
14
17
  const node_path_1 = __importDefault(require("node:path"));
15
18
  const config_1 = require("./config");
16
19
  function getStatePath() {
17
- return node_path_1.default.join((0, config_1.getCodexSwHome)(), "state.json");
20
+ return node_path_1.default.join((0, config_1.getCslotHome)(), "state.json");
18
21
  }
19
22
  /**
20
23
  * 读取 cslot 的本地运行状态;文件不存在时返回默认空状态。
@@ -26,7 +29,8 @@ function loadState() {
26
29
  if (!node_fs_1.default.existsSync(statePath)) {
27
30
  return {
28
31
  account_blocks: {},
29
- usage_cache: {}
32
+ usage_cache: {},
33
+ managed_codex_config: null
30
34
  };
31
35
  }
32
36
  const raw = node_fs_1.default.readFileSync(statePath, "utf8");
@@ -34,11 +38,13 @@ function loadState() {
34
38
  ? JSON.parse(raw)
35
39
  : {
36
40
  account_blocks: {},
37
- usage_cache: {}
41
+ usage_cache: {},
42
+ managed_codex_config: null
38
43
  };
39
44
  return {
40
45
  account_blocks: parsed.account_blocks ?? {},
41
- usage_cache: parsed.usage_cache ?? {}
46
+ usage_cache: parsed.usage_cache ?? {},
47
+ managed_codex_config: parsed.managed_codex_config ?? null
42
48
  };
43
49
  }
44
50
  /**
@@ -119,3 +125,33 @@ function getUsageCache(accountId) {
119
125
  const state = loadState();
120
126
  return state.usage_cache[accountId] ?? null;
121
127
  }
128
+ /**
129
+ * 读取当前记录的 Codex `config.toml` 接管快照。
130
+ *
131
+ * @returns 最近一次接管时保存的快照;不存在时返回 `null`。
132
+ */
133
+ function getManagedCodexConfigState() {
134
+ const state = loadState();
135
+ return state.managed_codex_config ?? null;
136
+ }
137
+ /**
138
+ * 保存 Codex `config.toml` 接管快照,用于后续停止服务时精确恢复。
139
+ *
140
+ * @param managedState 接管前保存的原始片段快照。
141
+ * @returns 无返回值。
142
+ */
143
+ function setManagedCodexConfigState(managedState) {
144
+ const state = loadState();
145
+ state.managed_codex_config = managedState;
146
+ saveState(state);
147
+ }
148
+ /**
149
+ * 清理 Codex `config.toml` 接管快照。
150
+ *
151
+ * @returns 无返回值。
152
+ */
153
+ function clearManagedCodexConfigState() {
154
+ const state = loadState();
155
+ state.managed_codex_config = null;
156
+ saveState(state);
157
+ }
@@ -0,0 +1,216 @@
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.handleStatus = handleStatus;
7
+ const node_readline_1 = __importDefault(require("node:readline"));
8
+ const account_service_1 = require("./app/account-service");
9
+ const status_service_1 = require("./app/status-service");
10
+ const scheduler_1 = require("./scheduler");
11
+ const status_1 = require("./status");
12
+ const text_1 = require("./text");
13
+ /**
14
+ * 进入交互式全屏缓冲区,并隐藏光标,确保后续重绘始终基于固定画布。
15
+ *
16
+ * @returns 无返回值。
17
+ * @throws 无显式抛出。
18
+ */
19
+ function enterInteractiveScreen() {
20
+ process.stdout.write("\x1b[?1049h");
21
+ process.stdout.write("\x1b[?25l");
22
+ }
23
+ /**
24
+ * 退出交互式全屏缓冲区,并恢复光标显示。
25
+ *
26
+ * @returns 无返回值。
27
+ * @throws 无显式抛出。
28
+ */
29
+ function leaveInteractiveScreen() {
30
+ process.stdout.write("\x1b[?25h");
31
+ process.stdout.write("\x1b[?1049l");
32
+ }
33
+ /**
34
+ * 在交互式全屏缓冲区中从左上角整块重绘内容。
35
+ *
36
+ * @param lines 待输出的文本行数组。
37
+ * @returns 无返回值。
38
+ * @throws 无显式抛出。
39
+ */
40
+ function renderInteractiveScreen(lines) {
41
+ node_readline_1.default.cursorTo(process.stdout, 0, 0);
42
+ node_readline_1.default.clearScreenDown(process.stdout);
43
+ process.stdout.write(lines.join("\n"));
44
+ process.stdout.write("\n");
45
+ }
46
+ /**
47
+ * 计算交互式状态面板的初始光标位置。
48
+ *
49
+ * 业务规则:
50
+ * 1. 优先定位到当前自动调度选中的账号。
51
+ * 2. 若没有自动选中账号,则回退到首个可用账号。
52
+ * 3. 若所有账号都不可用,则回退到首个已启用账号。
53
+ *
54
+ * @param accounts 已按展示顺序排好的账号列表。
55
+ * @param statuses 当前账号运行时状态快照。
56
+ * @returns 初始光标所在的数组下标。
57
+ * @throws 无显式抛出。
58
+ */
59
+ function resolveInitialCursorIndex(accounts, statuses) {
60
+ const selected = (0, scheduler_1.pickBestAccount)();
61
+ if (selected) {
62
+ const selectedIndex = accounts.findIndex((account) => account.id === selected.account.id);
63
+ if (selectedIndex >= 0) {
64
+ return selectedIndex;
65
+ }
66
+ }
67
+ const statusById = new Map(statuses.map((item) => [item.id, item]));
68
+ const availableIndex = accounts.findIndex((account) => statusById.get(account.id)?.isAvailable);
69
+ if (availableIndex >= 0) {
70
+ return availableIndex;
71
+ }
72
+ const enabledIndex = accounts.findIndex((account) => account.enabled);
73
+ if (enabledIndex >= 0) {
74
+ return enabledIndex;
75
+ }
76
+ return 0;
77
+ }
78
+ /**
79
+ * 进入账号启用状态的交互式切换界面,并在用户确认退出后恢复终端状态。
80
+ *
81
+ * @param initialStatuses 进入交互前刚刷新的账号状态快照,用于首屏复用同一块展示区域。
82
+ * @returns Promise,在用户按下 `Enter`、`q` 或 `Ctrl+C` 退出交互后完成。
83
+ * @throws 当终端读写异常时透传底层错误。
84
+ */
85
+ async function handleInteractiveToggle(initialStatuses) {
86
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
87
+ console.log((0, text_1.bi)("当前环境不支持交互式操作,请直接编辑配置文件或使用 --no-interactive 选项。", "Interactive mode is unavailable in the current environment. Edit the config file directly or use --no-interactive."));
88
+ return;
89
+ }
90
+ const stdin = process.stdin;
91
+ node_readline_1.default.emitKeypressEvents(stdin);
92
+ stdin.setRawMode?.(true);
93
+ const accountsFromConfig = (0, account_service_1.listAccounts)();
94
+ if (accountsFromConfig.length === 0) {
95
+ console.log((0, text_1.bi)("当前没有已录入账号。", "No managed accounts found."));
96
+ stdin.setRawMode?.(false);
97
+ return;
98
+ }
99
+ const accounts = [...accountsFromConfig].sort((left, right) => left.name.localeCompare(right.name));
100
+ let cursor = resolveInitialCursorIndex(accounts, initialStatuses ?? (0, status_1.collectAccountStatuses)());
101
+ let changed = false;
102
+ enterInteractiveScreen();
103
+ return await new Promise((resolve) => {
104
+ let closed = false;
105
+ const render = () => {
106
+ const latestSnapshot = (0, status_service_1.getStatusSnapshot)();
107
+ const statusSource = changed ? latestSnapshot.statuses : (initialStatuses ?? latestSnapshot.statuses);
108
+ const statusById = new Map(statusSource.map((item) => [item.id, item]));
109
+ const autoSelectedId = (0, scheduler_1.pickBestAccount)()?.account.id ?? null;
110
+ const summary = (0, status_1.summarizeAccountStatuses)(statusSource);
111
+ const displayStatuses = accounts
112
+ .map((account) => {
113
+ const status = statusById.get(account.id);
114
+ if (!status) {
115
+ return null;
116
+ }
117
+ return {
118
+ ...status,
119
+ name: account.id === autoSelectedId ? `${status.name}*` : status.name
120
+ };
121
+ })
122
+ .filter((item) => item !== null);
123
+ renderInteractiveScreen([
124
+ (0, status_1.renderStatusTable)(displayStatuses, {
125
+ selectorColumn: {
126
+ enabledById: Object.fromEntries(accounts.map((account) => [account.id, account.enabled])),
127
+ cursorAccountId: accounts[cursor]?.id ?? null
128
+ }
129
+ }),
130
+ "",
131
+ `available=${summary.available} 5h_limited=${summary.fiveHourLimited} weekly_limited=${summary.weeklyLimited}`,
132
+ `selected=${latestSnapshot.selectedName ?? "none"}`,
133
+ "",
134
+ (0, text_1.bi)("空格切换当前行启用状态,Enter / q 退出。", "Press Space to toggle the current row, Enter or q to exit.")
135
+ ]);
136
+ };
137
+ const applyChanges = () => {
138
+ if (!changed) {
139
+ return;
140
+ }
141
+ (0, status_service_1.persistAccountEnabledState)(accounts);
142
+ changed = false;
143
+ initialStatuses = (0, status_1.collectAccountStatuses)();
144
+ };
145
+ const exitInteractive = () => {
146
+ if (closed) {
147
+ return;
148
+ }
149
+ closed = true;
150
+ applyChanges();
151
+ stdin.off("keypress", onKeypress);
152
+ stdin.setRawMode?.(false);
153
+ stdin.pause();
154
+ leaveInteractiveScreen();
155
+ console.log((0, text_1.bi)("已退出账号启用状态编辑。", "Exited account toggle mode."));
156
+ resolve();
157
+ };
158
+ const onKeypress = (_input, key) => {
159
+ if (key.name === "up") {
160
+ const nextCursor = Math.max(0, cursor - 1);
161
+ if (nextCursor !== cursor) {
162
+ cursor = nextCursor;
163
+ render();
164
+ }
165
+ return;
166
+ }
167
+ if (key.name === "down") {
168
+ const nextCursor = Math.min(accounts.length - 1, cursor + 1);
169
+ if (nextCursor !== cursor) {
170
+ cursor = nextCursor;
171
+ render();
172
+ }
173
+ return;
174
+ }
175
+ if (key.name === "space") {
176
+ accounts[cursor].enabled = !accounts[cursor].enabled;
177
+ changed = true;
178
+ applyChanges();
179
+ render();
180
+ return;
181
+ }
182
+ if (key.name === "return" || key.name === "enter") {
183
+ exitInteractive();
184
+ return;
185
+ }
186
+ if (key.name === "q" || (key.ctrl && key.name === "c")) {
187
+ exitInteractive();
188
+ }
189
+ };
190
+ render();
191
+ stdin.on("keypress", onKeypress);
192
+ });
193
+ }
194
+ /**
195
+ * 刷新所有已录入账号的远端额度,并输出最新状态表格。
196
+ *
197
+ * @param options 状态命令配置;默认进入交互式启用开关界面。
198
+ * @returns Promise,无返回值。
199
+ * @throws 当额度刷新或终端交互失败时透传底层异常。
200
+ */
201
+ async function handleStatus(options) {
202
+ const snapshot = await (0, status_service_1.refreshStatusSnapshot)();
203
+ const interactive = options?.interactive ?? true;
204
+ if (interactive) {
205
+ await handleInteractiveToggle(snapshot.statuses);
206
+ return;
207
+ }
208
+ const displayStatuses = snapshot.statuses.map((item) => ({
209
+ ...item,
210
+ name: item.id === (0, scheduler_1.pickBestAccount)()?.account.id ? `${item.name}*` : item.name
211
+ }));
212
+ console.log((0, status_1.renderStatusTable)(displayStatuses));
213
+ console.log("");
214
+ console.log(`available=${snapshot.summary.available} 5h_limited=${snapshot.summary.fiveHourLimited} weekly_limited=${snapshot.summary.weeklyLimited}`);
215
+ console.log(`selected=${snapshot.selectedName ?? "none"}`);
216
+ }
package/dist/status.js CHANGED
@@ -1,10 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.collectAccountStatuses = collectAccountStatuses;
4
+ exports.summarizeAccountStatuses = summarizeAccountStatuses;
4
5
  exports.renderStatusTable = renderStatusTable;
5
6
  const config_1 = require("./config");
6
7
  const account_store_1 = require("./account-store");
7
8
  const state_1 = require("./state");
9
+ const text_1 = require("./text");
8
10
  function computeLeftPercent(usedPercent) {
9
11
  if (usedPercent === null || usedPercent === undefined || Number.isNaN(usedPercent)) {
10
12
  return null;
@@ -24,12 +26,7 @@ function formatPercent(value) {
24
26
  return value === null ? "-" : `${value}%`;
25
27
  }
26
28
  function formatReset(unixSeconds) {
27
- if (!unixSeconds) {
28
- return "-";
29
- }
30
- return new Date(unixSeconds * 1000).toLocaleString("zh-CN", {
31
- hour12: false
32
- });
29
+ return (0, text_1.formatLocalDateTime)(unixSeconds);
33
30
  }
34
31
  function formatLimitStatus(label, resetAt) {
35
32
  const remaining = formatRemainingDuration(resetAt);
@@ -133,6 +130,19 @@ function collectAccountStatuses() {
133
130
  };
134
131
  });
135
132
  }
133
+ /**
134
+ * 根据账号运行时状态汇总可用数与额度受限数,供 CLI 统一展示摘要。
135
+ *
136
+ * @param statuses 账号运行时状态列表。
137
+ * @returns 汇总后的数量统计。
138
+ */
139
+ function summarizeAccountStatuses(statuses) {
140
+ return {
141
+ available: statuses.filter((item) => item.isAvailable).length,
142
+ fiveHourLimited: statuses.filter((item) => item.isFiveHourLimited && !item.isWeeklyLimited).length,
143
+ weeklyLimited: statuses.filter((item) => item.isWeeklyLimited).length
144
+ };
145
+ }
136
146
  /**
137
147
  * 将账号状态渲染为适合终端输出的表格文本。
138
148
  *
package/dist/text.js ADDED
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.bi = bi;
4
+ exports.formatLocalDateTime = formatLocalDateTime;
5
+ /**
6
+ * 生成统一的中英双语文案,避免在各处手写不一致的分隔形式。
7
+ *
8
+ * @param zh 简体中文文案。
9
+ * @param en 英文文案。
10
+ * @returns 统一格式的中英双语字符串。
11
+ */
12
+ function bi(zh, en) {
13
+ return `${zh} / ${en}`;
14
+ }
15
+ /**
16
+ * 将时间格式化为与 locale 无关的本地时间文本,避免输出固定绑定某个语言区域。
17
+ *
18
+ * @param unixSeconds Unix 秒时间戳;为空时返回 `-`。
19
+ * @returns 形如 `2026-03-13 16:46:23` 的本地时间字符串。
20
+ */
21
+ function formatLocalDateTime(unixSeconds) {
22
+ if (!unixSeconds) {
23
+ return "-";
24
+ }
25
+ const date = new Date(unixSeconds * 1000);
26
+ const year = date.getFullYear();
27
+ const month = `${date.getMonth() + 1}`.padStart(2, "0");
28
+ const day = `${date.getDate()}`.padStart(2, "0");
29
+ const hour = `${date.getHours()}`.padStart(2, "0");
30
+ const minute = `${date.getMinutes()}`.padStart(2, "0");
31
+ const second = `${date.getSeconds()}`.padStart(2, "0");
32
+ return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
33
+ }
@@ -9,6 +9,7 @@ const undici_1 = require("undici");
9
9
  const account_store_1 = require("./account-store");
10
10
  const config_1 = require("./config");
11
11
  const state_1 = require("./state");
12
+ const text_1 = require("./text");
12
13
  const USAGE_CACHE_TTL_MS = 60 * 1000;
13
14
  const inflightUsageRefreshes = new Map();
14
15
  function normalizeResetAt(value, resetAfterSeconds) {
@@ -31,12 +32,12 @@ async function refreshAccountTokens(accountId) {
31
32
  const config = (0, config_1.loadConfig)();
32
33
  const account = (0, account_store_1.findManagedAccount)(accountId);
33
34
  if (!account) {
34
- throw new Error(`未找到账号 ${accountId}`);
35
+ throw new Error((0, text_1.bi)(`未找到账号 ${accountId}`, `Account not found: ${accountId}`));
35
36
  }
36
37
  const auth = (0, account_store_1.readAuthFile)(account.codex_home);
37
38
  const refreshToken = auth?.tokens?.refresh_token;
38
39
  if (!refreshToken) {
39
- throw new Error(`账号 ${accountId} 缺少 refresh_token`);
40
+ throw new Error((0, text_1.bi)(`账号 ${accountId} 缺少 refresh_token`, `Account ${accountId} is missing refresh_token`));
40
41
  }
41
42
  const response = await (0, undici_1.request)(`${config.upstream.auth_base_url}/oauth/token`, {
42
43
  method: "POST",
@@ -52,7 +53,7 @@ async function refreshAccountTokens(accountId) {
52
53
  });
53
54
  if (response.statusCode >= 400) {
54
55
  const errorText = await response.body.text();
55
- throw new Error(`刷新 token 失败: HTTP ${response.statusCode} ${errorText}`);
56
+ throw new Error((0, text_1.bi)(`刷新 token 失败: HTTP ${response.statusCode} ${errorText}`, `Failed to refresh token: HTTP ${response.statusCode} ${errorText}`));
56
57
  }
57
58
  const payload = (await response.body.json());
58
59
  const nextAuth = {
@@ -81,13 +82,13 @@ async function refreshAccountTokens(accountId) {
81
82
  async function refreshAccountUsage(accountId) {
82
83
  const account = (0, account_store_1.findManagedAccount)(accountId);
83
84
  if (!account) {
84
- throw new Error(`未找到账号 ${accountId}`);
85
+ throw new Error((0, text_1.bi)(`未找到账号 ${accountId}`, `Account not found: ${accountId}`));
85
86
  }
86
87
  const auth = (0, account_store_1.readAuthFile)(account.codex_home);
87
88
  const accessToken = auth?.tokens?.access_token;
88
89
  const accountIdHeader = auth?.tokens?.account_id;
89
90
  if (!accessToken) {
90
- throw new Error(`账号 ${accountId} 缺少 access_token`);
91
+ throw new Error((0, text_1.bi)(`账号 ${accountId} 缺少 access_token`, `Account ${accountId} is missing access_token`));
91
92
  }
92
93
  const response = await (0, undici_1.request)("https://chatgpt.com/backend-api/wham/usage", {
93
94
  method: "GET",
@@ -104,7 +105,7 @@ async function refreshAccountUsage(accountId) {
104
105
  }
105
106
  if (response.statusCode >= 400) {
106
107
  const errorText = await response.body.text();
107
- throw new Error(`刷新额度失败: HTTP ${response.statusCode} ${errorText}`);
108
+ throw new Error((0, text_1.bi)(`刷新额度失败: HTTP ${response.statusCode} ${errorText}`, `Failed to refresh usage: HTTP ${response.statusCode} ${errorText}`));
108
109
  }
109
110
  const payload = (await response.body.json());
110
111
  const primary = (0, account_store_1.resolvePrimaryRegistryAccount)(account.codex_home);
@@ -182,7 +183,7 @@ async function refreshAllAccountUsage() {
182
183
  }
183
184
  catch (error) {
184
185
  const message = error instanceof Error ? error.message : String(error);
185
- console.error(`[refresh] ${account.id} 失败: ${message}`);
186
+ console.error((0, text_1.bi)(`[refresh] ${account.id} 失败: ${message}`, `[refresh] ${account.id} failed: ${message}`));
186
187
  }
187
188
  }
188
189
  return results;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-slot",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "本地 Codex 多账号切换与状态管理工具",
5
5
  "type": "commonjs",
6
6
  "main": "dist/cli.js",
@@ -13,7 +13,7 @@
13
13
  ],
14
14
  "scripts": {
15
15
  "clean": "rm -rf dist",
16
- "build": "tsc -p tsconfig.json && chmod +x dist/cli.js dist/serve.js",
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
19
  "check": "tsc --noEmit -p tsconfig.json"