// ==UserScript== // @name CodeBridge - Universal 本地桥接助手 // @namespace http://tampermonkey.net/ // @version 4.4.15 // @description 将各大 AI 网页版的代码块同步到本地文件系统,支持文件写入和命令执行 // @author CodeBridge // @match https://gemini.google.com/* // @match https://aistudio.google.com/* // @match https://chat.deepseek.com/* // @match https://www.kimi.com/* // @match https://chatglm.cn/* // @match https://www.doubao.com/* // @match https://doubao.com/* // @match https://www.qianwen.com/* // @match https://claude.ai/* // @grant GM_xmlhttpRequest // @connect localhost // @connect 127.0.0.1 // @run-at document-idle // @license MIT // ==/UserScript== (() => { // adapters/default.js var default_default = { name: "通用", matches: [], findInputBox: () => { const selectors = [ 'textarea[placeholder*="输入"]', 'div[contenteditable="true"]', "textarea" ]; for (const sel of selectors) { const el2 = document.querySelector(sel); if (el2) return el2; } return null; }, findSendButton: () => { const selectors = [ 'button[aria-label="Send message"]', 'button[aria-label="发送消息"]', "button.send-button", ".send-button" ]; for (const sel of selectors) { const btn = document.querySelector(sel); if (btn && !btn.disabled) return btn; } return null; } }; var gemini = { name: "Gemini", matches: ["gemini.google.com", "aistudio.google.com"], findInputBox: () => { const selectors = [ 'div.ql-editor[role="textbox"]', "div.ql-editor.textarea", "div.ql-editor", 'rich-textarea div[contenteditable="true"]' ]; for (const sel of selectors) { const el2 = document.querySelector(sel); if (el2) return el2; } return null; } }; var deepseek = { name: "DeepSeek", matches: ["chat.deepseek.com"], findInputBox: () => document.querySelector('textarea[name="search"]') || document.querySelector("textarea.ds-scroll-area"), findSendButton: () => document.querySelector('div[role="button"].ds-button--circle:not(.ds-button--disabled)'), customInsertReply: (text) => { const el2 = document.querySelector('textarea[name="search"]'); if (!el2) return; const newContent = "```text\n" + text + "\n```"; const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value"); if (nativeSetter && nativeSetter.set) { nativeSetter.set.call(el2, newContent); } else { el2.value = newContent; } el2.dispatchEvent(new Event("input", { bubbles: true })); el2.dispatchEvent(new Event("change", { bubbles: true })); setTimeout(() => { const sendBtn = document.querySelector('div[role="button"].ds-button--circle:not(.ds-button--disabled)'); if (sendBtn) sendBtn.click(); }, 300); } }; var kimi = { name: "Kimi", matches: ["kimi.com"], findInputBox: () => document.querySelector(".chat-input-editor") || document.querySelector('textarea[placeholder*="输入"]'), findSendButton: () => document.querySelector(".send-button-container"), getRootObserverContainer: () => document.body }; var doubao = { name: "Doubao", matches: ["doubao.com"], findInputBox: () => document.querySelector("textarea.semi-input-textarea"), findSendButton: () => { const btn = document.getElementById("flow-end-msg-send"); if (!btn || btn.getAttribute("aria-disabled") === "true") return null; return btn; }, customInsertReply: (text) => { const el2 = document.querySelector("textarea.semi-input-textarea"); if (!el2) return; const newContent = "```text\n" + text + "\n```"; const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value"); if (nativeSetter && nativeSetter.set) { nativeSetter.set.call(el2, newContent); } else { el2.value = newContent; } el2.dispatchEvent(new Event("input", { bubbles: true })); el2.dispatchEvent(new Event("change", { bubbles: true })); setTimeout(() => { const sendBtn = document.getElementById("flow-end-msg-send"); if (sendBtn && sendBtn.getAttribute("aria-disabled") !== "true") sendBtn.click(); }, 300); } }; // adapters/qwen.js function slateInsertText(el2, text) { el2.dispatchEvent(new InputEvent("beforeinput", { inputType: "insertText", data: text, dataTransfer: null, bubbles: true, cancelable: true })); el2.dispatchEvent(new Event("input", { bubbles: true })); } function setCursorAtEnd(el2) { el2.focus(); const sel = window.getSelection(); const range = document.createRange(); const walker = document.createTreeWalker(el2, NodeFilter.SHOW_TEXT, null); let lastTextNode = null, node; while (node = walker.nextNode()) lastTextNode = node; if (lastTextNode) { range.setStart(lastTextNode, lastTextNode.length); } else { range.selectNodeContents(el2); } range.collapse(false); sel.removeAllRanges(); sel.addRange(range); } function selectAllInEditor(el2) { el2.focus(); const sel = window.getSelection(); const range = document.createRange(); range.selectNodeContents(el2); sel.removeAllRanges(); sel.addRange(range); } var qwen = { name: "Qwen", matches: ["qianwen.com"], customInjectPrompt: (text) => { const el2 = document.querySelector('[data-slate-editor="true"][contenteditable="true"]'); if (!el2) return; selectAllInEditor(el2); slateInsertText(el2, text); }, customInsertReply: (text) => { const el2 = document.querySelector('[data-slate-editor="true"][contenteditable="true"]'); if (!el2) return; setCursorAtEnd(el2); const newContent = "\n\n```text\n" + text + "\n```"; slateInsertText(el2, newContent); setTimeout(() => { const sendBtn = document.querySelector('button[aria-label="发送消息"]'); if (sendBtn && !sendBtn.disabled) sendBtn.click(); }, 500); }, findInputBox: () => { return document.querySelector('[data-slate-editor="true"][contenteditable="true"]'); }, findSendButton: () => { const btn = document.querySelector('button[aria-label="发送消息"]'); return btn && !btn.disabled ? btn : null; } }; // adapters/index.js var SITE_ADAPTERS = { "default": default_default, "gemini": gemini, "deepseek": deepseek, "kimi": kimi, "doubao": doubao, "qwen": qwen }; function getCurrentAdapter() { const hostname = window.location.hostname; for (const key in SITE_ADAPTERS) { if (key === "default") continue; const adapter = SITE_ADAPTERS[key]; if (adapter.matches && adapter.matches.some((m) => hostname.includes(m))) { console.log("当前适配器:", adapter); return adapter; } } return SITE_ADAPTERS.default; } // styles.css var styles_default = "/* ── 拖拽工具栏 ── */\n#CodeBridge-toolbar {\n position: fixed;\n bottom: 20px;\n right: 20px;\n z-index: 99999;\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 6px 10px;\n border-radius: 28px;\n font-family: 'Google Sans', 'Segoe UI', sans-serif;\n font-size: 13px;\n font-weight: 500;\n color: #fff;\n background: rgba(24, 24, 30, 0.92);\n backdrop-filter: blur(14px);\n box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);\n user-select: none;\n cursor: grab;\n}\n\n#CodeBridge-toolbar:active {\n cursor: grabbing;\n}\n\n#CodeBridge-toolbar .dot {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n flex-shrink: 0;\n transition: background 0.3s;\n margin: 0 2px;\n}\n\n#CodeBridge-toolbar .dot.connected { background: #34d399; box-shadow: 0 0 6px #34d39980; }\n#CodeBridge-toolbar .dot.disconnected { background: #f87171; box-shadow: 0 0 6px #f8717180; }\n#CodeBridge-toolbar .dot.connecting {\n background: #fbbf24; box-shadow: 0 0 6px #fbbf2480;\n animation: CodeBridge-pulse 1s infinite;\n}\n\n#CodeBridge-toolbar .tb-btn {\n background: rgba(255, 255, 255, 0.08);\n border: none;\n color: #e2e8f0;\n font-size: 14px;\n width: 32px;\n height: 32px;\n padding: 0;\n border-radius: 50%;\n cursor: pointer;\n transition: all 0.2s;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n position: relative;\n}\n\n#CodeBridge-toolbar .tb-btn:hover {\n background: rgba(255, 255, 255, 0.18);\n color: #fff;\n}\n\n#CodeBridge-toolbar .tb-btn.active {\n background: rgba(99, 102, 241, 0.35);\n color: #a5b4fc;\n}\n\n#CodeBridge-toolbar .badge {\n position: absolute;\n top: -2px;\n right: -4px;\n background: #6366f1;\n color: #fff;\n font-size: 9px;\n padding: 1px 4px;\n border-radius: 8px;\n min-width: 12px;\n text-align: center;\n line-height: 1.3;\n}\n\n@keyframes CodeBridge-pulse {\n\n 0%,\n 100% {\n opacity: 1;\n }\n\n 50% {\n opacity: 0.4;\n }\n}\n\n/* ── 操作按钮(代码块下方) ── */\n.CodeBridge-btn {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n padding: 5px 12px;\n margin: 4px 4px 4px 0;\n border: none;\n border-radius: 6px;\n font-family: 'Google Sans', 'Segoe UI', sans-serif;\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n transition: all 0.2s ease;\n color: #fff;\n}\n\n.CodeBridge-btn:hover {\n transform: translateY(-1px);\n filter: brightness(1.15);\n}\n\n.CodeBridge-btn:active {\n transform: translateY(0);\n}\n\n.CodeBridge-btn-write {\n background: linear-gradient(135deg, #6366f1, #8b5cf6);\n}\n\n.CodeBridge-btn-exec {\n background: linear-gradient(135deg, #f59e0b, #ef4444);\n}\n\n.CodeBridge-btn-sync {\n background: linear-gradient(135deg, #10b981, #059669);\n}\n\n.CodeBridge-btn:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n transform: none;\n}\n\n.CodeBridge-btn-bar {\n display: flex;\n flex-wrap: wrap;\n padding: 4px 8px;\n border-top: 1px solid rgba(255, 255, 255, 0.08);\n background: rgba(0, 0, 0, 0.15);\n border-radius: 0 0 8px 8px;\n width: 100%;\n box-sizing: border-box;\n margin-top: 8px;\n margin-bottom: 8px;\n position: relative;\n z-index: 10;\n}\n\n/* ── Toast ── */\n#CodeBridge-toast-container {\n position: fixed;\n top: 20px;\n right: 20px;\n z-index: 100000;\n display: flex;\n flex-direction: column;\n gap: 8px;\n pointer-events: none;\n}\n\n.CodeBridge-toast {\n padding: 12px 20px;\n border-radius: 10px;\n font-family: 'Google Sans', 'Segoe UI', sans-serif;\n font-size: 13px;\n font-weight: 500;\n color: #fff;\n max-width: 420px;\n word-break: break-word;\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.35);\n backdrop-filter: blur(10px);\n animation: CodeBridge-toastIn 0.3s ease forwards;\n pointer-events: auto;\n}\n\n.CodeBridge-toast.success {\n background: rgba(16, 185, 129, 0.92);\n}\n\n.CodeBridge-toast.error {\n background: rgba(239, 68, 68, 0.92);\n}\n\n.CodeBridge-toast.info {\n background: rgba(99, 102, 241, 0.92);\n}\n\n@keyframes CodeBridge-toastIn {\n from {\n opacity: 0;\n transform: translateX(40px);\n }\n\n to {\n opacity: 1;\n transform: translateX(0);\n }\n}\n\n@keyframes CodeBridge-toastOut {\n from {\n opacity: 1;\n transform: translateX(0);\n }\n\n to {\n opacity: 0;\n transform: translateX(40px);\n }\n}\n\n/* ── 日志面板 ── */\n#CodeBridge-log-panel {\n position: fixed;\n top: 0;\n right: -420px;\n width: 420px;\n height: 100vh;\n z-index: 99998;\n display: flex;\n flex-direction: column;\n background: rgba(20, 20, 25, 0.95);\n backdrop-filter: blur(16px);\n box-shadow: -4px 0 30px rgba(0, 0, 0, 0.5);\n transition: right 0.3s ease;\n font-family: 'Google Sans', 'Segoe UI', monospace;\n}\n\n#CodeBridge-log-panel.open {\n right: 0;\n}\n\n#CodeBridge-log-panel .panel-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 14px 16px;\n background: rgba(255, 255, 255, 0.05);\n border-bottom: 1px solid rgba(255, 255, 255, 0.1);\n flex-shrink: 0;\n}\n\n#CodeBridge-log-panel .panel-header .title {\n color: #e2e8f0;\n font-size: 14px;\n font-weight: 600;\n}\n\n#CodeBridge-log-panel .panel-header button {\n background: rgba(255, 255, 255, 0.1);\n border: none;\n color: #94a3b8;\n font-size: 12px;\n padding: 4px 10px;\n border-radius: 6px;\n cursor: pointer;\n transition: all 0.2s;\n}\n\n#CodeBridge-log-panel .panel-header button:hover {\n background: rgba(255, 255, 255, 0.2);\n color: #fff;\n}\n\n#CodeBridge-log-panel .panel-body {\n flex: 1;\n overflow-y: auto;\n padding: 8px;\n}\n\n#CodeBridge-log-panel .panel-body::-webkit-scrollbar {\n width: 6px;\n}\n\n#CodeBridge-log-panel .panel-body::-webkit-scrollbar-thumb {\n background: rgba(255, 255, 255, 0.15);\n border-radius: 3px;\n}\n\n.CodeBridge-log-entry {\n margin-bottom: 8px;\n border-radius: 8px;\n overflow: hidden;\n border: 1px solid rgba(255, 255, 255, 0.08);\n}\n\n.CodeBridge-log-entry .entry-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 8px 12px;\n font-size: 12px;\n font-weight: 600;\n color: #e2e8f0;\n}\n\n.CodeBridge-log-entry.success .entry-header {\n background: rgba(16, 185, 129, 0.15);\n}\n\n.CodeBridge-log-entry.error .entry-header {\n background: rgba(239, 68, 68, 0.15);\n}\n\n.CodeBridge-log-entry.pending .entry-header {\n background: rgba(251, 191, 36, 0.15);\n}\n\n.CodeBridge-log-entry .entry-header .time {\n color: #64748b;\n font-size: 11px;\n font-weight: 400;\n}\n\n.CodeBridge-log-entry .entry-body {\n padding: 8px 12px;\n background: rgba(0, 0, 0, 0.2);\n color: #a5b4c8;\n font-size: 12px;\n font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;\n white-space: pre-wrap;\n word-break: break-all;\n max-height: 200px;\n overflow-y: auto;\n line-height: 1.5;\n}\n\n.CodeBridge-log-entry .entry-actions {\n display: flex;\n gap: 4px;\n padding: 4px 12px 8px;\n background: rgba(0, 0, 0, 0.2);\n}\n\n.CodeBridge-log-entry .entry-actions button {\n background: rgba(255, 255, 255, 0.08);\n border: none;\n color: #94a3b8;\n font-size: 11px;\n padding: 3px 8px;\n border-radius: 4px;\n cursor: pointer;\n transition: all 0.2s;\n}\n\n.CodeBridge-log-entry .entry-actions button:hover {\n background: rgba(255, 255, 255, 0.18);\n color: #fff;\n}\n\n/* ── 目录树面板 ── */\n#CodeBridge-tree-panel {\n position: fixed;\n top: 0;\n left: -340px;\n width: 340px;\n height: 100vh;\n z-index: 99998;\n display: flex;\n flex-direction: column;\n background: rgba(20, 20, 25, 0.95);\n backdrop-filter: blur(16px);\n box-shadow: 4px 0 30px rgba(0, 0, 0, 0.5);\n transition: left 0.3s ease;\n font-family: 'Google Sans', 'Segoe UI', sans-serif;\n}\n\n#CodeBridge-tree-panel.open {\n left: 0;\n}\n\n#CodeBridge-tree-panel .panel-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 14px 16px;\n background: rgba(255, 255, 255, 0.05);\n border-bottom: 1px solid rgba(255, 255, 255, 0.1);\n flex-shrink: 0;\n}\n\n#CodeBridge-tree-panel .panel-header .title {\n color: #e2e8f0;\n font-size: 14px;\n font-weight: 600;\n}\n\n#CodeBridge-tree-panel .panel-header button {\n background: rgba(255, 255, 255, 0.1);\n border: none;\n color: #94a3b8;\n font-size: 14px;\n padding: 4px 10px;\n border-radius: 6px;\n cursor: pointer;\n transition: all 0.2s;\n}\n\n#CodeBridge-tree-panel .panel-header button:hover {\n background: rgba(255, 255, 255, 0.2);\n color: #fff;\n}\n\n#CodeBridge-tree-panel .panel-body {\n flex: 1;\n overflow: auto;\n padding: 6px 0;\n}\n\n#CodeBridge-tree-panel .panel-body::-webkit-scrollbar {\n width: 5px;\n}\n\n#CodeBridge-tree-panel .panel-body::-webkit-scrollbar-thumb {\n background: rgba(255, 255, 255, 0.12);\n border-radius: 3px;\n}\n\n.CodeBridge-tree-loading,\n.CodeBridge-tree-error {\n padding: 12px 16px;\n color: #94a3b8;\n font-size: 12px;\n}\n\n.CodeBridge-tree-error {\n color: #f87171;\n}\n\n.CodeBridge-tree-node {\n user-select: none;\n min-width: max-content;\n}\n\n.CodeBridge-tree-row {\n display: flex;\n align-items: center;\n gap: 4px;\n padding: 4px 12px;\n cursor: default;\n transition: background 0.15s;\n white-space: nowrap;\n}\n\n.CodeBridge-tree-row:hover {\n background: rgba(255, 255, 255, 0.05);\n}\n\n.CodeBridge-tree-toggle {\n width: 7px;\n height: 16px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n font-size: 10px;\n color: #94a3b8;\n cursor: pointer;\n flex-shrink: 0;\n transition: color 0.15s;\n}\n\n.CodeBridge-tree-toggle:hover {\n color: #e2e8f0;\n}\n\n.CodeBridge-tree-icon {\n flex-shrink: 0;\n font-size: 13px;\n}\n\n.CodeBridge-tree-name {\n flex: 1;\n font-size: 12px;\n color: #cbd5e1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n cursor: pointer;\n}\n\n.CodeBridge-tree-name:hover {\n color: #fff;\n}\n\n.CodeBridge-tree-inject-btn {\n flex-shrink: 0;\n background: rgba(255, 255, 255, 0.06);\n border: none;\n color: #64748b;\n font-size: 11px;\n padding: 2px 6px;\n border-radius: 4px;\n cursor: pointer;\n transition: all 0.15s;\n opacity: 0;\n}\n\n.CodeBridge-tree-row:hover .CodeBridge-tree-inject-btn {\n opacity: 1;\n}\n\n.CodeBridge-tree-inject-btn:hover {\n background: rgba(99, 102, 241, 0.25);\n color: #a5b4fc;\n}\n\n.CodeBridge-tree-children {\n /* indented via parent .CodeBridge-tree-node paddingLeft */\n}"; // config.js var CONFIG = { serverUrl: "http://localhost:8765", pingInterval: 5e3, // 心跳检测间隔 ms observeDebounce: 500 // DOM 监听防抖 ms }; var isConnected = false; var pingTimer = null; var serverRoot = "(未连接,请先启动服务端)"; var isAutoReply = false; var executedBlocks = /* @__PURE__ */ new Map(); function setConnected(val) { isConnected = val; } function setPingTimer(val) { pingTimer = val; } function setServerRoot(val) { serverRoot = val; } function setAutoReply(val) { isAutoReply = val; } // html.js var _trustedPolicy; function getPolicy() { if (!_trustedPolicy && window.trustedTypes) { _trustedPolicy = window.trustedTypes.createPolicy("codebridge", { createHTML: (s) => s }); } return _trustedPolicy; } function el(htmlStr) { const t = document.createElement("template"); if (window.trustedTypes) { t.innerHTML = getPolicy().createHTML(htmlStr); } else { t.innerHTML = htmlStr; } return t.content.children.length === 1 ? t.content.children[0] : t.content; } // commands.js var COMMANDS = [ // ── 写入类 ── { type: "write", priority: 1, match: (firstLine) => firstLine.match(/^(?:#|\/\/)\s*FILE:\s*(.+)$/i), parse: (match, lines) => ({ value: match[1].trim(), content: lines.slice(1).join("\n").trim() }), label: (data) => "写入: " + data.path, prefix: "写入结果:\n", button: { icon: "📁", label: (p) => "写入 " + p.value, cls: "CodeBridge-btn-write", timeout: 1500, buildReq: (p) => ({ action: "write", path: p.value, content: p.content }) } }, { type: "replace", priority: 1, match: (firstLine) => { const m = firstLine.match(/^(?:#|\/\/)\s*REPLACE:\s*(.+)$/i); if (!m) return null; const rest = m[1].trim(); return { path: rest.replace(/\s+--all\s*$/, "").trim(), replaceAll: /\s+--all\s*$/.test(rest) }; }, parse: (match, lines) => { const blockContent = lines.slice(1).join("\n"); const { patches, errors } = parseReplaceBlocks(blockContent); return { value: match.path, replaceAll: match.replaceAll, content: blockContent, patches, errors }; }, label: (data) => "替换: " + data.value + " (" + (data.patches ? data.patches.length : 0) + "处)" + (data.replaceAll ? " [全局]" : ""), prefix: "替换结果:\n", button: { icon: "📝", label: (p) => "替换 " + p.value + " (" + (p.patches ? p.patches.length : 0) + "处)" + (p.replaceAll ? " [全局]" : ""), cls: "CodeBridge-btn-write", timeout: 1500, buildReq: (p) => ({ action: "replace", path: p.value, patches: p.patches, replace_all: p.replaceAll || false }) } }, { type: "delete", priority: 1, match: (firstLine) => { const m = firstLine.match(/^(?:#|\/\/)\s*DELETE:\s*(.+)$/i); if (!m) return null; const rest = m[1].trim(); return { path: rest.replace(/\s+--all\s*$/, "").trim(), replaceAll: /\s+--all\s*$/.test(rest) }; }, parse: (match, lines) => { const blockContent = lines.slice(1).join("\n"); const { patches, errors } = parseDeleteBlocks(blockContent); return { value: match.path, replaceAll: match.replaceAll, content: blockContent, patches, errors }; }, label: (data) => "删除: " + data.value + " (" + (data.patches ? data.patches.length : 0) + "处)" + (data.replaceAll ? " [全局]" : ""), prefix: "删除结果:\n", button: { icon: "🗑", label: (p) => "删除 " + p.value + " (" + (p.patches ? p.patches.length : 0) + "处)" + (p.replaceAll ? " [全局]" : ""), cls: "CodeBridge-btn-write", timeout: 1500, buildReq: (p) => ({ action: "delete", path: p.value, patches: p.patches, replace_all: p.replaceAll || false }) } }, { type: "insert", priority: 1, match: (firstLine) => { const m = firstLine.match(/^(?:#|\/\/)\s*INSERT:\s*(.+)$/i); if (!m) return null; return { path: m[1].trim() }; }, parse: (match, lines) => { const blockContent = lines.slice(1).join("\n"); const { patches, errors } = parseInsertBlocks(blockContent); return { value: match.path, content: blockContent, patches, errors }; }, label: (data) => "插入: " + data.value + " (" + (data.patches ? data.patches.length : 0) + "处)", prefix: "插入结果:\n", button: { icon: "➕", label: (p) => "插入 " + p.value + " (" + (p.patches ? p.patches.length : 0) + "处)", cls: "CodeBridge-btn-write", timeout: 1500, buildReq: (p) => ({ action: "insert", path: p.value, patches: p.patches }) } }, { type: "sql", priority: 1, match: (firstLine) => { const m = firstLine.match(/^(?:#|\/\/)\s*SQL:\s*(?:@(\w+)\s*)?$/i); if (!m) return null; return { connection: m[1] || "default" }; }, parse: (match, lines, text) => { const sql = lines.slice(1).join("\n").trim(); return { value: sql, connection: match.connection, content: text }; }, label: (data) => "SQL: " + (data.value || "").replace(/\n/g, " ").substring(0, 50) + (data.connection !== "default" ? " @" + data.connection : ""), prefix: "SQL 执行结果:\n", button: { icon: "🗄️", label: (p) => "SQL: " + (p.value || "").replace(/\n/g, " ").substring(0, 40), cls: "CodeBridge-btn-sync", timeout: 5e3, buildReq: (p) => ({ action: "sql", sql: p.value, connection: p.connection }) } }, { type: "db_list", priority: 4, match: (firstLine) => firstLine.match(/^(?:#|\/\/)\s*DBLIST\s*$/i), parse: () => ({}), label: () => "数据库连接列表", prefix: "数据库连接:\n", button: { icon: "📋", label: () => "数据库连接", cls: "CodeBridge-btn-sync", timeout: 2e3, buildReq: () => ({ action: "db_list" }) } }, { type: "redis", priority: 1, match: (firstLine) => { const m = firstLine.match(/^(?:#|\/\/)\s*REDIS:\s*(?:@(\w+)\s*)?$/i); if (!m) return null; return { connection: m[1] || "default" }; }, parse: (match, lines, text) => { const commands = lines.slice(1).join("\n").trim(); return { value: commands, connection: match.connection, content: text }; }, label: (data) => "Redis: " + (data.value || "").replace(/\n/g, " ").substring(0, 50) + (data.connection !== "default" ? " @" + data.connection : ""), prefix: "Redis 执行结果:\n", button: { icon: "🔴", label: (p) => "Redis: " + (p.value || "").replace(/\n/g, " ").substring(0, 40), cls: "CodeBridge-btn-sync", timeout: 5e3, buildReq: (p) => ({ action: "redis", commands: p.value, connection: p.connection }) } }, { type: "redis_list", priority: 4, match: (firstLine) => firstLine.match(/^(?:#|\/\/)\s*REDISLIST\s*$/i), parse: () => ({}), label: () => "redis连接列表", prefix: "redis连接:\n", button: { icon: "📋", label: () => "redis连接", cls: "CodeBridge-btn-sync", timeout: 2e3, buildReq: () => ({ action: "redis_list" }) } }, { type: "rollback", priority: 1, match: (firstLine) => firstLine.match(/^(?:#|\/\/)\s*ROLLBACK:\s*(.+?)(?:\s+(\d{8}_\d{6}(?:_\d+)?))?\s*$/i), parse: (match, lines, text) => ({ value: match[1].trim(), version: match[2] || null, content: text }), label: (data) => "回滚: " + data.path, prefix: "回滚结果:\n", button: { icon: "↩️", label: (p) => "回滚 " + p.value + (p.version ? " (" + p.version + ")" : ""), cls: "CodeBridge-btn-write", timeout: 1500, buildReq: (p) => ({ action: "rollback", path: p.value, version: p.version }) } }, // ── 执行类 ── { type: "exec", priority: 1, match: (firstLine) => firstLine.match(/^(?:#|\/\/)\s*TERMINAL:\s*(.+)$/i), parse: (match, lines, text) => ({ value: match[1].trim(), content: text }), label: (data) => "执行: " + data.command, prefix: "执行结果:\n", button: { icon: "🚀", label: (p) => "执行: " + p.value, cls: "CodeBridge-btn-exec", timeout: 3e3, buildReq: (p) => ({ action: "exec", command: p.value }) } }, { type: "powershell", priority: 1, match: (firstLine) => firstLine.match(/^(?:#|\/\/)\s*POWERSHELL:\s*(.+)$/i), parse: (match, lines, text) => ({ value: match[1].trim(), content: text }), label: (data) => "PowerShell: " + data.command, prefix: "PowerShell 结果:\n", button: { icon: "🔵", label: (p) => "PowerShell: " + p.value, cls: "CodeBridge-btn-exec", timeout: 3e3, buildReq: (p) => ({ action: "powershell", command: p.value }) } }, // ── 信息类 ── { type: "read_range", priority: 2, match: (firstLine) => firstLine.match(/^(?:#|\/\/)\s*READ_RANGE:\s*(.+?)\s+(\d+)(?:-(\d+))?$/i), parse: (match) => ({ value: match[1].trim(), start: parseInt(match[2], 10), end: match[3] ? parseInt(match[3], 10) : null }), label: (data) => "读取: " + data.path, prefix: "读取结果:\n", button: { icon: "📋", label: (p) => "范围读取: " + p.value, cls: "CodeBridge-btn-sync", timeout: 2e3, buildReq: (p) => ({ action: "read_range", path: p.value, start: p.start, end: p.end }) } }, { type: "read", priority: 3, match: (firstLine) => firstLine.match(/^(?:#|\/\/)\s*READ:\s*(.+)$/i), parse: (match, lines, text) => ({ value: match[1].trim(), content: text }), label: (data) => "读取: " + data.path, prefix: "读取结果:\n", button: { icon: "📋", label: (p) => "读取: " + p.value, cls: "CodeBridge-btn-sync", timeout: 2e3, buildReq: (p) => ({ action: "read", path: p.value }) } }, { type: "search_range", priority: 2, match: (firstLine) => firstLine.match(/^(?:#|\/\/)\s*LOCATE:\s*(.+?)(?:\s+(\d+)(?:-(\d+))?)?\s+(\S+)\s*$/i), parse: (match) => ({ value: match[1].trim(), start: match[2] ? parseInt(match[2], 10) : 1, end: match[3] ? parseInt(match[3], 10) : null, keyword: match[4].trim() }), label: (data) => "定位: " + data.keyword, prefix: "搜索结果:\n", button: { icon: "📋", label: (p) => "精确定位: " + p.value, cls: "CodeBridge-btn-sync", timeout: 2e3, buildReq: (p) => ({ action: "search_range", path: p.value, keyword: p.keyword, start: p.start, end: p.end }) } }, { type: "search", priority: 3, match: (firstLine) => { const m = firstLine.match(/^(?:#|\/\/)\s*SEARCH:\s*(.+)$/i); return m ? { keyword: m[1].trim() } : null; }, parse: (match, lines, text) => { let kw = match.keyword; let mode = "content", headLimit = null; let changed = true; while (changed) { changed = false; const filesM = kw.match(/^--files\s+(?!--)(.*)$/); if (filesM) { mode = "files_with_matches"; kw = filesM[1].trim(); changed = true; } const maxM = kw.match(/^--max\s+(\d+)\s+(?!--)(.*)$/); if (maxM) { headLimit = parseInt(maxM[1]); kw = maxM[2].trim(); changed = true; } } return { keyword: kw.trim(), mode, headLimit, content: text }; }, label: (data) => "搜索: " + data.keyword, prefix: "搜索结果:\n", button: { icon: "📋", label: (p) => "搜索: " + (p.keyword || "/"), cls: "CodeBridge-btn-sync", timeout: 2e3, buildReq: (p) => ({ action: "search", keyword: p.keyword, mode: p.mode || "content", head_limit: p.headLimit }) } }, { type: "tree", priority: 4, match: (firstLine) => firstLine.match(/^(?:#|\/\/)\s*TREE(?:[:\s]*(.*))?$/i), parse: (match) => ({ value: (match[1] || "").trim() }), label: (data) => data.action, prefix: "目录树结构:\n", button: { icon: "📋", label: (p) => "目录分析" + (p.value ? ": " + p.value : ""), cls: "CodeBridge-btn-sync", timeout: 2e3, buildReq: (p) => ({ action: "tree", path: p.value }) } }, { type: "diff", priority: 3, match: (firstLine) => firstLine.match(/^(?:#|\/\/)\s*DIFF:\s*(.*)$/i), parse: (match) => { const v = match[1].trim(); return { value: v === "--staged" ? "" : v || "", staged: v === "--staged" }; }, label: (data) => "差异: " + (data.path || "全部文件"), prefix: "差异比较:\n", button: { icon: "📊", label: (p) => "差异比较" + (p.value ? ": " + p.value : ""), cls: "CodeBridge-btn-sync", timeout: 2e3, buildReq: (p) => ({ action: "diff", path: p.value, staged: p.staged || false }) } }, { type: "outline", priority: 4, match: (firstLine) => firstLine.match(/^(?:#|\/\/)\s*OUTLINE:\s*(.+)$/i), parse: (match) => ({ value: match[1].trim() }), label: (data) => data.action, prefix: "文档大纲结构:\n", button: { icon: "📋", label: (p) => "大纲: " + p.value, cls: "CodeBridge-btn-sync", timeout: 2e3, buildReq: (p) => ({ action: "outline", path: p.value }) } }, { type: "stats", priority: 4, match: (firstLine) => firstLine.match(/^(?:#|\/\/)\s*FILE_STATS:\s*(.+)$/i), parse: (match, lines, text) => ({ value: match[1].trim(), content: text }), label: (data) => "状态: " + data.path, prefix: "文件状态:\n", button: { icon: "📋", label: (p) => "状态侦察: " + p.value, cls: "CodeBridge-btn-sync", timeout: 2e3, buildReq: (p) => ({ action: "stats", path: p.value }) } } ]; COMMANDS.sort((a, b) => a.priority - b.priority); var byType = new Map(COMMANDS.map((c) => [c.type, c])); function getCommand(type) { return byType.get(type); } var _usageCache = null; async function fetchCommandUsages() { if (_usageCache && Object.keys(_usageCache).length > 0) return _usageCache; try { const res = await fetch("http://localhost:8765/command_usages"); _usageCache = await res.json(); } catch (e) { console.warn("[CodeBridge] 拉取 command_usages 失败,使用内置 fallback:", e); _usageCache = {}; } return _usageCache; } async function getCommandUsage(type) { const cache = await fetchCommandUsages(); return cache[type] || `⚠️ 命令格式错误,请检查语法。`; } function normalize(text) { return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/[ \t]+$/gm, ""); } function finalize(patches, errors, text) { if (errors.length > 0) { console.warn("[CodeBridge] 解析错误:\n" + errors.map((e) => ` 第 ${e.line} 行: ${e.msg}`).join("\n")); } if (patches.length === 0) { console.warn("[CodeBridge] 未解析出任何补丁块,原文:", text.substring(0, 200)); } return { patches, errors }; } function parseBlocks(text, { openTags, closeTags }) { const lines = text.split("\n"); const blocks = []; const errors = []; let state = "IDLE"; let current = null; const openPattern = new RegExp(`^<<<<<<<\\s*(${openTags.join("|")})(?:\\s+(.+))?$`); const closePattern = new RegExp(`^>>>>>>>\\s*(${closeTags.join("|")})\\s*$`); for (let i = 0; i < lines.length; i++) { const line = lines[i]; const lineNo = i + 1; if (state === "IDLE") { const m = line.match(openPattern); if (m) { current = { tag: m[1], args: (m[2] || "").trim(), bodyLines: [], startLine: lineNo }; state = "IN_BLOCK"; continue; } if (line.trim() !== "") { errors.push({ line: lineNo, msg: `块外出现非空内容("${line.trim().slice(0, 60)}"),请检查块结构是否正确` }); } continue; } if (state === "IN_BLOCK") { const cm = line.match(closePattern); if (cm) { blocks.push({ tag: current.tag, args: current.args, body: current.bodyLines.join("\n"), startLine: current.startLine }); current = null; state = "IDLE"; continue; } const nestedMatch = line.match(openPattern); if (nestedMatch) { blocks.push({ tag: current.tag, args: current.args, body: current.bodyLines.join("\n"), startLine: current.startLine, _incomplete: true }); errors.push({ line: lineNo, msg: `块内出现新的开始标记,但前一个块(起始于第 ${current.startLine} 行,标签 ${current.tag})未闭合` }); current = { tag: nestedMatch[1], args: (nestedMatch[2] || "").trim(), bodyLines: [], startLine: lineNo }; continue; } current.bodyLines.push(line); } } if (state === "IN_BLOCK") { errors.push({ line: current.startLine, msg: `块起始于第 ${current.startLine} 行(标签 ${current.tag}),但未找到对应的结束标记 >>>>>>>` }); } return { blocks, errors }; } function parseCodeBlock(text) { const lines = text.split("\n"); let firstLineIndex = 0; while (firstLineIndex < lines.length && lines[firstLineIndex].trim() === "") { firstLineIndex++; } if (firstLineIndex >= lines.length) return { type: "plain", value: "", content: text, errors: [] }; const firstLine = lines[firstLineIndex].trim().replace(/\\/g, "/"); const remainingLines = lines.slice(firstLineIndex); for (const cmd of COMMANDS) { const match = cmd.match(firstLine); if (match) { const parsed = cmd.parse(match, remainingLines, text); return { type: cmd.type, errors: [], ...parsed }; } } return { type: "plain", value: "", content: text, errors: [] }; } function parseReplaceBlocks(text) { text = normalize(text); const { blocks, errors } = parseBlocks(text, { openTags: ["SEARCH", "LINES"], closeTags: ["REPLACE"] }); const patches = []; for (const b of blocks) { if (b._incomplete) continue; if (b.tag === "SEARCH") { const sep = b.body.indexOf("\n=======\n"); if (sep === -1) { errors.push({ line: b.startLine, msg: "SEARCH 块内缺少 ======= 分隔线" }); continue; } const search = b.body.slice(0, sep); const replace = b.body.slice(sep + 8); if (!search.trim()) { errors.push({ line: b.startLine, msg: "SEARCH 块的搜索内容不能为空" }); continue; } patches.push({ type: "search", search, replace }); } if (b.tag === "LINES") { const m = b.args.match(/^(\d+)(?:-(\d+))?$/); if (!m) { errors.push({ line: b.startLine, msg: `LINES 参数非法: "${b.args}"` }); continue; } patches.push({ type: "line", start_line: parseInt(m[1], 10), end_line: parseInt(m[2] || m[1], 10), replace: b.body }); } } return finalize(patches, errors, text); } function parseDeleteBlocks(text) { text = normalize(text); const { blocks, errors } = parseBlocks(text, { openTags: ["DELETE", "LINES"], closeTags: ["DELETE"] }); const patches = []; for (const b of blocks) { if (b._incomplete) continue; if (b.tag === "LINES") { const m = b.args.match(/^(\d+)(?:-(\d+))?$/); if (!m) { errors.push({ line: b.startLine, msg: `LINES 参数非法: "${b.args}"` }); continue; } patches.push({ type: "delete_line", start_line: parseInt(m[1], 10), end_line: parseInt(m[2] || m[1], 10) }); } if (b.tag === "DELETE") { if (!b.body.trim()) { errors.push({ line: b.startLine, msg: "DELETE 文本块内容为空" }); continue; } patches.push({ type: "delete_search", search: b.body }); } } return finalize(patches, errors, text); } function parseInsertBlocks(text) { text = normalize(text); const { blocks, errors } = parseBlocks(text, { openTags: ["INSERT"], closeTags: ["INSERT"] }); const patches = []; for (const b of blocks) { if (b._incomplete) continue; const m = b.args.match(/^(AFTER|BEFORE)\s+(\d+)$/i); if (!m) { errors.push({ line: b.startLine, msg: `INSERT 参数非法: "${b.args}"` }); continue; } const type = m[1].toLowerCase() === "after" ? "insert_after" : "insert_before"; const content = b.body.replace(/^\n+/, "").replace(/\n+$/, ""); patches.push({ type, line: parseInt(m[2], 10), content }); } return finalize(patches, errors, text); } // network.js var _insertReply = null; function setInsertReplyRef(fn) { _insertReply = fn; } function insertReply(text) { if (_insertReply) _insertReply(text); } function fetchText(path) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: CONFIG.serverUrl + path, timeout: 5e3, onload: (response) => { if (response.status === 200) { resolve(response.responseText); } else { reject(new Error(response.responseText || `HTTP 错误: ${response.status}`)); } }, onerror: () => reject(new Error("网络请求失败")), ontimeout: () => reject(new Error("请求超时")) }); }); } function fetchApi(data, timeout = 1e4) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "POST", url: CONFIG.serverUrl + "/api", headers: { "Content-Type": "application/json" }, data: JSON.stringify(data), timeout, onload: (response) => { try { resolve(JSON.parse(response.responseText)); } catch (e) { reject(new Error("响应解析失败")); } }, onerror: () => reject(new Error("请求失败,服务端可能未启动")), ontimeout: () => reject(new Error("请求超时")) }); }); } function sendRequest(data) { return fetchApi(data, 3e4); } async function sendMessage(data) { if (!isConnected) { showToast("❌ 未连接到本地服务端,请先启动 Python 服务", "error"); return { status: "error", payload: "未连接到本地服务端" }; } const cmd = getCommand(data.action); const label = cmd && cmd.label ? cmd.label(data) : data.action; 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, 5e3); updateLogEntry(entryId, result.payload, type); if (isAutoReply) { let prefix = ""; if (result.status === "success") { prefix = cmd && cmd.prefix ? cmd.prefix : ""; } else { prefix = "执行报错:\n"; } if (prefix) { insertReply(prefix + result.payload); } } return result; } catch (e) { showToast("❌ " + e.message, "error"); updateLogEntry(entryId, e.message, "error"); if (isAutoReply) { insertReply(`请求失败(${data.action}): ${e.message}`); } return { status: "error", payload: e.message }; } } function checkConnection() { updateStatus("connecting"); GM_xmlhttpRequest({ method: "GET", url: CONFIG.serverUrl + "/ping", timeout: 3e3, onload: (response) => { try { const data = JSON.parse(response.responseText); if (data.status === "ok") { setServerRoot(data.root); setConnected(true); updateStatus("connected"); } else { updateStatus("disconnected"); } } catch (e) { updateStatus("disconnected"); } }, onerror: () => updateStatus("disconnected"), ontimeout: () => updateStatus("disconnected") }); } function startPingLoop() { checkConnection(); setPingTimer(setInterval(checkConnection, CONFIG.pingInterval)); } // ui.js function injectStyles() { const style = document.createElement("style"); style.textContent = styles_default; document.head.appendChild(style); } function escapeHtml(str) { return String(str).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); } function ensureToastContainer() { let c = document.getElementById("CodeBridge-toast-container"); if (!c) { c = document.createElement("div"); c.id = "CodeBridge-toast-container"; document.body.appendChild(c); } return c; } function showToast(message, type = "info", duration = 4e3) { const toast = el(`
${escapeHtml(message)}
`); ensureToastContainer().appendChild(toast); setTimeout(() => { toast.style.animation = "CodeBridge-toastOut 0.3s ease forwards"; toast.addEventListener("animationend", () => toast.remove()); }, duration); } var _injectPrompt = null; var _injectShortPrompt = null; var _treeFillPath = null; function setInjectPromptRef(fn) { _injectPrompt = fn; } function setInjectShortPromptRef(fn) { _injectShortPrompt = fn; } function setTreeFillPathRef(fn) { _treeFillPath = fn; } function createToolbar() { const toolbar = el(`
`); toolbar.querySelector("#CodeBridge-prompt-btn").addEventListener("click", (e) => { e.stopPropagation(); if (_injectPrompt) _injectPrompt(); }); toolbar.querySelector("#CodeBridge-prompt-short-btn").addEventListener("click", (e) => { e.stopPropagation(); if (_injectShortPrompt) _injectShortPrompt(); }); toolbar.querySelector("#CodeBridge-log-btn").addEventListener("click", (e) => { e.stopPropagation(); const panel = document.getElementById("CodeBridge-log-panel"); const logBtn = toolbar.querySelector("#CodeBridge-log-btn"); if (panel) panel.classList.toggle("open"); logBtn.classList.toggle("active"); }); toolbar.querySelector("#CodeBridge-tree-btn").addEventListener("click", (e) => { e.stopPropagation(); const panel = document.getElementById("CodeBridge-tree-panel"); const treeBtn = toolbar.querySelector("#CodeBridge-tree-btn"); if (panel) { panel.classList.toggle("open"); treeBtn.classList.toggle("active"); if (panel.classList.contains("open")) { const body = document.getElementById("CodeBridge-tree-body"); if (body && body.children.length === 0) { loadTreeRoot(body); } } } }); const autoBtn = toolbar.querySelector("#CodeBridge-auto-btn"); autoBtn.addEventListener("click", (e) => { e.stopPropagation(); setAutoReply(!isAutoReply); if (isAutoReply) { autoBtn.classList.add("active"); autoBtn.title = "自动回复:开"; } else { autoBtn.classList.remove("active"); autoBtn.title = "自动回复:关"; } }); document.body.appendChild(toolbar); let isDragging = false, dragOffsetX = 0, dragOffsetY = 0; toolbar.addEventListener("mousedown", (e) => { if (e.target.tagName === "BUTTON" || e.target.closest(".tb-btn")) return; isDragging = true; const rect = toolbar.getBoundingClientRect(); dragOffsetX = e.clientX - rect.left; dragOffsetY = e.clientY - rect.top; e.preventDefault(); }); document.addEventListener("mousemove", (e) => { if (!isDragging) return; toolbar.style.left = e.clientX - dragOffsetX + "px"; toolbar.style.top = e.clientY - dragOffsetY + "px"; toolbar.style.right = "auto"; toolbar.style.bottom = "auto"; }); document.addEventListener("mouseup", () => { isDragging = false; }); } function updateStatus(state) { const dot = document.getElementById("CodeBridge-dot"); if (!dot) return; dot.className = "dot"; const states = { connected: ["connected", "已连接"], disconnected: ["disconnected", "未连接"], connecting: ["connecting", "连接中..."] }; const [cls, text] = states[state] || []; if (cls) { dot.classList.add(cls); dot.title = text; } } var logCount = 0; var logIdCounter = 0; function createLogPanel() { const panel = el(`
📋 命令执行记录
`); panel.querySelector("#CodeBridge-log-clear").addEventListener("click", () => { const body = document.getElementById("CodeBridge-log-body"); if (body) body.replaceChildren(); logCount = 0; const badge = document.getElementById("CodeBridge-log-badge"); if (badge) badge.textContent = "0"; }); document.body.appendChild(panel); } function updateLogEntry(entryId, content, status) { const entry = document.getElementById(entryId); if (!entry) return; entry.className = "CodeBridge-log-entry " + status; const bodyDiv = entry.querySelector(".entry-body"); if (bodyDiv) bodyDiv.textContent = content; } function addLogEntry(label, content, status) { const id = "CodeBridge-log-" + ++logIdCounter; logCount++; const badge = document.getElementById("CodeBridge-log-badge"); if (badge) badge.textContent = String(logCount); const body = document.getElementById("CodeBridge-log-body"); if (!body) return id; const now = /* @__PURE__ */ new Date(); const time = now.getHours().toString().padStart(2, "0") + ":" + now.getMinutes().toString().padStart(2, "0") + ":" + now.getSeconds().toString().padStart(2, "0"); const entry = el(`
${label} ${time}
${content}
`); const copyBtn = entry.querySelector("button"); copyBtn.addEventListener("click", () => { const text = entry.querySelector(".entry-body").textContent; navigator.clipboard.writeText(text).then(() => { copyBtn.textContent = "✅ 已复制"; setTimeout(() => { copyBtn.textContent = "📋 复制结果"; }, 1500); }); }); body.appendChild(entry); body.scrollTop = body.scrollHeight; return id; } function createTreePanel() { const panel = el(`
📁 目录树
`); panel.querySelector("#CodeBridge-tree-close").addEventListener("click", () => { panel.classList.remove("open"); const btn = document.getElementById("CodeBridge-tree-btn"); if (btn) btn.classList.remove("active"); }); panel.querySelector("#CodeBridge-tree-refresh").addEventListener("click", () => { const body = document.getElementById("CodeBridge-tree-body"); if (body) loadTreeRoot(body); }); panel.querySelector("#CodeBridge-tree-preview").addEventListener("click", () => { window.open("http://localhost:8765/preview", "_blank"); }); document.body.appendChild(panel); } async function loadTreeRoot(bodyEl) { bodyEl.replaceChildren(); const loading = document.createElement("div"); loading.className = "CodeBridge-tree-loading"; loading.textContent = "加载中..."; bodyEl.appendChild(loading); try { const result = await fetchApi({ action: "tree_children", path: "", depth: 2 }); bodyEl.replaceChildren(); if (result.status === "success" && Array.isArray(result.payload)) { result.payload.forEach((entry) => renderTreeNode(bodyEl, entry, "", 0)); } else { bodyEl.innerHTML = `
${result.payload || "加载失败"}
`; } } catch (e) { bodyEl.replaceChildren(); bodyEl.innerHTML = `
请求失败: ${e.message}
`; } } function renderTreeNode(container, entry, parentPath, depth) { const rawName = entry.name.replace(/\/$/, ""); const isDir = entry.is_dir; const fullPath = parentPath ? parentPath + "\\" + rawName : rawName; const node = document.createElement("div"); node.className = "CodeBridge-tree-node"; node.style.paddingLeft = depth * 8 + "px"; const row = document.createElement("div"); row.className = "CodeBridge-tree-row"; const toggle = document.createElement("span"); toggle.className = "CodeBridge-tree-toggle"; if (isDir) { toggle.textContent = "▸"; toggle.style.display = "inline-block"; } else { toggle.textContent = ""; toggle.style.display = "none"; } row.appendChild(toggle); const icon = document.createElement("span"); icon.className = "CodeBridge-tree-icon"; icon.textContent = isDir ? "📁" : "📄"; row.appendChild(icon); const name = document.createElement("span"); name.className = "CodeBridge-tree-name"; name.textContent = rawName; name.title = fullPath; name.addEventListener("click", (e) => { e.stopPropagation(); if (_treeFillPath) _treeFillPath(fullPath); }); row.appendChild(name); const injectBtn = document.createElement("button"); injectBtn.className = "CodeBridge-tree-inject-btn"; injectBtn.title = "将路径填入输入框"; injectBtn.textContent = "📋"; injectBtn.addEventListener("click", (e) => { e.stopPropagation(); if (_treeFillPath) _treeFillPath(fullPath); }); row.appendChild(injectBtn); node.appendChild(row); if (isDir) { const children = document.createElement("div"); children.className = "CodeBridge-tree-children"; children.style.display = "none"; node.appendChild(children); let loaded = false; const childDepth = depth + 1; toggle.addEventListener("click", (e) => { e.stopPropagation(); if (children.style.display === "none") { children.style.display = "block"; toggle.textContent = "▾"; icon.textContent = "📂"; if (!loaded) { loaded = true; loadChildren(children, fullPath, childDepth); } } else { children.style.display = "none"; toggle.textContent = "▸"; icon.textContent = "📁"; } }); } container.appendChild(node); } async function loadChildren(container, parentPath, depth) { const loading = document.createElement("div"); loading.className = "CodeBridge-tree-loading"; loading.textContent = "..."; container.appendChild(loading); try { const result = await fetchApi({ action: "tree_children", path: parentPath.replace(/\\/g, "/"), depth: 1 }); container.replaceChildren(); if (result.status === "success" && Array.isArray(result.payload)) { result.payload.forEach((entry) => renderTreeNode(container, entry, parentPath, depth)); } else { container.innerHTML = `
${result.payload || "加载失败"}
`; } } catch (e) { container.replaceChildren(); container.innerHTML = `
请求失败: ${e.message}
`; } } // inject.js var currentAdapter = getCurrentAdapter(); function createButton(label, className, onClick) { const btn = document.createElement("button"); btn.className = "CodeBridge-btn " + className; btn.textContent = label; btn.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); onClick(btn); }); return btn; } function fillInput(text) { if (currentAdapter.customInjectPrompt) { currentAdapter.customInjectPrompt(text); return true; } const inputEl = findInputBox(); if (!inputEl) return false; if (inputEl.tagName === "TEXTAREA" || inputEl.tagName === "INPUT") { const proto = inputEl.tagName === "TEXTAREA" ? window.HTMLTextAreaElement.prototype : window.HTMLInputElement.prototype; const setter = Object.getOwnPropertyDescriptor(proto, "value"); if (setter && setter.set) { setter.set.call(inputEl, text); } else { inputEl.value = text; } inputEl.dispatchEvent(new Event("input", { bubbles: true })); inputEl.dispatchEvent(new Event("change", { bubbles: true })); } else { inputEl.focus(); document.execCommand("selectAll", false, null); document.execCommand("insertText", false, text); inputEl.dispatchEvent(new Event("input", { bubbles: true })); } return true; } function extractCodeText(preElement) { const codeEl = preElement.querySelector("code") || preElement; const clone = codeEl.cloneNode(true); if (currentAdapter.name === "Qwen") { const elements = clone.getElementsByClassName("linenumber"); Array.from(elements).forEach((el2) => el2.remove()); } return clone.textContent || ""; } function injectButtons(preElement) { const rawText = extractCodeText(preElement); if (!rawText.trim()) return; if (!preElement.parentNode) return; let h = 0; for (let i = 0; i < rawText.length; i++) h = (h << 5) - h + rawText.charCodeAt(i) | 0; preElement.dataset.codebridgeHash = String(h); preElement.dataset.codebridgeLen = String(rawText.length); const parsed = parseCodeBlock(rawText); const bar = document.createElement("div"); bar.className = "CodeBridge-btn-bar"; const updateState = (btnText, bgColor, bdColor) => { executedBlocks.set(rawText, { text: btnText, bg: bgColor, border: bdColor }); }; const cmd = getCommand(parsed.type); if (cmd && cmd.button) { const btnCfg = cmd.button; bar.appendChild(createButton(btnCfg.icon + " " + btnCfg.label(parsed), btnCfg.cls, async (btn) => { const originalLabel = btn.textContent; btn.disabled = true; btn.textContent = "⏳ 处理中..."; const hasErrors = parsed.errors && parsed.errors.length > 0; const noPatches = parsed.patches && parsed.patches.length === 0; const isBlockCommand = ["replace", "delete", "insert"].includes(parsed.type); if (isBlockCommand && (hasErrors || noPatches)) { const usage = await getCommandUsage(parsed.type); const detail = hasErrors ? parsed.errors.map((e) => `• 第 ${e.line} 行: ${e.msg}`).join("\n") : "• 未解析出任何有效补丁块"; const replyText = `${usage} 检测到以下问题: ${detail}`; addLogEntry(`格式校验: ${parsed.value || parsed.type}`, replyText, "error"); insertReply2(replyText); btn.disabled = false; btn.style.background = "#dc2626"; btn.textContent = originalLabel; return; } try { const result = await sendMessage(btnCfg.buildReq(parsed)); btn.disabled = false; btn.textContent = originalLabel; if (result && result.status === "success") { btn.style.background = "#059669"; } else { btn.style.background = "#dc2626"; } } catch (e) { btn.disabled = false; btn.textContent = originalLabel; btn.style.background = "#dc2626"; } })); } 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; } } if (bar.children.length > 0) { preElement.parentNode.insertBefore(bar, preElement.nextSibling); } } function findInputBox() { if (currentAdapter.findInputBox) { let el2 = currentAdapter.findInputBox(); if (el2) return el2; } return SITE_ADAPTERS.default.findInputBox(); } function getSendButton() { if (currentAdapter.findSendButton) { let el2 = currentAdapter.findSendButton(); if (el2) return el2; } return SITE_ADAPTERS.default.findSendButton(); } async function injectPrompt() { if (!isConnected) { showToast("❌ 未连接到本地服务端,请先启动 Python 服务", "error"); return; } let promptText = ""; try { showToast("⏳ 正在获取提示词...", "info", 2e3); promptText = await fetchText("/prompt"); } catch (e) { showToast("❌ 获取提示词失败: " + e.message, "error"); return; } const finalText = promptText.replace("{ROOT_DIR}", serverRoot); if (!fillInput(finalText)) { showToast("❌ 未找到输入框,请点选对话区域", "error"); return; } showToast("✅ 提示词已填入输入框", "success"); } async function injectShortPrompt() { if (!isConnected) { showToast("❌ 未连接到本地服务端,请先启动 Python 服务", "error"); return; } let promptText = ""; try { showToast("⏳ 正在获取指令速查...", "info", 2e3); promptText = await fetchText("/prompt_short"); } catch (e) { showToast("❌ 获取指令速查失败: " + e.message, "error"); return; } const finalText = promptText.replace("{ROOT_DIR}", serverRoot); if (!fillInput(finalText)) { showToast("❌ 未找到输入框", "error"); return; } showToast("✅ 指令速查已填入", "success"); setTimeout(() => { const sendBtn = getSendButton(); if (sendBtn) { sendBtn.click(); console.log("[CodeBridge] 指令速查自动发送完成"); } }, 300); } function insertReply2(text) { if (currentAdapter.customInsertReply) { currentAdapter.customInsertReply(text); return; } if (!fillInput("```text\n" + text + "\n```")) { showToast("❌ 未找到输入框,无法自动填入结果", "error"); return; } showToast("✅ 已自动填入执行结果", "success"); setTimeout(() => { const sendBtn = getSendButton(); if (sendBtn) { sendBtn.click(); console.log("[CodeBridge] 自动发送完成"); } }, 500); } setInjectPromptRef(injectPrompt); setInjectShortPromptRef(injectShortPrompt); setInsertReplyRef(insertReply2); setTreeFillPathRef((path) => { if (fillInput(path)) { showToast("✅ 路径已填入: " + path, "success", 2e3); } else { showToast("❌ 未找到输入框", "error"); } }); // observer.js var currentAdapter2 = getCurrentAdapter(); var observeTimer = null; function scanAndInject() { const fresh = document.querySelectorAll("pre:not([data-codebridge-hash])"); fresh.forEach((pre) => injectButtons(pre)); const processed = document.querySelectorAll("pre[data-codebridge-hash]"); processed.forEach((pre) => { const codeEl = pre.querySelector("code") || pre; const newLen = (codeEl.textContent || "").length; const oldLen = parseInt(pre.dataset.codebridgeLen || "0", 10); if (newLen === oldLen) return; const rawText = extractCodeText(pre).trim(); if (!rawText) return; const newHash = hashText(rawText); if (pre.dataset.codebridgeHash === newHash) return; const oldBar = pre.nextElementSibling; if (oldBar && oldBar.classList && oldBar.classList.contains("CodeBridge-btn-bar")) { oldBar.remove(); } injectButtons(pre); }); } function hashText(s) { let h = 0; for (let i = 0; i < s.length; i++) { h = (h << 5) - h + s.charCodeAt(i) | 0; } return String(h); } function startObserver() { scanAndInject(); const observer = new MutationObserver((mutations) => { const relevant = mutations.some((m) => { if (m.target.id && m.target.id.startsWith("CodeBridge-")) return false; if (m.target.classList && m.target.classList.contains("CodeBridge-btn-bar")) return false; for (const node of m.addedNodes) { if (node.id && node.id.startsWith("CodeBridge-")) return false; if (node.classList && (node.classList.contains("CodeBridge-btn-bar") || node.classList.contains("CodeBridge-btn"))) return false; } return true; }); if (!relevant) return; if (observeTimer) clearTimeout(observeTimer); observeTimer = setTimeout(scanAndInject, CONFIG.observeDebounce); }); let root = document.body; if (currentAdapter2.getRootObserverContainer) { const potentialRoot = currentAdapter2.getRootObserverContainer(); if (potentialRoot) root = potentialRoot; } observer.observe(root, { childList: true, subtree: true }); console.log("[CodeBridge] DOM 监听器已启动"); } // core.js var currentAdapter3 = getCurrentAdapter(); function init() { console.log(`[CodeBridge] 通用桥接助手 v3.0 启动中... (适配器: ${currentAdapter3.name})`); if (currentAdapter3.onInit) { currentAdapter3.onInit(); } injectStyles(); createLogPanel(); createTreePanel(); createToolbar(); startPingLoop(); startObserver(); fetchCommandUsages(); console.log("[CodeBridge] 初始化完成 ✨"); } // index.js (function() { "use strict"; init(); })(); })();