@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ian2018cs/agenthub",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "A web-based UI for AI Agents",
5
5
  "type": "module",
6
6
  "main": "server/index.js",
@@ -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()
@@ -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()