mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-16 23:07:14 +00:00
fix(telemetry): require acknowledged adapter evidence and prepare beta.3
This commit is contained in:
@@ -23,6 +23,18 @@ def boundary_fields(module, kind, operation, bound, parent):
|
||||
else:
|
||||
# Keep the generic boundary for investigation, not a false named test.
|
||||
evidence = False
|
||||
if (
|
||||
entry['adapter'] == 'lark-omni'
|
||||
and resolved
|
||||
in {
|
||||
'platform_api.check_tenant_access_token',
|
||||
'platform_api.refresh_app_access_token',
|
||||
'platform_api.refresh_tenant_access_token',
|
||||
}
|
||||
and getattr(bound.get('self'), 'config', {}).get('app_type', 'self') != 'isv'
|
||||
):
|
||||
# Self-built apps return {'ok': True} without checking/refreshing a token.
|
||||
evidence = False
|
||||
# A forwarding API may call another decorated method. Count the outer call.
|
||||
if parent and parent.adapter_api_active:
|
||||
evidence = False
|
||||
@@ -30,6 +42,8 @@ def boundary_fields(module, kind, operation, bound, parent):
|
||||
return {'_adapter_api_active': kind == 'api'}
|
||||
return {
|
||||
'_adapter_api_active': kind == 'api',
|
||||
'_adapter_void_ack': completed_void_api(module, resolved),
|
||||
'_adapter_read_ack': completed_read_api(module, resolved),
|
||||
'operation': resolved,
|
||||
'attributes': {'adapter_evidence': True, **message_scenario(bound)},
|
||||
}
|
||||
@@ -68,3 +82,171 @@ def message_scenario(bound):
|
||||
)
|
||||
result['content_type'] = next(iter(types)) if len(types) == 1 else 'mixed' if types else 'unknown'
|
||||
return result
|
||||
|
||||
|
||||
# These exact API implementations always await an exception-raising SDK call,
|
||||
# including SDKs whose successful acknowledgement has no return payload.
|
||||
# Do not extend this to methods with conditional no-op or queued work.
|
||||
VOID_ACKNOWLEDGED_APIS = {
|
||||
'aiocqhttp': frozenset(
|
||||
{
|
||||
'delete_message',
|
||||
'set_group_name',
|
||||
'mute_member',
|
||||
'unmute_member',
|
||||
'kick_member',
|
||||
'leave_group',
|
||||
'approve_friend_request',
|
||||
'approve_group_invite',
|
||||
}
|
||||
),
|
||||
'discord': frozenset(
|
||||
{
|
||||
'edit_message',
|
||||
'delete_message',
|
||||
'mute_member',
|
||||
'unmute_member',
|
||||
'kick_member',
|
||||
'leave_group',
|
||||
}
|
||||
),
|
||||
'kook': frozenset({'delete_message'}),
|
||||
}
|
||||
|
||||
|
||||
def completed_void_api(module, operation):
|
||||
return module.endswith('.api_impl') and any(
|
||||
directory in module.split('.') and operation in operations
|
||||
for directory, operations in VOID_ACKNOWLEDGED_APIS.items()
|
||||
)
|
||||
|
||||
|
||||
# These read methods return actual fetched/cached data or raise. Exclude the
|
||||
# known identity-only placeholders and file-ID passthroughs: a typed result alone
|
||||
# is not evidence that a lookup worked. Unsupported methods still raise normally.
|
||||
_READ_APIS = frozenset(
|
||||
{
|
||||
'get_message',
|
||||
'get_group_info',
|
||||
'get_group_list',
|
||||
'get_group_member_list',
|
||||
'get_group_member_info',
|
||||
'get_user_info',
|
||||
'get_friend_list',
|
||||
'get_file_url',
|
||||
}
|
||||
)
|
||||
READ_ACKNOWLEDGED_APIS = {
|
||||
'aiocqhttp': _READ_APIS,
|
||||
'telegram': _READ_APIS,
|
||||
'discord': _READ_APIS - {'get_file_url'},
|
||||
'dingtalk': _READ_APIS - {'get_group_info', 'get_user_info'},
|
||||
'kook': _READ_APIS - {'get_file_url'},
|
||||
'lark': _READ_APIS - {'get_user_info', 'get_group_member_info', 'get_file_url'},
|
||||
'officialaccount': _READ_APIS,
|
||||
'qqofficial': _READ_APIS,
|
||||
'slack': _READ_APIS,
|
||||
'wecom': _READ_APIS - {'get_user_info'},
|
||||
'wecombot': _READ_APIS,
|
||||
'wecomcs': _READ_APIS,
|
||||
}
|
||||
|
||||
|
||||
def completed_read_api(module, operation):
|
||||
return module.endswith('.api_impl') and any(
|
||||
directory in module.split('.') and operation in operations
|
||||
for directory, operations in READ_ACKNOWLEDGED_APIS.items()
|
||||
)
|
||||
|
||||
|
||||
def response_outcome(value, adapter=None):
|
||||
"""Finite acknowledgement contracts; never inspect or export message content.
|
||||
|
||||
A MessageResult may contain an *inbound* message ID even when nothing was
|
||||
sent. Only its actual raw acknowledgement is eligible. Empty/queued/unknown
|
||||
returns are unconfirmed, not failures. Unknown SDK shapes need an explicit
|
||||
confirmation at their operation boundary, not a truthiness fallback.
|
||||
"""
|
||||
from langbot_plugin.api.entities.builtin.platform.events import MessageResult
|
||||
|
||||
message_result = isinstance(value, MessageResult)
|
||||
if message_result:
|
||||
value = value.raw
|
||||
if not isinstance(value, dict):
|
||||
return 'skipped'
|
||||
if (
|
||||
value.get('ok') is False
|
||||
or value.get('status') == 'failed'
|
||||
or any(type(value.get(key)) is int and value[key] != 0 for key in ('retcode', 'errcode', 'code'))
|
||||
):
|
||||
return 'failed'
|
||||
if value.get('queued') is True or value.get('status') == 'async':
|
||||
return 'skipped'
|
||||
# The wrappers that batch sends retain the real responses in these fields.
|
||||
if 'results' in value:
|
||||
results = value['results']
|
||||
if not isinstance(results, list) or not results:
|
||||
return 'skipped'
|
||||
outcomes = [response_outcome(item, adapter) for item in results]
|
||||
return 'failed' if 'failed' in outcomes else 'skipped' if 'skipped' in outcomes else 'succeeded'
|
||||
for key in ('result', 'raw'):
|
||||
if key in value:
|
||||
return response_outcome(value[key], adapter)
|
||||
if (
|
||||
value.get('ok') is True
|
||||
or value.get('status') == 'ok'
|
||||
or any(type(value.get(key)) is int and value[key] == 0 for key in ('retcode', 'errcode', 'code'))
|
||||
):
|
||||
return 'succeeded'
|
||||
if message_result:
|
||||
# These wrappers put only SDK-returned IDs in raw (never the source ID).
|
||||
if adapter in {'aiocqhttp-omni', 'discord-omni', 'telegram-omni'}:
|
||||
message_id = value.get('message_id')
|
||||
if type(message_id) in (str, int) and message_id:
|
||||
return 'succeeded'
|
||||
if adapter == 'lark-omni':
|
||||
ids = value.get('message_ids')
|
||||
if isinstance(ids, list) and ids and all(isinstance(item, str) and item for item in ids):
|
||||
return 'succeeded'
|
||||
return 'skipped'
|
||||
|
||||
|
||||
def record_api_result(value, *, edited_content=None):
|
||||
"""Record Telegram's Message/True acknowledgement and return it unchanged.
|
||||
|
||||
This runs after the SDK await, not after conversion or local stream setup.
|
||||
No payload is retained; exceptions and the adapter's public return stay intact.
|
||||
"""
|
||||
try:
|
||||
from .diagnostics import current_span
|
||||
|
||||
span = current_span()
|
||||
if span is not None and span.kind == 'api' and span.fields.get('attributes', {}).get('adapter_evidence'):
|
||||
from telegram import Message
|
||||
|
||||
outcome = (
|
||||
'succeeded'
|
||||
if value is True or isinstance(value, Message)
|
||||
else 'failed'
|
||||
if value is False
|
||||
else 'skipped'
|
||||
)
|
||||
if outcome == 'succeeded' and edited_content is not None:
|
||||
from langbot_plugin.api.entities.builtin.platform.message import Plain
|
||||
|
||||
if any(not isinstance(item, Plain) for item in edited_content):
|
||||
outcome = 'partial'
|
||||
# One unacknowledged component must not be hidden by a later success.
|
||||
prior = span.adapter_api_result
|
||||
span.adapter_api_result = (
|
||||
'failed'
|
||||
if 'failed' in (prior, outcome)
|
||||
else 'skipped'
|
||||
if 'skipped' in (prior, outcome)
|
||||
else 'partial'
|
||||
if 'partial' in (prior, outcome)
|
||||
else outcome
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
return value
|
||||
|
||||
Reference in New Issue
Block a user