优化: workflow记录补project_id+前端审批/交互打磨
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 项)。
|
||||
|
||||
@@ -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)
|
||||
|
||||
/**
|
||||
|
||||
@@ -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()
|
||||
// 删的是活跃会话:回落相邻会话作为新活跃视图
|
||||
|
||||
@@ -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 窗口宽度可能被主窗口右边缘掩盖一部分,但不强迫主窗口缩小。
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,12 @@ export function dispatchApprovalIPC(
|
||||
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 { confirmState, confirmDialog, answerConfirm } = useConfirm()
|
||||
|
||||
@@ -123,22 +128,32 @@ export function useToolApproval(tc: Ref<AiToolCallInfo>, 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<AiToolCallInfo>, 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(后端回执/转态时)。
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user