ForcePilot/backend/package/yuxi/external_systems/use_cases/mappers.py
Kris 74709ba2d3 feat: 完成外部系统限界上下文核心代码实现
新增六边形架构核心代码包,包含:
1. 协议适配器层:HTTP/SMTP/IMAP/SSH/GRPC等多协议适配器实现
2. 认证插件体系:基础认证、API密钥、HMAC等多类型认证插件
3. 执行编排框架:工具执行器、上下文构建、运行时治理组件
4. 用例端口与DTO:定义领域服务端口与数据传输对象
5. 厂商集成包框架:支持第三方系统集成扩展
6. 基础设施装配层:实现依赖注入与服务装配

所有代码遵循六边形架构设计原则,实现端口与适配器解耦,支持动态扩展与自动发现。
2026-06-20 22:12:42 +08:00

528 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""领域模型(dataclass) ↔ Output DTO(Pydantic) 转换 + 集中脱敏。
规范 §6.1/§7.3
- persistence 返回领域模型 dataclassservice 返回 Output DTO
- 脱敏集中在 mapper调 crypto_service.mask_sensitive_fields
- use_cases 层不调用 encrypt / decrypt
脱敏策略:
- dict 型敏感字段(``connection_config`` / ``auth_config`` / ``secret_refs`` /
``adapter_config``)调用 ``crypto_service.mask_sensitive_fields`` 递归脱敏
- 字符串型敏感字段(``access_token`` / ``refresh_token`` / ``secret``
非空时替换为 ``"***"``
审计字段(``created_by`` / ``updated_by``)由 service 在调用 mapper 前补充,
mapper 仅做字段映射。
"""
from __future__ import annotations
from yuxi.external_systems.core.models import (
AdapterAsset,
Alert,
AuditLog,
Environment,
Execution,
ExecutionTrace,
ExternalSystem,
ExternalTool,
NotificationChannel,
QuotaUsage,
SecretRotationPolicy,
SystemToken,
ToolAccessRule,
ToolTestCase,
WebhookEvent,
WebhookSubscription,
)
from yuxi.external_systems.framework.runtime import crypto_service
from yuxi.external_systems.use_cases.dto.access_rule import AccessRuleOutput
from yuxi.external_systems.use_cases.dto.alert import AlertOutput
from yuxi.external_systems.use_cases.dto.asset import AssetOutput
from yuxi.external_systems.use_cases.dto.audit_log import AuditLogOutput
from yuxi.external_systems.use_cases.dto.environment import EnvironmentOutput
from yuxi.external_systems.use_cases.dto.execution import (
ExecutionDetailOutput,
ExecutionOutput,
ExecutionTraceOutput,
)
from yuxi.external_systems.use_cases.dto.notification_channel import (
NotificationChannelOutput,
)
from yuxi.external_systems.use_cases.dto.quota import QuotaOutput
from yuxi.external_systems.use_cases.dto.secret_rotation import (
SecretRotationPolicyOutput,
)
from yuxi.external_systems.use_cases.dto.system import (
GovernanceConfigOutput,
SystemOutput,
)
from yuxi.external_systems.use_cases.dto.test_case import TestCaseOutput
from yuxi.external_systems.use_cases.dto.token import TokenOutput
from yuxi.external_systems.use_cases.dto.tool import ToolOutput
from yuxi.external_systems.use_cases.dto.webhook import (
WebhookEventOutput,
WebhookSubscriptionOutput,
)
# 敏感字符串字段的统一脱敏占位符
_MASKED_VALUE = "***"
# ═══════════════════════════════════════════════════════════════════════════
# 含敏感字段的子域mask: bool = True
# ═══════════════════════════════════════════════════════════════════════════
def to_system_output(system: ExternalSystem, *, mask: bool = True) -> SystemOutput:
"""ExternalSystem dataclass → SystemOutput DTO含脱敏
dict 型敏感字段:``connection_config`` / ``auth_config`` / ``secret_refs``。
"""
connection_config = system.connection_config
auth_config = system.auth_config
secret_refs = system.secret_refs
if mask:
connection_config = crypto_service.mask_sensitive_fields(connection_config)
auth_config = crypto_service.mask_sensitive_fields(auth_config)
secret_refs = crypto_service.mask_sensitive_fields(secret_refs)
return SystemOutput(
id=system.id,
slug=system.slug,
name=system.name,
description=system.description,
category=system.category,
adapter_type=system.adapter_type,
source_type=system.source_type,
enabled=system.enabled,
connection_config=connection_config,
auth_type=system.auth_type,
auth_config=auth_config,
secret_refs=secret_refs,
rate_limit=system.rate_limit,
circuit_breaker=system.circuit_breaker,
pool_config=system.pool_config,
observability=system.observability,
timeout=system.timeout,
retry_policy=system.retry_policy,
created_by=system.created_by,
updated_by=system.updated_by,
env_count=system.env_count,
)
def to_tool_output(tool: ExternalTool, *, mask: bool = True) -> ToolOutput:
"""ExternalTool dataclass → ToolOutput DTO含脱敏
dict 型敏感字段:``adapter_config`` / ``auth_config``。
"""
adapter_config = tool.adapter_config
auth_config = tool.auth_config
if mask:
adapter_config = crypto_service.mask_sensitive_fields(adapter_config)
auth_config = crypto_service.mask_sensitive_fields(auth_config)
return ToolOutput(
id=tool.id,
slug=tool.slug,
name=tool.name,
description=tool.description,
category=tool.category,
adapter_type=tool.adapter_type,
auth_type=tool.auth_type,
adapter_config=adapter_config,
auth_config=auth_config,
timeout=tool.timeout,
retry_policy=tool.retry_policy,
enabled=tool.enabled,
system_id=tool.system_id,
created_by=tool.created_by,
updated_by=tool.updated_by,
)
def to_environment_output(env: Environment, *, mask: bool = True) -> EnvironmentOutput:
"""Environment dataclass → EnvironmentOutput DTO含脱敏
dict 型敏感字段:``connection_config`` / ``auth_config``。
整合旧 ``environment_service._mask_secrets`` 的脱敏职责(非 decrypt 部分)。
"""
connection_config = env.connection_config
auth_config = env.auth_config
if mask:
connection_config = crypto_service.mask_sensitive_fields(connection_config)
auth_config = crypto_service.mask_sensitive_fields(auth_config)
return EnvironmentOutput(
id=env.id,
system_id=env.system_id,
env_key=env.env_key,
name=env.name,
connection_config=connection_config,
auth_config=auth_config,
is_default=env.is_default,
enabled=env.enabled,
)
def to_token_output(token: SystemToken, *, mask: bool = True) -> TokenOutput:
"""SystemToken dataclass → TokenOutput DTO含脱敏
字符串型敏感字段:``access_token`` / ``refresh_token``,非空时替换为 ``"***"``。
"""
access_token = token.access_token
refresh_token = token.refresh_token
if mask:
access_token = _MASKED_VALUE if access_token else access_token
refresh_token = _MASKED_VALUE if refresh_token else refresh_token
return TokenOutput(
id=token.id,
system_id=token.system_id,
env_key=token.env_key,
token_type=token.token_type,
access_token=access_token,
refresh_token=refresh_token,
expires_at=token.expires_at,
extra=token.extra,
is_invalidated=token.is_invalidated,
)
def to_webhook_subscription_output(
subscription: WebhookSubscription,
*,
mask: bool = True,
) -> WebhookSubscriptionOutput:
"""WebhookSubscription dataclass → WebhookSubscriptionOutput DTO含脱敏
字符串型敏感字段:``secret``,非空时替换为 ``"***"``。
"""
secret = subscription.secret
if mask:
secret = _MASKED_VALUE if secret else secret
return WebhookSubscriptionOutput(
id=subscription.id,
slug=subscription.slug,
name=subscription.name,
system_id=subscription.system_id,
source_event_type=subscription.source_event_type,
target_handler_type=subscription.target_handler_type,
callback_path=subscription.callback_path,
description=subscription.description,
env_key=subscription.env_key,
subscription_id=subscription.subscription_id,
target_handler_id=subscription.target_handler_id,
secret=secret,
secret_algorithm=subscription.secret_algorithm,
filter_rules=subscription.filter_rules,
transform_rules=subscription.transform_rules,
status=subscription.status,
enabled=subscription.enabled,
expires_at=subscription.expires_at,
auto_renew=subscription.auto_renew,
last_renewed_at=subscription.last_renewed_at,
last_renew_error=subscription.last_renew_error,
renewal_attempts=subscription.renewal_attempts,
last_received_at=subscription.last_received_at,
received_count=subscription.received_count,
retention_days=subscription.retention_days,
created_by=subscription.created_by,
updated_by=subscription.updated_by,
)
# ═══════════════════════════════════════════════════════════════════════════
# 无敏感字段的子域(直接字段映射)
# ═══════════════════════════════════════════════════════════════════════════
def to_webhook_event_output(event: WebhookEvent) -> WebhookEventOutput:
"""将 WebhookEvent dataclass 转换为 WebhookEventOutput DTO。"""
return WebhookEventOutput(
id=event.id,
subscription_id=event.subscription_id,
subscription_slug=event.subscription_slug,
external_event_id=event.external_event_id,
event_type=event.event_type,
payload=event.payload,
payload_hash=event.payload_hash,
headers=event.headers,
signature=event.signature,
signature_verified=event.signature_verified,
received_at=event.received_at,
source_ip=event.source_ip,
processing_status=event.processing_status,
processing_attempts=event.processing_attempts,
last_processed_at=event.last_processed_at,
processing_error=event.processing_error,
correlation_id=event.correlation_id,
triggered_handler_type=event.triggered_handler_type,
triggered_handler_id=event.triggered_handler_id,
triggered_execution_id=event.triggered_execution_id,
)
def to_execution_output(execution: Execution) -> ExecutionOutput:
"""Execution dataclass → ExecutionOutput DTO无敏感字段直接映射"""
return ExecutionOutput(
id=execution.id,
execution_id=execution.execution_id,
trace_id=execution.trace_id,
system_id=execution.system_id,
env_key=execution.env_key,
tool_slug=execution.tool_slug,
caller=execution.caller,
caller_id=execution.caller_id,
status=execution.status,
started_at=execution.started_at,
operation=execution.operation,
request_summary=execution.request_summary,
response_summary=execution.response_summary,
error=execution.error,
ended_at=execution.ended_at,
duration_ms=execution.duration_ms,
correlation_id=execution.correlation_id,
retry_of=execution.retry_of,
retry_count=execution.retry_count,
tags=execution.tags,
)
def to_execution_detail_output(
execution: Execution,
traces: list[ExecutionTrace],
) -> ExecutionDetailOutput:
"""Execution + traces → ExecutionDetailOutput DTO。
注意:当前 ``ExecutionDetailOutput`` 未包含 traces 字段,``traces`` 参数
预留用于后续扩展service 层可单独调用 ``to_execution_trace_output``
处理 traces。``system_slug`` / ``system_name`` 由 service 层补充。
"""
_ = traces # 预留参数,当前 DTO 未包含 traces 字段
return ExecutionDetailOutput(
execution=to_execution_output(execution),
)
def to_execution_trace_output(trace: ExecutionTrace) -> ExecutionTraceOutput:
"""ExecutionTrace dataclass → ExecutionTraceOutput DTO无敏感字段直接映射"""
return ExecutionTraceOutput(
id=trace.id,
execution_id=trace.execution_id,
trace_id=trace.trace_id,
spans=trace.spans,
total_duration_ms=trace.total_duration_ms,
)
def to_audit_log_output(audit_log: AuditLog) -> AuditLogOutput:
"""AuditLog dataclass → AuditLogOutput DTO无敏感字段直接映射"""
return AuditLogOutput(
id=audit_log.id,
system_id=audit_log.system_id,
env_key=audit_log.env_key,
event_type=audit_log.event_type,
action_type=audit_log.action_type,
resource=audit_log.resource,
user=audit_log.user,
detail=audit_log.detail,
snapshot_before=audit_log.snapshot_before,
snapshot_after=audit_log.snapshot_after,
description=audit_log.description,
created_at=audit_log.created_at,
)
def to_asset_output(asset: AdapterAsset) -> AssetOutput:
"""AdapterAsset dataclass → AssetOutput DTO不含二进制 content"""
return AssetOutput(
id=asset.id,
adapter_type=asset.adapter_type,
asset_type=asset.asset_type,
name=asset.name,
size=asset.size,
checksum=asset.checksum,
status=asset.status,
status_message=asset.status_message,
created_by=asset.created_by,
updated_by=asset.updated_by,
created_at=asset.created_at,
updated_at=asset.updated_at,
)
def to_quota_output(quota: QuotaUsage) -> QuotaOutput:
"""QuotaUsage dataclass → QuotaOutput DTO无敏感字段直接映射"""
return QuotaOutput(
id=quota.id,
system_id=quota.system_id,
quota_key=quota.quota_key,
quota_name=quota.quota_name,
quota_window=quota.quota_window,
env_key=quota.env_key,
window_start=quota.window_start,
window_end=quota.window_end,
limit_value=quota.limit_value,
used_value=quota.used_value,
remaining_value=quota.remaining_value,
usage_ratio=quota.usage_ratio,
is_warning=quota.is_warning,
is_critical=quota.is_critical,
unit=quota.unit,
source=quota.source,
source_header_name=quota.source_header_name,
warning_threshold=quota.warning_threshold,
critical_threshold=quota.critical_threshold,
last_updated_at=quota.last_updated_at,
last_response_at=quota.last_response_at,
)
def to_alert_output(alert: Alert) -> AlertOutput:
"""Alert dataclass → AlertOutput DTO无敏感字段直接映射"""
return AlertOutput(
id=alert.id,
system_id=alert.system_id,
alert_type=alert.alert_type,
title=alert.title,
triggered_at=alert.triggered_at,
env_key=alert.env_key,
severity=alert.severity,
status=alert.status,
description=alert.description,
detail=alert.detail,
resource_type=alert.resource_type,
resource_id=alert.resource_id,
related_execution_id=alert.related_execution_id,
related_trace_id=alert.related_trace_id,
metric_snapshot=alert.metric_snapshot,
dedup_key=alert.dedup_key,
resolved_at=alert.resolved_at,
acknowledged_by=alert.acknowledged_by,
acknowledged_at=alert.acknowledged_at,
resolution_note=alert.resolution_note,
notification_status=alert.notification_status,
notification_sent_at=alert.notification_sent_at,
notification_channels=alert.notification_channels,
)
def to_secret_rotation_policy_output(
policy: SecretRotationPolicy,
) -> SecretRotationPolicyOutput:
"""SecretRotationPolicy dataclass → SecretRotationPolicyOutput DTO无敏感字段直接映射
注意:``secret_ref`` 仅是引用 key指向 ``ExternalSystem.secret_refs`` 中的
具名 secret非敏感值本身不脱敏。
"""
return SecretRotationPolicyOutput(
id=policy.id,
system_id=policy.system_id,
secret_type=policy.secret_type,
secret_name=policy.secret_name,
secret_ref=policy.secret_ref,
env_key=policy.env_key,
rotation_mode=policy.rotation_mode,
rotation_interval_days=policy.rotation_interval_days,
notify_before_days=policy.notify_before_days,
notify_channels=policy.notify_channels,
last_rotated_at=policy.last_rotated_at,
next_rotation_at=policy.next_rotation_at,
last_rotation_at=policy.last_rotation_at,
last_rotation_status=policy.last_rotation_status,
last_rotation_error=policy.last_rotation_error,
rotation_count=policy.rotation_count,
max_rotation_retries=policy.max_rotation_retries,
status=policy.status,
)
def to_access_rule_output(rule: ToolAccessRule) -> AccessRuleOutput:
"""ToolAccessRule dataclass → AccessRuleOutput DTO无敏感字段直接映射"""
return AccessRuleOutput(
id=rule.id,
principal_type=rule.principal_type,
principal_id=rule.principal_id,
created_by=rule.created_by,
tool_slug=rule.tool_slug,
system_id=rule.system_id,
principal_name=rule.principal_name,
env_key=rule.env_key,
effect=rule.effect,
priority=rule.priority,
conditions=rule.conditions,
expires_at=rule.expires_at,
enabled=rule.enabled,
description=rule.description,
updated_by=rule.updated_by,
)
def to_test_case_output(test_case: ToolTestCase) -> TestCaseOutput:
"""ToolTestCase dataclass → TestCaseOutput DTO无敏感字段直接映射"""
return TestCaseOutput(
id=test_case.id,
tool_slug=test_case.tool_slug,
name=test_case.name,
request_fixture=test_case.request_fixture,
created_by=test_case.created_by,
description=test_case.description,
env_key=test_case.env_key,
expected_status=test_case.expected_status,
assertions=test_case.assertions,
timeout=test_case.timeout,
enabled=test_case.enabled,
schedule_cron=test_case.schedule_cron,
alert_on_failure=test_case.alert_on_failure,
alert_threshold=test_case.alert_threshold,
last_run_at=test_case.last_run_at,
last_run_status=test_case.last_run_status,
last_run_duration_ms=test_case.last_run_duration_ms,
last_run_response=test_case.last_run_response,
last_run_error=test_case.last_run_error,
last_run_assertions_result=test_case.last_run_assertions_result,
consecutive_failures=test_case.consecutive_failures,
updated_by=test_case.updated_by,
)
def to_notification_channel_output(
channel: NotificationChannel,
*,
mask: bool = True,
) -> NotificationChannelOutput:
"""NotificationChannel dataclass → NotificationChannelOutput DTO含脱敏
dict 型敏感字段:``config``(可能含 webhook URL、SMTP 密码等)。
"""
config = channel.config
if mask:
config = crypto_service.mask_sensitive_fields(config)
return NotificationChannelOutput(
id=channel.id,
channel_type=channel.channel_type,
name=channel.name,
config=config,
enabled=channel.enabled,
description=channel.description,
created_by=channel.created_by,
updated_by=channel.updated_by,
created_at=channel.created_at,
updated_at=channel.updated_at,
)
def to_secret_refs_governance_output(
system: ExternalSystem,
*,
mask: bool = True,
) -> GovernanceConfigOutput:
"""ExternalSystem.secret_refs → GovernanceConfigOutput DTO含脱敏
dict 型敏感字段:``secret_refs``,调用 ``crypto_service.mask_sensitive_fields``
递归脱敏。整合旧 ``system_service.get_secret_refs`` / ``update_secret_refs``
的脱敏职责,使 service 层不再直接调用 crypto_service。
"""
secret_refs = system.secret_refs
if mask:
secret_refs = crypto_service.mask_sensitive_fields(secret_refs)
return GovernanceConfigOutput(system_id=system.id, config=secret_refs)