manager.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. from __future__ import annotations
  2. from collections.abc import Mapping
  3. from dataclasses import dataclass
  4. from typing import Any
  5. from gateway.core.channels.feishu.bridge import FeishuHttpRunApiExecutor
  6. from gateway.core.channels.feishu.connector import FeishuConnector, WebhookParseError
  7. from gateway.core.channels.feishu.identity import DefaultUserIdentityResolver
  8. from gateway.core.channels.feishu.router import FeishuMessageRouter
  9. from gateway.core.channels.manager import ChannelRegistry
  10. from gateway.core.channels.types import RouteResult
  11. from gateway.core.lifecycle import LifecycleTraceBackend, TraceManager, WorkspaceManager
  12. from utils.env_parse import env_bool, env_float, env_int, env_str
  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. # Trace 跟单结束后的生命周期(Workspace 沙箱 / 渠道绑定)
  39. # 默认不在单次 Trace 终态时停沙箱:同一用户共用一个 workspace/容器,多轮对话与多 trace 复用。
  40. stop_container_on_trace_terminal: bool = False
  41. stop_container_on_trace_not_found: bool = True
  42. release_ref_on_trace_terminal: bool = False
  43. # False:绑定新 trace 时仍保留旧 trace 在 workspace 的引用,便于 Executor 等按 trace_id 解析同一用户目录
  44. release_previous_trace_ref_on_bind: bool = False
  45. class FeishuChannelManager(ChannelRegistry):
  46. """飞书渠道:组装连接器、Trace 后端、HTTP Run API 执行器与消息路由。"""
  47. def __init__(self, config: FeishuChannelConfig | None = None) -> None:
  48. super().__init__()
  49. self._config = config or FeishuChannelConfig()
  50. self.register_channel(self._config.channel_id, self._config)
  51. self._connector = FeishuConnector(
  52. feishu_http_base_url=self._config.feishu_http_base_url,
  53. timeout=self._config.http_timeout,
  54. )
  55. self._workspace_manager = WorkspaceManager.from_env()
  56. self._trace_manager = TraceManager.from_env(self._workspace_manager)
  57. self._trace_backend = LifecycleTraceBackend(
  58. self._trace_manager,
  59. release_previous_trace_ref_on_bind=self._config.release_previous_trace_ref_on_bind,
  60. )
  61. self._identity = DefaultUserIdentityResolver()
  62. self._executor = FeishuHttpRunApiExecutor(
  63. base_url=self._config.agent_api_base_url,
  64. timeout=self._config.http_timeout,
  65. identity_resolver=self._identity,
  66. model=self._config.agent_run_model,
  67. max_iterations=self._config.agent_run_max_iterations,
  68. temperature=self._config.agent_run_temperature,
  69. notify_on_submit=self._config.feishu_run_notify_on_submit,
  70. poll_assistant_messages=self._config.poll_assistant_messages,
  71. poll_interval_seconds=self._config.poll_interval_seconds,
  72. poll_request_timeout=self._config.poll_request_timeout,
  73. poll_terminal_grace_rounds=self._config.poll_terminal_grace_rounds,
  74. poll_max_seconds=self._config.poll_max_seconds,
  75. assistant_max_text_chars=self._config.assistant_max_text_chars,
  76. typing_reaction_enabled=self._config.typing_reaction_enabled,
  77. typing_reaction_emoji=self._config.typing_reaction_emoji,
  78. workspace_manager=self._workspace_manager,
  79. workspace_prefix=self._config.workspace_prefix,
  80. channel_id=self._config.channel_id,
  81. lifecycle_trace_backend=self._trace_backend,
  82. stop_container_on_trace_terminal=self._config.stop_container_on_trace_terminal,
  83. stop_container_on_trace_not_found=self._config.stop_container_on_trace_not_found,
  84. release_ref_on_trace_terminal=self._config.release_ref_on_trace_terminal,
  85. )
  86. self._router = FeishuMessageRouter(
  87. connector=self._connector,
  88. trace_backend=self._trace_backend,
  89. executor_backend=self._executor,
  90. identity_resolver=self._identity,
  91. workspace_prefix=self._config.workspace_prefix,
  92. default_agent_type=self._config.default_agent_type,
  93. auto_create_trace=self._config.auto_create_trace,
  94. dispatch_reactions=self._config.dispatch_reactions,
  95. dispatch_card_actions=self._config.dispatch_card_actions,
  96. )
  97. @property
  98. def config(self) -> FeishuChannelConfig:
  99. return self._config
  100. @property
  101. def feishu_connector(self) -> FeishuConnector:
  102. return self._connector
  103. @property
  104. def message_router(self) -> FeishuMessageRouter:
  105. return self._router
  106. @classmethod
  107. def from_env(cls) -> FeishuChannelManager:
  108. """从环境变量构造实例(与 docker-compose / .env 配合)。"""
  109. return cls(
  110. FeishuChannelConfig(
  111. feishu_http_base_url=env_str("FEISHU_HTTP_BASE_URL", "http://127.0.0.1:4380"),
  112. http_timeout=env_float("FEISHU_HTTP_TIMEOUT", 120.0),
  113. dispatch_reactions=env_bool("CHANNELS_DISPATCH_REACTIONS", False),
  114. dispatch_card_actions=env_bool("CHANNELS_DISPATCH_CARD_ACTIONS", True),
  115. agent_api_base_url=env_str("GATEWAY_AGENT_API_BASE_URL", "http://127.0.0.1:8000"),
  116. agent_run_model=env_str("FEISHU_AGENT_RUN_MODEL", "qwen3.5-flash"),
  117. agent_run_max_iterations=env_int("FEISHU_AGENT_RUN_MAX_ITERATIONS", 200),
  118. agent_run_temperature=env_float("FEISHU_AGENT_RUN_TEMPERATURE", 0.3),
  119. feishu_run_notify_on_submit=env_bool("CHANNELS_FEISHU_RUN_NOTIFY", True),
  120. poll_assistant_messages=env_bool("FEISHU_AGENT_POLL_ASSISTANTS", True),
  121. poll_interval_seconds=env_float("FEISHU_AGENT_POLL_INTERVAL", 1.0),
  122. poll_request_timeout=env_float("FEISHU_AGENT_POLL_REQUEST_TIMEOUT", 30.0),
  123. poll_terminal_grace_rounds=env_int("FEISHU_AGENT_POLL_GRACE_ROUNDS", 2),
  124. poll_max_seconds=env_float("FEISHU_AGENT_POLL_MAX_SECONDS", 0.0),
  125. assistant_max_text_chars=env_int("FEISHU_AGENT_ASSISTANT_MAX_CHARS", 8000),
  126. typing_reaction_enabled=env_bool("FEISHU_TYPING_REACTION", True),
  127. typing_reaction_emoji=env_str("FEISHU_TYPING_REACTION_EMOJI", "Typing") or "Typing",
  128. stop_container_on_trace_terminal=env_bool("GATEWAY_WORKSPACE_STOP_ON_TRACE_TERMINAL", False),
  129. stop_container_on_trace_not_found=env_bool("GATEWAY_WORKSPACE_STOP_ON_TRACE_NOT_FOUND", True),
  130. release_ref_on_trace_terminal=env_bool("GATEWAY_LIFECYCLE_RELEASE_REF_ON_TRACE_TERMINAL", False),
  131. release_previous_trace_ref_on_bind=env_bool("GATEWAY_LIFECYCLE_RELEASE_PREV_TRACE_REF_ON_BIND", False),
  132. )
  133. )
  134. async def handle_feishu_inbound_webhook(self, body: Mapping[str, Any]) -> RouteResult:
  135. """处理飞书适配层 POST 到 ``/api/channels/feishu/inbound/webhook`` 的规范化事件。"""
  136. cid = self._config.channel_id
  137. if not self._running.get(cid, False):
  138. return RouteResult(ok=False, error="channel_stopped")
  139. try:
  140. event = FeishuConnector.parse_feishu_inbound_event(body)
  141. except WebhookParseError as e:
  142. return RouteResult(ok=False, error=str(e))
  143. return await self._router.route_feishu_inbound_event(event)