Skip to content
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ Run `opencli list` for the live registry.
| **doubao-app** | `status` `new` `send` `read` `ask` `screenshot` `dump` | Desktop |
| **notion** | `status` `search` `read` `new` `write` `sidebar` `favorites` `export` | Desktop |
| **discord-app** | `status` `send` `read` `channels` `servers` `search` `members` | Desktop |
| **v2ex** | `hot` `latest` `topic` `daily` `me` `notifications` | Public / Browser |
| **v2ex** | `hot` `latest` `topic` `node` `user` `member` `replies` `nodes` `daily` `me` `notifications` | Public / Browser |
| **xueqiu** | `feed` `hot-stock` `hot` `search` `stock` `watchlist` `earnings-date` | Browser |
| **antigravity** | `status` `send` `read` `new` `dump` `extract-code` `model` `watch` `serve` | Desktop |
| **chatgpt** | `status` `new` `send` `read` `ask` | Desktop |
Expand Down
2 changes: 1 addition & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ npm install -g @jackwener/opencli@latest
| **doubao-app** | `status` `new` `send` `read` `ask` `screenshot` `dump` | ๆกŒ้ข็ซฏ |
| **notion** | `status` `search` `read` `new` `write` `sidebar` `favorites` `export` | ๆกŒ้ข็ซฏ |
| **discord-app** | `status` `send` `read` `channels` `servers` `search` `members` | ๆกŒ้ข็ซฏ |
| **v2ex** | `hot` `latest` `topic` `daily` `me` `notifications` | ๅ…ฌๅผ€ / ๆต่งˆๅ™จ |
| **v2ex** | `hot` `latest` `topic` `node` `user` `member` `replies` `nodes` `daily` `me` `notifications` | ๅ…ฌๅผ€ / ๆต่งˆๅ™จ |
| **xueqiu** | `feed` `hot-stock` `hot` `search` `stock` `watchlist` `earnings-date` | ๆต่งˆๅ™จ |
| **antigravity** | `status` `send` `read` `new` `dump` `extract-code` `model` `watch` `serve` | ๆกŒ้ข็ซฏ |
| **chatgpt** | `status` `new` `send` `read` `ask` | ๆกŒ้ข็ซฏ |
Expand Down
41 changes: 31 additions & 10 deletions docs/adapters/browser/v2ex.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,48 @@

| Command | Description |
|---------|-------------|
| `opencli v2ex hot` | |
| `opencli v2ex latest` | |
| `opencli v2ex topic` | |
| `opencli v2ex daily` | |
| `opencli v2ex me` | |
| `opencli v2ex notifications` | |
| `opencli v2ex hot` | Hot topics |
| `opencli v2ex latest` | Latest topics |
| `opencli v2ex topic <id>` | Topic detail |
| `opencli v2ex node <name>` | Topics by node |
| `opencli v2ex user <username>` | Topics by user |
| `opencli v2ex member <username>` | User profile |
| `opencli v2ex replies <id>` | Topic replies |
| `opencli v2ex nodes` | All nodes (sorted by topic count) |
| `opencli v2ex daily` | Daily hot |
| `opencli v2ex me` | My profile (auth required) |
| `opencli v2ex notifications` | My notifications (auth required) |

## Usage Examples

```bash
# Quick start
# Hot topics
opencli v2ex hot --limit 5

# Browse topics in a node
opencli v2ex node python

# View topic replies
opencli v2ex replies 1000

# User's topics
opencli v2ex user Livid

# User profile
opencli v2ex member Livid

# List all nodes
opencli v2ex nodes --limit 10

# JSON output
opencli v2ex hot -f json

# Verbose mode
opencli v2ex hot -v
```

## Prerequisites

Most commands (`hot`, `latest`, `topic`, `node`, `user`, `member`, `replies`, `nodes`) use the public V2EX API and **require no browser or login**.

For `daily`, `me`, and `notifications`:

