@pure01fx/dsh-openai-codex-auth 0.5.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 +21 -0
- package/README.md +253 -0
- package/assets/readme/hero.svg +56 -0
- package/assets/readme/workflow.svg +56 -0
- package/client.js +601 -0
- package/cordis.patch.yml +9 -0
- package/lib/index.d.ts +128 -0
- package/lib/index.js +1258 -0
- package/package.json +78 -0
package/client.js
ADDED
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: '@pure01fx/dsh-openai-codex-auth',
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} }
|
|
5
|
+
var exports = module.exports
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
|
|
7
|
+
|
|
8
|
+
const React = require('react')
|
|
9
|
+
const { createElement: h, useCallback, useEffect, useMemo, useRef, useState } = React
|
|
10
|
+
const BASE = '/openai-codex'
|
|
11
|
+
const PLUGIN_ID = '@pure01fx/dsh-openai-codex-auth'
|
|
12
|
+
const statusSubscribers = new Set()
|
|
13
|
+
let sharedStatus = null
|
|
14
|
+
let statusRequestGeneration = 0
|
|
15
|
+
|
|
16
|
+
const css = `
|
|
17
|
+
.codexSection{box-sizing:border-box;max-width:720px;color:var(--dsw-alias-label-primary);display:flex;flex-direction:column;gap:12px}
|
|
18
|
+
.codexSection *{box-sizing:border-box}.codexTitle{color:var(--dsw-alias-label-primary);margin:0;font-size:16px;font-weight:500;line-height:24px}
|
|
19
|
+
.codexIntro{color:var(--dsw-alias-label-tertiary);margin:0;font-size:14px;line-height:22px}
|
|
20
|
+
.codexCard{overflow:hidden;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-3)}
|
|
21
|
+
.codexHero{display:flex;align-items:flex-start;justify-content:space-between;gap:20px;padding:16px 18px;background:var(--dsw-alias-bg-layer-2);border-bottom:1px solid var(--dsw-alias-border-l2)}
|
|
22
|
+
.codexBrand{display:flex;align-items:center;gap:12px;min-width:0}
|
|
23
|
+
.codexLogo{display:grid;place-items:center;width:40px;height:40px;flex:0 0 auto;border-radius:10px;background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground);font:700 13px/1 ui-monospace,SFMono-Regular,Consolas,monospace}
|
|
24
|
+
.codexName{color:var(--dsw-alias-label-primary);margin:0;font-size:15px;font-weight:500;line-height:22px}.codexMeta{margin:2px 0 0;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;overflow-wrap:anywhere}
|
|
25
|
+
.codexBadge{display:inline-flex;align-items:center;gap:6px;white-space:nowrap;border:1px solid var(--dsw-alias-border-l3);border-radius:999px;padding:3px 9px;font-size:12px;font-weight:500;line-height:18px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-secondary)}
|
|
26
|
+
.codexBadge.connected{color:var(--dsw-alias-state-success-primary)}.codexBadge.pending{color:var(--dsw-alias-state-warn-label)}
|
|
27
|
+
.codexDot{width:7px;height:7px;border-radius:50%;background:currentColor}
|
|
28
|
+
.codexBody{padding:18px}.codexActions{display:flex;flex-wrap:wrap;gap:8px;margin-top:16px}
|
|
29
|
+
.codexButton{appearance:none;box-sizing:border-box;min-height:34px;border:1px solid var(--dsw-alias-border-l2);border-radius:17px;background:transparent;color:var(--dsw-alias-label-primary);padding:5px 13px;font:500 13px/20px inherit;cursor:pointer;transition:background .15s,border-color .15s,color .15s}
|
|
30
|
+
.codexButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);border-color:var(--dsw-alias-border-l3)}.codexButton:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.codexButton:disabled{color:var(--dsw-alias-label-dimmed);cursor:not-allowed}
|
|
31
|
+
.codexButton.primary{border-color:transparent;background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground)}.codexButton.primary:hover:not(:disabled){border-color:transparent;background:var(--dsw-alias-button-primary-hover)}.codexButton.danger{color:var(--dsw-alias-state-error-primary)}
|
|
32
|
+
.codexGrid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin-top:14px}
|
|
33
|
+
.codexUsage{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:12px 14px;background:var(--dsw-alias-bg-layer-2)}
|
|
34
|
+
.codexUsageHead{display:flex;align-items:baseline;justify-content:space-between;gap:12px}.codexUsageName{font-size:13px;font-weight:500}.codexUsageValue{font-size:12px;color:var(--dsw-alias-label-tertiary)}
|
|
35
|
+
.codexBar{height:6px;margin:10px 0 8px;overflow:hidden;border-radius:999px;background:var(--dsw-alias-bg-layer-1)}.codexBarFill{height:100%;border-radius:inherit;background:var(--dsw-alias-state-success-primary);transition:width .25s ease}.codexBarFill.high{background:var(--dsw-alias-state-warn-label)}
|
|
36
|
+
.codexReset{font-size:12px;color:var(--dsw-alias-label-tertiary)}
|
|
37
|
+
.codexPlan{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:2px}.codexPlan strong{font-size:14px;font-weight:500}.codexPlan span{font-size:12px;color:var(--dsw-alias-label-tertiary)}
|
|
38
|
+
.codexNotice,.codexWarning,.codexError{margin:14px 0 0;border-radius:8px;padding:8px 10px;background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}
|
|
39
|
+
.codexWarning{color:var(--dsw-alias-state-warn-label)}.codexError{color:var(--dsw-alias-state-error-primary);overflow-wrap:anywhere}
|
|
40
|
+
.codexDevice{margin-top:14px;border:1px solid var(--dsw-alias-brand-primary);border-radius:10px;padding:14px;background:var(--dsw-alias-interactive-bg-hover)}
|
|
41
|
+
.codexDeviceTitle{margin:0;font-size:14px;font-weight:500;line-height:22px}.codexDeviceHelp{margin:4px 0 0;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}
|
|
42
|
+
.codexCode{display:block;margin:12px 0 5px;padding:10px;border:1px dashed var(--dsw-alias-border-l3);border-radius:8px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font:700 24px/1.25 ui-monospace,SFMono-Regular,Consolas,monospace;letter-spacing:.08em;text-align:center;user-select:all}
|
|
43
|
+
.codexDeviceMeta{text-align:center;color:var(--dsw-alias-label-tertiary);font-size:12px}.codexDeviceActions{display:flex;flex-wrap:wrap;justify-content:center;gap:8px;margin-top:12px}
|
|
44
|
+
.codexBrowser{margin-top:14px;border:1px solid var(--dsw-alias-border-l3);border-radius:10px;padding:14px;background:var(--dsw-alias-bg-layer-2)}
|
|
45
|
+
.codexBrowserHead{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}.codexProbe{display:flex;align-items:flex-start;gap:9px;margin-top:12px;padding:10px;border-radius:8px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}.codexProbe.ok{color:var(--dsw-alias-state-success-primary)}.codexProbe.failed{color:var(--dsw-alias-state-warn-label)}
|
|
46
|
+
.codexProbeIcon{display:grid;place-items:center;width:20px;height:20px;flex:0 0 auto;border:1px solid currentColor;border-radius:50%;font-weight:700}.codexProbeText strong{display:block;font-size:13px;font-weight:500}.codexProbeEndpoint{display:block;margin-top:3px;color:var(--dsw-alias-label-tertiary);font:12px/18px ui-monospace,SFMono-Regular,Consolas,monospace;overflow-wrap:anywhere}
|
|
47
|
+
.codexField{display:block;margin-top:14px}.codexFieldLabel{display:block;margin-bottom:6px;color:var(--dsw-alias-label-secondary);font-size:12px}.codexInput{display:block;width:100%;min-height:36px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:7px 10px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);font:13px/20px ui-monospace,SFMono-Regular,Consolas,monospace}.codexInput:focus{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}.codexInput::placeholder{color:var(--dsw-alias-label-dimmed)}
|
|
48
|
+
.codexEmpty{padding:4px 0;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:20px}.codexSkeleton{height:8px;margin:10px 0;border-radius:99px;background:linear-gradient(90deg,var(--dsw-alias-bg-layer-2),var(--dsw-alias-interactive-bg-hover-solid),var(--dsw-alias-bg-layer-2));background-size:200% 100%;animation:codexPulse 1.2s infinite}
|
|
49
|
+
.codexQuotaRoot{display:inline-flex;position:relative}.codexQuotaTrigger{width:28px;height:28px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:transparent;border:none;border-radius:999px;flex:none;place-items:center;display:grid}.codexQuotaTrigger:hover{background:var(--dsw-alias-interactive-bg-hover)}.codexQuotaTrigger:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
|
|
50
|
+
.codexQuotaTrack{fill:none;stroke:var(--dsw-alias-border-l3);stroke-width:2px}.codexQuotaAvailable{fill:none;stroke:var(--dsw-alias-state-success-primary);stroke-width:2px;transition:stroke .25s ease}.codexQuotaAvailable.high{stroke:var(--dsw-alias-state-warn-label)}.codexQuotaAvailable.critical{stroke:var(--dsw-alias-state-error-primary)}.codexQuotaConsumed{fill:none;stroke:var(--dsw-alias-border-l3);stroke-width:2px;stroke-linecap:round;transition:stroke-dasharray .25s ease}.codexQuotaLoading{animation:codexQuotaPulse .8s ease-in-out infinite alternate}
|
|
51
|
+
.codexQuotaPanel{z-index:100;box-sizing:border-box;width:272px;border:1px solid var(--dsw-alias-border-inverted);border-radius:12px;padding:12px;background:var(--dsw-specific-menu);box-shadow:var(--dsw-shadow-lv3);color:var(--dsw-alias-label-secondary);cursor:default;font-size:12px;line-height:20px;position:absolute;right:0;bottom:calc(100% + 8px)}.codexQuotaPanelHead{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:8px}.codexQuotaPanelHead strong{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500}.codexQuotaPanelHead span{color:var(--dsw-alias-label-tertiary)}
|
|
52
|
+
.codexQuotaWindow{padding:7px 0}.codexQuotaWindow+.codexQuotaWindow{border-top:1px solid var(--dsw-alias-border-l2)}.codexQuotaWindowHead{display:flex;align-items:center;justify-content:space-between;gap:10px}.codexQuotaWindowHead strong{font-weight:500;color:var(--dsw-alias-label-secondary)}.codexQuotaWindowHead span{font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-primary)}.codexQuotaBar{height:4px;margin:6px 0;border-radius:999px;overflow:hidden;background:var(--dsw-alias-interactive-bg-hover)}.codexQuotaBarFill{height:100%;border-radius:inherit;background:var(--dsw-alias-state-success-primary)}.codexQuotaBarFill.high{background:var(--dsw-alias-state-warn-label)}.codexQuotaBarFill.critical{background:var(--dsw-alias-state-error-primary)}.codexQuotaReset{color:var(--dsw-alias-label-tertiary)}.codexQuotaMessage{margin:4px 0;color:var(--dsw-alias-label-tertiary)}.codexQuotaMessage.error{color:var(--dsw-alias-state-error-primary)}
|
|
53
|
+
@keyframes codexPulse{to{background-position:-200% 0}}@keyframes codexQuotaPulse{to{opacity:.35}}@media(prefers-reduced-motion:reduce){.codexSkeleton,.codexQuotaLoading{animation:none}}@media(max-width:620px){.codexHero{padding:14px;flex-direction:column}.codexBody{padding:14px}.codexGrid{grid-template-columns:1fr}.codexCode{font-size:20px}}
|
|
54
|
+
`
|
|
55
|
+
if (typeof document !== 'undefined' && document.querySelector('style[data-plugin="' + PLUGIN_ID + '"]') === null) {
|
|
56
|
+
const style = document.createElement('style')
|
|
57
|
+
style.dataset.plugin = PLUGIN_ID
|
|
58
|
+
style.textContent = css
|
|
59
|
+
document.head.appendChild(style)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function messageOf(error) {
|
|
63
|
+
return error instanceof Error ? error.message : String(error)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function publishStatus(value) {
|
|
67
|
+
sharedStatus = value
|
|
68
|
+
for (const subscriber of statusSubscribers) subscriber(value)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function useSharedStatus() {
|
|
72
|
+
const [status, setStatus] = useState(sharedStatus)
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
statusSubscribers.add(setStatus)
|
|
75
|
+
return () => { statusSubscribers.delete(setStatus) }
|
|
76
|
+
}, [])
|
|
77
|
+
return status
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function requestStatus(refresh) {
|
|
81
|
+
const generation = ++statusRequestGeneration
|
|
82
|
+
const response = await fetch(BASE + '/status' + (refresh ? '?refresh=1' : ''), { cache: 'no-store' })
|
|
83
|
+
const value = await response.json()
|
|
84
|
+
if (!response.ok) throw new Error(value.error || 'HTTP ' + response.status)
|
|
85
|
+
if (generation === statusRequestGeneration) publishStatus(value)
|
|
86
|
+
return value
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function shortAccount(value) {
|
|
90
|
+
if (!value) return ''
|
|
91
|
+
return value.length > 22 ? value.slice(0, 10) + '…' + value.slice(-8) : value
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function formatReset(seconds) {
|
|
95
|
+
if (!Number.isFinite(seconds)) return '重置时间未知'
|
|
96
|
+
const date = new Date(seconds * 1000)
|
|
97
|
+
const remaining = date.getTime() - Date.now()
|
|
98
|
+
if (remaining <= 0) return '即将重置'
|
|
99
|
+
const hours = Math.floor(remaining / 3600000)
|
|
100
|
+
const minutes = Math.max(1, Math.floor((remaining % 3600000) / 60000))
|
|
101
|
+
const relative = hours >= 24 ? Math.floor(hours / 24) + ' 天后' : hours > 0 ? hours + ' 小时 ' + minutes + ' 分后' : minutes + ' 分后'
|
|
102
|
+
return relative + ' · ' + date.toLocaleString([], { month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' })
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function formatCountdown(expiresAt, now) {
|
|
106
|
+
const seconds = Math.max(0, Math.ceil((Number(expiresAt) - now) / 1000))
|
|
107
|
+
return Math.floor(seconds / 60) + ':' + String(seconds % 60).padStart(2, '0')
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function browserLoginAvailable(callbackUrl) {
|
|
111
|
+
if (!callbackUrl || typeof window === 'undefined') return false
|
|
112
|
+
try {
|
|
113
|
+
const callback = new URL(callbackUrl)
|
|
114
|
+
return callback.href === 'http://localhost:1455/auth/callback'
|
|
115
|
+
&& window.location.protocol === 'http:'
|
|
116
|
+
&& (window.location.hostname === '127.0.0.1' || window.location.hostname === 'localhost')
|
|
117
|
+
} catch {
|
|
118
|
+
return false
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function copyText(text) {
|
|
123
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
124
|
+
await navigator.clipboard.writeText(text)
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
const input = document.createElement('textarea')
|
|
128
|
+
input.value = text
|
|
129
|
+
input.style.position = 'fixed'
|
|
130
|
+
input.style.opacity = '0'
|
|
131
|
+
document.body.appendChild(input)
|
|
132
|
+
input.select()
|
|
133
|
+
document.execCommand('copy')
|
|
134
|
+
input.remove()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function UsageCard(props) {
|
|
138
|
+
const used = Math.max(0, Math.min(100, Number(props.window.usedPercent) || 0))
|
|
139
|
+
return h('div', { className: 'codexUsage' },
|
|
140
|
+
h('div', { className: 'codexUsageHead' },
|
|
141
|
+
h('span', { className: 'codexUsageName' }, props.name),
|
|
142
|
+
h('span', { className: 'codexUsageValue' }, '已用 ' + Math.round(used) + '% · 剩余 ' + Math.max(0, Math.round(100 - used)) + '%'),
|
|
143
|
+
),
|
|
144
|
+
h('div', { className: 'codexBar', role: 'progressbar', 'aria-valuemin': 0, 'aria-valuemax': 100, 'aria-valuenow': used },
|
|
145
|
+
h('div', { className: 'codexBarFill' + (used >= 80 ? ' high' : ''), style: { width: used + '%' } }),
|
|
146
|
+
),
|
|
147
|
+
h('div', { className: 'codexReset' }, formatReset(props.window.resetAt)),
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function quotaTone(used) {
|
|
152
|
+
return used >= 95 ? ' critical' : used >= 80 ? ' high' : ''
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function quotaWindowName(window, secondary) {
|
|
156
|
+
if (secondary) return '周额度'
|
|
157
|
+
return window && window.windowSeconds === 18000 ? '5 小时额度' : '短周期额度'
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function QuotaWindow(props) {
|
|
161
|
+
const used = Math.max(0, Math.min(100, Number(props.window.usedPercent) || 0))
|
|
162
|
+
const remaining = Math.max(0, Math.round(100 - used))
|
|
163
|
+
const tone = quotaTone(used)
|
|
164
|
+
return h('div', { className: 'codexQuotaWindow' },
|
|
165
|
+
h('div', { className: 'codexQuotaWindowHead' },
|
|
166
|
+
h('strong', null, props.name),
|
|
167
|
+
h('span', null, '剩余 ' + remaining + '%'),
|
|
168
|
+
),
|
|
169
|
+
h('div', { className: 'codexQuotaBar', role: 'progressbar', 'aria-label': props.name, 'aria-valuemin': 0, 'aria-valuemax': 100, 'aria-valuenow': used },
|
|
170
|
+
h('div', { className: 'codexQuotaBarFill' + tone, style: { width: used + '%' } }),
|
|
171
|
+
),
|
|
172
|
+
h('div', { className: 'codexQuotaReset' }, formatReset(props.window.resetAt)),
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function CodexQuotaRing(props) {
|
|
177
|
+
const running = props.useSession((snapshot) => snapshot.running)
|
|
178
|
+
const status = useSharedStatus()
|
|
179
|
+
const [open, setOpen] = useState(false)
|
|
180
|
+
const [loading, setLoading] = useState(false)
|
|
181
|
+
const [error, setError] = useState('')
|
|
182
|
+
const rootRef = useRef(null)
|
|
183
|
+
const wasRunning = useRef(running)
|
|
184
|
+
const loadGeneration = useRef(0)
|
|
185
|
+
|
|
186
|
+
const load = useCallback(async (refresh) => {
|
|
187
|
+
const generation = ++loadGeneration.current
|
|
188
|
+
if (refresh) setLoading(true)
|
|
189
|
+
try {
|
|
190
|
+
await requestStatus(refresh)
|
|
191
|
+
if (generation !== loadGeneration.current) return
|
|
192
|
+
setError('')
|
|
193
|
+
} catch (loadError) {
|
|
194
|
+
if (generation !== loadGeneration.current) return
|
|
195
|
+
setError(messageOf(loadError))
|
|
196
|
+
} finally {
|
|
197
|
+
if (generation === loadGeneration.current) setLoading(false)
|
|
198
|
+
}
|
|
199
|
+
}, [])
|
|
200
|
+
|
|
201
|
+
useEffect(() => { void load(false) }, [load])
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
const previous = wasRunning.current
|
|
204
|
+
wasRunning.current = running
|
|
205
|
+
if (previous && !running) void load(false)
|
|
206
|
+
}, [load, running])
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
if (!open) return undefined
|
|
209
|
+
const onPointerDown = (event) => {
|
|
210
|
+
if (event.target instanceof Node && rootRef.current && rootRef.current.contains(event.target)) return
|
|
211
|
+
setOpen(false)
|
|
212
|
+
}
|
|
213
|
+
const onKeyDown = (event) => { if (event.key === 'Escape') setOpen(false) }
|
|
214
|
+
document.addEventListener('pointerdown', onPointerDown)
|
|
215
|
+
document.addEventListener('keydown', onKeyDown)
|
|
216
|
+
return () => {
|
|
217
|
+
document.removeEventListener('pointerdown', onPointerDown)
|
|
218
|
+
document.removeEventListener('keydown', onKeyDown)
|
|
219
|
+
}
|
|
220
|
+
}, [open])
|
|
221
|
+
|
|
222
|
+
if (!status || !status.loggedIn) return null
|
|
223
|
+
const usage = status.usage
|
|
224
|
+
const primary = usage && usage.primary
|
|
225
|
+
const secondary = usage && usage.secondary
|
|
226
|
+
const meter = primary || secondary
|
|
227
|
+
const used = meter ? Math.max(0, Math.min(100, Number(meter.usedPercent) || 0)) : 0
|
|
228
|
+
const reading = meter ? Math.max(0, Math.round(100 - used)) + '%' : '待更新'
|
|
229
|
+
const title = loading ? '正在刷新 Codex 额度' : 'Codex 额度剩余 ' + reading
|
|
230
|
+
const circumference = 2 * Math.PI * 5.5
|
|
231
|
+
const windows = []
|
|
232
|
+
if (primary) windows.push({ name: quotaWindowName(primary, false), window: primary })
|
|
233
|
+
if (secondary) windows.push({ name: quotaWindowName(secondary, true), window: secondary })
|
|
234
|
+
|
|
235
|
+
return h('span', { ref: rootRef, className: 'codexQuotaRoot' },
|
|
236
|
+
h('button', {
|
|
237
|
+
type: 'button',
|
|
238
|
+
className: 'codexQuotaTrigger' + (loading ? ' codexQuotaLoading' : ''),
|
|
239
|
+
title,
|
|
240
|
+
'aria-label': title,
|
|
241
|
+
'aria-haspopup': 'dialog',
|
|
242
|
+
'aria-expanded': open,
|
|
243
|
+
onClick: () => {
|
|
244
|
+
setOpen((value) => !value)
|
|
245
|
+
void load(true)
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
h('svg', { viewBox: '0 0 14 14', width: '14', height: '14', 'aria-hidden': true },
|
|
249
|
+
h('circle', { className: 'codexQuotaTrack', cx: '7', cy: '7', r: '5.5' }),
|
|
250
|
+
meter ? h('circle', {
|
|
251
|
+
className: 'codexQuotaAvailable' + quotaTone(used),
|
|
252
|
+
cx: '7', cy: '7', r: '5.5',
|
|
253
|
+
}) : null,
|
|
254
|
+
meter && used > 0 ? h('circle', {
|
|
255
|
+
className: 'codexQuotaConsumed',
|
|
256
|
+
cx: '7', cy: '7', r: '5.5',
|
|
257
|
+
strokeDasharray: (circumference * used / 100) + ' ' + circumference,
|
|
258
|
+
transform: 'rotate(-90 7 7)',
|
|
259
|
+
}) : null,
|
|
260
|
+
)),
|
|
261
|
+
open ? h('div', { className: 'codexQuotaPanel', role: 'dialog', 'aria-label': 'OpenAI Codex 额度' },
|
|
262
|
+
h('div', { className: 'codexQuotaPanelHead' },
|
|
263
|
+
h('strong', null, 'Codex 额度'),
|
|
264
|
+
h('span', null, loading ? '刷新中…' : usage && usage.fetchedAt ? new Date(usage.fetchedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '尚未读取'),
|
|
265
|
+
),
|
|
266
|
+
windows.length > 0
|
|
267
|
+
? windows.map((row) => h(QuotaWindow, { key: row.name, name: row.name, window: row.window }))
|
|
268
|
+
: h('p', { className: 'codexQuotaMessage' }, '点击圆圈读取最新额度。'),
|
|
269
|
+
status.usageError ? h('p', { className: 'codexQuotaMessage error', role: 'status' }, '额度读取失败:' + status.usageError) : null,
|
|
270
|
+
error ? h('p', { className: 'codexQuotaMessage error', role: 'status' }, '无法连接额度接口:' + error) : null,
|
|
271
|
+
) : null,
|
|
272
|
+
)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function CodexSection() {
|
|
276
|
+
const status = useSharedStatus()
|
|
277
|
+
const [error, setError] = useState('')
|
|
278
|
+
const [busy, setBusy] = useState('')
|
|
279
|
+
const [watchLogin, setWatchLogin] = useState(false)
|
|
280
|
+
const [copied, setCopied] = useState(false)
|
|
281
|
+
const [probe, setProbe] = useState({ state: 'idle', message: '' })
|
|
282
|
+
const [manualInput, setManualInput] = useState('')
|
|
283
|
+
const [now, setNow] = useState(Date.now())
|
|
284
|
+
const probeGeneration = useRef(0)
|
|
285
|
+
|
|
286
|
+
const load = useCallback(async (refresh) => {
|
|
287
|
+
try {
|
|
288
|
+
const value = await requestStatus(refresh)
|
|
289
|
+
setError('')
|
|
290
|
+
setWatchLogin(Boolean(value.loginPending))
|
|
291
|
+
} catch (loadError) {
|
|
292
|
+
setError('无法连接 DSH Web 的 Codex 同源路由。请重启 Web profile 后再试。' + (messageOf(loadError) ? ' (' + messageOf(loadError) + ')' : ''))
|
|
293
|
+
}
|
|
294
|
+
}, [])
|
|
295
|
+
|
|
296
|
+
useEffect(() => {
|
|
297
|
+
void load(false)
|
|
298
|
+
if (!watchLogin) return undefined
|
|
299
|
+
const timer = window.setInterval(() => { void load(false) }, 2000)
|
|
300
|
+
return () => { window.clearInterval(timer) }
|
|
301
|
+
}, [load, watchLogin])
|
|
302
|
+
|
|
303
|
+
useEffect(() => {
|
|
304
|
+
const expiresAt = status && ((status.device && status.device.expiresAt) || (status.browser && status.browser.expiresAt))
|
|
305
|
+
if (!expiresAt) return undefined
|
|
306
|
+
setNow(Date.now())
|
|
307
|
+
const timer = window.setInterval(() => { setNow(Date.now()) }, 1000)
|
|
308
|
+
return () => { window.clearInterval(timer) }
|
|
309
|
+
}, [status && ((status.device && status.device.expiresAt) || (status.browser && status.browser.expiresAt))])
|
|
310
|
+
|
|
311
|
+
const post = useCallback(async (path, body) => {
|
|
312
|
+
if (!status || !status.csrf) throw new Error('登录状态尚未加载')
|
|
313
|
+
const headers = { 'x-dsh-csrf': status.csrf }
|
|
314
|
+
if (body !== undefined) headers['content-type'] = 'application/json'
|
|
315
|
+
const response = await fetch(BASE + path, {
|
|
316
|
+
method: 'POST',
|
|
317
|
+
headers,
|
|
318
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
319
|
+
})
|
|
320
|
+
const value = await response.json()
|
|
321
|
+
if (!response.ok) {
|
|
322
|
+
const problem = new Error(value.error || 'HTTP ' + response.status)
|
|
323
|
+
problem.code = value.code
|
|
324
|
+
throw problem
|
|
325
|
+
}
|
|
326
|
+
return value
|
|
327
|
+
}, [status])
|
|
328
|
+
|
|
329
|
+
const startDevice = async () => {
|
|
330
|
+
setBusy('device')
|
|
331
|
+
setCopied(false)
|
|
332
|
+
setError('')
|
|
333
|
+
try {
|
|
334
|
+
await post('/device/start')
|
|
335
|
+
setWatchLogin(true)
|
|
336
|
+
await load(false)
|
|
337
|
+
} catch (startError) {
|
|
338
|
+
const fallback = startError && startError.code === 'device_code_unavailable' ? ' 可改用下方本机浏览器 OAuth。' : ''
|
|
339
|
+
setError(messageOf(startError) + fallback)
|
|
340
|
+
} finally {
|
|
341
|
+
setBusy('')
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const probeBrowser = useCallback(async (browser) => {
|
|
346
|
+
if (!browser || !browser.probeUrl) return
|
|
347
|
+
const generation = ++probeGeneration.current
|
|
348
|
+
setProbe({ state: 'checking', message: '正在从当前浏览器检测 127.0.0.1:1455…' })
|
|
349
|
+
const controller = new AbortController()
|
|
350
|
+
const timer = window.setTimeout(() => { controller.abort() }, 4000)
|
|
351
|
+
try {
|
|
352
|
+
const response = await fetch(browser.probeUrl, { cache: 'no-store', mode: 'cors', signal: controller.signal })
|
|
353
|
+
const value = await response.json().catch(() => ({}))
|
|
354
|
+
if (!response.ok || value.ok !== true) throw new Error(value.error || 'HTTP ' + response.status)
|
|
355
|
+
if (generation === probeGeneration.current) setProbe({ state: 'ok', message: '当前浏览器可以访问 callback 端口,可以继续登录。' })
|
|
356
|
+
} catch (probeError) {
|
|
357
|
+
const reason = probeError && probeError.name === 'AbortError' ? '检测超时' : messageOf(probeError)
|
|
358
|
+
if (generation === probeGeneration.current) setProbe({ state: 'failed', message: '无法从当前浏览器访问 127.0.0.1:1455。远程 DSH 请转发 1455,然后重新检测。' + (reason ? ' (' + reason + ')' : '') })
|
|
359
|
+
} finally {
|
|
360
|
+
window.clearTimeout(timer)
|
|
361
|
+
}
|
|
362
|
+
}, [])
|
|
363
|
+
|
|
364
|
+
useEffect(() => {
|
|
365
|
+
if (status && status.browser && probe.state === 'idle') void probeBrowser(status.browser)
|
|
366
|
+
}, [status && status.browser && status.browser.probeUrl, probe.state, probeBrowser])
|
|
367
|
+
|
|
368
|
+
const startBrowser = async () => {
|
|
369
|
+
if (!(status && browserLoginAvailable(status.browserCallbackUrl))) return
|
|
370
|
+
setBusy('browser')
|
|
371
|
+
probeGeneration.current += 1
|
|
372
|
+
setError('')
|
|
373
|
+
setManualInput('')
|
|
374
|
+
setProbe({ state: 'checking', message: '正在启动 callback listener…' })
|
|
375
|
+
try {
|
|
376
|
+
const browser = await post('/browser/prepare')
|
|
377
|
+
setWatchLogin(true)
|
|
378
|
+
await load(false)
|
|
379
|
+
await probeBrowser(browser)
|
|
380
|
+
} catch (startError) {
|
|
381
|
+
setProbe({ state: 'failed', message: messageOf(startError) })
|
|
382
|
+
setError(messageOf(startError))
|
|
383
|
+
} finally {
|
|
384
|
+
setBusy('')
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const openBrowser = () => {
|
|
389
|
+
const browser = status && status.browser
|
|
390
|
+
if (!browser || !browser.authorizationUrl) return
|
|
391
|
+
const popup = window.open(browser.authorizationUrl, 'dsh-openai-codex-login', 'popup,width=560,height=760')
|
|
392
|
+
if (popup === null) setError('浏览器阻止了登录窗口,请允许此站点打开弹窗后重试。')
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const completeBrowser = async () => {
|
|
396
|
+
if (!manualInput.trim()) return
|
|
397
|
+
setBusy('complete')
|
|
398
|
+
probeGeneration.current += 1
|
|
399
|
+
setError('')
|
|
400
|
+
try {
|
|
401
|
+
await post('/browser/complete', { input: manualInput })
|
|
402
|
+
setManualInput('')
|
|
403
|
+
setWatchLogin(false)
|
|
404
|
+
await load(false)
|
|
405
|
+
} catch (completeError) {
|
|
406
|
+
setError('手动完成登录失败:' + messageOf(completeError))
|
|
407
|
+
} finally {
|
|
408
|
+
setBusy('')
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const cancel = async () => {
|
|
413
|
+
setBusy('cancel')
|
|
414
|
+
probeGeneration.current += 1
|
|
415
|
+
try {
|
|
416
|
+
await post('/cancel')
|
|
417
|
+
setWatchLogin(false)
|
|
418
|
+
setProbe({ state: 'idle', message: '' })
|
|
419
|
+
setManualInput('')
|
|
420
|
+
await load(false)
|
|
421
|
+
} catch (cancelError) {
|
|
422
|
+
setError(messageOf(cancelError))
|
|
423
|
+
} finally {
|
|
424
|
+
setBusy('')
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const logout = async () => {
|
|
429
|
+
setBusy('logout')
|
|
430
|
+
probeGeneration.current += 1
|
|
431
|
+
try {
|
|
432
|
+
await post('/logout')
|
|
433
|
+
setWatchLogin(false)
|
|
434
|
+
setProbe({ state: 'idle', message: '' })
|
|
435
|
+
setManualInput('')
|
|
436
|
+
await load(false)
|
|
437
|
+
} catch (logoutError) {
|
|
438
|
+
setError(messageOf(logoutError))
|
|
439
|
+
} finally {
|
|
440
|
+
setBusy('')
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const refresh = async () => {
|
|
445
|
+
setBusy('refresh')
|
|
446
|
+
await load(true)
|
|
447
|
+
setBusy('')
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const copyCode = async () => {
|
|
451
|
+
if (!(status && status.device)) return
|
|
452
|
+
try {
|
|
453
|
+
await copyText(status.device.userCode)
|
|
454
|
+
setCopied(true)
|
|
455
|
+
window.setTimeout(() => { setCopied(false) }, 1600)
|
|
456
|
+
} catch (copyError) {
|
|
457
|
+
setError('复制失败:' + messageOf(copyError))
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const usage = status && status.usage
|
|
462
|
+
const loading = status === null && !error
|
|
463
|
+
const connected = Boolean(status && status.loggedIn)
|
|
464
|
+
const pending = Boolean(status && status.loginPending) || watchLogin
|
|
465
|
+
const devicePending = Boolean(status && status.loginMethod === 'device' && status.device)
|
|
466
|
+
const browserPending = Boolean(status && status.loginMethod === 'browser' && status.browser)
|
|
467
|
+
const browserAvailable = Boolean(status && browserLoginAvailable(status.browserCallbackUrl))
|
|
468
|
+
const plan = usage && usage.planType ? String(usage.planType).toUpperCase() : 'ChatGPT 订阅'
|
|
469
|
+
const windows = useMemo(() => {
|
|
470
|
+
if (!usage) return []
|
|
471
|
+
const rows = []
|
|
472
|
+
if (usage.primary) rows.push({ name: usage.primary.windowSeconds === 18000 ? '5 小时额度' : '短周期额度', window: usage.primary })
|
|
473
|
+
if (usage.secondary) rows.push({ name: '周额度', window: usage.secondary })
|
|
474
|
+
return rows
|
|
475
|
+
}, [usage])
|
|
476
|
+
|
|
477
|
+
return h('section', { className: 'codexSection' },
|
|
478
|
+
h('h2', { className: 'codexTitle' }, 'OpenAI Codex'),
|
|
479
|
+
h('p', { className: 'codexIntro' }, '优先使用设备码连接 ChatGPT 订阅;无需本机回调,适合 SSH 和反向代理。登录结果自动用于“模型提供方”中的 openai-codex。'),
|
|
480
|
+
h('div', { className: 'codexCard' },
|
|
481
|
+
h('div', { className: 'codexHero' },
|
|
482
|
+
h('div', { className: 'codexBrand' },
|
|
483
|
+
h('div', { className: 'codexLogo', 'aria-hidden': true }, 'OA'),
|
|
484
|
+
h('div', null,
|
|
485
|
+
h('h3', { className: 'codexName' }, 'OpenAI Codex 订阅'),
|
|
486
|
+
h('p', { className: 'codexMeta', title: connected ? status.accountId : '' }, loading ? '正在读取 OpenAI 登录状态' : connected ? shortAccount(status.accountId) : '尚未连接 ChatGPT 账号'),
|
|
487
|
+
),
|
|
488
|
+
),
|
|
489
|
+
h('span', { className: 'codexBadge ' + (loading || pending ? 'pending' : connected ? 'connected' : '') },
|
|
490
|
+
h('span', { className: 'codexDot', 'aria-hidden': true }), loading ? '刷新中…' : pending ? '等待授权' : connected ? '已连接' : '未登录',
|
|
491
|
+
),
|
|
492
|
+
),
|
|
493
|
+
h('div', { className: 'codexBody' },
|
|
494
|
+
loading
|
|
495
|
+
? h('div', { 'aria-label': '加载中' }, h('div', { className: 'codexSkeleton' }), h('div', { className: 'codexSkeleton', style: { width: '72%' } }))
|
|
496
|
+
: connected
|
|
497
|
+
? h(React.Fragment, null,
|
|
498
|
+
h('div', { className: 'codexPlan' }, h('strong', null, plan), h('span', null, usage && usage.fetchedAt ? '更新于 ' + new Date(usage.fetchedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '额度信息待更新')),
|
|
499
|
+
windows.length > 0
|
|
500
|
+
? h('div', { className: 'codexGrid' }, windows.map((row) => h(UsageCard, { key: row.name, name: row.name, window: row.window })))
|
|
501
|
+
: h('p', { className: 'codexEmpty' }, '账号已连接,暂时没有返回可展示的额度窗口。'),
|
|
502
|
+
usage && Number.isFinite(usage.resetCredits) ? h('p', { className: 'codexNotice' }, '可用额度重置次数:' + usage.resetCredits) : null,
|
|
503
|
+
status.usageError ? h('p', { className: 'codexError', role: 'status' }, '额度读取失败:' + status.usageError) : null,
|
|
504
|
+
)
|
|
505
|
+
: h('p', { className: 'codexEmpty' }, '设备码登录可在任意设备完成授权,令牌只由 Host 保存和刷新,Web 页面不会读取令牌。'),
|
|
506
|
+
devicePending
|
|
507
|
+
? h('div', { className: 'codexDevice' },
|
|
508
|
+
h('h4', { className: 'codexDeviceTitle' }, '在 OpenAI 输入设备码'),
|
|
509
|
+
h('p', { className: 'codexDeviceHelp' }, '先复制下方代码,再按需点击“打开 OpenAI 验证页”并输入代码。此页面会自动等待结果。'),
|
|
510
|
+
h('code', { className: 'codexCode' }, status.device.userCode),
|
|
511
|
+
h('div', { className: 'codexDeviceMeta' }, '代码剩余 ' + formatCountdown(status.device.expiresAt, now)),
|
|
512
|
+
h('div', { className: 'codexDeviceActions' },
|
|
513
|
+
h('button', { type: 'button', className: 'codexButton', onClick: () => { void copyCode() } }, copied ? '已复制' : '复制代码'),
|
|
514
|
+
h('a', { className: 'codexButton primary', href: status.device.verificationUri, target: '_blank', rel: 'noreferrer' }, '打开 OpenAI 验证页'),
|
|
515
|
+
h('button', { type: 'button', className: 'codexButton danger', disabled: Boolean(busy), onClick: () => { void cancel() } }, busy === 'cancel' ? '取消中…' : '取消登录'),
|
|
516
|
+
),
|
|
517
|
+
)
|
|
518
|
+
: null,
|
|
519
|
+
browserPending
|
|
520
|
+
? h('div', { className: 'codexBrowser' },
|
|
521
|
+
h('div', { className: 'codexBrowserHead' },
|
|
522
|
+
h('div', null,
|
|
523
|
+
h('h4', { className: 'codexDeviceTitle' }, '浏览器 OAuth callback 检测'),
|
|
524
|
+
h('p', { className: 'codexDeviceHelp' }, '先确认当前浏览器能访问本机 callback,再由你点击按钮打开 OpenAI。'),
|
|
525
|
+
),
|
|
526
|
+
h('span', { className: 'codexBadge pending' }, '剩余 ' + formatCountdown(status.browser.expiresAt, now)),
|
|
527
|
+
),
|
|
528
|
+
h('div', { className: 'codexProbe ' + (probe.state === 'ok' ? 'ok' : probe.state === 'failed' ? 'failed' : '') },
|
|
529
|
+
h('span', { className: 'codexProbeIcon', 'aria-hidden': true }, probe.state === 'ok' ? '✓' : probe.state === 'failed' ? '!' : '…'),
|
|
530
|
+
h('div', { className: 'codexProbeText' },
|
|
531
|
+
h('strong', null, probe.state === 'ok' ? 'Callback 通道正常' : probe.state === 'failed' ? 'Callback 通道不可达' : '正在检测 callback 通道'),
|
|
532
|
+
h('span', null, probe.message || '正在检测当前浏览器到 127.0.0.1:1455 的连接。'),
|
|
533
|
+
h('code', { className: 'codexProbeEndpoint' }, 'http://127.0.0.1:1455 → localhost:1455/auth/callback'),
|
|
534
|
+
),
|
|
535
|
+
),
|
|
536
|
+
probe.state === 'failed'
|
|
537
|
+
? h('p', { className: 'codexWarning' }, '请先开放或转发端口。例如远程 SSH 可增加:-L 1455:127.0.0.1:1455。打通后点击“重新检测”,也可以选择强制继续。')
|
|
538
|
+
: null,
|
|
539
|
+
h('div', { className: 'codexActions' },
|
|
540
|
+
h('button', { type: 'button', className: 'codexButton', disabled: Boolean(busy) || probe.state === 'checking', onClick: () => { void probeBrowser(status.browser) } }, probe.state === 'checking' ? '检测中…' : '重新检测'),
|
|
541
|
+
h('button', { type: 'button', className: 'codexButton primary', disabled: Boolean(busy), onClick: openBrowser }, probe.state === 'failed' ? '强制继续打开 OpenAI' : '打开 OpenAI 登录页'),
|
|
542
|
+
h('button', { type: 'button', className: 'codexButton danger', disabled: Boolean(busy), onClick: () => { void cancel() } }, busy === 'cancel' ? '取消中…' : '取消登录'),
|
|
543
|
+
),
|
|
544
|
+
h('label', { className: 'codexField' },
|
|
545
|
+
h('span', { className: 'codexFieldLabel' }, '手动完成(可粘贴完整 callback URL 或 authorization code)'),
|
|
546
|
+
h('input', {
|
|
547
|
+
className: 'codexInput', type: 'text', value: manualInput,
|
|
548
|
+
placeholder: 'http://localhost:1455/auth/callback?code=…&state=…',
|
|
549
|
+
autoComplete: 'off', spellCheck: false,
|
|
550
|
+
onChange: (event) => { setManualInput(event.target.value) },
|
|
551
|
+
}),
|
|
552
|
+
),
|
|
553
|
+
h('div', { className: 'codexActions' },
|
|
554
|
+
h('button', { type: 'button', className: 'codexButton', disabled: Boolean(busy) || !manualInput.trim(), onClick: () => { void completeBrowser() } }, busy === 'complete' ? '提交中…' : '提交 code'),
|
|
555
|
+
),
|
|
556
|
+
)
|
|
557
|
+
: null,
|
|
558
|
+
status && status.loginError ? h('p', { className: 'codexError', role: 'alert' }, '登录失败:' + status.loginError) : null,
|
|
559
|
+
status && status.credentialError ? h('p', { className: 'codexError', role: 'alert' }, '凭据文件无法读取:' + status.credentialError + '。可点击退出登录删除并重新创建。') : null,
|
|
560
|
+
error ? h('p', { className: 'codexError', role: 'alert' }, error) : null,
|
|
561
|
+
!loading && !browserAvailable
|
|
562
|
+
? h('p', { className: 'codexWarning' }, '本机浏览器 OAuth 仅支持通过 HTTP 127.0.0.1 或 localhost 打开 DSH。当前入口请使用设备码登录。')
|
|
563
|
+
: null,
|
|
564
|
+
h('div', { className: 'codexActions' },
|
|
565
|
+
h('button', { type: 'button', className: 'codexButton primary', disabled: Boolean(busy) || pending || loading, onClick: () => { void startDevice() } },
|
|
566
|
+
busy === 'device' ? '正在申请设备码…' : connected ? '用设备码重新登录' : '使用设备码登录'),
|
|
567
|
+
h('button', {
|
|
568
|
+
type: 'button', className: 'codexButton', disabled: Boolean(busy) || pending || loading || !browserAvailable,
|
|
569
|
+
title: browserAvailable ? '展开 callback 检测,不会自动弹出窗口' : '仅支持 HTTP 127.0.0.1/localhost 入口', onClick: () => { void startBrowser() },
|
|
570
|
+
}, busy === 'browser' ? '正在准备浏览器登录…' : connected ? '用浏览器重新登录' : '本机浏览器 OAuth'),
|
|
571
|
+
pending && !devicePending && !browserPending ? h('button', { type: 'button', className: 'codexButton danger', disabled: Boolean(busy), onClick: () => { void cancel() } }, busy === 'cancel' ? '取消中…' : '取消登录') : null,
|
|
572
|
+
connected ? h('button', { type: 'button', className: 'codexButton', disabled: Boolean(busy), onClick: refresh }, busy === 'refresh' ? '刷新中…' : '刷新用量') : null,
|
|
573
|
+
connected || (status && status.credentialError) ? h('button', { type: 'button', className: 'codexButton danger', disabled: Boolean(busy), onClick: () => { void logout() } }, busy === 'logout' ? '退出中…' : '退出登录') : null,
|
|
574
|
+
),
|
|
575
|
+
),
|
|
576
|
+
),
|
|
577
|
+
h('p', { className: 'codexNotice' }, '设备码与浏览器登录都只在你点击对应按钮后打开外部页面。浏览器 OAuth 会临时监听 localhost:1455;远程 DSH 还需额外转发 1455。'),
|
|
578
|
+
)
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const inject = ['slots']
|
|
582
|
+
function apply(ctx) {
|
|
583
|
+
ctx.slots.inject('settings.section', () => ctx.slots.register({
|
|
584
|
+
name: 'settings.section',
|
|
585
|
+
id: 'openai-codex',
|
|
586
|
+
order: 11,
|
|
587
|
+
label: () => 'OpenAI Codex',
|
|
588
|
+
}, CodexSection))
|
|
589
|
+
ctx.slots.inject('conversation.input.right', () => ctx.slots.register({
|
|
590
|
+
name: 'conversation.input.right',
|
|
591
|
+
id: 'openai-codex-quota',
|
|
592
|
+
order: 100,
|
|
593
|
+
label: () => 'OpenAI Codex 额度',
|
|
594
|
+
}, CodexQuotaRing))
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
exports.inject = inject
|
|
598
|
+
exports.apply = apply
|
|
599
|
+
return module.exports
|
|
600
|
+
},
|
|
601
|
+
})
|