@lyogvce/dsh-codex-subscription 0.2.2 → 0.2.4
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 +5 -3
- package/lib/client.js +45 -7
- package/lib/index.d.ts +13 -0
- package/lib/index.js +127 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,15 +11,17 @@
|
|
|
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`。
|
|
19
21
|
4. **原生多轮思考链回放与工具调用**
|
|
20
22
|
- 底层复用 DSH 内置的 `PiAiAdapter` 与 `@earendil-works/pi-ai` 的 `openaiCodexProvider()`,固定使用 `sse` 传输(含 `zstd` 请求体压缩、规范化纯 UUID `session-id` / `x-client-request-id` 缓存头、`reasoning.encrypted_content` 签名回放与图片自动缩放卸载)。
|
|
21
23
|
5. **Codex 订阅限额内联展示(`conversation.input.right`)**
|
|
22
|
-
- 当会话选中 `openai-codex`
|
|
24
|
+
- 当会话选中 `openai-codex` 模型时,在输入框右下角紧邻模型选择器左侧展示限额胶囊(根据 `wham/usage` 返回的 `limit_window_seconds` 自适应展示 Plus 的 `5h X% · 7d Y%` 双窗口或 Pro 的 `7d X%` 单窗口,内置 60 秒缓存、单飞合并与每轮回复结束自动刷新),点击可展开查看对应窗口进度条、重置倒计时与手动刷新按钮;切换至其他 Provider 模型时自动隐藏。
|
|
23
25
|
|
|
24
26
|
---
|
|
25
27
|
|
package/lib/client.js
CHANGED
|
@@ -25,6 +25,8 @@ window.__ModuleLoader__.load({
|
|
|
25
25
|
//#endregion
|
|
26
26
|
//#region src/client/quota.ts
|
|
27
27
|
const RING_CIRCUMFERENCE = 2 * Math.PI * 5.5;
|
|
28
|
+
const DEFAULT_PRIMARY_WINDOW_SECONDS = 18e3;
|
|
29
|
+
const DEFAULT_SECONDARY_WINDOW_SECONDS = 604800;
|
|
28
30
|
function resolveActiveProvider(directorySnapshot, projectedSelection) {
|
|
29
31
|
const dir = directorySnapshot && typeof directorySnapshot === "object" ? directorySnapshot : void 0;
|
|
30
32
|
const proj = projectedSelection && typeof projectedSelection === "object" ? projectedSelection : void 0;
|
|
@@ -46,14 +48,53 @@ window.__ModuleLoader__.load({
|
|
|
46
48
|
if (usedPercent >= 80) return "warn";
|
|
47
49
|
return "ok";
|
|
48
50
|
}
|
|
51
|
+
function resolveWindowSpan(windowSeconds, fallbackSeconds) {
|
|
52
|
+
const sec = typeof windowSeconds === "number" && Number.isFinite(windowSeconds) && windowSeconds > 0 ? windowSeconds : fallbackSeconds;
|
|
53
|
+
if (sec >= 86400) return {
|
|
54
|
+
value: Math.round(sec / 86400),
|
|
55
|
+
unit: "d"
|
|
56
|
+
};
|
|
57
|
+
if (sec >= 3600) return {
|
|
58
|
+
value: Math.round(sec / 3600),
|
|
59
|
+
unit: "h"
|
|
60
|
+
};
|
|
61
|
+
return {
|
|
62
|
+
value: Math.max(1, Math.round(sec / 60)),
|
|
63
|
+
unit: "m"
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const WINDOW_UNIT_LABELS = {
|
|
67
|
+
zh: {
|
|
68
|
+
d: "天窗口",
|
|
69
|
+
h: "小时窗口",
|
|
70
|
+
m: "分钟窗口"
|
|
71
|
+
},
|
|
72
|
+
en: {
|
|
73
|
+
d: "-day window",
|
|
74
|
+
h: "-hour window",
|
|
75
|
+
m: "-minute window"
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
function formatWindowShortLabel(windowSeconds, fallbackSeconds = DEFAULT_PRIMARY_WINDOW_SECONDS) {
|
|
79
|
+
const { value, unit } = resolveWindowSpan(windowSeconds, fallbackSeconds);
|
|
80
|
+
return `${value}${unit}`;
|
|
81
|
+
}
|
|
82
|
+
function formatWindowTitle(windowSeconds, locale, fallbackSeconds = DEFAULT_PRIMARY_WINDOW_SECONDS) {
|
|
83
|
+
const { value, unit } = resolveWindowSpan(windowSeconds, fallbackSeconds);
|
|
84
|
+
const suffix = WINDOW_UNIT_LABELS[locale][unit];
|
|
85
|
+
return locale === "en" ? `${value}${suffix}` : `${value} ${suffix}`;
|
|
86
|
+
}
|
|
49
87
|
function formatQuotaSummary(snapshot) {
|
|
50
88
|
const primaryPercent = clampPercent(snapshot.primary.usedPercent);
|
|
51
89
|
const secondaryPercent = snapshot.secondary !== null ? clampPercent(snapshot.secondary.usedPercent) : null;
|
|
90
|
+
const level = windowLevelOf(secondaryPercent !== null ? Math.max(primaryPercent, secondaryPercent) : primaryPercent, snapshot.limitReached);
|
|
91
|
+
const primaryLabel = formatWindowShortLabel(snapshot.primary.windowSeconds, DEFAULT_PRIMARY_WINDOW_SECONDS);
|
|
92
|
+
const secondaryLabel = snapshot.secondary !== null ? formatWindowShortLabel(snapshot.secondary.windowSeconds, DEFAULT_SECONDARY_WINDOW_SECONDS) : null;
|
|
52
93
|
return {
|
|
53
94
|
primaryPercent,
|
|
54
95
|
secondaryPercent,
|
|
55
|
-
level
|
|
56
|
-
pillText: secondaryPercent !== null ?
|
|
96
|
+
level,
|
|
97
|
+
pillText: secondaryPercent !== null && secondaryLabel !== null ? `${primaryLabel} ${primaryPercent}% · ${secondaryLabel} ${secondaryPercent}%` : `${primaryLabel} ${primaryPercent}%`,
|
|
57
98
|
ringDash: `${(RING_CIRCUMFERENCE * primaryPercent / 100).toFixed(2)} ${RING_CIRCUMFERENCE.toFixed(2)}`
|
|
58
99
|
};
|
|
59
100
|
}
|
|
@@ -223,8 +264,6 @@ window.__ModuleLoader__.load({
|
|
|
223
264
|
"quota.loading": "正在获取 Codex 限额…",
|
|
224
265
|
"quota.refresh": "↻ 刷新",
|
|
225
266
|
"quota.refreshing": "↻ 刷新中…",
|
|
226
|
-
"quota.window5h": "5 小时窗口",
|
|
227
|
-
"quota.window7d": "7 天窗口",
|
|
228
267
|
"quota.usedPrefix": "已用 ",
|
|
229
268
|
"quota.remainPrefix": "剩余 "
|
|
230
269
|
};
|
|
@@ -233,8 +272,6 @@ window.__ModuleLoader__.load({
|
|
|
233
272
|
"quota.loading": "Loading Codex quota…",
|
|
234
273
|
"quota.refresh": "↻ Refresh",
|
|
235
274
|
"quota.refreshing": "↻ Refreshing…",
|
|
236
|
-
"quota.window5h": "5-hour window",
|
|
237
|
-
"quota.window7d": "7-day window",
|
|
238
275
|
"quota.usedPrefix": "Used ",
|
|
239
276
|
"quota.remainPrefix": "Remaining "
|
|
240
277
|
};
|
|
@@ -357,7 +394,7 @@ window.__ModuleLoader__.load({
|
|
|
357
394
|
onClick: () => {
|
|
358
395
|
quotaStore.refresh(true);
|
|
359
396
|
}
|
|
360
|
-
}, quotaSnap.loading ? t("quota.refreshing") : t("quota.refresh"))), quota !== null ? renderWindowSection(
|
|
397
|
+
}, 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, quotaSnap.error !== null ? h("div", { className: "dsh-codex-quota-error" }, quotaSnap.error) : null) : null);
|
|
361
398
|
}
|
|
362
399
|
const inject = ["slots", "locale"];
|
|
363
400
|
function apply(ctx) {
|
|
@@ -411,6 +448,7 @@ window.__ModuleLoader__.load({
|
|
|
411
448
|
isCodexProvider,
|
|
412
449
|
didTurnFinish,
|
|
413
450
|
windowLevelOf,
|
|
451
|
+
formatWindowTitle,
|
|
414
452
|
formatQuotaSummary,
|
|
415
453
|
formatResetCountdown,
|
|
416
454
|
createQuotaStore,
|
package/lib/index.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ interface JwtClaims {
|
|
|
19
19
|
interface AuthManagerOptions {
|
|
20
20
|
getCodexHome: () => string;
|
|
21
21
|
getClientVersion?: (() => string) | undefined;
|
|
22
|
+
onLog?: ((level: 'INFO' | 'WARN' | 'ERROR', event: string, detail: Record<string, unknown>) => void) | undefined;
|
|
22
23
|
fetchImpl?: typeof fetch | undefined;
|
|
23
24
|
now?: (() => number) | undefined;
|
|
24
25
|
}
|
|
@@ -54,9 +55,11 @@ interface RawCodexModelEntry {
|
|
|
54
55
|
default_verbosity?: string;
|
|
55
56
|
}
|
|
56
57
|
interface RawModelsCache {
|
|
58
|
+
fetched_at?: string;
|
|
57
59
|
client_version?: string;
|
|
58
60
|
models?: RawCodexModelEntry[];
|
|
59
61
|
}
|
|
62
|
+
declare function compareClientVersions(a: string | undefined, b: string | undefined): number;
|
|
60
63
|
interface CodexModelMetadata {
|
|
61
64
|
readonly contextWindow: number;
|
|
62
65
|
readonly supportVerbosity: boolean;
|
|
@@ -81,6 +84,7 @@ declare function sanitizeCodexPayload(payload: Record<string, unknown> | null |
|
|
|
81
84
|
declare function safeErrorMessage(error: unknown): string;
|
|
82
85
|
interface CodexQuotaWindow {
|
|
83
86
|
readonly usedPercent: number;
|
|
87
|
+
readonly windowSeconds?: number | null | undefined;
|
|
84
88
|
readonly resetAt: number | null;
|
|
85
89
|
}
|
|
86
90
|
interface CodexQuotaSnapshot {
|
|
@@ -118,8 +122,14 @@ interface LlmServiceLike {
|
|
|
118
122
|
};
|
|
119
123
|
emitAdaptersUpdated?: () => void;
|
|
120
124
|
}
|
|
125
|
+
interface LoggerLike {
|
|
126
|
+
info?: (message: string) => void;
|
|
127
|
+
warn?: (message: string) => void;
|
|
128
|
+
error?: (message: string) => void;
|
|
129
|
+
}
|
|
121
130
|
interface PluginContextLike {
|
|
122
131
|
llm: LlmServiceLike;
|
|
132
|
+
logger?: LoggerLike | undefined;
|
|
123
133
|
get?: (name: string) => unknown;
|
|
124
134
|
on?: (event: string, listener: () => void) => () => void;
|
|
125
135
|
effect?: (factory: () => void | (() => void), label?: string) => () => void;
|
|
@@ -129,6 +139,8 @@ interface PluginContextLike {
|
|
|
129
139
|
//#region src/index.d.ts
|
|
130
140
|
export declare const name = "codex-subscription";
|
|
131
141
|
export declare const inject: string[];
|
|
142
|
+
export declare const LOG_FILE_NAME = "dsh-codex-subscription.log";
|
|
143
|
+
export declare const BACKUP_CACHE_FILE_NAME = ".dsh-models-cache-backup.json";
|
|
132
144
|
export declare const Config: any;
|
|
133
145
|
export declare function apply(ctx: PluginContextLike, config?: Record<string, unknown>): void;
|
|
134
146
|
export declare const __test: Readonly<{
|
|
@@ -145,6 +157,7 @@ export declare const __test: Readonly<{
|
|
|
145
157
|
safeErrorMessage: typeof safeErrorMessage;
|
|
146
158
|
createAuthManager: typeof createAuthManager;
|
|
147
159
|
buildThinkingLevelMap: typeof buildThinkingLevelMap;
|
|
160
|
+
compareClientVersions: typeof compareClientVersions;
|
|
148
161
|
parseModelsCache: typeof parseModelsCache;
|
|
149
162
|
sanitizeCodexPayload: typeof sanitizeCodexPayload;
|
|
150
163
|
buildCodexHeaders: typeof buildCodexHeaders;
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { chmodSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
1
|
+
import { appendFileSync, chmodSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { 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";
|
|
@@ -29,6 +29,18 @@ const DEFAULT_DISABLED_MODELS = Object.freeze([
|
|
|
29
29
|
"gpt-5.6-terra",
|
|
30
30
|
"gpt-5.6-luna"
|
|
31
31
|
]);
|
|
32
|
+
function compareClientVersions(a, b) {
|
|
33
|
+
const parseParts = (ver) => typeof ver === "string" && ver.trim() !== "" ? ver.trim().split(/[^0-9]+/u).filter(Boolean).map((n) => Number.parseInt(n, 10)) : [];
|
|
34
|
+
const pa = parseParts(a);
|
|
35
|
+
const pb = parseParts(b);
|
|
36
|
+
const len = Math.max(pa.length, pb.length);
|
|
37
|
+
for (let i = 0; i < len; i += 1) {
|
|
38
|
+
const va = pa[i] ?? 0;
|
|
39
|
+
const vb = pb[i] ?? 0;
|
|
40
|
+
if (va !== vb) return va > vb ? 1 : -1;
|
|
41
|
+
}
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
32
44
|
function buildThinkingLevelMap(supportedLevels) {
|
|
33
45
|
const efforts = new Set((Array.isArray(supportedLevels) ? supportedLevels : []).map((item) => {
|
|
34
46
|
if (typeof item === "string") return item;
|
|
@@ -229,6 +241,8 @@ function parseWindow(raw, fetchedAtMs) {
|
|
|
229
241
|
const record = raw;
|
|
230
242
|
const usedRaw = record.used_percent;
|
|
231
243
|
if (typeof usedRaw !== "number" || !Number.isFinite(usedRaw)) return null;
|
|
244
|
+
const windowSecRaw = record.limit_window_seconds;
|
|
245
|
+
const windowSeconds = typeof windowSecRaw === "number" && Number.isFinite(windowSecRaw) && windowSecRaw > 0 ? Math.round(windowSecRaw) : null;
|
|
232
246
|
const resetAtRaw = record.reset_at;
|
|
233
247
|
const resetAfterRaw = record.reset_after_seconds;
|
|
234
248
|
let resetAt = null;
|
|
@@ -236,6 +250,7 @@ function parseWindow(raw, fetchedAtMs) {
|
|
|
236
250
|
else if (typeof resetAfterRaw === "number" && Number.isFinite(resetAfterRaw) && resetAfterRaw >= 0) resetAt = Math.floor(fetchedAtMs / 1e3) + Math.round(resetAfterRaw);
|
|
237
251
|
return {
|
|
238
252
|
usedPercent: clampPercent(usedRaw),
|
|
253
|
+
windowSeconds,
|
|
239
254
|
resetAt
|
|
240
255
|
};
|
|
241
256
|
}
|
|
@@ -452,8 +467,17 @@ function createAuthManager(options) {
|
|
|
452
467
|
};
|
|
453
468
|
try {
|
|
454
469
|
await writeJsonAtomic(authPath, nextAuth);
|
|
470
|
+
options.onLog?.("INFO", "auth.refreshed", {
|
|
471
|
+
authPath,
|
|
472
|
+
accountId: extractAccountId(nextAuth, fresh.access_token) ?? null
|
|
473
|
+
});
|
|
455
474
|
} catch (error) {
|
|
456
|
-
|
|
475
|
+
const errMsg = safeErrorMessage(error);
|
|
476
|
+
options.onLog?.("WARN", "auth.writeback_failed", {
|
|
477
|
+
authPath,
|
|
478
|
+
error: errMsg
|
|
479
|
+
});
|
|
480
|
+
console.error(`[dsh-codex-subscription] 写回刷新后的 Codex auth.json 失败 (${authPath}): ${errMsg}`);
|
|
457
481
|
}
|
|
458
482
|
return {
|
|
459
483
|
accessToken: fresh.access_token,
|
|
@@ -489,10 +513,55 @@ function createAuthManager(options) {
|
|
|
489
513
|
//#region src/index.ts
|
|
490
514
|
const name = "codex-subscription";
|
|
491
515
|
const inject = ["llm"];
|
|
516
|
+
const LOG_FILE_NAME = "dsh-codex-subscription.log";
|
|
517
|
+
const BACKUP_CACHE_FILE_NAME = ".dsh-models-cache-backup.json";
|
|
492
518
|
const STREAM_IDLE_TIMEOUT_MS = 3e5;
|
|
493
519
|
const MAX_REQUEST_IMAGE_BYTES = 20971520;
|
|
494
520
|
const REQUEST_IMAGE_PIXEL_BUDGET = 4194304;
|
|
495
521
|
const REQUEST_IMAGE_MAX_BYTES = 1048576;
|
|
522
|
+
const MAX_LOG_FILE_BYTES = 524288;
|
|
523
|
+
function appendPluginLog(codexHome, ctx, level, event, detail) {
|
|
524
|
+
const line = `[${new Date().toISOString()}] [${level}] ${event} ${JSON.stringify(detail)}\n`;
|
|
525
|
+
if (level === "ERROR") {
|
|
526
|
+
if (typeof ctx.logger?.error === "function") ctx.logger.error(`[dsh-codex-subscription] ${event}: ${JSON.stringify(detail)}`);
|
|
527
|
+
else console.error(`[dsh-codex-subscription] ${event}: ${JSON.stringify(detail)}`);
|
|
528
|
+
} else if (level === "WARN") {
|
|
529
|
+
if (typeof ctx.logger?.warn === "function") ctx.logger.warn(`[dsh-codex-subscription] ${event}: ${JSON.stringify(detail)}`);
|
|
530
|
+
}
|
|
531
|
+
try {
|
|
532
|
+
const logPath = join(codexHome, LOG_FILE_NAME);
|
|
533
|
+
try {
|
|
534
|
+
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
|
+
});
|
|
540
|
+
}
|
|
541
|
+
} catch {}
|
|
542
|
+
appendFileSync(logPath, line, {
|
|
543
|
+
encoding: "utf8",
|
|
544
|
+
mode: 384
|
|
545
|
+
});
|
|
546
|
+
} catch {}
|
|
547
|
+
}
|
|
548
|
+
function readBackupCache(codexHome) {
|
|
549
|
+
try {
|
|
550
|
+
return JSON.parse(readFileSync(join(codexHome, BACKUP_CACHE_FILE_NAME), "utf8"));
|
|
551
|
+
} catch {
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
function writeBackupCache(codexHome, raw) {
|
|
556
|
+
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
|
+
});
|
|
563
|
+
} catch {}
|
|
564
|
+
}
|
|
496
565
|
const Config = z.object({
|
|
497
566
|
disabledModels: z.array(z.string()).default([...DEFAULT_DISABLED_MODELS]).description("在模型选择器中屏蔽的模型 slug 列表").volatile(),
|
|
498
567
|
contextWindows: z.dict(z.number()).default({}).description("按模型 slug 覆盖上下文窗口大小(tokens),未配置时默认使用 872000").volatile()
|
|
@@ -535,7 +604,8 @@ function apply(ctx, config = {}) {
|
|
|
535
604
|
const getContextWindows = () => readVolatile(config.contextWindows, {});
|
|
536
605
|
const authManager = createAuthManager({
|
|
537
606
|
getCodexHome,
|
|
538
|
-
getClientVersion: () => ensureSnapshot().catalog.clientVersion
|
|
607
|
+
getClientVersion: () => ensureSnapshot().catalog.clientVersion,
|
|
608
|
+
onLog: (level, event, detail) => appendPluginLog(getCodexHome(), ctx, level, event, detail)
|
|
539
609
|
});
|
|
540
610
|
let fileSignature;
|
|
541
611
|
let cachedRaw;
|
|
@@ -596,21 +666,64 @@ function apply(ctx, config = {}) {
|
|
|
596
666
|
}
|
|
597
667
|
});
|
|
598
668
|
function ensureSnapshot() {
|
|
599
|
-
const
|
|
669
|
+
const codexHome = getCodexHome();
|
|
670
|
+
const cachePath = join(codexHome, "models_cache.json");
|
|
600
671
|
const disabled = getDisabledModels();
|
|
601
672
|
const contextWindows = getContextWindows();
|
|
602
673
|
try {
|
|
603
674
|
const stat = statSync(cachePath);
|
|
604
675
|
const nextFileSig = `${cachePath}:${stat.mtimeMs}:${stat.size}`;
|
|
605
676
|
if (nextFileSig !== fileSignature) {
|
|
606
|
-
|
|
677
|
+
const incomingRaw = JSON.parse(readFileSync(cachePath, "utf8"));
|
|
607
678
|
fileSignature = nextFileSig;
|
|
608
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;
|
|
698
|
+
writeBackupCache(codexHome, incomingRaw);
|
|
699
|
+
appendPluginLog(codexHome, ctx, "INFO", "catalog.load", {
|
|
700
|
+
cachePath,
|
|
701
|
+
clientVersion: incomingRaw.client_version ?? "0.163.0",
|
|
702
|
+
fetchedAt: incomingRaw.fetched_at ?? null,
|
|
703
|
+
rawModelCount: Array.isArray(incomingRaw.models) ? incomingRaw.models.length : 0
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
}
|
|
609
707
|
}
|
|
610
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
|
+
}
|
|
611
720
|
if (!warnedMissingFile) {
|
|
612
721
|
warnedMissingFile = true;
|
|
613
|
-
|
|
722
|
+
appendPluginLog(codexHome, ctx, cachedRaw === void 0 ? "ERROR" : "WARN", "catalog.read_error", {
|
|
723
|
+
cachePath,
|
|
724
|
+
hasFallbackSnapshot: cachedRaw !== void 0,
|
|
725
|
+
error: safeErrorMessage(error)
|
|
726
|
+
});
|
|
614
727
|
}
|
|
615
728
|
}
|
|
616
729
|
const nextSnapshotSig = JSON.stringify([
|
|
@@ -624,6 +737,12 @@ function apply(ctx, config = {}) {
|
|
|
624
737
|
models: [],
|
|
625
738
|
metadataById: new Map()
|
|
626
739
|
};
|
|
740
|
+
appendPluginLog(codexHome, ctx, currentCatalog.models.length === 0 ? "WARN" : "INFO", "catalog.snapshot", {
|
|
741
|
+
clientVersion: currentCatalog.clientVersion,
|
|
742
|
+
models: currentCatalog.models.map((m) => m.id),
|
|
743
|
+
disabledModels: disabled,
|
|
744
|
+
contextWindows
|
|
745
|
+
});
|
|
627
746
|
currentProfilesMap = new Map([[PROVIDER, Object.freeze({
|
|
628
747
|
provider: PROVIDER,
|
|
629
748
|
displayName: DISPLAY_NAME,
|
|
@@ -712,6 +831,7 @@ const __test = Object.freeze({
|
|
|
712
831
|
safeErrorMessage,
|
|
713
832
|
createAuthManager,
|
|
714
833
|
buildThinkingLevelMap,
|
|
834
|
+
compareClientVersions,
|
|
715
835
|
parseModelsCache,
|
|
716
836
|
sanitizeCodexPayload,
|
|
717
837
|
buildCodexHeaders,
|
|
@@ -721,4 +841,4 @@ const __test = Object.freeze({
|
|
|
721
841
|
createUsageFetcher
|
|
722
842
|
});
|
|
723
843
|
//#endregion
|
|
724
|
-
export { Config, __test, apply, inject, name };
|
|
844
|
+
export { BACKUP_CACHE_FILE_NAME, Config, LOG_FILE_NAME, __test, apply, inject, name };
|