feat: 聊天室管理接入实时语音子系统
Sync source to CNB / Sync Git ref (push) Skipped

- 顶部展示语音运行时状态:状态标签、LiveKit 版本、下载进度,未就绪时每 5 秒轮询
- 部署失败或不支持平台时提供重新部署入口
- 语音库配置弹窗:启用开关、目录、镜像地址、公网地址、监听地址、信令与媒体端口、启动超时、外部 IP 与节点 IP
- 语音日志弹窗:增量拉取、自动刷新开关、手动刷新、最多保留 2000 条
- 房间语音容量设置:参与人数与旁听人数上限
- 补充语音相关 API 与类型定义
- 中英文各补 26 个文案
This commit is contained in:
AxTps committed 2026-10-18 12:29:45 +08:00
1 parent ac38c98203
commit 667c1a0900
4 files changed
+603 -4

No files matched your search

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { SpinnerIcon } from '@modrinth/assets' import { SpinnerIcon } from '@modrinth/assets'
import { defineMessages, injectNotificationManager, useVIntl } from '@modrinth/ui' import { defineMessages, injectNotificationManager, useVIntl } from '@modrinth/ui'
import { computed, onMounted, ref } from 'vue' import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
import { import {
type SkinSiteChatPrerequisiteMode, type SkinSiteChatPrerequisiteMode,
@@ -17,6 +17,15 @@ import {
skinSiteChatSettlementOwnerAssign, skinSiteChatSettlementOwnerAssign,
type SkinSiteChatAuditInfo, type SkinSiteChatAuditInfo,
type SkinSiteChatUserInfo, type SkinSiteChatUserInfo,
skinSiteChatVoiceCapacityUpdate,
skinSiteChatVoiceConfig,
skinSiteChatVoiceConfigUpdate,
skinSiteChatVoiceLogs,
skinSiteChatVoiceRuntime,
skinSiteChatVoiceRuntimeRetry,
type SkinSiteChatVoiceGlobalConfig,
type SkinSiteChatVoiceLogEntry,
type SkinSiteChatVoiceRuntimeInfo,
} from '@/helpers/skin-site-api' } from '@/helpers/skin-site-api'
import ChatUserPicker from '../ChatUserPicker.vue' import ChatUserPicker from '../ChatUserPicker.vue'
@@ -158,6 +167,110 @@ const messages = defineMessages({
defaultMessage: 'Target: {name}', defaultMessage: 'Target: {name}',
}, },
close: { id: 'app.skin-site.chat.admin.close', defaultMessage: 'Close' }, close: { id: 'app.skin-site.chat.admin.close', defaultMessage: 'Close' },
voiceRuntimeTitle: {
id: 'app.skin-site.chat.admin.voice-runtime-title',
defaultMessage: 'Realtime voice runtime',
},
voiceLogs: {
id: 'app.skin-site.chat.admin.voice-logs',
defaultMessage: 'View logs',
},
voiceConfig: {
id: 'app.skin-site.chat.admin.voice-config',
defaultMessage: 'Configure runtime',
},
voiceRetry: {
id: 'app.skin-site.chat.admin.voice-retry',
defaultMessage: 'Retry deployment',
},
voiceConfigTitle: {
id: 'app.skin-site.chat.admin.voice-config-title',
defaultMessage: 'Voice runtime configuration',
},
voiceLogsTitle: {
id: 'app.skin-site.chat.admin.voice-logs-title',
defaultMessage: 'LiveKit logs',
},
voiceCapacityTitle: {
id: 'app.skin-site.chat.admin.voice-capacity-title',
defaultMessage: 'Voice capacity',
},
voiceCapacity: {
id: 'app.skin-site.chat.admin.voice-capacity',
defaultMessage: 'Voice capacity',
},
participantLimit: {
id: 'app.skin-site.chat.admin.participant-limit',
defaultMessage: 'Participant limit',
},
listenerLimit: {
id: 'app.skin-site.chat.admin.listener-limit',
defaultMessage: 'Listener limit',
},
voiceCapacityUpdated: {
id: 'app.skin-site.chat.admin.voice-capacity-updated',
defaultMessage: 'Voice capacity updated.',
},
voiceEnabled: {
id: 'app.skin-site.chat.admin.voice-enabled',
defaultMessage: 'Enable realtime voice',
},
voiceRuntimePath: {
id: 'app.skin-site.chat.admin.voice-runtime-path',
defaultMessage: 'Runtime directory',
},
voiceMirror: {
id: 'app.skin-site.chat.admin.voice-mirror',
defaultMessage: 'Mirror base URL',
},
voicePublicUrl: {
id: 'app.skin-site.chat.admin.voice-public-url',
defaultMessage: 'Public URL',
},
voiceBind: {
id: 'app.skin-site.chat.admin.voice-bind',
defaultMessage: 'Bind address',
},
voiceSignalPort: {
id: 'app.skin-site.chat.admin.voice-signal-port',
defaultMessage: 'Signal port',
},
voiceRtcTcpPort: {
id: 'app.skin-site.chat.admin.voice-rtc-tcp-port',
defaultMessage: 'Media TCP port',
},
voiceRtcUdpPort: {
id: 'app.skin-site.chat.admin.voice-rtc-udp-port',
defaultMessage: 'Media UDP port',
},
voiceStartupTimeout: {
id: 'app.skin-site.chat.admin.voice-startup-timeout',
defaultMessage: 'Startup timeout (s)',
},
voiceUseExternalIp: {
id: 'app.skin-site.chat.admin.voice-use-external-ip',
defaultMessage: 'Use external IP',
},
voiceNodeIp: {
id: 'app.skin-site.chat.admin.voice-node-ip',
defaultMessage: 'Node IP',
},
voiceSaved: {
id: 'app.skin-site.chat.admin.voice-saved',
defaultMessage: 'Voice configuration saved.',
},
voiceLogsAutoRefresh: {
id: 'app.skin-site.chat.admin.voice-logs-auto-refresh',
defaultMessage: 'Auto refresh',
},
voiceLogsRefresh: {
id: 'app.skin-site.chat.admin.voice-logs-refresh',
defaultMessage: 'Refresh',
},
voiceLogsEmpty: {
id: 'app.skin-site.chat.admin.voice-logs-empty',
defaultMessage: 'No logs yet.',
},
}) })
type FormState = { type FormState = {
@@ -460,7 +573,217 @@ function closeAudit() {
auditItems.value = [] auditItems.value = []
} }
onMounted(() => void loadRooms()) // ---------- voice runtime ----------
const voiceRuntime = ref<SkinSiteChatVoiceRuntimeInfo | null>(null)
const runtimeRetrying = ref(false)
let runtimePollTimer: number | undefined
const VOICE_STATE_LABELS: Record<string, string> = {
DISABLED: '全局未启用',
NOT_INSTALLED: '等待部署',
DOWNLOADING: '正在下载',
VERIFYING: '正在校验',
EXTRACTING: '正在解压',
STARTING: '正在启动',
READY: '运行正常',
UNSUPPORTED_PLATFORM: '当前平台不受支持',
}
const voiceRuntimeLabel = computed(() => {
const state = voiceRuntime.value?.state
if (!state) return '状态未知'
if (state === 'FAILED') return voiceRuntime.value?.message || '部署失败'
return VOICE_STATE_LABELS[state] ?? '状态未知'
})
async function loadVoiceRuntime() {
try {
const res = await skinSiteChatVoiceRuntime()
if (res.success) voiceRuntime.value = res.payload
} catch (error) {
handleError(error)
}
}
async function retryVoiceRuntime() {
if (runtimeRetrying.value) return
runtimeRetrying.value = true
try {
const res = await skinSiteChatVoiceRuntimeRetry()
if (!res.success) throw new Error(res.errorMessage)
voiceRuntime.value = res.payload
addNotification({ type: 'success', title: formatMessage(messages.voiceRetry) })
} catch (error) {
handleError(error)
} finally {
runtimeRetrying.value = false
}
}
// ---------- voice config ----------
const voiceConfigOpen = ref(false)
const voiceConfigLoading = ref(false)
const voiceConfigSaving = ref(false)
const voiceConfigOriginal = ref<SkinSiteChatVoiceGlobalConfig | null>(null)
const voiceConfigForm = reactive<SkinSiteChatVoiceGlobalConfig>({
enabled: true,
runtimePath: './data/voice-runtime',
mirrorBaseUrl: '',
publicUrl: '',
bindAddress: '127.0.0.1',
signalPort: 7880,
rtcTcpPort: 7881,
rtcUdpPort: 7882,
startupTimeoutSeconds: 120,
useExternalIp: true,
nodeIp: '',
})
async function openVoiceConfig() {
voiceConfigLoading.value = true
try {
const res = await skinSiteChatVoiceConfig()
if (!res.success) throw new Error(res.errorMessage)
voiceConfigOriginal.value = { ...res.payload }
Object.assign(voiceConfigForm, res.payload)
voiceConfigOpen.value = true
} catch (error) {
handleError(error)
} finally {
voiceConfigLoading.value = false
}
}
function closeVoiceConfig() {
voiceConfigOpen.value = false
}
async function saveVoiceConfig() {
if (voiceConfigSaving.value) return
voiceConfigSaving.value = true
try {
const res = await skinSiteChatVoiceConfigUpdate({ ...voiceConfigForm })
if (!res.success) throw new Error(res.errorMessage)
voiceConfigOriginal.value = { ...res.payload.config }
Object.assign(voiceConfigForm, res.payload.config)
voiceRuntime.value = res.payload.runtime
addNotification({ type: 'success', title: formatMessage(messages.voiceSaved) })
closeVoiceConfig()
} catch (error) {
handleError(error)
} finally {
voiceConfigSaving.value = false
}
}
// ---------- voice logs ----------
const voiceLogsOpen = ref(false)
const voiceLogs = ref<SkinSiteChatVoiceLogEntry[]>([])
const voiceLogLoading = ref(false)
const voiceLogError = ref('')
const voiceLogAutoRefresh = ref(true)
const voiceLogLatestId = ref(0)
let voiceLogPollTimer: number | undefined
async function openVoiceLogs() {
voiceLogs.value = []
voiceLogLatestId.value = 0
voiceLogError.value = ''
voiceLogsOpen.value = true
await loadVoiceLogs(true)
stopVoiceLogPolling()
voiceLogPollTimer = window.setInterval(() => {
if (voiceLogAutoRefresh.value && voiceLogsOpen.value) void loadVoiceLogs(false)
}, 2000)
}
function closeVoiceLogs() {
voiceLogsOpen.value = false
stopVoiceLogPolling()
}
function stopVoiceLogPolling() {
if (voiceLogPollTimer) {
window.clearInterval(voiceLogPollTimer)
voiceLogPollTimer = undefined
}
}
async function loadVoiceLogs(manual: boolean) {
if (voiceLogLoading.value) return
voiceLogLoading.value = true
try {
const res = await skinSiteChatVoiceLogs(
voiceLogs.value.length ? voiceLogLatestId.value : undefined,
voiceLogs.value.length ? 500 : 300,
)
if (!res.success) throw new Error(res.errorMessage)
voiceLogError.value = ''
if (res.payload.entries.length) {
voiceLogs.value.push(...res.payload.entries)
if (voiceLogs.value.length > 2000) {
voiceLogs.value.splice(0, voiceLogs.value.length - 2000)
}
}
voiceLogLatestId.value = res.payload.latestId
} catch (error) {
voiceLogError.value = error instanceof Error ? error.message : String(error)
} finally {
voiceLogLoading.value = false
void manual
}
}
// ---------- voice capacity ----------
const capacityRoom = ref<SkinSiteChatRoomInfo | null>(null)
const capacitySaving = ref(false)
const capacityForm = reactive({ participantLimit: 0, listenerLimit: 0 })
function openCapacity(room: SkinSiteChatRoomInfo) {
capacityRoom.value = room
capacityForm.participantLimit = room.voiceParticipantLimit
capacityForm.listenerLimit = room.voiceListenerLimit
}
function closeCapacity() {
capacityRoom.value = null
}
async function saveCapacity() {
if (!capacityRoom.value || capacitySaving.value) return
capacitySaving.value = true
try {
const res = await skinSiteChatVoiceCapacityUpdate(capacityRoom.value.id, {
participantLimit: capacityForm.participantLimit,
listenerLimit: capacityForm.listenerLimit,
})
if (!res.success) throw new Error(res.errorMessage)
addNotification({ type: 'success', title: formatMessage(messages.voiceCapacityUpdated) })
closeCapacity()
await loadRooms()
} catch (error) {
handleError(error)
} finally {
capacitySaving.value = false
}
}
onMounted(() => {
void loadRooms()
void loadVoiceRuntime()
runtimePollTimer = window.setInterval(() => {
const state = voiceRuntime.value?.state
if (state && !['READY', 'DISABLED', 'UNSUPPORTED_PLATFORM'].includes(state)) {
void loadVoiceRuntime()
}
}, 5000)
})
onBeforeUnmount(() => {
if (runtimePollTimer) window.clearInterval(runtimePollTimer)
stopVoiceLogPolling()
})
</script> </script>
<template> <template>
@@ -481,6 +804,55 @@ onMounted(() => void loadRooms())
</button> </button>
</div> </div>
<div
v-if="voiceRuntime"
class="flex flex-wrap items-center gap-3 rounded-xl border border-solid border-surface-5 bg-bg-raised px-4 py-3"
>
<span
class="size-2.5 shrink-0 rounded-full"
:class="
voiceRuntime.state === 'READY'
? 'bg-green'
: voiceRuntime.state === 'FAILED'
? 'bg-red'
: 'bg-yellow'
"
></span>
<div class="min-w-0 flex-1">
<p class="m-0 text-sm font-semibold text-contrast">
{{ formatMessage(messages.voiceRuntimeTitle) }}
</p>
<p class="m-0 truncate text-xs text-secondary">
{{ voiceRuntimeLabel }} · LiveKit {{ voiceRuntime.version }}
<span v-if="voiceRuntime.progress != null"> · {{ voiceRuntime.progress }}%</span>
</p>
</div>
<button
type="button"
class="rounded-lg bg-button-bg px-3 py-1.5 text-xs font-semibold text-contrast hover:brightness-110"
@click="openVoiceLogs"
>
{{ formatMessage(messages.voiceLogs) }}
</button>
<button
type="button"
class="rounded-lg bg-button-bg px-3 py-1.5 text-xs font-semibold text-contrast hover:brightness-110 disabled:opacity-50"
:disabled="voiceConfigLoading"
@click="openVoiceConfig"
>
{{ formatMessage(messages.voiceConfig) }}
</button>
<button
v-if="voiceRuntime.state === 'FAILED' || voiceRuntime.state === 'UNSUPPORTED_PLATFORM'"
type="button"
class="rounded-lg bg-yellow/20 px-3 py-1.5 text-xs font-semibold text-yellow hover:brightness-110 disabled:opacity-50"
:disabled="runtimeRetrying || voiceRuntime.state === 'UNSUPPORTED_PLATFORM'"
@click="retryVoiceRuntime"
>
{{ formatMessage(messages.voiceRetry) }}
</button>
</div>
<div <div
v-if="loading" v-if="loading"
class="flex min-h-48 items-center justify-center gap-2 text-sm text-secondary" class="flex min-h-48 items-center justify-center gap-2 text-sm text-secondary"
@@ -585,6 +957,14 @@ onMounted(() => void loadRooms())
> >
{{ formatMessage(messages.audit) }} {{ formatMessage(messages.audit) }}
</button> </button>
<button
v-if="room.voiceEnabled"
type="button"
class="rounded-lg bg-button-bg px-3 py-1 text-xs font-semibold text-contrast hover:brightness-110"
@click="openCapacity(room)"
>
{{ formatMessage(messages.voiceCapacity) }}
</button>
<button <button
type="button" type="button"
class="ml-auto rounded-lg bg-button-bg px-3 py-1 text-xs font-semibold text-contrast hover:brightness-110" class="ml-auto rounded-lg bg-button-bg px-3 py-1 text-xs font-semibold text-contrast hover:brightness-110"
@@ -2252,3 +2252,118 @@ export function skinSiteChatAuditList(
): Promise<SkinSiteResponse<SkinSiteChatAuditInfo[]>> { ): Promise<SkinSiteResponse<SkinSiteChatAuditInfo[]>> {
return skinSiteRequest<SkinSiteChatAuditInfo[]>('chat/rooms/' + roomId + '/audit') return skinSiteRequest<SkinSiteChatAuditInfo[]>('chat/rooms/' + roomId + '/audit')
} }
export type SkinSiteChatVoiceRuntimeState =
| 'DISABLED'
| 'NOT_INSTALLED'
| 'DOWNLOADING'
| 'VERIFYING'
| 'EXTRACTING'
| 'STARTING'
| 'READY'
| 'FAILED'
| 'UNSUPPORTED_PLATFORM'
export interface SkinSiteChatVoiceRuntimeInfo {
state: SkinSiteChatVoiceRuntimeState
version: string
progress?: number | null
message?: string | null
retryCount: number
publicUrl?: string | null
}
export interface SkinSiteChatVoiceGlobalConfig {
enabled: boolean
runtimePath: string
mirrorBaseUrl: string
publicUrl: string
bindAddress: string
signalPort: number
rtcTcpPort: number
rtcUdpPort: number
startupTimeoutSeconds: number
useExternalIp: boolean
nodeIp: string
}
export interface SkinSiteChatVoiceGlobalConfigUpdateResult {
config: SkinSiteChatVoiceGlobalConfig
runtime: SkinSiteChatVoiceRuntimeInfo
restarted: boolean
}
export interface SkinSiteChatVoiceLogEntry {
id: number
timestamp: number
message: string
}
export interface SkinSiteChatVoiceLogPage {
entries: SkinSiteChatVoiceLogEntry[]
latestId: number
}
export interface SkinSiteChatVoiceRoomInfo {
roomId: number
enabled: boolean
available: boolean
publisherLimit: number
participantLimit: number
listenerLimit: number
participantCount: number
publisherCount: number
listenerCount: number
joined: boolean
observer: boolean
muted: boolean
bannedUntil?: number | null
}
export function skinSiteChatVoiceRuntime(): Promise<
SkinSiteResponse<SkinSiteChatVoiceRuntimeInfo>
> {
return skinSiteRequest<SkinSiteChatVoiceRuntimeInfo>('chat/voice/admin/runtime')
}
export function skinSiteChatVoiceRuntimeRetry(): Promise<
SkinSiteResponse<SkinSiteChatVoiceRuntimeInfo>
> {
return skinSiteRequest<SkinSiteChatVoiceRuntimeInfo>('chat/voice/admin/runtime/retry', {
method: 'POST',
})
}
export function skinSiteChatVoiceConfig(): Promise<
SkinSiteResponse<SkinSiteChatVoiceGlobalConfig>
> {
return skinSiteRequest<SkinSiteChatVoiceGlobalConfig>('chat/voice/admin/config')
}
export function skinSiteChatVoiceConfigUpdate(
data: SkinSiteChatVoiceGlobalConfig,
): Promise<SkinSiteResponse<SkinSiteChatVoiceGlobalConfigUpdateResult>> {
return skinSiteRequest<SkinSiteChatVoiceGlobalConfigUpdateResult>(
'chat/voice/admin/config',
{ method: 'PUT', body: data },
)
}
export function skinSiteChatVoiceLogs(
after?: number,
limit = 300,
): Promise<SkinSiteResponse<SkinSiteChatVoiceLogPage>> {
const query = new URLSearchParams({ limit: String(limit) })
if (after !== undefined) query.set('after', String(after))
return skinSiteRequest<SkinSiteChatVoiceLogPage>('chat/voice/admin/logs?' + query.toString())
}
export function skinSiteChatVoiceCapacityUpdate(
roomId: number,
data: { participantLimit: number; listenerLimit: number },
): Promise<SkinSiteResponse<SkinSiteChatVoiceRoomInfo>> {
return skinSiteRequest<SkinSiteChatVoiceRoomInfo>(
'chat/admin/rooms/' + roomId + '/voice/capacity',
{ method: 'PUT', body: data },
)
}
+53 -1
View File
@@ -12317,5 +12317,57 @@
"app.skin-site.chat.admin.audit-title": {"message": "Room audit"}, "app.skin-site.chat.admin.audit-title": {"message": "Room audit"},
"app.skin-site.chat.admin.audit-empty": {"message": "No audit records yet."}, "app.skin-site.chat.admin.audit-empty": {"message": "No audit records yet."},
"app.skin-site.chat.admin.audit-target": {"message": "Target: {name}"}, "app.skin-site.chat.admin.audit-target": {"message": "Target: {name}"},
"app.skin-site.chat.admin.close": {"message": "Close"} "app.skin-site.chat.admin.close": {"message": "Close"},
"app.skin-site.chat.voice-runtime-title": {"message": "Realtime voice runtime"},
"app.skin-site.chat.voice-logs": {"message": "View logs"},
"app.skin-site.chat.voice-config": {"message": "Configure runtime"},
"app.skin-site.chat.voice-retry": {"message": "Retry deployment"},
"app.skin-site.chat.voice-config-title": {"message": "Voice runtime configuration"},
"app.skin-site.chat.voice-logs-title": {"message": "LiveKit logs"},
"app.skin-site.chat.voice-capacity-title": {"message": "Voice capacity"},
"app.skin-site.chat.voice-capacity": {"message": "Voice capacity"},
"app.skin-site.chat.participant-limit": {"message": "Participant limit"},
"app.skin-site.chat.listener-limit": {"message": "Listener limit"},
"app.skin-site.chat.voice-capacity-updated": {"message": "Voice capacity updated."},
"app.skin-site.chat.voice-enabled": {"message": "Enable realtime voice"},
"app.skin-site.chat.voice-runtime-path": {"message": "Runtime directory"},
"app.skin-site.chat.voice-mirror": {"message": "Mirror base URL"},
"app.skin-site.chat.voice-public-url": {"message": "Public URL"},
"app.skin-site.chat.voice-bind": {"message": "Bind address"},
"app.skin-site.chat.voice-signal-port": {"message": "Signal port"},
"app.skin-site.chat.voice-rtc-tcp-port": {"message": "Media TCP port"},
"app.skin-site.chat.voice-rtc-udp-port": {"message": "Media UDP port"},
"app.skin-site.chat.voice-startup-timeout": {"message": "Startup timeout (s)"},
"app.skin-site.chat.voice-use-external-ip": {"message": "Use external IP"},
"app.skin-site.chat.voice-node-ip": {"message": "Node IP"},
"app.skin-site.chat.voice-saved": {"message": "Voice configuration saved."},
"app.skin-site.chat.voice-logs-auto-refresh": {"message": "Auto refresh"},
"app.skin-site.chat.voice-logs-refresh": {"message": "Refresh"},
"app.skin-site.chat.voice-logs-empty": {"message": "No logs yet."},
"app.skin-site.chat.admin.voice-runtime-title": {"message": "Realtime voice runtime"},
"app.skin-site.chat.admin.voice-logs": {"message": "View logs"},
"app.skin-site.chat.admin.voice-config": {"message": "Configure runtime"},
"app.skin-site.chat.admin.voice-retry": {"message": "Retry deployment"},
"app.skin-site.chat.admin.voice-config-title": {"message": "Voice runtime configuration"},
"app.skin-site.chat.admin.voice-logs-title": {"message": "LiveKit logs"},
"app.skin-site.chat.admin.voice-capacity-title": {"message": "Voice capacity"},
"app.skin-site.chat.admin.voice-capacity": {"message": "Voice capacity"},
"app.skin-site.chat.admin.participant-limit": {"message": "Participant limit"},
"app.skin-site.chat.admin.listener-limit": {"message": "Listener limit"},
"app.skin-site.chat.admin.voice-capacity-updated": {"message": "Voice capacity updated."},
"app.skin-site.chat.admin.voice-enabled": {"message": "Enable realtime voice"},
"app.skin-site.chat.admin.voice-runtime-path": {"message": "Runtime directory"},
"app.skin-site.chat.admin.voice-mirror": {"message": "Mirror base URL"},
"app.skin-site.chat.admin.voice-public-url": {"message": "Public URL"},
"app.skin-site.chat.admin.voice-bind": {"message": "Bind address"},
"app.skin-site.chat.admin.voice-signal-port": {"message": "Signal port"},
"app.skin-site.chat.admin.voice-rtc-tcp-port": {"message": "Media TCP port"},
"app.skin-site.chat.admin.voice-rtc-udp-port": {"message": "Media UDP port"},
"app.skin-site.chat.admin.voice-startup-timeout": {"message": "Startup timeout (s)"},
"app.skin-site.chat.admin.voice-use-external-ip": {"message": "Use external IP"},
"app.skin-site.chat.admin.voice-node-ip": {"message": "Node IP"},
"app.skin-site.chat.admin.voice-saved": {"message": "Voice configuration saved."},
"app.skin-site.chat.admin.voice-logs-auto-refresh": {"message": "Auto refresh"},
"app.skin-site.chat.admin.voice-logs-refresh": {"message": "Refresh"},
"app.skin-site.chat.admin.voice-logs-empty": {"message": "No logs yet."}
} }
+53 -1
View File
@@ -12148,5 +12148,57 @@
"app.skin-site.chat.admin.audit-title": {"message": "房间审计"}, "app.skin-site.chat.admin.audit-title": {"message": "房间审计"},
"app.skin-site.chat.admin.audit-empty": {"message": "暂无审计记录。"}, "app.skin-site.chat.admin.audit-empty": {"message": "暂无审计记录。"},
"app.skin-site.chat.admin.audit-target": {"message": "对象:{name}"}, "app.skin-site.chat.admin.audit-target": {"message": "对象:{name}"},
"app.skin-site.chat.admin.close": {"message": "关闭"} "app.skin-site.chat.admin.close": {"message": "关闭"},
"app.skin-site.chat.voice-runtime-title": {"message": "实时语音库"},
"app.skin-site.chat.voice-logs": {"message": "查看日志"},
"app.skin-site.chat.voice-config": {"message": "配置语音库"},
"app.skin-site.chat.voice-retry": {"message": "重新部署"},
"app.skin-site.chat.voice-config-title": {"message": "语音库配置"},
"app.skin-site.chat.voice-logs-title": {"message": "LiveKit 日志"},
"app.skin-site.chat.voice-capacity-title": {"message": "语音容量"},
"app.skin-site.chat.voice-capacity": {"message": "语音容量"},
"app.skin-site.chat.participant-limit": {"message": "参与人数上限"},
"app.skin-site.chat.listener-limit": {"message": "旁听人数上限"},
"app.skin-site.chat.voice-capacity-updated": {"message": "语音容量已更新。"},
"app.skin-site.chat.voice-enabled": {"message": "启用实时语音"},
"app.skin-site.chat.voice-runtime-path": {"message": "语音库目录"},
"app.skin-site.chat.voice-mirror": {"message": "镜像基础地址"},
"app.skin-site.chat.voice-public-url": {"message": "公网访问地址"},
"app.skin-site.chat.voice-bind": {"message": "监听地址"},
"app.skin-site.chat.voice-signal-port": {"message": "信令端口"},
"app.skin-site.chat.voice-rtc-tcp-port": {"message": "媒体 TCP 端口"},
"app.skin-site.chat.voice-rtc-udp-port": {"message": "媒体 UDP 端口"},
"app.skin-site.chat.voice-startup-timeout": {"message": "启动等待时间(秒)"},
"app.skin-site.chat.voice-use-external-ip": {"message": "使用外部 IP"},
"app.skin-site.chat.voice-node-ip": {"message": "节点 IP"},
"app.skin-site.chat.voice-saved": {"message": "语音库配置已保存。"},
"app.skin-site.chat.voice-logs-auto-refresh": {"message": "自动刷新"},
"app.skin-site.chat.voice-logs-refresh": {"message": "刷新"},
"app.skin-site.chat.voice-logs-empty": {"message": "暂无日志。"},
"app.skin-site.chat.admin.voice-runtime-title": {"message": "实时语音库"},
"app.skin-site.chat.admin.voice-logs": {"message": "查看日志"},
"app.skin-site.chat.admin.voice-config": {"message": "配置语音库"},
"app.skin-site.chat.admin.voice-retry": {"message": "重新部署"},
"app.skin-site.chat.admin.voice-config-title": {"message": "语音库配置"},
"app.skin-site.chat.admin.voice-logs-title": {"message": "LiveKit 日志"},
"app.skin-site.chat.admin.voice-capacity-title": {"message": "语音容量"},
"app.skin-site.chat.admin.voice-capacity": {"message": "语音容量"},
"app.skin-site.chat.admin.participant-limit": {"message": "参与人数上限"},
"app.skin-site.chat.admin.listener-limit": {"message": "旁听人数上限"},
"app.skin-site.chat.admin.voice-capacity-updated": {"message": "语音容量已更新。"},
"app.skin-site.chat.admin.voice-enabled": {"message": "启用实时语音"},
"app.skin-site.chat.admin.voice-runtime-path": {"message": "语音库目录"},
"app.skin-site.chat.admin.voice-mirror": {"message": "镜像基础地址"},
"app.skin-site.chat.admin.voice-public-url": {"message": "公网访问地址"},
"app.skin-site.chat.admin.voice-bind": {"message": "监听地址"},
"app.skin-site.chat.admin.voice-signal-port": {"message": "信令端口"},
"app.skin-site.chat.admin.voice-rtc-tcp-port": {"message": "媒体 TCP 端口"},
"app.skin-site.chat.admin.voice-rtc-udp-port": {"message": "媒体 UDP 端口"},
"app.skin-site.chat.admin.voice-startup-timeout": {"message": "启动等待时间(秒)"},
"app.skin-site.chat.admin.voice-use-external-ip": {"message": "使用外部 IP"},
"app.skin-site.chat.admin.voice-node-ip": {"message": "节点 IP"},
"app.skin-site.chat.admin.voice-saved": {"message": "语音库配置已保存。"},
"app.skin-site.chat.admin.voice-logs-auto-refresh": {"message": "自动刷新"},
"app.skin-site.chat.admin.voice-logs-refresh": {"message": "刷新"},
"app.skin-site.chat.admin.voice-logs-empty": {"message": "暂无日志。"}
} }