Kaynağa Gözat

feat(nls): 两个壳组件切到阿里云 NLS 实现(用户:「切」)

改动只有两处、每处两行:
- BusinessAssistantPC.vue / BusinessAssistantMobile.vue:
  import ASR 从 '@/three-libs/asr/index' 改为 '@/three-libs/asr/nls'
- 构造函数里去掉硬编码的厂商 appKey(new ASR(undefined, {...}))——NLS 的 appkey 由服务端
  端点下发,前端不再内置任何云凭据
- 其余一行未改:NlsAsr 的对外契约与旧 SDK 一致(openMicroPhone/startRecord/openSocket/
  transform/close/clear + transform/error 事件 + 暴露 audioContext/source 供 PC 画波形)

验证:verify-nls-realtime.mjs 30 → 39/39(新增【4】9 项源码级断言:两壳 import 的是 nls、
不再 import 旧 SDK、不再 new ASR("…") 硬编码、六个调用点齐全、旧实现仍在作退路);
npm run build 通过;vue-tsc 无新错误。
产物核对:三个云凭据 + 旧厂商 appKey 在 dist/ 里全部 0 命中,nls-gateway-cn-shanghai
已进 main bundle(新实现确实被打进去)。

⚠️ 浏览器实测未做(harness 无浏览器自动化、麦克风也只能真人说):需用户点一次麦克风确认
①边说边出字 ②停顿后定稿 ③再点停止文本保留 ④桌面端波形在动。在用户确认前功能不算 passing。

Co-Authored-By: Claude Code <noreply@anthropic.com>
gongtianxiao 1 ay önce
ebeveyn
işleme
1daa1d1ab0

+ 14 - 1
README.md

@@ -422,7 +422,20 @@
     `harness/tools/get-nls-token.mjs` 与 dev 端点共用;带 5 分钟提前刷新 + 并发去重
   - 验证:端点 200;**拿端点发的 token 真调通一次识别**(`status:20000000 SUCCESS`);
     `npm run build` 后**产物里搜三个凭据值 0 命中**