- Chrome running and **logged into** v2ex.com
- [Browser Bridge extension](/guide/browser-bridge) installed
2 changes: 1 addition & 1 deletion docs/adapters/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Run `opencli list` for the live registry.
| **[xiaohongshu](/adapters/browser/xiaohongshu)** | `search` `notifications` `feed` `me` `user` `download` `publish` | ๐Ÿ” Browser |
| **[xueqiu](/adapters/browser/xueqiu)** | `feed` `hot-stock` `hot` `search` `stock` `watchlist` | ๐Ÿ” Browser |
| **[youtube](/adapters/browser/youtube)** | `search` `video` `transcript` | ๐Ÿ” Browser |
| **[v2ex](/adapters/browser/v2ex)** | `hot` `latest` `topic` `daily` `me` `notifications` | ๐ŸŒ / ๐Ÿ” |
| **[v2ex](/adapters/browser/v2ex)** | `hot` `latest` `topic` `node` `user` `member` `replies` `nodes` `daily` `me` `notifications` | ๐ŸŒ / ๐Ÿ” |
| **[bloomberg](/adapters/browser/bloomberg)** | `main` `markets` `economics` `industries` `tech` `politics` `businessweek` `opinions` `feeds` `news` | ๐ŸŒ / ๐Ÿ” |
| **[weibo](/adapters/browser/weibo)** | `hot` | ๐Ÿ” Browser |
| **[linkedin](/adapters/browser/linkedin)** | `search` | ๐Ÿ” Browser |
Expand Down
29 changes: 29 additions & 0 deletions src/clis/v2ex/member.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
site: v2ex
name: member
description: V2EX ็”จๆˆท่ต„ๆ–™
domain: www.v2ex.com
strategy: public
browser: false

args:
username:
positional: true
type: str
required: true
description: Username

pipeline:
- fetch:
url: https://www.v2ex.com/api/members/show.json
params:
username: ${{ args.username }}

- map:
username: ${{ item.username }}
tagline: ${{ item.tagline }}
website: ${{ item.website }}
github: ${{ item.github }}
twitter: ${{ item.twitter }}
location: ${{ item.location }}

columns: [username, tagline, website, github, twitter, location]
34 changes: 34 additions & 0 deletions src/clis/v2ex/node.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
site: v2ex
name: node
description: V2EX ่Š‚็‚น่ฏ้ข˜ๅˆ—่กจ
domain: www.v2ex.com
strategy: public
browser: false

args:
name:
positional: true
type: str
required: true
description: Node name (e.g. python, javascript, apple)
limit:
type: int
default: 10
description: Number of topics (API returns max 20)

pipeline:
- fetch:
url: https://www.v2ex.com/api/topics/show.json
params:
node_name: ${{ args.name }}

- map:
rank: ${{ index + 1 }}
title: ${{ item.title }}
author: ${{ item.member.username }}
replies: ${{ item.replies }}
url: ${{ item.url }}

- limit: ${{ args.limit }}

columns: [rank, title, author, replies, url]
31 changes: 31 additions & 0 deletions src/clis/v2ex/nodes.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
site: v2ex
name: nodes
description: V2EX ๆ‰€ๆœ‰่Š‚็‚นๅˆ—่กจ
domain: www.v2ex.com
strategy: public
browser: false

args:
limit:
type: int
default: 30
description: Number of nodes

pipeline:
- fetch:
url: https://www.v2ex.com/api/nodes/all.json

- sort:
by: topics
order: desc

- map:
rank: ${{ index + 1 }}
name: ${{ item.name }}
title: ${{ item.title }}
topics: ${{ item.topics }}
stars: ${{ item.stars }}

- limit: ${{ args.limit }}

columns: [rank, name, title, topics, stars]
32 changes: 32 additions & 0 deletions src/clis/v2ex/replies.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
site: v2ex
name: replies
description: V2EX ไธป้ข˜ๅ›žๅคๅˆ—่กจ
domain: www.v2ex.com
strategy: public
browser: false

args:
id:
positional: true
type: str
required: true
description: Topic ID
limit:
type: int
default: 20
description: Number of replies

pipeline:
- fetch:
url: https://www.v2ex.com/api/replies/show.json
params:
topic_id: ${{ args.id }}

- map:
floor: ${{ index + 1 }}
author: ${{ item.member.username }}
content: ${{ item.content }}

