feat(aihr): switch interview and case flows to real-model-first pipeline

AI 面试 /start 动态出题、/finish 按真实回答结构化评分(未配模型用本地
Rubric 估分);案例沉淀 /upload 改 multipart 真实语音+ASR 转写,
/organize 用真实 transcript 调 chat 模型整理;seed service 重构为
AihrInterviewService/AihrCaseService;前端面试/案例/对练页与移动端、
demo-check、验收文档同步真实链路口径。
This commit is contained in:
2026-07-03 23:45:20 +08:00
parent 4fa23617a4
commit 77aa992872
18 changed files with 1475 additions and 389 deletions
+3 -1
View File
@@ -30,6 +30,7 @@ export type CaseUploadResponse = {
fileName: string;
transcript: string;
tags: CaseTag[];
source: string;
};
export type CaseOrganizeResponse = {
@@ -37,6 +38,7 @@ export type CaseOrganizeResponse = {
summary: CaseSummaryItem[];
tags: CaseTag[];
aiSummary: string;
source: string;
};
export type CaseCurateResponse = {
@@ -45,7 +47,7 @@ export type CaseCurateResponse = {
sampleHint: string;
};
export function uploadCaseAudio(data: { fileName: string; projectExtOrgId: string }): Promise<ApiResult<CaseUploadResponse>> {
export function uploadCaseAudio(data: FormData): Promise<ApiResult<CaseUploadResponse>> {
return request({
url: '/api/knowledge/case/upload',
method: 'post',
+3
View File
@@ -27,6 +27,7 @@ export type InterviewStartResponse = {
candidateName: string;
positionCode: string;
questions: InterviewQuestionResponse[];
source: string;
};
export type InterviewAnswerRequest = {
@@ -44,6 +45,7 @@ export type InterviewAnswerResponse = {
export type InterviewFinishRequest = {
sessionId: string;
answers: Record<string, string>;
};
export type InterviewFinishResponse = {
@@ -65,6 +67,7 @@ export type InterviewFinishResponse = {
result: string;
summary: string;
}>;
source: string;
};
export function startInterview(data: InterviewStartRequest): Promise<ApiResult<InterviewStartResponse>> {
+79 -100
View File
@@ -1,15 +1,16 @@
<template>
<div class="cases-page">
<input ref="audioInput" class="hidden-input" type="file" accept="audio/*,.mp3,.wav,.m4a,.webm,.ogg" @change="handleAudioPicked" />
<section class="page-head">
<div>
<h1>案例语音整理</h1>
<p>语音上传或选择样例录音,完成 ASR 转写、AI 结构化整理、送审入库。</p>
<p>上传真实语音,完成 ASR 转写、AI 结构化整理、送审入库。</p>
</div>
<div class="head-actions">
<span class="status-pill" :class="apiPill.type">{{ apiPill.label }}</span>
<span class="status-pill" :class="flowStatus.type">{{ flowStatus.label }}</span>
<el-button @click="router.push('/index')">返回首页</el-button>
<el-button type="danger" @click="selectSample">选择样例录音</el-button>
<el-button type="danger" @click="audioInput?.click()">上传语音</el-button>
</div>
</section>
@@ -19,18 +20,18 @@
<el-icon><Microphone /></el-icon>
<h2>上传与转写</h2>
</div>
<div class="upload-box" @click="selectSample">
<div class="upload-box" @click="audioInput?.click()">
<el-icon><FolderChecked /></el-icon>
<strong>{{ sampleSelected ? displayFileName : '拖入语音 / 选择样例录音' }}</strong>
<span>{{ sampleSelected ? '已加载样例录音并完成 ASR 转写。' : 'Demo 暂用样例转写文本,ASR 接入后替换。' }}</span>
<strong>{{ audioUploaded ? displayFileName : '选择语音文件' }}</strong>
<span>{{ audioUploaded ? '真实语音已完成 ASR 转写。' : '支持 mp3/wav/m4a/webm/ogg,需先配置 ASR 模型。' }}</span>
</div>
<div v-if="sampleSelected" class="transcript-card">
<div v-if="audioUploaded" class="transcript-card">
<span>ASR 转写结果</span>
<p>{{ displayTranscript }}</p>
</div>
<div v-else class="empty-state">先选择样例录音,页面会展示转写结果和后续整理动作。</div>
<div v-else class="empty-state">先上传真实语音,页面会展示转写结果和后续整理动作。</div>
<div class="action-row">
<el-button type="danger" @click="organizeCase">AI 整理</el-button>
<el-button type="danger" :disabled="!audioUploaded" @click="organizeCase">AI 整理</el-button>
</div>
</article>
@@ -105,74 +106,59 @@ import { curateKnowledgeCase, organizeKnowledgeCase, uploadCaseAudio, type CaseR
const router = useRouter();
const seedCase = {
fileName: '车库积水投诉_20260702.wav',
transcript: '业主反映地下车库长期积水,担心车辆受损。管家先确认车位号与积水时间,安排工程人员排查排水沟,并在业主群同步处理进度。',
tags: [
{ text: '投诉处理', type: 'pill-success' },
{ text: '车库', type: 'pill-primary' },
{ text: '高情绪', type: 'pill-warning' }
],
summary: [
{ label: '背景', text: '雨后车库积水,业主担心车辆与安全问题。' },
{ label: '处理', text: '确认位置、派工排查、群内同步、次日复盘。' },
{ label: '结果', text: '排水沟堵塞已清理,业主确认问题解决。' },
{ label: '亮点', text: '响应快、过程透明、主动同步进度。' }
]
} as const;
const sampleSelected = ref(false);
const audioInput = ref<HTMLInputElement>();
const audioUploaded = ref(false);
const organized = ref(false);
const reviewed = ref(false);
const stored = ref(false);
const sampleVisible = ref(false);
const caseId = ref('');
const apiMode = ref<'seed' | 'api' | 'fallback'>('seed');
const apiMode = ref<'idle' | 'api' | 'error'>('idle');
const apiFileName = ref('');
const apiTranscript = ref('');
const apiSummary = ref<CaseSummaryItem[] | null>(null);
const apiTags = ref<CaseTag[] | null>(null);
const apiSummary = ref<CaseSummaryItem[]>([]);
const apiTags = ref<CaseTag[]>([]);
const apiRecord = ref<CaseRecord | null>(null);
const apiSampleHint = ref('');
const displayFileName = computed(() => apiFileName.value || seedCase.fileName);
const displayTranscript = computed(() => apiTranscript.value || seedCase.transcript);
const displaySummary = computed(() => apiSummary.value ?? seedCase.summary);
const displayTags = computed(() => apiTags.value ?? seedCase.tags);
const displayFileName = computed(() => apiFileName.value || '未选择文件');
const displayTranscript = computed(() => apiTranscript.value || '暂无转写结果');
const displaySummary = computed(() => apiSummary.value);
const displayTags = computed(() => apiTags.value);
const summary = computed(() => (organized.value ? displaySummary.value : []));
const apiPill = computed(() => {
if (apiMode.value === 'api') return { label: '后端 API seed', type: 'pill-primary' };
if (apiMode.value === 'fallback') return { label: 'API 失败回退', type: 'pill-warning' };
return { label: '本地 seed 演示', type: 'pill-primary' };
if (apiMode.value === 'api') return { label: '真实 ASR/API', type: 'pill-primary' };
if (apiMode.value === 'error') return { label: '接口失败', type: 'pill-warning' };
return { label: '待上传语音', type: 'pill-primary' };
});
const flowStatus = computed(() => {
if (stored.value) return { label: '已完成闭环', type: 'pill-success' };
if (reviewed.value) return { label: '已送审待入库', type: 'pill-primary' };
if (organized.value) return { label: '已生成案例稿', type: 'pill-warning' };
if (sampleSelected.value) return { label: '已完成 ASR', type: 'pill-primary' };
return { label: '样片兜底', type: 'pill-warning' };
if (audioUploaded.value) return { label: '已完成 ASR', type: 'pill-primary' };
return { label: '等待语音', type: 'pill-warning' };
});
const pipeline = computed(() => [
{
name: '语音上传',
count: sampleSelected.value ? '1 条' : '0 条',
note: sampleSelected.value ? '样例录音已选择' : '等待上传',
state: sampleSelected.value ? 'done' : 'todo'
count: audioUploaded.value ? '1 条' : '0 条',
note: audioUploaded.value ? '真实音频已上传' : '等待上传',
state: audioUploaded.value ? 'done' : 'todo'
},
{
name: 'ASR 转写',
count: sampleSelected.value ? '1 条' : '0 条',
note: sampleSelected.value ? '本地 seed 转写完成' : '等待语音',
state: sampleSelected.value ? 'done' : 'todo'
count: audioUploaded.value ? '1 条' : '0 条',
note: audioUploaded.value ? '真实 ASR 已转写' : '等待语音',
state: audioUploaded.value ? 'done' : 'todo'
},
{
name: 'AI 整理',
count: organized.value ? '1 条' : '0 条',
note: organized.value ? '结构化案例稿已生成' : '等待整理',
state: organized.value ? 'done' : sampleSelected.value ? 'active' : 'todo'
state: organized.value ? 'done' : audioUploaded.value ? 'active' : 'todo'
},
{
name: '审核入库',
@@ -215,68 +201,64 @@ const baseRecords = [
}
];
const seedRecord = {
title: '车库积水投诉处理 · seed入库',
type: '投诉处理',
status: '已入库',
time: '07-02 10:50',
owner: '培训组',
summary: 'ASR 转写后自动整理为案例稿,完成送审与入库,可用于三角色对练复用。'
};
const records = ref([...baseRecords]);
async function selectSample() {
sampleSelected.value = true;
async function handleAudioPicked(event: Event) {
const input = event.target as HTMLInputElement;
const file = input.files?.[0];
input.value = '';
if (!file) return;
await uploadAudio(file);
}
async function uploadAudio(file: File) {
audioUploaded.value = false;
organized.value = false;
reviewed.value = false;
stored.value = false;
sampleVisible.value = false;
apiSummary.value = null;
apiSummary.value = [];
apiRecord.value = null;
apiSampleHint.value = '';
apiTranscript.value = '';
apiTags.value = [];
try {
const { data } = await uploadCaseAudio({
fileName: seedCase.fileName,
projectExtOrgId: 'seed-project'
});
const form = new FormData();
form.append('file', file);
form.append('projectExtOrgId', 'mobile-upload');
const { data } = await uploadCaseAudio(form);
caseId.value = data.caseId;
apiMode.value = 'api';
apiFileName.value = data.fileName;
apiTranscript.value = data.transcript;
apiTags.value = data.tags;
ElMessage.success('已通过后端 API 加载样例录音并完成 ASR 转写');
audioUploaded.value = true;
ElMessage.success('真实语音已完成 ASR 转写');
} catch {
caseId.value = 'case-garage-water';
apiMode.value = 'fallback';
apiFileName.value = '';
apiTranscript.value = '';
apiTags.value = null;
ElMessage.warning('后端案例上传 API 不可用,已回退本地 seed 转写');
caseId.value = '';
apiMode.value = 'error';
apiFileName.value = file.name;
ElMessage.error('语音转写失败,请检查 ASR 模型配置或音频格式');
}
}
async function organizeCase() {
if (!sampleSelected.value) {
await selectSample();
}
if (!audioUploaded.value || !caseId.value) return;
if (apiMode.value === 'api') {
try {
const { data } = await organizeKnowledgeCase({ caseId: caseId.value });
apiSummary.value = data.summary;
apiTags.value = data.tags;
organized.value = true;
ElMessage.success('结构化案例稿已由后端 API 返回');
ElMessage.success(data.source === 'real-llm' ? '结构化案例稿已由真实模型返回' : '结构化案例稿已按真实转写生成');
return;
} catch {
apiMode.value = 'fallback';
apiSummary.value = null;
apiTags.value = null;
ElMessage.warning('后端案例整理 API 不可用,已回退本地 seed 案例稿');
apiMode.value = 'error';
apiSummary.value = [];
apiTags.value = [];
ElMessage.error('后端案例整理 API 不可用');
}
}
organized.value = true;
ElMessage.success('AI 已生成结构化案例稿');
}
function submitReview() {
@@ -295,31 +277,24 @@ async function storeCase() {
}
reviewed.value = true;
stored.value = true;
if (apiMode.value === 'api') {
try {
const { data } = await curateKnowledgeCase({
caseId: caseId.value,
criteria: '可复用培训案例',
limit: 1
});
apiRecord.value = data.records?.[0] ?? null;
apiSampleHint.value = data.sampleHint;
if (apiRecord.value && !records.value.some((item) => item.title === apiRecord.value?.title)) {
records.value.unshift(apiRecord.value);
}
ElMessage.success('案例已由后端 API 入库,可用于训练复用');
return;
} catch {
apiMode.value = 'fallback';
apiRecord.value = null;
apiSampleHint.value = '';
ElMessage.warning('后端案例入库 API 不可用,已回退本地 seed 入库');
try {
const { data } = await curateKnowledgeCase({
caseId: caseId.value,
criteria: '可复用培训案例',
limit: 1
});
apiRecord.value = data.records?.[0] ?? null;
apiSampleHint.value = data.sampleHint;
if (apiRecord.value && !records.value.some((item) => item.title === apiRecord.value?.title)) {
records.value.unshift(apiRecord.value);
}
ElMessage.success('案例已由后端 API 入库,可用于训练复用');
} catch {
apiMode.value = 'error';
apiRecord.value = null;
apiSampleHint.value = '';
ElMessage.error('后端案例入库 API 不可用');
}
if (!records.value.some((item) => item.title === seedRecord.title)) {
records.value.unshift(seedRecord);
}
ElMessage.success('案例已入库,可用于训练复用');
}
function viewSample() {
@@ -336,6 +311,10 @@ function viewSample() {
color: #162033;
}
.hidden-input {
display: none;
}
.page-head {
display: flex;
align-items: flex-end;
+36 -24
View File
@@ -3,7 +3,7 @@
<section class="page-head">
<div>
<h1>AI 面试出题</h1>
<p>岗位画像驱动出题、候选人作答、Rubric 评分、面试记录沉淀的本地 seed 演示闭环。</p>
<p>岗位画像驱动出题、候选人作答、Rubric 评分、面试记录沉淀。</p>
</div>
<div class="head-actions">
<span class="status-pill" :class="apiPill.className">{{ apiPill.text }}</span>
@@ -79,7 +79,7 @@
<div v-if="!generated" class="empty-state">
<strong>等待生成面试题</strong>
<p>点击左侧“生成题目”,系统会按当前岗位画像加载 3 道 seed 面试题。</p>
<p>点击左侧“生成题目”,系统会按当前岗位画像生成 3 道面试题。</p>
<el-button type="danger" :icon="DocumentChecked" @click="generateQuestions">生成题目</el-button>
</div>
@@ -105,21 +105,21 @@
<div class="answer-box">
<div class="answer-head">
<span>候选人回答 · {{ activeQuestion.scenario }}</span>
<el-button text type="danger" @click="fillSampleAnswer">填入样例回答</el-button>
<el-button text type="danger" @click="fillSampleAnswer">填入参考回答</el-button>
</div>
<el-input
v-model="answers[activeQuestion.id]"
type="textarea"
:rows="6"
resize="none"
placeholder="输入候选人回答,或点击“填入样例回答”快速推进演示"
placeholder="输入候选人真实回答,或点击“填入参考回答”快速推进测试"
/>
</div>
<div class="action-row question-actions">
<el-button @click="moveQuestion(-1)">上一题</el-button>
<el-button @click="moveQuestion(1)">下一题</el-button>
<el-button @click="fillAllSampleAnswers">填满 seed 回答</el-button>
<el-button @click="fillAllSampleAnswers">填满参考回答</el-button>
<el-button @click="saveDraft">保存草稿</el-button>
<el-button type="danger" :disabled="!canScore" @click="finishScore">完成评分</el-button>
</div>
@@ -225,7 +225,7 @@ const candidates: CandidateSeed[] = [
id: 'zhang-ming',
name: '张明',
project: '银城花园 / 住宅',
mode: '文本演示,语音待接入',
mode: '文本面试',
experience: '3 年客服与案场接待',
risk: '催缴冲突经验不足',
job: {
@@ -280,7 +280,7 @@ const candidates: CandidateSeed[] = [
id: 'li-na',
name: '李娜',
project: '铂悦府 / 高端住宅',
mode: '文本演示,语音待接入',
mode: '文本面试',
experience: '2 年呼叫中心经验',
risk: '现场协调经验偏弱',
job: {
@@ -362,13 +362,14 @@ const scored = ref(false);
const showRubric = ref(false);
const activeQuestionId = ref('');
const sessionId = ref('');
const apiMode = ref<'seed' | 'api' | 'fallback'>('seed');
const apiMode = ref<'idle' | 'api' | 'fallback'>('idle');
const apiQuestions = ref<InterviewQuestion[]>([]);
const apiScoreResult = ref<CandidateSeed['scoreResult'] | null>(null);
const answers = reactive<Record<string, string>>({});
const records = ref([...baseRecords]);
const selectedCandidate = computed(() => candidates.find((candidate) => candidate.id === candidateId.value) ?? candidates[0]);
const questions = computed(() => selectedCandidate.value.questions);
const questions = computed(() => (apiQuestions.value.length > 0 ? apiQuestions.value : selectedCandidate.value.questions));
const activeQuestion = computed(() => questions.value.find((question) => question.id === activeQuestionId.value) ?? questions.value[0]);
const scoreResult = computed(() => apiScoreResult.value ?? selectedCandidate.value.scoreResult);
@@ -385,12 +386,12 @@ const answeredCount = computed(() => questions.value.filter((question) => answer
const canScore = computed(() => generated.value && answeredCount.value === questions.value.length);
const apiPill = computed(() => {
if (apiMode.value === 'api') {
return { text: '后端 API seed', className: 'pill-info' };
return { text: '真实 API', className: 'pill-info' };
}
if (apiMode.value === 'fallback') {
return { text: 'API 失败回退', className: 'pill-warning' };
return { text: '本地 Rubric', className: 'pill-warning' };
}
return { text: '本地 seed 演示', className: 'pill-info' };
return { text: '待调用 API', className: 'pill-info' };
});
const flowPill = computed(() => {
if (!generated.value) {
@@ -425,7 +426,8 @@ const resetFlow = () => {
showRubric.value = false;
activeQuestionId.value = '';
sessionId.value = '';
apiMode.value = 'seed';
apiMode.value = 'idle';
apiQuestions.value = [];
apiScoreResult.value = null;
resetAnswers();
records.value = [...baseRecords];
@@ -434,23 +436,33 @@ const resetFlow = () => {
const generateQuestions = async () => {
resetAnswers();
scored.value = false;
apiQuestions.value = [];
apiScoreResult.value = null;
records.value = [...baseRecords];
try {
const { data } = await startInterview({
candidateId: selectedCandidate.value.id,
positionCode: selectedCandidate.value.job.title,
mode: 'seed'
mode: 'real'
});
sessionId.value = data.sessionId;
apiMode.value = 'api';
activeQuestionId.value = data.questions?.[0]?.questionId ?? questions.value[0].id;
ElMessage.success(`已通过后端 API 生成 ${questions.value.length} 道 seed 面试题`);
apiQuestions.value = (data.questions || []).map((question) => ({
id: question.questionId,
seq: question.seq,
text: question.questionText,
point: question.point,
scenario: question.scenario,
sampleAnswer: question.sampleAnswer
}));
activeQuestionId.value = apiQuestions.value[0]?.id ?? questions.value[0].id;
ElMessage.success(data.source === 'real-llm' ? '已由真实模型生成面试题' : '模型未配置,已使用本地 Rubric 题库');
} catch {
sessionId.value = '';
apiMode.value = 'fallback';
apiQuestions.value = [];
activeQuestionId.value = questions.value[0].id;
ElMessage.warning('后端 API 不可用,已回退本地 seed 题库');
ElMessage.warning('后端 API 不可用,已使用本地 Rubric 题库');
}
generated.value = true;
};
@@ -463,7 +475,7 @@ const fillAllSampleAnswers = () => {
questions.value.forEach((question) => {
answers[question.id] = question.sampleAnswer;
});
ElMessage.success('已填入全部 seed 样例回答');
ElMessage.success('已填入全部参考回答');
};
const moveQuestion = (offset: number) => {
@@ -485,11 +497,11 @@ const saveDraft = async () => {
questionId: activeQuestion.value.id,
answerText
});
ElMessage.success('回答已提交后端 seed API');
ElMessage.success('回答已提交后端 API');
return;
} catch {
apiMode.value = 'fallback';
ElMessage.warning('后端暂存 API 不可用,已回退本地草稿');
ElMessage.warning('后端暂存 API 不可用,已保留本地草稿');
}
}
ElMessage.success('草稿已暂存到本地演示状态');
@@ -519,14 +531,14 @@ const finishScore = async () => {
};
if (apiMode.value === 'api' && sessionId.value) {
try {
const { data } = await finishInterview({ sessionId: sessionId.value });
const { data } = await finishInterview({ sessionId: sessionId.value, answers: { ...answers } });
apiScoreResult.value = mapApiScoreResult(data);
nextRecord = data.records?.[0] ?? nextRecord;
ElMessage.success('AI 面试评分已由后端 API 返回');
ElMessage.success(data.source === 'real-llm' ? 'AI 面试评分已由真实模型返回' : 'AI 面试评分已由本地 Rubric 返回');
} catch {
apiMode.value = 'fallback';
apiScoreResult.value = null;
ElMessage.warning('后端评分 API 不可用,已回退本地 seed 评分');
ElMessage.warning('后端评分 API 不可用,已回退本地 Rubric 评分');
}
} else {
apiScoreResult.value = null;
@@ -534,7 +546,7 @@ const finishScore = async () => {
scored.value = true;
records.value = [nextRecord, ...baseRecords];
if (apiMode.value !== 'api') {
ElMessage.success('AI 面试评分已生成,并写入本地演示记录');
ElMessage.success('AI 面试评分已生成,并写入本地记录');
}
};
+13 -13
View File
@@ -87,7 +87,7 @@
<div class="reply-box">
<div class="reply-head">
<span>学员下一句</span>
<el-button text type="danger" :disabled="finished" @click="fillSeedReply">填入 seed 回复</el-button>
<el-button text type="danger" :disabled="finished" @click="fillReferenceReply">填入参考回复</el-button>
</div>
<el-input
v-model="draftReply"
@@ -95,7 +95,7 @@
:rows="5"
resize="none"
:disabled="finished"
placeholder="输入学员回复,或点击“填入 seed 回复”推进演示"
placeholder="输入学员回复,或点击“填入参考回复”推进测试"
/>
</div>
@@ -276,7 +276,7 @@ const showGoal = ref(false);
const activeRound = ref(0);
const draftReply = ref('');
const sessionId = ref('');
const apiMode = ref<'seed' | 'api' | 'fallback'>('seed');
const apiMode = ref<'local' | 'api' | 'fallback'>('local');
const apiScores = ref<ScenarioSeed['scores'] | null>(null);
const apiTotal = ref<number | null>(null);
const apiRewrite = ref('');
@@ -306,12 +306,12 @@ const trustScore = computed(() => {
});
const apiPill = computed(() => {
if (apiMode.value === 'api') {
return { text: '后端 API seed', className: 'pill-info' };
return { text: '真实 API', className: 'pill-info' };
}
if (apiMode.value === 'fallback') {
return { text: 'API 失败回退', className: 'pill-warning' };
return { text: '本地剧本', className: 'pill-warning' };
}
return { text: '本地 seed 演示', className: 'pill-info' };
return { text: '待调用 API', className: 'pill-info' };
});
const flowPill = computed(() => {
if (!started.value) {
@@ -354,7 +354,7 @@ const resetPractice = () => {
activeRound.value = 0;
draftReply.value = '';
sessionId.value = '';
apiMode.value = 'seed';
apiMode.value = 'local';
apiScores.value = null;
apiTotal.value = null;
apiRewrite.value = '';
@@ -371,23 +371,23 @@ const startPractice = async () => {
const { data } = await startPracticeSession({
extPartyId: selectedScenario.value.trainee,
scenarioId: selectedScenario.value.id,
mode: 'seed'
mode: 'real'
});
sessionId.value = data.sessionId;
apiMode.value = 'api';
apiTrust.value = data.trust;
appendTurn('customer', data.customerText || fallbackCustomer);
ElMessage.success(`已通过后端 API 进入 ${selectedScenario.value.name} seed 对练`);
ElMessage.success(`已通过后端 API 进入 ${selectedScenario.value.name} 对练`);
} catch {
sessionId.value = '';
apiMode.value = 'fallback';
appendTurn('customer', fallbackCustomer);
ElMessage.warning('后端对练 API 不可用,已回退本地 seed 对练');
ElMessage.warning('后端对练 API 不可用,已回退本地剧本');
}
started.value = true;
};
const fillSeedReply = () => {
const fillReferenceReply = () => {
draftReply.value = selectedScenario.value.rounds[activeRound.value].sampleReply;
};
@@ -420,7 +420,7 @@ const continueRound = async () => {
return;
} catch {
apiMode.value = 'fallback';
ElMessage.warning('后端回合 API 不可用,已回退本地 seed 推进');
ElMessage.warning('后端回合 API 不可用,已回退本地剧本推进');
}
}
@@ -460,7 +460,7 @@ const finishScore = async () => {
apiTotal.value = null;
apiRewrite.value = '';
apiSummary.value = '';
ElMessage.warning('后端评分 API 不可用,已回退本地 seed 评分');
ElMessage.warning('后端评分 API 不可用,已回退本地评分');
}
} else {
apiScores.value = null;