@lyogvce/dsh-codex-subscription 0.0.0-stage → 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 +59 -2
- package/cordis.patch.yml +10 -0
- package/icon.svg +6 -0
- package/lib/client.js +426 -0
- package/lib/index.d.ts +150 -0
- package/lib/index.js +666 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +73 -4
package/README.md
CHANGED
|
@@ -1,3 +1,60 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-codex-subscription
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
为 **DeepSeek Harness 桌面版**(DSH Desktop `0.2.1+`)提供极简、零后台进程的 **Codex 订阅直连能力**。**自用**。
|
|
4
|
+
|
|
5
|
+
直接读取本机 `~/.codex/auth.json` 与 `~/.codex/models_cache.json`,在进程内通过 SSE 直连调用 ChatGPT Codex 订阅模型,彻底替代外部网关进程(如 Magpie)。
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 核心特性
|
|
10
|
+
|
|
11
|
+
1. **与本机 Codex CLI / Desktop 100% 共享登录态**
|
|
12
|
+
- 直接读取 `$CODEX_HOME/auth.json`(默认 `~/.codex/auth.json`)。
|
|
13
|
+
- 每次请求前检查 `access_token` 的 JWT `exp`:距离过期不足 5 分钟时自动通过单飞锁(Single-flight)向 `https://auth.openai.com/oauth/token` 刷新,并通过 `realpathSync` + 临时文件 `rename` 原子写回 `auth.json`,保证与 Codex CLI 永不互踢下线。
|
|
14
|
+
2. **零网络轮询的本地模型目录发现**
|
|
15
|
+
- 直接读取 `~/.codex/models_cache.json`(按文件 `mtime` 与大小自动感知更新),提取 `visibility === "list"` 的模型及其 `context_window`、`supported_reasoning_levels`(自动将 `ultra` 映射为 DSH 的 `max` 档位)与 `default_verbosity`。
|
|
16
|
+
3. **支持按配置屏蔽不想看到的模型(`disabledModels`)**
|
|
17
|
+
- 在插件配置中通过 `disabledModels` 屏蔽指定模型 `slug`,保持模型选择器干净清爽。
|
|
18
|
+
4. **原生多轮思考链回放与工具调用**
|
|
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 模型时自动隐藏。
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 安装与配置
|
|
26
|
+
|
|
27
|
+
### 1. 安装到本地 Profile
|
|
28
|
+
|
|
29
|
+
在 DSH 中通过插件管理器安装本地目录 `/Users/lijiechengbj/projects/AI-agents/dsh-codex-subscription`,或执行:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
dsh plugin --profile desktop add @lyogvce/dsh-codex-subscription
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 2. 配置屏蔽模型(`disabledModels`)
|
|
36
|
+
|
|
37
|
+
默认配置(见 [cordis.patch.yml](cordis.patch.yml))已屏蔽旧模型与备用模型,仅保留主力模型(如 `gpt-6.1-sol`、`gpt-6-astra`):
|
|
38
|
+
|
|
39
|
+
```yaml
|
|
40
|
+
- id: codex-subscription
|
|
41
|
+
name: "@lyogvce/dsh-codex-subscription"
|
|
42
|
+
config:
|
|
43
|
+
disabledModels:
|
|
44
|
+
- gpt-6-sol
|
|
45
|
+
- gpt-6-luna
|
|
46
|
+
- gpt-5.6-sol
|
|
47
|
+
- gpt-5.6-terra
|
|
48
|
+
- gpt-5.6-luna
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
可在 DSH 的 **设置 → 插件 → Codex 订阅直连** 页面或 `cordis.patch.yml` 中随时修改 `disabledModels`,实时生效无需重启。
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## 开发与验证
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pnpm install
|
|
59
|
+
pnpm verify # 依次执行 tsc --noEmit、tsdown 构建、node --test 单测与产物卫生守卫、隔离脚本检查
|
|
60
|
+
```
|
package/cordis.patch.yml
ADDED
package/icon.svg
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
|
2
|
+
<rect width="64" height="64" rx="16" fill="#0F172A"/>
|
|
3
|
+
<path d="M22 22L30 32L22 42" stroke="#38BDF8" stroke-width="4.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
4
|
+
<path d="M34 42H44" stroke="#E2E8F0" stroke-width="4.5" stroke-linecap="round"/>
|
|
5
|
+
<circle cx="46" cy="20" r="4" fill="#34D399"/>
|
|
6
|
+
</svg>
|
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
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { PiCodexModel } from "@earendil-works/pi-ai/providers/openai-codex";
|
|
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
|
+
}
|
|
15
|
+
interface JwtClaims {
|
|
16
|
+
exp?: number;
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
}
|
|
19
|
+
interface AuthManagerOptions {
|
|
20
|
+
getCodexHome: () => string;
|
|
21
|
+
fetchImpl?: typeof fetch | undefined;
|
|
22
|
+
now?: (() => number) | undefined;
|
|
23
|
+
}
|
|
24
|
+
declare function resolveCodexHome(envCodexHome?: string | undefined): string;
|
|
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
|
+
}
|
|
31
|
+
declare function createAuthManager(options: AuthManagerOptions): {
|
|
32
|
+
resolveCredentials: () => Promise<CodexResolvedCredentials>;
|
|
33
|
+
resolveAccessToken: () => Promise<string>;
|
|
34
|
+
};
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/catalog.d.ts
|
|
37
|
+
interface RawReasoningLevel {
|
|
38
|
+
effort?: string;
|
|
39
|
+
description?: string;
|
|
40
|
+
}
|
|
41
|
+
interface RawCodexModelEntry {
|
|
42
|
+
slug?: string;
|
|
43
|
+
display_name?: string;
|
|
44
|
+
description?: string;
|
|
45
|
+
visibility?: string;
|
|
46
|
+
priority?: number;
|
|
47
|
+
context_window?: number;
|
|
48
|
+
max_context_window?: number;
|
|
49
|
+
input_modalities?: unknown[];
|
|
50
|
+
supported_reasoning_levels?: Array<string | RawReasoningLevel>;
|
|
51
|
+
default_reasoning_level?: string;
|
|
52
|
+
support_verbosity?: boolean;
|
|
53
|
+
default_verbosity?: string;
|
|
54
|
+
}
|
|
55
|
+
interface RawModelsCache {
|
|
56
|
+
client_version?: string;
|
|
57
|
+
models?: RawCodexModelEntry[];
|
|
58
|
+
}
|
|
59
|
+
interface CodexModelMetadata {
|
|
60
|
+
readonly supportVerbosity: boolean;
|
|
61
|
+
readonly defaultVerbosity: 'low' | 'medium' | 'high';
|
|
62
|
+
readonly defaultReasoningLevel?: string | undefined;
|
|
63
|
+
}
|
|
64
|
+
interface ParsedModelsCatalog {
|
|
65
|
+
readonly clientVersion: string;
|
|
66
|
+
readonly models: PiCodexModel[];
|
|
67
|
+
readonly metadataById: ReadonlyMap<string, CodexModelMetadata>;
|
|
68
|
+
}
|
|
69
|
+
declare function buildThinkingLevelMap(supportedLevels: unknown): Record<string, string | null>;
|
|
70
|
+
declare function parseModelsCache(rawCache: RawModelsCache | null | undefined, baseModels: readonly PiCodexModel[], disabledModels?: readonly string[]): ParsedModelsCatalog;
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region src/request.d.ts
|
|
73
|
+
declare function buildCodexUserAgent(version?: string): string;
|
|
74
|
+
declare function buildCodexHeaders(existingHeaders: Record<string, string> | undefined, clientVersion: string, sessionId?: string | undefined): Record<string, string>;
|
|
75
|
+
declare function sanitizeCodexPayload(payload: Record<string, unknown> | null | undefined, modelMeta?: CodexModelMetadata | undefined): Record<string, unknown> | null | undefined;
|
|
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
|
|
110
|
+
//#region src/host-types.d.ts
|
|
111
|
+
interface LlmServiceLike {
|
|
112
|
+
registerAdapter: (providers: string[], adapter: unknown) => (() => void) & {
|
|
113
|
+
replace?: (next: string[]) => void;
|
|
114
|
+
};
|
|
115
|
+
emitAdaptersUpdated?: () => void;
|
|
116
|
+
}
|
|
117
|
+
interface PluginContextLike {
|
|
118
|
+
llm: LlmServiceLike;
|
|
119
|
+
get?: (name: string) => unknown;
|
|
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;
|
|
123
|
+
}
|
|
124
|
+
//#endregion
|
|
125
|
+
//#region src/index.d.ts
|
|
126
|
+
export declare const name = "codex-subscription";
|
|
127
|
+
export declare const inject: string[];
|
|
128
|
+
export declare const Config: any;
|
|
129
|
+
export declare function apply(ctx: PluginContextLike, config?: Record<string, unknown>): void;
|
|
130
|
+
export declare const __test: Readonly<{
|
|
131
|
+
PROVIDER: "openai-codex";
|
|
132
|
+
DISPLAY_NAME: "Codex";
|
|
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;
|
|
137
|
+
resolveCodexHome: typeof resolveCodexHome;
|
|
138
|
+
decodeJwtPayload: typeof decodeJwtPayload;
|
|
139
|
+
extractAccountId: typeof extractAccountId;
|
|
140
|
+
createAuthManager: typeof createAuthManager;
|
|
141
|
+
buildThinkingLevelMap: typeof buildThinkingLevelMap;
|
|
142
|
+
parseModelsCache: typeof parseModelsCache;
|
|
143
|
+
sanitizeCodexPayload: typeof sanitizeCodexPayload;
|
|
144
|
+
buildCodexHeaders: typeof buildCodexHeaders;
|
|
145
|
+
buildCodexUserAgent: typeof buildCodexUserAgent;
|
|
146
|
+
parseUsagePayload: typeof parseUsagePayload;
|
|
147
|
+
createUsageFetcher: typeof createUsageFetcher;
|
|
148
|
+
}>;
|
|
149
|
+
//#endregion
|
|
150
|
+
export type { CodexModelMetadata, CodexQuotaResponse, CodexQuotaSnapshot, CodexQuotaWindow, CodexResolvedCredentials, ParsedModelsCatalog, PluginContextLike, RawModelsCache };
|