codex-slot 0.1.18 → 0.1.19
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 +4 -0
- package/dist/scheduler.js +166 -12
- package/dist/server.js +1 -0
- package/dist/state.js +36 -0
- package/package.json +1 -1
|
@@ -116,6 +116,10 @@ function renameAccount(oldName, newName) {
|
|
|
116
116
|
};
|
|
117
117
|
delete state.usage_cache[oldName];
|
|
118
118
|
}
|
|
119
|
+
if (state.scheduler_stats[oldName]) {
|
|
120
|
+
state.scheduler_stats[newName] = state.scheduler_stats[oldName];
|
|
121
|
+
delete state.scheduler_stats[oldName];
|
|
122
|
+
}
|
|
119
123
|
(0, state_1.saveState)(state);
|
|
120
124
|
return renamedAccount;
|
|
121
125
|
}
|
package/dist/scheduler.js
CHANGED
|
@@ -4,6 +4,12 @@ exports.pickBestAccount = pickBestAccount;
|
|
|
4
4
|
exports.listCandidateAccounts = listCandidateAccounts;
|
|
5
5
|
const config_1 = require("./config");
|
|
6
6
|
const status_1 = require("./status");
|
|
7
|
+
const state_1 = require("./state");
|
|
8
|
+
const CRITICAL_WEEKLY_LEFT_PERCENT = 5;
|
|
9
|
+
const LOW_WEEKLY_LEFT_PERCENT = 15;
|
|
10
|
+
const WASTE_HORIZON_SECONDS = 5 * 60 * 60;
|
|
11
|
+
const WEEKLY_WASTE_HORIZON_SECONDS = 7 * 24 * 60 * 60;
|
|
12
|
+
const RECENT_USE_RECOVERY_SECONDS = 30 * 60;
|
|
7
13
|
function nextResetWeight(resetAt) {
|
|
8
14
|
if (!resetAt) {
|
|
9
15
|
return Number.MAX_SAFE_INTEGER;
|
|
@@ -32,23 +38,171 @@ function isSoftLocalBlocked(status) {
|
|
|
32
38
|
].includes(status.localBlockReason ?? "");
|
|
33
39
|
}
|
|
34
40
|
/**
|
|
35
|
-
*
|
|
41
|
+
* 将百分比字段归一化为 0 到 1 的评分,缺失时按中性值处理。
|
|
42
|
+
*
|
|
43
|
+
* @param value 原始百分比。
|
|
44
|
+
* @returns 归一化后的评分。
|
|
45
|
+
*/
|
|
46
|
+
function normalizePercent(value) {
|
|
47
|
+
if (value === null || value === undefined || Number.isNaN(value)) {
|
|
48
|
+
return 0.5;
|
|
49
|
+
}
|
|
50
|
+
return Math.max(0, Math.min(1, value / 100));
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 计算 5 小时窗口的余额浪费压力,越接近重置且剩余额度越多分数越高。
|
|
54
|
+
*
|
|
55
|
+
* @param status 账号运行时状态。
|
|
56
|
+
* @returns 0 到 1 之间的浪费压力评分。
|
|
57
|
+
*/
|
|
58
|
+
function computeFiveHourWastePressure(status) {
|
|
59
|
+
const leftScore = normalizePercent(status.fiveHourLeftPercent);
|
|
60
|
+
if (!status.fiveHourResetsAt) {
|
|
61
|
+
return leftScore * 0.2;
|
|
62
|
+
}
|
|
63
|
+
const secondsUntilReset = status.fiveHourResetsAt - Math.floor(Date.now() / 1000);
|
|
64
|
+
if (secondsUntilReset <= 0) {
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
const urgency = Math.max(0.1, Math.min(1, (WASTE_HORIZON_SECONDS - secondsUntilReset) / WASTE_HORIZON_SECONDS));
|
|
68
|
+
return leftScore * urgency;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* 计算周窗口的余额浪费压力,周额度越接近重置且剩余越多分数越高。
|
|
72
|
+
*
|
|
73
|
+
* @param status 账号运行时状态。
|
|
74
|
+
* @returns 0 到 1 之间的周窗口浪费压力评分。
|
|
75
|
+
*/
|
|
76
|
+
function computeWeeklyWastePressure(status) {
|
|
77
|
+
const leftScore = normalizePercent(status.weeklyLeftPercent);
|
|
78
|
+
if (!status.weeklyResetsAt) {
|
|
79
|
+
return leftScore * 0.1;
|
|
80
|
+
}
|
|
81
|
+
const secondsUntilReset = status.weeklyResetsAt - Math.floor(Date.now() / 1000);
|
|
82
|
+
if (secondsUntilReset <= 0) {
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
const urgency = Math.max(0.05, Math.min(1, (WEEKLY_WASTE_HORIZON_SECONDS - secondsUntilReset) / WEEKLY_WASTE_HORIZON_SECONDS));
|
|
86
|
+
return leftScore * urgency;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* 计算 5 小时窗口调度时可借用的周额度承载系数,周余额越低越抑制短窗口冲动。
|
|
90
|
+
*
|
|
91
|
+
* @param status 账号运行时状态。
|
|
92
|
+
* @returns 0 到 1 之间的周额度承载系数。
|
|
93
|
+
*/
|
|
94
|
+
function computeWeeklyCapacityFactor(status) {
|
|
95
|
+
const weeklyLeft = status.weeklyLeftPercent;
|
|
96
|
+
if (weeklyLeft === null || weeklyLeft === undefined || Number.isNaN(weeklyLeft)) {
|
|
97
|
+
return 0.7;
|
|
98
|
+
}
|
|
99
|
+
if (weeklyLeft <= CRITICAL_WEEKLY_LEFT_PERCENT) {
|
|
100
|
+
return 0.05;
|
|
101
|
+
}
|
|
102
|
+
if (weeklyLeft < LOW_WEEKLY_LEFT_PERCENT) {
|
|
103
|
+
return 0.2;
|
|
104
|
+
}
|
|
105
|
+
if (weeklyLeft < 30) {
|
|
106
|
+
return 0.55;
|
|
107
|
+
}
|
|
108
|
+
return 1;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* 计算周额度健康度,低于保护线时非线性降权,避免个别账号过早打穿周窗口。
|
|
112
|
+
*
|
|
113
|
+
* @param status 账号运行时状态。
|
|
114
|
+
* @returns 0 到 1 之间的周额度健康评分。
|
|
115
|
+
*/
|
|
116
|
+
function computeWeeklyHealthScore(status) {
|
|
117
|
+
const weeklyLeft = status.weeklyLeftPercent;
|
|
118
|
+
if (weeklyLeft === null || weeklyLeft === undefined || Number.isNaN(weeklyLeft)) {
|
|
119
|
+
return 0.5;
|
|
120
|
+
}
|
|
121
|
+
if (weeklyLeft <= CRITICAL_WEEKLY_LEFT_PERCENT) {
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
if (weeklyLeft < LOW_WEEKLY_LEFT_PERCENT) {
|
|
125
|
+
return normalizePercent(weeklyLeft) * 0.35;
|
|
126
|
+
}
|
|
127
|
+
return normalizePercent(weeklyLeft);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* 计算账号使用分散度,成功次数更少且最近未使用的账号分数更高。
|
|
131
|
+
*
|
|
132
|
+
* @param status 账号运行时状态。
|
|
133
|
+
* @param minSuccessCount 当前候选账号中的最小成功次数。
|
|
134
|
+
* @param maxSuccessCount 当前候选账号中的最大成功次数。
|
|
135
|
+
* @returns 0 到 1 之间的分散调度评分。
|
|
136
|
+
*/
|
|
137
|
+
function computeSpreadScore(status, minSuccessCount, maxSuccessCount) {
|
|
138
|
+
const stats = (0, state_1.getSchedulerStats)(status.id);
|
|
139
|
+
const countRange = Math.max(1, maxSuccessCount - minSuccessCount);
|
|
140
|
+
const countScore = 1 - (stats.success_count - minSuccessCount) / countRange;
|
|
141
|
+
if (!stats.last_success_at) {
|
|
142
|
+
return Math.max(0, Math.min(1, countScore * 0.6 + 0.4));
|
|
143
|
+
}
|
|
144
|
+
const secondsSinceLastUse = (Date.now() - new Date(stats.last_success_at).getTime()) / 1000;
|
|
145
|
+
const recencyScore = Math.max(0, Math.min(1, secondsSinceLastUse / RECENT_USE_RECOVERY_SECONDS));
|
|
146
|
+
return Math.max(0, Math.min(1, countScore * 0.6 + recencyScore * 0.4));
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* 计算候选账号的综合调度评分。
|
|
150
|
+
*
|
|
151
|
+
* 业务含义:
|
|
152
|
+
* 1. 周窗口承担主防浪费压力,快重置且余额多的账号优先。
|
|
153
|
+
* 2. 5 小时窗口在周额度健康时参与放大,低周余额会抑制短窗口冲动。
|
|
154
|
+
* 3. 本地成功使用历史用于打散连续请求,降低单账号被持续命中的概率。
|
|
155
|
+
*
|
|
156
|
+
* @param status 账号运行时状态。
|
|
157
|
+
* @param minSuccessCount 当前候选账号中的最小成功次数。
|
|
158
|
+
* @param maxSuccessCount 当前候选账号中的最大成功次数。
|
|
159
|
+
* @returns 综合评分,分数越高越优先。
|
|
160
|
+
*/
|
|
161
|
+
function computeScheduleScore(status, minSuccessCount, maxSuccessCount) {
|
|
162
|
+
const weeklyWasteScore = computeWeeklyWastePressure(status);
|
|
163
|
+
const fiveHourWasteScore = computeFiveHourWastePressure(status) * computeWeeklyCapacityFactor(status);
|
|
164
|
+
const weeklyHealthScore = computeWeeklyHealthScore(status);
|
|
165
|
+
const fiveHourLeftScore = normalizePercent(status.fiveHourLeftPercent);
|
|
166
|
+
const spreadScore = computeSpreadScore(status, minSuccessCount, maxSuccessCount);
|
|
167
|
+
return (weeklyWasteScore * 0.5 +
|
|
168
|
+
fiveHourWasteScore * 0.25 +
|
|
169
|
+
weeklyHealthScore * 0.1 +
|
|
170
|
+
spreadScore * 0.1 +
|
|
171
|
+
fiveHourLeftScore * 0.05);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* 对候选账号按防浪费、周额度保护与均匀使用策略排序。
|
|
36
175
|
*
|
|
37
176
|
* @param statuses 待排序的账号状态列表。
|
|
38
177
|
* @returns 排序后的账号状态列表,优先返回更适合尝试的账号。
|
|
39
178
|
*/
|
|
40
179
|
function rankEligibleStatuses(statuses) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
180
|
+
const primaryPool = statuses.some((item) => item.weeklyLeftPercent === null ||
|
|
181
|
+
item.weeklyLeftPercent === undefined ||
|
|
182
|
+
item.weeklyLeftPercent > CRITICAL_WEEKLY_LEFT_PERCENT)
|
|
183
|
+
? statuses.filter((item) => item.weeklyLeftPercent === null ||
|
|
184
|
+
item.weeklyLeftPercent === undefined ||
|
|
185
|
+
item.weeklyLeftPercent > CRITICAL_WEEKLY_LEFT_PERCENT)
|
|
186
|
+
: statuses;
|
|
187
|
+
const deferredPool = statuses.filter((item) => !primaryPool.includes(item));
|
|
188
|
+
const rankPool = (items) => {
|
|
189
|
+
const successCounts = items.map((item) => (0, state_1.getSchedulerStats)(item.id).success_count);
|
|
190
|
+
const minSuccessCount = Math.min(...successCounts, 0);
|
|
191
|
+
const maxSuccessCount = Math.max(...successCounts, 0);
|
|
192
|
+
return [...items].sort((left, right) => {
|
|
193
|
+
const scoreDiff = computeScheduleScore(right, minSuccessCount, maxSuccessCount) -
|
|
194
|
+
computeScheduleScore(left, minSuccessCount, maxSuccessCount);
|
|
195
|
+
if (Math.abs(scoreDiff) > Number.EPSILON) {
|
|
196
|
+
return scoreDiff;
|
|
197
|
+
}
|
|
198
|
+
const resetDiff = nextResetWeight(left.fiveHourResetsAt) - nextResetWeight(right.fiveHourResetsAt);
|
|
199
|
+
if (resetDiff !== 0) {
|
|
200
|
+
return resetDiff;
|
|
201
|
+
}
|
|
202
|
+
return (right.weeklyLeftPercent ?? -1) - (left.weeklyLeftPercent ?? -1);
|
|
203
|
+
});
|
|
204
|
+
};
|
|
205
|
+
return [...rankPool(primaryPool), ...rankPool(deferredPool)];
|
|
52
206
|
}
|
|
53
207
|
/**
|
|
54
208
|
* 选择当前最适合激活的账号。
|
package/dist/server.js
CHANGED
|
@@ -355,6 +355,7 @@ async function startServer(port) {
|
|
|
355
355
|
if (typeof cacheControl === "string") {
|
|
356
356
|
headers["cache-control"] = cacheControl;
|
|
357
357
|
}
|
|
358
|
+
(0, state_1.recordAccountScheduleSuccess)(picked.account.id);
|
|
358
359
|
headers.connection = "keep-alive";
|
|
359
360
|
reply.raw.writeHead(upstream.statusCode, headers);
|
|
360
361
|
for await (const chunk of upstream.body) {
|
package/dist/state.js
CHANGED
|
@@ -13,6 +13,8 @@ exports.getUsageCache = getUsageCache;
|
|
|
13
13
|
exports.setUsageRefreshError = setUsageRefreshError;
|
|
14
14
|
exports.clearUsageRefreshError = clearUsageRefreshError;
|
|
15
15
|
exports.getUsageRefreshError = getUsageRefreshError;
|
|
16
|
+
exports.getSchedulerStats = getSchedulerStats;
|
|
17
|
+
exports.recordAccountScheduleSuccess = recordAccountScheduleSuccess;
|
|
16
18
|
exports.getManagedCodexConfigState = getManagedCodexConfigState;
|
|
17
19
|
exports.getManagedCodexAuthState = getManagedCodexAuthState;
|
|
18
20
|
exports.setManagedCodexConfigState = setManagedCodexConfigState;
|
|
@@ -37,6 +39,7 @@ function loadState() {
|
|
|
37
39
|
account_blocks: {},
|
|
38
40
|
usage_cache: {},
|
|
39
41
|
usage_refresh_errors: {},
|
|
42
|
+
scheduler_stats: {},
|
|
40
43
|
managed_codex_auth: null,
|
|
41
44
|
managed_codex_config: null
|
|
42
45
|
};
|
|
@@ -48,6 +51,7 @@ function loadState() {
|
|
|
48
51
|
account_blocks: {},
|
|
49
52
|
usage_cache: {},
|
|
50
53
|
usage_refresh_errors: {},
|
|
54
|
+
scheduler_stats: {},
|
|
51
55
|
managed_codex_auth: null,
|
|
52
56
|
managed_codex_config: null
|
|
53
57
|
};
|
|
@@ -55,6 +59,7 @@ function loadState() {
|
|
|
55
59
|
account_blocks: parsed.account_blocks ?? {},
|
|
56
60
|
usage_cache: parsed.usage_cache ?? {},
|
|
57
61
|
usage_refresh_errors: parsed.usage_refresh_errors ?? {},
|
|
62
|
+
scheduler_stats: parsed.scheduler_stats ?? {},
|
|
58
63
|
managed_codex_auth: parsed.managed_codex_auth ?? null,
|
|
59
64
|
managed_codex_config: parsed.managed_codex_config ?? null
|
|
60
65
|
};
|
|
@@ -172,6 +177,37 @@ function getUsageRefreshError(accountId) {
|
|
|
172
177
|
const state = loadState();
|
|
173
178
|
return state.usage_refresh_errors[accountId] ?? null;
|
|
174
179
|
}
|
|
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
|
+
}
|
|
175
211
|
/**
|
|
176
212
|
* 读取当前记录的 Codex `config.toml` 接管快照。
|
|
177
213
|
*
|