diff --git a/src/clis/notion/README.md b/src/clis/notion/README.md index 8cb86aa..6eb729c 100644 --- a/src/clis/notion/README.md +++ b/src/clis/notion/README.md @@ -25,4 +25,5 @@ export OPENCLI_CDP_ENDPOINT="http://127.0.0.1:9230" | `notion new "title"` | Create a new page (Cmd+N) | | `notion write "text"` | Append text to the current page | | `notion sidebar` | List pages from the sidebar | +| `notion favorites` | List pages from the Favorites section | | `notion export` | Export page as Markdown | diff --git a/src/clis/notion/README.zh-CN.md b/src/clis/notion/README.zh-CN.md index 8c8ba5d..b182efc 100644 --- a/src/clis/notion/README.zh-CN.md +++ b/src/clis/notion/README.zh-CN.md @@ -25,4 +25,5 @@ export OPENCLI_CDP_ENDPOINT="http://127.0.0.1:9230" | `notion new "标题"` | 新建页面(Cmd+N) | | `notion write "文本"` | 在当前页面追加文字 | | `notion sidebar` | 列出侧边栏页面列表 | +| `notion favorites` | 列出收藏夹页面列表 | | `notion export` | 导出页面为 Markdown | diff --git a/src/clis/notion/favorites.ts b/src/clis/notion/favorites.ts new file mode 100644 index 0000000..c635f59 --- /dev/null +++ b/src/clis/notion/favorites.ts @@ -0,0 +1,79 @@ +import { cli, Strategy } from '../../registry.js'; +import type { IPage } from '../../types.js'; + +export const favoritesCommand = cli({ + site: 'notion', + name: 'favorites', + description: 'List pages from the Notion Favorites section in the sidebar', + domain: 'localhost', + strategy: Strategy.UI, + browser: true, + args: [], + columns: ['Index', 'Title', 'Icon'], + func: async (page: IPage) => { + const items = await page.evaluate(` + (function() { + const results = []; + + // Strategy 1: Find the "Favorites" header and collect its sibling tree items + const allHeaders = document.querySelectorAll( + '[class*="sidebar"] [role="heading"], [class*="sidebar"] [class*="sectionHeader"], [class*="sidebar"] [class*="header"]' + ); + + let favSection = null; + for (const h of allHeaders) { + const text = (h.textContent || '').trim().toLowerCase(); + if (text === 'favorites' || text === '收藏' || text === '收藏夹') { + favSection = h; + break; + } + } + + if (favSection) { + // Walk up to find the section container, then get all tree items + let container = favSection.closest('[role="group"], [class*="section"], [class*="favorites"]'); + if (!container) container = favSection.parentElement; + if (container) { + const treeItems = container.querySelectorAll('[role="treeitem"], [role="button"], a[href]'); + treeItems.forEach((item, i) => { + const text = (item.textContent || '').trim().substring(0, 120); + // Skip the header itself + if (text && text.length > 1 && !text.toLowerCase().match(/^(favorites|收藏夹?)$/)) { + // Try to extract emoji/icon + const iconEl = item.querySelector('[class*="icon"], [class*="emoji"], [role="img"]'); + const icon = iconEl ? (iconEl.textContent || iconEl.getAttribute('aria-label') || '').trim() : ''; + results.push({ Index: results.length + 1, Title: text, Icon: icon || '📄' }); + } + }); + } + } + + // Strategy 2: Try data attributes or direct class-based selectors + if (results.length === 0) { + const favContainers = document.querySelectorAll( + '[class*="favorite"], [data-testid*="favorite"], [class*="Favorite"]' + ); + for (const container of favContainers) { + const items = container.querySelectorAll('[role="treeitem"], [role="button"], a'); + items.forEach((item, i) => { + const text = (item.textContent || '').trim().substring(0, 120); + if (text && text.length > 1) { + const iconEl = item.querySelector('[class*="icon"], [class*="emoji"], [role="img"]'); + const icon = iconEl ? (iconEl.textContent || iconEl.getAttribute('aria-label') || '').trim() : ''; + results.push({ Index: results.length + 1, Title: text, Icon: icon || '📄' }); + } + }); + if (results.length > 0) break; + } + } + + return results; + })() + `); + + if (items.length === 0) { + return [{ Index: 0, Title: 'No favorites found. Make sure sidebar is visible and you have favorites.', Icon: '⚠️' }]; + } + return items; + }, +});