diff --git a/api/reception.js b/api/reception.js
index c973d39..c2fbbbe 100644
--- a/api/reception.js
+++ b/api/reception.js
@@ -620,6 +620,32 @@ export function aiGeneratePrescriptionApi(data) {
})
}
+/**
+ * 语音转写(multipart:字段名 audio)
+ * @param {{ filePath: string, duration_ms?: number }} data
+ */
+export function aiVoiceAsrApi(data) {
+ return req.request({
+ url: '/newApi/doctor-reception-wx/ai-voice-asr',
+ method: 'POST',
+ type: 'upload',
+ filePath: data.filePath,
+ name: 'audio',
+ formData: {
+ duration_ms: String(data.duration_ms || 0)
+ }
+ })
+}
+
+/** 语音口述落方(只抽药名剂量) */
+export function aiGenerateVoicePrescriptionApi(data) {
+ return req.request({
+ url: '/newApi/doctor-reception-wx/ai-generate-voice-prescription',
+ method: 'POST',
+ data
+ })
+}
+
/** 本挂号 AI 生成历史(轻量列表) */
export function aiListGenerationsApi(data) {
return req.request({
diff --git a/subPackages/sub_workbench/prescription_v2/components/AiPrescriptionDrawer.vue b/subPackages/sub_workbench/prescription_v2/components/AiPrescriptionDrawer.vue
index 46eda6e..5b751f7 100644
--- a/subPackages/sub_workbench/prescription_v2/components/AiPrescriptionDrawer.vue
+++ b/subPackages/sub_workbench/prescription_v2/components/AiPrescriptionDrawer.vue
@@ -29,12 +29,12 @@
{{ consentText }}
-
+
配伍禁忌提示
· {{ line }}
-
+
历史
@@ -162,13 +162,13 @@
- 未对照({{ unmatchedCount }})
+ 未对照({{ unmatchedCount }})— 药库未找到,已标红
- {{ u.ai_name }}
+ {{ u.ai_name }}
去手动搜索
+
+
+
+ 语音开方
+ 返回
+
+
+
+
+ {{ voiceAsrLoading ? '转写中…' : (voiceRecording ? '点击结束录音' : '点击开始录音') }}
+
+ 说完后点结束,可改字
+
+
+ 转写文本(可改)
+
+
+
+ 请选择开方方式
+
+ 辨证出方
+ 根据口述病情,AI 辅助生成处方
+
+
+ 口述落方
+ 识别口述药名剂量,对照药库后导入
+
+
+
+
+ 委托调剂
+
+
+
+ 制剂要求
+ 加载中…
+
+ {{ item.name || item.title || ('#' + item.id) }}
+
+
+
+
+
+
+
+ 返回
+
+ {{ generating ? '生成中…' : '确认生成' }}
+
+
关闭
生成处方
@@ -343,10 +426,12 @@ import {
aiAgreeConsentApi,
aiFeatureGateApi,
aiGeneratePrescriptionApi,
+ aiGenerateVoicePrescriptionApi,
aiListGenerationsApi,
aiMarkGenerationReadApi,
aiMatchPrescriptionDrugsApi,
aiSavePrescriptionDrugSelectionApi,
+ aiVoiceAsrApi,
getProcessRuleList,
} from '@/api/reception.js'
@@ -391,11 +476,23 @@ export default {
patientAge: { type: [Number, String], default: 0 },
chiefComplaint: { type: String, default: '' },
medicalRecord: { type: Object, default: () => ({}) },
+ /** assist=携带病历;voice=工具栏语音入口,同意后直接进语音面板 */
+ entry: { type: String, default: 'assist' },
},
data() {
return {
visible: false,
genModalVisible: false,
+ /** 语音开方二级面板 */
+ voiceModalVisible: false,
+ voiceRecording: false,
+ voiceAsrLoading: false,
+ voiceTranscript: '',
+ voiceTranscriptTrimmed: '',
+ /** dialectic | dictation;默认口述落方 */
+ voiceMode: 'dictation',
+ voiceAsrProvider: '',
+ voiceAsrDurationMs: 0,
aiChief: '',
localMr: {},
editingKey: '',
@@ -496,6 +593,8 @@ export default {
this.aiChief = String(this.chiefComplaint || '').trim()
this.localMr = Object.assign({}, this.medicalRecord || {})
this.genModalVisible = false
+ this.voiceModalVisible = false
+ this.resetVoiceState()
this.editingKey = ''
this.consentVisible = false
this.consentSubmitting = false
@@ -506,6 +605,18 @@ export default {
this.resetPreview()
this.ensureConsentThenBootstrap()
},
+ /**
+ * 重置语音开方状态并停止录音
+ */
+ resetVoiceState() {
+ this.stopVoiceRecording(true)
+ this.voiceAsrLoading = false
+ this.voiceTranscript = ''
+ this.voiceTranscriptTrimmed = ''
+ this.voiceMode = 'dictation'
+ this.voiceAsrProvider = ''
+ this.voiceAsrDurationMs = 0
+ },
/**
* 首次使用 AI 出方:未签署则先展示知情同意
*/
@@ -528,6 +639,13 @@ export default {
},
afterConsentReady() {
this.consentVisible = false
+ // 工具栏「语音开方」:同意后直接进语音面板,默认口述落方
+ if (this.entry === 'voice') {
+ this.voiceMode = 'dictation'
+ this.openVoiceModal()
+ void this.loadHistory()
+ return
+ }
this.loadHistory().then(() => {
if (this.historyList.length) {
this.selectHistoryById(Number(this.historyList[0].id))
@@ -581,17 +699,36 @@ export default {
this.rebuildMatchView()
},
onClose() {
+ this.resetVoiceState()
this.visible = false
this.genModalVisible = false
+ this.voiceModalVisible = false
},
/**
- * Laravel jok 业务在 result;兼容 Yii data / 已解包数组
+ * Laravel jok 业务在 result;兼容 Yii data / 已解包数组 / upload 字符串 JSON
*/
unwrapBiz(res) {
if (res == null) return null
- if (res.result != null) return res.result
- if (res.data != null) return res.data
- return res
+ let body = res
+ if (typeof res === 'string') {
+ try {
+ body = JSON.parse(res)
+ } catch (e) {
+ return null
+ }
+ }
+ // uni.uploadFile 常见形态:{ data: '{"code":0,"result":...}' }
+ if (body && typeof body.data === 'string') {
+ try {
+ const parsed = JSON.parse(body.data)
+ if (parsed && typeof parsed === 'object') body = parsed
+ } catch (e2) {
+ /* keep body */
+ }
+ }
+ if (body && body.result != null) return body.result
+ if (body && body.data != null && typeof body.data === 'object') return body.data
+ return body
},
formatTime(ts) {
// 已是格式化字符串则直接展示(查询器可能一级格式化)
@@ -950,6 +1087,346 @@ export default {
this.genModalVisible = false
this.editingKey = ''
},
+ /**
+ * 打开语音开方面板
+ */
+ openVoiceModal() {
+ if (this.generating) return
+ if (!Number(this.prescriptionType || 0)) {
+ uni.showToast({ title: '请先选择处方类型', icon: 'none' })
+ return
+ }
+ this.resetVoiceState()
+ this.useEntrustedProcess = this.loadUseEntrustedProcessPref()
+ this.returnDosageProcessDetail = this.loadReturnDosageProcessDetailPref()
+ if (this.isTcmRx && this.useEntrustedProcess) {
+ if (!this.processRuleList.length) {
+ this.loadTopProcessRules()
+ } else {
+ this.applyProcessRulePrefFromStorage()
+ }
+ } else {
+ this.genProcessRuleId = 0
+ }
+ this.voiceModalVisible = true
+ },
+ closeVoiceModal() {
+ this.resetVoiceState()
+ this.voiceModalVisible = false
+ },
+ onVoiceTranscriptInput(e) {
+ const v = (e && e.detail && e.detail.value) || ''
+ this.voiceTranscript = v
+ this.voiceTranscriptTrimmed = String(v || '').trim()
+ },
+ onSelectVoiceModeTap(e) {
+ const mode = e && e.currentTarget && e.currentTarget.dataset
+ ? e.currentTarget.dataset.mode
+ : ''
+ if (mode === 'dialectic' || mode === 'dictation') {
+ this.voiceMode = mode
+ }
+ },
+ /**
+ * 确保 RecorderManager 已初始化(mp3 16k,适配讯飞 lame)
+ */
+ ensureRecorder() {
+ if (this._voiceRecorder) return this._voiceRecorder
+ const rm = uni.getRecorderManager()
+ rm.onStop((res) => {
+ this.voiceRecording = false
+ const path = (res && res.tempFilePath) || ''
+ const duration = Number((res && res.duration) || 0)
+ if (!path) {
+ uni.showToast({ title: '未录到有效音频', icon: 'none' })
+ return
+ }
+ this.handleVoiceAsr(path, duration)
+ })
+ rm.onError(() => {
+ this.voiceRecording = false
+ uni.showToast({ title: '录音失败', icon: 'none' })
+ })
+ this._voiceRecorder = rm
+ return rm
+ },
+ onToggleVoiceRecord() {
+ if (this.voiceAsrLoading) return
+ if (this.voiceRecording) {
+ this.stopVoiceRecording(false)
+ return
+ }
+ uni.authorize({
+ scope: 'scope.record',
+ success: () => this.startVoiceRecording(),
+ fail: () => {
+ uni.showModal({
+ title: '需要录音权限',
+ content: '请在设置中开启麦克风权限后重试',
+ success: (r) => {
+ if (r.confirm) uni.openSetting({})
+ },
+ })
+ },
+ })
+ },
+ startVoiceRecording() {
+ const rm = this.ensureRecorder()
+ this.voiceRecording = true
+ rm.start({
+ duration: 60000,
+ sampleRate: 16000,
+ numberOfChannels: 1,
+ encodeBitRate: 96000,
+ format: 'mp3',
+ })
+ },
+ /**
+ * @param {boolean} discard 丢弃本次录音(关闭面板时)
+ */
+ stopVoiceRecording(discard) {
+ if (!this.voiceRecording && !this._voiceRecorder) return
+ if (discard && this._voiceRecorder) {
+ // 丢弃:先卸 onStop 再 stop,避免触发转写
+ try {
+ this._voiceRecorder.onStop(() => {})
+ } catch (e) {
+ /* ignore */
+ }
+ try {
+ this._voiceRecorder.stop()
+ } catch (e2) {
+ /* ignore */
+ }
+ this.voiceRecording = false
+ this._voiceRecorder = null
+ return
+ }
+ if (this.voiceRecording && this._voiceRecorder) {
+ this._voiceRecorder.stop()
+ }
+ },
+ async handleVoiceAsr(filePath, durationMs) {
+ this.voiceAsrLoading = true
+ try {
+ const res = await aiVoiceAsrApi({
+ filePath,
+ duration_ms: durationMs || 0,
+ })
+ // upload 拦截器可能直接返回字符串 JSON
+ let body = res
+ if (typeof res === 'string') {
+ try {
+ body = JSON.parse(res)
+ } catch (e) {
+ body = null
+ }
+ }
+ if (body && typeof body.data === 'string') {
+ try {
+ body = JSON.parse(body.data)
+ } catch (e2) {
+ /* keep */
+ }
+ }
+ if (body && body.code != null && Number(body.code) !== 0) {
+ throw new Error(body.message || body.msg || '语音转写失败')
+ }
+ const data = this.unwrapBiz(body != null ? body : res) || {}
+ const text = String(data.text || '').trim()
+ if (!text) {
+ uni.showToast({ title: '未识别到有效语音', icon: 'none' })
+ return
+ }
+ this.voiceTranscript = text
+ this.voiceTranscriptTrimmed = text
+ this.voiceAsrProvider = String(data.provider || 'xunfei')
+ this.voiceAsrDurationMs = Number(data.duration_ms || durationMs || 0)
+ uni.showToast({ title: '转写完成,请选择方式', icon: 'none' })
+ } catch (e) {
+ const msg = (e && e.message) || (e && e.msg) || '语音转写失败'
+ uni.showToast({ title: String(msg), icon: 'none' })
+ } finally {
+ this.voiceAsrLoading = false
+ }
+ },
+ /**
+ * 语音面板确认:A 辨证 / B 口述
+ */
+ async onConfirmVoiceGenerate() {
+ if (this.generating) return
+ const text = String(this.voiceTranscript || '').trim()
+ if (!text) {
+ uni.showToast({ title: '请先完成语音转写', icon: 'none' })
+ return
+ }
+ if (!this.voiceMode) {
+ uni.showToast({ title: '请选择辨证出方或口述落方', icon: 'none' })
+ return
+ }
+ if (this.isTcmRx && this.useEntrustedProcess && !this.genProcessRuleId) {
+ uni.showToast({ title: '请选择制剂要求', icon: 'none' })
+ return
+ }
+ const mode = this.voiceMode
+ const transcript = text
+ const asrProvider = this.voiceAsrProvider
+ const asrDuration = this.voiceAsrDurationMs
+ this.voiceModalVisible = false
+ if (mode === 'dialectic') {
+ this.aiChief = transcript
+ await this.runGenerateWithChief(transcript, {
+ voice_mode: 'dialectic',
+ transcript,
+ asr_provider: asrProvider,
+ asr_duration_ms: asrDuration,
+ })
+ return
+ }
+ await this.runVoiceDictationGenerate(transcript, asrProvider, asrDuration)
+ },
+ /**
+ * 抽取原 onGenerate 核心,供文字生成与语音辨证复用
+ */
+ async runGenerateWithChief(chief, voiceMeta) {
+ const type = Number(this.prescriptionType || 0)
+ if (!type) {
+ uni.showToast({ title: '请先选择处方类型', icon: 'none' })
+ return
+ }
+ this.beginGeneratingPlaceholder()
+ this.generating = true
+ this.multiPrescriptions = []
+ this.multiActiveIndex = 0
+ this.rebuildMultiTabView()
+ try {
+ const req = {
+ register_id: Number(this.registerId),
+ store_id: Number(this.storeId || 0) || undefined,
+ prescription_type: type,
+ chief_complaint: chief,
+ medical_record: Object.assign({}, this.localMr || {}, {
+ chief_complaint: chief,
+ }),
+ }
+ if (this.isTcmRx && this.useEntrustedProcess && this.genProcessRuleId) {
+ req.use_entrusted_process = 1
+ req.process_rule_id = this.genProcessRuleId
+ req.return_dosage_process_detail = this.returnDosageProcessDetail ? 1 : 0
+ }
+ if (voiceMeta && voiceMeta.voice_mode) {
+ req.voice_mode = voiceMeta.voice_mode
+ req.transcript = voiceMeta.transcript || chief
+ if (voiceMeta.asr_provider) req.asr_provider = voiceMeta.asr_provider
+ if (voiceMeta.asr_duration_ms) {
+ req.asr_duration_ms = voiceMeta.asr_duration_ms
+ }
+ }
+ const res = await aiGeneratePrescriptionApi(req)
+ await this.applyGenerateResponse(this.unwrapBiz(res) || {}, '生成')
+ } catch (e) {
+ this.endGeneratingPlaceholder()
+ this.resetPreview()
+ const msg = (e && e.message) || (e && e.msg) || '生成失败'
+ uni.showToast({ title: String(msg), icon: 'none' })
+ } finally {
+ this.generating = false
+ }
+ },
+ async runVoiceDictationGenerate(transcript, asrProvider, asrDuration) {
+ this.beginGeneratingPlaceholder()
+ this.generating = true
+ this.multiPrescriptions = []
+ this.multiActiveIndex = 0
+ this.rebuildMultiTabView()
+ try {
+ const req = {
+ register_id: Number(this.registerId),
+ store_id: Number(this.storeId || 0) || undefined,
+ prescription_type: Number(this.prescriptionType || 0),
+ transcript,
+ asr_provider: asrProvider || undefined,
+ asr_duration_ms: asrDuration || undefined,
+ }
+ if (this.isTcmRx && this.useEntrustedProcess && this.genProcessRuleId) {
+ req.use_entrusted_process = 1
+ req.process_rule_id = this.genProcessRuleId
+ req.return_dosage_process_detail = this.returnDosageProcessDetail ? 1 : 0
+ }
+ const res = await aiGenerateVoicePrescriptionApi(req)
+ await this.applyGenerateResponse(this.unwrapBiz(res) || {}, '口述识别')
+ } catch (e) {
+ this.endGeneratingPlaceholder()
+ this.resetPreview()
+ const msg = (e && e.message) || (e && e.msg) || '口述识别失败'
+ uni.showToast({ title: String(msg), icon: 'none' })
+ } finally {
+ this.generating = false
+ }
+ },
+ /**
+ * 统一处理生成/口述返回
+ */
+ async applyGenerateResponse(data, actionLabel) {
+ const durationText = this.formatDurationMs(data.duration_ms)
+ const list = Array.isArray(data.prescriptions) ? data.prescriptions : []
+ if (data.ok === false) {
+ this.endGeneratingPlaceholder()
+ this.resetPreview()
+ if (list.length > 1) {
+ this.multiPrescriptions = list
+ this.rebuildMultiTabView()
+ }
+ if (data.generation_id) {
+ await this.loadHistory()
+ this.activeId = Number(data.generation_id)
+ }
+ uni.showToast({
+ title: String(data.message || actionLabel + '未成功'),
+ icon: 'none',
+ })
+ return
+ }
+ await this.loadHistory()
+ if (list.length > 1) {
+ this.multiPrescriptions = list
+ let firstOk = 0
+ for (let i = 0; i < list.length; i++) {
+ if (list[i] && list[i].ok !== false) {
+ firstOk = i
+ break
+ }
+ }
+ this.applyMultiSlot(list[firstOk], firstOk)
+ let okN = 0
+ for (let i = 0; i < list.length; i++) {
+ if (list[i] && list[i].ok !== false) okN++
+ }
+ const failN = list.length - okN
+ const base =
+ failN > 0
+ ? '已生成 ' + okN + ' 套可用方案(' + failN + ' 套失败)'
+ : '已生成 ' + okN + ' 套方案'
+ uni.showToast({
+ title: durationText ? base + '(' + durationText + ')' : base,
+ icon: 'none',
+ })
+ return
+ }
+ this.activeId = Number(data.generation_id || 0)
+ this.applyMatch(data.match || null)
+ this.applyConflict(data.conflict || null)
+ this.applyRxMeta(data)
+ this.markReadIfNeeded(this.activeId, 0)
+ const unmatchedN = this.unmatchedCount || 0
+ let tip = durationText
+ ? '已' + actionLabel + '(' + durationText + ')'
+ : '已' + actionLabel + ',请确认导入'
+ if (unmatchedN > 0) {
+ tip += ';' + unmatchedN + ' 味未找到已标红'
+ }
+ uni.showToast({ title: tip, icon: 'none' })
+ },
onStartEditTap(e) {
const key = e && e.currentTarget && e.currentTarget.dataset
? e.currentTarget.dataset.key
@@ -1049,99 +1526,12 @@ export default {
uni.showToast({ title: '请填写主诉后再生成', icon: 'none' })
return
}
- const type = Number(this.prescriptionType || 0)
- if (!type) {
- uni.showToast({ title: '请先选择处方类型', icon: 'none' })
- return
- }
if (this.isTcmRx && this.useEntrustedProcess && !this.genProcessRuleId) {
uni.showToast({ title: '请选择制剂要求', icon: 'none' })
return
}
- // 先关二级弹窗,回主抽屉;取消旧选中并插入生成中占位
this.closeGenModal()
- this.beginGeneratingPlaceholder()
- this.generating = true
- this.multiPrescriptions = []
- this.multiActiveIndex = 0
- this.rebuildMultiTabView()
- try {
- const req = {
- register_id: Number(this.registerId),
- store_id: Number(this.storeId || 0) || undefined,
- prescription_type: type,
- chief_complaint: chief,
- medical_record: Object.assign({}, this.localMr || {}, {
- chief_complaint: chief,
- }),
- }
- if (this.isTcmRx && this.useEntrustedProcess && this.genProcessRuleId) {
- req.use_entrusted_process = 1
- req.process_rule_id = this.genProcessRuleId
- req.return_dosage_process_detail = this.returnDosageProcessDetail ? 1 : 0
- }
- const res = await aiGeneratePrescriptionApi(req)
- const data = this.unwrapBiz(res) || {}
- const durationText = this.formatDurationMs(data.duration_ms)
- const list = Array.isArray(data.prescriptions) ? data.prescriptions : []
- // 业务软失败:HTTP 成功但 ok=false,toast 警告而非失败
- if (data.ok === false) {
- this.endGeneratingPlaceholder()
- this.resetPreview()
- if (list.length > 1) {
- this.multiPrescriptions = list
- this.rebuildMultiTabView()
- }
- if (data.generation_id) {
- await this.loadHistory()
- this.activeId = Number(data.generation_id)
- }
- uni.showToast({ title: String(data.message || '生成未成功'), icon: 'none' })
- return
- }
- await this.loadHistory()
- if (list.length > 1) {
- this.multiPrescriptions = list
- let firstOk = 0
- for (let i = 0; i < list.length; i++) {
- if (list[i] && list[i].ok !== false) {
- firstOk = i
- break
- }
- }
- this.applyMultiSlot(list[firstOk], firstOk)
- let okN = 0
- for (let i = 0; i < list.length; i++) {
- if (list[i] && list[i].ok !== false) okN++
- }
- const failN = list.length - okN
- const base =
- failN > 0
- ? '已生成' + okN + '套可用(' + failN + '套失败)'
- : '已生成' + okN + '套方案'
- uni.showToast({
- title: durationText ? base + '(' + durationText + ')' : base,
- icon: 'none',
- })
- return
- }
- this.activeId = Number(data.generation_id || 0)
- this.applyMatch(data.match || null)
- this.applyConflict(data.conflict || null)
- this.applyRxMeta(data)
- this.markReadIfNeeded(this.activeId, 0)
- uni.showToast({
- title: durationText ? ('已生成(耗时 ' + durationText + ')') : '已生成',
- icon: 'success',
- })
- } catch (e) {
- this.endGeneratingPlaceholder()
- this.resetPreview()
- const msg = (e && e.message) || (e && e.msg) || '生成失败'
- uni.showToast({ title: String(msg), icon: 'none' })
- } finally {
- this.generating = false
- }
+ await this.runGenerateWithChief(chief)
},
onSelectHistoryTap(e) {
const raw = e && e.currentTarget && e.currentTarget.dataset
@@ -1308,6 +1698,13 @@ export default {
uni.showToast({ title: '请先勾选已对照药品', icon: 'none' })
return
}
+ const skipN = this.unmatchedCount || 0
+ if (skipN > 0) {
+ uni.showToast({
+ title: '已跳过' + skipN + '味未对照药品',
+ icon: 'none',
+ })
+ }
this.$emit('import', {
rows: drugs,
// 中药导入必须带剂数/每日次数,缺省用处方页默认 7/2
@@ -1317,6 +1714,7 @@ export default {
process_rule_id: this.rxProcessRuleId > 0 ? this.rxProcessRuleId : undefined,
child_process_rule_id: this.rxChildProcessRuleId > 0 ? this.rxChildProcessRuleId : undefined,
process_rule_note_id: this.rxProcessRuleNoteId > 0 ? this.rxProcessRuleNoteId : undefined,
+ generation_id: Number(this.activeId || 0) > 0 ? Number(this.activeId) : undefined,
})
this.visible = false
},
@@ -1575,6 +1973,40 @@ export default {
border-radius: 12rpx;
border-bottom: none;
}
+.ai-match-row--danger {
+ margin-bottom: 12rpx;
+ padding: 16rpx;
+ border-radius: 12rpx;
+ border: 1px solid rgba(239, 68, 68, 0.4);
+ box-shadow: 0 0 6px rgba(239, 68, 68, 0.18);
+ border-bottom: none;
+ background: rgba(239, 68, 68, 0.06);
+}
+.ai-unmatched-title {
+ color: #ef4444;
+}
+.ai-unmatched-name {
+ color: #ef4444;
+ font-weight: 600;
+}
+.ai-mode-card {
+ padding: 24rpx;
+ border-radius: 16rpx;
+ border: 1px solid #e5e7eb;
+ background: #fff;
+}
+.ai-mode-card--on {
+ border: 1px solid rgba(106, 205, 187, 0.45);
+ box-shadow: 0 0 6px rgba(106, 205, 187, 0.22);
+ background: rgba(106, 205, 187, 0.08);
+}
+.ai-btn--recording {
+ border-color: #ef4444 !important;
+ color: #ef4444 !important;
+}
+.ai-voice-rec {
+ padding: 8rpx 0;
+}
.ai-match-name {
flex: 1;
min-width: 0;
diff --git a/subPackages/sub_workbench/prescription_v2/index.vue b/subPackages/sub_workbench/prescription_v2/index.vue
index ff47d06..5c2a71c 100644
--- a/subPackages/sub_workbench/prescription_v2/index.vue
+++ b/subPackages/sub_workbench/prescription_v2/index.vue
@@ -628,6 +628,14 @@
✦
AI辅助出方
+
+ 音
+ 语音开方
+