新增: ai_list_recoverable IPC+桌面可恢复入口双轨数据源(B3)

This commit is contained in:
lxy
2026-10-06 12:54:11 +08:00
parent 40392ebded
commit 7c252cff96
5 changed files with 56 additions and 5 deletions
+1 -1
View File
@@ -1058,7 +1058,7 @@ graph TD
## 无人值守可靠性专项(2026-10-06 swarm 立项,设计档:docs/02-架构设计/无人值守可靠性专项设计-20261006.md+temp/V49断点续跑设计-20261006.md) ## 无人值守可靠性专项(2026-10-06 swarm 立项,设计档:docs/02-架构设计/无人值守可靠性专项设计-20261006.md+temp/V49断点续跑设计-20261006.md)
- [x] B1(S):H1 汇报 git 实测附录 ✅(2026-10-06:deliver_completion_report_if_waited 单点 append_git_facts——branch/HEAD/未提交计数三行,非 git 仓静默跳过,超长报告不追,git.rs 增 git_exec 只读薄封装)+ 探活告警口径修正 ✅(同日 18 批:inflight 计数点前移到 handle_message 白名单通过处,覆盖占位卡 HTTP+per-chat 锁排队窗口,旧计数点删除防双计)- [ ] B2(M):H2 token 三闸(抄 sub_agent round_budget_stop)+ H4 产出审计 subscriber - [x] B1(S):H1 汇报 git 实测附录 ✅(2026-10-06:deliver_completion_report_if_waited 单点 append_git_facts——branch/HEAD/未提交计数三行,非 git 仓静默跳过,超长报告不追,git.rs 增 git_exec 只读薄封装)+ 探活告警口径修正 ✅(同日 18 批:inflight 计数点前移到 handle_message 白名单通过处,覆盖占位卡 HTTP+per-chat 锁排队窗口,旧计数点删除防双计)- [ ] B2(M):H2 token 三闸(抄 sub_agent round_budget_stop)+ H4 产出审计 subscriber
- [ ] B3(M):V49 防覆盖定稿+单测 + ai_list_recoverable IPC + 桌面「可恢复」入口 - [x] B3(M):V49 防覆盖定稿+单测 ✅(本会话前批:#11 update_full 剥离 loop_state 列,df-storage conversation 8 passed)+ ai_list_recoverable IPC ✅(2026-10-06:conversation.rs parked 专项完备真相清单 id/title/model/updated_at,不受侧栏 limit 截断,lib.rs 已注册)+ 桌面「可恢复」入口 ✅(2026-10-06 本批:TopBar recoverableTasks 双轨数据源——conversations loop_state 实时投影+ai_list_recoverable IPC 完备真相合并去重,面板展开触发 IPC 拉取,失败退回投影;ai.ts listRecoverable 封装)。验收:cargo check EXIT 0 + vue-tsc EXIT 0。
- [ ] B4(L):ai_recover_parked 重启恢复 + H3 外部干预感知(git HEAD 比对→Parked) - [ ] B4(L):ai_recover_parked 重启恢复 + H3 外部干预感知(git HEAD 比对→Parked)
## UI/UX 体验设计实施项(2026-10-06 设计档:temp/异步任务会话UIUX设计-20261006.md,依赖 13/14/16/17 机制先行) ## UI/UX 体验设计实施项(2026-10-06 设计档:temp/异步任务会话UIUX设计-20261006.md,依赖 13/14/16/17 机制先行)
@@ -278,6 +278,30 @@ pub async fn ai_conversation_list(
Ok(active) Ok(active)
} }
/// B3(无人值守可靠性专项):列出可恢复会话(loop_state='parked')。
///
/// V49 断点续跑的完备真相查询:重启时 mark_running_as_parked 改写的会话在此列出,
/// 桌面任务卡「可恢复」分区与飞书投影共用本 IPC(单一事实源)。与 ai_conversation_list
/// 的 loop_state 透出互补——list 是侧栏全量摘要(截断 limit 内),本 IPC 是 parked 专项
/// 全量清单(不受侧栏 limit 截断),且附任务上下文(标题+最后更新时间)供恢复决策。
#[tauri::command]
pub async fn ai_list_recoverable(
state: State<'_, AppState>,
) -> Result<Vec<serde_json::Value>, String> {
let records = state.ai_conversations.list_parked().await.map_err(err_str)?;
Ok(records
.iter()
.map(|r| {
serde_json::json!({
"id": r.id,
"title": r.title,
"model": r.model,
"updated_at": r.updated_at,
})
})
.collect())
}
/// 切换到指定对话(从 DB 加载 messages 到内存 + 返回 messages 给前端) /// 切换到指定对话(从 DB 加载 messages 到内存 + 返回 messages 给前端)
#[tauri::command] #[tauri::command]
pub async fn ai_conversation_switch( pub async fn ai_conversation_switch(
+1
View File
@@ -449,6 +449,7 @@ pub fn run() {
// AI 对话管理 // AI 对话管理
commands::ai::ai_conversation_create, commands::ai::ai_conversation_create,
commands::ai::ai_conversation_list, commands::ai::ai_conversation_list,
commands::ai::ai_list_recoverable,
commands::ai::ai_conversation_switch, commands::ai::ai_conversation_switch,
// S3 子代理轨迹只读窥视(复用 switch 读路径,不切任何会话状态) // S3 子代理轨迹只读窥视(复用 switch 读路径,不切任何会话状态)
commands::ai::ai_conversation_peek, commands::ai::ai_conversation_peek,
+5
View File
@@ -327,6 +327,11 @@ export const aiApi = {
}) })
}, },
/** B3 可恢复会话清单(loop_state='parked' 完备真相,不受侧栏 limit 截断) */
listRecoverable(): Promise<{ id: string; title: string | null; model: string | null; updated_at: string }[]> {
return invoke('ai_list_recoverable')
},
/** 切换到指定对话 */ /** 切换到指定对话 */
switchConversation(conversationId: string): Promise<AiConversationDetail> { switchConversation(conversationId: string): Promise<AiConversationDetail> {
return invoke('ai_conversation_switch', { conversationId }) return invoke('ai_conversation_switch', { conversationId })
+25 -4
View File
@@ -456,14 +456,31 @@ const runningTasks = computed(() => {
} }
return items return items
}) })
// V49 可恢复入口(断点续跑读侧):conversations 中 loop_state='parked' 的会话(重启中断待续), // V49 可恢复入口(B3):数据源双轨——conversations 中 loop_state='parked'(实时投影,
// 独立分区展示在任务面板下方,点击切回(继续与否由用户在会话内决定,不做自动续跑) // 侧栏 limit 内)+ ai_list_recoverable 完备真相 IPC(面板展开时拉取,补侧栏截断漏项)。
const recoverableTasks = computed(() => { // 合并去重按 id;点击切回,继续与否由用户在会话内决定,不做自动续跑
const recoverableFromList = computed(() => {
return store.state.conversations return store.state.conversations
.filter(c => c.loop_state === 'parked' && c.id !== store.state.activeConversationId) .filter(c => c.loop_state === 'parked' && c.id !== store.state.activeConversationId)
.slice(0, 10)
.map(c => ({ id: c.id, title: c.title || t('aiChat.untitledConversation') })) .map(c => ({ id: c.id, title: c.title || t('aiChat.untitledConversation') }))
}) })
const recoverableFromIpc = ref<{ id: string; title: string | null }[]>([])
async function refreshRecoverable() {
try {
const items = await aiApi.listRecoverable()
recoverableFromIpc.value = items.map(i => ({ id: i.id, title: i.title }))
} catch { /* IPC 失败退回 list 投影,不阻断面板 */ }
}
const recoverableTasks = computed(() => {
const seen = new Set<string>()
const merged: { id: string; title: string }[] = []
for (const item of [...recoverableFromList.value, ...recoverableFromIpc.value]) {
if (item.id === store.state.activeConversationId || seen.has(item.id)) continue
seen.add(item.id)
merged.push({ id: item.id, title: item.title || t('aiChat.untitledConversation') })
}
return merged.slice(0, 10)
})
async function switchToRunning(id: string) { async function switchToRunning(id: string) {
runningTasksExpanded.value = false runningTasksExpanded.value = false
if (id !== store.state.activeConversationId) { if (id !== store.state.activeConversationId) {
@@ -593,6 +610,10 @@ function togglePanel(panel: 'goals' | 'history' | 'summary' | 'context' | 'runni
const willOpen = !refs[panel].value const willOpen = !refs[panel].value
goalsExpanded.value = historyExpanded.value = summaryExpanded.value = contextExpanded.value = runningTasksExpanded.value = false goalsExpanded.value = historyExpanded.value = summaryExpanded.value = contextExpanded.value = runningTasksExpanded.value = false
refs[panel].value = willOpen refs[panel].value = willOpen
// B3:任务面板展开时拉一次可恢复清单完备真相(补侧栏 limit 截断漏项);失败退回 list 投影
if (panel === 'running' && willOpen) {
void refreshRecoverable()
}
} }
const contextInfo = computed<ContextInfo>(() => { const contextInfo = computed<ContextInfo>(() => {