feat: 添加飞书用户模块和订阅功能支持 - 新增feishu_users模块用于处理飞书用户身份验证和权限管理 - 新增subscriptions模块用于处理订阅相关功能 - 新增personalization模块用于个性化服务 - 在alembic迁移配置中注册新的模型模块 - 在API路由器中添加feishu_users和subscriptions路由 - 实现事件调度服务的改进,包括错误处理和状态更新优化 - 添加飞书命令处理的权限检查机制 - 实现飞书应用票据事件处理 - 改进审计日志记录功能 ```
302 lines
10 KiB
Python
302 lines
10 KiB
Python
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)
|