// ==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!
页面已创建,点击下方按钮打开
`;
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();
})();