diff --git a/tauri/src/main.ts b/tauri/src/main.ts index 588c56c..eaeba85 100644 --- a/tauri/src/main.ts +++ b/tauri/src/main.ts @@ -77,12 +77,32 @@ function normalizeAPIAddress(value: string): string { } function loginFailureMessage(reason: unknown): string { + if (reason instanceof Error && reason.message === "LINEUP_LOGIN_TIMEOUT") return "连接 AppServer 超时(10 秒)。请确认 Tailscale 已连接,并核对上方显示的服务地址。"; if (reason instanceof DOMException && reason.name === "AbortError") return "连接 AppServer 超时(10 秒)。请确认 Tailscale 已连接,且服务地址和端口可访问。"; const message = reason instanceof Error ? reason.message : ""; if (message === "Load failed" || message === "Failed to fetch") return "无法连接 AppServer。请确认 Tailscale 已连接,并检查地址是否为 http://100.121.118.116:8090。"; return message || "登录失败,请稍后重试。"; } +async function fetchLogin(endpoint: string, phone: string, code: string): Promise { + const controller = new AbortController(); + let timeoutID = 0; + const timeout = new Promise((_, reject) => { + timeoutID = window.setTimeout(() => { + controller.abort(); + reject(new Error("LINEUP_LOGIN_TIMEOUT")); + }, 10_000); + }); + try { + return await Promise.race([ + fetch(`${endpoint}/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ phone, code }), signal: controller.signal }), + timeout, + ]); + } finally { + window.clearTimeout(timeoutID); + } +} + function appendBubble(role: "human" | "agent", content: string, markdown = false): HTMLElement { const row = document.createElement("article"); row.className = `message-row ${role}`; if (role === "agent") { const avatar = document.createElement("span"); avatar.className = "message-avatar"; avatar.textContent = "✦"; row.append(avatar); } @@ -162,19 +182,17 @@ $("#login-form").addEventListener("submit", async event => { session.api = endpoint; localStorage.setItem("lineup.api", session.api); - error.textContent = "正在连接 AppServer…"; + error.textContent = `正在连接 ${session.api}…`; submit.disabled = true; submit.textContent = "正在进入…"; - const controller = new AbortController(); - const timeout = window.setTimeout(() => controller.abort(), 10_000); try { - const response = await fetch(api("/login"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ phone, code }), signal: controller.signal }); + const response = await fetchLogin(session.api, phone, code); const body = await response.json().catch(() => ({})) as LoginResponse & { error?: string }; if (!response.ok) throw new Error(body.error || "登录失败"); session = { ...session, uid: body.uid, agentUID: body.agent_uid || session.agentUID, channelID: body.channel_id || session.channelID, channelType: body.channel_type || session.channelType, lastSeq: 0, active: true }; loginView.hidden = true; chatView.hidden = false; addSystem("已进入与 AI Agent 的对话"); setPresence("正在同步消息…"); void syncLoop(); } catch (reason) { error.textContent = loginFailureMessage(reason); } - finally { window.clearTimeout(timeout); submit.disabled = false; submit.textContent = "进入 LineUp"; } + finally { submit.disabled = false; submit.textContent = "进入 LineUp"; } }); $("#message-form").addEventListener("submit", async event => {