refactor: 移除通用频道管理(LangBot)模块,IM Bot 改为直接对接;迁移 _call_real_llm 至 im_llm

This commit is contained in:
QihuAI Design committed 2026-07-14 08:21:56 +08:00
1 parent 945eeb8ba0
commit ab54d7344c
15 files changed
+3208 -8784

No files matched your search

-1
View File
@@ -1,6 +1,5 @@
from . import auth
from . import chat
from . import channels
from . import knowledge
from . import market
from . import token_usage
+1 -1
View File
@@ -608,7 +608,7 @@ def _qihua_reply(text: str) -> str:
自行创建 DB 会话,避免上层线程无可用 session 时崩溃。"""
db = None
try:
from app.api.channels import _call_real_llm
from app.api.im_llm import _call_real_llm
db = SessionLocal()
return _call_real_llm(db, None, text, "个人微信")
except Exception as e:
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+45
View File
@@ -0,0 +1,45 @@
"""
IM Bot 真实 LLM 调用。
原实现位于 channels.py(通用频道管理模块),该模块已移除。
为保持 IM Bot(微信机器人等)正常生成回复,将 _call_real_llm 迁移至此独立模块。
"""
from typing import Optional
import httpx
from sqlalchemy.orm import Session
from ..capability_registry import resolve_auto_model
from ..models import AIModel, ModelProvider
def _call_real_llm(db: Session, user=None, prompt: str = "", platform: str = "im") -> str:
"""调用七虎AI 默认文本模型生成回复,供 IM Bot 复用。"""
try:
ai_model = resolve_auto_model("text", db)
if not ai_model:
return "(未配置默认文本模型,无法生成回复)"
provider = db.query(ModelProvider).filter(ModelProvider.id == ai_model.provider_id).first()
api_key = ai_model.api_key or (provider.api_key if provider else None)
api_url = ai_model.endpoint
if not api_url and provider and provider.api_base_url:
api_url = f"{provider.api_base_url.rstrip('/')}/v1/chat/completions"
if not (api_key and api_url):
return "(模型未配置 API Key 或地址,无法生成回复)"
messages = [
{"role": "system", "content": "你是七虎AI 助手,负责在即时通讯中回复用户消息。"},
{"role": "user", "content": prompt},
]
request_body = {"model": ai_model.name, "messages": messages, "max_tokens": 2048}
with httpx.Client(timeout=ai_model.timeout or 120) as client:
resp = client.post(
api_url,
headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
json=request_body,
)
if resp.status_code != 200:
return f"(AI 模型调用失败 HTTP {resp.status_code})"
data = resp.json()
return data.get("choices", [{}])[0].get("message", {}).get("content", "")
except Exception as e:
return f"(AI 回复生成失败:{e})"
+244 -245
View File
@@ -1,245 +1,244 @@
"""
Qihu-AIFlow FastAPI 主应用
完整后端API,监听18080端口,所有接口以 /app 为前缀
"""
import sys
import os
import time
import uuid
import logging
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from starlette.middleware.base import BaseHTTPMiddleware
from datetime import datetime
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from app.api import auth, chat, channels, knowledge, market, token_usage, payment, profile, notifications, assets, evolution
from app.casdoor_auth import router as casdoor_router
from app.api.admin import dashboard, users, models, settings, payment_admin, services, membership_admin, ads, market_admin, logs, skills_admin, agents_admin, knowledge_admin, workflow_admin, storage_admin, automation_admin, mcp_admin, developer_admin, apps_admin, distro_admin, code_exec_admin, privacy_admin, im_bot_admin, promotion_admin, achievement_admin, tenant_admin, billing_admin, analytics_admin
from app.api.im_bot_user import router as im_bot_user_router
from app.api.portal import user_portal
from app.api.admin.developer_tools_admin import router as developer_tools_router
from app.api.admin.mcp_market_admin import router as mcp_market_router
from app.api.admin import asset_admin, achievement_admin
from app.config import setup_file_logging, SLOW_REQUEST_THRESHOLD
from app.database import SessionLocal
from app.models import OperationLog as OperationLogModel, TokenUsageLog as TokenUsageLogModel
# 导入平台适配器触发注册(AstrBot借鉴优化方案 P1)
import app.platform_adapters # noqa: F401 - 触发所有平台适配器装饰器注册
# 初始化文件日志(按天轮转)
access_logger = setup_file_logging()
logger = logging.getLogger("aiflow")
class APITrackingMiddleware(BaseHTTPMiddleware):
"""API调用统计中间件 - 记录请求耗时,标记慢请求"""
async def dispatch(self, request: Request, call_next):
request_id = str(uuid.uuid4())
start_time = time.time()
path = request.url.path
method = request.method
# 跳过健康检查和静态资源
skip_paths = ["/app/api/health", "/favicon.ico"]
should_skip = any(path.startswith(p) for p in skip_paths)
try:
response = await call_next(request)
elapsed = time.time() - start_time
status_code = response.status_code
if not should_skip:
access_logger.info(
f"[{request_id}] {method} {path} -> {status_code} ({elapsed:.3f}s)"
)
# 异步记录API调用统计
try:
db = SessionLocal()
try:
_record_api_call(db, method, path, status_code, elapsed)
finally:
db.close()
except Exception as e:
logger.warning(f"API统计记录失败: {e}")
return response
except Exception as e:
elapsed = time.time() - start_time
if not should_skip:
access_logger.info(
f"[{request_id}] {method} {path} -> ERROR ({elapsed:.3f}s): {str(e)}"
)
raise
async def __call__(self, scope, receive, send):
if scope["type"] != "http":
await self.app(scope, receive, send)
return
await super().__call__(scope, receive, send)
def _record_api_call(db, method: str, path: str, status_code: int, elapsed: float):
"""记录API调用到数据库统计表"""
# 简化路径以减少基数(去掉动态ID)
parts = path.split("/")
simplified_parts = []
for part in parts:
# 将UUID样式的路径段替换为{id}
if len(part) == 36 and part.count("-") == 4:
simplified_parts.append("{id}")
# 将纯数字路径段替换为{id}
elif part.isdigit():
simplified_parts.append("{id}")
else:
simplified_parts.append(part)
simplified_path = "/".join(simplified_parts)
# 统计记录通过数据库实现,简化版记录到日志
is_error = status_code >= 400
is_slow = elapsed >= SLOW_REQUEST_THRESHOLD
if is_slow:
logger.warning(
f"慢请求: {method} {simplified_path} -> {status_code} ({elapsed:.3f}s, 阈值: {SLOW_REQUEST_THRESHOLD}s)"
)
@asynccontextmanager
async def lifespan(app: FastAPI):
logger.info("Qihu-AIFlow v2.0.0 启动中...")
# 启动后台 Evolution Pipeline(知识库图谱已存在的用户配置自动恢复运行)
try:
from app.services.evolution_service import EvolutionWatcherManager
from app.models import UserEvolutionConfig
db = SessionLocal()
try:
configs = db.query(UserEvolutionConfig).filter(
UserEvolutionConfig.watcher_status == "running"
).all()
if configs:
watcher = EvolutionWatcherManager()
for cfg in configs:
try:
watcher.start_user_evolution(cfg.user_id, lambda: SessionLocal())
logger.info(f"[Evolution] 恢复用户 {cfg.user_id} 的进化Pipeline")
except Exception as e:
logger.warning(f"[Evolution] 恢复用户 {cfg.user_id} 失败: {e}")
logger.info(f"[Evolution] 已恢复 {len(configs)} 个用户的进化Pipeline")
else:
logger.info("[Evolution] 无待恢复的进化配置")
finally:
db.close()
except Exception as e:
logger.warning(f"[Evolution] Pipeline自动启动异常(不影响正常服务): {e}")
# 启动Redis事件总线
try:
from app.event_bus import get_event_bus
event_bus = get_event_bus()
event_bus.start(num_consumers=2)
logger.info("[EventBus] Redis事件总线已启动")
except Exception as e:
logger.warning(f"[EventBus] 启动失败(不影响正常服务): {e}")
logger.info("Qihu-AIFlow v2.0.0 启动完成")
yield
# 停止Redis事件总线
try:
from app.event_bus import get_event_bus
get_event_bus().stop()
logger.info("[EventBus] Redis事件总线已停止")
except Exception:
pass
# 优雅关闭:停止所有 Evolution Pipeline
try:
from app.services.evolution_service import EvolutionWatcherManager
EvolutionWatcherManager().shutdown_all()
logger.info("[Evolution] 所有Pipeline已停止")
except Exception:
pass
logger.info("Qihu-AIFlow 正在关闭...")
app = FastAPI(
title="Qihu-AIFlow - 智能自动化PaaS平台",
version="1.0.0",
lifespan=lifespan,
)
# API调用追踪中间件
app.add_middleware(APITrackingMiddleware)
# CORS配置
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 前台 API
app.include_router(auth.router, prefix="/app/api/auth", tags=["认证"])
app.include_router(chat.router, prefix="/app/api/chat", tags=["AI聊天"])
app.include_router(channels.router, prefix="/app/api/channels", tags=["频道"])
app.include_router(knowledge.router, prefix="/app/api/knowledge", tags=["知识库"])
app.include_router(market.router, prefix="/app/api/market", tags=["应用市场"])
app.include_router(token_usage.router, prefix="/app/api/token", tags=["Token驾驶舱"])
app.include_router(payment.router, prefix="/app/api/payment", tags=["付费中心"])
app.include_router(profile.router, prefix="/app/api/profile", tags=["个人中心"])
app.include_router(notifications.router, prefix="/app/api/notifications", tags=["消息通知"])
app.include_router(assets.router, prefix="/app/api/assets", tags=["资产管理"])
app.include_router(evolution.router, prefix="/app/api/evolution", tags=["AI自进化"])
# 后台 API
app.include_router(dashboard.router, prefix="/app/api/admin/dashboard", tags=["后台-仪表盘"])
app.include_router(users.router, prefix="/app/api/admin/users", tags=["后台-用户管理"])
app.include_router(models.router, prefix="/app/api/admin/models", tags=["后台-模型管理"])
app.include_router(settings.router, prefix="/app/api/admin/settings", tags=["后台-网站设置"])
app.include_router(payment_admin.router, prefix="/app/api/admin/payment", tags=["后台-支付配置"])
app.include_router(services.router, prefix="/app/api/admin/services", tags=["后台-增值服务"])
app.include_router(membership_admin.router, prefix="/app/api/admin/membership", tags=["后台-会员管理"])
app.include_router(ads.router, prefix="/app/api/admin/ads", tags=["后台-广告管理"])
app.include_router(market_admin.router, prefix="/app/api/admin/market", tags=["后台-应用审核"])
app.include_router(logs.router, prefix="/app/api/admin/logs", tags=["后台-日志风控"])
app.include_router(skills_admin.router, prefix="/app/api/admin/skills", tags=["后台-技能管理"])
app.include_router(agents_admin.router, prefix="/app/api/admin/agents", tags=["后台-Agent管理"])
app.include_router(knowledge_admin.router, prefix="/app/api/admin/knowledge", tags=["后台-知识库管理"])
app.include_router(workflow_admin.router, prefix="/app/api/admin/workflows", tags=["后台-工作流编排"])
app.include_router(asset_admin.router, prefix="/app/api/admin/assets", tags=["后台-资产管理"])
app.include_router(storage_admin.router, prefix="/app/api/admin/storage", tags=["后台-存储配置"])
app.include_router(automation_admin.router, prefix="/app/api/admin/automation", tags=["后台-自动化任务"])
app.include_router(mcp_admin.router, prefix="/app/api/admin/mcp", tags=["后台-MCP服务"])
app.include_router(developer_admin.router, prefix="/app/api/admin/developer", tags=["后台-开发者API"])
app.include_router(developer_tools_router, prefix="/app/api/admin/developer-tools", tags=["dev-tools"])
app.include_router(apps_admin.router, prefix="/app/api/admin/apps", tags=["admin-ai-apps"])
app.include_router(mcp_market_router, prefix="/app/api/admin/mcp-market", tags=["mcp-market"])
app.include_router(code_exec_admin.router, prefix="/app/api/admin/code-exec", tags=["code-exec"])
app.include_router(distro_admin.router, prefix="/app/api/admin/distributions", tags=["distributions"])
app.include_router(privacy_admin.router, prefix="/app/api/admin/privacy", tags=["privacy"])
app.include_router(im_bot_admin.router, prefix="/app/api/admin/im-bot", tags=["im-bot"])
app.include_router(im_bot_user_router, prefix="/app/api/im/wechat", tags=["im-wechat-user"])
app.include_router(promotion_admin.router, prefix="/app/api/admin/promotion", tags=["promotion"])
app.include_router(tenant_admin.router, prefix="/app/api/admin/tenant", tags=["tenant"])
app.include_router(billing_admin.router, prefix="/app/api/admin/billing", tags=["billing"])
app.include_router(analytics_admin.router, prefix="/app/api/admin/analytics", tags=["analytics"])
app.include_router(user_portal.router, prefix="/app/api/portal", tags=["portal"])
app.include_router(achievement_admin.router, prefix="/app/api/admin/achievement", tags=["achievement"])
app.include_router(casdoor_router, prefix="/app/api/auth", tags=["Casdoor SSO"])
@app.get("/app/api/health")
async def health_check():
return {"status": "ok", "service": "Qihu-AIFlow"}
"""
Qihu-AIFlow FastAPI 主应用
完整后端API,监听18080端口,所有接口以 /app 为前缀
"""
import sys
import os
import time
import uuid
import logging
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from starlette.middleware.base import BaseHTTPMiddleware
from datetime import datetime
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from app.api import auth, chat, knowledge, market, token_usage, payment, profile, notifications, assets, evolution
from app.casdoor_auth import router as casdoor_router
from app.api.admin import dashboard, users, models, settings, payment_admin, services, membership_admin, ads, market_admin, logs, skills_admin, agents_admin, knowledge_admin, workflow_admin, storage_admin, automation_admin, mcp_admin, developer_admin, apps_admin, distro_admin, code_exec_admin, privacy_admin, im_bot_admin, promotion_admin, achievement_admin, tenant_admin, billing_admin, analytics_admin
from app.api.im_bot_user import router as im_bot_user_router
from app.api.portal import user_portal
from app.api.admin.developer_tools_admin import router as developer_tools_router
from app.api.admin.mcp_market_admin import router as mcp_market_router
from app.api.admin import asset_admin, achievement_admin
from app.config import setup_file_logging, SLOW_REQUEST_THRESHOLD
from app.database import SessionLocal
from app.models import OperationLog as OperationLogModel, TokenUsageLog as TokenUsageLogModel
# 导入平台适配器触发注册(AstrBot借鉴优化方案 P1)
import app.platform_adapters # noqa: F401 - 触发所有平台适配器装饰器注册
# 初始化文件日志(按天轮转)
access_logger = setup_file_logging()
logger = logging.getLogger("aiflow")
class APITrackingMiddleware(BaseHTTPMiddleware):
"""API调用统计中间件 - 记录请求耗时,标记慢请求"""
async def dispatch(self, request: Request, call_next):
request_id = str(uuid.uuid4())
start_time = time.time()
path = request.url.path
method = request.method
# 跳过健康检查和静态资源
skip_paths = ["/app/api/health", "/favicon.ico"]
should_skip = any(path.startswith(p) for p in skip_paths)
try:
response = await call_next(request)
elapsed = time.time() - start_time
status_code = response.status_code
if not should_skip:
access_logger.info(
f"[{request_id}] {method} {path} -> {status_code} ({elapsed:.3f}s)"
)
# 异步记录API调用统计
try:
db = SessionLocal()
try:
_record_api_call(db, method, path, status_code, elapsed)
finally:
db.close()
except Exception as e:
logger.warning(f"API统计记录失败: {e}")
return response
except Exception as e:
elapsed = time.time() - start_time
if not should_skip:
access_logger.info(
f"[{request_id}] {method} {path} -> ERROR ({elapsed:.3f}s): {str(e)}"
)
raise
async def __call__(self, scope, receive, send):
if scope["type"] != "http":
await self.app(scope, receive, send)
return
await super().__call__(scope, receive, send)
def _record_api_call(db, method: str, path: str, status_code: int, elapsed: float):
"""记录API调用到数据库统计表"""
# 简化路径以减少基数(去掉动态ID)
parts = path.split("/")
simplified_parts = []
for part in parts:
# 将UUID样式的路径段替换为{id}
if len(part) == 36 and part.count("-") == 4:
simplified_parts.append("{id}")
# 将纯数字路径段替换为{id}
elif part.isdigit():
simplified_parts.append("{id}")
else:
simplified_parts.append(part)
simplified_path = "/".join(simplified_parts)
# 统计记录通过数据库实现,简化版记录到日志
is_error = status_code >= 400
is_slow = elapsed >= SLOW_REQUEST_THRESHOLD
if is_slow:
logger.warning(
f"慢请求: {method} {simplified_path} -> {status_code} ({elapsed:.3f}s, 阈值: {SLOW_REQUEST_THRESHOLD}s)"
)
@asynccontextmanager
async def lifespan(app: FastAPI):
logger.info("Qihu-AIFlow v2.0.0 启动中...")
# 启动后台 Evolution Pipeline(知识库图谱已存在的用户配置自动恢复运行)
try:
from app.services.evolution_service import EvolutionWatcherManager
from app.models import UserEvolutionConfig
db = SessionLocal()
try:
configs = db.query(UserEvolutionConfig).filter(
UserEvolutionConfig.watcher_status == "running"
).all()
if configs:
watcher = EvolutionWatcherManager()
for cfg in configs:
try:
watcher.start_user_evolution(cfg.user_id, lambda: SessionLocal())
logger.info(f"[Evolution] 恢复用户 {cfg.user_id} 的进化Pipeline")
except Exception as e:
logger.warning(f"[Evolution] 恢复用户 {cfg.user_id} 失败: {e}")
logger.info(f"[Evolution] 已恢复 {len(configs)} 个用户的进化Pipeline")
else:
logger.info("[Evolution] 无待恢复的进化配置")
finally:
db.close()
except Exception as e:
logger.warning(f"[Evolution] Pipeline自动启动异常(不影响正常服务): {e}")
# 启动Redis事件总线
try:
from app.event_bus import get_event_bus
event_bus = get_event_bus()
event_bus.start(num_consumers=2)
logger.info("[EventBus] Redis事件总线已启动")
except Exception as e:
logger.warning(f"[EventBus] 启动失败(不影响正常服务): {e}")
logger.info("Qihu-AIFlow v2.0.0 启动完成")
yield
# 停止Redis事件总线
try:
from app.event_bus import get_event_bus
get_event_bus().stop()
logger.info("[EventBus] Redis事件总线已停止")
except Exception:
pass
# 优雅关闭:停止所有 Evolution Pipeline
try:
from app.services.evolution_service import EvolutionWatcherManager
EvolutionWatcherManager().shutdown_all()
logger.info("[Evolution] 所有Pipeline已停止")
except Exception:
pass
logger.info("Qihu-AIFlow 正在关闭...")
app = FastAPI(
title="Qihu-AIFlow - 智能自动化PaaS平台",
version="1.0.0",
lifespan=lifespan,
)
# API调用追踪中间件
app.add_middleware(APITrackingMiddleware)
# CORS配置
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 前台 API
app.include_router(auth.router, prefix="/app/api/auth", tags=["认证"])
app.include_router(chat.router, prefix="/app/api/chat", tags=["AI聊天"])
app.include_router(knowledge.router, prefix="/app/api/knowledge", tags=["知识库"])
app.include_router(market.router, prefix="/app/api/market", tags=["应用市场"])
app.include_router(token_usage.router, prefix="/app/api/token", tags=["Token驾驶舱"])
app.include_router(payment.router, prefix="/app/api/payment", tags=["付费中心"])
app.include_router(profile.router, prefix="/app/api/profile", tags=["个人中心"])
app.include_router(notifications.router, prefix="/app/api/notifications", tags=["消息通知"])
app.include_router(assets.router, prefix="/app/api/assets", tags=["资产管理"])
app.include_router(evolution.router, prefix="/app/api/evolution", tags=["AI自进化"])
# 后台 API
app.include_router(dashboard.router, prefix="/app/api/admin/dashboard", tags=["后台-仪表盘"])
app.include_router(users.router, prefix="/app/api/admin/users", tags=["后台-用户管理"])
app.include_router(models.router, prefix="/app/api/admin/models", tags=["后台-模型管理"])
app.include_router(settings.router, prefix="/app/api/admin/settings", tags=["后台-网站设置"])
app.include_router(payment_admin.router, prefix="/app/api/admin/payment", tags=["后台-支付配置"])
app.include_router(services.router, prefix="/app/api/admin/services", tags=["后台-增值服务"])
app.include_router(membership_admin.router, prefix="/app/api/admin/membership", tags=["后台-会员管理"])
app.include_router(ads.router, prefix="/app/api/admin/ads", tags=["后台-广告管理"])
app.include_router(market_admin.router, prefix="/app/api/admin/market", tags=["后台-应用审核"])
app.include_router(logs.router, prefix="/app/api/admin/logs", tags=["后台-日志风控"])
app.include_router(skills_admin.router, prefix="/app/api/admin/skills", tags=["后台-技能管理"])
app.include_router(agents_admin.router, prefix="/app/api/admin/agents", tags=["后台-Agent管理"])
app.include_router(knowledge_admin.router, prefix="/app/api/admin/knowledge", tags=["后台-知识库管理"])
app.include_router(workflow_admin.router, prefix="/app/api/admin/workflows", tags=["后台-工作流编排"])
app.include_router(asset_admin.router, prefix="/app/api/admin/assets", tags=["后台-资产管理"])
app.include_router(storage_admin.router, prefix="/app/api/admin/storage", tags=["后台-存储配置"])
app.include_router(automation_admin.router, prefix="/app/api/admin/automation", tags=["后台-自动化任务"])
app.include_router(mcp_admin.router, prefix="/app/api/admin/mcp", tags=["后台-MCP服务"])
app.include_router(developer_admin.router, prefix="/app/api/admin/developer", tags=["后台-开发者API"])
app.include_router(developer_tools_router, prefix="/app/api/admin/developer-tools", tags=["dev-tools"])
app.include_router(apps_admin.router, prefix="/app/api/admin/apps", tags=["admin-ai-apps"])
app.include_router(mcp_market_router, prefix="/app/api/admin/mcp-market", tags=["mcp-market"])
app.include_router(code_exec_admin.router, prefix="/app/api/admin/code-exec", tags=["code-exec"])
app.include_router(distro_admin.router, prefix="/app/api/admin/distributions", tags=["distributions"])
app.include_router(privacy_admin.router, prefix="/app/api/admin/privacy", tags=["privacy"])
app.include_router(im_bot_admin.router, prefix="/app/api/admin/im-bot", tags=["im-bot"])
app.include_router(im_bot_user_router, prefix="/app/api/im/wechat", tags=["im-wechat-user"])
app.include_router(promotion_admin.router, prefix="/app/api/admin/promotion", tags=["promotion"])
app.include_router(tenant_admin.router, prefix="/app/api/admin/tenant", tags=["tenant"])
app.include_router(billing_admin.router, prefix="/app/api/admin/billing", tags=["billing"])
app.include_router(analytics_admin.router, prefix="/app/api/admin/analytics", tags=["analytics"])
app.include_router(user_portal.router, prefix="/app/api/portal", tags=["portal"])
app.include_router(achievement_admin.router, prefix="/app/api/admin/achievement", tags=["achievement"])
app.include_router(casdoor_router, prefix="/app/api/auth", tags=["Casdoor SSO"])
@app.get("/app/api/health")
async def health_check():
return {"status": "ok", "service": "Qihu-AIFlow"}
+171 -173
View File
@@ -1,173 +1,171 @@
import React, { useState, useEffect, Suspense } from 'react';
import { Routes, Route, Navigate } from 'react-router-dom';
import type { User } from '@/types';
import { AuthContext, AuthContextType } from '@/contexts/AuthContext';
import { authAPI } from '@/api';
// Layouts(保留为即时加载,构成应用外壳)
import FrontLayout from '@/components/layout/FrontLayout';
import AdminLayout from '@/components/layout/AdminLayout';
// 路由级代码分割:每个页面独立打包成 chunk,首屏只加载外壳与公共依赖,
// 访问具体页面时才按需加载对应 chunk,显著加快首屏渲染与路由切换速度。
const LandingPage = React.lazy(() => import('@/pages/front/LandingPage'));
const LoginPage = React.lazy(() => import('@/pages/front/LoginPage'));
const ChatPage = React.lazy(() => import('@/pages/front/ChatPage'));
const ChannelPage = React.lazy(() => import('@/pages/front/ChannelPage'));
const KnowledgePage = React.lazy(() => import('@/pages/front/KnowledgePage'));
const MarketPage = React.lazy(() => import('@/pages/front/MarketPage'));
const TokenDashboardPage = React.lazy(() => import('@/pages/front/TokenDashboardPage'));
const PricingPage = React.lazy(() => import('@/pages/front/PricingPage'));
const ProfilePage = React.lazy(() => import('@/pages/front/ProfilePage'));
const NotificationPage = React.lazy(() => import('@/pages/front/NotificationPage'));
const AssetsPage = React.lazy(() => import('@/pages/front/AssetsPage'));
const NovelPreviewPage = React.lazy(() => import('@/pages/front/NovelPreviewPage'));
const EvolutionDashboard = React.lazy(() => import('@/pages/front/EvolutionDashboard'));
const AdminDashboard = React.lazy(() => import('@/pages/admin/Dashboard'));
const AdminUsers = React.lazy(() => import('@/pages/admin/Users'));
const AdminModels = React.lazy(() => import('@/pages/admin/Models'));
const AdminSiteSettings = React.lazy(() => import('@/pages/admin/SiteSettings'));
const AdminSmsConfig = React.lazy(() => import('@/pages/admin/SmsConfig'));
const AdminPayment = React.lazy(() => import('@/pages/admin/Payment'));
const AdminServices = React.lazy(() => import('@/pages/admin/Services'));
const AdminMembership = React.lazy(() => import('@/pages/admin/Membership'));
const AdminAds = React.lazy(() => import('@/pages/admin/Ads'));
const AdminMarket = React.lazy(() => import('@/pages/admin/Market'));
const AdminLogs = React.lazy(() => import('@/pages/admin/Logs'));
const AdminSkills = React.lazy(() => import('@/pages/admin/Skills'));
const AdminAgents = React.lazy(() => import('@/pages/admin/Agents'));
const AdminCodeExec = React.lazy(() => import('@/pages/admin/CodeExec'));
const DistributionBuilder = React.lazy(() => import('@/pages/admin/DistributionBuilder'));
const ImBots = React.lazy(() => import('@/pages/admin/ImBots'));
const PrivacySettings = React.lazy(() => import('@/pages/admin/PrivacySettings'));
const Promotion = React.lazy(() => import('@/pages/admin/Promotion'));
const Achievement = React.lazy(() => import('@/pages/admin/Achievement'));
const Tenant = React.lazy(() => import('@/pages/admin/Tenant'));
const Billing = React.lazy(() => import('@/pages/admin/Billing'));
const Analytics = React.lazy(() => import('@/pages/admin/Analytics'));
const AdminKnowledge = React.lazy(() => import('@/pages/admin/Knowledge'));
const AdminWorkflow = React.lazy(() => import('@/pages/admin/Workflow'));
const AdminStorage = React.lazy(() => import('@/pages/admin/Storage'));
const AdminDeveloper = React.lazy(() => import('@/pages/admin/Developer'));
const AdminDeveloperTools = React.lazy(() => import('@/pages/admin/DeveloperTools'));
const AdminAutomation = React.lazy(() => import('@/pages/admin/Automation'));
const AdminMcp = React.lazy(() => import('@/pages/admin/Mcp'));
const AdminApps = React.lazy(() => import('@/pages/admin/Apps'));
const AssetCenter = React.lazy(() => import('@/pages/admin/AssetCenter'));
const UserPortal = React.lazy(() => import('@/pages/UserPortal'));
const WechatBot = React.lazy(() => import('@/pages/front/WechatBot'));
const pageFallback = <div className="loading-screen">加载中...</div>;
const App: React.FC = () => {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
// 初始化:从localStorage恢复登录状态
useEffect(() => {
const token = localStorage.getItem('aiflow_token');
const cachedUser = localStorage.getItem('aiflow_user');
if (token && cachedUser) {
try {
setUser(JSON.parse(cachedUser));
} catch {
localStorage.removeItem('aiflow_token');
localStorage.removeItem('aiflow_user');
}
}
setLoading(false);
}, []);
const login = (u: User) => {
setUser(u);
localStorage.setItem('aiflow_user', JSON.stringify(u));
};
const logout = () => {
setUser(null);
localStorage.removeItem('aiflow_token');
localStorage.removeItem('aiflow_user');
};
const isAdmin = user?.role === 'admin';
if (loading) {
return <div className="loading-screen">加载中...</div>;
}
return (
<AuthContext.Provider value={{ user, login, logout, isAdmin }}>
<Suspense fallback={pageFallback}>
<Routes>
{/* Login */}
<Route path="/login" element={
user ? (isAdmin ? <Navigate to="/admin" /> : <Navigate to="/chat" />) : <LoginPage />
} />
{/* Root - Landing for guests, redirect for authenticated users */}
<Route path="/" element={
user ? (isAdmin ? <Navigate to="/admin" /> : <Navigate to="/chat" />) : <LandingPage />
} />
{/* Front Routes (authenticated only) */}
<Route path="/" element={<FrontLayout />}>
<Route path="chat" element={<ChatPage />} />
<Route path="chat/:id" element={<ChatPage />} />
<Route path="channels" element={<ChannelPage />} />
<Route path="knowledge" element={<KnowledgePage />} />
<Route path="market" element={<MarketPage />} />
<Route path="token" element={<TokenDashboardPage />} />
<Route path="pricing" element={<PricingPage />} />
<Route path="profile" element={<ProfilePage />} />
<Route path="notifications" element={<NotificationPage />} />
<Route path="assets" element={<AssetsPage />} />
<Route path="assets/novel/:id" element={<NovelPreviewPage />} />
<Route path="evolution" element={<EvolutionDashboard />} />
<Route path="wechat-bot" element={<WechatBot />} />
</Route>
{/* Admin Routes */}
<Route path="/admin" element={<AdminLayout />}>
<Route index element={<AdminDashboard />} />
<Route path="users" element={<AdminUsers />} />
<Route path="models" element={<AdminModels />} />
<Route path="settings" element={<AdminSiteSettings />} />
<Route path="sms" element={<AdminSmsConfig />} />
<Route path="payment" element={<AdminPayment />} />
<Route path="services" element={<AdminServices />} />
<Route path="membership" element={<AdminMembership />} />
<Route path="ads" element={<AdminAds />} />
<Route path="market" element={<AdminMarket />} />
<Route path="logs" element={<AdminLogs />} />
<Route path="skills" element={<AdminSkills />} />
<Route path="agents" element={<AdminAgents />} />
<Route path="code-exec" element={<AdminCodeExec />} />
<Route path="distributions" element={<DistributionBuilder />} />
<Route path="im-bots" element={<ImBots />} />
<Route path="privacy" element={<PrivacySettings />} />
<Route path="promotion" element={<Promotion />} />
<Route path="achievement" element={<Achievement />} />
<Route path="tenant" element={<Tenant />} />
<Route path="billing" element={<Billing />} />
<Route path="analytics" element={<Analytics />} />
<Route path="knowledge" element={<AdminKnowledge />} />
<Route path="workflow" element={<AdminWorkflow />} />
<Route path="storage" element={<AdminStorage />} />
<Route path="developer" element={<AdminDeveloper />} />
<Route path="developer-tools" element={<AdminDeveloperTools />} />
<Route path="automation" element={<AdminAutomation />} />
<Route path="mcp" element={<AdminMcp />} />
<Route path="apps" element={<AdminApps />} />
<Route path="assets" element={<AssetCenter />} />
<Route path="portal" element={<UserPortal />} />
</Route>
{/* 公开前台首页:不受登录态拦截,供后台「访问前台」直接打开 */}
<Route path="/home" element={<LandingPage />} />
<Route path="*" element={<Navigate to="/" />} />
</Routes>
</Suspense>
</AuthContext.Provider>
);
};
export default App;
import React, { useState, useEffect, Suspense } from 'react';
import { Routes, Route, Navigate } from 'react-router-dom';
import type { User } from '@/types';
import { AuthContext, AuthContextType } from '@/contexts/AuthContext';
import { authAPI } from '@/api';
// Layouts(保留为即时加载,构成应用外壳)
import FrontLayout from '@/components/layout/FrontLayout';
import AdminLayout from '@/components/layout/AdminLayout';
// 路由级代码分割:每个页面独立打包成 chunk,首屏只加载外壳与公共依赖,
// 访问具体页面时才按需加载对应 chunk,显著加快首屏渲染与路由切换速度。
const LandingPage = React.lazy(() => import('@/pages/front/LandingPage'));
const LoginPage = React.lazy(() => import('@/pages/front/LoginPage'));
const ChatPage = React.lazy(() => import('@/pages/front/ChatPage'));
const KnowledgePage = React.lazy(() => import('@/pages/front/KnowledgePage'));
const MarketPage = React.lazy(() => import('@/pages/front/MarketPage'));
const TokenDashboardPage = React.lazy(() => import('@/pages/front/TokenDashboardPage'));
const PricingPage = React.lazy(() => import('@/pages/front/PricingPage'));
const ProfilePage = React.lazy(() => import('@/pages/front/ProfilePage'));
const NotificationPage = React.lazy(() => import('@/pages/front/NotificationPage'));
const AssetsPage = React.lazy(() => import('@/pages/front/AssetsPage'));
const NovelPreviewPage = React.lazy(() => import('@/pages/front/NovelPreviewPage'));
const EvolutionDashboard = React.lazy(() => import('@/pages/front/EvolutionDashboard'));
const AdminDashboard = React.lazy(() => import('@/pages/admin/Dashboard'));
const AdminUsers = React.lazy(() => import('@/pages/admin/Users'));
const AdminModels = React.lazy(() => import('@/pages/admin/Models'));
const AdminSiteSettings = React.lazy(() => import('@/pages/admin/SiteSettings'));
const AdminSmsConfig = React.lazy(() => import('@/pages/admin/SmsConfig'));
const AdminPayment = React.lazy(() => import('@/pages/admin/Payment'));
const AdminServices = React.lazy(() => import('@/pages/admin/Services'));
const AdminMembership = React.lazy(() => import('@/pages/admin/Membership'));
const AdminAds = React.lazy(() => import('@/pages/admin/Ads'));
const AdminMarket = React.lazy(() => import('@/pages/admin/Market'));
const AdminLogs = React.lazy(() => import('@/pages/admin/Logs'));
const AdminSkills = React.lazy(() => import('@/pages/admin/Skills'));
const AdminAgents = React.lazy(() => import('@/pages/admin/Agents'));
const AdminCodeExec = React.lazy(() => import('@/pages/admin/CodeExec'));
const DistributionBuilder = React.lazy(() => import('@/pages/admin/DistributionBuilder'));
const ImBots = React.lazy(() => import('@/pages/admin/ImBots'));
const PrivacySettings = React.lazy(() => import('@/pages/admin/PrivacySettings'));
const Promotion = React.lazy(() => import('@/pages/admin/Promotion'));
const Achievement = React.lazy(() => import('@/pages/admin/Achievement'));
const Tenant = React.lazy(() => import('@/pages/admin/Tenant'));
const Billing = React.lazy(() => import('@/pages/admin/Billing'));
const Analytics = React.lazy(() => import('@/pages/admin/Analytics'));
const AdminKnowledge = React.lazy(() => import('@/pages/admin/Knowledge'));
const AdminWorkflow = React.lazy(() => import('@/pages/admin/Workflow'));
const AdminStorage = React.lazy(() => import('@/pages/admin/Storage'));
const AdminDeveloper = React.lazy(() => import('@/pages/admin/Developer'));
const AdminDeveloperTools = React.lazy(() => import('@/pages/admin/DeveloperTools'));
const AdminAutomation = React.lazy(() => import('@/pages/admin/Automation'));
const AdminMcp = React.lazy(() => import('@/pages/admin/Mcp'));
const AdminApps = React.lazy(() => import('@/pages/admin/Apps'));
const AssetCenter = React.lazy(() => import('@/pages/admin/AssetCenter'));
const UserPortal = React.lazy(() => import('@/pages/UserPortal'));
const WechatBot = React.lazy(() => import('@/pages/front/WechatBot'));
const pageFallback = <div className="loading-screen">加载中...</div>;
const App: React.FC = () => {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
// 初始化:从localStorage恢复登录状态
useEffect(() => {
const token = localStorage.getItem('aiflow_token');
const cachedUser = localStorage.getItem('aiflow_user');
if (token && cachedUser) {
try {
setUser(JSON.parse(cachedUser));
} catch {
localStorage.removeItem('aiflow_token');
localStorage.removeItem('aiflow_user');
}
}
setLoading(false);
}, []);
const login = (u: User) => {
setUser(u);
localStorage.setItem('aiflow_user', JSON.stringify(u));
};
const logout = () => {
setUser(null);
localStorage.removeItem('aiflow_token');
localStorage.removeItem('aiflow_user');
};
const isAdmin = user?.role === 'admin';
if (loading) {
return <div className="loading-screen">加载中...</div>;
}
return (
<AuthContext.Provider value={{ user, login, logout, isAdmin }}>
<Suspense fallback={pageFallback}>
<Routes>
{/* Login */}
<Route path="/login" element={
user ? (isAdmin ? <Navigate to="/admin" /> : <Navigate to="/chat" />) : <LoginPage />
} />
{/* Root - Landing for guests, redirect for authenticated users */}
<Route path="/" element={
user ? (isAdmin ? <Navigate to="/admin" /> : <Navigate to="/chat" />) : <LandingPage />
} />
{/* Front Routes (authenticated only) */}
<Route path="/" element={<FrontLayout />}>
<Route path="chat" element={<ChatPage />} />
<Route path="chat/:id" element={<ChatPage />} />
<Route path="knowledge" element={<KnowledgePage />} />
<Route path="market" element={<MarketPage />} />
<Route path="token" element={<TokenDashboardPage />} />
<Route path="pricing" element={<PricingPage />} />
<Route path="profile" element={<ProfilePage />} />
<Route path="notifications" element={<NotificationPage />} />
<Route path="assets" element={<AssetsPage />} />
<Route path="assets/novel/:id" element={<NovelPreviewPage />} />
<Route path="evolution" element={<EvolutionDashboard />} />
<Route path="wechat-bot" element={<WechatBot />} />
</Route>
{/* Admin Routes */}
<Route path="/admin" element={<AdminLayout />}>
<Route index element={<AdminDashboard />} />
<Route path="users" element={<AdminUsers />} />
<Route path="models" element={<AdminModels />} />
<Route path="settings" element={<AdminSiteSettings />} />
<Route path="sms" element={<AdminSmsConfig />} />
<Route path="payment" element={<AdminPayment />} />
<Route path="services" element={<AdminServices />} />
<Route path="membership" element={<AdminMembership />} />
<Route path="ads" element={<AdminAds />} />
<Route path="market" element={<AdminMarket />} />
<Route path="logs" element={<AdminLogs />} />
<Route path="skills" element={<AdminSkills />} />
<Route path="agents" element={<AdminAgents />} />
<Route path="code-exec" element={<AdminCodeExec />} />
<Route path="distributions" element={<DistributionBuilder />} />
<Route path="im-bots" element={<ImBots />} />
<Route path="privacy" element={<PrivacySettings />} />
<Route path="promotion" element={<Promotion />} />
<Route path="achievement" element={<Achievement />} />
<Route path="tenant" element={<Tenant />} />
<Route path="billing" element={<Billing />} />
<Route path="analytics" element={<Analytics />} />
<Route path="knowledge" element={<AdminKnowledge />} />
<Route path="workflow" element={<AdminWorkflow />} />
<Route path="storage" element={<AdminStorage />} />
<Route path="developer" element={<AdminDeveloper />} />
<Route path="developer-tools" element={<AdminDeveloperTools />} />
<Route path="automation" element={<AdminAutomation />} />
<Route path="mcp" element={<AdminMcp />} />
<Route path="apps" element={<AdminApps />} />
<Route path="assets" element={<AssetCenter />} />
<Route path="portal" element={<UserPortal />} />
</Route>
{/* 公开前台首页:不受登录态拦截,供后台「访问前台」直接打开 */}
<Route path="/home" element={<LandingPage />} />
<Route path="*" element={<Navigate to="/" />} />
</Routes>
</Suspense>
</AuthContext.Provider>
);
};
export default App;
+1959 -2016
View File
File diff suppressed because it is too large. Load diff
+182 -183
View File
@@ -1,183 +1,182 @@
import React, { useState } from 'react';
import { Outlet, NavLink, useNavigate } from 'react-router-dom';
import { useAuth } from '@/contexts/AuthContext';
import './FrontLayout.css';
const navGroups = [
{
title: '智能体',
items: [
{ path: '/chat', icon: 'chat', label: 'AI聊天' },
{ path: '/knowledge', icon: 'knowledge', label: '知识库' },
{ path: '/market', icon: 'market', label: '技能市场' },
{ path: '/evolution', icon: 'evolution', label: 'AI自进化' },
],
},
{
title: 'IM Bot',
items: [
{ path: '/channels', icon: 'channel', label: '频道管理' },
{ path: '/wechat-bot', icon: 'wechat', label: '微信机器人' },
],
},
{
title: '资源中心',
items: [
{ path: '/assets', icon: 'assets', label: '资产文件' },
{ path: '/token', icon: 'token', label: 'Token驾驶舱' },
{ path: '/pricing', icon: 'pricing', label: '付费中心' },
],
},
];
const bottomItems = [
{ path: '/profile', icon: 'profile', label: '个人中心' },
{ path: '/notifications', icon: 'notify', label: '消息通知' },
];
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>,
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>,
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>,
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>,
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>,
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>,
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 { user, logout } = useAuth();
const navigate = useNavigate();
const [collapsed, setCollapsed] = useState(false);
if (!user) {
navigate('/login');
return null;
}
return (
<div className="front-layout">
{/* Sidebar */}
<aside className={`front-sidebar ${collapsed ? 'collapsed' : ''}`}>
{/* Header */}
<div className="sidebar-header">
<div className="sidebar-logo">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none">
<defs>
<linearGradient id="flGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stopColor="#5B5FED"/>
<stop offset="100%" stopColor="#4A4ECC"/>
</linearGradient>
</defs>
<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="M6 22h20" stroke="white" strokeWidth="1.5" strokeLinecap="round" opacity="0.5"/>
</svg>
{!collapsed && <span className="sidebar-brand">七虎AI</span>}
</div>
<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">
{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>
</button>
</div>
{/* Navigation */}
<nav className="sidebar-nav">
<NavLink to="/" className={({ isActive }) => `nav-item nav-home-link`} end>
<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>
</span>
{!collapsed && <span className="nav-item-label">返回首页</span>}
</NavLink>
{navGroups.map(group => (
<div className="nav-section" key={group.title}>
{!collapsed && <div className="nav-group-title">{group.title}</div>}
{group.items.map(item => (
<NavLink
key={item.path}
to={item.path}
className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`}
title={collapsed ? item.label : undefined}
>
<span className="nav-icon">{iconPaths[item.icon]}</span>
{!collapsed && <span className="nav-label">{item.label}</span>}
{!collapsed && <span className="nav-active-dot" />}
</NavLink>
))}
</div>
))}
<div className="nav-section nav-bottom">
{bottomItems.map(item => (
<NavLink
key={item.path}
to={item.path}
className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`}
title={collapsed ? item.label : undefined}
>
<span className="nav-icon">{iconPaths[item.icon]}</span>
{!collapsed && <span className="nav-label">{item.label}</span>}
{!collapsed && <span className="nav-active-dot" />}
</NavLink>
))}
</div>
</nav>
{/* Footer */}
<div className="sidebar-footer">
<div className="user-info-mini">
<div className="user-avatar-sm">
{user.username.charAt(0).toUpperCase()}
</div>
{!collapsed && (
<div className="user-details">
<span className="user-name">{user.username}</span>
<span className="user-plan">{user.membership?.planName || '免费版'}</span>
</div>
)}
</div>
{user.role === 'admin' && (
<NavLink
to="/admin"
className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`}
title="管理后台"
>
<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">
<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>
</span>
{!collapsed && <span className="nav-label">管理后台</span>}
</NavLink>
)}
<button
className="nav-item logout-btn"
onClick={() => { logout(); navigate('/login'); }}
title="退出登录"
>
<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">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
<polyline points="16 17 21 12 16 7"/>
<line x1="21" y1="12" x2="9" y2="12"/>
</svg>
</span>
{!collapsed && <span className="nav-label">退出登录</span>}
</button>
</div>
</aside>
{/* Main Content */}
<main className="front-main">
<Outlet />
</main>
</div>
);
};
export default FrontLayout;
import React, { useState } from 'react';
import { Outlet, NavLink, useNavigate } from 'react-router-dom';
import { useAuth } from '@/contexts/AuthContext';
import './FrontLayout.css';
const navGroups = [
{
title: '智能体',
items: [
{ path: '/chat', icon: 'chat', label: 'AI聊天' },
{ path: '/knowledge', icon: 'knowledge', label: '知识库' },
{ path: '/market', icon: 'market', label: '技能市场' },
{ path: '/evolution', icon: 'evolution', label: 'AI自进化' },
],
},
{
title: 'IM Bot',
items: [
{ path: '/wechat-bot', icon: 'wechat', label: '微信机器人' },
],
},
{
title: '资源中心',
items: [
{ path: '/assets', icon: 'assets', label: '资产文件' },
{ path: '/token', icon: 'token', label: 'Token驾驶舱' },
{ path: '/pricing', icon: 'pricing', label: '付费中心' },
],
},
];
const bottomItems = [
{ path: '/profile', icon: 'profile', label: '个人中心' },
{ path: '/notifications', icon: 'notify', label: '消息通知' },
];
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>,
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>,
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>,
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>,
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>,
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>,
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 { user, logout } = useAuth();
const navigate = useNavigate();
const [collapsed, setCollapsed] = useState(false);
if (!user) {
navigate('/login');
return null;
}
return (
<div className="front-layout">
{/* Sidebar */}
<aside className={`front-sidebar ${collapsed ? 'collapsed' : ''}`}>
{/* Header */}
<div className="sidebar-header">
<div className="sidebar-logo">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none">
<defs>
<linearGradient id="flGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stopColor="#5B5FED"/>
<stop offset="100%" stopColor="#4A4ECC"/>
</linearGradient>
</defs>
<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="M6 22h20" stroke="white" strokeWidth="1.5" strokeLinecap="round" opacity="0.5"/>
</svg>
{!collapsed && <span className="sidebar-brand">七虎AI</span>}
</div>
<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">
{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>
</button>
</div>
{/* Navigation */}
<nav className="sidebar-nav">
<NavLink to="/" className={({ isActive }) => `nav-item nav-home-link`} end>
<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>
</span>
{!collapsed && <span className="nav-item-label">返回首页</span>}
</NavLink>
{navGroups.map(group => (
<div className="nav-section" key={group.title}>
{!collapsed && <div className="nav-group-title">{group.title}</div>}
{group.items.map(item => (
<NavLink
key={item.path}
to={item.path}
className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`}
title={collapsed ? item.label : undefined}
>
<span className="nav-icon">{iconPaths[item.icon]}</span>
{!collapsed && <span className="nav-label">{item.label}</span>}
{!collapsed && <span className="nav-active-dot" />}
</NavLink>
))}
</div>
))}
<div className="nav-section nav-bottom">
{bottomItems.map(item => (
<NavLink
key={item.path}
to={item.path}
className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`}
title={collapsed ? item.label : undefined}
>
<span className="nav-icon">{iconPaths[item.icon]}</span>
{!collapsed && <span className="nav-label">{item.label}</span>}
{!collapsed && <span className="nav-active-dot" />}
</NavLink>
))}
</div>
</nav>
{/* Footer */}
<div className="sidebar-footer">
<div className="user-info-mini">
<div className="user-avatar-sm">
{user.username.charAt(0).toUpperCase()}
</div>
{!collapsed && (
<div className="user-details">
<span className="user-name">{user.username}</span>
<span className="user-plan">{user.membership?.planName || '免费版'}</span>
</div>
)}
</div>
{user.role === 'admin' && (
<NavLink
to="/admin"
className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`}
title="管理后台"
>
<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">
<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>
</span>
{!collapsed && <span className="nav-label">管理后台</span>}
</NavLink>
)}
<button
className="nav-item logout-btn"
onClick={() => { logout(); navigate('/login'); }}
title="退出登录"
>
<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">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
<polyline points="16 17 21 12 16 7"/>
<line x1="21" y1="12" x2="9" y2="12"/>
</svg>
</span>
{!collapsed && <span className="nav-label">退出登录</span>}
</button>
</div>
</aside>
{/* Main Content */}
<main className="front-main">
<Outlet />
</main>
</div>
);
};
export default FrontLayout;
+2 -8
View File
@@ -2,7 +2,7 @@
// 分离的Mock数据层,便于后续接入真实后端时替换
import type {
User, Conversation, Message, Channel, KnowledgeBase,
User, Conversation, Message, KnowledgeBase,
KnowledgeDocument, MarketApp, AppReview, TokenUsageLog,
TokenBalance, Order, MembershipPlan, Coupon, UserCoupon,
Notification, ModelProvider, AIModel, SiteSettings,
@@ -97,13 +97,7 @@ export const mockMessages: Record<string, Message[]> = {
],
};
// ==================== 频道数据 (IM账户绑定) ====================
export const mockChannels: Channel[] = [
{ id: 'ch1', userId: 'u1', platform: 'feishu', name: '飞书AI助手', accountName: 'zangsan@company.feishu.cn', agentId: 'a1', knowledgeBaseId: 'kb1', status: 'connected', lastActive: '2026-05-26T09:30:00Z', boundModel: 'DeepSeek-V3', features: ['AI对话', '文档问答', '任务提醒', '会议纪要'], createdAt: '2026-04-01T08:00:00Z' },
{ id: 'ch2', userId: 'u1', platform: 'dingtalk', name: '钉钉AI助理', accountName: '张经理', agentId: 'a2', knowledgeBaseId: 'kb2', status: 'connected', lastActive: '2026-05-26T08:15:00Z', boundModel: 'GPT-4o', features: ['AI对话', '审批辅助', '日程管理'], createdAt: '2026-04-15T10:00:00Z' },
{ id: 'ch3', userId: 'u1', platform: 'wechat', name: '微信AI伙伴', accountName: 'wxid_abc123', agentId: undefined, knowledgeBaseId: undefined, status: 'pending', lastActive: undefined, boundModel: 'DeepSeek-V3', features: ['AI对话', '内容创作'], createdAt: '2026-05-01T09:00:00Z' },
{ id: 'ch4', userId: 'u1', platform: 'telegram', name: 'Telegram Bot', accountName: '@aiflow_bot', agentId: undefined, knowledgeBaseId: undefined, status: 'disconnected', lastActive: '2026-05-10T14:00:00Z', boundModel: 'Qwen-Max', features: ['AI对话'], createdAt: '2026-05-10T14:00:00Z' },
];
// ==================== 知识库数据 ====================
export const mockKnowledgeBases: KnowledgeBase[] = [
+2 -178
View File
@@ -33,7 +33,7 @@ const statusColors: Record<string, string> = {
disconnected: '#6b7280',
};
type Tab = 'dashboard' | 'channels' | 'routes' | 'langbot' | 'messages' | 'setup';
type Tab = 'dashboard' | 'channels' | 'routes' | 'messages' | 'setup';
const ImBots: React.FC = () => {
const [activeTab, setActiveTab] = useState<Tab>('dashboard');
@@ -46,7 +46,6 @@ const ImBots: React.FC = () => {
const [filterPlatform, setFilterPlatform] = useState('');
const [filterStatus, setFilterStatus] = useState('');
const [routes, setRoutes] = useState<any[]>([]);
const [langbotConfig, setLangbotConfig] = useState<any[]>([]);
const [messages, setMessages] = useState<any[]>([]);
const [messagesTotal, setMessagesTotal] = useState(0);
const [messagesPage, setMessagesPage] = useState(1);
@@ -77,18 +76,6 @@ const ImBots: React.FC = () => {
credentials: '',
});
// LangBot form state
const [langbotForm, setLangbotForm] = useState({
id: 1,
gateway_url: '',
api_key: '',
webhook_secret: '',
bot_uuid: '',
callback_url: '',
sync_channels: 0,
status: 'inactive',
});
const showToast = (msg: string, type: 'success' | 'error' = 'success') => {
setToast({ msg, type });
setTimeout(() => setToast(null), 3000);
@@ -155,22 +142,6 @@ const ImBots: React.FC = () => {
}
}, []);
// Load LangBot config
const loadLangbot = useCallback(async () => {
try {
const res = await adminAPI.getLangbotConfig();
if ((res as any)?.success) {
const configs = res.data || [];
setLangbotConfig(configs);
if (configs.length > 0) {
setLangbotForm({ ...langbotForm, ...configs[0] });
}
}
} catch (e) {
showToast('Failed to load LangBot config', 'error');
}
}, []);
// Load messages
const loadMessages = useCallback(async () => {
try {
@@ -211,8 +182,6 @@ const ImBots: React.FC = () => {
} else if (activeTab === 'routes') {
loadRoutes();
loadChannels();
} else if (activeTab === 'langbot') {
loadLangbot();
} else if (activeTab === 'messages') {
loadMessages();
}
@@ -348,51 +317,7 @@ const ImBots: React.FC = () => {
}
};
// Handle LangBot config save
const handleSaveLangbot = async () => {
try {
const params = new URLSearchParams({
config_id: String(langbotForm.id || 1),
gateway_url: langbotForm.gateway_url,
status: langbotForm.status,
sync_channels: String(langbotForm.sync_channels),
});
if (langbotForm.api_key) params.append('api_key', langbotForm.api_key);
if (langbotForm.webhook_secret) params.append('webhook_secret', langbotForm.webhook_secret);
if (langbotForm.bot_uuid) params.append('bot_uuid', langbotForm.bot_uuid);
if (langbotForm.callback_url) params.append('callback_url', langbotForm.callback_url);
await adminAPI.updateLangbotConfig('?' + params.toString());
showToast('LangBot config saved');
loadLangbot();
} catch (e) {
showToast('Failed to save LangBot config', 'error');
}
};
// Handle LangBot test
const handleTestLangbot = async () => {
if (!langbotForm.gateway_url) {
showToast('Please enter gateway URL', 'error');
return;
}
try {
const params = new URLSearchParams({
gateway_url: langbotForm.gateway_url,
});
if (langbotForm.api_key) params.append('api_key', langbotForm.api_key);
const res = await adminAPI.testLangbot('?' + params.toString());
if ((res as any)?.success) {
const data = res.data;
if (data.reachable) {
showToast(`LangBot reachable! Status: ${data.status_code}`);
} else {
showToast(`LangBot unreachable: ${data.error}`, 'error');
}
}
} catch (e) {
showToast('Test failed', 'error');
}
};
// Copy to clipboard
const copyToClipboard = (text: string) => {
@@ -411,7 +336,6 @@ const ImBots: React.FC = () => {
{ key: 'dashboard', label: '仪表盘', icon: 'M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z' },
{ key: 'channels', label: '频道管理', icon: 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z' },
{ key: 'routes', label: '消息路由', icon: 'M3.5 18.5l6-6.01 4 4L22 6.92l-1.41-1.41-7.09 7.97-4-4L2 17l1.5 1.5z' },
{ key: 'langbot', label: 'LangBot 网关', icon: 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z' },
{ key: 'messages', label: '消息日志', icon: 'M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z' },
{ key: 'setup', label: '快速接入', icon: 'M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z' },
];
@@ -422,7 +346,7 @@ const ImBots: React.FC = () => {
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px' }}>
<div>
<h1 style={{ fontSize: '24px', fontWeight: 700, margin: 0 }}>IM 机器人管理</h1>
<p style={{ color: '#888', fontSize: '14px', margin: '4px 0 0 0' }}>管理即时通讯平台接入、消息路由和 LangBot 网关</p>
<p style={{ color: '#888', fontSize: '14px', margin: '4px 0 0 0' }}>管理即时通讯平台接入与消息路由</p>
</div>
<div style={{ display: 'flex', gap: '8px' }}>
{stats && (
@@ -430,7 +354,6 @@ const ImBots: React.FC = () => {
<StatBadge label="频道" value={stats.total_channels} color="#3370FF" />
<StatBadge label="在线" value={stats.active_channels} color="#10b981" />
<StatBadge label="今日消息" value={stats.today_messages_in + stats.today_messages_out} color="#f59e0b" />
<StatBadge label="LangBot" value={stats.langbot_status === 'active' ? 'ON' : 'OFF'} color={stats.langbot_status === 'active' ? '#10b981' : '#6b7280'} />
</div>
)}
</div>
@@ -744,105 +667,6 @@ const ImBots: React.FC = () => {
</div>
)}
{/* LangBot Tab */}
{activeTab === 'langbot' && (
<div style={{ maxWidth: '800px' }}>
<div style={{ background: '#1a1d27', borderRadius: '12px', padding: '24px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
<h3 style={{ margin: 0, fontSize: '16px' }}>LangBot 网关配置</h3>
<span style={{
padding: '4px 12px', borderRadius: '4px', fontSize: '12px',
background: langbotForm.status === 'active' ? '#10b98120' : '#6b728020',
color: langbotForm.status === 'active' ? '#10b981' : '#6b7280',
}}>
{langbotForm.status === 'active' ? '已启用' : '未启用'}
</span>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
<FormField label="网关 URL" >
<input
value={langbotForm.gateway_url}
onChange={(e) => setLangbotForm({ ...langbotForm, gateway_url: e.target.value })}
placeholder="http://localhost:5300"
style={inputStyle}
/>
</FormField>
<FormField label="Bot UUID">
<input
value={langbotForm.bot_uuid || ''}
onChange={(e) => setLangbotForm({ ...langbotForm, bot_uuid: e.target.value })}
placeholder="LangBot HTTP Bot UUID"
style={inputStyle}
/>
</FormField>
<FormField label="API Key">
<input
type="password"
value={langbotForm.api_key || ''}
onChange={(e) => setLangbotForm({ ...langbotForm, api_key: e.target.value })}
placeholder="LangBot REST API Key"
style={inputStyle}
/>
</FormField>
<FormField label="Webhook Secret">
<input
type="password"
value={langbotForm.webhook_secret || ''}
onChange={(e) => setLangbotForm({ ...langbotForm, webhook_secret: e.target.value })}
placeholder="HMAC-SHA256 签名密钥"
style={inputStyle}
/>
</FormField>
<FormField label="回调 URL">
<input
value={langbotForm.callback_url || ''}
onChange={(e) => setLangbotForm({ ...langbotForm, callback_url: e.target.value })}
placeholder="http://210.16.161.218:12888/app/api/channels/webhook/langbot"
style={inputStyle}
/>
</FormField>
<FormField label="自动同步频道">
<select
value={langbotForm.sync_channels}
onChange={(e) => setLangbotForm({ ...langbotForm, sync_channels: Number(e.target.value) })}
style={selectStyle}
>
<option value={0}>否</option>
<option value={1}>是</option>
</select>
</FormField>
<FormField label="状态">
<select
value={langbotForm.status}
onChange={(e) => setLangbotForm({ ...langbotForm, status: e.target.value })}
style={selectStyle}
>
<option value="inactive">未启用</option>
<option value="active">启用</option>
</select>
</FormField>
</div>
<div style={{ display: 'flex', gap: '12px', marginTop: '24px' }}>
<button onClick={handleSaveLangbot} style={primaryButtonStyle}>保存配置</button>
<button onClick={handleTestLangbot} style={secondaryButtonStyle}>测试连接</button>
</div>
</div>
{/* LangBot Info */}
<div style={{ background: '#1a1d27', borderRadius: '12px', padding: '24px', marginTop: '16px' }}>
<h4 style={{ marginTop: 0, fontSize: '14px', color: '#888' }}>LangBot 集成说明</h4>
<ul style={{ fontSize: '13px', color: '#aaa', lineHeight: '1.8', paddingLeft: '20px' }}>
<li>LangBot 是开源的 LLM 原生 IM 机器人平台,支持飞书、微信、钉钉等</li>
<li>配置 LangBot 作为外部网关后,IM 消息将通过 LangBot 转发到本平台</li>
<li>在 LangBot 中创建 HTTP Bot 适配器,将 Webhook URL 指向本平台</li>
<li>API Key 在 LangBot 的 config.yaml 中通过 api.global_api_key 配置</li>
<li>Webhook Secret 用于 HMAC-SHA256 双向签名验证</li>
</ul>
</div>
</div>
)}
{/* Messages Tab */}
{activeTab === 'messages' && (
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -68,7 +68,7 @@ const features = [
{ d: <><circle cx="12" cy="12" r="10" /><path d="M12 6v6l4 2" /></>,
title: 'Token 驾驶舱', desc: '实时监控 Token 消耗,费用透明可控,每一笔都有据可查。' },
{ d: <><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" /></>,
title: 'IM 频道绑定', desc: '微信 / 企微 / 飞书 / QQ 多平台绑定,AI 助手融入日常工作流。' },
title: 'AI 微信机器人', desc: '个人微信 AI 助手直接对接,消息自动回复,AI 融入你的日常沟通。' },
{ d: <><polyline points="16 3 21 3 21 8" /><line x1="4" y1="20" x2="21" y2="3" /><polyline points="21 16 21 21 16 21" /><line x1="15" y1="15" x2="21" y2="21" /><line x1="4" y1="4" x2="9" y2="9" /></>,
title: '自动化任务编排', desc: '关键词触发、定时执行等自动化任务,让 AI 按你的节奏工作。' },
];
-18
View File
@@ -63,24 +63,6 @@ export interface Message {
used_skills?: string[];
}
// 频道
// IM账户绑定频道
export type IMPlatform = 'feishu' | 'wechat' | 'dingtalk' | 'whatsapp' | 'telegram' | 'slack';
export interface Channel {
id: string;
userId: string;
platform: IMPlatform;
name: string;
accountName: string;
agentId?: string;
knowledgeBaseId?: string;
status: 'connected' | 'disconnected' | 'pending';
lastActive?: string;
boundModel: string;
features: string[];
createdAt: string;
}
// 知识库
export interface KnowledgeBase {