@lyogvce/dsh-codex-subscription 0.3.0 → 0.3.1
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/README.md +2 -1
- package/lib/client.js +304 -12
- package/lib/index.d.ts +42 -9
- package/lib/index.js +201 -10
- package/package.json +14 -12
package/README.md
CHANGED
|
@@ -21,8 +21,9 @@
|
|
|
21
21
|
- 通过 `retryPolicy` 暴露 DSH 原生 `RetryPolicySchema`(支持 `normal` / `always` 模式、`maxRetries`、`retryableCodes` 与 `backoff`,并随 `registration.replace` 热更新生效)。未配置时保持宿主默认策略(`mode: "normal"`、`maxRetries: 5`、默认 5 类错误码 `EMPTY_RESPONSE / RATE_LIMIT / SERVER / TIMEOUT / TRANSPORT` 及 `500ms..10000ms` 退避)。**注意:`retryableCodes` 为完整覆盖集合而非追加列表**,显式配置时需同时列出默认错误码与新增错误码。
|
|
22
22
|
4. **原生多轮思考链回放、工具调用与出站指纹净化**
|
|
23
23
|
- 底层复用 DSH 内置的 `PiAiAdapter` 与 `@earendil-works/pi-ai` 的 `openaiCodexProvider()`,固定使用 `sse` 传输(含 `zstd` 请求体压缩、规范化 36 位 RFC 4122 UUID `session-id` 缓存头与 `prompt_cache_key`、`reasoning.encrypted_content` 签名回放与图片自动缩放卸载),并通过自定义 `fetch` 与 `onPayload` 剥除 `OpenAI-Beta: responses=experimental`、静态 `x-client-request-id`、`msg_pi_*` 等非官方指纹。
|
|
24
|
-
5. **Codex
|
|
24
|
+
5. **Codex 订阅限额、限额重置卡(Rate Limit Reset Credits)与 ⚡ Fast 极速模式一键切换(`conversation.input.right`)**
|
|
25
25
|
- 当会话选中 `openai-codex` 模型时,在输入框右下角紧邻模型选择器左侧展示限额胶囊(根据 `wham/usage` 返回的 `limit_window_seconds` 自适应展示 Plus 的 `5h X% · 7d Y%` 双窗口或 Pro 的 `7d X%` 单窗口;由模型请求与流完成结算触发、按 60 秒限流在后台自动更新缓存,并由前端周期静默同步,无请求时零远端轮询,点击胶囊直接展开详情弹窗不触发额外请求),弹窗内可查看窗口进度条、重置倒计时与手动刷新按钮;切换至其他 Provider 模型时自动隐藏。
|
|
26
|
+
- **🎫 限额重置卡展示与二次确认核销**:自动解析 `wham/usage` 中的 `rate_limit_reset_credits`,并在持有可用重置卡(`available_count > 0`)时按需拉取 `wham/rate-limit-reset-credits` 详情,在弹窗内展示每张重置卡各自的过期时间,支持带内联二次确认的核销操作(`POST /wham/rate-limit-reset-credits/consume`)并在核销后自动刷新最新额度。
|
|
26
27
|
- **⚡ 会话树级 Fast 极速模式(`service_tier: "priority"`,最高 2x 推理速度,主会话与 Subagent / 智能体团队同开同关)**:每个主会话可在限额弹窗底部独立开关 Fast 模式(不同主会话互不影响,新会话默认继承插件设置中的 `fastMode`,未配置时默认 `false` 关闭),该主会话派生的所有 `subagent`、`subagent_fork`、`workflow` 子任务以及智能体团队成员(`spawn_teammate`)均自动绑定至同一主会话开关,实时联动、同开同关;状态自动持久化至 `~/.codex/cache/dsh-codex-subscription/runtime-state.json`。开启后胶囊显示闪电标识 `⚡`,并在请求体中自动注入 `"service_tier": "priority"`。
|
|
27
28
|
|
|
28
29
|
---
|
package/lib/client.js
CHANGED
|
@@ -47,6 +47,20 @@ window.__ModuleLoader__.load({
|
|
|
47
47
|
function clampPercent(value) {
|
|
48
48
|
return Math.max(0, Math.min(100, Math.round(value)));
|
|
49
49
|
}
|
|
50
|
+
function normalizeConsumeOutcome(raw) {
|
|
51
|
+
if (typeof raw !== "string") return null;
|
|
52
|
+
switch (raw.trim()) {
|
|
53
|
+
case "reset":
|
|
54
|
+
case "nothing_to_reset":
|
|
55
|
+
case "no_credit":
|
|
56
|
+
case "already_redeemed": return raw.trim();
|
|
57
|
+
default: return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function generateRedeemRequestId(nowMs) {
|
|
61
|
+
const rand = Math.random().toString(16).slice(2, 14).padEnd(12, "0");
|
|
62
|
+
return `redeem-${nowMs.toString(16)}-${rand}`;
|
|
63
|
+
}
|
|
50
64
|
//#endregion
|
|
51
65
|
//#region src/client/quota.ts
|
|
52
66
|
const RING_CIRCUMFERENCE = 2 * Math.PI * 5.5;
|
|
@@ -168,8 +182,36 @@ window.__ModuleLoader__.load({
|
|
|
168
182
|
const remHours = totalHours % 24;
|
|
169
183
|
return fmt.days(days, remHours);
|
|
170
184
|
}
|
|
185
|
+
const CREDIT_EXPIRY_FORMATTERS = {
|
|
186
|
+
zh: {
|
|
187
|
+
never: () => "永久有效",
|
|
188
|
+
expired: () => "已过期",
|
|
189
|
+
minutes: (m, dateStr) => `${m} 分钟后过期 (${dateStr})`,
|
|
190
|
+
hours: (h, dateStr) => `${h} 小时后过期 (${dateStr})`,
|
|
191
|
+
days: (d, dateStr) => `${d} 天后过期 (${dateStr})`
|
|
192
|
+
},
|
|
193
|
+
en: {
|
|
194
|
+
never: () => "Never expires",
|
|
195
|
+
expired: () => "Expired",
|
|
196
|
+
minutes: (m, dateStr) => `Expires in ${m}m (${dateStr})`,
|
|
197
|
+
hours: (h, dateStr) => `Expires in ${h}h (${dateStr})`,
|
|
198
|
+
days: (d, dateStr) => `Expires in ${d}d (${dateStr})`
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
function formatResetCreditExpiry(expiresAtSec, nowMs, locale) {
|
|
202
|
+
const fmt = CREDIT_EXPIRY_FORMATTERS[locale];
|
|
203
|
+
if (expiresAtSec === null || expiresAtSec === void 0) return fmt.never();
|
|
204
|
+
if (typeof expiresAtSec !== "number" || !Number.isFinite(expiresAtSec) || expiresAtSec <= 0) return "";
|
|
205
|
+
const diffSec = Math.round(expiresAtSec - nowMs / 1e3);
|
|
206
|
+
if (diffSec <= 0) return fmt.expired();
|
|
207
|
+
const expDate = new Date(expiresAtSec * 1e3);
|
|
208
|
+
const dateStr = `${padTwo(expDate.getMonth() + 1)}-${padTwo(expDate.getDate())}`;
|
|
209
|
+
if (diffSec < 3600) return fmt.minutes(Math.max(1, Math.ceil(diffSec / 60)), dateStr);
|
|
210
|
+
if (diffSec < 86400) return fmt.hours(Math.max(1, Math.ceil(diffSec / 3600)), dateStr);
|
|
211
|
+
return fmt.days(Math.max(1, Math.ceil(diffSec / 86400)), dateStr);
|
|
212
|
+
}
|
|
171
213
|
function createQuotaStore(options = {}) {
|
|
172
|
-
const
|
|
214
|
+
const getFetch = () => options.fetchImpl ?? globalThis.fetch;
|
|
173
215
|
const nowFn = options.now ?? Date.now;
|
|
174
216
|
const explicitTtlMs = options.ttlMs;
|
|
175
217
|
let state = {
|
|
@@ -177,6 +219,9 @@ window.__ModuleLoader__.load({
|
|
|
177
219
|
defaultFastMode: false,
|
|
178
220
|
sessionFastModes: {},
|
|
179
221
|
sessionParents: {},
|
|
222
|
+
redeeming: false,
|
|
223
|
+
redeemingCreditId: null,
|
|
224
|
+
redeemNoticeOutcome: null,
|
|
180
225
|
loading: false,
|
|
181
226
|
error: null
|
|
182
227
|
};
|
|
@@ -186,9 +231,11 @@ window.__ModuleLoader__.load({
|
|
|
186
231
|
const pendingBySeq = new Map();
|
|
187
232
|
let inflight = null;
|
|
188
233
|
let inflightForce = false;
|
|
234
|
+
let redeemingInflight = null;
|
|
189
235
|
let requestSeq = 0;
|
|
190
236
|
let fastModeSeq = 0;
|
|
191
237
|
let fastMutationEpoch = 0;
|
|
238
|
+
let redeemSeq = 0;
|
|
192
239
|
const listeners = new Set();
|
|
193
240
|
function setState(next) {
|
|
194
241
|
state = next;
|
|
@@ -272,6 +319,9 @@ window.__ModuleLoader__.load({
|
|
|
272
319
|
sessionParents: knownSessionParents
|
|
273
320
|
};
|
|
274
321
|
}
|
|
322
|
+
function extractPayloadError(payload, fallback) {
|
|
323
|
+
return typeof payload?.error === "string" && payload.error !== "" ? payload.error : fallback;
|
|
324
|
+
}
|
|
275
325
|
async function refresh(force = false) {
|
|
276
326
|
if (!force && explicitTtlMs !== void 0 && state.quota !== null && nowFn() - state.quota.fetchedAt < explicitTtlMs) return;
|
|
277
327
|
if (inflight !== null && (!force || inflightForce)) return inflight;
|
|
@@ -280,15 +330,16 @@ window.__ModuleLoader__.load({
|
|
|
280
330
|
const fastEpochAtStart = fastMutationEpoch;
|
|
281
331
|
inflightForce = force;
|
|
282
332
|
const showLoading = force || state.quota === null;
|
|
283
|
-
if (showLoading || state.error !== null) setState({
|
|
333
|
+
if (showLoading || state.error !== null || state.redeemNoticeOutcome !== null) setState({
|
|
284
334
|
...state,
|
|
285
335
|
loading: showLoading ? true : state.loading,
|
|
336
|
+
redeemNoticeOutcome: null,
|
|
286
337
|
error: null
|
|
287
338
|
});
|
|
288
339
|
const url = force ? `${USAGE_ROUTE_PATH}?force=1` : USAGE_ROUTE_PATH;
|
|
289
340
|
const task = (async () => {
|
|
290
341
|
try {
|
|
291
|
-
const response = await
|
|
342
|
+
const response = await getFetch()(url, {
|
|
292
343
|
method: "GET",
|
|
293
344
|
headers: { Accept: "application/json" }
|
|
294
345
|
});
|
|
@@ -297,6 +348,7 @@ window.__ModuleLoader__.load({
|
|
|
297
348
|
setState({
|
|
298
349
|
...state,
|
|
299
350
|
loading: false,
|
|
351
|
+
redeemNoticeOutcome: null,
|
|
300
352
|
error: `HTTP ${response.status}`
|
|
301
353
|
});
|
|
302
354
|
return;
|
|
@@ -305,18 +357,22 @@ window.__ModuleLoader__.load({
|
|
|
305
357
|
if (seq !== requestSeq) return;
|
|
306
358
|
const fastState = mergeFastStateFromPayload(payload, canReplaceFastAtStart && pendingBySeq.size === 0 && fastEpochAtStart === fastMutationEpoch);
|
|
307
359
|
if (payload && payload.ok === true && payload.quota) setState({
|
|
360
|
+
...state,
|
|
308
361
|
quota: payload.quota,
|
|
309
362
|
...fastState,
|
|
310
363
|
loading: false,
|
|
364
|
+
redeemNoticeOutcome: null,
|
|
311
365
|
error: null
|
|
312
366
|
});
|
|
313
367
|
else {
|
|
314
368
|
const fallbackQuota = payload && !payload.ok && payload.quota ? payload.quota : state.quota;
|
|
315
|
-
const errMsg = payload && !payload.ok
|
|
369
|
+
const errMsg = extractPayloadError(payload && !payload.ok ? payload : null, "Invalid response");
|
|
316
370
|
setState({
|
|
371
|
+
...state,
|
|
317
372
|
quota: fallbackQuota ?? null,
|
|
318
373
|
...fastState,
|
|
319
374
|
loading: false,
|
|
375
|
+
redeemNoticeOutcome: null,
|
|
320
376
|
error: errMsg
|
|
321
377
|
});
|
|
322
378
|
}
|
|
@@ -325,6 +381,7 @@ window.__ModuleLoader__.load({
|
|
|
325
381
|
setState({
|
|
326
382
|
...state,
|
|
327
383
|
loading: false,
|
|
384
|
+
redeemNoticeOutcome: null,
|
|
328
385
|
error: safeErrorMessage(error)
|
|
329
386
|
});
|
|
330
387
|
} finally {
|
|
@@ -362,6 +419,7 @@ window.__ModuleLoader__.load({
|
|
|
362
419
|
...state,
|
|
363
420
|
sessionFastModes: deriveSessionFastModes(),
|
|
364
421
|
sessionParents: knownSessionParents,
|
|
422
|
+
redeemNoticeOutcome: null,
|
|
365
423
|
error: errorMsg
|
|
366
424
|
});
|
|
367
425
|
};
|
|
@@ -369,10 +427,11 @@ window.__ModuleLoader__.load({
|
|
|
369
427
|
...state,
|
|
370
428
|
sessionFastModes: deriveSessionFastModes(),
|
|
371
429
|
sessionParents: knownSessionParents,
|
|
430
|
+
redeemNoticeOutcome: null,
|
|
372
431
|
error: null
|
|
373
432
|
});
|
|
374
433
|
try {
|
|
375
|
-
const response = await
|
|
434
|
+
const response = await getFetch()(USAGE_ROUTE_PATH, {
|
|
376
435
|
method: "POST",
|
|
377
436
|
headers: {
|
|
378
437
|
"Content-Type": "application/json",
|
|
@@ -386,7 +445,7 @@ window.__ModuleLoader__.load({
|
|
|
386
445
|
if (!response.ok) {
|
|
387
446
|
const errPayload = await response.json().catch(() => null);
|
|
388
447
|
mergeServerSessionParents(errPayload?.sessionParents);
|
|
389
|
-
rollback(
|
|
448
|
+
rollback(extractPayloadError(errPayload, `HTTP ${response.status}`));
|
|
390
449
|
return;
|
|
391
450
|
}
|
|
392
451
|
const payload = await response.json();
|
|
@@ -396,7 +455,7 @@ window.__ModuleLoader__.load({
|
|
|
396
455
|
return;
|
|
397
456
|
}
|
|
398
457
|
if (!payload || payload.ok === false) {
|
|
399
|
-
rollback(
|
|
458
|
+
rollback(extractPayloadError(payload, "Invalid response"));
|
|
400
459
|
return;
|
|
401
460
|
}
|
|
402
461
|
fastMutationEpoch += 1;
|
|
@@ -422,6 +481,7 @@ window.__ModuleLoader__.load({
|
|
|
422
481
|
defaultFastMode,
|
|
423
482
|
sessionFastModes: deriveSessionFastModes(),
|
|
424
483
|
sessionParents: knownSessionParents,
|
|
484
|
+
redeemNoticeOutcome: null,
|
|
425
485
|
error: null
|
|
426
486
|
});
|
|
427
487
|
} catch (error) {
|
|
@@ -430,6 +490,99 @@ window.__ModuleLoader__.load({
|
|
|
430
490
|
pendingBySeq.delete(seq);
|
|
431
491
|
}
|
|
432
492
|
}
|
|
493
|
+
async function consumeResetCredit(creditId) {
|
|
494
|
+
if (redeemingInflight !== null) return redeemingInflight;
|
|
495
|
+
const normalizedId = typeof creditId === "string" && creditId.trim() !== "" ? creditId.trim() : null;
|
|
496
|
+
redeemSeq += 1;
|
|
497
|
+
const redeemRequestId = generateRedeemRequestId(nowFn() + redeemSeq);
|
|
498
|
+
setState({
|
|
499
|
+
...state,
|
|
500
|
+
redeeming: true,
|
|
501
|
+
redeemingCreditId: normalizedId,
|
|
502
|
+
redeemNoticeOutcome: null,
|
|
503
|
+
error: null
|
|
504
|
+
});
|
|
505
|
+
const task = (async () => {
|
|
506
|
+
try {
|
|
507
|
+
const response = await getFetch()(USAGE_ROUTE_PATH, {
|
|
508
|
+
method: "POST",
|
|
509
|
+
headers: {
|
|
510
|
+
"Content-Type": "application/json",
|
|
511
|
+
Accept: "application/json"
|
|
512
|
+
},
|
|
513
|
+
body: JSON.stringify({
|
|
514
|
+
action: "consumeResetCredit",
|
|
515
|
+
...normalizedId !== null ? { creditId: normalizedId } : {},
|
|
516
|
+
redeemRequestId
|
|
517
|
+
})
|
|
518
|
+
});
|
|
519
|
+
if (!response.ok) {
|
|
520
|
+
const errMsg = extractPayloadError(await response.json().catch(() => null), `HTTP ${response.status}`);
|
|
521
|
+
setState({
|
|
522
|
+
...state,
|
|
523
|
+
redeeming: false,
|
|
524
|
+
redeemingCreditId: null,
|
|
525
|
+
redeemNoticeOutcome: null,
|
|
526
|
+
error: errMsg
|
|
527
|
+
});
|
|
528
|
+
return {
|
|
529
|
+
ok: false,
|
|
530
|
+
error: errMsg
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
const payload = await response.json();
|
|
534
|
+
const fastState = mergeFastStateFromPayload(payload, pendingBySeq.size === 0);
|
|
535
|
+
if (!payload || payload.ok !== true) {
|
|
536
|
+
const errMsg = extractPayloadError(payload, "Invalid response");
|
|
537
|
+
setState({
|
|
538
|
+
...state,
|
|
539
|
+
quota: payload?.quota ?? state.quota,
|
|
540
|
+
...fastState,
|
|
541
|
+
redeeming: false,
|
|
542
|
+
redeemingCreditId: null,
|
|
543
|
+
redeemNoticeOutcome: null,
|
|
544
|
+
error: errMsg
|
|
545
|
+
});
|
|
546
|
+
return {
|
|
547
|
+
ok: false,
|
|
548
|
+
error: errMsg
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
const outcome = normalizeConsumeOutcome(payload.outcome) ?? "reset";
|
|
552
|
+
setState({
|
|
553
|
+
...state,
|
|
554
|
+
quota: payload.quota ?? state.quota,
|
|
555
|
+
...fastState,
|
|
556
|
+
redeeming: false,
|
|
557
|
+
redeemingCreditId: null,
|
|
558
|
+
redeemNoticeOutcome: outcome === "reset" ? null : outcome,
|
|
559
|
+
error: null
|
|
560
|
+
});
|
|
561
|
+
return {
|
|
562
|
+
ok: true,
|
|
563
|
+
outcome,
|
|
564
|
+
...payload.quota !== void 0 ? { quota: payload.quota } : {}
|
|
565
|
+
};
|
|
566
|
+
} catch (error) {
|
|
567
|
+
const errMsg = safeErrorMessage(error);
|
|
568
|
+
setState({
|
|
569
|
+
...state,
|
|
570
|
+
redeeming: false,
|
|
571
|
+
redeemingCreditId: null,
|
|
572
|
+
redeemNoticeOutcome: null,
|
|
573
|
+
error: errMsg
|
|
574
|
+
});
|
|
575
|
+
return {
|
|
576
|
+
ok: false,
|
|
577
|
+
error: errMsg
|
|
578
|
+
};
|
|
579
|
+
} finally {
|
|
580
|
+
redeemingInflight = null;
|
|
581
|
+
}
|
|
582
|
+
})();
|
|
583
|
+
redeemingInflight = task;
|
|
584
|
+
return task;
|
|
585
|
+
}
|
|
433
586
|
return {
|
|
434
587
|
getSnapshot: () => state,
|
|
435
588
|
subscribe(listener) {
|
|
@@ -439,7 +592,8 @@ window.__ModuleLoader__.load({
|
|
|
439
592
|
};
|
|
440
593
|
},
|
|
441
594
|
refresh,
|
|
442
|
-
setFastMode
|
|
595
|
+
setFastMode,
|
|
596
|
+
consumeResetCredit
|
|
443
597
|
};
|
|
444
598
|
}
|
|
445
599
|
//#endregion
|
|
@@ -481,6 +635,26 @@ window.__ModuleLoader__.load({
|
|
|
481
635
|
".dsh-codex-fast-switch[data-checked=\"true\"]{background:var(--dsw-alias-brand-primary,#3b82f6)}",
|
|
482
636
|
".dsh-codex-fast-switch-thumb{position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,0.2);transition:transform .15s ease}",
|
|
483
637
|
".dsh-codex-fast-switch[data-checked=\"true\"] .dsh-codex-fast-switch-thumb{transform:translateX(14px)}",
|
|
638
|
+
".dsh-codex-reset-section{margin-top:8px;padding-top:8px;border-top:1px solid var(--dsw-alias-border-l1)}",
|
|
639
|
+
".dsh-codex-reset-row{display:flex;align-items:center;justify-content:space-between;gap:10px}",
|
|
640
|
+
".dsh-codex-reset-icon{color:var(--dsw-alias-brand-primary,#3b82f6);flex:none}",
|
|
641
|
+
".dsh-codex-reset-pill{display:inline-flex;align-items:center;gap:4px;padding:2px 8px;border-radius:999px;border:none;font-family:inherit;font-size:11px;font-weight:600;background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-brand-primary);white-space:nowrap;font-variant-numeric:tabular-nums;cursor:pointer;flex:none;transition:background-color .12s,color .12s}",
|
|
642
|
+
".dsh-codex-reset-pill[data-zero=\"true\"]{color:var(--dsw-alias-label-tertiary);font-weight:500;cursor:default}",
|
|
643
|
+
".dsh-codex-reset-list{margin-top:6px;display:flex;flex-direction:column;gap:5px}",
|
|
644
|
+
".dsh-codex-reset-card{padding:6px 8px;border-radius:6px;background:var(--dsw-alias-interactive-bg-hover);border:1px dashed var(--dsw-alias-border-l2);display:flex;flex-direction:column;gap:5px}",
|
|
645
|
+
".dsh-codex-reset-item{display:flex;align-items:center;justify-content:space-between;gap:8px}",
|
|
646
|
+
".dsh-codex-reset-item-info{display:flex;flex-direction:column;gap:1px;min-width:0}",
|
|
647
|
+
".dsh-codex-reset-item-title{font-size:11.5px;font-weight:500;color:var(--dsw-alias-label-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
648
|
+
".dsh-codex-reset-item-expiry{font-size:10.5px;line-height:14px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}",
|
|
649
|
+
".dsh-codex-reset-use-btn{border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-brand-primary);font-family:inherit;font-size:11px;font-weight:600;padding:2px 8px;border-radius:5px;cursor:pointer;white-space:nowrap;flex:none;transition:background-color .12s,color .12s,border-color .12s}",
|
|
650
|
+
".dsh-codex-reset-use-btn:hover:not(:disabled){background:var(--dsw-alias-brand-primary,#3b82f6);border-color:var(--dsw-alias-brand-primary,#3b82f6);color:#fff}",
|
|
651
|
+
".dsh-codex-reset-use-btn:disabled{opacity:0.45;cursor:default;color:var(--dsw-alias-label-tertiary)}",
|
|
652
|
+
".dsh-codex-reset-confirm{padding-top:5px;border-top:1px dashed var(--dsw-alias-border-l1);display:flex;align-items:center;justify-content:space-between;gap:6px}",
|
|
653
|
+
".dsh-codex-reset-confirm-text{font-size:11px;line-height:14px;color:var(--dsw-alias-state-warn-primary)}",
|
|
654
|
+
".dsh-codex-reset-confirm-actions{display:inline-flex;align-items:center;gap:4px;flex:none}",
|
|
655
|
+
".dsh-codex-reset-confirm-cancel,.dsh-codex-reset-confirm-submit{border:none;font-family:inherit;font-size:11px;font-weight:600;padding:2px 7px;border-radius:4px;cursor:pointer;white-space:nowrap}",
|
|
656
|
+
".dsh-codex-reset-confirm-cancel{background:transparent;color:var(--dsw-alias-label-secondary)}",
|
|
657
|
+
".dsh-codex-reset-confirm-submit{background:var(--dsw-alias-state-warn-primary,#f59e0b);color:#111}",
|
|
484
658
|
".dsh-codex-quota-error{margin-top:6px;padding:4px 6px;border-radius:4px;background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-state-error-primary);font-size:11px;word-break:break-word}"
|
|
485
659
|
].join("");
|
|
486
660
|
//#endregion
|
|
@@ -500,7 +674,22 @@ window.__ModuleLoader__.load({
|
|
|
500
674
|
"quota.usedPrefix": "已用 ",
|
|
501
675
|
"quota.remainPrefix": "剩余 ",
|
|
502
676
|
"quota.fastTitle": "Fast 极速模式",
|
|
503
|
-
"quota.fastDesc": "2x 推理速度,消耗更多限额"
|
|
677
|
+
"quota.fastDesc": "2x 推理速度,消耗更多限额",
|
|
678
|
+
"quota.resetTitle": "限额重置卡",
|
|
679
|
+
"quota.resetZero": "0 张可用",
|
|
680
|
+
"quota.resetZeroDesc": "暂无可用重置卡",
|
|
681
|
+
"quota.resetAvailableSuffix": " 张可用",
|
|
682
|
+
"quota.resetNextExpiryPrefix": "下次到期:",
|
|
683
|
+
"quota.resetReadyDesc": "可重置当前周期已用限额",
|
|
684
|
+
"quota.resetItemDefaultTitle": "重置卡",
|
|
685
|
+
"quota.resetUseBtn": "使用",
|
|
686
|
+
"quota.resetUsingBtn": "核销中…",
|
|
687
|
+
"quota.resetConfirmPrompt": "确认使用此卡重置当前已用限额?",
|
|
688
|
+
"quota.resetConfirmYes": "确认使用",
|
|
689
|
+
"quota.resetConfirmNo": "取消",
|
|
690
|
+
"quota.resetNothingToReset": "当前无需重置限额",
|
|
691
|
+
"quota.resetNoCredit": "没有可用的重置卡",
|
|
692
|
+
"quota.resetAlreadyRedeemed": "该重置卡已被使用"
|
|
504
693
|
};
|
|
505
694
|
const en = {
|
|
506
695
|
"quota.title": "Codex Subscription Quota",
|
|
@@ -510,7 +699,22 @@ window.__ModuleLoader__.load({
|
|
|
510
699
|
"quota.usedPrefix": "Used ",
|
|
511
700
|
"quota.remainPrefix": "Remaining ",
|
|
512
701
|
"quota.fastTitle": "Fast Mode",
|
|
513
|
-
"quota.fastDesc": "2x speed, increased usage"
|
|
702
|
+
"quota.fastDesc": "2x speed, increased usage",
|
|
703
|
+
"quota.resetTitle": "Reset Credits",
|
|
704
|
+
"quota.resetZero": "0 available",
|
|
705
|
+
"quota.resetZeroDesc": "No reset credits available",
|
|
706
|
+
"quota.resetAvailableSuffix": " available",
|
|
707
|
+
"quota.resetNextExpiryPrefix": "Next expiry: ",
|
|
708
|
+
"quota.resetReadyDesc": "Reset current window usage",
|
|
709
|
+
"quota.resetItemDefaultTitle": "Reset Credit",
|
|
710
|
+
"quota.resetUseBtn": "Use",
|
|
711
|
+
"quota.resetUsingBtn": "Using…",
|
|
712
|
+
"quota.resetConfirmPrompt": "Use this credit to reset current usage?",
|
|
713
|
+
"quota.resetConfirmYes": "Confirm",
|
|
714
|
+
"quota.resetConfirmNo": "Cancel",
|
|
715
|
+
"quota.resetNothingToReset": "Your usage does not need a reset right now",
|
|
716
|
+
"quota.resetNoCredit": "No reset credits available",
|
|
717
|
+
"quota.resetAlreadyRedeemed": "This reset credit was already redeemed"
|
|
514
718
|
};
|
|
515
719
|
const EMPTY_MODEL_STORE = {
|
|
516
720
|
getSnapshot: () => null,
|
|
@@ -528,6 +732,25 @@ window.__ModuleLoader__.load({
|
|
|
528
732
|
fill: "currentColor"
|
|
529
733
|
}));
|
|
530
734
|
}
|
|
735
|
+
function renderTicketIcon(size) {
|
|
736
|
+
return h("svg", {
|
|
737
|
+
className: "dsh-codex-reset-icon",
|
|
738
|
+
viewBox: "0 0 16 16",
|
|
739
|
+
width: size,
|
|
740
|
+
height: size,
|
|
741
|
+
fill: "none",
|
|
742
|
+
stroke: "currentColor",
|
|
743
|
+
strokeWidth: 1.5,
|
|
744
|
+
strokeLinecap: "round",
|
|
745
|
+
strokeLinejoin: "round",
|
|
746
|
+
"aria-hidden": true
|
|
747
|
+
}, h("path", { d: "M2 5a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1.6a1.4 1.4 0 0 0 0 2.8V11a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V9.4a1.4 1.4 0 0 0 0-2.8V5z" }), h("path", { d: "M6.5 4v8" }));
|
|
748
|
+
}
|
|
749
|
+
const REDEEM_OUTCOME_LOCALE_KEYS = {
|
|
750
|
+
nothing_to_reset: "quota.resetNothingToReset",
|
|
751
|
+
no_credit: "quota.resetNoCredit",
|
|
752
|
+
already_redeemed: "quota.resetAlreadyRedeemed"
|
|
753
|
+
};
|
|
531
754
|
function CodexQuotaPill(props) {
|
|
532
755
|
const { sessionId, quotaStore, getModelStore, useProjection, useSession, t, getLocale } = props;
|
|
533
756
|
const modelStore = getModelStore(sessionId);
|
|
@@ -539,6 +762,8 @@ window.__ModuleLoader__.load({
|
|
|
539
762
|
const isCodex = isCodexProvider(resolveActiveProvider(directorySnap, projectedSelection));
|
|
540
763
|
const quotaSnap = React.useSyncExternalStore(quotaStore.subscribe, quotaStore.getSnapshot);
|
|
541
764
|
const [open, setOpen] = React.useState(false);
|
|
765
|
+
const [detailsOpen, setDetailsOpen] = React.useState(true);
|
|
766
|
+
const [confirmingCreditKey, setConfirmingCreditKey] = React.useState(null);
|
|
542
767
|
const rootRef = React.useRef(null);
|
|
543
768
|
const prevRunningRef = React.useRef(running);
|
|
544
769
|
const prevSessionIdRef = React.useRef(sessionId);
|
|
@@ -612,6 +837,71 @@ window.__ModuleLoader__.load({
|
|
|
612
837
|
const isFastMode = resolveSessionFastMode(quotaSnap, sessionId, parentSessionId);
|
|
613
838
|
const secondaryReached = Boolean(quota?.secondary && quota.secondary.usedPercent >= 100);
|
|
614
839
|
const primaryReached = Boolean(quota?.limitReached && !secondaryReached);
|
|
840
|
+
const resetCredits = quota?.resetCredits;
|
|
841
|
+
const availableResetCount = resetCredits?.availableCount ?? 0;
|
|
842
|
+
const applicableResetCount = resetCredits?.applicableAvailableCount ?? availableResetCount;
|
|
843
|
+
const nowSec = Math.floor(nowMs / 1e3);
|
|
844
|
+
const displayCredits = availableResetCount > 0 ? resetCredits?.credits && resetCredits.credits.length > 0 ? resetCredits.credits.map((item) => ({
|
|
845
|
+
id: item.id,
|
|
846
|
+
slotKey: item.id,
|
|
847
|
+
title: item.title,
|
|
848
|
+
expiresAt: item.expiresAt
|
|
849
|
+
})) : Array.from({ length: availableResetCount }, (_unused, idx) => ({
|
|
850
|
+
id: null,
|
|
851
|
+
slotKey: `placeholder-${idx}`,
|
|
852
|
+
title: null,
|
|
853
|
+
expiresAt: null
|
|
854
|
+
})) : [];
|
|
855
|
+
const renderResetCreditsSection = () => {
|
|
856
|
+
const firstCard = displayCredits[0];
|
|
857
|
+
const subDesc = availableResetCount === 0 ? t("quota.resetZeroDesc") : firstCard !== void 0 && firstCard.expiresAt !== null ? `${t("quota.resetNextExpiryPrefix")}${formatResetCreditExpiry(firstCard.expiresAt, nowMs, locale)}` : t("quota.resetReadyDesc");
|
|
858
|
+
return h("div", { className: "dsh-codex-reset-section" }, h("div", { className: "dsh-codex-reset-row" }, h("div", { className: "dsh-codex-fast-info" }, h("div", { className: "dsh-codex-fast-title" }, renderTicketIcon(12), h("span", null, t("quota.resetTitle"))), h("div", { className: "dsh-codex-fast-desc" }, subDesc)), availableResetCount === 0 ? h("span", {
|
|
859
|
+
className: "dsh-codex-reset-pill",
|
|
860
|
+
"data-zero": "true"
|
|
861
|
+
}, t("quota.resetZero")) : h("button", {
|
|
862
|
+
type: "button",
|
|
863
|
+
className: "dsh-codex-reset-pill",
|
|
864
|
+
"data-zero": "false",
|
|
865
|
+
"aria-expanded": detailsOpen,
|
|
866
|
+
onClick: () => {
|
|
867
|
+
setDetailsOpen(!detailsOpen);
|
|
868
|
+
setConfirmingCreditKey(null);
|
|
869
|
+
}
|
|
870
|
+
}, `${availableResetCount}${t("quota.resetAvailableSuffix")} ${detailsOpen ? "▴" : "▾"}`)), availableResetCount > 0 && detailsOpen ? h("div", { className: "dsh-codex-reset-list" }, ...displayCredits.map((item, idx) => {
|
|
871
|
+
const itemKey = item.slotKey;
|
|
872
|
+
const itemTitle = item.title ?? `${t("quota.resetItemDefaultTitle")} #${idx + 1}`;
|
|
873
|
+
const itemExpiry = formatResetCreditExpiry(item.expiresAt, nowMs, locale);
|
|
874
|
+
const isItemExpired = item.expiresAt !== null && item.expiresAt <= nowSec;
|
|
875
|
+
const isRedeemingAny = quotaSnap.redeeming === true;
|
|
876
|
+
const isRedeemingThis = isRedeemingAny && (item.id !== null ? quotaSnap.redeemingCreditId === item.id : quotaSnap.redeemingCreditId === null);
|
|
877
|
+
const isConfirming = confirmingCreditKey === itemKey;
|
|
878
|
+
return h("div", {
|
|
879
|
+
key: itemKey,
|
|
880
|
+
className: "dsh-codex-reset-card"
|
|
881
|
+
}, h("div", { className: "dsh-codex-reset-item" }, h("div", { className: "dsh-codex-reset-item-info" }, h("div", { className: "dsh-codex-reset-item-title" }, itemTitle), h("div", { className: "dsh-codex-reset-item-expiry" }, itemExpiry)), h("button", {
|
|
882
|
+
type: "button",
|
|
883
|
+
className: "dsh-codex-reset-use-btn",
|
|
884
|
+
disabled: applicableResetCount <= 0 || isRedeemingAny || isItemExpired,
|
|
885
|
+
onClick: () => {
|
|
886
|
+
setConfirmingCreditKey(isConfirming ? null : itemKey);
|
|
887
|
+
}
|
|
888
|
+
}, isRedeemingThis ? t("quota.resetUsingBtn") : t("quota.resetUseBtn"))), isConfirming ? h("div", { className: "dsh-codex-reset-confirm" }, h("span", { className: "dsh-codex-reset-confirm-text" }, t("quota.resetConfirmPrompt")), h("div", { className: "dsh-codex-reset-confirm-actions" }, h("button", {
|
|
889
|
+
type: "button",
|
|
890
|
+
className: "dsh-codex-reset-confirm-cancel",
|
|
891
|
+
onClick: () => {
|
|
892
|
+
setConfirmingCreditKey(null);
|
|
893
|
+
}
|
|
894
|
+
}, t("quota.resetConfirmNo")), h("button", {
|
|
895
|
+
type: "button",
|
|
896
|
+
className: "dsh-codex-reset-confirm-submit",
|
|
897
|
+
disabled: isRedeemingAny || isItemExpired,
|
|
898
|
+
onClick: async () => {
|
|
899
|
+
setConfirmingCreditKey(null);
|
|
900
|
+
await quotaStore.consumeResetCredit(item.id ?? void 0);
|
|
901
|
+
}
|
|
902
|
+
}, t("quota.resetConfirmYes")))) : null);
|
|
903
|
+
})) : null);
|
|
904
|
+
};
|
|
615
905
|
return h("span", {
|
|
616
906
|
ref: rootRef,
|
|
617
907
|
className: "dsh-codex-quota-root",
|
|
@@ -626,6 +916,7 @@ window.__ModuleLoader__.load({
|
|
|
626
916
|
"aria-expanded": open,
|
|
627
917
|
title: `${t("quota.title")} (${pillText})`,
|
|
628
918
|
onClick: () => {
|
|
919
|
+
setConfirmingCreditKey(null);
|
|
629
920
|
setOpen(!open);
|
|
630
921
|
}
|
|
631
922
|
}, h("svg", {
|
|
@@ -659,7 +950,7 @@ window.__ModuleLoader__.load({
|
|
|
659
950
|
onClick: () => {
|
|
660
951
|
quotaStore.refresh(true);
|
|
661
952
|
}
|
|
662
|
-
}, quotaSnap.loading ? t("quota.refreshing") : t("quota.refresh"))), quota !== null ? renderWindowSection(formatWindowTitle(quota.primary.windowSeconds, locale, DEFAULT_PRIMARY_WINDOW_SECONDS), quota.primary, primaryReached) : quotaSnap.loading || quotaSnap.error === null ? h("div", { className: "dsh-codex-quota-meta" }, t("quota.loading")) : null, quota?.secondary !== null && quota?.secondary !== void 0 ? renderWindowSection(formatWindowTitle(quota.secondary.windowSeconds, locale, DEFAULT_SECONDARY_WINDOW_SECONDS), quota.secondary, secondaryReached) : null, h("div", { className: "dsh-codex-fast-row" }, h("div", { className: "dsh-codex-fast-info" }, h("div", { className: "dsh-codex-fast-title" }, renderFastBolt(12), h("span", null, t("quota.fastTitle"))), h("div", { className: "dsh-codex-fast-desc" }, t("quota.fastDesc"))), h("button", {
|
|
953
|
+
}, quotaSnap.loading ? t("quota.refreshing") : t("quota.refresh"))), quota !== null ? renderWindowSection(formatWindowTitle(quota.primary.windowSeconds, locale, DEFAULT_PRIMARY_WINDOW_SECONDS), quota.primary, primaryReached) : quotaSnap.loading || quotaSnap.error === null ? h("div", { className: "dsh-codex-quota-meta" }, t("quota.loading")) : null, quota?.secondary !== null && quota?.secondary !== void 0 ? renderWindowSection(formatWindowTitle(quota.secondary.windowSeconds, locale, DEFAULT_SECONDARY_WINDOW_SECONDS), quota.secondary, secondaryReached) : null, quota !== null ? renderResetCreditsSection() : null, h("div", { className: "dsh-codex-fast-row" }, h("div", { className: "dsh-codex-fast-info" }, h("div", { className: "dsh-codex-fast-title" }, renderFastBolt(12), h("span", null, t("quota.fastTitle"))), h("div", { className: "dsh-codex-fast-desc" }, t("quota.fastDesc"))), h("button", {
|
|
663
954
|
type: "button",
|
|
664
955
|
role: "switch",
|
|
665
956
|
className: "dsh-codex-fast-switch",
|
|
@@ -669,7 +960,7 @@ window.__ModuleLoader__.load({
|
|
|
669
960
|
onClick: () => {
|
|
670
961
|
quotaStore.setFastMode(sessionId, !isFastMode, parentSessionId);
|
|
671
962
|
}
|
|
672
|
-
}, h("span", { className: "dsh-codex-fast-switch-thumb" }))), quotaSnap.error !== null ? h("div", { className: "dsh-codex-quota-error" }, quotaSnap.error) : null) : null);
|
|
963
|
+
}, h("span", { className: "dsh-codex-fast-switch-thumb" }))), quotaSnap.error !== null || quotaSnap.redeemNoticeOutcome != null ? h("div", { className: "dsh-codex-quota-error" }, quotaSnap.error !== null ? quotaSnap.error : t(REDEEM_OUTCOME_LOCALE_KEYS[quotaSnap.redeemNoticeOutcome])) : null) : null);
|
|
673
964
|
}
|
|
674
965
|
const inject = ["slots", "locale"];
|
|
675
966
|
function apply(ctx) {
|
|
@@ -728,6 +1019,7 @@ window.__ModuleLoader__.load({
|
|
|
728
1019
|
formatWindowTitle,
|
|
729
1020
|
formatQuotaSummary,
|
|
730
1021
|
formatResetCountdown,
|
|
1022
|
+
formatResetCreditExpiry,
|
|
731
1023
|
createQuotaStore,
|
|
732
1024
|
CodexQuotaPill
|
|
733
1025
|
});
|
package/lib/index.d.ts
CHANGED
|
@@ -132,27 +132,51 @@ interface CodexQuotaWindow {
|
|
|
132
132
|
readonly windowSeconds?: number | null | undefined;
|
|
133
133
|
readonly resetAt: number | null;
|
|
134
134
|
}
|
|
135
|
+
interface CodexResetCreditItem {
|
|
136
|
+
readonly id: string;
|
|
137
|
+
readonly resetType: string;
|
|
138
|
+
readonly status: string;
|
|
139
|
+
readonly grantedAt: number | null;
|
|
140
|
+
readonly expiresAt: number | null;
|
|
141
|
+
readonly title: string | null;
|
|
142
|
+
readonly description: string | null;
|
|
143
|
+
}
|
|
144
|
+
interface CodexResetCreditsSnapshot {
|
|
145
|
+
readonly availableCount: number;
|
|
146
|
+
readonly applicableAvailableCount: number;
|
|
147
|
+
readonly credits: readonly CodexResetCreditItem[] | null;
|
|
148
|
+
}
|
|
135
149
|
interface CodexQuotaSnapshot {
|
|
136
150
|
readonly planType: string;
|
|
137
151
|
readonly limitReached: boolean;
|
|
138
152
|
readonly primary: CodexQuotaWindow;
|
|
139
153
|
readonly secondary: CodexQuotaWindow | null;
|
|
154
|
+
readonly resetCredits?: CodexResetCreditsSnapshot | undefined;
|
|
140
155
|
readonly fetchedAt: number;
|
|
141
156
|
}
|
|
142
|
-
type
|
|
143
|
-
|
|
144
|
-
readonly quota: CodexQuotaSnapshot;
|
|
157
|
+
type ConsumeResetCreditOutcome = 'reset' | 'nothing_to_reset' | 'no_credit' | 'already_redeemed';
|
|
158
|
+
interface CodexRouteStateProjection {
|
|
145
159
|
readonly defaultFastMode?: boolean | undefined;
|
|
146
160
|
readonly sessionFastModes?: Readonly<Record<string, boolean>> | undefined;
|
|
147
161
|
readonly sessionParents?: Readonly<Record<string, string>> | undefined;
|
|
148
|
-
}
|
|
162
|
+
}
|
|
163
|
+
type CodexConsumeResetCreditResponse = ({
|
|
164
|
+
readonly ok: true;
|
|
165
|
+
readonly outcome: ConsumeResetCreditOutcome;
|
|
166
|
+
readonly quota?: CodexQuotaSnapshot | undefined;
|
|
167
|
+
} & CodexRouteStateProjection) | ({
|
|
149
168
|
readonly ok: false;
|
|
150
169
|
readonly error: string;
|
|
151
170
|
readonly quota?: CodexQuotaSnapshot | undefined;
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
readonly
|
|
155
|
-
|
|
171
|
+
} & CodexRouteStateProjection);
|
|
172
|
+
type CodexQuotaResponse = ({
|
|
173
|
+
readonly ok: true;
|
|
174
|
+
readonly quota: CodexQuotaSnapshot;
|
|
175
|
+
} & CodexRouteStateProjection) | ({
|
|
176
|
+
readonly ok: false;
|
|
177
|
+
readonly error: string;
|
|
178
|
+
readonly quota?: CodexQuotaSnapshot | undefined;
|
|
179
|
+
} & CodexRouteStateProjection);
|
|
156
180
|
interface UsageFetcherOptions {
|
|
157
181
|
readonly resolveCredentials: () => Promise<CodexResolvedCredentials>;
|
|
158
182
|
readonly buildHeaders: (authHeaders: Record<string, string>) => Record<string, string>;
|
|
@@ -162,9 +186,17 @@ interface UsageFetcherOptions {
|
|
|
162
186
|
readonly minForceIntervalMs?: number | undefined;
|
|
163
187
|
readonly autoRefresh?: boolean | undefined;
|
|
164
188
|
}
|
|
189
|
+
declare function parseResetCreditsPayload(raw: unknown, nowMs?: number | undefined): {
|
|
190
|
+
readonly availableCount: number;
|
|
191
|
+
readonly credits: readonly CodexResetCreditItem[];
|
|
192
|
+
} | null;
|
|
165
193
|
declare function parseUsagePayload(raw: unknown, fetchedAt: number): CodexQuotaSnapshot | null;
|
|
166
194
|
declare function createUsageFetcher(options: UsageFetcherOptions): {
|
|
167
195
|
getUsage: (force?: boolean) => Promise<CodexQuotaResponse>;
|
|
196
|
+
consumeResetCredit: (params?: {
|
|
197
|
+
readonly creditId?: string | undefined;
|
|
198
|
+
readonly redeemRequestId?: string | undefined;
|
|
199
|
+
}) => Promise<CodexConsumeResetCreditResponse>;
|
|
168
200
|
onRequestStart: () => void;
|
|
169
201
|
invalidate: () => void;
|
|
170
202
|
dispose: () => void;
|
|
@@ -202,7 +234,8 @@ export declare const __test: Readonly<{
|
|
|
202
234
|
normalizeCodexSessionId: typeof normalizeCodexSessionId;
|
|
203
235
|
toCodexWireSessionId: typeof toCodexWireSessionId;
|
|
204
236
|
parseUsagePayload: typeof parseUsagePayload;
|
|
237
|
+
parseResetCreditsPayload: typeof parseResetCreditsPayload;
|
|
205
238
|
createUsageFetcher: typeof createUsageFetcher;
|
|
206
239
|
}>;
|
|
207
240
|
//#endregion
|
|
208
|
-
export type { CodexModelMetadata, CodexQuotaResponse, CodexQuotaSnapshot, CodexQuotaWindow, CodexResolvedCredentials, ParsedModelsCatalog, PluginContextLike, PluginLogLevel, RawModelsCache };
|
|
241
|
+
export type { CodexConsumeResetCreditResponse, CodexModelMetadata, CodexQuotaResponse, CodexQuotaSnapshot, CodexQuotaWindow, CodexResetCreditItem, CodexResetCreditsSnapshot, CodexResolvedCredentials, ConsumeResetCreditOutcome, ParsedModelsCatalog, PluginContextLike, PluginLogLevel, RawModelsCache };
|
package/lib/index.js
CHANGED
|
@@ -361,6 +361,8 @@ function safeErrorMessage(error) {
|
|
|
361
361
|
return (error instanceof Error ? error.message : String(error)).replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/gu, "[redacted]").replace(/\b(Bearer\s+)[^\s,;'"&]+/giu, "$1[redacted]").replace(/(\b(?:code|token|access_token|refresh_token|id_token)=)[^&\s"']+/giu, "$1[redacted]").replace(/("(?:access_token|refresh_token|id_token)"\s*:\s*")[^"]+"/giu, "$1[redacted]\"").slice(0, 1e3);
|
|
362
362
|
}
|
|
363
363
|
const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
364
|
+
const CODEX_RESET_CREDITS_URL = "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits";
|
|
365
|
+
const CODEX_RESET_CREDITS_CONSUME_URL = "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits/consume";
|
|
364
366
|
const USAGE_ROUTE_PATH = "/api/codex-subscription/usage";
|
|
365
367
|
const DEFAULT_USAGE_TTL_MS = 6e4;
|
|
366
368
|
const USAGE_FETCH_TIMEOUT_MS = 15e3;
|
|
@@ -385,6 +387,77 @@ function parseWindow(raw, fetchedAtMs) {
|
|
|
385
387
|
resetAt
|
|
386
388
|
};
|
|
387
389
|
}
|
|
390
|
+
function parseTimestampSeconds(raw) {
|
|
391
|
+
if (typeof raw === "number" && Number.isFinite(raw) && raw > 0) return Math.round(raw);
|
|
392
|
+
if (typeof raw === "string") {
|
|
393
|
+
const trimmed = raw.trim();
|
|
394
|
+
if (trimmed === "") return null;
|
|
395
|
+
const parsedMs = Date.parse(trimmed);
|
|
396
|
+
if (Number.isFinite(parsedMs) && parsedMs > 0) return Math.floor(parsedMs / 1e3);
|
|
397
|
+
}
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
function nonEmptyString(raw) {
|
|
401
|
+
if (typeof raw !== "string") return null;
|
|
402
|
+
const trimmed = raw.trim();
|
|
403
|
+
return trimmed !== "" ? trimmed : null;
|
|
404
|
+
}
|
|
405
|
+
function parseResetCreditsPayload(raw, nowMs) {
|
|
406
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
407
|
+
const record = raw;
|
|
408
|
+
const rawList = Array.isArray(record.credits) ? record.credits : null;
|
|
409
|
+
if (rawList === null) return null;
|
|
410
|
+
const nowSec = typeof nowMs === "number" && Number.isFinite(nowMs) && nowMs > 0 ? Math.floor(nowMs / 1e3) : null;
|
|
411
|
+
const credits = [];
|
|
412
|
+
for (const entry of rawList) {
|
|
413
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
|
|
414
|
+
const item = entry;
|
|
415
|
+
const id = nonEmptyString(item.id);
|
|
416
|
+
if (id === null) continue;
|
|
417
|
+
const statusRaw = (nonEmptyString(item.status) ?? "available").toLowerCase();
|
|
418
|
+
if (statusRaw !== "available") continue;
|
|
419
|
+
const expiresAt = parseTimestampSeconds(item.expires_at);
|
|
420
|
+
if (nowSec !== null && expiresAt !== null && expiresAt <= nowSec) continue;
|
|
421
|
+
const resetType = nonEmptyString(item.reset_type) ?? "codex_rate_limits";
|
|
422
|
+
const grantedAt = parseTimestampSeconds(item.granted_at);
|
|
423
|
+
const title = nonEmptyString(item.title);
|
|
424
|
+
const description = nonEmptyString(item.description);
|
|
425
|
+
credits.push({
|
|
426
|
+
id,
|
|
427
|
+
resetType,
|
|
428
|
+
status: statusRaw,
|
|
429
|
+
grantedAt,
|
|
430
|
+
expiresAt,
|
|
431
|
+
title,
|
|
432
|
+
description
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
credits.sort((a, b) => {
|
|
436
|
+
if (a.expiresAt !== null && b.expiresAt !== null) {
|
|
437
|
+
if (a.expiresAt !== b.expiresAt) return a.expiresAt - b.expiresAt;
|
|
438
|
+
} else if (a.expiresAt !== null) return -1;
|
|
439
|
+
else if (b.expiresAt !== null) return 1;
|
|
440
|
+
return (a.grantedAt ?? 0) - (b.grantedAt ?? 0);
|
|
441
|
+
});
|
|
442
|
+
const availRaw = record.available_count;
|
|
443
|
+
return {
|
|
444
|
+
availableCount: rawList.length > 0 ? credits.length : typeof availRaw === "number" && Number.isFinite(availRaw) && availRaw >= 0 ? Math.round(availRaw) : 0,
|
|
445
|
+
credits
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
function parseUsageResetCreditsSummary(raw) {
|
|
449
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
450
|
+
const record = raw;
|
|
451
|
+
const availRaw = record.available_count;
|
|
452
|
+
if (typeof availRaw !== "number" || !Number.isFinite(availRaw) || availRaw < 0) return;
|
|
453
|
+
const availableCount = Math.round(availRaw);
|
|
454
|
+
const appRaw = record.applicable_available_count;
|
|
455
|
+
return {
|
|
456
|
+
availableCount,
|
|
457
|
+
applicableAvailableCount: typeof appRaw === "number" && Number.isFinite(appRaw) && appRaw >= 0 ? Math.min(availableCount, Math.round(appRaw)) : availableCount,
|
|
458
|
+
credits: availableCount === 0 ? [] : null
|
|
459
|
+
};
|
|
460
|
+
}
|
|
388
461
|
function parseUsagePayload(raw, fetchedAt) {
|
|
389
462
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
390
463
|
const record = raw;
|
|
@@ -394,11 +467,15 @@ function parseUsagePayload(raw, fetchedAt) {
|
|
|
394
467
|
const primary = parseWindow(rateRecord.primary_window, fetchedAt);
|
|
395
468
|
if (primary === null) return null;
|
|
396
469
|
const secondary = parseWindow(rateRecord.secondary_window, fetchedAt);
|
|
470
|
+
const planType = typeof record.plan_type === "string" && record.plan_type.trim() !== "" ? record.plan_type.trim() : "plus";
|
|
471
|
+
const limitReached = rateRecord.limit_reached === true || rateRecord.allowed === false || primary.usedPercent >= 100 || secondary !== null && secondary.usedPercent >= 100;
|
|
472
|
+
const resetCredits = parseUsageResetCreditsSummary(record.rate_limit_reset_credits);
|
|
397
473
|
return {
|
|
398
|
-
planType
|
|
399
|
-
limitReached
|
|
474
|
+
planType,
|
|
475
|
+
limitReached,
|
|
400
476
|
primary,
|
|
401
477
|
secondary,
|
|
478
|
+
...resetCredits !== void 0 ? { resetCredits } : {},
|
|
402
479
|
fetchedAt
|
|
403
480
|
};
|
|
404
481
|
}
|
|
@@ -411,8 +488,22 @@ function isWindowResetDue(snapshot, nowMs) {
|
|
|
411
488
|
if (secondaryReset !== null && fetchedSec < secondaryReset && nowSec >= secondaryReset) return true;
|
|
412
489
|
return false;
|
|
413
490
|
}
|
|
491
|
+
function normalizeConsumeOutcome(raw) {
|
|
492
|
+
if (typeof raw !== "string") return null;
|
|
493
|
+
switch (raw.trim()) {
|
|
494
|
+
case "reset":
|
|
495
|
+
case "nothing_to_reset":
|
|
496
|
+
case "no_credit":
|
|
497
|
+
case "already_redeemed": return raw.trim();
|
|
498
|
+
default: return null;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
function generateRedeemRequestId(nowMs) {
|
|
502
|
+
const rand = Math.random().toString(16).slice(2, 14).padEnd(12, "0");
|
|
503
|
+
return `redeem-${nowMs.toString(16)}-${rand}`;
|
|
504
|
+
}
|
|
414
505
|
function createUsageFetcher(options) {
|
|
415
|
-
const
|
|
506
|
+
const getFetch = () => options.fetchImpl ?? globalThis.fetch;
|
|
416
507
|
const nowFn = options.now ?? Date.now;
|
|
417
508
|
const ttlMs = options.ttlMs ?? 6e4;
|
|
418
509
|
const minForceIntervalMs = options.minForceIntervalMs ?? 5e3;
|
|
@@ -426,6 +517,15 @@ function createUsageFetcher(options) {
|
|
|
426
517
|
let trailingTimer;
|
|
427
518
|
let disposed = false;
|
|
428
519
|
const isStale = () => dirtyRevision > fetchedRevision;
|
|
520
|
+
async function resolveAuthHeaders(extra) {
|
|
521
|
+
const creds = await options.resolveCredentials();
|
|
522
|
+
return options.buildHeaders({
|
|
523
|
+
Authorization: `Bearer ${creds.accessToken}`,
|
|
524
|
+
Accept: "application/json",
|
|
525
|
+
...creds.accountId !== void 0 ? { "ChatGPT-Account-Id": creds.accountId } : {},
|
|
526
|
+
...extra
|
|
527
|
+
});
|
|
528
|
+
}
|
|
429
529
|
function clearTrailingTimer() {
|
|
430
530
|
if (trailingTimer !== void 0) {
|
|
431
531
|
clearTimeout(trailingTimer);
|
|
@@ -456,12 +556,8 @@ function createUsageFetcher(options) {
|
|
|
456
556
|
async function fetchFresh() {
|
|
457
557
|
const revAtStart = dirtyRevision;
|
|
458
558
|
try {
|
|
459
|
-
const
|
|
460
|
-
const headers =
|
|
461
|
-
Authorization: `Bearer ${creds.accessToken}`,
|
|
462
|
-
Accept: "application/json",
|
|
463
|
-
...creds.accountId !== void 0 ? { "ChatGPT-Account-Id": creds.accountId } : {}
|
|
464
|
-
});
|
|
559
|
+
const fetchFn = getFetch();
|
|
560
|
+
const headers = await resolveAuthHeaders();
|
|
465
561
|
const response = await fetchFn(CODEX_USAGE_URL, {
|
|
466
562
|
method: "GET",
|
|
467
563
|
headers,
|
|
@@ -475,12 +571,33 @@ function createUsageFetcher(options) {
|
|
|
475
571
|
error: `Codex 限额查询失败 (HTTP ${response.status})`,
|
|
476
572
|
...cached !== void 0 ? { quota: cached } : {}
|
|
477
573
|
};
|
|
478
|
-
|
|
574
|
+
let parsed = parseUsagePayload(await response.json(), finishedAt);
|
|
479
575
|
if (parsed === null) return {
|
|
480
576
|
ok: false,
|
|
481
577
|
error: "Codex 限额响应格式无法解析",
|
|
482
578
|
...cached !== void 0 ? { quota: cached } : {}
|
|
483
579
|
};
|
|
580
|
+
if (parsed.resetCredits !== void 0 && parsed.resetCredits.availableCount > 0) try {
|
|
581
|
+
const detailRes = await fetchFn(CODEX_RESET_CREDITS_URL, {
|
|
582
|
+
method: "GET",
|
|
583
|
+
headers,
|
|
584
|
+
signal: AbortSignal.timeout(USAGE_FETCH_TIMEOUT_MS)
|
|
585
|
+
});
|
|
586
|
+
if (detailRes.ok) {
|
|
587
|
+
const detailParsed = parseResetCreditsPayload(await detailRes.json(), finishedAt);
|
|
588
|
+
if (detailParsed !== null) {
|
|
589
|
+
const nextAvailable = detailParsed.availableCount;
|
|
590
|
+
parsed = {
|
|
591
|
+
...parsed,
|
|
592
|
+
resetCredits: {
|
|
593
|
+
availableCount: nextAvailable,
|
|
594
|
+
applicableAvailableCount: Math.min(nextAvailable, parsed.resetCredits.applicableAvailableCount),
|
|
595
|
+
credits: detailParsed.credits
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
} catch {}
|
|
484
601
|
cached = parsed;
|
|
485
602
|
return {
|
|
486
603
|
ok: true,
|
|
@@ -517,6 +634,52 @@ function createUsageFetcher(options) {
|
|
|
517
634
|
disposed = true;
|
|
518
635
|
clearTrailingTimer();
|
|
519
636
|
},
|
|
637
|
+
async consumeResetCredit(params) {
|
|
638
|
+
const creditId = nonEmptyString(params?.creditId) ?? void 0;
|
|
639
|
+
const redeemRequestId = nonEmptyString(params?.redeemRequestId) ?? generateRedeemRequestId(nowFn());
|
|
640
|
+
try {
|
|
641
|
+
const fetchFn = getFetch();
|
|
642
|
+
const headers = await resolveAuthHeaders({ "Content-Type": "application/json" });
|
|
643
|
+
const response = await fetchFn(CODEX_RESET_CREDITS_CONSUME_URL, {
|
|
644
|
+
method: "POST",
|
|
645
|
+
headers,
|
|
646
|
+
body: JSON.stringify({
|
|
647
|
+
redeem_request_id: redeemRequestId,
|
|
648
|
+
...creditId !== void 0 ? { credit_id: creditId } : {}
|
|
649
|
+
}),
|
|
650
|
+
signal: AbortSignal.timeout(USAGE_FETCH_TIMEOUT_MS)
|
|
651
|
+
});
|
|
652
|
+
if (!response.ok) return {
|
|
653
|
+
ok: false,
|
|
654
|
+
error: `Codex 重置卡核销失败 (HTTP ${response.status})`,
|
|
655
|
+
...cached !== void 0 ? { quota: cached } : {}
|
|
656
|
+
};
|
|
657
|
+
const outcome = normalizeConsumeOutcome((await response.json())?.outcome);
|
|
658
|
+
if (outcome === null) return {
|
|
659
|
+
ok: false,
|
|
660
|
+
error: "Codex 重置卡核销响应格式无法解析",
|
|
661
|
+
...cached !== void 0 ? { quota: cached } : {}
|
|
662
|
+
};
|
|
663
|
+
dirtyRevision += 1;
|
|
664
|
+
if (inflight !== void 0) await inflight;
|
|
665
|
+
const refreshed = isStale() ? await runSingleFlight() : cached !== void 0 ? {
|
|
666
|
+
ok: true,
|
|
667
|
+
quota: cached
|
|
668
|
+
} : await runSingleFlight();
|
|
669
|
+
const latestQuota = refreshed.ok ? refreshed.quota : cached;
|
|
670
|
+
return {
|
|
671
|
+
ok: true,
|
|
672
|
+
outcome,
|
|
673
|
+
...latestQuota !== void 0 ? { quota: latestQuota } : {}
|
|
674
|
+
};
|
|
675
|
+
} catch (error) {
|
|
676
|
+
return {
|
|
677
|
+
ok: false,
|
|
678
|
+
error: safeErrorMessage(error),
|
|
679
|
+
...cached !== void 0 ? { quota: cached } : {}
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
},
|
|
520
683
|
async getUsage(force = false) {
|
|
521
684
|
hasConsumer = true;
|
|
522
685
|
if (cached !== void 0) {
|
|
@@ -1419,6 +1582,33 @@ function apply(ctx, config = {}) {
|
|
|
1419
1582
|
body = null;
|
|
1420
1583
|
}
|
|
1421
1584
|
const record = body !== null && typeof body === "object" && !Array.isArray(body) ? body : null;
|
|
1585
|
+
if (record !== null && record.action === "consumeResetCredit") {
|
|
1586
|
+
if (record.creditId !== void 0 && typeof record.creditId !== "string" || record.redeemRequestId !== void 0 && typeof record.redeemRequestId !== "string") return new Response(JSON.stringify({
|
|
1587
|
+
ok: false,
|
|
1588
|
+
error: "Invalid request body: creditId and redeemRequestId must be strings when provided"
|
|
1589
|
+
}), {
|
|
1590
|
+
status: 400,
|
|
1591
|
+
headers: { "content-type": "application/json" }
|
|
1592
|
+
});
|
|
1593
|
+
const consumeResult = await usageFetcher.consumeResetCredit({
|
|
1594
|
+
creditId: record.creditId,
|
|
1595
|
+
redeemRequestId: record.redeemRequestId
|
|
1596
|
+
});
|
|
1597
|
+
appendPluginLog(getCodexHome(), ctx, consumeResult.ok ? "INFO" : "WARN", "reset_credit.consume", {
|
|
1598
|
+
creditId: record.creditId ?? null,
|
|
1599
|
+
ok: consumeResult.ok,
|
|
1600
|
+
...consumeResult.ok ? { outcome: consumeResult.outcome } : { error: consumeResult.error }
|
|
1601
|
+
});
|
|
1602
|
+
return new Response(JSON.stringify({
|
|
1603
|
+
...consumeResult,
|
|
1604
|
+
defaultFastMode: getConfigFastMode(),
|
|
1605
|
+
sessionFastModes: buildProjectedSessionFastModes(),
|
|
1606
|
+
sessionParents: Object.fromEntries(sessionParents)
|
|
1607
|
+
}), {
|
|
1608
|
+
status: 200,
|
|
1609
|
+
headers: { "content-type": "application/json" }
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1422
1612
|
if (record === null || typeof record.fastMode !== "boolean" || record.sessionId !== void 0 && typeof record.sessionId !== "string") return new Response(JSON.stringify({
|
|
1423
1613
|
ok: false,
|
|
1424
1614
|
error: "Invalid request body: boolean fastMode is required"
|
|
@@ -1521,6 +1711,7 @@ const __test = Object.freeze({
|
|
|
1521
1711
|
normalizeCodexSessionId,
|
|
1522
1712
|
toCodexWireSessionId,
|
|
1523
1713
|
parseUsagePayload,
|
|
1714
|
+
parseResetCreditsPayload,
|
|
1524
1715
|
createUsageFetcher
|
|
1525
1716
|
});
|
|
1526
1717
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lyogvce/dsh-codex-subscription",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "复用本机 ~/.codex 订阅凭证与 models_cache.json 模型缓存,在 DSH 中通过 SSE 直连调用 Codex 模型。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -50,6 +50,17 @@
|
|
|
50
50
|
"tsdown": "0.23.0",
|
|
51
51
|
"typescript": "5.9.3"
|
|
52
52
|
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "tsdown",
|
|
55
|
+
"dev": "tsdown --watch",
|
|
56
|
+
"typecheck": "tsc --noEmit",
|
|
57
|
+
"pretest": "pnpm build",
|
|
58
|
+
"test": "node --test tests/*.test.mjs",
|
|
59
|
+
"test:isolation": "./tools/test-isolation.sh",
|
|
60
|
+
"verify": "pnpm typecheck && pnpm test && pnpm test:isolation",
|
|
61
|
+
"prepack": "pnpm build",
|
|
62
|
+
"prepublishOnly": "pnpm verify"
|
|
63
|
+
},
|
|
53
64
|
"publishConfig": {
|
|
54
65
|
"access": "public"
|
|
55
66
|
},
|
|
@@ -60,14 +71,5 @@
|
|
|
60
71
|
"codex",
|
|
61
72
|
"openai-codex",
|
|
62
73
|
"subscription"
|
|
63
|
-
]
|
|
64
|
-
|
|
65
|
-
"build": "tsdown",
|
|
66
|
-
"dev": "tsdown --watch",
|
|
67
|
-
"typecheck": "tsc --noEmit",
|
|
68
|
-
"pretest": "pnpm build",
|
|
69
|
-
"test": "node --test tests/*.test.mjs",
|
|
70
|
-
"test:isolation": "./tools/test-isolation.sh",
|
|
71
|
-
"verify": "pnpm typecheck && pnpm test && pnpm test:isolation"
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
+
]
|
|
75
|
+
}
|