mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-08 03:01:47 +00:00
fix(feishu): refresh bot identity during hydration
This commit is contained in:
parent
314361733f
commit
80b386a472
2 changed files with 75 additions and 42 deletions
|
|
@ -3862,47 +3862,50 @@ class FeishuAdapter(BasePlatformAdapter):
|
|||
and self-sent bot event filtering.
|
||||
|
||||
Populates ``_bot_open_id`` and ``_bot_name`` from /open-apis/bot/v3/info
|
||||
(no extra scopes required beyond the tenant access token). Falls back to
|
||||
the application info endpoint for ``_bot_name`` only when the first probe
|
||||
doesn't return it. Each field is hydrated independently — a value already
|
||||
supplied via env vars (FEISHU_BOT_OPEN_ID / FEISHU_BOT_USER_ID /
|
||||
FEISHU_BOT_NAME) is preserved and skips its probe.
|
||||
(no extra scopes required beyond the tenant access token). The probe
|
||||
always runs when a client is available so stale env vars from app/bot
|
||||
migrations do not break group @mention gating. Falls back to the
|
||||
application info endpoint for ``_bot_name`` only when the first probe
|
||||
doesn't return it. If the probe fails, env-provided values are preserved.
|
||||
"""
|
||||
if not self._client:
|
||||
return
|
||||
if self._bot_open_id and self._bot_name:
|
||||
# Everything the self-send filter and precise mention gate need is
|
||||
# already in place; nothing to probe.
|
||||
return
|
||||
|
||||
# Primary probe: /open-apis/bot/v3/info — returns bot_name + open_id, no
|
||||
# extra scopes required. This is the same endpoint the onboarding wizard
|
||||
# uses via probe_bot().
|
||||
if not self._bot_open_id or not self._bot_name:
|
||||
try:
|
||||
req = (
|
||||
BaseRequest.builder()
|
||||
.http_method(HttpMethod.GET)
|
||||
.uri("/open-apis/bot/v3/info")
|
||||
.token_types({AccessTokenType.TENANT})
|
||||
.build()
|
||||
)
|
||||
resp = await asyncio.to_thread(self._client.request, req)
|
||||
content = getattr(getattr(resp, "raw", None), "content", None)
|
||||
if content:
|
||||
payload = json.loads(content)
|
||||
parsed = _parse_bot_response(payload) or {}
|
||||
open_id = (parsed.get("bot_open_id") or "").strip()
|
||||
bot_name = (parsed.get("bot_name") or "").strip()
|
||||
if open_id and not self._bot_open_id:
|
||||
self._bot_open_id = open_id
|
||||
if bot_name and not self._bot_name:
|
||||
self._bot_name = bot_name
|
||||
except Exception:
|
||||
logger.debug(
|
||||
"[Feishu] /bot/v3/info probe failed during hydration",
|
||||
exc_info=True,
|
||||
)
|
||||
try:
|
||||
req = (
|
||||
BaseRequest.builder()
|
||||
.http_method(HttpMethod.GET)
|
||||
.uri("/open-apis/bot/v3/info")
|
||||
.token_types({AccessTokenType.TENANT})
|
||||
.build()
|
||||
)
|
||||
resp = await asyncio.to_thread(self._client.request, req)
|
||||
content = getattr(getattr(resp, "raw", None), "content", None)
|
||||
if content:
|
||||
payload = json.loads(content)
|
||||
parsed = _parse_bot_response(payload) or {}
|
||||
open_id = (parsed.get("bot_open_id") or "").strip()
|
||||
bot_name = (parsed.get("bot_name") or "").strip()
|
||||
if open_id:
|
||||
if self._bot_open_id and self._bot_open_id != open_id:
|
||||
logger.warning(
|
||||
"[Feishu] FEISHU_BOT_OPEN_ID is stale; using /bot/v3/info open_id for group @mention gating."
|
||||
)
|
||||
self._bot_open_id = open_id
|
||||
if bot_name:
|
||||
if self._bot_name and self._bot_name != bot_name:
|
||||
logger.info(
|
||||
"[Feishu] FEISHU_BOT_NAME differs from /bot/v3/info; using hydrated bot name for group @mention gating."
|
||||
)
|
||||
self._bot_name = bot_name
|
||||
except Exception:
|
||||
logger.debug(
|
||||
"[Feishu] /bot/v3/info probe failed during hydration",
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
# Fallback probe for _bot_name only: application info endpoint. Needs
|
||||
# admin:app.info:readonly or application:application:self_manage scope,
|
||||
|
|
|
|||
|
|
@ -2817,20 +2817,32 @@ class TestHydrateBotIdentity(unittest.TestCase):
|
|||
},
|
||||
clear=True,
|
||||
)
|
||||
def test_hydration_skipped_when_env_vars_supply_both_fields(self):
|
||||
def test_hydration_refreshes_env_values_when_bot_info_available(self):
|
||||
adapter = self._make_adapter()
|
||||
adapter._client = Mock()
|
||||
adapter._client.request = Mock()
|
||||
payload = json.dumps(
|
||||
{
|
||||
"code": 0,
|
||||
"bot": {
|
||||
"bot_name": "Hydrated Hermes",
|
||||
"open_id": "ou_hydrated",
|
||||
},
|
||||
}
|
||||
).encode("utf-8")
|
||||
adapter._client.request = Mock(return_value=SimpleNamespace(raw=SimpleNamespace(content=payload)))
|
||||
|
||||
asyncio.run(adapter._hydrate_bot_identity())
|
||||
|
||||
adapter._client.request.assert_not_called()
|
||||
self.assertEqual(adapter._bot_open_id, "ou_env")
|
||||
self.assertEqual(adapter._bot_name, "Env Hermes")
|
||||
# PR #16993 semantics: /bot/v3/info probe runs unconditionally
|
||||
# and hydrated values win over env vars so a stale FEISHU_BOT_*
|
||||
# from an old app registration doesn't break @mention gating.
|
||||
adapter._client.request.assert_called_once()
|
||||
self.assertEqual(adapter._bot_open_id, "ou_hydrated")
|
||||
self.assertEqual(adapter._bot_name, "Hydrated Hermes")
|
||||
|
||||
@patch.dict(os.environ, {"FEISHU_BOT_OPEN_ID": "ou_env"}, clear=True)
|
||||
def test_hydration_fills_only_missing_fields(self):
|
||||
"""Env-var open_id must NOT be overwritten by a different probe value."""
|
||||
def test_hydration_overwrites_stale_env_open_id(self):
|
||||
"""A stale env open_id should not break group mention gating after app migration."""
|
||||
adapter = self._make_adapter()
|
||||
adapter._client = Mock()
|
||||
payload = json.dumps(
|
||||
|
|
@ -2846,9 +2858,27 @@ class TestHydrateBotIdentity(unittest.TestCase):
|
|||
|
||||
asyncio.run(adapter._hydrate_bot_identity())
|
||||
|
||||
self.assertEqual(adapter._bot_open_id, "ou_env") # preserved
|
||||
self.assertEqual(adapter._bot_open_id, "ou_probe_DIFFERENT")
|
||||
self.assertEqual(adapter._bot_name, "Hermes Bot") # filled in
|
||||
|
||||
@patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"FEISHU_BOT_OPEN_ID": "ou_env",
|
||||
"FEISHU_BOT_NAME": "Env Hermes",
|
||||
},
|
||||
clear=True,
|
||||
)
|
||||
def test_hydration_preserves_env_values_when_bot_info_probe_fails(self):
|
||||
adapter = self._make_adapter()
|
||||
adapter._client = Mock()
|
||||
adapter._client.request = Mock(side_effect=RuntimeError("network down"))
|
||||
|
||||
asyncio.run(adapter._hydrate_bot_identity())
|
||||
|
||||
self.assertEqual(adapter._bot_open_id, "ou_env")
|
||||
self.assertEqual(adapter._bot_name, "Env Hermes")
|
||||
|
||||
@patch.dict(os.environ, {}, clear=True)
|
||||
def test_hydration_tolerates_probe_failure_and_falls_back_to_app_info(self):
|
||||
adapter = self._make_adapter()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue