跳转到内容

内置演示脚本 ​

音频需单独生成

内置演示脚本的音频不能直接使用官网生成的音频:官网直接导出的音频结尾带有「嗡嗡」杂音。需单独写一个工具调用接口来生成这些音频,保证音质干净、可复现。

系统导览模式(demo)面向大屏演示汇报,是一套确定性脚本:不调用大模型生成回答,全部走本地内置问答库 + NLU 语义识别,保证演示稳定、可复现。


一、内置演示脚本 ​

脚本定义在 data/demoQA.js(demoList 同时用于生成"演示指令"下拉按钮,新增条目即自动多一个按钮),匹配方式为 Fuse.js 模糊搜索(threshold: 0.9,keys: ['question'],取 Top1),兜底回复 fallbackReply,开场白 startReply("你好,我是管廊系统数智人小慧…")。

实现位置(会话核心已拆分为组装层 + core/ 模块,见前端项目结构):脚本数据在 data/demoQA.js;导览播放编排(分段循环、打字机、段内预取、段间 UE 预投喂 / 双工预准备、段结束动作)在 core/demoTour.js;functionTool 派发到 clientTools 在 core/clientTools.js;语音播放在 core/speech.js。

#指令回复文本触发的工具音频
1智能解说11 段讲解词,由 buildIntroAnswer() 异步构造、按当前账号实时填充统计数字;第 1 段 introduce2(keepOpen) 打开介绍弹框,第 4 段结束 closeIntroModal 关闭;第 5 段含动态统计(承建单位/管廊主轴里程/管线企业/管线数/管线长度/介质数),需实时 TTS 合成;第 8–11 段依次 switchModule 切到巡检/设备/隐患/施工模块introduce2 / closeIntroModal / switchModuleintro/intro_p1~p11.mp3(第 5 段为实时 TTS,无预录音频)
2查看气体设备"好的,地图上即将展示气体设备"selectDeviceCategory {category1:'气体', category2:null}device_气体.mp3
3今日在线设备"好的,这就为您打开在线设备信息"lookDeviceStatusDialog {module:'device', status:'online'}device_在线设备.mp3
4执行中的巡检任务"好的,这就为您打开执行中的巡检任务信息"lookInspectionStatusDialog {module:'inspection', status:'执行中'}inspection_执行中.mp3
5查看在建的管线"好的,这就为您查看在建管线信息"lookPipelineStatusDialog {module:'pipeLine', status:'在建'}pipeLine_在建.mp3
6管线隐患有哪些"好的,地图将展示管线隐患相关点位。"lookHazardStatusOrLevelMap {module:'hazard', type:'map', mapType:'category', categoryName:'管线隐患'}hazard_管线隐患.mp3
7漫游(已隐藏)"好的,这就为您演示漫游功能"openRoamPanel + closeAssistantroam.mp3
8作业单位有哪些"好的,这就为您打开作业单位信息"lookZypDialog {status:'作业单位'}construct_作业单位.mp3
9作业票有哪些"好的,这就为您打开作业票信息"lookZypDialog {status:'作业票'}construct_作业票.mp3
10高处作业有哪些"好的,地图即将展示高处作业票点位信息"lookZypTypeMap {type:'map', jobName:'高处'}construct_高处.mp3

指令 7「漫游」会同时调用 closeAssistant 隐藏对话面板;漫游进行中大屏还会通过 isRoaming 监听强制保持面板隐藏。

原「管廊系统平台介绍」已升级为「智能解说」:answer 改为 () => buildIntroAnswer() 异步构造函数,讲解词按当前登录账号实时拉取统计数字(承建单位、管廊主轴里程、管线企业、管线数、管线长度、介质数等)动态拼句,不同账号看到的数据不同;由 11 段预录音频(第 1–4、6–11 段)+ 第 5 段实时 TTS 组成,第 5 段因含动态数字必须实时合成。


二、自定义输入的 NLU 语义识别与动作触发 ​

用户输入(含系统导览下的自定义问题、智能交互、专家问答)都会异步 fire-and-forget 调用 NLU:

