feat: 重构项目心跳数据结构并实现项目列表API

- 新增统一项目列表Redis键和迁移工具
- 实现GET /api/projects端点获取项目列表
- 实现POST /api/projects/migrate端点支持数据迁移
- 更新前端ProjectSelector组件使用真实项目数据
- 扩展projectStore状态管理
- 更新相关文档和OpenSpec规范
- 添加测试用例验证新功能
This commit is contained in:
2026-01-13 19:45:05 +08:00
parent 19e65d78dc
commit 282f7268ed
66 changed files with 4378 additions and 456 deletions

262
docs/openapi.yaml Normal file
View File

@@ -0,0 +1,262 @@
openapi: 3.0.3
info:
title: BLS Project Console API
version: 1.0.0
description: |
BLS Project Console 后端 API与当前实现保持一致
servers:
- url: http://localhost:3001
paths:
/api/health:
get:
summary: Health check
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
status:
type: string
required: [status]
/api/projects:
get:
summary: 获取项目列表(来自 Redis 项目心跳列表)
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/ProjectsResponse'
'503':
description: Redis 未就绪
content:
application/json:
schema:
$ref: '#/components/schemas/ProjectsErrorResponse'
/api/projects/migrate:
post:
summary: 从旧 *_项目心跳 键迁移到 项目心跳 列表
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/MigrateRequest'
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/MigrateResponse'
'500':
description: Migration failed
content:
application/json:
schema:
$ref: '#/components/schemas/MigrateErrorResponse'
/api/logs:
get:
summary: 获取指定项目的日志
parameters:
- in: query
name: projectName
required: true
schema:
type: string
- in: query
name: limit
required: false
schema:
type: integer
default: 200
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/LogsResponse'
/api/commands:
post:
summary: 发送控制指令到目标项目 API
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CommandRequest'
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/CommandResponse'
components:
schemas:
Project:
type: object
properties:
id:
type: string
description: 项目 id当前实现为 projectName
name:
type: string
description: 项目名称
apiBaseUrl:
type: string
nullable: true
description: 项目 API 地址(基础 URL
lastActiveAt:
type: integer
format: int64
nullable: true
description: 最新心跳时间Unix 时间戳毫秒)
status:
type: string
enum: [online, offline, unknown]
isOnline:
type: boolean
ageMs:
type: integer
format: int64
nullable: true
required: [id, name, status, isOnline]
ProjectsResponse:
type: object
properties:
success:
type: boolean
projects:
type: array
items:
$ref: '#/components/schemas/Project'
count:
type: integer
required: [success, projects, count]
ProjectsErrorResponse:
type: object
properties:
success:
type: boolean
message:
type: string
projects:
type: array
items:
$ref: '#/components/schemas/Project'
required: [success, message, projects]
MigrateRequest:
type: object
properties:
deleteOldKeys:
type: boolean
default: false
dryRun:
type: boolean
default: false
MigrateResponse:
type: object
properties:
success:
type: boolean
message:
type: string
migrated:
type: integer
projects:
type: array
items:
type: object
properties:
projectName:
type: string
apiBaseUrl:
type: string
nullable: true
lastActiveAt:
type: integer
format: int64
nullable: true
required: [projectName]
listKey:
type: string
example: 项目心跳
deleteOldKeys:
type: boolean
required: [success, message]
MigrateErrorResponse:
type: object
properties:
success:
type: boolean
message:
type: string
error:
type: string
required: [success, message]
LogsResponse:
type: object
properties:
logs:
type: array
items:
type: object
additionalProperties: true
projectStatus:
type: string
nullable: true
heartbeat:
type: object
nullable: true
properties:
apiBaseUrl:
type: string
nullable: true
lastActiveAt:
type: integer
format: int64
nullable: true
isOnline:
type: boolean
ageMs:
type: integer
format: int64
nullable: true
CommandRequest:
type: object
properties:
targetProjectName:
type: string
command:
type: string
required: [targetProjectName, command]
CommandResponse:
type: object
properties:
success:
type: boolean
message:
type: string
commandId:
type: string
targetUrl:
type: string
upstreamStatus:
type: integer
upstreamData:
nullable: true
required: [success, message]

View File

