Files
DevFlow/src/api/ai.ts

250 lines
9.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! AI 聊天 API — IPC invoke 封装 + 事件监听
import { invoke } from '@tauri-apps/api/core'
import { listen, type UnlistenFn } from '@tauri-apps/api/event'
import type { AiChatEvent, AiConversationDetail, AiConversationSummary, AiProviderConfig, ModelConfig, SkillInfo } from './types'
export const aiApi = {
/** 发送消息(非阻塞,通过 ai-chat-event 流式返回skill 传技能名则注入其 SKILL.md */
sendMessage(message: string, language?: string, skill?: string, modelOverride?: string | null): Promise<string> {
return invoke('ai_chat_send', {
message,
language: language || 'zh-CN',
skill: skill || null,
modelOverride: modelOverride || null,
})
},
/** 强制发送B-260616-02: 复位 generating 残留后走 send 同款流程) */
forceSend(message: string, language?: string, skill?: string, modelOverride?: string | null): Promise<string> {
return invoke('ai_chat_force_send', {
message,
language: language || 'zh-CN',
skill: skill || null,
modelOverride: modelOverride || null,
})
},
/**
* 重新生成最后一条 AI 回复(UX-02)。
* 后端:占用 generating → 弹出末尾 AI 回复(保留触发它的 user 消息)→ 重跑 agentic loop。
* 前端消费:与 sendMessage 一样需本地 push 空气泡占位 + 置 streaming(见 useAiSend.regenerate)。
*/
regenerate(conversationId: string, language?: string, modelOverride?: string | null): Promise<string> {
return invoke('ai_regenerate', {
conversationId,
language: language || 'zh-CN',
modelOverride: modelOverride || null,
})
},
/**
* 编辑最后一条 user 消息并重新生成(UX-09)。
* 后端:占用 generating → 替换末条 user content → 其后消息标 truncated(软删)→ 重跑 agentic loop。
* 前端消费:调 editMessage 前需移除被编辑消息之后的所有消息(视图截断)+ push 空气泡占位 + 置 streaming。
* 中间 user 消息编辑被后端拒绝(只允许末条)。
*/
editMessage(conversationId: string, newMessage: string, language?: string, modelOverride?: string | null): Promise<string> {
return invoke('ai_chat_edit', {
conversationId,
newMessage,
language: language || 'zh-CN',
modelOverride: modelOverride || null,
})
},
/** 批准/拒绝工具调用 */
approve(toolCallId: string, approved: boolean): Promise<string> {
return invoke('ai_approve', { toolCallId, approved })
},
/**
* 续跑 agentic 循环(F-260616-03:达 max_iterations 暂停态点继续)。
* 后端 ai_continue_loop:复位 stop_flag → try_continue_agent_loop 重新 spawn
* run_agentic_loop(iteration 从 0 重计,再跑 max_iterations 轮)。
*/
continueLoop(conversationId: string): Promise<string> {
return invoke('ai_continue_loop', { conversationId })
},
/**
* 停止 agentic 循环并走完成流程(F-260616-03:达 max_iterations 暂停态点停止)。
* 后端 ai_stop_loop:复位 generating + emit AiCompleted(暂停前已 save)。
*/
stopLoop(conversationId: string): Promise<string> {
return invoke('ai_stop_loop', { conversationId })
},
/** 查询某对话积压的待审批工具(重启后恢复 toolCard pending_approval 态用) */
pendingToolCalls(convId: string): Promise<{ tool_call_id: string; conversation_id: string | null }[]> {
return invoke('ai_pending_tool_calls', { convId })
},
/** 清空对话 */
clearChat(): Promise<void> {
return invoke('ai_chat_clear')
},
/** 停止当前生成 */
stopChat(): Promise<void> {
return invoke('ai_chat_stop')
},
// ── F-15 阶段2: 手动上下文管理(清空 / 压缩) ──
// 后端经 ai-chat-event emit 生命周期事件:
// ai_context_cleared { conversation_id } — 清空完成
// ai_compressing { conversation_id } — 压缩开始(loading)
// ai_compressed { conversation_id, summary } — 压缩成功(摘要随事件到前端)
// ai_error { message, conversation_id } — 压缩失败
// 历史消息落库时 status 标 archived_segment(清空)/ compressed(压缩),
// 前端按 status 折叠成"已归档/已压缩"分隔条(可展开看原文)。
/** 清空当前对话上下文(历史消息归档保留,DB 不删;新对话不受影响) */
clearContext(conversationId: string): Promise<void> {
return invoke('ai_chat_clear_context', { conversationId })
},
/** 压缩当前对话上下文(LLM 摘要落 system 消息 + 历史消息标 compressed 归档) */
compressContext(conversationId: string, language: string): Promise<void> {
return invoke('ai_chat_compress_context', { conversationId, language })
},
/** 列出所有 AI 提供商 */
listProviders(): Promise<AiProviderConfig[]> {
return invoke('ai_list_providers')
},
/** 保存 AI 提供商 */
saveProvider(input: {
id?: string
name: string
providerType: string
baseUrl: string
apiKey: string
defaultModel: string
}): Promise<string> {
return invoke('ai_save_provider', {
id: input.id,
name: input.name,
providerType: input.providerType,
baseUrl: input.baseUrl,
apiKey: input.apiKey,
defaultModel: input.defaultModel,
})
},
/** 设置活跃提供商 */
setProvider(providerId: string): Promise<void> {
return invoke('ai_set_provider', { providerId })
},
/**
* 更新 provider 负载均衡池配置(F-260614-04c):仅 UPDATE enabled/weight + 重建 per_provider caps。
* 走轻量分支,不经 ai_save_provider 全量 INSERT OR REPLACE(避免空 api_key 触发 R-PD-1 密钥迁移)。
* 后端 weight clamp [0,100];落库后立即 reload_provider_caps,下条消息即按新配置 acquire。
*/
updateProviderPool(providerId: string, enabled: boolean, weight: number): Promise<void> {
return invoke('ai_update_provider_pool', { providerId, enabled, weight })
},
/** 删除 AI 提供商 */
deleteProvider(providerId: string): Promise<void> {
return invoke('ai_delete_provider', { providerId })
},
/**
* 测试连接并拉取厂商模型列表(F-01 阶段5)。
* 后端:DB 取 provider → FR-S1 内存解析 api_key → fetch_and_probe
* (网络拉模型名 + 探测 4 维度)→ 写回 model_configs → 返回。
* 返回值 ModelConfig 不含 api_key(FR-S1 闭环)。
* 需已落库的 providerId(新建态先保存再拉取)。
*/
fetchModels(providerId: string): Promise<ModelConfig[]> {
return invoke('ai_fetch_models', { providerId })
},
/**
* 单模型探测(F-01 阶段5):纯 CPU 启发式 + 预设表,无网络。
* 返回填充了 probe_source 的 ModelConfig。用途:手动补模型名后探测能力维度。
*/
probeModel(modelId: string): Promise<ModelConfig> {
return invoke('ai_probe_model', { modelId })
},
/** 设置 LLM 调用并发上限(全局 / 单对话,运行时即时生效;软收敛不中断进行中调用) */
setConcurrencyConfig(globalLimit: number, perConvLimit: number): Promise<void> {
return invoke('ai_set_concurrency_config', { globalLimit, perConvLimit })
},
/** 设置 Agentic 循环最大轮次运行时调整loop 入口锁定边界,热改下次发消息生效) */
setAgentMaxIterations(value: number): Promise<void> {
return invoke('ai_set_agent_max_iterations', { value })
},
/** 设置流式对话失败自动重试次数F-260616-07只重试流前失败0=不重试默认3上限10 */
setAgentMaxRetries(value: number): Promise<void> {
return invoke('ai_set_agent_max_retries', { value })
},
/** 列出本机 Claude 技能skills + commands + plugins */
listSkills(): Promise<SkillInfo[]> {
return invoke('ai_list_skills')
},
/** 监听 AI 聊天事件 */
onEvent(callback: (event: AiChatEvent) => void): Promise<UnlistenFn> {
return listen<AiChatEvent>('ai-chat-event', (e) => {
callback(e.payload)
})
},
// ── 对话管理 ──
/** 创建新对话 */
createConversation(): Promise<{ id: string }> {
return invoke('ai_conversation_create')
},
/** 列出对话摘要limit 默认 50includeArchived 默认 false 隐藏归档) */
listConversations(limit?: number, includeArchived?: boolean): Promise<AiConversationSummary[]> {
return invoke('ai_conversation_list', {
limit: limit ?? null,
includeArchived: includeArchived ?? false,
})
},
/** 切换到指定对话 */
switchConversation(conversationId: string): Promise<AiConversationDetail> {
return invoke('ai_conversation_switch', { conversationId })
},
/** 删除对话 */
deleteConversation(conversationId: string): Promise<void> {
return invoke('ai_conversation_delete', { conversationId })
},
/** 重命名对话标题 */
renameConversation(conversationId: string, title: string): Promise<void> {
return invoke('ai_conversation_rename', { conversationId, title })
},
/** 归档/取消归档对话 */
archiveConversation(conversationId: string, archived: boolean): Promise<void> {
return invoke('ai_conversation_archive', { conversationId, archived })
},
/** 置顶/取消置顶对话(UX-17) */
setPinnedConversation(conversationId: string, pinned: boolean): Promise<void> {
return invoke('ai_conversation_set_pinned', { conversationId, pinned })
},
/**
* 导出对话为指定格式(UX-18:对话导出;消费 batch46 ai_conversation_export IPC)。
* 后端返回导出内容 String(markdown/json/txt),前端落 Blob 下载或复制剪贴板。
* @param format 'markdown' | 'json' | 'txt'
*/
exportConversation(conversationId: string, format: 'markdown' | 'json' | 'txt'): Promise<string> {
return invoke('ai_conversation_export', { conversationId, format })
},
}