POST /digital-human/nlu/recognize  { text, mode }
  • 响应解析:兼容 JSON 对象 / key=value##key=value 串 / 单个 key=value / null(无匹配)。
  • 回复拼接 buildAnswer(nlu):前端依据结构化字段确定性拼接待朗读文本 + 本地音频路径(无 audio 则回退实时 TTS)。
  • 动作映射 buildToolFromNlu(nlu):把结构化意图映射到 clientTools 中的方法并 await 调用。
  • ID 推导:STATUS_TO_ID 表由 module + status 前端推导数字 id,避免 LLM 输出格式错误。

已覆盖的业务模块与取值:

moduletype识别字段触发工具示例说法
hazardmapmapType=level/levelNamelookHazardStatusOrLevelMap"展示高危隐患点位"
hazardmapmapType=category/categoryNamelookHazardStatusOrLevelMap"看看管线隐患有哪些"
hazarddialogstatus=当前隐患/今日新增/今日处理lookHazardStatusDialog"今日新增了多少隐患"
devicedialogstatus=online/offline/alarm/total/deallookDeviceStatusDialog"打开今日报警设备"
devicemapcategory1/category2selectDeviceCategory"地图上显示位移设备"
constructdialogstatus=作业单位/作业票lookZypDialog"作业单位都有哪些"
constructmapjobNamelookZypTypeMap"动火作业票在哪"
inspection—status=巡检任务/待执行/已完成/未完成/执行中/超期lookInspectionStatusDialog"有哪些超期的巡检任务"
pipeLine—status=total/在建/未投用/已投用/已停用lookPipelineStatusDialog"查看已停用管线"
gallery—value=introintroduce2"介绍一下管廊平台"
gallery—仅 moduleswitchGallery"切到管廊模块"
roam——openRoamPanel"开始漫游"

兜底策略:

  • NLU 返回无法识别(buildAnswer 返回 null)→ 追加"暂时无法回答您的问题" + 播放 audios/无法回答.mp3(失败回退 TTS),跳过动作触发;
  • NLU 请求异常 → 追加系统消息提示,并在移动端等待回合时兜底回执,避免手机端一直"思考中"。

三、客户端工具与大屏事件总线 ​

clientTools 由 views/home/index.vue 注入,合并了通用工具(mapLocate、cameraFlyTo、closeAssistant)与演示工具 demoFuncs.js 的 funcList(14 个函数):

jump、switchGallery、introduce、introduce2、openInfoPanel、openRoamPanel、selectDeviceCategory、lookDeviceStatusDialog、lookInspectionStatusDialog、lookPipelineStatusDialog、lookHazardStatusOrLevelMap、lookHazardStatusDialog、lookZypDialog、lookZypTypeMap

解耦方式:demoFuncs.js 不直接依赖业务 store,统一通过轻量事件总线 demoBus.js(基于原生 EventTarget)派发事件,由各业务模块自行订阅:

事件载荷含义
switch-menu'device' | 'inspection' | 'pipeLine' | 'hazard' | 'construct' | 'gallery'切换大屏底部模块
open-pipe-listtrue / { type:'lifecycle' } / { location_name, detail }打开期数列表 / 飞行定位
open-intro-gallery—管廊平台介绍特效
hideWindows—关闭所有弹窗
select-device-category{ category1, category2 }勾选设备分类
look-device-status-dialog{ status }打开设备状态弹窗
look-inspection-status-dialog{ status }打开巡检任务弹窗
look-pipeline-status-dialog / look-pipeline-total-dialog{ status }打开管线状态弹窗
look-hazard-status-or-level-map{ mapType, categoryName }地图勾选隐患分类/等级
look-hazard-status-dialog{ status }打开隐患统计弹窗
look-zyp-dialog{ status }打开作业票/作业单位弹窗
look-zyp-type-map{ type, jobName }地图展示作业票点位
open-info-panel{ name, info }打开设备信息面板
open-roam-panel{}打开漫游面板

现状:切模块已不再走该事件总线——switchGallery / switchPipeLine(以及讲解词里的按段切模块)统一改为调用能力注册表的系统级动作 invoke('system.switchModule', ...)(见第四节),demoFuncs.js 目前仅保留 open-pipe-list / hideWindows 两个纯地图事件的派发;上表为早期实现的完整事件表,保留作历史参考。


四、AI 动作注册中心(src/assistant) ​

