更新记录

1.0.0(2026-07-30) 下载此版本

initIflytekAikitAwaken(options) 的参数如下:

  • appId 讯飞开放平台应用 AppID
  • apiKey 讯飞开放平台 APIKey
  • apiSecret 讯飞开放平台 APISecret
  • abilityId 唤醒能力 ID,默认使用 e867a88f2
  • wakeWords 唤醒词数组,例如 ["你好小店", "小店小店"]
  • logEnabled 是否开启 SDK 日志

说明:

  • wakeWords 不是 SDK 自动下发的,而是你自己传入的
  • 插件会把这些词写入 keyword.txt,再通过 loadData/specifyDataSet 加载到引擎中

平台兼容性

uni-app(4.83)

Vue2 Vue3 Chrome Safari app-vue app-nvue Android iOS 鸿蒙
- - - - - 5.0 -
微信小程序 支付宝小程序 抖音小程序 百度小程序 快手小程序 京东小程序 鸿蒙元服务 QQ小程序 飞书小程序 小红书小程序 快应用-华为 快应用-联盟
- - - - - - - - - - - -

uni-app x

Chrome Safari Android iOS 鸿蒙 微信小程序
- - × - - -

其他

多语言 暗黑模式 宽屏模式
× ×

iflytek-aikit-awaken 中文文档

iflytek-aikit-awaken 是一个面向 uni-appUTS API 插件,用于在 App Android 端接入讯飞 AIKit 离线语音唤醒能力。

这份插件主要解决三件事:

  • uni-app Vue 页面里直接调用讯飞离线唤醒 SDK
  • 支持运行时动态更换唤醒词
  • AIKit.aar 和离线资源一起封装进 uni_modules,减少集成工作量

1. 功能概览

当前插件支持:

  • SDK 初始化
  • 麦克风实时离线唤醒
  • 本地 PCM 文件唤醒测试
  • 动态更新唤醒词
  • 鉴权、唤醒、音量、停止、错误等事件回调

当前插件限制:

  • 仅支持 uni-app App Android
  • 不支持 H5
  • 不支持 iOS
  • 首次鉴权仍可能需要联网,鉴权成功后才适合离线使用

2. 插件目录

插件目录位于:

uni_modules/iflytek-aikit-awaken

核心结构如下:

iflytek-aikit-awaken/
├── package.json
├── README.md
├── README.zh-CN.md
├── index.d.ts
├── CHANGELOG.md
└── utssdk/
    ├── index.uts
    ├── interface.uts
    └── app-android/
        ├── config.json
        ├── index.uts
        ├── IflytekAikitAwakenNative.kt
        ├── libs/
        │   └── AIKit.aar
        └── assets/
            └── iflytek/ivw/

其中:

  • libs/AIKit.aar 是讯飞 AIKit Android SDK
  • assets/iflytek/ivw/ 是离线唤醒资源
  • IflytekAikitAwakenNative.kt 是 Android 原生桥接实现
  • app-android/index.uts 是 UTS 对外导出层

3. 集成方式

把整个目录复制到你的项目:

uni_modules/iflytek-aikit-awaken

然后使用:

  • 自定义基座
  • 或云打包

注意:

  • 如果新增或修改了原生插件,通常需要重新制作自定义基座
  • 如果真机上提示插件不存在,优先检查当前运行包是否重新编译并包含了该插件

4. Android 权限

插件声明了以下权限:

  • android.permission.RECORD_AUDIO
  • android.permission.INTERNET
  • android.permission.ACCESS_NETWORK_STATE

其中:

  • RECORD_AUDIO 是开始麦克风唤醒前必须具备的运行时权限
  • INTERNET / ACCESS_NETWORK_STATE 主要用于首次鉴权

5. 工作目录与离线资源

本插件不会使用旧 Demo 里的 /sdcard/iflytek/ 方案。

运行时会自动把离线资源复制到应用私有目录,工作目录可通过:

getIflytekAikitAwakenWorkDir()

获取。

当前工作目录格式为:

<App files dir>/iflytek/

这样做的好处是:

  • 不依赖外部存储
  • 不需要广泛的读写存储权限
  • 更适合 uni-app 插件分发

6. 页面调用示例

import {
  setIflytekAikitAwakenListener,
  initIflytekAikitAwaken,
  startIflytekAikitAwaken,
  stopIflytekAikitAwaken,
  updateIflytekWakeWords,
  releaseIflytekAikitAwaken,
  getIflytekAikitAwakenWorkDir
} from "@/uni_modules/iflytek-aikit-awaken"

