优化: 清理无意义注释(走查编号前缀/过时历史标注/死代码注释)

This commit is contained in:
lxy
2026-08-15 18:05:43 +08:00
parent 5a4c821629
commit 5ac815b9ca
137 changed files with 735 additions and 737 deletions
+1 -1
View File
@@ -241,7 +241,7 @@ export function setConvState(convId: string | null | undefined, s: ConvState): v
// ── F-09 B 路线 per-conv 流式态(streaming/currentText) ──
//
// 背景(DEC-07a 父④):原 `state.streaming`(bool)/`state.currentText`(string)是 store 全局单例,
// 背景(a 父④):原 `state.streaming`(bool)/`state.currentText`(string)是 store 全局单例,
// F-09 多会话并发下:A 后台生成中切到 B 会话,B 末条 AI 气泡命中 `isLastAi && streaming &&
// currentText` 会渲染 A 残留 currentText(BUG-260624-01 同源根因)。本批改 per-conv Map,各会话
// 独立累积流式文本/流式开关,切会话即切到该会话的 stream state,真并发不串话。
+4 -4
View File
@@ -22,7 +22,7 @@ import { findToolCall } from './aiShared'
import { dispatchApprovalIPC } from './useToolApproval'
/**
* F-260616-06: 审批按钮防抖守卫——同一 id 短期多次点击只发一次 IPC。
* 审批按钮防抖守卫——同一 id 短期多次点击只发一次 IPC。
* IPC 往返 <100ms300ms 窗口足够覆盖竞态。
*/
const _pendingApprovalIds = new Set<string>()
@@ -30,7 +30,7 @@ const _pendingApprovalIds = new Set<string>()
/**
* 工具审批:保持 pending_approval 让审批卡片可见(按钮 loading 由 ToolCard 本地 ref 持有),
* IPC 失败时改 completed+错误文案。后端回事件后由 useAiEvents 转 completed/rejected。
* B-260616-08:不再乐观置 running——原写法让 .ai-tool-approval 整块消失(切骨架屏),
* 不再乐观置 running——原写法让 .ai-tool-approval 整块消失(切骨架屏),
* 按钮无 loading 中间态、用户无重审入口;loading 现下沉到 ToolCard 局部 ref,语义正确。
*
* path_auth 审批链阶段3b(统一审批模型):**按 tc.kind 分派**两条 IPC——
@@ -46,7 +46,7 @@ const _pendingApprovalIds = new Set<string>()
* @param decision path 类专用('once'|'session'|'always'|'deny');risk 类忽略。调用方须据 tc.kind 传对应入参。
*/
async function approveToolCall(toolCallId: string, approved: boolean, decision?: 'once' | 'session' | 'always' | 'deny') {
// F-260616-06: 防抖——同一 id 仍在处理中则跳过(竞态点击/网络慢重试)
// 防抖——同一 id 仍在处理中则跳过(竞态点击/网络慢重试)
if (_pendingApprovalIds.has(toolCallId)) return
_pendingApprovalIds.add(toolCallId)
try {
@@ -96,7 +96,7 @@ async function approveToolCall(toolCallId: string, approved: boolean, decision?:
// IPC 未送达:用户已点过按钮,清看门狗(避免 130s 后误触发假错误)再回滚到失败态
// TD-260621-03 per-conv:清审批 tc 所属会话的 timer(与上方 reset 同 conv,对称成对)。
clearStreamWatchdog(approvalConvId)
state.queue = [] // B-32:IPC 失败即对话结束,清队列防生成中入队消息静默丢失
state.queue = [] // IPC 失败即对话结束,清队列防生成中入队消息静默丢失
// 仅 IPC 真失败(审批已处理/网络断)走到这里:后端工具失败已改走 emit completed,不进此分支
// 不回滚 pending_approval(会卡死按钮),改设 completed + 错误提示,让用户知晓失败
console.error('[AI] 审批操作未送达后端:', e)
+6 -6
View File
@@ -45,7 +45,7 @@ async function loadConversations() {
await switchConversation(pending)
}
} catch (e) {
// UX-260617-08:网络抖动/IPC 断开时原静默失败,列表可能突空用户不知原因。
// 网络抖动/IPC 断开时原静默失败,列表可能突空用户不知原因。
// 此处保留旧列表(赋值语句抛出,旧 state.conversations 不被覆盖)+ 推错误气泡反馈。
// 仅首次加载(state.conversations 为空)时推气泡,避免拉取/侧栏刷新重试时反复弹错。
console.error('[AI] 加载会话列表失败:', e)
@@ -126,7 +126,7 @@ async function newConversation() {
}
}
// 切换 token:快速连点 A→B 时,后返回的 A 响应按 token 丢弃,防 messages 错配(FR-R1)
// 切换 token:快速连点 A→B 时,后返回的 A 响应按 token 丢弃,防 messages 错配
let _latestSwitchId = 0
// 最近一次切换的切换窗口缓冲(成功提交路径恢复续显用;失败/过期路径丢弃,
// 防陈旧文本在后续切换被误恢复造成内容重复)。由 switchConversation 的 finally 统一填充。
@@ -169,7 +169,7 @@ function parseConvMessages(rawMsgs: any[], fallbackPrefix: string): AiMessage[]
}
}
return rawMsgs
// 过滤 tool 消息 + truncated 软删消息(UX-09:编辑某条 user 后其后消息标 truncated,
// 过滤 tool 消息 + truncated 软删消息编辑某条 user 后其后消息标 truncated,
// 保留 DB 可追溯但从视图移除;前端无 status 字段故按原始 JSON 字段过滤)
.filter((m: any) => m.role !== 'tool' && m.status !== 'truncated')
.map((m: any, i: number) => ({
@@ -177,7 +177,7 @@ function parseConvMessages(rawMsgs: any[], fallbackPrefix: string): AiMessage[]
role: m.role,
content: m.content || '',
model: m.model,
// 用后端持久化的真实时间(BUG-260618-01);老数据无 timestamp 字段回退 Date.now()
// 用后端持久化的真实时间;老数据无 timestamp 字段回退 Date.now()
timestamp: typeof m.timestamp === 'number' ? m.timestamp : Date.now(),
// 消息级 token 回显:assistant 消息若 DB 持久化了 prompt_tokens/completion_tokens
// (V38 迁移后 push_assistant_message 落库的本轮 token),映射回 tokenUsage 供
@@ -350,7 +350,7 @@ export async function switchConversation(id: string, force = false) {
state.messages = parseConvMessages(rawMsgs, 'loaded')
// parse 成功才置 active(保证 active 与 messages 一致)
state.activeConversationId = id
// CSW-P1-4: 切到 round0 进行中的会话时,后端该轮 per_conv 只有 user 消息(assistant 占位
// 切到 round0 进行中的会话时,后端该轮 per_conv 只有 user 消息(assistant 占位
// 轮末才入),末条是 user → 流式 currentText 无 assistant 承载(MessageList isLastAi 不命中,
// flushCurrentText 遇 user break)整轮回复丢失。补前端占位 assistant 气泡承接 currentText,
// 生成完成由 AiCompleted/AiAgentRound 收尾 flushCurrentText 回填 content。
@@ -410,7 +410,7 @@ export async function switchConversation(id: string, force = false) {
// 恢复该对话积压的待审批:重启后后端从审计表重建了挂起审批,
// 此处查回并把对应 toolCard.status 置为待审批,使审批卡片重新可见。
// 按 IPC 返的 kind 渲染:'path' 类显 once/always/deny,'risk' 类显 approve/reject。
// F2-1(AC-EFF-F2-1):纯文本会话跳过挂起查询(省 1 次 IPC + 后端全扫)——仅当目标会话消息
// F2-1纯文本会话跳过挂起查询(省 1 次 IPC + 后端全扫)——仅当目标会话消息
// 含工具卡,或目标会话生成中(可能在途产生待审批)才需要恢复挂起。纯文本/无工具历史直接置空。
const hasToolCalls = state.messages.some(m => (m.toolCalls ?? []).length > 0)
if (!targetGen && !hasToolCalls) {
+8 -8
View File
@@ -19,7 +19,7 @@ import type { AiMessage, AiProviderConfig } from '@/api/types'
const appSettings = useAppSettingsStore()
/** 侧栏宽度可拖拽范围(UX-2025-16)。120=窄屏友好,280=宽屏舒适 */
/** 侧栏宽度可拖拽范围。120=窄屏友好,280=宽屏舒适 */
export const SIDEBAR_WIDTH_MIN = 120
export const SIDEBAR_WIDTH_MAX = 280
export const SIDEBAR_WIDTH_DEFAULT = 200
@@ -44,12 +44,12 @@ function applyUiState(s: any) {
if (s.foldedGroups && typeof s.foldedGroups === 'object') {
state.foldedGroups = { ...state.foldedGroups, ...s.foldedGroups }
}
// 侧栏宽度(UX-2025-16):数值字段,clamp 后写入(与上方 boolean 同级兜底)
// 侧栏宽度数值字段,clamp 后写入(与上方 boolean 同级兜底)
state.sidebarWidth = clampSidebarWidth(s.sidebarWidth)
}
// 启动恢复:从 appSettings(SQLite)读取上次的 UI 布局 + activeProvider(模块级单例 state)
// F-260616-10: activeProvider 恢复对齐 df-ai-active-conv 模式;loadProviders 后还会二次校验
// activeProvider 恢复对齐 df-ai-active-conv 模式;loadProviders 后还会二次校验
// DB is_default(双保险:localStorage 可能与 DB 不同步时以 DB 为准)。
function restoreUiState() {
applyUiState(appSettings.get<any>('df-ai-ui', null))
@@ -79,7 +79,7 @@ export function persistUiState() {
sidebarOpen: state.sidebarOpen,
archivedCollapsed: state.archivedCollapsed,
foldedGroups: state.foldedGroups,
// 侧栏宽度(UX-2025-16):拖拽 mouseup 时由 AiChat 调 persistUiState 落盘
// 侧栏宽度拖拽 mouseup 时由 AiChat 调 persistUiState 落盘
sidebarWidth: state.sidebarWidth,
})
}
@@ -141,8 +141,8 @@ function cyclePanelMode() {
async function loadProviders() {
const list = await aiApi.listProviders()
state.providers = list
// F-260616-10: 重启后 activeProvider=null,从 DB is_default 恢复(后端链路正确,仅前端缺同步)
// CR-34-1: 守卫加有效性检查 — savedProvider 指向已删除 provider 时残留无效 id
// 重启后 activeProvider=null,从 DB is_default 恢复(后端链路正确,仅前端缺同步)
// 守卫加有效性检查 — savedProvider 指向已删除 provider 时残留无效 id
if (!state.activeProvider || !list.some((p: AiProviderConfig) => p.id === state.activeProvider)) {
const def = list.find((p: AiProviderConfig) => p.is_default)
if (def) state.activeProvider = def.id
@@ -162,7 +162,7 @@ async function loadSkills() {
async function setProvider(providerId: string) {
await aiApi.setProvider(providerId)
state.activeProvider = providerId
// F-260616-10: 持久化到 appSettings,重启后恢复(对齐 df-ai-active-conv 模式)
// 持久化到 appSettings,重启后恢复(对齐 df-ai-active-conv 模式)
void appSettings.set('df-ai-active-provider', providerId)
}
@@ -201,7 +201,7 @@ export function useAiPanel() {
loadSkills,
setProvider,
clearChat,
// UX-2025-16:侧栏宽度拖拽
// 侧栏宽度拖拽
setSidebarWidth,
persistUiState,
}
+5 -5
View File
@@ -29,7 +29,7 @@ import { nextMsgId, startApprovalTimer, clearApprovalTimer, clearAllApprovalTime
/// 待发送队列上限(超过抛错提示用户)
const QUEUE_LIMIT = 10
/// 排队超时阈值(ms) — 超过后弹 confirm 让用户选择"强制发送"或继续等(B-260616-02)
/// 排队超时阈值(ms) — 超过后弹 confirm 让用户选择"强制发送"或继续等
const QUEUE_TIMEOUT_MS = 30_000
/**
@@ -134,7 +134,7 @@ async function doSend(text: string, skill?: string, force = false, parts?: Conte
}
/**
* 重新生成最后一条 AI 回复(UX-02)
* 重新生成最后一条 AI 回复。
*
* 与 doSend 的区别:
* - 不 push 新 user 消息(触发它的 user 消息已在历史末尾)
@@ -190,7 +190,7 @@ async function regenerate() {
}
/**
* 编辑最后一条 user 消息并重新生成(UX-09)
* 编辑最后一条 user 消息并重新生成。
*
* 与 regenerate 的区别:
* - regenerate:末条须是 assistant,pop 旧 AI 回复后重跑
@@ -497,7 +497,7 @@ async function sendQueuedNow(index: number) {
/** 停止当前生成:本地先复位 streaming,再发停止信号。
* 不依赖后端 AiCompleted 收尾——审批态看门狗已 clear(useAiEvents),若 AiCompleted 竞态丢失则 streaming 永久 true 卡死,故本地兜底。
*
* UX-260617-19: stop IPC 失败时的兜底。本地已先置 streaming=false,但若 stop 信号未送达后端,
* stop IPC 失败时的兜底。本地已先置 streaming=false,但若 stop 信号未送达后端,
* 后端会继续生成并发回 delta/Completed 事件,与用户已感知的「已停止」语义冲突;更坏情况是
* 后端 hang 既不收尾也不发事件(stop_flag 未置位、无 watchdog 兜底则无恢复路径)。
* 故 stop 失败时不 clearStreamWatchdog,反而重启看门狗:后端若继续生成,活跃 delta 会不断重置它
@@ -551,7 +551,7 @@ export function useAiSend() {
}
}
// ARC-06: 模块级监听 ai-drain-queue 事件(由 useAiEvents AiCompleted case 发射)
// 模块级监听 ai-drain-queue 事件(由 useAiEvents AiCompleted case 发射)
// 打破 useAiEvents ↔ useAiSend 循环依赖:events 不再直接 import 调用 drainQueue
let _drainUnlisten: (() => void) | null = null
export async function initDrainQueueListener(): Promise<void> {
+8 -8
View File
@@ -53,7 +53,7 @@ async function detachPanel(convId?: string) {
return
}
// 快照当前生成态,供分离窗口接管(保持正在进行的对话)
// F-09:用 per-conv key 写入(多会话各自快照互不覆盖)
// 用 per-conv key 写入(多会话各自快照互不覆盖)
// 批4 双轨收口:读 getConvState(enum 真相源)派生,替代旧 generatingConvs.has。
// 桥接语义:has(convId)=true 等价于 conv_state∈{generating,stopping,compressed}(非终止三态)。
if (state.streaming && targetConv) {
@@ -67,7 +67,7 @@ async function detachPanel(convId?: string) {
// 主窗口 state 与分离窗口 state 独立(各自 webview 独立 JS context,
// stores/ai.ts 模块级单例仅在同 webview 内共享),清主窗口 state 不影响分离窗口生成态。
// 分离窗口接管生成后,主窗口不应再持该会话生成态残留(防重开面板时 UI 显生成中幽灵/进度条)。
// F-09:仅从 convStates 移除该 conv(其他会话可能仍在生成),不全局清空。
// 仅从 convStates 移除该 conv(其他会话可能仍在生成),不全局清空。
// watchdog 在主窗口 AiChat 卸载(App.vue v-if detached)→ stopListener → clearStreamWatchdog 时已清,
// 此处仅清内存 state 视觉残留;localStorage 快照保留供 resumeInDetached 恢复。
setStreaming(false, { convId: targetConv || null, reason: 'detachPanel-clear-main' })
@@ -77,7 +77,7 @@ async function detachPanel(convId?: string) {
const win = new WebviewWindow(label, {
url,
title: 'DevFlow AI',
width: 600, // U-260618: 加宽(原 520 偏窄),与吸附态 AI_DOCK_WIDTH 一致
width: 600, // 加宽(原 520 偏窄),与吸附态 AI_DOCK_WIDTH 一致
height: 700,
minWidth: 360,
minHeight: 400,
@@ -109,7 +109,7 @@ async function detachPanel(convId?: string) {
}
/** 关闭分离窗口并回到内嵌面板(主窗口侧调用)。
* F-09:多会话并发下可能存在多个 detached 窗口,全部关闭并清各自 per-conv 快照。 */
* 多会话并发下可能存在多个 detached 窗口,全部关闭并清各自 per-conv 快照。 */
async function reattachPanel() {
stopFollowMain()
const { WebviewWindow } = await import('@tauri-apps/api/webviewWindow')
@@ -128,7 +128,7 @@ async function reattachPanel() {
// 时序加固:reattach 关闭分离窗口后,显式停分离窗口残留 listener(分离窗口 close 异步,
// onBeforeUnmount stopListener 可能延迟),防主面板重新挂载 startListener 时与旧 listener 重叠竞态。
stopListener()
// F-09:清所有 per-conv 快照(分离窗口已全关,快照无消费方)
// 清所有 per-conv 快照(分离窗口已全关,快照无消费方)
cleanupAllDetachedSnapshots()
}
@@ -230,12 +230,12 @@ export async function restoreGeneratingState(opts?: { fromMainPanel?: boolean; c
async function resumeInDetached(convId: string | null) {
// R5: 不自动 switchConversation —— 分离窗口可看历史列表,但不主动覆盖主窗口 activeConversationId
// (避免主窗口期间发消息落错会话)。若需切换会话,由用户在分离窗口显式操作触发。
// F-09: 透传 convId 给 restoreGeneratingState,定位正确的 per-conv 快照 key。
// 透传 convId 给 restoreGeneratingState,定位正确的 per-conv 快照 key。
await restoreGeneratingState({ convId: convId || undefined })
}
/** 关闭分离窗口自身(分离窗口侧调用)。
* F-09:从当前 webview 的 label 反推 convId,清自己的 per-conv 快照。 */
* 从当前 webview 的 label 反推 convId,清自己的 per-conv 快照。 */
async function closeDetachedWindow() {
state.docked = false
stopFollowMain()
@@ -275,7 +275,7 @@ async function dockDetached() {
}
}
/** 吸附态 AI 窗口宽度/间隙常量(U-260618) */
/** 吸附态 AI 窗口宽度/间隙常量*/
const AI_DOCK_WIDTH = 600
const AI_DOCK_GAP = 4
// 防 syncToMain 改主窗口 size 触发 onResized 递归回调
+9 -9
View File
@@ -26,9 +26,9 @@ export interface StreamBlock {
}
export interface UseStreamRendererOptions {
/** UX-2025-01:保存当前文字选区(v-html 替换前调) */
/** 保存当前文字选区(v-html 替换前调) */
saveSelection: () => void
/** UX-2025-01:恢复之前保存的选区(DOM 重建后调) */
/** 恢复之前保存的选区(DOM 重建后调) */
restoreSelection: () => void
/** 跟随意图锁存(跟随中则 rAF 回调内滚到真实底部) */
isFollowingBottom: Ref<boolean> | ComputedRef<boolean>
@@ -59,7 +59,7 @@ export function useStreamRenderer(opts: UseStreamRendererOptions) {
const _blockCache = new Map<string, string>()
const BLOCK_CACHE_LIMIT = 300
// 流式渲染状态:rAF 节流(多 delta 合并一帧,60fps 封顶防主线程阻塞掉帧)
// 分块渲染:已完成块 DOM 稳定不重建 → 选文字保持(UX-2025-01)
// 分块渲染:已完成块 DOM 稳定不重建 → 选文字保持
const streamingBlocks = ref<StreamBlock[]>([])
let rafId: number | null = null
let lastStreamText = ''
@@ -82,7 +82,7 @@ export function useStreamRenderer(opts: UseStreamRendererOptions) {
/// (跨双换行不切),非代码段按双换行切段。
/// 流式期末块可能不完整(未闭合围栏/半截段落),交给 parseBlockNoCache 每次重 parse。
///
/// CR-260615-04:旧实现用正则 /```[^\n]*\n[\s\S]*?(?:```|$)/ 切块,与 marked 围栏规则
/// 旧实现用正则 /```[^\n]*\n[\s\S]*?(?:```|$)/ 切块,与 marked 围栏规则
/// 不一致——不要求行首、固定 3 反引号,导致行中裸三反引号 / 4+ 反引号嵌套围栏切错,
/// 前块缓存固化错误 html。改用 marked.lexer()(经 getMarked() 取 marked 实例)做围栏
/// 感知切块:lexer 把代码围栏识别为单个 type==='code' token(.raw 含整段围栏),
@@ -113,7 +113,7 @@ export function useStreamRenderer(opts: UseStreamRendererOptions) {
/// 末块不缓存(可能不完整,下次 delta 变),每次重 parse 处理未闭合 token
function parseBlockNoCache(block: string): string {
// _marked/_purify 在 loadMarkdown 后才就绪;此处仅在 mdReady 后被流式分支调用。
// UX-260617-25:防御性 null check —— renderStreamingBlocks 顶部已有 mdReady/getMarked/
// 防御性 null check —— renderStreamingBlocks 顶部已有 mdReady/getMarked/
// getPurify 守卫,正常路径 marked/purify 必非 null。但 marked.use(highlightCode) 注入的
// renderer 在动态 import 后仍可能抛异常(hljs 注册竞态/未知语言)→ 此前直接 marked!.parse()
// 会让异常冒泡到 rAF 回调中断流式,表现为生成卡死。这里 null 兜底降级 escapeFallback,
@@ -187,7 +187,7 @@ export function useStreamRenderer(opts: UseStreamRendererOptions) {
// 流式过程中每 delta 重 parse 会闪烁/错乱)。降级为纯文本(转义围栏原文),AiCompleted
// 后整段 renderMd 会做完整代码块渲染(此时围栏已闭合)。已完成块不受影响(围栏已闭合)。
const degrade = isTail && isUnclosedCodeFence(b)
// AC-EFF-S1-1:单块回复病态(整回复无 \n\n/围栏边界,末块=全文)超阈值 → 降级纯文本,
// 单块回复病态(整回复无 \n\n/围栏边界,末块=全文)超阈值 → 降级纯文本,
// 防每帧 marked.lexer+parse O(n) 卡顿(流式期间临时纯文本,AiCompleted 后完整 markdown)。
const longSingleBlock = isTail && n === 1 && b.length > SINGLE_BLOCK_TEXT_LIMIT
const html = (degrade || longSingleBlock)
@@ -226,7 +226,7 @@ export function useStreamRenderer(opts: UseStreamRendererOptions) {
if (rafId !== null) return
rafId = requestAnimationFrame(() => {
rafId = null
// UX-2025-01: 末块 innerHTML 每帧替换,保存选区
// 末块 innerHTML 每帧替换,保存选区
opts.saveSelection()
streamingBlocks.value = renderStreamingBlocks(lastStreamText)
nextTick(() => {
@@ -245,7 +245,7 @@ export function useStreamRenderer(opts: UseStreamRendererOptions) {
if (rafId !== null) { cancelAnimationFrame(rafId); rafId = null }
streamingBlocks.value = []
lastStreamText = ''
// AC-EFF-S1-1:清增量基线(防新轮/新会话误命中旧帧已完成块)。
// 清增量基线(防新轮/新会话误命中旧帧已完成块)。
_prevStreamText = ''
_prevCompleted = []
_prevTail = ''
@@ -259,7 +259,7 @@ export function useStreamRenderer(opts: UseStreamRendererOptions) {
/// 清空实例级块缓存(内容寻址 Map + LRU)。会话切换/开关变化时调用,
/// 防旧会话已缓存块被新会话命中(跨会话内容错乱)。仅清缓存,不动 streamingBlocks。
/// AC-EFF-S1-1:同步清增量基线(已完成块引用旧会话内容,防跨会话误复用)。
/// 同步清增量基线(已完成块引用旧会话内容,防跨会话误复用)。
function resetStreamCache(): void {
_blockCache.clear()
_prevStreamText = ''
+9 -9
View File
@@ -1,8 +1,8 @@
/**
* ToolCard 审批状态机 — 从 ToolCard.vue 抽离的审批 UI 逻辑。
*
* 职责:审批按钮 loading 态、审批挂起时长显示(BUG-260624-02)
* 审批超时兜底 toast(UX-260617-14)、approve/reject/path_auth 三选项处理、
* 职责:审批按钮 loading 态、审批挂起时长显示、
* 审批超时兜底 toast、approve/reject/path_auth 三选项处理、
* status 变化自动复位 + pending 计时。
*
* 不含 store IPC 调用(useAiApproval 负责)和模板渲染——纯状态管理。
@@ -14,14 +14,14 @@ import { aiApi } from '@/api'
import type { AiToolCallInfo } from '@/api/types'
/**
* 审批按钮 loading 超时兜底(B-260616-08)
* 审批按钮 loading 超时兜底。
* 后端回事件使 tc.status 离开 pending_approval → watch 复位。
* 兜底计时到点复位 + toast 提示(后端 IPC 应在 1~3s 内返回;30s 兜底远足超时后允许用户重试)。
* 与 stream 看门狗(130s)解耦:审批是按钮级交互,不是后台流,不需要对齐后端 120s 空闲超时。
*/
const APPROVE_LOADING_TIMEOUT_MS = 30_000
/** AE-2025-05:High 风险工具白名单(后端 tool_registry.rs RiskLevel::High 对齐) */
/** High 风险工具白名单(后端 tool_registry.rs RiskLevel::High 对齐) */
const HIGH_RISK_TOOLS = new Set<string>([
'delete_task', 'delete_project', 'restore_project', 'purge_project', 'delete_file', 'run_command',
'http_request',
@@ -64,11 +64,11 @@ export function useToolApproval(tc: Ref<AiToolCallInfo>, emit: ToolApprovalEmit)
const { t } = useI18n()
const { confirmState, confirmDialog, answerConfirm } = useConfirm()
// ── B-260616-08: 审批 loading 态 ──
// ── 审批 loading 态 ──
const approving = ref(false)
let approvingTimer: ReturnType<typeof setTimeout> | null = null
// ── BUG-260624-02: 审批挂起时长显示 ──
// ── 审批挂起时长显示 ──
const pendingSince = ref<number | null>(null)
const waitSecs = ref(0)
let waitTimer: ReturnType<typeof setInterval> | null = null
@@ -103,7 +103,7 @@ export function useToolApproval(tc: Ref<AiToolCallInfo>, emit: ToolApprovalEmit)
return 'normal'
})
// ── UX-260617-14: 审批超时兜底 toast ──
// ── 审批超时兜底 toast ──
const approveToast = reactive({ visible: false, msg: '' })
let _approveToastTimer: ReturnType<typeof setTimeout> | null = null
@@ -114,7 +114,7 @@ export function useToolApproval(tc: Ref<AiToolCallInfo>, emit: ToolApprovalEmit)
_approveToastTimer = setTimeout(() => { approveToast.visible = false }, 3000)
}
/** AE-2025-05:High 风险工具的二次确认文案(按操作类型) */
/** High 风险工具的二次确认文案(按操作类型) */
function highRiskConfirmMsg(name: string): string {
if (name === 'run_command') return t('aiTool.confirmHighExec')
if (name === 'http_request') return t('aiTool.confirmHighHttp')
@@ -134,7 +134,7 @@ export function useToolApproval(tc: Ref<AiToolCallInfo>, emit: ToolApprovalEmit)
// 先锁防重入(含二次确认 await 期间)
approving.value = true
try {
// AE-2025-05:High 风险 → 先二次确认
// High 风险 → 先二次确认
if (HIGH_RISK_TOOLS.has(tc.value.name)) {
const ok = await confirmDialog(highRiskConfirmMsg(tc.value.name))
if (!ok) {
+20 -20
View File
@@ -26,7 +26,7 @@ import type { AiToolCallInfo } from '@/api/types'
/** 工具结果 JSON 已知字段(全可选;list_* 运行时返回数组,靠 Array.isArray 区分) */
export interface ToolResult {
path?: string
// A1-B5: 只读工具(git status/diff/log)失败路径后端返 {success:false,error} 合法 JSON 对象(非 Err),
// 只读工具(git status/diff/log)失败路径后端返 {success:false,error} 合法 JSON 对象(非 Err),
// 前端 isToolFailure 靠此字段判失败(防绿框假成功)
success?: boolean
error?: string
@@ -89,9 +89,9 @@ export interface ToolResult {
body?: string
body_bytes?: number
elapsed_ms?: number
// read_file search 模式(BUG-260625-02):传 search 参数返命中行 matches[{line,content}] 非全文 content
// read_file search 模式传 search 参数返命中行 matches[{line,content}] 非全文 content
search?: string
// grep 跨文件内容搜索结果字段(F-260621):
// grep 跨文件内容搜索结果字段
// - output_mode: content(默认)/ files_with_matches / count
// - matches: content 模式命中行 [{file,line,content,context?}]
// - files: files_with_matches 模式命中文件名数组
@@ -131,9 +131,9 @@ export function shouldKeepOpen(tc: AiToolCallInfo): boolean {
}
/**
* UX-260616-01: 工具执行失败检测(completed )
* (completed )
*
* AR-6:Low emit AiError, emit AiToolCallCompleted(result=),
* Low emit AiError, emit AiToolCallCompleted(result=),
* status 'completed',(绿+绿)
* :
* - run_command exit_code0:结果 JSON ,parseResult , exit_code
@@ -164,7 +164,7 @@ export function isToolFailure(tc: AiToolCallInfo): boolean {
if (!r) {
const raw = typeof tc.result === 'string' ? tc.result : ''
if (!raw) return false
// UX-260617-18:收窄为行首锚定(原 /执行失败|failed|error[:\s]/i 匹配任意位置的子串,
// 收窄为行首锚定(原 /执行失败|failed|error[:\s]/i 匹配任意位置的子串,
// 误伤 search_files 命中 error.log / run_command stderr 含 "warning" 等正常产出)。
// 仅当某行以「执行失败」(中文 err_msg 前缀)/「Error:」/「Failed:」开头才判失败。
return /^(执行失败|Error:|Failed:)/m.test(raw)
@@ -295,7 +295,7 @@ export function formatSizeDiff(diff: number | undefined | null): string {
return n >= 0 ? `+${n}` : `${n}`
}
/** SW-260618-19: 合并 stdout/stderr(空串跳过),供 cmdOutput 展开 + combineAndTruncateLines 截断共享 DRY */
/** 合并 stdout/stderr(空串跳过),供 cmdOutput 展开 + combineAndTruncateLines 截断共享 DRY */
export function combineOutputs(stdout: string | undefined, stderr: string | undefined): string {
const parts: string[] = []
if (stdout) parts.push(stdout)
@@ -346,7 +346,7 @@ export function formatFileInfoResult(r: ToolResult): string {
}
let base = t('aiTool.fileInfoSize', { path: r.path || '', size: formatBytes(typeof r.size === 'number' ? r.size : 0) })
const extras: string[] = []
// UX-260618-12: file_info modified 时间(epoch_ms → 客户端 toLocaleString)
// file_info modified 时间(epoch_ms → 客户端 toLocaleString)
if (typeof r.modified === 'number' && r.modified > 0) {
extras.push(t('aiTool.fileInfoModified', { time: new Date(r.modified).toLocaleString() }))
}
@@ -365,17 +365,17 @@ export function formatFileInfoResult(r: ToolResult): string {
/**
* ()
* UX-260616-04:统一 switch , JSON
* switch , JSON
* read_file/list_directory/write_file ,
*
* P0-3: switch 17 case TOOL_FORMATTERS(name ),formatToolResult
* switch 17 case TOOL_FORMATTERS(name ),formatToolResult
* , formatToolResult switch ()
*/
// list_* 系列(list_projects/list_tasks/list_ideas/list_trash)共享格式化:
// 渲染结构化列表替代裸 JSON,头行按 name 选 i18n key,条目取 name/title/id + status/stack 标注。
function formatListResult(r: ToolResult, name: string): string {
// UX-260618-12: list_* 后端返 {items,total,has_more} 对象(非数组)
// list_* 后端返 {items,total,has_more} 对象(非数组)
const items = Array.isArray(r) ? r : (Array.isArray(r.items) ? r.items : [])
const total = typeof r.total === 'number' ? r.total : items.length
const head = name === 'list_projects' ? t('aiTool.projectCount', { n: total })
@@ -398,8 +398,8 @@ function formatListResult(r: ToolResult, name: string): string {
/** advance_task 共享格式化(body + summary 一致): review_rounds>0 显退回累加轮数,否则首推原文案 */
function formatAdvanceTask(r: ToolResult): string {
// UX-260618-09: review_rounds>0 显退回累加轮数,=0 回退原文案(首推无噪音)
// UX-260618-14: 后端返 TaskRecord(含 title),优先显 title,无则回退 id(防裸 UUID)
// review_rounds>0 显退回累加轮数,=0 回退原文案(首推无噪音)
// 后端返 TaskRecord(含 title),优先显 title,无则回退 id(防裸 UUID)
return typeof r.review_rounds === 'number' && r.review_rounds > 0
? t('aiTool.advancedTaskWithRounds', { title: r.title || r.id || '', status: r.status || '', n: r.review_rounds })
: t('aiTool.advancedTask', { title: r.title || r.id || '', status: r.status || '' })
@@ -407,7 +407,7 @@ function formatAdvanceTask(r: ToolResult): string {
/** update_task 共享格式化: updated!==false 显字段更新,否则通用 updated */
function formatUpdateTask(r: ToolResult): string {
// UX-260619-04: title 优先(后端补返 title),无则回退 id(防裸 UUID),对齐 advance_task。
// title 优先(后端补返 title),无则回退 id(防裸 UUID),对齐 advance_task。
return r.updated !== false
? t('aiTool.updatedTaskField', { title: r.title || r.id || '', field: r.field || '' })
: t('aiTool.updated')
@@ -444,12 +444,12 @@ function formatSearchFiles(r: ToolResult): string {
}
/**
* grep /(F-260621): output_mode ,:
* grep / output_mode ,:
* - content(): N (total = ,total 退 matches.length)
* - files_with_matches: N (files.length)
* - count: N / M (total + total_files)
*
* G-1: ToolResultBody.vue grepHitLabel formatGrep ,
* ToolResultBody.vue grepHitLabel formatGrep ,
* export ,ToolResultBody.vue ,i18n @/i18n/i18n-helpers t
*/
export function formatGrep(r: ToolResult): string {
@@ -489,7 +489,7 @@ function formatBindDir(r: ToolResult): string {
/** run_workflow 格式化(body 版):无 execution_id 走 formatJson,否则显完整三要素(task_id/target_status/exec) */
function formatRunWorkflow(r: ToolResult): string {
// UX-260618-05: 后端返 {task_id,target_status,execution_id,status,note},显 execution_id 对应执行实例
// 后端返 {task_id,target_status,execution_id,status,note},显 execution_id 对应执行实例
const execId = typeof r.execution_id === 'string' && r.execution_id
? r.execution_id
: (typeof r.id === 'string' ? r.id : '')
@@ -613,10 +613,10 @@ export function formatFetchUrl(r: ToolResult): string {
/**
* completed ( header ,)
* UX-260616-04:覆盖范围与 formatToolResult (header+body );
* formatToolResult (header+body );
* read_file/list_directory/write_file ,header
*
* P0-3: switch TOOL_SUMMARIES(name ),toolResultSummary
* switch TOOL_SUMMARIES(name ),toolResultSummary
* TOOL_FORMATTERS 不共用:summary case (create/update_project/restore/purge summary ;
* rename/bind false formatJson;run_workflow workflowTriggeredShort;list_* has_more )
* (formatAdvanceTask/formatUpdateTask/formatDelete/formatDeleteFile/formatPatchFile/
@@ -669,7 +669,7 @@ function summaryBindDir(r: ToolResult): string {
/** run_workflow summary 版:无 execution_id 返空串,有则显 workflowTriggeredShort(body 版显完整三要素) */
function summaryRunWorkflow(r: ToolResult): string {
// UX-260618-05: header 副标题显 execution_id 概要(原 workflowHint「请到工作流页面运行」误导——工作流已触发)
// header 副标题显 execution_id 概要(原 workflowHint「请到工作流页面运行」误导——工作流已触发)
const execId = typeof r.execution_id === 'string' && r.execution_id
? r.execution_id
: (typeof r.id === 'string' ? r.id : '')
+2 -2
View File
@@ -122,7 +122,7 @@ export function toolDisplayName(tc: AiToolCallInfo): string {
*/
export function useToolCardHeader(getTc: () => AiToolCallInfo) {
/**
* (AR-3): id/project_id ,
* id/project_id ,
* + id key formatArgValue
*/
function displayArgValue(arg: { key: string; val: unknown }): string {
@@ -136,7 +136,7 @@ export function useToolCardHeader(getTc: () => AiToolCallInfo) {
return tGlobal('aiTool.projectIdNotFound', { id })
}
}
// UX-260618-14:advance_task/run_workflow 的 task_id → 任务标题
// advance_task/run_workflow 的 task_id → 任务标题
const taskArgKey = TASK_ID_TOOL_ARG[tc.name]
if (taskArgKey && arg.key === taskArgKey) {
const id = typeof arg.val === 'string' ? arg.val : ''