Greasy Fork is available in English.
ChatGPT 导出到 Notion:智能图片归位 (支持 PicList/PicGo)+隐私开关+单个对话导出
当前为
// ==UserScript==
// @name ChatGPT to Notion Exporter
// @namespace http://tampermonkey.net/
// @version 2.6
// @license MIT
// @description ChatGPT 导出到 Notion:智能图片归位 (支持 PicList/PicGo)+隐私开关+单个对话导出
// @author Wyih
// @match https://chatgpt.com/*
// @match https://chat.openai.com/*
// @connect api.notion.com
// @connect 127.0.0.1
// @connect *
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant GM_addStyle
// @run-at document-end
// ==/UserScript==
(function () {
'use strict';
console.log('[ChatGPT→Notion v2.6] script loaded');
// --- 基础配置 ---
const PICLIST_URL = "http://127.0.0.1:36677/upload";
const ASSET_PLACEHOLDER_PREFIX = "PICLIST_WAITING::";
const MAX_TEXT_LENGTH = 2000;
// ------------------- 0. PicList 环境自检 -------------------
function checkPicListConnection() {
GM_xmlhttpRequest({
method: "GET",
url: "http://127.0.0.1:36677/heartbeat",
timeout: 2000,
onload: (res) => { if (res.status === 200) console.log("✅ PicList 连接正常"); },
onerror: () => console.warn("⚠️ PicList 未连接 (图片可能无法导出)")
});
}
setTimeout(checkPicListConnection, 3000);
// ------------------- 1. Notion 配置管理 -------------------
function getConfig() {
return { token: GM_getValue('notion_token', ''), dbId: GM_getValue('notion_db_id', '') };
}
function promptConfig() {
const token = prompt('请输入 Notion Integration Secret:', GM_getValue('notion_token', ''));
if (token) {
const dbId = prompt('请输入 Notion Database ID:', GM_getValue('notion_db_id', ''));
if (dbId) { GM_setValue('notion_token', token); GM_setValue('notion_db_id', dbId); alert('配置已保存'); }
}
}
GM_registerMenuCommand('⚙️ 设置 Notion Token', promptConfig);
// ------------------- 2. UI 样式 (Sticky) -------------------
GM_addStyle(`
/* 主按钮 */
#chatgpt-saver-btn {
position: fixed; bottom: 20px; right: 20px; z-index: 9999;
background-color: #10A37F; color: white; border: none; border-radius: 6px;
padding: 10px 16px; cursor: pointer; box-shadow: 0 4px 12px rgba(0,0,0,0.15);
font-family: system-ui, sans-serif; font-weight: 600; font-size: 14px; transition: all 0.2s;
}
#chatgpt-saver-btn:hover { background-color: #0d8465; transform: translateY(-2px); }
#chatgpt-saver-btn.loading { background-color: #666; cursor: wait; }
/* 气泡基础 */
.cgpt-turn { position: relative; transition: background 0.2s; }
.cgpt-turn:hover {
box-shadow: 0 0 0 2px rgba(16, 163, 127, 0.2);
border-radius: 8px;
background-color: rgba(16, 163, 127, 0.02);
}
/* 工具条 */
.cgpt-tool-group {
z-index: 9500; display: flex; gap: 6px;
opacity: 0; transition: opacity 0.2s ease-in-out;
background: white; padding: 4px 6px; border-radius: 20px;
box-shadow: 0 2px 5px rgba(0,0,0,0.15); border: 1px solid #e0e0e0;
}
.cgpt-turn:hover .cgpt-tool-group { opacity: 1; }
.cgpt-tool-group:has(.cgpt-privacy-toggle[data-skip="true"]) { opacity: 1 !important; border-color: #fce8e6; background: #fff8f8; }
/* Assistant: 右浮动 + Sticky */
.cgpt-turn[data-role="assistant"] { display: block !important; }
.cgpt-turn[data-role="assistant"] .cgpt-tool-group {
position: sticky; top: 10px; float: right; margin-left: 10px; margin-bottom: 10px; z-index: 100;
}
/* User: Flex Column + Sticky */
.cgpt-turn[data-role="user"] { display: flex !important; flex-direction: column !important; }
.cgpt-turn[data-role="user"] .cgpt-tool-group {
position: sticky; top: 10px; align-self: flex-end; margin-bottom: -34px; z-index: 100; order: -1;
}
/* 按钮图标 */
.cgpt-icon-btn {
cursor: pointer; font-size: 16px; line-height: 24px; user-select: none;
width: 26px; height: 26px; text-align: center; border-radius: 50%;
transition: background 0.2s; display: flex; align-items: center; justify-content: center; color: #555;
}
.cgpt-icon-btn:hover { background: rgba(0,0,0,0.08); color: #000; }
.cgpt-privacy-toggle[data-skip="true"] { color: #d93025; background: #fce8e6; }
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
.cgpt-icon-btn.processing { cursor: wait; color: #1a73e8; background: #e8f0fe; }
.cgpt-icon-btn.processing span { display: block; animation: spin 1s linear infinite; }
.cgpt-icon-btn.success { color: #188038 !important; background: #e6f4ea; }
.cgpt-icon-btn.error { color: #d93025 !important; background: #fce8e6; }
`);
// ------------------- 3. 气泡定位与 UI 注入 -------------------
function getTurnWrappers() {
const nodes = new Set();
document.querySelectorAll('div[data-testid="conversation-turn"]').forEach(el => nodes.add(el));
document.querySelectorAll('[data-message-author-role="user"]').forEach(el => nodes.add(el));
document.querySelectorAll('[data-message-author-role="assistant"]').forEach(el => nodes.add(el));
document.querySelectorAll('.agent-turn').forEach(el => nodes.add(el));
return Array.from(nodes);
}
function getRoleFromWrapper(wrapper) {
let role = wrapper.getAttribute('data-message-author-role');
if (role) return role;
const inner = wrapper.querySelector('[data-message-author-role]');
if (inner) return inner.getAttribute('data-message-author-role');
if (wrapper.classList.contains('agent-turn')) return 'assistant';
if (wrapper.querySelector('div[class*="user"]')) return 'user';
return 'assistant';
}
function injectPerTurnControls() {
const turns = getTurnWrappers();
turns.forEach(turn => {
if (turn.querySelector('.cgpt-tool-group')) return;
const role = getRoleFromWrapper(turn);
turn.classList.add('cgpt-turn');
turn.setAttribute('data-role', role);
const group = document.createElement('div');
group.className = 'cgpt-tool-group';
// 👁️
const privacyBtn = document.createElement('div');
privacyBtn.className = 'cgpt-icon-btn cgpt-privacy-toggle';
privacyBtn.title = '切换隐私(不导出)';
privacyBtn.setAttribute('data-skip', 'false');
const privacyIcon = document.createElement('span');
privacyIcon.textContent = '👁️';
privacyBtn.appendChild(privacyIcon);
privacyBtn.onclick = (e) => {
e.stopPropagation();
const isSkipping = privacyBtn.getAttribute('data-skip') === 'true';
if (isSkipping) {
privacyBtn.setAttribute('data-skip', 'false'); privacyIcon.textContent = '👁️'; turn.setAttribute('data-privacy-skip', 'false');
} else {
privacyBtn.setAttribute('data-skip', 'true'); privacyIcon.textContent = '🚫'; turn.setAttribute('data-privacy-skip', 'true');
}
};
// 📤
const singleBtn = document.createElement('div');
singleBtn.className = 'cgpt-icon-btn';
singleBtn.title = '单条导出';
const exportIcon = document.createElement('span');
exportIcon.textContent = '📤';
singleBtn.appendChild(exportIcon);
singleBtn.onclick = (e) => {
e.stopPropagation();
handleSingleExport(turn, singleBtn, exportIcon);
};
group.appendChild(privacyBtn);
group.appendChild(singleBtn);
if (turn.firstChild) {
turn.insertBefore(group, turn.firstChild);
} else {
turn.appendChild(group);
}
});
}
// ------------------- 4. 资源处理 (Native Fetch + PicList Upload) -------------------
// 🌟 使用原生 fetch 获取 ArrayBuffer
// 优点:自动携带 chatgpt.com 的 cookie,解决 backend-api 鉴权问题;同时也支持 blob:
async function fetchUrlAsArrayBuffer(url) {
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const blob = await response.blob();
const buffer = await blob.arrayBuffer();
return { buffer, type: blob.type };
} catch (e) {
console.error("Fetch failed for:", url, e);
throw e;
}
}
function uploadToPicList(arrayBufferObj, filename) {
return new Promise((resolve, reject) => {
if (!arrayBufferObj.buffer) return reject("空数据");
let finalFilename = filename.split('?')[0];
const mime = (arrayBufferObj.type || '').split(';')[0].trim().toLowerCase();
const mimeMap = { 'image/png': '.png', 'image/jpeg': '.jpg', 'image/webp': '.webp' };
if (!finalFilename.includes('.')) {
if (mimeMap[mime]) finalFilename += mimeMap[mime]; else finalFilename += '.png';
}
const boundary = "----ChatGPTBoundary" + Math.random().toString(36).substring(2);
const preData = `--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${finalFilename.replace(/"/g, '')}"\r\nContent-Type: ${mime || 'application/octet-stream'}\r\n\r\n`;
const combinedBlob = new Blob([preData, arrayBufferObj.buffer, `\r\n--${boundary}--\r\n`]);
// 🌟 上传 PicList 必须用 GM_xmlhttpRequest (跨域)
GM_xmlhttpRequest({
method: "POST", url: PICLIST_URL, headers: { "Content-Type": `multipart/form-data; boundary=${boundary}` }, data: combinedBlob,
onload: (res) => { try { const r = JSON.parse(res.responseText); if (r.success && r.result) resolve(r.result[0]); else reject(r.message || "上传失败"); } catch (e) { reject(e.message); } },
onerror: () => reject("PicList 连接失败")
});
});
}
async function processAssets(blocks, statusCallback) {
const tasks = [];
const map = new Map();
blocks.forEach((b, i) => {
let urlObj = null;
if (b.type === 'image' && b.image?.external?.url?.startsWith(ASSET_PLACEHOLDER_PREFIX)) urlObj = b.image.external;
else if (b.type === 'file' && b.file?.external?.url?.startsWith(ASSET_PLACEHOLDER_PREFIX)) urlObj = b.file.external;
if (urlObj) {
const [_, name, realUrl] = urlObj.url.split('::');
// 普通文件 blob 无法导出,图片可以
if (realUrl.startsWith('blob:') && b.type === 'file') {
b.type = "paragraph";
b.paragraph = { rich_text: [{ type: "text", text: { content: `📄 [本地文件] ${name}` }, annotations: { color: "gray", italic: true } }] };
delete b.file; return;
}
const task = fetchUrlAsArrayBuffer(realUrl)
.then(buf => uploadToPicList(buf, name))
.then(u => ({ i, url: u, name, ok: true }))
.catch(e => ({ i, err: e, name, ok: false }));
tasks.push(task);
map.set(i, b);
}
});
if (tasks.length) {
statusCallback(`⏳ Uploading ${tasks.length} assets...`);
const res = await Promise.all(tasks);
res.forEach(r => {
const blk = map.get(r.i);
if (!blk) return;
if (r.ok) {
if (blk.type === 'image') blk.image.external.url = r.url;
else if (blk.type === 'file') { blk.file.external.url = r.url; blk.file.name = r.name; }
} else {
blk.type = "paragraph";
blk.paragraph = { rich_text: [{ type: "text", text: { content: `⚠️ 图片导出失败: ${r.name}` }, annotations: { color: "red" } }] };
delete blk.file; delete blk.image;
}
});
}
return blocks;
}
// ------------------- 5. DOM 转 Blocks (v1.2 Logic + Fix: ALLOW BUTTON) -------------------
const NOTION_LANGUAGES = new Set(["bash", "c", "c++", "css", "go", "html", "java", "javascript", "json", "python", "markdown", "sql", "typescript", "yaml", "plain text"]);
function detectLanguageRecursive(preNode) {
let c = preNode;
for (let i = 0; i < 3; i++) {
if (!c) break;
const h = c.previousElementSibling;
if (h && NOTION_LANGUAGES.has(h.innerText.toLowerCase())) return h.innerText.toLowerCase();
c = c.parentElement;
}
const code = preNode.querySelector('code');
return (code && code.className.match(/language-([\w-]+)/)) ? code.className.match(/language-([\w-]+)/)[1] : "plain text";
}
function splitCodeSafe(code) {
const chunks = [];
let remaining = code;
while (remaining.length > 0) {
if (remaining.length <= MAX_TEXT_LENGTH) { chunks.push(remaining); break; }
let splitIndex = remaining.lastIndexOf('\n', MAX_TEXT_LENGTH - 1);
if (splitIndex === -1) splitIndex = MAX_TEXT_LENGTH; else splitIndex += 1;
chunks.push(remaining.slice(0, splitIndex));
remaining = remaining.slice(splitIndex);
}
return chunks;
}
function parseInlineNodes(nodes) {
const rt = [];
function tr(n, s = {}) {
if (n.nodeType === 3) {
const fullText = n.textContent;
// v1.2 严格空行过滤
if (!fullText || !fullText.trim()) return;
for (let i = 0; i < fullText.length; i += MAX_TEXT_LENGTH) {
rt.push({
type: "text", text: { content: fullText.slice(i, i + MAX_TEXT_LENGTH), link: s.link },
annotations: { bold: !!s.bold, italic: !!s.italic, code: !!s.code, color: "default" }
});
}
} else if (n.nodeType === 1) {
const ns = { ...s };
if (['B', 'STRONG'].includes(n.tagName)) ns.bold = true;
if (['I', 'EM'].includes(n.tagName)) ns.italic = true;
if (n.tagName === 'CODE') ns.code = true;
if (n.tagName === 'A') ns.link = { url: n.href };
n.childNodes.forEach(c => tr(c, ns));
}
}
nodes.forEach(n => tr(n));
return rt;
}
// seenImages 用于 AI 图片去重
function processNodesToBlocks(nodes, seenImages = new Set()) {
const blocks = [];
const buf = [];
const flush = () => {
if (!buf.length) return;
const rt = parseInlineNodes(buf);
if (!rt.length) { buf.length = 0; return; }
for (let i = 0; i < rt.length; i += 90) {
blocks.push({ object: "block", type: "paragraph", paragraph: { rich_text: rt.slice(i, i + 90) } });
}
buf.length = 0;
};
Array.from(nodes).forEach(n => {
// 🌟 核心修复:从屏蔽列表中移除了 'BUTTON',允许脚本进入 <button> 内部查找 <img>
if (['SCRIPT', 'STYLE', 'SVG'].includes(n.nodeName)) return;
if (n.nodeType === 3 || ['B', 'I', 'CODE', 'SPAN', 'A', 'STRONG', 'EM'].includes(n.nodeName)) {
if (n.nodeName === 'A' && (n.hasAttribute('download') || n.href.includes('blob:'))) {
flush();
const fn = (n.innerText || 'file').trim();
blocks.push({ object: "block", type: "file", file: { type: "external", name: fn.slice(0, 60), external: { url: `${ASSET_PLACEHOLDER_PREFIX}${fn}::${n.href}` } } });
return;
}
buf.push(n);
return;
}
if (n.nodeType === 1) {
const t = n.tagName;
if (t === 'P' || t === 'DIV' || t === 'BUTTON') { // 🌟 显式允许 BUTTON
flush();
blocks.push(...processNodesToBlocks(n.childNodes, seenImages));
} else if (t === 'IMG') {
flush();
if (!n.className.includes('avatar') && n.src) {
// 🌟 图片去重
if (n.src.startsWith('http')) {
// 网络图片去重
if (!seenImages.has(n.src)) {
seenImages.add(n.src);
blocks.push({ object: "block", type: "image", image: { type: "external", external: { url: `${ASSET_PLACEHOLDER_PREFIX}image.png::${n.src}` } } });
}
} else {
// Blob 图片直接添加 (URL 每次不同,去重意义不大,且通常不重叠)
blocks.push({ object: "block", type: "image", image: { type: "external", external: { url: `${ASSET_PLACEHOLDER_PREFIX}image.png::${n.src}` } } });
}
}
} else if (t === 'PRE') {
flush();
const fullCode = n.textContent || "";
if (!fullCode.trim()) return;
const lang = detectLanguageRecursive(n);
const chunks = splitCodeSafe(fullCode);
const rt = chunks.map(c => ({ type: "text", text: { content: c } }));
blocks.push({ object: "block", type: "code", code: { rich_text: rt, language: lang } });
} else if (/^H[1-6]$/.test(t)) {
flush();
const rich = parseInlineNodes(n.childNodes);
if (!rich.length) return;
const hLevel = t[1] < 4 ? t[1] : 3;
const hType = `heading_${hLevel}`;
blocks.push({ object: "block", type: hType, [hType]: { rich_text: rich } });
} else if (t === 'BLOCKQUOTE') {
flush();
const rich = parseInlineNodes(n.childNodes);
if (!rich.length) return;
blocks.push({ object: "block", type: "quote", quote: { rich_text: rich } });
} else if (t === 'HR') {
flush();
blocks.push({ object: "block", type: "divider", divider: {} });
} else if (t === 'UL' || t === 'OL') {
flush();
const tp = t === 'UL' ? 'bulleted_list_item' : 'numbered_list_item';
Array.from(n.children).forEach(li => {
if (li.tagName === 'LI') {
const rich = parseInlineNodes(li.childNodes);
if (!rich.length) return;
blocks.push({ object: "block", type: tp, [tp]: { rich_text: rich } });
}
});
} else if (t === 'TABLE') {
flush();
const rows = Array.from(n.querySelectorAll('tr'));
if (rows.length) {
const tb = { object: "block", type: "table", table: { table_width: 1, children: [] } };
let max = 0;
rows.forEach(r => {
const cs = Array.from(r.querySelectorAll('td,th'));
max = Math.max(max, cs.length);
tb.table.children.push({
object: "block", type: "table_row",
table_row: { cells: cs.map(c => [{ type: "text", text: { content: c.innerText.trim().slice(0, 1000) } }]) }
});
});
tb.table.table_width = max;
blocks.push(tb);
}
} else {
blocks.push(...processNodesToBlocks(n.childNodes, seenImages));
}
}
});
flush();
return blocks;
}
// ------------------- 6. 导出 -------------------
function getChatBlocks(targetTurns = null) {
const allTurns = getTurnWrappers();
const turnsToProcess = targetTurns || allTurns;
const children = [];
turnsToProcess.forEach(turn => {
const role = getRoleFromWrapper(turn);
const isUser = role === 'user';
const label = isUser ? 'User' : 'ChatGPT';
if (turn.getAttribute('data-privacy-skip') === 'true') {
children.push({
object: "block", type: "callout",
callout: {
rich_text: [{ type: "text", text: { content: `🚫 此 ${label} 内容已标记为隐私,未导出。` }, annotations: { color: "gray", italic: true } }],
icon: { emoji: "🔒" }, color: "gray_background"
}
});
return;
}
children.push({
object: "block", type: "heading_3",
heading_3: { rich_text: [{ type: "text", text: { content: label } }], color: isUser ? "default" : "blue_background" }
});
const clone = turn.cloneNode(true);
clone.querySelectorAll('.cgpt-tool-group').forEach(el => el.remove());
children.push(...processNodesToBlocks(clone.childNodes, new Set()));
children.push({ object: "block", type: "divider", divider: {} });
});
return children;
}
function getChatTitle(specificTurn = null) {
const all = getTurnWrappers();
const el = specificTurn || (all.find(t => getRoleFromWrapper(t) === 'user') || all[0]);
return el ? el.innerText.replace(/\n/g, ' ').trim().slice(0, 60) : 'ChatGPT Chat';
}
function appendBlocksBatch(pageId, blocks, token, statusCallback) {
if (!blocks.length) {
statusCallback('✅ Saved!');
setTimeout(() => statusCallback(null), 3000);
return;
}
GM_xmlhttpRequest({
method: 'PATCH',
url: `https://api.notion.com/v1/blocks/${pageId}/children`,
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', 'Notion-Version': '2022-06-28' },
data: JSON.stringify({ children: blocks.slice(0, 90) }),
onload: (res) => {
if (res.status === 200) appendBlocksBatch(pageId, blocks.slice(90), token, statusCallback);
else { console.error(res.responseText); statusCallback('❌ Fail'); }
},
onerror: () => statusCallback('❌ Net Error')
});
}
function createPageAndUpload(title, blocks, token, dbId, statusCallback) {
GM_xmlhttpRequest({
method: 'POST',
url: 'https://api.notion.com/v1/pages',
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', 'Notion-Version': '2022-06-28' },
data: JSON.stringify({
parent: { database_id: dbId },
properties: {
'Name': { title: [{ text: { content: title } }] },
'Date': { date: { start: new Date().toISOString() } },
'URL': { url: location.href }
},
children: blocks.slice(0, 90)
}),
onload: (res) => {
if (res.status === 200) {
const page = JSON.parse(res.responseText);
appendBlocksBatch(page.id, blocks.slice(90), token, statusCallback);
} else {
statusCallback('❌ Fail'); alert(res.responseText);
}
},
onerror: () => statusCallback('❌ Net Error')
});
}
async function executeExport(blocks, title, btnOrLabel, iconElem) {
const { token, dbId } = getConfig();
if (!token || !dbId) { promptConfig(); return; }
const updateStatus = (msg) => {
if (btnOrLabel.classList && btnOrLabel.classList.contains('cgpt-icon-btn') && iconElem) {
if (msg && msg.includes('Saved')) {
btnOrLabel.classList.remove('processing'); btnOrLabel.classList.add('success'); iconElem.textContent = '✅';
setTimeout(() => { btnOrLabel.classList.remove('success'); iconElem.textContent = '📤'; }, 2500);
} else if (msg && (msg.includes('Fail') || msg.includes('Error'))) {
btnOrLabel.classList.remove('processing'); btnOrLabel.classList.add('error'); iconElem.textContent = '❌';
} else if (msg) {
btnOrLabel.classList.add('processing'); btnOrLabel.classList.remove('success', 'error'); iconElem.textContent = '⏳';
}
} else if (btnOrLabel.id === 'chatgpt-saver-btn') {
btnOrLabel.textContent = msg || '📥 Save to Notion';
}
};
if (btnOrLabel.id === 'chatgpt-saver-btn') {
btnOrLabel.classList.add('loading'); btnOrLabel.textContent = '🕵️ Processing...';
} else updateStatus('Processing...');
try {
blocks = await processAssets(blocks, updateStatus);
if (btnOrLabel.id === 'chatgpt-saver-btn') btnOrLabel.textContent = '💾 Saving...';
createPageAndUpload(title, blocks, token, dbId, updateStatus);
} catch (e) {
console.error(e);
if (btnOrLabel.id === 'chatgpt-saver-btn') btnOrLabel.textContent = '❌ Error';
updateStatus('❌ Fail'); alert(e.message);
} finally {
if (btnOrLabel.id === 'chatgpt-saver-btn') btnOrLabel.classList.remove('loading');
}
}
function handleFullExport() {
const btn = document.getElementById('chatgpt-saver-btn');
const blocks = getChatBlocks(null);
if (!blocks.length) return alert('空对话');
executeExport(blocks, getChatTitle(), btn);
}
function handleSingleExport(turnWrapper, iconBtn, iconElem) {
const all = getTurnWrappers();
const idx = all.indexOf(turnWrapper);
if (idx === -1) return alert('未找到气泡');
const targets = [turnWrapper];
const role = getRoleFromWrapper(turnWrapper);
if (role === 'user') {
for (let i = idx + 1; i < all.length; i++) {
const r = getRoleFromWrapper(all[i]);
if (r === 'assistant') {
if (all[i].getAttribute('data-privacy-skip') !== 'true') targets.push(all[i]);
break;
}
if (r === 'user') break;
}
}
const blocks = getChatBlocks(targets);
if (!blocks.length) return alert('空内容');
const title = getChatTitle(turnWrapper);
executeExport(blocks, title, iconBtn, iconElem);
}
function tryInit() {
if (!document.body) return;
if (!document.getElementById('chatgpt-saver-btn')) {
const btn = document.createElement('button');
btn.id = 'chatgpt-saver-btn'; btn.textContent = '📥 Save to Notion'; btn.onclick = handleFullExport;
document.body.appendChild(btn);
}
injectPerTurnControls();
}
setInterval(tryInit, 1500);
})();