fix online status && license revoke
This commit is contained in:
+2
-1
@@ -63,4 +63,5 @@ Desktop.ini
|
|||||||
|
|
||||||
# logs and runtime data
|
# logs and runtime data
|
||||||
logs/
|
logs/
|
||||||
需求.md
|
需求.md
|
||||||
|
toserver.md
|
||||||
@@ -20,6 +20,14 @@ const connectionState = {
|
|||||||
};
|
};
|
||||||
let pendingReview = null;
|
let pendingReview = null;
|
||||||
|
|
||||||
|
function resolveCredentials(credentials = {}) {
|
||||||
|
return {
|
||||||
|
apiUrl: String(credentials.apiUrl || connectionState.apiUrl || API_URL).trim(),
|
||||||
|
adminToken: String(credentials.adminToken || connectionState.adminToken || process.env.B_ADMIN_TOKEN || ""),
|
||||||
|
deviceId: String(credentials.deviceId || connectionState.deviceId || "").trim()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function startPanelInboxPoller(window) {
|
function startPanelInboxPoller(window) {
|
||||||
if (!Number.isFinite(INBOX_POLL_INTERVAL_MS) || INBOX_POLL_INTERVAL_MS < 500) {
|
if (!Number.isFinite(INBOX_POLL_INTERVAL_MS) || INBOX_POLL_INTERVAL_MS < 500) {
|
||||||
window.webContents.send("csv:watch-error", "POLL_INTERVAL_MS 必须大于或等于 500");
|
window.webContents.send("csv:watch-error", "POLL_INTERVAL_MS 必须大于或等于 500");
|
||||||
@@ -199,13 +207,23 @@ function registerHandlers() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
ipcMain.handle("license:list", (_event, credentials) =>
|
ipcMain.handle("license:list", (_event, credentials) =>
|
||||||
callServer({ type: "listLicenses" }, credentials));
|
callServer({ type: "listLicenses" }, resolveCredentials(credentials)));
|
||||||
ipcMain.handle("license:get", (_event, request) =>
|
ipcMain.handle("license:get", (_event, request) =>
|
||||||
callServer({ type: "getLicense", licenseId: request.licenseId }, request.credentials));
|
callServer(
|
||||||
|
{ type: "getLicense", licenseId: request.licenseId },
|
||||||
|
resolveCredentials(request.credentials)
|
||||||
|
));
|
||||||
ipcMain.handle("license:revoke", (_event, request) =>
|
ipcMain.handle("license:revoke", (_event, request) =>
|
||||||
callServer({ type: "revokeLicense", licenseId: request.licenseId, reason: request.reason }, request.credentials));
|
callServer(
|
||||||
|
{ type: "revokeLicense", licenseId: request.licenseId, reason: request.reason },
|
||||||
|
resolveCredentials(request.credentials)
|
||||||
|
));
|
||||||
ipcMain.handle("license:download", async (_event, request) => {
|
ipcMain.handle("license:download", async (_event, request) => {
|
||||||
const result = await callServer({ type: "getLicense", licenseId: request.licenseId }, request.credentials);
|
const resolvedCredentials = resolveCredentials(request.credentials);
|
||||||
|
const result = await callServer(
|
||||||
|
{ type: "getLicense", licenseId: request.licenseId },
|
||||||
|
resolvedCredentials
|
||||||
|
);
|
||||||
if (typeof result.license?.license !== "string" || !result.license.license) {
|
if (typeof result.license?.license !== "string" || !result.license.license) {
|
||||||
throw new Error("Server 未返回许可证原文,无法下载");
|
throw new Error("Server 未返回许可证原文,无法下载");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,9 +20,21 @@ async function callServer(payload, options = {}) {
|
|||||||
headers: { "content-type": "application/json" },
|
headers: { "content-type": "application/json" },
|
||||||
body: JSON.stringify({ ...payload, adminToken })
|
body: JSON.stringify({ ...payload, adminToken })
|
||||||
});
|
});
|
||||||
if (!response.ok) throw new Error(`server 请求失败: HTTP ${response.status}`);
|
let result = null;
|
||||||
|
try {
|
||||||
const result = await response.json();
|
result = await response.json();
|
||||||
|
} catch {
|
||||||
|
result = null;
|
||||||
|
}
|
||||||
|
if (!response.ok) {
|
||||||
|
const message = result && typeof result.errMsg === "string" && result.errMsg
|
||||||
|
? result.errMsg
|
||||||
|
: `server 请求失败: HTTP ${response.status}`;
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
if (!result || typeof result !== "object") {
|
||||||
|
throw new Error("server 返回格式无效");
|
||||||
|
}
|
||||||
if (!result.success) throw new Error(result.errMsg || "server 返回失败");
|
if (!result.success) throw new Error(result.errMsg || "server 返回失败");
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ class ConnectionManager:
|
|||||||
self.log(f"压力地址: {pressure_addr}, 电机地址: {motor_addr}, 流量计地址: {flowmeter_addr}")
|
self.log(f"压力地址: {pressure_addr}, 电机地址: {motor_addr}, 流量计地址: {flowmeter_addr}")
|
||||||
|
|
||||||
if self._on_status_change:
|
if self._on_status_change:
|
||||||
self._on_status_change(True, "已连接")
|
self._on_status_change(True, "● 已连接")
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ class ConnectionManager:
|
|||||||
self.log("已断开连接")
|
self.log("已断开连接")
|
||||||
|
|
||||||
if self._on_status_change:
|
if self._on_status_change:
|
||||||
self._on_status_change(False, "未连接")
|
self._on_status_change(False, "● 未连接")
|
||||||
|
|
||||||
def read_pressure(self):
|
def read_pressure(self):
|
||||||
"""读取当前压力值(转换为实际物理量)
|
"""读取当前压力值(转换为实际物理量)
|
||||||
|
|||||||
+11
-4
@@ -15,7 +15,7 @@ matplotlib.use('QtAgg')
|
|||||||
from PySide6.QtWidgets import QApplication, QMessageBox
|
from PySide6.QtWidgets import QApplication, QMessageBox
|
||||||
from PySide6.QtCore import Qt
|
from PySide6.QtCore import Qt
|
||||||
|
|
||||||
from ui.main_window import MainWindow
|
from license_utils import check_license
|
||||||
from styles import apply_app_style
|
from styles import apply_app_style
|
||||||
|
|
||||||
# 屏蔽多余警告
|
# 屏蔽多余警告
|
||||||
@@ -81,16 +81,23 @@ def main():
|
|||||||
except (AttributeError, TypeError):
|
except (AttributeError, TypeError):
|
||||||
pass # Qt < 6.5 无此方法,忽略
|
pass # Qt < 6.5 无此方法,忽略
|
||||||
|
|
||||||
# 2. 应用全局样式
|
# 2. 在导入 UI 前完成许可证校验。UI 导入的 `api` 模块会读取已验证
|
||||||
|
# 许可证中的 device_id,因此撤销状态和设备心跳会使用同一个设备标识。
|
||||||
|
check_license()
|
||||||
|
|
||||||
|
# 3. 应用全局样式
|
||||||
colors = apply_app_style(app)
|
colors = apply_app_style(app)
|
||||||
_write_log("样式加载完成")
|
_write_log("样式加载完成")
|
||||||
|
|
||||||
# 3. 创建主窗口
|
# 延迟导入,避免 `api` 在许可证校验前缓存默认 device_id。
|
||||||
|
from ui.main_window import MainWindow
|
||||||
|
|
||||||
|
# 4. 创建主窗口
|
||||||
window = MainWindow(colors)
|
window = MainWindow(colors)
|
||||||
_write_log("主窗口创建完成")
|
_write_log("主窗口创建完成")
|
||||||
window.show()
|
window.show()
|
||||||
|
|
||||||
# 4. 进入事件循环
|
# 5. 进入事件循环
|
||||||
_write_log("进入事件循环")
|
_write_log("进入事件循环")
|
||||||
sys.exit(app.exec())
|
sys.exit(app.exec())
|
||||||
|
|
||||||
|
|||||||
@@ -460,6 +460,7 @@ class MainWindow(QMainWindow):
|
|||||||
if self.conn_mgr.is_connected():
|
if self.conn_mgr.is_connected():
|
||||||
self.status_bar.set_log("正在断开连接...")
|
self.status_bar.set_log("正在断开连接...")
|
||||||
self.conn_mgr.disconnect()
|
self.conn_mgr.disconnect()
|
||||||
|
self.status_bar.set_connection_status(False, "● 未连接")
|
||||||
self.tab_connection._connect_text_lbl.setText(" 连接设备")
|
self.tab_connection._connect_text_lbl.setText(" 连接设备")
|
||||||
self.status_bar.set_log("已断开设备连接")
|
self.status_bar.set_log("已断开设备连接")
|
||||||
else:
|
else:
|
||||||
@@ -468,9 +469,11 @@ class MainWindow(QMainWindow):
|
|||||||
params = self.tab_connection.get_connection_params()
|
params = self.tab_connection.get_connection_params()
|
||||||
success = self.conn_mgr.connect(**params)
|
success = self.conn_mgr.connect(**params)
|
||||||
if success:
|
if success:
|
||||||
|
self.status_bar.set_connection_status(True, "● 已连接")
|
||||||
self.tab_connection._connect_text_lbl.setText(" 断开连接")
|
self.tab_connection._connect_text_lbl.setText(" 断开连接")
|
||||||
self.status_bar.set_log("设备连接成功")
|
self.status_bar.set_log("设备连接成功")
|
||||||
else:
|
else:
|
||||||
|
self.status_bar.set_connection_status(False, "● 未连接")
|
||||||
self.status_bar.set_log("设备连接失败,请检查参数和硬件连接")
|
self.status_bar.set_log("设备连接失败,请检查参数和硬件连接")
|
||||||
|
|
||||||
def _on_set_target(self, target: float):
|
def _on_set_target(self, target: float):
|
||||||
|
|||||||
@@ -1,178 +1,5 @@
|
|||||||
1. 控制数据(ReinLoop 里的 core/data_collecter)和辨识数据一样,上传服务器后,需要服务器向panel提供删除下载查看功能。
|
需要修改server的部分写入toserver.md会由服务器agent修改
|
||||||
2. 控制台许可证撤销界面点了没有反应,需要实现撤销和下载功能。
|
|
||||||
|
|
||||||
修改reinloop和panel,并给出服务器端需要实现的接口及功能。
|
1. reinloop控制台右下角一直显示未连接,需要修改为正确显示
|
||||||
|
|
||||||
## 服务端访问约定
|
2. panel 撤销license还是不能成功,当前server代码是uptodate的,你看看是哪个部分的问题。
|
||||||
|
|
||||||
不再调用微信云函数或云存储接口。ReinLoop 与 ControlPanel 统一访问云服务器:
|
|
||||||
|
|
||||||
```text
|
|
||||||
https://ReinLoop.dominatedconvergence.com/api
|
|
||||||
```
|
|
||||||
|
|
||||||
- 所有业务接口均使用 `POST /api`,请求与响应均为 JSON。
|
|
||||||
- 通过请求体中的 `type` 字段区分业务功能。
|
|
||||||
- 管理端接口必须携带 `adminToken`,由服务端校验 `B_ADMIN_TOKEN`;ReinLoop 客户端上传控制数据时不携带管理令牌。
|
|
||||||
- 所有响应必须包含 `success: true|false`;失败时必须提供可展示的 `errMsg`。
|
|
||||||
- 设备标识 `deviceId` 固定为 `<company-code>/<line-code>`,服务端必须校验其格式,禁止路径遍历。
|
|
||||||
- 服务端需要设置 `PUBLIC_BASE_URL=https://ReinLoop.dominatedconvergence.com`,确保上传地址和下载地址均为可从客户端访问的 HTTPS URL。
|
|
||||||
|
|
||||||
### 通用文件上传接口:`uploadDataFile`
|
|
||||||
|
|
||||||
ReinLoop 的控制数据、辨识数据及配置文件均通过此两步协议上传:
|
|
||||||
|
|
||||||
1. 客户端调用业务接口申请一次性上传地址:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "uploadDataFile",
|
|
||||||
"fileName": "episode_raw_data_20260730_120000_part1of2.pkl",
|
|
||||||
"folder": "<deviceId>/data_record/data_50SLM_5L"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
2. 服务端返回 `uploadMetadata.url` 后,客户端以 `multipart/form-data` 向该 URL 提交 `file` 字段。上传成功应返回 HTTP `204` 或 `200`。
|
|
||||||
|
|
||||||
服务端需要在上传完成时保存文件本体和 `fileRecords` 元数据(包括 `fileID`、`fileName`、`folder`、`uploadTime`、`size`)。控制数据目录必须以 `<deviceId>/data_record/` 为前缀。
|
|
||||||
|
|
||||||
## 服务器端接口需求(控制数据)
|
|
||||||
|
|
||||||
控制数据由 `ReinLoop/core/data_collector.py` 上传到
|
|
||||||
`<deviceId>/data_record/`,包括控制 Episode 的 `.pkl` 分片和对应的 JSON manifest。
|
|
||||||
以下接口均为管理端接口,要求请求体携带有效的 `adminToken`;响应统一包含
|
|
||||||
`success: true|false`,失败时返回 `errMsg`。
|
|
||||||
|
|
||||||
### `listControlFiles`
|
|
||||||
|
|
||||||
按设备分页查询控制数据文件,供 Panel 的“控制数据”列表使用。
|
|
||||||
|
|
||||||
请求:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "listControlFiles",
|
|
||||||
"adminToken": "<B_ADMIN_TOKEN>",
|
|
||||||
"deviceId": "<company-code>/<line-code>",
|
|
||||||
"page": 1,
|
|
||||||
"pageSize": 100
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
成功响应:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"files": [
|
|
||||||
{
|
|
||||||
"fileID": "local://ReinLoop_GUI/<deviceId>/data_record/...",
|
|
||||||
"fileName": "episode_raw_data_20260730_120000_part1of2.pkl",
|
|
||||||
"uploadTime": "2026-07-30T04:00:00.000Z",
|
|
||||||
"size": 123456
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"total": 1,
|
|
||||||
"page": 1,
|
|
||||||
"pageSize": 100
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
服务端只可返回指定 `deviceId` 的 `data_record` 目录及其子目录中的文件;按上传时间倒序排列,
|
|
||||||
`pageSize` 建议限制在 $1\dots100$。
|
|
||||||
|
|
||||||
### `getControlFileDownload`
|
|
||||||
|
|
||||||
按 `fileID` 获取控制数据原始文件的短期下载地址,供 Panel 查看 JSON manifest 或保存 `.pkl` / `.json` 文件。
|
|
||||||
|
|
||||||
请求:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "getControlFileDownload",
|
|
||||||
"adminToken": "<B_ADMIN_TOKEN>",
|
|
||||||
"fileID": "local://ReinLoop_GUI/<deviceId>/data_record/..."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
成功响应:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"fileID": "local://ReinLoop_GUI/<deviceId>/data_record/...",
|
|
||||||
"fileName": "episode_raw_data_20260730_120000_manifest.json",
|
|
||||||
"uploadTime": "2026-07-30T04:00:00.000Z",
|
|
||||||
"size": 1024,
|
|
||||||
"url": "https://server.example/files/...?..."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`url` 必须是绑定该文件且会过期的签名 URL,不能根据任意路径直接下载。服务端须校验文件存在,
|
|
||||||
且该文件必须属于控制数据目录。
|
|
||||||
|
|
||||||
### `deleteControlFile`
|
|
||||||
|
|
||||||
永久删除指定控制数据文件,供 Panel 的二次确认删除操作使用。
|
|
||||||
|
|
||||||
请求:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "deleteControlFile",
|
|
||||||
"adminToken": "<B_ADMIN_TOKEN>",
|
|
||||||
"fileID": "local://ReinLoop_GUI/<deviceId>/data_record/..."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
成功响应:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"deletedCount": 1
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
服务端须同时删除文件本体及 `fileRecords` 中的元数据;必须校验 `fileID` 属于控制数据目录,
|
|
||||||
禁止借此接口删除模型、辨识数据、配置或许可证相关文件。文件不存在时返回明确错误,不应将删除操作视为成功。
|
|
||||||
|
|
||||||
## 许可证接口补充
|
|
||||||
|
|
||||||
### `getLicense`
|
|
||||||
|
|
||||||
Panel 的许可证“下载”复用既有 `getLicense` 接口。请求:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "getLicense",
|
|
||||||
"adminToken": "<B_ADMIN_TOKEN>",
|
|
||||||
"licenseId": "<license-uuid>"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
成功响应中的 `license` 对象必须包含原始许可证文本字段 `license`;Panel 将该字段保存为 `.lic` 文件。
|
|
||||||
服务端不得将私钥或其他许可证的内容一并返回。
|
|
||||||
|
|
||||||
### `revokeLicense`
|
|
||||||
|
|
||||||
Panel 的许可证撤销使用既有 `revokeLicense` 接口:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "revokeLicense",
|
|
||||||
"adminToken": "<B_ADMIN_TOKEN>",
|
|
||||||
"licenseId": "<license-uuid>",
|
|
||||||
"reason": "管理员撤销原因"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
成功响应应返回 `success: true` 及更新后的许可证对象,其中 `status` 为 `revoked`。服务端必须保留
|
|
||||||
`revokedAt` 与 `revocationReason` 审计信息;许可证在线校验接口 `validateLicense` 随后应返回
|
|
||||||
`valid: false`、`status: "revoked"`,使 ReinLoop 客户端在下一次许可证巡检时生效。
|
|
||||||
|
|
||||||
### Panel 对应功能
|
|
||||||
|
|
||||||
- “控制数据”页面:调用 `listControlFiles` 刷新列表;JSON manifest 可请求下载后直接预览,`.pkl` 仅提供下载;删除前需二次确认。
|
|
||||||
- “许可证”页面:调用 `getLicense` 下载 `.lic`;调用 `revokeLicense` 撤销,并在成功后刷新许可证列表。
|
|
||||||
- Panel 不得自行拼接服务器文件路径、下载 URL 或绕过上述 Admin 接口访问文件。
|
|
||||||
Reference in New Issue
Block a user