优化: 清理无意义注释(走查编号前缀/过时历史标注/死代码注释)
This commit is contained in:
+21
-21
@@ -162,7 +162,7 @@ const router = useRouter()
|
||||
const appSettings = useAppSettingsStore()
|
||||
const unifiedApproval = appSettings.useSetting<boolean>('df-ai-unified-approval', true)
|
||||
|
||||
// ── UX-2025-20: 空状态示例问题随 MessageList 子组件迁移(examplePrompts 数组在子组件内);
|
||||
// ── 空状态示例问题随 MessageList 子组件迁移(examplePrompts 数组在子组件内);
|
||||
// 父仅保留 sendExamplePrompt 转发(子 emit 'send-example-prompt' → 父委托 ChatInput.sendExamplePrompt)。──
|
||||
|
||||
/** 跳 Settings 配置 provider:嵌入模式 router.push;分离窗口无 Settings 路由,降级 toast 提示 */
|
||||
@@ -184,9 +184,9 @@ function sendExamplePrompt(i18nKey: string) {
|
||||
// i18n 不在本任务白名单,文案硬编码,后续补 aiChat.toastSendFail key
|
||||
const { toast, showToast } = useToast()
|
||||
|
||||
// B-260616-12: 工具慢执行提示事件 unlistener(onMounted 注册,onBeforeUnmount 释放)
|
||||
// 工具慢执行提示事件 unlistener(onMounted 注册,onBeforeUnmount 释放)
|
||||
let _unlistenToolSlow: (() => void) | null = null
|
||||
// AE-2025-04: 会话信任自动放行 toast unlistener(useAiEvents 经事件总线中转,同 _unlistenToolSlow 模式)
|
||||
// 会话信任自动放行 toast unlistener(useAiEvents 经事件总线中转,同 _unlistenToolSlow 模式)
|
||||
let _unlistenAutoApproved: (() => void) | null = null
|
||||
// P1-e-d: 审批失败 toast unlistener(useAiApproval 审批 IPC 未送达经事件总线 emit,同 _unlistenToolSlow 模式)
|
||||
let _unlistenAiToast: (() => void) | null = null
|
||||
@@ -206,10 +206,10 @@ function onChatInputError(payload: { msg: string; type: 'error' | 'warning' | 'i
|
||||
// 第四批抽离: MessageList 子组件引用(expose scrollToBottom/scrollToFirstPending)
|
||||
const messageListRef = ref<InstanceType<typeof MessageList> | null>(null)
|
||||
|
||||
// AE-2025-02:头部审批徽标 — 当前对话积压的待审批工具数(>0 时显示)
|
||||
// 头部审批徽标 — 当前对话积压的待审批工具数(>0 时显示)
|
||||
const pendingApprovalCount = computed(() => store.state.pendingApprovals.length)
|
||||
|
||||
// AE-2025-07: Agentic 循环进度条
|
||||
// Agentic 循环进度条
|
||||
// 仅 agentic 多轮(streaming 且 agentRound>0)显示;单轮问答不收 AiAgentRound 故恒隐藏。
|
||||
const showAgenticProgress = computed(() =>
|
||||
store.state.streaming && store.state.agentRound > 0,
|
||||
@@ -236,7 +236,7 @@ async function confirmDeleteConversation(id: string) {
|
||||
const conv = store.state.conversations.find(c => c.id === id)
|
||||
if (!conv) console.warn('confirmDeleteConversation: conv not found, id=', id)
|
||||
const title = conv?.title || t('aiChat.newConversation')
|
||||
// UX-260617-24:删除当前活跃对话加醒目提示(通用文案不区分,活跃对话误删影响更大)。
|
||||
// 删除当前活跃对话加醒目提示(通用文案不区分,活跃对话误删影响更大)。
|
||||
const isActive = id === store.state.activeConversationId
|
||||
const msgKey = isActive ? 'aiChat.confirmDeleteActiveConversation' : 'aiChat.confirmDeleteConversation'
|
||||
if (!await confirmDialog(t(msgKey, { title }))) return
|
||||
@@ -270,13 +270,13 @@ async function confirmNewConversation() {
|
||||
// projectsStore 仍由父持有(onMounted 预加载 projects/tasks 供子组件 @ 联想读取)
|
||||
const projectsStore = useProjectStore()
|
||||
|
||||
// UX-09:切换对话/新对话时取消编辑态(编辑态绑定末条 user,切走后无效)
|
||||
// 切换对话/新对话时取消编辑态(编辑态绑定末条 user,切走后无效)
|
||||
// 2026-06-18 改:打开/切换会话默认进 specify 模式并选中权重最高 model(用户诉求:默认确定选中、
|
||||
// 不随机/不摇摆)。enabledModels 已按 weight 降序,[0]=最高。空池(未拉取)落 null=auto 兜底。
|
||||
// 后端 run_agentic_loop 对 override 兜底校验池内才用,前端预选无副作用。
|
||||
// 第三批抽离: 默认 model 选中逻辑(modelOverride=enabledModels[0])随 enabledModels 迁移至
|
||||
// TopBar.vue 的 activeConversationId watch;本组件仅保留编辑态取消。
|
||||
// 滚动边沿/跟随意图重置(SW-260618-18)随 MessageList 迁移,由子组件自管。
|
||||
// 滚动边沿/跟随意图重置随 MessageList 迁移,由子组件自管。
|
||||
watch(() => store.state.activeConversationId, () => {
|
||||
if (editingMsgId.value) cancelEdit()
|
||||
})
|
||||
@@ -291,13 +291,13 @@ const convSidebarRef = ref<InstanceType<typeof ConversationSidebar> | null>(null
|
||||
// cycleProvider/providerBarVisible/showProviderSwitched 已迁移至 ai/TopBar.vue
|
||||
// (顶部工具栏 + provider/model 选择逻辑,零行为变更,store 单例共享 modelOverride)。
|
||||
|
||||
/** UX-09:进入编辑态 — 置 editingMsgId(透传 ChatInput prop)+ 委托 ChatInput.startEditFocus 回填/聚焦。 */
|
||||
/** 进入编辑态 — 置 editingMsgId(透传 ChatInput prop)+ 委托 ChatInput.startEditFocus 回填/聚焦。 */
|
||||
function startEdit(msg: AiMessage): void {
|
||||
editingMsgId.value = msg.id
|
||||
chatInputRef.value?.startEditFocus(msg)
|
||||
}
|
||||
|
||||
/** UX-09:取消编辑态(切换对话/清空/ESC)。清 editingMsgId + 委托 ChatInput.clearInput 清输入。 */
|
||||
/** 取消编辑态(切换对话/清空/ESC)。清 editingMsgId + 委托 ChatInput.clearInput 清输入。 */
|
||||
function cancelEdit(): void {
|
||||
editingMsgId.value = null
|
||||
chatInputRef.value?.clearInput()
|
||||
@@ -366,7 +366,7 @@ function onGlobalKeydown(e: KeyboardEvent) {
|
||||
}
|
||||
|
||||
/**
|
||||
* UX-260617-16:未发送输入保护。检测 inputText/pendingImages/pendingSkill 任一非空,
|
||||
* 未发送输入保护。检测 inputText/pendingImages/pendingSkill 任一非空,
|
||||
* 用于 beforeunload 拦截(分离窗口关闭/刷新)与未来 onBeforeUnmount 拦截(面板切换)。
|
||||
*/
|
||||
function hasUnsentInput(): boolean {
|
||||
@@ -452,15 +452,15 @@ function scrollToBottom(smooth = false) {
|
||||
messageListRef.value?.scrollToBottom(smooth)
|
||||
}
|
||||
onBeforeUnmount(() => {
|
||||
// 全局快捷键(UX-2025-07):卸载时移除 window listener,防分离窗口/切组件后泄漏
|
||||
// 全局快捷键卸载时移除 window listener,防分离窗口/切组件后泄漏
|
||||
window.removeEventListener('keydown', onGlobalKeydown)
|
||||
// UX-260617-16:摘除未发送输入保护(分离窗口关闭/页面刷新触发 beforeunload)
|
||||
// 摘除未发送输入保护(分离窗口关闭/页面刷新触发 beforeunload)
|
||||
window.removeEventListener('beforeunload', onBeforeUnloadGuard)
|
||||
// 注:导出菜单外部点击监听/侧栏拖拽 listener/标题淡入计时器现由 ConversationSidebar
|
||||
// 子组件 onBeforeUnmount 自管(第一批抽离),父不再持有这些引用。
|
||||
// 流式 rAF 清理(streaming watch 内 cancelAnimationFrame)随 MessageList 迁移,子组件
|
||||
// onBeforeUnmount 自管(子 watch streaming 已注册,卸载自动停)。
|
||||
// CR-260615-24: 释放后端 listener(ai-chat-event/ai-conversation-changed)+清流式看门狗。
|
||||
// 释放后端 listener(ai-chat-event/ai-conversation-changed)+清流式看门狗。
|
||||
// 之前 stopListener 定义但全仓零调用 → listener+watchdog 永久泄漏;分离窗口主窗口 AiChat
|
||||
// 卸载(走 App.vue v-if detach)时 listener 仍挂后端,watchdog 130s 后写已卸载 state。
|
||||
store.stopListener()
|
||||
@@ -473,9 +473,9 @@ onBeforeUnmount(() => {
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
// 全局快捷键(UX-2025-07):window 级 listener,卸载时移除(对齐 _unlistenToolSlow 生命周期)
|
||||
// 全局快捷键window 级 listener,卸载时移除(对齐 _unlistenToolSlow 生命周期)
|
||||
window.addEventListener('keydown', onGlobalKeydown)
|
||||
// UX-260617-16:未发送输入保护。AiChat 非路由组件(App.vue 内嵌 + AiDetached 路由),
|
||||
// 未发送输入保护。AiChat 非路由组件(App.vue 内嵌 + AiDetached 路由),
|
||||
// beforeRouteLeave 不适用。分离窗口关闭/页面刷新时若有未发送 inputText/pendingImages/
|
||||
// pendingSkill,经 beforeunload 拦截提示用户。注意:e.preventDefault() 才是现代浏览器
|
||||
// 触发原生「确定离开?」的标准做法 returnValue 仅 Chrome 兼容旧写法。
|
||||
@@ -496,13 +496,13 @@ onMounted(async () => {
|
||||
// 此处按需预加载(已加载则跳过), 不阻塞首屏(无 await, 后台执行)。
|
||||
if (!projectsStore.projects.length) void projectsStore.loadProjects()
|
||||
if (!projectsStore.tasks.length) void projectsStore.loadTasks()
|
||||
// B-260616-12: 监听工具慢执行提示(useAiEvents 经 Tauri 事件总线广播;composable 无组件上下文,
|
||||
// 监听工具慢执行提示(useAiEvents 经 Tauri 事件总线广播;composable 无组件上下文,
|
||||
// 由本组件弹本地 toast。主窗口与分离窗口各挂一份 AiChat,各自消费,无重复——emit 单发,
|
||||
// 两窗口的 listener 各收一次但 msg 相同,toast 各自渲染互不干扰)
|
||||
_unlistenToolSlow = await listen<{ name: string }>('ai-tool-slow-toast', (e) => {
|
||||
showToast(t('aiTool.executionSlow', { name: e.payload.name }), 'warning')
|
||||
})
|
||||
// AE-2025-04: 会话信任自动放行 toast(轻量 info,非审批气泡,避免干扰主流程)
|
||||
// 会话信任自动放行 toast(轻量 info,非审批气泡,避免干扰主流程)
|
||||
_unlistenAutoApproved = await listen<{ tool: string; dir: string }>('ai-tool-auto-approved-toast', (e) => {
|
||||
showToast(t('aiChat.autoApprovedToast', { tool: e.payload.tool, dir: e.payload.dir }), 'info')
|
||||
})
|
||||
@@ -516,7 +516,7 @@ onMounted(async () => {
|
||||
// 恢复生成中态(刷新后/分离窗口接管):
|
||||
// - 分离模式(detached=true):resumeInDetached 接管主窗口当前对话
|
||||
// - 主面板模式(detached=false):restoreGeneratingState 恢复主面板自身的生成态
|
||||
// (UX-260617-13: 主面板刷新后无等价恢复链 → streaming=false + 无占位气泡 + 后端
|
||||
// 主面板刷新后无等价恢复链 → streaming=false + 无占位气泡 + 后端
|
||||
// 事件不进流式分支,退化为非流式。现对齐 resumeInDetached 同款恢复链)。
|
||||
// 两者互斥(detached 三元决定),不会双重恢复;restoreGeneratingState 内幂等守卫(state.streaming 已 true 则跳过)。
|
||||
// 在 startListener(L0 ai-client-ready 握手)之后调用,握手仍先于恢复逻辑生效。
|
||||
@@ -691,7 +691,7 @@ async function initContextEventListeners(): Promise<void> {
|
||||
}
|
||||
/* 第五批抽离: F-260616-03 max_iterations 操作卡样式已迁移至 ai/MaxRoundsCard.vue */
|
||||
|
||||
/* ── AE-2025-07: Agentic 循环进度条 ── */
|
||||
/* ── Agentic 循环进度条 ── */
|
||||
.ai-agentic-progress {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -803,7 +803,7 @@ async function initContextEventListeners(): Promise<void> {
|
||||
.ai-toast-error { background: var(--df-danger-bg); color: var(--df-danger); border: 0.5px solid var(--df-danger); }
|
||||
.ai-toast-warning { background: var(--df-warning-bg); color: var(--df-warning); border: 0.5px solid var(--df-warning); }
|
||||
.ai-toast-info { background: var(--df-accent-bg); color: var(--df-accent); border: 0.5px solid var(--df-accent); }
|
||||
/* P0-2: ToastType 新增 'success' 兜底样式 */
|
||||
/* ToastType 新增 'success' 兜底样式 */
|
||||
.ai-toast-success { background: var(--df-success-bg); color: var(--df-success); border: 0.5px solid var(--df-success); }
|
||||
.ai-toast-enter-active, .ai-toast-leave-active { transition: opacity 0.2s, transform 0.2s; }
|
||||
.ai-toast-enter-from, .ai-toast-leave-to { opacity: 0; transform: translate(-50%, -8px); }
|
||||
|
||||
@@ -213,7 +213,7 @@ const isFailed = computed(() => {
|
||||
if (r && typeof r.exit_code === 'number') return r.exit_code !== 0
|
||||
if (r) return true
|
||||
}
|
||||
// A1-B5:git 只读工具失败返 {success:false,error}(结构化非 Err),通用分支识别
|
||||
// git 只读工具失败返 {success:false,error}(结构化非 Err),通用分支识别
|
||||
if (r && r.success === false) return true
|
||||
if (!r) {
|
||||
const raw = typeof tc.result === 'string' ? tc.result : ''
|
||||
@@ -223,7 +223,7 @@ const isFailed = computed(() => {
|
||||
return false
|
||||
})
|
||||
|
||||
/** AE-2025-03: write_file 审批 diff 行解析(复用 parseDiffLines 纯函数,数据源 props.tc.diff) */
|
||||
/** write_file 审批 diff 行解析(复用 parseDiffLines 纯函数,数据源 props.tc.diff) */
|
||||
const diffLines = computed(() => parseDiffLines(props.tc.diff))
|
||||
</script>
|
||||
|
||||
@@ -236,16 +236,16 @@ const diffLines = computed(() => parseDiffLines(props.tc.diff))
|
||||
overflow: hidden;
|
||||
max-width: var(--df-msg-max-width);
|
||||
animation: fadeInUp 0.2s var(--df-ease);
|
||||
/* UX-260617-14:相对定位锚定本卡 toast(absolute 子元素) */
|
||||
/* 相对定位锚定本卡 toast(absolute 子元素) */
|
||||
position: relative;
|
||||
/* UX-260616-03: border/shadow/opacity 过渡让折叠/失败态切换不突兀。
|
||||
/* border/shadow/opacity 过渡让折叠/失败态切换不突兀。
|
||||
不做 body 高度动画(v-show display 切换 + flex gap 会留双倍间距致布局跳变,见 ToolCardList 注释)。 */
|
||||
transition: border-color 0.2s var(--df-ease), box-shadow 0.2s var(--df-ease), opacity 0.2s var(--df-ease);
|
||||
}
|
||||
.ai-tool-card--pending_approval { border-color: rgba(240,199,94,0.3); box-shadow: 0 0 0 1px rgba(240,199,94,0.06); }
|
||||
.ai-tool-card--completed { border-color: rgba(61,219,160,0.2); }
|
||||
.ai-tool-card--rejected { border-color: rgba(240,101,101,0.2); opacity: 0.55; }
|
||||
/* UX-260616-01: 工具失败态——AR-6 下 status 仍为 completed,故单独覆盖边框与 completed 区分 */
|
||||
/* 工具失败态——AR-6 下 status 仍为 completed,故单独覆盖边框与 completed 区分 */
|
||||
.ai-tool-card--failed { border-color: rgba(240,101,101,0.35); box-shadow: 0 0 0 1px rgba(240,101,101,0.06); }
|
||||
.ai-tool-status-dot--failed { background: var(--df-danger); }
|
||||
.ai-tool-sub--failed { color: var(--df-danger); }
|
||||
@@ -326,7 +326,7 @@ const diffLines = computed(() => parseDiffLines(props.tc.diff))
|
||||
border-radius: var(--df-radius-sm, 4px);
|
||||
}
|
||||
|
||||
/* BUG-260624-02: 审批挂起时长提示(>2分钟橙色,>5分钟红色) */
|
||||
/* 审批挂起时长提示(>2分钟橙色,>5分钟红色) */
|
||||
.ai-tool-wait-time {
|
||||
padding: 3px 10px; margin-bottom: 6px; font-size: 11px;
|
||||
font-family: var(--df-font-mono);
|
||||
@@ -342,7 +342,7 @@ const diffLines = computed(() => parseDiffLines(props.tc.diff))
|
||||
animation: pulseGlow 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* AE-2025-03: write_file 审批 diff 预览区(红删绿增行级) */
|
||||
/* write_file 审批 diff 预览区(红删绿增行级) */
|
||||
.ai-tool-approval-diff { margin: 0 10px 8px; border: 0.5px solid var(--df-border); border-radius: var(--df-radius-sm, 4px); overflow: hidden; }
|
||||
.ai-tool-diff-pre {
|
||||
margin: 0; padding: 6px 0; max-height: 240px; overflow: auto;
|
||||
@@ -375,7 +375,7 @@ const diffLines = computed(() => parseDiffLines(props.tc.diff))
|
||||
.ai-tool-btn--always:hover { background: var(--df-warning); color: #000; border-color: var(--df-warning); }
|
||||
.ai-tool-btn--reject { background: var(--df-danger-bg); color: var(--df-danger); }
|
||||
.ai-tool-btn--reject:hover { background: var(--df-danger); color: #fff; border-color: var(--df-danger); }
|
||||
/* 审批 loading(B-260616-08):禁用两按钮防重复点击,spinner 转在原位 */
|
||||
/* 审批 loading禁用两按钮防重复点击,spinner 转在原位 */
|
||||
.ai-tool-btn:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||
.ai-tool-btn--approve:disabled:hover { background: var(--df-success-bg); color: var(--df-success); border-color: var(--df-border); }
|
||||
.ai-tool-btn--reject:disabled:hover { background: var(--df-danger-bg); color: var(--df-danger); border-color: var(--df-border); }
|
||||
@@ -385,7 +385,7 @@ const diffLines = computed(() => parseDiffLines(props.tc.diff))
|
||||
}
|
||||
@keyframes aiToolBtnSpin { to { transform: rotate(360deg); } }
|
||||
|
||||
/* -- UX-260617-14: 审批 loading 超时兜底 toast(复用 AiChat 同款视觉 token) -- */
|
||||
/* -- 审批 loading 超时兜底 toast(复用 AiChat 同款视觉 token) -- */
|
||||
.ai-tool-toast {
|
||||
position: absolute; top: 6px; left: 50%; transform: translateX(-50%);
|
||||
padding: 6px 12px; border-radius: var(--df-radius-sm); font-size: 11px; z-index: 100;
|
||||
|
||||
@@ -81,12 +81,12 @@ const emit = defineEmits<{
|
||||
const rootEl = ref<HTMLElement>()
|
||||
// 卡片级折叠(整卡 body 显隐,区别于 read_file 内容级)
|
||||
const expandedCards = ref(new Set<string>())
|
||||
// UX-260616-03:用户主动展开的卡片(记忆态,不被自动收起清除)。
|
||||
// 用户主动展开的卡片(记忆态,不被自动收起清除)。
|
||||
// 与 expandedCards 区分:expandedCards 是"展开"语义的当前态(含默认首卡),
|
||||
// userExpandedCards 是"用户明确点开"的持久标记,collapseInactive 跳过这些卡。
|
||||
// 语义:用户读过的卡不强制折叠,符合"跟随阅读位置"的增强目标。
|
||||
const userExpandedCards = ref(new Set<string>())
|
||||
// UX-260617-23:用户主动折叠记忆(修复首卡 ci===0 兜底 return true 导致无法折叠)。
|
||||
// 用户主动折叠记忆(修复首卡 ci===0 兜底 return true 导致无法折叠)。
|
||||
// isCardExpanded 最优先判断,覆盖默认首卡展开 + userExpandedCards 记忆。
|
||||
const userCollapsedCards = ref(new Set<string>())
|
||||
// 内容级展开(read_file "展开全部")
|
||||
@@ -125,7 +125,7 @@ const collapsibleGroupCount = computed(() =>
|
||||
groupedToolCalls.value.filter(g => g.calls.length >= 2).length
|
||||
)
|
||||
|
||||
/** 首个可折叠分组索引(U-260618:全部展开/收起按钮下沉到此分组标题行尾巴) */
|
||||
/** 首个可折叠分组索引全部展开/收起按钮下沉到此分组标题行尾巴) */
|
||||
const firstCollapsibleGi = computed(() => {
|
||||
for (let i = 0; i < groupedToolCalls.value.length; i++) {
|
||||
if (groupedToolCalls.value[i].calls.length >= 2) return i
|
||||
@@ -196,7 +196,7 @@ function toggleAllGroups(): void {
|
||||
|
||||
/**
|
||||
* 分组显示名:工具名 → i18n 友好标签(英文化,原硬编码中文)。
|
||||
* UX-260617-17:用 aiTool.toolGroup.* 子键查表;未命中的工具回退 _ → 空格风格。
|
||||
* 用 aiTool.toolGroup.* 子键查表;未命中的工具回退 _ → 空格风格。
|
||||
*/
|
||||
// tool.name → aiTool.toolGroup.* 子键(与后端 tool_registry 工具名一一对应)
|
||||
const TOOL_GROUP_I18N_KEY: Record<string, string> = {
|
||||
@@ -248,14 +248,14 @@ function groupFileSummary(group: ToolCallGroup): string {
|
||||
* - 分组未折叠 → 用户手动 expandedCards 或首卡默认展开
|
||||
* - 分组已折叠 → false(整组隐藏)
|
||||
* - 多卡片分组 + 未手动操作过 → 仅首卡(ci===0)展开
|
||||
* - UX-260616-03:userExpandedCards(用户主动展开记忆)优先,即使父级 collapseInactive
|
||||
* - userExpandedCards(用户主动展开记忆)优先,即使父级 collapseInactive
|
||||
* 清了 expandedCards,记忆态仍保持展开(用户读过的卡不强制折叠)。
|
||||
*/
|
||||
function isCardExpanded(tc: AiToolCallInfo, gi: number, ci: number): boolean {
|
||||
if (isGroupCollapsed(gi)) return false
|
||||
// UX-260617-23:用户主动折叠最优先(覆盖默认首卡 ci===0 兜底展开 + 用户展开记忆)
|
||||
// 用户主动折叠最优先(覆盖默认首卡 ci===0 兜底展开 + 用户展开记忆)
|
||||
if (userCollapsedCards.value.has(tc.id)) return false
|
||||
// UX-260616-03:用户主动展开的卡片记忆态优先(不被自动收起影响)
|
||||
// 用户主动展开的卡片记忆态优先(不被自动收起影响)
|
||||
if (userExpandedCards.value.has(tc.id)) return true
|
||||
// 用户手动切换过该卡(当前态)
|
||||
if (expandedCards.value.has(tc.id)) return true
|
||||
@@ -290,19 +290,19 @@ function toggleExpand(id: string) {
|
||||
|
||||
/**
|
||||
* 收起不在 activeIds 中的已完成/已拒绝卡片(供父组件 auto-collapse watch 调用)。
|
||||
* UX-260616-03:userExpandedCards(用户主动展开记忆)不被清除——用户读过的卡
|
||||
* userExpandedCards(用户主动展开记忆)不被清除——用户读过的卡
|
||||
* 即使不再 active(running/pending/write_file)也保持展开,避免"我刚才在看,突然被收起"。
|
||||
* 仅清 expandedCards(默认态/首卡)与 expandedTools(内容级展开,跟随卡片折叠)。
|
||||
*/
|
||||
function collapseInactive(activeIds: Set<string>): void {
|
||||
// CR-260615-31:对称过滤 expandedTools(原只清 expandedCards,expandedTools 残留致折叠后内容展开态)
|
||||
// 对称过滤 expandedTools(原只清 expandedCards,expandedTools 残留致折叠后内容展开态)
|
||||
if (expandedCards.value.size > 0) {
|
||||
expandedCards.value = new Set([...expandedCards.value].filter(id => activeIds.has(id)))
|
||||
}
|
||||
if (expandedTools.value.size > 0) {
|
||||
expandedTools.value = new Set([...expandedTools.value].filter(id => activeIds.has(id)))
|
||||
}
|
||||
// UX-260616-03:不清 userExpandedCards(记忆态),isCardExpanded 优先读它保持展开。
|
||||
// 不清 userExpandedCards(记忆态),isCardExpanded 优先读它保持展开。
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -377,7 +377,7 @@ function groupIcon(name: string): string {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* -- 顶部操作栏:批量审批 + 全局收起合并同行(UX-260616-02) -- */
|
||||
/* -- 顶部操作栏:批量审批 + 全局收起合并同行-- */
|
||||
.ai-tool-topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -419,7 +419,7 @@ function groupIcon(name: string): string {
|
||||
}
|
||||
.ai-btn--danger:hover { filter: brightness(1.1); }
|
||||
|
||||
/* -- 全部展开/收起(U-260618:从 topbar 下沉到分组标题行,移除独立按钮 padding/背景,
|
||||
/* -- 全部展开/收起从 topbar 下沉到分组标题行,移除独立按钮 padding/背景,
|
||||
高度=文字行高,不撑高分组标题行;融入标题视觉,仅 hover 变色) -- */
|
||||
.ai-tool-global-toggle {
|
||||
display: flex;
|
||||
@@ -447,7 +447,7 @@ function groupIcon(name: string): string {
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
/* U-260618: max-width 对齐气泡/卡片(90%),不拉满 ai-msg-content 右侧 */
|
||||
/* max-width 对齐气泡/卡片(90%),不拉满 ai-msg-content 右侧 */
|
||||
max-width: var(--df-msg-max-width);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
@@ -483,7 +483,7 @@ function groupIcon(name: string): string {
|
||||
color: var(--df-accent);
|
||||
}
|
||||
.ai-tool-group-chevron {
|
||||
/* U-260618: 移除 margin-left:auto,让全部展开/收起按钮(global-toggle)占据行尾右对齐 */
|
||||
/* 移除 margin-left:auto,让全部展开/收起按钮(global-toggle)占据行尾右对齐 */
|
||||
font-size: 10px;
|
||||
color: var(--df-text-dim);
|
||||
transition: transform 0.15s var(--df-ease);
|
||||
|
||||
@@ -277,7 +277,7 @@ const isFailed = computed(() => {
|
||||
if (r && typeof r.exit_code === 'number') return r.exit_code !== 0
|
||||
if (r) return true
|
||||
}
|
||||
// A1-B5: 可解析 JSON 且显式 success===false(git status/diff/log 只读工具失败返
|
||||
// 可解析 JSON 且显式 success===false(git status/diff/log 只读工具失败返
|
||||
// {success:false,error} 合法 JSON 对象非 Err)→ 判失败,防绿框假成功
|
||||
if (r && r.success === false) return true
|
||||
if (!r) {
|
||||
@@ -296,13 +296,13 @@ const cmdOutput = computed(() => {
|
||||
})
|
||||
|
||||
/**
|
||||
* UX-260618-06: patch_file 结果 diff 行拆解(复用 parseDiffLines +add/-del/ ctx 着色)。
|
||||
* patch_file 结果 diff 行拆解(复用 parseDiffLines +add/-del/ ctx 着色)。
|
||||
* 数据源:parsed.diff(patch_file 工具执行结果字段)。长 diff 截断到前 120 行防撑爆卡片。
|
||||
*/
|
||||
const resultDiffLines = computed(() => parseDiffLines(parsed.value?.diff, 120))
|
||||
|
||||
/**
|
||||
* grep 命中计数标签(F-260621):G-1 复用 useToolCard.formatGrep 单一函数,
|
||||
* grep 命中计数标签G-1 复用 useToolCard.formatGrep 单一函数,
|
||||
* 消除原与 ToolResultBody 重复的三分支(files_with_matches/count/content 文案)。
|
||||
* parsed 为 null 返空串,等价原逻辑;非 null 委托 formatGrep。
|
||||
*/
|
||||
@@ -333,7 +333,7 @@ function onInstallObscura() {
|
||||
emit('install-obscura')
|
||||
}
|
||||
|
||||
// SW-260618-06: status 切到 completed 时按成功/失败初始化折叠态(失败默认展开便于看 stderr/错误 body)。
|
||||
// status 切到 completed 时按成功/失败初始化折叠态(失败默认展开便于看 stderr/错误 body)。
|
||||
// immediate 时若初始非 completed,两 ref 保持默认 false,无副作用。
|
||||
watch(() => props.tc.status, (s) => {
|
||||
if (s === 'completed') {
|
||||
@@ -402,7 +402,7 @@ watch(() => props.tc.status, (s) => {
|
||||
color: var(--df-warning);
|
||||
}
|
||||
|
||||
/* grep 结果(F-260621):文件栏复用 ai-tool-file-bar,命中行用等宽渲染 */
|
||||
/* grep 结果文件栏复用 ai-tool-file-bar,命中行用等宽渲染 */
|
||||
.ai-tool-grep-result {
|
||||
border-top: 0.5px solid var(--df-border);
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ import type { AiMessage, ContentPart, SkillInfo, ProjectRecord, TaskRecord, Idea
|
||||
import type { MentionItem } from './MentionItem'
|
||||
|
||||
const props = defineProps<{
|
||||
/** UX-09: 编辑末条 user 消息态;父持有,handleSend 据此走 editMessage 而非 sendMessage */
|
||||
/** 编辑末条 user 消息态;父持有,handleSend 据此走 editMessage 而非 sendMessage */
|
||||
editingMsgId: string | null
|
||||
}>()
|
||||
|
||||
@@ -317,7 +317,7 @@ watch(inputText, (v) => {
|
||||
// / 技能联想(仅当输入以 / 开头且光标在首字符区)
|
||||
skillOpen.value = v.startsWith('/')
|
||||
if (skillOpen.value) skillIndex.value = 0
|
||||
// UX-10: @ 实体引用(技能联想与 @ 联想互斥,二者同时仅一开;视觉打架防护)
|
||||
// @ 实体引用(技能联想与 @ 联想互斥,二者同时仅一开;视觉打架防护)
|
||||
if (skillOpen.value) {
|
||||
mentionOpen.value = false
|
||||
mentionStart.value = -1
|
||||
@@ -327,7 +327,7 @@ watch(inputText, (v) => {
|
||||
})
|
||||
|
||||
/**
|
||||
* UX-10: 检测光标前最近的 @ 触发符。
|
||||
* 检测光标前最近的 @ 触发符。
|
||||
* 规则:从光标位置向前找最近的 @,期间无空白/换行则激活(支持 "abc @dev" 但不支持 "a @b c @d" 中第二个被空格阻断)。
|
||||
* 找到 → mentionStart=该 @ 下标, mentionOpen=true; 否则关闭。
|
||||
*/
|
||||
@@ -675,7 +675,7 @@ function selectMention(item: MentionItem) {
|
||||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
// UX-09:编辑态 Escape 取消编辑(最高优先级)
|
||||
// 编辑态 Escape 取消编辑(最高优先级)
|
||||
if (props.editingMsgId && e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
emit('cancel-edit')
|
||||
@@ -716,7 +716,7 @@ function handleKeydown(e: KeyboardEvent) {
|
||||
return
|
||||
}
|
||||
}
|
||||
// UX-10: @ 实体引用浮层导航(仅有匹配项时拦截方向/回车;与技能联想互斥)
|
||||
// @ 实体引用浮层导航(仅有匹配项时拦截方向/回车;与技能联想互斥)
|
||||
if (mentionOpen.value && mentionItems.value.length) {
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault()
|
||||
@@ -907,7 +907,7 @@ function focus(): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* UX-260617-16:未发送输入保护。检测 inputText/pendingImages/pendingSkill 任一非空,
|
||||
* 未发送输入保护。检测 inputText/pendingImages/pendingSkill 任一非空,
|
||||
* 用于 beforeunload 拦截(分离窗口关闭/刷新)与未来 onBeforeUnmount 拦截(面板切换)。
|
||||
*/
|
||||
function hasUnsentInput(): boolean {
|
||||
|
||||
@@ -322,7 +322,7 @@ function onActionsOutsideClick(e: MouseEvent) {
|
||||
}
|
||||
|
||||
/**
|
||||
* UX-260617-23:搜索结果点击切对话后清空 searchQuery,退出搜索模式回原时间分组视图。
|
||||
* 搜索结果点击切对话后清空 searchQuery,退出搜索模式回原时间分组视图。
|
||||
* M3:命中项可能在折叠的时间分组或折叠的归档分组内,选中态不可见。切回分组视图后展开所在分组
|
||||
* 并 scrollIntoView,确保选中项立即可见(原仅切 searchQuery,选中项落在折叠组里看不到高亮)。
|
||||
*/
|
||||
@@ -425,7 +425,7 @@ const searchResults = computed<AiConversationSummary[] | null>(
|
||||
() => store.filteredConversations.value,
|
||||
)
|
||||
|
||||
// ── UX-2025-16: 侧栏宽度拖拽 ──
|
||||
// ── 侧栏宽度拖拽 ──
|
||||
const resizeState = { startX: 0, startWidth: 0 }
|
||||
function onResizeMove(e: MouseEvent) {
|
||||
store.setSidebarWidth(resizeState.startWidth + (e.clientX - resizeState.startX))
|
||||
@@ -451,11 +451,11 @@ onMounted(() => {
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', onActionsOutsideClick)
|
||||
// UX-2025-16:拖拽中途卸载兜底摘临时 document listener(原 AiChat 同段迁移)
|
||||
// 拖拽中途卸载兜底摘临时 document listener(原 AiChat 同段迁移)
|
||||
document.removeEventListener('mousemove', onResizeMove)
|
||||
document.removeEventListener('mouseup', onResizeEnd)
|
||||
document.body.classList.remove('ai-sidebar-resizing')
|
||||
// UX-2025-20:释放标题淡入计时器
|
||||
// 释放标题淡入计时器
|
||||
if (_titleFlashTimer) { clearTimeout(_titleFlashTimer); _titleFlashTimer = null }
|
||||
})
|
||||
|
||||
@@ -482,7 +482,7 @@ defineExpose({
|
||||
height: 100%;
|
||||
background: var(--df-bg);
|
||||
}
|
||||
/* UX-2025-16:右边缘 2px 拖拽条。hover/active 显高亮;拖拽中 body.ai-sidebar-resizing 全局锁 cursor 防抖 */
|
||||
/* 右边缘 2px 拖拽条。hover/active 显高亮;拖拽中 body.ai-sidebar-resizing 全局锁 cursor 防抖 */
|
||||
.ai-sidebar-resize-handle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -662,7 +662,7 @@ body.ai-sidebar-resizing * {
|
||||
font-size: 9px;
|
||||
color: var(--df-text-dim);
|
||||
}
|
||||
/* F-09: 多会话并发生成指示器(会话项标题行内脉冲点,多会话可同时显示) */
|
||||
/* 多会话并发生成指示器(会话项标题行内脉冲点,多会话可同时显示) */
|
||||
.ai-conv-gen-dot {
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
@@ -706,7 +706,7 @@ body.ai-sidebar-resizing * {
|
||||
background: var(--df-sidebar-hover);
|
||||
color: var(--df-text);
|
||||
}
|
||||
/* CR-22-1: 已置顶对话视觉强调(左边线,不依赖菜单展开,一眼识别)。
|
||||
/* 已置顶对话视觉强调(左边线,不依赖菜单展开,一眼识别)。
|
||||
pinned 类挂在 .ai-conv-item 上(操作收进更多菜单后,按钮不再常驻,改 item 级类)。 */
|
||||
.ai-conv-item--pinned {
|
||||
box-shadow: inset 2px 0 0 var(--df-accent, var(--df-text));
|
||||
|
||||
@@ -70,7 +70,7 @@ const { confirmState, confirmDialog, answerConfirm } = useConfirm()
|
||||
// 首轮慢响应(看门狗已超时清 streaming)之后——此时 streaming=false 但后端 generating=true、
|
||||
// generatingConvs 含 conv(useAiEvents handleEvent 对非完成/错误事件 add)。若守卫仍 && streaming,
|
||||
// 挂起弹窗永不显示 → loop 无声卡死(用户发消息无响应)。去 streaming,isGenerating(conv) 单条件覆盖。
|
||||
// F-09: 多会话并发 — isGenerating(active) 替代单值比对
|
||||
// 多会话并发 — isGenerating(active) 替代单值比对
|
||||
const isViewingGenerating = computed(() =>
|
||||
store.isGenerating(store.state.activeConversationId),
|
||||
)
|
||||
|
||||
@@ -20,7 +20,7 @@ const emit = defineEmits<{
|
||||
(e: 'go-to-settings'): void
|
||||
}>()
|
||||
|
||||
// ── UX-2025-20: 空状态示例问题 ──
|
||||
// ── 空状态示例问题 ──
|
||||
// 示例问题卡片(点击自动填入并发送);文案走 i18n key 数组,模板按 key 翻译。
|
||||
const examplePrompts = [
|
||||
'aiChat.examplePrompt1',
|
||||
@@ -85,7 +85,7 @@ const examplePrompts = [
|
||||
text-align: center;
|
||||
line-height: 1.5;
|
||||
}
|
||||
/* UX-2025-20: 无 provider 引导 — 跳 Settings 按钮(克制:边框线 + accent 文字,不喧宾) */
|
||||
/* 无 provider 引导 — 跳 Settings 按钮(克制:边框线 + accent 文字,不喧宾) */
|
||||
.ai-empty-action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -43,7 +43,7 @@ const { t } = useI18n()
|
||||
// 耗时长,130s 流式看门狗极易在迭代间隙超时清 streaming → 卡片因 streaming=false 不弹 → 用户看不到
|
||||
// "继续/停止" → generating 永真卡死。达 max 比 DirAuthAuth 更易触发(必经多轮)。去 streaming,
|
||||
// isGenerating(conv) 单条件覆盖挂起态(达 max 时 generatingConvs 含 conv,useAiEvents handleEvent 对非完成/错误事件 add)。
|
||||
// F-09: 多会话并发 — isGenerating(active) 替代单值比对
|
||||
// 多会话并发 — isGenerating(active) 替代单值比对
|
||||
const isViewingGenerating = computed(() =>
|
||||
store.isGenerating(store.state.activeConversationId),
|
||||
)
|
||||
|
||||
@@ -28,7 +28,7 @@ import { formatRelative, formatDate } from '../../utils/time'
|
||||
import type { AiMessage } from '../../api/types'
|
||||
|
||||
defineProps<{
|
||||
/** UX-09:当前正在编辑的消息 id(父持有,经 ChatInput 透传);末条 user 编辑按钮据此隐藏 */
|
||||
/** 当前正在编辑的消息 id(父持有,经 ChatInput 透传);末条 user 编辑按钮据此隐藏 */
|
||||
editingMsgId?: string | null
|
||||
}>()
|
||||
|
||||
@@ -42,8 +42,8 @@ const emit = defineEmits<{
|
||||
const store = useAiStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
// ═══ Markdown 渲染(ARC-260615-08:自研块级 memo,借鉴方案D流式核心) ═══
|
||||
// B-23:渲染基础设施(_marked/_purify/mdReady/_mdCache/loadMarkdown/escapeFallback/
|
||||
// ═══ Markdown 渲染自研块级 memo,借鉴方案D流式核心) ═══
|
||||
// 渲染基础设施(_marked/_purify/mdReady/_mdCache/loadMarkdown/escapeFallback/
|
||||
// renderMd)抽至 composables/useMarkdown(模块级单例),与 TaskDetail/AiChat 共享。
|
||||
// 流式核心(splitBlocks/parseBlock/renderStreamingBlocks/scheduleStreamParse)抽至
|
||||
// composables/ai/useStreamRenderer(本组件经 opts 注入 saveSelection/restoreSelection/
|
||||
@@ -54,14 +54,14 @@ const { t } = useI18n()
|
||||
// scheduleStreamParse/renderStreamingBlocks/clearStreamingState 均由 composable 返回。
|
||||
const { loadMarkdown } = useMarkdown()
|
||||
|
||||
// ── UX-2025-20: 空状态示例问题(examplePrompts 已随 EmptyState.vue 提取) ──
|
||||
// ── 空状态示例问题(examplePrompts 已随 EmptyState.vue 提取) ──
|
||||
|
||||
const messagesContainer = ref<HTMLDivElement>()
|
||||
// 工具卡片列表(子组件 ToolCardList 自治折叠态,父级仅经 expose 调 collapseInactive 收起旧卡)
|
||||
const toolCardListRef = ref<InstanceType<typeof ToolCardList>>()
|
||||
|
||||
// 当前视图是否正在生成(切走后台生成时光标不显示)
|
||||
// F-09: 多会话并发 — isGenerating(active) 替代单值比对
|
||||
// 多会话并发 — isGenerating(active) 替代单值比对
|
||||
const isViewingGenerating = computed(() =>
|
||||
store.state.streaming && store.isGenerating(store.state.activeConversationId)
|
||||
)
|
||||
@@ -72,10 +72,10 @@ function isLastAi(msg: AiMessage): boolean {
|
||||
}
|
||||
|
||||
/**
|
||||
* B-260618-19: 是否渲染某条消息(供 .ai-msg-slot v-if 守卫)。
|
||||
* 是否渲染某条消息(供 .ai-msg-slot v-if 守卫)。
|
||||
*
|
||||
* 背景:agentic 多轮下,LLM 某轮只发 tool_calls 不输出文本时(GLM anthropic_compat 常见),
|
||||
* 该轮 assistant 消息 content='' + toolCalls=[...];变中间条后,旧 v-else-if(UX-260617-22)
|
||||
* 该轮 assistant 消息 content='' + toolCalls=[...];变中间条后,旧 v-else-if
|
||||
* 漏判 toolCalls 把它整条过滤 → 工具卡片消失,且外层 .ai-msg-slot 恒渲染留空 div,
|
||||
* flex gap 堆积成大片空白,末条光标气泡随每轮 AiAgentRound push 往下移。
|
||||
*
|
||||
@@ -207,14 +207,14 @@ async function handleMessagesScroll(): Promise<void> {
|
||||
}
|
||||
|
||||
watch(() => store.state.messages.length, onContentChange)
|
||||
// SW-260618-07: currentText → onContentChange 合并到下方 scheduleStreamParse watch(同源 currentText,单 callback 顺序执行)
|
||||
// currentText → onContentChange 合并到下方 scheduleStreamParse watch(同源 currentText,单 callback 顺序执行)
|
||||
|
||||
// 流式 Markdown 渲染驱动(ARC-260615-08 块级 memo):
|
||||
// - currentText 变化(每 delta):rAF 节流 scheduleStreamParse,块级 memo 只重 parse 末块
|
||||
// - streaming 翻转:结束清 rAF + 清 streamingBlocks(回 renderMd 走 final marked + 缓存)
|
||||
// - mdReady 翻转(CR-260615-06):首屏 marked 慢 + 暂无新 delta 时末段停留 escapeFallback
|
||||
// - mdReady 翻转首屏 marked 慢 + 暂无新 delta 时末段停留 escapeFallback
|
||||
// 纯文本;marked 就绪后主动重算一次,使纯文本转格式化
|
||||
// SW-260618-07: 合并 currentText 双 watch(原 :2040 onContentChange 滚动 + 本 watch scheduleStreamParse 分块)。
|
||||
// 合并 currentText 双 watch(原 :2040 onContentChange 滚动 + 本 watch scheduleStreamParse 分块)。
|
||||
// 顺序:onContentChange(滚动跟随)先 → scheduleStreamParse(rAF 分块渲染)后,保持原注册顺序。
|
||||
watch(() => store.state.currentText, (text) => {
|
||||
// 流式滚动跟随由 scheduleStreamParse 的 rAF 回调承接(DOM 高度就绪后滚),
|
||||
@@ -244,7 +244,7 @@ watch(() => store.state.streaming, (s) => {
|
||||
// streamingBlocks(依赖 currentText watch 的 !text 分支按新轮时机清,BUG-260624-01)。
|
||||
cancelPendingRaf()
|
||||
if (s) {
|
||||
// UX-260616-03: 新一轮生成开始 → 收起上一轮已完成的旧工具卡,聚焦当前生成内容。
|
||||
// 新一轮生成开始 → 收起上一轮已完成的旧工具卡,聚焦当前生成内容。
|
||||
// 仅当前视图正在生成时触发(isGenerating(activeConversationId)),
|
||||
// 切走后台生成/恢复会话(streaming 初值已 true 的 onMounted 路径)不误触。
|
||||
// buildActiveToolIds/collapseAllToolLists 是 function 声明,提升可用。
|
||||
@@ -252,7 +252,7 @@ watch(() => store.state.streaming, (s) => {
|
||||
collapseAllToolLists(buildActiveToolIds(store.state.messages))
|
||||
}
|
||||
} else {
|
||||
// UX-2025-01: 流式→完成切换,模板从 v-for 分块切到 v-html 整段,DOM 重建
|
||||
// 流式→完成切换,模板从 v-for 分块切到 v-html 整段,DOM 重建
|
||||
saveSelection()
|
||||
clearStreamingState()
|
||||
nextTick(() => restoreSelection())
|
||||
@@ -270,7 +270,7 @@ watch(() => store.state.streaming, (s) => {
|
||||
// 只在 toolCall 状态翻转(新卡片/完成/拒绝)时触发,与折叠语义精确对齐。
|
||||
// snap/MESSAGE_CAP 兜底机制保持不变(deep watch 仍做变化检测,只是 body 短路掉无意义重算)。
|
||||
//
|
||||
// UX-260616-03: 触发时机扩展。除原 messages deep watch(toolCall 状态翻转/新消息条数),
|
||||
// 触发时机扩展。除原 messages deep watch(toolCall 状态翻转/新消息条数),
|
||||
// 另加 agentRound watch(AiAgentRound 新轮次 → 收起上一轮工具卡,聚焦当前轮)。
|
||||
// active Set 推导逻辑复用 buildActiveToolIds,两 watch 共用。
|
||||
function buildActiveToolIds(msgs: typeof store.state.messages): Set<string> {
|
||||
@@ -330,7 +330,7 @@ watch(
|
||||
},
|
||||
)
|
||||
|
||||
// UX-260616-03: AiAgentRound 新轮次到达时收起上一轮工具卡(round 递增)。
|
||||
// AiAgentRound 新轮次到达时收起上一轮工具卡(round 递增)。
|
||||
// 首次加载不触发(等真到达更"新"的轮次才收起,避免历史会话加载即折叠)。
|
||||
watch(
|
||||
() => store.state.agentRound,
|
||||
@@ -340,12 +340,12 @@ watch(
|
||||
},
|
||||
)
|
||||
|
||||
// CR-260615-06: mdReady 就绪后主动触发首屏 Markdown 重渲染
|
||||
// mdReady 就绪后主动触发首屏 Markdown 重渲染
|
||||
// (marked 加载慢 + 无新 delta 时末段停留纯文本,不触发模板重算)
|
||||
const _mdRenderKey = ref(0)
|
||||
function forceUpdate() { _mdRenderKey.value++ }
|
||||
|
||||
// ── UX-2025-01: 选文字保持(save/restore Selection) ──
|
||||
// ── 选文字保持(save/restore Selection) ──
|
||||
// v-html 整体替换 innerHTML 会销毁子 DOM 节点 → Node 引用断连 → 选区丢失。
|
||||
// 方案: 保存为「气泡内文本偏移量 + 选中文字」,重建后按偏移量重新定位。
|
||||
// 偏移量对 Markdown 渲染结果稳定(同内容→同DOM文本节点序列→同偏移)。
|
||||
@@ -484,7 +484,7 @@ watch(mdReady, (ready) => {
|
||||
}
|
||||
})
|
||||
|
||||
// ── 消息气泡拷贝(F-260615-13) ──
|
||||
// ── 消息气泡拷贝──
|
||||
// 必须在 click 事件内(或其同步调用链)调 clipboard API (浏览器安全限制)
|
||||
// navigator.clipboard 在非 secure context (Electron file:// / 旧版 webview) 不可用,
|
||||
// 回退到 execCommand('copy') 兜底;两者均失败时提示复制失败而非假成功。
|
||||
@@ -558,7 +558,7 @@ async function copyMsgAsImage(msg: AiMessage): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
// ── UX-03: 错误气泡操作入口 ──
|
||||
// ── 错误气泡操作入口 ──
|
||||
// errorType 经 useAiEvents push 错误消息时附带(types.ts AiMessage 未含此字段,
|
||||
// 经 cast 读写闭环在 AiChat.vue / useAiEvents 两端,不污染 types.ts 定义)。
|
||||
interface AiMessageWithErrorType extends AiMessage {
|
||||
@@ -572,7 +572,7 @@ function canOpenSettings(msg: AiMessage): boolean {
|
||||
}
|
||||
|
||||
/**
|
||||
* UX-260616-01: 错误是否可重试(显隐「重试」按钮)。
|
||||
* 错误是否可重试(显隐「重试」按钮)。
|
||||
* Fatal 类错误(鉴权/Provider 配置错——4xx/401/403/invalid_api_key)重试必再败,
|
||||
* 隐藏重试按钮,引导用户走「去设置」。network/timeout/unknown 为瞬时错误,可重试。
|
||||
* retryable 推断依据后端 error_type(本批前端推断;后端 AiStreamRetry/AiError 加 retryable 字段下批 df-ai 增强)。
|
||||
@@ -587,7 +587,7 @@ function canRetry(msg: AiMessage): boolean {
|
||||
/**
|
||||
* 错误气泡「重试」:取错误前的末尾 user 消息重发。
|
||||
* 复用 regenerate 链路(后端弹末尾 AI 回复——错误消息本身——取触发它的 user 消息重跑)。
|
||||
* 语义等价"重新生成",但语义场景是错误恢复,文案/入口独立(UX-03)。
|
||||
* 语义等价"重新生成",但语义场景是错误恢复,文案/入口独立。
|
||||
*/
|
||||
async function handleRetry(): Promise<void> {
|
||||
try {
|
||||
@@ -741,9 +741,9 @@ const renderItems = computed<RenderItem[]>(() => {
|
||||
return items
|
||||
})
|
||||
|
||||
// UX-09:切换对话/新对话时由父取消编辑态;滚动边沿/跟随意图重置在此自管(SW-260618-18)。
|
||||
// 切换对话/新对话时由父取消编辑态;滚动边沿/跟随意图重置在此自管。
|
||||
watch(() => store.state.activeConversationId, () => {
|
||||
// SW-260618-18: 切会话重置滚动边沿检测 + 跟随意图(新会话默认跟随底部)
|
||||
// 切会话重置滚动边沿检测 + 跟随意图(新会话默认跟随底部)
|
||||
setFollowing(true)
|
||||
// BUG-260624-01(消息重叠根治·论证 workflow 完整性高优):MessageList 是持久单实例(AiChat.vue
|
||||
// 无 :key/v-if 卸载),实例级 streamingBlocks(:69)跨会话/跨轮残留是消息重叠主根因之一。
|
||||
@@ -1043,7 +1043,7 @@ defineExpose({
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
/* UX-2025-19: .ai-messages 的直接 flex 子(B-260618-01:虚拟滚动 sentinel 已移除,
|
||||
/* .ai-messages 的直接 flex 子虚拟滚动 sentinel 已移除,
|
||||
现仅作消息分组的语义容器,gap:14px 由 .ai-messages 承担)。 */
|
||||
.ai-msg-slot { display: flex; flex-direction: column; }
|
||||
|
||||
@@ -1148,7 +1148,7 @@ defineExpose({
|
||||
color: var(--df-danger);
|
||||
border-color: rgba(240,101,101,0.3);
|
||||
}
|
||||
/* B-260618-06:修正 UX-260617-21 的 display:block 方案——display:block 让 table 失去表格
|
||||
/* 修正 UX-260617-21 的 display:block 方案——display:block 让 table 失去表格
|
||||
布局上下文,tr/td 脱离 table 上下文塌成块级堆叠丢列对齐(用户截图反馈测试报告四列表格错乱)。
|
||||
正解:table 保持默认 display:table(列对齐恢复)+ max-width:100%;宽表格横向滚动交由气泡
|
||||
容器 .ai-msg-bubble--ai.ai-md 的 overflow-x:auto 承载(CSS spec 中 table-display 下
|
||||
@@ -1253,7 +1253,7 @@ defineExpose({
|
||||
.ai-msg-chip--idea { background: rgba(255, 255, 255, 0.22); border: 1px solid rgba(255, 255, 255, 0.35); }
|
||||
.ai-msg-chip--skill { background: rgba(0, 0, 0, 0.18); }
|
||||
|
||||
/* ── 消息时间戳(UX-2025-13) ── */
|
||||
/* ── 消息时间戳── */
|
||||
.ai-msg-time {
|
||||
font-size: 10px;
|
||||
line-height: 1;
|
||||
@@ -1268,7 +1268,7 @@ defineExpose({
|
||||
align-self: flex-end; /* 用户消息:靠气泡右对齐 */
|
||||
}
|
||||
|
||||
/* ── 消息气泡拷贝按钮(F-260615-13) ── */
|
||||
/* ── 消息气泡拷贝按钮── */
|
||||
.ai-copy-btn {
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
@@ -1299,7 +1299,7 @@ defineExpose({
|
||||
right: 30px;
|
||||
}
|
||||
|
||||
/* ── UX-09: 末条 user 消息编辑按钮(hover 显示,回填输入框编辑重生成) ── */
|
||||
/* ── 末条 user 消息编辑按钮(hover 显示,回填输入框编辑重生成) ── */
|
||||
.ai-msg-edit-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -1324,7 +1324,7 @@ defineExpose({
|
||||
color: var(--df-text);
|
||||
}
|
||||
|
||||
/* ── UX-02/UX-03: 消息操作栏(hover 显示 / 错误气泡常显) ── */
|
||||
/* ── UX-02/消息操作栏(hover 显示 / 错误气泡常显) ── */
|
||||
.ai-msg-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -1338,7 +1338,7 @@ defineExpose({
|
||||
.ai-msg-actions--error {
|
||||
opacity: 1;
|
||||
}
|
||||
/* UX-260616-01: Fatal 错误(不可重试)的提示文案——替代隐藏的「重试」按钮,
|
||||
/* Fatal 错误(不可重试)的提示文案——替代隐藏的「重试」按钮,
|
||||
说明为何无重试入口,引导走「去设置」。 */
|
||||
.ai-msg-not-retryable {
|
||||
font-size: 10.5px;
|
||||
@@ -1492,7 +1492,7 @@ defineExpose({
|
||||
background: var(--df-bg);
|
||||
}
|
||||
|
||||
/* ═══ F-15: 压缩摘要 system 块(compressed 原文已展开,摘要置顶提示发给 AI 的是它) ═══
|
||||
/* ═══ 压缩摘要 system 块(compressed 原文已展开,摘要置顶提示发给 AI 的是它) ═══
|
||||
用户诉求:压缩只后端省 token,界面不藏消息。原文 compressed 照常 user/ai 气泡展开,
|
||||
摘要 system 块置顶 accent 提示,让用户知情"AI 看到的是摘要"。 */
|
||||
.ai-msg-system {
|
||||
|
||||
@@ -334,7 +334,7 @@ watch(() => store.state.activeConversationId, () => {
|
||||
}
|
||||
})
|
||||
|
||||
// 切 provider 后模型 override 同步(BUG-2026-07-07:provider 切换后 header 模型名不刷新):
|
||||
// 切 provider 后模型 override 同步provider 切换后 header 模型名不刷新):
|
||||
// - modelOverride 是模块级 ref(useAiSend)不持久化,切 provider 后仍持旧 provider 的 model_id,
|
||||
// 该 id 不在新 provider 池里 → 下拉显示错位(UI 显示 X,实际后端用路由默认 Y)。
|
||||
// - 守卫:当前选择对新 provider 模型池无效(空/不在池)时,默认选权重最高模型。
|
||||
@@ -400,7 +400,7 @@ const historyExpanded = ref(false)
|
||||
function scrollToMessage(msgId: string) {
|
||||
historyExpanded.value = false
|
||||
// 消息列表已有 data-msg-id 属性,直接 DOM 查询定位。
|
||||
// BUG-260802:msgId 由后端生成,可能含 CSS 选择器元字符(: . [ ] / 空格 等),
|
||||
// msgId 由后端生成,可能含 CSS 选择器元字符(: . [ ] / 空格 等),
|
||||
// 模板拼 selector 会被选择器解析器当伪类/类名异常 → querySelector 抛错或返回 null。
|
||||
// 用 CSS.escape 转义 msgId,确保任意字符串安全嵌入属性选择器。
|
||||
// 旧浏览器无 CSS.escape 的兜底:encodeURIComponent + attr*= 模糊匹配(非精确,降级路径)。
|
||||
@@ -542,18 +542,18 @@ async function removeGoal(i: number) {
|
||||
function cycleProvider() {
|
||||
const ps = store.state.providers
|
||||
if (ps.length < 2) return
|
||||
// F-260616-10: activeProvider 为 null 时从 DB 默认(is_default)起算,避免 findIndex=-1 跳到 providers[0]
|
||||
// activeProvider 为 null 时从 DB 默认(is_default)起算,避免 findIndex=-1 跳到 providers[0]
|
||||
const startId = store.state.activeProvider || ps.find(p => p.is_default)?.id || null
|
||||
const idx = startId ? ps.findIndex(p => p.id === startId) : -1
|
||||
// idx=-1 且无默认 provider 时 fallback 到 0(极端边界,正常路径不会走到)
|
||||
const next = ps[(Math.max(idx, 0) + 1) % ps.length]
|
||||
store.setProvider(next.id)
|
||||
// UX-2025-14: 切换反馈 — toast 提示当前 model + 临时 bar 展示 model 名(2s 后淡出)。
|
||||
// 切换反馈 — toast 提示当前 model + 临时 bar 展示 model 名(2s 后淡出)。
|
||||
// toast 机制:复用父自管 reactive showToast(分离窗口无 App.vue 根 toast),经 emit 回父弹。
|
||||
showProviderSwitched(next.name || next.id)
|
||||
}
|
||||
|
||||
// ── UX-2025-14: Provider 切换临时 bar ──
|
||||
// ── Provider 切换临时 bar ──
|
||||
// 切换后展示当前 model 名小字,0.15s 淡入,2s 后淡出。本子组件自管 bar 显隐(ref + Transition),
|
||||
// toast 经 emit 回父(父持有 toast reactive 容器)。
|
||||
const providerBarVisible = ref(false)
|
||||
@@ -567,7 +567,7 @@ function showProviderSwitched(modelName: string) {
|
||||
_providerBarTimer = setTimeout(() => { providerBarVisible.value = false }, 2000)
|
||||
}
|
||||
|
||||
// UX-2025-14:释放 provider 切换 bar 计时器(从 AiChat onBeforeUnmount 迁移,与 _providerBarTimer 同处)
|
||||
// 释放 provider 切换 bar 计时器(从 AiChat onBeforeUnmount 迁移,与 _providerBarTimer 同处)
|
||||
onBeforeUnmount(() => {
|
||||
if (_providerBarTimer) { clearTimeout(_providerBarTimer); _providerBarTimer = null }
|
||||
})
|
||||
@@ -604,7 +604,7 @@ onBeforeUnmount(() => {
|
||||
white-space: nowrap;
|
||||
max-width: 200px;
|
||||
}
|
||||
/* AE-2025-02:审批徽标(待审批>0 时显于头部,点击跳首个审批卡) */
|
||||
/* 审批徽标(待审批>0 时显于头部,点击跳首个审批卡) */
|
||||
.ai-approval-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -1014,7 +1014,7 @@ onBeforeUnmount(() => {
|
||||
font-size: 11px;
|
||||
color: var(--df-danger);
|
||||
}
|
||||
/* UX-2025-14: Provider 切换临时 bar(淡入淡出 0.15s) */
|
||||
/* Provider 切换临时 bar(淡入淡出 0.15s) */
|
||||
.ai-provider-switch-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -114,7 +114,7 @@ const displayProjects = computed(() =>
|
||||
.dot-release { background: var(--df-success); }
|
||||
.dot-planning { background: var(--df-info); }
|
||||
.dot-cancelled { background: var(--df-text-dim); }
|
||||
/* WK-1:paused 置灰(对齐 cancelled dim 档,临时暂停不误标为已取消) */
|
||||
/* paused 置灰(对齐 cancelled dim 档,临时暂停不误标为已取消) */
|
||||
.dot-paused { background: var(--df-text-dim); }
|
||||
|
||||
.project-name {
|
||||
|
||||
@@ -322,7 +322,7 @@ const emit = defineEmits<{
|
||||
(e: 'promote'): void
|
||||
(e: 'delete'): void
|
||||
(e: 'status-change', value: IdeaStatus): void
|
||||
// WK-16:返 Promise<boolean> 让子组件 await 知成功/失败。
|
||||
// 返 Promise<boolean> 让子组件 await 知成功/失败。
|
||||
// true=落库成功(关编辑态);false=失败(保持编辑态,toast 由父组件处理)。
|
||||
(e: 'update-desc', value: string): Promise<boolean>
|
||||
(e: 'update-related', ids: string[]): Promise<void>
|
||||
@@ -344,7 +344,7 @@ function startEdit() {
|
||||
}
|
||||
|
||||
// P1-⑥ saveEdit 改异步:emit 返回 Promise<boolean>(父组件 store.updateIdea 落库结果),
|
||||
// 仅成功时关编辑态;失败保持编辑态 + toast 由父组件处理(WK-16)。
|
||||
// 仅成功时关编辑态;失败保持编辑态 + toast 由父组件处理。
|
||||
async function saveEdit() {
|
||||
if (savingDesc.value) return
|
||||
savingDesc.value = true
|
||||
@@ -392,7 +392,7 @@ watch(() => props.idea.description, () => void nextTick(measureDesc))
|
||||
// P1-④ 状态切换 select → badge 点击:点击当前 badge 展开/收起下拉菜单,
|
||||
// 选中项后 emit('status-change') 交父组件确认+落库(沿用既有契约,不破坏 statusOptions 来源)。
|
||||
const statusMenuOpen = ref(false)
|
||||
// WK-5:状态下拉排除 promoted(立项走独立 promoteToProject 流程,列表状态菜单同排除,
|
||||
// 状态下拉排除 promoted(立项走独立 promoteToProject 流程,列表状态菜单同排除,
|
||||
// 见 Ideas.vue onStatusChange 兜底);statusOptions 仍含 promoted 供 label 映射(已转立项的灵感 badge 正常显示)。
|
||||
const menuStatusOptions = computed(() => props.statusOptions.filter(s => s.value !== 'promoted'))
|
||||
function closeStatusMenu() {
|
||||
@@ -502,7 +502,7 @@ async function saveTagsEdit() {
|
||||
savingTags.value = true
|
||||
try {
|
||||
const tags = tagsEditInput.value.split(',').map(t => t.trim()).filter(Boolean)
|
||||
// WK-16:emit 返回落库结果,失败保持编辑态(成功才关),toast 由父组件处理
|
||||
// emit 返回落库结果,失败保持编辑态(成功才关),toast 由父组件处理
|
||||
const ok = await emit('update-tags', JSON.stringify(tags))
|
||||
if (ok) tagsEditing.value = false
|
||||
await nextTick()
|
||||
@@ -520,9 +520,9 @@ function statusLabelKey(status: IdeaStatus): string {
|
||||
return props.statusOptions.find(s => s.value === status)?.labelKey ?? status
|
||||
}
|
||||
|
||||
// B-260615-25:灵感描述 Markdown 渲染(复用 AiChat/TaskDetail 同款渲染器,模块级单例),
|
||||
// 灵感描述 Markdown 渲染(复用 AiChat/TaskDetail 同款渲染器,模块级单例),
|
||||
// useRendered 封装 computed(读 mdReady 触发响应式 + renderMd)+ ensureLoaded(幂等预热)
|
||||
// BUG-2026-07-08:原漏解构 ensureLoaded + onMounted 未调用,冷启动(marked 未被其他组件预热)
|
||||
// 原漏解构 ensureLoaded + onMounted 未调用,冷启动(marked 未被其他组件预热)
|
||||
// 时 mdReady 永远 false,renderMd 走 escapeFallback 纯文本降级,Markdown 语法不渲染。
|
||||
const { rendered: renderedDesc, ensureLoaded: ensureMdLoaded } = useRendered(
|
||||
() => props.idea.description ?? '',
|
||||
@@ -548,7 +548,7 @@ interface AdversarialEval {
|
||||
|
||||
// 对抗式评估数据持久化在 ai_analysis JSON 字段中,前端解析渲染
|
||||
//
|
||||
// WK-15:旧实现直接 JSON.parse 后强转 AdversarialEval,字段缺失/非数值时
|
||||
// 旧实现直接 JSON.parse 后强转 AdversarialEval,字段缺失/非数值时
|
||||
// 模板对 undefined*100 调 .toFixed 显 NaN,assessmentClass 对 undefined 调
|
||||
// .toLowerCase() 崩。这里在解析层补齐数值默认值(防御式归一化),模板直读不二次判空:
|
||||
// - 数值字段缺失/非数值 → 0
|
||||
@@ -599,7 +599,7 @@ const localAssessmentLabel = (recommendation: string): string =>
|
||||
assessmentLabel(t, recommendation)
|
||||
|
||||
function sentimentClass(sentiment: number) {
|
||||
// 统一三档(FR-C2: 原 >=0 与模板 >0 矛盾,net_sentiment=0 时文案负面样式 positive)
|
||||
// 统一三档原 >=0 与模板 >0 矛盾,net_sentiment=0 时文案负面样式 positive)
|
||||
if (sentiment > 0) return 'positive'
|
||||
if (sentiment < 0) return 'negative'
|
||||
return 'neutral'
|
||||
|
||||
@@ -106,7 +106,7 @@ const resizing = ref(false)
|
||||
// maximized=全宽(flex:1填充) | sidebar=固定宽
|
||||
const panelStyle = computed(() => {
|
||||
if (aiStore.state.maximized) return { flex: '1', minWidth: '0' }
|
||||
// LW-11:内联 width 用 min(panelWidth, 100vw) —— 小屏 media query 的 width:100vw 会被内联样式
|
||||
// 内联 width 用 min(panelWidth, 100vw) —— 小屏 media query 的 width:100vw 会被内联样式
|
||||
// 更高优先级覆盖致失效;min() 让窄视口下宽度钳制到 100vw,media query 的 absolute 定位/层级仍生效
|
||||
return {
|
||||
width: `min(${panelWidth.value}px, 100vw)`,
|
||||
|
||||
@@ -92,7 +92,7 @@ const appSettings = useAppSettingsStore()
|
||||
// 展开 192px(文字+图标) / 收缩 56px(仅图标,hover tooltip 显示文字)
|
||||
// 优先级:用户手动设置 > 小窗口自动收缩(≤768px)
|
||||
const COLLAPSE_THRESHOLD = 768
|
||||
// LW-10:useSetting 响应式读(loadAll 异步填充缓存后 watch 自动同步刷新)——
|
||||
// useSetting 响应式读(loadAll 异步填充缓存后 watch 自动同步刷新)——
|
||||
// 原 onMounted 用 appSettings.get 同步读早于父级 loadAll 完成,重启后偏好丢失
|
||||
const userCollapsed = appSettings.useSetting('df-sidebar-collapsed', false)
|
||||
const isNarrowWindow = ref(false) // 小窗口自动收缩标记
|
||||
@@ -119,7 +119,7 @@ function handleCollapseKeydown(e: KeyboardEvent) {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// LW-10:收缩偏好由 useSetting 响应式恢复(loadAll 填充缓存后 watch 自动同步),移除 onMounted 同步读
|
||||
// 收缩偏好由 useSetting 响应式恢复(loadAll 填充缓存后 watch 自动同步),移除 onMounted 同步读
|
||||
// 检查当前窗口宽度
|
||||
checkWindowWidth()
|
||||
window.addEventListener('resize', checkWindowWidth)
|
||||
|
||||
@@ -135,7 +135,7 @@ async function handleApproval(decision: string) {
|
||||
}
|
||||
}
|
||||
|
||||
// F-260615-01: 多选审批(UX-260618-17 submitting 防双击保留)
|
||||
// 多选审批(UX-260618-17 submitting 防双击保留)
|
||||
async function handleApprovalMulti() {
|
||||
if (multiDecisions.value.length === 0) return
|
||||
submitting.value = true
|
||||
@@ -153,7 +153,7 @@ async function handleApprovalMulti() {
|
||||
}
|
||||
}
|
||||
|
||||
// F-260615-01: 每次新审批清空多选状态(避免上轮残留)
|
||||
// 每次新审批清空多选状态(避免上轮残留)
|
||||
watch(() => pendingApproval.value, (newApproval) => {
|
||||
if (newApproval) {
|
||||
multiDecisions.value = []
|
||||
|
||||
@@ -86,7 +86,7 @@ import SettingRow from './SettingRow.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const appSettings = useAppSettingsStore()
|
||||
// F-260620: 接共享 toast 通道(Settings.vue showToast),替代 console.error/warn 用户零反馈
|
||||
// 接共享 toast 通道(Settings.vue showToast),替代 console.error/warn 用户零反馈
|
||||
const emit = defineEmits<{ (e: 'toast', msg: string, type: 'error' | 'warning' | 'info' | 'success'): void }>()
|
||||
|
||||
// F-260619-03 Phase A: AI 工具文件访问授权目录白名单 UI
|
||||
@@ -126,7 +126,7 @@ async function pickDir() {
|
||||
const selected = await open({ directory: true, multiple: false })
|
||||
if (!selected || Array.isArray(selected)) return
|
||||
newDir.value = selected as string
|
||||
// F-260620: 选完即加(消除选目录后需二次点"添加"的摩擦)
|
||||
// 选完即加(消除选目录后需二次点"添加"的摩擦)
|
||||
await addDir()
|
||||
} catch (e) {
|
||||
console.error('选择目录失败:', e)
|
||||
|
||||
@@ -88,7 +88,7 @@ const { t } = useI18n()
|
||||
const appSettings = useAppSettingsStore()
|
||||
|
||||
const emit = defineEmits<{
|
||||
// P0-2: 对齐 ToastType 加 'success'(本面板暂未用,保类型一致)
|
||||
// 对齐 ToastType 加 'success'(本面板暂未用,保类型一致)
|
||||
(e: 'toast', msg: string, type: 'error' | 'warning' | 'info' | 'success'): void
|
||||
}>()
|
||||
|
||||
|
||||
@@ -67,9 +67,9 @@ const appSettings = useAppSettingsStore()
|
||||
const settings = reactive({
|
||||
llmGlobalConcurrency: appSettings.get<number>('df-llm-global-concurrency', 3),
|
||||
llmPerConvConcurrency: appSettings.get<number>('df-llm-per-conv-concurrency', 2),
|
||||
// F-260616-01: Agentic 循环最大轮次,默认 10(与后端 DEFAULT_MAX_AGENT_ITERATIONS 对齐)
|
||||
// Agentic 循环最大轮次,默认 10(与后端 DEFAULT_MAX_AGENT_ITERATIONS 对齐)
|
||||
agentMaxIterations: appSettings.get<number>('df-ai-agent-max-iterations', 10),
|
||||
// F-260616-07: 流式失败重试次数,默认 3(与后端 DEFAULT_MAX_AGENT_RETRIES 对齐)
|
||||
// 流式失败重试次数,默认 3(与后端 DEFAULT_MAX_AGENT_RETRIES 对齐)
|
||||
agentMaxRetries: appSettings.get<number>('df-ai-agent-max-retries', 3),
|
||||
})
|
||||
|
||||
@@ -148,7 +148,7 @@ function onRetryChange() {
|
||||
}
|
||||
|
||||
// 同步 Agentic 最大轮次到后端(debounce 300ms)
|
||||
// F-260616-01: 后端 loop 入口 load 快照锁定边界——热改后当前 loop 不受影响,
|
||||
// 后端 loop 入口 load 快照锁定边界——热改后当前 loop 不受影响,
|
||||
// 下次发消息才生效(与 llm_concurrency 传 Arc 实时反映的区别)。
|
||||
// 双 clamp:此处 1-50 + 后端 command 再 clamp 1-50,防越界输入致 loop 失效/失控
|
||||
let _agentIterTimer: ReturnType<typeof setTimeout> | null = null
|
||||
@@ -184,7 +184,7 @@ const unlimited = computed({
|
||||
})
|
||||
|
||||
// 同步流式失败重试次数到后端(debounce 300ms)
|
||||
// F-260616-07: 后端 loop 入口 load 快照锁定边界——热改后当前 loop 不受影响,
|
||||
// 后端 loop 入口 load 快照锁定边界——热改后当前 loop 不受影响,
|
||||
// 下次发消息才生效。双 clamp:此处 0-10 + 后端 command 再 clamp 0-10
|
||||
let _agentRetryTimer: ReturnType<typeof setTimeout> | null = null
|
||||
function syncAgentMaxRetries() {
|
||||
@@ -206,12 +206,12 @@ function syncAgentMaxRetries() {
|
||||
onMounted(() => {
|
||||
// 启动同步一次 LLM 并发配置(防刷新页面后后端未恢复用户设定值)
|
||||
syncConcurrencyConfig()
|
||||
// F-260616-01: 启动同步一次 Agentic 最大轮次(防刷新页面后后端未恢复用户设定值)
|
||||
// F-260618-23: App.vue 根 onMounted 已在应用启动时恢复(覆盖用户没先进 Settings 直接用 AI Chat 的场景),
|
||||
// 启动同步一次 Agentic 最大轮次(防刷新页面后后端未恢复用户设定值)
|
||||
// App.vue 根 onMounted 已在应用启动时恢复(覆盖用户没先进 Settings 直接用 AI Chat 的场景),
|
||||
// 此处保留双保险(IPC 幂等值相同无害)
|
||||
syncAgentMaxIterations()
|
||||
// F-260616-07: 启动同步一次流式失败重试次数(防刷新页面后后端未恢复用户设定值)
|
||||
// F-260618-23: 同上,App.vue 根 onMounted 已恢复,此处双保险保留
|
||||
// 启动同步一次流式失败重试次数(防刷新页面后后端未恢复用户设定值)
|
||||
// 同上,App.vue 根 onMounted 已恢复,此处双保险保留
|
||||
syncAgentMaxRetries()
|
||||
})
|
||||
|
||||
|
||||
@@ -185,7 +185,7 @@ import type { AiProviderConfig, ModelConfig, Modality, Capability, ProbeSource }
|
||||
import { PROVIDER_PRESETS, CUSTOM_PRESET_ID, findPreset } from './providerPresets'
|
||||
|
||||
// ============================================================
|
||||
// AI 提供商域 — 真实后端 CRUD + F-04c 负载均衡池(enabled / weight)
|
||||
// AI 提供商域 — 真实后端 CRUD + c 负载均衡池(enabled / weight)
|
||||
// ============================================================
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -200,7 +200,7 @@ async function syncAiStoreProviders() {
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
// P0-2: 加 'success'(保存成功绿色 toast)
|
||||
// 加 'success'(保存成功绿色 toast)
|
||||
(e: 'toast', msg: string, type: 'error' | 'warning' | 'info' | 'success'): void
|
||||
(e: 'providers-changed'): void
|
||||
}>()
|
||||
@@ -229,7 +229,7 @@ const providerForm = reactive({
|
||||
// F-01 阶段6:模型拉取 + 列表
|
||||
fetching: false,
|
||||
models: [] as ModelConfig[],
|
||||
// P0-2: 保存失败的持久错误横幅(非自隐 toast)。
|
||||
// 保存失败的持久错误横幅(非自隐 toast)。
|
||||
// toast 3s 自隐易被忽略,保存失败时表单内常驻错误条 + 可操作建议,
|
||||
// 直到下次保存尝试/重开表单才清除。null=无错误。
|
||||
saveError: null as string | null,
|
||||
@@ -401,7 +401,7 @@ function openProviderForm(p?: AiProviderConfig) {
|
||||
providerForm.models = []
|
||||
}
|
||||
providerForm.fetching = false
|
||||
providerForm.saveError = null // P0-2: 重开表单清旧错误横幅
|
||||
providerForm.saveError = null // 重开表单清旧错误横幅
|
||||
providerForm.visible = true
|
||||
// L19/M32: 记初始快照(在 visible=true、字段回填完成后),isFormDirty 据此比对。
|
||||
providerForm.savedSnapshot = formBusinessSnapshot()
|
||||
@@ -426,7 +426,7 @@ function onPresetChange() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试连接按钮可用条件(P0-1):
|
||||
* 测试连接按钮可用条件
|
||||
* - baseUrl 必填(无论新建/编辑,表单值都要有)
|
||||
* - 新建态(editId 空):api_key 必填(无已保存配置可沿用)
|
||||
* - 编辑态(editId 有):api_key 可空(留空=沿用已保存密钥)
|
||||
@@ -438,7 +438,7 @@ const canTestConnection = computed(() => {
|
||||
})
|
||||
|
||||
/**
|
||||
* 测试连接并拉取模型列表(P0-1)。
|
||||
* 测试连接并拉取模型列表。
|
||||
* 用**表单当前值**测试(非 DB 值):新建态直接传表单值;编辑态传表单值 + editId,
|
||||
* apiKey 留空时后端从已保存配置沿用密钥。一次性测试不写库。
|
||||
*
|
||||
@@ -486,7 +486,7 @@ async function fetchModels() {
|
||||
}
|
||||
|
||||
/**
|
||||
* P0-2: 把后端返回的错误文本分类成「用户可操作的建议」。
|
||||
* 把后端返回的错误文本分类成「用户可操作的建议」。
|
||||
*
|
||||
* 后端 commands/provider.rs 已对密钥相关失败返回友好中文 Err(含原因+建议),
|
||||
* 这里只做轻量分类挂上"建议下一步",避免裸 Err 文本吓到用户:
|
||||
@@ -526,14 +526,14 @@ async function saveProvider() {
|
||||
defaultModel: providerForm.defaultModel,
|
||||
modelConfigs: providerForm.models,
|
||||
})
|
||||
// P0-2: 成功明确反馈——清错误横幅 + 关表单 + 绿色「已保存」toast
|
||||
// 成功明确反馈——清错误横幅 + 关表单 + 绿色「已保存」toast
|
||||
providerForm.saveError = null
|
||||
providerForm.visible = false
|
||||
providerForm.savedSnapshot = '' // L19: 关表单清快照
|
||||
await loadProviders()
|
||||
emit('toast', t('settings.toastSaved'), 'success')
|
||||
} catch (e) {
|
||||
// P0-2: 失败时①表单保持编辑态(不关)②表单内常驻错误横幅(分类建议,非自隐)
|
||||
// 失败时①表单保持编辑态(不关)②表单内常驻错误横幅(分类建议,非自隐)
|
||||
// ③仍发 error toast(用户视线不在表单时仍可见),双通道确保反馈不被忽略。
|
||||
const raw = errMsg(e)
|
||||
console.error(t('settings.toastSaveFail', { msg: raw }), e)
|
||||
@@ -570,10 +570,10 @@ async function setDefaultProvider(id: string) {
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// F-260614-04c: 负载均衡池可编辑层(enabled / weight)
|
||||
// c: 负载均衡池可编辑层(enabled / weight)
|
||||
// 卡片即时调 IPC(对齐 syncConcurrencyConfig 模式);落库后后端 reload_provider_caps
|
||||
// 重建 per_provider caps,下条消息即按新配置 acquire。
|
||||
// 不走 saveProvider(全量 INSERT OR REPLACE 会触发 R-PD-1 密钥迁移分支,与 F-04c 轻量 UPDATE 路径冲突)。
|
||||
// 不走 saveProvider(全量 INSERT OR REPLACE 会触发 R-PD-1 密钥迁移分支,与 c 轻量 UPDATE 路径冲突)。
|
||||
// ============================================================
|
||||
let _poolTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
@@ -693,7 +693,7 @@ onMounted(loadProviders)
|
||||
}
|
||||
.detail-value.mask { color: var(--df-text-dim); letter-spacing: 1px; }
|
||||
|
||||
/* Provider 卡片 disabled 态(F-260614-04c:enabled=false 视觉弱化) */
|
||||
/* Provider 卡片 disabled 态(c:enabled=false 视觉弱化) */
|
||||
.provider-card--disabled { opacity: 0.6; }
|
||||
.pool-badge {
|
||||
font-size: 10px;
|
||||
@@ -718,7 +718,7 @@ onMounted(loadProviders)
|
||||
.form-field-hint { flex-basis: 100%; margin-left: 82px; font-size: 11px; color: var(--df-text-dim); line-height: 1.5; }
|
||||
.form-actions { display: flex; justify-content: flex-end; padding-top: 4px; }
|
||||
|
||||
/* P0-2: 保存失败持久错误横幅(常驻 form-actions 上方,醒目于自隐 toast) */
|
||||
/* 保存失败持久错误横幅(常驻 form-actions 上方,醒目于自隐 toast) */
|
||||
.save-error-banner {
|
||||
background: var(--df-danger-bg);
|
||||
border: 0.5px solid var(--df-danger);
|
||||
|
||||
@@ -60,7 +60,7 @@ export const PROVIDER_PRESETS: readonly ProviderPreset[] = [
|
||||
baseUrl: 'https://api.deepseek.com',
|
||||
defaultModel: 'deepseek-v4-flash',
|
||||
// 重要:deepseek-chat / deepseek-reasoner 旧名已于 2026-07-24 停用,
|
||||
// 现分别指向 deepseek-v4-flash 的非思考/思考模式(V4-Flash 2026-07-31 发布)。
|
||||
// 现分别指向 deepseek-v4-flash 的非思考/思考模式(lash 2026-07-31 发布)。
|
||||
// 思考模式改 deepseek-v4-flash(传 thinking 参数)或 deepseek-v4-pro。
|
||||
},
|
||||
{
|
||||
@@ -74,7 +74,7 @@ export const PROVIDER_PRESETS: readonly ProviderPreset[] = [
|
||||
baseUrl: 'https://token.sensenova.cn/v1',
|
||||
// 官方文档(platform.sensenova.cn/docs)核实:Token Plan API 仅 3 个模型 ——
|
||||
// sensenova-6.7-flash-lite(多模态对话主力)/ sensenova-u1-fast(图像生成,不能对话)/ deepseek-v4-flash(纯文本对话)。
|
||||
// 无 U1 / U1-Pro 对话模型(那是官网产品名,API 不提供);旧 SenseNova-Turbo 已退役。
|
||||
// 无 U1 / ro 对话模型(那是官网产品名,API 不提供);旧 SenseNova-Turbo 已退役。
|
||||
// 默认填对话主力 sensenova-6.7-flash-lite(文档所有工具接入示例的 Model ID)。
|
||||
defaultModel: 'sensenova-6.7-flash-lite',
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user