1use super::dto::{
4 DesktopContextResponse, DesktopHistoryResponse, DesktopImportSessionRequest,
5 DesktopImportSessionResponse, DesktopLifecycleActionDto, DesktopMemoryActionRequest,
6 DesktopMemoryDraftRequest, DesktopPromptOptimizeRequest, DesktopPromptOptimizeResponse,
7 DesktopRecordLookupRequest, DesktopRecordResponse, DesktopRouteRequest,
8 DesktopSessionActionRequest, DesktopSessionActionResponse, DesktopSessionBrowserRequest,
9 DesktopSessionBrowserResponse, DesktopSessionDetailRequest, DesktopSessionDetailResponse,
10 DesktopStatusRequest, DesktopWakeupRequest, DesktopWakeupResponse, DesktopWikiIndexRequest,
11 DesktopWikiIndexResponse, DesktopWikiLintRequest, DesktopWikiLintResponse,
12 DesktopWorkbenchRequest, DesktopWorkbenchResponse, DesktopWriteResponse,
13};
14use super::errors::{DesktopResult, input_error, runtime_error};
15use super::helpers::{
16 build_continue_command, delete_session_file, launch_terminal_command, lifecycle_read_options,
17 load_session_index, load_session_index_filtered, store_from_config_path,
18};
19use super::validate::{
20 validate_config_path, validate_memory_action_request, validate_memory_draft_request,
21 validate_record_lookup_request, validate_route_inputs, validate_route_request,
22 validate_session_action_request, validate_session_detail_request,
23};
24use crate::app;
25use crate::daemon::{read_history, read_record, read_workbench};
26use crate::desktop_status::{DesktopStatusResponse, collect_status};
27use crate::lifecycle_service::{LifecycleService, available_actions};
28use crate::lifecycle_store::latest_state_entries;
29use crate::lifecycle_summary;
30use crate::memory_gateway;
31use crate::memory_importer;
32use crate::session_sources::{
33 ProviderSessionMessages, entry_session_refs, load_provider_messages, load_provider_sessions,
34 raw_session_id,
35};
36
37#[derive(Debug, Default, Clone, Copy)]
38pub struct DesktopService;
39
40impl DesktopService {
41 pub fn new() -> Self {
42 Self
43 }
44
45 pub fn run_context(
46 self,
47 request: DesktopRouteRequest,
48 ) -> DesktopResult<DesktopContextResponse> {
49 validate_route_request(&request).map_err(input_error)?;
50 let config_path = request.config_path.clone();
51 let result = app::run_with_overrides(
52 config_path.as_path(),
53 request.to_route_input(),
54 Some(request.format),
55 request.vault_root_override.as_deref(),
56 )
57 .map_err(runtime_error)?;
58 Ok(DesktopContextResponse {
59 rendered: result.rendered,
60 explain: result.explain,
61 used_format: result.used_format,
62 used_vault_root: result.used_vault_root,
63 bundle: result.bundle,
64 })
65 }
66
67 pub fn build_wakeup(
68 self,
69 request: DesktopWakeupRequest,
70 ) -> DesktopResult<DesktopWakeupResponse> {
71 validate_route_inputs(
72 request.config_path.as_path(),
73 request.cwd.as_path(),
74 &request.task,
75 request.vault_root_override.as_deref(),
76 )
77 .map_err(input_error)?;
78 let gateway_request =
79 memory_gateway::wakeup_request(request.to_route_input(), request.profile);
80 let response = memory_gateway::execute(
81 request.config_path.as_path(),
82 gateway_request,
83 request.vault_root_override.as_deref(),
84 )
85 .map_err(runtime_error)?;
86 let packet = response.wakeup_packet.ok_or_else(|| {
87 runtime_error(anyhow::anyhow!(
88 "wakeup packet missing from gateway response"
89 ))
90 })?;
91 Ok(DesktopWakeupResponse {
92 used_vault_root: response.used_vault_root,
93 bundle: response.bundle,
94 packet,
95 })
96 }
97
98 pub fn optimize_prompt(
99 self,
100 request: DesktopPromptOptimizeRequest,
101 ) -> DesktopResult<DesktopPromptOptimizeResponse> {
102 validate_route_inputs(
103 request.config_path.as_path(),
104 request.cwd.as_path(),
105 &request.task,
106 request.vault_root_override.as_deref(),
107 )
108 .map_err(input_error)?;
109 memory_gateway::execute_prompt_optimize(
110 request.config_path.as_path(),
111 memory_gateway::prompt_optimize_request(
112 request.to_route_input(),
113 request.profile,
114 request.provider,
115 request.session_id,
116 false,
117 ),
118 request.vault_root_override.as_deref(),
119 )
120 .map_err(runtime_error)
121 }
122
123 pub fn load_workbench(
124 self,
125 request: DesktopWorkbenchRequest,
126 ) -> DesktopResult<DesktopWorkbenchResponse> {
127 validate_config_path(request.config_path.as_path()).map_err(input_error)?;
128 let snapshot = read_workbench(
129 request.config_path.as_path(),
130 &lifecycle_read_options(request.daemon.as_ref()),
131 )
132 .map_err(runtime_error)?;
133 Ok(DesktopWorkbenchResponse {
134 payload: serde_json::json!({
135 "pending_review": lifecycle_summary::queue_payload(&snapshot.pending_review, "pending_review"),
136 "wakeup_ready": lifecycle_summary::queue_payload(&snapshot.wakeup_ready, "wakeup_ready"),
137 }),
138 snapshot,
139 })
140 }
141
142 pub fn get_record(
143 self,
144 request: DesktopRecordLookupRequest,
145 ) -> DesktopResult<Option<DesktopRecordResponse>> {
146 validate_record_lookup_request(&request).map_err(input_error)?;
147 let record = read_record(
148 request.config_path.as_path(),
149 &request.record_id,
150 &lifecycle_read_options(request.daemon.as_ref()),
151 )
152 .map_err(runtime_error)?;
153 Ok(record.map(|record| DesktopRecordResponse {
154 payload: lifecycle_summary::record_payload(&record),
155 rendered: lifecycle_summary::render_record_text(&record, false, true),
156 available_actions: available_actions(&record.record)
157 .iter()
158 .copied()
159 .map(DesktopLifecycleActionDto::from_lifecycle_action)
160 .collect(),
161 record,
162 }))
163 }
164
165 pub fn get_history(
166 self,
167 request: DesktopRecordLookupRequest,
168 ) -> DesktopResult<DesktopHistoryResponse> {
169 validate_record_lookup_request(&request).map_err(input_error)?;
170 let history = read_history(
171 request.config_path.as_path(),
172 &request.record_id,
173 &lifecycle_read_options(request.daemon.as_ref()),
174 )
175 .map_err(runtime_error)?;
176 Ok(DesktopHistoryResponse {
177 payload: lifecycle_summary::history_payload(&request.record_id, &history),
178 rendered: lifecycle_summary::render_history_text(&request.record_id, &history, false),
179 record_id: request.record_id,
180 history,
181 })
182 }
183
184 pub fn record_manual(
185 self,
186 request: DesktopMemoryDraftRequest,
187 ) -> DesktopResult<DesktopWriteResponse> {
188 validate_memory_draft_request(&request).map_err(input_error)?;
189 let service = LifecycleService::new();
190 let result = service
191 .record_manual(request.config_path.as_path(), request.to_record_request())
192 .map_err(runtime_error)?;
193 crate::vault_writer::writeback_from_config(request.config_path.as_path(), &result.entry);
194 Ok(DesktopWriteResponse {
195 payload: lifecycle_summary::create_payload(
196 "record_manual",
197 &result.entry,
198 &result.snapshot,
199 ),
200 entry: result.entry,
201 })
202 }
203
204 pub fn propose_memory(
205 self,
206 request: DesktopMemoryDraftRequest,
207 ) -> DesktopResult<DesktopWriteResponse> {
208 validate_memory_draft_request(&request).map_err(input_error)?;
209 let service = LifecycleService::new();
210 let result = service
211 .propose_ai(request.config_path.as_path(), request.to_propose_request())
212 .map_err(runtime_error)?;
213 crate::vault_writer::writeback_from_config(request.config_path.as_path(), &result.entry);
214 Ok(DesktopWriteResponse {
215 payload: lifecycle_summary::create_payload("propose", &result.entry, &result.snapshot),
216 entry: result.entry,
217 })
218 }
219
220 pub fn apply_memory_action(
221 self,
222 request: DesktopMemoryActionRequest,
223 ) -> DesktopResult<DesktopWriteResponse> {
224 validate_memory_action_request(&request).map_err(input_error)?;
225 let service = LifecycleService::new();
226 let action = request.action.into_lifecycle_action();
227 let result = service
228 .apply_action_with_metadata(
229 request.config_path.as_path(),
230 &request.record_id,
231 action,
232 request.metadata.into_transition_metadata(),
233 )
234 .map_err(runtime_error)?;
235 crate::vault_writer::writeback_from_config(request.config_path.as_path(), &result.entry);
236 Ok(DesktopWriteResponse {
237 payload: lifecycle_summary::action_payload(&result.entry, &result.snapshot, action),
238 entry: result.entry,
239 })
240 }
241
242 pub fn browse_sessions(
243 self,
244 request: DesktopSessionBrowserRequest,
245 ) -> DesktopResult<DesktopSessionBrowserResponse> {
246 validate_config_path(request.config_path.as_path()).map_err(input_error)?;
247 let sessions =
248 load_session_index_filtered(request.config_path.as_path(), request.provider.as_deref())
249 .map_err(runtime_error)?;
250 let page = request.page.max(1);
251 let per_page = request.per_page.max(1);
252 let total = sessions.len();
253 let start = (page - 1) * per_page;
254 let paged = sessions
255 .into_iter()
256 .skip(start)
257 .take(per_page)
258 .collect::<Vec<_>>();
259 let has_more = start + paged.len() < total;
260
261 Ok(DesktopSessionBrowserResponse {
262 sessions: paged,
263 page,
264 per_page,
265 total,
266 has_more,
267 })
268 }
269
270 pub fn get_session(
271 self,
272 request: DesktopSessionDetailRequest,
273 ) -> DesktopResult<Option<DesktopSessionDetailResponse>> {
274 validate_session_detail_request(&request).map_err(input_error)?;
275 let entries = latest_state_entries(&store_from_config_path(request.config_path.as_path()))
276 .map_err(runtime_error)?;
277 let sessions = load_session_index(request.config_path.as_path()).map_err(runtime_error)?;
278 let session = sessions
279 .iter()
280 .find(|item| item.session_id == request.session_id)
281 .cloned();
282 let Some(session) = session else {
283 return Ok(None);
284 };
285
286 let mut records: Vec<_> = entries
287 .into_iter()
288 .filter(|entry| {
289 entry_session_refs(entry).contains(&raw_session_id(&request.session_id))
290 })
291 .collect();
292 records.sort_by(|left, right| right.recorded_at.cmp(&left.recorded_at));
293
294 let offset = request.message_offset.unwrap_or(0);
295 let limit = request.message_limit.unwrap_or(0);
296 let ProviderSessionMessages {
297 messages,
298 total_messages,
299 has_more_messages,
300 } = load_provider_messages(&session, offset, limit).map_err(runtime_error)?;
301 let latest_user_message = messages
302 .iter()
303 .rev()
304 .find(|message| message.role == "user")
305 .map(|message| message.content.clone());
306
307 Ok(Some(DesktopSessionDetailResponse {
308 session,
309 records,
310 total_messages,
311 showing_recent_messages: messages.len(),
312 has_more_messages,
313 messages,
314 latest_user_message,
315 }))
316 }
317
318 pub fn continue_session(
319 self,
320 request: DesktopSessionActionRequest,
321 ) -> DesktopResult<DesktopSessionActionResponse> {
322 validate_session_action_request(&request).map_err(input_error)?;
323 let sessions = load_session_index(request.config_path.as_path()).map_err(runtime_error)?;
324 let session = sessions
325 .into_iter()
326 .find(|item| item.session_id == request.session_id)
327 .ok_or_else(|| input_error(format!("未找到会话:{}", request.session_id)))?;
328 let command = build_continue_command(&session).ok_or_else(|| {
329 input_error(format!(
330 "当前 provider 暂不支持继续会话:{}",
331 session.provider
332 ))
333 })?;
334
335 launch_terminal_command(&command)
336 .map_err(|error| runtime_error(anyhow::anyhow!("继续对话失败:{error}")))?;
337
338 Ok(DesktopSessionActionResponse {
339 session_id: session.session_id,
340 provider: session.provider,
341 command: Some(command),
342 message: "已在终端中打开继续对话命令。".to_string(),
343 })
344 }
345
346 pub fn delete_session(
347 self,
348 request: DesktopSessionActionRequest,
349 ) -> DesktopResult<DesktopSessionActionResponse> {
350 validate_session_action_request(&request).map_err(input_error)?;
351 let sessions = load_session_index(request.config_path.as_path()).map_err(runtime_error)?;
352 let session = sessions
353 .into_iter()
354 .find(|item| item.session_id == request.session_id)
355 .ok_or_else(|| input_error(format!("未找到会话:{}", request.session_id)))?;
356 delete_session_file(&session)
357 .map_err(|error| runtime_error(anyhow::anyhow!("删除会话失败:{error}")))?;
358
359 Ok(DesktopSessionActionResponse {
360 session_id: session.session_id,
361 provider: session.provider,
362 command: None,
363 message: "已删除本地会话文件。".to_string(),
364 })
365 }
366
367 pub fn collect_status(
368 self,
369 request: DesktopStatusRequest,
370 ) -> DesktopResult<DesktopStatusResponse> {
371 validate_config_path(request.config_path.as_path()).map_err(input_error)?;
372 let provider_sessions = load_provider_sessions(None).map_err(runtime_error)?;
373 Ok(collect_status(
374 request.config_path.as_path(),
375 request.cwd.as_path(),
376 request.vault_root_override.as_deref(),
377 provider_sessions.len(),
378 ))
379 }
380
381 pub fn import_session(
382 self,
383 request: DesktopImportSessionRequest,
384 ) -> DesktopResult<DesktopImportSessionResponse> {
385 validate_config_path(request.config_path.as_path()).map_err(input_error)?;
386 if request.session_id.trim().is_empty() {
387 return Err(input_error("session_id 不能为空".to_string()));
388 }
389 let provider = memory_importer::ImportProvider::parse(request.provider.as_str())
390 .map_err(|err| input_error(err.to_string()))?;
391 let response = memory_importer::import_session(
392 request.config_path.as_path(),
393 provider,
394 &request.session_id,
395 request.apply,
396 request.actor.clone(),
397 )
398 .map_err(runtime_error)?;
399 Ok(response)
400 }
401
402 pub fn wiki_lint(
403 self,
404 request: DesktopWikiLintRequest,
405 ) -> DesktopResult<DesktopWikiLintResponse> {
406 validate_config_path(request.config_path.as_path()).map_err(input_error)?;
407 let vault_root = resolve_vault_root(request.config_path.as_path())?;
408 let report = crate::wiki_lint::run_lint_from_config(request.config_path.as_path())
409 .map_err(runtime_error)?;
410 let markdown = crate::wiki_lint::render_lint_markdown(&report);
411 Ok(DesktopWikiLintResponse {
412 used_vault_root: vault_root,
413 report,
414 markdown,
415 })
416 }
417
418 pub fn read_wiki_index(
419 self,
420 request: DesktopWikiIndexRequest,
421 ) -> DesktopResult<DesktopWikiIndexResponse> {
422 validate_config_path(request.config_path.as_path()).map_err(input_error)?;
423 let vault_root = resolve_vault_root(request.config_path.as_path())?;
424 let markdown =
425 crate::wiki_index::load_index_section(&vault_root, request.project_id.as_deref());
426 Ok(DesktopWikiIndexResponse {
427 used_vault_root: vault_root,
428 markdown,
429 })
430 }
431}
432
433fn resolve_vault_root(config_path: &std::path::Path) -> DesktopResult<std::path::PathBuf> {
434 let config = app::load(config_path).map_err(runtime_error)?;
435 app::resolve_override_path(&config.vault.root, config_path).map_err(runtime_error)
436}