diff --git a/src-tauri/src/commands/workflow.rs b/src-tauri/src/commands/workflow.rs index a45a401..47da4dc 100644 --- a/src-tauri/src/commands/workflow.rs +++ b/src-tauri/src/commands/workflow.rs @@ -148,7 +148,18 @@ pub async fn run_workflow_inner( // 1. 先构建运行时 DAG,校验失败直接返回,不落库 let runtime_dag = state.registry.build_dag(&dag).map_err(err_str)?; - // 2. 写入执行记录(status=running) + // 2. P2-I: 反查 project_id(task_id 有值时),不阻塞工作流 + let project_id = match &task_id { + Some(tid) => TaskRepo::new(&state.db) + .get_by_id(tid) + .await + .ok() + .flatten() + .map(|t| t.project_id), + None => None, + }; + + // 3. 写入执行记录(status=running) let execution_id: ExecutionId = new_id().into(); let dag_json = serde_json::to_string(&dag).map_err(err_str)?; let record = WorkflowRecord { @@ -159,7 +170,7 @@ pub async fn run_workflow_inner( // CR-52: triggered_by 由调用方透传(命令层 run_workflow="manual", // execute_run_workflow_for_tool="ai"),不再硬编码 "manual" 致 AI 路径误标。 triggered_by: Some(triggered_by.to_string()), - project_id: None, + project_id, task_id: task_id.clone(), created_at: now_millis(), completed_at: None, diff --git a/src/components/ai/ConversationSidebar.vue b/src/components/ai/ConversationSidebar.vue index 2cdd371..8dc94b1 100644 --- a/src/components/ai/ConversationSidebar.vue +++ b/src/components/ai/ConversationSidebar.vue @@ -257,9 +257,8 @@ const activeConv = computed(() => watch( () => activeConv.value?.title, (newTitle, oldTitle) => { - if (!newTitle) return - if (oldTitle && oldTitle === newTitle) return - if (oldTitle) return + // FIX-17 P2-4:合并为单条件——标题从空→非空才触发淡入,其他变化不闪 + if (!newTitle || oldTitle) return titleFlash.value = true if (_titleFlashTimer) clearTimeout(_titleFlashTimer) _titleFlashTimer = setTimeout(() => { titleFlash.value = false }, 320) diff --git a/src/composables/ai/aiShared.ts b/src/composables/ai/aiShared.ts index ff36c94..4fb9f90 100644 --- a/src/composables/ai/aiShared.ts +++ b/src/composables/ai/aiShared.ts @@ -316,13 +316,23 @@ export function setConvStreaming(convId: string | null | undefined, value: boole /** * 写某 conv 的 currentText(空串时仅清字段,streaming 仍 true 则保留项)。 + * FIX-17 P2-3:收敛对称——当 streaming=false 且 currentText 被清空时,删 Map 项。 * * @param convId 目标会话 id * @param value currentText 目标值(空串=清流式累积,但会话可能仍在 streaming) */ export function setConvCurrentText(convId: string | null | undefined, value: string): void { if (!convId) return - ensureConvStreamState(convId).currentText = value + if (value) { + ensureConvStreamState(convId).currentText = value + } else { + // 空串收敛:若 streaming=false 则删项,否则仅清字段(保留项) + const cur = convStreamStates.get(convId) + if (cur) { + cur.currentText = '' + if (!cur.streaming) convStreamStates.delete(convId) + } + } } /** 清某 conv 的 stream state(会话删除/收尾彻底清,删整个 Map 项)。 diff --git a/src/composables/ai/useAiContext.ts b/src/composables/ai/useAiContext.ts index 6f704a5..b58544c 100644 --- a/src/composables/ai/useAiContext.ts +++ b/src/composables/ai/useAiContext.ts @@ -25,7 +25,9 @@ import { state } from '@/stores/ai' import { resolveAiLang } from './aiShared' import { setContextCallbacks } from './useAiEvents' -/// 压缩中 loading(防重复点击;AiCompressing→true / AiManualCompressed|AiAutoCompressed|AiError→false) +// FIX-17 P2-7:模块级单例 ref,多窗口共享(分离窗口与主窗口共用同一 isCompressing)。 +// 已知限制:分离窗口压缩时,主窗口的压缩按钮也显示 loading(反之亦然);非功能 bug, +// 但若需 per-window 独立压缩态需改为 inject/provide 或独立 store 实例,本版暂不处理。 const isCompressing = ref(false) /** diff --git a/src/composables/ai/useAiConversations.ts b/src/composables/ai/useAiConversations.ts index 54dde52..26ddc17 100644 --- a/src/composables/ai/useAiConversations.ts +++ b/src/composables/ai/useAiConversations.ts @@ -516,6 +516,10 @@ async function deleteConversation(id: string) { loadMoreCursor.earliestSeq = null loadMoreCursor.convId = null loadMoreCursor.loading = false + // FIX-17 P1-11:回落相邻会话,避免空白无引导 + if (neighborId) { + await switchConversation(neighborId) + } } scheduleConversationsRefresh() // 删的是活跃会话:回落相邻会话作为新活跃视图 diff --git a/src/composables/ai/useAiWindow.ts b/src/composables/ai/useAiWindow.ts index a889a37..dc11816 100644 --- a/src/composables/ai/useAiWindow.ts +++ b/src/composables/ai/useAiWindow.ts @@ -326,7 +326,8 @@ async function syncToMain() { if (screenWidth > 0) { targetX = screenWidth - AI_DOCK_WIDTH - AI_DOCK_GAP } - targetHeight = size.height > targetHeight ? targetHeight : size.height + // FIX-17 P2-5: 最大化时 targetHeight = size.height, height > height 恒 false, + // 原三元是无操作死代码,已移除。 } else if (screenWidth > 0 && targetX + AI_DOCK_WIDTH > screenWidth) { // 非最大化但主+AI 超屏幕:AI 窗口本身让位(缩窄或往左偏),不动主窗口。 // 优先策略:贴屏幕右侧边,AI 窗口宽度可能被主窗口右边缘掩盖一部分,但不强迫主窗口缩小。 diff --git a/src/composables/ai/useStreamRenderer.ts b/src/composables/ai/useStreamRenderer.ts index 12f6cfc..44ce04a 100644 --- a/src/composables/ai/useStreamRenderer.ts +++ b/src/composables/ai/useStreamRenderer.ts @@ -21,7 +21,7 @@ import { useAppSettingsStore } from '../../stores/appSettings' /// 流式渲染返回的单块渲染结果(html + 模板 v-for key) export interface StreamBlock { html: string - /** 已完成块=block文本hash(稳定), 末块='tail-'+递增序号(每次变) */ + /** 已完成块=block文本hash(稳定), 末块='tail-'+递增序号(每帧内唯一) */ key: string } diff --git a/src/composables/ai/useToolApproval.ts b/src/composables/ai/useToolApproval.ts index 78798e7..7107954 100644 --- a/src/composables/ai/useToolApproval.ts +++ b/src/composables/ai/useToolApproval.ts @@ -55,7 +55,12 @@ export function dispatchApprovalIPC( return aiApi.approve(tc.id, approved) } -export function useToolApproval(tc: Ref, emit: any) { +/** useToolApproval emit 签名 — FIX-17 P2-6:补全类型替代 any */ +interface ToolApprovalEmit { + (event: 'approve', payload: { id: string; approved: boolean; decision?: 'once' | 'session' | 'always' | 'deny' }): void +} + +export function useToolApproval(tc: Ref, emit: ToolApprovalEmit) { const { t } = useI18n() const { confirmState, confirmDialog, answerConfirm } = useConfirm() @@ -123,22 +128,32 @@ export function useToolApproval(tc: Ref, emit: any) { * 批准按钮处理(risk 类专用)。 * AE-2025-05:High 风险工具 → 先二次确认(useConfirm 弹层)。 * loading + 超时兜底复用 B-260616-08 机制。 + * FIX-17 P1-10:approving 提前到 confirmDialog 前,防二次确认期间双击重入。 */ async function onApprove() { if (approving.value) return // 防重入 - // AE-2025-05:High 风险 → 先二次确认 - if (HIGH_RISK_TOOLS.has(tc.value.name)) { - const ok = await confirmDialog(highRiskConfirmMsg(tc.value.name)) - if (!ok) return - } + // 先锁防重入(含二次确认 await 期间) approving.value = true - approvingTimer = setTimeout(() => { - approving.value = false - approvingTimer = null - console.warn('[AI] 审批 loading 超时,后端可能未回执,已复位允许重试:', tc.value.id) - showApproveTimeoutToast() - }, APPROVE_LOADING_TIMEOUT_MS) - emit('approve', { id: tc.value.id, approved: true }) + try { + // AE-2025-05:High 风险 → 先二次确认 + if (HIGH_RISK_TOOLS.has(tc.value.name)) { + const ok = await confirmDialog(highRiskConfirmMsg(tc.value.name)) + if (!ok) { + approving.value = false // 取消二次确认 → 解锁 + return + } + } + approvingTimer = setTimeout(() => { + approving.value = false + approvingTimer = null + console.warn('[AI] 审批 loading 超时,后端可能未回执,已复位允许重试:', tc.value.id) + showApproveTimeoutToast() + }, APPROVE_LOADING_TIMEOUT_MS) + emit('approve', { id: tc.value.id, approved: true }) + } catch (e) { + approving.value = false // 异常解锁 + throw e + } } /** @@ -213,19 +228,28 @@ export function useToolApproval(tc: Ref, emit: any) { * M5:拒绝前二次确认(useConfirm 弹层)。拒绝不可逆——工具返 Err、loop 续跑不再重试该工具, * 与放行(once/session/always 均可后续再授权)不对称,误点代价高;补确认与 approve(高风险)对称。 * risk 类拒绝(onReject)不补确认:risk reject 是常规审批否决,语义明确且可重新触发;仅 path 类加。 + * FIX-17 P1-10:approving 提前到 confirmDialog 前,防二次确认期间双击重入。 */ async function onDeny() { if (approving.value) return - const ok = await confirmDialog(t('aiChat.dirAuthDenyConfirm')) - if (!ok) return approving.value = true - approvingTimer = setTimeout(() => { - approving.value = false - approvingTimer = null - console.warn('[AI] 路径授权 loading 超时,后端可能未回执,已复位允许重试:', tc.value.id) - showApproveTimeoutToast() - }, APPROVE_LOADING_TIMEOUT_MS) - emit('approve', { id: tc.value.id, approved: false, decision: 'deny' }) + try { + const ok = await confirmDialog(t('aiChat.dirAuthDenyConfirm')) + if (!ok) { + approving.value = false // 取消二次确认 → 解锁 + return + } + approvingTimer = setTimeout(() => { + approving.value = false + approvingTimer = null + console.warn('[AI] 路径授权 loading 超时,后端可能未回执,已复位允许重试:', tc.value.id) + showApproveTimeoutToast() + }, APPROVE_LOADING_TIMEOUT_MS) + emit('approve', { id: tc.value.id, approved: false, decision: 'deny' }) + } catch (e) { + approving.value = false // 异常解锁 + throw e + } } // status 离开 pending_approval → 复位 approving(后端回执/转态时)。 diff --git a/src/views/TaskDetail.vue b/src/views/TaskDetail.vue index 608d9a1..8102473 100644 --- a/src/views/TaskDetail.vue +++ b/src/views/TaskDetail.vue @@ -715,7 +715,7 @@ async function handleWorkflowAdvance(target: string) { const execId = await workflowApi.run( `task-advance:${target}`, {}, // 空 dag → 后端 template_for(target) 选模板 - {}, // 全局 config 留空(节点级 config 由模板/运行时注入) + {}, // 全局 config 留空(节点级 config 由模板 gate 注入 + 运行时 task_id 注入) task.value.id, target, )