manager.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. from __future__ import annotations
  2. import os
  3. from collections.abc import Mapping
  4. from dataclasses import dataclass
  5. from typing import Any
  6. from gateway.core.channels.backends.memory_trace import MemoryTraceBackend
  7. from gateway.core.channels.feishu.connector import FeishuConnector, WebhookParseError
  8. from gateway.core.channels.feishu.http_run_executor import FeishuHttpRunApiExecutor
  9. from gateway.core.channels.feishu.identity import DefaultUserIdentityResolver
  10. from gateway.core.channels.feishu.router import FeishuMessageRouter
  11. from gateway.core.channels.manager import ChannelRegistry
  12. from gateway.core.channels.types import RouteResult
  13. @dataclass
  14. class FeishuChannelConfig:
  15. channel_id: str = "feishu"
  16. feishu_http_base_url: str = "http://127.0.0.1:4380"
  17. http_timeout: float = 120.0
  18. enabled: bool = True
  19. auto_create_trace: bool = True
  20. workspace_prefix: str = "feishu"
  21. default_agent_type: str = "personal_assistant"
  22. dispatch_reactions: bool = False
  23. dispatch_card_actions: bool = True # 卡片授权等交互后需续跑 Agent;可用 CHANNELS_DISPATCH_CARD_ACTIONS=false 关闭
  24. agent_api_base_url: str = "http://127.0.0.1:8000"
  25. agent_run_model: str = "qwen3.5-flash"
  26. agent_run_max_iterations: int = 200
  27. agent_run_temperature: float = 0.3
  28. feishu_run_notify_on_submit: bool = True
  29. # 以下为「Trace 跟单」参数(WebSocket watch;不再 HTTP 轮询 messages)
  30. poll_assistant_messages: bool = True # 是否把 assistant 推到飞书(False 时仍可连 WS 等终态清 Typing)
  31. poll_interval_seconds: float = 1.0 # WS recv 超时;无 WS 时 HTTP 查 trace 状态的间隔
  32. poll_request_timeout: float = 30.0 # 仅 HTTP 兜底 GET /api/traces/{id} 的超时
  33. poll_terminal_grace_rounds: int = 2 # 连续 N 轮终态后结束跟单
  34. poll_max_seconds: float = 0.0 # 跟单最长秒数,0=不限制
  35. assistant_max_text_chars: int = 8000
  36. typing_reaction_enabled: bool = True
  37. typing_reaction_emoji: str = "Typing"
  38. class FeishuChannelManager(ChannelRegistry):
  39. """飞书渠道:组装连接器、Trace 后端、HTTP Run API 执行器与消息路由。"""
  40. def __init__(self, config: FeishuChannelConfig | None = None) -> None:
  41. super().__init__()
  42. self._config = config or FeishuChannelConfig()
  43. self.register_channel(self._config.channel_id, self._config)
  44. self._connector = FeishuConnector(
  45. feishu_http_base_url=self._config.feishu_http_base_url,
  46. timeout=self._config.http_timeout,
  47. )
  48. self._trace_backend = MemoryTraceBackend()
  49. self._identity = DefaultUserIdentityResolver()
  50. self._executor = FeishuHttpRunApiExecutor(
  51. base_url=self._config.agent_api_base_url,
  52. timeout=self._config.http_timeout,
  53. identity_resolver=self._identity,
  54. model=self._config.agent_run_model,
  55. max_iterations=self._config.agent_run_max_iterations,
  56. temperature=self._config.agent_run_temperature,
  57. notify_on_submit=self._config.feishu_run_notify_on_submit,
  58. poll_assistant_messages=self._config.poll_assistant_messages,
  59. poll_interval_seconds=self._config.poll_interval_seconds,
  60. poll_request_timeout=self._config.poll_request_timeout,
  61. poll_terminal_grace_rounds=self._config.poll_terminal_grace_rounds,
  62. poll_max_seconds=self._config.poll_max_seconds,
  63. assistant_max_text_chars=self._config.assistant_max_text_chars,
  64. typing_reaction_enabled=self._config.typing_reaction_enabled,
  65. typing_reaction_emoji=self._config.typing_reaction_emoji,
  66. )
  67. self._router = FeishuMessageRouter(
  68. connector=self._connector,
  69. trace_backend=self._trace_backend,
  70. executor_backend=self._executor,
  71. identity_resolver=self._identity,
  72. workspace_prefix=self._config.workspace_prefix,
  73. default_agent_type=self._config.default_agent_type,
  74. auto_create_trace=self._config.auto_create_trace,
  75. dispatch_reactions=self._config.dispatch_reactions,
  76. dispatch_card_actions=self._config.dispatch_card_actions,
  77. )
  78. @property
  79. def config(self) -> FeishuChannelConfig:
  80. return self._config
  81. @property
  82. def feishu_connector(self) -> FeishuConnector:
  83. return self._connector
  84. @property
  85. def message_router(self) -> FeishuMessageRouter:
  86. return self._router
  87. @classmethod
  88. def from_env(cls) -> FeishuChannelManager:
  89. """从环境变量构造实例(与 docker-compose / .env 配合)。"""
  90. return cls(
  91. FeishuChannelConfig(
  92. feishu_http_base_url=os.getenv("FEISHU_HTTP_BASE_URL", "http://127.0.0.1:4380").strip(),
  93. http_timeout=float(os.getenv("FEISHU_HTTP_TIMEOUT", "120")),
  94. dispatch_reactions=os.getenv("CHANNELS_DISPATCH_REACTIONS", "false").lower() in ("1", "true", "yes"),
  95. dispatch_card_actions=os.getenv("CHANNELS_DISPATCH_CARD_ACTIONS", "true").lower()
  96. in ("1", "true", "yes"),
  97. agent_api_base_url=os.getenv("GATEWAY_AGENT_API_BASE_URL", "http://127.0.0.1:8000").strip(),
  98. agent_run_model=os.getenv("FEISHU_AGENT_RUN_MODEL", "qwen3.5-flash").strip(),
  99. agent_run_max_iterations=int(os.getenv("FEISHU_AGENT_RUN_MAX_ITERATIONS", "200")),
  100. agent_run_temperature=float(os.getenv("FEISHU_AGENT_RUN_TEMPERATURE", "0.3")),
  101. feishu_run_notify_on_submit=os.getenv("CHANNELS_FEISHU_RUN_NOTIFY", "true").lower()
  102. in ("1", "true", "yes"),
  103. poll_assistant_messages=os.getenv("FEISHU_AGENT_POLL_ASSISTANTS", "true").lower()
  104. in ("1", "true", "yes"),
  105. poll_interval_seconds=float(os.getenv("FEISHU_AGENT_POLL_INTERVAL", "1.0")),
  106. poll_request_timeout=float(os.getenv("FEISHU_AGENT_POLL_REQUEST_TIMEOUT", "30")),
  107. poll_terminal_grace_rounds=int(os.getenv("FEISHU_AGENT_POLL_GRACE_ROUNDS", "2")),
  108. poll_max_seconds=float(os.getenv("FEISHU_AGENT_POLL_MAX_SECONDS", "0")),
  109. assistant_max_text_chars=int(os.getenv("FEISHU_AGENT_ASSISTANT_MAX_CHARS", "8000")),
  110. typing_reaction_enabled=os.getenv("FEISHU_TYPING_REACTION", "true").lower()
  111. in ("1", "true", "yes"),
  112. typing_reaction_emoji=os.getenv("FEISHU_TYPING_REACTION_EMOJI", "Typing").strip()
  113. or "Typing",
  114. )
  115. )
  116. async def handle_feishu_inbound_webhook(self, body: Mapping[str, Any]) -> RouteResult:
  117. """处理飞书适配层 POST 到 ``/api/channels/feishu/inbound/webhook`` 的规范化事件。"""
  118. cid = self._config.channel_id
  119. if not self._running.get(cid, False):
  120. return RouteResult(ok=False, error="channel_stopped")
  121. try:
  122. event = FeishuConnector.parse_feishu_inbound_event(body)
  123. except WebhookParseError as e:
  124. return RouteResult(ok=False, error=str(e))
  125. return await self._router.route_feishu_inbound_event(event)