@jackwener/opencli 1.5.8 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -0
- package/README.md +35 -1
- package/README.zh-CN.md +17 -1
- package/SKILL.md +31 -851
- package/autoresearch/baseline-browse.txt +1 -0
- package/autoresearch/baseline-skill.txt +1 -0
- package/autoresearch/browse-tasks.json +688 -0
- package/autoresearch/eval-browse.ts +185 -0
- package/autoresearch/eval-skill.ts +248 -0
- package/autoresearch/run-browse.sh +9 -0
- package/autoresearch/run-skill.sh +9 -0
- package/dist/browser/base-page.d.ts +48 -0
- package/dist/browser/base-page.js +160 -0
- package/dist/browser/cdp.js +4 -106
- package/dist/browser/daemon-client.d.ts +20 -7
- package/dist/browser/daemon-client.js +39 -39
- package/dist/browser/daemon-client.test.js +77 -0
- package/dist/browser/discover.d.ts +1 -4
- package/dist/browser/discover.js +9 -23
- package/dist/browser/errors.d.ts +4 -0
- package/dist/browser/errors.js +20 -0
- package/dist/browser/index.d.ts +1 -1
- package/dist/browser/index.js +1 -1
- package/dist/browser/page.d.ts +10 -35
- package/dist/browser/page.js +55 -187
- package/dist/browser/tabs.js +5 -5
- package/dist/browser.test.js +15 -15
- package/dist/cli-manifest.json +294 -22
- package/dist/cli.js +392 -0
- package/dist/clis/amazon/bestsellers.d.ts +21 -0
- package/dist/clis/amazon/bestsellers.js +130 -0
- package/dist/clis/amazon/bestsellers.test.js +20 -0
- package/dist/clis/amazon/discussion.d.ts +20 -0
- package/dist/clis/amazon/discussion.js +91 -0
- package/dist/clis/amazon/discussion.test.d.ts +1 -0
- package/dist/clis/amazon/discussion.test.js +36 -0
- package/dist/clis/amazon/offer.d.ts +23 -0
- package/dist/clis/amazon/offer.js +140 -0
- package/dist/clis/amazon/offer.test.d.ts +1 -0
- package/dist/clis/amazon/offer.test.js +29 -0
- package/dist/clis/amazon/product.d.ts +18 -0
- package/dist/clis/amazon/product.js +92 -0
- package/dist/clis/amazon/product.test.d.ts +1 -0
- package/dist/clis/amazon/product.test.js +24 -0
- package/dist/clis/amazon/search.d.ts +18 -0
- package/dist/clis/amazon/search.js +87 -0
- package/dist/clis/amazon/search.test.d.ts +1 -0
- package/dist/clis/amazon/search.test.js +22 -0
- package/dist/clis/amazon/shared.d.ts +64 -0
- package/dist/clis/amazon/shared.js +255 -0
- package/dist/clis/amazon/shared.test.d.ts +1 -0
- package/dist/clis/amazon/shared.test.js +33 -0
- package/dist/clis/gemini/ask.d.ts +1 -0
- package/dist/clis/gemini/ask.js +40 -0
- package/dist/clis/gemini/image.d.ts +1 -0
- package/dist/clis/gemini/image.js +105 -0
- package/dist/clis/gemini/new.d.ts +1 -0
- package/dist/clis/gemini/new.js +20 -0
- package/dist/clis/gemini/utils.d.ts +34 -0
- package/dist/clis/gemini/utils.js +463 -0
- package/dist/clis/gemini/utils.test.d.ts +1 -0
- package/dist/clis/gemini/utils.test.js +31 -0
- package/dist/clis/notebooklm/compat.test.d.ts +1 -1
- package/dist/clis/notebooklm/compat.test.js +3 -3
- package/dist/clis/notebooklm/current.js +2 -3
- package/dist/clis/notebooklm/get.js +2 -3
- package/dist/clis/notebooklm/history.js +2 -3
- package/dist/clis/notebooklm/note-list.js +2 -3
- package/dist/clis/notebooklm/notes-get.js +2 -3
- package/dist/clis/notebooklm/open.d.ts +1 -0
- package/dist/clis/notebooklm/open.js +41 -0
- package/dist/clis/notebooklm/open.test.d.ts +1 -0
- package/dist/clis/notebooklm/open.test.js +63 -0
- package/dist/clis/notebooklm/source-fulltext.js +2 -3
- package/dist/clis/notebooklm/source-get.js +2 -3
- package/dist/clis/notebooklm/source-guide.js +2 -3
- package/dist/clis/notebooklm/source-list.js +2 -3
- package/dist/clis/notebooklm/status.js +1 -2
- package/dist/clis/notebooklm/summary.js +2 -3
- package/dist/clis/notebooklm/utils.d.ts +2 -1
- package/dist/clis/notebooklm/utils.js +20 -21
- package/dist/clis/twitter/article.js +28 -1
- package/dist/clis/xiaohongshu/creator-note-detail.test.js +11 -11
- package/dist/clis/xiaohongshu/creator-notes-summary.test.js +6 -6
- package/dist/clis/xiaohongshu/creator-notes.test.js +22 -22
- package/dist/clis/xiaohongshu/note.js +11 -0
- package/dist/clis/xiaohongshu/note.test.js +49 -0
- package/dist/commanderAdapter.js +7 -4
- package/dist/commanderAdapter.test.js +76 -0
- package/dist/commands/daemon.js +8 -47
- package/dist/commands/daemon.test.js +45 -70
- package/dist/discovery.js +27 -0
- package/dist/doctor.d.ts +1 -2
- package/dist/doctor.js +7 -8
- package/dist/explore.js +1 -1
- package/dist/output.js +28 -0
- package/dist/output.test.js +15 -0
- package/dist/pipeline/executor.js +2 -7
- package/dist/pipeline/steps/browser.js +1 -1
- package/dist/pipeline/template.js +25 -3
- package/dist/record.d.ts +50 -0
- package/dist/record.js +298 -57
- package/dist/record.test.d.ts +1 -0
- package/dist/record.test.js +293 -0
- package/dist/registry.d.ts +2 -0
- package/dist/registry.js +1 -0
- package/dist/registry.test.js +10 -0
- package/dist/runtime.js +3 -3
- package/dist/snapshotFormatter.d.ts +1 -1
- package/dist/snapshotFormatter.js +4 -4
- package/dist/snapshotFormatter.test.d.ts +1 -1
- package/dist/snapshotFormatter.test.js +2 -2
- package/dist/types.d.ts +11 -1
- package/dist/types.js +1 -1
- package/docs/.vitepress/config.mts +2 -0
- package/docs/adapters/browser/amazon.md +53 -0
- package/docs/adapters/browser/gemini.md +72 -0
- package/docs/adapters/browser/notebooklm.md +5 -5
- package/docs/adapters/index.md +3 -1
- package/docs/guide/getting-started.md +21 -0
- package/docs/superpowers/specs/2026-04-02-browse-skill-testing-design.md +144 -0
- package/docs/zh/guide/getting-started.md +21 -0
- package/extension/package-lock.json +2 -2
- package/extension/src/background.test.ts +7 -163
- package/extension/src/background.ts +58 -161
- package/extension/src/cdp.ts +77 -124
- package/extension/src/protocol.ts +5 -5
- package/package.json +1 -1
- package/skills/opencli-explorer/SKILL.md +853 -0
- package/skills/opencli-oneshot/SKILL.md +222 -0
- package/skills/opencli-operate/SKILL.md +213 -0
- package/skills/opencli-usage/SKILL.md +152 -0
- package/skills/opencli-usage/browser.md +429 -0
- package/skills/opencli-usage/desktop.md +118 -0
- package/skills/opencli-usage/plugins.md +82 -0
- package/skills/opencli-usage/public-api.md +149 -0
- package/src/browser/base-page.ts +197 -0
- package/src/browser/cdp.ts +7 -131
- package/src/browser/daemon-client.test.ts +103 -0
- package/src/browser/daemon-client.ts +55 -43
- package/src/browser/discover.ts +9 -21
- package/src/browser/errors.ts +22 -0
- package/src/browser/index.ts +1 -1
- package/src/browser/page.ts +57 -209
- package/src/browser/tabs.ts +5 -5
- package/src/browser.test.ts +15 -15
- package/src/cli.ts +392 -0
- package/src/clis/amazon/bestsellers.test.ts +22 -0
- package/src/clis/amazon/bestsellers.ts +180 -0
- package/src/clis/amazon/discussion.test.ts +38 -0
- package/src/clis/amazon/discussion.ts +131 -0
- package/src/clis/amazon/offer.test.ts +35 -0
- package/src/clis/amazon/offer.ts +185 -0
- package/src/clis/amazon/product.test.ts +26 -0
- package/src/clis/amazon/product.ts +131 -0
- package/src/clis/amazon/search.test.ts +24 -0
- package/src/clis/amazon/search.ts +128 -0
- package/src/clis/amazon/shared.test.ts +37 -0
- package/src/clis/amazon/shared.ts +316 -0
- package/src/clis/gemini/ask.ts +46 -0
- package/src/clis/gemini/image.ts +115 -0
- package/src/clis/gemini/new.ts +22 -0
- package/src/clis/gemini/utils.test.ts +36 -0
- package/src/clis/gemini/utils.ts +523 -0
- package/src/clis/notebooklm/compat.test.ts +3 -3
- package/src/clis/notebooklm/current.ts +2 -3
- package/src/clis/notebooklm/get.ts +1 -3
- package/src/clis/notebooklm/history.ts +1 -3
- package/src/clis/notebooklm/note-list.ts +1 -3
- package/src/clis/notebooklm/notes-get.ts +1 -3
- package/src/clis/notebooklm/open.test.ts +78 -0
- package/src/clis/notebooklm/open.ts +61 -0
- package/src/clis/notebooklm/source-fulltext.ts +1 -3
- package/src/clis/notebooklm/source-get.ts +1 -3
- package/src/clis/notebooklm/source-guide.ts +1 -3
- package/src/clis/notebooklm/source-list.ts +1 -3
- package/src/clis/notebooklm/status.ts +1 -2
- package/src/clis/notebooklm/summary.ts +1 -3
- package/src/clis/notebooklm/utils.ts +29 -20
- package/src/clis/twitter/article.ts +31 -1
- package/src/clis/xiaohongshu/creator-note-detail.test.ts +11 -11
- package/src/clis/xiaohongshu/creator-notes-summary.test.ts +6 -6
- package/src/clis/xiaohongshu/creator-notes.test.ts +22 -22
- package/src/clis/xiaohongshu/note.test.ts +51 -0
- package/src/clis/xiaohongshu/note.ts +18 -0
- package/src/commanderAdapter.test.ts +109 -0
- package/src/commanderAdapter.ts +8 -4
- package/src/commands/daemon.test.ts +50 -84
- package/src/commands/daemon.ts +8 -56
- package/src/discovery.ts +22 -0
- package/src/doctor.ts +8 -9
- package/src/explore.ts +1 -1
- package/src/output.test.ts +17 -0
- package/src/output.ts +27 -0
- package/src/pipeline/executor.ts +2 -7
- package/src/pipeline/steps/browser.ts +1 -1
- package/src/pipeline/template.ts +27 -4
- package/src/record.test.ts +362 -0
- package/src/record.ts +341 -62
- package/src/registry.test.ts +12 -0
- package/src/registry.ts +3 -0
- package/src/runtime.ts +3 -3
- package/src/snapshotFormatter.test.ts +2 -2
- package/src/snapshotFormatter.ts +4 -4
- package/src/types.ts +11 -1
- package/.agents/skills/cross-project-adapter-migration/SKILL.md +0 -249
- package/.agents/workflows/cross-project-adapter-migration.md +0 -54
- package/dist/clis/notebooklm/bind-current.js +0 -29
- package/dist/clis/notebooklm/bind-current.test.d.ts +0 -1
- package/dist/clis/notebooklm/bind-current.test.js +0 -35
- package/dist/clis/notebooklm/binding.test.js +0 -44
- package/extension/dist/background.js +0 -819
- package/src/clis/notebooklm/bind-current.test.ts +0 -43
- package/src/clis/notebooklm/bind-current.ts +0 -36
- package/src/clis/notebooklm/binding.test.ts +0 -53
- /package/dist/browser/{mcp.d.ts → bridge.d.ts} +0 -0
- /package/dist/browser/{mcp.js → bridge.js} +0 -0
- /package/dist/{clis/notebooklm/bind-current.d.ts → browser/daemon-client.test.d.ts} +0 -0
- /package/dist/clis/{notebooklm/binding.test.d.ts → amazon/bestsellers.test.d.ts} +0 -0
- /package/src/browser/{mcp.ts → bridge.ts} +0 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import { CliError, EmptyResultError } from '../../errors.js';
|
|
3
|
+
import { NOTEBOOKLM_DOMAIN, NOTEBOOKLM_SITE } from './shared.js';
|
|
4
|
+
import { buildNotebooklmNotebookUrl, getNotebooklmPageState, parseNotebooklmNotebookTarget, readCurrentNotebooklm, requireNotebooklmSession, } from './utils.js';
|
|
5
|
+
cli({
|
|
6
|
+
site: NOTEBOOKLM_SITE,
|
|
7
|
+
name: 'open',
|
|
8
|
+
aliases: ['select'],
|
|
9
|
+
description: 'Open one NotebookLM notebook in the automation workspace by id or URL',
|
|
10
|
+
domain: NOTEBOOKLM_DOMAIN,
|
|
11
|
+
strategy: Strategy.COOKIE,
|
|
12
|
+
browser: true,
|
|
13
|
+
navigateBefore: false,
|
|
14
|
+
args: [
|
|
15
|
+
{
|
|
16
|
+
name: 'notebook',
|
|
17
|
+
positional: true,
|
|
18
|
+
required: true,
|
|
19
|
+
help: 'Notebook id from list output, or a full NotebookLM notebook URL',
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
columns: ['id', 'title', 'url', 'source'],
|
|
23
|
+
func: async (page, kwargs) => {
|
|
24
|
+
const notebookId = parseNotebooklmNotebookTarget(String(kwargs.notebook ?? ''));
|
|
25
|
+
await page.goto(buildNotebooklmNotebookUrl(notebookId));
|
|
26
|
+
await page.wait(2);
|
|
27
|
+
await requireNotebooklmSession(page);
|
|
28
|
+
const state = await getNotebooklmPageState(page);
|
|
29
|
+
if (state.kind !== 'notebook') {
|
|
30
|
+
throw new CliError('NOTEBOOKLM_OPEN_FAILED', `NotebookLM notebook "${notebookId}" did not open in the automation workspace`, 'Run `opencli notebooklm list -f json` first and pass a valid notebook id.');
|
|
31
|
+
}
|
|
32
|
+
if (state.notebookId !== notebookId) {
|
|
33
|
+
console.warn(`[notebooklm open] expected notebook "${notebookId}" but page reports "${state.notebookId}"; continuing`);
|
|
34
|
+
}
|
|
35
|
+
const current = await readCurrentNotebooklm(page);
|
|
36
|
+
if (!current) {
|
|
37
|
+
throw new EmptyResultError('opencli notebooklm open', 'NotebookLM notebook metadata was not found after navigation.');
|
|
38
|
+
}
|
|
39
|
+
return [current];
|
|
40
|
+
},
|
|
41
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import './open.js';
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
const { mockGetNotebooklmPageState, mockReadCurrentNotebooklm, mockRequireNotebooklmSession, } = vi.hoisted(() => ({
|
|
3
|
+
mockGetNotebooklmPageState: vi.fn(),
|
|
4
|
+
mockReadCurrentNotebooklm: vi.fn(),
|
|
5
|
+
mockRequireNotebooklmSession: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
vi.mock('./utils.js', async () => {
|
|
8
|
+
const actual = await vi.importActual('./utils.js');
|
|
9
|
+
return {
|
|
10
|
+
...actual,
|
|
11
|
+
getNotebooklmPageState: mockGetNotebooklmPageState,
|
|
12
|
+
readCurrentNotebooklm: mockReadCurrentNotebooklm,
|
|
13
|
+
requireNotebooklmSession: mockRequireNotebooklmSession,
|
|
14
|
+
};
|
|
15
|
+
});
|
|
16
|
+
import { getRegistry } from '../../registry.js';
|
|
17
|
+
import './open.js';
|
|
18
|
+
describe('notebooklm open', () => {
|
|
19
|
+
const command = getRegistry().get('notebooklm/open');
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
mockGetNotebooklmPageState.mockReset();
|
|
22
|
+
mockReadCurrentNotebooklm.mockReset();
|
|
23
|
+
mockRequireNotebooklmSession.mockReset();
|
|
24
|
+
mockRequireNotebooklmSession.mockResolvedValue(undefined);
|
|
25
|
+
mockGetNotebooklmPageState.mockResolvedValue({
|
|
26
|
+
url: 'https://notebooklm.google.com/notebook/nb-demo',
|
|
27
|
+
title: 'Browser Automation',
|
|
28
|
+
hostname: 'notebooklm.google.com',
|
|
29
|
+
kind: 'notebook',
|
|
30
|
+
notebookId: 'nb-demo',
|
|
31
|
+
loginRequired: false,
|
|
32
|
+
notebookCount: 1,
|
|
33
|
+
});
|
|
34
|
+
mockReadCurrentNotebooklm.mockResolvedValue({
|
|
35
|
+
id: 'nb-demo',
|
|
36
|
+
title: 'Browser Automation',
|
|
37
|
+
url: 'https://notebooklm.google.com/notebook/nb-demo',
|
|
38
|
+
source: 'current-page',
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
it('opens a notebook by id in the automation workspace', async () => {
|
|
42
|
+
const page = {
|
|
43
|
+
goto: vi.fn(async () => { }),
|
|
44
|
+
wait: vi.fn(async () => { }),
|
|
45
|
+
};
|
|
46
|
+
const result = await command.func(page, { notebook: 'nb-demo' });
|
|
47
|
+
expect(page.goto).toHaveBeenCalledWith('https://notebooklm.google.com/notebook/nb-demo');
|
|
48
|
+
expect(result).toEqual([{
|
|
49
|
+
id: 'nb-demo',
|
|
50
|
+
title: 'Browser Automation',
|
|
51
|
+
url: 'https://notebooklm.google.com/notebook/nb-demo',
|
|
52
|
+
source: 'current-page',
|
|
53
|
+
}]);
|
|
54
|
+
});
|
|
55
|
+
it('accepts a full notebook url', async () => {
|
|
56
|
+
const page = {
|
|
57
|
+
goto: vi.fn(async () => { }),
|
|
58
|
+
wait: vi.fn(async () => { }),
|
|
59
|
+
};
|
|
60
|
+
await command.func(page, { notebook: 'https://notebooklm.google.com/notebook/nb-demo?pli=1' });
|
|
61
|
+
expect(page.goto).toHaveBeenCalledWith('https://notebooklm.google.com/notebook/nb-demo');
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { cli, Strategy } from '../../registry.js';
|
|
2
2
|
import { EmptyResultError } from '../../errors.js';
|
|
3
3
|
import { NOTEBOOKLM_DOMAIN, NOTEBOOKLM_SITE } from './shared.js';
|
|
4
|
-
import {
|
|
4
|
+
import { findNotebooklmSourceRow, getNotebooklmPageState, getNotebooklmSourceFulltextViaRpc, listNotebooklmSourcesFromPage, listNotebooklmSourcesViaRpc, requireNotebooklmSession, } from './utils.js';
|
|
5
5
|
cli({
|
|
6
6
|
site: NOTEBOOKLM_SITE,
|
|
7
7
|
name: 'source-fulltext',
|
|
@@ -20,11 +20,10 @@ cli({
|
|
|
20
20
|
],
|
|
21
21
|
columns: ['title', 'kind', 'char_count', 'url', 'source'],
|
|
22
22
|
func: async (page, kwargs) => {
|
|
23
|
-
await ensureNotebooklmNotebookBinding(page);
|
|
24
23
|
await requireNotebooklmSession(page);
|
|
25
24
|
const state = await getNotebooklmPageState(page);
|
|
26
25
|
if (state.kind !== 'notebook') {
|
|
27
|
-
throw new EmptyResultError('opencli notebooklm source-fulltext', '
|
|
26
|
+
throw new EmptyResultError('opencli notebooklm source-fulltext', 'No NotebookLM notebook is open in the automation workspace. Run `opencli notebooklm open <notebook>` first.');
|
|
28
27
|
}
|
|
29
28
|
const rpcRows = await listNotebooklmSourcesViaRpc(page).catch(() => []);
|
|
30
29
|
const rows = rpcRows.length > 0 ? rpcRows : await listNotebooklmSourcesFromPage(page);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { cli, Strategy } from '../../registry.js';
|
|
2
2
|
import { EmptyResultError } from '../../errors.js';
|
|
3
3
|
import { NOTEBOOKLM_DOMAIN, NOTEBOOKLM_SITE } from './shared.js';
|
|
4
|
-
import {
|
|
4
|
+
import { findNotebooklmSourceRow, getNotebooklmPageState, listNotebooklmSourcesFromPage, listNotebooklmSourcesViaRpc, requireNotebooklmSession, } from './utils.js';
|
|
5
5
|
cli({
|
|
6
6
|
site: NOTEBOOKLM_SITE,
|
|
7
7
|
name: 'source-get',
|
|
@@ -20,11 +20,10 @@ cli({
|
|
|
20
20
|
],
|
|
21
21
|
columns: ['title', 'id', 'type', 'size', 'created_at', 'updated_at', 'url', 'source'],
|
|
22
22
|
func: async (page, kwargs) => {
|
|
23
|
-
await ensureNotebooklmNotebookBinding(page);
|
|
24
23
|
await requireNotebooklmSession(page);
|
|
25
24
|
const state = await getNotebooklmPageState(page);
|
|
26
25
|
if (state.kind !== 'notebook') {
|
|
27
|
-
throw new EmptyResultError('opencli notebooklm source-get', '
|
|
26
|
+
throw new EmptyResultError('opencli notebooklm source-get', 'No NotebookLM notebook is open in the automation workspace. Run `opencli notebooklm open <notebook>` first.');
|
|
28
27
|
}
|
|
29
28
|
const rpcRows = await listNotebooklmSourcesViaRpc(page).catch(() => []);
|
|
30
29
|
const rows = rpcRows.length > 0 ? rpcRows : await listNotebooklmSourcesFromPage(page);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { cli, Strategy } from '../../registry.js';
|
|
2
2
|
import { EmptyResultError } from '../../errors.js';
|
|
3
3
|
import { NOTEBOOKLM_DOMAIN, NOTEBOOKLM_SITE } from './shared.js';
|
|
4
|
-
import {
|
|
4
|
+
import { findNotebooklmSourceRow, getNotebooklmPageState, getNotebooklmSourceGuideViaRpc, listNotebooklmSourcesFromPage, listNotebooklmSourcesViaRpc, requireNotebooklmSession, } from './utils.js';
|
|
5
5
|
cli({
|
|
6
6
|
site: NOTEBOOKLM_SITE,
|
|
7
7
|
name: 'source-guide',
|
|
@@ -20,11 +20,10 @@ cli({
|
|
|
20
20
|
],
|
|
21
21
|
columns: ['source_id', 'notebook_id', 'title', 'type', 'summary', 'keywords', 'source'],
|
|
22
22
|
func: async (page, kwargs) => {
|
|
23
|
-
await ensureNotebooklmNotebookBinding(page);
|
|
24
23
|
await requireNotebooklmSession(page);
|
|
25
24
|
const state = await getNotebooklmPageState(page);
|
|
26
25
|
if (state.kind !== 'notebook') {
|
|
27
|
-
throw new EmptyResultError('opencli notebooklm source-guide', '
|
|
26
|
+
throw new EmptyResultError('opencli notebooklm source-guide', 'No NotebookLM notebook is open in the automation workspace. Run `opencli notebooklm open <notebook>` first.');
|
|
28
27
|
}
|
|
29
28
|
const rpcRows = await listNotebooklmSourcesViaRpc(page).catch(() => []);
|
|
30
29
|
const rows = rpcRows.length > 0 ? rpcRows : await listNotebooklmSourcesFromPage(page);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { cli, Strategy } from '../../registry.js';
|
|
2
2
|
import { EmptyResultError } from '../../errors.js';
|
|
3
3
|
import { NOTEBOOKLM_DOMAIN, NOTEBOOKLM_SITE } from './shared.js';
|
|
4
|
-
import {
|
|
4
|
+
import { getNotebooklmPageState, listNotebooklmSourcesFromPage, listNotebooklmSourcesViaRpc, requireNotebooklmSession, } from './utils.js';
|
|
5
5
|
cli({
|
|
6
6
|
site: NOTEBOOKLM_SITE,
|
|
7
7
|
name: 'source-list',
|
|
@@ -13,11 +13,10 @@ cli({
|
|
|
13
13
|
args: [],
|
|
14
14
|
columns: ['title', 'id', 'type', 'size', 'created_at', 'updated_at', 'url', 'source'],
|
|
15
15
|
func: async (page) => {
|
|
16
|
-
await ensureNotebooklmNotebookBinding(page);
|
|
17
16
|
await requireNotebooklmSession(page);
|
|
18
17
|
const state = await getNotebooklmPageState(page);
|
|
19
18
|
if (state.kind !== 'notebook') {
|
|
20
|
-
throw new EmptyResultError('opencli notebooklm source-list', '
|
|
19
|
+
throw new EmptyResultError('opencli notebooklm source-list', 'No NotebookLM notebook is open in the automation workspace. Run `opencli notebooklm open <notebook>` first.');
|
|
21
20
|
}
|
|
22
21
|
const rpcRows = await listNotebooklmSourcesViaRpc(page).catch(() => []);
|
|
23
22
|
if (rpcRows.length > 0)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { cli, Strategy } from '../../registry.js';
|
|
2
2
|
import { NOTEBOOKLM_DOMAIN, NOTEBOOKLM_HOME_URL, NOTEBOOKLM_SITE } from './shared.js';
|
|
3
|
-
import {
|
|
3
|
+
import { getNotebooklmPageState } from './utils.js';
|
|
4
4
|
cli({
|
|
5
5
|
site: NOTEBOOKLM_SITE,
|
|
6
6
|
name: 'status',
|
|
@@ -12,7 +12,6 @@ cli({
|
|
|
12
12
|
args: [],
|
|
13
13
|
columns: ['status', 'login', 'page', 'url', 'title', 'notebooks'],
|
|
14
14
|
func: async (page) => {
|
|
15
|
-
await ensureNotebooklmNotebookBinding(page);
|
|
16
15
|
const currentUrl = await page.getCurrentUrl?.().catch(() => null);
|
|
17
16
|
if (!currentUrl || !currentUrl.includes(NOTEBOOKLM_DOMAIN)) {
|
|
18
17
|
await page.goto(NOTEBOOKLM_HOME_URL);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { cli, Strategy } from '../../registry.js';
|
|
2
2
|
import { EmptyResultError } from '../../errors.js';
|
|
3
3
|
import { NOTEBOOKLM_DOMAIN, NOTEBOOKLM_SITE } from './shared.js';
|
|
4
|
-
import {
|
|
4
|
+
import { getNotebooklmPageState, getNotebooklmSummaryViaRpc, readNotebooklmSummaryFromPage, requireNotebooklmSession, } from './utils.js';
|
|
5
5
|
cli({
|
|
6
6
|
site: NOTEBOOKLM_SITE,
|
|
7
7
|
name: 'summary',
|
|
@@ -13,11 +13,10 @@ cli({
|
|
|
13
13
|
args: [],
|
|
14
14
|
columns: ['title', 'summary', 'source', 'url'],
|
|
15
15
|
func: async (page) => {
|
|
16
|
-
await ensureNotebooklmNotebookBinding(page);
|
|
17
16
|
await requireNotebooklmSession(page);
|
|
18
17
|
const state = await getNotebooklmPageState(page);
|
|
19
18
|
if (state.kind !== 'notebook') {
|
|
20
|
-
throw new EmptyResultError('opencli notebooklm summary', '
|
|
19
|
+
throw new EmptyResultError('opencli notebooklm summary', 'No NotebookLM notebook is open in the automation workspace. Run `opencli notebooklm open <notebook>` first.');
|
|
21
20
|
}
|
|
22
21
|
const domSummary = await readNotebooklmSummaryFromPage(page);
|
|
23
22
|
if (domSummary)
|
|
@@ -2,6 +2,8 @@ import type { IPage } from '../../types.js';
|
|
|
2
2
|
import { type NotebooklmHistoryRow, type NotebooklmNotebookDetailRow, type NotebooklmNoteDetailRow, type NotebooklmNoteRow, type NotebooklmPageKind, type NotebooklmPageState, type NotebooklmRow, type NotebooklmSourceFulltextRow, type NotebooklmSourceGuideRow, type NotebooklmSourceRow, type NotebooklmSummaryRow } from './shared.js';
|
|
3
3
|
export { buildNotebooklmRpcBody, extractNotebooklmRpcResult, fetchNotebooklmInPage, getNotebooklmPageAuth, parseNotebooklmChunkedResponse, stripNotebooklmAntiXssi, } from './rpc.js';
|
|
4
4
|
export declare function parseNotebooklmIdFromUrl(url: string): string;
|
|
5
|
+
export declare function parseNotebooklmNotebookTarget(value: string): string;
|
|
6
|
+
export declare function buildNotebooklmNotebookUrl(notebookId: string): string;
|
|
5
7
|
export declare function classifyNotebooklmPage(url: string): NotebooklmPageKind;
|
|
6
8
|
export declare function normalizeNotebooklmTitle(value: unknown, fallback?: string): string;
|
|
7
9
|
type NotebooklmRawNoteRow = {
|
|
@@ -29,7 +31,6 @@ export declare function getNotebooklmSourceFulltextViaRpc(page: IPage, sourceId:
|
|
|
29
31
|
export declare function getNotebooklmSourceGuideViaRpc(page: IPage, source: Pick<NotebooklmSourceRow, 'id' | 'notebook_id' | 'title' | 'type'>): Promise<NotebooklmSourceGuideRow | null>;
|
|
30
32
|
export declare function readNotebooklmVisibleNoteFromPage(page: IPage): Promise<NotebooklmNoteDetailRow | null>;
|
|
31
33
|
export declare function ensureNotebooklmHome(page: IPage): Promise<void>;
|
|
32
|
-
export declare function ensureNotebooklmNotebookBinding(page: IPage): Promise<boolean>;
|
|
33
34
|
export declare function getNotebooklmPageState(page: IPage): Promise<NotebooklmPageState>;
|
|
34
35
|
export declare function readCurrentNotebooklm(page: IPage): Promise<NotebooklmRow | null>;
|
|
35
36
|
export declare function listNotebooklmLinks(page: IPage): Promise<NotebooklmRow[]>;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { AuthRequiredError, CliError } from '../../errors.js';
|
|
2
|
-
import {
|
|
3
|
-
import { NOTEBOOKLM_DOMAIN, NOTEBOOKLM_HOME_URL, NOTEBOOKLM_SITE, } from './shared.js';
|
|
2
|
+
import { NOTEBOOKLM_DOMAIN, NOTEBOOKLM_HOME_URL, } from './shared.js';
|
|
4
3
|
import { callNotebooklmRpc, getNotebooklmPageAuth, } from './rpc.js';
|
|
5
4
|
export { buildNotebooklmRpcBody, extractNotebooklmRpcResult, fetchNotebooklmInPage, getNotebooklmPageAuth, parseNotebooklmChunkedResponse, stripNotebooklmAntiXssi, } from './rpc.js';
|
|
6
5
|
const NOTEBOOKLM_LIST_RPC_ID = 'wXbhsf';
|
|
@@ -18,6 +17,25 @@ export function parseNotebooklmIdFromUrl(url) {
|
|
|
18
17
|
const match = url.match(/\/notebook\/([^/?#]+)/);
|
|
19
18
|
return match?.[1] ?? '';
|
|
20
19
|
}
|
|
20
|
+
export function parseNotebooklmNotebookTarget(value) {
|
|
21
|
+
const normalized = value.trim();
|
|
22
|
+
if (!normalized) {
|
|
23
|
+
throw new CliError('NOTEBOOKLM_INVALID_NOTEBOOK', 'NotebookLM notebook id is required', 'Pass a notebook id from `opencli notebooklm list` or a full notebook URL.');
|
|
24
|
+
}
|
|
25
|
+
if (/^https?:\/\//i.test(normalized)) {
|
|
26
|
+
const notebookId = parseNotebooklmIdFromUrl(normalized);
|
|
27
|
+
if (notebookId)
|
|
28
|
+
return notebookId;
|
|
29
|
+
throw new CliError('NOTEBOOKLM_INVALID_NOTEBOOK', 'NotebookLM notebook URL is invalid', 'Pass a full NotebookLM notebook URL like https://notebooklm.google.com/notebook/<id>.');
|
|
30
|
+
}
|
|
31
|
+
const pathMatch = normalized.match(/(?:^|\/)notebook\/([^/?#]+)/);
|
|
32
|
+
if (pathMatch?.[1])
|
|
33
|
+
return pathMatch[1];
|
|
34
|
+
return normalized;
|
|
35
|
+
}
|
|
36
|
+
export function buildNotebooklmNotebookUrl(notebookId) {
|
|
37
|
+
return new URL(`/notebook/${encodeURIComponent(notebookId)}`, NOTEBOOKLM_HOME_URL).toString();
|
|
38
|
+
}
|
|
21
39
|
export function classifyNotebooklmPage(url) {
|
|
22
40
|
try {
|
|
23
41
|
const parsed = new URL(url);
|
|
@@ -511,25 +529,6 @@ export async function ensureNotebooklmHome(page) {
|
|
|
511
529
|
await page.goto(NOTEBOOKLM_HOME_URL);
|
|
512
530
|
await page.wait(2);
|
|
513
531
|
}
|
|
514
|
-
export async function ensureNotebooklmNotebookBinding(page) {
|
|
515
|
-
if (!page.getCurrentUrl)
|
|
516
|
-
return false;
|
|
517
|
-
if (process.env.OPENCLI_CDP_ENDPOINT)
|
|
518
|
-
return false;
|
|
519
|
-
const currentUrl = await page.getCurrentUrl().catch(() => null);
|
|
520
|
-
if (currentUrl && classifyNotebooklmPage(currentUrl) === 'notebook')
|
|
521
|
-
return false;
|
|
522
|
-
try {
|
|
523
|
-
await bindCurrentTab(`site:${NOTEBOOKLM_SITE}`, {
|
|
524
|
-
matchDomain: NOTEBOOKLM_DOMAIN,
|
|
525
|
-
matchPathPrefix: '/notebook/',
|
|
526
|
-
});
|
|
527
|
-
return true;
|
|
528
|
-
}
|
|
529
|
-
catch {
|
|
530
|
-
return false;
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
532
|
export async function getNotebooklmPageState(page) {
|
|
534
533
|
const raw = await page.evaluate(`(() => {
|
|
535
534
|
const url = window.location.href;
|
|
@@ -14,11 +14,38 @@ cli({
|
|
|
14
14
|
],
|
|
15
15
|
columns: ['title', 'author', 'content', 'url'],
|
|
16
16
|
func: async (page, kwargs) => {
|
|
17
|
-
// Extract tweet ID from URL if needed
|
|
17
|
+
// Extract tweet ID from URL if needed.
|
|
18
|
+
// Article URLs (x.com/i/article/{articleId}) use a different ID than
|
|
19
|
+
// tweet status URLs — the GraphQL endpoint needs the parent tweet ID.
|
|
18
20
|
let tweetId = kwargs['tweet-id'];
|
|
21
|
+
const isArticleUrl = /\/article\/\d+/.test(tweetId);
|
|
19
22
|
const urlMatch = tweetId.match(/\/(?:status|article)\/(\d+)/);
|
|
20
23
|
if (urlMatch)
|
|
21
24
|
tweetId = urlMatch[1];
|
|
25
|
+
if (isArticleUrl) {
|
|
26
|
+
// Navigate to the article page and resolve the parent tweet ID from DOM
|
|
27
|
+
await page.goto(`https://x.com/i/article/${tweetId}`);
|
|
28
|
+
await page.wait(3);
|
|
29
|
+
const resolvedId = await page.evaluate(`
|
|
30
|
+
(function() {
|
|
31
|
+
var links = document.querySelectorAll('a[href*="/status/"]');
|
|
32
|
+
for (var i = 0; i < links.length; i++) {
|
|
33
|
+
var m = links[i].href.match(/\\/status\\/(\\d+)/);
|
|
34
|
+
if (m) return m[1];
|
|
35
|
+
}
|
|
36
|
+
var og = document.querySelector('meta[property="og:url"]');
|
|
37
|
+
if (og && og.content) {
|
|
38
|
+
var m2 = og.content.match(/\\/status\\/(\\d+)/);
|
|
39
|
+
if (m2) return m2[1];
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
})()
|
|
43
|
+
`);
|
|
44
|
+
if (!resolvedId || typeof resolvedId !== 'string') {
|
|
45
|
+
throw new CommandExecutionError(`Could not resolve article ${tweetId} to a tweet ID. The article page may not contain a linked tweet.`);
|
|
46
|
+
}
|
|
47
|
+
tweetId = resolvedId;
|
|
48
|
+
}
|
|
22
49
|
// Navigate to the tweet page for cookie context
|
|
23
50
|
await page.goto(`https://x.com/i/status/${tweetId}`);
|
|
24
51
|
await page.wait(3);
|
|
@@ -36,9 +36,9 @@ function createPageMock(evaluateResult) {
|
|
|
36
36
|
describe('xiaohongshu creator-note-detail', () => {
|
|
37
37
|
it('parses note detail page text into info and metric rows', () => {
|
|
38
38
|
const bodyText = `笔记数据详情
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
示例内容复盘
|
|
40
|
+
#测试标签
|
|
41
|
+
#内容分析
|
|
42
42
|
2026-03-18 20:01
|
|
43
43
|
切换笔记
|
|
44
44
|
笔记诊断
|
|
@@ -81,9 +81,9 @@ describe('xiaohongshu creator-note-detail', () => {
|
|
|
81
81
|
分享数
|
|
82
82
|
6
|
|
83
83
|
粉丝占比 0%`;
|
|
84
|
-
expect(parseCreatorNoteDetailText(bodyText, '
|
|
85
|
-
{ section: '笔记信息', metric: 'note_id', value: '
|
|
86
|
-
{ section: '笔记信息', metric: 'title', value: '
|
|
84
|
+
expect(parseCreatorNoteDetailText(bodyText, 'cccccccccccccccccccccccc')).toEqual([
|
|
85
|
+
{ section: '笔记信息', metric: 'note_id', value: 'cccccccccccccccccccccccc', extra: '' },
|
|
86
|
+
{ section: '笔记信息', metric: 'title', value: '示例内容复盘', extra: '' },
|
|
87
87
|
{ section: '笔记信息', metric: 'published_at', value: '2026-03-18 20:01', extra: '' },
|
|
88
88
|
{ section: '基础数据', metric: '曝光数', value: '1733', extra: '粉丝占比 6.6%' },
|
|
89
89
|
{ section: '基础数据', metric: '观看数', value: '544', extra: '粉丝占比 7.2%' },
|
|
@@ -98,8 +98,8 @@ describe('xiaohongshu creator-note-detail', () => {
|
|
|
98
98
|
});
|
|
99
99
|
it('parses structured note detail dom data into rows', () => {
|
|
100
100
|
expect(parseCreatorNoteDetailDomData({
|
|
101
|
-
title: '
|
|
102
|
-
infoText: '
|
|
101
|
+
title: '测试笔记一',
|
|
102
|
+
infoText: '测试笔记一\n#测试标签\n2025-12-04 19:45\n切换笔记',
|
|
103
103
|
sections: [
|
|
104
104
|
{
|
|
105
105
|
title: '基础数据',
|
|
@@ -121,9 +121,9 @@ describe('xiaohongshu creator-note-detail', () => {
|
|
|
121
121
|
],
|
|
122
122
|
},
|
|
123
123
|
],
|
|
124
|
-
}, '
|
|
125
|
-
{ section: '笔记信息', metric: 'note_id', value: '
|
|
126
|
-
{ section: '笔记信息', metric: 'title', value: '
|
|
124
|
+
}, 'bbbbbbbbbbbbbbbbbbbbbbbb')).toEqual([
|
|
125
|
+
{ section: '笔记信息', metric: 'note_id', value: 'bbbbbbbbbbbbbbbbbbbbbbbb', extra: '' },
|
|
126
|
+
{ section: '笔记信息', metric: 'title', value: '测试笔记一', extra: '' },
|
|
127
127
|
{ section: '笔记信息', metric: 'published_at', value: '2025-12-04 19:45', extra: '' },
|
|
128
128
|
{ section: '基础数据', metric: '曝光数', value: '898204', extra: '粉丝占比 0.5%' },
|
|
129
129
|
{ section: '基础数据', metric: '观看数', value: '148284', extra: '粉丝占比 0.6%' },
|
|
@@ -4,14 +4,14 @@ import './creator-notes-summary.js';
|
|
|
4
4
|
describe('xiaohongshu creator-notes-summary', () => {
|
|
5
5
|
it('summarizes note list row and detail rows into one compact row', () => {
|
|
6
6
|
const note = {
|
|
7
|
-
id: '
|
|
8
|
-
title: '
|
|
7
|
+
id: 'cccccccccccccccccccccccc',
|
|
8
|
+
title: '示例内容复盘',
|
|
9
9
|
date: '2026年03月18日 20:01',
|
|
10
10
|
views: 549,
|
|
11
11
|
likes: 19,
|
|
12
12
|
collects: 10,
|
|
13
13
|
comments: 7,
|
|
14
|
-
url: 'https://creator.xiaohongshu.com/statistics/note-detail?noteId=
|
|
14
|
+
url: 'https://creator.xiaohongshu.com/statistics/note-detail?noteId=cccccccccccccccccccccccc',
|
|
15
15
|
};
|
|
16
16
|
const rows = [
|
|
17
17
|
{ section: '笔记信息', metric: 'published_at', value: '2026-03-18 20:01', extra: '' },
|
|
@@ -29,8 +29,8 @@ describe('xiaohongshu creator-notes-summary', () => {
|
|
|
29
29
|
];
|
|
30
30
|
expect(summarizeCreatorNote(note, rows, 1)).toEqual({
|
|
31
31
|
rank: 1,
|
|
32
|
-
id: '
|
|
33
|
-
title: '
|
|
32
|
+
id: 'cccccccccccccccccccccccc',
|
|
33
|
+
title: '示例内容复盘',
|
|
34
34
|
published_at: '2026-03-18 20:01',
|
|
35
35
|
views: '549',
|
|
36
36
|
likes: '19',
|
|
@@ -43,7 +43,7 @@ describe('xiaohongshu creator-notes-summary', () => {
|
|
|
43
43
|
top_source_pct: '89.9%',
|
|
44
44
|
top_interest: '二次元',
|
|
45
45
|
top_interest_pct: '13%',
|
|
46
|
-
url: 'https://creator.xiaohongshu.com/statistics/note-detail?noteId=
|
|
46
|
+
url: 'https://creator.xiaohongshu.com/statistics/note-detail?noteId=cccccccccccccccccccccccc',
|
|
47
47
|
});
|
|
48
48
|
});
|
|
49
49
|
});
|
|
@@ -41,7 +41,7 @@ describe('xiaohongshu creator-notes', () => {
|
|
|
41
41
|
const bodyText = `笔记管理
|
|
42
42
|
全部笔记(366)
|
|
43
43
|
已发布
|
|
44
|
-
|
|
44
|
+
测试笔记一
|
|
45
45
|
发布于 2025年12月04日 19:45
|
|
46
46
|
148208
|
|
47
47
|
324
|
|
@@ -53,7 +53,7 @@ describe('xiaohongshu creator-notes', () => {
|
|
|
53
53
|
编辑
|
|
54
54
|
删除
|
|
55
55
|
仅自己可见
|
|
56
|
-
|
|
56
|
+
测试笔记二
|
|
57
57
|
发布于 2026年03月18日 12:39
|
|
58
58
|
10
|
|
59
59
|
0
|
|
@@ -64,7 +64,7 @@ describe('xiaohongshu creator-notes', () => {
|
|
|
64
64
|
expect(parseCreatorNotesText(bodyText)).toEqual([
|
|
65
65
|
{
|
|
66
66
|
id: '',
|
|
67
|
-
title: '
|
|
67
|
+
title: '测试笔记一',
|
|
68
68
|
date: '2025年12月04日 19:45',
|
|
69
69
|
views: 148208,
|
|
70
70
|
likes: 2279,
|
|
@@ -74,7 +74,7 @@ describe('xiaohongshu creator-notes', () => {
|
|
|
74
74
|
},
|
|
75
75
|
{
|
|
76
76
|
id: '',
|
|
77
|
-
title: '
|
|
77
|
+
title: '测试笔记二',
|
|
78
78
|
date: '2026年03月18日 12:39',
|
|
79
79
|
views: 10,
|
|
80
80
|
likes: 0,
|
|
@@ -98,7 +98,7 @@ describe('xiaohongshu creator-notes', () => {
|
|
|
98
98
|
4
|
|
99
99
|
5
|
|
100
100
|
权限设置`,
|
|
101
|
-
html: '"noteId":"
|
|
101
|
+
html: '"noteId":"aaaaaaaaaaaaaaaaaaaaaaaa"',
|
|
102
102
|
},
|
|
103
103
|
]);
|
|
104
104
|
const result = await cmd.func(page, { limit: 1 });
|
|
@@ -106,14 +106,14 @@ describe('xiaohongshu creator-notes', () => {
|
|
|
106
106
|
expect(result).toEqual([
|
|
107
107
|
{
|
|
108
108
|
rank: 1,
|
|
109
|
-
id: '
|
|
109
|
+
id: 'aaaaaaaaaaaaaaaaaaaaaaaa',
|
|
110
110
|
title: '示例笔记',
|
|
111
111
|
date: '2026年03月19日 12:00',
|
|
112
112
|
views: 10,
|
|
113
113
|
likes: 3,
|
|
114
114
|
collects: 4,
|
|
115
115
|
comments: 2,
|
|
116
|
-
url: 'https://creator.xiaohongshu.com/statistics/note-detail?noteId=
|
|
116
|
+
url: 'https://creator.xiaohongshu.com/statistics/note-detail?noteId=aaaaaaaaaaaaaaaaaaaaaaaa',
|
|
117
117
|
},
|
|
118
118
|
]);
|
|
119
119
|
});
|
|
@@ -124,8 +124,8 @@ describe('xiaohongshu creator-notes', () => {
|
|
|
124
124
|
undefined,
|
|
125
125
|
[
|
|
126
126
|
{
|
|
127
|
-
id: '
|
|
128
|
-
title: '
|
|
127
|
+
id: 'bbbbbbbbbbbbbbbbbbbbbbbb',
|
|
128
|
+
title: '测试笔记一',
|
|
129
129
|
date: '2025年12月04日 19:45',
|
|
130
130
|
metrics: [148284, 319, 2280, 466, 33],
|
|
131
131
|
},
|
|
@@ -135,14 +135,14 @@ describe('xiaohongshu creator-notes', () => {
|
|
|
135
135
|
expect(result).toEqual([
|
|
136
136
|
{
|
|
137
137
|
rank: 1,
|
|
138
|
-
id: '
|
|
139
|
-
title: '
|
|
138
|
+
id: 'bbbbbbbbbbbbbbbbbbbbbbbb',
|
|
139
|
+
title: '测试笔记一',
|
|
140
140
|
date: '2025年12月04日 19:45',
|
|
141
141
|
views: 148284,
|
|
142
142
|
likes: 2280,
|
|
143
143
|
collects: 466,
|
|
144
144
|
comments: 319,
|
|
145
|
-
url: 'https://creator.xiaohongshu.com/statistics/note-detail?noteId=
|
|
145
|
+
url: 'https://creator.xiaohongshu.com/statistics/note-detail?noteId=bbbbbbbbbbbbbbbbbbbbbbbb',
|
|
146
146
|
},
|
|
147
147
|
]);
|
|
148
148
|
});
|
|
@@ -153,8 +153,8 @@ describe('xiaohongshu creator-notes', () => {
|
|
|
153
153
|
data: {
|
|
154
154
|
note_infos: [
|
|
155
155
|
{
|
|
156
|
-
id: '
|
|
157
|
-
title: '
|
|
156
|
+
id: 'cccccccccccccccccccccccc',
|
|
157
|
+
title: '示例内容复盘',
|
|
158
158
|
post_time: new Date('2026-03-18T20:01:00+08:00').getTime(),
|
|
159
159
|
read_count: 521,
|
|
160
160
|
like_count: 18,
|
|
@@ -169,26 +169,26 @@ describe('xiaohongshu creator-notes', () => {
|
|
|
169
169
|
expect(result).toEqual([
|
|
170
170
|
{
|
|
171
171
|
rank: 1,
|
|
172
|
-
id: '
|
|
173
|
-
title: '
|
|
172
|
+
id: 'cccccccccccccccccccccccc',
|
|
173
|
+
title: '示例内容复盘',
|
|
174
174
|
date: '2026年03月18日 20:01',
|
|
175
175
|
views: 521,
|
|
176
176
|
likes: 18,
|
|
177
177
|
collects: 10,
|
|
178
178
|
comments: 7,
|
|
179
|
-
url: 'https://creator.xiaohongshu.com/statistics/note-detail?noteId=
|
|
179
|
+
url: 'https://creator.xiaohongshu.com/statistics/note-detail?noteId=cccccccccccccccccccccccc',
|
|
180
180
|
},
|
|
181
181
|
]);
|
|
182
182
|
});
|
|
183
183
|
it('extracts note ids from creator note-manager html', () => {
|
|
184
184
|
const html = `
|
|
185
|
-
<div>"noteId":"
|
|
186
|
-
<div>"noteId":"
|
|
187
|
-
<div>"noteId":"
|
|
185
|
+
<div>"noteId":"aaaaaaaaaaaaaaaaaaaaaaaa"</div>
|
|
186
|
+
<div>"noteId":"dddddddddddddddddddddddd"</div>
|
|
187
|
+
<div>"noteId":"aaaaaaaaaaaaaaaaaaaaaaaa"</div>
|
|
188
188
|
`;
|
|
189
189
|
expect(parseCreatorNoteIdsFromHtml(html)).toEqual([
|
|
190
|
-
'
|
|
191
|
-
'
|
|
190
|
+
'aaaaaaaaaaaaaaaaaaaaaaaa',
|
|
191
|
+
'dddddddddddddddddddddddd',
|
|
192
192
|
]);
|
|
193
193
|
});
|
|
194
194
|
});
|
|
@@ -19,6 +19,7 @@ cli({
|
|
|
19
19
|
columns: ['field', 'value'],
|
|
20
20
|
func: async (page, kwargs) => {
|
|
21
21
|
const raw = String(kwargs['note-id']);
|
|
22
|
+
const isBareNoteId = !/^https?:\/\//.test(raw.trim());
|
|
22
23
|
const noteId = parseNoteId(raw);
|
|
23
24
|
const url = buildNoteUrl(raw);
|
|
24
25
|
await page.goto(url);
|
|
@@ -60,6 +61,16 @@ cli({
|
|
|
60
61
|
// XHS renders placeholder text like "赞"/"收藏"/"评论" when count is 0;
|
|
61
62
|
// normalize to '0' unless the value looks numeric.
|
|
62
63
|
const numOrZero = (v) => /^\d+/.test(v) ? v : '0';
|
|
64
|
+
// XHS sometimes renders an empty shell page for bare /explore/<id> visits
|
|
65
|
+
// when the request lacks a valid xsec_token. Title + author are always
|
|
66
|
+
// present on a real note, so their absence is the simplest reliable signal.
|
|
67
|
+
const emptyShell = !d.title && !d.author;
|
|
68
|
+
if (emptyShell) {
|
|
69
|
+
if (isBareNoteId) {
|
|
70
|
+
throw new EmptyResultError('xiaohongshu/note', 'Pass the full search_result URL with xsec_token, for example from `opencli xiaohongshu search`, instead of a bare note ID.');
|
|
71
|
+
}
|
|
72
|
+
throw new EmptyResultError('xiaohongshu/note', 'The note page loaded without visible content. Retry with a fresh URL or run with --verbose; if it persists, the page structure may have changed.');
|
|
73
|
+
}
|
|
63
74
|
const rows = [
|
|
64
75
|
{ field: 'title', value: d.title || '' },
|
|
65
76
|
{ field: 'author', value: d.author || '' },
|