dsh-openai-subscription 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AQian
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # dsh-openai-subscription
2
+
3
+ English | [中文](README.zh.md)
4
+
5
+ Use a ChatGPT subscription in [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) through OpenAI device authorization. No API key is required. After sign-in, the plugin attempts to add supported GPT models to the DSH model picker without manual provider setup.
6
+
7
+ ## Features
8
+
9
+ - Sign in with a ChatGPT plan that includes Codex access
10
+ - Attempt to enable the `openai-codex` provider and supported GPT models automatically
11
+ - View account status, refresh authorization, or sign out from DSH settings
12
+ - Keep access and refresh tokens in the local DSH credential store
13
+
14
+ ## Requirements
15
+
16
+ - DSH CLI 0.1.2-rc.1 or newer
17
+ - `pnpm` available on `PATH` for plugin installation
18
+ - Node.js 22.19 or newer available as `node`
19
+ - A [ChatGPT plan with Codex access](https://help.openai.com/en/articles/11369540-use-codex-with-a-chatgpt-plan) and device-code authorization enabled
20
+ - Network access to `auth.openai.com` and `chatgpt.com`
21
+
22
+ Install or update DSH when needed:
23
+
24
+ ```sh
25
+ npm install -g pnpm @deepseek-ai/dsh@next
26
+ ```
27
+
28
+ ## Installation
29
+
30
+ Install the plugin into the Web profile:
31
+
32
+ ```sh
33
+ dsh plugin --profile web add dsh-openai-subscription
34
+ ```
35
+
36
+ Restart `dsh web`, then refresh the page. DSH registers the plugin automatically; no profile edits are required.
37
+
38
+ ## Usage
39
+
40
+ 1. Open **Settings → ChatGPT 订阅登录** (ChatGPT Subscription Sign-in).
41
+ 2. Select **使用 ChatGPT 账号登录** (Sign in with ChatGPT).
42
+ 3. Open the displayed link and enter the device code with your ChatGPT account.
43
+ 4. Return to DSH after authorization completes and select a supported GPT model.
44
+
45
+ Use **刷新授权** (Refresh authorization) if the session cannot renew automatically. Use **退出登录** (Sign out) before changing accounts or uninstalling the plugin.
46
+
47
+ ## Uninstallation
48
+
49
+ Sign out from the settings page first so the stored authorization is removed, then run:
50
+
51
+ ```sh
52
+ dsh plugin --profile web remove dsh-openai-subscription
53
+ ```
54
+
55
+ Restart the Web profile afterward.
56
+
57
+ ## Troubleshooting
58
+
59
+ - **The settings entry is missing:** confirm the plugin is installed in the `web` profile with `dsh plugin --profile web why dsh-openai-subscription`, then restart DSH and refresh the page.
60
+ - **Authorization cannot start:** update DSH, confirm `node --version` meets the requirement, and check access to the OpenAI authentication service.
61
+ - **The device code is rejected:** enable device-code authorization in the ChatGPT security settings and start a new sign-in.
62
+ - **Refresh fails:** sign out and complete device authorization again.
63
+
64
+ ## Security and privacy
65
+
66
+ Your password is entered only on OpenAI's website; the plugin never receives it. OAuth credentials are stored in the local DSH credential store, and the status interface never returns token values. Do not share the temporary device code.
67
+
68
+ OpenAI's terms, privacy policy, model availability, and usage limits still apply. This is an independent community plugin and is not affiliated with or endorsed by OpenAI or DeepSeek.
69
+
70
+ ## License
71
+
72
+ MIT — see [LICENSE](LICENSE).
package/README.zh.md ADDED
@@ -0,0 +1,72 @@
1
+ # dsh-openai-subscription
2
+
3
+ [English](README.md) | 中文
4
+
5
+ 通过 OpenAI 设备授权,在 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 中使用 ChatGPT 订阅。无需 API 密钥。登录后,插件会尝试自动将受支持的 GPT 模型加入 DSH 模型选择器。
6
+
7
+ ## 功能
8
+
9
+ - 使用包含 Codex 权限的 ChatGPT 套餐登录
10
+ - 尝试自动启用 `openai-codex` 提供商和受支持的 GPT 模型
11
+ - 在 DSH 设置中查看账号状态、刷新授权或退出登录
12
+ - 将访问令牌和刷新令牌保存在本机 DSH 凭证库中
13
+
14
+ ## 使用要求
15
+
16
+ - DSH CLI 0.1.2-rc.1 或更高版本
17
+ - `PATH` 中可用的 `pnpm`,用于安装插件
18
+ - `node` 命令对应 Node.js 22.19 或更高版本
19
+ - [包含 Codex 权限的 ChatGPT 套餐](https://help.openai.com/en/articles/11369540-use-codex-with-a-chatgpt-plan),并已启用设备码授权
20
+ - 能够访问 `auth.openai.com` 和 `chatgpt.com`
21
+
22
+ 如需安装或更新 DSH:
23
+
24
+ ```sh
25
+ npm install -g pnpm @deepseek-ai/dsh@next
26
+ ```
27
+
28
+ ## 安装
29
+
30
+ 将插件安装到 Web profile:
31
+
32
+ ```sh
33
+ dsh plugin --profile web add dsh-openai-subscription
34
+ ```
35
+
36
+ 重启 `dsh web`,然后刷新页面。DSH 会自动注册插件,无需手动修改 profile。
37
+
38
+ ## 使用
39
+
40
+ 1. 打开 **设置 → ChatGPT 订阅登录**。
41
+ 2. 点击 **使用 ChatGPT 账号登录**。
42
+ 3. 打开页面中显示的链接,使用 ChatGPT 账号输入设备码。
43
+ 4. 授权完成后返回 DSH,选择受支持的 GPT 模型。
44
+
45
+ 会话无法自动续期时,可点击 **刷新授权**。更换账号或卸载插件前,请先点击 **退出登录**。
46
+
47
+ ## 卸载
48
+
49
+ 先在设置页退出登录以删除本机授权,再执行:
50
+
51
+ ```sh
52
+ dsh plugin --profile web remove dsh-openai-subscription
53
+ ```
54
+
55
+ 随后重启 Web profile。
56
+
57
+ ## 故障排查
58
+
59
+ - **设置中没有登录入口:** 使用 `dsh plugin --profile web why dsh-openai-subscription` 确认插件已安装到 `web` profile,然后重启 DSH 并刷新页面。
60
+ - **无法开始授权:** 更新 DSH,确认 `node --version` 满足要求,并检查 OpenAI 登录服务是否可访问。
61
+ - **设备码被拒绝:** 在 ChatGPT 安全设置中启用设备码授权,然后重新登录。
62
+ - **刷新授权失败:** 退出登录后重新完成设备授权。
63
+
64
+ ## 安全与隐私
65
+
66
+ 密码只在 OpenAI 官方页面输入,本插件不会接触密码。OAuth 凭证保存在本机 DSH 凭证库中,状态接口不会返回令牌内容。设备验证码在有效期内属于敏感信息,请勿分享。
67
+
68
+ 使用过程仍受 OpenAI 的条款、隐私政策、模型可用性和用量限制约束。本项目是独立的社区插件,与 OpenAI 或 DeepSeek 无隶属或背书关系。
69
+
70
+ ## 许可证
71
+
72
+ MIT——见 [LICENSE](LICENSE)。
@@ -0,0 +1,4 @@
1
+ # Mount the ChatGPT subscription sign-in plugin.
2
+ - insert:
3
+ - id: openai-subscription
4
+ name: 'dsh-openai-subscription'
@@ -0,0 +1,95 @@
1
+ /** Notice queued by the host during an authorization attempt. */
2
+ interface FlowNotice {
3
+ message?: string;
4
+ url?: string | null;
5
+ code?: string | null;
6
+ }
7
+ /** `openaiSubscription/status` reply. */
8
+ interface StatusInfo {
9
+ configured?: boolean;
10
+ ready?: boolean;
11
+ accountId?: string | null;
12
+ expires?: number | null;
13
+ }
14
+ /** `openaiSubscription/poll` reply. */
15
+ interface PollInfo {
16
+ status?: 'idle' | 'pending' | 'done';
17
+ notices?: FlowNotice[];
18
+ outcome?: string | null;
19
+ error?: string | null;
20
+ }
21
+ /** `openaiSubscription/authorize` reply. */
22
+ interface AuthorizeInfo {
23
+ started?: boolean;
24
+ error?: string;
25
+ }
26
+ /** Envelope of `connection.rpc.call`. */
27
+ interface RemoteResult<T> {
28
+ ok?: boolean;
29
+ error?: {
30
+ message?: string;
31
+ } | null;
32
+ value?: T;
33
+ }
34
+ interface RpcCaller {
35
+ call(route: string, method: string, payload: {
36
+ args: Record<string, unknown>;
37
+ }, signal?: AbortSignal): Promise<RemoteResult<unknown>>;
38
+ }
39
+ interface ConnectionService {
40
+ rpc: RpcCaller;
41
+ }
42
+ interface ClientTimer {
43
+ interval(callback: () => void, delay: number): () => void;
44
+ }
45
+ /** Context passed to the client plugin. */
46
+ interface ClientContext {
47
+ get(name: string): unknown;
48
+ connection: ConnectionService;
49
+ timer: ClientTimer | undefined;
50
+ }
51
+ interface SlotsService {
52
+ inject(name: string, setup: () => void): unknown;
53
+ register(meta: {
54
+ name: string;
55
+ id: string;
56
+ order: number;
57
+ label: string;
58
+ }, render: () => unknown): unknown;
59
+ }
60
+ /** Exports the web module table expects from a `dsh.client` bundle. */
61
+ interface ClientModuleExports {
62
+ apply(ctx: ClientContext): void;
63
+ inject: string[];
64
+ }
65
+ interface ModuleRegistration {
66
+ id: string;
67
+ factory(require: (id: string) => unknown): ClientModuleExports;
68
+ }
69
+ interface Window {
70
+ __ModuleLoader__: {
71
+ load(registration: ModuleRegistration): void;
72
+ };
73
+ }
74
+ type ReactChild = string | number | boolean | null | undefined;
75
+ type ReactNode = ReactChild | ReactElement | ReadonlyArray<ReactNode>;
76
+ interface ReactElement {
77
+ type: unknown;
78
+ props: Record<string, unknown> | null;
79
+ key: string | number | null;
80
+ }
81
+ interface SectionProps {
82
+ connection: ConnectionService;
83
+ timer: ClientTimer | undefined;
84
+ }
85
+ type SectionComponent = (props: SectionProps) => ReactElement;
86
+ interface ReactModule {
87
+ createElement(type: string | SectionComponent, props: Record<string, unknown> | null, ...children: ReactNode[]): ReactElement;
88
+ useState<S>(initial: S): [S, (update: S | ((previous: S) => S)) => void];
89
+ useEffect(effect: () => void | (() => void), deps?: ReadonlyArray<unknown>): void;
90
+ useRef<T>(initial: T): {
91
+ current: T;
92
+ };
93
+ }
94
+ /** Stringify an unknown thrown value for display. */
95
+ declare function messageOf(error: unknown): string;
package/dist/client.js ADDED
@@ -0,0 +1,335 @@
1
+ "use strict";
2
+ // Web settings UI for ChatGPT subscription authorization.
3
+ // This remains a global script for the DSH client module loader.
4
+ //#endregion
5
+ /** Stringify an unknown thrown value for display. */
6
+ function messageOf(error) {
7
+ const message = error?.message;
8
+ return message ? String(message) : String(error);
9
+ }
10
+ window.__ModuleLoader__.load({
11
+ id: 'dsh-openai-subscription',
12
+ factory: (require) => {
13
+ const module = { exports: {} };
14
+ const exports = module.exports;
15
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
16
+ const React = require('react');
17
+ const CSS = [
18
+ '.oasub-wrap { display: flex; flex-direction: column; gap: 12px; max-width: 620px; }',
19
+ '.oasub-title { font-size: 15px; font-weight: 600; }',
20
+ '.oasub-desc { font-size: 13px; line-height: 1.6; opacity: .8; }',
21
+ '.oasub-card { border: 1px solid rgba(128,128,128,.28); border-radius: 10px; padding: 12px 14px; display: flex; flex-direction: column; gap: 6px; }',
22
+ '.oasub-row { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }',
23
+ '.oasub-code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 18px; font-weight: 700; letter-spacing: 2px; padding: 8px 14px; border-radius: 8px; background: rgba(128,128,128,.16); user-select: all; }',
24
+ '.oasub-link { color: inherit; }',
25
+ '.oasub-btn { padding: 7px 14px; border-radius: 8px; border: 1px solid rgba(128,128,128,.4); background: transparent; color: inherit; cursor: pointer; font-size: 13px; }',
26
+ '.oasub-btn:disabled { opacity: .45; cursor: default; }',
27
+ '.oasub-btn.primary { background: #087c61; border-color: #087c61; color: #fff; }',
28
+ '.oasub-btn:focus-visible, .oasub-link:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }',
29
+ '.oasub-err { color: #c52b32; font-size: 13px; }',
30
+ '.oasub-ok { color: #087c61; font-size: 13px; }',
31
+ ].join('\n');
32
+ function ensureCss() {
33
+ const tagId = 'dsh-openai-subscription/settings.css';
34
+ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin-css=' + JSON.stringify(tagId) + ']') === null) {
35
+ const tag = document.createElement('style');
36
+ tag.setAttribute('data-plugin-css', tagId);
37
+ tag.textContent = CSS;
38
+ document.head.appendChild(tag);
39
+ }
40
+ }
41
+ const RPC_TIMEOUT_MS = 12000;
42
+ function remoteCall(connection, method, args, signal, timeoutMs = RPC_TIMEOUT_MS) {
43
+ return new Promise((resolve, reject) => {
44
+ const controller = new AbortController();
45
+ let settled = false;
46
+ let timeout;
47
+ const cleanup = () => {
48
+ if (timeout !== undefined)
49
+ clearTimeout(timeout);
50
+ signal?.removeEventListener('abort', onAbort);
51
+ };
52
+ const finish = (callback) => {
53
+ if (settled)
54
+ return;
55
+ settled = true;
56
+ cleanup();
57
+ callback();
58
+ };
59
+ const onAbort = () => {
60
+ controller.abort();
61
+ finish(() => reject(new Error('请求已取消')));
62
+ };
63
+ if (signal?.aborted) {
64
+ onAbort();
65
+ return;
66
+ }
67
+ signal?.addEventListener('abort', onAbort, { once: true });
68
+ timeout = setTimeout(() => {
69
+ controller.abort();
70
+ finish(() => reject(new Error('请求超时,请检查连接后重试')));
71
+ }, timeoutMs);
72
+ let request;
73
+ try {
74
+ request = connection.rpc.call('/api', 'openaiSubscription/' + method, { args }, controller.signal);
75
+ }
76
+ catch (error) {
77
+ finish(() => reject(error));
78
+ return;
79
+ }
80
+ request.then((result) => {
81
+ finish(() => {
82
+ if (!result || !result.ok) {
83
+ reject(new Error('DSH 服务请求失败,请重试'));
84
+ return;
85
+ }
86
+ if (result.value === undefined) {
87
+ reject(new Error('服务返回了无效结果'));
88
+ return;
89
+ }
90
+ resolve(result.value);
91
+ });
92
+ }).catch((error) => finish(() => reject(error)));
93
+ });
94
+ }
95
+ function parseStatus(value) {
96
+ if (typeof value !== 'object' || value === null)
97
+ throw new Error('登录状态响应无效');
98
+ const raw = value;
99
+ if (typeof raw.configured !== 'boolean' || typeof raw.ready !== 'boolean')
100
+ throw new Error('登录状态响应不完整');
101
+ const status = { configured: raw.configured, ready: raw.ready };
102
+ if (typeof raw.accountId === 'string')
103
+ status.accountId = raw.accountId;
104
+ if (typeof raw.expires === 'number' && Number.isFinite(raw.expires) && raw.expires > 0)
105
+ status.expires = raw.expires;
106
+ return status;
107
+ }
108
+ function parseAuthorize(value) {
109
+ if (typeof value !== 'object' || value === null)
110
+ throw new Error('启动授权响应无效');
111
+ const raw = value;
112
+ if (typeof raw.started !== 'boolean')
113
+ throw new Error('启动授权响应不完整');
114
+ return { started: raw.started, error: typeof raw.error === 'string' ? raw.error : undefined };
115
+ }
116
+ function parsePoll(value) {
117
+ if (typeof value !== 'object' || value === null)
118
+ throw new Error('授权状态响应无效');
119
+ const raw = value;
120
+ if (raw.status !== 'idle' && raw.status !== 'pending' && raw.status !== 'done')
121
+ throw new Error('授权状态响应不完整');
122
+ const notices = [];
123
+ if (Array.isArray(raw.notices)) {
124
+ for (const item of raw.notices) {
125
+ if (typeof item !== 'object' || item === null)
126
+ continue;
127
+ const notice = item;
128
+ notices.push({
129
+ message: typeof notice.message === 'string' ? notice.message : undefined,
130
+ url: typeof notice.url === 'string' ? notice.url : undefined,
131
+ code: typeof notice.code === 'string' ? notice.code : undefined,
132
+ });
133
+ }
134
+ }
135
+ return {
136
+ status: raw.status,
137
+ notices,
138
+ outcome: typeof raw.outcome === 'string' ? raw.outcome : null,
139
+ error: typeof raw.error === 'string' ? raw.error : null,
140
+ };
141
+ }
142
+ function safeHttpUrl(value) {
143
+ if (typeof value !== 'string')
144
+ return null;
145
+ try {
146
+ const url = new URL(value, window.location.href);
147
+ return url.protocol === 'https:' || url.protocol === 'http:' ? url.href : null;
148
+ }
149
+ catch {
150
+ return null;
151
+ }
152
+ }
153
+ function Section(props) {
154
+ const el = React.createElement;
155
+ const connection = props.connection;
156
+ const timer = props.timer;
157
+ const [info, setInfo] = React.useState(null);
158
+ const [phase, setPhase] = React.useState('idle');
159
+ const [notices, setNotices] = React.useState([]);
160
+ const [error, setError] = React.useState('');
161
+ const [statusError, setStatusError] = React.useState('');
162
+ const [statusRetry, setStatusRetry] = React.useState(0);
163
+ const [result, setResult] = React.useState(null);
164
+ const actionLock = React.useRef(false);
165
+ const ready = info ? !!info.ready : true;
166
+ const configured = info ? !!info.configured : false;
167
+ const busy = phase !== 'idle' && phase !== 'done';
168
+ const authBusy = phase === 'starting' || phase === 'pending' || phase === 'cancelling';
169
+ React.useEffect(() => {
170
+ if (phase !== 'idle' && phase !== 'done')
171
+ return;
172
+ let alive = true;
173
+ const controller = new AbortController();
174
+ remoteCall(connection, 'status', {}, controller.signal).then(parseStatus).then((status) => {
175
+ if (!alive)
176
+ return;
177
+ setInfo(status);
178
+ setStatusError('');
179
+ }).catch((cause) => {
180
+ if (!alive || controller.signal.aborted)
181
+ return;
182
+ setStatusError('无法读取登录状态:' + messageOf(cause));
183
+ });
184
+ return () => {
185
+ alive = false;
186
+ controller.abort();
187
+ };
188
+ }, [connection, phase, statusRetry]);
189
+ React.useEffect(() => {
190
+ if (phase !== 'pending' && phase !== 'cancelling')
191
+ return;
192
+ if (timer === undefined || typeof timer.interval !== 'function') {
193
+ setError('授权状态服务不可用,请重启 DSH 后重试。');
194
+ setPhase('done');
195
+ return;
196
+ }
197
+ let alive = true;
198
+ let inFlight = false;
199
+ let failures = 0;
200
+ const controller = new AbortController();
201
+ const finish = (nextResult, nextError = '') => {
202
+ if (!alive)
203
+ return;
204
+ setResult(nextResult);
205
+ setError(nextError);
206
+ setPhase('done');
207
+ };
208
+ const pollOnce = () => {
209
+ if (!alive || inFlight)
210
+ return;
211
+ inFlight = true;
212
+ remoteCall(connection, 'poll', {}, controller.signal).then(parsePoll).then((poll) => {
213
+ if (!alive)
214
+ return;
215
+ failures = 0;
216
+ setError((previous) => previous.startsWith('授权状态连接暂时中断') ? '' : previous);
217
+ if (poll.notices && poll.notices.length)
218
+ setNotices((previous) => previous.concat(poll.notices ?? []).slice(-8));
219
+ if (poll.status === 'done') {
220
+ finish(poll.outcome || 'done', poll.error || '');
221
+ return;
222
+ }
223
+ if (poll.status === 'idle') {
224
+ return remoteCall(connection, 'status', {}, controller.signal).then(parseStatus).then((status) => {
225
+ if (!alive)
226
+ return;
227
+ setInfo(status);
228
+ if (status.configured)
229
+ finish('authorized');
230
+ else
231
+ finish('ended', '授权流程已结束(未获取到结果)。');
232
+ });
233
+ }
234
+ }).catch((cause) => {
235
+ if (!alive || controller.signal.aborted)
236
+ return;
237
+ failures += 1;
238
+ const suffix = failures > 1 ? '(已重试 ' + failures + ' 次)' : '';
239
+ setError('授权状态连接暂时中断,正在自动重试…' + suffix);
240
+ console.warn('[openai-subscription] poll failed: ' + messageOf(cause));
241
+ }).finally(() => {
242
+ inFlight = false;
243
+ });
244
+ };
245
+ pollOnce();
246
+ const stop = timer.interval(pollOnce, 1000);
247
+ return () => {
248
+ alive = false;
249
+ controller.abort();
250
+ stop();
251
+ };
252
+ }, [phase, timer, connection]);
253
+ const start = (method) => {
254
+ if (busy || actionLock.current)
255
+ return;
256
+ if (timer === undefined || typeof timer.interval !== 'function') {
257
+ setError('授权状态服务不可用,请重启 DSH 后重试。');
258
+ setPhase('done');
259
+ return;
260
+ }
261
+ actionLock.current = true;
262
+ setNotices([]);
263
+ setError('');
264
+ setResult(null);
265
+ setPhase('starting');
266
+ remoteCall(connection, 'authorize', { method }).then(parseAuthorize).then((reply) => {
267
+ if (reply.started)
268
+ setPhase('pending');
269
+ else {
270
+ setError(reply.error || '无法启动授权流程');
271
+ setPhase('done');
272
+ }
273
+ }).catch((cause) => {
274
+ // The request might have reached the host before its acknowledgement
275
+ // was lost, so cancel best-effort to avoid an orphaned flow.
276
+ void remoteCall(connection, 'cancel', {}).catch(() => { });
277
+ setError(messageOf(cause));
278
+ setPhase('done');
279
+ }).finally(() => {
280
+ actionLock.current = false;
281
+ });
282
+ };
283
+ const cancel = () => {
284
+ if (phase !== 'pending' || actionLock.current)
285
+ return;
286
+ actionLock.current = true;
287
+ setPhase('cancelling');
288
+ remoteCall(connection, 'cancel', {}).catch((cause) => {
289
+ setError('取消失败:' + messageOf(cause));
290
+ setPhase('pending');
291
+ }).finally(() => {
292
+ actionLock.current = false;
293
+ });
294
+ };
295
+ const logout = () => {
296
+ if (busy || actionLock.current)
297
+ return;
298
+ actionLock.current = true;
299
+ setError('');
300
+ setPhase('logging-out');
301
+ remoteCall(connection, 'logout', {}).then(() => {
302
+ setInfo({ configured: false, ready });
303
+ setNotices([]);
304
+ setResult('logged-out');
305
+ setPhase('done');
306
+ }).catch((cause) => {
307
+ setError('退出失败:' + messageOf(cause));
308
+ setPhase('done');
309
+ }).finally(() => {
310
+ actionLock.current = false;
311
+ });
312
+ };
313
+ return el('div', { className: 'oasub-wrap' }, el('div', { className: 'oasub-title', role: 'heading', 'aria-level': 2 }, 'ChatGPT 订阅登录'), el('div', { className: 'oasub-desc' }, '使用有 Codex 权限的 ChatGPT 账号授权 DSH。登录后,插件会尝试自动启用受支持的 GPT 模型,凭证保存在本机。'), !ready ? el('div', { className: 'oasub-err', role: 'alert' }, '当前 DSH 环境缺少 OpenAI 登录组件,请更新 DSH 后重试。') : null, info === null
314
+ ? (statusError ? null : el('div', { className: 'oasub-card', role: 'status', 'aria-live': 'polite' }, '正在读取登录状态…'))
315
+ : el('div', { className: 'oasub-card' }, configured ? el('div', { className: 'oasub-ok' }, '已登录 ChatGPT 订阅账号') : el('div', { className: 'oasub-desc' }, '尚未登录 ChatGPT 订阅账号'), configured && info.accountId ? el('div', { className: 'oasub-desc' }, '账号:' + info.accountId) : null, configured && info.expires ? el('div', { className: 'oasub-desc' }, '授权有效期至:' + new Date(info.expires).toLocaleString()) : null), notices.map((notice, index) => {
316
+ const url = safeHttpUrl(notice.url);
317
+ return el('div', { key: 'n' + index, className: 'oasub-card', role: 'status', 'aria-live': 'polite' }, notice.message ? el('div', { className: 'oasub-desc' }, notice.message) : null, notice.code ? el('div', { className: 'oasub-row' }, el('code', { className: 'oasub-code' }, notice.code), url ? el('a', { className: 'oasub-link', href: url, target: '_blank', rel: 'noreferrer' }, '打开登录页(新窗口)') : null) : null);
318
+ }), statusError ? el('div', { className: 'oasub-row' }, el('div', { className: 'oasub-err', role: 'alert' }, statusError), el('button', { type: 'button', className: 'oasub-btn', disabled: busy, onClick: () => setStatusRetry((value) => value + 1) }, '重试')) : null, error ? el('div', { className: 'oasub-err', role: 'alert' }, error) : null, phase === 'done' && result === 'authorized' ? el('div', { className: 'oasub-ok', role: 'status', 'aria-live': 'polite' }, '授权成功,凭证已保存。') : null, phase === 'done' && result === 'cancelled' ? el('div', { className: 'oasub-desc', role: 'status', 'aria-live': 'polite' }, '授权已取消。') : null, phase === 'done' && result === 'logged-out' ? el('div', { className: 'oasub-ok', role: 'status', 'aria-live': 'polite' }, '已退出 ChatGPT 订阅账号。') : null, info === null ? null : el('div', { className: 'oasub-row' }, configured
319
+ ? el('button', { type: 'button', className: 'oasub-btn', disabled: busy || !ready, onClick: () => start('refresh') }, authBusy && phase !== 'cancelling' ? '刷新中…' : '刷新授权')
320
+ : el('button', { type: 'button', className: 'oasub-btn primary', disabled: busy || !ready, onClick: () => start('device_code') }, authBusy && phase !== 'cancelling' ? '登录中…' : '使用 ChatGPT 账号登录'), configured ? el('button', { type: 'button', className: 'oasub-btn', disabled: busy, onClick: logout }, phase === 'logging-out' ? '退出中…' : '退出登录') : null, phase === 'pending' || phase === 'cancelling'
321
+ ? el('button', { type: 'button', className: 'oasub-btn', disabled: phase === 'cancelling', onClick: cancel }, phase === 'cancelling' ? '取消中…' : '取消')
322
+ : null));
323
+ }
324
+ function apply(ctx) {
325
+ const slots = ctx.get('slots');
326
+ if (slots === undefined)
327
+ return;
328
+ ensureCss();
329
+ slots.inject('settings.section', () => slots.register({ name: 'settings.section', id: 'openai-subscription', order: 25, label: 'ChatGPT 订阅登录' }, () => React.createElement(Section, { connection: ctx.connection, timer: ctx.timer })));
330
+ }
331
+ exports.apply = apply;
332
+ exports.inject = ['connection', 'timer'];
333
+ return module.exports;
334
+ }
335
+ });