Skip to content

Commit 74e9a5c

Browse files
committed
refactor(rust): 拆 ai_refine_task 步 3 — 屏卡图装载
1 parent d2443ce commit 74e9a5c

3 files changed

Lines changed: 85 additions & 69 deletions

File tree

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

Lines changed: 5 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
//! 线程内再开 worker——网络调用阻塞 worker 线程,互不干扰;
1010
//! AiClient 为 Clone+Send,跨线程共享安全)。
1111
12-
use base64::Engine;
1312
use tauri::Emitter;
1413

1514
use crate::ai_chat::AiTurn;
@@ -38,6 +37,11 @@ mod skeleton;
3837
/// 子模块再导出:既有 `crate::ai_refine_task::{…}` 导入路径保持不变。
3938
pub(crate) use self::skeleton::run_refine_task_skeleton;
4039

40+
/// 屏卡图装载子模块(视觉增强的目录时序/缩放口径见 vision.rs 模块头)
41+
#[path = "ai_refine_task/vision.rs"]
42+
mod vision;
43+
use self::vision::load_session_vision_images;
44+
4145
/// 切片并发上限(REQ-145:并发 2-3——配额并发安全由 command 层启动前按
4246
/// 预估片数一次性消耗保证,此处 worker 数不超切片数)。v0.17.0:pub(crate)
4347
/// ——笔记级精修任务共用同一上限。
@@ -265,68 +269,3 @@ fn run_refine_task_inner(
265269
turns,
266270
))
267271
}
268-
269-
/// v0.12.0 M5:装载会话屏卡图(≤1280px 控 token → WebP/PNG base64 data URI)。
270-
///
271-
/// @ai-context: 仅 vision_refine_enabled 开启时调用;目录缺失/解码失败 → 跳过
272-
/// (空列表 → 精修纯文本,现有行为零变化——画面理解是可选增强,
273-
/// 不是硬依赖,绝不阻塞精修主链路)。图片库为
274-
/// data_dir/session-images/<id> 下平铺 `{timestamp_ms}.webp`(存图
275-
/// 口径——审查修复:旧过滤器漏 webp 导致取不到图)。
276-
/// @ai-context: 数量上限 MAX_IMAGES=3(审查修复):会话屏卡可数百张,全量装载 →
277-
/// 每切片请求携带全部图 → 内存/带宽/token 失控;按时间戳取最近 3 张
278-
/// (画面要点随精修的最新上下文,时序语义正确)。
279-
/// @ai-context: 隐私红线(ADR-010 扩展):图片仅随精修切片请求上云,不作独立
280-
/// 提取命令;缩放控 token(长图缩放不裁剪,保画面完整)。
281-
fn load_session_vision_images(data_dir: &std::path::Path, session_id: i64) -> Vec<String> {
282-
const MAX_EDGE: u32 = 1280;
283-
const MAX_IMAGES: usize = 3;
284-
let dir = data_dir.join("session-images").join(session_id.to_string());
285-
let Ok(entries) = std::fs::read_dir(&dir) else { return Vec::new() };
286-
// 文件名 = 时间戳({ts}.webp)→ 按数值排序取最近 N 张(字典序对变长数字不成立,
287-
// 须解析数值——"1000" 字典序 < "999")
288-
let mut names: Vec<String> = entries
289-
.flatten()
290-
.filter_map(|e| {
291-
let p = e.path();
292-
if !p.is_file() {
293-
return None;
294-
}
295-
let ext = p.extension().and_then(|x| x.to_str()).unwrap_or("").to_lowercase();
296-
if ext != "webp" && ext != "png" && ext != "jpg" && ext != "jpeg" {
297-
return None;
298-
}
299-
Some(p.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default())
300-
})
301-
.collect();
302-
names.sort_by_key(|n| {
303-
n.split('.').next().and_then(|s| s.parse::<u64>().ok()).unwrap_or(0)
304-
});
305-
names.reverse();
306-
names.truncate(MAX_IMAGES);
307-
let mut out = Vec::new();
308-
for name in names {
309-
let path = dir.join(&name);
310-
let Ok(frame) = image::open(&path) else { continue };
311-
// 长边 >1280 按比例缩放(保比例;小图原样——控 token 同时不糊画面)。
312-
// resize 返回 RgbaImage,与 to_rgba8() 同型。
313-
let (w, h) = (frame.width(), frame.height());
314-
let max_edge = w.max(h);
315-
let rgb = if max_edge > MAX_EDGE {
316-
let scale = MAX_EDGE as f32 / max_edge as f32;
317-
let nw = (w as f32 * scale).round().max(1.0) as u32;
318-
let nh = (h as f32 * scale).round().max(1.0) as u32;
319-
image::imageops::resize(&frame, nw, nh, image::imageops::FilterType::Triangle)
320-
} else {
321-
frame.to_rgba8()
322-
};
323-
let mut buf = std::io::Cursor::new(Vec::new());
324-
let encoder = image::codecs::png::PngEncoder::new(&mut buf);
325-
if rgb.write_with_encoder(encoder).is_err() {
326-
continue;
327-
}
328-
let b64 = base64::engine::general_purpose::STANDARD.encode(buf.into_inner());
329-
out.push(format!("data:image/png;base64,{}", b64));
330-
}
331-
out
332-
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
//! 会话屏卡图装载(拆分自 ai_refine_task.rs;AGENTS.md §3 单文件 ≤300 行)。
2+
//!
3+
//! @ai-context: 仅 vision_refine_enabled 开启时调用;目录缺失/解码失败 → 跳过(空
4+
//! 列表 → 精修纯文本,现有行为零变化——画面理解是可选增强,绝不阻塞
5+
//! 精修主链路)。图片库为 data_dir/session-images/<id> 下平铺
6+
//! `{timestamp_ms}.webp`(审查修复:旧过滤器漏 webp 导致取不到图)。
7+
//! @ai-context: 数量上限 MAX_IMAGES=3:按时间戳**数值**倒序取最近 3 张(字典序对
8+
//! 变长数字不成立);长边 >1280 按比例缩放(长图缩放不裁剪,保完整)。
9+
//! @ai-context: 隐私红线(ADR-010 扩展):图片仅随精修切片请求上云,不作独立提取
10+
//! 命令;base64 不入轨迹库。
11+
12+
use base64::Engine;
13+
14+
/// v0.12.0 M5:装载会话屏卡图(≤1280px 控 token → WebP/PNG base64 data URI)。
15+
///
16+
/// @ai-context: 仅 vision_refine_enabled 开启时调用;目录缺失/解码失败 → 跳过
17+
/// (空列表 → 精修纯文本,现有行为零变化——画面理解是可选增强,
18+
/// 不是硬依赖,绝不阻塞精修主链路)。图片库为
19+
/// data_dir/session-images/<id> 下平铺 `{timestamp_ms}.webp`(存图
20+
/// 口径——审查修复:旧过滤器漏 webp 导致取不到图)。
21+
/// @ai-context: 数量上限 MAX_IMAGES=3(审查修复):会话屏卡可数百张,全量装载 →
22+
/// 每切片请求携带全部图 → 内存/带宽/token 失控;按时间戳取最近 3 张
23+
/// (画面要点随精修的最新上下文,时序语义正确)。
24+
/// @ai-context: 隐私红线(ADR-010 扩展):图片仅随精修切片请求上云,不作独立
25+
/// 提取命令;缩放控 token(长图缩放不裁剪,保画面完整)。
26+
pub(super) fn load_session_vision_images(data_dir: &std::path::Path, session_id: i64) -> Vec<String> {
27+
const MAX_EDGE: u32 = 1280;
28+
const MAX_IMAGES: usize = 3;
29+
let dir = data_dir.join("session-images").join(session_id.to_string());
30+
let Ok(entries) = std::fs::read_dir(&dir) else { return Vec::new() };
31+
// 文件名 = 时间戳({ts}.webp)→ 按数值排序取最近 N 张(字典序对变长数字不成立,
32+
// 须解析数值——"1000" 字典序 < "999")
33+
let mut names: Vec<String> = entries
34+
.flatten()
35+
.filter_map(|e| {
36+
let p = e.path();
37+
if !p.is_file() {
38+
return None;
39+
}
40+
let ext = p.extension().and_then(|x| x.to_str()).unwrap_or("").to_lowercase();
41+
if ext != "webp" && ext != "png" && ext != "jpg" && ext != "jpeg" {
42+
return None;
43+
}
44+
Some(p.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default())
45+
})
46+
.collect();
47+
names.sort_by_key(|n| {
48+
n.split('.').next().and_then(|s| s.parse::<u64>().ok()).unwrap_or(0)
49+
});
50+
names.reverse();
51+
names.truncate(MAX_IMAGES);
52+
let mut out = Vec::new();
53+
for name in names {
54+
let path = dir.join(&name);
55+
let Ok(frame) = image::open(&path) else { continue };
56+
// 长边 >1280 按比例缩放(保比例;小图原样——控 token 同时不糊画面)。
57+
// resize 返回 RgbaImage,与 to_rgba8() 同型。
58+
let (w, h) = (frame.width(), frame.height());
59+
let max_edge = w.max(h);
60+
let rgb = if max_edge > MAX_EDGE {
61+
let scale = MAX_EDGE as f32 / max_edge as f32;
62+
let nw = (w as f32 * scale).round().max(1.0) as u32;
63+
let nh = (h as f32 * scale).round().max(1.0) as u32;
64+
image::imageops::resize(&frame, nw, nh, image::imageops::FilterType::Triangle)
65+
} else {
66+
frame.to_rgba8()
67+
};
68+
let mut buf = std::io::Cursor::new(Vec::new());
69+
let encoder = image::codecs::png::PngEncoder::new(&mut buf);
70+
if rgb.write_with_encoder(encoder).is_err() {
71+
continue;
72+
}
73+
let b64 = base64::engine::general_purpose::STANDARD.encode(buf.into_inner());
74+
out.push(format!("data:image/png;base64,{}", b64));
75+
}
76+
out
77+
}

0 commit comments

Comments
 (0)