feat(feishu): 完善飞书适配器的错误处理与功能适配
1. 为飞书登录、消息操作等适配器添加基础错误捕获转发 2. 新增未加密事件校验逻辑,完善会话解析文档 3. 关闭飞书原生输入指示支持,移除相关缓存逻辑 4. 新增飞书向导OAuth适配方法,修正OAuth方法命名 5. 更新插件配置项,添加bot菜单与用户访问令牌配置 6. 修正消息操作枚举匹配与文档描述
This commit is contained in:
parent
ce0a7e7232
commit
6c0d0c7bd8
@ -93,6 +93,8 @@ class FeishuLoginAdapter:
|
|||||||
state = uuid.uuid4().hex
|
state = uuid.uuid4().hex
|
||||||
try:
|
try:
|
||||||
data = await self._client.get_qr_login_url(acct, redirect_uri, state)
|
data = await self._client.get_qr_login_url(acct, redirect_uri, state)
|
||||||
|
except Error:
|
||||||
|
raise
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise DependencyError(dep="feishu_api", cause=exc) from exc
|
raise DependencyError(dep="feishu_api", cause=exc) from exc
|
||||||
|
|
||||||
|
|||||||
@ -138,11 +138,16 @@ class FeishuMessageOpsAdapter:
|
|||||||
if sender_type != "app":
|
if sender_type != "app":
|
||||||
raise PermissionDeniedError(reason="not_bot_message")
|
raise PermissionDeniedError(reason="not_bot_message")
|
||||||
|
|
||||||
await self._client.update_message(
|
try:
|
||||||
account_id,
|
await self._client.update_message(
|
||||||
channel_msg_id,
|
account_id,
|
||||||
json.dumps({"text": content.text}),
|
channel_msg_id,
|
||||||
)
|
json.dumps({"text": content.text}),
|
||||||
|
)
|
||||||
|
except Error:
|
||||||
|
raise
|
||||||
|
except Exception as exc:
|
||||||
|
raise DependencyError(dep="feishu_api", cause=exc) from exc
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def recallMessage(self, channel_msg_id: str) -> bool:
|
async def recallMessage(self, channel_msg_id: str) -> bool:
|
||||||
@ -155,7 +160,12 @@ class FeishuMessageOpsAdapter:
|
|||||||
field="channel_msg_id",
|
field="channel_msg_id",
|
||||||
message="recall_time_exceeded",
|
message="recall_time_exceeded",
|
||||||
)
|
)
|
||||||
await self._client.recall_message(account_id, channel_msg_id)
|
try:
|
||||||
|
await self._client.recall_message(account_id, channel_msg_id)
|
||||||
|
except Error:
|
||||||
|
raise
|
||||||
|
except Exception as exc:
|
||||||
|
raise DependencyError(dep="feishu_api", cause=exc) from exc
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def getMessage(self, channel_msg_id: str) -> Message | None:
|
async def getMessage(self, channel_msg_id: str) -> Message | None:
|
||||||
@ -170,7 +180,12 @@ class FeishuMessageOpsAdapter:
|
|||||||
async def sendReaction(self, channel_msg_id: str, emoji: str) -> bool:
|
async def sendReaction(self, channel_msg_id: str, emoji: str) -> bool:
|
||||||
"""发送表情反应,并将 reaction_id 存入缓存。"""
|
"""发送表情反应,并将 reaction_id 存入缓存。"""
|
||||||
account_id = await self._resolve_account_id(channel_msg_id)
|
account_id = await self._resolve_account_id(channel_msg_id)
|
||||||
resp = await self._client.add_reaction(account_id, channel_msg_id, emoji)
|
try:
|
||||||
|
resp = await self._client.add_reaction(account_id, channel_msg_id, emoji)
|
||||||
|
except Error:
|
||||||
|
raise
|
||||||
|
except Exception as exc:
|
||||||
|
raise DependencyError(dep="feishu_api", cause=exc) from exc
|
||||||
reaction_id = resp["data"]["reaction_id"]
|
reaction_id = resp["data"]["reaction_id"]
|
||||||
await self._cache.set(_reaction_key(channel_msg_id, emoji), reaction_id)
|
await self._cache.set(_reaction_key(channel_msg_id, emoji), reaction_id)
|
||||||
return True
|
return True
|
||||||
@ -182,20 +197,35 @@ class FeishuMessageOpsAdapter:
|
|||||||
if opt.is_nothing():
|
if opt.is_nothing():
|
||||||
return False
|
return False
|
||||||
reaction_id = opt.unwrap()
|
reaction_id = opt.unwrap()
|
||||||
await self._client.delete_reaction(account_id, channel_msg_id, reaction_id)
|
try:
|
||||||
|
await self._client.delete_reaction(account_id, channel_msg_id, reaction_id)
|
||||||
|
except Error:
|
||||||
|
raise
|
||||||
|
except Exception as exc:
|
||||||
|
raise DependencyError(dep="feishu_api", cause=exc) from exc
|
||||||
await self._cache.delete(_reaction_key(channel_msg_id, emoji))
|
await self._cache.delete(_reaction_key(channel_msg_id, emoji))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def pinMessage(self, channel_msg_id: str) -> bool:
|
async def pinMessage(self, channel_msg_id: str) -> bool:
|
||||||
"""置顶消息。"""
|
"""置顶消息。"""
|
||||||
account_id = await self._resolve_account_id(channel_msg_id)
|
account_id = await self._resolve_account_id(channel_msg_id)
|
||||||
await self._client.pin_message(account_id, channel_msg_id)
|
try:
|
||||||
|
await self._client.pin_message(account_id, channel_msg_id)
|
||||||
|
except Error:
|
||||||
|
raise
|
||||||
|
except Exception as exc:
|
||||||
|
raise DependencyError(dep="feishu_api", cause=exc) from exc
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def unpinMessage(self, channel_msg_id: str) -> bool:
|
async def unpinMessage(self, channel_msg_id: str) -> bool:
|
||||||
"""取消置顶消息。"""
|
"""取消置顶消息。"""
|
||||||
account_id = await self._resolve_account_id(channel_msg_id)
|
account_id = await self._resolve_account_id(channel_msg_id)
|
||||||
await self._client.unpin_message(account_id, channel_msg_id)
|
try:
|
||||||
|
await self._client.unpin_message(account_id, channel_msg_id)
|
||||||
|
except Error:
|
||||||
|
raise
|
||||||
|
except Exception as exc:
|
||||||
|
raise DependencyError(dep="feishu_api", cause=exc) from exc
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def updateCard(
|
async def updateCard(
|
||||||
@ -213,13 +243,23 @@ class FeishuMessageOpsAdapter:
|
|||||||
card_content = payload.rich_message
|
card_content = payload.rich_message
|
||||||
if card_content is None:
|
if card_content is None:
|
||||||
card_content = card_builder.build_markdown_card(payload.content)
|
card_content = card_builder.build_markdown_card(payload.content)
|
||||||
await self._client.update_card(account_id, channel_msg_id, card_content)
|
try:
|
||||||
|
await self._client.update_card(account_id, channel_msg_id, card_content)
|
||||||
|
except Error:
|
||||||
|
raise
|
||||||
|
except Exception as exc:
|
||||||
|
raise DependencyError(dep="feishu_api", cause=exc) from exc
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def deleteCard(self, channel_msg_id: str) -> bool:
|
async def deleteCard(self, channel_msg_id: str) -> bool:
|
||||||
"""删除卡片消息(飞书通过撤回实现删除)。"""
|
"""删除卡片消息(飞书通过撤回实现删除)。"""
|
||||||
account_id = await self._resolve_account_id(channel_msg_id)
|
account_id = await self._resolve_account_id(channel_msg_id)
|
||||||
await self._client.recall_message(account_id, channel_msg_id)
|
try:
|
||||||
|
await self._client.recall_message(account_id, channel_msg_id)
|
||||||
|
except Error:
|
||||||
|
raise
|
||||||
|
except Exception as exc:
|
||||||
|
raise DependencyError(dep="feishu_api", cause=exc) from exc
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def replyMessage(
|
async def replyMessage(
|
||||||
@ -229,12 +269,17 @@ class FeishuMessageOpsAdapter:
|
|||||||
) -> str:
|
) -> str:
|
||||||
"""回复消息,返回新消息的 channel_msg_id。"""
|
"""回复消息,返回新消息的 channel_msg_id。"""
|
||||||
account_id = await self._resolve_account_id(channel_msg_id)
|
account_id = await self._resolve_account_id(channel_msg_id)
|
||||||
resp = await self._client.reply_message(
|
try:
|
||||||
account_id,
|
resp = await self._client.reply_message(
|
||||||
channel_msg_id,
|
account_id,
|
||||||
"text",
|
channel_msg_id,
|
||||||
json.dumps({"text": content.text}),
|
"text",
|
||||||
)
|
json.dumps({"text": content.text}),
|
||||||
|
)
|
||||||
|
except Error:
|
||||||
|
raise
|
||||||
|
except Exception as exc:
|
||||||
|
raise DependencyError(dep="feishu_api", cause=exc) from exc
|
||||||
return resp["data"]["message_id"]
|
return resp["data"]["message_id"]
|
||||||
|
|
||||||
async def sendUrgentMessage(
|
async def sendUrgentMessage(
|
||||||
@ -244,12 +289,17 @@ class FeishuMessageOpsAdapter:
|
|||||||
) -> str:
|
) -> str:
|
||||||
"""发送加急消息,返回加急消息的 channel_msg_id。"""
|
"""发送加急消息,返回加急消息的 channel_msg_id。"""
|
||||||
account_id = await self._resolve_account_id(channel_msg_id)
|
account_id = await self._resolve_account_id(channel_msg_id)
|
||||||
resp = await self._client.request(
|
try:
|
||||||
"POST",
|
resp = await self._client.request(
|
||||||
f"/open-apis/im/v1/messages/{channel_msg_id}/urgent",
|
"POST",
|
||||||
account_id,
|
f"/open-apis/im/v1/messages/{channel_msg_id}/urgent",
|
||||||
json={"msg_type": "text", "content": json.dumps({"text": content.text})},
|
account_id,
|
||||||
)
|
json={"msg_type": "text", "content": json.dumps({"text": content.text})},
|
||||||
|
)
|
||||||
|
except Error:
|
||||||
|
raise
|
||||||
|
except Exception as exc:
|
||||||
|
raise DependencyError(dep="feishu_api", cause=exc) from exc
|
||||||
return resp["data"]["message_id"]
|
return resp["data"]["message_id"]
|
||||||
|
|
||||||
async def batchGetMessages(
|
async def batchGetMessages(
|
||||||
@ -265,10 +315,15 @@ class FeishuMessageOpsAdapter:
|
|||||||
if not channel_msg_ids:
|
if not channel_msg_ids:
|
||||||
return []
|
return []
|
||||||
account_id = await self._resolve_account_id(channel_msg_ids[0])
|
account_id = await self._resolve_account_id(channel_msg_ids[0])
|
||||||
resp = await self._client.batch_get_messages(
|
try:
|
||||||
account_id,
|
resp = await self._client.batch_get_messages(
|
||||||
list(channel_msg_ids),
|
account_id,
|
||||||
)
|
list(channel_msg_ids),
|
||||||
|
)
|
||||||
|
except Error:
|
||||||
|
raise
|
||||||
|
except Exception as exc:
|
||||||
|
raise DependencyError(dep="feishu_api", cause=exc) from exc
|
||||||
items = resp.get("data", {}).get("items", [])
|
items = resp.get("data", {}).get("items", [])
|
||||||
return [self._to_message(item) for item in items]
|
return [self._to_message(item) for item in items]
|
||||||
|
|
||||||
@ -291,16 +346,21 @@ class FeishuMessageOpsAdapter:
|
|||||||
message="requires_at_least_2",
|
message="requires_at_least_2",
|
||||||
)
|
)
|
||||||
account_id = await self._resolve_account_id(channel_msg_ids[0])
|
account_id = await self._resolve_account_id(channel_msg_ids[0])
|
||||||
resp = await self._client.request(
|
try:
|
||||||
"POST",
|
resp = await self._client.request(
|
||||||
"/open-apis/im/v1/messages/merge_forward",
|
"POST",
|
||||||
account_id,
|
"/open-apis/im/v1/messages/merge_forward",
|
||||||
json={
|
account_id,
|
||||||
"message_ids": list(channel_msg_ids),
|
json={
|
||||||
"receive_id": target.get("receive_id", ""),
|
"message_ids": list(channel_msg_ids),
|
||||||
},
|
"receive_id": target.get("receive_id", ""),
|
||||||
receive_id_type=target.get("receive_id_type", "open_id"),
|
},
|
||||||
)
|
receive_id_type=target.get("receive_id_type", "open_id"),
|
||||||
|
)
|
||||||
|
except Error:
|
||||||
|
raise
|
||||||
|
except Exception as exc:
|
||||||
|
raise DependencyError(dep="feishu_api", cause=exc) from exc
|
||||||
return resp["data"]["message_id"]
|
return resp["data"]["message_id"]
|
||||||
|
|
||||||
async def markMessage(
|
async def markMessage(
|
||||||
@ -310,7 +370,12 @@ class FeishuMessageOpsAdapter:
|
|||||||
) -> bool:
|
) -> bool:
|
||||||
"""标记消息(飞书通过 Pin 实现,``mark_type`` 不区分)。"""
|
"""标记消息(飞书通过 Pin 实现,``mark_type`` 不区分)。"""
|
||||||
account_id = await self._resolve_account_id(channel_msg_id)
|
account_id = await self._resolve_account_id(channel_msg_id)
|
||||||
await self._client.pin_message(account_id, channel_msg_id)
|
try:
|
||||||
|
await self._client.pin_message(account_id, channel_msg_id)
|
||||||
|
except Error:
|
||||||
|
raise
|
||||||
|
except Exception as exc:
|
||||||
|
raise DependencyError(dep="feishu_api", cause=exc) from exc
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def favoriteMessage(self, channel_msg_id: str) -> bool:
|
async def favoriteMessage(self, channel_msg_id: str) -> bool:
|
||||||
@ -407,7 +472,7 @@ class FeishuMessageOpsAdapter:
|
|||||||
has_side_effects=True,
|
has_side_effects=True,
|
||||||
),
|
),
|
||||||
MessageOperationDefinition(
|
MessageOperationDefinition(
|
||||||
operation="recall",
|
operation=MessageOperation.RECALL,
|
||||||
tool_name="撤回消息",
|
tool_name="撤回消息",
|
||||||
description="撤回指定消息(24小时内)",
|
description="撤回指定消息(24小时内)",
|
||||||
parameters=(_PARAM_CHANNEL_MSG_ID,),
|
parameters=(_PARAM_CHANNEL_MSG_ID,),
|
||||||
@ -539,7 +604,7 @@ class FeishuMessageOpsAdapter:
|
|||||||
parameters["content"],
|
parameters["content"],
|
||||||
)
|
)
|
||||||
result_data = None
|
result_data = None
|
||||||
elif op == "recall":
|
elif op == MessageOperation.RECALL:
|
||||||
ok = await self.recallMessage(parameters["channel_msg_id"])
|
ok = await self.recallMessage(parameters["channel_msg_id"])
|
||||||
elif op == "get":
|
elif op == "get":
|
||||||
msg = await self.getMessage(parameters["channel_msg_id"])
|
msg = await self.getMessage(parameters["channel_msg_id"])
|
||||||
|
|||||||
@ -30,10 +30,16 @@ class FeishuSessionAdapter:
|
|||||||
async def parseSession(self, raw_event: RawEvent) -> SessionInfo:
|
async def parseSession(self, raw_event: RawEvent) -> SessionInfo:
|
||||||
"""从原始事件解析会话信息。
|
"""从原始事件解析会话信息。
|
||||||
|
|
||||||
@failure 事件结构不符合飞书协议(缺少 ``app_id`` 或 ``open_id``)
|
@failure 事件尚未解密(payload 含 ``encrypt`` 字段)抛
|
||||||
抛 ``ValidationError``。
|
``ValidationError``;事件结构不符合飞书协议(缺少 ``app_id``
|
||||||
|
或 ``open_id``)抛 ``ValidationError``。
|
||||||
"""
|
"""
|
||||||
event = raw_event.payload
|
event = raw_event.payload
|
||||||
|
if "encrypt" in event:
|
||||||
|
raise ValidationError(
|
||||||
|
field="encrypt",
|
||||||
|
message="event_not_decrypted: parseSession requires decrypted payload",
|
||||||
|
)
|
||||||
header = event.get("header", {})
|
header = event.get("header", {})
|
||||||
inner = event.get("event", {})
|
inner = event.get("event", {})
|
||||||
message = inner.get("message", {})
|
message = inner.get("message", {})
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
"""飞书流式适配器。
|
"""飞书流式适配器。
|
||||||
|
|
||||||
实现 ``StreamingAdapter`` Protocol,管理飞书流式卡片的生命周期
|
实现 ``StreamingAdapter`` Protocol,管理飞书流式卡片的生命周期
|
||||||
(开始 / 分块更新 / 结束)、输入指示(通过 Reaction API 模拟)与
|
(开始 / 分块更新 / 结束)与卡片整体 / 局部更新。覆盖 FR-13 流式输出能力。
|
||||||
卡片整体 / 局部更新。覆盖 FR-13 流式输出能力。
|
|
||||||
|
|
||||||
流式会话状态通过 ``CachePort`` 管理(INV-5 无 mutable 实例变量):
|
流式会话状态通过 ``CachePort`` 管理(INV-5 无 mutable 实例变量):
|
||||||
- ``feishu:streaming:{account_id}:{peer_id}`` → ``card_id``
|
- ``feishu:streaming:{account_id}:{peer_id}`` → ``card_id``
|
||||||
@ -10,8 +9,10 @@
|
|||||||
- ``feishu:streaming:card:{card_id}`` → ``account_id``
|
- ``feishu:streaming:card:{card_id}`` → ``account_id``
|
||||||
反向映射,供 ``updateFullCard`` / ``updatePartialCard`` 与
|
反向映射,供 ``updateFullCard`` / ``updatePartialCard`` 与
|
||||||
``MessageOpsAdapter`` 解析 account_id。
|
``MessageOpsAdapter`` 解析 account_id。
|
||||||
- ``feishu:typing:{account_id}:{peer_id}`` → ``reaction_id``
|
|
||||||
输入指示状态,飞书无原生 typing indicator,通过 Reaction 模拟。
|
输入指示(typing indicator):飞书无原生 typing indicator API,
|
||||||
|
``supports_typing_indicator`` 声明为 ``False``,``startTypingIndicator``
|
||||||
|
/ ``stopTypingIndicator`` 为无操作返回 ``False``。
|
||||||
|
|
||||||
依赖方向:仅 import yuxi.channels.contract.* 与同插件内部模块
|
依赖方向:仅 import yuxi.channels.contract.* 与同插件内部模块
|
||||||
(card_builder / FeishuClient),不污染框架层。
|
(card_builder / FeishuClient),不污染框架层。
|
||||||
@ -35,9 +36,7 @@ from yuxi.channels.contract.ports.driven.logger_port import LoggerPort
|
|||||||
from .. import card_builder
|
from .. import card_builder
|
||||||
from ..feishu_client import FeishuClient
|
from ..feishu_client import FeishuClient
|
||||||
|
|
||||||
_TYPING_EMOJI = "TYPING"
|
|
||||||
_STREAMING_TTL_SECONDS = 600
|
_STREAMING_TTL_SECONDS = 600
|
||||||
_TYPING_TTL_SECONDS = 30
|
|
||||||
|
|
||||||
|
|
||||||
def _streaming_key(account_id: str, peer_id: str) -> str:
|
def _streaming_key(account_id: str, peer_id: str) -> str:
|
||||||
@ -48,10 +47,6 @@ def _card_key(card_id: str) -> str:
|
|||||||
return f"feishu:streaming:card:{card_id}"
|
return f"feishu:streaming:card:{card_id}"
|
||||||
|
|
||||||
|
|
||||||
def _typing_key(account_id: str, peer_id: str) -> str:
|
|
||||||
return f"feishu:typing:{account_id}:{peer_id}"
|
|
||||||
|
|
||||||
|
|
||||||
class FeishuStreamingAdapter:
|
class FeishuStreamingAdapter:
|
||||||
"""飞书流式适配器。
|
"""飞书流式适配器。
|
||||||
|
|
||||||
@ -167,43 +162,26 @@ class FeishuStreamingAdapter:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
async def getStreamingCapability(self) -> StreamingCapability:
|
async def getStreamingCapability(self) -> StreamingCapability:
|
||||||
"""飞书流式能力声明。"""
|
"""飞书流式能力声明。
|
||||||
|
|
||||||
|
``supports_typing_indicator=False``:飞书无原生 typing indicator API,
|
||||||
|
不模拟输入指示。
|
||||||
|
"""
|
||||||
return StreamingCapability(
|
return StreamingCapability(
|
||||||
supports_streaming=True,
|
supports_streaming=True,
|
||||||
supports_typing_indicator=True,
|
supports_typing_indicator=False,
|
||||||
chunk_mode=ChunkMode.TEXT,
|
chunk_mode=ChunkMode.TEXT,
|
||||||
min_chunk_interval_ms=200,
|
min_chunk_interval_ms=200,
|
||||||
max_chunk_length=4000,
|
max_chunk_length=4000,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def startTypingIndicator(self, account_id: str, peer_id: str) -> bool:
|
async def startTypingIndicator(self, account_id: str, peer_id: str) -> bool:
|
||||||
"""开始输入指示(通过 Reaction API 添加 TYPING emoji)。
|
"""开始输入指示。飞书无原生 API,返回 ``False`` 表示未执行。"""
|
||||||
|
return False
|
||||||
飞书无原生 typing indicator,通过 ``client.add_reaction`` 对
|
|
||||||
对端最近消息添加 TYPING emoji 模拟。若已有活跃 typing reaction,
|
|
||||||
返回 ``False``。
|
|
||||||
"""
|
|
||||||
typing_key = _typing_key(account_id, peer_id)
|
|
||||||
existing = await self._cache.get(typing_key)
|
|
||||||
if existing.is_some():
|
|
||||||
return False
|
|
||||||
|
|
||||||
resp = await self._client.add_reaction(account_id, peer_id, _TYPING_EMOJI)
|
|
||||||
reaction_id = resp["data"]["reaction_id"]
|
|
||||||
await self._cache.set(typing_key, reaction_id, ttl_seconds=_TYPING_TTL_SECONDS)
|
|
||||||
return True
|
|
||||||
|
|
||||||
async def stopTypingIndicator(self, account_id: str, peer_id: str) -> bool:
|
async def stopTypingIndicator(self, account_id: str, peer_id: str) -> bool:
|
||||||
"""停止输入指示(移除 TYPING emoji Reaction)。"""
|
"""停止输入指示。飞书无原生 API,返回 ``False`` 表示未执行。"""
|
||||||
typing_key = _typing_key(account_id, peer_id)
|
return False
|
||||||
opt = await self._cache.get(typing_key)
|
|
||||||
if opt.is_nothing():
|
|
||||||
return False
|
|
||||||
reaction_id = opt.unwrap()
|
|
||||||
|
|
||||||
await self._client.delete_reaction(account_id, peer_id, reaction_id)
|
|
||||||
await self._cache.delete(typing_key)
|
|
||||||
return True
|
|
||||||
|
|
||||||
async def updateFullCard(self, card_id: str, content: Any) -> None:
|
async def updateFullCard(self, card_id: str, content: Any) -> None:
|
||||||
"""整体更新卡片内容。
|
"""整体更新卡片内容。
|
||||||
@ -212,7 +190,12 @@ class FeishuStreamingAdapter:
|
|||||||
``client.update_card`` 全量替换卡片。
|
``client.update_card`` 全量替换卡片。
|
||||||
"""
|
"""
|
||||||
account_id = await self._resolve_account_id(card_id)
|
account_id = await self._resolve_account_id(card_id)
|
||||||
await self._client.update_card(account_id, card_id, content)
|
try:
|
||||||
|
await self._client.update_card(account_id, card_id, content)
|
||||||
|
except Error:
|
||||||
|
raise
|
||||||
|
except Exception as exc:
|
||||||
|
raise DependencyError(dep="feishu_api", cause=exc) from exc
|
||||||
|
|
||||||
async def updatePartialCard(
|
async def updatePartialCard(
|
||||||
self,
|
self,
|
||||||
@ -233,7 +216,12 @@ class FeishuStreamingAdapter:
|
|||||||
"content": content,
|
"content": content,
|
||||||
"render_strategy": str(render_strategy),
|
"render_strategy": str(render_strategy),
|
||||||
}
|
}
|
||||||
await self._client.update_card(account_id, card_id, payload)
|
try:
|
||||||
|
await self._client.update_card(account_id, card_id, payload)
|
||||||
|
except Error:
|
||||||
|
raise
|
||||||
|
except Exception as exc:
|
||||||
|
raise DependencyError(dep="feishu_api", cause=exc) from exc
|
||||||
|
|
||||||
async def _resolve_account_id(self, card_id: str) -> str:
|
async def _resolve_account_id(self, card_id: str) -> str:
|
||||||
"""从 ``CachePort`` 反向映射解析 card_id 对应的 account_id。
|
"""从 ``CachePort`` 反向映射解析 card_id 对应的 account_id。
|
||||||
|
|||||||
@ -23,6 +23,8 @@ from yuxi.channels.contract.dtos.wizard import (
|
|||||||
)
|
)
|
||||||
from yuxi.channels.contract.errors import (
|
from yuxi.channels.contract.errors import (
|
||||||
DependencyError,
|
DependencyError,
|
||||||
|
Error,
|
||||||
|
NotImplementedError,
|
||||||
ValidationError,
|
ValidationError,
|
||||||
)
|
)
|
||||||
from yuxi.channels.contract.ports.driven.config_port import ConfigPort
|
from yuxi.channels.contract.ports.driven.config_port import ConfigPort
|
||||||
@ -312,6 +314,17 @@ class FeishuWizardAdapter:
|
|||||||
)
|
)
|
||||||
return WizardConfigPatch(step_id=step_id, values=dict(values))
|
return WizardConfigPatch(step_id=step_id, values=dict(values))
|
||||||
|
|
||||||
|
def supportsWizardOAuth(self) -> bool:
|
||||||
|
"""声明本适配器是否支持 OAuth 授权流程。
|
||||||
|
|
||||||
|
飞书向导的扫码登录步骤通过 ``handleOAuthCallback`` 处理授权回调,
|
||||||
|
但不通过 ``buildOAuthAuthorizeUrl`` 发起标准 OAuth 授权(扫码由
|
||||||
|
飞书客户端原生完成),返回 ``False``。``WizardService.initiateOAuth``
|
||||||
|
据此守卫,避免对不支持标准 OAuth 发起的渠道调用
|
||||||
|
``buildOAuthAuthorizeUrl``。
|
||||||
|
"""
|
||||||
|
return False
|
||||||
|
|
||||||
async def handleOAuthCallback(
|
async def handleOAuthCallback(
|
||||||
self,
|
self,
|
||||||
code: str,
|
code: str,
|
||||||
@ -352,13 +365,13 @@ class FeishuWizardAdapter:
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = await self._client.exchangeOAuthCode(
|
data = await self._client.exchange_oauth_code(
|
||||||
app_id,
|
app_id,
|
||||||
app_secret,
|
app_secret,
|
||||||
code,
|
code,
|
||||||
redirect_uri,
|
redirect_uri,
|
||||||
)
|
)
|
||||||
except ValidationError:
|
except Error:
|
||||||
raise
|
raise
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise DependencyError(dep="feishu_oauth", cause=exc) from exc
|
raise DependencyError(dep="feishu_oauth", cause=exc) from exc
|
||||||
@ -380,6 +393,21 @@ class FeishuWizardAdapter:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def buildOAuthAuthorizeUrl(
|
||||||
|
self,
|
||||||
|
redirect_uri: str,
|
||||||
|
state: str,
|
||||||
|
*,
|
||||||
|
applied_config: dict[str, Any],
|
||||||
|
) -> str:
|
||||||
|
"""构造 OAuth 授权 URL(飞书向导不支持,抛 ``NotImplementedError``)。
|
||||||
|
|
||||||
|
飞书扫码登录由客户端原生完成,不通过后端构造授权 URL,本方法始终
|
||||||
|
抛 ``NotImplementedError``,由 dispatch handler 映射为 501
|
||||||
|
NOT_IMPLEMENTED。
|
||||||
|
"""
|
||||||
|
raise NotImplementedError(operation="buildOAuthAuthorizeUrl")
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
# ----------------------------------------------------------------------
|
||||||
# 步骤校验辅助
|
# 步骤校验辅助
|
||||||
|
|||||||
@ -30,7 +30,7 @@
|
|||||||
"capabilities": {
|
"capabilities": {
|
||||||
"rich_message": true,
|
"rich_message": true,
|
||||||
"streaming": true,
|
"streaming": true,
|
||||||
"typing_indicator": true,
|
"typing_indicator": false,
|
||||||
"message_edit": true,
|
"message_edit": true,
|
||||||
"message_recall": true,
|
"message_recall": true,
|
||||||
"supports_reaction": true,
|
"supports_reaction": true,
|
||||||
@ -99,7 +99,9 @@
|
|||||||
{"key": "max_retries", "type": "integer", "required": false, "default": 3, "hot_reloadable": true, "scope": "channel", "constraints": {"min": 0, "max": 5}},
|
{"key": "max_retries", "type": "integer", "required": false, "default": 3, "hot_reloadable": true, "scope": "channel", "constraints": {"min": 0, "max": 5}},
|
||||||
{"key": "rate_limit_per_sec", "type": "integer", "required": false, "default": 50, "hot_reloadable": true, "scope": "channel", "constraints": {"min": 1, "max": 100}},
|
{"key": "rate_limit_per_sec", "type": "integer", "required": false, "default": 50, "hot_reloadable": true, "scope": "channel", "constraints": {"min": 1, "max": 100}},
|
||||||
{"key": "user_cache_ttl_sec", "type": "integer", "required": false, "default": 1800, "hot_reloadable": true, "scope": "channel", "constraints": {"min": 60, "max": 7200}},
|
{"key": "user_cache_ttl_sec", "type": "integer", "required": false, "default": 1800, "hot_reloadable": true, "scope": "channel", "constraints": {"min": 60, "max": 7200}},
|
||||||
{"key": "identity_cache_ttl_sec", "type": "integer", "required": false, "default": 60, "hot_reloadable": true, "scope": "channel", "constraints": {"min": 30, "max": 600}}
|
{"key": "identity_cache_ttl_sec", "type": "integer", "required": false, "default": 60, "hot_reloadable": true, "scope": "channel", "constraints": {"min": 30, "max": 600}},
|
||||||
|
{"key": "bot_menu_commands", "type": "json", "required": false, "default": null, "hot_reloadable": true, "scope": "channel", "constraints": {}, "description": "飞书 Bot 菜单命令列表,每项为 {name, description, permission, is_silent}"},
|
||||||
|
{"key": "user_access_token", "type": "string", "required": false, "default": null, "hot_reloadable": false, "sensitive": true, "scope": "account", "constraints": {}, "description": "用户访问令牌(OAuth 授权获取,用于用户身份操作)"}
|
||||||
],
|
],
|
||||||
"env_vars": [
|
"env_vars": [
|
||||||
{"name": "FEISHU_APP_ID", "description": "飞书应用 App ID", "required": true, "default": null, "sensitive": false},
|
{"name": "FEISHU_APP_ID", "description": "飞书应用 App ID", "required": true, "default": null, "sensitive": false},
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user