"use strict"; (() => { const app = document.getElementById("chat-app"); const appID = app.dataset.appId; const credentialForm = document.getElementById("credential-form"); const credentialInput = document.getElementById("token"); const credentialState = document.getElementById("credential-state"); const clearTokenButton = document.getElementById("clear-token"); const newChatButton = document.getElementById("new-chat"); const composer = document.getElementById("composer"); const promptInput = document.getElementById("prompt"); const sendButton = document.getElementById("send"); const requestState = document.getElementById("request-state"); const messages = document.getElementById("messages"); const errorMessages = { CHAT_APP_NOT_FOUND: "对话应用未找到,请联系管理员检查 App ID。", CHAT_AUTH_INVALID: "访问凭证无效,请重新输入。", CHAT_ORIGIN_FORBIDDEN: "当前页面来源未获授权,请联系管理员检查允许来源。", CHAT_REQUEST_INVALID: "请求内容无效,请修改后重试。", CHAT_REQUEST_BODY_TOO_LARGE: "问题内容过长,请缩短后重试。", CHAT_CONVERSATION_NOT_FOUND: "会话已失效,请重新开始。", CHAT_CONVERSATION_BUSY: "当前会话仍在处理上一轮请求,请稍后重试。", CHAT_CAPACITY_REACHED: "当前会话容量已满,请稍后重试。", CHAT_UPSTREAM_TIMEOUT: "助手响应超时,请重新开始对话后重试。", CHAT_RUN_FAILED: "助手未能完成本轮请求,请重新开始对话后重试。", CHAT_UPSTREAM_PROTOCOL_ERROR: "助手返回了不完整的响应,请重新开始对话后重试。", CHAT_UPSTREAM_UNAVAILABLE: "助手服务暂时不可用,请稍后重试。", CHAT_INTERNAL_ERROR: "对话服务暂时不可用,请稍后重试。", RATE_LIMITED: "请求过于频繁,请稍后重试。" }; let authToken = ""; let committedSessionID = ""; let activeController = null; credentialForm.addEventListener("submit", (event) => { event.preventDefault(); const candidate = credentialInput.value.trim(); if (!candidate) { credentialState.textContent = "请输入有效凭证"; credentialInput.focus(); return; } if (candidate !== authToken) { if (activeController) activeController.abort(); committedSessionID = ""; } authToken = candidate; credentialInput.value = ""; credentialState.textContent = "凭证已应用,仅保存在页面内存中"; promptInput.focus(); }); clearTokenButton.addEventListener("click", () => { if (activeController) activeController.abort(); authToken = ""; committedSessionID = ""; credentialInput.value = ""; credentialState.textContent = "凭证已清除"; credentialInput.focus(); }); newChatButton.addEventListener("click", () => { if (activeController) activeController.abort(); committedSessionID = ""; messages.replaceChildren(); appendMessage("assistant", "已开始新对话。之前的会话标识已从页面内存中清除。"); requestState.textContent = "新对话已就绪"; promptInput.focus(); }); promptInput.addEventListener("keydown", (event) => { if (event.key === "Enter" && !event.shiftKey && !event.isComposing) { event.preventDefault(); composer.requestSubmit(); } }); composer.addEventListener("submit", async (event) => { event.preventDefault(); const prompt = promptInput.value.trim(); if (!prompt || activeController) return; if (!authToken) { credentialState.textContent = "发送前请先应用访问凭证"; credentialInput.focus(); return; } appendMessage("user", prompt); promptInput.value = ""; setBusy(true, "正在等待助手完成查询…"); const controller = new AbortController(); activeController = controller; try { const input = {prompt}; if (committedSessionID) input.session_id = committedSessionID; const response = await fetch(`/api/v1/apps/${encodeURIComponent(appID)}/completion`, { method: "POST", headers: { "Accept": "text/event-stream", "Content-Type": "application/json", "xtoken": authToken }, body: JSON.stringify({input, parameters: {}}), credentials: "omit", cache: "no-store", signal: controller.signal }); if (!response.ok) { throw new Error(await readHTTPError(response)); } if (!response.body) throw new Error("浏览器无法读取流式响应"); const completed = await consumeSSE(response.body); committedSessionID = completed.sessionID; appendMessage("assistant", completed.text || "助手已完成,但没有返回正文。"); requestState.textContent = "回答完成,可以继续提问"; } catch (error) { committedSessionID = ""; if (error && error.name === "AbortError") { requestState.textContent = "请求已取消"; } else { appendMessage("error", error && error.message ? error.message : "请求失败,请稍后重试。"); requestState.textContent = "本轮未成功"; } } finally { if (activeController === controller) activeController = null; setBusy(false); promptInput.focus(); } }); async function consumeSSE(stream) { const reader = stream.getReader(); const decoder = new TextDecoder(); let buffer = ""; let latest = {sessionID: "", text: null, sawInitial: false, stopped: false}; while (true) { const {value, done} = await reader.read(); buffer += decoder.decode(value || new Uint8Array(), {stream: !done}); buffer = buffer.replaceAll("\r\n", "\n"); let boundary; while ((boundary = buffer.indexOf("\n\n")) >= 0) { const block = buffer.slice(0, boundary); buffer = buffer.slice(boundary + 2); latest = processSSEBlock(block, latest); } if (done) break; } if (buffer.trim()) latest = processSSEBlock(buffer, latest); if (!latest.sawInitial || !latest.stopped || !latest.sessionID || typeof latest.text !== "string") { throw new Error("响应在完成前中断;为避免会话状态不一致,请开始新对话后重试。"); } return {sessionID: latest.sessionID, text: latest.text}; } function processSSEBlock(block, latest) { let eventName = "message"; const dataLines = []; for (const line of block.split("\n")) { if (line.startsWith("event:")) eventName = line.slice(6).trim(); if (line.startsWith("data:")) dataLines.push(line.slice(5).trimStart()); } if (!dataLines.length) return latest; let payload; try { payload = JSON.parse(dataLines.join("\n")); } catch { throw new Error("服务返回了无法解析的数据"); } if (eventName === "error") throw new Error(publicErrorMessage(payload, "对话处理失败,请重新开始后重试。")); if (eventName !== "result") return latest; if (!payload.output || typeof payload.output !== "object" || Array.isArray(payload.output)) { throw new Error("服务返回了无效的对话结果"); } const sessionID = payload.output.session_id; const finishReason = payload.output.finish_reason; if (typeof sessionID !== "string" || !sessionID) throw new Error("服务返回了无效的会话标识"); if (typeof finishReason !== "string") throw new Error("服务返回了无效的完成状态"); if (Object.prototype.hasOwnProperty.call(payload.output, "text") && typeof payload.output.text !== "string") { throw new Error("服务返回了无效的回答内容"); } if (latest.sessionID && latest.sessionID !== sessionID) throw new Error("服务在同一响应中返回了不一致的会话标识"); latest.sessionID = sessionID; if (finishReason === "null") { if (latest.stopped) throw new Error("服务在完成后继续返回了中间结果"); latest.sawInitial = true; return latest; } if (finishReason !== "stop") throw new Error("服务返回了未知的完成状态"); if (!latest.sawInitial || latest.stopped || typeof payload.output.text !== "string") { throw new Error("服务返回了不完整的最终结果"); } latest.stopped = true; latest.text = payload.output.text; return latest; } async function readHTTPError(response) { try { const payload = await response.json(); return publicErrorMessage(payload, statusErrorMessage(response.status)); } catch { return statusErrorMessage(response.status); } } function publicErrorMessage(payload, fallback) { const nested = payload && payload.error && typeof payload.error === "object" ? payload.error : null; const code = payload && typeof payload.code === "string" ? payload.code : nested && typeof nested.code === "string" ? nested.code : ""; return errorMessages[code] || fallback; } function statusErrorMessage(status) { if (status === 401) return errorMessages.CHAT_AUTH_INVALID; if (status === 403) return errorMessages.CHAT_ORIGIN_FORBIDDEN; if (status === 404) return errorMessages.CHAT_CONVERSATION_NOT_FOUND; if (status === 409) return errorMessages.CHAT_CONVERSATION_BUSY; if (status === 429) return errorMessages.RATE_LIMITED; if (status === 502 || status === 503) return errorMessages.CHAT_UPSTREAM_UNAVAILABLE; if (status === 504) return errorMessages.CHAT_UPSTREAM_TIMEOUT; return `请求失败(HTTP ${status})`; } function appendMessage(kind, text) { const article = document.createElement("article"); const paragraph = document.createElement("p"); article.className = `message ${kind}-message`; paragraph.textContent = text; article.appendChild(paragraph); messages.appendChild(article); messages.scrollTop = messages.scrollHeight; } function setBusy(busy, message) { sendButton.disabled = busy; promptInput.disabled = busy; newChatButton.disabled = false; if (message) requestState.textContent = message; } })();