ForcePilot/backend/package/yuxi/channels/adapters/signal/install.py
Kris 8dc86766f1 feat(channels/signal): 新增Signal渠道适配器完整实现
新增了完整的Signal渠道适配器实现,包含RPC客户端、守护进程管理、安全策略、消息处理、安装配置工具等全套功能,支持通过signal-cli与Signal网络进行通信,包含账户管理、消息收发、反应处理、媒体分析、健康检查等能力。
2026-05-12 00:48:25 +08:00

211 lines
7.2 KiB
Python

import logging
import os
import platform
import shutil
import subprocess
import tempfile
from urllib.request import urlopen, Request
import json
logger = logging.getLogger(__name__)
INSTALL_GUIDE_URL = "https://github.com/AsamK/signal-cli/releases"
GITHUB_API_RELEASES = "https://api.github.com/repos/AsamK/signal-cli/releases/latest"
DEFAULT_MAX_DOWNLOAD_MB = 256
def _get_platform_suffix() -> str | None:
system = platform.system().lower()
machine = platform.machine().lower()
if system == "linux" and machine in ("x86_64", "amd64"):
return "linux-x86_64"
if system == "linux" and machine in ("aarch64", "arm64"):
return "linux-aarch64"
return None
async def fetch_latest_release_url(prefer_native: bool = True) -> str | None:
try:
req = Request(GITHUB_API_RELEASES, headers={"User-Agent": "ForcePilot"})
with urlopen(req, timeout=15) as resp:
data = json.loads(resp.read().decode())
assets = data.get("assets", [])
native_suffix = _get_platform_suffix() if prefer_native else None
native_candidate = None
universal_candidate = None
for asset in assets:
name = asset.get("name", "")
if native_suffix and native_suffix in name.lower() and name.endswith(".tar.gz") and "signal-cli" in name:
native_candidate = asset.get("browser_download_url")
if name.endswith(".tar.gz") and "signal-cli" in name:
universal_candidate = asset.get("browser_download_url")
return native_candidate or universal_candidate
except Exception as e:
logger.warning(f"Failed to fetch latest signal-cli release: {e}")
return None
async def auto_install_signal_cli(
target_dir: str | None = None, max_download_mb: int = DEFAULT_MAX_DOWNLOAD_MB
) -> bool:
url = await fetch_latest_release_url()
if not url:
logger.error("Could not find signal-cli download URL")
return False
if target_dir is None:
target_dir = os.path.join(os.path.expanduser("~"), ".local", "signal-cli")
os.makedirs(target_dir, exist_ok=True)
logger.info(f"Downloading signal-cli from {url}")
try:
req = Request(url, headers={"User-Agent": "ForcePilot"})
with urlopen(req, timeout=300) as resp:
content_length = resp.headers.get("Content-Length")
if content_length:
size_mb = int(content_length) / (1024 * 1024)
if size_mb > max_download_mb:
max_size_bytes = max_download_mb * 1024 * 1024
raise ValueError(
f"signal-cli download size ({size_mb:.1f} MB) exceeds limit ({max_download_mb} MB)."
)
data = resp.read()
if len(data) > max_download_mb * 1024 * 1024:
raise ValueError(
f"signal-cli download size ({len(data) / (1024 * 1024):.1f} MB) exceeds limit ({max_download_mb} MB)."
)
except Exception as e:
logger.error(f"Failed to download signal-cli: {e}")
return False
with tempfile.NamedTemporaryFile(suffix=".tar.gz", delete=False) as tmp:
tmp.write(data)
tarball = tmp.name
try:
import tarfile
with tarfile.open(tarball, "r:gz") as tar:
tar.extractall(target_dir)
logger.info(f"signal-cli installed to {target_dir}")
return True
except Exception as e:
logger.error(f"Failed to extract signal-cli: {e}")
return False
finally:
try:
os.unlink(tarball)
except OSError:
pass
def check_signal_cli_installed(cli_path: str = "signal-cli") -> tuple[bool, str | None]:
if shutil.which(cli_path):
try:
result = subprocess.run(
[cli_path, "--version"],
capture_output=True,
text=True,
timeout=10,
)
if result.returncode == 0:
version = result.stdout.strip() or result.stderr.strip()
logger.info(f"signal-cli found: {version}")
return True, version
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
pass
return False, None
def check_java_installed() -> tuple[bool, str | None]:
java_home = shutil.which("java")
if not java_home:
return False, None
try:
result = subprocess.run(
[java_home, "-version"],
capture_output=True,
text=True,
timeout=10,
)
version_output = result.stderr or result.stdout
logger.info(f"Java found: {version_output.strip().split(chr(10))[0]}")
return True, version_output.strip()
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
return False, None
def get_install_instructions() -> str:
system = platform.system().lower()
if system == "linux":
return (
"Install signal-cli on Linux:\n"
" 1. Download the latest release from:\n"
f" {INSTALL_GUIDE_URL}\n"
" 2. Install the .deb package:\n"
" sudo dpkg -i signal-cli_*.deb\n"
" Or use the universal tar:\n"
" tar xf signal-cli-*.tar.gz -C /usr/local\n"
" ln -s /usr/local/signal-cli-*/bin/signal-cli /usr/local/bin/signal-cli\n"
"\n"
"Requires Java 17+ JRE:\n"
" sudo apt install openjdk-17-jre-headless"
)
elif system == "darwin":
return (
"Install signal-cli on macOS:\n"
" brew install signal-cli\n"
"\n"
"Requires Java 17+ JRE:\n"
" brew install openjdk@17"
)
else:
return (
"Install signal-cli on Windows:\n"
f" 1. Download the latest release from {INSTALL_GUIDE_URL}\n"
" 2. Extract signal-cli-*.tar.gz\n"
" 3. Run signal-cli.bat from the bin directory\n"
"\n"
"Requires Java 17+ JRE:\n"
" Download from https://adoptium.net/"
)
def print_install_guide() -> None:
java_ok, java_version = check_java_installed()
cli_ok, cli_version = check_signal_cli_installed()
print("=== Signal CLI Environment Check ===")
print(f" Java 17+: {'OK' if java_ok else 'NOT FOUND'}")
if java_version:
print(f" {java_version.strip().split(chr(10))[0]}")
print(f" signal-cli: {'OK' if cli_ok else 'NOT FOUND'}")
if cli_version:
print(f" version: {cli_version}")
print()
if not java_ok:
print("[!] Java 17+ JRE is required to run signal-cli.")
print()
if not cli_ok:
print("[!] signal-cli is not installed.")
print(get_install_instructions())
else:
print("signal-cli is ready to use.")
print()
print("Next steps:")
print(" 1. Register your phone number:")
print(" signal-cli -a +1234567890 register")
print(" 2. Verify with the code received via SMS:")
print(" signal-cli -a +1234567890 verify <CODE>")
print(" 3. Start daemon mode:")
print(" signal-cli -a +1234567890 daemon --http-listen 127.0.0.1:8080")