// ==UserScript== // @name AIIDE -网页AI本地桥接助手 // @namespace http://tampermonkey.net/ // @version 2.0.0 // @description 将AI 网页版的代码块同步到本地文件系统,支持文件写入和命令执行 // @author AIIDE // @match https://gemini.google.com/* // @match https://aistudio.google.com/* // @match https://chat.deepseek.com/* // @match https://www.kimi.com/* // @match https://chatglm.cn/* // @grant GM_xmlhttpRequest // @connect localhost // @connect 127.0.0.1 // @run-at document-idle // ==/UserScript== (function () { "use strict"; // ═══════════════════════════ 配置 ═══════════════════════════ const CONFIG = { serverUrl: "http://localhost:8765", pingInterval: 5000, // 心跳检测间隔 ms observeDebounce: 500, // DOM 监听防抖 ms }; // ═══════════════════════════ 状态 ═══════════════════════════ let isConnected = false; let pingTimer = null; let serverRoot = "(未连接,请先启动服务端)"; let isAutoReply = false; const executedBlocks = new Map(); // 用于持久化记录代码块的执行状态 // ═══════════════════════════ 提示词模板 ═══════════════════════════ const PROMPT_TEXT = `你现在是一个深度集成了本地文件系统的 AI 编程助手。我的电脑上运行着一个桥接程序,可以自动执行你输出的代码。 ⚠️ 【最重要的规则】你输出的每一个操作,都必须包裹在 Markdown 的 \`\`\` 代码块中,绝对不能作为普通文本直接输出!代码块的第一行必须是以下注释之一,没有例外: 1️⃣ 新建/全量写入 → # FILE: 相对路径/文件名 2️⃣ 增量修改 → # PATCH: 相对路径/文件名 支持精准行号替换(极力推荐,修改多行务必写全区间如10-20): <<<<<<< REPLACE 10-20 新代码 >>>>>>> 或旧的搜索替换: <<<<<<< SEARCH 旧代码 ======= 新代码 >>>>>>> REPLACE 3️⃣ 执行命令 → # TERMINAL: 要执行的命令 4️⃣ 读取文件 → # READ: 相对路径/文件名 5️⃣ 状态侦察 → # FILE_STATS: 相对路径/文件名 6️⃣ 搜索代码 → # SEARCH: 关键词 7️⃣ 范围读取 → # READ_RANGE: 相对路径/文件名 起始行号-结束行号 8️⃣ 目录大纲 → # TREE: 相对路径 (可选留空) 9️⃣ 文件大纲 → # OUTLINE: 相对路径/文件名 ✅ 正确示范: 全量写入新文件: \`\`\`python # FILE: src/app.py from flask import Flask app = Flask(__name__) \`\`\` 增量修改已有文件(推荐基于行号精准修改,多处修改必须合并): \`\`\`python # PATCH: src/app.py <<<<<<< REPLACE 10-15 app = Flask(__name__) app.config['DEBUG'] = True >>>>>>> <<<<<<< REPLACE 40-42 def new_function(): pass >>>>>>> \`\`\` ⚠️ 警告: 1. 切勿算错行区间!如果目标函数跨越 10~15 行,务必写 \`REPLACE 10-15\`,否则旧代码会残留重叠! 2. 同一文件的多处修改,必须像上方示范一样合并装在同一个 \`# PATCH\` 代码块内,绝对不要分开发送多个小代码块! 执行命令: \`\`\`bash # TERMINAL: pip install flask \`\`\` 状态侦察: \`\`\`text # FILE_STATS: src/app.py \`\`\` 📌 何时用 FILE vs PATCH: - 新建文件或大幅重写 → 用 # FILE:(全量写入) - 小范围修改已有文件 → 用 # PATCH:(增量修改,更安全高效,配合 # READ_RANGE 获取行号后使用最佳) 🎯 【AI 行动铁律】 1. 严禁盲猜:在用 # PATCH 修改前,必须先用 # OUTLINE 或 # READ_RANGE 确认真实代码和行号,绝对不可凭记忆虚构! 2. 三思后行:写代码前,必须先以普通文本输出【思考过程】,简短规划替换的行号区间,比如“【思考过程】:我要改 formatTime,它在 5~20 行,我用 REPLACE 5-20”。坚决禁止使用带有尖括号的 XML 标签触发敏感拦截! 3. 原样缩进:提供的新代码必须【绝对保持】与原文相同的缩进风格,不要擅自将 tab 改为空格或改变格式化策略。 4. 小步快跑:一次回答最多只发 1~2 个文件的闭环修改,大重构请主动询问我“是否继续”,避免代码块过长截断! 5. 极其严格的串行等待:无论是 # READ、# OUTLINE、# PATCH 还是 # TERMINAL,【严禁】在一次回答中连续输出多个命令块!你每次回答【只能输出唯一一个】操作命令代码块,然后在末尾说“请执行并告诉我结果”。必须严格等待我把上一条命令的结果反馈给你后,再根据真实结果决定下一步操作! 6. 依靠终端自纠错:修改 Java, Vue, Typescript 等需要编译的语言后,请主动请求执行 # TERMINAL (如执行 javac, mvn test 或 npm run build) 来检查你刚才的改动是否产生了语法错误,并根据终端报错主动修复,不要等我来指出! 7. 代码块都是\`\`\`text \`\`\`包裹,不能作为普通文本直接输出! 8. 不要输出emoji表情 当前项目根目录:{ROOT_DIR} 明白以上规则请仔细阅读铁律并回复"已就绪",随时等待我的第一个开发任务。`; // ═══════════════════════════ 样式注入 ═══════════════════════════ const STYLES = ` /* ── 拖拽工具栏 ── */ #aiide-toolbar { position: fixed; bottom: 20px; right: 20px; z-index: 99999; display: flex; align-items: center; gap: 6px; padding: 6px 10px; border-radius: 28px; font-family: 'Google Sans', 'Segoe UI', sans-serif; font-size: 13px; font-weight: 500; color: #fff; background: rgba(24, 24, 30, 0.92); backdrop-filter: blur(14px); box-shadow: 0 4px 24px rgba(0,0,0,0.4); user-select: none; cursor: grab; } #aiide-toolbar:active { cursor: grabbing; } #aiide-toolbar .dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; transition: background 0.3s; } #aiide-toolbar .dot.connected { background: #34d399; box-shadow: 0 0 8px #34d39980; } #aiide-toolbar .dot.disconnected { background: #f87171; box-shadow: 0 0 8px #f8717180; } #aiide-toolbar .dot.connecting { background: #fbbf24; box-shadow: 0 0 8px #fbbf2480; animation: aiide-pulse 1s infinite; } #aiide-toolbar .status-label { font-size: 12px; color: #cbd5e1; white-space: nowrap; margin-right: 4px; } #aiide-toolbar .sep { width: 1px; height: 20px; background: rgba(255,255,255,0.15); flex-shrink: 0; } #aiide-toolbar .tb-btn { background: rgba(255,255,255,0.08); border: none; color: #e2e8f0; font-size: 12px; font-weight: 500; padding: 5px 12px; border-radius: 16px; cursor: pointer; transition: all 0.2s; white-space: nowrap; font-family: inherit; } #aiide-toolbar .tb-btn:hover { background: rgba(255,255,255,0.18); color: #fff; } #aiide-toolbar .tb-btn.active { background: rgba(99,102,241,0.3); color: #a5b4fc; } #aiide-toolbar .badge { background: #6366f1; color: #fff; font-size: 10px; padding: 1px 6px; border-radius: 10px; min-width: 14px; text-align: center; margin-left: 4px; } @keyframes aiide-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } } /* ── 操作按钮(代码块下方) ── */ .aiide-btn { display: inline-flex; align-items: center; gap: 5px; padding: 5px 12px; margin: 4px 4px 4px 0; border: none; border-radius: 6px; font-family: 'Google Sans', 'Segoe UI', sans-serif; font-size: 12px; font-weight: 600; cursor: pointer; transition: all 0.2s ease; color: #fff; } .aiide-btn:hover { transform: translateY(-1px); filter: brightness(1.15); } .aiide-btn:active { transform: translateY(0); } .aiide-btn-write { background: linear-gradient(135deg, #6366f1, #8b5cf6); } .aiide-btn-exec { background: linear-gradient(135deg, #f59e0b, #ef4444); } .aiide-btn-sync { background: linear-gradient(135deg, #10b981, #059669); } .aiide-btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; } .aiide-btn-bar { display: flex; flex-wrap: wrap; padding: 4px 8px; border-top: 1px solid rgba(255,255,255,0.08); background: rgba(0,0,0,0.15); border-radius: 0 0 8px 8px; } /* ── Toast ── */ #aiide-toast-container { position: fixed; top: 20px; right: 20px; z-index: 100000; display: flex; flex-direction: column; gap: 8px; pointer-events: none; } .aiide-toast { padding: 12px 20px; border-radius: 10px; font-family: 'Google Sans', 'Segoe UI', sans-serif; font-size: 13px; font-weight: 500; color: #fff; max-width: 420px; word-break: break-word; box-shadow: 0 4px 20px rgba(0,0,0,0.35); backdrop-filter: blur(10px); animation: aiide-toastIn 0.3s ease forwards; pointer-events: auto; } .aiide-toast.success { background: rgba(16, 185, 129, 0.92); } .aiide-toast.error { background: rgba(239, 68, 68, 0.92); } .aiide-toast.info { background: rgba(99, 102, 241, 0.92); } @keyframes aiide-toastIn { from { opacity: 0; transform: translateX(40px); } to { opacity: 1; transform: translateX(0); } } @keyframes aiide-toastOut { from { opacity: 1; transform: translateX(0); } to { opacity: 0; transform: translateX(40px); } } /* ── 日志面板 ── */ #aiide-log-panel { position: fixed; top: 0; right: -420px; width: 420px; height: 100vh; z-index: 99998; display: flex; flex-direction: column; background: rgba(20, 20, 25, 0.95); backdrop-filter: blur(16px); box-shadow: -4px 0 30px rgba(0,0,0,0.5); transition: right 0.3s ease; font-family: 'Google Sans', 'Segoe UI', monospace; } #aiide-log-panel.open { right: 0; } #aiide-log-panel .panel-header { display: flex; align-items: center; justify-content: space-between; padding: 14px 16px; background: rgba(255,255,255,0.05); border-bottom: 1px solid rgba(255,255,255,0.1); flex-shrink: 0; } #aiide-log-panel .panel-header .title { color: #e2e8f0; font-size: 14px; font-weight: 600; } #aiide-log-panel .panel-header button { background: rgba(255,255,255,0.1); border: none; color: #94a3b8; font-size: 12px; padding: 4px 10px; border-radius: 6px; cursor: pointer; transition: all 0.2s; } #aiide-log-panel .panel-header button:hover { background: rgba(255,255,255,0.2); color: #fff; } #aiide-log-panel .panel-body { flex: 1; overflow-y: auto; padding: 8px; } #aiide-log-panel .panel-body::-webkit-scrollbar { width: 6px; } #aiide-log-panel .panel-body::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.15); border-radius: 3px; } .aiide-log-entry { margin-bottom: 8px; border-radius: 8px; overflow: hidden; border: 1px solid rgba(255,255,255,0.08); } .aiide-log-entry .entry-header { display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; font-size: 12px; font-weight: 600; color: #e2e8f0; } .aiide-log-entry.success .entry-header { background: rgba(16,185,129,0.15); } .aiide-log-entry.error .entry-header { background: rgba(239,68,68,0.15); } .aiide-log-entry.pending .entry-header { background: rgba(251,191,36,0.15); } .aiide-log-entry .entry-header .time { color: #64748b; font-size: 11px; font-weight: 400; } .aiide-log-entry .entry-body { padding: 8px 12px; background: rgba(0,0,0,0.2); color: #a5b4c8; font-size: 12px; font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace; white-space: pre-wrap; word-break: break-all; max-height: 200px; overflow-y: auto; line-height: 1.5; } .aiide-log-entry .entry-actions { display: flex; gap: 4px; padding: 4px 12px 8px; background: rgba(0,0,0,0.2); } .aiide-log-entry .entry-actions button { background: rgba(255,255,255,0.08); border: none; color: #94a3b8; font-size: 11px; padding: 3px 8px; border-radius: 4px; cursor: pointer; transition: all 0.2s; } .aiide-log-entry .entry-actions button:hover { background: rgba(255,255,255,0.18); color: #fff; } `; function injectStyles() { const style = document.createElement("style"); style.textContent = STYLES; document.head.appendChild(style); } // ═══════════════════════════ Toast 通知 ═══════════════════════════ function ensureToastContainer() { let container = document.getElementById("aiide-toast-container"); if (!container) { container = document.createElement("div"); container.id = "aiide-toast-container"; document.body.appendChild(container); } return container; } function showToast(message, type = "info", duration = 4000) { const container = ensureToastContainer(); const toast = document.createElement("div"); toast.className = "aiide-toast " + type; toast.textContent = message; container.appendChild(toast); setTimeout(() => { toast.style.animation = "aiide-toastOut 0.3s ease forwards"; toast.addEventListener("animationend", () => toast.remove()); }, duration); } // ═══════════════════════════ 拖拽工具栏 + 日志面板 ═══════════════════════════ function createToolbar() { // ── 日志面板 ── const panel = document.createElement("div"); panel.id = "aiide-log-panel"; const panelHeader = document.createElement("div"); panelHeader.className = "panel-header"; const panelTitle = document.createElement("span"); panelTitle.className = "title"; panelTitle.textContent = "📋 命令执行记录"; const clearBtn = document.createElement("button"); clearBtn.textContent = "🗑 清空"; clearBtn.addEventListener("click", () => { const b = document.getElementById("aiide-log-body"); if (b) b.textContent = ""; logCount = 0; updateLogBadge(); }); panelHeader.appendChild(panelTitle); panelHeader.appendChild(clearBtn); const panelBody = document.createElement("div"); panelBody.className = "panel-body"; panelBody.id = "aiide-log-body"; panel.appendChild(panelHeader); panel.appendChild(panelBody); document.body.appendChild(panel); // ── 工具栏 ── const toolbar = document.createElement("div"); toolbar.id = "aiide-toolbar"; // 状态指示器 const dot = document.createElement("span"); dot.className = "dot connecting"; dot.id = "aiide-dot"; const statusLabel = document.createElement("span"); statusLabel.className = "status-label"; statusLabel.id = "aiide-status-label"; statusLabel.textContent = "连接中..."; // 分隔线 1 const sep1 = document.createElement("span"); sep1.className = "sep"; // 注入提示词按钮 const promptBtn = document.createElement("button"); promptBtn.className = "tb-btn"; promptBtn.textContent = "📝 注入提示词"; promptBtn.title = "将 IDE 模式提示词填入 Gemini 输入框"; promptBtn.addEventListener("click", (e) => { e.stopPropagation(); injectPrompt(); }); // 分隔线 2 const sep2 = document.createElement("span"); sep2.className = "sep"; // 日志切换按钮 const logBtn = document.createElement("button"); logBtn.className = "tb-btn"; logBtn.id = "aiide-log-btn"; logBtn.title = "打开/关闭命令记录面板"; const logBtnLabel = document.createElement("span"); logBtnLabel.textContent = "📋 命令记录"; const badge = document.createElement("span"); badge.className = "badge"; badge.id = "aiide-log-badge"; badge.textContent = "0"; logBtn.appendChild(logBtnLabel); logBtn.appendChild(badge); logBtn.addEventListener("click", (e) => { e.stopPropagation(); panel.classList.toggle("open"); logBtn.classList.toggle("active"); }); // 分隔线 3 const sep3 = document.createElement("span"); sep3.className = "sep"; // 自动回复切换按钮 const autoBtn = document.createElement("button"); autoBtn.className = "tb-btn"; autoBtn.title = "自动将执行或读取结果填入输入框"; autoBtn.textContent = "🔄 自动回复: 关"; autoBtn.addEventListener("click", (e) => { e.stopPropagation(); isAutoReply = !isAutoReply; if (isAutoReply) { autoBtn.classList.add("active"); autoBtn.textContent = "🔄 自动回复: 开"; } else { autoBtn.classList.remove("active"); autoBtn.textContent = "🔄 自动回复: 关"; } }); // 组装工具栏 toolbar.appendChild(dot); toolbar.appendChild(statusLabel); toolbar.appendChild(sep1); toolbar.appendChild(promptBtn); toolbar.appendChild(sep2); toolbar.appendChild(logBtn); toolbar.appendChild(sep3); toolbar.appendChild(autoBtn); document.body.appendChild(toolbar); // ── 拖拽逻辑 ── let isDragging = false; let dragOffsetX = 0; let dragOffsetY = 0; let hasMoved = false; toolbar.addEventListener("mousedown", (e) => { // 点击按钮时不触发拖拽 if (e.target.tagName === "BUTTON" || e.target.closest(".tb-btn")) return; isDragging = true; hasMoved = false; const rect = toolbar.getBoundingClientRect(); dragOffsetX = e.clientX - rect.left; dragOffsetY = e.clientY - rect.top; e.preventDefault(); }); document.addEventListener("mousemove", (e) => { if (!isDragging) return; hasMoved = true; const x = e.clientX - dragOffsetX; const y = e.clientY - dragOffsetY; toolbar.style.left = x + "px"; toolbar.style.top = y + "px"; toolbar.style.right = "auto"; toolbar.style.bottom = "auto"; }); document.addEventListener("mouseup", () => { isDragging = false; }); } function updateStatus(state) { const dot = document.getElementById("aiide-dot"); const label = document.getElementById("aiide-status-label"); if (!dot || !label) return; dot.className = "dot"; switch (state) { case "connected": dot.classList.add("connected"); label.textContent = "已连接"; isConnected = true; break; case "disconnected": dot.classList.add("disconnected"); label.textContent = "未连接"; isConnected = false; break; case "connecting": dot.classList.add("connecting"); label.textContent = "连接中..."; break; } } // ═══════════════════════════ HTTP 通信 ═══════════════════════════ /** * 使用 GM_xmlhttpRequest 发送请求(绕过 CSP 限制) * 返回 Promise */ function sendRequest(data) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "POST", url: CONFIG.serverUrl + "/api", headers: { "Content-Type": "application/json" }, data: JSON.stringify(data), timeout: 30000, onload: (response) => { try { const result = JSON.parse(response.responseText); resolve(result); } catch (e) { reject(new Error("响应解析失败")); } }, onerror: (err) => { reject(new Error("请求失败,服务端可能未启动")); }, ontimeout: () => { reject(new Error("请求超时")); }, }); }); } /** * 发送消息并显示结果通知 + 记录到日志面板 */ async function sendMessage(data) { if (!isConnected) { showToast("❌ 未连接到本地服务端,请先启动 Python 服务", "error"); return; } // 构造描述文本 let label = ""; if (data.action === "write") label = "写入: " + data.path; else if (data.action === "patch") label = "增量修改: " + data.path + " (" + (data.patches ? data.patches.length : 0) + "处)"; else if (data.action === "exec") label = "执行: " + data.command; else if (data.action === "read") label = "读取: " + data.path; else if (data.action === "read_range") label = "范围读取: " + data.path; else if (data.action === "stats") label = "状态: " + data.path; else if (data.action === "search") label = "搜索: " + data.keyword; else if (data.action === "tree") label = "目录树: " + (data.path || "/"); else if (data.action === "outline") label = "大纲: " + data.path; else label = data.action; // 先添加 pending 日志 const entryId = addLogEntry(label, "⏳ 执行中...", "pending"); try { const result = await sendRequest(data); const type = result.status === "success" ? "success" : "error"; const icon = type === "success" ? "✅" : "❌"; const brief = result.payload.length > 200 ? result.payload.substring(0, 200) + "..." : result.payload; showToast(icon + " " + brief, type, 5000); // 更新日志条目 updateLogEntry(entryId, result.payload, type); // 自动回复功能 if (isAutoReply) { let prefix = ""; if (data.action === "exec") prefix = "执行结果:\n"; else if (data.action === "read") prefix = "读取结果:\n"; else if (data.action === "read_range") prefix = "范围读取结果:\n"; else if (data.action === "stats") prefix = "文件状态:\n"; else if (data.action === "patch") prefix = "增量修改结果:\n"; else if (data.action === "search") prefix = "搜索结果:\n"; else if (data.action === "tree") prefix = "目录树结构:\n"; else if (data.action === "outline") prefix = "文档大纲结构:\n"; else if (data.action === "write") prefix = "全新写入/覆盖结果:\n"; if (prefix) { insertReply(prefix + result.payload); } } } catch (e) { showToast("❌ " + e.message, "error"); updateLogEntry(entryId, e.message, "error"); } } /** * 心跳检测 — 定期 ping 服务端 */ function checkConnection() { updateStatus("connecting"); GM_xmlhttpRequest({ method: "GET", url: CONFIG.serverUrl + "/ping", timeout: 3000, onload: (response) => { try { const data = JSON.parse(response.responseText); if (data.status === "ok") { serverRoot = data.root; updateStatus("connected"); console.log("[AIIDE] 服务端已连接, root:", data.root); } else { updateStatus("disconnected"); } } catch (e) { updateStatus("disconnected"); } }, onerror: () => { updateStatus("disconnected"); console.log("[AIIDE] 服务端未响应"); }, ontimeout: () => { updateStatus("disconnected"); }, }); } function startPingLoop() { checkConnection(); pingTimer = setInterval(checkConnection, CONFIG.pingInterval); } // ═══════════════════════════ 代码块解析 ═══════════════════════════ /** * 解析代码块首行的特殊注释,判断类型 * 返回 { type: "file"|"patch"|"terminal"|"plain", value: string, content: string } */ function parseCodeBlock(text) { const lines = text.split("\n"); const firstLine = lines[0].trim(); // 匹配 # FILE: path const fileMatch = firstLine.match(/^(?:#|\/\/)\s*FILE:\s*(.+)$/i); if (fileMatch) { return { type: "file", value: fileMatch[1].trim(), content: lines.slice(1).join("\n").trim(), }; } // 匹配 # PATCH: path const patchMatch = firstLine.match(/^(?:#|\/\/)\s*PATCH:\s*(.+)$/i); if (patchMatch) { const patchContent = lines.slice(1).join("\n"); const patches = parsePatchBlocks(patchContent); return { type: "patch", value: patchMatch[1].trim(), content: patchContent, patches: patches, }; } // 匹配 # TERMINAL: command const termMatch = firstLine.match(/^(?:#|\/\/)\s*TERMINAL:\s*(.+)$/i); if (termMatch) { return { type: "terminal", value: termMatch[1].trim(), content: text, }; } // 匹配 # FILE_STATS: path const statsMatch = firstLine.match(/^(?:#|\/\/)\s*FILE_STATS:\s*(.+)$/i); if (statsMatch) { return { type: "stats", value: statsMatch[1].trim(), content: text, }; } // 匹配 # READ: path const readMatch = firstLine.match(/^(?:#|\/\/)\s*READ:\s*(.+)$/i); if (readMatch) { return { type: "read", value: readMatch[1].trim(), content: text, }; } // 匹配 # SEARCH: keyword const searchMatch = firstLine.match(/^(?:#|\/\/)\s*SEARCH:\s*(.+)$/i); if (searchMatch) { return { type: "search", value: searchMatch[1].trim(), content: text, }; } // 匹配 # READ_RANGE: path start-end const readRangeMatch = firstLine.match(/^(?:#|\/\/)\s*READ_RANGE:\s*(.+?)\s+(\d+)(?:-(\d+))?$/i); if (readRangeMatch) { return { type: "read_range", value: readRangeMatch[1].trim(), start: parseInt(readRangeMatch[2], 10), end: readRangeMatch[3] ? parseInt(readRangeMatch[3], 10) : null, content: text, }; } // 匹配 # TREE: path const treeMatch = firstLine.match(/^(?:#|\/\/)\s*TREE(?:[:\s]*(.*))?$/i); if (treeMatch) { return { type: "tree", value: (treeMatch[1] || "").trim(), content: text, }; } // 匹配 # OUTLINE: path const outlineMatch = firstLine.match(/^(?:#|\/\/)\s*OUTLINE:\s*(.+)$/i); if (outlineMatch) { return { type: "outline", value: outlineMatch[1].trim(), content: text, }; } return { type: "plain", value: "", content: text }; } /** * 解析 SEARCH/REPLACE 块 * 格式: * <<<<<<< SEARCH * old code * ======= * new code * >>>>>>> REPLACE */ function parsePatchBlocks(text) { const patches = []; // 1. 解析 SEARCH/REPLACE const searchRegex = /<<<<<<< SEARCH\n([\s\S]*?)\n=======\n([\s\S]*?)\n>>>>>>> REPLACE/g; let match; while ((match = searchRegex.exec(text)) !== null) { patches.push({ search: match[1], replace: match[2], }); } // 2. 解析 REPLACE 行号替换 const lineRegex = /<<<<<<< REPLACE\s+(\d+)(?:-(\d+))?\n([\s\S]*?)\n>>>>>>>(?:\s+REPLACE)?/g; while ((match = lineRegex.exec(text)) !== null) { patches.push({ start_line: parseInt(match[1], 10), end_line: parseInt(match[2] || match[1], 10), replace: match[3], }); } return patches; } // ═══════════════════════════ 按钮注入 ═══════════════════════════ function createButton(label, className, onClick) { const btn = document.createElement("button"); btn.className = "aiide-btn " + className; btn.textContent = label; btn.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); onClick(btn); }); return btn; } function injectButtons(preElement) { // 避免重复注入 if (preElement.dataset.aiideProcessed) return; preElement.dataset.aiideProcessed = "true"; const codeEl = preElement.querySelector("code") || preElement; const rawText = codeEl.textContent || ""; if (!rawText.trim()) return; const parsed = parseCodeBlock(rawText); const bar = document.createElement("div"); bar.className = "aiide-btn-bar"; if (parsed.type === "file") { const filePath = parsed.value; bar.appendChild( createButton("📁 写入 " + filePath, "aiide-btn-write", (btn) => { btn.disabled = true; btn.textContent = "⏳ 写入中..."; sendMessage({ action: "write", path: filePath, content: parsed.content }); setTimeout(() => { btn.disabled = false; btn.textContent = "✅ 已写入 " + filePath; btn.style.background = "#059669"; btn.style.borderColor = "#047857"; executedBlocks.set(rawText, { text: btn.textContent, bg: btn.style.background, border: btn.style.borderColor }); }, 1500); }) ); } else if (parsed.type === "patch") { const filePath = parsed.value; const patchCount = parsed.patches ? parsed.patches.length : 0; bar.appendChild( createButton("🔧 增量修改 " + filePath + " (" + patchCount + "处)", "aiide-btn-write", (btn) => { if (!parsed.patches || parsed.patches.length === 0) { showToast("❌ 未解析到 SEARCH/REPLACE 或 REPLACE 行号块", "error"); return; } btn.disabled = true; btn.textContent = "⏳ 修改中..."; sendMessage({ action: "patch", path: filePath, patches: parsed.patches }); setTimeout(() => { btn.disabled = false; btn.textContent = "✅ 已修改 " + filePath + " (" + patchCount + "处)"; btn.style.background = "#059669"; btn.style.borderColor = "#047857"; executedBlocks.set(rawText, { text: btn.textContent, bg: btn.style.background, border: btn.style.borderColor }); }, 1500); }) ); } else if (parsed.type === "terminal") { const cmd = parsed.value; bar.appendChild( createButton("🚀 执行: " + cmd, "aiide-btn-exec", (btn) => { btn.disabled = true; btn.textContent = "⏳ 执行中..."; sendMessage({ action: "exec", command: cmd }); setTimeout(() => { btn.disabled = false; btn.textContent = "✅ 已执行: " + cmd; btn.style.background = "#059669"; executedBlocks.set(rawText, { text: btn.textContent, bg: btn.style.background, border: btn.style.borderColor }); }, 3000); }) ); } else if (parsed.type === "stats") { const filePath = parsed.value; bar.appendChild( createButton("📊 状态侦察: " + filePath, "aiide-btn-sync", (btn) => { btn.disabled = true; btn.textContent = "⏳ 获取中..."; sendMessage({ action: "stats", path: filePath }); setTimeout(() => { btn.disabled = false; btn.textContent = "✅ 已获取: " + filePath; btn.style.background = "#059669"; executedBlocks.set(rawText, { text: btn.textContent, bg: btn.style.background, border: btn.style.borderColor }); }, 2000); }) ); } else if (parsed.type === "read") { const filePath = parsed.value; bar.appendChild( createButton("📖 读取: " + filePath, "aiide-btn-sync", (btn) => { btn.disabled = true; btn.textContent = "⏳ 读取中..."; sendMessage({ action: "read", path: filePath }); setTimeout(() => { btn.disabled = false; btn.textContent = "✅ 已读取: " + filePath; btn.style.background = "#059669"; executedBlocks.set(rawText, { text: btn.textContent, bg: btn.style.background, border: btn.style.borderColor }); }, 2000); }) ); } else if (parsed.type === "search") { const keyword = parsed.value; bar.appendChild( createButton("🔍 搜索: " + keyword, "aiide-btn-sync", (btn) => { btn.disabled = true; btn.textContent = "⏳ 检索中..."; sendMessage({ action: "search", keyword: keyword }); setTimeout(() => { btn.disabled = false; btn.textContent = "✅ 已检索: " + keyword; btn.style.background = "#059669"; executedBlocks.set(rawText, { text: btn.textContent, bg: btn.style.background, border: btn.style.borderColor }); }, 2000); }) ); } else if (parsed.type === "read_range") { const filePath = parsed.value; const start = parsed.start; const end = parsed.end; const rangeStr = end ? `${start}-${end}` : `${start}起`; bar.appendChild( createButton(`📖 范围读取: ${filePath} (${rangeStr}行)`, "aiide-btn-sync", (btn) => { btn.disabled = true; btn.textContent = "⏳ 读取中..."; sendMessage({ action: "read_range", path: filePath, start: start, end: end }); setTimeout(() => { btn.disabled = false; btn.textContent = `✅ 已读取: ${filePath} (${rangeStr}行)`; btn.style.background = "#059669"; }, 2000); }) ); } else if (parsed.type === "tree") { const treePath = parsed.value || ""; bar.appendChild( createButton(`🌲 目录分析: ${treePath || "/"}`, "aiide-btn-sync", (btn) => { btn.disabled = true; btn.textContent = "⏳ 构建中..."; sendMessage({ action: "tree", path: treePath }); setTimeout(() => { btn.disabled = false; btn.textContent = `✅ 已构建: ${treePath || "/"}`; btn.style.background = "#059669"; }, 2000); }) ); } else if (parsed.type === "outline") { const outlinePath = parsed.value; bar.appendChild( createButton(`🏷️ 提取大纲: ${outlinePath}`, "aiide-btn-sync", (btn) => { btn.disabled = true; btn.textContent = "⏳ 扫描中..."; sendMessage({ action: "outline", path: outlinePath }); setTimeout(() => { btn.disabled = false; btn.textContent = `✅ 已提取: ${outlinePath}`; btn.style.background = "#059669"; }, 2000); }) ); } else { bar.appendChild( createButton("📋 同步到本地", "aiide-btn-sync", (btn) => { const filePath = prompt("请输入文件路径(相对于项目根目录):", ""); if (filePath) { btn.disabled = true; btn.textContent = "⏳ 写入中..."; sendMessage({ action: "write", path: filePath, content: parsed.content }); setTimeout(() => { btn.disabled = false; btn.textContent = "📋 同步到本地"; executedBlocks.set(rawText, { text: btn.textContent, bg: btn.style.background, border: btn.style.borderColor }); }, 2000); } }) ); } if (executedBlocks.has(rawText)) { const state = executedBlocks.get(rawText); const btn = bar.querySelector("button"); if (btn) { btn.textContent = state.text; btn.style.background = state.bg; if (state.border) btn.style.borderColor = state.border; } } // 将按钮栏插入到 pre 元素后面 preElement.parentNode.insertBefore(bar, preElement.nextSibling); } // ═══════════════════════════ DOM 监听 ═══════════════════════════ let observeTimer = null; function scanAndInject() { const preBlocks = document.querySelectorAll("pre:not([data-aiide-processed])"); preBlocks.forEach(injectButtons); } function startObserver() { scanAndInject(); const observer = new MutationObserver(() => { if (observeTimer) clearTimeout(observeTimer); observeTimer = setTimeout(scanAndInject, CONFIG.observeDebounce); }); observer.observe(document.body, { childList: true, subtree: true, }); console.log("[AIIDE] DOM 监听器已启动"); } // ═══════════════════════════ 提示词注入 ═══════════════════════════ /** * 查找 Gemini 的输入框元素 */ function findInputBox() { // Gemini 输入框选择器(按优先级排列) const selectors = [ 'div.ql-editor[role="textbox"]', // Gemini 主输入框(Quill 编辑器) 'div.ql-editor.textarea', // Gemini 备选选择器 'div.ql-editor', // Quill 编辑器通用 'div[contenteditable="true"][role="textbox"]', // contenteditable textbox 'rich-textarea div[contenteditable="true"]', // rich-textarea 组件 'div[contenteditable="true"]', // 通用 contenteditable 'textarea', // 普通 textarea '.send-button-container', //kimi 'input[type="file"] + div' //deepseek ]; for (const sel of selectors) { const el = document.querySelector(sel); if (el) return el; } return null; } /** * 向输入框注入提示词 */ function injectPrompt() { const inputEl = findInputBox(); if (!inputEl) return; const finalText = PROMPT_TEXT.replace("{ROOT_DIR}", serverRoot); // 1. 针对 TextArea 或 Input if (inputEl.tagName === "TEXTAREA" || inputEl.tagName === "INPUT") { const lastValue = inputEl.value; inputEl.value = finalText; // 获取原生 setter 并调用,这能绕过 React 的 setter 拦截 const event = new Event('input', { bubbles: true }); const tracker = inputEl._valueTracker; if (tracker) { tracker.setValue(lastValue); } inputEl.dispatchEvent(event); } // 2. 针对 Contenteditable (Qwen 网页版常用) else { inputEl.focus(); // 尝试模拟“粘贴”事件,这是很多富文本框架唯一接受的外部注入方式 const dataTransfer = new DataTransfer(); dataTransfer.setData('text/plain', finalText); const pasteEvent = new ClipboardEvent('paste', { clipboardData: dataTransfer, bubbles: true, cancelable: true }); inputEl.dispatchEvent(pasteEvent); // 如果粘贴事件被拦截没生效,强制触发一个 input 事件 inputEl.innerText = finalText; inputEl.dispatchEvent(new Event('input', { bubbles: true })); } showToast("✅ 注入尝试完成", "success"); } /** * 自动将结果追加到输入框并尝试发送 */ function insertReply(text) { const inputEl = findInputBox(); if (!inputEl) { showToast("❌ 未找到输入框,无法自动填入结果", "error"); return; } const newContent = "```text\n" + text + "\n```"; if (inputEl.tagName === "TEXTAREA" || inputEl.tagName === "INPUT") { const nativeSetter = Object.getOwnPropertyDescriptor( window.HTMLTextAreaElement.prototype, "value" ).set; const current = inputEl.value; nativeSetter.call(inputEl, current + (current ? "\n\n" : "") + newContent); inputEl.dispatchEvent(new Event("input", { bubbles: true })); } else { inputEl.focus(); const lines = ["", "", "```text"].concat(text.split("\n")).concat(["```"]); lines.forEach((line) => { const p = document.createElement("p"); p.textContent = line || "\u200B"; inputEl.appendChild(p); }); inputEl.dispatchEvent(new Event("input", { bubbles: true })); } showToast("✅ 已自动填入执行结果", "success"); // 尝试找出发送按钮并自动点击 setTimeout(() => { const selectors = [ 'button[aria-label="Send message"]', 'button[aria-label="发送消息"]', 'button.send-button', '.send-button', '.send-button-container', //kimi 'input[type="file"] + div > div' //deepseek ]; for (const sel of selectors) { const sendBtn = document.querySelector(sel); if (sendBtn && !sendBtn.disabled) { sendBtn.click(); console.log("[AIIDE] 自动发送完成"); return; } } }, 500); } // ═══════════════════════════ 命令日志面板 ═══════════════════════════ let logCount = 0; let logIdCounter = 0; function createLogPanel() { // 面板容器 const panel = document.createElement("div"); panel.id = "aiide-log-panel"; // 头部 const header = document.createElement("div"); header.className = "panel-header"; const title = document.createElement("span"); title.className = "title"; title.textContent = "📋 命令执行记录"; const clearBtn = document.createElement("button"); clearBtn.textContent = "🗑 清空"; clearBtn.addEventListener("click", () => { const body = document.getElementById("aiide-log-body"); if (body) body.textContent = ""; logCount = 0; updateLogBadge(); }); header.appendChild(title); header.appendChild(clearBtn); // 内容区 const body = document.createElement("div"); body.className = "panel-body"; body.id = "aiide-log-body"; panel.appendChild(header); panel.appendChild(body); document.body.appendChild(panel); // 切换按钮 const toggle = document.createElement("button"); toggle.id = "aiide-log-toggle"; toggle.title = "打开/关闭命令记录面板"; const toggleLabel = document.createElement("span"); toggleLabel.textContent = "📋 命令记录"; const badge = document.createElement("span"); badge.className = "badge"; badge.id = "aiide-log-badge"; badge.textContent = "0"; toggle.appendChild(toggleLabel); toggle.appendChild(badge); toggle.addEventListener("click", () => { panel.classList.toggle("open"); document.body.classList.toggle("aiide-panel-open"); }); document.body.appendChild(toggle); } function updateLogBadge() { const badge = document.getElementById("aiide-log-badge"); if (badge) badge.textContent = String(logCount); } function addLogEntry(label, content, status) { const id = "aiide-log-" + (++logIdCounter); logCount++; updateLogBadge(); const body = document.getElementById("aiide-log-body"); if (!body) return id; const entry = document.createElement("div"); entry.className = "aiide-log-entry " + status; entry.id = id; // 头部:标签 + 时间 const header = document.createElement("div"); header.className = "entry-header"; const labelSpan = document.createElement("span"); labelSpan.textContent = label; const timeSpan = document.createElement("span"); timeSpan.className = "time"; const now = new Date(); timeSpan.textContent = now.getHours().toString().padStart(2, "0") + ":" + now.getMinutes().toString().padStart(2, "0") + ":" + now.getSeconds().toString().padStart(2, "0"); header.appendChild(labelSpan); header.appendChild(timeSpan); // 内容区 const bodyDiv = document.createElement("div"); bodyDiv.className = "entry-body"; bodyDiv.textContent = content; // 操作按钮 const actions = document.createElement("div"); actions.className = "entry-actions"; const copyBtn = document.createElement("button"); copyBtn.textContent = "📋 复制结果"; copyBtn.addEventListener("click", () => { const text = bodyDiv.textContent || ""; navigator.clipboard.writeText(text).then(() => { copyBtn.textContent = "✅ 已复制"; setTimeout(() => { copyBtn.textContent = "📋 复制结果"; }, 1500); }).catch(() => { // fallback const ta = document.createElement("textarea"); ta.value = text; ta.style.position = "fixed"; ta.style.left = "-9999px"; document.body.appendChild(ta); ta.select(); document.execCommand("copy"); document.body.removeChild(ta); copyBtn.textContent = "✅ 已复制"; setTimeout(() => { copyBtn.textContent = "📋 复制结果"; }, 1500); }); }); const copyAllBtn = document.createElement("button"); copyAllBtn.textContent = "📝 复制全部"; copyAllBtn.addEventListener("click", () => { const fullText = "[" + labelSpan.textContent + "]\n" + (bodyDiv.textContent || ""); navigator.clipboard.writeText(fullText).then(() => { copyAllBtn.textContent = "✅ 已复制"; setTimeout(() => { copyAllBtn.textContent = "📝 复制全部"; }, 1500); }); }); actions.appendChild(copyBtn); actions.appendChild(copyAllBtn); entry.appendChild(header); entry.appendChild(bodyDiv); entry.appendChild(actions); body.appendChild(entry); body.scrollTop = body.scrollHeight; return id; } function updateLogEntry(entryId, content, status) { const entry = document.getElementById(entryId); if (!entry) return; entry.className = "aiide-log-entry " + status; const bodyDiv = entry.querySelector(".entry-body"); if (bodyDiv) bodyDiv.textContent = content; } // ═══════════════════════════ 初始化 ═══════════════════════════ function init() { console.log("[AIIDE] 桥接助手 v2.0 启动中..."); injectStyles(); createToolbar(); startPingLoop(); startObserver(); console.log("[AIIDE] 初始化完成 ✨"); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); } })();