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, )