setIflytekAikitAwakenListener((event) => {
  console.log("iflytek awaken event", event)

  if (event.type === "auth" && event.code === 0) {
    console.log("auth success")
  }

  if (event.type === "wakeup") {
    console.log("wake matched", event.raw)
  }
})

const code = initIflytekAikitAwaken({
  appId: "your_app_id",
  apiKey: "your_api_key",
  apiSecret: "your_api_secret",
  abilityId: "e867a88f2",
  wakeWords: ["你好小店", "小店小店"],
  logEnabled: true
})

if (code === 0) {
  console.log("init call success, wait auth event")
}

开始监听:

const startCode = startIflytekAikitAwaken()

停止监听:

const stopCode = stopIflytekAikitAwaken()

释放资源:

const releaseCode = releaseIflytekAikitAwaken()

7. 初始化参数说明

initIflytekAikitAwaken(options) 的参数如下:

  • appId 讯飞开放平台应用 AppID
  • apiKey 讯飞开放平台 APIKey
  • apiSecret 讯飞开放平台 APISecret
  • abilityId 唤醒能力 ID,默认使用 e867a88f2
  • wakeWords 唤醒词数组,例如 ["你好小店", "小店小店"]
  • logEnabled 是否开启 SDK 日志

说明:

  • wakeWords 不是 SDK 自动下发的,而是你自己传入的
  • 插件会把这些词写入 keyword.txt,再通过 loadData/specifyDataSet 加载到引擎中

8. 怎么判断当前有几个唤醒词

当前插件没有单独提供“查询已加载唤醒词数量”的 API。

你可以从两个层面判断:

  1. 页面侧

如果你传入的是:

["你好小店", "小店小店", "欢迎光临"]

那么当前准备加载的唤醒词数量就是:

3
  1. 插件侧

插件最终会把唤醒词写到 keyword.txt,一行一个。

因此从实现角度看:

  • 数组长度 = 计划写入数量
  • keyword.txt 行数 = 实际写入数量

9. 动态更换唤醒词

这是本插件的核心能力之一。

调用方式:

updateIflytekWakeWords(["你好小店", "欢迎回家"], true)

第二个参数 restartIfListening 的含义:

  • true 如果当前正在监听,则自动停止旧会话,重建关键词文件,并重新开始监听
  • false 仅更新内存中的唤醒词配置,不主动重启当前监听

推荐做法:

  • 用户改了唤醒词后,直接调用 updateIflytekWakeWords(newWords, true)

10. 事件回调说明

通过:

setIflytekAikitAwakenListener((event) => {})

注册长期监听。

事件对象格式示例:

{
  type: "wakeup",
  code: 0,
  message: "wake-word matched",
  raw: "{...sdk json...}",
  volume: 0
}

常见 type

  • auth SDK 鉴权结果
  • listening 开始监听
  • stopped 当前监听会话结束
  • wakeup 命中唤醒
  • preWakeup 预唤醒命中
  • volume 当前音量值
  • log 普通日志
  • error 错误事件

其中最重要的是:

  • auth + code === 0 表示鉴权成功,此后才能稳定启动监听
  • wakeup 表示真正命中唤醒词

11. 文件测试模式

如果你不想先接麦克风,也可以用 PCM 文件做测试:

import { detectIflytekWakeupFromFile } from "@/uni_modules/iflytek-aikit-awaken"

detectIflytekWakeupFromFile("/storage/emulated/0/Download/test.pcm")

PCM 要求:

  • 16 kHz
  • 单声道
  • 16-bit PCM

12. 常见问题

12.1 插件找不到

如果出现类似:

Iflytek AIKit Awaken not found

通常表示:

  • 当前运行包没有包含该原生插件
  • 新增插件后没有重做自定义基座
  • 真机安装的仍是旧包
  • 当前不是 App Android 环境

优先检查:

  1. 是否在 App Android 真机运行
  2. 是否重新制作自定义基座
  3. 是否重新打包并安装了新包

12.2 回调函数已释放

如果出现类似:

回调函数已释放,不能再次执行

这是因为监听器属于长期回调,UTS 侧必须使用 keepAlive 机制。

本插件已经按长期监听方式处理 setIflytekAikitAwakenListener

12.3 首次鉴权失败

请重点检查:

  • appId/apiKey/apiSecret 是否正确
  • 首次运行时设备是否可联网
  • 讯飞控制台能力是否已开通

12.4 麦克风无法启动

请检查:

  • RECORD_AUDIO 是否已授予
  • 是否已有其他模块占用了麦克风

13. 推荐接入流程

