@ian2018cs/agenthub 0.1.0 → 0.1.1
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/package.json +1 -1
- package/server/builtin-skills/.gitkeep +0 -0
- package/server/builtin-skills/deploy-frontend/SKILL.md +192 -0
- package/server/builtin-skills/deploy-frontend/scripts/cleanup.py +133 -0
- package/server/builtin-skills/deploy-frontend/scripts/deploy.py +191 -0
- package/server/cli.js +7 -6
- package/server/database/db.js +16 -15
- package/server/routes/skills.js +8 -0
- package/server/services/builtin-skills.js +147 -0
- package/server/services/user-directories.js +4 -0
package/package.json
CHANGED
|
File without changes
|
|
@@ -0,0 +1,192 @@
|
|
|
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
|
+
## 故障排查
|
|
179
|
+
|
|
180
|
+
**容器未启动**:脚本会自动执行 `docker-compose up -d`
|
|
181
|
+
|
|
182
|
+
**端口冲突**:脚本会自动查找可用端口(8080-8179 范围)
|
|
183
|
+
|
|
184
|
+
**配置未生效**:手动重新加载 nginx:
|
|
185
|
+
```bash
|
|
186
|
+
docker exec nginx-web nginx -s reload
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
**查看 nginx 日志**:
|
|
190
|
+
```bash
|
|
191
|
+
docker logs nginx-web
|
|
192
|
+
```
|
|
@@ -0,0 +1,133 @@
|
|
|
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 cleanup_deployment(project_id, nginx_base_dir=None):
|
|
15
|
+
"""
|
|
16
|
+
清理指定部署
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
project_id: 项目 ID
|
|
20
|
+
nginx_base_dir: nginx 基础目录,默认为 DEFAULT_NGINX_BASE_DIR
|
|
21
|
+
"""
|
|
22
|
+
if nginx_base_dir is None:
|
|
23
|
+
nginx_base_dir = Path(DEFAULT_NGINX_BASE_DIR)
|
|
24
|
+
else:
|
|
25
|
+
nginx_base_dir = Path(nginx_base_dir)
|
|
26
|
+
|
|
27
|
+
deployments_dir = nginx_base_dir / "deployments"
|
|
28
|
+
info_file = deployments_dir / f"{project_id}.json"
|
|
29
|
+
|
|
30
|
+
if not info_file.exists():
|
|
31
|
+
print(f"❌ 项目不存在: {project_id}")
|
|
32
|
+
return False
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
with open(info_file) as f:
|
|
36
|
+
info = json.load(f)
|
|
37
|
+
|
|
38
|
+
# 删除 HTML 目录
|
|
39
|
+
html_dir = Path(info['html_dir'])
|
|
40
|
+
if html_dir.exists():
|
|
41
|
+
print(f"删除 HTML 目录: {html_dir}")
|
|
42
|
+
shutil.rmtree(html_dir)
|
|
43
|
+
|
|
44
|
+
# 删除 nginx 配置文件
|
|
45
|
+
conf_file = Path(info['conf_file'])
|
|
46
|
+
if conf_file.exists():
|
|
47
|
+
print(f"删除配置文件: {conf_file}")
|
|
48
|
+
conf_file.unlink()
|
|
49
|
+
|
|
50
|
+
# 重新加载 nginx
|
|
51
|
+
print("重新加载 nginx 配置...")
|
|
52
|
+
subprocess.run(
|
|
53
|
+
["docker", "exec", "nginx-web", "nginx", "-s", "reload"],
|
|
54
|
+
check=True
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# 删除部署信息文件
|
|
58
|
+
info_file.unlink()
|
|
59
|
+
|
|
60
|
+
print(f"✅ 清理完成: {project_id}")
|
|
61
|
+
return True
|
|
62
|
+
|
|
63
|
+
except Exception as e:
|
|
64
|
+
print(f"❌ 清理失败: {e}")
|
|
65
|
+
import traceback
|
|
66
|
+
traceback.print_exc()
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
def list_deployments(nginx_base_dir=None):
|
|
70
|
+
"""列出所有部署"""
|
|
71
|
+
if nginx_base_dir is None:
|
|
72
|
+
nginx_base_dir = Path(DEFAULT_NGINX_BASE_DIR)
|
|
73
|
+
else:
|
|
74
|
+
nginx_base_dir = Path(nginx_base_dir)
|
|
75
|
+
|
|
76
|
+
deployments_dir = nginx_base_dir / "deployments"
|
|
77
|
+
|
|
78
|
+
if not deployments_dir.exists() or not list(deployments_dir.glob("*.json")):
|
|
79
|
+
print("没有找到部署")
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
print("\n当前部署:")
|
|
83
|
+
print("-" * 80)
|
|
84
|
+
|
|
85
|
+
for info_file in sorted(deployments_dir.glob("*.json")):
|
|
86
|
+
with open(info_file) as f:
|
|
87
|
+
info = json.load(f)
|
|
88
|
+
|
|
89
|
+
print(f"项目 ID: {info['project_id']}")
|
|
90
|
+
print(f" 部署时间: {info.get('deployed_at', 'N/A')}")
|
|
91
|
+
print(f" 访问地址: {info['url']}")
|
|
92
|
+
print(f" HTML 目录: {info['html_dir']}")
|
|
93
|
+
print()
|
|
94
|
+
|
|
95
|
+
def cleanup_all(nginx_base_dir=None):
|
|
96
|
+
"""清理所有部署"""
|
|
97
|
+
if nginx_base_dir is None:
|
|
98
|
+
nginx_base_dir = Path(DEFAULT_NGINX_BASE_DIR)
|
|
99
|
+
else:
|
|
100
|
+
nginx_base_dir = Path(nginx_base_dir)
|
|
101
|
+
|
|
102
|
+
deployments_dir = nginx_base_dir / "deployments"
|
|
103
|
+
|
|
104
|
+
if not deployments_dir.exists():
|
|
105
|
+
print("没有找到部署")
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
for info_file in deployments_dir.glob("*.json"):
|
|
109
|
+
project_id = info_file.stem
|
|
110
|
+
print(f"\n清理项目: {project_id}")
|
|
111
|
+
cleanup_deployment(project_id, nginx_base_dir)
|
|
112
|
+
|
|
113
|
+
def main():
|
|
114
|
+
if len(sys.argv) < 2:
|
|
115
|
+
print("用法:")
|
|
116
|
+
print(f" 默认 nginx 目录: {DEFAULT_NGINX_BASE_DIR}")
|
|
117
|
+
print(" 列出所有部署: python cleanup.py list [nginx基础目录]")
|
|
118
|
+
print(" 清理指定项目: python cleanup.py <project_id> [nginx基础目录]")
|
|
119
|
+
print(" 清理所有项目: python cleanup.py all [nginx基础目录]")
|
|
120
|
+
sys.exit(1)
|
|
121
|
+
|
|
122
|
+
command = sys.argv[1]
|
|
123
|
+
nginx_base_dir = sys.argv[2] if len(sys.argv) > 2 else None
|
|
124
|
+
|
|
125
|
+
if command == "list":
|
|
126
|
+
list_deployments(nginx_base_dir)
|
|
127
|
+
elif command == "all":
|
|
128
|
+
cleanup_all(nginx_base_dir)
|
|
129
|
+
else:
|
|
130
|
+
cleanup_deployment(command, nginx_base_dir)
|
|
131
|
+
|
|
132
|
+
if __name__ == "__main__":
|
|
133
|
+
main()
|
|
@@ -0,0 +1,191 @@
|
|
|
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 find_available_port(start_port=8080, max_attempts=100):
|
|
18
|
+
"""查找可用端口"""
|
|
19
|
+
for port in range(start_port, start_port + max_attempts):
|
|
20
|
+
try:
|
|
21
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
22
|
+
s.bind(('', port))
|
|
23
|
+
return port
|
|
24
|
+
except OSError:
|
|
25
|
+
continue
|
|
26
|
+
raise RuntimeError(f"无法在 {start_port}-{start_port + max_attempts} 范围内找到可用端口")
|
|
27
|
+
|
|
28
|
+
def get_local_ip():
|
|
29
|
+
"""获取本机内网 IP"""
|
|
30
|
+
try:
|
|
31
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
32
|
+
s.connect(("8.8.8.8", 80))
|
|
33
|
+
ip = s.getsockname()[0]
|
|
34
|
+
s.close()
|
|
35
|
+
return ip
|
|
36
|
+
except Exception:
|
|
37
|
+
return "localhost"
|
|
38
|
+
|
|
39
|
+
def generate_project_id():
|
|
40
|
+
"""生成项目 ID"""
|
|
41
|
+
return f"project-{int(time.time())}"
|
|
42
|
+
|
|
43
|
+
def create_nginx_conf(project_id, port, nginx_base_dir):
|
|
44
|
+
"""创建项目的 nginx 配置文件"""
|
|
45
|
+
config_content = f"""server {{
|
|
46
|
+
listen {port};
|
|
47
|
+
server_name localhost;
|
|
48
|
+
|
|
49
|
+
root /usr/share/nginx/html/{project_id};
|
|
50
|
+
index index.html index.htm;
|
|
51
|
+
|
|
52
|
+
location / {{
|
|
53
|
+
try_files $uri $uri/ /index.html;
|
|
54
|
+
}}
|
|
55
|
+
|
|
56
|
+
# 启用 gzip 压缩
|
|
57
|
+
gzip on;
|
|
58
|
+
gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript;
|
|
59
|
+
|
|
60
|
+
# 缓存静态资源
|
|
61
|
+
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {{
|
|
62
|
+
expires 1y;
|
|
63
|
+
add_header Cache-Control "public, immutable";
|
|
64
|
+
}}
|
|
65
|
+
}}
|
|
66
|
+
"""
|
|
67
|
+
conf_dir = nginx_base_dir / "config" / "conf.d"
|
|
68
|
+
conf_dir.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
|
|
70
|
+
conf_file = conf_dir / f"{project_id}.conf"
|
|
71
|
+
conf_file.write_text(config_content)
|
|
72
|
+
return conf_file
|
|
73
|
+
|
|
74
|
+
def reload_nginx(nginx_base_dir):
|
|
75
|
+
"""重新加载 nginx 配置"""
|
|
76
|
+
compose_file = nginx_base_dir / "docker-compose.yml"
|
|
77
|
+
if not compose_file.exists():
|
|
78
|
+
raise RuntimeError(f"docker-compose.yml 不存在: {compose_file}")
|
|
79
|
+
|
|
80
|
+
# 检查容器是否运行
|
|
81
|
+
result = subprocess.run(
|
|
82
|
+
["docker", "ps", "--filter", "name=nginx-web", "--format", "{{.Names}}"],
|
|
83
|
+
capture_output=True,
|
|
84
|
+
text=True,
|
|
85
|
+
cwd=nginx_base_dir
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
if "nginx-web" not in result.stdout:
|
|
89
|
+
# 容器未运行,启动它
|
|
90
|
+
print("启动 nginx 容器...")
|
|
91
|
+
subprocess.run(
|
|
92
|
+
["docker-compose", "up", "-d"],
|
|
93
|
+
cwd=nginx_base_dir,
|
|
94
|
+
check=True
|
|
95
|
+
)
|
|
96
|
+
else:
|
|
97
|
+
# 重新加载配置
|
|
98
|
+
print("重新加载 nginx 配置...")
|
|
99
|
+
subprocess.run(
|
|
100
|
+
["docker", "exec", "nginx-web", "nginx", "-s", "reload"],
|
|
101
|
+
check=True
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def deploy_frontend(source_dir, nginx_base_dir=None):
|
|
105
|
+
"""
|
|
106
|
+
部署前端项目
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
source_dir: 源代码目录路径
|
|
110
|
+
nginx_base_dir: nginx docker-compose 所在目录,默认为 DEFAULT_NGINX_BASE_DIR
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
dict: 包含部署信息的字典
|
|
114
|
+
"""
|
|
115
|
+
source_path = Path(source_dir).resolve()
|
|
116
|
+
if not source_path.exists():
|
|
117
|
+
raise ValueError(f"源目录不存在: {source_dir}")
|
|
118
|
+
|
|
119
|
+
# 设置 nginx 基础目录
|
|
120
|
+
if nginx_base_dir is None:
|
|
121
|
+
nginx_base_dir = Path(DEFAULT_NGINX_BASE_DIR)
|
|
122
|
+
else:
|
|
123
|
+
nginx_base_dir = Path(nginx_base_dir).resolve()
|
|
124
|
+
|
|
125
|
+
# 生成项目信息
|
|
126
|
+
project_id = generate_project_id()
|
|
127
|
+
|
|
128
|
+
# 查找可用端口
|
|
129
|
+
port = find_available_port()
|
|
130
|
+
|
|
131
|
+
# 复制前端文件到 html/project-id/
|
|
132
|
+
html_dir = nginx_base_dir / "html" / project_id
|
|
133
|
+
if html_dir.exists():
|
|
134
|
+
shutil.rmtree(html_dir)
|
|
135
|
+
shutil.copytree(source_path, html_dir)
|
|
136
|
+
|
|
137
|
+
# 创建 nginx 配置
|
|
138
|
+
conf_file = create_nginx_conf(project_id, port, nginx_base_dir)
|
|
139
|
+
|
|
140
|
+
# 重新加载 nginx
|
|
141
|
+
reload_nginx(nginx_base_dir)
|
|
142
|
+
|
|
143
|
+
# 获取访问 URL
|
|
144
|
+
local_ip = get_local_ip()
|
|
145
|
+
url = f"http://{local_ip}:{port}"
|
|
146
|
+
|
|
147
|
+
# 保存部署信息到部署目录
|
|
148
|
+
deployments_dir = nginx_base_dir / "deployments"
|
|
149
|
+
deployments_dir.mkdir(exist_ok=True)
|
|
150
|
+
|
|
151
|
+
deploy_info = {
|
|
152
|
+
"project_id": project_id,
|
|
153
|
+
"port": port,
|
|
154
|
+
"url": url,
|
|
155
|
+
"html_dir": str(html_dir),
|
|
156
|
+
"conf_file": str(conf_file),
|
|
157
|
+
"source_dir": str(source_path),
|
|
158
|
+
"deployed_at": time.strftime("%Y-%m-%d %H:%M:%S")
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
info_file = deployments_dir / f"{project_id}.json"
|
|
162
|
+
info_file.write_text(json.dumps(deploy_info, indent=2, ensure_ascii=False))
|
|
163
|
+
|
|
164
|
+
return deploy_info
|
|
165
|
+
|
|
166
|
+
def main():
|
|
167
|
+
if len(sys.argv) < 2:
|
|
168
|
+
print("用法: python deploy.py <前端项目目录> [nginx基础目录]")
|
|
169
|
+
print(f"默认 nginx 目录: {DEFAULT_NGINX_BASE_DIR}")
|
|
170
|
+
print("示例: python deploy.py ./my-app")
|
|
171
|
+
sys.exit(1)
|
|
172
|
+
|
|
173
|
+
source_dir = sys.argv[1]
|
|
174
|
+
nginx_base_dir = sys.argv[2] if len(sys.argv) > 2 else None
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
info = deploy_frontend(source_dir, nginx_base_dir)
|
|
178
|
+
print(f"\n✅ 部署成功!")
|
|
179
|
+
print(f"项目 ID: {info['project_id']}")
|
|
180
|
+
print(f"端口: {info['port']}")
|
|
181
|
+
print(f"访问地址: {info['url']}")
|
|
182
|
+
print(f"HTML 目录: {info['html_dir']}")
|
|
183
|
+
print(f"配置文件: {info['conf_file']}")
|
|
184
|
+
except Exception as e:
|
|
185
|
+
print(f"\n❌ 部署失败: {e}")
|
|
186
|
+
import traceback
|
|
187
|
+
traceback.print_exc()
|
|
188
|
+
sys.exit(1)
|
|
189
|
+
|
|
190
|
+
if __name__ == "__main__":
|
|
191
|
+
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
|
|
package/server/database/db.js
CHANGED
|
@@ -21,25 +21,25 @@ const c = {
|
|
|
21
21
|
dim: (text) => `${colors.dim}${text}${colors.reset}`,
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
-
// Use DATABASE_PATH environment variable if set, otherwise use
|
|
25
|
-
//
|
|
24
|
+
// Use DATABASE_PATH environment variable if set, otherwise use DATA_DIR/auth.db
|
|
25
|
+
// DATA_DIR defaults to ./data relative to project root
|
|
26
|
+
const PROJECT_ROOT = path.join(__dirname, '../..');
|
|
27
|
+
const DATA_DIR = process.env.DATA_DIR || path.join(PROJECT_ROOT, 'data');
|
|
26
28
|
const DB_PATH = process.env.DATABASE_PATH
|
|
27
|
-
? path.resolve(
|
|
28
|
-
: path.join(
|
|
29
|
+
? path.resolve(PROJECT_ROOT, process.env.DATABASE_PATH)
|
|
30
|
+
: path.join(DATA_DIR, 'auth.db');
|
|
29
31
|
const INIT_SQL_PATH = path.join(__dirname, 'init.sql');
|
|
30
32
|
|
|
31
|
-
// Ensure database directory exists
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
console.log(`Created database directory: ${dbDir}`);
|
|
38
|
-
}
|
|
39
|
-
} catch (error) {
|
|
40
|
-
console.error(`Failed to create database directory ${dbDir}:`, error.message);
|
|
41
|
-
throw error;
|
|
33
|
+
// Ensure database directory exists
|
|
34
|
+
const dbDir = path.dirname(DB_PATH);
|
|
35
|
+
try {
|
|
36
|
+
if (!fs.existsSync(dbDir)) {
|
|
37
|
+
fs.mkdirSync(dbDir, { recursive: true });
|
|
38
|
+
console.log(`Created database directory: ${dbDir}`);
|
|
42
39
|
}
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.error(`Failed to create database directory ${dbDir}:`, error.message);
|
|
42
|
+
throw error;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
// Create database connection
|
|
@@ -50,6 +50,7 @@ const appInstallPath = path.join(__dirname, '../..');
|
|
|
50
50
|
console.log('');
|
|
51
51
|
console.log(c.dim('═'.repeat(60)));
|
|
52
52
|
console.log(`${c.info('[INFO]')} App Installation: ${c.bright(appInstallPath)}`);
|
|
53
|
+
console.log(`${c.info('[INFO]')} Data Directory: ${c.dim(path.relative(appInstallPath, DATA_DIR))}`);
|
|
53
54
|
console.log(`${c.info('[INFO]')} Database: ${c.dim(path.relative(appInstallPath, DB_PATH))}`);
|
|
54
55
|
if (process.env.DATABASE_PATH) {
|
|
55
56
|
console.log(` ${c.dim('(Using custom DATABASE_PATH from environment)')}`);
|
package/server/routes/skills.js
CHANGED
|
@@ -5,6 +5,7 @@ import { spawn } from 'child_process';
|
|
|
5
5
|
import multer from 'multer';
|
|
6
6
|
import AdmZip from 'adm-zip';
|
|
7
7
|
import { getUserPaths, getPublicPaths, DATA_DIR } from '../services/user-directories.js';
|
|
8
|
+
import { markBuiltinSkillRemoved, isBuiltinSkillPath } from '../services/builtin-skills.js';
|
|
8
9
|
|
|
9
10
|
const router = express.Router();
|
|
10
11
|
|
|
@@ -238,6 +239,8 @@ router.get('/', async (req, res) => {
|
|
|
238
239
|
if (repoMatch) {
|
|
239
240
|
repository = `${repoMatch[1]}/${repoMatch[2]}`;
|
|
240
241
|
}
|
|
242
|
+
} else if (realPath.includes('/builtin-skills/')) {
|
|
243
|
+
source = 'builtin';
|
|
241
244
|
}
|
|
242
245
|
}
|
|
243
246
|
|
|
@@ -374,12 +377,14 @@ router.delete('/:name', async (req, res) => {
|
|
|
374
377
|
// Check the symlink target to determine source
|
|
375
378
|
let realPath = null;
|
|
376
379
|
let isImported = false;
|
|
380
|
+
let isBuiltin = false;
|
|
377
381
|
|
|
378
382
|
try {
|
|
379
383
|
const stat = await fs.lstat(linkPath);
|
|
380
384
|
if (stat.isSymbolicLink()) {
|
|
381
385
|
realPath = await fs.realpath(linkPath);
|
|
382
386
|
isImported = realPath.includes('/skills-import/');
|
|
387
|
+
isBuiltin = isBuiltinSkillPath(realPath);
|
|
383
388
|
}
|
|
384
389
|
} catch (err) {
|
|
385
390
|
return res.status(404).json({ error: 'Skill not found' });
|
|
@@ -395,6 +400,9 @@ router.delete('/:name', async (req, res) => {
|
|
|
395
400
|
} catch (err) {
|
|
396
401
|
console.error('Error removing imported skill files:', err);
|
|
397
402
|
}
|
|
403
|
+
} else if (isBuiltin) {
|
|
404
|
+
// Mark as removed so it won't be re-added on next sync
|
|
405
|
+
await markBuiltinSkillRemoved(userUuid, name);
|
|
398
406
|
}
|
|
399
407
|
|
|
400
408
|
res.json({ success: true, message: 'Skill deleted' });
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { getUserPaths } from './user-directories.js';
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
|
|
9
|
+
// Path to built-in skills directory
|
|
10
|
+
const BUILTIN_SKILLS_DIR = path.join(__dirname, '../builtin-skills');
|
|
11
|
+
|
|
12
|
+
// State file version for future migrations
|
|
13
|
+
const STATE_VERSION = 1;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get list of all available built-in skills
|
|
17
|
+
*/
|
|
18
|
+
export async function getBuiltinSkills() {
|
|
19
|
+
const skills = [];
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const entries = await fs.readdir(BUILTIN_SKILLS_DIR, { withFileTypes: true });
|
|
23
|
+
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
26
|
+
const skillPath = path.join(BUILTIN_SKILLS_DIR, entry.name);
|
|
27
|
+
|
|
28
|
+
// Check for SKILLS.md or SKILL.md
|
|
29
|
+
const skillsFile = path.join(skillPath, 'SKILLS.md');
|
|
30
|
+
const skillFile = path.join(skillPath, 'SKILL.md');
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
// Try SKILLS.md first, then SKILL.md
|
|
34
|
+
let found = false;
|
|
35
|
+
try {
|
|
36
|
+
await fs.access(skillsFile);
|
|
37
|
+
found = true;
|
|
38
|
+
} catch {
|
|
39
|
+
await fs.access(skillFile);
|
|
40
|
+
found = true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (found) {
|
|
44
|
+
skills.push({
|
|
45
|
+
name: entry.name,
|
|
46
|
+
path: skillPath
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
// Skip if neither file exists
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} catch (err) {
|
|
55
|
+
if (err.code !== 'ENOENT') {
|
|
56
|
+
console.error('Error reading builtin skills:', err);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return skills;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get path to user's builtin skills state file
|
|
65
|
+
*/
|
|
66
|
+
function getStatePath(userUuid) {
|
|
67
|
+
const userPaths = getUserPaths(userUuid);
|
|
68
|
+
return path.join(userPaths.claudeDir, '.builtin-skills-state.json');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Load user's builtin skills state
|
|
73
|
+
*/
|
|
74
|
+
export async function loadBuiltinSkillsState(userUuid) {
|
|
75
|
+
try {
|
|
76
|
+
const statePath = getStatePath(userUuid);
|
|
77
|
+
const content = await fs.readFile(statePath, 'utf-8');
|
|
78
|
+
return JSON.parse(content);
|
|
79
|
+
} catch {
|
|
80
|
+
return {
|
|
81
|
+
version: STATE_VERSION,
|
|
82
|
+
removedSkills: []
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Save user's builtin skills state
|
|
89
|
+
*/
|
|
90
|
+
export async function saveBuiltinSkillsState(userUuid, state) {
|
|
91
|
+
const statePath = getStatePath(userUuid);
|
|
92
|
+
await fs.writeFile(statePath, JSON.stringify(state, null, 2));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Initialize built-in skills for a user
|
|
97
|
+
* Creates symlinks for all built-in skills not in user's removed list
|
|
98
|
+
*/
|
|
99
|
+
export async function initBuiltinSkills(userUuid) {
|
|
100
|
+
const userPaths = getUserPaths(userUuid);
|
|
101
|
+
const builtinSkills = await getBuiltinSkills();
|
|
102
|
+
const state = await loadBuiltinSkillsState(userUuid);
|
|
103
|
+
|
|
104
|
+
for (const skill of builtinSkills) {
|
|
105
|
+
// Skip if user has explicitly removed this skill
|
|
106
|
+
if (state.removedSkills.includes(skill.name)) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const linkPath = path.join(userPaths.skillsDir, skill.name);
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
// Check if link already exists
|
|
114
|
+
await fs.lstat(linkPath);
|
|
115
|
+
// Link exists, skip
|
|
116
|
+
} catch {
|
|
117
|
+
// Link doesn't exist, create it
|
|
118
|
+
try {
|
|
119
|
+
await fs.symlink(skill.path, linkPath);
|
|
120
|
+
console.log(`Created builtin skill symlink: ${skill.name}`);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
console.error(`Error creating builtin skill symlink ${skill.name}:`, err);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Mark a built-in skill as removed by user
|
|
130
|
+
*/
|
|
131
|
+
export async function markBuiltinSkillRemoved(userUuid, skillName) {
|
|
132
|
+
const state = await loadBuiltinSkillsState(userUuid);
|
|
133
|
+
|
|
134
|
+
if (!state.removedSkills.includes(skillName)) {
|
|
135
|
+
state.removedSkills.push(skillName);
|
|
136
|
+
await saveBuiltinSkillsState(userUuid, state);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Check if a path is a built-in skill
|
|
142
|
+
*/
|
|
143
|
+
export function isBuiltinSkillPath(realPath) {
|
|
144
|
+
return realPath?.includes('/builtin-skills/') ?? false;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export { BUILTIN_SKILLS_DIR };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { promises as fs } from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { initBuiltinSkills } from './builtin-skills.js';
|
|
3
4
|
|
|
4
5
|
// Base data directory (configurable via env)
|
|
5
6
|
const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'data');
|
|
@@ -85,6 +86,9 @@ export async function initUserDirectories(userUuid) {
|
|
|
85
86
|
await fs.writeFile(usageScanStatePath, JSON.stringify(scanState, null, 2));
|
|
86
87
|
console.log(`Created .usage-scan-state.json for user ${userUuid}`);
|
|
87
88
|
|
|
89
|
+
// Initialize built-in skills
|
|
90
|
+
await initBuiltinSkills(userUuid);
|
|
91
|
+
|
|
88
92
|
return paths;
|
|
89
93
|
}
|
|
90
94
|
|