na-tools 1.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. na_tools-1.0.1/.github/workflows/release.yml +45 -0
  2. na_tools-1.0.1/.gitignore +10 -0
  3. na_tools-1.0.1/.python-version +1 -0
  4. na_tools-1.0.1/LICENSE +21 -0
  5. na_tools-1.0.1/PKG-INFO +81 -0
  6. na_tools-1.0.1/README.md +69 -0
  7. na_tools-1.0.1/pyproject.toml +17 -0
  8. na_tools-1.0.1/src/na_tools/__init__.py +3 -0
  9. na_tools-1.0.1/src/na_tools/__main__.py +6 -0
  10. na_tools-1.0.1/src/na_tools/cli.py +39 -0
  11. na_tools-1.0.1/src/na_tools/commands/__init__.py +0 -0
  12. na_tools-1.0.1/src/na_tools/commands/backup.py +218 -0
  13. na_tools-1.0.1/src/na_tools/commands/config_cmd.py +307 -0
  14. na_tools-1.0.1/src/na_tools/commands/install.py +139 -0
  15. na_tools-1.0.1/src/na_tools/commands/list_cmd.py +54 -0
  16. na_tools-1.0.1/src/na_tools/commands/logs.py +43 -0
  17. na_tools-1.0.1/src/na_tools/commands/restore.py +283 -0
  18. na_tools-1.0.1/src/na_tools/commands/status.py +39 -0
  19. na_tools-1.0.1/src/na_tools/commands/update.py +61 -0
  20. na_tools-1.0.1/src/na_tools/commands/use.py +57 -0
  21. na_tools-1.0.1/src/na_tools/core/__init__.py +0 -0
  22. na_tools-1.0.1/src/na_tools/core/compose.py +145 -0
  23. na_tools-1.0.1/src/na_tools/core/config.py +142 -0
  24. na_tools-1.0.1/src/na_tools/core/docker.py +364 -0
  25. na_tools-1.0.1/src/na_tools/core/na_config.py +158 -0
  26. na_tools-1.0.1/src/na_tools/core/platform.py +142 -0
  27. na_tools-1.0.1/src/na_tools/utils/__init__.py +0 -0
  28. na_tools-1.0.1/src/na_tools/utils/console.py +53 -0
  29. na_tools-1.0.1/src/na_tools/utils/crypto.py +10 -0
  30. na_tools-1.0.1/src/na_tools/utils/network.py +59 -0
  31. na_tools-1.0.1/src/na_tools/utils/privilege.py +39 -0
  32. na_tools-1.0.1/uv.lock +240 -0
