Skip to content

Commit 05b237e

Browse files
committed
refactor(frontend): NotesPage 组件拆分与质量清扫批次落地
1 parent 07b4cca commit 05b237e

42 files changed

Lines changed: 2933 additions & 1774 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎app/package-lock.json‎

Lines changed: 373 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎app/package.json‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@
77
"dev": "vite",
88
"build": "tsc && vite build",
99
"preview": "vite preview",
10+
"test": "vitest run",
1011
"tauri": "tauri"
1112
},
1213
"dependencies": {
1314
"@tauri-apps/api": "^2",
1415
"@tauri-apps/plugin-dialog": "^2.7.2",
15-
"@tauri-apps/plugin-opener": "^2",
1616
"@types/katex": "^0.16.8",
1717
"katex": "^0.18.4",
1818
"react": "^19.1.0",
@@ -28,6 +28,7 @@
2828
"@types/react-dom": "^19.1.6",
2929
"@vitejs/plugin-react": "^4.6.0",
3030
"typescript": "~5.8.3",
31-
"vite": "^7.0.4"
31+
"vite": "^7.0.4",
32+
"vitest": "^4.1.11"
3233
}
3334
}

‎app/src/App.tsx‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,10 @@ function App() {
128128
if (ok) {
129129
// 停止采集(失败也继续尝试关闭——无活动会话时 CloseRequested 直接放行;
130130
// 若会话仍存活则再次拦截弹框,用户可二次决定)
131-
await invoke("stop_live_session").catch(() => {});
131+
// Low 清扫:不吞异常——失败时记录上下文便于诊断(不阻断退出流程)
132+
await invoke("stop_live_session").catch((e) => {
133+
console.warn("[App] app:close-requested 停止采集失败(继续关闭):", e);
134+
});
132135
await getCurrentWindow().close();
133136
}
134137
}),

‎app/src/components/AiRefineCard.tsx‎

