CAN桥接离线
座椅ECU未检测
@@ -128,22 +173,15 @@
-
- PRODUCTION CASES
量产故障用例
0 / 6
-
- 等待启动批量验证
-
-
@@ -153,9 +191,31 @@
影子动作仿真等待启动座椅任务待机
实际动作将同步显示在座椅模型
-
- HEALING FLOW
座椅任务自修复
0.000 s
-
+
+ FAULT INJECTION
量产故障注入
6 个用例
+ 先启动座椅持续运行,再按需选择故障。注入事件会明确标记为管理员操作。
+
+
+
+
+
+
+
本次连续运行00:00:00
+
累计健康运行00:00:00
+
故障 / 已修复0 / 0
+
+
+
@@ -240,36 +300,92 @@
|| latest.scenarios[0];
}
- function timelineDetail(index, scenario, healing) {
- const telemetry = healing.telemetry || {};
- const details = [
- scenario.normal_task,
- telemetry.bench_reaction || scenario.fault_reaction,
- scenario.diagnosis,
- scenario.repair,
- scenario.recovery_reaction,
- ];
- const phases = TIMELINE[index].phases;
- const event = [...(healing.events || [])].reverse()
- .find((item) => phases.includes(item.phase));
- return event?.detail || event?.title || details[index] || "等待运行日志";
+ function formatDuration(seconds) {
+ const whole = Math.max(0, Math.floor(Number(seconds || 0)));
+ const hours = Math.floor(whole / 3600);
+ const minutes = Math.floor((whole % 3600) / 60);
+ const remainder = whole % 60;
+ return [hours, minutes, remainder]
+ .map((value) => String(value).padStart(2, "0"))
+ .join(":");
}
- function renderTimeline() {
- const healing = latest.healing;
- const scenario = scenarioForDisplay();
- if (!scenario) return;
- const phaseIndex = PHASE_INDEX[healing.phase] ?? -1;
- const passed = healing.phase === "PASSED";
+ function formatEventTime(epoch) {
+ if (!epoch) return "--:--:--.---";
+ const value = new Date(Number(epoch) * 1000);
+ const base = [value.getHours(), value.getMinutes(), value.getSeconds()]
+ .map((item) => String(item).padStart(2, "0"))
+ .join(":");
+ return `${base}.${String(value.getMilliseconds()).padStart(3, "0")}`;
+ }
+
+ function runtimeState() {
+ return latest.runtime || fallbackState().runtime;
+ }
+
+ function renderEventStream() {
+ const runtime = runtimeState();
+ const healing = latest.healing || {};
+ const scenario = (latest.scenarios || []).find((item) => item.id === healing.case_id)
+ || scenarioForDisplay();
+ const runtimeEvents = (runtime.events || []).map((event) => ({
+ ...event,
+ kind: event.kind || "system",
+ label: event.label || "运行",
+ }));
+ const healingEvents = (healing.events || []).map((event) => {
+ const meta = EVENT_META[event.phase] || EVENT_META.BASELINE;
+ const injected = event.phase === "INJECTED";
+ return {
+ ...event,
+ kind: meta.kind,
+ label: meta.label,
+ title: injected
+ ? `管理员注入「${scenario?.short_title || "故障用例"}」`
+ : event.title,
+ tags: [
+ injected ? "来源:admin" : null,
+ healing.run_id ? `事件:${healing.run_id}` : null,
+ healing.diagnosed_code && ["DIAGNOSED", "REPAIRING", "VERIFYING", "PASSED", "FAILED"].includes(event.phase)
+ ? `诊断码:${healing.diagnosed_code}` : null,
+ ].filter(Boolean),
+ };
+ });
+ const activeRepair = ["INJECTED", "DIAGNOSED", "REPAIRING", "VERIFYING"].includes(healing.phase);
+ const twin = latest.bench_twin || {};
+ const naturalFault = runtime.active && !activeRepair && ["IDLE", "PASSED"].includes(healing.phase)
+ && Number(twin.system?.faulted_mask || 0) !== 0;
+ const liveEvents = runtime.active && !activeRepair ? [{
+ epoch: latest.generated_epoch,
+ time: formatEventTime(latest.generated_epoch),
+ kind: naturalFault ? "fault" : "heartbeat",
+ label: naturalFault ? "自然故障" : "运行心跳",
+ title: naturalFault
+ ? "运行中检测到座椅异常"
+ : `座椅已正常运行 ${formatDuration(runtime.healthy_elapsed_s)}`,
+ detail: naturalFault
+ ? `ECU FaultedMask=0x${Number(twin.system.faulted_mask).toString(16).toUpperCase()},AI正在持续监测。`
+ : `${runtime.motion_profile} · 已完成 ${runtime.cycle_count || 0} 个动作周期。`,
+ tags: (twin.axes?.length ? twin.axes : [35, 42, 48, 20].map((position_pct, motor) => ({ motor, position_pct })))
+ .map((axis) => `M${axis.motor} ${Number(axis.position_pct || 0).toFixed(0)}%`),
+ }] : [];
+ const events = [...runtimeEvents, ...healingEvents, ...liveEvents]
+ .filter((event) => eventFilter === "all" || ["fault", "diagnosis", "repair", "success"].includes(event.kind))
+ .sort((left, right) => Number(left.epoch || 0) - Number(right.epoch || 0));
+
+ $("#repairEventStream").innerHTML = events.length
+ ? events.map((event) => `
${esc(event.label)}${esc(event.title)}${esc(event.detail || "状态已更新")}
${event.tags?.length ? `
` : ""}
`).join("")
+ : `
${runtime.active ? "座椅正在正常运行" : "等待启动座椅运行"}${runtime.active ? "运行心跳、故障、诊断和修复事件将在这里持续追加。" : "启动后将记录运行时长、四轴状态和后续故障注入。"}
`;
+ $("#repairEventStream").scrollTop = $("#repairEventStream").scrollHeight;
+
+ $("#repairRuntimeElapsed").textContent = formatDuration(runtime.elapsed_s);
+ $("#repairHealthyElapsed").textContent = formatDuration(runtime.healthy_elapsed_s);
+ $("#repairFaultSummary").textContent = `${Number(runtime.injected_count || 0)} / ${Number(runtime.repaired_count || 0)}`;
const failed = healing.phase === "FAILED";
- $("#repairTimeline").innerHTML = TIMELINE.map((stage, index) => {
- let state = "pending";
- if (passed || phaseIndex > index) state = "complete";
- else if (phaseIndex === index && healing.phase !== "IDLE") state = failed ? "failed" : "active";
- const label = state === "complete" ? "已完成" : state === "active" ? "进行中" : state === "failed" ? "失败" : "等待";
- return `
${esc(stage.title)}${label}${esc(timelineDetail(index, scenario, healing))}
`;
- }).join("");
- $("#repairElapsed").textContent = `${Number(healing.elapsed_s || 0).toFixed(3)} s`;
+ const repairing = activeRepair;
+ const logState = $("#repairLogState");
+ logState.textContent = failed ? "需要处理" : repairing ? "自修复中" : healing.phase === "PASSED" && runtime.active ? "已恢复运行" : runtime.active ? "健康运行" : "等待启动";
+ logState.dataset.state = failed ? "fault" : repairing ? "repairing" : runtime.active ? "healthy" : "idle";
}
function renderCases() {
@@ -290,26 +406,27 @@
if (latest.batch?.active) return;
selectedCaseId = button.dataset.repairCase;
renderCases();
- renderTimeline();
renderTwin();
});
});
- const batch = latest.batch || {};
- const completed = Number(batch.completed || 0);
- const total = Number(batch.total || 6);
- $("#repairBatchProgress").style.width = `${Math.min(100, (completed / Math.max(1, total)) * 100)}%`;
- $("#repairBatchCount").textContent = `${completed} / ${total}`;
- $("#repairBatchSummary").textContent = batch.active
- ? `正在验证 ${batch.current_case_id || "下一项"} · 通过 ${batch.passed} · 失败 ${batch.failed}`
- : completed
- ? `批量验证完成 · 通过 ${batch.passed} · 失败 ${batch.failed}`
- : "等待启动批量验证";
+ const selected = scenarioForDisplay();
+ const runtime = runtimeState();
+ $("#repairCaseCount").textContent = `${(latest.scenarios || []).length} 个用例`;
+ $("#repairSelectedCase").textContent = selected
+ ? `当前选择:${selected.short_title}` : "当前选择:—";
+ $("#repairInjectionCount").textContent = Number(runtime.injected_count || 0)
+ ? `本次已注入 ${runtime.injected_count} 次 · 已修复 ${runtime.repaired_count || 0} 次`
+ : "本次尚未注入故障";
+ const activeRepair = !["IDLE", "PASSED", "FAILED"].includes(latest.healing?.phase);
+ $("#repairInject").disabled = !runtime.active || activeRepair || latest.batch?.active;
+ $("#repairInject").innerHTML = `
${activeRepair ? "自修复处理中" : runtime.active ? "注入所选故障" : "请先启动座椅运行"}`;
}
function renderTwin() {
const healing = latest.healing;
const scenario = scenarioForDisplay();
if (!scenario) return;
+ const runtime = runtimeState();
const telemetry = healing.telemetry || {};
const live = latest.bench_twin || {};
const bench = executionMode === "bench";
@@ -337,11 +454,19 @@
? (live.online ? "实物 CAN 同步" : "实物 CAN 暂离线")
: "影子动作仿真";
$("#repairTwinTask").textContent = healing.phase === "IDLE"
- ? `等待启动:${scenario.normal_task}` : scenario.normal_task;
- $("#repairTwinStage").textContent = stage || "待机";
- $("#repairTwinState").textContent = healing.phase_text;
+ ? (runtime.active ? runtime.motion_profile : "等待启动座椅运行")
+ : scenario.normal_task;
+ $("#repairTwinStage").textContent = healing.phase === "IDLE" && runtime.active
+ ? "自由运动" : stage || "待机";
+ $("#repairTwinState").textContent = healing.phase === "IDLE"
+ ? (runtime.active ? "持续运行中" : "等待启动")
+ : healing.phase === "PASSED" && runtime.active ? "已恢复运行" : healing.phase_text;
$("#repairTwinReaction").textContent = telemetry.bench_reaction
- || (healing.phase === "IDLE" ? scenario.field_basis : scenario.fault_reaction);
+ || (healing.phase === "IDLE"
+ ? (runtime.active
+ ? `四电机按设定轨迹协同运动 · 已完成 ${runtime.cycle_count || 0} 个周期`
+ : "实际动作将同步显示在座椅模型")
+ : scenario.fault_reaction);
$("#repairAxisStrip").innerHTML = axes.map((axis) => {
const position = Math.max(0, Math.min(100, Number(axis.position_pct || 0)));
return `
M${axis.motor}${position.toFixed(0)}%`;
@@ -441,6 +566,7 @@
const healing = latest.healing || {};
const hardwarePhase = latest.hardware_session?.progress?.phase;
const integration = latest.integration || {};
+ const runtime = runtimeState();
$("#repairCanStatus").textContent = bridge.online ? "在线" : "离线";
$("#repairCanDot").dataset.state = bridge.online ? "online" : manager.state === "STARTING" ? "working" : "offline";
$("#repairProtocolStatus").textContent = bridge.protocol_online ? "协议在线" : "未检测";
@@ -453,9 +579,13 @@
}[hardwarePhase];
const aiState = activeView === "hardware" && hardwareState
? hardwareState
- : [healing.phase_text || "等待验证", healing.phase === "PASSED"
- ? "online"
- : healing.phase === "IDLE" ? "offline" : "working"];
+ : runtime.active && healing.phase === "IDLE"
+ ? ["持续监控", "online"]
+ : runtime.active && healing.phase === "PASSED"
+ ? ["已恢复监控", "online"]
+ : [healing.phase_text || "等待验证", healing.phase === "PASSED"
+ ? "online"
+ : healing.phase === "IDLE" ? "offline" : "working"];
$("#repairAiStatus").textContent = aiState[0];
$("#repairAiDot").dataset.state = aiState[1];
$("#repairConnectionText").textContent = bridge.online ? "CAN 桥接在线" : manager.message || "CAN 桥接离线";
@@ -472,6 +602,14 @@
? "真实台架动作受本机 admin、安全确认与桥接状态约束"
: "影子仿真不会写入 ECU 或驱动实体设备";
$("#repairFirmwareFlash").disabled = !integration.firmware_tools_configured;
+ const runtimeToggle = $("#repairRuntimeToggle");
+ const repairActive = ["INJECTED", "DIAGNOSED", "REPAIRING", "VERIFYING"].includes(healing.phase);
+ runtimeToggle.innerHTML = runtime.active
+ ? '
停止座椅运行'
+ : '
启动座椅运行';
+ runtimeToggle.classList.toggle("danger", runtime.active);
+ runtimeToggle.classList.toggle("primary", !runtime.active);
+ runtimeToggle.disabled = repairActive;
window.CognitiveAgentConsoleV2?.enhanceSelect($("#repairExecutionMode"));
}
@@ -484,7 +622,7 @@
function renderAll() {
renderGlobal();
renderCases();
- renderTimeline();
+ renderEventStream();
renderTwin();
renderHardware();
renderRecords();
@@ -550,20 +688,33 @@
sessionStorage.setItem(MODE_KEY, executionMode);
if (latest) renderAll();
});
- $("#repairStart").addEventListener("click", async () => {
- if (executionMode === "bench" && !(await confirmAction("即将向实物 ECU 写入受控故障并驱动座椅恢复。请确认运动区域无人、无遮挡且急停可用。"))) return;
- runAction(() => apiPost("/healing/start", { case_id: selectedCaseId, execution_mode: executionMode }), executionMode === "bench" ? "实物自修复用例已启动" : "影子故障已注入,开始自修复");
- });
- $("#repairRunAll").addEventListener("click", async () => {
+ $("#repairRuntimeToggle").addEventListener("click", async () => {
+ const active = Boolean(runtimeState().active);
if (executionMode === "bench" && !(await confirmAction(
- "将依次执行6个实物故障用例;每项开始前会自动停止输出并完成四轴归零。请确认运动区域无人、无遮挡且急停可用。",
- "确认实物批量验证",
+ active
+ ? "即将停止实物座椅连续运行。请确认可安全停止。"
+ : "即将启动实物座椅四电机协同自由运动。请确认运动区域无人、无遮挡且急停可用。",
+ active ? "确认停止" : "确认启动",
))) return;
- runAction(() => apiPost("/healing/run-all", { execution_mode: executionMode }), "六项自动验证已启动");
+ runAction(
+ () => apiPost("/runtime", { active: !active, execution_mode: executionMode }),
+ active ? "座椅连续运行已停止" : "座椅连续运行已启动",
+ );
+ });
+ $("#repairInject").addEventListener("click", async () => {
+ const scenario = scenarioForDisplay();
+ if (!runtimeState().active || !scenario) return;
+ if (executionMode === "bench" && !(await confirmAction(
+ `即将向实物 ECU 注入「${scenario.short_title}」并驱动座椅自修复。请确认运动区域无人、无遮挡且急停可用。`,
+ "确认注入",
+ ))) return;
+ runAction(
+ () => apiPost("/healing/start", { case_id: selectedCaseId, execution_mode: executionMode }),
+ executionMode === "bench" ? "实物故障已注入" : "影子故障已注入,开始自修复",
+ );
});
$("#repairReset").addEventListener("click", async () => {
- if (executionMode === "bench" && !(await confirmAction("四轴将依次回到机械零位。请确认运动区域无人、无遮挡且急停可用。", "确认归零"))) return;
- runAction(() => apiPost("/healing/reset", { execution_mode: executionMode }), executionMode === "bench" ? "四轴归零请求已提交" : "自修复验证已重置");
+ runAction(() => apiPost("/runtime/reset-statistics"), "本次运行统计已重置");
});
$("#repairFirmwareFlash").addEventListener("click", async () => {
if (!(await confirmAction("即将构建并烧录台架测试固件。请确认座椅运动区域无人、急停可用且 CMSIS-DAP 已连接。", "确认构建/烧录"))) return;
@@ -594,6 +745,12 @@
});
$("#repairSoftwareRecords").addEventListener("click", () => $("#repairSoftwareDialog").showModal());
$("#repairHardwareRecords").addEventListener("click", () => $("#repairHardwareDialog").showModal());
+ $$('[data-repair-log-filter]').forEach((button) => button.addEventListener("click", () => {
+ eventFilter = button.dataset.repairLogFilter === "fault" ? "fault" : "all";
+ $$('[data-repair-log-filter]').forEach((item) => item.classList.toggle("active", item === button));
+ renderEventStream();
+ window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
+ }));
$$('[data-repair-close]').forEach((button) => button.addEventListener("click", () => $(`#${button.dataset.repairClose}`).close()));
window.addEventListener("cognitive-agent:module-change", (event) => {
if (event.detail?.module === "self-repair") poll();
@@ -607,6 +764,7 @@
if (!buildShell()) return;
bindEvents();
showView(activeView);
+ renderAll();
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
if (isActive()) poll();
window.setInterval(poll, 600);
diff --git a/apps/web-console/service-worker.js b/apps/web-console/service-worker.js
index 5e2da91..4428d8b 100644
--- a/apps/web-console/service-worker.js
+++ b/apps/web-console/service-worker.js
@@ -1,4 +1,4 @@
-const CACHE_VERSION = "cognitive-agent-pad-v80";
+const CACHE_VERSION = "cognitive-agent-pad-v115";
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`;
const APP_SHELL = [
@@ -6,19 +6,18 @@ const APP_SHELL = [
"/index.html",
"/styles.css?v=dual-seat-9",
"/network.js?v=android-pad-1",
- "/app.js?v=dual-seat-67",
+ "/app.js?v=dual-seat-80",
"/gesture-control.js?v=android-pad-7",
"/pwa.js?v=android-pad-1",
- "/console-v2.css?v=desktop-refactor-87",
- "/console-v2.js?v=desktop-refactor-28",
- "/self-extension.css?v=extension-12",
- "/self-extension.js?v=extension-18",
- "/self-calibration.css?v=calibration-6",
- "/self-calibration.js?v=calibration-3",
- "/self-repair.css?v=repair-17",
- "/self-repair.js?v=repair-17",
- "/self-repair-seat3d.js?v=repair-17",
- "/assets/self-calibration-model.bundle.js",
+ "/console-v2.css?v=desktop-refactor-106",
+ "/console-v2.js?v=desktop-refactor-34",
+ "/self-extension.css?v=extension-18",
+ "/self-extension.js?v=extension-20",
+ "/self-calibration.css?v=calibration-10",
+ "/self-calibration.js?v=calibration-6",
+ "/self-repair.css?v=repair-21",
+ "/self-repair.js?v=repair-20",
+ "/self-repair-seat3d.js?v=repair-18",
"/assets/models/n7_seat.glb",
"/manifest.webmanifest",
"/icons/app-icon-192.png",
diff --git a/src/cognitive_agent/application/self_repair/service.py b/src/cognitive_agent/application/self_repair/service.py
index 1f98d83..e6f8384 100644
--- a/src/cognitive_agent/application/self_repair/service.py
+++ b/src/cognitive_agent/application/self_repair/service.py
@@ -260,6 +260,7 @@ class ConsoleState(object):
self.latest_report_path = None
self.batch = self._new_batch_state()
self.batch_thread = None
+ self.runtime = self._new_runtime_state()
self.hardware_active = False
self.hardware_run_id = None
self.hardware_started_epoch = None
@@ -535,6 +536,202 @@ class ConsoleState(object):
"finished_epoch": None, "results": [],
}
+ @staticmethod
+ def _new_runtime_state(execution_mode="shadow"):
+ return {
+ "active": False,
+ "execution_mode": execution_mode,
+ "started_epoch": None,
+ "stopped_epoch": None,
+ "healthy_started_epoch": None,
+ "healthy_accumulated_s": 0.0,
+ "motion_profile": "四电机协同自由运动",
+ "cycle_count": 0,
+ "injected_count": 0,
+ "repaired_count": 0,
+ "last_healing_run_id": None,
+ "events": [],
+ }
+
+ @staticmethod
+ def _runtime_event(kind, label, title, detail, now):
+ return {
+ "kind": kind,
+ "label": label,
+ "title": title,
+ "detail": detail,
+ "epoch": now,
+ "time": time.strftime("%H:%M:%S", time.localtime(now)) +
+ (".%03d" % int((now * 1000) % 1000)),
+ }
+
+ def _runtime_snapshot(self, healing, now):
+ runtime = dict(self.runtime)
+ elapsed = 0.0
+ if runtime.get("started_epoch") is not None:
+ end = now if runtime.get("active") else (
+ runtime.get("stopped_epoch") or now)
+ elapsed = max(0.0, end - float(runtime["started_epoch"]))
+ healthy = float(runtime.get("healthy_accumulated_s") or 0.0)
+ if runtime.get("active") and runtime.get("healthy_started_epoch") is not None:
+ healthy += max(
+ 0.0, now - float(runtime["healthy_started_epoch"]))
+
+ run_id = healing.get("run_id")
+ phase = healing.get("phase")
+ if run_id and run_id != runtime.get("last_healing_run_id"):
+ self.runtime["last_healing_run_id"] = run_id
+ self.runtime["injected_count"] += 1
+ self.runtime["healthy_accumulated_s"] = healthy
+ self.runtime["healthy_started_epoch"] = None
+ runtime.update(self.runtime)
+ if phase == PHASE_PASSED and run_id and \
+ run_id not in self.runtime.get("repaired_run_ids", []):
+ repaired = list(self.runtime.get("repaired_run_ids", []))
+ repaired.append(run_id)
+ self.runtime["repaired_run_ids"] = repaired[-100:]
+ self.runtime["repaired_count"] += 1
+ if self.runtime.get("active"):
+ self.runtime["healthy_started_epoch"] = now
+ if self.runtime.get("execution_mode") == "bench":
+ request_id = "SELF_REPAIR_RUNTIME_RESUME_%d" % int(
+ now * 1000)
+ _atomic_json(
+ PENDING_DIR / (request_id + ".json"),
+ self._dynamic_runtime_request(request_id, True))
+ runtime.update(self.runtime)
+ if phase == PHASE_FAILED and run_id and \
+ run_id not in self.runtime.get("failed_run_ids", []):
+ failed = list(self.runtime.get("failed_run_ids", []))
+ failed.append(run_id)
+ self.runtime["failed_run_ids"] = failed[-100:]
+ self.runtime.update({
+ "active": False,
+ "stopped_epoch": now,
+ "healthy_started_epoch": None,
+ })
+ self.runtime["events"].append(self._runtime_event(
+ "fault", "安全停止", "自修复未通过,连续运行已停止",
+ "座椅保持安全停止,请检查故障证据后重新启动。", now))
+ runtime.update(self.runtime)
+ healthy = float(self.runtime.get("healthy_accumulated_s") or 0.0)
+ if self.runtime.get("active") and self.runtime.get(
+ "healthy_started_epoch") is not None:
+ healthy += max(
+ 0.0, now - float(self.runtime["healthy_started_epoch"]))
+ runtime["elapsed_s"] = round(elapsed, 3)
+ runtime["healthy_elapsed_s"] = round(healthy, 3)
+ runtime["cycle_count"] = int(elapsed // 20)
+ runtime.pop("healthy_started_epoch", None)
+ runtime.pop("healthy_accumulated_s", None)
+ runtime.pop("repaired_run_ids", None)
+ runtime.pop("failed_run_ids", None)
+ return runtime
+
+ @staticmethod
+ def _dynamic_runtime_request(request_id, start):
+ opcode = 0x0E if start else 0x0F
+ mode_id = 1 if start else 0
+ capability = ("seat.start_dynamic_mode" if start else
+ "seat.stop_dynamic_mode")
+ operation_id = "self_repair_runtime_dynamic"
+ return {
+ "schema": "seat-function-request-v1",
+ "request_id": request_id,
+ "created_epoch": time.time(),
+ "armed_until_epoch": time.time() + 60.0,
+ "source": "seat_self_healing_console",
+ "action": "execute_command_plan",
+ "payload": {
+ "operations": [{
+ "id": operation_id,
+ "domain": "seat",
+ "capability": capability,
+ "parameters": ({"mode_id": mode_id} if start else {}),
+ }],
+ "commands": [{
+ "id": "runtime_dynamic_start" if start else
+ "runtime_dynamic_stop",
+ "operation_ref": operation_id,
+ "frame_id": 0x300,
+ "message": "T2G_Seat_Move_Command_300",
+ "data": [opcode, 0x10, 0, 0, mode_id, 0, 0, 0],
+ "signals": {
+ "Opcode": opcode,
+ "Motor_Selector": 0,
+ "Direction": 0,
+ "Protocol_Version": 1,
+ "Sequence": 0,
+ "Param_A": mode_id,
+ "Param_B": 0,
+ },
+ "completion": {
+ "value": mode_id,
+ "accepted_states": [1, 2],
+ "timeout_ms": 8000,
+ },
+ }],
+ },
+ }
+
+ def set_runtime(self, active, execution_mode="shadow"):
+ if execution_mode not in ("shadow", "bench"):
+ raise ValueError("未知运行方式")
+ with self.lock:
+ now = time.time()
+ if bool(self.runtime.get("active")) == bool(active):
+ return self._runtime_snapshot(self.engine.snapshot(now), now)
+ if active and (self.hardware_active or self.batch.get("active")):
+ raise ValueError("硬件诊断或批量验证正在运行,不能启动座椅连续运行")
+ if active and self.engine.phase not in ("IDLE", PHASE_PASSED,
+ PHASE_FAILED):
+ raise ValueError("自修复正在处理故障,请等待完成后再启动连续运行")
+ if execution_mode == "bench":
+ self.bridge_manager.ensure_async()
+ _, bridge = self._wait_bridge(
+ require_protocol=True, timeout_s=12.0)
+ if not bridge.get("online") or not bridge.get("protocol_online"):
+ raise ValueError("CAN桥接或座椅ECU协议未在线,不能控制实物座椅")
+ request_id = "SELF_REPAIR_RUNTIME_%s_%d" % (
+ "START" if active else "STOP", int(now * 1000))
+ _atomic_json(
+ PENDING_DIR / (request_id + ".json"),
+ self._dynamic_runtime_request(request_id, bool(active)))
+ if active:
+ self.engine.reset()
+ self.runtime = self._new_runtime_state(execution_mode)
+ self.runtime.update({
+ "active": True,
+ "started_epoch": now,
+ "healthy_started_epoch": now,
+ })
+ self.runtime["events"].append(self._runtime_event(
+ "system", "运行", "座椅自由运动任务已启动",
+ "四个电机进入协同自由运动,AI开始持续监测。", now))
+ else:
+ healthy_started = self.runtime.get("healthy_started_epoch")
+ if healthy_started is not None:
+ self.runtime["healthy_accumulated_s"] += max(
+ 0.0, now - float(healthy_started))
+ self.runtime.update({
+ "active": False,
+ "stopped_epoch": now,
+ "healthy_started_epoch": None,
+ })
+ self.runtime["events"].append(self._runtime_event(
+ "system", "运行", "座椅自由运动任务已停止",
+ "管理员停止了连续运行,故障注入入口已关闭。", now))
+ return self._runtime_snapshot(self.engine.snapshot(now), now)
+
+ def reset_runtime_statistics(self):
+ with self.lock:
+ if self.runtime.get("active"):
+ raise ValueError("请先停止座椅运行,再重置本次统计")
+ self.engine.reset()
+ mode = self.runtime.get("execution_mode", "shadow")
+ self.runtime = self._new_runtime_state(mode)
+ return self._runtime_snapshot(self.engine.snapshot(), time.time())
+
@staticmethod
def _bridge_summary(status, now):
heartbeat = None if not isinstance(status, dict) else status.get(
@@ -944,6 +1141,7 @@ class ConsoleState(object):
hardware = self._hardware_snapshot(status, now)
bridge = self._bridge_summary(status, now)
healing = self.engine.snapshot(now)
+ runtime = self._runtime_snapshot(healing, now)
self._save_completed_report(healing, hardware, bridge)
return {
"schema": "seat-self-healing-console-state-v1",
@@ -959,6 +1157,7 @@ class ConsoleState(object):
"bench_twin": self._bench_twin_snapshot(status, now),
"bridge_manager": self.bridge_manager.snapshot(),
"healing": healing,
+ "runtime": runtime,
"bench_reset": dict(self.bench_reset),
"hardware": hardware,
"batch": dict(self.batch),
@@ -987,6 +1186,8 @@ class ConsoleState(object):
def start(self, case_id, execution_mode="shadow", allow_batch=False):
with self.lock:
+ if not self.runtime.get("active") and not allow_batch:
+ raise ValueError("请先启动座椅连续运行,再注入故障")
if execution_mode == "bench":
if (self.hardware_active or
(self.batch.get("active") and not allow_batch)):
@@ -1006,10 +1207,15 @@ class ConsoleState(object):
raise ValueError("未识别到本工程座椅ECU协议,不能执行台架故障注入")
self._sync_bench_reset(status, now)
zero = self._bench_zero_snapshot(status)
- if (self.bench_reset.get("phase") != "PASSED" or
- not zero.get("all_zero")):
+ system = (status or {}).get("system") or {}
+ runtime_ready = (
+ self.runtime.get("active") and
+ int(system.get("position_valid_mask", 0) or 0) == 0x0F and
+ int(system.get("faulted_mask", 0) or 0) == 0)
+ if ((self.bench_reset.get("phase") != "PASSED" or
+ not zero.get("all_zero")) and not runtime_ready):
raise ValueError(
- "请先点击‘重置并四轴归零’,确认四轴均为零位后再启动该用例")
+ "连续运行状态无可信四轴位置,请先停止运行并完成四轴归零")
request_id = "SELF_HEAL_BENCH_%d" % int(now * 1000)
request = {
"schema": "seat-function-request-v1",
@@ -1026,11 +1232,12 @@ class ConsoleState(object):
case_id, now=now, execution_mode="bench")
self.engine.telemetry["bench_request_id"] = request_id
_atomic_json(PENDING_DIR / (request_id + ".json"), request)
- self.bench_reset.update({
- "phase": "CONSUMED",
- "summary": "本次归零许可已用于%s;下一个用例前须重新归零" %
- case_id,
- })
+ if not runtime_ready:
+ self.bench_reset.update({
+ "phase": "CONSUMED",
+ "summary": "本次归零许可已用于%s;下一个用例前须重新归零" %
+ case_id,
+ })
return value
self.latest_report_path = None
self.bench_request_id = None
@@ -1317,6 +1524,21 @@ class ConsoleState(object):
return value
def emergency_stop(self):
+ with self.lock:
+ if self.runtime.get("active"):
+ now = time.time()
+ healthy_started = self.runtime.get("healthy_started_epoch")
+ if healthy_started is not None:
+ self.runtime["healthy_accumulated_s"] += max(
+ 0.0, now - float(healthy_started))
+ self.runtime.update({
+ "active": False,
+ "stopped_epoch": now,
+ "healthy_started_epoch": None,
+ })
+ self.runtime["events"].append(self._runtime_event(
+ "fault", "停止", "安全链路触发紧急停止",
+ "STOP ALL 与热管理关闭请求已提交。", now))
request_id = "SELF_HEAL_STOP_%d" % int(time.time() * 1000)
value = {
"schema": "seat-function-emergency-v1",
@@ -1591,6 +1813,14 @@ class SelfRepairService(object):
return self.state.start(
payload.get("case_id"), payload.get("execution_mode", "shadow"))
+ def set_runtime(self, payload):
+ return self.state.set_runtime(
+ bool(payload.get("active")),
+ payload.get("execution_mode", "shadow"))
+
+ def reset_runtime_statistics(self):
+ return self.state.reset_runtime_statistics()
+
def start_batch(self, payload):
return self.state.start_batch(payload.get("execution_mode", "shadow"))
diff --git a/src/cognitive_agent/server.py b/src/cognitive_agent/server.py
index ab86ba7..81c129d 100755
--- a/src/cognitive_agent/server.py
+++ b/src/cognitive_agent/server.py
@@ -573,6 +573,7 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
path in physical_paths
or (
path in {
+ "/api/self-repair/runtime",
"/api/self-repair/healing/start",
"/api/self-repair/healing/reset",
}
@@ -584,6 +585,15 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
if path == "/api/self-repair/healing/start":
self._json({"ok": True, "healing": self.repair.start_healing(payload)})
return
+ if path == "/api/self-repair/runtime":
+ self._json({"ok": True, "runtime": self.repair.set_runtime(payload)})
+ return
+ if path == "/api/self-repair/runtime/reset-statistics":
+ self._json({
+ "ok": True,
+ "runtime": self.repair.reset_runtime_statistics(),
+ })
+ return
if path == "/api/self-repair/healing/run-all":
self._json({"ok": True, "batch": self.repair.start_batch(payload)})
return
diff --git a/tests/e2e/test_seat_web_api.py b/tests/e2e/test_seat_web_api.py
index 6bcc4c2..f2b4575 100755
--- a/tests/e2e/test_seat_web_api.py
+++ b/tests/e2e/test_seat_web_api.py
@@ -177,28 +177,28 @@ operations:
def test_static_console_and_runtime_endpoint(self) -> None:
with self.opener.open(f"{self.base_url}/", timeout=3) as response:
html = response.read().decode("utf-8")
- with self.opener.open(f"{self.base_url}/app.js?v=dual-seat-67", timeout=3) as response:
+ with self.opener.open(f"{self.base_url}/app.js?v=dual-seat-80", timeout=3) as response:
app_script = response.read().decode("utf-8")
with self.opener.open(
f"{self.base_url}/gesture-control.js?v=android-pad-7", timeout=3
) as response:
gesture_script = response.read().decode("utf-8")
with self.opener.open(
- f"{self.base_url}/console-v2.js?v=desktop-refactor-28", timeout=3
+ f"{self.base_url}/console-v2.js?v=desktop-refactor-34", timeout=3
) as response:
console_script = response.read().decode("utf-8")
with self.opener.open(
- f"{self.base_url}/console-v2.css?v=desktop-refactor-87", timeout=3
+ f"{self.base_url}/console-v2.css?v=desktop-refactor-106", timeout=3
) as response:
console_styles = response.read().decode("utf-8")
with self.opener.open(
- f"{self.base_url}/self-extension.js?v=extension-18", timeout=3
+ f"{self.base_url}/self-extension.js?v=extension-20", timeout=3
) as response:
extension_script = response.read().decode("utf-8")
with self.opener.open(
- f"{self.base_url}/self-extension.css?v=extension-12", timeout=3
+ f"{self.base_url}/self-extension.css?v=extension-18", timeout=3
) as response:
- extension_styles = response.read().decode("utf-8")
+ extension_styles = response.read().decode("utf-8").replace("\r\n", "\n")
with self.opener.open(
f"{self.base_url}/api/extension/health", timeout=3
) as response:
@@ -219,22 +219,39 @@ operations:
self.assertIn("CMD 0x431 · FB 0x41A · CH1", html)
self.assertIn("CMD 0x431 · FB 0x388 · CH1", html)
self.assertNotIn("透明车体 · 实体设备", html)
- self.assertIn('src="/app.js?v=dual-seat-67"', html)
+ self.assertIn('src="/app.js?v=dual-seat-80"', html)
self.assertIn('href="/styles.css?v=dual-seat-9"', html)
self.assertIn('src="/gesture-control.js?v=android-pad-7"', html)
self.assertNotIn('class="vehicle-system-hud"', html)
- self.assertIn('src="/console-v2.js?v=desktop-refactor-28"', html)
- self.assertIn('href="/console-v2.css?v=desktop-refactor-87"', html)
- self.assertIn('src="/self-extension.js?v=extension-18"', html)
- self.assertIn('href="/self-extension.css?v=extension-12"', html)
+ self.assertIn('src="/console-v2.js?v=desktop-refactor-34"', html)
+ self.assertIn('href="/console-v2.css?v=desktop-refactor-106"', html)
+ self.assertIn('src="/self-extension.js?v=extension-20"', html)
+ self.assertIn('href="/self-extension.css?v=extension-18"', html)
self.assertNotIn("滚动查看更多", html)
self.assertIn('aria-label="执行车舱控制指令"', html)
self.assertIn('sessionStorage.getItem("cognitive-agent.demo-role")', console_script)
self.assertIn('id="v2SidebarToggle"', console_script)
self.assertIn('id = "v2ModuleSwitcher"', console_script)
+ self.assertIn(
+ 'panelHeading.classList.add("v2-operation-heading", "v2-module-heading")',
+ console_script,
+ )
+ self.assertIn('kicker.textContent = "SELF-ORCHESTRATION"', console_script)
+ self.assertIn(
+ 'if (ui.activeModule === "self-extension" && ui.role !== "user")',
+ console_script,
+ )
+ self.assertIn(
+ 'if (ui.activeModule === "self-extension" && role !== "user" && !allowModuleTransition)',
+ console_script,
+ )
self.assertIn('label: "自编排", available: true', console_script)
self.assertIn('id: "self-calibration"', console_script)
self.assertIn('label: "自标定"', console_script)
+ self.assertLess(
+ console_script.index('label: "自标定"'),
+ console_script.index('label: "自编排"'),
+ )
self.assertIn("adminOnly: true", console_script)
self.assertIn(
"$('.v2-module-tab[data-module=\"self-calibration\"]')",
@@ -245,6 +262,16 @@ operations:
self.assertIn('
Hi,user', console_script)
self.assertIn('id: "self-repair"', console_script)
self.assertIn('label: "自修复"', console_script)
+ self.assertIn('id: "self-evolution", label: "自进化", available: true', console_script)
+ self.assertIn(
+ 'evolution.setAttribute("aria-label", "自进化能力日志")',
+ console_script,
+ )
+ self.assertIn('
SELF-EVOLUTION', console_script)
+ self.assertIn('v2-evolution-heading v2-module-heading', console_script)
+ self.assertIn('data-evolution-index', console_script)
+ self.assertIn('NEXT EVOLUTION · v0.6', console_script)
+ self.assertIn('["self-orchestration", "self-extension", "self-evolution"]', console_script)
self.assertIn('roleSelect.disabled = ui.activeModule === "self-repair"', console_script)
self.assertNotIn('label: "自诊断"', console_script)
self.assertIn('
手势模型', console_script)
@@ -262,7 +289,37 @@ operations:
self.assertIn('grid-template-columns: clamp(245px, 18.5vw, 310px)', console_styles)
self.assertIn('.v2-control-switch {', console_styles)
self.assertIn('.v2-module-switcher {', console_styles)
- self.assertIn('grid-template-columns: repeat(4, minmax(0, 1fr))', console_styles)
+ self.assertIn('grid-template-columns: repeat(5, minmax(0, 1fr))', console_styles)
+ self.assertIn('[data-module="self-evolution"] .v2-self-evolution', console_styles)
+ self.assertIn(
+ '.console-v2 .v2-module-heading.v2-module-heading h1',
+ console_styles,
+ )
+ self.assertIn('font-size: clamp(24px, 2vw, 31px)', console_styles)
+ self.assertIn('--v2-module-title-block: clamp(12px, 2vh, 22px)', console_styles)
+ self.assertIn('--v2-module-title-inline: clamp(20px, 2.3vw, 34px)', console_styles)
+ self.assertIn(
+ 'grid-template-rows: 36px clamp(236px, 33dvh, 246px) minmax(0, 1fr)',
+ console_styles,
+ )
+ self.assertIn(
+ '[data-module="self-calibration"] .v2-self-calibration',
+ console_styles,
+ )
+ self.assertIn(
+ '[data-module="self-repair"] .v2-self-repair',
+ console_styles,
+ )
+ self.assertIn('.v2-evolution-version.active', console_styles)
+ self.assertIn('[data-evolution-scope="admin"]', console_styles)
+ self.assertIn(
+ 'color-mix(in srgb, var(--v2-accent-soft) 86%, white)',
+ console_styles,
+ )
+ self.assertNotIn(
+ '.cabin-channel-legend [data-channel="ch0"]',
+ console_styles,
+ )
self.assertIn('margin-inline: calc(0px - var(--v2-main-pad))', console_styles)
self.assertIn('background: color-mix(in srgb, var(--v2-accent) 30%, var(--v2-accent-soft))', console_styles)
self.assertIn('width: clamp(48px, 45%, 72px)', console_styles)
@@ -333,8 +390,10 @@ operations:
self.assertIn('root.id = "v2SelfExtension"', extension_script)
self.assertIn('data-extension-mode="study"', extension_script)
self.assertIn('data-extension-mode="control"', extension_script)
- self.assertIn('外设玩具能力一览与学习', extension_script)
- self.assertIn('说一句,联动外设玩具与车端设备', extension_script)
+ self.assertIn('外设玩具能力学习', extension_script)
+ self.assertIn('外设玩具与车端联动', extension_script)
+ self.assertGreaterEqual(extension_script.count('class="v2-module-heading"'), 2)
+ self.assertGreaterEqual(extension_script.count('SELF-EXTENSION'), 2)
self.assertIn('id="extensionExecuteButton"', extension_script)
self.assertIn('data-lucide="play"', extension_script)
self.assertNotIn('id="extensionControlHealth"', extension_script)
@@ -363,6 +422,10 @@ operations:
self.assertIn('.v2-extension-twin-host > .twin-band {', extension_styles)
self.assertIn('.v2-extension-type-card[data-extension-connected="true"]', extension_styles)
self.assertIn('.v2-extension-type-card[data-extension-connected="false"]', extension_styles)
+ self.assertIn(
+ '.v2-extension-control\n.v2-extension-learned-action {\n --extension-card-color: var(--v2-accent);',
+ extension_styles,
+ )
self.assertIn('grid-template-columns: repeat(3, minmax(0, 1fr))', extension_styles)
self.assertTrue(extension_health["success"])
self.assertIn(extension_health["transport"], {"tcp", "mqtt", "simulation"})
@@ -444,6 +507,45 @@ operations:
self.assertIn('id="vehicleSpeed"', html)
self.assertNotIn('data-domain-panel="control_link"', html)
self.assertIn('id="cabinStateTitle">整车设备状态', html)
+ self.assertIn('id="cabinCarouselPrev"', app_script)
+ self.assertIn('id="cabinCarouselNext"', app_script)
+ self.assertIn('aria-label="左右浏览设备卡片"', app_script)
+ self.assertNotIn('class="cabin-category-tabs"', app_script)
+ self.assertIn('
LIVE VEHICLE DIGITAL TWIN', html)
+ self.assertIn('.joint-twin-shell > .twin-band {', console_styles)
+ self.assertIn('grid-template-rows: 62px minmax(0, 1fr)', console_styles)
+ self.assertIn('grid-template-columns: repeat(2, minmax(0, 1fr))', console_styles)
+ self.assertIn('--cabin-cards-per-view: 2', console_styles)
+ self.assertIn('const cabinCardsPerView = () => 2', app_script)
+ self.assertIn('scroll-snap-type: x mandatory', console_styles)
+ self.assertIn('data-device-power-value', app_script)
+ self.assertIn('查看${name}功率详情', app_script)
+ self.assertIn('name: "车内灯光", measurable: false', app_script)
+ self.assertIn('.device-power-entry.unavailable', console_styles)
+ self.assertIn('powerDrawer.id = "devicePowerDrawer"', app_script)
+ self.assertIn('window.updateDevicePowerTelemetry = updatePowerTelemetry', app_script)
+ self.assertIn('["longitudinal_motor", "水平电机"]', app_script)
+ self.assertIn('["cushion_motor", "坐盆电机"]', app_script)
+ self.assertIn('["backrest_motor", "座椅靠背电机"]', app_script)
+ self.assertIn('["legrest_motor", "腿托电机"]', app_script)
+ self.assertIn('["ventilation_control", "通风控制"]', app_script)
+ self.assertIn('["heating_control", "加热控制"]', app_script)
+ self.assertIn('["compressor", "压缩机"]', app_script)
+ self.assertIn('["electronic_fan", "电子风扇"]', app_script)
+ self.assertIn('["blower", "鼓风机"]', app_script)
+ self.assertIn('["thermal_management_module", "空调热管理集成模块"]', app_script)
+ self.assertIn('["compressor_controller", "压缩机电控"]', app_script)
+ self.assertIn('["electronic_cooling_pump", "电控冷却水泵"]', app_script)
+ self.assertIn('["front_left_window", "左前车窗"]', app_script)
+ self.assertIn('["front_right_window", "右前车窗"]', app_script)
+ self.assertIn('["rear_left_window", "左后车窗"]', app_script)
+ self.assertIn('["rear_right_window", "右后车窗"]', app_script)
+ self.assertIn('["front_left_combination_lamp", "左前组合灯"]', app_script)
+ self.assertIn('["front_right_combination_lamp", "右前组合灯"]', app_script)
+ self.assertIn('name: "主驾座椅", measurable: true, components: []', app_script)
+ self.assertIn('name: "遮阳帘", measurable: true, components: []', app_script)
+ self.assertIn("仅支持设备总功率", app_script)
+ self.assertIn('.device-power-drawer {', console_styles)
self.assertNotIn('class="roof-visual"', html)
self.assertNotIn('class="ambient-visual"', html)
self.assertIn('id="driverAirDirection"', html)
@@ -536,11 +638,11 @@ operations:
def test_self_extension_vehicle_success_contract_is_strict(self) -> None:
with self.opener.open(
- f"{self.base_url}/self-extension.js?v=extension-18", timeout=3
+ f"{self.base_url}/self-extension.js?v=extension-20", timeout=3
) as response:
extension_script = response.read().decode("utf-8")
with self.opener.open(
- f"{self.base_url}/app.js?v=dual-seat-67", timeout=3
+ f"{self.base_url}/app.js?v=dual-seat-80", timeout=3
) as response:
app_script = response.read().decode("utf-8")
diff --git a/tests/e2e/test_self_calibration_api.py b/tests/e2e/test_self_calibration_api.py
index 52c3ada..4f9b7b0 100644
--- a/tests/e2e/test_self_calibration_api.py
+++ b/tests/e2e/test_self_calibration_api.py
@@ -126,17 +126,17 @@ class TestSelfCalibrationApi:
with urllib.request.urlopen(f"{self.base_url}/", timeout=3) as response:
html = response.read().decode("utf-8")
with urllib.request.urlopen(
- f"{self.base_url}/self-calibration.js?v=calibration-3",
+ f"{self.base_url}/self-calibration.js?v=calibration-6",
timeout=3,
) as response:
script = response.read().decode("utf-8")
with urllib.request.urlopen(
- f"{self.base_url}/self-calibration.css?v=calibration-6",
+ f"{self.base_url}/self-calibration.css?v=calibration-10",
timeout=3,
) as response:
styles = response.read().decode("utf-8")
with urllib.request.urlopen(
- f"{self.base_url}/console-v2.js?v=desktop-refactor-28",
+ f"{self.base_url}/console-v2.js?v=desktop-refactor-31",
timeout=3,
) as response:
console_script = response.read().decode("utf-8")
@@ -148,24 +148,30 @@ class TestSelfCalibrationApi:
content_type = response.headers["Content-Type"]
content_security_policy = response.headers["Content-Security-Policy"]
- assert 'href="/self-calibration.css?v=calibration-6"' in html
- assert 'src="/self-calibration.js?v=calibration-3"' in html
+ assert 'href="/self-calibration.css?v=calibration-10"' in html
+ assert 'src="/self-calibration.js?v=calibration-6"' in html
assert 'data-calibration-page="live"' in script
assert 'data-calibration-view="live"' in script
assert 'data-calibration-page="hall"' not in script
assert 'data-calibration-page="stereo"' not in script
- assert "座椅—双目联合标定" in script
+ assert "座椅与双目联合标定" in script
+ assert 'class="v2-module-heading"' in script
+ assert 'SELF-CALIBRATION' in script
+ assert 'padding: var(--v2-module-title-block) 0 0' in styles
+ assert 'margin-inline: var(--v2-module-title-inline)' in styles
assert "实时座椅姿态" in script
assert "四轴实时控制" in script
assert "ZED 实时画面" in script
- assert "双目位姿数据" in script
+ assert "双目位姿数据" not in script
+ assert 'class="v2-calibration-panel v2-calibration-mqtt-panel"' not in script
+ assert "grid-template-columns: minmax(420px, 1.06fr) minmax(380px, 0.94fr)" in styles
assert "标定仿真台架" in script
assert "X-Cognitive-Agent-Role" in script
assert "--v2-sidebar: #dae3f5" in styles
assert "--v2-sidebar-deep: #b6c7ea" in styles
assert "--v2-accent-strong: #1e386b" in styles
assert '["self-calibration", "self-repair"].includes(module)' in console_script
- assert 'setRole("admin")' in console_script
+ assert 'setRole("admin", { allowModuleTransition: true })' in console_script
assert len(model) == 845_992
assert content_type.startswith("model/gltf-binary")
assert "img-src 'self' data: blob:;" in content_security_policy
diff --git a/tests/e2e/test_self_repair_api.py b/tests/e2e/test_self_repair_api.py
index fea045a..9bc020a 100644
--- a/tests/e2e/test_self_repair_api.py
+++ b/tests/e2e/test_self_repair_api.py
@@ -95,6 +95,17 @@ class TestSelfRepairApi:
assert len(state["scenarios"]) == 6
assert state["hardware"]["connector_enabled_count"] == 21
+ with urllib.request.urlopen(
+ self.request(
+ "/api/self-repair/runtime",
+ method="POST",
+ payload={"active": True, "execution_mode": "shadow"},
+ ),
+ timeout=3,
+ ) as response:
+ runtime = json.loads(response.read().decode("utf-8"))["runtime"]
+ assert runtime["active"] is True
+
with urllib.request.urlopen(
self.request(
"/api/self-repair/healing/start",
@@ -135,26 +146,37 @@ class TestSelfRepairApi:
with urllib.request.urlopen(f"{self.base_url}/", timeout=3) as response:
html = response.read().decode("utf-8")
with urllib.request.urlopen(
- f"{self.base_url}/self-repair.js?v=repair-17", timeout=3
+ f"{self.base_url}/self-repair.js?v=repair-20", timeout=3
) as response:
- script = response.read().decode("utf-8")
+ script = response.read().decode("utf-8").replace("\r\n", "\n")
with urllib.request.urlopen(
- f"{self.base_url}/self-repair.css?v=repair-17", timeout=3
+ f"{self.base_url}/self-repair.css?v=repair-21", timeout=3
) as response:
styles = response.read().decode("utf-8")
with urllib.request.urlopen(
- f"{self.base_url}/self-repair-seat3d.js?v=repair-17", timeout=3
+ f"{self.base_url}/self-repair-seat3d.js?v=repair-18", timeout=3
) as response:
seat3d = response.read().decode("utf-8")
with urllib.request.urlopen(
- f"{self.base_url}/console-v2.js?v=desktop-refactor-28", timeout=3
+ f"{self.base_url}/console-v2.js?v=desktop-refactor-31", timeout=3
) as response:
console_script = response.read().decode("utf-8")
- assert 'href="/self-repair.css?v=repair-17"' in html
- assert 'src="/self-repair.js?v=repair-17"' in html
- assert 'src="/self-repair-seat3d.js?v=repair-17"' in html
- assert "量产故障用例" in script
+ assert 'href="/self-repair.css?v=repair-21"' in html
+ assert 'src="/self-repair.js?v=repair-20"' in html
+ assert 'class="v2-module-heading"' in script
+ assert 'SELF-REPAIR' in script
+ assert 'padding: var(--v2-module-title-block) 0 0' in styles
+ assert 'margin-inline: var(--v2-module-title-inline)' in styles
+ assert 'src="/self-repair-seat3d.js?v=repair-18"' in html
+ assert "量产故障注入" in script
+ assert "运行与自修复日志" in script
+ assert "启动座椅运行" in script
+ assert "开始正常任务" not in script
+ assert 'id="repairSeatCanvas"' in script
+ assert 'id="repairSeatViewer"' in script
+ assert "self-repair-seat-reset" in script
+ assert "seat-healing-state" in script
assert 'id="repairConfirmDialog"' in script
assert "confirmAction(" in script
assert "window.confirm" not in script
@@ -163,10 +185,15 @@ class TestSelfRepairApi:
assert "connectorRows" in script
assert "Polling refreshes data only" in script
assert "renderRecords();\n showView(activeView);" not in script
+ assert 'let latest = fallbackState();' in script
+ assert 'renderAll();\n window.lucide?.createIcons' in script
+ assert '"WDT复位后失能"' in script
assert "--repair-deep-blue" in styles
assert '.console-v2[data-module="self-repair"] .section-kicker' in styles
- assert 'grid-template-areas: "twin cases timeline"' in styles
- assert "background: color-mix(in srgb, var(--v2-accent-soft) 82%, white)" in styles
+ assert 'grid-template-areas: "twin cases logs"' in styles
+ assert ".v2-repair-event-stream" in styles
+ assert "background: rgba(255, 255, 255, 0.94)" in styles
+ assert "border-color: color-mix(in srgb, var(--v2-accent) 18%, white)" in styles
assert ".v2-repair-toolbar button.primary:hover:not(:disabled)" in styles
assert "color: #fff;" in styles
assert "seat-healing-state" in seat3d
@@ -175,7 +202,7 @@ class TestSelfRepairApi:
assert 'cssThemeColor("--v2-accent", "#315eaa")' in seat3d
assert 'viewer.addEventListener("pointerdown"' in seat3d
assert 'window.addEventListener("pointermove"' in seat3d
- assert 'window.addEventListener("self-repair-seat-reset"' in seat3d
+ assert "window.addEventListener(resetEvent" in seat3d
assert '["self-calibration", "self-repair"].includes(module)' in console_script
assert 'roleSelect.disabled = ui.activeModule === "self-repair"' in console_script
diff --git a/tests/unit/test_frontend_vehicle_state_sync_contract.py b/tests/unit/test_frontend_vehicle_state_sync_contract.py
index 5f8d124..62a108b 100644
--- a/tests/unit/test_frontend_vehicle_state_sync_contract.py
+++ b/tests/unit/test_frontend_vehicle_state_sync_contract.py
@@ -47,6 +47,49 @@ class FrontendVehicleStateSyncContractTests(unittest.TestCase):
self.extension_source,
)
+ def test_shared_twin_theme_tracks_demo_role_and_extension_is_always_user(self) -> None:
+ self.assertIn("function applyTwinSceneTheme(", self.app_source)
+ self.assertIn('canvas.dataset.themeRole = themeRole', self.app_source)
+ self.assertIn(
+ 'canvas.dataset.themeAccent = `#${palette.accent.toString(16).padStart(6, "0")}`',
+ self.app_source,
+ )
+ self.assertIn("accent: 0x0e7773", self.app_source)
+ self.assertIn("accent: 0x315eaa", self.app_source)
+ self.assertIn(
+ 'if (document.body.dataset.module === "self-extension") return "user"',
+ self.app_source,
+ )
+ self.assertIn(
+ 'const themeRole = document.body.dataset.module === "self-extension"',
+ self.app_source,
+ )
+ self.assertIn(
+ 'window.addEventListener("cognitive-agent:demo-role-change"',
+ self.app_source,
+ )
+
+ def test_shared_twin_ground_uses_theme_radial_alpha_texture(self) -> None:
+ self.assertIn("function createTwinGroundTexture(", self.app_source)
+ self.assertIn("new THREE.PlaneGeometry(30, 30)", self.app_source)
+ self.assertIn(
+ 'surfaceGradient.addColorStop(0, "rgba(255, 255, 255, 1)")',
+ self.app_source,
+ )
+ self.assertIn(
+ 'surfaceGradient.addColorStop(1, "rgba(255, 255, 255, 0)")',
+ self.app_source,
+ )
+ self.assertIn("const outerRadius = size * 0.48", self.app_source)
+ self.assertIn(
+ "const gridColor = accent.clone().lerp(new THREE.Color(0xffffff), 0.52)",
+ self.app_source,
+ )
+ self.assertIn("gridContext.lineWidth = major ? 0.9 : 0.45", self.app_source)
+ self.assertIn('gridContext.globalCompositeOperation = "destination-in"', self.app_source)
+ self.assertIn("applyTwinGroundTheme(twin, palette)", self.app_source)
+ self.assertNotIn("new THREE.GridHelper(10, 20", self.app_source)
+
def test_lan_http_origin_does_not_require_crypto_random_uuid(self) -> None:
id_source = function_source(
self.app_source,
diff --git a/tests/unit/test_self_repair.py b/tests/unit/test_self_repair.py
index b52694f..6271b0b 100644
--- a/tests/unit/test_self_repair.py
+++ b/tests/unit/test_self_repair.py
@@ -4,6 +4,8 @@ import json
import time
from pathlib import Path
+import pytest
+
from cognitive_agent.application.self_repair.healing_model import (
PHASE_DIAGNOSED,
PHASE_INJECTED,
@@ -75,6 +77,30 @@ def test_service_is_safe_without_machine_specific_tool_paths() -> None:
assert service.run_self_test() is True
+def test_runtime_must_start_before_fault_injection() -> None:
+ service = SelfRepairService()
+
+ with pytest.raises(ValueError, match="请先启动座椅连续运行"):
+ service.start_healing({
+ "case_id": "FI-FIELD-01",
+ "execution_mode": "shadow",
+ })
+
+ runtime = service.set_runtime({"active": True, "execution_mode": "shadow"})
+ assert runtime["active"] is True
+ assert runtime["motion_profile"] == "四电机协同自由运动"
+ assert runtime["events"][0]["title"] == "座椅自由运动任务已启动"
+
+ healing = service.start_healing({
+ "case_id": "FI-FIELD-01",
+ "execution_mode": "shadow",
+ })
+ assert healing["case_id"] == "FI-FIELD-01"
+
+ snapshot = service.snapshot()
+ assert snapshot["runtime"]["injected_count"] == 1
+
+
def test_bridge_manager_does_not_arm_visible_tooling_by_default(tmp_path: Path) -> None:
manager = ZcanBridgeManager(
tmp_path / "bridge.json",