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

feat(platform): 平台化改造 + 用户密码更新

- 新增平台首页路由 /(platform Blueprint),平台名称改为"运行维护平台"
- 新增 modules.py 声明式模块清单 + get_modules(role) 按角色过滤
- 新增 platform.html 平台首页模板(卡片网格 + 响应式布局)
- 原 / 路由从 auth.py 迁移至 platform.py,troubleshoot 独立为 /troubleshoot
- index.html 加"返回首页"导航链接
- test_routes_auth.py TestIndex 适配平台首页断言
- users.json 更新 admin/user 密码哈希
- 新增 ARCHITECTURE.md 技术架构文档
- deploy 注释更新
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 b4fad0c1
# 运行维护平台 — 技术架构与部署说明
## 1. 开发框架
### 1.1 技术栈
| 层 | 技术 | 版本 | 说明 |
|----|------|------|------|
| Web 框架 | Flask | 2.x | Python 轻量级 Web 框架,应用工厂模式 |
| 搜索引擎 | TF-IDF(自研) | — | 基于词频-逆文档频率的文本相似度搜索 |
| 向量搜索 | Embedding API(预留) | — | OpenAI 兼容 `/v1/embeddings`,当前关闭 |
| AI 分析 | GLM 系列(智谱) | glm-5.1 / glm-4-flash / glm-4-plus | 通过 OpenAI 兼容 API 调用 |
| 前端 | 原生 HTML/CSS/JS | — | 单页应用,SSE 流式输出,无前端框架 |
| 认证 | Flask Session | — | 服务端 Session + Werkzeug 密码哈希 |
| 缓存 | 文件缓存(自研) | — | JSON 文件 + TTL 过期清理 |
| 安全过滤 | 正则匹配(自研) | — | 危险命令检测 + 敏感信息脱敏 |
| 测试 | pytest | 9.x | 145 用例,覆盖率 85%~99% |
| 部署 | systemd + nohup | — | Linux 服务管理 |
### 1.2 项目结构
```
skill/code/web/ # Web 服务(唯一开发源)
├── server.py # 入口:create_app() 应用工厂 + 启动(~150 行)
├── container.py # 依赖容器:单例 + config 集中
├── auth.py # UserManager 认证
├── decorators.py # login_required / admin_required / page_login_required
├── search_engine.py # 搜索引擎(TF-IDF + Embedding 降级兜底)
├── safety_filter.py # 安全过滤器
├── cache_manager.py # 缓存管理器
├── config.json # 运行配置(模型/Embedding 开关等)
├── users.json # 用户数据
├── routes/ # 路由层(Blueprint)
│ ├── platform.py # / 平台首页(模块卡片)
│ ├── auth.py # /login /logout /api/user/info
│ ├── troubleshoot.py # /troubleshoot /api/troubleshoot /search /analyze /stream /health /projects /categories
│ ├── cache.py # /api/cache/stats /clear
│ ├── export.py # /api/export
│ └── submit.py # /api/submit
├── services/ # 业务层
│ ├── ai_service.py # build_prompt / call_claude_api / 流式
│ └── record_service.py # rebuild_search_index
├── utils/ # 工具层
│ ├── modules.py # 模块清单声明 + get_modules()
│ ├── paths.py # 路径常量
│ ├── vector_builder.py # 向量预计算脚本(预留)
│ ├── audit.py # log_audit
│ ├── record_utils.py # 入库纯函数
│ ├── logger.py # 通用日志
│ ├── error_codes.py # 统一错误码
│ └── response.py # success/error 响应封装
└── templates/ # Jinja2 模板
├── platform.html # 平台首页(模块卡片)
├── index.html # 排查助手主页
└── login.html # 登录页
```
### 1.3 架构设计
```
┌──────────────────────────────────────────────────────┐
│ Flask App (create_app) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌───────────────────┐ │
│ │ platform │ │ auth │ │ troubleshoot │ │
│ │ Blueprint │ │ Blueprint│ │ Blueprint │ │
│ │ / │ │ /login │ │ /troubleshoot │ │
│ │ (首页) │ │ /logout │ │ /api/troubleshoot│ │
│ │ │ │ /api/... │ │ /api/search │ │
│ └──────────┘ └──────────┘ │ /api/analyze │ │
│ │ /api/health │ │
│ ┌──────────┐ ┌──────────┐ └───────────────────┘ │
│ │ cache │ │ export │ ┌──────────┐ │
│ │ Blueprint│ │ Blueprint│ │ submit │ │
│ └──────────┘ └──────────┘ │ Blueprint│ │
│ └──────────┘ │
│ ┌──────────────────────────────────────────────┐ │
│ │ container.py(依赖容器) │ │
│ │ get_search_engine / get_safety_filter / │ │
│ │ get_cache_manager / get_config / get_skill_md│ │
│ └──────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ utils/(工具层) │ │
│ │ modules / paths / logger / error_codes / │ │
│ │ response / audit / record_utils │ │
│ └──────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘
```
**依赖方向**:routes/services → container → utils(单向,禁止反向)
### 1.4 模块注册机制
平台通过 `utils/modules.py` 声明式注册模块,首页动态渲染卡片:
```python
# 新增模块只需两步:
# 1. 在 modules.py 的 MODULES 列表追加声明
# 2. 在 server.py 注册对应 Blueprint
MODULES = [
{"id": "troubleshoot", "name": "问题排查助手", "icon": "🔍", ...},
{"id": "service-monitor", "name": "服务监控", "icon": "📊", "enabled": False, ...},
]
```
---
## 2. 服务器部署
### 2.1 服务器信息
| 项 | 值 |
|----|-----|
| 服务器 IP | `192.168.5.60` |
| SSH 用户 | `ubains` |
| SSH 密码 | 环境变量 `SSH_PASSWORD`(不硬编码) |
| 操作系统 | Linux(CentOS/Ubuntu) |
| Python | `python3` |
| 部署目录 | `/opt/troubleshoot/` |
| Web 目录 | `/opt/troubleshoot/web/` |
| 服务端口 | `8088` |
| 访问地址 | `http://192.168.5.60:8088` |
### 2.2 目录结构(服务器上)
```
/opt/troubleshoot/
├── web/ # Flask 应用代码
│ ├── server.py # 启动入口
│ ├── container.py # 依赖容器
│ ├── search_engine.py # 搜索引擎
│ ├── config.json # 运行配置
│ ├── users.json # 用户数据
│ ├── routes/ # 路由层
│ ├── services/ # 业务层
│ ├── utils/ # 工具层
│ ├── templates/ # 前端模板
│ ├── cache/ # 缓存目录(运行时生成)
│ ├── logs/ # 日志目录
│ │ └── app.log # 通用日志
│ └── audit.log # 审计日志
├── 搜索索引.json # 知识库索引(357 条)
├── 问题记录/ # 问题记录存储目录
└── SKILL.md # 排查技能参考文档
```
### 2.3 服务管理
#### 启动服务
```bash
cd /opt/troubleshoot/web
nohup python3 server.py > /tmp/troubleshoot.log 2>&1 &
```
#### 停止服务
```bash
pkill -f "python.*server.py"
```
#### 重启服务
```bash
pkill -f "python.*server.py" || true
sleep 2
cd /opt/troubleshoot/web
nohup python3 server.py > /tmp/troubleshoot.log 2>&1 &
```
#### 查看日志
```bash
# 启动日志
tail -f /tmp/troubleshoot.log
# 应用日志
tail -f /opt/troubleshoot/web/logs/app.log
# 审计日志
tail -f /opt/troubleshoot/web/audit.log
```
#### 健康检查
```bash
curl -s http://localhost:8088/api/health
# 期望:{"status":"ok","version":"1.3.0",...}
```
### 2.4 systemd 服务(可选,更规范)
如需开机自启,可配置 systemd:
```ini
# /etc/systemd/system/troubleshoot.service
[Unit]
Description=Troubleshoot AI Assistant
After=network.target
[Service]
Type=simple
User=ubains
WorkingDirectory=/opt/troubleshoot/web
ExecStart=/usr/bin/python3 /opt/troubleshoot/web/server.py
Restart=on-failure
RestartSec=5
Environment=FLASK_DEBUG=0
Environment=SECRET_KEY=your-secret-key-here
[Install]
WantedBy=multi-user.target
```
```bash
# 启用并启动
sudo systemctl daemon-reload
sudo systemctl enable troubleshoot
sudo systemctl start troubleshoot
# 管理
sudo systemctl status troubleshoot
sudo systemctl restart troubleshoot
sudo systemctl stop troubleshoot
```
### 2.5 部署流程
#### 自动部署(推荐)
```bash
# 在本地项目根目录执行(需设置 SSH_PASSWORD 环境变量)
cd deploy && SSH_PASSWORD='***' python upload_to_server.py
```
脚本会:
1. SSH 连接到 5.60
2. 上传代码文件和目录
3. 重启服务
4. 健康检查
> ⚠️ 脚本的健康检查 `[FAIL]` 是误报(sleep 3 秒不够),权威验证用 `verify_deployment.py`
#### 权威验证
```bash
cd deploy && python verify_deployment.py
```
#### 手动部署
```bash
# 1. 上传代码(scp 或其他方式)
scp -r skill/code/web/* ubains@192.168.5.60:/opt/troubleshoot/web/
# 2. SSH 到服务器重启
ssh ubains@192.168.5.60
pkill -f "python.*server.py" || true
sleep 2
cd /opt/troubleshoot/web
nohup python3 server.py > /tmp/troubleshoot.log 2>&1 &
# 3. 验证
sleep 15
curl -s http://localhost:8088/api/health
```
---
## 3. 环境配置
### 3.1 环境变量
| 变量 | 必需 | 说明 | 示例 |
|------|------|------|------|
| `SECRET_KEY` | 生产必需 | Flask Session 密钥 | 随机 64 位 hex |
| `CLAUDE_API_BASE` | 是 | AI API 地址 | `https://office.ubainsyun.com:8400` |
| `CLAUDE_API_KEY` | 是 | AI API 密钥 | `sk-xxx` |
| `SSH_PASSWORD` | 部署时 | 部署脚本 SSH 密码 | — |
| `FLASK_DEBUG` | 否 | 调试模式(生产设 0) | `0` |
### 3.2 config.json
```json
{
"claude_api_base": "https://office.ubainsyun.com:8400",
"claude_api_key": "在这里填写你的API密钥",
"models": [
{"id": "glm-4-flash", "name": "快速模式", "description": "5-10秒"},
{"id": "glm-5.1", "name": "标准模式", "description": "20-50秒,推荐"},
{"id": "glm-4-plus", "name": "深度模式", "description": "30-60秒"}
],
"default_model": "glm-5.1",
"embedding_enabled": false,
"embedding_model": "text-embedding-3-small",
"embedding_api_timeout": 10
}
```
---
## 4. 开发指南
### 4.1 本地开发
```bash
# 安装依赖
pip install flask pytest pytest-cov pytest-mock requests numpy
# 运行测试
cd skill/code && python -m pytest -v
# 启动服务(需先解决索引路径,见 CLAUDE.md 坑 2)
python skill/code/web/server.py
```
### 4.2 新增模块步骤
1.`utils/modules.py``MODULES` 列表追加模块声明
2.`routes/` 下新建 Blueprint 文件
3.`server.py``create_app()` 注册 Blueprint
4.`templates/` 下新建模块页面模板
5. 更新 `deploy/upload_to_server.py` 上传清单(如有新目录)
6. 测试 + 部署
### 4.3 代码分层约定
- **routes/** — 路由层:只做参数校验、调用 services、返回响应
- **services/** — 业务层:业务逻辑编排
- **utils/** — 工具层:纯函数、无副作用
- **container.py** — 单例与配置集中,不要在各模块重建模块级全局单例
- **依赖方向单向**:routes/services → container → utils
# PRD_计划执行_平台化改造与模块切换
## 1. 项目概述
### 1.1 背景
当前站点是单功能"问题排查 AI 助手",访问 `/` 直接进入排查页面。需升级为多模块平台:新增平台首页展示模块卡片,问题排查助手降级为其中一个模块入口(`/troubleshoot`),后续可按需接入其他维护模块。
### 1.2 目标
| 目标编号 | 描述 | 优先级 |
|---------|------|--------|
| 1 | 模块注册机制 `modules.py` | 🟠 高 |
| 2 | 平台首页 `/`(模块卡片) | 🟠 高 |
| 3 | 排查助手路由迁移 `/``/troubleshoot` | 🟠 高 |
| 4 | 排查助手页加返回首页导航 | 🟡 中 |
### 1.3 开发周期
预估 5.5 小时(约 1 个工作日)。
---
## 2. 技术方案
### 2.1 模块注册机制(`utils/modules.py`,新增)
集中声明模块清单,提供 `get_modules(role=None)` 查询函数:
```python
"""modules.py — 平台模块注册
集中声明平台所有可用模块,供首页渲染和路由使用。
新增模块只需在此文件追加声明 + 注册 Blueprint,不改首页代码。
"""
from flask import session
# ============================================================
# 模块清单(声明式)
# ============================================================
MODULES = [
{
"id": "troubleshoot",
"name": "问题排查助手",
"icon": "🔍",
"description": "基于历史知识库的 AI 问题排查,357 条记录",
"url": "/troubleshoot",
"enabled": True,
"roles": [], # 空 = 所有登录用户可访问
"sort": 1,
},
{
"id": "service-monitor",
"name": "服务监控",
"icon": "📊",
"description": "服务器与服务运行状态监控、告警通知",
"url": "/service-monitor",
"enabled": False, # 预留,未上线
"roles": ["admin"],
"sort": 2,
},
# 后续模块在此追加
]
def get_modules(role=None):
"""获取可用模块列表。
参数:
role: 用户角色(None=不过滤,返回全部启用模块)
返回:
按 sort 排序的模块列表
"""
result = []
for m in MODULES:
if not m.get("enabled", True):
continue
if role is not None and m.get("roles") and role not in m["roles"]:
continue
result.append(m)
result.sort(key=lambda x: x.get("sort", 99))
return result
```
**关键约束**
- 模块声明集中在此文件,不散落各处
- `roles` 为空表示所有登录用户可访问(不限制)
- `enabled: False` 的模块不展示(预留但未上线)
### 2.2 平台首页路由(`routes/platform.py`,新增)
```python
"""platform.py — 平台首页路由"""
from flask import Blueprint, render_template, session, redirect, url_for
from decorators import page_login_required
from utils.modules import get_modules
bp = Blueprint('platform', __name__)
@bp.route('/')
def index():
"""平台首页 — 模块列表"""
user = session.get('user')
if not user:
return redirect(url_for('auth.login'))
role = user.get('role', '')
modules = get_modules(role=role)
return render_template('platform.html',
modules=modules,
user=user,
platform_name="运维辅助平台",
)
```
### 2.3 平台首页模板(`templates/platform.html`,新增)
独立模板,风格与现有 `index.html` / `login.html` 一致(渐变背景 + 卡片布局):
```html
<!-- platform.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5">
<title>{{ platform_name }}</title>
<style>
/* 复用 login/index 的设计系统变量 + 渐变背景 */
/* 模块卡片网格:桌面 3 列、平板 2 列、手机 1 列 */
/* 触控热区 ≥44px */
</style>
</head>
<body>
<!-- 顶部:平台标题 + 用户信息 + 退出 -->
<!-- 主体:模块卡片网格 -->
<!-- 每个卡片:图标 + 名称 + 描述 + "进入" 按钮 -->
<!-- 空状态:暂无可用模块 -->
</body>
</html>
```
### 2.4 排查助手路由迁移
#### 2.4.1 `routes/auth.py` 改动
`auth.py``index` 路由(`/` 重定向到 `/login` 或渲染 `index.html`)需移除或改为重定向到 `/troubleshoot`
实际上 `/` 现在由 `platform.py` 接管,`auth.py``index` 路由删除即可。
#### 2.4.2 `routes/troubleshoot.py` 改动
新增 `/troubleshoot` 页面路由(渲染 `index.html`):
```python
@bp.route('/troubleshoot')
def troubleshoot_page():
"""排查助手主页"""
return render_template('index.html')
```
#### 2.4.3 `templates/index.html` 改动
顶部用户信息栏增加"返回首页"链接:
```html
<a href="/" class="back-home">🏠 返回首页</a>
```
### 2.5 `server.py` 改动
注册 `platform` Blueprint:
```python
from routes.platform import bp as platform_bp
app.register_blueprint(platform_bp)
```
### 2.6 测试方案
| 模块 | 用例 | 覆盖点 |
|------|------|--------|
| `test_routes_auth.py` | 修改 | `/` 不再重定向到 `/login`(由 platform 接管) |
| `test_routes_troubleshoot.py` | 新增 | `GET /troubleshoot` 返回 200 |
| `test_routes_platform.py` | 新增 | `/` 未登录跳 `/login`;已登录返回模块卡片;角色过滤 |
| `test_modules.py` | 新增 | `get_modules()` 角色过滤 / enabled 过滤 / sort 排序 |
---
## 3. 实施计划
### 3.1 任务分解
| 序号 | 任务 | 预计时间 | 状态 | 依赖 |
|------|------|---------|------|------|
| 1 | `utils/modules.py` 模块注册 | 0.5h | 待开始 | — |
| 2 | `routes/platform.py` 平台首页路由 | 0.5h | 待开始 | 1 |
| 3 | `templates/platform.html` 平台首页模板 | 1.5h | 待开始 | 2 |
| 4 | 排查助手路由迁移 `/``/troubleshoot` | 0.5h | 待开始 | 2 |
| 5 | `templates/index.html` 加返回首页导航 | 0.5h | 待开始 | 4 |
| 6 | `server.py` 注册 platform Blueprint | 0.5h | 待开始 | 2 |
| 7 | 测试用例更新 + 新增 | 1h | 待开始 | 4, 5, 6 |
| 8 | 部署 + 验证 | 0.5h | 待开始 | 7 |
---
## 4. 测试验证
### 4.1 验证项
| 验收项 | 标准 | 实测 |
|--------|------|------|
| `/` 显示模块卡片 | 登录后看到"问题排查助手"卡片 | — |
| 卡片点击进入模块 | 跳转到 `/troubleshoot` | — |
| `/troubleshoot` 显示排查助手 | 原有功能全部正常 | — |
| 返回首页 | 排查助手页顶部有"返回首页"链接 | — |
| 角色过滤 | 普通用户/管理员看到各自模块 | — |
| 未登录 | `/``/login` | — |
| 移动端 | 首页卡片单列/多列自适应 | — |
| API 不变 | `/api/*` 全部正常 | — |
### 4.2 回归测试
- [ ] 145 用例全绿(路由变更需同步改测试 fixture)
- [ ] 排查助手全流程手动验证
### 4.3 端到端验证
```bash
# 1. 首页验证
curl -s http://192.168.5.60:8088/ -o /dev/null -w "%{http_code}"
# 期望:302(未登录跳 /login)
# 2. 登录后访问首页
# 期望:看到模块卡片
# 3. 排查助手验证
curl -s http://192.168.5.60:8088/api/health
# 期望:ok, 357 记录
# 4. 浏览器验证
# 登录 → 首页 → 点击"问题排查助手" → 排查页面 → 返回首页
```
---
## 5. 执行记录
### 5.1 执行日志
| 日期 | 任务 | 执行人 | 结果 | 备注 |
|------|------|--------|------|------|
| — | — | — | — | 待执行 |
### 5.2 问题记录
| 日期 | 问题 | 解决方案 | 状态 |
|------|------|---------|------|
| — | — | — | — |
---
## 6. 注意事项
1. **路由冲突**`platform.py` 注册 `/``auth.py` 原有 `/` 路由必须删除,否则 Flask 报 `AssertionError: duplicate route`
2. **Blueprint 注册顺序**`platform` Blueprint 需在 `auth` 之前注册(或确保 `/` 只定义一次)
3. **前端 API 路径不变**:排查助手 JS 中所有 `/api/...` 是绝对路径,不受主页路径迁移影响
4. **测试 fixture 更新**`conftest.py``app` fixture 创建的 test client 需适配新路由(`/` 不再是排查助手主页)
5. **部署清单同步**:新增 `modules.py` / `platform.py` / `platform.html` 需加入 `upload_to_server.py`
---
## 7. 相关文档
- [PRD_需求文档_平台化改造与模块切换](PRD_需求文档_平台化改造与模块切换.md) — 本项目需求文档
- [PRD_需求文档_P2级功能增强](PRD_需求文档_P2级功能增强.md) — P2 背景
# PRD_需求文档_平台化改造与模块切换
## 基本信息
| 项目 | 内容 |
|------|------|
| 文档类型 | 需求文档 |
| 创建日期 | 2026-07-14 |
| 最后更新 | 2026-07-14 |
| 负责人 | 研发组(Claude 协助) |
| 优先级 | P2 🟠 |
| 状态 | 待开始 |
---
## 一、背景与目标
### 1.1 问题背景
当前「问题排查 AI 助手」是一个独立单功能站点:访问 `/` 直接进入排查助手主页面。平台后续将承载**多个维护类模块**(不仅限于问题知识助手),需要把现有站点升级为一个可扩展的**多模块平台**,问题排查助手降级为其中一个模块。
| 序号 | 现状 | 问题 |
|------|------|------|
| 1 | `/` 直接是排查助手主页 | 无法承载多模块,新模块无入口 |
| 2 | 站点定位是"问题排查助手" | 与"运维平台"定位不符,扩展性差 |
| 3 | 无模块切换机制 | 多模块并存时用户无法在模块间导航 |
### 1.2 修复目标
1. **新增平台首页** `/`:作为统一入口,以卡片/列表形式展示所有可用模块,点击进入对应模块
2. **问题排查助手降级为模块**:原 `/` 主页迁移到 `/troubleshoot`,首页改为模块列表
3. **预留服务监控模块**:在模块清单中声明"服务监控"模块(`enabled: false`,仅管理员可见),为后续接入预留入口
4. **建立模块注册机制**:通过配置声明模块清单,首页动态渲染,便于后续接入新模块
5. **共用服务与认证**:所有模块共用同一 Flask 服务、用户认证、缓存管理,各模块独立路由与页面
6. **保持现有功能零回归**:问题排查助手的搜索/AI 分析/提交/导出/缓存管理等全部功能不变,仅路径调整
### 1.3 范围说明
- 本次**只建框架**,先接入"问题排查助手"一个模块,**预留"服务监控"模块**`enabled: false`),后续按需接入
- 不涉及微服务拆分,全部在现有 Flask 服务内通过路由隔离实现
---
## 二、需求详情
### 2.1 模块注册机制
#### 2.1.1 需求规格
平台通过配置声明模块清单,首页动态渲染卡片。模块清单字段:
| 字段 | 说明 | 示例 |
|------|------|------|
| `id` | 模块唯一标识 | `troubleshoot` |
| `name` | 模块显示名 | `问题排查助手` |
| `icon` | 模块图标(emoji 或图标类) | `🔍` |
| `description` | 模块一句话描述 | `基于历史知识库的 AI 问题排查` |
| `url` | 模块入口路径 | `/troubleshoot` |
| `enabled` | 是否启用 | `true` |
| `roles` | 允许访问的角色(空=所有登录用户) | `[]` |
| `sort` | 排序权重 | `1` |
#### 2.1.2 实现方式
-`config.json` 新增 `modules` 数组,或新建 `utils/modules.py` 集中声明(推荐后者,便于扩展)
- 首页通过 `get_modules()` 读取清单并渲染卡片
#### 2.1.3 验收标准
- [ ] 模块清单可通过配置增删
- [ ] `enabled: false` 的模块不在首页展示
- [ ] 模块按 `sort` 排序展示
---
### 2.2 平台首页(新增)
#### 2.2.1 需求规格
| 项 | 规格 |
|----|------|
| 路径 | `/`(覆盖原排查助手主页) |
| 访问控制 | 登录后可见(未登录跳 `/login`) |
| 页面内容 | 顶部:平台标题 + 用户信息 + 退出;主体:模块卡片网格 |
| 卡片内容 | 图标 + 模块名 + 描述 + "进入"按钮 |
| 卡片点击 | 跳转到模块 `url` |
| 角色过滤 | 按 `modules[].roles` 过滤当前用户可见模块 |
| 响应式 | 移动端单列、桌面多列(沿用 P2-2 断点策略) |
| 空状态 | 无可用模块时显示"暂无可用模块"提示 |
#### 2.2.2 验收标准
- [ ] 访问 `/` 显示模块列表(非排查助手主页)
- [ ] 未登录跳转 `/login`
- [ ] 卡片点击进入对应模块
- [ ] 移动端响应式正常
- [ ] 普通用户/管理员看到各自有权限的模块
---
### 2.3 问题排查助手降级为模块
#### 2.3.1 路径调整
| 原路径 | 新路径 | 说明 |
|--------|--------|------|
| `/`(排查助手主页) | `/troubleshoot` | 排查助手主页迁移 |
| `/login` `/logout` | `/login` `/logout` | 不变(平台级认证) |
| `/api/*` | `/api/*` | 不变(API 路径保持) |
#### 2.3.2 需求规格
- 排查助手主页路由从 `/` 改为 `/troubleshoot`
- 排查助手页面顶部增加"返回平台首页"入口(链接到 `/`
- 排查助手所有功能(搜索/分析/提交/导出/缓存)**零变更**
- 前端 JS 中的 API 调用路径(相对路径 `/api/...`**无需改**
#### 2.3.3 验收标准
- [ ] `/troubleshoot` 正常显示排查助手主页
- [ ] `/` 显示平台首页(非排查助手)
- [ ] 排查助手全部功能正常(搜索/AI 分析/提交/导出/缓存)
- [ ] 排查助手页面有返回首页入口
---
### 2.4 平台导航
#### 2.4.1 需求规格
- 排查助手(及后续模块)页面顶部增加统一导航:平台名 + "返回首页"链接
- 首页顶部:平台标题 + 用户信息 + 退出(沿用现有用户信息栏样式)
#### 2.4.2 验收标准
- [ ] 各模块页可一键返回平台首页
- [ ] 平台标题一致
---
## 三、影响范围
### 3.1 涉及文件
| 文件 / 目录 | 变更类型 | 说明 |
|------------|---------|------|
| `web/utils/modules.py` | 新增 | 模块清单声明 + `get_modules()` |
| `web/routes/platform.py` | 新增 | 平台首页路由 `/` |
| `web/routes/auth.py` | 修改 | `/` 重定向改为 `/troubleshoot` 之外的处理(或交由 platform 路由) |
| `web/routes/troubleshoot.py` 或 server.py | 修改 | 排查助手主页路由 `/``/troubleshoot` |
| `web/templates/platform.html` | 新增 | 平台首页模板(模块卡片) |
| `web/templates/index.html` | 修改 | 顶部加"返回首页"导航 |
| `web/config.json` | 修改 | 可选:模块配置(若用配置驱动) |
| `deploy/upload_to_server.py` | 修改 | 上传清单补新文件 |
### 3.2 风险评估
| 风险项 | 等级 | 缓解措施 |
|--------|------|---------|
| 原 `/` 路径行为变化,外部书签失效 | 🟠 中 | `/troubleshoot` 与原 `/` 行为一致;老用户从首页点入即可 |
| 排查助手前端 JS 路径依赖 | 🟡 低 | API 用相对路径 `/api/...`,不随主页路径变化 |
| 认证流程受影响 | 🟡 低 | `/login` 不变,首页加 `@page_login_required` |
| 移动端首页布局 | 🟡 低 | 复用 P2-2 断点策略 |
---
## 四、非功能需求
| 项 | 要求 |
|----|------|
| 扩展性 | 新增模块只需在 `modules.py` 声明 + 注册路由,不改首页代码 |
| 兼容性 | 排查助手功能零回归;现有测试用例保持全绿 |
| 性能 | 首页轻量,无额外 API 调用(模块清单内存读取) |
| 安全 | 首页需登录;模块按角色过滤 |
| 可维护性 | 模块声明集中管理,不散落各处 |
---
## 五、时间估算
| 任务 | 工时 |
|------|------|
| 模块注册机制 `modules.py` | 0.5h |
| 平台首页路由 + 模板 | 2h |
| 排查助手路由迁移 `/``/troubleshoot` | 0.5h |
| 排查助手页加返回首页导航 | 0.5h |
| 移动端适配首页 | 1h |
| 测试 + 部署 | 1h |
| **合计** | **5.5h** |
---
## 六、验收清单
### 模块注册
- [ ] `modules.py` 声明模块清单
- [ ] `get_modules()` 支持角色过滤
### 平台首页
- [ ] `/` 显示模块卡片列表
- [ ] 未登录跳 `/login`
- [ ] 卡片点击进入模块
- [ ] 移动端响应式
### 排查助手降级
- [ ] `/troubleshoot` 显示排查助手主页
- [ ] 排查助手全部功能正常
- [ ] 页面有返回首页入口
### 测试与部署
- [ ] 现有 145 用例全绿(路由变更需同步改测试)
- [ ] 部署到 5.60 验证
---
## 维护记录
| 日期 | 更新内容 | 更新人 |
|------|---------|--------|
| 2026-07-14 | 初版创建 | 研发组 |
......@@ -47,10 +47,10 @@ DEPLOY_FILES_TO_UPLOAD = [
# 需要上传的目录(相对 LOCAL_BASE → 远程 web 目录下同名子目录)
# P1-1 新增 utils/ 模块;P1-3 新增 routes/ services/ 目录;P1 收尾新增 templates
DIRS_TO_UPLOAD = [
('utils', 'web/utils'),
('routes', 'web/routes'), # P1-3:5 个 Blueprint
('utils', 'web/utils'), # 含 modules.py / vector_builder.py 等
('routes', 'web/routes'), # 含 platform.py(平台首页)
('services', 'web/services'), # P1-3:ai_service / record_service
('templates', 'web/templates'), # P1 收尾:errMsg 前端兼容适配
('templates', 'web/templates'), # 含 platform.html
]
def upload_files():
......
......@@ -92,7 +92,7 @@ class TestUserInfo:
class TestIndex:
"""首页"""
"""平台首页(/ 由 platform Blueprint 接管)"""
def test_index_not_logged_in_redirect(self, client):
"""未登录访问 / 重定向到 /login"""
......@@ -101,6 +101,8 @@ class TestIndex:
assert r.headers["Location"].endswith("/login")
def test_index_logged_in(self, auth_client):
"""已登录访问 / 返回主页"""
"""已登录访问 / 返回平台首页(模块卡片)"""
r = auth_client.get("/")
assert r.status_code == 200
# 平台首页应包含模块名
assert b"\xe9\x97\xae\xe9\xa2\x98" in r.data # "问题" UTF-8
......@@ -12,7 +12,7 @@ from flask import Blueprint, request, jsonify, render_template, session, redirec
import container
from auth import user_manager
from decorators import login_required, page_login_required
from decorators import login_required
from utils.audit import log_audit
from utils.response import error_response
from utils.error_codes import ErrorCodes
......@@ -110,13 +110,3 @@ def get_user_info():
'success': True,
'user': session.get('user')
})
@bp.route('/')
@page_login_required
def index():
"""首页"""
return render_template('index.html',
projects=container.get_search_engine().get_projects(),
system_types=container.get_config().get('system_types', []),
)
# -*- coding: utf-8 -*-
"""
platform.py — 平台首页路由
提供平台级首页(模块列表),作为统一入口。
各模块独立 Blueprint,首页通过 modules.py 动态渲染模块卡片。
"""
from flask import Blueprint, render_template, session, redirect, url_for
from utils.modules import get_modules
from utils.logger import get_logger
logger = get_logger(__name__)
bp = Blueprint('platform', __name__)
@bp.route('/')
def index():
"""平台首页 — 模块列表"""
user = session.get('user')
if not user:
return redirect(url_for('auth.login'))
role = user.get('role', '')
modules = get_modules(role=role)
return render_template('platform.html',
modules=modules,
user=user,
platform_name="运行维护平台",
)
......@@ -12,9 +12,10 @@ import json
import time
from datetime import datetime
from flask import Blueprint, request, jsonify, Response, session
from flask import Blueprint, request, jsonify, Response, session, render_template
import container
from decorators import page_login_required
from services.ai_service import build_prompt, call_claude_api, call_claude_api_stream
from utils.audit import log_audit
from utils.response import error_response
......@@ -26,6 +27,16 @@ logger = get_logger(__name__)
bp = Blueprint('troubleshoot', __name__)
@bp.route('/troubleshoot')
@page_login_required
def troubleshoot_page():
"""排查助手主页"""
return render_template('index.html',
projects=container.get_search_engine().get_projects(),
system_types=container.get_config().get('system_types', []),
)
def format_matched_cases(matched_cases_data):
"""格式化匹配案例数据为标准返回格式"""
if not matched_cases_data:
......
......@@ -83,11 +83,13 @@ def create_app():
app.config['PERMANENT_SESSION_LIFETIME'] = 86400 # 24小时
# 注册 Blueprint(url_prefix 留空,保持 API 路径不变)
from routes.platform import bp as platform_bp
from routes.auth import bp as auth_bp
from routes.troubleshoot import bp as troubleshoot_bp
from routes.cache import bp as cache_bp
from routes.export import bp as export_bp
from routes.submit import bp as submit_bp
app.register_blueprint(platform_bp)
app.register_blueprint(auth_bp)
app.register_blueprint(troubleshoot_bp)
app.register_blueprint(cache_bp)
......
......@@ -511,7 +511,8 @@
<!-- 用户信息栏 -->
<div class="user-info-bar" style="background: #eff6ff; padding: 12px 32px; border-bottom: 1px solid #dbeafe; display: flex; justify-content: space-between; align-items: center;">
<div>
<div style="display: flex; align-items: center; gap: 12px;">
<a href="/" style="text-decoration: none; color: #1e40af; font-size: 14px; padding: 4px 8px; border-radius: 6px; background: #dbeafe; font-weight: 500;">🏠 返回首页</a>
<span id="userDisplayName" style="font-weight: 600; color: #1e40af;">加载中...</span>
</div>
<button class="btn btn-secondary" onclick="logout()" style="padding: 8px 16px; font-size: 14px;">
......
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5">
<title>{{ platform_name }}</title>
<style>
:root {
--primary: #2563eb;
--primary-hover: #1d4ed8;
--gray-50: #f9fafb;
--gray-100: #f3f4f6;
--gray-200: #e5e7eb;
--gray-500: #6b7280;
--gray-700: #374151;
--gray-900: #111827;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Microsoft YaHei", sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* 顶部导航 */
.navbar {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
padding: 16px 24px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid rgba(255, 255, 255, 0.15);
}
.navbar-brand {
color: white;
font-size: 20px;
font-weight: 700;
text-decoration: none;
display: flex;
align-items: center;
gap: 8px;
}
.navbar-user {
color: white;
display: flex;
align-items: center;
gap: 16px;
font-size: 14px;
}
.btn-logout {
background: rgba(255, 255, 255, 0.15);
color: white;
border: 1px solid rgba(255, 255, 255, 0.3);
padding: 8px 16px;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
text-decoration: none;
transition: background 0.2s;
}
.btn-logout:hover {
background: rgba(255, 255, 255, 0.25);
}
/* 主体 */
.container {
max-width: 960px;
margin: 0 auto;
padding: 40px 24px;
flex: 1;
}
.page-header {
text-align: center;
margin-bottom: 40px;
}
.page-header h1 {
color: white;
font-size: 28px;
font-weight: 700;
margin-bottom: 8px;
}
.page-header p {
color: rgba(255, 255, 255, 0.8);
font-size: 16px;
}
/* 模块卡片网格 */
.module-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
}
.module-card {
background: white;
border-radius: 16px;
padding: 32px 24px;
text-align: center;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.1);
transition: transform 0.2s, box-shadow 0.2s;
cursor: pointer;
text-decoration: none;
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.module-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
}
.module-icon {
font-size: 48px;
line-height: 1;
}
.module-name {
font-size: 18px;
font-weight: 700;
color: var(--gray-900);
}
.module-desc {
font-size: 14px;
color: var(--gray-500);
line-height: 1.5;
}
.module-enter {
display: inline-flex;
align-items: center;
gap: 6px;
background: var(--primary);
color: white;
padding: 10px 24px;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
margin-top: 8px;
transition: background 0.2s;
}
.module-card:hover .module-enter {
background: var(--primary-hover);
}
/* 空状态 */
.empty-state {
text-align: center;
padding: 60px 20px;
color: rgba(255, 255, 255, 0.8);
}
.empty-state .icon {
font-size: 64px;
margin-bottom: 16px;
}
.empty-state p {
font-size: 16px;
}
/* 页脚 */
.footer {
text-align: center;
padding: 20px;
color: rgba(255, 255, 255, 0.6);
font-size: 12px;
}
/* ===== 移动端响应式 ===== */
@media screen and (max-width: 1024px) {
.module-grid { grid-template-columns: repeat(2, 1fr); }
}
@media screen and (max-width: 768px) {
.container { padding: 24px 16px; }
.module-grid { grid-template-columns: 1fr; }
.page-header h1 { font-size: 22px; }
.module-card { padding: 24px 20px; }
.navbar { padding: 12px 16px; }
.navbar-brand { font-size: 16px; }
.btn-logout { min-height: 44px; font-size: 16px; }
}
</style>
</head>
<body>
<!-- 顶部导航 -->
<div class="navbar">
<a href="/" class="navbar-brand">🛠️ {{ platform_name }}</a>
<div class="navbar-user">
<span>{{ user.username }}({{ '管理员' if user.role == 'admin' else '用户' }})</span>
<button class="btn-logout" onclick="handleLogout()">🚪 退出</button>
</div>
</div>
<!-- 主体 -->
<div class="container">
<div class="page-header">
<h1>🛠️ {{ platform_name }}</h1>
<p>选择要使用的功能模块</p>
</div>
{% if modules %}
<div class="module-grid">
{% for m in modules %}
<a href="{{ m.url }}" class="module-card">
<div class="module-icon">{{ m.icon }}</div>
<div class="module-name">{{ m.name }}</div>
<div class="module-desc">{{ m.description }}</div>
<div class="module-enter">进入 →</div>
</a>
{% endfor %}
</div>
{% else %}
<div class="empty-state">
<div class="icon">📭</div>
<p>暂无可用模块</p>
</div>
{% endif %}
</div>
<div class="footer">
{{ platform_name }} · 运行维护工具集
</div>
<script>
async function handleLogout() {
try {
await fetch('/logout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include'
});
} catch (e) {}
window.location.href = '/login';
}
</script>
</body>
</html>
......@@ -3,7 +3,7 @@
{
"id": 1,
"username": "admin",
"password_hash": "scrypt:32768:8:1$4pS80HZDH81TmN0w$4e2359f166b15cb24d2a13dce9d97fb558eb3d801b60d04c22c6f35122a3035a53396c42a35765f4dbe55c9349477798b42b2cbaa67f9fa18e7e51ce0d93697f",
"password_hash": "scrypt:32768:8:1$evVCVFTTv2ZVvjto$10300880338aae565f2a1874ace20fd30470e6970132da309399fb7ccf6a1982732b715da4a569f168e8f517a8b07d267cccd16f09c9352c7abb61d21383a03f",
"role": "admin",
"created_at": "2026-07-12T16:00:00",
"last_login": "2026-07-12T16:06:00",
......@@ -13,7 +13,7 @@
{
"id": 2,
"username": "user",
"password_hash": "scrypt:32768:8:1$nus73sCDpqNJTD3X$fd9fcf0f0a6633a1e68606e3b3cac55ed250d5e961c33d421cc52458759ac6bb64bf4ce01810c2356be92f37536691faf687f3a42fcba36581182a52c14de6e1",
"password_hash": "scrypt:32768:8:1$DeqSGwN59n15hR0S$26c4df80a9b164dbba7e5148542f490360e6c488fe38bea9db35019028792ed514295fc35fac528d95698cb2c654f3b5ade855332583d0b04745553dddcab1f0",
"role": "user",
"created_at": "2026-07-12T16:10:00",
"last_login": null,
......
# -*- coding: utf-8 -*-
"""
modules.py — 平台模块注册
集中声明平台所有可用模块,供首页渲染和路由使用。
新增模块只需在此文件追加声明 + 注册 Blueprint,不改首页代码。
用法:
from utils.modules import get_modules
modules = get_modules(role='admin')
"""
# ============================================================
# 模块清单(声明式)
# ============================================================
MODULES = [
{
"id": "troubleshoot",
"name": "问题排查助手",
"icon": "🔍",
"description": "基于历史知识库的 AI 问题排查,357 条记录",
"url": "/troubleshoot",
"enabled": True,
"roles": [], # 空 = 所有登录用户可访问
"sort": 1,
},
{
"id": "service-monitor",
"name": "服务监控",
"icon": "📊",
"description": "服务器与服务运行状态监控、告警通知",
"url": "/service-monitor",
"enabled": False, # 预留,未上线
"roles": ["admin"],
"sort": 2,
},
# 后续模块在此追加
]
def get_modules(role=None):
"""获取可用模块列表。
参数:
role: 用户角色(None=不过滤,返回全部启用模块)
返回:
按 sort 排序的模块列表
"""
result = []
for m in MODULES:
if not m.get("enabled", True):
continue
if role is not None and m.get("roles") and role not in m["roles"]:
continue
result.append(m)
result.sort(key=lambda x: x.get("sort", 99))
return result
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论