fix: 第一轮修复 - 记忆管理/IoT操控/历史消息持久化/动作消息/链路优化/安全配置

- 修复记忆管理数据库连接不可用 (ai-core重编译+Unicode修复)
- 修复IoT子会话工具调用链路日志缺失
- 新增最终审查子会话(review_provider) 支持消息格式解析拆分
- 实现历史消息持久化(后端存储+前端分页加载)
- 前端新增动作消息(ActionMessage)类型和渲染
- 优化对话链路速度(非阻塞子会话+快速问候通道)
- JWT密钥环境变量化(无默认值启动panic)
- Token自动刷新机制(401拦截器+refresh接口)
- WebSocket指数退避重连(jitter+最大10次)
- localStorage清理一致性(cyrene_前缀+版本检查)
- IoT环境变量统一为IOT_SERVICE_URL
This commit is contained in:
2026-05-21 23:10:07 +08:00
parent 8b7d4ec19a
commit a058b0ab8e
53 changed files with 5535 additions and 241 deletions
+2
View File
@@ -5,6 +5,8 @@ export {
refreshToken,
setToken,
getToken,
getRefreshToken,
setTokens,
clearToken,
isAuthenticated,
} from './client';
+200 -55
View File
@@ -1,9 +1,154 @@
// HTTP 客户端封装
// HTTP 客户端封装 — 带 Token 自动刷新拦截器
import type { AuthResponse } from '@/types/session';
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8080/api/v1';
/** localStorage key 前缀 */
const LS_TOKEN_KEY = 'token';
const LS_REFRESH_TOKEN_KEY = 'refresh_token';
const LS_USER_ID_KEY = 'user_id';
// ========== Token 刷新队列(避免多个并发请求同时刷新 token ==========
let isRefreshing = false;
let refreshPromise: Promise<string | null> | null = null;
/**
* 等待中的请求队列。当 token 刷新完成后,用新 token 重放这些请求。
*/
type QueuedRequest = {
resolve: (token: string | null) => void;
reject: (err: unknown) => void;
};
let refreshQueue: QueuedRequest[] = [];
function onRefreshDone(newToken: string | null) {
for (const q of refreshQueue) {
q.resolve(newToken);
}
refreshQueue = [];
}
function onRefreshFailed(err: unknown) {
for (const q of refreshQueue) {
q.reject(err);
}
refreshQueue = [];
}
// ========== 存储辅助函数 ==========
/** 存储认证令牌 */
export function setToken(token: string) {
try {
localStorage.setItem(LS_TOKEN_KEY, token);
} catch { /* ignore */ }
}
/** 获取认证令牌 */
export function getToken(): string | null {
try {
return localStorage.getItem(LS_TOKEN_KEY);
} catch {
return null;
}
}
/** 获取刷新令牌 */
export function getRefreshToken(): string | null {
try {
return localStorage.getItem(LS_REFRESH_TOKEN_KEY);
} catch {
return null;
}
}
/** 存储 tokens */
export function setTokens(accessToken: string, refreshTokenValue?: string) {
setToken(accessToken);
if (refreshTokenValue) {
try {
localStorage.setItem(LS_REFRESH_TOKEN_KEY, refreshTokenValue);
} catch { /* ignore */ }
}
}
/** 清除认证令牌 */
export function clearToken() {
try {
localStorage.removeItem(LS_TOKEN_KEY);
localStorage.removeItem(LS_REFRESH_TOKEN_KEY);
localStorage.removeItem(LS_USER_ID_KEY);
} catch { /* ignore */ }
}
/** 检查是否已认证 */
export function isAuthenticated(): boolean {
return !!getToken();
}
// ========== Token 刷新逻辑 ==========
/**
* 尝试刷新 token。如果已经有正在进行的刷新请求,等待它完成。
*/
async function tryRefreshToken(): Promise<string | null> {
const refreshTokenValue = getRefreshToken();
if (!refreshTokenValue) {
// 没有 refresh token,无法刷新
return null;
}
// 如果已经有刷新正在进行中,加入队列等待
if (isRefreshing && refreshPromise) {
return new Promise((resolve, reject) => {
refreshQueue.push({ resolve, reject });
});
}
isRefreshing = true;
refreshPromise = (async () => {
try {
const response = await fetch(`${API_BASE_URL}/auth/refresh`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${refreshTokenValue}`,
},
body: JSON.stringify({ refresh_token: refreshTokenValue }),
});
if (!response.ok) {
throw new Error(`Refresh failed: ${response.status}`);
}
const data: AuthResponse = await response.json();
if (data.token) {
setTokens(data.token, data.refresh_token);
return data.token;
}
return null;
} catch {
// 刷新失败
clearToken();
return null;
} finally {
isRefreshing = false;
refreshPromise = null;
}
})();
try {
const newToken = await refreshPromise;
onRefreshDone(newToken);
return newToken;
} catch (err) {
onRefreshFailed(err);
return null;
}
}
/** 请求选项 */
interface RequestOptions {
method?: string;
@@ -20,70 +165,70 @@ interface ApiResponse<T = unknown> {
}
/**
* 发送API请求
* 发送 API 请求,内置 401 自动刷新拦截
*/
async function request<T = unknown>(endpoint: string, options: RequestOptions = {}): Promise<ApiResponse<T>> {
const { method = 'GET', body, auth = true } = options;
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...options.headers,
};
const makeRequest = async (tokenOverride?: string): Promise<ApiResponse<T>> => {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...options.headers,
};
if (auth) {
const token = localStorage.getItem('token');
const token = tokenOverride || (auth ? getToken() : null);
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
}
try {
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
});
try {
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
});
const data = await response.json().catch(() => null);
// 401 且当前请求需要认证、且不是 refresh 接口本身、且尚未重试
if (response.status === 401 && auth && endpoint !== '/auth/refresh' && !tokenOverride) {
const newToken = await tryRefreshToken();
if (newToken) {
// 用新 token 重试
return makeRequest(newToken);
}
// 刷新失败,清除状态,跳转登录页
clearToken();
// 触发页面重新加载以显示登录页
if (typeof window !== 'undefined') {
window.location.href = '/login';
}
return {
error: '认证已过期,请重新登录',
status: 401,
};
}
if (!response.ok) {
const data = await response.json().catch(() => null);
if (!response.ok) {
return {
error: data?.error || `请求失败 (${response.status})`,
status: response.status,
};
}
return { data: data as T, status: response.status };
} catch (err) {
return {
error: data?.error || `请求失败 (${response.status})`,
status: response.status,
error: err instanceof Error ? err.message : '网络错误',
status: 0,
};
}
};
return { data: data as T, status: response.status };
} catch (err) {
return {
error: err instanceof Error ? err.message : '网络错误',
status: 0,
};
}
return makeRequest();
}
/** 存储认证令牌 */
export function setToken(token: string) {
localStorage.setItem('token', token);
}
/** 获取认证令牌 */
export function getToken(): string | null {
return localStorage.getItem('token');
}
/** 清除认证令牌 */
export function clearToken() {
localStorage.removeItem('token');
localStorage.removeItem('user_id');
}
/** 检查是否已认证 */
export function isAuthenticated(): boolean {
return !!getToken();
}
// ========== 认证API ==========
// ========== 认证 API ==========
export async function login(username: string, password: string): Promise<ApiResponse<AuthResponse>> {
const resp = await request<AuthResponse>('/auth/login', {
@@ -92,8 +237,8 @@ export async function login(username: string, password: string): Promise<ApiResp
auth: false,
});
if (resp.data?.token) {
setToken(resp.data.token);
localStorage.setItem('user_id', resp.data.user_id);
setTokens(resp.data.token, resp.data.refresh_token);
localStorage.setItem(LS_USER_ID_KEY, resp.data.user_id);
}
return resp;
}
@@ -105,8 +250,8 @@ export async function register(username: string, password: string, email: string
auth: false,
});
if (resp.data?.token) {
setToken(resp.data.token);
localStorage.setItem('user_id', resp.data.user_id);
setTokens(resp.data.token, resp.data.refresh_token);
localStorage.setItem(LS_USER_ID_KEY, resp.data.user_id);
}
return resp;
}
@@ -114,12 +259,12 @@ export async function register(username: string, password: string, email: string
export async function refreshToken(): Promise<ApiResponse<AuthResponse>> {
const resp = await request<AuthResponse>('/auth/refresh', { method: 'POST' });
if (resp.data?.token) {
setToken(resp.data.token);
setTokens(resp.data.token, resp.data.refresh_token);
}
return resp;
}
// ========== 会话API ==========
// ========== 会话 API ==========
export async function createSession(title?: string) {
return request('/sessions', { method: 'POST', body: { title } });
@@ -141,7 +286,7 @@ export async function fetchSessionMessages(id: string) {
return request(`/sessions/${id}/messages`);
}
// ========== 记忆API ==========
// ========== 记忆 API ==========
export async function searchMemory(query: string) {
return request(`/memory/search?q=${encodeURIComponent(query)}`);