dsh-oauth-login 0.2.4
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/INSTALL.md +59 -0
- package/INSTALL.zh.md +45 -0
- package/LICENSE +201 -0
- package/README.en.md +104 -0
- package/README.md +104 -0
- package/README.zh.md +3 -0
- package/SECURITY.md +33 -0
- package/cordis.patch.yml +5 -0
- package/docs/network-proxy.md +45 -0
- package/docs/openrouter-sync.md +84 -0
- package/docs/screenshots/01-providers-signed-out.png +0 -0
- package/docs/screenshots/03-copilot-signing-in.png +0 -0
- package/docs/screenshots/04-mixed-live-states.png +0 -0
- package/docs/screenshots/05-copilot-sign-in.gif +0 -0
- package/docs/screenshots/06-cli-status-store.png +0 -0
- package/lib/bin.d.ts +5 -0
- package/lib/bin.js +2132 -0
- package/lib/client.js +1915 -0
- package/lib/client.js.map +1 -0
- package/lib/index.d.ts +482 -0
- package/lib/index.js +3105 -0
- package/lib/invariant.d.ts +7 -0
- package/lib/invariant.js +8 -0
- package/package.json +180 -0
- package/src/adapter.ts +207 -0
- package/src/auth-routes.ts +654 -0
- package/src/auth.ts +76 -0
- package/src/bin.ts +185 -0
- package/src/catalog.ts +129 -0
- package/src/client/OpenRouterSyncStatus.tsx +66 -0
- package/src/client/PiLoginSettings.tsx +470 -0
- package/src/client/ProxySettings.tsx +123 -0
- package/src/client/SearchableModelSelect.tsx +595 -0
- package/src/client/draft-input.ts +12 -0
- package/src/client/index.tsx +71 -0
- package/src/client/locales.ts +176 -0
- package/src/client/login-input-copy.ts +34 -0
- package/src/client/login-window.ts +24 -0
- package/src/client/model-filter.ts +45 -0
- package/src/client/openrouter-store.ts +122 -0
- package/src/client/proxy-draft.ts +57 -0
- package/src/extra-models.ts +201 -0
- package/src/hosted-capture.ts +41 -0
- package/src/hosted-images.ts +154 -0
- package/src/http.ts +241 -0
- package/src/ids.ts +17 -0
- package/src/index.ts +183 -0
- package/src/invariant.ts +12 -0
- package/src/model-error-hint.ts +98 -0
- package/src/native-tools.ts +442 -0
- package/src/oauth-refresh.ts +49 -0
- package/src/openrouter-catalog.ts +245 -0
- package/src/openrouter-free.ts +76 -0
- package/src/openrouter-models.ts +155 -0
- package/src/openrouter-types.ts +40 -0
- package/src/plugin-config.ts +30 -0
- package/src/provider.ts +137 -0
- package/src/proxy-config.ts +61 -0
- package/src/proxy-store.ts +45 -0
- package/src/proxy-transport.ts +220 -0
- package/src/redact.ts +22 -0
- package/src/responses-tap.ts +97 -0
- package/src/session.ts +104 -0
- package/src/store.ts +234 -0
package/src/auth.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/** DSH-owned models.login() for every subscribed provider in the catalog. */
|
|
2
|
+
|
|
3
|
+
import { createModels } from '@earendil-works/pi-ai'
|
|
4
|
+
import { dirname, join } from 'node:path'
|
|
5
|
+
import type { AuthInteraction } from '@earendil-works/pi-ai'
|
|
6
|
+
import { requirePiLoginProvider } from './catalog.ts'
|
|
7
|
+
import { OAuthProxyTransport } from './proxy-transport.ts'
|
|
8
|
+
import { PROXY_SETTINGS_FILENAME } from './proxy-config.ts'
|
|
9
|
+
import { catalogProvider } from './provider.ts'
|
|
10
|
+
import type { PiLoginSession } from './session.ts'
|
|
11
|
+
import { PiLoginCredentialStore } from './store.ts'
|
|
12
|
+
|
|
13
|
+
export interface PiLoginAuthStatus {
|
|
14
|
+
providerId: string
|
|
15
|
+
authenticated: boolean
|
|
16
|
+
credentialType?: 'oauth' | 'api_key'
|
|
17
|
+
expiresAt?: Date
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function loginPiProvider(
|
|
21
|
+
providerId: string,
|
|
22
|
+
interaction: AuthInteraction,
|
|
23
|
+
store: PiLoginCredentialStore = new PiLoginCredentialStore(),
|
|
24
|
+
): Promise<void> {
|
|
25
|
+
const spec = requirePiLoginProvider(providerId)
|
|
26
|
+
const models = createModels({ credentials: store })
|
|
27
|
+
models.setProvider(catalogProvider(providerId))
|
|
28
|
+
const proxy = new OAuthProxyTransport(join(dirname(store.filename), PROXY_SETTINGS_FILENAME))
|
|
29
|
+
try { await proxy.run(() => models.login(providerId, spec.authType, interaction)) } finally { await proxy.dispose() }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function logoutPiProvider(
|
|
33
|
+
providerId: string,
|
|
34
|
+
store: PiLoginCredentialStore = new PiLoginCredentialStore(),
|
|
35
|
+
): Promise<void> {
|
|
36
|
+
requirePiLoginProvider(providerId)
|
|
37
|
+
await store.delete(providerId)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function piLoginStatus(
|
|
41
|
+
store: PiLoginCredentialStore = new PiLoginCredentialStore(),
|
|
42
|
+
providerId?: string,
|
|
43
|
+
): Promise<PiLoginAuthStatus[]> {
|
|
44
|
+
const ids = providerId === undefined ? (await store.list()).map(item => item.providerId) : [providerId]
|
|
45
|
+
const out: PiLoginAuthStatus[] = []
|
|
46
|
+
for (const id of ids) {
|
|
47
|
+
const credential = await store.read(id)
|
|
48
|
+
if (credential?.type === 'oauth') {
|
|
49
|
+
out.push({
|
|
50
|
+
providerId: id,
|
|
51
|
+
authenticated: true,
|
|
52
|
+
credentialType: 'oauth',
|
|
53
|
+
expiresAt: new Date(credential.expires),
|
|
54
|
+
})
|
|
55
|
+
continue
|
|
56
|
+
}
|
|
57
|
+
if (credential?.type === 'api_key' && typeof credential.key === 'string' && credential.key.length > 0) {
|
|
58
|
+
out.push({ providerId: id, authenticated: true, credentialType: 'api_key' })
|
|
59
|
+
continue
|
|
60
|
+
}
|
|
61
|
+
out.push({ providerId: id, authenticated: false })
|
|
62
|
+
}
|
|
63
|
+
return out
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function loginPiProviderSession(
|
|
67
|
+
providerId: string,
|
|
68
|
+
interaction: AuthInteraction,
|
|
69
|
+
session: PiLoginSession,
|
|
70
|
+
): Promise<void> {
|
|
71
|
+
const spec = requirePiLoginProvider(providerId)
|
|
72
|
+
await session.ensureTransport()
|
|
73
|
+
session.models.setProvider(catalogProvider(providerId))
|
|
74
|
+
await session.proxy.run(() => session.models.login(providerId, spec.authType, interaction))
|
|
75
|
+
if (providerId === 'openrouter') await session.openRouter.syncAuthentication('login')
|
|
76
|
+
}
|
package/src/bin.ts
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** Standalone credential CLI. Never reads or writes official CLI auth files. */
|
|
3
|
+
|
|
4
|
+
import { spawn } from 'node:child_process'
|
|
5
|
+
import { realpathSync } from 'node:fs'
|
|
6
|
+
import { createInterface } from 'node:readline/promises'
|
|
7
|
+
import { fileURLToPath } from 'node:url'
|
|
8
|
+
import type { AuthEvent, AuthPrompt } from '@earendil-works/pi-ai'
|
|
9
|
+
import { PI_LOGIN_PROVIDERS, requirePiLoginProvider } from './catalog.ts'
|
|
10
|
+
import {
|
|
11
|
+
loginPiProviderSession,
|
|
12
|
+
piLoginAuthPath,
|
|
13
|
+
piLoginStatus,
|
|
14
|
+
PiLoginSession,
|
|
15
|
+
} from './index.ts'
|
|
16
|
+
import { isSafeAuthUrl, safeMessage } from './redact.ts'
|
|
17
|
+
|
|
18
|
+
type Action = 'login' | 'logout' | 'status'
|
|
19
|
+
|
|
20
|
+
function openBrowser(rawUrl: string, providerId: string): void {
|
|
21
|
+
const spec = requirePiLoginProvider(providerId)
|
|
22
|
+
if (!isSafeAuthUrl(rawUrl, spec)) {
|
|
23
|
+
throw new Error(`refusing to open authorization URL outside ${spec.displayName} official hosts`)
|
|
24
|
+
}
|
|
25
|
+
const url = new URL(rawUrl)
|
|
26
|
+
const command = process.platform === 'win32'
|
|
27
|
+
? { file: 'rundll32.exe', args: ['url.dll,FileProtocolHandler', url.href] }
|
|
28
|
+
: process.platform === 'darwin'
|
|
29
|
+
? { file: 'open', args: [url.href] }
|
|
30
|
+
: { file: 'xdg-open', args: [url.href] }
|
|
31
|
+
try {
|
|
32
|
+
const child = spawn(command.file, command.args, {
|
|
33
|
+
detached: true,
|
|
34
|
+
stdio: 'ignore',
|
|
35
|
+
windowsHide: true,
|
|
36
|
+
})
|
|
37
|
+
child.on('error', () => {})
|
|
38
|
+
child.unref()
|
|
39
|
+
} catch {
|
|
40
|
+
// printed URL remains the fallback
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function notify(event: AuthEvent, providerId: string, useBrowser: boolean): void {
|
|
45
|
+
switch (event.type) {
|
|
46
|
+
case 'auth_url':
|
|
47
|
+
process.stdout.write(`Open this URL to sign in:\n${event.url}\n`)
|
|
48
|
+
if (event.instructions !== undefined) process.stdout.write(`${event.instructions}\n`)
|
|
49
|
+
if (useBrowser) openBrowser(event.url, providerId)
|
|
50
|
+
break
|
|
51
|
+
case 'device_code':
|
|
52
|
+
process.stdout.write(`Open this URL to sign in:\n${event.verificationUri}\n`)
|
|
53
|
+
if (event.userCode.length > 0) process.stdout.write(`Enter code: ${event.userCode}\n`)
|
|
54
|
+
if (useBrowser) openBrowser(event.verificationUri, providerId)
|
|
55
|
+
break
|
|
56
|
+
case 'info':
|
|
57
|
+
case 'progress':
|
|
58
|
+
process.stdout.write(`${event.message}\n`)
|
|
59
|
+
break
|
|
60
|
+
default:
|
|
61
|
+
event satisfies never
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function answerPrompt(
|
|
66
|
+
prompt: AuthPrompt,
|
|
67
|
+
question: (text: string, options: { signal?: AbortSignal }) => Promise<string>,
|
|
68
|
+
): Promise<string> {
|
|
69
|
+
if (prompt.type === 'select') {
|
|
70
|
+
const oauth = prompt.options.find(option => option.id === 'oauth' || option.id.includes('oauth'))
|
|
71
|
+
const browser = prompt.options.find(option => option.id.includes('browser'))
|
|
72
|
+
return oauth?.id ?? browser?.id ?? prompt.options[0]?.id ?? 'oauth'
|
|
73
|
+
}
|
|
74
|
+
const suffix = prompt.placeholder === undefined ? '' : ` (${prompt.placeholder})`
|
|
75
|
+
return question(`${prompt.message}${suffix}: `, {
|
|
76
|
+
...prompt.signal === undefined ? {} : { signal: prompt.signal },
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function printHelp(): void {
|
|
81
|
+
const ids = PI_LOGIN_PROVIDERS.map(provider => ` ${provider.id.padEnd(18)} ${provider.displayName}`).join('\n')
|
|
82
|
+
process.stdout.write([
|
|
83
|
+
'Usage: dsh-oauth-login <login|logout|status> [provider]',
|
|
84
|
+
'',
|
|
85
|
+
' login [provider] Subscription OAuth or official Plan API key',
|
|
86
|
+
' logout [provider] remove a dsh credential (or all if omitted)',
|
|
87
|
+
' status [provider] report non-secret credential state',
|
|
88
|
+
'',
|
|
89
|
+
'Providers:',
|
|
90
|
+
ids,
|
|
91
|
+
'',
|
|
92
|
+
].join('\n'))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function resolveIds(raw: string | undefined): string[] {
|
|
96
|
+
if (raw === undefined) return PI_LOGIN_PROVIDERS.map(provider => provider.id)
|
|
97
|
+
return [requirePiLoginProvider(raw).id]
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function run(argv: readonly string[]): Promise<number> {
|
|
101
|
+
if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') {
|
|
102
|
+
printHelp()
|
|
103
|
+
return 0
|
|
104
|
+
}
|
|
105
|
+
const [rawAction, rawProvider, ...rest] = argv
|
|
106
|
+
if (rawAction !== 'login' && rawAction !== 'logout' && rawAction !== 'status') {
|
|
107
|
+
process.stderr.write(`dsh-oauth-login: expected login, logout, or status; got ${JSON.stringify(rawAction)}\n`)
|
|
108
|
+
return 1
|
|
109
|
+
}
|
|
110
|
+
if (rest.length > 0) {
|
|
111
|
+
process.stderr.write(`dsh-oauth-login: unexpected extra arguments: ${rest.join(' ')}\n`)
|
|
112
|
+
return 1
|
|
113
|
+
}
|
|
114
|
+
const action: Action = rawAction
|
|
115
|
+
try {
|
|
116
|
+
const session = new PiLoginSession()
|
|
117
|
+
switch (action) {
|
|
118
|
+
case 'status': {
|
|
119
|
+
const ids = resolveIds(rawProvider)
|
|
120
|
+
let failed = false
|
|
121
|
+
process.stdout.write(`store: ${piLoginAuthPath()}\n`)
|
|
122
|
+
await session.refreshStoredGrants()
|
|
123
|
+
for (const id of ids) {
|
|
124
|
+
const [status] = await piLoginStatus(session.store, id)
|
|
125
|
+
const spec = requirePiLoginProvider(id)
|
|
126
|
+
if (status === undefined || !status.authenticated) {
|
|
127
|
+
process.stdout.write(`${spec.displayName}: signed out\n`)
|
|
128
|
+
failed = true
|
|
129
|
+
continue
|
|
130
|
+
}
|
|
131
|
+
const expires = status.expiresAt
|
|
132
|
+
const suffix = expires === undefined || Number.isNaN(expires.valueOf())
|
|
133
|
+
? ''
|
|
134
|
+
: `; access expires ${expires.toISOString()}`
|
|
135
|
+
const models = session.visibleModels(id).map(model => model.id).slice(0, 8).join(', ')
|
|
136
|
+
process.stdout.write(`${spec.displayName}: signed in${suffix}\n`)
|
|
137
|
+
process.stdout.write(` route: ${spec.route}\n`)
|
|
138
|
+
process.stdout.write(` models: ${models}\n`)
|
|
139
|
+
}
|
|
140
|
+
return failed && rawProvider !== undefined ? 1 : 0
|
|
141
|
+
}
|
|
142
|
+
case 'logout': {
|
|
143
|
+
const ids = resolveIds(rawProvider)
|
|
144
|
+
for (const id of ids) {
|
|
145
|
+
await session.logout(id)
|
|
146
|
+
process.stdout.write(`${requirePiLoginProvider(id).displayName}: signed out\n`)
|
|
147
|
+
}
|
|
148
|
+
process.stdout.write('Official CLI auth files were not touched.\n')
|
|
149
|
+
return 0
|
|
150
|
+
}
|
|
151
|
+
case 'login': {
|
|
152
|
+
if (rawProvider === undefined) {
|
|
153
|
+
process.stderr.write('dsh-oauth-login: login requires a provider id (see --help)\n')
|
|
154
|
+
return 1
|
|
155
|
+
}
|
|
156
|
+
const id = requirePiLoginProvider(rawProvider).id
|
|
157
|
+
const spec = requirePiLoginProvider(id)
|
|
158
|
+
if (spec.authType === 'api_key' && spec.loginUrl !== undefined) {
|
|
159
|
+
process.stdout.write(`Create or copy the Plan API key here:\n${spec.loginUrl}\n`)
|
|
160
|
+
openBrowser(spec.loginUrl, id)
|
|
161
|
+
}
|
|
162
|
+
const readline = createInterface({ input: process.stdin, output: process.stdout })
|
|
163
|
+
try {
|
|
164
|
+
await loginPiProviderSession(id, {
|
|
165
|
+
prompt: prompt => answerPrompt(prompt, (text, options) => readline.question(text, options)),
|
|
166
|
+
notify: event => notify(event, id, true),
|
|
167
|
+
}, session)
|
|
168
|
+
} finally {
|
|
169
|
+
readline.close()
|
|
170
|
+
}
|
|
171
|
+
process.stdout.write(`${requirePiLoginProvider(id).displayName}: signed in\n`)
|
|
172
|
+
process.stdout.write(`store: ${piLoginAuthPath()}\n`)
|
|
173
|
+
process.stdout.write('Official CLI auth files were not read or written.\n')
|
|
174
|
+
return 0
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
} catch (error: unknown) {
|
|
178
|
+
process.stderr.write(`dsh-oauth-login: ${action} failed: ${safeMessage(error)}\n`)
|
|
179
|
+
return 1
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (process.argv[1] !== undefined && fileURLToPath(import.meta.url) === realpathSync(process.argv[1])) {
|
|
184
|
+
process.exitCode = await run(process.argv.slice(2))
|
|
185
|
+
}
|
package/src/catalog.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/** OAuth subscriptions Pi Agent exposes via /login. Radius is omitted (needs a gateway). */
|
|
2
|
+
|
|
3
|
+
export interface PiLoginProvider {
|
|
4
|
+
/** pi-ai provider id used by models.login(). */
|
|
5
|
+
readonly id: string
|
|
6
|
+
/** Harness LLM route. Distinct from catalog routes and from other plugins. */
|
|
7
|
+
readonly route: string
|
|
8
|
+
/** Credential method this plugin deliberately exposes for this provider. */
|
|
9
|
+
readonly authType: 'oauth' | 'api_key'
|
|
10
|
+
/** Official account page opened before an interactive API-key prompt. */
|
|
11
|
+
readonly loginUrl?: string
|
|
12
|
+
readonly displayName: string
|
|
13
|
+
readonly shortName: string
|
|
14
|
+
readonly blurb: string
|
|
15
|
+
readonly blurbZh: string
|
|
16
|
+
/** HTTPS hosts this provider is allowed to open during login. */
|
|
17
|
+
readonly allowedHosts: readonly string[]
|
|
18
|
+
/** Extra allowed host suffixes (leading-dot match). */
|
|
19
|
+
readonly allowedSuffixes: readonly string[]
|
|
20
|
+
readonly preferredModels: readonly string[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const PI_LOGIN_PROVIDERS: readonly PiLoginProvider[] = [
|
|
24
|
+
{
|
|
25
|
+
id: 'openai-codex',
|
|
26
|
+
route: 'pi-openai-codex',
|
|
27
|
+
authType: 'oauth',
|
|
28
|
+
displayName: 'ChatGPT Codex',
|
|
29
|
+
shortName: 'Codex',
|
|
30
|
+
blurb: 'ChatGPT Plus/Pro Codex. Independent of official `codex login`.',
|
|
31
|
+
blurbZh: 'ChatGPT Plus/Pro 的 Codex。和官方 `codex login` 互不影响。',
|
|
32
|
+
allowedHosts: ['auth.openai.com', 'chatgpt.com', 'www.chatgpt.com'],
|
|
33
|
+
allowedSuffixes: ['.openai.com', '.chatgpt.com'],
|
|
34
|
+
preferredModels: ['gpt-5.4', 'gpt-5.3-codex', 'gpt-5.3-codex-spark'],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'anthropic',
|
|
38
|
+
route: 'pi-anthropic',
|
|
39
|
+
authType: 'oauth',
|
|
40
|
+
displayName: 'Claude Pro/Max',
|
|
41
|
+
shortName: 'Claude',
|
|
42
|
+
blurb: 'Claude subscription. Independent of official Claude Code login.',
|
|
43
|
+
blurbZh: 'Claude 订阅。和官方 Claude Code 登录互不影响。',
|
|
44
|
+
allowedHosts: ['claude.ai', 'www.claude.ai', 'platform.claude.com'],
|
|
45
|
+
allowedSuffixes: ['.anthropic.com', '.claude.ai'],
|
|
46
|
+
preferredModels: ['claude-opus-4-6', 'claude-sonnet-4-6', 'claude-opus-4-5'],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: 'xai',
|
|
50
|
+
route: 'pi-xai',
|
|
51
|
+
authType: 'oauth',
|
|
52
|
+
displayName: 'xAI Grok',
|
|
53
|
+
shortName: 'Grok',
|
|
54
|
+
blurb: 'SuperGrok / X Premium. Independent of official `grok` CLI.',
|
|
55
|
+
blurbZh: 'SuperGrok / X Premium。和官方 `grok` CLI 互不影响。',
|
|
56
|
+
allowedHosts: ['auth.x.ai', 'accounts.x.ai', 'x.ai', 'www.x.ai'],
|
|
57
|
+
allowedSuffixes: ['.x.ai'],
|
|
58
|
+
preferredModels: ['grok-4.6', 'grok-4.5', 'grok-4.3'],
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: 'github-copilot',
|
|
62
|
+
route: 'pi-github-copilot',
|
|
63
|
+
authType: 'oauth',
|
|
64
|
+
displayName: 'GitHub Copilot',
|
|
65
|
+
shortName: 'Copilot',
|
|
66
|
+
blurb: 'GitHub Copilot subscription via device code.',
|
|
67
|
+
blurbZh: 'GitHub Copilot 订阅,走 device code。',
|
|
68
|
+
allowedHosts: ['github.com', 'www.github.com', 'api.github.com'],
|
|
69
|
+
allowedSuffixes: ['.github.com', '.githubcopilot.com'],
|
|
70
|
+
preferredModels: ['gpt-5.4', 'claude-sonnet-4.6', 'claude-opus-4.6'],
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: 'openrouter',
|
|
74
|
+
route: 'pi-openrouter',
|
|
75
|
+
authType: 'oauth',
|
|
76
|
+
displayName: 'OpenRouter',
|
|
77
|
+
shortName: 'OpenRouter',
|
|
78
|
+
blurb: 'OpenRouter OAuth mints a key billed from your OpenRouter credits.',
|
|
79
|
+
blurbZh: 'OpenRouter OAuth 会签发一把钥匙,从你的 OpenRouter 余额扣费。',
|
|
80
|
+
allowedHosts: ['openrouter.ai', 'www.openrouter.ai'],
|
|
81
|
+
allowedSuffixes: ['.openrouter.ai'],
|
|
82
|
+
preferredModels: [],
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: 'kimi-coding',
|
|
86
|
+
route: 'pi-kimi-coding',
|
|
87
|
+
authType: 'oauth',
|
|
88
|
+
displayName: 'Kimi For Coding',
|
|
89
|
+
shortName: 'Kimi',
|
|
90
|
+
blurb: 'Kimi Code subscription.',
|
|
91
|
+
blurbZh: 'Kimi Code 订阅。',
|
|
92
|
+
allowedHosts: ['auth.kimi.com', 'kimi.com', 'www.kimi.com', 'api.kimi.com'],
|
|
93
|
+
allowedSuffixes: ['.kimi.com'],
|
|
94
|
+
preferredModels: ['kimi-for-coding', 'k3'],
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: 'zai-coding-cn',
|
|
98
|
+
route: 'pi-zai-coding-cn',
|
|
99
|
+
authType: 'api_key',
|
|
100
|
+
loginUrl: 'https://bigmodel.cn/coding-plan/personal/overview',
|
|
101
|
+
displayName: '智谱 GLM Coding Plan',
|
|
102
|
+
shortName: 'GLM',
|
|
103
|
+
blurb: 'Zhipu GLM Coding Plan (China) via its official Plan API key.',
|
|
104
|
+
blurbZh: '智谱 GLM Coding Plan 中国区套餐,使用套餐页签发的专用 API Key。',
|
|
105
|
+
allowedHosts: ['bigmodel.cn', 'www.bigmodel.cn', 'open.bigmodel.cn'],
|
|
106
|
+
allowedSuffixes: ['.bigmodel.cn'],
|
|
107
|
+
preferredModels: ['glm-5.3-flash', 'glm-5.3', 'glm-5.2', 'glm-5-turbo', 'glm-5.1', 'glm-4.7'],
|
|
108
|
+
},
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
export function piLoginProvider(id: string): PiLoginProvider | undefined {
|
|
112
|
+
return PI_LOGIN_PROVIDERS.find(provider => provider.id === id)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function piLoginProviderByRoute(route: string): PiLoginProvider | undefined {
|
|
116
|
+
return PI_LOGIN_PROVIDERS.find(provider => provider.route === route)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function requirePiLoginProvider(id: string): PiLoginProvider {
|
|
120
|
+
const provider = piLoginProvider(id)
|
|
121
|
+
if (provider === undefined) {
|
|
122
|
+
throw new Error(`dsh-oauth-login: unknown provider "${id}"`)
|
|
123
|
+
}
|
|
124
|
+
return provider
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function piLoginRoutes(): string[] {
|
|
128
|
+
return PI_LOGIN_PROVIDERS.map(provider => provider.route)
|
|
129
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { useEffect, useState, useSyncExternalStore } from 'react'
|
|
2
|
+
import type { OpenRouterCatalogClient } from './openrouter-store.ts'
|
|
3
|
+
import type { TranslateNS } from '@deepseek-ai/dsh-client-ui-slots'
|
|
4
|
+
|
|
5
|
+
const CSS = [
|
|
6
|
+
'.dsh-or-sync{display:flex;flex-direction:column;gap:4px;padding:6px 8px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);min-width:0}',
|
|
7
|
+
'.dsh-or-sync-row{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:4px 8px}',
|
|
8
|
+
'.dsh-or-sync-copy{min-width:0;overflow-wrap:anywhere}',
|
|
9
|
+
'.dsh-or-sync time{font-variant-numeric:tabular-nums}',
|
|
10
|
+
'.dsh-or-refresh{min-height:28px;padding:3px 8px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer}',
|
|
11
|
+
'.dsh-or-refresh:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}',
|
|
12
|
+
'.dsh-or-refresh:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}',
|
|
13
|
+
'.dsh-or-refresh:disabled{opacity:.55;cursor:default}',
|
|
14
|
+
'.dsh-or-sync-error{color:color-mix(in srgb,var(--dsw-alias-state-warn-label) 50%,var(--dsw-alias-label-primary))}',
|
|
15
|
+
].join('\n')
|
|
16
|
+
|
|
17
|
+
export interface OpenRouterSyncStatusProps {
|
|
18
|
+
catalog: OpenRouterCatalogClient
|
|
19
|
+
ts: TranslateNS<'model-search'>
|
|
20
|
+
details?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function OpenRouterSyncStatus({ catalog, ts, details = false }: OpenRouterSyncStatusProps) {
|
|
24
|
+
const state = useSyncExternalStore(catalog.subscribe, catalog.getSnapshot)
|
|
25
|
+
const [now, setNow] = useState(Date.now)
|
|
26
|
+
const data = state.data
|
|
27
|
+
const retryAt = data?.retryAt ?? 0
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
setNow(Date.now())
|
|
30
|
+
if (retryAt <= Date.now()) return
|
|
31
|
+
const timer = setTimeout(() => { setNow(Date.now()) }, retryAt - Date.now() + 20)
|
|
32
|
+
return () => { clearTimeout(timer) }
|
|
33
|
+
}, [retryAt])
|
|
34
|
+
const cooling = retryAt > now
|
|
35
|
+
const working = state.loading || data?.refreshing === true
|
|
36
|
+
const unavailable = state.error !== null || data?.error !== null && data?.error !== undefined
|
|
37
|
+
const timestamp = data?.lastUpdatedAt
|
|
38
|
+
return (
|
|
39
|
+
<div className="dsh-or-sync" data-ud-check={details ? 'openrouter-settings-sync' : 'openrouter-menu-sync'} data-ud-min-gap="4">
|
|
40
|
+
<style>{CSS}</style>
|
|
41
|
+
<div className="dsh-or-sync-row">
|
|
42
|
+
<span className="dsh-or-sync-copy" role="status" aria-live="polite">
|
|
43
|
+
{working ? ts('syncing') : data?.connected ? ts('syncSchedule') : ts('syncSignIn')}
|
|
44
|
+
</span>
|
|
45
|
+
<button type="button" className="dsh-or-refresh"
|
|
46
|
+
disabled={working || cooling || data?.connected !== true}
|
|
47
|
+
onClick={() => { void catalog.load(true) }}
|
|
48
|
+
>{working ? ts('syncing') : cooling ? ts('syncCooldown') : ts('syncRefresh')}</button>
|
|
49
|
+
</div>
|
|
50
|
+
{timestamp != null && (
|
|
51
|
+
<span className="dsh-or-sync-copy">{ts('syncUpdated')}{' '}
|
|
52
|
+
<time dateTime={new Date(timestamp).toISOString()}>{new Date(timestamp).toLocaleString()}</time>
|
|
53
|
+
{data?.stale ? ' · ' + ts('syncStale') : ''}
|
|
54
|
+
</span>
|
|
55
|
+
)}
|
|
56
|
+
{timestamp == null && data?.connected && <span>{ts('syncBuiltin')}</span>}
|
|
57
|
+
{unavailable && <span role="status" className="dsh-or-sync-copy dsh-or-sync-error">
|
|
58
|
+
{ts(state.error === 'restart' ? 'syncRestart' : data?.error === 'save' ? 'syncSaveError' : 'syncError')}
|
|
59
|
+
</span>}
|
|
60
|
+
{details && <>
|
|
61
|
+
<span>{ts('freeHelp')}</span>
|
|
62
|
+
<span>{ts('forcedPluginWarning')}</span>
|
|
63
|
+
</>}
|
|
64
|
+
</div>
|
|
65
|
+
)
|
|
66
|
+
}
|