随着演示脚本与 AI 语义识别规模扩大,原 demoBus 事件总线 + 各业务文件手写 busOn 的写法难以维护,新增了 src/assistant 作为统一的动作注册中心 / 能力中心,把"业务原子能力"与"AI 语义映射 / 演示编排"彻底分离(上一节的 clientTools/demoBus 是早期实现,src/assistant 是其解耦后的继任者)。

4.1 核心文件与对外 API ​

文件职责
index.js插件入口:用 import.meta.glob 自动扫描并 eager 注册 actions/*.js 与 orchestrations/*.js,新增一个能力 = 新增一个文件,主流程零改动;对外只暴露统一 API(invoke / registerAction / unregisterAction / hasAction / whenReady / ensureModule / listActions)
registry.js动作注册表(能力中心):Map<actionId, record> 保存动作,提供 registerAction、unregisterAction、hasAction、whenReady(就绪握手,替代 sleep(600) 猜时序)、ensureModule(切模块并等目标动作就绪)、invoke(调用动作并注入 ctx)、listActions(可直接导出 NLU / LLM 工具 schema)
useAssistantAction.js业务组件侧唯一需要写的一行:useAssistantAction(id, handler, meta) 声明"我能被怎么调用",组件卸载时自动注销(无需手写 onUnmounted / 反订阅)

约定:actionId 全局唯一,命名 module.verbObject(如 hazard.openStatusDialog);区分两类动作——原子能力(组件注册、与用户手动点击等价的最小动作)与 AI 动作(插件注册、对外给助手调用、内部再调原子能力)。

4.2 actions/(AI 语义 → 业务原子能力 的映射插件) ​

每个文件只放语义映射与编排,不碰业务组件:

actionId文件作用
axis.viewDetailactions/axis.js按管轴编号在本地列表定位,飞入并打开管轴详情弹框(全局弹框,无需切模块)
device.selectCategoryactions/device.js地图展示某类设备(参数 category1/category2),先 ensureModule('device')
device.viewStatusDialogactions/device.js设备状态弹框,AI 语义 status(online/offline/alarm/total/deal) → 组件 label
hazard.viewLevelOrCategoryMapactions/hazard.js地图展示某类 / 某等级隐患点位(mapType=level/category)
hazard.viewStatusDialogactions/hazard.js隐患统计弹框,status(当前隐患/今日新增/今日处理) → (index,title)
inspection.viewStatusDialogactions/inspection.js巡检任务弹框,status → (index,title)(0 全部 … 5 超期)
pipeLine.viewStatusDialogactions/pipeLine.js管线状态弹框,status(total/在建/已投用/未投用/已停用) → (statusName,statusId);total 走总览弹框
pipeLine.viewDetailactions/pipeLine.js按编号 / 名称查管线,飞入起点管轴 + 高亮并打开管线详情弹框
construct.viewZypTypeMapactions/construct.js地图展示某类作业票点位(jobName)
construct.viewZypDialogactions/construct.js作业单位 / 作业票汇总弹框,status ∈ {作业单位, 作业票}

4.3 orchestrations/(演示编排) ​

把"讲解词 / 指令 → 多步业务动作"的时序编排外置(原写在 views/map/index.vue 中):

actionId文件作用
gallery.introduceorchestrations/gallery.js切 3D + 打开「管廊介绍」弹框并自动下滚;keepOpen=true(智能解说 第 1 段传入)时弹框保持到第 4 段 closeIntroModal 才关,模块轮切由讲解词各段 switchModule 驱动;每步检查 demo-interrupt 打断信号
gallery.closeIntroModalorchestrations/gallery.js仅关闭介绍弹框(不连累演示中其他模块弹框)
hazard.openPanelDemoorchestrations/hazard.js切隐患模块 → 主动预加载点位 → 飞到首个隐患点 → 模拟点击打开详情弹框(对齐用户点击行为)
roam.openDemoorchestrations/roam.js切漫游模块 → 等挂件挂载(roamWidgetStore.visible['left1']) → 切 3D → 播放第一条漫游(调用原子能力 roam.start)

4.4 演示代码侵入业务代码的情况 ​

src/assistant 的设计目标是最小化侵入:业务组件只需 1 行 useAssistantAction(...) 声明原子能力、不认识任何 AI 词汇;AI 语义与编排全部外置。下面先列出散落在业务组件里的原子能力声明(即"1 行侵入"),再说明历史与过渡期残留的更深侵入。

业务组件中声明的原子能力(最小侵入,每处仅 1 行):

原子能力 actionId声明位置(业务组件)
system.switchModuleFooterBox(底部菜单切换,并回报"切换完成"信号)
device.applyCategorydevice/MenuBox/index.vue
device.openGalleryItemdevice/leftWidgets/LeftWidget1/Left1Content.vue
hazard.applyLevelOrCategoryMap / hazard.openStatusDialoghazard/mainBox/totalView.vue 等
inspection.openStatusDialoginspection/realTimeOverview/index.vue
pipeLine.openTotalDialogpipeLine/totalView/index.vue
pipeLine.openStatusDialogpipeLine/statusDistribution/index.vue
construct.toggleZypTypeconstruct/MenuBox/index.vue
construct.openZypDialogconstruct/leftWidgets/LeftWidget1/index.vue
roam.start漫游 Left1Content(等价于列表"开始"按钮)

历史与过渡期残留的更深侵入(待清理):

  1. 历史深耦合(已外置):演示编排原先直接写在业务组件 views/map/index.vue 内(约数十行 demoBus 订阅 + handler);各业务文件里 import demoBus → busOn('xxx-event') → handler → onUnmounted 反订阅,事件名(魔法字符串)与 AI 语义散落在十几个业务文件中。现已把 gallery / hazard / roam 编排与 axis/device/hazard/inspection/pipeLine/construct 语义映射迁移到 src/assistant,业务组件恢复干净。
  2. demoBus 过渡桥接(已清理):原先 demoFuncs.js 的 switchGallery / switchPipeLine 通过 demoBus.emit('switch-menu', …) 切模块,再由 registry.js 的 bus.on('switch-menu', …) 桥接到 system.switchModule。现已把这两处统一改为 invoke('system.switchModule', ...),并删除 registry.js 中的桥接订阅(registry.js 不再依赖 demoBus),行为与用户点击底部菜单完全一致;demoBus 现仅用于系统级广播(demo-interrupt)与 open-pipe-list / hideWindows 等纯地图事件。
  3. 系统级广播仍走 demoBus:打断信号 demo-interrupt 由 stopBroadcast() 经 demoBus 派发,gallery.js 仍 bus.on('demo-interrupt') 订阅——打断不是"能力调用",所以不走 action,仍以事件总线方式侵入。
  4. 切模块时序依赖业务组件:ensureModule 依赖 settingsStore.currentMenu 判断当前模块,并依赖 FooterBox 注册 system.switchModule 回报"切换完成";仅当该动作未注册(FooterBox 未挂载,理论不可达)时,才退回 MODULE_SWITCH_FALLBACK_MS = 600 固定等待(替代原先的 sleep(600) 猜时序),已不再经由 demoBus 派发事件。
  5. 编排直接操作业务 store:roam.openDemo 轮询 roamWidgetStore.visible['left1'] 判断组件是否挂载;ensureModule / whenReady 通过"业务组件挂载即注册原子能力"来握手;axis/pipeLine 等直接调用 mapStore.switchMode/flyTo/setHighLightAxis、modalStore.openModal/closeModal、useCloseAllChildModals 以及 galleryDataStore / pipeLineWidgetStore / hazardStore 等业务状态。
  6. 业务侧残留助手概念:漫游指令会调用 closeAssistant 隐藏对话面板,大屏通过 isRoaming 监听强制保持面板隐藏;这些"助手 / 演示"语义仍写在业务侧,属于单向侵入。

小结:当前主链路(AI 动作调用)已通过 src/assistant 与 actionId 解耦,业务组件的侵入收敛为"1 行声明";切模块已统一收敛为 system.switchModule 能力(demoBus 桥接已删除),剩下的脏活(打断信号广播、切模块时序兜底、store 直调)集中在 registry.js 与 orchestrations/* 的过渡期代码里,是后续要清理的重点。


五、AI 动作注册示例(以「打开作业单位/作业票弹框」为例) ​

用一个真实能力 construct.openZypDialog 走完整条链路:演示指令 / NLU 语义识别 → 演示工具 → AI 动作 → 业务组件原子能力,逐层贴出要写的代码与声明位置。

5.1 触发链路(4 层) ​

① 触发源:演示指令 / NLU 语义识别
     data/demoQA.js      { func: 'lookZypDialog', args: { status: '作业单位' } }
     VisAssistant.vue    buildToolFromNlu() 把 NLU 结果映射成同一批 clientTools
          │
          ▼
② 演示工具(clientTools,由 views/home/index.vue 注入)
     data/demoFuncs.js   lookZypDialog(args) → invokeAction('construct.viewZypDialog', { status })
          │
          ▼
③ AI 动作(语义映射 / 参数转换 / 切模块 / 等就绪)
     src/assistant/actions/construct.js
     registerAction('construct.viewZypDialog', { …, handler })
          │   ctx.ensureModule('construct', 'construct.openZypDialog')
          │   ctx.invoke('construct.openZypDialog', { status })
          ▼
④ 原子能力(业务组件声明,与用户手动点击等价)
     src/views/construct/leftWidgets/LeftWidget1/index.vue
     useAssistantAction('construct.openZypDialog', handleLookZypDialog, { … })

分工:② 只认 actionId、③ 只做语义映射与编排、④ 只做业务动作;业务组件不认识任何 AI 词汇。

5.2 业务组件侧:1 行声明原子能力(唯一的侵入点) ​

文件:src/views/construct/leftWidgets/LeftWidget1/index.vue

js
/**
 * 处理 AI 助手的"查看作业单位/作业票弹框"请求
 * 注意:Left1Content 在 v-if="visible" 内,初始可能未挂载,
 *       所以能力声明放在始终挂载的 LeftWidget1/index.vue 中
 * @param {{ status: string }} payload
 *   - status: '作业单位' / '作业票'
 */
