ForcePilot/backend/package/yuxi/channels/adapters/feishu/dynamic_agent.py
Kris a6fa7245e5 feat(feishu): 完整实现飞书适配器核心模块
新增飞书机器人适配器全套功能,包括:
- 基础适配器入口与工具导出
- 消息格式化、卡片渲染、回复调度逻辑
- 会话ID生成、模型覆盖策略
- 消息发送缓存、顺序队列管理
- 飞书签名验证、加解密webhook请求
- 审批权限校验、机器人菜单事件处理
- 文档评论、钉消息、语音转码处理
- 静态/动态目录管理、子代理生命周期管理
- 各类工具集:聊天、云盘、文档、知识库API封装
2026-05-12 00:43:59 +08:00

172 lines
6.2 KiB
Python

from __future__ import annotations
import json
import os
import time
from dataclasses import dataclass
from typing import Any
from yuxi.utils.logging_config import logger
@dataclass
class FeishuDynamicAgentConfig:
enabled: bool = False
max_agent_count: int = 50
ttl_s: float = 3600.0
template_agent_id: int = 0
auto_create: bool = False
workspace_template: str = ""
agent_dir_template: str = ""
config_file_template: str = ""
@classmethod
def from_config(cls, config: dict[str, Any]) -> FeishuDynamicAgentConfig:
dac = config.get("dynamicAgentCreation", {}) or {}
return cls(
enabled=dac.get("enabled", False),
max_agent_count=int(dac.get("maxCount", 50)),
ttl_s=float(dac.get("ttl", 3600)),
template_agent_id=int(dac.get("templateAgentId", 0)),
auto_create=dac.get("autoCreate", False),
workspace_template=dac.get("workspaceTemplate", ""),
agent_dir_template=dac.get("agentDirTemplate", ""),
config_file_template=dac.get("configFileTemplate", ""),
)
class FeishuDynamicAgentManager:
def __init__(self, config: FeishuDynamicAgentConfig | None = None):
self.config = config or FeishuDynamicAgentConfig()
self._dm_agents: dict[str, str] = {}
self._agent_metadata: dict[str, dict[str, Any]] = {}
self._agent_created_at: dict[str, float] = {}
self._agent_last_access: dict[str, float] = {}
@property
def active_count(self) -> int:
return len(self._dm_agents)
def get_or_resolve_agent(self, open_id: str) -> str | None:
if not self.config.enabled:
return None
if open_id in self._dm_agents:
self._agent_last_access[open_id] = time.monotonic()
return self._dm_agents[open_id]
if self.config.auto_create:
agent_id = self._create_dm_agent(open_id)
if agent_id:
return agent_id
return None
def _create_dm_agent(self, open_id: str) -> str | None:
if len(self._dm_agents) >= self.config.max_agent_count:
logger.warning("[DynamicAgent] Max agent count reached (%d)", self.config.max_agent_count)
self._evict_expired()
if len(self._dm_agents) >= self.config.max_agent_count:
return None
agent_id = f"feishu-{open_id}"
now = time.monotonic()
self._dm_agents[open_id] = agent_id
self._agent_created_at[open_id] = now
self._agent_last_access[open_id] = now
self._agent_metadata[open_id] = {
"open_id": open_id,
"agent_id": agent_id,
"created_at": now,
}
logger.info("[DynamicAgent] Created DM agent for %s: %s", open_id, agent_id)
if self.config.workspace_template:
self._ensure_agent_workspace(open_id, agent_id)
if self.config.config_file_template:
self._write_agent_config(open_id, agent_id)
return agent_id
def _ensure_agent_workspace(self, open_id: str, agent_id: str) -> None:
ws_template = self.config.workspace_template
dir_template = self.config.agent_dir_template or "{agent_id}"
agent_dir = dir_template.replace("{agent_id}", agent_id).replace("{open_id}", open_id)
workspace_path = os.path.join(ws_template, agent_dir)
try:
os.makedirs(workspace_path, exist_ok=True)
logger.info("[DynamicAgent] Created agent workspace: %s", workspace_path)
except OSError as e:
logger.warning("[DynamicAgent] Failed to create workspace %s: %s", workspace_path, e)
def _write_agent_config(self, open_id: str, agent_id: str) -> None:
config_template = self.config.config_file_template
if not config_template:
return
config_path = config_template.replace("{agent_id}", agent_id).replace("{open_id}", open_id)
config_dir = os.path.dirname(config_path)
if config_dir:
try:
os.makedirs(config_dir, exist_ok=True)
except OSError:
pass
config_data = {
"agent_id": agent_id,
"open_id": open_id,
"dynamic": True,
"created_at": self._agent_created_at.get(open_id, 0),
}
try:
with open(config_path, "w", encoding="utf-8") as f:
json.dump(config_data, f, ensure_ascii=False, indent=2)
logger.info("[DynamicAgent] Wrote agent config: %s", config_path)
except OSError as e:
logger.warning("[DynamicAgent] Failed to write config %s: %s", config_path, e)
def _evict_expired(self) -> int:
if not self.config.ttl_s:
return 0
now = time.monotonic()
expired = [oid for oid, created in self._agent_created_at.items() if (now - created) > self.config.ttl_s]
for oid in expired:
self.remove_agent(oid)
if expired:
logger.info("[DynamicAgent] Evicted %d expired agents", len(expired))
return len(expired)
def remove_agent(self, open_id: str) -> None:
self._dm_agents.pop(open_id, None)
self._agent_created_at.pop(open_id, None)
self._agent_last_access.pop(open_id, None)
self._agent_metadata.pop(open_id, None)
def list_agents(self) -> list[dict[str, Any]]:
result: list[dict[str, Any]] = []
now = time.monotonic()
for open_id, agent_id in self._dm_agents.items():
metadata = self._agent_metadata.get(open_id, {})
result.append(
{
"agent_id": agent_id,
"open_id": open_id,
"created_at": self._agent_created_at.get(open_id, 0),
"age_s": round(now - self._agent_created_at.get(open_id, now), 1),
"last_access_s": round(now - self._agent_last_access.get(open_id, now), 1),
"metadata": metadata,
}
)
return result
def agent_count(self) -> int:
return len(self._dm_agents)
def clear(self) -> None:
self._dm_agents.clear()
self._agent_created_at.clear()
self._agent_last_access.clear()
self._agent_metadata.clear()