codex-slot 0.1.17 → 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/account-store.js +45 -14
- 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
package/dist/account-store.js
CHANGED
|
@@ -71,15 +71,50 @@ function resolveEmailFromAuth(auth) {
|
|
|
71
71
|
return undefined;
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* 将来源 HOME 下的账号级 `.auth.json` 文件集合镜像到目标 HOME。
|
|
76
|
+
*
|
|
77
|
+
* 业务含义:
|
|
78
|
+
* 1. 仅同步 `accounts` 目录下的账号级认证文件。
|
|
79
|
+
* 2. 来源不存在某个 `.auth.json` 时,会删除目标中的同名残留,避免旧登录态混入。
|
|
80
|
+
* 3. 来源缺少 `accounts` 目录时,视为没有任何账号级认证文件。
|
|
81
|
+
*
|
|
82
|
+
* @param sourceAccountsDir 来源 `accounts` 目录绝对路径。
|
|
83
|
+
* @param targetAccountsDir 目标 `accounts` 目录绝对路径。
|
|
84
|
+
* @returns 无返回值。
|
|
85
|
+
* @throws 当文件复制或删除失败时透传文件系统错误。
|
|
86
|
+
*/
|
|
87
|
+
function syncAccountAuthFiles(sourceAccountsDir, targetAccountsDir) {
|
|
88
|
+
const sourceAuthFiles = new Set();
|
|
89
|
+
if (node_fs_1.default.existsSync(sourceAccountsDir)) {
|
|
90
|
+
for (const entry of node_fs_1.default.readdirSync(sourceAccountsDir, { withFileTypes: true })) {
|
|
91
|
+
if (!entry.isFile() || !entry.name.endsWith(".auth.json")) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
sourceAuthFiles.add(entry.name);
|
|
95
|
+
node_fs_1.default.copyFileSync(node_path_1.default.join(sourceAccountsDir, entry.name), node_path_1.default.join(targetAccountsDir, entry.name));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
for (const entry of node_fs_1.default.readdirSync(targetAccountsDir, { withFileTypes: true })) {
|
|
99
|
+
if (!entry.isFile() || !entry.name.endsWith(".auth.json")) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (!sourceAuthFiles.has(entry.name)) {
|
|
103
|
+
node_fs_1.default.rmSync(node_path_1.default.join(targetAccountsDir, entry.name), { force: true });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
74
107
|
/**
|
|
75
108
|
* 将来源 HOME 下的官方 `.codex` 登录态复制到目标 HOME。
|
|
76
109
|
*
|
|
77
110
|
* 只复制认证和账号元数据所需文件,不复制历史日志、缓存等无关内容。
|
|
111
|
+
* 其中 `auth.json` 为必需文件,`accounts/registry.json` 与账号级 `.auth.json`
|
|
112
|
+
* 允许缺失;缺失时会同步删除目标中的对应残留文件,避免旧缓存污染当前登录态。
|
|
78
113
|
*
|
|
79
114
|
* @param sourceHome 来源 HOME 目录。
|
|
80
115
|
* @param targetHome 目标 HOME 目录。
|
|
81
116
|
* @returns 无返回值。
|
|
82
|
-
* @throws
|
|
117
|
+
* @throws 当来源目录缺少 `auth.json` 或复制失败时抛出错误。
|
|
83
118
|
*/
|
|
84
119
|
function cloneCodexAuthState(sourceHome, targetHome) {
|
|
85
120
|
const sourceCodexDir = getCodexDataDir(sourceHome);
|
|
@@ -87,25 +122,21 @@ function cloneCodexAuthState(sourceHome, targetHome) {
|
|
|
87
122
|
const sourceAuthPath = node_path_1.default.join(sourceCodexDir, "auth.json");
|
|
88
123
|
const sourceAccountsDir = node_path_1.default.join(sourceCodexDir, "accounts");
|
|
89
124
|
const sourceRegistryPath = node_path_1.default.join(sourceAccountsDir, "registry.json");
|
|
125
|
+
const targetAccountsDir = node_path_1.default.join(targetCodexDir, "accounts");
|
|
126
|
+
const targetRegistryPath = node_path_1.default.join(targetAccountsDir, "registry.json");
|
|
90
127
|
if (!node_fs_1.default.existsSync(sourceAuthPath)) {
|
|
91
128
|
throw new Error((0, text_1.bi)(`来源目录缺少 auth.json: ${sourceAuthPath}`, `Source directory is missing auth.json: ${sourceAuthPath}`));
|
|
92
129
|
}
|
|
93
|
-
if (!node_fs_1.default.existsSync(sourceRegistryPath)) {
|
|
94
|
-
throw new Error((0, text_1.bi)(`来源目录缺少 registry.json: ${sourceRegistryPath}`, `Source directory is missing registry.json: ${sourceRegistryPath}`));
|
|
95
|
-
}
|
|
96
130
|
node_fs_1.default.mkdirSync(targetCodexDir, { recursive: true });
|
|
97
|
-
node_fs_1.default.mkdirSync(
|
|
131
|
+
node_fs_1.default.mkdirSync(targetAccountsDir, { recursive: true });
|
|
98
132
|
node_fs_1.default.copyFileSync(sourceAuthPath, node_path_1.default.join(targetCodexDir, "auth.json"));
|
|
99
|
-
node_fs_1.default.
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
if (!entry.name.endsWith(".auth.json")) {
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
node_fs_1.default.copyFileSync(node_path_1.default.join(sourceAccountsDir, entry.name), node_path_1.default.join(targetCodexDir, "accounts", entry.name));
|
|
133
|
+
if (node_fs_1.default.existsSync(sourceRegistryPath)) {
|
|
134
|
+
node_fs_1.default.copyFileSync(sourceRegistryPath, targetRegistryPath);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
node_fs_1.default.rmSync(targetRegistryPath, { force: true });
|
|
108
138
|
}
|
|
139
|
+
syncAccountAuthFiles(sourceAccountsDir, targetAccountsDir);
|
|
109
140
|
}
|
|
110
141
|
/**
|
|
111
142
|
* 检查某个 HOME 下的官方登录态是否完整。
|
|
@@ -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
|
*
|