function handleLookZypDialog({ status }) {
  if (status !== '作业单位' && status !== '作业票') return

  // 确保施工概览面板可见
  constructWidgetStore.show(widgetUniqueKey)

  if (status === '作业单位') {
    constructWidgetStore.showDialog('unitWidget', { name: '作业单位' })
    constructWidgetStore.hideDialog('ticketsWidget')
  } else {
    constructWidgetStore.showDialog('ticketsWidget', { name: '作业票', isMapSelect: false })
    constructWidgetStore.hideDialog('unitWidget')
    constructWidgetStore.hideDialog('unitTicketWidget')
  }
  constructWidgetStore.hideDialog('ticketDetailWidget')
}

// === AI 可调用能力声明 ===
// 只暴露"打开作业单位/作业票汇总弹框"的原子能力,
// AI 语义映射见 src/assistant/actions/construct.js
import { useAssistantAction } from '@/assistant'

useAssistantAction('construct.openZypDialog', handleLookZypDialog, {
  module: 'construct',
  title: '打开作业单位/作业票汇总弹框',
  params: { status: '作业单位 | 作业票' },
})

要点:

  • handler 内只有业务动作(constructWidgetStore.show / showDialog / hideDialog),没有 AI / 助手 / demo 任何字样;
  • useAssistantAction(id, handler, meta) 内部调 registerAction 注册,并在组件卸载时自动注销(内部 onUnmounted),因此不需要手写 onUnmounted / busOff;
  • 声明点刻意放在始终挂载的 LeftWidget1/index.vue,而不是 v-if="visible" 内的 Left1Content:whenReady('construct.openZypDialog') 的"就绪"判定就是组件挂载即注册,放进 v-if 会在面板未显示时握手超时;
  • meta 是自描述信息(module / title / params),会被 listActions() 导出成 NLU / LLM 的 function-calling schema。

