v1.9.40: add Codex integration — skill setup, deploy, GUI buttons
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,15 @@
|
||||
"""
|
||||
Claude Code integration setup.
|
||||
Installs ssh.py, encryption.py, /ssh skill, SSH key — everything needed
|
||||
for Claude Code to manage servers via the shared servers.json.
|
||||
Local AI agent integration setup.
|
||||
Installs the shared ssh.py/encryption.py backend, Claude /ssh command,
|
||||
Codex skill package, platform-specific wrappers, and SSH key material.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from core.logger import log
|
||||
|
||||
SHARED_DIR = os.path.expanduser("~/.server-connections")
|
||||
@@ -19,13 +22,24 @@ else:
|
||||
|
||||
SSH_SCRIPT_SRC = os.path.join(_BASE_DIR, "tools", "ssh.py")
|
||||
ENCRYPTION_SRC = os.path.join(_BASE_DIR, "core", "encryption.py")
|
||||
SKILL_SRC = os.path.join(_BASE_DIR, "tools", "skill-ssh.md")
|
||||
CLAUDE_SKILL_SRC = os.path.join(_BASE_DIR, "tools", "skill-ssh.md")
|
||||
|
||||
SKILL_DST_DIR = os.path.expanduser("~/.claude/commands")
|
||||
SKILL_DST = os.path.join(SKILL_DST_DIR, "ssh.md")
|
||||
CLAUDE_SKILL_DST_DIR = os.path.expanduser("~/.claude/commands")
|
||||
CLAUDE_SKILL_DST = os.path.join(CLAUDE_SKILL_DST_DIR, "ssh.md")
|
||||
SSH_KEY_PATH = os.path.expanduser("~/.ssh/id_ed25519")
|
||||
GLOBAL_CLAUDE_MD = os.path.expanduser("~/.claude/CLAUDE.md")
|
||||
|
||||
CODEX_SKILL_SRC_DIR = os.path.join(_BASE_DIR, ".codex", "skills", "server-manager")
|
||||
CODEX_SKILL_DST_ROOT = os.path.expanduser("~/.codex/skills")
|
||||
CODEX_SKILL_DST_DIR = os.path.join(CODEX_SKILL_DST_ROOT, "server-manager")
|
||||
CODEX_SKILL_ENTRY = os.path.join(CODEX_SKILL_DST_DIR, "SKILL.md")
|
||||
CODEX_WRAPPER_SRC_SH = os.path.join(CODEX_SKILL_SRC_DIR, "scripts", "codex-ssh-wrapper.sh")
|
||||
CODEX_WRAPPER_SRC_CMD = os.path.join(CODEX_SKILL_SRC_DIR, "scripts", "codex-ssh-wrapper.cmd")
|
||||
CODEX_WRAPPER_DST = os.path.join(
|
||||
SHARED_DIR,
|
||||
"codex-ssh.cmd" if sys.platform == "win32" else "codex-ssh",
|
||||
)
|
||||
|
||||
_BLOCK_START = "<!-- server-manager:start -->"
|
||||
_BLOCK_END = "<!-- server-manager:end -->"
|
||||
|
||||
@@ -67,6 +81,27 @@ python ~/.server-connections/ssh.py --status # online/offline
|
||||
"""
|
||||
|
||||
|
||||
def _ensure_executable(path: str):
|
||||
if sys.platform == "win32" or not os.path.exists(path):
|
||||
return
|
||||
mode = os.stat(path).st_mode
|
||||
os.chmod(path, mode | 0o755)
|
||||
|
||||
|
||||
def _copy_file(src: str, dst: str, executable: bool = False) -> str:
|
||||
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
||||
shutil.copy2(src, dst)
|
||||
if executable:
|
||||
_ensure_executable(dst)
|
||||
return dst
|
||||
|
||||
|
||||
def _copy_tree(src: str, dst: str) -> str:
|
||||
os.makedirs(dst, exist_ok=True)
|
||||
shutil.copytree(src, dst, dirs_exist_ok=True)
|
||||
return dst
|
||||
|
||||
|
||||
def check_status() -> dict:
|
||||
"""Check what's installed and what's missing."""
|
||||
return {
|
||||
@@ -74,7 +109,9 @@ def check_status() -> dict:
|
||||
"servers_json": os.path.exists(os.path.join(SHARED_DIR, "servers.json")),
|
||||
"ssh_script": os.path.exists(os.path.join(SHARED_DIR, "ssh.py")),
|
||||
"encryption": os.path.exists(os.path.join(SHARED_DIR, "encryption.py")),
|
||||
"skill_installed": os.path.exists(SKILL_DST),
|
||||
"claude_skill_installed": os.path.exists(CLAUDE_SKILL_DST),
|
||||
"codex_skill_installed": os.path.exists(CODEX_SKILL_ENTRY),
|
||||
"codex_wrapper_installed": os.path.exists(CODEX_WRAPPER_DST),
|
||||
"ssh_key_exists": os.path.exists(SSH_KEY_PATH),
|
||||
"ssh_key_pub": os.path.exists(SSH_KEY_PATH + ".pub"),
|
||||
}
|
||||
@@ -85,10 +122,9 @@ def install_ssh_script() -> str:
|
||||
os.makedirs(SHARED_DIR, exist_ok=True)
|
||||
results = []
|
||||
|
||||
# Copy ssh.py
|
||||
dst = os.path.join(SHARED_DIR, "ssh.py")
|
||||
if os.path.exists(SSH_SCRIPT_SRC):
|
||||
shutil.copy2(SSH_SCRIPT_SRC, dst)
|
||||
_copy_file(SSH_SCRIPT_SRC, dst, executable=True)
|
||||
log.info(f"ssh.py installed: {dst}")
|
||||
results.append(f"ssh.py installed: {dst}")
|
||||
elif os.path.exists(dst):
|
||||
@@ -96,10 +132,9 @@ def install_ssh_script() -> str:
|
||||
else:
|
||||
results.append("ERROR: ssh.py source not found")
|
||||
|
||||
# Copy encryption.py
|
||||
enc_dst = os.path.join(SHARED_DIR, "encryption.py")
|
||||
if os.path.exists(ENCRYPTION_SRC):
|
||||
shutil.copy2(ENCRYPTION_SRC, enc_dst)
|
||||
_copy_file(ENCRYPTION_SRC, enc_dst)
|
||||
log.info(f"encryption.py installed: {enc_dst}")
|
||||
results.append(f"encryption.py installed: {enc_dst}")
|
||||
elif os.path.exists(enc_dst):
|
||||
@@ -110,22 +145,59 @@ def install_ssh_script() -> str:
|
||||
return "\n".join(results)
|
||||
|
||||
|
||||
def install_skill() -> str:
|
||||
def install_claude_skill() -> str:
|
||||
"""Install /ssh skill for Claude Code."""
|
||||
os.makedirs(SKILL_DST_DIR, exist_ok=True)
|
||||
if os.path.exists(SKILL_SRC):
|
||||
shutil.copy2(SKILL_SRC, SKILL_DST)
|
||||
log.info(f"Skill installed: {SKILL_DST}")
|
||||
return f"Skill installed: {SKILL_DST}"
|
||||
# Fallback: check existing
|
||||
if os.path.exists(SKILL_DST):
|
||||
return f"Skill already exists: {SKILL_DST}"
|
||||
# Generate minimal skill
|
||||
os.makedirs(CLAUDE_SKILL_DST_DIR, exist_ok=True)
|
||||
if os.path.exists(CLAUDE_SKILL_SRC):
|
||||
_copy_file(CLAUDE_SKILL_SRC, CLAUDE_SKILL_DST)
|
||||
log.info(f"Claude skill installed: {CLAUDE_SKILL_DST}")
|
||||
return f"Claude skill installed: {CLAUDE_SKILL_DST}"
|
||||
if os.path.exists(CLAUDE_SKILL_DST):
|
||||
return f"Claude skill already exists: {CLAUDE_SKILL_DST}"
|
||||
|
||||
skill_content = _generate_skill_content()
|
||||
with open(SKILL_DST, "w", encoding="utf-8") as f:
|
||||
with open(CLAUDE_SKILL_DST, "w", encoding="utf-8") as f:
|
||||
f.write(skill_content)
|
||||
log.info(f"Skill generated: {SKILL_DST}")
|
||||
return f"Skill generated: {SKILL_DST}"
|
||||
log.info(f"Claude skill generated: {CLAUDE_SKILL_DST}")
|
||||
return f"Claude skill generated: {CLAUDE_SKILL_DST}"
|
||||
|
||||
|
||||
def install_codex_skill() -> str:
|
||||
"""Install ServerManager skill package for Codex and the local wrapper."""
|
||||
results = []
|
||||
|
||||
if os.path.isdir(CODEX_SKILL_SRC_DIR):
|
||||
_copy_tree(CODEX_SKILL_SRC_DIR, CODEX_SKILL_DST_DIR)
|
||||
for rel_path in [
|
||||
os.path.join("scripts", "server-manager-doctor.sh"),
|
||||
os.path.join("scripts", "server-manager-doctor.cmd"),
|
||||
os.path.join("scripts", "codex-ssh-wrapper.sh"),
|
||||
os.path.join("scripts", "codex-ssh-wrapper.cmd"),
|
||||
]:
|
||||
_ensure_executable(os.path.join(CODEX_SKILL_DST_DIR, rel_path))
|
||||
log.info(f"Codex skill installed: {CODEX_SKILL_DST_DIR}")
|
||||
results.append(f"Codex skill installed: {CODEX_SKILL_DST_DIR}")
|
||||
elif os.path.exists(CODEX_SKILL_ENTRY):
|
||||
results.append(f"Codex skill already exists: {CODEX_SKILL_DST_DIR}")
|
||||
else:
|
||||
results.append("ERROR: Codex skill source not found")
|
||||
|
||||
wrapper_src = CODEX_WRAPPER_SRC_CMD if sys.platform == "win32" else CODEX_WRAPPER_SRC_SH
|
||||
if os.path.exists(wrapper_src):
|
||||
_copy_file(wrapper_src, CODEX_WRAPPER_DST, executable=(sys.platform != "win32"))
|
||||
log.info(f"Codex wrapper installed: {CODEX_WRAPPER_DST}")
|
||||
results.append(f"Codex wrapper installed: {CODEX_WRAPPER_DST}")
|
||||
elif os.path.exists(CODEX_WRAPPER_DST):
|
||||
results.append(f"Codex wrapper already exists: {CODEX_WRAPPER_DST}")
|
||||
else:
|
||||
results.append("ERROR: Codex wrapper source not found")
|
||||
|
||||
return "\n".join(results)
|
||||
|
||||
|
||||
def install_skill() -> str:
|
||||
"""Backward-compatible alias for the Claude /ssh skill installer."""
|
||||
return install_claude_skill()
|
||||
|
||||
|
||||
def generate_ssh_key() -> str:
|
||||
@@ -135,7 +207,6 @@ def generate_ssh_key() -> str:
|
||||
|
||||
os.makedirs(os.path.dirname(SSH_KEY_PATH), exist_ok=True)
|
||||
|
||||
import subprocess
|
||||
try:
|
||||
subprocess.run(
|
||||
["ssh-keygen", "-t", "ed25519", "-f", SSH_KEY_PATH,
|
||||
@@ -156,11 +227,7 @@ def generate_ssh_key() -> str:
|
||||
|
||||
|
||||
def install_global_claude_md() -> str:
|
||||
"""Add/update server manager section in global ~/.claude/CLAUDE.md.
|
||||
|
||||
Uses start/end markers to safely replace existing block without duplication.
|
||||
"""
|
||||
import re
|
||||
"""Add/update server manager section in global ~/.claude/CLAUDE.md."""
|
||||
os.makedirs(os.path.dirname(GLOBAL_CLAUDE_MD), exist_ok=True)
|
||||
|
||||
existing = ""
|
||||
@@ -174,29 +241,28 @@ def install_global_claude_md() -> str:
|
||||
)
|
||||
|
||||
if pattern.search(existing):
|
||||
# Блок уже есть — заменяем на актуальную версию
|
||||
updated = pattern.sub(GLOBAL_CLAUDE_MD_BLOCK.strip(), existing)
|
||||
with open(GLOBAL_CLAUDE_MD, "w", encoding="utf-8") as f:
|
||||
f.write(updated)
|
||||
log.info(f"Global CLAUDE.md block updated: {GLOBAL_CLAUDE_MD}")
|
||||
return f"Global CLAUDE.md block updated: {GLOBAL_CLAUDE_MD}"
|
||||
else:
|
||||
# Блока нет — добавляем в конец
|
||||
with open(GLOBAL_CLAUDE_MD, "a", encoding="utf-8") as f:
|
||||
if existing and not existing.endswith("\n"):
|
||||
f.write("\n")
|
||||
f.write("\n" + GLOBAL_CLAUDE_MD_BLOCK)
|
||||
log.info(f"Global CLAUDE.md block added: {GLOBAL_CLAUDE_MD}")
|
||||
return f"Global CLAUDE.md block added: {GLOBAL_CLAUDE_MD}"
|
||||
|
||||
with open(GLOBAL_CLAUDE_MD, "a", encoding="utf-8") as f:
|
||||
if existing and not existing.endswith("\n"):
|
||||
f.write("\n")
|
||||
f.write("\n" + GLOBAL_CLAUDE_MD_BLOCK)
|
||||
log.info(f"Global CLAUDE.md block added: {GLOBAL_CLAUDE_MD}")
|
||||
return f"Global CLAUDE.md block added: {GLOBAL_CLAUDE_MD}"
|
||||
|
||||
|
||||
def install_all() -> list[str]:
|
||||
"""Full setup — install everything."""
|
||||
"""Full setup — install everything for Claude Code and Codex."""
|
||||
results = []
|
||||
|
||||
steps = [
|
||||
("ssh_script", install_ssh_script),
|
||||
("skill", install_skill),
|
||||
("claude_skill", install_claude_skill),
|
||||
("codex_skill", install_codex_skill),
|
||||
("ssh_key", generate_ssh_key),
|
||||
("global_claude_md", install_global_claude_md),
|
||||
]
|
||||
|
||||
57
core/i18n.py
57
core/i18n.py
@@ -46,7 +46,7 @@ _EN = {
|
||||
"about_desc": (
|
||||
"Desktop application for managing remote servers.\n"
|
||||
"SSH terminal, SFTP file transfer, key management,\n"
|
||||
"encrypted credentials, and Claude Code integration."
|
||||
"encrypted credentials, and Claude Code / Codex integration."
|
||||
),
|
||||
"about_features_title": "⚡ Features",
|
||||
"about_features": (
|
||||
@@ -56,13 +56,13 @@ _EN = {
|
||||
"• TOTP / 2FA (Google Authenticator)\n"
|
||||
"• Encrypted credentials (Fernet)\n"
|
||||
"• Automatic backups\n"
|
||||
"• Claude Code integration"
|
||||
"• Claude Code and Codex integration"
|
||||
),
|
||||
"about_howto_title": "🚀 Quick Start",
|
||||
"about_howto": (
|
||||
"1. Click \"+ Add\" to add a server\n"
|
||||
"2. Select server → Terminal / Files\n"
|
||||
"3. Setup tab → Claude Code integration"
|
||||
"3. Setup tab → Claude Code / Codex integration"
|
||||
),
|
||||
"version": "Version",
|
||||
"author": "Author",
|
||||
@@ -157,6 +157,12 @@ _EN = {
|
||||
"no_public_key": "[!] No public key to copy",
|
||||
|
||||
# Setup
|
||||
"agent_integration": "AI Agent Integration",
|
||||
"agent_desc": (
|
||||
"Setup everything so Claude Code and Codex can manage your servers via shared local skills.\n"
|
||||
"ServerManager, Claude Code, and Codex share the same servers.json — add a server here,\n"
|
||||
"both agents see it immediately."
|
||||
),
|
||||
"claude_integration": "Claude Code Integration",
|
||||
"claude_desc": (
|
||||
"Setup everything so Claude Code can manage your servers via /ssh skill.\n"
|
||||
@@ -169,11 +175,16 @@ _EN = {
|
||||
"status_ssh_script": "ssh.py (CLI tool)",
|
||||
"status_encryption": "Encryption module",
|
||||
"status_skill": "/ssh skill for Claude Code",
|
||||
"status_claude_skill": "/ssh skill for Claude Code",
|
||||
"status_codex_skill": "ServerManager skill for Codex",
|
||||
"status_codex_wrapper": "Codex wrapper (codex-ssh)",
|
||||
"status_ssh_key": "SSH key (ed25519)",
|
||||
"install_everything": "Install Everything",
|
||||
"installing_all": "Installing...",
|
||||
"install_ssh_py": "ssh.py",
|
||||
"install_skill": "/ssh skill",
|
||||
"install_claude_skill": "Claude skill",
|
||||
"install_codex_skill": "Codex skill",
|
||||
"install_ssh_key": "SSH key",
|
||||
"refresh": "Refresh",
|
||||
"configuration": "Configuration",
|
||||
@@ -183,7 +194,7 @@ _EN = {
|
||||
"select_backup": "Select backup...",
|
||||
"no_backups": "No backups",
|
||||
"restore": "Restore",
|
||||
"install_done": "Done! Claude Code can now use /ssh to manage your servers.",
|
||||
"install_done": "Done! Claude Code and Codex can now use ServerManager to manage your servers.",
|
||||
"config_changed": "Config path changed: {path}",
|
||||
"backup_created": "Backup created: {name}",
|
||||
"backup_failed": "Backup failed: {e}",
|
||||
@@ -603,7 +614,7 @@ _RU = {
|
||||
"about_desc": (
|
||||
"Настольное приложение для управления удалёнными серверами.\n"
|
||||
"SSH-терминал, SFTP-передача файлов, управление ключами,\n"
|
||||
"шифрование паролей и интеграция с Claude Code."
|
||||
"шифрование паролей и интеграция с Claude Code / Codex."
|
||||
),
|
||||
"about_features_title": "⚡ Возможности",
|
||||
"about_features": (
|
||||
@@ -613,13 +624,13 @@ _RU = {
|
||||
"• TOTP / 2FA (Google Authenticator)\n"
|
||||
"• Шифрование паролей (Fernet)\n"
|
||||
"• Автоматические бэкапы\n"
|
||||
"• Интеграция с Claude Code"
|
||||
"• Интеграция с Claude Code и Codex"
|
||||
),
|
||||
"about_howto_title": "🚀 Быстрый старт",
|
||||
"about_howto": (
|
||||
"1. Нажмите \"+ Добавить\" для добавления сервера\n"
|
||||
"2. Выберите сервер → Терминал / Файлы\n"
|
||||
"3. Вкладка Настройка → интеграция Claude Code"
|
||||
"3. Вкладка Настройка → интеграция Claude Code / Codex"
|
||||
),
|
||||
"version": "Версия",
|
||||
"author": "Автор",
|
||||
@@ -714,6 +725,12 @@ _RU = {
|
||||
"no_public_key": "[!] Нет публичного ключа",
|
||||
|
||||
# Setup
|
||||
"agent_integration": "Интеграция AI-агентов",
|
||||
"agent_desc": (
|
||||
"Настройте всё, чтобы Claude Code и Codex могли управлять серверами через локальные skills.\n"
|
||||
"ServerManager, Claude Code и Codex используют один и тот же servers.json — добавьте сервер здесь,\n"
|
||||
"и оба агента увидят его сразу."
|
||||
),
|
||||
"claude_integration": "Интеграция с Claude Code",
|
||||
"claude_desc": (
|
||||
"Настройте всё, чтобы Claude Code мог управлять серверами через скилл /ssh.\n"
|
||||
@@ -726,11 +743,16 @@ _RU = {
|
||||
"status_ssh_script": "ssh.py (CLI-утилита)",
|
||||
"status_encryption": "Модуль шифрования",
|
||||
"status_skill": "Скилл /ssh для Claude Code",
|
||||
"status_claude_skill": "Скилл /ssh для Claude Code",
|
||||
"status_codex_skill": "Скилл ServerManager для Codex",
|
||||
"status_codex_wrapper": "Обёртка Codex (codex-ssh)",
|
||||
"status_ssh_key": "SSH-ключ (ed25519)",
|
||||
"install_everything": "Установить всё",
|
||||
"installing_all": "Установка...",
|
||||
"install_ssh_py": "ssh.py",
|
||||
"install_skill": "Скилл /ssh",
|
||||
"install_claude_skill": "Скилл Claude",
|
||||
"install_codex_skill": "Скилл Codex",
|
||||
"install_ssh_key": "SSH-ключ",
|
||||
"refresh": "Обновить",
|
||||
"configuration": "Конфигурация",
|
||||
@@ -740,7 +762,7 @@ _RU = {
|
||||
"select_backup": "Выберите бэкап...",
|
||||
"no_backups": "Нет бэкапов",
|
||||
"restore": "Восстановить",
|
||||
"install_done": "Готово! Claude Code теперь может использовать /ssh для управления серверами.",
|
||||
"install_done": "Готово! Claude Code и Codex теперь могут использовать ServerManager для управления серверами.",
|
||||
"config_changed": "Путь конфига изменён: {path}",
|
||||
"backup_created": "Бэкап создан: {name}",
|
||||
"backup_failed": "Ошибка бэкапа: {e}",
|
||||
@@ -1160,7 +1182,7 @@ _ZH = {
|
||||
"about_desc": (
|
||||
"用于管理远程服务器的桌面应用程序。\n"
|
||||
"SSH终端、SFTP文件传输、密钥管理、\n"
|
||||
"凭据加密以及Claude Code集成。"
|
||||
"凭据加密以及Claude Code / Codex集成。"
|
||||
),
|
||||
"about_features_title": "⚡ 功能特点",
|
||||
"about_features": (
|
||||
@@ -1170,13 +1192,13 @@ _ZH = {
|
||||
"• TOTP / 2FA(Google Authenticator)\n"
|
||||
"• 凭据加密(Fernet)\n"
|
||||
"• 自动备份\n"
|
||||
"• Claude Code集成"
|
||||
"• Claude Code 和 Codex 集成"
|
||||
),
|
||||
"about_howto_title": "🚀 快速开始",
|
||||
"about_howto": (
|
||||
"1. 点击\"+ 添加\"来添加服务器\n"
|
||||
"2. 选择服务器 → 终端 / 文件\n"
|
||||
"3. 设置标签 → Claude Code集成"
|
||||
"3. 设置标签 → Claude Code / Codex 集成"
|
||||
),
|
||||
"version": "版本",
|
||||
"author": "作者",
|
||||
@@ -1271,6 +1293,12 @@ _ZH = {
|
||||
"no_public_key": "[!] 没有公钥可复制",
|
||||
|
||||
# Setup
|
||||
"agent_integration": "AI代理集成",
|
||||
"agent_desc": (
|
||||
"完成设置后,Claude Code 和 Codex 都可以通过共享的本地技能来管理您的服务器。\n"
|
||||
"ServerManager、Claude Code 和 Codex 共用同一个 servers.json — 在此添加服务器后,\n"
|
||||
"两个代理都会立即看到。"
|
||||
),
|
||||
"claude_integration": "Claude Code集成",
|
||||
"claude_desc": (
|
||||
"设置一切以便Claude Code通过/ssh技能管理您的服务器。\n"
|
||||
@@ -1283,11 +1311,16 @@ _ZH = {
|
||||
"status_ssh_script": "ssh.py(CLI工具)",
|
||||
"status_encryption": "加密模块",
|
||||
"status_skill": "Claude Code的/ssh技能",
|
||||
"status_claude_skill": "Claude Code 的 /ssh 技能",
|
||||
"status_codex_skill": "Codex 的 ServerManager 技能",
|
||||
"status_codex_wrapper": "Codex 包装器(codex-ssh)",
|
||||
"status_ssh_key": "SSH密钥(ed25519)",
|
||||
"install_everything": "全部安装",
|
||||
"installing_all": "安装中...",
|
||||
"install_ssh_py": "ssh.py",
|
||||
"install_skill": "/ssh技能",
|
||||
"install_claude_skill": "Claude 技能",
|
||||
"install_codex_skill": "Codex 技能",
|
||||
"install_ssh_key": "SSH密钥",
|
||||
"refresh": "刷新",
|
||||
"configuration": "配置",
|
||||
@@ -1297,7 +1330,7 @@ _ZH = {
|
||||
"select_backup": "选择备份...",
|
||||
"no_backups": "无备份",
|
||||
"restore": "恢复",
|
||||
"install_done": "完成!Claude Code现在可以使用/ssh来管理您的服务器。",
|
||||
"install_done": "完成!Claude Code 和 Codex 现在都可以使用 ServerManager 来管理您的服务器。",
|
||||
"config_changed": "配置路径已更改:{path}",
|
||||
"backup_created": "备份已创建:{name}",
|
||||
"backup_failed": "备份失败:{e}",
|
||||
|
||||
Reference in New Issue
Block a user