gogeo-client-agentos 0.2.2__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.
- gogeo_client_agentos-0.2.2/PKG-INFO +98 -0
- gogeo_client_agentos-0.2.2/README.md +86 -0
- gogeo_client_agentos-0.2.2/agentos/__init__.py +0 -0
- gogeo_client_agentos-0.2.2/agentos/cli.py +928 -0
- gogeo_client_agentos-0.2.2/agentos/resources/__init__.py +0 -0
- gogeo_client_agentos-0.2.2/agentos/resources/app/__init__.py +0 -0
- gogeo_client_agentos-0.2.2/agentos/resources/app/app.asar +0 -0
- gogeo_client_agentos-0.2.2/agentos/resources/desktop/__init__.py +0 -0
- gogeo_client_agentos-0.2.2/agentos/resources/desktop/chromium.desktop +9 -0
- gogeo_client_agentos-0.2.2/agentos/resources/desktop/gogeoclaw.desktop +10 -0
- gogeo_client_agentos-0.2.2/agentos/resources/desktop/terminal.desktop +9 -0
- gogeo_client_agentos-0.2.2/agentos/resources/desktop/vscode.desktop +9 -0
- gogeo_client_agentos-0.2.2/agentos/resources/icons/@nuwax-ainuwaclaw.png +0 -0
- gogeo_client_agentos-0.2.2/agentos/resources/icons/__init__.py +0 -0
- gogeo_client_agentos-0.2.2/agentos/resources/wallpaper/__init__.py +0 -0
- gogeo_client_agentos-0.2.2/agentos/resources/wallpaper/gogeoDesk.jpg +0 -0
- gogeo_client_agentos-0.2.2/gogeo_client_agentos.egg-info/PKG-INFO +98 -0
- gogeo_client_agentos-0.2.2/gogeo_client_agentos.egg-info/SOURCES.txt +21 -0
- gogeo_client_agentos-0.2.2/gogeo_client_agentos.egg-info/dependency_links.txt +1 -0
- gogeo_client_agentos-0.2.2/gogeo_client_agentos.egg-info/entry_points.txt +2 -0
- gogeo_client_agentos-0.2.2/gogeo_client_agentos.egg-info/top_level.txt +1 -0
- gogeo_client_agentos-0.2.2/pyproject.toml +30 -0
- gogeo_client_agentos-0.2.2/setup.cfg +4 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gogeo-client-agentos
|
|
3
|
+
Version: 0.2.2
|
|
4
|
+
Summary: AgentOS CLI tool for managing the AgentOS Client Docker container
|
|
5
|
+
Author-email: agentos-team <agentos@example.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/agentos/client
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.8
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# AgentOS Client
|
|
14
|
+
|
|
15
|
+
AgentOS Client Docker 容器管理 CLI 工具。
|
|
16
|
+
|
|
17
|
+
## 安装
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install agentos
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## 使用
|
|
24
|
+
|
|
25
|
+
### 首次安装/设置
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# 方式一:直接设置(从环境变量或交互式输入读取 key)
|
|
29
|
+
agentos setup
|
|
30
|
+
|
|
31
|
+
# 方式二:命令行指定 key
|
|
32
|
+
agentos setup --key "your-nuwax-saved-key"
|
|
33
|
+
|
|
34
|
+
# 方式三:先设置环境变量
|
|
35
|
+
export NUWAX_SAVED_KEY="your-key"
|
|
36
|
+
agentos setup
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
setup 流程:
|
|
40
|
+
1. 检查 Docker 是否安装,未安装则自动安装(仅 Linux)
|
|
41
|
+
2. 拉取 agentos-client 镜像
|
|
42
|
+
3. 生成 docker-compose.yml 到 `~/.agentos/`
|
|
43
|
+
4. 保存 key 到 `~/.agentos/.env`
|
|
44
|
+
|
|
45
|
+
### 日常管理
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
agentos start # 启动容器
|
|
49
|
+
agentos stop # 停止容器
|
|
50
|
+
agentos restart # 重启容器
|
|
51
|
+
agentos status # 查看容器状态
|
|
52
|
+
agentos logs # 查看最近 50 行日志
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 更新 key
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
agentos update-key "new-key-here"
|
|
59
|
+
agentos restart # 重启生效
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 重新部署
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
agentos redeploy # 删除旧配置,重新生成并重启
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## 发布到 PyPI
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# 安装构建工具
|
|
72
|
+
pip install build twine
|
|
73
|
+
|
|
74
|
+
# 构建
|
|
75
|
+
cd gogeo-client
|
|
76
|
+
python -m build
|
|
77
|
+
|
|
78
|
+
# 发布到 TestPyPI(测试)
|
|
79
|
+
python -m twine upload --repository testpypi dist/*
|
|
80
|
+
|
|
81
|
+
# 发布到 PyPI(正式)
|
|
82
|
+
python -m twine upload dist/*
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## 目录结构
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
gogeo-client/
|
|
89
|
+
├── pyproject.toml # Python 包配置
|
|
90
|
+
├── agentos/
|
|
91
|
+
│ ├── __init__.py
|
|
92
|
+
│ └── cli.py # CLI 入口
|
|
93
|
+
├── app/ # 应用覆盖文件
|
|
94
|
+
├── desktop/ # 桌面快捷方式
|
|
95
|
+
├── icons/ # 桌面图标
|
|
96
|
+
├── wallpaper/ # 桌面壁纸
|
|
97
|
+
└── docker-compose.yml # 本地开发用 compose
|
|
98
|
+
```
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# AgentOS Client
|
|
2
|
+
|
|
3
|
+
AgentOS Client Docker 容器管理 CLI 工具。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install agentos
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 使用
|
|
12
|
+
|
|
13
|
+
### 首次安装/设置
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# 方式一:直接设置(从环境变量或交互式输入读取 key)
|
|
17
|
+
agentos setup
|
|
18
|
+
|
|
19
|
+
# 方式二:命令行指定 key
|
|
20
|
+
agentos setup --key "your-nuwax-saved-key"
|
|
21
|
+
|
|
22
|
+
# 方式三:先设置环境变量
|
|
23
|
+
export NUWAX_SAVED_KEY="your-key"
|
|
24
|
+
agentos setup
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
setup 流程:
|
|
28
|
+
1. 检查 Docker 是否安装,未安装则自动安装(仅 Linux)
|
|
29
|
+
2. 拉取 agentos-client 镜像
|
|
30
|
+
3. 生成 docker-compose.yml 到 `~/.agentos/`
|
|
31
|
+
4. 保存 key 到 `~/.agentos/.env`
|
|
32
|
+
|
|
33
|
+
### 日常管理
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
agentos start # 启动容器
|
|
37
|
+
agentos stop # 停止容器
|
|
38
|
+
agentos restart # 重启容器
|
|
39
|
+
agentos status # 查看容器状态
|
|
40
|
+
agentos logs # 查看最近 50 行日志
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 更新 key
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
agentos update-key "new-key-here"
|
|
47
|
+
agentos restart # 重启生效
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 重新部署
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
agentos redeploy # 删除旧配置,重新生成并重启
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## 发布到 PyPI
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# 安装构建工具
|
|
60
|
+
pip install build twine
|
|
61
|
+
|
|
62
|
+
# 构建
|
|
63
|
+
cd gogeo-client
|
|
64
|
+
python -m build
|
|
65
|
+
|
|
66
|
+
# 发布到 TestPyPI(测试)
|
|
67
|
+
python -m twine upload --repository testpypi dist/*
|
|
68
|
+
|
|
69
|
+
# 发布到 PyPI(正式)
|
|
70
|
+
python -m twine upload dist/*
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## 目录结构
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
gogeo-client/
|
|
77
|
+
├── pyproject.toml # Python 包配置
|
|
78
|
+
├── agentos/
|
|
79
|
+
│ ├── __init__.py
|
|
80
|
+
│ └── cli.py # CLI 入口
|
|
81
|
+
├── app/ # 应用覆盖文件
|
|
82
|
+
├── desktop/ # 桌面快捷方式
|
|
83
|
+
├── icons/ # 桌面图标
|
|
84
|
+
├── wallpaper/ # 桌面壁纸
|
|
85
|
+
└── docker-compose.yml # 本地开发用 compose
|
|
86
|
+
```
|
|
File without changes
|
|
@@ -0,0 +1,928 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
AgentOS CLI tool
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import os
|
|
8
|
+
import platform
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
import time
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
VERSION = "0.2.0"
|
|
15
|
+
IMAGE_NAME = "nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com/nuwax/nuwax-agent-client:latest"
|
|
16
|
+
CONTAINER_NAME = "agentos-client"
|
|
17
|
+
|
|
18
|
+
COLORS = {
|
|
19
|
+
"green": "\033[0;32m",
|
|
20
|
+
"yellow": "\033[1;33m",
|
|
21
|
+
"red": "\033[0;31m",
|
|
22
|
+
"cyan": "\033[0;36m",
|
|
23
|
+
"reset": "\033[0m",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def color(name, text):
|
|
28
|
+
return f"{COLORS.get(name, '')}{text}{COLORS['reset']}"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def info(text):
|
|
32
|
+
print(color("green", f"[INFO] {text}"))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def warn(text):
|
|
36
|
+
print(color("yellow", f"[WARN] {text}"))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def error(text):
|
|
40
|
+
print(color("red", f"[ERROR] {text}"))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ============================================================
|
|
44
|
+
# Docker mirrors (acceleration)
|
|
45
|
+
# ============================================================
|
|
46
|
+
|
|
47
|
+
DOCKER_MIRRORS = [
|
|
48
|
+
"https://docker.1ms.run",
|
|
49
|
+
"https://hub-mirror.c.163.com",
|
|
50
|
+
"https://docker.m.daocloud.io",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def configure_docker_mirrors():
|
|
55
|
+
"""配置 Docker 镜像加速器"""
|
|
56
|
+
system = platform.system()
|
|
57
|
+
info("正在检查 Docker 镜像加速配置...")
|
|
58
|
+
|
|
59
|
+
if system == "Linux":
|
|
60
|
+
_configure_linux_mirrors()
|
|
61
|
+
elif system == "Darwin":
|
|
62
|
+
_configure_darwin_mirrors()
|
|
63
|
+
elif system == "Windows":
|
|
64
|
+
_configure_windows_mirrors()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _configure_linux_mirrors():
|
|
68
|
+
daemon_json = Path("/etc/docker/daemon.json")
|
|
69
|
+
if daemon_json.exists():
|
|
70
|
+
import json
|
|
71
|
+
try:
|
|
72
|
+
existing = json.loads(daemon_json.read_text())
|
|
73
|
+
if "registry-mirrors" in existing:
|
|
74
|
+
info("Docker 镜像加速已配置")
|
|
75
|
+
return
|
|
76
|
+
except (json.JSONDecodeError, IOError):
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
import json
|
|
80
|
+
config = {"registry-mirrors": DOCKER_MIRRORS}
|
|
81
|
+
if daemon_json.exists():
|
|
82
|
+
try:
|
|
83
|
+
existing = json.loads(daemon_json.read_text())
|
|
84
|
+
existing.update(config)
|
|
85
|
+
config = existing
|
|
86
|
+
except (json.JSONDecodeError, IOError):
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
daemon_json.write_text(json.dumps(config, indent=2))
|
|
91
|
+
info("已写入 Docker 镜像加速配置")
|
|
92
|
+
subprocess.run(["sudo", "systemctl", "reload", "docker"], capture_output=True, text=True)
|
|
93
|
+
result = subprocess.run(["docker", "info"], capture_output=True, text=True, timeout=5)
|
|
94
|
+
if result.returncode != 0:
|
|
95
|
+
subprocess.run(["sudo", "systemctl", "restart", "docker"])
|
|
96
|
+
except PermissionError:
|
|
97
|
+
warn("无权限写入 /etc/docker/daemon.json,跳过镜像加速配置")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _configure_darwin_mirrors():
|
|
101
|
+
daemon_json = Path.home() / ".docker" / "daemon.json"
|
|
102
|
+
daemon_json.parent.mkdir(parents=True, exist_ok=True)
|
|
103
|
+
import json
|
|
104
|
+
if daemon_json.exists():
|
|
105
|
+
try:
|
|
106
|
+
existing = json.loads(daemon_json.read_text())
|
|
107
|
+
if "registry-mirrors" in existing:
|
|
108
|
+
info("Docker 镜像加速已配置")
|
|
109
|
+
return
|
|
110
|
+
existing.update({"registry-mirrors": DOCKER_MIRRORS})
|
|
111
|
+
config = existing
|
|
112
|
+
except (json.JSONDecodeError, IOError):
|
|
113
|
+
config = {"registry-mirrors": DOCKER_MIRRORS}
|
|
114
|
+
else:
|
|
115
|
+
config = {"registry-mirrors": DOCKER_MIRRORS}
|
|
116
|
+
|
|
117
|
+
daemon_json.write_text(json.dumps(config, indent=2))
|
|
118
|
+
info("已写入 Docker 镜像加速配置,请重启 Docker Desktop")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _configure_windows_mirrors():
|
|
122
|
+
import json
|
|
123
|
+
docker_config_dir = Path(os.environ.get("PROGRAMDATA", "")) / "Docker" / "config"
|
|
124
|
+
daemon_json = docker_config_dir / "daemon.json"
|
|
125
|
+
daemon_json_alt = docker_config_dir / "daemon" / "daemon.json"
|
|
126
|
+
|
|
127
|
+
for config_path in [daemon_json, daemon_json_alt]:
|
|
128
|
+
if config_path.exists():
|
|
129
|
+
try:
|
|
130
|
+
existing = json.loads(config_path.read_text())
|
|
131
|
+
if "registry-mirrors" in existing:
|
|
132
|
+
info("Docker 镜像加速已配置")
|
|
133
|
+
return
|
|
134
|
+
existing.update({"registry-mirrors": DOCKER_MIRRORS})
|
|
135
|
+
config = existing
|
|
136
|
+
except (json.JSONDecodeError, IOError):
|
|
137
|
+
config = {"registry-mirrors": DOCKER_MIRRORS}
|
|
138
|
+
config_path.write_text(json.dumps(config, indent=2))
|
|
139
|
+
info("已写入 Docker 镜像加速配置")
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
docker_config_dir.mkdir(parents=True, exist_ok=True)
|
|
143
|
+
daemon_json.write_text(json.dumps({"registry-mirrors": DOCKER_MIRRORS}, indent=2))
|
|
144
|
+
info("已写入 Docker 镜像加速配置")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# ============================================================
|
|
148
|
+
# Docker check / install
|
|
149
|
+
# ============================================================
|
|
150
|
+
|
|
151
|
+
def docker_installed():
|
|
152
|
+
"""检查 Docker 是否已安装并运行"""
|
|
153
|
+
try:
|
|
154
|
+
result = subprocess.run(
|
|
155
|
+
["docker", "--version"], capture_output=True, text=True, timeout=5
|
|
156
|
+
)
|
|
157
|
+
if result.returncode != 0:
|
|
158
|
+
return False
|
|
159
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
|
160
|
+
return False
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
result = subprocess.run(
|
|
164
|
+
["docker", "info"], capture_output=True, text=True, timeout=10
|
|
165
|
+
)
|
|
166
|
+
return result.returncode == 0
|
|
167
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
|
168
|
+
return False
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def check_docker():
|
|
172
|
+
"""检查 Docker,未安装则自动安装"""
|
|
173
|
+
try:
|
|
174
|
+
result = subprocess.run(
|
|
175
|
+
["docker", "--version"], capture_output=True, text=True, timeout=5
|
|
176
|
+
)
|
|
177
|
+
docker_binary_exists = result.returncode == 0
|
|
178
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
|
179
|
+
docker_binary_exists = False
|
|
180
|
+
|
|
181
|
+
if docker_installed():
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
if docker_binary_exists:
|
|
185
|
+
warn("Docker 已安装但服务未启动,正在尝试启动...")
|
|
186
|
+
_start_docker()
|
|
187
|
+
if docker_installed():
|
|
188
|
+
info(f"Docker 已启动: {subprocess.run(['docker', '--version'], capture_output=True, text=True).stdout.strip()}")
|
|
189
|
+
return
|
|
190
|
+
warn("Docker 服务启动失败,请手动检查")
|
|
191
|
+
sys.exit(1)
|
|
192
|
+
|
|
193
|
+
# Windows 特殊处理:检查 Docker Desktop 是否已安装(即使 docker 不在 PATH 中)
|
|
194
|
+
if platform.system() == "Windows":
|
|
195
|
+
if _check_docker_desktop_installed_windows():
|
|
196
|
+
warn("Docker Desktop 已安装但 docker 命令不可用,正在启动...")
|
|
197
|
+
_start_docker()
|
|
198
|
+
if docker_installed():
|
|
199
|
+
info(f"Docker 已启动: {subprocess.run(['docker', '--version'], capture_output=True, text=True).stdout.strip()}")
|
|
200
|
+
return
|
|
201
|
+
warn("Docker 服务启动失败,请手动检查")
|
|
202
|
+
sys.exit(1)
|
|
203
|
+
|
|
204
|
+
warn("未检测到 Docker,开始自动安装...")
|
|
205
|
+
install_docker()
|
|
206
|
+
|
|
207
|
+
if not docker_installed():
|
|
208
|
+
error("Docker 安装完成但服务未启动,请手动检查")
|
|
209
|
+
sys.exit(1)
|
|
210
|
+
|
|
211
|
+
info(f"Docker 已安装: {subprocess.run(['docker', '--version'], capture_output=True, text=True).stdout.strip()}")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _check_docker_desktop_installed_windows():
|
|
215
|
+
"""Windows 上检查 Docker Desktop 是否已安装"""
|
|
216
|
+
try:
|
|
217
|
+
result = subprocess.run(
|
|
218
|
+
["winget", "list", "--id", "Docker.DockerDesktop",
|
|
219
|
+
"--accept-source-agreements"],
|
|
220
|
+
capture_output=True, text=True
|
|
221
|
+
)
|
|
222
|
+
return "Docker.DockerDesktop" in result.stdout or "Docker Desktop" in result.stdout
|
|
223
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
|
224
|
+
return False
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _start_docker():
|
|
228
|
+
"""尝试启动 Docker 服务"""
|
|
229
|
+
system = platform.system()
|
|
230
|
+
if system == "Windows":
|
|
231
|
+
try:
|
|
232
|
+
subprocess.Popen(
|
|
233
|
+
["C:\\Program Files\\Docker\\Docker\\Docker Desktop.exe"],
|
|
234
|
+
creationflags=subprocess.CREATE_NO_WINDOW
|
|
235
|
+
)
|
|
236
|
+
except FileNotFoundError:
|
|
237
|
+
pass
|
|
238
|
+
_wait_for_docker_ready()
|
|
239
|
+
elif system == "Darwin":
|
|
240
|
+
subprocess.Popen(["open", "/Applications/Docker.app"])
|
|
241
|
+
_wait_for_docker_ready()
|
|
242
|
+
elif system == "Linux":
|
|
243
|
+
subprocess.run(["sudo", "systemctl", "start", "docker"])
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _wait_for_docker_ready(timeout=300):
|
|
247
|
+
"""轮询等待 Docker 就绪"""
|
|
248
|
+
start = time.time()
|
|
249
|
+
dots = 0
|
|
250
|
+
while time.time() - start < timeout:
|
|
251
|
+
try:
|
|
252
|
+
result = subprocess.run(
|
|
253
|
+
["docker", "info"],
|
|
254
|
+
capture_output=True, text=True, timeout=5
|
|
255
|
+
)
|
|
256
|
+
if result.returncode == 0:
|
|
257
|
+
print()
|
|
258
|
+
info("Docker 已就绪")
|
|
259
|
+
return
|
|
260
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
|
261
|
+
pass
|
|
262
|
+
|
|
263
|
+
dots += 1
|
|
264
|
+
if dots % 5 == 0:
|
|
265
|
+
elapsed = int(time.time() - start)
|
|
266
|
+
print(f"\r 等待中... ({elapsed}s)", end="", flush=True)
|
|
267
|
+
|
|
268
|
+
time.sleep(3)
|
|
269
|
+
|
|
270
|
+
print()
|
|
271
|
+
error(f"Docker 在 {timeout}s 内未就绪,请稍后重试")
|
|
272
|
+
sys.exit(1)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def install_docker():
|
|
276
|
+
"""自动安装 Docker"""
|
|
277
|
+
system = platform.system()
|
|
278
|
+
|
|
279
|
+
if system == "Windows":
|
|
280
|
+
_install_docker_windows()
|
|
281
|
+
elif system == "Darwin":
|
|
282
|
+
_install_docker_mac()
|
|
283
|
+
else:
|
|
284
|
+
_install_docker_linux()
|
|
285
|
+
|
|
286
|
+
# 启动服务(非 Windows)
|
|
287
|
+
if system != "Windows":
|
|
288
|
+
info("正在启动 Docker 服务...")
|
|
289
|
+
subprocess.run(["sudo", "systemctl", "start", "docker"])
|
|
290
|
+
subprocess.run(["sudo", "systemctl", "enable", "docker"])
|
|
291
|
+
_wait_for_docker_ready()
|
|
292
|
+
return
|
|
293
|
+
|
|
294
|
+
info("Docker 安装完成,等待服务就绪...")
|
|
295
|
+
_wait_for_docker_ready()
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _install_docker_windows():
|
|
299
|
+
# 先检查 winget 是否显示已安装(避免重复下载安装)
|
|
300
|
+
try:
|
|
301
|
+
result = subprocess.run(
|
|
302
|
+
["winget", "list", "--id", "Docker.DockerDesktop",
|
|
303
|
+
"--accept-source-agreements"],
|
|
304
|
+
capture_output=True, text=True
|
|
305
|
+
)
|
|
306
|
+
if "Docker.DockerDesktop" in result.stdout or "Docker Desktop" in result.stdout:
|
|
307
|
+
info("Docker Desktop 已安装,正在启动...")
|
|
308
|
+
_start_docker()
|
|
309
|
+
return
|
|
310
|
+
except FileNotFoundError:
|
|
311
|
+
pass
|
|
312
|
+
|
|
313
|
+
info("正在安装 Docker Desktop (Windows)...")
|
|
314
|
+
|
|
315
|
+
# 优先使用 winget
|
|
316
|
+
try:
|
|
317
|
+
result = subprocess.run(
|
|
318
|
+
["winget", "--version"], capture_output=True, text=True
|
|
319
|
+
)
|
|
320
|
+
if result.returncode == 0:
|
|
321
|
+
info("使用 winget 安装...")
|
|
322
|
+
rc = subprocess.run([
|
|
323
|
+
"winget", "install", "-e", "--id", "Docker.DockerDesktop",
|
|
324
|
+
"--accept-package-agreements", "--accept-source-agreements"
|
|
325
|
+
])
|
|
326
|
+
if rc.returncode == 0:
|
|
327
|
+
_start_docker()
|
|
328
|
+
return
|
|
329
|
+
warn("winget 安装失败,尝试直接下载...")
|
|
330
|
+
except FileNotFoundError:
|
|
331
|
+
pass
|
|
332
|
+
|
|
333
|
+
# 备选:PowerShell 直接下载安装
|
|
334
|
+
info("使用 PowerShell 下载安装...")
|
|
335
|
+
docker_installer_url = (
|
|
336
|
+
"https://desktop.docker.com/win/main/amd64/225177/Docker%20Desktop%20Installer.exe"
|
|
337
|
+
)
|
|
338
|
+
installer_path = os.path.join(
|
|
339
|
+
os.environ.get("TEMP", "."), "DockerDesktopInstaller.exe"
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
info(f"正在下载: {docker_installer_url}")
|
|
343
|
+
rc = subprocess.run([
|
|
344
|
+
"powershell", "-Command",
|
|
345
|
+
f"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; "
|
|
346
|
+
f"Invoke-WebRequest -Uri '{docker_installer_url}' -OutFile '{installer_path}'"
|
|
347
|
+
])
|
|
348
|
+
if rc.returncode != 0:
|
|
349
|
+
warn("下载失败,请手动安装 Docker Desktop:")
|
|
350
|
+
warn("https://www.docker.com/products/docker-desktop/")
|
|
351
|
+
sys.exit(1)
|
|
352
|
+
|
|
353
|
+
info("正在安装 Docker Desktop...")
|
|
354
|
+
subprocess.run([installer_path, "install", "--quiet", "--accept-license"])
|
|
355
|
+
_start_docker()
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _install_docker_mac():
|
|
359
|
+
info("正在安装 Docker Desktop (macOS)...")
|
|
360
|
+
|
|
361
|
+
try:
|
|
362
|
+
result = subprocess.run(
|
|
363
|
+
["brew", "--version"], capture_output=True, text=True
|
|
364
|
+
)
|
|
365
|
+
if result.returncode == 0:
|
|
366
|
+
info("使用 Homebrew 安装...")
|
|
367
|
+
subprocess.run(["brew", "install", "--cask", "docker"])
|
|
368
|
+
_start_docker()
|
|
369
|
+
return
|
|
370
|
+
except FileNotFoundError:
|
|
371
|
+
pass
|
|
372
|
+
|
|
373
|
+
info("Homebrew 不可用,直接下载 Docker Desktop...")
|
|
374
|
+
docker_dmg_url = "https://desktop.docker.com/mac/main/arm64/Docker.dmg"
|
|
375
|
+
dmg_path = "/tmp/Docker.dmg"
|
|
376
|
+
|
|
377
|
+
info(f"正在下载: {docker_dmg_url}")
|
|
378
|
+
subprocess.run(["curl", "-fsSL", docker_dmg_url, "-o", dmg_path])
|
|
379
|
+
|
|
380
|
+
info("正在挂载并安装...")
|
|
381
|
+
result = subprocess.run(
|
|
382
|
+
["hdiutil", "attach", dmg_path],
|
|
383
|
+
capture_output=True, text=True
|
|
384
|
+
)
|
|
385
|
+
mount_point = None
|
|
386
|
+
for line in result.stdout.splitlines():
|
|
387
|
+
if "Docker" in line and "/Volumes/" in line:
|
|
388
|
+
mount_point = line.split("/Volumes/")[-1].strip()
|
|
389
|
+
break
|
|
390
|
+
|
|
391
|
+
if mount_point:
|
|
392
|
+
subprocess.run(["cp", "-R", f"/Volumes/{mount_point}/Docker.app", "/Applications/"])
|
|
393
|
+
subprocess.run(["hdiutil", "detach", f"/Volumes/{mount_point}"])
|
|
394
|
+
|
|
395
|
+
os.remove(dmg_path)
|
|
396
|
+
_start_docker()
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _install_docker_linux():
|
|
400
|
+
try:
|
|
401
|
+
with open("/etc/os-release") as f:
|
|
402
|
+
os_release = f.read()
|
|
403
|
+
except FileNotFoundError:
|
|
404
|
+
os_release = ""
|
|
405
|
+
|
|
406
|
+
if "ubuntu" in os_release or "debian" in os_release:
|
|
407
|
+
_install_docker_debian()
|
|
408
|
+
elif "centos" in os_release or "rhel" in os_release or "rocky" in os_release or "almalinux" in os_release:
|
|
409
|
+
_install_docker_centos()
|
|
410
|
+
else:
|
|
411
|
+
_install_docker_generic()
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _install_docker_debian():
|
|
415
|
+
info("正在安装 Docker (Debian/Ubuntu)...")
|
|
416
|
+
_run_shell(["sudo", "apt-get", "update"])
|
|
417
|
+
_run_shell([
|
|
418
|
+
"sudo", "apt-get", "install", "-y",
|
|
419
|
+
"ca-certificates", "curl", "gnupg", "lsb-release",
|
|
420
|
+
])
|
|
421
|
+
_run_shell(["sudo", "install", "-m", "0755", "-d", "/etc/apt/keyrings"])
|
|
422
|
+
try:
|
|
423
|
+
_run_shell([
|
|
424
|
+
"sudo", "bash", "-c",
|
|
425
|
+
"curl -fsSL https://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg "
|
|
426
|
+
"| gpg --dearmor -o /etc/apt/keyrings/docker.gpg"
|
|
427
|
+
])
|
|
428
|
+
except RuntimeError:
|
|
429
|
+
_run_shell([
|
|
430
|
+
"sudo", "bash", "-c",
|
|
431
|
+
"curl -fsSL https://mirrors.aliyun.com/docker-ce/linux/debian/gpg "
|
|
432
|
+
"| gpg --dearmor -o /etc/apt/keyrings/docker.gpg"
|
|
433
|
+
])
|
|
434
|
+
_run_shell(["sudo", "chmod", "a+r", "/etc/apt/keyrings/docker.gpg"])
|
|
435
|
+
_run_shell([
|
|
436
|
+
"sudo", "bash", "-c",
|
|
437
|
+
"echo 'deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] "
|
|
438
|
+
"https://mirrors.aliyun.com/docker-ce/linux/ubuntu $(lsb_release -cs) stable' "
|
|
439
|
+
"> /etc/apt/sources.list.d/docker.list"
|
|
440
|
+
])
|
|
441
|
+
_run_shell(["sudo", "apt-get", "update"])
|
|
442
|
+
_run_shell([
|
|
443
|
+
"sudo", "apt-get", "install", "-y",
|
|
444
|
+
"docker-ce", "docker-ce-cli", "containerd.io", "docker-compose-plugin",
|
|
445
|
+
])
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _install_docker_centos():
|
|
449
|
+
info("正在安装 Docker (CentOS/RHEL)...")
|
|
450
|
+
_run_shell(["sudo", "yum", "install", "-y", "yum-utils"])
|
|
451
|
+
try:
|
|
452
|
+
_run_shell([
|
|
453
|
+
"sudo", "yum-config-manager", "--add-repo",
|
|
454
|
+
"https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo"
|
|
455
|
+
])
|
|
456
|
+
except RuntimeError:
|
|
457
|
+
_run_shell([
|
|
458
|
+
"sudo", "yum-config-manager", "--add-repo",
|
|
459
|
+
"https://download.docker.com/linux/centos/docker-ce.repo"
|
|
460
|
+
])
|
|
461
|
+
_run_shell([
|
|
462
|
+
"sudo", "yum", "install", "-y",
|
|
463
|
+
"docker-ce", "docker-ce-cli", "containerd.io", "docker-compose-plugin",
|
|
464
|
+
])
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def _install_docker_generic():
|
|
468
|
+
info("正在安装 Docker (通用脚本)...")
|
|
469
|
+
_run_shell(["sudo", "bash", "-c", "curl -fsSL https://get.docker.com | sh -s -- --mirror Aliyun"])
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def _run_shell(cmd, check=True):
|
|
473
|
+
"""执行 shell 命令(静默)"""
|
|
474
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
475
|
+
if check and result.returncode != 0:
|
|
476
|
+
raise RuntimeError(f"命令执行失败: {' '.join(cmd)}\n{result.stderr}")
|
|
477
|
+
return result
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
# ============================================================
|
|
481
|
+
# Docker compose
|
|
482
|
+
# ============================================================
|
|
483
|
+
|
|
484
|
+
def compose_command():
|
|
485
|
+
"""返回可用的 docker compose 命令"""
|
|
486
|
+
for cmd in [["docker", "compose"], ["docker-compose"]]:
|
|
487
|
+
try:
|
|
488
|
+
result = subprocess.run(
|
|
489
|
+
cmd + ["version"], capture_output=True, text=True, timeout=5
|
|
490
|
+
)
|
|
491
|
+
if result.returncode == 0:
|
|
492
|
+
return cmd
|
|
493
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
|
494
|
+
continue
|
|
495
|
+
return None
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def run_cmd(cmd, check=True):
|
|
499
|
+
"""执行 shell 命令(可见输出)"""
|
|
500
|
+
print(color("cyan", f"$ {' '.join(cmd)}"))
|
|
501
|
+
result = subprocess.run(cmd)
|
|
502
|
+
if check and result.returncode != 0:
|
|
503
|
+
raise RuntimeError(f"命令执行失败: {' '.join(cmd)}")
|
|
504
|
+
return result
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
# ============================================================
|
|
508
|
+
# Pull image with progress
|
|
509
|
+
# ============================================================
|
|
510
|
+
|
|
511
|
+
def pull_image():
|
|
512
|
+
"""拉取镜像(带进度显示)"""
|
|
513
|
+
DISPLAY_NAME = "gogeo-agentos:latest"
|
|
514
|
+
info(f"正在拉取镜像: {DISPLAY_NAME}")
|
|
515
|
+
print()
|
|
516
|
+
|
|
517
|
+
process = subprocess.Popen(
|
|
518
|
+
["docker", "pull", IMAGE_NAME],
|
|
519
|
+
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
|
520
|
+
text=True, bufsize=1
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
layers = {}
|
|
524
|
+
dots = 0
|
|
525
|
+
|
|
526
|
+
for line in process.stdout:
|
|
527
|
+
line = line.strip()
|
|
528
|
+
if not line:
|
|
529
|
+
continue
|
|
530
|
+
|
|
531
|
+
if ":" in line and any(kw in line for kw in [
|
|
532
|
+
"Downloading", "Extracting", "Pulling fs layer", "Waiting",
|
|
533
|
+
"Verifying Checksum", "Download complete", "Pull complete", "Already exists"
|
|
534
|
+
]):
|
|
535
|
+
parts = line.split(":", 1)
|
|
536
|
+
layer_id = parts[0].strip()
|
|
537
|
+
layers[layer_id] = parts[1].strip() if len(parts) > 1 else ""
|
|
538
|
+
elif line and not line.startswith("{"):
|
|
539
|
+
print(f" {line}")
|
|
540
|
+
|
|
541
|
+
dots += 1
|
|
542
|
+
if dots % 20 == 0:
|
|
543
|
+
_print_pull_progress(layers)
|
|
544
|
+
|
|
545
|
+
process.wait()
|
|
546
|
+
print()
|
|
547
|
+
_print_pull_progress(layers, final=True)
|
|
548
|
+
|
|
549
|
+
if process.returncode != 0:
|
|
550
|
+
error("镜像拉取失败")
|
|
551
|
+
sys.exit(1)
|
|
552
|
+
|
|
553
|
+
info("镜像拉取完成")
|
|
554
|
+
print()
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def _print_pull_progress(layers, final=False):
|
|
558
|
+
"""打印拉取进度"""
|
|
559
|
+
total = len(layers)
|
|
560
|
+
if total == 0:
|
|
561
|
+
return
|
|
562
|
+
|
|
563
|
+
completed = 0
|
|
564
|
+
for layer_id, status in layers.items():
|
|
565
|
+
if any(kw in status for kw in ["Pull complete", "Download complete", "Already exists"]):
|
|
566
|
+
completed += 1
|
|
567
|
+
|
|
568
|
+
bar_len = 30
|
|
569
|
+
filled = int(bar_len * completed / total)
|
|
570
|
+
bar = "█" * filled + "░" * (bar_len - filled)
|
|
571
|
+
sys.stdout.write(f"\r [{bar}] {completed}/{total} 层 ")
|
|
572
|
+
if final:
|
|
573
|
+
sys.stdout.write("\n")
|
|
574
|
+
sys.stdout.flush()
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def ensure_image():
|
|
578
|
+
"""确保镜像存在"""
|
|
579
|
+
result = subprocess.run(
|
|
580
|
+
["docker", "image", "inspect", IMAGE_NAME],
|
|
581
|
+
capture_output=True, text=True
|
|
582
|
+
)
|
|
583
|
+
if result.returncode != 0:
|
|
584
|
+
pull_image()
|
|
585
|
+
else:
|
|
586
|
+
info("镜像已存在,跳过拉取")
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
# ============================================================
|
|
590
|
+
# Config
|
|
591
|
+
# ============================================================
|
|
592
|
+
|
|
593
|
+
def get_config_dir():
|
|
594
|
+
"""获取配置目录"""
|
|
595
|
+
return Path.home() / ".agentos"
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def get_compose_file():
|
|
599
|
+
return get_config_dir() / "docker-compose.yml"
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def get_env_file():
|
|
603
|
+
return get_config_dir() / ".env"
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def get_saved_key():
|
|
607
|
+
"""获取 NUWAX_SAVED_KEY"""
|
|
608
|
+
key = os.environ.get("NUWAX_SAVED_KEY")
|
|
609
|
+
if key:
|
|
610
|
+
return key
|
|
611
|
+
|
|
612
|
+
env_file = get_env_file()
|
|
613
|
+
if env_file.exists():
|
|
614
|
+
for line in env_file.read_text().splitlines():
|
|
615
|
+
if line.startswith("NUWAX_SAVED_KEY="):
|
|
616
|
+
return line.split("=", 1)[1].strip()
|
|
617
|
+
|
|
618
|
+
return None
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def save_saved_key(key):
|
|
622
|
+
"""保存 key 到 .env"""
|
|
623
|
+
config_dir = get_config_dir()
|
|
624
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
625
|
+
|
|
626
|
+
env_file = get_env_file()
|
|
627
|
+
env_file.write_text(f"# AgentOS 配置\nNUWAX_SAVED_KEY={key}\n")
|
|
628
|
+
info(f"已保存 key 到 {env_file}")
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
# ============================================================
|
|
632
|
+
# Setup
|
|
633
|
+
# ============================================================
|
|
634
|
+
|
|
635
|
+
def setup(key=None, force=False):
|
|
636
|
+
"""首次安装/设置"""
|
|
637
|
+
print()
|
|
638
|
+
print("=" * 50)
|
|
639
|
+
print(" AgentOS 安装")
|
|
640
|
+
print("=" * 50)
|
|
641
|
+
print()
|
|
642
|
+
|
|
643
|
+
check_docker()
|
|
644
|
+
configure_docker_mirrors()
|
|
645
|
+
|
|
646
|
+
compose = compose_command()
|
|
647
|
+
if not compose:
|
|
648
|
+
error("未找到 docker compose 或 docker-compose 命令")
|
|
649
|
+
sys.exit(1)
|
|
650
|
+
|
|
651
|
+
info(f"使用 compose 命令: {' '.join(compose)}")
|
|
652
|
+
|
|
653
|
+
ensure_image()
|
|
654
|
+
|
|
655
|
+
if key:
|
|
656
|
+
saved_key = key
|
|
657
|
+
else:
|
|
658
|
+
saved_key = get_saved_key()
|
|
659
|
+
|
|
660
|
+
if not saved_key:
|
|
661
|
+
saved_key = input(color("yellow", "[输入] 请输入 NUWAX_SAVED_KEY: ")).strip()
|
|
662
|
+
if not saved_key:
|
|
663
|
+
error("key 不能为空")
|
|
664
|
+
sys.exit(1)
|
|
665
|
+
|
|
666
|
+
save_saved_key(saved_key)
|
|
667
|
+
|
|
668
|
+
deploy_compose(saved_key, force)
|
|
669
|
+
copy_resources(force)
|
|
670
|
+
|
|
671
|
+
print()
|
|
672
|
+
info("安装完成!")
|
|
673
|
+
info(f"配置文件: {get_config_dir()}")
|
|
674
|
+
print()
|
|
675
|
+
print(" 后续使用命令:")
|
|
676
|
+
print(" agentos start 启动容器")
|
|
677
|
+
print(" agentos stop 停止容器")
|
|
678
|
+
print(" agentos restart 重启容器")
|
|
679
|
+
print(" agentos status 查看状态")
|
|
680
|
+
print()
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
def copy_resources(force=False):
|
|
684
|
+
"""从包内复制资源文件到配置目录"""
|
|
685
|
+
config_dir = get_config_dir()
|
|
686
|
+
|
|
687
|
+
import gzip
|
|
688
|
+
import importlib.resources as pkg_resources
|
|
689
|
+
|
|
690
|
+
resource_files = [
|
|
691
|
+
("app/app.asar", True), # True = gzip 压缩
|
|
692
|
+
("desktop/gogeoclaw.desktop", False),
|
|
693
|
+
("desktop/terminal.desktop", False),
|
|
694
|
+
("desktop/vscode.desktop", False),
|
|
695
|
+
("desktop/chromium.desktop", False),
|
|
696
|
+
("icons/@nuwax-ainuwaclaw.png", False),
|
|
697
|
+
("wallpaper/gogeoDesk.jpg", False),
|
|
698
|
+
]
|
|
699
|
+
|
|
700
|
+
copied = 0
|
|
701
|
+
replaced = 0
|
|
702
|
+
for rel_path, is_gzipped in resource_files:
|
|
703
|
+
dest = config_dir / rel_path
|
|
704
|
+
|
|
705
|
+
try:
|
|
706
|
+
res = pkg_resources.files("agentos.resources") / rel_path
|
|
707
|
+
data = res.read_bytes()
|
|
708
|
+
if is_gzipped:
|
|
709
|
+
data = gzip.decompress(data)
|
|
710
|
+
except (FileNotFoundError, TypeError):
|
|
711
|
+
data = b""
|
|
712
|
+
|
|
713
|
+
if dest.exists() and not dest.is_dir():
|
|
714
|
+
if not force and dest.stat().st_size == len(data):
|
|
715
|
+
continue
|
|
716
|
+
if dest.stat().st_size < len(data):
|
|
717
|
+
replaced += 1
|
|
718
|
+
|
|
719
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
720
|
+
dest.write_bytes(data)
|
|
721
|
+
copied += 1
|
|
722
|
+
|
|
723
|
+
if replaced > 0:
|
|
724
|
+
info(f"已更新 {replaced} 个过时的资源文件")
|
|
725
|
+
if copied > 0 and replaced == 0:
|
|
726
|
+
info(f"已复制 {copied} 个资源文件到配置目录")
|
|
727
|
+
elif copied == 0:
|
|
728
|
+
info("资源文件已是最新版本")
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
def deploy_compose(key, force=False):
|
|
732
|
+
"""部署 docker-compose.yml"""
|
|
733
|
+
config_dir = get_config_dir()
|
|
734
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
735
|
+
|
|
736
|
+
compose_file = get_compose_file()
|
|
737
|
+
|
|
738
|
+
if compose_file.exists() and not force:
|
|
739
|
+
info(f"docker-compose.yml 已存在: {compose_file}")
|
|
740
|
+
return
|
|
741
|
+
|
|
742
|
+
content = f"""services:
|
|
743
|
+
agentos-client:
|
|
744
|
+
image: {IMAGE_NAME}
|
|
745
|
+
container_name: {CONTAINER_NAME}
|
|
746
|
+
restart: always
|
|
747
|
+
ports:
|
|
748
|
+
- "6080:6080"
|
|
749
|
+
volumes:
|
|
750
|
+
- agentos_home:/home/user
|
|
751
|
+
- ./desktop/gogeoclaw.desktop:/usr/share/applications/@nuwax-ainuwaclaw.desktop
|
|
752
|
+
- ./desktop/terminal.desktop:/usr/share/applications/user-terminal.desktop
|
|
753
|
+
- ./desktop/vscode.desktop:/usr/share/applications/user-vscode.desktop
|
|
754
|
+
- ./desktop/chromium.desktop:/usr/share/applications/chromium-browser.desktop
|
|
755
|
+
- ./icons/@nuwax-ainuwaclaw.png:/usr/share/icons/hicolor/512x512/apps/@nuwax-ainuwaclaw.png
|
|
756
|
+
- ./wallpaper/gogeoDesk.jpg:/usr/share/backgrounds/xfce/wallpaper.jpeg
|
|
757
|
+
|
|
758
|
+
environment:
|
|
759
|
+
- TZ=Asia/Shanghai
|
|
760
|
+
- NUWAX_SERVER_HOST=https://agentos.topprismdata.com/claw-api
|
|
761
|
+
- NUWAX_AGENT_PORT=60006
|
|
762
|
+
- NUWAX_FILE_SERVER_PORT=60005
|
|
763
|
+
- NUWAX_WORKSPACE_DIR=/home/user/nuwaxbot/workspace
|
|
764
|
+
- NUWAX_SAVED_KEY={key}
|
|
765
|
+
|
|
766
|
+
volumes:
|
|
767
|
+
agentos_home:
|
|
768
|
+
"""
|
|
769
|
+
|
|
770
|
+
compose_file.write_text(content)
|
|
771
|
+
info(f"已生成 docker-compose.yml: {compose_file}")
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
# ============================================================
|
|
775
|
+
# Commands
|
|
776
|
+
# ============================================================
|
|
777
|
+
|
|
778
|
+
def cmd_start():
|
|
779
|
+
compose = compose_command()
|
|
780
|
+
if not compose:
|
|
781
|
+
error("未找到 docker compose")
|
|
782
|
+
sys.exit(1)
|
|
783
|
+
|
|
784
|
+
compose_file = get_compose_file()
|
|
785
|
+
if not compose_file.exists():
|
|
786
|
+
warn("未找到配置,请先运行: agentos setup")
|
|
787
|
+
sys.exit(1)
|
|
788
|
+
|
|
789
|
+
info("启动 AgentOS Client...")
|
|
790
|
+
run_cmd(compose + ["-f", str(compose_file), "up", "-d"])
|
|
791
|
+
|
|
792
|
+
print()
|
|
793
|
+
info("容器已启动")
|
|
794
|
+
print()
|
|
795
|
+
print(" 访问地址: http://localhost:6080")
|
|
796
|
+
print()
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
def cmd_stop():
|
|
800
|
+
compose = compose_command()
|
|
801
|
+
if not compose:
|
|
802
|
+
error("未找到 docker compose")
|
|
803
|
+
sys.exit(1)
|
|
804
|
+
|
|
805
|
+
compose_file = get_compose_file()
|
|
806
|
+
if not compose_file.exists():
|
|
807
|
+
warn("未找到配置")
|
|
808
|
+
sys.exit(1)
|
|
809
|
+
|
|
810
|
+
info("停止 AgentOS Client...")
|
|
811
|
+
run_cmd(compose + ["-f", str(compose_file), "down"])
|
|
812
|
+
info("容器已停止")
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
def cmd_restart():
|
|
816
|
+
cmd_stop()
|
|
817
|
+
print()
|
|
818
|
+
cmd_start()
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
def cmd_status():
|
|
822
|
+
result = subprocess.run(
|
|
823
|
+
["docker", "ps", "-a", "--filter", f"name={CONTAINER_NAME}",
|
|
824
|
+
"--format", "table {{.Names}}\t{{.Status}}\t{{.Ports}}"],
|
|
825
|
+
capture_output=True, text=True
|
|
826
|
+
)
|
|
827
|
+
print(result.stdout)
|
|
828
|
+
|
|
829
|
+
if CONTAINER_NAME not in result.stdout:
|
|
830
|
+
warn("容器不存在,请先运行: agentos setup")
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
def cmd_logs():
|
|
834
|
+
result = subprocess.run(
|
|
835
|
+
["docker", "logs", "--tail", "50", CONTAINER_NAME],
|
|
836
|
+
capture_output=True, text=True
|
|
837
|
+
)
|
|
838
|
+
print(result.stdout)
|
|
839
|
+
print(result.stderr)
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
def cmd_update_key(new_key):
|
|
843
|
+
"""更新 key"""
|
|
844
|
+
save_saved_key(new_key)
|
|
845
|
+
|
|
846
|
+
compose_file = get_compose_file()
|
|
847
|
+
if compose_file.exists():
|
|
848
|
+
content = compose_file.read_text()
|
|
849
|
+
content = content.replace(
|
|
850
|
+
"NUWAX_SAVED_KEY=",
|
|
851
|
+
f"NUWAX_SAVED_KEY={new_key}"
|
|
852
|
+
)
|
|
853
|
+
compose_file.write_text(content)
|
|
854
|
+
info("已更新 docker-compose.yml 中的 key")
|
|
855
|
+
|
|
856
|
+
info("如需生效,请运行: agentos restart")
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
def cmd_redeploy():
|
|
860
|
+
"""重新部署(删除旧配置重新生成)"""
|
|
861
|
+
compose_file = get_compose_file()
|
|
862
|
+
if compose_file.exists():
|
|
863
|
+
compose_file.unlink()
|
|
864
|
+
info("已删除旧配置")
|
|
865
|
+
|
|
866
|
+
key = get_saved_key()
|
|
867
|
+
if key:
|
|
868
|
+
setup(key=key, force=True)
|
|
869
|
+
else:
|
|
870
|
+
warn("未找到已保存的 key,请先运行: agentos setup")
|
|
871
|
+
sys.exit(1)
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
# ============================================================
|
|
875
|
+
# CLI
|
|
876
|
+
# ============================================================
|
|
877
|
+
|
|
878
|
+
def main():
|
|
879
|
+
parser = argparse.ArgumentParser(
|
|
880
|
+
prog="agentos",
|
|
881
|
+
description="AgentOS Client Docker 容器管理工具",
|
|
882
|
+
)
|
|
883
|
+
parser.add_argument(
|
|
884
|
+
"--version", action="version", version=f"agentos {VERSION}"
|
|
885
|
+
)
|
|
886
|
+
|
|
887
|
+
subparsers = parser.add_subparsers(dest="command", help="命令")
|
|
888
|
+
|
|
889
|
+
p_setup = subparsers.add_parser("setup", help="首次安装/设置")
|
|
890
|
+
p_setup.add_argument("--key", help="NUWAX_SAVED_KEY")
|
|
891
|
+
p_setup.add_argument("--force", action="store_true", help="强制重新生成配置")
|
|
892
|
+
|
|
893
|
+
subparsers.add_parser("start", help="启动容器")
|
|
894
|
+
subparsers.add_parser("stop", help="停止容器")
|
|
895
|
+
subparsers.add_parser("restart", help="重启容器")
|
|
896
|
+
subparsers.add_parser("status", help="查看容器状态")
|
|
897
|
+
subparsers.add_parser("logs", help="查看容器日志")
|
|
898
|
+
|
|
899
|
+
p_key = subparsers.add_parser("update-key", help="更新 NUWAX_SAVED_KEY")
|
|
900
|
+
p_key.add_argument("key", help="新的 key")
|
|
901
|
+
|
|
902
|
+
subparsers.add_parser("redeploy", help="重新部署(重新生成配置并重启)")
|
|
903
|
+
|
|
904
|
+
args = parser.parse_args()
|
|
905
|
+
|
|
906
|
+
if args.command == "setup":
|
|
907
|
+
setup(key=args.key, force=args.force)
|
|
908
|
+
elif args.command == "start":
|
|
909
|
+
cmd_start()
|
|
910
|
+
elif args.command == "stop":
|
|
911
|
+
cmd_stop()
|
|
912
|
+
elif args.command == "restart":
|
|
913
|
+
cmd_restart()
|
|
914
|
+
elif args.command == "status":
|
|
915
|
+
cmd_status()
|
|
916
|
+
elif args.command == "logs":
|
|
917
|
+
cmd_logs()
|
|
918
|
+
elif args.command == "update-key":
|
|
919
|
+
cmd_update_key(args.key)
|
|
920
|
+
elif args.command == "redeploy":
|
|
921
|
+
cmd_redeploy()
|
|
922
|
+
else:
|
|
923
|
+
parser.print_help()
|
|
924
|
+
sys.exit(1)
|
|
925
|
+
|
|
926
|
+
|
|
927
|
+
if __name__ == "__main__":
|
|
928
|
+
main()
|
|
File without changes
|
|
File without changes
|
|
Binary file
|
|
File without changes
|
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
Binary file
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gogeo-client-agentos
|
|
3
|
+
Version: 0.2.2
|
|
4
|
+
Summary: AgentOS CLI tool for managing the AgentOS Client Docker container
|
|
5
|
+
Author-email: agentos-team <agentos@example.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/agentos/client
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.8
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# AgentOS Client
|
|
14
|
+
|
|
15
|
+
AgentOS Client Docker 容器管理 CLI 工具。
|
|
16
|
+
|
|
17
|
+
## 安装
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install agentos
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## 使用
|
|
24
|
+
|
|
25
|
+
### 首次安装/设置
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# 方式一:直接设置(从环境变量或交互式输入读取 key)
|
|
29
|
+
agentos setup
|
|
30
|
+
|
|
31
|
+
# 方式二:命令行指定 key
|
|
32
|
+
agentos setup --key "your-nuwax-saved-key"
|
|
33
|
+
|
|
34
|
+
# 方式三:先设置环境变量
|
|
35
|
+
export NUWAX_SAVED_KEY="your-key"
|
|
36
|
+
agentos setup
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
setup 流程:
|
|
40
|
+
1. 检查 Docker 是否安装,未安装则自动安装(仅 Linux)
|
|
41
|
+
2. 拉取 agentos-client 镜像
|
|
42
|
+
3. 生成 docker-compose.yml 到 `~/.agentos/`
|
|
43
|
+
4. 保存 key 到 `~/.agentos/.env`
|
|
44
|
+
|
|
45
|
+
### 日常管理
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
agentos start # 启动容器
|
|
49
|
+
agentos stop # 停止容器
|
|
50
|
+
agentos restart # 重启容器
|
|
51
|
+
agentos status # 查看容器状态
|
|
52
|
+
agentos logs # 查看最近 50 行日志
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 更新 key
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
agentos update-key "new-key-here"
|
|
59
|
+
agentos restart # 重启生效
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 重新部署
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
agentos redeploy # 删除旧配置,重新生成并重启
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## 发布到 PyPI
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# 安装构建工具
|
|
72
|
+
pip install build twine
|
|
73
|
+
|
|
74
|
+
# 构建
|
|
75
|
+
cd gogeo-client
|
|
76
|
+
python -m build
|
|
77
|
+
|
|
78
|
+
# 发布到 TestPyPI(测试)
|
|
79
|
+
python -m twine upload --repository testpypi dist/*
|
|
80
|
+
|
|
81
|
+
# 发布到 PyPI(正式)
|
|
82
|
+
python -m twine upload dist/*
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## 目录结构
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
gogeo-client/
|
|
89
|
+
├── pyproject.toml # Python 包配置
|
|
90
|
+
├── agentos/
|
|
91
|
+
│ ├── __init__.py
|
|
92
|
+
│ └── cli.py # CLI 入口
|
|
93
|
+
├── app/ # 应用覆盖文件
|
|
94
|
+
├── desktop/ # 桌面快捷方式
|
|
95
|
+
├── icons/ # 桌面图标
|
|
96
|
+
├── wallpaper/ # 桌面壁纸
|
|
97
|
+
└── docker-compose.yml # 本地开发用 compose
|
|
98
|
+
```
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
agentos/__init__.py
|
|
4
|
+
agentos/cli.py
|
|
5
|
+
agentos/resources/__init__.py
|
|
6
|
+
agentos/resources/app/__init__.py
|
|
7
|
+
agentos/resources/app/app.asar
|
|
8
|
+
agentos/resources/desktop/__init__.py
|
|
9
|
+
agentos/resources/desktop/chromium.desktop
|
|
10
|
+
agentos/resources/desktop/gogeoclaw.desktop
|
|
11
|
+
agentos/resources/desktop/terminal.desktop
|
|
12
|
+
agentos/resources/desktop/vscode.desktop
|
|
13
|
+
agentos/resources/icons/@nuwax-ainuwaclaw.png
|
|
14
|
+
agentos/resources/icons/__init__.py
|
|
15
|
+
agentos/resources/wallpaper/__init__.py
|
|
16
|
+
agentos/resources/wallpaper/gogeoDesk.jpg
|
|
17
|
+
gogeo_client_agentos.egg-info/PKG-INFO
|
|
18
|
+
gogeo_client_agentos.egg-info/SOURCES.txt
|
|
19
|
+
gogeo_client_agentos.egg-info/dependency_links.txt
|
|
20
|
+
gogeo_client_agentos.egg-info/entry_points.txt
|
|
21
|
+
gogeo_client_agentos.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
agentos
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "gogeo-client-agentos"
|
|
7
|
+
version = "0.2.2"
|
|
8
|
+
description = "AgentOS CLI tool for managing the AgentOS Client Docker container"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.8"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "agentos-team", email = "agentos@example.com"},
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
agentos = "agentos.cli:main"
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
"Homepage" = "https://github.com/agentos/client"
|
|
25
|
+
|
|
26
|
+
[tool.setuptools.packages.find]
|
|
27
|
+
include = ["agentos*"]
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.package-data]
|
|
30
|
+
agentos = ["resources/**/*"]
|