@lyogvce/dsh-codex-subscription 0.2.13 → 0.3.0
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 +1 -1
- package/lib/client.js +23 -8
- package/lib/index.d.ts +3 -0
- package/lib/index.js +101 -19
- package/package.json +12 -14
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
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
24
|
5. **Codex 订阅限额内联展示与 ⚡ Fast 极速模式一键切换(`conversation.input.right`)**
|
|
25
|
-
- 当会话选中 `openai-codex` 模型时,在输入框右下角紧邻模型选择器左侧展示限额胶囊(根据 `wham/usage` 返回的 `limit_window_seconds` 自适应展示 Plus 的 `5h X% · 7d Y%` 双窗口或 Pro 的 `7d X%`
|
|
25
|
+
- 当会话选中 `openai-codex` 模型时,在输入框右下角紧邻模型选择器左侧展示限额胶囊(根据 `wham/usage` 返回的 `limit_window_seconds` 自适应展示 Plus 的 `5h X% · 7d Y%` 双窗口或 Pro 的 `7d X%` 单窗口;由模型请求与流完成结算触发、按 60 秒限流在后台自动更新缓存,并由前端周期静默同步,无请求时零远端轮询,点击胶囊直接展开详情弹窗不触发额外请求),弹窗内可查看窗口进度条、重置倒计时与手动刷新按钮;切换至其他 Provider 模型时自动隐藏。
|
|
26
26
|
- **⚡ 会话树级 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
27
|
|
|
28
28
|
---
|
package/lib/client.js
CHANGED
|
@@ -171,7 +171,7 @@ window.__ModuleLoader__.load({
|
|
|
171
171
|
function createQuotaStore(options = {}) {
|
|
172
172
|
const fetchFn = options.fetchImpl ?? globalThis.fetch;
|
|
173
173
|
const nowFn = options.now ?? Date.now;
|
|
174
|
-
const
|
|
174
|
+
const explicitTtlMs = options.ttlMs;
|
|
175
175
|
let state = {
|
|
176
176
|
quota: null,
|
|
177
177
|
defaultFastMode: false,
|
|
@@ -273,15 +273,16 @@ window.__ModuleLoader__.load({
|
|
|
273
273
|
};
|
|
274
274
|
}
|
|
275
275
|
async function refresh(force = false) {
|
|
276
|
-
if (!force && state.quota !== null && nowFn() - state.quota.fetchedAt <
|
|
276
|
+
if (!force && explicitTtlMs !== void 0 && state.quota !== null && nowFn() - state.quota.fetchedAt < explicitTtlMs) return;
|
|
277
277
|
if (inflight !== null && (!force || inflightForce)) return inflight;
|
|
278
278
|
const seq = ++requestSeq;
|
|
279
279
|
const canReplaceFastAtStart = pendingBySeq.size === 0;
|
|
280
280
|
const fastEpochAtStart = fastMutationEpoch;
|
|
281
281
|
inflightForce = force;
|
|
282
|
-
|
|
282
|
+
const showLoading = force || state.quota === null;
|
|
283
|
+
if (showLoading || state.error !== null) setState({
|
|
283
284
|
...state,
|
|
284
|
-
loading: true,
|
|
285
|
+
loading: showLoading ? true : state.loading,
|
|
285
286
|
error: null
|
|
286
287
|
});
|
|
287
288
|
const url = force ? `${USAGE_ROUTE_PATH}?force=1` : USAGE_ROUTE_PATH;
|
|
@@ -489,6 +490,8 @@ window.__ModuleLoader__.load({
|
|
|
489
490
|
const NS = "codex-subscription";
|
|
490
491
|
const SLOT_NAME = "conversation.input.right";
|
|
491
492
|
const SLOT_ENTRY_ID = "codex-subscription:quota";
|
|
493
|
+
const ACTIVE_QUOTA_SYNC_INTERVAL_MS = 5e3;
|
|
494
|
+
const IDLE_QUOTA_SYNC_INTERVAL_MS = 15e3;
|
|
492
495
|
const zh = {
|
|
493
496
|
"quota.title": "Codex 订阅限额",
|
|
494
497
|
"quota.loading": "正在获取 Codex 限额…",
|
|
@@ -547,8 +550,8 @@ window.__ModuleLoader__.load({
|
|
|
547
550
|
prevRunningRef.current = running;
|
|
548
551
|
return;
|
|
549
552
|
}
|
|
550
|
-
quotaStore.refresh(false);
|
|
551
553
|
if (sameSession && didTurnFinish(prevRunningRef.current, running)) quotaStore.refresh(true);
|
|
554
|
+
else if (!sameSession || !running || quotaStore.getSnapshot().quota === null) quotaStore.refresh(false);
|
|
552
555
|
prevRunningRef.current = running;
|
|
553
556
|
}, [
|
|
554
557
|
isCodex,
|
|
@@ -556,6 +559,20 @@ window.__ModuleLoader__.load({
|
|
|
556
559
|
sessionId,
|
|
557
560
|
quotaStore
|
|
558
561
|
]);
|
|
562
|
+
React.useEffect(() => {
|
|
563
|
+
if (!isCodex || typeof globalThis.setInterval !== "function") return;
|
|
564
|
+
const intervalMs = running ? ACTIVE_QUOTA_SYNC_INTERVAL_MS : IDLE_QUOTA_SYNC_INTERVAL_MS;
|
|
565
|
+
const timer = globalThis.setInterval(() => {
|
|
566
|
+
quotaStore.refresh(false);
|
|
567
|
+
}, intervalMs);
|
|
568
|
+
return () => {
|
|
569
|
+
globalThis.clearInterval(timer);
|
|
570
|
+
};
|
|
571
|
+
}, [
|
|
572
|
+
isCodex,
|
|
573
|
+
running,
|
|
574
|
+
quotaStore
|
|
575
|
+
]);
|
|
559
576
|
React.useEffect(() => {
|
|
560
577
|
if (!open || !isCodex || typeof globalThis.document === "undefined") return;
|
|
561
578
|
const onPointerDown = (event) => {
|
|
@@ -609,9 +626,7 @@ window.__ModuleLoader__.load({
|
|
|
609
626
|
"aria-expanded": open,
|
|
610
627
|
title: `${t("quota.title")} (${pillText})`,
|
|
611
628
|
onClick: () => {
|
|
612
|
-
|
|
613
|
-
setOpen(nextOpen);
|
|
614
|
-
if (nextOpen) quotaStore.refresh(false);
|
|
629
|
+
setOpen(!open);
|
|
615
630
|
}
|
|
616
631
|
}, h("svg", {
|
|
617
632
|
viewBox: "0 0 14 14",
|
package/lib/index.d.ts
CHANGED
|
@@ -160,11 +160,14 @@ interface UsageFetcherOptions {
|
|
|
160
160
|
readonly now?: (() => number) | undefined;
|
|
161
161
|
readonly ttlMs?: number | undefined;
|
|
162
162
|
readonly minForceIntervalMs?: number | undefined;
|
|
163
|
+
readonly autoRefresh?: boolean | undefined;
|
|
163
164
|
}
|
|
164
165
|
declare function parseUsagePayload(raw: unknown, fetchedAt: number): CodexQuotaSnapshot | null;
|
|
165
166
|
declare function createUsageFetcher(options: UsageFetcherOptions): {
|
|
166
167
|
getUsage: (force?: boolean) => Promise<CodexQuotaResponse>;
|
|
168
|
+
onRequestStart: () => void;
|
|
167
169
|
invalidate: () => void;
|
|
170
|
+
dispose: () => void;
|
|
168
171
|
};
|
|
169
172
|
//#endregion
|
|
170
173
|
//#region src/index.d.ts
|
package/lib/index.js
CHANGED
|
@@ -402,15 +402,59 @@ function parseUsagePayload(raw, fetchedAt) {
|
|
|
402
402
|
fetchedAt
|
|
403
403
|
};
|
|
404
404
|
}
|
|
405
|
+
function isWindowResetDue(snapshot, nowMs) {
|
|
406
|
+
const nowSec = nowMs / 1e3;
|
|
407
|
+
const fetchedSec = snapshot.fetchedAt / 1e3;
|
|
408
|
+
const primaryReset = snapshot.primary.resetAt;
|
|
409
|
+
if (primaryReset !== null && fetchedSec < primaryReset && nowSec >= primaryReset) return true;
|
|
410
|
+
const secondaryReset = snapshot.secondary?.resetAt ?? null;
|
|
411
|
+
if (secondaryReset !== null && fetchedSec < secondaryReset && nowSec >= secondaryReset) return true;
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
405
414
|
function createUsageFetcher(options) {
|
|
406
415
|
const fetchFn = options.fetchImpl ?? globalThis.fetch;
|
|
407
416
|
const nowFn = options.now ?? Date.now;
|
|
408
417
|
const ttlMs = options.ttlMs ?? 6e4;
|
|
409
418
|
const minForceIntervalMs = options.minForceIntervalMs ?? 5e3;
|
|
419
|
+
const autoRefresh = options.autoRefresh === true;
|
|
410
420
|
let cached;
|
|
411
|
-
let
|
|
421
|
+
let lastAttemptAt;
|
|
422
|
+
let hasConsumer = false;
|
|
423
|
+
let dirtyRevision = 0;
|
|
424
|
+
let fetchedRevision = 0;
|
|
412
425
|
let inflight;
|
|
426
|
+
let trailingTimer;
|
|
427
|
+
let disposed = false;
|
|
428
|
+
const isStale = () => dirtyRevision > fetchedRevision;
|
|
429
|
+
function clearTrailingTimer() {
|
|
430
|
+
if (trailingTimer !== void 0) {
|
|
431
|
+
clearTimeout(trailingTimer);
|
|
432
|
+
trailingTimer = void 0;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
function scheduleBackgroundRefresh(deferIfCached = false) {
|
|
436
|
+
if (!autoRefresh || !hasConsumer || disposed || !isStale() || inflight !== void 0) return;
|
|
437
|
+
const baseTime = cached?.fetchedAt ?? lastAttemptAt;
|
|
438
|
+
if (baseTime === void 0) {
|
|
439
|
+
runSingleFlight();
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
const ageMs = Math.max(0, nowFn() - baseTime);
|
|
443
|
+
if (!deferIfCached && ageMs >= ttlMs) {
|
|
444
|
+
runSingleFlight();
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
if (trailingTimer !== void 0) return;
|
|
448
|
+
const delayMs = deferIfCached ? Math.max(1, ageMs >= ttlMs ? ttlMs : ttlMs - ageMs) : Math.max(1, ttlMs - ageMs);
|
|
449
|
+
trailingTimer = setTimeout(() => {
|
|
450
|
+
trailingTimer = void 0;
|
|
451
|
+
if (disposed || !isStale()) return;
|
|
452
|
+
runSingleFlight();
|
|
453
|
+
}, delayMs);
|
|
454
|
+
trailingTimer.unref?.();
|
|
455
|
+
}
|
|
413
456
|
async function fetchFresh() {
|
|
457
|
+
const revAtStart = dirtyRevision;
|
|
414
458
|
try {
|
|
415
459
|
const creds = await options.resolveCredentials();
|
|
416
460
|
const headers = options.buildHeaders({
|
|
@@ -423,24 +467,28 @@ function createUsageFetcher(options) {
|
|
|
423
467
|
headers,
|
|
424
468
|
signal: AbortSignal.timeout(USAGE_FETCH_TIMEOUT_MS)
|
|
425
469
|
});
|
|
470
|
+
const finishedAt = nowFn();
|
|
471
|
+
lastAttemptAt = finishedAt;
|
|
472
|
+
fetchedRevision = Math.max(fetchedRevision, revAtStart);
|
|
426
473
|
if (!response.ok) return {
|
|
427
474
|
ok: false,
|
|
428
475
|
error: `Codex 限额查询失败 (HTTP ${response.status})`,
|
|
429
476
|
...cached !== void 0 ? { quota: cached } : {}
|
|
430
477
|
};
|
|
431
|
-
const parsed = parseUsagePayload(await response.json(),
|
|
478
|
+
const parsed = parseUsagePayload(await response.json(), finishedAt);
|
|
432
479
|
if (parsed === null) return {
|
|
433
480
|
ok: false,
|
|
434
481
|
error: "Codex 限额响应格式无法解析",
|
|
435
482
|
...cached !== void 0 ? { quota: cached } : {}
|
|
436
483
|
};
|
|
437
484
|
cached = parsed;
|
|
438
|
-
stale = false;
|
|
439
485
|
return {
|
|
440
486
|
ok: true,
|
|
441
487
|
quota: parsed
|
|
442
488
|
};
|
|
443
489
|
} catch (error) {
|
|
490
|
+
lastAttemptAt = nowFn();
|
|
491
|
+
fetchedRevision = Math.max(fetchedRevision, revAtStart);
|
|
444
492
|
return {
|
|
445
493
|
ok: false,
|
|
446
494
|
error: safeErrorMessage(error),
|
|
@@ -448,26 +496,52 @@ function createUsageFetcher(options) {
|
|
|
448
496
|
};
|
|
449
497
|
}
|
|
450
498
|
}
|
|
499
|
+
function runSingleFlight() {
|
|
500
|
+
clearTrailingTimer();
|
|
501
|
+
if (!inflight) inflight = fetchFresh().finally(() => {
|
|
502
|
+
inflight = void 0;
|
|
503
|
+
if (autoRefresh && !disposed && isStale()) scheduleBackgroundRefresh();
|
|
504
|
+
});
|
|
505
|
+
return inflight;
|
|
506
|
+
}
|
|
451
507
|
return {
|
|
508
|
+
onRequestStart() {
|
|
509
|
+
dirtyRevision += 1;
|
|
510
|
+
scheduleBackgroundRefresh(true);
|
|
511
|
+
},
|
|
452
512
|
invalidate() {
|
|
453
|
-
|
|
513
|
+
dirtyRevision += 1;
|
|
514
|
+
scheduleBackgroundRefresh(false);
|
|
515
|
+
},
|
|
516
|
+
dispose() {
|
|
517
|
+
disposed = true;
|
|
518
|
+
clearTrailingTimer();
|
|
454
519
|
},
|
|
455
520
|
async getUsage(force = false) {
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
if (force
|
|
521
|
+
hasConsumer = true;
|
|
522
|
+
if (cached !== void 0) {
|
|
523
|
+
const nowMs = nowFn();
|
|
524
|
+
const ageMs = nowMs - cached.fetchedAt;
|
|
525
|
+
const stale = isStale();
|
|
526
|
+
const resetDue = isWindowResetDue(cached, nowMs);
|
|
527
|
+
if (!force) {
|
|
528
|
+
if (ageMs >= 0 && ageMs < ttlMs && (!stale || autoRefresh)) {
|
|
529
|
+
if (stale) scheduleBackgroundRefresh(false);
|
|
530
|
+
return {
|
|
531
|
+
ok: true,
|
|
532
|
+
quota: cached
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
if (autoRefresh && !stale && !resetDue) return {
|
|
536
|
+
ok: true,
|
|
537
|
+
quota: cached
|
|
538
|
+
};
|
|
539
|
+
} else if (!stale && ageMs >= 0 && ageMs < minForceIntervalMs) return {
|
|
463
540
|
ok: true,
|
|
464
541
|
quota: cached
|
|
465
542
|
};
|
|
466
543
|
}
|
|
467
|
-
|
|
468
|
-
inflight = void 0;
|
|
469
|
-
});
|
|
470
|
-
return inflight;
|
|
544
|
+
return runSingleFlight();
|
|
471
545
|
}
|
|
472
546
|
};
|
|
473
547
|
}
|
|
@@ -1101,7 +1175,7 @@ function apply(ctx, config = {}) {
|
|
|
1101
1175
|
});
|
|
1102
1176
|
function wrapOptions(catalog, model, options = {}) {
|
|
1103
1177
|
const modelMeta = catalog.metadataById.get(model?.id);
|
|
1104
|
-
const { temperature: _temperature, sessionId, onPayload, ...rest } = options;
|
|
1178
|
+
const { temperature: _temperature, sessionId, onPayload, onProviderStreamEvent, ...rest } = options;
|
|
1105
1179
|
const initiatorId = (ctx.get?.("agents"))?.currentInitiator?.()?.id;
|
|
1106
1180
|
const normalizedSessionId = normalizeCodexSessionId(sessionId) ?? normalizeCodexSessionId(initiatorId);
|
|
1107
1181
|
const wireSessionId = toCodexWireSessionId(normalizedSessionId);
|
|
@@ -1117,12 +1191,18 @@ function apply(ctx, config = {}) {
|
|
|
1117
1191
|
}),
|
|
1118
1192
|
async onPayload(payload, requestModel) {
|
|
1119
1193
|
return sanitizeCodexPayload(await onPayload?.(payload, requestModel) ?? payload, modelMeta, getEffectiveFastMode(normalizedSessionId));
|
|
1194
|
+
},
|
|
1195
|
+
async onProviderStreamEvent(event, eventModel) {
|
|
1196
|
+
await onProviderStreamEvent?.(event, eventModel);
|
|
1197
|
+
const eventType = event !== null && typeof event === "object" ? event.type : void 0;
|
|
1198
|
+
if (eventType === "response.completed" || eventType === "response.done" || eventType === "response.incomplete") usageFetcher.invalidate();
|
|
1120
1199
|
}
|
|
1121
1200
|
};
|
|
1122
1201
|
}
|
|
1123
1202
|
const usageFetcher = createUsageFetcher({
|
|
1124
1203
|
resolveCredentials: () => authManager.resolveCredentials(),
|
|
1125
|
-
buildHeaders: (authHeaders) => buildCodexHeaders(authHeaders, ensureSnapshot().catalog.clientVersion)
|
|
1204
|
+
buildHeaders: (authHeaders) => buildCodexHeaders(authHeaders, ensureSnapshot().catalog.clientVersion),
|
|
1205
|
+
autoRefresh: true
|
|
1126
1206
|
});
|
|
1127
1207
|
function createSnapshotPiProvider(snapshotCatalog) {
|
|
1128
1208
|
return Object.freeze({
|
|
@@ -1135,11 +1215,11 @@ function apply(ctx, config = {}) {
|
|
|
1135
1215
|
}),
|
|
1136
1216
|
getModels: () => snapshotCatalog.models,
|
|
1137
1217
|
stream: (model, context, options) => {
|
|
1138
|
-
usageFetcher.
|
|
1218
|
+
usageFetcher.onRequestStart();
|
|
1139
1219
|
return baseProvider.stream(model, context, wrapOptions(snapshotCatalog, model, options));
|
|
1140
1220
|
},
|
|
1141
1221
|
streamSimple: (model, context, options) => {
|
|
1142
|
-
usageFetcher.
|
|
1222
|
+
usageFetcher.onRequestStart();
|
|
1143
1223
|
return baseProvider.streamSimple(model, context, wrapOptions(snapshotCatalog, model, options));
|
|
1144
1224
|
}
|
|
1145
1225
|
});
|
|
@@ -1311,6 +1391,7 @@ function apply(ctx, config = {}) {
|
|
|
1311
1391
|
return () => {
|
|
1312
1392
|
watcherDisposed = true;
|
|
1313
1393
|
closeModelsCacheWatchers();
|
|
1394
|
+
usageFetcher.dispose();
|
|
1314
1395
|
};
|
|
1315
1396
|
}, "codex-subscription: models_cache watcher");
|
|
1316
1397
|
else {
|
|
@@ -1318,6 +1399,7 @@ function apply(ctx, config = {}) {
|
|
|
1318
1399
|
ctx.on?.("dispose", () => {
|
|
1319
1400
|
watcherDisposed = true;
|
|
1320
1401
|
closeModelsCacheWatchers();
|
|
1402
|
+
usageFetcher.dispose();
|
|
1321
1403
|
});
|
|
1322
1404
|
}
|
|
1323
1405
|
const mountUsageRoute = (connCtx) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lyogvce/dsh-codex-subscription",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "复用本机 ~/.codex 订阅凭证与 models_cache.json 模型缓存,在 DSH 中通过 SSE 直连调用 Codex 模型。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -50,17 +50,6 @@
|
|
|
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
|
-
},
|
|
64
53
|
"publishConfig": {
|
|
65
54
|
"access": "public"
|
|
66
55
|
},
|
|
@@ -71,5 +60,14 @@
|
|
|
71
60
|
"codex",
|
|
72
61
|
"openai-codex",
|
|
73
62
|
"subscription"
|
|
74
|
-
]
|
|
75
|
-
|
|
63
|
+
],
|
|
64
|
+
"scripts": {
|
|
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
|
+
}
|