ForcePilot/backend/package/yuxi/channels/adapters/googlechat/setup.py
Kris 87fc26cd15 refactor(googlechat): 整理并优化Google Chat适配器代码
- 调整依赖导入顺序与清理冗余空行
- 修复setup wizard步骤存储逻辑
- 新增消息编辑/转发标记、卡片控件支持
- 完善目标校验、媒体上传、配对存储功能
- 优化流式响应与错误处理逻辑
- 增强SSRF防护与配置灵活性
- 新增/扩展内置命令与卡片处理能力
- 调整媒体大小限制与类型参数
2026-05-13 16:09:31 +08:00

338 lines
13 KiB
Python

from __future__ import annotations
from typing import Any
_SETUP_GUIDE = """
Google Chat Setup Guide
=======================
1. Go to Google Cloud Console → APIs & Services → Enable APIs
Enable the "Google Chat API"
2. Go to IAM & Admin → Service Accounts
Create a new service account or use an existing one
Generate a JSON key and download it
3. Go to Google Chat API → Configuration
Set up the bot:
- App name: Your bot name
- Avatar URL: (optional)
- Description: Your bot description
- Functionality: Enable "Receive messages" and "Join spaces and group conversations"
- Connection settings:
* App URL: https://your-domain.com/api/webhook/googlechat
* (Or use Pub/Sub for production)
4. Configure the adapter with your service account credentials
using one of the supported methods (file, inline, or environment variable).
5. Optional: Set audienceType to "project-number" if using project-number auth,
and configure the audience accordingly.
6. Configure security policies:
- dmPolicy: "open" | "pairing" | "allowlist" | "disabled"
- groupPolicy: "open" | "allowlist" | "disabled"
- allowFrom: list of users/spaces allowed to interact
- requireMention: require @mention in group messages
"""
class SetupWizard:
def __init__(self, adapter=None):
self._adapter = adapter
self._steps: list[dict[str, Any]] = []
self._current_step = 0
self._results: dict[str, Any] = {}
@property
def current_step(self) -> dict[str, Any] | None:
if 0 <= self._current_step < len(self._steps):
return self._steps[self._current_step]
return None
def start(self) -> dict[str, Any]:
self._results = {}
self._current_step = 0
self._steps = [
self._step_credential_mode(),
self._step_credential_input(),
self._step_audience_config(),
self._step_policy_config(),
self._step_review(),
]
return {"step": self._current_step, "steps": len(self._steps), "data": self.current_step}
def next(self, answer: Any = None) -> dict[str, Any]:
if answer is not None:
self._results[self._steps[self._current_step]["id"]] = answer
self._current_step += 1
if self._current_step >= len(self._steps):
return {"step": self._current_step, "done": True, "config": self._build_config()}
return {"step": self._current_step, "steps": len(self._steps), "data": self.current_step}
def _step_credential_mode(self) -> dict[str, Any]:
return {
"id": "credential_mode",
"question": "How would you like to provide service account credentials?",
"type": "select",
"options": [
{
"id": "env_file",
"label": "Environment (GOOGLE_SERVICE_ACCOUNT_FILE)",
"description": (
"Set the path to a service account JSON key file "
"via the GOOGLE_SERVICE_ACCOUNT_FILE environment variable"
),
},
{
"id": "env_json",
"label": "Environment (GOOGLE_CHAT_SERVICE_ACCOUNT)",
"description": (
"Paste the full service account JSON content "
"into the GOOGLE_CHAT_SERVICE_ACCOUNT environment variable"
),
},
{
"id": "file",
"label": "File (serviceAccountFile)",
"description": "Specify the path to a service account JSON key file on disk in the config",
},
{
"id": "inline",
"label": "Inline (serviceAccount)",
"description": "Paste the full service account JSON content directly into the config",
},
{
"id": "secret_ref",
"label": "Secret Reference (serviceAccountRef)",
"description": "Reference a secret via the secret management system",
},
],
}
def _step_credential_input(self) -> dict[str, Any]:
mode = self._results.get("credential_mode", "")
if mode == "file":
return {
"id": "credential_value",
"question": "Please provide the path to your Google Cloud service account JSON key file:",
"type": "text",
"required": True,
"hint": "e.g., /etc/secrets/google-service-account.json or ~/.config/gcp-sa-key.json",
}
if mode == "inline":
return {
"id": "credential_value",
"question": "Please paste the full content of your Google Cloud service account JSON key:",
"type": "text",
"required": True,
"hint": (
"This file can be downloaded from Google Cloud Console "
"→ IAM & Admin → Service Accounts → Create Key → JSON"
),
}
if mode == "secret_ref":
return {
"id": "credential_value",
"question": "Please provide the secret reference identifier:",
"type": "text",
"required": True,
"hint": "e.g., myapp/gcp/googlechat-sa-key",
}
return {
"id": "credential_value",
"question": (
"Credential will be read from environment variables. "
"Ensure GOOGLE_CHAT_SERVICE_ACCOUNT or "
"GOOGLE_SERVICE_ACCOUNT_FILE is set."
),
"type": "info",
"required": False,
}
def _step_audience_config(self) -> dict[str, Any]:
return {
"id": "audience_config",
"question": "Configure audience validation for webhook JWT:",
"type": "form",
"fields": [
{
"id": "audience_type",
"question": "Audience type:",
"type": "select",
"options": [
{"id": "app-url", "label": "App URL (recommended)", "description": "Match the app's HTTPS URL"},
{
"id": "project-number",
"label": "Project Number",
"description": "Match the GCP project number",
},
],
"default": "app-url",
},
{
"id": "audience",
"question": "Audience value:",
"type": "text",
"hint": "For app-url: your HTTPS app URL. For project-number: your GCP project number.",
},
],
}
def _step_policy_config(self) -> dict[str, Any]:
return {
"id": "policy_config",
"question": "Configure access policies:",
"type": "form",
"fields": [
{
"id": "dm_policy",
"question": "DM Policy:",
"type": "select",
"options": [
{"id": "open", "label": "Open", "description": "Any user can DM the bot"},
{"id": "pairing", "label": "Pairing", "description": "Users must complete a pairing challenge"},
{"id": "allowlist", "label": "Allowlist", "description": "Only users in allowFrom can DM"},
{"id": "disabled", "label": "Disabled", "description": "No DM access"},
],
"default": "open",
},
{
"id": "group_policy",
"question": "Group Policy:",
"type": "select",
"options": [
{"id": "open", "label": "Open", "description": "Bot responds in all groups"},
{
"id": "allowlist",
"label": "Allowlist",
"description": "Only spaces in groupAllowFrom are active",
},
{"id": "disabled", "label": "Disabled", "description": "No group access"},
],
"default": "open",
},
{
"id": "require_mention",
"question": "Require @mention in groups?",
"type": "boolean",
"default": False,
},
],
}
def _step_review(self) -> dict[str, Any]:
return {
"id": "review",
"question": "Review configuration before saving:",
"type": "review",
"summary": self._build_config(),
}
def _build_config(self) -> dict[str, Any]:
config: dict[str, Any] = {}
mode = self._results.get("credential_mode", "")
credential_value = self._results.get("credential_value", "")
if mode == "inline" and credential_value:
config["serviceAccount"] = credential_value
elif mode == "file" and credential_value:
config["serviceAccountFile"] = credential_value
elif mode == "secret_ref" and credential_value:
config["serviceAccountRef"] = credential_value
audience = self._results.get("audience_config", {})
if isinstance(audience, dict):
if audience.get("audience_type"):
config["audienceType"] = audience["audience_type"]
if audience.get("audience"):
config["audience"] = audience["audience"]
policy = self._results.get("policy_config", {})
if isinstance(policy, dict):
if policy.get("dm_policy"):
config["dmPolicy"] = policy["dm_policy"]
if policy.get("group_policy"):
config["groupPolicy"] = policy["group_policy"]
if policy.get("require_mention") is not None:
config["requireMention"] = policy["require_mention"]
config["enabled"] = True
return config
def wizard_prompt_credential_mode() -> dict[str, Any]:
return {
"step": "select_credential_mode",
"question": "How would you like to provide service account credentials?",
"options": [
{
"id": "env_file",
"label": "Environment (GOOGLE_SERVICE_ACCOUNT_FILE)",
"description": (
"Set the path to a service account JSON key file "
"via the GOOGLE_SERVICE_ACCOUNT_FILE environment variable"
),
},
{
"id": "env_json",
"label": "Environment (GOOGLE_CHAT_SERVICE_ACCOUNT)",
"description": (
"Paste the full service account JSON content "
"into the GOOGLE_CHAT_SERVICE_ACCOUNT environment variable"
),
},
{
"id": "file",
"label": "File (serviceAccountFile)",
"description": "Specify the path to a service account JSON key file on disk",
},
{
"id": "inline",
"label": "Inline (serviceAccount)",
"description": "Paste the full service account JSON content directly into the config",
},
],
}
def wizard_prompt_file_path() -> dict[str, Any]:
return {
"step": "provide_credential_path",
"question": "Please provide the path to your Google Cloud service account JSON key file:",
"required": True,
}
def wizard_prompt_service_account_json() -> dict[str, Any]:
return {
"step": "provide_credential_json",
"question": (
"Please paste the full content of your Google Cloud service account "
"JSON key (or set the environment variable directly):"
),
"required": True,
"hint": (
"This file can be downloaded from Google Cloud Console → IAM & Admin → Service Accounts → Create Key → JSON"
),
}
def wizard_validate_service_account(cred_json: dict) -> dict[str, Any]:
errors: list[str] = []
if cred_json.get("type") != "service_account":
errors.append("Invalid credential type: expected 'service_account'")
if not cred_json.get("private_key"):
errors.append("Missing 'private_key' field")
if not cred_json.get("client_email"):
errors.append("Missing 'client_email' field")
if not cred_json.get("token_uri"):
errors.append("Missing 'token_uri' field")
return {"valid": len(errors) == 0, "errors": errors}
def get_setup_guide() -> str:
return _SETUP_GUIDE.strip()