diff --git a/ai.js b/ai.js new file mode 100644 index 0000000..508dc5d --- /dev/null +++ b/ai.js @@ -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 } + })); + }); +} \ No newline at end of file diff --git a/background.js b/background.js new file mode 100644 index 0000000..fb1bba8 --- /dev/null +++ b/background.js @@ -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; // 保持异步 + } +}); \ No newline at end of file diff --git a/commands.js b/commands.js index 2d50036..9a3966c 100644 --- a/commands.js +++ b/commands.js @@ -51,19 +51,39 @@ export function expandParentMenu(span) { } export function goToRoute(route) { - window.location.hash = route; + // 兼容 Hash 路由和普通路由,根据实际项目情况 + window.location.hash = route; console.log("✅ 已跳转到路由:", route); } -export function handleCommand(text) { - console.log("📥 收到指令:", text); - const command = COMMANDS.find(c => text.includes(c.key)); +export function handleCommand(keyOrText) { + console.log("🚀 执行指令逻辑:", keyOrText); + + // 尝试精确匹配 (AI返回的Key) 或 模糊匹配 (用户输入的Text) + const command = COMMANDS.find(c => c.key === keyOrText) || + COMMANDS.find(c => keyOrText.includes(c.key)); + if (!command) { - alert("未识别指令:" + text); + console.warn("无法执行,未找到对应菜单动作:", keyOrText); return; } - const span = [...document.querySelectorAll("span")] - .find(el => el.innerText.trim() === command.menu); - span && expandParentMenu(span); - goToRoute(command.route); + + // 查找菜单 DOM 元素 (根据你原有的逻辑) + const allSpans = Array.from(document.querySelectorAll("span")); + + // 优先全匹配,防止"报表"匹配到"高级报表" + 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}`); + } } \ No newline at end of file diff --git a/content.js b/content.js index b05c89f..0b5dc4a 100644 --- a/content.js +++ b/content.js @@ -1,7 +1,22 @@ -// content.js 是 content script 的入口,不直接写全部逻辑 -// 用 module 方式加载其他文件 +// content.js +// 1. 注入 main.js const script = document.createElement('script'); script.type = 'module'; script.src = chrome.runtime.getURL('main.js'); -document.head.appendChild(script); \ No newline at end of file +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 })); + }); +}); \ No newline at end of file diff --git a/main.js b/main.js index 9e3f4ad..d1488bb 100644 --- a/main.js +++ b/main.js @@ -1,21 +1,64 @@ import { createPanel } from './panel.js'; -import { handleCommand } from './commands.js'; +import { handleCommand, COMMANDS } from './commands.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; -// 文字输入支持 -const input = panel.querySelector("#voiceTextInput"); -input.addEventListener("keydown", (e) => { - if (e.key === "Enter") { - const value = input.value.trim(); - if (value) { - handleCommand(value); - input.value = ""; + 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); + 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); \ No newline at end of file + // 输入框回车事件监听 + 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); +} \ No newline at end of file diff --git a/manifest.json b/manifest.json index 14e167b..0102021 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,11 @@ { "manifest_version": 3, - "name": "语音界面跳转", - "version": "0.0.1", + "name": "Supmea Automation AI", + "version": "0.1.0", + "permissions": ["activeTab", "storage"], + "background": { + "service_worker": "background.js" + }, "content_scripts": [ { "matches": ["*://1718cloud.com/*"], @@ -9,11 +13,10 @@ "run_at": "document_idle" } ], - "permissions": ["activeTab"], "web_accessible_resources": [ { - "resources": ["*.js"], + "resources": ["*.js", "*.css"], "matches": ["*://1718cloud.com/*"] } ] -} +} \ No newline at end of file diff --git a/options.html b/options.html new file mode 100644 index 0000000..f25d606 --- /dev/null +++ b/options.html @@ -0,0 +1,42 @@ + + +
+ +