sk-voice-command 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/plugin.js ADDED
@@ -0,0 +1,252 @@
1
+ import CommandMatcher from './core/command-matcher'
2
+ import store from './core/store'
3
+ import { validateConfig } from './utils/helpers'
4
+ import { STATUS, ERROR_CODES, ERROR_MESSAGES } from './utils/constants'
5
+
6
+ /**
7
+ * Vue 插件
8
+ */
9
+ function install(Vue, options = {}) {
10
+ // 验证配置
11
+ let config
12
+ try {
13
+ config = validateConfig(options)
14
+ } catch (error) {
15
+ console.error('[sk-voice-command] 配置错误:', error.message)
16
+ throw error
17
+ }
18
+
19
+ // 创建指令匹配器
20
+ const matcher = new CommandMatcher(config)
21
+
22
+ // 创建语音命令实例
23
+ const voiceCommand = {
24
+ // 配置
25
+ _config: config,
26
+ _matcher: matcher,
27
+ _store: store,
28
+
29
+ // ===== 公开 API =====
30
+
31
+ /**
32
+ * 注册指令
33
+ * @param {Object} options - 选项
34
+ */
35
+ registerCommands(options) {
36
+ const { scope = 'page', commands = [] } = options
37
+
38
+ try {
39
+ this._matcher.registerCommands(scope, commands)
40
+
41
+ if (this._config.debug) {
42
+ console.log('[sk-voice-command] 注册指令:', { scope, commands })
43
+ }
44
+ } catch (error) {
45
+ console.error('[sk-voice-command] 注册指令失败:', error)
46
+ throw error
47
+ }
48
+ },
49
+
50
+ /**
51
+ * 注销指令
52
+ * @param {string} scope - 作用域
53
+ */
54
+ unregisterCommands(scope) {
55
+ try {
56
+ // 获取当前页面路径(如果在 uni-app 环境中)
57
+ const pages = getCurrentPages()
58
+ const currentPage = pages[pages.length - 1]
59
+ const route = currentPage ? currentPage.route : ''
60
+
61
+ this._matcher.unregisterCommands(scope, route)
62
+
63
+ if (this._config.debug) {
64
+ console.log('[sk-voice-command] 注销指令:', { scope, route })
65
+ }
66
+ } catch (error) {
67
+ console.error('[sk-voice-command] 注销指令失败:', error)
68
+ }
69
+ },
70
+
71
+ /**
72
+ * 开始语音识别
73
+ * @returns {Promise<void>}
74
+ */
75
+ async start() {
76
+ try {
77
+ if (this._store.state.isRecording) {
78
+ console.warn('[sk-voice-command] 已经在录音中')
79
+ return
80
+ }
81
+
82
+ // TODO: 实现真实的录音和识别逻辑
83
+ // 这里先使用模拟实现
84
+ this._store.setStatus(STATUS.RECORDING)
85
+ this._store.setRecording(true)
86
+ this._store.setError(null)
87
+
88
+ if (this._config.debug) {
89
+ console.log('[sk-voice-command] 开始录音')
90
+ }
91
+
92
+ // 模拟:触发状态变化事件
93
+ this._store.emit('recording-started')
94
+ } catch (error) {
95
+ console.error('[sk-voice-command] 启动失败:', error)
96
+ this._store.setError(error.message || ERROR_MESSAGES.UNKNOWN_ERROR)
97
+ this._store.setStatus(STATUS.ERROR)
98
+ throw error
99
+ }
100
+ },
101
+
102
+ /**
103
+ * 停止语音识别
104
+ * @returns {Promise<Object>} 识别结果
105
+ */
106
+ async stop() {
107
+ try {
108
+ if (!this._store.state.isRecording) {
109
+ console.warn('[sk-voice-command] 没有在录音中')
110
+ return { text: '', matched: false }
111
+ }
112
+
113
+ // TODO: 实现真实的停止逻辑
114
+ this._store.setRecording(false)
115
+ this._store.setStatus(STATUS.COMPLETED)
116
+
117
+ // 模拟:识别结果
118
+ const mockText = this._store.state.recognitionText || '测试识别'
119
+
120
+ if (this._config.debug) {
121
+ console.log('[sk-voice-command] 停止录音:', mockText)
122
+ }
123
+
124
+ // 匹配指令
125
+ const matchResult = this._matcher.match(mockText)
126
+
127
+ if (matchResult.matched && matchResult.action === 'auto') {
128
+ // 自动执行
129
+ if (this._config.debug) {
130
+ console.log('[sk-voice-command] 自动执行指令:', matchResult.command)
131
+ }
132
+
133
+ try {
134
+ const result = this._matcher.executeCommand(matchResult.command)
135
+ this._store.setCommand({
136
+ ...matchResult.command,
137
+ executed: true,
138
+ result
139
+ })
140
+ } catch (error) {
141
+ console.error('[sk-voice-command] 执行指令失败:', error)
142
+ this._store.setError(error.message)
143
+ }
144
+ }
145
+
146
+ this._store.emit('recording-stopped', {
147
+ text: mockText,
148
+ matchResult
149
+ })
150
+
151
+ return {
152
+ text: mockText,
153
+ ...matchResult
154
+ }
155
+ } catch (error) {
156
+ console.error('[sk-voice-command] 停止失败:', error)
157
+ this._store.setError(error.message || ERROR_MESSAGES.UNKNOWN_ERROR)
158
+ this._store.setStatus(STATUS.ERROR)
159
+ throw error
160
+ }
161
+ },
162
+
163
+ /**
164
+ * 手动匹配指令(用于测试)
165
+ * @param {string} text - 文本
166
+ * @returns {Object} 匹配结果
167
+ */
168
+ matchCommand(text) {
169
+ const result = this._matcher.match(text)
170
+
171
+ if (this._config.debug) {
172
+ console.log('[sk-voice-command] 匹配指令:', { text, result })
173
+ }
174
+
175
+ return result
176
+ },
177
+
178
+ /**
179
+ * 执行指令
180
+ * @param {Object} command - 指令对象
181
+ * @returns {*} 执行结果
182
+ */
183
+ executeCommand(command) {
184
+ return this._matcher.executeCommand(command)
185
+ },
186
+
187
+ /**
188
+ * 监听事件
189
+ * @param {string} event - 事件名称
190
+ * @param {Function} callback - 回调函数
191
+ */
192
+ on(event, callback) {
193
+ this._store.on(event, callback)
194
+ },
195
+
196
+ /**
197
+ * 移除监听
198
+ * @param {string} event - 事件名称
199
+ * @param {Function} callback - 回调函数
200
+ */
201
+ off(event, callback) {
202
+ this._store.off(event, callback)
203
+ },
204
+
205
+ // ===== 只读属性 =====
206
+
207
+ /**
208
+ * 获取状态(只读)
209
+ */
210
+ get state() {
211
+ return this._store.state
212
+ },
213
+
214
+ /**
215
+ * 获取配置(只读)
216
+ */
217
+ get config() {
218
+ return { ...this._config }
219
+ }
220
+ }
221
+
222
+ // 挂载到 Vue 原型
223
+ Vue.prototype.$voice = voiceCommand
224
+
225
+ // 混入生命周期钩子(自动清理页面指令)
226
+ Vue.mixin({
227
+ onUnload() {
228
+ // 页面卸载时自动清除页面指令
229
+ if (this.$voice && this.$voice._matcher) {
230
+ const pages = getCurrentPages()
231
+ const currentPage = pages[pages.length - 1]
232
+ const route = currentPage ? currentPage.route : ''
233
+
234
+ this.$voice._matcher.unregisterCommands('page', route)
235
+ }
236
+ }
237
+ })
238
+
239
+ if (config.debug) {
240
+ console.log('[sk-voice-command] 插件已安装', config)
241
+ }
242
+ }
243
+
244
+ // 默认导出插件
245
+ export default { install }
246
+
247
+ // 导出常量和工具(供高级使用)
248
+ export { STATUS, ERROR_CODES, ERROR_MESSAGES }
249
+ export { CommandMatcher } from './core/command-matcher'
250
+ export { default as store } from './core/store'
251
+ export * from './utils/helpers'
252
+ export * from './utils/leven'
@@ -0,0 +1,73 @@
1
+ /**
2
+ * 状态常量
3
+ */
4
+ export const STATUS = {
5
+ IDLE: 'idle', // 空闲
6
+ RECORDING: 'recording', // 录音中
7
+ RECOGNIZING: 'recognizing', // 识别中
8
+ COMPLETED: 'completed', // 已完成
9
+ ERROR: 'error' // 错误
10
+ }
11
+
12
+ /**
13
+ * 默认配置
14
+ */
15
+ export const DEFAULT_CONFIG = {
16
+ // 匹配阈值 (0-1)
17
+ matchThreshold: 0.8,
18
+ // 确认阈值 (0-1)
19
+ confirmThreshold: 0.6,
20
+ // 启用中间结果
21
+ enableIntermediateResult: true,
22
+ // 调试模式
23
+ debug: false,
24
+ // 录音超时时间(毫秒)
25
+ recordTimeout: 60000,
26
+ // 音频格式
27
+ format: 'pcm',
28
+ // 采样率
29
+ sampleRate: 16000
30
+ }
31
+
32
+ /**
33
+ * 错误码映射
34
+ */
35
+ export const ERROR_CODES = {
36
+ // 权限错误
37
+ PERMISSION_DENIED: 'PERMISSION_DENIED',
38
+ PERMISSION_DENIED_ALWAYS: 'PERMISSION_DENIED_ALWAYS',
39
+
40
+ // 网络错误
41
+ NETWORK_ERROR: 'NETWORK_ERROR',
42
+ TOKEN_EXPIRED: 'TOKEN_EXPIRED',
43
+ TOKEN_INVALID: 'TOKEN_INVALID',
44
+ CONNECTION_FAILED: 'CONNECTION_FAILED',
45
+
46
+ // 录音错误
47
+ RECORD_FAILED: 'RECORD_FAILED',
48
+ RECORD_TIMEOUT: 'RECORD_TIMEOUT',
49
+
50
+ // 识别错误
51
+ RECOGNITION_FAILED: 'RECOGNITION_FAILED',
52
+ RECOGNITION_TIMEOUT: 'RECOGNITION_TIMEOUT',
53
+
54
+ // 其他错误
55
+ UNKNOWN_ERROR: 'UNKNOWN_ERROR'
56
+ }
57
+
58
+ /**
59
+ * 错误信息映射
60
+ */
61
+ export const ERROR_MESSAGES = {
62
+ [ERROR_CODES.PERMISSION_DENIED]: '需要麦克风权限才能使用语音功能',
63
+ [ERROR_CODES.PERMISSION_DENIED_ALWAYS]: '请在系统设置中开启麦克风权限',
64
+ [ERROR_CODES.NETWORK_ERROR]: '网络连接失败,请检查网络设置',
65
+ [ERROR_CODES.TOKEN_EXPIRED]: '访问凭证已过期',
66
+ [ERROR_CODES.TOKEN_INVALID]: '访问凭证无效',
67
+ [ERROR_CODES.CONNECTION_FAILED]: '连接服务器失败',
68
+ [ERROR_CODES.RECORD_FAILED]: '录音失败,请检查麦克风',
69
+ [ERROR_CODES.RECORD_TIMEOUT]: '录音超时',
70
+ [ERROR_CODES.RECOGNITION_FAILED]: '语音识别失败',
71
+ [ERROR_CODES.RECOGNITION_TIMEOUT]: '语音识别超时',
72
+ [ERROR_CODES.UNKNOWN_ERROR]: '发生未知错误'
73
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * 格式化时间(毫秒转 MM:SS)
3
+ * @param {number} ms - 毫秒数
4
+ * @returns {string} 格式化后的时间字符串
5
+ */
6
+ export function formatTime(ms) {
7
+ const totalSeconds = Math.floor(ms / 1000)
8
+ const minutes = Math.floor(totalSeconds / 60)
9
+ const seconds = totalSeconds % 60
10
+
11
+ return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`
12
+ }
13
+
14
+ /**
15
+ * 清理文本
16
+ * @param {string} text - 待清理的文本
17
+ * @returns {string} 清理后的文本
18
+ */
19
+ export function cleanText(text) {
20
+ if (!text || typeof text !== 'string') return ''
21
+ return text.trim().replace(/\s+/g, ' ')
22
+ }
23
+
24
+ /**
25
+ * 验证指令配置
26
+ * @param {Object} command - 指令对象
27
+ * @returns {boolean} 是否有效
28
+ * @throws {Error} 如果配置无效
29
+ */
30
+ export function validateCommand(command) {
31
+ if (!command || typeof command !== 'object') {
32
+ throw new Error('指令必须是对象')
33
+ }
34
+
35
+ if (!command.id || typeof command.id !== 'string') {
36
+ throw new Error('指令必须包含 id 字段(字符串)')
37
+ }
38
+
39
+ if (!command.keywords || !Array.isArray(command.keywords)) {
40
+ throw new Error('指令必须包含 keywords 字段(数组)')
41
+ }
42
+
43
+ if (command.keywords.length === 0) {
44
+ throw new Error('指令 keywords 不能为空')
45
+ }
46
+
47
+ if (typeof command.action !== 'function') {
48
+ throw new Error('指令必须包含 action 字段(函数)')
49
+ }
50
+
51
+ return true
52
+ }
53
+
54
+ /**
55
+ * 验证配置对象
56
+ * @param {Object} config - 配置对象
57
+ * @returns {Object} 验证后的配置(合并默认值)
58
+ * @throws {Error} 如果配置无效
59
+ */
60
+ export function validateConfig(config = {}) {
61
+ const { DEFAULT_CONFIG, ERROR_CODES } = require('./constants')
62
+
63
+ if (!config.appkey || typeof config.appkey !== 'string') {
64
+ throw new Error('必须提供 appkey(字符串)')
65
+ }
66
+
67
+ if (!config.tokenUrl || typeof config.tokenUrl !== 'string') {
68
+ throw new Error('必须提供 tokenUrl(字符串)')
69
+ }
70
+
71
+ return {
72
+ ...DEFAULT_CONFIG,
73
+ ...config
74
+ }
75
+ }
76
+
77
+ /**
78
+ * 防抖函数
79
+ * @param {Function} func - 要防抖的函数
80
+ * @param {number} wait - 等待时间(毫秒)
81
+ * @returns {Function} 防抖后的函数
82
+ */
83
+ export function debounce(func, wait) {
84
+ let timeout
85
+ return function executedFunction(...args) {
86
+ const later = () => {
87
+ clearTimeout(timeout)
88
+ func(...args)
89
+ }
90
+ clearTimeout(timeout)
91
+ timeout = setTimeout(later, wait)
92
+ }
93
+ }
94
+
95
+ /**
96
+ * 节流函数
97
+ * @param {Function} func - 要节流的函数
98
+ * @param {number} wait - 等待时间(毫秒)
99
+ * @returns {Function} 节流后的函数
100
+ */
101
+ export function throttle(func, wait) {
102
+ let inThrottle
103
+ return function executedFunction(...args) {
104
+ if (!inThrottle) {
105
+ func(...args)
106
+ inThrottle = true
107
+ setTimeout(() => inThrottle = false, wait)
108
+ }
109
+ }
110
+ }
111
+
112
+ /**
113
+ * 深度克隆对象
114
+ * @param {*} obj - 要克隆的对象
115
+ * @returns {*} 克隆后的对象
116
+ */
117
+ export function deepClone(obj) {
118
+ if (obj === null || typeof obj !== 'object') return obj
119
+ if (obj instanceof Date) return new Date(obj.getTime())
120
+ if (obj instanceof Array) return obj.map(item => deepClone(item))
121
+
122
+ const clonedObj = {}
123
+ for (const key in obj) {
124
+ if (obj.hasOwnProperty(key)) {
125
+ clonedObj[key] = deepClone(obj[key])
126
+ }
127
+ }
128
+ return clonedObj
129
+ }
130
+
131
+ /**
132
+ * 简单的 ID 生成器
133
+ * @returns {string} 唯一 ID
134
+ */
135
+ export function generateId() {
136
+ return `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
137
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * 计算 Levenshtein 距离(编辑距离)
3
+ * @param {string} str1 - 字符串1
4
+ * @param {string} str2 - 字符串2
5
+ * @returns {number} 编辑距离
6
+ */
7
+ export function levenshteinDistance(str1, str2) {
8
+ if (!str1 || !str2) return 0
9
+
10
+ const m = str1.length
11
+ const n = str2.length
12
+
13
+ // 如果其中一个字符串为空,返回另一个字符串的长度
14
+ if (m === 0) return n
15
+ if (n === 0) return m
16
+
17
+ // 创建二维数组
18
+ const dp = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0))
19
+
20
+ // 初始化第一行和第一列
21
+ for (let i = 0; i <= m; i++) dp[i][0] = i
22
+ for (let j = 0; j <= n; j++) dp[0][j] = j
23
+
24
+ // 动态规划计算
25
+ for (let i = 1; i <= m; i++) {
26
+ for (let j = 1; j <= n; j++) {
27
+ if (str1[i - 1] === str2[j - 1]) {
28
+ // 字符相同,不需要操作
29
+ dp[i][j] = dp[i - 1][j - 1]
30
+ } else {
31
+ // 取三种操作的最小值:插入、删除、替换
32
+ dp[i][j] = Math.min(
33
+ dp[i - 1][j] + 1, // 删除
34
+ dp[i][j - 1] + 1, // 插入
35
+ dp[i - 1][j - 1] + 1 // 替换
36
+ )
37
+ }
38
+ }
39
+ }
40
+
41
+ return dp[m][n]
42
+ }
43
+
44
+ /**
45
+ * 计算两个字符串的相似度(0-1)
46
+ * @param {string} str1 - 字符串1
47
+ * @param {string} str2 - 字符串2
48
+ * @returns {number} 相似度(0-1之间,1表示完全相同)
49
+ */
50
+ export function calculateSimilarity(str1, str2) {
51
+ if (!str1 || !str2) return 0
52
+ if (str1 === str2) return 1
53
+
54
+ const distance = levenshteinDistance(str1, str2)
55
+ const maxLen = Math.max(str1.length, str2.length)
56
+
57
+ if (maxLen === 0) return 1
58
+
59
+ // 相似度 = (最大长度 - 编辑距离) / 最大长度
60
+ return (maxLen - distance) / maxLen
61
+ }
62
+
63
+ /**
64
+ * 在多个关键词中找到最佳匹配
65
+ * @param {string} text - 待匹配的文本
66
+ * @param {Array<string>} keywords - 关键词数组
67
+ * @returns {Object} 匹配结果 { keyword, similarity, index }
68
+ */
69
+ export function findBestMatch(text, keywords) {
70
+ if (!text || !keywords || keywords.length === 0) {
71
+ return { keyword: null, similarity: 0, index: -1 }
72
+ }
73
+
74
+ let bestMatch = { keyword: null, similarity: 0, index: -1 }
75
+
76
+ keywords.forEach((keyword, index) => {
77
+ const similarity = calculateSimilarity(text, keyword)
78
+ if (similarity > bestMatch.similarity) {
79
+ bestMatch = { keyword, similarity, index }
80
+ }
81
+ })
82
+
83
+ return bestMatch
84
+ }
85
+
86
+ /**
87
+ * 批量计算文本与多个关键词的相似度
88
+ * @param {string} text - 待匹配的文本
89
+ * @param {Array<string>} keywords - 关键词数组
90
+ * @returns {Array<Object>} 相似度结果数组
91
+ */
92
+ export function calculateAllSimilarities(text, keywords) {
93
+ if (!text || !keywords || keywords.length === 0) {
94
+ return []
95
+ }
96
+
97
+ return keywords.map((keyword, index) => ({
98
+ keyword,
99
+ similarity: calculateSimilarity(text, keyword),
100
+ index
101
+ })).sort((a, b) => b.similarity - a.similarity) // 按相似度降序排序
102
+ }
103
+
104
+ // 缓存优化(可选)
105
+ const similarityCache = new Map()
106
+
107
+ /**
108
+ * 带缓存的相似度计算
109
+ * @param {string} str1 - 字符串1
110
+ * @param {string} str2 - 字符串2
111
+ * @returns {number} 相似度
112
+ */
113
+ export function calculateSimilarityCached(str1, str2) {
114
+ if (!str1 || !str2) return 0
115
+
116
+ const cacheKey = `${str1}|||${str2}`
117
+ const reverseKey = `${str2}|||${str1}`
118
+
119
+ if (similarityCache.has(cacheKey)) {
120
+ return similarityCache.get(cacheKey)
121
+ }
122
+
123
+ if (similarityCache.has(reverseKey)) {
124
+ return similarityCache.get(reverseKey)
125
+ }
126
+
127
+ const similarity = calculateSimilarity(str1, str2)
128
+ similarityCache.set(cacheKey, similarity)
129
+
130
+ // 限制缓存大小(避免内存泄漏)
131
+ if (similarityCache.size > 1000) {
132
+ const firstKey = similarityCache.keys().next().value
133
+ similarityCache.delete(firstKey)
134
+ }
135
+
136
+ return similarity
137
+ }
138
+
139
+ /**
140
+ * 清空缓存
141
+ */
142
+ export function clearSimilarityCache() {
143
+ similarityCache.clear()
144
+ }