@lyogvce/dsh-codex-subscription 0.2.3 → 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/README.md +4 -2
- package/lib/index.d.ts +32 -16
- package/lib/index.js +194 -39
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,8 +11,10 @@
|
|
|
11
11
|
1. **与本机 Codex CLI / Desktop 100% 共享登录态**
|
|
12
12
|
- 直接读取 `$CODEX_HOME/auth.json`(默认 `~/.codex/auth.json`)。
|
|
13
13
|
- 每次请求前检查 `access_token` 的 JWT `exp`:距离过期不足 5 分钟时自动通过单飞锁(Single-flight)向 `https://auth.openai.com/oauth/token` 刷新,并通过 `realpathSync` + 临时文件 `rename` 原子写回 `auth.json`,保证与 Codex CLI 永不互踢下线。
|
|
14
|
-
2.
|
|
15
|
-
- 直接读取 `~/.codex/models_cache.json
|
|
14
|
+
2. **零网络轮询的本地模型目录发现、防降级保护与可观测性日志**
|
|
15
|
+
- 直接读取 `~/.codex/models_cache.json`(每次按文件 `mtime` 与大小做微秒级 `stat` 校验,仅在文件变化时重读解析),提取 `visibility === "list"` 的模型及其 `context_window`、`supported_reasoning_levels`(自动将 `ultra` 映射为 DSH 的 `max` 档位)与 `default_verbosity`。
|
|
16
|
+
- **防旧版客户端降级污染**:自动将高版本 `models_cache.json` 备份至 `~/.codex/.dsh-models-cache-backup.json`;当外部旧版进程(如 `dsh-subagent-codex` 内置的 `0.153.4`)将 `models_cache.json` 降级覆写为不含新模型的旧缓存时,自动拦截降级并保留高版本模型目录。
|
|
17
|
+
- **可观测性日志**:自动在 `~/.codex/dsh-codex-subscription.log`(带 512KB 自动滚动截断)记录目录加载(`catalog.load` / `catalog.snapshot`)、降级拦截(`catalog.downgrade_rejected`)、备份恢复(`catalog.backup_restored`)与凭证续期(`auth.refreshed`)事件。
|
|
16
18
|
3. **支持按配置屏蔽模型(`disabledModels`)与自定义上下文窗口(`contextWindows`)**
|
|
17
19
|
- 在插件配置中通过 `disabledModels` 屏蔽指定模型 `slug`,保持模型选择器干净清爽。
|
|
18
20
|
- 通过 `contextWindows` 按模型 `slug` 自定义向 DSH 会话与自动压缩器暴露的预算窗口(例如设为 `400000`);未配置时默认使用模型物理上限 `max_context_window`(`872000`)。同时底层 `PiCodexModel.contextWindow` 始终保持物理上限 `872000`,彻底防止 `dsh-llm-pi-ai` 在预算窗口与物理上限之间误判 `CONTEXT_WINDOW_EXCEEDED`。
|
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,6 +42,7 @@ interface JwtClaims {
|
|
|
19
42
|
interface AuthManagerOptions {
|
|
20
43
|
getCodexHome: () => string;
|
|
21
44
|
getClientVersion?: (() => string) | undefined;
|
|
45
|
+
onLog?: PluginLogFn | undefined;
|
|
22
46
|
fetchImpl?: typeof fetch | undefined;
|
|
23
47
|
now?: (() => number) | undefined;
|
|
24
48
|
}
|
|
@@ -35,6 +59,7 @@ declare function createAuthManager(options: AuthManagerOptions): {
|
|
|
35
59
|
};
|
|
36
60
|
//#endregion
|
|
37
61
|
//#region src/catalog.d.ts
|
|
62
|
+
export declare const BACKUP_CACHE_FILE_NAME = ".dsh-models-cache-backup.json";
|
|
38
63
|
interface RawReasoningLevel {
|
|
39
64
|
effort?: string;
|
|
40
65
|
description?: string;
|
|
@@ -54,9 +79,12 @@ interface RawCodexModelEntry {
|
|
|
54
79
|
default_verbosity?: string;
|
|
55
80
|
}
|
|
56
81
|
interface RawModelsCache {
|
|
82
|
+
fetched_at?: string;
|
|
57
83
|
client_version?: string;
|
|
58
84
|
models?: RawCodexModelEntry[];
|
|
59
85
|
}
|
|
86
|
+
declare function parseRawModelsCache(rawText: string): RawModelsCache;
|
|
87
|
+
declare function compareClientVersions(a: string | undefined, b: string | undefined): number;
|
|
60
88
|
interface CodexModelMetadata {
|
|
61
89
|
readonly contextWindow: number;
|
|
62
90
|
readonly supportVerbosity: boolean;
|
|
@@ -112,24 +140,10 @@ declare function createUsageFetcher(options: UsageFetcherOptions): {
|
|
|
112
140
|
invalidate: () => void;
|
|
113
141
|
};
|
|
114
142
|
//#endregion
|
|
115
|
-
//#region src/host-types.d.ts
|
|
116
|
-
interface LlmServiceLike {
|
|
117
|
-
registerAdapter: (providers: string[], adapter: unknown) => (() => void) & {
|
|
118
|
-
replace?: (next: string[]) => void;
|
|
119
|
-
};
|
|
120
|
-
emitAdaptersUpdated?: () => void;
|
|
121
|
-
}
|
|
122
|
-
interface PluginContextLike {
|
|
123
|
-
llm: LlmServiceLike;
|
|
124
|
-
get?: (name: string) => unknown;
|
|
125
|
-
on?: (event: string, listener: () => void) => () => void;
|
|
126
|
-
effect?: (factory: () => void | (() => void), label?: string) => () => void;
|
|
127
|
-
inject?: (services: readonly string[], callback: (ctx: PluginContextLike) => void) => () => void;
|
|
128
|
-
}
|
|
129
|
-
//#endregion
|
|
130
143
|
//#region src/index.d.ts
|
|
131
144
|
export declare const name = "codex-subscription";
|
|
132
145
|
export declare const inject: string[];
|
|
146
|
+
export declare const LOG_FILE_NAME = "dsh-codex-subscription.log";
|
|
133
147
|
export declare const Config: any;
|
|
134
148
|
export declare function apply(ctx: PluginContextLike, config?: Record<string, unknown>): void;
|
|
135
149
|
export declare const __test: Readonly<{
|
|
@@ -146,7 +160,9 @@ export declare const __test: Readonly<{
|
|
|
146
160
|
safeErrorMessage: typeof safeErrorMessage;
|
|
147
161
|
createAuthManager: typeof createAuthManager;
|
|
148
162
|
buildThinkingLevelMap: typeof buildThinkingLevelMap;
|
|
163
|
+
compareClientVersions: typeof compareClientVersions;
|
|
149
164
|
parseModelsCache: typeof parseModelsCache;
|
|
165
|
+
parseRawModelsCache: typeof parseRawModelsCache;
|
|
150
166
|
sanitizeCodexPayload: typeof sanitizeCodexPayload;
|
|
151
167
|
buildCodexHeaders: typeof buildCodexHeaders;
|
|
152
168
|
buildCodexUserAgent: typeof buildCodexUserAgent;
|
|
@@ -155,4 +171,4 @@ export declare const __test: Readonly<{
|
|
|
155
171
|
createUsageFetcher: typeof createUsageFetcher;
|
|
156
172
|
}>;
|
|
157
173
|
//#endregion
|
|
158
|
-
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 { chmodSync, readFileSync, realpathSync, statSync } 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,23 @@ 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
|
+
}
|
|
38
|
+
function compareClientVersions(a, b) {
|
|
39
|
+
const parseParts = (ver) => typeof ver === "string" && ver.trim() !== "" ? ver.trim().split(/[^0-9]+/u).filter(Boolean).map((n) => Number.parseInt(n, 10)) : [];
|
|
40
|
+
const pa = parseParts(a);
|
|
41
|
+
const pb = parseParts(b);
|
|
42
|
+
const len = Math.max(pa.length, pb.length);
|
|
43
|
+
for (let i = 0; i < len; i += 1) {
|
|
44
|
+
const va = pa[i] ?? 0;
|
|
45
|
+
const vb = pb[i] ?? 0;
|
|
46
|
+
if (va !== vb) return va > vb ? 1 : -1;
|
|
47
|
+
}
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
32
50
|
function buildThinkingLevelMap(supportedLevels) {
|
|
33
51
|
const efforts = new Set((Array.isArray(supportedLevels) ? supportedLevels : []).map((item) => {
|
|
34
52
|
if (typeof item === "string") return item;
|
|
@@ -350,11 +368,35 @@ function extractAccountId(parsed, accessToken) {
|
|
|
350
368
|
if (typeof jwtAccountId === "string" && jwtAccountId.trim() !== "") return jwtAccountId.trim();
|
|
351
369
|
}
|
|
352
370
|
}
|
|
353
|
-
|
|
354
|
-
let targetPath = filePath;
|
|
371
|
+
function resolveRealTargetPath(filePath) {
|
|
355
372
|
try {
|
|
356
|
-
|
|
357
|
-
} 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);
|
|
358
400
|
const tempPath = join(dirname(targetPath), `.auth.json.tmp-${process.pid}-${randomUUID()}`);
|
|
359
401
|
const content = `${JSON.stringify(data, null, 2)}\n`;
|
|
360
402
|
await writeFile(tempPath, content, {
|
|
@@ -419,25 +461,34 @@ function createAuthManager(options) {
|
|
|
419
461
|
return exp * 1e3 - nowFn() > TOKEN_REFRESH_LEAD_MS;
|
|
420
462
|
}
|
|
421
463
|
async function refreshToken(authPath, rawAuth) {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
+
}
|
|
441
492
|
const nextAuth = {
|
|
442
493
|
...rawAuth,
|
|
443
494
|
tokens: {
|
|
@@ -455,8 +506,17 @@ function createAuthManager(options) {
|
|
|
455
506
|
};
|
|
456
507
|
try {
|
|
457
508
|
await writeJsonAtomic(authPath, nextAuth);
|
|
509
|
+
options.onLog?.("INFO", "auth.refreshed", {
|
|
510
|
+
authPath,
|
|
511
|
+
accountId: extractAccountId(nextAuth, fresh.access_token) ?? null
|
|
512
|
+
});
|
|
458
513
|
} catch (error) {
|
|
459
|
-
|
|
514
|
+
const errMsg = safeErrorMessage(error);
|
|
515
|
+
options.onLog?.("WARN", "auth.writeback_failed", {
|
|
516
|
+
authPath,
|
|
517
|
+
error: errMsg
|
|
518
|
+
});
|
|
519
|
+
console.error(`[dsh-codex-subscription] 写回刷新后的 Codex auth.json 失败 (${authPath}): ${errMsg}`);
|
|
460
520
|
}
|
|
461
521
|
return {
|
|
462
522
|
accessToken: fresh.access_token,
|
|
@@ -492,10 +552,48 @@ function createAuthManager(options) {
|
|
|
492
552
|
//#region src/index.ts
|
|
493
553
|
const name = "codex-subscription";
|
|
494
554
|
const inject = ["llm"];
|
|
555
|
+
const LOG_FILE_NAME = "dsh-codex-subscription.log";
|
|
495
556
|
const STREAM_IDLE_TIMEOUT_MS = 3e5;
|
|
496
557
|
const MAX_REQUEST_IMAGE_BYTES = 20971520;
|
|
497
558
|
const REQUEST_IMAGE_PIXEL_BUDGET = 4194304;
|
|
498
559
|
const REQUEST_IMAGE_MAX_BYTES = 1048576;
|
|
560
|
+
const MAX_LOG_FILE_BYTES = 524288;
|
|
561
|
+
function appendPluginLog(codexHome, ctx, level, event, detail) {
|
|
562
|
+
const line = `[${new Date().toISOString()}] [${level}] ${event} ${JSON.stringify(detail)}\n`;
|
|
563
|
+
if (level === "ERROR") {
|
|
564
|
+
if (typeof ctx.logger?.error === "function") ctx.logger.error(`[dsh-codex-subscription] ${event}: ${JSON.stringify(detail)}`);
|
|
565
|
+
else console.error(`[dsh-codex-subscription] ${event}: ${JSON.stringify(detail)}`);
|
|
566
|
+
} else if (level === "WARN") {
|
|
567
|
+
if (typeof ctx.logger?.warn === "function") ctx.logger.warn(`[dsh-codex-subscription] ${event}: ${JSON.stringify(detail)}`);
|
|
568
|
+
}
|
|
569
|
+
try {
|
|
570
|
+
const logPath = join(codexHome, LOG_FILE_NAME);
|
|
571
|
+
try {
|
|
572
|
+
if (statSync(logPath).size > MAX_LOG_FILE_BYTES) {
|
|
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));
|
|
577
|
+
}
|
|
578
|
+
} catch {}
|
|
579
|
+
appendFileSync(logPath, line, {
|
|
580
|
+
encoding: "utf8",
|
|
581
|
+
mode: 384
|
|
582
|
+
});
|
|
583
|
+
} catch {}
|
|
584
|
+
}
|
|
585
|
+
function readBackupCache(codexHome) {
|
|
586
|
+
try {
|
|
587
|
+
return parseRawModelsCache(readFileSync(join(codexHome, BACKUP_CACHE_FILE_NAME), "utf8"));
|
|
588
|
+
} catch {
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
function writeBackupCache(codexHome, raw) {
|
|
593
|
+
try {
|
|
594
|
+
writeFileAtomicSync(join(codexHome, BACKUP_CACHE_FILE_NAME), `${JSON.stringify(raw, null, 2)}\n`);
|
|
595
|
+
} catch {}
|
|
596
|
+
}
|
|
499
597
|
const Config = z.object({
|
|
500
598
|
disabledModels: z.array(z.string()).default([...DEFAULT_DISABLED_MODELS]).description("在模型选择器中屏蔽的模型 slug 列表").volatile(),
|
|
501
599
|
contextWindows: z.dict(z.number()).default({}).description("按模型 slug 覆盖上下文窗口大小(tokens),未配置时默认使用 872000").volatile()
|
|
@@ -538,11 +636,12 @@ function apply(ctx, config = {}) {
|
|
|
538
636
|
const getContextWindows = () => readVolatile(config.contextWindows, {});
|
|
539
637
|
const authManager = createAuthManager({
|
|
540
638
|
getCodexHome,
|
|
541
|
-
getClientVersion: () => ensureSnapshot().catalog.clientVersion
|
|
639
|
+
getClientVersion: () => ensureSnapshot().catalog.clientVersion,
|
|
640
|
+
onLog: (level, event, detail) => appendPluginLog(getCodexHome(), ctx, level, event, detail)
|
|
542
641
|
});
|
|
543
642
|
let fileSignature;
|
|
544
643
|
let cachedRaw;
|
|
545
|
-
let
|
|
644
|
+
let rawRevision = 0;
|
|
546
645
|
let snapshotSignature;
|
|
547
646
|
let currentCatalog = {
|
|
548
647
|
clientVersion: DEFAULT_CLIENT_VERSION,
|
|
@@ -599,25 +698,73 @@ function apply(ctx, config = {}) {
|
|
|
599
698
|
}
|
|
600
699
|
});
|
|
601
700
|
function ensureSnapshot() {
|
|
602
|
-
const
|
|
701
|
+
const codexHome = getCodexHome();
|
|
702
|
+
const cachePath = join(codexHome, "models_cache.json");
|
|
603
703
|
const disabled = getDisabledModels();
|
|
604
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;
|
|
605
730
|
try {
|
|
606
|
-
|
|
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) {
|
|
607
740
|
const nextFileSig = `${cachePath}:${stat.mtimeMs}:${stat.size}`;
|
|
608
741
|
if (nextFileSig !== fileSignature) {
|
|
609
|
-
cachedRaw = JSON.parse(readFileSync(cachePath, "utf8"));
|
|
610
742
|
fileSignature = nextFileSig;
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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);
|
|
753
|
+
writeBackupCache(codexHome, incomingRaw);
|
|
754
|
+
appendPluginLog(codexHome, ctx, "INFO", "catalog.load", {
|
|
755
|
+
cachePath,
|
|
756
|
+
clientVersion: incomingRaw.client_version ?? "0.163.0",
|
|
757
|
+
fetchedAt: incomingRaw.fetched_at ?? null,
|
|
758
|
+
rawModelCount: Array.isArray(incomingRaw.models) ? incomingRaw.models.length : 0
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
} catch (error) {
|
|
762
|
+
handleReadError(error);
|
|
763
|
+
}
|
|
617
764
|
}
|
|
618
765
|
}
|
|
619
766
|
const nextSnapshotSig = JSON.stringify([
|
|
620
|
-
|
|
767
|
+
rawRevision,
|
|
621
768
|
disabled,
|
|
622
769
|
contextWindows
|
|
623
770
|
]);
|
|
@@ -627,6 +774,12 @@ function apply(ctx, config = {}) {
|
|
|
627
774
|
models: [],
|
|
628
775
|
metadataById: new Map()
|
|
629
776
|
};
|
|
777
|
+
appendPluginLog(codexHome, ctx, currentCatalog.models.length === 0 ? "WARN" : "INFO", "catalog.snapshot", {
|
|
778
|
+
clientVersion: currentCatalog.clientVersion,
|
|
779
|
+
models: currentCatalog.models.map((m) => m.id),
|
|
780
|
+
disabledModels: disabled,
|
|
781
|
+
contextWindows
|
|
782
|
+
});
|
|
630
783
|
currentProfilesMap = new Map([[PROVIDER, Object.freeze({
|
|
631
784
|
provider: PROVIDER,
|
|
632
785
|
displayName: DISPLAY_NAME,
|
|
@@ -715,7 +868,9 @@ const __test = Object.freeze({
|
|
|
715
868
|
safeErrorMessage,
|
|
716
869
|
createAuthManager,
|
|
717
870
|
buildThinkingLevelMap,
|
|
871
|
+
compareClientVersions,
|
|
718
872
|
parseModelsCache,
|
|
873
|
+
parseRawModelsCache,
|
|
719
874
|
sanitizeCodexPayload,
|
|
720
875
|
buildCodexHeaders,
|
|
721
876
|
buildCodexUserAgent,
|
|
@@ -724,4 +879,4 @@ const __test = Object.freeze({
|
|
|
724
879
|
createUsageFetcher
|
|
725
880
|
});
|
|
726
881
|
//#endregion
|
|
727
|
-
export { Config, __test, apply, inject, name };
|
|
882
|
+
export { BACKUP_CACHE_FILE_NAME, Config, LOG_FILE_NAME, __test, apply, inject, name };
|