feat: 完善人工关注与内容合规配置

This commit is contained in:
2026-08-24 17:49:56 +08:00
parent 6ffdd2c259
commit 910e67bd89
53 changed files with 1808 additions and 49 deletions

View File

@@ -0,0 +1,174 @@
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy import func, or_, select
from sqlalchemy.orm import Session
from app.api.pagination import page_result
from app.core.database import get_db
from app.core.dependencies import get_current_admin
from app.core.responses import api_success
from app.models.admin import Admin
from app.models.chat import ChatMessage, ChatSession
from app.models.knowledge import KnowledgeRetrievalLog
from app.models.user import User
from app.schemas.knowledge import AttentionConfigRequest, AttentionPreviewRequest
from app.services.admin_service import OperationLogService
from app.services.human_attention_service import HumanAttentionService
router = APIRouter()
@router.get("/attention/config")
def get_attention_config(
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
return api_success(HumanAttentionService.config_dict(HumanAttentionService.get_config(db)))
@router.put("/attention/config")
def save_attention_config(
payload: AttentionConfigRequest,
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
config = HumanAttentionService.save_config(db, payload.model_dump(), current_admin.id)
OperationLogService.write(db, admin_id=current_admin.id, module="attention", action="config_update")
db.commit()
return api_success(HumanAttentionService.config_dict(config))
@router.get("/attention/preview/messages")
def attention_preview_messages(
keyword: str = Query(default="", max_length=100),
page: int = Query(default=1, ge=1),
pageSize: int = Query(default=10, ge=5, le=50),
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
query = (
select(ChatMessage, ChatSession, User)
.join(ChatSession, ChatSession.id == ChatMessage.session_id)
.join(User, User.id == ChatMessage.user_id)
.where(ChatMessage.role == "user", ChatMessage.message_status == "FINISHED")
)
normalized_keyword = keyword.strip()
if normalized_keyword:
pattern = f"%{normalized_keyword}%"
query = query.where(or_(ChatMessage.content.like(pattern), User.name.like(pattern), User.phone.like(pattern)))
total = db.scalar(select(func.count()).select_from(query.order_by(None).subquery())) or 0
rows = db.execute(
query.order_by(ChatMessage.id.desc()).offset((page - 1) * pageSize).limit(pageSize)
).all()
items = [_preview_message_dict(db, message, session, user) for message, session, user in rows]
return api_success(page_result(items, total=total, page=page, page_size=pageSize))
@router.get("/attention/preview/config")
def get_attention_preview_config(
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
return api_success(HumanAttentionService.config_dict(HumanAttentionService.get_config(db)))
@router.post("/attention/preview")
def preview_attention_filter(
payload: AttentionPreviewRequest,
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
messages = db.scalars(
select(ChatMessage).where(
ChatMessage.id.in_(payload.messageIds),
ChatMessage.role == "user",
ChatMessage.message_status == "FINISHED",
)
).all()
by_id = {message.id: message for message in messages}
missing_ids = [message_id for message_id in payload.messageIds if message_id not in by_id]
if missing_ids:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"历史消息不存在:{missing_ids[0]}")
results = []
for message_id in payload.messageIds:
message = by_id[message_id]
answer, knowledge_missing = _answer_and_knowledge_state(db, message)
decision = HumanAttentionService.preview(
db,
question=message.content,
answer=answer,
knowledge_missing=knowledge_missing,
config_payload=payload.config.model_dump(),
user_id=message.user_id,
)
results.append(
{
"messageId": message.id,
"sessionId": message.session_id,
"question": message.content,
"answer": answer,
"knowledgeMissing": knowledge_missing,
"needsAttention": decision.needs_attention,
"priority": decision.priority,
"reason": decision.reason,
"summary": decision.summary,
"source": decision.source,
"rawOutput": decision.raw_output,
"renderedPrompt": decision.rendered_prompt,
"matchedItem": decision.matched_item,
}
)
OperationLogService.write(
db,
admin_id=current_admin.id,
module="attention",
action=f"preview:{len(results)}",
)
db.commit()
return api_success(results)
def _preview_message_dict(db: Session, message: ChatMessage, session: ChatSession, user: User) -> dict:
answer, knowledge_missing = _answer_and_knowledge_state(db, message)
return {
"messageId": message.id,
"sessionId": message.session_id,
"userId": message.user_id,
"userName": user.name,
"userPhone": user.phone,
"sessionTitle": session.title,
"question": message.content,
"answer": answer,
"knowledgeMissing": knowledge_missing,
"createdAt": message.created_at,
}
def _answer_and_knowledge_state(db: Session, user_message: ChatMessage) -> tuple[str, bool]:
answer = db.scalar(
select(ChatMessage)
.where(
ChatMessage.session_id == user_message.session_id,
ChatMessage.user_id == user_message.user_id,
ChatMessage.role == "assistant",
ChatMessage.id > user_message.id,
)
.order_by(ChatMessage.id.asc())
.limit(1)
)
if answer is None:
return "", False
retrieval_log = db.scalar(
select(KnowledgeRetrievalLog)
.where(KnowledgeRetrievalLog.message_id == answer.id)
.order_by(KnowledgeRetrievalLog.id.desc())
.limit(1)
)
knowledge_missing = bool(
retrieval_log
and retrieval_log.knowledge_called == 1
and not (retrieval_log.final_section_ids or "").strip()
)
return answer.content, knowledge_missing

View File

@@ -30,6 +30,7 @@ from app.services.agent_debug_service import AgentDebugService
from app.services.feishu_service import FeishuKnowledgeService
from app.services.knowledge_service import KnowledgeScope
from app.services.model_service import ModelClientService
from app.services.public_site_config_service import PublicSiteConfigService
from app.services.admin_permission_service import require_permission
from app.services.reasoning_policy_service import ReasoningPolicyService
from app.services.response_style_service import ResponseStyleService
@@ -477,6 +478,7 @@ def save_config(
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
payload.configValue = PublicSiteConfigService.normalize_admin_value(payload.configKey, payload.configValue)
config = db.scalar(select(SystemConfig).where(SystemConfig.config_key == payload.configKey))
if config is None:
config = SystemConfig(config_key=payload.configKey, config_value=payload.configValue)

View File

@@ -13,6 +13,7 @@ from sqlalchemy import func, select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
from app.core.ai_content_label import ensure_ai_generated_notice
from app.api.pagination import page_result
from app.core.database import get_db
from app.core.dependencies import get_current_admin, get_current_user
@@ -146,7 +147,7 @@ def _feedback_workbook(rows: list[tuple]) -> Workbook:
sheet = workbook.active
sheet.title = "反馈记录"
sheet.sheet_view.showGridLines = False
headers = ["序号", "状态", "用户姓名", "手机号", "反馈内容", "会话标题", "消息ID", "对应AI回答", "提交时间", "阅读时间"]
headers = ["序号", "状态", "用户姓名", "手机号", "反馈内容", "会话标题", "消息ID", "对应AI回答AI生成", "提交时间", "阅读时间"]
sheet.append(headers)
for index, (feedback, user, message, session) in enumerate(rows, start=1):
sheet.append([
@@ -157,7 +158,7 @@ def _feedback_workbook(rows: list[tuple]) -> Workbook:
_excel_safe_text(feedback.content),
_excel_safe_text(session.title),
message.id,
_excel_safe_text(message.content),
_excel_safe_text(ensure_ai_generated_notice(message.content)),
feedback.created_at,
feedback.read_at,
])

View File

@@ -0,0 +1,16 @@
from __future__ import annotations
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.core.responses import api_success
from app.services.public_site_config_service import PublicSiteConfigService
router = APIRouter()
@router.get("/config")
def public_site_config(db: Session = Depends(get_db)) -> dict:
return api_success(PublicSiteConfigService.public_config(db))

View File

@@ -5,6 +5,7 @@ from app.core.dependencies import enforce_admin_access
from app.api import (
admin_auth,
admin_attention,
admin_content_generation,
admin_user_behavior,
admin_agent_records,
@@ -23,6 +24,7 @@ from app.api import (
chat,
health,
integration_sso,
public_config,
user,
voice,
behavior,
@@ -30,6 +32,7 @@ from app.api import (
api_router = APIRouter()
api_router.include_router(health.router, tags=["health"])
api_router.include_router(public_config.router, prefix="/site", tags=["public-site-config"])
api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
api_router.include_router(integration_sso.router, prefix="/integration/sso", tags=["integration-sso"])
api_router.include_router(user.router, prefix="/user", tags=["user"])
@@ -40,6 +43,7 @@ api_router.include_router(behavior.router, prefix="/behavior", tags=["user-behav
api_router.include_router(admin_auth.router, prefix="/admin", tags=["admin"])
api_router.include_router(admin_management.router, prefix="/admin", tags=["admin-management"])
guard = [Depends(enforce_admin_access)]
api_router.include_router(admin_attention.router, prefix="/admin", tags=["admin-attention"], dependencies=guard)
api_router.include_router(admin_content_generation.router, prefix="/admin", tags=["admin-content-generation"], dependencies=guard)
api_router.include_router(admin_user_behavior.router, prefix="/admin", tags=["admin-user-behavior"], dependencies=guard)
api_router.include_router(admin_agent_records.router, prefix="/admin", tags=["admin-agent-records"], dependencies=guard)