// ==UserScript== // @name Notion Web Clipper // @namespace https://github.com/yuhaung/notion-web-clipper // @version 2.1 // @description 悬停高亮 + 单击选取,保留超链接、富文本、表格/折叠块,知乎自动提取作者,高清图标,自动标签,Twitter 优化,大图隐藏按钮。 // @author yuhaung // @match *://*/* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @connect api.notion.com // @connect * // @license MIT // @supportURL https://github.com/yuhaung/notion-web-clipper/issues // @updateURL https://raw.githubusercontent.com/yuhaung/notion-web-clipper/main/notion-web-clipper.user.js // @downloadURL https://raw.githubusercontent.com/yuhaung/notion-web-clipper/main/notion-web-clipper.user.js // ==/UserScript== (function () { 'use strict'; // 防 iframe 重复 if (window.self !== window.top) return; const oldHost = document.getElementById('nc-host'); if (oldHost) oldHost.remove(); const host = document.createElement('div'); host.id = 'nc-host'; host.style.cssText = 'position:fixed;top:0;left:0;width:0;height:0;z-index:2147483647;'; document.body.appendChild(host); const shadow = host.attachShadow({ mode: 'closed' }); // ==================== 常量 ==================== const NOTION_TEXT_MAX_LEN = 2000; const NOTION_BATCH_SIZE = 100; const BTN_SIZE = 50; const VISIBLE_PART = 25; const SNAP_THRESHOLD = 30; const LARGE_IMG_THRESHOLD = 0.8; const API_RETRY_MAX = 3; const STORAGE_KEYS = { TOKEN: 'notion_token', DB_ID: 'notion_db_id', TAGS_PROP: 'notion_tags_prop', BTN_LEFT: 'nc_btn_left', BTN_TOP: 'nc_btn_top', BTN_HIDDEN: 'nc_btn_hidden', BTN_EDGE: 'nc_btn_edge', }; const BLOCK_TAGS = new Set(['P','DIV','SECTION','ARTICLE','LI','BLOCKQUOTE','H1','H2','H3','H4','H5','H6','PRE','CODE','TABLE','ASIDE','MAIN','HEADER','FOOTER']); const INLINE_TAGS = new Set(['SPAN','A','EM','STRONG','B','I','U','CODE','MARK','SMALL','SUB','SUP','S','DEL']); const SKIP_TAGS = new Set(['STYLE','SCRIPT','NOSCRIPT']); // 知乎清理选择器(去除噪音,但保留作者信息元素,后续会单独提取) const ZHIHU_REMOVE_SELECTORS = [ '.ContentItem-actions','.Post-actions','.VoteButtons', '.ArticleHeaderActions','.ContentItem-more','.RichContent-actions', '.ContentItem-time','.ContentItem-arrowIcon','.ContentItem-extra','.ContentItem-status', '.Reward','.Post-Subtitle','.CornerButtons','.QuestionAnswer-actions', '.QuestionAnswer-meta','.ArticleHeader-info','.FollowButton', '.AnswerItem-extra','.AnswerItem-status', '.ContentItem-arrowIcon','.Post-Header','.ArticleHeader','.QuestionHeader', '.QuestionButtonGroup','.Question-mainColumn .Question-sideColumn','.Question-sideColumn', '.Question-actions','.Question-follow','.Question-status','.Post-bottom','.Article-actions', '.Question-related','.Question-answerItem--status','.Question-answerItem--arrow', '.Question-answerItem--divider','.Question-answerItem--extra','.RichContent-cover', '.RichContent-cover-inner','.Voters','.ContentItem-more','.ContentItem-extra' ]; // ==================== 简写 DOM 查询 ==================== const $ = (sel, base = shadow) => base.querySelector(sel); const $$ = (sel, base = shadow) => base.querySelectorAll(sel); // ==================== 样式 ==================== const style = document.createElement('style'); style.textContent = ` :host { all:initial; } * { box-sizing:border-box; margin:0; padding:0; font-family:sans-serif; } .nc-clipper-btn { position:fixed; width:50px; height:50px; border-radius:50%; background:#2383e2; color:#fff; border:2px solid #fff; cursor:pointer; box-shadow:0 4px 12px rgba(0,0,0,0.3); font-size:24px; display:flex; align-items:center; justify-content:center; transition: left 0.25s ease, top 0.25s ease, opacity 0.2s ease; user-select:none; touch-action:none; opacity:1; left:auto; right:20px; top:auto; bottom:20px; } .nc-clipper-btn:hover { background:#1b6ec2; } .nc-clipper-btn.nc-hidden-edge { opacity:0.5; } .nc-select-tip { position:fixed; top:20px; left:50%; transform:translateX(-50%); background:rgba(0,0,0,0.85); color:#fff; padding:10px 20px; border-radius:24px; font-size:14px; pointer-events:none; box-shadow:0 4px 12px rgba(0,0,0,0.2); display:none; } .nc-highlight-overlay { position:fixed; top:0; left:0; width:0; height:0; border:3px solid #2383e2; background:rgba(35,131,226,0.08); pointer-events:none; display:none; transition: all 0.1s ease; } .nc-overlay { position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.6); display:none; align-items:center; justify-content:center; font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; } .nc-modal { background:white; padding:24px; border-radius:12px; width:550px; max-width:90vw; max-height:85vh; overflow-y:auto; box-shadow:0 10px 25px rgba(0,0,0,0.2); display:flex; flex-direction:column; gap:12px; } .nc-modal h2 { margin:0; font-size:18px; color:#333; } .nc-modal label { font-size:13px; color:#555; font-weight:600; margin-top:4px; } .nc-modal input, .nc-modal textarea { width:100%; padding:10px; border:1px solid #ddd; border-radius:6px; font-size:14px; } .nc-modal textarea { height:200px; resize:vertical; font-family:monospace; font-size:13px; line-height:1.5; } .nc-btn-row { display:flex; gap:10px; justify-content:flex-end; margin-top:12px; } .nc-btn { padding:9px 18px; border:none; border-radius:6px; cursor:pointer; font-weight:600; font-size:14px; } .nc-btn-primary { background:#2383e2; color:#fff; } .nc-btn-primary:hover { background:#1b6ec2; } .nc-btn-primary:disabled { background:#a0c4e8; cursor:not-allowed; } .nc-btn-secondary { background:#f0f0f0; color:#333; } .nc-btn-secondary:hover { background:#e0e0e0; } .nc-help-text { font-size:12px; color:#888; margin-top:-6px; line-height:1.4; } .nc-token-wrapper { position:relative; display:flex; align-items:center; } .nc-token-wrapper input { flex:1; padding-right:40px; } .nc-toggle-vis { position:absolute; right:8px; background:none; border:none; cursor:pointer; font-size:16px; color:#666; padding:4px; } .nc-preview-box { border:1px solid #eee; border-radius:8px; padding:12px; margin-top:8px; max-height:250px; overflow-y:auto; background:#fafafa; font-size:13px; line-height:1.6; user-select:text; -webkit-user-select:text; outline:none; } .nc-preview-box img { max-width:100%; max-height:150px; display:block; margin:8px 0; border-radius:4px; } .nc-preview-box p { margin:4px 0; color:#333; white-space:pre-wrap; user-select:text; -webkit-user-select:text; } .nc-preview-box h1,.nc-preview-box h2,.nc-preview-box h3 { margin:8px 0 4px; color:#111; user-select:text; -webkit-user-select:text; } .nc-preview-box h1 { font-size:1.4em; } .nc-preview-box h2 { font-size:1.2em; } .nc-preview-box h3 { font-size:1.1em; } .nc-preview-box li { margin-left:1.5em; list-style:disc; user-select:text; -webkit-user-select:text; } .nc-preview-box blockquote { border-left:3px solid #2383e2; padding-left:10px; color:#555; margin:8px 0; user-select:text; -webkit-user-select:text; } .nc-preview-box pre { background:#f0f0f0; padding:8px; border-radius:4px; white-space:pre-wrap; font-family:monospace; user-select:text; -webkit-user-select:text; } .nc-video-preview,.nc-embed-preview { color:#2383e2; font-weight:600; margin:8px 0; background:#eef4fb; padding:6px 10px; border-radius:4px; user-select:text; -webkit-user-select:text; } .nc-preview-item { position:relative; margin:2px 0; } .nc-preview-delete { position:absolute; top:2px; right:2px; width:20px; height:20px; background:#ff3b30; color:#fff; border:none; border-radius:50%; font-size:12px; line-height:20px; text-align:center; cursor:pointer; opacity:0; transition:opacity 0.15s; z-index:2; pointer-events:auto; } .nc-preview-item:hover .nc-preview-delete { opacity:1; } .nc-success-message { font-size:15px; color:#2d7d46; font-weight:600; text-align:center; margin:8px 0; } .nc-toast-container { position:fixed; top:20px; right:20px; z-index:2147483647; display:flex; flex-direction:column; gap:8px; pointer-events:none; } .nc-toast { padding:12px 20px; border-radius:6px; color:#fff; font-size:14px; box-shadow:0 4px 12px rgba(0,0,0,0.15); pointer-events:auto; animation: nc-toast-in 0.3s ease; display:flex; align-items:center; gap:8px; max-width:300px; word-break:break-word; } .nc-toast-success { background:#2d7d46; } .nc-toast-error { background:#d32f2f; } @keyframes nc-toast-in { from { opacity:0; transform:translateX(50px); } to { opacity:1; transform:translateX(0); } } `; shadow.appendChild(style); // ==================== UI 构建 ==================== const uiContainer = document.createElement('div'); uiContainer.innerHTML = `
🔍 悬停高亮元素,单击提取内容 (Esc取消)

