fix(interview): repair interview-history endpoint and surface past interviews in UI

The interview-history feature already existed (interviews are recorded to each
platform's OASIS sqlite DB and `POST /api/simulation/interview/history` reads
them) but was broken on two fronts:

1. Backend crash: get_interview_history sorts merged twitter+reddit results by
   "timestamp", but the two platforms store created_at with different types
   (one int-like, one datetime string), raising
   "'<' not supported between instances of 'int' and 'str'". Coerce the sort
   key to str so mixed-type timestamps sort safely.

2. Step 5 (Interaction) never loaded that history, so past interviews vanished
   on reload. Add getInterviewHistory() and load it on mount, seeding the
   per-agent chat cache (chronological, prompt-prefix stripped, deduped across
   platforms) so selecting an agent shows prior Q&A.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
FeelRatchanons 2026-06-13 14:28:29 +07:00
parent 96096ea0ff
commit 878481eef3
3 changed files with 70 additions and 3 deletions

View File

@ -1757,8 +1757,8 @@ class SimulationRunner:
)
results.extend(platform_results)
# 按时间降序排序
results.sort(key=lambda x: x.get("timestamp", ""), reverse=True)
# 按时间降序排序(不同平台 created_at 可能是 int 或 datetime 字符串,统一转字符串避免类型比较报错)
results.sort(key=lambda x: str(x.get("timestamp", "")), reverse=True)
# 如果查询了多个平台,限制总数
if len(platforms) > 1 and len(results) > limit:

View File

@ -176,6 +176,18 @@ export const interviewAgents = (data) => {
return requestWithRetry(() => service.post('/api/simulation/interview/batch', data), 3, 1000)
}
/**
* 获取某个模拟的采访历史持久化记录 OASIS 数据库读取
* @param {string} simulationId
* @param {Object} opts - { platform?, agent_id?, limit? }
*/
export const getInterviewHistory = (simulationId, opts = {}) => {
return service.post('/api/simulation/interview/history', {
simulation_id: simulationId,
...opts
})
}
/**
* 获取历史模拟列表带项目详情
* 用于首页历史项目展示

View File

@ -414,7 +414,7 @@
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
import { useI18n } from 'vue-i18n'
import { chatWithReport, getReport, getAgentLog } from '../api/report'
import { interviewAgents, getSimulationProfilesRealtime } from '../api/simulation'
import { interviewAgents, getSimulationProfilesRealtime, getInterviewHistory } from '../api/simulation'
const { t } = useI18n()
@ -928,6 +928,60 @@ const loadProfiles = async () => {
}
}
// optimize_interview_prompt
const stripInterviewPrefix = (p) => {
if (!p) return ''
for (const marker of ['我的问题是:', '我的问题是:', '问题:', '问题:']) {
const idx = p.indexOf(marker)
if (idx >= 0) return p.slice(idx + marker.length).trim()
}
return p
}
// 访 OASIS agent
const loadInterviewHistory = async () => {
if (!props.simulationId) return
try {
const res = await getInterviewHistory(props.simulationId, { limit: 200 })
// { history: [{ agent_id, prompt, response, platform, timestamp }] }
const records = res.success && res.data ? (res.data.history || []) : []
if (!records.length) return
// agent_id (+)
const byAgent = {}
records
.slice()
.sort((a, b) => String(a.timestamp).localeCompare(String(b.timestamp)))
.forEach(rec => {
const aid = rec.agent_id
if (!(aid in byAgent)) byAgent[aid] = { seen: new Set(), msgs: [] }
const prompt = stripInterviewPrefix(rec.prompt || '')
const answer = typeof rec.response === 'string' ? rec.response : JSON.stringify(rec.response)
const dedupeKey = prompt + '|' + answer
if (byAgent[aid].seen.has(dedupeKey)) return
byAgent[aid].seen.add(dedupeKey)
byAgent[aid].msgs.push(
{ role: 'user', content: prompt, timestamp: rec.timestamp },
{ role: 'assistant', content: answer, timestamp: rec.timestamp }
)
})
//
Object.keys(byAgent).forEach(aid => {
const key = `agent_${aid}`
chatHistoryCache.value[key] = [...byAgent[aid].msgs, ...(chatHistoryCache.value[key] || [])]
})
// Agent
if (chatTarget.value !== 'report_agent' && selectedAgentIndex.value != null) {
chatHistory.value = chatHistoryCache.value[`agent_${selectedAgentIndex.value}`] || []
}
addLog(`Loaded ${records.length} interview record(s) from history`)
} catch (err) {
console.warn('加载采访历史失败:', err)
}
}
// Click outside to close dropdown
const handleClickOutside = (e) => {
const dropdown = document.querySelector('.agent-dropdown')
@ -941,6 +995,7 @@ onMounted(() => {
addLog(t('log.step5Init'))
loadReportData()
loadProfiles()
loadInterviewHistory()
document.addEventListener('click', handleClickOutside)
})