优化: UI/UX遗留批(审计/AI命令层/前端组件,会话前基线收尾)
- AuditLog +298(筛选/详情/i18n)+ audit 后端 record/mod - AI 命令层:generate_image +81 / fetch_url / fetch_search / skills / tool_registry / tools/file / provider / conversation - 前端组件:AiChat/TopBar/ConversationSidebar/GitChanges/ApprovalPopup/Dashboard/ProjectDetail 等 30+ + composables + i18n - 诊断文档: aichat历史会话实证诊断-2026-08-04 + project_soft_delete 测试
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
//! 面板与持久化 — togglePanel/toggleMaximize + UI 持久化(restoreUiState/persistUiState)
|
||||
//! 面板与持久化 — togglePanel/toggleMaximize/cyclePanelMode + UI 持久化(restoreUiState/persistUiState)
|
||||
//! + token 开关 + providers/skills/clearChat
|
||||
//!
|
||||
//! 模块级私有:
|
||||
@@ -13,7 +13,9 @@ import { aiApi } from '@/api'
|
||||
import { useAppSettingsStore } from '@/stores/appSettings'
|
||||
import { state } from '@/stores/ai'
|
||||
import { setStreaming } from './streamingGuard'
|
||||
import type { AiProviderConfig } from '@/api/types'
|
||||
import { nextMsgId } from './aiShared'
|
||||
import { t } from '@/i18n/i18n-helpers'
|
||||
import type { AiMessage, AiProviderConfig } from '@/api/types'
|
||||
|
||||
const appSettings = useAppSettingsStore()
|
||||
|
||||
@@ -46,16 +48,25 @@ function applyUiState(s: any) {
|
||||
state.sidebarWidth = clampSidebarWidth(s.sidebarWidth)
|
||||
}
|
||||
|
||||
// 启动恢复:从 appSettings(SQLite)读取上次的 UI 布局(模块级单例 state)
|
||||
// 启动恢复:从 appSettings(SQLite)读取上次的 UI 布局 + activeProvider(模块级单例 state)
|
||||
// F-260616-10: activeProvider 恢复对齐 df-ai-active-conv 模式;loadProviders 后还会二次校验
|
||||
// DB is_default(双保险:localStorage 可能与 DB 不同步时以 DB 为准)。
|
||||
function restoreUiState() {
|
||||
applyUiState(appSettings.get<any>('df-ai-ui', null))
|
||||
const savedProvider = appSettings.get<string | null>('df-ai-active-provider', null)
|
||||
if (savedProvider) state.activeProvider = savedProvider
|
||||
}
|
||||
restoreUiState()
|
||||
|
||||
// F-260616-10: 启动恢复 activeProvider(对齐 df-ai-active-conv 模式)。
|
||||
// loadProviders 后还会二次校验 DB is_default(双保险:localStorage 可能与 DB 不同步时以 DB 为准)。
|
||||
const savedProvider = appSettings.get<string | null>('df-ai-active-provider', null)
|
||||
if (savedProvider) state.activeProvider = savedProvider
|
||||
// 循环依赖兜底:stores/ai ↔ useAiPanel 互 import,顶层 restoreUiState 执行时 state 可能尚未
|
||||
// 初始化(TDZ)。冷启动靠 appSettings 缓存为空 → applyUiState(null) 提前 return 绕过访问;
|
||||
// 但 HMR 热重载 stores/ai.ts 时 appSettings 已有缓存,restoreUiState 真正访问 state 触发
|
||||
// ReferenceError: Cannot access 'state' before initialization。对齐 stores/ai.ts __bindMessages
|
||||
// 既存模式:try 失败则微任务延迟重试,待 state 就绪后落库恢复。
|
||||
try {
|
||||
restoreUiState()
|
||||
} catch {
|
||||
Promise.resolve().then(restoreUiState)
|
||||
}
|
||||
|
||||
// loadAll() 异步填充缓存后,再次应用 UI 布局(模块级 state,watch 仅触发一次)
|
||||
watch(() => appSettings.cache['df-ai-ui'], (v) => applyUiState(v as any))
|
||||
@@ -96,6 +107,34 @@ function toggleMaximize() {
|
||||
persistUiState()
|
||||
}
|
||||
|
||||
/**
|
||||
* Ctrl+I 三态循环(关闭 → 分栏打开 → 最大化 → 关闭)。
|
||||
*
|
||||
* 与 togglePanel(二态开关)区分:本函数仅供全局快捷键 Ctrl+I 用;UI 按钮(侧栏 AI 按钮 /
|
||||
* TopBar 关闭钮)与审批浮层激活仍走 togglePanel,保持"开/关"二态语义不被破坏。
|
||||
*
|
||||
* 循环方向逐级增强再回关闭,贴合"要用 AI → 专注看 AI → 用完了"心智:
|
||||
* 关闭(panelOpen=false) → 分栏打开(panelOpen=true, maximized=false)
|
||||
* 分栏打开(maximized=false) → 最大化(maximized=true,全宽占满 main)
|
||||
* 最大化(maximized=true) → 关闭(panelOpen=false, maximized=false)
|
||||
*
|
||||
* detached 模式 no-op:分离窗口下主窗口不渲染面板,循环无意义;分离窗口自身走系统标题栏最大化。
|
||||
*/
|
||||
function cyclePanelMode() {
|
||||
// 分离窗口模式:主窗口无面板,三态循环无意义(no-op;关闭分离窗口时 destroyed 钩子自恢复 panelOpen)
|
||||
if (state.detached) return
|
||||
if (!state.panelOpen) {
|
||||
state.panelOpen = true
|
||||
state.maximized = false
|
||||
} else if (!state.maximized) {
|
||||
state.maximized = true
|
||||
} else {
|
||||
state.panelOpen = false
|
||||
state.maximized = false
|
||||
}
|
||||
persistUiState()
|
||||
}
|
||||
|
||||
// ── Providers / Skills / 清屏 ──
|
||||
|
||||
/** 拉取 provider 列表(用于顶部 bar 展示与切换) + 同步 DB 默认 provider 到 activeProvider */
|
||||
@@ -129,17 +168,35 @@ async function setProvider(providerId: string) {
|
||||
|
||||
/** 清空当前会话消息(后端 + 本地 state) */
|
||||
async function clearChat() {
|
||||
await aiApi.clearChat()
|
||||
state.messages = []
|
||||
state.currentText = ''
|
||||
state.pendingApprovals = []
|
||||
setStreaming(false, { reason: 'clearChat' })
|
||||
try {
|
||||
await aiApi.clearChat()
|
||||
state.messages = []
|
||||
state.currentText = ''
|
||||
state.pendingApprovals = []
|
||||
setStreaming(false, { reason: 'clearChat' })
|
||||
} catch (e) {
|
||||
// A2-B9(G3.2):清空失败需反馈,原裸 await 失败静默(异常进 unhandledrejection,用户无感)。
|
||||
// 后端裁决「先停 loop → DB 单事务 → 内存清」:DB 失败时内存不动(回滚一致),故此处
|
||||
// 只推错误气泡告知用户(对齐 useAiConversations.pushConvOpFail 模式),不吞异常日志。
|
||||
// 注意:不 rethrow——confirmClearChat 无 try,裸抛会进 unhandledrejection 且用户无感;
|
||||
// 错误气泡已足够反馈,且状态保持未清(与后端内存一致)。
|
||||
const msg = e instanceof Error ? e.message : String(e)
|
||||
state.messages.push({
|
||||
id: `conv-op-fail-${nextMsgId()}`,
|
||||
role: 'assistant',
|
||||
content: t('aiChat.clearError', { msg }),
|
||||
isError: true,
|
||||
timestamp: Date.now(),
|
||||
} as AiMessage)
|
||||
console.error('[AI] 清空对话失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
export function useAiPanel() {
|
||||
return {
|
||||
togglePanel,
|
||||
toggleMaximize,
|
||||
cyclePanelMode,
|
||||
loadProviders,
|
||||
loadSkills,
|
||||
setProvider,
|
||||
|
||||
@@ -16,7 +16,7 @@ export function useMessageScroll(containerRef: Ref<HTMLDivElement | undefined>)
|
||||
// 回到底部按钮:内容溢出且不在底部时显示
|
||||
const showBackToBottom = ref(false)
|
||||
|
||||
// B-260618-24: 跟随意图锁存。点回到底部/滚到底/发新消息→true;用户上滑→false。
|
||||
// 跟随意图锁存。点回到底部/滚到底/发新消息→true;用户上滑→false。
|
||||
const isFollowingBottom = ref(true)
|
||||
// 边沿检测:上一帧是否在底部(用于触发 onNearBottom 回调)
|
||||
let wasNearBottom = true
|
||||
|
||||
@@ -30,7 +30,7 @@ export interface UseStreamRendererOptions {
|
||||
saveSelection: () => void
|
||||
/** UX-2025-01:恢复之前保存的选区(DOM 重建后调) */
|
||||
restoreSelection: () => void
|
||||
/** B-260618-24:跟随意图锁存(跟随中则 rAF 回调内滚到真实底部) */
|
||||
/** 跟随意图锁存(跟随中则 rAF 回调内滚到真实底部) */
|
||||
isFollowingBottom: Ref<boolean> | ComputedRef<boolean>
|
||||
/** 回到底部操作 */
|
||||
scrollToBottom: () => void
|
||||
@@ -185,7 +185,7 @@ export function useStreamRenderer(opts: UseStreamRendererOptions) {
|
||||
streamingBlocks.value = renderStreamingBlocks(lastStreamText)
|
||||
nextTick(() => {
|
||||
opts.restoreSelection()
|
||||
// B-260618-24: rAF 回调内 streamingBlocks 已赋值、DOM 高度就绪,跟随中则滚到真实底部。
|
||||
// rAF 回调内 streamingBlocks 已赋值、DOM 高度就绪,跟随中则滚到真实底部。
|
||||
// 治 onContentChange 的 nextTick(scrollToBottom) 抢跑旧 scrollHeight(微任务先于 rAF,
|
||||
// 滚到渲染前高度)致"差一截"。
|
||||
if (opts.isFollowingBottom.value) opts.scrollToBottom()
|
||||
|
||||
@@ -26,6 +26,10 @@ 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),
|
||||
// 前端 isToolFailure 靠此字段判失败(防绿框假成功)
|
||||
success?: boolean
|
||||
error?: string
|
||||
pattern?: string
|
||||
from?: string
|
||||
to?: string
|
||||
@@ -131,9 +135,11 @@ export function shouldKeepOpen(tc: AiToolCallInfo): boolean {
|
||||
*
|
||||
* 后端 AR-6:Low 风险工具失败不 emit AiError,改 emit AiToolCallCompleted(result=错误),
|
||||
* 故失败工具 status 仍为 'completed',与成功工具视觉同态(原绿色边框+绿色结果框)。
|
||||
* 此函数区分两类失败:
|
||||
* 此函数区分三类失败:
|
||||
* - run_command exit_code≠0:结果 JSON 合法,parseResult 命中,看 exit_code 判失败
|
||||
* - 通用工具失败:AR-6 路径后端塞 serde_json::Value::String("工具 X 执行失败: ..."),
|
||||
* - 通用工具 success===false:只读工具(git status/diff/log)失败返 {success:false,error}
|
||||
* 合法 JSON 对象(非 Err),parseResult 命中但显式 success 为 false → 判失败
|
||||
* - 通用工具 raw 文本:AR-6 路径后端塞 serde_json::Value::String("工具 X 执行失败: ..."),
|
||||
* 前端 parseResult 返回 null(非 JSON 对象)→ 走 raw 文本兜底,无结构信号
|
||||
* 此时按行首前缀 "执行失败"/"Error:"/"Failed:" 启发式判定(对齐后端 err_msg 格式,
|
||||
* 行首锚定避免误伤含这些子串的正常产出,见 UX-260617-18)。
|
||||
@@ -150,8 +156,12 @@ export function isToolFailure(tc: AiToolCallInfo): boolean {
|
||||
// run_command 但无 exit_code(异常)→ 保守判失败
|
||||
if (r) return true
|
||||
}
|
||||
// 通用工具:可解析 JSON 且显式 success===false(git status/diff/log 只读工具失败返
|
||||
// {success:false,error} 合法 JSON 对象非 Err)→ 判失败,防绿框假成功
|
||||
const r = parseResult(tc.result)
|
||||
if (r && r.success === false) return true
|
||||
// 通用工具:parseResult 返回 null(非 JSON,AR-6 错误字符串路径)→ 看文本前缀
|
||||
if (!parseResult(tc.result)) {
|
||||
if (!r) {
|
||||
const raw = typeof tc.result === 'string' ? tc.result : ''
|
||||
if (!raw) return false
|
||||
// UX-260617-18:收窄为行首锚定(原 /执行失败|failed|error[:\s]/i 匹配任意位置的子串,
|
||||
|
||||
Reference in New Issue
Block a user