@ian2018cs/agenthub 0.1.0 → 0.1.2
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.
- package/dist/assets/index-BITEl9tD.js +154 -0
- package/dist/assets/index-BWbEF217.css +32 -0
- package/dist/assets/{vendor-icons-CJV4dnDL.js → vendor-icons-q7OlK-Uk.js} +76 -61
- package/dist/index.html +3 -3
- package/package.json +2 -1
- package/server/builtin-skills/.gitkeep +0 -0
- package/server/builtin-skills/deploy-frontend/SKILL.md +220 -0
- package/server/builtin-skills/deploy-frontend/scripts/cleanup.py +158 -0
- package/server/builtin-skills/deploy-frontend/scripts/deploy.py +216 -0
- package/server/cli.js +7 -6
- package/server/database/db.js +262 -17
- package/server/database/init.sql +35 -3
- package/server/middleware/auth.js +5 -4
- package/server/routes/admin.js +113 -2
- package/server/routes/auth.js +113 -38
- package/server/routes/skills.js +8 -0
- package/server/services/builtin-skills.js +147 -0
- package/server/services/email.js +90 -0
- package/server/services/user-directories.js +4 -0
- package/dist/assets/index-B4ru3EJb.css +0 -32
- package/dist/assets/index-DDFuyrpY.js +0 -154
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: deploy-frontend
|
|
3
|
+
description: 将生成的 HTML 或前端页面部署到 Docker nginx 容器。用于快速部署和预览前端项目,自动配置端口隔离,返回可访问的内网 URL。使用场景:(1) 部署刚生成的 HTML/React/Vue 等前端项目 (2) 需要让用户通过浏览器查看前端页面 (3) 需要为不同项目分配独立的访问端口 (4) 清理或列出已部署的项目
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Deploy Frontend
|
|
7
|
+
|
|
8
|
+
将前端项目部署到共享的 nginx Docker 容器,通过不同端口和配置文件实现项目隔离。
|
|
9
|
+
|
|
10
|
+
## 配置
|
|
11
|
+
|
|
12
|
+
默认 nginx 目录已配置为:`/home/xubuntu001/AI/nginx`
|
|
13
|
+
|
|
14
|
+
如需修改,编辑脚本文件中的 `DEFAULT_NGINX_BASE_DIR` 变量:
|
|
15
|
+
- `scripts/deploy.py` 第 14 行
|
|
16
|
+
- `scripts/cleanup.py` 第 10 行
|
|
17
|
+
|
|
18
|
+
## 工作原理
|
|
19
|
+
|
|
20
|
+
- **单容器架构**:所有项目共享一个 nginx 容器(节省资源)
|
|
21
|
+
- **配置隔离**:每个项目在 `config/conf.d/` 下有独立的 `.conf` 文件
|
|
22
|
+
- **端口隔离**:自动分配不同端口(从 8080 开始递增)
|
|
23
|
+
- **目录隔离**:每个项目的文件存放在 `html/project-{timestamp}/`
|
|
24
|
+
|
|
25
|
+
## 部署前端项目
|
|
26
|
+
|
|
27
|
+
使用 `scripts/deploy.py` 部署项目:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
python scripts/deploy.py <前端项目目录>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**参数说明:**
|
|
34
|
+
- `<前端项目目录>`:包含 index.html 等前端文件的目录(必需)
|
|
35
|
+
|
|
36
|
+
默认使用 `/home/xubuntu001/AI/nginx` 作为 nginx 基础目录,无需额外指定。
|
|
37
|
+
|
|
38
|
+
**脚本执行流程:**
|
|
39
|
+
1. 生成唯一的项目 ID(project-{timestamp})
|
|
40
|
+
2. 查找可用端口(从 8080 开始)
|
|
41
|
+
3. 复制前端文件到 `html/{project_id}/`
|
|
42
|
+
4. 在 `config/conf.d/` 创建 nginx 配置文件
|
|
43
|
+
5. 重新加载 nginx(如果容器未运行则启动)
|
|
44
|
+
6. 返回访问 URL(内网IP:端口)
|
|
45
|
+
7. 保存部署信息到 `deployments/{project_id}.json`
|
|
46
|
+
|
|
47
|
+
**示例:**
|
|
48
|
+
```bash
|
|
49
|
+
# 部署前端项目(使用默认 nginx 目录)
|
|
50
|
+
python .claude/skills/deploy-frontend/scripts/deploy.py ./my-frontend-app
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**输出示例:**
|
|
54
|
+
```
|
|
55
|
+
✅ 部署成功!
|
|
56
|
+
项目 ID: project-1738151234
|
|
57
|
+
端口: 8080
|
|
58
|
+
访问地址: http://192.168.1.100:8080
|
|
59
|
+
HTML 目录: /path/to/nginx/html/project-1738151234
|
|
60
|
+
配置文件: /path/to/nginx/config/conf.d/project-1738151234.conf
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## 管理部署
|
|
64
|
+
|
|
65
|
+
使用 `scripts/cleanup.py` 管理已部署的项目。
|
|
66
|
+
|
|
67
|
+
### 列出所有部署
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
python scripts/cleanup.py list
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### 清理指定项目
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
python scripts/cleanup.py <project_id>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
会删除:
|
|
80
|
+
- HTML 目录
|
|
81
|
+
- nginx 配置文件
|
|
82
|
+
- 部署信息文件
|
|
83
|
+
|
|
84
|
+
并重新加载 nginx 配置。
|
|
85
|
+
|
|
86
|
+
### 清理所有项目
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
python scripts/cleanup.py all
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## 目录结构
|
|
93
|
+
|
|
94
|
+
部署后的 nginx 基础目录结构:
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
nginx-base/
|
|
98
|
+
├── docker-compose.yml # nginx 容器配置
|
|
99
|
+
├── html/ # 前端文件
|
|
100
|
+
│ ├── project-1738151234/ # 项目1
|
|
101
|
+
│ │ ├── index.html
|
|
102
|
+
│ │ └── ...
|
|
103
|
+
│ └── project-1738151456/ # 项目2
|
|
104
|
+
│ ├── index.html
|
|
105
|
+
│ └── ...
|
|
106
|
+
├── config/
|
|
107
|
+
│ └── conf.d/ # nginx 配置
|
|
108
|
+
│ ├── project-1738151234.conf
|
|
109
|
+
│ └── project-1738151456.conf
|
|
110
|
+
├── logs/ # nginx 日志
|
|
111
|
+
└── deployments/ # 部署信息
|
|
112
|
+
├── project-1738151234.json
|
|
113
|
+
└── project-1738151456.json
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## 使用流程
|
|
117
|
+
|
|
118
|
+
### 典型工作流
|
|
119
|
+
|
|
120
|
+
1. **生成前端代码**:创建 HTML/React/Vue 项目
|
|
121
|
+
2. **调用部署脚本**:
|
|
122
|
+
```python
|
|
123
|
+
from pathlib import Path
|
|
124
|
+
import subprocess
|
|
125
|
+
|
|
126
|
+
result = subprocess.run([
|
|
127
|
+
"python",
|
|
128
|
+
str(Path.home() / ".claude/skills/deploy-frontend/scripts/deploy.py"),
|
|
129
|
+
"./frontend-output"
|
|
130
|
+
], capture_output=True, text=True)
|
|
131
|
+
|
|
132
|
+
print(result.stdout)
|
|
133
|
+
```
|
|
134
|
+
3. **提取访问 URL**:从输出中获取 URL 返回给用户
|
|
135
|
+
4. **提醒用户访问**:告知用户可以通过浏览器访问该 URL
|
|
136
|
+
|
|
137
|
+
### 在对话中使用
|
|
138
|
+
|
|
139
|
+
当用户要求生成前端页面时:
|
|
140
|
+
|
|
141
|
+
1. 生成前端代码(HTML/CSS/JS 等)
|
|
142
|
+
2. 将文件写入临时目录
|
|
143
|
+
3. 调用 deploy.py 部署
|
|
144
|
+
4. 将访问 URL 返回给用户
|
|
145
|
+
|
|
146
|
+
示例代码模式:
|
|
147
|
+
```python
|
|
148
|
+
# 1. 创建输出目录
|
|
149
|
+
output_dir = Path("/tmp/frontend-{timestamp}")
|
|
150
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
151
|
+
|
|
152
|
+
# 2. 写入前端文件
|
|
153
|
+
(output_dir / "index.html").write_text(html_content)
|
|
154
|
+
(output_dir / "style.css").write_text(css_content)
|
|
155
|
+
|
|
156
|
+
# 3. 部署
|
|
157
|
+
deploy_script = Path.home() / ".claude/skills/deploy-frontend/scripts/deploy.py"
|
|
158
|
+
result = subprocess.run(
|
|
159
|
+
["python", str(deploy_script), str(output_dir)],
|
|
160
|
+
capture_output=True,
|
|
161
|
+
text=True
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# 4. 提取 URL(从输出中解析)
|
|
165
|
+
for line in result.stdout.split('\n'):
|
|
166
|
+
if line.startswith('访问地址:'):
|
|
167
|
+
url = line.split(':', 1)[1].strip()
|
|
168
|
+
print(f"您的前端页面已部署: {url}")
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## 前置要求
|
|
172
|
+
|
|
173
|
+
- Docker 和 docker-compose 已安装
|
|
174
|
+
- nginx 基础目录位于 `/home/xubuntu001/AI/nginx`
|
|
175
|
+
- docker-compose.yml 使用 `network_mode: host`
|
|
176
|
+
- Python 3.6+
|
|
177
|
+
|
|
178
|
+
## Docker 权限处理
|
|
179
|
+
|
|
180
|
+
脚本已内置自动权限处理机制:
|
|
181
|
+
|
|
182
|
+
1. **自动检测权限**:脚本会首先尝试直接运行 docker 命令
|
|
183
|
+
2. **自动降级处理**:如果遇到权限错误,会自动使用 `sg docker -c` 运行
|
|
184
|
+
3. **无需手动干预**:用户无需关心是否在 docker 组中
|
|
185
|
+
|
|
186
|
+
**注意**:如果您刚刚被添加到 docker 组,可能需要重新登录或使用以下命令激活组权限:
|
|
187
|
+
```bash
|
|
188
|
+
newgrp docker
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## 更新日志
|
|
192
|
+
|
|
193
|
+
### v1.1 (2026-01-30)
|
|
194
|
+
- ✅ 修复正则表达式转义警告
|
|
195
|
+
- ✅ 添加自动 Docker 权限处理(`run_docker_command` 函数)
|
|
196
|
+
- ✅ 使用 `docker compose` 替代旧版 `docker-compose` 命令
|
|
197
|
+
- ✅ deploy.py 和 cleanup.py 都支持自动权限处理
|
|
198
|
+
|
|
199
|
+
### v1.0
|
|
200
|
+
- 初始版本:支持多项目部署和端口隔离
|
|
201
|
+
|
|
202
|
+
## 故障排查
|
|
203
|
+
|
|
204
|
+
**容器未启动**:脚本会自动执行 `docker compose up -d`
|
|
205
|
+
|
|
206
|
+
**端口冲突**:脚本会自动查找可用端口(8080-8179 范围)
|
|
207
|
+
|
|
208
|
+
**权限问题**:脚本会自动使用 `sg docker -c` 处理权限问题
|
|
209
|
+
|
|
210
|
+
**配置未生效**:手动重新加载 nginx:
|
|
211
|
+
```bash
|
|
212
|
+
docker exec nginx-web nginx -s reload
|
|
213
|
+
# 或如果有权限问题:
|
|
214
|
+
sg docker -c "docker exec nginx-web nginx -s reload"
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**查看 nginx 日志**:
|
|
218
|
+
```bash
|
|
219
|
+
docker logs nginx-web
|
|
220
|
+
```
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
清理部署的前端项目
|
|
4
|
+
"""
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import shutil
|
|
9
|
+
import json
|
|
10
|
+
|
|
11
|
+
# 默认 nginx 基础目录(与 deploy.py 保持一致)
|
|
12
|
+
DEFAULT_NGINX_BASE_DIR = "/home/xubuntu001/AI/nginx"
|
|
13
|
+
|
|
14
|
+
def run_docker_command(cmd, **kwargs):
|
|
15
|
+
"""
|
|
16
|
+
运行 docker 命令,自动处理权限问题
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
cmd: 命令列表
|
|
20
|
+
**kwargs: 传递给 subprocess.run 的其他参数
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
subprocess.CompletedProcess 对象
|
|
24
|
+
"""
|
|
25
|
+
try:
|
|
26
|
+
# 首先尝试直接运行
|
|
27
|
+
return subprocess.run(cmd, **kwargs)
|
|
28
|
+
except (subprocess.CalledProcessError, PermissionError) as e:
|
|
29
|
+
# 如果失败,尝试使用 sg docker -c 运行
|
|
30
|
+
if 'check' in kwargs:
|
|
31
|
+
del kwargs['check'] # sg 会处理 check
|
|
32
|
+
|
|
33
|
+
# 将命令转换为 sg docker -c 格式
|
|
34
|
+
cmd_str = ' '.join(str(arg) for arg in cmd)
|
|
35
|
+
sg_cmd = ['sg', 'docker', '-c', cmd_str]
|
|
36
|
+
|
|
37
|
+
return subprocess.run(sg_cmd, **kwargs)
|
|
38
|
+
|
|
39
|
+
def cleanup_deployment(project_id, nginx_base_dir=None):
|
|
40
|
+
"""
|
|
41
|
+
清理指定部署
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
project_id: 项目 ID
|
|
45
|
+
nginx_base_dir: nginx 基础目录,默认为 DEFAULT_NGINX_BASE_DIR
|
|
46
|
+
"""
|
|
47
|
+
if nginx_base_dir is None:
|
|
48
|
+
nginx_base_dir = Path(DEFAULT_NGINX_BASE_DIR)
|
|
49
|
+
else:
|
|
50
|
+
nginx_base_dir = Path(nginx_base_dir)
|
|
51
|
+
|
|
52
|
+
deployments_dir = nginx_base_dir / "deployments"
|
|
53
|
+
info_file = deployments_dir / f"{project_id}.json"
|
|
54
|
+
|
|
55
|
+
if not info_file.exists():
|
|
56
|
+
print(f"❌ 项目不存在: {project_id}")
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
with open(info_file) as f:
|
|
61
|
+
info = json.load(f)
|
|
62
|
+
|
|
63
|
+
# 删除 HTML 目录
|
|
64
|
+
html_dir = Path(info['html_dir'])
|
|
65
|
+
if html_dir.exists():
|
|
66
|
+
print(f"删除 HTML 目录: {html_dir}")
|
|
67
|
+
shutil.rmtree(html_dir)
|
|
68
|
+
|
|
69
|
+
# 删除 nginx 配置文件
|
|
70
|
+
conf_file = Path(info['conf_file'])
|
|
71
|
+
if conf_file.exists():
|
|
72
|
+
print(f"删除配置文件: {conf_file}")
|
|
73
|
+
conf_file.unlink()
|
|
74
|
+
|
|
75
|
+
# 重新加载 nginx
|
|
76
|
+
print("重新加载 nginx 配置...")
|
|
77
|
+
run_docker_command(
|
|
78
|
+
["docker", "exec", "nginx-web", "nginx", "-s", "reload"],
|
|
79
|
+
check=True
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# 删除部署信息文件
|
|
83
|
+
info_file.unlink()
|
|
84
|
+
|
|
85
|
+
print(f"✅ 清理完成: {project_id}")
|
|
86
|
+
return True
|
|
87
|
+
|
|
88
|
+
except Exception as e:
|
|
89
|
+
print(f"❌ 清理失败: {e}")
|
|
90
|
+
import traceback
|
|
91
|
+
traceback.print_exc()
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
def list_deployments(nginx_base_dir=None):
|
|
95
|
+
"""列出所有部署"""
|
|
96
|
+
if nginx_base_dir is None:
|
|
97
|
+
nginx_base_dir = Path(DEFAULT_NGINX_BASE_DIR)
|
|
98
|
+
else:
|
|
99
|
+
nginx_base_dir = Path(nginx_base_dir)
|
|
100
|
+
|
|
101
|
+
deployments_dir = nginx_base_dir / "deployments"
|
|
102
|
+
|
|
103
|
+
if not deployments_dir.exists() or not list(deployments_dir.glob("*.json")):
|
|
104
|
+
print("没有找到部署")
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
print("\n当前部署:")
|
|
108
|
+
print("-" * 80)
|
|
109
|
+
|
|
110
|
+
for info_file in sorted(deployments_dir.glob("*.json")):
|
|
111
|
+
with open(info_file) as f:
|
|
112
|
+
info = json.load(f)
|
|
113
|
+
|
|
114
|
+
print(f"项目 ID: {info['project_id']}")
|
|
115
|
+
print(f" 部署时间: {info.get('deployed_at', 'N/A')}")
|
|
116
|
+
print(f" 访问地址: {info['url']}")
|
|
117
|
+
print(f" HTML 目录: {info['html_dir']}")
|
|
118
|
+
print()
|
|
119
|
+
|
|
120
|
+
def cleanup_all(nginx_base_dir=None):
|
|
121
|
+
"""清理所有部署"""
|
|
122
|
+
if nginx_base_dir is None:
|
|
123
|
+
nginx_base_dir = Path(DEFAULT_NGINX_BASE_DIR)
|
|
124
|
+
else:
|
|
125
|
+
nginx_base_dir = Path(nginx_base_dir)
|
|
126
|
+
|
|
127
|
+
deployments_dir = nginx_base_dir / "deployments"
|
|
128
|
+
|
|
129
|
+
if not deployments_dir.exists():
|
|
130
|
+
print("没有找到部署")
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
for info_file in deployments_dir.glob("*.json"):
|
|
134
|
+
project_id = info_file.stem
|
|
135
|
+
print(f"\n清理项目: {project_id}")
|
|
136
|
+
cleanup_deployment(project_id, nginx_base_dir)
|
|
137
|
+
|
|
138
|
+
def main():
|
|
139
|
+
if len(sys.argv) < 2:
|
|
140
|
+
print("用法:")
|
|
141
|
+
print(f" 默认 nginx 目录: {DEFAULT_NGINX_BASE_DIR}")
|
|
142
|
+
print(" 列出所有部署: python cleanup.py list [nginx基础目录]")
|
|
143
|
+
print(" 清理指定项目: python cleanup.py <project_id> [nginx基础目录]")
|
|
144
|
+
print(" 清理所有项目: python cleanup.py all [nginx基础目录]")
|
|
145
|
+
sys.exit(1)
|
|
146
|
+
|
|
147
|
+
command = sys.argv[1]
|
|
148
|
+
nginx_base_dir = sys.argv[2] if len(sys.argv) > 2 else None
|
|
149
|
+
|
|
150
|
+
if command == "list":
|
|
151
|
+
list_deployments(nginx_base_dir)
|
|
152
|
+
elif command == "all":
|
|
153
|
+
cleanup_all(nginx_base_dir)
|
|
154
|
+
else:
|
|
155
|
+
cleanup_deployment(command, nginx_base_dir)
|
|
156
|
+
|
|
157
|
+
if __name__ == "__main__":
|
|
158
|
+
main()
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
部署前端项目到共享的 nginx 容器(通过 conf.d 配置隔离)
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import subprocess
|
|
8
|
+
import socket
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import shutil
|
|
11
|
+
import json
|
|
12
|
+
import time
|
|
13
|
+
|
|
14
|
+
# 默认 nginx 基础目录(可在这里修改)
|
|
15
|
+
DEFAULT_NGINX_BASE_DIR = "/home/xubuntu001/AI/nginx"
|
|
16
|
+
|
|
17
|
+
def run_docker_command(cmd, **kwargs):
|
|
18
|
+
"""
|
|
19
|
+
运行 docker 命令,自动处理权限问题
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
cmd: 命令列表
|
|
23
|
+
**kwargs: 传递给 subprocess.run 的其他参数
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
subprocess.CompletedProcess 对象
|
|
27
|
+
"""
|
|
28
|
+
try:
|
|
29
|
+
# 首先尝试直接运行
|
|
30
|
+
return subprocess.run(cmd, **kwargs)
|
|
31
|
+
except (subprocess.CalledProcessError, PermissionError) as e:
|
|
32
|
+
# 如果失败,尝试使用 sg docker -c 运行
|
|
33
|
+
if 'check' in kwargs:
|
|
34
|
+
del kwargs['check'] # sg 会处理 check
|
|
35
|
+
|
|
36
|
+
# 将命令转换为 sg docker -c 格式
|
|
37
|
+
cmd_str = ' '.join(str(arg) for arg in cmd)
|
|
38
|
+
sg_cmd = ['sg', 'docker', '-c', cmd_str]
|
|
39
|
+
|
|
40
|
+
return subprocess.run(sg_cmd, **kwargs)
|
|
41
|
+
|
|
42
|
+
def find_available_port(start_port=8080, max_attempts=100):
|
|
43
|
+
"""查找可用端口"""
|
|
44
|
+
for port in range(start_port, start_port + max_attempts):
|
|
45
|
+
try:
|
|
46
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
47
|
+
s.bind(('', port))
|
|
48
|
+
return port
|
|
49
|
+
except OSError:
|
|
50
|
+
continue
|
|
51
|
+
raise RuntimeError(f"无法在 {start_port}-{start_port + max_attempts} 范围内找到可用端口")
|
|
52
|
+
|
|
53
|
+
def get_local_ip():
|
|
54
|
+
"""获取本机内网 IP"""
|
|
55
|
+
try:
|
|
56
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
57
|
+
s.connect(("8.8.8.8", 80))
|
|
58
|
+
ip = s.getsockname()[0]
|
|
59
|
+
s.close()
|
|
60
|
+
return ip
|
|
61
|
+
except Exception:
|
|
62
|
+
return "localhost"
|
|
63
|
+
|
|
64
|
+
def generate_project_id():
|
|
65
|
+
"""生成项目 ID"""
|
|
66
|
+
return f"project-{int(time.time())}"
|
|
67
|
+
|
|
68
|
+
def create_nginx_conf(project_id, port, nginx_base_dir):
|
|
69
|
+
"""创建项目的 nginx 配置文件"""
|
|
70
|
+
config_content = f"""server {{
|
|
71
|
+
listen {port};
|
|
72
|
+
server_name localhost;
|
|
73
|
+
|
|
74
|
+
root /usr/share/nginx/html/{project_id};
|
|
75
|
+
index index.html index.htm;
|
|
76
|
+
|
|
77
|
+
location / {{
|
|
78
|
+
try_files $uri $uri/ /index.html;
|
|
79
|
+
}}
|
|
80
|
+
|
|
81
|
+
# 启用 gzip 压缩
|
|
82
|
+
gzip on;
|
|
83
|
+
gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript;
|
|
84
|
+
|
|
85
|
+
# 缓存静态资源
|
|
86
|
+
location ~* \\.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {{
|
|
87
|
+
expires 1y;
|
|
88
|
+
add_header Cache-Control "public, immutable";
|
|
89
|
+
}}
|
|
90
|
+
}}
|
|
91
|
+
"""
|
|
92
|
+
conf_dir = nginx_base_dir / "config" / "conf.d"
|
|
93
|
+
conf_dir.mkdir(parents=True, exist_ok=True)
|
|
94
|
+
|
|
95
|
+
conf_file = conf_dir / f"{project_id}.conf"
|
|
96
|
+
conf_file.write_text(config_content)
|
|
97
|
+
return conf_file
|
|
98
|
+
|
|
99
|
+
def reload_nginx(nginx_base_dir):
|
|
100
|
+
"""重新加载 nginx 配置"""
|
|
101
|
+
compose_file = nginx_base_dir / "docker-compose.yml"
|
|
102
|
+
if not compose_file.exists():
|
|
103
|
+
raise RuntimeError(f"docker-compose.yml 不存在: {compose_file}")
|
|
104
|
+
|
|
105
|
+
# 检查容器是否运行
|
|
106
|
+
result = run_docker_command(
|
|
107
|
+
["docker", "ps", "--filter", "name=nginx-web", "--format", "{{.Names}}"],
|
|
108
|
+
capture_output=True,
|
|
109
|
+
text=True,
|
|
110
|
+
cwd=nginx_base_dir
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
if "nginx-web" not in result.stdout:
|
|
114
|
+
# 容器未运行,启动它
|
|
115
|
+
print("启动 nginx 容器...")
|
|
116
|
+
run_docker_command(
|
|
117
|
+
["docker", "compose", "up", "-d"],
|
|
118
|
+
cwd=nginx_base_dir,
|
|
119
|
+
check=True
|
|
120
|
+
)
|
|
121
|
+
else:
|
|
122
|
+
# 重新加载配置
|
|
123
|
+
print("重新加载 nginx 配置...")
|
|
124
|
+
run_docker_command(
|
|
125
|
+
["docker", "exec", "nginx-web", "nginx", "-s", "reload"],
|
|
126
|
+
check=True
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def deploy_frontend(source_dir, nginx_base_dir=None):
|
|
130
|
+
"""
|
|
131
|
+
部署前端项目
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
source_dir: 源代码目录路径
|
|
135
|
+
nginx_base_dir: nginx docker-compose 所在目录,默认为 DEFAULT_NGINX_BASE_DIR
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
dict: 包含部署信息的字典
|
|
139
|
+
"""
|
|
140
|
+
source_path = Path(source_dir).resolve()
|
|
141
|
+
if not source_path.exists():
|
|
142
|
+
raise ValueError(f"源目录不存在: {source_dir}")
|
|
143
|
+
|
|
144
|
+
# 设置 nginx 基础目录
|
|
145
|
+
if nginx_base_dir is None:
|
|
146
|
+
nginx_base_dir = Path(DEFAULT_NGINX_BASE_DIR)
|
|
147
|
+
else:
|
|
148
|
+
nginx_base_dir = Path(nginx_base_dir).resolve()
|
|
149
|
+
|
|
150
|
+
# 生成项目信息
|
|
151
|
+
project_id = generate_project_id()
|
|
152
|
+
|
|
153
|
+
# 查找可用端口
|
|
154
|
+
port = find_available_port()
|
|
155
|
+
|
|
156
|
+
# 复制前端文件到 html/project-id/
|
|
157
|
+
html_dir = nginx_base_dir / "html" / project_id
|
|
158
|
+
if html_dir.exists():
|
|
159
|
+
shutil.rmtree(html_dir)
|
|
160
|
+
shutil.copytree(source_path, html_dir)
|
|
161
|
+
|
|
162
|
+
# 创建 nginx 配置
|
|
163
|
+
conf_file = create_nginx_conf(project_id, port, nginx_base_dir)
|
|
164
|
+
|
|
165
|
+
# 重新加载 nginx
|
|
166
|
+
reload_nginx(nginx_base_dir)
|
|
167
|
+
|
|
168
|
+
# 获取访问 URL
|
|
169
|
+
local_ip = get_local_ip()
|
|
170
|
+
url = f"http://{local_ip}:{port}"
|
|
171
|
+
|
|
172
|
+
# 保存部署信息到部署目录
|
|
173
|
+
deployments_dir = nginx_base_dir / "deployments"
|
|
174
|
+
deployments_dir.mkdir(exist_ok=True)
|
|
175
|
+
|
|
176
|
+
deploy_info = {
|
|
177
|
+
"project_id": project_id,
|
|
178
|
+
"port": port,
|
|
179
|
+
"url": url,
|
|
180
|
+
"html_dir": str(html_dir),
|
|
181
|
+
"conf_file": str(conf_file),
|
|
182
|
+
"source_dir": str(source_path),
|
|
183
|
+
"deployed_at": time.strftime("%Y-%m-%d %H:%M:%S")
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
info_file = deployments_dir / f"{project_id}.json"
|
|
187
|
+
info_file.write_text(json.dumps(deploy_info, indent=2, ensure_ascii=False))
|
|
188
|
+
|
|
189
|
+
return deploy_info
|
|
190
|
+
|
|
191
|
+
def main():
|
|
192
|
+
if len(sys.argv) < 2:
|
|
193
|
+
print("用法: python deploy.py <前端项目目录> [nginx基础目录]")
|
|
194
|
+
print(f"默认 nginx 目录: {DEFAULT_NGINX_BASE_DIR}")
|
|
195
|
+
print("示例: python deploy.py ./my-app")
|
|
196
|
+
sys.exit(1)
|
|
197
|
+
|
|
198
|
+
source_dir = sys.argv[1]
|
|
199
|
+
nginx_base_dir = sys.argv[2] if len(sys.argv) > 2 else None
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
info = deploy_frontend(source_dir, nginx_base_dir)
|
|
203
|
+
print(f"\n✅ 部署成功!")
|
|
204
|
+
print(f"项目 ID: {info['project_id']}")
|
|
205
|
+
print(f"端口: {info['port']}")
|
|
206
|
+
print(f"访问地址: {info['url']}")
|
|
207
|
+
print(f"HTML 目录: {info['html_dir']}")
|
|
208
|
+
print(f"配置文件: {info['conf_file']}")
|
|
209
|
+
except Exception as e:
|
|
210
|
+
print(f"\n❌ 部署失败: {e}")
|
|
211
|
+
import traceback
|
|
212
|
+
traceback.print_exc()
|
|
213
|
+
sys.exit(1)
|
|
214
|
+
|
|
215
|
+
if __name__ == "__main__":
|
|
216
|
+
main()
|
package/server/cli.js
CHANGED
|
@@ -74,10 +74,11 @@ function loadEnvFile() {
|
|
|
74
74
|
// Get the database path (same logic as db.js)
|
|
75
75
|
function getDatabasePath() {
|
|
76
76
|
loadEnvFile();
|
|
77
|
-
|
|
77
|
+
const projectRoot = path.join(__dirname, '..');
|
|
78
|
+
const dataDir = process.env.DATA_DIR || path.join(projectRoot, 'data');
|
|
78
79
|
return process.env.DATABASE_PATH
|
|
79
|
-
? path.resolve(
|
|
80
|
-
: path.join(
|
|
80
|
+
? path.resolve(projectRoot, process.env.DATABASE_PATH)
|
|
81
|
+
: path.join(dataDir, 'auth.db');
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
// Get the installation directory
|
|
@@ -205,7 +206,7 @@ function isNewerVersion(v1, v2) {
|
|
|
205
206
|
async function checkForUpdates(silent = false) {
|
|
206
207
|
try {
|
|
207
208
|
const { execSync } = await import('child_process');
|
|
208
|
-
const latestVersion = execSync('npm show @
|
|
209
|
+
const latestVersion = execSync('npm show @ian2018cs/agenthub version', { encoding: 'utf8' }).trim();
|
|
209
210
|
const currentVersion = packageJson.version;
|
|
210
211
|
|
|
211
212
|
if (isNewerVersion(latestVersion, currentVersion)) {
|
|
@@ -238,11 +239,11 @@ async function updatePackage() {
|
|
|
238
239
|
}
|
|
239
240
|
|
|
240
241
|
console.log(`${c.info('[INFO]')} Updating from ${currentVersion} to ${latestVersion}...`);
|
|
241
|
-
execSync('npm update -g @
|
|
242
|
+
execSync('npm update -g @ian2018cs/agenthub', { stdio: 'inherit' });
|
|
242
243
|
console.log(`${c.ok('[OK]')} Update complete! Restart cloudcli to use the new version.`);
|
|
243
244
|
} catch (e) {
|
|
244
245
|
console.error(`${c.error('[ERROR]')} Update failed: ${e.message}`);
|
|
245
|
-
console.log(`${c.tip('[TIP]')} Try running manually: npm update -g @
|
|
246
|
+
console.log(`${c.tip('[TIP]')} Try running manually: npm update -g @ian2018cs/agenthub`);
|
|
246
247
|
}
|
|
247
248
|
}
|
|
248
249
|
|