yzx 1 天之前
父节点
当前提交
3c13f05c65

+ 2 - 0
.dbg/xfvoiceclone-403.env

@@ -0,0 +1,2 @@
+DEBUG_SERVER_URL=http://127.0.0.1:7777/event
+DEBUG_SESSION_ID=xfvoiceclone-403

+ 45 - 0
debug-xfvoiceclone-403.md

@@ -0,0 +1,45 @@
+# Debug Session: xfvoiceclone-403 [OPEN]
+
+## Context
+- Endpoint: `POST /aicall/xfvoiceclone/ttsTest`
+- Symptom: clone trial synthesis returns `HTTP 403/401` with HMAC date header verification errors
+- Scope: GUI controller `XfVoiceCloneController`
+
+## Hypotheses
+1. Deployment machine time or timezone is skewed, so the RFC1123 `Date` is rejected.
+2. OkHttp WebSocket handshake does not actually send the expected `Date/Authorization/Host` headers.
+3. The `voice_clone` gateway expects a stricter auth format than the current query/header combination.
+4. The deployed service is still running older controller code.
+
+## Plan
+1. Add instrumentation only, no business logic change.
+2. Capture runtime auth shape before WebSocket connect.
+3. Ask user to reproduce once and collect logs.
+4. Confirm or reject hypotheses from evidence, then apply minimal fix.
+
+## Evidence
+- 2026-06-05 runtime log:
+  - `clone websocket auth built ... date=Fri, 5 Jun 2026 02:04:03 GMT ...`
+  - `clone websocket failure ... HTTP 403 ... valid date or x-date header is required ...`
+- This shows the app did run new code and generated a Date value, but the gateway still rejected it as invalid.
+- 2026-06-05 second runtime log after local formatter fix attempt:
+  - `clone websocket auth built ... date=Fri, 5 Jun 2026 02:12:44 GMT ...`
+  - `clone websocket failure ... HTTP 403 ... valid date or x-date header is required ...`
+- The log still shows single-digit day (`5`) instead of expected two-digit day (`05`), so the newest formatter change was not reflected in the deployed runtime.
+
+## Hypothesis Status
+- H1 Time/zone skew: not confirmed by current evidence; system zone/time look plausible.
+- H2 Request headers missing: weakened, because runtime shows Date is generated and attached by current code path.
+- H3 Auth format/date format mismatch: still plausible, but cannot be re-tested until latest formatter change is actually deployed.
+- H4 Old code deployed: rejected for early instrumentation, but re-opened for the latest formatter fix because runtime still prints old date format.
+
+## Fix Attempt
+- Force clone auth Date format to English RFC1123-like string with two-digit day:
+  - `EEE, dd MMM yyyy HH:mm:ss 'GMT'`
+- After deployment, handshake succeeded with `HTTP 101`, so HMAC/date issue is considered resolved.
+- New strongest hypothesis after handshake success:
+  - clone payload text must be base64 and GUI should also read clone response code from `header.code`.
+- Applied minimal fix:
+  - encode `payload.text.text` with base64
+  - read message code from `header.code` first, fallback to root `code`
+  - add `onMessage/onClosed` runtime logs for verification

+ 81 - 0
ruoyi-admin/src/main/resources/templates/cc/txasr1bridgeconf/txasr1bridgeconf.html