@@ -0,0 +1,45 @@
1
+ name: Release to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*.*.*'
7
+ workflow_dispatch:
8
+
9
+ jobs:
10
+ release:
11
+ name: Build and Release
12
+ runs-on: ubuntu-latest
13
+ permissions:
14
+ contents: write
15
+
16
+ steps:
17
+ - name: Checkout Code
18
+ uses: actions/checkout@v4
19
+
20
+ - name: Set up uv
21
+ uses: astral-sh/setup-uv@v5
22
+ with:
23
+ enable-cache: true
24
+
25
+ - name: Set up Python
26
+ uses: actions/setup-python@v5
27
+ with:
28
+ python-version-file: "pyproject.toml"
29
+
30
+ - name: Build project
31
+ run: uv build
32
+
33
+ - name: Publish to PyPI
34
+ uses: pypa/gh-action-pypi-publish@release/v1
35
+ with:
36
+ password: ${{ secrets.PYPI_TOKEN }}
37
+
38
+ - name: Create GitHub Release
39
+ uses: softprops/action-gh-release@v2
40
+ with:
41
+ generate_release_notes: true
42
+ files: |
43
+ dist/*
44
+ env:
45
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -0,0 +1,10 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
@@ -0,0 +1 @@
1
+ 3.13
na_tools-1.0.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 NekroAI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,81 @@
1
+ Metadata-Version: 2.4
2
+ Name: na-tools
3
+ Version: 1.0.1
4
+ Summary: Nekro Agent 跨平台自动部署 CLI 工具
5
+ License-File: LICENSE
6
+ Requires-Python: >=3.10
7
+ Requires-Dist: click>=8.1
8
+ Requires-Dist: httpx>=0.27
9
+ Requires-Dist: pyyaml>=6.0
10
+ Requires-Dist: rich>=13.0
11
+ Description-Content-Type: text/markdown
12
+
13
+ # NA-Tools
14
+
15
+ **Nekro Agent 跨平台自动部署 CLI 工具**
16
+
17
+ 支持 macOS / Linux,提供一键安装、更新、备份、恢复和配置管理。
18
+
19
+ ## 安装
20
+
21
+ ```bash
22
+ # 需要 Python 3.10+
23
+ pip install -e .
24
+
25
+ # 或使用 uv
26
+ uv sync
27
+ ```
28
+
29
+ ## 命令
30
+
31
+ | 命令 | 说明 |
32
+ |------|------|
33
+ | `na-tools install` | 安装 Nekro Agent(Docker 检测 → 配置 → 部署) |
34
+ | `na-tools update` | 更新服务到最新版本 |
35
+ | `na-tools backup` | 备份数据和配置 |
36
+ | `na-tools restore [file]` | 从备份恢复(不指定文件则从列表选择) |
37
+ | `na-tools config` | 快捷配置 nekro-agent.yaml |
38
+ | `na-tools status` | 查看服务状态 |
39
+ | `na-tools logs [service]` | 查看服务日志 |
40
+ | `na-tools list` | 列出所有已安装的 Nekro Agent 及序号 |
41
+ | `na-tools use <id/path>` | 切换当前激活的数据目录 |
42
+
43
+ ## 快速开始
44
+
45
+ ```bash
46
+ # 一键安装
47
+ na-tools install
48
+
49
+ # 配置模型 API
50
+ na-tools config model
51
+
52
+ # 添加管理员
53
+ na-tools config admin --add 12345678
54
+
55
+ # 更新到最新版
56
+ na-tools update
57
+
58
+ # 备份数据
59
+ na-tools backup
60
+
61
+ # 恢复备份(交互式选择)
62
+ na-tools restore
63
+
64
+ # 查看多开或所有安装实例
65
+ na-tools list
66
+
67
+ # 切换到另一个安装实例
68
+ na-tools use 1
69
+
70
+ # 查看状态
71
+ na-tools status
72
+ ```
73
+
74
+ ## 跨平台支持
75
+
76
+ | 功能 | Linux | macOS |
77
+ |------|-------|-------|
78
+ | Docker 安装 | ✅ 自动 | ⚠️ 引导 |
79
+ | 服务部署 | ✅ | ✅ |
80
+ | 备份恢复 | ✅ | ✅ |
81
+ | 配置管理 | ✅ | ✅ |
@@ -0,0 +1,69 @@
1
+ # NA-Tools
2
+
3
+ **Nekro Agent 跨平台自动部署 CLI 工具**
4
+
5
+ 支持 macOS / Linux,提供一键安装、更新、备份、恢复和配置管理。
6
+
7
+ ## 安装
8
+
9
+ ```bash
10
+ # 需要 Python 3.10+
11
+ pip install -e .
12
+
13
+ # 或使用 uv
14
+ uv sync
15
+ ```
16
+
17
+ ## 命令
18
+
19
+ | 命令 | 说明 |
20
+ |------|------|
21
+ | `na-tools install` | 安装 Nekro Agent(Docker 检测 → 配置 → 部署) |
22
+ | `na-tools update` | 更新服务到最新版本 |
23
+ | `na-tools backup` | 备份数据和配置 |
24
+ | `na-tools restore [file]` | 从备份恢复(不指定文件则从列表选择) |
25
+ | `na-tools config` | 快捷配置 nekro-agent.yaml |
26
+ | `na-tools status` | 查看服务状态 |
27
+ | `na-tools logs [service]` | 查看服务日志 |
28
+ | `na-tools list` | 列出所有已安装的 Nekro Agent 及序号 |
29
+ | `na-tools use <id/path>` | 切换当前激活的数据目录 |
30
+
31
+ ## 快速开始
32
+
33
+ ```bash
34
+ # 一键安装
35
+ na-tools install
36
+
37
+ # 配置模型 API
38
+ na-tools config model
39
+
40
+ # 添加管理员
41
+ na-tools config admin --add 12345678
42
+
43
+ # 更新到最新版
44
+ na-tools update
45
+
46
+ # 备份数据
47
+ na-tools backup
48
+
49
+ # 恢复备份(交互式选择)
50
+ na-tools restore
51
+
52
+ # 查看多开或所有安装实例
53
+ na-tools list
54
+
55
+ # 切换到另一个安装实例
56
+ na-tools use 1
57
+
58
+ # 查看状态
59
+ na-tools status
60
+ ```
61
+
62
+ ## 跨平台支持
63
+
64
+ | 功能 | Linux | macOS |
65
+ |------|-------|-------|
66
+ | Docker 安装 | ✅ 自动 | ⚠️ 引导 |
67
+ | 服务部署 | ✅ | ✅ |
68
+ | 备份恢复 | ✅ | ✅ |
69
+ | 配置管理 | ✅ | ✅ |
@@ -0,0 +1,17 @@
1
+ [project]
2
+ name = "na-tools"
3
+ version = "1.0.1"
4
+ description = "Nekro Agent 跨平台自动部署 CLI 工具"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ dependencies = ["click>=8.1", "rich>=13.0", "httpx>=0.27", "pyyaml>=6.0"]
8
+
9
+ [project.scripts]
10
+ na-tools = "na_tools.cli:main"
11
+
12
+ [build-system]
13
+ requires = ["hatchling"]
14
+ build-backend = "hatchling.build"
15
+
16
+ [tool.hatch.build.targets.wheel]
17
+ packages = ["src/na_tools"]
@@ -0,0 +1,3 @@
1
+ """NA-Tools: Nekro Agent 跨平台自动部署 CLI 工具"""
2
+
3
+ __version__ = "1.0.1"
@@ -0,0 +1,6 @@
1
+ """NA-Tools 入口模块。"""
2
+
3
+ from na_tools.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,39 @@
1
+ """NA-Tools CLI 入口。"""
2
+
3
+ import click
4
+
5
+ from . import __version__
6
+ from .commands.backup import backup
7
+ from .commands.config_cmd import config
8
+ from .commands.install import install
9
+ from .commands.logs import logs
10
+ from .commands.restore import restore
11
+ from .commands.status import status
12
+ from .commands.update import update
13
+ from .commands.use import use
14
+ from .commands.list_cmd import list_cmd
15
+
16
+
17
+ @click.group()
18
+ @click.version_option(version=__version__, prog_name="na-tools")
19
+ def main() -> None:
20
+ """NA-Tools: Nekro Agent 部署管理工具
21
+
22
+ 支持一键安装、更新、备份、恢复和配置管理。
23
+ """
24
+ pass
25
+
26
+
27
+ main.add_command(install)
28
+ main.add_command(update)
29
+ main.add_command(backup)
30
+ main.add_command(restore)
31
+ main.add_command(config)
32
+ main.add_command(status)
33
+ main.add_command(logs)
34
+ main.add_command(use)
35
+ main.add_command(list_cmd)
36
+
37
+
38
+ if __name__ == "__main__":
39
+ main()
File without changes
@@ -0,0 +1,218 @@
1
+ """backup 命令:备份 Nekro Agent 数据。"""
2
+
3
+ import tarfile
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+
7
+ import click
8
+
9
+ from ..core.compose import compose_exists
10
+ from ..core.docker import DockerEnv
11
+ from ..core.platform import default_data_dir
12
+ from ..utils.privilege import with_sudo_fallback
13
+ from ..utils.console import error, info, success, warning
14
+
15
+
16
+ @click.group(invoke_without_command=True)
17
+ @click.pass_context
18
+ @with_sudo_fallback
19
+ @click.option("--data-dir", type=click.Path(), default=None, help="数据目录路径")
20
+ @click.option(
21
+ "--output", "-o", type=click.Path(), default=None, help="备份文件输出路径"
22
+ )
23
+ @click.option("--no-restart", is_flag=True, default=False, help="备份后不重启服务")
24
+ def backup(
25
+ ctx: click.Context, data_dir: str | None, output: str | None, no_restart: bool
26
+ ) -> None:
27
+ """备份 Nekro Agent 数据和配置。"""
28
+ if ctx.invoked_subcommand is not None:
29
+ return
30
+
31
+ data_dir_path = Path(data_dir or default_data_dir()).expanduser().resolve()
32
+
33
+ if not data_dir_path.exists():
34
+ error(f"数据目录不存在: {data_dir_path}")
35
+ raise click.Abort()
36
+
37
+ docker = DockerEnv()
38
+ env_path = data_dir_path / ".env"
39
+
40
+ # 生成备份文件名
41
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
42
+ if output:
43
+ backup_path = Path(output)
44
+ else:
45
+ backup_dir = Path("~/.config/na-tools/backup").expanduser() / data_dir_path.name
46
+ backup_dir.mkdir(parents=True, exist_ok=True)
47
+ backup_path = backup_dir / f"{data_dir_path.name}_backup_{timestamp}.tar.gz"
48
+
49
+ backup_path.parent.mkdir(parents=True, exist_ok=True)
50
+
51
+ # 准备备份存储卷(在停止服务前解析卷名)
52
+ volume_backups_map: list[
53
+ tuple[str, str, Path]
54
+ ] = [] # (volume_name, backup_filename, backup_path)
55
+ volumes_dir = data_dir_path / "volumes_backup_tmp"
56
+
57
+ if compose_exists(data_dir_path) and docker.compose_installed:
58
+ config = docker.get_compose_config(
59
+ cwd=data_dir_path, env_file=env_path if env_path.exists() else None
60
+ )
61
+ if config and "services" in config and isinstance(config["services"], dict):
62
+ from typing import cast
63
+
64
+ services = cast(dict[str, dict[str, object]], config["services"])
65
+
66
+ # 映射关系: 服务名 -> 内部挂载点 -> 备份文件名
67
+ backup_targets = {
68
+ "nekro_postgres": ("/var/lib/postgresql/data", "postgres.tar.gz"),
69
+ "nekro_qdrant": ("/qdrant/storage", "qdrant.tar.gz"),
70
+ }
71
+
72
+ for svc_name, (internal_path, filename) in backup_targets.items():
73
+ if svc_name in services:
74
+ # 尝试解析卷名
75
+ real_volume_name = docker.get_service_volume(
76
+ cwd=data_dir_path,
77
+ service=svc_name,
78
+ target=internal_path,
79
+ env_file=env_path if env_path.exists() else None,
80
+ )
81
+
82
+ # 如果解析失败(容器未运行?),尝试从配置读取
83
+ if not real_volume_name:
84
+ svc_config = services[svc_name]
85
+ volumes_config = svc_config.get("volumes", [])
86
+ if isinstance(volumes_config, list):
87
+ volumes = cast(list[dict[str, str]], volumes_config)
88
+ for vol in volumes:
89
+ if not vol:
90
+ continue
91
+ if (
92
+ vol.get("type") == "volume"
93
+ and vol.get("target") == internal_path
94
+ ):
95
+ real_volume_name = vol.get("source")
96
+ break
97
+
98
+ if real_volume_name:
99
+ volume_backups_map.append(
100
+ (real_volume_name, filename, volumes_dir / filename)
101
+ )
102
+
103
+ # 停止服务
104
+ should_restart = False
105
+ if compose_exists(data_dir_path) and docker.compose_installed:
106
+ info("正在停止服务以确保数据一致性...")
107
+ _ = docker.down(
108
+ cwd=data_dir_path, env_file=env_path if env_path.exists() else None
109
+ )
110
+ should_restart = True
111
+
112
+ # 执行卷备份
113
+ volume_backups: list[Path] = []
114
+ if volume_backups_map:
115
+ volumes_dir.mkdir(exist_ok=True)
116
+ for vol_name, filename, backup_file in volume_backups_map:
117
+ info(f"正在备份存储卷 {vol_name}...")
118
+
119
+ # 使用 alpine 打包
120
+ success_backup = docker.run_ephemeral(
121
+ image="alpine:latest",
122
+ cmd=["tar", "czf", f"/backup/{filename}", "-C", "/data", "."],
123
+ volumes={vol_name: "/data", str(volumes_dir): "/backup"},
124
+ )
125
+
126
+ if success_backup:
127
+ volume_backups.append(backup_file)
128
+ success(f"卷备份完成: {filename}")
129
+ else:
130
+ error(f"卷备份失败: {vol_name}")
131
+
132
+ # 打包数据
133
+ info(f"正在备份数据到: {backup_path}")
134
+ try:
135
+ with tarfile.open(backup_path, "w:gz") as tar:
136
+ # 添加主数据目录
137
+ tar.add(
138
+ data_dir_path,
139
+ arcname=data_dir_path.name,
140
+ filter=lambda x: None if "volumes_backup_tmp" in x.name else x,
141
+ )
142
+
143
+ # 添加卷备份
144
+ if volume_backups:
145
+ # 在 tar 中创建一个 volumes 目录
146
+ for vb in volume_backups:
147
+ tar.add(vb, arcname=f"volumes/{vb.name}")
148
+
149
+ success(
150
+ f"备份完成: {backup_path} ({backup_path.stat().st_size / 1024 / 1024:.1f} MB)"
151
+ )
152
+ except Exception as e:
153
+ error(f"备份失败: {e}")
154
+ # 即使备份失败也要尝试重启
155
+ if should_restart and not no_restart:
156
+ info("正在重新启动服务...")
157
+ _ = docker.up(
158
+ cwd=data_dir_path, env_file=env_path if env_path.exists() else None
159
+ )
160
+ raise click.Abort()
161
+ finally:
162
+ # 清理临时卷备份目录
163
+ import shutil
164
+
165
+ if volumes_dir.exists():
166
+ shutil.rmtree(volumes_dir)
167
+
168
+ # 重启服务
169
+ if should_restart and not no_restart:
170
+ info("正在重新启动服务...")
171
+ if docker.up(
172
+ cwd=data_dir_path, env_file=env_path if env_path.exists() else None
173
+ ):
174
+ success("服务已重新启动。")
175
+ else:
176
+ warning("服务重启失败,请手动启动。")
177
+
178
+ success("🎉 备份完成!")
179
+
180
+
181
+ @backup.command("list")
182
+ @click.pass_context
183
+ def list_backups(ctx: click.Context) -> None:
184
+ """列出可用的备份文件。"""
185
+ data_dir = None
186
+ import typing
187
+
188
+ obj = typing.cast(object, ctx.obj)
189
+ if isinstance(obj, dict):
190
+ from typing import cast
191
+
192
+ obj_dict = cast(dict[str, object], obj)
193
+ val = obj_dict.get("data_dir")
194
+ if isinstance(val, str):
195
+ data_dir = val
196
+ data_dir_path = Path(data_dir or default_data_dir()).expanduser().resolve()
197
+ backup_dir = Path("~/.config/na-tools/backup").expanduser() / data_dir_path.name
198
+
199
+ if not backup_dir.exists():
200
+ info("备份目录不存在或为空。")
201
+ return
202
+
203
+ backups = sorted(
204
+ list(backup_dir.glob("*.tar.gz")),
205
+ key=lambda x: x.stat().st_mtime,
206
+ reverse=True,
207
+ )
208
+
209
+ if not backups:
210
+ info("没有任何历史备份。")
211
+ return
212
+
213
+ info("发现以下历史备份:")
214
+ for i, b in enumerate(backups, 1):
215
+ mtime = datetime.fromtimestamp(b.stat().st_mtime).strftime("%Y-%m-%d %H:%M:%S")
216
+ click.echo(
217
+ f" [{i}] {b.name} (备份时间: {mtime}, 大小: {b.stat().st_size / 1024 / 1024:.1f} MB)"
218
+ )