⚙️ Notion 配置

⚠️ 必须在 Notion 数据库右上角 ... -> Connections 中添加你的 Integration。

✂️ 确认发送

✅ 成功发送到 Notion!

页面已创建,点击下方按钮打开

`; shadow.appendChild(uiContainer); // ==================== DOM 引用 ==================== const btn = $('.nc-clipper-btn'); const selectTip = $('.nc-select-tip'); const highlightOverlay = $('.nc-highlight-overlay'); const settingsOverlay = $('#nc-settings-overlay'); const confirmOverlay = $('#nc-confirm-overlay'); const successOverlay = $('#nc-success-overlay'); const previewBox = $('#nc-preview'); const tokenInput = $('#nc-token'); const dbIdInput = $('#nc-db-id'); const tagsPropInput = $('#nc-tags-prop'); const titleInput = $('#nc-title'); const tagsInput = $('#nc-tags'); const sendBtn = $('#nc-confirm-send'); const successOpenBtn = $('#nc-success-open'); const successCloseBtn = $('#nc-success-close'); const tokenToggle = $('#nc-token-toggle'); const toastContainer = $('#nc-toast-container'); // ==================== 状态变量 ==================== let isSelecting = false; let isConfirmOpen = false; let currentNotionBlocks = []; let highlightedEl = null; let lastCreatedPageId = null; let tokenVisible = false; let isDragging = false; let dragStartX = 0, dragStartY = 0; let initialLeft = 0, initialTop = 0; let lastDragDist = 0; let isHidden = false; let hiddenEdge = ''; let isHiddenForLargeImage = false; let cachedPageIcon = null; function isOurUI(el) { return el === host; } // ==================== Toast 通知 ==================== function showToast(message, type = 'success') { const toast = document.createElement('div'); toast.className = `nc-toast nc-toast-${type}`; toast.innerHTML = `${message}`; toastContainer.appendChild(toast); setTimeout(() => { toast.style.transition = 'opacity 0.3s ease'; toast.style.opacity = '0'; setTimeout(() => toast.remove(), 300); }, 3000); } const alert = (msg) => showToast(msg, 'error'); tokenToggle.addEventListener('click', () => { tokenVisible = !tokenVisible; tokenInput.type = tokenVisible ? 'text' : 'password'; tokenToggle.textContent = tokenVisible ? '🙈' : '👁️'; }); // ==================== 坐标与位置函数 ==================== function clampFullPos(left, top) { const winW = innerWidth; const winH = innerHeight; left = Math.max(0, Math.min(left, winW - BTN_SIZE)); top = Math.max(0, Math.min(top, winH - BTN_SIZE)); return { left, top }; } function getFullPosFromHidden(edge, hiddenLeft, hiddenTop) { const winW = innerWidth, winH = innerHeight; let left = hiddenLeft, top = hiddenTop; if (edge === 'left') left = 0; else if (edge === 'right') left = winW - BTN_SIZE; else if (edge === 'top') top = 0; else if (edge === 'bottom') top = winH - BTN_SIZE; return clampFullPos(left, top); } function getHiddenPos(edge, fullLeft, fullTop) { const winW = innerWidth, winH = innerHeight; let left = fullLeft, top = fullTop; if (edge === 'left') left = -BTN_SIZE + VISIBLE_PART; else if (edge === 'right') left = winW - VISIBLE_PART; else if (edge === 'top') top = -BTN_SIZE + VISIBLE_PART; else if (edge === 'bottom') top = winH - VISIBLE_PART; return { left, top }; } function applyPosition(left, top) { const { left: clampedLeft, top: clampedTop } = clampFullPos(left, top); btn.style.left = clampedLeft + 'px'; btn.style.top = clampedTop + 'px'; btn.style.right = 'auto'; btn.style.bottom = 'auto'; } function setFullVisible() { btn.classList.remove('nc-hidden-edge'); isHidden = false; hiddenEdge = ''; } function setHidden(edge) { btn.classList.add('nc-hidden-edge'); isHidden = true; hiddenEdge = edge; } function savePosition() { let fullLeft, fullTop; if (isHidden) { const rect = btn.getBoundingClientRect(); const pos = getFullPosFromHidden(hiddenEdge, rect.left, rect.top); fullLeft = pos.left; fullTop = pos.top; } else { const rect = btn.getBoundingClientRect(); fullLeft = rect.left; fullTop = rect.top; } const clamped = clampFullPos(fullLeft, fullTop); GM_setValue(STORAGE_KEYS.BTN_LEFT, clamped.left); GM_setValue(STORAGE_KEYS.BTN_TOP, clamped.top); GM_setValue(STORAGE_KEYS.BTN_HIDDEN, isHidden); GM_setValue(STORAGE_KEYS.BTN_EDGE, hiddenEdge); } function loadPosition() { const savedLeft = GM_getValue(STORAGE_KEYS.BTN_LEFT, null); const savedTop = GM_getValue(STORAGE_KEYS.BTN_TOP, null); const savedHidden = GM_getValue(STORAGE_KEYS.BTN_HIDDEN, false); const savedEdge = GM_getValue(STORAGE_KEYS.BTN_EDGE, ''); if (savedLeft !== null && savedTop !== null) { const clamped = clampFullPos(savedLeft, savedTop); if (savedHidden && savedEdge) { const hiddenPos = getHiddenPos(savedEdge, clamped.left, clamped.top); applyPosition(hiddenPos.left, hiddenPos.top); setHidden(savedEdge); } else { applyPosition(clamped.left, clamped.top); setFullVisible(); } } } function snapToEdge(left, top) { const winW = innerWidth, winH = innerHeight; let edge = ''; if (left < SNAP_THRESHOLD) edge = 'left'; else if (left + BTN_SIZE > winW - SNAP_THRESHOLD) edge = 'right'; else if (top < SNAP_THRESHOLD) edge = 'top'; else if (top + BTN_SIZE > winH - SNAP_THRESHOLD) edge = 'bottom'; if (edge) { const hiddenPos = getHiddenPos(edge, left, top); applyPosition(hiddenPos.left, hiddenPos.top); setHidden(edge); } else { applyPosition(left, top); setFullVisible(); } savePosition(); } // ==================== 按钮拖拽事件 ==================== btn.addEventListener('mouseenter', () => { if (isDragging || isHiddenForLargeImage) return; if (isHidden) { const rect = btn.getBoundingClientRect(); const fullPos = getFullPosFromHidden(hiddenEdge, rect.left, rect.top); applyPosition(fullPos.left, fullPos.top); setFullVisible(); } }); btn.addEventListener('mouseleave', () => { if (isDragging) return; if (!isHidden) { const rect = btn.getBoundingClientRect(); snapToEdge(rect.left, rect.top); } }); btn.addEventListener('mousedown', (e) => { if (e.button === 2) return; e.preventDefault(); e.stopPropagation(); isDragging = true; btn.style.transition = 'none'; if (isHidden) { const rect = btn.getBoundingClientRect(); const fullPos = getFullPosFromHidden(hiddenEdge, rect.left, rect.top); applyPosition(fullPos.left, fullPos.top); setFullVisible(); } const rect = btn.getBoundingClientRect(); dragStartX = e.clientX; dragStartY = e.clientY; initialLeft = rect.left; initialTop = rect.top; document.addEventListener('mousemove', onDragMove, true); document.addEventListener('mouseup', onDragEnd, true); }); function onDragMove(e) { if (!isDragging) return; e.preventDefault(); const dx = e.clientX - dragStartX; const dy = e.clientY - dragStartY; let newLeft = initialLeft + dx; let newTop = initialTop + dy; const winW = innerWidth, winH = innerHeight; newLeft = Math.max(-BTN_SIZE + 8, Math.min(newLeft, winW - 8)); newTop = Math.max(-BTN_SIZE + 8, Math.min(newTop, winH - 8)); applyPosition(newLeft, newTop); } function onDragEnd(e) { if (!isDragging) return; document.removeEventListener('mousemove', onDragMove, true); document.removeEventListener('mouseup', onDragEnd, true); isDragging = false; btn.style.transition = 'left 0.25s ease, top 0.25s ease, opacity 0.2s ease'; const rect = btn.getBoundingClientRect(); const dx = e.clientX - dragStartX; const dy = e.clientY - dragStartY; lastDragDist = Math.sqrt(dx*dx + dy*dy); snapToEdge(rect.left, rect.top); } btn.addEventListener('click', (e) => { if (lastDragDist > 4) { e.preventDefault(); e.stopPropagation(); lastDragDist = 0; return; } if (isHidden) { e.preventDefault(); e.stopPropagation(); const rect = btn.getBoundingClientRect(); const fullPos = getFullPosFromHidden(hiddenEdge, rect.left, rect.top); applyPosition(fullPos.left, fullPos.top); setFullVisible(); savePosition(); lastDragDist = 0; return; } e.stopPropagation(); triggerClipper(); lastDragDist = 0; }); btn.addEventListener('contextmenu', e => { e.preventDefault(); e.stopPropagation(); openSettings(); }); window.addEventListener('resize', () => { if (isHidden) { const savedLeft = GM_getValue(STORAGE_KEYS.BTN_LEFT, null); const savedTop = GM_getValue(STORAGE_KEYS.BTN_TOP, null); if (savedLeft !== null && savedTop !== null) { const clamped = clampFullPos(savedLeft, savedTop); const pos = getHiddenPos(hiddenEdge, clamped.left, clamped.top); applyPosition(pos.left, pos.top); } } else { const rect = btn.getBoundingClientRect(); const clamped = clampFullPos(rect.left, rect.top); applyPosition(clamped.left, clamped.top); } }); // ==================== 大图检测 ==================== function isLargeImage(img) { const rect = img.getBoundingClientRect(); return rect.width >= innerWidth * LARGE_IMG_THRESHOLD || rect.height >= innerHeight * LARGE_IMG_THRESHOLD; } document.addEventListener('mousemove', function(e) { if (isDragging || isSelecting) return; const target = document.elementFromPoint(e.clientX, e.clientY); if (target && target.tagName === 'IMG' && isLargeImage(target)) { if (!isHiddenForLargeImage) { isHiddenForLargeImage = true; btn.style.display = 'none'; } } else { if (isHiddenForLargeImage) { isHiddenForLargeImage = false; btn.style.display = ''; if (isHidden) { const savedLeft = GM_getValue(STORAGE_KEYS.BTN_LEFT, null); const savedTop = GM_getValue(STORAGE_KEYS.BTN_TOP, null); if (savedLeft !== null && savedTop !== null) { const pos = getHiddenPos(hiddenEdge, savedLeft, savedTop); applyPosition(pos.left, pos.top); setHidden(hiddenEdge); } } else { loadPosition(); } } } }, true); // ==================== 媒体辅助函数 ==================== function getRealImageURL(img) { if (!img) return null; if (img.src && !img.src.startsWith('data:') && !img.src.includes('placeholder')) { let url = img.src; if (url.startsWith('//')) url = 'https:' + url; return url; } const candidates = ['data-gif','data-animated','data-original','data-actualsrc','data-src']; for (const attr of candidates) { let url = img.getAttribute(attr); if (url && !url.startsWith('data:') && !url.includes('placeholder')) { if (url.startsWith('//')) url = 'https:' + url; return url; } } return null; } function isAvatar(img) { if (!img) return true; const src = img.src || img.getAttribute('data-src') || ''; if (/\.(gif|webp)($|\?|&)/i.test(src)) return false; const rect = img.getBoundingClientRect(); if (rect.width > 0 && rect.height > 0 && (rect.width <= 80 || rect.height <= 80)) return true; const classNames = ['avatar','icon','emoji','face']; if (img.className && classNames.some(c => img.className.toLowerCase().includes(c))) return true; let parent = img.parentElement; for (let i = 0; i < 3 && parent; i++) { if (parent.className && classNames.some(c => parent.className.toLowerCase().includes(c))) return true; parent = parent.parentElement; } if (/avatar|emoji|icon/i.test(src)) return true; if (/_(is|xs|s)\.(jpg|jpeg|png|webp)/i.test(src)) return true; return false; } function isZhihuMemberImage(img) { if (!isZhihu) return false; const className = (img.className || '').toLowerCase(); const src = (img.src || img.getAttribute('data-src') || '').toLowerCase(); const alt = (img.alt || '').toLowerCase(); const title = (img.title || '').toLowerCase(); const combined = [className, src, alt, title].join(' '); if (/member|vip|盐选|pay|lock/.test(combined)) return true; let parent = img.parentElement; for (let i = 0; i < 3 && parent; i++) { const parentClass = (parent.className || '').toLowerCase(); if (/member|vip|pay|lock|盐选/.test(parentClass)) return true; parent = parent.parentElement; } return false; } function getVideoURL(videoEl) { if (!videoEl) return null; if (videoEl.src && !videoEl.src.startsWith('blob:')) return videoEl.src; const sources = videoEl.querySelectorAll('source'); for (const src of sources) if (src.src) return src.src; return null; } function getIframeEmbedURL(iframe) { return iframe && iframe.src ? iframe.src : null; } function getGifPlayerMediaURL(container) { const video = container.querySelector('video'); if (video) { const url = getVideoURL(video); if (url) return { type: 'video', url }; } const img = container.querySelector('img'); if (img) { const gifSrc = img.getAttribute('data-gif'); if (gifSrc) return { type: 'image', url: gifSrc }; const src = getRealImageURL(img); if (src && /\.(gif|webp)($|\?|&)/i.test(src)) return { type: 'image', url: src }; if (src) return { type: 'image', url: src }; } return null; } function getPageMainImage() { const og = document.querySelector('meta[property="og:image"]'); if (og?.content) return og.content; const tw = document.querySelector('meta[name="twitter:image"]'); if (tw?.content) return tw.content; return ''; } function getPageIcon() { if (cachedPageIcon !== null) return cachedPageIcon; const icons = document.querySelectorAll('link[rel="apple-touch-icon"], link[rel="apple-touch-icon-precomposed"], link[rel="mask-icon"], link[rel="icon"], link[rel="shortcut icon"]'); let bestHref = ''; let bestSize = 0; for (const link of icons) { const href = link.href; if (!href || href.startsWith('data:')) continue; if (link.type === 'image/svg+xml' || href.endsWith('.svg')) { cachedPageIcon = href; return href; } const sizes = link.getAttribute('sizes'); if (sizes) { const parts = sizes.trim().split(/\s+/); for (const part of parts) { const match = part.match(/^(\d+)x(\d+)$/i); if (match) { const area = parseInt(match[1]) * parseInt(match[2]); if (area > bestSize) { bestSize = area; bestHref = href; } } else if (part.toLowerCase() === 'any') { cachedPageIcon = href; return href; } } } else { if (link.rel === 'apple-touch-icon' || link.rel === 'apple-touch-icon-precomposed') { if (180 * 180 > bestSize) { bestSize = 180 * 180; bestHref = href; } } else { if (16 * 16 > bestSize) { bestSize = 16 * 16; bestHref = href; } } } } if (bestHref) { cachedPageIcon = bestHref; return bestHref; } cachedPageIcon = origin + '/favicon.ico'; return cachedPageIcon; } // ==================== 构建块 ==================== function buildTextBlock(text) { const safeText = text.length > NOTION_TEXT_MAX_LEN - 10 ? text.substring(0, NOTION_TEXT_MAX_LEN - 10) + '...' : text; return { object: "block", type: "paragraph", paragraph: { rich_text: [{ type: "text", text: { content: safeText } }] } }; } function buildRichTextBlock(richTextArray, annotations) { try { let totalLen = 0; const truncated = []; for (const item of richTextArray) { if (totalLen >= NOTION_TEXT_MAX_LEN) break; let content = item.text.content; if (totalLen + content.length > NOTION_TEXT_MAX_LEN) { content = content.substring(0, NOTION_TEXT_MAX_LEN - totalLen) + '...'; } totalLen += content.length; const element = { type: "text", text: { content: content, link: item.text.link || undefined } }; if (annotations) element.annotations = annotations; truncated.push(element); } return { object: "block", type: "paragraph", paragraph: { rich_text: truncated } }; } catch (e) { console.warn('构建富文本块失败', e); return buildTextBlock(richTextArray.map(i => i.text.content).join('')); } } function buildHeadingBlock(level, text) { const type = `heading_${level}`; return { object: "block", type, [type]: { rich_text: [{ type: "text", text: { content: text } }] } }; } function buildBulletBlock(text) { return { object: "block", type: "bulleted_list_item", bulleted_list_item: { rich_text: [{ type: "text", text: { content: text } }] } }; } function buildNumberedBlock(text) { return { object: "block", type: "numbered_list_item", numbered_list_item: { rich_text: [{ type: "text", text: { content: text } }] } }; } function buildQuoteBlock(text) { return { object: "block", type: "quote", quote: { rich_text: [{ type: "text", text: { content: text } }] } }; } function buildCodeBlock(text, language) { const code = { object: "block", type: "code", code: { rich_text: [{ type: "text", text: { content: text } }] } }; if (language) code.code.language = language; return code; } function buildImageBlock(url) { return { object: "block", type: "image", image: { type: "external", external: { url } } }; } function buildVideoBlock(url) { return { object: "block", type: "video", video: { type: "external", external: { url } } }; } function buildEmbedBlock(url) { return { object: "block", type: "embed", embed: { url } }; } function buildTableBlock(rows, hasHeader) { const tableRows = rows.map((row, idx) => { const cells = row.slice(0, 5).map(cell => ({ type: "text", text: { content: cell } })); return { type: "table_row", table_row: { cells } }; }); return { object: "block", type: "table", table: { table_width: Math.min(rows[0]?.length || 1, 5), has_column_header: hasHeader, children: tableRows } }; } function buildToggleBlock(summary, children) { return { object: "block", type: "toggle", toggle: { rich_text: [{ type: "text", text: { content: summary } }], children: children } }; } // ==================== 知乎清理(保留作者信息) ==================== function cleanZhihuElement(clone) { clone.querySelectorAll(ZHIHU_REMOVE_SELECTORS.join(',')).forEach(el => el.remove()); clone.querySelectorAll('img').forEach(img => { if (isAvatar(img) || isZhihuMemberImage(img)) img.remove(); }); return clone; } // 提取知乎作者名称 function getZhihuAuthorName(element) { const authorSelectors = [ '.UserLink', '.AuthorInfo-name', '.AnswerItem-authorInfo .UserLink', '.ContentItem-authorInfo .UserLink', '.Post-Author .UserLink', '.AuthorInfo .UserLink', '.AnswerItem-authorInfo a[href*="/people/"]', '.ContentItem-authorInfo a[href*="/people/"]' ]; for (const sel of authorSelectors) { const el = element.querySelector(sel) || element.closest('.AnswerItem')?.querySelector(sel); if (el) { return el.textContent.trim().replace(/\s+/g, ' '); } } return null; } // ==================== 内容解析 ==================== function parseFragmentToBlocks(fragment) { const blocks = []; let currentFragments = []; const flushFragments = () => { if (currentFragments.length === 0) return; const nonEmpty = currentFragments.filter(f => f.text.trim() !== ''); if (nonEmpty.length === 0) { currentFragments = []; return; } const hasLink = nonEmpty.some(f => f.link); const hasFormat = nonEmpty.some(f => f.annotations); if (hasLink || hasFormat) { const richTexts = []; let tempText = ''; for (const frag of nonEmpty) { if (!frag.link && !frag.annotations) { tempText += frag.text; } else { if (tempText) { richTexts.push({ text: { content: tempText } }); tempText = ''; } const element = { text: { content: frag.text } }; if (frag.link) element.text.link = { url: frag.link }; if (frag.annotations) element.annotations = frag.annotations; richTexts.push(element); } } if (tempText) richTexts.push({ text: { content: tempText } }); blocks.push(buildRichTextBlock(richTexts)); } else { const fullText = nonEmpty.map(f => f.text).join(''); blocks.push(buildTextBlock(fullText)); } currentFragments = []; }; function getInnerText(node) { if (node.nodeType === Node.TEXT_NODE) return node.textContent; if (node.nodeType === Node.ELEMENT_NODE) { const tag = node.tagName.toUpperCase(); if (tag === 'IMG' || tag === 'VIDEO' || tag === 'IFRAME') return ''; return Array.from(node.childNodes).map(getInnerText).join(''); } return ''; } function getAnnotations(tag) { const annot = {}; if (tag === 'B' || tag === 'STRONG') annot.bold = true; if (tag === 'I' || tag === 'EM') annot.italic = true; if (tag === 'U' || tag === 'INS') annot.underline = true; if (tag === 'S' || tag === 'DEL' || tag === 'STRIKE') annot.strikethrough = true; if (tag === 'CODE') annot.code = true; return Object.keys(annot).length > 0 ? annot : null; } const walk = (node, parentAnnotations) => { if (node.nodeType === Node.TEXT_NODE) { currentFragments.push({ text: node.textContent, link: null, annotations: parentAnnotations }); return; } if (node.nodeType === Node.ELEMENT_NODE) { const tag = node.tagName.toUpperCase(); if (SKIP_TAGS.has(tag)) return; if (node.classList?.contains('GifPlayer')) { flushFragments(); const media = getGifPlayerMediaURL(node); if (media) { if (media.type === 'video') blocks.push(buildVideoBlock(media.url)); else blocks.push(buildImageBlock(media.url)); } else { node.childNodes.forEach(c => walk(c, parentAnnotations)); } return; } if (tag === 'TABLE') { flushFragments(); const rows = []; let hasHeader = false; const trs = node.querySelectorAll('tr'); trs.forEach((tr, idx) => { const cells = []; tr.querySelectorAll('td, th').forEach(cell => cells.push(getInnerText(cell).trim())); if (idx === 0 && tr.querySelector('th')) hasHeader = true; if (cells.length > 0) rows.push(cells); }); if (rows.length > 0) blocks.push(buildTableBlock(rows, hasHeader)); return; } if (tag === 'DETAILS') { flushFragments(); const summary = node.querySelector('summary'); const summaryText = summary ? getInnerText(summary).trim() : '展开'; const children = []; Array.from(node.childNodes).forEach(c => { if (c === summary) return; const fragment = document.createDocumentFragment(); fragment.appendChild(c.cloneNode(true)); const childBlocks = parseFragmentToBlocks(fragment); children.push(...childBlocks); }); blocks.push(buildToggleBlock(summaryText, children)); return; } if (tag === 'A') { const href = node.href || ''; const linkText = getInnerText(node); if (linkText && href) { currentFragments.push({ text: linkText, link: href, annotations: parentAnnotations }); } else if (linkText) { currentFragments.push({ text: linkText, link: null, annotations: parentAnnotations }); } return; } if (INLINE_TAGS.has(tag)) { const newAnnot = getAnnotations(tag) || parentAnnotations; node.childNodes.forEach(c => walk(c, newAnnot)); return; } if (tag === 'BR') { currentFragments.push({ text: '\n', link: null, annotations: parentAnnotations }); return; } if (tag === 'IMG') { if (!isAvatar(node) && !isZhihuMemberImage(node)) { flushFragments(); const url = getRealImageURL(node); if (url) blocks.push(buildImageBlock(url)); } return; } if (tag === 'VIDEO') { flushFragments(); const url = getVideoURL(node); if (url) blocks.push(buildVideoBlock(url)); return; } if (tag === 'IFRAME') { flushFragments(); const url = getIframeEmbedURL(node); if (url) blocks.push(buildEmbedBlock(url)); return; } if (/^H[1-6]$/.test(tag)) { flushFragments(); const headingText = getInnerText(node).trim(); if (headingText) blocks.push(buildHeadingBlock(parseInt(tag[1]), headingText)); return; } if (tag === 'LI') { flushFragments(); const text = getInnerText(node).trim(); if (text) { const parentTag = node.parentElement?.tagName.toUpperCase() || ''; blocks.push(parentTag === 'OL' ? buildNumberedBlock(text) : buildBulletBlock(text)); } return; } if (tag === 'BLOCKQUOTE') { flushFragments(); const text = getInnerText(node).trim(); if (text) blocks.push(buildQuoteBlock(text)); return; } if (tag === 'PRE' || (tag === 'DIV' && node.querySelector('pre'))) { flushFragments(); const pre = tag === 'PRE' ? node : node.querySelector('pre'); if (pre) { const codeText = pre.textContent || ''; const language = pre.getAttribute('data-language') || ''; blocks.push(buildCodeBlock(codeText, language)); } return; } if (tag === 'FIGURE') { flushFragments(); node.childNodes.forEach(c => walk(c, parentAnnotations)); return; } if (BLOCK_TAGS.has(tag)) { flushFragments(); node.childNodes.forEach(c => walk(c, parentAnnotations)); flushFragments(); } else { node.childNodes.forEach(c => walk(c, parentAnnotations)); } } }; fragment.childNodes.forEach(c => walk(c, null)); flushFragments(); return blocks.filter(b => { if (b.type === 'paragraph') { const content = b.paragraph?.rich_text?.map(t => t.text?.content || '').join('').trim(); return content !== ''; } return true; }); } // ==================== Twitter 对话提取 ==================== function extractTwitterConversationBlocks() { if (!isTwitterStatus) return null; const main = document.querySelector('main[role="main"]') || document.querySelector('div[data-testid="primaryColumn"]') || document.body; const tweets = main.querySelectorAll('article[data-testid="tweet"]'); if (tweets.length < 2) return null; const allBlocks = []; for (let i = 0; i < tweets.length; i++) { const clone = tweets[i].cloneNode(true); const fragment = document.createDocumentFragment(); fragment.appendChild(clone); const tweetBlocks = parseFragmentToBlocks(fragment); if (tweetBlocks.length > 0) { if (i > 0) allBlocks.push(buildQuoteBlock('---')); allBlocks.push(...tweetBlocks); } } return allBlocks.length > 0 ? allBlocks : null; } function extractBlocksFromElement(el) { const twitterConv = extractTwitterConversationBlocks(); if (twitterConv) return twitterConv; if (el.tagName === 'IMG') { if (!isAvatar(el) && !isZhihuMemberImage(el)) { const url = getRealImageURL(el); return url ? [buildImageBlock(url)] : []; } return []; } if (el.tagName === 'VIDEO') { const url = getVideoURL(el); return url ? [buildVideoBlock(url)] : []; } if (el.tagName === 'IFRAME') { const url = getIframeEmbedURL(el); return url ? [buildEmbedBlock(url)] : []; } if (el.classList?.contains('GifPlayer')) { const media = getGifPlayerMediaURL(el); if (media) { return [media.type === 'video' ? buildVideoBlock(media.url) : buildImageBlock(media.url)]; } } const clone = el.cloneNode(true); if (isZhihu) cleanZhihuElement(clone); const fragment = document.createDocumentFragment(); fragment.appendChild(clone); let blocks = parseFragmentToBlocks(fragment); // 知乎:提取作者并前置 if (isZhihu) { const author = getZhihuAuthorName(el); if (author) { const authorBlock = buildTextBlock(`作者:${author}`); blocks = [authorBlock, ...blocks]; } } return blocks; } // ==================== 平台判断 ==================== const isZhihu = location.hostname.includes('zhihu.com'); const isTwitter = location.hostname.includes('x.com') || location.hostname.includes('twitter.com'); const isTwitterStatus = isTwitter && location.pathname.includes('/status/'); function findBestTarget(element) { if (!element || element === document.body || element === document.documentElement) return null; if (isOurUI(element)) return null; if (element.tagName === 'IMG') return (!isAvatar(element) && !isZhihuMemberImage(element) && getRealImageURL(element)) ? element : null; if (element.tagName === 'VIDEO' && getVideoURL(element)) return element; if (element.tagName === 'IFRAME' && getIframeEmbedURL(element)) return element; if (element.classList?.contains('GifPlayer')) return element; if (isZhihu) { const selectors = ['.AnswerItem','.PostIndex-answerItem','.List-item','.QuestionAnswer-content','[itemprop="suggestedAnswer"]','.ContentItem','.Card','.RichContent','.RichContent-inner']; for (const sel of selectors) { const card = element.closest(sel); if (card) { const rect = card.getBoundingClientRect(); if (rect.width > 50 && rect.height > 100) return card; } } } if (isTwitter) { const tweet = element.closest('article[data-testid="tweet"]'); if (tweet) return tweet; } for (const tag of BLOCK_TAGS) { const el = element.closest(tag); if (el) { const rect = el.getBoundingClientRect(); if (rect.width > 20 && rect.height > 20) return el; } } return element.closest('p, div, li, blockquote') || null; } // ==================== 事件处理 ==================== let rafId = null; function handleMouseMove(e) { if (!isSelecting) return; if (rafId) cancelAnimationFrame(rafId); rafId = requestAnimationFrame(() => { const target = document.elementFromPoint(e.clientX, e.clientY); if (!target || isOurUI(target)) { removeHighlight(); return; } const best = findBestTarget(target); if (best) { highlightedEl = best; const rect = best.getBoundingClientRect(); highlightOverlay.style.display = 'block'; highlightOverlay.style.top = rect.top + 'px'; highlightOverlay.style.left = rect.left + 'px'; highlightOverlay.style.width = rect.width + 'px'; highlightOverlay.style.height = rect.height + 'px'; } else { removeHighlight(); } }); } function handleClick(e) { if (!isSelecting) return; if (isOurUI(e.target)) return; const target = document.elementFromPoint(e.clientX, e.clientY); if (!target || isOurUI(target)) return; const best = findBestTarget(target); if (!best) return; const blocks = extractBlocksFromElement(best); if (blocks.length === 0) { showToast('所选元素未提取到有效内容', 'error'); return; } stopSelectMode(); currentNotionBlocks = blocks; showConfirmModal(document.title); e.preventDefault(); e.stopPropagation(); } function handleEsc(e) { if (e.key === 'Escape') { e.preventDefault(); if (isSelecting) stopSelectMode(); } } function onConfirmKeydown(e) { if (!isConfirmOpen) return; if (e.key === 'Escape') { e.preventDefault(); closeConfirmModal(); return; } if (e.ctrlKey && e.key === 'a') { const active = shadow.activeElement || document.activeElement; if (active && (active.tagName === 'INPUT' || active.tagName === 'TEXTAREA')) return; e.preventDefault(); previewBox.focus(); const range = document.createRange(); range.selectNodeContents(previewBox); const sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range); } } function closeConfirmModal() { confirmOverlay.style.display = 'none'; isConfirmOpen = false; document.removeEventListener('keydown', onConfirmKeydown, true); } function removeHighlight() { highlightOverlay.style.display = 'none'; highlightedEl = null; } function startSelectMode() { if (isSelecting) stopSelectMode(); isSelecting = true; selectTip.style.display = 'block'; document.addEventListener('mousemove', handleMouseMove, true); document.addEventListener('click', handleClick, true); document.addEventListener('keydown', handleEsc, true); } function stopSelectMode() { if (!isSelecting) return; isSelecting = false; selectTip.style.display = 'none'; removeHighlight(); if (rafId) cancelAnimationFrame(rafId); document.removeEventListener('mousemove', handleMouseMove, true); document.removeEventListener('click', handleClick, true); document.removeEventListener('keydown', handleEsc, true); } // ==================== 预览渲染(安全) ==================== function renderBlockToPreview(block, container, index) { const wrapper = document.createElement('div'); wrapper.className = 'nc-preview-item'; wrapper.dataset.index = index; const delBtn = document.createElement('button'); delBtn.className = 'nc-preview-delete'; delBtn.textContent = '❌'; delBtn.title = '删除此块'; wrapper.appendChild(delBtn); let content; if (block.type === 'paragraph') { const p = document.createElement('p'); if (block.paragraph?.rich_text?.length) { for (const rt of block.paragraph.rich_text) { if (rt.text?.link) { const a = document.createElement('a'); a.href = rt.text.link.url; a.textContent = rt.text.content; a.target = '_blank'; a.rel = 'noopener noreferrer'; p.appendChild(a); } else { const textNode = document.createTextNode(rt.text?.content || ''); p.appendChild(textNode); } } } content = p; } else if (block.type.startsWith('heading')) { const level = block.type.split('_')[1]; const h = document.createElement(`h${level}`); h.textContent = block[block.type]?.rich_text?.[0]?.text?.content || ''; content = h; } else if (block.type === 'bulleted_list_item') { const li = document.createElement('li'); li.textContent = block.bulleted_list_item?.rich_text?.[0]?.text?.content || ''; content = li; } else if (block.type === 'numbered_list_item') { const li = document.createElement('li'); li.textContent = block.numbered_list_item?.rich_text?.[0]?.text?.content || ''; content = li; } else if (block.type === 'quote') { const bq = document.createElement('blockquote'); bq.textContent = block.quote?.rich_text?.[0]?.text?.content || ''; content = bq; } else if (block.type === 'code') { const pre = document.createElement('pre'); pre.textContent = block.code?.rich_text?.[0]?.text?.content || ''; content = pre; } else if (block.type === 'image') { const img = document.createElement('img'); img.src = block.image?.external?.url || ''; img.onerror = () => img.style.display = 'none'; content = img; } else if (block.type === 'video') { const div = document.createElement('div'); div.className = 'nc-video-preview'; div.textContent = `🎬 视频: ${block.video?.external?.url || ''}`; content = div; } else if (block.type === 'embed') { const div = document.createElement('div'); div.className = 'nc-embed-preview'; div.textContent = `📺 嵌入: ${block.embed?.url || ''}`; content = div; } else if (block.type === 'table') { const table = document.createElement('table'); table.style.borderCollapse = 'collapse'; table.style.width = '100%'; if (block.table?.children) { block.table.children.forEach(row => { const tr = document.createElement('tr'); row.table_row?.cells?.forEach(cell => { const td = document.createElement('td'); td.textContent = cell.text?.content || ''; td.style.border = '1px solid #ccc'; td.style.padding = '4px'; tr.appendChild(td); }); table.appendChild(tr); }); } content = table; } else if (block.type === 'toggle') { const details = document.createElement('details'); const summary = document.createElement('summary'); summary.textContent = block.toggle?.rich_text?.[0]?.text?.content || ''; details.appendChild(summary); if (block.toggle?.children) { block.toggle.children.forEach(child => { const childDiv = document.createElement('div'); childDiv.style.marginLeft = '1em'; renderBlockToPreview(child, childDiv, -1); details.appendChild(childDiv); }); } content = details; } if (content) wrapper.appendChild(content); container.appendChild(wrapper); } function refreshPreview() { previewBox.innerHTML = ''; if (currentNotionBlocks.length === 0) { previewBox.textContent = '无内容'; return; } currentNotionBlocks.forEach((block, idx) => renderBlockToPreview(block, previewBox, idx)); } previewBox.addEventListener('click', (e) => { const deleteBtn = e.target.closest('.nc-preview-delete'); if (!deleteBtn) return; e.preventDefault(); e.stopPropagation(); const item = deleteBtn.closest('.nc-preview-item'); if (!item) return; const index = parseInt(item.dataset.index, 10); if (!isNaN(index) && index >= 0 && index < currentNotionBlocks.length) { currentNotionBlocks.splice(index, 1); refreshPreview(); } }); function showConfirmModal(title) { titleInput.value = title; tagsInput.value = ''; refreshPreview(); confirmOverlay.style.display = 'flex'; isConfirmOpen = true; document.addEventListener('keydown', onConfirmKeydown, true); } function openSettings() { tokenInput.value = GM_getValue(STORAGE_KEYS.TOKEN, ''); dbIdInput.value = GM_getValue(STORAGE_KEYS.DB_ID, ''); tagsPropInput.value = GM_getValue(STORAGE_KEYS.TAGS_PROP, 'Tags'); tokenInput.type = 'password'; tokenVisible = false; tokenToggle.textContent = '👁️'; settingsOverlay.style.display = 'flex'; } function triggerClipper() { if (!GM_getValue(STORAGE_KEYS.TOKEN) || !GM_getValue(STORAGE_KEYS.DB_ID)) { showToast('请先右键点击 ✂️ 按钮进行 Notion 配置!', 'error'); openSettings(); return; } startSelectMode(); } function showSuccessModal(pageId) { lastCreatedPageId = pageId; successOverlay.style.display = 'flex'; } // ==================== Notion API(带重试) ==================== async function notionRequest(method, url, data, retries = API_RETRY_MAX) { for (let attempt = 0; attempt < retries; attempt++) { try { const response = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method, url, headers: { 'Authorization': `Bearer ${GM_getValue(STORAGE_KEYS.TOKEN)}`, 'Content-Type': 'application/json', 'Notion-Version': '2022-06-28' }, data: data ? JSON.stringify(data) : null, onload: (res) => { if (res.status >= 200 && res.status < 300) { resolve(JSON.parse(res.responseText)); } else { const msg = (() => { try { const err = JSON.parse(res.responseText); return err.message || JSON.stringify(err).substring(0, 200); } catch { return res.responseText?.substring(0, 200) || 'Unknown error'; } })(); const error = new Error(`API ${res.status}: ${msg}`); error.status = res.status; error.retryAfter = parseInt(res.responseHeaders?.match(/Retry-After: (\d+)/i)?.[1] || 0); reject(error); } }, onerror: () => reject(new Error('Network error')) }); }); return response; } catch (err) { if (attempt < retries - 1 && (err.status === 429 || err.status >= 500)) { const delay = Math.max(err.retryAfter * 1000 || 1000 * Math.pow(2, attempt), 1000); await new Promise(r => setTimeout(r, delay)); continue; } throw err; } } } async function appendBlocks(pageId, blocks) { for (let i = 0; i < blocks.length; i += NOTION_BATCH_SIZE) { const batch = blocks.slice(i, i + NOTION_BATCH_SIZE); await notionRequest('PATCH', `https://api.notion.com/v1/blocks/${pageId}/children`, { children: batch }); } } async function sendToNotion() { sendBtn.disabled = true; sendBtn.innerText = '发送中...'; const dbId = GM_getValue(STORAGE_KEYS.DB_ID).replace(/-/g, ''); const title = titleInput.value || document.title || 'Untitled'; const tags = tagsInput.value.split(',').map(t => t.trim()).filter(t => t); const tagsPropName = GM_getValue(STORAGE_KEYS.TAGS_PROP, 'Tags').trim(); try { const dbInfo = await notionRequest('GET', `https://api.notion.com/v1/databases/${dbId}`); let dbProps = dbInfo.properties; if (tagsPropName && tags.length > 0 && !dbProps[tagsPropName]) { try { await notionRequest('PATCH', `https://api.notion.com/v1/databases/${dbId}`, { properties: { [tagsPropName]: { type: 'multi_select', multi_select: {} } } }); dbProps[tagsPropName] = { type: 'multi_select' }; } catch (e) { console.warn('自动创建标签失败', e); } } let titleProp = 'Name'; for (const key in dbProps) if (dbProps[key].type === 'title') { titleProp = key; break; } const properties = { [titleProp]: { title: [{ text: { content: title.substring(0, 200) } }] } }; if (tagsPropName && tags.length > 0 && dbProps[tagsPropName]) { const type = dbProps[tagsPropName].type; if (type === 'select') properties[tagsPropName] = { select: { name: tags[0] } }; else if (type === 'multi_select') properties[tagsPropName] = { multi_select: tags.map(t => ({ name: t })) }; } if (dbProps['URL']?.type === 'url') properties['URL'] = { url: location.href }; if (dbProps['Content Image']?.type === 'url') { const img = getPageMainImage(); if (img) properties['Content Image'] = { url: img }; } if (dbProps['Icon']?.type === 'url') { const icon = getPageIcon(); if (icon) properties['Icon'] = { url: icon }; } const children = currentNotionBlocks; const firstBatch = children.length <= NOTION_BATCH_SIZE ? children : children.slice(0, NOTION_BATCH_SIZE); const data = { parent: { database_id: dbId }, properties, children: firstBatch }; const iconUrl = getPageIcon(); if (iconUrl) data.icon = { type: 'external', external: { url: iconUrl } }; const response = await notionRequest('POST', 'https://api.notion.com/v1/pages', data); const pageId = response.id; if (children.length > NOTION_BATCH_SIZE) { await appendBlocks(pageId, children.slice(NOTION_BATCH_SIZE)); } closeConfirmModal(); showSuccessModal(pageId); } catch (error) { console.error(error); const msg = error.message?.substring(0, 200) || '未知错误'; showToast(`❌ 发送失败: ${msg}`, 'error'); } finally { sendBtn.disabled = false; sendBtn.innerText = '发送'; } } // ==================== 事件绑定 ==================== $('#nc-settings-close').addEventListener('click', () => settingsOverlay.style.display = 'none'); $('#nc-settings-save').addEventListener('click', () => { const token = tokenInput.value.trim(), dbId = dbIdInput.value.trim().replace(/-/g, ''); if (!token || !dbId) { showToast('Token 和 ID 不能为空', 'error'); return; } GM_setValue(STORAGE_KEYS.TOKEN, token); GM_setValue(STORAGE_KEYS.DB_ID, dbId); GM_setValue(STORAGE_KEYS.TAGS_PROP, tagsPropInput.value.trim()); settingsOverlay.style.display = 'none'; showToast('✅ 保存成功!'); }); $('#nc-confirm-cancel').addEventListener('click', closeConfirmModal); $('#nc-confirm-send').addEventListener('click', sendToNotion); successOpenBtn.addEventListener('click', () => { if (!lastCreatedPageId) return; window.open(`https://www.notion.so/${lastCreatedPageId.replace(/-/g, '')}`, '_blank'); }); successCloseBtn.addEventListener('click', () => successOverlay.style.display = 'none'); successOverlay.addEventListener('click', e => { if (e.target === successOverlay) successOverlay.style.display = 'none'; }); // ==================== 初始化 ==================== loadPosition(); })();