新增: AI Chat多项增强(审批去重/编辑重发/导出/实体引用/会话置顶搜索)+任务推进链df-nodes落地
This commit is contained in:
+168
-3
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user