ForcePilot/backend/package/yuxi/channels/adapters/googlechat/doctor.py
Kris ca07c593a8 feat(googlechat): 实现 Google Chat 适配器完整功能集
新增 Google Chat 对接的全套工具模块,包括:
- 会话线程管理、消息解析与格式化
- Pub/Sub 消息解码、提及和命令识别
- 权限审批、目录管理和审计日志
- 消息缓存、媒体上传下载和 SSFR 防护
- 策略配置、卡片构建和流式回复支持
2026-05-12 00:44:18 +08:00

165 lines
5.6 KiB
Python

from __future__ import annotations
import re
from typing import Any
def run_diagnostics(config: dict[str, Any] | None) -> list[dict[str, Any]]:
if not config:
return []
issues: list[dict[str, Any]] = []
issues.extend(_check_mutable_allowlist(config))
issues.extend(_check_deprecated_formats(config))
issues.extend(_check_legacy_stream_mode(config))
issues.extend(_check_credential_readiness(config))
return issues
def _check_mutable_allowlist(config: dict[str, Any]) -> list[dict[str, Any]]:
issues: list[dict[str, Any]] = []
allow_from = _normalize_allowlist(config.get("allowFrom", config.get("allow_from", [])))
for entry in allow_from:
if "@" in entry and not _is_users_prefix(entry):
issues.append(
{
"severity": "warning",
"category": "mutable_allowlist",
"field": "allowFrom",
"value": entry,
"message": (
f"allowFrom contains email '{entry}' without 'users/' prefix. "
"Email-based allowlists are mutable; consider migrating to 'users/<email>' format"
),
}
)
group_allow_from = _normalize_allowlist(config.get("groupAllowFrom", config.get("group_allow_from", [])))
for entry in group_allow_from:
if "@" in entry and not entry.startswith("spaces/"):
issues.append(
{
"severity": "warning",
"category": "mutable_allowlist",
"field": "groupAllowFrom",
"value": entry,
"message": (
f"groupAllowFrom contains email '{entry}'. "
"Group allowlists should use 'spaces/<id>' format as emails are mutable"
),
}
)
return issues
def _check_deprecated_formats(config: dict[str, Any]) -> list[dict[str, Any]]:
issues: list[dict[str, Any]] = []
allow_from = _normalize_allowlist(config.get("allowFrom", config.get("allow_from", [])))
for entry in allow_from:
if _is_users_prefix_with_email_legacy(entry):
issues.append(
{
"severity": "warning",
"category": "deprecated_format",
"field": "allowFrom",
"value": entry,
"message": (
f"allowFrom entry '{entry}' uses deprecated format. "
"Remove the trailing '/<email>' portion: use 'users/<userId>' instead"
),
}
)
if config.get("streamMode"):
issues.append(
{
"severity": "info",
"category": "deprecated_config",
"field": "streamMode",
"value": config["streamMode"],
"message": (
"streamMode config is deprecated; "
"streaming is configured via 'supports_streaming' capability"
),
}
)
if config.get("enableThreadReply"):
issues.append(
{
"severity": "info",
"category": "deprecated_config",
"field": "enableThreadReply",
"value": config["enableThreadReply"],
"message": "enableThreadReply is deprecated; use replyToMode instead",
}
)
return issues
def _check_legacy_stream_mode(config: dict[str, Any]) -> list[dict[str, Any]]:
issues: list[dict[str, Any]] = []
if config.get("streamMode"):
issues.append(
{
"severity": "info",
"category": "legacy_config",
"field": "streamMode",
"value": config["streamMode"],
"message": "Remove 'streamMode' config key; streaming is now controlled by capability flags",
}
)
return issues
def _check_credential_readiness(config: dict[str, Any]) -> list[dict[str, Any]]:
issues: list[dict[str, Any]] = []
has_inline = bool(config.get("service_account"))
has_file = bool(config.get("service_account_file"))
has_secret_ref = bool(config.get("serviceAccountRef", config.get("service_account_ref")))
has_env = bool(
__import__("os").getenv("GOOGLE_CHAT_SERVICE_ACCOUNT")
or __import__("os").getenv("GOOGLE_SERVICE_ACCOUNT_FILE")
or __import__("os").getenv("GOOGLE_CHAT_SERVICE_ACCOUNT_FILE")
)
if not (has_inline or has_file or has_secret_ref or has_env):
issues.append(
{
"severity": "error",
"category": "credential_readiness",
"field": "serviceAccount",
"message": (
"No Google Chat service account configured. "
"Provide one via service_account, service_account_file, "
"serviceAccountRef, or environment variables"
),
}
)
return issues
def _normalize_allowlist(raw: Any) -> list[str]:
if isinstance(raw, str):
return [x.strip() for x in raw.split(",") if x.strip()]
if isinstance(raw, (list, tuple)):
return [str(x).strip() for x in raw if x]
return []
def _is_users_prefix(entry: str) -> bool:
return entry.startswith("users/") or entry.startswith("user:")
def _is_users_prefix_with_email_legacy(entry: str) -> bool:
if not entry.startswith("users/"):
return False
inner = entry.removeprefix("users/")
return bool(re.match(r"^[\w.+-]+@[\w-]+\.[\w.-]+/", inner))