@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<html lang="zh" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
+<head>
+    <th:block th:include="include :: header('腾讯ASR1参数配置')" />
+    <th:block th:include="include :: layout-latest-css" />
+    <th:block th:include="include :: ztree-css" />
+    <style>
+    </style>
+</head>
+
+<body>
+<div class="main-content">
+    <div class="h4 form-header" th:text="#{switchconf.asr.tx1.header}"></div>
+    <div id="baseConfigs"></div>
+
+    <div class="row"></div>
+    <div class="row">
+        <div class="col-sm-offset-5 col-sm-10">
+            <button id="saveConfig" type="button" class="btn btn-sm btn-primary" onclick="submitHandler()"><i class="fa fa-check"></i><span th:text="#{switchconf.btn.save}"></span></button>&nbsp;
+        </div>
+    </div>
+</div>
+
+<th:block th:include="include :: footer" />
+<th:block th:include="include :: layout-latest-js" />
+<script>
+    var prefix = ctx + "cc/fsconf";
+    $(function() {
+        $.ajax({
+            url: prefix + '/getTxBridgeAsr1Conf',
+            type: 'GET',
+            success: function(_data) {
+                var baseConfigsHtml = '';
+                var data = _data.data;
+
+                $.each(data, function(index, config) {
+                    baseConfigsHtml += '<div className="row">';
+                    baseConfigsHtml += '<div class="col-sm-12">';
+                    baseConfigsHtml += '<label class="col-sm-2 control-label">' + config.aliasName + '</label><div class="col-sm-10"><input class="config-value form-control" type="text" id="' + config.name + '" value="' + config.value + '" /></div>';
+                    baseConfigsHtml += '</div>';
+                    baseConfigsHtml += '</div>';
+                });
+                $('#baseConfigs').html(baseConfigsHtml);
+            },
+            error: function(error) {
+                console.error('Error fetching configuration:', error);
+            }
+        });
+    });
+
+    $('#saveConfig').click(function() {
+        var configs = [];
+        $('input.config-value').each(function() {
+            var config = {
+                name: $(this).attr('id'),
+                value: $(this).val().trim()
+            };
+            configs.push(config);
+        });
+        $.ajax({
+            url: prefix + '/setTxBridgeAsr1Conf',
+            type: 'POST',
+            contentType: 'application/json',
+            data: JSON.stringify(configs),
+            beforeSend: function () {
+                $.modal.loading(i18n("common.tip.loading"));
+                $.modal.disable();
+            },
+            success: function(response) {
+                processAjaxReponseJson(response);
+            },
+            error: function(error) {
+            }
+        });
+    });
+
+</script>
+
+</body>
+</html>
+

+ 80 - 0
ruoyi-admin/src/main/resources/templates/cc/txasrbridgeconf/txasrbridgeconf.html

@@ -0,0 +1,80 @@
+<!DOCTYPE html>
+<html lang="zh" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
+<head>
+    <th:block th:include="include :: header('腾讯ASR参数配置')" />
+    <th:block th:include="include :: layout-latest-css" />
+    <th:block th:include="include :: ztree-css" />
+    <style>
+    </style>
+</head>
+
+<body>
+<div class="main-content">
+    <div class="h4 form-header" th:text="#{switchconf.asr.tx.header}"></div>
+    <div id="baseConfigs"></div>
+
+    <div class="row"></div>
+    <div class="row">
+        <div class="col-sm-offset-5 col-sm-10">
+            <button id="saveConfig" type="button" class="btn btn-sm btn-primary" onclick="submitHandler()"><i class="fa fa-check"></i><span th:text="#{switchconf.btn.save}"></span></button>&nbsp;
+        </div>
+    </div>
+</div>
+
+<th:block th:include="include :: footer" />
+<th:block th:include="include :: layout-latest-js" />
+<script>
+    var prefix = ctx + "cc/fsconf";
+    $(function() {
+        $.ajax({
+            url: prefix + '/getTxBridgeAsrConf',
+            type: 'GET',
+            success: function(_data) {
+                var baseConfigsHtml = '';
+                var data = _data.data;
+
+                $.each(data, function(index, config) {
+                    baseConfigsHtml += '<div className="row">';
+                    baseConfigsHtml += '<div class="col-sm-12">';
+                    baseConfigsHtml += '<label class="col-sm-2 control-label">' + config.aliasName + '</label><div class="col-sm-10"><input class="config-value form-control" type="text" id="' + config.name + '" value="' + config.value + '" /></div>';
+                    baseConfigsHtml += '</div>';
+                    baseConfigsHtml += '</div>';
+                });
+                $('#baseConfigs').html(baseConfigsHtml);
+            },
+            error: function(error) {
+                console.error('Error fetching configuration:', error);
+            }
+        });
+    });
+
+    $('#saveConfig').click(function() {
+        var configs = [];
+        $('input.config-value').each(function() {
+            var config = {
+                name: $(this).attr('id'),
+                value: $(this).val().trim()
+            };
+            configs.push(config);
+        });
+        $.ajax({
+            url: prefix + '/setTxBridgeAsrConf',
+            type: 'POST',
+            contentType: 'application/json',
+            data: JSON.stringify(configs),
+            beforeSend: function () {
+                $.modal.loading(i18n("common.tip.loading"));
+                $.modal.disable();
+            },
+            success: function(response) {
+                processAjaxReponseJson(response);
+            },
+            error: function(error) {
+            }
+        });
+    });
+
+</script>
+
+</body>
+</html>

