feat(im): mirror WeChat bot chats into AI chat as WeXin Bot conversation; add conversations.source; migrate history; remove redundant history page

This commit is contained in:
qyyfw committed 2026-07-10 10:09:49 +08:00
1 parent 7c71d193fc
commit 7b7871a14d
5 files changed
+1044 -955

No files matched your search

+96 -64
View File
@@ -691,6 +691,7 @@ def list_conversations(
"status": conv.status, "status": conv.status,
"pinned": conv.pinned, "pinned": conv.pinned,
"knowledge_graph_count": conv.knowledge_graph_count or 0, "knowledge_graph_count": conv.knowledge_graph_count or 0,
"source": getattr(conv, "source", None),
"created_at": str(conv.created_at) if conv.created_at else None, "created_at": str(conv.created_at) if conv.created_at else None,
"updated_at": str(conv.updated_at) if conv.updated_at else None, "updated_at": str(conv.updated_at) if conv.updated_at else None,
}) })
@@ -1140,69 +1141,8 @@ def clear_conversation(
return {"code": 0, "message": "对话上下文已清空"} return {"code": 0, "message": "对话上下文已清空"}
@router.post("/conversations/{conv_id}/to-knowledge-graph") def _build_knowledge_graph(db: Session, kb: KnowledgeBase) -> dict:
def convert_to_knowledge_graph( """从知识库文档构建知识图谱节点和边,返回 graph_data。"""
conv_id: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""将对话内容转换为知识图谱"""
conv = db.query(Conversation).filter(
and_(Conversation.id == conv_id, Conversation.user_id == current_user.id)
).first()
if not conv:
raise HTTPException(status_code=404, detail="对话不存在")
messages = (
db.query(Message)
.filter(Message.conversation_id == conv_id)
.order_by(asc(Message.created_at))
.all()
)
if not messages:
raise HTTPException(status_code=400, detail="对话中没有消息, 无法生成知识图谱")
kb_name = f"对话图谱-{conv.title}"
kb = KnowledgeBase(
id=gen_uuid(),
user_id=current_user.id,
name=kb_name,
description=f"从对话「{conv.title}」自动生成的知识图谱",
directory_path="/",
document_count=0,
created_at=cst_now(),
updated_at=cst_now(),
)
db.add(kb)
db.flush()
doc_count = 0
for idx, msg in enumerate(messages):
role_label = "用户" if msg.role == "user" else "AI"
preview = re.sub(r'[\\n\\r\\t]+', ' ', msg.content[:40]).strip()
filename = f"msg_{idx+1:03d}_{role_label}_{preview}.txt"
doc = KnowledgeDocument(
id=gen_uuid(),
knowledge_base_id=kb.id,
name=filename,
filename=filename,
file_type="txt",
file_size=len(msg.content.encode('utf-8')),
content=msg.content,
status="completed",
chunk_count=max(1, len(msg.content) // 500 + 1),
created_at=cst_now(),
updated_at=cst_now(),
)
db.add(doc)
doc_count += 1
kb.document_count = doc_count
db.flush()
# 构建知识图谱节点和边
documents = db.query(KnowledgeDocument).filter( documents = db.query(KnowledgeDocument).filter(
and_( and_(
KnowledgeDocument.knowledge_base_id == kb.id, KnowledgeDocument.knowledge_base_id == kb.id,
@@ -1268,7 +1208,96 @@ def convert_to_knowledge_graph(
kb.knowledge_graph_count = len(graph_nodes) kb.knowledge_graph_count = len(graph_nodes)
kb.updated_at = cst_now() kb.updated_at = cst_now()
conv.knowledge_graph_count = (conv.knowledge_graph_count or 0) + 1 return graph_data
@router.post("/conversations/{conv_id}/to-knowledge-graph")
def convert_to_knowledge_graph(
conv_id: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""将对话内容转换为知识图谱(幂等:同一对话仅生成一次知识库)"""
conv = db.query(Conversation).filter(
and_(Conversation.id == conv_id, Conversation.user_id == current_user.id)
).first()
if not conv:
raise HTTPException(status_code=404, detail="对话不存在")
messages = (
db.query(Message)
.filter(Message.conversation_id == conv_id)
.order_by(asc(Message.created_at))
.all()
)
if not messages:
raise HTTPException(status_code=400, detail="对话中没有消息, 无法生成知识图谱")
# 幂等:若该对话已生成过知识图谱且知识库仍存在,则直接刷新返回
if conv.knowledge_graph_base_id:
existing_kb = db.query(KnowledgeBase).filter(
KnowledgeBase.id == conv.knowledge_graph_base_id
).first()
if existing_kb:
graph_data = _build_knowledge_graph(db, existing_kb)
conv.knowledge_graph_count = 1
conv.updated_at = cst_now()
db.commit()
return {
"code": 0,
"data": {
"knowledge_base_id": existing_kb.id,
"knowledge_base_name": existing_kb.name,
"graph": graph_data,
"knowledge_graph_count": 1,
"already_exists": True,
},
"message": "知识图谱已存在, 已刷新展示",
}
kb_name = f"对话图谱-{conv.title}"
kb = KnowledgeBase(
id=gen_uuid(),
user_id=current_user.id,
name=kb_name,
description=f"从对话「{conv.title}」自动生成的知识图谱",
directory_path="/",
document_count=0,
created_at=cst_now(),
updated_at=cst_now(),
)
db.add(kb)
db.flush()
doc_count = 0
for idx, msg in enumerate(messages):
role_label = "用户" if msg.role == "user" else "AI"
preview = re.sub(r'[\\n\\r\\t]+', ' ', msg.content[:40]).strip()
filename = f"msg_{idx+1:03d}_{role_label}_{preview}.txt"
doc = KnowledgeDocument(
id=gen_uuid(),
knowledge_base_id=kb.id,
filename=filename,
file_type="txt",
file_size=len(msg.content.encode('utf-8')),
content=msg.content,
status="completed",
chunk_count=max(1, len(msg.content) // 500 + 1),
created_at=cst_now(),
updated_at=cst_now(),
)
db.add(doc)
doc_count += 1
kb.document_count = doc_count
db.flush()
# 构建知识图谱节点和边
graph_data = _build_knowledge_graph(db, kb)
conv.knowledge_graph_base_id = kb.id
conv.knowledge_graph_count = 1
conv.updated_at = cst_now() conv.updated_at = cst_now()
db.commit() db.commit()
@@ -1285,6 +1314,8 @@ def convert_to_knowledge_graph(
"knowledge_base_id": kb.id, "knowledge_base_id": kb.id,
"knowledge_base_name": kb.name, "knowledge_base_name": kb.name,
"graph": graph_data, "graph": graph_data,
"knowledge_graph_count": 1,
"already_exists": False,
}, },
"message": f"知识图谱构建完成", "message": f"知识图谱构建完成",
} }
@@ -1299,6 +1330,7 @@ def get_models(db: Session = Depends(get_db)):
result.append({ result.append({
"id": m.id, "id": m.id,
"name": m.name, "name": m.name,
"display_name": getattr(m, "display_name", None) or m.name,
"provider_id": m.provider_id, "provider_id": m.provider_id,
"provider_name": provider.name if provider else "", "provider_name": provider.name if provider else "",
"status": m.status, "status": m.status,
File diff suppressed because it is too large. Load diff
+1
View File
@@ -85,6 +85,7 @@ class Conversation(Base):
pinned = Column(Boolean, default=False) pinned = Column(Boolean, default=False)
knowledge_graph_count = Column(Integer, default=0) knowledge_graph_count = Column(Integer, default=0)
knowledge_graph_base_id = Column(String(64), default=None, comment="已生成的知识图谱知识库ID") knowledge_graph_base_id = Column(String(64), default=None, comment="已生成的知识图谱知识库ID")
source = Column(String(32), default=None, comment="对话来源,如 wechat_bot")
created_at = Column(DateTime, default=datetime.now) created_at = Column(DateTime, default=datetime.now)
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
+166 -168
View File
@@ -1,168 +1,166 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Routes, Route, Navigate } from 'react-router-dom'; import { Routes, Route, Navigate } from 'react-router-dom';
import type { User } from '@/types'; import type { User } from '@/types';
import { AuthContext, AuthContextType } from '@/contexts/AuthContext'; import { AuthContext, AuthContextType } from '@/contexts/AuthContext';
import { authAPI } from '@/api'; import { authAPI } from '@/api';
// Layouts // Layouts
import FrontLayout from '@/components/layout/FrontLayout'; import FrontLayout from '@/components/layout/FrontLayout';
import AdminLayout from '@/components/layout/AdminLayout'; import AdminLayout from '@/components/layout/AdminLayout';
// Front Pages // Front Pages
import LandingPage from '@/pages/front/LandingPage'; import LandingPage from '@/pages/front/LandingPage';
import LoginPage from '@/pages/front/LoginPage'; import LoginPage from '@/pages/front/LoginPage';
import ChatPage from '@/pages/front/ChatPage'; import ChatPage from '@/pages/front/ChatPage';
import ChannelPage from '@/pages/front/ChannelPage'; import ChannelPage from '@/pages/front/ChannelPage';
import KnowledgePage from '@/pages/front/KnowledgePage'; import KnowledgePage from '@/pages/front/KnowledgePage';
import MarketPage from '@/pages/front/MarketPage'; import MarketPage from '@/pages/front/MarketPage';
import TokenDashboardPage from '@/pages/front/TokenDashboardPage'; import TokenDashboardPage from '@/pages/front/TokenDashboardPage';
import PricingPage from '@/pages/front/PricingPage'; import PricingPage from '@/pages/front/PricingPage';
import ProfilePage from '@/pages/front/ProfilePage'; import ProfilePage from '@/pages/front/ProfilePage';
import NotificationPage from '@/pages/front/NotificationPage'; import NotificationPage from '@/pages/front/NotificationPage';
import AssetsPage from '@/pages/front/AssetsPage'; import AssetsPage from '@/pages/front/AssetsPage';
import NovelPreviewPage from '@/pages/front/NovelPreviewPage'; import NovelPreviewPage from '@/pages/front/NovelPreviewPage';
import EvolutionDashboard from '@/pages/front/EvolutionDashboard'; import EvolutionDashboard from '@/pages/front/EvolutionDashboard';
import WechatBot from '@/pages/front/WechatBot'; import WechatBot from '@/pages/front/WechatBot';
import WechatHistory from '@/pages/front/WechatHistory';
// Admin Pages
// Admin Pages import AdminDashboard from '@/pages/admin/Dashboard';
import AdminDashboard from '@/pages/admin/Dashboard'; import AdminUsers from '@/pages/admin/Users';
import AdminUsers from '@/pages/admin/Users'; import AdminModels from '@/pages/admin/Models';
import AdminModels from '@/pages/admin/Models'; import AdminSiteSettings from '@/pages/admin/SiteSettings';
import AdminSiteSettings from '@/pages/admin/SiteSettings'; import AdminPayment from '@/pages/admin/Payment';
import AdminPayment from '@/pages/admin/Payment'; import AdminServices from '@/pages/admin/Services';
import AdminServices from '@/pages/admin/Services'; import AdminMembership from '@/pages/admin/Membership';
import AdminMembership from '@/pages/admin/Membership'; import AdminAds from '@/pages/admin/Ads';
import AdminAds from '@/pages/admin/Ads'; import AdminMarket from '@/pages/admin/Market';
import AdminMarket from '@/pages/admin/Market'; import AdminLogs from '@/pages/admin/Logs';
import AdminLogs from '@/pages/admin/Logs'; import AdminSkills from '@/pages/admin/Skills'
import AdminSkills from '@/pages/admin/Skills' import AdminAgents from '@/pages/admin/Agents';
import AdminAgents from '@/pages/admin/Agents'; import AdminCodeExec from '@/pages/admin/CodeExec';;
import AdminCodeExec from '@/pages/admin/CodeExec';; import DistributionBuilder from '@/pages/admin/DistributionBuilder';
import DistributionBuilder from '@/pages/admin/DistributionBuilder'; import PrivacySettings from '@/pages/admin/PrivacySettings';
import PrivacySettings from '@/pages/admin/PrivacySettings'; import ImBots from '@/pages/admin/ImBots';
import ImBots from '@/pages/admin/ImBots'; import Promotion from '@/pages/admin/Promotion';
import Promotion from '@/pages/admin/Promotion'; import Achievement from '@/pages/admin/Achievement';
import Achievement from '@/pages/admin/Achievement'; import Tenant from '@/pages/admin/Tenant';
import Tenant from '@/pages/admin/Tenant'; import Billing from '@/pages/admin/Billing';
import Billing from '@/pages/admin/Billing'; import Analytics from '@/pages/admin/Analytics';
import Analytics from '@/pages/admin/Analytics'; import UserPortal from '@/pages/UserPortal';
import UserPortal from '@/pages/UserPortal'; import AdminKnowledge from '@/pages/admin/Knowledge';
import AdminKnowledge from '@/pages/admin/Knowledge'; import AdminWorkflow from '@/pages/admin/Workflow';
import AdminWorkflow from '@/pages/admin/Workflow'; import AdminStorage from '@/pages/admin/Storage';
import AdminStorage from '@/pages/admin/Storage'; import AdminDeveloper from '@/pages/admin/Developer';
import AdminDeveloper from '@/pages/admin/Developer'; import AdminDeveloperTools from '@/pages/admin/DeveloperTools';
import AdminDeveloperTools from '@/pages/admin/DeveloperTools'; import AdminAutomation from '@/pages/admin/Automation';
import AdminAutomation from '@/pages/admin/Automation'; import AdminMcp from '@/pages/admin/Mcp';
import AdminMcp from '@/pages/admin/Mcp'; import AdminApps from '@/pages/admin/Apps';
import AdminApps from '@/pages/admin/Apps'; import AssetCenter from '@/pages/admin/AssetCenter';
import AssetCenter from '@/pages/admin/AssetCenter';
const App: React.FC = () => {
const App: React.FC = () => { const [user, setUser] = useState<User | null>(null);
const [user, setUser] = useState<User | null>(null); const [loading, setLoading] = useState(true);
const [loading, setLoading] = useState(true);
// 初始化:从localStorage恢复登录状态
// 初始化:从localStorage恢复登录状态 useEffect(() => {
useEffect(() => { const token = localStorage.getItem('aiflow_token');
const token = localStorage.getItem('aiflow_token'); const cachedUser = localStorage.getItem('aiflow_user');
const cachedUser = localStorage.getItem('aiflow_user'); if (token && cachedUser) {
if (token && cachedUser) { try {
try { setUser(JSON.parse(cachedUser));
setUser(JSON.parse(cachedUser)); } catch {
} catch { localStorage.removeItem('aiflow_token');
localStorage.removeItem('aiflow_token'); localStorage.removeItem('aiflow_user');
localStorage.removeItem('aiflow_user'); }
} }
} setLoading(false);
setLoading(false); }, []);
}, []);
const login = (u: User) => {
const login = (u: User) => { setUser(u);
setUser(u); localStorage.setItem('aiflow_user', JSON.stringify(u));
localStorage.setItem('aiflow_user', JSON.stringify(u)); };
}; const logout = () => {
const logout = () => { setUser(null);
setUser(null); localStorage.removeItem('aiflow_token');
localStorage.removeItem('aiflow_token'); localStorage.removeItem('aiflow_user');
localStorage.removeItem('aiflow_user'); };
}; const isAdmin = user?.role === 'admin';
const isAdmin = user?.role === 'admin';
if (loading) {
if (loading) { return <div className="loading-screen">加载中...</div>;
return <div className="loading-screen">加载中...</div>; }
}
return (
return ( <AuthContext.Provider value={{ user, login, logout, isAdmin }}>
<AuthContext.Provider value={{ user, login, logout, isAdmin }}> <Routes>
<Routes> {/* Login */}
{/* Login */} <Route path="/login" element={
<Route path="/login" element={ user ? (isAdmin ? <Navigate to="/admin" /> : <Navigate to="/chat" />) : <LoginPage />
user ? (isAdmin ? <Navigate to="/admin" /> : <Navigate to="/chat" />) : <LoginPage /> } />
} />
{/* Root - Landing for guests, redirect for authenticated users */}
{/* Root - Landing for guests, redirect for authenticated users */} <Route path="/" element={
<Route path="/" element={ user ? (isAdmin ? <Navigate to="/admin" /> : <Navigate to="/chat" />) : <LandingPage />
user ? (isAdmin ? <Navigate to="/admin" /> : <Navigate to="/chat" />) : <LandingPage /> } />
} />
{/* Front Routes (authenticated only) */}
{/* Front Routes (authenticated only) */} <Route path="/" element={<FrontLayout />}>
<Route path="/" element={<FrontLayout />}> <Route path="chat" element={<ChatPage />} />
<Route path="chat" element={<ChatPage />} /> <Route path="chat/:id" element={<ChatPage />} />
<Route path="chat/:id" element={<ChatPage />} /> <Route path="channels" element={<ChannelPage />} />
<Route path="channels" element={<ChannelPage />} /> <Route path="knowledge" element={<KnowledgePage />} />
<Route path="knowledge" element={<KnowledgePage />} /> <Route path="market" element={<MarketPage />} />
<Route path="market" element={<MarketPage />} /> <Route path="token" element={<TokenDashboardPage />} />
<Route path="token" element={<TokenDashboardPage />} /> <Route path="pricing" element={<PricingPage />} />
<Route path="pricing" element={<PricingPage />} /> <Route path="profile" element={<ProfilePage />} />
<Route path="profile" element={<ProfilePage />} /> <Route path="notifications" element={<NotificationPage />} />
<Route path="notifications" element={<NotificationPage />} /> <Route path="assets" element={<AssetsPage />} />
<Route path="assets" element={<AssetsPage />} /> <Route path="assets/novel/:id" element={<NovelPreviewPage />} />
<Route path="assets/novel/:id" element={<NovelPreviewPage />} /> <Route path="evolution" element={<EvolutionDashboard />} />
<Route path="evolution" element={<EvolutionDashboard />} /> <Route path="wechat-bot" element={<WechatBot />} />
<Route path="wechat-bot" element={<WechatBot />} /> </Route>
<Route path="wechat-history" element={<WechatHistory />} />
</Route> {/* Admin Routes */}
<Route path="/admin" element={<AdminLayout />}>
{/* Admin Routes */} <Route index element={<AdminDashboard />} />
<Route path="/admin" element={<AdminLayout />}> <Route path="users" element={<AdminUsers />} />
<Route index element={<AdminDashboard />} /> <Route path="models" element={<AdminModels />} />
<Route path="users" element={<AdminUsers />} /> <Route path="settings" element={<AdminSiteSettings />} />
<Route path="models" element={<AdminModels />} /> <Route path="payment" element={<AdminPayment />} />
<Route path="settings" element={<AdminSiteSettings />} /> <Route path="services" element={<AdminServices />} />
<Route path="payment" element={<AdminPayment />} /> <Route path="membership" element={<AdminMembership />} />
<Route path="services" element={<AdminServices />} /> <Route path="ads" element={<AdminAds />} />
<Route path="membership" element={<AdminMembership />} /> <Route path="market" element={<AdminMarket />} />
<Route path="ads" element={<AdminAds />} /> <Route path="logs" element={<AdminLogs />} />
<Route path="market" element={<AdminMarket />} />
<Route path="logs" element={<AdminLogs />} /> <Route path="skills" element={<AdminSkills />} />
<Route path="agents" element={<AdminAgents />} />
<Route path="skills" element={<AdminSkills />} /> <Route path="code-exec" element={<AdminCodeExec />} />
<Route path="agents" element={<AdminAgents />} /> <Route path="distributions" element={<DistributionBuilder />} />
<Route path="code-exec" element={<AdminCodeExec />} /> <Route path="im-bots" element={<ImBots />} />
<Route path="distributions" element={<DistributionBuilder />} /> <Route path="privacy" element={<PrivacySettings />} />
<Route path="im-bots" element={<ImBots />} /> <Route path="promotion" element={<Promotion />} />
<Route path="privacy" element={<PrivacySettings />} /> <Route path="achievement" element={<Achievement />} />
<Route path="promotion" element={<Promotion />} /> <Route path="tenant" element={<Tenant />} />
<Route path="achievement" element={<Achievement />} /> <Route path="billing" element={<Billing />} />
<Route path="tenant" element={<Tenant />} /> <Route path="analytics" element={<Analytics />} />
<Route path="billing" element={<Billing />} /> <Route path="knowledge" element={<AdminKnowledge />} />
<Route path="analytics" element={<Analytics />} /> <Route path="workflow" element={<AdminWorkflow />} />
<Route path="knowledge" element={<AdminKnowledge />} /> <Route path="storage" element={<AdminStorage />} />
<Route path="workflow" element={<AdminWorkflow />} /> <Route path="developer" element={<AdminDeveloper />} />
<Route path="storage" element={<AdminStorage />} /> <Route path="developer-tools" element={<AdminDeveloperTools />} />
<Route path="developer" element={<AdminDeveloper />} /> <Route path="automation" element={<AdminAutomation />} />
<Route path="developer-tools" element={<AdminDeveloperTools />} /> <Route path="mcp" element={<AdminMcp />} />
<Route path="automation" element={<AdminAutomation />} /> <Route path="apps" element={<AdminApps />} />
<Route path="mcp" element={<AdminMcp />} /> <Route path="assets" element={<AssetCenter />} />
<Route path="apps" element={<AdminApps />} /> <Route path="portal" element={<UserPortal />} />
<Route path="assets" element={<AssetCenter />} /> </Route>
<Route path="portal" element={<UserPortal />} />
</Route> <Route path="*" element={<Navigate to="/" />} />
</Routes>
<Route path="*" element={<Navigate to="/" />} /> </AuthContext.Provider>
</Routes> );
</AuthContext.Provider> };
);
}; export default App;
export default App;
+183 -184
View File
@@ -1,184 +1,183 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Outlet, NavLink, useNavigate } from 'react-router-dom'; import { Outlet, NavLink, useNavigate } from 'react-router-dom';
import { useAuth } from '@/contexts/AuthContext'; import { useAuth } from '@/contexts/AuthContext';
import './FrontLayout.css'; import './FrontLayout.css';
const navGroups = [ const navGroups = [
{ {
title: '智能体', title: '智能体',
items: [ items: [
{ path: '/chat', icon: 'chat', label: 'AI聊天' }, { path: '/chat', icon: 'chat', label: 'AI聊天' },
{ path: '/knowledge', icon: 'knowledge', label: '知识库' }, { path: '/knowledge', icon: 'knowledge', label: '知识库' },
{ path: '/market', icon: 'market', label: '技能市场' }, { path: '/market', icon: 'market', label: '技能市场' },
{ path: '/evolution', icon: 'evolution', label: 'AI自进化' }, { path: '/evolution', icon: 'evolution', label: 'AI自进化' },
], ],
}, },
{ {
title: 'IM Bot', title: 'IM Bot',
items: [ items: [
{ path: '/channels', icon: 'channel', label: '频道管理' }, { path: '/channels', icon: 'channel', label: '频道管理' },
{ path: '/wechat-bot', icon: 'wechat', label: '微信机器人' }, { path: '/wechat-bot', icon: 'wechat', label: '微信机器人' },
{ path: '/wechat-history', icon: 'chat', label: '对话记录' }, ],
], },
}, {
{ title: '资源中心',
title: '资源中心', items: [
items: [ { path: '/assets', icon: 'assets', label: '资产文件' },
{ path: '/assets', icon: 'assets', label: '资产文件' }, { path: '/token', icon: 'token', label: 'Token驾驶舱' },
{ path: '/token', icon: 'token', label: 'Token驾驶舱' }, { path: '/pricing', icon: 'pricing', label: '付费中心' },
{ path: '/pricing', icon: 'pricing', label: '付费中心' }, ],
], },
}, ];
];
const bottomItems = [
const bottomItems = [ { path: '/profile', icon: 'profile', label: '个人中心' },
{ path: '/profile', icon: 'profile', label: '个人中心' }, { path: '/notifications', icon: 'notify', label: '消息通知' },
{ path: '/notifications', icon: 'notify', label: '消息通知' }, ];
];
const iconPaths: Record<string, JSX.Element> = {
const iconPaths: Record<string, JSX.Element> = { chat: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>,
chat: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>, channel: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>,
channel: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>, knowledge: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>,
knowledge: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>, market: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="7" height="7" rx="1.2"/><rect x="14" y="3" width="7" height="7" rx="1.2"/><rect x="3" y="14" width="7" height="7" rx="1.2"/><rect x="14" y="14" width="7" height="7" rx="1.2"/></svg>,
market: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="7" height="7" rx="1.2"/><rect x="14" y="3" width="7" height="7" rx="1.2"/><rect x="3" y="14" width="7" height="7" rx="1.2"/><rect x="14" y="14" width="7" height="7" rx="1.2"/></svg>, apps: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="2" width="9" height="9" rx="2"/><rect x="13" y="2" width="9" height="9" rx="2"/><rect x="2" y="13" width="9" height="9" rx="2"/><rect x="13" y="13" width="5" height="5" rx="1.5"/><path d="M18 18v3"/><path d="M19.5 16.5v6"/></svg>,
apps: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="2" width="9" height="9" rx="2"/><rect x="13" y="2" width="9" height="9" rx="2"/><rect x="2" y="13" width="9" height="9" rx="2"/><rect x="13" y="13" width="5" height="5" rx="1.5"/><path d="M18 18v3"/><path d="M19.5 16.5v6"/></svg>, token: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>,
token: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>, pricing: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><path d="M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8"/><path d="M12 18V6"/></svg>,
pricing: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><path d="M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8"/><path d="M12 18V6"/></svg>, profile: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>,
profile: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>, notify: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>,
notify: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>, assets: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M13 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V9z"/><polyline points="13 2 13 9 20 9"/></svg>,
assets: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M13 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V9z"/><polyline points="13 2 13 9 20 9"/></svg>, evolution: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>,
evolution: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>, wechat: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/><circle cx="9" cy="11" r="1"/><circle cx="15" cy="11" r="1"/></svg>,
wechat: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/><circle cx="9" cy="11" r="1"/><circle cx="15" cy="11" r="1"/></svg>, };
};
const FrontLayout: React.FC = () => {
const FrontLayout: React.FC = () => { const { user, logout } = useAuth();
const { user, logout } = useAuth(); const navigate = useNavigate();
const navigate = useNavigate(); const [collapsed, setCollapsed] = useState(false);
const [collapsed, setCollapsed] = useState(false);
if (!user) {
if (!user) { navigate('/login');
navigate('/login'); return null;
return null; }
}
return (
return ( <div className="front-layout">
<div className="front-layout"> {/* Sidebar */}
{/* Sidebar */} <aside className={`front-sidebar ${collapsed ? 'collapsed' : ''}`}>
<aside className={`front-sidebar ${collapsed ? 'collapsed' : ''}`}> {/* Header */}
{/* Header */} <div className="sidebar-header">
<div className="sidebar-header"> <div className="sidebar-logo">
<div className="sidebar-logo"> <svg width="32" height="32" viewBox="0 0 32 32" fill="none">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none"> <defs>
<defs> <linearGradient id="flGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<linearGradient id="flGrad" x1="0%" y1="0%" x2="100%" y2="100%"> <stop offset="0%" stopColor="#6C63FF"/>
<stop offset="0%" stopColor="#6C63FF"/> <stop offset="100%" stopColor="#A78BFA"/>
<stop offset="100%" stopColor="#A78BFA"/> </linearGradient>
</linearGradient> </defs>
</defs> <rect width="32" height="32" rx="8" fill="url(#flGrad)"/>
<rect width="32" height="32" rx="8" fill="url(#flGrad)"/> <path d="M8 20V12l4 4 4-4v8" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" fill="none"/>
<path d="M8 20V12l4 4 4-4v8" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" fill="none"/> <path d="M6 22h20" stroke="white" strokeWidth="1.5" strokeLinecap="round" opacity="0.5"/>
<path d="M6 22h20" stroke="white" strokeWidth="1.5" strokeLinecap="round" opacity="0.5"/> </svg>
</svg> {!collapsed && <span className="sidebar-brand">七虎AI</span>}
{!collapsed && <span className="sidebar-brand">七虎AI</span>} </div>
</div> <button className="collapse-btn" onClick={() => setCollapsed(!collapsed)}>
<button className="collapse-btn" onClick={() => setCollapsed(!collapsed)}> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> {collapsed ? <><polyline points="13 17 18 12 13 7"/><polyline points="6 17 11 12 6 7"/></> : <><polyline points="11 17 6 12 11 7"/><polyline points="18 17 13 12 18 7"/></>}
{collapsed ? <><polyline points="13 17 18 12 13 7"/><polyline points="6 17 11 12 6 7"/></> : <><polyline points="11 17 6 12 11 7"/><polyline points="18 17 13 12 18 7"/></>} </svg>
</svg> </button>
</button> </div>
</div>
{/* Navigation */}
{/* Navigation */} <nav className="sidebar-nav">
<nav className="sidebar-nav"> <NavLink to="/" className={({ isActive }) => `nav-item nav-home-link`} end>
<NavLink to="/" className={({ isActive }) => `nav-item nav-home-link`} end> <span className="nav-item-icon">
<span className="nav-item-icon"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg> </span>
</span> {!collapsed && <span className="nav-item-label">返回首页</span>}
{!collapsed && <span className="nav-item-label">返回首页</span>} </NavLink>
</NavLink> {navGroups.map(group => (
{navGroups.map(group => ( <div className="nav-section" key={group.title}>
<div className="nav-section" key={group.title}> {!collapsed && <div className="nav-group-title">{group.title}</div>}
{!collapsed && <div className="nav-group-title">{group.title}</div>} {group.items.map(item => (
{group.items.map(item => ( <NavLink
<NavLink key={item.path}
key={item.path} to={item.path}
to={item.path} className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`}
className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`} title={collapsed ? item.label : undefined}
title={collapsed ? item.label : undefined} >
> <span className="nav-icon">{iconPaths[item.icon]}</span>
<span className="nav-icon">{iconPaths[item.icon]}</span> {!collapsed && <span className="nav-label">{item.label}</span>}
{!collapsed && <span className="nav-label">{item.label}</span>} {!collapsed && <span className="nav-active-dot" />}
{!collapsed && <span className="nav-active-dot" />} </NavLink>
</NavLink> ))}
))} </div>
</div> ))}
))} <div className="nav-section nav-bottom">
<div className="nav-section nav-bottom"> {bottomItems.map(item => (
{bottomItems.map(item => ( <NavLink
<NavLink key={item.path}
key={item.path} to={item.path}
to={item.path} className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`}
className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`} title={collapsed ? item.label : undefined}
title={collapsed ? item.label : undefined} >
> <span className="nav-icon">{iconPaths[item.icon]}</span>
<span className="nav-icon">{iconPaths[item.icon]}</span> {!collapsed && <span className="nav-label">{item.label}</span>}
{!collapsed && <span className="nav-label">{item.label}</span>} {!collapsed && <span className="nav-active-dot" />}
{!collapsed && <span className="nav-active-dot" />} </NavLink>
</NavLink> ))}
))} </div>
</div> </nav>
</nav>
{/* Footer */}
{/* Footer */} <div className="sidebar-footer">
<div className="sidebar-footer"> <div className="user-info-mini">
<div className="user-info-mini"> <div className="user-avatar-sm">
<div className="user-avatar-sm"> {user.username.charAt(0).toUpperCase()}
{user.username.charAt(0).toUpperCase()} </div>
</div> {!collapsed && (
{!collapsed && ( <div className="user-details">
<div className="user-details"> <span className="user-name">{user.username}</span>
<span className="user-name">{user.username}</span> <span className="user-plan">{user.membership?.planName || '免费版'}</span>
<span className="user-plan">{user.membership?.planName || '免费版'}</span> </div>
</div> )}
)} </div>
</div> {user.role === 'admin' && (
{user.role === 'admin' && ( <NavLink
<NavLink to="/admin"
to="/admin" className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`}
className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`} title="管理后台"
title="管理后台" >
> <span className="nav-icon">
<span className="nav-icon"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"> <circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/> </svg>
</svg> </span>
</span> {!collapsed && <span className="nav-label">管理后台</span>}
{!collapsed && <span className="nav-label">管理后台</span>} </NavLink>
</NavLink> )}
)} <button
<button className="nav-item logout-btn"
className="nav-item logout-btn" onClick={() => { logout(); navigate('/login'); }}
onClick={() => { logout(); navigate('/login'); }} title="退出登录"
title="退出登录" >
> <span className="nav-icon">
<span className="nav-icon"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"> <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/> <polyline points="16 17 21 12 16 7"/>
<polyline points="16 17 21 12 16 7"/> <line x1="21" y1="12" x2="9" y2="12"/>
<line x1="21" y1="12" x2="9" y2="12"/> </svg>
</svg> </span>
</span> {!collapsed && <span className="nav-label">退出登录</span>}
{!collapsed && <span className="nav-label">退出登录</span>} </button>
</button> </div>
</div> </aside>
</aside>
{/* Main Content */}
{/* Main Content */} <main className="front-main">
<main className="front-main"> <Outlet />
<Outlet /> </main>
</main> </div>
</div> );
); };
};
export default FrontLayout;
export default FrontLayout;