fix(avatar): prevent takeover loops and isolate settings
This commit is contained in:
@@ -64,7 +64,7 @@
|
||||
<span class="permission-copy">
|
||||
<strong>{{ item.title }}</strong>
|
||||
<small>
|
||||
{{ item.description }}
|
||||
{{ item.key === 'takeover' ? takeoverDescription : item.description }}
|
||||
<span
|
||||
v-if="item.key === 'takeover' && takeoverConnectionLabel"
|
||||
class="connection-state"
|
||||
@@ -79,6 +79,33 @@
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section v-if="permissionState.takeover" class="takeover-delay-card" aria-label="自动回复等待时间">
|
||||
<div class="delay-heading">
|
||||
<div>
|
||||
<strong>自动回复等待时间</strong>
|
||||
<small>等待期间主人发言会取消本次回复,最短 3 秒</small>
|
||||
</div>
|
||||
<span>{{ formattedTakeoverDelay }}</span>
|
||||
</div>
|
||||
<div class="delay-control">
|
||||
<input
|
||||
v-model.number="takeoverDelayValue"
|
||||
type="number"
|
||||
inputmode="numeric"
|
||||
step="1"
|
||||
:min="takeoverDelayUnit === 'minutes' ? 1 : 3"
|
||||
:max="takeoverDelayUnit === 'minutes' ? 1440 : 86400"
|
||||
aria-label="等待时间"
|
||||
:disabled="loading || saving"
|
||||
@blur="normalizeTakeoverDelay"
|
||||
/>
|
||||
<select v-model="takeoverDelayUnit" aria-label="等待时间单位" :disabled="loading || saving">
|
||||
<option value="seconds">秒</option>
|
||||
<option value="minutes">分钟</option>
|
||||
</select>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<p v-if="errorMessage" class="error-message" role="alert">{{ errorMessage }}</p>
|
||||
</template>
|
||||
|
||||
@@ -171,7 +198,7 @@ const permissionItems: Array<{
|
||||
{
|
||||
key: 'takeover',
|
||||
title: '分身主动接管聊天回复',
|
||||
description: '收到私聊消息 3 秒后回复,主人发言时暂停',
|
||||
description: '收到私聊消息后按设定时间回复,主人发言时暂停',
|
||||
tone: 'cyan',
|
||||
},
|
||||
]
|
||||
@@ -190,6 +217,8 @@ const saving = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const toastMessage = ref('')
|
||||
const takeoverStatus = ref<TakeoverStatus | null>(null)
|
||||
const takeoverDelayValue = ref(3)
|
||||
const takeoverDelayUnit = ref<'seconds' | 'minutes'>('minutes')
|
||||
let toastTimer: number | undefined
|
||||
let takeoverStatusTimer: number | undefined
|
||||
|
||||
@@ -210,6 +239,38 @@ const takeoverConnectionTone = computed(() => {
|
||||
return 'connecting'
|
||||
})
|
||||
|
||||
const takeoverDelaySeconds = computed(() => {
|
||||
const value = Math.trunc(Number(takeoverDelayValue.value) || 0)
|
||||
return takeoverDelayUnit.value === 'minutes' ? value * 60 : value
|
||||
})
|
||||
|
||||
const formattedTakeoverDelay = computed(() => {
|
||||
const seconds = takeoverDelaySeconds.value
|
||||
if (seconds > 0 && seconds % 60 === 0) return `${seconds / 60} 分钟`
|
||||
return `${seconds} 秒`
|
||||
})
|
||||
|
||||
const takeoverDescription = computed(() =>
|
||||
`收到私聊消息 ${formattedTakeoverDelay.value}后回复,主人发言时暂停`
|
||||
)
|
||||
|
||||
const applyTakeoverDelay = (seconds: number) => {
|
||||
const normalized = Number.isFinite(seconds) && seconds >= 3 ? Math.trunc(seconds) : 180
|
||||
if (normalized % 60 === 0) {
|
||||
takeoverDelayUnit.value = 'minutes'
|
||||
takeoverDelayValue.value = normalized / 60
|
||||
} else {
|
||||
takeoverDelayUnit.value = 'seconds'
|
||||
takeoverDelayValue.value = normalized
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeTakeoverDelay = () => {
|
||||
const min = takeoverDelayUnit.value === 'minutes' ? 1 : 3
|
||||
const max = takeoverDelayUnit.value === 'minutes' ? 1440 : 86400
|
||||
takeoverDelayValue.value = Math.min(max, Math.max(min, Math.trunc(Number(takeoverDelayValue.value) || min)))
|
||||
}
|
||||
|
||||
const setPermissions = (permissions: AvatarPermission[]) => {
|
||||
const enabled = new Set(permissions)
|
||||
for (const item of permissionItems) permissionState[item.key] = enabled.has(item.key)
|
||||
@@ -265,6 +326,7 @@ const loadSettings = async () => {
|
||||
try {
|
||||
const settings = await getAvatarPermissionSettings(activeAvatarId.value)
|
||||
setPermissions(settings.permissions || [])
|
||||
applyTakeoverDelay(settings.takeoverReplyDelaySeconds || 180)
|
||||
await loadTakeoverStatus()
|
||||
scheduleTakeoverStatusRefresh()
|
||||
} catch (error: any) {
|
||||
@@ -291,8 +353,18 @@ const saveSettings = async (takeoverToggle = false): Promise<boolean> => {
|
||||
saving.value = true
|
||||
errorMessage.value = ''
|
||||
try {
|
||||
const settings = await updateAvatarPermissionSettings(activeAvatarId.value, selectedPermissions())
|
||||
normalizeTakeoverDelay()
|
||||
if (takeoverDelaySeconds.value < 3 || takeoverDelaySeconds.value > 86400) {
|
||||
errorMessage.value = '自动回复等待时间需在 3 秒到 24 小时之间'
|
||||
return false
|
||||
}
|
||||
const settings = await updateAvatarPermissionSettings(
|
||||
activeAvatarId.value,
|
||||
selectedPermissions(),
|
||||
takeoverDelaySeconds.value,
|
||||
)
|
||||
setPermissions(settings.permissions || [])
|
||||
applyTakeoverDelay(settings.takeoverReplyDelaySeconds || 180)
|
||||
await loadTakeoverStatus()
|
||||
scheduleTakeoverStatusRefresh()
|
||||
if (takeoverToggle) {
|
||||
@@ -468,6 +540,75 @@ svg {
|
||||
min-height: 76px;
|
||||
}
|
||||
|
||||
.takeover-delay-card {
|
||||
margin-top: 12px;
|
||||
padding: 16px;
|
||||
border: 1px solid #dff1ef;
|
||||
border-radius: 15px;
|
||||
background: linear-gradient(135deg, #f5fcfb 0%, #fff 100%);
|
||||
box-shadow: 0 8px 24px rgba(53, 166, 162, .06);
|
||||
}
|
||||
|
||||
.delay-heading {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.delay-heading strong,
|
||||
.delay-heading small {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.delay-heading strong {
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.delay-heading small {
|
||||
margin-top: 5px;
|
||||
color: #8c929f;
|
||||
font-size: 11px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.delay-heading > span {
|
||||
flex: none;
|
||||
padding: 4px 8px;
|
||||
border-radius: 999px;
|
||||
color: #258e8a;
|
||||
background: #e8f8f6;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.delay-control {
|
||||
margin-top: 14px;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 88px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.delay-control input,
|
||||
.delay-control select {
|
||||
min-width: 0;
|
||||
height: 42px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid #dfe5e8;
|
||||
border-radius: 11px;
|
||||
outline: none;
|
||||
color: #222528;
|
||||
background: #fff;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.delay-control input:focus,
|
||||
.delay-control select:focus {
|
||||
border-color: #35a6a2;
|
||||
box-shadow: 0 0 0 3px rgba(53, 166, 162, .1);
|
||||
}
|
||||
|
||||
.permission-icon {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
|
||||
Reference in New Issue
Block a user