-  - ⚠️ 生产要由后端/网关做同样形态的端点(已登记技术债 #10);前端侧接 NLS 实时识别是下一步
+- **实时语音识别已切到阿里云 NLS**(2026-09-23,接管旧厂商 SDK):
+  - 协议层 `src/three-libs/asr/nls-protocol.ts`(纯函数:帧构建 / 事件折叠 / 16k PCM 编码)
+    + 类 `src/three-libs/asr/nls.ts`,**对外契约与旧 SDK 一致** → 两个壳组件只改了 import
+    与一处构造参数(去掉硬编码 appKey),其余一行没动
+  - **协议是实测出来的**:`StartTranscription`→`TranscriptionStarted`→二进制音频帧→
+    `TranscriptionResultChanged`(中间结果,会被覆盖)→`SentenceEnd`(整句定稿)→`TranscriptionCompleted`;
+    两条硬约束——**每条指令都要带 appkey**、**task_id 不能是空串**(各踩过一次,已写成断言)
+  - 拼接全文只用 `SentenceEnd`;中间结果只用于实时显示
+  - 验证:`harness/tools/verify-nls-realtime.mjs` **39/39**,含**真实识别**(Windows TTS 造语音 →
+    推流给真实网关 → 识别结果与所说话一致)与切线的源码级断言
+  - 重采样按实际 `audioContext.sampleRate` 算(旧实现写死 44100 的隐患不再沿用)
+  - 文档:`harness/docs/reference/asr-nls.md`(旧的 `asr-realtime.md` 标注为"换之前那套")
+  - ⚠️ 生产要由后端/网关做同样形态的 `/nls-token/` 端点(已登记技术债 #10);
+    **浏览器实测由用户确认**(harness 不能替真人说话)
 - **实时语音转文字补了一份接口文档**(用户 2026-09-23 要求):
   [`harness/docs/reference/asr-realtime.md`](harness/docs/reference/asr-realtime.md) ——
   SDK 在仓库里(`src/three-libs/asr/index.ts`),文档逐条对源码核对:构造参数与方法、

+ 8 - 4
harness/docs/reference/asr-nls.md

@@ -116,9 +116,13 @@ node harness/tools/probe-nls-realtime.mjs --say="青浦区高新技术企业奖
 
 ## 7. 现状与未决
 
-- ✅ 协议层、类、token 通路都已实现并**打真实网关验证通过**(30/30,含真实识别)
-- ⏳ **壳组件还没切过来**:`BusinessAssistantPC.vue` / `BusinessAssistantMobile.vue`
-  目前仍 `import ASR from '@/three-libs/asr/index'`(旧实现)。切换只需改 import
-  (契约一致),切完请在浏览器里点一次麦克风实测
+- ✅ 协议层、类、token 通路都已实现并**打真实网关验证通过**(39/39,含真实识别)
+- ✅ **两个壳组件已切过来**(2026-09-23):`BusinessAssistantPC.vue` / `BusinessAssistantMobile.vue`
+  改为 `import ASR from '@/three-libs/asr/nls'`,并把构造函数里的硬编码 appKey 去掉
+  (appkey 由端点下发)。契约一致,所以除了 import 与那一处构造参数,**其余一行没改**
+  —— 有源码级断言守着(`verify-nls-realtime.mjs` 的【4】)
+- ⏳ **需要用户在浏览器实测一次**:点麦克风说一句 → 输入框应边说边出字;再点一次停止 →
+  文本保留。桌面端还应看到波形在动(波形走的是 `audioContext`/`source`,契约没变)
 - ⏳ **生产要由后端/网关提供 `/nls-token/`**(技术债 #10),否则线上拿不到 token
 - 📌 旧实现(`asr/index.ts`)**保留**:切回它只改 import,且它承载着"厂商中转"那条退路
+  (旧实现依赖的 `VITE_ASR` / `VITE_XF_ASR` 仍在四份 env 里)

+ 20 - 0
harness/progress.md

@@ -4083,6 +4083,26 @@ payload 里含模型里**不存在**的列时,`addContent` 会忽略该列照
 (旧厂商实现)。切换只改 import(契约一致),但**必须由用户在浏览器点一次麦克风实测**才能算完成。
 旧实现保留,切回去也只改 import。
 
+### 续 34:把两个壳组件切到 NLS(用户:「切」)
+
+**改了什么**(两处、每处两行):
+- `BusinessAssistantPC.vue` / `BusinessAssistantMobile.vue`:
+  `import ASR from '@/three-libs/asr/index'` → `'@/three-libs/asr/nls'`
+- 构造函数里的**硬编码厂商 appKey 去掉**(`new ASR(undefined, {audio:true,video:false})`)——
+  NLS 的 appkey 由端点下发,前端不再内置任何云凭据
+- 其余**一行没改**:契约一致(`openMicroPhone` / `startRecord` / `openSocket` / `transform` /
+  `close` / `clear` + `transform`/`error` 事件 + `audioContext`/`source` 供 PC 画波形)
+
+**验证**:`verify-nls-realtime.mjs` 30 → **39/39**(新增【4】9 项**源码级**断言:
+两个壳确实 import 的是 nls、不再 import 旧 SDK、不再 `new ASR("…")` 硬编码、六个调用点齐全、
+旧实现仍在仓库里作退路)。`npm run build` 通过,`vue-tsc` 无新错误。
+**产物核对**:三个云凭据 + 旧厂商 appKey 在 `dist/` 里**全部 0 命中**(连旧的也清掉了),
+而 `nls-gateway-cn-shanghai` 已进 main bundle(新实现确实被打进去)。
+
+**⚠️ 仍算未完成的部分**:**浏览器实测没做**(harness 里没有浏览器自动化,麦克风也只能真人说)。
+请用户点一次麦克风:① 边说边出字(中间结果)② 说话停顿后那句定稿 ③ 再点一次停止文本保留
+④ 桌面端波形在动。**在用户确认之前,功能状态不能算 passing。**
+
 ### 续 31:把实时语音转文字整理成接口文档(用户要求)
 
 **用户要求**:「将当前实时语音转文字的功能整理成一份 API 文档放在 harness 文件夹里面」。

+ 25 - 0
harness/tools/verify-nls-realtime.mjs

@@ -237,5 +237,30 @@ if (!credentials.ak || !credentials.sk || !credentials.appkey) {
   check('识别结果长度合理(≥ 8 字)', text.length >= 8, String(text.length));
 }
 
+// ── 4. 接线(源码级:这段是 Vue 壳组件里的事,node 跑不起来)────────────────
+console.log('\n【4】两个壳组件已切到 NLS 实现(源码级断言)');
+{
+  const { readFileSync } = await import('node:fs');
+  for (const file of [
+    'src/components/BusinessAssistantPC.vue',
+    'src/components/BusinessAssistantMobile.vue',
+  ]) {
+    const source = readFileSync(file, 'utf8');
+    check(`${file}:import 的是 NLS 实现`,
+      /import ASR from '@\/three-libs\/asr\/nls';/.test(source));
+    check(`${file}:不再 import 旧的厂商 SDK`,
+      !/import ASR from '@\/three-libs\/asr\/index';/.test(source));
+    check(`${file}:不再硬编码 appKey(改由端点下发)`,
+      !/new ASR\("/.test(source) && /new ASR\(undefined/.test(source));
+    // 契约没变:这几个调用点必须还在(换了实现就得跟着改)
+    check(`${file}:openMicroPhone / startRecord / openSocket / transform 调用点齐全`,
+      /openMicroPhone\(\)/.test(source) && /startRecord\(\)/.test(source) &&
+        /openSocket\(\)/.test(source) && /\.transform\(\)/.test(source) &&
+        /\.close\(\)/.test(source) && /\.clear\(\)/.test(source));
+  }
+  // 旧实现仍在仓库里(切回去的退路),别被顺手删掉
+  check('旧的厂商 SDK 仍在(切回去只改 import)', existsSync('src/three-libs/asr/index.ts'));
+}
+
 console.log(`\n===== 通过 ${pass} 项,失败 ${fail} 项 =====`);
 process.exit(fail ? 1 : 0);

+ 6 - 2
src/components/BusinessAssistantMobile.vue

@@ -404,7 +404,10 @@ import {
   maybeStopAutoScrollAtLastAiRowTop,
 } from './business-assistant/auto-scroll-top-stop';
 import { scrollBusinessAssistantListToBottom } from './business-assistant/scroll-list-bottom';
-import ASR from '@/three-libs/asr/index';
+// 实时语音识别:**阿里云 NLS 直连**(2026-09-23 换)。
+// 对外契约与旧厂商 SDK(`@/three-libs/asr/index`)一致,所以这里只有这一行变了;
+// 切回旧实现也只改这一行。详见 harness/docs/reference/asr-nls.md
+import ASR from '@/three-libs/asr/nls';
 import AccountInfo from "./Chat/AccountInfo.vue";
 import { useEnterpriseAuth } from '@/components/useEnterpriseAuth';
 import { createModalMutex, MODAL_MUTEX_KEY } from './business-assistant/modal-mutex';
@@ -1115,7 +1118,8 @@ const toggleVoiceInput = async () => {
   isVoiceInputStarting.value = true;
 
   try {
-    asrInstance = new ASR("ce6cb2020037ed20f8a09a4822e869f380a0de96", {
+    // 不传 appKey:NLS 的 appkey 由服务端端点下发(前端不再硬编码厂商/云凭据)
+    asrInstance = new ASR(undefined, {
       audio: true,
       video: false,
     });

+ 6 - 2
src/components/BusinessAssistantPC.vue

@@ -346,7 +346,10 @@ import { useResizeObserver } from '@vueuse/core';
 import BusinessRecord from "./Chat/BusinessRecord.vue";
 // [已停用] import Toast from "./Common/Toast.vue";
 import ScrollList from './Common/ScrollList.vue';
-import ASR from '@/three-libs/asr/index';
+// 实时语音识别:**阿里云 NLS 直连**(2026-09-23 换)。
+// 对外契约与旧厂商 SDK(`@/three-libs/asr/index`)一致,所以这里只有这一行变了;
+// 切回旧实现也只改这一行。详见 harness/docs/reference/asr-nls.md
+import ASR from '@/three-libs/asr/nls';
 import ParticleBackground from "./Common/ParticleBackground.vue";
 import { useBusinessAssistantChat } from './business-assistant/useBusinessAssistantChat';
 import { useBusinessAssistantUpload } from './business-assistant/useBusinessAssistantUpload';
@@ -1013,7 +1016,8 @@ const toggleVoiceInput = async () => {
 
   try {
     if (!asrInstance) {
-      asrInstance = new ASR("ce6cb2020037ed20f8a09a4822e869f380a0de96", {
+      // 不传 appKey:NLS 的 appkey 由服务端端点下发(前端不再硬编码厂商/云凭据)
+      asrInstance = new ASR(undefined, {
         audio: true,
         video: false,
       });