@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 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%` 单窗口,内置 60 秒缓存、单飞合并与每轮回复结束自动刷新),点击可展开查看对应窗口进度条、重置倒计时与手动刷新按钮;切换至其他 Provider 模型时自动隐藏。
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 ttlMs = options.ttlMs ?? 6e4;
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 < ttlMs) return;
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
- setState({
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
- const nextOpen = !open;
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 stale = false;
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(), nowFn());
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
- stale = true;
513
+ dirtyRevision += 1;
514
+ scheduleBackgroundRefresh(false);
515
+ },
516
+ dispose() {
517
+ disposed = true;
518
+ clearTrailingTimer();
454
519
  },
455
520
  async getUsage(force = false) {
456
- if (!stale && cached !== void 0) {
457
- const ageMs = nowFn() - cached.fetchedAt;
458
- if (!force && ageMs < ttlMs) return {
459
- ok: true,
460
- quota: cached
461
- };
462
- if (force && ageMs >= 0 && ageMs < minForceIntervalMs) return {
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
- if (!inflight) inflight = fetchFresh().finally(() => {
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.invalidate();
1218
+ usageFetcher.onRequestStart();
1139
1219
  return baseProvider.stream(model, context, wrapOptions(snapshotCatalog, model, options));
1140
1220
  },
1141
1221
  streamSimple: (model, context, options) => {
1142
- usageFetcher.invalidate();
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.2.13",
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
+ }