//! 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, ContentPart, MentionSpan, ModelConfig, SkillInfo } from './types' export const aiApi = { /** * 发送消息(非阻塞,通过 ai-chat-event 流式返回);skill 传技能名则注入其 SKILL.md。 * F-260614-05 Phase 2c: parts 透传给后端 ai_chat_send → user_parts → provider vision 端点。 * parts 为空/undefined → 后端走 user() 纯文本(向后兼容)。 * Input Augmentation: mentionSpans 透传给后端 ai_chat_send 的 mention_spans 参数 * (后端 resolve_and_inject 按 kind 投影成 Augmentation 注入 system prompt)。 * undefined/空数组 → 不传(后端 None 走无 mention 路径,向后兼容)。 */ sendMessage(message: string, language?: string, skill?: string, modelOverride?: string | null, parts?: ContentPart[], conversationId?: string | null, mentionSpans?: MentionSpan[]): Promise { return invoke('ai_chat_send', { message, language: language || 'zh-CN', skill: skill || null, modelOverride: modelOverride || null, parts: parts && parts.length > 0 ? parts : null, // F-260616-09 B 批4(决策 e):传 conv_id,操作指定 conv 的 per_conv(不依赖 active 单值)。 conversationId: conversationId || null, // Input Augmentation: 非空数组才传(undefined/空让 payload 不含该键,后端默认 None)。 mentionSpans: mentionSpans && mentionSpans.length > 0 ? mentionSpans : null, }) }, /** 强制发送(B-260616-02: 复位 generating 残留后走 send 同款流程);parts/mentionSpans 透传同 sendMessage。 */ forceSend(message: string, language?: string, skill?: string, modelOverride?: string | null, parts?: ContentPart[], conversationId?: string | null, mentionSpans?: MentionSpan[]): Promise { return invoke('ai_chat_force_send', { message, language: language || 'zh-CN', skill: skill || null, modelOverride: modelOverride || null, parts: parts && parts.length > 0 ? parts : null, // F-260616-09 B 批4(决策 e):传 conv_id,强制复位+发送仅作用于目标 conv。 conversationId: conversationId || null, // Input Augmentation: 同 sendMessage,非空数组才传(后端 mention_spans 参数)。 mentionSpans: mentionSpans && mentionSpans.length > 0 ? mentionSpans : null, }) }, /** * 重新生成最后一条 AI 回复(UX-02)。 * 后端:占用 generating → 弹出末尾 AI 回复(保留触发它的 user 消息)→ 重跑 agentic loop。 * 前端消费:与 sendMessage 一样需本地 push 空气泡占位 + 置 streaming(见 useAiSend.regenerate)。 */ regenerate(conversationId: string, language?: string, modelOverride?: string | null): Promise { 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 { return invoke('ai_chat_edit', { conversationId, newMessage, language: language || 'zh-CN', modelOverride: modelOverride || null, }) }, /** 批准/拒绝工具调用 */ approve(toolCallId: string, approved: boolean): Promise { return invoke('ai_approve', { toolCallId, approved }) }, /** * F-260619-03 Phase B: 路径授权弹窗决策(消费 AiDirAuthRequired 挂起)。 * decision: 'once'(本次单次,执行后清)/ 'session'(当前会话,切会话清)/ 'always'(始终,持久化)/ 'deny'(拒绝)。 * 后端 remove pending → 写授权目录(once/session/persistent)→ 执行工具 → try_continue 恢复 loop。 */ authorizeDir(toolCallId: string, decision: 'once' | 'session' | 'always' | 'deny'): Promise { return invoke('ai_authorize_dir', { toolCallId, decision }) }, /** * 续跑 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 { return invoke('ai_continue_loop', { conversationId }) }, /** * 停止 agentic 循环并走完成流程(F-260616-03:达 max_iterations 暂停态点停止)。 * 后端 ai_stop_loop:复位 generating + emit AiCompleted(暂停前已 save)。 */ stopLoop(conversationId: string): Promise { return invoke('ai_stop_loop', { conversationId }) }, /** 查询某对话积压的待审批工具(重启后恢复 toolCard pending_approval 态用)。 * 阶段4:返 kind 字段(risk/path),前端按 kind 渲染不同决策按钮。 */ pendingToolCalls(convId: string): Promise<{ tool_call_id: string; conversation_id: string | null; kind: 'risk' | 'path' }[]> { return invoke('ai_pending_tool_calls', { convId }) }, /** 清空对话 */ clearChat(): Promise { return invoke('ai_chat_clear') }, /** * 停止当前生成。 * F-260616-09 B 批4(决策 e):传 convId,停止仅作用于目标 conv(不杀其他后台 conv 的 loop)。 * convId 省略时后端 fallback active conv。 */ stopChat(convId?: string | null): Promise { return invoke('ai_chat_stop', { conversationId: convId || null }) }, /** * 查询后端真实 generating 状态(B-260615-22:发送前 IPC 查后端真值)。 * F-260616-09 B 批4(决策 e):传 convId 精确查指定 conv;省略时后端 fallback active conv。 */ isGenerating(convId?: string | null): Promise { return invoke('ai_is_generating', { conversationId: convId || null }) }, // ── 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 { return invoke('ai_chat_clear_context', { conversationId }) }, /** 压缩当前对话上下文(LLM 摘要落 system 消息 + 历史消息标 compressed 归档) */ compressContext(conversationId: string, language: string): Promise { return invoke('ai_chat_compress_context', { conversationId, language }) }, /** 列出所有 AI 提供商 */ listProviders(): Promise { return invoke('ai_list_providers') }, /** 保存 AI 提供商 */ saveProvider(input: { id?: string name: string providerType: string baseUrl: string apiKey: string defaultModel: string modelConfigs: ModelConfig[] }): Promise { return invoke('ai_save_provider', { id: input.id, name: input.name, providerType: input.providerType, baseUrl: input.baseUrl, apiKey: input.apiKey, defaultModel: input.defaultModel, modelConfigs: input.modelConfigs, }) }, /** 设置活跃提供商 */ setProvider(providerId: string): Promise { 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 { return invoke('ai_update_provider_pool', { providerId, enabled, weight }) }, /** 删除 AI 提供商 */ deleteProvider(providerId: string): Promise { 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 { return invoke('ai_fetch_models', { providerId }) }, /** * 单模型探测(F-01 阶段5):纯 CPU 启发式 + 预设表,无网络。 * 返回填充了 probe_source 的 ModelConfig。用途:手动补模型名后探测能力维度。 */ probeModel(modelId: string): Promise { return invoke('ai_probe_model', { modelId }) }, /** 设置 LLM 调用并发上限(全局 / 单对话,运行时即时生效;软收敛不中断进行中调用) */ setConcurrencyConfig(globalLimit: number, perConvLimit: number): Promise { return invoke('ai_set_concurrency_config', { globalLimit, perConvLimit }) }, /** 设置 Agentic 循环最大轮次(运行时调整;loop 入口锁定边界,热改下次发消息生效) */ setAgentMaxIterations(value: number): Promise { return invoke('ai_set_agent_max_iterations', { value }) }, /** 设置流式对话失败自动重试次数(F-260616-07:只重试流前失败,0=不重试,默认3,上限10) */ setAgentMaxRetries(value: number): Promise { return invoke('ai_set_agent_max_retries', { value }) }, /** 列出本机 Claude 技能(skills + commands + plugins) */ listSkills(): Promise { return invoke('ai_list_skills') }, /** * Input Augmentation:进程内重载 skills 缓存(OnceLock → RwLock 后支持热重载)。 * * 用户增删/修改 SKILL.md 后无需重启即可让后续 `/技能` 注入读到最新内容。 * 后端 ai_reload_skills 复用 scan_skills 走剥 frontmatter 的读取 + invalidate 缓存, * 返回重载后的全量 skills 列表(供前端同步刷新 skills state)。 */ reloadSkills(): Promise { return invoke('ai_reload_skills') }, /** F-260619-03 Phase A: 获取 AI 工具文件访问授权目录列表(含 workspace_root) */ getAllowedDirs(): Promise { return invoke('ai_get_allowed_dirs') }, /** F-260619-03 Phase A: 设置 AI 工具授权目录(持久化 + 同步内存白名单),返回规范化后的列表 */ setAllowedDirs(dirs: string[]): Promise { return invoke('ai_set_allowed_dirs', { dirs }) }, /** 监听 AI 聊天事件 */ onEvent(callback: (event: AiChatEvent) => void): Promise { return listen('ai-chat-event', (e) => { callback(e.payload) }) }, // ── 对话管理 ── /** 创建新对话 */ createConversation(): Promise<{ id: string }> { return invoke('ai_conversation_create') }, /** 列出对话(摘要;limit 默认 50,includeArchived 默认 false 隐藏归档) */ listConversations(limit?: number, includeArchived?: boolean): Promise { return invoke('ai_conversation_list', { limit: limit ?? null, includeArchived: includeArchived ?? false, }) }, /** 切换到指定对话 */ switchConversation(conversationId: string): Promise { return invoke('ai_conversation_switch', { conversationId }) }, /** 删除对话 */ deleteConversation(conversationId: string): Promise { return invoke('ai_conversation_delete', { conversationId }) }, /** 重命名对话标题 */ renameConversation(conversationId: string, title: string): Promise { return invoke('ai_conversation_rename', { conversationId, title }) }, /** 归档/取消归档对话 */ archiveConversation(conversationId: string, archived: boolean): Promise { return invoke('ai_conversation_archive', { conversationId, archived }) }, /** 置顶/取消置顶对话(UX-17) */ setPinnedConversation(conversationId: string, pinned: boolean): Promise { 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 { return invoke('ai_conversation_export', { conversationId, format }) }, /** * 更新对话目标钉扎列表(G1 目标钉扎持久化:对话透明化 L1)。 * 接收完整目标列表(非增量,前端增/删后传全量 new Vec)。 */ updateConversationGoals(conversationId: string, goals: string[]): Promise { return invoke('ai_update_conversation_goals', { conversationId, goals }) }, }