ForcePilot/backend/package/yuxi/channels/adapters/nostr/setup.py
Kris a1f8288d20 feat(nostr): 实现完整的 Nostr 协议适配器模块
该提交新增了完整的 Nostr 去中心化社交网络适配器实现,包含以下核心功能:
1. 基础加密与密钥处理:支持 nsec/npub/hex 格式密钥转换,实现 NIP-04/NIP-17 加解密
2. Relay 管理与健康监控:支持多 Relay 连接、自动重连、健康评分与自动选优
3. 事件与消息处理:实现事件校验、去重、速率限制、消息缓存与状态持久化
4. 个人资料管理:支持发布/导入/合并 Nostr Kind 0 元数据事件
5. 配对机制:实现安全的双向配对通信流程
6. Zap 功能:支持 NIP-57 打赏请求与收据解析
7. NIP-05 验证:实现域名身份验证
8. 配置系统:完整的配置校验与多账户支持
9. 监控与指标:提供连接监控、事件统计与健康快照
10. HTTP API:提供个人资料管理的 RESTful 接口
11. 安装向导:命令行配置向导与初始化流程

所有模块均遵循 Nostr 协议规范,支持多账户、多 Relay 部署,内置安全防护与流量控制机制。
2026-05-12 00:47:41 +08:00

277 lines
11 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.

