Compare commits
No commits in common. "e806d61edec0cdf36483c96f84cdae6397a679ab" and "432bc69120979309a37045999c34f4f4eb34fdf6" have entirely different histories.
e806d61ede
...
432bc69120
@ -3,7 +3,7 @@ script.type = 'module';
|
|||||||
script.src = chrome.runtime.getURL('main.js');
|
script.src = chrome.runtime.getURL('main.js');
|
||||||
document.head.appendChild(script);
|
document.head.appendChild(script);
|
||||||
|
|
||||||
// 监听来自网页(main.js)的请求
|
// 2. 监听来自网页(main.js)的请求
|
||||||
window.addEventListener("DO_AI_REQUEST", async (event) => {
|
window.addEventListener("DO_AI_REQUEST", async (event) => {
|
||||||
const { userInput, systemPrompt } = event.detail;
|
const { userInput, systemPrompt } = event.detail;
|
||||||
|
|
||||||
|
|||||||
107
main.js
107
main.js
@ -1,93 +1,64 @@
|
|||||||
// main.js
|
|
||||||
import { createPanel } from './scripts/panel.js';
|
import { createPanel } from './scripts/panel.js';
|
||||||
import { handleCommand, COMMANDS } from './scripts/commands.js';
|
import { handleCommand, COMMANDS } from './scripts/commands.js';
|
||||||
import { initVoice } from './scripts/voice.js';
|
import { initVoice } from './scripts/voice.js';
|
||||||
import { translateToCommand } from './scripts/ai.js';
|
import { translateToCommand } from './scripts/ai.js';
|
||||||
|
|
||||||
|
// 等待页面加载完成
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
|
console.log("🚀 插件主程序启动...");
|
||||||
const ui = createPanel();
|
const ui = createPanel();
|
||||||
let spaceTimer = null;
|
|
||||||
let isRecording = false;
|
|
||||||
|
|
||||||
async function startProcess(text) {
|
async function startProcess(text) {
|
||||||
if (!text) return;
|
const rawText = text.trim();
|
||||||
|
if (!rawText) return;
|
||||||
|
|
||||||
|
console.log("📩 接收到输入:", rawText);
|
||||||
|
|
||||||
|
// 1. 本地匹配逻辑
|
||||||
|
const localMatch = COMMANDS.find(c => rawText.includes(c.key));
|
||||||
|
if (localMatch) {
|
||||||
|
console.log("🎯 本地关键词匹配成功:", localMatch.key);
|
||||||
|
handleCommand(localMatch.key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. AI 翻译逻辑
|
||||||
ui.setLoading(true);
|
ui.setLoading(true);
|
||||||
try {
|
try {
|
||||||
const localMatch = COMMANDS.find(c => text.includes(c.key));
|
const aiResult = await translateToCommand(rawText);
|
||||||
if (localMatch) {
|
if (aiResult) {
|
||||||
handleCommand(localMatch.key);
|
handleCommand(aiResult);
|
||||||
} else {
|
} else {
|
||||||
const aiResult = await translateToCommand(text);
|
console.warn("❌ AI 无法理解该指令");
|
||||||
if (aiResult) handleCommand(aiResult);
|
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("AI 流程出错:", err);
|
||||||
} finally {
|
} finally {
|
||||||
ui.setLoading(false);
|
ui.setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const voiceCtrl = initVoice(document.getElementById("automation-ai-panel"), (text) => {
|
// 输入框回车事件监听
|
||||||
ui.input.value = text;
|
ui.input.addEventListener("keydown", (e) => {
|
||||||
startProcess(text);
|
|
||||||
});
|
|
||||||
|
|
||||||
// --- 逻辑 A: 保留原有按钮点击录音 ---
|
|
||||||
ui.btn.onclick = () => {
|
|
||||||
if (!isRecording) {
|
|
||||||
voiceCtrl.start();
|
|
||||||
ui.setRecording(true);
|
|
||||||
isRecording = true;
|
|
||||||
// 按钮模式下 3秒后自动停止,或靠 recognition 自动结束
|
|
||||||
setTimeout(() => {
|
|
||||||
if (isRecording) {
|
|
||||||
voiceCtrl.stop();
|
|
||||||
ui.setRecording(false);
|
|
||||||
isRecording = false;
|
|
||||||
}
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- 逻辑 B: 空格长按 0.5s 触发 ---
|
|
||||||
window.addEventListener("keydown", (e) => {
|
|
||||||
if (e.code === "Space" && e.target.tagName !== "INPUT" && e.target.tagName !== "TEXTAREA") {
|
|
||||||
if (spaceTimer || isRecording) return;
|
|
||||||
|
|
||||||
spaceTimer = setTimeout(() => {
|
|
||||||
if (voiceCtrl.supportSpeech) {
|
|
||||||
voiceCtrl.start();
|
|
||||||
ui.setRecording(true);
|
|
||||||
isRecording = true;
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener("keyup", (e) => {
|
|
||||||
if (e.code === "Space") {
|
|
||||||
if (spaceTimer) {
|
|
||||||
clearTimeout(spaceTimer);
|
|
||||||
spaceTimer = null;
|
|
||||||
}
|
|
||||||
if (isRecording) {
|
|
||||||
// 松开空格,延迟一小会儿停止,确保最后几个字能录进去
|
|
||||||
setTimeout(() => {
|
|
||||||
voiceCtrl.stop();
|
|
||||||
ui.setRecording(false);
|
|
||||||
isRecording = false;
|
|
||||||
}, 200);
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.input.onkeydown = (e) => {
|
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault(); // 防止某些页面表单提交刷新
|
||||||
const val = ui.input.value;
|
const val = ui.input.value;
|
||||||
ui.input.value = "";
|
ui.input.value = "";
|
||||||
startProcess(val);
|
startProcess(val);
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
|
// 语音识别初始化
|
||||||
|
// 修正:确保 voice.js 里的 handleCommand 回调指向我们的 startProcess
|
||||||
|
initVoice(document.getElementById("automation-ai-panel"), (spokenText) => {
|
||||||
|
ui.input.value = spokenText;
|
||||||
|
startProcess(spokenText);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (document.readyState === "complete") init();
|
// 确保在 DOM 完成后运行
|
||||||
else window.addEventListener("load", init);
|
if (document.readyState === "complete" || document.readyState === "interactive") {
|
||||||
|
init();
|
||||||
|
} else {
|
||||||
|
window.addEventListener("DOMContentLoaded", init);
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
// scripts/panel.js
|
|
||||||
export function createPanel() {
|
export function createPanel() {
|
||||||
const panelId = "automation-ai-panel";
|
const panelId = "automation-ai-panel";
|
||||||
|
// 防止重复创建
|
||||||
let panel = document.getElementById(panelId);
|
let panel = document.getElementById(panelId);
|
||||||
if (panel) return getUIRefs(panel);
|
if (panel) return getUIRefs(panel);
|
||||||
|
|
||||||
@ -10,30 +10,21 @@ export function createPanel() {
|
|||||||
position: fixed; bottom: 24px; right: 24px; z-index: 2147483647;
|
position: fixed; bottom: 24px; right: 24px; z-index: 2147483647;
|
||||||
background: white; border-radius: 10px;
|
background: white; border-radius: 10px;
|
||||||
box-shadow: 0 4px 12px rgba(0,0,0,.2); padding: 12px; width: 260px;
|
box-shadow: 0 4px 12px rgba(0,0,0,.2); padding: 12px; width: 260px;
|
||||||
font-size: 14px; font-family: sans-serif;
|
font-size: 14px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
panel.innerHTML = `
|
panel.innerHTML = `
|
||||||
<style>
|
|
||||||
@keyframes breathe {
|
|
||||||
0% { opacity: 1; transform: scale(1); }
|
|
||||||
50% { opacity: 0.5; transform: scale(1.1); }
|
|
||||||
100% { opacity: 1; transform: scale(1); }
|
|
||||||
}
|
|
||||||
.recording-dot {
|
|
||||||
width: 8px; height: 8px; background: #ff4d4f; border-radius: 50%;
|
|
||||||
display: none; margin-right: 6px; animation: breathe 1.2s infinite;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<div style="display:flex;gap:8px;align-items:center;">
|
<div style="display:flex;gap:8px;align-items:center;">
|
||||||
<input id="voiceTextInput" type="text" placeholder="输入指令或长按空格..."
|
<input id="voiceTextInput" type="text" placeholder="输入指令或自然语言..."
|
||||||
style="flex:1;padding:8px;border:1px solid #dcdfe6;border-radius:4px;outline:none;font-size:13px;" />
|
style="flex:1;padding:8px;border:1px solid #dcdfe6;border-radius:4px;outline:none;font-size:13px;" />
|
||||||
<button id="voiceBtn" style="padding:8px 12px;border:none;border-radius:4px;background:#409eff;color:#fff;cursor:pointer;">🎤</button>
|
<button id="voiceBtn" title="按 Alt+V 快捷开启"
|
||||||
|
style="padding:8px 12px;border:none;border-radius:4px;background:#409eff;color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;">
|
||||||
|
🎤
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="panelStatus" style="margin-top:10px;color:#999;font-size:12px;display:flex;align-items:center;">
|
<div id="panelStatus" style="margin-top:8px;color:#999;font-size:12px;display:flex;justify-content:space-between;align-items:center;">
|
||||||
<div id="recordingIndicator" class="recording-dot"></div>
|
|
||||||
<span id="statusText">准备就绪</span>
|
<span id="statusText">准备就绪</span>
|
||||||
<span id="aiLoading" style="display:none;color:#409eff;margin-left:auto;font-weight:bold;">🤖 思考中...</span>
|
<span id="aiLoading" style="display:none;color:#409eff;font-weight:bold;">🤖 思考中...</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -42,25 +33,19 @@ export function createPanel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getUIRefs(panel) {
|
function getUIRefs(panel) {
|
||||||
|
const input = panel.querySelector("#voiceTextInput");
|
||||||
|
const btn = panel.querySelector("#voiceBtn");
|
||||||
|
const loading = panel.querySelector("#aiLoading");
|
||||||
|
const statusText = panel.querySelector("#statusText");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
btn: panel.querySelector("#voiceBtn"),
|
input,
|
||||||
input: panel.querySelector("#voiceTextInput"),
|
btn,
|
||||||
setLoading: (loading) => {
|
setLoading: (isLoading) => {
|
||||||
panel.querySelector("#aiLoading").style.display = loading ? "inline" : "none";
|
loading.style.display = isLoading ? "inline" : "none";
|
||||||
panel.querySelector("#statusText").style.visibility = loading ? "hidden" : "visible";
|
statusText.style.display = isLoading ? "none" : "inline";
|
||||||
},
|
input.disabled = isLoading;
|
||||||
setRecording: (isRecording) => {
|
if (!isLoading) input.focus();
|
||||||
const dot = panel.querySelector("#recordingIndicator");
|
|
||||||
const text = panel.querySelector("#statusText");
|
|
||||||
if (isRecording) {
|
|
||||||
dot.style.display = "inline-block";
|
|
||||||
text.innerText = "正在聆听...";
|
|
||||||
text.style.color = "#ff4d4f";
|
|
||||||
} else {
|
|
||||||
dot.style.display = "none";
|
|
||||||
text.innerText = "准备就绪";
|
|
||||||
text.style.color = "#999";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -1,28 +1,36 @@
|
|||||||
// scripts/voice.js
|
// 语音识别
|
||||||
export function initVoice(panel, onResultCallback) {
|
|
||||||
|
export function initVoice(panel, handleCommand) {
|
||||||
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||||
if (!SpeechRecognition) return { supportSpeech: false };
|
const supportSpeech = !!SpeechRecognition;
|
||||||
|
let recognition;
|
||||||
|
|
||||||
const recognition = new SpeechRecognition();
|
if (supportSpeech) {
|
||||||
recognition.lang = "zh-CN";
|
recognition = new SpeechRecognition();
|
||||||
recognition.continuous = false; // 改为 false,确保每次停止都能立即结算
|
recognition.lang = "zh-CN";
|
||||||
recognition.interimResults = false;
|
recognition.continuous = false;
|
||||||
|
recognition.interimResults = false;
|
||||||
|
|
||||||
recognition.onresult = (event) => {
|
recognition.onresult = (event) => {
|
||||||
const text = event.results[0][0].transcript.trim();
|
const text = event.results[0][0].transcript.trim();
|
||||||
if (text) {
|
handleCommand(text);
|
||||||
console.log("🎤 识别结果:", text);
|
};
|
||||||
onResultCallback(text);
|
|
||||||
|
recognition.onerror = (event) => {
|
||||||
|
console.error("语音识别错误", event.error);
|
||||||
|
};
|
||||||
|
|
||||||
|
panel.querySelector("#voiceBtn").onclick = () => recognition.start();
|
||||||
|
} else {
|
||||||
|
panel.querySelector("#voiceBtn").disabled = true;
|
||||||
|
panel.querySelector("#voiceBtn").innerText = "❌";
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("keydown", (e) => {
|
||||||
|
if (e.altKey && e.key.toLowerCase() === "v" && supportSpeech && recognition) {
|
||||||
|
recognition.start();
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
recognition.onerror = (event) => {
|
return supportSpeech;
|
||||||
if (event.error !== 'aborted') console.error("语音错误:", event.error);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
supportSpeech: true,
|
|
||||||
start: () => { try { recognition.start(); } catch(e){} },
|
|
||||||
stop: () => { try { recognition.stop(); } catch(e){} }
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user