mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-14 04:02:26 +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.
|
and self-sent bot event filtering.
|
||||||
|
|
||||||
Populates ``_bot_open_id`` and ``_bot_name`` from /open-apis/bot/v3/info
|
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
|
(no extra scopes required beyond the tenant access token). The probe
|
||||||
the application info endpoint for ``_bot_name`` only when the first probe
|
always runs when a client is available so stale env vars from app/bot
|
||||||
doesn't return it. Each field is hydrated independently — a value already
|
migrations do not break group @mention gating. Falls back to the
|
||||||
supplied via env vars (FEISHU_BOT_OPEN_ID / FEISHU_BOT_USER_ID /
|
application info endpoint for ``_bot_name`` only when the first probe
|
||||||
FEISHU_BOT_NAME) is preserved and skips its probe.
|
doesn't return it. If the probe fails, env-provided values are preserved.
|
||||||
"""
|
"""
|
||||||
if not self._client:
|
if not self._client:
|
||||||
return
|
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
|
# Primary probe: /open-apis/bot/v3/info — returns bot_name + open_id, no
|
||||||
# extra scopes required. This is the same endpoint the onboarding wizard
|
# extra scopes required. This is the same endpoint the onboarding wizard
|
||||||
# uses via probe_bot().
|
# uses via probe_bot().
|
||||||
if not self._bot_open_id or not self._bot_name:
|
try:
|
||||||
try:
|
req = (
|
||||||
req = (
|
BaseRequest.builder()
|
||||||
BaseRequest.builder()
|
.http_method(HttpMethod.GET)
|
||||||
.http_method(HttpMethod.GET)
|
.uri("/open-apis/bot/v3/info")
|
||||||
.uri("/open-apis/bot/v3/info")
|
.token_types({AccessTokenType.TENANT})
|
||||||
.token_types({AccessTokenType.TENANT})
|
.build()
|
||||||
.build()
|
)
|
||||||
)
|
resp = await asyncio.to_thread(self._client.request, req)
|
||||||
resp = await asyncio.to_thread(self._client.request, req)
|
content = getattr(getattr(resp, "raw", None), "content", None)
|
||||||
content = getattr(getattr(resp, "raw", None), "content", None)
|
if content:
|
||||||
if content:
|
payload = json.loads(content)
|
||||||
payload = json.loads(content)
|
parsed = _parse_bot_response(payload) or {}
|
||||||
parsed = _parse_bot_response(payload) or {}
|
open_id = (parsed.get("bot_open_id") or "").strip()
|
||||||
open_id = (parsed.get("bot_open_id") or "").strip()
|
bot_name = (parsed.get("bot_name") or "").strip()
|
||||||
bot_name = (parsed.get("bot_name") or "").strip()
|
if open_id:
|
||||||
if open_id and not self._bot_open_id:
|
if self._bot_open_id and self._bot_open_id != open_id:
|
||||||
self._bot_open_id = open_id
|
logger.warning(
|
||||||
if bot_name and not self._bot_name:
|
"[Feishu] FEISHU_BOT_OPEN_ID is stale; using /bot/v3/info open_id for group @mention gating."
|
||||||
self._bot_name = bot_name
|
)
|
||||||
except Exception:
|
self._bot_open_id = open_id
|
||||||
logger.debug(
|
if bot_name:
|
||||||
"[Feishu] /bot/v3/info probe failed during hydration",
|
if self._bot_name and self._bot_name != bot_name:
|
||||||
exc_info=True,
|
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
|
# Fallback probe for _bot_name only: application info endpoint. Needs
|
||||||
# admin:app.info:readonly or application:application:self_manage scope,
|
# admin:app.info:readonly or application:application:self_manage scope,
|
||||||
|
|
|
||||||
|
|
@ -2817,20 +2817,32 @@ class TestHydrateBotIdentity(unittest.TestCase):
|
||||||
},
|
},
|
||||||
clear=True,
|
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 = self._make_adapter()
|
||||||
adapter._client = Mock()
|
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())
|
asyncio.run(adapter._hydrate_bot_identity())
|
||||||
|
|
||||||
adapter._client.request.assert_not_called()
|
# PR #16993 semantics: /bot/v3/info probe runs unconditionally
|
||||||
self.assertEqual(adapter._bot_open_id, "ou_env")
|
# and hydrated values win over env vars so a stale FEISHU_BOT_*
|
||||||
self.assertEqual(adapter._bot_name, "Env Hermes")
|
# 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)
|
@patch.dict(os.environ, {"FEISHU_BOT_OPEN_ID": "ou_env"}, clear=True)
|
||||||
def test_hydration_fills_only_missing_fields(self):
|
def test_hydration_overwrites_stale_env_open_id(self):
|
||||||
"""Env-var open_id must NOT be overwritten by a different probe value."""
|
"""A stale env open_id should not break group mention gating after app migration."""
|
||||||
adapter = self._make_adapter()
|
adapter = self._make_adapter()
|
||||||
adapter._client = Mock()
|
adapter._client = Mock()
|
||||||
payload = json.dumps(
|
payload = json.dumps(
|
||||||
|
|
@ -2846,9 +2858,27 @@ class TestHydrateBotIdentity(unittest.TestCase):
|
||||||
|
|
||||||
asyncio.run(adapter._hydrate_bot_identity())
|
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
|
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)
|
@patch.dict(os.environ, {}, clear=True)
|
||||||
def test_hydration_tolerates_probe_failure_and_falls_back_to_app_info(self):
|
def test_hydration_tolerates_probe_failure_and_falls_back_to_app_info(self):
|
||||||
adapter = self._make_adapter()
|
adapter = self._make_adapter()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue