Pull Request已成功合入, 合并人@曹玉哲
(感谢 xiongxing 的贡献)变更摘要
此 PR 为 versatile_adapter 新增了 A2A 网关协议适配模式。核心是新增 VersatileA2AGateway 类(继承自 VersatileProxy),将 VA 内部统一入参转换为 A2A 1.0 JSON-RPC SendStreamingMessage 请求,解析 SSE 响应中的 taskArtifactUpdate / taskStatusUpdate 事件,并映射为 VA 统一的 AdapterEvent。通过环境变量 VA_WORKFLOW_ADAPTER_TYPE 可在网关模式(a2a_gateway)与直连低码工作流模式(workflow)之间切换。
主要改动
- 新增
VersatileA2AGateway适配器:实现_build_url、_build_headers、_build_request_body、_process_chunk、_on_stream_end等钩子方法,完成 A2A 1.0 协议请求构造、SSE 响应解析(artifact 累积/状态终态处理),并将结果按低码工作流统一格式输出{"event":"message","data":{"text":"…"}}帧。 VersatileProxy基类增强:VersatileStreamCtx新增artifact_texts、last_artifact_id、input_required三个字段以支持 A2A 网关的 artifact 累积与状态跟踪;dispatch_stream签名扩展trace_id/agent_id参数并存储入口上下文;_log_request对token/authorization等敏感 header 脱敏。- 路由层支持
a2a_gateway类型切换:VersatileAdapterRunner读取环境变量VA_WORKFLOW_ADAPTER_TYPE(默认a2a_gateway),_match_workflow据此匹配对应类型的 adapter;新增_merge_a2a_gateway_defaults函数合并 YAML 中a2a_gateway_defaults默认配置。 - 配置与环境变量扩展:
Settings新增versatile_workflow_adapter_type字段(映射VA_WORKFLOW_ADAPTER_TYPE环境变量);YAML 配置示例新增a2a_gateway_defaults默认段和type: a2a_gateway的适配器条目,支持a2a_gateway_base、agent_card_name、token等专属字段。 - 日志与上下文透传增强:
response_wrapper.py和remote_agent_handler.py增加关键事件调试日志;A2aVersatileExecutor在runner_kw中透传trace_id和agent_id。


代码审查
审查总结
共审查 11 个变更文件,发现问题 3 个:
| 优先级 | 数量 | 说明 |
|---|---|---|
| P2 | 2 | _extract_text_from_status_message 缺少 parts 非列表防御;_on_stream_end 对空字符串 execution_result 真值误判导致不产出终态事件 |
| P3 | 1 | VersatileAdapterRunner 通过 os.getenv 直接读取路由模式,与 Pydantic Settings 不一致 |
逐文件审查结果:
applications/a2a_service/common/response_wrapper.py— 无问题(仅添加 debug 日志)applications/a2a_service/orchestrator/handlers/remote_agent_handler.py— 无问题(仅添加 info 日志)applications/versatile_adapter/.env.example— 无问题applications/versatile_adapter/a2a_facade/executor.py— 无问题(新增 trace_id/agent_id 透传 + 日志)applications/versatile_adapter/adapters/versatile_a2a_gateway.py— 2 个 P2 问题(见上方)applications/versatile_adapter/adapters/versatile_proxy.py— 无问题(签名扩展 + 敏感头脱敏均为改进)applications/versatile_adapter/config.py— 无问题applications/versatile_adapter/dispatcher/runner.py— 1 个 P3 问题(见上方)applications/versatile_adapter/tests/conftest.py— 无问题applications/versatile_adapter/tests/test_versatile_a2a_gateway.py— 无问题applications/versatile_adapter/versatile_proxy.yaml.example— 无问题
整体风险评估: 低至中等。两个 P2 问题均位于新增的 VersatileA2AGateway 适配器中,属于边界情况的防御缺失,不会在正常 A2A 协议交互中触发。核心主流程(A2A Gateway 正常 SSE 解析、路由匹配、事件映射)设计合理,测试覆盖充分。建议优先修复两个 P2 问题后再合入。
| 类型 | 数量 |
|---|---|
| 🔴 阻塞 | 0 |
| 🟡 建议 | 5 |
💬 仅评论


欢迎来到 openJiuwen 社区
Hey @xiongxing , 感谢你对社区的贡献.
机器人使用手册
有关指令的使用,可以点击 此处 查看详情。开发人员可以在每个PR或Issue下方评论特定指令来触发机器人任务。


🟡 Medium Priority
executor.py 第 77-81 行新增的 logger.info(...) 将完整的 runner_kw.get('headers', {}) 以 INFO 级别写入日志,未对 token、authorization、cookie 等敏感 header 做任何脱敏处理。body 字段有 [:200] 截断,但 headers 完全没有截断或脱敏。
对比 versatile_proxy.py 中 _log_request 使用 _SENSITIVE_HEADERS 集合对 curl 命令中的敏感 header 做 *** 替换,且请求头日志使用 DEBUG 级别。此处以 INFO 级别(生产环境通常开启)记录完整 headers,若上游 a2a_service 传入的 headers 中包含 [REDACTED]、session cookie 等,将直接写入日志文件。
风险:日志被非授权人员/系统访问时泄露凭据。
建议:将日志级别降为 DEBUG,或对 headers 做脱敏处理(过滤 token/authorization/cookie 等敏感 key),或只记录 header keys 而非 values。


🟡 Medium Priority
在 VersatileA2AGateway._extract_text_from_status_message 方法中(第 330-334 行),parts = message.get("parts", []) 获得的 parts 可能不是列表(例如上游返回 {"parts": {"unexpected": "dict"}}),后续的生成器表达式 for part in parts 将抛出 TypeError: 'dict' object is not iterable。
相比之下,_handle_artifact_update 方法(第 218 行)已对此做了防御:if not isinstance(parts, list): parts = []。同样的防护应在 _extract_text_from_status_message 中添加,保持一致性并避免运行时崩溃。
触发条件:A2A Gateway 返回的 taskStatusUpdate.message.parts 字段为非列表值(如 dict、字符串)。
建议:在迭代 parts 前增加 isinstance(parts, list) 检查,与 _handle_artifact_update 保持一致。
|
329 | + parts = message.get("parts", []) |
|
330 | + if not isinstance(parts, list): |
|
331 | + parts = [] |
| 329
332 | return "".join( |


🟡 Medium Priority
VersatileA2AGateway._on_stream_end 第 182 行使用真值判断 ctx.execution_result 来决定是否产出 execution_completed 终态事件。但 _handle_artifact_update 在处理非文本 parts(如纯文件 parts)时,仍会将空字符串 "" 存入 ctx.artifact_texts(第 230 行),并设置 ctx.last_artifact_id(第 231 行)。当 TASK_STATE_COMPLETED 到来时,_handle_status_update 会从 artifact_texts 中取出空字符串赋给 ctx.execution_result(第 259 行)。
此时 ctx.is_failed or ctx.execution_result → False or "" → ""(falsy),_on_stream_end 返回 [],不产出 execution_completed。executor 的 for 循环结束后触发兜底逻辑(executor.py 第 133 行),以 result=None 完成任务——丢失了原本可能从 status.message 降级提取到的有效文本。
触发条件:A2A Gateway 返回的 artifact 中 parts 只有非 text 类型(如 {"type":"file", ...}),同时 TASK_STATE_COMPLETED 的 message 中包含可用于降级提取的文本。
建议:将 ctx.execution_result 的真值检查改为 ctx.execution_result is not None,以区分"已设置空结果"与"未设置结果"两种语义。ctx.execution_result 初始为 None(VersatileStreamCtx 第 36 行),只有被明确赋值后才应视为有结果。
| 180
| - if ctx.is_failed or ctx.execution_result: |
|
180 | + if ctx.is_failed or ctx.execution_result is not None: |


🟡 Medium Priority
在 a2a_gateway_defaults 的 forward_header_whitelist(第 69 行)中包含了 x-b3-spanid。父类 VersatileProxy._build_headers(versatile_proxy.py 第 79 行)会按白名单原样转发该头(带原始 key x-b3-spanid 和原始值)。而子类 VersatileA2AGateway._build_headers(versatile_a2a_gateway.py 第 90 行)在 x-b3-parentspanid 存在时会通过 uuid.uuid4().hex[:16] 重新生成 X-B3-SpanId。HTTP 头部是大小写不敏感的(RFC 7230),因此最终请求中会同时存在 x-b3-spanid(转发的上游 span-id)和 X-B3-SpanId(新生成的 span-id),两个值不同,导致下游无法确定哪个是正确的 span-id,破坏分布式追踪语义。
证据链:
- YAML 第 69 行:
- x-b3-spanid在白名单中 - versatile_proxy.py 第 79 行:父类按原始 key 转发匹配白名单的 header
- versatile_a2a_gateway.py 第 90 行:子类生成新
X-B3-SpanId = uuid.uuid4().hex[:16] - 两者同时出现在同一次 HTTP 请求中,值不同,HTTP 大小写不敏感
修复方向:从 a2a_gateway_defaults.forward_header_whitelist 中移除 x-b3-spanid(span-id 应由网关重新生成,不应透传上游值)。
建议:从 a2a_gateway_defaults.forward_header_whitelist 中移除 - x-b3-spanid 这一行。SpanId 由 A2A Gateway 的 _build_headers 重新生成(versatile_a2a_gateway.py:90),不应透传上游的 span-id。


🟡 Medium Priority
_on_stream_end 第 182 行的条件 if ctx.is_failed or ctx.execution_result: 在 ctx.execution_result 为空字符串 "" 时求值为 False or "" → ""(falsy),导致整个方法返回空列表 []。
触发条件:A2A Gateway 返回 TASK_STATE_COMPLETED 但无 artifact 且 status message 也为空,或 artifact 累积的文本恰为空串。此时 ctx.completed=True、ctx.is_failed=False、ctx.execution_result=""。
后果:适配器层不产出任何终态事件,完全依赖 executor 的 if not terminal_sent 兜底逻辑发送 result=None 的 completed 事件。虽然 executor 有兜底,但适配器自身的契约被打破(completed 状态不产生事件),且当 executor 兜底逻辑未来变更时可能引入回归。
修复方向:在条件中增加对 ctx.completed 的判断,确保 completed 状态下始终产出 ExecutionCompletedContent 事件。
建议:将条件改为 if ctx.is_failed or ctx.execution_result or ctx.completed:,或在条件体内明确处理 completed-but-no-result 情况,确保产出 ExecutionCompletedContent(is_failed=False, result="", error_message="") 事件。


【openlibing.ci】识别到代码检查告警抑制注释,匹配工具:pylint,请Committer检视其合理性。


【openlibing.ci】识别到代码检查告警抑制注释,匹配工具:pylint,请Committer检视其合理性。


【openlibing.ci】识别到代码检查告警抑制注释,匹配工具:pylint,请Committer检视其合理性。


What type of PR is this?
/kind
Self-checklist:(请自检,在[ ]内打上x,我们将检视你的完成情况,否则会导致pr无法合入)