新增: 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:
178
src/composables/ai/useAiConversations.ts
Normal file
178
src/composables/ai/useAiConversations.ts
Normal 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 }
|
||||
Reference in New Issue
Block a user