Просмотр исходного кода

feat: dashboard 新手引导 onboarding 引导与冒烟测试

ETO-kai 1 месяц назад
Родитель
Сommit
961cd4cde4

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
mcp/src/dashboard/driver.css


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
mcp/src/dashboard/driver.min.js


+ 13 - 1
mcp/src/dashboard/index.html

@@ -5,7 +5,9 @@
 <meta name="viewport" content="width=device-width, initial-scale=1">
 <title>企微智能办公助手</title>
 <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='14' fill='%23fa8c16'/%3E%3Cpath d='M18 20h28v20H34l-8 7v-7h-8z' fill='white'/%3E%3C/svg%3E">
-<link rel="stylesheet" href="/dashboard/styles.css?v=20260807-voice-preview-persist">
+<link rel="stylesheet" href="/dashboard/styles.css?v=20260818-layout-fix">
+<link rel="stylesheet" href="/dashboard/driver.css?v=20260814g-onboarding">
+<link rel="stylesheet" href="/dashboard/onboarding.css?v=20260814g-onboarding">
 </head>
 <body>
 <div class="app" id="app">
@@ -76,6 +78,14 @@
           <span>连接与设置</span>
         </a>
       </div>
+      <div class="nav-group">
+        <div class="nav-label">新手引导</div>
+        <a class="nav-item onboarding-nav-entry" id="onboarding-nav-entry" href="#" role="button" aria-label="新手引导">
+          <svg viewBox="0 0 24 24"><path d="M7 2h9l1 3h2a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h2l1-3zm1.5 2l-.7 2h8.4l-.7-2h-7zm.5 8.5h6v-1.5H9v1.5zm0 4h4v-1.5H9v1.5z"/></svg>
+          <span>新手引导</span>
+          <span class="onboarding-progress-badge" id="onboarding-progress-badge" hidden>0/6</span>
+        </a>
+      </div>
     </nav>
   </aside>
   <button class="sidebar-backdrop" id="sidebar-backdrop" type="button" aria-label="关闭导航"></button>
@@ -108,5 +118,7 @@
 </div>
 <script src="/dashboard/echarts.min.js"></script>
 <script src="/dashboard/app.js?v=20260807-voice-preview-persist"></script>
+<script src="/dashboard/driver.min.js?v=20260814g-onboarding"></script>
+<script src="/dashboard/onboarding-guide.js?v=20260814g-onboarding"></script>
 </body>
 </html>

+ 1043 - 0
mcp/src/dashboard/onboarding-guide.js

