增加后台管理项目;
This commit is contained in:
@@ -1 +1 @@
|
|||||||
177144
|
285640
|
||||||
|
|||||||
@@ -24,8 +24,27 @@ async def lifespan(app: FastAPI):
|
|||||||
) # 批量注册模型
|
) # 批量注册模型
|
||||||
print(f"🗄️ 数据库连接成功: {database_name}")
|
print(f"🗄️ 数据库连接成功: {database_name}")
|
||||||
|
|
||||||
|
# 初始化GLM服务,确保默认角色存在
|
||||||
|
try:
|
||||||
|
from app.services.glm_service import get_glm_service
|
||||||
|
|
||||||
|
glm_service = get_glm_service()
|
||||||
|
await glm_service.ensure_default_role()
|
||||||
|
print(f"✅ GLM角色配置初始化完成")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ GLM角色配置初始化失败: {e}")
|
||||||
|
|
||||||
yield # 应用在此处进入正常运行状态,接收请求
|
yield # 应用在此处进入正常运行状态,接收请求
|
||||||
|
|
||||||
|
# 关闭GLM服务
|
||||||
|
try:
|
||||||
|
from app.services.glm_service import close_glm_service
|
||||||
|
|
||||||
|
close_glm_service()
|
||||||
|
print(f"GLM服务已关闭")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"关闭GLM服务时出错: {e}")
|
||||||
|
|
||||||
# 关闭数据库连接
|
# 关闭数据库连接
|
||||||
await client.close()
|
await client.close()
|
||||||
print(f"数据库连接关闭")
|
print(f"数据库连接关闭")
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
from .base import BaseOut
|
from .base import BaseOut
|
||||||
from .message import Message
|
from .message import Message
|
||||||
|
from .glm_role import GLMRole
|
||||||
|
|
||||||
models = [Message]
|
models = [Message, GLMRole]
|
||||||
|
|
||||||
# 在所有模型导入后调用 model_rebuild 来解决循环导入问题
|
# 在所有模型导入后调用 model_rebuild 来解决循环导入问题
|
||||||
# Message.model_rebuild()
|
# Message.model_rebuild()
|
||||||
|
|||||||
56
backend/app/models/glm_role.py
Normal file
56
backend/app/models/glm_role.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
from pydantic import Field
|
||||||
|
from beanie import Document
|
||||||
|
import arrow
|
||||||
|
|
||||||
|
|
||||||
|
class GLMRole(Document):
|
||||||
|
"""GLM角色配置模型"""
|
||||||
|
|
||||||
|
# 基本信息
|
||||||
|
name: str = Field(..., description="角色名称")
|
||||||
|
description: str = Field(..., description="角色描述")
|
||||||
|
skills: str = Field(default="", description="技能列表(多行文本字符串)")
|
||||||
|
restrictions: str = Field(default="", description="限制列表(多行文本字符串)")
|
||||||
|
|
||||||
|
# 状态管理
|
||||||
|
is_active: bool = Field(default=True, description="是否激活")
|
||||||
|
is_default: bool = Field(default=False, description="是否为默认角色")
|
||||||
|
|
||||||
|
# 时间戳
|
||||||
|
created_at: datetime = Field(default_factory=lambda: arrow.now().datetime)
|
||||||
|
updated_at: datetime = Field(default_factory=lambda: arrow.now().datetime)
|
||||||
|
|
||||||
|
class Settings:
|
||||||
|
name = "glm_roles" # MongoDB集合名称
|
||||||
|
indexes = [
|
||||||
|
[("is_active", 1)], # 按激活状态查询
|
||||||
|
[("is_default", 1)], # 按默认状态查询
|
||||||
|
[("created_at", -1)], # 按创建时间排序
|
||||||
|
]
|
||||||
|
|
||||||
|
def update_timestamp(self):
|
||||||
|
"""更新更新时间戳"""
|
||||||
|
self.updated_at = arrow.now().datetime
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_role_config(cls, role_config: dict) -> "GLMRole":
|
||||||
|
"""从角色配置字典创建GLMRole实例"""
|
||||||
|
return cls(
|
||||||
|
name=role_config.get("name", ""),
|
||||||
|
description=role_config.get("description", ""),
|
||||||
|
skills=role_config.get("skills", ""),
|
||||||
|
restrictions=role_config.get("restrictions", ""),
|
||||||
|
is_active=True,
|
||||||
|
is_default=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_role_config_dict(self) -> dict:
|
||||||
|
"""转换为角色配置字典"""
|
||||||
|
return {
|
||||||
|
"name": self.name,
|
||||||
|
"description": self.description,
|
||||||
|
"skills": self.skills,
|
||||||
|
"restrictions": self.restrictions,
|
||||||
|
}
|
||||||
@@ -1,12 +1,18 @@
|
|||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException, Depends
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
import httpx
|
import httpx
|
||||||
import os
|
import os
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from app.services.glm_service import get_glm_service, GLMService
|
||||||
|
from app.models.base import BaseOut
|
||||||
|
|
||||||
# 创建API路由,使用/glm前缀
|
# 创建API路由,使用/glm前缀
|
||||||
router = APIRouter(prefix="/glm", tags=["GLM聊天"])
|
router = APIRouter(prefix="/glm", tags=["GLM聊天"])
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
# 数据模型
|
# 数据模型
|
||||||
class Message(BaseModel):
|
class Message(BaseModel):
|
||||||
@@ -27,73 +33,79 @@ class ChatResponse(BaseModel):
|
|||||||
class RoleConfig(BaseModel):
|
class RoleConfig(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
description: str
|
description: str
|
||||||
skills: List[str]
|
skills: str # 多行文本字符串
|
||||||
restrictions: List[str]
|
restrictions: str # 多行文本字符串
|
||||||
|
|
||||||
|
|
||||||
# 默认角色配置
|
# 默认角色配置(使用字符串格式)
|
||||||
DEFAULT_ROLE_CONFIG = RoleConfig(
|
DEFAULT_ROLE_CONFIG = RoleConfig(
|
||||||
name="兜兜",
|
name="兜兜",
|
||||||
description="你是兜兜,1989年出生,有一个5岁的女儿(2018年出生)。你是一位对用户负责、选品严格的宝妈客服。当用户给你发来疑问时,你总能第一时间回复,并以亲切温暖的风格与用户交流。",
|
description="你是兜兜,1989年出生,有一个5岁的女儿(2018年出生)。你是一位对用户负责、选品严格的宝妈客服。当用户给你发来疑问时,你总能第一时间回复,并以亲切温暖的风格与用户交流。",
|
||||||
skills=[
|
skills="亲切温暖的沟通\n轻松友好的语气\n简洁明了的描述\n亲昵且口语化的用词\n乱码话术重复问询",
|
||||||
"亲切温暖的沟通",
|
restrictions="回复必须基于提供的知识库中的内容\n每条回复应保持在80字以内,使用口语化语言\n回复中可以适当加入表情符号,但不要过度使用\n只讨论与宝妈相关的主题,如育儿、选品、团购等\n始终以用户的需求和满意度为优先",
|
||||||
"轻松友好的语气",
|
|
||||||
"简洁明了的描述",
|
|
||||||
"亲昵且口语化的用词",
|
|
||||||
"乱码话术重复问询",
|
|
||||||
],
|
|
||||||
restrictions=[
|
|
||||||
"回复必须基于提供的知识库中的内容",
|
|
||||||
"每条回复应保持在80字以内,使用口语化语言",
|
|
||||||
"回复中可以适当加入表情符号,但不要过度使用",
|
|
||||||
"只讨论与宝妈相关的主题,如育儿、选品、团购等",
|
|
||||||
"始终以用户的需求和满意度为优先",
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# 全局变量存储角色配置和对话历史
|
# 全局变量存储对话历史(暂时保持内存存储,根据用户要求不持久化)
|
||||||
current_role_config = DEFAULT_ROLE_CONFIG
|
|
||||||
conversations = {}
|
conversations = {}
|
||||||
|
|
||||||
|
|
||||||
def build_system_prompt(role_config: RoleConfig) -> str:
|
def build_system_prompt(role_config: dict) -> str:
|
||||||
"""构建系统提示词"""
|
"""构建系统提示词
|
||||||
|
|
||||||
|
Args:
|
||||||
|
role_config: 角色配置字典,包含name, description, skills, restrictions字段
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
系统提示词字符串
|
||||||
|
"""
|
||||||
|
# 解析技能字符串为列表
|
||||||
|
skills_list = [
|
||||||
|
skill.strip() for skill in role_config["skills"].split("\n") if skill.strip()
|
||||||
|
]
|
||||||
|
restrictions_list = [
|
||||||
|
restriction.strip()
|
||||||
|
for restriction in role_config["restrictions"].split("\n")
|
||||||
|
if restriction.strip()
|
||||||
|
]
|
||||||
|
|
||||||
|
# 构建技能部分
|
||||||
|
skills_section = ""
|
||||||
|
for i, skill in enumerate(skills_list, 1):
|
||||||
|
skills_section += f"\n### 技能 {i}: {skill}"
|
||||||
|
# 添加技能描述(这里可以根据技能名称添加不同的描述)
|
||||||
|
if "亲切温暖" in skill:
|
||||||
|
skills_section += '\n- **风格**:你的风格非常亲切,像是一个知心朋友在和大家聊天,给人一种温暖的感觉。你经常使用"宝"、"美妈"等亲昵的称呼,拉近了与用户的距离。'
|
||||||
|
skills_section += "\n- **耐心细致**:你总是耐心解答用户的问题,尤其是对于新用户或不熟悉流程的用户,你会反复提醒他们查看群公告,保持安静等待开团。"
|
||||||
|
skills_section += "\n- **负责靠谱**:你给人一种非常靠谱的感觉,尤其是在售后问题上,你会主动承担责任,承诺包处理满意,给用户很强的信任感。"
|
||||||
|
elif "轻松友好" in skill:
|
||||||
|
skills_section += '\n- **轻松友好**:你的语气非常轻松,像是在和朋友聊天,不会给人压力。比如"不着急哦"、"爱你!"等表达方式,让人感到轻松愉快。'
|
||||||
|
skills_section += '\n- **安抚性**:当用户有疑问或遇到问题时,你的语气总是充满安抚性,比如"美妈不用担心"、"兜兜来处理哈",给人一种安心感。'
|
||||||
|
skills_section += '\n- **鼓励性**:你经常用"不着急"、"等开团的时候会有统一说明"等话语,鼓励用户耐心等待,保持积极的心态。'
|
||||||
|
elif "简洁明了" in skill:
|
||||||
|
skills_section += '\n- **简洁明了**:你的描述非常简洁,直接回答用户的问题,不会绕弯子。比如"贝亲是咱们深度合作品牌,经常上团的",直接给出了明确的答案。'
|
||||||
|
skills_section += '\n- **细节丰富**:在回答问题时,你会提供一些细节信息,比如"一般2天左右就会来"、"奶粉类的,贝亲,世喜,赫根,等常见品牌都有",让用户对情况有更清晰的了解。'
|
||||||
|
skills_section += '\n- **引导性**:你经常引导用户查看群公告,保持安静等待开团,比如"不熟悉的可以看一下群公告哦"、"现在安静的等开团就行啦",帮助用户更好地参与活动。'
|
||||||
|
elif "亲昵且口语化" in skill:
|
||||||
|
skills_section += '\n- **亲昵称呼**:你经常使用"宝"、"美妈"、"大家"等亲昵的称呼,拉近与用户的距离,给人一种亲切感。'
|
||||||
|
skills_section += '\n- **口语化**:你的用词非常口语化,像是日常聊天,比如"不着急哦"、"爱你!"、"兜兜来处理哈",给人一种轻松自然的感觉,称呼自己为"兜兜"或者"我"。'
|
||||||
|
skills_section += '\n- **积极正面**:你的用词总是积极正面的,比如"不用担心"、"包处理满意"、"很快"、"常见品牌都有",给用户传递了积极的信息。'
|
||||||
|
elif "乱码话术" in skill:
|
||||||
|
skills_section += '\n- **确认问题**:用户说的看不懂的情况来进行再次确认,比如 "45615465" "分隔符规划规范的" "8分隔符公司的"等跟常规问题没关联的话术来重复确认,例如:"宝看不懂你说的是什么啊,能在具体点嘛"'
|
||||||
|
else:
|
||||||
|
skills_section += f"\n- **{skill}**:请根据这个技能特点来回答问题。"
|
||||||
|
|
||||||
|
# 构建限制部分
|
||||||
|
restrictions_section = "\n".join(
|
||||||
|
[f"- {restriction}" for restriction in restrictions_list]
|
||||||
|
)
|
||||||
|
|
||||||
prompt = f"""# 角色
|
prompt = f"""# 角色
|
||||||
{role_config.description}
|
{role_config["description"]}
|
||||||
|
|
||||||
## 技能
|
## 技能{skills_section}
|
||||||
|
|
||||||
### 技能 1: 亲切温暖的沟通
|
|
||||||
- **风格**:你的风格非常亲切,像是一个知心朋友在和大家聊天,给人一种温暖的感觉。你经常使用"宝"、"美妈"等亲昵的称呼,拉近了与用户的距离。
|
|
||||||
- **耐心细致**:你总是耐心解答用户的问题,尤其是对于新用户或不熟悉流程的用户,你会反复提醒他们查看群公告,保持安静等待开团。
|
|
||||||
- **负责靠谱**:你给人一种非常靠谱的感觉,尤其是在售后问题上,你会主动承担责任,承诺包处理满意,给用户很强的信任感。
|
|
||||||
|
|
||||||
### 技能 2: 轻松友好的语气
|
|
||||||
- **轻松友好**:你的语气非常轻松,像是在和朋友聊天,不会给人压力。比如"不着急哦"、"爱你!"等表达方式,让人感到轻松愉快。
|
|
||||||
- **安抚性**:当用户有疑问或遇到问题时,你的语气总是充满安抚性,比如"美妈不用担心"、"兜兜来处理哈",给人一种安心感。
|
|
||||||
- **鼓励性**:你经常用"不着急"、"等开团的时候会有统一说明"等话语,鼓励用户耐心等待,保持积极的心态。
|
|
||||||
|
|
||||||
### 技能 3: 简洁明了的描述
|
|
||||||
- **简洁明了**:你的描述非常简洁,直接回答用户的问题,不会绕弯子。比如"贝亲是咱们深度合作品牌,经常上团的",直接给出了明确的答案。
|
|
||||||
- **细节丰富**:在回答问题时,你会提供一些细节信息,比如"一般2天左右就会来"、"奶粉类的,贝亲,世喜,赫根,等常见品牌都有",让用户对情况有更清晰的了解。
|
|
||||||
- **引导性**:你经常引导用户查看群公告,保持安静等待开团,比如"不熟悉的可以看一下群公告哦"、"现在安静的等开团就行啦",帮助用户更好地参与活动。
|
|
||||||
|
|
||||||
### 技能 4: 亲昵且口语化的用词
|
|
||||||
- **亲昵称呼**:你经常使用"宝"、"美妈"、"大家"等亲昵的称呼,拉近与用户的距离,给人一种亲切感。
|
|
||||||
- **口语化**:你的用词非常口语化,像是日常聊天,比如"不着急哦"、"爱你!"、"兜兜来处理哈",给人一种轻松自然的感觉,称呼自己为"兜兜"或者"我"。
|
|
||||||
- **积极正面**:你的用词总是积极正面的,比如"不用担心"、"包处理满意"、"很快"、"常见品牌都有",给用户传递了积极的信息。
|
|
||||||
|
|
||||||
### 技能 5: 乱码话术重复问询
|
|
||||||
- **确认问题**:用户说的看不懂的情况来进行再次确认,比如 "45615465" "分隔符规划规范的" "8分隔符公司的"等跟常规问题没关联的话术来重复确认,例如:"宝看不懂你说的是什么啊,能在具体点嘛"
|
|
||||||
|
|
||||||
## 限制
|
## 限制
|
||||||
- 回复必须基于提供的知识库中的内容。
|
{restrictions_section}
|
||||||
- 每条回复应保持在80字以内,使用口语化语言。
|
|
||||||
- 回复中可以适当加入表情符号,但不要过度使用。
|
|
||||||
- 只讨论与宝妈相关的主题,如育儿、选品、团购等。
|
|
||||||
- 始终以用户的需求和满意度为优先。
|
|
||||||
- 在回答问题时,确保信息准确无误,避免误导用户。
|
|
||||||
- 保持沟通的亲切性和温暖感,但不要过度使用亲昵称呼,以免显得过于随意。
|
|
||||||
|
|
||||||
请按照以上角色设定和限制来回答用户的问题。"""
|
请按照以上角色设定和限制来回答用户的问题。"""
|
||||||
return prompt
|
return prompt
|
||||||
@@ -145,22 +157,42 @@ async def root():
|
|||||||
return {"message": "DeepSeek AI Chat Assistant API"}
|
return {"message": "DeepSeek AI Chat Assistant API"}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/role")
|
@router.get("/role", response_model=BaseOut)
|
||||||
async def get_role_config():
|
async def get_role_config(service: GLMService = Depends(get_glm_service)):
|
||||||
"""获取当前角色配置"""
|
"""获取当前角色配置"""
|
||||||
return current_role_config
|
try:
|
||||||
|
role = await service.get_current_role()
|
||||||
|
return BaseOut(code=200, message="ok", result=role.to_role_config_dict())
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取角色配置失败: {e}")
|
||||||
|
return BaseOut(code=500, message=f"获取角色配置失败: {str(e)}", result=None)
|
||||||
|
|
||||||
|
|
||||||
@router.put("/role")
|
@router.put("/role", response_model=BaseOut)
|
||||||
async def update_role_config(role_config: RoleConfig):
|
async def update_role_config(
|
||||||
|
role_config: RoleConfig, service: GLMService = Depends(get_glm_service)
|
||||||
|
):
|
||||||
"""更新角色配置"""
|
"""更新角色配置"""
|
||||||
global current_role_config
|
try:
|
||||||
current_role_config = role_config
|
# 将Pydantic模型转换为字典
|
||||||
return {"message": "角色配置已更新", "role_config": current_role_config}
|
role_data = role_config.dict()
|
||||||
|
updated_role = await service.update_role(role_data)
|
||||||
|
|
||||||
|
return BaseOut(
|
||||||
|
code=200,
|
||||||
|
message="角色配置已更新",
|
||||||
|
result={
|
||||||
|
"message": "角色配置已更新",
|
||||||
|
"role_config": updated_role.to_role_config_dict(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"更新角色配置失败: {e}")
|
||||||
|
return BaseOut(code=500, message=f"更新角色配置失败: {str(e)}", result=None)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/chat", response_model=ChatResponse)
|
@router.post("/chat", response_model=ChatResponse)
|
||||||
async def chat(request: ChatRequest):
|
async def chat(request: ChatRequest, service: GLMService = Depends(get_glm_service)):
|
||||||
"""处理聊天请求"""
|
"""处理聊天请求"""
|
||||||
# 创建或获取对话历史
|
# 创建或获取对话历史
|
||||||
if request.conversation_id and request.conversation_id in conversations:
|
if request.conversation_id and request.conversation_id in conversations:
|
||||||
@@ -170,8 +202,16 @@ async def chat(request: ChatRequest):
|
|||||||
request.conversation_id = f"conv_{len(conversations) + 1}"
|
request.conversation_id = f"conv_{len(conversations) + 1}"
|
||||||
conversations[request.conversation_id] = conversation_history
|
conversations[request.conversation_id] = conversation_history
|
||||||
|
|
||||||
|
# 获取当前角色配置
|
||||||
|
try:
|
||||||
|
role = await service.get_current_role()
|
||||||
|
system_prompt = build_system_prompt(role.to_role_config_dict())
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取角色配置失败: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"获取角色配置失败: {str(e)}")
|
||||||
|
|
||||||
# 构建消息列表
|
# 构建消息列表
|
||||||
messages = [{"role": "system", "content": build_system_prompt(current_role_config)}]
|
messages = [{"role": "system", "content": system_prompt}]
|
||||||
|
|
||||||
# 添加历史消息
|
# 添加历史消息
|
||||||
for msg in conversation_history[-10:]: # 保留最近10条历史
|
for msg in conversation_history[-10:]: # 保留最近10条历史
|
||||||
@@ -204,3 +244,24 @@ async def get_conversation(conversation_id: str):
|
|||||||
"conversation_id": conversation_id,
|
"conversation_id": conversation_id,
|
||||||
"messages": conversations[conversation_id],
|
"messages": conversations[conversation_id],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/role/reset", response_model=BaseOut)
|
||||||
|
async def reset_role_config(service: GLMService = Depends(get_glm_service)):
|
||||||
|
"""重置角色配置为默认值"""
|
||||||
|
try:
|
||||||
|
# 使用 DEFAULT_ROLE_CONFIG 更新角色
|
||||||
|
role_data = DEFAULT_ROLE_CONFIG.dict()
|
||||||
|
updated_role = await service.update_role(role_data)
|
||||||
|
|
||||||
|
return BaseOut(
|
||||||
|
code=200,
|
||||||
|
message="角色配置已重置为默认值",
|
||||||
|
result={
|
||||||
|
"message": "角色配置已重置为默认值",
|
||||||
|
"role_config": updated_role.to_role_config_dict(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"重置角色配置失败: {e}")
|
||||||
|
return BaseOut(code=500, message=f"重置角色配置失败: {str(e)}", result=None)
|
||||||
|
|||||||
125
backend/app/services/glm_service.py
Normal file
125
backend/app/services/glm_service.py
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
"""
|
||||||
|
GLM聊天服务,包含角色配置管理
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
from app.models.glm_role import GLMRole
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class GLMService:
|
||||||
|
"""GLM聊天服务,包含角色配置管理"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._current_role: Optional[GLMRole] = None
|
||||||
|
|
||||||
|
async def get_current_role(self) -> GLMRole:
|
||||||
|
"""获取当前激活的角色配置"""
|
||||||
|
if self._current_role is None:
|
||||||
|
self._current_role = await self._load_or_create_role()
|
||||||
|
return self._current_role
|
||||||
|
|
||||||
|
async def update_role(self, role_data: dict) -> GLMRole:
|
||||||
|
"""更新角色配置
|
||||||
|
|
||||||
|
Args:
|
||||||
|
role_data: 角色配置数据,包含name, description, skills, restrictions字段
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
更新后的GLMRole实例
|
||||||
|
"""
|
||||||
|
role = await self.get_current_role()
|
||||||
|
|
||||||
|
# 更新字段
|
||||||
|
if "name" in role_data:
|
||||||
|
role.name = role_data["name"]
|
||||||
|
if "description" in role_data:
|
||||||
|
role.description = role_data["description"]
|
||||||
|
if "skills" in role_data:
|
||||||
|
role.skills = role_data["skills"]
|
||||||
|
if "restrictions" in role_data:
|
||||||
|
role.restrictions = role_data["restrictions"]
|
||||||
|
|
||||||
|
role.update_timestamp()
|
||||||
|
await role.save()
|
||||||
|
|
||||||
|
# 更新缓存
|
||||||
|
self._current_role = role
|
||||||
|
|
||||||
|
logger.info(f"角色配置已更新: {role.name}")
|
||||||
|
return role
|
||||||
|
|
||||||
|
async def _load_or_create_role(self) -> GLMRole:
|
||||||
|
"""加载或创建默认角色"""
|
||||||
|
# 1. 尝试获取激活的角色
|
||||||
|
active_role = await GLMRole.find_one(GLMRole.is_active == True)
|
||||||
|
if active_role:
|
||||||
|
logger.info(f"加载激活角色: {active_role.name}")
|
||||||
|
return active_role
|
||||||
|
|
||||||
|
# 2. 尝试获取默认角色
|
||||||
|
default_role = await GLMRole.find_one(GLMRole.is_default == True)
|
||||||
|
if default_role:
|
||||||
|
default_role.is_active = True
|
||||||
|
await default_role.save()
|
||||||
|
logger.info(f"加载默认角色并激活: {default_role.name}")
|
||||||
|
return default_role
|
||||||
|
|
||||||
|
# 3. 创建默认角色
|
||||||
|
logger.info("创建默认角色配置")
|
||||||
|
return await self._create_default_role()
|
||||||
|
|
||||||
|
async def _create_default_role(self) -> GLMRole:
|
||||||
|
"""创建默认角色配置"""
|
||||||
|
# 导入DEFAULT_ROLE_CONFIG
|
||||||
|
from app.routes.glm_routes import DEFAULT_ROLE_CONFIG
|
||||||
|
|
||||||
|
role = GLMRole(
|
||||||
|
name=DEFAULT_ROLE_CONFIG.name,
|
||||||
|
description=DEFAULT_ROLE_CONFIG.description,
|
||||||
|
skills=DEFAULT_ROLE_CONFIG.skills,
|
||||||
|
restrictions=DEFAULT_ROLE_CONFIG.restrictions,
|
||||||
|
is_active=True,
|
||||||
|
is_default=True,
|
||||||
|
)
|
||||||
|
await role.save()
|
||||||
|
|
||||||
|
logger.info(f"默认角色创建成功: {role.name}")
|
||||||
|
return role
|
||||||
|
|
||||||
|
async def ensure_default_role(self):
|
||||||
|
"""确保存在默认角色配置(应用启动时调用)"""
|
||||||
|
try:
|
||||||
|
role = await self._load_or_create_role()
|
||||||
|
logger.info(f"默认角色已确保存在: {role.name}")
|
||||||
|
return role
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"确保默认角色存在时出错: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def close(self):
|
||||||
|
"""关闭服务,清理资源"""
|
||||||
|
self._current_role = None
|
||||||
|
logger.info("GLM服务已关闭")
|
||||||
|
|
||||||
|
|
||||||
|
# 全局服务实例(单例模式)
|
||||||
|
_glm_service: Optional[GLMService] = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_glm_service() -> GLMService:
|
||||||
|
"""获取GLM服务实例(单例)"""
|
||||||
|
global _glm_service
|
||||||
|
if _glm_service is None:
|
||||||
|
_glm_service = GLMService()
|
||||||
|
return _glm_service
|
||||||
|
|
||||||
|
|
||||||
|
def close_glm_service():
|
||||||
|
"""关闭GLM服务"""
|
||||||
|
global _glm_service
|
||||||
|
if _glm_service:
|
||||||
|
_glm_service.close()
|
||||||
|
_glm_service = None
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"access_token": "TfPE_kJUsbokpSRqeeDNW5-S-lREfLNrpITU42JkTaHhjDqasI6dHAkZ4_63A1-qTmZb9W1KzqZZ9d11QFQMV1hhBK7M_33Qt5kU8rHyyXxj_2raPgEG2UIexe3txVNhjLY872z8R0HXB_pwiacTgJuYJtJvgPRum0G9taHftuoh6Pwcy_tiewe5jlhwdjy5SDOOMUO8LzadhRAHrx0xGQ",
|
"access_token": "cB51r2D8h73vFEY2WOUOpT7oKOAsTrYzh5kb_xcXTbLsw69U-Wxy9KnM_ealJEef_XdrPyqXcc82IYiJFIFVnwEXy65wF3xQmtm3JxvGq293eSDnNvuxWWRFjoQSf7ani47d_GMAELYoaLcM4S7kFPJxoq7kZz0g2dizOQ0vKiApNgQs3B_nxqm8cAuCgZuSw0JJDPAYZyci4lnRCVe0ww",
|
||||||
"fetch_time": 1765394181.8370981,
|
"fetch_time": 1765476407.16114,
|
||||||
"expires_in": 7200,
|
"expires_in": 7200,
|
||||||
"expiry_time": 1765400181.8370981,
|
"expiry_time": 1765482407.16114,
|
||||||
"ticket_data": {
|
"ticket_data": {
|
||||||
"ticket": "EVSxsqHHAbWPlLvl91IqbA==",
|
"ticket": "fkOdR8Y0baUUdG33FhsIfg==",
|
||||||
"fetch_time": 1765394182.1750567,
|
"fetch_time": 1765476407.4518044,
|
||||||
"expires_in": 7200,
|
"expires_in": 7200,
|
||||||
"expiry_time": 1765401382.1750567
|
"expiry_time": 1765483607.4518046
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
32
backstage/eslint.config.js
Normal file
32
backstage/eslint.config.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{ ignores: ['dist'] },
|
||||||
|
{
|
||||||
|
files: ['**/*.{js,jsx}'],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2022,
|
||||||
|
globals: globals.browser,
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
ecmaFeatures: { jsx: true },
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
'react-hooks': reactHooks,
|
||||||
|
'react-refresh': reactRefresh,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...js.configs.recommended.rules,
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
13
backstage/index.html
Normal file
13
backstage/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>后台管理系统 - 会话存档管理</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
32
backstage/package.json
Normal file
32
backstage/package.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "backstage",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"lint": "eslint ."
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
|
"axios": "^1.13.2",
|
||||||
|
"daisyui": "^5.5.5",
|
||||||
|
"react": "^19.2.0",
|
||||||
|
"react-dom": "^19.2.0",
|
||||||
|
"react-router-dom": "^7.3.0",
|
||||||
|
"tailwindcss": "^4.1.18"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.39.1",
|
||||||
|
"@types/react": "^19.2.5",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"@vitejs/plugin-react": "^5.1.1",
|
||||||
|
"eslint": "^9.39.1",
|
||||||
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.24",
|
||||||
|
"globals": "^16.5.0",
|
||||||
|
"vite": "^7.2.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
2380
backstage/pnpm-lock.yaml
generated
Normal file
2380
backstage/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
26
backstage/src/App.jsx
Normal file
26
backstage/src/App.jsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'
|
||||||
|
import Layout from './components/layout/Layout'
|
||||||
|
import Dashboard from './pages/Dashboard'
|
||||||
|
import Messages from './pages/Messages'
|
||||||
|
import Settings from './pages/Settings'
|
||||||
|
import Login from './pages/Login'
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
// 简单的认证检查(实际项目中应该使用更完善的认证方案)
|
||||||
|
const isAuthenticated = true // 暂时设为 true 以便测试
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Router basename="/chat_bm">
|
||||||
|
<Routes>
|
||||||
|
<Route path="/login" element={<Login />} />
|
||||||
|
<Route path="/" element={isAuthenticated ? <Layout /> : <Navigate to="/login" />}>
|
||||||
|
<Route index element={<Dashboard />} />
|
||||||
|
<Route path="messages" element={<Messages />} />
|
||||||
|
<Route path="settings" element={<Settings />} />
|
||||||
|
</Route>
|
||||||
|
</Routes>
|
||||||
|
</Router>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
||||||
115
backstage/src/components/layout/Layout.jsx
Normal file
115
backstage/src/components/layout/Layout.jsx
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import { Outlet, Link, useLocation } from 'react-router-dom'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
const Layout = () => {
|
||||||
|
const location = useLocation()
|
||||||
|
const [sidebarOpen, setSidebarOpen] = useState(true)
|
||||||
|
|
||||||
|
const navigation = [
|
||||||
|
{ name: '仪表板', href: '/', icon: '🏠', current: location.pathname === '/' },
|
||||||
|
{ name: '消息管理', href: '/messages', icon: '💬', current: location.pathname === '/messages' },
|
||||||
|
{ name: '系统设置', href: '/settings', icon: '⚙️', current: location.pathname === '/settings' },
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-base-100">
|
||||||
|
{/* 侧边栏 */}
|
||||||
|
<div className={`fixed inset-y-0 left-0 z-50 w-64 bg-base-200 shadow-xl transition-transform duration-300 ${sidebarOpen ? 'translate-x-0' : '-translate-x-full'}`}>
|
||||||
|
<div className="flex h-16 items-center justify-between border-b border-base-300 px-4">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="text-2xl font-bold text-primary">后台管理</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setSidebarOpen(false)}
|
||||||
|
className="btn btn-ghost btn-sm"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav className="flex-1 space-y-1 p-4">
|
||||||
|
{navigation.map((item) => (
|
||||||
|
<Link
|
||||||
|
key={item.name}
|
||||||
|
to={item.href}
|
||||||
|
className={`flex items-center rounded-lg px-3 py-2 text-sm font-medium transition-colors ${
|
||||||
|
item.current
|
||||||
|
? 'bg-primary text-primary-content'
|
||||||
|
: 'text-base-content hover:bg-base-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="mr-3 text-lg">{item.icon}</span>
|
||||||
|
{item.name}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div className="border-t border-base-300 p-4">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="avatar placeholder">
|
||||||
|
<div className="bg-neutral text-neutral-content w-8 rounded-full">
|
||||||
|
<span className="text-xs">AD</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="ml-3">
|
||||||
|
<p className="text-sm font-medium">管理员</p>
|
||||||
|
<p className="text-xs text-base-content/70">admin@example.com</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 主内容区 */}
|
||||||
|
<div className={`transition-all duration-300 ${sidebarOpen ? 'ml-64' : 'ml-0'}`}>
|
||||||
|
{/* 顶部导航栏 */}
|
||||||
|
<header className="sticky top-0 z-40 flex h-16 items-center justify-between border-b border-base-300 bg-base-100 px-6 shadow-sm">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<button
|
||||||
|
onClick={() => setSidebarOpen(!sidebarOpen)}
|
||||||
|
className="btn btn-ghost btn-sm mr-4"
|
||||||
|
>
|
||||||
|
{sidebarOpen ? '◀' : '▶'}
|
||||||
|
</button>
|
||||||
|
<h1 className="text-lg font-semibold">
|
||||||
|
{navigation.find(item => item.current)?.name || '后台管理系统'}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<button className="btn btn-ghost btn-sm">
|
||||||
|
🔔
|
||||||
|
<span className="badge badge-xs badge-primary absolute -top-1 -right-1">3</span>
|
||||||
|
</button>
|
||||||
|
<div className="dropdown dropdown-end">
|
||||||
|
<div tabIndex={0} role="button" className="btn btn-ghost btn-sm">
|
||||||
|
<div className="avatar placeholder">
|
||||||
|
<div className="bg-neutral text-neutral-content w-8 rounded-full">
|
||||||
|
<span className="text-xs">AD</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul tabIndex={0} className="dropdown-content menu bg-base-100 rounded-box z-50 w-52 p-2 shadow">
|
||||||
|
<li><a>个人资料</a></li>
|
||||||
|
<li><a>修改密码</a></li>
|
||||||
|
<li className="divider my-1"></li>
|
||||||
|
<li><a className="text-error">退出登录</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* 页面内容 */}
|
||||||
|
<main className="p-6">
|
||||||
|
<Outlet />
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{/* 页脚 */}
|
||||||
|
<footer className="border-t border-base-300 p-4 text-center text-sm text-base-content/70">
|
||||||
|
<p>© 2025 后台管理系统 - 会话存档管理 | 版本 1.0.0</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Layout
|
||||||
35
backstage/src/components/ui/DocumentationViewer.jsx
Normal file
35
backstage/src/components/ui/DocumentationViewer.jsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
const DocumentationViewer = () => {
|
||||||
|
const [library, setLibrary] = useState('react')
|
||||||
|
const [mode, setMode] = useState('code')
|
||||||
|
const [topic, setTopic] = useState('')
|
||||||
|
const [page, setPage] = useState(1)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [docs, setDocs] = useState(null)
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
|
const libraries = [
|
||||||
|
{ id: 'react', name: 'React', description: '用于构建用户界面的 JavaScript 库' },
|
||||||
|
{ id: 'tailwindcss', name: 'Tailwind CSS', description: '实用优先的 CSS 框架' },
|
||||||
|
{ id: 'daisyui', name: 'DaisyUI', description: 'Tailwind CSS 组件库' },
|
||||||
|
{ id: 'vite', name: 'Vite', description: '下一代前端工具' },
|
||||||
|
{ id: 'axios', name: 'Axios', description: '基于 Promise 的 HTTP 客户端' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const modes = [
|
||||||
|
{ id: 'code', name: '代码文档', description: 'API 参考和代码示例' },
|
||||||
|
{ id: 'info', name: '概念指南', description: '概念性指南和架构信息' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const handleFetchDocs = async () => {
|
||||||
|
setLoading(true)
|
||||||
|
setError('')
|
||||||
|
setDocs(null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 这里应该调用 Context7 MCP 工具
|
||||||
|
// 由于我们无法直接在前端调用 MCP 工具,这里模拟 API 调用
|
||||||
|
// 实际项目中应该通过后端代理调用 MCP 工具
|
||||||
|
|
||||||
|
// 模拟延迟
|
||||||
39
backstage/src/index.css
Normal file
39
backstage/src/index.css
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
/* DaisyUI 主题配置 */
|
||||||
|
@plugin "daisyui" {
|
||||||
|
themes: light --default, dark --prefersdark;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 自定义样式 */
|
||||||
|
:root {
|
||||||
|
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-width: 320px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滚动条样式 */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #888;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
10
backstage/src/main.jsx
Normal file
10
backstage/src/main.jsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom/client'
|
||||||
|
import App from './App.jsx'
|
||||||
|
import './index.css'
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
)
|
||||||
167
backstage/src/pages/Dashboard.jsx
Normal file
167
backstage/src/pages/Dashboard.jsx
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
const Dashboard = () => {
|
||||||
|
const stats = [
|
||||||
|
{ title: '总消息数', value: '1,234', change: '+12%', icon: '💬', color: 'bg-primary' },
|
||||||
|
{ title: '今日新增', value: '56', change: '+5%', icon: '📈', color: 'bg-success' },
|
||||||
|
{ title: '用户数', value: '89', change: '+8%', icon: '👥', color: 'bg-info' },
|
||||||
|
{ title: '加载成功率', value: '98%', change: '+2%', icon: '✅', color: 'bg-warning' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const recentActivities = [
|
||||||
|
{ id: 1, user: '张三', action: '加载了会话存档', time: '10分钟前', type: 'success' },
|
||||||
|
{ id: 2, user: '李四', action: '查看了消息详情', time: '30分钟前', type: 'info' },
|
||||||
|
{ id: 3, user: '王五', action: '更新了系统设置', time: '1小时前', type: 'warning' },
|
||||||
|
{ id: 4, user: '赵六', action: '导出了消息数据', time: '2小时前', type: 'success' },
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* 欢迎标题 */}
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold">欢迎回来,管理员</h1>
|
||||||
|
<p className="text-base-content/70 mt-2">这里是会话存档管理系统的控制面板</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 统计卡片 */}
|
||||||
|
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
|
||||||
|
{stats.map((stat, index) => (
|
||||||
|
<div key={index} className="card bg-base-100 shadow">
|
||||||
|
<div className="card-body">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-base-content/70">{stat.title}</p>
|
||||||
|
<p className="text-2xl font-bold mt-1">{stat.value}</p>
|
||||||
|
<p className="text-xs mt-1">
|
||||||
|
<span className="text-success">{stat.change}</span> 较昨日
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className={`${stat.color} rounded-lg p-3`}>
|
||||||
|
<span className="text-2xl">{stat.icon}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 快速操作 */}
|
||||||
|
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||||
|
{/* 最近活动 */}
|
||||||
|
<div className="card bg-base-100 shadow">
|
||||||
|
<div className="card-body">
|
||||||
|
<h2 className="card-title">最近活动</h2>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{recentActivities.map((activity) => (
|
||||||
|
<div key={activity.id} className="flex items-center justify-between border-b border-base-300 pb-3 last:border-0">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className={`avatar placeholder ${activity.type === 'success' ? 'bg-success' : activity.type === 'warning' ? 'bg-warning' : 'bg-info'}`}>
|
||||||
|
<div className="text-white w-8 rounded-full">
|
||||||
|
<span className="text-xs">{activity.user.charAt(0)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="ml-3">
|
||||||
|
<p className="font-medium">{activity.user}</p>
|
||||||
|
<p className="text-sm text-base-content/70">{activity.action}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm text-base-content/70">{activity.time}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 快速操作卡片 */}
|
||||||
|
<div className="card bg-base-100 shadow">
|
||||||
|
<div className="card-body">
|
||||||
|
<h2 className="card-title">快速操作</h2>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<button className="btn btn-primary">
|
||||||
|
<span className="mr-2">📥</span>
|
||||||
|
加载存档
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-outline">
|
||||||
|
<span className="mr-2">📊</span>
|
||||||
|
查看统计
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-outline">
|
||||||
|
<span className="mr-2">🔍</span>
|
||||||
|
搜索消息
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-outline">
|
||||||
|
<span className="mr-2">⚙️</span>
|
||||||
|
系统设置
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="divider my-4"></div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="font-medium mb-2">系统状态</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>后端服务</span>
|
||||||
|
<span className="badge badge-success">运行中</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>数据库连接</span>
|
||||||
|
<span className="badge badge-success">正常</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>API 可用性</span>
|
||||||
|
<span className="badge badge-success">100%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 消息类型分布 */}
|
||||||
|
<div className="card bg-base-100 shadow">
|
||||||
|
<div className="card-body">
|
||||||
|
<h2 className="card-title">消息类型分布</h2>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>消息类型</th>
|
||||||
|
<th>数量</th>
|
||||||
|
<th>占比</th>
|
||||||
|
<th>趋势</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>文本消息</td>
|
||||||
|
<td>856</td>
|
||||||
|
<td>69%</td>
|
||||||
|
<td className="text-success">↗ 增长</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>图片消息</td>
|
||||||
|
<td>234</td>
|
||||||
|
<td>19%</td>
|
||||||
|
<td className="text-warning">→ 稳定</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>文件消息</td>
|
||||||
|
<td>98</td>
|
||||||
|
<td>8%</td>
|
||||||
|
<td className="text-success">↗ 增长</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>语音消息</td>
|
||||||
|
<td>46</td>
|
||||||
|
<td>4%</td>
|
||||||
|
<td className="text-error">↘ 下降</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Dashboard
|
||||||
143
backstage/src/pages/Login.jsx
Normal file
143
backstage/src/pages/Login.jsx
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
|
const Login = () => {
|
||||||
|
const [username, setUsername] = useState('')
|
||||||
|
const [password, setPassword] = useState('')
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setError('')
|
||||||
|
|
||||||
|
if (!username || !password) {
|
||||||
|
setError('请输入用户名和密码')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
// 模拟登录 API 调用
|
||||||
|
setTimeout(() => {
|
||||||
|
setLoading(false)
|
||||||
|
if (username === 'admin' && password === 'admin123') {
|
||||||
|
// 登录成功,跳转到首页
|
||||||
|
navigate('/')
|
||||||
|
} else {
|
||||||
|
setError('用户名或密码错误')
|
||||||
|
}
|
||||||
|
}, 1500)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-primary/10 to-secondary/10 flex items-center justify-center p-4">
|
||||||
|
<div className="card bg-base-100 shadow-2xl w-full max-w-md">
|
||||||
|
<div className="card-body">
|
||||||
|
{/* Logo 和标题 */}
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary text-primary-content text-2xl font-bold mb-4">
|
||||||
|
BS
|
||||||
|
</div>
|
||||||
|
<h1 className="text-3xl font-bold">后台管理系统</h1>
|
||||||
|
<p className="text-base-content/70 mt-2">会话存档管理平台</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 登录表单 */}
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
{error && (
|
||||||
|
<div className="alert alert-error">
|
||||||
|
<span>{error}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">用户名</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="input input-bordered"
|
||||||
|
placeholder="请输入用户名"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">密码</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
className="input input-bordered"
|
||||||
|
placeholder="请输入密码"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
<label className="label">
|
||||||
|
<a href="#" className="label-text-alt link link-hover">忘记密码?</a>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label cursor-pointer justify-start gap-3">
|
||||||
|
<input type="checkbox" className="checkbox checkbox-primary" defaultChecked />
|
||||||
|
<span className="label-text">记住我</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-control mt-6">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="btn btn-primary"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<span className="loading loading-spinner loading-sm"></span>
|
||||||
|
登录中...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'登录'
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* 分隔线 */}
|
||||||
|
<div className="divider my-6">或</div>
|
||||||
|
|
||||||
|
{/* 其他登录方式 */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<button className="btn btn-outline btn-block">
|
||||||
|
<span className="mr-2">👤</span>
|
||||||
|
使用访客账户登录
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-outline btn-block">
|
||||||
|
<span className="mr-2">🔑</span>
|
||||||
|
使用 API Token 登录
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 底部信息 */}
|
||||||
|
<div className="mt-8 text-center text-sm text-base-content/70">
|
||||||
|
<p>默认账户:admin / admin123</p>
|
||||||
|
<p className="mt-2">© 2025 后台管理系统 | 版本 1.0.0</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 背景装饰 */}
|
||||||
|
<div className="absolute inset-0 -z-10 overflow-hidden">
|
||||||
|
<div className="absolute -top-40 -right-40 w-80 h-80 bg-primary/20 rounded-full blur-3xl"></div>
|
||||||
|
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-secondary/20 rounded-full blur-3xl"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Login
|
||||||
265
backstage/src/pages/Messages.jsx
Normal file
265
backstage/src/pages/Messages.jsx
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
const Messages = () => {
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [messages, setMessages] = useState([
|
||||||
|
{ id: 1, from: '张三', content: '你好,我想了解一下产品详情', time: '2025-12-12 10:30', type: 'text', room: '单聊' },
|
||||||
|
{ id: 2, from: '李四', content: '这是会议纪要文件', time: '2025-12-12 09:15', type: 'file', room: '群聊-项目组' },
|
||||||
|
{ id: 3, from: '王五', content: '产品图片已发送', time: '2025-12-11 16:45', type: 'image', room: '单聊' },
|
||||||
|
{ id: 4, from: '赵六', content: '关于下周会议安排', time: '2025-12-11 14:20', type: 'text', room: '群聊-管理组' },
|
||||||
|
{ id: 5, from: '钱七', content: '语音消息', time: '2025-12-10 11:10', type: 'voice', room: '单聊' },
|
||||||
|
])
|
||||||
|
|
||||||
|
const handleLoadArchive = async () => {
|
||||||
|
setLoading(true)
|
||||||
|
// 模拟 API 调用
|
||||||
|
setTimeout(() => {
|
||||||
|
setLoading(false)
|
||||||
|
alert('会话存档加载成功!')
|
||||||
|
}, 1500)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSearch = (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
// 搜索逻辑
|
||||||
|
alert('搜索功能开发中...')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* 页面标题和操作 */}
|
||||||
|
<div className="flex flex-col justify-between gap-4 sm:flex-row sm:items-center">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold">消息管理</h1>
|
||||||
|
<p className="text-base-content/70 mt-2">管理企业微信会话存档消息</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<button
|
||||||
|
onClick={handleLoadArchive}
|
||||||
|
disabled={loading}
|
||||||
|
className="btn btn-primary"
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<span className="loading loading-spinner loading-sm"></span>
|
||||||
|
加载中...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span className="mr-2">📥</span>
|
||||||
|
加载会话存档
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-outline">
|
||||||
|
<span className="mr-2">📊</span>
|
||||||
|
查看统计
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-outline">
|
||||||
|
<span className="mr-2">📤</span>
|
||||||
|
导出数据
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 搜索和过滤 */}
|
||||||
|
<div className="card bg-base-100 shadow">
|
||||||
|
<div className="card-body">
|
||||||
|
<form onSubmit={handleSearch} className="flex flex-col gap-4 md:flex-row">
|
||||||
|
<div className="flex-1">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="搜索消息内容、发送者..."
|
||||||
|
className="input input-bordered w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<select className="select select-bordered">
|
||||||
|
<option value="">所有消息类型</option>
|
||||||
|
<option value="text">文本消息</option>
|
||||||
|
<option value="image">图片消息</option>
|
||||||
|
<option value="file">文件消息</option>
|
||||||
|
<option value="voice">语音消息</option>
|
||||||
|
</select>
|
||||||
|
<select className="select select-bordered">
|
||||||
|
<option value="">所有聊天类型</option>
|
||||||
|
<option value="single">单聊</option>
|
||||||
|
<option value="group">群聊</option>
|
||||||
|
</select>
|
||||||
|
<button type="submit" className="btn btn-primary">
|
||||||
|
<span className="mr-2">🔍</span>
|
||||||
|
搜索
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 消息列表 */}
|
||||||
|
<div className="card bg-base-100 shadow">
|
||||||
|
<div className="card-body">
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" className="checkbox" />
|
||||||
|
</label>
|
||||||
|
</th>
|
||||||
|
<th>发送者</th>
|
||||||
|
<th>消息内容</th>
|
||||||
|
<th>消息类型</th>
|
||||||
|
<th>聊天类型</th>
|
||||||
|
<th>时间</th>
|
||||||
|
<th>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{messages.map((message) => (
|
||||||
|
<tr key={message.id}>
|
||||||
|
<td>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" className="checkbox" />
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="avatar placeholder">
|
||||||
|
<div className="bg-neutral text-neutral-content w-8 rounded-full">
|
||||||
|
<span className="text-xs">{message.from.charAt(0)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-bold">{message.from}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div className="max-w-xs truncate" title={message.content}>
|
||||||
|
{message.content}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span className={`badge ${message.type === 'text' ? 'badge-info' : message.type === 'image' ? 'badge-success' : message.type === 'file' ? 'badge-warning' : 'badge-error'}`}>
|
||||||
|
{message.type === 'text' ? '文本' : message.type === 'image' ? '图片' : message.type === 'file' ? '文件' : '语音'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span className={`badge ${message.room.includes('群聊') ? 'badge-primary' : 'badge-secondary'}`}>
|
||||||
|
{message.room}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>{message.time}</td>
|
||||||
|
<td>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button className="btn btn-ghost btn-xs">
|
||||||
|
<span className="mr-1">👁️</span>
|
||||||
|
查看
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-ghost btn-xs">
|
||||||
|
<span className="mr-1">📋</span>
|
||||||
|
复制
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 分页 */}
|
||||||
|
<div className="flex items-center justify-between pt-4">
|
||||||
|
<div className="text-sm text-base-content/70">
|
||||||
|
显示 1-5 条,共 1234 条消息
|
||||||
|
</div>
|
||||||
|
<div className="join">
|
||||||
|
<button className="join-item btn btn-sm">«</button>
|
||||||
|
<button className="join-item btn btn-sm btn-active">1</button>
|
||||||
|
<button className="join-item btn btn-sm">2</button>
|
||||||
|
<button className="join-item btn btn-sm">3</button>
|
||||||
|
<button className="join-item btn btn-sm">4</button>
|
||||||
|
<button className="join-item btn btn-sm">»</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 统计信息 */}
|
||||||
|
<div className="grid grid-cols-1 gap-6 md:grid-cols-3">
|
||||||
|
<div className="card bg-base-100 shadow">
|
||||||
|
<div className="card-body">
|
||||||
|
<h3 className="card-title">今日消息统计</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>文本消息</span>
|
||||||
|
<span className="font-bold">45 条</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>图片消息</span>
|
||||||
|
<span className="font-bold">12 条</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>文件消息</span>
|
||||||
|
<span className="font-bold">8 条</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>语音消息</span>
|
||||||
|
<span className="font-bold">3 条</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card bg-base-100 shadow">
|
||||||
|
<div className="card-body">
|
||||||
|
<h3 className="card-title">加载状态</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between mb-1">
|
||||||
|
<span>上次加载时间</span>
|
||||||
|
<span className="font-bold">2025-12-12 10:30</span>
|
||||||
|
</div>
|
||||||
|
<progress className="progress progress-primary w-full" value="100" max="100"></progress>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between mb-1">
|
||||||
|
<span>加载成功率</span>
|
||||||
|
<span className="font-bold">98.5%</span>
|
||||||
|
</div>
|
||||||
|
<progress className="progress progress-success w-full" value="98.5" max="100"></progress>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card bg-base-100 shadow">
|
||||||
|
<div className="card-body">
|
||||||
|
<h3 className="card-title">快速操作</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<button className="btn btn-outline btn-block justify-start">
|
||||||
|
<span className="mr-2">🔄</span>
|
||||||
|
刷新消息列表
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-outline btn-block justify-start">
|
||||||
|
<span className="mr-2">🧹</span>
|
||||||
|
清理旧数据
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-outline btn-block justify-start">
|
||||||
|
<span className="mr-2">📋</span>
|
||||||
|
批量操作
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-outline btn-block justify-start">
|
||||||
|
<span className="mr-2">⚙️</span>
|
||||||
|
消息设置
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Messages
|
||||||
270
backstage/src/pages/Settings.jsx
Normal file
270
backstage/src/pages/Settings.jsx
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
const Settings = () => {
|
||||||
|
const [apiConfig, setApiConfig] = useState({
|
||||||
|
baseUrl: 'http://localhost:8000/api',
|
||||||
|
timeout: 10000,
|
||||||
|
retryCount: 3,
|
||||||
|
})
|
||||||
|
|
||||||
|
const [systemSettings, setSystemSettings] = useState({
|
||||||
|
autoLoad: true,
|
||||||
|
loadInterval: 30,
|
||||||
|
keepHistory: 90,
|
||||||
|
theme: 'light',
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSaveApiConfig = () => {
|
||||||
|
alert('API 配置已保存!')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSaveSystemSettings = () => {
|
||||||
|
alert('系统设置已保存!')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* 页面标题 */}
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold">系统设置</h1>
|
||||||
|
<p className="text-base-content/70 mt-2">配置系统参数和 API 设置</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* API 配置 */}
|
||||||
|
<div className="card bg-base-100 shadow">
|
||||||
|
<div className="card-body">
|
||||||
|
<h2 className="card-title">API 配置</h2>
|
||||||
|
<p className="text-base-content/70">配置后端 API 连接参数</p>
|
||||||
|
|
||||||
|
<div className="divider my-4"></div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">API 基础地址</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="input input-bordered"
|
||||||
|
value={apiConfig.baseUrl}
|
||||||
|
onChange={(e) => setApiConfig({...apiConfig, baseUrl: e.target.value})}
|
||||||
|
placeholder="http://localhost:8000/api"
|
||||||
|
/>
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text-alt">后端 API 服务的基础地址</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">请求超时时间(毫秒)</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="input input-bordered"
|
||||||
|
value={apiConfig.timeout}
|
||||||
|
onChange={(e) => setApiConfig({...apiConfig, timeout: parseInt(e.target.value)})}
|
||||||
|
min="1000"
|
||||||
|
max="30000"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">重试次数</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="input input-bordered"
|
||||||
|
value={apiConfig.retryCount}
|
||||||
|
onChange={(e) => setApiConfig({...apiConfig, retryCount: parseInt(e.target.value)})}
|
||||||
|
min="0"
|
||||||
|
max="10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label cursor-pointer justify-start gap-3">
|
||||||
|
<input type="checkbox" className="toggle toggle-primary" defaultChecked />
|
||||||
|
<span className="label-text">启用请求日志</span>
|
||||||
|
</label>
|
||||||
|
<label className="label cursor-pointer justify-start gap-3">
|
||||||
|
<input type="checkbox" className="toggle toggle-primary" defaultChecked />
|
||||||
|
<span className="label-text">启用错误重试</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<button onClick={handleSaveApiConfig} className="btn btn-primary">
|
||||||
|
<span className="mr-2">💾</span>
|
||||||
|
保存 API 配置
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 系统设置 */}
|
||||||
|
<div className="card bg-base-100 shadow">
|
||||||
|
<div className="card-body">
|
||||||
|
<h2 className="card-title">系统设置</h2>
|
||||||
|
<p className="text-base-content/70">配置系统行为和外观</p>
|
||||||
|
|
||||||
|
<div className="divider my-4"></div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">自动加载间隔(分钟)</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="input input-bordered"
|
||||||
|
value={systemSettings.loadInterval}
|
||||||
|
onChange={(e) => setSystemSettings({...systemSettings, loadInterval: parseInt(e.target.value)})}
|
||||||
|
min="1"
|
||||||
|
max="1440"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">历史数据保留天数</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="input input-bordered"
|
||||||
|
value={systemSettings.keepHistory}
|
||||||
|
onChange={(e) => setSystemSettings({...systemSettings, keepHistory: parseInt(e.target.value)})}
|
||||||
|
min="1"
|
||||||
|
max="365"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">主题设置</span>
|
||||||
|
</label>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<label className="flex cursor-pointer items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="theme"
|
||||||
|
className="radio radio-primary"
|
||||||
|
checked={systemSettings.theme === 'light'}
|
||||||
|
onChange={() => setSystemSettings({...systemSettings, theme: 'light'})}
|
||||||
|
/>
|
||||||
|
<span>浅色主题</span>
|
||||||
|
</label>
|
||||||
|
<label className="flex cursor-pointer items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="theme"
|
||||||
|
className="radio radio-primary"
|
||||||
|
checked={systemSettings.theme === 'dark'}
|
||||||
|
onChange={() => setSystemSettings({...systemSettings, theme: 'dark'})}
|
||||||
|
/>
|
||||||
|
<span>深色主题</span>
|
||||||
|
</label>
|
||||||
|
<label className="flex cursor-pointer items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="theme"
|
||||||
|
className="radio radio-primary"
|
||||||
|
checked={systemSettings.theme === 'auto'}
|
||||||
|
onChange={() => setSystemSettings({...systemSettings, theme: 'auto'})}
|
||||||
|
/>
|
||||||
|
<span>自动切换</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label cursor-pointer justify-start gap-3">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="toggle toggle-primary"
|
||||||
|
checked={systemSettings.autoLoad}
|
||||||
|
onChange={(e) => setSystemSettings({...systemSettings, autoLoad: e.target.checked})}
|
||||||
|
/>
|
||||||
|
<span className="label-text">启用自动加载会话存档</span>
|
||||||
|
</label>
|
||||||
|
<label className="label cursor-pointer justify-start gap-3">
|
||||||
|
<input type="checkbox" className="toggle toggle-primary" defaultChecked />
|
||||||
|
<span className="label-text">启用消息通知</span>
|
||||||
|
</label>
|
||||||
|
<label className="label cursor-pointer justify-start gap-3">
|
||||||
|
<input type="checkbox" className="toggle toggle-primary" defaultChecked />
|
||||||
|
<span className="label-text">启用数据备份</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<button onClick={handleSaveSystemSettings} className="btn btn-primary">
|
||||||
|
<span className="mr-2">💾</span>
|
||||||
|
保存系统设置
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 系统信息 */}
|
||||||
|
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||||
|
<div className="card bg-base-100 shadow">
|
||||||
|
<div className="card-body">
|
||||||
|
<h2 className="card-title">系统信息</h2>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>前端版本</span>
|
||||||
|
<span className="font-mono">1.0.0</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>React 版本</span>
|
||||||
|
<span className="font-mono">19.2.0</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>构建时间</span>
|
||||||
|
<span className="font-mono">2025-12-12</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>运行环境</span>
|
||||||
|
<span className="font-mono">Production</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card bg-base-100 shadow">
|
||||||
|
<div className="card-body">
|
||||||
|
<h2 className="card-title">危险操作</h2>
|
||||||
|
<p className="text-base-content/70 mb-4">请谨慎操作以下功能</p>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<button className="btn btn-outline btn-error btn-block justify-start">
|
||||||
|
<span className="mr-2">🧹</span>
|
||||||
|
清理所有缓存
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-outline btn-error btn-block justify-start">
|
||||||
|
<span className="mr-2">🔄</span>
|
||||||
|
重置所有设置
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-outline btn-error btn-block justify-start">
|
||||||
|
<span className="mr-2">🗑️</span>
|
||||||
|
删除所有数据
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 text-xs text-error">
|
||||||
|
⚠️ 注意:这些操作不可逆,请确认后再执行
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Settings
|
||||||
123
backstage/src/services/api.js
Normal file
123
backstage/src/services/api.js
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
// API 基础配置
|
||||||
|
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000/api'
|
||||||
|
|
||||||
|
// 创建 axios 实例
|
||||||
|
const apiClient = axios.create({
|
||||||
|
baseURL: API_BASE_URL,
|
||||||
|
timeout: 10000,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 请求拦截器
|
||||||
|
apiClient.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
// 可以在这里添加认证 token
|
||||||
|
const token = localStorage.getItem('auth_token')
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 响应拦截器
|
||||||
|
apiClient.interceptors.response.use(
|
||||||
|
(response) => {
|
||||||
|
return response.data
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error('API 请求错误:', error)
|
||||||
|
|
||||||
|
if (error.response) {
|
||||||
|
// 服务器返回错误状态码
|
||||||
|
const { status, data } = error.response
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case 401:
|
||||||
|
// 未授权,跳转到登录页
|
||||||
|
localStorage.removeItem('auth_token')
|
||||||
|
window.location.href = '/login'
|
||||||
|
break
|
||||||
|
case 403:
|
||||||
|
console.error('权限不足:', data)
|
||||||
|
break
|
||||||
|
case 404:
|
||||||
|
console.error('资源不存在:', data)
|
||||||
|
break
|
||||||
|
case 500:
|
||||||
|
console.error('服务器内部错误:', data)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
console.error(`HTTP 错误 ${status}:`, data)
|
||||||
|
}
|
||||||
|
} else if (error.request) {
|
||||||
|
// 请求已发出但没有收到响应
|
||||||
|
console.error('网络错误,请检查网络连接')
|
||||||
|
} else {
|
||||||
|
// 请求配置错误
|
||||||
|
console.error('请求配置错误:', error.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 消息相关 API
|
||||||
|
export const messageApi = {
|
||||||
|
// 加载会话存档
|
||||||
|
loadMessages: (seq = 0, limit = 100) =>
|
||||||
|
apiClient.post('/messages/load', null, {
|
||||||
|
params: { seq, limit }
|
||||||
|
}),
|
||||||
|
|
||||||
|
// 获取消息列表
|
||||||
|
getMessages: (params = {}) =>
|
||||||
|
apiClient.get('/messages', { params }),
|
||||||
|
|
||||||
|
// 获取消息统计
|
||||||
|
getMessageStats: () =>
|
||||||
|
apiClient.get('/messages/stats'),
|
||||||
|
|
||||||
|
// 获取单个消息详情
|
||||||
|
getMessageById: (msgid) =>
|
||||||
|
apiClient.get(`/messages/${msgid}`),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 认证相关 API
|
||||||
|
export const authApi = {
|
||||||
|
// 登录
|
||||||
|
login: (username, password) =>
|
||||||
|
apiClient.post('/auth/login', { username, password }),
|
||||||
|
|
||||||
|
// 获取用户信息
|
||||||
|
getUserInfo: () =>
|
||||||
|
apiClient.get('/auth/user'),
|
||||||
|
|
||||||
|
// 刷新 token
|
||||||
|
refreshToken: () =>
|
||||||
|
apiClient.post('/auth/refresh'),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 系统相关 API
|
||||||
|
export const systemApi = {
|
||||||
|
// 获取系统状态
|
||||||
|
getSystemStatus: () =>
|
||||||
|
apiClient.get('/system/status'),
|
||||||
|
|
||||||
|
// 获取系统配置
|
||||||
|
getSystemConfig: () =>
|
||||||
|
apiClient.get('/system/config'),
|
||||||
|
|
||||||
|
// 更新系统配置
|
||||||
|
updateSystemConfig: (config) =>
|
||||||
|
apiClient.put('/system/config', config),
|
||||||
|
}
|
||||||
|
|
||||||
|
export default apiClient
|
||||||
17
backstage/vite.config.js
Normal file
17
backstage/vite.config.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
tailwindcss(),
|
||||||
|
react(),
|
||||||
|
],
|
||||||
|
base: '/chat_bm',
|
||||||
|
server: {
|
||||||
|
port: 3020,
|
||||||
|
host: '0.0.0.0',
|
||||||
|
allowedHosts: ['mp.yizhezhe.com']
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -10,14 +10,13 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@wecom/jssdk": "^2.3.3",
|
"@wecom/jssdk": "^2.3.3",
|
||||||
"autoprefixer": "^10.4.22",
|
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"daisyui": "^5.5.5",
|
"daisyui": "^5.5.5",
|
||||||
"postcss": "^8.5.6",
|
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"tailwindcss": "^3.4.0"
|
"tailwindcss": "^4.1.18"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.1",
|
"@eslint/js": "^9.39.1",
|
||||||
|
|||||||
719
frontend/pnpm-lock.yaml
generated
719
frontend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +0,0 @@
|
|||||||
export default {
|
|
||||||
plugins: {
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -2,6 +2,7 @@ import { useState, useEffect, useRef, useCallback } from 'react';
|
|||||||
import { useWeCom } from './hooks/useWeCom';
|
import { useWeCom } from './hooks/useWeCom';
|
||||||
import { workflowService, customerService } from './services/wecomService';
|
import { workflowService, customerService } from './services/wecomService';
|
||||||
import WorkflowProgress from './components/WorkflowProgress';
|
import WorkflowProgress from './components/WorkflowProgress';
|
||||||
|
import RolePromptDrawer from './components/RolePromptDrawer';
|
||||||
import { getStepNames } from './constants/workflowSteps';
|
import { getStepNames } from './constants/workflowSteps';
|
||||||
import { getSceneDescription } from './constants/wecomScenes';
|
import { getSceneDescription } from './constants/wecomScenes';
|
||||||
|
|
||||||
@@ -24,6 +25,9 @@ function App() {
|
|||||||
// 执行计数
|
// 执行计数
|
||||||
const [executionCount, setExecutionCount] = useState(0);
|
const [executionCount, setExecutionCount] = useState(0);
|
||||||
|
|
||||||
|
// 角色提示词抽屉状态
|
||||||
|
const [rolePromptDrawerOpen, setRolePromptDrawerOpen] = useState(false);
|
||||||
|
|
||||||
const messagesEndRef = useRef(null);
|
const messagesEndRef = useRef(null);
|
||||||
const hasAutoExecutedRef = useRef(false);
|
const hasAutoExecutedRef = useRef(false);
|
||||||
|
|
||||||
@@ -346,11 +350,21 @@ function App() {
|
|||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<button
|
<button
|
||||||
className="hidden sm:inline px-3 py-1 bg-white/20 rounded text-sm hover:bg-white/30 transition-colors"
|
className="hidden sm:inline px-3 py-1 bg-white/20 rounded text-sm hover:bg-white/30 transition-colors flex items-center space-x-1"
|
||||||
onClick={executeSmartReplyWorkflow}
|
onClick={executeSmartReplyWorkflow}
|
||||||
disabled={workflowStatus.isRunning || !weCom.sdkReady}
|
disabled={workflowStatus.isRunning || !weCom.sdkReady}
|
||||||
>
|
>
|
||||||
{workflowStatus.isRunning ? '执行中...' : '执行工作流'}
|
<span>{workflowStatus.isRunning ? '⏳' : '🔄️'}</span>
|
||||||
|
<span>{workflowStatus.isRunning ? '执行中...' : '执行工作流'}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="hidden sm:inline px-3 py-1 bg-white/20 rounded text-sm hover:bg-white/30 transition-colors flex items-center space-x-1"
|
||||||
|
onClick={() => setRolePromptDrawerOpen(true)}
|
||||||
|
title="配置AI角色提示词"
|
||||||
|
>
|
||||||
|
<span>🤖</span>
|
||||||
|
<span>角色配置</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* 移动端操作按钮 */}
|
{/* 移动端操作按钮 */}
|
||||||
@@ -363,6 +377,13 @@ function App() {
|
|||||||
>
|
>
|
||||||
{workflowStatus.isRunning ? '⏳' : '🔄️'}
|
{workflowStatus.isRunning ? '⏳' : '🔄️'}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
className="w-8 h-8 flex items-center justify-center bg-white/20 rounded hover:bg-white/30 transition-colors"
|
||||||
|
onClick={() => setRolePromptDrawerOpen(true)}
|
||||||
|
title="角色配置"
|
||||||
|
>
|
||||||
|
🤖
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -439,33 +460,41 @@ function App() {
|
|||||||
) : (
|
) : (
|
||||||
// 聊天消息列表
|
// 聊天消息列表
|
||||||
<div className="max-w-4xl mx-auto space-y-3 sm:space-y-4">
|
<div className="max-w-4xl mx-auto space-y-3 sm:space-y-4">
|
||||||
{messages.map((message, index) => (
|
{messages.map((message, index) => {
|
||||||
<div
|
// 确定justify类名
|
||||||
key={index}
|
let justifyClass = 'justify-start'; // 默认左对齐
|
||||||
className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'} ${
|
if (message.role === 'user') {
|
||||||
message.role === 'system' ? 'justify-center' : ''
|
justifyClass = 'justify-end'; // 用户消息右对齐
|
||||||
}`}
|
} else if (message.role === 'system') {
|
||||||
>
|
justifyClass = 'justify-center'; // 系统消息居中
|
||||||
<div className={`max-w-[85%] sm:max-w-[70%] rounded-2xl px-3 py-2 sm:px-4 sm:py-3 ${
|
}
|
||||||
message.role === 'user'
|
|
||||||
? 'bg-blue-600 text-white rounded-br-none'
|
return (
|
||||||
: message.role === 'system'
|
<div
|
||||||
? 'bg-gray-100 text-gray-700 border border-gray-200'
|
key={index}
|
||||||
: 'bg-white border border-gray-200 rounded-bl-none shadow-sm'
|
className={`flex ${justifyClass}`}
|
||||||
}`}>
|
>
|
||||||
<div className="text-sm leading-relaxed break-words">
|
<div className={`max-w-[85%] sm:max-w-[70%] rounded-2xl px-3 py-2 sm:px-4 sm:py-3 ${
|
||||||
{message.content}
|
message.role === 'user'
|
||||||
</div>
|
? 'bg-blue-600 text-white rounded-br-none'
|
||||||
{/* <div className={`text-xs mt-1 ${
|
: message.role === 'system'
|
||||||
message.role === 'user' ? 'text-blue-100' :
|
? 'bg-gray-100 text-gray-700 border border-gray-200'
|
||||||
message.role === 'system' ? 'text-gray-500' :
|
: 'bg-white border border-gray-200 rounded-bl-none shadow-sm'
|
||||||
'text-gray-500'
|
|
||||||
}`}>
|
}`}>
|
||||||
{message.timestamp}
|
<div className="text-sm leading-relaxed break-words">
|
||||||
</div> */}
|
{message.content}
|
||||||
|
</div>
|
||||||
|
{/* <div className={`text-xs mt-1 ${
|
||||||
|
message.role === 'user' ? 'text-blue-100' :
|
||||||
|
message.role === 'system' ? 'text-gray-500' :
|
||||||
|
'text-gray-500'
|
||||||
|
}`}>
|
||||||
|
{message.timestamp}
|
||||||
|
</div> */}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
))}
|
})}
|
||||||
|
|
||||||
{/* 加载状态 */}
|
{/* 加载状态 */}
|
||||||
{workflowStatus.isRunning && (
|
{workflowStatus.isRunning && (
|
||||||
@@ -591,6 +620,12 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 角色提示词配置抽屉 */}
|
||||||
|
<RolePromptDrawer
|
||||||
|
isOpen={rolePromptDrawerOpen}
|
||||||
|
onClose={() => setRolePromptDrawerOpen(false)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
448
frontend/src/components/RolePromptDrawer.jsx
Normal file
448
frontend/src/components/RolePromptDrawer.jsx
Normal file
@@ -0,0 +1,448 @@
|
|||||||
|
import { useState, useEffect, useRef } from 'react';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://mp.yizhezhe.com/api';
|
||||||
|
|
||||||
|
const apiClient = axios.create({
|
||||||
|
baseURL: API_BASE_URL,
|
||||||
|
timeout: 10000,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const RolePromptDrawer = ({ isOpen, onClose }) => {
|
||||||
|
// 引用滚动容器
|
||||||
|
const drawerContentRef = useRef(null);
|
||||||
|
|
||||||
|
// 表单数据状态
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
skills: '',
|
||||||
|
restrictions: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
// 加载和保存状态
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
const [isResetting, setIsResetting] = useState(false);
|
||||||
|
const [saveStatus, setSaveStatus] = useState({ type: null, message: '' });
|
||||||
|
// Popover确认状态
|
||||||
|
const [showResetConfirm, setShowResetConfirm] = useState(false);
|
||||||
|
|
||||||
|
// 加载角色配置
|
||||||
|
const loadRoleConfig = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get('/glm/role');
|
||||||
|
const result = response.data;
|
||||||
|
|
||||||
|
if (result.code === 200 && result.result) {
|
||||||
|
const roleConfig = result.result;
|
||||||
|
// 直接使用字符串(后端已返回字符串格式)
|
||||||
|
setFormData({
|
||||||
|
name: roleConfig.name || '',
|
||||||
|
description: roleConfig.description || '',
|
||||||
|
skills: roleConfig.skills || '',
|
||||||
|
restrictions: roleConfig.restrictions || ''
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error(result.message || '加载角色配置失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载角色配置失败:', error);
|
||||||
|
setSaveStatus({
|
||||||
|
type: 'error',
|
||||||
|
message: `加载失败: ${error.response?.data?.message || error.message}`
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存角色配置
|
||||||
|
const handleSave = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// 验证数据
|
||||||
|
if (!formData.name.trim() || !formData.description.trim()) {
|
||||||
|
setSaveStatus({ type: 'error', message: '角色名称和描述不能为空' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSaving(true);
|
||||||
|
setSaveStatus({ type: 'loading', message: '保存中...' });
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 直接发送字符串(后端期望字符串格式)
|
||||||
|
const roleConfigToSave = {
|
||||||
|
name: formData.name.trim(),
|
||||||
|
description: formData.description.trim(),
|
||||||
|
skills: formData.skills.trim(),
|
||||||
|
restrictions: formData.restrictions.trim()
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await apiClient.put('/glm/role', roleConfigToSave);
|
||||||
|
const result = response.data;
|
||||||
|
|
||||||
|
if (result.code === 200) {
|
||||||
|
setSaveStatus({ type: 'success', message: '保存成功!' });
|
||||||
|
|
||||||
|
// 滚动到顶部,让用户看到提示
|
||||||
|
if (drawerContentRef.current) {
|
||||||
|
drawerContentRef.current.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3秒后清除成功消息
|
||||||
|
setTimeout(() => {
|
||||||
|
setSaveStatus({ type: null, message: '' });
|
||||||
|
}, 3000);
|
||||||
|
} else {
|
||||||
|
throw new Error(result.message || '保存角色配置失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存角色配置失败:', error);
|
||||||
|
setSaveStatus({
|
||||||
|
type: 'error',
|
||||||
|
message: `保存失败: ${error.response?.data?.message || error.message}`
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置按钮点击 - 显示确认Popover
|
||||||
|
const handleResetButtonClick = () => {
|
||||||
|
setShowResetConfirm(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 取消重置
|
||||||
|
const handleCancelReset = () => {
|
||||||
|
setShowResetConfirm(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 确认重置到默认角色配置
|
||||||
|
const handleConfirmReset = async () => {
|
||||||
|
setShowResetConfirm(false);
|
||||||
|
|
||||||
|
setIsResetting(true);
|
||||||
|
setSaveStatus({ type: 'loading', message: '重置到默认配置中...' });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await apiClient.post('/glm/role/reset');
|
||||||
|
const result = response.data;
|
||||||
|
|
||||||
|
if (result.code === 200) {
|
||||||
|
setSaveStatus({ type: 'success', message: '已重置为默认角色配置!' });
|
||||||
|
|
||||||
|
// 重新加载角色配置
|
||||||
|
await loadRoleConfig();
|
||||||
|
|
||||||
|
// 滚动到顶部,让用户看到提示
|
||||||
|
if (drawerContentRef.current) {
|
||||||
|
drawerContentRef.current.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3秒后清除成功消息
|
||||||
|
setTimeout(() => {
|
||||||
|
setSaveStatus({ type: null, message: '' });
|
||||||
|
}, 3000);
|
||||||
|
} else {
|
||||||
|
throw new Error(result.message || '重置角色配置失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('重置角色配置失败:', error);
|
||||||
|
setSaveStatus({
|
||||||
|
type: 'error',
|
||||||
|
message: `重置失败: ${error.response?.data?.message || error.message}`
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsResetting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置表单
|
||||||
|
const handleReset = () => {
|
||||||
|
loadRoleConfig();
|
||||||
|
setSaveStatus({ type: null, message: '' });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 当抽屉打开时加载数据
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
loadRoleConfig();
|
||||||
|
setSaveStatus({ type: null, message: '' });
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
// 点击外部关闭Popover
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event) => {
|
||||||
|
if (showResetConfirm && !event.target.closest('.reset-confirm-container')) {
|
||||||
|
setShowResetConfirm(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
}, [showResetConfirm]);
|
||||||
|
|
||||||
|
// ESC键关闭Popover
|
||||||
|
useEffect(() => {
|
||||||
|
const handleEscKey = (event) => {
|
||||||
|
if (event.key === 'Escape' && showResetConfirm) {
|
||||||
|
setShowResetConfirm(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('keydown', handleEscKey);
|
||||||
|
return () => document.removeEventListener('keydown', handleEscKey);
|
||||||
|
}, [showResetConfirm]);
|
||||||
|
|
||||||
|
// 处理输入变化
|
||||||
|
const handleInputChange = (field, value) => {
|
||||||
|
setFormData(prev => ({ ...prev, [field]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed right-0 top-0 h-full w-full lg:w-3/5 z-50 bg-white border-l border-gray-200 shadow-2xl overflow-y-auto">
|
||||||
|
<div ref={drawerContentRef} className="p-4 lg:p-6 h-full overflow-y-auto">
|
||||||
|
{/* 头部 */}
|
||||||
|
<div className="flex items-center justify-between mb-4 lg:mb-6 pb-3 lg:pb-4 border-b border-gray-200">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg lg:text-xl font-bold text-gray-800">角色提示词配置</h3>
|
||||||
|
<p className="text-xs lg:text-sm text-gray-500 mt-1">配置AI角色的名称、描述、技能和限制</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="p-2 hover:bg-gray-100 rounded-full transition-colors"
|
||||||
|
onClick={onClose}
|
||||||
|
disabled={isSaving}
|
||||||
|
title="关闭"
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4 lg:w-5 lg:h-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 状态提示 */}
|
||||||
|
{saveStatus.type && (
|
||||||
|
<div className={`mb-3 lg:mb-4 p-2 lg:p-3 rounded-lg ${
|
||||||
|
saveStatus.type === 'success' ? 'bg-green-50 border border-green-200 text-green-800' :
|
||||||
|
saveStatus.type === 'error' ? 'bg-red-50 border border-red-200 text-red-800' :
|
||||||
|
'bg-blue-50 border border-blue-200 text-blue-800'
|
||||||
|
}`}>
|
||||||
|
<div className="flex items-center">
|
||||||
|
{saveStatus.type === 'loading' && (
|
||||||
|
<span className="loading loading-spinner loading-xs lg:loading-sm mr-2"></span>
|
||||||
|
)}
|
||||||
|
<span className="text-xs lg:text-sm">{saveStatus.message}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 加载状态 */}
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex justify-center items-center h-48 lg:h-64">
|
||||||
|
<div className="text-center">
|
||||||
|
<span className="loading loading-spinner loading-md lg:loading-lg text-gray-400"></span>
|
||||||
|
<p className="mt-3 lg:mt-4 text-sm lg:text-base text-gray-500">加载角色配置中...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
/* 表单内容 */
|
||||||
|
<form onSubmit={handleSave} className="space-y-4 lg:space-y-6">
|
||||||
|
{/* 角色名称 */}
|
||||||
|
<div className="space-y-1 lg:space-y-2">
|
||||||
|
<label className="block text-xs lg:text-sm font-medium text-gray-700">
|
||||||
|
角色名称
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="w-full px-3 py-2 text-xs lg:text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors focus:outline-none"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(e) => handleInputChange('name', e.target.value)}
|
||||||
|
placeholder="例如:兜兜"
|
||||||
|
required
|
||||||
|
disabled={isSaving}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 角色描述 */}
|
||||||
|
<div className="space-y-1 lg:space-y-2">
|
||||||
|
<label className="block text-xs lg:text-sm font-medium text-gray-700">
|
||||||
|
角色描述
|
||||||
|
<span className="text-gray-500 font-normal ml-1">(AI角色的背景、性格特点等)</span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
className="w-full px-3 py-2 text-xs lg:text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors h-32 lg:h-40 focus:outline-none"
|
||||||
|
value={formData.description}
|
||||||
|
onChange={(e) => handleInputChange('description', e.target.value)}
|
||||||
|
placeholder="描述AI角色的背景、性格特点等..."
|
||||||
|
required
|
||||||
|
disabled={isSaving}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 技能列表 */}
|
||||||
|
<div className="space-y-1 lg:space-y-2">
|
||||||
|
<label className="block text-xs lg:text-sm font-medium text-gray-700">
|
||||||
|
技能列表
|
||||||
|
<span className="text-gray-500 font-normal ml-1">(每行输入一个技能)</span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
className="w-full px-3 py-2 text-xs lg:text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors font-mono h-36 lg:h-48 focus:outline-none"
|
||||||
|
value={formData.skills}
|
||||||
|
onChange={(e) => handleInputChange('skills', e.target.value)}
|
||||||
|
placeholder="亲切温暖的沟通 轻松友好的语气 简洁明了的描述"
|
||||||
|
disabled={isSaving}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
每行输入一个技能,空行会被自动过滤
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 限制列表 */}
|
||||||
|
<div className="space-y-1 lg:space-y-2">
|
||||||
|
<label className="block text-xs lg:text-sm font-medium text-gray-700">
|
||||||
|
限制列表
|
||||||
|
<span className="text-gray-500 font-normal ml-1">(每行输入一个限制)</span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
className="w-full px-3 py-2 text-xs lg:text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-200 focus:border-blue-200 transition-colors font-mono h-36 lg:h-48 focus:outline-none"
|
||||||
|
value={formData.restrictions}
|
||||||
|
onChange={(e) => handleInputChange('restrictions', e.target.value)}
|
||||||
|
placeholder="回复必须基于提供的知识库中的内容 每条回复应保持在80字以内 只讨论与宝妈相关的主题"
|
||||||
|
disabled={isSaving}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
每行输入一个限制,空行会被自动过滤
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 操作按钮 */}
|
||||||
|
<div className="flex justify-between items-center pt-4 lg:pt-6 border-t border-gray-200">
|
||||||
|
{/* 左边:重置到默认按钮 */}
|
||||||
|
<div className="relative reset-confirm-container">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="px-3 py-1.5 lg:px-4 lg:py-2 text-xs lg:text-sm font-medium text-white bg-orange-500 hover:bg-orange-600 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
onClick={handleResetButtonClick}
|
||||||
|
disabled={isSaving || isResetting}
|
||||||
|
>
|
||||||
|
{isResetting ? (
|
||||||
|
<span className="flex items-center">
|
||||||
|
<span className="loading loading-spinner loading-xs mr-1 lg:mr-2"></span>
|
||||||
|
重置中...
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
'重置到默认'
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Popover确认气泡 */}
|
||||||
|
{showResetConfirm && (
|
||||||
|
<div className="absolute bottom-full mb-2 left-0 bg-white border border-gray-300 rounded-lg shadow-xl p-4 w-64 z-50">
|
||||||
|
{/* 三角形箭头 - 调整到左边 */}
|
||||||
|
<div className="absolute -bottom-2 left-4 w-4 h-4 bg-white border-r border-b border-gray-300 transform rotate-45"></div>
|
||||||
|
|
||||||
|
<p className="text-sm text-gray-700 mb-3">
|
||||||
|
确定要重置为默认角色配置吗?当前配置将会被覆盖。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex justify-end space-x-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="px-3 py-1.5 text-xs font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
|
||||||
|
onClick={handleCancelReset}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="px-3 py-1.5 text-xs font-medium text-white bg-orange-500 hover:bg-orange-600 rounded-lg transition-colors"
|
||||||
|
onClick={handleConfirmReset}
|
||||||
|
>
|
||||||
|
确定重置
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 右边:其他操作按钮 */}
|
||||||
|
<div className="flex space-x-2 lg:space-x-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="px-3 py-1.5 lg:px-4 lg:py-2 text-xs lg:text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
|
||||||
|
onClick={handleReset}
|
||||||
|
disabled={isSaving || isResetting}
|
||||||
|
>
|
||||||
|
重置
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="px-3 py-1.5 lg:px-4 lg:py-2 text-xs lg:text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
|
||||||
|
onClick={onClose}
|
||||||
|
disabled={isSaving || isResetting}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="px-3 py-1.5 lg:px-4 lg:py-2 text-xs lg:text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
disabled={isSaving || isResetting}
|
||||||
|
>
|
||||||
|
{isSaving ? (
|
||||||
|
<span className="flex items-center">
|
||||||
|
<span className="loading loading-spinner loading-xs mr-1 lg:mr-2"></span>
|
||||||
|
保存中...
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
'保存配置'
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 使用说明 */}
|
||||||
|
{!isLoading && (
|
||||||
|
<div className="mt-6 lg:mt-8 pt-4 lg:pt-6 border-t border-gray-200">
|
||||||
|
<div className="text-xs lg:text-sm text-gray-600">
|
||||||
|
<h4 className="font-medium mb-2 lg:mb-3 text-gray-700">使用说明</h4>
|
||||||
|
<ul className="space-y-1 lg:space-y-2">
|
||||||
|
<li className="flex items-start">
|
||||||
|
<span className="text-blue-500 mr-1 lg:mr-2 text-xs">•</span>
|
||||||
|
<span className="text-xs lg:text-sm">角色配置将影响AI的回复风格和内容限制</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start">
|
||||||
|
<span className="text-blue-500 mr-1 lg:mr-2 text-xs">•</span>
|
||||||
|
<span className="text-xs lg:text-sm">技能列表定义AI的能力特点,每行输入一个技能</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start">
|
||||||
|
<span className="text-blue-500 mr-1 lg:mr-2 text-xs">•</span>
|
||||||
|
<span className="text-xs lg:text-sm">限制列表确保AI回复符合业务要求,每行输入一个限制</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start">
|
||||||
|
<span className="text-blue-500 mr-1 lg:mr-2 text-xs">•</span>
|
||||||
|
<span className="text-xs lg:text-sm">保存后配置将立即生效,影响后续AI回复</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start">
|
||||||
|
<span className="text-orange-500 mr-1 lg:mr-2 text-xs">•</span>
|
||||||
|
<span className="text-xs lg:text-sm">点击"重置到默认"可将角色配置恢复为系统默认值</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RolePromptDrawer;
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
@tailwind base;
|
@import "tailwindcss";
|
||||||
@tailwind components;
|
@plugin "daisyui";
|
||||||
@tailwind utilities;
|
|
||||||
|
|
||||||
/* 自定义滚动条样式 */
|
/* 自定义滚动条样式 */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
@@ -41,7 +40,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply antialiased;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
@@ -50,12 +48,12 @@
|
|||||||
/* 触摸友好的按钮 */
|
/* 触摸友好的按钮 */
|
||||||
@layer components {
|
@layer components {
|
||||||
.touch-button {
|
.touch-button {
|
||||||
@apply active:scale-95 transition-transform duration-150;
|
transform: scale(0.95);
|
||||||
|
transition: transform 150ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 移动端优化输入框 */
|
/* 移动端优化输入框 */
|
||||||
.mobile-input {
|
.mobile-input {
|
||||||
@apply text-base;
|
|
||||||
font-size: 16px !important; /* 防止iOS缩放 */
|
font-size: 16px !important; /* 防止iOS缩放 */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,7 +83,13 @@
|
|||||||
|
|
||||||
/* 移动端优化模态框 */
|
/* 移动端优化模态框 */
|
||||||
.mobile-modal {
|
.mobile-modal {
|
||||||
@apply fixed inset-0 z-50 flex items-center justify-center p-4;
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 50;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 1rem;
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
backdrop-filter: blur(4px);
|
backdrop-filter: blur(4px);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
|
||||||
export default {
|
|
||||||
content: [
|
|
||||||
"./index.html",
|
|
||||||
"./src/**/*.{js,ts,jsx,tsx}",
|
|
||||||
],
|
|
||||||
theme: {
|
|
||||||
extend: {},
|
|
||||||
},
|
|
||||||
plugins: [],
|
|
||||||
daisyui: {
|
|
||||||
themes: ["light", "dark", "cupcake"],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [
|
||||||
|
tailwindcss(),
|
||||||
|
react(),
|
||||||
|
],
|
||||||
base: '/chat_fe',
|
base: '/chat_fe',
|
||||||
server: {
|
server: {
|
||||||
port: 3010,
|
port: 3010,
|
||||||
|
|||||||
Reference in New Issue
Block a user