Spaces:
Running
Running
| /* Gradio API 桥: POST /gradio_api/call/<name> -> event_id -> SSE complete */ | |
| export const LS = { | |
| get: (k: string, d: string | null = null) => localStorage.getItem("celestial_" + k) ?? d, | |
| set: (k: string, v: string) => localStorage.setItem("celestial_" + k, v), | |
| del: (k: string) => localStorage.removeItem("celestial_" + k), | |
| } | |
| /* 设备标识: 档案与创建它的设备/浏览器绑定 */ | |
| function deviceId(): string { | |
| let d = LS.get("device") | |
| if (!d) { | |
| d = crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(36).slice(2)}` | |
| LS.set("device", d) | |
| } | |
| return d | |
| } | |
| export const DEVICE = deviceId() | |
| export async function api<T = unknown>(name: string, args: unknown[] = []): Promise<T> { | |
| const post = await fetch(`/gradio_api/call/${name}`, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ data: args }), | |
| }) | |
| if (!post.ok) throw new Error(`后端未应答 (${post.status})`) | |
| const { event_id } = await post.json() | |
| const res = await fetch(`/gradio_api/call/${name}/${event_id}`) | |
| const text = await res.text() | |
| let event = "" | |
| let data: unknown[] | null = null | |
| for (const raw of text.split("\n")) { | |
| if (raw.startsWith("event:")) event = raw.slice(6).trim() | |
| else if (raw.startsWith("data:")) { | |
| const payload = raw.slice(5).trim() | |
| if (event === "complete" && payload) data = JSON.parse(payload) | |
| if (event === "error") throw new Error(payload && payload !== "null" ? payload : "天问服务出错") | |
| } | |
| } | |
| if (data === null) throw new Error("天问服务未应答,请确认 app.py 已运行") | |
| return data[0] as T | |
| } | |
| /* Windows 后端返回的 FileData url 含反斜杠 */ | |
| export const fileUrl = (fd: { url?: string }) => String(fd.url || "").replace(/\\/g, "/") | |
| /* 流式解读 (FastAPI 原生 SSE 路由 /stream/*): 每个事件 data:{"text": 累积全文}, | |
| onText 每次收到当前完整文本; 命中安全护栏时后端直接推送替换后的安全文案。 */ | |
| export async function apiStream(path: string, body: object, onText: (text: string) => void): Promise<void> { | |
| // 90s 断流超时: 防止 LLM 卡死时无限等待 | |
| const ctrl = new AbortController() | |
| const timer = setTimeout(() => ctrl.abort(), 90000) | |
| try { | |
| const res = await fetch(path, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(body), | |
| signal: ctrl.signal, | |
| }) | |
| if (!res.ok || !res.body) throw new Error(`流式服务未应答 (${res.status})`) | |
| const reader = res.body.getReader() | |
| const decoder = new TextDecoder() | |
| let buf = "" | |
| for (;;) { | |
| const { done, value } = await reader.read() | |
| if (done) break | |
| buf += decoder.decode(value, { stream: true }) | |
| const events = buf.split("\n\n") | |
| buf = events.pop() ?? "" | |
| for (const evt of events) { | |
| const dataLine = evt.split("\n").find((l) => l.startsWith("data:")) | |
| if (!dataLine) continue | |
| try { | |
| const o = JSON.parse(dataLine.slice(5).trim()) | |
| if (typeof o.text === "string") onText(o.text) | |
| } catch { /* 忽略不完整片段 */ } | |
| } | |
| } | |
| } finally { | |
| clearTimeout(timer) | |
| } | |
| } | |