From 706fffbdecbb66bc200ee31874532d056b041fb7 Mon Sep 17 00:00:00 2001 From: jackwener Date: Sun, 29 Mar 2026 17:24:47 +0800 Subject: [PATCH 1/2] fix(zsxq): require active group context --- src/clis/zsxq/search.test.ts | 29 ++++++++++++++++++++++++ src/clis/zsxq/search.ts | 6 ++--- src/clis/zsxq/topics.test.ts | 29 ++++++++++++++++++++++++ src/clis/zsxq/topics.ts | 6 ++--- src/clis/zsxq/utils.ts | 44 +++++------------------------------- 5 files changed, 70 insertions(+), 44 deletions(-) create mode 100644 src/clis/zsxq/search.test.ts create mode 100644 src/clis/zsxq/topics.test.ts diff --git a/src/clis/zsxq/search.test.ts b/src/clis/zsxq/search.test.ts new file mode 100644 index 00000000..9bd9b2d5 --- /dev/null +++ b/src/clis/zsxq/search.test.ts @@ -0,0 +1,29 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { getRegistry } from '../../registry.js'; +import './search.js'; + +describe('zsxq search command', () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + it('requires an explicit group_id when there is no active group context', async () => { + const command = getRegistry().get('zsxq/search'); + expect(command?.func).toBeTypeOf('function'); + + const mockPage = { + goto: vi.fn().mockResolvedValue(undefined), + evaluate: vi.fn() + .mockResolvedValueOnce(true) + .mockResolvedValueOnce(null), + } as any; + + await expect(command!.func!(mockPage, { keyword: 'opencli', limit: 20 })).rejects.toMatchObject({ + code: 'ARGUMENT', + message: 'Cannot determine active group_id', + }); + + expect(mockPage.goto).toHaveBeenCalledWith('https://wx.zsxq.com'); + expect(mockPage.evaluate).toHaveBeenCalledTimes(2); + }); +}); diff --git a/src/clis/zsxq/search.ts b/src/clis/zsxq/search.ts index 8aca1238..cdfc93aa 100644 --- a/src/clis/zsxq/search.ts +++ b/src/clis/zsxq/search.ts @@ -1,9 +1,9 @@ import { cli, Strategy } from '../../registry.js'; import { + getActiveGroupId, ensureZsxqAuth, ensureZsxqPage, fetchFirstJson, - getDefaultGroupId, getGroupsFromResponse, getTopicsFromResponse, toTopicRow, @@ -19,7 +19,7 @@ cli({ args: [ { name: 'keyword', required: true, positional: true, help: 'Search keyword' }, { name: 'limit', type: 'int', default: 20, help: 'Number of results to return' }, - { name: 'group_id', help: 'Optional group id; defaults to group_id cookie' }, + { name: 'group_id', help: 'Optional group id; defaults to the active group in Chrome' }, ], columns: ['topic_id', 'group', 'author', 'title', 'comments', 'likes', 'time', 'url'], func: async (page, kwargs) => { @@ -28,7 +28,7 @@ cli({ const keyword = String(kwargs.keyword || '').trim(); const limit = Math.max(1, Number(kwargs.limit) || 20); - const groupId = String(kwargs.group_id || await getDefaultGroupId(page)); + const groupId = String(kwargs.group_id || await getActiveGroupId(page)); const query = encodeURIComponent(keyword); // Resolve group name from groups API diff --git a/src/clis/zsxq/topics.test.ts b/src/clis/zsxq/topics.test.ts new file mode 100644 index 00000000..046e5250 --- /dev/null +++ b/src/clis/zsxq/topics.test.ts @@ -0,0 +1,29 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { getRegistry } from '../../registry.js'; +import './topics.js'; + +describe('zsxq topics command', () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + it('requires an explicit group_id when there is no active group context', async () => { + const command = getRegistry().get('zsxq/topics'); + expect(command?.func).toBeTypeOf('function'); + + const mockPage = { + goto: vi.fn().mockResolvedValue(undefined), + evaluate: vi.fn() + .mockResolvedValueOnce(true) + .mockResolvedValueOnce(null), + } as any; + + await expect(command!.func!(mockPage, { limit: 20 })).rejects.toMatchObject({ + code: 'ARGUMENT', + message: 'Cannot determine active group_id', + }); + + expect(mockPage.goto).toHaveBeenCalledWith('https://wx.zsxq.com'); + expect(mockPage.evaluate).toHaveBeenCalledTimes(2); + }); +}); diff --git a/src/clis/zsxq/topics.ts b/src/clis/zsxq/topics.ts index af2f7f11..a4a1bad6 100644 --- a/src/clis/zsxq/topics.ts +++ b/src/clis/zsxq/topics.ts @@ -1,9 +1,9 @@ import { cli, Strategy } from '../../registry.js'; import { + getActiveGroupId, ensureZsxqAuth, ensureZsxqPage, fetchFirstJson, - getDefaultGroupId, getTopicsFromResponse, toTopicRow, } from './utils.js'; @@ -17,7 +17,7 @@ cli({ browser: true, args: [ { name: 'limit', type: 'int', default: 20, help: 'Number of topics to return' }, - { name: 'group_id', help: 'Optional group id; defaults to group_id cookie' }, + { name: 'group_id', help: 'Optional group id; defaults to the active group in Chrome' }, ], columns: ['topic_id', 'type', 'author', 'title', 'comments', 'likes', 'time', 'url'], func: async (page, kwargs) => { @@ -25,7 +25,7 @@ cli({ await ensureZsxqAuth(page); const limit = Math.max(1, Number(kwargs.limit) || 20); - const groupId = String(kwargs.group_id || await getDefaultGroupId(page)); + const groupId = String(kwargs.group_id || await getActiveGroupId(page)); const { data } = await fetchFirstJson(page, [ `https://api.zsxq.com/v2/groups/${groupId}/topics?scope=all&count=${limit}`, diff --git a/src/clis/zsxq/utils.ts b/src/clis/zsxq/utils.ts index eae12ac8..8bc6e960 100644 --- a/src/clis/zsxq/utils.ts +++ b/src/clis/zsxq/utils.ts @@ -1,4 +1,4 @@ -import { AuthRequiredError, CliError } from '../../errors.js'; +import { ArgumentError, AuthRequiredError, CliError } from '../../errors.js'; import type { IPage } from '../../types.js'; export interface ZsxqUser { @@ -147,11 +147,9 @@ export async function getCookieValue(page: IPage, name: string): Promise cookie.name === name)?.value; } -export async function getDefaultGroupId(page: IPage): Promise { - // Try localStorage first, then fall back to first joined group +export async function getActiveGroupId(page: IPage): Promise { const groupId = await page.evaluate(` (() => { - // Check localStorage const target = localStorage.getItem('target_group'); if (target) { try { @@ -164,40 +162,10 @@ export async function getDefaultGroupId(page: IPage): Promise { `); if (groupId) return groupId; - // Fall back: call API to get first group - const raw = await page.evaluate(` - (async () => { - try { - const r = await new Promise((resolve, reject) => { - const xhr = new XMLHttpRequest(); - xhr.open('GET', 'https://api.zsxq.com/v2/groups', true); - xhr.withCredentials = true; - xhr.setRequestHeader('accept', 'application/json'); - xhr.onload = () => { - if (xhr.status >= 200 && xhr.status < 300) { - try { resolve(JSON.parse(xhr.responseText)); } - catch { resolve(null); } - } else { resolve(null); } - }; - xhr.onerror = () => resolve(null); - xhr.send(); - }); - if (r && r.resp_data && r.resp_data.groups && r.resp_data.groups.length > 0) { - return String(r.resp_data.groups[0].group_id); - } - return null; - } catch { return null; } - })() - `); - - if (!raw) { - throw new CliError( - 'AUTH_REQUIRED', - 'Cannot determine group_id', - 'Open a specific 知识星球 page in Chrome first', - ); - } - return raw; + throw new ArgumentError( + 'Cannot determine active group_id', + 'Pass --group_id or open the target 知识星球 page in Chrome first', + ); } export async function browserJsonRequest(page: IPage, path: string): Promise { From 793e369e68e594beb0d9f8fee6e88f1ee49e9a6c Mon Sep 17 00:00:00 2001 From: jackwener Date: Sun, 29 Mar 2026 17:27:39 +0800 Subject: [PATCH 2/2] docs(zsxq): add adapter guide --- docs/adapters/browser/zsxq.md | 49 +++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 docs/adapters/browser/zsxq.md diff --git a/docs/adapters/browser/zsxq.md b/docs/adapters/browser/zsxq.md new file mode 100644 index 00000000..2813a6c6 --- /dev/null +++ b/docs/adapters/browser/zsxq.md @@ -0,0 +1,49 @@ +# 知识星球 (ZSXQ) + +**Mode**: 🔐 Browser · **Domain**: `wx.zsxq.com` + +Read groups, topics, search results, dynamics, and single-topic details from [知识星球](https://wx.zsxq.com) using your logged-in Chrome session. + +## Commands + +| Command | Description | +|---------|-------------| +| `opencli zsxq groups` | List the groups your account has joined | +| `opencli zsxq topics` | List topics in the active group | +| `opencli zsxq topic ` | Fetch a single topic with comments | +| `opencli zsxq search ` | Search topics inside a group | +| `opencli zsxq dynamics` | List recent dynamics across groups | + +## Usage Examples + +```bash +# List your groups +opencli zsxq groups + +# List topics from the active group in Chrome +opencli zsxq topics --limit 20 + +# Search inside the active group +opencli zsxq search "opencli" + +# Search inside a specific group explicitly +opencli zsxq search "opencli" --group_id 123456789 + +# Export a single topic with comments +opencli zsxq topic 987654321 --comment_limit 20 + +# Read recent dynamics across all joined groups +opencli zsxq dynamics --limit 20 +``` + +## Prerequisites + +- Chrome running and **logged into** [wx.zsxq.com](https://wx.zsxq.com) +- [Browser Bridge extension](/guide/browser-bridge) installed + +## Notes + +- `zsxq topics` and `zsxq search` use the current active group context from Chrome by default +- If there is no active group context, pass `--group_id ` or open the target group in Chrome first +- `zsxq groups` returns `group_id`, which you can reuse with `--group_id` +- `zsxq topic` surfaces a missing topic as `NOT_FOUND` instead of a generic fetch error