Check live FIR enable state before each analysis
Deploy B9 / deploy (push) Successful in 3m36s

This commit is contained in:
fucheng
2026-11-05 14:33:36 +08:00
parent 4b3b86f256
commit d20d2bb11e
4 changed files with 59 additions and 5 deletions
+16
View File
@@ -40,6 +40,22 @@ export function validatePair(pair: FirPair) {
encodeFir(pair.left);
encodeFir(pair.right);
}
export async function requireFirEnabled(
api: Pick<LuxsinAPI, "getDeviceState">
): Promise<void> {
// 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<LuxsinAPI, "getDeviceState" | "getFirChannel">
): Promise<FirPair> {
await requireFirEnabled(api);
return readCurrentFir(api);
}
export async function readCurrentFir(
api: Pick<LuxsinAPI, "getFirChannel">
): Promise<FirPair> {
+1 -1
View File
@@ -383,7 +383,7 @@ export class LuxsinAPI {
async getDeviceState(): Promise<DeviceState> {
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;
+9 -4
View File
@@ -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<string | null>(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,
},
+33
View File
@@ -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/);
});