5.3 AI 侧:注册 AI 动作(语义映射与切模块) ​

文件:src/assistant/actions/construct.js

js
// src/assistant/actions/construct.js
// 施工模块的 AI 能力插件
// - 组件侧各声明 1 行原子能力:
//   construct.toggleZypType   construct/MenuBox/index.vue
//   construct.openZypDialog   construct/leftWidgets/LeftWidget1/index.vue

import { registerAction } from '../registry'

/** AI 语义允许的作业票弹框类型 */
const ZYP_DIALOG_STATUS = ['作业单位', '作业票']

registerAction('construct.viewZypDialog', {
  module: 'construct',
  title: '查看作业单位/作业票汇总弹框',
  params: { status: '作业单位 | 作业票' },
  async handler({ status } = {}, ctx) {
    if (!ZYP_DIALOG_STATUS.includes(status)) {
      throw new Error(`[assistant] 未知的作业票弹框类型: ${status}`)
    }
    await ctx.ensureModule('construct', 'construct.openZypDialog')
    return ctx.invoke('construct.openZypDialog', { status })
  },
})

要点:

  • 白名单校验把 AI 的自由文本收窄为确定取值(作业单位 / 作业票),脏数据不会流进业务组件;
  • ctx.ensureModule(module, readyActionId):当前不在 construct 模块就先调系统级动作 system.switchModule 切过去(与用户点底部菜单完全一致),再用 whenReady 等目标原子能力就绪——替代早期 sleep(600) 猜时序;
  • ctx.invoke('construct.openZypDialog', { status }) 才真正调 5.2 那一行声明;
  • 新增文件即自动注册:src/assistant/index.js 用 import.meta.glob('./actions/*.js', { eager: true }) 扫描,加一个业务模块的能力只需新增一个文件,主流程零改动。

