fix(acceptance): repair official account replies and processor navigation

This commit is contained in:
Hyu
2026-10-09 20:13:51 +08:00
parent fea35e8b3f
commit 396b09ecce
9 changed files with 256 additions and 101 deletions
+21 -2
View File
@@ -465,7 +465,11 @@ def build_platform_tool_resources(
reason = None
if definition is None:
reason = 'unknown_tool'
elif definition.api not in supported_apis:
elif definition.api not in supported_apis and not (
definition.name == 'event_reply'
and {'reply_message', 'get_message'} <= supported_apis
and (event.delivery.reply_target or {}).get('message_id')
):
reason = 'adapter_api_unsupported'
elif definition.scope == 'event' and not _event_matches(event.event_type, definition.event_patterns):
reason = 'event_incompatible'
@@ -727,9 +731,24 @@ async def execute_platform_tool(
bot = await ap.platform_mgr.get_bot_by_uuid(execution_context, bot_id)
if bot is None:
raise ValueError(f'Bot {bot_id} is not running')
if definition.api not in set(bot.adapter.get_supported_apis() or []):
supported_apis = set(bot.adapter.get_supported_apis() or [])
reply_via_source = (
definition.name == 'event_reply'
and 'send_message' not in supported_apis
and {'reply_message', 'get_message'} <= supported_apis
and (delivery.get('reply_target') or {}).get('message_id')
)
if definition.api not in supported_apis and not reply_via_source:
raise ValueError(f'Platform API {definition.api} is no longer supported by bot {bot_id}')
api_func = getattr(bot.adapter, definition.api, None)
if reply_via_source:
async def api_func(target_type, target_id, message):
source = await bot.adapter.get_message(
chat_type=target_type,
chat_id=target_id,
message_id=delivery['reply_target']['message_id'],
)
return await bot.adapter.reply_message(source, message)
if not callable(api_func):
raise ValueError(f'Platform API {definition.api} is declared but not implemented')
if definition.api == 'send_message':
+4 -3
View File
@@ -704,7 +704,8 @@ class BotService:
bot = await self.get_bot(context, bot_data['uuid'], include_secret=True)
try:
await self.ap.platform_mgr.load_bot(context, bot)
if bot.get('enable'):
await self.ap.platform_mgr.load_bot(context, bot)
except Exception as exc:
raise BotApplyError(bot_error_message(exc, bot), bot['uuid']) from exc
@@ -740,8 +741,8 @@ class BotService:
bot = await self.get_bot(context, bot_uuid, include_secret=True)
try:
await self.ap.platform_mgr.remove_bot(context, bot_uuid)
runtime_bot = await self.ap.platform_mgr.load_bot(context, bot)
if runtime_bot.enable:
if bot.get('enable'):
runtime_bot = await self.ap.platform_mgr.load_bot(context, bot)
await runtime_bot.run()
except Exception as exc:
raise BotApplyError(bot_error_message(exc, bot), bot['uuid']) from exc
@@ -56,13 +56,14 @@ class OfficialAccountAdapter(OfficialAccountAPIMixin, abstract_platform_adapter.
}
if mode == 'drop':
bot = OAClient(**common_kwargs)
elif mode == 'passive':
elif mode in ('passive', 'auto'):
bot = OAClientForLongerResponse(
**common_kwargs,
LoadingMessage=config.get('LoadingMessage', ''),
customer_service=mode == 'auto',
)
else:
raise KeyError('OfficialAccount Mode must be "drop" or "passive"')
raise KeyError('OfficialAccount Mode must be "drop", "passive" or "auto"')
super().__init__(
config=config,
@@ -112,11 +113,12 @@ class OfficialAccountAdapter(OfficialAccountAPIMixin, abstract_platform_adapter.
if not isinstance(source, OAEvent):
raise ValueError('OfficialAccount reply_message requires an OAEvent source object')
content = await OfficialAccountMessageConverter.yiri2target(message)
if self.config.get('Mode') == 'passive':
await self.bot.set_message(source.user_id, source.message_id, content)
if self.config.get('Mode') in ('passive', 'auto'):
queued = await self.bot.set_message(source.user_id, source.message_id, content)
else:
await self.bot.set_message(source.message_id, content)
return platform_events.MessageResult(message_id=source.message_id, raw={'queued': True})
queued = True
return platform_events.MessageResult(message_id=source.message_id, raw={'queued': queued})
async def call_platform_api(self, action: str, params: dict = {}) -> dict:
handler = PLATFORM_API_MAP.get(action)
@@ -34,6 +34,22 @@ spec:
type: array[string]
required: false
default: []
- name: AppID
label:
en_US: App ID
zh_Hans: 应用 ID
zh_Hant: 應用 ID
type: string
required: true
default: ""
- name: AppSecret
label:
en_US: App Secret
zh_Hans: 应用密钥
zh_Hant: 應用密鑰
type: string
required: true
default: ""
- name: webhook_url
label:
en_US: Webhook Callback URL
@@ -62,32 +78,32 @@ spec:
type: string
required: true
default: ""
- name: AppID
label:
en_US: App ID
zh_Hans: 应用 ID
zh_Hant: 應用 ID
type: string
required: true
default: ""
- name: AppSecret
label:
en_US: App Secret
zh_Hans: 应用密钥
zh_Hant: 應用密鑰
type: string
required: true
default: ""
- name: Mode
label:
en_US: Mode
zh_Hans: 接入模式
zh_Hant: 接入模式
description:
en_US: "drop replies within the current callback; passive returns a loading message first and queues the real reply for the user's next message."
zh_Hans: "drop 会在当前回调内等待回复;passive 会先返回加载提示,并将真实回复排队到用户下一条消息。"
zh_Hant: "drop 會在目前回調內等待回覆;passive 會先回傳載入提示,並將真實回覆排隊到使用者下一則訊息。"
type: string
en_US: "Auto uses customer service messaging when available, falling back to next-message retrieval for permission, window or quota limits. Passive always waits for the next message. Drop requires a reply within 5 seconds."
zh_Hans: "自动模式在生成完成后通过客服接口回复;无权限、超出回复窗口或额度不足时,改为下一条消息领取。passive 始终由下一条消息领取;drop 需要在 5 秒内完成回复。客服接口通常需企业主体认证,以微信实际权限为准。"
zh_Hant: "自動模式在生成完成後透過客服介面回覆;無權限、超出回覆時限或額度不足時,改為下一則訊息領取。passive 始終由下一則訊息領取;drop 需要在 5 秒內完成回覆。客服介面通常需企業主體認證,以微信實際權限為準。"
type: select
options:
- name: auto
label:
en_US: Automatic reply with fallback (auto)
zh_Hans: 自动回复,受限时转为领取(auto)
zh_Hant: 自動回覆,受限時轉為領取(auto)
- name: drop
label:
en_US: Reply in current callback (drop)
zh_Hans: 当前回调内回复(drop)
zh_Hant: 目前回調內回覆(drop)
- name: passive
label:
en_US: Queue reply for next message (passive)
zh_Hans: 下一条消息时返回回复(passive)
zh_Hant: 下一則訊息時傳回回覆(passive)
required: true
default: "drop"
- name: LoadingMessage
+9 -2
View File
@@ -450,10 +450,13 @@ class RuntimeBot:
return {}
if hasattr(model, 'model_dump'):
try:
return model.model_dump(mode='json')
# Native adapter objects are for replying, not event logging.
# Exclude them before JSON serialization, since some (OAEvent,
# for example) cannot be serialized by Pydantic.
return model.model_dump(mode='json', exclude={'source_platform_object'})
except TypeError:
try:
return model.model_dump()
return model.model_dump(exclude={'source_platform_object'})
except Exception:
return {}
except Exception:
@@ -2231,6 +2234,10 @@ class PlatformManager:
)
binding = _binding
for bot in result.all():
# Disabled drafts may not have the credentials needed to construct
# an adapter. They become runtime bots when explicitly enabled.
if not bot.enable:
continue
try:
if binding is None:
binding = await self.ap.workspace_service.get_execution_binding(workspace_uuid)