优化: workflow记录补project_id+前端审批/交互打磨

This commit is contained in:
lxy
2026-08-12 00:40:40 +08:00
parent d31a27512c
commit 7a1e80eac2
9 changed files with 83 additions and 32 deletions
+13 -2
View File
@@ -148,7 +148,18 @@ pub async fn run_workflow_inner(
// 1. 先构建运行时 DAG,校验失败直接返回,不落库 // 1. 先构建运行时 DAG,校验失败直接返回,不落库
let runtime_dag = state.registry.build_dag(&dag).map_err(err_str)?; let runtime_dag = state.registry.build_dag(&dag).map_err(err_str)?;
// 2. 写入执行记录(status=running // 2. P2-I: 反查 project_idtask_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 execution_id: ExecutionId = new_id().into();
let dag_json = serde_json::to_string(&dag).map_err(err_str)?; let dag_json = serde_json::to_string(&dag).map_err(err_str)?;
let record = WorkflowRecord { let record = WorkflowRecord {
@@ -159,7 +170,7 @@ pub async fn run_workflow_inner(
// CR-52: triggered_by 由调用方透传(命令层 run_workflow="manual", // CR-52: triggered_by 由调用方透传(命令层 run_workflow="manual",
// execute_run_workflow_for_tool="ai"),不再硬编码 "manual" 致 AI 路径误标。 // execute_run_workflow_for_tool="ai"),不再硬编码 "manual" 致 AI 路径误标。
triggered_by: Some(triggered_by.to_string()), triggered_by: Some(triggered_by.to_string()),
project_id: None, project_id,
task_id: task_id.clone(), task_id: task_id.clone(),
created_at: now_millis(), created_at: now_millis(),
completed_at: None, completed_at: None,
+2 -3
View File
@@ -257,9 +257,8 @@ const activeConv = computed(() =>
watch( watch(
() => activeConv.value?.title, () => activeConv.value?.title,
(newTitle, oldTitle) => { (newTitle, oldTitle) => {
if (!newTitle) return // FIX-17 P2-4:合并为单条件——标题从空→非空才触发淡入,其他变化不闪
if (oldTitle && oldTitle === newTitle) return if (!newTitle || oldTitle) return
if (oldTitle) return
titleFlash.value = true titleFlash.value = true
if (_titleFlashTimer) clearTimeout(_titleFlashTimer) if (_titleFlashTimer) clearTimeout(_titleFlashTimer)
_titleFlashTimer = setTimeout(() => { titleFlash.value = false }, 320) _titleFlashTimer = setTimeout(() => { titleFlash.value = false }, 320)
+10
View File
@@ -316,13 +316,23 @@ export function setConvStreaming(convId: string | null | undefined, value: boole
/** /**
* 写某 conv 的 currentText(空串时仅清字段,streaming 仍 true 则保留项)。 * 写某 conv 的 currentText(空串时仅清字段,streaming 仍 true 则保留项)。
* FIX-17 P2-3:收敛对称——当 streaming=false 且 currentText 被清空时,删 Map 项。
* *
* @param convId 目标会话 id * @param convId 目标会话 id
* @param value currentText 目标值(空串=清流式累积,但会话可能仍在 streaming) * @param value currentText 目标值(空串=清流式累积,但会话可能仍在 streaming)
*/ */
export function setConvCurrentText(convId: string | null | undefined, value: string): void { export function setConvCurrentText(convId: string | null | undefined, value: string): void {
if (!convId) return if (!convId) return
if (value) {
ensureConvStreamState(convId).currentText = 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 项)。 /** 清某 conv 的 stream state(会话删除/收尾彻底清,删整个 Map 项)。
+3 -1
View File
@@ -25,7 +25,9 @@ import { state } from '@/stores/ai'
import { resolveAiLang } from './aiShared' import { resolveAiLang } from './aiShared'
import { setContextCallbacks } from './useAiEvents' 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) const isCompressing = ref(false)
/** /**
+4
View File
@@ -516,6 +516,10 @@ async function deleteConversation(id: string) {
loadMoreCursor.earliestSeq = null loadMoreCursor.earliestSeq = null
loadMoreCursor.convId = null loadMoreCursor.convId = null
loadMoreCursor.loading = false loadMoreCursor.loading = false
// FIX-17 P1-11:回落相邻会话,避免空白无引导
if (neighborId) {
await switchConversation(neighborId)
}
} }
scheduleConversationsRefresh() scheduleConversationsRefresh()
// 删的是活跃会话:回落相邻会话作为新活跃视图 // 删的是活跃会话:回落相邻会话作为新活跃视图
+2 -1
View File
@@ -326,7 +326,8 @@ async function syncToMain() {
if (screenWidth > 0) { if (screenWidth > 0) {
targetX = screenWidth - AI_DOCK_WIDTH - AI_DOCK_GAP 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) { } else if (screenWidth > 0 && targetX + AI_DOCK_WIDTH > screenWidth) {
// 非最大化但主+AI 超屏幕:AI 窗口本身让位(缩窄或往左偏),不动主窗口。 // 非最大化但主+AI 超屏幕:AI 窗口本身让位(缩窄或往左偏),不动主窗口。
// 优先策略:贴屏幕右侧边,AI 窗口宽度可能被主窗口右边缘掩盖一部分,但不强迫主窗口缩小。 // 优先策略:贴屏幕右侧边,AI 窗口宽度可能被主窗口右边缘掩盖一部分,但不强迫主窗口缩小。
+1 -1
View File
@@ -21,7 +21,7 @@ import { useAppSettingsStore } from '../../stores/appSettings'
/// 流式渲染返回的单块渲染结果(html + 模板 v-for key) /// 流式渲染返回的单块渲染结果(html + 模板 v-for key)
export interface StreamBlock { export interface StreamBlock {
html: string html: string
/** 已完成块=block文本hash(稳定), 末块='tail-'+递增序号(每次变) */ /** 已完成块=block文本hash(稳定), 末块='tail-'+递增序号(每帧内唯一) */
key: string key: string
} }
+29 -5
View File
@@ -55,7 +55,12 @@ export function dispatchApprovalIPC(
return aiApi.approve(tc.id, approved) return aiApi.approve(tc.id, approved)
} }
export function useToolApproval(tc: Ref<AiToolCallInfo>, 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<AiToolCallInfo>, emit: ToolApprovalEmit) {
const { t } = useI18n() const { t } = useI18n()
const { confirmState, confirmDialog, answerConfirm } = useConfirm() const { confirmState, confirmDialog, answerConfirm } = useConfirm()
@@ -123,15 +128,21 @@ export function useToolApproval(tc: Ref<AiToolCallInfo>, emit: any) {
* 批准按钮处理(risk 类专用)。 * 批准按钮处理(risk 类专用)。
* AE-2025-05:High 风险工具 → 先二次确认(useConfirm 弹层)。 * AE-2025-05:High 风险工具 → 先二次确认(useConfirm 弹层)。
* loading + 超时兜底复用 B-260616-08 机制。 * loading + 超时兜底复用 B-260616-08 机制。
* FIX-17 P1-10:approving 提前到 confirmDialog 前,防二次确认期间双击重入。
*/ */
async function onApprove() { async function onApprove() {
if (approving.value) return // 防重入 if (approving.value) return // 防重入
// 先锁防重入(含二次确认 await 期间)
approving.value = true
try {
// AE-2025-05:High 风险 → 先二次确认 // AE-2025-05:High 风险 → 先二次确认
if (HIGH_RISK_TOOLS.has(tc.value.name)) { if (HIGH_RISK_TOOLS.has(tc.value.name)) {
const ok = await confirmDialog(highRiskConfirmMsg(tc.value.name)) const ok = await confirmDialog(highRiskConfirmMsg(tc.value.name))
if (!ok) return if (!ok) {
approving.value = false // 取消二次确认 → 解锁
return
}
} }
approving.value = true
approvingTimer = setTimeout(() => { approvingTimer = setTimeout(() => {
approving.value = false approving.value = false
approvingTimer = null approvingTimer = null
@@ -139,6 +150,10 @@ export function useToolApproval(tc: Ref<AiToolCallInfo>, emit: any) {
showApproveTimeoutToast() showApproveTimeoutToast()
}, APPROVE_LOADING_TIMEOUT_MS) }, APPROVE_LOADING_TIMEOUT_MS)
emit('approve', { id: tc.value.id, approved: true }) emit('approve', { id: tc.value.id, approved: true })
} catch (e) {
approving.value = false // 异常解锁
throw e
}
} }
/** /**
@@ -213,12 +228,17 @@ export function useToolApproval(tc: Ref<AiToolCallInfo>, emit: any) {
* M5:拒绝前二次确认(useConfirm 弹层)。拒绝不可逆——工具返 Err、loop 续跑不再重试该工具, * M5:拒绝前二次确认(useConfirm 弹层)。拒绝不可逆——工具返 Err、loop 续跑不再重试该工具,
* 与放行(once/session/always 均可后续再授权)不对称,误点代价高;补确认与 approve(高风险)对称。 * 与放行(once/session/always 均可后续再授权)不对称,误点代价高;补确认与 approve(高风险)对称。
* risk 类拒绝(onReject)不补确认:risk reject 是常规审批否决,语义明确且可重新触发;仅 path 类加。 * risk 类拒绝(onReject)不补确认:risk reject 是常规审批否决,语义明确且可重新触发;仅 path 类加。
* FIX-17 P1-10:approving 提前到 confirmDialog 前,防二次确认期间双击重入。
*/ */
async function onDeny() { async function onDeny() {
if (approving.value) return if (approving.value) return
const ok = await confirmDialog(t('aiChat.dirAuthDenyConfirm'))
if (!ok) return
approving.value = true approving.value = true
try {
const ok = await confirmDialog(t('aiChat.dirAuthDenyConfirm'))
if (!ok) {
approving.value = false // 取消二次确认 → 解锁
return
}
approvingTimer = setTimeout(() => { approvingTimer = setTimeout(() => {
approving.value = false approving.value = false
approvingTimer = null approvingTimer = null
@@ -226,6 +246,10 @@ export function useToolApproval(tc: Ref<AiToolCallInfo>, emit: any) {
showApproveTimeoutToast() showApproveTimeoutToast()
}, APPROVE_LOADING_TIMEOUT_MS) }, APPROVE_LOADING_TIMEOUT_MS)
emit('approve', { id: tc.value.id, approved: false, decision: 'deny' }) emit('approve', { id: tc.value.id, approved: false, decision: 'deny' })
} catch (e) {
approving.value = false // 异常解锁
throw e
}
} }
// status 离开 pending_approval → 复位 approving(后端回执/转态时)。 // status 离开 pending_approval → 复位 approving(后端回执/转态时)。
+1 -1
View File
@@ -715,7 +715,7 @@ async function handleWorkflowAdvance(target: string) {
const execId = await workflowApi.run( const execId = await workflowApi.run(
`task-advance:${target}`, `task-advance:${target}`,
{}, // 空 dag → 后端 template_for(target) 选模板 {}, // 空 dag → 后端 template_for(target) 选模板
{}, // 全局 config 留空(节点级 config 由模板/运行时注入) {}, // 全局 config 留空(节点级 config 由模板 gate 注入 + 运行时 task_id 注入)
task.value.id, task.value.id,
target, target,
) )