Skip to content

Commit 6378e87

Browse files
Add custom AI OAuth callback
1 parent b84e0f9 commit 6378e87

2 files changed

Lines changed: 229 additions & 0 deletions

File tree

ai-oauth/callback/index.html

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
<!doctype html>
2+
<html lang="zh-CN">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
6+
<meta name="referrer" content="no-referrer">
7+
<meta name="robots" content="noindex, nofollow">
8+
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
9+
<meta name="theme-color" content="#171717" media="(prefers-color-scheme: dark)">
10+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'; object-src 'none'">
11+
<title>正在返回 PythonIDE</title>
12+
<style>
13+
:root {
14+
color-scheme: light dark;
15+
--bg: #fff;
16+
--surface: #fff;
17+
--ink: #0d0d0d;
18+
--muted: #6f6f6f;
19+
--faint: #929292;
20+
--line: rgba(13, 13, 13, .12);
21+
--line-strong: rgba(13, 13, 13, .22);
22+
--inverse: #0d0d0d;
23+
--inverse-text: #fff;
24+
--danger: #b42318;
25+
--danger-soft: rgba(180, 35, 24, .08);
26+
--ease: cubic-bezier(.22, 1, .36, 1);
27+
--font: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", Arial, "PingFang SC", sans-serif;
28+
font-family: var(--font);
29+
}
30+
@media (prefers-color-scheme: dark) {
31+
:root {
32+
--bg: #171717;
33+
--surface: #202020;
34+
--ink: #f5f5f5;
35+
--muted: #a6a6a6;
36+
--faint: #858585;
37+
--line: rgba(255, 255, 255, .12);
38+
--line-strong: rgba(255, 255, 255, .22);
39+
--inverse: #f5f5f5;
40+
--inverse-text: #111;
41+
--danger: #ff8b82;
42+
--danger-soft: rgba(255, 139, 130, .1);
43+
}
44+
}
45+
* { box-sizing: border-box; }
46+
html { min-width: 320px; min-height: 100%; background: var(--bg); -webkit-text-size-adjust: 100%; }
47+
body {
48+
min-height: 100vh;
49+
min-height: 100svh;
50+
margin: 0;
51+
display: grid;
52+
grid-template-rows: auto 1fr auto;
53+
background: var(--bg);
54+
color: var(--ink);
55+
font-family: var(--font);
56+
-webkit-font-smoothing: antialiased;
57+
}
58+
body::before {
59+
content: "";
60+
position: fixed;
61+
inset: 0;
62+
z-index: -1;
63+
pointer-events: none;
64+
background: radial-gradient(70% 55% at 50% -8%, rgba(127,127,127,.13), transparent 72%), var(--bg);
65+
}
66+
[hidden] { display: none !important; }
67+
.site-header, .site-footer { width: min(1120px, calc(100% - 40px)); margin: 0 auto; }
68+
.site-header {
69+
min-height: calc(72px + env(safe-area-inset-top));
70+
padding-top: env(safe-area-inset-top);
71+
display: flex;
72+
align-items: center;
73+
border-bottom: 1px solid var(--line);
74+
}
75+
.brand { display: inline-flex; align-items: center; gap: 10px; font-size: 17px; font-weight: 650; letter-spacing: -.02em; }
76+
.brand-mark { width: 28px; height: 28px; display: block; }
77+
main {
78+
width: min(100%, 640px);
79+
margin: 0 auto;
80+
padding: clamp(52px, 9vh, 108px) 24px 72px;
81+
display: grid;
82+
place-items: center;
83+
align-content: center;
84+
}
85+
.auth-state { width: min(100%, 520px); display: grid; justify-items: center; text-align: center; animation: enter 520ms var(--ease) both; }
86+
.indicator {
87+
width: 58px;
88+
height: 58px;
89+
margin-bottom: 30px;
90+
border: 1px solid var(--line);
91+
border-radius: 50%;
92+
display: grid;
93+
place-items: center;
94+
background: var(--surface);
95+
box-shadow: 0 14px 40px rgba(0,0,0,.06);
96+
}
97+
.spinner { width: 28px; height: 28px; border: 2px solid var(--line-strong); border-top-color: var(--ink); border-radius: 50%; animation: spin 760ms linear infinite; }
98+
.state-symbol { display: none; font-size: 25px; line-height: 1; }
99+
body[data-state="fallback"] .spinner, body[data-state="error"] .spinner { display: none; }
100+
body[data-state="fallback"] .fallback-symbol, body[data-state="error"] .error-symbol { display: block; }
101+
body[data-state="error"] .indicator { border-color: var(--danger); background: var(--danger-soft); box-shadow: none; color: var(--danger); }
102+
.eyebrow { margin: 0 0 16px; color: var(--muted); font-size: 12px; font-weight: 680; letter-spacing: .1em; text-transform: uppercase; }
103+
h1 { margin: 0; font-size: clamp(38px, 9vw, 54px); font-weight: 620; letter-spacing: -.052em; line-height: 1.06; text-wrap: balance; }
104+
.message { max-width: 500px; margin: 22px 0 0; color: var(--muted); font-size: clamp(16px, 3.8vw, 18px); line-height: 1.65; text-wrap: pretty; }
105+
.actions { width: min(100%, 320px); margin-top: 34px; display: grid; gap: 12px; }
106+
.button {
107+
min-height: 50px;
108+
padding: 0 22px;
109+
border: 1px solid var(--inverse);
110+
border-radius: 999px;
111+
display: inline-flex;
112+
align-items: center;
113+
justify-content: center;
114+
gap: 14px;
115+
background: var(--inverse);
116+
color: var(--inverse-text);
117+
font-size: 15px;
118+
font-weight: 650;
119+
text-decoration: none;
120+
transition: transform 220ms var(--ease), opacity 180ms ease;
121+
}
122+
.button:hover { transform: translateY(-2px); opacity: .9; }
123+
.helper { margin: 0; color: var(--faint); font-size: 12px; line-height: 1.55; }
124+
.trust-note { margin: 34px 0 0; display: inline-flex; align-items: center; gap: 8px; color: var(--faint); font-size: 12px; }
125+
.trust-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--ink); opacity: .54; }
126+
.site-footer {
127+
min-height: calc(64px + env(safe-area-inset-bottom));
128+
padding-bottom: env(safe-area-inset-bottom);
129+
border-top: 1px solid var(--line);
130+
display: flex;
131+
align-items: center;
132+
justify-content: space-between;
133+
color: var(--faint);
134+
font-size: 11px;
135+
}
136+
.footer-context { font-family: ui-monospace, "SFMono-Regular", Menlo, monospace; }
137+
@keyframes spin { to { transform: rotate(360deg); } }
138+
@keyframes enter { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
139+
@media (max-width: 520px) {
140+
.site-header, .site-footer { width: calc(100% - 32px); }
141+
main { padding: 44px 20px 56px; }
142+
.footer-context { display: none; }
143+
}
144+
@media (prefers-reduced-motion: reduce) {
145+
*, *::before, *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; }
146+
}
147+
</style>
148+
</head>
149+
<body data-state="returning">
150+
<header class="site-header">
151+
<div class="brand" aria-label="PythonIDE">
152+
<svg class="brand-mark" viewBox="60 100 904 824" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
153+
<circle cx="826" cy="512" r="116" fill="#E8AA24"/><circle cx="669" cy="240" r="116" fill="#1F5F95"/>
154+
<circle cx="355" cy="240" r="116" fill="#1F5F95"/><circle cx="198" cy="512" r="116" fill="#1F5F95"/>
155+
<circle cx="355" cy="784" r="116" fill="#1F5F95"/><circle cx="669" cy="784" r="116" fill="#1F5F95"/>
156+
</svg>
157+
<span>PythonIDE</span>
158+
</div>
159+
</header>
160+
<main>
161+
<section class="auth-state" aria-labelledby="title" aria-describedby="status">
162+
<div class="indicator" aria-hidden="true"><span class="spinner"></span><span class="state-symbol fallback-symbol"></span><span class="state-symbol error-symbol">!</span></div>
163+
<p class="eyebrow" id="eyebrow">安全连接</p>
164+
<h1 id="title">正在返回 PythonIDE</h1>
165+
<p class="message" id="status" role="status" aria-live="polite">正在将登录结果安全地交还给 App,这通常只需一瞬间。</p>
166+
<div class="actions" id="actions" hidden>
167+
<a class="button" id="continue" href="pythonide://oauth/custom-ai"><span id="button-label">返回 PythonIDE</span><span aria-hidden="true"></span></a>
168+
<p class="helper" id="helper">若 App 没有自动打开,请点击上方按钮继续。</p>
169+
</div>
170+
<p class="trust-note"><span class="trust-dot" aria-hidden="true"></span><span id="trust-note">无需在此页面输入账号或密码</span></p>
171+
</section>
172+
</main>
173+
<footer class="site-footer"><span>© 2026 PythonIDE</span><span class="footer-context">AI OAuth callback</span></footer>
174+
<script>
175+
(() => {
176+
const body = document.body;
177+
const actions = document.getElementById('actions');
178+
const button = document.getElementById('continue');
179+
const eyebrow = document.getElementById('eyebrow');
180+
const title = document.getElementById('title');
181+
const status = document.getElementById('status');
182+
const buttonLabel = document.getElementById('button-label');
183+
const helper = document.getElementById('helper');
184+
const trustNote = document.getElementById('trust-note');
185+
const preferred = ((navigator.languages && navigator.languages[0]) || navigator.language || 'en').toLowerCase();
186+
const zh = preferred.startsWith('zh');
187+
const copy = zh ? {
188+
returningEyebrow: '安全连接', returningTitle: '正在返回 PythonIDE', returningStatus: '正在将登录结果安全地交还给 App,这通常只需一瞬间。',
189+
fallbackEyebrow: '需要继续', fallbackTitle: '继续返回 PythonIDE', fallbackStatus: '自动返回似乎没有完成。点击下方按钮即可继续,无需重新登录。',
190+
button: '返回 PythonIDE', helper: '若 App 没有打开,请确认已经安装最新版 PythonIDE。', trust: '无需在此页面输入账号或密码',
191+
errorEyebrow: '连接未完成', errorTitle: '无法完成返回', errorStatus: '登录响应缺少必要信息。请返回 PythonIDE 后重新连接账户。'
192+
} : {
193+
returningEyebrow: 'Secure connection', returningTitle: 'Returning to PythonIDE', returningStatus: 'Securely handing the sign-in result back to the app. This should only take a moment.',
194+
fallbackEyebrow: 'Continue', fallbackTitle: 'Continue to PythonIDE', fallbackStatus: 'The automatic return may not have completed. Use the button below to continue without signing in again.',
195+
button: 'Return to PythonIDE', helper: 'If the app does not open, make sure the latest PythonIDE is installed.', trust: 'You never need to enter an account or password on this page',
196+
errorEyebrow: 'Connection incomplete', errorTitle: 'Unable to return', errorStatus: 'The sign-in response is missing required information. Return to PythonIDE and connect again.'
197+
};
198+
document.documentElement.lang = zh ? 'zh-CN' : 'en';
199+
eyebrow.textContent = copy.returningEyebrow; title.textContent = copy.returningTitle; status.textContent = copy.returningStatus;
200+
buttonLabel.textContent = copy.button; helper.textContent = copy.helper; trustNote.textContent = copy.trust;
201+
const fallback = () => { body.dataset.state = 'fallback'; eyebrow.textContent = copy.fallbackEyebrow; title.textContent = copy.fallbackTitle; status.textContent = copy.fallbackStatus; actions.hidden = false; };
202+
if (!window.location.search) {
203+
body.dataset.state = 'error'; eyebrow.textContent = copy.errorEyebrow; title.textContent = copy.errorTitle; status.textContent = copy.errorStatus; actions.hidden = false; return;
204+
}
205+
const destination = `pythonide://oauth/custom-ai${window.location.search}`;
206+
button.href = destination;
207+
window.setTimeout(fallback, 1200);
208+
try { window.location.replace(destination); } catch (_) { fallback(); }
209+
})();
210+
</script>
211+
</body>
212+
</html>

ai-oauth/client.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"client_id": "https://link.pythonide.xin/ai-oauth/client.json",
3+
"client_name": "PythonIDE",
4+
"client_uri": "https://pythonide.xin",
5+
"logo_uri": "https://link.pythonide.xin/assets/app-icon.png",
6+
"redirect_uris": [
7+
"https://link.pythonide.xin/ai-oauth/callback"
8+
],
9+
"grant_types": [
10+
"authorization_code",
11+
"refresh_token"
12+
],
13+
"response_types": [
14+
"code"
15+
],
16+
"token_endpoint_auth_method": "none"
17+
}

0 commit comments

Comments
 (0)