Files
DevFlow/src/api/ai.ts
绝尘 53e1c1da77 重构: 对话透明化 L1 + ③.2 审批拆分 + ⑥.4 @展开 + DAG 展示
修复 tool_result 压缩零效果(BUG-260628-01):单行/少行/JSON 大字符
串字段逃逸压缩的问题,基于实测数据(53/94 次迭代零节省)调参,
增加字符级保底截断,压缩有效率从 8% 提升至 ~85%+
2026-06-28 03:31:40 +08:00

320 lines
14 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, 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<string> {
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<string> {
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<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 })
},
/**
* 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<string> {
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<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 态用)。
* 阶段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<void> {
return invoke('ai_chat_clear')
},
/**
* 停止当前生成。
* F-260616-09 B 批4(决策 e):传 convId,停止仅作用于目标 conv(不杀其他后台 conv 的 loop)。
* convId 省略时后端 fallback active conv。
*/
stopChat(convId?: string | null): Promise<void> {
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<boolean> {
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<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
modelConfigs: ModelConfig[]
}): 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,
modelConfigs: input.modelConfigs,
})
},
/** 设置活跃提供商 */
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')
},
/**
* Input Augmentation:进程内重载 skills 缓存(OnceLock → RwLock 后支持热重载)。
*
* 用户增删/修改 SKILL.md 后无需重启即可让后续 `/技能` 注入读到最新内容。
* 后端 ai_reload_skills 复用 scan_skills 走剥 frontmatter 的读取 + invalidate 缓存,
* 返回重载后的全量 skills 列表(供前端同步刷新 skills state)。
*/
reloadSkills(): Promise<SkillInfo[]> {
return invoke('ai_reload_skills')
},
/** F-260619-03 Phase A: 获取 AI 工具文件访问授权目录列表(含 workspace_root) */
getAllowedDirs(): Promise<string[]> {
return invoke<string[]>('ai_get_allowed_dirs')
},
/** F-260619-03 Phase A: 设置 AI 工具授权目录(持久化 + 同步内存白名单),返回规范化后的列表 */
setAllowedDirs(dirs: string[]): Promise<string[]> {
return invoke<string[]>('ai_set_allowed_dirs', { dirs })
},
/** 监听 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 })
},
/**
* 更新对话目标钉扎列表(G1 目标钉扎持久化:对话透明化 L1)。
* 接收完整目标列表(非增量,前端增/删后传全量 new Vec)。
*/
updateConversationGoals(conversationId: string, goals: string[]): Promise<void> {
return invoke('ai_update_conversation_goals', { conversationId, goals })
},
}