新增 Google Chat 对接的全套工具模块,包括: - 会话线程管理、消息解析与格式化 - Pub/Sub 消息解码、提及和命令识别 - 权限审批、目录管理和审计日志 - 消息缓存、媒体上传下载和 SSFR 防护 - 策略配置、卡片构建和流式回复支持
165 lines
5.6 KiB
Python
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))
|