新增: AI 助手 Markdown 渲染+打字机流式+桌面 Rust 代理修复 CORS

This commit is contained in:
lxy
2026-08-24 02:13:15 +08:00
parent 7f08f654d9
commit db3b2c24b7
8 changed files with 463 additions and 167 deletions
+57 -30
View File
@@ -1,44 +1,71 @@
/* =====================================================================
* bridge.ts — Tauri IPC 安全调用桥接
*
* 安全检测 __TAURI__ 环境,动态 import @tauri-apps/api/core。
* Web 模式下返回 mock,组件无需关心环境。
* bridge.ts — Tauri IPC 桥接AI 代理专用)
* 桌面版(Tauri WebView)直连 AI 网关受 CORS 限制,走 Rust 侧 reqwest 代理;
* Web 版返回 null,调用方回退浏览器 fetch(智谱等支持 CORS 的网关直连可用)
* ===================================================================== */
/** 动态获取 invoke 函数 */
let _invoke: ((cmd: string, args?: Record<string, unknown>) => Promise<unknown>) | null = null
/** 是否运行在 Tauri 桌面环境 */
export function isTauri(): boolean {
return typeof window !== 'undefined' && ('__TAURI__' in window || '__TAURI_INTERNALS__' in window)
}
async function ensureInvoke() {
if (_invoke) return true
if (typeof window === 'undefined' || !('__TAURI__' in window)) return false
let _invoke: ((cmd: string, args?: Record<string, unknown>) => Promise<unknown>) | null = null
let _invokeTried = false
/** 懒加载 Tauri invoke(动态 importWeb 版不会打进包) */
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
return true
} catch {
return false
}
} catch { _invoke = null }
return _invoke
}
/**
* 安全调用 Tauri IPC 命令。
* Web 模式下返回 undefined(调用方需处理)。
*/
export async function invoke<T = unknown>(cmd: string, args?: Record<string, unknown>): Promise<T | undefined> {
const ok = await ensureInvoke()
if (!ok || !_invoke) return undefined
return _invoke(cmd, args) as Promise<T>
export interface ProxyResp {
status: number
body: string
error?: string
}
/** 注册 Tauri 事件监听(用于 Rust → 前端推送) */
export async function listen<T>(event: string, handler: (payload: T) => void): Promise<() => void> {
if (typeof window === 'undefined' || !('__TAURI__' in window)) {
return () => {} // 空注销函数
}
/** 非流式代理: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 { listen } = await import('@tauri-apps/api/event')
return listen(event, (e: { payload: T }) => handler(e.payload))
} catch {
return () => {}
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 }
}
}