5.4 触发入口:脚本与 NLU 侧的声明位置 ​

(1)演示脚本 —— data/demoQA.js(functionTool 的 func 就是 clientTools 的键名):

js
{
  question: '查询作业单位有哪些',
  answer: [
    {
      text: '好的,这就为您打开作业单位信息',
      audio: './audios/construct_作业单位.mp3',
      extensions: {
        functionTool: [
          { func: 'lookZypDialog', args: { module: 'construct', type: 'dialog', status: '作业单位' } },
        ],
      },
    },
  ],
}

(2)演示工具 —— data/demoFuncs.js:只把 clientTools 的调用转成能力注册表调用,自己不再派发 demoBus 事件:

js
import { invoke, ensureModule } from '@/assistant'

/** 统一封装:invoke + 日志与错误兜底 */
async function invokeAction(actionId, args, extra = {}) {
  try {
    await invoke(actionId, args)
    return { success: true, action: actionId, ...extra }
  } catch (error) {
    console.error(`[bigScreen][demo] ${actionId} 执行失败:`, error)
    return { success: false, action: actionId, error: error?.message || String(error) }
  }
}

async function lookZypDialog(args) {
  const { type, status } = args || {}
  if (!status) {
    // 与原逻辑一致:缺少 status 时仍切到施工模块
    await ensureModule('construct')
    return { success: false, error: 'status is required' }
  }
  // 切模块 / 就绪等待见 src/assistant/actions/construct.js
  return invokeAction('construct.viewZypDialog', { status }, { type, status })
}

(3)NLU 分支 —— VisAssistant.vue 的 buildToolFromNlu()(把 NLU 结构化结果映射成同一批工具,module=construct##type=dialog##status=作业单位):

js
return { func: 'lookZypDialog', args: { type: 'dialog', status: nlu.status } }

注:demoFuncs.js / index.vue 里"由 lookZypDialog 派发 look-zyp-dialog 事件触发"是早期 demoBus 实现的注释残留;当前链路已改为上图的 src/assistant 动作调用,事件总线仅保留 demo-interrupt / open-pipe-list / hideWindows(见第三节「现状」)。

5.5 新增一个能力要改哪些地方 ​

目标要改的文件写入内容
能力可被 AI / 演示调用业务组件(如 construct/leftWidgets/LeftWidget1/index.vue)useAssistantAction('module.verbObject', handler, meta) 1 行
AI 语义 → 该能力(参数转换 / 切模块 / 等就绪)src/assistant/actions/<module>.jsregisterAction('module.viewXxx', { module, title, params, handler });新文件自动扫描注册
演示指令能触发data/demoQA.js + data/demoFuncs.js脚本条目 { func: 'xxx', args } + 工具函数内 invokeAction('module.viewXxx', …)
自然语言能触发(NLU)后端 NLU 模板(SiliconFlowNluService)+ VisAssistant.vue增补 模块名: keywords + prompt,并在 buildToolFromNlu() 加分支
给 LLM function-calling 用—listActions() 直接导出 { id, module, title, params } schema

约定:actionId 全局唯一、命名 module.verbObject;原子能力由组件注册(与用户手动点击等价),AI 动作由 actions/*.js 注册(内部再调原子能力)。不要跨层——把 AI 语义映射写进组件、或在组件里判断 status 的业务含义,等于把两者重新耦合回去。

数字人「小慧」项目文档