修复: 全栈审查 FR-C1~C5 五项正确性 bug

- FR-C1 formattedEvents 时间漂移:事件入数组固 _ts 时间戳(project.ts push + ProjectDetail 用 _ts),computed 重算不再刷历史时间为当前时刻
- FR-C2 sentiment 三档统一:模板 >0/<0/===0 + sentimentClass 同源 + i18n neutral(zh/en),原模板>0 与 class>=0 矛盾
- FR-C3 Settings timer 泄漏:onUnmounted 清全部三个(原仅 _concurrencyTimer)
- FR-C4 MIGRATION_VERSION 死常量:删(零代码引用,run() if 链自管版本)
- FR-C5 AiConversationDetail 缺 readonly:加 readonly?: boolean(后端 generating 时返回)
来源 fullstack-review §3(已核实);cargo/vue-tsc 0 err
This commit is contained in:
2026-06-14 17:00:33 +08:00
parent 3e1f119003
commit cf18678634
8 changed files with 18 additions and 11 deletions

View File

@@ -3,9 +3,6 @@
use anyhow::Result; use anyhow::Result;
use rusqlite::Connection; use rusqlite::Connection;
/// 当前迁移版本
const MIGRATION_VERSION: i32 = 12;
/// 检测表是否存在指定列(幂等迁移通用辅助) /// 检测表是否存在指定列(幂等迁移通用辅助)
fn column_exists(conn: &Connection, table: &str, col: &str) -> bool { fn column_exists(conn: &Connection, table: &str, col: &str) -> bool {
let Ok(mut stmt) = conn.prepare(&format!("PRAGMA table_info({table})")) else { return false }; let Ok(mut stmt) = conn.prepare(&format!("PRAGMA table_info({table})")) else { return false };

View File

@@ -243,6 +243,8 @@ export interface AiConversationDetail {
id: string id: string
title: string | null title: string | null
messages: string // JSON string of ChatMessage[] messages: string // JSON string of ChatMessage[]
/** 生成中返回 true(后端 ai_conversation_switch 在 generating 时置),前端据此禁编辑(FR-C5) */
readonly?: boolean
} }
/** 技能信息(本机 Claude skill供 `/` 联想 + 注入) */ /** 技能信息(本机 Claude skill供 `/` 联想 + 注入) */

View File

@@ -28,6 +28,7 @@ export default {
netSentiment: 'Overall sentiment: {tone} ({n})', netSentiment: 'Overall sentiment: {tone} ({n})',
sentimentPositive: 'Positive', sentimentPositive: 'Positive',
sentimentNegative: 'Cautious', sentimentNegative: 'Cautious',
sentimentNeutral: 'Neutral',
actionTitle: '💡 Action Items', actionTitle: '💡 Action Items',
evaluating: '⏳ Evaluating…', evaluating: '⏳ Evaluating…',
startEval: '🔍 Start Adversarial Eval', startEval: '🔍 Start Adversarial Eval',

View File

@@ -28,6 +28,7 @@ export default {
netSentiment: '整体倾向: {tone} ({n})', netSentiment: '整体倾向: {tone} ({n})',
sentimentPositive: '积极', sentimentPositive: '积极',
sentimentNegative: '谨慎', sentimentNegative: '谨慎',
sentimentNeutral: '中性',
actionTitle: '💡 行动建议', actionTitle: '💡 行动建议',
evaluating: '⏳ 评估中…', evaluating: '⏳ 评估中…',
startEval: '🔍 开始对抗评估', startEval: '🔍 开始对抗评估',

View File

@@ -11,7 +11,7 @@ const state = reactive({
tasks: [] as TaskRecord[], tasks: [] as TaskRecord[],
ideas: [] as IdeaRecord[], ideas: [] as IdeaRecord[],
workflowExecutions: [] as WorkflowRecord[], workflowExecutions: [] as WorkflowRecord[],
liveEvents: [] as WorkflowEventPayload[], liveEvents: [] as Array<WorkflowEventPayload & { _ts: number }>,
pendingApproval: null as { pendingApproval: null as {
execution_id: string execution_id: string
node_id: string node_id: string
@@ -209,7 +209,8 @@ function createStore() {
if (_eventUnlisten) return _eventUnlisten if (_eventUnlisten) return _eventUnlisten
_eventUnlisten = await workflowApi.onEvent((payload) => { _eventUnlisten = await workflowApi.onEvent((payload) => {
try { try {
state.liveEvents.push(payload) // 固时间戳(FR-C1):formattedEvents computed 重算时用事件发生时刻,非当前时刻
state.liveEvents.push({ ...payload, _ts: Date.now() })
// 处理人工审批请求 // 处理人工审批请求
// WorkflowEvent serde tag=type rename_all=snake_case → type='human_approval_request',字段扁平(无 data 包装)(B-03b-R7) // WorkflowEvent serde tag=type rename_all=snake_case → type='human_approval_request',字段扁平(无 data 包装)(B-03b-R7)
if (payload.event?.type === 'human_approval_request') { if (payload.event?.type === 'human_approval_request') {

View File

@@ -100,7 +100,7 @@
</div> </div>
<p class="final-score">{{ $t('ideas.finalScore', { score: adversarialEval.final_score.toFixed(1) }) }}</p> <p class="final-score">{{ $t('ideas.finalScore', { score: adversarialEval.final_score.toFixed(1) }) }}</p>
<div class="net-sentiment" :class="sentimentClass(adversarialEval.net_sentiment)"> <div class="net-sentiment" :class="sentimentClass(adversarialEval.net_sentiment)">
{{ $t('ideas.netSentiment', { tone: adversarialEval.net_sentiment > 0 ? $t('ideas.sentimentPositive') : $t('ideas.sentimentNegative'), n: (adversarialEval.net_sentiment * 100).toFixed(0) }) }} {{ $t('ideas.netSentiment', { tone: adversarialEval.net_sentiment > 0 ? $t('ideas.sentimentPositive') : (adversarialEval.net_sentiment < 0 ? $t('ideas.sentimentNegative') : $t('ideas.sentimentNeutral')), n: (adversarialEval.net_sentiment * 100).toFixed(0) }) }}
</div> </div>
<p class="summary">{{ adversarialEval.analyst.summary }}</p> <p class="summary">{{ adversarialEval.analyst.summary }}</p>
</div> </div>
@@ -366,7 +366,10 @@ function assessmentLabel(recommendation: string) {
} }
function sentimentClass(sentiment: number) { function sentimentClass(sentiment: number) {
return sentiment >= 0 ? 'positive' : 'negative' // 统一三档(FR-C2: 原 >=0 与模板 >0 矛盾net_sentiment=0 时文案负面样式 positive)
if (sentiment > 0) return 'positive'
if (sentiment < 0) return 'negative'
return 'neutral'
} }
function scoreClass(score: number | null) { function scoreClass(score: number | null) {

View File

@@ -212,7 +212,6 @@ import { formatDate } from '../utils/time'
import { parseStack } from '../utils/project' import { parseStack } from '../utils/project'
import { projectStatusLabel, projectStageInfo, taskStatusLabel, taskStatusClass } from '../constants/project' import { projectStatusLabel, projectStageInfo, taskStatusLabel, taskStatusClass } from '../constants/project'
import ConfirmDialog from '../components/ConfirmDialog.vue' import ConfirmDialog from '../components/ConfirmDialog.vue'
import type { WorkflowEventPayload } from '../api/types'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
@@ -296,12 +295,13 @@ interface LogEntry {
} }
const formattedEvents = computed<LogEntry[]>(() => { const formattedEvents = computed<LogEntry[]>(() => {
return store.liveEvents.map((evt: WorkflowEventPayload) => { return store.liveEvents.map((evt) => {
const ev = evt.event const ev = evt.event
const type = ev.type ?? 'unknown' const type = ev.type ?? 'unknown'
const level = type.includes('error') || type.includes('fail') ? 'error' const level = type.includes('error') || type.includes('fail') ? 'error'
: type.includes('warn') ? 'warn' : 'info' : type.includes('warn') ? 'warn' : 'info'
const time = new Date().toLocaleTimeString(locale.value, { hour: '2-digit', minute: '2-digit', second: '2-digit' }) // 用事件入数组时固化的时间戳(FR-C1),非 computed 重算的当前时刻
const time = new Date(evt._ts).toLocaleTimeString(locale.value, { hour: '2-digit', minute: '2-digit', second: '2-digit' })
const message = ev.label const message = ev.label
? `[${ev.label}] ${type}: ${ev.output ?? ev.code ?? ev.message ?? JSON.stringify(ev)}` ? `[${ev.label}] ${type}: ${ev.output ?? ev.code ?? ev.message ?? JSON.stringify(ev)}`
: `${type}: ${ev.output ?? ev.code ?? ev.message ?? JSON.stringify(ev)}` : `${type}: ${ev.output ?? ev.code ?? ev.message ?? JSON.stringify(ev)}`

View File

@@ -745,9 +745,11 @@ onMounted(() => {
syncConcurrencyConfig() syncConcurrencyConfig()
}) })
// 组件卸载清 debounce timer,防 unmount 后旧 timer 仍 fire 发无效 IPC(timer 泄漏) // 组件卸载清全部 debounce timer,防 unmount 后旧 timer 仍 fire 写已销毁 reactive(timer 泄漏 FR-C3)
onUnmounted(() => { onUnmounted(() => {
if (_concurrencyTimer) clearTimeout(_concurrencyTimer) if (_concurrencyTimer) clearTimeout(_concurrencyTimer)
if (_knowledgeSaveTimer) clearTimeout(_knowledgeSaveTimer)
if (_toastTimer) clearTimeout(_toastTimer)
}) })
</script> </script>