from __future__ import annotations
from yuxi.channels.adapters.nostr.crypto import NostrCrypto, NostrCryptoError
from yuxi.channels.adapters.nostr.probe import probe_relay
from yuxi.utils.logging_config import logger
import asyncio
class NostrSetupAdapter:
def __init__(self, adapter=None):
self._adapter = adapter
def get_setup_steps(self) -> list[dict]:
return [
{
"step": 1,
"title": "生成或输入私钥",
"description": "Nostr 私钥用于签名和加密。可以留空自动生成,或输入现有的 nsec/hex 格式私钥",
"field_key": "private_key",
"required": False,
"help": "支持 nsec1... 或 64 位 hex 格式",
},
{
"step": 2,
"title": "配置 Relay 服务器",
"description": "Relay 是中继服务器,用于收发 Nostr 消息。支持 ws:// 和 wss:// 协议",
"field_key": "relays",
"required": False,
"default": [
"wss://relay.damus.io",
"wss://relay.primal.net",
"wss://relay.nostr.info",
"wss://nos.lol",
],
"help": "多个 Relay URL 以逗号分隔",
},
{
"step": 3,
"title": "选择 DM 策略",
"description": "控制谁可以向你的 Nostr Bot 发送私信",
"field_key": "dm_policy",
"required": True,
"default": "pairing",
"options": ["pairing", "open", "whitelist", "disabled"],
"help": "pairing=配对认证 | open=所有人 | whitelist=白名单 | disabled=禁用",
},
{
"step": 4,
"title": "配置高级选项",
"description": "配置 NIP-17 加密、流式输出模式、Markdown 表格转换等高级选项",
"field_key": "advanced",
"required": False,
"default": {
"nip17_enabled": True,
"streaming_mode": "block",
"markdown_table_mode": "off",
"nip42_auth_enabled": False,
},
},
{
"step": 5,
"title": "探测 Relay 连接",
"description": "验证 Relay 服务器是否可达",
"action": "probe_relays",
"field_key": "relays",
},
{
"step": 6,
"title": "验证私钥",
"description": "验证私钥格式并获取对应的 npub",
"action": "verify_key",
"field_key": "private_key",
},
]
async def probe_relays(self, relays: list[str]) -> dict:
probe_tasks = [probe_relay(url, timeout=8.0) for url in relays]
results = await asyncio.gather(*probe_tasks, return_exceptions=True)
reachable = 0
total = len(relays)
details: list[dict] = []
for result in results:
if isinstance(result, Exception):
details.append({"error": str(result)})
continue
entry = {
"url": result.url,
"connected": result.connected,
"latency_ms": round(result.latency_ms, 1) if result.latency_ms else None,
}
if result.connected:
reachable += 1
if result.error:
entry["error"] = result.error[:120]
details.append(entry)
return {
"success": reachable > 0,
"reachable": reachable,
"total": total,
"details": details,
}
async def verify_key(self, private_key: str) -> dict:
if not private_key:
crypto = NostrCrypto(None)
return {
"success": True,
"auto_generated": True,
"npub": crypto.npub,
"hex": crypto.pubkey_hex(),
"nsec": crypto.nsec(),
}
try:
crypto = NostrCrypto(private_key)
return {
"success": True,
"npub": crypto.npub,
"hex": crypto.pubkey_hex(),
}
except NostrCryptoError as e:
return {"success": False, "error": str(e)}
async def run_interactive_setup(self) -> dict:
logger.info("[Nostr Setup] Starting interactive setup wizard")
steps = self.get_setup_steps()
return {
"status": "setup_required",
"channel": "nostr",
"steps": steps,
"help": "详细文档请参考: /docs/channels/nostr",
}
def generate_config_schema(self) -> dict:
return {
"private_key": {
"type": "str",
"required": False,
"label": "私钥 (nsec/hex)",
"description": "Nostr 私钥 (nsec/hex),留空自动生成",
"placeholder": "nsec1... 或 64 位 hex 格式",
"help": "建议使用 nsec1 格式。留空则自动生成一个新的临时密钥",
},
"relays": {
"type": "list[str]",
"required": False,
"label": "Relay 中继服务器",
"description": "Relay 中继服务器 URL 列表 (ws:// 或 wss://)",
"placeholder": "wss://relay.damus.io",
"help": "至少配置 1 个 Relay 用于收发消息。建议使用 wss:// 加密连接",
"default": [
"wss://relay.damus.io",
"wss://relay.primal.net",
"wss://relay.nostr.info",
"wss://nos.lol",
],
},
"dm_policy": {
"type": "str",
"required": False,
"default": "pairing",
"label": "DM 策略",
"description": "控制谁可以向您的 Nostr Bot 发送私信",
"help": "pairing=配对认证 | open=所有人 | whitelist=白名单 | disabled=禁用 DM",
"enum": ["pairing", "open", "whitelist", "disabled"],
},
"nip17_enabled": {
"type": "bool",
"required": False,
"default": True,
"label": "NIP-17 Gift Wrap 加密",
"description": "启用 NIP-17 Gift Wrap 加密(推荐)",
"help": "NIP-17 提供更好的隐私保护,建议开启",
},
"streaming_mode": {
"type": "str",
"required": False,
"default": "block",
"label": "流式输出模式",
"description": "控制消息的流式输出方式",
"help": "off=关闭流式 | block=分段输出 | progress=进度式输出",
"enum": ["off", "block", "progress"],
},
"markdown_table_mode": {
"type": "str",
"required": False,
"default": "off",
"label": "Markdown 表格转换",
"description": "将 Markdown 表格转换为适合 Nostr 的格式",
"help": "Nostr 客户端通常不支持原生的 Markdown 表格渲染",
"enum": ["off", "convert"],
},
"nip42_auth_enabled": {
"type": "bool",
"required": False,
"default": False,
"label": "NIP-42 AUTH 认证",
"description": "启用 Relay AUTH 认证 (NIP-42)",
"help": "需要 Relay 支持 NIP-42 才能生效",
},
"allow_from": {
"type": "list[str]",
"required": False,
"default": [],
"label": "DM 白名单",
"description": "DM 白名单 (npub/hex/pubkey)",
"placeholder": "npub1...",
"help": "当 DM 策略为 whitelist 时,只有此列表中的 pubkey 能发消息",
},
"profile": {
"type": "dict",
"required": False,
"label": "Bot Profile",
"description": "Nostr Kind 0 元数据 Profile",
"schema": {
"name": {
"type": "str",
"default": "",
"label": "用户名",
"description": "Nostr 用户名",
"placeholder": "my_nostr_bot",
},
"display_name": {
"type": "str",
"default": "",
"label": "显示名称",
"description": "Nostr 显示名称",
"placeholder": "My Nostr Bot",
},
"about": {
"type": "str",
"default": "",
"label": "简介",
"description": "Bot 简介",
"placeholder": "A Nostr bot powered by ForcePilot",
},
"picture": {
"type": "str",
"default": "",
"label": "头像 URL",
"description": "头像图片 URL需要 https",
"placeholder": "https://example.com/avatar.png",
},
"banner": {
"type": "str",
"default": "",
"label": "横幅图 URL",
"description": "Profile 横幅图 URL需要 https",
"placeholder": "https://example.com/banner.png",
},
"website": {
"type": "str",
"default": "",
"label": "个人网站",
"description": "个人/项目网站 URL",
"placeholder": "https://example.com",
},
"nip05": {
"type": "str",
"default": "",
"label": "NIP-05 标识",
"description": "NIP-05 验证标识 (name@domain)",
"placeholder": "bot@example.com",
},
"lud16": {
"type": "str",
"default": "",
"label": "Lightning Address",
"description": "Lightning 地址用于接收 Zap 打赏",
"placeholder": "bot@getalby.com",
},
},
},
}