// ==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(`