@lyogvce/dsh-codex-subscription 0.2.8 → 0.2.9

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.
Files changed (3) hide show
  1. package/lib/client.js +85 -38
  2. package/lib/index.js +66 -28
  3. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -157,35 +157,49 @@ window.__ModuleLoader__.load({
157
157
  loading: false,
158
158
  error: null
159
159
  };
160
+ let confirmedSessionFastModes = {};
161
+ let lastConfirmedFastSeq = 0;
162
+ const latestSeqBySession = new Map();
163
+ const pendingBySession = new Map();
160
164
  let inflight = null;
161
165
  let inflightForce = false;
162
166
  let requestSeq = 0;
163
167
  let fastModeSeq = 0;
168
+ let fastMutationEpoch = 0;
164
169
  const listeners = new Set();
165
170
  function setState(next) {
166
171
  state = next;
167
172
  for (const listener of listeners) listener();
168
173
  }
174
+ function deriveSessionFastModes() {
175
+ if (pendingBySession.size === 0) return confirmedSessionFastModes;
176
+ const merged = { ...confirmedSessionFastModes };
177
+ for (const [k, pending] of pendingBySession) merged[k] = pending.enabled;
178
+ return merged;
179
+ }
180
+ function parseServerSessionFastModes(raw) {
181
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
182
+ const nextMap = {};
183
+ for (const [k, v] of Object.entries(raw)) if (typeof k === "string" && k !== "" && typeof v === "boolean") nextMap[k] = v;
184
+ return nextMap;
185
+ }
169
186
  function mergeFastStateFromPayload(payload, allowSessionReplace) {
170
187
  const defaultFastMode = typeof payload?.defaultFastMode === "boolean" ? payload.defaultFastMode : state.defaultFastMode;
171
- if (allowSessionReplace && payload?.sessionFastModes && typeof payload.sessionFastModes === "object" && !Array.isArray(payload.sessionFastModes)) {
172
- const nextMap = {};
173
- for (const [k, v] of Object.entries(payload.sessionFastModes)) if (typeof k === "string" && k !== "" && typeof v === "boolean") nextMap[k] = v;
174
- return {
175
- defaultFastMode,
176
- sessionFastModes: nextMap
177
- };
188
+ if (allowSessionReplace) {
189
+ const parsedMap = parseServerSessionFastModes(payload?.sessionFastModes);
190
+ if (parsedMap !== void 0) confirmedSessionFastModes = parsedMap;
178
191
  }
179
192
  return {
180
193
  defaultFastMode,
181
- sessionFastModes: state.sessionFastModes
194
+ sessionFastModes: deriveSessionFastModes()
182
195
  };
183
196
  }
184
197
  async function refresh(force = false) {
185
198
  if (!force && state.quota !== null && nowFn() - state.quota.fetchedAt < ttlMs) return;
186
199
  if (inflight !== null && (!force || inflightForce)) return inflight;
187
200
  const seq = ++requestSeq;
188
- const fastSeqAtStart = fastModeSeq;
201
+ const canReplaceFastAtStart = pendingBySession.size === 0;
202
+ const fastEpochAtStart = fastMutationEpoch;
189
203
  inflightForce = force;
190
204
  setState({
191
205
  ...state,
@@ -210,7 +224,7 @@ window.__ModuleLoader__.load({
210
224
  }
211
225
  const payload = await response.json();
212
226
  if (seq !== requestSeq) return;
213
- const fastState = mergeFastStateFromPayload(payload, fastSeqAtStart === fastModeSeq);
227
+ const fastState = mergeFastStateFromPayload(payload, canReplaceFastAtStart && pendingBySession.size === 0 && fastEpochAtStart === fastMutationEpoch);
214
228
  if (payload && payload.ok === true && payload.quota) setState({
215
229
  quota: payload.quota,
216
230
  ...fastState,
@@ -248,12 +262,26 @@ window.__ModuleLoader__.load({
248
262
  const nextFastMode = Boolean(enabled);
249
263
  const key = normalizeCodexSessionId(sessionId) ?? DEFAULT_SESSION_KEY;
250
264
  const seq = ++fastModeSeq;
265
+ fastMutationEpoch += 1;
266
+ latestSeqBySession.set(key, seq);
267
+ pendingBySession.set(key, {
268
+ seq,
269
+ enabled: nextFastMode
270
+ });
271
+ const rollback = (errorMsg) => {
272
+ if (latestSeqBySession.get(key) !== seq) return;
273
+ fastMutationEpoch += 1;
274
+ pendingBySession.delete(key);
275
+ setState({
276
+ ...state,
277
+ sessionFastModes: deriveSessionFastModes(),
278
+ error: errorMsg
279
+ });
280
+ };
251
281
  setState({
252
282
  ...state,
253
- sessionFastModes: {
254
- ...state.sessionFastModes,
255
- [key]: nextFastMode
256
- }
283
+ sessionFastModes: deriveSessionFastModes(),
284
+ error: null
257
285
  });
258
286
  try {
259
287
  const response = await fetchFn(USAGE_ROUTE_PATH, {
@@ -267,13 +295,38 @@ window.__ModuleLoader__.load({
267
295
  fastMode: nextFastMode
268
296
  })
269
297
  });
270
- if (seq !== fastModeSeq || !response.ok) return;
298
+ if (latestSeqBySession.get(key) !== seq) return;
299
+ if (!response.ok) {
300
+ const errPayload = await response.json().catch(() => null);
301
+ rollback(typeof errPayload?.error === "string" && errPayload.error !== "" ? errPayload.error : `HTTP ${response.status}`);
302
+ return;
303
+ }
271
304
  const payload = await response.json();
272
- if (seq === fastModeSeq) setState({
305
+ if (latestSeqBySession.get(key) !== seq) return;
306
+ if (!payload || payload.ok === false) {
307
+ rollback(typeof payload?.error === "string" && payload.error !== "" ? payload.error : "Invalid response");
308
+ return;
309
+ }
310
+ fastMutationEpoch += 1;
311
+ pendingBySession.delete(key);
312
+ const parsedMap = parseServerSessionFastModes(payload.sessionFastModes);
313
+ if (seq > lastConfirmedFastSeq && parsedMap !== void 0) {
314
+ lastConfirmedFastSeq = seq;
315
+ confirmedSessionFastModes = parsedMap;
316
+ } else confirmedSessionFastModes = {
317
+ ...confirmedSessionFastModes,
318
+ [key]: parsedMap?.[key] ?? nextFastMode
319
+ };
320
+ const defaultFastMode = typeof payload.defaultFastMode === "boolean" ? payload.defaultFastMode : state.defaultFastMode;
321
+ setState({
273
322
  ...state,
274
- ...mergeFastStateFromPayload(payload, true)
323
+ defaultFastMode,
324
+ sessionFastModes: deriveSessionFastModes(),
325
+ error: null
275
326
  });
276
- } catch {}
327
+ } catch (error) {
328
+ rollback(safeErrorMessage(error));
329
+ }
277
330
  }
278
331
  return {
279
332
  getSnapshot: () => state,
@@ -359,6 +412,18 @@ window.__ModuleLoader__.load({
359
412
  getSnapshot: () => null,
360
413
  subscribe: () => () => {}
361
414
  };
415
+ function renderFastBolt(size) {
416
+ return h("svg", {
417
+ className: "dsh-codex-quota-fast-bolt",
418
+ viewBox: "0 0 12 12",
419
+ width: size,
420
+ height: size,
421
+ "aria-hidden": true
422
+ }, h("path", {
423
+ d: "M6.8 1L2.5 6.8h3.1L4.8 11l4.7-5.8H6.4L6.8 1z",
424
+ fill: "currentColor"
425
+ }));
426
+ }
362
427
  function CodexQuotaPill(props) {
363
428
  const { sessionId, quotaStore, getModelStore, useProjection, useSession, t, getLocale } = props;
364
429
  const modelStore = getModelStore(sessionId);
@@ -462,16 +527,7 @@ window.__ModuleLoader__.load({
462
527
  r: 5.5,
463
528
  strokeDasharray: ringDash,
464
529
  transform: "rotate(-90 7 7)"
465
- })), isFastMode ? h("svg", {
466
- className: "dsh-codex-quota-fast-bolt",
467
- viewBox: "0 0 12 12",
468
- width: 11,
469
- height: 11,
470
- "aria-hidden": true
471
- }, h("path", {
472
- d: "M6.8 1L2.5 6.8h3.1L4.8 11l4.7-5.8H6.4L6.8 1z",
473
- fill: "currentColor"
474
- })) : null, h("span", { className: "dsh-codex-quota-text" }, pillText)), open ? h("div", {
530
+ })), isFastMode ? renderFastBolt(11) : null, h("span", { className: "dsh-codex-quota-text" }, pillText)), open ? h("div", {
475
531
  className: "dsh-codex-quota-popover",
476
532
  role: "dialog",
477
533
  "aria-label": t("quota.title"),
@@ -485,16 +541,7 @@ window.__ModuleLoader__.load({
485
541
  onClick: () => {
486
542
  quotaStore.refresh(true);
487
543
  }
488
- }, 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" }, h("svg", {
489
- className: "dsh-codex-quota-fast-bolt",
490
- viewBox: "0 0 12 12",
491
- width: 12,
492
- height: 12,
493
- "aria-hidden": true
494
- }, h("path", {
495
- d: "M6.8 1L2.5 6.8h3.1L4.8 11l4.7-5.8H6.4L6.8 1z",
496
- fill: "currentColor"
497
- })), h("span", null, t("quota.fastTitle"))), h("div", { className: "dsh-codex-fast-desc" }, t("quota.fastDesc"))), h("button", {
544
+ }, 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", {
498
545
  type: "button",
499
546
  role: "switch",
500
547
  className: "dsh-codex-fast-switch",
package/lib/index.js CHANGED
@@ -35,6 +35,8 @@ const DEFAULT_DISABLED_MODELS = Object.freeze([
35
35
  function parseRawModelsCache(rawText) {
36
36
  const parsed = JSON.parse(rawText);
37
37
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new SyntaxError("models_cache.json root must be a JSON object");
38
+ const models = parsed.models;
39
+ if (!Array.isArray(models) || !models.some((entry) => entry !== null && typeof entry === "object" && typeof entry.slug === "string" && entry.slug.trim() !== "" && entry.visibility === "list")) throw new SyntaxError("models_cache.json must contain at least one visible model entry");
38
40
  return parsed;
39
41
  }
40
42
  function normalizeCodexSessionId(sessionId) {
@@ -94,7 +96,7 @@ function resolveFastServiceTier(entry) {
94
96
  if (entry.service_tiers.length === 0 && Array.isArray(entry.additional_speed_tiers)) return entry.additional_speed_tiers.some((t) => typeof t === "string" && t.trim().toLowerCase() === "fast") ? "priority" : void 0;
95
97
  return;
96
98
  }
97
- if (Array.isArray(entry.additional_speed_tiers) && entry.additional_speed_tiers.some((t) => typeof t === "string" && t.trim().toLowerCase() === "fast")) return "priority";
99
+ if (Array.isArray(entry.additional_speed_tiers)) return entry.additional_speed_tiers.some((t) => typeof t === "string" && t.trim().toLowerCase() === "fast") ? "priority" : void 0;
98
100
  return "priority";
99
101
  }
100
102
  function parseModelsCache(rawCache, baseModels, disabledModels = [], contextWindows = {}) {
@@ -218,7 +220,7 @@ function sanitizeCodexPayload(payload, modelMeta, fastMode = false) {
218
220
  const tier = modelMeta === void 0 ? "priority" : modelMeta.fastServiceTier;
219
221
  if (typeof tier === "string" && tier !== "") cleaned.service_tier = tier;
220
222
  else delete cleaned.service_tier;
221
- } else if (cleaned.service_tier !== "priority" && cleaned.service_tier !== "ultrafast") delete cleaned.service_tier;
223
+ } else delete cleaned.service_tier;
222
224
  const currentText = cleaned.text && typeof cleaned.text === "object" && !Array.isArray(cleaned.text) ? { ...cleaned.text } : void 0;
223
225
  if (modelMeta?.supportVerbosity === false) {
224
226
  if (currentText !== void 0) {
@@ -591,6 +593,12 @@ const DEFAULT_SESSION_KEY = "__default__";
591
593
  function resolvePluginCacheDir(codexHome) {
592
594
  return join(codexHome, PLUGIN_CACHE_DIR_NAME);
593
595
  }
596
+ function sliceUtf8Prefix(buf, maxBytes) {
597
+ if (buf.length <= maxBytes) return buf;
598
+ let end = maxBytes;
599
+ while (end > 0 && (buf[end] & 192) === 128) end -= 1;
600
+ return buf.subarray(0, end);
601
+ }
594
602
  function appendPluginLog(codexHome, ctx, level, event, detail) {
595
603
  const line = `[${new Date().toISOString()}] [${level}] ${event} ${JSON.stringify(detail)}\n`;
596
604
  if (level === "ERROR") {
@@ -598,6 +606,7 @@ function appendPluginLog(codexHome, ctx, level, event, detail) {
598
606
  else console.error(`[dsh-codex-subscription] ${event}: ${JSON.stringify(detail)}`);
599
607
  } else if (level === "WARN") {
600
608
  if (typeof ctx.logger?.warn === "function") ctx.logger.warn(`[dsh-codex-subscription] ${event}: ${JSON.stringify(detail)}`);
609
+ else console.warn(`[dsh-codex-subscription] ${event}: ${JSON.stringify(detail)}`);
601
610
  }
602
611
  try {
603
612
  const cacheDir = resolvePluginCacheDir(codexHome);
@@ -606,18 +615,24 @@ function appendPluginLog(codexHome, ctx, level, event, detail) {
606
615
  mode: 448
607
616
  });
608
617
  const logPath = join(cacheDir, LOG_FILE_NAME);
618
+ const rawLineBuf = Buffer.from(line, "utf8");
619
+ const lineBuf = rawLineBuf.length <= MAX_LOG_FILE_BYTES ? rawLineBuf : Buffer.concat([sliceUtf8Prefix(rawLineBuf, 524287), Buffer.from("\n", "utf8")]);
620
+ let st;
609
621
  try {
610
- if (statSync(logPath).size > MAX_LOG_FILE_BYTES) {
611
- const buf = readFileSync(logPath);
612
- const halfOffset = Math.max(0, buf.length - Math.floor(MAX_LOG_FILE_BYTES / 2));
613
- const newlineIdx = buf.indexOf(10, halfOffset);
614
- writeFileAtomicSync(logPath, buf.subarray(newlineIdx >= 0 && newlineIdx + 1 < buf.length ? newlineIdx + 1 : halfOffset));
615
- }
616
- } catch {}
617
- appendFileSync(logPath, line, {
618
- encoding: "utf8",
619
- mode: 384
620
- });
622
+ st = statSync(logPath);
623
+ } catch {
624
+ st = void 0;
625
+ }
626
+ if (st !== void 0 && st.size + lineBuf.length > MAX_LOG_FILE_BYTES) {
627
+ const buf = readFileSync(logPath);
628
+ const keepBudget = Math.max(0, Math.floor(MAX_LOG_FILE_BYTES / 2) - lineBuf.length);
629
+ const startOffset = Math.max(0, buf.length - keepBudget);
630
+ const newlineIdx = keepBudget > 0 ? buf.indexOf(10, startOffset) : -1;
631
+ const tail = newlineIdx >= 0 && newlineIdx + 1 < buf.length ? buf.subarray(newlineIdx + 1) : Buffer.alloc(0);
632
+ writeFileAtomicSync(logPath, Buffer.concat([tail, lineBuf]));
633
+ return;
634
+ }
635
+ appendFileSync(logPath, lineBuf, { mode: 384 });
621
636
  } catch {}
622
637
  }
623
638
  function readBackupCache(codexHome) {
@@ -627,10 +642,12 @@ function readBackupCache(codexHome) {
627
642
  return;
628
643
  }
629
644
  }
630
- function writeBackupCache(codexHome, raw) {
645
+ function writeBackupCache(codexHome, ctx, raw) {
631
646
  try {
632
647
  writeFileAtomicSync(join(resolvePluginCacheDir(codexHome), BACKUP_CACHE_FILE_NAME), `${JSON.stringify(raw, null, 2)}\n`);
633
- } catch {}
648
+ } catch (error) {
649
+ appendPluginLog(codexHome, ctx, "WARN", "catalog.backup_write_error", { error: safeErrorMessage(error) });
650
+ }
634
651
  }
635
652
  function readPersistedSessionFastModes(codexHome) {
636
653
  const result = new Map();
@@ -642,10 +659,12 @@ function readPersistedSessionFastModes(codexHome) {
642
659
  } catch {}
643
660
  return result;
644
661
  }
645
- function writePersistedSessionFastModes(codexHome, sessionFastModes) {
662
+ function writePersistedSessionFastModes(codexHome, ctx, sessionFastModes) {
646
663
  try {
647
664
  writeFileAtomicSync(join(resolvePluginCacheDir(codexHome), RUNTIME_STATE_FILE_NAME), `${JSON.stringify({ sessionFastModes: Object.fromEntries(sessionFastModes) }, null, 2)}\n`);
648
- } catch {}
665
+ } catch (error) {
666
+ appendPluginLog(codexHome, ctx, "WARN", "fast_mode.persist_error", { error: safeErrorMessage(error) });
667
+ }
649
668
  }
650
669
  const Config = z.object({
651
670
  fastMode: z.boolean().default(false).description("新会话默认是否开启 Fast 极速模式(未填时默认 false;单会话可在右下角限额弹窗内独立切换)").volatile(),
@@ -706,7 +725,7 @@ function apply(ctx, config = {}) {
706
725
  if (oldest === void 0) break;
707
726
  sessionFastModes.delete(oldest);
708
727
  }
709
- writePersistedSessionFastModes(codexHome, sessionFastModes);
728
+ writePersistedSessionFastModes(codexHome, ctx, sessionFastModes);
710
729
  appendPluginLog(codexHome, ctx, "INFO", "fast_mode.updated", {
711
730
  sessionId: key,
712
731
  fastMode: nextFastMode
@@ -719,6 +738,7 @@ function apply(ctx, config = {}) {
719
738
  });
720
739
  let fileSignature;
721
740
  let cachedRaw;
741
+ let lastRejectedRaw;
722
742
  let rawRevision = 0;
723
743
  let snapshotSignature;
724
744
  let currentCatalog = {
@@ -782,6 +802,7 @@ function apply(ctx, config = {}) {
782
802
  const contextWindows = getContextWindows();
783
803
  const setActiveRaw = (next) => {
784
804
  cachedRaw = next;
805
+ lastRejectedRaw = void 0;
785
806
  rawRevision += 1;
786
807
  };
787
808
  const tryRestoreFromBackup = (extra, minVersion) => {
@@ -820,16 +841,20 @@ function apply(ctx, config = {}) {
820
841
  fileSignature = nextFileSig;
821
842
  try {
822
843
  const incomingRaw = parseRawModelsCache(readFileSync(cachePath, "utf8"));
823
- if (cachedRaw !== void 0 && compareClientVersions(incomingRaw.client_version, cachedRaw.client_version) < 0) appendPluginLog(codexHome, ctx, "WARN", "catalog.downgrade_rejected", {
824
- cachePath,
825
- keptVersion: cachedRaw.client_version ?? "0.163.0",
826
- rejectedVersion: incomingRaw.client_version ?? "unknown",
827
- incomingFetchedAt: incomingRaw.fetched_at ?? null
828
- });
829
- else if (cachedRaw !== void 0 || !tryRestoreFromBackup({ diskVersion: incomingRaw.client_version ?? "unknown" }, incomingRaw.client_version)) {
844
+ if (cachedRaw !== void 0 && compareClientVersions(incomingRaw.client_version, cachedRaw.client_version) < 0) {
845
+ if (!isSameModelsCatalogPayload(lastRejectedRaw, incomingRaw)) {
846
+ lastRejectedRaw = incomingRaw;
847
+ appendPluginLog(codexHome, ctx, "WARN", "catalog.downgrade_rejected", {
848
+ cachePath,
849
+ keptVersion: cachedRaw.client_version ?? "0.163.0",
850
+ rejectedVersion: incomingRaw.client_version ?? "unknown",
851
+ incomingFetchedAt: incomingRaw.fetched_at ?? null
852
+ });
853
+ }
854
+ } else if (cachedRaw !== void 0 || !tryRestoreFromBackup({ diskVersion: incomingRaw.client_version ?? "unknown" }, incomingRaw.client_version)) {
830
855
  if (!isSameModelsCatalogPayload(cachedRaw, incomingRaw)) {
831
856
  setActiveRaw(incomingRaw);
832
- writeBackupCache(codexHome, incomingRaw);
857
+ writeBackupCache(codexHome, ctx, incomingRaw);
833
858
  appendPluginLog(codexHome, ctx, "INFO", "catalog.load", {
834
859
  cachePath,
835
860
  clientVersion: incomingRaw.client_version ?? "0.163.0",
@@ -917,8 +942,21 @@ function apply(ctx, config = {}) {
917
942
  requestBody: "buffered",
918
943
  fetch: async (request) => {
919
944
  if (request.method === "POST") {
920
- const body = await request.json().catch(() => null);
921
- if (typeof body?.fastMode === "boolean") setSessionFastMode(typeof body.sessionId === "string" ? body.sessionId : void 0, body.fastMode);
945
+ let body;
946
+ try {
947
+ body = await request.json();
948
+ } catch {
949
+ body = null;
950
+ }
951
+ const record = body !== null && typeof body === "object" && !Array.isArray(body) ? body : null;
952
+ if (record === null || typeof record.fastMode !== "boolean" || record.sessionId !== void 0 && typeof record.sessionId !== "string") return new Response(JSON.stringify({
953
+ ok: false,
954
+ error: "Invalid request body: boolean fastMode is required"
955
+ }), {
956
+ status: 400,
957
+ headers: { "content-type": "application/json" }
958
+ });
959
+ setSessionFastMode(record.sessionId, record.fastMode);
922
960
  return new Response(JSON.stringify({
923
961
  ok: true,
924
962
  defaultFastMode: getConfigFastMode(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lyogvce/dsh-codex-subscription",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "复用本机 ~/.codex 订阅凭证与 models_cache.json 模型缓存,在 DSH 中通过 SSE 直连调用 Codex 模型。",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",