Skip to content

Commit c624f4c

Browse files
committed
fix(ai): v0.12.0 新增代码审查修复 4 项
审查发现(按严重度): - HIGH: load_session_vision_images 过滤器漏 .webp——会话屏卡图为 {ts}.webp(image_store 落库口径),视觉精修永远取不到图(功能死路);修复:支持 webp + 按时间戳取最近 3 张 (原全量装载数百张 → 每切片携带全部图 → 内存/带宽/token 失控) - MEDIUM: 视频全帧 OCR 下线后 last_full_ocr_at 不再刷新(原全帧 OCR 成功路径写入)—— force_full 的 2s 冷却基准恒满足 → 连续带外变化期逐帧强制全帧分支;修复:关键帧 处理分支恢复刷新(保持冷却语义) - LOW: WGC 分支 expect()(不变量假设)→ 改 if-let 防御降级,不 panic(AGENTS.md §4) - LOW: CaptureOverlayPanel 键盘监听 [sel] 依赖拖拽 mousemove 高频重注册 + 闭包陈旧 → 单次注册 + ref 读最新值 审核确认通过项:WebviewUrl::App("index.html?float=1") 经 url.join 保留 query( tauri 2.11.5 源码核实,M6/M3 路由正确);M1 Empty 分支对字幕块无回归( screens::filter_usable_blocks 只保留 region==full,旧路径同样过滤 subtitle 块)。
1 parent 7e92a36 commit c624f4c

4 files changed

Lines changed: 78 additions & 31 deletions

File tree

‎app/src-tauri/src/ai_refine_task.rs‎

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -295,28 +295,47 @@ fn slice_summary(text: &str, head: bool) -> Option<String> {
295295
}
296296
}
297297

