From a564d48154949049e4c0127ef5760c045c6a3d13 Mon Sep 17 00:00:00 2001 From: yanweidong Date: Wed, 22 Jul 2026 23:13:55 +0800 Subject: [PATCH] fix(auth): highlight failed server connections --- apps/mini/scripts/visual-check.mjs | 7 +++++++ apps/mini/src/App.tsx | 28 +++++++++++++++++++++++++++- apps/mini/src/api.ts | 12 ++++++++++++ apps/mini/src/styles.css | 6 ++++++ apps/web_v1/src/App.css | 20 ++++++++++++++++++++ 5 files changed, 72 insertions(+), 1 deletion(-) diff --git a/apps/mini/scripts/visual-check.mjs b/apps/mini/scripts/visual-check.mjs index a22e7ed..5c1eac0 100644 --- a/apps/mini/scripts/visual-check.mjs +++ b/apps/mini/scripts/visual-check.mjs @@ -36,6 +36,7 @@ const workspace = { await page.route('http://localhost:9150/api/v1/**', async (route) => { const request = route.request() const path = new URL(request.url()).pathname + if (path === '/api/v1/status') return route.fulfill({ json: { status: 'unavailable' } }) if (path === '/api/v1/auth/login') return route.fulfill({ json: { token: 'mini-token' } }) if (path === '/api/v1/projects') { if (request.method() === 'POST') return route.fulfill({ status: 201, json: workspace.project }) @@ -57,6 +58,11 @@ await page.route('http://localhost:9150/api/v1/**', async (route) => { }) await page.goto(`http://127.0.0.1:${port}/`, { waitUntil: 'networkidle' }) +await page.waitForSelector('.mini-connection.offline') +const offlineConnection = await page.locator('.mini-connection').evaluate((element) => ({ + text: element.textContent, + borderColor: getComputedStyle(element).borderColor, +})) await page.screenshot({ path: 'test-results/mini-login.png', fullPage: true }) const loginDefaults = await page.locator('.login-note input').evaluateAll((inputs) => inputs.map((input) => input.value)) await page.locator('.login-note input').nth(2).press('Enter') @@ -96,6 +102,7 @@ await browser.close() await server.close() const failures = [] +if (!offlineConnection.text?.includes('连接异常') || offlineConnection.borderColor !== 'rgb(245, 63, 63)') failures.push(`offline connection must be red: ${JSON.stringify(offlineConnection)}`) if (loginDefaults[1] !== 'demo@senlin.ai' || loginDefaults[2] !== 'password123') failures.push(`login defaults failed: ${JSON.stringify(loginDefaults)}`) if (errors.length) failures.push(`console errors: ${errors.join('; ')}`) if (home.width !== 420) failures.push(`expected 420px shell, got ${home.width}`) diff --git a/apps/mini/src/App.tsx b/apps/mini/src/App.tsx index 70db6cd..f4ad3aa 100644 --- a/apps/mini/src/App.tsx +++ b/apps/mini/src/App.tsx @@ -5,7 +5,7 @@ import { IconFile, IconFolder, IconPlus, IconRefresh, IconRobot, IconSearch, IconSettings, } from '@arco-design/web-react/icon' import { - ApiError, createAISession, createCron, createProject, createTask, fetchAISessions, fetchExperts, + ApiError, checkServerConnection, createAISession, createCron, createProject, createTask, fetchAISessions, fetchExperts, fetchProjects, fetchWorkspace, login, updateTask, uploadSource, type AISession, type ApiSession, type Expert, type Project, type Workspace, } from './api' @@ -14,6 +14,7 @@ const { Text, Title } = Typography type View = 'home' | 'tasks' | 'notes' | 'ai' | 'more' type DockSide = 'left' | 'right' type Action = 'project' | 'task' | 'cron' | null +type ConnectionStatus = 'checking' | 'online' | 'offline' const navItems: Array<{ id: View; label: string; icon: React.ReactNode }> = [ { id: 'home', label: '项目', icon: }, @@ -311,6 +312,26 @@ function Login({ onLogin }: { onLogin: (session: ApiSession) => void }) { const [password, setPassword] = useState('password123') const [loading, setLoading] = useState(false) const [error, setError] = useState('') + const [connection, setConnection] = useState('checking') + + useEffect(() => { + const controller = new AbortController() + const timer = window.setTimeout(() => void checkConnection(controller.signal), 300) + return () => { + controller.abort() + window.clearTimeout(timer) + } + }, [server]) + + async function checkConnection(signal?: AbortSignal) { + setConnection('checking') + try { + const online = await checkServerConnection(server, signal) + if (!signal?.aborted) setConnection(online ? 'online' : 'offline') + } catch (requestError) { + if (!(requestError instanceof DOMException && requestError.name === 'AbortError')) setConnection('offline') + } + } async function submitLogin() { if (loading) return @@ -332,6 +353,11 @@ function Login({ onLogin }: { onLogin: (session: ApiSession) => void }) { 森林AI Mini 停靠在屏幕一侧,随时记录与推进项目 +
+ + {connection === 'checking' ? '正在检查' : connection === 'online' ? '连接正常' : '连接异常'} + +
{error ? : null} diff --git a/apps/mini/src/api.ts b/apps/mini/src/api.ts index 5476512..c851320 100644 --- a/apps/mini/src/api.ts +++ b/apps/mini/src/api.ts @@ -94,6 +94,18 @@ export async function login(server: string, email: string, password: string): Pr return { baseUrl, token: response.token } } +export async function checkServerConnection(server: string, signal?: AbortSignal) { + try { + const response = await fetch(`${normalizeBaseUrl(server)}/api/v1/status`, { signal }) + if (!response.ok) return false + const payload = await response.json() as { timestamp?: unknown } + return typeof payload.timestamp === 'string' + } catch (error) { + if (error instanceof DOMException && error.name === 'AbortError') throw error + return false + } +} + export async function fetchProjects(session: ApiSession) { useSessionBase(session) return request('/api/v1/projects', { token: session.token }) diff --git a/apps/mini/src/styles.css b/apps/mini/src/styles.css index 9a7c7d1..31bcda8 100644 --- a/apps/mini/src/styles.css +++ b/apps/mini/src/styles.css @@ -224,6 +224,12 @@ html[data-dock="left"] .mini-shell { margin-right: auto; } .login-note label, .mini-form label { display: grid; gap: 5px; color: #4e5969; font-size: 11px; } .login-note .arco-input-wrapper, .mini-form .arco-input-wrapper { border-radius: 9px; background: rgba(255,255,255,.82); } .login-note > small { color: var(--muted); text-align: center; } +.mini-connection { display: grid; grid-template-columns: 9px 1fr auto; align-items: center; gap: 8px; margin-top: -5px; border: 1px solid #bedaff; border-radius: 9px; background: #f2f8ff; padding: 8px 10px; color: #165dff; font-size: 11px; } +.mini-connection > i { width: 8px; height: 8px; border-radius: 50%; background: currentColor; } +.mini-connection > strong { font-weight: 600; } +.mini-connection > button { border: 0; background: transparent; padding: 0; color: inherit; cursor: pointer; font-size: 11px; } +.mini-connection.online { border-color: #7be188; background: #f0fff4; color: #168a2f; } +.mini-connection.offline { border-color: #f53f3f; background: #fff2f0; color: #d91d35; } .mini-modal .arco-modal { width: min(380px, calc(100vw - 24px)); } .mini-modal .arco-modal-content { border-radius: 15px; } diff --git a/apps/web_v1/src/App.css b/apps/web_v1/src/App.css index 8aa5158..91edaa3 100644 --- a/apps/web_v1/src/App.css +++ b/apps/web_v1/src/App.css @@ -191,6 +191,26 @@ font-weight: 600; } +.connection-card.checking { + border-color: #bedaff; + background: #f2f8ff; + color: #165dff; +} + +.connection-card.checking .connection-title { + color: #165dff; +} + +.connection-card.offline { + border-color: #f53f3f; + background: #fff2f0; + color: #d91d35; +} + +.connection-card.offline .connection-title { + color: #d91d35; +} + .login-value-panel { display: grid; align-content: center;