@lyogvce/dsh-codex-subscription 0.2.12 → 0.3.0
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 +3 -3
- package/lib/client.js +23 -8
- package/lib/index.d.ts +5 -0
- package/lib/index.js +224 -26
- package/package.json +12 -14
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
|
-
-
|
|
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`)**
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
4. **原生多轮思考链回放、工具调用与出站指纹净化**
|
|
23
23
|
- 底层复用 DSH 内置的 `PiAiAdapter` 与 `@earendil-works/pi-ai` 的 `openaiCodexProvider()`,固定使用 `sse` 传输(含 `zstd` 请求体压缩、规范化 36 位 RFC 4122 UUID `session-id` 缓存头与 `prompt_cache_key`、`reasoning.encrypted_content` 签名回放与图片自动缩放卸载),并通过自定义 `fetch` 与 `onPayload` 剥除 `OpenAI-Beta: responses=experimental`、静态 `x-client-request-id`、`msg_pi_*` 等非官方指纹。
|
|
24
24
|
5. **Codex 订阅限额内联展示与 ⚡ Fast 极速模式一键切换(`conversation.input.right`)**
|
|
25
|
-
- 当会话选中 `openai-codex` 模型时,在输入框右下角紧邻模型选择器左侧展示限额胶囊(根据 `wham/usage` 返回的 `limit_window_seconds` 自适应展示 Plus 的 `5h X% · 7d Y%` 双窗口或 Pro 的 `7d X%`
|
|
25
|
+
- 当会话选中 `openai-codex` 模型时,在输入框右下角紧邻模型选择器左侧展示限额胶囊(根据 `wham/usage` 返回的 `limit_window_seconds` 自适应展示 Plus 的 `5h X% · 7d Y%` 双窗口或 Pro 的 `7d X%` 单窗口;由模型请求与流完成结算触发、按 60 秒限流在后台自动更新缓存,并由前端周期静默同步,无请求时零远端轮询,点击胶囊直接展开详情弹窗不触发额外请求),弹窗内可查看窗口进度条、重置倒计时与手动刷新按钮;切换至其他 Provider 模型时自动隐藏。
|
|
26
26
|
- **⚡ 会话树级 Fast 极速模式(`service_tier: "priority"`,最高 2x 推理速度,主会话与 Subagent / 智能体团队同开同关)**:每个主会话可在限额弹窗底部独立开关 Fast 模式(不同主会话互不影响,新会话默认继承插件设置中的 `fastMode`,未配置时默认 `false` 关闭),该主会话派生的所有 `subagent`、`subagent_fork`、`workflow` 子任务以及智能体团队成员(`spawn_teammate`)均自动绑定至同一主会话开关,实时联动、同开同关;状态自动持久化至 `~/.codex/cache/dsh-codex-subscription/runtime-state.json`。开启后胶囊显示闪电标识 `⚡`,并在请求体中自动注入 `"service_tier": "priority"`。
|
|
27
27
|
|
|
28
28
|
---
|
package/lib/client.js
CHANGED
|
@@ -171,7 +171,7 @@ window.__ModuleLoader__.load({
|
|
|
171
171
|
function createQuotaStore(options = {}) {
|
|
172
172
|
const fetchFn = options.fetchImpl ?? globalThis.fetch;
|
|
173
173
|
const nowFn = options.now ?? Date.now;
|
|
174
|
-
const
|
|
174
|
+
const explicitTtlMs = options.ttlMs;
|
|
175
175
|
let state = {
|
|
176
176
|
quota: null,
|
|
177
177
|
defaultFastMode: false,
|
|
@@ -273,15 +273,16 @@ window.__ModuleLoader__.load({
|
|
|
273
273
|
};
|
|
274
274
|
}
|
|
275
275
|
async function refresh(force = false) {
|
|
276
|
-
if (!force && state.quota !== null && nowFn() - state.quota.fetchedAt <
|
|
276
|
+
if (!force && explicitTtlMs !== void 0 && state.quota !== null && nowFn() - state.quota.fetchedAt < explicitTtlMs) return;
|
|
277
277
|
if (inflight !== null && (!force || inflightForce)) return inflight;
|
|
278
278
|
const seq = ++requestSeq;
|
|
279
279
|
const canReplaceFastAtStart = pendingBySeq.size === 0;
|
|
280
280
|
const fastEpochAtStart = fastMutationEpoch;
|
|
281
281
|
inflightForce = force;
|
|
282
|
-
|
|
282
|
+
const showLoading = force || state.quota === null;
|
|
283
|
+
if (showLoading || state.error !== null) setState({
|
|
283
284
|
...state,
|
|
284
|
-
loading: true,
|
|
285
|
+
loading: showLoading ? true : state.loading,
|
|
285
286
|
error: null
|
|
286
287
|
});
|
|
287
288
|
const url = force ? `${USAGE_ROUTE_PATH}?force=1` : USAGE_ROUTE_PATH;
|
|
@@ -489,6 +490,8 @@ window.__ModuleLoader__.load({
|
|
|
489
490
|
const NS = "codex-subscription";
|
|
490
491
|
const SLOT_NAME = "conversation.input.right";
|
|
491
492
|
const SLOT_ENTRY_ID = "codex-subscription:quota";
|
|
493
|
+
const ACTIVE_QUOTA_SYNC_INTERVAL_MS = 5e3;
|
|
494
|
+
const IDLE_QUOTA_SYNC_INTERVAL_MS = 15e3;
|
|
492
495
|
const zh = {
|
|
493
496
|
"quota.title": "Codex 订阅限额",
|
|
494
497
|
"quota.loading": "正在获取 Codex 限额…",
|
|
@@ -547,8 +550,8 @@ window.__ModuleLoader__.load({
|
|
|
547
550
|
prevRunningRef.current = running;
|
|
548
551
|
return;
|
|
549
552
|
}
|
|
550
|
-
quotaStore.refresh(false);
|
|
551
553
|
if (sameSession && didTurnFinish(prevRunningRef.current, running)) quotaStore.refresh(true);
|
|
554
|
+
else if (!sameSession || !running || quotaStore.getSnapshot().quota === null) quotaStore.refresh(false);
|
|
552
555
|
prevRunningRef.current = running;
|
|
553
556
|
}, [
|
|
554
557
|
isCodex,
|
|
@@ -556,6 +559,20 @@ window.__ModuleLoader__.load({
|
|
|
556
559
|
sessionId,
|
|
557
560
|
quotaStore
|
|
558
561
|
]);
|
|
562
|
+
React.useEffect(() => {
|
|
563
|
+
if (!isCodex || typeof globalThis.setInterval !== "function") return;
|
|
564
|
+
const intervalMs = running ? ACTIVE_QUOTA_SYNC_INTERVAL_MS : IDLE_QUOTA_SYNC_INTERVAL_MS;
|
|
565
|
+
const timer = globalThis.setInterval(() => {
|
|
566
|
+
quotaStore.refresh(false);
|
|
567
|
+
}, intervalMs);
|
|
568
|
+
return () => {
|
|
569
|
+
globalThis.clearInterval(timer);
|
|
570
|
+
};
|
|
571
|
+
}, [
|
|
572
|
+
isCodex,
|
|
573
|
+
running,
|
|
574
|
+
quotaStore
|
|
575
|
+
]);
|
|
559
576
|
React.useEffect(() => {
|
|
560
577
|
if (!open || !isCodex || typeof globalThis.document === "undefined") return;
|
|
561
578
|
const onPointerDown = (event) => {
|
|
@@ -609,9 +626,7 @@ window.__ModuleLoader__.load({
|
|
|
609
626
|
"aria-expanded": open,
|
|
610
627
|
title: `${t("quota.title")} (${pillText})`,
|
|
611
628
|
onClick: () => {
|
|
612
|
-
|
|
613
|
-
setOpen(nextOpen);
|
|
614
|
-
if (nextOpen) quotaStore.refresh(false);
|
|
629
|
+
setOpen(!open);
|
|
615
630
|
}
|
|
616
631
|
}, h("svg", {
|
|
617
632
|
viewBox: "0 0 14 14",
|
package/lib/index.d.ts
CHANGED
|
@@ -160,17 +160,22 @@ interface UsageFetcherOptions {
|
|
|
160
160
|
readonly now?: (() => number) | undefined;
|
|
161
161
|
readonly ttlMs?: number | undefined;
|
|
162
162
|
readonly minForceIntervalMs?: number | undefined;
|
|
163
|
+
readonly autoRefresh?: boolean | undefined;
|
|
163
164
|
}
|
|
164
165
|
declare function parseUsagePayload(raw: unknown, fetchedAt: number): CodexQuotaSnapshot | null;
|
|
165
166
|
declare function createUsageFetcher(options: UsageFetcherOptions): {
|
|
166
167
|
getUsage: (force?: boolean) => Promise<CodexQuotaResponse>;
|
|
168
|
+
onRequestStart: () => void;
|
|
167
169
|
invalidate: () => void;
|
|
170
|
+
dispose: () => void;
|
|
168
171
|
};
|
|
169
172
|
//#endregion
|
|
170
173
|
//#region src/index.d.ts
|
|
171
174
|
export declare const name = "codex-subscription";
|
|
172
175
|
export declare const inject: string[];
|
|
173
176
|
export declare const LOG_FILE_NAME = "plugin.log";
|
|
177
|
+
export declare const MODELS_CACHE_FILE_NAME = "models_cache.json";
|
|
178
|
+
export declare const MODELS_CACHE_WATCH_DEBOUNCE_MS = 100;
|
|
174
179
|
export declare const Config: any;
|
|
175
180
|
export declare function apply(ctx: PluginContextLike, config?: Record<string, unknown>): void;
|
|
176
181
|
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";
|
|
@@ -402,15 +402,59 @@ function parseUsagePayload(raw, fetchedAt) {
|
|
|
402
402
|
fetchedAt
|
|
403
403
|
};
|
|
404
404
|
}
|
|
405
|
+
function isWindowResetDue(snapshot, nowMs) {
|
|
406
|
+
const nowSec = nowMs / 1e3;
|
|
407
|
+
const fetchedSec = snapshot.fetchedAt / 1e3;
|
|
408
|
+
const primaryReset = snapshot.primary.resetAt;
|
|
409
|
+
if (primaryReset !== null && fetchedSec < primaryReset && nowSec >= primaryReset) return true;
|
|
410
|
+
const secondaryReset = snapshot.secondary?.resetAt ?? null;
|
|
411
|
+
if (secondaryReset !== null && fetchedSec < secondaryReset && nowSec >= secondaryReset) return true;
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
405
414
|
function createUsageFetcher(options) {
|
|
406
415
|
const fetchFn = options.fetchImpl ?? globalThis.fetch;
|
|
407
416
|
const nowFn = options.now ?? Date.now;
|
|
408
417
|
const ttlMs = options.ttlMs ?? 6e4;
|
|
409
418
|
const minForceIntervalMs = options.minForceIntervalMs ?? 5e3;
|
|
419
|
+
const autoRefresh = options.autoRefresh === true;
|
|
410
420
|
let cached;
|
|
411
|
-
let
|
|
421
|
+
let lastAttemptAt;
|
|
422
|
+
let hasConsumer = false;
|
|
423
|
+
let dirtyRevision = 0;
|
|
424
|
+
let fetchedRevision = 0;
|
|
412
425
|
let inflight;
|
|
426
|
+
let trailingTimer;
|
|
427
|
+
let disposed = false;
|
|
428
|
+
const isStale = () => dirtyRevision > fetchedRevision;
|
|
429
|
+
function clearTrailingTimer() {
|
|
430
|
+
if (trailingTimer !== void 0) {
|
|
431
|
+
clearTimeout(trailingTimer);
|
|
432
|
+
trailingTimer = void 0;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
function scheduleBackgroundRefresh(deferIfCached = false) {
|
|
436
|
+
if (!autoRefresh || !hasConsumer || disposed || !isStale() || inflight !== void 0) return;
|
|
437
|
+
const baseTime = cached?.fetchedAt ?? lastAttemptAt;
|
|
438
|
+
if (baseTime === void 0) {
|
|
439
|
+
runSingleFlight();
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
const ageMs = Math.max(0, nowFn() - baseTime);
|
|
443
|
+
if (!deferIfCached && ageMs >= ttlMs) {
|
|
444
|
+
runSingleFlight();
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
if (trailingTimer !== void 0) return;
|
|
448
|
+
const delayMs = deferIfCached ? Math.max(1, ageMs >= ttlMs ? ttlMs : ttlMs - ageMs) : Math.max(1, ttlMs - ageMs);
|
|
449
|
+
trailingTimer = setTimeout(() => {
|
|
450
|
+
trailingTimer = void 0;
|
|
451
|
+
if (disposed || !isStale()) return;
|
|
452
|
+
runSingleFlight();
|
|
453
|
+
}, delayMs);
|
|
454
|
+
trailingTimer.unref?.();
|
|
455
|
+
}
|
|
413
456
|
async function fetchFresh() {
|
|
457
|
+
const revAtStart = dirtyRevision;
|
|
414
458
|
try {
|
|
415
459
|
const creds = await options.resolveCredentials();
|
|
416
460
|
const headers = options.buildHeaders({
|
|
@@ -423,24 +467,28 @@ function createUsageFetcher(options) {
|
|
|
423
467
|
headers,
|
|
424
468
|
signal: AbortSignal.timeout(USAGE_FETCH_TIMEOUT_MS)
|
|
425
469
|
});
|
|
470
|
+
const finishedAt = nowFn();
|
|
471
|
+
lastAttemptAt = finishedAt;
|
|
472
|
+
fetchedRevision = Math.max(fetchedRevision, revAtStart);
|
|
426
473
|
if (!response.ok) return {
|
|
427
474
|
ok: false,
|
|
428
475
|
error: `Codex 限额查询失败 (HTTP ${response.status})`,
|
|
429
476
|
...cached !== void 0 ? { quota: cached } : {}
|
|
430
477
|
};
|
|
431
|
-
const parsed = parseUsagePayload(await response.json(),
|
|
478
|
+
const parsed = parseUsagePayload(await response.json(), finishedAt);
|
|
432
479
|
if (parsed === null) return {
|
|
433
480
|
ok: false,
|
|
434
481
|
error: "Codex 限额响应格式无法解析",
|
|
435
482
|
...cached !== void 0 ? { quota: cached } : {}
|
|
436
483
|
};
|
|
437
484
|
cached = parsed;
|
|
438
|
-
stale = false;
|
|
439
485
|
return {
|
|
440
486
|
ok: true,
|
|
441
487
|
quota: parsed
|
|
442
488
|
};
|
|
443
489
|
} catch (error) {
|
|
490
|
+
lastAttemptAt = nowFn();
|
|
491
|
+
fetchedRevision = Math.max(fetchedRevision, revAtStart);
|
|
444
492
|
return {
|
|
445
493
|
ok: false,
|
|
446
494
|
error: safeErrorMessage(error),
|
|
@@ -448,26 +496,52 @@ function createUsageFetcher(options) {
|
|
|
448
496
|
};
|
|
449
497
|
}
|
|
450
498
|
}
|
|
499
|
+
function runSingleFlight() {
|
|
500
|
+
clearTrailingTimer();
|
|
501
|
+
if (!inflight) inflight = fetchFresh().finally(() => {
|
|
502
|
+
inflight = void 0;
|
|
503
|
+
if (autoRefresh && !disposed && isStale()) scheduleBackgroundRefresh();
|
|
504
|
+
});
|
|
505
|
+
return inflight;
|
|
506
|
+
}
|
|
451
507
|
return {
|
|
508
|
+
onRequestStart() {
|
|
509
|
+
dirtyRevision += 1;
|
|
510
|
+
scheduleBackgroundRefresh(true);
|
|
511
|
+
},
|
|
452
512
|
invalidate() {
|
|
453
|
-
|
|
513
|
+
dirtyRevision += 1;
|
|
514
|
+
scheduleBackgroundRefresh(false);
|
|
515
|
+
},
|
|
516
|
+
dispose() {
|
|
517
|
+
disposed = true;
|
|
518
|
+
clearTrailingTimer();
|
|
454
519
|
},
|
|
455
520
|
async getUsage(force = false) {
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
if (force
|
|
521
|
+
hasConsumer = true;
|
|
522
|
+
if (cached !== void 0) {
|
|
523
|
+
const nowMs = nowFn();
|
|
524
|
+
const ageMs = nowMs - cached.fetchedAt;
|
|
525
|
+
const stale = isStale();
|
|
526
|
+
const resetDue = isWindowResetDue(cached, nowMs);
|
|
527
|
+
if (!force) {
|
|
528
|
+
if (ageMs >= 0 && ageMs < ttlMs && (!stale || autoRefresh)) {
|
|
529
|
+
if (stale) scheduleBackgroundRefresh(false);
|
|
530
|
+
return {
|
|
531
|
+
ok: true,
|
|
532
|
+
quota: cached
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
if (autoRefresh && !stale && !resetDue) return {
|
|
536
|
+
ok: true,
|
|
537
|
+
quota: cached
|
|
538
|
+
};
|
|
539
|
+
} else if (!stale && ageMs >= 0 && ageMs < minForceIntervalMs) return {
|
|
463
540
|
ok: true,
|
|
464
541
|
quota: cached
|
|
465
542
|
};
|
|
466
543
|
}
|
|
467
|
-
|
|
468
|
-
inflight = void 0;
|
|
469
|
-
});
|
|
470
|
-
return inflight;
|
|
544
|
+
return runSingleFlight();
|
|
471
545
|
}
|
|
472
546
|
};
|
|
473
547
|
}
|
|
@@ -704,6 +778,8 @@ function createAuthManager(options) {
|
|
|
704
778
|
const name = "codex-subscription";
|
|
705
779
|
const inject = ["llm"];
|
|
706
780
|
const LOG_FILE_NAME = "plugin.log";
|
|
781
|
+
const MODELS_CACHE_FILE_NAME = "models_cache.json";
|
|
782
|
+
const MODELS_CACHE_WATCH_DEBOUNCE_MS = 100;
|
|
707
783
|
const STREAM_IDLE_TIMEOUT_MS = 3e5;
|
|
708
784
|
const MAX_REQUEST_IMAGE_BYTES = 20971520;
|
|
709
785
|
const REQUEST_IMAGE_PIXEL_BUDGET = 4194304;
|
|
@@ -711,6 +787,27 @@ const REQUEST_IMAGE_MAX_BYTES = 1048576;
|
|
|
711
787
|
const MAX_LOG_FILE_BYTES = 524288;
|
|
712
788
|
const MAX_PERSISTED_SESSIONS = 200;
|
|
713
789
|
const MAX_PERSISTED_PARENTS = 1e3;
|
|
790
|
+
function resolveModelsCacheWatchTargets(codexHome) {
|
|
791
|
+
const dirs = new Set();
|
|
792
|
+
const fileNames = new Set([MODELS_CACHE_FILE_NAME]);
|
|
793
|
+
try {
|
|
794
|
+
dirs.add(realpathSync(codexHome));
|
|
795
|
+
} catch {
|
|
796
|
+
try {
|
|
797
|
+
if (statSync(codexHome).isDirectory()) dirs.add(codexHome);
|
|
798
|
+
} catch {}
|
|
799
|
+
}
|
|
800
|
+
const cachePath = join(codexHome, MODELS_CACHE_FILE_NAME);
|
|
801
|
+
try {
|
|
802
|
+
const realCachePath = realpathSync(cachePath);
|
|
803
|
+
dirs.add(dirname(realCachePath));
|
|
804
|
+
fileNames.add(basename(realCachePath));
|
|
805
|
+
} catch {}
|
|
806
|
+
return {
|
|
807
|
+
dirs: [...dirs],
|
|
808
|
+
fileNames
|
|
809
|
+
};
|
|
810
|
+
}
|
|
714
811
|
function resolvePluginCacheDir(codexHome) {
|
|
715
812
|
return join(codexHome, PLUGIN_CACHE_DIR_NAME);
|
|
716
813
|
}
|
|
@@ -840,7 +937,8 @@ var CodexPiAiAdapter = class extends PiAiAdapter {
|
|
|
840
937
|
function apply(ctx, config = {}) {
|
|
841
938
|
const baseProvider = openaiCodexProvider();
|
|
842
939
|
const baseModels = baseProvider.getModels();
|
|
843
|
-
const
|
|
940
|
+
const boundCodexHome = resolveCodexHome();
|
|
941
|
+
const getCodexHome = () => boundCodexHome;
|
|
844
942
|
const getConfigFastMode = () => readVolatile(config.fastMode, false) === true;
|
|
845
943
|
const getDisabledModels = () => readVolatile(config.disabledModels, DEFAULT_DISABLED_MODELS);
|
|
846
944
|
const getContextWindows = () => readVolatile(config.contextWindows, {});
|
|
@@ -993,7 +1091,77 @@ function apply(ctx, config = {}) {
|
|
|
993
1091
|
let rawRevision = 0;
|
|
994
1092
|
let activeRetryPolicy = resolveConfiguredRetryPolicy();
|
|
995
1093
|
let currentState;
|
|
1094
|
+
let adapterRegistered = false;
|
|
996
1095
|
const catalogByProfiles = new WeakMap();
|
|
1096
|
+
const activeWatchers = new Map();
|
|
1097
|
+
let watchedFileNames = new Set([MODELS_CACHE_FILE_NAME]);
|
|
1098
|
+
let watchDebounceTimer;
|
|
1099
|
+
let watcherDisposed = false;
|
|
1100
|
+
const closeModelsCacheWatchers = () => {
|
|
1101
|
+
if (watchDebounceTimer !== void 0) {
|
|
1102
|
+
clearTimeout(watchDebounceTimer);
|
|
1103
|
+
watchDebounceTimer = void 0;
|
|
1104
|
+
}
|
|
1105
|
+
for (const watcher of activeWatchers.values()) try {
|
|
1106
|
+
watcher.close();
|
|
1107
|
+
} catch {}
|
|
1108
|
+
activeWatchers.clear();
|
|
1109
|
+
};
|
|
1110
|
+
const scheduleModelsCacheCheck = () => {
|
|
1111
|
+
if (watcherDisposed) return;
|
|
1112
|
+
if (watchDebounceTimer !== void 0) clearTimeout(watchDebounceTimer);
|
|
1113
|
+
watchDebounceTimer = setTimeout(() => {
|
|
1114
|
+
watchDebounceTimer = void 0;
|
|
1115
|
+
if (watcherDisposed) return;
|
|
1116
|
+
try {
|
|
1117
|
+
if (!statSync(getCodexHome()).isDirectory()) {
|
|
1118
|
+
watcherDisposed = true;
|
|
1119
|
+
closeModelsCacheWatchers();
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
} catch {
|
|
1123
|
+
watcherDisposed = true;
|
|
1124
|
+
closeModelsCacheWatchers();
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
syncModelsCacheWatchers();
|
|
1128
|
+
ensureSnapshot();
|
|
1129
|
+
}, 100);
|
|
1130
|
+
watchDebounceTimer.unref?.();
|
|
1131
|
+
};
|
|
1132
|
+
const syncModelsCacheWatchers = () => {
|
|
1133
|
+
if (watcherDisposed) return;
|
|
1134
|
+
const { dirs, fileNames } = resolveModelsCacheWatchTargets(getCodexHome());
|
|
1135
|
+
watchedFileNames = fileNames;
|
|
1136
|
+
const nextDirSet = new Set(dirs);
|
|
1137
|
+
for (const [dir, watcher] of activeWatchers.entries()) if (!nextDirSet.has(dir)) {
|
|
1138
|
+
try {
|
|
1139
|
+
watcher.close();
|
|
1140
|
+
} catch {}
|
|
1141
|
+
activeWatchers.delete(dir);
|
|
1142
|
+
}
|
|
1143
|
+
for (const dir of nextDirSet) {
|
|
1144
|
+
if (activeWatchers.has(dir)) continue;
|
|
1145
|
+
try {
|
|
1146
|
+
const watcher = watch(dir, { persistent: false }, (_eventType, rawFilename) => {
|
|
1147
|
+
if (watcherDisposed) return;
|
|
1148
|
+
if (rawFilename != null) {
|
|
1149
|
+
const name = typeof rawFilename === "string" ? rawFilename : String(rawFilename);
|
|
1150
|
+
if (name !== "" && !watchedFileNames.has(name)) return;
|
|
1151
|
+
}
|
|
1152
|
+
scheduleModelsCacheCheck();
|
|
1153
|
+
});
|
|
1154
|
+
watcher.on("error", () => {
|
|
1155
|
+
try {
|
|
1156
|
+
watcher.close();
|
|
1157
|
+
} catch {}
|
|
1158
|
+
activeWatchers.delete(dir);
|
|
1159
|
+
});
|
|
1160
|
+
watcher.unref?.();
|
|
1161
|
+
activeWatchers.set(dir, watcher);
|
|
1162
|
+
} catch {}
|
|
1163
|
+
}
|
|
1164
|
+
};
|
|
997
1165
|
const requestTokenAuth = Object.freeze({
|
|
998
1166
|
name: "Codex CLI auth.json token",
|
|
999
1167
|
async resolve({ credential }) {
|
|
@@ -1007,7 +1175,7 @@ function apply(ctx, config = {}) {
|
|
|
1007
1175
|
});
|
|
1008
1176
|
function wrapOptions(catalog, model, options = {}) {
|
|
1009
1177
|
const modelMeta = catalog.metadataById.get(model?.id);
|
|
1010
|
-
const { temperature: _temperature, sessionId, onPayload, ...rest } = options;
|
|
1178
|
+
const { temperature: _temperature, sessionId, onPayload, onProviderStreamEvent, ...rest } = options;
|
|
1011
1179
|
const initiatorId = (ctx.get?.("agents"))?.currentInitiator?.()?.id;
|
|
1012
1180
|
const normalizedSessionId = normalizeCodexSessionId(sessionId) ?? normalizeCodexSessionId(initiatorId);
|
|
1013
1181
|
const wireSessionId = toCodexWireSessionId(normalizedSessionId);
|
|
@@ -1023,12 +1191,18 @@ function apply(ctx, config = {}) {
|
|
|
1023
1191
|
}),
|
|
1024
1192
|
async onPayload(payload, requestModel) {
|
|
1025
1193
|
return sanitizeCodexPayload(await onPayload?.(payload, requestModel) ?? payload, modelMeta, getEffectiveFastMode(normalizedSessionId));
|
|
1194
|
+
},
|
|
1195
|
+
async onProviderStreamEvent(event, eventModel) {
|
|
1196
|
+
await onProviderStreamEvent?.(event, eventModel);
|
|
1197
|
+
const eventType = event !== null && typeof event === "object" ? event.type : void 0;
|
|
1198
|
+
if (eventType === "response.completed" || eventType === "response.done" || eventType === "response.incomplete") usageFetcher.invalidate();
|
|
1026
1199
|
}
|
|
1027
1200
|
};
|
|
1028
1201
|
}
|
|
1029
1202
|
const usageFetcher = createUsageFetcher({
|
|
1030
1203
|
resolveCredentials: () => authManager.resolveCredentials(),
|
|
1031
|
-
buildHeaders: (authHeaders) => buildCodexHeaders(authHeaders, ensureSnapshot().catalog.clientVersion)
|
|
1204
|
+
buildHeaders: (authHeaders) => buildCodexHeaders(authHeaders, ensureSnapshot().catalog.clientVersion),
|
|
1205
|
+
autoRefresh: true
|
|
1032
1206
|
});
|
|
1033
1207
|
function createSnapshotPiProvider(snapshotCatalog) {
|
|
1034
1208
|
return Object.freeze({
|
|
@@ -1041,19 +1215,20 @@ function apply(ctx, config = {}) {
|
|
|
1041
1215
|
}),
|
|
1042
1216
|
getModels: () => snapshotCatalog.models,
|
|
1043
1217
|
stream: (model, context, options) => {
|
|
1044
|
-
usageFetcher.
|
|
1218
|
+
usageFetcher.onRequestStart();
|
|
1045
1219
|
return baseProvider.stream(model, context, wrapOptions(snapshotCatalog, model, options));
|
|
1046
1220
|
},
|
|
1047
1221
|
streamSimple: (model, context, options) => {
|
|
1048
|
-
usageFetcher.
|
|
1222
|
+
usageFetcher.onRequestStart();
|
|
1049
1223
|
return baseProvider.streamSimple(model, context, wrapOptions(snapshotCatalog, model, options));
|
|
1050
1224
|
}
|
|
1051
1225
|
});
|
|
1052
1226
|
}
|
|
1053
|
-
function ensureSnapshot() {
|
|
1227
|
+
function ensureSnapshot(options) {
|
|
1228
|
+
const prevCatalogSig = currentState?.catalogSig;
|
|
1054
1229
|
const retryPolicy = activeRetryPolicy;
|
|
1055
1230
|
const codexHome = getCodexHome();
|
|
1056
|
-
const cachePath = join(codexHome,
|
|
1231
|
+
const cachePath = join(codexHome, MODELS_CACHE_FILE_NAME);
|
|
1057
1232
|
const disabled = getDisabledModels();
|
|
1058
1233
|
const contextWindows = getContextWindows();
|
|
1059
1234
|
const setActiveRaw = (next) => {
|
|
@@ -1172,6 +1347,11 @@ function apply(ctx, config = {}) {
|
|
|
1172
1347
|
retryPolicySig
|
|
1173
1348
|
});
|
|
1174
1349
|
}
|
|
1350
|
+
if (adapterRegistered && !watcherDisposed) syncModelsCacheWatchers();
|
|
1351
|
+
if (adapterRegistered && options?.notifyAdaptersUpdated !== false && prevCatalogSig !== void 0 && nextCatalogSig !== prevCatalogSig) {
|
|
1352
|
+
committedState = currentState;
|
|
1353
|
+
ctx.llm.emitAdaptersUpdated?.();
|
|
1354
|
+
}
|
|
1175
1355
|
return currentState;
|
|
1176
1356
|
}
|
|
1177
1357
|
const adapter = new CodexPiAiAdapter({
|
|
@@ -1202,8 +1382,26 @@ function apply(ctx, config = {}) {
|
|
|
1202
1382
|
const snapProfiles = snapshot?.profiles;
|
|
1203
1383
|
return ((snapProfiles !== void 0 ? catalogByProfiles.get(snapProfiles) : void 0) ?? ensureSnapshot().catalog).metadataById.get(modelId);
|
|
1204
1384
|
});
|
|
1205
|
-
let committedState = ensureSnapshot();
|
|
1385
|
+
let committedState = ensureSnapshot({ notifyAdaptersUpdated: false });
|
|
1206
1386
|
const registration = ctx.llm.registerAdapter([PROVIDER], adapter);
|
|
1387
|
+
adapterRegistered = true;
|
|
1388
|
+
if (typeof ctx.effect === "function") ctx.effect(() => {
|
|
1389
|
+
watcherDisposed = false;
|
|
1390
|
+
syncModelsCacheWatchers();
|
|
1391
|
+
return () => {
|
|
1392
|
+
watcherDisposed = true;
|
|
1393
|
+
closeModelsCacheWatchers();
|
|
1394
|
+
usageFetcher.dispose();
|
|
1395
|
+
};
|
|
1396
|
+
}, "codex-subscription: models_cache watcher");
|
|
1397
|
+
else {
|
|
1398
|
+
syncModelsCacheWatchers();
|
|
1399
|
+
ctx.on?.("dispose", () => {
|
|
1400
|
+
watcherDisposed = true;
|
|
1401
|
+
closeModelsCacheWatchers();
|
|
1402
|
+
usageFetcher.dispose();
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1207
1405
|
const mountUsageRoute = (connCtx) => {
|
|
1208
1406
|
const connection = connCtx.get?.("connection");
|
|
1209
1407
|
const registerFetch = connection?.fetch?.register;
|
|
@@ -1284,7 +1482,7 @@ function apply(ctx, config = {}) {
|
|
|
1284
1482
|
activeRetryPolicy = candidateRetryPolicy;
|
|
1285
1483
|
let nextState;
|
|
1286
1484
|
try {
|
|
1287
|
-
nextState = ensureSnapshot();
|
|
1485
|
+
nextState = ensureSnapshot({ notifyAdaptersUpdated: false });
|
|
1288
1486
|
if (nextState.retryPolicySig !== prevCommitted.retryPolicySig) {
|
|
1289
1487
|
registration.replace([PROVIDER]);
|
|
1290
1488
|
committedState = nextState;
|
|
@@ -1326,4 +1524,4 @@ const __test = Object.freeze({
|
|
|
1326
1524
|
createUsageFetcher
|
|
1327
1525
|
});
|
|
1328
1526
|
//#endregion
|
|
1329
|
-
export { BACKUP_CACHE_FILE_NAME, Config, LOG_FILE_NAME, PLUGIN_CACHE_DIR_NAME, RUNTIME_STATE_FILE_NAME, __test, apply, inject, name };
|
|
1527
|
+
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.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "复用本机 ~/.codex 订阅凭证与 models_cache.json 模型缓存,在 DSH 中通过 SSE 直连调用 Codex 模型。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -50,17 +50,6 @@
|
|
|
50
50
|
"tsdown": "0.23.0",
|
|
51
51
|
"typescript": "5.9.3"
|
|
52
52
|
},
|
|
53
|
-
"scripts": {
|
|
54
|
-
"build": "tsdown",
|
|
55
|
-
"dev": "tsdown --watch",
|
|
56
|
-
"typecheck": "tsc --noEmit",
|
|
57
|
-
"pretest": "pnpm build",
|
|
58
|
-
"test": "node --test tests/*.test.mjs",
|
|
59
|
-
"test:isolation": "./tools/test-isolation.sh",
|
|
60
|
-
"verify": "pnpm typecheck && pnpm test && pnpm test:isolation",
|
|
61
|
-
"prepack": "pnpm build",
|
|
62
|
-
"prepublishOnly": "pnpm verify"
|
|
63
|
-
},
|
|
64
53
|
"publishConfig": {
|
|
65
54
|
"access": "public"
|
|
66
55
|
},
|
|
@@ -71,5 +60,14 @@
|
|
|
71
60
|
"codex",
|
|
72
61
|
"openai-codex",
|
|
73
62
|
"subscription"
|
|
74
|
-
]
|
|
75
|
-
|
|
63
|
+
],
|
|
64
|
+
"scripts": {
|
|
65
|
+
"build": "tsdown",
|
|
66
|
+
"dev": "tsdown --watch",
|
|
67
|
+
"typecheck": "tsc --noEmit",
|
|
68
|
+
"pretest": "pnpm build",
|
|
69
|
+
"test": "node --test tests/*.test.mjs",
|
|
70
|
+
"test:isolation": "./tools/test-isolation.sh",
|
|
71
|
+
"verify": "pnpm typecheck && pnpm test && pnpm test:isolation"
|
|
72
|
+
}
|
|
73
|
+
}
|