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.core.config import get_settings from app.modules.ai_memory.constants import AIMemoryStatus from app.modules.ai_memory.service import AIMemoryService from app.modules.feishu.constants import FeishuCommandName, FeishuReplyType from app.modules.feishu.service import FeishuService from app.modules.feishu_users.constants import FeishuCapability from app.modules.feishu_users.principal import FeishuPrincipal RULE_TITLE = "AI 学习规则" RULE_CREATE_PATTERN = re.compile(r"^学习规则(?:\s+(\d{1,3}))?\s*[::]\s*(.*)$") MARKET_RULE_CREATE_PATTERN = re.compile( r"^学习市场规则(?:\s+(\d{1,3}))?\s*[::]\s*(.*)$" ) RULE_UPDATE_PATTERN = re.compile( r"^修改规则\s+(MEM-[A-Za-z0-9-]+)(?:\s+(\d{1,3}))?\s*[::]\s*(.*)$", re.IGNORECASE, ) RULE_DISABLE_PATTERN = re.compile(r"^停用规则\s+(MEM-[A-Za-z0-9-]+)$", re.IGNORECASE) RULE_ENABLE_PATTERN = re.compile(r"^启用规则\s+(MEM-[A-Za-z0-9-]+)$", re.IGNORECASE) RULE_DELETE_PATTERN = re.compile(r"^删除规则\s+(MEM-[A-Za-z0-9-]+)$", re.IGNORECASE) RULE_LIST_COMMANDS = {"查看规则", "规则列表", "查看市场规则"} RULE_COMMAND_PREFIXES = ( "学习公司市场规则", "学习公司规则", "查看公司市场规则", "查看公司规则", "修改公司规则", "启用公司规则", "停用公司规则", "删除公司规则", "学习市场规则", "学习规则", "查看市场规则", "查看规则", "规则列表", "修改规则", "停用规则", "启用规则", "删除规则", ) RULE_COMMAND_HELP = ( "规则指令格式:\n" "学习规则 80:<规则内容>\n" "查看规则\n" "修改规则 <编号> 80:<新内容>\n" "启用规则 <编号>\n" "停用规则 <编号>\n" "删除规则 <编号>" ) _COMPANY_REPLACEMENTS = ( ("学习公司市场规则", "学习市场规则"), ("查看公司市场规则", "查看市场规则"), ("学习公司规则", "学习规则"), ("查看公司规则", "查看规则"), ("修改公司规则", "修改规则"), ("启用公司规则", "启用规则"), ("停用公司规则", "停用规则"), ("删除公司规则", "删除规则"), ) def handle_rule_command( db: Session, feishu: FeishuService, command_text: str, chat_id: str | None, actor: str, auto_reply: bool, principal: FeishuPrincipal | None = None, ) -> dict[str, Any] | None: """Handle company or owner-scoped persistent AI rule commands.""" if not command_text.startswith(RULE_COMMAND_PREFIXES): return None normalized, company_rule = _normalize_company_command(command_text) owner_id: int | None = None if get_settings().feishu_user_features_enabled: if principal is None: return _result( feishu, FeishuCommandName.PERMISSION_DENIED, "个人规则只能由已验证的飞书账号管理。", chat_id, actor, auto_reply, ) if company_rule: principal.require_capability(FeishuCapability.COMPANY_RULES) else: owner_id = principal.owner_id command = _command_name(normalized) content = RULE_COMMAND_HELP try: command, content = _execute( db, normalized, command, actor, owner_id=owner_id, company_rule=company_rule or owner_id is None, ) except HTTPException as exc: detail = str(exc.detail) if "secret-like" in detail: content = "规则疑似包含密码、令牌或其他密钥信息,已拒绝学习。" elif exc.status_code == 404: content = "没有找到该规则,请先发送“查看规则”确认规则编号。" elif "priority" in detail: content = "规则优先级必须在 1 到 100 之间。" else: content = "规则未保存,请检查指令内容后重试。" return _result(feishu, command, content, chat_id, actor, auto_reply) def is_company_rule_command(command_text: str) -> bool: return any(command_text.startswith(prefix) for prefix, _ in _COMPANY_REPLACEMENTS) def _normalize_company_command(command_text: str) -> tuple[str, bool]: for prefix, replacement in _COMPANY_REPLACEMENTS: if command_text.startswith(prefix): return replacement + command_text[len(prefix) :], True return command_text, False def _execute( db: Session, command_text: str, command: FeishuCommandName, actor: str, *, owner_id: int | None, company_rule: bool, ) -> tuple[FeishuCommandName, str]: market_create_match = MARKET_RULE_CREATE_PATTERN.fullmatch(command_text) create_match = market_create_match or RULE_CREATE_PATTERN.fullmatch(command_text) update_match = RULE_UPDATE_PATTERN.fullmatch(command_text) disable_match = RULE_DISABLE_PATTERN.fullmatch(command_text) enable_match = RULE_ENABLE_PATTERN.fullmatch(command_text) delete_match = RULE_DELETE_PATTERN.fullmatch(command_text) memory = AIMemoryService(db) if create_match: return ( command, _create_rule( memory, create_match, market_create_match is not None, actor, owner_id, company_rule, ), ) if command_text in RULE_LIST_COMMANDS: return ( FeishuCommandName.RULE_LIST, _list_rules(memory, command_text, owner_id, company_rule), ) if update_match: priority = int(update_match.group(2) or 50) content = update_match.group(3).strip() if not content: return FeishuCommandName.RULE_UPDATE, "规则内容不能为空。" rule = memory.update_rule( code=update_match.group(1), content=content, priority=priority, tags=None, enabled=None, actor=actor, owner_id=owner_id, ) return FeishuCommandName.RULE_UPDATE, _rule_state(rule, "已修改") if disable_match or enable_match: enabled = enable_match is not None match = enable_match or disable_match rule = memory.update_rule( code=match.group(1), content=None, priority=None, tags=None, enabled=enabled, actor=actor, owner_id=owner_id, ) state = "已启用" if enabled else "已停用" return ( FeishuCommandName.RULE_ENABLE if enabled else FeishuCommandName.RULE_DISABLE, _rule_state(rule, state), ) if delete_match: memory.delete_rule(delete_match.group(1), actor=actor, owner_id=owner_id) return FeishuCommandName.RULE_DELETE, "规则已删除。" return command, RULE_COMMAND_HELP def _create_rule( memory: AIMemoryService, match: re.Match[str], market_rule: bool, actor: str, owner_id: int | None, company_rule: bool, ) -> str: priority = int(match.group(1) or 50) content = match.group(2).strip() if not content: return f"规则内容不能为空。\n\n{RULE_COMMAND_HELP}" if not 1 <= priority <= 100: return "规则优先级必须在 1 到 100 之间。" rule = memory.create_rule( content=content, scope="market" if market_rule else "global", subject=( "market" if market_rule else ("company" if company_rule else "personal") ), priority=priority, tags=[ "feishu", "company" if company_rule else "personal", *(["market"] if market_rule else []), ], actor=actor, owner_id=owner_id, ) return _rule_state(rule, "已学习") def _list_rules( memory: AIMemoryService, command_text: str, owner_id: int | None, company_rule: bool, ) -> str: rules = memory.list_rules( scope="market" if command_text == "查看市场规则" else None, status_filter=AIMemoryStatus.ACTIVE, limit=20, owner_id=owner_id, ) if not rules: target = "公司" if company_rule else "个人" return f"当前没有已启用的{target}规则。" lines = ["当前已启用的公司规则:" if company_rule else "当前已启用的个人规则:"] for rule in rules: rule_text = str(rule["content"]) if len(rule_text) > 80: rule_text = f"{rule_text[:80]}…" lines.append( f"{rule['code']}|优先级 {rule['importance']}|" f"{rule['scope']}/{rule['subject']}\n{rule_text}" ) return "\n\n".join(lines) def _rule_state(rule: dict[str, Any], state: str) -> str: return ( f"规则{state}。\n" f"编号:{rule['code']}\n" f"优先级:{rule['importance']}\n" f"范围:{rule['scope']} / {rule['subject']}\n" f"状态:{state}" ) def _command_name(command_text: str) -> FeishuCommandName: if command_text.startswith("停用规则"): return FeishuCommandName.RULE_DISABLE if command_text.startswith("启用规则"): return FeishuCommandName.RULE_ENABLE if command_text.startswith("修改规则"): return FeishuCommandName.RULE_UPDATE if command_text.startswith("删除规则"): return FeishuCommandName.RULE_DELETE if command_text.startswith(("查看市场规则", "查看规则", "规则列表")): return FeishuCommandName.RULE_LIST return FeishuCommandName.RULE_CREATE def _result( feishu: FeishuService, command: FeishuCommandName, content: str, chat_id: str | None, actor: str, auto_reply: bool, ) -> dict[str, Any]: response = send_text_if_configured(feishu, chat_id, content, actor) if auto_reply else None return command_result(command, FeishuReplyType.TEXT, RULE_TITLE, content, response)