增加调试前端页面

This commit is contained in:
andy committed 2026-09-06 00:25:25 +08:00
1 parent 3080b17d02
commit ee9d2d7cf1
22 files changed
+1442 -59

No files matched your search

+251
View File
@@ -0,0 +1,251 @@
"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;
}
})();