This commit is contained in:
@@ -40,6 +40,22 @@ export function validatePair(pair: FirPair) {
|
|||||||
encodeFir(pair.left);
|
encodeFir(pair.left);
|
||||||
encodeFir(pair.right);
|
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(
|
export async function readCurrentFir(
|
||||||
api: Pick<LuxsinAPI, "getFirChannel">
|
api: Pick<LuxsinAPI, "getFirChannel">
|
||||||
): Promise<FirPair> {
|
): Promise<FirPair> {
|
||||||
|
|||||||
@@ -383,7 +383,7 @@ export class LuxsinAPI {
|
|||||||
|
|
||||||
async getDeviceState(): Promise<DeviceState> {
|
async getDeviceState(): Promise<DeviceState> {
|
||||||
try {
|
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 text = await response.text();
|
||||||
const decoded = decodeCustomBase64(text.trim());
|
const decoded = decodeCustomBase64(text.trim());
|
||||||
const state = JSON.parse(decoded) as DeviceState;
|
const state = JSON.parse(decoded) as DeviceState;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import AiFirCard from '@/components/AiFirCard';
|
import AiFirCard from '@/components/AiFirCard';
|
||||||
import {aiFirText} from '@/lib/aiFirLocale';
|
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
|
AI PAGE — Luxsin X9 AI Assistant
|
||||||
- 首次进入: 中央 Logo + 建议气泡,引导用户如何提问
|
- 首次进入: 中央 Logo + 建议气泡,引导用户如何提问
|
||||||
@@ -260,11 +260,13 @@ export default function AIPage({ variant = "page", onClose }: AIPageProps = {})
|
|||||||
try {
|
try {
|
||||||
if(block.name==='get_current_fir') {
|
if(block.name==='get_current_fir') {
|
||||||
firSnapshot.current=null;
|
firSnapshot.current=null;
|
||||||
const pair=await readCurrentFir(api);
|
setFirProposal(null);
|
||||||
|
const pair=await readEnabledCurrentFir(api);
|
||||||
if(firDeviceRef.current!==device) throw new Error('Device changed');
|
if(firDeviceRef.current!==device) throw new Error('Device changed');
|
||||||
const id=makeId();firSnapshot.current={id,pair};
|
const id=makeId();firSnapshot.current={id,pair};
|
||||||
return {ok:true,content:{snapshot_id:id,...firMetrics(pair)}};
|
return {ok:true,content:{snapshot_id:id,...firMetrics(pair)}};
|
||||||
}
|
}
|
||||||
|
await requireFirEnabled(api);
|
||||||
const a=block.input as FirAdjustment;
|
const a=block.input as FirAdjustment;
|
||||||
const snapshot=firSnapshot.current;
|
const snapshot=firSnapshot.current;
|
||||||
if(!snapshot || a.snapshot_id!==snapshot.id) throw new Error('Read current FIR in this turn before proposing an adjustment');
|
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);
|
setFirCardVisible(true);
|
||||||
setFirProposal({id:makeId(),device,before:snapshot.pair,after,adjustment:a,created:new Date().toISOString()});
|
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.'}};
|
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);
|
const [chatId, setChatId] = useState<string | null>(null);
|
||||||
@@ -534,7 +536,10 @@ export default function AIPage({ variant = "page", onClose }: AIPageProps = {})
|
|||||||
// 组装新版 /sse/question 请求体
|
// 组装新版 /sse/question 请求体
|
||||||
const buildAndStream = async () => {
|
const buildAndStream = async () => {
|
||||||
let devicePeq: any;
|
let devicePeq: any;
|
||||||
|
let latestDeviceState;
|
||||||
try {
|
try {
|
||||||
|
latestDeviceState = await api.getDeviceState();
|
||||||
|
if (firDeviceRef.current !== mac) throw new Error("Device changed");
|
||||||
const peqState = await api.getPeqState();
|
const peqState = await api.getPeqState();
|
||||||
devicePeq = {
|
devicePeq = {
|
||||||
peqSelect: peqState.peqSelect ?? deviceState.peqSelect ?? 0,
|
peqSelect: peqState.peqSelect ?? deviceState.peqSelect ?? 0,
|
||||||
@@ -566,7 +571,7 @@ export default function AIPage({ variant = "page", onClose }: AIPageProps = {})
|
|||||||
const controller = streamQuestion(
|
const controller = streamQuestion(
|
||||||
{
|
{
|
||||||
question: trimmed,
|
question: trimmed,
|
||||||
device_setting: { ...deviceState },
|
device_setting: { ...latestDeviceState },
|
||||||
device_peq: devicePeq,
|
device_peq: devicePeq,
|
||||||
chat_id: currentChatId,
|
chat_id: currentChatId,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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/);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user