const fs = require("node:fs"); const path = require("node:path"); const { randomUUID, constants, createHmac, timingSafeEqual, verify } = require("node:crypto"); const express = require("express"); const multer = require("multer"); const BASE_FOLDER = "ReinLoop_GUI"; const VOLUME_REQUEST_TTL_MS = Number(process.env.VOLUME_REQUEST_TTL_MS || 300000); const DOWNLOAD_TOKEN_TTL_MS = Number(process.env.DOWNLOAD_TOKEN_TTL_MS || 300000); const IDENTIFICATION_RETENTION_MS = Number(process.env.IDENTIFICATION_RETENTION_MS || 30 * 24 * 60 * 60 * 1000); const DEVICE_HEARTBEAT_TTL_MS = 30_000; const CONFIG_SCHEMAS = { volume: { q_in_val: "number", dt: "number", xa_full: "number", p_max: "number", fit_low: "number", fit_high: "number", T_delta: "number", num_runs: "integer" }, identification: { q_in_val: "number", dt: "number", n_order: "integer", t_c: "number", levels: "array", dead_area: "number", xa_full: "number", V_val: "number", repeat: "integer" } }; function normalizeRelativePath(value, fallback = "") { const normalized = String(value || fallback).trim().replace(/\\/g, "/").replace(/^\/+|\/+$/g, ""); if (!normalized || normalized.split("/").some((part) => !part || part === "." || part === "..")) { throw new Error("目录格式无效"); } return normalized; } function normalizeFileName(value) { const fileName = path.basename(String(value || "").trim()); if (!fileName || fileName === "." || fileName === "..") throw new Error("缺少有效的 fileName"); return fileName; } function normalizeDeviceId(value) { if (typeof value !== "string") throw new Error("缺少有效的 deviceId"); const deviceId = value.trim().replace(/\\/g, "/"); const parts = deviceId.split("/"); if (!deviceId || parts.length !== 2 || parts.some((part) => !/^[a-z0-9][a-z0-9_-]{1,63}$/.test(part))) { throw new Error("deviceId 格式无效"); } return deviceId; } function controlDataDeviceId(folder) { const parts = String(folder || "").split("/"); if (parts.length < 3 || parts[2] !== "data_record") return null; return normalizeDeviceId(parts.slice(0, 2).join("/")); } function isControlDataRecord(record, deviceId) { if (!record || typeof record.folder !== "string") return false; const prefix = `${deviceId}/data_record`; return record.folder === prefix || record.folder.startsWith(`${prefix}/`); } function parseLicenseTimestamp(value, fieldName) { const text = String(value || "").trim(); const matched = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2})$/.exec(text); if (!matched) throw new Error(`${fieldName} 必须是 YYYY-MM-DD HH:MM 格式`); const [, year, month, day, hour, minute] = matched.map(Number); const wallClock = new Date(Date.UTC(year, month - 1, day, hour, minute)); if ( wallClock.getUTCFullYear() !== year || wallClock.getUTCMonth() !== month - 1 || wallClock.getUTCDate() !== day || wallClock.getUTCHours() !== hour || wallClock.getUTCMinutes() !== minute ) { throw new Error(`${fieldName} 无效`); } const utcMs = Date.UTC(year, month - 1, day, hour - 8, minute); return new Date(utcMs); } function parseSignedLicense(content, publicKeyPath) { if (!publicKeyPath) throw new Error("服务器未配置 LICENSE_PUBLIC_KEY_PATH"); const parts = String(content || "").split("|"); if (parts.length !== 2 || !parts.every(Boolean)) throw new Error("许可证格式无效"); const [payloadBase64, signatureBase64] = parts; let payload; let signature; try { payload = JSON.parse(Buffer.from(payloadBase64, "base64").toString("utf8")); signature = Buffer.from(signatureBase64, "base64"); } catch { throw new Error("许可证内容解析失败"); } const publicKey = fs.readFileSync(publicKeyPath); const valid = verify("sha256", Buffer.from(payloadBase64), { key: publicKey, padding: constants.RSA_PKCS1_PSS_PADDING, saltLength: constants.RSA_PSS_SALTLEN_AUTO }, signature); if (!valid) throw new Error("许可证签名验证失败"); return payload; } function licenseContentsMatch(record, event) { return ["companyId", "productionLineId", "customer", "issued", "expiry", "features", "license"] .every((field) => record[field] === event[field]); } function normalizeCode(value, fieldName) { const code = String(value || "").trim().toLowerCase(); if (!/^[a-z0-9][a-z0-9_-]{1,63}$/.test(code)) { throw new Error(`${fieldName}只能包含 2-64 位小写字母、数字、下划线或连字符`); } return code; } function publicLicense(record, includeContent = false) { const result = { ...record }; if (!includeContent) delete result.license; return result; } function validateConfigParameters(configType, parameters) { const schema = CONFIG_SCHEMAS[configType]; if (!schema) return `未知 configType: ${configType}`; if (!parameters || typeof parameters !== "object" || Array.isArray(parameters)) return "parameters 必须是对象"; const expected = Object.keys(schema); const actual = Object.keys(parameters); const missing = expected.filter((field) => !actual.includes(field)); const extra = actual.filter((field) => !expected.includes(field)); if (missing.length) return `缺少字段: ${missing.join(", ")}`; if (extra.length) return `包含不允许的字段: ${extra.join(", ")}`; for (const [field, type] of Object.entries(schema)) { const value = parameters[field]; if (type === "array" && (!Array.isArray(value) || !value.length)) return `${field} 必须是非空数组`; if (type === "number" && !Number.isFinite(value)) return `${field} 必须是数字`; if (type === "integer" && !Number.isInteger(value)) return `${field} 必须是整数`; } if (configType === "identification") { const { levels } = parameters; if (levels.length < 2 || (levels.length & (levels.length - 1)) !== 0) { return "levels 长度必须是大于等于 2 的 2 的整数次幂"; } if (levels.some((value) => !Number.isFinite(value) || value < 0 || value > 100)) { return "levels 中的开度必须是 0 到 100 的有限数字"; } if (parameters.q_in_val < 0) return "q_in_val 不能小于 0"; if (parameters.dt <= 0 || parameters.t_c < parameters.dt) return "必须满足 0 < dt <= t_c"; if (parameters.n_order < 2) return "n_order 必须大于等于 2"; if (parameters.repeat <= 0) return "repeat 必须是正整数"; if (parameters.dead_area < 0 || parameters.xa_full <= parameters.dead_area) { return "必须满足 0 <= dead_area < xa_full"; } if (parameters.xa_full < 1000) return "xa_full 不能小于 1000"; if (parameters.V_val <= 0) return "V_val 必须大于 0"; } return null; } function createApp({ store, adminToken = process.env.B_ADMIN_TOKEN || "dev-admin-token", licensePublicKeyPath = process.env.LICENSE_PUBLIC_KEY_PATH }) { const app = express(); const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 100 * 1024 * 1024 } }); const pendingUploads = new Map(); app.disable("x-powered-by"); app.use(express.json({ limit: "2mb" })); function publicBaseUrl(req) { return (process.env.PUBLIC_BASE_URL || `${req.protocol}://${req.get("host")}`).replace(/\/$/, ""); } function requireAdmin(event) { if (!adminToken) return "服务器未配置 B_ADMIN_TOKEN"; return event.adminToken === adminToken ? null : "B端管理令牌无效"; } function signDownload(fileID, expiresAtMs) { return createHmac("sha256", adminToken).update(`${fileID}\n${expiresAtMs}`).digest("hex"); } function downloadUrl(req, fileID) { const baseUrl = `${publicBaseUrl(req)}/files/${encodeURIComponent(fileID)}`; if (fileID.startsWith("model://")) return baseUrl; const expires = Date.now() + DOWNLOAD_TOKEN_TTL_MS; return `${baseUrl}?expires=${expires}&token=${signDownload(fileID, expires)}`; } function hasValidDownloadToken(req, fileID) { const expires = Number(req.query.expires); const token = String(req.query.token || ""); if (!Number.isSafeInteger(expires) || expires < Date.now() || !/^[0-9a-f]{64}$/.test(token)) return false; const expected = signDownload(fileID, expires); return timingSafeEqual(Buffer.from(token, "hex"), Buffer.from(expected, "hex")); } //config test----------------------------------- function logReceivedConfig({ configType, deviceId, requestId, record, config }) { console.info("[config] received", { configType, deviceId, requestId: requestId || null, fileID: record.fileID, cloudPath: record.cloudPath, storagePath: store.resolveStoredFile(record.fileID), config }); } function logConfigRead({ configType, deviceId, requestId, record }) { console.info("[config] read", { configType, deviceId, requestId: requestId || null, fileID: record.fileID, cloudPath: record.cloudPath, storagePath: store.resolveStoredFile(record.fileID) }); } //end config test------------------------------------ async function issueUpload(event, req) { const originalFileName = normalizeFileName(event.fileName); const folder = normalizeRelativePath(event.folder, "data_record"); const folderParts = folder.split("/"); if (folderParts.includes("data_record")) { const deviceId = controlDataDeviceId(folder); if (!deviceId) { return { success: false, errMsg: "控制数据目录必须为 /data_record/ 的子目录" }; } } const isModel = folderParts.at(-1) === "model_config"; const fileName = isModel && event.modelName ? normalizeFileName(event.modelName) : originalFileName; if (isModel && path.extname(fileName).toLowerCase() !== path.extname(originalFileName).toLowerCase()) { return { success: false, errMsg: "重命名后的模型扩展名必须与原文件一致" }; } const modelDeviceId = isModel ? normalizeDeviceId(folderParts.slice(0, -1).join("/")) : null; const cloudPath = isModel ? `${modelDeviceId}/${fileName}` : `${BASE_FOLDER}/${folder}/${fileName}`; const fileID = `${isModel ? "model" : "local"}://${cloudPath}`; if (isModel && event.overwrite !== true) { const database = await store.read(); const existing = database.fileRecords.find((record) => record.cloudPath === cloudPath); if (existing) { return { success: false, conflict: true, errMsg: "同名模型已存在", existing: { ...existing, originalFileName: existing.originalFileName || existing.fileName } }; } } const token = randomUUID(); pendingUploads.set(token, { fileName, originalFileName: isModel ? originalFileName : undefined, folder, cloudPath, fileID, configType: event.configType, parameters: event.parameters, expiresAt: Date.now() + 10 * 60 * 1000 }); return { code: 200, success: true, uploadMetadata: { url: `${publicBaseUrl(req)}/upload/${token}`, token, authorization: token, cosFileId: cloudPath, fileId: fileID }, fileID, cloudPath, fileName, originalFileName: isModel ? originalFileName : undefined }; } async function removeFile(database, fileID) { const filePath = store.resolveStoredFile(fileID); if (filePath) await fs.promises.rm(filePath, { force: true }); const before = database.fileRecords.length; database.fileRecords = database.fileRecords.filter((record) => record.fileID !== fileID); database.panelInbox = database.panelInbox.filter((record) => record.fileID !== fileID); database.identificationFiles = database.identificationFiles.filter((record) => record.fileID !== fileID); return before - database.fileRecords.length; } function backfillIdentificationFiles(database) { for (const record of database.fileRecords) { if (!record.folder.endsWith("/ind_data") || ![".csv", ".json"].includes(path.extname(record.fileName).toLowerCase())) continue; if (database.identificationFiles.some((item) => item.fileID === record.fileID)) continue; const deviceId = normalizeDeviceId(record.folder.slice(0, -"/ind_data".length)); const pending = database.panelInbox.some((item) => item.fileID === record.fileID); database.identificationFiles.push({ fileID: record.fileID, deviceId, fileName: record.fileName, mediaType: path.extname(record.fileName).slice(1).toLowerCase(), uploadTime: record.uploadTime, size: record.size, status: pending ? "pending" : "processed", processedAt: pending ? null : record.uploadTime, expiresAt: pending ? null : new Date(Date.parse(record.uploadTime) + IDENTIFICATION_RETENTION_MS).toISOString() }); } } async function purgeExpiredIdentificationFiles() { return store.update(async (database) => { backfillIdentificationFiles(database); const now = Date.now(); const expired = database.identificationFiles.filter((record) => record.status === "processed" && record.expiresAt && Date.parse(record.expiresAt) <= now ); for (const record of expired) await removeFile(database, record.fileID); return expired.length; }); } app.locals.purgeExpiredIdentificationFiles = purgeExpiredIdentificationFiles; async function dispatch(event, req) { switch (event.type) { case "listOrganizations": { const authError = requireAdmin(event); if (authError) return { success: false, errMsg: authError }; const database = await store.read(); const now = Date.now(); const companies = database.companies.map((company) => ({ ...company, productionLines: database.productionLines .filter((line) => line.companyId === company.id) .map((line) => ({ ...line, online: Boolean(line.lastSeenAt && now - Date.parse(line.lastSeenAt) <= DEVICE_HEARTBEAT_TTL_MS), lastSeenAt: line.lastSeenAt || null })) })); return { success: true, companies }; } case "deviceHeartbeat": { const deviceId = normalizeDeviceId(event.deviceId); return store.update((database) => { const line = database.productionLines.find((item) => item.deviceId === deviceId); if (!line) return { success: false, errMsg: "设备未注册" }; line.lastSeenAt = new Date().toISOString(); return { success: true, deviceId, lastSeenAt: line.lastSeenAt }; }); } case "createCompany": { const authError = requireAdmin(event); if (authError) return { success: false, errMsg: authError }; const name = String(event.name || "").trim(); if (!name) return { success: false, errMsg: "公司名称不能为空" }; const code = normalizeCode(event.code, "公司编码"); return store.update((database) => { if (database.companies.some((item) => item.code === code)) { return { success: false, errMsg: `公司编码 ${code} 已存在` }; } const company = { id: store.createId("company"), name, code, createdAt: new Date().toISOString() }; database.companies.push(company); return { success: true, company }; }); } case "createProductionLine": { const authError = requireAdmin(event); if (authError) return { success: false, errMsg: authError }; const name = String(event.name || "").trim(); if (!name) return { success: false, errMsg: "产线名称不能为空" }; const code = normalizeCode(event.code, "产线编码"); return store.update((database) => { const company = database.companies.find((item) => item.id === event.companyId); if (!company) return { success: false, errMsg: "公司不存在" }; if (database.productionLines.some((item) => item.companyId === company.id && item.code === code)) { return { success: false, errMsg: `该公司下产线编码 ${code} 已存在` }; } const productionLine = { id: store.createId("line"), companyId: company.id, name, code, deviceId: `${company.code}/${code}`, createdAt: new Date().toISOString(), lastSeenAt: null }; database.productionLines.push(productionLine); return { success: true, productionLine }; }); } case "createLicense": { const authError = requireAdmin(event); if (authError) return { success: false, errMsg: authError }; const required = ["licenseId", "companyId", "productionLineId", "customer", "issued", "expiry", "license"]; const missing = required.filter((field) => !String(event[field] || "").trim()); if (missing.length) return { success: false, errMsg: `缺少必填字段: ${missing.join(", ")}` }; const licenseId = String(event.licenseId).trim(); if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(licenseId)) { return { success: false, errMsg: "licenseId 必须是 UUID" }; } const issuedAt = parseLicenseTimestamp(event.issued, "issued"); const expiryAt = parseLicenseTimestamp(event.expiry, "expiry"); if (expiryAt <= issuedAt) return { success: false, errMsg: "expiry 必须晚于 issued" }; const payload = parseSignedLicense(event.license, licensePublicKeyPath); const signedFields = { license_id: licenseId, company_id: String(event.companyId), production_line_id: String(event.productionLineId), customer: String(event.customer), issued: String(event.issued), expiry: String(event.expiry), features: String(event.features || "*") }; if (Object.entries(signedFields).some(([field, value]) => payload[field] !== value)) { return { success: false, errMsg: "许可证载荷与请求字段不一致" }; } return store.update((database) => { const company = database.companies.find((item) => item.id === event.companyId); const line = database.productionLines.find((item) => item.id === event.productionLineId && item.companyId === event.companyId); if (!company || !line) return { success: false, errMsg: "公司或产线不存在" }; if (payload.device_id !== line.deviceId) return { success: false, errMsg: "许可证 device_id 与产线不匹配" }; const existing = database.licenses.find((item) => item.licenseId === licenseId); if (existing) { return licenseContentsMatch(existing, { ...event, features: event.features || "*" }) ? { success: true, license: publicLicense(existing), idempotent: true } : { success: false, conflict: true, errMsg: "licenseId 已存在且内容不同" }; } const record = { licenseId, companyId: company.id, productionLineId: line.id, companyName: company.name, productionLineName: line.name, deviceId: line.deviceId, customer: String(event.customer), issued: String(event.issued), expiry: String(event.expiry), issuedAt: issuedAt.toISOString(), expiryAt: expiryAt.toISOString(), features: event.features || "*", license: String(event.license), status: "active", createdAt: new Date().toISOString(), revokedAt: null, revocationReason: null }; database.licenses.push(record); return { success: true, license: publicLicense(record) }; }); } case "listLicenses": { const authError = requireAdmin(event); if (authError) return { success: false, errMsg: authError }; const database = await store.read(); return { success: true, licenses: database.licenses.map((record) => publicLicense(record)) .sort((left, right) => right.createdAt.localeCompare(left.createdAt)) }; } case "getLicense": { const authError = requireAdmin(event); if (authError) return { success: false, errMsg: authError }; const database = await store.read(); const record = database.licenses.find((item) => item.licenseId === event.licenseId); return record ? { success: true, license: publicLicense(record, true) } : { success: false, errMsg: "许可证不存在" }; } case "revokeLicense": { const authError = requireAdmin(event); if (authError) return { success: false, errMsg: authError }; return store.update((database) => { const record = database.licenses.find((item) => item.licenseId === event.licenseId); if (!record) return { success: false, errMsg: "许可证不存在" }; record.status = "revoked"; record.revokedAt = new Date().toISOString(); record.revocationReason = String(event.reason || "管理员撤销").trim(); return { success: true, license: publicLicense(record) }; }); } case "validateLicense": { const database = await store.read(); const record = database.licenses.find((item) => item.licenseId === event.licenseId); if (!record) return { success: true, valid: false, status: "not_found" }; if (event.deviceId && normalizeDeviceId(event.deviceId) !== record.deviceId) { return { success: true, valid: false, status: "device_mismatch", licenseId: record.licenseId }; } if (new Date(record.expiryAt || parseLicenseTimestamp(record.expiry, "expiry")) <= new Date()) { return { success: true, valid: false, status: "expired", licenseId: record.licenseId }; } return { success: true, valid: record.status === "active", status: record.status, licenseId: record.licenseId }; } case "uploadDataFile": if (normalizeRelativePath(event.folder, "data_record").endsWith("/model_config")) { const authError = requireAdmin(event); if (authError) return { success: false, errMsg: authError }; } return issueUpload(event, req); case "issueModelUpload": { const authError = requireAdmin(event); if (authError) return { success: false, errMsg: authError }; const deviceId = normalizeDeviceId(event.deviceId); return issueUpload({ ...event, folder: `${deviceId}/model_config` }, req); } case "listModels": { const folder = normalizeRelativePath(event.folder, "model_config"); const database = await store.read(); const fileList = database.fileRecords .filter((record) => record.folder === folder) .sort((left, right) => right.uploadTime.localeCompare(left.uploadTime)) .slice(0, 100); return { success: true, files: fileList.map((record) => record.fileName), fileList: fileList.map((record) => ({ ...record, originalFileName: record.originalFileName || record.fileName })) }; } case "downloadModel": { const database = await store.read(); const record = database.fileRecords.find((item) => item.fileID === event.fileID); if (!record || !store.resolveStoredFile(record.fileID)) return { success: false, errMsg: "文件不存在" }; if (!record.fileID.startsWith("model://")) { const authError = requireAdmin(event); if (authError) return { success: false, errMsg: authError }; } return { success: true, url: downloadUrl(req, record.fileID), fileName: record.fileName, originalFileName: record.originalFileName || record.fileName }; } case "deleteFile": case "deleteModel": { const authError = requireAdmin(event); if (authError) return { success: false, errMsg: authError }; return store.update(async (database) => { let records = event.fileID ? database.fileRecords.filter((item) => item.fileID === event.fileID) : database.fileRecords.filter((item) => item.folder === normalizeRelativePath(event.folder, "model_config") && item.fileName === normalizeFileName(event.fileName) ); if (!event.fileID && records.length > 1) { return { success: false, ambiguous: true, errMsg: `发现 ${records.length} 个同名文件,请用 fileID 精确指定`, candidates: records }; } if (!records.length) return { success: false, errMsg: "数据库中未找到对应记录" }; let deletedCount = 0; for (const record of records) deletedCount += await removeFile(database, record.fileID); return { success: true, deletedFileID: records.map((record) => record.fileID).join(", "), deletedCount }; }); } case "publishIdentificationConfig": { const authError = requireAdmin(event); if (authError) return { success: false, errMsg: authError }; const deviceId = normalizeDeviceId(event.deviceId); const validationError = validateConfigParameters("identification", event.parameters); if (validationError) return { success: false, errMsg: validationError }; return issueUpload({ fileName: "identification_config.csv", folder: `${deviceId}/identification_config` }, req); } case "publishVolumeConfig": { const authError = requireAdmin(event); if (authError) return { success: false, errMsg: authError }; const validationError = validateConfigParameters("volume", event.parameters); if (validationError) return { success: false, errMsg: validationError }; const result = await issueUpload({ fileName: "volume_config.json", folder: process.env.VOLUME_CONFIG_FOLDER || "volume_config", configType: "volume", parameters: event.parameters }, req); return { ...result, parameters: event.parameters }; } case "getVolumeConfigFile": { const folder = process.env.VOLUME_CONFIG_FOLDER || "volume_config"; const fileID = `local://${BASE_FOLDER}/${folder}/volume_config.json`; const database = await store.read(); if (!database.fileRecords.some((record) => record.fileID === fileID)) { return { success: false, notFound: true, errMsg: "容积配置文件尚未发布" }; } return { success: true, fileID, cloudPath: `${BASE_FOLDER}/${folder}/volume_config.json`, url: downloadUrl(req, fileID) }; } case "getFunctionConfig": { if (event.configType !== "volume") return { success: false, errMsg: `未知 configType: ${event.configType}` }; const database = await store.read(); const config = database.functionConfigs.find((item) => item.configType === event.configType); return config ? { success: true, ...config } : { success: false, notFound: true, errMsg: "参数配置尚未发布" }; } case "getIdentificationConfig": { const deviceId = normalizeDeviceId(event.deviceId); const database = await store.read(); const record = database.fileRecords.find((item) => item.folder === `${deviceId}/identification_config` && item.fileName === "identification_config.csv"); if (!record) return { success: false, errMsg: "服务器尚未配置辨识参数" }; logConfigRead({ configType: "identification", deviceId, record }); return { success: true, fileName: record.fileName, fileID: record.fileID, cloudPath: record.cloudPath, url: downloadUrl(req, record.fileID) }; } case "getPendingPanelFile": { const authError = requireAdmin(event); if (authError) return { success: false, errMsg: authError }; const deviceId = normalizeDeviceId(event.deviceId); const database = await store.read(); const message = database.panelInbox.find((item) => { if (item.deviceId !== deviceId) return false; if (item.mediaType !== "csv") return true; return database.identificationFeedback.some((feedback) => feedback.deviceId === deviceId && feedback.runId === item.fileName ); }); if (!message) return { success: true, pending: false }; return { success: true, pending: true, fileID: message.fileID, fileName: message.fileName, mediaType: message.mediaType, uploadTime: message.uploadTime, url: downloadUrl(req, message.fileID) }; } case "ackPanelFile": { const authError = requireAdmin(event); if (authError) return { success: false, errMsg: authError }; const deviceId = normalizeDeviceId(event.deviceId); if (!event.fileID) return { success: false, errMsg: "缺少 fileID" }; return store.update((database) => { backfillIdentificationFiles(database); const messages = database.panelInbox.filter((item) => item.deviceId === deviceId && item.fileID === String(event.fileID) ); const historyRecord = database.identificationFiles.find((item) => item.deviceId === deviceId && item.fileID === String(event.fileID) ); if (!messages.length && !historyRecord) return { success: false, errMsg: "辨识文件不存在" }; const processedAt = new Date().toISOString(); if (historyRecord) { historyRecord.status = "processed"; historyRecord.processedAt = processedAt; historyRecord.expiresAt = new Date(Date.now() + IDENTIFICATION_RETENTION_MS).toISOString(); } const before = database.panelInbox.length; database.panelInbox = database.panelInbox.filter((item) => !(item.deviceId === deviceId && item.fileID === String(event.fileID)) ); return { success: true, processed: true, deleted: before - database.panelInbox.length, processedAt }; }); } case "listIdentificationFiles": { const authError = requireAdmin(event); if (authError) return { success: false, errMsg: authError }; const deviceId = normalizeDeviceId(event.deviceId); await purgeExpiredIdentificationFiles(); const database = await store.read(); const page = Math.max(1, Number.parseInt(event.page, 10) || 1); const pageSize = Math.min(100, Math.max(1, Number.parseInt(event.pageSize, 10) || 20)); const files = database.identificationFiles .filter((record) => record.deviceId === deviceId) .filter((record) => !event.mediaType || record.mediaType === event.mediaType) .filter((record) => !event.status || record.status === event.status) .sort((left, right) => right.uploadTime.localeCompare(left.uploadTime)); const offset = (page - 1) * pageSize; return { success: true, files: files.slice(offset, offset + pageSize), total: files.length, page, pageSize }; } case "listControlFiles": { const authError = requireAdmin(event); if (authError) return { success: false, errMsg: authError }; const deviceId = normalizeDeviceId(event.deviceId); const page = Math.max(1, Number.parseInt(event.page, 10) || 1); const pageSize = Math.min(100, Math.max(1, Number.parseInt(event.pageSize, 10) || 20)); const database = await store.read(); const files = database.fileRecords .filter((record) => isControlDataRecord(record, deviceId)) .sort((left, right) => right.uploadTime.localeCompare(left.uploadTime)); const offset = (page - 1) * pageSize; return { success: true, files: files.slice(offset, offset + pageSize).map((record) => ({ fileID: record.fileID, fileName: record.fileName, uploadTime: record.uploadTime, size: record.size })), total: files.length, page, pageSize }; } case "getControlFileDownload": { const authError = requireAdmin(event); if (authError) return { success: false, errMsg: authError }; if (!event.fileID) return { success: false, errMsg: "缺少 fileID" }; const database = await store.read(); const record = database.fileRecords.find((item) => item.fileID === String(event.fileID)); if (!record || !controlDataDeviceId(record.folder) || !store.resolveStoredFile(record.fileID)) { return { success: false, errMsg: "控制数据文件不存在或不属于控制数据目录" }; } try { await fs.promises.access(store.resolveStoredFile(record.fileID)); } catch { return { success: false, errMsg: "控制数据文件不存在" }; } return { success: true, fileID: record.fileID, fileName: record.fileName, uploadTime: record.uploadTime, size: record.size, url: downloadUrl(req, record.fileID) }; } case "deleteControlFile": { const authError = requireAdmin(event); if (authError) return { success: false, errMsg: authError }; if (!event.fileID) return { success: false, errMsg: "缺少 fileID" }; return store.update(async (database) => { const record = database.fileRecords.find((item) => item.fileID === String(event.fileID)); if (!record || !controlDataDeviceId(record.folder)) { return { success: false, errMsg: "控制数据文件不存在或不属于控制数据目录" }; } const deletedCount = await removeFile(database, record.fileID); return deletedCount ? { success: true, deletedCount } : { success: false, errMsg: "控制数据文件不存在" }; }); } case "getIdentificationFileDownload": { const authError = requireAdmin(event); if (authError) return { success: false, errMsg: authError }; const database = await store.read(); const historyRecord = database.identificationFiles.find((item) => item.fileID === event.fileID); const fileRecord = database.fileRecords.find((item) => item.fileID === event.fileID); if (!historyRecord || !fileRecord || !store.resolveStoredFile(fileRecord.fileID)) { return { success: false, errMsg: "辨识文件不存在" }; } return { success: true, fileID: fileRecord.fileID, fileName: fileRecord.fileName, mediaType: historyRecord.mediaType, url: downloadUrl(req, fileRecord.fileID) }; } case "deleteIdentificationFile": { const authError = requireAdmin(event); if (authError) return { success: false, errMsg: authError }; if (!event.fileID) return { success: false, errMsg: "缺少 fileID" }; return store.update(async (database) => { if (!database.identificationFiles.some((item) => item.fileID === event.fileID)) { return { success: false, errMsg: "辨识文件不存在" }; } const deletedCount = await removeFile(database, event.fileID); return { success: true, deletedCount }; }); } case "registerIdentificationResult": { const deviceId = normalizeDeviceId(event.deviceId); if (!event.runId) return { success: false, errMsg: "缺少 deviceId 或 runId" }; return store.update((database) => { const record = { deviceId, runId: String(event.runId), fileName: event.fileName || String(event.runId), status: "waiting_feedback", result: null, updateTime: new Date().toISOString() }; database.identificationFeedback = database.identificationFeedback.filter((item) => item.deviceId !== deviceId); database.identificationFeedback.push(record); return { success: true, runId: record.runId, status: record.status }; }); } case "getIdentificationFeedback": { const deviceId = normalizeDeviceId(event.deviceId); if (!event.runId) return { success: false, errMsg: "缺少 deviceId 或 runId" }; const database = await store.read(); const record = database.identificationFeedback.find((item) => item.deviceId === deviceId && item.runId === String(event.runId)); if (!record || record.status !== "feedback_ready") return { success: true, ready: false }; return { success: true, ready: true, result: record.result, runId: record.runId }; } case "setIdentificationFeedback": { const authError = requireAdmin(event); if (authError) return { success: false, errMsg: authError }; const deviceId = normalizeDeviceId(event.deviceId); if (event.result !== 0 && event.result !== 1) return { success: false, errMsg: "result 必须是数字 0/1" }; return store.update((database) => { const record = database.identificationFeedback.find((item) => item.deviceId === deviceId); if (!record) return { success: false, errMsg: "当前没有待审核的辨识结果" }; if (event.runId && String(event.runId) !== record.runId) return { success: false, errMsg: "runId 与当前待审核结果不一致" }; record.status = "feedback_ready"; record.result = event.result; record.updateTime = new Date().toISOString(); return { success: true, runId: record.runId, fileName: record.fileName, result: record.result }; }); } case "ackIdentificationFeedback": { const deviceId = normalizeDeviceId(event.deviceId); if (!event.runId) return { success: false, errMsg: "缺少 deviceId 或 runId" }; return store.update((database) => { const before = database.identificationFeedback.length; database.identificationFeedback = database.identificationFeedback.filter((item) => !(item.deviceId === deviceId && item.runId === String(event.runId))); return { success: true, deleted: before - database.identificationFeedback.length }; }); } case "createVolumeConfigRequest": { const deviceId = normalizeDeviceId(event.deviceId); return store.update(async (database) => { const previous = database.volumeConfigRequests.filter((item) => item.deviceId === deviceId); for (const record of previous) if (record.configFileID) await removeFile(database, record.configFileID); database.volumeConfigRequests = database.volumeConfigRequests.filter((item) => item.deviceId !== deviceId); const createdAtMs = Date.now(); const record = { deviceId, requestId: `${createdAtMs}-${randomUUID().slice(0, 10)}`, status: "waiting_upload", createdAtMs, expiresAtMs: createdAtMs + VOLUME_REQUEST_TTL_MS, configFileID: null, configFileName: null, uploadedAtMs: null, updateTime: new Date().toISOString() }; database.volumeConfigRequests.push(record); return { success: true, requestId: record.requestId, createdAtMs, expiresAtMs: record.expiresAtMs }; }); } case "getPendingVolumeConfigRequest": { const deviceId = normalizeDeviceId(event.deviceId); return store.update(async (database) => { const record = database.volumeConfigRequests.find((item) => item.deviceId === deviceId && item.status === "waiting_upload"); if (!record) return { success: true, pending: false }; if (Date.now() > record.expiresAtMs) { database.volumeConfigRequests = database.volumeConfigRequests.filter((item) => item !== record); return { success: true, pending: false, expired: true }; } return { success: true, pending: true, requestId: record.requestId, createdAtMs: record.createdAtMs, expiresAtMs: record.expiresAtMs }; }); } case "submitVolumeConfigFile": { const deviceId = normalizeDeviceId(event.deviceId); if (!event.requestId || !event.fileID) return { success: false, errMsg: "缺少 deviceId、requestId 或 fileID" }; return store.update((database) => { const record = database.volumeConfigRequests.find((item) => item.deviceId === deviceId && item.requestId === String(event.requestId)); if (!record) return { success: false, errMsg: "容积参数请求不存在" }; if (record.status !== "waiting_upload" || Date.now() > record.expiresAtMs) return { success: false, errMsg: "容积参数请求已失效" }; const fileName = event.fileName || "volume_measurement.json"; const expectedFolder = `${deviceId}/volume_config_requests/${record.requestId}`; const fileRecord = database.fileRecords.find((item) => item.fileID === String(event.fileID) && item.folder === expectedFolder && item.fileName === fileName); if (!fileRecord) return { success: false, errMsg: "上传文件不属于本次容积参数请求" }; record.status = "ready"; record.configFileID = fileRecord.fileID; record.configFileName = fileName; record.uploadedAtMs = Date.now(); record.updateTime = new Date().toISOString(); return { success: true, requestId: record.requestId, uploadedAtMs: record.uploadedAtMs }; }); } case "getVolumeConfigRequest": { const deviceId = normalizeDeviceId(event.deviceId); if (!event.requestId) return { success: false, errMsg: "缺少 deviceId 或 requestId" }; const database = await store.read(); const record = database.volumeConfigRequests.find((item) => item.deviceId === deviceId && item.requestId === String(event.requestId)); if (!record || Date.now() > record.expiresAtMs) return { success: true, ready: false, expired: true }; if (record.status !== "ready") return { success: true, ready: false, expired: false }; const fileRecord = database.fileRecords.find((item) => item.fileID === record.configFileID); if (!fileRecord) return { success: false, errMsg: "容积配置文件记录不存在" }; logConfigRead({ configType: "volume", deviceId, requestId: record.requestId, record: fileRecord }); return { success: true, ready: true, expired: false, requestId: record.requestId, fileName: record.configFileName, fileID: fileRecord.fileID, cloudPath: fileRecord.cloudPath, uploadedAtMs: record.uploadedAtMs, url: downloadUrl(req, record.configFileID) }; } case "ackVolumeConfigRequest": { const deviceId = normalizeDeviceId(event.deviceId); if (!event.requestId) return { success: false, errMsg: "缺少 deviceId 或 requestId" }; return store.update(async (database) => { const records = database.volumeConfigRequests.filter((item) => item.deviceId === deviceId && item.requestId === String(event.requestId)); for (const record of records) if (record.configFileID) await removeFile(database, record.configFileID); database.volumeConfigRequests = database.volumeConfigRequests.filter((item) => !records.includes(item)); return { success: true, deleted: records.length }; }); } default: return { success: false, errMsg: "无效的 type 字段" }; } } app.get("/health", (req, res) => res.json({ success: true, service: "reinloop-server" })); app.post("/upload/:token", upload.single("file"), async (req, res, next) => { try { const pending = pendingUploads.get(req.params.token); if (!pending || pending.expiresAt < Date.now()) return res.status(403).json({ success: false, errMsg: "上传凭证无效或已过期" }); if (!req.file) return res.status(400).json({ success: false, errMsg: "缺少 file 表单字段" }); const destination = store.resolveStoredFile(pending.fileID); await fs.promises.mkdir(path.dirname(destination), { recursive: true }); await fs.promises.writeFile(destination, req.file.buffer); await store.update((database) => { const record = { _id: store.createId("file"), fileName: pending.fileName, originalFileName: pending.originalFileName, folder: pending.folder, cloudPath: pending.cloudPath, fileID: pending.fileID, uploadTime: new Date().toISOString(), size: req.file.size }; database.fileRecords = database.fileRecords.filter((item) => item.cloudPath !== pending.cloudPath); database.fileRecords.push(record); const folderParts = pending.folder.split("/"); const extension = path.extname(pending.fileName).toLowerCase(); if (folderParts.at(-1) === "ind_data" && [".csv", ".json"].includes(extension)) { const deviceId = normalizeDeviceId(folderParts.slice(0, -1).join("/")); const message = { deviceId, fileID: pending.fileID, fileName: pending.fileName, mediaType: extension.slice(1), uploadTime: record.uploadTime }; database.panelInbox = database.panelInbox.filter((item) => item.fileID !== pending.fileID); database.panelInbox.push(message); database.identificationFiles = database.identificationFiles.filter((item) => item.fileID !== pending.fileID); database.identificationFiles.push({ ...message, size: record.size, status: "pending", processedAt: null, expiresAt: null }); } if (pending.configType && pending.parameters) { const config = { configType: pending.configType, parameters: pending.parameters, version: Date.now(), updateTime: new Date().toISOString() }; database.functionConfigs = database.functionConfigs.filter( (item) => item.configType !== pending.configType ); database.functionConfigs.push(config); } }); const folderParts = pending.folder.split("/"); if (folderParts.at(-1) === "identification_config") { logReceivedConfig({ configType: "identification", deviceId: folderParts.slice(0, -1).join("/"), record: { fileID: pending.fileID, cloudPath: pending.cloudPath }, config: req.file.buffer.toString("utf8").trim() }); } else if (folderParts.at(-2) === "volume_config_requests") { let config; try { config = JSON.parse(req.file.buffer.toString("utf8")); } catch { config = ""; } logReceivedConfig({ configType: "volume", deviceId: folderParts.slice(0, -2).join("/"), requestId: folderParts.at(-1), record: { fileID: pending.fileID, cloudPath: pending.cloudPath }, config }); } pendingUploads.delete(req.params.token); res.status(204).end(); } catch (error) { next(error); } }); app.get("/files/:fileID", async (req, res, next) => { try { const fileID = decodeURIComponent(req.params.fileID); if (!fileID.startsWith("model://") && !hasValidDownloadToken(req, fileID)) { return res.status(403).json({ success: false, errMsg: "下载凭证无效或已过期" }); } const database = await store.read(); const record = database.fileRecords.find((item) => item.fileID === fileID); const filePath = record && store.resolveStoredFile(fileID); if (!record || !filePath) return res.status(404).json({ success: false, errMsg: "文件不存在" }); await fs.promises.access(filePath); res.download(filePath, record.fileName); } catch (error) { if (error.code === "ENOENT") return res.status(404).json({ success: false, errMsg: "文件不存在" }); next(error); } }); const apiHandler = async (req, res) => { try { res.json(await dispatch(req.body || {}, req)); } catch (error) { res.status(400).json({ success: false, errMsg: error.message }); } }; app.post("/", apiHandler); app.post("/api", apiHandler); app.use((error, req, res, next) => { console.error(error); res.status(500).json({ success: false, errMsg: "服务器内部错误" }); }); return app; } module.exports = { createApp, validateConfigParameters };