Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
5 changes: 4 additions & 1 deletion clis/douyin/draft.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,12 @@ function createPageMock(
getInterceptedRequests: vi.fn().mockResolvedValue([]),
waitForCapture: vi.fn().mockResolvedValue(undefined),
screenshot: vi.fn().mockResolvedValue(''),
startNetworkCapture: vi.fn().mockResolvedValue(undefined),
readNetworkCapture: vi.fn().mockResolvedValue([]),
stopCapture: vi.fn().mockResolvedValue(undefined),
setFileInput: vi.fn().mockResolvedValue(undefined),
...overrides,
};
} as IPage;
}

describe('douyin draft registration', () => {
Expand Down
5 changes: 4 additions & 1 deletion clis/facebook/search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,16 @@ function createMockPage(): IPage {
tabs: vi.fn().mockResolvedValue([]),
selectTab: vi.fn(),
networkRequests: vi.fn().mockResolvedValue([]),
consoleMessages: vi.fn().mockResolvedValue(''),
consoleMessages: vi.fn().mockResolvedValue([]),
scroll: vi.fn(),
autoScroll: vi.fn(),
installInterceptor: vi.fn(),
getInterceptedRequests: vi.fn().mockResolvedValue([]),
waitForCapture: vi.fn().mockResolvedValue(undefined),
screenshot: vi.fn().mockResolvedValue(''),
startNetworkCapture: vi.fn().mockResolvedValue(undefined),
readNetworkCapture: vi.fn().mockResolvedValue([]),
stopCapture: vi.fn().mockResolvedValue(undefined),
};
}

Expand Down
10 changes: 9 additions & 1 deletion clis/hackernews/jobs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,15 @@ pipeline:
rank: ${{ index + 1 }}
title: ${{ item.title }}
author: ${{ item.by }}
url: ${{ item.url }}
url: >-
${{
item.url
|| item.text?.match(/href="([^"]+)"/)?.[1]
?.replace(///g, '/')
?.replace(/&/g, '&')
?.replace(/'/g, "'")
|| ('https://news.ycombinator.com/item?id=' + item.id)
}}

- limit: ${{ args.limit }}

Expand Down
27 changes: 27 additions & 0 deletions clis/instagram/_shared/private-publish.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,33 @@ describe('instagram private publish helpers', () => {
expect(evaluateAttempts).toBe(2);
});

it('resolves private publish config when capture metadata is unavailable', async () => {
const page = {
goto: async () => undefined,
wait: async () => undefined,
getCookies: async () => [{ name: 'csrftoken', value: 'csrf-cookie', domain: 'instagram.com' }],
startNetworkCapture: async () => undefined,
readNetworkCapture: async () => [{ url: 'https://www.instagram.com/api/v1/feed/timeline/', method: 'GET' }],
evaluate: async () => ({
appId: '936619743392459',
csrfToken: 'csrf-from-html',
instagramAjax: '1036523242',
}),
} as any;

await expect(resolveInstagramPrivatePublishConfig(page)).resolves.toEqual({
apiContext: {
asbdId: '',
csrfToken: 'csrf-from-html',
igAppId: '936619743392459',
igWwwClaim: '',
instagramAjax: '1036523242',
webSessionId: '',
},
jazoest: deriveInstagramJazoest('csrf-from-html'),
});
});

it('builds the single-image configure form body', () => {
expect(buildConfigureBody({
uploadId: '1775134280303',
Expand Down
11 changes: 4 additions & 7 deletions clis/instagram/_shared/private-publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as path from 'node:path';
import { spawnSync } from 'node:child_process';

import { CommandExecutionError } from '@jackwener/opencli/errors';
import type { BrowserCookie, IPage } from '@jackwener/opencli/types';
import type { BrowserCookie, CaptureCapablePage, IPage } from '@jackwener/opencli/types';
import type { InstagramProtocolCaptureEntry } from './protocol-capture.js';
import { instagramPrivateApiFetch } from './protocol-capture.js';
import {
Expand Down Expand Up @@ -174,21 +174,18 @@ export async function resolveInstagramPrivatePublishConfig(page: IPage): Promise
apiContext: InstagramPrivateApiContext;
jazoest: string;
}> {
const capturePage = page as CaptureCapablePage;
let lastError: unknown;
for (let attempt = 0; attempt < INSTAGRAM_PRIVATE_CONFIG_RETRY_BUDGET; attempt += 1) {
try {
if (typeof page.startNetworkCapture === 'function') {
await page.startNetworkCapture(INSTAGRAM_PRIVATE_CAPTURE_PATTERN);
}
await capturePage.startNetworkCapture(INSTAGRAM_PRIVATE_CAPTURE_PATTERN);
await page.goto(`${INSTAGRAM_HOME_URL}?__opencli_private_probe=${Date.now()}`);
await page.wait({ time: 2 });

const [cookies, runtime, entries] = await Promise.all([
page.getCookies({ domain: 'instagram.com' }),
page.evaluate(buildReadInstagramRuntimeInfoJs()) as Promise<InstagramRuntimeInfo>,
typeof page.readNetworkCapture === 'function'
? page.readNetworkCapture() as Promise<unknown[]>
: Promise.resolve([]),
capturePage.readNetworkCapture() as Promise<unknown[]>,
]);

const captureEntries = (Array.isArray(entries) ? entries : []) as InstagramProtocolCaptureEntry[];
Expand Down
39 changes: 37 additions & 2 deletions clis/instagram/_shared/protocol-capture.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,27 @@ describe('instagram protocol capture helpers', () => {
it('prefers native page network capture when available', async () => {
const startNetworkCapture = vi.fn().mockResolvedValue(undefined);
const evaluate = vi.fn();
const page = { startNetworkCapture, evaluate } as unknown as IPage;
const hasNativeCaptureSupport = vi.fn().mockReturnValue(true);
const page = { startNetworkCapture, evaluate, hasNativeCaptureSupport } as unknown as IPage;

await installInstagramProtocolCapture(page);

expect(startNetworkCapture).toHaveBeenCalledTimes(1);
expect(evaluate).not.toHaveBeenCalled();
});

it('falls back to the page patch when native capture is unavailable after start', async () => {
const startNetworkCapture = vi.fn().mockResolvedValue(undefined);
const hasNativeCaptureSupport = vi.fn().mockReturnValue(false);
const evaluate = vi.fn().mockResolvedValue({ ok: true });
const page = { startNetworkCapture, evaluate, hasNativeCaptureSupport } as unknown as IPage;

await installInstagramProtocolCapture(page);

expect(startNetworkCapture).toHaveBeenCalledTimes(1);
expect(evaluate).toHaveBeenCalledTimes(1);
});

it('reads and normalizes captured protocol entries', async () => {
const evaluate = vi.fn().mockResolvedValue({
data: [{ kind: 'fetch', url: 'https://www.instagram.com/api/v1/media/configure/' }],
Expand All @@ -62,7 +75,8 @@ describe('instagram protocol capture helpers', () => {
{ kind: 'cdp', url: 'https://www.instagram.com/rupload_igphoto/test', method: 'POST' },
]);
const evaluate = vi.fn();
const page = { readNetworkCapture, evaluate } as unknown as IPage;
const hasNativeCaptureSupport = vi.fn().mockReturnValue(true);
const page = { readNetworkCapture, evaluate, hasNativeCaptureSupport } as unknown as IPage;

const result = await readInstagramProtocolCapture(page);

Expand All @@ -74,6 +88,27 @@ describe('instagram protocol capture helpers', () => {
});
});

it('falls back to the page patch read when native capture is unavailable after read', async () => {
const readNetworkCapture = vi.fn().mockResolvedValue([
{ url: 'https://www.instagram.com/api/v1/web/resource/', method: 'GET' },
]);
const hasNativeCaptureSupport = vi.fn().mockReturnValue(false);
const evaluate = vi.fn().mockResolvedValue({
data: [{ kind: 'fetch', url: 'https://www.instagram.com/api/v1/media/configure/', method: 'POST' }],
errors: ['fallback-used'],
});
const page = { readNetworkCapture, evaluate, hasNativeCaptureSupport } as unknown as IPage;

const result = await readInstagramProtocolCapture(page);

expect(readNetworkCapture).toHaveBeenCalledTimes(1);
expect(evaluate).toHaveBeenCalledTimes(1);
expect(result).toEqual({
data: [{ kind: 'fetch', url: 'https://www.instagram.com/api/v1/media/configure/', method: 'POST' }],
errors: ['fallback-used'],
});
});

it('dumps protocol traces to /tmp only when capture env is enabled', async () => {
process.env.OPENCLI_INSTAGRAM_CAPTURE = '1';
const page = {
Expand Down
22 changes: 17 additions & 5 deletions clis/instagram/_shared/protocol-capture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ export interface InstagramProtocolCaptureEntry {
timestamp: number;
}

type CaptureAwarePage = IPage & {
hasNativeCaptureSupport?: () => boolean | undefined;
};

function hasNativeCaptureSupport(page: IPage): boolean | undefined {
return (page as CaptureAwarePage).hasNativeCaptureSupport?.();
}

export function buildInstallInstagramProtocolCaptureJs(
captureVar: string = DEFAULT_CAPTURE_VAR,
captureErrorsVar: string = DEFAULT_CAPTURE_ERRORS_VAR,
Expand Down Expand Up @@ -226,7 +234,9 @@ export async function installInstagramProtocolCapture(page: IPage): Promise<void
if (typeof page.startNetworkCapture === 'function') {
try {
await page.startNetworkCapture(INSTAGRAM_PROTOCOL_CAPTURE_PATTERN);
return;
if (hasNativeCaptureSupport(page) !== false) {
return;
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (!message.includes('Unknown action') && !message.includes('network-capture')) {
Expand All @@ -244,10 +254,12 @@ export async function readInstagramProtocolCapture(page: IPage): Promise<{
if (typeof page.readNetworkCapture === 'function') {
try {
const data = await page.readNetworkCapture();
return {
data: Array.isArray(data) ? data as InstagramProtocolCaptureEntry[] : [],
errors: [],
};
if (hasNativeCaptureSupport(page) !== false) {
return {
data: Array.isArray(data) ? data as InstagramProtocolCaptureEntry[] : [],
errors: [],
};
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (!message.includes('Unknown action') && !message.includes('network-capture')) {
Expand Down
3 changes: 3 additions & 0 deletions clis/instagram/note.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ function createPageMock(): IPage {
setFileInput: vi.fn().mockResolvedValue(undefined),
insertText: vi.fn().mockResolvedValue(undefined),
getCurrentUrl: vi.fn().mockResolvedValue(null),
startNetworkCapture: vi.fn().mockResolvedValue(undefined),
readNetworkCapture: vi.fn().mockResolvedValue([]),
stopCapture: vi.fn().mockResolvedValue(undefined),
};
}

Expand Down
13 changes: 9 additions & 4 deletions clis/instagram/post.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,14 @@ function createPageMock(evaluateResults: unknown[], overrides: Partial<IPage> =
getInterceptedRequests: vi.fn().mockResolvedValue([]),
waitForCapture: vi.fn().mockResolvedValue(undefined),
screenshot: vi.fn().mockResolvedValue(''),
startNetworkCapture: vi.fn().mockResolvedValue(undefined),
readNetworkCapture: vi.fn().mockResolvedValue([]),
stopCapture: vi.fn().mockResolvedValue(undefined),
setFileInput: vi.fn().mockResolvedValue(undefined),
insertText: undefined,
getCurrentUrl: vi.fn().mockResolvedValue(null),
...overrides,
};
} as IPage;
}

afterAll(() => {
Expand Down Expand Up @@ -708,7 +711,7 @@ describe('instagram post registration', () => {
]);
});

it('installs and dumps protocol capture when OPENCLI_INSTAGRAM_CAPTURE is enabled', async () => {
it('uses native protocol capture and dumps traces when OPENCLI_INSTAGRAM_CAPTURE is enabled', async () => {
process.env.OPENCLI_INSTAGRAM_CAPTURE = '1';
const imagePath = createTempImage('capture-enabled.jpg');
const evaluate = vi.fn(async (js: string) => {
Expand All @@ -735,8 +738,10 @@ describe('instagram post registration', () => {
});

const evaluateCalls = evaluate.mock.calls.map((args) => String(args[0]));
expect(evaluateCalls.some((js) => js.includes('__opencli_ig_protocol_capture') && js.includes('PATCH_GUARD'))).toBe(true);
expect(evaluateCalls.some((js) => js.includes('const data = Array.isArray(window[') && js.includes('__opencli_ig_protocol_capture'))).toBe(true);
expect((page.startNetworkCapture as ReturnType<typeof vi.fn>)).toHaveBeenCalled();
expect((page.readNetworkCapture as ReturnType<typeof vi.fn>)).toHaveBeenCalled();
expect(evaluateCalls.some((js) => js.includes('__opencli_ig_protocol_capture') && js.includes('PATCH_GUARD'))).toBe(false);
expect(evaluateCalls.some((js) => js.includes('const data = Array.isArray(window[') && js.includes('__opencli_ig_protocol_capture'))).toBe(false);
expect(result).toEqual([
{
status: '✅ Posted',
Expand Down
5 changes: 4 additions & 1 deletion clis/instagram/reel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,11 @@ function createPageMock(evaluateResults: unknown[], overrides: Partial<IPage> =
setFileInput: vi.fn().mockResolvedValue(undefined),
insertText: vi.fn().mockResolvedValue(undefined),
getCurrentUrl: vi.fn().mockResolvedValue(null),
startNetworkCapture: vi.fn().mockResolvedValue(undefined),
readNetworkCapture: vi.fn().mockResolvedValue([]),
stopCapture: vi.fn().mockResolvedValue(undefined),
...overrides,
};
} as IPage;
}

afterAll(() => {
Expand Down
6 changes: 2 additions & 4 deletions clis/instagram/reel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as path from 'node:path';
import { Page as BrowserPage } from '@jackwener/opencli/browser/page';
import { cli, Strategy } from '@jackwener/opencli/registry';
import { ArgumentError, AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import type { BrowserCookie, IPage } from '@jackwener/opencli/types';
import type { BrowserCookie, CaptureCapablePage, IPage } from '@jackwener/opencli/types';
import {
buildClickActionJs,
buildEnsureComposerOpenJs,
Expand Down Expand Up @@ -809,9 +809,7 @@ cli({
activePage: IPage,
existingMediaPaths: ReadonlySet<string> = new Set(),
): Promise<InstagramReelSuccessRow[]> => {
if (typeof activePage.startNetworkCapture === 'function') {
await activePage.startNetworkCapture('/rupload_igvideo/|/api/v1/|/reel/|/clips/|/media/|/configure|/upload');
}
await (activePage as CaptureCapablePage).startNetworkCapture('/rupload_igvideo/|/api/v1/|/reel/|/clips/|/media/|/configure|/upload');
await gotoInstagramHome(activePage, true);
await activePage.wait({ time: 2 });
await dismissResidualDialogs(activePage);
Expand Down
5 changes: 4 additions & 1 deletion clis/instagram/story.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,11 @@ function createPageMock(evaluateResults: unknown[] = [], overrides: Partial<IPag
setFileInput: vi.fn().mockResolvedValue(undefined),
insertText: vi.fn().mockResolvedValue(undefined),
getCurrentUrl: vi.fn().mockResolvedValue(null),
startNetworkCapture: vi.fn().mockResolvedValue(undefined),
readNetworkCapture: vi.fn().mockResolvedValue([]),
stopCapture: vi.fn().mockResolvedValue(undefined),
...overrides,
};
} as IPage;
}

afterAll(() => {
Expand Down
3 changes: 3 additions & 0 deletions clis/substack/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ function createPageMock(evaluateResult: unknown): IPage {
getCookies: vi.fn().mockResolvedValue([]),
screenshot: vi.fn().mockResolvedValue(''),
waitForCapture: vi.fn().mockResolvedValue(undefined),
startNetworkCapture: vi.fn().mockResolvedValue(undefined),
readNetworkCapture: vi.fn().mockResolvedValue([]),
stopCapture: vi.fn().mockResolvedValue(undefined),
};
}

Expand Down
5 changes: 4 additions & 1 deletion clis/twitter/reply.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,11 @@ function createPageMock(evaluateResults: any[], overrides: Partial<IPage> = {}):
getCookies: vi.fn().mockResolvedValue([]),
screenshot: vi.fn().mockResolvedValue(''),
waitForCapture: vi.fn().mockResolvedValue(undefined),
startNetworkCapture: vi.fn().mockResolvedValue(undefined),
readNetworkCapture: vi.fn().mockResolvedValue([]),
stopCapture: vi.fn().mockResolvedValue(undefined),
...overrides,
};
} as IPage;
}

describe('twitter reply command', () => {
Expand Down
3 changes: 3 additions & 0 deletions clis/xianyu/item.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ function createPageMock(evaluateResult: unknown): IPage {
getCookies: vi.fn().mockResolvedValue([]),
screenshot: vi.fn().mockResolvedValue(''),
waitForCapture: vi.fn().mockResolvedValue(undefined),
startNetworkCapture: vi.fn().mockResolvedValue(undefined),
readNetworkCapture: vi.fn().mockResolvedValue([]),
stopCapture: vi.fn().mockResolvedValue(undefined),
} as IPage;
}

Expand Down
3 changes: 3 additions & 0 deletions clis/xiaohongshu/comments.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ function createPageMock(evaluateResult: any): IPage {
getCookies: vi.fn().mockResolvedValue([]),
screenshot: vi.fn().mockResolvedValue(''),
waitForCapture: vi.fn().mockResolvedValue(undefined),
startNetworkCapture: vi.fn().mockResolvedValue(undefined),
readNetworkCapture: vi.fn().mockResolvedValue([]),
stopCapture: vi.fn().mockResolvedValue(undefined),
};
}

Expand Down
3 changes: 3 additions & 0 deletions clis/xiaohongshu/creator-note-detail.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ function createPageMock(evaluateResult: any): IPage {
getCookies: vi.fn().mockResolvedValue([]),
screenshot: vi.fn().mockResolvedValue(''),
waitForCapture: vi.fn().mockResolvedValue(undefined),
startNetworkCapture: vi.fn().mockResolvedValue(undefined),
readNetworkCapture: vi.fn().mockResolvedValue([]),
stopCapture: vi.fn().mockResolvedValue(undefined),
};
}

Expand Down
3 changes: 3 additions & 0 deletions clis/xiaohongshu/creator-notes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ function createPageMock(evaluateResult: any, interceptedRequests: any[] = []): I
getCookies: vi.fn().mockResolvedValue([]),
screenshot: vi.fn().mockResolvedValue(''),
waitForCapture: vi.fn().mockResolvedValue(undefined),
startNetworkCapture: vi.fn().mockResolvedValue(undefined),
readNetworkCapture: vi.fn().mockResolvedValue([]),
stopCapture: vi.fn().mockResolvedValue(undefined),
};
}

Expand Down
3 changes: 3 additions & 0 deletions clis/xiaohongshu/download.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ function createPageMock(evaluateResult: any): IPage {
getCookies: vi.fn().mockResolvedValue([{ name: 'sid', value: 'secret', domain: '.xiaohongshu.com' }]),
screenshot: vi.fn().mockResolvedValue(''),
waitForCapture: vi.fn().mockResolvedValue(undefined),
startNetworkCapture: vi.fn().mockResolvedValue(undefined),
readNetworkCapture: vi.fn().mockResolvedValue([]),
stopCapture: vi.fn().mockResolvedValue(undefined),
};
}

Expand Down
Loading
Loading