推荐页面逻辑如下:

  1. 用户填写 appId/apiKey/apiSecret
  2. 调用 initIflytekAikitAwaken(...)
  3. 监听 auth 事件
  4. auth.code === 0 时,再允许用户点击“开始监听”
  5. 需要改唤醒词时,调用 updateIflytekWakeWords(newWords, true)
  6. 页面销毁时执行:
stopIflytekAikitAwaken()
releaseIflytekAikitAwaken()
setIflytekAikitAwakenListener(null)

14. 当前版本说明

当前版本重点是:

  • 跑通 Android UTS 原生桥接
  • 支持离线唤醒
  • 支持动态更新唤醒词

暂未覆盖:

  • iOS
  • 可视化组件封装
  • 唤醒词数量查询专用 API

如果后续继续扩展,比较合适的方向是:

  • 增加 getWakeWords() / getWakeWordCount() API
  • 增加鉴权状态查询 API
  • 增加更完整的错误码说明

15.11实例

<template>
  <view class="page">
    <view class="hero">
      <view class="hero__title">讯飞语音唤醒测试</view>
      <view class="hero__desc">仅支持 uni-app App Android。先初始化,等鉴权成功后再开始监听。</view>
    </view>

    <view class="card">
      <view class="card__title">SDK 配置</view>

      <view class="field">
        <view class="field__label">App ID</view>
        <input v-model="form.appId" class="field__input" placeholder="请输入 appId" />
      </view>

      <view class="field">
        <view class="field__label">API Key</view>
        <input v-model="form.apiKey" class="field__input" placeholder="请输入 apiKey" />
      </view>

      <view class="field">
        <view class="field__label">API Secret</view>
        <input v-model="form.apiSecret" class="field__input" placeholder="请输入 apiSecret" password />
      </view>

      <view class="field">
        <view class="field__label">Ability ID</view>
        <input v-model="form.abilityId" class="field__input" placeholder="默认 e867a88f2" />
      </view>

      <view class="field">
        <view class="field__label">唤醒词</view>
        <textarea
          v-model="wakeWordsText"
          class="field__textarea"
          placeholder="每行一个唤醒词,例如:&#10;你好小店&#10;小店小店"
        />
      </view>
    </view>

    <view class="card">
      <view class="card__title">运行状态</view>
      <view class="status-grid">
        <view class="status-item">
          <view class="status-item__label">初始化</view>
          <view class="status-item__value">{{ initStatusText }}</view>
        </view>
        <view class="status-item">
          <view class="status-item__label">监听中</view>
          <view class="status-item__value">{{ listening ? '是' : '否' }}</view>
        </view>
        <view class="status-item">
          <view class="status-item__label">最近事件</view>
          <view class="status-item__value">{{ lastEventType || '暂无' }}</view>
        </view>
        <view class="status-item">
          <view class="status-item__label">命中次数</view>
          <view class="status-item__value">{{ wakeCount }}</view>
        </view>
      </view>
      <view class="workdir">工作目录:{{ workDir || '未获取' }}</view>
    </view>

    <view class="card">
      <view class="card__title">控制面板</view>
      <view class="actions">
        <button class="action-btn action-btn--primary" :disabled="busy" @click="handleInit">初始化</button>
        <button class="action-btn"  @click="handleStart">开始监听</button>
        <button class="action-btn" :disabled="busy" @click="handleStop">停止监听</button>
        <button class="action-btn" :disabled="busy || !initialized" @click="handleUpdateWakeWords">更新唤醒词</button>
        <button class="action-btn action-btn--danger" :disabled="busy" @click="handleRelease">释放资源</button>
      </view>
    </view>

    <view class="card">
      <view class="card__title">事件日志</view>
      <scroll-view scroll-y class="log-list">
        <view v-for="item in logs" :key="item.id" class="log-item">
          <view class="log-item__head">
            <text class="log-item__type">{{ item.type }}</text>
            <text class="log-item__time">{{ item.time }}</text>
          </view>
          <view class="log-item__body">{{ item.message }}</view>
          <view v-if="item.raw" class="log-item__raw">{{ item.raw }}</view>
        </view>
        <view v-if="!logs.length" class="log-empty">暂无事件</view>
      </scroll-view>
    </view>
  </view>
</template>

<script setup lang="ts">
import { computed, onBeforeUnmount, reactive, ref } from 'vue'
import {
  getIflytekAikitAwakenWorkDir,
  initIflytekAikitAwaken,
  isIflytekAikitAwakenListening,
  releaseIflytekAikitAwaken,
  setIflytekAikitAwakenListener,
  startIflytekAikitAwaken,
  stopIflytekAikitAwaken,
  updateIflytekWakeWords
} from '@/uni_modules/iflytek-aikit-awaken'
import type { IflytekAikitAwakenEvent } from '@/uni_modules/iflytek-aikit-awaken'

