@openxiaobu/codexl 0.1.0

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/config.js ADDED
@@ -0,0 +1,202 @@
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.getCodexSwHome = getCodexSwHome;
7
+ exports.getConfigPath = getConfigPath;
8
+ exports.getPidPath = getPidPath;
9
+ exports.getServiceLogPath = getServiceLogPath;
10
+ exports.expandHome = expandHome;
11
+ exports.loadConfig = loadConfig;
12
+ exports.saveConfig = saveConfig;
13
+ exports.getManagedHome = getManagedHome;
14
+ exports.upsertAccount = upsertAccount;
15
+ const node_fs_1 = __importDefault(require("node:fs"));
16
+ const node_os_1 = __importDefault(require("node:os"));
17
+ const node_path_1 = __importDefault(require("node:path"));
18
+ const yaml_1 = __importDefault(require("yaml"));
19
+ const zod_1 = require("zod");
20
+ const managedAccountSchema = zod_1.z.object({
21
+ id: zod_1.z.string().min(1),
22
+ name: zod_1.z.string().min(1),
23
+ codex_home: zod_1.z.string().min(1),
24
+ email: zod_1.z.string().email().optional(),
25
+ enabled: zod_1.z.boolean().default(true),
26
+ imported_at: zod_1.z.string().optional()
27
+ });
28
+ const configSchema = zod_1.z.object({
29
+ version: zod_1.z.number().int().default(1),
30
+ server: zod_1.z
31
+ .object({
32
+ host: zod_1.z.string().default("127.0.0.1"),
33
+ port: zod_1.z.number().int().default(4389),
34
+ api_key: zod_1.z.string().default("codexl-defaultkey")
35
+ })
36
+ .default({
37
+ host: "127.0.0.1",
38
+ port: 4389,
39
+ api_key: "codexl-defaultkey"
40
+ }),
41
+ upstream: zod_1.z
42
+ .object({
43
+ codex_base_url: zod_1.z.string().default("https://chatgpt.com/backend-api/codex"),
44
+ auth_base_url: zod_1.z.string().default("https://auth.openai.com"),
45
+ oauth_client_id: zod_1.z.string().default("app_EMoamEEZ73f0CkXaXp7hrann")
46
+ })
47
+ .default({
48
+ codex_base_url: "https://chatgpt.com/backend-api/codex",
49
+ auth_base_url: "https://auth.openai.com",
50
+ oauth_client_id: "app_EMoamEEZ73f0CkXaXp7hrann"
51
+ }),
52
+ accounts: zod_1.z.array(managedAccountSchema).default([])
53
+ });
54
+ /**
55
+ * 返回 codexl 的根目录,并确保基础目录结构存在。
56
+ *
57
+ * @returns codexl 根目录绝对路径。
58
+ * @throws 当目录无法创建时抛出文件系统错误。
59
+ */
60
+ function getCodexSwHome() {
61
+ const home = node_path_1.default.join(node_os_1.default.homedir(), ".codexl");
62
+ const legacyHome = node_path_1.default.join(node_os_1.default.homedir(), ".codexsw");
63
+ if (!node_fs_1.default.existsSync(home) && node_fs_1.default.existsSync(legacyHome)) {
64
+ node_fs_1.default.cpSync(legacyHome, home, { recursive: true });
65
+ }
66
+ // 先创建 codexl 根目录,后续命令统一基于该目录读写状态。
67
+ node_fs_1.default.mkdirSync(home, { recursive: true });
68
+ node_fs_1.default.mkdirSync(node_path_1.default.join(home, "homes"), { recursive: true });
69
+ node_fs_1.default.mkdirSync(node_path_1.default.join(home, "logs"), { recursive: true });
70
+ return home;
71
+ }
72
+ /**
73
+ * 返回 codexl 配置文件路径。
74
+ *
75
+ * @returns 配置文件绝对路径。
76
+ */
77
+ function getConfigPath() {
78
+ return node_path_1.default.join(getCodexSwHome(), "config.yaml");
79
+ }
80
+ /**
81
+ * 返回后台服务 PID 文件路径。
82
+ *
83
+ * @returns PID 文件绝对路径。
84
+ */
85
+ function getPidPath() {
86
+ return node_path_1.default.join(getCodexSwHome(), "codexl.pid");
87
+ }
88
+ /**
89
+ * 返回后台服务日志文件路径。
90
+ *
91
+ * @returns 日志文件绝对路径。
92
+ */
93
+ function getServiceLogPath() {
94
+ return node_path_1.default.join(getCodexSwHome(), "logs", "service.log");
95
+ }
96
+ /**
97
+ * 将路径中的 `~` 展开为当前用户家目录。
98
+ *
99
+ * @param input 原始路径,允许以 `~` 开头。
100
+ * @returns 展开后的绝对或原始路径。
101
+ */
102
+ function expandHome(input) {
103
+ if (input === "~") {
104
+ return node_os_1.default.homedir();
105
+ }
106
+ if (input.startsWith("~/")) {
107
+ return node_path_1.default.join(node_os_1.default.homedir(), input.slice(2));
108
+ }
109
+ return input;
110
+ }
111
+ /**
112
+ * 读取 codexl 配置;若配置不存在则返回默认配置。
113
+ *
114
+ * @returns 经过 schema 校验后的配置对象。
115
+ * @throws 当配置存在但内容非法时抛出错误。
116
+ */
117
+ function loadConfig() {
118
+ const configPath = getConfigPath();
119
+ const legacyConfigPath = node_path_1.default.join(node_os_1.default.homedir(), ".codexsw", "config.yaml");
120
+ if (!node_fs_1.default.existsSync(configPath)) {
121
+ const defaultConfig = {
122
+ version: 1,
123
+ server: {
124
+ host: "127.0.0.1",
125
+ port: 4389,
126
+ api_key: "codexl-defaultkey"
127
+ },
128
+ upstream: {
129
+ codex_base_url: "https://chatgpt.com/backend-api/codex",
130
+ auth_base_url: "https://auth.openai.com",
131
+ oauth_client_id: "app_EMoamEEZ73f0CkXaXp7hrann"
132
+ },
133
+ accounts: []
134
+ };
135
+ saveConfig(defaultConfig);
136
+ return defaultConfig;
137
+ }
138
+ const raw = node_fs_1.default.readFileSync(configPath, "utf8");
139
+ const parsed = raw.trim() ? yaml_1.default.parse(raw) : {};
140
+ const normalized = configSchema.parse(parsed);
141
+ let changed = JSON.stringify(parsed) !== JSON.stringify(normalized);
142
+ if (normalized.accounts.length === 0 &&
143
+ node_fs_1.default.existsSync(legacyConfigPath)) {
144
+ const legacyRaw = node_fs_1.default.readFileSync(legacyConfigPath, "utf8");
145
+ const legacyParsed = legacyRaw.trim() ? yaml_1.default.parse(legacyRaw) : {};
146
+ const legacyConfig = configSchema.parse(legacyParsed);
147
+ if (legacyConfig.accounts.length > 0) {
148
+ normalized.accounts = legacyConfig.accounts;
149
+ changed = true;
150
+ }
151
+ }
152
+ // 兼容历史默认值,统一迁移到新的简短本地 key。
153
+ if (normalized.server.api_key === "local-only-key" ||
154
+ normalized.server.api_key === "codexsw-defaultkey") {
155
+ normalized.server.api_key = "codexl-defaultkey";
156
+ changed = true;
157
+ }
158
+ // 当旧配置缺少新字段时,将补全后的配置回写,便于用户直接编辑查看。
159
+ if (changed) {
160
+ saveConfig(normalized);
161
+ }
162
+ return normalized;
163
+ }
164
+ /**
165
+ * 持久化 codexl 配置文件。
166
+ *
167
+ * @param config 待写入的配置对象。
168
+ * @returns 无返回值。
169
+ * @throws 当配置写入失败时抛出文件系统错误。
170
+ */
171
+ function saveConfig(config) {
172
+ const configPath = getConfigPath();
173
+ const text = yaml_1.default.stringify(config);
174
+ node_fs_1.default.writeFileSync(configPath, text, "utf8");
175
+ }
176
+ /**
177
+ * 根据账号标识生成其独立的 HOME 目录。
178
+ *
179
+ * @param accountId 账号标识,仅用于本地目录名。
180
+ * @returns 该账号对应的 HOME 目录绝对路径。
181
+ */
182
+ function getManagedHome(accountId) {
183
+ return node_path_1.default.join(getCodexSwHome(), "homes", accountId);
184
+ }
185
+ /**
186
+ * 将账号追加到配置中;若已存在相同 id 则覆盖更新。
187
+ *
188
+ * @param account 待写入的账号配置。
189
+ * @returns 更新后的完整配置对象。
190
+ */
191
+ function upsertAccount(account) {
192
+ const config = loadConfig();
193
+ const index = config.accounts.findIndex((item) => item.id === account.id);
194
+ if (index >= 0) {
195
+ config.accounts[index] = account;
196
+ }
197
+ else {
198
+ config.accounts.push(account);
199
+ }
200
+ saveConfig(config);
201
+ return config;
202
+ }
package/dist/login.js ADDED
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.loginManagedAccount = loginManagedAccount;
4
+ const node_child_process_1 = require("node:child_process");
5
+ const account_store_1 = require("./account-store");
6
+ const config_1 = require("./config");
7
+ /**
8
+ * 使用独立 HOME 目录拉起官方 `codex login`,完成单账号录入。
9
+ *
10
+ * @param accountId 本地账号标识。
11
+ * @returns Promise,成功时返回导入后的 HOME 目录。
12
+ * @throws 当 `codex login` 执行失败时抛出错误。
13
+ */
14
+ async function loginManagedAccount(accountId) {
15
+ const managedHome = (0, config_1.getManagedHome)(accountId);
16
+ (0, account_store_1.registerManagedAccount)(accountId, managedHome);
17
+ return await new Promise((resolve, reject) => {
18
+ const child = (0, node_child_process_1.spawn)("codex", ["login"], {
19
+ env: {
20
+ ...process.env,
21
+ HOME: managedHome
22
+ },
23
+ stdio: "inherit"
24
+ });
25
+ child.on("exit", (code) => {
26
+ if (code === 0) {
27
+ (0, account_store_1.registerManagedAccount)(accountId, managedHome);
28
+ resolve(managedHome);
29
+ return;
30
+ }
31
+ reject(new Error(`codex login 失败,退出码: ${code ?? "unknown"}`));
32
+ });
33
+ child.on("error", (error) => {
34
+ reject(error);
35
+ });
36
+ });
37
+ }
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.pickBestAccount = pickBestAccount;
4
+ exports.listCandidateAccounts = listCandidateAccounts;
5
+ const config_1 = require("./config");
6
+ const status_1 = require("./status");
7
+ function nextResetWeight(resetAt) {
8
+ if (!resetAt) {
9
+ return Number.MAX_SAFE_INTEGER;
10
+ }
11
+ const diff = resetAt * 1000 - Date.now();
12
+ return diff > 0 ? diff : Number.MAX_SAFE_INTEGER;
13
+ }
14
+ /**
15
+ * 选择当前最适合激活的账号。
16
+ *
17
+ * 业务规则:
18
+ * 1. 仅在账号启用且存在凭据时参与调度。
19
+ * 2. 优先选择当前 5 小时和周窗口都未受限的账号。
20
+ * 3. 在多个可用账号间,优先选择 5 小时剩余额度更高的账号。
21
+ *
22
+ * @returns 调度结果;若没有可用账号则返回 `null`。
23
+ */
24
+ function pickBestAccount() {
25
+ return listCandidateAccounts()[0] ?? null;
26
+ }
27
+ /**
28
+ * 返回按优先级排序后的可用账号列表,供代理重试链路使用。
29
+ *
30
+ * @returns 候选账号列表,已按优先级从高到低排序。
31
+ */
32
+ function listCandidateAccounts() {
33
+ const config = (0, config_1.loadConfig)();
34
+ const statuses = (0, status_1.collectAccountStatuses)();
35
+ const accountMap = new Map(config.accounts.map((item) => [item.id, item]));
36
+ const available = statuses
37
+ .filter((item) => item.isAvailable)
38
+ .sort((left, right) => {
39
+ const fiveHourDiff = (right.fiveHourLeftPercent ?? -1) - (left.fiveHourLeftPercent ?? -1);
40
+ if (fiveHourDiff !== 0) {
41
+ return fiveHourDiff;
42
+ }
43
+ const weeklyDiff = (right.weeklyLeftPercent ?? -1) - (left.weeklyLeftPercent ?? -1);
44
+ if (weeklyDiff !== 0) {
45
+ return weeklyDiff;
46
+ }
47
+ return nextResetWeight(left.fiveHourResetsAt) - nextResetWeight(right.fiveHourResetsAt);
48
+ });
49
+ return available
50
+ .map((winner) => {
51
+ const account = accountMap.get(winner.id);
52
+ if (!account) {
53
+ return null;
54
+ }
55
+ return {
56
+ account,
57
+ status: winner,
58
+ reason: "优先选择 5 小时窗口剩余额度最高且当前可用的账号"
59
+ };
60
+ })
61
+ .filter((item) => item !== null);
62
+ }
package/dist/serve.js ADDED
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const config_1 = require("./config");
5
+ const server_1 = require("./server");
6
+ /**
7
+ * 后台服务进程入口。
8
+ *
9
+ * @returns Promise,无返回值。
10
+ * @throws 当端口参数非法或服务启动失败时抛出异常。
11
+ */
12
+ async function main() {
13
+ const config = (0, config_1.loadConfig)();
14
+ const portArgIndex = process.argv.findIndex((item) => item === "--port");
15
+ const port = portArgIndex >= 0 && process.argv[portArgIndex + 1]
16
+ ? Number(process.argv[portArgIndex + 1])
17
+ : config.server.port;
18
+ await (0, server_1.startServer)(port);
19
+ }
20
+ void main().catch((error) => {
21
+ const message = error instanceof Error ? error.message : String(error);
22
+ console.error(`codexl service 启动失败: ${message}`);
23
+ process.exit(1);
24
+ });
package/dist/server.js ADDED
@@ -0,0 +1,235 @@
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.startServer = startServer;
7
+ const fastify_1 = __importDefault(require("fastify"));
8
+ const undici_1 = require("undici");
9
+ const account_store_1 = require("./account-store");
10
+ const config_1 = require("./config");
11
+ const status_1 = require("./status");
12
+ const scheduler_1 = require("./scheduler");
13
+ const state_1 = require("./state");
14
+ const usage_sync_1 = require("./usage-sync");
15
+ function getBearerToken(headerValue) {
16
+ if (!headerValue) {
17
+ return null;
18
+ }
19
+ const match = /^Bearer\s+(.+)$/i.exec(headerValue);
20
+ return match?.[1] ?? null;
21
+ }
22
+ /**
23
+ * 根据错误文本与当前账号状态,决定本地禁用时长。
24
+ *
25
+ * 业务规则:
26
+ * 1. 周限制优先,直到周窗口重置时间。
27
+ * 2. 5 小时额度限制次之,直到 5 小时窗口重置时间。
28
+ * 3. 未能明确识别时,按 5 分钟临时熔断处理。
29
+ *
30
+ * @param picked 当前被选中的账号及状态。
31
+ * @param errorText 上游返回的错误文本。
32
+ * @returns 本地禁用窗口与原因。
33
+ */
34
+ function resolveBlockWindow(picked, errorText) {
35
+ const lowerText = errorText.toLowerCase();
36
+ if (lowerText.includes("weekly") ||
37
+ lowerText.includes("7 day") ||
38
+ lowerText.includes("7-day") ||
39
+ picked.status.isWeeklyLimited) {
40
+ return {
41
+ until: picked.status.weeklyResetsAt ?? Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60,
42
+ reason: "weekly_limited"
43
+ };
44
+ }
45
+ if (lowerText.includes("5 hour") ||
46
+ lowerText.includes("5-hour") ||
47
+ lowerText.includes("5h") ||
48
+ lowerText.includes("usage limit") ||
49
+ picked.status.isFiveHourLimited) {
50
+ return {
51
+ until: picked.status.fiveHourResetsAt ?? Math.floor(Date.now() / 1000) + 5 * 60,
52
+ reason: "five_hour_limited"
53
+ };
54
+ }
55
+ return {
56
+ until: Math.floor(Date.now() / 1000) + 5 * 60,
57
+ reason: "temporary_5m_limit"
58
+ };
59
+ }
60
+ /**
61
+ * 启动一个极轻量本地服务,供后续接入代理或脚本化查询使用。
62
+ *
63
+ * 当前阶段服务用途:
64
+ * 1. 暴露健康检查。
65
+ * 2. 暴露账号状态与当前推荐账号。
66
+ * 3. 为后续真正的 OpenAI-compatible proxy 预留入口。
67
+ *
68
+ * @param port 本地监听端口。
69
+ * @returns Fastify 实例,便于调用方在测试或脚本中复用。
70
+ * @throws 当端口占用或服务启动失败时抛出异常。
71
+ */
72
+ async function startServer(port) {
73
+ const config = (0, config_1.loadConfig)();
74
+ const app = (0, fastify_1.default)({ logger: false });
75
+ app.addHook("onRequest", async (request, reply) => {
76
+ if (request.url === "/health") {
77
+ return;
78
+ }
79
+ const bearer = getBearerToken(request.headers.authorization);
80
+ if (bearer !== config.server.api_key) {
81
+ reply.code(401);
82
+ throw new Error("invalid local api key");
83
+ }
84
+ });
85
+ app.get("/health", async () => {
86
+ return { ok: true };
87
+ });
88
+ app.post("/refresh", async () => {
89
+ const results = await refreshAccountUsageForBestEffort();
90
+ return { refreshed: results };
91
+ });
92
+ app.get("/accounts", async () => {
93
+ return {
94
+ accounts: (0, status_1.collectAccountStatuses)(),
95
+ selected: (0, scheduler_1.pickBestAccount)()
96
+ };
97
+ });
98
+ const proxyHandler = async (requestMessage, reply) => {
99
+ const candidates = (0, scheduler_1.listCandidateAccounts)();
100
+ if (candidates.length === 0) {
101
+ reply.code(503);
102
+ reply.send({
103
+ error: {
104
+ message: "当前没有可用账号",
105
+ type: "no_available_account"
106
+ }
107
+ });
108
+ return;
109
+ }
110
+ let lastErrorPayload = {
111
+ error: {
112
+ message: "所有账号都请求失败",
113
+ type: "all_accounts_failed"
114
+ }
115
+ };
116
+ let lastStatusCode = 503;
117
+ for (const picked of candidates) {
118
+ try {
119
+ await (0, usage_sync_1.refreshAccountUsage)(picked.account.id);
120
+ }
121
+ catch {
122
+ // 刷新失败时继续使用本地缓存,不中断请求链路。
123
+ }
124
+ const auth = (0, account_store_1.readAuthFile)(picked.account.codex_home);
125
+ let accessToken = auth?.tokens?.access_token;
126
+ const accountIdHeader = auth?.tokens?.account_id;
127
+ if (!accessToken) {
128
+ lastErrorPayload = {
129
+ error: {
130
+ message: `账号 ${picked.account.id} 缺少 access_token`,
131
+ type: "invalid_account_auth"
132
+ }
133
+ };
134
+ lastStatusCode = 503;
135
+ continue;
136
+ }
137
+ const sendUpstream = async () => await (0, undici_1.request)(`${config.upstream.codex_base_url}/responses`, {
138
+ method: "POST",
139
+ headers: {
140
+ authorization: `Bearer ${accessToken}`,
141
+ accept: "text/event-stream, application/json",
142
+ "content-type": "application/json",
143
+ "user-agent": "codexl/0.1.0",
144
+ ...(accountIdHeader ? { "chatgpt-account-id": accountIdHeader } : {})
145
+ },
146
+ body: JSON.stringify(requestMessage)
147
+ });
148
+ let upstream = await sendUpstream();
149
+ if (upstream.statusCode === 401) {
150
+ const refreshed = await (0, usage_sync_1.refreshAccountTokens)(picked.account.id);
151
+ accessToken = refreshed.tokens?.access_token ?? accessToken;
152
+ upstream = await sendUpstream();
153
+ }
154
+ if (upstream.statusCode === 429 || upstream.statusCode === 403) {
155
+ const errorText = await upstream.body.text();
156
+ const block = resolveBlockWindow(picked, errorText);
157
+ (0, state_1.setAccountBlock)(picked.account.id, block.until, block.reason);
158
+ lastStatusCode = upstream.statusCode;
159
+ lastErrorPayload = {
160
+ error: {
161
+ message: `账号 ${picked.account.id} 受限: ${errorText}`,
162
+ type: "account_rate_limited"
163
+ }
164
+ };
165
+ continue;
166
+ }
167
+ if (upstream.statusCode >= 400) {
168
+ const errorText = await upstream.body.text();
169
+ const lowerText = errorText.toLowerCase();
170
+ if (lowerText.includes("usage limit") || lowerText.includes("try again later")) {
171
+ const block = resolveBlockWindow(picked, errorText);
172
+ (0, state_1.setAccountBlock)(picked.account.id, block.until, block.reason);
173
+ lastStatusCode = upstream.statusCode;
174
+ lastErrorPayload = {
175
+ error: {
176
+ message: `账号 ${picked.account.id} 命中额度限制: ${errorText}`,
177
+ type: "account_usage_limited"
178
+ }
179
+ };
180
+ continue;
181
+ }
182
+ reply.raw.writeHead(upstream.statusCode, {
183
+ "content-type": "application/json"
184
+ });
185
+ reply.raw.end(errorText);
186
+ return;
187
+ }
188
+ const headers = {};
189
+ const contentType = upstream.headers["content-type"];
190
+ const cacheControl = upstream.headers["cache-control"];
191
+ if (typeof contentType === "string") {
192
+ headers["content-type"] = contentType;
193
+ }
194
+ if (typeof cacheControl === "string") {
195
+ headers["cache-control"] = cacheControl;
196
+ }
197
+ headers.connection = "keep-alive";
198
+ reply.raw.writeHead(upstream.statusCode, headers);
199
+ for await (const chunk of upstream.body) {
200
+ reply.raw.write(chunk);
201
+ }
202
+ reply.raw.end();
203
+ return;
204
+ }
205
+ reply.code(lastStatusCode);
206
+ reply.send(lastErrorPayload);
207
+ };
208
+ app.post("/v1/responses", async (request, reply) => {
209
+ await proxyHandler(request.body, reply);
210
+ });
211
+ app.post("/backend-api/codex/responses", async (request, reply) => {
212
+ await proxyHandler(request.body, reply);
213
+ });
214
+ await app.listen({
215
+ host: config.server.host,
216
+ port: Number.isFinite(port) ? port : config.server.port
217
+ });
218
+ }
219
+ async function refreshAccountUsageForBestEffort() {
220
+ const statuses = (0, status_1.collectAccountStatuses)();
221
+ const refreshed = [];
222
+ for (const status of statuses) {
223
+ try {
224
+ const item = await (0, usage_sync_1.refreshAccountUsage)(status.id);
225
+ refreshed.push(item);
226
+ }
227
+ catch (error) {
228
+ refreshed.push({
229
+ accountId: status.id,
230
+ error: error instanceof Error ? error.message : String(error)
231
+ });
232
+ }
233
+ }
234
+ return refreshed;
235
+ }
package/dist/state.js ADDED
@@ -0,0 +1,121 @@
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.loadState = loadState;
7
+ exports.saveState = saveState;
8
+ exports.setAccountBlock = setAccountBlock;
9
+ exports.pruneExpiredBlocks = pruneExpiredBlocks;
10
+ exports.getAccountBlock = getAccountBlock;
11
+ exports.setUsageCache = setUsageCache;
12
+ exports.getUsageCache = getUsageCache;
13
+ const node_fs_1 = __importDefault(require("node:fs"));
14
+ const node_path_1 = __importDefault(require("node:path"));
15
+ const config_1 = require("./config");
16
+ function getStatePath() {
17
+ return node_path_1.default.join((0, config_1.getCodexSwHome)(), "state.json");
18
+ }
19
+ /**
20
+ * 读取 codexl 的本地运行状态;文件不存在时返回默认空状态。
21
+ *
22
+ * @returns 当前持久化状态。
23
+ */
24
+ function loadState() {
25
+ const statePath = getStatePath();
26
+ if (!node_fs_1.default.existsSync(statePath)) {
27
+ return {
28
+ account_blocks: {},
29
+ usage_cache: {}
30
+ };
31
+ }
32
+ const raw = node_fs_1.default.readFileSync(statePath, "utf8");
33
+ const parsed = raw.trim()
34
+ ? JSON.parse(raw)
35
+ : {
36
+ account_blocks: {},
37
+ usage_cache: {}
38
+ };
39
+ return {
40
+ account_blocks: parsed.account_blocks ?? {},
41
+ usage_cache: parsed.usage_cache ?? {}
42
+ };
43
+ }
44
+ /**
45
+ * 持久化 codexl 的本地运行状态。
46
+ *
47
+ * @param state 待写入状态对象。
48
+ * @returns 无返回值。
49
+ */
50
+ function saveState(state) {
51
+ const statePath = getStatePath();
52
+ node_fs_1.default.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
53
+ }
54
+ /**
55
+ * 为指定账号设置本地禁用窗口,用于临时熔断或周限制冷却。
56
+ *
57
+ * @param accountId 账号标识。
58
+ * @param until 禁用截止时间,Unix 秒时间戳;为 `null` 时表示仅记录原因。
59
+ * @param reason 禁用原因。
60
+ * @returns 无返回值。
61
+ */
62
+ function setAccountBlock(accountId, until, reason) {
63
+ const state = loadState();
64
+ state.account_blocks[accountId] = {
65
+ until,
66
+ reason,
67
+ updated_at: new Date().toISOString()
68
+ };
69
+ saveState(state);
70
+ }
71
+ /**
72
+ * 清理已过期的账号禁用记录,并返回最新状态。
73
+ *
74
+ * @returns 清理后的状态对象。
75
+ */
76
+ function pruneExpiredBlocks() {
77
+ const state = loadState();
78
+ const now = Math.floor(Date.now() / 1000);
79
+ let changed = false;
80
+ for (const [accountId, block] of Object.entries(state.account_blocks)) {
81
+ if (block.until !== null && block.until <= now) {
82
+ delete state.account_blocks[accountId];
83
+ changed = true;
84
+ }
85
+ }
86
+ if (changed) {
87
+ saveState(state);
88
+ }
89
+ return state;
90
+ }
91
+ /**
92
+ * 读取指定账号当前的本地禁用状态;若已过期会自动清理。
93
+ *
94
+ * @param accountId 账号标识。
95
+ * @returns 账号禁用状态;不存在或已过期时返回 `null`。
96
+ */
97
+ function getAccountBlock(accountId) {
98
+ const state = pruneExpiredBlocks();
99
+ return state.account_blocks[accountId] ?? null;
100
+ }
101
+ /**
102
+ * 更新指定账号的最新额度缓存,仅写入 codexl 自己的状态文件。
103
+ *
104
+ * @param usage 最新额度结果。
105
+ * @returns 无返回值。
106
+ */
107
+ function setUsageCache(usage) {
108
+ const state = loadState();
109
+ state.usage_cache[usage.accountId] = usage;
110
+ saveState(state);
111
+ }
112
+ /**
113
+ * 读取指定账号最近一次成功刷新的额度缓存。
114
+ *
115
+ * @param accountId 账号标识。
116
+ * @returns 最新额度缓存;不存在时返回 `null`。
117
+ */
118
+ function getUsageCache(accountId) {
119
+ const state = loadState();
120
+ return state.usage_cache[accountId] ?? null;
121
+ }