@lyogvce/dsh-codex-subscription 0.2.4 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -448,6 +448,7 @@ window.__ModuleLoader__.load({
448
448
  isCodexProvider,
449
449
  didTurnFinish,
450
450
  windowLevelOf,
451
+ formatWindowShortLabel,
451
452
  formatWindowTitle,
452
453
  formatQuotaSummary,
453
454
  formatResetCountdown,
package/lib/index.d.ts CHANGED
@@ -1,4 +1,27 @@
1
1
  import { PiCodexModel } from "@earendil-works/pi-ai/providers/openai-codex";
2
+ //#region src/host-types.d.ts
3
+ interface LlmServiceLike {
4
+ registerAdapter: (providers: string[], adapter: unknown) => (() => void) & {
5
+ replace?: (next: string[]) => void;
6
+ };
7
+ emitAdaptersUpdated?: () => void;
8
+ }
9
+ type PluginLogLevel = 'INFO' | 'WARN' | 'ERROR';
10
+ type PluginLogFn = (level: PluginLogLevel, event: string, detail: Record<string, unknown>) => void;
11
+ interface LoggerLike {
12
+ info?: (message: string) => void;
13
+ warn?: (message: string) => void;
14
+ error?: (message: string) => void;
15
+ }
16
+ interface PluginContextLike {
17
+ llm: LlmServiceLike;
18
+ logger?: LoggerLike | undefined;
19
+ get?: (name: string) => unknown;
20
+ on?: (event: string, listener: () => void) => () => void;
21
+ effect?: (factory: () => void | (() => void), label?: string) => () => void;
22
+ inject?: (services: readonly string[], callback: (ctx: PluginContextLike) => void) => () => void;
23
+ }
24
+ //#endregion
2
25
  //#region src/auth.d.ts
3
26
  interface CodexAuthTokens {
4
27
  access_token?: string;
@@ -19,7 +42,7 @@ interface JwtClaims {
19
42
  interface AuthManagerOptions {
20
43
  getCodexHome: () => string;
21
44
  getClientVersion?: (() => string) | undefined;
22
- onLog?: ((level: 'INFO' | 'WARN' | 'ERROR', event: string, detail: Record<string, unknown>) => void) | undefined;
45
+ onLog?: PluginLogFn | undefined;
23
46
  fetchImpl?: typeof fetch | undefined;
24
47
  now?: (() => number) | undefined;
25
48
  }
@@ -36,6 +59,7 @@ declare function createAuthManager(options: AuthManagerOptions): {
36
59
  };
37
60
  //#endregion
38
61
  //#region src/catalog.d.ts
62
+ export declare const BACKUP_CACHE_FILE_NAME = ".dsh-models-cache-backup.json";
39
63
  interface RawReasoningLevel {
40
64
  effort?: string;
41
65
  description?: string;
@@ -59,6 +83,7 @@ interface RawModelsCache {
59
83
  client_version?: string;
60
84
  models?: RawCodexModelEntry[];
61
85
  }
86
+ declare function parseRawModelsCache(rawText: string): RawModelsCache;
62
87
  declare function compareClientVersions(a: string | undefined, b: string | undefined): number;
63
88
  interface CodexModelMetadata {
64
89
  readonly contextWindow: number;
@@ -115,32 +140,10 @@ declare function createUsageFetcher(options: UsageFetcherOptions): {
115
140
  invalidate: () => void;
116
141
  };
117
142
  //#endregion
118
- //#region src/host-types.d.ts
119
- interface LlmServiceLike {
120
- registerAdapter: (providers: string[], adapter: unknown) => (() => void) & {
121
- replace?: (next: string[]) => void;
122
- };
123
- emitAdaptersUpdated?: () => void;
124
- }
125
- interface LoggerLike {
126
- info?: (message: string) => void;
127
- warn?: (message: string) => void;
128
- error?: (message: string) => void;
129
- }
130
- interface PluginContextLike {
131
- llm: LlmServiceLike;
132
- logger?: LoggerLike | undefined;
133
- get?: (name: string) => unknown;
134
- on?: (event: string, listener: () => void) => () => void;
135
- effect?: (factory: () => void | (() => void), label?: string) => () => void;
136
- inject?: (services: readonly string[], callback: (ctx: PluginContextLike) => void) => () => void;
137
- }
138
- //#endregion
139
143
  //#region src/index.d.ts
140
144
  export declare const name = "codex-subscription";
141
145
  export declare const inject: string[];
142
146
  export declare const LOG_FILE_NAME = "dsh-codex-subscription.log";
143
- export declare const BACKUP_CACHE_FILE_NAME = ".dsh-models-cache-backup.json";
144
147
  export declare const Config: any;
145
148
  export declare function apply(ctx: PluginContextLike, config?: Record<string, unknown>): void;
146
149
  export declare const __test: Readonly<{
@@ -159,6 +162,7 @@ export declare const __test: Readonly<{
159
162
  buildThinkingLevelMap: typeof buildThinkingLevelMap;
160
163
  compareClientVersions: typeof compareClientVersions;
161
164
  parseModelsCache: typeof parseModelsCache;
165
+ parseRawModelsCache: typeof parseRawModelsCache;
162
166
  sanitizeCodexPayload: typeof sanitizeCodexPayload;
163
167
  buildCodexHeaders: typeof buildCodexHeaders;
164
168
  buildCodexUserAgent: typeof buildCodexUserAgent;
@@ -167,4 +171,4 @@ export declare const __test: Readonly<{
167
171
  createUsageFetcher: typeof createUsageFetcher;
168
172
  }>;
169
173
  //#endregion
170
- export type { CodexModelMetadata, CodexQuotaResponse, CodexQuotaSnapshot, CodexQuotaWindow, CodexResolvedCredentials, ParsedModelsCatalog, PluginContextLike, RawModelsCache };
174
+ export type { CodexModelMetadata, CodexQuotaResponse, CodexQuotaSnapshot, CodexQuotaWindow, CodexResolvedCredentials, ParsedModelsCatalog, PluginContextLike, PluginLogLevel, RawModelsCache };
package/lib/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { appendFileSync, chmodSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
2
- import { dirname, join, resolve } from "node:path";
1
+ import { appendFileSync, chmodSync, readFileSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { basename, dirname, join, resolve } from "node:path";
3
3
  import z from "@deepseek-ai/schemastery";
4
4
  import { LlmError, resolveImageAttachmentAccess, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
5
5
  import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
@@ -13,6 +13,7 @@ const PROVIDER = "openai-codex";
13
13
  const DISPLAY_NAME = "Codex";
14
14
  const DEFAULT_CLIENT_VERSION = "0.163.0";
15
15
  const DEFAULT_CONTEXT_WINDOW = 872e3;
16
+ const BACKUP_CACHE_FILE_NAME = ".dsh-models-cache-backup.json";
16
17
  const THINKING_LEVELS = [
17
18
  "off",
18
19
  "minimal",
@@ -29,6 +30,11 @@ const DEFAULT_DISABLED_MODELS = Object.freeze([
29
30
  "gpt-5.6-terra",
30
31
  "gpt-5.6-luna"
31
32
  ]);
33
+ function parseRawModelsCache(rawText) {
34
+ const parsed = JSON.parse(rawText);
35
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new SyntaxError("models_cache.json root must be a JSON object");
36
+ return parsed;
37
+ }
32
38
  function compareClientVersions(a, b) {
33
39
  const parseParts = (ver) => typeof ver === "string" && ver.trim() !== "" ? ver.trim().split(/[^0-9]+/u).filter(Boolean).map((n) => Number.parseInt(n, 10)) : [];
34
40
  const pa = parseParts(a);
@@ -362,11 +368,35 @@ function extractAccountId(parsed, accessToken) {
362
368
  if (typeof jwtAccountId === "string" && jwtAccountId.trim() !== "") return jwtAccountId.trim();
363
369
  }
364
370
  }
365
- async function writeJsonAtomic(filePath, data) {
366
- let targetPath = filePath;
371
+ function resolveRealTargetPath(filePath) {
367
372
  try {
368
- targetPath = realpathSync(filePath);
369
- } catch {}
373
+ return realpathSync(filePath);
374
+ } catch {
375
+ try {
376
+ return join(realpathSync(dirname(filePath)), basename(filePath));
377
+ } catch {
378
+ return filePath;
379
+ }
380
+ }
381
+ }
382
+ function writeFileAtomicSync(filePath, content) {
383
+ const targetPath = resolveRealTargetPath(filePath);
384
+ const tempPath = join(dirname(targetPath), `.${basename(targetPath)}.tmp-${process.pid}-${randomUUID()}`);
385
+ try {
386
+ writeFileSync(tempPath, content, { mode: 384 });
387
+ try {
388
+ chmodSync(tempPath, 384);
389
+ } catch {}
390
+ renameSync(tempPath, targetPath);
391
+ } catch (error) {
392
+ try {
393
+ unlinkSync(tempPath);
394
+ } catch {}
395
+ throw error;
396
+ }
397
+ }
398
+ async function writeJsonAtomic(filePath, data) {
399
+ const targetPath = resolveRealTargetPath(filePath);
370
400
  const tempPath = join(dirname(targetPath), `.auth.json.tmp-${process.pid}-${randomUUID()}`);
371
401
  const content = `${JSON.stringify(data, null, 2)}\n`;
372
402
  await writeFile(tempPath, content, {
@@ -431,25 +461,34 @@ function createAuthManager(options) {
431
461
  return exp * 1e3 - nowFn() > TOKEN_REFRESH_LEAD_MS;
432
462
  }
433
463
  async function refreshToken(authPath, rawAuth) {
434
- const refreshTok = rawAuth?.tokens?.refresh_token;
435
- if (typeof refreshTok !== "string" || refreshTok.trim() === "") throw new LlmError("Codex 凭证已过期且缺少 refresh_token,请重新运行 codex login", "AUTH_FAILED");
436
- const response = await fetchFn(CODEX_TOKEN_URL, {
437
- method: "POST",
438
- headers: buildCodexHeaders({
439
- "Content-Type": "application/json",
440
- Accept: "application/json"
441
- }, options.getClientVersion?.()),
442
- body: JSON.stringify({
443
- client_id: CODEX_CLIENT_ID,
444
- grant_type: "refresh_token",
445
- refresh_token: refreshTok,
446
- scope: "openid profile email"
447
- }),
448
- signal: AbortSignal.timeout(TOKEN_REFRESH_TIMEOUT_MS)
449
- });
450
- if (!response.ok) throw new LlmError(`Codex Token 刷新失败 (HTTP ${response.status}),请重新运行 codex login`, "AUTH_FAILED");
451
- const fresh = await response.json();
452
- if (typeof fresh?.access_token !== "string" || fresh.access_token.trim() === "") throw new LlmError("Codex Token 刷新响应缺少 access_token", "AUTH_FAILED");
464
+ let fresh;
465
+ try {
466
+ const refreshTok = rawAuth?.tokens?.refresh_token;
467
+ if (typeof refreshTok !== "string" || refreshTok.trim() === "") throw new LlmError("Codex 凭证已过期且缺少 refresh_token,请重新运行 codex login", "AUTH_FAILED");
468
+ const response = await fetchFn(CODEX_TOKEN_URL, {
469
+ method: "POST",
470
+ headers: buildCodexHeaders({
471
+ "Content-Type": "application/json",
472
+ Accept: "application/json"
473
+ }, options.getClientVersion?.()),
474
+ body: JSON.stringify({
475
+ client_id: CODEX_CLIENT_ID,
476
+ grant_type: "refresh_token",
477
+ refresh_token: refreshTok,
478
+ scope: "openid profile email"
479
+ }),
480
+ signal: AbortSignal.timeout(TOKEN_REFRESH_TIMEOUT_MS)
481
+ });
482
+ if (!response.ok) throw new LlmError(`Codex Token 刷新失败 (HTTP ${response.status}),请重新运行 codex login`, "AUTH_FAILED");
483
+ fresh = await response.json();
484
+ if (typeof fresh?.access_token !== "string" || fresh.access_token.trim() === "") throw new LlmError("Codex Token 刷新响应缺少 access_token", "AUTH_FAILED");
485
+ } catch (error) {
486
+ options.onLog?.("ERROR", "auth.refresh_failed", {
487
+ authPath,
488
+ error: safeErrorMessage(error)
489
+ });
490
+ throw error;
491
+ }
453
492
  const nextAuth = {
454
493
  ...rawAuth,
455
494
  tokens: {
@@ -514,7 +553,6 @@ function createAuthManager(options) {
514
553
  const name = "codex-subscription";
515
554
  const inject = ["llm"];
516
555
  const LOG_FILE_NAME = "dsh-codex-subscription.log";
517
- const BACKUP_CACHE_FILE_NAME = ".dsh-models-cache-backup.json";
518
556
  const STREAM_IDLE_TIMEOUT_MS = 3e5;
519
557
  const MAX_REQUEST_IMAGE_BYTES = 20971520;
520
558
  const REQUEST_IMAGE_PIXEL_BUDGET = 4194304;
@@ -532,11 +570,10 @@ function appendPluginLog(codexHome, ctx, level, event, detail) {
532
570
  const logPath = join(codexHome, LOG_FILE_NAME);
533
571
  try {
534
572
  if (statSync(logPath).size > MAX_LOG_FILE_BYTES) {
535
- const tail = readFileSync(logPath, "utf8").slice(-Math.floor(MAX_LOG_FILE_BYTES / 2));
536
- writeFileSync(logPath, tail, {
537
- encoding: "utf8",
538
- mode: 384
539
- });
573
+ const buf = readFileSync(logPath);
574
+ const halfOffset = Math.max(0, buf.length - Math.floor(MAX_LOG_FILE_BYTES / 2));
575
+ const newlineIdx = buf.indexOf(10, halfOffset);
576
+ writeFileAtomicSync(logPath, buf.subarray(newlineIdx >= 0 && newlineIdx + 1 < buf.length ? newlineIdx + 1 : halfOffset));
540
577
  }
541
578
  } catch {}
542
579
  appendFileSync(logPath, line, {
@@ -547,19 +584,14 @@ function appendPluginLog(codexHome, ctx, level, event, detail) {
547
584
  }
548
585
  function readBackupCache(codexHome) {
549
586
  try {
550
- return JSON.parse(readFileSync(join(codexHome, BACKUP_CACHE_FILE_NAME), "utf8"));
587
+ return parseRawModelsCache(readFileSync(join(codexHome, BACKUP_CACHE_FILE_NAME), "utf8"));
551
588
  } catch {
552
589
  return;
553
590
  }
554
591
  }
555
592
  function writeBackupCache(codexHome, raw) {
556
593
  try {
557
- const existing = readBackupCache(codexHome);
558
- if (existing && compareClientVersions(raw.client_version, existing.client_version) < 0) return;
559
- writeFileSync(join(codexHome, BACKUP_CACHE_FILE_NAME), `${JSON.stringify(raw, null, 2)}\n`, {
560
- encoding: "utf8",
561
- mode: 384
562
- });
594
+ writeFileAtomicSync(join(codexHome, BACKUP_CACHE_FILE_NAME), `${JSON.stringify(raw, null, 2)}\n`);
563
595
  } catch {}
564
596
  }
565
597
  const Config = z.object({
@@ -609,7 +641,7 @@ function apply(ctx, config = {}) {
609
641
  });
610
642
  let fileSignature;
611
643
  let cachedRaw;
612
- let warnedMissingFile = false;
644
+ let rawRevision = 0;
613
645
  let snapshotSignature;
614
646
  let currentCatalog = {
615
647
  clientVersion: DEFAULT_CLIENT_VERSION,
@@ -670,31 +702,54 @@ function apply(ctx, config = {}) {
670
702
  const cachePath = join(codexHome, "models_cache.json");
671
703
  const disabled = getDisabledModels();
672
704
  const contextWindows = getContextWindows();
705
+ const setActiveRaw = (next) => {
706
+ cachedRaw = next;
707
+ rawRevision += 1;
708
+ };
709
+ const tryRestoreFromBackup = (extra, minVersion) => {
710
+ const backupRaw = readBackupCache(codexHome);
711
+ if (backupRaw === void 0 || minVersion !== void 0 && compareClientVersions(backupRaw.client_version, minVersion) <= 0) return false;
712
+ setActiveRaw(backupRaw);
713
+ appendPluginLog(codexHome, ctx, "WARN", "catalog.backup_restored", {
714
+ cachePath,
715
+ ...extra,
716
+ restoredVersion: backupRaw.client_version ?? "0.163.0",
717
+ restoredFetchedAt: backupRaw.fetched_at ?? null
718
+ });
719
+ return true;
720
+ };
721
+ const handleReadError = (error) => {
722
+ if (cachedRaw === void 0) tryRestoreFromBackup({ reason: safeErrorMessage(error) });
723
+ appendPluginLog(codexHome, ctx, cachedRaw === void 0 ? "ERROR" : "WARN", "catalog.read_error", {
724
+ cachePath,
725
+ hasFallbackSnapshot: cachedRaw !== void 0,
726
+ error: safeErrorMessage(error)
727
+ });
728
+ };
729
+ let stat;
673
730
  try {
674
- const stat = statSync(cachePath);
731
+ stat = statSync(cachePath);
732
+ } catch (error) {
733
+ const missingSig = `missing:${cachePath}:${error?.code ?? "ERR"}`;
734
+ if (missingSig !== fileSignature) {
735
+ fileSignature = missingSig;
736
+ handleReadError(error);
737
+ }
738
+ }
739
+ if (stat !== void 0) {
675
740
  const nextFileSig = `${cachePath}:${stat.mtimeMs}:${stat.size}`;
676
741
  if (nextFileSig !== fileSignature) {
677
- const incomingRaw = JSON.parse(readFileSync(cachePath, "utf8"));
678
742
  fileSignature = nextFileSig;
679
- warnedMissingFile = false;
680
- if (cachedRaw !== void 0 && compareClientVersions(incomingRaw.client_version, cachedRaw.client_version) < 0) appendPluginLog(codexHome, ctx, "WARN", "catalog.downgrade_rejected", {
681
- cachePath,
682
- keptVersion: cachedRaw.client_version ?? "0.163.0",
683
- rejectedVersion: incomingRaw.client_version ?? "unknown",
684
- incomingFetchedAt: incomingRaw.fetched_at ?? null
685
- });
686
- else {
687
- const backupRaw = readBackupCache(codexHome);
688
- if (backupRaw !== void 0 && compareClientVersions(backupRaw.client_version, incomingRaw.client_version) > 0) {
689
- cachedRaw = backupRaw;
690
- appendPluginLog(codexHome, ctx, "WARN", "catalog.backup_restored", {
691
- cachePath,
692
- diskVersion: incomingRaw.client_version ?? "unknown",
693
- restoredVersion: backupRaw.client_version ?? "0.163.0",
694
- restoredFetchedAt: backupRaw.fetched_at ?? null
695
- });
696
- } else {
697
- cachedRaw = incomingRaw;
743
+ try {
744
+ const incomingRaw = parseRawModelsCache(readFileSync(cachePath, "utf8"));
745
+ if (cachedRaw !== void 0 && compareClientVersions(incomingRaw.client_version, cachedRaw.client_version) < 0) appendPluginLog(codexHome, ctx, "WARN", "catalog.downgrade_rejected", {
746
+ cachePath,
747
+ keptVersion: cachedRaw.client_version ?? "0.163.0",
748
+ rejectedVersion: incomingRaw.client_version ?? "unknown",
749
+ incomingFetchedAt: incomingRaw.fetched_at ?? null
750
+ });
751
+ else if (cachedRaw !== void 0 || !tryRestoreFromBackup({ diskVersion: incomingRaw.client_version ?? "unknown" }, incomingRaw.client_version)) {
752
+ setActiveRaw(incomingRaw);
698
753
  writeBackupCache(codexHome, incomingRaw);
699
754
  appendPluginLog(codexHome, ctx, "INFO", "catalog.load", {
700
755
  cachePath,
@@ -703,31 +758,13 @@ function apply(ctx, config = {}) {
703
758
  rawModelCount: Array.isArray(incomingRaw.models) ? incomingRaw.models.length : 0
704
759
  });
705
760
  }
761
+ } catch (error) {
762
+ handleReadError(error);
706
763
  }
707
764
  }
708
- } catch (error) {
709
- if (cachedRaw === void 0) {
710
- const backupRaw = readBackupCache(codexHome);
711
- if (backupRaw !== void 0) {
712
- cachedRaw = backupRaw;
713
- appendPluginLog(codexHome, ctx, "WARN", "catalog.backup_restored", {
714
- cachePath,
715
- reason: safeErrorMessage(error),
716
- restoredVersion: backupRaw.client_version ?? "0.163.0"
717
- });
718
- }
719
- }
720
- if (!warnedMissingFile) {
721
- warnedMissingFile = true;
722
- appendPluginLog(codexHome, ctx, cachedRaw === void 0 ? "ERROR" : "WARN", "catalog.read_error", {
723
- cachePath,
724
- hasFallbackSnapshot: cachedRaw !== void 0,
725
- error: safeErrorMessage(error)
726
- });
727
- }
728
765
  }
729
766
  const nextSnapshotSig = JSON.stringify([
730
- fileSignature ?? "none",
767
+ rawRevision,
731
768
  disabled,
732
769
  contextWindows
733
770
  ]);
@@ -833,6 +870,7 @@ const __test = Object.freeze({
833
870
  buildThinkingLevelMap,
834
871
  compareClientVersions,
835
872
  parseModelsCache,
873
+ parseRawModelsCache,
836
874
  sanitizeCodexPayload,
837
875
  buildCodexHeaders,
838
876
  buildCodexUserAgent,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lyogvce/dsh-codex-subscription",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "复用本机 ~/.codex 订阅凭证与 models_cache.json 模型缓存,在 DSH 中通过 SSE 直连调用 Codex 模型。",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",