提交 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 \
......
# HANDOFF 服务管理模块交接文档
> 文档生成时间:2026-08-13
> 文档生成时间:2026-08-17
> 分支:troubleshoot-ai-assistant
> 状态:后端代理转发开发完成(Vue SPA 前端 + Flask 后端 + 5.44 代理客户端)
> 状态:服务管理模块已 **完整部署到 5.60 生产环境并验证通过** ✅
> 覆盖:5.44 代理转发 3 项修复 + 页面 500 / assets 302 / 部署模式(systemd)4 项二次修复
---
......@@ -13,7 +14,8 @@
**关键里程碑**
1. 阶段一(2026-07-22):静态页面 + 基础路由 ✅
2. 阶段二(2026-08-13):后端业务层 + 5.44 代理转发 ✅
3. 阶段三(待部署):部署到 5.60 生产环境
3. 阶段三(2026-08-13):生产问题修复(代码完成)✅
4. 阶段四(2026-08-17):部署到 5.60 生产环境 + 验证通过 ✅
---
......@@ -92,6 +94,53 @@
- 全部 238 个测试通过(含核心模块 106 用例 + 服务监测模块 132 用例)
- 服务管理模块暂未单独编写测试(功能依赖 5.44 真实服务器)
### 2.7 生产问题修复(2026-08-13 本次会话)
用户报告 3 个生产问题,全部定位根因并完成代码修复(**2026-08-17 已部署到 5.60,代码已确认在线**):
| # | 问题 | 根因 | 修复 | 文件 |
|---|------|------|------|------|
| 1 | save_project 签名不匹配(`5.44 业务失败 [/exapi/system/setOtherInfo]: 非法操作,签名不匹配`) | JSON 序列化 Unicode 转义差异:JS `JSON.stringify()` 默认 `ensure_ascii=true`(非 ASCII 字符转 `\uXXXX`),Python 端误用 `ensure_ascii=False`(输出原始 UTF-8 字节),导致含中文的 body 签名串不一致 | `json.dumps(..., ensure_ascii=False)``ensure_ascii=True` | `services/five44_client.py:116` |
| 2 | 下载部署视图 500(`下载失败:Request failed with status code 500`) | 5.44 返回的 `Content-Disposition` 含完整路径(如 `C:\fakepath\deploymentView.xlsx`),`download_name()` 直接作文件名,`_download_remote()` 拼接到 `uploads/` 生成深层嵌套路径,Flask `send_file` 失败 | `download_name()` 新增 `os.path.basename()` 净化路径 | `services/five44_client.py:389` |
| 3 | 下载自检文档 500 | 同 #2,复用同一 `download_name()` 路径净化修复 | 同 #2 | 同 #2 |
**排查关键过程(签名问题)**
- 下载 5.44 新版 platform JS bundle(87KB,`app.c0b07f468dc71fa73cdc2.0.2632.642 2026-08-11.js`),逆向其 axios 请求拦截器签名逻辑
- 新 bundle 签名链:`e = "Bearer " + access_token`**Bearer 前缀**)→ `SHA256(e)[16:32]` 为 AES key、`SHA256(e)[0:8]+SHA256(e)[-8:]` 为 IV → AES-CBC(PKCS7) 加密 `SHA256(timestamp + JSON.stringify(body) + random)`
- 容器内逐一验证变体:raw token 密钥、无 Bearer 头、`convertNumbersToStrings``sort_keys`、body 字节序列化方式,全部仍报"签名不匹配"
- 最终定位:登录 payload 纯 ASCII(`ensure_ascii` 无影响)所以登录成功;save_project payload 含中文,两种序列化产出不同字节 → 签名串不同 → 改 `ensure_ascii=True` 后与 JS 一致
**部署清单同步**`deploy/deploy_service_manage.py``BACKEND_FILES` 此前**漏了 `five44_client.py`**,本次补上。
### 2.8 生产部署与二次修复(2026-08-17 本次会话)⭐
阶段四部署到 5.60 过程中发现并修复 4 个新问题,全部已在生产生效:
| # | 问题 | 根因 | 修复 |
|---|------|------|------|
| 1 | 部署模式判定错误(Docker 容器 exited、8088 端口被占) | 服务实际跑在 systemd `troubleshoot.service``python3 /opt/troubleshoot/web/server.py`),Docker 只是遗留容器 | 部署脚本改为 cp 到 `/opt/troubleshoot/web/` + `systemctl restart troubleshoot` |
| 2 | `/service-manage` 访问 500(内部服务器错误) | `server.py``url_for('service-manage.page_authorization')` 构建跳转,但该 Flask 端点不存在(页面路由已迁移 Vue SPA)→ BuildError | 硬编码重定向 `redirect('/service-manage/authorization')``server.py:211`) |
| 3 | `/assets/*.js` 返回 302 或 index.html | ① `VUE_DIST_DIR` 候选路径未含 `/opt/troubleshoot/dist`;② `_vue_built()` 分支未处理 `assets/` 前缀,统一回退 index.html | ① 候选新增 `PROJECT_ROOT / "dist"`;② assets 前缀直接 `send_from_directory(VUE_DIST_DIR, path)``server.py:178-182`) |
| 4 | 生产 dist 残留多版本哈希资产 | 增量部署遗留旧 hash 文件 | 部署前 `sudo rm -rf /opt/troubleshoot/dist` 全量重建 + `chown ubains:ubains` |
另:`users.json` 在 5.60 为 root 属主挂载,SFTP 直写报 Permission denied —— 改为 SFTP→`/tmp``sudo cp``/opt/troubleshoot/data/users.json`
**部署验证结果(全部在 5.60 实测,2026-08-17)**
| 验证项 | 结果 |
|--------|------|
| `/assets/*.js` 带 session cookie | 200 `text/javascript` ✅ |
| SPA 页面 `/service-manage`(6 个子页) | 全部 200 ✅ |
| 下载激活文件 / 部署视图 / 自检文档 | 全部 200 ✅(本地占位 24 字节) |
| 左侧菜单 | 5 项齐全(服务授权/目标配置/服务升级/服务信息/操作日志)✅ |
| 项目信息保存 + 回填 | 成功(chen / chen)✅ |
| 远程 `five44_client.py` | `ensure_ascii=True`(L116)+ `os.path.basename`(L407)在线 ✅ |
| 浏览器实测 | `http://192.168.5.60:8088/service-manage/authorization` 完整渲染 ✅ |
> ⚠️ 三个下载当前走**本地占位**——5.60 上 targets 为空(`targets: []`)。需先在「目标配置」页添加 5.44 目标,下载才会切到真实代理,3 项修复才算端到端验证。
>
> 设计说明:服务授权页**没有目标选择器**是有意设计——目标在「目标配置」页统一维护,授权操作自动使用 `targets[0]`(`service_manage.py::_get_proxy_target`)。如需在授权页显示当前代理目标状态,待定需求。
---
## 三、架构设计
......@@ -190,6 +239,7 @@
- 所有授权操作的代理转发要求先配置目标服务器(targets 中至少有一条有效记录)
- 未配置目标时自动降级本地占位,不影响页面功能验证
- **当前 5.60 状态(2026-08-17)**:targets 为空,三个下载均返回 24 字节本地占位文件;配置 5.44 目标后自动切换真实代理
### 5.2 下载激活文件端点
......@@ -204,38 +254,40 @@
## 六、待办项
### 6.1 部署到 5.60 生产环境
### 6.1 部署到 5.60 生产环境 ✅ 已完成
1. 更新 `deploy/upload_to_server.py` 的上传清单:
- 新增 `skill/code/web/services/five44_client.py`
- 确认 `skill/code/web/services/service_manage.py` 已在清单
- 确认 `skill/code/web/routes/service_manage.py` 已在清单
- 确认 `skill/code/web/utils/paths.py` 中的服务管理路径常量已同步
- 确认前端 Vue 构建后的 dist 文件已包含
本次会话修复的 7 个问题(3 个生产修复 + 4 个二次修复)已于 **2026-08-17** 全部部署到 5.60 并验证通过:
2. 部署命令:
```bash
! cd deploy && SSH_PASSWORD='***' python upload_to_server.py
```
- 部署脚本:`deploy/deploy_service_manage.py` 重构为 systemd 模式,含 `sudo rm -rf` 全量重建 dist
- `BACKEND_FILES` 已含 `five44_client.py``server.py``paths.py` 等全部关键文件
- 部署命令:`! cd deploy && SSH_PASSWORD='***' python deploy_service_manage.py`
- 部署后验证:页面访问 200、assets 200、3 个下载接口 200、项目信息保存回填正常
> ⚠️ 3 个下载接口当前走本地占位(5.60 targets 为空)。配置 5.44 目标后走真实代理,3 项修复才算端到端验证。
### 6.2 配置 5.44 目标完成代理端到端验证 ⏳ 最高优先级
3. 部署后验证:
- 在浏览器中访问 `/service-manage/authorization` 确认页面正常
- 配置目标服务器(5.44)
- 测试授权文件下载/上传流程,使用 `F12` 网络面板确认请求转发到 5.44
在服务管理 → 🎯 目标配置页新增目标(按 §4.1 凭据),使授权操作从本地占位切到真实代理:
### 6.2 编写单元测试
- **保存项目信息**:填中文内容 → 不再报"非法操作,签名不匹配"(验证 ensure_ascii 修复)
- **下载部署视图**:返回真实 `deploymentView.xlsx`(非 24 字节占位),不再 500(验证 basename 修复)
- **下载自检文档**:返回真实 `checkList.xlsx`(非 24 字节占位),不再 500(验证 basename 修复)
可页面手动添加,或调 API:`POST /api/service-manage/targets`(admin 登录态)。
### 6.3 编写单元测试
-`service_manage.py` 的业务函数编写测试(目标 CRUD、项目信息读写)
-`five44_client.py` 编写 mock 测试(签名算法、登录流程、业务接口)
- 目标:核心函数覆盖率 > 80%
### 6.3 前端页面完善
### 6.4 前端页面完善
- 服务升级页(Upgrade.vue)的后端 API 尚未实现
- 服务信息页(Info.vue)的实时监控数据获取尚未实现
- 操作日志页(Logs.vue)的后端 API 尚未实现
### 6.4 服务升级与回滚
### 6.5 服务升级与回滚
- 服务重启/回滚的后端 API 需要开发
- 下载配置、备份数据库、上传更新服务的功能需要实现
......@@ -244,6 +296,15 @@
## 七、踩坑警示
### 7.0 JSON 签名串的 Unicode 转义(最重要,本次踩过)⭐
- **JS `JSON.stringify()` 默认 `ensure_ascii=true`**:所有非 ASCII 字符(中文等)转成 `\uXXXX` 转义序列
- **Python `json.dumps()` 默认 `ensure_ascii=True`,但本项目原代码却显式传 `ensure_ascii=False`** → 输出原始 UTF-8 字节
- **签名串 = timestamp + JSON(body) + random**,因此只要 body 含中文,两端序列化字节不同 → SHA256 不同 → 签名不匹配 → 5.44 返回"非法操作,签名不匹配"
- **登录请求 payload 纯 ASCII(username/password/code/uuid)**`ensure_ascii` 无影响,所以登录一直成功 —— 这一假象掩盖了签名问题
- **结论**:与 CryptoJS / JS 前端对齐的签名场景,`json.dumps` 必须 `ensure_ascii=True`,且 `separators=(",", ":")` 去空格
- 排查时若所有 token 变体(raw/Bearer/无前缀)都失败,应优先怀疑 body 序列化而非密钥派生
### 7.1 5.44 签名算法
- AES 密钥和 IV 按 UTF-8 字符串字节解析(与 CryptoJS 行为一致),**不是 hex 解码**
......@@ -267,6 +328,35 @@
- 路径常量从 `utils/paths.py` 取,不要在各文件重算
- 依赖方向单向:routes → services → utils,禁止反向导入
### 7.5 5.44 下载文件的 Content-Disposition 含完整路径 ⭐ 本次踩过
- 5.44 的 `exportDeployView` / `exportServiceDetect` 返回的 `Content-Disposition``filename`**完整路径**(如 `C:\fakepath\deploymentView.xlsx`),不是纯文件名
- 直接作本地文件名会与 `uploads/` 拼接出深层嵌套目录,`send_file` 报 500
- `download_name()` 已用 `os.path.basename()` 净化,**不要去掉这层净化**
### 7.6 部署清单漏文件 ⭐ 本次踩过
- `deploy_service_manage.py``BACKEND_FILES`**漏了 `five44_client.py`**,导致旧签名代码未更新
- 新增后端文件后,务必检查 `deploy_service_manage.py``deploy/upload_to_server.py``DIRS_TO_UPLOAD``services` 目录整体上传已覆盖)
### 7.7 5.60 服务是 systemd 而非 Docker ⭐ 本次踩过
- 生产服务跑在 systemd `troubleshoot.service``python3 /opt/troubleshoot/web/server.py`),**不是 Docker**
- 5.60 上存在同名遗留容器(exited),曾被误认为部署目标;重启容器报 `address already in use` 正是因为端口被 systemd 服务占用
- 部署路径:后端 `/opt/troubleshoot/web/`、前端 `/opt/troubleshoot/dist/`、数据 `/opt/troubleshoot/data/`
- `/opt/troubleshoot/data/` 下部分文件为 root 属主,SFTP 直写报 Permission denied → 走 `/tmp` + `sudo cp`
### 7.8 SPA 回退路由的三层陷阱 ⭐ 本次踩过
- **端点不存在即 500**:页面迁移 Vue 后,Flask 侧 `url_for('bp.page_xxx')` 指向已删除端点会直接 BuildError。跳转统一用硬编码路径(如 `/service-manage/authorization`
- **VUE_DIST_DIR 候选要覆盖部署布局**:生产是 `/opt/troubleshoot/dist`(PROJECT_ROOT/dist),本地是 `frontend/dist``server.py``_VUE_DIST_CANDIDATES` 两边都要有,漏了就静默回退 Flask 模板
- **assets 前缀必须单独处理**:SPA 回退统一返回 index.html 会劫持 `/assets/*.js``_vue_built()` 分支内 `path.startswith('assets/')``send_from_directory` 真实文件
### 7.9 远程验证的两个假象 ⭐ 本次踩过
- **assets 不带 cookie 302 是正常行为**:验证静态资源务必带 session cookie(`curl -b`),否则看到的是登录重定向,不是 bug
- **paramiko `exec_command` 是异步的**:连续多条命令(登录写 cookie → 带 cookie 请求)会并发执行,读到未写完的 cookie 文件产生**假 401**。远程串联验证必须写在**同一条 shell 命令**里用 `&&` 连接
---
## 八、相关文档
......
# -*- 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 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论