From abe5479bf99743030fe7b1b4bc1cd68cbe6c3235 Mon Sep 17 00:00:00 2001 From: Kyrie Date: Tue, 7 Apr 2026 11:47:19 +0800 Subject: [PATCH 1/3] Add 1point3acres hot adapter --- README.md | 3 +- README.zh-CN.md | 3 +- clis/1point3acres/hot.yaml | 80 +++++++++++++++++++++++++++ docs/adapters/browser/1point3acres.md | 30 ++++++++++ docs/adapters/index.md | 1 + 5 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 clis/1point3acres/hot.yaml create mode 100644 docs/adapters/browser/1point3acres.md diff --git a/README.md b/README.md index 811d068bc..8aaed6416 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,7 @@ git clone git@github.com:jackwener/opencli.git && cd opencli && npm install && n | **hupu** | `hot` `search` `detail` `mentions` `reply` `like` `unlike` | | **twitter** | `trending` `search` `timeline` `bookmarks` `post` `download` `profile` `article` `like` `likes` `notifications` `reply` `reply-dm` `thread` `follow` `unfollow` `followers` `following` `block` `unblock` `bookmark` `unbookmark` `delete` `hide-reply` `accept` | | **reddit** | `hot` `frontpage` `popular` `search` `subreddit` `read` `user` `user-posts` `user-comments` `upvote` `upvoted` `save` `saved` `comment` `subscribe` | +| **1point3acres** | `hot` | | **amazon** | `bestsellers` `search` `product` `offer` `discussion` `movers-shakers` `new-releases` | | **1688** | `search` `item` `assets` `download` `store` | | **gemini** | `new` `ask` `image` `deep-research` `deep-research-result` | @@ -141,7 +142,7 @@ git clone git@github.com:jackwener/opencli.git && cd opencli && npm install && n | **xianyu** | `search` `item` `chat` | | **xiaoe** | `courses` `detail` `catalog` `play-url` `content` | -79+ adapters in total — **[→ see all supported sites & commands](./docs/adapters/index.md)** +80+ adapters in total — **[→ see all supported sites & commands](./docs/adapters/index.md)** ## CLI Hub diff --git a/README.zh-CN.md b/README.zh-CN.md index d9b534a3c..c32341536 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -131,6 +131,7 @@ npx skills add jackwener/opencli --skill opencli-oneshot # 快速命令参 |------|------|------| | **twitter** | `trending` `bookmarks` `profile` `search` `timeline` `thread` `following` `followers` `notifications` `post` `reply` `delete` `like` `article` `follow` `unfollow` `bookmark` `unbookmark` `download` `accept` `reply-dm` `block` `unblock` `hide-reply` | 浏览器 | | **reddit** | `hot` `frontpage` `popular` `search` `subreddit` `read` `user` `user-posts` `user-comments` `upvote` `save` `comment` `subscribe` `saved` `upvoted` | 浏览器 | +| **1point3acres** | `hot` | 浏览器 | | **tieba** | `hot` `posts` `search` `read` | 浏览器 | | **hupu** | `hot` `search` `detail` `mentions` `reply` `like` `unlike` | 浏览器 | | **cursor** | `status` `send` `read` `new` `dump` `composer` `model` `extract-code` `ask` `screenshot` `history` `export` | 桌面端 | @@ -205,7 +206,7 @@ npx skills add jackwener/opencli --skill opencli-oneshot # 快速命令参 | **douyin** | `videos` `publish` `drafts` `draft` `delete` `stats` `profile` `update` `hashtag` `location` `activities` `collections` | 浏览器 | | **yuanbao** | `new` `ask` | 浏览器 | -79+ 适配器 — **[→ 查看完整命令列表](./docs/adapters/index.md)** +80+ 适配器 — **[→ 查看完整命令列表](./docs/adapters/index.md)** ### 外部 CLI 枢纽 diff --git a/clis/1point3acres/hot.yaml b/clis/1point3acres/hot.yaml new file mode 100644 index 000000000..e3ff585a0 --- /dev/null +++ b/clis/1point3acres/hot.yaml @@ -0,0 +1,80 @@ +site: 1point3acres +name: hot +description: 1Point3Acres hot topics +domain: www.1point3acres.com +strategy: cookie +browser: true +navigateBefore: false +timeout: 45 + +args: + limit: + type: int + default: 20 + description: Number of topics + +pipeline: + - navigate: https://www.1point3acres.com/bbs/forum.php?mod=guide + + - evaluate: | + (async () => { + const limit = Math.max(1, Math.min(Number(${{ args.limit }}) || 20, 100)); + const clean = (value) => (value || '').replace(/\s+/g, ' ').trim(); + + const waitForTopics = async () => { + const deadline = Date.now() + 20000; + while (Date.now() < deadline) { + if (document.querySelector('a.xst')) return; + await new Promise((resolve) => setTimeout(resolve, 500)); + } + }; + + await waitForTopics(); + + if (/just a moment/i.test(document.title) || document.querySelector('#challenge-error-text')) { + throw new Error('1Point3Acres is still showing the Cloudflare challenge; open Chrome, pass the challenge, then retry'); + } + + const rows = Array.from(document.querySelectorAll('a.xst')) + .map((link) => { + const row = link.closest('tr'); + if (!row) return null; + + const href = link.getAttribute('href') || ''; + const threadId = (href.match(/thread-(\d+)-/) || href.match(/[?&]tid=(\d+)/) || [])[1] || ''; + if (!threadId) return null; + + const byCells = Array.from(row.querySelectorAll('td.by')); + const statsCell = row.querySelector('td.num'); + + return { + id: threadId, + title: clean(link.textContent), + category: clean(byCells[0]?.textContent), + author: clean(byCells[1]?.querySelector('cite')?.textContent || ''), + created: clean(byCells[1]?.querySelector('em')?.textContent || ''), + replies: clean(statsCell?.querySelector('a.xi2')?.textContent || ''), + views: clean(statsCell?.querySelector('em')?.textContent || ''), + participants: clean(row.querySelector('th .xi1')?.textContent || ''), + last_author: clean(byCells[2]?.querySelector('cite')?.textContent || ''), + last_reply: clean(byCells[2]?.querySelector('em')?.textContent || ''), + url: new URL(href, location.href).href, + }; + }) + .filter(Boolean); + + const seen = new Set(); + const topics = rows.filter((item) => { + if (!item.title || seen.has(item.id)) return false; + seen.add(item.id); + return true; + }); + + if (!topics.length) { + throw new Error('1Point3Acres returned no topics; make sure Chrome is logged in and the Cloudflare challenge has passed'); + } + + return topics.slice(0, limit).map((item, index) => ({ rank: index + 1, ...item })); + })() + +columns: [rank, title, category, replies, views, last_reply, url] diff --git a/docs/adapters/browser/1point3acres.md b/docs/adapters/browser/1point3acres.md new file mode 100644 index 000000000..ac8f0ebbe --- /dev/null +++ b/docs/adapters/browser/1point3acres.md @@ -0,0 +1,30 @@ +# 1Point3Acres + +**Mode**: 🔐 Browser · **Domain**: `1point3acres.com` + +## Commands + +| Command | Description | +|---------|-------------| +| `opencli 1point3acres hot` | Hot forum topics | + +## Usage Examples + +```bash +# Hot topics +opencli 1point3acres hot --limit 10 + +# JSON output +opencli 1point3acres hot --limit 10 -f json +``` + +## Prerequisites + +- Chrome running with the [Browser Bridge extension](/guide/browser-bridge) installed +- Chrome logged into `1point3acres.com` +- Cloudflare challenge passed in Chrome before running the command + +## Notes + +- The command reads `https://www.1point3acres.com/bbs/forum.php?mod=guide`. +- Output columns: `rank`, `title`, `category`, `replies`, `views`, `last_reply`, `url`. diff --git a/docs/adapters/index.md b/docs/adapters/index.md index 304d16d3d..240940fb6 100644 --- a/docs/adapters/index.md +++ b/docs/adapters/index.md @@ -8,6 +8,7 @@ Run `opencli list` for the live registry. |------|----------|------| | **[twitter](./browser/twitter)** | `trending` `bookmarks` `profile` `search` `timeline` `thread` `following` `followers` `notifications` `post` `reply` `delete` `like` `likes` `article` `follow` `unfollow` `bookmark` `unbookmark` `download` `accept` `reply-dm` `block` `unblock` `hide-reply` | 🔐 Browser | | **[reddit](./browser/reddit)** | `hot` `frontpage` `popular` `search` `subreddit` `read` `user` `user-posts` `user-comments` `upvote` `save` `comment` `subscribe` `saved` `upvoted` | 🔐 Browser | +| **[1point3acres](./browser/1point3acres)** | `hot` | 🔐 Browser | | **[tieba](./browser/tieba)** | `hot` `posts` `search` `read` | 🔐 Browser | | **[hupu](./browser/hupu)** | `hot` `search` `detail` `mentions` `reply` `like` `unlike` | 🌐 / 🔐 | | **[bilibili](./browser/bilibili)** | `hot` `search` `me` `favorite` `history` `feed` `subtitle` `dynamic` `ranking` `following` `user-videos` `download` | 🔐 Browser | From b88035e558cedbe99ef74582b454913df0af0f1b Mon Sep 17 00:00:00 2001 From: Kyrie Date: Tue, 7 Apr 2026 12:03:22 +0800 Subject: [PATCH 2/3] Add 1point3acres API commands --- README.md | 2 +- README.zh-CN.md | 2 +- clis/1point3acres/forums.yaml | 30 +++++++++ clis/1point3acres/hot.yaml | 97 ++++++++------------------- clis/1point3acres/posts.yaml | 41 +++++++++++ clis/1point3acres/search.yaml | 43 ++++++++++++ clis/1point3acres/threads.yaml | 39 +++++++++++ docs/adapters/browser/1point3acres.md | 27 ++++++-- docs/adapters/index.md | 2 +- 9 files changed, 206 insertions(+), 77 deletions(-) create mode 100644 clis/1point3acres/forums.yaml create mode 100644 clis/1point3acres/posts.yaml create mode 100644 clis/1point3acres/search.yaml create mode 100644 clis/1point3acres/threads.yaml diff --git a/README.md b/README.md index 8aaed6416..e84c10236 100644 --- a/README.md +++ b/README.md @@ -132,7 +132,7 @@ git clone git@github.com:jackwener/opencli.git && cd opencli && npm install && n | **hupu** | `hot` `search` `detail` `mentions` `reply` `like` `unlike` | | **twitter** | `trending` `search` `timeline` `bookmarks` `post` `download` `profile` `article` `like` `likes` `notifications` `reply` `reply-dm` `thread` `follow` `unfollow` `followers` `following` `block` `unblock` `bookmark` `unbookmark` `delete` `hide-reply` `accept` | | **reddit** | `hot` `frontpage` `popular` `search` `subreddit` `read` `user` `user-posts` `user-comments` `upvote` `upvoted` `save` `saved` `comment` `subscribe` | -| **1point3acres** | `hot` | +| **1point3acres** | `hot` `threads` `forums` `posts` `search` | | **amazon** | `bestsellers` `search` `product` `offer` `discussion` `movers-shakers` `new-releases` | | **1688** | `search` `item` `assets` `download` `store` | | **gemini** | `new` `ask` `image` `deep-research` `deep-research-result` | diff --git a/README.zh-CN.md b/README.zh-CN.md index c32341536..a591d6923 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -131,7 +131,7 @@ npx skills add jackwener/opencli --skill opencli-oneshot # 快速命令参 |------|------|------| | **twitter** | `trending` `bookmarks` `profile` `search` `timeline` `thread` `following` `followers` `notifications` `post` `reply` `delete` `like` `article` `follow` `unfollow` `bookmark` `unbookmark` `download` `accept` `reply-dm` `block` `unblock` `hide-reply` | 浏览器 | | **reddit** | `hot` `frontpage` `popular` `search` `subreddit` `read` `user` `user-posts` `user-comments` `upvote` `save` `comment` `subscribe` `saved` `upvoted` | 浏览器 | -| **1point3acres** | `hot` | 浏览器 | +| **1point3acres** | `hot` `threads` `forums` `posts` `search` | 公开 / 浏览器 | | **tieba** | `hot` `posts` `search` `read` | 浏览器 | | **hupu** | `hot` `search` `detail` `mentions` `reply` `like` `unlike` | 浏览器 | | **cursor** | `status` `send` `read` `new` `dump` `composer` `model` `extract-code` `ask` `screenshot` `history` `export` | 桌面端 | diff --git a/clis/1point3acres/forums.yaml b/clis/1point3acres/forums.yaml new file mode 100644 index 000000000..e5f7ea14f --- /dev/null +++ b/clis/1point3acres/forums.yaml @@ -0,0 +1,30 @@ +site: 1point3acres +name: forums +description: 1Point3Acres forum list +domain: api.1point3acres.com +strategy: public +browser: false + +args: + limit: + type: int + default: 200 + description: Number of forums + +pipeline: + - fetch: + url: https://api.1point3acres.com/api/forums + + - select: forums + + - map: + fid: ${{ item.fid }} + name: ${{ item.name }} + type: ${{ item.type }} + parent_fid: ${{ item.fup }} + display_order: ${{ item.displayorder }} + today_posts: ${{ item.todayposts }} + + - limit: ${{ args.limit }} + +columns: [fid, name, type, parent_fid, display_order, today_posts] diff --git a/clis/1point3acres/hot.yaml b/clis/1point3acres/hot.yaml index e3ff585a0..bef11f147 100644 --- a/clis/1point3acres/hot.yaml +++ b/clis/1point3acres/hot.yaml @@ -1,80 +1,39 @@ site: 1point3acres name: hot description: 1Point3Acres hot topics -domain: www.1point3acres.com -strategy: cookie -browser: true -navigateBefore: false -timeout: 45 +domain: api.1point3acres.com +strategy: public +browser: false args: limit: type: int default: 20 description: Number of topics + page: + type: int + default: 1 + description: Page number pipeline: - - navigate: https://www.1point3acres.com/bbs/forum.php?mod=guide - - - evaluate: | - (async () => { - const limit = Math.max(1, Math.min(Number(${{ args.limit }}) || 20, 100)); - const clean = (value) => (value || '').replace(/\s+/g, ' ').trim(); - - const waitForTopics = async () => { - const deadline = Date.now() + 20000; - while (Date.now() < deadline) { - if (document.querySelector('a.xst')) return; - await new Promise((resolve) => setTimeout(resolve, 500)); - } - }; - - await waitForTopics(); - - if (/just a moment/i.test(document.title) || document.querySelector('#challenge-error-text')) { - throw new Error('1Point3Acres is still showing the Cloudflare challenge; open Chrome, pass the challenge, then retry'); - } - - const rows = Array.from(document.querySelectorAll('a.xst')) - .map((link) => { - const row = link.closest('tr'); - if (!row) return null; - - const href = link.getAttribute('href') || ''; - const threadId = (href.match(/thread-(\d+)-/) || href.match(/[?&]tid=(\d+)/) || [])[1] || ''; - if (!threadId) return null; - - const byCells = Array.from(row.querySelectorAll('td.by')); - const statsCell = row.querySelector('td.num'); - - return { - id: threadId, - title: clean(link.textContent), - category: clean(byCells[0]?.textContent), - author: clean(byCells[1]?.querySelector('cite')?.textContent || ''), - created: clean(byCells[1]?.querySelector('em')?.textContent || ''), - replies: clean(statsCell?.querySelector('a.xi2')?.textContent || ''), - views: clean(statsCell?.querySelector('em')?.textContent || ''), - participants: clean(row.querySelector('th .xi1')?.textContent || ''), - last_author: clean(byCells[2]?.querySelector('cite')?.textContent || ''), - last_reply: clean(byCells[2]?.querySelector('em')?.textContent || ''), - url: new URL(href, location.href).href, - }; - }) - .filter(Boolean); - - const seen = new Set(); - const topics = rows.filter((item) => { - if (!item.title || seen.has(item.id)) return false; - seen.add(item.id); - return true; - }); - - if (!topics.length) { - throw new Error('1Point3Acres returned no topics; make sure Chrome is logged in and the Cloudflare challenge has passed'); - } - - return topics.slice(0, limit).map((item, index) => ({ rank: index + 1, ...item })); - })() - -columns: [rank, title, category, replies, views, last_reply, url] + - fetch: + url: https://api.1point3acres.com/api/threads + params: + ps: ${{ args.limit }} + page: ${{ args.page }} + + - select: threads + + - map: + rank: ${{ index + 1 }} + tid: ${{ item.tid }} + title: ${{ item.subject }} + forum: ${{ item.forum_name }} + fid: ${{ item.fid }} + author: ${{ item.author }} + replies: ${{ item.replies }} + views: ${{ item.views }} + heats: ${{ item.heats }} + url: https://www.1point3acres.com/bbs/thread-${{ item.tid }}-1-1.html + +columns: [rank, title, forum, author, replies, views, heats, url] diff --git a/clis/1point3acres/posts.yaml b/clis/1point3acres/posts.yaml new file mode 100644 index 000000000..05c7ed36b --- /dev/null +++ b/clis/1point3acres/posts.yaml @@ -0,0 +1,41 @@ +site: 1point3acres +name: posts +description: 1Point3Acres thread posts (login required) +domain: api.1point3acres.com +strategy: cookie +browser: true + +args: + tid: + type: str + required: true + positional: true + description: Thread ID + limit: + type: int + default: 20 + description: Number of posts + page: + type: int + default: 1 + description: Page number + +pipeline: + - fetch: + url: https://api.1point3acres.com/api/threads/${{ args.tid }}/posts + method: POST + params: + ps: ${{ args.limit }} + page: ${{ args.page }} + + - select: posts + + - map: + floor: ${{ item.position || item.number || index + 1 }} + pid: ${{ item.pid }} + author: ${{ item.author || item.username || item.user?.username || item.user?.name || '' }} + author_id: ${{ item.authorid || item.user?.uid || item.user_id || '' }} + created_at: ${{ item.dateline || item.created_at || item.create_time || '' }} + content: ${{ item.message || item.content || item.text || item.markdown || '' }} + +columns: [floor, author, created_at, content] diff --git a/clis/1point3acres/search.yaml b/clis/1point3acres/search.yaml new file mode 100644 index 000000000..2f6b9cfe0 --- /dev/null +++ b/clis/1point3acres/search.yaml @@ -0,0 +1,43 @@ +site: 1point3acres +name: search +description: 1Point3Acres search (login required) +domain: api.1point3acres.com +strategy: cookie +browser: true + +args: + query: + type: str + required: true + positional: true + description: Search keyword + limit: + type: int + default: 20 + description: Number of results + page: + type: int + default: 1 + description: Page number + +pipeline: + - fetch: + url: https://api.1point3acres.com/api/search + params: + keyword: ${{ args.query }} + page: ${{ args.page }} + ps: ${{ args.limit }} + + - select: threads + + - map: + rank: ${{ index + 1 }} + tid: ${{ item.tid }} + title: ${{ item.subject || item.title }} + forum: ${{ item.forum_name || item.forum }} + author: ${{ item.author || item.username || '' }} + replies: ${{ item.replies }} + views: ${{ item.views }} + url: https://www.1point3acres.com/bbs/thread-${{ item.tid }}-1-1.html + +columns: [rank, title, forum, author, replies, views, url] diff --git a/clis/1point3acres/threads.yaml b/clis/1point3acres/threads.yaml new file mode 100644 index 000000000..cc61b4f11 --- /dev/null +++ b/clis/1point3acres/threads.yaml @@ -0,0 +1,39 @@ +site: 1point3acres +name: threads +description: 1Point3Acres hot thread list +domain: api.1point3acres.com +strategy: public +browser: false + +args: + limit: + type: int + default: 20 + description: Number of threads + page: + type: int + default: 1 + description: Page number + +pipeline: + - fetch: + url: https://api.1point3acres.com/api/threads + params: + ps: ${{ args.limit }} + page: ${{ args.page }} + + - select: threads + + - map: + rank: ${{ index + 1 }} + tid: ${{ item.tid }} + title: ${{ item.subject }} + forum: ${{ item.forum_name }} + fid: ${{ item.fid }} + author: ${{ item.author }} + replies: ${{ item.replies }} + views: ${{ item.views }} + heats: ${{ item.heats }} + url: https://www.1point3acres.com/bbs/thread-${{ item.tid }}-1-1.html + +columns: [rank, title, forum, author, replies, views, heats, url] diff --git a/docs/adapters/browser/1point3acres.md b/docs/adapters/browser/1point3acres.md index ac8f0ebbe..831f79c0f 100644 --- a/docs/adapters/browser/1point3acres.md +++ b/docs/adapters/browser/1point3acres.md @@ -7,6 +7,10 @@ | Command | Description | |---------|-------------| | `opencli 1point3acres hot` | Hot forum topics | +| `opencli 1point3acres threads` | Hot thread list from the public API | +| `opencli 1point3acres forums` | Public forum list | +| `opencli 1point3acres posts ` | Thread posts (auth required) | +| `opencli 1point3acres search ` | Search threads (auth required) | ## Usage Examples @@ -14,17 +18,30 @@ # Hot topics opencli 1point3acres hot --limit 10 +# Equivalent public API thread list +opencli 1point3acres threads --limit 10 --page 1 + +# List forum IDs and names +opencli 1point3acres forums + +# Read thread posts (requires login) +opencli 1point3acres posts 1171864 --limit 20 + +# Search (requires login) +opencli 1point3acres search "USC CS" + # JSON output opencli 1point3acres hot --limit 10 -f json ``` ## Prerequisites -- Chrome running with the [Browser Bridge extension](/guide/browser-bridge) installed -- Chrome logged into `1point3acres.com` -- Cloudflare challenge passed in Chrome before running the command +- `hot`, `threads`, and `forums`: no browser or login required. +- `posts` and `search`: Chrome running with the [Browser Bridge extension](/guide/browser-bridge) installed and logged into `1point3acres.com`. ## Notes -- The command reads `https://www.1point3acres.com/bbs/forum.php?mod=guide`. -- Output columns: `rank`, `title`, `category`, `replies`, `views`, `last_reply`, `url`. +- Public commands use `https://api.1point3acres.com/api/forums` and `https://api.1point3acres.com/api/threads`. +- `posts` uses `POST https://api.1point3acres.com/api/threads//posts`. +- `search` uses `GET https://api.1point3acres.com/api/search`. +- The `fid` parameter on `threads` is not exposed because the upstream API currently ignores it. diff --git a/docs/adapters/index.md b/docs/adapters/index.md index 240940fb6..41d519e6d 100644 --- a/docs/adapters/index.md +++ b/docs/adapters/index.md @@ -8,7 +8,7 @@ Run `opencli list` for the live registry. |------|----------|------| | **[twitter](./browser/twitter)** | `trending` `bookmarks` `profile` `search` `timeline` `thread` `following` `followers` `notifications` `post` `reply` `delete` `like` `likes` `article` `follow` `unfollow` `bookmark` `unbookmark` `download` `accept` `reply-dm` `block` `unblock` `hide-reply` | 🔐 Browser | | **[reddit](./browser/reddit)** | `hot` `frontpage` `popular` `search` `subreddit` `read` `user` `user-posts` `user-comments` `upvote` `save` `comment` `subscribe` `saved` `upvoted` | 🔐 Browser | -| **[1point3acres](./browser/1point3acres)** | `hot` | 🔐 Browser | +| **[1point3acres](./browser/1point3acres)** | `hot` `threads` `forums` `posts` `search` | 🌐 / 🔐 | | **[tieba](./browser/tieba)** | `hot` `posts` `search` `read` | 🔐 Browser | | **[hupu](./browser/hupu)** | `hot` `search` `detail` `mentions` `reply` `like` `unlike` | 🌐 / 🔐 | | **[bilibili](./browser/bilibili)** | `hot` `search` `me` `favorite` `history` `feed` `subtitle` `dynamic` `ranking` `following` `user-videos` `download` | 🔐 Browser | From 6e0cacd98c7ac799bc3337dd225b59b2e5f5aee0 Mon Sep 17 00:00:00 2001 From: Kyrie Date: Tue, 7 Apr 2026 13:28:24 +0800 Subject: [PATCH 3/3] Fix 1Point3Acres logged-in commands --- clis/1point3acres/posts.ts | 114 ++++++++++++++++++++++++++++++++++ clis/1point3acres/posts.yaml | 41 ------------ clis/1point3acres/search.ts | 93 +++++++++++++++++++++++++++ clis/1point3acres/search.yaml | 43 ------------- 4 files changed, 207 insertions(+), 84 deletions(-) create mode 100644 clis/1point3acres/posts.ts delete mode 100644 clis/1point3acres/posts.yaml create mode 100644 clis/1point3acres/search.ts delete mode 100644 clis/1point3acres/search.yaml diff --git a/clis/1point3acres/posts.ts b/clis/1point3acres/posts.ts new file mode 100644 index 000000000..cf7e93425 --- /dev/null +++ b/clis/1point3acres/posts.ts @@ -0,0 +1,114 @@ +import { cli, Strategy } from '@jackwener/opencli/registry'; +import { CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors'; +import type { IPage } from '@jackwener/opencli/types'; + +type PostRow = { + floor: number; + pid: string; + author: string; + created_at: string; + content: string; +}; + +function requirePositiveInt(value: unknown, fallback: number): number { + const parsed = Number(value ?? fallback); + return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback; +} + +cli({ + site: '1point3acres', + name: 'posts', + description: '1Point3Acres thread posts (login required)', + strategy: Strategy.COOKIE, + browser: true, + args: [ + { name: 'tid', type: 'str', required: true, positional: true, help: 'Thread ID' }, + { name: 'limit', type: 'int', default: 20, help: 'Number of posts' }, + { name: 'page', type: 'int', default: 1, help: 'Page number' }, + ], + columns: ['floor', 'author', 'created_at', 'content'], + func: async (page: IPage | null, args) => { + if (!page) throw new CommandExecutionError('Browser session required for 1point3acres posts'); + + const tid = String(args.tid ?? '').trim(); + if (!/^\d+$/.test(tid)) { + throw new CommandExecutionError(`Invalid 1Point3Acres thread ID: ${tid}`); + } + + const limit = requirePositiveInt(args.limit, 20); + const pageNum = requirePositiveInt(args.page, 1); + await page.goto(`https://www.1point3acres.com/bbs/thread-${tid}-${pageNum}-1.html`); + + const result = await page.evaluate(`(() => { + const limit = ${JSON.stringify(limit)}; + const pageNum = ${JSON.stringify(pageNum)}; + const clean = (value) => String(value || '') + .replace(/\\s+/g, ' ') + .replace(/\\bwindow\\.[^\\n]+/g, '') + .trim(); + const textOf = (root, selector) => { + const el = root.querySelector(selector); + return el ? clean(el.textContent) : ''; + }; + const sanitizeAuthor = (value) => clean(value) + .replace(/^[^\\w\\u4e00-\\u9fff]+\\s*/, '') + .replace(/\\s*发消息\\s*$/, '') + .split('|')[0] + .replace(/\\s+\\d+\\s*(?:秒|分钟|小时|天)前.*$/, '') + .trim(); + const findAuthor = (post) => { + const selectors = [ + '.pls .xw1 a', + '.pls .xw1', + '.authi a[href*="space-uid"]', + '.authi a[href*="mod=space"]', + '.authi a[href*="/next/contact-post/"]', + '.authi' + ]; + for (const selector of selectors) { + const text = textOf(post, selector); + const author = sanitizeAuthor(text); + if (author && !/^#?$/.test(author)) return author; + } + return ''; + }; + const findCreatedAt = (post) => { + const authText = textOf(post, '.pti .authi, .authi'); + const match = authText.match(/(\\d+\\s*(?:秒|分钟|小时|天)前|\\d{4}[-/]\\d{1,2}[-/]\\d{1,2}[^|\\n]*)/); + return match ? clean(match[1]) : ''; + }; + const rows = []; + for (const post of Array.from(document.querySelectorAll('div[id^="post_"]'))) { + const rawId = post.id || ''; + if (!/^post_\\d+$/.test(rawId)) continue; + const pid = rawId.slice('post_'.length); + const contentEl = post.querySelector('#postmessage_' + pid) || post.querySelector('.t_f') || post.querySelector('.pcb'); + const content = contentEl ? clean(contentEl.textContent) : ''; + if (!content) continue; + rows.push({ + floor: (pageNum - 1) * limit + rows.length + 1, + pid, + author: findAuthor(post), + created_at: findCreatedAt(post), + content: content.slice(0, 500) + }); + if (rows.length >= limit) break; + } + const permissionText = document.body?.innerText || ''; + return { rows, permissionText: permissionText.slice(0, 5000) }; + })()`) as { rows?: PostRow[]; permissionText?: string }; + + if (result.permissionText?.includes('无法进行此操作')) { + throw new CommandExecutionError( + '1Point3Acres refused access for this account.', + 'The current 1Point3Acres user group does not have permission to read this thread.', + ); + } + + if (!result.rows?.length) { + throw new EmptyResultError('1point3acres posts', 'No posts found. Check the thread ID and account permissions.'); + } + + return result.rows; + }, +}); diff --git a/clis/1point3acres/posts.yaml b/clis/1point3acres/posts.yaml deleted file mode 100644 index 05c7ed36b..000000000 --- a/clis/1point3acres/posts.yaml +++ /dev/null @@ -1,41 +0,0 @@ -site: 1point3acres -name: posts -description: 1Point3Acres thread posts (login required) -domain: api.1point3acres.com -strategy: cookie -browser: true - -args: - tid: - type: str - required: true - positional: true - description: Thread ID - limit: - type: int - default: 20 - description: Number of posts - page: - type: int - default: 1 - description: Page number - -pipeline: - - fetch: - url: https://api.1point3acres.com/api/threads/${{ args.tid }}/posts - method: POST - params: - ps: ${{ args.limit }} - page: ${{ args.page }} - - - select: posts - - - map: - floor: ${{ item.position || item.number || index + 1 }} - pid: ${{ item.pid }} - author: ${{ item.author || item.username || item.user?.username || item.user?.name || '' }} - author_id: ${{ item.authorid || item.user?.uid || item.user_id || '' }} - created_at: ${{ item.dateline || item.created_at || item.create_time || '' }} - content: ${{ item.message || item.content || item.text || item.markdown || '' }} - -columns: [floor, author, created_at, content] diff --git a/clis/1point3acres/search.ts b/clis/1point3acres/search.ts new file mode 100644 index 000000000..2ec1afdf1 --- /dev/null +++ b/clis/1point3acres/search.ts @@ -0,0 +1,93 @@ +import { cli, Strategy } from '@jackwener/opencli/registry'; +import { CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors'; +import type { IPage } from '@jackwener/opencli/types'; + +type SearchThread = { + tid?: number | string; + subject?: string; + title?: string; + forum_name?: string; + forum?: string; + author?: string; + username?: string; + replies?: number | string; + views?: number | string; +}; + +type SearchPayload = { + errno?: number; + msg?: string; + threads?: SearchThread[]; +}; + +function positiveInt(value: unknown, fallback: number): number { + const parsed = Number(value ?? fallback); + return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback; +} + +cli({ + site: '1point3acres', + name: 'search', + description: '1Point3Acres search (login and search permission required)', + strategy: Strategy.COOKIE, + browser: true, + args: [ + { name: 'query', type: 'str', required: true, positional: true, help: 'Search keyword' }, + { name: 'limit', type: 'int', default: 20, help: 'Number of results' }, + { name: 'page', type: 'int', default: 1, help: 'Page number' }, + ], + columns: ['rank', 'title', 'forum', 'author', 'replies', 'views', 'url'], + func: async (page: IPage | null, args) => { + if (!page) throw new CommandExecutionError('Browser session required for 1point3acres search'); + + const query = String(args.query ?? '').trim(); + if (!query) throw new CommandExecutionError('1Point3Acres search query cannot be empty'); + + const limit = positiveInt(args.limit, 20); + const pageNum = positiveInt(args.page, 1); + + await page.goto('https://api.1point3acres.com/api/threads'); + const payload = await page.evaluate(`(async () => { + const url = 'https://api.1point3acres.com/api/search' + + '?keyword=' + encodeURIComponent(${JSON.stringify(query)}) + + '&page=' + encodeURIComponent(${JSON.stringify(pageNum)}) + + '&ps=' + encodeURIComponent(${JSON.stringify(limit)}); + const res = await fetch(url, { credentials: 'include' }); + const text = await res.text(); + let body; + try { body = JSON.parse(text); } catch { body = { errno: -1, msg: text.slice(0, 200) }; } + return { status: res.status, body }; + })()`) as { status?: number; body?: SearchPayload }; + + if (payload.status && payload.status >= 400) { + throw new CommandExecutionError(`1Point3Acres search failed with HTTP ${payload.status}`); + } + if (payload.body?.errno && payload.body.errno !== 0) { + throw new CommandExecutionError(`1Point3Acres search failed: ${payload.body.msg ?? payload.body.errno}`); + } + + const threads = payload.body?.threads ?? []; + if (threads.length === 0) { + await page.goto(`https://www.1point3acres.com/bbs/search.php?mod=forum&searchsubmit=yes&kw=${encodeURIComponent(query)}`); + const permissionText = await page.evaluate('() => document.body?.innerText?.slice(0, 5000) || ""') as string; + if (permissionText.includes('无法进行此操作') || permissionText.includes('所在的用户组')) { + throw new CommandExecutionError( + '1Point3Acres search is not available for the current account.', + 'The logged-in account user group does not have permission to use site search yet.', + ); + } + throw new EmptyResultError('1point3acres search', `No threads found for query "${query}".`); + } + + return threads.slice(0, limit).map((item, index) => ({ + rank: index + 1, + tid: item.tid, + title: item.subject || item.title || '', + forum: item.forum_name || item.forum || '', + author: item.author || item.username || '', + replies: item.replies ?? '', + views: item.views ?? '', + url: item.tid ? `https://www.1point3acres.com/bbs/thread-${item.tid}-1-1.html` : '', + })); + }, +}); diff --git a/clis/1point3acres/search.yaml b/clis/1point3acres/search.yaml deleted file mode 100644 index 2f6b9cfe0..000000000 --- a/clis/1point3acres/search.yaml +++ /dev/null @@ -1,43 +0,0 @@ -site: 1point3acres -name: search -description: 1Point3Acres search (login required) -domain: api.1point3acres.com -strategy: cookie -browser: true - -args: - query: - type: str - required: true - positional: true - description: Search keyword - limit: - type: int - default: 20 - description: Number of results - page: - type: int - default: 1 - description: Page number - -pipeline: - - fetch: - url: https://api.1point3acres.com/api/search - params: - keyword: ${{ args.query }} - page: ${{ args.page }} - ps: ${{ args.limit }} - - - select: threads - - - map: - rank: ${{ index + 1 }} - tid: ${{ item.tid }} - title: ${{ item.subject || item.title }} - forum: ${{ item.forum_name || item.forum }} - author: ${{ item.author || item.username || '' }} - replies: ${{ item.replies }} - views: ${{ item.views }} - url: https://www.1point3acres.com/bbs/thread-${{ item.tid }}-1-1.html - -columns: [rank, title, forum, author, replies, views, url]