@ian2018cs/agenthub 0.1.11 → 0.1.13
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/html-deploy/SKILL.md +40 -0
- package/server/builtin-skills/html-deploy/scripts/delete.py +31 -0
- package/server/builtin-skills/html-deploy/scripts/deploy.py +36 -0
- package/server/builtin-skills/html-deploy/scripts/update.py +38 -0
- package/server/routes/auth.js +4 -0
- package/server/services/builtin-skills.js +35 -0
- package/server/builtin-skills/deploy-frontend/SKILL.md +0 -298
- package/server/builtin-skills/deploy-frontend/scripts/cleanup.py +0 -158
- package/server/builtin-skills/deploy-frontend/scripts/deploy.py +0 -216
package/package.json
CHANGED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: html-deploy
|
|
3
|
+
description: 将 HTML 内容部署为可访问的网页。使用场景:(1) 用户要求将生成的 HTML/网页部署上线或发布,(2) 用户要求更新已部署的页面内容,(3) 用户要求删除已部署的页面,(4) 用户提到"部署"、"发布"、"上线"HTML 页面,(5) 用户要求预览生成的网页效果。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# HTML Deploy
|
|
7
|
+
|
|
8
|
+
将 HTML 内容部署到远程服务,生成可访问的 URL。
|
|
9
|
+
|
|
10
|
+
## 使用流程
|
|
11
|
+
|
|
12
|
+
1. 根据用户需求生成完整的 HTML 页面,写入文件
|
|
13
|
+
2. 调用脚本部署、更新或删除,传入相应参数
|
|
14
|
+
3. 将结果告知用户
|
|
15
|
+
|
|
16
|
+
### 部署新页面
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
python3 skills/html-deploy/scripts/deploy.py <html_file>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### 更新已有页面
|
|
23
|
+
|
|
24
|
+
从之前的部署 URL 中提取 page_id(UUID 部分):
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
python3 skills/html-deploy/scripts/update.py <page_id> <html_file>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
page_id 不存在时会报 404 错误。
|
|
31
|
+
|
|
32
|
+
### 删除已有页面
|
|
33
|
+
|
|
34
|
+
从之前的部署 URL 中提取 page_id(UUID 部分):
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
python3 skills/html-deploy/scripts/delete.py <page_id>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
page_id 不存在时会报 404 错误。
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""删除已部署的页面。
|
|
3
|
+
|
|
4
|
+
用法: delete.py <page_id>
|
|
5
|
+
输出: 删除结果
|
|
6
|
+
"""
|
|
7
|
+
import sys
|
|
8
|
+
import json
|
|
9
|
+
import urllib.request
|
|
10
|
+
|
|
11
|
+
API_BASE = "http://10.0.0.252:11004"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main():
|
|
15
|
+
if len(sys.argv) != 2:
|
|
16
|
+
print(f"用法: {sys.argv[0]} <page_id>", file=sys.stderr)
|
|
17
|
+
sys.exit(1)
|
|
18
|
+
|
|
19
|
+
page_id = sys.argv[1]
|
|
20
|
+
req = urllib.request.Request(
|
|
21
|
+
f"{API_BASE}/deploy/{page_id}",
|
|
22
|
+
method="DELETE",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
with urllib.request.urlopen(req) as resp:
|
|
26
|
+
result = json.loads(resp.read())
|
|
27
|
+
print(result["detail"])
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
if __name__ == "__main__":
|
|
31
|
+
main()
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""部署 HTML 文件为网页。
|
|
3
|
+
|
|
4
|
+
用法: deploy.py <html_file>
|
|
5
|
+
输出: 部署成功后的访问 URL
|
|
6
|
+
"""
|
|
7
|
+
import sys
|
|
8
|
+
import json
|
|
9
|
+
import urllib.request
|
|
10
|
+
|
|
11
|
+
API_BASE = "http://10.0.0.252:11004"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main():
|
|
15
|
+
if len(sys.argv) != 2:
|
|
16
|
+
print(f"用法: {sys.argv[0]} <html_file>", file=sys.stderr)
|
|
17
|
+
sys.exit(1)
|
|
18
|
+
|
|
19
|
+
with open(sys.argv[1], "r", encoding="utf-8") as f:
|
|
20
|
+
html = f.read()
|
|
21
|
+
|
|
22
|
+
data = json.dumps({"html": html}).encode("utf-8")
|
|
23
|
+
req = urllib.request.Request(
|
|
24
|
+
f"{API_BASE}/deploy",
|
|
25
|
+
data=data,
|
|
26
|
+
headers={"Content-Type": "application/json"},
|
|
27
|
+
method="POST",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
with urllib.request.urlopen(req) as resp:
|
|
31
|
+
result = json.loads(resp.read())
|
|
32
|
+
print(result["url"])
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
if __name__ == "__main__":
|
|
36
|
+
main()
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""更新已部署的页面内容。
|
|
3
|
+
|
|
4
|
+
用法: update.py <page_id> <html_file>
|
|
5
|
+
输出: 更新成功后的访问 URL
|
|
6
|
+
"""
|
|
7
|
+
import sys
|
|
8
|
+
import json
|
|
9
|
+
import urllib.request
|
|
10
|
+
|
|
11
|
+
API_BASE = "http://10.0.0.252:11004"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main():
|
|
15
|
+
if len(sys.argv) != 3:
|
|
16
|
+
print(f"用法: {sys.argv[0]} <page_id> <html_file>", file=sys.stderr)
|
|
17
|
+
sys.exit(1)
|
|
18
|
+
|
|
19
|
+
page_id = sys.argv[1]
|
|
20
|
+
|
|
21
|
+
with open(sys.argv[2], "r", encoding="utf-8") as f:
|
|
22
|
+
html = f.read()
|
|
23
|
+
|
|
24
|
+
data = json.dumps({"html": html}).encode("utf-8")
|
|
25
|
+
req = urllib.request.Request(
|
|
26
|
+
f"{API_BASE}/deploy/{page_id}",
|
|
27
|
+
data=data,
|
|
28
|
+
headers={"Content-Type": "application/json"},
|
|
29
|
+
method="PUT",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
with urllib.request.urlopen(req) as resp:
|
|
33
|
+
result = json.loads(resp.read())
|
|
34
|
+
print(result["url"])
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
if __name__ == "__main__":
|
|
38
|
+
main()
|
package/server/routes/auth.js
CHANGED
|
@@ -4,6 +4,7 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
4
4
|
import { userDb, verificationDb, domainWhitelistDb, usageDb } from '../database/db.js';
|
|
5
5
|
import { generateToken, authenticateToken } from '../middleware/auth.js';
|
|
6
6
|
import { initUserDirectories } from '../services/user-directories.js';
|
|
7
|
+
import { initBuiltinSkills } from '../services/builtin-skills.js';
|
|
7
8
|
import { sendVerificationCode, isSmtpConfigured } from '../services/email.js';
|
|
8
9
|
|
|
9
10
|
const router = express.Router();
|
|
@@ -115,6 +116,9 @@ router.post('/verify-code', async (req, res) => {
|
|
|
115
116
|
|
|
116
117
|
// Initialize user directories
|
|
117
118
|
await initUserDirectories(uuid);
|
|
119
|
+
} else {
|
|
120
|
+
// Sync built-in skills for existing user (clean up dangling + add new)
|
|
121
|
+
await initBuiltinSkills(user.uuid);
|
|
118
122
|
}
|
|
119
123
|
|
|
120
124
|
// Check if user is disabled
|
|
@@ -95,12 +95,47 @@ export async function saveBuiltinSkillsState(userUuid, state) {
|
|
|
95
95
|
/**
|
|
96
96
|
* Initialize built-in skills for a user
|
|
97
97
|
* Creates symlinks for all built-in skills not in user's removed list
|
|
98
|
+
* Also cleans up dangling symlinks from deleted built-in skills
|
|
98
99
|
*/
|
|
99
100
|
export async function initBuiltinSkills(userUuid) {
|
|
100
101
|
const userPaths = getUserPaths(userUuid);
|
|
101
102
|
const builtinSkills = await getBuiltinSkills();
|
|
102
103
|
const state = await loadBuiltinSkillsState(userUuid);
|
|
104
|
+
// Clean up dangling symlinks pointing to removed built-in skills
|
|
105
|
+
try {
|
|
106
|
+
const entries = await fs.readdir(userPaths.skillsDir, { withFileTypes: true });
|
|
107
|
+
for (const entry of entries) {
|
|
108
|
+
const entryPath = path.join(userPaths.skillsDir, entry.name);
|
|
109
|
+
try {
|
|
110
|
+
const stat = await fs.lstat(entryPath);
|
|
111
|
+
if (!stat.isSymbolicLink()) continue;
|
|
112
|
+
|
|
113
|
+
const target = await fs.readlink(entryPath);
|
|
114
|
+
const realBuiltinDir = await fs.realpath(BUILTIN_SKILLS_DIR);
|
|
115
|
+
const resolvedTarget = path.resolve(path.dirname(entryPath), target);
|
|
116
|
+
|
|
117
|
+
// Only clean up symlinks that point into builtin-skills directory
|
|
118
|
+
if (!resolvedTarget.startsWith(realBuiltinDir)) continue;
|
|
119
|
+
|
|
120
|
+
// Check if the symlink target still exists
|
|
121
|
+
try {
|
|
122
|
+
await fs.access(resolvedTarget);
|
|
123
|
+
} catch {
|
|
124
|
+
// Target no longer exists, remove the dangling symlink
|
|
125
|
+
await fs.unlink(entryPath);
|
|
126
|
+
console.log(`Removed dangling builtin skill symlink: ${entry.name}`);
|
|
127
|
+
}
|
|
128
|
+
} catch (err) {
|
|
129
|
+
// Ignore errors on individual entries
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
} catch (err) {
|
|
133
|
+
if (err.code !== 'ENOENT') {
|
|
134
|
+
console.error('Error cleaning up builtin skill symlinks:', err);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
103
137
|
|
|
138
|
+
// Create symlinks for new built-in skills
|
|
104
139
|
for (const skill of builtinSkills) {
|
|
105
140
|
// Skip if user has explicitly removed this skill
|
|
106
141
|
if (state.removedSkills.includes(skill.name)) {
|
|
@@ -1,298 +0,0 @@
|
|
|
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
|
-
```bash
|
|
13
|
-
# 1. 确保你有一个包含 index.html 的目录
|
|
14
|
-
mkdir my-project
|
|
15
|
-
echo "<h1>Hello World</h1>" > my-project/index.html
|
|
16
|
-
|
|
17
|
-
# 2. 使用 python3 部署(注意:是目录,不是文件)
|
|
18
|
-
python3 /path/to/deploy-frontend/scripts/deploy.py my-project
|
|
19
|
-
|
|
20
|
-
# 3. 访问输出的 URL,例如 http://10.0.1.133:8080
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
## 常见错误
|
|
24
|
-
|
|
25
|
-
| 错误 | 原因 | 解决方案 |
|
|
26
|
-
|------|------|----------|
|
|
27
|
-
| `python: command not found` | 使用了 `python` 而不是 `python3` | 使用 `python3` 命令 |
|
|
28
|
-
| `NotADirectoryError` | 传入了单个文件而不是目录 | 传入包含 index.html 的目录路径 |
|
|
29
|
-
| `404 Not Found` | 主文件不叫 index.html | 将主 HTML 文件重命名为 index.html |
|
|
30
|
-
| 本地能访问,其他设备不能 | IP 地址或网络问题 | 使用内网 IP,检查防火墙 |
|
|
31
|
-
|
|
32
|
-
## 配置
|
|
33
|
-
|
|
34
|
-
默认 nginx 目录已配置为:`/home/xubuntu001/AI/nginx`
|
|
35
|
-
|
|
36
|
-
如需修改,编辑脚本文件中的 `DEFAULT_NGINX_BASE_DIR` 变量:
|
|
37
|
-
- `scripts/deploy.py` 第 14 行
|
|
38
|
-
- `scripts/cleanup.py` 第 10 行
|
|
39
|
-
|
|
40
|
-
## 工作原理
|
|
41
|
-
|
|
42
|
-
- **单容器架构**:所有项目共享一个 nginx 容器(节省资源)
|
|
43
|
-
- **配置隔离**:每个项目在 `config/conf.d/` 下有独立的 `.conf` 文件
|
|
44
|
-
- **端口隔离**:自动分配不同端口(从 8080 开始递增)
|
|
45
|
-
- **目录隔离**:每个项目的文件存放在 `html/project-{timestamp}/`
|
|
46
|
-
|
|
47
|
-
## 部署前端项目
|
|
48
|
-
|
|
49
|
-
使用 `scripts/deploy.py` 部署项目:
|
|
50
|
-
|
|
51
|
-
```bash
|
|
52
|
-
python3 scripts/deploy.py <前端项目目录>
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
**重要说明:**
|
|
56
|
-
- 必须使用 `python3` 命令(不是 `python`)
|
|
57
|
-
- `<前端项目目录>`:必须是一个**目录**(不能是单个文件)
|
|
58
|
-
- 目录中必须包含 `index.html` 作为入口文件(nginx 默认查找 index.html)
|
|
59
|
-
- 如果你的 HTML 文件不叫 `index.html`,需要先重命名
|
|
60
|
-
|
|
61
|
-
**参数说明:**
|
|
62
|
-
- `<前端项目目录>`:包含 index.html 等前端文件的目录(必需)
|
|
63
|
-
|
|
64
|
-
默认使用 `/home/xubuntu001/AI/nginx` 作为 nginx 基础目录,无需额外指定。
|
|
65
|
-
|
|
66
|
-
**脚本执行流程:**
|
|
67
|
-
1. 生成唯一的项目 ID(project-{timestamp})
|
|
68
|
-
2. 查找可用端口(从 8080 开始)
|
|
69
|
-
3. 复制前端文件到 `html/{project_id}/`
|
|
70
|
-
4. 在 `config/conf.d/` 创建 nginx 配置文件
|
|
71
|
-
5. 重新加载 nginx(如果容器未运行则启动)
|
|
72
|
-
6. 返回访问 URL(内网IP:端口)
|
|
73
|
-
7. 保存部署信息到 `deployments/{project_id}.json`
|
|
74
|
-
|
|
75
|
-
**示例:**
|
|
76
|
-
```bash
|
|
77
|
-
# 部署前端项目(使用默认 nginx 目录)
|
|
78
|
-
python3 .claude/skills/deploy-frontend/scripts/deploy.py ./my-frontend-app
|
|
79
|
-
|
|
80
|
-
# 错误示例 - 不要这样做:
|
|
81
|
-
# python3 deploy.py ./my-app/index.html ❌ 不能传单个文件
|
|
82
|
-
# python deploy.py ./my-app ❌ 必须使用 python3
|
|
83
|
-
|
|
84
|
-
# 正确示例:
|
|
85
|
-
# python3 deploy.py ./my-app ✅ 传目录,使用 python3
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
**输出示例:**
|
|
89
|
-
```
|
|
90
|
-
✅ 部署成功!
|
|
91
|
-
项目 ID: project-1738151234
|
|
92
|
-
端口: 8080
|
|
93
|
-
访问地址: http://192.168.1.100:8080
|
|
94
|
-
HTML 目录: /path/to/nginx/html/project-1738151234
|
|
95
|
-
配置文件: /path/to/nginx/config/conf.d/project-1738151234.conf
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
## 管理部署
|
|
99
|
-
|
|
100
|
-
使用 `scripts/cleanup.py` 管理已部署的项目。
|
|
101
|
-
|
|
102
|
-
### 列出所有部署
|
|
103
|
-
|
|
104
|
-
```bash
|
|
105
|
-
python3 scripts/cleanup.py list
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
### 清理指定项目
|
|
109
|
-
|
|
110
|
-
```bash
|
|
111
|
-
python3 scripts/cleanup.py <project_id>
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
会删除:
|
|
115
|
-
- HTML 目录
|
|
116
|
-
- nginx 配置文件
|
|
117
|
-
- 部署信息文件
|
|
118
|
-
|
|
119
|
-
并重新加载 nginx 配置。
|
|
120
|
-
|
|
121
|
-
### 清理所有项目
|
|
122
|
-
|
|
123
|
-
```bash
|
|
124
|
-
python3 scripts/cleanup.py all
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
## 目录结构
|
|
128
|
-
|
|
129
|
-
部署后的 nginx 基础目录结构:
|
|
130
|
-
|
|
131
|
-
```
|
|
132
|
-
nginx-base/
|
|
133
|
-
├── docker-compose.yml # nginx 容器配置
|
|
134
|
-
├── html/ # 前端文件
|
|
135
|
-
│ ├── project-1738151234/ # 项目1
|
|
136
|
-
│ │ ├── index.html
|
|
137
|
-
│ │ └── ...
|
|
138
|
-
│ └── project-1738151456/ # 项目2
|
|
139
|
-
│ ├── index.html
|
|
140
|
-
│ └── ...
|
|
141
|
-
├── config/
|
|
142
|
-
│ └── conf.d/ # nginx 配置
|
|
143
|
-
│ ├── project-1738151234.conf
|
|
144
|
-
│ └── project-1738151456.conf
|
|
145
|
-
├── logs/ # nginx 日志
|
|
146
|
-
└── deployments/ # 部署信息
|
|
147
|
-
├── project-1738151234.json
|
|
148
|
-
└── project-1738151456.json
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
## 使用流程
|
|
152
|
-
|
|
153
|
-
### 典型工作流
|
|
154
|
-
|
|
155
|
-
1. **生成前端代码**:创建 HTML/React/Vue 项目
|
|
156
|
-
2. **调用部署脚本**:
|
|
157
|
-
```python
|
|
158
|
-
from pathlib import Path
|
|
159
|
-
import subprocess
|
|
160
|
-
|
|
161
|
-
result = subprocess.run([
|
|
162
|
-
"python3", # 必须使用 python3
|
|
163
|
-
str(Path.home() / ".claude/skills/deploy-frontend/scripts/deploy.py"),
|
|
164
|
-
"./frontend-output" # 必须是目录,不能是单个文件
|
|
165
|
-
], capture_output=True, text=True)
|
|
166
|
-
|
|
167
|
-
print(result.stdout)
|
|
168
|
-
```
|
|
169
|
-
3. **提取访问 URL**:从输出中获取 URL 返回给用户
|
|
170
|
-
4. **提醒用户访问**:告知用户可以通过浏览器访问该 URL
|
|
171
|
-
|
|
172
|
-
### 在对话中使用
|
|
173
|
-
|
|
174
|
-
当用户要求生成前端页面时:
|
|
175
|
-
|
|
176
|
-
1. 生成前端代码(HTML/CSS/JS 等)
|
|
177
|
-
2. 将文件写入目录(确保主文件命名为 index.html)
|
|
178
|
-
3. 调用 deploy.py 部署
|
|
179
|
-
4. 将访问 URL 返回给用户
|
|
180
|
-
|
|
181
|
-
示例代码模式:
|
|
182
|
-
```python
|
|
183
|
-
# 1. 创建输出目录
|
|
184
|
-
output_dir = Path("/tmp/frontend-{timestamp}")
|
|
185
|
-
output_dir.mkdir(parents=True, exist_ok=True)
|
|
186
|
-
|
|
187
|
-
# 2. 写入前端文件(重要:主文件必须命名为 index.html)
|
|
188
|
-
(output_dir / "index.html").write_text(html_content) # ✅ 使用 index.html
|
|
189
|
-
(output_dir / "style.css").write_text(css_content)
|
|
190
|
-
|
|
191
|
-
# 3. 部署(使用 python3,传目录而不是文件)
|
|
192
|
-
deploy_script = Path.home() / ".claude/skills/deploy-frontend/scripts/deploy.py"
|
|
193
|
-
result = subprocess.run(
|
|
194
|
-
["python3", str(deploy_script), str(output_dir)], # ✅ python3 + 目录路径
|
|
195
|
-
capture_output=True,
|
|
196
|
-
text=True
|
|
197
|
-
)
|
|
198
|
-
|
|
199
|
-
# 4. 提取 URL(从输出中解析)
|
|
200
|
-
for line in result.stdout.split('\n'):
|
|
201
|
-
if line.startswith('访问地址:'):
|
|
202
|
-
url = line.split(':', 1)[1].strip()
|
|
203
|
-
print(f"您的前端页面已部署: {url}")
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
## 前置要求
|
|
207
|
-
|
|
208
|
-
- Docker 和 docker-compose 已安装
|
|
209
|
-
- nginx 基础目录位于 `/home/xubuntu001/AI/nginx`
|
|
210
|
-
- docker-compose.yml 使用 `network_mode: host`
|
|
211
|
-
- Python 3.6+
|
|
212
|
-
|
|
213
|
-
## Docker 权限处理
|
|
214
|
-
|
|
215
|
-
脚本已内置自动权限处理机制:
|
|
216
|
-
|
|
217
|
-
1. **自动检测权限**:脚本会首先尝试直接运行 docker 命令
|
|
218
|
-
2. **自动降级处理**:如果遇到权限错误,会自动使用 `sg docker -c` 运行
|
|
219
|
-
3. **无需手动干预**:用户无需关心是否在 docker 组中
|
|
220
|
-
|
|
221
|
-
**注意**:如果您刚刚被添加到 docker 组,可能需要重新登录或使用以下命令激活组权限:
|
|
222
|
-
```bash
|
|
223
|
-
newgrp docker
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
## 更新日志
|
|
227
|
-
|
|
228
|
-
### v1.2 (2026-01-30)
|
|
229
|
-
- ✅ 更新文档:明确必须使用 `python3` 而不是 `python`
|
|
230
|
-
- ✅ 更新文档:强调必须传入目录而不是单个文件
|
|
231
|
-
- ✅ 更新文档:说明主文件必须命名为 `index.html`
|
|
232
|
-
- ✅ 添加常见错误示例和最佳实践
|
|
233
|
-
- ✅ 新增部署后文件重命名的故障排查说明
|
|
234
|
-
|
|
235
|
-
### v1.1 (2026-01-30)
|
|
236
|
-
- ✅ 修复正则表达式转义警告
|
|
237
|
-
- ✅ 添加自动 Docker 权限处理(`run_docker_command` 函数)
|
|
238
|
-
- ✅ 使用 `docker compose` 替代旧版 `docker-compose` 命令
|
|
239
|
-
- ✅ deploy.py 和 cleanup.py 都支持自动权限处理
|
|
240
|
-
|
|
241
|
-
### v1.0
|
|
242
|
-
- 初始版本:支持多项目部署和端口隔离
|
|
243
|
-
|
|
244
|
-
## 故障排查
|
|
245
|
-
|
|
246
|
-
**容器未启动**:脚本会自动执行 `docker compose up -d`
|
|
247
|
-
|
|
248
|
-
**端口冲突**:脚本会自动查找可用端口(8080-8179 范围)
|
|
249
|
-
|
|
250
|
-
**权限问题**:脚本会自动使用 `sg docker -c` 处理权限问题
|
|
251
|
-
|
|
252
|
-
**找不到 python 命令**:
|
|
253
|
-
```bash
|
|
254
|
-
# 错误:python: command not found
|
|
255
|
-
# 解决:使用 python3
|
|
256
|
-
python3 scripts/deploy.py ./my-app
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
**传入单个文件报错**:
|
|
260
|
-
```bash
|
|
261
|
-
# 错误:NotADirectoryError
|
|
262
|
-
# 原因:传入的是文件而不是目录
|
|
263
|
-
# 解决:传入包含 index.html 的目录
|
|
264
|
-
python3 scripts/deploy.py ./my-app/ # 正确
|
|
265
|
-
# 而不是
|
|
266
|
-
python3 scripts/deploy.py ./my-app/index.html # 错误
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
**页面无法访问(404 Not Found)**:
|
|
270
|
-
```bash
|
|
271
|
-
# 检查是否存在 index.html
|
|
272
|
-
ls /home/xubuntu001/AI/nginx/html/project-*/
|
|
273
|
-
|
|
274
|
-
# 如果文件名不是 index.html,重命名它:
|
|
275
|
-
cd /home/xubuntu001/AI/nginx/html/project-xxxxxxxxxx/
|
|
276
|
-
mv yourfile.html index.html
|
|
277
|
-
|
|
278
|
-
# 重新加载 nginx
|
|
279
|
-
sg docker -c "docker exec nginx-web nginx -s reload"
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
**配置未生效**:手动重新加载 nginx:
|
|
283
|
-
```bash
|
|
284
|
-
docker exec nginx-web nginx -s reload
|
|
285
|
-
# 或如果有权限问题:
|
|
286
|
-
sg docker -c "docker exec nginx-web nginx -s reload"
|
|
287
|
-
```
|
|
288
|
-
|
|
289
|
-
**查看 nginx 日志**:
|
|
290
|
-
```bash
|
|
291
|
-
docker logs nginx-web
|
|
292
|
-
```
|
|
293
|
-
|
|
294
|
-
**本地能访问,其他设备访问不了**:
|
|
295
|
-
- 检查防火墙设置
|
|
296
|
-
- 确认设备在同一网络
|
|
297
|
-
- 使用正确的内网 IP(不要用 localhost)
|
|
298
|
-
- 确认路由器没有阻止端口
|
|
@@ -1,158 +0,0 @@
|
|
|
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()
|
|
@@ -1,216 +0,0 @@
|
|
|
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()
|