Tianwen / frontend /src /lib /api.ts
liuyd-dev's picture
v0: zero is infinite
3a7a5e1
Raw
History Blame Contribute Delete
3.23 kB
/* 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)
}
}