298-
/// v0.12.0 M5:装载会话屏卡图(≤1280px 控 token → PNG base64 data URI)。
298+
/// v0.12.0 M5:装载会话屏卡图(≤1280px 控 token → WebP/PNG base64 data URI)。
299299
///
300300
/// @ai-context: 仅 vision_refine_enabled 开启时调用;目录缺失/解码失败 → 跳过
301301
/// (空列表 → 精修纯文本,现有行为零变化——画面理解是可选增强,
302302
/// 不是硬依赖,绝不阻塞精修主链路)。图片库为
303-
/// data_dir/session-images/<id> 下平铺 PNG/JPEG 屏卡。
303+
/// data_dir/session-images/<id> 下平铺 `{timestamp_ms}.webp`(存图
304+
/// 口径——审查修复:旧过滤器漏 webp 导致取不到图)。
305+
/// @ai-context: 数量上限 MAX_IMAGES=3(审查修复):会话屏卡可数百张,全量装载 →
306+
/// 每切片请求携带全部图 → 内存/带宽/token 失控;按时间戳取最近 3 张
307+
/// (画面要点随精修的最新上下文,时序语义正确)。
304308
/// @ai-context: 隐私红线(ADR-010 扩展):图片仅随精修切片请求上云,不作独立
305309
/// 提取命令;缩放控 token(长图缩放不裁剪,保画面完整)。
306310
fn load_session_vision_images(data_dir: &std::path::Path, session_id: i64) -> Vec<String> {
307311
const MAX_EDGE: u32 = 1280;
312+
const MAX_IMAGES: usize = 3;
308313
let dir = data_dir.join("session-images").join(session_id.to_string());
309314
let Ok(entries) = std::fs::read_dir(&dir) else { return Vec::new() };
315+
// 文件名 = 时间戳({ts}.webp)→ 按数值排序取最近 N 张(字典序对变长数字不成立,
316+
// 须解析数值——"1000" 字典序 < "999")
317+
let mut names: Vec<String> = entries
318+
.flatten()
319+
.filter_map(|e| {
320+
let p = e.path();
321+
if !p.is_file() {
322+
return None;
323+
}
324+
let ext = p.extension().and_then(|x| x.to_str()).unwrap_or("").to_lowercase();
325+
if ext != "webp" && ext != "png" && ext != "jpg" && ext != "jpeg" {
326+
return None;
327+
}
328+
Some(p.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default())
329+
})
330+
.collect();
331+
names.sort_by_key(|n| {
332+
n.split('.').next().and_then(|s| s.parse::<u64>().ok()).unwrap_or(0)
333+
});
334+
names.reverse();
335+
names.truncate(MAX_IMAGES);
310336
let mut out = Vec::new();
311-
for entry in entries.flatten() {
312-
let path = entry.path();
313-
if !path.is_file() {
314-
continue;
315-
}
316-
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("").to_lowercase();
317-
if ext != "png" && ext != "jpg" && ext != "jpeg" {
318-
continue;
319-
}
337+
for name in names {
338+
let path = dir.join(&name);
320339
let Ok(frame) = image::open(&path) else { continue };
321340
// 长边 >1280 按比例缩放(保比例;小图原样——控 token 同时不糊画面)。
322341
// resize 返回 RgbaImage,与 to_rgba8() 同型。

‎app/src-tauri/src/capture/dxgi_capture.rs‎

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -195,22 +195,35 @@ impl ScreenCaptureSampler {
195195
let elapsed_ms = self.started.elapsed().as_millis() as u64;
196196
// WGC 主路径(窗口级——帧即窗口内容,不做窗口矩形裁剪)
197197
if self.backend == CaptureBackend::Wgc {
198-
match self.wgc.as_mut().expect("backend=Wgc 必有 wgc").capture(elapsed_ms) {
199-
Ok(Some(mut frame)) => {
200-
crop_frame(&mut frame.bgraw, &mut frame.width, &mut frame.height, crop);
201-
return Ok(Some(frame));
202-
}
203-
Ok(None) => return Ok(None),
204-
Err(e) => {
205-
// WGC 失效(窗口关闭/最小化/会话丢失)→ 降级 DXGI(一次性,
206-
// 不回切 WGC——YAGNI,沿用 DXGI 周期重建)
207-
eprintln!("[ScreenCapture] WGC 失效,降级 DXGI: {}", e);
208-
self.wgc = None;
198+
match self.wgc.as_mut() {
199+
Some(wgc) => match wgc.capture(elapsed_ms) {
200+
Ok(Some(mut frame)) => {
201+
crop_frame(&mut frame.bgraw, &mut frame.width, &mut frame.height, crop);
202+
return Ok(Some(frame));
203+
}
204+
Ok(None) => return Ok(None),
205+
Err(e) => {
206+
// WGC 失效(窗口关闭/最小化/会话丢失)→ 降级 DXGI(一次性,
207+
// 不回切 WGC——YAGNI,沿用 DXGI 周期重建)
208+
eprintln!("[ScreenCapture] WGC 失效,降级 DXGI: {}", e);
209+
self.wgc = None;
210+
self.dxgi = DxgiState::create(self.hwnd).ok();
211+
self.backend = if self.dxgi.is_some() {
212+
CaptureBackend::Dxgi
213+
} else {
214+
eprintln!("[ScreenCapture] DXGI 重建失败,降级 GDI");
215+
CaptureBackend::Gdi
216+
};
217+
}
218+
},
219+
// 防御(审查修复):backend 标记与 wgc 状态失配(不变量,不可达)
220+
// ——不 panic(AGENTS.md 防御性编程),保守降级 DXGI
221+
None => {
222+
eprintln!("[ScreenCapture] WGC 状态失配,降级 DXGI");
209223
self.dxgi = DxgiState::create(self.hwnd).ok();
210224
self.backend = if self.dxgi.is_some() {
211225
CaptureBackend::Dxgi
212226
} else {
213-
eprintln!("[ScreenCapture] DXGI 重建失败,降级 GDI");
214227
CaptureBackend::Gdi
215228
};
216229
}

‎app/src-tauri/src/live_frame_process.rs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,7 +464,11 @@ pub fn process_frame(
464464
// v0.12.0 M5(视频会话全帧 OCR 下线):全帧画面要点不再识别——关键帧纯图
465465
// 归档(handle_full_frame 空块走 grid-change 触发,存图不识别;真实画面
466466
// 要点经 vision-exp 精修提取)。前台非目标窗口期间不归档(REQ-157)。
467+
// 审查修复:全帧处理时刻刷新 last_full_ocr_at——force_full 的 2s 冷却基准
468+
// (EVENT_FULL_OCR_COOLDOWN_MS)依赖该字段,旧实现每次全帧 OCR 成功刷新;
469+
// 全帧 OCR 下线后若不刷新,冷却恒满足 → 连续带外变化期逐帧强制全帧分支
467470
if !roi_tracker.foreground_foreign() {
471+
trigger.last_full_ocr_at = Instant::now();
468472
crate::live_keyframes::handle_full_frame(
469473
&frame, &[], db, app, session_id, last_full_texts, frame_samples,
470474
last_archived_text, last_archived_at, image_store, ocr_input_hash,

‎app/src/components/CaptureOverlayPanel.tsx‎

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,14 @@ export default function CaptureOverlayPanel() {
2626
const imgRef = useRef<HTMLImageElement>(null);
2727
const scaleRef = useRef<{ x: number; y: number }>({ x: 1, y: 1 });
2828

29+
// 加载待选截图(仅一次)
2930
useEffect(() => {
3031
void invoke<string | null>("overlay_get_image")
3132
.then((p) => {
3233
if (p) setSrc(convertFileSrc(p));
3334
})
3435
.catch(() => undefined);
35-
const onKey = (e: KeyboardEvent) => {
36-
if (e.key === "Escape") void invoke("overlay_cancel").catch(() => undefined);
37-
if (e.key === "Enter" && sel) void submit();
38-
};
39-
window.addEventListener("keydown", onKey);
40-
return () => window.removeEventListener("keydown", onKey);
41-
// eslint-disable-next-line react-hooks/exhaustive-deps
42-
}, [sel]);
36+
}, []);
4337

4438
const onLoad = () => {
4539
const img = imgRef.current;
@@ -75,6 +69,23 @@ export default function CaptureOverlayPanel() {
7569
void invoke("overlay_submit_capture", { rect: r }).catch(() => undefined);
7670
};
7771

72+
// 键盘(Esc/Enter)单次注册——经 ref 读最新 sel/submit
73+
// (审查修复:原 [sel] 依赖在拖拽 mousemove 高频重注册 + 闭包陈旧风险)
74+
const selRef = useRef<Sel | null>(null);
75+
const submitRef = useRef<() => void>(() => {});
76+
useEffect(() => {
77+
selRef.current = sel;
78+
submitRef.current = submit;
79+
});
80+
useEffect(() => {
81+
const onKey = (e: KeyboardEvent) => {
82+
if (e.key === "Escape") void invoke("overlay_cancel").catch(() => undefined);
83+
if (e.key === "Enter" && selRef.current) submitRef.current();
84+
};
85+
window.addEventListener("keydown", onKey);
86+
return () => window.removeEventListener("keydown", onKey);
87+
}, []);
88+
7889
const onMouseDown = (e: React.MouseEvent) => {
7990
dragging.current = true;
8091
setSel({ x1: e.clientX, y1: e.clientY, x2: e.clientX, y2: e.clientY });

0 commit comments

Comments
 (0)