新增: Phase2 阶段收尾(Sprint 1-20)

重构:删 5 零引用 crate(df-evolve/plugin/stages/task/traceability)+ 清死模块、ai.rs 拆 11 子 module、ai.ts 拆 6 composable、i18n 拆目录
功能:知识库全栈(df-project/scan + CRUD + 时间线 + 前端)、Settings 拆分、appSettings KV 迁移、模型池、LLM 并发 Semaphore
修复:审批持久化根治、ConditionEngine 默认拒绝、NodeRegistry unimplemented 清除、promote 补偿删除、工具结果截断 50KB、路径校验防 symlink 逃逸
文档:B-03 人工审批设计、决策记录三分档、规格契约自检、经验记录、todo 看板、PROGRESS 更新

详见 PROGRESS.md。src-tauri/儿童每日打卡应用/ 与本项目无关,已排除。
This commit is contained in:
2026-06-14 14:08:20 +08:00
parent 98393b4908
commit cf017f81e2
167 changed files with 19549 additions and 6886 deletions

View File

@@ -0,0 +1,178 @@
//! 对话管理 — load/list/new/switch/delete/rename/archive + 侧栏折叠态
//!
//! 耦合:
//! - 多处调 useAiEvents.notifyConversationChanged
//! - newConversation/switchConversation 写 appSettings 持久化活跃会话 id
import { aiApi } from '../../api'
import { useAppSettingsStore } from '../../stores/appSettings'
import { state } from '../../stores/ai'
import { notifyConversationChanged } from './useAiEvents'
import { persistUiState } from './useAiPanel'
import type { AiToolCallInfo } from '../../api/types'
const appSettings = useAppSettingsStore()
/** 拉取会话列表;首次加载时若 appSettings 仍有有效 active-conv 且当前无活跃对话则恢复 */
async function loadConversations() {
try {
state.conversations = await aiApi.listConversations()
// 恢复上次活跃对话(仅在首次加载、ID 仍有效且当前无活跃对话时)
const pending = appSettings.get<string | null>('df-ai-active-conv', null)
if (pending && !state.activeConversationId && state.conversations.some(c => c.id === pending)) {
await switchConversation(pending)
}
} catch {
// 静默失败,不影响主流程
}
}
/** 新建空对话并切过去 */
async function newConversation() {
const result = await aiApi.createConversation()
state.activeConversationId = result.id
void appSettings.set('df-ai-active-conv', result.id)
state.messages = []
state.currentText = ''
state.pendingApprovals = []
state.streaming = false
await loadConversations()
notifyConversationChanged()
}
/** 切换到指定会话:加载历史消息(含 tool_calls 回填 + tool_result 映射 + pending 审批恢复) */
export async function switchConversation(id: string) {
// 允许生成中切换:后台继续生成,事件按 conversation_id 路由不污染当前视图
const detail = await aiApi.switchConversation(id)
state.activeConversationId = id
void appSettings.set('df-ai-active-conv', id)
try {
const rawMsgs = typeof detail.messages === 'string'
? JSON.parse(detail.messages)
: detail.messages
// 构建 tool_call_id → tool_result 映射,用于回填工具执行结果
const toolResultMap = new Map<string, string>()
for (const m of rawMsgs) {
if (m.role === 'tool' && m.tool_call_id) {
toolResultMap.set(m.tool_call_id, m.content || '')
}
}
state.messages = rawMsgs
.filter((m: any) => m.role !== 'tool')
.map((m: any, i: number) => ({
id: `loaded-${i}`,
role: m.role,
content: m.content || '',
model: m.model,
timestamp: Date.now(),
toolCalls: m.tool_calls?.map((tc: any) => ({
id: tc.id,
name: tc.function?.name || '',
args: typeof tc.function?.arguments === 'string'
? JSON.parse(tc.function.arguments || '{}')
: tc.function?.arguments || {},
status: 'completed' as const,
result: toolResultMap.get(tc.id),
})),
}))
} catch {
state.messages = []
}
// 加载历史对话 token 总量(来自 DB summary);切换对话清空实时值
const conv = state.conversations.find(c => c.id === id)
if (conv && conv.prompt_tokens != null && conv.completion_tokens != null) {
state.convTokenTotal = {
prompt: conv.prompt_tokens,
completion: conv.completion_tokens,
total: conv.prompt_tokens + conv.completion_tokens,
}
} else {
state.convTokenTotal = null
}
state.lastTokenUsage = null
state.currentText = ''
// 恢复该对话积压的待审批:重启后后端从审计表重建了 pending_approvals,
// 此处查回并把对应 toolCard.status 置 pending_approval,使审批卡片重新可见
try {
const pending = await aiApi.pendingToolCalls(id)
if (pending.length) {
const pendingIds = new Set(pending.map(p => p.tool_call_id))
const restored: AiToolCallInfo[] = []
for (const m of state.messages) {
for (const tc of (m.toolCalls || [])) {
if (pendingIds.has(tc.id)) {
tc.status = 'pending_approval'
restored.push({ id: tc.id, name: tc.name, args: tc.args, status: 'pending_approval' })
}
}
}
state.pendingApprovals = restored
} else {
state.pendingApprovals = []
}
} catch {
state.pendingApprovals = []
}
}
/** 删除会话;若删的是当前活跃会话则清空消息+移除活跃 id 持久化 */
async function deleteConversation(id: string) {
await aiApi.deleteConversation(id)
if (state.activeConversationId === id) {
state.activeConversationId = null
void appSettings.remove('df-ai-active-conv')
state.messages = []
}
await loadConversations()
notifyConversationChanged()
}
/** 重命名会话(后端 + 本地侧栏摘要同步) */
async function renameConversation(id: string, title: string) {
await aiApi.renameConversation(id, title)
// 本地同步更新侧栏摘要标题
const conv = state.conversations.find(c => c.id === id)
if (conv) conv.title = title
notifyConversationChanged()
}
/** 归档/取消归档(后端 + 本地侧栏分组同步) */
async function archiveConversation(id: string, archived: boolean) {
await aiApi.archiveConversation(id, archived)
// 本地同步归档态(侧栏立即移入/移出归档分组)
const conv = state.conversations.find(c => c.id === id)
if (conv) conv.archived = archived
notifyConversationChanged()
}
/** 折叠/展开归档分组 */
function toggleArchivedFold() {
state.archivedCollapsed = !state.archivedCollapsed
persistUiState()
}
/** 展开/收起侧栏(会话列表) */
function toggleSidebar() {
state.sidebarOpen = !state.sidebarOpen
persistUiState()
}
export function useAiConversations() {
return {
loadConversations,
newConversation,
switchConversation,
deleteConversation,
renameConversation,
archiveConversation,
toggleArchivedFold,
toggleSidebar,
}
}
export { loadConversations }