@@ -0,0 +1,299 @@
# Redis数据结构文档
## 概述
本文档详细说明了BLS Project Console中使用的Redis数据结构包括新的统一项目列表结构和旧结构的迁移方案。
## 数据结构
### 1. 项目心跳列表(新结构)
**键名**: `项目心跳`
**类型**: String (JSON数组)
**描述**: 统一存储所有项目的心跳信息,替代原有的分散键结构。
**数据格式**:
```json
[
{
"projectName": "string",
"apiBaseUrl": "string",
"lastActiveAt": "number"
}
]
```
**字段说明**:
- `projectName`: 项目名称(字符串)
- `apiBaseUrl`: 项目API基础URL字符串对应“API地址”
- `lastActiveAt`: 最新心跳时间Unix 时间戳毫秒,数字),对应“最新心跳时间”
**与需求字段对应**:
- 项目名称 → `projectName`
- API地址 → `apiBaseUrl`
- 最新心跳时间 → `lastActiveAt`
**示例**:
```json
[
{
"projectName": "用户管理系统",
"apiBaseUrl": "http://localhost:8080",
"lastActiveAt": 1704067200000
},
{
"projectName": "数据可视化平台",
"apiBaseUrl": "http://localhost:8081",
"lastActiveAt": 1704067260000
}
]
```
### 2. 项目控制台日志
**键名**: `{projectName}_项目控制台`
**类型**: List
**描述**: 存储项目的日志记录使用Redis列表结构实现日志队列。
**数据格式**: 每个列表元素是一个JSON字符串包含日志信息。
**日志对象格式**:
```json
{
"id": "string",
"timestamp": "ISO-8601 string",
"level": "string",
"message": "string",
"metadata": "object (optional)"
}
```
**字段说明**:
- `id`: 日志唯一标识符
- `timestamp`: ISO-8601格式的时间戳
- `level`: 日志级别info, warn, error, debug
- `message`: 日志消息内容
- `metadata`: 可选的附加元数据
**示例**:
```json
{
"id": "log-1704067200000-abc123",
"timestamp": "2024-01-01T00:00:00.000Z",
"level": "info",
"message": "系统启动成功",
"metadata": {
"version": "1.0.0",
"environment": "production"
}
}
```
### 3. 项目控制指令
**键名**: `{projectName}_控制`
**类型**: List
**描述**: 存储发送给项目的控制指令。
**指令对象格式**:
```json
{
"commandId": "string",
"timestamp": "ISO-8601 string",
"source": "string",
"apiName": "string",
"args": "array",
"argsText": "string",
"extraArgs": "object (optional)"
}
```
**字段说明**:
- `commandId`: 指令唯一标识符
- `timestamp`: ISO-8601格式的时间戳
- `source`: 指令来源(如"BLS Project Console"
- `apiName`: 目标API接口名称
- `args`: 指令参数数组
- `argsText`: 指令参数文本
- `extraArgs`: 可选的额外参数
**示例**:
```json
{
"commandId": "cmd-1704067200000-xyz789",
"timestamp": "2024-01-01T00:00:00.000Z",
"source": "BLS Project Console",
"apiName": "/api/reload",
"args": ["config"],
"argsText": "config",
"extraArgs": {
"force": true
}
}
```
### 4. 项目心跳(旧结构,已废弃)
**键名**: `{projectName}_项目心跳`
**类型**: String (JSON对象)
**描述**: 旧的项目心跳数据结构,已迁移到统一的项目列表结构。
**注意**: 此结构已废弃,仅用于向后兼容。新项目应使用统一的`项目心跳`列表结构。
## 数据迁移
### 迁移工具
位置: `src/backend/services/migrateHeartbeatData.js`
### 迁移步骤
1. **准备阶段**
- 确保Redis服务正常运行
- 备份现有数据(可选)
2. **执行迁移**
```javascript
import { migrateHeartbeatData } from './services/migrateHeartbeatData.js';
// 干运行模式(不实际写入数据)
await migrateHeartbeatData({ dryRun: true });
// 实际迁移
await migrateHeartbeatData({ deleteOldKeys: false });
// 迁移并删除旧键
await migrateHeartbeatData({ deleteOldKeys: true });
```
3. **验证迁移**
- 检查`项目心跳`键是否包含所有项目
- 验证每个项目的数据完整性
- 测试项目选择和日志功能
### 迁移API
**端点**: `POST /api/projects/migrate`
**请求体**:
```json
{
"deleteOldKeys": false,
"dryRun": false
}
```
**参数说明**:
- `deleteOldKeys`: 是否删除旧的心跳键(默认: false
- `dryRun`: 是否为干运行模式(默认: false
**响应**:
```json
{
"success": true,
"message": "数据迁移完成",
"migrated": 2,
"projects": [...],
"listKey": "项目心跳",
"deleteOldKeys": false
}
```
## 向后兼容性
为确保平滑过渡,系统在读取项目心跳时采用以下策略:
1. **优先读取新结构**: 首先尝试从`项目心跳`列表中查找项目
2. **回退到旧结构**: 如果新结构中未找到,则尝试从`{projectName}_项目心跳`键中读取
3. **自动迁移**: 当检测到旧结构数据时,可以自动迁移到新结构
## 性能优化
### 1. 项目列表查询
- 使用单一键存储所有项目减少Redis查询次数
- 支持一次性获取所有项目信息
### 2. 日志轮询
- 使用Redis列表的`lRange`命令高效获取最新日志
- 支持限制返回的日志数量
### 3. 心跳更新
- 直接更新项目列表中的对应项目
- 避免频繁的键操作
## 监控和维护
### 健康检查
定期检查以下指标:
- Redis连接状态
- 项目列表完整性
- 日志队列长度
### 清理策略
- 定期清理过期的日志记录
- 移除长时间未活跃的项目
- 清理已废弃的旧键
## 安全考虑
1. **访问控制**: 确保只有授权的应用可以写入心跳数据
2. **数据验证**: 验证心跳数据的格式和内容
3. **速率限制**: 限制心跳更新频率,防止滥用
## 故障排查
### 常见问题
1. **项目列表为空**
- 检查Redis连接
- 验证`项目心跳`键是否存在
- 检查数据格式是否正确
2. **日志不显示**
- 确认项目名称正确
- 检查`{projectName}_项目控制台`键是否存在
- 验证日志数据格式
3. **命令发送失败**
- 检查项目是否在线
- 验证API地址是否正确
- 确认目标项目是否正常运行
## 版本历史
- **v2.0** (2024-01-13): 引入统一的项目列表结构
- **v1.0** (初始版本): 使用分散的项目心跳键
## 参考资料
- [Redis数据类型](https://redis.io/docs/data-types/)
- [项目OpenSpec规范](../openspec/specs/)
- [API文档](../docs/api-documentation.md)

View File

@@ -3,39 +3,57 @@
本文档定义“外部项目 ↔ BLS Project Console”之间通过 Redis 交互的 **Key 命名、数据类型、写入方式、读取方式与数据格式**
> 约束:每个需要关联本控制台的外部项目,必须在本项目使用的同一个 Redis 实例中:
> - 写入 2 个 Key状态 + 控制台信息)
> - 读取 1 个 Key控制指令队列
>
> - 写入 2 个 Key心跳 + 控制台信息
> - 命令下发为 HTTP API 调用
## 1. 命名约定
令:
- `projectName`:外部项目名称(建议只用字母数字下划线 `A-Za-z0-9_`;如使用中文也可,但需保证统一且 UTF-8
固定后缀:
- 状态:`${projectName}_项目状态`
- 心跳:`${projectName}_项目心跳`
- 控制台:`${projectName}_项目控制台`
- 控制:`${projectName}_控制`
示例projectName = `订单系统`
- `订单系统_项目状态`
- `订单系统_项目心跳`
- `订单系统_项目控制台`
- `订单系统_控制`
## 2. 外部项目需要写入的 2 个 Key
### 2.1 `${projectName}_项目状态`
### 2.1 `${projectName}_项目心跳`
- Redis 数据类型:**STRING**
- 写入方式:`SET ${projectName}_项目状态 <status>`
- value状态枚举(必须为以下之一)
- `在线`
- `离线`
- `故障`
- `报错`
- 写入方式:`SET ${projectName}_项目心跳 <json>`
- valueJSON 字符串,必须包含目标项目可被调用的 `apiBaseUrl`,以及活跃时间戳 `lastActiveAt`
建议(非强制)
- 定期刷新(如 5~30 秒)
- 可设置 TTL 防止僵尸在线(如 `SET key value EX 60`
推荐 JSON Schema
```json
{
"apiBaseUrl": "http://127.0.0.1:4001",
"lastActiveAt": 1760000000000
}
```
字段说明:
- `apiBaseUrl`:目标项目对外提供的 API 地址(基地址,后端将基于它拼接 `apiName`
- `lastActiveAt`:状态时间(活跃时间戳,毫秒)。建议每 **3 秒**刷新一次。
在线/离线判定BLS Project Console 使用):
-`now - lastActiveAt > 10_000ms`,则认为该应用 **离线**
- 否则认为 **在线**
建议:
- `lastActiveAt` 使用 `Date.now()` 生成(毫秒)
- 可设置 TTL可选例如 `SET key value EX 30`
### 2.2 `${projectName}_项目控制台`
@@ -45,6 +63,7 @@
value推荐格式一条 JSON 字符串,表示“错误/调试信息”或日志记录。
推荐 JSON Schema字段尽量保持稳定便于控制台解析
```json
{
"timestamp": "2026-01-12T12:34:56.789Z",
@@ -58,46 +77,52 @@ value推荐格式一条 JSON 字符串,表示“错误/调试信息
```
字段说明:
- `timestamp`ISO-8601 时间字符串
- `level`:建议取值 `info|warn|error|debug`(小写)
- `message`:日志文本
- `metadata`:可选对象(附加信息)
## 3. 外部项目需要读取的 1 个 Key控制指令
## 3. 命令下发方式HTTP API 控制
### 3.1 `${projectName}_控制`
控制台不再通过 Redis 写入控制指令队列;改为由 BLS Project Console 后端根据目标项目心跳里的 `apiBaseUrl` 直接调用目标项目 HTTP API。
- Redis 数据类型:**LIST**(控制台向目标项目下发的“命令队列”)
- 队列模式(必须):**Redis List 队列**(生产 `RPUSH`,消费 `BLPOP`;必要时配合 `LTRIM`
- 控制台写入方式FIFO`RPUSH ${targetProjectName}_控制 <json>`
- 外部项目读取方式(阻塞消费,推荐):`BLPOP ${projectName}_控制 0`
### 3.1 控制台输入格式
> 说明:`BLPOP` 本身会原子性地“取出并移除”队列头元素,通常不需要额外 `LTRIM`。
> 如果你的运行环境/客户端库无法可靠使用 `BLPOP`,或你希望采用“读取 + 修剪LTRIM”的显式确认方式可使用兼容模式
> 1) `LRANGE ${projectName}_控制 0 0` 读取 1 条
> 2) 处理成功后执行 `LTRIM ${projectName}_控制 1 -1` 修剪已消费元素
一行文本按空格拆分:
value**JSON 对象**(在 Redis 中以 JSON 字符串形式存储)。
- 第一个 token`apiName`(接口名/路径片段)
- 剩余 token参数列表字符串数组
示例:
- `reload`
- `reload force`
- `user/refreshCache tenantA`
### 3.2 目标项目需要提供的 API
后端默认使用 `POST` 调用:
- `POST {apiBaseUrl}/{apiName}`
请求体JSON示例
推荐 JSON Schema
```json
{
"id": "cmd-1700000000000-abc123",
"timestamp": "2026-01-12T12:35:00.000Z",
"commandId": "cmd-1700000000000-abc123",
"timestamp": "2026-01-13T00:00:00.000Z",
"source": "BLS Project Console",
"command": "reload",
"args": {
"force": true
}
"apiName": "reload",
"args": ["force"],
"argsText": "force"
}
```
字段说明
- `id`:命令唯一 ID用于追踪
- `timestamp`:命令下发时间
- `source`:固定可写 `BLS Project Console`
- `command`:命令名称或命令文本
- `args`:可选对象参数
返回建议
- 2xx 表示成功
- 非 2xx 表示失败(控制台会展示 upstreamStatus 与部分返回内容)
## 4. 兼容与错误处理建议
@@ -107,4 +132,5 @@ value**JSON 对象**(在 Redis 中以 JSON 字符串形式存储)。
## 5. 与本项目代码的对应关系(实现中)
后端通过 `/api/commands` 将命令写入 `${targetProjectName}_控制`;通过 `/api/logs` 读取 `${projectName}_项目控制台`(以仓库当前实现为准)
- 后端通过 `/api/commands`:从 `${targetProjectName}_项目心跳` 读取 `apiBaseUrl` `lastActiveAt`,在线时调用目标项目 API
- 后端通过 `/api/logs`:读取 `${projectName}_项目控制台`;并基于 `${projectName}_项目心跳` 返回在线/离线与 API 地址信息。