Compare commits
4 Commits
cea49bb685
...
5d956dd712
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d956dd712 | |||
| 6d5e77c834 | |||
| 2042ec2efd | |||
| 51a1202168 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -214,3 +214,6 @@ test/
|
|||||||
hs_err_pid*
|
hs_err_pid*
|
||||||
replay_pid*
|
replay_pid*
|
||||||
|
|
||||||
|
|
||||||
|
# BitFun snapshot data - auto managed
|
||||||
|
.bitfun/
|
||||||
|
|||||||
874
multi_agent_plan/group_chat_implementation_plan.md
Normal file
874
multi_agent_plan/group_chat_implementation_plan.md
Normal file
@@ -0,0 +1,874 @@
|
|||||||
|
# 多智能体群聊系统实现计划
|
||||||
|
|
||||||
|
## 项目概述
|
||||||
|
|
||||||
|
实现类似"一人公司"的多智能体群聊系统,支持多个 Agent 在群聊中讨论问题,类似于人类的团队会议。
|
||||||
|
|
||||||
|
### 核心特性
|
||||||
|
- **三阶段流程**: 观点提出 → 讨论完善 → CEO 总结决策
|
||||||
|
- **智能轮数**: AI 判断讨论是否充分,自动决定轮数
|
||||||
|
- **用户插话**: 用户可以随时打断发表意见
|
||||||
|
- **角色扮演**: 可配置不同角色的 Agent(CEO、CTO、Designer 等)
|
||||||
|
- **复用架构**: 复用现有的 LLM、ToolRegistry、SessionManager
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、系统架构
|
||||||
|
|
||||||
|
### 1.1 整体架构图
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Agent Group Chat System │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────────┐│
|
||||||
|
│ │ GroupChatManager ││
|
||||||
|
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ││
|
||||||
|
│ │ │ Presenter │ │ Discusser │ │ Summarizer │ ││
|
||||||
|
│ │ │ Controller │ │ Controller │ │ Controller │ ││
|
||||||
|
│ │ └─────────────┘ └─────────────┘ └─────────────┘ ││
|
||||||
|
│ │ │ │ │ ││
|
||||||
|
│ │ └────────────────┴────────────────┘ ││
|
||||||
|
│ │ │ ││
|
||||||
|
│ │ SmartRoundController ││
|
||||||
|
│ └─────────────────────────────────────────────────────────────┘│
|
||||||
|
│ │ │
|
||||||
|
│ ┌──────────────────┼──────────────────┐ │
|
||||||
|
│ ▼ ▼ ▼ │
|
||||||
|
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||||
|
│ │ CEO Agent │ │ CTO Agent │ │Designer Agent│ │
|
||||||
|
│ │ Participant │ │ Participant │ │ Participant │ │
|
||||||
|
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ └──────────────────┴──────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌─────────────┐ │
|
||||||
|
│ │GroupContext │ │
|
||||||
|
│ │(共享上下文) │ │
|
||||||
|
│ └─────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 核心组件
|
||||||
|
|
||||||
|
| 组件 | 职责 | 文件位置 |
|
||||||
|
|------|------|----------|
|
||||||
|
| **GroupChatManager** | 群聊管理器,控制整体流程 | `group_chat/manager.py` |
|
||||||
|
| **Participant** | 参与群聊的 Agent 实例 | `group_chat/participant.py` |
|
||||||
|
| **StageController** | 阶段控制器,管理各阶段 | `group_chat/stages/controller.py` |
|
||||||
|
| **PresenterStage** | 观点提出阶段 | `group_chat/stages/presenter.py` |
|
||||||
|
| **DiscusserStage** | 讨论完善阶段 | `group_chat/stages/discusser.py` |
|
||||||
|
| **SummarizerStage** | 总结决策阶段 | `group_chat/stages/summarizer.py` |
|
||||||
|
| **SmartRoundController** | 智能轮数控制 | `group_chat/round_controller.py` |
|
||||||
|
| **GroupContext** | 共享讨论上下文 | `group_chat/context.py` |
|
||||||
|
| **GroupMessage** | 群聊消息 | `group_chat/message.py` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、数据结构设计
|
||||||
|
|
||||||
|
### 2.1 消息定义
|
||||||
|
|
||||||
|
```python
|
||||||
|
# group_chat/message.py
|
||||||
|
from typing import Optional
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from datetime import datetime
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class MessageStage(str, Enum):
|
||||||
|
"""消息所属阶段"""
|
||||||
|
PRESENT = "present" # 观点提出
|
||||||
|
DISCUSS = "discuss" # 讨论完善
|
||||||
|
SUMMARIZE = "summarize" # 总结决策
|
||||||
|
|
||||||
|
|
||||||
|
class GroupMessage(BaseModel):
|
||||||
|
"""群聊消息"""
|
||||||
|
id: str = Field(..., description="消息唯一标识")
|
||||||
|
agent_id: str = Field(..., description="Agent ID")
|
||||||
|
agent_name: str = Field(..., description="Agent 名称")
|
||||||
|
agent_role: str = Field(..., description="Agent 角色")
|
||||||
|
content: str = Field(..., description="消息内容")
|
||||||
|
timestamp: datetime = Field(default_factory=datetime.now)
|
||||||
|
stage: MessageStage = Field(..., description="所属阶段")
|
||||||
|
round: int = Field(default=1, description="轮数")
|
||||||
|
replying_to: Optional[str] = Field(default=None, description="回复的消息ID")
|
||||||
|
|
||||||
|
|
||||||
|
class UserInterruption(BaseModel):
|
||||||
|
"""用户插话"""
|
||||||
|
id: str
|
||||||
|
content: str
|
||||||
|
timestamp: datetime = Field(default_factory=datetime.now)
|
||||||
|
stage: MessageStage
|
||||||
|
round: int
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 共享上下文
|
||||||
|
|
||||||
|
```python
|
||||||
|
# group_chat/context.py
|
||||||
|
from typing import Optional
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from .message import GroupMessage, UserInterruption
|
||||||
|
|
||||||
|
|
||||||
|
class GroupContext(BaseModel):
|
||||||
|
"""群聊共享上下文"""
|
||||||
|
topic: str = Field(..., description="讨论主题")
|
||||||
|
stage: str = Field(default="present", description="当前阶段")
|
||||||
|
round: int = Field(default=1, description="当前轮数")
|
||||||
|
messages: list[GroupMessage] = Field(default_factory=list)
|
||||||
|
user_interruptions: list[UserInterruption] = Field(default_factory=list)
|
||||||
|
final_decision: Optional[str] = Field(default=None, description="最终决策")
|
||||||
|
status: str = Field(default="running", description="running/completed/failed")
|
||||||
|
|
||||||
|
|
||||||
|
class GroupState(BaseModel):
|
||||||
|
"""群聊状态"""
|
||||||
|
context: GroupContext
|
||||||
|
participants: dict[str, dict] # {agent_id: config}
|
||||||
|
config: dict # 群聊配置
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、核心实现
|
||||||
|
|
||||||
|
### 3.1 Agent 角色定义
|
||||||
|
|
||||||
|
```python
|
||||||
|
# group_chat/roles.py
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
# 默认角色配置
|
||||||
|
DEFAULT_ROLES = {
|
||||||
|
"ceo": {
|
||||||
|
"id": "ceo",
|
||||||
|
"name": "CEO",
|
||||||
|
"role": "首席执行官",
|
||||||
|
"system_prompt": """你是一家公司的 CEO,负责制定公司战略方向和重大决策。
|
||||||
|
你的特点是:高瞻远瞩、战略思维、注重长远利益。
|
||||||
|
在讨论中,你应该:
|
||||||
|
- 从公司整体战略角度分析问题
|
||||||
|
- 权衡短期和长期利益
|
||||||
|
- 做最终决策并承担责任
|
||||||
|
- 善于总结和归纳各方观点""",
|
||||||
|
"stages": ["present", "summarize"],
|
||||||
|
"is_final_decider": True,
|
||||||
|
"priority": 1
|
||||||
|
},
|
||||||
|
"cto": {
|
||||||
|
"id": "cto",
|
||||||
|
"name": "CTO",
|
||||||
|
"role": "首席技术官",
|
||||||
|
"system_prompt": """你是一家公司的 CTO,负责技术决策和技术团队管理。
|
||||||
|
你的特点是:技术深厚、注重可行性、关注技术趋势。
|
||||||
|
在讨论中,你应该:
|
||||||
|
- 从技术角度分析方案的可行性和风险
|
||||||
|
- 提供技术实现建议
|
||||||
|
- 评估技术选型
|
||||||
|
- 关注系统的可扩展性和稳定性""",
|
||||||
|
"stages": ["present", "discuss"],
|
||||||
|
"is_final_decider": False,
|
||||||
|
"priority": 2
|
||||||
|
},
|
||||||
|
"designer": {
|
||||||
|
"id": "designer",
|
||||||
|
"name": "Designer",
|
||||||
|
"role": "产品设计师",
|
||||||
|
"system_prompt": """你是公司的产品设计师,负责用户体验和界面设计。
|
||||||
|
你的特点是:创意丰富、注重用户体验、审美在线。
|
||||||
|
在讨论中,你应该:
|
||||||
|
- 从用户角度分析问题
|
||||||
|
- 提出用户体验优化建议
|
||||||
|
- 关注产品细节
|
||||||
|
- 平衡美观和实用性""",
|
||||||
|
"stages": ["present", "discuss"],
|
||||||
|
"is_final_decider": False,
|
||||||
|
"priority": 3
|
||||||
|
},
|
||||||
|
"analyst": {
|
||||||
|
"id": "analyst",
|
||||||
|
"name": "Analyst",
|
||||||
|
"role": "数据分析师",
|
||||||
|
"system_prompt": """你是公司的数据分析师,负责数据分析和决策支持。
|
||||||
|
你的特点是:数据敏感、逻辑严谨、注重证据。
|
||||||
|
在讨论中,你应该:
|
||||||
|
- 用数据支撑观点
|
||||||
|
- 提供数据分析结果
|
||||||
|
- 指出数据驱动的机会和风险
|
||||||
|
- 关注关键指标""",
|
||||||
|
"stages": ["present", "discuss"],
|
||||||
|
"is_final_decider": False,
|
||||||
|
"priority": 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 Participant(参与者 Agent)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# group_chat/participant.py
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
from langchain_core.language_models import BaseChatModel
|
||||||
|
from langchain_core.messages import HumanMessage, SystemMessage
|
||||||
|
|
||||||
|
from .message import GroupMessage, MessageStage
|
||||||
|
from .context import GroupContext
|
||||||
|
|
||||||
|
|
||||||
|
class Participant:
|
||||||
|
"""参与群聊的 Agent"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
llm: BaseChatModel,
|
||||||
|
config: dict,
|
||||||
|
stage: str = "present"
|
||||||
|
):
|
||||||
|
self.llm = llm
|
||||||
|
self.config = config
|
||||||
|
self.id = config["id"]
|
||||||
|
self.name = config["name"]
|
||||||
|
self.role = config["role"]
|
||||||
|
self.system_prompt = config["system_prompt"]
|
||||||
|
self.stages = config.get("stages", ["present", "discuss", "summarize"])
|
||||||
|
self.is_final_decider = config.get("is_final_decider", False)
|
||||||
|
|
||||||
|
async def generate_message(
|
||||||
|
self,
|
||||||
|
context: GroupContext,
|
||||||
|
stage: MessageStage,
|
||||||
|
round_num: int,
|
||||||
|
replying_to: Optional[str] = None
|
||||||
|
) -> GroupMessage:
|
||||||
|
"""生成消息"""
|
||||||
|
|
||||||
|
# 构建 prompt
|
||||||
|
prompt = self._build_prompt(context, stage, round_num)
|
||||||
|
|
||||||
|
# 调用 LLM
|
||||||
|
response = await self.llm.ainvoke([
|
||||||
|
SystemMessage(content=prompt),
|
||||||
|
HumanMessage(content=f"请就「{context.topic}」发表你的观点。")
|
||||||
|
])
|
||||||
|
|
||||||
|
content = response.content if hasattr(response, 'content') else str(response)
|
||||||
|
|
||||||
|
# 创建消息
|
||||||
|
message = GroupMessage(
|
||||||
|
id=str(uuid.uuid4()),
|
||||||
|
agent_id=self.id,
|
||||||
|
agent_name=self.name,
|
||||||
|
agent_role=self.role,
|
||||||
|
content=content,
|
||||||
|
timestamp=datetime.now(),
|
||||||
|
stage=stage,
|
||||||
|
round=round_num,
|
||||||
|
replying_to=replying_to
|
||||||
|
)
|
||||||
|
|
||||||
|
return message
|
||||||
|
|
||||||
|
def _build_prompt(self, context: GroupContext, stage: MessageStage, round_num: int) -> str:
|
||||||
|
"""构建 prompt"""
|
||||||
|
|
||||||
|
base_prompt = self.system_prompt
|
||||||
|
|
||||||
|
# 添加上下文
|
||||||
|
context_info = f"""
|
||||||
|
## 当前讨论
|
||||||
|
主题:{context.topic}
|
||||||
|
阶段:{stage.value}
|
||||||
|
轮数:{round_num}
|
||||||
|
|
||||||
|
## 历史消息
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 添加历史消息(限制数量)
|
||||||
|
recent_messages = context.messages[-10:] if context.messages else []
|
||||||
|
for msg in recent_messages:
|
||||||
|
context_info += f"\n【{msg.agent_name}】{msg.content}\n"
|
||||||
|
|
||||||
|
# 添加用户插话
|
||||||
|
if context.user_interruptions:
|
||||||
|
context_info += "\n## 用户插话\n"
|
||||||
|
for interruption in context.user_interruptions[-3:]:
|
||||||
|
context_info += f"\n【用户】{interruption.content}\n"
|
||||||
|
|
||||||
|
# 阶段特定的指令
|
||||||
|
stage_instruction = {
|
||||||
|
"present": "请简洁地提出你的观点和建议。",
|
||||||
|
"discuss": "请回应其他人的观点,进行讨论和完善。",
|
||||||
|
"summarize": "请总结各方观点,给出最终决策建议。"
|
||||||
|
}
|
||||||
|
|
||||||
|
full_prompt = f"{base_prompt}\n\n{context_info}\n\n{stage_instruction.get(stage.value, '')}"
|
||||||
|
|
||||||
|
return full_prompt
|
||||||
|
|
||||||
|
def can_participate(self, stage: MessageStage) -> bool:
|
||||||
|
"""判断是否可以参与当前阶段"""
|
||||||
|
return stage.value in self.stages
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 智能轮数控制器
|
||||||
|
|
||||||
|
```python
|
||||||
|
# group_chat/round_controller.py
|
||||||
|
from langchain_core.language_models import BaseChatModel
|
||||||
|
from langchain_core.messages import SystemMessage, HumanMessage
|
||||||
|
|
||||||
|
from .context import GroupContext
|
||||||
|
from .message import MessageStage
|
||||||
|
|
||||||
|
|
||||||
|
class SmartRoundController:
|
||||||
|
"""智能轮数控制器"""
|
||||||
|
|
||||||
|
def __init__(self, llm: BaseChatModel, max_rounds: int = 3):
|
||||||
|
self.llm = llm
|
||||||
|
self.max_rounds = max_rounds
|
||||||
|
|
||||||
|
async def should_continue(
|
||||||
|
self,
|
||||||
|
context: GroupContext,
|
||||||
|
stage: MessageStage
|
||||||
|
) -> tuple[bool, str]:
|
||||||
|
"""
|
||||||
|
判断是否继续下一轮
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(是否继续, 原因)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 达到最大轮数
|
||||||
|
if context.round >= self.max_rounds:
|
||||||
|
return False, "max_rounds_reached"
|
||||||
|
|
||||||
|
# 构建判断 prompt
|
||||||
|
prompt = self._build_judge_prompt(context, stage)
|
||||||
|
|
||||||
|
# 调用 LLM 判断
|
||||||
|
response = await self.llm.ainvoke([
|
||||||
|
SystemMessage(content=prompt),
|
||||||
|
HumanMessage(content="请判断讨论是否已经充分,是否需要更多轮数。")
|
||||||
|
])
|
||||||
|
|
||||||
|
content = response.content.lower() if hasattr(response, 'content') else str(response)
|
||||||
|
|
||||||
|
# 解析判断结果
|
||||||
|
if "充分" in content or "足够" in content or "不需要" in content:
|
||||||
|
return False, "discussion_sufficient"
|
||||||
|
elif "不充分" in content or "不够" in content or "需要" in content:
|
||||||
|
return True, "need_more_discussion"
|
||||||
|
|
||||||
|
# 默认继续
|
||||||
|
return True, "default_continue"
|
||||||
|
|
||||||
|
def _build_judge_prompt(self, context: GroupContext, stage: MessageStage) -> str:
|
||||||
|
"""构建判断 prompt"""
|
||||||
|
|
||||||
|
messages_summary = "\n".join([
|
||||||
|
f"【{msg.agent_name}】{msg.content[:200]}..."
|
||||||
|
for msg in context.messages[-5:]
|
||||||
|
])
|
||||||
|
|
||||||
|
return f"""你是一个讨论质量评估专家。请判断当前的讨论是否已经充分。
|
||||||
|
|
||||||
|
## 讨论信息
|
||||||
|
主题:{context.topic}
|
||||||
|
当前阶段:{stage.value}
|
||||||
|
当前轮数:{context.round}
|
||||||
|
最大轮数:{self.max_rounds}
|
||||||
|
|
||||||
|
## 最近的讨论内容
|
||||||
|
{messages_summary}
|
||||||
|
|
||||||
|
## 判断标准
|
||||||
|
- 各方观点是否已经充分表达?
|
||||||
|
- 是否有建设性的讨论和回应?
|
||||||
|
- 是否已经形成明确的结论或方向?
|
||||||
|
|
||||||
|
## 请输出
|
||||||
|
如果讨论已经充分,请输出"充分",并简要说明原因。
|
||||||
|
如果还需要更多讨论,请输出"不充分",并说明需要讨论哪些方面。
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 群聊管理器
|
||||||
|
|
||||||
|
```python
|
||||||
|
# group_chat/manager.py
|
||||||
|
import uuid
|
||||||
|
from typing import Optional
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from langchain_core.language_models import BaseChatModel
|
||||||
|
|
||||||
|
from .context import GroupContext
|
||||||
|
from .message import GroupMessage, MessageStage, UserInterruption
|
||||||
|
from .participant import Participant
|
||||||
|
from .roles import DEFAULT_ROLES
|
||||||
|
from .round_controller import SmartRoundController
|
||||||
|
|
||||||
|
|
||||||
|
class GroupChatManager:
|
||||||
|
"""群聊管理器"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
llm: BaseChatModel,
|
||||||
|
roles: dict = None,
|
||||||
|
max_rounds: int = 3,
|
||||||
|
enable_user_interrupt: bool = True
|
||||||
|
):
|
||||||
|
self.llm = llm
|
||||||
|
self.roles = roles or DEFAULT_ROLES
|
||||||
|
self.max_rounds = max_rounds
|
||||||
|
self.enable_user_interrupt = enable_user_interrupt
|
||||||
|
|
||||||
|
# 初始化组件
|
||||||
|
self.round_controller = SmartRoundController(llm, max_rounds)
|
||||||
|
|
||||||
|
# 运行时状态
|
||||||
|
self.context: Optional[GroupContext] = None
|
||||||
|
self.participants: dict[str, Participant] = {}
|
||||||
|
|
||||||
|
async def start_chat(self, topic: str) -> dict:
|
||||||
|
"""开始群聊"""
|
||||||
|
|
||||||
|
# 创建上下文
|
||||||
|
self.context = GroupContext(
|
||||||
|
topic=topic,
|
||||||
|
stage="present",
|
||||||
|
round=1,
|
||||||
|
messages=[],
|
||||||
|
user_interruptions=[],
|
||||||
|
status="running"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 创建参与者
|
||||||
|
self.participants = {
|
||||||
|
role_id: Participant(self.llm, config)
|
||||||
|
for role_id, config in self.roles.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
# 按优先级排序参与者
|
||||||
|
sorted_participants = sorted(
|
||||||
|
self.participants.values(),
|
||||||
|
key=lambda p: p.config.get("priority", 999)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 开始第一阶段
|
||||||
|
result = await self._run_stage(MessageStage.PRESENT, sorted_participants)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
async def _run_stage(
|
||||||
|
self,
|
||||||
|
stage: MessageStage,
|
||||||
|
participants: list[Participant]
|
||||||
|
) -> dict:
|
||||||
|
"""运行指定阶段"""
|
||||||
|
|
||||||
|
# 更新上下文
|
||||||
|
self.context.stage = stage.value
|
||||||
|
|
||||||
|
# 按阶段获取参与者
|
||||||
|
stage_participants = [p for p in participants if p.can_participate(stage)]
|
||||||
|
|
||||||
|
# 执行多轮
|
||||||
|
for round_num in range(1, self.max_rounds + 1):
|
||||||
|
self.context.round = round_num
|
||||||
|
|
||||||
|
# 当前阶段参与者发言
|
||||||
|
messages = await self._run_round(stage, stage_participants, round_num)
|
||||||
|
|
||||||
|
# 智能判断是否继续
|
||||||
|
should_continue, reason = await self.round_controller.should_continue(
|
||||||
|
self.context, stage
|
||||||
|
)
|
||||||
|
|
||||||
|
if not should_continue:
|
||||||
|
break
|
||||||
|
|
||||||
|
# 阶段完成后的处理
|
||||||
|
if stage == MessageStage.PRESENT:
|
||||||
|
# 进入讨论阶段
|
||||||
|
if any(p.can_participate(MessageStage.DISCUSS) for p in participants):
|
||||||
|
return await self._run_stage(MessageStage.DISCUSS, participants)
|
||||||
|
|
||||||
|
elif stage == MessageStage.DISCUSS:
|
||||||
|
# 进入总结阶段
|
||||||
|
summarizer = next(
|
||||||
|
(p for p in participants if p.is_final_decider),
|
||||||
|
participants[0]
|
||||||
|
)
|
||||||
|
return await self._run_stage(MessageStage.SUMMARIZE, [summarizer])
|
||||||
|
|
||||||
|
elif stage == MessageStage.SUMMARIZE:
|
||||||
|
# 完成
|
||||||
|
self.context.status = "completed"
|
||||||
|
|
||||||
|
return self._build_result()
|
||||||
|
|
||||||
|
async def _run_round(
|
||||||
|
self,
|
||||||
|
stage: MessageStage,
|
||||||
|
participants: list[Participant],
|
||||||
|
round_num: int
|
||||||
|
) -> list[GroupMessage]:
|
||||||
|
"""运行一轮发言"""
|
||||||
|
|
||||||
|
messages = []
|
||||||
|
|
||||||
|
for participant in participants:
|
||||||
|
# 获取前一轮的最新消息(用于回复)
|
||||||
|
replying_to = None
|
||||||
|
if self.context.messages:
|
||||||
|
last_message = self.context.messages[-1]
|
||||||
|
if last_message.agent_id != participant.id:
|
||||||
|
replying_to = last_message.id
|
||||||
|
|
||||||
|
# 生成消息
|
||||||
|
message = await participant.generate_message(
|
||||||
|
context=self.context,
|
||||||
|
stage=stage,
|
||||||
|
round_num=round_num,
|
||||||
|
replying_to=replying_to
|
||||||
|
)
|
||||||
|
|
||||||
|
# 保存消息
|
||||||
|
self.context.messages.append(message)
|
||||||
|
messages.append(message)
|
||||||
|
|
||||||
|
return messages
|
||||||
|
|
||||||
|
async def add_interruption(self, content: str) -> dict:
|
||||||
|
"""添加用户插话"""
|
||||||
|
|
||||||
|
if not self.enable_user_interrupt:
|
||||||
|
return {"error": "用户插话已禁用"}
|
||||||
|
|
||||||
|
interruption = UserInterruption(
|
||||||
|
id=str(uuid.uuid4()),
|
||||||
|
content=content,
|
||||||
|
timestamp=datetime.now(),
|
||||||
|
stage=MessageStage(self.context.stage),
|
||||||
|
round=self.context.round
|
||||||
|
)
|
||||||
|
|
||||||
|
self.context.user_interruptions.append(interruption)
|
||||||
|
|
||||||
|
return {"success": True, "interruption_id": interruption.id}
|
||||||
|
|
||||||
|
def get_messages(self) -> list[GroupMessage]:
|
||||||
|
"""获取所有消息"""
|
||||||
|
return self.context.messages if self.context else []
|
||||||
|
|
||||||
|
def get_result(self) -> dict:
|
||||||
|
"""获取结果"""
|
||||||
|
return self._build_result()
|
||||||
|
|
||||||
|
def _build_result(self) -> dict:
|
||||||
|
"""构建结果"""
|
||||||
|
return {
|
||||||
|
"topic": self.context.topic,
|
||||||
|
"status": self.context.status,
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"agent_name": m.agent_name,
|
||||||
|
"agent_role": m.agent_role,
|
||||||
|
"content": m.content,
|
||||||
|
"stage": m.stage.value,
|
||||||
|
"round": m.round
|
||||||
|
}
|
||||||
|
for m in self.context.messages
|
||||||
|
],
|
||||||
|
"final_decision": self.context.final_decision,
|
||||||
|
"user_interruptions": [
|
||||||
|
{"content": i.content, "timestamp": i.timestamp.isoformat()}
|
||||||
|
for i in self.context.user_interruptions
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、阶段实现
|
||||||
|
|
||||||
|
### 4.1 Presenter Stage(观点提出)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# group_chat/stages/presenter.py
|
||||||
|
from typing import list
|
||||||
|
from ..participant import Participant
|
||||||
|
from ..message import GroupMessage
|
||||||
|
|
||||||
|
|
||||||
|
class PresenterStage:
|
||||||
|
"""观点提出阶段"""
|
||||||
|
|
||||||
|
async def execute(
|
||||||
|
self,
|
||||||
|
participants: list[Participant],
|
||||||
|
context
|
||||||
|
) -> list[GroupMessage]:
|
||||||
|
"""执行观点提出"""
|
||||||
|
|
||||||
|
messages = []
|
||||||
|
|
||||||
|
# 按优先级顺序发言
|
||||||
|
for participant in participants:
|
||||||
|
if not participant.can_participate("present"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
message = await participant.generate_message(
|
||||||
|
context=context,
|
||||||
|
stage="present",
|
||||||
|
round=context.round
|
||||||
|
)
|
||||||
|
|
||||||
|
messages.append(message)
|
||||||
|
context.messages.append(message)
|
||||||
|
|
||||||
|
return messages
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 Discusser Stage(讨论完善)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# group_chat/stages/discusser.py
|
||||||
|
from ..participant import Participant
|
||||||
|
from ..message import GroupMessage
|
||||||
|
|
||||||
|
|
||||||
|
class DiscusserStage:
|
||||||
|
"""讨论完善阶段"""
|
||||||
|
|
||||||
|
async def execute(
|
||||||
|
self,
|
||||||
|
participants: list[Participant],
|
||||||
|
context
|
||||||
|
) -> list[GroupMessage]:
|
||||||
|
"""执行讨论完善"""
|
||||||
|
|
||||||
|
messages = []
|
||||||
|
|
||||||
|
# 获取上一轮的消息
|
||||||
|
last_round_messages = [
|
||||||
|
m for m in context.messages
|
||||||
|
if m.stage == "present" and m.round == context.round - 1
|
||||||
|
]
|
||||||
|
|
||||||
|
for participant in participants:
|
||||||
|
if not participant.can_participate("discuss"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 选择要回复的消息
|
||||||
|
replying_to = None
|
||||||
|
for msg in reversed(last_round_messages):
|
||||||
|
if msg.agent_id != participant.id:
|
||||||
|
replying_to = msg.id
|
||||||
|
break
|
||||||
|
|
||||||
|
message = await participant.generate_message(
|
||||||
|
context=context,
|
||||||
|
stage="discuss",
|
||||||
|
round=context.round,
|
||||||
|
replying_to=replying_to
|
||||||
|
)
|
||||||
|
|
||||||
|
messages.append(message)
|
||||||
|
context.messages.append(message)
|
||||||
|
|
||||||
|
return messages
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 Summarizer Stage(总结决策)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# group_chat/stages/summarizer.py
|
||||||
|
from ..participant import Participant
|
||||||
|
from ..message import GroupMessage
|
||||||
|
|
||||||
|
|
||||||
|
class SummarizerStage:
|
||||||
|
"""总结决策阶段"""
|
||||||
|
|
||||||
|
async def execute(
|
||||||
|
self,
|
||||||
|
participants: list[Participant],
|
||||||
|
context
|
||||||
|
) -> GroupMessage:
|
||||||
|
"""执行总结决策"""
|
||||||
|
|
||||||
|
# CEO 总结
|
||||||
|
summarizer = next(
|
||||||
|
(p for p in participants if p.is_final_decider),
|
||||||
|
participants[0]
|
||||||
|
)
|
||||||
|
|
||||||
|
message = await summarizer.generate_message(
|
||||||
|
context=context,
|
||||||
|
stage="summarize",
|
||||||
|
round=context.round
|
||||||
|
)
|
||||||
|
|
||||||
|
# 保存最终决策
|
||||||
|
context.final_decision = message.content
|
||||||
|
context.messages.append(message)
|
||||||
|
|
||||||
|
return message
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、与现有系统集成
|
||||||
|
|
||||||
|
### 5.1 复用现有组件
|
||||||
|
|
||||||
|
```python
|
||||||
|
# group_chat/integration.py
|
||||||
|
from typing import Optional
|
||||||
|
from app.llm.factory import LLMFactory
|
||||||
|
from app.agent.tools.registry import ToolRegistry
|
||||||
|
from app.agent.memory.session import SessionManager
|
||||||
|
|
||||||
|
from .manager import GroupChatManager
|
||||||
|
|
||||||
|
|
||||||
|
class GroupChatSystem:
|
||||||
|
"""群聊系统 - 集成现有组件"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
llm_provider: str = "openai",
|
||||||
|
openai_api_key: Optional[str] = None,
|
||||||
|
anthropic_api_key: Optional[str] = None,
|
||||||
|
roles: dict = None,
|
||||||
|
max_rounds: int = 3,
|
||||||
|
enable_user_interrupt: bool = True
|
||||||
|
):
|
||||||
|
# 初始化 LLM Factory
|
||||||
|
self.llm_factory = LLMFactory(
|
||||||
|
provider=llm_provider,
|
||||||
|
openai_api_key=openai_api_key,
|
||||||
|
anthropic_api_key=anthropic_api_key
|
||||||
|
)
|
||||||
|
|
||||||
|
# 初始化 Tool Registry
|
||||||
|
self.tool_registry = ToolRegistry()
|
||||||
|
|
||||||
|
# 初始化 Session Manager
|
||||||
|
self.session_manager = SessionManager()
|
||||||
|
|
||||||
|
# 配置
|
||||||
|
self.roles = roles
|
||||||
|
self.max_rounds = max_rounds
|
||||||
|
self.enable_user_interrupt = enable_user_interrupt
|
||||||
|
|
||||||
|
async def start_group_chat(
|
||||||
|
self,
|
||||||
|
topic: str,
|
||||||
|
session_id: str = None
|
||||||
|
) -> dict:
|
||||||
|
"""开始群聊"""
|
||||||
|
|
||||||
|
# 获取 LLM
|
||||||
|
llm = self.llm_factory.get_llm()
|
||||||
|
|
||||||
|
# 创建群聊管理器
|
||||||
|
manager = GroupChatManager(
|
||||||
|
llm=llm,
|
||||||
|
roles=self.roles,
|
||||||
|
max_rounds=self.max_rounds,
|
||||||
|
enable_user_interrupt=self.enable_user_interrupt
|
||||||
|
)
|
||||||
|
|
||||||
|
# 开始群聊
|
||||||
|
result = await manager.start_chat(topic)
|
||||||
|
|
||||||
|
# 保存到 session
|
||||||
|
if session_id:
|
||||||
|
self.session_manager.add_message(session_id, "user", topic)
|
||||||
|
self.session_manager.add_message(
|
||||||
|
session_id,
|
||||||
|
"assistant",
|
||||||
|
result.get("final_decision", str(result))
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
async def add_message(
|
||||||
|
self,
|
||||||
|
message: str,
|
||||||
|
session_id: str
|
||||||
|
) -> dict:
|
||||||
|
"""添加用户消息(插话)"""
|
||||||
|
|
||||||
|
# 获取 session 中的 manager
|
||||||
|
# ... 实现获取逻辑
|
||||||
|
|
||||||
|
return manager.add_interruption(message)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
agent/app/agent/multi/
|
||||||
|
├── group_chat/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── roles.py # Agent 角色定义
|
||||||
|
│ ├── message.py # 消息类型
|
||||||
|
│ ├── context.py # 共享上下文
|
||||||
|
│ ├── participant.py # 参与者 Agent
|
||||||
|
│ ├── manager.py # 群聊管理器
|
||||||
|
│ ├── round_controller.py # 智能轮数控制器
|
||||||
|
│ ├── stages/
|
||||||
|
│ │ ├── __init__.py
|
||||||
|
│ │ ├── controller.py # 阶段控制器
|
||||||
|
│ │ ├── presenter.py # 观点提出阶段
|
||||||
|
│ │ ├── discusser.py # 讨论完善阶段
|
||||||
|
│ │ └── summarizer.py # 总结决策阶段
|
||||||
|
│ └── integration.py # 与现有系统集成
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、实现顺序
|
||||||
|
|
||||||
|
1. **Phase 1: 基础架构**
|
||||||
|
- 定义数据类型 (message.py, context.py)
|
||||||
|
- 创建角色配置 (roles.py)
|
||||||
|
|
||||||
|
2. **Phase 2: 核心组件**
|
||||||
|
- 实现 Participant (participant.py)
|
||||||
|
- 实现 SmartRoundController (round_controller.py)
|
||||||
|
|
||||||
|
3. **Phase 3: 阶段实现**
|
||||||
|
- 实现 PresenterStage
|
||||||
|
- 实现 DiscusserStage
|
||||||
|
- 实现 SummarizerStage
|
||||||
|
|
||||||
|
4. **Phase 4: 集成**
|
||||||
|
- 实现 GroupChatManager
|
||||||
|
- 与现有系统集成
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、测试计划
|
||||||
|
|
||||||
|
1. **单元测试**: 测试各 Participant 的消息生成
|
||||||
|
2. **集成测试**: 测试完整的群聊流程
|
||||||
|
3. **轮数控制测试**: 测试智能轮数判断
|
||||||
|
4. **用户插话测试**: 测试插话机制
|
||||||
|
5. **端到端测试**: 模拟真实群聊场景
|
||||||
80
multi_agent_plan/group_chat_notes.md
Normal file
80
multi_agent_plan/group_chat_notes.md
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
# Notes: 多智能体群聊系统研究
|
||||||
|
|
||||||
|
## 核心概念
|
||||||
|
|
||||||
|
### 群聊系统 vs 之前的 Supervisor 系统
|
||||||
|
|
||||||
|
| 特性 | Supervisor 系统 | 群聊系统 |
|
||||||
|
|------|----------------|----------|
|
||||||
|
| 流程 | 线性:规划 → 执行 → 汇总 | 多阶段循环 |
|
||||||
|
| Agent 关系 | 层级:Supervisor 管理 Workers | 平等协作 |
|
||||||
|
| 通信方式 | 单向:任务分发 | 多向:互相讨论 |
|
||||||
|
| 决策方式 | Supervisor 决定 | CEO 最终决策 |
|
||||||
|
| 用户参与 | 旁观 | 可插话 |
|
||||||
|
|
||||||
|
### 设计模式
|
||||||
|
|
||||||
|
#### 1. 流水线模式
|
||||||
|
```
|
||||||
|
Stage 1 (Presenter) → Stage 2 (Discusser) → Stage 3 (Summarizer)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 消息传递
|
||||||
|
- 每个 Stage 维护一个消息队列
|
||||||
|
- Agent 的输出成为下一个 Agent 的输入
|
||||||
|
- 使用 Shared Context 存储共享状态
|
||||||
|
|
||||||
|
#### 3. 智能轮数
|
||||||
|
```python
|
||||||
|
class SmartRoundController:
|
||||||
|
def should_continue(self, stage, round_num, messages):
|
||||||
|
# 使用 LLM 判断是否继续
|
||||||
|
prompt = f"""
|
||||||
|
当前阶段: {stage}
|
||||||
|
当前轮数: {round_num}
|
||||||
|
讨论内容: {messages}
|
||||||
|
|
||||||
|
讨论是否已经充分?是否需要更多轮数?
|
||||||
|
"""
|
||||||
|
return llm.judge(prompt)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 复用现有架构
|
||||||
|
|
||||||
|
### 可复用的组件
|
||||||
|
1. **LLM Factory** - 语言模型
|
||||||
|
2. **Tool Registry** - 工具注册
|
||||||
|
3. **Session Manager** - 会话管理
|
||||||
|
4. **Agent Executor** - Agent 执行逻辑(部分)
|
||||||
|
|
||||||
|
### 需要新增的组件
|
||||||
|
1. **GroupChatManager** - 群聊管理器
|
||||||
|
2. **Participant** - 参与者 Agent
|
||||||
|
3. **Stage Controller** - 阶段控制器
|
||||||
|
4. **SmartRoundController** - 智能轮数控制器
|
||||||
|
|
||||||
|
## 关键数据结构
|
||||||
|
|
||||||
|
### GroupMessage
|
||||||
|
```python
|
||||||
|
class GroupMessage(BaseModel):
|
||||||
|
id: str
|
||||||
|
agent_id: str
|
||||||
|
agent_name: str
|
||||||
|
content: str
|
||||||
|
timestamp: datetime
|
||||||
|
stage: str # presenter/discusser/summarizer
|
||||||
|
round: int
|
||||||
|
replying_to: Optional[str] # 回复的消息 ID
|
||||||
|
```
|
||||||
|
|
||||||
|
### GroupContext
|
||||||
|
```python
|
||||||
|
class GroupContext(BaseModel):
|
||||||
|
topic: str # 讨论主题
|
||||||
|
stage: str # 当前阶段
|
||||||
|
round: int # 当前轮数
|
||||||
|
messages: list[GroupMessage] # 所有消息
|
||||||
|
user_interruptions: list[str] # 用户插话
|
||||||
|
final_decision: Optional[str] # 最终决策
|
||||||
|
```
|
||||||
32
multi_agent_plan/group_chat_task_plan.md
Normal file
32
multi_agent_plan/group_chat_task_plan.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Task Plan: 多智能体群聊系统实现计划
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
实现类似"一人公司"的多智能体群聊系统,支持头脑风暴、任务协作、角色扮演等多模式讨论。
|
||||||
|
|
||||||
|
## Phases
|
||||||
|
- [x] Phase 1: 基础架构设计和核心组件规划
|
||||||
|
- [ ] Phase 2: 群聊管理器 (GroupChatManager) 实现
|
||||||
|
- [ ] Phase 3: 参与 Agent (Participant) 实现
|
||||||
|
- [ ] Phase 4: 三个阶段实现 (Presenter/Discusser/Summarizer)
|
||||||
|
- [ ] Phase 5: 智能轮数控制实现
|
||||||
|
- [ ] Phase 6: 用户插话机制实现
|
||||||
|
- [ ] Phase 7: 与现有系统集成和 API 接口
|
||||||
|
|
||||||
|
## Key Questions
|
||||||
|
1. 如何复用现有的 Supervisor + Workers 架构?
|
||||||
|
2. 如何实现智能轮数控制?
|
||||||
|
3. 如何处理用户插话?
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
- 架构:任务流水线模式(观点提出 → 讨论完善 → 总结决策)
|
||||||
|
- 决策机制:CEO Agent 最终决策
|
||||||
|
- 轮数控制:AI 智能判断
|
||||||
|
- 用户参与:插话模式
|
||||||
|
- 复用策略:复用现有 LLM、ToolRegistry、SessionManager
|
||||||
|
|
||||||
|
## Status
|
||||||
|
**Currently in Phase 1** - 系统架构设计和核心组件规划已完成
|
||||||
|
|
||||||
|
## 实现计划文件
|
||||||
|
- `group_chat_implementation_plan.md` - 详细实现计划
|
||||||
|
- `group_chat_notes.md` - 研究笔记
|
||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
"x-agents/server/internal/config"
|
"x-agents/server/internal/config"
|
||||||
"x-agents/server/internal/handler"
|
"x-agents/server/internal/handler"
|
||||||
|
"x-agents/server/internal/middleware"
|
||||||
"x-agents/server/internal/model"
|
"x-agents/server/internal/model"
|
||||||
"x-agents/server/internal/repository"
|
"x-agents/server/internal/repository"
|
||||||
"x-agents/server/internal/service"
|
"x-agents/server/internal/service"
|
||||||
@@ -70,13 +71,14 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. 自动迁移表
|
// 3. 自动迁移表
|
||||||
db.AutoMigrate(&model.DatabaseInfo{}, &model.SubTableInfo{}, &model.ModelInfo{}, &model.KnowledgeBase{}, &model.KnowledgeDocument{})
|
db.AutoMigrate(&model.DatabaseInfo{}, &model.SubTableInfo{}, &model.ModelInfo{}, &model.KnowledgeBase{}, &model.KnowledgeDocument{}, &model.User{}, &model.Role{})
|
||||||
|
|
||||||
// 4. 初始化 Repository
|
// 4. 初始化 Repository
|
||||||
dbRepo := repository.NewDatabaseRepository(db)
|
dbRepo := repository.NewDatabaseRepository(db)
|
||||||
subTableRepo := repository.NewSubTableRepository(db)
|
subTableRepo := repository.NewSubTableRepository(db)
|
||||||
modelRepo := repository.NewModelRepository(db)
|
modelRepo := repository.NewModelRepository(db)
|
||||||
knowledgeRepo := repository.NewKnowledgeRepository(db)
|
knowledgeRepo := repository.NewKnowledgeRepository(db)
|
||||||
|
userRepo := repository.NewUserRepository(db)
|
||||||
|
|
||||||
// 5. 初始化 Service
|
// 5. 初始化 Service
|
||||||
dbService := service.NewDatabaseService(dbRepo, subTableRepo)
|
dbService := service.NewDatabaseService(dbRepo, subTableRepo)
|
||||||
@@ -88,6 +90,7 @@ func main() {
|
|||||||
log.Printf("Warning: Failed to initialize upload service: %v (files will not be available)", err)
|
log.Printf("Warning: Failed to initialize upload service: %v (files will not be available)", err)
|
||||||
}
|
}
|
||||||
knowledgeService := service.NewKnowledgeService(knowledgeRepo, modelRepo, uploadService, cfg.PythonServiceURL, cfg.AICoreServiceAddr, cfg.MarkdownLocalPath)
|
knowledgeService := service.NewKnowledgeService(knowledgeRepo, modelRepo, uploadService, cfg.PythonServiceURL, cfg.AICoreServiceAddr, cfg.MarkdownLocalPath)
|
||||||
|
authService := service.NewAuthService(cfg.JWTSecret, userRepo)
|
||||||
|
|
||||||
// 6. 初始化 Handler
|
// 6. 初始化 Handler
|
||||||
dbHandler := handler.NewDatabaseHandler(dbService)
|
dbHandler := handler.NewDatabaseHandler(dbService)
|
||||||
@@ -96,6 +99,7 @@ func main() {
|
|||||||
modelHandler := handler.NewModelHandler(modelService)
|
modelHandler := handler.NewModelHandler(modelService)
|
||||||
systemHandler := handler.NewSystemHandler()
|
systemHandler := handler.NewSystemHandler()
|
||||||
knowledgeHandler := handler.NewKnowledgeHandler(knowledgeService)
|
knowledgeHandler := handler.NewKnowledgeHandler(knowledgeService)
|
||||||
|
authHandler := handler.NewAuthHandler(authService)
|
||||||
var uploadHandler *handler.UploadHandler
|
var uploadHandler *handler.UploadHandler
|
||||||
if uploadService != nil {
|
if uploadService != nil {
|
||||||
uploadHandler = handler.NewUploadHandler(uploadService, knowledgeRepo)
|
uploadHandler = handler.NewUploadHandler(uploadService, knowledgeRepo)
|
||||||
@@ -144,6 +148,20 @@ func main() {
|
|||||||
c.Next()
|
c.Next()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 认证模块(无需登录)
|
||||||
|
authGroup := r.Group("/auth")
|
||||||
|
{
|
||||||
|
authGroup.POST("/register", authHandler.Register)
|
||||||
|
authGroup.POST("/login", authHandler.Login)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 需要登录的认证模块
|
||||||
|
authProtectedGroup := r.Group("/auth")
|
||||||
|
authProtectedGroup.Use(middleware.Auth(cfg.JWTSecret))
|
||||||
|
{
|
||||||
|
authProtectedGroup.GET("/me", authHandler.GetCurrentUser)
|
||||||
|
}
|
||||||
|
|
||||||
// 数据库管理模块
|
// 数据库管理模块
|
||||||
databaseGroup := r.Group("/database")
|
databaseGroup := r.Group("/database")
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -78,3 +78,27 @@ func (h *AuthHandler) Register(c *gin.Context) {
|
|||||||
"email": user.Email,
|
"email": user.Email,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetCurrentUser 获取当前登录用户信息
|
||||||
|
func (h *AuthHandler) GetCurrentUser(c *gin.Context) {
|
||||||
|
userID, exists := c.Get("user_id")
|
||||||
|
if !exists {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "user not found in context"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := h.authService.GetUserByID(userID.(string))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"id": user.ID,
|
||||||
|
"username": user.Username,
|
||||||
|
"email": user.Email,
|
||||||
|
"role_id": user.RoleID,
|
||||||
|
"is_active": user.IsActive,
|
||||||
|
"created_at": user.CreatedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
import Dashboard from '@/views/Dashboard.vue'
|
import Dashboard from '@/views/Dashboard.vue'
|
||||||
import Login from '@/views/Login.vue'
|
import Login from '@/views/Login.vue'
|
||||||
|
import Signup from '@/views/Signup.vue'
|
||||||
import Chat from '@/views/Chat.vue'
|
import Chat from '@/views/Chat.vue'
|
||||||
import Agents from '@/views/Agents.vue'
|
import Agents from '@/views/Agents.vue'
|
||||||
import Team from '@/views/Team.vue'
|
import Team from '@/views/Team.vue'
|
||||||
@@ -23,6 +24,11 @@ const router = createRouter({
|
|||||||
name: 'login',
|
name: 'login',
|
||||||
component: Login
|
component: Login
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/signup',
|
||||||
|
name: 'signup',
|
||||||
|
component: Signup
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/dashboard',
|
path: '/dashboard',
|
||||||
name: 'dashboard',
|
name: 'dashboard',
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ const handleLogin = () => {
|
|||||||
<!-- 注册链接 -->
|
<!-- 注册链接 -->
|
||||||
<p class="text-center mt-6 text-gray-400">
|
<p class="text-center mt-6 text-gray-400">
|
||||||
Don't have an account?
|
Don't have an account?
|
||||||
<a href="#" class="text-primary-orange hover:text-orange-400 font-medium transition-colors">Sign up</a>
|
<a @click="router.push('/signup')" class="text-primary-orange hover:text-orange-400 font-medium transition-colors cursor-pointer">Sign up</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ import { ref, watch, computed } from 'vue'
|
|||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { useModelSettings } from './settings/useModelSettings'
|
import { useModelSettings } from './settings/useModelSettings'
|
||||||
import FormDialog from '@/components/FormDialog.vue'
|
import FormDialog from '@/components/FormDialog.vue'
|
||||||
import './settings/settings.css'
|
|
||||||
import './settings/modelSettings.css'
|
|
||||||
|
|
||||||
// 当前选中的设置菜单
|
// 当前选中的设置菜单
|
||||||
const activeMenu = ref('general')
|
const activeMenu = ref('general')
|
||||||
@@ -164,22 +162,25 @@ const clearLogs = () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="settings-page">
|
<div class="p-6 min-h-screen">
|
||||||
<!-- 页面标题 -->
|
<!-- 页面标题 -->
|
||||||
<div class="flex items-center gap-2 mb-6">
|
<div class="flex items-center gap-2 mb-6">
|
||||||
<i class="fa-solid fa-gear text-orange-500"></i>
|
<i class="fa-solid fa-gear text-orange-500"></i>
|
||||||
<span class="font-medium">Settings</span>
|
<span class="font-medium">Settings</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-container">
|
<div class="flex gap-6">
|
||||||
<!-- 左侧菜单 -->
|
<!-- 左侧菜单 -->
|
||||||
<nav class="settings-nav">
|
<nav class="w-48 flex-shrink-0">
|
||||||
<ul>
|
<ul class="space-y-1">
|
||||||
<li
|
<li
|
||||||
v-for="item in menuItems"
|
v-for="item in menuItems"
|
||||||
:key="item.key"
|
:key="item.key"
|
||||||
:class="['nav-item', { active: activeMenu === item.key }]"
|
|
||||||
@click="activeMenu = item.key"
|
@click="activeMenu = item.key"
|
||||||
|
class="px-4 py-3 rounded-lg cursor-pointer transition-colors flex items-center gap-3"
|
||||||
|
:class="activeMenu === item.key
|
||||||
|
? 'bg-orange-500/10 text-orange-400'
|
||||||
|
: 'text-gray-400 hover:bg-dark-600 hover:text-white'"
|
||||||
>
|
>
|
||||||
<i :class="['fa-solid', item.icon]"></i>
|
<i :class="['fa-solid', item.icon]"></i>
|
||||||
<span>{{ item.label }}</span>
|
<span>{{ item.label }}</span>
|
||||||
@@ -188,13 +189,19 @@ const clearLogs = () => {
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- 右侧内容 -->
|
<!-- 右侧内容 -->
|
||||||
<div class="settings-content">
|
<div class="flex-1 space-y-4">
|
||||||
<!-- General 设置 -->
|
<!-- General 设置 -->
|
||||||
<div v-if="activeMenu === 'general'" class="settings-section">
|
<div v-if="activeMenu === 'general'">
|
||||||
<h2 class="section-title">General Settings</h2>
|
<div class="flex items-center justify-between mb-6">
|
||||||
<p class="section-desc">Manage your personal information and preferences</p>
|
<div>
|
||||||
|
<h2 class="text-xl font-semibold">General Settings</h2>
|
||||||
|
<p class="text-sm text-gray-400 mt-1">Manage your personal information and preferences</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-form :model="generalForm" label-position="top" class="settings-form">
|
<div class="bg-dark-700 rounded-xl p-6">
|
||||||
|
<el-form :model="generalForm" label-position="top">
|
||||||
|
<div class="grid grid-cols-2 gap-6">
|
||||||
<el-form-item label="Name">
|
<el-form-item label="Name">
|
||||||
<el-input v-model="generalForm.name" placeholder="Enter your name" />
|
<el-input v-model="generalForm.name" placeholder="Enter your name" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -204,14 +211,14 @@ const clearLogs = () => {
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="Password">
|
<el-form-item label="Password">
|
||||||
<div class="password-field">
|
<div class="flex gap-3">
|
||||||
<el-input v-model="generalForm.password" type="password" disabled />
|
<el-input v-model="generalForm.password" type="password" disabled class="flex-1" />
|
||||||
<el-button @click="showChangePassword">Change</el-button>
|
<el-button @click="showChangePassword">Change</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="Language">
|
<el-form-item label="Language">
|
||||||
<el-select v-model="generalForm.language" placeholder="Select language">
|
<el-select v-model="generalForm.language" placeholder="Select language" class="w-full">
|
||||||
<el-option
|
<el-option
|
||||||
v-for="lang in languageOptions"
|
v-for="lang in languageOptions"
|
||||||
:key="lang.value"
|
:key="lang.value"
|
||||||
@@ -222,7 +229,7 @@ const clearLogs = () => {
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="Timezone">
|
<el-form-item label="Timezone">
|
||||||
<el-select v-model="generalForm.timezone" placeholder="Select timezone">
|
<el-select v-model="generalForm.timezone" placeholder="Select timezone" class="w-full">
|
||||||
<el-option
|
<el-option
|
||||||
v-for="tz in timezoneOptions"
|
v-for="tz in timezoneOptions"
|
||||||
:key="tz.value"
|
:key="tz.value"
|
||||||
@@ -231,87 +238,101 @@ const clearLogs = () => {
|
|||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-form-item>
|
<div class="mt-4">
|
||||||
<el-button type="primary" @click="saveChanges">Save Changes</el-button>
|
<el-button type="primary" @click="saveChanges">Save Changes</el-button>
|
||||||
</el-form-item>
|
</div>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- Members 设置 -->
|
<!-- Members 设置 -->
|
||||||
<div v-if="activeMenu === 'members'" class="settings-section">
|
<div v-if="activeMenu === 'members'">
|
||||||
<h2 class="section-title">Members</h2>
|
<div class="flex items-center justify-between mb-6">
|
||||||
<p class="section-desc">Manage team members</p>
|
<div>
|
||||||
|
<h2 class="text-xl font-semibold">Members</h2>
|
||||||
|
<p class="text-sm text-gray-400 mt-1">Manage team members</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Notifications 设置 -->
|
<!-- Notifications 设置 -->
|
||||||
<div v-if="activeMenu === 'notifications'" class="settings-section">
|
<div v-if="activeMenu === 'notifications'">
|
||||||
<h2 class="section-title">Notifications</h2>
|
<div class="flex items-center justify-between mb-6">
|
||||||
<p class="section-desc">Configure notification preferences</p>
|
<div>
|
||||||
|
<h2 class="text-xl font-semibold">Notifications</h2>
|
||||||
|
<p class="text-sm text-gray-400 mt-1">Configure notification preferences</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Model Settings 设置 -->
|
<!-- Model Settings 设置 -->
|
||||||
<div v-if="activeMenu === 'modelSettings'" class="settings-section">
|
<div v-if="activeMenu === 'modelSettings'">
|
||||||
<div class="flex justify-between items-center mb-6">
|
<div class="flex items-center justify-between mb-6">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="section-title">Model Settings</h2>
|
<h2 class="text-xl font-semibold">Model Settings</h2>
|
||||||
<p class="section-desc">Configure AI model settings</p>
|
<p class="text-sm text-gray-400 mt-1">Configure AI model settings</p>
|
||||||
</div>
|
</div>
|
||||||
<el-button type="primary" class="add-model-btn" @click="showAddModelForm = true">
|
<button class="bg-gradient-to-r from-primary-orange to-red-500 hover:from-orange-500 hover:to-red-600 text-white px-4 py-2 rounded-lg font-medium flex items-center gap-2 transition-all" @click="showAddModelForm = true">
|
||||||
<i class="fa-solid fa-plus mr-2"></i>
|
<i class="fa-solid fa-plus"></i>
|
||||||
Add New Model
|
Add New Model
|
||||||
</el-button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 模型列表 -->
|
<!-- 模型列表 -->
|
||||||
<div v-if="modelsLoading" class="loading-spinner">
|
<div v-if="modelsLoading" class="py-12 text-center text-gray-500">
|
||||||
<i class="fa-solid fa-spinner"></i>
|
<i class="fa-solid fa-spinner fa-spin text-2xl"></i>
|
||||||
</div>
|
</div>
|
||||||
<table v-else class="model-table">
|
<div v-else class="bg-dark-700 rounded-xl overflow-hidden">
|
||||||
<thead>
|
<table class="w-full">
|
||||||
|
<thead class="bg-dark-600">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Model Name</th>
|
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Model Name</th>
|
||||||
<th class="text-center">Provider</th>
|
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Provider</th>
|
||||||
<th class="text-center">Model</th>
|
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Model</th>
|
||||||
<th class="text-center">Model Type</th>
|
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Model Type</th>
|
||||||
<th class="text-center">Base URL</th>
|
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Base URL</th>
|
||||||
<th class="text-center">Status</th>
|
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Status</th>
|
||||||
<th class="text-center">Actions</th>
|
<th class="text-right px-5 py-3 text-sm font-medium text-gray-400">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="model in models" :key="model.id" class="table-row">
|
<tr v-for="model in models" :key="model.id" class="border-t border-dark-600 hover:bg-dark-600/50">
|
||||||
<td>
|
<td class="px-5 py-4">
|
||||||
<span class="font-medium">{{ model.name }}</span>
|
<span class="font-medium">{{ model.name }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center text-sm">
|
<td class="px-5 py-4 text-sm text-gray-300">
|
||||||
{{ model.provider }}
|
{{ model.provider }}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center text-sm">
|
<td class="px-5 py-4 text-sm text-gray-300">
|
||||||
{{ model.model }}
|
{{ model.model }}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center">
|
<td class="px-5 py-4">
|
||||||
<span class="model-type-tag" :class="model.model_type">{{ model.model_type }}</span>
|
<span class="px-2 py-1 rounded text-xs" :class="model.model_type === 'chat' ? 'bg-primary-cyan/20 text-primary-cyan' : 'bg-purple-500/20 text-purple-400'">{{ model.model_type }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center text-sm">
|
<td class="px-5 py-4 text-sm text-gray-300">
|
||||||
{{ model.base_url }}
|
{{ model.base_url }}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center">
|
<td class="px-5 py-4">
|
||||||
<span v-if="model.status === 'active'" class="status-active">Active</span>
|
<span v-if="model.status === 'active'" class="px-2 py-1 rounded text-xs bg-primary-success/20 text-primary-success">Active</span>
|
||||||
<span v-else class="status-inactive">Inactive</span>
|
<span v-else class="px-2 py-1 rounded text-xs bg-gray-500/20 text-gray-400">Inactive</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center">
|
<td class="px-5 py-4">
|
||||||
<button class="btn-icon" title="Edit" @click="openEditDialog(model)">
|
<div class="flex items-center justify-end gap-2">
|
||||||
<i class="fa-solid fa-pen"></i>
|
<button class="p-2 rounded-lg hover:bg-dark-500 transition-colors" title="Edit" @click="openEditDialog(model)">
|
||||||
|
<i class="fa-solid fa-pen text-gray-400 hover:text-white"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-icon" title="Delete" @click="deleteModel(model.id)">
|
<button class="p-2 rounded-lg hover:bg-dark-500 transition-colors" title="Delete" @click="deleteModel(model.id)">
|
||||||
<i class="fa-solid fa-trash"></i>
|
<i class="fa-solid fa-trash text-gray-400 hover:text-red-400"></i>
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 新增模型弹窗 -->
|
<!-- 新增模型弹窗 -->
|
||||||
<FormDialog
|
<FormDialog
|
||||||
@@ -514,11 +535,11 @@ const clearLogs = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Logs 设置 -->
|
<!-- Logs 设置 -->
|
||||||
<div v-if="activeMenu === 'logs'" class="settings-section">
|
<div v-if="activeMenu === 'logs'">
|
||||||
<div class="flex items-center justify-between mb-6">
|
<div class="flex items-center justify-between mb-6">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="section-title">Logs</h2>
|
<h2 class="text-xl font-semibold">Logs</h2>
|
||||||
<p class="section-desc">View system logs</p>
|
<p class="text-sm text-gray-400 mt-1">View system logs</p>
|
||||||
</div>
|
</div>
|
||||||
<button @click="clearLogs" class="px-4 py-2 rounded-lg bg-dark-600 text-gray-300 hover:bg-dark-500 transition-colors flex items-center gap-2">
|
<button @click="clearLogs" class="px-4 py-2 rounded-lg bg-dark-600 text-gray-300 hover:bg-dark-500 transition-colors flex items-center gap-2">
|
||||||
<i class="fa-solid fa-trash"></i>
|
<i class="fa-solid fa-trash"></i>
|
||||||
|
|||||||
232
web/src/views/Signup.vue
Normal file
232
web/src/views/Signup.vue
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const username = ref('')
|
||||||
|
const email = ref('')
|
||||||
|
const password = ref('')
|
||||||
|
const confirmPassword = ref('')
|
||||||
|
const agreeTerms = ref(false)
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const showPassword = ref(false)
|
||||||
|
const showConfirmPassword = ref(false)
|
||||||
|
const errorMsg = ref('')
|
||||||
|
|
||||||
|
// 表单验证
|
||||||
|
const validateForm = () => {
|
||||||
|
errorMsg.value = ''
|
||||||
|
|
||||||
|
if (!username.value || username.value.length < 3) {
|
||||||
|
errorMsg.value = 'Username must be at least 3 characters'
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!email.value || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value)) {
|
||||||
|
errorMsg.value = 'Please enter a valid email address'
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!password.value || password.value.length < 6) {
|
||||||
|
errorMsg.value = 'Password must be at least 6 characters'
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.value !== confirmPassword.value) {
|
||||||
|
errorMsg.value = 'Passwords do not match'
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!agreeTerms.value) {
|
||||||
|
errorMsg.value = 'Please agree to the Terms of Service'
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册处理
|
||||||
|
const handleSignup = () => {
|
||||||
|
if (!validateForm()) return
|
||||||
|
|
||||||
|
isLoading.value = true
|
||||||
|
// 模拟注册
|
||||||
|
setTimeout(() => {
|
||||||
|
isLoading.value = false
|
||||||
|
// 注册成功,跳转到登录页
|
||||||
|
router.push('/')
|
||||||
|
}, 1500)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳转到登录
|
||||||
|
const goToLogin = () => {
|
||||||
|
router.push('/')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="min-h-screen bg-dark-900 flex items-center justify-center p-4">
|
||||||
|
<!-- 背景装饰 -->
|
||||||
|
<div class="absolute inset-0 overflow-hidden">
|
||||||
|
<div class="absolute top-1/4 -left-20 w-80 h-80 bg-primary-orange/10 rounded-full blur-3xl"></div>
|
||||||
|
<div class="absolute bottom-1/4 -right-20 w-80 h-80 bg-primary-purple/10 rounded-full blur-3xl"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 注册卡片 -->
|
||||||
|
<div class="w-full max-w-md relative">
|
||||||
|
<!-- Logo -->
|
||||||
|
<div class="text-center mb-8">
|
||||||
|
<div class="w-14 h-14 rounded-2xl bg-gradient-to-br from-primary-orange to-red-500 flex items-center justify-center mx-auto mb-4 shadow-lg shadow-primary-orange/20">
|
||||||
|
<i class="fa-solid fa-basketball text-white text-2xl"></i>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-2xl font-bold text-white">Create an account</h1>
|
||||||
|
<p class="text-gray-400 mt-2">Join us and start your journey</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 注册表单 -->
|
||||||
|
<div class="bg-dark-700 rounded-2xl p-8 shadow-xl border border-dark-500/50">
|
||||||
|
<!-- 错误提示 -->
|
||||||
|
<div v-if="errorMsg" class="mb-4 p-3 bg-red-500/10 border border-red-500/30 rounded-lg text-red-400 text-sm flex items-center gap-2">
|
||||||
|
<i class="fa-solid fa-circle-exclamation"></i>
|
||||||
|
{{ errorMsg }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form @submit.prevent="handleSignup" class="space-y-4">
|
||||||
|
<!-- 用户名 -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-300 mb-2">Username</label>
|
||||||
|
<div class="relative">
|
||||||
|
<i class="fa-solid fa-user absolute left-4 top-1/2 -translate-y-1/2 text-gray-400"></i>
|
||||||
|
<input
|
||||||
|
v-model="username"
|
||||||
|
type="text"
|
||||||
|
placeholder="Choose a username"
|
||||||
|
required
|
||||||
|
class="w-full bg-dark-600 border border-dark-500 rounded-xl py-3 pl-12 pr-4 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange focus:ring-1 focus:ring-primary-orange transition-colors"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 邮箱 -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-300 mb-2">Email</label>
|
||||||
|
<div class="relative">
|
||||||
|
<i class="fa-solid fa-envelope absolute left-4 top-1/2 -translate-y-1/2 text-gray-400"></i>
|
||||||
|
<input
|
||||||
|
v-model="email"
|
||||||
|
type="email"
|
||||||
|
placeholder="Enter your email"
|
||||||
|
required
|
||||||
|
class="w-full bg-dark-600 border border-dark-500 rounded-xl py-3 pl-12 pr-4 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange focus:ring-1 focus:ring-primary-orange transition-colors"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 密码 -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-300 mb-2">Password</label>
|
||||||
|
<div class="relative">
|
||||||
|
<i class="fa-solid fa-lock absolute left-4 top-1/2 -translate-y-1/2 text-gray-400"></i>
|
||||||
|
<input
|
||||||
|
v-model="password"
|
||||||
|
:type="showPassword ? 'text' : 'password'"
|
||||||
|
placeholder="Create a password"
|
||||||
|
required
|
||||||
|
class="w-full bg-dark-600 border border-dark-500 rounded-xl py-3 pl-12 pr-12 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange focus:ring-1 focus:ring-primary-orange transition-colors"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="showPassword = !showPassword"
|
||||||
|
class="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-300 transition-colors"
|
||||||
|
>
|
||||||
|
<i :class="['fa-solid', showPassword ? 'fa-eye-slash' : 'fa-eye']"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 确认密码 -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-300 mb-2">Confirm Password</label>
|
||||||
|
<div class="relative">
|
||||||
|
<i class="fa-solid fa-lock absolute left-4 top-1/2 -translate-y-1/2 text-gray-400"></i>
|
||||||
|
<input
|
||||||
|
v-model="confirmPassword"
|
||||||
|
:type="showConfirmPassword ? 'text' : 'password'"
|
||||||
|
placeholder="Confirm your password"
|
||||||
|
required
|
||||||
|
class="w-full bg-dark-600 border border-dark-500 rounded-xl py-3 pl-12 pr-12 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange focus:ring-1 focus:ring-primary-orange transition-colors"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="showConfirmPassword = !showConfirmPassword"
|
||||||
|
class="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-300 transition-colors"
|
||||||
|
>
|
||||||
|
<i :class="['fa-solid', showConfirmPassword ? 'fa-eye-slash' : 'fa-eye']"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 服务条款 -->
|
||||||
|
<div class="flex items-start">
|
||||||
|
<label class="flex items-center cursor-pointer">
|
||||||
|
<input
|
||||||
|
v-model="agreeTerms"
|
||||||
|
type="checkbox"
|
||||||
|
class="sr-only peer"
|
||||||
|
>
|
||||||
|
<div class="w-5 h-5 border-2 border-dark-400 rounded-md peer-checked:border-primary-orange peer-checked:bg-primary-orange transition-colors flex items-center justify-center">
|
||||||
|
<i v-if="agreeTerms" class="fa-solid fa-check text-white text-xs"></i>
|
||||||
|
</div>
|
||||||
|
<span class="ml-2 text-sm text-gray-400">
|
||||||
|
I agree to the
|
||||||
|
<a href="#" class="text-primary-orange hover:text-orange-400 transition-colors">Terms of Service</a>
|
||||||
|
and
|
||||||
|
<a href="#" class="text-primary-orange hover:text-orange-400 transition-colors">Privacy Policy</a>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 注册按钮 -->
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
:disabled="isLoading"
|
||||||
|
class="w-full bg-gradient-to-r from-primary-orange to-red-500 hover:from-orange-500 hover:to-red-600 text-white font-semibold py-3 rounded-xl transition-all duration-300 shadow-lg shadow-primary-orange/20 hover:shadow-primary-orange/40 flex items-center justify-center gap-2 disabled:opacity-70 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
<i v-if="isLoading" class="fa-solid fa-circle-notch fa-spin"></i>
|
||||||
|
<span>{{ isLoading ? 'Creating account...' : 'Create account' }}</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- 分割线 -->
|
||||||
|
<div class="relative my-6">
|
||||||
|
<div class="absolute inset-0 flex items-center">
|
||||||
|
<div class="w-full border-t border-dark-500"></div>
|
||||||
|
</div>
|
||||||
|
<div class="relative flex justify-center text-sm">
|
||||||
|
<span class="px-4 bg-dark-700 text-gray-500">Or sign up with</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 第三方注册 -->
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<button class="flex items-center justify-center gap-2 bg-dark-600 hover:bg-dark-500 border border-dark-500 rounded-xl py-2.5 text-gray-300 font-medium transition-colors">
|
||||||
|
<i class="fa-brands fa-google"></i>
|
||||||
|
<span>Google</span>
|
||||||
|
</button>
|
||||||
|
<button class="flex items-center justify-center gap-2 bg-dark-600 hover:bg-dark-500 border border-dark-500 rounded-xl py-2.5 text-gray-300 font-medium transition-colors">
|
||||||
|
<i class="fa-brands fa-github"></i>
|
||||||
|
<span>GitHub</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 登录链接 -->
|
||||||
|
<p class="text-center mt-6 text-gray-400">
|
||||||
|
Already have an account?
|
||||||
|
<a @click="goToLogin" class="text-primary-orange hover:text-orange-400 font-medium transition-colors cursor-pointer">Sign in</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
Reference in New Issue
Block a user