page-analyzer 1.2.1 → 1.2.3

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/.env.example ADDED
@@ -0,0 +1,36 @@
1
+ # ----------------------------------------------------------------------------
2
+ # LLM backend
3
+ # ----------------------------------------------------------------------------
4
+ # Three backends are supported via LLM_TYPE:
5
+ # openai - OpenAI-compatible HTTP API (default; needs API key + endpoint)
6
+ # codex - Local `codex exec` CLI (uses your local codex auth, no API key)
7
+ # claude - Local `claude -p` CLI (uses your local claude auth, no API key)
8
+ LLM_TYPE=openai
9
+
10
+ # Model name. Required for all backends.
11
+ # openai : e.g. gpt-4, gpt-4o, gpt-5-mini, ...
12
+ # codex : e.g. gpt-5-codex, gpt-5.5 (gpt-5.5 auto-enables service_tier=fast)
13
+ # claude : e.g. sonnet, opus, haiku, claude-sonnet-4-6, ...
14
+ LLM_MODEL=gpt-4
15
+
16
+ # Required only when LLM_TYPE=openai
17
+ LLM_API_ENDPOINT=https://api.openai.com/v1/chat/completions
18
+ LLM_API_KEY=sk-your-openai-or-compatible-key
19
+
20
+ # ----------------------------------------------------------------------------
21
+ # S3 screenshot upload (optional)
22
+ # Only used when the calling code wires `extractorConfig.s3` from these vars
23
+ # (page-analyzer itself does not read them). Leave unset to keep screenshots
24
+ # on local disk.
25
+ # ----------------------------------------------------------------------------
26
+ # S3_BUCKET=my-page-analyzer-bucket
27
+ # S3_REGION=ap-northeast-1
28
+ # S3_PREFIX=page-analyzer/snapshots
29
+ # S3_PUBLIC_BASE_URL=https://cdn.example.com
30
+ # S3_ACCESS_KEY_ID=
31
+ # S3_SECRET_ACCESS_KEY=
32
+
33
+ # ----------------------------------------------------------------------------
34
+ # Result viewer (scripts/serve-result-viewer.js)
35
+ # ----------------------------------------------------------------------------
36
+ # PORT=4173
package/README.md CHANGED
@@ -114,6 +114,8 @@ const result = await analyzeUrl('https://example.com', {
114
114
  },
115
115
  showEvents: true,
116
116
  showBlockIdx: true,
117
+ showElement: true,
118
+ elementSize: 24,
117
119
  fullPageScreenshot: true,
118
120
  blockScreenshots: true,
119
121
  waitForImagesLoaded: true,
@@ -148,6 +150,8 @@ const result = await analyzeUrl('https://example.com', {
148
150
  | `options.extractorConfig` | `object` | 否 | Playwright 页面抓取配置 |
149
151
  | `options.showEvents` | `boolean` | 否 | 是否返回完整事件数组和元素明细 |
150
152
  | `options.showBlockIdx` | `boolean` | 否 | 是否返回 CSV 与区块索引相关字段 |
153
+ | `options.showElement` | `boolean` | 否 | 是否采集所有"有尺寸"的可见 DOM 元素并嵌套到各区块的 `elements` 下,默认 `false`(不开启时走之前的逻辑,不采集) |
154
+ | `options.elementSize` | `number` | 否 | `showElement` 的尺寸阈值(px),元素满足 `width > elementSize 或 height > elementSize` 才返回,默认 `24` |
151
155
  | `options.fullPageScreenshot` | `boolean` | 否 | 是否保存整页截图到当前运行目录的 `snapshots/` 并返回文件路径 |
152
156
  | `options.blockScreenshots` | `boolean` | 否 | 是否在 LLM 合并区块后,保存每个逻辑区块截图到当前运行目录的 `snapshots/` 并返回文件路径 |
153
157
  | `options.waitForImagesLoaded` | `boolean` | 否 | 是否在提取区块、分析和截图前等待页面图片加载完成,默认 `false` |
@@ -189,6 +193,7 @@ const result = await analyzePageEvents({
189
193
  | `url` | `string` | 是 | 页面 URL,用于解析相对链接 |
190
194
  | `blocks` | `Array` | 否 | 视觉区块快照 |
191
195
  | `elementGeometries` | `Array` | 否 | 页面中交互元素的几何信息 |
196
+ | `sizedElements` | `Array` | 否 | 页面中所有"有尺寸"的可见元素(`width > 24 或 height > 24`),会按几何位置嵌套到对应区块的 `elements` 下 |
192
197
  | `llm` | `object` | 是 | LLM 配置 |
193
198
  | `knownEventTypes` | `string[]` | 否 | 已知事件类型 |
194
199
  | `parserConfig` | `object` | 否 | HTML 解析配置 |
@@ -223,7 +228,26 @@ const result = await analyzePageEvents({
223
228
  blockDescription: '...',
224
229
  blockSemantics: [],
225
230
  blockCssPath: '...',
226
- blockPosition: {}
231
+ blockPosition: {},
232
+ elements: [
233
+ {
234
+ tag: 'a',
235
+ text: 'Sign up',
236
+ href: 'https://example.com/signup',
237
+ src: '',
238
+ width: 80,
239
+ height: 30,
240
+ top: 0,
241
+ left: 0,
242
+ cssSelector: 'body > main:nth-of-type(1) > section:nth-of-type(1) > a:nth-of-type(1)',
243
+ id: '',
244
+ class: 'cta',
245
+ role: '',
246
+ ariaLabel: '',
247
+ imageAlt: '',
248
+ interactive: true
249
+ }
250
+ ]
227
251
  }
228
252
  ],
229
253
  stats: {
@@ -234,6 +258,8 @@ const result = await analyzePageEvents({
234
258
  }
235
259
  ```
236
260
 
261
+ 启用 `showElement: true` 后,每个区块会带上 `elements` 数组,列出该区块下所有"有尺寸"的可见 DOM 元素(任意标签,`width > elementSize 或 height > elementSize`,默认阈值 24),按 `top, left` 排序;每个元素携带原始信息:`tag, text, href, src, width, height, top, left, cssSelector, id, class, role, ariaLabel, imageAlt, interactive`(坐标含滚动偏移)。极少数无法归入任何区块的元素(页面无视觉区块时)会放在 `analysis.block_analysis.unassignedElements`。不开启 `showElement` 时不会采集这些元素,走之前的逻辑。阈值通过 `elementSize`(或 `extractorConfig.sizedElementMinSize`)调整。
262
+
237
263
  启用 `showEvents: true` 后,输出会包含更多用于调试和下游处理的字段,例如:
238
264
 
239
265
  - `elements`:解析出的页面元素明细
@@ -250,7 +276,7 @@ const result = await analyzePageEvents({
250
276
 
251
277
  启用 `blockScreenshots: true` 后,模块会在 LLM 合并区块后再截图。返回结果会包含 `screenshots.blocks`,每项包含逻辑区块序号 `blockIdx` 和对应截图 `path`;区块分析结果中的每个 block 也会额外带上 `blockScreenshotPaths`,每个逻辑区块最多对应一张截图。无法通过 `blockCssPath` 截图的隐藏或空区块会被跳过。
252
278
 
253
- 如果配置 `extractorConfig.s3`,截图不会写入本地 `snapshots/`,而是直接上传到 S3;`screenshots.fullPage`、`screenshots.blocks[].path` 和 `blockScreenshotPaths` 会返回 HTTPS URL。上传不会设置 ACL,访问权限沿用 bucket 策略。单张截图上传失败会重试 3 次,仍失败则跳过该截图。
279
+ 如果配置 `extractorConfig.s3`,截图不会写入本地 `snapshots/`,而是直接上传到 S3;`screenshots.fullPage`、`screenshots.blocks[].path` 和 `blockScreenshotPaths` 会返回 HTTPS URL。S3 对象 key 使用 `<prefix>/<domain>/<file-md5>.png`,上传前会先检查对象是否已存在,已存在时直接返回对应 URL,避免重复上传和冗余对象。上传不会设置 ACL,访问权限沿用 bucket 策略。单张截图检查或上传失败会重试 3 次,仍失败则跳过该截图。
254
280
 
255
281
  启用 `waitForImagesLoaded: true` 后,模块会先滚动页面触发懒加载,再等待当前 DOM 中的 `<img>` 完成加载或失败,之后再提取区块、分析和截图;等待时间受 `extractorConfig.timeoutMs` 控制。
256
282
 
@@ -316,7 +342,7 @@ const result = await analyzeUrl('https://example.com', {
316
342
  });
317
343
  ```
318
344
 
319
- `extractorConfig.s3.bucket` 和 `extractorConfig.s3.region` 必填。`credentials` 可省略,省略时使用 AWS SDK 默认凭证链。`publicBaseUrl` 可省略,省略时返回 `https://<bucket>.s3.<region>.amazonaws.com/<key>`;配置后返回 `${publicBaseUrl}/<key>`。
345
+ `extractorConfig.s3.bucket` 和 `extractorConfig.s3.region` 必填。`credentials` 可省略,省略时使用 AWS SDK 默认凭证链。`publicBaseUrl` 可省略,省略时返回 `https://<bucket>.s3.<region>.amazonaws.com/<key>`;配置后返回 `${publicBaseUrl}/<key>`。启用 S3 上传时,需要凭证具备 `s3:GetObject` 和 `s3:PutObject` 权限;如果希望不存在的对象能被稳定识别为 404,还需要对应 bucket/prefix 的 `s3:ListBucket` 权限。
320
346
 
321
347
  ### parserConfig
322
348
 
@@ -56,7 +56,7 @@ function overlapArea(rectA, rectB) {
56
56
  return width * height;
57
57
  }
58
58
 
59
- function mapRectToBlock(rect, blocks = []) {
59
+ export function mapRectToBlock(rect, blocks = []) {
60
60
  const hasRect = rect && rect.width > 0 && rect.height > 0;
61
61
  if (!hasRect || !Array.isArray(blocks) || blocks.length === 0) {
62
62
  return -1;
package/index.d.ts ADDED
@@ -0,0 +1,318 @@
1
+ /**
2
+ * Type declarations for page-analyzer.
3
+ *
4
+ * Standalone module: Playwright → HTML parse → block assign → CSV → LLM
5
+ * block/event analysis. Authored to mirror the runtime shapes produced by
6
+ * index.js (analyzeUrl / analyzePageEvents) and the PageExtractor bundle.
7
+ */
8
+
9
+ export type LlmProviderType = 'openai' | 'codex' | 'claude';
10
+
11
+ export const LLM_PROVIDER_TYPES: readonly LlmProviderType[];
12
+
13
+ export interface LlmConfig {
14
+ /** Backend type. Default: 'openai'. */
15
+ type?: LlmProviderType;
16
+ /** Model name (required for all backends). */
17
+ model: string;
18
+ /** API key (required when type === 'openai'). */
19
+ apiKey?: string;
20
+ /** API endpoint URL (required when type === 'openai'). */
21
+ apiEndpoint?: string;
22
+ /** Override CLI binary path (codex/claude). */
23
+ cliPath?: string;
24
+ /** Working directory for the CLI child process (codex/claude). */
25
+ cwd?: string;
26
+ /** Codex only; auto-enabled when model === 'gpt-5.5'. */
27
+ fast?: boolean;
28
+ /** Max tokens (openai only). */
29
+ maxTokens?: number;
30
+ /** Temperature (openai only). */
31
+ temperature?: number;
32
+ /** Request timeout (ms). */
33
+ timeout?: number;
34
+ /** Max retries. */
35
+ maxRetries?: number;
36
+ /** Pre-configured known event types. */
37
+ knownEventTypes?: string[];
38
+ /** Optional interaction logger. */
39
+ interactionLogger?: (...args: any[]) => void;
40
+ }
41
+
42
+ /** Optional S3 config for uploading screenshots instead of saving locally. */
43
+ export interface S3Config {
44
+ bucket: string;
45
+ region?: string;
46
+ prefix?: string;
47
+ publicBaseUrl?: string;
48
+ [key: string]: unknown;
49
+ }
50
+
51
+ /** PageExtractor config (passed via options.extractorConfig). */
52
+ export interface PageExtractorConfig {
53
+ timeoutMs?: number;
54
+ viewportWidth?: number;
55
+ viewportHeight?: number;
56
+ minBlockHeight?: number;
57
+ minBlockWidthRatio?: number;
58
+ blockMaxHeightRatio?: number;
59
+ blockMaxDepth?: number;
60
+ textPreviewMaxChars?: number;
61
+ /** Enable collection of all visible sized DOM elements. */
62
+ sizedElementsEnabled?: boolean;
63
+ /** Min size (px) threshold for sized-element collection. Default: 24. */
64
+ sizedElementMinSize?: number;
65
+ waitForImagesLoaded?: boolean;
66
+ fullPageScreenshot?: boolean;
67
+ blockScreenshots?: boolean;
68
+ snapshotDir?: string;
69
+ s3?: S3Config;
70
+ [key: string]: unknown;
71
+ }
72
+
73
+ /** A visible DOM element with "some size" (width or height > elementSize). */
74
+ export interface SizedElement {
75
+ tag: string;
76
+ text: string;
77
+ /** Resolved href / action / formaction (link-like elements). */
78
+ href: string;
79
+ /** Resolved src (img/source/video/iframe/audio/embed). */
80
+ src: string;
81
+ width: number;
82
+ height: number;
83
+ /** Absolute top (includes scroll offset). */
84
+ top: number;
85
+ /** Absolute left (includes scroll offset). */
86
+ left: number;
87
+ /** nth-of-type CSS path, e.g. "body > main:nth-of-type(1) > a:nth-of-type(1)". */
88
+ cssSelector: string;
89
+ id: string;
90
+ class: string;
91
+ role: string;
92
+ ariaLabel: string;
93
+ imageAlt: string;
94
+ interactive: boolean;
95
+ }
96
+
97
+ export interface BlockPosition {
98
+ left: number;
99
+ top: number;
100
+ width: number;
101
+ height: number;
102
+ }
103
+
104
+ export interface BlockSemanticGroup {
105
+ blockIdxs: string;
106
+ blockSemantic: string;
107
+ }
108
+
109
+ /** One logical (output) block in analysis.block_analysis.blocks. */
110
+ export interface BlockAnalysisBlock {
111
+ blockName: string;
112
+ blockDescription: string;
113
+ blockSemantics: string[];
114
+ blockCssPath: string;
115
+ blockPosition: BlockPosition;
116
+ fixed: boolean;
117
+ tag: string;
118
+ branchPath: string;
119
+ depth: number;
120
+ domOrder: number;
121
+ textPreview: string;
122
+ childInteractiveCount: number;
123
+ /**
124
+ * Sized DOM elements nested under this block (present when showElement=true,
125
+ * sorted by top then left). Each satisfies width > elementSize OR height > elementSize.
126
+ */
127
+ elements?: SizedElement[];
128
+ /** Present when showBlockIdx=true: dot-joined physical block indices, e.g. "0.1.2". */
129
+ blockIdxs?: string;
130
+ blockSemanticGroups?: BlockSemanticGroup[];
131
+ rowCount?: number;
132
+ /** Present when showEvents=true. */
133
+ blockPossibleEvents?: string[];
134
+ /** Present when showEvents=true: 'skipped' | 'direct' | 'llm'. */
135
+ mode?: string;
136
+ /** Screenshot path(s) for this block (when blockScreenshots=true). */
137
+ blockScreenshotPaths?: string[];
138
+ }
139
+
140
+ export interface BlockAnalysisStats {
141
+ total_blocks: number;
142
+ skipped_blocks?: number;
143
+ direct_blocks?: number;
144
+ llm_blocks?: number;
145
+ llm_group_count?: number;
146
+ llm_group_rows?: number;
147
+ }
148
+
149
+ export interface BlockAnalysis {
150
+ site_summary: string;
151
+ blocks: BlockAnalysisBlock[];
152
+ /** Present when showEvents=true. */
153
+ possible_event_types?: string[];
154
+ /** Sized elements that matched no block (only when the page has no visual blocks). */
155
+ unassignedElements?: SizedElement[];
156
+ stats: BlockAnalysisStats;
157
+ }
158
+
159
+ export interface AnalysisResult {
160
+ block_analysis: BlockAnalysis;
161
+ /** Present when showEvents=true. */
162
+ events_by_node?: unknown[];
163
+ event_types_summary?: unknown;
164
+ new_event_types?: string[];
165
+ [key: string]: unknown;
166
+ }
167
+
168
+ export interface ParseMetrics {
169
+ parseMs: number;
170
+ contextBuildMs: number;
171
+ elementsCount: number;
172
+ linksCount: number;
173
+ heapUsedMB: number;
174
+ contextLevel: string;
175
+ }
176
+
177
+ export interface ScreenshotInfo {
178
+ fullPage?: string;
179
+ blocks?: Array<{ blockIdx: number; path: string }>;
180
+ [key: string]: unknown;
181
+ }
182
+
183
+ export interface AnalyzeResult {
184
+ title: string;
185
+ parseMetrics: ParseMetrics;
186
+ analysis: AnalysisResult;
187
+ screenshots?: ScreenshotInfo;
188
+ /** Present when showEvents=true. */
189
+ elements?: unknown[];
190
+ csvContent?: string;
191
+ links?: unknown[];
192
+ }
193
+
194
+ export interface AnalyzeUrlOptions {
195
+ llm: LlmConfig;
196
+ /** Accumulated event types for consistency across pages. */
197
+ knownEventTypes?: string[];
198
+ /** HtmlParser config overrides. */
199
+ parserConfig?: Record<string, unknown>;
200
+ /** PageExtractor config overrides. */
201
+ extractorConfig?: PageExtractorConfig;
202
+ /** Include event arrays + full event metadata; also enables node-level events. */
203
+ showEvents?: boolean;
204
+ /** Include CSV/block index alignment fields. */
205
+ showBlockIdx?: boolean;
206
+ /**
207
+ * Collect all visible DOM elements with width > elementSize OR height > elementSize
208
+ * and nest them under each block as `elements`. Default false (previous behavior).
209
+ */
210
+ showElement?: boolean;
211
+ /** Min size (px) threshold for showElement. Default 24. */
212
+ elementSize?: number;
213
+ /** Save a full-page screenshot and return its path. */
214
+ fullPageScreenshot?: boolean;
215
+ /** Save one screenshot per merged logical block. */
216
+ blockScreenshots?: boolean;
217
+ /** Wait for page images before extracting/screenshotting. */
218
+ waitForImagesLoaded?: boolean;
219
+ }
220
+
221
+ export interface AnalyzePageEventsInput {
222
+ /** Raw HTML of the page. */
223
+ html: string;
224
+ /** Page URL (for resolving relative links). */
225
+ url: string;
226
+ /** Visual blocks from Playwright extraction. */
227
+ blocks?: unknown[];
228
+ /** Element geometry records (interactive). */
229
+ elementGeometries?: unknown[];
230
+ /**
231
+ * All visible sized DOM elements (width or height > 24) to nest under blocks.
232
+ * Typically PageExtractor.collectSizedElements output.
233
+ */
234
+ sizedElements?: SizedElement[];
235
+ /** Markdown content (reserved). */
236
+ markdown?: string;
237
+ llm: LlmConfig;
238
+ knownEventTypes?: string[];
239
+ parserConfig?: Record<string, unknown>;
240
+ showEvents?: boolean;
241
+ showBlockIdx?: boolean;
242
+ screenshots?: ScreenshotInfo | null;
243
+ nodeId?: string;
244
+ domain?: string;
245
+ }
246
+
247
+ /**
248
+ * One-call entry: pass a URL, get back the analysis.
249
+ * Playwright → HTML parse → block assign → CSV → LLM block/event analysis.
250
+ */
251
+ export function analyzeUrl(url: string, options: AnalyzeUrlOptions): Promise<AnalyzeResult>;
252
+
253
+ /**
254
+ * Run the pipeline on pre-fetched data (no browser): HTML parse → block assign
255
+ * → CSV → LLM block/event analysis.
256
+ */
257
+ export function analyzePageEvents(input: AnalyzePageEventsInput): Promise<AnalyzeResult>;
258
+
259
+ export interface PageExtractorBundle {
260
+ html: string;
261
+ blocks: unknown[];
262
+ elementGeometries: unknown[];
263
+ sizedElements: SizedElement[];
264
+ screenshots: ScreenshotInfo;
265
+ pageSize: { width: number; height: number };
266
+ }
267
+
268
+ export class PageExtractor {
269
+ constructor(config?: PageExtractorConfig);
270
+ config: Required<PageExtractorConfig> & Record<string, unknown>;
271
+ withPreparedPage<T>(url: string, callback: (page: any, targetUrl: string) => Promise<T>): Promise<T>;
272
+ extractPreparedPage(page: any, targetUrl: string): Promise<PageExtractorBundle>;
273
+ extract(url: string): Promise<PageExtractorBundle>;
274
+ collectElementGeometries(page: any): Promise<unknown[]>;
275
+ /** Collect all visible DOM elements with width or height > sizedElementMinSize. */
276
+ collectSizedElements(page: any): Promise<SizedElement[]>;
277
+ }
278
+
279
+ export function createLlmProvider(config: LlmConfig): unknown;
280
+
281
+ export function assignBlocksToElements(
282
+ elements?: unknown[],
283
+ blocks?: unknown[],
284
+ elementGeometries?: unknown[],
285
+ pageUrl?: string
286
+ ): unknown[];
287
+
288
+ export class HtmlParser {
289
+ constructor(config?: Record<string, unknown>);
290
+ parse(html: string, url: string): { elements: unknown[]; links: unknown[]; title: string; metrics: ParseMetrics };
291
+ }
292
+
293
+ export class CsvExporter {
294
+ buildCsvContent(nodeId: string, elements: unknown[]): string;
295
+ }
296
+
297
+ export class EventAnalyzer {
298
+ constructor(provider: unknown, config: LlmConfig, runContext?: Record<string, unknown>);
299
+ analyzeEvents(
300
+ csvContent: string,
301
+ markdown: string,
302
+ knownEventTypes: string[],
303
+ options?: { blocks?: unknown[]; analyzeNodeEvents?: boolean }
304
+ ): Promise<AnalysisResult>;
305
+ }
306
+
307
+ export class BaseLlmProvider {
308
+ analyze(prompt: string, options?: Record<string, unknown>): Promise<unknown>;
309
+ }
310
+ export class OpenAiProvider extends BaseLlmProvider {
311
+ constructor(config: Record<string, unknown>);
312
+ }
313
+ export class CodexCliProvider extends BaseLlmProvider {
314
+ constructor(config?: Record<string, unknown>);
315
+ }
316
+ export class ClaudeCliProvider extends BaseLlmProvider {
317
+ constructor(config?: Record<string, unknown>);
318
+ }