const fs = require("node:fs"); const path = require("node:path"); const { randomUUID, constants, 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_URL_TTL_MS = Number(process.env.DOWNLOAD_URL_TTL_MS || process.env.DOWNLOAD_TOKEN_TTL_MS || 300000); const DEVICE_TOKEN_TTL_MS = Number(process.env.DEVICE_TOKEN_TTL_MS || 15 * 60 * 1000); 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(); const pendingDownloads = new Map(); const pendingDeviceTokens = 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 logRevokeAction({ event, licenseId, success, errCode = null }) { console.info("[license] revoke", { time: new Date().toISOString(), requestId: event.requestId || null, type: "revokeLicense", licenseId: licenseId || null, result: success ? "success" : "failed", errCode }); } function normalizeEventCompat(event) { if (!event || typeof event !== "object" || Array.isArray(event)) return event; const type = String(event.type || event.action || "").trim(); const legacyRevokeTypes = new Set(["revoke_license", "licenseRevoke", "revoke"]); if (!legacyRevokeTypes.has(type) && !(event.action === "revokeLicense" && !event.type)) return event; console.warn("[license] legacy revoke request mapped", { type: type || null, requestId: event.requestId || null }); return { ...event, type: "revokeLicense", licenseId: event.licenseId ?? event.license_id ?? event.id, reason: event.reason ?? event.revocationReason, adminToken: event.adminToken ?? event.admin_token ?? event.token }; } function requestIp(req) { return String(req.ip || req.socket?.remoteAddress || "").trim(); } function issueDownloadUrl(req, fileID) { const ticket = randomUUID().replace(/-/g, ""); pendingDownloads.set(ticket, { fileID, requestIp: requestIp(req), expiresAtMs: Date.now() + DOWNLOAD_URL_TTL_MS }); return `${publicBaseUrl(req)}/downloads/${ticket}`; } function consumeDownloadTicket(req, ticket) { const request = pendingDownloads.get(ticket); if (!request) return { ok: false, errMsg: "下载链接无效或已失效" }; pendingDownloads.delete(ticket); if (request.expiresAtMs < Date.now()) return { ok: false, errMsg: "下载链接已过期" }; if (!request.requestIp || request.requestIp !== requestIp(req)) { return { ok: false, errMsg: "下载请求来源IP不匹配" }; } return { ok: true, fileID: request.fileID }; } function verifyLicenseRecord(record, deviceId) { if (!record) return { success: false, valid: false, status: "not_found" }; if (deviceId && 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 }; } function issueDeviceToken({ deviceId, licenseId }) { const token = randomUUID().replace(/-/g, ""); const expiresAtMs = Date.now() + DEVICE_TOKEN_TTL_MS; pendingDeviceTokens.set(token, { deviceId, licenseId, expiresAtMs }); return { token, expiresAtMs }; } function consumeDeviceContext(event) { const deviceToken = String(event.deviceToken || "").trim(); if (!deviceToken) { return { ok: false, errCode: "DEVICE_TOKEN_REQUIRED", errMsg: "缺少 deviceToken" }; } const session = pendingDeviceTokens.get(deviceToken); if (!session) { return { ok: false, errCode: "DEVICE_TOKEN_INVALID", errMsg: "deviceToken 无效" }; } if (session.expiresAtMs <= Date.now()) { pendingDeviceTokens.delete(deviceToken); return { ok: false, errCode: "DEVICE_TOKEN_EXPIRED", errMsg: "deviceToken 已过期" }; } return { ok: true, ...session }; } //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); database.volumeConfigs = database.volumeConfigs.filter((record) => record.fileID !== fileID); database.panelNotifications = database.panelNotifications.filter((record) => record.fileID !== fileID); return before - database.fileRecords.length; } async function removeDeviceDirectories(deviceId) { const segments = String(deviceId || "").split("/"); if (segments.length !== 2 || !segments[0] || !segments[1]) return 0; const [companyCode, lineCode] = segments; const targets = [ path.join(store.modelsDirectory, companyCode, lineCode), path.join(store.filesDirectory, "ReinLoop_GUI", companyCode, lineCode) ]; let removed = 0; for (const directory of targets) { try { const stat = await fs.promises.stat(directory); if (!stat.isDirectory()) continue; } catch (_error) { continue; } await fs.promises.rm(directory, { recursive: true, force: true }); removed += 1; } return removed; } async function removeProductionLineData(database, { companyId, productionLineId, failOnActiveLicense = true }) { const line = database.productionLines.find((item) => item.id === productionLineId && item.companyId === companyId); if (!line) return { success: false, errMsg: "产线不存在" }; const activeLicenses = database.licenses.filter((item) => item.companyId === companyId && item.productionLineId === productionLineId && item.status === "active" ); if (failOnActiveLicense && activeLicenses.length) { return { success: false, errMsg: `请先撤销该产线的 ${activeLicenses.length} 个有效许可证后再删除产线`, errCode: "ACTIVE_LICENSES_PRESENT" }; } const deviceId = line.deviceId; const relatedFileIDs = database.fileRecords .filter((record) => { if (record.fileID.startsWith(`model://${deviceId}/`)) return true; const prefix = `${deviceId}/`; return record.folder === deviceId || record.folder.startsWith(prefix); }) .map((record) => record.fileID); let deletedFileCount = 0; for (const fileID of new Set(relatedFileIDs)) { deletedFileCount += await removeFile(database, fileID); } const beforeFeedback = database.identificationFeedback.length; database.identificationFeedback = database.identificationFeedback.filter((item) => item.deviceId !== deviceId); const deletedFeedback = beforeFeedback - database.identificationFeedback.length; const beforeRequests = database.volumeConfigRequests.length; database.volumeConfigRequests = database.volumeConfigRequests.filter((item) => item.deviceId !== deviceId); const deletedRequests = beforeRequests - database.volumeConfigRequests.length; const beforeConfigs = database.volumeConfigs.length; database.volumeConfigs = database.volumeConfigs.filter((item) => item.deviceId !== deviceId); const deletedConfigs = beforeConfigs - database.volumeConfigs.length; const beforeNotifications = database.panelNotifications.length; database.panelNotifications = database.panelNotifications.filter((item) => item.deviceId !== deviceId); const deletedNotifications = beforeNotifications - database.panelNotifications.length; const deletedLicenses = database.licenses.filter((item) => item.companyId === companyId && item.productionLineId === productionLineId ).length; database.licenses = database.licenses.filter((item) => !(item.companyId === companyId && item.productionLineId === productionLineId) ); const deletedDirectories = await removeDeviceDirectories(deviceId); database.productionLines = database.productionLines.filter((item) => item.id !== productionLineId); return { success: true, deletedProductionLineId: productionLineId, deletedFiles: deletedFileCount, deletedDirectories, deletedLicenses, deletedFeedback, deletedVolumeRequests: deletedRequests, deletedVolumeConfigs: deletedConfigs, deletedNotifications }; } 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() }); } } function addPanelNotification(database, { deviceId, type, title, message, requestId = null, fileID = null }) { const notification = { notificationId: store.createId("pn"), deviceId, type, title, message, requestId, fileID, createdAtMs: Date.now() }; database.panelNotifications.push(notification); return notification; } function findIdentificationFeedbackRecord(database, deviceId, fileName) { if (!deviceId || !fileName) return null; const normalizedFileName = String(fileName); return database.identificationFeedback.find((item) => item.deviceId === deviceId && (item.fileName === normalizedFileName || item.runId === normalizedFileName) ) || null; } function resolveIdentificationRunId(database, record) { if (!record) return null; if (record.runId) return String(record.runId); const feedback = findIdentificationFeedbackRecord(database, record.deviceId, record.fileName); if (feedback) return String(feedback.runId); if (record.fileName) return String(record.fileName); return null; } function enrichIdentificationFile(database, record) { const runId = resolveIdentificationRunId(database, record); return runId ? { ...record, runId } : { ...record }; } function enrichNotification(database, notification) { if (!notification || notification.type !== "identification_result_ready" || !notification.fileID) { return notification; } const related = database.identificationFiles.find((item) => item.fileID === notification.fileID) || database.panelInbox.find((item) => item.fileID === notification.fileID); const runId = resolveIdentificationRunId(database, related); return runId ? { ...notification, runId } : notification; } 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 "deleteProductionLine": { const authError = requireAdmin(event); if (authError) return { success: false, errMsg: authError }; const companyId = String(event.companyId || "").trim(); const productionLineId = String(event.productionLineId || "").trim(); if (!companyId || !productionLineId) { return { success: false, errMsg: "缺少 companyId 或 productionLineId" }; } return store.update(async (database) => { const company = database.companies.find((item) => item.id === companyId); if (!company) return { success: false, errMsg: "公司不存在" }; return removeProductionLineData(database, { companyId, productionLineId, failOnActiveLicense: true }); }); } case "deleteCompany": { const authError = requireAdmin(event); if (authError) return { success: false, errMsg: authError }; const companyId = String(event.companyId || "").trim(); if (!companyId) return { success: false, errMsg: "缺少 companyId" }; return store.update(async (database) => { const company = database.companies.find((item) => item.id === companyId); if (!company) return { success: false, errMsg: "公司不存在" }; const activeLicenses = database.licenses.filter((item) => item.companyId === companyId && item.status === "active" ); if (activeLicenses.length) { return { success: false, errMsg: `请先撤销该公司的 ${activeLicenses.length} 个有效许可证后再删除公司`, errCode: "ACTIVE_LICENSES_PRESENT" }; } const lines = database.productionLines.filter((item) => item.companyId === companyId); const summary = { deletedProductionLines: 0, deletedFiles: 0, deletedDirectories: 0, deletedLicenses: 0, deletedFeedback: 0, deletedVolumeRequests: 0, deletedVolumeConfigs: 0, deletedNotifications: 0 }; for (const line of lines) { const lineDeleted = await removeProductionLineData(database, { companyId, productionLineId: line.id, failOnActiveLicense: false }); if (!lineDeleted.success) return lineDeleted; summary.deletedProductionLines += 1; summary.deletedFiles += lineDeleted.deletedFiles; summary.deletedDirectories += lineDeleted.deletedDirectories; summary.deletedLicenses += lineDeleted.deletedLicenses; summary.deletedFeedback += lineDeleted.deletedFeedback; summary.deletedVolumeRequests += lineDeleted.deletedVolumeRequests; summary.deletedVolumeConfigs += lineDeleted.deletedVolumeConfigs; summary.deletedNotifications += lineDeleted.deletedNotifications; } const orphanCompanyLicenses = database.licenses.filter((item) => item.companyId === companyId).length; if (orphanCompanyLicenses > 0) { summary.deletedLicenses += orphanCompanyLicenses; database.licenses = database.licenses.filter((item) => item.companyId !== companyId); } database.companies = database.companies.filter((item) => item.id !== companyId); return { success: true, deletedCompanyId: companyId, ...summary }; }); } 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); const licenseId = String(event.licenseId || "").trim(); if (authError) { const errCode = authError === "B端管理令牌无效" ? "ADMIN_TOKEN_INVALID" : "ADMIN_TOKEN_NOT_CONFIGURED"; const result = { success: false, errMsg: authError, errCode }; logRevokeAction({ event, licenseId, success: false, errCode: result.errCode }); return result; } if (!licenseId) { const result = { success: false, errMsg: "licenseId 不能为空", errCode: "LICENSE_ID_REQUIRED" }; logRevokeAction({ event, licenseId, success: false, errCode: result.errCode }); return result; } const result = await store.update((database) => { const record = database.licenses.find((item) => item.licenseId === licenseId); if (!record) return { success: false, errMsg: "许可证不存在", errCode: "LICENSE_NOT_FOUND" }; record.status = "revoked"; record.revokedAt = new Date().toISOString(); record.revocationReason = String(event.reason || "管理员撤销").trim(); return { success: true, license: publicLicense(record) }; }); logRevokeAction({ event, licenseId, success: result.success, errCode: result.errCode }); return result; } case "deleteLicense": { const authError = requireAdmin(event); if (authError) return { success: false, errMsg: authError }; const licenseId = String(event.licenseId || "").trim(); if (!licenseId) return { success: false, errMsg: "licenseId 不能为空", errCode: "LICENSE_ID_REQUIRED" }; return store.update((database) => { const record = database.licenses.find((item) => item.licenseId === licenseId); if (!record) return { success: false, errMsg: "许可证不存在", errCode: "LICENSE_NOT_FOUND" }; if (record.status === "active") { return { success: false, errMsg: "请先撤销许可证后再删除", errCode: "LICENSE_ACTIVE" }; } database.licenses = database.licenses.filter((item) => item.licenseId !== licenseId); return { success: true, deletedLicenseId: licenseId }; }); } case "validateLicense": { const database = await store.read(); const record = database.licenses.find((item) => item.licenseId === event.licenseId); const deviceId = event.deviceId ? normalizeDeviceId(event.deviceId) : null; return verifyLicenseRecord(record, deviceId); } 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: "文件不存在" }; const authError = requireAdmin(event); if (authError) return { success: false, errMsg: authError }; return { success: true, url: issueDownloadUrl(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; const fileIDs = [...new Set(records.map((record) => record.fileID))]; for (const fileID of fileIDs) deletedCount += await removeFile(database, fileID); return { success: true, deletedFileID: fileIDs.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 authError = requireAdmin(event); if (authError) return { success: false, errMsg: authError }; const deviceId = normalizeDeviceId(event.deviceId); const database = await store.read(); const config = database.volumeConfigs.find((item) => item.deviceId === deviceId); if (!config) return { success: true, found: false, deviceId }; const record = database.fileRecords.find((item) => item.fileID === config.fileID); if (!record) return { success: true, found: false, deviceId }; return { success: true, found: true, deviceId, fileID: record.fileID, fileName: record.fileName, url: issueDownloadUrl(req, record.fileID), updatedAtMs: config.updatedAtMs, requestId: config.requestId }; } 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: issueDownloadUrl(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 Boolean(findIdentificationFeedbackRecord(database, deviceId, item.fileName)); }); if (!message) return { success: true, pending: false }; const runId = resolveIdentificationRunId(database, message); return { success: true, pending: true, fileID: message.fileID, fileName: message.fileName, runId, mediaType: message.mediaType, uploadTime: message.uploadTime, url: issueDownloadUrl(req, message.fileID) }; } case "getPendingPanelNotification": { const authError = requireAdmin(event); if (authError) return { success: false, errMsg: authError }; const deviceId = normalizeDeviceId(event.deviceId); const database = await store.read(); const notification = database.panelNotifications.find((item) => item.deviceId === deviceId); return notification ? { success: true, pending: true, notification: enrichNotification(database, notification) } : { success: true, pending: false }; } case "ackPanelNotification": { const authError = requireAdmin(event); if (authError) return { success: false, errMsg: authError }; const deviceId = normalizeDeviceId(event.deviceId); const notificationId = String(event.notificationId || ""); if (!notificationId) return { success: false, errMsg: "缺少 notificationId" }; return store.update((database) => { const exactMatch = database.panelNotifications.find((item) => item.deviceId === deviceId && item.notificationId === notificationId ); if (exactMatch) { database.panelNotifications = database.panelNotifications.filter((item) => item !== exactMatch); return { success: true, notificationId, deleted: 1 }; } // 兼容:客户端 deviceId 切换或重连后,允许仅凭 notificationId 完成幂等关闭。 const fallbackMatch = database.panelNotifications.find((item) => item.notificationId === notificationId); if (fallbackMatch) { database.panelNotifications = database.panelNotifications.filter((item) => item !== fallbackMatch); return { success: true, notificationId, deleted: 1, idempotent: true, reassignedDeviceId: fallbackMatch.deviceId }; } return { success: true, notificationId, deleted: 0, idempotent: true }; }); } 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).map((record) => enrichIdentificationFile(database, record)), 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: issueDownloadUrl(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: "辨识文件不存在" }; } const runId = resolveIdentificationRunId(database, historyRecord); return { success: true, fileID: fileRecord.fileID, fileName: fileRecord.fileName, runId, mediaType: historyRecord.mediaType, url: issueDownloadUrl(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); addPanelNotification(database, { deviceId, type: "volume_request_started", title: "产线请求容积测试配置", message: "请完成容积测试配置并提交。", requestId: record.requestId }); 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(); database.volumeConfigs = database.volumeConfigs.filter((item) => item.deviceId !== deviceId); database.volumeConfigs.push({ deviceId, fileID: fileRecord.fileID, fileName: fileRecord.fileName, requestId: record.requestId, updatedAtMs: record.uploadedAtMs }); 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: issueDownloadUrl(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) { const isLatest = database.volumeConfigs.some((item) => item.fileID === record.configFileID); if (record.configFileID && !isLatest) 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 字段" }; } } async function dispatchDevice(event, req) { if (event.type === "deviceAuth") { const licenseId = String(event.licenseId || "").trim(); if (!licenseId) return { success: false, errCode: "LICENSE_ID_REQUIRED", errMsg: "缺少 licenseId" }; const deviceId = normalizeDeviceId(event.deviceId); const database = await store.read(); const record = database.licenses.find((item) => item.licenseId === licenseId); const validation = verifyLicenseRecord(record, deviceId); if (!validation.valid) { const errCodeMap = { not_found: "LICENSE_NOT_FOUND", device_mismatch: "DEVICE_ID_MISMATCH", expired: "LICENSE_EXPIRED", revoked: "LICENSE_REVOKED" }; return { success: false, errCode: errCodeMap[validation.status] || "LICENSE_INVALID", errMsg: `许可证不可用: ${validation.status}` }; } const issued = issueDeviceToken({ deviceId, licenseId }); return { success: true, deviceId, licenseId, deviceToken: issued.token, expiresAtMs: issued.expiresAtMs }; } const context = consumeDeviceContext(event); if (!context.ok) return { success: false, errCode: context.errCode, errMsg: context.errMsg }; const deviceId = context.deviceId; switch (event.type) { case "deviceHeartbeat": case "registerIdentificationResult": case "getIdentificationFeedback": case "ackIdentificationFeedback": case "createVolumeConfigRequest": case "getPendingVolumeConfigRequest": case "submitVolumeConfigFile": case "getVolumeConfigRequest": case "ackVolumeConfigRequest": case "getIdentificationConfig": return dispatch({ ...event, deviceId }, req); case "uploadDataFile": { const folder = normalizeRelativePath(event.folder, "data_record"); const devicePrefix = `${deviceId}/`; if (!folder.startsWith(devicePrefix)) { return { success: false, errMsg: "设备接口仅允许访问当前 deviceId 目录" }; } if (folder.endsWith("/model_config")) { return { success: false, errMsg: "设备接口不允许上传模型" }; } return dispatch({ ...event, folder }, req); } case "listModels": { const folder = `${deviceId}/model_config`; return dispatch({ ...event, folder }, req); } case "downloadModel": { if (!event.fileID) return { success: false, errMsg: "缺少 fileID" }; 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.folder !== `${deviceId}/model_config`) { return { success: false, errMsg: "设备接口无权下载该模型" }; } return { success: true, url: issueDownloadUrl(req, record.fileID), fileName: record.fileName, originalFileName: record.originalFileName || record.fileName }; } 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 }); addPanelNotification(database, { deviceId, type: "identification_result_ready", title: "系统辨识结果已返回", message: "新的系统辨识 CSV 或 JSON 数据已进入暂存。", fileID: pending.fileID }); } if (folderParts.at(-1) === "V_config" && extension === ".json") { const deviceId = normalizeDeviceId(folderParts.slice(0, -1).join("/")); addPanelNotification(database, { deviceId, type: "volume_result_ready", title: "容积测试结果已返回", message: "容积测试结果 JSON 已上传,可打开查看。", fileID: pending.fileID }); } 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("/downloads/:ticket", async (req, res, next) => { try { const ticket = String(req.params.ticket || "").trim(); if (!ticket) return res.status(403).json({ success: false, errMsg: "下载链接无效或已失效" }); const consumed = consumeDownloadTicket(req, ticket); if (!consumed.ok) return res.status(403).json({ success: false, errMsg: consumed.errMsg }); const fileID = consumed.fileID; 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); } }); app.get("/files/:fileID", (req, res) => { res.status(403).json({ success: false, errMsg: "旧下载链接已停用,请先通过 API 获取临时下载链接" }); }); const apiHandler = async (req, res) => { try { res.json(await dispatch(normalizeEventCompat(req.body || {}), req)); } catch (error) { res.status(400).json({ success: false, errMsg: error.message }); } }; app.post("/", apiHandler); const deviceApiHandler = async (req, res) => { try { res.json(await dispatchDevice(normalizeEventCompat(req.body || {}), req)); } catch (error) { res.status(400).json({ success: false, errMsg: error.message }); } }; app.post("/device", deviceApiHandler); app.use((error, req, res, next) => { console.error(error); res.status(500).json({ success: false, errMsg: "服务器内部错误" }); }); return app; } module.exports = { createApp, validateConfigParameters };