stepfun-status 1.0.4 → 1.0.5
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/LICENSE +21 -21
- package/README.md +190 -175
- package/cli/api.js +408 -345
- package/package.json +1 -1
- package/ARCHITECTURE.md +0 -175
package/cli/api.js
CHANGED
|
@@ -1,345 +1,408 @@
|
|
|
1
|
-
const https = require("https");
|
|
2
|
-
const fs = require("fs");
|
|
3
|
-
const path = require("path");
|
|
4
|
-
const os = require("os");
|
|
5
|
-
|
|
6
|
-
// ─── 常量 ─────────────────────────────────────────────────────────────────────
|
|
7
|
-
|
|
8
|
-
const STEPFUN_HOST = "platform.stepfun.com";
|
|
9
|
-
const STEPFUN_ORIGIN = `https://${STEPFUN_HOST}`;
|
|
10
|
-
|
|
11
|
-
const CONFIG = {
|
|
12
|
-
CACHE_TIMEOUT: 30000, // 缓存超时:30 秒
|
|
13
|
-
REQUEST_TIMEOUT: 8000, // 网络请求超时:8 秒
|
|
14
|
-
CONFIG_FILENAME: ".stepfun-config.json",
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
// ─── 跨平台 User-Agent ────────────────────────────────────────────────────────
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* 根据当前操作系统返回对应的浏览器 User-Agent 字符串,
|
|
21
|
-
* 避免 macOS/Linux 下使用 Windows UA 被服务端检测。
|
|
22
|
-
*/
|
|
23
|
-
function getPlatformUserAgent() {
|
|
24
|
-
const platform = os.platform();
|
|
25
|
-
if (platform === "darwin") {
|
|
26
|
-
return "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36";
|
|
27
|
-
}
|
|
28
|
-
if (platform === "linux") {
|
|
29
|
-
return "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36";
|
|
30
|
-
}
|
|
31
|
-
// Windows(默认)
|
|
32
|
-
return "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36";
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// 配置文件路径(模块级常量,供外部共享,避免各处重复计算)
|
|
36
|
-
const CONFIG_PATH = path.join(
|
|
37
|
-
process.env.HOME || process.env.USERPROFILE,
|
|
38
|
-
CONFIG.CONFIG_FILENAME
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
// ─── 模块级纯函数(提升出 parseUsageData,避免每次调用重新创建) ──────────────
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* 将 Unix 时间戳转换为剩余时间对象
|
|
45
|
-
* @param {string|number|null} timestamp Unix 秒级时间戳
|
|
46
|
-
* @returns {{ ts: number, date: string, hoursUntil: number, minutesUntil: number } | null}
|
|
47
|
-
*/
|
|
48
|
-
function formatResetTime(timestamp) {
|
|
49
|
-
if (!timestamp) return null;
|
|
50
|
-
const ts = parseInt(timestamp, 10);
|
|
51
|
-
const date = new Date(ts * 1000);
|
|
52
|
-
const diffMs = date.getTime() - Date.now();
|
|
53
|
-
return {
|
|
54
|
-
ts,
|
|
55
|
-
date: date.toLocaleString(),
|
|
56
|
-
hoursUntil: Math.max(0, Math.floor(diffMs / (1000 * 60 * 60))),
|
|
57
|
-
minutesUntil: Math.max(0, Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60))),
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* 将剩余时间对象格式化为可读文本
|
|
63
|
-
* @param {{ hoursUntil: number, minutesUntil: number } | null} resetInfo
|
|
64
|
-
* @returns {string}
|
|
65
|
-
*/
|
|
66
|
-
function calcRemainingText(resetInfo) {
|
|
67
|
-
if (!resetInfo) return "未知";
|
|
68
|
-
const { hoursUntil, minutesUntil } = resetInfo;
|
|
69
|
-
return hoursUntil > 0
|
|
70
|
-
? `${hoursUntil} 小时 ${minutesUntil} 分钟后重置`
|
|
71
|
-
: `${minutesUntil} 分钟后重置`;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// ───
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
1
|
+
const https = require("https");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const os = require("os");
|
|
5
|
+
|
|
6
|
+
// ─── 常量 ─────────────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
const STEPFUN_HOST = "platform.stepfun.com";
|
|
9
|
+
const STEPFUN_ORIGIN = `https://${STEPFUN_HOST}`;
|
|
10
|
+
|
|
11
|
+
const CONFIG = {
|
|
12
|
+
CACHE_TIMEOUT: 30000, // 缓存超时:30 秒
|
|
13
|
+
REQUEST_TIMEOUT: 8000, // 网络请求超时:8 秒
|
|
14
|
+
CONFIG_FILENAME: ".stepfun-config.json",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// ─── 跨平台 User-Agent ────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 根据当前操作系统返回对应的浏览器 User-Agent 字符串,
|
|
21
|
+
* 避免 macOS/Linux 下使用 Windows UA 被服务端检测。
|
|
22
|
+
*/
|
|
23
|
+
function getPlatformUserAgent() {
|
|
24
|
+
const platform = os.platform();
|
|
25
|
+
if (platform === "darwin") {
|
|
26
|
+
return "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36";
|
|
27
|
+
}
|
|
28
|
+
if (platform === "linux") {
|
|
29
|
+
return "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36";
|
|
30
|
+
}
|
|
31
|
+
// Windows(默认)
|
|
32
|
+
return "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 配置文件路径(模块级常量,供外部共享,避免各处重复计算)
|
|
36
|
+
const CONFIG_PATH = path.join(
|
|
37
|
+
process.env.HOME || process.env.USERPROFILE,
|
|
38
|
+
CONFIG.CONFIG_FILENAME
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
// ─── 模块级纯函数(提升出 parseUsageData,避免每次调用重新创建) ──────────────
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 将 Unix 时间戳转换为剩余时间对象
|
|
45
|
+
* @param {string|number|null} timestamp Unix 秒级时间戳
|
|
46
|
+
* @returns {{ ts: number, date: string, hoursUntil: number, minutesUntil: number } | null}
|
|
47
|
+
*/
|
|
48
|
+
function formatResetTime(timestamp) {
|
|
49
|
+
if (!timestamp) return null;
|
|
50
|
+
const ts = parseInt(timestamp, 10);
|
|
51
|
+
const date = new Date(ts * 1000);
|
|
52
|
+
const diffMs = date.getTime() - Date.now();
|
|
53
|
+
return {
|
|
54
|
+
ts,
|
|
55
|
+
date: date.toLocaleString(),
|
|
56
|
+
hoursUntil: Math.max(0, Math.floor(diffMs / (1000 * 60 * 60))),
|
|
57
|
+
minutesUntil: Math.max(0, Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60))),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 将剩余时间对象格式化为可读文本
|
|
63
|
+
* @param {{ hoursUntil: number, minutesUntil: number } | null} resetInfo
|
|
64
|
+
* @returns {string}
|
|
65
|
+
*/
|
|
66
|
+
function calcRemainingText(resetInfo) {
|
|
67
|
+
if (!resetInfo) return "未知";
|
|
68
|
+
const { hoursUntil, minutesUntil } = resetInfo;
|
|
69
|
+
return hoursUntil > 0
|
|
70
|
+
? `${hoursUntil} 小时 ${minutesUntil} 分钟后重置`
|
|
71
|
+
: `${minutesUntil} 分钟后重置`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ─── Cookie 合并工具函数 ──────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 将 Set-Cookie 响应头合并到现有 Cookie 字符串中(模拟浏览器行为)。
|
|
78
|
+
* 新值覆盖同名旧值,从而自动延长会话有效期。
|
|
79
|
+
*
|
|
80
|
+
* @param {string} existingCookie 当前 Cookie 字符串("k1=v1; k2=v2" 格式)
|
|
81
|
+
* @param {string[]} setCookieHeaders 响应头中的 Set-Cookie 数组
|
|
82
|
+
* @returns {string} 合并后的 Cookie 字符串
|
|
83
|
+
*/
|
|
84
|
+
function mergeCookies(existingCookie, setCookieHeaders) {
|
|
85
|
+
// 1. 解析现有 cookie 为 Map(保持插入顺序)
|
|
86
|
+
const cookieMap = new Map();
|
|
87
|
+
if (existingCookie) {
|
|
88
|
+
existingCookie.split(/;\s*/).forEach((c) => {
|
|
89
|
+
const idx = c.indexOf("=");
|
|
90
|
+
if (idx === -1) return;
|
|
91
|
+
const key = c.slice(0, idx).trim();
|
|
92
|
+
if (key) cookieMap.set(key, c.slice(idx + 1));
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 2. 从 Set-Cookie 头中提取 name=value(忽略 Path/Domain/Expires 等属性)
|
|
97
|
+
let updated = false;
|
|
98
|
+
for (const header of setCookieHeaders) {
|
|
99
|
+
// Set-Cookie 格式: "name=value; Path=/; Domain=...; Expires=..."
|
|
100
|
+
const firstPart = header.split(";")[0].trim();
|
|
101
|
+
const idx = firstPart.indexOf("=");
|
|
102
|
+
if (idx === -1) continue;
|
|
103
|
+
const key = firstPart.slice(0, idx).trim();
|
|
104
|
+
const val = firstPart.slice(idx + 1);
|
|
105
|
+
if (!key) continue;
|
|
106
|
+
const oldVal = cookieMap.get(key);
|
|
107
|
+
if (oldVal !== val) {
|
|
108
|
+
cookieMap.set(key, val);
|
|
109
|
+
updated = true;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!updated) return existingCookie;
|
|
114
|
+
|
|
115
|
+
// 3. 重新拼接
|
|
116
|
+
return Array.from(cookieMap.entries())
|
|
117
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
118
|
+
.join("; ");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ─── StepFunAPI 类 ────────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
class StepFunAPI {
|
|
124
|
+
constructor() {
|
|
125
|
+
this.cookie = null;
|
|
126
|
+
this.cache = {
|
|
127
|
+
statusData: null,
|
|
128
|
+
quotaData: null,
|
|
129
|
+
statusTimestamp: 0,
|
|
130
|
+
quotaTimestamp: 0,
|
|
131
|
+
};
|
|
132
|
+
this.loadConfig();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** 是否已配置凭据 */
|
|
136
|
+
isAuthenticated() {
|
|
137
|
+
return Boolean(this.cookie);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
loadConfig() {
|
|
141
|
+
try {
|
|
142
|
+
if (fs.existsSync(CONFIG_PATH)) {
|
|
143
|
+
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8"));
|
|
144
|
+
this.cookie = config.cookie || null;
|
|
145
|
+
}
|
|
146
|
+
} catch (error) {
|
|
147
|
+
console.error("Failed to load config:", error.message);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
saveConfig() {
|
|
152
|
+
try {
|
|
153
|
+
const config = { cookie: this.cookie };
|
|
154
|
+
// 0o600:仅所有者可读写,在 Unix/Mac 上防止其他用户读取 Cookie
|
|
155
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
156
|
+
} catch (error) {
|
|
157
|
+
console.error("Failed to save config:", error.message);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
setCredentials(cookie) {
|
|
162
|
+
if (!cookie || typeof cookie !== "string" || !cookie.trim()) {
|
|
163
|
+
throw new Error("Cookie 不能为空");
|
|
164
|
+
}
|
|
165
|
+
this.cookie = cookie.trim();
|
|
166
|
+
this.saveConfig();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** 从 Cookie 字符串中提取指定字段 */
|
|
170
|
+
extractFromCookie() {
|
|
171
|
+
if (!this.cookie) return { oasisWebid: null };
|
|
172
|
+
|
|
173
|
+
const cookies = {};
|
|
174
|
+
this.cookie.split(/;\s*/).forEach((c) => {
|
|
175
|
+
const idx = c.indexOf("=");
|
|
176
|
+
if (idx === -1) return;
|
|
177
|
+
const key = c.slice(0, idx).trim();
|
|
178
|
+
const val = c.slice(idx + 1);
|
|
179
|
+
if (!key) return;
|
|
180
|
+
try {
|
|
181
|
+
cookies[key] = decodeURIComponent(val);
|
|
182
|
+
} catch {
|
|
183
|
+
cookies[key] = val;
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
return { oasisWebid: cookies["Oasis-Webid"] || null };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async makePlatformRequest(endpoint, data = {}) {
|
|
191
|
+
if (!this.cookie) {
|
|
192
|
+
throw new Error(
|
|
193
|
+
'Missing cookie. Please run "stepfun auth <cookie>" first.\n' +
|
|
194
|
+
"获取方式:\n" +
|
|
195
|
+
"1. 登录 https://platform.stepfun.com/plan-subscribe\n" +
|
|
196
|
+
"2. F12 -> Network -> 刷新页面\n" +
|
|
197
|
+
"3. 点击任意 API 请求 -> Copy -> Copy request headers -> 复制 Cookie 行"
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const { oasisWebid } = this.extractFromCookie();
|
|
202
|
+
const postData = JSON.stringify(data);
|
|
203
|
+
|
|
204
|
+
const headers = {
|
|
205
|
+
accept: "*/*",
|
|
206
|
+
"content-type": "application/json",
|
|
207
|
+
"oasis-appid": "10300",
|
|
208
|
+
"oasis-platform": "web",
|
|
209
|
+
"oasis-webid": oasisWebid || "",
|
|
210
|
+
origin: STEPFUN_ORIGIN,
|
|
211
|
+
referer: `${STEPFUN_ORIGIN}/plan-subscribe`,
|
|
212
|
+
"user-agent": getPlatformUserAgent(),
|
|
213
|
+
"content-length": Buffer.byteLength(postData),
|
|
214
|
+
Cookie: this.cookie,
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
return new Promise((resolve, reject) => {
|
|
218
|
+
const req = https.request(
|
|
219
|
+
{
|
|
220
|
+
hostname: STEPFUN_HOST, // 与 STEPFUN_ORIGIN 保持同一数据源
|
|
221
|
+
port: 443,
|
|
222
|
+
path: endpoint,
|
|
223
|
+
method: "POST",
|
|
224
|
+
headers,
|
|
225
|
+
rejectUnauthorized: true, // 显式启用 SSL 证书验证
|
|
226
|
+
},
|
|
227
|
+
(res) => {
|
|
228
|
+
let body = "";
|
|
229
|
+
res.on("data", (chunk) => { body += chunk; });
|
|
230
|
+
res.on("end", () => {
|
|
231
|
+
// 自动捕获 Set-Cookie 响应头,合并更新本地 Cookie(模拟浏览器保活)
|
|
232
|
+
const setCookieHeaders = res.headers["set-cookie"];
|
|
233
|
+
if (setCookieHeaders && setCookieHeaders.length > 0 && this.cookie) {
|
|
234
|
+
const merged = mergeCookies(this.cookie, setCookieHeaders);
|
|
235
|
+
if (merged !== this.cookie) {
|
|
236
|
+
this.cookie = merged;
|
|
237
|
+
this.saveConfig();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (res.statusCode === 200) {
|
|
242
|
+
try {
|
|
243
|
+
resolve(JSON.parse(body));
|
|
244
|
+
} catch {
|
|
245
|
+
reject(new Error("Failed to parse response: invalid JSON"));
|
|
246
|
+
}
|
|
247
|
+
} else if (res.statusCode === 401) {
|
|
248
|
+
this.clearCache();
|
|
249
|
+
reject(
|
|
250
|
+
new Error(
|
|
251
|
+
"Cookie 已失效或过期,请重新运行 auth 命令\n" +
|
|
252
|
+
"获取方式: 登录 https://platform.stepfun.com/plan-subscribe → F12 → Network → 复制 Cookie"
|
|
253
|
+
)
|
|
254
|
+
);
|
|
255
|
+
} else {
|
|
256
|
+
reject(new Error(`API error: HTTP ${res.statusCode}`));
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
// 超时保护
|
|
263
|
+
req.setTimeout(CONFIG.REQUEST_TIMEOUT, () => {
|
|
264
|
+
req.destroy();
|
|
265
|
+
reject(new Error(`Request timeout after ${CONFIG.REQUEST_TIMEOUT / 1000}s`));
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
req.on("error", (e) => {
|
|
269
|
+
reject(new Error(`Network error: ${e.message}`));
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
req.write(postData);
|
|
273
|
+
req.end();
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* 带缓存的请求封装,消除 getSubscriptionStatus / getQuotaInfo 的重复逻辑
|
|
279
|
+
* @param {'statusData'|'quotaData'} dataKey 缓存数据字段名
|
|
280
|
+
* @param {'statusTimestamp'|'quotaTimestamp'} tsKey 缓存时间戳字段名
|
|
281
|
+
* @param {string} endpoint API 路径
|
|
282
|
+
* @param {boolean} forceRefresh 是否强制跳过缓存
|
|
283
|
+
*/
|
|
284
|
+
async _cachedRequest(dataKey, tsKey, endpoint, forceRefresh) {
|
|
285
|
+
const now = Date.now();
|
|
286
|
+
if (
|
|
287
|
+
!forceRefresh &&
|
|
288
|
+
this.cache[dataKey] &&
|
|
289
|
+
now - this.cache[tsKey] < CONFIG.CACHE_TIMEOUT
|
|
290
|
+
) {
|
|
291
|
+
return this.cache[dataKey];
|
|
292
|
+
}
|
|
293
|
+
const data = await this.makePlatformRequest(endpoint);
|
|
294
|
+
this.cache[dataKey] = data;
|
|
295
|
+
this.cache[tsKey] = now;
|
|
296
|
+
return data;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async getSubscriptionStatus(forceRefresh = false) {
|
|
300
|
+
return this._cachedRequest(
|
|
301
|
+
"statusData",
|
|
302
|
+
"statusTimestamp",
|
|
303
|
+
"/api/step.openapi.devcenter.Dashboard/GetStepPlanStatus",
|
|
304
|
+
forceRefresh
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async getQuotaInfo(forceRefresh = false) {
|
|
309
|
+
return this._cachedRequest(
|
|
310
|
+
"quotaData",
|
|
311
|
+
"quotaTimestamp",
|
|
312
|
+
"/api/step.openapi.devcenter.Dashboard/QueryStepPlanRateLimit",
|
|
313
|
+
forceRefresh
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ── 预留扩展区 ──────────────────────────────────────────────────────────────
|
|
318
|
+
// 后续新增接口时,只需:
|
|
319
|
+
// 1. 在 this.cache 里加对应的 data/timestamp 字段
|
|
320
|
+
// 2. 仿照下方模板写一个 get 方法
|
|
321
|
+
// 3. 在 getUsageStatus / parseUsageData 里消费新数据
|
|
322
|
+
//
|
|
323
|
+
// 模板(等 Token 用量明细接口开放后接入):
|
|
324
|
+
//
|
|
325
|
+
// async getTokenUsage(forceRefresh = false) {
|
|
326
|
+
// return this._cachedRequest(
|
|
327
|
+
// "tokenData",
|
|
328
|
+
// "tokenTimestamp",
|
|
329
|
+
// "/api/step.openapi.devcenter.Dashboard/QueryTokenUsage", // 待确认
|
|
330
|
+
// forceRefresh
|
|
331
|
+
// );
|
|
332
|
+
// }
|
|
333
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
334
|
+
|
|
335
|
+
async getUsageStatus(forceRefresh = false) {
|
|
336
|
+
const [statusData, quotaData] = await Promise.all([
|
|
337
|
+
this.getSubscriptionStatus(forceRefresh),
|
|
338
|
+
this.getQuotaInfo(forceRefresh),
|
|
339
|
+
]);
|
|
340
|
+
return this.parseUsageData(statusData, quotaData);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
parseUsageData(statusData, quotaData) {
|
|
344
|
+
const subscription = statusData.subscription || {};
|
|
345
|
+
const planDefinition = statusData.plan_definition || {};
|
|
346
|
+
|
|
347
|
+
const fiveHourLeftRate = quotaData.five_hour_usage_left_rate || 0;
|
|
348
|
+
const weeklyLeftRate = quotaData.weekly_usage_left_rate || 0;
|
|
349
|
+
|
|
350
|
+
const fiveHourUsedPercent = Math.round((1 - fiveHourLeftRate) * 100);
|
|
351
|
+
const weeklyUsedPercent = Math.round((1 - weeklyLeftRate) * 100);
|
|
352
|
+
|
|
353
|
+
const fiveHourReset = formatResetTime(quotaData.five_hour_usage_reset_time);
|
|
354
|
+
const weeklyReset = formatResetTime(quotaData.weekly_usage_reset_time);
|
|
355
|
+
|
|
356
|
+
let expiry = null;
|
|
357
|
+
if (subscription.expired_at) {
|
|
358
|
+
const expiryDate = new Date(parseInt(subscription.expired_at, 10) * 1000);
|
|
359
|
+
const daysRemaining = Math.ceil(
|
|
360
|
+
(expiryDate.getTime() - Date.now()) / (1000 * 3600 * 24)
|
|
361
|
+
);
|
|
362
|
+
expiry = {
|
|
363
|
+
date: expiryDate.toLocaleString(),
|
|
364
|
+
daysRemaining,
|
|
365
|
+
text:
|
|
366
|
+
daysRemaining > 0
|
|
367
|
+
? `还剩 ${daysRemaining} 天`
|
|
368
|
+
: daysRemaining === 0
|
|
369
|
+
? "今天到期"
|
|
370
|
+
: `已过期 ${Math.abs(daysRemaining)} 天`,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
modelName: planDefinition.support_models?.[0] || "step-3.5-flash",
|
|
376
|
+
planName: subscription.name || "Mini",
|
|
377
|
+
remaining: {
|
|
378
|
+
hours: fiveHourReset?.hoursUntil || 0,
|
|
379
|
+
minutes: fiveHourReset?.minutesUntil || 0,
|
|
380
|
+
text: calcRemainingText(fiveHourReset),
|
|
381
|
+
},
|
|
382
|
+
usage: {
|
|
383
|
+
percentage: fiveHourUsedPercent,
|
|
384
|
+
remaining: Math.round(fiveHourLeftRate * 100),
|
|
385
|
+
},
|
|
386
|
+
weekly: {
|
|
387
|
+
percentage: weeklyUsedPercent,
|
|
388
|
+
remaining: Math.round(weeklyLeftRate * 100),
|
|
389
|
+
text: calcRemainingText(weeklyReset),
|
|
390
|
+
daysUntilReset: weeklyReset?.hoursUntil
|
|
391
|
+
? Math.floor(weeklyReset.hoursUntil / 24)
|
|
392
|
+
: 0,
|
|
393
|
+
},
|
|
394
|
+
expiry,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
clearCache() {
|
|
399
|
+
this.cache = {
|
|
400
|
+
statusData: null,
|
|
401
|
+
quotaData: null,
|
|
402
|
+
statusTimestamp: 0,
|
|
403
|
+
quotaTimestamp: 0,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
module.exports = { StepFunAPI, CONFIG_PATH };
|