Merge remote-tracking branch 'upstream/main' into reinlooptest
This commit is contained in:
@@ -86,6 +86,7 @@ function startPanelInboxPoller(window) {
|
|||||||
filePath: imagePath,
|
filePath: imagePath,
|
||||||
dataUrl: toDataUrl(imagePath),
|
dataUrl: toDataUrl(imagePath),
|
||||||
fileName: pending.fileName,
|
fileName: pending.fileName,
|
||||||
|
runId: pending.runId || pending.fileName,
|
||||||
mediaType: pending.mediaType,
|
mediaType: pending.mediaType,
|
||||||
uploadTime: pending.uploadTime,
|
uploadTime: pending.uploadTime,
|
||||||
deviceId: requestContext.deviceId,
|
deviceId: requestContext.deviceId,
|
||||||
@@ -95,7 +96,7 @@ function startPanelInboxPoller(window) {
|
|||||||
pendingReview = {
|
pendingReview = {
|
||||||
deviceId: requestContext.deviceId,
|
deviceId: requestContext.deviceId,
|
||||||
fileID: pending.fileID,
|
fileID: pending.fileID,
|
||||||
runId: pending.fileName,
|
runId: pending.runId || pending.fileName,
|
||||||
credentials: requestContext
|
credentials: requestContext
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
@@ -378,6 +379,7 @@ function registerHandlers() {
|
|||||||
filePath: imagePath,
|
filePath: imagePath,
|
||||||
dataUrl: toDataUrl(imagePath),
|
dataUrl: toDataUrl(imagePath),
|
||||||
fileName: download.fileName || request.fileName,
|
fileName: download.fileName || request.fileName,
|
||||||
|
runId: download.runId || request.runId || request.fileName,
|
||||||
mediaType,
|
mediaType,
|
||||||
uploadTime: download.uploadTime || request.uploadTime,
|
uploadTime: download.uploadTime || request.uploadTime,
|
||||||
deviceId: request.deviceId
|
deviceId: request.deviceId
|
||||||
|
|||||||
@@ -111,7 +111,7 @@
|
|||||||
<button id="refresh-identification-files" class="button secondary">刷新</button>
|
<button id="refresh-identification-files" class="button secondary">刷新</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="data-surface">
|
<div class="data-surface">
|
||||||
<table><thead><tr><th>上传时间</th><th>文件名</th><th>类型</th><th>大小</th><th>状态</th><th>操作</th></tr></thead><tbody id="identification-file-list"></tbody></table>
|
<table><thead><tr><th>上传时间</th><th>文件名</th><th>runId</th><th>类型</th><th>大小</th><th>状态</th><th>操作</th></tr></thead><tbody id="identification-file-list"></tbody></table>
|
||||||
<p id="identification-file-empty" class="empty-row">选择公司和产线后刷新辨识数据</p>
|
<p id="identification-file-empty" class="empty-row">选择公司和产线后刷新辨识数据</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -305,6 +305,7 @@ async function refreshIdentificationFiles() {
|
|||||||
state.identificationFiles = result.files || result.fileList || [];
|
state.identificationFiles = result.files || result.fileList || [];
|
||||||
elements.identificationFileList.innerHTML = state.identificationFiles.map((file) => `
|
elements.identificationFileList.innerHTML = state.identificationFiles.map((file) => `
|
||||||
<tr><td>${escapeHtml(file.uploadTime || "-")}</td><td>${escapeHtml(file.fileName)}</td>
|
<tr><td>${escapeHtml(file.uploadTime || "-")}</td><td>${escapeHtml(file.fileName)}</td>
|
||||||
|
<td>${escapeHtml(file.runId || "-")}</td>
|
||||||
<td>${file.mediaType === "json" ? "行程 JSON" : "辨识 CSV"}</td><td>${formatSize(file.size)}</td>
|
<td>${file.mediaType === "json" ? "行程 JSON" : "辨识 CSV"}</td><td>${formatSize(file.size)}</td>
|
||||||
<td>${file.status === "processed" ? "已处理" : "待处理"}</td>
|
<td>${file.status === "processed" ? "已处理" : "待处理"}</td>
|
||||||
<td><button class="table-action" data-identification-preview="${escapeHtml(file.fileID)}">查看</button><button class="table-action" data-identification-download="${escapeHtml(file.fileID)}">下载</button><button class="table-action danger" data-identification-delete="${escapeHtml(file.fileID)}">删除</button></td></tr>
|
<td><button class="table-action" data-identification-preview="${escapeHtml(file.fileID)}">查看</button><button class="table-action" data-identification-download="${escapeHtml(file.fileID)}">下载</button><button class="table-action danger" data-identification-delete="${escapeHtml(file.fileID)}">删除</button></td></tr>
|
||||||
@@ -457,8 +458,9 @@ function showImage(result) {
|
|||||||
plot.showImage.disabled = false;
|
plot.showImage.disabled = false;
|
||||||
plot.openImage.disabled = false;
|
plot.openImage.disabled = false;
|
||||||
if (mediaType === "csv" && result.reviewable) {
|
if (mediaType === "csv" && result.reviewable) {
|
||||||
state.review = { runId: result.fileName, deviceId: result.deviceId };
|
const runId = result.runId || result.fileName;
|
||||||
elements.reviewTarget.textContent = result.fileName;
|
state.review = { runId, deviceId: result.deviceId };
|
||||||
|
elements.reviewTarget.textContent = `${result.fileName} (runId: ${runId})`;
|
||||||
elements.approveReview.disabled = false;
|
elements.approveReview.disabled = false;
|
||||||
elements.rejectReview.disabled = false;
|
elements.rejectReview.disabled = false;
|
||||||
}
|
}
|
||||||
@@ -954,6 +956,12 @@ elements.identificationFileList.addEventListener("click", async (event) => {
|
|||||||
if (result) {
|
if (result) {
|
||||||
showImage(result);
|
showImage(result);
|
||||||
activateTab("plot-panel");
|
activateTab("plot-panel");
|
||||||
|
if ((file.mediaType !== "json") && file.runId) {
|
||||||
|
state.review = { runId: file.runId, deviceId: elements.deviceId.value };
|
||||||
|
elements.reviewTarget.textContent = `${result.fileName} (runId: ${file.runId})`;
|
||||||
|
elements.approveReview.disabled = false;
|
||||||
|
elements.rejectReview.disabled = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (event.target.dataset.identificationDownload) {
|
} else if (event.target.dataset.identificationDownload) {
|
||||||
const result = await runBusy("正在保存辨识原始数据", () => window.reinloop.downloadIdentificationFile({
|
const result = await runBusy("正在保存辨识原始数据", () => window.reinloop.downloadIdentificationFile({
|
||||||
@@ -1146,7 +1154,12 @@ function renderNotifications() {
|
|||||||
? "查看结果"
|
? "查看结果"
|
||||||
: "查看绘图";
|
: "查看绘图";
|
||||||
const secondaryAction = notification.type === "identification_result_ready" ? "查看辨识数据" : "";
|
const secondaryAction = notification.type === "identification_result_ready" ? "查看辨识数据" : "";
|
||||||
return `<article class="panel-notification" data-notification-id="${escapeHtml(notification.notificationId)}" data-type="${escapeHtml(notification.type)}"><div class="notification-copy"><strong>${escapeHtml(notification.title)}</strong><span>${escapeHtml(notification.message)}</span></div><div class="notification-actions"><button class="button secondary" data-notification-action="primary" data-notification-id="${escapeHtml(notification.notificationId)}">${action}</button>${secondaryAction ? `<button class="button secondary" data-notification-action="secondary" data-notification-id="${escapeHtml(notification.notificationId)}">${secondaryAction}</button>` : ""}<button class="button icon" title="关闭提醒" aria-label="关闭提醒" data-notification-action="close" data-notification-id="${escapeHtml(notification.notificationId)}">x</button></div></article>`;
|
const traces = [
|
||||||
|
notification.runId ? `runId: ${notification.runId}` : "",
|
||||||
|
notification.fileID ? `fileID: ${notification.fileID}` : "",
|
||||||
|
notification.requestId ? `requestId: ${notification.requestId}` : ""
|
||||||
|
].filter(Boolean).join(" | ");
|
||||||
|
return `<article class="panel-notification" data-notification-id="${escapeHtml(notification.notificationId)}" data-type="${escapeHtml(notification.type)}"><div class="notification-copy"><strong>${escapeHtml(notification.title)}</strong><span>${escapeHtml(notification.message)}</span>${traces ? `<small class="notification-trace">${escapeHtml(traces)}</small>` : ""}</div><div class="notification-actions"><button class="button secondary" data-notification-action="primary" data-notification-id="${escapeHtml(notification.notificationId)}">${action}</button>${secondaryAction ? `<button class="button secondary" data-notification-action="secondary" data-notification-id="${escapeHtml(notification.notificationId)}">${secondaryAction}</button>` : ""}<button class="button icon" title="关闭提醒" aria-label="关闭提醒" data-notification-action="close" data-notification-id="${escapeHtml(notification.notificationId)}">x</button></div></article>`;
|
||||||
}).join("");
|
}).join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -197,6 +197,14 @@ input:focus, select:focus, textarea:focus { border-color: var(--green); box-shad
|
|||||||
.notification-copy { display: grid; gap: 3px; min-width: 0; }
|
.notification-copy { display: grid; gap: 3px; min-width: 0; }
|
||||||
.notification-copy strong { font-size: 13px; }
|
.notification-copy strong { font-size: 13px; }
|
||||||
.notification-copy span { color: var(--muted); font-size: 12px; overflow-wrap: anywhere; }
|
.notification-copy span { color: var(--muted); font-size: 12px; overflow-wrap: anywhere; }
|
||||||
|
.notification-copy .notification-trace {
|
||||||
|
display: block;
|
||||||
|
margin-top: 4px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
.notification-actions { display: flex; gap: 8px; }
|
.notification-actions { display: flex; gap: 8px; }
|
||||||
.editor-layout { display: grid; grid-template-columns: minmax(0, 1fr) 270px; gap: 18px; }
|
.editor-layout { display: grid; grid-template-columns: minmax(0, 1fr) 270px; gap: 18px; }
|
||||||
.editor-column, .publish-aside { background: var(--surface); border: 1px solid var(--line); }
|
.editor-column, .publish-aside { background: var(--surface); border: 1px solid var(--line); }
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
function parseArgs(argv) {
|
||||||
|
const result = {};
|
||||||
|
for (let index = 2; index < argv.length; index += 1) {
|
||||||
|
const arg = argv[index];
|
||||||
|
if (!arg.startsWith("--")) continue;
|
||||||
|
const [key, ...rest] = arg.slice(2).split("=");
|
||||||
|
if (!key) continue;
|
||||||
|
const value = rest.length ? rest.join("=") : "";
|
||||||
|
result[key] = value;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv);
|
||||||
|
const BASE_URL = (args.baseUrl || process.env.BASE_URL || "http://127.0.0.1:3000").replace(/\/$/, "");
|
||||||
|
const ADMIN_TOKEN = args.adminToken || process.env.ADMIN_TOKEN || "";
|
||||||
|
const DEVICE_A = args.deviceA || process.env.DEVICE_A || "develop-test2/test1";
|
||||||
|
const DEVICE_B = args.deviceB || process.env.DEVICE_B || "develop-test/test1";
|
||||||
|
|
||||||
|
if (!ADMIN_TOKEN) {
|
||||||
|
console.error("[ERROR] 缺少 ADMIN_TOKEN 环境变量。");
|
||||||
|
console.error("示例1: node scripts/test-cross-device-notification.js --adminToken=your-token");
|
||||||
|
console.error("示例2: node scripts/test-cross-device-notification.js --adminToken=your-token --baseUrl=http://127.0.0.1:3000 --deviceA=develop-test2/test1 --deviceB=develop-test/test1");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function postJson(path, body) {
|
||||||
|
const response = await fetch(`${BASE_URL}${path}`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
});
|
||||||
|
|
||||||
|
let data;
|
||||||
|
try {
|
||||||
|
data = await response.json();
|
||||||
|
} catch (_error) {
|
||||||
|
data = { success: false, errMsg: "响应不是 JSON" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { status: response.status, data };
|
||||||
|
}
|
||||||
|
|
||||||
|
function assert(condition, message, details) {
|
||||||
|
if (condition) return;
|
||||||
|
const error = new Error(message);
|
||||||
|
error.details = details;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log(`[INFO] BASE_URL=${BASE_URL}`);
|
||||||
|
console.log(`[INFO] DEVICE_A=${DEVICE_A}`);
|
||||||
|
console.log(`[INFO] DEVICE_B=${DEVICE_B}`);
|
||||||
|
|
||||||
|
console.log("\n[1/5] 为 DEVICE_A 生成一条通知(createVolumeConfigRequest)...");
|
||||||
|
const createReq = await postJson("/", {
|
||||||
|
type: "createVolumeConfigRequest",
|
||||||
|
deviceId: DEVICE_A
|
||||||
|
});
|
||||||
|
assert(createReq.data.success === true, "createVolumeConfigRequest 失败", createReq);
|
||||||
|
|
||||||
|
console.log("[2/5] 读取 DEVICE_A 的 pending notification...");
|
||||||
|
const pendingA = await postJson("/", {
|
||||||
|
type: "getPendingPanelNotification",
|
||||||
|
adminToken: ADMIN_TOKEN,
|
||||||
|
deviceId: DEVICE_A
|
||||||
|
});
|
||||||
|
assert(pendingA.data.success === true, "getPendingPanelNotification(DEVICE_A) 调用失败", pendingA);
|
||||||
|
assert(pendingA.data.pending === true, "DEVICE_A 没有待处理通知", pendingA);
|
||||||
|
const notificationId = pendingA.data.notification?.notificationId;
|
||||||
|
assert(Boolean(notificationId), "未拿到 notificationId", pendingA);
|
||||||
|
console.log(`[INFO] notificationId=${notificationId}`);
|
||||||
|
|
||||||
|
console.log("[3/5] 使用 DEVICE_B 尝试确认 DEVICE_A 的 notificationId(预期失败)...");
|
||||||
|
const crossAck = await postJson("/", {
|
||||||
|
type: "ackPanelNotification",
|
||||||
|
adminToken: ADMIN_TOKEN,
|
||||||
|
deviceId: DEVICE_B,
|
||||||
|
notificationId
|
||||||
|
});
|
||||||
|
assert(crossAck.data.success === false, "跨 deviceId 确认意外成功(存在越权风险)", crossAck);
|
||||||
|
assert(crossAck.data.errMsg === "Panel 通知不存在", "跨 deviceId 失败文案非预期", crossAck);
|
||||||
|
|
||||||
|
console.log("[4/5] 使用 DEVICE_A 正常确认同一 notificationId(预期成功)...");
|
||||||
|
const ownerAck = await postJson("/", {
|
||||||
|
type: "ackPanelNotification",
|
||||||
|
adminToken: ADMIN_TOKEN,
|
||||||
|
deviceId: DEVICE_A,
|
||||||
|
notificationId
|
||||||
|
});
|
||||||
|
assert(ownerAck.data.success === true, "DEVICE_A 确认自身通知失败", ownerAck);
|
||||||
|
|
||||||
|
console.log("[5/5] 再次读取 DEVICE_A pending,确认已被消费...");
|
||||||
|
const pendingAfter = await postJson("/", {
|
||||||
|
type: "getPendingPanelNotification",
|
||||||
|
adminToken: ADMIN_TOKEN,
|
||||||
|
deviceId: DEVICE_A
|
||||||
|
});
|
||||||
|
assert(pendingAfter.data.success === true, "二次查询 pending 失败", pendingAfter);
|
||||||
|
|
||||||
|
console.log("\n[PASS] 测试通过:notificationId 不能被其他 deviceId 确认。\n");
|
||||||
|
console.log("关键结果:");
|
||||||
|
console.log(`- crossAck.success = ${crossAck.data.success}`);
|
||||||
|
console.log(`- crossAck.errMsg = ${crossAck.data.errMsg}`);
|
||||||
|
console.log(`- ownerAck.success = ${ownerAck.data.success}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error("\n[FAIL]", error.message);
|
||||||
|
if (error.details) {
|
||||||
|
console.error("details=");
|
||||||
|
console.error(JSON.stringify(error.details, null, 2));
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
+59
-12
@@ -496,6 +496,39 @@ function createApp({
|
|||||||
return 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() {
|
async function purgeExpiredIdentificationFiles() {
|
||||||
return store.update(async (database) => {
|
return store.update(async (database) => {
|
||||||
backfillIdentificationFiles(database);
|
backfillIdentificationFiles(database);
|
||||||
@@ -901,16 +934,16 @@ function createApp({
|
|||||||
const message = database.panelInbox.find((item) => {
|
const message = database.panelInbox.find((item) => {
|
||||||
if (item.deviceId !== deviceId) return false;
|
if (item.deviceId !== deviceId) return false;
|
||||||
if (item.mediaType !== "csv") return true;
|
if (item.mediaType !== "csv") return true;
|
||||||
return database.identificationFeedback.some((feedback) =>
|
return Boolean(findIdentificationFeedbackRecord(database, deviceId, item.fileName));
|
||||||
feedback.deviceId === deviceId && feedback.runId === item.fileName
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
if (!message) return { success: true, pending: false };
|
if (!message) return { success: true, pending: false };
|
||||||
|
const runId = resolveIdentificationRunId(database, message);
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
pending: true,
|
pending: true,
|
||||||
fileID: message.fileID,
|
fileID: message.fileID,
|
||||||
fileName: message.fileName,
|
fileName: message.fileName,
|
||||||
|
runId,
|
||||||
mediaType: message.mediaType,
|
mediaType: message.mediaType,
|
||||||
uploadTime: message.uploadTime,
|
uploadTime: message.uploadTime,
|
||||||
url: issueDownloadUrl(req, message.fileID)
|
url: issueDownloadUrl(req, message.fileID)
|
||||||
@@ -923,7 +956,7 @@ function createApp({
|
|||||||
const database = await store.read();
|
const database = await store.read();
|
||||||
const notification = database.panelNotifications.find((item) => item.deviceId === deviceId);
|
const notification = database.panelNotifications.find((item) => item.deviceId === deviceId);
|
||||||
return notification
|
return notification
|
||||||
? { success: true, pending: true, notification }
|
? { success: true, pending: true, notification: enrichNotification(database, notification) }
|
||||||
: { success: true, pending: false };
|
: { success: true, pending: false };
|
||||||
}
|
}
|
||||||
case "ackPanelNotification": {
|
case "ackPanelNotification": {
|
||||||
@@ -933,16 +966,28 @@ function createApp({
|
|||||||
const notificationId = String(event.notificationId || "");
|
const notificationId = String(event.notificationId || "");
|
||||||
if (!notificationId) return { success: false, errMsg: "缺少 notificationId" };
|
if (!notificationId) return { success: false, errMsg: "缺少 notificationId" };
|
||||||
return store.update((database) => {
|
return store.update((database) => {
|
||||||
const exists = database.panelNotifications.some((item) =>
|
const exactMatch = database.panelNotifications.find((item) =>
|
||||||
item.deviceId === deviceId && item.notificationId === notificationId
|
item.deviceId === deviceId && item.notificationId === notificationId
|
||||||
);
|
);
|
||||||
if (!exists) {
|
if (exactMatch) {
|
||||||
return { success: true, notificationId, deleted: 0, idempotent: true };
|
database.panelNotifications = database.panelNotifications.filter((item) => item !== exactMatch);
|
||||||
|
return { success: true, notificationId, deleted: 1 };
|
||||||
}
|
}
|
||||||
database.panelNotifications = database.panelNotifications.filter((item) =>
|
|
||||||
!(item.deviceId === deviceId && item.notificationId === notificationId)
|
// 兼容:客户端 deviceId 切换或重连后,允许仅凭 notificationId 完成幂等关闭。
|
||||||
);
|
const fallbackMatch = database.panelNotifications.find((item) => item.notificationId === notificationId);
|
||||||
return { success: true, notificationId, deleted: 1 };
|
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": {
|
case "ackPanelFile": {
|
||||||
@@ -988,7 +1033,7 @@ function createApp({
|
|||||||
const offset = (page - 1) * pageSize;
|
const offset = (page - 1) * pageSize;
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
files: files.slice(offset, offset + pageSize),
|
files: files.slice(offset, offset + pageSize).map((record) => enrichIdentificationFile(database, record)),
|
||||||
total: files.length,
|
total: files.length,
|
||||||
page,
|
page,
|
||||||
pageSize
|
pageSize
|
||||||
@@ -1065,10 +1110,12 @@ function createApp({
|
|||||||
if (!historyRecord || !fileRecord || !store.resolveStoredFile(fileRecord.fileID)) {
|
if (!historyRecord || !fileRecord || !store.resolveStoredFile(fileRecord.fileID)) {
|
||||||
return { success: false, errMsg: "辨识文件不存在" };
|
return { success: false, errMsg: "辨识文件不存在" };
|
||||||
}
|
}
|
||||||
|
const runId = resolveIdentificationRunId(database, historyRecord);
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
fileID: fileRecord.fileID,
|
fileID: fileRecord.fileID,
|
||||||
fileName: fileRecord.fileName,
|
fileName: fileRecord.fileName,
|
||||||
|
runId,
|
||||||
mediaType: historyRecord.mediaType,
|
mediaType: historyRecord.mediaType,
|
||||||
url: issueDownloadUrl(req, fileRecord.fileID)
|
url: issueDownloadUrl(req, fileRecord.fileID)
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -294,6 +294,7 @@ test("panel consumes uploaded device files from an inbox without scanning folder
|
|||||||
});
|
});
|
||||||
assert.equal(pending.pending, true);
|
assert.equal(pending.pending, true);
|
||||||
assert.equal(pending.fileName, "result_20260724_120000.csv");
|
assert.equal(pending.fileName, "result_20260724_120000.csv");
|
||||||
|
assert.equal(pending.runId, "result_20260724_120000.csv");
|
||||||
assert.equal(await (await fetch(pending.url)).text(), "t,u,p\n0,10,20\n");
|
assert.equal(await (await fetch(pending.url)).text(), "t,u,p\n0,10,20\n");
|
||||||
|
|
||||||
assert.equal((await post({
|
assert.equal((await post({
|
||||||
@@ -328,6 +329,7 @@ test("panel consumes uploaded device files from an inbox without scanning folder
|
|||||||
});
|
});
|
||||||
assert.equal(history.total, 2);
|
assert.equal(history.total, 2);
|
||||||
const csvHistory = history.files.find((file) => file.fileID === pending.fileID);
|
const csvHistory = history.files.find((file) => file.fileID === pending.fileID);
|
||||||
|
assert.equal(csvHistory.runId, "result_20260724_120000.csv");
|
||||||
assert.equal(csvHistory.status, "processed");
|
assert.equal(csvHistory.status, "processed");
|
||||||
assert.ok(csvHistory.processedAt);
|
assert.ok(csvHistory.processedAt);
|
||||||
assert.ok(csvHistory.expiresAt);
|
assert.ok(csvHistory.expiresAt);
|
||||||
@@ -553,6 +555,9 @@ test("panel notifications and volume configuration stay isolated by device", asy
|
|||||||
assert.equal(duplicatedAck.success, true);
|
assert.equal(duplicatedAck.success, true);
|
||||||
assert.equal(duplicatedAck.idempotent, true);
|
assert.equal(duplicatedAck.idempotent, true);
|
||||||
assert.equal(duplicatedAck.deleted, 0);
|
assert.equal(duplicatedAck.deleted, 0);
|
||||||
|
assert.equal((await post({
|
||||||
|
type: "getPendingPanelNotification", deviceId, adminToken: "test-token"
|
||||||
|
})).pending, false);
|
||||||
|
|
||||||
async function uploadResult(folderName, fileName, content) {
|
async function uploadResult(folderName, fileName, content) {
|
||||||
const result = await post({ type: "uploadDataFile", fileName, folder: `${deviceId}/${folderName}` });
|
const result = await post({ type: "uploadDataFile", fileName, folder: `${deviceId}/${folderName}` });
|
||||||
@@ -566,6 +571,15 @@ test("panel notifications and volume configuration stay isolated by device", asy
|
|||||||
});
|
});
|
||||||
assert.equal(volumeResult.notification.type, "volume_result_ready");
|
assert.equal(volumeResult.notification.type, "volume_result_ready");
|
||||||
assert.ok(volumeResult.notification.fileID);
|
assert.ok(volumeResult.notification.fileID);
|
||||||
|
const fallbackAck = await post({
|
||||||
|
type: "ackPanelNotification",
|
||||||
|
deviceId: otherDeviceId,
|
||||||
|
notificationId: volumeResult.notification.notificationId,
|
||||||
|
adminToken: "test-token"
|
||||||
|
});
|
||||||
|
assert.equal(fallbackAck.success, true);
|
||||||
|
assert.equal(fallbackAck.idempotent, true);
|
||||||
|
assert.equal(fallbackAck.deleted, 1);
|
||||||
await post({
|
await post({
|
||||||
type: "ackPanelNotification", deviceId, notificationId: volumeResult.notification.notificationId,
|
type: "ackPanelNotification", deviceId, notificationId: volumeResult.notification.notificationId,
|
||||||
adminToken: "test-token"
|
adminToken: "test-token"
|
||||||
@@ -576,6 +590,7 @@ test("panel notifications and volume configuration stay isolated by device", asy
|
|||||||
type: "getPendingPanelNotification", deviceId, adminToken: "test-token"
|
type: "getPendingPanelNotification", deviceId, adminToken: "test-token"
|
||||||
});
|
});
|
||||||
assert.equal(identificationResult.notification.type, "identification_result_ready");
|
assert.equal(identificationResult.notification.type, "identification_result_ready");
|
||||||
|
assert.equal(identificationResult.notification.runId, "identification_result.json");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("admin manages companies and production lines with a stable device id", async () => {
|
test("admin manages companies and production lines with a stable device id", async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user