From d20d2bb11e168433d5c7048fc172b44ec4724118 Mon Sep 17 00:00:00 2001 From: fucheng Date: Thu, 5 Nov 2026 14:33:36 +0800 Subject: [PATCH] Check live FIR enable state before each analysis --- client/src/lib/aiFir.ts | 16 ++++++++++++++++ client/src/lib/luxsinApi.ts | 2 +- client/src/pages/AIPage.tsx | 13 +++++++++---- scripts/ai-fir.test.mjs | 33 +++++++++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 5 deletions(-) diff --git a/client/src/lib/aiFir.ts b/client/src/lib/aiFir.ts index 2c64710..82b2131 100644 --- a/client/src/lib/aiFir.ts +++ b/client/src/lib/aiFir.ts @@ -40,6 +40,22 @@ export function validatePair(pair: FirPair) { encodeFir(pair.left); encodeFir(pair.right); } +export async function requireFirEnabled( + api: Pick +): Promise { + // Always query the device: a React state snapshot may predate the FIR toggle. + const state = await api.getDeviceState(); + if (state.fir_enable !== 1) { + throw new Error("FIR 未启用或无法确认开关状态,请先前往 Effect → FIR 手动启用,再重新分析。 / FIR is disabled or its status is unknown. Please manually enable it at Effect → FIR before analyzing."); + } +} +export async function readEnabledCurrentFir( + api: Pick +): Promise { + await requireFirEnabled(api); + return readCurrentFir(api); +} + export async function readCurrentFir( api: Pick ): Promise { diff --git a/client/src/lib/luxsinApi.ts b/client/src/lib/luxsinApi.ts index 0a6d869..91641c2 100644 --- a/client/src/lib/luxsinApi.ts +++ b/client/src/lib/luxsinApi.ts @@ -383,7 +383,7 @@ export class LuxsinAPI { async getDeviceState(): Promise { try { - const response = await fetch(`${this.baseUrl}/dev/info.cgi?action=syncData`); + const response = await fetch(`${this.baseUrl}/dev/info.cgi?action=syncData`, { cache: "no-store" }); const text = await response.text(); const decoded = decodeCustomBase64(text.trim()); const state = JSON.parse(decoded) as DeviceState; diff --git a/client/src/pages/AIPage.tsx b/client/src/pages/AIPage.tsx index e996571..44e8d76 100644 --- a/client/src/pages/AIPage.tsx +++ b/client/src/pages/AIPage.tsx @@ -1,6 +1,6 @@ import AiFirCard from '@/components/AiFirCard'; import {aiFirText} from '@/lib/aiFirLocale'; -import {readCurrentFir, firMetrics, renderFir, type FirPair, type FirProposal, type FirAdjustment} from '@/lib/aiFir'; +import {readEnabledCurrentFir, requireFirEnabled, firMetrics, renderFir, type FirPair, type FirProposal, type FirAdjustment} from '@/lib/aiFir'; /* ============================================================ AI PAGE — Luxsin X9 AI Assistant - 首次进入: 中央 Logo + 建议气泡,引导用户如何提问 @@ -260,11 +260,13 @@ export default function AIPage({ variant = "page", onClose }: AIPageProps = {}) try { if(block.name==='get_current_fir') { firSnapshot.current=null; - const pair=await readCurrentFir(api); + setFirProposal(null); + const pair=await readEnabledCurrentFir(api); if(firDeviceRef.current!==device) throw new Error('Device changed'); const id=makeId();firSnapshot.current={id,pair}; return {ok:true,content:{snapshot_id:id,...firMetrics(pair)}}; } + await requireFirEnabled(api); const a=block.input as FirAdjustment; const snapshot=firSnapshot.current; if(!snapshot || a.snapshot_id!==snapshot.id) throw new Error('Read current FIR in this turn before proposing an adjustment'); @@ -273,7 +275,7 @@ export default function AIPage({ variant = "page", onClose }: AIPageProps = {}) setFirCardVisible(true); setFirProposal({id:makeId(),device,before:snapshot.pair,after,adjustment:a,created:new Date().toISOString()}); return {ok:true,content:{status:'awaiting_user_confirmation',before:firMetrics(snapshot.pair),after:firMetrics(after),message:'FIR arrays staged only. Device unchanged. User must confirm upload in the AI FIR card; backup precedes upload, then A/B is available.'}}; - }catch(error){return {ok:false,message:error instanceof Error?error.message:'FIR failed'};} + }catch(error){firSnapshot.current=null;setFirProposal(null);return {ok:false,message:error instanceof Error?error.message:'FIR failed'};} }; const [chatId, setChatId] = useState(null); @@ -534,7 +536,10 @@ export default function AIPage({ variant = "page", onClose }: AIPageProps = {}) // 组装新版 /sse/question 请求体 const buildAndStream = async () => { let devicePeq: any; + let latestDeviceState; try { + latestDeviceState = await api.getDeviceState(); + if (firDeviceRef.current !== mac) throw new Error("Device changed"); const peqState = await api.getPeqState(); devicePeq = { peqSelect: peqState.peqSelect ?? deviceState.peqSelect ?? 0, @@ -566,7 +571,7 @@ export default function AIPage({ variant = "page", onClose }: AIPageProps = {}) const controller = streamQuestion( { question: trimmed, - device_setting: { ...deviceState }, + device_setting: { ...latestDeviceState }, device_peq: devicePeq, chat_id: currentChatId, }, diff --git a/scripts/ai-fir.test.mjs b/scripts/ai-fir.test.mjs index 039086f..13b9f83 100644 --- a/scripts/ai-fir.test.mjs +++ b/scripts/ai-fir.test.mjs @@ -237,3 +237,36 @@ test('A/B WAV exports preserve coefficients and signed zero', () => { } } }); + + +test("each FIR analysis rechecks live switch, including enabled then disabled", async () => { + let enabled = 1; + const calls = []; + const coefficients = pair(); + const api = { + async getDeviceState() { calls.push("state"); return {fir_enable: enabled}; }, + async getFirChannel(channel) { calls.push(channel); return binary(coefficients[channel]); }, + }; + await fir.readEnabledCurrentFir(api); + assert.deepEqual(calls, ["state", "left", "right"]); + enabled = 0; + calls.length = 0; + await assert.rejects(() => fir.readEnabledCurrentFir(api), /Effect → FIR/); + assert.deepEqual(calls, ["state"]); +}); + +test("unknown or failed FIR switch reads block all coefficient reads", async () => { + for (const getDeviceState of [async () => ({}), async () => { throw new Error("offline"); }]) { + let reads = 0; + await assert.rejects(() => fir.readEnabledCurrentFir({getDeviceState, async getFirChannel() { reads++; }})); + assert.equal(reads, 0); + } +}); + +test("proposal prerequisite also checks live switch every time", async () => { + let enabled = 1; + const api = {async getDeviceState() { return {fir_enable: enabled}; }}; + await fir.requireFirEnabled(api); + enabled = 0; + await assert.rejects(() => fir.requireFirEnabled(api), /Effect → FIR/); +});