use super::dto::{
DesktopContextResponse, DesktopHistoryResponse, DesktopImportSessionRequest,
DesktopImportSessionResponse, DesktopLifecycleActionDto, DesktopMemoryActionRequest,
DesktopMemoryDraftRequest, DesktopPromptOptimizeRequest, DesktopPromptOptimizeResponse,
DesktopRecordLookupRequest, DesktopRecordResponse, DesktopRouteRequest,
DesktopSessionActionRequest, DesktopSessionActionResponse, DesktopSessionBrowserRequest,
DesktopSessionBrowserResponse, DesktopSessionDetailRequest, DesktopSessionDetailResponse,
DesktopStatusRequest, DesktopWakeupRequest, DesktopWakeupResponse, DesktopWikiIndexRequest,
DesktopWikiIndexResponse, DesktopWikiLintRequest, DesktopWikiLintResponse,
DesktopWorkbenchRequest, DesktopWorkbenchResponse, DesktopWriteResponse,
};
use super::errors::{DesktopResult, input_error, runtime_error};
use super::helpers::{
build_continue_command, delete_session_file, launch_terminal_command, lifecycle_read_options,
load_session_index, load_session_index_filtered, store_from_config_path,
};
use super::validate::{
validate_config_path, validate_memory_action_request, validate_memory_draft_request,
validate_record_lookup_request, validate_route_inputs, validate_route_request,
validate_session_action_request, validate_session_detail_request,
};
use crate::app;
use crate::daemon::{read_history, read_record, read_workbench};
use crate::desktop_status::{DesktopStatusResponse, collect_status};
use crate::lifecycle_service::{LifecycleService, available_actions};
use crate::lifecycle_store::latest_state_entries;
use crate::lifecycle_summary;
use crate::memory_gateway;
use crate::memory_importer;
use crate::session_sources::{
ProviderSessionMessages, entry_session_refs, load_provider_messages, load_provider_sessions,
raw_session_id,
};
#[derive(Debug, Default, Clone, Copy)]
pub struct DesktopService;
impl DesktopService {
pub fn new() -> Self {
Self
}
pub fn run_context(
self,
request: DesktopRouteRequest,
) -> DesktopResult<DesktopContextResponse> {
validate_route_request(&request).map_err(input_error)?;
let config_path = request.config_path.clone();
let result = app::run_with_overrides(
config_path.as_path(),
request.to_route_input(),
Some(request.format),
request.vault_root_override.as_deref(),
)
.map_err(runtime_error)?;
Ok(DesktopContextResponse {
rendered: result.rendered,
explain: result.explain,
used_format: result.used_format,
used_vault_root: result.used_vault_root,
bundle: result.bundle,
})
}
pub fn build_wakeup(
self,
request: DesktopWakeupRequest,
) -> DesktopResult<DesktopWakeupResponse> {
validate_route_inputs(
request.config_path.as_path(),
request.cwd.as_path(),
&request.task,
request.vault_root_override.as_deref(),
)
.map_err(input_error)?;
let gateway_request =
memory_gateway::wakeup_request(request.to_route_input(), request.profile);
let response = memory_gateway::execute(
request.config_path.as_path(),
gateway_request,
request.vault_root_override.as_deref(),
)
.map_err(runtime_error)?;
let packet = response.wakeup_packet.ok_or_else(|| {
runtime_error(anyhow::anyhow!(
"wakeup packet missing from gateway response"
))
})?;
Ok(DesktopWakeupResponse {
used_vault_root: response.used_vault_root,
bundle: response.bundle,
packet,
})
}
pub fn optimize_prompt(
self,
request: DesktopPromptOptimizeRequest,
) -> DesktopResult<DesktopPromptOptimizeResponse> {
validate_route_inputs(
request.config_path.as_path(),
request.cwd.as_path(),
&request.task,
request.vault_root_override.as_deref(),
)
.map_err(input_error)?;
memory_gateway::execute_prompt_optimize(
request.config_path.as_path(),
memory_gateway::prompt_optimize_request(
request.to_route_input(),
request.profile,
request.provider,
request.session_id,
false,
),
request.vault_root_override.as_deref(),
)
.map_err(runtime_error)
}
pub fn load_workbench(
self,
request: DesktopWorkbenchRequest,
) -> DesktopResult<DesktopWorkbenchResponse> {
validate_config_path(request.config_path.as_path()).map_err(input_error)?;
let snapshot = read_workbench(
request.config_path.as_path(),
&lifecycle_read_options(request.daemon.as_ref()),
)
.map_err(runtime_error)?;
Ok(DesktopWorkbenchResponse {
payload: serde_json::json!({
"pending_review": lifecycle_summary::queue_payload(&snapshot.pending_review, "pending_review"),
"wakeup_ready": lifecycle_summary::queue_payload(&snapshot.wakeup_ready, "wakeup_ready"),
}),
snapshot,
})
}
pub fn get_record(
self,
request: DesktopRecordLookupRequest,
) -> DesktopResult<Option<DesktopRecordResponse>> {
validate_record_lookup_request(&request).map_err(input_error)?;
let record = read_record(
request.config_path.as_path(),
&request.record_id,
&lifecycle_read_options(request.daemon.as_ref()),
)
.map_err(runtime_error)?;
Ok(record.map(|record| DesktopRecordResponse {
payload: lifecycle_summary::record_payload(&record),
rendered: lifecycle_summary::render_record_text(&record, false, true),
available_actions: available_actions(&record.record)
.iter()
.copied()
.map(DesktopLifecycleActionDto::from_lifecycle_action)
.collect(),
record,
}))
}
pub fn get_history(
self,
request: DesktopRecordLookupRequest,
) -> DesktopResult<DesktopHistoryResponse> {
validate_record_lookup_request(&request).map_err(input_error)?;
let history = read_history(
request.config_path.as_path(),
&request.record_id,
&lifecycle_read_options(request.daemon.as_ref()),
)
.map_err(runtime_error)?;
Ok(DesktopHistoryResponse {
payload: lifecycle_summary::history_payload(&request.record_id, &history),
rendered: lifecycle_summary::render_history_text(&request.record_id, &history, false),
record_id: request.record_id,
history,
})
}
pub fn record_manual(
self,
request: DesktopMemoryDraftRequest,
) -> DesktopResult<DesktopWriteResponse> {
validate_memory_draft_request(&request).map_err(input_error)?;
let service = LifecycleService::new();
let result = service
.record_manual(request.config_path.as_path(), request.to_record_request())
.map_err(runtime_error)?;
crate::vault_writer::writeback_from_config(request.config_path.as_path(), &result.entry);
Ok(DesktopWriteResponse {
payload: lifecycle_summary::create_payload(
"record_manual",
&result.entry,
&result.snapshot,
),
entry: result.entry,
})
}
pub fn propose_memory(
self,
request: DesktopMemoryDraftRequest,
) -> DesktopResult<DesktopWriteResponse> {
validate_memory_draft_request(&request).map_err(input_error)?;
let service = LifecycleService::new();
let result = service
.propose_ai(request.config_path.as_path(), request.to_propose_request())
.map_err(runtime_error)?;
crate::vault_writer::writeback_from_config(request.config_path.as_path(), &result.entry);
Ok(DesktopWriteResponse {
payload: lifecycle_summary::create_payload("propose", &result.entry, &result.snapshot),
entry: result.entry,
})
}
pub fn apply_memory_action(
self,
request: DesktopMemoryActionRequest,
) -> DesktopResult<DesktopWriteResponse> {
validate_memory_action_request(&request).map_err(input_error)?;
let service = LifecycleService::new();
let action = request.action.into_lifecycle_action();
let result = service
.apply_action_with_metadata(
request.config_path.as_path(),
&request.record_id,
action,
request.metadata.into_transition_metadata(),
)
.map_err(runtime_error)?;
crate::vault_writer::writeback_from_config(request.config_path.as_path(), &result.entry);
Ok(DesktopWriteResponse {
payload: lifecycle_summary::action_payload(&result.entry, &result.snapshot, action),
entry: result.entry,
})
}
pub fn browse_sessions(
self,
request: DesktopSessionBrowserRequest,
) -> DesktopResult<DesktopSessionBrowserResponse> {
validate_config_path(request.config_path.as_path()).map_err(input_error)?;
let sessions =
load_session_index_filtered(request.config_path.as_path(), request.provider.as_deref())
.map_err(runtime_error)?;
let page = request.page.max(1);
let per_page = request.per_page.max(1);
let total = sessions.len();
let start = (page - 1) * per_page;
let paged = sessions
.into_iter()
.skip(start)
.take(per_page)
.collect::<Vec<_>>();
let has_more = start + paged.len() < total;
Ok(DesktopSessionBrowserResponse {
sessions: paged,
page,
per_page,
total,
has_more,
})
}
pub fn get_session(
self,
request: DesktopSessionDetailRequest,
) -> DesktopResult<Option<DesktopSessionDetailResponse>> {
validate_session_detail_request(&request).map_err(input_error)?;
let entries = latest_state_entries(&store_from_config_path(request.config_path.as_path()))
.map_err(runtime_error)?;
let sessions = load_session_index(request.config_path.as_path()).map_err(runtime_error)?;
let session = sessions
.iter()
.find(|item| item.session_id == request.session_id)
.cloned();
let Some(session) = session else {
return Ok(None);
};
let mut records: Vec<_> = entries
.into_iter()
.filter(|entry| {
entry_session_refs(entry).contains(&raw_session_id(&request.session_id))
})
.collect();
records.sort_by(|left, right| right.recorded_at.cmp(&left.recorded_at));
let offset = request.message_offset.unwrap_or(0);
let limit = request.message_limit.unwrap_or(0);
let ProviderSessionMessages {
messages,
total_messages,
has_more_messages,
} = load_provider_messages(&session, offset, limit).map_err(runtime_error)?;
let latest_user_message = messages
.iter()
.rev()
.find(|message| message.role == "user")
.map(|message| message.content.clone());
Ok(Some(DesktopSessionDetailResponse {
session,
records,
total_messages,
showing_recent_messages: messages.len(),
has_more_messages,
messages,
latest_user_message,
}))
}
pub fn continue_session(
self,
request: DesktopSessionActionRequest,
) -> DesktopResult<DesktopSessionActionResponse> {
validate_session_action_request(&request).map_err(input_error)?;
let sessions = load_session_index(request.config_path.as_path()).map_err(runtime_error)?;
let session = sessions
.into_iter()
.find(|item| item.session_id == request.session_id)
.ok_or_else(|| input_error(format!("未找到会话:{}", request.session_id)))?;
let command = build_continue_command(&session).ok_or_else(|| {
input_error(format!(
"当前 provider 暂不支持继续会话:{}",
session.provider
))
})?;
launch_terminal_command(&command)
.map_err(|error| runtime_error(anyhow::anyhow!("继续对话失败:{error}")))?;
Ok(DesktopSessionActionResponse {
session_id: session.session_id,
provider: session.provider,
command: Some(command),
message: "已在终端中打开继续对话命令。".to_string(),
})
}
pub fn delete_session(
self,
request: DesktopSessionActionRequest,
) -> DesktopResult<DesktopSessionActionResponse> {
validate_session_action_request(&request).map_err(input_error)?;
let sessions = load_session_index(request.config_path.as_path()).map_err(runtime_error)?;
let session = sessions
.into_iter()
.find(|item| item.session_id == request.session_id)
.ok_or_else(|| input_error(format!("未找到会话:{}", request.session_id)))?;
delete_session_file(&session)
.map_err(|error| runtime_error(anyhow::anyhow!("删除会话失败:{error}")))?;
Ok(DesktopSessionActionResponse {
session_id: session.session_id,
provider: session.provider,
command: None,
message: "已删除本地会话文件。".to_string(),
})
}
pub fn collect_status(
self,
request: DesktopStatusRequest,
) -> DesktopResult<DesktopStatusResponse> {
validate_config_path(request.config_path.as_path()).map_err(input_error)?;
let provider_sessions = load_provider_sessions(None).map_err(runtime_error)?;
Ok(collect_status(
request.config_path.as_path(),
request.cwd.as_path(),
request.vault_root_override.as_deref(),
provider_sessions.len(),
))
}
pub fn import_session(
self,
request: DesktopImportSessionRequest,
) -> DesktopResult<DesktopImportSessionResponse> {
validate_config_path(request.config_path.as_path()).map_err(input_error)?;
if request.session_id.trim().is_empty() {
return Err(input_error("session_id 不能为空".to_string()));
}
let provider = memory_importer::ImportProvider::parse(request.provider.as_str())
.map_err(|err| input_error(err.to_string()))?;
let response = memory_importer::import_session(
request.config_path.as_path(),
provider,
&request.session_id,
request.apply,
request.actor.clone(),
)
.map_err(runtime_error)?;
Ok(response)
}
pub fn wiki_lint(
self,
request: DesktopWikiLintRequest,
) -> DesktopResult<DesktopWikiLintResponse> {
validate_config_path(request.config_path.as_path()).map_err(input_error)?;
let vault_root = resolve_vault_root(request.config_path.as_path())?;
let report = crate::wiki_lint::run_lint_from_config(request.config_path.as_path())
.map_err(runtime_error)?;
let markdown = crate::wiki_lint::render_lint_markdown(&report);
Ok(DesktopWikiLintResponse {
used_vault_root: vault_root,
report,
markdown,
})
}
pub fn read_wiki_index(
self,
request: DesktopWikiIndexRequest,
) -> DesktopResult<DesktopWikiIndexResponse> {
validate_config_path(request.config_path.as_path()).map_err(input_error)?;
let vault_root = resolve_vault_root(request.config_path.as_path())?;
let markdown =
crate::wiki_index::load_index_section(&vault_root, request.project_id.as_deref());
Ok(DesktopWikiIndexResponse {
used_vault_root: vault_root,
markdown,
})
}
}
fn resolve_vault_root(config_path: &std::path::Path) -> DesktopResult<std::path::PathBuf> {
let config = app::load(config_path).map_err(runtime_error)?;
app::resolve_override_path(&config.vault.root, config_path).map_err(runtime_error)
}