匹配范围
*://share.note.youdao.com/**://note.youdao.com/**://share.note.youdao.cn/**://note.youdao.cn/*youdao-shared-note读取有道云笔记公开分享 分类:有道云笔记 标签:分享、笔记、内容提取 用途 从当前标签页已经打开的有道云笔记公开分享页中读取笔记标题、正文、摘要、关键词、创建时间、文件大小和页面 URL。脚本只读取当前页面 DOM 和页面中的 React 数据,不会自动跳转、刷新或等待新页面加载。 执行前提 先在浏览器中打开有道云笔记公开分享页,URL 域名必须是 sha…
*://share.note.youdao.com/**://note.youdao.com/**://share.note.youdao.cn/**://note.youdao.cn/*dom.read1const input = typeof params === 'undefined' || params == null ? {} : params;2 3const allowedHosts = {4 'share.note.youdao.com': true,5 'note.youdao.com': true,6 'share.note.youdao.cn': true,7 'note.youdao.cn': true,8};9 10function cleanText(value) {11 return String(value == null ? '' : value)12 .replace(/\u00a0/g, ' ')13 .replace(/\r\n?/g, '\n')14 .replace(/[ \t]+\n/g, '\n')15 .replace(/\n[ \t]+/g, '\n')16 .replace(/[ \t]{2,}/g, ' ')17 .replace(/\n{3,}/g, '\n\n')18 .trim();19}20 21function compactText(value) {22 return cleanText(value).replace(/\s+/g, ' ').trim();23}24 25function parseShareUrl(raw, label) {26 const value = String(raw == null ? '' : raw).trim();27 if (!value) {28 throw new Error(label + '不能为空。');29 }30 31 let parsed;32 try {33 parsed = new URL(value);34 } catch (_error) {35 throw new Error(label + '不是有效 URL。请使用有道云笔记公开分享页完整地址。');36 }37 38 if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {39 throw new Error(label + '必须使用 http 或 https。');40 }41 if (!allowedHosts[parsed.hostname]) {42 throw new Error(43 label + '域名不支持。只允许 share.note.youdao.com、note.youdao.com、share.note.youdao.cn、note.youdao.cn。',44 );45 }46 const id = parsed.searchParams.get('id');47 if (!id) {48 throw new Error(label + '必须包含 id 参数。');49 }50 const type = parsed.searchParams.get('type');51 if (type && type !== 'note') {52 throw new Error(label + '的 type=' + type + ',脚本只支持 type=note 的笔记分享。');53 }54 return parsed;55}56 57const currentUrl = parseShareUrl(window.location.href, '当前页面 URL');58if (Object.prototype.hasOwnProperty.call(input, 'url') && input.url != null && String(input.url).trim()) {59 const requestedUrl = parseShareUrl(input.url, 'params.url');60 if (requestedUrl.origin !== currentUrl.origin) {61 throw new Error('params.url 与当前标签页不同源。请先打开同一个有道云笔记分享页。');62 }63 if (requestedUrl.searchParams.get('id') !== currentUrl.searchParams.get('id')) {64 throw new Error('params.url 与当前标签页的 id 不一致。请确认当前标签页就是要读取的分享页。');65 }66}67 68function mainPageText() {69 const selectors = [70 '#root',71 'main',72 'article',73 '.note-content',74 '.file-content',75 '.file-preview',76 '.content',77 ];78 for (let index = 0; index < selectors.length; index += 1) {79 const node = document.querySelector(selectors[index]);80 const text = cleanText(node && (node.innerText || node.textContent));81 if (text) return text.slice(0, 4000);82 }83 return cleanText(document.body && (document.body.innerText || document.body.textContent)).slice(0, 4000);84}85 86function classifyPageText(text) {87 const value = compactText(text);88 const permissionPatterns = [89 /请先?登录后?(查看|访问|阅读|继续)/,90 /登录后(查看|访问|阅读|继续)/,91 /需要登录(后)?(查看|访问|阅读)?/,92 /无权(查看|访问|阅读)/,93 /没有权限/,94 /权限不足/,95 /访问受限/,96 /私密笔记/,97 /该分享仅.*(可见|访问|查看)/,98 /not authorized|forbidden|permission denied/i,99 ];100 for (let index = 0; index < permissionPatterns.length; index += 1) {101 if (permissionPatterns[index].test(value)) return 'permission';102 }103 104 const missingPatterns = [105 /分享(已)?(被)?(取消|失效|过期|不存在)/,106 /取消分享/,107 /链接(已)?(失效|过期)/,108 /笔记(已)?(删除|不存在)/,109 /文件(已)?(删除|不存在)/,110 /页面不存在/,111 /404|not found/i,112 ];113 for (let index = 0; index < missingPatterns.length; index += 1) {114 if (missingPatterns[index].test(value)) return 'missing';115 }116 return '';117}118 119function pageErrorFromText(text) {120 const kind = classifyPageText(text);121 if (kind === 'permission') {122 throw new Error('当前分享页正文显示需要登录或无权访问,请确认分享权限。');123 }124 if (kind === 'missing') {125 throw new Error('当前分享页显示分享已取消、过期或不存在。');126 }127}128 129function findStoreState(value, depth, seen) {130 if (!value || typeof value !== 'object' || depth > 12) return null;131 if (seen.indexOf(value) !== -1) return null;132 seen.push(value);133 134 if (value.content && value.content.data && typeof value.content.data === 'object') {135 return value;136 }137 if (value.storeState && typeof value.storeState === 'object') {138 const direct = findStoreState(value.storeState, depth + 1, seen);139 if (direct) return direct;140 }141 142 const keys = Object.keys(value);143 for (let index = 0; index < keys.length; index += 1) {144 const key = keys[index];145 if (key === 'window' || key === 'document' || key === 'ownerDocument') continue;146 const found = findStoreState(value[key], depth + 1, seen);147 if (found) return found;148 }149 return null;150}151 152function findStoreFromFiber(fiber) {153 const stack = [fiber];154 const seenFibers = [];155 while (stack.length) {156 const cursor = stack.pop();157 if (!cursor || seenFibers.indexOf(cursor) !== -1) continue;158 seenFibers.push(cursor);159 160 const fromState = findStoreState(cursor.memoizedState, 0, []);161 if (fromState) return fromState;162 const fromProps = findStoreState(cursor.memoizedProps, 0, []);163 if (fromProps) return fromProps;164 165 if (cursor.child) stack.push(cursor.child);166 if (cursor.sibling) stack.push(cursor.sibling);167 }168 return null;169}170 171function fibersFromNode(node) {172 const fibers = [];173 if (!node || typeof node !== 'object') return fibers;174 175 if (176 node._reactRootContainer &&177 node._reactRootContainer._internalRoot &&178 node._reactRootContainer._internalRoot.current179 ) {180 fibers.push(node._reactRootContainer._internalRoot.current);181 }182 183 const keys = Object.keys(node);184 for (let index = 0; index < keys.length; index += 1) {185 const key = keys[index];186 if (187 key.indexOf('__reactContainer$') === 0 ||188 key.indexOf('__reactFiber$') === 0 ||189 key.indexOf('__reactInternalInstance$') === 0190 ) {191 fibers.push(node[key]);192 }193 }194 return fibers;195}196 197function findStoreFromDom() {198 const roots = [];199 const root = document.querySelector('#root');200 if (root) roots.push(root);201 if (document.body && roots.indexOf(document.body) === -1) roots.push(document.body);202 203 for (let rootIndex = 0; rootIndex < roots.length; rootIndex += 1) {204 const rootNode = roots[rootIndex];205 const stack = [rootNode];206 let visited = 0;207 while (stack.length && visited < 2500) {208 const node = stack.pop();209 visited += 1;210 211 const fibers = fibersFromNode(node);212 for (let fiberIndex = 0; fiberIndex < fibers.length; fiberIndex += 1) {213 const store = findStoreFromFiber(fibers[fiberIndex]);214 if (store) return store;215 }216 217 const children = node && node.children;218 if (children) {219 for (let childIndex = children.length - 1; childIndex >= 0; childIndex -= 1) {220 stack.push(children[childIndex]);221 }222 }223 }224 }225 return null;226}227 228function pushText(parts, text) {229 const value = cleanText(text);230 if (!value) return;231 if (parts.length && parts[parts.length - 1] === value) return;232 parts.push(value);233}234 235function walkYoudaoText(node, parts, seen) {236 if (!node || typeof node !== 'object') return;237 if (seen.indexOf(node) !== -1) return;238 seen.push(node);239 240 if (typeof node[8] === 'string') {241 pushText(parts, node[8]);242 }243 244 const keys = Object.keys(node);245 for (let index = 0; index < keys.length; index += 1) {246 const value = node[keys[index]];247 if (value && typeof value === 'object') walkYoudaoText(value, parts, seen);248 }249}250 251function walkGenericText(node, parts, seen) {252 if (!node || typeof node !== 'object') return;253 if (seen.indexOf(node) !== -1) return;254 seen.push(node);255 256 const textKeys = {257 text: true,258 value: true,259 insert: true,260 plainText: true,261 title: true,262 };263 const keys = Object.keys(node);264 for (let index = 0; index < keys.length; index += 1) {265 const key = keys[index];266 const value = node[key];267 if (typeof value === 'string' && textKeys[key]) {268 pushText(parts, value);269 } else if (value && typeof value === 'object') {270 walkGenericText(value, parts, seen);271 }272 }273}274 275function htmlToText(html) {276 if (typeof DOMParser === 'undefined') return '';277 const parsed = new DOMParser().parseFromString(html, 'text/html');278 return cleanText(parsed.body && (parsed.body.innerText || parsed.body.textContent));279}280 281function parseNoteContent(rawContent) {282 const raw = String(rawContent == null ? '' : rawContent);283 if (!raw) return '';284 285 try {286 const parsed = JSON.parse(raw);287 const parts = [];288 walkYoudaoText(parsed, parts, []);289 if (!parts.length) walkGenericText(parsed, parts, []);290 return cleanText(parts.join('\n'));291 } catch (_error) {292 if (/<[a-z][\s\S]*>/i.test(raw)) {293 const text = htmlToText(raw);294 if (text) return text;295 }296 return cleanText(raw);297 }298}299 300function formatTimestamp(value) {301 if (value == null || value === '') return '';302 const numeric = Number(value);303 if (!Number.isFinite(numeric) || numeric <= 0) return String(value);304 const millis = numeric < 10000000000 ? numeric * 1000 : numeric;305 const date = new Date(millis);306 if (Number.isNaN(date.getTime())) return String(value);307 return date.toISOString();308}309 310function parseAiSummary(store) {311 const result = { summary: '', keywords: [] };312 const holder = store && store.aiSummary;313 if (!holder) return result;314 315 let payload = holder.aiSummary || holder.summary || holder.data || holder;316 if (typeof payload === 'string') {317 const trimmed = payload.trim();318 if (!trimmed) return result;319 try {320 payload = JSON.parse(trimmed);321 } catch (_error) {322 result.summary = cleanText(trimmed);323 return result;324 }325 }326 327 if (!payload || typeof payload !== 'object') return result;328 result.summary = cleanText(payload.description || payload.summary || payload.content || '');329 330 if (Array.isArray(payload.keywords)) {331 for (let index = 0; index < payload.keywords.length; index += 1) {332 const keyword = payload.keywords[index];333 if (typeof keyword === 'string') {334 pushText(result.keywords, keyword);335 } else if (keyword && typeof keyword === 'object') {336 pushText(result.keywords, ((keyword.emoji || '') + ' ' + (keyword.title || keyword.name || '')).trim());337 }338 }339 }340 return result;341}342 343function firstText(values) {344 for (let index = 0; index < values.length; index += 1) {345 const text = cleanText(values[index]);346 if (text) return text;347 }348 return '';349}350 351const visibleText = mainPageText();352pageErrorFromText(visibleText);353 354const store = findStoreFromDom();355if (!store || !store.content || !store.content.data || typeof store.content.data !== 'object') {356 pageErrorFromText(visibleText);357 throw new Error('未在当前页面找到有道云笔记内容数据,请确认页面已完成加载并且是公开笔记分享页。');358}359 360const contentData = store.content.data;361const hasContentField = Object.prototype.hasOwnProperty.call(contentData, 'content');362if (!hasContentField) {363 pageErrorFromText(visibleText);364 throw new Error('当前页面数据中没有找到完整笔记内容,请确认分享页已加载完成且不是权限受限页面。');365}366 367const title = firstText([368 contentData.tl,369 contentData.title,370 contentData.name,371 document.querySelector('.file-name') && document.querySelector('.file-name').textContent,372 document.title,373]);374 375if (!title) {376 throw new Error('已找到笔记数据,但未能读取笔记标题。');377}378 379const rawContent = String(contentData.content == null ? '' : contentData.content);380const content = parseNoteContent(rawContent);381if (rawContent && !content) {382 throw new Error('已找到笔记内容字段,但未解析出可读文本。');383}384 385const ai = parseAiSummary(store);386const createdAt = contentData.ct || contentData.createTime || contentData.createdAt || contentData.created_at || '';387const fileSize = contentData.sz || contentData.size || contentData.fileSize || contentData.file_size || '';388 389return {390 title,391 content,392 summary: ai.summary,393 keywords: ai.keywords,394 created_at: formatTimestamp(createdAt),395 file_size: fileSize == null ? '' : String(fileSize),396 url: currentUrl.href,397};