@@ -144,6 +144,94 @@ pub fn list_group_fragments(state: State<'_, AppState>, group_id: i64) -> Result
144144 state. db . list_fragments_by_group ( group_id) . map_err ( |e| e. to_string ( ) )
145145}
146146
147+ /// 移动碎片到组(None=移出组;用户纠错/重新归组——REQ-201 消费闭环)。
148+ ///
149+ /// @ai-context: 与 capture_fragment 同开关准入(feed 能力默认关纪律对称——
150+ /// 后端不信前端隐藏);目标组存在性校验(不写孤儿引用)。
151+ #[ tauri:: command]
152+ pub fn update_fragment_group (
153+ state : State < ' _ , AppState > ,
154+ fragment_id : i64 ,
155+ group_id : Option < i64 > ,
156+ ) -> Result < bool , String > {
157+ require_feed_enabled ( & state) ?;
158+ if fragment_id <= 0 {
159+ return Err ( "无效的碎片 id" . to_string ( ) ) ;
160+ }
161+ if state. db . get_fragment ( fragment_id) . map_err ( |e| e. to_string ( ) ) ?. is_none ( ) {
162+ return Err ( format ! ( "碎片不存在: {}" , fragment_id) ) ;
163+ }
164+ if let Some ( gid) = group_id {
165+ if gid <= 0 {
166+ return Err ( "无效的组 id" . to_string ( ) ) ;
167+ }
168+ if state. db . get_group ( gid) . map_err ( |e| e. to_string ( ) ) ?. is_none ( ) {
169+ return Err ( format ! ( "笔记组不存在: {}" , gid) ) ;
170+ }
171+ }
172+ state
173+ . db
174+ . update_fragment_group ( fragment_id, group_id)
175+ . map_err ( |e| e. to_string ( ) )
176+ }
177+
178+ /// 删除碎片(REQ-201 用户主动删除——真删;绑定卡自动解绑保留)。
179+ ///
180+ /// @ai-context: 开关准入同 capture_fragment(feed 能力对称纪律);存在性校验
181+ /// 前置(删不存在的碎片返回明确错误而非静默 false)。
182+ #[ tauri:: command]
183+ pub fn delete_fragment ( state : State < ' _ , AppState > , fragment_id : i64 ) -> Result < bool , String > {
184+ require_feed_enabled ( & state) ?;
185+ if fragment_id <= 0 {
186+ return Err ( "无效的碎片 id" . to_string ( ) ) ;
187+ }
188+ if state. db . get_fragment ( fragment_id) . map_err ( |e| e. to_string ( ) ) ?. is_none ( ) {
189+ return Err ( format ! ( "碎片不存在: {}" , fragment_id) ) ;
190+ }
191+ state. db . delete_fragment ( fragment_id) . map_err ( |e| e. to_string ( ) )
192+ }
193+
194+ /// 碎片图片 → 本地绝对路径(前端 convertFileSrc 消费;REQ-201 缩略图)。
195+ ///
196+ /// @ai-context: resolve 先例同 resolve_note_image——WebView 不能直接读本地文件;
197+ /// 本命令从库读 image_path 而非信任前端传参(防任意路径穿越),
198+ /// 只放行 fragments/ 前缀;无图 → None(UI 降级文本预览)。
199+ #[ tauri:: command]
200+ pub fn resolve_fragment_image (
201+ state : State < ' _ , AppState > ,
202+ fragment_id : i64 ,
203+ ) -> Result < Option < String > , String > {
204+ if fragment_id <= 0 {
205+ return Err ( "无效的碎片 id" . to_string ( ) ) ;
206+ }
207+ let frag = state
208+ . db
209+ . get_fragment ( fragment_id)
210+ . map_err ( |e| e. to_string ( ) ) ?
211+ . ok_or_else ( || format ! ( "碎片不存在: {}" , fragment_id) ) ?;
212+ let Some ( rel) = frag. image_path else { return Ok ( None ) } ;
213+ // 只放行 fragments/ 前缀 + 防穿越(落盘时的白名单格式已由 image crate 把关)
214+ let rel_trim = rel. trim_start_matches ( [ '/' , '\\' ] ) ;
215+ if !rel_trim. starts_with ( "fragments/" ) || rel_trim. split ( [ '/' , '\\' ] ) . any ( |seg| seg == ".." ) {
216+ eprintln ! ( "[fragments] 异常图片路径被拒: {}" , rel) ;
217+ return Ok ( None ) ;
218+ }
219+ let abs = state. data_dir . join ( rel_trim) ;
220+ Ok ( Some ( abs. to_string_lossy ( ) . into_owned ( ) ) )
221+ }
222+
223+ /// feed 能力开关准入(capture/delete/移组共用——后端不信前端隐藏)。
224+ fn require_feed_enabled ( state : & AppState ) -> Result < ( ) , String > {
225+ let guard = state
226+ . feature_flags
227+ . lock ( )
228+ . map_err ( |e| format ! ( "开关锁失败: {}" , e) ) ?;
229+ if !guard. feed_capture {
230+ return Err ( "feed 能力未启用(请在设置中开启 feed 捕获开关)" . to_string ( ) ) ;
231+ }
232+ Ok ( ( ) )
233+ }
234+
147235/// base64 解码 + 解码验证并落盘碎片图片(返回 fragments/ 下相对路径)。
148236///
149237/// @ai-context: image::load_from_memory 把关格式白名单(png/jpeg/webp/gif/bmp…
0 commit comments