ソースを参照

fix(html): 修「界面卡在正在初始化」——上一轮删出的括号不配对 + 补内联脚本语法检查

用户反馈(截图:整页模糊 + 「正在初始化,请稍候...」):「现在界面有问题」。

根因:上一提交删 initToken / else 分支时,index.html 的内联脚本里留下一个不配对的右花括号
→ 内联脚本整体语法错误 → 一加载就抛 → globalThis.hasSequence 没置位、sequenceLoaded 事件
没派发 → BusinessAssistant.vue 的 initSequence() 一直等事件 → 加载遮罩永远不隐藏。

为什么没被验证抓到(教训):npm run build 不解析 index.html 里那种普通 script 块
(Vite 只处理 type=module 的,其余原样拷贝)→ 带语法错误的页面构建照样绿;
而上一轮跑的脚本都不看 index.html 的内联脚本,所以"验证通过"是假的。

修法:
- 删掉多余的右花括号(修后三块内联脚本 node --check 全过)
- 新增 harness/tools/verify-inline-scripts.mjs:抽 dist/*.html 的内联脚本用 new vm.Script()
  编译(只编译不执行),语法错即 FAIL;顺带报出产物里未替换的 %VITE_*% 占位符
  (VITE_DMS_API 属技术债 #9 的已知项,单独提示不算失败)
- 写进 sops/verification.md:改了 index.html / redirect.html 就必须跑它

验证:npm run build + verify-inline-scripts 6/6;变异测试(往产物注入不配对的右花括号)→
立刻 FAIL "Unexpected token '}'",还原后 6/6(证明不是哑弹);回归 NLS 44/44、附件 57/57、
toast 27/27、DMS 契约 19/19、DMS 端到端 89/89、validate-harness 通过。

⚠️ 请用户刷新页面确认界面恢复正常。

Co-Authored-By: Claude Code <noreply@anthropic.com>
gongtianxiao 1 ヶ月 前
親
コミット
c96e6ae43e
5 ファイル変更、126 行追加、4 行削除
  1. 7 0
      README.md
  2. 30 1
      harness/progress.md
  3. 1 0
      harness/sops/verification.md
  4. 88 0
      harness/tools/verify-inline-scripts.mjs
  5. 0 3
      index.html

+ 7 - 0
README.md

@@ -422,6 +422,13 @@
     `harness/tools/get-nls-token.mjs` 与 dev 端点共用;带 5 分钟提前刷新 + 并发去重
   - 验证:端点 200;**拿端点发的 token 真调通一次识别**(`status:20000000 SUCCESS`);
     `npm run build` 后**产物里搜三个凭据值 0 命中**
+- **修:界面卡在「正在初始化,请稍候...」**(2026-09-24,我自己上一轮删出来的回归):
+  删 `initToken` 时在 index.html 的内联脚本里留了个不配对的 `}` → 脚本语法错误 →
+  `hasSequence` 没置位、`sequenceLoaded` 没派发 → 加载遮罩永不隐藏。
+  **构建没抓到**,因为 Vite **不解析** index.html 里的普通 `<script>`(原样拷贝);
+  已修,并新增 `harness/tools/verify-inline-scripts.mjs`(编译 `dist/*.html` 的内联脚本 +
+  报出未替换的 `%VITE_*%` 占位符,变异测试确认会红),同时写进 `sops/verification.md`:
+  改了 index.html / redirect.html 必须跑它
 - **删掉走不到的卡片引导链 + asr 搬家**(用户 2026-09-24):
   - `index.html` 的 `initToken()` / `initCardInfo()` / `else` 分支:**两个环境都走不到**
     (`isKnowledgeApi` 恒 true),且它们的产物(`globalThis.token` / `cardInfo`)**全仓库无读者**;

+ 30 - 1
harness/progress.md

@@ -4280,7 +4280,36 @@ payload 里含模型里**不存在**的列时,`addContent` 会忽略该列照
   切换说明)、`current-api-call-sites.md`(§0 A 表第 6 行、§三 的旧路径标注为已删)、
   `asr-realtime.md`(历史文档里指向已删文件的**链接**改成纯文本,避免 404 链接)
 
-**⚠️ 部署必配(已写进技术债 #10 与 asr-nls.md)**:
+### 续 40:修「界面卡在正在初始化,请稍候...」(我上一轮删出来的回归)
+
+**用户反馈**(截图:整页模糊 + 中间一行「正在初始化,请稍候...」):「现在界面有问题」。
+
+**根因**:续 39 删 `initToken` / `else` 分支时,**`index.html` 的内联脚本里留下一个不配对的 `}`**
+(第 83 行)→ 内联脚本整体**语法错误** → 脚本一加载就抛 → `globalThis.hasSequence` 没置位、
+`sequenceLoaded` 事件没派发 → `BusinessAssistant.vue` 的 `initSequence()` 一直等事件 →
+**加载遮罩永远不隐藏**(截图上那层模糊就是 `#loading-cover`)。
+
+**为什么我没抓到(关键教训)**:**`npm run build` 不解析 index.html 里那种普通 `<script>`**
+(Vite 只处理 `type="module"` 的,其余原样拷贝)→ 语法错误的页面**构建照样绿**。
+续 39 我跑的是 build + 各条脚本(它们都不看 index.html 的脚本),于是"验证通过"是假的。
+
+**修法**:删掉那个多余的 `}`;随之**新增守护脚本**
+[`harness/tools/verify-inline-scripts.mjs`](tools/verify-inline-scripts.mjs)——
+抽 `dist/*.html` 里的内联脚本用 `new vm.Script()` 编译(只编译不执行),语法错就 FAIL;
+顺带把「产物里残留未替换的 `%VITE_*%` 占位符」也报出来(`VITE_DMS_API` 属已知项)。
+并把它写进 [`sops/verification.md`](sops/verification.md):**改了 index.html / redirect.html
+就必须跑它**。
+
+**运行过的验证**:
+- 修后三块内联脚本全部通过语法检查(源码里抽出来 `node --check` ✓)
+- `npm run build` + `verify-inline-scripts.mjs` **6/6**;
+  **变异测试**:往产物里注入一个不配对的 `}` → 立刻 `FAIL … Unexpected token '}'`,还原后 6/6
+  (证明它不是哑弹)
+- 回归:NLS 44/44、附件 57/57、toast 27/27、DMS 契约 19/19、DMS 端到端 89/89、validate-harness 通过
+
+⚠️ **请用户刷新页面确认**:应能正常进入界面(遮罩消失)。
+
+**🔴 部署必配(已写进技术债 #10 与 asr-nls.md)**:
 ① nginx 要把 `/aly-api/` 转发到 `http://121.43.55.7:10011/`(去掉前缀);
 ② **`.env.production` / `.env.qingpu` / `.env.test` 都没有 `VITE_NLS_APPKEY`** ——
 实测生产构建的包里 appkey 为空,上线会因空 appkey 失败。**需用户决定**:由部署侧注入,

+ 1 - 0
harness/sops/verification.md

@@ -11,6 +11,7 @@
 | 样式、模板结构 | 构建通过 + 实际页面看一眼 |
 | 业务逻辑(不改协议) | 构建通过 + 在 `tools/` 写脚本对真实数据断言 |
 | **任何接口 / 协议相关** | 上面全部 **+ 对真实后端端到端跑一遍**(见下) |
+| **改了 `index.html` / `redirect.html`(含内联脚本)** | 上面全部 **+ `npm run build && node harness/tools/verify-inline-scripts.mjs`** —— ⚠️ **构建不会解析内联 `<script>`**(原样拷贝),删代码删出个不配对的 `}` 也能构建通过,但页面一加载就抛语法错误、加载遮罩永远不隐藏(2026-09-24 实际踩过) |
 
 ## 构建
 

+ 88 - 0
harness/tools/verify-inline-scripts.mjs

@@ -0,0 +1,88 @@
+/**
+ * 验证**构建产物里的内联脚本语法正确** —— 守的是一个真实的盲区。
+ *
+ * 为什么需要它(2026-09-24 实际踩到):`npm run build` **不会解析** index.html 里那些普通
+ * `<script>`(没有 type=module 的内联块),Vite 原样拷过去 —— 于是删代码时删出个不配对的
+ * `}`,构建照样绿,但页面一加载内联脚本就抛语法错误:`globalThis.hasSequence` 没置位、
+ * `sequenceLoaded` 没派发 → **加载遮罩永远不隐藏**,用户看到的就是一片"正在初始化,请稍候..."。
+ *
+ * 所以这条检查盯的是**产物**(dist/*.html):那里 `%VITE_*%` 已被替换成真值,
+ * 拿到的就是浏览器真正会执行的那段代码(源码里的 redirect.html 带占位符,本来就不是合法 JS,
+ * 不能用它做检查)。
+ *
+ * 怎么跑:
+ *   npm run build && node harness/tools/verify-inline-scripts.mjs
+ */
+
+import { existsSync, readFileSync, readdirSync } from 'node:fs';
+import { basename, join } from 'node:path';
+import vm from 'node:vm';
+
+const DIST = 'dist';
+let pass = 0;
+let fail = 0;
+const check = (name, ok, extra = '') => {
+  if (ok) {
+    pass++;
+    console.log('  ok   ' + name);
+  } else {
+    fail++;
+    console.log('  FAIL ' + name + '  ' + extra);
+  }
+};
+
+console.log('\n【1】dist 里的内联脚本能通过语法检查');
+
+if (!existsSync(DIST)) {
+  console.log('  ⏭  没有 dist/ —— 先跑 `npm run build`');
+  process.exit(2);
+}
+
+const htmlFiles = readdirSync(DIST).filter((f) => f.endsWith('.html'));
+check(`dist 下有 html 产物(${htmlFiles.join(', ')})`, htmlFiles.length > 0);
+
+/** 抽出内联 `<script>`(跳过带 src= 的) */
+const extractInlineScripts = (html) =>
+  [...html.matchAll(/<script(?![^>]*\bsrc=)[^>]*>([\s\S]*?)<\/script>/gi)].map((m) => m[1]);
+
+for (const file of htmlFiles) {
+  const html = readFileSync(join(DIST, file), 'utf8');
+  const blocks = extractInlineScripts(html);
+  if (!blocks.length) {
+    console.log(`  ⏭  ${file} 没有内联脚本`);
+    continue;
+  }
+  blocks.forEach((code, index) => {
+    const label = `${file} 的第 ${index + 1} 块内联脚本`;
+    try {
+      // new vm.Script 只编译不执行 —— 语法错就抛,正好用来当检查
+      new vm.Script(code);
+      check(`${label} 语法正确(${code.length} 字符)`, true);
+    } catch (error) {
+      const message = error instanceof Error ? error.message : String(error);
+      check(`${label} 语法正确`, false, message);
+    }
+  });
+}
+
+console.log('\n【2】本仓库这次踩过的具体症状:占位符没被替换');
+{
+  // 产物里出现未替换的 `%VITE_*%` 说明那个变量没配 —— 轻则功能失效,重则像
+  // redirect.html 的 redirectMap 一样直接语法错误。这里顺带把它们报出来。
+  const leftovers = [];
+  for (const file of htmlFiles) {
+    const html = readFileSync(join(DIST, file), 'utf8');
+    for (const m of html.matchAll(/%VITE_[A-Z0-9_]+%/g)) leftovers.push(`${file}: ${m[0]}`);
+  }
+  // VITE_DMS_API 在生产构建里本来就未配置(见技术债 #9),单独提示、不算失败
+  const unexpected = leftovers.filter((item) => !item.includes('%VITE_DMS_API%'));
+  check('产物里没有被漏替换的 VITE_ 占位符(VITE_DMS_API 单独见技术债 #9)',
+    unexpected.length === 0, unexpected.join(' | '));
+  const dmsLeftover = leftovers.filter((item) => item.includes('%VITE_DMS_API%'));
+  if (dmsLeftover.length) {
+    console.log(`  ℹ️  ${dmsLeftover.join(' | ')} —— 生产未配 DMS,属已知项(技术债 #9)`);
+  }
+}
+
+console.log(`\n===== 通过 ${pass} 项,失败 ${fail} 项 =====`);
+process.exit(fail ? 1 : 0);

+ 0 - 3
index.html

@@ -78,9 +78,6 @@
         }
       }
 
-
-      }
-
       function initMessageChannel() {
         window.addEventListener("message", () => {
           if (event.data && event.data.type === "initMessage") {