@lyogvce/dsh-codex-subscription 0.1.0 → 0.2.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 +2 -0
- package/lib/client.js +426 -0
- package/lib/index.d.ts +60 -1
- package/lib/index.js +172 -8
- package/package.json +12 -1
package/README.md
CHANGED
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
- 在插件配置中通过 `disabledModels` 屏蔽指定模型 `slug`,保持模型选择器干净清爽。
|
|
18
18
|
4. **原生多轮思考链回放与工具调用**
|
|
19
19
|
- 底层复用 DSH 内置的 `PiAiAdapter` 与 `@earendil-works/pi-ai` 的 `openaiCodexProvider()`,固定使用 `sse` 传输(含 `zstd` 请求体压缩、`session-id` / `thread-id` 缓存头、`reasoning.encrypted_content` 签名回放与图片自动缩放卸载)。
|
|
20
|
+
5. **Codex 订阅限额内联展示(`conversation.input.right`)**
|
|
21
|
+
- 当会话选中 `openai-codex` 模型时,在输入框右下角紧邻模型选择器左侧展示 `5h X% · 7d Y%` 限额胶囊(数据源来自 `https://chatgpt.com/backend-api/wham/usage`,内置 60 秒缓存、单飞合并与每轮回复结束自动刷新),点击可展开查看 5 小时 / 7 天窗口进度条、重置倒计时与手动刷新按钮;切换至其他 Provider 模型时自动隐藏。
|
|
20
22
|
|
|
21
23
|
---
|
|
22
24
|
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,426 @@
|
|
|
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
|
+
const USAGE_ROUTE_PATH = "/api/codex-subscription/usage";
|
|
19
|
+
function clampPercent(value) {
|
|
20
|
+
return Math.max(0, Math.min(100, Math.round(value)));
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region src/client/quota.ts
|
|
24
|
+
const RING_CIRCUMFERENCE = 2 * Math.PI * 5.5;
|
|
25
|
+
function resolveActiveProvider(directorySnapshot, projectedSelection) {
|
|
26
|
+
const dir = directorySnapshot && typeof directorySnapshot === "object" ? directorySnapshot : void 0;
|
|
27
|
+
const proj = projectedSelection && typeof projectedSelection === "object" ? projectedSelection : void 0;
|
|
28
|
+
return [
|
|
29
|
+
dir?.pending?.provider,
|
|
30
|
+
dir?.current?.provider,
|
|
31
|
+
proj?.next?.provider,
|
|
32
|
+
proj?.provider
|
|
33
|
+
].find((candidate) => typeof candidate === "string" && candidate !== "");
|
|
34
|
+
}
|
|
35
|
+
function isCodexProvider(provider) {
|
|
36
|
+
return provider === PROVIDER;
|
|
37
|
+
}
|
|
38
|
+
function didTurnFinish(previousRunning, currentRunning) {
|
|
39
|
+
return previousRunning === true && currentRunning === false;
|
|
40
|
+
}
|
|
41
|
+
function windowLevelOf(usedPercent, limitReached = false) {
|
|
42
|
+
if (limitReached || usedPercent >= 100) return "error";
|
|
43
|
+
if (usedPercent >= 80) return "warn";
|
|
44
|
+
return "ok";
|
|
45
|
+
}
|
|
46
|
+
function formatQuotaSummary(snapshot) {
|
|
47
|
+
const primaryPercent = clampPercent(snapshot.primary.usedPercent);
|
|
48
|
+
const secondaryPercent = snapshot.secondary !== null ? clampPercent(snapshot.secondary.usedPercent) : null;
|
|
49
|
+
return {
|
|
50
|
+
primaryPercent,
|
|
51
|
+
secondaryPercent,
|
|
52
|
+
level: windowLevelOf(secondaryPercent !== null ? Math.max(primaryPercent, secondaryPercent) : primaryPercent, snapshot.limitReached),
|
|
53
|
+
pillText: secondaryPercent !== null ? `5h ${primaryPercent}% · 7d ${secondaryPercent}%` : `5h ${primaryPercent}%`,
|
|
54
|
+
ringDash: `${(RING_CIRCUMFERENCE * primaryPercent / 100).toFixed(2)} ${RING_CIRCUMFERENCE.toFixed(2)}`
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function padTwo(n) {
|
|
58
|
+
return String(n).padStart(2, "0");
|
|
59
|
+
}
|
|
60
|
+
const COUNTDOWN_FORMATTERS = {
|
|
61
|
+
zh: {
|
|
62
|
+
soon: () => "即将重置",
|
|
63
|
+
minutes: (m, clock) => `${m} 分钟后重置 (${clock})`,
|
|
64
|
+
hours: (h, m) => m > 0 ? `${h} 小时 ${m} 分钟后重置` : `${h} 小时后重置`,
|
|
65
|
+
days: (d, h) => h > 0 ? `${d} 天 ${h} 小时后重置` : `${d} 天后重置`
|
|
66
|
+
},
|
|
67
|
+
en: {
|
|
68
|
+
soon: () => "Resetting soon",
|
|
69
|
+
minutes: (m, clock) => `Resets in ${m}m (${clock})`,
|
|
70
|
+
hours: (h, m) => m > 0 ? `Resets in ${h}h ${m}m` : `Resets in ${h}h`,
|
|
71
|
+
days: (d, h) => h > 0 ? `Resets in ${d}d ${h}h` : `Resets in ${d}d`
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
function formatResetCountdown(resetAtSec, nowMs, locale) {
|
|
75
|
+
if (typeof resetAtSec !== "number" || !Number.isFinite(resetAtSec) || resetAtSec <= 0) return "";
|
|
76
|
+
const fmt = COUNTDOWN_FORMATTERS[locale];
|
|
77
|
+
const diffSec = Math.max(0, Math.round(resetAtSec - nowMs / 1e3));
|
|
78
|
+
if (diffSec === 0) return fmt.soon();
|
|
79
|
+
const totalMinutes = Math.max(1, Math.ceil(diffSec / 60));
|
|
80
|
+
if (totalMinutes < 60) {
|
|
81
|
+
const resetDate = new Date(resetAtSec * 1e3);
|
|
82
|
+
const clock = `${padTwo(resetDate.getHours())}:${padTwo(resetDate.getMinutes())}`;
|
|
83
|
+
return fmt.minutes(totalMinutes, clock);
|
|
84
|
+
}
|
|
85
|
+
const totalHours = Math.floor(totalMinutes / 60);
|
|
86
|
+
const remMinutes = totalMinutes % 60;
|
|
87
|
+
if (totalHours < 24) return fmt.hours(totalHours, remMinutes);
|
|
88
|
+
const days = Math.floor(totalHours / 24);
|
|
89
|
+
const remHours = totalHours % 24;
|
|
90
|
+
return fmt.days(days, remHours);
|
|
91
|
+
}
|
|
92
|
+
function createQuotaStore(options = {}) {
|
|
93
|
+
const fetchFn = options.fetchImpl ?? globalThis.fetch;
|
|
94
|
+
const nowFn = options.now ?? Date.now;
|
|
95
|
+
const ttlMs = options.ttlMs ?? 6e4;
|
|
96
|
+
let state = {
|
|
97
|
+
quota: null,
|
|
98
|
+
loading: false,
|
|
99
|
+
error: null
|
|
100
|
+
};
|
|
101
|
+
let inflight = null;
|
|
102
|
+
let inflightForce = false;
|
|
103
|
+
let requestSeq = 0;
|
|
104
|
+
const listeners = new Set();
|
|
105
|
+
function setState(next) {
|
|
106
|
+
state = next;
|
|
107
|
+
for (const listener of listeners) listener();
|
|
108
|
+
}
|
|
109
|
+
async function refresh(force = false) {
|
|
110
|
+
if (!force && state.quota !== null && nowFn() - state.quota.fetchedAt < ttlMs) return;
|
|
111
|
+
if (inflight !== null && (!force || inflightForce)) return inflight;
|
|
112
|
+
const seq = ++requestSeq;
|
|
113
|
+
inflightForce = force;
|
|
114
|
+
setState({
|
|
115
|
+
...state,
|
|
116
|
+
loading: true,
|
|
117
|
+
error: null
|
|
118
|
+
});
|
|
119
|
+
const url = force ? `${USAGE_ROUTE_PATH}?force=1` : USAGE_ROUTE_PATH;
|
|
120
|
+
const task = (async () => {
|
|
121
|
+
try {
|
|
122
|
+
const response = await fetchFn(url, {
|
|
123
|
+
method: "GET",
|
|
124
|
+
headers: { Accept: "application/json" }
|
|
125
|
+
});
|
|
126
|
+
if (seq !== requestSeq) return;
|
|
127
|
+
if (!response.ok) {
|
|
128
|
+
setState({
|
|
129
|
+
quota: state.quota,
|
|
130
|
+
loading: false,
|
|
131
|
+
error: `HTTP ${response.status}`
|
|
132
|
+
});
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const payload = await response.json();
|
|
136
|
+
if (seq !== requestSeq) return;
|
|
137
|
+
if (payload && payload.ok === true && payload.quota) setState({
|
|
138
|
+
quota: payload.quota,
|
|
139
|
+
loading: false,
|
|
140
|
+
error: null
|
|
141
|
+
});
|
|
142
|
+
else {
|
|
143
|
+
const fallbackQuota = payload && !payload.ok && payload.quota ? payload.quota : state.quota;
|
|
144
|
+
const errMsg = payload && !payload.ok && typeof payload.error === "string" ? payload.error : "Invalid response";
|
|
145
|
+
setState({
|
|
146
|
+
quota: fallbackQuota ?? null,
|
|
147
|
+
loading: false,
|
|
148
|
+
error: errMsg
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
} catch (error) {
|
|
152
|
+
if (seq !== requestSeq) return;
|
|
153
|
+
setState({
|
|
154
|
+
quota: state.quota,
|
|
155
|
+
loading: false,
|
|
156
|
+
error: error instanceof Error ? error.message : String(error)
|
|
157
|
+
});
|
|
158
|
+
} finally {
|
|
159
|
+
if (seq === requestSeq) {
|
|
160
|
+
inflight = null;
|
|
161
|
+
inflightForce = false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
})();
|
|
165
|
+
inflight = task;
|
|
166
|
+
return task;
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
getSnapshot: () => state,
|
|
170
|
+
subscribe(listener) {
|
|
171
|
+
listeners.add(listener);
|
|
172
|
+
return () => {
|
|
173
|
+
listeners.delete(listener);
|
|
174
|
+
};
|
|
175
|
+
},
|
|
176
|
+
refresh
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
//#endregion
|
|
180
|
+
//#region src/client/styles.ts
|
|
181
|
+
const STYLESHEET = [
|
|
182
|
+
".dsh-codex-quota-root{position:relative;display:inline-flex;align-items:center;flex:none}",
|
|
183
|
+
".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}",
|
|
184
|
+
".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)}",
|
|
185
|
+
".dsh-codex-quota-trigger[data-level=\"warn\"]{color:var(--dsw-alias-state-warn-primary)}",
|
|
186
|
+
".dsh-codex-quota-trigger[data-level=\"error\"]{color:var(--dsw-alias-state-error-primary)}",
|
|
187
|
+
"[data-model-compact] .dsh-codex-quota-text{display:none}",
|
|
188
|
+
".dsh-codex-quota-ring-track{fill:none;stroke:var(--dsw-alias-border-l2);stroke-width:2px}",
|
|
189
|
+
".dsh-codex-quota-ring-fill{fill:none;stroke:var(--dsw-alias-brand-primary);stroke-width:2px;stroke-linecap:round}",
|
|
190
|
+
".dsh-codex-quota-trigger[data-level=\"warn\"] .dsh-codex-quota-ring-fill{stroke:var(--dsw-alias-state-warn-primary)}",
|
|
191
|
+
".dsh-codex-quota-trigger[data-level=\"error\"] .dsh-codex-quota-ring-fill{stroke:var(--dsw-alias-state-error-primary)}",
|
|
192
|
+
".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}",
|
|
193
|
+
".dsh-codex-quota-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:10px}",
|
|
194
|
+
".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)}",
|
|
195
|
+
".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)}",
|
|
196
|
+
".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}",
|
|
197
|
+
".dsh-codex-quota-refresh:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}",
|
|
198
|
+
".dsh-codex-quota-refresh:disabled{opacity:0.55;cursor:default}",
|
|
199
|
+
".dsh-codex-quota-section{margin-bottom:10px}",
|
|
200
|
+
".dsh-codex-quota-section:last-of-type{margin-bottom:4px}",
|
|
201
|
+
".dsh-codex-quota-row{display:flex;align-items:baseline;justify-content:space-between;font-variant-numeric:tabular-nums}",
|
|
202
|
+
".dsh-codex-quota-label{color:var(--dsw-alias-label-primary);font-weight:500}",
|
|
203
|
+
".dsh-codex-quota-pct{color:var(--dsw-alias-label-primary);font-weight:600}",
|
|
204
|
+
".dsh-codex-quota-bar{height:5px;border-radius:999px;background:var(--dsw-alias-interactive-bg-hover);margin:5px 0 4px;overflow:hidden}",
|
|
205
|
+
".dsh-codex-quota-bar-fill{height:100%;border-radius:999px;background:var(--dsw-alias-brand-primary);transition:width .2s ease}",
|
|
206
|
+
".dsh-codex-quota-bar-fill[data-level=\"warn\"]{background:var(--dsw-alias-state-warn-primary)}",
|
|
207
|
+
".dsh-codex-quota-bar-fill[data-level=\"error\"]{background:var(--dsw-alias-state-error-primary)}",
|
|
208
|
+
".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}",
|
|
209
|
+
".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}"
|
|
210
|
+
].join("");
|
|
211
|
+
//#endregion
|
|
212
|
+
//#region src/client/index.ts
|
|
213
|
+
const React = require("react");
|
|
214
|
+
const h = React.createElement;
|
|
215
|
+
const NS = "codex-subscription";
|
|
216
|
+
const SLOT_NAME = "conversation.input.right";
|
|
217
|
+
const SLOT_ENTRY_ID = "codex-subscription:quota";
|
|
218
|
+
const zh = {
|
|
219
|
+
"quota.title": "Codex 订阅限额",
|
|
220
|
+
"quota.loading": "正在获取 Codex 限额…",
|
|
221
|
+
"quota.refresh": "↻ 刷新",
|
|
222
|
+
"quota.refreshing": "↻ 刷新中…",
|
|
223
|
+
"quota.window5h": "5 小时窗口",
|
|
224
|
+
"quota.window7d": "7 天窗口",
|
|
225
|
+
"quota.usedPrefix": "已用 ",
|
|
226
|
+
"quota.remainPrefix": "剩余 "
|
|
227
|
+
};
|
|
228
|
+
const en = {
|
|
229
|
+
"quota.title": "Codex Subscription Quota",
|
|
230
|
+
"quota.loading": "Loading Codex quota…",
|
|
231
|
+
"quota.refresh": "↻ Refresh",
|
|
232
|
+
"quota.refreshing": "↻ Refreshing…",
|
|
233
|
+
"quota.window5h": "5-hour window",
|
|
234
|
+
"quota.window7d": "7-day window",
|
|
235
|
+
"quota.usedPrefix": "Used ",
|
|
236
|
+
"quota.remainPrefix": "Remaining "
|
|
237
|
+
};
|
|
238
|
+
const EMPTY_MODEL_STORE = {
|
|
239
|
+
getSnapshot: () => null,
|
|
240
|
+
subscribe: () => () => {}
|
|
241
|
+
};
|
|
242
|
+
function CodexQuotaPill(props) {
|
|
243
|
+
const { sessionId, quotaStore, getModelStore, useProjection, useSession, t, getLocale } = props;
|
|
244
|
+
const modelStore = getModelStore(sessionId);
|
|
245
|
+
const directorySnap = React.useSyncExternalStore(modelStore.subscribe, modelStore.getSnapshot);
|
|
246
|
+
const projectedSelection = typeof useProjection === "function" ? useProjection("modelSelection") : void 0;
|
|
247
|
+
const running = typeof useSession === "function" ? Boolean(useSession((s) => s?.running)) : false;
|
|
248
|
+
const isCodex = isCodexProvider(resolveActiveProvider(directorySnap, projectedSelection));
|
|
249
|
+
const quotaSnap = React.useSyncExternalStore(quotaStore.subscribe, quotaStore.getSnapshot);
|
|
250
|
+
const [open, setOpen] = React.useState(false);
|
|
251
|
+
const rootRef = React.useRef(null);
|
|
252
|
+
const prevRunningRef = React.useRef(running);
|
|
253
|
+
const prevSessionIdRef = React.useRef(sessionId);
|
|
254
|
+
React.useEffect(() => {
|
|
255
|
+
const sameSession = prevSessionIdRef.current === sessionId;
|
|
256
|
+
prevSessionIdRef.current = sessionId;
|
|
257
|
+
if (!isCodex) {
|
|
258
|
+
setOpen(false);
|
|
259
|
+
prevRunningRef.current = running;
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
quotaStore.refresh(false);
|
|
263
|
+
if (sameSession && didTurnFinish(prevRunningRef.current, running)) quotaStore.refresh(true);
|
|
264
|
+
prevRunningRef.current = running;
|
|
265
|
+
}, [
|
|
266
|
+
isCodex,
|
|
267
|
+
running,
|
|
268
|
+
sessionId,
|
|
269
|
+
quotaStore
|
|
270
|
+
]);
|
|
271
|
+
React.useEffect(() => {
|
|
272
|
+
if (!open || !isCodex || typeof globalThis.document === "undefined") return;
|
|
273
|
+
const onPointerDown = (event) => {
|
|
274
|
+
if (rootRef.current && typeof rootRef.current.contains === "function") {
|
|
275
|
+
if (!rootRef.current.contains(event.target)) setOpen(false);
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
const onKeyDown = (event) => {
|
|
279
|
+
if (event.key === "Escape") setOpen(false);
|
|
280
|
+
};
|
|
281
|
+
globalThis.document.addEventListener("pointerdown", onPointerDown);
|
|
282
|
+
globalThis.document.addEventListener("keydown", onKeyDown);
|
|
283
|
+
return () => {
|
|
284
|
+
globalThis.document.removeEventListener("pointerdown", onPointerDown);
|
|
285
|
+
globalThis.document.removeEventListener("keydown", onKeyDown);
|
|
286
|
+
};
|
|
287
|
+
}, [open, isCodex]);
|
|
288
|
+
if (!isCodex) return null;
|
|
289
|
+
const summary = quotaSnap.quota !== null ? formatQuotaSummary(quotaSnap.quota) : null;
|
|
290
|
+
const level = summary?.level ?? "ok";
|
|
291
|
+
const pillText = summary !== null ? summary.pillText : quotaSnap.loading ? "Codex …" : "Codex --";
|
|
292
|
+
const ringDash = summary?.ringDash ?? "0 34.56";
|
|
293
|
+
const locale = getLocale();
|
|
294
|
+
const nowMs = Date.now();
|
|
295
|
+
const renderWindowSection = (title, win, windowLimitReached) => {
|
|
296
|
+
const usedPercent = clampPercent(win.usedPercent);
|
|
297
|
+
const winLevel = windowLevelOf(usedPercent, windowLimitReached);
|
|
298
|
+
const remainPercent = 100 - usedPercent;
|
|
299
|
+
const countdown = formatResetCountdown(win.resetAt, nowMs, locale);
|
|
300
|
+
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", {
|
|
301
|
+
className: "dsh-codex-quota-bar-fill",
|
|
302
|
+
"data-level": winLevel,
|
|
303
|
+
style: { width: `${usedPercent}%` }
|
|
304
|
+
})), h("div", { className: "dsh-codex-quota-meta" }, h("span", null, `${t("quota.remainPrefix")}${remainPercent}%`), countdown !== "" ? h("span", null, countdown) : null));
|
|
305
|
+
};
|
|
306
|
+
const quota = quotaSnap.quota;
|
|
307
|
+
const secondaryReached = Boolean(quota?.secondary && quota.secondary.usedPercent >= 100);
|
|
308
|
+
const primaryReached = Boolean(quota?.limitReached && !secondaryReached);
|
|
309
|
+
return h("span", {
|
|
310
|
+
ref: rootRef,
|
|
311
|
+
className: "dsh-codex-quota-root",
|
|
312
|
+
"data-composer-overlay": true
|
|
313
|
+
}, h("button", {
|
|
314
|
+
type: "button",
|
|
315
|
+
className: "dsh-codex-quota-trigger",
|
|
316
|
+
"data-level": level,
|
|
317
|
+
"aria-label": `${t("quota.title")}: ${pillText}`,
|
|
318
|
+
"aria-haspopup": "dialog",
|
|
319
|
+
"aria-expanded": open,
|
|
320
|
+
title: `${t("quota.title")} (${pillText})`,
|
|
321
|
+
onClick: () => {
|
|
322
|
+
const nextOpen = !open;
|
|
323
|
+
setOpen(nextOpen);
|
|
324
|
+
if (nextOpen) quotaStore.refresh(false);
|
|
325
|
+
}
|
|
326
|
+
}, h("svg", {
|
|
327
|
+
viewBox: "0 0 14 14",
|
|
328
|
+
width: 14,
|
|
329
|
+
height: 14,
|
|
330
|
+
"aria-hidden": true
|
|
331
|
+
}, h("circle", {
|
|
332
|
+
className: "dsh-codex-quota-ring-track",
|
|
333
|
+
cx: 7,
|
|
334
|
+
cy: 7,
|
|
335
|
+
r: 5.5
|
|
336
|
+
}), h("circle", {
|
|
337
|
+
className: "dsh-codex-quota-ring-fill",
|
|
338
|
+
cx: 7,
|
|
339
|
+
cy: 7,
|
|
340
|
+
r: 5.5,
|
|
341
|
+
strokeDasharray: ringDash,
|
|
342
|
+
transform: "rotate(-90 7 7)"
|
|
343
|
+
})), h("span", { className: "dsh-codex-quota-text" }, pillText)), open ? h("div", {
|
|
344
|
+
className: "dsh-codex-quota-popover",
|
|
345
|
+
role: "dialog",
|
|
346
|
+
"aria-label": t("quota.title"),
|
|
347
|
+
onMouseDown: (e) => {
|
|
348
|
+
e.stopPropagation?.();
|
|
349
|
+
}
|
|
350
|
+
}, 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", {
|
|
351
|
+
type: "button",
|
|
352
|
+
className: "dsh-codex-quota-refresh",
|
|
353
|
+
disabled: quotaSnap.loading,
|
|
354
|
+
onClick: () => {
|
|
355
|
+
quotaStore.refresh(true);
|
|
356
|
+
}
|
|
357
|
+
}, 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);
|
|
358
|
+
}
|
|
359
|
+
const inject = ["slots", "locale"];
|
|
360
|
+
function apply(ctx) {
|
|
361
|
+
ctx.effect(() => {
|
|
362
|
+
if (typeof globalThis.document === "undefined") return;
|
|
363
|
+
const tag = globalThis.document.createElement("style");
|
|
364
|
+
tag.dataset.pluginCss = "@lyogvce/dsh-codex-subscription";
|
|
365
|
+
tag.textContent = STYLESHEET;
|
|
366
|
+
globalThis.document.head.appendChild(tag);
|
|
367
|
+
return () => {
|
|
368
|
+
tag.remove();
|
|
369
|
+
};
|
|
370
|
+
}, "codex-subscription: stylesheet");
|
|
371
|
+
ctx.effect(() => ctx.locale.register(NS, {
|
|
372
|
+
zh,
|
|
373
|
+
en
|
|
374
|
+
}), "codex-subscription: dictionaries");
|
|
375
|
+
const t = ctx.locale.bind(NS);
|
|
376
|
+
const quotaStore = createQuotaStore();
|
|
377
|
+
const getLocale = () => {
|
|
378
|
+
return (ctx.locale.getLocale?.()?.id ?? "").toLowerCase().startsWith("en") ? "en" : "zh";
|
|
379
|
+
};
|
|
380
|
+
const getModelStore = (sessionId) => {
|
|
381
|
+
if (typeof sessionId !== "string" || sessionId === "") return EMPTY_MODEL_STORE;
|
|
382
|
+
const models = ctx.get("modelDirectories");
|
|
383
|
+
if (!models || typeof models.directoryFor !== "function") return EMPTY_MODEL_STORE;
|
|
384
|
+
try {
|
|
385
|
+
return models.directoryFor(sessionId)?.store ?? EMPTY_MODEL_STORE;
|
|
386
|
+
} catch {
|
|
387
|
+
return EMPTY_MODEL_STORE;
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
ctx.effect(() => ctx.slots.inject(SLOT_NAME, () => ctx.slots.register({
|
|
391
|
+
name: SLOT_NAME,
|
|
392
|
+
id: SLOT_ENTRY_ID,
|
|
393
|
+
order: 10,
|
|
394
|
+
locale: NS,
|
|
395
|
+
inject: (sessionId) => ({
|
|
396
|
+
sessionId,
|
|
397
|
+
quotaStore,
|
|
398
|
+
getModelStore,
|
|
399
|
+
t,
|
|
400
|
+
getLocale
|
|
401
|
+
})
|
|
402
|
+
}, CodexQuotaPill)), "codex-subscription: quota pill");
|
|
403
|
+
}
|
|
404
|
+
const __test = Object.freeze({
|
|
405
|
+
PROVIDER_ID: PROVIDER,
|
|
406
|
+
USAGE_API_PATH: USAGE_ROUTE_PATH,
|
|
407
|
+
resolveActiveProvider,
|
|
408
|
+
isCodexProvider,
|
|
409
|
+
didTurnFinish,
|
|
410
|
+
windowLevelOf,
|
|
411
|
+
formatQuotaSummary,
|
|
412
|
+
formatResetCountdown,
|
|
413
|
+
createQuotaStore,
|
|
414
|
+
CodexQuotaPill
|
|
415
|
+
});
|
|
416
|
+
//#endregion
|
|
417
|
+
exports.CodexQuotaPill = CodexQuotaPill;
|
|
418
|
+
exports.NS = NS;
|
|
419
|
+
exports.__test = __test;
|
|
420
|
+
exports.apply = apply;
|
|
421
|
+
exports.en = en;
|
|
422
|
+
exports.inject = inject;
|
|
423
|
+
exports.zh = zh;
|
|
424
|
+
return module.exports;
|
|
425
|
+
}
|
|
426
|
+
});
|
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
|
|
@@ -56,6 +74,39 @@ declare function buildCodexUserAgent(version?: string): string;
|
|
|
56
74
|
declare function buildCodexHeaders(existingHeaders: Record<string, string> | undefined, clientVersion: string, sessionId?: string | undefined): Record<string, string>;
|
|
57
75
|
declare function sanitizeCodexPayload(payload: Record<string, unknown> | null | undefined, modelMeta?: CodexModelMetadata | undefined): Record<string, unknown> | null | undefined;
|
|
58
76
|
//#endregion
|
|
77
|
+
//#region src/usage.d.ts
|
|
78
|
+
interface CodexQuotaWindow {
|
|
79
|
+
readonly usedPercent: number;
|
|
80
|
+
readonly resetAt: number | null;
|
|
81
|
+
}
|
|
82
|
+
interface CodexQuotaSnapshot {
|
|
83
|
+
readonly planType: string;
|
|
84
|
+
readonly limitReached: boolean;
|
|
85
|
+
readonly primary: CodexQuotaWindow;
|
|
86
|
+
readonly secondary: CodexQuotaWindow | null;
|
|
87
|
+
readonly fetchedAt: number;
|
|
88
|
+
}
|
|
89
|
+
type CodexQuotaResponse = {
|
|
90
|
+
readonly ok: true;
|
|
91
|
+
readonly quota: CodexQuotaSnapshot;
|
|
92
|
+
} | {
|
|
93
|
+
readonly ok: false;
|
|
94
|
+
readonly error: string;
|
|
95
|
+
readonly quota?: CodexQuotaSnapshot | undefined;
|
|
96
|
+
};
|
|
97
|
+
interface UsageFetcherOptions {
|
|
98
|
+
readonly resolveCredentials: () => Promise<CodexResolvedCredentials>;
|
|
99
|
+
readonly buildHeaders: (authHeaders: Record<string, string>) => Record<string, string>;
|
|
100
|
+
readonly fetchImpl?: typeof fetch | undefined;
|
|
101
|
+
readonly now?: (() => number) | undefined;
|
|
102
|
+
readonly ttlMs?: number | undefined;
|
|
103
|
+
}
|
|
104
|
+
declare function parseUsagePayload(raw: unknown, fetchedAt: number): CodexQuotaSnapshot | null;
|
|
105
|
+
declare function createUsageFetcher(options: UsageFetcherOptions): {
|
|
106
|
+
getUsage: (force?: boolean) => Promise<CodexQuotaResponse>;
|
|
107
|
+
invalidate: () => void;
|
|
108
|
+
};
|
|
109
|
+
//#endregion
|
|
59
110
|
//#region src/host-types.d.ts
|
|
60
111
|
interface LlmServiceLike {
|
|
61
112
|
registerAdapter: (providers: string[], adapter: unknown) => (() => void) & {
|
|
@@ -67,6 +118,8 @@ interface PluginContextLike {
|
|
|
67
118
|
llm: LlmServiceLike;
|
|
68
119
|
get?: (name: string) => unknown;
|
|
69
120
|
on?: (event: string, listener: () => void) => () => void;
|
|
121
|
+
effect?: (factory: () => void | (() => void), label?: string) => () => void;
|
|
122
|
+
inject?: (services: readonly string[], callback: (ctx: PluginContextLike) => void) => () => void;
|
|
70
123
|
}
|
|
71
124
|
//#endregion
|
|
72
125
|
//#region src/index.d.ts
|
|
@@ -78,14 +131,20 @@ export declare const __test: Readonly<{
|
|
|
78
131
|
PROVIDER: "openai-codex";
|
|
79
132
|
DISPLAY_NAME: "Codex";
|
|
80
133
|
DEFAULT_DISABLED_MODELS: readonly string[];
|
|
134
|
+
CODEX_USAGE_URL: "https://chatgpt.com/backend-api/wham/usage";
|
|
135
|
+
USAGE_ROUTE_PATH: "/api/codex-subscription/usage";
|
|
136
|
+
DEFAULT_USAGE_TTL_MS: 60000;
|
|
81
137
|
resolveCodexHome: typeof resolveCodexHome;
|
|
82
138
|
decodeJwtPayload: typeof decodeJwtPayload;
|
|
139
|
+
extractAccountId: typeof extractAccountId;
|
|
83
140
|
createAuthManager: typeof createAuthManager;
|
|
84
141
|
buildThinkingLevelMap: typeof buildThinkingLevelMap;
|
|
85
142
|
parseModelsCache: typeof parseModelsCache;
|
|
86
143
|
sanitizeCodexPayload: typeof sanitizeCodexPayload;
|
|
87
144
|
buildCodexHeaders: typeof buildCodexHeaders;
|
|
88
145
|
buildCodexUserAgent: typeof buildCodexUserAgent;
|
|
146
|
+
parseUsagePayload: typeof parseUsagePayload;
|
|
147
|
+
createUsageFetcher: typeof createUsageFetcher;
|
|
89
148
|
}>;
|
|
90
149
|
//#endregion
|
|
91
|
-
export type { CodexModelMetadata, ParsedModelsCatalog, PluginContextLike, RawModelsCache };
|
|
150
|
+
export type { CodexModelMetadata, CodexQuotaResponse, CodexQuotaSnapshot, CodexQuotaWindow, CodexResolvedCredentials, ParsedModelsCatalog, PluginContextLike, RawModelsCache };
|
package/lib/index.js
CHANGED
|
@@ -28,6 +28,15 @@ function decodeJwtPayload(token) {
|
|
|
28
28
|
return;
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
+
function extractAccountId(parsed, accessToken) {
|
|
32
|
+
const fileAccountId = parsed?.tokens?.account_id;
|
|
33
|
+
if (typeof fileAccountId === "string" && fileAccountId.trim() !== "") return fileAccountId.trim();
|
|
34
|
+
const authClaim = decodeJwtPayload(accessToken)?.["https://api.openai.com/auth"];
|
|
35
|
+
if (authClaim && typeof authClaim === "object" && !Array.isArray(authClaim)) {
|
|
36
|
+
const jwtAccountId = authClaim.chatgpt_account_id;
|
|
37
|
+
if (typeof jwtAccountId === "string" && jwtAccountId.trim() !== "") return jwtAccountId.trim();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
31
40
|
async function writeJsonAtomic(filePath, data) {
|
|
32
41
|
let targetPath = filePath;
|
|
33
42
|
try {
|
|
@@ -118,20 +127,35 @@ function createAuthManager(options) {
|
|
|
118
127
|
} catch (error) {
|
|
119
128
|
console.error(`[dsh-codex-subscription] 写回刷新后的 Codex auth.json 失败 (${authPath}): ${error instanceof Error ? error.message : String(error)}`);
|
|
120
129
|
}
|
|
121
|
-
return
|
|
130
|
+
return {
|
|
131
|
+
accessToken: fresh.access_token,
|
|
132
|
+
accountId: extractAccountId(nextAuth, fresh.access_token)
|
|
133
|
+
};
|
|
122
134
|
}
|
|
123
|
-
|
|
135
|
+
function toCredentials(state) {
|
|
136
|
+
return {
|
|
137
|
+
accessToken: state.accessToken,
|
|
138
|
+
accountId: extractAccountId(state.parsed, state.accessToken)
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
async function resolveCredentials() {
|
|
124
142
|
const current = readAuthState();
|
|
125
|
-
if (isTokenFresh(current.accessToken)) return current
|
|
143
|
+
if (isTokenFresh(current.accessToken)) return toCredentials(current);
|
|
126
144
|
if (!inflightRefresh) inflightRefresh = (async () => {
|
|
127
145
|
const latest = readAuthState();
|
|
128
|
-
if (isTokenFresh(latest.accessToken)) return latest
|
|
146
|
+
if (isTokenFresh(latest.accessToken)) return toCredentials(latest);
|
|
129
147
|
return refreshToken(latest.authPath, latest.parsed);
|
|
130
148
|
})().finally(() => {
|
|
131
149
|
inflightRefresh = void 0;
|
|
132
150
|
});
|
|
133
151
|
return inflightRefresh;
|
|
134
|
-
}
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
resolveCredentials,
|
|
155
|
+
async resolveAccessToken() {
|
|
156
|
+
return (await resolveCredentials()).accessToken;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
135
159
|
}
|
|
136
160
|
//#endregion
|
|
137
161
|
//#region src/catalog.ts
|
|
@@ -327,6 +351,108 @@ function sanitizeCodexPayload(payload, modelMeta) {
|
|
|
327
351
|
return cleaned;
|
|
328
352
|
}
|
|
329
353
|
//#endregion
|
|
354
|
+
//#region src/usage.ts
|
|
355
|
+
const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
356
|
+
const USAGE_ROUTE_PATH = "/api/codex-subscription/usage";
|
|
357
|
+
const DEFAULT_USAGE_TTL_MS = 6e4;
|
|
358
|
+
const USAGE_FETCH_TIMEOUT_MS = 15e3;
|
|
359
|
+
function clampPercent(value) {
|
|
360
|
+
return Math.max(0, Math.min(100, Math.round(value)));
|
|
361
|
+
}
|
|
362
|
+
function parseWindow(raw, fetchedAtMs) {
|
|
363
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
364
|
+
const record = raw;
|
|
365
|
+
const usedRaw = record.used_percent;
|
|
366
|
+
if (typeof usedRaw !== "number" || !Number.isFinite(usedRaw)) return null;
|
|
367
|
+
const resetAtRaw = record.reset_at;
|
|
368
|
+
const resetAfterRaw = record.reset_after_seconds;
|
|
369
|
+
let resetAt = null;
|
|
370
|
+
if (typeof resetAtRaw === "number" && Number.isFinite(resetAtRaw) && resetAtRaw > 0) resetAt = Math.round(resetAtRaw);
|
|
371
|
+
else if (typeof resetAfterRaw === "number" && Number.isFinite(resetAfterRaw) && resetAfterRaw >= 0) resetAt = Math.floor(fetchedAtMs / 1e3) + Math.round(resetAfterRaw);
|
|
372
|
+
return {
|
|
373
|
+
usedPercent: clampPercent(usedRaw),
|
|
374
|
+
resetAt
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
function parseUsagePayload(raw, fetchedAt) {
|
|
378
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
379
|
+
const record = raw;
|
|
380
|
+
const rateLimit = record.rate_limit;
|
|
381
|
+
if (!rateLimit || typeof rateLimit !== "object" || Array.isArray(rateLimit)) return null;
|
|
382
|
+
const rateRecord = rateLimit;
|
|
383
|
+
const primary = parseWindow(rateRecord.primary_window, fetchedAt);
|
|
384
|
+
if (primary === null) return null;
|
|
385
|
+
const secondary = parseWindow(rateRecord.secondary_window, fetchedAt);
|
|
386
|
+
return {
|
|
387
|
+
planType: typeof record.plan_type === "string" && record.plan_type.trim() !== "" ? record.plan_type.trim() : "plus",
|
|
388
|
+
limitReached: rateRecord.limit_reached === true || rateRecord.allowed === false || primary.usedPercent >= 100 || secondary !== null && secondary.usedPercent >= 100,
|
|
389
|
+
primary,
|
|
390
|
+
secondary,
|
|
391
|
+
fetchedAt
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
function createUsageFetcher(options) {
|
|
395
|
+
const fetchFn = options.fetchImpl ?? globalThis.fetch;
|
|
396
|
+
const nowFn = options.now ?? Date.now;
|
|
397
|
+
const ttlMs = options.ttlMs ?? 6e4;
|
|
398
|
+
let cached;
|
|
399
|
+
let stale = false;
|
|
400
|
+
let inflight;
|
|
401
|
+
async function fetchFresh() {
|
|
402
|
+
try {
|
|
403
|
+
const creds = await options.resolveCredentials();
|
|
404
|
+
const headers = options.buildHeaders({
|
|
405
|
+
Authorization: `Bearer ${creds.accessToken}`,
|
|
406
|
+
Accept: "application/json",
|
|
407
|
+
...creds.accountId !== void 0 ? { "ChatGPT-Account-Id": creds.accountId } : {}
|
|
408
|
+
});
|
|
409
|
+
const response = await fetchFn(CODEX_USAGE_URL, {
|
|
410
|
+
method: "GET",
|
|
411
|
+
headers,
|
|
412
|
+
signal: AbortSignal.timeout(USAGE_FETCH_TIMEOUT_MS)
|
|
413
|
+
});
|
|
414
|
+
if (!response.ok) return {
|
|
415
|
+
ok: false,
|
|
416
|
+
error: `Codex 限额查询失败 (HTTP ${response.status})`,
|
|
417
|
+
...cached !== void 0 ? { quota: cached } : {}
|
|
418
|
+
};
|
|
419
|
+
const parsed = parseUsagePayload(await response.json(), nowFn());
|
|
420
|
+
if (parsed === null) return {
|
|
421
|
+
ok: false,
|
|
422
|
+
error: "Codex 限额响应格式无法解析",
|
|
423
|
+
...cached !== void 0 ? { quota: cached } : {}
|
|
424
|
+
};
|
|
425
|
+
cached = parsed;
|
|
426
|
+
stale = false;
|
|
427
|
+
return {
|
|
428
|
+
ok: true,
|
|
429
|
+
quota: parsed
|
|
430
|
+
};
|
|
431
|
+
} catch (error) {
|
|
432
|
+
return {
|
|
433
|
+
ok: false,
|
|
434
|
+
error: error instanceof Error ? error.message : String(error),
|
|
435
|
+
...cached !== void 0 ? { quota: cached } : {}
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return {
|
|
440
|
+
invalidate() {
|
|
441
|
+
stale = true;
|
|
442
|
+
},
|
|
443
|
+
async getUsage(force = false) {
|
|
444
|
+
if (!force && !stale && cached !== void 0 && nowFn() - cached.fetchedAt < ttlMs) return {
|
|
445
|
+
ok: true,
|
|
446
|
+
quota: cached
|
|
447
|
+
};
|
|
448
|
+
if (!inflight) inflight = fetchFresh().finally(() => {
|
|
449
|
+
inflight = void 0;
|
|
450
|
+
});
|
|
451
|
+
return inflight;
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
//#endregion
|
|
330
456
|
//#region src/index.ts
|
|
331
457
|
const name = "codex-subscription";
|
|
332
458
|
const inject = ["llm"];
|
|
@@ -398,6 +524,10 @@ function apply(ctx, config = {}) {
|
|
|
398
524
|
}
|
|
399
525
|
};
|
|
400
526
|
}
|
|
527
|
+
const usageFetcher = createUsageFetcher({
|
|
528
|
+
resolveCredentials: () => authManager.resolveCredentials(),
|
|
529
|
+
buildHeaders: (authHeaders) => buildCodexHeaders(authHeaders, ensureSnapshot().catalog.clientVersion)
|
|
530
|
+
});
|
|
401
531
|
const piProvider = Object.freeze({
|
|
402
532
|
...baseProvider,
|
|
403
533
|
id: PROVIDER,
|
|
@@ -407,8 +537,14 @@ function apply(ctx, config = {}) {
|
|
|
407
537
|
apiKey: requestTokenAuth
|
|
408
538
|
}),
|
|
409
539
|
getModels: () => ensureSnapshot().catalog.models,
|
|
410
|
-
stream: (model, context, options) =>
|
|
411
|
-
|
|
540
|
+
stream: (model, context, options) => {
|
|
541
|
+
usageFetcher.invalidate();
|
|
542
|
+
return baseProvider.stream(model, context, wrapOptions(model, options));
|
|
543
|
+
},
|
|
544
|
+
streamSimple: (model, context, options) => {
|
|
545
|
+
usageFetcher.invalidate();
|
|
546
|
+
return baseProvider.streamSimple(model, context, wrapOptions(model, options));
|
|
547
|
+
}
|
|
412
548
|
});
|
|
413
549
|
function ensureSnapshot() {
|
|
414
550
|
const cachePath = join(getCodexHome(), "models_cache.json");
|
|
@@ -480,6 +616,28 @@ function apply(ctx, config = {}) {
|
|
|
480
616
|
}
|
|
481
617
|
}, (modelId) => ensureSnapshot().catalog.metadataById.get(modelId));
|
|
482
618
|
ctx.llm.registerAdapter([PROVIDER], adapter);
|
|
619
|
+
const mountUsageRoute = (connCtx) => {
|
|
620
|
+
const connection = connCtx.get?.("connection");
|
|
621
|
+
const registerFetch = connection?.fetch?.register;
|
|
622
|
+
if (typeof registerFetch !== "function") return;
|
|
623
|
+
const registerRoute = () => registerFetch.call(connection?.fetch, {
|
|
624
|
+
path: USAGE_ROUTE_PATH,
|
|
625
|
+
methods: ["GET"],
|
|
626
|
+
requestBody: "buffered",
|
|
627
|
+
fetch: async (request) => {
|
|
628
|
+
const force = new URL(request.url, "http://localhost").searchParams.get("force") === "1";
|
|
629
|
+
const result = await usageFetcher.getUsage(force);
|
|
630
|
+
return new Response(JSON.stringify(result), {
|
|
631
|
+
status: 200,
|
|
632
|
+
headers: { "content-type": "application/json" }
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
if (typeof connCtx.effect === "function") connCtx.effect(() => registerRoute(), "codex-subscription: usage route");
|
|
637
|
+
else registerRoute();
|
|
638
|
+
};
|
|
639
|
+
if (typeof ctx.inject === "function") ctx.inject(["connection"], mountUsageRoute);
|
|
640
|
+
else mountUsageRoute(ctx);
|
|
483
641
|
ctx.on?.("loader/volatile-update", () => {
|
|
484
642
|
ensureSnapshot();
|
|
485
643
|
ctx.llm.emitAdaptersUpdated?.();
|
|
@@ -489,14 +647,20 @@ const __test = Object.freeze({
|
|
|
489
647
|
PROVIDER,
|
|
490
648
|
DISPLAY_NAME,
|
|
491
649
|
DEFAULT_DISABLED_MODELS,
|
|
650
|
+
CODEX_USAGE_URL,
|
|
651
|
+
USAGE_ROUTE_PATH,
|
|
652
|
+
DEFAULT_USAGE_TTL_MS,
|
|
492
653
|
resolveCodexHome,
|
|
493
654
|
decodeJwtPayload,
|
|
655
|
+
extractAccountId,
|
|
494
656
|
createAuthManager,
|
|
495
657
|
buildThinkingLevelMap,
|
|
496
658
|
parseModelsCache,
|
|
497
659
|
sanitizeCodexPayload,
|
|
498
660
|
buildCodexHeaders,
|
|
499
|
-
buildCodexUserAgent
|
|
661
|
+
buildCodexUserAgent,
|
|
662
|
+
parseUsagePayload,
|
|
663
|
+
createUsageFetcher
|
|
500
664
|
});
|
|
501
665
|
//#endregion
|
|
502
666
|
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.
|
|
3
|
+
"version": "0.2.0",
|
|
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": {
|