Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` `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` |
Expand All @@ -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

Expand Down
3 changes: 2 additions & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` `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` | 桌面端 |
Expand Down Expand Up @@ -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 枢纽

Expand Down
30 changes: 30 additions & 0 deletions clis/1point3acres/forums.yaml
Original file line number Diff line number Diff line change
@@ -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]
39 changes: 39 additions & 0 deletions clis/1point3acres/hot.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
site: 1point3acres
name: hot
description: 1Point3Acres hot topics
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:
- 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]
114 changes: 114 additions & 0 deletions clis/1point3acres/posts.ts
Original file line number Diff line number Diff line change
@@ -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;
},
});
93 changes: 93 additions & 0 deletions clis/1point3acres/search.ts
Original file line number Diff line number Diff line change
@@ -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` : '',
}));
},
});
39 changes: 39 additions & 0 deletions clis/1point3acres/threads.yaml
Original file line number Diff line number Diff line change
@@ -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]
Loading