@lyogvce/dsh-codex-subscription 0.2.3 → 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 +4 -2
- package/lib/client.js +0 -1
- package/lib/index.d.ts +12 -0
- package/lib/index.js +124 -7
- 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/client.js
CHANGED
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;
|
|
@@ -119,8 +122,14 @@ interface LlmServiceLike {
|
|
|
119
122
|
};
|
|
120
123
|
emitAdaptersUpdated?: () => void;
|
|
121
124
|
}
|
|
125
|
+
interface LoggerLike {
|
|
126
|
+
info?: (message: string) => void;
|
|
127
|
+
warn?: (message: string) => void;
|
|
128
|
+
error?: (message: string) => void;
|
|
129
|
+
}
|
|
122
130
|
interface PluginContextLike {
|
|
123
131
|
llm: LlmServiceLike;
|
|
132
|
+
logger?: LoggerLike | undefined;
|
|
124
133
|
get?: (name: string) => unknown;
|
|
125
134
|
on?: (event: string, listener: () => void) => () => void;
|
|
126
135
|
effect?: (factory: () => void | (() => void), label?: string) => () => void;
|
|
@@ -130,6 +139,8 @@ interface PluginContextLike {
|
|
|
130
139
|
//#region src/index.d.ts
|
|
131
140
|
export declare const name = "codex-subscription";
|
|
132
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";
|
|
133
144
|
export declare const Config: any;
|
|
134
145
|
export declare function apply(ctx: PluginContextLike, config?: Record<string, unknown>): void;
|
|
135
146
|
export declare const __test: Readonly<{
|
|
@@ -146,6 +157,7 @@ export declare const __test: Readonly<{
|
|
|
146
157
|
safeErrorMessage: typeof safeErrorMessage;
|
|
147
158
|
createAuthManager: typeof createAuthManager;
|
|
148
159
|
buildThinkingLevelMap: typeof buildThinkingLevelMap;
|
|
160
|
+
compareClientVersions: typeof compareClientVersions;
|
|
149
161
|
parseModelsCache: typeof parseModelsCache;
|
|
150
162
|
sanitizeCodexPayload: typeof sanitizeCodexPayload;
|
|
151
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;
|
|
@@ -455,8 +467,17 @@ function createAuthManager(options) {
|
|
|
455
467
|
};
|
|
456
468
|
try {
|
|
457
469
|
await writeJsonAtomic(authPath, nextAuth);
|
|
470
|
+
options.onLog?.("INFO", "auth.refreshed", {
|
|
471
|
+
authPath,
|
|
472
|
+
accountId: extractAccountId(nextAuth, fresh.access_token) ?? null
|
|
473
|
+
});
|
|
458
474
|
} catch (error) {
|
|
459
|
-
|
|
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}`);
|
|
460
481
|
}
|
|
461
482
|
return {
|
|
462
483
|
accessToken: fresh.access_token,
|
|
@@ -492,10 +513,55 @@ function createAuthManager(options) {
|
|
|
492
513
|
//#region src/index.ts
|
|
493
514
|
const name = "codex-subscription";
|
|
494
515
|
const inject = ["llm"];
|
|
516
|
+
const LOG_FILE_NAME = "dsh-codex-subscription.log";
|
|
517
|
+
const BACKUP_CACHE_FILE_NAME = ".dsh-models-cache-backup.json";
|
|
495
518
|
const STREAM_IDLE_TIMEOUT_MS = 3e5;
|
|
496
519
|
const MAX_REQUEST_IMAGE_BYTES = 20971520;
|
|
497
520
|
const REQUEST_IMAGE_PIXEL_BUDGET = 4194304;
|
|
498
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
|
+
}
|
|
499
565
|
const Config = z.object({
|
|
500
566
|
disabledModels: z.array(z.string()).default([...DEFAULT_DISABLED_MODELS]).description("在模型选择器中屏蔽的模型 slug 列表").volatile(),
|
|
501
567
|
contextWindows: z.dict(z.number()).default({}).description("按模型 slug 覆盖上下文窗口大小(tokens),未配置时默认使用 872000").volatile()
|
|
@@ -538,7 +604,8 @@ function apply(ctx, config = {}) {
|
|
|
538
604
|
const getContextWindows = () => readVolatile(config.contextWindows, {});
|
|
539
605
|
const authManager = createAuthManager({
|
|
540
606
|
getCodexHome,
|
|
541
|
-
getClientVersion: () => ensureSnapshot().catalog.clientVersion
|
|
607
|
+
getClientVersion: () => ensureSnapshot().catalog.clientVersion,
|
|
608
|
+
onLog: (level, event, detail) => appendPluginLog(getCodexHome(), ctx, level, event, detail)
|
|
542
609
|
});
|
|
543
610
|
let fileSignature;
|
|
544
611
|
let cachedRaw;
|
|
@@ -599,21 +666,64 @@ function apply(ctx, config = {}) {
|
|
|
599
666
|
}
|
|
600
667
|
});
|
|
601
668
|
function ensureSnapshot() {
|
|
602
|
-
const
|
|
669
|
+
const codexHome = getCodexHome();
|
|
670
|
+
const cachePath = join(codexHome, "models_cache.json");
|
|
603
671
|
const disabled = getDisabledModels();
|
|
604
672
|
const contextWindows = getContextWindows();
|
|
605
673
|
try {
|
|
606
674
|
const stat = statSync(cachePath);
|
|
607
675
|
const nextFileSig = `${cachePath}:${stat.mtimeMs}:${stat.size}`;
|
|
608
676
|
if (nextFileSig !== fileSignature) {
|
|
609
|
-
|
|
677
|
+
const incomingRaw = JSON.parse(readFileSync(cachePath, "utf8"));
|
|
610
678
|
fileSignature = nextFileSig;
|
|
611
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
|
+
}
|
|
612
707
|
}
|
|
613
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
|
+
}
|
|
614
720
|
if (!warnedMissingFile) {
|
|
615
721
|
warnedMissingFile = true;
|
|
616
|
-
|
|
722
|
+
appendPluginLog(codexHome, ctx, cachedRaw === void 0 ? "ERROR" : "WARN", "catalog.read_error", {
|
|
723
|
+
cachePath,
|
|
724
|
+
hasFallbackSnapshot: cachedRaw !== void 0,
|
|
725
|
+
error: safeErrorMessage(error)
|
|
726
|
+
});
|
|
617
727
|
}
|
|
618
728
|
}
|
|
619
729
|
const nextSnapshotSig = JSON.stringify([
|
|
@@ -627,6 +737,12 @@ function apply(ctx, config = {}) {
|
|
|
627
737
|
models: [],
|
|
628
738
|
metadataById: new Map()
|
|
629
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
|
+
});
|
|
630
746
|
currentProfilesMap = new Map([[PROVIDER, Object.freeze({
|
|
631
747
|
provider: PROVIDER,
|
|
632
748
|
displayName: DISPLAY_NAME,
|
|
@@ -715,6 +831,7 @@ const __test = Object.freeze({
|
|
|
715
831
|
safeErrorMessage,
|
|
716
832
|
createAuthManager,
|
|
717
833
|
buildThinkingLevelMap,
|
|
834
|
+
compareClientVersions,
|
|
718
835
|
parseModelsCache,
|
|
719
836
|
sanitizeCodexPayload,
|
|
720
837
|
buildCodexHeaders,
|
|
@@ -724,4 +841,4 @@ const __test = Object.freeze({
|
|
|
724
841
|
createUsageFetcher
|
|
725
842
|
});
|
|
726
843
|
//#endregion
|
|
727
|
-
export { Config, __test, apply, inject, name };
|
|
844
|
+
export { BACKUP_CACHE_FILE_NAME, Config, LOG_FILE_NAME, __test, apply, inject, name };
|