提交 b05775a4 authored 作者: 陈泽健's avatar 陈泽健

fix(service-manage): 部署到 5.60 生产环境并修复 7 个问题

- fix: 5.44 签名 ensure_ascii=True 修复含中文保存失败
- fix: 5.44 下载文件名 basename 净化修复 500
- fix: server.py assets 处理 + VUE_DIST_DIR 修复 assets 302
- fix: server.py 页面跳转 url_for 硬编码修复 500
- chore: 部署脚本重构为 systemd 模式 + 全量重建 dist
- chore: upload_to_server.py 补充 pycryptodome 依赖
- docs: 更新 HANDOFF 服务管理文档
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 e3ace44f
......@@ -36,8 +36,8 @@ COPY skill/SKILL.md /app/SKILL.md
# ---------- Vue 前端构建产物(本地 npm run build 后直接复制) ----------
COPY frontend/dist/ /data/dist/
# ---------- 产品维护手册(离线模式文档下载) ----------
COPY Docs/维护手册/ /app/Docs/维护手册/
# ---------- 产品维护手册(离线模式文档下载,按需部署) ----------
# COPY Docs/维护手册/ /app/Docs/维护手册/
# ---------- 运行时数据目录(volume 挂载点) ----------
RUN mkdir -p /app/data \
......
# -*- coding: utf-8 -*-
"""
deploy_service_manage.py — 部署服务管理模块到 5.60 Docker 容器
部署步骤:
1. 上传后端 Python 文件到 /data/third_party/monitor-platform/
2. docker cp 到 troubleshoot 容器
3. 上传前端 dist 到 /data/third_party/monitor-platform/frontend/dist/
4. 同步到 /opt/troubleshoot/dist/
5. 重启容器
6. 验证 health
"""
import paramiko
import os
import time
from pathlib import Path
import os
s = paramiko.SSHClient()
s.set_missing_host_key_policy(paramiko.AutoAddPolicy())
s.connect('192.168.5.60', username='ubains', password='Ubains@123', timeout=15)
sf = s.open_sftp()
HOST = '192.168.5.60'
USER = 'ubains'
PASSWORD = 'Ubains@123'
REPO = 'E:/GithubData/ubains-module-test/troubleshoot-ai-assistant'
REMOTE_BASE = '/data/third_party/monitor-platform'
CONTAINER = 'troubleshoot'
WEB_DIR = '/opt/troubleshoot/web'
DIST_DIR = '/opt/troubleshoot/dist'
# 需要部署的后端文件(相对仓库根目录)
BACKEND_FILES = [
# 核心:服务管理模块(新功能)
'skill/code/web/services/service_manage.py',
'skill/code/web/services/five44_client.py',
'skill/code/web/routes/service_manage.py',
'skill/code/web/utils/paths.py',
# 其他有变更的文件
'skill/code/web/server.py',
'skill/code/web/services/product_service.py',
'skill/code/web/routes/troubleshoot.py',
'skill/code/web/routes/version.py',
'skill/code/web/services/ai_service.py',
'skill/code/web/cache_manager.py',
'skill/code/web/users.json',
'skill/code/web/products.json',
]
def main():
s = paramiko.SSHClient()
s.set_missing_host_key_policy(paramiko.AutoAddPolicy())
s.connect(HOST, username=USER, password=PASSWORD)
sf = s.open_sftp()
print("=" * 60)
print(" Deploy Service Management Module")
print("=" * 60)
# ============================================================
# Step 1: 上传后端文件
# ============================================================
print("\n[1/4] Uploading backend files...")
for f in BACKEND_FILES:
local = os.path.join(REPO, f.replace('/', os.sep))
remote = f'{REMOTE_BASE}/{f}'
# 确保远程目录存在
rdir = os.path.dirname(remote)
LOCAL_REPO = r'E:/GithubData/ubains-module-test/troubleshoot-ai-assistant'
print("=" * 60)
print(" Deploy Service Management (clean deploy)")
print("=" * 60)
# Step 1: Upload backend
print("\n[1/3] Uploading backend files...")
for f in BACKEND_FILES:
local = os.path.join(LOCAL_REPO, f.replace('/', os.sep))
remote = f'{REMOTE_BASE}/{f}'
rdir = '/'.join(remote.split('/')[:-1])
s.exec_command(f'mkdir -p "{rdir}"')
sf.put(local, remote)
print(f" [OK] {f}")
# Copy to web dir
print("\n[2/3] Copying to web directory...")
for f in BACKEND_FILES:
if f == 'skill/code/web/users.json':
tmp = '/tmp/users.json'
sf.put(os.path.join(LOCAL_REPO, f.replace('/', os.sep)), tmp)
s.exec_command(f'sudo cp "{tmp}" "/opt/troubleshoot/data/users.json" && sudo rm -f "{tmp}"')
print(f' [OK] {f} -> /opt/troubleshoot/data/users.json')
continue
relative = f.replace('skill/code/web/', '')
target = f'{WEB_DIR}/{relative}'
target_dir = '/'.join(target.split('/')[:-1])
s.exec_command(f'mkdir -p "{target_dir}"')
s.exec_command(f'cp "{REMOTE_BASE}/{f}" "{target}"')
print(f' [OK] {relative}')
# Step 3: Clean frontend dist completely
print("\n[3/3] Clean deploy frontend dist...")
# 1) 彻底删除服务器上的所有 dist 文件和源缓存
s.exec_command(f'sudo rm -rf {DIST_DIR}')
s.exec_command(f'sudo rm -rf {REMOTE_BASE}/frontend/dist')
# 2) 重新创建目录
s.exec_command(f'mkdir -p {DIST_DIR}')
s.exec_command(f'mkdir -p {REMOTE_BASE}/frontend/dist')
# 3) 上传本地 dist 文件
local_dist = os.path.join(LOCAL_REPO, 'frontend', 'dist')
for root, dirs, files in os.walk(local_dist):
for fn in files:
lpath = os.path.join(root, fn)
rel = os.path.relpath(lpath, local_dist)
rpath = f'{REMOTE_BASE}/frontend/dist/{rel.replace(chr(92), "/")}'
rdir = '/'.join(rpath.split('/')[:-1])
s.exec_command(f'mkdir -p "{rdir}"')
# 上传
sf.put(local, remote)
print(f" [OK] {f}")
# ============================================================
# Step 2: docker cp 到容器(挂载卷文件除外)
# ============================================================
print("\n[2/4] Copying to container...")
# users.json 是挂载卷(/opt/troubleshoot/data/users.json -> /app/web/users.json)
# 不能 docker cp,需直接更新宿主机挂载文件
MOUNTED_FILES = {'skill/code/web/users.json'}
for f in BACKEND_FILES:
if f in MOUNTED_FILES:
local = os.path.join(REPO, f.replace('/', os.sep))
mount_target = '/opt/troubleshoot/data/users.json'
sf.put(local, mount_target)
print(f" [OK] {f} -> host mount {mount_target}")
continue
remote = f'{REMOTE_BASE}/{f}'
container_path = f'/app/web/{f.replace("skill/code/web/", "")}'
stdin, stdout, stderr = s.exec_command(
f'sudo docker cp "{remote}" "{CONTAINER}:{container_path}"'
)
err = stderr.read().decode().strip()
if err:
print(f" [FAIL] {f}: {err}")
else:
print(f" [OK] {container_path}")
# ============================================================
# Step 3: 上传前端 dist
# ============================================================
print("\n[3/4] Uploading frontend dist...")
local_dist = os.path.join(REPO, 'frontend', 'dist')
for root, dirs, files in os.walk(local_dist):
for fn in files:
lpath = os.path.join(root, fn)
rel = os.path.relpath(lpath, local_dist)
rpath = f'{REMOTE_BASE}/frontend/dist/{rel.replace(chr(92), "/")}'
rdir = os.path.dirname(rpath)
s.exec_command(f'mkdir -p "{rdir}"')
sf.put(lpath, rpath)
# 同步到宿主机挂载目录
s.exec_command('sudo rm -rf /opt/troubleshoot/dist/*')
s.exec_command('sudo cp -r /data/third_party/monitor-platform/frontend/dist/* /opt/troubleshoot/dist/')
print(" [OK] Frontend dist deployed to /opt/troubleshoot/dist/")
# ============================================================
# Step 4: 重启容器
# ============================================================
print("\n[4/4] Restarting container...")
stdin, stdout, stderr = s.exec_command('sudo docker restart troubleshoot')
out = stdout.read().decode().strip()
err = stderr.read().decode().strip()
print(f" {'[OK]' if 'troubleshoot' in out else '[FAIL]'} {out or err}")
# 等待启动
print("\nWaiting for service...")
time.sleep(8)
# 验证
print("\nVerifying...")
stdin, stdout, stderr = s.exec_command(
'curl -s http://localhost/api/health | python3 -m json.tool 2>/dev/null || curl -s http://localhost/api/health'
)
result = stdout.read().decode().strip()
if result:
print(f" Health check response:\n{result[:500]}")
if 'ok' in result.lower() or 'success' in result.lower():
print("\n[OK] Deployment verified successfully!")
else:
print("\n[WARN] Service may not be fully ready yet")
else:
err_text = stderr.read().decode().strip()
print(f" [FAIL] {err_text}")
# 验证容器内文件
print("\nVerifying container files...")
stdin, stdout, stderr = s.exec_command(
'sudo docker exec troubleshoot ls -la /app/web/services/service_manage.py /app/web/routes/service_manage.py 2>&1'
)
print(stdout.read().decode().strip())
sf.close()
s.close()
print("\n" + "=" * 60)
print(" Deployment Complete!")
print("=" * 60)
if __name__ == '__main__':
main()
\ No newline at end of file
sf.put(lpath, rpath)
# 4) 复制到最终目录
s.exec_command(f'sudo cp -r {REMOTE_BASE}/frontend/dist/* {DIST_DIR}/')
s.exec_command(f'sudo chown -R ubains:ubains {DIST_DIR}')
print(" [OK] Frontend dist deployed (clean)")
# Verify
stdin, stdout, stderr = s.exec_command(f'ls {DIST_DIR}/assets/ | wc -l')
print(f" Dist assets: {stdout.read().decode().strip()} files")
stdin, stdout, stderr = s.exec_command(f'ls {DIST_DIR}/assets/ManageLayout*')
manage = stdout.read().decode().strip().split('\n')
print(f' ManageLayout: {len(manage)} files')
for m in manage:
print(f' {m}')
# Restart service
print("\nRestarting troubleshoot service...")
s.exec_command('sudo systemctl restart troubleshoot')
time.sleep(8)
# Check health
stdin, stdout, stderr = s.exec_command('curl -s http://localhost:8088/api/health')
health = stdout.read().decode().strip()
print(f" Health: {'OK' if 'ok' in health else 'FAIL'}")
sf.close()
s.close()
print("\n=== Deployment Complete ===")
\ No newline at end of file
......@@ -68,7 +68,8 @@ _EXCLUDE_SUFFIXES = ('.pyc', '.pyo')
_UNIX_LINE_END_SUFFIXES = ('.sh', '.template')
# 服务监测模块需要的 Python 依赖(SSH 远程执行 + 定时调度)
_REQUIRED_PACKAGES = ['paramiko', 'cryptography', 'apscheduler', 'croniter', 'pytz']
# pycryptodome:服务管理 5.44 代理签名(AES-CBC)必需
_REQUIRED_PACKAGES = ['paramiko', 'cryptography', 'apscheduler', 'croniter', 'pytz', 'pycryptodome']
def _check_and_install_deps(ssh):
......@@ -168,10 +169,11 @@ def upload_files():
print("\n[4/5] Uploading files...")
sftp = ssh.open_sftp()
# 确保远程 web 目录存在
# 确保远程目录存在
ssh.exec_command(f'mkdir -p {REMOTE_WEB_DIR}')[1].channel.recv_exit_status()
# 确保远程 utils 目录存在
ssh.exec_command(f'mkdir -p {REMOTE_BASE}/web/utils')[1].channel.recv_exit_status()
# 服务管理模块数据目录(DATA_DIR=部署时 /opt/troubleshoot:targets.json / projects.json / uploads/)
ssh.exec_command(f'mkdir -p {REMOTE_BASE}/service_manage_data/uploads')[1].channel.recv_exit_status()
# 上传单个文件
for local_rel, remote_rel in FILES_TO_UPLOAD:
......
......@@ -57,6 +57,7 @@ _VUE_DIST_CANDIDATES = [
PROJECT_ROOT / "frontend" / "dist",
PROJECT_ROOT.parent / "frontend" / "dist", # 开发环境:skill/code -> 仓库根
PROJECT_ROOT.parent.parent / "frontend" / "dist", # 备用
PROJECT_ROOT / "dist", # 部署环境:/opt/troubleshoot/dist
]
VUE_DIST_DIR = next((p for p in _VUE_DIST_CANDIDATES if p.exists()), _VUE_DIST_CANDIDATES[0])
......@@ -175,6 +176,9 @@ def create_app():
# Vue SPA 前端(已构建)
if _vue_built():
# 处理 /assets/ 静态资源 —— 直接返回真实文件,而非 index.html
if path.startswith('assets/'):
return send_from_directory(VUE_DIST_DIR, path)
return send_from_directory(VUE_DIST_DIR, 'index.html')
# ===== Vue 未构建时的 Flask 模板回退 =====
......@@ -208,7 +212,7 @@ def create_app():
# 服务管理页面
if path == 'service-manage':
return redirect(url_for('service-manage.page_authorization'))
return redirect('/service-manage/authorization')
if path == 'service-manage/authorization':
return render_template('service_manage/authorization.html',
user=user, is_admin=(role == 'admin'),
......
......@@ -113,7 +113,7 @@ class Five44Client:
if body_data is not None:
sign_str = (
x_timestamp
+ json.dumps(body_data, separators=(",", ":"), ensure_ascii=False)
+ json.dumps(body_data, separators=(",", ":"), ensure_ascii=True)
+ x_random
)
else:
......@@ -310,7 +310,7 @@ class Five44Client:
Returns: (文件字节, 服务端文件名)
"""
path = f"/exapi/system/downLincenceFile?category={LICENSE_CATEGORY}"
resp = self._request("GET", path)
resp = self._request("POST", path)
if resp.status_code >= 400:
raise Five44Error(
f"下载激活文件失败:HTTP {resp.status_code}"
......@@ -383,11 +383,16 @@ class Five44Client:
resp = self._request("GET", path)
if resp.status_code >= 400:
raise Five44Error(f"导出自检文档失败:HTTP {resp.status_code}")
return resp.content, self._download_name(resp, "checkList.xlsx")
return resp.content, self.download_name(resp, "checkList.xlsx")
@staticmethod
def download_name(resp: requests.Response, default: str) -> str:
"""从 Content-Disposition 解析文件名,解析失败用默认名。"""
"""从 Content-Disposition 解析文件名,解析失败用默认名。
注意:5.44 可能返回带路径的文件名(如 C:\\fakepath\\file.xlsx),
此处净化只保留 basename,避免下游拼接路径时产生深层嵌套。
"""
import os
import re
disposition = resp.headers.get("Content-Disposition") or ""
......@@ -398,5 +403,6 @@ class Five44Client:
if match:
name = (match.group(1) or match.group(2) or "").strip()
if name:
return name
# 净化:只保留文件名,去除路径信息
return os.path.basename(name)
return default
\ No newline at end of file
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论