Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions openless-all/app/src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,7 @@ async fn validate_llm_provider() -> Result<(), String> {
ChineseScriptPreference::Auto,
OutputLanguagePreference::Auto,
None,
&[],
)
.await
.map(|_| ())
Expand Down
41 changes: 38 additions & 3 deletions openless-all/app/src-tauri/src/coordinator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -838,6 +838,8 @@ impl Coordinator {
// repolish 是历史记录里手动重新润色,不再绑定原 session 的前台 app;
// 当下用户调起的 app 才是相关上下文(如果可拿)。
let front_app = capture_frontmost_app();
// repolish 是用户主动对单条历史"重新润色",不应该被对话感知上下文影响——
// 用户改的就是这一条本身,不要把别的会话拿进来。所以始终走单轮路径。
polish_text(
&raw_text,
mode,
Expand All @@ -846,6 +848,7 @@ impl Coordinator {
chinese_script_preference,
output_language_preference,
front_app.as_deref(),
&[],
)
.await
.map_err(|e| e.to_string())
Expand Down Expand Up @@ -2664,7 +2667,7 @@ async fn end_session(inner: &Arc<Inner>) -> Result<(), String> {
duration_ms: Some(raw.duration_ms),
dictionary_entry_count: Some(enabled_phrases(inner).len() as u32),
};
if let Err(e) = inner.history.append(session) {
if let Err(e) = inner.history.append_with_retention(session, inner.prefs.get().history_retention_days) {
log::error!("[coord] history append failed: {e}");
}
emit_capsule(
Expand Down Expand Up @@ -2693,6 +2696,33 @@ async fn end_session(inner: &Arc<Inner>) -> Result<(), String> {
let translation_target = prefs.translation_target_language.trim().to_string();
let translation_active =
inner.translation_modifier_seen.load(Ordering::SeqCst) && !translation_target.is_empty();
// 对话感知 polish:拉最近 N 分钟的会话作为 LLM 上下文。仅在非翻译路径且非 Raw mode
// 才有意义(Raw 不走 LLM、翻译走单轮独立 prompt)。窗口=0 时 prior_turns 是空 Vec,
// polish 路径自动退化成单轮单消息——跟历史行为一致。
let polish_context_window_minutes = prefs.polish_context_window_minutes;
let prior_turns: Vec<(String, String)> = if !translation_active
&& mode != PolishMode::Raw
&& polish_context_window_minutes > 0
{
match inner
.history
.recent_within_minutes(polish_context_window_minutes)
{
Ok(sessions) => sessions
.into_iter()
// 只取实际成功润色过的会话作为上下文:失败的会话 final_text 是 raw 兜底,
// 喂回 LLM 会让模型以为"上一轮我什么都没做"——没意义且占 token。
.filter(|s| s.error_code.is_none() && !s.final_text.trim().is_empty())
.map(|s| (s.raw_transcript, s.final_text))
.collect(),
Err(e) => {
log::warn!("[coord] fetch polish context failed: {e}; fall back to single-turn");
Vec::new()
}
}
} else {
Vec::new()
};
let (polished, polish_error) = if translation_active {
log::info!(
"[coord] translation mode → target=\u{300C}{}\u{300D} working={:?} front_app={:?}",
Expand All @@ -2718,6 +2748,7 @@ async fn end_session(inner: &Arc<Inner>) -> Result<(), String> {
chinese_script_preference,
output_language_preference,
front_app.as_deref(),
&prior_turns,
)
.await
};
Expand Down Expand Up @@ -2838,7 +2869,7 @@ async fn end_session(inner: &Arc<Inner>) -> Result<(), String> {
// 比"启用词条总数"更能反映本段口述命中了多少。u64 → u32 截断对单段听写足够。
dictionary_entry_count: Some(total_hits.min(u32::MAX as u64) as u32),
};
if let Err(e) = inner.history.append(session) {
if let Err(e) = inner.history.append_with_retention(session, inner.prefs.get().history_retention_days) {
log::error!("[coord] history append failed: {e}");
}

Expand Down Expand Up @@ -3318,6 +3349,7 @@ async fn polish_or_passthrough(
chinese_script_preference: ChineseScriptPreference,
output_language_preference: OutputLanguagePreference,
front_app: Option<&str>,
prior_turns: &[(String, String)],
) -> (String, Option<String>) {
if mode == PolishMode::Raw {
return (raw.text.clone(), None);
Expand All @@ -3330,6 +3362,7 @@ async fn polish_or_passthrough(
chinese_script_preference,
output_language_preference,
front_app,
prior_turns,
)
.await
{
Expand All @@ -3350,6 +3383,7 @@ async fn polish_text(
chinese_script_preference: ChineseScriptPreference,
output_language_preference: OutputLanguagePreference,
front_app: Option<&str>,
prior_turns: &[(String, String)],
) -> anyhow::Result<String> {
let api_key = CredentialsVault::get(CredentialAccount::ArkApiKey)?.unwrap_or_default();
let model = CredentialsVault::get(CredentialAccount::ArkModelId)?
Expand All @@ -3372,6 +3406,7 @@ async fn polish_text(
chinese_script_preference,
output_language_preference,
front_app,
prior_turns,
)
.await?)
}
Expand Down Expand Up @@ -3865,7 +3900,7 @@ async fn end_qa_session(inner: &Arc<Inner>) -> Result<(), String> {
duration_ms: Some(raw.duration_ms),
dictionary_entry_count: None,
};
if let Err(e) = inner.history.append(session) {
if let Err(e) = inner.history.append_with_retention(session, inner.prefs.get().history_retention_days) {
log::error!("[coord] QA history append failed: {e}");
}
}
Expand Down
42 changes: 42 additions & 0 deletions openless-all/app/src-tauri/src/persistence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -665,16 +665,58 @@ impl HistoryStore {
}

pub fn append(&self, session: DictationSession) -> Result<()> {
self.append_with_retention(session, 0)
}

/// `retention_days == 0` 跟旧 append 行为一致(不按时间清理)。
/// `> 0` 时在写入新条目后顺手把超过 N 天的会话裁掉,写入时就完成清理,
/// 不需要后台轮询。最后再受 200 条硬上限约束(HISTORY_CAP)。
pub fn append_with_retention(
&self,
session: DictationSession,
retention_days: u32,
) -> Result<()> {
let _guard = self.lock.lock();
let mut sessions = self.read_locked()?;
// Prepend so the newest session is at index 0, matching the Swift impl.
sessions.insert(0, session);
if retention_days > 0 {
let cutoff =
chrono::Utc::now() - chrono::Duration::days(i64::from(retention_days));
sessions.retain(|s| {
chrono::DateTime::parse_from_rfc3339(&s.created_at)
.map(|t| t.with_timezone(&chrono::Utc) >= cutoff)
// 解析失败时保守保留——避免错误的时间戳让用户丢历史。
.unwrap_or(true)
});
}
if sessions.len() > HISTORY_CAP {
sessions.truncate(HISTORY_CAP);
}
self.write_locked(&sessions)
}

/// 返回最近 N 分钟内的会话(newest-first)。`minutes == 0` → 空 Vec,
/// 调用方据此跳过对话感知 polish 路径。
pub fn recent_within_minutes(&self, minutes: u32) -> Result<Vec<DictationSession>> {
if minutes == 0 {
return Ok(Vec::new());
}
let _guard = self.lock.lock();
let sessions = self.read_locked()?;
let cutoff = chrono::Utc::now() - chrono::Duration::minutes(i64::from(minutes));
// sessions 是 newest-first,超出窗口的会话之后的都更老,take_while 即可。
let filtered: Vec<DictationSession> = sessions
.into_iter()
.take_while(|s| {
chrono::DateTime::parse_from_rfc3339(&s.created_at)
.map(|t| t.with_timezone(&chrono::Utc) >= cutoff)
.unwrap_or(false)
})
.collect();
Ok(filtered)
}

pub fn delete(&self, id: &str) -> Result<()> {
let _guard = self.lock.lock();
let mut sessions = self.read_locked()?;
Expand Down
Loading
Loading