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.
@@ -0,0 +1,293 @@
1
+ import { findBestMatch } from '../utils/leven'
2
+ import { validateCommand } from '../utils/helpers'
3
+ import { DEFAULT_CONFIG } from '../utils/constants'
4
+
5
+ /**
6
+ * 指令匹配引擎
7
+ */
8
+ class CommandMatcher {
9
+ constructor(config = {}) {
10
+ this.config = {
11
+ matchThreshold: DEFAULT_CONFIG.matchThreshold,
12
+ confirmThreshold: DEFAULT_CONFIG.confirmThreshold,
13
+ ...config
14
+ }
15
+
16
+ // 全局指令
17
+ this.globalCommands = new Map()
18
+
19
+ // 页面指令(使用路由路径作为 key)
20
+ this.pageCommands = new Map()
21
+
22
+ // 当前路由路径
23
+ this.currentRoute = ''
24
+ }
25
+
26
+ /**
27
+ * 注册指令
28
+ * @param {string} scope - 作用域 ('global' | 'page')
29
+ * @param {Array<Object>} commands - 指令数组
30
+ * @throws {Error} 如果指令配置无效
31
+ */
32
+ registerCommands(scope, commands) {
33
+ if (scope !== 'global' && scope !== 'page') {
34
+ throw new Error('作用域必须是 global 或 page')
35
+ }
36
+
37
+ if (!Array.isArray(commands)) {
38
+ throw new Error('指令必须是数组')
39
+ }
40
+
41
+ // 验证所有指令
42
+ commands.forEach(cmd => validateCommand(cmd))
43
+
44
+ if (scope === 'global') {
45
+ // 注册全局指令
46
+ commands.forEach(cmd => {
47
+ this.globalCommands.set(cmd.id, cmd)
48
+ })
49
+ } else {
50
+ // 注册页面指令
51
+ const routeKey = this.currentRoute || 'default'
52
+ const pageCmds = this.pageCommands.get(routeKey) || []
53
+
54
+ // 合并指令(相同 id 的会被覆盖)
55
+ const cmdMap = new Map()
56
+ pageCmds.forEach(cmd => cmdMap.set(cmd.id, cmd))
57
+ commands.forEach(cmd => cmdMap.set(cmd.id, cmd))
58
+
59
+ this.pageCommands.set(routeKey, Array.from(cmdMap.values()))
60
+ }
61
+ }
62
+
63
+ /**
64
+ * 注销指令
65
+ * @param {string} scope - 作用域
66
+ * @param {string} route - 路由(页面作用域时使用)
67
+ */
68
+ unregisterCommands(scope, route) {
69
+ if (scope === 'global') {
70
+ this.globalCommands.clear()
71
+ } else if (scope === 'page') {
72
+ const routeKey = route || this.currentRoute || 'default'
73
+ this.pageCommands.delete(routeKey)
74
+ }
75
+ }
76
+
77
+ /**
78
+ * 设置当前路由
79
+ * @param {string} route - 路由路径
80
+ */
81
+ setCurrentRoute(route) {
82
+ this.currentRoute = route
83
+ }
84
+
85
+ /**
86
+ * 获取当前可用的所有指令
87
+ * @returns {Array<Object>} 指令数组
88
+ */
89
+ getAllCommands() {
90
+ const routeKey = this.currentRoute || 'default'
91
+ const pageCmds = this.pageCommands.get(routeKey) || []
92
+
93
+ // 合并页面指令和全局指令
94
+ // 页面指令优先级更高,所以放在前面
95
+ return [...pageCmds, ...Array.from(this.globalCommands.values())]
96
+ }
97
+
98
+ /**
99
+ * 获取所有关键词(包含权重)
100
+ * @returns {Array<{keyword: string, command: Object, weight: number}>} 关键词数组
101
+ */
102
+ getAllKeywords() {
103
+ const commands = this.getAllCommands()
104
+ const keywords = []
105
+
106
+ commands.forEach(cmd => {
107
+ if (cmd.keywords && Array.isArray(cmd.keywords)) {
108
+ cmd.keywords.forEach(keyword => {
109
+ keywords.push({
110
+ keyword,
111
+ command: cmd,
112
+ weight: cmd.weight || 0 // 默认权重为 0
113
+ })
114
+ })
115
+ }
116
+ })
117
+
118
+ return keywords
119
+ }
120
+
121
+ /**
122
+ * 匹配指令(支持权重)
123
+ * @param {string} text - 识别的文本
124
+ * @returns {Object} 匹配结果
125
+ */
126
+ match(text) {
127
+ if (!text || typeof text !== 'string') {
128
+ return {
129
+ matched: false,
130
+ command: null,
131
+ similarity: 0,
132
+ action: 'none'
133
+ }
134
+ }
135
+
136
+ const keywords = this.getAllKeywords()
137
+
138
+ if (keywords.length === 0) {
139
+ return {
140
+ matched: false,
141
+ command: null,
142
+ similarity: 0,
143
+ action: 'none'
144
+ }
145
+ }
146
+
147
+ // 计算所有关键词的相似度
148
+ const matches = keywords.map(k => ({
149
+ ...k,
150
+ similarity: this._calculateSimilarity(text, k.keyword)
151
+ }))
152
+
153
+ // 过滤出超过确认阈值的匹配项
154
+ const validMatches = matches.filter(m => m.similarity >= this.config.confirmThreshold)
155
+
156
+ if (validMatches.length === 0) {
157
+ return {
158
+ matched: false,
159
+ command: null,
160
+ similarity: 0,
161
+ action: 'none'
162
+ }
163
+ }
164
+
165
+ // 找出最佳匹配(考虑权重)
166
+ const bestMatch = this._findBestMatchWithWeight(validMatches)
167
+
168
+ const command = bestMatch.command
169
+ const similarity = bestMatch.similarity
170
+ let action = 'none'
171
+
172
+ // 判断操作类型
173
+ if (similarity >= this.config.matchThreshold) {
174
+ // 高相似度,自动执行
175
+ action = 'auto'
176
+ } else if (similarity >= this.config.confirmThreshold) {
177
+ // 中等相似度,需要确认
178
+ action = 'confirm'
179
+ } else {
180
+ // 低相似度,不执行
181
+ action = 'reject'
182
+ }
183
+
184
+ return {
185
+ matched: action !== 'reject',
186
+ command,
187
+ similarity,
188
+ action,
189
+ matchedKeyword: bestMatch.keyword,
190
+ weight: bestMatch.weight,
191
+ allMatches: validMatches.map(m => ({
192
+ keyword: m.keyword,
193
+ similarity: m.similarity,
194
+ weight: m.weight,
195
+ commandId: m.command.id
196
+ }))
197
+ }
198
+ }
199
+
200
+ /**
201
+ * 计算两个字符串的相似度
202
+ * @private
203
+ */
204
+ _calculateSimilarity(text1, text2) {
205
+ const { calculateSimilarity } = require('../utils/leven')
206
+ return calculateSimilarity(text1, text2)
207
+ }
208
+
209
+ /**
210
+ * 在多个匹配项中找出最佳匹配(考虑权重)
211
+ * @private
212
+ */
213
+ _findBestMatchWithWeight(matches) {
214
+ if (matches.length === 0) return null
215
+ if (matches.length === 1) return matches[0]
216
+
217
+ // 按相似度降序排序
218
+ matches.sort((a, b) => b.similarity - a.similarity)
219
+
220
+ const highestSimilarity = matches[0].similarity
221
+
222
+ // 找出所有相似度与最高相似度差异 <= 5% 的匹配项
223
+ const similarMatches = matches.filter(
224
+ m => (highestSimilarity - m.similarity) <= 0.05
225
+ )
226
+
227
+ if (similarMatches.length === 1) {
228
+ // 只有一个最高相似度项,直接返回
229
+ return similarMatches[0]
230
+ }
231
+
232
+ // 有多个相似度相近的项,按权重排序
233
+ similarMatches.sort((a, b) => b.weight - a.weight)
234
+
235
+ // 返回权重最高的(如果权重相同,返回第一个,也就是相似度最高的)
236
+ return similarMatches[0]
237
+ }
238
+
239
+ /**
240
+ * 执行指令
241
+ * @param {Object} command - 指令对象
242
+ * @returns {*} 执行结果
243
+ */
244
+ executeCommand(command) {
245
+ if (!command || typeof command.action !== 'function') {
246
+ throw new Error('无效的指令对象')
247
+ }
248
+
249
+ try {
250
+ return command.action()
251
+ } catch (error) {
252
+ console.error('执行指令失败:', error)
253
+ throw error
254
+ }
255
+ }
256
+
257
+ /**
258
+ * 获取统计信息
259
+ * @returns {Object} 统计信息
260
+ */
261
+ getStats() {
262
+ const routeKey = this.currentRoute || 'default'
263
+ const pageCmds = this.pageCommands.get(routeKey) || []
264
+
265
+ return {
266
+ globalCommandsCount: this.globalCommands.size,
267
+ pageCommandsCount: pageCmds.length,
268
+ totalCommandsCount: pageCmds.length + this.globalCommands.size,
269
+ currentRoute: this.currentRoute
270
+ }
271
+ }
272
+
273
+ /**
274
+ * 清空所有指令
275
+ */
276
+ clear() {
277
+ this.globalCommands.clear()
278
+ this.pageCommands.clear()
279
+ }
280
+
281
+ /**
282
+ * 更新配置
283
+ * @param {Object} config - 新配置
284
+ */
285
+ updateConfig(config) {
286
+ this.config = {
287
+ ...this.config,
288
+ ...config
289
+ }
290
+ }
291
+ }
292
+
293
+ export default CommandMatcher
@@ -0,0 +1,263 @@
1
+ import Vue from 'vue'
2
+ import { STATUS } from '../utils/constants'
3
+
4
+ /**
5
+ * 创建响应式状态
6
+ */
7
+ const state = Vue.observable({
8
+ // 录音状态
9
+ isRecording: false,
10
+ // 识别文本
11
+ recognitionText: '',
12
+ // 最后执行的指令
13
+ lastCommand: null,
14
+ // 错误信息
15
+ error: null,
16
+ // 当前状态
17
+ status: STATUS.IDLE,
18
+ // 录音时长(毫秒)
19
+ recordDuration: 0,
20
+ // 是否正在识别
21
+ isRecognizing: false
22
+ })
23
+
24
+ /**
25
+ * 事件监听器映射表
26
+ */
27
+ const eventListeners = new Map()
28
+
29
+ /**
30
+ * 触发事件
31
+ * @param {string} event - 事件名称
32
+ * @param {*} data - 事件数据
33
+ */
34
+ function emit(event, data) {
35
+ const listeners = eventListeners.get(event) || []
36
+ listeners.forEach(callback => {
37
+ try {
38
+ callback(data)
39
+ } catch (error) {
40
+ console.error(`事件监听器错误 [${event}]:`, error)
41
+ }
42
+ })
43
+ }
44
+
45
+ /**
46
+ * Store 对象
47
+ */
48
+ const store = {
49
+ // 状态对象(只读)
50
+ state,
51
+
52
+ // ===== Mutations =====
53
+
54
+ /**
55
+ * 设置录音状态
56
+ * @param {boolean} isRecording - 是否正在录音
57
+ */
58
+ setRecording(isRecording) {
59
+ state.isRecording = isRecording
60
+ emit('recording-change', isRecording)
61
+ emit('state-change', { ...state })
62
+ },
63
+
64
+ /**
65
+ * 设置识别文本
66
+ * @param {string} text - 识别文本
67
+ */
68
+ setText(text) {
69
+ state.recognitionText = text
70
+ emit('text-change', text)
71
+ emit('state-change', { ...state })
72
+ },
73
+
74
+ /**
75
+ * 设置状态
76
+ * @param {string} status - 状态值
77
+ */
78
+ setStatus(status) {
79
+ state.status = status
80
+ emit('status-change', status)
81
+ emit('state-change', { ...state })
82
+ },
83
+
84
+ /**
85
+ * 设置最后执行的指令
86
+ * @param {Object} command - 指令对象
87
+ */
88
+ setCommand(command) {
89
+ state.lastCommand = command
90
+ emit('command-change', command)
91
+ emit('state-change', { ...state })
92
+ },
93
+
94
+ /**
95
+ * 设置错误信息
96
+ * @param {Error|string} error - 错误信息
97
+ */
98
+ setError(error) {
99
+ state.error = error
100
+ emit('error-change', error)
101
+ emit('state-change', { ...state })
102
+ },
103
+
104
+ /**
105
+ * 设置录音时长
106
+ * @param {number} duration - 时长(毫秒)
107
+ */
108
+ setDuration(duration) {
109
+ state.recordDuration = duration
110
+ emit('duration-change', duration)
111
+ emit('state-change', { ...state })
112
+ },
113
+
114
+ /**
115
+ * 设置识别状态
116
+ * @param {boolean} isRecognizing - 是否正在识别
117
+ */
118
+ setRecognizing(isRecognizing) {
119
+ state.isRecognizing = isRecognizing
120
+ emit('recognizing-change', isRecognizing)
121
+ emit('state-change', { ...state })
122
+ },
123
+
124
+ /**
125
+ * 重置状态
126
+ */
127
+ reset() {
128
+ state.isRecording = false
129
+ state.recognitionText = ''
130
+ state.lastCommand = null
131
+ state.error = null
132
+ state.status = STATUS.IDLE
133
+ state.recordDuration = 0
134
+ state.isRecognizing = false
135
+ emit('reset')
136
+ emit('state-change', { ...state })
137
+ },
138
+
139
+ // ===== Getters =====
140
+
141
+ /**
142
+ * 获取状态
143
+ * @returns {Object} 状态对象
144
+ */
145
+ getState() {
146
+ return { ...state }
147
+ },
148
+
149
+ /**
150
+ * 是否正在录音
151
+ * @returns {boolean}
152
+ */
153
+ isRecording() {
154
+ return state.isRecording
155
+ },
156
+
157
+ /**
158
+ * 是否正在识别
159
+ * @returns {boolean}
160
+ */
161
+ isRecognizing() {
162
+ return state.isRecognizing
163
+ },
164
+
165
+ /**
166
+ * 是否空闲
167
+ * @returns {boolean}
168
+ */
169
+ isIdle() {
170
+ return state.status === STATUS.IDLE
171
+ },
172
+
173
+ /**
174
+ * 是否有错误
175
+ * @returns {boolean}
176
+ */
177
+ hasError() {
178
+ return state.error !== null
179
+ },
180
+
181
+ // ===== Event System =====
182
+
183
+ /**
184
+ * 监听事件
185
+ * @param {string} event - 事件名称
186
+ * @param {Function} callback - 回调函数
187
+ */
188
+ on(event, callback) {
189
+ if (typeof callback !== 'function') {
190
+ throw new Error('回调必须是函数')
191
+ }
192
+
193
+ if (!eventListeners.has(event)) {
194
+ eventListeners.set(event, [])
195
+ }
196
+
197
+ eventListeners.get(event).push(callback)
198
+ },
199
+
200
+ /**
201
+ * 移除事件监听
202
+ * @param {string} event - 事件名称
203
+ * @param {Function} callback - 回调函数
204
+ */
205
+ off(event, callback) {
206
+ if (!eventListeners.has(event)) return
207
+
208
+ const listeners = eventListeners.get(event)
209
+ const index = listeners.indexOf(callback)
210
+
211
+ if (index > -1) {
212
+ listeners.splice(index, 1)
213
+ }
214
+
215
+ // 如果没有监听器了,删除该事件
216
+ if (listeners.length === 0) {
217
+ eventListeners.delete(event)
218
+ }
219
+ },
220
+
221
+ /**
222
+ * 移除所有事件监听
223
+ * @param {string} event - 事件名称(可选,不传则移除所有)
224
+ */
225
+ removeAllListeners(event) {
226
+ if (event) {
227
+ eventListeners.delete(event)
228
+ } else {
229
+ eventListeners.clear()
230
+ }
231
+ },
232
+
233
+ /**
234
+ * 获取事件监听器数量
235
+ * @param {string} event - 事件名称(可选)
236
+ * @returns {number} 监听器数量
237
+ */
238
+ listenerCount(event) {
239
+ if (event) {
240
+ return (eventListeners.get(event) || []).length
241
+ }
242
+ let total = 0
243
+ for (const listeners of eventListeners.values()) {
244
+ total += listeners.length
245
+ }
246
+ return total
247
+ }
248
+ }
249
+
250
+ // 支持调试模式
251
+ if (process.env.NODE_ENV === 'development') {
252
+ Object.defineProperty(store, '__debug__', {
253
+ get() {
254
+ return {
255
+ state: { ...state },
256
+ listeners: Object.fromEntries(eventListeners),
257
+ listenerCount: store.listenerCount()
258
+ }
259
+ }
260
+ })
261
+ }
262
+
263
+ export default store
package/src/index.js ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * sk-voice-command - 阿里云语音识别指令模块
3
+ * 主入口文件
4
+ */
5
+
6
+ import plugin from './plugin'
7
+
8
+ // 默认导出 Vue 插件
9
+ export default plugin
10
+
11
+ // 导出版本号
12
+ export const version = '1.0.0'
13
+
14
+ // 导出工具函数和常量
15
+ export * from './utils/constants'
16
+ export * from './utils/helpers'
17
+ export * from './utils/leven'