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