| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158 |
- from __future__ import annotations
- from collections.abc import Mapping
- from dataclasses import dataclass
- from typing import Any
- from gateway.core.channels.feishu.bridge import FeishuHttpRunApiExecutor
- from gateway.core.channels.feishu.connector import FeishuConnector, WebhookParseError
- from gateway.core.channels.feishu.identity import DefaultUserIdentityResolver
- from gateway.core.channels.feishu.router import FeishuMessageRouter
- from gateway.core.channels.manager import ChannelRegistry
- from gateway.core.channels.types import RouteResult
- from gateway.core.lifecycle import LifecycleTraceBackend, TraceManager, WorkspaceManager
- from utils.env_parse import env_bool, env_float, env_int, env_str
- @dataclass
- class FeishuChannelConfig:
- channel_id: str = "feishu"
- feishu_http_base_url: str = "http://127.0.0.1:4380"
- http_timeout: float = 120.0
- enabled: bool = True
- auto_create_trace: bool = True
- workspace_prefix: str = "feishu"
- default_agent_type: str = "personal_assistant"
- dispatch_reactions: bool = False
- dispatch_card_actions: bool = True # 卡片授权等交互后需续跑 Agent;可用 CHANNELS_DISPATCH_CARD_ACTIONS=false 关闭
- agent_api_base_url: str = "http://127.0.0.1:8000"
- agent_run_model: str = "qwen3.5-flash"
- agent_run_max_iterations: int = 200
- agent_run_temperature: float = 0.3
- feishu_run_notify_on_submit: bool = True
- # 以下为「Trace 跟单」参数(WebSocket watch;不再 HTTP 轮询 messages)
- poll_assistant_messages: bool = True # 是否把 assistant 推到飞书(False 时仍可连 WS 等终态清 Typing)
- poll_interval_seconds: float = 1.0 # WS recv 超时;无 WS 时 HTTP 查 trace 状态的间隔
- poll_request_timeout: float = 30.0 # 仅 HTTP 兜底 GET /api/traces/{id} 的超时
- poll_terminal_grace_rounds: int = 2 # 连续 N 轮终态后结束跟单
- poll_max_seconds: float = 0.0 # 跟单最长秒数,0=不限制
- assistant_max_text_chars: int = 8000
- typing_reaction_enabled: bool = True
- typing_reaction_emoji: str = "Typing"
- # Trace 跟单结束后的生命周期(Workspace 沙箱 / 渠道绑定)
- # 默认不在单次 Trace 终态时停沙箱:同一用户共用一个 workspace/容器,多轮对话与多 trace 复用。
- stop_container_on_trace_terminal: bool = False
- stop_container_on_trace_not_found: bool = True
- release_ref_on_trace_terminal: bool = False
- # False:绑定新 trace 时仍保留旧 trace 在 workspace 的引用,便于 Executor 等按 trace_id 解析同一用户目录
- release_previous_trace_ref_on_bind: bool = False
- class FeishuChannelManager(ChannelRegistry):
- """飞书渠道:组装连接器、Trace 后端、HTTP Run API 执行器与消息路由。"""
- def __init__(self, config: FeishuChannelConfig | None = None) -> None:
- super().__init__()
- self._config = config or FeishuChannelConfig()
- self.register_channel(self._config.channel_id, self._config)
- self._connector = FeishuConnector(
- feishu_http_base_url=self._config.feishu_http_base_url,
- timeout=self._config.http_timeout,
- )
- self._workspace_manager = WorkspaceManager.from_env()
- self._trace_manager = TraceManager.from_env(self._workspace_manager)
- self._trace_backend = LifecycleTraceBackend(
- self._trace_manager,
- release_previous_trace_ref_on_bind=self._config.release_previous_trace_ref_on_bind,
- )
- self._identity = DefaultUserIdentityResolver()
- self._executor = FeishuHttpRunApiExecutor(
- base_url=self._config.agent_api_base_url,
- timeout=self._config.http_timeout,
- identity_resolver=self._identity,
- model=self._config.agent_run_model,
- max_iterations=self._config.agent_run_max_iterations,
- temperature=self._config.agent_run_temperature,
- notify_on_submit=self._config.feishu_run_notify_on_submit,
- poll_assistant_messages=self._config.poll_assistant_messages,
- poll_interval_seconds=self._config.poll_interval_seconds,
- poll_request_timeout=self._config.poll_request_timeout,
- poll_terminal_grace_rounds=self._config.poll_terminal_grace_rounds,
- poll_max_seconds=self._config.poll_max_seconds,
- assistant_max_text_chars=self._config.assistant_max_text_chars,
- typing_reaction_enabled=self._config.typing_reaction_enabled,
- typing_reaction_emoji=self._config.typing_reaction_emoji,
- workspace_manager=self._workspace_manager,
- workspace_prefix=self._config.workspace_prefix,
- channel_id=self._config.channel_id,
- lifecycle_trace_backend=self._trace_backend,
- stop_container_on_trace_terminal=self._config.stop_container_on_trace_terminal,
- stop_container_on_trace_not_found=self._config.stop_container_on_trace_not_found,
- release_ref_on_trace_terminal=self._config.release_ref_on_trace_terminal,
- )
- self._router = FeishuMessageRouter(
- connector=self._connector,
- trace_backend=self._trace_backend,
- executor_backend=self._executor,
- identity_resolver=self._identity,
- workspace_prefix=self._config.workspace_prefix,
- default_agent_type=self._config.default_agent_type,
- auto_create_trace=self._config.auto_create_trace,
- dispatch_reactions=self._config.dispatch_reactions,
- dispatch_card_actions=self._config.dispatch_card_actions,
- )
- @property
- def config(self) -> FeishuChannelConfig:
- return self._config
- @property
- def feishu_connector(self) -> FeishuConnector:
- return self._connector
- @property
- def message_router(self) -> FeishuMessageRouter:
- return self._router
- @classmethod
- def from_env(cls) -> FeishuChannelManager:
- """从环境变量构造实例(与 docker-compose / .env 配合)。"""
- return cls(
- FeishuChannelConfig(
- feishu_http_base_url=env_str("FEISHU_HTTP_BASE_URL", "http://127.0.0.1:4380"),
- http_timeout=env_float("FEISHU_HTTP_TIMEOUT", 120.0),
- dispatch_reactions=env_bool("CHANNELS_DISPATCH_REACTIONS", False),
- dispatch_card_actions=env_bool("CHANNELS_DISPATCH_CARD_ACTIONS", True),
- agent_api_base_url=env_str("GATEWAY_AGENT_API_BASE_URL", "http://127.0.0.1:8000"),
- agent_run_model=env_str("FEISHU_AGENT_RUN_MODEL", "qwen3.5-flash"),
- agent_run_max_iterations=env_int("FEISHU_AGENT_RUN_MAX_ITERATIONS", 200),
- agent_run_temperature=env_float("FEISHU_AGENT_RUN_TEMPERATURE", 0.3),
- feishu_run_notify_on_submit=env_bool("CHANNELS_FEISHU_RUN_NOTIFY", True),
- poll_assistant_messages=env_bool("FEISHU_AGENT_POLL_ASSISTANTS", True),
- poll_interval_seconds=env_float("FEISHU_AGENT_POLL_INTERVAL", 1.0),
- poll_request_timeout=env_float("FEISHU_AGENT_POLL_REQUEST_TIMEOUT", 30.0),
- poll_terminal_grace_rounds=env_int("FEISHU_AGENT_POLL_GRACE_ROUNDS", 2),
- poll_max_seconds=env_float("FEISHU_AGENT_POLL_MAX_SECONDS", 0.0),
- assistant_max_text_chars=env_int("FEISHU_AGENT_ASSISTANT_MAX_CHARS", 8000),
- typing_reaction_enabled=env_bool("FEISHU_TYPING_REACTION", True),
- typing_reaction_emoji=env_str("FEISHU_TYPING_REACTION_EMOJI", "Typing") or "Typing",
- stop_container_on_trace_terminal=env_bool("GATEWAY_WORKSPACE_STOP_ON_TRACE_TERMINAL", False),
- stop_container_on_trace_not_found=env_bool("GATEWAY_WORKSPACE_STOP_ON_TRACE_NOT_FOUND", True),
- release_ref_on_trace_terminal=env_bool("GATEWAY_LIFECYCLE_RELEASE_REF_ON_TRACE_TERMINAL", False),
- release_previous_trace_ref_on_bind=env_bool("GATEWAY_LIFECYCLE_RELEASE_PREV_TRACE_REF_ON_BIND", False),
- )
- )
- async def handle_feishu_inbound_webhook(self, body: Mapping[str, Any]) -> RouteResult:
- """处理飞书适配层 POST 到 ``/api/channels/feishu/inbound/webhook`` 的规范化事件。"""
- cid = self._config.channel_id
- if not self._running.get(cid, False):
- return RouteResult(ok=False, error="channel_stopped")
- try:
- event = FeishuConnector.parse_feishu_inbound_event(body)
- except WebhookParseError as e:
- return RouteResult(ok=False, error=str(e))
- return await self._router.route_feishu_inbound_event(event)
|