Compare commits

..

2 Commits

Author SHA1 Message Date
95c75ce331 build(deps): 同步包管理与后端依赖配置
新增 pnpm workspace 构建白名单配置,允许 esbuild 与 @parcel/watcher 安装。\n同步更新 pnpm 锁文件,并调整后端 requirements 中的依赖版本与补充缺失包。
2026-06-12 14:09:53 +08:00
4a434f9580 fix(auth): 修复启动脚本与前端认证流程
修复 start.sh 未记录后台进程 PID 导致启动后误判失败的问题。\n调整前端认证逻辑,支持开发环境下优先读取 URL token,并使用 POST 请求直连外部认证接口解析新的返回结构。\n补充认证回归测试,覆盖请求方法、请求头和用户信息映射。
2026-06-12 14:09:19 +08:00
6 changed files with 4776 additions and 3743 deletions

File diff suppressed because it is too large Load Diff

3
pnpm-workspace.yaml Normal file
View File

@ -0,0 +1,3 @@
allowBuilds:
'@parcel/watcher': true
esbuild: true

View File

@ -46,20 +46,10 @@ requests==2.32.5
jiter==0.13.0 jiter==0.13.0
distro==1.9.0 distro==1.9.0
pydantic_core==2.41.5 pydantic_core==2.41.5
annotated-types==0.7.0 PyJWT==2.8.0
typing_extensions==4.15.0 python-discovery==1.1.0
typing-inspect==0.9.0 python-dotenv==1.0.1
tenacity==9.1.4 python-multipart==0.0.18
pytokens==0.4.1
# ============================================================
# 异步/网络
# ============================================================
aiohttp==3.13.3
aiofiles==24.1.0
# ============================================================
# 其他工具
# ============================================================
PyJWT==2.11.0
PyYAML==6.0.3 PyYAML==6.0.3
pillow==12.1.1 pillow==12.1.1

View File

@ -0,0 +1,54 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createPinia, setActivePinia } from 'pinia'
describe('Auth Store', () => {
beforeEach(() => {
setActivePinia(createPinia())
localStorage.clear()
vi.clearAllMocks()
window.history.replaceState({}, '', '/')
})
it('prefers URL token and validates it even in dev mode', async () => {
window.history.replaceState({}, '', '/?token=url-token')
const fetchMock = vi.mocked(fetch)
fetchMock.mockResolvedValueOnce({
ok: true,
json: async () => ({
status: 1000,
message: '返回成功',
data: {
token: 'url-token',
userInfo: {
userId: 19,
userName: 'test20',
realName: '测试学生3',
email: '',
userIcon: 'defaultUserIcon.png',
},
},
}),
} as Response)
const { useAuthStore } = await import('@/stores/auth')
const store = useAuthStore()
await store.init()
expect(fetchMock).toHaveBeenCalledTimes(1)
expect(fetchMock).toHaveBeenCalledWith(
'http://test.xueai.art/newapi/api/login/validateToken',
{
method: 'POST',
headers: {
authorization: 'url-token',
},
},
)
expect(store.token).toBe('url-token')
expect(store.user?.id).toBe('19')
expect(store.user?.username).toBe('test20')
expect(store.user?.nickname).toBe('测试学生3')
})
})

View File

