ForcePilot/backend/package/yuxi/channels/adapters/nostr/setup_wizard.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

140 lines
5.4 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
import asyncio
import os
from typing import Any
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
async def run_setup_wizard(config_dir: str | None = None) -> dict[str, Any]:
logger.info("[Nostr] Starting setup wizard")
accounts: dict[str, dict[str, Any]] = {}
account_counter = 0
while True:
if account_counter == 0:
account_name = "default"
else:
account_name = input(f"\n账户名称 (默认: account_{account_counter}): ").strip()
if not account_name:
account_name = f"account_{account_counter}"
if account_name in accounts:
logger.warning(f"账户 '{account_name}' 已存在,请使用不同名称")
continue
account_config = await _configure_account(account_name, account_counter)
if account_config is None:
if account_counter == 0:
return {"status": "cancelled", "message": "初始帐户配置已取消"}
continue
accounts[account_name] = account_config
account_counter += 1
if account_counter > 0:
more = input("\n添加其他账户? [y/n] (默认: n): ").strip().lower()
if more != "y":
break
config: dict[str, Any] = {"accounts": accounts}
primary = accounts.get("default", list(accounts.values())[0])
primary_crypto = NostrCrypto(primary.get("private_key", ""))
npub = primary_crypto.npub
logger.info("[Nostr] 设置向导完成: %s 个账户", len(accounts))
return {"status": "ok", "npub": npub, "config": config, "account_count": len(accounts)}
async def _configure_account(account_name: str, index: int) -> dict[str, Any] | None:
logger.info("[Nostr] 配置账户: %s", account_name)
env_key = os.environ.get("NOSTR_PRIVATE_KEY", "")
if env_key and index == 0:
use_env = input("\n检测到环境变量 NOSTR_PRIVATE_KEY。是否使用? [y/n] (默认: y): ").strip().lower()
if use_env != "n":
try:
crypto = NostrCrypto(env_key)
logger.info(f"[Nostr] 使用环境变量密钥: npub={crypto.npub}")
private_key = env_key
skip_key_input = True
except NostrCryptoError as e:
logger.warning(f"[Nostr] 环境变量密钥无效: {e},将跳转到手动输入")
skip_key_input = False
else:
skip_key_input = False
else:
skip_key_input = False
if not skip_key_input:
private_key = input("\nNostr 私钥 (nsec/hex留空自动生成): ").strip()
else:
private_key = env_key
try:
crypto = NostrCrypto(private_key if private_key else None)
private_key = crypto.nsec()
logger.info(f"[Nostr] 密钥就绪: npub={crypto.npub}")
except NostrCryptoError as e:
logger.error(f"[Nostr] 私钥无效: {e}")
return None
relays_input = input("Relay URL 列表 (逗号分隔,默认: damus.io, primal.net, nostr.info, nos.lol): ").strip()
if relays_input:
relays = [r.strip() for r in relays_input.split(",") if r.strip()]
else:
relays = [
"wss://relay.damus.io",
"wss://relay.primal.net",
"wss://relay.nostr.info",
"wss://nos.lol",
]
dm_policy = input("DM 策略 [pairing/open/whitelist/disabled] (默认: pairing): ").strip().lower()
dm_policy = dm_policy if dm_policy in ("pairing", "open", "whitelist", "disabled") else "pairing"
nip17_input = input("启用 NIP-17 加密 [y/n] (默认: y): ").strip().lower()
nip17_enabled = nip17_input != "n"
streaming_mode = input("流式模式 [off/block/progress] (默认: block): ").strip().lower()
streaming_mode = streaming_mode if streaming_mode in ("off", "block", "progress") else "block"
markdown_tables = input("启用 Markdown 表格转换 [y/n] (默认: n): ").strip().lower()
markdown_table_mode = "convert" if markdown_tables == "y" else "off"
probe_input = input("是否探测 Relay 连接? [y/n] (默认: y): ").strip().lower()
if probe_input != "n":
logger.info("[Nostr] 正在探测 Relay 连接...")
probe_tasks = [probe_relay(url, timeout=8.0) for url in relays]
results = await asyncio.gather(*probe_tasks, return_exceptions=True)
reachable = 0
for result in results:
if isinstance(result, Exception):
continue
if result.connected:
reachable += 1
logger.info(f"{result.url} (延迟: {result.latency_ms:.1f}ms)")
else:
logger.warning(f"{result.url} - {result.error or '不可达'}")
if reachable == 0:
logger.warning("[Nostr] 所有 Relay 不可达,请检查网络和 URL")
else:
logger.info(f"[Nostr] {reachable}/{len(relays)} 个 Relay 可连接")
account_config: dict[str, Any] = {
"private_key": private_key,
"relays": relays,
"dm_policy": dm_policy,
"nip17_enabled": nip17_enabled,
"streaming_mode": streaming_mode,
"markdown_table_mode": markdown_table_mode,
}
if index > 0:
account_config["enabled"] = True
return account_config