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:
parent
96096ea0ff
commit
878481eef3
|
|
@ -1757,8 +1757,8 @@ class SimulationRunner:
|
||||||
)
|
)
|
||||||
results.extend(platform_results)
|
results.extend(platform_results)
|
||||||
|
|
||||||
# 按时间降序排序
|
# 按时间降序排序(不同平台 created_at 可能是 int 或 datetime 字符串,统一转字符串避免类型比较报错)
|
||||||
results.sort(key=lambda x: x.get("timestamp", ""), reverse=True)
|
results.sort(key=lambda x: str(x.get("timestamp", "")), reverse=True)
|
||||||
|
|
||||||
# 如果查询了多个平台,限制总数
|
# 如果查询了多个平台,限制总数
|
||||||
if len(platforms) > 1 and len(results) > limit:
|
if len(platforms) > 1 and len(results) > limit:
|
||||||
|
|
|
||||||
|
|
@ -176,6 +176,18 @@ export const interviewAgents = (data) => {
|
||||||
return requestWithRetry(() => service.post('/api/simulation/interview/batch', data), 3, 1000)
|
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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取历史模拟列表(带项目详情)
|
* 获取历史模拟列表(带项目详情)
|
||||||
* 用于首页历史项目展示
|
* 用于首页历史项目展示
|
||||||
|
|
|
||||||
|
|
@ -414,7 +414,7 @@
|
||||||
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
|
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { chatWithReport, getReport, getAgentLog } from '../api/report'
|
import { chatWithReport, getReport, getAgentLog } from '../api/report'
|
||||||
import { interviewAgents, getSimulationProfilesRealtime } from '../api/simulation'
|
import { interviewAgents, getSimulationProfilesRealtime, getInterviewHistory } from '../api/simulation'
|
||||||
|
|
||||||
const { t } = useI18n()
|
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
|
// Click outside to close dropdown
|
||||||
const handleClickOutside = (e) => {
|
const handleClickOutside = (e) => {
|
||||||
const dropdown = document.querySelector('.agent-dropdown')
|
const dropdown = document.querySelector('.agent-dropdown')
|
||||||
|
|
@ -941,6 +995,7 @@ onMounted(() => {
|
||||||
addLog(t('log.step5Init'))
|
addLog(t('log.step5Init'))
|
||||||
loadReportData()
|
loadReportData()
|
||||||
loadProfiles()
|
loadProfiles()
|
||||||
|
loadInterviewHistory()
|
||||||
document.addEventListener('click', handleClickOutside)
|
document.addEventListener('click', handleClickOutside)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue