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

@@ -1,12 +1,13 @@
import json
from typing import Any
from fastapi import HTTPException, status
from app.modules.ai_agent.constants import (
CHAT_USER_CONTENT_TEMPLATE,
COMPANY_MANAGEMENT_SYSTEM_INSTRUCTIONS,
UNEXPECTED_HERMES_RESPONSE,
AIChatRole,
AIContextKey,
AIErrorKey,
AIHttpPayloadKey,
AIResponseKey,
@@ -41,6 +42,7 @@ def _chat_completion_payload(response: Any, error_key: AIErrorKey) -> dict[str,
def _chat_messages(prompt: str, context: dict[str, Any] | None = None) -> list[dict[str, str]]:
request_context = context or {}
return [
{
AIHttpPayloadKey.ROLE: AIChatRole.SYSTEM,
@@ -48,14 +50,57 @@ def _chat_messages(prompt: str, context: dict[str, Any] | None = None) -> list[d
},
{
AIHttpPayloadKey.ROLE: AIChatRole.USER,
AIHttpPayloadKey.CONTENT: CHAT_USER_CONTENT_TEMPLATE.format(
context=context or {},
task=prompt,
),
AIHttpPayloadKey.CONTENT: _ordered_context(prompt, request_context),
},
]
def _ordered_context(prompt: str, context: dict[str, Any]) -> str:
"""Serialize trusted personalization layers in their required precedence."""
company_rules = context.get(AIContextKey.COMPANY_RULES)
if company_rules is None:
company_rules = context.get(AIContextKey.USER_RULES) or []
controlled_keys = {
AIContextKey.USER_RULES,
AIContextKey.COMPANY_RULES,
AIContextKey.PERSONAL_RULES,
AIContextKey.PREFERENCES,
AIContextKey.INTERESTS,
AIContextKey.LOCAL_MEMORY,
AIContextKey.CONVERSATION_HISTORY,
AIContextKey.PROVIDER_SESSION_ID,
AIContextKey.ALLOW_PROVIDER_MEMORY,
}
current_context = {
str(key): value for key, value in context.items() if key not in controlled_keys
}
sections = [
("公司规则", company_rules),
("个人规则", context.get(AIContextKey.PERSONAL_RULES) or []),
(
"当前请求",
{
"prompt": prompt,
"context": current_context,
},
),
(
"个人偏好与兴趣",
{
"preferences": context.get(AIContextKey.PREFERENCES) or [],
"interests": context.get(AIContextKey.INTERESTS) or [],
},
),
("个人相关记忆", context.get(AIContextKey.LOCAL_MEMORY) or []),
("当前会话历史", context.get(AIContextKey.CONVERSATION_HISTORY) or []),
]
return "\n\n".join(
f"{title}:\n{json.dumps(value, ensure_ascii=False, default=str)}"
for title, value in sections
)
def _service_root(base_url: str, suffix: str) -> str:
root = base_url.rstrip("/")
normalized_suffix = suffix.rstrip("/")

View File

@@ -15,6 +15,7 @@ from app.modules.ai_agent.constants import (
AUTHORIZATION_BEARER_TEMPLATE,
UNEXPECTED_HERMES_RESPONSE,
AIErrorKey,
AIContextKey,
AIHttpHeader,
AIHttpPath,
AIHttpPayloadKey,
@@ -38,11 +39,16 @@ class HermesAdapter(AIAdapter):
headers[AIHttpHeader.AUTHORIZATION] = AUTHORIZATION_BEARER_TEMPLATE.format(
token=self.settings.hermes_api_key
)
if self.settings.hermes_session_id:
headers[AIHttpHeader.HERMES_SESSION_ID] = self.settings.hermes_session_id
request_context = context or {}
session_id = (
request_context.get(AIContextKey.PROVIDER_SESSION_ID)
or self.settings.hermes_session_id
)
if session_id:
headers[AIHttpHeader.HERMES_SESSION_ID] = str(session_id)
payload = {
AIHttpPayloadKey.MODEL: self.settings.hermes_model,
AIHttpPayloadKey.MESSAGES: _chat_messages(prompt, context),
AIHttpPayloadKey.MESSAGES: _chat_messages(prompt, request_context),
AIHttpPayloadKey.STREAM: False,
}
with httpx.Client(timeout=300, trust_env=False) as client:

View File

@@ -32,7 +32,14 @@ class OpenClawHermesAdapter(AIAdapter):
def ask(self, prompt: str, context: dict[str, Any] | None = None) -> dict[str, Any]:
base_context = context or {}
recall = self._recall_memory(prompt, base_context)
allow_provider_memory = bool(
base_context.get(AIContextKey.ALLOW_PROVIDER_MEMORY, True)
)
recall = (
self._recall_memory(prompt, base_context)
if allow_provider_memory
else {AIResponseKey.ANSWER: "", AIResponseKey.RAW: {}}
)
openclaw = self._openclaw_context(base_context)
hermes_context = {
**base_context,
@@ -41,10 +48,14 @@ class OpenClawHermesAdapter(AIAdapter):
AIContextKey.OPENCLAW: openclaw,
}
hermes_result = self.hermes.ask(prompt, hermes_context)
remember = self._remember_interaction(
prompt,
base_context,
hermes_result[AIResponseKey.ANSWER],
remember = (
self._remember_interaction(
prompt,
base_context,
hermes_result[AIResponseKey.ANSWER],
)
if allow_provider_memory
else {AIResponseKey.OK: False, AIResponseKey.RAW: {}}
)
return {
AIResponseKey.ANSWER: hermes_result[AIResponseKey.ANSWER],