72 lines
2.6 KiB
TypeScript
72 lines
2.6 KiB
TypeScript
/* =====================================================================
|
||
* bridge.ts — Tauri IPC 桥接(AI 代理专用)
|
||
* 桌面版(Tauri WebView)直连 AI 网关受 CORS 限制,走 Rust 侧 reqwest 代理;
|
||
* Web 版返回 null,调用方回退浏览器 fetch(智谱等支持 CORS 的网关直连可用)
|
||
* ===================================================================== */
|
||
|
||
/** 是否运行在 Tauri 桌面环境 */
|
||
export function isTauri(): boolean {
|
||
return typeof window !== 'undefined' && ('__TAURI__' in window || '__TAURI_INTERNALS__' in window)
|
||
}
|
||
|
||
let _invoke: ((cmd: string, args?: Record<string, unknown>) => Promise<unknown>) | null = null
|
||
let _invokeTried = false
|
||
|
||
/** 懒加载 Tauri invoke(动态 import,Web 版不会打进包) */
|
||
async function getInvoke() {
|
||
if (_invokeTried) return _invoke
|
||
_invokeTried = true
|
||
if (!isTauri()) return null
|
||
try {
|
||
const mod = await import('@tauri-apps/api/core')
|
||
_invoke = mod.invoke
|
||
} catch { _invoke = null }
|
||
return _invoke
|
||
}
|
||
|
||
export interface ProxyResp {
|
||
status: number
|
||
body: string
|
||
error?: string
|
||
}
|
||
|
||
/** 非流式代理:Rust 侧 fetch,返回 {status, body};非桌面环境返回 null */
|
||
export async function aiProxy(url: string, apiKey: string, body: string): Promise<ProxyResp | null> {
|
||
const invoke = await getInvoke()
|
||
if (!invoke) return null
|
||
try {
|
||
const r = await invoke('ai_proxy', { url, apiKey, body }) as ProxyResp
|
||
return r
|
||
} catch (e: any) {
|
||
return { status: 0, body: '', error: String(e?.message || e) }
|
||
}
|
||
}
|
||
|
||
/** 流式代理:Rust 侧 SSE,原始 chunk(含 "data: {...}" 帧)通过 onChunk 推送;
|
||
* SSE 解析由 ai.ts 的 createSSESink 统一处理(与浏览器路径共用)。非桌面返回 null */
|
||
export async function aiProxyStream(
|
||
url: string, apiKey: string, body: string,
|
||
onChunk: (raw: string) => void
|
||
): Promise<ProxyResp | null> {
|
||
const invoke = await getInvoke()
|
||
if (!invoke) return null
|
||
try {
|
||
const eventMod = await import('@tauri-apps/api/event')
|
||
const unlisten = await eventMod.listen<string>('ai-delta', (ev) => {
|
||
onChunk(ev.payload || '')
|
||
})
|
||
try {
|
||
const full = await invoke('ai_proxy_stream', { url, apiKey, body }) as unknown as string
|
||
return { status: 200, body: full }
|
||
} finally {
|
||
unlisten()
|
||
}
|
||
} catch (e: any) {
|
||
// Rust 侧错误协议:__HTTP_<status>__<body>
|
||
const raw = String(e?.message || e)
|
||
const m = raw.match(/^__HTTP_(\d+)__/)
|
||
if (m) return { status: Number(m[1]), body: raw.slice(m[0].length) }
|
||
return { status: 0, body: '', error: raw }
|
||
}
|
||
}
|