@@ -11,11 +11,13 @@ use tauri::ipc::Channel;
1111use tauri:: State ;
1212
1313use crate :: ai_chat:: {
14- AiTurn , CancelFlag , ChatMessageInput , ChatRole , build_messages, trajectory_from_json,
14+ AiTurn , ChatMessageInput , ChatRole , build_messages, trajectory_from_json,
15+ } ;
16+ use crate :: ai_chat_client:: {
17+ end_stream, resolve_chat_client, truncate_chars, try_begin_stream, validate_session,
1518} ;
1619use crate :: ai_chat_stream:: { ChatStreamEvent , stream_chat} ;
1720use crate :: ai_client:: AiClient ;
18- use crate :: ai_provider:: { AiProviderConfig , ProviderKind , provider_scope} ;
1921use crate :: commands:: AppState ;
2022use crate :: db_ai_chat:: { ChatMessage , ChatSession } ;
2123
@@ -26,59 +28,6 @@ const CHAT_SYSTEM_PROMPT: &str =
2628/// 单条消息最大字符数(防误粘贴巨文——超限明确拒绝而非静默截断)。
2729const MAX_MESSAGE_CHARS : usize = 16000 ;
2830
29- fn validate_session ( state : & AppState , session_id : i64 ) -> Result < ChatSession , String > {
30- state
31- . db
32- . get_chat_session ( session_id)
33- . map_err ( |e| e. to_string ( ) ) ?
34- . ok_or_else ( || "会话不存在" . to_string ( ) )
35- }
36-
37- /// 会话客户端解析:session.provider_id 显式 > 默认 Provider;密钥口径
38- /// 显式=per-scope(env 不覆盖显式选择),默认=resolve_default_provider_key
39- /// (env > per-provider > legacy,与精修链同口径)。
40- fn resolve_chat_client (
41- state : & AppState ,
42- session : & ChatSession ,
43- ) -> Result < ( AiClient , Option < String > ) , String > {
44- let store = state
45- . ai_providers
46- . lock ( )
47- . map_err ( |e| format ! ( "AI Provider 存储锁中毒: {}" , e) ) ?
48- . clone ( ) ;
49- if let Some ( pid) = session. provider_id . clone ( ) {
50- let provider = store
51- . get ( & pid)
52- . cloned ( )
53- . ok_or_else ( || format ! ( "Provider {} 不存在(请到设置页检查)" , pid) ) ?;
54- let client = build_provider_client ( state, & provider, & pid) ?;
55- return Ok ( ( client, Some ( pid) ) ) ;
56- }
57- let settings = state
58- . ai_settings
59- . lock ( )
60- . map_err ( |e| format ! ( "AI 设置锁中毒: {}" , e) ) ?
61- . clone ( ) ;
62- let stored_key = crate :: commands_ai_providers:: resolve_default_provider_key ( state) ?;
63- Ok ( ( AiClient :: from_settings_with_store ( & settings, stored_key, & store) , None ) )
64- }
65-
66- /// 显式 Provider 客户端(Ollama 免密钥;其余 per-scope 凭据缺失 → 明确报错)。
67- fn build_provider_client (
68- state : & AppState ,
69- provider : & AiProviderConfig ,
70- pid : & str ,
71- ) -> Result < AiClient , String > {
72- if provider. kind == ProviderKind :: Ollama {
73- return Ok ( AiClient :: from_provider ( provider, None ) ) ;
74- }
75- let key = state
76- . ai_credentials
77- . load_key ( & provider_scope ( pid) ) ?
78- . ok_or_else ( || format ! ( "Provider {} 未保存密钥(设置页保存后重试)" , provider. name) ) ?;
79- Ok ( AiClient :: from_provider ( provider, Some ( key) ) )
80- }
81-
8231/// 新建会话(标题可空——默认"新对话")。
8332#[ tauri:: command]
8433pub fn chat_create_session ( state : State < ' _ , AppState > , title : Option < String > ) -> Result < ChatSession , String > {
@@ -171,19 +120,42 @@ pub async fn chat_send(
171120 // 先解析客户端(Provider/密钥缺失 → 明确报错,不落无应答的用户消息)
172121 let ( client, provider_id) = resolve_chat_client ( & state, & session) ?;
173122 let model = client. config . model . clone ( ) ;
174- // 编辑后重发:改内容 + 作废旧回答(其后消息全删)
123+ // 单活跃流注册必须早于任何落库(审查修复:原在 run_stream 内注册——
124+ // 并发 chat_send 会先重复落库再被拒,防重复扣费/重复请求)
125+ let flag = try_begin_stream ( & state, session_id) ?;
126+ // 编辑后重发:改内容 + 作废旧回答(其后消息全删);
127+ // 入参校验(审查修复):消息必须属于本会话且为 user 角色(防跨会话误改)
175128 if let Some ( mid) = resend_message_id {
176- state. db . update_chat_message_content ( mid as i64 , & content) . map_err ( |e| e. to_string ( ) ) ?;
177- state. db . delete_chat_messages_after ( session_id, mid as i64 ) . map_err ( |e| e. to_string ( ) ) ?;
178- } else {
179- state. db . insert_chat_message ( session_id, "user" , & content, "done" ) . map_err ( |e| e. to_string ( ) ) ?;
129+ let role = state. db . chat_message_role ( session_id, mid as i64 ) . map_err ( |e| e. to_string ( ) ) ?;
130+ match role. as_deref ( ) {
131+ Some ( "user" ) => { }
132+ Some ( _) => {
133+ end_stream ( & state, session_id) ;
134+ return Err ( "编辑重发只能作用于用户消息(assistant 消息用「重发」)" . to_string ( ) ) ;
135+ }
136+ None => {
137+ end_stream ( & state, session_id) ;
138+ return Err ( "要编辑的消息不存在或不属于该会话" . to_string ( ) ) ;
139+ }
140+ }
141+ if let Err ( e) = state. db . update_chat_message_content ( session_id, mid as i64 , & content) {
142+ end_stream ( & state, session_id) ;
143+ return Err ( e. to_string ( ) ) ;
144+ }
145+ if let Err ( e) = state. db . delete_chat_messages_after ( session_id, mid as i64 ) {
146+ end_stream ( & state, session_id) ;
147+ return Err ( e. to_string ( ) ) ;
148+ }
149+ } else if let Err ( e) = state. db . insert_chat_message ( session_id, "user" , & content, "done" ) {
150+ end_stream ( & state, session_id) ;
151+ return Err ( e. to_string ( ) ) ;
180152 }
181153 if let Some ( pid) = & provider_id {
182154 state. db . set_chat_session_model ( session_id, Some ( pid) , & model) . map_err ( |e| e. to_string ( ) ) ?;
183155 } else {
184156 state. db . set_chat_session_model ( session_id, None , & model) . map_err ( |e| e. to_string ( ) ) ?;
185157 }
186- run_stream ( & state, session_id, model, client, channel)
158+ run_stream ( & state, session_id, model, client, channel, flag )
187159}
188160
189161/// 重新生成(重发/重试):删除最后一条 assistant(含 failed/aborted 占位)后重流。
@@ -203,13 +175,19 @@ pub async fn chat_regenerate(
203175 // 先解析客户端(Provider/密钥失败 → 不删旧回答,保留可重发状态)
204176 let ( client, _provider_id) = resolve_chat_client ( & state, & session) ?;
205177 let model = client. config . model . clone ( ) ;
178+ // 单活跃流注册早于删除动作(审查修复:与 chat_send 同纪律)
179+ let flag = try_begin_stream ( & state, session_id) ?;
206180 let msgs = state. db . list_chat_messages ( session_id) . map_err ( |e| e. to_string ( ) ) ?;
207181 if let Some ( last_assistant) = msgs. iter ( ) . rev ( ) . find ( |m| m. role == "assistant" ) {
208- state. db . delete_chat_message ( session_id, last_assistant. id ) . map_err ( |e| e. to_string ( ) ) ?;
182+ if let Err ( e) = state. db . delete_chat_message ( session_id, last_assistant. id ) {
183+ end_stream ( & state, session_id) ;
184+ return Err ( e. to_string ( ) ) ;
185+ }
209186 } else {
187+ end_stream ( & state, session_id) ;
210188 return Err ( "没有可重新生成的消息" . to_string ( ) ) ;
211189 }
212- run_stream ( & state, session_id, model, client, channel)
190+ run_stream ( & state, session_id, model, client, channel, flag )
213191}
214192
215193/// 停止(置取消标志 → 流循环下一行检查短路;无进行中流则 no-op)。
@@ -225,25 +203,15 @@ pub fn chat_cancel(state: State<'_, AppState>, session_id: i64) -> Result<(), St
225203 Ok ( ( ) )
226204}
227205
228- /// 单活跃流编排(chat_send/chat_regenerate 共用)。
206+ /// 单活跃流编排(chat_send/chat_regenerate 共用;flag 由调用方注册 )。
229207fn run_stream (
230208 state : & AppState ,
231209 session_id : i64 ,
232210 model : String ,
233211 client : AiClient ,
234212 channel : Channel < ChatStreamEvent > ,
213+ flag : crate :: ai_chat:: CancelFlag ,
235214) -> Result < ( ) , String > {
236- // 单活跃流:同会话已有进行中的发送 → 拒绝(防并发重复扣费)
237- let flag = CancelFlag :: new ( ) ;
238- {
239- let mut cancels = state
240- . chat_cancels
241- . lock ( )
242- . map_err ( |e| format ! ( "取消表锁中毒: {}" , e) ) ?;
243- if cancels. insert ( session_id, flag. clone ( ) ) . is_some ( ) {
244- return Err ( "该会话已有进行中的对话——请等待完成或先停止" . to_string ( ) ) ;
245- }
246- }
247215 // 历史组装(最新消息已入库;failed 占位不喂上下文——用户已见错误)。
248216 // 列表失败不阻断流传输(消息刚插入过);打印可观测,历史为空保守降级。
249217 let messages = match state. db . list_chat_messages ( session_id) {
@@ -271,11 +239,13 @@ fn run_stream(
271239 } ) {
272240 Ok ( o) => o,
273241 Err ( e) => {
274- // 失败:落 assistant 占位(status=failed,空内容)——前端错误
275- // 气泡 + 重试(chat_regenerate 删占位后重流);不静默不丢失
242+ // 失败:占位消息 content=错误原文(审查修复:L2 优先级——错误
243+ // 文本持久化,前端失败气泡不再依赖瞬态事件态,重启/切换后仍可读);
244+ // 重试=chat_regenerate(删占位后重流)
245+ let msg = e. to_string ( ) ;
276246 let _ = channel. send ( ChatStreamEvent :: from ( & e) ) ;
277- let _ = st. db . insert_chat_message ( session_id, "assistant" , "" , "failed" ) ;
278- st . chat_cancels . lock ( ) . unwrap_or_else ( |x| x . into_inner ( ) ) . remove ( & session_id) ;
247+ let _ = st. db . insert_chat_message ( session_id, "assistant" , & truncate_chars ( & msg , 500 ) , "failed" ) ;
248+ end_stream ( & st , session_id) ;
279249 return ;
280250 }
281251 } ;
@@ -298,7 +268,7 @@ fn run_stream(
298268 } else {
299269 let _ = channel. send ( ChatStreamEvent :: Done { content : outcome. content , usage_json : outcome. usage_json } ) ;
300270 }
301- st . chat_cancels . lock ( ) . unwrap_or_else ( |x| x . into_inner ( ) ) . remove ( & session_id) ;
271+ end_stream ( & st , session_id) ;
302272 } ) ;
303273 Ok ( ( ) )
304274}
0 commit comments