diff --git a/package.json b/package.json new file mode 100644 index 0000000..ce5ba7f --- /dev/null +++ b/package.json @@ -0,0 +1,33 @@ +{ + "name": "my-vue-app", + "private": true, + "version": "0.2.0", + "scripts": { + "dev": "vite", + "build": "vite build --mode production", + "preview": "vite preview" + }, + "dependencies": { + "@element-plus/icons-vue": "^2.3.1", + "@types/dat.gui": "^0.7.13", + "axios": "^1.6.7", + "dat.gui": "^0.7.9", + "echarts": "^6.0.0", + "element-plus": "^2.10.2", + "less": "^4.3.0", + "moment": "^2.30.1", + "mqtt": "^5.13.2", + "sass": "^1.89.2", + "sass-loader": "^16.0.5", + "vue": "^3.2.25", + "vue-router": "^4.5.1", + "wav-encoder": "^1.3.0", + "xlsx": "^0.18.5" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^2.3.4", + "unplugin-auto-import": "^19.3.0", + "unplugin-vue-components": "^28.7.0", + "vite": "^2.9.15" + } +} diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..df36fcf Binary files /dev/null and b/public/favicon.ico differ diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..02b8155 --- /dev/null +++ b/src/App.vue @@ -0,0 +1,16 @@ + + + + diff --git a/src/config/index.js b/src/config/index.js new file mode 100644 index 0000000..5178f4e --- /dev/null +++ b/src/config/index.js @@ -0,0 +1,90 @@ +/* + * @Author: 季万俊 + * @Date: 2025-08-22 17:04:41 + * @Description: + */ +const config = { + "page": "Ecommerce Home", + "commands": [ + { + "command": "open_login_111", + "description": "打开登录弹窗或页面", + "action": "click", + "selector": "#login-button", + "params": [] + }, + { + "command": "open_login_222_333", + "description": "打开登录弹窗或页面", + "action": "click", + "selector": "#login-button", + "params": [] + }, + { + "command": "input_username", + "description": "在用户名输入框中填写用户名", + "action": "input", + "selector": "#username-input", + "params": [ + { + "name": "username", + "type": "string", + "description": "要输入的用户名" + } + ] + }, + { + "command": "input_password", + "description": "在密码输入框中填写密码", + "action": "input", + "selector": "#password-input", + "params": [ + { + "name": "password", + "type": "string", + "description": "要输入的密码" + } + ] + }, + { + "command": "submit_login", + "description": "提交登录表单", + "action": "click", + "selector": "#submit-login", + "params": [] + }, + { + "command": "navigate_to_product", + "description": "导航到指定产品页面", + "action": "navigate", + "selector": "", + "params": [ + { + "name": "product_url", + "type": "string", + "description": "产品页面的URL" + } + ] + }, + { + "command": "add_to_cart", + "description": "将当前产品添加到购物车", + "action": "click", + "selector": ".add-to-cart-btn", + "params": [] + }, + { + "command": "search_product", + "description": "在搜索框中输入关键词并搜索", + "action": "input_and_submit", + "selector": "#search-input", + "params": [ + { + "name": "keyword", + "type": "string", + "description": "搜索关键词" + } + ] + } + ] +}; \ No newline at end of file diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..53a1295 --- /dev/null +++ b/src/main.js @@ -0,0 +1,18 @@ +/* + * @Author: 季万俊 + * @Date: 2025-06-10 14:36:16 + * @Description: + */ +import { createApp } from 'vue' +import App from './App.vue' +import 'element-plus/dist/index.css' +import ElementPlus from 'element-plus' +import zhCN from 'element-plus/dist/locale/zh-cn.mjs' // 引入中文 +import router from './router' // 引入路由配置 +import * as ElementPlusIconsVue from '@element-plus/icons-vue' + +const app = createApp(App); +for (const [key, component] of Object.entries(ElementPlusIconsVue)) { + app.component(key, component) +} +app.use(ElementPlus, { locale: zhCN }).use(router).mount('#app') diff --git a/src/router/index.js b/src/router/index.js new file mode 100644 index 0000000..aef8d67 --- /dev/null +++ b/src/router/index.js @@ -0,0 +1,24 @@ +/* + * @Author: 季万俊 + * @Date: 2025-08-07 10:45:45 + * @Description: + */ +// src/router/index.js +import { createRouter, createWebHistory } from 'vue-router' +// 导入需要路由的组件 +import index from './../view/index.vue'; + +const routes = [ + { + path: '/', + name: 'index', + component: index + }, +] + +const router = createRouter({ + history: createWebHistory('/'), + routes +}) + +export default router \ No newline at end of file diff --git a/src/util/client.js b/src/util/client.js new file mode 100644 index 0000000..995f915 --- /dev/null +++ b/src/util/client.js @@ -0,0 +1,604 @@ +import mqtt from "mqtt"; + +// 实例池,用于缓存相同配置的实例(仅用于MQTT) +const instancePool = new Map(); + +class DataMiddleware { + /** + * 构造函数 + * @param {Object} options - 实例配置 + */ + constructor(options = {}) { + // 存储实例配置 + this.options = { + url: options.url || '', + reconnectConfig: { + maxRetries: 10, //最大重连次数 + initialDelay: 1000, // + maxDelay: 30000, + ...options.reconnectConfig + }, + mqttOptions: options.mqttOptions || {}, + httpOptions: options.httpOptions || {}, + reconnect: true + }; + + // 存储所有请求/订阅的信息 + this.requests = { + http: new Map(), // HTTP请求仅用于跟踪当前请求以便取消 + mqtt: new Map() // MQTT订阅缓存: key为topic + }; + + // 客户端实例 + this.mqttClient = null; + + // 连接状态 + this.connectionStatus = { + mqtt: 'disconnected', // disconnected, connecting, connected, error + http: 'idle', + retryCount: 0 + }; + + // 用于生成唯一订阅ID + this.subscriptionIdCounter = 0; + } + + /** + * 获取实例的唯一标识键 + */ + getInstanceKey() { + return this.options.url; + } + + /** + * 静态方法:获取或创建实例 + */ + static getInstance(options = {}) { + const key = options.url || ''; + + if (instancePool.has(key)) { + return instancePool.get(key); + } + + const instance = new DataMiddleware(options); + instancePool.set(key, instance); + + return instance; + } + + /** + * 获取当前连接状态 + */ + getStatus() { + return { ...this.connectionStatus }; + } + + /** + * 获取当前MQTT客户端实例(用于发布消息) + */ + getMqttClient() { + return this.mqttClient; + } + + /** + * HTTP请求方法 - 优化参数处理,支持keys可选 + * @param {string} url - 请求地址 + * @param {string[]|Function} [keys] - 需要提取的key数组,可选 + * @param {Function} [callback] - 回调函数,格式: (data, error) => {},可选 + * @returns {Promise} 返回Promise对象,同时通过callback暴露结果 + */ + httpRequest(url, keys, callback) { + // 处理参数:支持keys可选,自动识别callback + let actualKeys = []; + let actualCallback = null; + + if (typeof keys === 'function') { + actualCallback = keys; + actualKeys = []; + } else if (Array.isArray(keys) && typeof callback === 'function') { + actualKeys = keys; + actualCallback = callback; + } else if (Array.isArray(keys)) { + actualKeys = keys; + actualCallback = null; + } else if (arguments.length === 1) { + actualKeys = []; + actualCallback = null; + } + + // 生成唯一请求ID + const requestId = this.generateSubscriptionId(); + + // 创建AbortController用于取消请求 + const controller = new AbortController(); + + // 存储请求信息以便后续取消 + this.requests.http.set(requestId, { + controller, + url + }); + + // 将keys数组转换为逗号分隔的字符串 + const keysStr = Array.isArray(actualKeys) && actualKeys.length > 0 + ? actualKeys.join(',') + : ''; + + // 构建URL并添加keys参数 + const fullUrl = new URL(url); + if (keysStr) { + fullUrl.searchParams.append('keys', keysStr); + } + + // 构建请求选项,禁用缓存 + const fetchOptions = { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0', + ...this.options.httpOptions.headers + }, + signal: controller.signal, + ...this.options.httpOptions + }; + + this.connectionStatus.http = 'loading'; + + // 返回Promise,同时支持callback + const promise = new Promise((resolve, reject) => { + fetch(fullUrl.toString(), fetchOptions) + .then(response => { + this.connectionStatus.http = 'idle'; + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return response.json(); + }) + .then(data => { + // 请求完成后从跟踪表中移除 + this.requests.http.delete(requestId); + + // 根据keys过滤数据 + // const filteredData = this._filterDataByKeys(data, actualKeys); + + // 调用回调函数(如果提供) + if (typeof actualCallback === 'function') { + actualCallback(data, null); + } + + resolve(data); + }) + .catch(error => { + this.connectionStatus.http = 'idle'; + this.requests.http.delete(requestId); + + // 忽略取消请求的错误 + if (error.name !== 'AbortError') { + // 调用回调函数(如果提供) + if (typeof actualCallback === 'function') { + actualCallback(null, error); + } + reject(error); + } + }); + }); + + // 将请求ID附加到promise上,方便取消请求 + promise.requestId = requestId; + return promise; + } + + /** + * 取消HTTP请求 + * @param {string} requestId - 请求ID + * @returns {boolean} 是否取消成功 + */ + cancelHttpRequest(requestId) { + if (this.requests.http.has(requestId)) { + const { controller } = this.requests.http.get(requestId); + controller.abort(); + this.requests.http.delete(requestId); + return true; + } + return false; + } + + /** + * 连接到MQTT服务器 + */ + _connectMqtt() { + if (!this.options.url) { + console.error('MQTT WebSocket URL未配置'); + return; + } + + // 如果已有连接,先断开 + if (this.mqttClient) { + this.mqttClient.end(false, {}, () => { + console.log('已断开现有MQTT连接'); + }); + } + + this.connectionStatus.mqtt = 'connecting'; + this._notifyAllMqttSubscribers({ + type: 'status', + status: 'connecting' + }); + + // 使用mqtt.js连接 + this.mqttClient = mqtt.connect(this.options.url, { + clientId: `vue-client-${Math.random().toString(36).substr(2, 10)}`, + clean: true, + reconnectPeriod: 0, // 禁用内置重连,使用自定义重连策略 + ...this.options.mqttOptions + }); + + + // 连接成功回调 + this.mqttClient.on('connect', () => { + console.log('MQTT连接成功'); + this.connectionStatus.mqtt = 'connected'; + this.connectionStatus.retryCount = 0; + + // 通知所有订阅者 + this._notifyAllMqttSubscribers({ + type: 'status', + status: 'connected' + }); + + // 重新订阅所有主题 + this.requests.mqtt.forEach((info, topic) => { + this.mqttClient.subscribe(topic, { qos: info.qos }); + }); + }); + + // 收到消息回调 + this.mqttClient.on('message', (topic, message) => { + try { + const data = JSON.parse(message.toString()); + this._onMqttMessage(topic, data); + } catch (error) { + console.error('解析MQTT消息失败:', error); + this._notifyMqttError(topic, new Error('消息格式错误')); + } + }); + + // 连接断开回调 + this.mqttClient.on('close', () => { + if (this.connectionStatus.mqtt !== 'disconnected') { + console.log('MQTT连接已关闭'); + this.connectionStatus.mqtt = 'disconnected'; + this._notifyAllMqttSubscribers({ + type: 'status', + status: 'disconnected' + }); + if(this.reconnect) { + this._scheduleReconnect(); + } + } + }); + + // 错误回调 + this.mqttClient.on('error', (error) => { + console.error('MQTT错误:', error); + this.connectionStatus.mqtt = 'error'; + this._notifyAllMqttSubscribers({ + type: 'error', + error: new Error(`连接错误: ${error.message}`) + }); + }); + } + + /** + * 订阅MQTT主题 + */ + + + mqttSubscribe(topics, callback, options = {}) { + if (!this.options.url) { + throw new Error('MQTT WebSocket URL未配置'); + } + + // 验证输入参数 + if (!Array.isArray(topics) || topics.length === 0) { + throw new Error('请提供有效的主题数组'); + } + + if (typeof callback !== 'function') { + throw new Error('请提供回调函数'); + } + + const subIds = []; + + // 遍历主题数组,为每个主题创建订阅 + topics.forEach(topic => { + const subId = this.generateSubscriptionId(); + const mqttKey = topic; + + // 添加订阅者 + if (this.requests.mqtt.has(mqttKey)) { + const mqttInfo = this.requests.mqtt.get(mqttKey); + mqttInfo.subscribers[subId] = { callback }; + } else { + const mqttInfo = { + topic, + options, + subscribers: { + [subId]: { callback } + }, + qos: options.qos || 1 + }; + this.requests.mqtt.set(mqttKey, mqttInfo); + } + + subIds.push(subId); + }); + + // 连接MQTT(如果尚未连接) + if (!this.mqttClient || !this.mqttClient.connected) { + this._connectMqtt(); + } else { + // 已连接状态下直接订阅所有主题 + topics.forEach(topic => { + this.mqttClient.subscribe(topic, { qos: options.qos || 0 }); + }); + } + + // 返回所有订阅ID的数组 + return subIds; + } + + /** + * 处理接收到的MQTT消息 + */ + _onMqttMessage(topic, data) { + const mqttInfo = this.requests.mqtt.get(topic); + if (!mqttInfo) return; + + // 通知该主题的所有订阅者 + Object.values(mqttInfo.subscribers).forEach(({ keys, callback }) => { + try { + const filteredData = this._filterDataByKeys(data, keys); + callback({ + type: 'data', + data: filteredData, + topic, + timestamp: Date.now() + }, null); + } catch (error) { + console.error('通知MQTT订阅者失败:', error); + } + }); + } + + /** + * 发布MQTT消息 + */ + mqttPublish(topic, message, options = {}) { + return new Promise((resolve, reject) => { + if (!this.mqttClient || !this.mqttClient.connected) { + reject(new Error('MQTT客户端未连接')); + return; + } + + this.mqttClient.publish(topic, JSON.stringify(message), options, (error) => { + if (error) { + reject(error); + } else { + resolve(true); + } + }); + }); + } + + /** + * 安排重连 + */ + _scheduleReconnect() { + if (this.connectionStatus.retryCount >= this.options.reconnectConfig.maxRetries) { + console.error('已达到最大重连次数'); + return; + } + + this.connectionStatus.retryCount++; + const delay = Math.min( + this.options.reconnectConfig.initialDelay * Math.pow(2, this.connectionStatus.retryCount - 1), + this.options.reconnectConfig.maxDelay + ); + + console.log(`将在 ${delay}ms 后尝试重连 (第 ${this.connectionStatus.retryCount} 次)`); + + setTimeout(() => { + this._connectMqtt(); + }, delay); + } + + /** + * 计算重连延迟 + */ + _calculateReconnectDelay(attempt) { + const delay = this.options.reconnectConfig.initialDelay * Math.pow(2, attempt); + return Math.min(delay, this.options.reconnectConfig.maxDelay); + } + + /** + * 取消订阅 + */ + unsubscribe(data) { + if(data && data.length) { + data.forEach((d)=> { + this.unsubscribeFun(d); + }) + } + } + + + /** + * 取消订阅 + */ + unsubscribeFun(subId) { + // if (!['http', 'mqtt'].includes(type)) { + // console.error('无效的订阅类型'); + // return false; + // } + + let removed = false; + this.requests['mqtt'].forEach((info, key) => { + + if (info.subscribers && info.subscribers[subId]) { + delete info.subscribers[subId]; + removed = true; + console.log(info.subscribers,"----info.subscribers"); + + // 如果没有订阅者了,清理资源 + if (info.subscribers && Object.keys(info.subscribers).length === 0) { + console.log('移除------'); + this._cleanupRequest( key, info); + } + } + }); + + return removed; + } + + /** + * 清理不再有订阅者的请求/订阅 + */ + _cleanupRequest(key, info) { + if (this.mqttClient && this.mqttClient.connected) { + this.mqttClient.unsubscribe(info.topic); + } + this.requests.mqtt.delete(key); + if(!this.requests.mqtt.size) { + this.destroy(); + } + // if (type === 'http') { + // 清除HTTP请求的控制器 + // if (info.controller) { + // info.controller.abort(); + // } + // this.requests.http.delete(key); + // } else if (type === 'mqtt') { + // if (this.mqttClient && this.mqttClient.connected) { + // this.mqttClient.unsubscribe(info.topic); + // } + // this.requests.mqtt.delete(key); + // } + } + + /** + * 生成唯一订阅ID + */ + generateSubscriptionId() { + return `sub_${this.subscriptionIdCounter++}`; + } + + /** + * 根据key数组过滤数据 + */ + _filterDataByKeys(data, keys) { + if (!keys || keys.length === 0) { + return data; + } + + if (Array.isArray(data)) { + return data.map(item => this._filterObjectByKeys(item, keys)); + } + + return this._filterObjectByKeys(data, keys); + } + + /** + * 根据key数组过滤对象属性 + */ + _filterObjectByKeys(obj, keys) { + if (typeof obj !== 'object' || obj === null) { + return obj; + } + + return keys.reduce((result, key) => { + const value = this._getNestedValue(obj, key); + if (value !== undefined) { + result[key] = value; + } + return result; + }, {}); + } + + /** + * 获取对象的嵌套属性值 + */ + _getNestedValue(obj, key) { + return key.split('.').reduce((current, part) => { + if (current && typeof current === 'object' && part in current) { + return current[part]; + } + return undefined; + }, obj); + } + + /** + * 通知所有MQTT订阅者 + */ + _notifyAllMqttSubscribers(message) { + this.requests.mqtt.forEach((mqttInfo) => { + Object.values(mqttInfo.subscribers).forEach(({ callback }) => { + try { + if (message.error) { + callback(null, message.error); + } else { + callback(message, null); + } + } catch (err) { + console.error('通知MQTT订阅者失败:', err); + } + }); + }); + } + + /** + * 通知MQTT订阅者错误 + */ + _notifyMqttError(topic, error) { + const mqttInfo = this.requests.mqtt.get(topic); + if (!mqttInfo) return; + + Object.values(mqttInfo.subscribers).forEach(({ callback }) => { + try { + callback(null, error); + } catch (err) { + console.error('通知MQTT订阅者错误失败:', err); + } + }); + } + + /** + * 销毁实例 + */ + destroy() { + // 断开MQTT连接 + if (this.mqttClient) { + this.reconnect = false; + this.mqttClient.end(true); + this.mqttClient = null; + } + + // 取消所有HTTP请求 + // this.requests.http.forEach(info => { + // if (info.controller) { + // info.controller.abort(); + // } + // }); + + // 清空请求缓存 + // this.requests.http.clear(); + this.requests.mqtt.clear(); + + // 从实例池移除 + instancePool.delete(this.getInstanceKey()); + } +} + +export default DataMiddleware; \ No newline at end of file diff --git a/src/util/index.js b/src/util/index.js new file mode 100644 index 0000000..7dfb8d0 --- /dev/null +++ b/src/util/index.js @@ -0,0 +1,55 @@ +// 防抖函数实现 +export const debounce = (func, delay) => { + let timer = null; + + // 返回一个新函数 + return function (...args) { + // 保存当前上下文 + const context = this; + + // 如果定时器已存在,则清除它 + if (timer) { + clearTimeout(timer); + } + + // 创建新的定时器 + timer = setTimeout(() => { + // 延迟后执行原函数,并绑定正确的上下文和参数 + func.apply(context, args); + // 执行后清除定时器 + timer = null; + }, delay); + }; +} + +// 节流函数实现 - 定时器版(确保最后一次调用会被执行) +export const throttle = (func, interval) => { + console.log(interval); + + let timer = null; + let lastTime = 0; + + return function(...args) { + const context = this; + const now = Date.now(); + + // 清除之前的定时器 + if (timer) { + clearTimeout(timer); + timer = null; + } + + // 如果达到执行间隔,立即执行 + if (now - lastTime >= interval) { + func.apply(context, args); + lastTime = now; + } else { + // 否则设置定时器,确保最后一次调用会被执行 + timer = setTimeout(() => { + func.apply(context, args); + lastTime = Date.now(); + timer = null; + }, interval - (now - lastTime)); + } + }; + } \ No newline at end of file diff --git a/src/util/voice-system.js b/src/util/voice-system.js new file mode 100644 index 0000000..d720501 --- /dev/null +++ b/src/util/voice-system.js @@ -0,0 +1,451 @@ +class VoiceControlSystem { + constructor(config) { + this.config = config; + this.isListening = false; + this.recognition = null; + this.apiKey = 'sk-020189889aac40f3b050f7c60ca597f8'; // 替换为您的DeepSeek API密钥 + this.init(); + } + + init() { + console.log(123); + this.setupSpeechRecognition(); + this.setupEventListeners(); + this.uploadConfigToModel(); + this.getSupportedLanguages(); + } + + // 检测浏览器支持的语音识别语言(部分 Chromium 浏览器支持) + async getSupportedLanguages() { + console.log(123); + + if (window.SpeechRecognition?.getSupportedLanguages) { + try { + const languages = await window.SpeechRecognition.getSupportedLanguages(); + console.log('浏览器支持的语言列表:', languages); + return languages; + } catch (e) { + console.warn('无法获取支持的语言列表:', e); + } + } + // 若浏览器不支持 getSupportedLanguages,返回常见兼容语言 + return ['zh-CN', 'en-US', 'zh-TW', 'en-GB']; + } + + setupSpeechRecognition() { + const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; + + if (!SpeechRecognition) { + this.updateStatus('浏览器不支持语音识别功能', 'error'); + return; + } + + this.recognition = new SpeechRecognition(); + this.recognition.lang = 'zh-CN'; + this.recognition.extra = { + 'alternatives': 5, + 'language': ['zh-CN', 'zh-HK', 'zh-TW', 'en-US'] // 提供备选语言 + }; + this.recognition.continuous = false; + this.recognition.interimResults = false; + + this.recognition.onstart = () => { + this.isListening = true; + this.updateStatus('正在聆听...', 'listening'); + document.getElementById('voice-btn').classList.add('listening'); + }; + + this.recognition.onend = () => { + this.isListening = false; + document.getElementById('voice-btn').classList.remove('listening'); + this.updateStatus('准备就绪', 'ready'); + }; + + this.recognition.onresult = async (event) => { + const transcript = event.results[0][0].transcript; + console.log(transcript,"-----transcript"); + + this.updateStatus(`识别结果: "${transcript}"`, 'success'); + await this.processVoiceCommand(transcript); + }; + + this.recognition.onerror = (event) => { + this.updateStatus(`语音识别错误: ${event.error}`, 'error'); + }; + } + + setupEventListeners() { + const voiceBtn = document.getElementById('voice-btn'); + voiceBtn.addEventListener('click', () => { + const input = document.getElementById('input-text'); + let val = input.value; + console.log(val,"---val"); + this.toggleListening(val) + }); + + // 添加API密钥配置界面 + this.setupApiKeyConfig(); + } + + setupApiKeyConfig() { + const configBtn = document.createElement('button'); + configBtn.textContent = '配置API密钥'; + configBtn.style.marginLeft = '10px'; + configBtn.addEventListener('click', () => this.showApiKeyConfig()); + document.getElementById('voice-btn').after(configBtn); + } + + showApiKeyConfig() { + const apiKey = prompt('请输入DeepSeek API密钥:', this.apiKey); + if (apiKey !== null) { + this.apiKey = apiKey; + this.updateStatus('API密钥已更新', 'success'); + } + } + + async toggleListening(value) { + if (!this.apiKey) { + this.updateStatus('请先配置DeepSeek API密钥', 'error'); + this.showApiKeyConfig(); + return; + } + + let transcript = value; + await this.processVoiceCommand(transcript); + + // if (this.isListening) { + // this.recognition.stop(); + // } else { + // this.recognition.start(); + // } + } + + async uploadConfigToModel() { + try { + this.updateStatus('配置文件已加载', 'info'); + console.log('DeepSeek配置已准备:', this.config); + } catch (error) { + this.updateStatus('配置加载失败', 'error'); + } + } + + async processVoiceCommand(voiceText) { + try { + this.updateStatus('正在调用DeepSeek分析指令...', 'info'); + const sequence = await this.callDeepSeekAPI(voiceText); + this.updateStatus('DeepSeek指令序列生成成功', 'success'); + await this.executeSequence(sequence); + } catch (error) { + console.error('DeepSeek处理错误:', error); + this.updateStatus(`处理失败: ${error.message}`, 'error'); + + // 尝试使用备用方案 + try { + this.updateStatus('尝试使用备用方案...', 'info'); + const fallbackSequence = this.fallbackModelResponse(voiceText); + await this.executeSequence(fallbackSequence); + } catch (fallbackError) { + this.updateStatus('备用方案也失败了', 'error'); + } + } + } + + async callDeepSeekAPI(voiceText) { + const API_URL = 'https://api.deepseek.com/v1/chat/completions'; + + if (!this.apiKey) { + throw new Error('请先配置DeepSeek API密钥'); + } + + const prompt = this.buildDeepSeekPrompt(voiceText); + + try { + const response = await fetch(API_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${this.apiKey}` + }, + body: JSON.stringify({ + model: "deepseek-chat", + messages: [ + { + role: "system", + content: `你是一个专业的网页语音控制助手。请严格根据提供的配置文件和用户指令,生成准确的操作序列。 + +重要规则: +1. 只返回纯JSON格式,不要包含任何其他文本 +2. JSON结构必须包含sequence数组 +3. 每个指令必须存在于配置文件中 +4. 参数必须匹配指令定义 +5. 按逻辑顺序排列指令` + }, + { + role: "user", + content: prompt + } + ], + temperature: 0.1, + max_tokens: 1000, + response_format: { type: "json_object" } + }) + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(`DeepSeek API错误: ${response.status} ${errorData.message || ''}`); + } + + const data = await response.json(); + const content = data.choices[0].message.content; + + // 解析JSON响应 + let result; + try { + result = JSON.parse(content); + } catch (parseError) { + // 尝试提取JSON内容 + const jsonMatch = content.match(/\{[\s\S]*\}/); + if (jsonMatch) { + result = JSON.parse(jsonMatch[0]); + } else { + throw new Error('DeepSeek返回了非JSON格式的响应'); + } + } + console.log(result,"---result"); + + // 验证响应结构 + if (!result.sequence || !Array.isArray(result.sequence)) { + throw new Error('DeepSeek返回了无效的指令序列格式'); + } + + return result; + + } catch (error) { + console.error('DeepSeek API调用错误:', error); + throw new Error(`DeepSeek处理失败: ${error.message}`); + } + } + + buildDeepSeekPrompt(voiceText) { + // 构建指令选择指南 + const commandSelectionGuide = this.buildCommandSelectionGuide(); + + return `网页交互配置文件: + ${JSON.stringify(this.config, null, 2)} + + 用户语音指令:"${voiceText}" + + ${commandSelectionGuide} + + 请严格遵循以下规则生成指令序列: + 1. 每个用户指令只选择最匹配的一个命令 + 2. 不要重复执行相同功能的命令 + 3. 如果多个命令描述相似,选择命令名称最简洁的那个 + 4. 确保指令顺序合理 + 5. 只返回纯JSON格式 + + 要求格式: + { + "sequence": [ + {"command": "指令名", "params": {参数对象}} + ] + } + + 请为指令"${voiceText}"生成正确的序列:`; + } + + buildCommandSelectionGuide() { + // 分组相似命令 + const commandGroups = {}; + this.config.commands.forEach(cmd => { + const key = cmd.description + cmd.selector; + if (!commandGroups[key]) { + commandGroups[key] = []; + } + commandGroups[key].push(cmd); + }); + + let guide = "指令选择指南:\n"; + + Object.values(commandGroups).forEach(group => { + if (group.length > 1) { + guide += `\n相似命令组(选择其中一个):\n`; + group.forEach(cmd => { + guide += `- ${cmd.command}: ${cmd.description}\n`; + }); + + // 推荐选择规则 + const recommended = group.reduce((prev, current) => + prev.command.length < current.command.length ? prev : current + ); + guide += `推荐选择: ${recommended.command} (名称最简洁)\n`; + } + }); + + return guide; + } + + // 备用方案(当DeepSeek API不可用时) + fallbackModelResponse(voiceText) { + const lowerText = voiceText.toLowerCase(); + + // 简单的规则匹配 + if (lowerText.includes('登录')) { + const username = this.extractParam(voiceText, '用户') || this.extractParam(voiceText, '账号') || 'testuser'; + const password = this.extractParam(voiceText, '密码') || '123456'; + + return { + sequence: [ + { command: "open_login" }, + { command: "input_username", params: { username } }, + { command: "input_password", params: { password } }, + { command: "submit_login" } + ] + }; + } + + if (lowerText.includes('搜索')) { + const keyword = this.extractSearchKeyword(voiceText) || '商品'; + return { + sequence: [ + { command: "search_product", params: { keyword } } + ] + }; + } + + if (lowerText.includes('购物车') || lowerText.includes('加入')) { + return { + sequence: [ + { command: "add_to_cart" } + ] + }; + } + + throw new Error('无法识别的指令'); + } + + extractParam(text, paramName) { + const regex = new RegExp(`${paramName}[::]*\\s*([^\\s]+)`, 'i'); + const match = text.match(regex); + return match ? match[1] : null; + } + + extractSearchKeyword(text) { + const regex = /搜索[::]*\s*([^。,!?]+)/i; + const match = text.match(regex); + return match ? match[1].trim() : null; + } + + async executeSequence(sequence) { + for (const [index, instruction] of sequence.sequence.entries()) { + try { + await this.executeInstruction(instruction); + this.logCommand(`✓ 完成: ${instruction.command}`); + + // 在指令之间添加延迟 + if (index < sequence.sequence.length - 1) { + await this.delay(800); + } + } catch (error) { + this.logCommand(`✗ 错误: ${instruction.command} - ${error.message}`); + throw error; + } + } + } + + async executeInstruction(instruction) { + const commandConfig = this.config.commands.find(c => c.command === instruction.command); + + if (!commandConfig) { + throw new Error(`未知指令: ${instruction.command}`); + } + + this.logCommand(`开始执行: ${instruction.command}`); + + const element = document.querySelector(commandConfig.selector); + if (!element) { + throw new Error(`找不到元素: ${commandConfig.selector}`); + } + + // 滚动到元素可见 + element.scrollIntoView({ behavior: 'smooth', block: 'center' }); + + switch (commandConfig.action) { + case 'click': + element.click(); + this.logCommand(`点击: ${commandConfig.selector}`); + break; + + case 'input': + const inputParam = commandConfig.params[0]; + if (instruction.params && instruction.params[inputParam.name]) { + element.value = instruction.params[inputParam.name]; + element.dispatchEvent(new Event('input', { bubbles: true })); + this.logCommand(`输入${inputParam.name}: ${instruction.params[inputParam.name]}`); + } + break; + + case 'navigate': + if (instruction.params && instruction.params.product_url) { + window.location.href = instruction.params.product_url; + } + break; + + case 'input_and_submit': + if (instruction.params && instruction.params.keyword) { + element.value = instruction.params.keyword; + element.dispatchEvent(new Event('input', { bubbles: true })); + + // 尝试提交表单或点击相关按钮 + if (element.form) { + element.form.submit(); + } else { + // 查找提交按钮 + const submitBtn = document.querySelector('#search-submit, [type="submit"]'); + if (submitBtn) submitBtn.click(); + } + this.logCommand(`搜索: ${instruction.params.keyword}`); + } + break; + + default: + throw new Error(`未知动作类型: ${commandConfig.action}`); + } + + // 添加视觉反馈 + this.highlightElement(element); + } + + highlightElement(element) { + const originalStyle = element.style.boxShadow; + element.style.boxShadow = '0 0 0 3px #4CAF50'; + setTimeout(() => { + element.style.boxShadow = originalStyle; + }, 1000); + } + + updateStatus(message, type = 'info') { + const statusElement = document.getElementById('status'); + statusElement.textContent = message; + statusElement.className = `status ${type}`; + } + + logCommand(message) { + return + const list = document.getElementById('command-list'); + const item = document.createElement('li'); + item.textContent = `${new Date().toLocaleTimeString()}: ${message}`; + list.appendChild(item); + list.scrollTop = list.scrollHeight; + } + + delay(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } +} + +// 初始化系统 +document.addEventListener('DOMContentLoaded', () => { + new VoiceControlSystem(config); +}); \ No newline at end of file diff --git a/src/view/components/speechControl.vue b/src/view/components/speechControl.vue new file mode 100644 index 0000000..c6e74af --- /dev/null +++ b/src/view/components/speechControl.vue @@ -0,0 +1,684 @@ + + + + \ No newline at end of file diff --git a/src/view/index copy 2.vue b/src/view/index copy 2.vue new file mode 100644 index 0000000..ec228fd --- /dev/null +++ b/src/view/index copy 2.vue @@ -0,0 +1,644 @@ + + + + \ No newline at end of file diff --git a/src/view/index copy.vue b/src/view/index copy.vue new file mode 100644 index 0000000..33462a2 --- /dev/null +++ b/src/view/index copy.vue @@ -0,0 +1,316 @@ + + + + + \ No newline at end of file diff --git a/src/view/index.vue b/src/view/index.vue new file mode 100644 index 0000000..cb7ca8f --- /dev/null +++ b/src/view/index.vue @@ -0,0 +1,431 @@ + + + + + \ No newline at end of file diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..3a41f2a --- /dev/null +++ b/vite.config.js @@ -0,0 +1,28 @@ +/* + * @Author: 季万俊 + * @Date: 2025-06-10 14:36:16 + * @Description: + */ +import { defineConfig } from 'vite' +import { fileURLToPath, URL } from 'node:url' +import vue from '@vitejs/plugin-vue' +// https://vitejs.dev/config/ + +export default defineConfig({ + plugins: [vue()], + base: '', + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + } + }, + server: { + host: '0.0.0.0', // 关键:允许通过IP访问(监听所有网络接口) + port: 8080, // 可选:指定端口(默认5173) + open: true, // 可选:启动时自动打开浏览器, + proxy: { + + } + } +}) +