@ -16,17 +16,26 @@ const DEV_BYPASS_USER: UserInfo = {
nickname: '开发环境用户', nickname: '开发环境用户',
}; };
// 认证接口返回格式 // 认证接口返回格式
interface AuthResponse { interface AuthResponse {
code: string; status: number;
msg: string; message: string;
success: boolean; data: {
timestamp: number; token: string;
data: UserInfo | null; userInfo: {
} userId: number | string;
userName?: string;
// 认证接口 realName?: string;
const AUTH_CHECK_URL = '/api/auth/check/checkTokenRn'; email?: string;
userIcon?: string;
[key: string]: unknown;
};
[key: string]: unknown;
} | null;
}
// 认证接口
const AUTH_CHECK_URL = 'http://test.xueai.art/newapi/api/login/validateToken';
const AUTH_TOKEN_STORAGE_KEY = 'DEV_DEFAULT_TOKEN'; const AUTH_TOKEN_STORAGE_KEY = 'DEV_DEFAULT_TOKEN';
export const useAuthStore = defineStore('auth', () => { export const useAuthStore = defineStore('auth', () => {
@ -42,21 +51,33 @@ export const useAuthStore = defineStore('auth', () => {
/** /**
* token * token
*/ */
async function checkToken(tokenToCheck: string): Promise<UserInfo | null> { async function checkToken(tokenToCheck: string): Promise<UserInfo | null> {
try { try {
const response = await fetch(`${AUTH_CHECK_URL}/${tokenToCheck}`); const response = await fetch(AUTH_CHECK_URL, {
if (!response.ok) { method: 'POST',
return null; headers: {
} authorization: tokenToCheck,
const data: AuthResponse = await response.json(); },
});
if (!response.ok) {
if (data.success && data.data) { return null;
return data.data; }
}else{ const data: AuthResponse = await response.json();
window.$toast?.('[Auth] Token 验证失败:Token无效');
} if (data.status === 1000 && data.data?.userInfo) {
return null; const { userInfo } = data.data;
return {
...userInfo,
id: String(userInfo.userId),
username: userInfo.userName,
nickname: userInfo.realName,
email: userInfo.email,
avatar: userInfo.userIcon,
};
} else {
window.$toast?.('[Auth] Token 验证失败:Token无效');
}
return null;
} catch (error) { } catch (error) {
console.error('[Auth] Token 验证失败:', error); console.error('[Auth] Token 验证失败:', error);
@ -68,21 +89,21 @@ export const useAuthStore = defineStore('auth', () => {
* - URL token * - URL token
*/ */
async function init() { async function init() {
if (DEV_AUTH_BYPASS) { const searchParams = new URLSearchParams(window.location.search);
const urlToken = searchParams.get('token');
// 获取 tokenURL > localStorage > 默认值
const tokenValue = urlToken
|| localStorage.getItem(AUTH_TOKEN_STORAGE_KEY)
|| DEV_DEFAULT_TOKEN;
if (DEV_AUTH_BYPASS && !tokenValue) {
token.value = null; token.value = null;
user.value = DEV_BYPASS_USER; user.value = DEV_BYPASS_USER;
isInitialized.value = true; isInitialized.value = true;
return; return;
} }
const searchParams = new URLSearchParams(window.location.search);
const urlToken = searchParams.get('token');
// 获取 tokenURL > localStorage > 默认值
const tokenValue = urlToken
|| localStorage.getItem(AUTH_TOKEN_STORAGE_KEY)
|| DEV_DEFAULT_TOKEN;
if (!tokenValue) { if (!tokenValue) {
isInitialized.value = true; isInitialized.value = true;
window.$toast?.('未登录,请先登录', 'error'); window.$toast?.('未登录,请先登录', 'error');
@ -122,10 +143,7 @@ export const useAuthStore = defineStore('auth', () => {
return {}; return {};
} }
// 初始化(不等待,让调用方通过 isInitialized 判断) return {
init();
return {
// 状态 // 状态
token, token,
user, user,

102
start.sh
View File

@ -1,46 +1,112 @@
#!/bin/bash #!/usr/bin/env bash
set -u
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$SCRIPT_DIR"
SERVER_DIR="$ROOT_DIR/server"
BACKEND_PORT="${PORT:-8002}"
FRONTEND_PORT="${FRONTEND_PORT:-5173}"
BACKEND_PID=""
FRONTEND_PID=""
echo "==========================================" echo "=========================================="
echo " 启动 AI Chat 平台 (前端 + 后端) " echo " 启动 AI Chat 平台 (前端 + 后端)"
echo "==========================================" echo "=========================================="
# 设置清理函数,在收到 Ctrl+C 时关闭所有子进程 require_file() {
cleanup() { local path="$1"
echo "" local message="$2"
echo "正在关闭所有服务..." if [[ ! -e "$path" ]]; then
kill $(jobs -p) 2>/dev/null echo "[错误] $message"
exit exit 1
fi
} }
# 清除旧进程 kill_port() {
lsof -i :8000 -t | xargs -r kill -9; lsof -i :5173 -t | xargs -r kill -9; local port="$1"
local pids
if ! command -v lsof >/dev/null 2>&1; then
return 0
fi
pids="$(lsof -ti :"$port" 2>/dev/null || true)"
if [[ -n "$pids" ]]; then
echo "[系统] 端口 $port 已被占用,正在停止旧进程..."
kill $pids 2>/dev/null || true
sleep 1
pids="$(lsof -ti :"$port" 2>/dev/null || true)"
if [[ -n "$pids" ]]; then
kill -9 $pids 2>/dev/null || true
fi
fi
}
cleanup() {
local exit_code=$?
trap - SIGINT SIGTERM EXIT
echo
echo "[系统] 正在关闭所有服务..."
if [[ -n "$FRONTEND_PID" ]] && kill -0 "$FRONTEND_PID" 2>/dev/null; then
kill "$FRONTEND_PID" 2>/dev/null || true
fi
if [[ -n "$BACKEND_PID" ]] && kill -0 "$BACKEND_PID" 2>/dev/null; then
kill "$BACKEND_PID" 2>/dev/null || true
fi
wait "$FRONTEND_PID" 2>/dev/null || true
wait "$BACKEND_PID" 2>/dev/null || true
exit "$exit_code"
}
# 捕获退出信号
trap cleanup SIGINT SIGTERM EXIT trap cleanup SIGINT SIGTERM EXIT
# 启动后端 require_file "$ROOT_DIR/package.json" "未找到前端 package.json"
require_file "$SERVER_DIR/main.py" "未找到后端入口 server/main.py"
require_file "$SERVER_DIR/.venv/bin/python" "未找到后端虚拟环境,请先在 server/.venv 安装依赖"
if ! command -v npm >/dev/null 2>&1; then
echo "[错误] 未找到 npm"
exit 1
fi
kill_port "$BACKEND_PORT"
kill_port "$FRONTEND_PORT"
echo "[系统] 正在启动后端服务器..." echo "[系统] 正在启动后端服务器..."
cd /home/mt/Project/ai-chat-ui/server cd /home/mt/Projects/ai-chat-ui/server
if [ -d ".venv" ]; then if [ -d ".venv" ]; then
source .venv/bin/activate source .venv/bin/activate
# 使用 -u 参数强制不缓冲输出,实时显示日志 # 使用 -u 参数强制不缓冲输出,实时显示日志
python3 -u main.py & python3 -u main.py &
BACKEND_PID=$!
else else
echo "[错误] 未找到虚拟环境 (.venv)。请先创建。" echo "[错误] 未找到虚拟环境 (.venv)。请先创建。"
fi fi
# 等待一小段时间确保后端启动 # 等待一小段时间确保后端启动
sleep 2 sleep 2
if ! kill -0 "$BACKEND_PID" 2>/dev/null; then
echo "[错误] 后端启动失败"
exit 1
fi
# 启动前端
echo "[系统] 正在启动前端服务器..." echo "[系统] 正在启动前端服务器..."
cd /home/mt/Project/ai-chat-ui cd /home/mt/Projects/ai-chat-ui
# 启动 vite 开发服务器 # 启动 vite 开发服务器
npm run dev & npm run dev &
FRONTEND_PID=$!
echo "==========================================" echo "=========================================="
echo " 服务已启动,按 Ctrl+C 停止 " echo " 服务已启动,按 Ctrl+C 停止"
echo "------------------------------------------"
echo " 前端: http://localhost:$FRONTEND_PORT"
echo " 后端: http://localhost:$BACKEND_PORT"
echo "==========================================" echo "=========================================="
# 使用 wait 阻塞主进程,保持脚本运行,这样可以看到调试打印信息 wait "$BACKEND_PID" "$FRONTEND_PID"
wait