Lines changed: 18 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@
88
* @ai-context: 降级可见:任务失败按原因四类展示引导(未授权→设置密钥/授权、
99
* 网络→重试、余额→充值、配额→明日再试),本地规则版保留。
1010
*/
11-
import { useCallback, useEffect, useRef, useState } from "react";
11+
import { useCallback, useEffect, useState } from "react";
1212
import { invoke } from "@tauri-apps/api/core";
13-
import { listen } from "@tauri-apps/api/event";
13+
// M8:轮询/事件双通道/卡住检测抽入共用 hook(与 EnrichPanel 同源)
14+
import { useAiTaskPolling } from "../hooks/useAiTaskPolling";
15+
import { failureGuide } from "./aiTaskFailure";
16+
import type { AiTaskFailureLike } from "./aiTaskFailure";
1417
import type {
1518
AiRefineResult,
1619
AiSettingsView,
@@ -22,20 +25,6 @@ import type {
2225

2326
const btn: React.CSSProperties = { padding: "5px 10px", cursor: "pointer", fontSize: 12, borderRadius: 6 };
2427

25-
/** 任务失败原因 → 引导文案(REQ-145 四类出口) */
26-
function failureGuide(failure: AiTaskFailureLike): string {
27-
const [kind, msg] = Object.entries(failure)[0] ?? ["other", "未知错误"];
28-
switch (kind) {
29-
case "unauthorized": return `未授权:${msg}(请到设置页配置密钥并开启 AI 功能)`;
30-
case "network": return `网络错误:${msg}(可重试)`;
31-
case "balance": return `余额不足:${msg}(请充值或切换免费档模型)`;
32-
case "quota": return `配额受限:${msg}(请明日再试)`;
33-
case "invalid": return `响应非法已丢弃:${msg}(本地规则版保留)`;
34-
default: return msg;
35-
}
36-
}
37-
type AiTaskFailureLike = Record<string, string>;
38-
3928
/** diff 三态渲染(本地版为基线:删除红划线/新增绿底/未变灰) */
4029
function DiffLine({ op }: { op: DiffOp }) {
4130
if ("added" in op) {
@@ -57,55 +46,23 @@ export default function AiRefineCard({ sessionId, onApplied }: { sessionId: numb
5746
const [failure, setFailure] = useState<AiTaskFailureLike | null>(null);
5847
const [remember, setRemember] = useState(false);
5948
const [msg, setMsg] = useState("");
60-
const polling = useRef<ReturnType<typeof setInterval> | null>(null);
61-
// 审查修复(2026-08-21 真机 debug):taskId 与 handleState 用 ref 持有——
62-
// 事件/轮询回调在任务期间多次触发,若闭包捕获旧渲染版本(taskId=null),
63-
// 任务成功时 ai_refine_result 以 null 查询会失败 → 永久卡"排队中"
6449
const [, setTaskId] = useState<number | null>(null);
65-
const taskIdRef = useRef<number | null>(null);
66-
const handleStateRef = useRef<(st: AiTaskState) => Promise<void>>(async () => {});
67-
// 卡住检测(2026-08-21 真机"排队中"排查):任务 30s 仍 Pending → 提示查看日志
68-
const lastChangeRef = useRef(0);
69-
70-
// 事件通道(ai:task-update)——与轮询双通道,事件优先即时;订阅一次,
71-
// 回调经 handleStateRef 取最新实现(无闭包过期)
72-
useEffect(() => {
73-
const un = listen<[number, AiTaskState]>("ai:task-update", (e) => {
74-
const [tid, st] = e.payload;
75-
if (tid !== taskIdRef.current) return;
76-
void handleStateRef.current(st);
77-
});
78-
return () => {
79-
un.then((f) => f());
80-
};
81-
}, []);
82-
83-
// 审查修复(2026-08-21):组件卸载时停止轮询——否则 interval 持续 invoke
84-
// 并对已卸载组件 setState(切会话/关面板后泄漏)
85-
useEffect(() => {
86-
return () => {
87-
if (polling.current) {
88-
clearInterval(polling.current);
89-
polling.current = null;
90-
}
91-
};
92-
}, []);
9350

9451
useEffect(() => {
9552
void invoke<AiSettingsView>("ai_get_settings").then(setSettings).catch(() => undefined);
9653
}, []);
9754

98-
const handleState = useCallback(async (st: AiTaskState) => {
99-
const tid = taskIdRef.current;
55+
// M8:任务状态派发(纯业务)——轮询/事件/卡住检测/卸载清理均由
56+
// useAiTaskPolling 承担;taskId 以参数传入(原实现靠 ref 镜像防闭包过期,
57+
// 现由 hook 内部 ref 统一保障)
58+
const handleState = useCallback(async (st: AiTaskState, tid: number | null) => {
10059
if (st === "Succeeded") {
101-
stopPolling();
10260
const r = await invoke<AiRefineResult>("ai_refine_result", { taskId: tid }).catch(() => null);
10361
if (r) {
10462
setResult(r);
10563
setPhase("done");
10664
}
10765
} else if (typeof st === "object" && st !== null && "Failed" in st) {
108-
stopPolling();
10966
setFailure(st.Failed.reason as AiTaskFailureLike);
11067
setPhase("failed");
11168
} else if (typeof st === "object" && st !== null && "Running" in st) {
@@ -114,38 +71,12 @@ export default function AiRefineCard({ sessionId, onApplied }: { sessionId: numb
11471
}
11572
}, []);
11673

117-
// 同步最新 handleState 到 ref(事件/轮询回调总用最新实现)
118-
useEffect(() => {
119-
handleStateRef.current = handleState;
120-
}, [handleState]);
121-
122-
const stopPolling = () => {
123-
if (polling.current) {
124-
clearInterval(polling.current);
125-
polling.current = null;
126-
}
127-
};
128-
129-
const poll = (id: number) => {
130-
stopPolling();
131-
lastChangeRef.current = Date.now();
132-
polling.current = setInterval(async () => {
133-
const st = await invoke<AiTaskState>("ai_refine_status", { taskId: id }).catch(() => null);
134-
if (st) {
135-
// 状态有进展(Running/Succeeded/Failed)→ 刷新时间戳;仅 Pending 计入卡住窗口
136-
if (st !== "Pending") lastChangeRef.current = Date.now();
137-
void handleStateRef.current(st);
138-
}
139-
// 卡住检测:长时间仍 Pending = 任务未启动或后台卡死(tauri 终端看 [refine-task] 日志)
140-
if (Date.now() - lastChangeRef.current > 30_000) {
141-
stopPolling();
142-
taskIdRef.current = null; // 隔离旧任务后续事件
143-
setTaskId(null);
144-
setPhase("idle");
145-
setMsg("任务 30 秒无进展(可能未启动或后台卡住)——请查看 tauri 终端 [refine-task] 日志后重试");
146-
}
147-
}, 1500);
148-
};
74+
const { taskIdRef, startPolling, stopPolling } = useAiTaskPolling(handleState, () => {
75+
// 30s 卡住 UI 复位(文案与抽取前一致)
76+
setTaskId(null);
77+
setPhase("idle");
78+
setMsg("任务 30 秒无进展(可能未启动或后台卡住)——请查看 tauri 终端 [refine-task] 日志后重试");
79+
});
14980

15081
/** ① 预估 + 余额(确认弹窗数据源) */
15182
const prepare = async () => {
@@ -205,8 +136,8 @@ export default function AiRefineCard({ sessionId, onApplied }: { sessionId: numb
205136
if (handle) {
206137
taskIdRef.current = handle.taskId;
207138
setTaskId(handle.taskId);
208-
void handleState(handle.state);
209-
poll(handle.taskId);
139+
void handleState(handle.state, handle.taskId);
140+
startPolling(handle.taskId);
210141
}
211142
};
212143

@@ -330,7 +261,7 @@ export default function AiRefineCard({ sessionId, onApplied }: { sessionId: numb
330261
{/* 失败(降级可见 + 原因引导 + 重试) */}
331262
{phase === "failed" && failure && (
332263
<div style={{ fontSize: 12, color: "#b91c1c", marginBottom: 6 }}>
333-
❌ {failureGuide(failure)}
264+
❌ {failureGuide(failure, "本地规则版保留")}
334265
<div style={{ marginTop: 6 }}>
335266
<button style={{ ...btn, border: "1px solid #d1d5db" }} onClick={reset}>重试</button>
336267
<span style={{ marginLeft: 8, color: "#6b7280" }}>本地规则版保留(不丢不假)</span>

‎app/src/components/BoxSelectOverlay.tsx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,13 +121,15 @@ export default function BoxSelectOverlay({ src, sessionId, firstSeenMs, onDone,
121121
: {};
122122

123123
// CSS 放大镜预览:以框选区域为视口显示原图(确认浮层)
124+
// Low 清扫:框贴近右/下缘时 1-w/1-h → 0 导致除零(NaN%/Infinity%)——
125+
// clamp 分母下限 0.05,语义上预览偏移封顶,行为不变但数值安全
124126
const zoomStyle: React.CSSProperties = confirm
125127
? {
126128
width: 240,
127129
height: 160,
128130
backgroundImage: `url(${src})`,
129131
backgroundSize: `${100 / confirm.w}% ${100 / confirm.h}%`,
130-
backgroundPosition: `${(confirm.x / (1 - confirm.w)) * 100}% ${(confirm.y / (1 - confirm.h)) * 100}%`,
132+
backgroundPosition: `${(confirm.x / Math.max(1 - confirm.w, 0.05)) * 100}% ${(confirm.y / Math.max(1 - confirm.h, 0.05)) * 100}%`,
131133
backgroundRepeat: "no-repeat",
132134
border: "1px solid #e5e7eb",
133135
borderRadius: 6,

0 commit comments

Comments
 (0)