@lyogvce/dsh-codex-subscription 0.2.12 → 0.2.13

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 CHANGED
@@ -11,8 +11,8 @@
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`(每次按文件 `mtime` 与大小做微秒级 `stat` 校验,仅在文件变化时重读解析),提取 `visibility === "list"` 的模型及其 `context_window`、`supported_reasoning_levels`(自动将 `ultra` 映射为 DSH 的 `max` 档位)与 `default_verbosity`。
14
+ 2. **零网络轮询的本地模型目录实时监听、防降级保护与可观测性日志**
15
+ - 直接读取并监听 `~/.codex/models_cache.json`(同时结合 `realpathSync` 目录级防抖 `fs.watch` 实时监听与微秒级 `stat` 校验,兼容软链接目录与临时文件 `rename` 原子替换),提取 `visibility === "list"` 的模型及其 `context_window`、`supported_reasoning_levels`(自动将 `ultra` 映射为 DSH 的 `max` 档位)与 `default_verbosity`;当本地模型缓存新增或变更模型时,自动调用 `ctx.llm.emitAdaptersUpdated()` 通知 DSH 桌面端前端立即刷新模型下拉框,无需重启或刷新页面。
16
16
  - **防旧版客户端降级污染**:自动将高版本 `models_cache.json` 备份至 `~/.codex/cache/dsh-codex-subscription/models-cache-backup.json`;当外部旧版进程(如 `dsh-subagent-codex` 内置的 `0.153.4`)将 `models_cache.json` 降级覆写为不含新模型的旧缓存时,自动拦截降级并保留高版本模型目录。
17
17
  - **可观测性日志**:自动在 `~/.codex/cache/dsh-codex-subscription/plugin.log`(带 512KB 自动滚动截断,忽略纯 `fetched_at` 心跳刷新)记录目录加载(`catalog.load` / `catalog.snapshot`)、降级拦截(`catalog.downgrade_rejected`)、备份恢复(`catalog.backup_restored`)与凭证续期(`auth.refreshed` / `auth.refresh_failed`)事件。
18
18
  3. **支持按配置屏蔽模型(`disabledModels`)、自定义上下文窗口(`contextWindows`)与原生重试策略(`retryPolicy`)**
package/lib/index.d.ts CHANGED
@@ -171,6 +171,8 @@ declare function createUsageFetcher(options: UsageFetcherOptions): {
171
171
  export declare const name = "codex-subscription";
172
172
  export declare const inject: string[];
173
173
  export declare const LOG_FILE_NAME = "plugin.log";
174
+ export declare const MODELS_CACHE_FILE_NAME = "models_cache.json";
175
+ export declare const MODELS_CACHE_WATCH_DEBOUNCE_MS = 100;
174
176
  export declare const Config: any;
175
177
  export declare function apply(ctx: PluginContextLike, config?: Record<string, unknown>): void;
176
178
  export declare const __test: Readonly<{
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { appendFileSync, chmodSync, mkdirSync, readFileSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
1
+ import { appendFileSync, chmodSync, mkdirSync, readFileSync, realpathSync, renameSync, statSync, unlinkSync, watch, writeFileSync } from "node:fs";
2
2
  import { basename, dirname, join, resolve } from "node:path";
3
3
  import z from "@deepseek-ai/schemastery";
4
4
  import { LlmError, RetryPolicySchema, resolveImageAttachmentAccess, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
@@ -704,6 +704,8 @@ function createAuthManager(options) {
704
704
  const name = "codex-subscription";
705
705
  const inject = ["llm"];
706
706
  const LOG_FILE_NAME = "plugin.log";
707
+ const MODELS_CACHE_FILE_NAME = "models_cache.json";
708
+ const MODELS_CACHE_WATCH_DEBOUNCE_MS = 100;
707
709
  const STREAM_IDLE_TIMEOUT_MS = 3e5;
708
710
  const MAX_REQUEST_IMAGE_BYTES = 20971520;
709
711
  const REQUEST_IMAGE_PIXEL_BUDGET = 4194304;
@@ -711,6 +713,27 @@ const REQUEST_IMAGE_MAX_BYTES = 1048576;
711
713
  const MAX_LOG_FILE_BYTES = 524288;
712
714
  const MAX_PERSISTED_SESSIONS = 200;
713
715
  const MAX_PERSISTED_PARENTS = 1e3;
716
+ function resolveModelsCacheWatchTargets(codexHome) {
717
+ const dirs = new Set();
718
+ const fileNames = new Set([MODELS_CACHE_FILE_NAME]);
719
+ try {
720
+ dirs.add(realpathSync(codexHome));
721
+ } catch {
722
+ try {
723
+ if (statSync(codexHome).isDirectory()) dirs.add(codexHome);
724
+ } catch {}
725
+ }
726
+ const cachePath = join(codexHome, MODELS_CACHE_FILE_NAME);
727
+ try {
728
+ const realCachePath = realpathSync(cachePath);
729
+ dirs.add(dirname(realCachePath));
730
+ fileNames.add(basename(realCachePath));
731
+ } catch {}
732
+ return {
733
+ dirs: [...dirs],
734
+ fileNames
735
+ };
736
+ }
714
737
  function resolvePluginCacheDir(codexHome) {
715
738
  return join(codexHome, PLUGIN_CACHE_DIR_NAME);
716
739
  }
@@ -840,7 +863,8 @@ var CodexPiAiAdapter = class extends PiAiAdapter {
840
863
  function apply(ctx, config = {}) {
841
864
  const baseProvider = openaiCodexProvider();
842
865
  const baseModels = baseProvider.getModels();
843
- const getCodexHome = () => resolveCodexHome();
866
+ const boundCodexHome = resolveCodexHome();
867
+ const getCodexHome = () => boundCodexHome;
844
868
  const getConfigFastMode = () => readVolatile(config.fastMode, false) === true;
845
869
  const getDisabledModels = () => readVolatile(config.disabledModels, DEFAULT_DISABLED_MODELS);
846
870
  const getContextWindows = () => readVolatile(config.contextWindows, {});
@@ -993,7 +1017,77 @@ function apply(ctx, config = {}) {
993
1017
  let rawRevision = 0;
994
1018
  let activeRetryPolicy = resolveConfiguredRetryPolicy();
995
1019
  let currentState;
1020
+ let adapterRegistered = false;
996
1021
  const catalogByProfiles = new WeakMap();
1022
+ const activeWatchers = new Map();
1023
+ let watchedFileNames = new Set([MODELS_CACHE_FILE_NAME]);
1024
+ let watchDebounceTimer;
1025
+ let watcherDisposed = false;
1026
+ const closeModelsCacheWatchers = () => {
1027
+ if (watchDebounceTimer !== void 0) {
1028
+ clearTimeout(watchDebounceTimer);
1029
+ watchDebounceTimer = void 0;
1030
+ }
1031
+ for (const watcher of activeWatchers.values()) try {
1032
+ watcher.close();
1033
+ } catch {}
1034
+ activeWatchers.clear();
1035
+ };
1036
+ const scheduleModelsCacheCheck = () => {
1037
+ if (watcherDisposed) return;
1038
+ if (watchDebounceTimer !== void 0) clearTimeout(watchDebounceTimer);
1039
+ watchDebounceTimer = setTimeout(() => {
1040
+ watchDebounceTimer = void 0;
1041
+ if (watcherDisposed) return;
1042
+ try {
1043
+ if (!statSync(getCodexHome()).isDirectory()) {
1044
+ watcherDisposed = true;
1045
+ closeModelsCacheWatchers();
1046
+ return;
1047
+ }
1048
+ } catch {
1049
+ watcherDisposed = true;
1050
+ closeModelsCacheWatchers();
1051
+ return;
1052
+ }
1053
+ syncModelsCacheWatchers();
1054
+ ensureSnapshot();
1055
+ }, 100);
1056
+ watchDebounceTimer.unref?.();
1057
+ };
1058
+ const syncModelsCacheWatchers = () => {
1059
+ if (watcherDisposed) return;
1060
+ const { dirs, fileNames } = resolveModelsCacheWatchTargets(getCodexHome());
1061
+ watchedFileNames = fileNames;
1062
+ const nextDirSet = new Set(dirs);
1063
+ for (const [dir, watcher] of activeWatchers.entries()) if (!nextDirSet.has(dir)) {
1064
+ try {
1065
+ watcher.close();
1066
+ } catch {}
1067
+ activeWatchers.delete(dir);
1068
+ }
1069
+ for (const dir of nextDirSet) {
1070
+ if (activeWatchers.has(dir)) continue;
1071
+ try {
1072
+ const watcher = watch(dir, { persistent: false }, (_eventType, rawFilename) => {
1073
+ if (watcherDisposed) return;
1074
+ if (rawFilename != null) {
1075
+ const name = typeof rawFilename === "string" ? rawFilename : String(rawFilename);
1076
+ if (name !== "" && !watchedFileNames.has(name)) return;
1077
+ }
1078
+ scheduleModelsCacheCheck();
1079
+ });
1080
+ watcher.on("error", () => {
1081
+ try {
1082
+ watcher.close();
1083
+ } catch {}
1084
+ activeWatchers.delete(dir);
1085
+ });
1086
+ watcher.unref?.();
1087
+ activeWatchers.set(dir, watcher);
1088
+ } catch {}
1089
+ }
1090
+ };
997
1091
  const requestTokenAuth = Object.freeze({
998
1092
  name: "Codex CLI auth.json token",
999
1093
  async resolve({ credential }) {
@@ -1050,10 +1144,11 @@ function apply(ctx, config = {}) {
1050
1144
  }
1051
1145
  });
1052
1146
  }
1053
- function ensureSnapshot() {
1147
+ function ensureSnapshot(options) {
1148
+ const prevCatalogSig = currentState?.catalogSig;
1054
1149
  const retryPolicy = activeRetryPolicy;
1055
1150
  const codexHome = getCodexHome();
1056
- const cachePath = join(codexHome, "models_cache.json");
1151
+ const cachePath = join(codexHome, MODELS_CACHE_FILE_NAME);
1057
1152
  const disabled = getDisabledModels();
1058
1153
  const contextWindows = getContextWindows();
1059
1154
  const setActiveRaw = (next) => {
@@ -1172,6 +1267,11 @@ function apply(ctx, config = {}) {
1172
1267
  retryPolicySig
1173
1268
  });
1174
1269
  }
1270
+ if (adapterRegistered && !watcherDisposed) syncModelsCacheWatchers();
1271
+ if (adapterRegistered && options?.notifyAdaptersUpdated !== false && prevCatalogSig !== void 0 && nextCatalogSig !== prevCatalogSig) {
1272
+ committedState = currentState;
1273
+ ctx.llm.emitAdaptersUpdated?.();
1274
+ }
1175
1275
  return currentState;
1176
1276
  }
1177
1277
  const adapter = new CodexPiAiAdapter({
@@ -1202,8 +1302,24 @@ function apply(ctx, config = {}) {
1202
1302
  const snapProfiles = snapshot?.profiles;
1203
1303
  return ((snapProfiles !== void 0 ? catalogByProfiles.get(snapProfiles) : void 0) ?? ensureSnapshot().catalog).metadataById.get(modelId);
1204
1304
  });
1205
- let committedState = ensureSnapshot();
1305
+ let committedState = ensureSnapshot({ notifyAdaptersUpdated: false });
1206
1306
  const registration = ctx.llm.registerAdapter([PROVIDER], adapter);
1307
+ adapterRegistered = true;
1308
+ if (typeof ctx.effect === "function") ctx.effect(() => {
1309
+ watcherDisposed = false;
1310
+ syncModelsCacheWatchers();
1311
+ return () => {
1312
+ watcherDisposed = true;
1313
+ closeModelsCacheWatchers();
1314
+ };
1315
+ }, "codex-subscription: models_cache watcher");
1316
+ else {
1317
+ syncModelsCacheWatchers();
1318
+ ctx.on?.("dispose", () => {
1319
+ watcherDisposed = true;
1320
+ closeModelsCacheWatchers();
1321
+ });
1322
+ }
1207
1323
  const mountUsageRoute = (connCtx) => {
1208
1324
  const connection = connCtx.get?.("connection");
1209
1325
  const registerFetch = connection?.fetch?.register;
@@ -1284,7 +1400,7 @@ function apply(ctx, config = {}) {
1284
1400
  activeRetryPolicy = candidateRetryPolicy;
1285
1401
  let nextState;
1286
1402
  try {
1287
- nextState = ensureSnapshot();
1403
+ nextState = ensureSnapshot({ notifyAdaptersUpdated: false });
1288
1404
  if (nextState.retryPolicySig !== prevCommitted.retryPolicySig) {
1289
1405
  registration.replace([PROVIDER]);
1290
1406
  committedState = nextState;
@@ -1326,4 +1442,4 @@ const __test = Object.freeze({
1326
1442
  createUsageFetcher
1327
1443
  });
1328
1444
  //#endregion
1329
- export { BACKUP_CACHE_FILE_NAME, Config, LOG_FILE_NAME, PLUGIN_CACHE_DIR_NAME, RUNTIME_STATE_FILE_NAME, __test, apply, inject, name };
1445
+ export { BACKUP_CACHE_FILE_NAME, Config, LOG_FILE_NAME, MODELS_CACHE_FILE_NAME, MODELS_CACHE_WATCH_DEBOUNCE_MS, PLUGIN_CACHE_DIR_NAME, RUNTIME_STATE_FILE_NAME, __test, apply, inject, name };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lyogvce/dsh-codex-subscription",
3
- "version": "0.2.12",
3
+ "version": "0.2.13",
4
4
  "description": "复用本机 ~/.codex 订阅凭证与 models_cache.json 模型缓存,在 DSH 中通过 SSE 直连调用 Codex 模型。",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",