Compare commits
2 Commits
a130e0d3f5
...
95c75ce331
| Author | SHA1 | Date | |
|---|---|---|---|
| 95c75ce331 | |||
| 4a434f9580 |
1174
pnpm-lock.yaml
1174
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
3
pnpm-workspace.yaml
Normal file
3
pnpm-workspace.yaml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
allowBuilds:
|
||||||
|
'@parcel/watcher': true
|
||||||
|
esbuild: true
|
||||||
@ -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
|
||||||
54
src/__tests__/authStore.test.ts
Normal file
54
src/__tests__/authStore.test.ts
Normal 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')
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -18,15 +18,24 @@ const DEV_BYPASS_USER: UserInfo = {
|
|||||||
|
|
||||||
// 认证接口返回格式
|
// 认证接口返回格式
|
||||||
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;
|
||||||
|
email?: string;
|
||||||
|
userIcon?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
[key: string]: unknown;
|
||||||
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 认证接口
|
// 认证接口
|
||||||
const AUTH_CHECK_URL = '/api/auth/check/checkTokenRn';
|
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', () => {
|
||||||
@ -44,16 +53,28 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
*/
|
*/
|
||||||
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, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
authorization: tokenToCheck,
|
||||||
|
},
|
||||||
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const data: AuthResponse = await response.json();
|
const data: AuthResponse = await response.json();
|
||||||
|
|
||||||
|
if (data.status === 1000 && data.data?.userInfo) {
|
||||||
if (data.success && data.data) {
|
const { userInfo } = data.data;
|
||||||
return data.data;
|
return {
|
||||||
}else{
|
...userInfo,
|
||||||
|
id: String(userInfo.userId),
|
||||||
|
username: userInfo.userName,
|
||||||
|
nickname: userInfo.realName,
|
||||||
|
email: userInfo.email,
|
||||||
|
avatar: userInfo.userIcon,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
window.$toast?.('[Auth] Token 验证失败:Token无效');
|
window.$toast?.('[Auth] Token 验证失败:Token无效');
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@ -68,13 +89,6 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
* 初始化 - 从 URL 参数获取 token,验证后设置用户信息
|
* 初始化 - 从 URL 参数获取 token,验证后设置用户信息
|
||||||
*/
|
*/
|
||||||
async function init() {
|
async function init() {
|
||||||
if (DEV_AUTH_BYPASS) {
|
|
||||||
token.value = null;
|
|
||||||
user.value = DEV_BYPASS_USER;
|
|
||||||
isInitialized.value = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const searchParams = new URLSearchParams(window.location.search);
|
const searchParams = new URLSearchParams(window.location.search);
|
||||||
const urlToken = searchParams.get('token');
|
const urlToken = searchParams.get('token');
|
||||||
|
|
||||||
@ -83,6 +97,13 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
|| localStorage.getItem(AUTH_TOKEN_STORAGE_KEY)
|
|| localStorage.getItem(AUTH_TOKEN_STORAGE_KEY)
|
||||||
|| DEV_DEFAULT_TOKEN;
|
|| DEV_DEFAULT_TOKEN;
|
||||||
|
|
||||||
|
if (DEV_AUTH_BYPASS && !tokenValue) {
|
||||||
|
token.value = null;
|
||||||
|
user.value = DEV_BYPASS_USER;
|
||||||
|
isInitialized.value = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!tokenValue) {
|
if (!tokenValue) {
|
||||||
isInitialized.value = true;
|
isInitialized.value = true;
|
||||||
window.$toast?.('未登录,请先登录', 'error');
|
window.$toast?.('未登录,请先登录', 'error');
|
||||||
@ -122,9 +143,6 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化(不等待,让调用方通过 isInitialized 判断)
|
|
||||||
init();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// 状态
|
// 状态
|
||||||
token,
|
token,
|
||||||
|
|||||||
102
start.sh
102
start.sh
@ -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
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user