可以实现AI的模糊输入跳转
This commit is contained in:
parent
b7af0b0f0f
commit
4d07077ea2
30
ai.js
Normal file
30
ai.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
// ai.js
|
||||||
|
import { COMMANDS } from './commands.js';
|
||||||
|
|
||||||
|
export async function translateToCommand(userInput) {
|
||||||
|
const availableKeys = COMMANDS.map(c => c.key).join(', ');
|
||||||
|
const systemPrompt = `你是一个助手。将输入映射到以下关键词之一:${availableKeys}。只输出关键词,不要其他字。无法匹配则输出 UNKNOWN。`;
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// 1. 监听结果
|
||||||
|
const handler = (event) => {
|
||||||
|
const response = event.detail;
|
||||||
|
window.removeEventListener("AI_RESULT", handler);
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
const content = response.data.choices[0].message.content.trim();
|
||||||
|
console.log("📥 AI 识别结果:", content);
|
||||||
|
resolve(content === "UNKNOWN" ? null : content);
|
||||||
|
} else {
|
||||||
|
console.error("AI 失败:", response.error);
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener("AI_RESULT", handler);
|
||||||
|
|
||||||
|
// 2. 触发请求
|
||||||
|
window.dispatchEvent(new CustomEvent("DO_AI_REQUEST", {
|
||||||
|
detail: { userInput, systemPrompt }
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
45
background.js
Normal file
45
background.js
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
// background.js
|
||||||
|
|
||||||
|
// 监听来自 main.js 的消息
|
||||||
|
chrome.runtime.onMessageExternal.addListener((message, sender, sendResponse) => {
|
||||||
|
// 这种方式需要知道 Extension ID,更简单的方法是统一由 content.js 转发
|
||||||
|
});
|
||||||
|
|
||||||
|
// 通用监听(推荐)
|
||||||
|
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||||
|
if (request.type === "AI_TRANSLATE") {
|
||||||
|
// 1. 先从 storage 获取配置
|
||||||
|
chrome.storage.sync.get(['aiConfig'], async (result) => {
|
||||||
|
const config = result.aiConfig;
|
||||||
|
if (!config || !config.apiKey) {
|
||||||
|
sendResponse({ success: false, error: "未配置 API Key" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 2. 发起请求
|
||||||
|
const response = await fetch(`${config.apiUrl}/chat/completions`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${config.apiKey}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: config.modelName,
|
||||||
|
messages: [
|
||||||
|
{ role: "system", content: request.systemPrompt },
|
||||||
|
{ role: "user", content: request.userInput }
|
||||||
|
],
|
||||||
|
temperature: 0.1
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
sendResponse({ success: true, data: data });
|
||||||
|
} catch (err) {
|
||||||
|
sendResponse({ success: false, error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true; // 保持异步
|
||||||
|
}
|
||||||
|
});
|
||||||
36
commands.js
36
commands.js
@ -51,19 +51,39 @@ export function expandParentMenu(span) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function goToRoute(route) {
|
export function goToRoute(route) {
|
||||||
|
// 兼容 Hash 路由和普通路由,根据实际项目情况
|
||||||
window.location.hash = route;
|
window.location.hash = route;
|
||||||
console.log("✅ 已跳转到路由:", route);
|
console.log("✅ 已跳转到路由:", route);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleCommand(text) {
|
export function handleCommand(keyOrText) {
|
||||||
console.log("📥 收到指令:", text);
|
console.log("🚀 执行指令逻辑:", keyOrText);
|
||||||
const command = COMMANDS.find(c => text.includes(c.key));
|
|
||||||
|
// 尝试精确匹配 (AI返回的Key) 或 模糊匹配 (用户输入的Text)
|
||||||
|
const command = COMMANDS.find(c => c.key === keyOrText) ||
|
||||||
|
COMMANDS.find(c => keyOrText.includes(c.key));
|
||||||
|
|
||||||
if (!command) {
|
if (!command) {
|
||||||
alert("未识别指令:" + text);
|
console.warn("无法执行,未找到对应菜单动作:", keyOrText);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const span = [...document.querySelectorAll("span")]
|
|
||||||
.find(el => el.innerText.trim() === command.menu);
|
// 查找菜单 DOM 元素 (根据你原有的逻辑)
|
||||||
span && expandParentMenu(span);
|
const allSpans = Array.from(document.querySelectorAll("span"));
|
||||||
goToRoute(command.route);
|
|
||||||
|
// 优先全匹配,防止"报表"匹配到"高级报表"
|
||||||
|
let span = allSpans.find(el => el.innerText.trim() === command.menu);
|
||||||
|
|
||||||
|
if (span) {
|
||||||
|
expandParentMenu(span);
|
||||||
|
// 模拟点击触发 Vue/React 的路由跳转,或者直接改 Hash
|
||||||
|
// 如果页面需要点击才能触发加载,建议 span.click()
|
||||||
|
// 如果只是纯 hash 跳转,goToRoute 即可
|
||||||
|
|
||||||
|
// 建议先点击,确保侧边栏高亮
|
||||||
|
span.click();
|
||||||
|
goToRoute(command.route);
|
||||||
|
} else {
|
||||||
|
console.error(`❌ 未找到菜单元素: ${command.menu}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
19
content.js
19
content.js
@ -1,7 +1,22 @@
|
|||||||
// content.js 是 content script 的入口,不直接写全部逻辑
|
// content.js
|
||||||
// 用 module 方式加载其他文件
|
|
||||||
|
|
||||||
|
// 1. 注入 main.js
|
||||||
const script = document.createElement('script');
|
const script = document.createElement('script');
|
||||||
script.type = 'module';
|
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);
|
||||||
|
|
||||||
|
// 2. 监听来自网页(main.js)的请求
|
||||||
|
window.addEventListener("DO_AI_REQUEST", async (event) => {
|
||||||
|
const { userInput, systemPrompt } = event.detail;
|
||||||
|
|
||||||
|
// 转发给 background.js
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
type: "AI_TRANSLATE",
|
||||||
|
userInput,
|
||||||
|
systemPrompt
|
||||||
|
}, (response) => {
|
||||||
|
// 将结果传回给网页(main.js)
|
||||||
|
window.dispatchEvent(new CustomEvent("AI_RESULT", { detail: response }));
|
||||||
|
});
|
||||||
|
});
|
||||||
69
main.js
69
main.js
@ -1,21 +1,64 @@
|
|||||||
import { createPanel } from './panel.js';
|
import { createPanel } from './panel.js';
|
||||||
import { handleCommand } from './commands.js';
|
import { handleCommand, COMMANDS } from './commands.js';
|
||||||
import { initVoice } from './voice.js';
|
import { initVoice } from './voice.js';
|
||||||
|
import { translateToCommand } from './ai.js';
|
||||||
|
|
||||||
console.log("✅ 语音 + 文字插件已运行");
|
// 等待页面加载完成
|
||||||
|
const init = async () => {
|
||||||
|
console.log("🚀 插件主程序启动...");
|
||||||
|
const ui = createPanel();
|
||||||
|
|
||||||
const panel = createPanel();
|
async function startProcess(text) {
|
||||||
|
const rawText = text.trim();
|
||||||
|
if (!rawText) return;
|
||||||
|
|
||||||
// 文字输入支持
|
console.log("📩 接收到输入:", rawText);
|
||||||
const input = panel.querySelector("#voiceTextInput");
|
|
||||||
input.addEventListener("keydown", (e) => {
|
// 1. 本地匹配逻辑
|
||||||
if (e.key === "Enter") {
|
const localMatch = COMMANDS.find(c => rawText.includes(c.key));
|
||||||
const value = input.value.trim();
|
if (localMatch) {
|
||||||
if (value) {
|
console.log("🎯 本地关键词匹配成功:", localMatch.key);
|
||||||
handleCommand(value);
|
handleCommand(localMatch.key);
|
||||||
input.value = "";
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. AI 翻译逻辑
|
||||||
|
ui.setLoading(true);
|
||||||
|
try {
|
||||||
|
const aiResult = await translateToCommand(rawText);
|
||||||
|
if (aiResult) {
|
||||||
|
handleCommand(aiResult);
|
||||||
|
} else {
|
||||||
|
console.warn("❌ AI 无法理解该指令");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("AI 流程出错:", err);
|
||||||
|
} finally {
|
||||||
|
ui.setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
initVoice(panel, handleCommand);
|
// 输入框回车事件监听
|
||||||
|
ui.input.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault(); // 防止某些页面表单提交刷新
|
||||||
|
const val = ui.input.value;
|
||||||
|
ui.input.value = "";
|
||||||
|
startProcess(val);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 语音识别初始化
|
||||||
|
// 修正:确保 voice.js 里的 handleCommand 回调指向我们的 startProcess
|
||||||
|
initVoice(document.getElementById("automation-ai-panel"), (spokenText) => {
|
||||||
|
ui.input.value = spokenText;
|
||||||
|
startProcess(spokenText);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 确保在 DOM 完成后运行
|
||||||
|
if (document.readyState === "complete" || document.readyState === "interactive") {
|
||||||
|
init();
|
||||||
|
} else {
|
||||||
|
window.addEventListener("DOMContentLoaded", init);
|
||||||
|
}
|
||||||
@ -1,7 +1,11 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "语音界面跳转",
|
"name": "Supmea Automation AI",
|
||||||
"version": "0.0.1",
|
"version": "0.1.0",
|
||||||
|
"permissions": ["activeTab", "storage"],
|
||||||
|
"background": {
|
||||||
|
"service_worker": "background.js"
|
||||||
|
},
|
||||||
"content_scripts": [
|
"content_scripts": [
|
||||||
{
|
{
|
||||||
"matches": ["*://1718cloud.com/*"],
|
"matches": ["*://1718cloud.com/*"],
|
||||||
@ -9,10 +13,9 @@
|
|||||||
"run_at": "document_idle"
|
"run_at": "document_idle"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"permissions": ["activeTab"],
|
|
||||||
"web_accessible_resources": [
|
"web_accessible_resources": [
|
||||||
{
|
{
|
||||||
"resources": ["*.js"],
|
"resources": ["*.js", "*.css"],
|
||||||
"matches": ["*://1718cloud.com/*"]
|
"matches": ["*://1718cloud.com/*"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
42
options.html
Normal file
42
options.html
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>AI 配置中心</title>
|
||||||
|
<style>
|
||||||
|
body { padding: 30px; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; width: 400px; }
|
||||||
|
h2 { color: #333; border-bottom: 2px solid #409eff; padding-bottom: 10px; }
|
||||||
|
.form-group { margin-bottom: 15px; }
|
||||||
|
label { display: block; margin-bottom: 5px; font-weight: bold; color: #666; font-size: 14px; }
|
||||||
|
input { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box; }
|
||||||
|
.btn { background: #409eff; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; width: 100%; font-size: 16px; transition: background 0.3s; }
|
||||||
|
.btn:hover { background: #66b1ff; }
|
||||||
|
#status { margin-top: 10px; text-align: center; font-size: 13px; height: 20px; }
|
||||||
|
.note { font-size: 12px; color: #999; margin-top: 5px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>⚙️ 插件 AI 配置</h2>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>API Base URL</label>
|
||||||
|
<input type="text" id="apiUrl" placeholder="例如: https://dashscope.aliyuncs.com/compatible-mode/v1">
|
||||||
|
<div class="note">魔搭/阿里千问通常使用兼容模式地址</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>API Key</label>
|
||||||
|
<input type="password" id="apiKey" placeholder="sk-...">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Model Name</label>
|
||||||
|
<input type="text" id="modelName" placeholder="例如: qwen-plus">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="save" class="btn">保存配置</button>
|
||||||
|
<div id="status"></div>
|
||||||
|
|
||||||
|
<script src="options.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
33
options.js
Normal file
33
options.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
const defaultApiUrl = "https://dashscope.aliyuncs.com/compatible-mode/v1";
|
||||||
|
const defaultModel = "qwen-plus";
|
||||||
|
|
||||||
|
// 保存设置
|
||||||
|
document.getElementById('save').addEventListener('click', () => {
|
||||||
|
const config = {
|
||||||
|
apiUrl: document.getElementById('apiUrl').value.trim() || defaultApiUrl,
|
||||||
|
apiKey: document.getElementById('apiKey').value.trim(),
|
||||||
|
modelName: document.getElementById('modelName').value.trim() || defaultModel
|
||||||
|
};
|
||||||
|
|
||||||
|
chrome.storage.sync.set({ aiConfig: config }, () => {
|
||||||
|
const status = document.getElementById('status');
|
||||||
|
status.textContent = '✅ 配置已保存,请刷新网页生效。';
|
||||||
|
status.style.color = 'green';
|
||||||
|
setTimeout(() => { status.textContent = ''; }, 3000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 加载设置
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
chrome.storage.sync.get(['aiConfig'], (result) => {
|
||||||
|
if (result.aiConfig) {
|
||||||
|
document.getElementById('apiUrl').value = result.aiConfig.apiUrl;
|
||||||
|
document.getElementById('apiKey').value = result.aiConfig.apiKey;
|
||||||
|
document.getElementById('modelName').value = result.aiConfig.modelName;
|
||||||
|
} else {
|
||||||
|
// 默认填充
|
||||||
|
document.getElementById('apiUrl').value = defaultApiUrl;
|
||||||
|
document.getElementById('modelName').value = defaultModel;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
59
panel.js
59
panel.js
@ -1,30 +1,51 @@
|
|||||||
export function createPanel() {
|
export function createPanel() {
|
||||||
const panel = document.createElement("div");
|
const panelId = "automation-ai-panel";
|
||||||
|
// 防止重复创建
|
||||||
|
let panel = document.getElementById(panelId);
|
||||||
|
if (panel) return getUIRefs(panel);
|
||||||
|
|
||||||
|
panel = document.createElement("div");
|
||||||
|
panel.id = panelId;
|
||||||
panel.style.cssText = `
|
panel.style.cssText = `
|
||||||
position: fixed;
|
position: fixed; bottom: 24px; right: 24px; z-index: 2147483647;
|
||||||
bottom: 24px;
|
background: white; border-radius: 10px;
|
||||||
right: 24px;
|
box-shadow: 0 4px 12px rgba(0,0,0,.2); padding: 12px; width: 260px;
|
||||||
z-index: 999999;
|
font-size: 14px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
background: white;
|
|
||||||
border-radius: 10px;
|
|
||||||
box-shadow: 0 4px 12px rgba(0,0,0,.15);
|
|
||||||
padding: 10px;
|
|
||||||
width: 220px;
|
|
||||||
font-size: 14px;
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
panel.innerHTML = `
|
panel.innerHTML = `
|
||||||
<div style="display:flex;gap:6px;">
|
<div style="display:flex;gap:8px;align-items:center;">
|
||||||
<input id="voiceTextInput" placeholder="输入指令,如:添加设备"
|
<input id="voiceTextInput" type="text" placeholder="输入指令或自然语言..."
|
||||||
style="flex:1;padding:6px;border:1px solid #dcdfe6;border-radius:4px;" />
|
style="flex:1;padding:8px;border:1px solid #dcdfe6;border-radius:4px;outline:none;font-size:13px;" />
|
||||||
<button id="voiceBtn"
|
<button id="voiceBtn" title="按 Alt+V 快捷开启"
|
||||||
style="padding:6px 10px;border:none;border-radius:4px;background:#409eff;color:#fff;cursor:pointer;">
|
style="padding:8px 12px;border:none;border-radius:4px;background:#409eff;color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;">
|
||||||
🎤
|
🎤
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-top:6px;color:#999;font-size:12px;">
|
<div id="panelStatus" style="margin-top:8px;color:#999;font-size:12px;display:flex;justify-content:space-between;align-items:center;">
|
||||||
支持语音 / 回车文字指令
|
<span id="statusText">准备就绪</span>
|
||||||
|
<span id="aiLoading" style="display:none;color:#409eff;font-weight:bold;">🤖 思考中...</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
document.body.appendChild(panel);
|
document.body.appendChild(panel);
|
||||||
return panel;
|
return 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 {
|
||||||
|
input,
|
||||||
|
btn,
|
||||||
|
setLoading: (isLoading) => {
|
||||||
|
loading.style.display = isLoading ? "inline" : "none";
|
||||||
|
statusText.style.display = isLoading ? "none" : "inline";
|
||||||
|
input.disabled = isLoading;
|
||||||
|
if (!isLoading) input.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user