mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-23 23:53:55 +08:00
feat: improve agent snapshot history
This commit is contained in:
@@ -8,6 +8,13 @@
|
||||
@open="open"
|
||||
@close="close"
|
||||
>
|
||||
<template slot="title">
|
||||
<div class="snapshot-dialog-title">
|
||||
<i class="el-icon-time snapshot-title-icon" aria-hidden="true"></i>
|
||||
<span>{{ $t('agentSnapshot.title') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="snapshots"
|
||||
@@ -116,6 +123,13 @@
|
||||
width="860px"
|
||||
class="snapshot-detail-dialog"
|
||||
>
|
||||
<template slot="title">
|
||||
<div class="snapshot-dialog-title">
|
||||
<i class="el-icon-time snapshot-title-icon" aria-hidden="true"></i>
|
||||
<span>{{ detailDialogTitle }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-loading="detailLoading" class="snapshot-diff">
|
||||
<template v-if="currentSnapshot && snapshotDiffs.length">
|
||||
<div class="detail-summary">
|
||||
@@ -289,6 +303,13 @@
|
||||
width="860px"
|
||||
class="snapshot-detail-dialog"
|
||||
>
|
||||
<template slot="title">
|
||||
<div class="snapshot-dialog-title">
|
||||
<i class="el-icon-time snapshot-title-icon" aria-hidden="true"></i>
|
||||
<span>{{ restorePreviewTitle }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-loading="restorePreviewLoading" class="snapshot-diff">
|
||||
<template v-if="restorePreviewSnapshot && restorePreviewDiffs.length">
|
||||
<div class="detail-summary">
|
||||
@@ -418,17 +439,23 @@
|
||||
:title="$t('agentSnapshot.restoreMemoryWarning')"
|
||||
/>
|
||||
</div>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="restorePreviewVisible = false">
|
||||
<span slot="footer" class="snapshot-dialog-footer">
|
||||
<el-button
|
||||
class="snapshot-footer-button snapshot-footer-cancel"
|
||||
@click="restorePreviewVisible = false"
|
||||
>
|
||||
{{ $t('button.cancel') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
class="snapshot-footer-button snapshot-footer-confirm"
|
||||
:loading="restoring"
|
||||
:disabled="restorePreviewLoading || !restorePreviewSnapshot || restorePreviewDiffs.length === 0"
|
||||
@click="confirmRestoreSnapshot"
|
||||
>
|
||||
{{ $t('agentSnapshot.confirmRestore') }}
|
||||
<span class="confirm-inner">
|
||||
<i class="el-icon-refresh-left confirm-icon" aria-hidden="true"></i>
|
||||
{{ $t('agentSnapshot.confirmRestore') }}
|
||||
</span>
|
||||
</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
@@ -437,6 +464,7 @@
|
||||
|
||||
<script>
|
||||
import Api from "@/apis/api";
|
||||
import correctWord from "@/apis/module/correctWord";
|
||||
import { formatDate } from "@/utils/date";
|
||||
|
||||
const FALLBACK_PLUGIN_NAME_KEYS = {
|
||||
@@ -545,6 +573,9 @@ export default {
|
||||
voiceNameMap: {},
|
||||
voiceMetadataLoading: {},
|
||||
loadedVoiceModelIds: {},
|
||||
correctWordNameMap: {},
|
||||
correctWordMetadataLoaded: false,
|
||||
correctWordMetadataLoading: null,
|
||||
snapshotFetchSeq: 0,
|
||||
detailFetchSeq: 0,
|
||||
restorePreviewFetchSeq: 0,
|
||||
@@ -598,12 +629,13 @@ export default {
|
||||
|
||||
const beforeData = this.currentSnapshot.snapshotData || {};
|
||||
const afterData = this.currentSnapshot.afterSnapshotData || {};
|
||||
return this.buildDiffs(beforeData, afterData, this.currentSnapshot.changedFields || [], {
|
||||
return this.buildDiffs(beforeData, afterData, this.currentSnapshot.changedFields, {
|
||||
beforeLabel: this.$t("agentSnapshot.beforeChange"),
|
||||
afterLabel: this.$t("agentSnapshot.afterChange"),
|
||||
beforeVersionNo: this.currentSnapshot.beforeVersionNo,
|
||||
afterVersionNo: this.currentSnapshot.afterVersionNo || this.currentSnapshot.versionNo,
|
||||
fieldOrder: this.currentSnapshot.fieldOrder
|
||||
fieldOrder: this.currentSnapshot.fieldOrder,
|
||||
forceCompare: Boolean(this.currentSnapshot.forceCompare)
|
||||
});
|
||||
},
|
||||
restorePreviewDiffs() {
|
||||
@@ -620,7 +652,8 @@ export default {
|
||||
afterLabel: this.$t("agentSnapshot.afterRestore"),
|
||||
beforeVersionNo: this.restorePreviewSnapshot.beforeVersionNo || this.resolveCurrentVersionNo(),
|
||||
afterVersionNo: this.restorePreviewSnapshot.afterVersionNo || this.restorePreviewSnapshot.versionNo,
|
||||
fieldOrder: this.restorePreviewSnapshot.fieldOrder
|
||||
fieldOrder: this.restorePreviewSnapshot.fieldOrder,
|
||||
forceCompare: true
|
||||
}
|
||||
);
|
||||
},
|
||||
@@ -668,7 +701,13 @@ export default {
|
||||
});
|
||||
},
|
||||
buildDiffs(beforeData, afterData, changedFields, options = {}) {
|
||||
const fields = this.resolveDiffFields(beforeData, afterData, changedFields, options.fieldOrder);
|
||||
const fields = this.resolveDiffFields(
|
||||
beforeData,
|
||||
afterData,
|
||||
changedFields,
|
||||
options.fieldOrder,
|
||||
options.forceCompare
|
||||
);
|
||||
return fields.map((field) => {
|
||||
const beforeValue = this.getFieldValue(beforeData, field);
|
||||
const afterValue = this.getFieldValue(afterData, field);
|
||||
@@ -1023,6 +1062,7 @@ export default {
|
||||
...selectedSnapshot,
|
||||
versionNo: displayVersionNo,
|
||||
changedFields: adjacentVersion ? (selectedSnapshot.changedFields || row.changedFields || []) : [],
|
||||
forceCompare: !adjacentVersion,
|
||||
snapshotData: previousSnapshot.snapshotData || {},
|
||||
afterSnapshotData: selectedSnapshot.snapshotData || {},
|
||||
beforeVersionNo: previousSnapshot.versionNo,
|
||||
@@ -1210,9 +1250,44 @@ export default {
|
||||
const afterData = snapshot.afterSnapshotData || {};
|
||||
return Promise.all([
|
||||
this.ensureModelMetadata(),
|
||||
this.ensureVoiceMetadataForData(beforeData, afterData)
|
||||
this.ensureVoiceMetadataForData(beforeData, afterData),
|
||||
this.ensureCorrectWordMetadataForData(beforeData, afterData)
|
||||
]);
|
||||
},
|
||||
ensureCorrectWordMetadataForData(...dataList) {
|
||||
const hasCorrectWordIds = dataList.some((data) => {
|
||||
return Array.isArray(data?.correctWordFileIds) && data.correctWordFileIds.length > 0;
|
||||
});
|
||||
return hasCorrectWordIds ? this.ensureCorrectWordMetadata() : Promise.resolve();
|
||||
},
|
||||
ensureCorrectWordMetadata() {
|
||||
if (this.correctWordMetadataLoaded) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (this.correctWordMetadataLoading) {
|
||||
return this.correctWordMetadataLoading;
|
||||
}
|
||||
|
||||
this.correctWordMetadataLoading = new Promise((resolve) => {
|
||||
correctWord.selectAll(({ data }) => {
|
||||
if (data.code === 0) {
|
||||
const metadata = {};
|
||||
(data.data || []).forEach((item) => {
|
||||
if (item.id) {
|
||||
metadata[item.id] = item.fileName || item.id;
|
||||
}
|
||||
});
|
||||
this.correctWordNameMap = metadata;
|
||||
this.correctWordMetadataLoaded = true;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
}).finally(() => {
|
||||
this.correctWordMetadataLoading = null;
|
||||
});
|
||||
|
||||
return this.correctWordMetadataLoading;
|
||||
},
|
||||
ensureVoiceMetadataForData(...dataList) {
|
||||
const modelIds = Array.from(new Set(
|
||||
dataList
|
||||
@@ -1661,17 +1736,21 @@ export default {
|
||||
? `${this.$t("agentSnapshot.restoreFailedRollback")}: ${data.msg}`
|
||||
: this.$t("agentSnapshot.restoreFailedRollback");
|
||||
},
|
||||
resolveDiffFields(beforeData, afterData, changedFields = [], fieldOrder = []) {
|
||||
const safeChangedFields = Array.isArray(changedFields) ? changedFields : [];
|
||||
resolveDiffFields(beforeData, afterData, changedFields = null, fieldOrder = [], forceCompare = false) {
|
||||
const hasBackendChangedFields = Array.isArray(changedFields);
|
||||
const safeChangedFields = hasBackendChangedFields ? changedFields : [];
|
||||
const directFields = safeChangedFields
|
||||
.filter((field) => field && field !== "restore")
|
||||
.map((field) => this.canonicalField(field));
|
||||
const candidates = directFields.length > 0 && !safeChangedFields.includes("restore")
|
||||
? directFields
|
||||
: this.snapshotFieldOrder(fieldOrder, beforeData, afterData);
|
||||
if (!forceCompare && hasBackendChangedFields) {
|
||||
return Array.from(new Set(directFields));
|
||||
}
|
||||
|
||||
const candidates = this.snapshotFieldOrder(fieldOrder, beforeData, afterData);
|
||||
|
||||
return Array.from(new Set(candidates)).filter((field) => {
|
||||
return !this.isSameValue(
|
||||
return !this.isSameFieldValue(
|
||||
field,
|
||||
this.getFieldValue(beforeData, field),
|
||||
this.getFieldValue(afterData, field)
|
||||
);
|
||||
@@ -1706,6 +1785,52 @@ export default {
|
||||
isSameValue(left, right) {
|
||||
return this.isEquivalentValue(this.normalizeValue(left), this.normalizeValue(right));
|
||||
},
|
||||
isSameFieldValue(field, left, right) {
|
||||
return this.isEquivalentValue(
|
||||
this.normalizeValueForField(field, left),
|
||||
this.normalizeValueForField(field, right)
|
||||
);
|
||||
},
|
||||
normalizeValueForField(field, value) {
|
||||
if (field === "functions") {
|
||||
return this.normalizeFunctionMap(value);
|
||||
}
|
||||
if (field === "contextProviders") {
|
||||
return this.normalizeSortedJsonList(value);
|
||||
}
|
||||
if (["ttsVolume", "ttsRate", "ttsPitch"].includes(field)) {
|
||||
return this.normalizeDefaultTtsNumber(value);
|
||||
}
|
||||
if (field === "summaryMemory") {
|
||||
return value === null || value === undefined || String(value).trim() === "" ? "" : String(value);
|
||||
}
|
||||
if (field === "correctWordFileIds" || field === "tagNames") {
|
||||
return Array.isArray(value)
|
||||
? value.filter((item) => item !== null && item !== undefined && String(item).trim() !== "")
|
||||
.map((item) => String(item))
|
||||
.sort()
|
||||
: [];
|
||||
}
|
||||
return this.normalizeValue(value);
|
||||
},
|
||||
normalizeSortedJsonList(value) {
|
||||
if (!Array.isArray(value)) {
|
||||
return [];
|
||||
}
|
||||
return value
|
||||
.map((item) => this.normalizeValue(item))
|
||||
.sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
|
||||
},
|
||||
normalizeDefaultTtsNumber(value) {
|
||||
if (value === null || value === undefined || String(value).trim() === "") {
|
||||
return 0;
|
||||
}
|
||||
if (typeof value === "number") {
|
||||
return Math.trunc(value);
|
||||
}
|
||||
const text = String(value).trim();
|
||||
return /^[+-]?\d+$/.test(text) ? Number.parseInt(text, 10) : text;
|
||||
},
|
||||
isEquivalentValue(left, right) {
|
||||
if (left === SNAPSHOT_SECRET_REDACTED || right === SNAPSHOT_SECRET_REDACTED) {
|
||||
return true;
|
||||
@@ -1754,6 +1879,9 @@ export default {
|
||||
return this.translateKey(CHAT_HISTORY_CONF_LABEL_KEYS[value] || CHAT_HISTORY_CONF_LABEL_KEYS[Number(value)])
|
||||
|| this.formatValue(value);
|
||||
}
|
||||
if (field === "correctWordFileIds") {
|
||||
return this.correctWordDisplayNames(value);
|
||||
}
|
||||
return this.formatValue(value);
|
||||
},
|
||||
modelDisplayName(value) {
|
||||
@@ -1769,6 +1897,14 @@ export default {
|
||||
return this.voiceNameMap[`${modelId}:${value}`] || this.voiceNameMap[value]
|
||||
|| this.translateKey(FALLBACK_VOICE_NAME_KEYS[value], String(value)) || String(value);
|
||||
},
|
||||
correctWordDisplayNames(value) {
|
||||
if (!Array.isArray(value) || value.length === 0) {
|
||||
return this.$t("agentSnapshot.emptyValue");
|
||||
}
|
||||
return value
|
||||
.map((id) => this.correctWordNameMap[id] || id)
|
||||
.join(", ");
|
||||
},
|
||||
formatValue(value) {
|
||||
if (value === null || value === undefined || value === "") {
|
||||
return this.$t("agentSnapshot.emptyValue");
|
||||
@@ -1846,6 +1982,59 @@ export default {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.snapshot-dialog-title {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 18px;
|
||||
|
||||
> span {
|
||||
line-height: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.snapshot-title-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 8px;
|
||||
color: #4a7cfd;
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog__headerbtn {
|
||||
top: 12px;
|
||||
right: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
|
||||
|
||||
.el-dialog__close {
|
||||
position: static;
|
||||
color: #666;
|
||||
font-size: 18px;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
|
||||
|
||||
.el-dialog__close {
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep .el-dialog__body {
|
||||
padding: 20px;
|
||||
}
|
||||
@@ -1854,6 +2043,81 @@ export default {
|
||||
padding: 12px 20px 16px;
|
||||
}
|
||||
|
||||
.snapshot-dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.snapshot-footer-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 96px;
|
||||
height: 36px;
|
||||
padding: 10px 20px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
& + .snapshot-footer-button {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.snapshot-footer-cancel {
|
||||
border: 1px solid #d7e1ff;
|
||||
background: #f7f9ff;
|
||||
color: #4a7cfd;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
border-color: #4a7cfd;
|
||||
background: #eef4ff;
|
||||
color: #4a7cfd;
|
||||
box-shadow: 0 2px 8px rgba(74, 124, 253, 0.14);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
.snapshot-footer-confirm {
|
||||
border: none;
|
||||
background: linear-gradient(to right, #4a7cfd, #8154fc);
|
||||
color: #fff;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
border-color: transparent;
|
||||
background: linear-gradient(to right, #4a7cfd, #8154fc);
|
||||
color: #fff;
|
||||
box-shadow: 0 2px 8px rgba(74, 124, 253, 0.3);
|
||||
transform: translateY(-2px);
|
||||
opacity: 0.88;
|
||||
}
|
||||
|
||||
&.is-disabled,
|
||||
&.is-disabled:hover,
|
||||
&.is-disabled:focus {
|
||||
border-color: transparent;
|
||||
background: linear-gradient(to right, #a0b4fd, #b8a4fd);
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
box-shadow: none;
|
||||
transform: none;
|
||||
cursor: not-allowed;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.confirm-inner {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.confirm-icon {
|
||||
margin-right: 5px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.snapshot-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
:step="1"
|
||||
:format-tooltip="formatTooltip"
|
||||
class="tts-slider"
|
||||
@change="markTtsSettingChanged('volume')"
|
||||
/>
|
||||
<span class="slider-hint">{{ $t('roleConfig.volumeHint') }}</span>
|
||||
</div>
|
||||
@@ -40,6 +41,7 @@
|
||||
:step="1"
|
||||
:format-tooltip="formatTooltip"
|
||||
class="tts-slider"
|
||||
@change="markTtsSettingChanged('speed')"
|
||||
/>
|
||||
<span class="slider-hint">{{ $t('roleConfig.speedHint') }}</span>
|
||||
</div>
|
||||
@@ -55,6 +57,7 @@
|
||||
:step="1"
|
||||
:format-tooltip="formatTooltip"
|
||||
class="tts-slider"
|
||||
@change="markTtsSettingChanged('pitch')"
|
||||
/>
|
||||
<span class="slider-hint">{{ $t('roleConfig.pitchHint') }}</span>
|
||||
</div>
|
||||
@@ -122,6 +125,11 @@ export default {
|
||||
speed: 0,
|
||||
pitch: 0,
|
||||
},
|
||||
changedTtsFields: {
|
||||
volume: false,
|
||||
speed: false,
|
||||
pitch: false,
|
||||
},
|
||||
replacementWordIds: [],
|
||||
replacementWordList: []
|
||||
};
|
||||
@@ -141,6 +149,11 @@ export default {
|
||||
if (newVal) {
|
||||
// 当抽屉打开时,复制当前设置到本地
|
||||
this.localSettings = { ...this.settings };
|
||||
this.changedTtsFields = {
|
||||
volume: false,
|
||||
speed: false,
|
||||
pitch: false,
|
||||
};
|
||||
this.replacementWordIds = [...this.checkedReplacementWordIds];
|
||||
this.fetchReplacementWordList();
|
||||
}
|
||||
@@ -156,9 +169,18 @@ export default {
|
||||
},
|
||||
handleSave() {
|
||||
// 保存设置并关闭
|
||||
this.$emit('save', { ...this.localSettings, replacementWordIds: this.replacementWordIds });
|
||||
const changedTtsFields = Object.keys(this.changedTtsFields)
|
||||
.filter((field) => this.changedTtsFields[field]);
|
||||
this.$emit('save', {
|
||||
...this.localSettings,
|
||||
changedTtsFields,
|
||||
replacementWordIds: this.replacementWordIds
|
||||
});
|
||||
this.handleClose();
|
||||
},
|
||||
markTtsSettingChanged(field) {
|
||||
this.$set(this.changedTtsFields, field, true);
|
||||
},
|
||||
formatTooltip(val) {
|
||||
return `${val}%`;
|
||||
},
|
||||
|
||||
@@ -51,7 +51,12 @@
|
||||
<el-button class="history-btn" @click="showSnapshotDialog = true">
|
||||
{{ $t("roleConfig.snapshotHistory") }}
|
||||
</el-button>
|
||||
<el-button type="primary" class="save-btn" @click="saveConfig">
|
||||
<el-button
|
||||
type="primary"
|
||||
class="save-btn"
|
||||
:disabled="voiceOptionsLoading"
|
||||
@click="saveConfig"
|
||||
>
|
||||
{{ $t("roleConfig.saveConfig") }}
|
||||
</el-button>
|
||||
<el-button class="reset-btn" @click="resetConfig">{{
|
||||
@@ -367,7 +372,7 @@
|
||||
v-model="selectedLanguage"
|
||||
:placeholder="$t('roleConfig.selectLanguage')"
|
||||
class="form-select language-select"
|
||||
@change="filterVoicesByLanguage"
|
||||
@change="handleLanguageChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="(lang, index) in languageOptions"
|
||||
@@ -392,6 +397,7 @@
|
||||
filterable
|
||||
:placeholder="$t('roleConfig.pleaseSelect')"
|
||||
class="form-select"
|
||||
@change="handleVoiceChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="(item, index) in voiceOptions"
|
||||
@@ -556,6 +562,10 @@ export default {
|
||||
// 语言筛选相关状态
|
||||
languageOptions: [], // 语言选项列表
|
||||
selectedLanguage: '', // 当前选中的语言
|
||||
ttsLanguageTouched: false,
|
||||
ttsVoiceTouched: false,
|
||||
voiceFetchSeq: 0,
|
||||
voiceOptionsLoading: false,
|
||||
// 功能状态
|
||||
featureStatus: {
|
||||
vad: false, // 语言检测活动功能状态
|
||||
@@ -592,6 +602,9 @@ export default {
|
||||
return { ...fallback };
|
||||
},
|
||||
async saveConfig() {
|
||||
if (this.voiceOptionsLoading) {
|
||||
return;
|
||||
}
|
||||
const configData = {
|
||||
agentCode: this.form.agentCode,
|
||||
agentName: this.form.agentName,
|
||||
@@ -601,8 +614,6 @@ export default {
|
||||
slmModelId: this.form.model.slmModelId,
|
||||
vllmModelId: this.form.model.vllmModelId,
|
||||
ttsModelId: this.form.model.ttsModelId,
|
||||
ttsVoiceId: this.form.ttsVoiceId,
|
||||
ttsLanguage: this.selectedLanguage,
|
||||
chatHistoryConf: this.form.chatHistoryConf,
|
||||
memModelId: this.form.model.memModelId,
|
||||
intentModelId: this.form.model.intentModelId,
|
||||
@@ -625,6 +636,17 @@ export default {
|
||||
if (tagsChanged) {
|
||||
configData.tagNames = tagNames;
|
||||
}
|
||||
if (this.shouldSubmitTtsLanguage()) {
|
||||
configData.ttsLanguage = this.selectedLanguage;
|
||||
}
|
||||
if (this.ttsVoiceTouched && this.form.ttsVoiceId !== null && this.form.ttsVoiceId !== undefined) {
|
||||
configData.ttsVoiceId = this.form.ttsVoiceId;
|
||||
}
|
||||
const submittedTtsLanguageTouched = this.ttsLanguageTouched;
|
||||
const submittedTtsVoiceTouched = this.ttsVoiceTouched;
|
||||
const submittedTtsLanguage = configData.ttsLanguage;
|
||||
const submittedTtsVoiceId = configData.ttsVoiceId;
|
||||
const submittedVoiceFetchSeq = this.voiceFetchSeq;
|
||||
|
||||
// 只在用户设置了TTS参数时才传递(不为null/undefined)
|
||||
if (this.form.ttsVolume !== null && this.form.ttsVolume !== undefined) {
|
||||
@@ -643,6 +665,17 @@ export default {
|
||||
if (tagsChanged) {
|
||||
this.originalTagNames = [...tagNames];
|
||||
}
|
||||
if (submittedVoiceFetchSeq === this.voiceFetchSeq
|
||||
&& submittedTtsLanguageTouched
|
||||
&& this.selectedLanguage === submittedTtsLanguage) {
|
||||
this.form.ttsLanguage = submittedTtsLanguage;
|
||||
this.ttsLanguageTouched = false;
|
||||
}
|
||||
if (submittedVoiceFetchSeq === this.voiceFetchSeq
|
||||
&& submittedTtsVoiceTouched
|
||||
&& this.form.ttsVoiceId === submittedTtsVoiceId) {
|
||||
this.ttsVoiceTouched = false;
|
||||
}
|
||||
this.$message.success({
|
||||
message: i18n.t("roleConfig.saveSuccess"),
|
||||
showClose: true,
|
||||
@@ -686,10 +719,14 @@ export default {
|
||||
type: "warning",
|
||||
})
|
||||
.then(() => {
|
||||
this.selectedLanguage = "";
|
||||
this.ttsLanguageTouched = true;
|
||||
this.ttsVoiceTouched = true;
|
||||
this.form = {
|
||||
agentCode: "",
|
||||
agentName: "",
|
||||
ttsVoiceId: "",
|
||||
ttsLanguage: "",
|
||||
chatHistoryConf: 0,
|
||||
systemPrompt: "",
|
||||
summaryMemory: "",
|
||||
@@ -707,6 +744,7 @@ export default {
|
||||
intentModelId: "",
|
||||
},
|
||||
};
|
||||
this.fetchVoiceOptions("");
|
||||
this.dynamicTags = [];
|
||||
this.currentFunctions = [];
|
||||
this.$message.success({
|
||||
@@ -745,6 +783,7 @@ export default {
|
||||
}
|
||||
},
|
||||
applyTemplateData(templateData) {
|
||||
const currentLanguage = this.selectedLanguage;
|
||||
this.form = {
|
||||
...this.form,
|
||||
agentName: templateData.agentName || this.form.agentName,
|
||||
@@ -764,11 +803,28 @@ export default {
|
||||
intentModelId: templateData.intentModelId || this.form.model.intentModelId,
|
||||
},
|
||||
};
|
||||
if (templateData.ttsLanguage) {
|
||||
this.selectedLanguage = templateData.ttsLanguage;
|
||||
this.ttsLanguageTouched = true;
|
||||
}
|
||||
if (templateData.ttsModelId || templateData.ttsVoiceId || templateData.ttsLanguage) {
|
||||
if (templateData.ttsModelId || templateData.ttsVoiceId) {
|
||||
this.ttsVoiceTouched = true;
|
||||
}
|
||||
this.ttsLanguageTouched = true;
|
||||
this.fetchVoiceOptions(this.form.model.ttsModelId, {
|
||||
autoSelectVoice: true,
|
||||
preferredLanguage: templateData.ttsLanguage || (templateData.ttsVoiceId ? "" : currentLanguage),
|
||||
preferredVoiceId: templateData.ttsVoiceId || ""
|
||||
});
|
||||
}
|
||||
},
|
||||
fetchAgentConfig(agentId) {
|
||||
Api.agent.getDeviceConfig(agentId, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.tempSummaryMemory = "";
|
||||
this.ttsLanguageTouched = false;
|
||||
this.ttsVoiceTouched = false;
|
||||
this.form = {
|
||||
...this.form,
|
||||
...data.data,
|
||||
@@ -783,6 +839,10 @@ export default {
|
||||
intentModelId: data.data.intentModelId,
|
||||
},
|
||||
};
|
||||
this.fetchVoiceOptions(data.data.ttsModelId, {
|
||||
preferredLanguage: data.data.ttsLanguage,
|
||||
preferredVoiceId: data.data.ttsVoiceId
|
||||
});
|
||||
|
||||
// 同步TTS设置到ttsSettings
|
||||
this.ttsSettings = {
|
||||
@@ -874,15 +934,22 @@ export default {
|
||||
}
|
||||
});
|
||||
},
|
||||
fetchVoiceOptions(modelId) {
|
||||
fetchVoiceOptions(modelId, options = {}) {
|
||||
const requestSeq = ++this.voiceFetchSeq;
|
||||
if (!modelId) {
|
||||
this.voiceOptionsLoading = false;
|
||||
this.voiceOptions = [];
|
||||
this.voiceDetails = {};
|
||||
this.languageOptions = [];
|
||||
this.selectedLanguage = '';
|
||||
return;
|
||||
}
|
||||
this.voiceOptionsLoading = true;
|
||||
Api.model.getModelVoices(modelId, "", ({ data }) => {
|
||||
if (requestSeq !== this.voiceFetchSeq) {
|
||||
return;
|
||||
}
|
||||
this.voiceOptionsLoading = false;
|
||||
if (data.code === 0 && data.data) {
|
||||
// 保存完整的音色信息
|
||||
this.voiceDetails = data.data.reduce((acc, voice) => {
|
||||
@@ -904,15 +971,27 @@ export default {
|
||||
label: lang
|
||||
}));
|
||||
|
||||
// 使用后端返回的用户选择的语言,如果没有则使用第一个语言选项
|
||||
if (this.form.ttsLanguage && this.languageOptions.some(option => option.value === this.form.ttsLanguage)) {
|
||||
// 优先保留调用方指定或后端保存的语言,其次使用当前音色的默认语言
|
||||
const requestedLanguage = options.preferredLanguage;
|
||||
const preferredVoiceLanguage = options.preferredVoiceId
|
||||
? this.getVoiceDefaultLanguage(options.preferredVoiceId)
|
||||
: "";
|
||||
if (requestedLanguage && this.languageOptions.some(option => option.value === requestedLanguage)) {
|
||||
this.selectedLanguage = requestedLanguage;
|
||||
} else if (preferredVoiceLanguage && this.languageOptions.some(option => option.value === preferredVoiceLanguage)) {
|
||||
this.selectedLanguage = preferredVoiceLanguage;
|
||||
} else if (this.form.ttsLanguage && this.languageOptions.some(option => option.value === this.form.ttsLanguage)) {
|
||||
this.selectedLanguage = this.form.ttsLanguage;
|
||||
} else if (this.getVoiceDefaultLanguage(this.form.ttsVoiceId)) {
|
||||
this.selectedLanguage = this.getVoiceDefaultLanguage(this.form.ttsVoiceId);
|
||||
} else if (this.languageOptions.length > 0) {
|
||||
this.selectedLanguage = this.languageOptions[0].value;
|
||||
} else {
|
||||
this.selectedLanguage = "";
|
||||
}
|
||||
|
||||
// 根据选中的语言筛选音色
|
||||
this.filterVoicesByLanguage();
|
||||
this.filterVoicesByLanguage(options);
|
||||
} else {
|
||||
this.voiceOptions = [];
|
||||
this.voiceDetails = {};
|
||||
@@ -921,9 +1000,19 @@ export default {
|
||||
}
|
||||
});
|
||||
},
|
||||
getVoiceDefaultLanguage(voiceId) {
|
||||
if (!voiceId || !this.voiceDetails || !this.voiceDetails[voiceId]?.languages) {
|
||||
return "";
|
||||
}
|
||||
const languages = this.voiceDetails[voiceId].languages
|
||||
.split(/[、;;,,]/)
|
||||
.map(lang => lang.trim())
|
||||
.filter(Boolean);
|
||||
return languages[0] || "";
|
||||
},
|
||||
|
||||
// 根据语言筛选音色
|
||||
filterVoicesByLanguage() {
|
||||
filterVoicesByLanguage(options = {}) {
|
||||
if (!this.voiceDetails || Object.keys(this.voiceDetails).length === 0) {
|
||||
this.voiceOptions = [];
|
||||
return;
|
||||
@@ -954,8 +1043,9 @@ export default {
|
||||
const currentVoiceSupportsLanguage = this.form.ttsVoiceId &&
|
||||
filteredVoices.some(voice => voice.id === this.form.ttsVoiceId);
|
||||
|
||||
if (!currentVoiceSupportsLanguage) {
|
||||
if (!currentVoiceSupportsLanguage && options.autoSelectVoice) {
|
||||
this.form.ttsVoiceId = filteredVoices.length > 0 ? filteredVoices[0].id : '';
|
||||
this.ttsVoiceTouched = true;
|
||||
}
|
||||
|
||||
// 同步到ttsSettings(如果值为null,使用0作为显示默认值,但不修改form中的值)
|
||||
@@ -965,6 +1055,21 @@ export default {
|
||||
pitch: this.form.ttsPitch !== null && this.form.ttsPitch !== undefined ? this.form.ttsPitch : 0
|
||||
};
|
||||
},
|
||||
handleLanguageChange() {
|
||||
this.ttsLanguageTouched = true;
|
||||
this.form.ttsLanguage = this.selectedLanguage;
|
||||
this.filterVoicesByLanguage({ autoSelectVoice: true });
|
||||
},
|
||||
handleVoiceChange() {
|
||||
this.ttsVoiceTouched = true;
|
||||
if (this.selectedLanguage) {
|
||||
this.form.ttsLanguage = this.selectedLanguage;
|
||||
this.ttsLanguageTouched = true;
|
||||
}
|
||||
},
|
||||
shouldSubmitTtsLanguage() {
|
||||
return this.ttsLanguageTouched;
|
||||
},
|
||||
|
||||
getFunctionDisplayChar(name) {
|
||||
if (!name || name.length === 0) return "";
|
||||
@@ -1006,6 +1111,17 @@ export default {
|
||||
// 当LLM类型改变时,更新意图识别选项的可见性
|
||||
this.updateIntentOptionsVisibility();
|
||||
}
|
||||
if (type === "TTS") {
|
||||
const preferredLanguage = this.selectedLanguage;
|
||||
this.ttsLanguageTouched = true;
|
||||
this.ttsVoiceTouched = true;
|
||||
this.form.ttsVoiceId = "";
|
||||
this.form.ttsLanguage = "";
|
||||
this.fetchVoiceOptions(value, {
|
||||
autoSelectVoice: true,
|
||||
preferredLanguage
|
||||
});
|
||||
}
|
||||
},
|
||||
fetchAllFunctions() {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -1042,13 +1158,20 @@ export default {
|
||||
this.showTtsAdvancedDialog = true;
|
||||
},
|
||||
handleTtsSettingsSave(settings) {
|
||||
const { replacementWordIds, ...ttsSettings } = settings;
|
||||
const { replacementWordIds, changedTtsFields = [], ...ttsSettings } = settings;
|
||||
this.checkedReplacementWordIds = replacementWordIds;
|
||||
// 保存TTS设置
|
||||
this.ttsSettings = ttsSettings;
|
||||
this.form.ttsVolume = ttsSettings.volume;
|
||||
this.form.ttsRate = ttsSettings.speed;
|
||||
this.form.ttsPitch = ttsSettings.pitch;
|
||||
const changedFields = new Set(changedTtsFields);
|
||||
if (changedFields.has("volume")) {
|
||||
this.form.ttsVolume = ttsSettings.volume;
|
||||
}
|
||||
if (changedFields.has("speed")) {
|
||||
this.form.ttsRate = ttsSettings.speed;
|
||||
}
|
||||
if (changedFields.has("pitch")) {
|
||||
this.form.ttsPitch = ttsSettings.pitch;
|
||||
}
|
||||
},
|
||||
handleUpdateContext(providers) {
|
||||
this.currentContextProviders = providers;
|
||||
@@ -1424,27 +1547,6 @@ export default {
|
||||
});
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
"form.model.ttsModelId": {
|
||||
handler(newVal, oldVal) {
|
||||
if (oldVal && newVal !== oldVal) {
|
||||
this.form.ttsVoiceId = "";
|
||||
this.fetchVoiceOptions(newVal);
|
||||
} else {
|
||||
this.fetchVoiceOptions(newVal);
|
||||
}
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
voiceOptions: {
|
||||
handler(newVal) {
|
||||
if (newVal && newVal.length > 0 && !this.form.ttsVoiceId) {
|
||||
this.form.ttsVoiceId = newVal[0].value;
|
||||
}
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
const agentId = this.$route.query.agentId;
|
||||
if (agentId) {
|
||||
|
||||
Reference in New Issue
Block a user