feat: 添加 Tauri Mac 原生应用支持

- 新增 mac-app/ 目录:Tauri 项目结构(Rust 外壳 + sidecar 进程管理)
- 前端添加 Tauri 环境检测,API 调用和 WebSocket 自动适配
- 后端支持 MEETING_DATA_DIR 环境变量指定数据目录
- 新增 /api/meetings/{id}/retranscribe 接口用于重新转录
- 修复轮询超时:2分钟→30分钟,适配长音频
- 添加 .gitignore 排除构建产物
This commit is contained in:
freedakgmail
2026-07-10 00:04:27 +08:00
parent 3ab8f22bae
commit 2752ea4e54
73 changed files with 5465 additions and 17 deletions
+2 -1
View File
@@ -1,6 +1,7 @@
import axios from 'axios'
const API_BASE = '/api'
const isTauri = typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window
const API_BASE = isTauri ? 'http://localhost:8501/api' : '/api'
const api = axios.create({
baseURL: API_BASE,
+4 -2
View File
@@ -87,7 +87,7 @@ export default function UploadPage() {
await pollMeetingStatus(result.meeting_id, (status) => {
if (status === 'processing') {
setTranscribeProgress(prev => Math.min(prev + 5, 90))
setTranscribeProgress(prev => Math.min(prev + 2, 95))
}
})
@@ -111,7 +111,9 @@ export default function UploadPage() {
const connectWebSocket = () => {
const sessionId = `realtime_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
const ws = new WebSocket(`ws://localhost:8501/ws/realtime/${sessionId}`)
const isTauri = typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window
const wsHost = isTauri ? 'localhost:8501' : window.location.host
const ws = new WebSocket(`ws://${wsHost}/ws/realtime/${sessionId}`)
ws.onopen = () => {
setWsConnected(true)
+9 -6
View File
@@ -1,5 +1,8 @@
import { create } from 'zustand'
const isTauri = typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window
const API_BASE = isTauri ? 'http://localhost:8501' : ''
export const useAppStore = create((set, get) => ({
// 会议列表
meetings: [],
@@ -38,7 +41,7 @@ export const useAppStore = create((set, get) => ({
refreshMeetings: async () => {
set({ loading: true, error: null })
try {
const res = await fetch('/api/meetings')
const res = await fetch(`${API_BASE}/api/meetings`)
const data = await res.json()
set({
meetings: data.meetings || [],
@@ -59,7 +62,7 @@ export const useAppStore = create((set, get) => ({
fetchMeetingDetail: async (meetingId) => {
set({ loading: true, error: null })
try {
const res = await fetch(`/api/meetings/${meetingId}`)
const res = await fetch(`${API_BASE}/api/meetings/${meetingId}`)
const data = await res.json()
set({ currentMeeting: data, loading: false })
return data
@@ -101,7 +104,7 @@ export const useAppStore = create((set, get) => ({
set({ loading: false, isTranscribing: false })
reject(new Error('Network error'))
}
xhr.open('POST', '/api/meetings/upload')
xhr.open('POST', `${API_BASE}/api/meetings/upload`)
xhr.send(formData)
})
} catch (err) {
@@ -112,7 +115,7 @@ export const useAppStore = create((set, get) => ({
// 轮询会议状态直到完成
pollMeetingStatus: async (meetingId, onStatusChange) => {
const maxAttempts = 60 // 最多轮询60次(约2分钟)
const maxAttempts = 600 // 最多轮询600次(约30分钟)
let attempts = 0
const poll = async () => {
@@ -122,7 +125,7 @@ export const useAppStore = create((set, get) => ({
attempts++
try {
const res = await fetch(`/api/meetings/${meetingId}`)
const res = await fetch(`${API_BASE}/api/meetings/${meetingId}`)
const data = await res.json()
onStatusChange?.(data.status)
@@ -132,7 +135,7 @@ export const useAppStore = create((set, get) => ({
}
// 继续轮询
await new Promise(r => setTimeout(r, 2000)) // 每2秒轮询一次
await new Promise(r => setTimeout(r, 3000)) // 每3秒轮询一次
return poll()
} catch (err) {
throw err