451 lines
16 KiB
JavaScript
451 lines
16 KiB
JavaScript
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);
|
||
}); |