```
feat: 添加飞书用户模块和订阅功能支持 - 新增feishu_users模块用于处理飞书用户身份验证和权限管理 - 新增subscriptions模块用于处理订阅相关功能 - 新增personalization模块用于个性化服务 - 在alembic迁移配置中注册新的模型模块 - 在API路由器中添加feishu_users和subscriptions路由 - 实现事件调度服务的改进,包括错误处理和状态更新优化 - 添加飞书命令处理的权限检查机制 - 实现飞书应用票据事件处理 - 改进审计日志记录功能 ```
This commit is contained in:
300
app/application/feishu/handlers/personalization.py
Normal file
300
app/application/feishu/handlers/personalization.py
Normal file
@@ -0,0 +1,300 @@
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.application.feishu.delivery import send_text_if_configured
|
||||
from app.application.feishu.results import command_result
|
||||
from app.modules.audit.constants import AuditRiskLevel, AuditSource
|
||||
from app.modules.audit.schemas import AuditLogCreate
|
||||
from app.modules.audit.service import AuditService
|
||||
from app.modules.feishu.constants import FeishuCommandName, FeishuReplyType
|
||||
from app.modules.feishu.service import FeishuService
|
||||
from app.modules.feishu_users.principal import FeishuPrincipal
|
||||
from app.modules.personalization.constants import PreferenceCategory
|
||||
from app.modules.personalization.services import ConversationService, PreferenceService
|
||||
|
||||
PERSONALIZATION_TITLE = "个人设置"
|
||||
PREFERENCE_HELP = (
|
||||
"偏好指令格式:\n"
|
||||
"记住偏好 语言:中文\n"
|
||||
"记住偏好 语气:简洁\n"
|
||||
"记住偏好 详略:详细\n"
|
||||
"关注主题:人工智能\n"
|
||||
"我的偏好\n"
|
||||
"删除偏好 <偏好编号>"
|
||||
)
|
||||
_CATEGORY_LABELS = {
|
||||
"语言": PreferenceCategory.LANGUAGE,
|
||||
"语气": PreferenceCategory.TONE,
|
||||
"详略": PreferenceCategory.DETAIL,
|
||||
"主题": PreferenceCategory.TOPIC,
|
||||
"兴趣": PreferenceCategory.INTEREST,
|
||||
}
|
||||
_CATEGORY_NAMES = {
|
||||
PreferenceCategory.LANGUAGE: "语言",
|
||||
PreferenceCategory.TONE: "语气",
|
||||
PreferenceCategory.DETAIL: "详略",
|
||||
PreferenceCategory.TOPIC: "关注主题",
|
||||
PreferenceCategory.INTEREST: "兴趣",
|
||||
}
|
||||
_SET_PATTERN = re.compile(
|
||||
r"^记住偏好\s+(语言|语气|详略|主题|兴趣)\s*[::]\s*(.+)$"
|
||||
)
|
||||
_TOPIC_PATTERN = re.compile(r"^关注主题\s*[::]?\s*(.+)$")
|
||||
_DELETE_PATTERN = re.compile(
|
||||
r"^删除偏好\s+(PREF-[A-Za-z0-9-]+)$",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
_LIST_COMMANDS = {"我的偏好", "查看偏好", "我的兴趣", "查看兴趣"}
|
||||
_HELP_COMMANDS = {"帮助", "使用帮助", "命令帮助"}
|
||||
_COMMAND_PREFIXES = (
|
||||
"记住偏好",
|
||||
"关注主题",
|
||||
"我的偏好",
|
||||
"查看偏好",
|
||||
"我的兴趣",
|
||||
"查看兴趣",
|
||||
"删除偏好",
|
||||
"重置对话",
|
||||
*_HELP_COMMANDS,
|
||||
)
|
||||
|
||||
|
||||
def handle_personalization_command(
|
||||
db: Session,
|
||||
feishu: FeishuService,
|
||||
command_text: str,
|
||||
principal: FeishuPrincipal,
|
||||
auto_reply: bool,
|
||||
) -> dict[str, Any] | None:
|
||||
"""Handle owner-scoped preferences, interests, help, and conversation reset."""
|
||||
|
||||
text = command_text.strip()
|
||||
if not text.startswith(_COMMAND_PREFIXES):
|
||||
return None
|
||||
principal.require_active()
|
||||
if text in _HELP_COMMANDS:
|
||||
return _result(
|
||||
feishu,
|
||||
FeishuCommandName.HELP,
|
||||
_help_content(principal),
|
||||
principal,
|
||||
auto_reply,
|
||||
)
|
||||
if text == "重置对话":
|
||||
chat_type, chat_key = _conversation_key(principal)
|
||||
reset = ConversationService(db).reset(
|
||||
principal.owner_id,
|
||||
chat_type,
|
||||
chat_key,
|
||||
)
|
||||
_audit(
|
||||
db,
|
||||
principal,
|
||||
action="personalization.conversation.reset",
|
||||
target_type="ai-conversation",
|
||||
response={"reset": reset},
|
||||
)
|
||||
content = "当前会话已重置。" if reset else "当前会话没有可清除的历史。"
|
||||
return _result(
|
||||
feishu,
|
||||
FeishuCommandName.CONVERSATION_RESET,
|
||||
content,
|
||||
principal,
|
||||
auto_reply,
|
||||
)
|
||||
|
||||
service = PreferenceService(db)
|
||||
set_match = _SET_PATTERN.fullmatch(text)
|
||||
topic_match = _TOPIC_PATTERN.fullmatch(text)
|
||||
delete_match = _DELETE_PATTERN.fullmatch(text)
|
||||
try:
|
||||
if set_match:
|
||||
category = _CATEGORY_LABELS[set_match.group(1)]
|
||||
record = service.upsert(
|
||||
principal.owner_id,
|
||||
category,
|
||||
set_match.group(2),
|
||||
)
|
||||
_audit_preference(db, principal, "upsert", record)
|
||||
content = (
|
||||
"偏好已保存。\n"
|
||||
f"编号:{record['code']}\n"
|
||||
f"类别:{_category_name(record['category'])}\n"
|
||||
f"内容:{record['value']}"
|
||||
)
|
||||
command = FeishuCommandName.PREFERENCE_SET
|
||||
elif topic_match:
|
||||
record = service.upsert(
|
||||
principal.owner_id,
|
||||
PreferenceCategory.TOPIC,
|
||||
topic_match.group(1),
|
||||
)
|
||||
_audit_preference(db, principal, "upsert", record)
|
||||
content = (
|
||||
"关注主题已保存。\n"
|
||||
f"编号:{record['code']}\n"
|
||||
f"主题:{record['value']}"
|
||||
)
|
||||
command = FeishuCommandName.PREFERENCE_SET
|
||||
elif text in _LIST_COMMANDS:
|
||||
records = service.list_preferences(principal.owner_id)
|
||||
_audit(
|
||||
db,
|
||||
principal,
|
||||
action="personalization.preference.list",
|
||||
target_type="user-preference",
|
||||
response={"count": len(records)},
|
||||
)
|
||||
content = _preference_list(records)
|
||||
command = FeishuCommandName.PREFERENCE_LIST
|
||||
elif delete_match:
|
||||
code = delete_match.group(1)
|
||||
service.delete(principal.owner_id, code)
|
||||
_audit(
|
||||
db,
|
||||
principal,
|
||||
action="personalization.preference.delete",
|
||||
target_type="user-preference",
|
||||
target_id=code,
|
||||
response={"deleted": True},
|
||||
)
|
||||
content = "偏好已删除。"
|
||||
command = FeishuCommandName.PREFERENCE_DELETE
|
||||
else:
|
||||
content = PREFERENCE_HELP
|
||||
command = FeishuCommandName.PREFERENCE_SET
|
||||
except HTTPException as exc:
|
||||
db.rollback()
|
||||
if exc.status_code == 404:
|
||||
content = "没有找到该偏好,请先发送“我的偏好”确认编号。"
|
||||
elif "Sensitive preference" in str(exc.detail):
|
||||
content = "该内容可能涉及敏感个人信息或密钥,已拒绝保存。"
|
||||
else:
|
||||
content = f"偏好指令未执行,请检查格式。\n\n{PREFERENCE_HELP}"
|
||||
command = (
|
||||
FeishuCommandName.PREFERENCE_DELETE
|
||||
if delete_match
|
||||
else FeishuCommandName.PREFERENCE_SET
|
||||
)
|
||||
return _result(feishu, command, content, principal, auto_reply)
|
||||
|
||||
|
||||
def _preference_list(records: list[dict[str, Any]]) -> str:
|
||||
if not records:
|
||||
return "当前没有已保存的偏好或兴趣。\n\n" + PREFERENCE_HELP
|
||||
lines = ["我的偏好与兴趣:"]
|
||||
for record in records:
|
||||
lines.append(
|
||||
f"{record['code']}|{_category_name(record['category'])}"
|
||||
f"|{record['value']}"
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _help_content(principal: FeishuPrincipal) -> str:
|
||||
lines = [
|
||||
"你可以使用:",
|
||||
"问 <问题>",
|
||||
"学习规则:<个人规则>;查看/修改/启用/停用/删除规则",
|
||||
"记住偏好、关注主题、我的偏好、删除偏好",
|
||||
"加入自选 <股票代码>;查看自选",
|
||||
"订阅 <自然语言时间>:<提示词>;我的订阅、暂停、恢复、退订",
|
||||
"设置时区、设置/关闭安静时段",
|
||||
"重置对话、我的数据、忘记我",
|
||||
]
|
||||
if principal.is_admin:
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"管理员还可以使用:",
|
||||
"日报、周报、财务、风险和考勤查询",
|
||||
"学习/查看/修改/启停/删除公司规则",
|
||||
"在当前群创建群订阅",
|
||||
"通过真实 @用户 管理角色和状态",
|
||||
]
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _conversation_key(principal: FeishuPrincipal) -> tuple[str, str]:
|
||||
chat_type = principal.chat_type or "p2p"
|
||||
is_group = chat_type in {"group", "group_chat"}
|
||||
chat_key = principal.chat_id if is_group else (principal.chat_id or principal.open_id)
|
||||
if not chat_key:
|
||||
raise HTTPException(status_code=422, detail="Missing Feishu chat identity")
|
||||
return chat_type, chat_key
|
||||
|
||||
|
||||
def _audit_preference(
|
||||
db: Session,
|
||||
principal: FeishuPrincipal,
|
||||
action: str,
|
||||
record: dict[str, Any],
|
||||
) -> None:
|
||||
_audit(
|
||||
db,
|
||||
principal,
|
||||
action=f"personalization.preference.{action}",
|
||||
target_type="user-preference",
|
||||
target_id=str(record["code"]),
|
||||
response={"category": record["category"]},
|
||||
)
|
||||
|
||||
|
||||
def _audit(
|
||||
db: Session,
|
||||
principal: FeishuPrincipal,
|
||||
*,
|
||||
action: str,
|
||||
target_type: str,
|
||||
target_id: str | None = None,
|
||||
response: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
AuditService(db).record(
|
||||
AuditLogCreate(
|
||||
actor=principal.user_code,
|
||||
source=AuditSource.FEISHU,
|
||||
action=action,
|
||||
target_type=target_type,
|
||||
target_id=target_id,
|
||||
risk_level=AuditRiskLevel.LOW,
|
||||
response_payload=response,
|
||||
)
|
||||
)
|
||||
db.commit()
|
||||
|
||||
|
||||
def _category_name(value: str) -> str:
|
||||
try:
|
||||
return _CATEGORY_NAMES[PreferenceCategory(value)]
|
||||
except ValueError:
|
||||
return value
|
||||
|
||||
|
||||
def _result(
|
||||
feishu: FeishuService,
|
||||
command: FeishuCommandName,
|
||||
content: str,
|
||||
principal: FeishuPrincipal,
|
||||
auto_reply: bool,
|
||||
) -> dict[str, Any]:
|
||||
response = (
|
||||
send_text_if_configured(
|
||||
feishu,
|
||||
principal.chat_id,
|
||||
content,
|
||||
principal.user_code,
|
||||
)
|
||||
if auto_reply
|
||||
else None
|
||||
)
|
||||
return command_result(
|
||||
command,
|
||||
FeishuReplyType.TEXT,
|
||||
PERSONALIZATION_TITLE,
|
||||
content,
|
||||
response,
|
||||
)
|
||||
Reference in New Issue
Block a user