新增: TaskDetail任务产出与AI自审结论展示(output_json解析+review红绿标)

This commit is contained in:
2026-06-16 22:41:05 +08:00
parent c10adaf0b0
commit 741b0b9d0c
4 changed files with 118 additions and 0 deletions

View File

@@ -93,6 +93,13 @@ export interface TaskRecord {
base_branch: string | null base_branch: string | null
/** review 退回累计轮数(in_review→in_progress / testing→in_review 各 +1) */ /** review 退回累计轮数(in_review→in_progress / testing→in_review 各 +1) */
review_rounds: number review_rounds: number
/**
* 任务产出 JSON 字符串(决策 a:task 中心,产出跟 task 走)。
* schema: { text: string, model?: string, usage?: {...}, review?: {
* verdict: 'pass'|'fail'|'unknown', summary: string, suggestions: string[], ... } }
* 旧任务无产出时为 undefined。前端 parsedOutput try/catch 兜底解析。
*/
output_json?: string
created_at: string created_at: string
updated_at: string updated_at: string
} }

View File

@@ -18,6 +18,13 @@ export default {
assignee: 'Assignee', assignee: 'Assignee',
baseBranch: 'Base Branch', baseBranch: 'Base Branch',
workflowDef: 'Workflow Definition', workflowDef: 'Workflow Definition',
// F-AiNodeSelfReview: task output + AI self-review verdict
output: 'Output',
review: {
pass: 'Review Passed',
fail: 'Review Failed',
unknown: 'Review Inconclusive',
},
reviewRounds: 'Review Round {n}', reviewRounds: 'Review Round {n}',
createdAt: 'Created At', createdAt: 'Created At',
updatedAt: 'Updated At', updatedAt: 'Updated At',

View File

@@ -18,6 +18,13 @@ export default {
assignee: '负责人', assignee: '负责人',
baseBranch: '基础分支', baseBranch: '基础分支',
workflowDef: '工作流定义', workflowDef: '工作流定义',
// F-AiNodeSelfReview: 任务产出 + AI 自审结论展示
output: '产出',
review: {
pass: '自审通过',
fail: '自审未通过',
unknown: '自审结论未知',
},
reviewRounds: '第 {n} 轮 Review', reviewRounds: '第 {n} 轮 Review',
createdAt: '创建时间', createdAt: '创建时间',
updatedAt: '更新时间', updatedAt: '更新时间',

View File

@@ -125,6 +125,23 @@
<span class="label">{{ $t('taskDetail.workflowDef') }}</span> <span class="label">{{ $t('taskDetail.workflowDef') }}</span>
<span class="value mono">{{ task.workflow_def_id ?? '—' }}</span> <span class="value mono">{{ task.workflow_def_id ?? '—' }}</span>
</div> </div>
<!-- F-AiNodeSelfReview: 任务产出 + AI 自审结果展示 -->
<!-- 仅当 task.output_json 存在( AiNode 写产出/自审后由 df-data-changed 自动刷新出现) -->
<div v-if="task.output_json && parsedOutput" class="info-item info-block output-block">
<span class="label">{{ $t('taskDetail.output') }}</span>
<div class="value output-body">
<!-- AI 自审结论卡(若有 review 子字段) -->
<div v-if="parsedOutput.review" class="review-card" :class="reviewVerdictClass">
<span class="review-verdict">{{ reviewVerdictLabel }}</span>
<span v-if="parsedOutput.review.summary" class="review-summary">{{ parsedOutput.review.summary }}</span>
<ul v-if="reviewSuggestions.length" class="review-suggestions">
<li v-for="(s, i) in reviewSuggestions" :key="i">{{ s }}</li>
</ul>
</div>
<!-- AI 产出正文(Markdown 渲染,复用 useRendered 单例渲染器) -->
<div v-if="parsedOutput.text" class="output-text ai-md" v-html="renderedOutput"></div>
</div>
</div>
<div class="info-item"> <div class="info-item">
<span class="label">{{ $t('taskDetail.createdAt') }}</span> <span class="label">{{ $t('taskDetail.createdAt') }}</span>
<span class="value">{{ formatDate(task.created_at) }}</span> <span class="value">{{ formatDate(task.created_at) }}</span>
@@ -191,6 +208,57 @@ const { rendered: renderedDesc, ensureLoaded } = useRendered(
() => task.value?.description ?? '', () => task.value?.description ?? '',
) )
// ============================================================
// F-AiNodeSelfReview: output_json 解析 + AI 产出 Markdown 渲染
// ------------------------------------------------------------
// output_json schema(与 df-nodes ai_node.rs 写产出现对齐):
// { text, model?, usage?, review?: { verdict: 'pass'|'fail'|'unknown', summary?, suggestions? } }
// parsedOutput try/catch: JSON.parse 边界必要兜底(LLM 自审输出可能不合规/老数据格式漂移),
// 解析失败返回 null → v-if 守卫整块不渲染(不裸显报错)。
interface TaskOutputReview {
verdict?: string
summary?: string
suggestions?: unknown
}
interface TaskOutput {
text?: string
model?: string
usage?: unknown
review?: TaskOutputReview
}
const parsedOutput = computed<TaskOutput | null>(() => {
const raw = task.value?.output_json
if (!raw) return null
try {
return JSON.parse(raw) as TaskOutput
} catch {
return null
}
})
// AI 产出正文渲染(复用模块单例渲染器,与 renderedDesc 共享 marked/DOMPurify/缓存)
const { rendered: renderedOutput } = useRendered(
() => parsedOutput.value?.text ?? '',
)
// review.verdict 红绿标:pass→绿(success),fail→红(danger),未知/缺失→灰(dim)
const reviewVerdictClass = computed(() => {
const v = parsedOutput.value?.review?.verdict
if (v === 'pass') return 'review-pass'
if (v === 'fail') return 'review-fail'
return 'review-unknown'
})
// review.verdict 标签(pass/fail/unknown 走 i18n,异常值兜底 unknown)
const reviewVerdictLabel = computed(() => {
const v = parsedOutput.value?.review?.verdict
if (v === 'pass') return t('taskDetail.review.pass')
if (v === 'fail') return t('taskDetail.review.fail')
return t('taskDetail.review.unknown')
})
// review.suggestions 列表(数组守卫,非数组时返回空避免 v-for 报错)
const reviewSuggestions = computed<string[]>(() => {
const s = parsedOutput.value?.review?.suggestions
return Array.isArray(s) ? s.map(String) : []
})
const projectName = computed(() => { const projectName = computed(() => {
const p = projects.value.find(p => p.id === task.value?.project_id) const p = projects.value.find(p => p.id === task.value?.project_id)
return p?.name ?? task.value?.project_id ?? '—' return p?.name ?? task.value?.project_id ?? '—'
@@ -564,6 +632,35 @@ onBeforeUnmount(() => {
/* B-24 漏 white-space 覆盖致 v-html 后 HTML 标签间 \n 被 pre-wrap 渲染为空行间距——移除 pre-wrap,描述由 .ai-md 接管(line-height 1.5 对齐 AiChat li) */ /* B-24 漏 white-space 覆盖致 v-html 后 HTML 标签间 \n 被 pre-wrap 渲染为空行间距——移除 pre-wrap,描述由 .ai-md 接管(line-height 1.5 对齐 AiChat li) */
.info-item .value.description { line-height: 1.5; } .info-item .value.description { line-height: 1.5; }
/* ===== F-AiNodeSelfReview: output_json 区块(产出 + 自审结论卡) ===== */
.output-block .output-body {
display: flex;
flex-direction: column;
gap: 10px;
}
/* 自审结论卡:verdict 红绿标(pass 绿 / fail 红 / unknown 灰),复用全局 --df-success/danger token */
.review-card {
display: flex;
flex-direction: column;
gap: 4px;
padding: 8px 12px;
border-radius: var(--df-radius-sm);
border: 0.5px solid var(--df-border);
font-size: 13px;
}
.review-pass { background: var(--df-success-bg); border-color: rgba(61,219,160,0.3); }
.review-fail { background: var(--df-danger-bg); border-color: rgba(240,101,101,0.3); }
.review-unknown { background: var(--df-bg-card); }
.review-verdict { font-weight: 600; }
.review-pass .review-verdict { color: var(--df-success); }
.review-fail .review-verdict { color: var(--df-danger); }
.review-unknown .review-verdict { color: var(--df-text-dim); }
.review-summary { color: var(--df-text-secondary); line-height: 1.5; }
.review-suggestions { margin: 4px 0 0; padding-left: 20px; color: var(--df-text-secondary); }
.review-suggestions li { line-height: 1.6; }
/* AI 产出正文(继承全局 .ai-md 块级样式,line-height 对齐描述) */
.output-text { line-height: 1.5; }
/* ===== 任务描述 Markdown 渲染(B-24,基础样式收敛至全局 ai-md.css) ===== */ /* ===== 任务描述 Markdown 渲染(B-24,基础样式收敛至全局 ai-md.css) ===== */
.project-link { .project-link {