type LogItem = {
  id: string
  type: string
  message: string
  raw: string
  time: string
}

const form = reactive({
  appId: '901e339d',
  apiKey: 'e34c678f97b83fba3f89324f399a4736',
  apiSecret: 'NDEwZWQwMmQzOGM3OGM5Mzg2ODRmNTZh',
  abilityId: 'e867a88f2'
})

const wakeWordsText = ref('你好小店\n小店小店')
const busy = ref(false)
const initialized = ref(false)
const authReady = ref(false)
const listening = ref(false)
const lastEventType = ref('')
const wakeCount = ref(0)
const workDir = ref('')
const logs = ref<LogItem[]>([])

const initStatusText = computed(() => {
  if (listening.value) return '监听中'
  if (authReady.value) return '鉴权成功'
  if (initialized.value) return '已初始化'
  return '未初始化'
})

function nowText() {
  const date = new Date()
  const hh = `${date.getHours()}`.padStart(2, '0')
  const mm = `${date.getMinutes()}`.padStart(2, '0')
  const ss = `${date.getSeconds()}`.padStart(2, '0')
  return `${hh}:${mm}:${ss}`
}

function pushLog(type: string, message: string, raw = '') {
  logs.value.unshift({
    id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
    type,
    message,
    raw,
    time: nowText()
  })
  if (logs.value.length > 80) {
    logs.value.length = 80
  }
}

function parseWakeWords() {
  return wakeWordsText.value
    .split(/\r?\n/)
    .map((item) => item.trim())
    .filter(Boolean)
}

function syncListeningStatus() {
  listening.value = isIflytekAikitAwakenListening()
}

function validateInitForm() {
  if (!form.appId.trim() || !form.apiKey.trim() || !form.apiSecret.trim()) {
    uni.showToast({
      title: '请先填写 appId/apiKey/apiSecret',
      icon: 'none'
    })
    return false
  }

  if (!parseWakeWords().length) {
    uni.showToast({
      title: '请至少填写一个唤醒词',
      icon: 'none'
    })
    return false
  }

  return true
}

setIflytekAikitAwakenListener((event: IflytekAikitAwakenEvent) => {
  lastEventType.value = event.type || ''

  if (event.type === 'auth' && event.code === 0) {
    authReady.value = true
    initialized.value = true
    workDir.value = getIflytekAikitAwakenWorkDir()
  }

  if (event.type === 'listening') {
    listening.value = true
  }

  if (event.type === 'stopped' || event.type === 'error') {
    listening.value = false
  }

  if (event.type === 'wakeup') {
    wakeCount.value += 1
  }

  const message = event.message || `code=${event.code ?? ''}`
  pushLog(event.type || 'log', message, event.raw || '')
})

async function handleInit() {
  if (busy.value) return
  if (!validateInitForm()) return

  busy.value = true
  try {
    authReady.value = false
    initialized.value = false
    wakeCount.value = 0

    const code = initIflytekAikitAwaken({
      appId: form.appId,
      apiKey: form.apiKey,
      apiSecret: form.apiSecret,
      abilityId: 'e867a88f2',
      wakeWords: parseWakeWords(),
      logEnabled: true
    })

    pushLog('init', `init code=${code}`)
    if (code === 0) {
      initialized.value = true
      workDir.value = getIflytekAikitAwakenWorkDir()
    } else {
      uni.showToast({
        title: `初始化失败: ${code}`,
        icon: 'none'
      })
    }
  } finally {
    busy.value = false
  }
}

async function handleStart() {
  if (busy.value) return

  busy.value = true
  try {
    const code = startIflytekAikitAwaken()
    syncListeningStatus()
    pushLog('start', `start code=${code}`)
    if (code !== 0) {
      uni.showToast({
        title: `启动失败: ${code}`,
        icon: 'none'
      })
    }
  } finally {
    busy.value = false
  }
}

function handleStop() {
  if (busy.value) return
  const code = stopIflytekAikitAwaken()
  syncListeningStatus()
  pushLog('stop', `stop code=${code}`)
}

function handleUpdateWakeWords() {
  if (busy.value) return

  const wakeWords = parseWakeWords()
  if (!wakeWords.length) {
    uni.showToast({
      title: '请至少填写一个唤醒词',
      icon: 'none'
    })
    return
  }

  const code = updateIflytekWakeWords(wakeWords, true)
  pushLog('update', `update wake words code=${code}`)
  if (code !== 0) {
    uni.showToast({
      title: `更新失败: ${code}`,
      icon: 'none'
    })
  }
}