@@ -0,0 +1,1043 @@
+/* ============================================================
+ * 新手引导(onboarding-guide.js)
+ * 独立 IIFE,与 app.js / server.js 零耦合:
+ *   - 通过 DOM 事件(hashchange)+ 原生 fetch 感知业务状态
+ *   - 引导进度从真实业务状态推导 + localStorage 记录行为标记
+ * 依赖:window.driver(Driver.js v1,加载于本文件之前)
+ * ============================================================ */
+(function () {
+  'use strict';
+
+  // ---------- 常量 ----------
+  var NS = 'qiwei-onboarding-v1';
+  var ENTRY_ID = 'onboarding-nav-entry';
+  var BADGE_ID = 'onboarding-progress-badge';
+
+  // ---------- 关卡定义 ----------
+  // steps[i].element: 选择器字符串 或 返回 Element 的函数
+  // steps[i].mode === 'panel': 不遮挡页面的悬浮说明卡(用于需要用户自由操作的步骤)
+  var LEVELS = [
+    {
+      id: 'welcome',
+      title: '欢迎与开始',
+      desc: '了解引导入口与两个关键页面',
+      steps: [
+        { element: '#' + ENTRY_ID, title: '新手引导入口', description: '这是常驻入口,随时点它可以查看引导进度、继续未完成的步骤。' },
+        { element: '.nav-item[data-page="status"]', title: '连接与设置', description: '开通订阅、绑定企微账号都在这里完成。' },
+        { element: '.nav-item[data-page="agent"]', title: '客服工作台', description: '白名单、监听、音色训练都在这个页面,是核心工作区。' }
+      ]
+    },
+    {
+      id: 'subscribe',
+      title: '开通订阅',
+      desc: '开通企微席位,解锁完整能力',
+      autoWait: true,
+      doneText: '我已完成,继续',
+      steps: [
+        {
+          page: 'status',
+          element: '#btn-subscribe-action',
+          title: '开通企微席位',
+          description: '点击「开通订阅」按钮,会在新窗口打开 4 步引导流程页(开通订阅 → 扫码 → 验证码 → 完成)。跟着流程页走完后回到这里。'
+        },
+        {
+          mode: 'panel',
+          element: function () { return document.querySelector('.status-page-layout') || document.querySelector('#content'); },
+          title: '确认订阅状态',
+          description: '到订阅流程页完成「开通订阅 → 扫码 → 验证码 → 完成」。完成后点下方「我已完成,继续」;状态有约 2 分钟缓存,若未更新稍候再点一次即可。'
+        }
+      ]
+    },
+    {
+      id: 'account',
+      title: '绑定企微账号',
+      desc: '扫码登录,绑定你的企业微信',
+      autoWait: true,
+      doneText: '我已完成,继续',
+      steps: [
+        {
+          page: 'status',
+          element: function () {
+            var loginBtn = document.querySelector('#btn-start-login');
+            var addBtn = document.querySelector('#btn-add-account-page');
+            if (loginBtn && !addBtn) return loginBtn; // 全新用户:已进入登录页
+            return addBtn || document.querySelector('#btn-add-account');
+          },
+          title: '添加 / 绑定企微账号',
+          description: '点击「添加账号」或「生成登录二维码」,用企业微信 App 扫码。若提示需要订阅,点「打开订阅与登录流程页」即可一并完成。'
+        },
+        {
+          mode: 'panel',
+          element: function () { return document.querySelector('.login-page') || document.querySelector('.status-page-layout') || document.querySelector('#content'); },
+          title: '扫码并确认登录',
+          description: '用手机企业微信扫码;出现 6 位验证码时直接在页面输入。完成后点「我已完成,继续」;若引导检测到还未登录,会提示你重新操作或选择其他任务。'
+        }
+      ]
+    },
+    {
+      id: 'allowlist',
+      title: '配置白名单与监听',
+      desc: '勾选允许 AI 服务的客户',
+      autoWait: true,
+      steps: [
+        {
+          page: 'agent',
+          element: '[data-agent-action="manage-allowlist"]',
+          title: '打开白名单',
+          description: '点击「白名单」,从企微联系人里勾选至少一位允许 AI 处理的客户。白名单决定 AI 会回复谁的消息,非常重要。'
+        },
+        {
+          element: '#allowlist-contact-list',
+          title: '勾选联系人',
+          description: '在列表里搜索姓名 / 备注 / 企业,勾选至少一位客户。找不到时可用「手工添加联系人 ID」。',
+          timeout: 6000
+        },
+        {
+          element: '#allowlist-save',
+          title: '保存白名单',
+          description: '点击「保存白名单」。保存后 AI 监听会自动保持开启。',
+          timeout: 6000
+        },
+        {
+          element: '[data-agent-intake-form]',
+          title: '接入策略',
+          description: '这是「接入策略」设置。默认「仅白名单」最稳妥:只有白名单客户 AI 才会处理。新手先保持默认即可。',
+          timeout: 6000
+        }
+      ]
+    },
+    {
+      id: 'voice',
+      title: '音色训练',
+      desc: '让 AI 用你的声音回消息(可选但推荐)',
+      autoWait: true,
+      steps: [
+        {
+          page: 'agent',
+          mode: 'panel',
+          element: function () { return document.querySelector('.agent-page') || document.querySelector('#content'); },
+          title: '选择客户',
+          description: '先在左侧会话列表选中任意一个客户(或新建会话)。之后引导会自动打开「初始化声音」弹窗,无需手动点击。'
+        },
+        {
+          prep: autoOpenVoiceModal,
+          element: '#voice-profile-body',
+          title: '录制你的声音',
+          description: '点「开始录音」录一段 5~30 秒的本人语音(自然读一段话即可),或上传 m4a / wav 音频。',
+          timeout: 9000
+        },
+        {
+          element: '[data-voice-save]',
+          title: '保存声音档案',
+          description: '点击「保存」生成你的专属音色。之后 AI 就可以用你的声音合成语音回复客户。',
+          timeout: 6000
+        }
+      ]
+    }
+  ];
+
+  // 链式推荐顺序(适应"全新用户":先绑定,因为订阅卡在登录后才可见)
+  var PRIORITY = ['welcome', 'account', 'subscribe', 'allowlist', 'voice'];
+
+  // ---------- 每页引导注册表 ----------
+  // 每个页面首次进入自动触发;steps.element 为选择器字符串或函数;元素缺失/超时则跳过该步
+  var PAGE_GUIDES = {
+    overview: {
+      title: '经营总览',
+      steps: [
+        { element: '#overview-query', title: '经营诊断', description: '在这里输入经营问题(如「本周客户跟进情况如何」),一键发起 AI 诊断。' },
+        { element: '.kpi-tile', title: '核心指标卡', description: '客户、社群、客服等核心指标一屏掌握,点击卡片可跳转到对应功能页。' },
+        { element: '#overview-diagnosis-result', title: '诊断结果', description: '诊断结论、数据缺口和下一步行动都会展示在这里。' }
+      ]
+    },
+    analytics: {
+      title: '数据看板',
+      steps: [
+        { element: '.analytics-root', title: '全链路数据看板', description: '汇总 Agent 运行、客户价值、社群、交接与知识资产的只读统计。' },
+        { element: '.analytics-domain', title: '数据分区', description: '按业务域分区展示,向下滚动可查看每一块的明细图表。' },
+        { element: '#an-radar', title: '运营健康度', description: '首个图表是运营健康度雷达,帮助快速判断整体状态。' }
+      ]
+    },
+    agent: {
+      title: '客服工作台',
+      steps: [
+        { element: '.agent-commandbar', title: '顶部命令条', description: '账号状态、全局模式开关和常用操作都在这里。' },
+        { element: '.agent-mode-switch', title: '全局模式', description: '切换「待审核 / 高置信自动 / 全自动接管 / 全局暂停」,决定 AI 如何回复。' },
+        { element: '[data-agent-action="manage-allowlist"]', title: '白名单', description: '管理允许 AI 处理的客户白名单,白名单外客户不会收到 AI 回复。' },
+        { element: '.agent-conversation-list', title: '会话列表', description: '左侧选择客户会话,右侧进行回复、审核与语音发送。' },
+        { element: '#agent-reply-editor', title: '回复编辑器', description: '先选中左侧一个私聊会话,在这里编辑并发送回复。', timeout: 6000 },
+        { element: '[data-agent-action="voice-profile"]', title: '音色设置', description: '选中会话后可在这里录制/切换声音,让 AI 用你的音色发语音。', timeout: 6000 }
+      ]
+    },
+    knowledge: {
+      title: '知识与任务',
+      steps: [
+        { element: '.knowledge-tree-panel', title: '知识库目录', description: '左侧是本地知识库目录树,上传业务文档供 AI 回复时参考。' },
+        { element: '.knowledge-viewer', title: '内容预览区', description: '点击文件可预览;任务中心、会议纪要等也在这里展示。' }
+      ]
+    },
+    skills: {
+      title: '数字员工能力',
+      steps: [
+        { element: '#skill-search', title: '搜索技能', description: '按关键词搜索技能包与能力实例。' },
+        { element: '.skill-package-list', title: '技能包列表', description: '左侧浏览已接入的技能包,右侧查看能力详情。' }
+      ]
+    },
+    'customer-ops': {
+      title: '客户运营',
+      steps: [
+        { element: '#customer-master-search', title: '客户搜索', description: '按姓名/手机号等搜索客户,进入客户主档。' },
+        { element: '.customer-master-detail', title: '客户详情', description: '画像、跟进任务、预警、房源推荐和时间线都在这里。', timeout: 6000 },
+        { element: '.optional-batch-ops', title: '批量运营工具', description: '展开后可批量导入客户、加好友、检查好友状态和自动建群。' }
+      ]
+    },
+    portraits: {
+      title: '画像与标签',
+      steps: [
+        { element: '#btn-pt-batch', title: '批量生成画像', description: '为指定客户批量生成/更新画像。' },
+        { element: '#btn-pt-tag', title: '打标签', description: '为客户添加标签,方便筛选与运营。' },
+        { element: '#pt-filter-keyword', title: '筛选画像', description: '按关键词、来源或标签筛选画像列表。' }
+      ]
+    },
+    transfers: {
+      title: '客户交接',
+      steps: [
+        { element: '#btn-new-transfer', title: '新建交接', description: '从旧顾问到新顾问生成交接包预览。' },
+        { element: '.handover-flow', title: '交接流程', description: '三步流程:生成预览 → 邀请新顾问 → 移除旧顾问并发送欢迎语。' }
+      ]
+    },
+    'group-ops': {
+      title: '社群运营',
+      steps: [
+        { element: '.group-ops-tabs', title: '功能页签', description: '运营总览、今日工作台、群洞察、SOP、质检与自动化的入口。' },
+        { element: '[data-group-ops-view]', title: '切换视图', description: '点击页签可切换到对应的运营视图。' },
+        { element: '#group-ops-refresh', title: '刷新数据', description: '任务与质检数据可随时刷新。' }
+      ]
+    },
+    groups: {
+      title: '客户群',
+      steps: [
+        { element: '#btn-sync-groups', title: '扫描客户群', description: '从企微扫描群目录,按关键词自动识别客户群。' },
+        { element: '#sync-scope', title: '扫描范围', description: '选择扫描范围:自己创建的群 / 最近会话 / 全部。' },
+        { element: '#filter-keyword', title: '筛选群列表', description: '按关键词、状态筛选扫描结果,并人工确认。' }
+      ]
+    },
+    status: {
+      title: '连接与设置',
+      steps: [
+        { element: '.status-card', title: '状态总览', description: '账号在线、订阅、鉴权三张状态卡,一眼看清连接情况。' },
+        { element: '#btn-add-account-page', title: '添加账号', description: '可添加并切换多个企微账号。' },
+        { element: '.status-accounts-card', title: '已保存账号', description: '管理已保存的账号:切换、删除,离线时可恢复登录。' }
+      ]
+    }
+  };
+
+  // ---------- 本地状态 ----------
+  var local = readLocal();
+  var running = false;
+  var pageGuideRunning = false; // 每页引导进行中标志(与关卡引导 running 互斥)
+  var explicitPageGuide = null; // 总览里主动点开某页引导(避免与自动触发重复)
+  var tourDriver = null;
+  var activePanel = null;
+  var pendingStepDone = null; // 当前步骤的完成回调(供外部终止解析)
+  var aborted = false;        // 用户切页/切号等外部中断标记
+  var navigating = false;     // 引导自身切页标志(区别于用户切页)
+
+  function readLocal() {
+    try { return JSON.parse(localStorage.getItem(NS) || '{}'); }
+    catch (e) { return {}; }
+  }
+  function setLocal(patch) {
+    try {
+      local = Object.assign({}, local, patch);
+      localStorage.setItem(NS, JSON.stringify(local));
+    } catch (e) { /* 忽略存储错误 */ }
+  }
+  function markDone(levelId) {
+    var completed = Object.assign({}, local.completed || {});
+    completed[levelId] = Date.now();
+    setLocal({ completed: completed });
+  }
+
+  // ---------- 基础工具 ----------
+  function sleep(ms) { return new Promise(function (r) { setTimeout(r, ms); }); }
+
+  function fetchJson(path) {
+    return fetch(path, { headers: { 'Accept': 'application/json' } }).then(function (r) {
+      if (!r.ok) throw new Error('HTTP ' + r.status);
+      return r.json();
+    });
+  }
+
+  function escapeHtml(s) {
+    return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
+      return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
+    });
+  }
+
+  function toast(msg) {
+    var t = document.createElement('div');
+    t.className = 'onboarding-toast';
+    t.textContent = msg;
+    document.body.appendChild(t);
+    setTimeout(function () { if (t.parentNode) t.parentNode.removeChild(t); }, 3400);
+  }
+
+  function resolveTarget(target) {
+    if (typeof target === 'function') return target();
+    return document.querySelector(target);
+  }
+
+  function waitForElement(target, timeout, interval) {
+    timeout = timeout || 9000;
+    interval = interval || 250;
+    var node = resolveTarget(target);
+    if (node) return Promise.resolve(node);
+    return new Promise(function (resolve, reject) {
+      var start = Date.now();
+      var timer = setInterval(function () {
+        var el = resolveTarget(target);
+        if (el) { clearInterval(timer); resolve(el); }
+        else if (Date.now() - start > timeout) { clearInterval(timer); reject(new Error('元素未出现')); }
+      }, interval);
+    });
+  }
+
+  function goto(page) {
+    if ((location.hash.slice(1) || 'overview') === page) return Promise.resolve();
+    navigating = true;
+    location.hash = '#' + page;
+    return sleep(350).then(function () { navigating = false; });
+  }
+
+  // ---------- 进度计算 ----------
+  function computeProgress() {
+    return Promise.all([
+      fetchJson('/api/status'),
+      fetchJson('/api/agent/status'),
+      fetchJson('/api/agent/voice/status')
+    ]).then(function (results) {
+      var summary = (results[0] && results[0].summary) || {};
+      var agentData = (results[1] && results[1].data) || {};
+      var voiceData = (results[2] && results[2].data) || {};
+      var config = agentData.config || {};
+      var completed = local.completed || {};
+      return LEVELS.map(function (L) {
+        var status;
+        if (completed[L.id]) status = 'done';
+        else if (L.id === 'welcome') status = local.welcomeGuided ? 'done' : 'todo';
+        else if (L.id === 'subscribe') status = summary.subscribed === true ? 'done' : (summary.subscriptionPending ? 'pending' : 'todo');
+        else if (L.id === 'account') status = summary.online === true ? 'done' : (summary.loginPending ? 'pending' : 'todo');
+        else if (L.id === 'allowlist') status = Number(config.allowedSenderCount) > 0 ? 'done' : 'todo';
+        else if (L.id === 'voice') status = voiceData.enrolled === true ? 'done' : 'todo';
+        else status = 'todo';
+        return { level: L, status: status };
+      });
+    }).catch(function () {
+      // 网络/接口失败:仅凭行为标记推导
+      var completed = local.completed || {};
+      return LEVELS.map(function (L) {
+        var status;
+        if (completed[L.id]) status = 'done';
+        else if (L.id === 'welcome') status = local.welcomeGuided ? 'done' : 'todo';
+        else status = 'pending';
+        return { level: L, status: status };
+      });
+    });
+  }
+  function isDone(prog, id) {
+    var p = prog.find(function (x) { return x.level.id === id; });
+    return !!(p && p.status === 'done');
+  }
+
+  function nextTodo(prog, excludeId) {
+    var ordered = PRIORITY.map(function (id) {
+      return prog.find(function (x) { return x.level.id === id; });
+    });
+    for (var i = 0; i < ordered.length; i++) {
+      var p = ordered[i];
+      if (p && p.level.id !== excludeId && p.status === 'todo') {
+        // 订阅关卡在未绑定时先引导绑定(订阅卡登录后才可见)
+        if (p.level.id === 'subscribe') {
+          var acc = prog.find(function (x) { return x.level.id === 'account'; });
+          if (acc && acc.status !== 'done') continue;
+        }
+        return p.level.id;
+      }
+    }
+    return null;
+  }
+
+  function waitForLevelDone(levelId, timeout, notified) {
+    if (timeout == null) timeout = 200000;
+    return computeProgress().then(function check(p) {
+      if (aborted) return false;
+      if (isDone(p, levelId)) return true;
+      if (timeout <= 0) return false;
+      if (!notified) { toast('正在等待本步骤完成,请完成页面上的操作…'); notified = true; }
+      return sleep(2500).then(function () {
+        return waitForLevelDone(levelId, timeout - 2500, notified);
+      });
+    });
+  }
+
+  // ---------- Driver.js 封装 ----------
+  function driverFactory() {
+    var g = window;
+    if (typeof g.driver === 'function') return g.driver;
+    if (g.driver && g.driver.js && typeof g.driver.js.driver === 'function') return g.driver.js.driver;
+    return null;
+  }
+
+  function createDriver(extra) {
+    var factory = driverFactory();
+    if (!factory) return null;
+    var cfg = {
+      animate: true,
+      overlayColor: '#1a1a2e',
+      overlayOpacity: 0.72,
+      smoothScroll: true,
+      allowClose: true,
+      overlayClickBehavior: 'close',
+      popoverClass: 'onboarding-driver-popover',
+      stagePadding: 8,
+      nextBtnText: '下一步',
+      prevBtnText: '上一步',
+      doneBtnText: '完成'
+    };
+    if (extra) Object.assign(cfg, extra);
+    return factory(cfg);
+  }
+
+  function closeDriver() {
+    if (tourDriver) {
+      try { tourDriver.destroy(); } catch (e) { /* 忽略 */ }
+      tourDriver = null;
+    }
+  }
+
+  // 外部中断:解析当前步骤为 'close',并提示
+  function stopGuide(message) {
+    aborted = true;
+    if (pendingStepDone) {
+      var fn = pendingStepDone;
+      pendingStepDone = null;
+      fn('close');
+    }
+    if (message) toast(message);
+  }
+
+  // 逐步高亮一个步骤;返回 'next' | 'prev' | 'close'
+  function showDriverStep(el, step, ctx) {
+    return new Promise(function (resolve) {
+      closeDriver();
+      var settled = false;
+      function done(res) {
+        if (settled) return;
+        settled = true;
+        if (pendingStepDone === done) pendingStepDone = null;
+        closeDriver();
+        resolve(res);
+      }
+      // 用户点击遮罩空白处 / Esc / 关闭按钮 → driver 销毁 → 统一解析为 'close'
+      var d = createDriver({ onDestroyed: function () { done('close'); } });
+      if (!d) { resolve('next'); return; }
+      tourDriver = d;
+      pendingStepDone = done;
+      var popover = {
+        title: step.title,
+        description: step.description,
+        showButtons: [],
+        onCloseClick: function () { done('close'); }
+      };
+      if (ctx.last) {
+        popover.showButtons = ['next'];
+        popover.nextBtnText = ctx.doneText || '完成本步';
+        popover.onNextClick = function () { done('next'); };
+      } else {
+        popover.showButtons = ['next'];
+        popover.nextBtnText = '下一步';
+        popover.onNextClick = function () { done('next'); };
+        if (!ctx.first) {
+          popover.showButtons.push('previous');
+          popover.prevBtnText = '上一步';
+          popover.onPrevClick = function () { done('prev'); };
+        }
+      }
+      try {
+        d.highlight({ element: el, popover: popover });
+      } catch (e) {
+        done('close');
+      }
+    });
+  }
+
+  // 不遮挡页面的说明卡;返回 'next' | 'prev' | 'close'
+  function showPanelStep(step, ctx) {
+    return new Promise(function (resolve) {
+      closeDriver();
+      closePanel();
+      var settled = false;
+      function done(res) {
+        if (settled) return;
+        settled = true;
+        if (pendingStepDone === done) pendingStepDone = null;
+        closePanel();
+        resolve(res);
+      }
+      var panel = document.createElement('div');
+      panel.className = 'onboarding-panel';
+      panel.innerHTML =
+        '<div class="onboarding-panel-title">' + escapeHtml(step.title) + '</div>' +
+        '<div class="onboarding-panel-desc">' + escapeHtml(step.description) + '</div>' +
+        '<div class="onboarding-panel-actions">' +
+        (ctx.first && ctx.last ? '' : '<button type="button" class="btn btn-secondary" data-panel="prev">上一步</button>') +
+        '<button type="button" class="btn" data-panel="next">' + escapeHtml(ctx.doneText || '下一步') + '</button>' +
+        '</div>';
+      document.body.appendChild(panel);
+      activePanel = panel;
+      pendingStepDone = done;
+      var prevBtn = panel.querySelector('[data-panel="prev"]');
+      if (prevBtn) prevBtn.addEventListener('click', function () { done('prev'); });
+      panel.querySelector('[data-panel="next"]').addEventListener('click', function () { done('next'); });
+    });
+  }
+
+  function closePanel() {
+    if (activePanel) {
+      try { if (activePanel.parentNode) activePanel.parentNode.removeChild(activePanel); } catch (e) { /* 忽略 */ }
+      activePanel = null;
+    }
+  }
+
+  // 语音训练:若「初始化声音」弹窗未打开,先聚焦按钮再自动点击打开(视觉上有聚焦→弹窗过程)
+  function autoOpenVoiceModal() {
+    var existing = document.querySelector('#voice-profile-body');
+    if (existing && existing.isConnected) return Promise.resolve();
+    return waitForElement('[data-agent-action="voice-profile"]', 9000).then(function (btn) {
+      var d = createDriver({ onDestroyed: function () {} });
+      if (d) {
+        try {
+          d.highlight({
+            element: btn,
+            popover: {
+              title: '正在为你打开声音设置…',
+              description: '自动聚焦「初始化声音」按钮,稍候会打开声音档案弹窗。',
+              showButtons: []
+            }
+          });
+        } catch (e) { /* 忽略高亮失败 */ }
+      }
+      return sleep(1200).then(function () {
+        if (d) { try { d.destroy(); } catch (e) {} }
+        if (btn.isConnected) {
+          try { btn.click(); } catch (e) { /* 忽略 */ }
+        }
+        return sleep(500);
+      });
+    }).catch(function () {
+      // 按钮不存在(未选中会话等):交给后续步骤的元素等待统一处理
+      return undefined;
+    });
+  }
+
+  // 各关卡未完成时的检测提示文案
+  var CHECK_MESSAGES = {
+    subscribe: '还没检测到订阅已开通。请确认订阅流程页已走完(「开通订阅 → 扫码 → 验证码 → 完成」)。',
+    account: '还没检测到账号登录完成。请完成扫码登录后重试。',
+    allowlist: '还没检测到白名单已保存。请至少勾选一位联系人并保存。',
+    voice: '还没检测到声音档案已保存。请完成录音并点击「保存」。'
+  };
+
+  // 完成后检测对话框:返回 'done' | 'restart' | 'other' | 'reset' | 'skip'
+  function showStepCheckDialog(level) {
+    return new Promise(function (resolve) {
+      var message = CHECK_MESSAGES[level.id] || '还没检测到这一步已完成,请先完成页面上的操作。';
+      var ov = createOverlay('onboarding-check-overlay', '' +
+        '<div class="onboarding-welcome-card" style="max-width:440px;text-align:left">' +
+          '<h2 style="font-size:18px;margin:0 0 8px">「' + escapeHtml(level.title) + '」还未完成</h2>' +
+          '<p style="color:var(--text-secondary);font-size:13px;line-height:1.7;margin:0 0 6px">' + escapeHtml(message) + '</p>' +
+          '<p style="color:var(--text-muted);font-size:12px;margin:0 0 18px">也可以从这里选择其他操作。</p>' +
+          '<div class="onboarding-check-actions">' +
+            '<button type="button" class="btn" data-check="check">我已完成,再检查</button>' +
+            '<button type="button" class="btn btn-secondary" data-check="restart">重新指引</button>' +
+            '<button type="button" class="btn btn-secondary" data-check="other">其他任务</button>' +
+            '<button type="button" class="btn btn-secondary" data-check="reset">从头开始</button>' +
+            '<button type="button" class="btn btn-secondary" data-check="skip">跳过本步</button>' +
+          '</div>' +
+        '</div>', 'onboarding-welcome-overlay');
+      ov.querySelector('[data-check="check"]').addEventListener('click', function () {
+        computeProgress().then(function (p) {
+          if (isDone(p, level.id)) { closeOverlay(ov); resolve('done'); }
+          else toast('仍未检测到完成,可稍后再检查或选择其他操作');
+        });
+      });
+      ov.querySelector('[data-check="restart"]').addEventListener('click', function () { closeOverlay(ov); resolve('restart'); });
+      ov.querySelector('[data-check="other"]').addEventListener('click', function () { closeOverlay(ov); resolve('other'); });
+      ov.querySelector('[data-check="reset"]').addEventListener('click', function () { closeOverlay(ov); resolve('reset'); });
+      ov.querySelector('[data-check="skip"]').addEventListener('click', function () { closeOverlay(ov); resolve('skip'); });
+      ov.addEventListener('click', function (e) { if (e.target === ov) { closeOverlay(ov); resolve('other'); } });
+    });
+  }
+
+  // 从头开始:清空引导的行为标记(真实业务进度仍按实际状态推导),重新走欢迎引导
+  function resetGuideProgress() {
+    setLocal({ welcomeSeen: true, welcomeGuided: false, completed: {} });
+  }
+
+  // ---------- 自定义浮层(欢迎层 / 总览 / 重试) ----------
+  function createOverlay(id, innerHtml, className) {
+    closeOverlayById(id);
+    var ov = document.createElement('div');
+    ov.id = id;
+    ov.className = className || '';
+    ov.innerHTML = innerHtml;
+    document.body.appendChild(ov);
+    return ov;
+  }
+  function closeOverlay(ov) {
+    if (ov && ov.parentNode) ov.parentNode.removeChild(ov);
+  }
+  function closeOverlayById(id) {
+    var el = document.getElementById(id);
+    if (el) el.parentNode.removeChild(el);
+  }
+  function closeAllOverlays() {
+    ['onboarding-welcome-overlay', 'onboarding-overview-overlay', 'onboarding-retry-overlay', 'onboarding-check-overlay', 'onboarding-confetti'].forEach(function (id) {
+      closeOverlayById(id);
+    });
+  }
+
+  // ---------- 欢迎层 ----------
+  function showWelcome() {
+    closeAllOverlays();
+    var levelsHtml = LEVELS.map(function (L, i) {
+      return '<div class="onboarding-welcome-level"><span class="onboarding-wl-num">' + (i + 1) + '</span><span>' + escapeHtml(L.title) + '</span></div>';
+    }).join('');
+    var ov = createOverlay('onboarding-welcome-overlay', '' +
+      '<div class="onboarding-welcome-card">' +
+        '<div class="onboarding-welcome-flag">' +
+          '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M7 2h9l1 3h2a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h2l1-3zm1.5 2l-.7 2h8.4l-.7-2h-7zm.5 8.5h6v-1.5H9v1.5zm0 4h4v-1.5H9v1.5z"/></svg>' +
+        '</div>' +
+        '<h2>欢迎使用企微智能助手</h2>' +
+        '<p class="onboarding-welcome-sub">这是给你的上手指引。跟着步骤一步步完成开通订阅、绑定账号、训练 AI 与熟悉页面,全程无需人工指导。</p>' +
+        '<div class="onboarding-welcome-levels">' + levelsHtml + '</div>' +
+        '<div class="onboarding-welcome-actions">' +
+          '<button type="button" class="btn" id="onboarding-start-btn">开始引导</button>' +
+          '<button type="button" class="btn btn-secondary" id="onboarding-skip-btn">跳过引导</button>' +
+        '</div>' +
+        '<p class="onboarding-welcome-tip">可随时从左侧「新手引导」入口重新进入。</p>' +
+      '</div>', 'onboarding-welcome-overlay');
+    ov.querySelector('#onboarding-start-btn').addEventListener('click', function () {
+      setLocal({ welcomeSeen: true });
+      closeOverlay(ov);
+      refreshProgressBadge();
+      runLevel('welcome', { chain: true });
+    });
+    ov.querySelector('#onboarding-skip-btn').addEventListener('click', function () {
+      setLocal({ welcomeSeen: true });
+      closeOverlay(ov);
+      refreshProgressBadge();
+      toast('已跳过引导,可随时从左侧「新手引导」进入');
+    });
+    ov.addEventListener('click', function (e) {
+      if (e.target === ov) {
+        setLocal({ welcomeSeen: true });
+        closeOverlay(ov);
+        refreshProgressBadge();
+      }
+    });
+  }
+
+  // ---------- 关卡总览 ----------
+  function levelStateText(status) {
+    if (status === 'done') return '<span style="color:var(--success);font-size:12px">已完成</span>';
+    if (status === 'pending') return '<span style="color:var(--text-muted);font-size:12px">检测中…</span>';
+    return '<span style="color:var(--brand);font-size:12px">去完成 →</span>';
+  }
+
+  function openOverview() {
+    if (running) { toast('引导进行中,请先完成或关闭当前步骤'); return; }
+    closeDriver();
+    closePanel();
+    closeAllOverlays();
+    computeProgress().then(function (prog) {
+      var done = prog.filter(function (p) { return p.status === 'done'; }).length;
+      var total = prog.length;
+      var firstTodo = prog.find(function (p) { return p.status === 'todo'; });
+      var items = prog.map(function (p, i) {
+        return '<div class="onboarding-level-item is-' + p.status + '" data-run="' + p.level.id + '">' +
+          '<span class="onboarding-level-state">' + (p.status === 'done' ? '✓' : (i + 1)) + '</span>' +
+          '<div class="onboarding-level-main">' +
+            '<div class="onboarding-level-title">' + escapeHtml(p.level.title) + '</div>' +
+            '<div class="onboarding-level-desc">' + escapeHtml(p.level.desc) + '</div>' +
+          '</div>' +
+          '<div class="onboarding-level-cta">' + levelStateText(p.status) + '</div>' +
+        '</div>';
+      }).join('');
+      var pageSeen = local.pageSeen || {};
+      var pageItems = Object.keys(PAGE_GUIDES).map(function (pg) {
+        var seen = !!pageSeen[pg];
+        return '<div class="onboarding-level-item onboarding-page-guide-item ' + (seen ? 'is-done' : 'is-todo') + '" data-page-guide="' + pg + '">' +
+          '<span class="onboarding-level-state">' + (seen ? '✓' : '→') + '</span>' +
+          '<div class="onboarding-level-main">' +
+            '<div class="onboarding-level-title">' + escapeHtml(PAGE_GUIDES[pg].title) + '</div>' +
+            '<div class="onboarding-level-desc">' + (seen ? '已看过,点击可重播' : '点击进入该页开始引导') + '</div>' +
+          '</div>' +
+          '<div class="onboarding-level-cta"><span style="color:' + (seen ? 'var(--success)' : 'var(--brand)') + ';font-size:12px">' + (seen ? '重播' : '进入 →') + '</span></div>' +
+        '</div>';
+      }).join('');
+      var ov = createOverlay('onboarding-overview-overlay', '' +
+        '<div class="onboarding-welcome-card onboarding-overview-card" style="max-width:620px;text-align:left">' +
+          '<div class="onboarding-overview-body">' +
+            '<div class="onboarding-overview-head">' +
+              '<div class="onboarding-ov-flag"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M7 2h9l1 3h2a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h2l1-3zm1.5 2l-.7 2h8.4l-.7-2h-7zm.5 8.5h6v-1.5H9v1.5zm0 4h4v-1.5H9v1.5z"/></svg></div>' +
+              '<div><h3>新手引导</h3><p>已完成 ' + done + '/' + total + ' 步,跟着步骤一步步上手</p></div>' +
+            '</div>' +
+            '<div class="onboarding-progress-track"><div class="onboarding-progress-fill" style="width:' + (total ? (done / total * 100).toFixed(1) : 0) + '%"></div></div>' +
+            '<div class="onboarding-level-list">' + items + '</div>' +
+            '<div class="onboarding-page-guides">' +
+              '<div class="onboarding-page-guides-head">页面引导<small>进入各页面会自动开始,点击可重播</small></div>' +
+              '<div class="onboarding-level-list onboarding-page-guides-list">' + pageItems + '</div>' +
+            '</div>' +
+          '</div>' +
+          '<div class="onboarding-overview-foot">' +
+            '<button type="button" class="btn btn-secondary" id="onboarding-ov-close">关闭</button>' +
+            '<button type="button" class="btn" id="onboarding-ov-start" ' + (firstTodo ? '' : 'disabled') + '>' + (firstTodo ? '继续引导' : '全部完成 🎉') + '</button>' +
+          '</div>' +
+        '</div>', 'onboarding-welcome-overlay');
+      ov.querySelector('#onboarding-ov-close').addEventListener('click', function () { closeOverlay(ov); });
+      ov.querySelector('#onboarding-ov-start').addEventListener('click', function () {
+        closeOverlay(ov);
+        if (firstTodo) runLevel(firstTodo.level.id, { chain: true });
+      });
+      ov.querySelectorAll('.onboarding-level-item[data-run]').forEach(function (item) {
+        item.addEventListener('click', function () {
+          var id = item.getAttribute('data-run');
+          closeOverlay(ov);
+          if (!running) runLevel(id, { chain: false });
+        });
+      });
+      ov.querySelectorAll('[data-page-guide]').forEach(function (item) {
+        item.addEventListener('click', function () {
+          var page = item.getAttribute('data-page-guide');
+          closeOverlay(ov);
+          openPageGuideFromOverview(page);
+        });
+      });
+    });
+  }
+
+  // ---------- 重试 / 跳过对话框 ----------
+  function showRetryDialog(message) {
+    return new Promise(function (resolve) {
+      closeAllOverlays();
+      var ov = createOverlay('onboarding-retry-overlay', '' +
+        '<div class="onboarding-welcome-card" style="max-width:420px;text-align:left">' +
+          '<h2 style="font-size:18px;margin:0 0 10px">这一步暂时无法继续</h2>' +
+          '<p style="color:var(--text-secondary);font-size:13px;line-height:1.7;margin:0 0 18px">' + escapeHtml(message || '目标页面元素未出现,可能页面还没加载完,或需要先在页面里选中某项。') + '</p>' +
+          '<div class="onboarding-welcome-actions" style="flex-direction:column;align-items:stretch">' +
+            '<button type="button" class="btn" data-retry="retry">重试</button>' +
+            '<button type="button" class="btn btn-secondary" data-retry="skip">跳过此步</button>' +
+            '<button type="button" class="btn btn-secondary" data-retry="stop">停止引导</button>' +
+          '</div>' +
+        '</div>', 'onboarding-welcome-overlay');
+      ov.querySelector('[data-retry="retry"]').addEventListener('click', function () { closeOverlay(ov); resolve('retry'); });
+      ov.querySelector('[data-retry="skip"]').addEventListener('click', function () { closeOverlay(ov); resolve('skip'); });
+      ov.querySelector('[data-retry="stop"]').addEventListener('click', function () { closeOverlay(ov); resolve('stop'); });
+      ov.addEventListener('click', function (e) { if (e.target === ov) { closeOverlay(ov); resolve('stop'); } });
+    });
+  }
+
+  // ---------- 关卡完成庆祝(CSS 撒花) ----------
+  function celebration() {
+    closeOverlayById('onboarding-confetti');
+    var wrap = document.createElement('div');
+    wrap.className = 'onboarding-confetti';
+    wrap.id = 'onboarding-confetti';
+    document.body.appendChild(wrap);
+    var colors = ['#fa8c16', '#52c41a', '#1890ff', '#faad14', '#eb2f96', '#722ed1'];
+    for (var i = 0; i < 42; i++) {
+      var p = document.createElement('span');
+      p.className = 'onboarding-confetti-piece';
+      p.style.left = (Math.random() * 100) + 'vw';
+      p.style.background = colors[i % colors.length];
+      p.style.setProperty('--cf-x', (Math.random() * 180 - 90).toFixed(0) + 'px');
+      p.style.setProperty('--cf-y', (Math.random() * 220 + 380).toFixed(0) + 'px');
+      p.style.setProperty('--cf-r', (Math.random() * 720 - 360).toFixed(0) + 'deg');
+      p.style.animationDelay = (Math.random() * 0.3).toFixed(2) + 's';
+      wrap.appendChild(p);
+    }
+    setTimeout(function () { closeOverlayById('onboarding-confetti'); }, 2300);
+    toast('恭喜完成 🎉');
+  }
+
+  // ---------- 关卡执行 ----------
+  function currentAccountId() {
+    try { return localStorage.getItem('qiwei-current-account') || ''; }
+    catch (e) { return ''; }
+  }
+
+  function runLevel(levelId, opts) {
+    opts = opts || {};
+    if (running) { toast('已有引导进行中,请先完成或关闭'); return; }
+    var level = LEVELS.find(function (l) { return l.id === levelId; });
+    if (!level) return;
+    closeAllOverlays();
+    closePanel();
+
+    computeProgress().then(function (prog0) {
+      // 订阅关卡在未绑定时先引导绑定(订阅卡登录后才可见)
+      if (levelId === 'subscribe') {
+        var acc0 = prog0.find(function (x) { return x.level.id === 'account'; });
+        if (acc0 && acc0.status !== 'done') {
+          toast('请先完成「绑定企微账号」,绑定后订阅通常会自动完成');
+          runLevel('account', { chain: true });
+          return;
+        }
+      }
+      if (isDone(prog0, levelId)) {
+        markDone(levelId);
+        if (opts.chain) {
+          var n0 = nextTodo(prog0, levelId);
+          if (n0) { runLevel(n0, { chain: true }); return; }
+        }
+        openOverview();
+        return;
+      }
+
+      running = true;
+      aborted = false;
+      runLevelSteps(level, { accountKey: currentAccountId() }).then(function (result) {
+        if (result === 'completed') {
+          markDone(levelId);
+          if (level.id === 'welcome') setLocal({ welcomeGuided: true });
+          celebration();
+          if (opts.chain) {
+            return computeProgress().then(function (prog3) {
+              var n = nextTodo(prog3, levelId);
+              if (n) {
+                setTimeout(function () { runLevel(n, { chain: true }); }, 900);
+                return;
+              }
+              setTimeout(openOverview, 0);
+            });
+          }
+          setTimeout(openOverview, 0);
+        } else if (result === 'reset') {
+          // 从头开始:清空行为标记后重新走欢迎引导(延迟到本关 finally 完成后启动)
+          resetGuideProgress();
+          setTimeout(function () { runLevel('welcome', { chain: true }); }, 400);
+        } else if (result === 'other') {
+          setTimeout(openOverview, 0);
+        } else {
+          refreshProgressBadge();
+        }
+      }).finally(function () {
+        running = false;
+        closeDriver();
+        closePanel();
+        refreshProgressBadge();
+      });
+    });
+  }
+
+  function runLevelSteps(level, opts) {
+    var idx = 0;
+    var result = 'stopped';
+    var stop = false;
+    function loop() {
+      if (stop || aborted) return Promise.resolve(result);
+      if (idx >= level.steps.length) { result = 'completed'; return Promise.resolve(result); }
+      var i = idx;
+      var stepDef = level.steps[i];
+      var isLast = i === level.steps.length - 1;
+      var doneText = level.doneText || '完成本步';
+      var prep;
+      if (stepDef.mode === 'panel') {
+        prep = Promise.resolve().then(function () {
+          if (stepDef.page) return goto(stepDef.page);
+        });
+      } else {
+        prep = Promise.resolve().then(function () {
+          if (stepDef.page) return goto(stepDef.page);
+        }).then(function () {
+          if (stepDef.prep) return stepDef.prep();
+        }).then(function () {
+          return waitForElement(stepDef.element, stepDef.timeout || 9000);
+        });
+      }
+      return prep.then(function (el) {
+        // 账号切换检测:切号后停止本次引导
+        if (opts.accountKey && currentAccountId() !== opts.accountKey) {
+          stop = true;
+          toast('检测到账号切换,本次引导已停止');
+          return loop();
+        }
+        if (aborted) { stop = true; return loop(); }
+        var show = stepDef.mode === 'panel'
+          ? showPanelStep(stepDef, { first: i === 0, last: isLast, doneText: doneText })
+          : showDriverStep(el, stepDef, { first: i === 0, last: isLast, doneText: doneText });
+        return show.then(function (res) {
+          if (aborted) { stop = true; return loop(); }
+          if (res === 'close') { stop = true; return loop(); }
+          if (res === 'prev') { idx = i > 0 ? i - 1 : 0; return loop(); }
+          // next:真实业务关卡,检测上一步是否完成;未完成则重新指引/提供其他出口
+          if (isLast && level.autoWait) {
+            return waitForLevelDone(level.id, 15000).then(function (ok) {
+              if (aborted) { stop = true; return loop(); }
+              if (ok) { idx = level.steps.length; return loop(); }
+              return showStepCheckDialog(level).then(function (action) {
+                if (aborted) { stop = true; return loop(); }
+                if (action === 'done') { idx = level.steps.length; return loop(); }
+                if (action === 'restart') { idx = 0; return loop(); }
+                if (action === 'reset') { stop = true; result = 'reset'; return loop(); }
+                if (action === 'other') { stop = true; result = 'other'; return loop(); }
+                if (action === 'skip') { idx = level.steps.length; return loop(); }
+                return loop();
+              });
+            });
+          }
+          idx = i + 1;
+          return loop();
+        });
+      }).catch(function () {
+        if (aborted) { stop = true; return loop(); }
+        return showRetryDialog().then(function (d) {
+          if (d === 'stop') { stop = true; return loop(); }
+          if (d === 'skip') { idx = i + 1; return loop(); }
+          return loop(); // retry:重跑当前步骤
+        });
+      });
+    }
+    return loop();
+  }
+
+  // ---------- 每页引导 ----------
+  function markPageSeen(page) {
+    var pageSeen = Object.assign({}, local.pageSeen || {});
+    pageSeen[page] = true;
+    setLocal({ pageSeen: pageSeen });
+  }
+
+  // 运行某个页面的引导;元素缺失/超时的步骤自动跳过;全程一屏未展示则不标记已看
+  function runPageGuide(page) {
+    var guide = PAGE_GUIDES[page];
+    if (!guide || running || pageGuideRunning) return;
+    var idx = 0;
+    var shown = 0;
+    pageGuideRunning = true;
+    aborted = false;
+    function finish() {
+      pageGuideRunning = false;
+      explicitPageGuide = null;
+      closeDriver();
+      closePanel();
+      if (shown > 0) markPageSeen(page);
+    }
+    function next() {
+      if (aborted || running) { finish(); return; }
+      if (idx >= guide.steps.length) { finish(); return; }
+      var i = idx;
+      var step = guide.steps[i];
+      var isLast = i === guide.steps.length - 1;
+      var promise;
+      if (step.mode === 'panel') {
+        promise = showPanelStep(step, { first: i === 0, last: isLast, doneText: '完成' });
+      } else {
+        promise = waitForElement(step.element, step.timeout || 5000).then(function (el) {
+          shown++;
+          return showDriverStep(el, step, { first: i === 0, last: isLast, doneText: '完成' });
+        });
+      }
+      promise.then(function (res) {
+        if (aborted || running) { finish(); return; }
+        if (res === 'close') { finish(); return; }
+        if (res === 'prev' && i > 0) { idx = i - 1; return next(); }
+        idx = i + 1;
+        next();
+      }).catch(function () {
+        // 元素未出现:首步失败则中止(页面可能未就绪或需登录,不误标已看);后续步跳过继续
+        if (i === 0) { finish(); return; }
+        idx = i + 1;
+        next();
+      });
+    }
+    next();
+  }
+
+  // 用户进入页面后自动触发该页引导(仅首次;已在总览主动打开时跳过以免重复)
+  function maybeAutoPageGuide() {
+    var page = location.hash.slice(1) || 'overview';
+    if (explicitPageGuide) {
+      if (explicitPageGuide === page) { explicitPageGuide = null; return; }
+      explicitPageGuide = null;
+    }
+    if (running || pageGuideRunning || !local.welcomeSeen) return;
+    if (!PAGE_GUIDES[page]) return;
+    if (local.pageSeen && local.pageSeen[page]) return;
+    runPageGuide(page);
+  }
+
+  // 从总览主动打开某页引导(重播,无论是否已看过)
+  function openPageGuideFromOverview(page) {
+    if (running || pageGuideRunning) { toast('引导进行中,请先完成或关闭'); return; }
+    closeAllOverlays();
+    closePanel();
+    explicitPageGuide = page;
+    if ((location.hash.slice(1) || 'overview') !== page) {
+      location.hash = '#' + page;
+    }
+    setTimeout(function () { runPageGuide(page); }, 600);
+  }
+
+  // ---------- 侧边栏徽标 ----------
+  function refreshProgressBadge() {
+    var badge = document.getElementById(BADGE_ID);
+    if (!badge) return;
+    computeProgress().then(function (prog) {
+      var done = prog.filter(function (p) { return p.status === 'done'; }).length;
+      var total = prog.length;
+      badge.textContent = done + '/' + total;
+      badge.hidden = false;
+      badge.classList.toggle('is-done', done >= total);
+    }).catch(function () { /* 失败保持现状 */ });
+  }
+
+  // ---------- 启动 ----------
+  function bindEntry() {
+    var entry = document.getElementById(ENTRY_ID);
+    if (!entry) return;
+    entry.addEventListener('click', function (e) {
+      e.preventDefault();
+      openOverview();
+    });
+  }
+
+  function boot() {
+    bindEntry();
+    refreshProgressBadge();
+    if (!local.welcomeSeen) {
+      setTimeout(showWelcome, 600);
+    } else {
+      // 已处理欢迎层:对初始页面按需自动触发每页引导
+      setTimeout(maybeAutoPageGuide, 800);
+    }
+    setInterval(refreshProgressBadge, 30000);
+    window.addEventListener('hashchange', function () {
+      closeAllOverlays();
+      closePanel();
+      refreshProgressBadge();
+      // 用户手动切页(非引导自身导航)→ 终止本次关卡引导,避免 running 卡死
+      if (running && !navigating) { stopGuide('已切换页面,本次引导已停止,可重新开始'); return; }
+      // 用户切页 → 400ms 后按需自动触发该页引导
+      if (!navigating) setTimeout(maybeAutoPageGuide, 400);
+    });
+  }
+
+  if (document.readyState === 'loading') {
+    document.addEventListener('DOMContentLoaded', boot);
+  } else {
+    boot();
+  }
+})();

