feat(electron): adapt renderer API and WebSocket layer for dynamic backend URL
This commit is contained in:
@@ -1,8 +1,19 @@
|
||||
import axios from 'axios'
|
||||
|
||||
import { getApiBaseUrl } from './api-base'
|
||||
|
||||
const apiClient = axios.create({
|
||||
baseURL: import.meta.env.DEV ? 'http://localhost:8000' : '',
|
||||
baseURL: '', // 统一为空,通过拦截器动态设置
|
||||
timeout: 10000,
|
||||
})
|
||||
|
||||
// Electron 端:动态注入后端 URL;浏览器端 getApiBaseUrl() 返回空字符串,行为不变
|
||||
apiClient.interceptors.request.use(async (config) => {
|
||||
const baseUrl = await getApiBaseUrl()
|
||||
if (baseUrl && !config.baseURL) {
|
||||
config.baseURL = baseUrl
|
||||
}
|
||||
return config
|
||||
})
|
||||
|
||||
export default apiClient
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
import { getApiBaseUrl } from './api-base'
|
||||
import { isElectron } from './runtime'
|
||||
|
||||
// 带自动认证处理的 fetch 封装
|
||||
|
||||
/**
|
||||
* 将相对路径在 Electron 端转换为绝对路径
|
||||
* 浏览器端直接返回原始 input,行为不变
|
||||
*/
|
||||
async function resolveUrl(input: RequestInfo | URL): Promise<RequestInfo | URL> {
|
||||
if (isElectron() && typeof input === 'string' && input.startsWith('/')) {
|
||||
const base = await getApiBaseUrl()
|
||||
return base ? `${base}${input}` : input
|
||||
}
|
||||
return input
|
||||
}
|
||||
|
||||
/**
|
||||
* 增强的 fetch 函数,自动处理 401 错误并跳转到登录页
|
||||
* 使用 HttpOnly Cookie 进行认证,自动携带 credentials
|
||||
@@ -25,7 +40,7 @@ export async function fetchWithAuth(
|
||||
headers,
|
||||
}
|
||||
|
||||
const response = await fetch(input, config)
|
||||
const response = await fetch(await resolveUrl(input), config)
|
||||
|
||||
// 检测 401 未授权错误
|
||||
if (response.status === 401) {
|
||||
@@ -54,7 +69,7 @@ export function getAuthHeaders(): HeadersInit {
|
||||
*/
|
||||
export async function logout(): Promise<void> {
|
||||
try {
|
||||
await fetch('/api/webui/auth/logout', {
|
||||
await fetch(await resolveUrl('/api/webui/auth/logout'), {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
})
|
||||
@@ -70,7 +85,7 @@ export async function logout(): Promise<void> {
|
||||
*/
|
||||
export async function checkAuthStatus(): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch('/api/webui/auth/check', {
|
||||
const response = await fetch(await resolveUrl('/api/webui/auth/check'), {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
})
|
||||
|
||||
@@ -2,7 +2,16 @@
|
||||
* 知识库 API
|
||||
*/
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api/webui'
|
||||
import { getApiBaseUrl } from './api-base'
|
||||
import { isElectron } from './runtime'
|
||||
|
||||
async function getKnowledgeApiBase(): Promise<string> {
|
||||
if (isElectron()) {
|
||||
const base = await getApiBaseUrl()
|
||||
return base ? `${base}/api/webui` : '/api/webui'
|
||||
}
|
||||
return import.meta.env.VITE_API_BASE_URL || '/api/webui'
|
||||
}
|
||||
|
||||
export interface KnowledgeNode {
|
||||
id: string
|
||||
@@ -35,7 +44,7 @@ export interface KnowledgeStats {
|
||||
* 获取知识图谱数据
|
||||
*/
|
||||
export async function getKnowledgeGraph(limit: number = 100, nodeType: 'all' | 'entity' | 'paragraph' = 'all'): Promise<KnowledgeGraph> {
|
||||
const url = `${API_BASE_URL}/knowledge/graph?limit=${limit}&node_type=${nodeType}`
|
||||
const url = `${await getKnowledgeApiBase()}/knowledge/graph?limit=${limit}&node_type=${nodeType}`
|
||||
|
||||
const response = await fetch(url)
|
||||
|
||||
@@ -50,7 +59,7 @@ export async function getKnowledgeGraph(limit: number = 100, nodeType: 'all' | '
|
||||
* 获取知识图谱统计信息
|
||||
*/
|
||||
export async function getKnowledgeStats(): Promise<KnowledgeStats> {
|
||||
const response = await fetch(`${API_BASE_URL}/knowledge/stats`)
|
||||
const response = await fetch(`${await getKnowledgeApiBase()}/knowledge/stats`)
|
||||
if (!response.ok) {
|
||||
throw new Error('获取知识图谱统计信息失败')
|
||||
}
|
||||
@@ -61,7 +70,7 @@ export async function getKnowledgeStats(): Promise<KnowledgeStats> {
|
||||
* 搜索知识节点
|
||||
*/
|
||||
export async function searchKnowledgeNode(query: string): Promise<KnowledgeNode[]> {
|
||||
const response = await fetch(`${API_BASE_URL}/knowledge/search?query=${encodeURIComponent(query)}`)
|
||||
const response = await fetch(`${await getKnowledgeApiBase()}/knowledge/search?query=${encodeURIComponent(query)}`)
|
||||
if (!response.ok) {
|
||||
throw new Error('搜索知识节点失败')
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import { checkAuthStatus } from './fetch-with-auth'
|
||||
import { getSetting } from './settings-manager'
|
||||
import { createReconnectingWebSocket } from './ws-utils'
|
||||
|
||||
import { getWsBaseUrl } from '@/lib/api-base'
|
||||
|
||||
export interface LogEntry {
|
||||
id: string
|
||||
timestamp: string
|
||||
@@ -54,18 +56,9 @@ class LogWebSocketManager {
|
||||
/**
|
||||
* 获取 WebSocket URL(不含 token 参数)
|
||||
*/
|
||||
private getWebSocketUrl(): string {
|
||||
let baseUrl: string
|
||||
if (import.meta.env.DEV) {
|
||||
// 开发模式:连接到 WebUI 后端服务器
|
||||
baseUrl = 'ws://127.0.0.1:8001/ws/logs'
|
||||
} else {
|
||||
// 生产模式:使用当前页面的 host
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
const host = window.location.host
|
||||
baseUrl = `${protocol}//${host}/ws/logs`
|
||||
}
|
||||
return baseUrl
|
||||
private async getWebSocketUrl(): Promise<string> {
|
||||
const wsBase = await getWsBaseUrl()
|
||||
return `${wsBase}/ws/logs`
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,7 +78,7 @@ class LogWebSocketManager {
|
||||
return
|
||||
}
|
||||
|
||||
const wsUrl = this.getWebSocketUrl()
|
||||
const wsUrl = await this.getWebSocketUrl()
|
||||
|
||||
// 使用 ws-utils 创建 WebSocket
|
||||
this.wsControl = createReconnectingWebSocket(wsUrl, {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { ApiResponse } from '@/types/api'
|
||||
import type { PluginInfo } from '@/types/plugin'
|
||||
|
||||
import { getWsBaseUrl } from '@/lib/api-base'
|
||||
import { fetchWithAuth } from '@/lib/fetch-with-auth'
|
||||
import { parseResponse } from '@/lib/api-helpers'
|
||||
|
||||
import type { GitStatus, MaimaiVersion } from './types'
|
||||
|
||||
/**
|
||||
@@ -213,9 +213,8 @@ export async function connectPluginProgressWebSocket(
|
||||
onProgress: (progress: import('./types').PluginLoadProgress) => void,
|
||||
onError?: (error: Event) => void
|
||||
): Promise<WebSocket | null> {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
const host = window.location.host
|
||||
const wsUrl = `${protocol}//${host}/api/webui/ws/plugin-progress`
|
||||
const wsBase = await getWsBaseUrl()
|
||||
const wsUrl = `${wsBase}/api/webui/ws/plugin-progress`
|
||||
|
||||
// 使用 ws-utils 创建 WebSocket
|
||||
const { createReconnectingWebSocket } = await import('@/lib/ws-utils')
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { useToast } from '@/hooks/use-toast'
|
||||
import { getWsBaseUrl } from '@/lib/api-base'
|
||||
import { fetchWithAuth } from '@/lib/fetch-with-auth'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Bot, Edit2, Loader2, RefreshCw, User, Send, Wifi, WifiOff, UserCircle2 } from 'lucide-react'
|
||||
@@ -299,7 +300,7 @@ export function ChatPage() {
|
||||
return
|
||||
}
|
||||
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
const wsBase = await getWsBaseUrl()
|
||||
const params = new URLSearchParams()
|
||||
|
||||
// 添加 token 到参数
|
||||
@@ -320,7 +321,7 @@ export function ChatPage() {
|
||||
params.append('user_name', userName)
|
||||
}
|
||||
|
||||
const wsUrl = `${protocol}//${window.location.host}/api/chat/ws?${params.toString()}`
|
||||
const wsUrl = `${wsBase}/api/chat/ws?${params.toString()}`
|
||||
console.log(`[Tab ${tabId}] 正在连接 WebSocket:`, wsUrl)
|
||||
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user