- limit: ${{ args.limit }}

columns: [floor, author, content]
34 changes: 34 additions & 0 deletions src/clis/v2ex/user.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
site: v2ex
name: user
description: V2EX ็”จๆˆทๅ‘ๅธ–ๅˆ—่กจ
domain: www.v2ex.com
strategy: public
browser: false

args:
username:
positional: true
type: str
required: true
description: Username
limit:
type: int
default: 10
description: Number of topics (API returns max 20)

pipeline:
- fetch:
url: https://www.v2ex.com/api/topics/show.json
params:
username: ${{ args.username }}

- map:
rank: ${{ index + 1 }}
title: ${{ item.title }}
node: ${{ item.node.title }}
replies: ${{ item.replies }}
url: ${{ item.url }}

- limit: ${{ args.limit }}

columns: [rank, title, node, replies, url]
63 changes: 63 additions & 0 deletions tests/e2e/public-commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,69 @@ describe('public commands E2E', () => {
}
}, 30_000);

it('v2ex node returns topics for a given node', async () => {
const { stdout, code } = await runCli(['v2ex', 'node', 'python', '--limit', '3', '-f', 'json']);
// V2EX may rate-limit; only assert when successful
if (code === 0) {
const data = parseJsonOutput(stdout);
expect(Array.isArray(data)).toBe(true);
expect(data.length).toBeGreaterThanOrEqual(1);
expect(data.length).toBeLessThanOrEqual(3);
expect(data[0]).toHaveProperty('title');
expect(data[0]).toHaveProperty('author');
expect(data[0]).toHaveProperty('url');
}
}, 30_000);

it('v2ex user returns topics by username', async () => {
const { stdout, code } = await runCli(['v2ex', 'user', 'Livid', '--limit', '3', '-f', 'json']);
if (code === 0) {
const data = parseJsonOutput(stdout);
expect(Array.isArray(data)).toBe(true);
expect(data.length).toBeGreaterThanOrEqual(1);
expect(data.length).toBeLessThanOrEqual(3);
expect(data[0]).toHaveProperty('title');
expect(data[0]).toHaveProperty('node');
expect(data[0]).toHaveProperty('url');
}
}, 30_000);

it('v2ex member returns user profile', async () => {
const { stdout, code } = await runCli(['v2ex', 'member', 'Livid', '-f', 'json']);
if (code === 0) {
const data = parseJsonOutput(stdout);
expect(Array.isArray(data)).toBe(true);
expect(data.length).toBe(1);
expect(data[0].username).toBe('Livid');
}
}, 30_000);

it('v2ex replies returns topic replies', async () => {
const { stdout, code } = await runCli(['v2ex', 'replies', '1000', '--limit', '3', '-f', 'json']);
if (code === 0) {
const data = parseJsonOutput(stdout);
expect(Array.isArray(data)).toBe(true);
expect(data.length).toBeGreaterThanOrEqual(1);
expect(data.length).toBeLessThanOrEqual(3);
expect(data[0]).toHaveProperty('author');
expect(data[0]).toHaveProperty('content');
}
}, 30_000);

it('v2ex nodes returns node list sorted by topics', async () => {
const { stdout, code } = await runCli(['v2ex', 'nodes', '--limit', '5', '-f', 'json']);
if (code === 0) {
const data = parseJsonOutput(stdout);
expect(Array.isArray(data)).toBe(true);
expect(data.length).toBe(5);
expect(data[0]).toHaveProperty('name');
expect(data[0]).toHaveProperty('title');
expect(data[0]).toHaveProperty('topics');
// Verify descending sort by topic count
expect(Number(data[0].topics)).toBeGreaterThanOrEqual(Number(data[data.length - 1].topics));
}
}, 30_000);

// โ”€โ”€ xiaoyuzhou (Chinese site โ€” may return empty on overseas CI runners) โ”€โ”€
it('xiaoyuzhou podcast returns podcast profile', async () => {
const { stdout, stderr, code } = await runCli(['xiaoyuzhou', 'podcast', '6013f9f58e2f7ee375cf4216', '-f', 'json']);
Expand Down
Loading
Loading