feat: 添加飞书用户模块和订阅功能支持

- 新增feishu_users模块用于处理飞书用户身份验证和权限管理
- 新增subscriptions模块用于处理订阅相关功能
- 新增personalization模块用于个性化服务
- 在alembic迁移配置中注册新的模型模块
- 在API路由器中添加feishu_users和subscriptions路由
- 实现事件调度服务的改进,包括错误处理和状态更新优化
- 添加飞书命令处理的权限检查机制
- 实现飞书应用票据事件处理
- 改进审计日志记录功能
```
This commit is contained in:
2026-07-27 08:02:17 +08:00
parent db751f03b4
commit d7db84571d
148 changed files with 17110 additions and 765 deletions

View File

@@ -11,32 +11,60 @@ 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"
"学习规则:<规则内容>\n"
"学习规则 80<规则内容>\n"
"学习市场规则 80<仅用于市场分析的规则内容>\n"
"查看规则\n"
"停用规则 <规则编号>\n"
"启用规则 <规则编号>"
"修改规则 <编号> 80<新内容>\n"
"启用规则 <编号>\n"
"停用规则 <编号>\n"
"删除规则 <编号>"
)
_COMPANY_REPLACEMENTS = (
("学习公司市场规则", "学习市场规则"),
("查看公司市场规则", "查看市场规则"),
("学习公司规则", "学习规则"),
("查看公司规则", "查看规则"),
("修改公司规则", "修改规则"),
("启用公司规则", "启用规则"),
("停用公司规则", "停用规则"),
("删除公司规则", "删除规则"),
)
@@ -47,29 +75,40 @@ def handle_rule_command(
chat_id: str | None,
actor: str,
auto_reply: bool,
principal: FeishuPrincipal | None = None,
) -> dict[str, Any] | None:
"""Handle persistent AI rule commands."""
"""Handle company or owner-scoped persistent AI rule commands."""
if not command_text.startswith(RULE_COMMAND_PREFIXES):
return None
command = _command_name(command_text)
if command in {
FeishuCommandName.RULE_CREATE,
FeishuCommandName.RULE_DISABLE,
FeishuCommandName.RULE_ENABLE,
} and get_settings().read_only_mode:
return _result(
feishu,
command,
"当前为只读模式,不能新增或修改学习规则。请由管理员启用操作后重试。",
chat_id,
actor,
auto_reply,
)
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, command_text, command, actor)
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:
@@ -83,21 +122,65 @@ def handle_rule_command(
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)
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)
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
@@ -108,16 +191,16 @@ def _execute(
tags=None,
enabled=enabled,
actor=actor,
owner_id=owner_id,
)
state = "已启用" if enabled else "已停用"
return (
FeishuCommandName.RULE_ENABLE if enabled else FeishuCommandName.RULE_DISABLE,
f"规则{state}\n"
f"编号:{rule['code']}\n"
f"优先级:{rule['importance']}\n"
f"范围:{rule['scope']} / {rule['subject']}\n"
f"状态:{state}",
_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
@@ -126,6 +209,8 @@ def _create_rule(
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()
@@ -136,29 +221,39 @@ def _create_rule(
rule = memory.create_rule(
content=content,
scope="market" if market_rule else "global",
subject="market" if market_rule else "company",
subject=(
"market"
if market_rule
else ("company" if company_rule else "personal")
),
priority=priority,
tags=["feishu", *(["market"] if market_rule else [])],
tags=[
"feishu",
"company" if company_rule else "personal",
*(["market"] if market_rule else []),
],
actor=actor,
owner_id=owner_id,
)
return (
"规则已学习。\n"
f"编号:{rule['code']}\n"
f"优先级:{rule['importance']}\n"
f"范围:{rule['scope']} / {rule['subject']}\n"
"状态:已启用"
)
return _rule_state(rule, "已学习")
def _list_rules(memory: AIMemoryService, command_text: str) -> str:
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:
return "当前没有已启用的学习规则。"
lines = ["当前已启用的学习规则"]
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:
@@ -170,11 +265,25 @@ def _list_rules(memory: AIMemoryService, command_text: str) -> str:
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