feat: add auth api for aura

feat/unify-api-and-responsive-pages
yannyang 2026-07-09 20:53:57 +08:00
parent 1e55de78dd
commit 6dc5f1f750
14 changed files with 385 additions and 204 deletions

View File

@ -54,9 +54,10 @@ export interface Agent {
} }
export const AgentAPI = { export const AgentAPI = {
list: () => api.get<Agent[]>('/agents').then((r) => r.data), list: (phone: string) => api.post<Agent[]>('/agents', { phone }).then((r) => r.data),
mine: (phone: string) => api.post<Agent[]>('/agents/mine', { phone }).then((r) => r.data),
detail: (id: string) => api.get<Agent>(`/agents/${id}`).then((r) => r.data), detail: (id: string) => api.get<Agent>(`/agents/${id}`).then((r) => r.data),
create: (payload: Partial<Agent>) => api.post<Agent>('/agents', payload).then((r) => r.data), create: (payload: Partial<Agent>) => api.post<Agent>('/agents/create', payload).then((r) => r.data),
update: (id: string, payload: Partial<Agent>) => api.put<Agent>(`/agents/${id}`, payload).then((r) => r.data), update: (id: string, payload: Partial<Agent>) => api.put<Agent>(`/agents/${id}`, payload).then((r) => r.data),
remove: (id: string) => api.delete(`/agents/${id}`).then((r) => r.data), remove: (id: string) => api.delete(`/agents/${id}`).then((r) => r.data),

View File

@ -3,28 +3,28 @@ import { api, API_BASE_URL } from './http';
export interface AuthUser { export interface AuthUser {
id: string; id: string;
email: string; phone: string;
name: string; name: string;
role: 'admin' | 'user'; role: 'admin' | 'user';
token: string;
} }
export const AuthAPI = { export const AuthAPI = {
me: () => api.get<AuthUser>('/auth/me').then((r) => r.data), me: () => api.get<AuthUser>('/auth/me').then((r) => r.data),
verify: async (email: string, password: string) => { verify: async (phone: string, password: string) => {
try { try {
const res = await axios.post(`${API_BASE_URL}/urser`, { email, password }, { timeout: 3000 }); const res = await axios.post(`${API_BASE_URL}/urser`, { phone, password }, { timeout: 3000 });
return res.data; return res.data;
} catch (e) { } catch (e) {
console.warn('Backend /urser not available, fallback to mock true', e); console.warn('Backend /urser not available, fallback to mock true', e);
return true; return true;
} }
}, },
login: (email: string, password: string) => api.post<AuthUser>('/auth/login', { email, password }).then((r) => r.data), login: (phone: string, password: string) => api.post<AuthUser>('/auth/login', { phone, password }).then((r) => r.data),
register: (payload: { email: string; password: string; name: string; inviteCode?: string }) => register: (payload: { phone: string; password: string; name: string; inviteCode?: string }) =>
api.post<AuthUser>('/auth/register', payload).then((r) => r.data), api.post<AuthUser>('/auth/register', payload).then((r) => r.data),
logout: () => api.post('/auth/logout').then((r) => r.data), logout: () => api.post('/auth/logout').then((r) => r.data),
listInvites: () => api.get('/auth/invites').then((r) => r.data), listInvites: () => api.get('/auth/invites').then((r) => r.data),
createInvite: (payload: { email?: string; teamId?: string; role?: string; ttlHours?: number }) => api.post('/auth/invites', payload).then((r) => r.data), createInvite: (payload: { phone?: string; teamId?: string; role?: string; ttlHours?: number }) => api.post('/auth/invites', payload).then((r) => r.data),
deleteInvite: (code: string) => api.delete(`/auth/invites/${code}`).then((r) => r.data) deleteInvite: (code: string) => api.delete(`/auth/invites/${code}`).then((r) => r.data)
}; };

View File

@ -1,10 +1,10 @@
import axios from 'axios'; import axios from 'axios';
import { clearUserStorage } from '../utils/storage';
export const API_BASE_URL = 'https://tianchaoai.cc/aura/v1'; export const API_BASE_URL = import.meta.env.DEV ? '/api' : 'https://tianchaoai.cc/aura/v1';
const APP_BASE = (import.meta.env.BASE_URL || '/').replace(/\/$/, ''); const APP_BASE = (import.meta.env.BASE_URL || '/').replace(/\/$/, '');
export const withAppBase = (path: string) => `${APP_BASE}${path.startsWith('/') ? path : `/${path}`}`; export const withAppBase = (path: string) => `${APP_BASE}${path.startsWith('/') ? path : `/${path}`}`;
export const withApiBase = (path: string) => `${API_BASE_URL}${path.startsWith('/') ? path : `/${path}`}`; export const withApiBase = (path: string) => `${API_BASE_URL}${path.startsWith('/') ? path : `/${path}`}`;
const isMockAuth = () => typeof localStorage !== 'undefined' && localStorage.getItem('mock-auth') === '1';
export const api = axios.create({ export const api = axios.create({
baseURL: API_BASE_URL, baseURL: API_BASE_URL,
@ -12,15 +12,36 @@ export const api = axios.create({
withCredentials: true withCredentials: true
}); });
// 请求拦截器
api.interceptors.request.use((config) => {
const token = localStorage.getItem('aura-token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
api.interceptors.response.use( api.interceptors.response.use(
(r) => r, (r) => r,
(err) => { (err) => {
const isLoginPage = location.pathname === '/login' || location.pathname === withAppBase('/login'); const isLoginPage = location.pathname === '/login' || location.pathname === withAppBase('/login');
if (err?.response?.status === 401 && !isLoginPage && !isMockAuth()) { const isAuthPath = err.config?.url?.includes('/auth/login') || err.config?.url?.includes('/auth/register');
const next = encodeURIComponent(location.pathname + location.search);
if (err?.response?.status === 401 && !isLoginPage && !isAuthPath) {
clearUserStorage();
let nextPath = location.pathname;
let nextSearch = location.search;
// 如果是聊天页面,只进入 /chat 列表页,不进入具体的智能体或会话,防止切换账号后的权限冲突
if (nextPath.includes('/chat')) {
nextPath = '/chat';
nextSearch = '';
}
const next = encodeURIComponent(nextPath + nextSearch);
location.href = `${withAppBase('/login')}?next=${next}`; location.href = `${withAppBase('/login')}?next=${next}`;
} }
return Promise.reject(err); return Promise.reject(err);
} }
); );

View File

@ -118,7 +118,7 @@ export default function Sidebar({ onOpenPalette, onNavigate }: Props) {
items: [ items: [
{ {
key: 'name', key: 'name',
label: <span className="sidebar-user-role">{user.email}</span>, label: <span className="sidebar-user-role">{user.phone}</span>,
disabled: true disabled: true
}, },
{ type: 'divider' }, { type: 'divider' },

View File

@ -1,14 +1,17 @@
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { Agent, AgentAPI } from '../../api'; import { Agent, AgentAPI } from '../../api';
import { useAuth } from '../../store/auth';
export function useAgentListLogic() { export function useAgentListLogic() {
const { user } = useAuth();
const [list, setList] = useState<Agent[]>([]); const [list, setList] = useState<Agent[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const load = async () => { const load = async () => {
if (!user?.phone) return;
setLoading(true); setLoading(true);
try { try {
setList(await AgentAPI.list()); setList(await AgentAPI.mine(user.phone));
} finally { } finally {
setLoading(false); setLoading(false);
} }
@ -16,7 +19,7 @@ export function useAgentListLogic() {
useEffect(() => { useEffect(() => {
load(); load();
}, []); }, [user?.phone]);
const handleDelete = async (id: string) => { const handleDelete = async (id: string) => {
await AgentAPI.remove(id); await AgentAPI.remove(id);

View File

@ -0,0 +1,197 @@
.agent-list-header {
border-radius: 24px;
padding: 30px 30px 26px;
background: linear-gradient(135deg, rgba(255,255,255,0.98) 0%, rgba(236,253,245,0.92) 48%, rgba(239,246,255,0.96) 100%);
border: 1px solid rgba(8, 145, 178, 0.12);
box-shadow: 0 20px 48px rgba(15, 23, 42, 0.06);
margin-bottom: 24px;
}
.agent-list-header-content {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 20px;
flex-wrap: wrap;
margin-bottom: 22px;
}
.agent-list-intro {
max-width: 620px;
}
.agent-list-badge {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
border-radius: 999px;
background: rgba(255,255,255,0.78);
border: 1px solid rgba(8, 145, 178, 0.10);
color: var(--color-text-secondary);
font-size: 12px;
font-weight: 600;
margin-bottom: 16px;
}
.agent-list-badge-icon {
color: var(--color-brand);
}
.agent-list-stats-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px;
}
.agent-list-stat-card {
border-radius: 18px;
padding: 16px 18px;
background: rgba(255,255,255,0.72);
border: 1px solid rgba(255,255,255,0.7);
}
.agent-list-stat-label {
font-size: 12.5px;
color: var(--color-text-secondary);
margin-bottom: 10px;
}
.agent-list-stat-content {
display: flex;
align-items: baseline;
gap: 8px;
}
.agent-list-stat-value {
font-size: 28px;
font-weight: 700;
color: var(--color-text);
}
.agent-list-stat-badge {
border-radius: 999px;
padding: 4px 8px;
font-size: 12px;
font-weight: 600;
}
.agent-card {
border-radius: 20px;
padding: 20px;
min-height: 292px;
background: linear-gradient(180deg, rgba(255,255,255,0.98) 0%, rgba(252,252,253,1) 100%);
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.045);
display: flex;
flex-direction: column;
}
.agent-card-header {
display: flex;
align-items: flex-start;
gap: 12px;
}
.agent-card-avatar {
border-radius: 50%;
overflow: hidden;
width: 54px;
height: 54px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
font-weight: 700;
color: #fff;
}
.agent-card-title-group {
flex: 1;
min-width: 0;
}
.agent-card-name {
font-weight: 700;
font-size: 17px;
color: var(--color-text);
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.agent-card-update-time {
font-size: 12.5px;
color: var(--color-text-tertiary);
}
.agent-card-desc-container {
margin-top: 16px;
padding: 16px 16px 18px;
border-radius: 16px;
background: linear-gradient(180deg, rgba(248,250,252,0.9) 0%, rgba(255,255,255,0.95) 100%);
border: 1px solid rgba(148, 163, 184, 0.14);
}
.agent-card-desc {
min-height: 66px;
font-size: 13.5px;
line-height: 1.7;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.agent-card-tags {
margin-top: 14px;
}
/* 修复模型标签上下不居中问题 */
.agent-card-tag-model {
display: inline-flex !important;
align-items: center !important;
vertical-align: middle;
}
.agent-card-tag-model-text {
display: inline-block;
max-width: 190px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.2; /* 确保文字行高不影响居中 */
}
.agent-card-actions {
display: flex;
gap: 8px;
margin-top: auto;
padding-top: 16px;
border-top: 1px solid var(--color-border);
}
.agent-list-banner {
margin-top: 24px;
border-radius: 20px;
padding: 18px 20px;
background: var(--color-surface);
border: 1px solid var(--color-border);
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
}
.agent-list-banner-title {
font-size: 15px;
font-weight: 600;
color: var(--color-text);
margin-bottom: 4px;
}
.agent-list-banner-desc {
font-size: 13px;
color: var(--color-text-secondary);
}

View File

@ -9,8 +9,8 @@ import {
import { Button, Col, Row, Empty, Popconfirm, App as AntApp, Tag, Space } from 'antd'; import { Button, Col, Row, Empty, Popconfirm, App as AntApp, Tag, Space } from 'antd';
import { Link, useNavigate } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import type { Agent } from '../../../api';
import type { AgentListLogicOutput } from '../AgentListLogic'; import type { AgentListLogicOutput } from '../AgentListLogic';
import './AgentListWeb.css';
interface Props { interface Props {
logic: AgentListLogicOutput; logic: AgentListLogicOutput;
@ -23,44 +23,11 @@ export default function AgentListWeb({ logic }: Props) {
return ( return (
<div className="page-container"> <div className="page-container">
<div <div className="agent-list-header">
style={{ <div className="agent-list-header-content">
borderRadius: 24, <div className="agent-list-intro">
padding: '30px 30px 26px', <div className="agent-list-badge">
background: <RobotOutlined className="agent-list-badge-icon" />
'linear-gradient(135deg, rgba(255,255,255,0.98) 0%, rgba(236,253,245,0.92) 48%, rgba(239,246,255,0.96) 100%)',
border: '1px solid rgba(8, 145, 178, 0.12)',
boxShadow: '0 20px 48px rgba(15, 23, 42, 0.06)',
marginBottom: 24,
}}
>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-start',
gap: 20,
flexWrap: 'wrap',
marginBottom: 22,
}}
>
<div style={{ maxWidth: 620 }}>
<div
style={{
display: 'inline-flex',
alignItems: 'center',
gap: 8,
padding: '6px 12px',
borderRadius: 999,
background: 'rgba(255,255,255,0.78)',
border: '1px solid rgba(8, 145, 178, 0.10)',
color: 'var(--color-text-secondary)',
fontSize: 12,
fontWeight: 600,
marginBottom: 16,
}}
>
<RobotOutlined style={{ color: 'var(--color-brand)' }} />
Agent Agent
</div> </div>
@ -82,28 +49,17 @@ export default function AgentListWeb({ logic }: Props) {
</Button> </Button>
</div> </div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: 14 }}> <div className="agent-list-stats-grid">
{stats.map((item) => ( {stats.map((item) => (
<div <div key={item.label} className="agent-list-stat-card">
key={item.label} <div className="agent-list-stat-label">{item.label}</div>
style={{ <div className="agent-list-stat-content">
borderRadius: 18, <span className="agent-list-stat-value">{item.value}</span>
padding: '16px 18px',
background: 'rgba(255,255,255,0.72)',
border: '1px solid rgba(255,255,255,0.7)',
}}
>
<div style={{ fontSize: 12.5, color: 'var(--color-text-secondary)', marginBottom: 10 }}>{item.label}</div>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8 }}>
<span style={{ fontSize: 28, fontWeight: 700, color: 'var(--color-text)' }}>{item.value}</span>
<span <span
className="agent-list-stat-badge"
style={{ style={{
borderRadius: 999,
padding: '4px 8px',
background: item.tone, background: item.tone,
color: item.color, color: item.color,
fontSize: 12,
fontWeight: 600,
}} }}
> >
@ -126,20 +82,11 @@ export default function AgentListWeb({ logic }: Props) {
<Row gutter={[18, 18]}> <Row gutter={[18, 18]}>
{list.map((a) => ( {list.map((a) => (
<Col xs={24} sm={12} md={8} lg={6} key={a.id}> <Col xs={24} sm={12} md={8} lg={6} key={a.id}>
<div className="agent-card">
<div className="agent-card-header">
<div <div
className="agent-card" className="agent-card-avatar"
style={{ style={{ background: a.avatar || 'var(--gradient-brand)' }}
borderRadius: 20,
padding: 20,
minHeight: 292,
background: 'linear-gradient(180deg, rgba(255,255,255,0.98) 0%, rgba(252,252,253,1) 100%)',
boxShadow: '0 12px 28px rgba(15, 23, 42, 0.045)',
}}
>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 12 }}>
<div
className="avatar"
style={{ background: a.avatar || 'var(--gradient-brand)', borderRadius: '50%', overflow: 'hidden', width: 54, height: 54 }}
> >
{isImageUrl(a.avatar) ? ( {isImageUrl(a.avatar) ? (
<img src={a.avatar} className="w-full h-full object-cover" alt="avatar" /> <img src={a.avatar} className="w-full h-full object-cover" alt="avatar" />
@ -147,29 +94,21 @@ export default function AgentListWeb({ logic }: Props) {
(a.name?.charAt(0) || '?').toUpperCase() (a.name?.charAt(0) || '?').toUpperCase()
)} )}
</div> </div>
<div style={{ flex: 1, minWidth: 0 }}> <div className="agent-card-title-group">
<div style={{ fontWeight: 700, fontSize: 17, color: 'var(--color-text)', marginBottom: 4 }}>{a.name}</div> <div className="agent-card-name">{a.name}</div>
<div style={{ fontSize: 12.5, color: 'var(--color-text-tertiary)' }}> <div className="agent-card-update-time">
{dayjs(a.updated_at).format('YYYY-MM-DD')} {dayjs(a.updated_at).format('YYYY-MM-DD')}
</div> </div>
</div> </div>
</div> </div>
<div <div className="agent-card-desc-container">
style={{ <div className="agent-card-desc">
marginTop: 16,
padding: '16px 16px 18px',
borderRadius: 16,
background: 'linear-gradient(180deg, rgba(248,250,252,0.9) 0%, rgba(255,255,255,0.95) 100%)',
border: '1px solid rgba(148, 163, 184, 0.14)',
}}
>
<div className="desc" style={{ minHeight: 66, fontSize: 13.5, lineHeight: 1.7 }}>
{a.description || '还没有填写描述,可以补充这个智能体适合解决什么问题。'} {a.description || '还没有填写描述,可以补充这个智能体适合解决什么问题。'}
</div> </div>
</div> </div>
<Space size={6} wrap style={{ marginTop: 14 }}> <Space size={6} wrap className="agent-card-tags">
{a.visibility === 'public' && ( {a.visibility === 'public' && (
<Tag bordered={false} style={{ background: 'var(--color-success-soft)', color: 'var(--color-success)', borderRadius: 999, margin: 0 }}> <Tag bordered={false} style={{ background: 'var(--color-success-soft)', color: 'var(--color-success)', borderRadius: 999, margin: 0 }}>
@ -186,8 +125,12 @@ export default function AgentListWeb({ logic }: Props) {
</Tag> </Tag>
)} )}
{getModelLabel(a.model) && ( {getModelLabel(a.model) && (
<Tag bordered={false} style={{ background: 'var(--color-brand-soft)', color: 'var(--color-brand)', borderRadius: 999, margin: 0, maxWidth: '100%' }}> <Tag
<span style={{ display: 'inline-block', maxWidth: 190, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}> bordered={false}
className="agent-card-tag-model"
style={{ background: 'var(--color-brand-soft)', color: 'var(--color-brand)', borderRadius: 999, margin: 0, maxWidth: '100%' }}
>
<span className="agent-card-tag-model-text">
{getModelLabel(a.model)} {getModelLabel(a.model)}
</span> </span>
</Tag> </Tag>
@ -199,7 +142,7 @@ export default function AgentListWeb({ logic }: Props) {
)} )}
</Space> </Space>
<div style={{ display: 'flex', gap: 8, marginTop: 'auto', paddingTop: 16, borderTop: '1px solid var(--color-border)' }}> <div className="agent-card-actions">
<Link to={`/chat/${a.id}`} style={{ flex: 1 }}> <Link to={`/chat/${a.id}`} style={{ flex: 1 }}>
<Button type="primary" block icon={<MessageOutlined />} style={{ borderRadius: 12, height: 40, fontWeight: 600 }}> <Button type="primary" block icon={<MessageOutlined />} style={{ borderRadius: 12, height: 40, fontWeight: 600 }}>
@ -230,25 +173,12 @@ export default function AgentListWeb({ logic }: Props) {
)} )}
{list.length > 0 && ( {list.length > 0 && (
<div <div className="agent-list-banner">
style={{
marginTop: 24,
borderRadius: 20,
padding: '18px 20px',
background: 'var(--color-surface)',
border: '1px solid var(--color-border)',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: 16,
flexWrap: 'wrap',
}}
>
<div> <div>
<div style={{ fontSize: 15, fontWeight: 600, color: 'var(--color-text)', marginBottom: 4 }}> <div className="agent-list-banner-title">
</div> </div>
<div style={{ fontSize: 13, color: 'var(--color-text-secondary)' }}> <div className="agent-list-banner-desc">
广 广
</div> </div>
</div> </div>

View File

@ -17,7 +17,7 @@ export function useLoginPageLogic() {
const onLogin = async (values: any) => { const onLogin = async (values: any) => {
setLoading(true); setLoading(true);
try { try {
await login(values.email, values.password); await login(values.phone, values.password);
message.success('登录成功'); message.success('登录成功');
navigate(next, { replace: true }); navigate(next, { replace: true });
} catch (e: any) { } catch (e: any) {
@ -31,13 +31,14 @@ export function useLoginPageLogic() {
setLoading(true); setLoading(true);
try { try {
await register({ await register({
email: values.email, phone: values.phone,
password: values.password, password: values.password,
name: values.name, name: values.name,
inviteCode: values.inviteCode || undefined inviteCode: values.inviteCode || undefined
}); });
message.success('注册成功,已自动登录'); message.success('注册成功,请登录');
navigate(next, { replace: true }); setTab('login');
return { phone: values.phone, password: values.password };
} catch (e: any) { } catch (e: any) {
message.error(e?.response?.data?.error ?? e?.message ?? '注册失败'); message.error(e?.response?.data?.error ?? e?.message ?? '注册失败');
} finally { } finally {

View File

@ -1,14 +1,26 @@
import { Alert, Button, Form, Input, Tabs } from 'antd'; import { Button, Form, Input, Tabs } from 'antd';
import type { LoginPageLogic, LoginTab } from '../LoginPageLogic'; import type { LoginPageLogic, LoginTab } from '../LoginPageLogic';
export default function LoginFormCard({ logic }: { logic: LoginPageLogic }) { export default function LoginFormCard({ logic }: { logic: LoginPageLogic }) {
const { tab, setTab, loading, onLogin, onRegister } = logic; const { tab, setTab, loading, onLogin, onRegister } = logic;
const [loginForm] = Form.useForm();
const [registerForm] = Form.useForm();
const handleRegister = async (values: any) => {
const result = await onRegister(values);
if (result) {
loginForm.setFieldsValue({
phone: result.phone,
password: result.password,
});
}
};
return ( return (
<div className="login-card"> <div className="login-card">
<div className="login-card-header"> <div className="login-card-header">
<h2 className="login-card-title"></h2> <h2 className="login-card-title"></h2>
<div className="login-card-subtitle">使</div> <div className="login-card-subtitle">使</div>
</div> </div>
<Tabs <Tabs
@ -19,9 +31,14 @@ export default function LoginFormCard({ logic }: { logic: LoginPageLogic }) {
key: 'login', key: 'login',
label: '登录', label: '登录',
children: ( children: (
<Form layout="vertical" onFinish={onLogin} className="login-form"> <Form
<Form.Item name="email" label="邮箱" rules={[{ required: true, type: 'email', message: '请填写合法邮箱' }]}> form={loginForm}
<Input placeholder="you@example.com" size="large" autoFocus /> layout="vertical"
onFinish={onLogin}
className="login-form"
>
<Form.Item name="phone" label="手机号" rules={[{ required: true, message: '请填写手机号' }]}>
<Input placeholder="13800138000" size="large" autoFocus />
</Form.Item> </Form.Item>
<Form.Item name="password" label="密码" rules={[{ required: true }]}> <Form.Item name="password" label="密码" rules={[{ required: true }]}>
<Input.Password placeholder="••••••" size="large" /> <Input.Password placeholder="••••••" size="large" />
@ -36,15 +53,14 @@ export default function LoginFormCard({ logic }: { logic: LoginPageLogic }) {
key: 'register', key: 'register',
label: '注册', label: '注册',
children: ( children: (
<Form layout="vertical" onFinish={onRegister} className="login-form"> <Form
<Alert form={registerForm}
className="login-register-alert" layout="vertical"
type="info" onFinish={handleRegister}
showIcon className="login-form"
message="第一个注册的用户自动成为管理员;之后需要邀请码" >
/> <Form.Item name="phone" label="手机号" rules={[{ required: true, message: '请填写手机号' }]}>
<Form.Item name="email" label="邮箱" rules={[{ required: true, type: 'email' }]}> <Input placeholder="13800138000" size="large" />
<Input placeholder="you@example.com" size="large" />
</Form.Item> </Form.Item>
<Form.Item name="name" label="昵称" rules={[{ required: true }]}> <Form.Item name="name" label="昵称" rules={[{ required: true }]}>
<Input placeholder="张三" size="large" /> <Input placeholder="张三" size="large" />

View File

@ -31,7 +31,7 @@ export function useTeamsPageLogic() {
if (!active) return; if (!active) return;
const inv = await AuthAPI.createInvite({ const inv = await AuthAPI.createInvite({
teamId: active.id, teamId: active.id,
email: v.email || undefined, phone: v.phone || undefined,
ttlHours: Number(v.ttlHours) || 168, ttlHours: Number(v.ttlHours) || 168,
}); });
setLastInviteCode(inv.code); setLastInviteCode(inv.code);

View File

@ -145,8 +145,6 @@ export default function ChatInput(props: {
a.name.toLowerCase().includes(mentionQuery.toLowerCase()) a.name.toLowerCase().includes(mentionQuery.toLowerCase())
); );
console.log('[@mention] filtered:', { mentionQuery, count: filteredAgents.length, agents: filteredAgents.map(a => a.name) });
const handleSelectAgent = (agent: Agent) => { const handleSelectAgent = (agent: Agent) => {
const textarea = inputRef.current?.resizableTextArea?.textArea; const textarea = inputRef.current?.resizableTextArea?.textArea;
if (!textarea) return; if (!textarea) return;

View File

@ -1,6 +1,7 @@
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import type { Agent, BranchInfo, ChatMessage, ModelOverrides } from '../../../api'; import type { Agent, BranchInfo, ChatMessage, ModelOverrides } from '../../../api';
import { AgentAPI, ChatAPI } from '../../../api'; import { AgentAPI, ChatAPI } from '../../../api';
import { useAuth } from '../../../store/auth';
import { parseAgentModels } from '../utils/agentModels'; import { parseAgentModels } from '../utils/agentModels';
export function useChatData(args: { export function useChatData(args: {
@ -13,6 +14,7 @@ export function useChatData(args: {
setOverrides: (updater: (prev: ModelOverrides) => ModelOverrides) => void; setOverrides: (updater: (prev: ModelOverrides) => ModelOverrides) => void;
abort: () => void; abort: () => void;
}) { }) {
const { user } = useAuth();
const { agentId, roomId, highlightId, setHighlightId, scrollBottom, initialScrollDoneRef, setOverrides, abort } = args; const { agentId, roomId, highlightId, setHighlightId, scrollBottom, initialScrollDoneRef, setOverrides, abort } = args;
const [agent, setAgent] = useState<Agent | null>(null); const [agent, setAgent] = useState<Agent | null>(null);
const [agentList, setAgentList] = useState<Agent[]>([]); const [agentList, setAgentList] = useState<Agent[]>([]);
@ -40,8 +42,9 @@ export function useChatData(args: {
}; };
const loadAgentList = async () => { const loadAgentList = async () => {
if (!user?.phone) return;
try { try {
const list = await AgentAPI.list(); const list = await AgentAPI.list(user.phone);
setAgentList(list); setAgentList(list);
} catch { } catch {
// ignore // ignore
@ -69,7 +72,7 @@ export function useChatData(args: {
useEffect(() => { useEffect(() => {
loadAgentList(); loadAgentList();
}, []); }, [user?.phone]);
useEffect(() => { useEffect(() => {
if (!agentId) { if (!agentId) {

View File

@ -1,19 +1,20 @@
import { create } from 'zustand'; import { create } from 'zustand';
import { AuthAPI, AuthUser } from '../api'; import { AuthAPI, AuthUser } from '../api';
import { clearUserStorage } from '../utils/storage';
interface AuthState { interface AuthState {
user: AuthUser | null; user: AuthUser | null;
loading: boolean; loading: boolean;
/** 启动时调用:从后端拉当前登录态 */ /** 启动时调用:从后端拉当前登录态 */
bootstrap: () => Promise<void>; bootstrap: () => Promise<void>;
login: (email: string, password: string) => Promise<void>; login: (phone: string, password: string) => Promise<void>;
register: (p: { email: string; password: string; name: string; inviteCode?: string }) => Promise<void>; register: (p: { phone: string; password: string; name: string; inviteCode?: string }) => Promise<void>;
logout: () => Promise<void>; logout: () => Promise<void>;
} }
const loadSavedUser = (): AuthUser | null => { const loadSavedUser = (): AuthUser | null => {
try { try {
const raw = localStorage.getItem('mock-user'); const raw = localStorage.getItem('aura-user');
if (!raw) return null; if (!raw) return null;
return JSON.parse(raw) as AuthUser; return JSON.parse(raw) as AuthUser;
} catch { } catch {
@ -25,38 +26,28 @@ export const useAuth = create<AuthState>((set) => ({
user: typeof localStorage === 'undefined' ? null : loadSavedUser(), user: typeof localStorage === 'undefined' ? null : loadSavedUser(),
loading: false, loading: false,
bootstrap: async () => { bootstrap: async () => {
set({ loading: false });
},
login: async (email, password) => {
const ok = await AuthAPI.verify(email, password);
if (!ok) throw new Error('身份验证失败');
const u: AuthUser = {
id: 'mock',
email,
name: email.split('@')[0] || 'User',
role: 'user'
};
try { try {
localStorage.setItem('mock-auth', '1'); const u = await AuthAPI.me();
localStorage.setItem('mock-user', JSON.stringify(u)); set({ user: u });
} catch { localStorage.setItem('aura-user', JSON.stringify(u));
} } catch {}
},
login: async (phone, password) => {
const u = await AuthAPI.login(phone, password);
if (!u || !u.token) throw new Error('登录失败');
localStorage.setItem('aura-token', u.token);
localStorage.setItem('aura-user', JSON.stringify(u));
set({ user: u }); set({ user: u });
}, },
register: async (p) => { register: async (p) => {
const u = await AuthAPI.register(p); // 注册成功后不自动登录
set({ user: u }); await AuthAPI.register(p);
}, },
logout: async () => { logout: async () => {
try { try {
await AuthAPI.logout(); await AuthAPI.logout();
} catch { } catch {}
} clearUserStorage();
try {
localStorage.removeItem('mock-auth');
localStorage.removeItem('mock-user');
} catch {
}
set({ user: null }); set({ user: null });
} }
})); }));

20
src/utils/storage.ts Normal file
View File

@ -0,0 +1,20 @@
/**
*
* token访
*/
export function clearUserStorage() {
const keysToRemove: string[] = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key) {
// 清除 aura 前缀的 key (aura-token, aura-user)
// 以及聊天相关的 key (chat:lastRoom:*)
if (key.startsWith('aura-') || key.startsWith('chat:')) {
keysToRemove.push(key);
}
}
}
keysToRemove.forEach(key => localStorage.removeItem(key));
}