Merge branch 'develop-mark' into develop

This commit is contained in:
Mark
2025-12-26 10:25:56 +08:00
7 changed files with 161 additions and 136 deletions

View File

@@ -674,7 +674,8 @@ async def draft_run_compare(
workspace_id=workspace_id,
user=current_user
)
if storage_type is None: storage_type = 'neo4j'
if storage_type is None:
storage_type = 'neo4j'
user_rag_memory_id = ''
if workspace_id:
knowledge = knowledge_repository.get_knowledge_by_name(
@@ -682,7 +683,8 @@ async def draft_run_compare(
name="USER_RAG_MERORY",
workspace_id=workspace_id
)
if knowledge: user_rag_memory_id = str(knowledge.id)
if knowledge:
user_rag_memory_id = str(knowledge.id)
logger.info(
"多模型对比试运行",

View File

@@ -1,31 +1,23 @@
from fastapi import APIRouter, Depends, Query, Request, Header
import hashlib
import uuid
from fastapi import APIRouter, Depends, Query, Request
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session
import uuid
import hashlib
import time
import jwt
from app.services import task_service, workspace_service
from typing import Optional, Dict
from functools import wraps
from app.dependencies import get_current_superuser, get_current_user, get_current_tenant, workspace_access_guard, cur_workspace_access_guard
from app.db import get_db
from app.core.response_utils import success
from app.core.logging_config import get_business_logger
from app.core.exceptions import BusinessException
from app.core.error_codes import BizCode
from app.core.config import settings
from app.core.response_utils import success
from app.db import get_db
from app.dependencies import get_share_user_id, ShareTokenData
from app.repositories import knowledge_repository
from app.schemas import release_share_schema, conversation_schema
from app.schemas.response_schema import PageData, PageMeta
from app.services import workspace_service
from app.services.auth_service import create_access_token
from app.services.conversation_service import ConversationService
from app.services.release_share_service import ReleaseShareService
from app.services.shared_chat_service import SharedChatService
from app.services.conversation_service import ConversationService
from app.services.auth_service import create_access_token
from app.dependencies import get_share_user_id, ShareTokenData
from app.models.user_model import User
from app.repositories.app_repository import AppRepository
from app.repositories.workspace_repository import WorkspaceRepository
from app.repositories import knowledge_repository
router = APIRouter(prefix="/public/share", tags=["Public Share"])
logger = get_business_logger()
@@ -37,31 +29,31 @@ def get_base_url(request: Request) -> str:
def get_or_generate_user_id(payload_user_id: str, request: Request) -> str:
"""获取或生成用户 ID
优先级:
1. 使用前端传递的 user_id
2. 基于 IP + User-Agent 生成唯一 ID
Args:
payload_user_id: 前端传递的 user_id
request: FastAPI Request 对象
Returns:
用户 ID
"""
if payload_user_id:
return payload_user_id
# 获取客户端 IP
client_ip = request.client.host if request.client else "unknown"
# 获取 User-Agent
user_agent = request.headers.get("user-agent", "unknown")
# 生成唯一 ID基于 IP + User-Agent 的哈希
unique_string = f"{client_ip}_{user_agent}"
hash_value = hashlib.md5(unique_string.encode()).hexdigest()[:16]
return f"guest_{hash_value}"
@@ -76,13 +68,13 @@ def get_access_token(
db: Session = Depends(get_db),
):
"""获取访问 token
- 用户通过 user_id + share_token 换取访问 token
- 后续请求需要携带此 token
"""
# 获取或生成 user_id
user_id = get_or_generate_user_id(payload.user_id, request)
# 验证分享链接(可选:验证密码)
service = ReleaseShareService(db)
try:
@@ -93,10 +85,10 @@ def get_access_token(
except Exception as e:
logger.error(f"获取分享信息失败: {str(e)}")
raise
# 生成 token
access_token = create_access_token(user_id, share_token)
logger.info(
"生成访问 token",
extra={
@@ -104,7 +96,7 @@ def get_access_token(
"user_id": user_id
}
)
return success(data={
"access_token": access_token,
"token_type": "Bearer",
@@ -123,7 +115,7 @@ def get_shared_release(
db: Session = Depends(get_db),
):
"""获取公开分享的发布版本信息
- 无需认证即可访问
- 如果设置了密码保护,需要提供正确的密码
- 如果密码错误或未提供密码,返回基本信息(不含配置详情)
@@ -133,7 +125,7 @@ def get_shared_release(
share_token=share_data.share_token,
password=password
)
return success(data=info)
@@ -147,7 +139,7 @@ def verify_password(
db: Session = Depends(get_db),
):
"""验证分享的访问密码
- 用于前端先验证密码,再获取完整信息
"""
service = ReleaseShareService(db)
@@ -155,7 +147,7 @@ def verify_password(
share_token=share_data.share_token,
password=payload.password
)
return success(data={"valid": is_valid})
@@ -163,7 +155,7 @@ def verify_password(
"/embed",
summary="获取嵌入代码"
)
def get_embed_code(
def get_embed_code(
width: str = Query("100%", description="iframe 宽度"),
height: str = Query("600px", description="iframe 高度"),
request: Request = None,
@@ -171,12 +163,12 @@ def get_embed_code(
db: Session = Depends(get_db),
):
"""获取嵌入代码
- 返回 iframe 嵌入代码
- 可以自定义宽度和高度
"""
base_url = get_base_url(request) if request else None
service = ReleaseShareService(db)
embed_code = service.get_embed_code(
share_token=share_data.share_token,
@@ -184,7 +176,7 @@ def get_embed_code(
height=height,
base_url=base_url
)
return success(data=embed_code)
@@ -203,7 +195,7 @@ def list_conversations(
db: Session = Depends(get_db),
):
"""获取分享应用的会话列表
- 可以按 user_id 筛选
- 支持分页
"""
@@ -214,7 +206,7 @@ def list_conversations(
from app.repositories.end_user_repository import EndUserRepository
end_user_repo = EndUserRepository(db)
new_end_user = end_user_repo.get_or_create_end_user(
app_id=share.app_id,
app_id=share.app_id,
other_id=other_id
)
logger.debug(new_end_user.id)
@@ -226,10 +218,10 @@ def list_conversations(
page=page,
pagesize=pagesize
)
items = [conversation_schema.Conversation.model_validate(c) for c in conversations]
meta = PageMeta(page=page, pagesize=pagesize, total=total, hasnext=(page * pagesize) < total)
return success(data=PageData(page=meta, items=items))
@@ -250,17 +242,17 @@ def get_conversation(
conversation_id=conversation_id,
password=password
)
# 获取消息
conv_service = ConversationService(db)
messages = conv_service.get_messages(conversation_id)
# 构建响应
conv_dict = conversation_schema.Conversation.model_validate(conversation).model_dump()
conv_dict["messages"] = [
conversation_schema.Message.model_validate(m) for m in messages
]
return success(data=conv_dict)
@@ -276,17 +268,17 @@ async def chat(
db: Session = Depends(get_db)
):
"""发送消息并获取回复
使用 Bearer token 认证:
- Header: Authorization: Bearer {token}
- user_id 和 share_token 从 token 中解码
- 支持多轮对话(提供 conversation_id
- 支持流式返回(设置 stream=true
- 如果不提供 conversation_id会自动创建新会话
"""
service = SharedChatService(db)
# 从依赖中获取 user_id 和 share_token
user_id = share_data.user_id
share_token = share_data.share_token
@@ -299,19 +291,19 @@ async def chat(
from app.models.app_model import AppType
try:
from app.core.exceptions import BusinessException
from app.core.error_codes import BizCode
from app.core.error_codes import BizCode
from app.services.app_service import AppService
# 验证分享链接和密码
share, release = service._get_release_by_share_token(share_token, password)
# # Create end_user_id by concatenating app_id with user_id
# end_user_id = f"{share.app_id}_{user_id}"
# Store end_user_id in database with original user_id
from app.repositories.end_user_repository import EndUserRepository
end_user_repo = EndUserRepository(db)
new_end_user = end_user_repo.get_or_create_end_user(
app_id=share.app_id,
app_id=share.app_id,
other_id=other_id,
original_user_id=user_id # Save original user_id to other_id
)
@@ -319,21 +311,21 @@ async def chat(
appid=share.app_id
"""获取存储类型和工作空间的ID"""
# 直接通过 SQLAlchemy 查询 app
from app.models.app_model import App
app = db.query(App).filter(App.id == appid).first()
if not app:
raise BusinessException("应用不存在", BizCode.APP_NOT_FOUND)
workspace_id = app.workspace_id
# 直接从 workspace 获取 storage_type公开分享场景无需权限检查
storage_type = workspace_service.get_workspace_storage_type_without_auth(
db=db,
workspace_id=workspace_id
)
if storage_type is None:
if storage_type is None:
storage_type = 'neo4j'
user_rag_memory_id = ''
@@ -357,7 +349,7 @@ async def chat(
# 获取应用类型
app_type = release.app.type if release.app else None
# 根据应用类型验证配置
if app_type == "agent":
# Agent 类型:验证模型配置
@@ -371,7 +363,7 @@ async def chat(
raise BusinessException("多 Agent 应用未配置子 Agent", BizCode.AGENT_CONFIG_MISSING)
else:
raise BusinessException(f"不支持的应用类型: {app_type}", BizCode.APP_TYPE_NOT_SUPPORTED)
# 获取或创建会话(提前验证)
conversation = service.create_or_get_conversation(
share_token=share_data.share_token,
@@ -379,7 +371,7 @@ async def chat(
user_id=str(new_end_user.id), # 转换为字符串
password=password
)
logger.debug(
"参数验证完成",
extra={
@@ -389,12 +381,12 @@ async def chat(
"stream": payload.stream
}
)
except Exception as e:
# 验证失败,直接抛出异常(会被 FastAPI 的异常处理器捕获)
logger.error(f"参数验证失败: {str(e)}")
raise
if app_type == AppType.AGENT:
# 流式返回
if payload.stream:
@@ -412,7 +404,7 @@ async def chat(
user_rag_memory_id=user_rag_memory_id
):
yield event
return StreamingResponse(
event_generator(),
media_type="text/event-stream",
@@ -422,7 +414,7 @@ async def chat(
"X-Accel-Buffering": "no"
}
)
# 非流式返回
result = await service.chat(
share_token=share_token,
@@ -454,7 +446,7 @@ async def chat(
user_rag_memory_id=user_rag_memory_id
):
yield event
return StreamingResponse(
event_generator(),
media_type="text/event-stream",
@@ -464,7 +456,7 @@ async def chat(
"X-Accel-Buffering": "no"
}
)
# 多 Agent 非流式返回
result = await service.multi_agent_chat(
share_token=share_token,
@@ -478,7 +470,7 @@ async def chat(
storage_type=storage_type,
user_rag_memory_id=user_rag_memory_id
)
return success(data=conversation_schema.ChatResponse(**result))
else:
from app.core.exceptions import BusinessException

View File

@@ -1,30 +1,28 @@
"""App 服务接口 - 基于 API Key 认证"""
import uuid
from typing import Annotated
from fastapi import APIRouter, Depends, Request, Body
from sqlalchemy.orm import Session
from typing import Optional, Annotated
from starlette.responses import StreamingResponse
from app.core.api_key_auth import require_api_key
from app.db import get_db
from app.core.response_utils import success
from app.core.error_codes import BizCode
from app.core.exceptions import BusinessException
from app.core.logging_config import get_business_logger
from app.core.response_utils import success
from app.db import get_db
from app.dependencies import get_app_or_workspace
from app.models import AppRelease
from app.repositories import knowledge_repository
from app.schemas import AppChatRequest, conversation_schema
from app.models.app_model import App
from app.models.app_model import AppType
from app.repositories import knowledge_repository
from app.repositories.end_user_repository import EndUserRepository
from app.core.exceptions import BusinessException
from app.core.error_codes import BizCode
from app.schemas import AppChatRequest, conversation_schema
from app.schemas.api_key_schema import ApiKeyAuth
from app.services import workspace_service
from app.services.app_chat_service import AppChatService, get_app_chat_service
from app.services.app_service import AppService
from app.services.conversation_service import ConversationService, get_conversation_service
from app.services.workflow_service import WorkflowService, get_workflow_service
from app.utils.app_config_utils import dict_to_multi_agent_config,dict_to_workflow_config,agent_config_4_app_release
from app.utils.app_config_utils import dict_to_multi_agent_config, dict_to_workflow_config, agent_config_4_app_release
from app.services.app_service import get_app_service, AppService
router = APIRouter(prefix="/app", tags=["V1 - App API"])
logger = get_business_logger()
@@ -37,8 +35,9 @@ async def list_apps():
# /v1/apps/{resource_id}/chat
# async def chat(
# @router.post("/chat")
# @require_api_key(scopes=["app"])
# async def chat2(
# request: Request,
# api_key_auth: ApiKeyAuth = None,
# db: Session = Depends(get_db),
@@ -57,7 +56,6 @@ async def list_apps():
# db: db_session
# """
# logger.info(f"API Key Auth: {api_key_auth}")
# logger.info(f"Resource ID: {resource_id}")
# logger.info(f"Message: {message}")
# return success(data={"received": True}, msg="消息已接收")
@@ -76,15 +74,21 @@ def _checkAppConfig(app: App):
raise BusinessException("不支持的应用类型", BizCode.AGENT_CONFIG_MISSING)
@router.post("/chat")
# @require_api_key(scopes=["app"])
@require_api_key(scopes=["app"])
async def chat(
payload: AppChatRequest,
app: App = Depends(get_app_or_workspace),
request:Request,
api_key_auth: ApiKeyAuth = None,
db: Session = Depends(get_db),
conversation_service: Annotated[ConversationService, Depends(get_conversation_service)] = None,
app_chat_service: Annotated[AppChatService, Depends(get_app_chat_service)] = None,
app_service: Annotated[AppService, Depends(get_app_service)] = None,
message: str = Body(..., description="聊天消息内容"),
):
body = await request.json()
payload = AppChatRequest(**body)
other_id = payload.user_id
app = app_service.get_app(api_key_auth.resource_id, api_key_auth.workspace_id)
other_id = payload.user_id
workspace_id = app.workspace_id
end_user_repo = EndUserRepository(db)
@@ -176,7 +180,7 @@ async def chat(
storage_type=storage_type,
user_rag_memory_id=user_rag_memory_id
)
return success(data=conversation_schema.ChatResponse(**result))
return success(data=conversation_schema.ChatResponse(**result).model_dump(mode="json"))
elif app_type == AppType.MULTI_AGENT:
# 多 Agent 流式返回
config = dict_to_multi_agent_config(app.current_release.config,app.id)
@@ -220,7 +224,7 @@ async def chat(
user_rag_memory_id=user_rag_memory_id
)
return success(data=conversation_schema.ChatResponse(**result))
return success(data=conversation_schema.ChatResponse(**result).model_dump(mode="json"))
elif app_type == AppType.WORKFLOW:
# 多 Agent 流式返回
config = dict_to_workflow_config(app.current_release.config,app.id)
@@ -264,7 +268,7 @@ async def chat(
user_rag_memory_id=user_rag_memory_id
)
return success(data=conversation_schema.ChatResponse(**result))
return success(data=conversation_schema.ChatResponse(**result).model_dump(mode="json"))
else:
from app.core.exceptions import BusinessException
from app.core.error_codes import BizCode

View File

@@ -1,26 +1,42 @@
"""多 Agent 相关数据模型"""
import datetime
import uuid
from enum import StrEnum
from sqlalchemy import Column, String, Boolean, DateTime, Integer, Float, Text, ForeignKey
from sqlalchemy.dialects.postgresql import UUID, JSON
from sqlalchemy.orm import relationship
from app.db import Base
class OrchestrationMode(StrEnum):
"""图标类型枚举"""
SEQUENTIAL = "sequential"
PARALLEL = "parallel"
CONDITIONAL = "conditional"
class AggregationStrategy(StrEnum):
"""图标类型枚举"""
MERGE = "merge"
VOTE = "vote"
PRIORITY = "priority"
class MultiAgentConfig(Base):
"""多 Agent 配置表"""
__tablename__ = "multi_agent_configs"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True)
# 关联应用
app_id = Column(UUID(as_uuid=True), ForeignKey("apps.id"), nullable=False, unique=True, index=True, comment="关联应用")
# 主 Agent (存储发布版本 ID)
master_agent_id = Column(UUID(as_uuid=True), ForeignKey("app_releases.id"), nullable=False, comment="主 Agent 发布版本 ID")
master_agent_id = Column(UUID(as_uuid=True), ForeignKey("app_releases.id"), nullable=True, comment="主 Agent 发布版本 ID")
master_agent_name = Column(String(100), comment="主 Agent 名称")
default_model_config_id = Column(UUID(as_uuid=True), ForeignKey("model_configs.id"), nullable=False,server_default=str(uuid.UUID(int=0)), index=True, comment="默认模型配置ID")
# 结构化配置(直接存储 JSON
model_parameters = Column(JSON, nullable=True, comment="模型参数配置temperature、max_tokens等")
# 协作模式
orchestration_mode = Column(
String(20),
@@ -28,7 +44,7 @@ class MultiAgentConfig(Base):
default="conditional",
comment="协作模式: sequential|parallel|conditional|loop"
)
# 子 Agent 列表
sub_agents = Column(
JSON,
@@ -36,13 +52,13 @@ class MultiAgentConfig(Base):
default=list,
comment="子 Agent 列表: [{'agent_id': 'uuid', 'name': '...', 'role': '...', 'priority': 1}]"
)
# 路由规则
routing_rules = Column(
JSON,
comment="路由规则: [{'condition': '...', 'target_agent_id': 'uuid', 'priority': 1}]"
)
# 执行配置
execution_config = Column(
JSON,
@@ -50,7 +66,7 @@ class MultiAgentConfig(Base):
default=dict,
comment="执行配置: {'max_iterations': 5, 'timeout': 60, 'parallel_limit': 3}"
)
# 结果整合策略
aggregation_strategy = Column(
String(20),
@@ -58,12 +74,12 @@ class MultiAgentConfig(Base):
default="merge",
comment="结果整合策略: merge|vote|priority|custom"
)
# 状态
is_active = Column(Boolean, default=True, nullable=False)
created_at = Column(DateTime, default=datetime.datetime.now)
updated_at = Column(DateTime, default=datetime.datetime.now, onupdate=datetime.datetime.now)
# 关系
app = relationship("App")
master_agent_release = relationship("AppRelease", foreign_keys=[master_agent_id])
@@ -77,7 +93,7 @@ class AgentInvocation(Base):
__tablename__ = "agent_invocations"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True)
# 调用关系
caller_agent_id = Column(
UUID(as_uuid=True),
@@ -93,7 +109,7 @@ class AgentInvocation(Base):
index=True,
comment="被调用者 Agent ID"
)
# 关联信息
conversation_id = Column(
UUID(as_uuid=True),
@@ -106,12 +122,12 @@ class AgentInvocation(Base):
index=True,
comment="父调用 ID用于追踪调用链"
)
# 输入输出
input_message = Column(Text, nullable=False, comment="输入消息")
output_message = Column(Text, comment="输出消息")
context = Column(JSON, comment="上下文信息")
# 状态
status = Column(
String(20),
@@ -121,18 +137,18 @@ class AgentInvocation(Base):
comment="状态: pending|running|completed|failed"
)
error_message = Column(Text, comment="错误信息")
# 性能指标
started_at = Column(DateTime, nullable=False, default=datetime.datetime.now, index=True)
completed_at = Column(DateTime)
elapsed_time = Column(Float, comment="耗时(秒)")
token_usage = Column(JSON, comment="Token 使用情况")
# 元数据
meta_data = Column(JSON, comment="额外元数据")
created_at = Column(DateTime, default=datetime.datetime.now)
# 关系
caller = relationship("AgentConfig", foreign_keys=[caller_agent_id])
callee = relationship("AgentConfig", foreign_keys=[callee_agent_id])

View File

@@ -9,8 +9,9 @@
"""
import datetime
import uuid
from typing import Optional, List, Dict, Any, Tuple, Type
from typing import Optional, List, Dict, Any, Tuple, Annotated
from fastapi import Depends
from sqlalchemy import select, func, or_, and_
from sqlalchemy.orm import Session
@@ -20,6 +21,7 @@ from app.core.exceptions import (
BusinessException,
)
from app.core.logging_config import get_business_logger
from app.db import get_db
from app.models import App, AgentConfig, AppRelease, MultiAgentConfig, WorkflowConfig
from app.models.app_model import AppStatus, AppType
from app.repositories.app_repository import get_apps_by_id
@@ -27,6 +29,7 @@ from app.repositories.workflow_repository import WorkflowConfigRepository
from app.schemas import app_schema
from app.schemas.workflow_schema import WorkflowConfigUpdate
from app.services.agent_config_converter import AgentConfigConverter
from app.models import AppShare, Workspace
# 获取业务日志器
logger = get_business_logger()
@@ -1390,7 +1393,7 @@ class AppService:
target_workspace_ids: List[uuid.UUID],
user_id: uuid.UUID,
workspace_id: Optional[uuid.UUID] = None
) -> List["AppShare"]:
) -> AppShare:
"""分享应用到其他工作空间
Args:
@@ -1406,7 +1409,7 @@ class AppService:
ResourceNotFoundException: 当应用不存在时
BusinessException: 当应用不在指定工作空间或目标工作空间无效时
"""
from app.models import AppShare, Workspace
logger.info(
"分享应用",
@@ -1548,7 +1551,7 @@ class AppService:
*,
app_id: uuid.UUID,
workspace_id: Optional[uuid.UUID] = None
) -> List["AppShare"]:
) -> List[AppShare]:
"""列出应用的所有分享记录
Args:
@@ -2094,3 +2097,14 @@ async def draft_run_stream(
workspace_id=workspace_id
):
yield event
# ==================== 依赖注入函数 ====================
def get_app_service(
db: Annotated[Session, Depends(get_db)]
) -> AppService:
"""获取工作流服务(依赖注入)"""
return AppService(db)

View File

@@ -1,16 +1,18 @@
"""会话服务"""
import uuid
from typing import Optional, List, Tuple, Annotated
from typing import Annotated
from typing import Optional, List, Tuple
from fastapi import Depends
from sqlalchemy.orm import Session
from sqlalchemy import select, desc
from sqlalchemy.orm import Session
from app.core.error_codes import BizCode
from app.core.exceptions import BusinessException
from app.core.exceptions import ResourceNotFoundException
from app.core.logging_config import get_business_logger
from app.db import get_db
from app.models import Conversation, Message
from app.core.exceptions import ResourceNotFoundException, BusinessException
from app.core.error_codes import BizCode
from app.core.logging_config import get_business_logger
logger = get_business_logger()

View File

@@ -11,31 +11,26 @@ from app.core.exceptions import BusinessException, PermissionDeniedException
from app.core.logging_config import get_business_logger
from app.models.user_model import User
# 获取业务逻辑专用日志器
business_logger = get_business_logger()
from app.models.workspace_model import (
InviteStatus,
Workspace,
WorkspaceMember,
WorkspaceRole,
from app.schemas.workspace_schema import (
WorkspaceModelsUpdate,
)
from sqlalchemy.orm import Session
from app.models.workspace_model import Workspace, WorkspaceRole, InviteStatus, WorkspaceMember
from app.repositories import workspace_repository
from app.repositories.workspace_invite_repository import WorkspaceInviteRepository
from app.schemas.workspace_schema import (
InviteAcceptRequest,
InviteValidateResponse,
WorkspaceCreate,
WorkspaceUpdate,
WorkspaceInviteCreate,
WorkspaceInviteResponse,
WorkspaceMemberUpdate,
WorkspaceModelsUpdate,
WorkspaceUpdate,
InviteValidateResponse,
InviteAcceptRequest,
WorkspaceMemberUpdate
)
from dotenv import load_dotenv
from sqlalchemy.orm import Session
# 获取业务逻辑专用日志器
business_logger = get_business_logger()
from dotenv import load_dotenv
load_dotenv()
def switch_workspace(
db: Session,
@@ -329,7 +324,7 @@ def _check_workspace_admin_permission(db: Session, workspace_id: uuid.UUID, user
)
# 使用统一权限服务检查管理权限
from app.core.permissions import Action, Resource, Subject, permission_service
from app.core.permissions import permission_service, Subject, Resource, Action
# 获取用户的工作空间成员关系
member = workspace_repository.get_member_in_workspace(