+ 452 - 0
mcp/src/dashboard/onboarding.css

@@ -0,0 +1,452 @@
+/* ============================================================
+ * 新手闯关引导样式(onboarding)
+ * 与 styles.css 的设计令牌保持一致:--brand: #fa8c16 等
+ * 覆盖四块:侧边栏入口徽标 / 欢迎层 / 关卡总览 Modal / driver 主题
+ * ============================================================ */
+
+/* ---------- 侧边栏常驻入口 ---------- */
+.onboarding-nav-entry .onboarding-progress-badge {
+  margin-left: auto;
+  min-width: 34px;
+  padding: 2px 8px;
+  border-radius: 999px;
+  background: var(--brand);
+  color: #fff;
+  font-size: 11px;
+  font-weight: 600;
+  line-height: 1.6;
+  text-align: center;
+}
+.onboarding-nav-entry .onboarding-progress-badge.is-done {
+  background: var(--success);
+}
+.onboarding-nav-entry.onboarding-entry-blink .onboarding-progress-badge {
+  animation: onboarding-blink 1.4s ease-in-out infinite;
+}
+@keyframes onboarding-blink {
+  0%, 100% { box-shadow: 0 0 0 0 rgba(250, 140, 22, 0.55); }
+  50% { box-shadow: 0 0 0 8px rgba(250, 140, 22, 0); }
+}
+
+/* ---------- 欢迎层(全屏) ---------- */
+.onboarding-welcome-overlay {
+  position: fixed;
+  inset: 0;
+  z-index: 2147483000;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: rgba(15, 17, 32, 0.82);
+  backdrop-filter: blur(3px);
+  animation: onboarding-fade-in 0.28s ease-out;
+}
+.onboarding-welcome-card {
+  width: min(560px, 88vw);
+  max-height: 86vh;
+  overflow: auto;
+  border-radius: var(--radius-lg);
+  background: var(--panel-bg);
+  box-shadow: 0 24px 80px rgba(0, 0, 0, 0.45);
+  padding: 34px 36px 28px;
+  text-align: center;
+  animation: onboarding-pop-in 0.34s cubic-bezier(0.22, 1.2, 0.36, 1);
+}
+.onboarding-welcome-flag {
+  width: 72px;
+  height: 72px;
+  margin: 0 auto 16px;
+  border-radius: 20px;
+  background: var(--brand-bg);
+  color: var(--brand);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  animation: onboarding-float 2.6s ease-in-out infinite;
+}
+.onboarding-welcome-flag svg {
+  width: 40px;
+  height: 40px;
+}
+.onboarding-welcome-card h2 {
+  margin: 0 0 8px;
+  font-size: 24px;
+  color: var(--text-primary);
+}
+.onboarding-welcome-card .onboarding-welcome-sub {
+  margin: 0 0 18px;
+  font-size: 14px;
+  color: var(--text-secondary);
+  line-height: 1.7;
+}
+.onboarding-welcome-levels {
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  gap: 8px;
+  margin: 0 0 22px;
+  text-align: left;
+}
+.onboarding-welcome-level {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 8px 10px;
+  border-radius: var(--radius-sm);
+  background: var(--page-bg);
+  font-size: 13px;
+  color: var(--text-primary);
+}
+.onboarding-welcome-level .onboarding-wl-num {
+  flex: none;
+  width: 22px;
+  height: 22px;
+  border-radius: 50%;
+  background: var(--brand);
+  color: #fff;
+  font-size: 12px;
+  font-weight: 600;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.onboarding-welcome-actions {
+  display: flex;
+  gap: 12px;
+  justify-content: center;
+}
+.onboarding-welcome-actions .btn {
+  min-width: 120px;
+}
+.onboarding-welcome-tip {
+  margin: 14px 0 0;
+  font-size: 12px;
+  color: var(--text-muted);
+}
+
+/* ---------- 关卡总览 Modal ---------- */
+.onboarding-overview-head {
+  display: flex;
+  align-items: center;
+  gap: 14px;
+  margin-bottom: 16px;
+}
+.onboarding-overview-head .onboarding-ov-flag {
+  flex: none;
+  width: 44px;
+  height: 44px;
+  border-radius: 12px;
+  background: var(--brand-bg);
+  color: var(--brand);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.onboarding-overview-head .onboarding-ov-flag svg {
+  width: 24px;
+  height: 24px;
+}
+.onboarding-overview-head h3 {
+  margin: 0;
+  font-size: 16px;
+  color: var(--text-primary);
+}
+.onboarding-overview-head p {
+  margin: 2px 0 0;
+  font-size: 12px;
+  color: var(--text-muted);
+}
+.onboarding-progress-track {
+  height: 8px;
+  border-radius: 999px;
+  background: var(--page-bg);
+  margin: 0 0 16px;
+  overflow: hidden;
+}
+.onboarding-progress-track .onboarding-progress-fill {
+  height: 100%;
+  border-radius: 999px;
+  background: linear-gradient(90deg, var(--brand) 0%, #ffb469 100%);
+  transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1);
+}
+.onboarding-level-list {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+  max-height: 48vh;
+  overflow: auto;
+  margin: 0 0 18px;
+}
+.onboarding-level-item {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  padding: 12px 14px;
+  border-radius: var(--radius-sm);
+  border: 1px solid var(--border);
+  background: var(--panel-bg);
+  cursor: pointer;
+  transition: border-color 0.2s, box-shadow 0.2s;
+}
+.onboarding-level-item:hover {
+  border-color: var(--brand);
+  box-shadow: var(--shadow);
+}
+.onboarding-level-item.is-done {
+  background: var(--success-bg);
+  border-color: var(--success-border);
+}
+.onboarding-level-item .onboarding-level-state {
+  flex: none;
+  width: 30px;
+  height: 30px;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 15px;
+  font-weight: 700;
+  color: #fff;
+}
+.onboarding-level-item.is-done .onboarding-level-state {
+  background: var(--success);
+}
+.onboarding-level-item.is-todo .onboarding-level-state {
+  background: var(--brand);
+}
+.onboarding-level-item.is-pending .onboarding-level-state {
+  background: var(--text-muted);
+}
+.onboarding-level-item .onboarding-level-main {
+  flex: 1;
+  min-width: 0;
+}
+.onboarding-level-item .onboarding-level-title {
+  font-size: 14px;
+  font-weight: 600;
+  color: var(--text-primary);
+}
+.onboarding-level-item .onboarding-level-desc {
+  margin-top: 2px;
+  font-size: 12px;
+  color: var(--text-muted);
+  line-height: 1.5;
+}
+.onboarding-level-item .onboarding-level-cta {
+  flex: none;
+}
+/* ---------- 总览:布局与底部固定操作栏 ---------- */
+.onboarding-overview-card {
+  display: flex;
+  flex-direction: column;
+  max-height: 92vh;
+  overflow: hidden;
+  padding: 26px 30px 18px;
+}
+.onboarding-overview-body {
+  flex: 1;
+  min-height: 0;
+  overflow: auto;
+  padding-right: 4px;
+}
+/* 总览卡片内:让中间内容区统一滚动,避免内层再出现独立滚动条 */
+.onboarding-overview-card .onboarding-level-list,
+.onboarding-overview-card .onboarding-page-guides-list {
+  max-height: none;
+  overflow: visible;
+}
+.onboarding-overview-foot {
+  flex: none;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+  margin-top: 14px;
+  padding-top: 12px;
+  border-top: 1px solid var(--border);
+  background: var(--panel-bg);
+}
+
+/* ---------- 完成检测对话框 ---------- */
+.onboarding-check-actions {
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  gap: 8px;
+}
+.onboarding-check-actions .btn {
+  justify-content: center;
+}
+@media (max-width: 480px) {
+  .onboarding-check-actions { grid-template-columns: 1fr; }
+}
+
+/* ---------- 总览:页面引导区块 ---------- */
+.onboarding-page-guides {
+  margin: 18px 0 0;
+  border-top: 1px solid var(--border);
+  padding-top: 14px;
+}
+.onboarding-page-guides-head {
+  display: flex;
+  align-items: baseline;
+  gap: 8px;
+  font-size: 14px;
+  font-weight: 600;
+  color: var(--text-primary);
+  margin-bottom: 10px;
+}
+.onboarding-page-guides-head small {
+  font-weight: 400;
+  font-size: 12px;
+  color: var(--text-muted);
+}
+.onboarding-page-guides-list {
+  max-height: 30vh;
+  overflow: auto;
+  display: flex;
+  flex-direction: column;
+}
+.onboarding-page-guide-item .onboarding-level-desc {
+  font-size: 11px;
+}
+
+/* ---------- Driver.js 主题覆盖(对齐 --brand) ---------- */
+.onboarding-driver-popover {
+  border-radius: 12px;
+  border: 1px solid var(--brand-bg);
+  box-shadow: 0 8px 40px rgba(0, 0, 0, 0.18);
+}
+.onboarding-driver-popover .driver-popover-title {
+  color: var(--text-primary);
+}
+.onboarding-driver-popover .driver-popover-description {
+  color: var(--text-secondary);
+}
+.onboarding-driver-popover .driver-popover-footer button {
+  border-radius: 6px;
+  border: 1px solid var(--border);
+  font-size: 13px;
+  padding: 5px 12px;
+}
+.onboarding-driver-popover .driver-popover-footer button:hover,
+.onboarding-driver-popover .driver-popover-footer button:focus {
+  background: var(--brand-bg);
+}
+.onboarding-driver-popover .driver-popover-footer button.driver-popover-btn-primary {
+  background: var(--brand);
+  border-color: var(--brand);
+  color: #fff;
+  text-shadow: none;
+}
+.onboarding-driver-popover .driver-popover-footer button.driver-popover-btn-primary:hover,
+.onboarding-driver-popover .driver-popover-footer button.driver-popover-btn-primary:focus {
+  background: var(--brand-dark);
+  border-color: var(--brand-dark);
+}
+.onboarding-driver-popover .driver-popover-close-btn:hover {
+  color: var(--brand);
+}
+/* 高亮元素描边改成品牌色 */
+.driver-active-element {
+  box-shadow: 0 0 0 4px var(--brand), 0 0 24px rgba(250, 140, 22, 0.45) !important;
+  border-radius: 10px !important;
+  transition: box-shadow 0.2s;
+}
+
+/* ---------- 引导内极简提示 ---------- */
+.onboarding-toast {
+  position: fixed;
+  top: 18px;
+  left: 50%;
+  transform: translateX(-50%);
+  z-index: 2147483600;
+  max-width: 80vw;
+  padding: 10px 18px;
+  border-radius: 8px;
+  background: rgba(26, 26, 46, 0.95);
+  color: #fff;
+  font-size: 13px;
+  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.25);
+  animation: onboarding-fade-in 0.2s ease-out;
+}
+
+/* ---------- 引导悬浮说明卡(不遮挡页面) ---------- */
+.onboarding-panel {
+  position: fixed;
+  right: 20px;
+  bottom: 24px;
+  z-index: 2147483500;
+  width: min(360px, 86vw);
+  border-radius: 14px;
+  background: var(--panel-bg);
+  border: 1px solid var(--border);
+  box-shadow: var(--shadow-hover);
+  padding: 16px 18px;
+  animation: onboarding-pop-in 0.3s ease-out;
+}
+.onboarding-panel-title {
+  font-size: 15px;
+  font-weight: 600;
+  color: var(--text-primary);
+  margin-bottom: 6px;
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+.onboarding-panel-title::before {
+  content: '';
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  background: var(--brand);
+  box-shadow: 0 0 0 3px var(--brand-bg);
+}
+.onboarding-panel-desc {
+  font-size: 13px;
+  color: var(--text-secondary);
+  line-height: 1.7;
+  margin-bottom: 14px;
+}
+.onboarding-panel-actions {
+  display: flex;
+  gap: 8px;
+  justify-content: flex-end;
+}
+
+/* ---------- 通用动画 ---------- */
+@keyframes onboarding-fade-in {
+  from { opacity: 0; }
+  to { opacity: 1; }
+}
+@keyframes onboarding-pop-in {
+  from { opacity: 0; transform: translateY(16px) scale(0.96); }
+  to { opacity: 1; transform: translateY(0) scale(1); }
+}
+@keyframes onboarding-float {
+  0%, 100% { transform: translateY(0); }
+  50% { transform: translateY(-6px); }
+}
+
+/* ---------- 关卡完成庆祝(CSS 撒花) ---------- */
+.onboarding-confetti {
+  position: fixed;
+  top: 0;
+  left: 50%;
+  z-index: 2147483400;
+  pointer-events: none;
+}
+.onboarding-confetti-piece {
+  position: absolute;
+  width: 10px;
+  height: 14px;
+  border-radius: 2px;
+  animation: onboarding-confetti-fall 1.5s ease-in forwards;
+}
+@keyframes onboarding-confetti-fall {
+  0% { opacity: 1; transform: translate3d(0, -10px, 0) rotate(0deg); }
+  100% { opacity: 0; transform: translate3d(var(--cf-x, 0px), var(--cf-y, 480px), 0) rotate(var(--cf-r, 540deg)); }
+}
+
+/* 移动端:欢迎层关卡网格收成单列 */
+@media (max-width: 640px) {
+  .onboarding-welcome-levels { grid-template-columns: 1fr; }
+  .onboarding-overview-foot { flex-direction: column; align-items: stretch; }
+}

+ 38 - 4
mcp/src/dashboard/styles.css

@@ -946,7 +946,7 @@ html, body {
   background: var(--agent-panel-muted);
 }
 
-.agent-conversation-list { display: grid; grid-template-rows: auto auto minmax(0, 1fr); overflow: hidden; border-right: 1px solid var(--agent-line); }
+.agent-conversation-list { display: grid; grid-template-rows: auto auto auto minmax(0, 1fr); overflow: hidden; border-right: 1px solid var(--agent-line); }
 .agent-insight-panel { border-left: 1px solid var(--agent-line); overflow-y: auto; max-height: none; }
 
 .agent-panel-heading {
@@ -3321,6 +3321,9 @@ body.business-modal-open { overflow: hidden; }
 .knowledge-tree-panel > header strong { font-size: 14px; }
 .knowledge-tree-panel > header span { margin-top: 3px; color: var(--text-muted); font-size: 9px; }
 .knowledge-tree { max-height: 780px; overflow: auto; padding: 9px 7px 20px; }
+/* 防御:部分环境中 <details> 折叠的 UA 隐藏样式不生效,折叠文件夹内容会全部显示导致树内重叠错乱。
+   这里显式隐藏折叠内容,展开态不受影响。 */
+.knowledge-tree-folder:not([open]) > div { display: none; }
 .knowledge-tree-folder { margin: 2px 0; }
 .knowledge-tree-folder > summary { display: grid; grid-template-columns: 18px minmax(0, 1fr) auto; align-items: center; gap: 4px; padding: 8px 8px 8px calc(8px + var(--tree-depth) * 12px); border-radius: 8px; color: #42534d; cursor: pointer; list-style: none; }
 .knowledge-tree-folder > summary:hover { background: #eaf2ee; }
@@ -4719,7 +4722,7 @@ body.business-modal-open { overflow: hidden; }
 .agent-list-search input:focus { border-color: #6ba890; box-shadow: 0 0 0 3px rgba(19,122,91,.1); background: #fff; }
 .agent-conversation-item[hidden] { display: none; }
 
-@media (min-width: 1101px) {
+@media (min-width: 1251px) {
   .main.agent-shell { height: 100vh; min-height: 0; overflow: hidden; }
   .main.agent-shell .content.agent-content { height: auto; min-height: 0; flex: 1 1 auto; padding: 8px 10px 10px; overflow: hidden; }
   .agent-content .agent-page { height: 100%; min-height: 0; display: flex; flex-direction: column; gap: 6px; overflow: hidden; animation: none; }
@@ -4813,7 +4816,7 @@ body.business-modal-open { overflow: hidden; }
   .agent-page .rtp-disclosure summary { min-height: 36px; padding: 0 9px; }
 }
 
-@media (max-width: 1100px) {
+@media (max-width: 1250px) {
   .content.agent-content { padding: 10px; overflow: visible; }
   .agent-content .agent-page { display: block; }
   .agent-command-title { display: flex; align-items: center; gap: 10px; }
@@ -4824,7 +4827,7 @@ body.business-modal-open { overflow: hidden; }
 }
 
 /* ==================== Readable horizontal workspaces (V6) ==================== */
-@media (min-width: 1101px) {
+@media (min-width: 1251px) {
   .agent-page .agent-commandbar { min-height: 56px; }
   .agent-command-title strong { font-size: 15px; }
   .agent-command-title small { font-size: 11px; }
@@ -5107,3 +5110,34 @@ body.business-modal-open { overflow: hidden; }
   .customer-timeline-section > summary em,
   .optional-batch-ops > summary em { display: none; }
 }
+
+/* ==================== Agent 客服工作台:中宽度响应式修复 ==================== */
+/* 窗口缩小到 1051-1250px 时,三列工作区(260/420/330 最小合计 1010px)放不下,
+   会横向溢出并把右侧洞察面板推出屏幕。这里退回两列,洞察面板移到下方。 */
+@media (max-width: 1250px) and (min-width: 1051px) {
+  .agent-page .agent-workspace { grid-template-columns: 210px minmax(0, 1fr); }
+  .agent-page .agent-insight-panel {
+    grid-column: 1 / -1;
+    max-height: 400px;
+    display: grid;
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+    gap: 8px;
+    border-top: 1px solid var(--agent-line);
+    border-left: 0;
+  }
+}
+
+/* 聊天头部模式按钮较多(最多 6 个),任何宽度下都不再换行挤占/裁切,
+   空间不足时横向滚动,按钮保持原尺寸不挤压。 */
+.agent-conversation-modes {
+  min-width: 0;
+  flex-wrap: nowrap;
+  justify-content: flex-start;
+  overflow-x: auto;
+  scrollbar-width: thin;
+}
+.agent-conversation-modes .btn { flex: 0 0 auto; }
+@media (max-width: 760px) {
+  .agent-conversation-modes { width: 100%; justify-content: flex-start; }
+}
+

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
package.json


+ 102 - 0
scripts/onboarding-guide-smoke-test.js

@@ -0,0 +1,102 @@
+'use strict';
+
+// 新手闯关引导静态 smoke test
+// 无浏览器依赖:校验资源文件、引入顺序、关键锚点与 localStorage 键。
+// 运行:node scripts/onboarding-guide-smoke-test.js
+
+const assert = require('assert/strict');
+const fs = require('fs');
+const path = require('path');
+const { spawnSync } = require('child_process');
+
+const ROOT = path.resolve(__dirname, '..');
+const DASH = path.join(ROOT, 'mcp', 'src', 'dashboard');
+
+function read(file) {
+  return fs.readFileSync(path.join(DASH, file), 'utf8');
+}
+
+function testStaticAssetsExist() {
+  ['driver.min.js', 'driver.css', 'onboarding.css', 'onboarding-guide.js'].forEach((name) => {
+    const stat = fs.statSync(path.join(DASH, name));
+    assert(stat.size > 0, `${name} 应为非空文件`);
+  });
+}
+
+function testIndexHtmlReferences() {
+  const html = read('index.html');
+  // 资源必须被引入,版本号随改动变化,只断言路径前缀
+  assert.match(html, /\/dashboard\/driver\.css\?v=/);
+  assert.match(html, /\/dashboard\/onboarding\.css\?v=/);
+  assert.match(html, /\/dashboard\/driver\.min\.js\?v=/);
+  assert.match(html, /\/dashboard\/onboarding-guide\.js\?v=/);
+  // 加载顺序:driver.min.js 必须先于 onboarding-guide.js
+  const driverIdx = html.indexOf('driver.min.js');
+  const guideIdx = html.indexOf('onboarding-guide.js');
+  assert(driverIdx > -1 && guideIdx > driverIdx, 'driver.min.js 应加载于 onboarding-guide.js 之前');
+}
+
+function testSidebarEntry() {
+  const html = read('index.html');
+  const start = html.indexOf('<a class="nav-item onboarding-nav-entry"');
+  assert(start > -1, '侧边栏应包含 id="onboarding-nav-entry" 的入口');
+  const end = html.indexOf('</a>', start);
+  assert(end > start, '入口 <a> 应闭合');
+  const entry = html.slice(start, end + 4);
+  assert(entry.includes('id="onboarding-nav-entry"'), '入口应带 id');
+  assert(entry.includes('onboarding-progress-badge'), '入口应包含进度徽标');
+  // 入口绝不能带 data-page(避免被 renderPage 当路由 / setActiveNav 匹配)
+  assert(!/data-page\s*=/.test(entry), '新手闯关入口不得包含 data-page,否则会与 hash 路由冲突');
+}
+
+function testGuideSourceContainsKeyMarkers() {
+  const src = read('onboarding-guide.js');
+  ['qiwei-onboarding-v1', '/api/status', '/api/agent/status', '/api/agent/voice/status',
+   '#btn-subscribe-action', '[data-agent-action="manage-allowlist"]', '#allowlist-contact-list',
+   '#allowlist-save', '#voice-profile-body', '[data-voice-save]', "ENTRY_ID = 'onboarding-nav-entry'"].forEach((marker) => {
+    assert(src.includes(marker), `onboarding-guide.js 应包含关键标记:${marker}`);
+  });
+  // 关卡完整性:5 个设置关卡(巡览关已改为每页引导)
+  ['welcome', 'subscribe', 'account', 'allowlist', 'voice'].forEach((id) => {
+    assert(new RegExp(`id: '${id}'`).test(src), `应定义关卡:${id}`);
+  });
+  // 不再存在 tour 关卡
+  assert(!new RegExp(`id: 'tour'`).test(src), '不应再有关卡 tour(已改为每页引导)');
+  // 每页引导:注册表 + 引擎 + 自动触发标记
+  ['PAGE_GUIDES', 'pageSeen', 'runPageGuide', 'maybeAutoPageGuide', 'openPageGuideFromOverview'].forEach((marker) => {
+    assert(src.includes(marker), `onboarding-guide.js 应包含每页引导标记:${marker}`);
+  });
+  // 覆盖全部页面
+  ['overview', 'analytics', 'agent', 'knowledge', 'skills', 'customer-ops', 'portraits', 'transfers', 'group-ops', 'groups', 'status'].forEach((pg) => {
+    assert(new RegExp(`['"]?${pg}['"]?: \\{`).test(src), `PAGE_GUIDES 应覆盖页面:${pg}`);
+  });
+}
+
+function testGuideSyntax() {
+  const result = spawnSync(process.execPath, ['--check', path.join(DASH, 'onboarding-guide.js')], {
+    encoding: 'utf8',
+    windowsHide: true,
+  });
+  assert.equal(result.status, 0, result.stderr || 'onboarding-guide.js 语法错误');
+}
+
+function testPackageJsonScript() {
+  const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
+  assert.equal(typeof pkg.scripts['onboarding:smoke'], 'string', 'package.json 应有 onboarding:smoke 脚本');
+}
+
+function main() {
+  testStaticAssetsExist();
+  testIndexHtmlReferences();
+  testSidebarEntry();
+  testGuideSourceContainsKeyMarkers();
+  testGuideSyntax();
+  testPackageJsonScript();
+  process.stdout.write(`${JSON.stringify({ status: 'ok', checks: 6 }, null, 2)}\n`);
+}
+
+try { main(); }
+catch (error) {
+  process.stderr.write(`${error.message}\n`);
+  process.exit(1);
+}

Некоторые файлы не были показаны из-за большого количества измененных файлов