+ 83 - 0
sql/v20260614_tx_asr.sql

@@ -0,0 +1,83 @@
+-- 2026-06-14
+-- Add Tencent realtime ASR bridge configuration for mod_tx_asr.
+
+DELETE FROM `sys_config`
+WHERE `config_key` = 'config_asr_provider_tx';
+
+INSERT INTO `sys_config` (`config_name`, `config_key`, `config_value`, `config_type`, `create_by`, `create_time`, `remark`)
+VALUES ('腾讯ASR', 'config_asr_provider_tx', 'tx', 'Y', 'admin', NOW(), 'ASR厂商-mod_tx_asr');
+
+DELETE FROM `sys_role_menu`
+WHERE `menu_id` = 4023;
+
+DELETE FROM `sys_menu`
+WHERE `menu_id` = 4023
+   OR `perms` = 'cc:txasrbridgeconf:view'
+   OR `url` = '/cc/fsconf/txasrbridgeconf';
+
+INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `menu_code`, `parent_id`, `order_num`, `url`, `target`, `menu_type`, `visible`, `is_refresh`, `perms`, `icon`, `create_by`, `create_time`, `remark`)
+VALUES (4023, '腾讯ASR配置', 'txAsrBridgeConf', 3018, 8, '/cc/fsconf/txasrbridgeconf', 'menuItem', 'C', '0', '1', 'cc:txasrbridgeconf:view', '#', 'admin', NOW(), 'mod_tx_asr 参数配置菜单');
+
+INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
+VALUES (2, 4023);
+
+DELETE FROM `fs_variables`
+WHERE `var_field_name` IN (
+    'appid',
+    'secret-id',
+    'secret-key',
+    'websocket-host',
+    'engine-model-type',
+    'voice-format',
+    'need-vad',
+    'filter-dirty',
+    'filter-modal',
+    'filter-punc',
+    'filter-empty-result',
+    'convert-num-mode',
+    'word-info',
+    'vad-silence-time',
+    'max-speak-time',
+    'hotword-id',
+    'hotword-list',
+    'customization-id',
+    'noise-threshold',
+    'signature-expire-seconds'
+)
+AND `cat` = 5;
+
+INSERT INTO `fs_variables` (`id`, `cat`, `var_field_name`, `var_field_alias`) VALUES
+(400, 5, 'appid', '腾讯 ASR AppID'),
+(401, 5, 'secret-id', '腾讯 ASR SecretId'),
+(402, 5, 'secret-key', '腾讯 ASR SecretKey'),
+(403, 5, 'websocket-host', 'WebSocket Host'),
+(404, 5, 'engine-model-type', '引擎模型'),
+(405, 5, 'voice-format', '音频编码格式'),
+(406, 5, 'need-vad', '启用 VAD'),
+(407, 5, 'filter-dirty', '脏词过滤'),
+(408, 5, 'filter-modal', '语气词过滤'),
+(409, 5, 'filter-punc', '标点过滤'),
+(410, 5, 'filter-empty-result', '过滤空识别结果'),
+(411, 5, 'convert-num-mode', '数字转换模式'),
+(412, 5, 'word-info', '词级时间戳'),
+(413, 5, 'vad-silence-time', '句尾静音时长(毫秒)'),
+(414, 5, 'max-speak-time', '强制断句时长(毫秒)'),
+(415, 5, 'hotword-id', '热词表 ID'),
+(416, 5, 'hotword-list', '临时热词表'),
+(417, 5, 'customization-id', '自学习模型 ID'),
+(418, 5, 'noise-threshold', '噪音阈值'),
+(419, 5, 'signature-expire-seconds', '签名有效期(秒)');
+
+DELETE FROM `cc_asr_languages`
+WHERE `asr_provider` = 'tx';
+
+INSERT INTO `cc_asr_languages` (`asr_provider`, `models`, `language_code`, `language_name`) VALUES
+('tx', '8k_zh', 'zh-CN', '中文(8k)'),
+('tx', '16k_zh', 'zh-CN', '中文(16k)'),
+('tx', '8k_en', 'en-US', '英文(8k)'),
+('tx', '16k_en', 'en-US', '英文(16k)'),
+('tx', '16k_yue', 'zh-HK', '粤语');
+
+UPDATE `cc_params`
+SET `param_name` = '双向asr语音识别,使用哪个asr引擎(chinatelecom/funasr/aliyun/ali/tx)'
+WHERE `param_code` = 'fs_call_asr_engine';

