diff --git a/src/clis/linux-do/categories.yaml b/src/clis/linux-do/categories.yaml new file mode 100644 index 0000000..f5a6435 --- /dev/null +++ b/src/clis/linux-do/categories.yaml @@ -0,0 +1,41 @@ +site: linux-do +name: categories +description: linux.do 分类列表 +domain: linux.do +browser: true + +args: + limit: + type: int + default: 20 + description: Number of categories + +pipeline: + - navigate: https://linux.do + + - evaluate: | + (async () => { + const res = await fetch('/categories.json', { credentials: 'include' }); + if (!res.ok) throw new Error('HTTP ' + res.status + ' - 请先登录 linux.do'); + let data; + try { data = await res.json(); } catch { throw new Error('响应不是有效 JSON - 请先登录 linux.do'); } + const cats = data?.category_list?.categories || []; + return cats.slice(0, ${{ args.limit }}).map(c => ({ + name: c.name, + slug: c.slug, + id: c.id, + topics: c.topic_count, + description: (c.description_text || '').slice(0, 80), + })); + })() + + - map: + name: ${{ item.name }} + slug: ${{ item.slug }} + id: ${{ item.id }} + topics: ${{ item.topics }} + description: ${{ item.description }} + + - limit: ${{ args.limit }} + +columns: [name, slug, id, topics, description] diff --git a/src/clis/linux-do/category.yaml b/src/clis/linux-do/category.yaml new file mode 100644 index 0000000..9b52bfe --- /dev/null +++ b/src/clis/linux-do/category.yaml @@ -0,0 +1,49 @@ +site: linux-do +name: category +description: linux.do 分类内话题 +domain: linux.do +browser: true + +args: + slug: + type: str + required: true + description: Category slug (use 'categories' command to find) + id: + type: int + required: true + description: Category ID (use 'categories' command to find) + limit: + type: int + default: 20 + description: Number of topics + +pipeline: + - navigate: https://linux.do + + - evaluate: | + (async () => { + const slug = ${{ args.slug | json }}; + const res = await fetch('/c/' + encodeURIComponent(slug) + '/${{ args.id }}.json', { credentials: 'include' }); + if (!res.ok) throw new Error('HTTP ' + res.status + ' - 请先登录 linux.do'); + let data; + try { data = await res.json(); } catch { throw new Error('响应不是有效 JSON - 请先登录 linux.do'); } + const topics = data?.topic_list?.topics || []; + return topics.slice(0, ${{ args.limit }}).map(t => ({ + title: t.title, + replies: (t.posts_count || 1) - 1, + views: t.views, + likes: t.like_count, + })); + })() + + - map: + rank: ${{ index + 1 }} + title: ${{ item.title }} + replies: ${{ item.replies }} + views: ${{ item.views }} + likes: ${{ item.likes }} + + - limit: ${{ args.limit }} + +columns: [rank, title, replies, views, likes] diff --git a/src/clis/linux-do/hot.yaml b/src/clis/linux-do/hot.yaml new file mode 100644 index 0000000..0ca59de --- /dev/null +++ b/src/clis/linux-do/hot.yaml @@ -0,0 +1,50 @@ +site: linux-do +name: hot +description: linux.do 热门话题 +domain: linux.do +browser: true + +args: + limit: + type: int + default: 20 + description: Number of topics + period: + type: str + default: weekly + description: Time period + choices: [all, daily, weekly, monthly, yearly] + +pipeline: + - navigate: https://linux.do + + - evaluate: | + (async () => { + const period = ${{ args.period | json }}; + const res = await fetch('/top.json?period=' + encodeURIComponent(period), { credentials: 'include' }); + if (!res.ok) throw new Error('HTTP ' + res.status + ' - 请先登录 linux.do'); + let data; + try { data = await res.json(); } catch { throw new Error('响应不是有效 JSON - 请先登录 linux.do'); } + const topics = data?.topic_list?.topics || []; + const cats = data?.topic_list?.categories || data?.categories || []; + const catMap = Object.fromEntries(cats.map(c => [c.id, c.name])); + return topics.slice(0, ${{ args.limit }}).map(t => ({ + title: t.title, + replies: (t.posts_count || 1) - 1, + views: t.views, + likes: t.like_count, + category: catMap[t.category_id] || String(t.category_id), + })); + })() + + - map: + rank: ${{ index + 1 }} + title: ${{ item.title }} + replies: ${{ item.replies }} + views: ${{ item.views }} + likes: ${{ item.likes }} + category: ${{ item.category }} + + - limit: ${{ args.limit }} + +columns: [rank, title, replies, views, likes, category] diff --git a/src/clis/linux-do/latest.yaml b/src/clis/linux-do/latest.yaml new file mode 100644 index 0000000..3709346 --- /dev/null +++ b/src/clis/linux-do/latest.yaml @@ -0,0 +1,40 @@ +site: linux-do +name: latest +description: linux.do 最新话题 +domain: linux.do +browser: true + +args: + limit: + type: int + default: 20 + description: Number of topics + +pipeline: + - navigate: https://linux.do + + - evaluate: | + (async () => { + const res = await fetch('/latest.json', { credentials: 'include' }); + if (!res.ok) throw new Error('HTTP ' + res.status + ' - 请先登录 linux.do'); + let data; + try { data = await res.json(); } catch { throw new Error('响应不是有效 JSON - 请先登录 linux.do'); } + const topics = data?.topic_list?.topics || []; + return topics.slice(0, ${{ args.limit }}).map(t => ({ + title: t.title, + replies: (t.posts_count || 1) - 1, + views: t.views, + likes: t.like_count, + })); + })() + + - map: + rank: ${{ index + 1 }} + title: ${{ item.title }} + replies: ${{ item.replies }} + views: ${{ item.views }} + likes: ${{ item.likes }} + + - limit: ${{ args.limit }} + +columns: [rank, title, replies, views, likes] diff --git a/src/clis/linux-do/search.yaml b/src/clis/linux-do/search.yaml new file mode 100644 index 0000000..edc74d1 --- /dev/null +++ b/src/clis/linux-do/search.yaml @@ -0,0 +1,45 @@ +site: linux-do +name: search +description: 搜索 linux.do +domain: linux.do +browser: true + +args: + keyword: + type: str + required: true + description: Search keyword + limit: + type: int + default: 20 + description: Number of results + +pipeline: + - navigate: https://linux.do + + - evaluate: | + (async () => { + const keyword = ${{ args.keyword | json }}; + const res = await fetch('/search.json?q=' + encodeURIComponent(keyword), { credentials: 'include' }); + if (!res.ok) throw new Error('HTTP ' + res.status + ' - 请先登录 linux.do'); + let data; + try { data = await res.json(); } catch { throw new Error('响应不是有效 JSON - 请先登录 linux.do'); } + const topics = data?.topics || []; + return topics.slice(0, ${{ args.limit }}).map(t => ({ + title: t.title, + views: t.views, + likes: t.like_count, + replies: (t.posts_count || 1) - 1, + })); + })() + + - map: + rank: ${{ index + 1 }} + title: ${{ item.title }} + views: ${{ item.views }} + likes: ${{ item.likes }} + replies: ${{ item.replies }} + + - limit: ${{ args.limit }} + +columns: [rank, title, views, likes, replies] diff --git a/src/clis/linux-do/topic.yaml b/src/clis/linux-do/topic.yaml new file mode 100644 index 0000000..84bf151 --- /dev/null +++ b/src/clis/linux-do/topic.yaml @@ -0,0 +1,38 @@ +site: linux-do +name: topic +description: linux.do 帖子详情和回复(首页) +domain: linux.do +browser: true + +args: + id: + type: int + required: true + description: Topic ID + +pipeline: + - navigate: https://linux.do + + - evaluate: | + (async () => { + const res = await fetch('/t/${{ args.id }}.json', { credentials: 'include' }); + if (!res.ok) throw new Error('HTTP ' + res.status + ' - 请先登录 linux.do'); + let data; + try { data = await res.json(); } catch { throw new Error('响应不是有效 JSON - 请先登录 linux.do'); } + const strip = (html) => (html || '').replace(//gi, ' ').replace(/<\/(p|div|li|blockquote|h[1-6])>/gi, ' ').replace(/<[^>]+>/g, '').replace(/ /g, ' ').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/&#(?:(\d+)|x([0-9a-fA-F]+));/g, (_, dec, hex) => { try { return String.fromCodePoint(dec !== undefined ? Number(dec) : parseInt(hex, 16)); } catch { return ''; } }).replace(/\s+/g, ' ').trim(); + const posts = data?.post_stream?.posts || []; + return posts.map(p => ({ + author: p.username, + content: strip(p.cooked).slice(0, 200), + likes: p.like_count, + created_at: p.created_at, + })); + })() + + - map: + author: ${{ item.author }} + content: ${{ item.content }} + likes: ${{ item.likes }} + created_at: ${{ item.created_at }} + +columns: [author, content, likes, created_at] diff --git a/tests/e2e/browser-auth.test.ts b/tests/e2e/browser-auth.test.ts index ccab466..3fff314 100644 --- a/tests/e2e/browser-auth.test.ts +++ b/tests/e2e/browser-auth.test.ts @@ -79,6 +79,31 @@ describe('login-required commands — graceful failure', () => { await expectGracefulAuthFailure(['xueqiu', 'watchlist', '-f', 'json'], 'xueqiu watchlist'); }, 60_000); + // ── linux-do (requires login — all endpoints need authentication) ── + it('linux-do hot fails gracefully without login', async () => { + await expectGracefulAuthFailure(['linux-do', 'hot', '--limit', '3', '-f', 'json'], 'linux-do hot'); + }, 60_000); + + it('linux-do latest fails gracefully without login', async () => { + await expectGracefulAuthFailure(['linux-do', 'latest', '--limit', '3', '-f', 'json'], 'linux-do latest'); + }, 60_000); + + it('linux-do categories fails gracefully without login', async () => { + await expectGracefulAuthFailure(['linux-do', 'categories', '--limit', '3', '-f', 'json'], 'linux-do categories'); + }, 60_000); + + it('linux-do category fails gracefully without login', async () => { + await expectGracefulAuthFailure(['linux-do', 'category', '--slug', 'general', '--id', '1', '--limit', '3', '-f', 'json'], 'linux-do category'); + }, 60_000); + + it('linux-do topic fails gracefully without login', async () => { + await expectGracefulAuthFailure(['linux-do', 'topic', '--id', '1', '-f', 'json'], 'linux-do topic'); + }, 60_000); + + it('linux-do search fails gracefully without login', async () => { + await expectGracefulAuthFailure(['linux-do', 'search', '--keyword', 'test', '--limit', '3', '-f', 'json'], 'linux-do search'); + }, 60_000); + // ── xiaohongshu (requires login) ── it('xiaohongshu feed fails gracefully without login', async () => { await expectGracefulAuthFailure(['xiaohongshu', 'feed', '--limit', '3', '-f', 'json'], 'xiaohongshu feed');