@lyogvce/dsh-codex-subscription 0.1.0 → 0.2.1
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 +10 -4
- package/lib/client.js +429 -0
- package/lib/index.d.ts +65 -2
- package/lib/index.js +233 -24
- package/package.json +12 -1
package/README.md
CHANGED
|
@@ -13,10 +13,13 @@
|
|
|
13
13
|
- 每次请求前检查 `access_token` 的 JWT `exp`:距离过期不足 5 分钟时自动通过单飞锁(Single-flight)向 `https://auth.openai.com/oauth/token` 刷新,并通过 `realpathSync` + 临时文件 `rename` 原子写回 `auth.json`,保证与 Codex CLI 永不互踢下线。
|
|
14
14
|
2. **零网络轮询的本地模型目录发现**
|
|
15
15
|
- 直接读取 `~/.codex/models_cache.json`(按文件 `mtime` 与大小自动感知更新),提取 `visibility === "list"` 的模型及其 `context_window`、`supported_reasoning_levels`(自动将 `ultra` 映射为 DSH 的 `max` 档位)与 `default_verbosity`。
|
|
16
|
-
3.
|
|
16
|
+
3. **支持按配置屏蔽模型(`disabledModels`)与自定义上下文窗口(`contextWindows`)**
|
|
17
17
|
- 在插件配置中通过 `disabledModels` 屏蔽指定模型 `slug`,保持模型选择器干净清爽。
|
|
18
|
+
- 通过 `contextWindows` 按模型 `slug` 自定义向 DSH 会话与自动压缩器暴露的预算窗口(例如设为 `400000`);未配置时默认使用模型物理上限 `max_context_window`(`872000`)。同时底层 `PiCodexModel.contextWindow` 始终保持物理上限 `872000`,彻底防止 `dsh-llm-pi-ai` 在预算窗口与物理上限之间误判 `CONTEXT_WINDOW_EXCEEDED`。
|
|
18
19
|
4. **原生多轮思考链回放与工具调用**
|
|
19
20
|
- 底层复用 DSH 内置的 `PiAiAdapter` 与 `@earendil-works/pi-ai` 的 `openaiCodexProvider()`,固定使用 `sse` 传输(含 `zstd` 请求体压缩、`session-id` / `thread-id` 缓存头、`reasoning.encrypted_content` 签名回放与图片自动缩放卸载)。
|
|
21
|
+
5. **Codex 订阅限额内联展示(`conversation.input.right`)**
|
|
22
|
+
- 当会话选中 `openai-codex` 模型时,在输入框右下角紧邻模型选择器左侧展示 `5h X% · 7d Y%` 限额胶囊(数据源来自 `https://chatgpt.com/backend-api/wham/usage`,内置 60 秒缓存、单飞合并与每轮回复结束自动刷新),点击可展开查看 5 小时 / 7 天窗口进度条、重置倒计时与手动刷新按钮;切换至其他 Provider 模型时自动隐藏。
|
|
20
23
|
|
|
21
24
|
---
|
|
22
25
|
|
|
@@ -30,9 +33,9 @@
|
|
|
30
33
|
dsh plugin --profile desktop add @lyogvce/dsh-codex-subscription
|
|
31
34
|
```
|
|
32
35
|
|
|
33
|
-
### 2. 配置屏蔽模型(`disabledModels`)
|
|
36
|
+
### 2. 配置屏蔽模型(`disabledModels`)与上下文窗口(`contextWindows`)
|
|
34
37
|
|
|
35
|
-
默认配置(见 [cordis.patch.yml](cordis.patch.yml))已屏蔽旧模型与备用模型,仅保留主力模型(如 `gpt-6.1-sol`、`gpt-6-astra
|
|
38
|
+
默认配置(见 [cordis.patch.yml](cordis.patch.yml))已屏蔽旧模型与备用模型,仅保留主力模型(如 `gpt-6.1-sol`、`gpt-6-astra`)。未配置 `contextWindows` 时,模型默认使用 `872000` 上下文窗口;若希望主动限制长上下文预算(例如统一设为 400K),可在 Profile 的 `cordis.patch.yml` 中配置:
|
|
36
39
|
|
|
37
40
|
```yaml
|
|
38
41
|
- id: codex-subscription
|
|
@@ -44,9 +47,12 @@ dsh plugin --profile desktop add @lyogvce/dsh-codex-subscription
|
|
|
44
47
|
- gpt-5.6-sol
|
|
45
48
|
- gpt-5.6-terra
|
|
46
49
|
- gpt-5.6-luna
|
|
50
|
+
contextWindows:
|
|
51
|
+
gpt-6.1-sol: 400000
|
|
52
|
+
gpt-6-astra: 400000
|
|
47
53
|
```
|
|
48
54
|
|
|
49
|
-
可在 DSH 的 **设置 → 插件 → Codex 订阅直连** 页面或 `cordis.patch.yml` 中随时修改 `disabledModels`,实时生效无需重启。
|
|
55
|
+
可在 DSH 的 **设置 → 插件 → Codex 订阅直连** 页面或 `cordis.patch.yml` 中随时修改 `disabledModels` 与 `contextWindows`,实时生效无需重启。
|
|
50
56
|
|
|
51
57
|
---
|
|
52
58
|
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@lyogvce/dsh-codex-subscription",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
//#region src/catalog.ts
|
|
8
|
+
const PROVIDER = "openai-codex";
|
|
9
|
+
Object.freeze([
|
|
10
|
+
"gpt-6-sol",
|
|
11
|
+
"gpt-6-luna",
|
|
12
|
+
"gpt-5.6-sol",
|
|
13
|
+
"gpt-5.6-terra",
|
|
14
|
+
"gpt-5.6-luna"
|
|
15
|
+
]);
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/usage.ts
|
|
18
|
+
function safeErrorMessage(error) {
|
|
19
|
+
return (error instanceof Error ? error.message : String(error)).replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/gu, "[redacted]").replace(/\b(Bearer\s+)[^\s,;'"&]+/giu, "$1[redacted]").replace(/(\b(?:code|token|access_token|refresh_token|id_token)=)[^&\s"']+/giu, "$1[redacted]").replace(/("(?:access_token|refresh_token|id_token)"\s*:\s*")[^"]+"/giu, "$1[redacted]\"").slice(0, 1e3);
|
|
20
|
+
}
|
|
21
|
+
const USAGE_ROUTE_PATH = "/api/codex-subscription/usage";
|
|
22
|
+
function clampPercent(value) {
|
|
23
|
+
return Math.max(0, Math.min(100, Math.round(value)));
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/client/quota.ts
|
|
27
|
+
const RING_CIRCUMFERENCE = 2 * Math.PI * 5.5;
|
|
28
|
+
function resolveActiveProvider(directorySnapshot, projectedSelection) {
|
|
29
|
+
const dir = directorySnapshot && typeof directorySnapshot === "object" ? directorySnapshot : void 0;
|
|
30
|
+
const proj = projectedSelection && typeof projectedSelection === "object" ? projectedSelection : void 0;
|
|
31
|
+
return [
|
|
32
|
+
dir?.pending?.provider,
|
|
33
|
+
dir?.current?.provider,
|
|
34
|
+
proj?.next?.provider,
|
|
35
|
+
proj?.provider
|
|
36
|
+
].find((candidate) => typeof candidate === "string" && candidate !== "");
|
|
37
|
+
}
|
|
38
|
+
function isCodexProvider(provider) {
|
|
39
|
+
return provider === PROVIDER;
|
|
40
|
+
}
|
|
41
|
+
function didTurnFinish(previousRunning, currentRunning) {
|
|
42
|
+
return previousRunning === true && currentRunning === false;
|
|
43
|
+
}
|
|
44
|
+
function windowLevelOf(usedPercent, limitReached = false) {
|
|
45
|
+
if (limitReached || usedPercent >= 100) return "error";
|
|
46
|
+
if (usedPercent >= 80) return "warn";
|
|
47
|
+
return "ok";
|
|
48
|
+
}
|
|
49
|
+
function formatQuotaSummary(snapshot) {
|
|
50
|
+
const primaryPercent = clampPercent(snapshot.primary.usedPercent);
|
|
51
|
+
const secondaryPercent = snapshot.secondary !== null ? clampPercent(snapshot.secondary.usedPercent) : null;
|
|
52
|
+
return {
|
|
53
|
+
primaryPercent,
|
|
54
|
+
secondaryPercent,
|
|
55
|
+
level: windowLevelOf(secondaryPercent !== null ? Math.max(primaryPercent, secondaryPercent) : primaryPercent, snapshot.limitReached),
|
|
56
|
+
pillText: secondaryPercent !== null ? `5h ${primaryPercent}% · 7d ${secondaryPercent}%` : `5h ${primaryPercent}%`,
|
|
57
|
+
ringDash: `${(RING_CIRCUMFERENCE * primaryPercent / 100).toFixed(2)} ${RING_CIRCUMFERENCE.toFixed(2)}`
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function padTwo(n) {
|
|
61
|
+
return String(n).padStart(2, "0");
|
|
62
|
+
}
|
|
63
|
+
const COUNTDOWN_FORMATTERS = {
|
|
64
|
+
zh: {
|
|
65
|
+
soon: () => "即将重置",
|
|
66
|
+
minutes: (m, clock) => `${m} 分钟后重置 (${clock})`,
|
|
67
|
+
hours: (h, m) => m > 0 ? `${h} 小时 ${m} 分钟后重置` : `${h} 小时后重置`,
|
|
68
|
+
days: (d, h) => h > 0 ? `${d} 天 ${h} 小时后重置` : `${d} 天后重置`
|
|
69
|
+
},
|
|
70
|
+
en: {
|
|
71
|
+
soon: () => "Resetting soon",
|
|
72
|
+
minutes: (m, clock) => `Resets in ${m}m (${clock})`,
|
|
73
|
+
hours: (h, m) => m > 0 ? `Resets in ${h}h ${m}m` : `Resets in ${h}h`,
|
|
74
|
+
days: (d, h) => h > 0 ? `Resets in ${d}d ${h}h` : `Resets in ${d}d`
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
function formatResetCountdown(resetAtSec, nowMs, locale) {
|
|
78
|
+
if (typeof resetAtSec !== "number" || !Number.isFinite(resetAtSec) || resetAtSec <= 0) return "";
|
|
79
|
+
const fmt = COUNTDOWN_FORMATTERS[locale];
|
|
80
|
+
const diffSec = Math.max(0, Math.round(resetAtSec - nowMs / 1e3));
|
|
81
|
+
if (diffSec === 0) return fmt.soon();
|
|
82
|
+
const totalMinutes = Math.max(1, Math.ceil(diffSec / 60));
|
|
83
|
+
if (totalMinutes < 60) {
|
|
84
|
+
const resetDate = new Date(resetAtSec * 1e3);
|
|
85
|
+
const clock = `${padTwo(resetDate.getHours())}:${padTwo(resetDate.getMinutes())}`;
|
|
86
|
+
return fmt.minutes(totalMinutes, clock);
|
|
87
|
+
}
|
|
88
|
+
const totalHours = Math.floor(totalMinutes / 60);
|
|
89
|
+
const remMinutes = totalMinutes % 60;
|
|
90
|
+
if (totalHours < 24) return fmt.hours(totalHours, remMinutes);
|
|
91
|
+
const days = Math.floor(totalHours / 24);
|
|
92
|
+
const remHours = totalHours % 24;
|
|
93
|
+
return fmt.days(days, remHours);
|
|
94
|
+
}
|
|
95
|
+
function createQuotaStore(options = {}) {
|
|
96
|
+
const fetchFn = options.fetchImpl ?? globalThis.fetch;
|
|
97
|
+
const nowFn = options.now ?? Date.now;
|
|
98
|
+
const ttlMs = options.ttlMs ?? 6e4;
|
|
99
|
+
let state = {
|
|
100
|
+
quota: null,
|
|
101
|
+
loading: false,
|
|
102
|
+
error: null
|
|
103
|
+
};
|
|
104
|
+
let inflight = null;
|
|
105
|
+
let inflightForce = false;
|
|
106
|
+
let requestSeq = 0;
|
|
107
|
+
const listeners = new Set();
|
|
108
|
+
function setState(next) {
|
|
109
|
+
state = next;
|
|
110
|
+
for (const listener of listeners) listener();
|
|
111
|
+
}
|
|
112
|
+
async function refresh(force = false) {
|
|
113
|
+
if (!force && state.quota !== null && nowFn() - state.quota.fetchedAt < ttlMs) return;
|
|
114
|
+
if (inflight !== null && (!force || inflightForce)) return inflight;
|
|
115
|
+
const seq = ++requestSeq;
|
|
116
|
+
inflightForce = force;
|
|
117
|
+
setState({
|
|
118
|
+
...state,
|
|
119
|
+
loading: true,
|
|
120
|
+
error: null
|
|
121
|
+
});
|
|
122
|
+
const url = force ? `${USAGE_ROUTE_PATH}?force=1` : USAGE_ROUTE_PATH;
|
|
123
|
+
const task = (async () => {
|
|
124
|
+
try {
|
|
125
|
+
const response = await fetchFn(url, {
|
|
126
|
+
method: "GET",
|
|
127
|
+
headers: { Accept: "application/json" }
|
|
128
|
+
});
|
|
129
|
+
if (seq !== requestSeq) return;
|
|
130
|
+
if (!response.ok) {
|
|
131
|
+
setState({
|
|
132
|
+
quota: state.quota,
|
|
133
|
+
loading: false,
|
|
134
|
+
error: `HTTP ${response.status}`
|
|
135
|
+
});
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const payload = await response.json();
|
|
139
|
+
if (seq !== requestSeq) return;
|
|
140
|
+
if (payload && payload.ok === true && payload.quota) setState({
|
|
141
|
+
quota: payload.quota,
|
|
142
|
+
loading: false,
|
|
143
|
+
error: null
|
|
144
|
+
});
|
|
145
|
+
else {
|
|
146
|
+
const fallbackQuota = payload && !payload.ok && payload.quota ? payload.quota : state.quota;
|
|
147
|
+
const errMsg = payload && !payload.ok && typeof payload.error === "string" ? payload.error : "Invalid response";
|
|
148
|
+
setState({
|
|
149
|
+
quota: fallbackQuota ?? null,
|
|
150
|
+
loading: false,
|
|
151
|
+
error: errMsg
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (seq !== requestSeq) return;
|
|
156
|
+
setState({
|
|
157
|
+
quota: state.quota,
|
|
158
|
+
loading: false,
|
|
159
|
+
error: safeErrorMessage(error)
|
|
160
|
+
});
|
|
161
|
+
} finally {
|
|
162
|
+
if (seq === requestSeq) {
|
|
163
|
+
inflight = null;
|
|
164
|
+
inflightForce = false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
})();
|
|
168
|
+
inflight = task;
|
|
169
|
+
return task;
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
getSnapshot: () => state,
|
|
173
|
+
subscribe(listener) {
|
|
174
|
+
listeners.add(listener);
|
|
175
|
+
return () => {
|
|
176
|
+
listeners.delete(listener);
|
|
177
|
+
};
|
|
178
|
+
},
|
|
179
|
+
refresh
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
//#endregion
|
|
183
|
+
//#region src/client/styles.ts
|
|
184
|
+
const STYLESHEET = [
|
|
185
|
+
".dsh-codex-quota-root{position:relative;display:inline-flex;align-items:center;flex:none}",
|
|
186
|
+
".dsh-codex-quota-trigger{height:28px;padding:0 8px;border-radius:var(--dsw-radius-sm,6px);border:none;background:transparent;color:var(--dsw-alias-label-secondary);font-family:inherit;font-size:12.5px;font-variant-numeric:tabular-nums;line-height:20px;white-space:nowrap;cursor:pointer;display:inline-flex;align-items:center;gap:5px;transition:background-color .12s,color .12s}",
|
|
187
|
+
".dsh-codex-quota-trigger:hover,.dsh-codex-quota-trigger[aria-expanded=\"true\"]{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}",
|
|
188
|
+
".dsh-codex-quota-trigger[data-level=\"warn\"]{color:var(--dsw-alias-state-warn-primary)}",
|
|
189
|
+
".dsh-codex-quota-trigger[data-level=\"error\"]{color:var(--dsw-alias-state-error-primary)}",
|
|
190
|
+
"[data-model-compact] .dsh-codex-quota-text{display:none}",
|
|
191
|
+
".dsh-codex-quota-ring-track{fill:none;stroke:var(--dsw-alias-border-l2);stroke-width:2px}",
|
|
192
|
+
".dsh-codex-quota-ring-fill{fill:none;stroke:var(--dsw-alias-brand-primary);stroke-width:2px;stroke-linecap:round}",
|
|
193
|
+
".dsh-codex-quota-trigger[data-level=\"warn\"] .dsh-codex-quota-ring-fill{stroke:var(--dsw-alias-state-warn-primary)}",
|
|
194
|
+
".dsh-codex-quota-trigger[data-level=\"error\"] .dsh-codex-quota-ring-fill{stroke:var(--dsw-alias-state-error-primary)}",
|
|
195
|
+
".dsh-codex-quota-popover{position:absolute;bottom:calc(100% + 10px);right:0;z-index:1200;width:280px;box-sizing:border-box;border-radius:var(--dsw-radius-lg,12px);background:var(--dsw-specific-menu,var(--dsw-alias-bg-overlay));backdrop-filter:var(--dsw-menu-backdrop-filter,blur(16px));box-shadow:var(--dsw-elevation-prominent,0 12px 32px rgba(0,0,0,0.16));border:1px solid var(--dsw-alias-border-l1);padding:12px 14px;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;cursor:default;user-select:none}",
|
|
196
|
+
".dsh-codex-quota-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:10px}",
|
|
197
|
+
".dsh-codex-quota-title{display:inline-flex;align-items:center;gap:6px;font-size:12.5px;font-weight:600;color:var(--dsw-alias-label-primary)}",
|
|
198
|
+
".dsh-codex-quota-plan{font-size:10px;font-weight:600;text-transform:uppercase;padding:1px 6px;border-radius:4px;background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-brand-primary)}",
|
|
199
|
+
".dsh-codex-quota-refresh{border:none;background:transparent;color:var(--dsw-alias-label-tertiary);font-family:inherit;font-size:11.5px;padding:2px 6px;border-radius:4px;cursor:pointer;display:inline-flex;align-items:center;gap:4px}",
|
|
200
|
+
".dsh-codex-quota-refresh:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}",
|
|
201
|
+
".dsh-codex-quota-refresh:disabled{opacity:0.55;cursor:default}",
|
|
202
|
+
".dsh-codex-quota-section{margin-bottom:10px}",
|
|
203
|
+
".dsh-codex-quota-section:last-of-type{margin-bottom:4px}",
|
|
204
|
+
".dsh-codex-quota-row{display:flex;align-items:baseline;justify-content:space-between;font-variant-numeric:tabular-nums}",
|
|
205
|
+
".dsh-codex-quota-label{color:var(--dsw-alias-label-primary);font-weight:500}",
|
|
206
|
+
".dsh-codex-quota-pct{color:var(--dsw-alias-label-primary);font-weight:600}",
|
|
207
|
+
".dsh-codex-quota-bar{height:5px;border-radius:999px;background:var(--dsw-alias-interactive-bg-hover);margin:5px 0 4px;overflow:hidden}",
|
|
208
|
+
".dsh-codex-quota-bar-fill{height:100%;border-radius:999px;background:var(--dsw-alias-brand-primary);transition:width .2s ease}",
|
|
209
|
+
".dsh-codex-quota-bar-fill[data-level=\"warn\"]{background:var(--dsw-alias-state-warn-primary)}",
|
|
210
|
+
".dsh-codex-quota-bar-fill[data-level=\"error\"]{background:var(--dsw-alias-state-error-primary)}",
|
|
211
|
+
".dsh-codex-quota-meta{display:flex;justify-content:space-between;gap:8px;font-size:11px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}",
|
|
212
|
+
".dsh-codex-quota-error{margin-top:6px;padding:4px 6px;border-radius:4px;background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-state-error-primary);font-size:11px;word-break:break-word}"
|
|
213
|
+
].join("");
|
|
214
|
+
//#endregion
|
|
215
|
+
//#region src/client/index.ts
|
|
216
|
+
const React = require("react");
|
|
217
|
+
const h = React.createElement;
|
|
218
|
+
const NS = "codex-subscription";
|
|
219
|
+
const SLOT_NAME = "conversation.input.right";
|
|
220
|
+
const SLOT_ENTRY_ID = "codex-subscription:quota";
|
|
221
|
+
const zh = {
|
|
222
|
+
"quota.title": "Codex 订阅限额",
|
|
223
|
+
"quota.loading": "正在获取 Codex 限额…",
|
|
224
|
+
"quota.refresh": "↻ 刷新",
|
|
225
|
+
"quota.refreshing": "↻ 刷新中…",
|
|
226
|
+
"quota.window5h": "5 小时窗口",
|
|
227
|
+
"quota.window7d": "7 天窗口",
|
|
228
|
+
"quota.usedPrefix": "已用 ",
|
|
229
|
+
"quota.remainPrefix": "剩余 "
|
|
230
|
+
};
|
|
231
|
+
const en = {
|
|
232
|
+
"quota.title": "Codex Subscription Quota",
|
|
233
|
+
"quota.loading": "Loading Codex quota…",
|
|
234
|
+
"quota.refresh": "↻ Refresh",
|
|
235
|
+
"quota.refreshing": "↻ Refreshing…",
|
|
236
|
+
"quota.window5h": "5-hour window",
|
|
237
|
+
"quota.window7d": "7-day window",
|
|
238
|
+
"quota.usedPrefix": "Used ",
|
|
239
|
+
"quota.remainPrefix": "Remaining "
|
|
240
|
+
};
|
|
241
|
+
const EMPTY_MODEL_STORE = {
|
|
242
|
+
getSnapshot: () => null,
|
|
243
|
+
subscribe: () => () => {}
|
|
244
|
+
};
|
|
245
|
+
function CodexQuotaPill(props) {
|
|
246
|
+
const { sessionId, quotaStore, getModelStore, useProjection, useSession, t, getLocale } = props;
|
|
247
|
+
const modelStore = getModelStore(sessionId);
|
|
248
|
+
const directorySnap = React.useSyncExternalStore(modelStore.subscribe, modelStore.getSnapshot);
|
|
249
|
+
const projectedSelection = typeof useProjection === "function" ? useProjection("modelSelection") : void 0;
|
|
250
|
+
const running = typeof useSession === "function" ? Boolean(useSession((s) => s?.running)) : false;
|
|
251
|
+
const isCodex = isCodexProvider(resolveActiveProvider(directorySnap, projectedSelection));
|
|
252
|
+
const quotaSnap = React.useSyncExternalStore(quotaStore.subscribe, quotaStore.getSnapshot);
|
|
253
|
+
const [open, setOpen] = React.useState(false);
|
|
254
|
+
const rootRef = React.useRef(null);
|
|
255
|
+
const prevRunningRef = React.useRef(running);
|
|
256
|
+
const prevSessionIdRef = React.useRef(sessionId);
|
|
257
|
+
React.useEffect(() => {
|
|
258
|
+
const sameSession = prevSessionIdRef.current === sessionId;
|
|
259
|
+
prevSessionIdRef.current = sessionId;
|
|
260
|
+
if (!isCodex) {
|
|
261
|
+
setOpen(false);
|
|
262
|
+
prevRunningRef.current = running;
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
quotaStore.refresh(false);
|
|
266
|
+
if (sameSession && didTurnFinish(prevRunningRef.current, running)) quotaStore.refresh(true);
|
|
267
|
+
prevRunningRef.current = running;
|
|
268
|
+
}, [
|
|
269
|
+
isCodex,
|
|
270
|
+
running,
|
|
271
|
+
sessionId,
|
|
272
|
+
quotaStore
|
|
273
|
+
]);
|
|
274
|
+
React.useEffect(() => {
|
|
275
|
+
if (!open || !isCodex || typeof globalThis.document === "undefined") return;
|
|
276
|
+
const onPointerDown = (event) => {
|
|
277
|
+
if (rootRef.current && typeof rootRef.current.contains === "function") {
|
|
278
|
+
if (!rootRef.current.contains(event.target)) setOpen(false);
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
const onKeyDown = (event) => {
|
|
282
|
+
if (event.key === "Escape") setOpen(false);
|
|
283
|
+
};
|
|
284
|
+
globalThis.document.addEventListener("pointerdown", onPointerDown);
|
|
285
|
+
globalThis.document.addEventListener("keydown", onKeyDown);
|
|
286
|
+
return () => {
|
|
287
|
+
globalThis.document.removeEventListener("pointerdown", onPointerDown);
|
|
288
|
+
globalThis.document.removeEventListener("keydown", onKeyDown);
|
|
289
|
+
};
|
|
290
|
+
}, [open, isCodex]);
|
|
291
|
+
if (!isCodex) return null;
|
|
292
|
+
const summary = quotaSnap.quota !== null ? formatQuotaSummary(quotaSnap.quota) : null;
|
|
293
|
+
const level = summary?.level ?? "ok";
|
|
294
|
+
const pillText = summary !== null ? summary.pillText : quotaSnap.loading ? "Codex …" : "Codex --";
|
|
295
|
+
const ringDash = summary?.ringDash ?? "0 34.56";
|
|
296
|
+
const locale = getLocale();
|
|
297
|
+
const nowMs = Date.now();
|
|
298
|
+
const renderWindowSection = (title, win, windowLimitReached) => {
|
|
299
|
+
const usedPercent = clampPercent(win.usedPercent);
|
|
300
|
+
const winLevel = windowLevelOf(usedPercent, windowLimitReached);
|
|
301
|
+
const remainPercent = 100 - usedPercent;
|
|
302
|
+
const countdown = formatResetCountdown(win.resetAt, nowMs, locale);
|
|
303
|
+
return h("div", { className: "dsh-codex-quota-section" }, h("div", { className: "dsh-codex-quota-row" }, h("span", { className: "dsh-codex-quota-label" }, title), h("span", { className: "dsh-codex-quota-pct" }, `${t("quota.usedPrefix")}${usedPercent}%`)), h("div", { className: "dsh-codex-quota-bar" }, h("div", {
|
|
304
|
+
className: "dsh-codex-quota-bar-fill",
|
|
305
|
+
"data-level": winLevel,
|
|
306
|
+
style: { width: `${usedPercent}%` }
|
|
307
|
+
})), h("div", { className: "dsh-codex-quota-meta" }, h("span", null, `${t("quota.remainPrefix")}${remainPercent}%`), countdown !== "" ? h("span", null, countdown) : null));
|
|
308
|
+
};
|
|
309
|
+
const quota = quotaSnap.quota;
|
|
310
|
+
const secondaryReached = Boolean(quota?.secondary && quota.secondary.usedPercent >= 100);
|
|
311
|
+
const primaryReached = Boolean(quota?.limitReached && !secondaryReached);
|
|
312
|
+
return h("span", {
|
|
313
|
+
ref: rootRef,
|
|
314
|
+
className: "dsh-codex-quota-root",
|
|
315
|
+
"data-composer-overlay": true
|
|
316
|
+
}, h("button", {
|
|
317
|
+
type: "button",
|
|
318
|
+
className: "dsh-codex-quota-trigger",
|
|
319
|
+
"data-level": level,
|
|
320
|
+
"aria-label": `${t("quota.title")}: ${pillText}`,
|
|
321
|
+
"aria-haspopup": "dialog",
|
|
322
|
+
"aria-expanded": open,
|
|
323
|
+
title: `${t("quota.title")} (${pillText})`,
|
|
324
|
+
onClick: () => {
|
|
325
|
+
const nextOpen = !open;
|
|
326
|
+
setOpen(nextOpen);
|
|
327
|
+
if (nextOpen) quotaStore.refresh(false);
|
|
328
|
+
}
|
|
329
|
+
}, h("svg", {
|
|
330
|
+
viewBox: "0 0 14 14",
|
|
331
|
+
width: 14,
|
|
332
|
+
height: 14,
|
|
333
|
+
"aria-hidden": true
|
|
334
|
+
}, h("circle", {
|
|
335
|
+
className: "dsh-codex-quota-ring-track",
|
|
336
|
+
cx: 7,
|
|
337
|
+
cy: 7,
|
|
338
|
+
r: 5.5
|
|
339
|
+
}), h("circle", {
|
|
340
|
+
className: "dsh-codex-quota-ring-fill",
|
|
341
|
+
cx: 7,
|
|
342
|
+
cy: 7,
|
|
343
|
+
r: 5.5,
|
|
344
|
+
strokeDasharray: ringDash,
|
|
345
|
+
transform: "rotate(-90 7 7)"
|
|
346
|
+
})), h("span", { className: "dsh-codex-quota-text" }, pillText)), open ? h("div", {
|
|
347
|
+
className: "dsh-codex-quota-popover",
|
|
348
|
+
role: "dialog",
|
|
349
|
+
"aria-label": t("quota.title"),
|
|
350
|
+
onMouseDown: (e) => {
|
|
351
|
+
e.stopPropagation?.();
|
|
352
|
+
}
|
|
353
|
+
}, h("div", { className: "dsh-codex-quota-head" }, h("span", { className: "dsh-codex-quota-title" }, t("quota.title"), quota?.planType ? h("span", { className: "dsh-codex-quota-plan" }, quota.planType) : null), h("button", {
|
|
354
|
+
type: "button",
|
|
355
|
+
className: "dsh-codex-quota-refresh",
|
|
356
|
+
disabled: quotaSnap.loading,
|
|
357
|
+
onClick: () => {
|
|
358
|
+
quotaStore.refresh(true);
|
|
359
|
+
}
|
|
360
|
+
}, quotaSnap.loading ? t("quota.refreshing") : t("quota.refresh"))), quota !== null ? renderWindowSection(t("quota.window5h"), 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(t("quota.window7d"), quota.secondary, secondaryReached) : null, quotaSnap.error !== null ? h("div", { className: "dsh-codex-quota-error" }, quotaSnap.error) : null) : null);
|
|
361
|
+
}
|
|
362
|
+
const inject = ["slots", "locale"];
|
|
363
|
+
function apply(ctx) {
|
|
364
|
+
ctx.effect(() => {
|
|
365
|
+
if (typeof globalThis.document === "undefined") return;
|
|
366
|
+
const tag = globalThis.document.createElement("style");
|
|
367
|
+
tag.dataset.pluginCss = "@lyogvce/dsh-codex-subscription";
|
|
368
|
+
tag.textContent = STYLESHEET;
|
|
369
|
+
globalThis.document.head.appendChild(tag);
|
|
370
|
+
return () => {
|
|
371
|
+
tag.remove();
|
|
372
|
+
};
|
|
373
|
+
}, "codex-subscription: stylesheet");
|
|
374
|
+
ctx.effect(() => ctx.locale.register(NS, {
|
|
375
|
+
zh,
|
|
376
|
+
en
|
|
377
|
+
}), "codex-subscription: dictionaries");
|
|
378
|
+
const t = ctx.locale.bind(NS);
|
|
379
|
+
const quotaStore = createQuotaStore();
|
|
380
|
+
const getLocale = () => {
|
|
381
|
+
return (ctx.locale.getLocale?.()?.id ?? "").toLowerCase().startsWith("en") ? "en" : "zh";
|
|
382
|
+
};
|
|
383
|
+
const getModelStore = (sessionId) => {
|
|
384
|
+
if (typeof sessionId !== "string" || sessionId === "") return EMPTY_MODEL_STORE;
|
|
385
|
+
const models = ctx.get("modelDirectories");
|
|
386
|
+
if (!models || typeof models.directoryFor !== "function") return EMPTY_MODEL_STORE;
|
|
387
|
+
try {
|
|
388
|
+
return models.directoryFor(sessionId)?.store ?? EMPTY_MODEL_STORE;
|
|
389
|
+
} catch {
|
|
390
|
+
return EMPTY_MODEL_STORE;
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
ctx.effect(() => ctx.slots.inject(SLOT_NAME, () => ctx.slots.register({
|
|
394
|
+
name: SLOT_NAME,
|
|
395
|
+
id: SLOT_ENTRY_ID,
|
|
396
|
+
order: 10,
|
|
397
|
+
locale: NS,
|
|
398
|
+
inject: (sessionId) => ({
|
|
399
|
+
sessionId,
|
|
400
|
+
quotaStore,
|
|
401
|
+
getModelStore,
|
|
402
|
+
t,
|
|
403
|
+
getLocale
|
|
404
|
+
})
|
|
405
|
+
}, CodexQuotaPill)), "codex-subscription: quota pill");
|
|
406
|
+
}
|
|
407
|
+
const __test = Object.freeze({
|
|
408
|
+
PROVIDER_ID: PROVIDER,
|
|
409
|
+
USAGE_API_PATH: USAGE_ROUTE_PATH,
|
|
410
|
+
resolveActiveProvider,
|
|
411
|
+
isCodexProvider,
|
|
412
|
+
didTurnFinish,
|
|
413
|
+
windowLevelOf,
|
|
414
|
+
formatQuotaSummary,
|
|
415
|
+
formatResetCountdown,
|
|
416
|
+
createQuotaStore,
|
|
417
|
+
CodexQuotaPill
|
|
418
|
+
});
|
|
419
|
+
//#endregion
|
|
420
|
+
exports.CodexQuotaPill = CodexQuotaPill;
|
|
421
|
+
exports.NS = NS;
|
|
422
|
+
exports.__test = __test;
|
|
423
|
+
exports.apply = apply;
|
|
424
|
+
exports.en = en;
|
|
425
|
+
exports.inject = inject;
|
|
426
|
+
exports.zh = zh;
|
|
427
|
+
return module.exports;
|
|
428
|
+
}
|
|
429
|
+
});
|
package/lib/index.d.ts
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import { PiCodexModel } from "@earendil-works/pi-ai/providers/openai-codex";
|
|
2
2
|
//#region src/auth.d.ts
|
|
3
|
+
interface CodexAuthTokens {
|
|
4
|
+
access_token?: string;
|
|
5
|
+
refresh_token?: string;
|
|
6
|
+
id_token?: string;
|
|
7
|
+
account_id?: string;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
}
|
|
10
|
+
interface CodexAuthFile {
|
|
11
|
+
tokens?: CodexAuthTokens;
|
|
12
|
+
last_refresh?: string;
|
|
13
|
+
[key: string]: unknown;
|
|
14
|
+
}
|
|
3
15
|
interface JwtClaims {
|
|
4
16
|
exp?: number;
|
|
5
17
|
[key: string]: unknown;
|
|
@@ -11,7 +23,13 @@ interface AuthManagerOptions {
|
|
|
11
23
|
}
|
|
12
24
|
declare function resolveCodexHome(envCodexHome?: string | undefined): string;
|
|
13
25
|
declare function decodeJwtPayload(token: unknown): JwtClaims | undefined;
|
|
26
|
+
declare function extractAccountId(parsed: CodexAuthFile | undefined, accessToken: string): string | undefined;
|
|
27
|
+
interface CodexResolvedCredentials {
|
|
28
|
+
readonly accessToken: string;
|
|
29
|
+
readonly accountId?: string | undefined;
|
|
30
|
+
}
|
|
14
31
|
declare function createAuthManager(options: AuthManagerOptions): {
|
|
32
|
+
resolveCredentials: () => Promise<CodexResolvedCredentials>;
|
|
15
33
|
resolveAccessToken: () => Promise<string>;
|
|
16
34
|
};
|
|
17
35
|
//#endregion
|
|
@@ -39,6 +57,7 @@ interface RawModelsCache {
|
|
|
39
57
|
models?: RawCodexModelEntry[];
|
|
40
58
|
}
|
|
41
59
|
interface CodexModelMetadata {
|
|
60
|
+
readonly contextWindow: number;
|
|
42
61
|
readonly supportVerbosity: boolean;
|
|
43
62
|
readonly defaultVerbosity: 'low' | 'medium' | 'high';
|
|
44
63
|
readonly defaultReasoningLevel?: string | undefined;
|
|
@@ -49,13 +68,47 @@ interface ParsedModelsCatalog {
|
|
|
49
68
|
readonly metadataById: ReadonlyMap<string, CodexModelMetadata>;
|
|
50
69
|
}
|
|
51
70
|
declare function buildThinkingLevelMap(supportedLevels: unknown): Record<string, string | null>;
|
|
52
|
-
declare function parseModelsCache(rawCache: RawModelsCache | null | undefined, baseModels: readonly PiCodexModel[], disabledModels?: readonly string[]): ParsedModelsCatalog;
|
|
71
|
+
declare function parseModelsCache(rawCache: RawModelsCache | null | undefined, baseModels: readonly PiCodexModel[], disabledModels?: readonly string[], contextWindows?: Readonly<Record<string, number>>): ParsedModelsCatalog;
|
|
53
72
|
//#endregion
|
|
54
73
|
//#region src/request.d.ts
|
|
55
74
|
declare function buildCodexUserAgent(version?: string): string;
|
|
56
75
|
declare function buildCodexHeaders(existingHeaders: Record<string, string> | undefined, clientVersion: string, sessionId?: string | undefined): Record<string, string>;
|
|
57
76
|
declare function sanitizeCodexPayload(payload: Record<string, unknown> | null | undefined, modelMeta?: CodexModelMetadata | undefined): Record<string, unknown> | null | undefined;
|
|
58
77
|
//#endregion
|
|
78
|
+
//#region src/usage.d.ts
|
|
79
|
+
declare function safeErrorMessage(error: unknown): string;
|
|
80
|
+
interface CodexQuotaWindow {
|
|
81
|
+
readonly usedPercent: number;
|
|
82
|
+
readonly resetAt: number | null;
|
|
83
|
+
}
|
|
84
|
+
interface CodexQuotaSnapshot {
|
|
85
|
+
readonly planType: string;
|
|
86
|
+
readonly limitReached: boolean;
|
|
87
|
+
readonly primary: CodexQuotaWindow;
|
|
88
|
+
readonly secondary: CodexQuotaWindow | null;
|
|
89
|
+
readonly fetchedAt: number;
|
|
90
|
+
}
|
|
91
|
+
type CodexQuotaResponse = {
|
|
92
|
+
readonly ok: true;
|
|
93
|
+
readonly quota: CodexQuotaSnapshot;
|
|
94
|
+
} | {
|
|
95
|
+
readonly ok: false;
|
|
96
|
+
readonly error: string;
|
|
97
|
+
readonly quota?: CodexQuotaSnapshot | undefined;
|
|
98
|
+
};
|
|
99
|
+
interface UsageFetcherOptions {
|
|
100
|
+
readonly resolveCredentials: () => Promise<CodexResolvedCredentials>;
|
|
101
|
+
readonly buildHeaders: (authHeaders: Record<string, string>) => Record<string, string>;
|
|
102
|
+
readonly fetchImpl?: typeof fetch | undefined;
|
|
103
|
+
readonly now?: (() => number) | undefined;
|
|
104
|
+
readonly ttlMs?: number | undefined;
|
|
105
|
+
}
|
|
106
|
+
declare function parseUsagePayload(raw: unknown, fetchedAt: number): CodexQuotaSnapshot | null;
|
|
107
|
+
declare function createUsageFetcher(options: UsageFetcherOptions): {
|
|
108
|
+
getUsage: (force?: boolean) => Promise<CodexQuotaResponse>;
|
|
109
|
+
invalidate: () => void;
|
|
110
|
+
};
|
|
111
|
+
//#endregion
|
|
59
112
|
//#region src/host-types.d.ts
|
|
60
113
|
interface LlmServiceLike {
|
|
61
114
|
registerAdapter: (providers: string[], adapter: unknown) => (() => void) & {
|
|
@@ -67,6 +120,8 @@ interface PluginContextLike {
|
|
|
67
120
|
llm: LlmServiceLike;
|
|
68
121
|
get?: (name: string) => unknown;
|
|
69
122
|
on?: (event: string, listener: () => void) => () => void;
|
|
123
|
+
effect?: (factory: () => void | (() => void), label?: string) => () => void;
|
|
124
|
+
inject?: (services: readonly string[], callback: (ctx: PluginContextLike) => void) => () => void;
|
|
70
125
|
}
|
|
71
126
|
//#endregion
|
|
72
127
|
//#region src/index.d.ts
|
|
@@ -78,14 +133,22 @@ export declare const __test: Readonly<{
|
|
|
78
133
|
PROVIDER: "openai-codex";
|
|
79
134
|
DISPLAY_NAME: "Codex";
|
|
80
135
|
DEFAULT_DISABLED_MODELS: readonly string[];
|
|
136
|
+
DEFAULT_CONTEXT_WINDOW: 872000;
|
|
137
|
+
CODEX_USAGE_URL: "https://chatgpt.com/backend-api/wham/usage";
|
|
138
|
+
USAGE_ROUTE_PATH: "/api/codex-subscription/usage";
|
|
139
|
+
DEFAULT_USAGE_TTL_MS: 60000;
|
|
81
140
|
resolveCodexHome: typeof resolveCodexHome;
|
|
82
141
|
decodeJwtPayload: typeof decodeJwtPayload;
|
|
142
|
+
extractAccountId: typeof extractAccountId;
|
|
143
|
+
safeErrorMessage: typeof safeErrorMessage;
|
|
83
144
|
createAuthManager: typeof createAuthManager;
|
|
84
145
|
buildThinkingLevelMap: typeof buildThinkingLevelMap;
|
|
85
146
|
parseModelsCache: typeof parseModelsCache;
|
|
86
147
|
sanitizeCodexPayload: typeof sanitizeCodexPayload;
|
|
87
148
|
buildCodexHeaders: typeof buildCodexHeaders;
|
|
88
149
|
buildCodexUserAgent: typeof buildCodexUserAgent;
|
|
150
|
+
parseUsagePayload: typeof parseUsagePayload;
|
|
151
|
+
createUsageFetcher: typeof createUsageFetcher;
|
|
89
152
|
}>;
|
|
90
153
|
//#endregion
|
|
91
|
-
export type { CodexModelMetadata, ParsedModelsCatalog, PluginContextLike, RawModelsCache };
|
|
154
|
+
export type { CodexModelMetadata, CodexQuotaResponse, CodexQuotaSnapshot, CodexQuotaWindow, CodexResolvedCredentials, ParsedModelsCatalog, PluginContextLike, RawModelsCache };
|
package/lib/index.js
CHANGED
|
@@ -1,13 +1,118 @@
|
|
|
1
|
-
import { readFileSync, realpathSync, statSync } from "node:fs";
|
|
1
|
+
import { chmodSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
3
|
import z from "@deepseek-ai/schemastery";
|
|
4
|
-
import { LlmError, resolveImageAttachmentAccess } from "@deepseek-ai/dsh-llm";
|
|
4
|
+
import { LlmError, resolveImageAttachmentAccess, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
|
|
5
5
|
import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
|
|
6
6
|
import { openaiCodexProvider } from "@earendil-works/pi-ai/providers/openai-codex";
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
8
8
|
import { chmod, rename, writeFile } from "node:fs/promises";
|
|
9
9
|
import { arch, homedir, platform, release } from "node:os";
|
|
10
10
|
import { execFileSync } from "node:child_process";
|
|
11
|
+
//#region src/usage.ts
|
|
12
|
+
function safeErrorMessage(error) {
|
|
13
|
+
return (error instanceof Error ? error.message : String(error)).replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/gu, "[redacted]").replace(/\b(Bearer\s+)[^\s,;'"&]+/giu, "$1[redacted]").replace(/(\b(?:code|token|access_token|refresh_token|id_token)=)[^&\s"']+/giu, "$1[redacted]").replace(/("(?:access_token|refresh_token|id_token)"\s*:\s*")[^"]+"/giu, "$1[redacted]\"").slice(0, 1e3);
|
|
14
|
+
}
|
|
15
|
+
const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
16
|
+
const USAGE_ROUTE_PATH = "/api/codex-subscription/usage";
|
|
17
|
+
const DEFAULT_USAGE_TTL_MS = 6e4;
|
|
18
|
+
const USAGE_FETCH_TIMEOUT_MS = 15e3;
|
|
19
|
+
function clampPercent(value) {
|
|
20
|
+
return Math.max(0, Math.min(100, Math.round(value)));
|
|
21
|
+
}
|
|
22
|
+
function parseWindow(raw, fetchedAtMs) {
|
|
23
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
24
|
+
const record = raw;
|
|
25
|
+
const usedRaw = record.used_percent;
|
|
26
|
+
if (typeof usedRaw !== "number" || !Number.isFinite(usedRaw)) return null;
|
|
27
|
+
const resetAtRaw = record.reset_at;
|
|
28
|
+
const resetAfterRaw = record.reset_after_seconds;
|
|
29
|
+
let resetAt = null;
|
|
30
|
+
if (typeof resetAtRaw === "number" && Number.isFinite(resetAtRaw) && resetAtRaw > 0) resetAt = Math.round(resetAtRaw);
|
|
31
|
+
else if (typeof resetAfterRaw === "number" && Number.isFinite(resetAfterRaw) && resetAfterRaw >= 0) resetAt = Math.floor(fetchedAtMs / 1e3) + Math.round(resetAfterRaw);
|
|
32
|
+
return {
|
|
33
|
+
usedPercent: clampPercent(usedRaw),
|
|
34
|
+
resetAt
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function parseUsagePayload(raw, fetchedAt) {
|
|
38
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
39
|
+
const record = raw;
|
|
40
|
+
const rateLimit = record.rate_limit;
|
|
41
|
+
if (!rateLimit || typeof rateLimit !== "object" || Array.isArray(rateLimit)) return null;
|
|
42
|
+
const rateRecord = rateLimit;
|
|
43
|
+
const primary = parseWindow(rateRecord.primary_window, fetchedAt);
|
|
44
|
+
if (primary === null) return null;
|
|
45
|
+
const secondary = parseWindow(rateRecord.secondary_window, fetchedAt);
|
|
46
|
+
return {
|
|
47
|
+
planType: typeof record.plan_type === "string" && record.plan_type.trim() !== "" ? record.plan_type.trim() : "plus",
|
|
48
|
+
limitReached: rateRecord.limit_reached === true || rateRecord.allowed === false || primary.usedPercent >= 100 || secondary !== null && secondary.usedPercent >= 100,
|
|
49
|
+
primary,
|
|
50
|
+
secondary,
|
|
51
|
+
fetchedAt
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function createUsageFetcher(options) {
|
|
55
|
+
const fetchFn = options.fetchImpl ?? globalThis.fetch;
|
|
56
|
+
const nowFn = options.now ?? Date.now;
|
|
57
|
+
const ttlMs = options.ttlMs ?? 6e4;
|
|
58
|
+
let cached;
|
|
59
|
+
let stale = false;
|
|
60
|
+
let inflight;
|
|
61
|
+
async function fetchFresh() {
|
|
62
|
+
try {
|
|
63
|
+
const creds = await options.resolveCredentials();
|
|
64
|
+
const headers = options.buildHeaders({
|
|
65
|
+
Authorization: `Bearer ${creds.accessToken}`,
|
|
66
|
+
Accept: "application/json",
|
|
67
|
+
...creds.accountId !== void 0 ? { "ChatGPT-Account-Id": creds.accountId } : {}
|
|
68
|
+
});
|
|
69
|
+
const response = await fetchFn(CODEX_USAGE_URL, {
|
|
70
|
+
method: "GET",
|
|
71
|
+
headers,
|
|
72
|
+
signal: AbortSignal.timeout(USAGE_FETCH_TIMEOUT_MS)
|
|
73
|
+
});
|
|
74
|
+
if (!response.ok) return {
|
|
75
|
+
ok: false,
|
|
76
|
+
error: `Codex 限额查询失败 (HTTP ${response.status})`,
|
|
77
|
+
...cached !== void 0 ? { quota: cached } : {}
|
|
78
|
+
};
|
|
79
|
+
const parsed = parseUsagePayload(await response.json(), nowFn());
|
|
80
|
+
if (parsed === null) return {
|
|
81
|
+
ok: false,
|
|
82
|
+
error: "Codex 限额响应格式无法解析",
|
|
83
|
+
...cached !== void 0 ? { quota: cached } : {}
|
|
84
|
+
};
|
|
85
|
+
cached = parsed;
|
|
86
|
+
stale = false;
|
|
87
|
+
return {
|
|
88
|
+
ok: true,
|
|
89
|
+
quota: parsed
|
|
90
|
+
};
|
|
91
|
+
} catch (error) {
|
|
92
|
+
return {
|
|
93
|
+
ok: false,
|
|
94
|
+
error: safeErrorMessage(error),
|
|
95
|
+
...cached !== void 0 ? { quota: cached } : {}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
invalidate() {
|
|
101
|
+
stale = true;
|
|
102
|
+
},
|
|
103
|
+
async getUsage(force = false) {
|
|
104
|
+
if (!force && !stale && cached !== void 0 && nowFn() - cached.fetchedAt < ttlMs) return {
|
|
105
|
+
ok: true,
|
|
106
|
+
quota: cached
|
|
107
|
+
};
|
|
108
|
+
if (!inflight) inflight = fetchFresh().finally(() => {
|
|
109
|
+
inflight = void 0;
|
|
110
|
+
});
|
|
111
|
+
return inflight;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
//#endregion
|
|
11
116
|
//#region src/auth.ts
|
|
12
117
|
const CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
13
118
|
const CODEX_TOKEN_URL = "https://auth.openai.com/oauth/token";
|
|
@@ -28,6 +133,15 @@ function decodeJwtPayload(token) {
|
|
|
28
133
|
return;
|
|
29
134
|
}
|
|
30
135
|
}
|
|
136
|
+
function extractAccountId(parsed, accessToken) {
|
|
137
|
+
const fileAccountId = parsed?.tokens?.account_id;
|
|
138
|
+
if (typeof fileAccountId === "string" && fileAccountId.trim() !== "") return fileAccountId.trim();
|
|
139
|
+
const authClaim = decodeJwtPayload(accessToken)?.["https://api.openai.com/auth"];
|
|
140
|
+
if (authClaim && typeof authClaim === "object" && !Array.isArray(authClaim)) {
|
|
141
|
+
const jwtAccountId = authClaim.chatgpt_account_id;
|
|
142
|
+
if (typeof jwtAccountId === "string" && jwtAccountId.trim() !== "") return jwtAccountId.trim();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
31
145
|
async function writeJsonAtomic(filePath, data) {
|
|
32
146
|
let targetPath = filePath;
|
|
33
147
|
try {
|
|
@@ -50,11 +164,26 @@ function createAuthManager(options) {
|
|
|
50
164
|
let memoryFallback;
|
|
51
165
|
function readAuthState() {
|
|
52
166
|
const authPath = join(getCodexHome(), "auth.json");
|
|
167
|
+
if (process.platform !== "win32") try {
|
|
168
|
+
const stat = statSync(authPath);
|
|
169
|
+
if ((stat.mode & 256) === 0) {
|
|
170
|
+
if (memoryFallback?.authPath === authPath) return memoryFallback;
|
|
171
|
+
const octal = (stat.mode & 511).toString(8).padStart(4, "0");
|
|
172
|
+
throw new LlmError(`Codex 凭证文件属主不可读 (${authPath}, mode ${octal}),请执行 chmod 600 "${authPath}"`, "INVALID_CREDENTIAL");
|
|
173
|
+
}
|
|
174
|
+
if ((stat.mode & 63) !== 0) try {
|
|
175
|
+
chmodSync(realpathSync(authPath), 384);
|
|
176
|
+
} catch {}
|
|
177
|
+
} catch (error) {
|
|
178
|
+
if (error instanceof LlmError) throw error;
|
|
179
|
+
}
|
|
53
180
|
let rawText;
|
|
54
181
|
try {
|
|
55
182
|
rawText = readFileSync(authPath, "utf8");
|
|
56
183
|
} catch (error) {
|
|
57
184
|
if (memoryFallback?.authPath === authPath) return memoryFallback;
|
|
185
|
+
const code = error?.code;
|
|
186
|
+
if (code === "EACCES" || code === "EPERM") throw new LlmError(`Codex 凭证文件无读取权限 (${authPath}),请执行 chmod 600 "${authPath}"`, "INVALID_CREDENTIAL", { cause: error });
|
|
58
187
|
throw new LlmError(`无法读取 Codex 登录凭证 (${authPath}),请先在终端运行 codex login`, "MISSING_CREDENTIAL", { cause: error });
|
|
59
188
|
}
|
|
60
189
|
let parsed;
|
|
@@ -116,28 +245,44 @@ function createAuthManager(options) {
|
|
|
116
245
|
try {
|
|
117
246
|
await writeJsonAtomic(authPath, nextAuth);
|
|
118
247
|
} catch (error) {
|
|
119
|
-
console.error(`[dsh-codex-subscription] 写回刷新后的 Codex auth.json 失败 (${authPath}): ${
|
|
248
|
+
console.error(`[dsh-codex-subscription] 写回刷新后的 Codex auth.json 失败 (${authPath}): ${safeErrorMessage(error)}`);
|
|
120
249
|
}
|
|
121
|
-
return
|
|
250
|
+
return {
|
|
251
|
+
accessToken: fresh.access_token,
|
|
252
|
+
accountId: extractAccountId(nextAuth, fresh.access_token)
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
function toCredentials(state) {
|
|
256
|
+
return {
|
|
257
|
+
accessToken: state.accessToken,
|
|
258
|
+
accountId: extractAccountId(state.parsed, state.accessToken)
|
|
259
|
+
};
|
|
122
260
|
}
|
|
123
|
-
|
|
261
|
+
async function resolveCredentials() {
|
|
124
262
|
const current = readAuthState();
|
|
125
|
-
if (isTokenFresh(current.accessToken)) return current
|
|
263
|
+
if (isTokenFresh(current.accessToken)) return toCredentials(current);
|
|
126
264
|
if (!inflightRefresh) inflightRefresh = (async () => {
|
|
127
265
|
const latest = readAuthState();
|
|
128
|
-
if (isTokenFresh(latest.accessToken)) return latest
|
|
266
|
+
if (isTokenFresh(latest.accessToken)) return toCredentials(latest);
|
|
129
267
|
return refreshToken(latest.authPath, latest.parsed);
|
|
130
268
|
})().finally(() => {
|
|
131
269
|
inflightRefresh = void 0;
|
|
132
270
|
});
|
|
133
271
|
return inflightRefresh;
|
|
134
|
-
}
|
|
272
|
+
}
|
|
273
|
+
return {
|
|
274
|
+
resolveCredentials,
|
|
275
|
+
async resolveAccessToken() {
|
|
276
|
+
return (await resolveCredentials()).accessToken;
|
|
277
|
+
}
|
|
278
|
+
};
|
|
135
279
|
}
|
|
136
280
|
//#endregion
|
|
137
281
|
//#region src/catalog.ts
|
|
138
282
|
const PROVIDER = "openai-codex";
|
|
139
283
|
const DISPLAY_NAME = "Codex";
|
|
140
284
|
const DEFAULT_CLIENT_VERSION = "0.163.0";
|
|
285
|
+
const DEFAULT_CONTEXT_WINDOW = 872e3;
|
|
141
286
|
const THINKING_LEVELS = [
|
|
142
287
|
"off",
|
|
143
288
|
"minimal",
|
|
@@ -178,7 +323,10 @@ function normalizeDefaultReasoningLevel(rawLevel, thinkingLevelMap) {
|
|
|
178
323
|
function isVerbosity(value) {
|
|
179
324
|
return value === "low" || value === "medium" || value === "high";
|
|
180
325
|
}
|
|
181
|
-
function
|
|
326
|
+
function isPositiveSafeInt(value) {
|
|
327
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
328
|
+
}
|
|
329
|
+
function parseModelsCache(rawCache, baseModels, disabledModels = [], contextWindows = {}) {
|
|
182
330
|
const disabledSet = new Set((Array.isArray(disabledModels) ? disabledModels : []).filter((id) => typeof id === "string").map((id) => id.trim()).filter(Boolean));
|
|
183
331
|
const clientVersion = typeof rawCache?.client_version === "string" && rawCache.client_version.trim() !== "" ? rawCache.client_version.trim() : DEFAULT_CLIENT_VERSION;
|
|
184
332
|
const rawModels = Array.isArray(rawCache?.models) ? rawCache.models : [];
|
|
@@ -194,8 +342,11 @@ function parseModelsCache(rawCache, baseModels, disabledModels = []) {
|
|
|
194
342
|
if (seen.has(id)) continue;
|
|
195
343
|
seen.add(id);
|
|
196
344
|
const base = baseModels.find((model) => model.id === id) ?? templateModel;
|
|
197
|
-
const
|
|
198
|
-
const
|
|
345
|
+
const maxContextWindow = isPositiveSafeInt(entry.max_context_window) ? entry.max_context_window : DEFAULT_CONTEXT_WINDOW;
|
|
346
|
+
const configuredOverride = contextWindows && typeof contextWindows === "object" ? contextWindows[id] : void 0;
|
|
347
|
+
const targetContextWindow = isPositiveSafeInt(configuredOverride) ? configuredOverride : maxContextWindow;
|
|
348
|
+
const wireContextWindow = Math.max(maxContextWindow, targetContextWindow);
|
|
349
|
+
const maxTokens = Math.min(128e3, Math.max(32768, Math.floor(targetContextWindow / 4)));
|
|
199
350
|
const inputs = Array.isArray(entry.input_modalities) ? entry.input_modalities.filter((m) => m === "text" || m === "image") : ["text", "image"];
|
|
200
351
|
const thinkingLevelMap = buildThinkingLevelMap(entry.supported_reasoning_levels);
|
|
201
352
|
const hasReasoning = Object.entries(thinkingLevelMap).some(([k, v]) => k !== "off" && v !== null);
|
|
@@ -203,6 +354,7 @@ function parseModelsCache(rawCache, baseModels, disabledModels = []) {
|
|
|
203
354
|
const defaultVerbosity = isVerbosity(entry.default_verbosity) ? entry.default_verbosity : "low";
|
|
204
355
|
const defaultReasoningLevel = normalizeDefaultReasoningLevel(entry.default_reasoning_level, thinkingLevelMap);
|
|
205
356
|
metadataById.set(id, {
|
|
357
|
+
contextWindow: targetContextWindow,
|
|
206
358
|
supportVerbosity,
|
|
207
359
|
defaultVerbosity,
|
|
208
360
|
defaultReasoningLevel
|
|
@@ -216,7 +368,7 @@ function parseModelsCache(rawCache, baseModels, disabledModels = []) {
|
|
|
216
368
|
provider: PROVIDER,
|
|
217
369
|
baseUrl: "https://chatgpt.com/backend-api",
|
|
218
370
|
input: inputs.length > 0 ? inputs : ["text"],
|
|
219
|
-
contextWindow,
|
|
371
|
+
contextWindow: wireContextWindow,
|
|
220
372
|
maxTokens,
|
|
221
373
|
reasoning: hasReasoning,
|
|
222
374
|
thinkingLevelMap,
|
|
@@ -334,7 +486,10 @@ const STREAM_IDLE_TIMEOUT_MS = 3e5;
|
|
|
334
486
|
const MAX_REQUEST_IMAGE_BYTES = 20971520;
|
|
335
487
|
const REQUEST_IMAGE_PIXEL_BUDGET = 4194304;
|
|
336
488
|
const REQUEST_IMAGE_MAX_BYTES = 1048576;
|
|
337
|
-
const Config = z.object({
|
|
489
|
+
const Config = z.object({
|
|
490
|
+
disabledModels: z.array(z.string()).default([...DEFAULT_DISABLED_MODELS]).description("在模型选择器中屏蔽的模型 slug 列表").volatile(),
|
|
491
|
+
contextWindows: z.dict(z.number()).default({}).description("按模型 slug 覆盖上下文窗口大小(tokens),未配置时默认使用 872000").volatile()
|
|
492
|
+
});
|
|
338
493
|
function readVolatile(field, fallback) {
|
|
339
494
|
const value = typeof field?.get === "function" ? field.get() : field;
|
|
340
495
|
return value === void 0 || value === null ? fallback : value;
|
|
@@ -347,22 +502,30 @@ var CodexPiAiAdapter = class extends PiAiAdapter {
|
|
|
347
502
|
}
|
|
348
503
|
modelInfo(snapshot, provider, model) {
|
|
349
504
|
const info = super.modelInfo(snapshot, provider, model);
|
|
350
|
-
const
|
|
351
|
-
if (
|
|
505
|
+
const meta = this.#getModelMetadata(model);
|
|
506
|
+
if (meta === void 0) return info;
|
|
507
|
+
const defaultLevel = meta.defaultReasoningLevel;
|
|
508
|
+
const hasDefaultLevel = defaultLevel !== void 0 && info.reasoning !== void 0 && info.reasoning.efforts.some((e) => e.id === defaultLevel);
|
|
509
|
+
return {
|
|
352
510
|
...info,
|
|
353
|
-
|
|
511
|
+
context: {
|
|
512
|
+
...info.context ?? {},
|
|
513
|
+
contextWindow: meta.contextWindow
|
|
514
|
+
},
|
|
515
|
+
...hasDefaultLevel && info.reasoning !== void 0 ? { reasoning: {
|
|
354
516
|
...info.reasoning,
|
|
355
517
|
defaultEffort: defaultLevel
|
|
356
|
-
}
|
|
518
|
+
} } : {}
|
|
357
519
|
};
|
|
358
|
-
return info;
|
|
359
520
|
}
|
|
360
521
|
};
|
|
361
522
|
function apply(ctx, config = {}) {
|
|
362
523
|
const baseProvider = openaiCodexProvider();
|
|
363
524
|
const baseModels = baseProvider.getModels();
|
|
525
|
+
const retryPolicy = resolveRetryPolicy(void 0, "codex-subscription retryPolicy");
|
|
364
526
|
const getCodexHome = () => resolveCodexHome();
|
|
365
527
|
const getDisabledModels = () => readVolatile(config.disabledModels, DEFAULT_DISABLED_MODELS);
|
|
528
|
+
const getContextWindows = () => readVolatile(config.contextWindows, {});
|
|
366
529
|
const authManager = createAuthManager({ getCodexHome });
|
|
367
530
|
let fileSignature;
|
|
368
531
|
let cachedRaw;
|
|
@@ -398,6 +561,10 @@ function apply(ctx, config = {}) {
|
|
|
398
561
|
}
|
|
399
562
|
};
|
|
400
563
|
}
|
|
564
|
+
const usageFetcher = createUsageFetcher({
|
|
565
|
+
resolveCredentials: () => authManager.resolveCredentials(),
|
|
566
|
+
buildHeaders: (authHeaders) => buildCodexHeaders(authHeaders, ensureSnapshot().catalog.clientVersion)
|
|
567
|
+
});
|
|
401
568
|
const piProvider = Object.freeze({
|
|
402
569
|
...baseProvider,
|
|
403
570
|
id: PROVIDER,
|
|
@@ -407,12 +574,19 @@ function apply(ctx, config = {}) {
|
|
|
407
574
|
apiKey: requestTokenAuth
|
|
408
575
|
}),
|
|
409
576
|
getModels: () => ensureSnapshot().catalog.models,
|
|
410
|
-
stream: (model, context, options) =>
|
|
411
|
-
|
|
577
|
+
stream: (model, context, options) => {
|
|
578
|
+
usageFetcher.invalidate();
|
|
579
|
+
return baseProvider.stream(model, context, wrapOptions(model, options));
|
|
580
|
+
},
|
|
581
|
+
streamSimple: (model, context, options) => {
|
|
582
|
+
usageFetcher.invalidate();
|
|
583
|
+
return baseProvider.streamSimple(model, context, wrapOptions(model, options));
|
|
584
|
+
}
|
|
412
585
|
});
|
|
413
586
|
function ensureSnapshot() {
|
|
414
587
|
const cachePath = join(getCodexHome(), "models_cache.json");
|
|
415
588
|
const disabled = getDisabledModels();
|
|
589
|
+
const contextWindows = getContextWindows();
|
|
416
590
|
try {
|
|
417
591
|
const stat = statSync(cachePath);
|
|
418
592
|
const nextFileSig = `${cachePath}:${stat.mtimeMs}:${stat.size}`;
|
|
@@ -424,12 +598,16 @@ function apply(ctx, config = {}) {
|
|
|
424
598
|
} catch (error) {
|
|
425
599
|
if (!warnedMissingFile) {
|
|
426
600
|
warnedMissingFile = true;
|
|
427
|
-
console.error(`[dsh-codex-subscription] 读取 Codex models_cache.json 失败 (${cachePath}): ${
|
|
601
|
+
console.error(`[dsh-codex-subscription] 读取 Codex models_cache.json 失败 (${cachePath}): ${safeErrorMessage(error)}`);
|
|
428
602
|
}
|
|
429
603
|
}
|
|
430
|
-
const nextSnapshotSig = JSON.stringify([
|
|
604
|
+
const nextSnapshotSig = JSON.stringify([
|
|
605
|
+
fileSignature ?? "none",
|
|
606
|
+
disabled,
|
|
607
|
+
contextWindows
|
|
608
|
+
]);
|
|
431
609
|
if (nextSnapshotSig !== snapshotSignature || currentProfilesMap === void 0) {
|
|
432
|
-
currentCatalog = cachedRaw !== void 0 ? parseModelsCache(cachedRaw, baseModels, disabled) : {
|
|
610
|
+
currentCatalog = cachedRaw !== void 0 ? parseModelsCache(cachedRaw, baseModels, disabled, contextWindows) : {
|
|
433
611
|
clientVersion: DEFAULT_CLIENT_VERSION,
|
|
434
612
|
models: [],
|
|
435
613
|
metadataById: new Map()
|
|
@@ -438,6 +616,7 @@ function apply(ctx, config = {}) {
|
|
|
438
616
|
provider: PROVIDER,
|
|
439
617
|
displayName: DISPLAY_NAME,
|
|
440
618
|
piProvider,
|
|
619
|
+
retryPolicy,
|
|
441
620
|
configuredMaxTokens: new Map(),
|
|
442
621
|
modelErrors: new Map(),
|
|
443
622
|
maxRequestImageBytes: MAX_REQUEST_IMAGE_BYTES,
|
|
@@ -480,6 +659,28 @@ function apply(ctx, config = {}) {
|
|
|
480
659
|
}
|
|
481
660
|
}, (modelId) => ensureSnapshot().catalog.metadataById.get(modelId));
|
|
482
661
|
ctx.llm.registerAdapter([PROVIDER], adapter);
|
|
662
|
+
const mountUsageRoute = (connCtx) => {
|
|
663
|
+
const connection = connCtx.get?.("connection");
|
|
664
|
+
const registerFetch = connection?.fetch?.register;
|
|
665
|
+
if (typeof registerFetch !== "function") return;
|
|
666
|
+
const registerRoute = () => registerFetch.call(connection?.fetch, {
|
|
667
|
+
path: USAGE_ROUTE_PATH,
|
|
668
|
+
methods: ["GET"],
|
|
669
|
+
requestBody: "buffered",
|
|
670
|
+
fetch: async (request) => {
|
|
671
|
+
const force = new URL(request.url, "http://localhost").searchParams.get("force") === "1";
|
|
672
|
+
const result = await usageFetcher.getUsage(force);
|
|
673
|
+
return new Response(JSON.stringify(result), {
|
|
674
|
+
status: 200,
|
|
675
|
+
headers: { "content-type": "application/json" }
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
});
|
|
679
|
+
if (typeof connCtx.effect === "function") connCtx.effect(() => registerRoute(), "codex-subscription: usage route");
|
|
680
|
+
else registerRoute();
|
|
681
|
+
};
|
|
682
|
+
if (typeof ctx.inject === "function") ctx.inject(["connection"], mountUsageRoute);
|
|
683
|
+
else mountUsageRoute(ctx);
|
|
483
684
|
ctx.on?.("loader/volatile-update", () => {
|
|
484
685
|
ensureSnapshot();
|
|
485
686
|
ctx.llm.emitAdaptersUpdated?.();
|
|
@@ -489,14 +690,22 @@ const __test = Object.freeze({
|
|
|
489
690
|
PROVIDER,
|
|
490
691
|
DISPLAY_NAME,
|
|
491
692
|
DEFAULT_DISABLED_MODELS,
|
|
693
|
+
DEFAULT_CONTEXT_WINDOW,
|
|
694
|
+
CODEX_USAGE_URL,
|
|
695
|
+
USAGE_ROUTE_PATH,
|
|
696
|
+
DEFAULT_USAGE_TTL_MS,
|
|
492
697
|
resolveCodexHome,
|
|
493
698
|
decodeJwtPayload,
|
|
699
|
+
extractAccountId,
|
|
700
|
+
safeErrorMessage,
|
|
494
701
|
createAuthManager,
|
|
495
702
|
buildThinkingLevelMap,
|
|
496
703
|
parseModelsCache,
|
|
497
704
|
sanitizeCodexPayload,
|
|
498
705
|
buildCodexHeaders,
|
|
499
|
-
buildCodexUserAgent
|
|
706
|
+
buildCodexUserAgent,
|
|
707
|
+
parseUsagePayload,
|
|
708
|
+
createUsageFetcher
|
|
500
709
|
});
|
|
501
710
|
//#endregion
|
|
502
711
|
export { Config, __test, apply, inject, name };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lyogvce/dsh-codex-subscription",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "复用本机 ~/.codex 订阅凭证与 models_cache.json 模型缓存,在 DSH 中通过 SSE 直连调用 Codex 模型。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"types": "./lib/index.d.ts",
|
|
12
12
|
"default": "./lib/index.js"
|
|
13
13
|
},
|
|
14
|
+
"./client": "./lib/client.js",
|
|
14
15
|
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
15
16
|
"./package.json": "./package.json",
|
|
16
17
|
"./locale/*.json": "./locale/*.json",
|
|
@@ -26,6 +27,16 @@
|
|
|
26
27
|
"dsh": {
|
|
27
28
|
"bundle": {
|
|
28
29
|
"patch": "./cordis.patch.yml"
|
|
30
|
+
},
|
|
31
|
+
"client": {
|
|
32
|
+
"platform": "web",
|
|
33
|
+
"immediately": true,
|
|
34
|
+
"inject": [
|
|
35
|
+
"@deepseek-ai/dsh-client-locale",
|
|
36
|
+
"@deepseek-ai/dsh-client-ui-slots",
|
|
37
|
+
"@deepseek-ai/dsh-client-ui-conversation",
|
|
38
|
+
"@deepseek-ai/dsh-client-ui-model-selection"
|
|
39
|
+
]
|
|
29
40
|
}
|
|
30
41
|
},
|
|
31
42
|
"peerDependencies": {
|