灵猴市集Linghou Market

读取有道云笔记公开分享

有道云笔记youdao-shared-note

读取有道云笔记公开分享 分类:有道云笔记 标签:分享、笔记、内容提取 用途 从当前标签页已经打开的有道云笔记公开分享页中读取笔记标题、正文、摘要、关键词、创建时间、文件大小和页面 URL。脚本只读取当前页面 DOM 和页面中的 React 数据,不会自动跳转、刷新或等待新页面加载。 执行前提 先在浏览器中打开有道云笔记公开分享页,URL 域名必须是 sha…

分享笔记内容提取
版本1.0.0
扫描passed
更新2026-06-28
超时30000ms

匹配范围

*://share.note.youdao.com/**://note.youdao.com/**://share.note.youdao.cn/**://note.youdao.cn/*

排除范围

未声明

能力声明

dom.read

代码

源码已展开
1const 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};