fix model del & rename
This commit is contained in:
@@ -6,6 +6,7 @@ const { callServer, downloadFromUrl, downloadToPath } = require("./server-client
|
||||
const { signLicense } = require("./license-manager");
|
||||
const { plotCsv } = require("./plot-csv");
|
||||
const { plotJson } = require("./plot-json");
|
||||
const { createModelHandlers } = require("./model-handlers");
|
||||
|
||||
const VOLUME_FIELDS = [
|
||||
"q_in_val", "dt", "p_max", "fit_low", "fit_high", "T_delta", "xa_full", "num_runs"
|
||||
@@ -198,6 +199,7 @@ function toDataUrl(filePath) {
|
||||
}
|
||||
|
||||
function registerHandlers() {
|
||||
const modelHandlers = createModelHandlers({ callServer });
|
||||
ipcMain.handle("app:get-defaults", () => ({
|
||||
apiUrl: API_URL,
|
||||
deviceId: process.env.REINLOOP_DEVICE_ID || "",
|
||||
@@ -331,35 +333,19 @@ function registerHandlers() {
|
||||
return result;
|
||||
});
|
||||
|
||||
ipcMain.handle("model:list", (_event, request) =>
|
||||
callServer({ type: "listModels", folder: `${request.deviceId}/model_config` }, request.credentials));
|
||||
ipcMain.handle("model:list", (_event, request) => modelHandlers.list(request));
|
||||
ipcMain.handle("model:choose-upload-file", async () => {
|
||||
const selection = await dialog.showOpenDialog({ title: "选择模型文件", properties: ["openFile"] });
|
||||
if (selection.canceled) return null;
|
||||
const sourcePath = selection.filePaths[0];
|
||||
return { sourcePath, fileName: path.basename(sourcePath) };
|
||||
});
|
||||
ipcMain.handle("model:upload", async (_event, request) => {
|
||||
if (!request.sourcePath || !request.fileName) throw new Error("请先选择模型文件");
|
||||
const sourcePath = path.resolve(request.sourcePath);
|
||||
const fileName = path.basename(request.fileName);
|
||||
await fs.promises.access(sourcePath, fs.constants.R_OK);
|
||||
const issued = await callServer({
|
||||
type: "uploadDataFile", fileName, folder: `${request.deviceId}/model_config`,
|
||||
overwrite: request.overwrite === true
|
||||
}, request.credentials);
|
||||
const form = new FormData();
|
||||
form.append("file", new Blob([await fs.promises.readFile(sourcePath)]), fileName);
|
||||
const response = await fetch(issued.uploadMetadata.url, { method: "POST", body: form });
|
||||
if (![200, 204].includes(response.status)) throw new Error(`模型上传失败: HTTP ${response.status}`);
|
||||
return { success: true, fileID: issued.fileID, fileName };
|
||||
});
|
||||
ipcMain.handle("model:upload", (_event, request) => modelHandlers.upload(request));
|
||||
ipcMain.handle("model:download", async (_event, request) => {
|
||||
const result = await callServer({ type: "downloadModel", fileID: request.fileID }, request.credentials);
|
||||
return { filePath: await downloadFromUrl(result.url, request.fileName, request.credentials) };
|
||||
});
|
||||
ipcMain.handle("model:delete", (_event, request) =>
|
||||
callServer({ type: "deleteFile", fileID: request.fileID }, request.credentials));
|
||||
ipcMain.handle("model:delete", (_event, request) => modelHandlers.delete(request));
|
||||
|
||||
ipcMain.handle("identification:list", (_event, request) =>
|
||||
callServer({
|
||||
|
||||
@@ -172,7 +172,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-surface">
|
||||
<table><thead><tr><th>文件名</th><th>上传时间</th><th>大小</th><th>操作</th></tr></thead><tbody id="model-list"></tbody></table>
|
||||
<table><thead><tr><th>服务器文件名</th><th>原始文件名</th><th>上传时间</th><th>大小</th><th>操作</th></tr></thead><tbody id="model-list"></tbody></table>
|
||||
<p id="model-empty" class="empty-row">选择公司和产线后刷新模型列表</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -281,7 +281,7 @@ async function refreshModels() {
|
||||
if (!result) return;
|
||||
state.models = result.fileList;
|
||||
elements.modelList.innerHTML = state.models.map((model) => `
|
||||
<tr><td>${escapeHtml(model.fileName)}</td><td>${escapeHtml(model.uploadTime || "-")}</td>
|
||||
<tr><td>${escapeHtml(model.fileName)}</td><td>${escapeHtml(model.originalFileName || model.fileName)}</td><td>${escapeHtml(model.uploadTime || "-")}</td>
|
||||
<td>${formatSize(model.size)}</td><td><button class="table-action" data-model-download="${escapeHtml(model.fileID)}">下载</button><button class="table-action danger" data-model-delete="${escapeHtml(model.fileID)}">删除</button></td></tr>
|
||||
`).join("");
|
||||
elements.modelEmpty.hidden = state.models.length > 0;
|
||||
@@ -698,12 +698,16 @@ document.querySelector("#upload-model").addEventListener("click", async () => {
|
||||
state.models = models.fileList;
|
||||
const selected = await runBusy("正在选择模型文件", () => window.reinloop.chooseModelUploadFile());
|
||||
if (!selected) return;
|
||||
const existing = state.models.find((model) => model.fileName === selected.fileName);
|
||||
const enteredModelName = window.prompt("请输入模型在服务器上的文件名:", selected.fileName);
|
||||
if (enteredModelName === null) return;
|
||||
const modelName = enteredModelName.trim();
|
||||
if (!modelName) return showError(new Error("服务器文件名不能为空"));
|
||||
const existing = state.models.find((model) => model.fileName === modelName);
|
||||
let overwrite = false;
|
||||
if (existing) {
|
||||
if (!window.confirm(`已存在同名模型 ${selected.fileName},覆盖后无法恢复。是否继续?`)) return;
|
||||
const confirmation = window.prompt(`请输入完整文件名以确认覆盖:${selected.fileName}`);
|
||||
if (confirmation !== selected.fileName) {
|
||||
if (!window.confirm(`服务器已存在模型 ${modelName},覆盖后无法恢复。是否继续?`)) return;
|
||||
const confirmation = window.prompt(`请输入服务器文件名以确认覆盖:${modelName}`);
|
||||
if (confirmation !== modelName) {
|
||||
setStatus("文件名不匹配,已取消覆盖", "idle");
|
||||
return;
|
||||
}
|
||||
@@ -711,7 +715,7 @@ document.querySelector("#upload-model").addEventListener("click", async () => {
|
||||
}
|
||||
const result = await runBusy("正在上传模型", () => window.reinloop.uploadModel({
|
||||
deviceId: elements.deviceId.value, sourcePath: selected.sourcePath, fileName: selected.fileName,
|
||||
overwrite, credentials: credentials()
|
||||
modelName, overwrite, credentials: credentials()
|
||||
}));
|
||||
if (result) {
|
||||
setStatus(overwrite ? "模型已覆盖" : "模型已上传", "success");
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
function createModelHandlers({ callServer, fetchImpl = fetch }) {
|
||||
return {
|
||||
list(request) {
|
||||
return callServer({
|
||||
type: "listModels",
|
||||
folder: `${request.deviceId}/model_config`
|
||||
}, request.credentials);
|
||||
},
|
||||
|
||||
async upload(request) {
|
||||
if (!request.sourcePath || !request.fileName) throw new Error("请先选择模型文件");
|
||||
const sourcePath = path.resolve(request.sourcePath);
|
||||
const fileName = path.basename(request.fileName);
|
||||
const modelName = path.basename(request.modelName || fileName);
|
||||
await fs.promises.access(sourcePath, fs.constants.R_OK);
|
||||
const issued = await callServer({
|
||||
type: "uploadDataFile",
|
||||
fileName,
|
||||
folder: `${request.deviceId}/model_config`,
|
||||
modelName,
|
||||
overwrite: request.overwrite === true
|
||||
}, request.credentials);
|
||||
const form = new FormData();
|
||||
form.append("file", new Blob([await fs.promises.readFile(sourcePath)]), fileName);
|
||||
const response = await fetchImpl(issued.uploadMetadata.url, { method: "POST", body: form });
|
||||
if (![200, 204].includes(response.status)) throw new Error(`模型上传失败: HTTP ${response.status}`);
|
||||
return {
|
||||
success: true,
|
||||
fileID: issued.fileID,
|
||||
fileName: issued.fileName,
|
||||
originalFileName: issued.originalFileName
|
||||
};
|
||||
},
|
||||
|
||||
delete(request) {
|
||||
return callServer({ type: "deleteModel", fileID: request.fileID }, request.credentials);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { createModelHandlers };
|
||||
@@ -0,0 +1,59 @@
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
const test = require("node:test");
|
||||
const { createModelHandlers } = require("../model-handlers");
|
||||
const { callServer } = require("../server-client");
|
||||
const { createApp } = require("../../server/src/app");
|
||||
const { JsonStore } = require("../../server/src/store");
|
||||
|
||||
test("panel model kernel renames, overwrites, lists both names, and deletes on the server", async (context) => {
|
||||
const dataDirectory = await fs.promises.mkdtemp(path.join(os.tmpdir(), "reinloop-panel-server-"));
|
||||
const store = new JsonStore(dataDirectory);
|
||||
await store.initialize();
|
||||
const app = createApp({ store, adminToken: "test-token" });
|
||||
const server = await new Promise((resolve) => {
|
||||
const listeningServer = app.listen(0, "127.0.0.1", () => resolve(listeningServer));
|
||||
});
|
||||
context.after(async () => {
|
||||
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
||||
await fs.promises.rm(dataDirectory, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
const credentials = {
|
||||
apiUrl: `http://127.0.0.1:${server.address().port}/api`,
|
||||
adminToken: "test-token"
|
||||
};
|
||||
const handlers = createModelHandlers({ callServer });
|
||||
const sourcePath = path.join(dataDirectory, "controller-original.bin");
|
||||
const request = {
|
||||
deviceId: "company-a/line-1",
|
||||
sourcePath,
|
||||
fileName: "controller-original.bin",
|
||||
modelName: "pressure-controller.bin",
|
||||
credentials
|
||||
};
|
||||
|
||||
await fs.promises.writeFile(sourcePath, "first-version");
|
||||
const uploaded = await handlers.upload(request);
|
||||
assert.equal(uploaded.fileName, "pressure-controller.bin");
|
||||
assert.equal(uploaded.originalFileName, "controller-original.bin");
|
||||
|
||||
await fs.promises.writeFile(sourcePath, "second-version");
|
||||
const overwritten = await handlers.upload({ ...request, overwrite: true });
|
||||
assert.equal(overwritten.fileID, uploaded.fileID);
|
||||
|
||||
const listed = await handlers.list({ deviceId: request.deviceId, credentials });
|
||||
assert.equal(listed.fileList.length, 1);
|
||||
assert.equal(listed.fileList[0].fileName, "pressure-controller.bin");
|
||||
assert.equal(listed.fileList[0].originalFileName, "controller-original.bin");
|
||||
|
||||
const deleted = await handlers.delete({ fileID: overwritten.fileID, credentials });
|
||||
assert.equal(deleted.deletedCount, 1);
|
||||
const afterDelete = await handlers.list({ deviceId: request.deviceId, credentials });
|
||||
assert.deepEqual(afterDelete.fileList, []);
|
||||
await assert.rejects(fs.promises.access(
|
||||
path.join(dataDirectory, "models", "company-a", "line-1", "pressure-controller.bin")
|
||||
), { code: "ENOENT" });
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
const test = require("node:test");
|
||||
const { createModelHandlers } = require("../model-handlers");
|
||||
|
||||
test("panel model kernel renames, overwrites, and deletes by fileID", async (context) => {
|
||||
const directory = await fs.promises.mkdtemp(path.join(os.tmpdir(), "reinloop-panel-model-"));
|
||||
context.after(() => fs.promises.rm(directory, { recursive: true, force: true }));
|
||||
const sourcePath = path.join(directory, "controller-original.bin");
|
||||
await fs.promises.writeFile(sourcePath, "first-version");
|
||||
|
||||
const records = new Map();
|
||||
const requests = [];
|
||||
const callServer = async (payload) => {
|
||||
requests.push(payload);
|
||||
if (payload.type === "uploadDataFile") {
|
||||
const fileID = `model://${payload.folder.replace(/\/model_config$/, "")}/${payload.modelName}`;
|
||||
if (records.has(fileID) && !payload.overwrite) throw new Error("文件已存在");
|
||||
records.set(fileID, {
|
||||
fileID,
|
||||
fileName: payload.modelName,
|
||||
originalFileName: payload.fileName
|
||||
});
|
||||
return { ...records.get(fileID), uploadMetadata: { url: `memory://${encodeURIComponent(fileID)}` } };
|
||||
}
|
||||
if (payload.type === "deleteModel") {
|
||||
const deleted = records.delete(payload.fileID);
|
||||
return { success: true, deletedCount: deleted ? 1 : 0 };
|
||||
}
|
||||
throw new Error(`unexpected request: ${payload.type}`);
|
||||
};
|
||||
const handlers = createModelHandlers({
|
||||
callServer,
|
||||
fetchImpl: async () => ({ status: 204 })
|
||||
});
|
||||
const request = {
|
||||
deviceId: "company-a/line-1",
|
||||
sourcePath,
|
||||
fileName: "controller-original.bin",
|
||||
modelName: "pressure-controller.bin",
|
||||
credentials: { adminToken: "test-token" }
|
||||
};
|
||||
|
||||
const uploaded = await handlers.upload(request);
|
||||
assert.equal(uploaded.fileName, "pressure-controller.bin");
|
||||
assert.equal(uploaded.originalFileName, "controller-original.bin");
|
||||
assert.equal(records.size, 1);
|
||||
|
||||
await fs.promises.writeFile(sourcePath, "second-version");
|
||||
const overwritten = await handlers.upload({ ...request, overwrite: true });
|
||||
assert.equal(overwritten.fileID, uploaded.fileID);
|
||||
assert.equal(records.size, 1);
|
||||
|
||||
const deleted = await handlers.delete({ fileID: overwritten.fileID, credentials: request.credentials });
|
||||
assert.equal(deleted.deletedCount, 1);
|
||||
assert.equal(records.size, 0);
|
||||
assert.equal(requests.at(-1).type, "deleteModel");
|
||||
assert.equal(requests.at(-1).fileID, overwritten.fileID);
|
||||
});
|
||||
Reference in New Issue
Block a user