@eddyskywalker/dsh-chatgpt-subscription 0.1.5 → 0.1.6
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/CHANGELOG.md +7 -0
- package/README.md +30 -15
- package/lib/client.js +34 -249
- package/lib/client.js.map +1 -1
- package/lib/index.js +271 -16
- package/lib/types/client/CodexSubscriptionSection.d.ts +3 -1
- package/lib/types/client/CodexSubscriptionSection.d.ts.map +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/locales.d.ts +17 -5
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/styles.d.ts.map +1 -1
- package/lib/types/host/oauth-service.d.ts.map +1 -1
- package/lib/types/host/platform-token-store.d.ts +3 -0
- package/lib/types/host/platform-token-store.d.ts.map +1 -0
- package/lib/types/host/subagent-report-scheduling-compat.d.ts +21 -0
- package/lib/types/host/subagent-report-scheduling-compat.d.ts.map +1 -0
- package/lib/types/host/token-store-linux.d.ts +20 -0
- package/lib/types/host/token-store-linux.d.ts.map +1 -0
- package/lib/types/host/token-store-windows.d.ts +4 -0
- package/lib/types/host/token-store-windows.d.ts.map +1 -1
- package/lib/types/host/token-store.d.ts +6 -0
- package/lib/types/host/token-store.d.ts.map +1 -1
- package/lib/types/index.d.ts +3 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/shared/contracts.d.ts +7 -4
- package/lib/types/shared/contracts.d.ts.map +1 -1
- package/package.json +3 -1
- package/lib/types/client/process-folding.d.ts +0 -5
- package/lib/types/client/process-folding.d.ts.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.6 - 2026-08-17
|
|
4
|
+
|
|
5
|
+
- 临时兼容 DSH `0.1.0-rc.6` 的子代理报告去重:当 `subagent-settled` 已携带与同一子代理排队 `subagent-report` 完全相同的最终内容时,删除重复报告,保留 settlement 通知;部分报告、不同内容和不同子代理不受影响。代码统一标记为 `DSH_COMPAT_REMOVE(subagent-report-settlement-dedup)`,待 DSH 原生修复后可整体移除。
|
|
6
|
+
- 新增 Linux Host 支持:平台凭据存储选择、用户私有 `0600` TokenStore,以及 Bash/POSIX shell 工具调用兼容说明与测试。
|
|
7
|
+
- 设置页现在根据 Host 返回值展示 Windows DPAPI 或 Linux 文件存储及其真实安全边界。
|
|
8
|
+
- 移除与 Codex 订阅接入无关的执行过程详情折叠功能。
|
|
9
|
+
|
|
3
10
|
## 0.1.2 - 2026-08-16
|
|
4
11
|
|
|
5
12
|
- 优化过程详情折叠(Process folding)的初始化时机与节点匹配探测逻辑。
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
让 DSH(DeepSeek Harness)通过 ChatGPT 订阅使用 Gpt 系列模型的插件。
|
|
4
4
|
|
|
5
|
-
插件注册 `codex-chatgpt` Provider(显示名 **“Codex(ChatGPT 订阅)”**),以当前
|
|
5
|
+
插件注册 `codex-chatgpt` Provider(显示名 **“Codex(ChatGPT 订阅)”**),以当前 Host 用户的 ChatGPT OAuth 登录态访问模型,并在设置页展示账号信息、连接状态与订阅额度。支持 Windows 与 Linux。
|
|
6
6
|
|
|
7
7
|
## 目录
|
|
8
8
|
|
|
@@ -23,13 +23,16 @@
|
|
|
23
23
|
|
|
24
24
|
- Authorization Code + PKCE(S256)登录,一次性 localhost 回调;
|
|
25
25
|
- 支持 token 刷新、登录取消与账号注销;
|
|
26
|
-
-
|
|
26
|
+
- Windows 使用 CurrentUser DPAPI 加密存储 token;Linux 使用当前用户独占的 `0600` 文件存储;明文不发送给 Client;
|
|
27
|
+
- 设置页会明确显示当前存储类型,并在 Linux 上提示文件存储未额外加密。
|
|
27
28
|
|
|
28
29
|
**模型接入**
|
|
29
30
|
|
|
30
31
|
- 固定 Codex Responses 地址,支持流式文本、reasoning summary、图片输入与工具调用/结果;
|
|
32
|
+
- 原样转发 DSH 暴露的工具 schema;命令工具兼容 `pwsh` / `powershell`、`bash`、`sh` 与 `shell`,并按 PowerShell、Bash 或 POSIX sh 注入对应说明;
|
|
31
33
|
- 429/5xx 由 DSH retry policy 接管;401 只强制刷新并重试一次,支持 `AbortSignal`;
|
|
32
|
-
- Codex 与 Code review 多窗口额度,60 秒缓存、15 秒上游节流并遵守 `Retry-After
|
|
34
|
+
- Codex 与 Code review 多窗口额度,60 秒缓存、15 秒上游节流并遵守 `Retry-After`;
|
|
35
|
+
- 临时兼容 DSH `0.1.0-rc.6` 的子代理最终报告去重:若 settlement 已向父 Agent 提供同一子代理、完全相同的最终内容,则删除仍在 inbox 中的重复 report;部分报告、不同内容和不同子代理不受影响。兼容代码以 `DSH_COMPAT_REMOVE(subagent-report-settlement-dedup)` 集中标记,待 DSH 原生修复后删除。
|
|
33
36
|
|
|
34
37
|
**设置页**
|
|
35
38
|
|
|
@@ -44,8 +47,10 @@
|
|
|
44
47
|
|
|
45
48
|
## 环境要求
|
|
46
49
|
|
|
47
|
-
- Windows
|
|
48
|
-
-
|
|
50
|
+
- Windows 或 Linux;
|
|
51
|
+
- Windows:系统需提供 Windows PowerShell,以使用 CurrentUser DPAPI;
|
|
52
|
+
- Linux:Host 用户必须拥有可写的 `~/.dsh`(或 `$DSH_HOME`),凭据文件会强制使用 `0600`、目录使用 `0700`;
|
|
53
|
+
- 已安装 DSH;
|
|
49
54
|
- Node.js 与 npm。
|
|
50
55
|
|
|
51
56
|
## 安装
|
|
@@ -54,7 +59,8 @@
|
|
|
54
59
|
|
|
55
60
|
直接从 npm 安装已发布的插件包:
|
|
56
61
|
|
|
57
|
-
```
|
|
62
|
+
```sh
|
|
63
|
+
# Windows PowerShell、Bash 和 POSIX sh 均可执行
|
|
58
64
|
# 如果全局安装了 dsh
|
|
59
65
|
dsh plugin --profile web add @eddyskywalker/dsh-chatgpt-subscription
|
|
60
66
|
|
|
@@ -72,11 +78,16 @@ npx @deepseek-ai/dsh plugin --profile web add @eddyskywalker/dsh-chatgpt-subscri
|
|
|
72
78
|
|
|
73
79
|
如需进行二次开发或本地源码调试:
|
|
74
80
|
|
|
75
|
-
```
|
|
81
|
+
```sh
|
|
76
82
|
git clone https://github.com/Aa728848/dsh-chatgpt-subscription.git
|
|
77
83
|
cd dsh-chatgpt-subscription
|
|
78
84
|
npm install
|
|
79
85
|
npm run build
|
|
86
|
+
|
|
87
|
+
# Linux
|
|
88
|
+
npx @deepseek-ai/dsh plugin --profile web add "link:/absolute/path/to/dsh-chatgpt-subscription"
|
|
89
|
+
|
|
90
|
+
# Windows PowerShell
|
|
80
91
|
npx @deepseek-ai/dsh plugin --profile web add "link:C:\absolute\path\to\dsh-chatgpt-subscription"
|
|
81
92
|
```
|
|
82
93
|
|
|
@@ -89,22 +100,23 @@ npx @deepseek-ai/dsh plugin --profile web add "link:C:\absolute\path\to\dsh-chat
|
|
|
89
100
|
|
|
90
101
|
DSH 模型选择器应显示 **“Codex(ChatGPT 订阅)”**。
|
|
91
102
|
|
|
92
|
-
卸载前建议先在设置页点击
|
|
93
|
-
|
|
103
|
+
卸载前建议先在设置页点击 **“注销”**,它会删除当前平台的凭据和 Host 内存中的额度缓存。
|
|
94
104
|
|
|
95
|
-
若 DSH
|
|
105
|
+
若 DSH 已异常退出,可在确认路径后手动处理凭据文件:
|
|
96
106
|
|
|
97
|
-
-
|
|
98
|
-
-
|
|
107
|
+
- Windows:`%DSH_HOME%\storages\dsh-chatgpt-subscription\oauth.dpapi`,未设置 `DSH_HOME` 时为 `%USERPROFILE%\.dsh\storages\dsh-chatgpt-subscription\oauth.dpapi`;
|
|
108
|
+
- Linux:`$DSH_HOME/storages/dsh-chatgpt-subscription/oauth.json`,未设置 `DSH_HOME` 时为 `~/.dsh/storages/dsh-chatgpt-subscription/oauth.json`。
|
|
99
109
|
|
|
100
|
-
>
|
|
110
|
+
> Windows 文件只能由创建它的用户通过 DPAPI 解密。Linux 文件是未额外加密的 JSON,依赖目录 `0700` 和文件 `0600` 隔离;不要复制、打印或提交该文件。跨平台迁移需要重新登录。
|
|
101
111
|
|
|
102
112
|
## 安全边界
|
|
103
113
|
|
|
104
114
|
- OAuth 回调固定为 `http://localhost:1455/auth/callback`,登录任务五分钟超时,同一时刻只允许一个;
|
|
105
115
|
- OAuth token 只发送到 `https://auth.openai.com/oauth/token`;
|
|
106
116
|
- 模型与额度地址分别固定为 `https://chatgpt.com/backend-api/codex/responses` 和 `https://chatgpt.com/backend-api/wham/usage`,没有 endpoint override;
|
|
107
|
-
- token bundle 使用
|
|
117
|
+
- Windows token bundle 使用 CurrentUser DPAPI;明文只经过 Host 内存和 PowerShell stdin/stdout;
|
|
118
|
+
- Linux token bundle 原子写入当前用户私有文件并在读取时校验普通文件、所有者与 `0600` 权限,同时拒绝符号链接;该文件没有应用层加密,同 UID 进程、root、备份和磁盘快照仍可读取;
|
|
119
|
+
- 两个平台的 token 都不会进入浏览器、`settings.yaml` 或日志;
|
|
108
120
|
- 工具调用只有在参数是完整 JSON 对象时才产生可执行的 `block-end`;畸形参数终止本次生成;
|
|
109
121
|
- 所有修改状态的路由只接受同源 JSON POST,并校验 `Origin` 与 `Host`。
|
|
110
122
|
|
|
@@ -127,7 +139,7 @@ DSH 模型选择器应显示 **“Codex(ChatGPT 订阅)”**。
|
|
|
127
139
|
|
|
128
140
|
## 开发与验证
|
|
129
141
|
|
|
130
|
-
```
|
|
142
|
+
```sh
|
|
131
143
|
npm run typecheck
|
|
132
144
|
npm test
|
|
133
145
|
npm run build
|
|
@@ -146,3 +158,6 @@ npm pack --dry-run
|
|
|
146
158
|
| 429 | 插件遵守 `Retry-After`,不会高频轮询;模型请求由 DSH retry policy 有界重试 |
|
|
147
159
|
| 模型不可用 | 检查 ChatGPT 套餐、workspace 权限与当前模型可用性 |
|
|
148
160
|
| DPAPI 读取失败 | 确认 DSH 以创建凭据时的同一 Windows 用户运行;必要时清理凭据后重新登录 |
|
|
161
|
+
| Linux 凭据存储不可用 | 确认凭据属于当前用户且权限为 `0600`,父目录权限为 `0700`;修复权限或注销后重新登录 |
|
|
162
|
+
| Linux 上工具调用语法错误 | 确认 DSH 暴露的是 `bash`、`sh` 或 `shell`,并使用相应的 Bash/POSIX 语法与 `/` 路径 |
|
|
163
|
+
| 父 Agent 已收到子代理结果,但相同报告仍显示为排队消息 | 本插件为 DSH `0.1.0-rc.6` 提供临时 report/settlement 精确去重;升级或重载插件并新建会话后验证。若未来 DSH 已原生修复,可移除所有 `DSH_COMPAT_REMOVE(subagent-report-settlement-dedup)` 标记代码 |
|
package/lib/client.js
CHANGED
|
@@ -247,11 +247,11 @@ window.__ModuleLoader__.load({
|
|
|
247
247
|
] }) : null,
|
|
248
248
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoRow, {
|
|
249
249
|
label: t("storage"),
|
|
250
|
-
value: t
|
|
250
|
+
value: storageLabel(status?.storage, t)
|
|
251
251
|
}),
|
|
252
252
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
253
253
|
className: "dsh-codex-notice",
|
|
254
|
-
children: t
|
|
254
|
+
children: storageNotice(status?.storage, t)
|
|
255
255
|
}),
|
|
256
256
|
status?.login.active ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
257
257
|
className: "dsh-codex-muted",
|
|
@@ -277,7 +277,7 @@ window.__ModuleLoader__.load({
|
|
|
277
277
|
children: t("cancel")
|
|
278
278
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
279
279
|
primary: true,
|
|
280
|
-
disabled: busy !== null,
|
|
280
|
+
disabled: busy !== null || status?.storage.available === false,
|
|
281
281
|
onClick: startLogin,
|
|
282
282
|
children: status?.authenticated ? t("signInAgain") : t("signIn")
|
|
283
283
|
}), status?.authenticated ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
@@ -405,6 +405,20 @@ window.__ModuleLoader__.load({
|
|
|
405
405
|
})]
|
|
406
406
|
});
|
|
407
407
|
}
|
|
408
|
+
function storageLabel(storage, t) {
|
|
409
|
+
if (storage === void 0 || !storage.available) return t("storageUnavailable");
|
|
410
|
+
if (storage.kind === "windows-dpapi") return t("storageWindows");
|
|
411
|
+
if (storage.kind === "linux-file") return t("storageLinuxFile");
|
|
412
|
+
if (storage.kind === "memory") return t("storageMemory");
|
|
413
|
+
return t("storageUnavailable");
|
|
414
|
+
}
|
|
415
|
+
function storageNotice(storage, t) {
|
|
416
|
+
if (storage === void 0 || !storage.available) return t("securityUnavailable");
|
|
417
|
+
if (storage.kind === "windows-dpapi") return t("securityWindows");
|
|
418
|
+
if (storage.kind === "linux-file") return t("securityLinuxFile");
|
|
419
|
+
if (storage.kind === "memory") return t("securityMemory");
|
|
420
|
+
return t("securityUnavailable");
|
|
421
|
+
}
|
|
408
422
|
function QuotaBucket({ bucket, t }) {
|
|
409
423
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("article", {
|
|
410
424
|
className: "dsh-codex-quota-card",
|
|
@@ -504,8 +518,14 @@ window.__ModuleLoader__.load({
|
|
|
504
518
|
accountId: "账号 ID",
|
|
505
519
|
expires: "令牌到期",
|
|
506
520
|
storage: "凭据存储",
|
|
507
|
-
|
|
508
|
-
|
|
521
|
+
storageWindows: "Windows DPAPI(当前用户加密)",
|
|
522
|
+
storageLinuxFile: "Linux 用户私有文件(权限 0600)",
|
|
523
|
+
storageMemory: "仅 Host 内存(不持久化)",
|
|
524
|
+
storageUnavailable: "凭据存储不可用",
|
|
525
|
+
securityWindows: "令牌由 Host 使用 Windows CurrentUser DPAPI 加密,不会进入浏览器、settings.yaml 或日志。",
|
|
526
|
+
securityLinuxFile: "令牌仅写入 Host 上当前 Linux 用户可读的 0600 文件,但不会额外加密;同 UID 进程、root、备份和磁盘快照仍可能读取。",
|
|
527
|
+
securityMemory: "令牌仅保留在 Host 内存中,Host 退出后丢失。",
|
|
528
|
+
securityUnavailable: "Host 无法安全访问凭据存储;请修复文件所有者或权限后重试。",
|
|
509
529
|
signIn: "使用 ChatGPT 登录",
|
|
510
530
|
signInAgain: "重新登录",
|
|
511
531
|
cancel: "取消登录",
|
|
@@ -552,8 +572,14 @@ window.__ModuleLoader__.load({
|
|
|
552
572
|
accountId: "Account ID",
|
|
553
573
|
expires: "Token expires",
|
|
554
574
|
storage: "Credential storage",
|
|
555
|
-
|
|
556
|
-
|
|
575
|
+
storageWindows: "Windows DPAPI (current-user encrypted)",
|
|
576
|
+
storageLinuxFile: "Linux user-private file (mode 0600)",
|
|
577
|
+
storageMemory: "Host memory only (not persistent)",
|
|
578
|
+
storageUnavailable: "Credential storage unavailable",
|
|
579
|
+
securityWindows: "The Host encrypts tokens with Windows CurrentUser DPAPI; they never enter the browser, settings.yaml, or logs.",
|
|
580
|
+
securityLinuxFile: "Tokens are stored in a mode-0600 file readable only by the current Linux user, but are not additionally encrypted; same-UID processes, root, backups, and disk snapshots can still read them.",
|
|
581
|
+
securityMemory: "Tokens remain only in Host memory and are lost when the Host exits.",
|
|
582
|
+
securityUnavailable: "The Host cannot safely access credential storage. Fix the file owner or permissions and retry.",
|
|
557
583
|
signIn: "Sign in with ChatGPT",
|
|
558
584
|
signInAgain: "Sign in again",
|
|
559
585
|
cancel: "Cancel sign-in",
|
|
@@ -592,239 +618,6 @@ window.__ModuleLoader__.load({
|
|
|
592
618
|
}
|
|
593
619
|
};
|
|
594
620
|
//#endregion
|
|
595
|
-
//#region src/client/process-folding.ts
|
|
596
|
-
const ROW_CLASS = "dsh-codex-process-row";
|
|
597
|
-
const GROUP_HEAD_CLASS = "dsh-codex-process-group-head";
|
|
598
|
-
const GROUP_COLLAPSED_CLASS = "dsh-codex-process-group-collapsed";
|
|
599
|
-
const GROUP_HIDDEN_CLASS = "dsh-codex-process-group-hidden";
|
|
600
|
-
const USER_TOGGLED_ATTR = "data-dsh-codex-process-user-toggled";
|
|
601
|
-
const TITLE_ATTR = "data-dsh-codex-process-title";
|
|
602
|
-
const TITLE = "Click to expand or collapse process details";
|
|
603
|
-
const DEFAULT_AUTO_COLLAPSE_MS = 350;
|
|
604
|
-
const MAX_PROCESS_LABELS_PER_ROW = 3;
|
|
605
|
-
const MAX_SCAN_ROOTS_PER_FRAME = 80;
|
|
606
|
-
const MAX_TEXT_SAMPLE = 2e4;
|
|
607
|
-
const PROCESS_CANDIDATE_SELECTOR = "article,section,li,div,p,span,button,h1,h2,h3,h4,h5,h6,[role=\"button\"],[role=\"listitem\"]";
|
|
608
|
-
const PROCESS_PREFIX_PATTERN = "(?:上下文注入|Think|Search|Code|代码|Pwsh|PowerShell|Bash|Shell|Read|Glob|Grep|Web(?:\\s+Search)?|Tool|工具|思考)";
|
|
609
|
-
const COMMAND_PATTERN = /(?:Code|代码|Pwsh|PowerShell|Bash|Shell|Read|Glob|Grep|Tool|工具)/i;
|
|
610
|
-
const SEARCH_PATTERN = /(?:Search|Web\s+Search|搜索)/i;
|
|
611
|
-
const PROCESS_PREFIX_RE = new RegExp(`^\\s*(?:[•●◦▪▫·#-]\\s*)?${PROCESS_PREFIX_PATTERN}\\s*(?:[·::-]|$)`, "i");
|
|
612
|
-
const PROCESS_LINE_RE = new RegExp(`(?:^|\\n)\\s*(?:[•●◦▪▫·#-]\\s*)?${PROCESS_PREFIX_PATTERN}\\s*(?:[·::-]|$)`, "gi");
|
|
613
|
-
function installProcessFolding(options = {}) {
|
|
614
|
-
if (typeof document === "undefined") return () => void 0;
|
|
615
|
-
if (document.body === null) {
|
|
616
|
-
let dispose = () => void 0;
|
|
617
|
-
let started = false;
|
|
618
|
-
const start = () => {
|
|
619
|
-
if (started) return;
|
|
620
|
-
started = true;
|
|
621
|
-
dispose = installProcessFolding(options);
|
|
622
|
-
};
|
|
623
|
-
document.addEventListener("DOMContentLoaded", start, { once: true });
|
|
624
|
-
return () => {
|
|
625
|
-
document.removeEventListener("DOMContentLoaded", start);
|
|
626
|
-
dispose();
|
|
627
|
-
};
|
|
628
|
-
}
|
|
629
|
-
const autoCollapseMs = options.autoCollapseMs ?? DEFAULT_AUTO_COLLAPSE_MS;
|
|
630
|
-
const groups = /* @__PURE__ */ new Map();
|
|
631
|
-
const pending = /* @__PURE__ */ new Set();
|
|
632
|
-
let frame = null;
|
|
633
|
-
const scheduleFlush = () => {
|
|
634
|
-
if (frame !== null) return;
|
|
635
|
-
frame = requestAnimationFrame(() => {
|
|
636
|
-
frame = null;
|
|
637
|
-
flush();
|
|
638
|
-
});
|
|
639
|
-
};
|
|
640
|
-
const queue = (element) => {
|
|
641
|
-
if (element === null || !element.isConnected) return;
|
|
642
|
-
pending.add(element);
|
|
643
|
-
scheduleFlush();
|
|
644
|
-
};
|
|
645
|
-
const scheduleAutoCollapse = (head) => {
|
|
646
|
-
const state = groups.get(head);
|
|
647
|
-
if (state === void 0 || head.hasAttribute(USER_TOGGLED_ATTR)) return;
|
|
648
|
-
if (state.timer !== null) clearTimeout(state.timer);
|
|
649
|
-
state.timer = setTimeout(() => {
|
|
650
|
-
state.timer = null;
|
|
651
|
-
if (head.isConnected && !head.hasAttribute(USER_TOGGLED_ATTR)) setGroupCollapsed(head, true);
|
|
652
|
-
}, autoCollapseMs);
|
|
653
|
-
};
|
|
654
|
-
const syncGroup = (rows, previous) => {
|
|
655
|
-
const head = rows[0];
|
|
656
|
-
const title = titleForProcessRows(rows);
|
|
657
|
-
const click = (event) => {
|
|
658
|
-
if (isInteractiveTarget(event.target, head)) return;
|
|
659
|
-
markUserToggled(head);
|
|
660
|
-
setGroupCollapsed(head, !head.classList.contains(GROUP_COLLAPSED_CLASS));
|
|
661
|
-
};
|
|
662
|
-
const keydown = (event) => {
|
|
663
|
-
if (event.key !== "Enter" && event.key !== " ") return;
|
|
664
|
-
event.preventDefault();
|
|
665
|
-
markUserToggled(head);
|
|
666
|
-
setGroupCollapsed(head, !head.classList.contains(GROUP_COLLAPSED_CLASS));
|
|
667
|
-
};
|
|
668
|
-
groups.set(head, {
|
|
669
|
-
rows,
|
|
670
|
-
click,
|
|
671
|
-
keydown,
|
|
672
|
-
timer: null
|
|
673
|
-
});
|
|
674
|
-
for (const row of rows) row.classList.add(ROW_CLASS);
|
|
675
|
-
head.classList.add(GROUP_HEAD_CLASS);
|
|
676
|
-
if (!head.hasAttribute("tabindex")) head.tabIndex = 0;
|
|
677
|
-
head.setAttribute("aria-expanded", String(!(previous?.collapsed ?? false)));
|
|
678
|
-
head.setAttribute(TITLE_ATTR, title);
|
|
679
|
-
head.title ||= TITLE;
|
|
680
|
-
if (previous?.userToggled === true) head.setAttribute(USER_TOGGLED_ATTR, "true");
|
|
681
|
-
head.addEventListener("click", click);
|
|
682
|
-
head.addEventListener("keydown", keydown);
|
|
683
|
-
setGroupCollapsed(head, previous?.collapsed ?? false);
|
|
684
|
-
scheduleAutoCollapse(head);
|
|
685
|
-
};
|
|
686
|
-
const rebuildParentGroups = (parent) => {
|
|
687
|
-
const previous = /* @__PURE__ */ new Map();
|
|
688
|
-
for (const [head, state] of groups) {
|
|
689
|
-
if (head.parentElement !== parent) continue;
|
|
690
|
-
previous.set(head, {
|
|
691
|
-
collapsed: head.classList.contains(GROUP_COLLAPSED_CLASS),
|
|
692
|
-
userToggled: head.hasAttribute(USER_TOGGLED_ATTR)
|
|
693
|
-
});
|
|
694
|
-
cleanupGroup(head, state);
|
|
695
|
-
groups.delete(head);
|
|
696
|
-
}
|
|
697
|
-
let rows = [];
|
|
698
|
-
const flushRows = () => {
|
|
699
|
-
if (rows.length > 0) syncGroup(rows, previous.get(rows[0]));
|
|
700
|
-
rows = [];
|
|
701
|
-
};
|
|
702
|
-
for (const child of Array.from(parent.children)) if (child instanceof HTMLElement && isProcessRow(child)) rows.push(child);
|
|
703
|
-
else flushRows();
|
|
704
|
-
flushRows();
|
|
705
|
-
};
|
|
706
|
-
const flush = () => {
|
|
707
|
-
const parents = /* @__PURE__ */ new Set();
|
|
708
|
-
let scanned = 0;
|
|
709
|
-
for (const root of Array.from(pending)) {
|
|
710
|
-
pending.delete(root);
|
|
711
|
-
if (!root.isConnected) continue;
|
|
712
|
-
for (const row of findProcessRows(root)) if (row.parentElement !== null) parents.add(row.parentElement);
|
|
713
|
-
const grouped = nearestGroupHead(root);
|
|
714
|
-
if (grouped !== null && grouped.parentElement !== null) parents.add(grouped.parentElement);
|
|
715
|
-
scanned++;
|
|
716
|
-
if (scanned >= MAX_SCAN_ROOTS_PER_FRAME && pending.size > 0) {
|
|
717
|
-
scheduleFlush();
|
|
718
|
-
break;
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
for (const parent of parents) rebuildParentGroups(parent);
|
|
722
|
-
};
|
|
723
|
-
const observer = new MutationObserver((mutations) => {
|
|
724
|
-
for (const mutation of mutations) {
|
|
725
|
-
if (mutation.type === "characterData") {
|
|
726
|
-
queue(nodeElement(mutation.target)?.closest(PROCESS_CANDIDATE_SELECTOR) ?? nodeElement(mutation.target));
|
|
727
|
-
continue;
|
|
728
|
-
}
|
|
729
|
-
for (const node of mutation.addedNodes) queue(nodeElement(node));
|
|
730
|
-
}
|
|
731
|
-
});
|
|
732
|
-
observer.observe(document.body, {
|
|
733
|
-
childList: true,
|
|
734
|
-
characterData: true,
|
|
735
|
-
subtree: true
|
|
736
|
-
});
|
|
737
|
-
queue(document.body);
|
|
738
|
-
return () => {
|
|
739
|
-
observer.disconnect();
|
|
740
|
-
if (frame !== null) cancelAnimationFrame(frame);
|
|
741
|
-
pending.clear();
|
|
742
|
-
for (const [head, state] of groups) cleanupGroup(head, state);
|
|
743
|
-
groups.clear();
|
|
744
|
-
};
|
|
745
|
-
}
|
|
746
|
-
function cleanupGroup(head, state) {
|
|
747
|
-
if (state.timer !== null) clearTimeout(state.timer);
|
|
748
|
-
head.removeEventListener("click", state.click);
|
|
749
|
-
head.removeEventListener("keydown", state.keydown);
|
|
750
|
-
for (const row of state.rows) row.classList.remove(ROW_CLASS, GROUP_HIDDEN_CLASS);
|
|
751
|
-
head.classList.remove(GROUP_HEAD_CLASS, GROUP_COLLAPSED_CLASS);
|
|
752
|
-
head.removeAttribute("aria-expanded");
|
|
753
|
-
head.removeAttribute(USER_TOGGLED_ATTR);
|
|
754
|
-
head.removeAttribute(TITLE_ATTR);
|
|
755
|
-
if (head.title === TITLE) head.removeAttribute("title");
|
|
756
|
-
}
|
|
757
|
-
function findProcessRows(root) {
|
|
758
|
-
const rows = [];
|
|
759
|
-
if (root instanceof HTMLElement && isProcessRow(root)) pushProcessRow(rows, root);
|
|
760
|
-
for (const element of root.querySelectorAll(PROCESS_CANDIDATE_SELECTOR)) if (element instanceof HTMLElement && isProcessRow(element)) pushProcessRow(rows, element);
|
|
761
|
-
return rows;
|
|
762
|
-
}
|
|
763
|
-
function pushProcessRow(rows, row) {
|
|
764
|
-
if (rows.some((existing) => existing.contains(row))) return;
|
|
765
|
-
rows.push(row);
|
|
766
|
-
}
|
|
767
|
-
function isProcessRow(element) {
|
|
768
|
-
if (element.closest(".dsh-codex-page") !== null || shouldIgnore(element)) return false;
|
|
769
|
-
const raw = (element.textContent ?? "").slice(0, MAX_TEXT_SAMPLE);
|
|
770
|
-
const normalized = raw.replace(/\s+/g, " ").trim();
|
|
771
|
-
if (!PROCESS_PREFIX_RE.test(normalized)) return false;
|
|
772
|
-
const stats = processTextStats(raw);
|
|
773
|
-
if (stats.processLabels === 0 || stats.processLabels > MAX_PROCESS_LABELS_PER_ROW) return false;
|
|
774
|
-
return !(element.children.length > 0 && stats.hasNonProcessLine);
|
|
775
|
-
}
|
|
776
|
-
function processTextStats(text) {
|
|
777
|
-
PROCESS_LINE_RE.lastIndex = 0;
|
|
778
|
-
let processLabels = 0;
|
|
779
|
-
while (PROCESS_LINE_RE.exec(text) !== null) processLabels++;
|
|
780
|
-
const hasNonProcessLine = text.split(/\r?\n/).map((line) => line.replace(/\s+/g, " ").trim()).some((line) => line !== "" && !PROCESS_PREFIX_RE.test(line));
|
|
781
|
-
return {
|
|
782
|
-
processLabels,
|
|
783
|
-
hasNonProcessLine
|
|
784
|
-
};
|
|
785
|
-
}
|
|
786
|
-
function setGroupCollapsed(head, collapsed) {
|
|
787
|
-
const state = findCurrentGroupState(head);
|
|
788
|
-
if (state === null) return;
|
|
789
|
-
head.classList.toggle(GROUP_COLLAPSED_CLASS, collapsed);
|
|
790
|
-
head.setAttribute("aria-expanded", String(!collapsed));
|
|
791
|
-
for (const row of state.rows.slice(1)) row.classList.toggle(GROUP_HIDDEN_CLASS, collapsed);
|
|
792
|
-
}
|
|
793
|
-
function findCurrentGroupState(head) {
|
|
794
|
-
const rows = [head];
|
|
795
|
-
let sibling = head.nextElementSibling;
|
|
796
|
-
while (sibling instanceof HTMLElement && isProcessRow(sibling)) {
|
|
797
|
-
rows.push(sibling);
|
|
798
|
-
sibling = sibling.nextElementSibling;
|
|
799
|
-
}
|
|
800
|
-
return rows.length > 0 ? { rows } : null;
|
|
801
|
-
}
|
|
802
|
-
function markUserToggled(head) {
|
|
803
|
-
head.setAttribute(USER_TOGGLED_ATTR, "true");
|
|
804
|
-
}
|
|
805
|
-
function nearestGroupHead(element) {
|
|
806
|
-
const decorated = element.closest(`.${GROUP_HEAD_CLASS}`);
|
|
807
|
-
return decorated instanceof HTMLElement ? decorated : null;
|
|
808
|
-
}
|
|
809
|
-
function titleForProcessRows(rows) {
|
|
810
|
-
const text = rows.map((row) => row.textContent ?? "").join("\n");
|
|
811
|
-
if (COMMAND_PATTERN.test(text)) return "运行了命令";
|
|
812
|
-
if (SEARCH_PATTERN.test(text)) return "进行了搜索";
|
|
813
|
-
return "思考过程";
|
|
814
|
-
}
|
|
815
|
-
function nodeElement(node) {
|
|
816
|
-
if (node instanceof Element) return node;
|
|
817
|
-
return node.parentElement;
|
|
818
|
-
}
|
|
819
|
-
function shouldIgnore(element) {
|
|
820
|
-
return element.closest("textarea,input,select,a,[contenteditable=\"true\"],script,style") !== null;
|
|
821
|
-
}
|
|
822
|
-
function isInteractiveTarget(target, groupHead) {
|
|
823
|
-
if (!(target instanceof Element)) return false;
|
|
824
|
-
const interactive = target.closest("button,a,input,textarea,select,[role=\"button\"],[contenteditable=\"true\"]");
|
|
825
|
-
return interactive !== null && interactive !== groupHead;
|
|
826
|
-
}
|
|
827
|
-
//#endregion
|
|
828
621
|
//#region src/client/styles.ts
|
|
829
622
|
const STYLE_ID = "@eddyskywalker/dsh-chatgpt-subscription/main";
|
|
830
623
|
const CSS = `
|
|
@@ -869,16 +662,9 @@ window.__ModuleLoader__.load({
|
|
|
869
662
|
.dsh-codex-skeleton span{animation:dsh-codex-pulse 1.4s ease-in-out infinite;background:var(--dsw-alias-bg-layer-2);border-radius:5px;height:42px}
|
|
870
663
|
.dsh-codex-skeleton span:nth-child(2){animation-delay:.12s}.dsh-codex-skeleton span:nth-child(3){animation-delay:.24s}
|
|
871
664
|
.dsh-codex-sr{height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;clip:rect(0 0 0 0);white-space:nowrap}
|
|
872
|
-
.dsh-codex-process-group-head{border-radius:6px;cursor:pointer;transition:background-color .15s ease,opacity .15s ease}
|
|
873
|
-
.dsh-codex-process-group-head:hover{background:color-mix(in srgb,var(--dsw-alias-bg-layer-2,rgba(127,127,127,.18)) 72%,transparent)}
|
|
874
|
-
.dsh-codex-process-group-head:focus-visible{outline:2px solid var(--dsw-alias-button-info-fill,#397ee8);outline-offset:2px}
|
|
875
|
-
.dsh-codex-process-group-head.dsh-codex-process-group-collapsed{align-items:center!important;color:transparent!important;display:flex!important;font-size:0!important;line-height:28px!important;max-height:28px!important;min-height:28px!important;overflow:hidden!important;opacity:.78}
|
|
876
|
-
.dsh-codex-process-group-head.dsh-codex-process-group-collapsed>*{display:none!important}
|
|
877
|
-
.dsh-codex-process-group-head.dsh-codex-process-group-collapsed::before{color:var(--dsw-alias-label-secondary);content:"▸ " attr(data-dsh-codex-process-title);display:block;font-size:13px;line-height:28px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
878
|
-
.dsh-codex-process-group-hidden{display:none!important}
|
|
879
665
|
@keyframes dsh-codex-pulse{0%,100%{opacity:.55}50%{opacity:1}}
|
|
880
666
|
@media(max-width:560px){.dsh-codex-row{align-items:flex-start;flex-direction:column;gap:3px}.dsh-codex-value{text-align:left}.dsh-codex-actions{justify-content:flex-start}.dsh-codex-grouphead{align-items:flex-start;flex-direction:column;gap:0;padding:12px 0}.dsh-codex-meter-meta{align-items:flex-start;flex-direction:column;gap:2px}.dsh-codex-errorbar{align-items:flex-start;flex-direction:column}}
|
|
881
|
-
@media(prefers-reduced-motion:reduce){.dsh-codex-meter>span
|
|
667
|
+
@media(prefers-reduced-motion:reduce){.dsh-codex-meter>span{transition:none}.dsh-codex-skeleton span{animation:none}}
|
|
882
668
|
`;
|
|
883
669
|
function installStyles() {
|
|
884
670
|
if (document.querySelector(`style[data-plugin-css="${STYLE_ID}"]`) !== null) return () => void 0;
|
|
@@ -895,7 +681,6 @@ window.__ModuleLoader__.load({
|
|
|
895
681
|
function apply(ctx) {
|
|
896
682
|
ctx.effect(() => ctx.locale.register(NS, dictionaries), "dsh-chatgpt-subscription: dictionaries");
|
|
897
683
|
ctx.effect(() => installStyles(), "dsh-chatgpt-subscription: styles");
|
|
898
|
-
ctx.effect(() => installProcessFolding(), "dsh-chatgpt-subscription: process folding");
|
|
899
684
|
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
900
685
|
name: "settings.section",
|
|
901
686
|
id: "codex-subscription",
|