新增: AI Chat多项增强(审批去重/编辑重发/导出/实体引用/会话置顶搜索)+任务推进链df-nodes落地

This commit is contained in:
lxy
2026-06-16 12:41:13 +08:00
parent 212a927eee
commit 7d5cd4c89a
62 changed files with 4576 additions and 248 deletions
+168 -3
View File
@@ -63,6 +63,32 @@
>{{ advancing ? $t('taskDetail.advancing') : $t(act.label) }}</button>
</div>
</div>
<!-- F-260616-06 -1 / B-41 工作流推进按钮(联动任务, target_status 后端自动选推进链模板)
与手动 advance 并存:仅当当前态的 primary 前向推进目标 {in_progress, testing, done}
( template_for 有对应模板)时显示手动 advance 仍走 advance_task IPC;工作流推进
run_workflow, AiNode/HumanNode 执行后再由后端回调推进任务
独立 loading 标志 wfAdvancing advancing 互不干扰 -->
<div v-if="wfAdvanceAction" class="info-item info-block advance-row">
<span class="label">{{ $t('taskDetail.workflowAdvanceTitle') }}</span>
<div class="advance-actions">
<button
type="button"
class="btn btn-sm btn-primary"
:disabled="wfAdvancing || advancing"
@click="handleWorkflowAdvance(wfAdvanceAction.target)"
>{{ wfAdvancing ? $t('taskDetail.workflowAdvancing') : $t('taskDetail.workflowAdvance') }}</button>
</div>
<!-- B-41 轻量进度(监听 workflow-event: NodeStarted/NodeCompleted/WorkflowFailed)
完整进度条/预估留后续批;完成/失败后任务态由后端回调推进,df-data-changed 自动刷新 task -->
<div v-if="wfAdvancing || wfProgressHint" class="wf-progress">
<span v-if="wfRunningNode">{{ $t('taskDetail.workflowStepRunning', { node: wfRunningNode }) }}</span>
<span v-else-if="wfDoneTotal > 0" class="wf-progress-count">
{{ $t('taskDetail.workflowStepsProgress', { done: wfDoneCount, total: wfDoneTotal }) }}
</span>
<span v-if="wfCompletedHint" class="wf-progress-hint">{{ $t('taskDetail.workflowCompletedHint') }}</span>
<span v-else-if="wfFailedHint" class="wf-progress-hint wf-progress-hint-fail">{{ $t('taskDetail.workflowFailedHint') }}</span>
</div>
</div>
<div class="info-item">
<span class="label">{{ $t('taskDetail.priority') }}</span>
<span class="value">
@@ -119,6 +145,7 @@ import { useI18n } from 'vue-i18n'
import { useRoute } from 'vue-router'
import { listen } from '@tauri-apps/api/event'
import { taskApi, projectApi } from '@/api'
import { workflowApi } from '@/api'
import { formatDate } from '@/utils/time'
import { useRendered } from '@/composables/useMarkdown'
import {
@@ -127,7 +154,7 @@ import {
priorityLabel,
priorityClass,
} from '../constants/project'
import type { TaskRecord, ProjectRecord, DfDataChangedPayload } from '@/api/types'
import type { TaskRecord, ProjectRecord, DfDataChangedPayload, WorkflowEventPayload } from '@/api/types'
const { t } = useI18n()
const route = useRoute()
@@ -138,6 +165,24 @@ const task = ref<TaskRecord | null>(null)
const projects = ref<ProjectRecord[]>([])
const advancing = ref(false)
// ============================================================
// F-260616-06 ①-1 / B-41 工作流推进状态(与手动 advance 的 advancing 独立,互不干扰)
// ------------------------------------------------------------
// wfAdvancing:推进中态;wfExecId:当前监听的工作流执行 ID(过滤 workflow-event);
// wfTotalNodes/wfDoneCount/wfRunningNode:轻量进度(听 NodeStarted/NodeCompleted);
// wfResult: 'completed' | 'failed' | null —— 终态提示,完成/失败后自动清空(由新一次推进重置)。
const wfAdvancing = ref(false)
const wfExecId = ref<string | null>(null)
const wfTotalNodes = ref(0)
const wfDoneCount = ref(0)
const wfRunningNode = ref<string>('')
const wfResult = ref<'completed' | 'failed' | null>(null)
const wfProgressHint = computed(() => wfResult.value !== null)
const wfCompletedHint = computed(() => wfResult.value === 'completed')
const wfFailedHint = computed(() => wfResult.value === 'failed')
const wfDoneTotal = computed(() => wfTotalNodes.value)
const taskId = computed(() => route.params.id as string)
// B-24:任务描述 Markdown 渲染(复用 AiChat 同款渲染器,模块级单例),useRendered 封装
@@ -206,6 +251,28 @@ const advanceActions = computed<AdvanceAction[]>(() => {
return ADVANCE_MAP[s] ?? []
})
// F-260616-06 ①-1: 工作流推进按钮的目标态。
// 仅当当前态的 primary(前向推进)目标 ∈ template_for 支持的三态 {in_progress, testing, done} 时显示。
// 非前向(退回/阻塞/取消)不走工作流(无对应模板,且语义上工作流只做前向推进)。
// ADVANCE_MAP 各行的首个 primary 项:todo→in_progress, in_review→testing, testing→done, blocked→in_progress
// in_progress 的 primary 是 in_review(无模板,不显示工作流推进按钮)。
const WF_SUPPORTED_TARGETS = new Set(['in_progress', 'testing', 'done'])
// CR-08-O1: blocked 是阻塞态(任务被阻塞),其 primary=in_progress 虽 ∈ WF_SUPPORTED_TARGETS,
// 但 template_for('in_progress') 是「todo→in_progress 从头执行」拓扑(AiNode 等重新跑一遍),
// blocked 恢复语义不该重跑 AiNode(任务已有上下文/进度,只需恢复非从头执行)。
// 最小修法:blocked 不显示工作流推进按钮,用户手动 advance(blocked→in_progress 走后端状态机)或先解除阻塞。
// (引入 blocked 恢复模板需后端 template_for 扩展,复杂度高,非阻断 med 项留待 UX 决策。)
const WF_EXCLUDED_FROM = new Set(['blocked'])
const wfAdvanceAction = computed<AdvanceAction | null>(() => {
const s = task.value?.status
if (!s) return null
if (WF_EXCLUDED_FROM.has(s)) return null
const actions = ADVANCE_MAP[s] ?? []
const primary = actions.find(a => a.variant === 'btn-primary')
if (!primary) return null
return WF_SUPPORTED_TARGETS.has(primary.target) ? primary : null
})
async function handleAdvance(target: string) {
if (!task.value || advancing.value) return
advancing.value = true
@@ -215,12 +282,87 @@ async function handleAdvance(target: string) {
task.value = updated
errorMsg.value = ''
} catch (e: any) {
errorMsg.value = e?.toString() ?? t('taskDetail.advanceFailed')
errorMsg.value = t('taskDetail.advanceFailed', { msg: e?.toString() ?? t('common.unknownError') })
} finally {
advancing.value = false
}
}
// F-260616-06 ①-1: 工作流推进 — 传空 dag + target_status,后端自动选推进链模板。
// B-41: 监听 workflow-event 显示轻量进度;完成/失败后任务态由后端回调推进(②-3/②-4),
// df-data-changed 自动刷新 task(AR-11 已通),此处不直接改 task.value。
// name 用推进链标识(模板内未硬编码 task_id,运行时由后端 config 注入,前端只传 topology hint)。
async function handleWorkflowAdvance(target: string) {
if (!task.value || wfAdvancing.value || advancing.value) return
wfAdvancing.value = true
wfResult.value = null
wfRunningNode.value = ''
wfDoneCount.value = 0
wfTotalNodes.value = 0
try {
const execId = await workflowApi.run(
`task-advance:${target}`,
{}, // 空 dag → 后端 template_for(target) 选模板
{}, // 全局 config 留空(节点级 config 由模板/运行时注入)
task.value.id,
target,
)
wfExecId.value = execId
errorMsg.value = ''
} catch (e: any) {
wfAdvancing.value = false
wfExecId.value = null
errorMsg.value = t('taskDetail.workflowAdvanceFailed', { msg: e?.toString() ?? t('common.unknownError') })
}
}
// B-41: 处理单个工作流事件(由 onEvent 回调 dispatch)。按 execution_id 过滤当前推进。
function handleWorkflowEvent(payload: WorkflowEventPayload) {
if (!wfExecId.value || payload.execution_id !== wfExecId.value) return
const evt = payload.event
switch (evt?.type) {
case 'node_started': {
// 第一次 NodeStarted 时初始化 total(后端模板节点数,无法前端预知,
// 用「已启动 + 已完成」近似 total,显示已完成/已启动进度)
const node = String(evt.node_id ?? '')
wfRunningNode.value = node
if (wfTotalNodes.value === 0) wfTotalNodes.value = 1
else wfTotalNodes.value = Math.max(wfTotalNodes.value, wfDoneCount.value + 1)
break
}
case 'node_completed': {
wfDoneCount.value += 1
wfRunningNode.value = ''
break
}
case 'workflow_completed': {
wfAdvancing.value = false
wfRunningNode.value = ''
wfResult.value = 'completed'
// 终态提示保留 3s 后清空(任务本体由 df-data-changed 刷新,提示不影响数据)
setTimeout(() => {
if (wfResult.value === 'completed') wfResult.value = null
}, 3000)
break
}
case 'workflow_failed':
case 'node_failed': {
if (evt?.type === 'workflow_failed') {
wfAdvancing.value = false
wfRunningNode.value = ''
wfResult.value = 'failed'
setTimeout(() => {
if (wfResult.value === 'failed') wfResult.value = null
}, 3000)
}
// node_failed: 单节点失败,工作流后续会发 workflow_failed,此处不提前改终态
break
}
default:
break
}
}
async function load() {
loading.value = true
errorMsg.value = ''
@@ -234,7 +376,7 @@ async function load() {
projects.value = ps
} catch (e: any) {
task.value = null
errorMsg.value = e?.toString() ?? t('taskDetail.loadFailed')
errorMsg.value = t('taskDetail.loadFailed', { msg: e?.toString() ?? t('common.unknownError') })
} finally {
loading.value = false
}
@@ -252,6 +394,8 @@ watch(taskId, () => { load() })
// 不享受 store 全局 df-data-changed 监听(该监听只刷 store.tasks 列表,不含本视图的当前 task 单体),
// 故此本地监听 entity=task/project 时重载当前 task。
let _unlistenDataChanged: (() => void) | null = null
// B-41: 工作流推进事件 unlistener(听 NodeStarted/NodeCompleted/WorkflowFailed 显示轻量进度)
let _unlistenWorkflowEvent: (() => void) | null = null
onMounted(async () => {
ensureLoaded() // 后台预热 Markdown 渲染器(模块单例,与 AiChat 共享),不阻塞 load
@@ -268,10 +412,18 @@ onMounted(async () => {
} catch (e) {
console.error('[TaskDetail] 启动 df-data-changed 监听失败:', e)
}
// B-41: 工作流推进事件监听(复用 workflowApi.onEvent, 与 workflow store 的全局监听并存无冲突 —
// 多个 listen 各自接收全量事件,本视图按 execution_id 过滤只处理自己发起的推进)
try {
_unlistenWorkflowEvent = await workflowApi.onEvent(handleWorkflowEvent)
} catch (e) {
console.error('[TaskDetail] 启动 workflow-event 监听失败:', e)
}
})
onBeforeUnmount(() => {
if (_unlistenDataChanged) { _unlistenDataChanged(); _unlistenDataChanged = null }
if (_unlistenWorkflowEvent) { _unlistenWorkflowEvent(); _unlistenWorkflowEvent = null }
})
</script>
@@ -317,6 +469,19 @@ onBeforeUnmount(() => {
gap: 8px;
}
/* B-41 工作流推进轻量进度提示 */
.wf-progress {
display: flex;
flex-direction: column;
gap: 2px;
margin-top: 6px;
font-size: 12px;
color: var(--df-text-secondary);
}
.wf-progress-count { color: var(--df-text-dim); }
.wf-progress-hint { color: var(--df-success); }
.wf-progress-hint-fail { color: var(--df-danger); }
/* F-04 review 轮次徽章 */
.review-rounds-badge {
font-size: 11px;