function handleRelease() {
  if (busy.value) return
  stopIflytekAikitAwaken()
  const code = releaseIflytekAikitAwaken()
  listening.value = false
  initialized.value = false
  authReady.value = false
  workDir.value = ''
  pushLog('release', `release code=${code}`)
}

onBeforeUnmount(() => {
  stopIflytekAikitAwaken()
  releaseIflytekAikitAwaken()
  setIflytekAikitAwakenListener(null)
})
</script>

<style lang="less">
.page {
  min-height: 100vh;
  padding: 24rpx;
  background: linear-gradient(180deg, #eef6ff 0%, #f7fbff 38%, #f4f6f8 100%);
  box-sizing: border-box;
}

.hero {
  padding: 30rpx 28rpx;
  margin-bottom: 20rpx;
  border-radius: 28rpx;
  background: linear-gradient(135deg, #0f4c81, #1d79bf);
  color: #ffffff;
  box-shadow: 0 18rpx 40rpx rgba(15, 76, 129, 0.18);
}

.hero__title {
  font-size: 36rpx;
  font-weight: 800;
  line-height: 1.3;
}

.hero__desc {
  margin-top: 10rpx;
  font-size: 24rpx;
  line-height: 1.5;
  color: rgba(255, 255, 255, 0.88);
}

.card {
  padding: 24rpx;
  margin-bottom: 20rpx;
  border-radius: 24rpx;
  background: #ffffff;
  box-shadow: 0 12rpx 30rpx rgba(15, 23, 42, 0.06);
}

.card__title {
  margin-bottom: 20rpx;
  font-size: 28rpx;
  font-weight: 800;
  color: #0f172a;
}

.field + .field {
  margin-top: 18rpx;
}

.field__label {
  margin-bottom: 10rpx;
  font-size: 24rpx;
  font-weight: 700;
  color: #334155;
}

.field__input,
.field__textarea {
  width: 100%;
  padding: 18rpx 20rpx;
  border: 2rpx solid #dbe7f3;
  border-radius: 18rpx;
  background: #f8fbff;
  box-sizing: border-box;
  font-size: 24rpx;
  color: #0f172a;
}

.field__textarea {
  min-height: 180rpx;
}

.status-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 16rpx;
}

.status-item {
  padding: 20rpx;
  border-radius: 18rpx;
  background: #f8fbff;
}

.status-item__label {
  font-size: 22rpx;
  color: #64748b;
}

.status-item__value {
  margin-top: 8rpx;
  font-size: 28rpx;
  font-weight: 800;
  color: #0f172a;
}

.workdir {
  margin-top: 18rpx;
  font-size: 22rpx;
  line-height: 1.5;
  color: #475569;
  word-break: break-all;
}

.actions {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 16rpx;
}

.action-btn {
  height: 82rpx;
  line-height: 82rpx;
  border: none;
  border-radius: 18rpx;
  background: #e7eef6;
  color: #0f172a;
  font-size: 26rpx;
  font-weight: 700;
}

.action-btn::after {
  border: none;
}

.action-btn--primary {
  background: linear-gradient(135deg, #0f4c81, #1d79bf);
  color: #ffffff;
}

.action-btn--danger {
  background: #fee2e2;
  color: #b91c1c;
}

.log-list {
  max-height: 720rpx;
}

.log-item {
  padding: 18rpx 0;
  border-bottom: 1rpx solid rgba(148, 163, 184, 0.2);
}

.log-item:last-child {
  border-bottom: none;
}

.log-item__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16rpx;
}

.log-item__type {
  font-size: 24rpx;
  font-weight: 800;
  color: #0f4c81;
}

.log-item__time {
  font-size: 20rpx;
  color: #94a3b8;
}

.log-item__body {
  margin-top: 8rpx;
  font-size: 24rpx;
  line-height: 1.5;
  color: #334155;
}

.log-item__raw {
  margin-top: 8rpx;
  padding: 14rpx 16rpx;
  border-radius: 14rpx;
  background: #f8fafc;
  font-size: 20rpx;
  line-height: 1.5;
  color: #475569;
  word-break: break-all;
}

.log-empty {
  padding: 40rpx 0 20rpx;
  text-align: center;
  font-size: 24rpx;
  color: #94a3b8;
}
</style>

隐私、权限声明

1. 本插件需要申请的系统权限列表:

麦克风权限

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

只会请求讯飞语音服务

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

许可协议

MIT协议

暂无用户评论。