// ==UserScript== // @name 抖音火花助手 // @namespace http://tampermonkey.net/ // @version 1.0.5 // @description 自动抓取聊天列表到暂存,支持将对象添加为续火花目标、每对象模板、$date/$targetName/$sinceDate()、简单条件语句。参考 fire.js 的选择器与发送逻辑。 // @author WorldMargin // @match https://creator.douyin.com/creator-micro/data/following/chat // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @grant GM_notification // @grant GM_xmlhttpRequest // @grant GM_getResourceText // @grant GM_addStyle // @homepage https://github.com/WorldMargin2/DouYinFireTool // @source https://github.com/WorldMargin2/DouYinFireTool/抖音火花助手.js // ==/UserScript== (function() { 'use strict'; const DEFAULT_TEMPLATE='res= \`自动续火花-$date\n$targetName\`'; // 创建命名空间 window.DyFireScript = window.DyFireScript || {}; // 预处理变量函数,用于替换编辑器中的变量 function preprocessVariables(code, targetName) { let processedCode = code; // 替换$targetName为实际目标名称 processedCode = processedCode.replace(/\$targetName/g, `${targetName}`); // 替换$date为当前日期 processedCode = processedCode.replace(/\$date/g, `${new Date().toLocaleDateString()}`); // 处理$sinceDate函数,将其转换为实际的天数 processedCode = processedCode.replace(/\$sinceDate\(\s*["\']([^"\']+)["\']\s*\)/g, (_, dateStr) => { const days = daysSince(dateStr); return days; }); return processedCode; } // 计算天数差 function daysSince(dateStr) { try { const d = new Date(dateStr); if (isNaN(d)) return 0; const now = new Date(); const diff = now - d; return Math.floor(diff / (1000 * 60 * 60 * 24)); } catch (e) { return 0; } } // 存储键 const KEY_PERSIST = 'dy_fire_persistent_targets_v1'; const KEY_MACROS = 'dy_fire_macros_v1'; const SELECTORS = { userName: '.item-header-name-vL_79m', chatInput: '.chat-input-dccKiL', sendBtn: '.chat-btn', }; // 内存数据 let staged = []; // 暂存数组 of {name} let persistent = {}; // { name: { template: string, macros: [], lastSendDate: string } } let activeEdit = null; // 当前编辑对象名 let selectedSet = new Set(); // 选中用于批量发送的名字 let macros = {}; // { name: { code: string, enabled: boolean, description: string } } const KEY_SETTINGS = 'dy_fire_settings_v1'; let settings = { schedulerTime: '', // 'HH:MM' sendIntervalSec: 3, autoEnabled: false, sendMode: 'scheduled', // 'scheduled' or 'automatic' theme: 'dark' }; let schedulerTimer = null; let lastScheduledRun = ''; function loadPersistent() { const raw = GM_getValue(KEY_PERSIST, '{}'); try { persistent = typeof raw === 'string' ? JSON.parse(raw) : raw; // Ensure all templates have the macros array and lastSendDate for backward compatibility for (const [name, templateData] of Object.entries(persistent)) { if (!templateData.macros) { templateData.macros = []; } if (!templateData.lastSendDate) { templateData.lastSendDate = ''; } } } catch (e) { persistent = {}; } } // 注入样式表(一次) function injectStyles() { if (document.getElementById('dy-fire-styles')) return; let css = ` .dy-panel { position: fixed; z-index: 9999; font-family: Microsoft YaHei; } .dy-panel .dy-root { width: 540px; background: linear-gradient(180deg,#1c1c22, #141418); color: #fff; border-radius:12px; padding:14px; box-shadow: 0 20px 50px rgba(0,0,0,0.6); position:relative } .dy-panel.dy-theme-light .dy-root { background: linear-gradient(180deg,#ffffff,#f3f4f6); color:#111 } .dy-panel .dy-header{ display:flex; justify-content:space-between; align-items:center; margin-bottom:8px } .dy-panel .dy-header strong{ font-size:16px } .dy-panel .dy-controls{ display:flex; gap:8px; align-items:center } .dy-panel .dy-body{ display:flex; gap:10px; flex-wrap:wrap } .dy-panel .dy-column{ flex:1; background:rgba(255,255,255,0.03); padding:8px; border-radius:6px; max-height:300px; overflow:visible; min-width:220px; max-width:calc(50% - 10px) } /* 面板自适应窗口,防止整体溢出 */ .dy-panel .dy-root{ max-width: calc(100vw - 40px); max-height: calc(100vh - 40px); box-sizing: border-box; overflow:auto } @media (max-width: 640px) { .dy-panel .dy-body{ flex-direction:column } .dy-panel .dy-column{ max-width:100% } .dy-panel .dy-controls{ flex-wrap:wrap } } .dy-panel .dy-title{ font-size:12px; color:#bbb; margin-bottom:6px } .dy-panel .dy-select-all{ margin-bottom:6px } .dy-panel .dy-btn{ background: linear-gradient(90deg,#ff6b8b,#ff2c54); border:none; color:#fff; padding:6px 8px; height:30px; line-height:18px; border-radius:8px; cursor:pointer; font-size:13px; box-shadow:0 6px 18px rgba(255,44,84,0.12); } .dy-panel .dy-btn-light{ background: linear-gradient(90deg,#4b5563,#374151); color:#fff } .dy-panel .dy-btn-add{ background: linear-gradient(90deg,#2dd4bf,#06b6d4); } .dy-panel .dy-btn-send{ background: linear-gradient(90deg,#10b981,#059669); } .dy-panel .dy-btn-remove{ background: linear-gradient(90deg,#f97316,#ef4444); } .dy-panel .dy-btn-macro{ background: linear-gradient(90deg,#8b5cf6,#7c3aed); } .dy-panel input, .dy-panel textarea{ background:#0f1114; border:1px solid rgba(255,255,255,0.06); color:#e6eef8; padding:6px 8px; border-radius:6px; font-size:13px } .dy-panel .dy-list{ padding:6px; margin:0; list-style:none; max-height:40vh; overflow:auto; border-top:1px solid rgba(255,255,255,0.04); } /* 底部设置横向占满(避免全局竖向滚动) */ .dy-panel .dy-settings-bottom{ width:100%; display:flex; gap:10px; flex-wrap:wrap; align-items:flex-start; justify-content:flex-start; padding:6px 6px; border-top:1px solid rgba(255,255,255,0.03); box-sizing:border-box } .dy-panel .dy-settings-bottom .dy-settings-row{ display:flex; gap:8px; align-items:center; flex:0 0 auto; height:30px } .dy-panel .dy-settings-bottom label{ font-size:12px; min-width:80px } .dy-panel .dy-settings-bottom input[type=time], .dy-panel .dy-settings-bottom input[type=number]{ height:26px; padding:2px 6px; font-size:13px } .dy-panel .dy-settings-bottom .dy-btn{ padding:4px 8px; height:28px; font-size:13px } .dy-panel .dy-settings-bottom .dy-status{ font-size:12px; color:inherit } @media (max-width:640px){ .dy-panel .dy-settings-bottom{ flex-direction:column; align-items:stretch } .dy-panel .dy-settings-bottom .dy-settings-row{ width:100%; justify-content:space-between } } .dy-panel .dy-item{ display:block; padding:8px 6px; border-radius:6px; margin-bottom:8px; background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(0,0,0,0.02)); border:1px solid rgba(255,255,255,0.03); } .dy-panel .dy-item + .dy-item{ margin-top:6px } .dy-panel .dy-item .dy-item-top{ display:block; font-size:13px; color:#e6eef8; white-space:normal; overflow:hidden; text-overflow:ellipsis; max-height:3.2em } .dy-panel.dy-theme-light .dy-item .dy-item-top{ color:#111 } .dy-panel .dy-item-name{ color:inherit } .dy-panel.dy-theme-light .dy-item-name{ color:#111 } .dy-panel .dy-item .dy-item-actions{ display:flex; gap:6px; margin-top:8px; justify-content:flex-end } .dy-panel .dy-item label{ display:inline-flex; align-items:center; gap:8px } .dy-panel .dy-resizer{ width:14px;height:14px; position:absolute; right:6px; bottom:6px; cursor:se-resize; border-radius:3px; background:linear-gradient(135deg, rgba(255,255,255,0.06), rgba(0,0,0,0.06)); } .dy-panel .dy-template-editor{ margin-top:8px; background:rgba(0,0,0,0.15); padding:8px; border-radius:6px } .dy-panel .dy-tpl-desc{ font-size:12px; color:#ddd; margin-bottom:6px } /* 宏管理面板样式 */ .dy-panel .dy-macro-panel { display: none; } .dy-panel .dy-macro-panel.active { display: block; } .dy-panel .dy-macro-body { display: flex; gap: 10px; } .dy-panel .dy-macro-column { flex: 1; background: rgba(255,255,255,0.03); padding: 8px; border-radius: 6px; max-height: 400px; overflow: auto; min-width: 250px; } .dy-panel .dy-macro-column.manage-macros { border-right: 2px solid rgba(255,255,255,0.1); } .dy-panel .dy-macro-column.apply-macros { border-left: 2px solid rgba(255,255,255,0.1); } .dy-panel .dy-macro-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; } .dy-panel .dy-macro-title { font-size: 14px; font-weight: bold; color: #e6eef8; } .dy-panel .dy-macro-item { padding: 8px; margin-bottom: 6px; background: rgba(0,0,0,0.2); border-radius: 4px; border: 1px solid rgba(255,255,255,0.1); } .dy-panel .dy-macro-item.enabled { border-left: 3px solid #10b981; } .dy-panel .dy-macro-item.disabled { border-left: 3px solid #ef4444; opacity: 0.7; } .dy-panel .dy-macro-item-name { font-weight: bold; margin-bottom: 4px; } .dy-panel .dy-macro-item-desc { font-size: 12px; color: #aaa; margin-bottom: 6px; } .dy-panel .dy-macro-item-code { font-family: monospace; font-size: 11px; background: rgba(0,0,0,0.3); padding: 4px; border-radius: 3px; overflow: auto; max-height: 60px; } .dy-panel .dy-macro-actions { display: flex; gap: 4px; margin-top: 6px; } .dy-panel .dy-macro-toggle { padding: 4px 6px; font-size: 11px; } .dy-panel .dy-macro-edit { padding: 4px 6px; font-size: 11px; } .dy-panel .dy-macro-delete { padding: 4px 6px; font-size: 11px; } .dy-panel .dy-macro-form { margin-top: 10px; padding: 8px; background: rgba(0,0,0,0.2); border-radius: 6px; } .dy-panel .dy-macro-form input, .dy-panel .dy-macro-form textarea { width: 100%; box-sizing: border-box; margin-bottom: 6px; } .dy-panel .dy-macro-form textarea { min-height: 80px; } .dy-panel .dy-macro-form-buttons { text-align: right; } .dy-panel .dy-macro-select { width: 100%; padding: 6px; border-radius: 6px; background: #0f1114; border: 1px solid rgba(255,255,255,0.06); color: #e6eef8; } .dy-panel .dy-macro-assign-btn { background: linear-gradient(90deg,#8b5cf6,#7c3aed); width: 100%; margin-top: 4px; } .dy-panel .dy-macro-clear-btn { background: linear-gradient(90deg,#f97316,#ef4444); width: 100%; margin-top: 4px; } /* 模态模板编辑器 */ #dy-template-modal { position: fixed; left: 0; top: 0; right: 0; bottom: 0; display: none; z-index: 10000; } #dy-template-modal .dy-tpl-overlay { position: absolute; left:0;top:0;right:0;bottom:0; background: rgba(0,0,0,0.45); display:flex; align-items:center; justify-content:center; padding:20px; transition: opacity 200ms ease; } #dy-template-modal .dy-tpl-box { width: min(900px, 96%); background: linear-gradient(180deg,#0f1114,#09090a); color:#fff; border-radius:10px; padding:12px; box-shadow:0 14px 50px rgba(0,0,0,0.6); max-height:90vh; overflow:auto; transition: all 0.3s ease; } #dy-template-modal .dy-tpl-box.dy-fullscreen { width: 100%; height: 100%; max-width: 100%; max-height: 100%; border-radius: 0; display: flex; flex-direction: column; } #dy-template-modal .dy-tpl-box.dy-fullscreen .dy-tpl-box-body { flex: 1; display: flex; flex-direction: column; overflow: hidden; } #dy-template-modal .dy-tpl-box.dy-fullscreen .monaco-container { flex: 1; min-height: 200px; } #dy-template-modal .dy-tpl-box.dy-fullscreen .dy-tpl-preview { max-height: 150px; } #dy-template-modal .dy-tpl-box.dy-fullscreen .dy-tpl-box-foot { position: sticky; bottom: 0; background: inherit; padding-top: 10px; border-top: 1px solid rgba(255,255,255,0.1); } #dy-template-modal .dy-tpl-box-controls { display: flex; gap: 8px; } #dy-template-modal .dy-tpl-fullscreen { font-size: 14px; padding: 6px 10px; } #dy-template-modal.dy-theme-light .dy-tpl-box { background: #fff; color:#111 } #dy-template-modal .dy-tpl-box-header{ display:flex; justify-content:space-between; align-items:center; margin-bottom:8px } #dy-template-modal textarea{ width:100%; box-sizing:border-box; min-height:120px; max-height:60vh; resize:vertical; padding:8px; border-radius:6px; font-size:13px; font-family: Consolas, "Courier New", monospace } #dy-template-modal .dy-tpl-box-body{ margin-bottom:8px } #dy-template-modal .dy-tpl-box-foot{ text-align:right } #dy-template-modal .dy-tpl-desc{ font-size:13px; color: #cfd8e3 } #dy-template-modal .dy-tpl-syntax{ display:flex; gap:8px; flex-wrap:wrap; margin:8px 0 6px } #dy-template-modal .dy-tpl-syntax button{ background: rgba(255,255,255,0.03); border:1px solid rgba(255,255,255,0.04); color:inherit; padding:6px 8px; border-radius:6px; cursor:pointer; font-size:13px } #dy-template-modal.dy-theme-light .dy-tpl-syntax button{ background: rgba(0,0,0,0.03); border:1px solid rgba(0,0,0,0.06) } #dy-template-modal .dy-tpl-preview{ margin-top:8px; padding:8px; background: rgba(0,0,0,0.06); border-radius:6px; font-family: Consolas, "Courier New", monospace; font-size:13px; color:#e6eef8; max-height:160px; overflow:auto } #dy-template-modal.dy-theme-light .dy-tpl-preview{ background:#f6f7f9; color:#111 } /* 设置定时发送等控件响应换行,避免溢出 */ .dy-panel .dy-settings{ display:flex; flex-direction:column; gap:8px; padding-top:6px } .dy-panel .dy-settings-row{ display:flex; gap:8px; align-items:center; flex-wrap:wrap; margin-top:6px } .dy-panel .dy-settings-row label{ min-width:120px; flex: 0 0 auto } .dy-panel .dy-settings-row input[type=time], .dy-panel .dy-settings-row input[type=number]{ flex: 0 0 auto; min-width:60px; max-width:160px } .dy-panel .dy-settings-row .dy-status{ flex:1 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis; } .dy-panel .dy-settings-row .dy-btn{ flex:0 0 auto; white-space:nowrap } .dy-panel .dy-settings{ max-width:100%; box-sizing:border-box; } .dy-panel.dy-theme-light .dy-template-editor{ background: rgba(0,0,0,0.04); } .dy-panel.dy-theme-light input, .dy-panel.dy-theme-light textarea{ background: #fff; border:1px solid rgba(0,0,0,0.08); color:#111 } .dy-panel.dy-theme-light .dy-btn{ color:#fff } /* 最小化时隐藏主体和 resizer,避免黑色矩形 */ .dy-panel .dy-root.dy-minimized { height: auto; overflow: visible; background: transparent; box-shadow: none; padding:6px 10px } .dy-panel .dy-root.dy-minimized .dy-body, .dy-panel .dy-root.dy-minimized .dy-template-editor { display: none } .dy-panel .dy-root.dy-minimized .dy-resizer { display: none } /* 将列表与其它区域视觉切割 */ .dy-panel .dy-column{ box-shadow: inset 0 1px 0 rgba(255,255,255,0.02); } /* CodeMirror 占位符高亮 */ .cm-placeholder { background: rgba(255,235,59,0.08); color:#ffd54f; padding:0 2px; border-radius:3px } .dy-panel.dy-theme-light .cm-placeholder { background: rgba(16,24,32,0.04); color:#b45309 } `; // VSCode-like CodeMirror theme (dark/light) — minimal rules to mimic VSCode appearance css += ` /* CodeMirror VSCode dark theme approximation */ .cm-s-dy-vscode-dark .CodeMirror { background: #1e1e1e; color: #d4d4d4; font-family: Consolas, 'Courier New', monospace } .cm-s-dy-vscode-dark .CodeMirror-gutters { background: #252526; border-right: 1px solid #2a2a2a } .cm-s-dy-vscode-dark .CodeMirror-linenumber { color: #858585 } .cm-s-dy-vscode-dark .CodeMirror-selected { background: rgba(128, 203, 255, 0.12) } .cm-s-dy-vscode-dark .cm-placeholder { background: rgba(255,235,59,0.06); color:#ffd54f } .cm-s-dy-vscode-dark .cm-keyword { color: #569cd6 } .cm-s-dy-vscode-dark .cm-comment { color: #6a9955 } .cm-s-dy-vscode-dark .cm-string { color: #ce9178 } .n.cm-s-dy-vscode-dark .CodeMirror-cursor { border-left: 1px solid #aeafad } /* Light variant */ .cm-s-dy-vscode-light .CodeMirror { background: #ffffff; color: #333333; font-family: Consolas, 'Courier New', monospace } .cm-s-dy-vscode-light .CodeMirror-gutters { background: #f3f3f3; border-right: 1px solid #e1e1e1 } .cm-s-dy-vscode-light .CodeMirror-linenumber { color: #888888 } .cm-s-dy-vscode-light .CodeMirror-selected { background: rgba(10, 132, 255, 0.08) } .cm-s-dy-vscode-light .cm-placeholder { background: rgba(16,24,32,0.04); color:#b45309 } .cm-s-dy-vscode-light .cm-keyword { color: #0000ff } .cm-s-dy-vscode-light .cm-comment { color: #008000 } .cm-s-dy-vscode-light .cm-string { color: #a31515 } `; const style = document.createElement('style'); style.id = 'dy-fire-styles'; style.innerHTML = css; document.head.appendChild(style); } // 允许拖动面板 function makeDraggable(panel) { const root = panel.querySelector('.dy-root'); if (!root) return; const header = root.querySelector('.dy-header'); if (!header) return; let dragging = false, offsetX = 0, offsetY = 0; header.style.cursor = 'move'; header.addEventListener('mousedown', (e) => { dragging = true; const rect = panel.getBoundingClientRect(); offsetX = e.clientX - rect.left; offsetY = e.clientY - rect.top; document.body.style.userSelect = 'none'; }); document.addEventListener('mousemove', (e) => { if (!dragging) return; let x = e.clientX - offsetX; let y = e.clientY - offsetY; x = Math.max(0, Math.min(x, window.innerWidth - panel.offsetWidth)); y = Math.max(0, Math.min(y, window.innerHeight - panel.offsetHeight)); panel.style.left = x + 'px'; panel.style.top = y + 'px'; panel.style.right = 'auto'; // 实时保存位置(节流) savePanelPositionThrottled(panel, x, y); }); document.addEventListener('mouseup', () => { dragging = false; document.body.style.userSelect = ''; }); } // 可调整大小 function makeResizable(panel) { let resizer = panel.querySelector('.dy-resizer'); if (!resizer) { resizer = document.createElement('div'); resizer.className = 'dy-resizer'; panel.appendChild(resizer); } let resizing = false, startW = 0, startH = 0, startX = 0, startY = 0; resizer.addEventListener('mousedown', (e) => { resizing = true; const rect = panel.getBoundingClientRect(); startW = rect.width; startH = rect.height; startX = e.clientX; startY = e.clientY; document.body.style.userSelect = 'none'; e.preventDefault(); }); document.addEventListener('mousemove', (e) => { if (!resizing) return; const dx = e.clientX - startX; const dy = e.clientY - startY; const newW = Math.max(320, Math.min(window.innerWidth - 40, startW + dx)); const newH = Math.max(160, Math.min(window.innerHeight - 40, startH + dy)); panel.style.width = newW + 'px'; panel.style.height = newH + 'px'; }); document.addEventListener('mouseup', () => { resizing = false; document.body.style.userSelect = ''; // 保存尺寸 try { const rect = panel.getBoundingClientRect(); settings.panel = settings.panel || {}; settings.panel.width = Math.round(Math.min(rect.width, window.innerWidth - 40)); settings.panel.height = Math.round(Math.min(rect.height, window.innerHeight - 40)); saveSettings(); } catch (e) {} }); } function toggleTheme() { // toggle between dark and light; if using vscode alias, preserve it with vscode-light/vscode-dark if (settings.theme === 'light' || settings.theme === 'vscode-light') settings.theme = 'dark'; else if (settings.theme === 'vscode-dark') settings.theme = 'light'; else settings.theme = settings.theme === 'dark' ? 'light' : 'dark'; saveSettings(); const panel = document.getElementById('dy-fire-new-panel'); if (panel) { if (settings.theme === 'light' || settings.theme === 'vscode-light') panel.classList.add('dy-theme-light'); else panel.classList.remove('dy-theme-light'); } } // 保存位置节流 let _savePosTimer = null; function savePanelPositionThrottled(panel, left, top) { if (_savePosTimer) clearTimeout(_savePosTimer); _savePosTimer = setTimeout(() => { settings.panel = settings.panel || {}; settings.panel.left = Math.round(left); settings.panel.top = Math.round(top); saveSettings(); }, 300); } function toggleMinimize(panel) { const root = panel.querySelector('.dy-root'); if (!root) return; const minimized = root.classList.toggle('dy-minimized'); settings.panel = settings.panel || {}; settings.panel.minimized = minimized; saveSettings(); // 简单实现:隐藏 body 与 template-editor const body = root.querySelector('.dy-body'); const tpl = root.querySelector('.dy-template-editor'); const settingsEl = root.querySelector('.dy-settings'); if (minimized) { if (body) body.style.display = 'none'; if (tpl) tpl.style.display = 'none'; if (settingsEl) settingsEl.style.display = 'none'; // 关闭模态(如果打开)并隐藏整个面板可确保视觉最小化 try { closeTemplateModal(); } catch (e) {} // 不再隐藏整个面板(避免用户误解为关闭),仅收缩为头部视图 } else { if (body) body.style.display = ''; if (tpl) tpl.style.display = 'none'; if (settingsEl) settingsEl.style.display = ''; // 保持 panel 可见 } } // 监听并应用系统主题(如果启用) let _mq = null; function updateSystemThemeListener() { if (settings.followSystemTheme) { if (!_mq) _mq = window.matchMedia('(prefers-color-scheme: light)'); const apply = () => { const panel = document.getElementById('dy-fire-new-panel'); if (!panel) return; if (_mq.matches) { panel.classList.add('dy-theme-light'); settings.theme = 'light'; } else { panel.classList.remove('dy-theme-light'); settings.theme = 'dark'; } }; _mq.addEventListener ? _mq.addEventListener('change', apply) : _mq.addListener(apply); apply(); } else { if (_mq) { try { _mq.removeEventListener ? _mq.removeEventListener('change', null) : _mq.removeListener(null); } catch(e){} _mq = null; } } } function savePersistent() { // Ensure all templates have the macros array and lastSendDate before saving for (const [name, templateData] of Object.entries(persistent)) { if (!templateData.macros) { templateData.macros = []; } if (!templateData.lastSendDate) { templateData.lastSendDate = ''; } } GM_setValue(KEY_PERSIST, JSON.stringify(persistent)); } function loadMacros() { const raw = GM_getValue(KEY_MACROS, '{}'); try { macros = typeof raw === 'string' ? JSON.parse(raw) : raw; // Ensure all macros have the enabled property for backward compatibility for (const [name, macroData] of Object.entries(macros)) { if (typeof macroData.enabled === 'undefined') { macroData.enabled = true; // Default to enabled for backward compatibility } } } catch (e) { macros = {}; } } function saveMacros() { GM_setValue(KEY_MACROS, JSON.stringify(macros)); } function loadSettings() { const raw = GM_getValue(KEY_SETTINGS, null); if (raw) { try { settings = typeof raw === 'string' ? JSON.parse(raw) : raw; } catch (e) {} } } function saveSettings() { GM_setValue(KEY_SETTINGS, JSON.stringify(settings)); } // 自动抓取聊天列表到暂存(不加入已为续火花目标的对象) function autoFetchChats() { const els = document.querySelectorAll(SELECTORS.userName); const names = []; els.forEach(el => { const name = el.textContent && el.textContent.trim(); if (name) names.push(name); }); let added = 0; names.forEach(name => { if (persistent[name]) return; // 已为续火花目标则忽略 if (!staged.includes(name)) { staged.push(name); added++; } }); if (added > 0) { renderPanel(); } } // 渲染面板 function renderPanel() { const existing = document.getElementById('dy-fire-new-panel'); if (existing) existing.remove(); const panel = document.createElement('div'); panel.id = 'dy-fire-new-panel'; // 基本位置/尺寸(可能由设置覆盖) panel.style.position = 'fixed'; panel.style.zIndex = '9999'; panel.style.fontFamily = 'Microsoft YaHei'; panel.innerHTML = `
续火目标管理
暂存列表
    续火花目标
      `; document.body.appendChild(panel); // 注入样式并设置 class injectStyles(); panel.classList.add('dy-panel'); const root = panel.firstElementChild; if (root) root.classList.add('dy-root'); if (settings.theme === 'light') panel.classList.add('dy-theme-light'); // 恢复保存的位置与大小 if (settings.panel && typeof settings.panel === 'object') { if (settings.panel.left) panel.style.left = settings.panel.left + 'px'; if (settings.panel.top) panel.style.top = settings.panel.top + 'px'; if (settings.panel.width) panel.style.width = settings.panel.width + 'px'; if (settings.panel.height) panel.style.height = settings.panel.height + 'px'; if (settings.panel.minimized) root.classList.add('dy-minimized'); } else { // 默认位置 panel.style.right = '20px'; panel.style.top = '60px'; panel.style.width = '540px'; } // 应用主题调色(兼容旧内联样式) applyTheme(panel); // 使面板可拖动与调整大小 makeDraggable(panel); makeResizable(panel); // 创建并准备模板模态编辑器(全局仅一份) ensureTemplateModalExists(); // 确保面板在视口内(首次渲染与窗口变化时) function ensurePanelInViewport() { try { const rect = panel.getBoundingClientRect(); let changed = false; let left = rect.left; let top = rect.top; const pad = 12; const maxW = window.innerWidth - pad * 2; const maxH = window.innerHeight - pad * 2; // 限制宽高 if (panel.offsetWidth > maxW) { panel.style.width = Math.max(320, maxW) + 'px'; changed = true; } if (panel.offsetHeight > maxH) { panel.style.height = Math.max(160, maxH) + 'px'; changed = true; } // 修正位置 if (rect.right > window.innerWidth - pad) { left = Math.max(pad, window.innerWidth - pad - panel.offsetWidth); changed = true; } if (rect.left < pad) { left = pad; changed = true; } if (rect.top < pad) { top = pad; changed = true; } if (rect.bottom > window.innerHeight - pad) { top = Math.max(pad, window.innerHeight - pad - panel.offsetHeight); changed = true; } if (changed) { panel.style.left = left + 'px'; panel.style.top = top + 'px'; panel.style.right = 'auto'; } } catch (e) {} } // 监听窗口变化,自动调整 const _onWinResize = () => ensurePanelInViewport(); window.addEventListener('resize', _onWinResize); // 在 panel 被移除时清理监听 panel.addEventListener('remove', () => window.removeEventListener('resize', _onWinResize)); // 事件绑定 document.getElementById('dy-fetch-chats').addEventListener('click', () => { autoFetchChats(); }); document.getElementById('dy-close-panel').addEventListener('click', () => panel.remove()); const themeToggle = document.getElementById('dy-theme-toggle'); if (themeToggle) themeToggle.addEventListener('click', toggleTheme); const minBtn = document.getElementById('dy-minimize'); if (minBtn) minBtn.addEventListener('click', () => toggleMinimize(panel)); const macroManagerBtn = document.getElementById('dy-macro-manager'); if (macroManagerBtn) macroManagerBtn.addEventListener('click', () => { openMacroManagerModal(); }); const followCb = document.getElementById('dy-follow-system'); if (followCb) { followCb.checked = !!settings.followSystemTheme; followCb.addEventListener('change', (e) => { settings.followSystemTheme = !!e.target.checked; saveSettings(); updateSystemThemeListener(); }); } // 根据保存的最小化状态应用初始显示(避免仅有 class 而未调整 display 的情况) try { const bodyEl = root.querySelector('.dy-body'); const tplEl = root.querySelector('.dy-template-editor'); const settingsEl = root.querySelector('.dy-settings'); if (root.classList.contains('dy-minimized')) { if (bodyEl) bodyEl.style.display = 'none'; if (tplEl) tplEl.style.display = 'none'; if (settingsEl) settingsEl.style.display = 'none'; } else { if (bodyEl) bodyEl.style.display = ''; if (tplEl) tplEl.style.display = 'none'; if (settingsEl) settingsEl.style.display = ''; } } catch (e) {} renderLists(); } // 为面板注入统一样式(美化) function applyTheme(panel) { try { // 根据面板主题应用不同配色,避免在浅色主题下文字对比不足 const isLight = panel.classList.contains('dy-theme-light'); panel.style.width = panel.style.width || '540px'; panel.style.top = panel.style.top || '40px'; panel.style.right = panel.style.right || '24px'; panel.style.padding = panel.style.padding || '0'; panel.style.borderRadius = panel.style.borderRadius || '14px'; panel.style.overflow = panel.style.overflow || 'visible'; panel.style.boxShadow = '0 20px 50px rgba(0,0,0,0.6)'; const root = panel.firstElementChild; if (!root) return; root.style.padding = root.style.padding || '14px'; root.style.borderRadius = root.style.borderRadius || '12px'; const header = root.querySelector('strong'); if (header) { header.style.fontSize = '16px'; header.style.letterSpacing = '0.2px'; header.style.color = isLight ? '#111' : '#fff'; } // 按钮样式(浅色主题使用偏暗底色以保证文字可读) const buttons = root.querySelectorAll('button'); buttons.forEach(btn => { if (isLight) { btn.style.background = 'linear-gradient(90deg,#374151,#4b5563)'; btn.style.border = '1px solid rgba(0,0,0,0.06)'; } else { btn.style.background = 'linear-gradient(90deg,#ff6b8b,#ff2c54)'; btn.style.border = 'none'; } btn.style.color = '#fff'; btn.style.padding = '6px 10px'; btn.style.borderRadius = '8px'; btn.style.cursor = 'pointer'; btn.style.fontSize = '12px'; btn.style.boxShadow = isLight ? 'none' : '0 6px 18px rgba(255,44,84,0.12)'; }); // 更醒目的关闭按钮 const closeBtn = root.querySelector('#dy-close-panel'); if (closeBtn) { closeBtn.style.background = 'transparent'; closeBtn.style.color = isLight ? '#444' : '#bbb'; closeBtn.style.fontSize = '16px'; closeBtn.style.padding = '4px 8px'; closeBtn.style.boxShadow = 'none'; } // 面板内输入与 textarea const inputs = root.querySelectorAll('input, textarea'); inputs.forEach(i => { if (isLight) { i.style.background = '#fff'; i.style.border = '1px solid rgba(0,0,0,0.08)'; i.style.color = '#111'; } else { i.style.background = '#0f1114'; i.style.border = '1px solid rgba(255,255,255,0.06)'; i.style.color = '#e6eef8'; } i.style.padding = '6px 8px'; i.style.borderRadius = '6px'; }); // 列表样式 const lists = root.querySelectorAll('ul'); lists.forEach(ul => { ul.style.padding = '6px'; ul.style.margin = '0'; ul.style.maxHeight = ul.style.maxHeight || '260px'; ul.style.overflow = 'auto'; }); // list items 调整 const lis = root.querySelectorAll('li'); lis.forEach(li => { li.style.display = 'block'; li.style.padding = '8px 6px'; li.style.borderRadius = '6px'; li.style.marginBottom = '6px'; li.style.background = isLight ? 'linear-gradient(180deg, rgba(0,0,0,0.02), rgba(0,0,0,0.01))' : 'linear-gradient(180deg, rgba(255,255,255,0.02), rgba(0,0,0,0.02))'; }); // checkbox 样式微调 const cbs = root.querySelectorAll('.dy-select-checkbox'); cbs.forEach(cb => { cb.style.width = '16px'; cb.style.height = '16px'; }); // 文本区域高亮 const log = root.querySelector('#dy-template-editor'); if (log) { log.style.background = isLight ? 'rgba(0,0,0,0.02)' : 'linear-gradient(180deg, rgba(255,255,255,0.02), rgba(0,0,0,0.02))'; } } catch (e) { console.warn('applyTheme error', e); } } // 模态模板编辑器辅助:创建、打开、关闭、键盘保存 // Monaco Editor 加载器(使用 createElement) function loadMonacoEditorOnce() { if (window.__dy_monaco_promise) return window.__dy_monaco_promise; window.__dy_monaco_promise = new Promise((resolve) => { try { // 创建并添加 loader.js 脚本 const loaderScript = document.createElement('script'); loaderScript.src = 'https://cdn.jsdelivr.net/npm/monaco-editor@latest/min/vs/loader.js'; loaderScript.onload = function() { // 设置 Monaco Editor 的基础路径 require.config({ paths: { 'vs': 'https://cdn.jsdelivr.net/npm/monaco-editor@latest/min/vs' } }); // 加载 Monaco Editor require(['vs/editor/editor.main'], function() { // 自定义主题 monaco.editor.defineTheme('dy-dark', { base: 'vs-dark', inherit: true, rules: [ { token: 'comment', foreground: '6a9955' }, { token: 'keyword', foreground: '569cd6' }, { token: 'string', foreground: 'ce9178' } ], colors: { 'editor.background': '#1e1e1e', 'editor.foreground': '#d4d4d4' } }); monaco.editor.defineTheme('dy-light', { base: 'vs', inherit: true, rules: [ { token: 'comment', foreground: '008000' }, { token: 'keyword', foreground: '0000ff' }, { token: 'string', foreground: 'a31515' } ], colors: { 'editor.background': '#ffffff', 'editor.foreground': '#333333' } }); resolve(monaco); }); }; document.head.appendChild(loaderScript); } catch (e) { console.warn('Monaco Editor 加载失败', e); resolve(null); } }); return window.__dy_monaco_promise; } // Update modal preview function - global scope window.updateModalPreview = function() { const preview = document.getElementById('dy-modal-preview'); if (!preview) return; const ta = document.getElementById('dy-modal-editor-text'); const source = (window.__dy_monaco_editor && window.__dy_monaco_editor.getModel) ? window.__dy_monaco_editor.getModel().getValue() : (ta ? ta.value : ''); const sampleCtx = { targetName: activeEdit || '目标' }; try { // 尝试渲染模板,检查是否有语法错误 const out = renderTemplate(source || '', sampleCtx, activeEdit || '目标'); preview.style.color = ''; preview.style.background = ''; preview.textContent = out; } catch (e) { preview.style.color = '#ff6b6b'; preview.style.background = 'rgba(255, 107, 107, 0.1)'; preview.textContent = `模板错误: ${e.message}`; } }; function ensureTemplateModalExists() { if (document.getElementById('dy-template-modal')) return; const modal = document.createElement('div'); modal.id = 'dy-template-modal'; modal.style.display = 'none'; modal.innerHTML = `
      编辑模板
      编辑模板(支持 $date $targetName $sinceDate("YYYY-M-D") 和直接JavaScript代码)
      `; document.body.appendChild(modal); // 事件绑定 document.getElementById('dy-tpl-cancel').addEventListener('click', closeTemplateModal); document.getElementById('dy-tpl-save').addEventListener('click', saveTemplateForActive); document.getElementById('dy-tpl-fullscreen').addEventListener('click', toggleFullscreen); // 语法插入按钮与实时预览(支持 Monaco Editor) const ta = document.getElementById('dy-modal-editor-text'); const monacoContainer = document.getElementById('dy-modal-editor-monaco'); const preview = document.getElementById('dy-modal-preview'); let __dyEditor = null; function insertToEditor(text) { // 优先使用 Monaco Editor if (window.__dy_monaco_editor && window.__dy_monaco_editor.getModel) { const model = window.__dy_monaco_editor.getModel(); const position = window.__dy_monaco_editor.getPosition(); const range = new monaco.Range( position.lineNumber, position.column, position.lineNumber, position.column ); model.pushEditOperations([], [{ range: range, text: text }]); window.__dy_monaco_editor.focus(); // 同步到textarea if (ta) { ta.value = model.getValue(); } // 更新预览 updateModalPreview(); return; } // 回退到textarea try { const start = ta.selectionStart || 0; const end = ta.selectionEnd || 0; const val = ta.value || ''; ta.value = val.slice(0, start) + text + val.slice(end); const pos = start + text.length; ta.selectionStart = ta.selectionEnd = pos; ta.focus(); // 更新预览 updateModalPreview(); } catch (e) { if (ta) { ta.value += text; // 更新预览 updateModalPreview(); } } } // Monaco Editor 将在第一次打开模态框时初始化 const btnDate = document.getElementById('dy-tpl-ins-date'); const btnTarget = document.getElementById('dy-tpl-ins-target'); const btnSince = document.getElementById('dy-tpl-ins-since'); const btnIf = document.getElementById('dy-tpl-ins-if'); const btnJs = document.getElementById('dy-tpl-ins-js'); if (btnDate) btnDate.addEventListener('click', () => insertToEditor('$date')); if (btnTarget) btnTarget.addEventListener('click', () => insertToEditor('$targetName')); if (btnSince) btnSince.addEventListener('click', () => insertToEditor('$sinceDate("YYYY-M-D")')); // 键盘监听(全局但仅在模态开启时生效) modal._kbdHandler = function(e) { if ((e.ctrlKey || e.metaKey) && (e.key === 's' || e.key === 'S')) { e.preventDefault(); saveTemplateForActive(); } else if (e.key === 'Escape') { e.preventDefault(); closeTemplateModal(); } }; } function openTemplateModal(name) { activeEdit = name; const modal = document.getElementById('dy-template-modal'); if (!modal) return; // 根据面板主题同步模态主题 if (settings.theme === 'light') modal.classList.add('dy-theme-light'); else modal.classList.remove('dy-theme-light'); const targetEl = document.getElementById('dy-modal-editor-target'); const textEl = document.getElementById('dy-modal-editor-text'); const monacoContainer = document.getElementById('dy-modal-editor-monaco'); targetEl.textContent = name; const initial = (persistent[name] && persistent[name].template) || ''; // 初始化 Monaco Editor(如果尚未初始化) if (!window.__dy_monaco_editor) { // 先显示 textarea,Monaco Editor 加载后会替换 textEl.style.display = 'block'; monacoContainer.style.display = 'none'; textEl.value = initial; // 懒加载 Monaco Editor loadMonacoEditorOnce().then((monaco) => { if (!monaco) return; // 初始化编辑器 const chosenTheme = (settings && settings.theme === 'light') ? 'dy-light' : 'dy-dark'; // 使用JavaScript语法高亮 // 不再需要自定义语言,直接使用JavaScript monaco.languages.setMonarchTokensProvider('template-language', { tokenizer: { root: [ [/\$date/g, 'keyword'], [/\$targetName/g, 'keyword'], [/\$sinceDate\([^)]+\)/g, 'keyword'], [/{%\s*if[^}]*?%}[\s\S]*?{%\s*endif\s*%}/g, 'keyword'], ] } }); // 创建编辑器实例 window.__dy_monaco_editor = monaco.editor.create(monacoContainer, { value: textEl.value, language: 'javascript', theme: chosenTheme, lineNumbers: 'on', wordWrap: 'on', minimap: { enabled: false }, scrollBeyondLastLine: false, automaticLayout: true, suggestOnTriggerCharacters: true, quickSuggestions: true, parameterHints: { enabled: true }, }); // 添加自动补全 monaco.languages.registerCompletionItemProvider('javascript', { provideCompletionItems: function(model, position) { const word = model.getWordUntilPosition(position); const range = { startLineNumber: position.lineNumber, endLineNumber: position.lineNumber, startColumn: word.startColumn, endColumn: word.endColumn }; return { suggestions: [ { label: '$date', kind: monaco.languages.CompletionItemKind.Keyword, insertText: '$date', range: range, documentation: '当前日期' }, { label: '$targetName', kind: monaco.languages.CompletionItemKind.Keyword, insertText: '$targetName', range: range, documentation: '对象名称' }, { label: '$sinceDate("YYYY-M-D")', kind: monaco.languages.CompletionItemKind.Keyword, insertText: '$sinceDate("YYYY-M-D")', range: range, documentation: '相识天数' } ] }; } }); // 将 textarea 隐藏 textEl.style.display = 'none'; monacoContainer.style.display = 'block'; // 绑定 change 事件,同时更新textarea和预览 const changeHandler = () => { // 获取当前编辑器内容 const content = window.__dy_monaco_editor.getModel().getValue(); // 立即同步内容到textarea if (textEl) { textEl.value = content; } // 立即更新预览 updateModalPreview(); }; // 移除旧的监听器(如果存在) if (window.__dy_monaco_editor._changeDisposable) { window.__dy_monaco_editor._changeDisposable.dispose(); } // 添加新的监听器并保存引用 window.__dy_monaco_editor._changeDisposable = window.__dy_monaco_editor.onDidChangeModelContent(changeHandler); // 添加快捷键 window.__dy_monaco_editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => saveTemplateForActive()); window.__dy_monaco_editor.addCommand(monaco.KeyCode.Escape, () => closeTemplateModal()); // 更新预览 updateModalPreview(); // 聚焦并选中全部文本 setTimeout(() => { window.__dy_monaco_editor.focus(); try { window.__dy_monaco_editor.setSelection(window.__dy_monaco_editor.getModel().getFullModelRange()); } catch(e){} }, 100); }); } else { // Monaco Editor 已初始化,直接设置内容 if (window.__dy_monaco_editor && window.__dy_monaco_editor.getModel) { window.__dy_monaco_editor.getModel().setValue(initial); // 同步到textarea if (textEl) { textEl.value = initial; // 确保textarea已有input事件监听器 if (!textEl._hasMonacoSync) { textEl.addEventListener('input', () => { if (window.__dy_monaco_editor && window.__dy_monaco_editor.getModel) { const editorValue = window.__dy_monaco_editor.getModel().getValue(); if (editorValue !== textEl.value) { window.__dy_monaco_editor.getModel().setValue(textEl.value); } } updateModalPreview(); }); textEl._hasMonacoSync = true; } } // 确保编辑器内容变化时更新预览 const changeHandler = () => { // 获取当前编辑器内容 const content = window.__dy_monaco_editor.getModel().getValue(); // 立即同步内容到textarea if (textEl) { textEl.value = content; } // 立即更新预览 updateModalPreview(); }; // 移除旧的监听器(如果存在) if (window.__dy_monaco_editor._changeDisposable) { window.__dy_monaco_editor._changeDisposable.dispose(); } // 添加新的监听器并保存引用 window.__dy_monaco_editor._changeDisposable = window.__dy_monaco_editor.onDidChangeModelContent(changeHandler); // 根据主题设置编辑器主题 const chosenTheme = (settings && settings.theme === 'light') ? 'dy-light' : 'dy-dark'; monaco.editor.setTheme(chosenTheme); textEl.style.display = 'none'; monacoContainer.style.display = 'block'; // 更新预览 updateModalPreview(); } else { textEl.style.display = 'block'; monacoContainer.style.display = 'none'; textEl.value = initial; // 添加textarea的input事件监听器,确保预览更新 if (!textEl._hasChangeListener) { textEl.addEventListener('input', updateModalPreview); textEl._hasChangeListener = true; } // 更新预览 updateModalPreview(); } } modal.style.display = 'block'; // 简单淡入 const overlay = modal.querySelector('.dy-tpl-overlay'); if (overlay) overlay.style.opacity = '1'; // 如果 Monaco Editor 已初始化,更新布局 setTimeout(() => { try { if (window.__dy_monaco_editor) { // 更新布局 window.__dy_monaco_editor.layout(); } } catch (e) {} }, 50); window.addEventListener('keydown', modal._kbdHandler); } function toggleFullscreen() { const modal = document.getElementById('dy-template-modal'); if (!modal) return; const box = modal.querySelector('.dy-tpl-box'); if (!box) return; const fullscreenBtn = document.getElementById('dy-tpl-fullscreen'); const overlay = modal.querySelector('.dy-tpl-overlay'); if (box.classList.contains('dy-fullscreen')) { // 退出全屏 box.classList.remove('dy-fullscreen'); fullscreenBtn.textContent = '⛶'; fullscreenBtn.title = '全屏显示'; // 恢复overlay样式 if (overlay) { overlay.style.alignItems = 'center'; overlay.style.justifyContent = 'center'; } } else { // 进入全屏 box.classList.add('dy-fullscreen'); fullscreenBtn.textContent = '⛶'; fullscreenBtn.title = '退出全屏'; // 调整overlay样式以适应全屏 if (overlay) { overlay.style.alignItems = 'stretch'; overlay.style.justifyContent = 'stretch'; overlay.style.padding = '0'; } } // 如果Monaco编辑器已加载,重新计算布局 setTimeout(() => { if (window.__dy_monaco_editor) { window.__dy_monaco_editor.layout(); } }, 300); } function closeTemplateModal() { const modal = document.getElementById('dy-template-modal'); if (!modal) return; modal.style.display = 'none'; try { window.removeEventListener('keydown', modal._kbdHandler); } catch (e) {} activeEdit = null; // 退出全屏状态(如果有) const box = modal.querySelector('.dy-tpl-box'); if (box && box.classList.contains('dy-fullscreen')) { box.classList.remove('dy-fullscreen'); } } // Macro Manager Modal Functions function ensureMacroModalExists() { if (document.getElementById('dy-macro-modal')) return; const modal = document.createElement('div'); modal.id = 'dy-macro-modal'; modal.style.display = 'none'; modal.innerHTML = `
      宏管理系统
      管理宏
        应用宏
          `; document.body.appendChild(modal); // Add CSS for the macro modal if (!document.getElementById('dy-macro-styles')) { const macroStyles = document.createElement('style'); macroStyles.id = 'dy-macro-styles'; macroStyles.innerHTML = ` /* Enhanced macro management panel styles */ #dy-macro-modal { position: fixed; left: 0; top: 0; right: 0; bottom: 0; display: none; z-index: 10001; } #dy-macro-modal .dy-macro-overlay { position: absolute; left:0; top:0; right:0; bottom:0; background: rgba(0,0,0,0.65); display:flex; align-items:center; justify-content:center; padding:20px; transition: opacity 200ms ease; } #dy-macro-modal .dy-macro-box { width: min(1000px, 96%); background: linear-gradient(160deg, #1e1e2a, #14141c); color:#e6eef8; border-radius:16px; padding:16px; box-shadow:0 20px 60px rgba(0,0,0,0.7); max-height:92vh; overflow:auto; transition: all 0.3s ease; border: 1px solid rgba(255,255,255,0.08); } #dy-macro-modal .dy-macro-box-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:12px; padding-bottom: 12px; border-bottom: 1px solid rgba(255,255,255,0.1); } #dy-macro-modal .dy-macro-box-controls { display:flex; gap: 8px; } #dy-macro-modal.dy-theme-light .dy-macro-box { background: linear-gradient(160deg, #ffffff, #f8fafc); color:#111827; border: 1px solid rgba(0,0,0,0.08); } #dy-macro-modal .dy-macro-box-body { margin-bottom:8px } .dy-macro-body { display: flex; gap: 16px; min-height: 500px; } .dy-macro-column { flex: 1; background: rgba(255,255,255,0.04); padding: 12px; border-radius: 10px; max-height: 550px; overflow: auto; min-width: 300px; border: 1px solid rgba(255,255,255,0.06); box-shadow: 0 4px 6px rgba(0,0,0,0.05); transition: all 0.3s ease; } .dy-macro-column:hover { box-shadow: 0 6px 12px rgba(0,0,0,0.1); border-color: rgba(255,255,255,0.1); } .dy-macro-column.manage-macros { border-right: 2px solid rgba(139, 92, 246, 0.2); } .dy-macro-column.apply-macros { border-left: 2px solid rgba(59, 130, 246, 0.2); } .dy-macro-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; } .dy-macro-title { font-size: 16px; font-weight: 600; color: #c7d2fe; display: flex; align-items: center; } .dy-macro-title::before { content: "⚡"; margin-right: 8px; font-size: 14px; } .dy-macro-item { padding: 12px; margin-bottom: 10px; background: rgba(0,0,0,0.2); border-radius: 8px; border: 1px solid rgba(255,255,255,0.1); transition: all 0.2s ease; position: relative; overflow: hidden; } .dy-macro-item::before { content: ''; position: absolute; top: 0; left: 0; width: 3px; height: 100%; background: linear-gradient(to bottom, #8b5cf6, #3b82f6); } .dy-macro-item:hover { background: rgba(255,255,255,0.06); transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.15); } .dy-macro-item.enabled { border-left: 4px solid #10b981; } .dy-macro-item.enabled::before { background: linear-gradient(to bottom, #10b981, #34d399); } .dy-macro-item.disabled { border-left: 4px solid #ef4444; opacity: 0.7; } .dy-macro-item.disabled::before { background: linear-gradient(to bottom, #ef4444, #f87171); } .dy-macro-item-name { font-weight: 600; margin-bottom: 4px; color: #e0e7ff; font-size: 14px; } .dy-macro-item-desc { font-size: 13px; color: #94a3b8; margin-bottom: 6px; } .dy-macro-item-code { font-family: 'Fira Code', 'Consolas', monospace; font-size: 12px; background: rgba(0,0,0,0.3); padding: 6px; border-radius: 4px; overflow: auto; max-height: 80px; color: #cbd5e1; border: 1px solid rgba(255,255,255,0.05); } .dy-macro-item-templates { font-size: 11px; color: #64748b; margin-top: 6px; padding-top: 6px; border-top: 1px solid rgba(255,255,255,0.05); } .dy-macro-actions { display: flex; gap: 6px; margin-top: 8px; justify-content: flex-end; } .dy-macro-toggle { padding: 6px 10px; font-size: 12px; border-radius: 6px; min-width: 60px; } .dy-macro-edit { padding: 6px 10px; font-size: 12px; border-radius: 6px; min-width: 50px; } .dy-macro-delete { padding: 6px 10px; font-size: 12px; border-radius: 6px; min-width: 50px; } .dy-macro-form { margin-top: 16px; padding: 12px; background: rgba(0,0,0,0.2); border-radius: 8px; border: 1px solid rgba(255,255,255,0.08); } .dy-macro-form input, .dy-macro-form textarea { width: 100%; box-sizing: border-box; margin-bottom: 8px; padding: 10px; border-radius: 6px; background: rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.1); color: #e6eef8; } .dy-macro-form textarea { min-height: 120px; font-family: 'Fira Code', 'Consolas', monospace; font-size: 13px; } .dy-macro-form-buttons { text-align: right; margin-top: 8px; } .dy-macro-select { width: 100%; padding: 10px; border-radius: 8px; background: rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.1); color: #e6eef8; font-size: 13px; margin-bottom: 8px; } .dy-macro-assign-btn { background: linear-gradient(90deg,#8b5cf6,#6366f1); width: 100%; margin-top: 4px; padding: 10px; border-radius: 8px; font-weight: 500; } .dy-macro-clear-btn { background: linear-gradient(90deg,#f97316,#ea580c); width: 100%; margin-top: 6px; padding: 10px; border-radius: 8px; font-weight: 500; } .dy-macro-assign-btn:hover { background: linear-gradient(90deg,#7c3aed,#4f46e5); transform: translateY(-1px); box-shadow: 0 4px 8px rgba(139, 92, 246, 0.3); } .dy-macro-clear-btn:hover { background: linear-gradient(90deg,#ea580c,#c2410c); transform: translateY(-1px); box-shadow: 0 4px 8px rgba(249, 115, 22, 0.3); } .dy-macro-toggle:hover { background: linear-gradient(90deg,#4b5563,#374151); transform: translateY(-1px); box-shadow: 0 2px 6px rgba(0,0,0,0.2); } .dy-macro-edit:hover { background: linear-gradient(90deg,#22c55e,#16a34a); transform: translateY(-1px); box-shadow: 0 2px 6px rgba(34, 197, 94, 0.3); } .dy-macro-delete:hover { background: linear-gradient(90deg,#ef4444,#dc2626); transform: translateY(-1px); box-shadow: 0 2px 6px rgba(239, 68, 68, 0.3); } .dy-macro-form input:focus, .dy-macro-form textarea:focus, .dy-macro-select:focus { outline: none; border-color: #8b5cf6; box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.3); } /* Light theme overrides */ .dy-macro-column.dy-theme-light { background: rgba(0,0,0,0.03); border: 1px solid rgba(0,0,0,0.06); } .dy-macro-item.dy-theme-light { background: rgba(0,0,0,0.02); border: 1px solid rgba(0,0,0,0.05); color: #111827; } .dy-macro-item-name.dy-theme-light { color: #111827; } .dy-macro-item-desc.dy-theme-light { color: #6b7280; } .dy-macro-item-code.dy-theme-light { background: rgba(0,0,0,0.03); border: 1px solid rgba(0,0,0,0.05); color: #374151; } .dy-macro-form.dy-theme-light { background: rgba(0,0,0,0.02); border: 1px solid rgba(0,0,0,0.06); } .dy-macro-form input.dy-theme-light, .dy-macro-form textarea.dy-theme-light { background: #ffffff; border: 1px solid rgba(0,0,0,0.1); color: #111827; } .dy-macro-select.dy-theme-light { background: #ffffff; border: 1px solid rgba(0,0,0,0.1); color: #111827; } /* Scrollbar styling */ .dy-macro-column::-webkit-scrollbar { width: 8px; } .dy-macro-column::-webkit-scrollbar-track { background: rgba(0,0,0,0.1); border-radius: 4px; } .dy-macro-column::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); border-radius: 4px; } .dy-macro-column::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.3); } `; document.head.appendChild(macroStyles); } // Event bindings for macro modal document.getElementById('dy-macro-cancel').addEventListener('click', closeMacroModal); document.getElementById('dy-save-macro').addEventListener('click', saveMacroFromForm); document.getElementById('dy-clear-macro-form').addEventListener('click', () => { document.getElementById('dy-macro-name').value = ''; document.getElementById('dy-macro-desc').value = ''; if (window.__dy_macro_monaco_editor && window.__dy_macro_monaco_editor.getModel) { window.__dy_macro_monaco_editor.getModel().setValue(''); } else { document.getElementById('dy-macro-code').value = ''; } }); } function openMacroManagerModal() { ensureMacroModalExists(); const modal = document.getElementById('dy-macro-modal'); if (!modal) return; // Apply theme if (settings.theme === 'light') modal.classList.add('dy-theme-light'); else modal.classList.remove('dy-theme-light'); // Initialize Monaco Editor for macro code if not already done const codeTextarea = document.getElementById('dy-macro-code'); const editorContainer = document.getElementById('dy-macro-editor-container'); if (!window.__dy_macro_monaco_editor) { // Show textarea initially, Monaco will replace it codeTextarea.style.display = 'block'; editorContainer.style.display = 'none'; // Load Monaco Editor for macro loadMonacoEditorOnce().then((monaco) => { if (!monaco) return; // Initialize editor const chosenTheme = (settings && settings.theme === 'light') ? 'dy-light' : 'dy-dark'; // Create editor instance for macro window.__dy_macro_monaco_editor = monaco.editor.create(editorContainer, { value: codeTextarea.value, language: 'javascript', theme: chosenTheme, lineNumbers: 'on', wordWrap: 'on', minimap: { enabled: false }, scrollBeyondLastLine: false, automaticLayout: true, suggestOnTriggerCharacters: true, quickSuggestions: true, parameterHints: { enabled: true }, }); // Add auto-completion for macro-specific keywords monaco.languages.registerCompletionItemProvider('javascript', { provideCompletionItems: function(model, position) { const word = model.getWordUntilPosition(position); const range = { startLineNumber: position.lineNumber, endLineNumber: position.lineNumber, startColumn: word.startColumn, endColumn: word.endColumn }; return { suggestions: [ { label: '$targetName', kind: monaco.languages.CompletionItemKind.Variable, insertText: '$targetName', range: range, documentation: '目标名称' }, { label: '$date', kind: monaco.languages.CompletionItemKind.Variable, insertText: '$date', range: range, documentation: '当前日期' }, { label: '$sinceDate("YYYY-M-D")', kind: monaco.languages.CompletionItemKind.Function, insertText: '$sinceDate("YYYY-M-D")', range: range, documentation: '相识天数' } ] }; } }); // Hide textarea and show editor codeTextarea.style.display = 'none'; editorContainer.style.display = 'block'; // Sync changes between editor and textarea const changeHandler = () => { const content = window.__dy_macro_monaco_editor.getModel().getValue(); codeTextarea.value = content; }; // Remove old listener if exists if (window.__dy_macro_monaco_editor._changeDisposable) { window.__dy_macro_monaco_editor._changeDisposable.dispose(); } // Add new listener window.__dy_macro_monaco_editor._changeDisposable = window.__dy_macro_monaco_editor.onDidChangeModelContent(changeHandler); }); } else { // Monaco editor already exists, just update content and theme if (window.__dy_macro_monaco_editor && window.__dy_macro_monaco_editor.getModel) { // Update theme const chosenTheme = (settings && settings.theme === 'light') ? 'dy-light' : 'dy-dark'; monaco.editor.setTheme(chosenTheme); // Show editor and hide textarea codeTextarea.style.display = 'none'; editorContainer.style.display = 'block'; } else { // Fallback to textarea codeTextarea.style.display = 'block'; editorContainer.style.display = 'none'; } } // Show the modal modal.style.display = 'block'; const overlay = modal.querySelector('.dy-macro-overlay'); if (overlay) overlay.style.opacity = '1'; // Render macro lists renderMacroLists(); } function closeMacroModal() { const modal = document.getElementById('dy-macro-modal'); if (!modal) return; modal.style.display = 'none'; } function renderLists() { const stagedList = document.getElementById('dy-staged-list'); const persistList = document.getElementById('dy-persist-list'); if (!stagedList || !persistList) return; stagedList.innerHTML = ''; staged.forEach(name => { const li = document.createElement('li'); li.className = 'dy-item'; // Check if this name is already in persistent storage (for consistency) const targetData = persistent[name]; const lastSendDate = targetData && targetData.lastSendDate ? `上次发送: ${targetData.lastSendDate}` : '未发送'; li.innerHTML = `
          ${escapeHtml(lastSendDate)}
          `; stagedList.appendChild(li); }); persistList.innerHTML = ''; Object.keys(persistent).forEach(name => { const targetData = persistent[name]; const lastSendDate = targetData.lastSendDate ? `上次发送: ${targetData.lastSendDate}` : '未发送'; const li = document.createElement('li'); li.className = 'dy-item'; li.innerHTML = `
          ${escapeHtml(lastSendDate)}
          `; persistList.appendChild(li); }); // 绑定事件 document.querySelectorAll('.dy-btn-persist').forEach(btn => btn.addEventListener('click', onPersist)); document.querySelectorAll('.dy-btn-unpersist').forEach(btn => btn.addEventListener('click', onUnpersist)); document.querySelectorAll('.dy-btn-edit').forEach(btn => btn.addEventListener('click', onEditTemplate)); document.querySelectorAll('.dy-btn-send').forEach(btn => btn.addEventListener('click', onSendNow)); // checkbox 事件 document.querySelectorAll('.dy-select-checkbox').forEach(cb => cb.addEventListener('change', onSelectToggle)); const selectAll = document.getElementById('dy-select-all'); if (selectAll) selectAll.onchange = onSelectAll; // 批量发送按钮 const batchBtn = document.getElementById('dy-batch-send'); if (batchBtn) batchBtn.onclick = batchSendSelected; // scheduler controls const saveScheduleBtn = document.getElementById('dy-save-schedule'); if (saveScheduleBtn) saveScheduleBtn.onclick = saveScheduleFromUI; const toggleSchedulerBtn = document.getElementById('dy-toggle-scheduler'); if (toggleSchedulerBtn) toggleSchedulerBtn.onclick = toggleScheduler; const intervalInput = document.getElementById('dy-interval-sec'); if (intervalInput) intervalInput.onchange = () => { settings.sendIntervalSec = Number(intervalInput.value) || 3; saveSettings(); }; // send mode selector const sendModeSelect = document.getElementById('dy-send-mode'); if (sendModeSelect) { sendModeSelect.value = settings.sendMode || 'scheduled'; sendModeSelect.onchange = () => { settings.sendMode = sendModeSelect.value; saveSettings(); updateSchedulerStatus(); // Show/hide schedule time input based on mode const scheduleTimeRow = document.getElementById('dy-schedule-time-row'); if (scheduleTimeRow) { scheduleTimeRow.style.display = settings.sendMode === 'scheduled' ? 'flex' : 'none'; } }; // Initialize visibility based on current mode const scheduleTimeRow = document.getElementById('dy-schedule-time-row'); if (scheduleTimeRow) { scheduleTimeRow.style.display = settings.sendMode === 'scheduled' ? 'flex' : 'none'; } } // 初始化 UI 值 const timeInput = document.getElementById('dy-schedule-time'); if (timeInput) timeInput.value = settings.schedulerTime || ''; if (intervalInput) intervalInput.value = settings.sendIntervalSec || 3; updateSchedulerStatus(); // 宏管理面板事件绑定 const saveMacroBtn = document.getElementById('dy-save-macro'); if (saveMacroBtn) saveMacroBtn.addEventListener('click', saveMacroFromForm); const clearMacroFormBtn = document.getElementById('dy-clear-macro-form'); if (clearMacroFormBtn) clearMacroFormBtn.addEventListener('click', () => { document.getElementById('dy-macro-name').value = ''; document.getElementById('dy-macro-desc').value = ''; document.getElementById('dy-macro-code').value = ''; }); // 渲染宏列表 renderMacroLists(); } function onPersist(e) { const name = e.currentTarget.dataset.name; if (!name) return; if (!persistent[name]) { persistent[name] = { template: DEFAULT_TEMPLATE, macros: [], lastSendDate: '' }; } // 从暂存移除 staged = staged.filter(n => n !== name); savePersistent(); renderLists(); } function onUnpersist(e) { const name = e.currentTarget.dataset.name; if (!name) return; delete persistent[name]; savePersistent(); renderLists(); } function onEditTemplate(e) { const name = e.currentTarget.dataset.name; if (!name) return; // 使用模态窗口编辑模板 openTemplateModal(name); } function saveTemplateForActive() { if (!activeEdit) return; let tpl = ''; if (window.__dy_monaco_editor && window.__dy_monaco_editor.getModel) { try { tpl = window.__dy_monaco_editor.getModel().getValue(); } catch (e) { tpl = ''; } } else { const editorText = document.getElementById('dy-modal-editor-text') || document.getElementById('dy-editor-text'); tpl = (editorText && editorText.value) ? editorText.value : ''; } if (!persistent[activeEdit]) persistent[activeEdit] = { template: tpl, macros: [], lastSendDate: '' }; else { persistent[activeEdit].template = tpl; // Ensure macros array exists if (!persistent[activeEdit].macros) persistent[activeEdit].macros = []; // Ensure lastSendDate exists if (!persistent[activeEdit].lastSendDate) persistent[activeEdit].lastSendDate = ''; } savePersistent(); renderLists(); // 关闭模态 closeTemplateModal(); } function onSendNow(e) { const name = e.currentTarget.dataset.name; if (!name) return; const tpl = (persistent[name] && persistent[name].template) || DEFAULT_TEMPLATE; const rendered = renderTemplate(tpl, { targetName: name }, name); sendToTarget(name, rendered).then(ok => { if (ok) { // Update lastSendDate if in automatic mode if (settings.sendMode === 'automatic') { persistent[name].lastSendDate = new Date().toDateString(); savePersistent(); } notify('发送成功', name + ' 已发送'); // Refresh the UI to show updated status renderLists(); } else { notify('发送失败', '请检查页面或稍后重试'); } }); } function onSelectToggle(e) { const name = e.currentTarget.dataset.name; if (!name) return; if (e.currentTarget.checked) selectedSet.add(name); else selectedSet.delete(name); } function onSelectAll(e) { const checked = e.currentTarget.checked; document.querySelectorAll('.dy-select-checkbox').forEach(cb => { cb.checked = checked; const name = cb.dataset.name; if (checked) selectedSet.add(name); else selectedSet.delete(name); }); } async function batchSendSelected() { const names = Array.from(selectedSet); if (names.length === 0) return notify('未选中', '请先选择要批量发送的对象'); await batchSend(names); } async function batchSend(names) { const statusEl = document.getElementById('dy-scheduler-status'); if (statusEl) statusEl.textContent = `发送中: 0/${names.length}`; for (let i = 0; i < names.length; i++) { const name = names[i]; const tpl = (persistent[name] && persistent[name].template) || 'return \`自动续火花-$date\n$targetName\`'; const rendered = renderTemplate(tpl, { targetName: name }, name); const ok = await sendToTarget(name, rendered); if (ok && settings.sendMode === 'automatic') { // Update lastSendDate for automatic mode persistent[name].lastSendDate = new Date().toDateString(); savePersistent(); } if (statusEl) statusEl.textContent = `发送中: ${i+1}/${names.length}`; await sleep((settings.sendIntervalSec || 3) * 1000); } if (statusEl) statusEl.textContent = `上次批量完成: ${new Date().toLocaleTimeString()}`; notify('批量发送完成', `共 ${names.length} 条`); // Refresh the UI to show updated status renderLists(); } function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // 宏管理相关函数 function addMacro(name, code, description = '', enabled = true) { if (!name || !code) return false; macros[name] = { code: code, enabled: enabled, description: description }; saveMacros(); return true; } function updateMacro(name, code, description = '', enabled = true) { if (!name || !macros[name]) return false; macros[name] = { code: code, enabled: enabled, description: description }; saveMacros(); return true; } function deleteMacro(name) { if (!macros[name]) return false; // Remove this macro from all templates that use it for (const [templateName, templateData] of Object.entries(persistent)) { if (templateData.macros && templateData.macros.includes(name)) { templateData.macros = templateData.macros.filter(macroName => macroName !== name); } } delete macros[name]; saveMacros(); savePersistent(); // Save the updated templates return true; } function toggleMacro(name) { if (!macros[name]) return false; macros[name].enabled = !macros[name].enabled; saveMacros(); return true; } function renderMacroLists() { const manageList = document.getElementById('dy-manage-macros-list'); const applyList = document.getElementById('dy-apply-macros-list'); if (!manageList || !applyList) return; // Clear lists manageList.innerHTML = ''; applyList.innerHTML = ''; // Populate manage list Object.keys(macros).forEach(name => { const macro = macros[name]; // Find which templates use this macro const templatesUsingMacro = []; for (const [templateName, templateData] of Object.entries(persistent)) { if (templateData.macros && templateData.macros.includes(name)) { templatesUsingMacro.push(templateName); } } const li = document.createElement('li'); li.className = `dy-macro-item ${macro.enabled ? 'enabled' : 'disabled'}`; li.innerHTML = `
          ${escapeHtml(name)}
          ${escapeHtml(macro.description || '无描述')}
          ${escapeHtml(macro.code.substring(0, 100))}${macro.code.length > 100 ? '...' : ''}
          ${templatesUsingMacro.length > 0 ? `被 ${templatesUsingMacro.length} 个模板使用: ${escapeHtml(templatesUsingMacro.slice(0, 3).join(', '))}${templatesUsingMacro.length > 3 ? '...' : ''}` : '未被任何模板使用'}
          `; manageList.appendChild(li); }); // Populate apply list - show all templates with macro assignment interface Object.keys(persistent).forEach(templateName => { const templateData = persistent[templateName]; const li = document.createElement('li'); li.className = 'dy-macro-item'; li.innerHTML = `
          ${escapeHtml(templateName)}
          当前宏: ${templateData.macros && templateData.macros.length > 0 ? escapeHtml(templateData.macros.join(', ')) : '无'}
          `; applyList.appendChild(li); }); // If no templates exist, show a message if (applyList.children.length === 0) { const li = document.createElement('li'); li.className = 'dy-macro-item'; li.innerHTML = `
          暂无续火花目标
          `; applyList.appendChild(li); } // Bind events for manage list document.querySelectorAll('.dy-macro-toggle').forEach(btn => { btn.addEventListener('click', (e) => { const name = e.currentTarget.dataset.name; toggleMacro(name); renderMacroLists(); // Refresh the lists }); }); document.querySelectorAll('.dy-macro-edit').forEach(btn => { btn.addEventListener('click', (e) => { const name = e.currentTarget.dataset.name; const macro = macros[name]; if (macro) { document.getElementById('dy-macro-name').value = name; document.getElementById('dy-macro-desc').value = macro.description || ''; document.getElementById('dy-macro-code').value = macro.code; // Also update the Monaco editor if it exists if (window.__dy_macro_monaco_editor && window.__dy_macro_monaco_editor.getModel) { window.__dy_macro_monaco_editor.getModel().setValue(macro.code); } else { document.getElementById('dy-macro-code').value = macro.code; } } }); }); document.querySelectorAll('.dy-macro-delete').forEach(btn => { btn.addEventListener('click', (e) => { const name = e.currentTarget.dataset.name; if (confirm(`确定要删除宏 "${name}" 吗?\n注意:此宏可能被某些模板使用,删除后这些模板将无法执行该宏。`)) { deleteMacro(name); renderMacroLists(); // Refresh the lists // Clear form if the deleted macro was being edited if (document.getElementById('dy-macro-name').value === name) { document.getElementById('dy-macro-name').value = ''; document.getElementById('dy-macro-desc').value = ''; document.getElementById('dy-macro-code').value = ''; // Clear Monaco editor if it exists if (window.__dy_macro_monaco_editor && window.__dy_macro_monaco_editor.getModel) { window.__dy_macro_monaco_editor.getModel().setValue(''); } } } }); }); // Bind events for macro assignment document.querySelectorAll('.dy-macro-assign-btn').forEach(btn => { btn.addEventListener('click', (e) => { const templateName = e.currentTarget.dataset.template; const selectElement = document.querySelector(`.dy-macro-select[data-template="${escapeAttr(templateName)}"]`); const macroName = selectElement.value; if (!macroName) { notify('错误', '请选择一个宏'); return; } // Add macro to template if (!persistent[templateName]) { persistent[templateName] = { template: '', macros: [] }; } if (!persistent[templateName].macros) { persistent[templateName].macros = []; } // Avoid duplicates if (!persistent[templateName].macros.includes(macroName)) { persistent[templateName].macros.push(macroName); savePersistent(); renderMacroLists(); // Refresh the lists notify('成功', `宏 "${macroName}" 已添加到模板 "${templateName}"`); } else { notify('提示', `宏 "${macroName}" 已存在于模板 "${templateName}" 中`); } }); }); document.querySelectorAll('.dy-macro-clear-btn').forEach(btn => { btn.addEventListener('click', (e) => { const templateName = e.currentTarget.dataset.template; if (confirm(`确定要清空 "${templateName}" 的所有宏吗?`)) { if (persistent[templateName]) { persistent[templateName].macros = []; savePersistent(); renderMacroLists(); // Refresh the lists notify('成功', `模板 "${templateName}" 的宏已清空`); } } }); }); } function saveMacroFromForm() { const nameInput = document.getElementById('dy-macro-name'); const descInput = document.getElementById('dy-macro-desc'); const codeInput = document.getElementById('dy-macro-code'); const name = nameInput.value.trim(); const desc = descInput.value.trim(); // Get code from Monaco editor if available, otherwise from textarea let code = ''; if (window.__dy_macro_monaco_editor && window.__dy_macro_monaco_editor.getModel) { code = window.__dy_macro_monaco_editor.getModel().getValue(); } else { code = codeInput.value.trim(); } if (!name || !code) { notify('错误', '宏名称和代码不能为空'); return; } // Check if macro exists to update or add new if (macros[name]) { updateMacro(name, code, desc, macros[name].enabled); notify('成功', `宏 "${name}" 已更新`); } else { addMacro(name, code, desc); notify('成功', `宏 "${name}" 已创建`); } // Refresh lists and clear form renderMacroLists(); nameInput.value = ''; descInput.value = ''; // Clear both textarea and Monaco editor codeInput.value = ''; if (window.__dy_macro_monaco_editor && window.__dy_macro_monaco_editor.getModel) { window.__dy_macro_monaco_editor.getModel().setValue(''); } } function saveScheduleFromUI() { const timeInput = document.getElementById('dy-schedule-time'); const sendModeSelect = document.getElementById('dy-send-mode'); if (!timeInput || !sendModeSelect) return; settings.schedulerTime = timeInput.value || ''; settings.sendMode = sendModeSelect.value || 'scheduled'; settings.autoEnabled = true; saveSettings(); startScheduler(); updateSchedulerStatus(); if (settings.sendMode === 'scheduled') { notify('已保存定时', `每天 ${settings.schedulerTime} 将批量发送续火花目标`); } else if (settings.sendMode === 'automatic') { notify('已启用自动发送', `将自动检查并发送给未发送的联系人`); } } function toggleScheduler() { settings.autoEnabled = !settings.autoEnabled; saveSettings(); if (settings.autoEnabled) startScheduler(); else stopScheduler(); updateSchedulerStatus(); } function updateSchedulerStatus() { const statusEl = document.getElementById('dy-scheduler-status'); const toggleBtn = document.getElementById('dy-toggle-scheduler'); if (!statusEl || !toggleBtn) return; if (settings.autoEnabled) { if (settings.sendMode === 'scheduled') { statusEl.textContent = `定时启用:${settings.schedulerTime || '(无时间)'},间隔 ${settings.sendIntervalSec}s`; } else if (settings.sendMode === 'automatic') { statusEl.textContent = `自动发送启用,间隔 ${settings.sendIntervalSec}s`; } } else { statusEl.textContent = '定时未启用'; } toggleBtn.textContent = settings.autoEnabled ? '禁用定时' : '启用定时'; } function startScheduler() { if (schedulerTimer) clearInterval(schedulerTimer); schedulerTimer = setInterval(schedulerTick, 30 * 1000); lastScheduledRun = ''; } function stopScheduler() { if (schedulerTimer) clearInterval(schedulerTimer); schedulerTimer = null; } function schedulerTick() { if (!settings.autoEnabled) return; const now = new Date(); const currentDate = now.toDateString(); const hh = String(now.getHours()).padStart(2,'0'); const mm = String(now.getMinutes()).padStart(2,'0'); const cur = `${hh}:${mm}`; // Check if we're in scheduled mode if (settings.sendMode === 'scheduled') { if (settings.schedulerTime && cur === settings.schedulerTime && lastScheduledRun !== currentDate) { lastScheduledRun = currentDate; // 执行批量发送:续火花目标列表 const names = Object.keys(persistent); if (names.length > 0) batchSend(names); } } // Check if we're in automatic mode else if (settings.sendMode === 'automatic') { // Check each target to see if it needs to be sent to today const names = Object.keys(persistent); const targetsToSend = []; for (const name of names) { const targetData = persistent[name]; // If lastSendDate is not today, add to targets to send if (targetData.lastSendDate !== currentDate) { targetsToSend.push(name); } } if (targetsToSend.length > 0) { // Update lastSendDate for all targets that will be sent for (const name of targetsToSend) { persistent[name].lastSendDate = currentDate; } savePersistent(); // Send to all targets that need to be sent batchSend(targetsToSend); } } } // 支持$开头的占位符 function renderTemplate(tpl, ctx, targetName = null) { // 预处理变量 let out = preprocessVariables(tpl, ctx.targetName || ''); try { // Execute macros associated with this specific template let macroCode = ''; if (targetName && persistent[targetName] && persistent[targetName].macros) { // Get macros associated with this specific template const templateMacros = persistent[targetName].macros; for (const macroName of templateMacros) { if (macros[macroName] && macros[macroName].code) { // Preprocess variables in macro code as well let processedMacroCode = preprocessVariables(macros[macroName].code, ctx.targetName || ''); macroCode += processedMacroCode + ';'; } } } else { // Fallback: execute globally enabled macros (for backward compatibility) for (const [name, macro] of Object.entries(macros)) { if (macro.enabled && macro.code) { // Preprocess variables in macro code as well let processedMacroCode = preprocessVariables(macro.code, ctx.targetName || ''); macroCode += processedMacroCode + ';'; } } } // 将预处理后的代码直接视为JavaScript代码执行,先执行模板,再执行宏 const result = eval(`(function(){let res="";${out};${macroCode};return res;})()`); return result; } catch (e) { return '错误: ' + e.message; } } function prepareExpr(expr, ctx) { // 提供 daysSince(name) 与 targetName 变量 // 将 daysSince("2025-1-2") 替换为 number literal const replaced = expr.replace(/daysSince\((['\"])(.*?)\1\)/g, (_, q, d) => { return String(daysSince(d)); }).replace(/targetName/g, JSON.stringify(ctx.targetName || '')); return replaced; } function daysSince(dateStr) { try { const d = new Date(dateStr); if (isNaN(d)) return 0; const now = new Date(); const diff = now - d; return Math.floor(diff / (1000 * 60 * 60 * 24)); } catch (e) { return 0; } } function findUserElementByName(name) { const els = document.querySelectorAll(SELECTORS.userName); for (const el of els) { if (el.textContent && el.textContent.trim() === name) return el; } return null; } function waitForChatInput(timeout = 8000) { return new Promise((resolve, reject) => { const start = Date.now(); const tick = () => { const input = document.querySelector(SELECTORS.chatInput); if (input) return resolve(input); if (Date.now() - start > timeout) return reject(new Error('chat input timeout')); setTimeout(tick, 200); }; tick(); }); } async function sendToTarget(name, message) { try { const el = findUserElementByName(name); if (!el) return false; // 点击目标 try { el.click(); } catch (e) { el.dispatchEvent(new MouseEvent('click', { bubbles: true })); } await waitForPageLoadShort(); const input = await waitForChatInput(); // 填入内容 input.textContent = ''; input.focus(); const lines = message.split('\n'); for (let i = 0; i < lines.length; i++) { document.execCommand('insertText', false, lines[i]); if (i < lines.length - 1) document.execCommand('insertLineBreak'); } input.dispatchEvent(new Event('input', { bubbles: true })); // 点击发送 const sendBtn = document.querySelector(SELECTORS.sendBtn); if (sendBtn) { if (!sendBtn.disabled) sendBtn.click(); else return false; return true; } return false; } catch (e) { return false; } } function waitForPageLoadShort() { return new Promise(resolve => setTimeout(resolve, 600)); } // 辅助:安全显示/转义 function escapeHtml(s) { return String(s).replace(/[&<>\"]/g, c => ({'&':'&','<':'<','>':'>','"':'"'}[c])); } function escapeAttr(s) { return String(s).replace(/"/g, '"'); } function notify(title, text) { if (typeof GM_notification !== 'undefined') { try { GM_notification({ title, text, timeout: 3000 }); } catch (e) { console.log(title, text); } } else { console.log(title, text); } } // 执行JS代码 - 使用函数模板包装 function executeScript(code) { try { // 将代码包装在函数中执行,使用模板处理 const wrappedCode = '(function(){' + code + '})()'; // 创建函数并执行 const result = eval(wrappedCode); return result; } catch (e) { console.error('代码执行错误:', e); return { error: e.message }; } } // 测试宏系统 function testMacroSystem() { // Test that macros are properly integrated console.log('Testing macro system...'); console.log('Current macros:', macros); console.log('Current persistent data:', persistent); // Test template rendering with macros const testTemplate = 'return "Hello " + targetName;'; const context = { targetName: 'TestUser' }; const result = renderTemplate(testTemplate, context, 'TestUser'); console.log('Template result:', result); } // 测试自动发送功能 function testAutoSendFeature() { console.log('Testing auto send feature...'); console.log('Current settings:', settings); console.log('Current persistent data structure:', persistent); // Test that all persistent entries have lastSendDate for (const [name, data] of Object.entries(persistent)) { if (!data.hasOwnProperty('lastSendDate')) { console.error(`Missing lastSendDate for ${name}`); } else { console.log(`${name} lastSendDate: ${data.lastSendDate}`); } } // Test date comparison logic const today = new Date().toDateString(); console.log('Today is:', today); // Test scheduler tick logic console.log('Testing scheduler tick with current settings...'); console.log('Send mode:', settings.sendMode); console.log('Auto enabled:', settings.autoEnabled); } // 启动:加载持久化并创建面板,然后开始定期抓取 function start() { loadPersistent(); loadMacros(); loadSettings(); renderPanel(); // 初次抓取 autoFetchChats(); // 定时抓取以应对DOM变化 setInterval(autoFetchChats, 5000); // 启动 scheduler(若启用) if (settings.autoEnabled) startScheduler(); // Test the macro system testMacroSystem(); // Test the new auto send feature testAutoSendFeature(); } // 全局快捷键菜单 if (typeof GM_registerMenuCommand !== 'undefined') { GM_registerMenuCommand('打开续火目标面板', () => { renderPanel(); }); } // 初始化 start(); })();