ForcePilot/backend/package/yuxi/channel/extensions/farcaster/config.py
Kris 7c6186e9a4 feat(farcaster): 新增Farcaster去中心化社交协议通道插件
该提交完整实现了ForcePilot的Farcaster通道插件,包含:
1. 基础配置适配器与多账户支持
2. Neynar API客户端封装,带重试机制与签名验证
3. 消息去重处理模块
4. 网关服务与健康检查、轮询降级逻辑
5. Webhook回调端点与事件解析
6. 收发消息、 reactions、媒体发送等完整交互能力
7. 插件元数据与系统集成适配
2026-05-21 10:46:17 +08:00

173 lines
6.7 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.

import logging
import os
from pathlib import Path
logger = logging.getLogger(__name__)
ENV_NEYNAR_API_KEY = "NEYNAR_API_KEY"
ENV_NEYNAR_SIGNER_UUID = "NEYNAR_SIGNER_UUID"
class FarcasterConfigAdapter:
def __init__(self):
self._config: dict = {}
def update_config(self, config: dict) -> None:
self._config = config
def list_account_ids(self, config: dict) -> list[str]:
self._config = config
fc_cfg = self._resolve_fc_config(config)
accounts = fc_cfg.get("accounts", {}) if isinstance(fc_cfg, dict) else {}
if accounts:
return list(accounts.keys())
return ["default"]
async def resolve_account(self, account_id: str) -> dict:
fc_cfg = self._get_farcaster_config()
accounts = fc_cfg.get("accounts", {}) if isinstance(fc_cfg, dict) else {}
raw = accounts.get(account_id, {}) if isinstance(accounts, dict) else {}
base = {k: v for k, v in fc_cfg.items() if k != "accounts"}
api_key = (
raw.get("api_key")
or base.get("api_key", "")
or _read_file(raw.get("api_key_file", "") or base.get("api_key_file", ""))
or os.environ.get(ENV_NEYNAR_API_KEY, "")
)
signer_uuid = (
raw.get("signer_uuid") or base.get("signer_uuid", "") or os.environ.get(ENV_NEYNAR_SIGNER_UUID, "")
)
return {
"account_id": account_id,
"name": raw.get("name", account_id),
"enabled": raw.get("enabled", base.get("enabled", True)),
"configured": bool(api_key),
"api_key": api_key,
"signer_uuid": signer_uuid,
"fid": raw.get("fid", base.get("fid", 0)),
"fname": raw.get("fname", base.get("fname", "")),
"dm_policy": raw.get("dm_policy", base.get("dm_policy", "pairing")),
"allow_from": raw.get("allow_from", base.get("allow_from", [])),
"webhook_base_url": raw.get("webhook_base_url", base.get("webhook_base_url", "")),
"channel_id": raw.get("channel_id", base.get("channel_id", "")),
}
def is_configured(self, account: dict) -> bool:
return bool(account.get("api_key"))
def is_enabled(self, account: dict) -> bool:
return account.get("enabled", True)
def disabled_reason(self, account: dict) -> str:
if not account.get("api_key"):
return "Neynar API Key is required"
if not account.get("signer_uuid"):
return "Signer UUID is required for write operations"
return ""
def describe_account(self, account: dict) -> dict:
return {
"account_id": account.get("account_id", ""),
"name": account.get("name", ""),
"configured": account.get("configured", False),
"fname": account.get("fname", ""),
}
def config_schema(self) -> dict:
return {
"type": "object",
"properties": {
"enabled": {"type": "boolean", "default": True, "title": "启用"},
"api_key": {
"type": "string",
"title": "Neynar API Key",
"description": "从 dev.neynar.com 获取的 API Key",
"sensitive": True,
},
"signer_uuid": {
"type": "string",
"title": "Signer UUID",
"description": "Neynar 托管签名的 UUID写操作必需",
"sensitive": True,
},
"fid": {
"type": "integer",
"title": "Bot FID",
"description": "Bot 的 Farcaster FID",
},
"fname": {
"type": "string",
"title": "Bot Username",
"description": "Bot 的 Farcaster 用户名(用于 @提及匹配)",
},
"channel_id": {
"type": "string",
"title": "默认频道 ID",
"description": "Farcaster Channel ID所有公开 Cast 默认发布到此频道",
},
"dm_policy": {
"type": "string",
"enum": ["pairing", "allowlist", "open", "disabled"],
"default": "pairing",
"title": "DM 安全策略",
"description": "pairing=配对码模式, allowlist=白名单, open=开放, disabled=禁用",
},
"allow_from": {
"type": "array",
"items": {"type": "string"},
"title": "允许列表",
"description": 'FID 白名单(如 ["1234", "5678"])',
},
"webhook_base_url": {
"type": "string",
"title": "Webhook 回调地址",
"description": "Neynar Webhook 推送的公网地址(如 https://forcepilot.example.com",
},
"default_account": {"type": "string", "default": "default"},
"accounts": {
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"name": {"type": "string"},
"enabled": {"type": "boolean", "default": True},
"api_key": {"type": "string", "sensitive": True},
"signer_uuid": {"type": "string", "sensitive": True},
"fid": {"type": "integer"},
"fname": {"type": "string"},
"channel_id": {"type": "string"},
"dm_policy": {
"type": "string",
"enum": ["pairing", "allowlist", "open", "disabled"],
},
"allow_from": {"type": "array", "items": {"type": "string"}},
"webhook_base_url": {"type": "string"},
},
},
},
},
}
@staticmethod
def _resolve_fc_config(config: dict) -> dict:
channels = config.get("channels", {})
fc_cfg = channels.get("farcaster", {})
return fc_cfg if isinstance(fc_cfg, dict) else {}
def _get_farcaster_config(self) -> dict:
return self._resolve_fc_config(self._config)
def _read_file(filepath: str) -> str:
if not filepath:
return ""
try:
path = Path(filepath)
if path.exists():
return path.read_text(encoding="utf-8").strip()
except OSError:
pass
return ""