+ 86 - 0
sql/v20260615_tx_asr1.sql

@@ -0,0 +1,86 @@
+-- 2026-06-15
+-- Add Tencent MPS ASR bridge configuration for mod_tx_asr1.
+
+DELETE FROM `sys_config`
+WHERE `config_key` = 'config_asr_provider_tx1';
+
+INSERT INTO `sys_config` (`config_name`, `config_key`, `config_value`, `config_type`, `create_by`, `create_time`, `remark`)
+VALUES ('腾讯ASR1', 'config_asr_provider_tx1', 'tx1', 'Y', 'admin', NOW(), 'ASR厂商-mod_tx_asr1');
+
+DELETE FROM `sys_role_menu`
+WHERE `menu_id` = 4024;
+
+DELETE FROM `sys_menu`
+WHERE `menu_id` = 4024
+   OR `perms` = 'cc:txasr1bridgeconf:view'
+   OR `url` = '/cc/fsconf/txasr1bridgeconf';
+
+INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `menu_code`, `parent_id`, `order_num`, `url`, `target`, `menu_type`, `visible`, `is_refresh`, `perms`, `icon`, `create_by`, `create_time`, `remark`)
+VALUES (4024, '腾讯ASR1配置', 'txAsr1BridgeConf', 3018, 9, '/cc/fsconf/txasr1bridgeconf', 'menuItem', 'C', '0', '1', 'cc:txasr1bridgeconf:view', '#', 'admin', NOW(), 'mod_tx_asr1 参数配置菜单');
+
+INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
+VALUES (2, 4024);
+
+DELETE FROM `fs_variables`
+WHERE `var_field_name` IN (
+    'appid',
+    'secret-id',
+    'secret-key',
+    'websocket-host',
+    'asr-dst',
+    'trans-src',
+    'trans-dst',
+    'fragment-notify',
+    'result-type',
+    'steady-text-trans',
+    'steady-result',
+    'speaker-diarization',
+    'timeout-sec',
+    'max-speak-ms',
+    'hot-words-list',
+    'res-id',
+    'audio-src-id',
+    'packet-format',
+    'sample-rate',
+    'send-chunk-ms',
+    'signature-expire-seconds',
+    'verify-peer'
+)
+AND `cat` = 5;
+
+INSERT INTO `fs_variables` (`cat`, `var_field_name`, `var_field_alias`) VALUES
+(5, 'appid', 'AppID'),
+(5, 'secret-id', 'SecretId'),
+(5, 'secret-key', 'SecretKey'),
+(5, 'websocket-host', 'WebSocket Host'),
+(5, 'asr-dst', '识别目标语言'),
+(5, 'trans-src', '翻译源语言'),
+(5, 'trans-dst', '翻译目标语言'),
+(5, 'fragment-notify', '片段通知'),
+(5, 'result-type', '结果类型'),
+(5, 'steady-text-trans', '稳态翻译文本'),
+(5, 'steady-result', '稳态结果'),
+(5, 'speaker-diarization', '说话人分离'),
+(5, 'timeout-sec', '空闲超时(秒)'),
+(5, 'max-speak-ms', '最大说话时长(毫秒)'),
+(5, 'hot-words-list', '热词列表'),
+(5, 'res-id', '资源 ID'),
+(5, 'audio-src-id', '音频源 ID'),
+(5, 'packet-format', '音频格式'),
+(5, 'sample-rate', '采样率'),
+(5, 'send-chunk-ms', '发送分片时长(毫秒)'),
+(5, 'signature-expire-seconds', '签名有效期(秒)'),
+(5, 'verify-peer', '校验证书');
+
+DELETE FROM `cc_asr_languages`
+WHERE `asr_provider` = 'tx1';
+
+INSERT INTO `cc_asr_languages` (`asr_provider`, `models`, `language_code`, `language_name`) VALUES
+('tx1', 'zh', 'zh-CN', '中文'),
+('tx1', 'en', 'en-US', '英文'),
+('tx1', 'ja', 'ja-JP', '日文'),
+('tx1', 'ko', 'ko-KR', '韩文');
+
+UPDATE `cc_params`
+SET `param_name` = '双向asr语音识别,使用哪个asr引擎(chinatelecom/funasr/aliyun/ali/tx/tx1)'
+WHERE `param_code` = 'fs_call_asr_engine';