codex-slot 0.1.19 → 0.1.20
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/app/account-service.js +18 -18
- package/dist/codex-auth.js +2 -1
- package/dist/codex-config.js +1 -1
- package/dist/config.js +13 -3
- package/dist/login.js +2 -1
- package/dist/proxy-retry-service.js +237 -0
- package/dist/scheduler-strategy.js +311 -0
- package/dist/scheduler.js +77 -206
- package/dist/server.js +19 -290
- package/dist/state-repository.js +41 -0
- package/dist/state.js +115 -92
- package/dist/upstream-client.js +63 -0
- package/dist/upstream-error-policy.js +114 -0
- package/dist/usage-sync.js +23 -0
- package/package.json +3 -3
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
290
|
+
updateState((state) => {
|
|
291
|
+
state.managed_codex_auth = null;
|
|
292
|
+
});
|
|
270
293
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildUpstreamHeaders = buildUpstreamHeaders;
|
|
4
|
+
exports.sendCodexResponsesRequest = sendCodexResponsesRequest;
|
|
5
|
+
const undici_1 = require("undici");
|
|
6
|
+
/**
|
|
7
|
+
* 构造发往上游的请求头,并移除仅属于本地代理链路的头信息。
|
|
8
|
+
*
|
|
9
|
+
* 业务含义:
|
|
10
|
+
* 1. 本地服务使用独立 api_key 鉴权,转发时必须替换为真实上游 access token。
|
|
11
|
+
* 2. body 会在本地先读取成 Buffer 以支持失败后切换账号重试,因此这里重算 content-length。
|
|
12
|
+
*
|
|
13
|
+
* @param requestHeaders 客户端发到本地服务的原始请求头。
|
|
14
|
+
* @param accessToken 当前候选账号可用的上游访问令牌。
|
|
15
|
+
* @param bodyLength 请求体字节长度。
|
|
16
|
+
* @param accountIdHeader 可选的 ChatGPT 账号标识头。
|
|
17
|
+
* @returns 可直接传给上游请求的请求头对象。
|
|
18
|
+
* @throws 无显式抛出。
|
|
19
|
+
*/
|
|
20
|
+
function buildUpstreamHeaders(requestHeaders, accessToken, bodyLength, accountIdHeader) {
|
|
21
|
+
const headers = {};
|
|
22
|
+
for (const [headerName, headerValue] of Object.entries(requestHeaders)) {
|
|
23
|
+
const normalizedName = headerName.toLowerCase();
|
|
24
|
+
if (headerValue == null ||
|
|
25
|
+
normalizedName === "authorization" ||
|
|
26
|
+
normalizedName === "host" ||
|
|
27
|
+
normalizedName === "connection" ||
|
|
28
|
+
normalizedName === "content-length") {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
headers[normalizedName] = Array.isArray(headerValue)
|
|
32
|
+
? headerValue.join(", ")
|
|
33
|
+
: headerValue;
|
|
34
|
+
}
|
|
35
|
+
headers.authorization = `Bearer ${accessToken}`;
|
|
36
|
+
if (!headers.accept) {
|
|
37
|
+
headers.accept = "text/event-stream, application/json";
|
|
38
|
+
}
|
|
39
|
+
headers["content-length"] = String(bodyLength);
|
|
40
|
+
headers["user-agent"] = "codex-slot/0.1.1";
|
|
41
|
+
if (accountIdHeader) {
|
|
42
|
+
headers["chatgpt-account-id"] = accountIdHeader;
|
|
43
|
+
}
|
|
44
|
+
return headers;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 向 Codex responses 上游发送一次请求。
|
|
48
|
+
*
|
|
49
|
+
* 业务含义:
|
|
50
|
+
* 1. 该 Adapter 隔离 undici 与上游 URL 细节,代理重试服务不直接依赖 HTTP 客户端实现。
|
|
51
|
+
* 2. 调用方负责决定 access token、账号切换与失败策略。
|
|
52
|
+
*
|
|
53
|
+
* @param options 上游请求参数。
|
|
54
|
+
* @returns undici 上游响应对象。
|
|
55
|
+
* @throws 当网络层或 undici 请求失败时透传底层异常。
|
|
56
|
+
*/
|
|
57
|
+
async function sendCodexResponsesRequest(options) {
|
|
58
|
+
return await (0, undici_1.request)(`${options.codexBaseUrl}/responses`, {
|
|
59
|
+
method: "POST",
|
|
60
|
+
headers: buildUpstreamHeaders(options.requestHeaders, options.accessToken, options.body.length, options.accountIdHeader),
|
|
61
|
+
body: options.body
|
|
62
|
+
});
|
|
63
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveBlockWindow = resolveBlockWindow;
|
|
4
|
+
exports.extractErrorCode = extractErrorCode;
|
|
5
|
+
exports.isNetworkUnavailableError = isNetworkUnavailableError;
|
|
6
|
+
exports.buildNetworkUnavailablePayload = buildNetworkUnavailablePayload;
|
|
7
|
+
exports.isUsageLimitErrorText = isUsageLimitErrorText;
|
|
8
|
+
const text_1 = require("./text");
|
|
9
|
+
/**
|
|
10
|
+
* 根据错误文本与当前账号状态,决定本地禁用时长。
|
|
11
|
+
*
|
|
12
|
+
* 业务规则:
|
|
13
|
+
* 1. 周限制优先,直到周窗口重置时间。
|
|
14
|
+
* 2. 5 小时额度限制次之,直到 5 小时窗口重置时间。
|
|
15
|
+
* 3. 未能明确识别时,按 5 分钟临时熔断处理。
|
|
16
|
+
*
|
|
17
|
+
* @param picked 当前被选中的账号及状态。
|
|
18
|
+
* @param errorText 上游返回的错误文本。
|
|
19
|
+
* @returns 本地禁用窗口与原因。
|
|
20
|
+
* @throws 无显式抛出。
|
|
21
|
+
*/
|
|
22
|
+
function resolveBlockWindow(picked, errorText) {
|
|
23
|
+
const lowerText = errorText.toLowerCase();
|
|
24
|
+
if (lowerText.includes("weekly") ||
|
|
25
|
+
lowerText.includes("7 day") ||
|
|
26
|
+
lowerText.includes("7-day") ||
|
|
27
|
+
picked.status.isWeeklyLimited) {
|
|
28
|
+
return {
|
|
29
|
+
until: picked.status.weeklyResetsAt ?? Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60,
|
|
30
|
+
reason: "weekly_limited"
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
if (lowerText.includes("5 hour") ||
|
|
34
|
+
lowerText.includes("5-hour") ||
|
|
35
|
+
lowerText.includes("5h") ||
|
|
36
|
+
lowerText.includes("usage limit") ||
|
|
37
|
+
picked.status.isFiveHourLimited) {
|
|
38
|
+
return {
|
|
39
|
+
until: picked.status.fiveHourResetsAt ?? Math.floor(Date.now() / 1000) + 5 * 60,
|
|
40
|
+
reason: "5h_limited"
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
until: Math.floor(Date.now() / 1000) + 5 * 60,
|
|
45
|
+
reason: "temporary_5m_limit"
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* 提取错误对象中最接近底层网络层的错误码。
|
|
50
|
+
*
|
|
51
|
+
* @param error 捕获到的异常对象。
|
|
52
|
+
* @returns 错误码;若无法识别则返回 `null`。
|
|
53
|
+
* @throws 无显式抛出。
|
|
54
|
+
*/
|
|
55
|
+
function extractErrorCode(error) {
|
|
56
|
+
if (!error || typeof error !== "object") {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const errnoError = error;
|
|
60
|
+
if (typeof errnoError.code === "string" && errnoError.code.length > 0) {
|
|
61
|
+
return errnoError.code;
|
|
62
|
+
}
|
|
63
|
+
return extractErrorCode(errnoError.cause);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* 判断一次请求失败是否属于本机到上游之间的网络不可达场景。
|
|
67
|
+
*
|
|
68
|
+
* @param error 捕获到的异常对象。
|
|
69
|
+
* @returns `true` 表示网络层异常,不应写入账号熔断;否则返回 `false`。
|
|
70
|
+
* @throws 无显式抛出。
|
|
71
|
+
*/
|
|
72
|
+
function isNetworkUnavailableError(error) {
|
|
73
|
+
const errorCode = extractErrorCode(error);
|
|
74
|
+
return [
|
|
75
|
+
"ECONNREFUSED",
|
|
76
|
+
"ECONNRESET",
|
|
77
|
+
"EHOSTUNREACH",
|
|
78
|
+
"ENETDOWN",
|
|
79
|
+
"ENETUNREACH",
|
|
80
|
+
"ENOTFOUND",
|
|
81
|
+
"EAI_AGAIN",
|
|
82
|
+
"ETIMEDOUT",
|
|
83
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
84
|
+
"UND_ERR_SOCKET"
|
|
85
|
+
].includes(errorCode ?? "");
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* 将网络层异常转换为统一的响应体,避免误导成“当前没有可用账号”。
|
|
89
|
+
*
|
|
90
|
+
* @param accountId 当前尝试的账号标识。
|
|
91
|
+
* @param error 捕获到的异常对象。
|
|
92
|
+
* @returns 统一的网络异常响应体。
|
|
93
|
+
* @throws 无显式抛出。
|
|
94
|
+
*/
|
|
95
|
+
function buildNetworkUnavailablePayload(accountId, error) {
|
|
96
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
97
|
+
return {
|
|
98
|
+
error: {
|
|
99
|
+
message: (0, text_1.bi)(`网络不可用,账号 ${accountId} 无法连接上游: ${message}`, `Network unavailable. Account ${accountId} cannot reach upstream: ${message}`),
|
|
100
|
+
type: "network_unavailable"
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* 判断错误响应文本是否表示上游额度限制。
|
|
106
|
+
*
|
|
107
|
+
* @param errorText 上游返回的错误文本。
|
|
108
|
+
* @returns `true` 表示可按额度限制处理并切换账号。
|
|
109
|
+
* @throws 无显式抛出。
|
|
110
|
+
*/
|
|
111
|
+
function isUsageLimitErrorText(errorText) {
|
|
112
|
+
const lowerText = errorText.toLowerCase();
|
|
113
|
+
return lowerText.includes("usage limit") || lowerText.includes("try again later");
|
|
114
|
+
}
|
package/dist/usage-sync.js
CHANGED
|
@@ -12,6 +12,12 @@ const state_1 = require("./state");
|
|
|
12
12
|
const text_1 = require("./text");
|
|
13
13
|
const USAGE_CACHE_TTL_MS = 60 * 1000;
|
|
14
14
|
const inflightUsageRefreshes = new Map();
|
|
15
|
+
const SHORT_LIVED_ACCOUNT_BLOCK_REASONS = new Set([
|
|
16
|
+
"request_failed",
|
|
17
|
+
"upstream_5xx",
|
|
18
|
+
"temporary_5m_limit",
|
|
19
|
+
"token_refresh_failed"
|
|
20
|
+
]);
|
|
15
21
|
function normalizeResetAt(value, resetAfterSeconds) {
|
|
16
22
|
if (typeof value === "number" && Number.isFinite(value)) {
|
|
17
23
|
return value;
|
|
@@ -21,6 +27,21 @@ function normalizeResetAt(value, resetAfterSeconds) {
|
|
|
21
27
|
}
|
|
22
28
|
return null;
|
|
23
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* 当账号已经成功完成鉴权或额度刷新时,清理与瞬时异常相关的本地熔断。
|
|
32
|
+
*
|
|
33
|
+
* 只会移除短期失败类熔断,不会误清理 5 小时或周额度限制。
|
|
34
|
+
*
|
|
35
|
+
* @param accountId 账号标识。
|
|
36
|
+
* @returns 无返回值。
|
|
37
|
+
*/
|
|
38
|
+
function clearShortLivedAccountBlock(accountId) {
|
|
39
|
+
const block = (0, state_1.getAccountBlock)(accountId);
|
|
40
|
+
if (!block || !SHORT_LIVED_ACCOUNT_BLOCK_REASONS.has(block.reason)) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
(0, state_1.clearAccountBlock)(accountId);
|
|
44
|
+
}
|
|
24
45
|
/**
|
|
25
46
|
* 将额度刷新异常归类为可直接展示在 `status` 表格中的状态码。
|
|
26
47
|
*
|
|
@@ -96,6 +117,7 @@ async function refreshAccountTokens(accountId) {
|
|
|
96
117
|
last_refresh: new Date().toISOString()
|
|
97
118
|
};
|
|
98
119
|
(0, account_store_1.writeAuthFile)(account.codex_home, nextAuth);
|
|
120
|
+
clearShortLivedAccountBlock(accountId);
|
|
99
121
|
return nextAuth;
|
|
100
122
|
}
|
|
101
123
|
/**
|
|
@@ -149,6 +171,7 @@ async function refreshAccountUsage(accountId) {
|
|
|
149
171
|
};
|
|
150
172
|
(0, state_1.setUsageCache)(result);
|
|
151
173
|
(0, state_1.clearUsageRefreshError)(accountId);
|
|
174
|
+
clearShortLivedAccountBlock(accountId);
|
|
152
175
|
return result;
|
|
153
176
|
}
|
|
154
177
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-slot",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.20",
|
|
4
4
|
"description": "本地 Codex 多账号切换与状态管理工具",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "dist/cli.js",
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
"dist"
|
|
13
13
|
],
|
|
14
14
|
"scripts": {
|
|
15
|
-
"clean": "
|
|
16
|
-
"build": "npm run clean && tsc -p tsconfig.json &&
|
|
15
|
+
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
16
|
+
"build": "npm run clean && tsc -p tsconfig.json && node -e \"const fs=require('node:fs'); for (const file of ['dist/cli.js','dist/serve.js']) { try { fs.chmodSync(file, 0o755); } catch {} }\"",
|
|
17
17
|
"prepublishOnly": "npm run build",
|
|
18
18
|
"dev": "tsx src/cli.ts",
|
|
19
19
|
"check": "tsc --noEmit -p tsconfig.json",
|