@cloudcreate/adsense-check 1.3.0 → 1.4.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @cloudcreate/adsense-check
2
2
 
3
- Automated website checker for Google AdSense review requirements. Detects "low value content" — the #1 rejection reason. Supports content sites, tool sites, and game sites with AI-powered topic analysis and content relevance checking.
3
+ Automated website checker for Google AdSense review requirements. Detects "low value content" — the #1 rejection reason. Supports content sites, tool sites, game sites, video sites, and reference sites with AI-powered topic analysis and content relevance checking.
4
4
 
5
5
  ## Install
6
6
 
@@ -43,9 +43,11 @@ Automatically classifies websites into three supported types:
43
43
 
44
44
  | Type | Description | Examples |
45
45
  |------|-------------|----------|
46
- | **Content** | News, blogs, reference material | theexceltranslator.com |
46
+ | **Content** | News, blogs, educational articles, guides | theexceltranslator.com |
47
47
  | **Tool** | Online calculators, converters, generators | ishowspeedsaid.com |
48
48
  | **Game** | Online games, game portals | popstone2.com |
49
+ | **Video** | Video sharing, video blogs, YouTube-style sites | — |
50
+ | **Reference** | Wiki, encyclopedia, glossary, knowledge base | — |
49
51
  | **Unsupported** | Other types (e-commerce, social, etc.) | — |
50
52
 
51
53
  AI analysis classifies the site type and topic. Falls back to DOM signal detection when AI is unavailable.
@@ -55,7 +57,7 @@ AI analysis classifies the site type and topic. Falls back to DOM signal detecti
55
57
  With `--ai`, the tool analyzes the homepage to determine:
56
58
  - **Topic**: What the site is about (e.g., "online match-3 puzzle games")
57
59
  - **Description**: One-line summary of the site's purpose
58
- - **Type**: content / tool / game / unsupported
60
+ - **Type**: content / tool / game / video / reference / unsupported
59
61
 
60
62
  ### Content Relevance Checking
61
63
 
@@ -101,6 +103,14 @@ Page score = geometric mean of all four dimensions. This means any weak dimensio
101
103
 
102
104
  Site score = page-type weighted average across all analyzed pages (homepage and content pages have highest weight).
103
105
 
106
+ ### AI Page Classification
107
+
108
+ With `--ai`, each page is classified by content analysis into one of: homepage, listing, content, game_detail, video_detail, reference_detail, required, utility. This overrides URL-based classification for sites with non-standard URL patterns.
109
+
110
+ ### Compliance Re-check
111
+
112
+ Pages flagged with borderline compliance scores (3-5) receive a second-pass AI review to reduce false positives. Context-aware: informational/educational mentions of sensitive topics are not treated as violations.
113
+
104
114
  ### Single-Page Analysis
105
115
 
106
116
  ```bash
@@ -124,7 +134,7 @@ adsense-check <site> --page <url> --ai
124
134
  -o, --output <dir> Report output dir (default: tmp)
125
135
  --no-save Skip auto-saving report
126
136
  -l, --lang <lang> Output language: en|zh (default: en)
127
- --type <type> Force site type: content|tool|game
137
+ --type <type> Force site type: content|tool|game|video|reference
128
138
  --detect-only Only detect site type/topic, skip full check
129
139
  ```
130
140
 
@@ -36,15 +36,13 @@ var BrowserManager = class {
36
36
  async function fetchPage(page, url, timeout = 3e4) {
37
37
  const response = await page.goto(url, { waitUntil: "domcontentloaded", timeout });
38
38
  const status = response?.status() ?? 0;
39
- let text = await page.evaluate(() => document.body?.innerText ?? "");
40
- if (text.replace(/\s+/g, "").length < 100) {
41
- try {
42
- await page.waitForLoadState("networkidle", { timeout: 1e4 });
43
- } catch {
44
- }
45
- await page.waitForTimeout(2e3);
46
- text = await page.evaluate(() => document.body?.innerText ?? "");
39
+ try {
40
+ await page.waitForLoadState("networkidle", { timeout: 1e4 });
41
+ } catch {
47
42
  }
43
+ await page.waitForTimeout(1500);
44
+ const urlAfterRender = page.url();
45
+ let text = await page.evaluate(() => document.body?.innerText ?? "");
48
46
  const content = await page.content();
49
47
  const links = await page.evaluate(
50
48
  () => Array.from(document.querySelectorAll("a[href]")).map((a) => a.href).filter((href) => href.startsWith("http"))
@@ -52,7 +50,7 @@ async function fetchPage(page, url, timeout = 3e4) {
52
50
  const linkDetails = await page.evaluate(
53
51
  () => Array.from(document.querySelectorAll("a[href]")).filter((a) => a.href.startsWith("http")).map((a) => ({
54
52
  href: a.href,
55
- text: a.innerText.trim()
53
+ text: a.innerText?.trim() ?? ""
56
54
  }))
57
55
  );
58
56
  const navText = await page.evaluate(() => {
@@ -89,10 +87,11 @@ async function fetchPage(page, url, timeout = 3e4) {
89
87
  canvasCount: document.querySelectorAll("canvas").length,
90
88
  articleCount: document.querySelectorAll("article").length,
91
89
  textLength: (document.body?.innerText ?? "").replace(/\s+/g, "").length,
92
- gameLinks
90
+ gameLinks,
91
+ videoElementCount: document.querySelectorAll("video").length
93
92
  };
94
93
  });
95
- return { status, content, text, links, linkDetails, navText, footerText, title, url, signals };
94
+ return { status, content, text, links, linkDetails, navText, footerText, title, url: urlAfterRender, signals };
96
95
  }
97
96
  async function checkRobotsTxt(origin) {
98
97
  try {
@@ -180,6 +179,10 @@ var en = {
180
179
  "item.content.game_desc": "Game Descriptions",
181
180
  "item.content.iframe_quality": "Iframe Quality",
182
181
  "item.content.game_variety": "Game Variety",
182
+ "item.content.video_desc": "Video Descriptions",
183
+ "item.content.video_variety": "Video Variety",
184
+ "item.content.reference_entry": "Reference Entries",
185
+ "item.content.reference_variety": "Reference Variety",
183
186
  // Structure items
184
187
  "item.structure.internal": "Internal Links",
185
188
  "item.structure.deadlinks": "Dead Links",
@@ -230,9 +233,21 @@ var en = {
230
233
  "content.iframe_quality.warn": "{count} game iframes detected \u2014 ensure each has proper title and size attributes",
231
234
  "content.game_variety.pass": "Game pages show good variety",
232
235
  "content.game_variety.warn": "Game pages are {pct}% similar \u2014 may look like mass-produced content",
236
+ // Video-specific messages
237
+ "content.video_desc.pass": "{total} video page(s) have sufficient description text",
238
+ "content.video_desc.warn": "{thin}/{total} video pages lack description text (recommend 50+ chars)",
239
+ "content.video_variety.pass": "Video pages show good variety (similarity {pct}%)",
240
+ "content.video_variety.warn": "Video pages are {pct}% similar \u2014 may look like mass-produced content",
241
+ // Reference-specific messages
242
+ "content.reference_entry.pass": "Reference entries have sufficient structure and metadata",
243
+ "content.reference_entry.warn": "{thin}/{total} reference entries lack structure (recommend 100+ chars)",
244
+ "content.reference_variety.pass": "Reference pages show good variety (similarity {pct}%)",
245
+ "content.reference_variety.warn": "Reference pages are {pct}% similar \u2014 expected for structured entries",
233
246
  // Site type detection
234
247
  "detector.type.content": "Content Site",
235
248
  "detector.type.game": "Game Site",
249
+ "detector.type.video": "Video Site",
250
+ "detector.type.reference": "Reference Site",
236
251
  "detector.signals": "Signals: {details}",
237
252
  // Required pages messages
238
253
  "pages.found": "Found {name} page ({path})",
@@ -266,6 +281,7 @@ var en = {
266
281
  // Policy messages
267
282
  "policy.keywords.pass": "No policy-violating keywords found",
268
283
  "policy.keywords.fail": "{count} potentially violating keyword(s) found",
284
+ "policy.keywords.warn": "{count} potentially violating keyword(s) found (on pages with substantial content \u2014 verify with AI analysis)",
269
285
  // AI messages
270
286
  "ai.skip": "AI_API_KEY not configured, skipping AI analysis",
271
287
  "ai.fail": "AI analysis failed: {error}",
@@ -312,7 +328,68 @@ var en = {
312
328
  "detector.type.unsupported": "Unsupported Type",
313
329
  "topic.info": "Site topic: {topic}",
314
330
  "topic.description": "{description}",
315
- "topic.unsupported_warning": "This site type ({type}) is not supported by AdSense checklist"
331
+ "topic.unsupported_warning": "This site type ({type}) is not supported by AdSense checklist",
332
+ // Reporter UI
333
+ "reporter.site_type": "Site type",
334
+ "reporter.topic": "Topic",
335
+ "reporter.pages_label": "Pages",
336
+ "reporter.confidence": "{confidence} confidence",
337
+ "reporter.ai_value_label": "AI Value Score",
338
+ "reporter.ai_value_note": "geometric mean \xD7 page-type weights",
339
+ "reporter.ai_dimensions": "AI Dimensions",
340
+ "reporter.avg_per_10": "avg /10",
341
+ "reporter.dim_value": "Value",
342
+ "reporter.dim_originality": "Originality",
343
+ "reporter.dim_relevance": "Relevance",
344
+ "reporter.dim_compliance": "Compliance",
345
+ "reporter.formula_label": "Hard {hardPct}% \xD7 0.4 + Soft {softPct}% \xD7 0.6 - Penalty {penalty} = {total}",
346
+ "reporter.mechanical_label": "mechanical",
347
+ // Markdown report
348
+ "md.report_title": "AdSense Review Report",
349
+ "md.table.project": "Item",
350
+ "md.table.value": "Value",
351
+ "md.table.url": "URL",
352
+ "md.table.time": "Time",
353
+ "md.table.site_type": "Site Type",
354
+ "md.table.topic": "Topic",
355
+ "md.table.description": "Description",
356
+ "md.table.sampling": "Sampling",
357
+ "md.table.total": "total",
358
+ "md.table.recent": "recent (6mo)",
359
+ "md.table.sampled": "sampled",
360
+ "md.table.confidence": "confidence",
361
+ "md.composite_score_title": "Composite Score",
362
+ "md.hard_requirements": "Hard Requirements",
363
+ "md.soft_scoring": "Soft Scoring",
364
+ "md.ai_value_title": "AI Value Analysis",
365
+ "md.table.dimension": "Dimension",
366
+ "md.table.avg_score": "Avg Score",
367
+ "md.dim_value": "Value",
368
+ "md.dim_originality": "Originality",
369
+ "md.dim_relevance": "Relevance",
370
+ "md.dim_compliance": "Compliance",
371
+ "md.site_ai_score": "Site AI Score",
372
+ "md.geometric_weighted": "geometric mean \xD7 page-type weights",
373
+ "md.page_details": "Page Details",
374
+ "md.pages_count": "{count} pages",
375
+ "md.table.status": "Status",
376
+ "md.table.type": "Type",
377
+ "md.table.path": "Path",
378
+ "md.table.score": "Score",
379
+ "md.table.content_ratio": "Content Ratio",
380
+ "md.table.ai_composite": "AI Composite",
381
+ "md.table.title": "Title",
382
+ "md.problem_pages": "Problem Page Details",
383
+ "md.ai_status": "AI Status",
384
+ "md.four_dimensions": "Four-dimension scores",
385
+ "md.ai_composite_score": "AI composite score",
386
+ "md.geometric_mean": "geometric mean",
387
+ "md.assessment": "Assessment",
388
+ "md.suggestions": "Suggestions",
389
+ "md.summary.not_ready": "**\u274C NOT READY** \u2014 {count} item(s) must be fixed",
390
+ "md.summary.needs_fixes": "**\u26A0\uFE0F NEEDS FIXES** \u2014 {count} warning(s) to address",
391
+ "md.summary.mostly_ready": "**\u26A0\uFE0F MOSTLY READY** \u2014 fix {count} warning(s) before submitting",
392
+ "md.summary.ready": "**\u2705 READY** \u2014 can submit for AdSense review"
316
393
  };
317
394
  var zh = {
318
395
  // 分类
@@ -334,6 +411,10 @@ var zh = {
334
411
  "item.content.game_desc": "\u6E38\u620F\u63CF\u8FF0",
335
412
  "item.content.iframe_quality": "Iframe \u8D28\u91CF",
336
413
  "item.content.game_variety": "\u6E38\u620F\u591A\u6837\u6027",
414
+ "item.content.video_desc": "\u89C6\u9891\u63CF\u8FF0",
415
+ "item.content.video_variety": "\u89C6\u9891\u591A\u6837\u6027",
416
+ "item.content.reference_entry": "\u53C2\u8003\u6761\u76EE\u5B8C\u6574\u6027",
417
+ "item.content.reference_variety": "\u53C2\u8003\u591A\u6837\u6027",
337
418
  // 结构检查项
338
419
  "item.structure.internal": "\u5185\u90E8\u94FE\u63A5",
339
420
  "item.structure.deadlinks": "\u6B7B\u94FE\u68C0\u6D4B",
@@ -384,9 +465,21 @@ var zh = {
384
465
  "content.iframe_quality.warn": "\u68C0\u6D4B\u5230 {count} \u4E2A\u6E38\u620F iframe \u2014 \u786E\u4FDD\u6BCF\u4E2A\u90FD\u6709 title \u548C\u5408\u7406\u5C3A\u5BF8",
385
466
  "content.game_variety.pass": "\u6E38\u620F\u9875\u9762\u591A\u6837\u6027\u6B63\u5E38",
386
467
  "content.game_variety.warn": "\u6E38\u620F\u9875\u9762\u76F8\u4F3C\u5EA6 {pct}% \u2014 \u53EF\u80FD\u662F\u6A21\u677F\u6279\u91CF\u751F\u6210",
468
+ // 视频站专用消息
469
+ "content.video_desc.pass": "{total} \u4E2A\u89C6\u9891\u9875\u9762\u6709\u8DB3\u591F\u7684\u63CF\u8FF0\u6587\u5B57",
470
+ "content.video_desc.warn": "{thin}/{total} \u4E2A\u89C6\u9891\u9875\u9762\u7F3A\u5C11\u63CF\u8FF0\u6587\u5B57\uFF08\u5EFA\u8BAE 50+ \u5B57\uFF09",
471
+ "content.video_variety.pass": "\u89C6\u9891\u9875\u9762\u591A\u6837\u6027\u6B63\u5E38 (\u76F8\u4F3C\u5EA6 {pct}%)",
472
+ "content.video_variety.warn": "\u89C6\u9891\u9875\u9762\u76F8\u4F3C\u5EA6 {pct}% \u2014 \u53EF\u80FD\u662F\u6A21\u677F\u6279\u91CF\u751F\u6210",
473
+ // 参考站专用消息
474
+ "content.reference_entry.pass": "\u53C2\u8003\u6761\u76EE\u5177\u6709\u8DB3\u591F\u7684\u7ED3\u6784\u548C\u5143\u6570\u636E",
475
+ "content.reference_entry.warn": "{thin}/{total} \u4E2A\u53C2\u8003\u6761\u76EE\u7ED3\u6784\u4E0D\u8DB3\uFF08\u5EFA\u8BAE 100+ \u5B57\uFF09",
476
+ "content.reference_variety.pass": "\u53C2\u8003\u9875\u9762\u591A\u6837\u6027\u6B63\u5E38 (\u76F8\u4F3C\u5EA6 {pct}%)",
477
+ "content.reference_variety.warn": "\u53C2\u8003\u9875\u9762\u76F8\u4F3C\u5EA6 {pct}% \u2014 \u7ED3\u6784\u5316\u6761\u76EE\u5C5E\u4E8E\u6B63\u5E38",
387
478
  // 站点类型检测
388
479
  "detector.type.content": "\u5185\u5BB9\u7AD9",
389
480
  "detector.type.game": "\u6E38\u620F\u7AD9",
481
+ "detector.type.video": "\u89C6\u9891\u7AD9",
482
+ "detector.type.reference": "\u53C2\u8003\u7AD9",
390
483
  "detector.signals": "\u68C0\u6D4B\u4FE1\u53F7: {details}",
391
484
  // 必要页面消息
392
485
  "pages.found": "\u627E\u5230 {name} \u9875\u9762 ({path})",
@@ -420,6 +513,7 @@ var zh = {
420
513
  // 合规消息
421
514
  "policy.keywords.pass": "\u672A\u68C0\u6D4B\u5230\u660E\u663E\u7684\u8FDD\u89C4\u5173\u952E\u8BCD",
422
515
  "policy.keywords.fail": "\u68C0\u6D4B\u5230 {count} \u4E2A\u53EF\u7591\u5173\u952E\u8BCD",
516
+ "policy.keywords.warn": "\u68C0\u6D4B\u5230 {count} \u4E2A\u53EF\u7591\u5173\u952E\u8BCD\uFF08\u4F4D\u4E8E\u5185\u5BB9\u5145\u5B9E\u9875\u9762\uFF0C\u8BF7\u7ED3\u5408 AI \u5206\u6790\u786E\u8BA4\uFF09",
423
517
  // AI 消息
424
518
  "ai.skip": "\u672A\u914D\u7F6E AI_API_KEY\uFF0C\u8DF3\u8FC7 AI \u5206\u6790",
425
519
  "ai.fail": "AI \u5206\u6790\u5931\u8D25: {error}",
@@ -466,7 +560,69 @@ var zh = {
466
560
  "detector.type.unsupported": "\u4E0D\u652F\u6301\u7684\u7C7B\u578B",
467
561
  "topic.info": "\u7AD9\u70B9\u4E3B\u9898: {topic}",
468
562
  "topic.description": "{description}",
469
- "topic.unsupported_warning": "\u8BE5\u7AD9\u70B9\u7C7B\u578B\uFF08{type}\uFF09\u4E0D\u5728 AdSense \u68C0\u67E5\u652F\u6301\u8303\u56F4\u5185"
563
+ "topic.unsupported_warning": "\u8BE5\u7AD9\u70B9\u7C7B\u578B\uFF08{type}\uFF09\u4E0D\u5728 AdSense \u68C0\u67E5\u652F\u6301\u8303\u56F4\u5185",
564
+ // 报告 UI(终端)
565
+ "reporter.site_type": "\u7AD9\u70B9\u7C7B\u578B",
566
+ "reporter.topic": "\u4E3B\u9898",
567
+ "reporter.pages_label": "\u9875\u9762",
568
+ "reporter.confidence": "\u7F6E\u4FE1\u5EA6: {confidence}",
569
+ "reporter.ai_value_label": "AI \u4EF7\u503C\u8BC4\u5206",
570
+ "reporter.ai_value_note": "\u51E0\u4F55\u5747\u503C \xD7 \u9875\u9762\u7C7B\u578B\u52A0\u6743",
571
+ "reporter.ai_dimensions": "AI \u7EF4\u5EA6",
572
+ "reporter.avg_per_10": "\u5747\u5206 /10",
573
+ "reporter.formula_label": "\u786C\u6027 {hardPct}% \xD7 0.4 + \u67D4\u6027 {softPct}% \xD7 0.6 - \u6263\u5206 {penalty} = {total}",
574
+ "reporter.mechanical_label": "\u673A\u68B0\u8BC4\u5206",
575
+ // 维度名称(终端和 Markdown)
576
+ "reporter.dim_value": "\u4EF7\u503C",
577
+ "reporter.dim_originality": "\u539F\u521B",
578
+ "reporter.dim_relevance": "\u76F8\u5173",
579
+ "reporter.dim_compliance": "\u5408\u89C4",
580
+ // Markdown 报告
581
+ "md.report_title": "AdSense \u5BA1\u6838\u62A5\u544A",
582
+ "md.table.project": "\u9879\u76EE",
583
+ "md.table.value": "\u503C",
584
+ "md.table.url": "URL",
585
+ "md.table.time": "\u65F6\u95F4",
586
+ "md.table.site_type": "\u7AD9\u70B9\u7C7B\u578B",
587
+ "md.table.topic": "\u4E3B\u9898",
588
+ "md.table.description": "\u63CF\u8FF0",
589
+ "md.table.sampling": "\u62BD\u6837",
590
+ "md.table.total": "\u603B\u8BA1",
591
+ "md.table.recent": "\u8FD1 6 \u4E2A\u6708",
592
+ "md.table.sampled": "\u5DF2\u62BD\u6837",
593
+ "md.table.confidence": "\u7F6E\u4FE1\u5EA6",
594
+ "md.composite_score_title": "\u7EFC\u5408\u8BC4\u5206",
595
+ "md.hard_requirements": "\u786C\u6027\u8981\u6C42",
596
+ "md.soft_scoring": "\u67D4\u6027\u8BC4\u5206",
597
+ "md.ai_value_title": "AI \u4EF7\u503C\u5206\u6790",
598
+ "md.table.dimension": "\u7EF4\u5EA6",
599
+ "md.table.avg_score": "\u5747\u5206",
600
+ "md.dim_value": "\u4EF7\u503C",
601
+ "md.dim_originality": "\u539F\u521B",
602
+ "md.dim_relevance": "\u76F8\u5173",
603
+ "md.dim_compliance": "\u5408\u89C4",
604
+ "md.site_ai_score": "\u7AD9\u70B9 AI \u8BC4\u5206",
605
+ "md.geometric_weighted": "\u51E0\u4F55\u5747\u503C \xD7 \u9875\u9762\u7C7B\u578B\u52A0\u6743",
606
+ "md.page_details": "\u9010\u9875\u8BE6\u60C5",
607
+ "md.pages_count": "{count} \u4E2A\u9875\u9762",
608
+ "md.table.status": "\u72B6\u6001",
609
+ "md.table.type": "\u7C7B\u578B",
610
+ "md.table.path": "\u8DEF\u5F84",
611
+ "md.table.score": "\u8BC4\u5206",
612
+ "md.table.content_ratio": "\u6B63\u6587\u6BD4",
613
+ "md.table.ai_composite": "AI \u7EFC\u5408",
614
+ "md.table.title": "\u6807\u9898",
615
+ "md.problem_pages": "\u95EE\u9898\u9875\u9762\u8BE6\u60C5",
616
+ "md.ai_status": "AI \u72B6\u6001",
617
+ "md.four_dimensions": "\u56DB\u7EF4\u8BC4\u5206",
618
+ "md.ai_composite_score": "AI \u7EFC\u5408\u5206",
619
+ "md.geometric_mean": "\u51E0\u4F55\u5747\u503C",
620
+ "md.assessment": "\u8BC4\u4F30",
621
+ "md.suggestions": "\u6539\u8FDB\u5EFA\u8BAE",
622
+ "md.summary.not_ready": "**\u274C NOT READY** \u2014 {count} \u9879\u5931\u8D25\u9700\u8981\u4FEE\u590D",
623
+ "md.summary.needs_fixes": "**\u26A0\uFE0F NEEDS FIXES** \u2014 {count} \u9879\u8B66\u544A\u5F85\u4FEE\u590D",
624
+ "md.summary.mostly_ready": "**\u26A0\uFE0F MOSTLY READY** \u2014 \u4FEE\u590D {count} \u9879\u8B66\u544A\u540E\u53EF\u63D0\u4EA4\u5BA1\u6838",
625
+ "md.summary.ready": "**\u2705 READY** \u2014 \u53EF\u4EE5\u63D0\u4EA4 AdSense \u5BA1\u6838"
470
626
  };
471
627
  var langMap = { en, zh };
472
628
  function getSupportedLangs() {
@@ -552,6 +708,21 @@ Score each dimension from 0 to 10:
552
708
  Also set "relevanceLabel": "relevant" | "tangential" | "off-topic".
553
709
  4. compliance (0-10): Does the content comply with Google AdSense policies? 10 = fully compliant, 0 = serious violations.
554
710
  Flag: adult content, gambling, drugs, violence, copyright infringement, deceptive content.
711
+ Important context rules:
712
+ - Words like "crack", "bet", "drug", "gamble" used in educational, news, or informational contexts are NOT violations.
713
+ - If the page discusses or reports on sensitive topics (e.g., "puzzle crack" as a news headline, "betting odds" in sports analysis), this is NOT a violation.
714
+ - Only flag actual promotion or facilitation of policy-violating content.
715
+ - If the page appears to be a 404 error page or has minimal content, do not flag it as a compliance violation. Note it as "insufficient content for compliance review".
716
+
717
+ Also classify the page type based on its content and purpose. Choose ONE:
718
+ - "homepage": The site's main landing page
719
+ - "listing": An index/category page listing multiple items (articles, mods, products)
720
+ - "content": A standalone article, blog post, guide, or tutorial
721
+ - "game_detail": A game page with playable game or game download
722
+ - "video_detail": A page centered around a video or video embed
723
+ - "reference_detail": A wiki entry, glossary term, encyclopedia article, or database record
724
+ - "required": About, Privacy, Terms, Contact, Editorial Policy, Legal
725
+ - "utility": Search, Login, Signup, Download, 404, or functional tool pages
555
726
 
556
727
  Page: ${page.url}
557
728
 
@@ -565,6 +736,7 @@ Reply in ${langName} with JSON:
565
736
  "relevance": <0-10>,
566
737
  "relevanceLabel": "relevant|tangential|off-topic",
567
738
  "compliance": <0-10>,
739
+ "pageType": "homepage|listing|content|game_detail|video_detail|reference_detail|required|utility",
568
740
  "assessment": "Brief assessment covering the key findings across all dimensions",
569
741
  "suggestions": ["Specific actionable suggestion to improve this page"]
570
742
  }`;
@@ -577,6 +749,8 @@ Reply in ${langName} with JSON:
577
749
  const complianceScore = clampScore(result.compliance);
578
750
  const geoMean = Math.pow(valueScore * originalityScore * relevanceScore * complianceScore, 0.25);
579
751
  const status = geoMean >= 7 ? "pass" : geoMean >= 4 ? "warn" : "fail";
752
+ const validPageTypes = ["homepage", "listing", "content", "game_detail", "video_detail", "reference_detail", "required", "utility"];
753
+ const inferredPageType = validPageTypes.includes(result.pageType) ? result.pageType : void 0;
580
754
  return {
581
755
  url: page.url,
582
756
  status,
@@ -586,7 +760,8 @@ Reply in ${langName} with JSON:
586
760
  relevanceScore,
587
761
  complianceScore,
588
762
  assessment: result.assessment ?? "",
589
- suggestions: result.suggestions ?? []
763
+ suggestions: result.suggestions ?? [],
764
+ inferredPageType
590
765
  };
591
766
  } catch (err) {
592
767
  return {
@@ -602,6 +777,62 @@ function clampScore(v) {
602
777
  if (isNaN(n)) return 5;
603
778
  return Math.max(0, Math.min(10, Math.round(n)));
604
779
  }
780
+ async function recheckCompliance(pages, langName, onProgress) {
781
+ const result = /* @__PURE__ */ new Map();
782
+ if (pages.length === 0) return result;
783
+ const progress = onProgress ?? (() => {
784
+ });
785
+ progress(`AI: re-checking ${pages.length} suspicious page(s) for compliance...`);
786
+ for (const page of pages) {
787
+ const content = page.text.slice(0, PAGE_CHARS);
788
+ const prompt = `You are a Google AdSense policy compliance expert. A previous analysis flagged this page as potentially non-compliant (score: ${page.firstComplianceScore}/10). Perform a careful second review.
789
+
790
+ Focus ONLY on compliance. Check for:
791
+ - Adult or sexually explicit content
792
+ - Gambling or casino promotion
793
+ - Illegal drugs or controlled substances
794
+ - Violence, gore, or hate speech
795
+ - Copyright infringement or pirated content
796
+ - Deceptive content, phishing, or scams
797
+ - Excessive profanity
798
+ - Misleading medical/financial claims
799
+ - Content that targets children inappropriately
800
+
801
+ Be fair \u2014 informational/educational content ABOUT sensitive topics (e.g., health articles, news reporting) is NOT a violation. Only flag actual policy violations.
802
+
803
+ Additional instructions:
804
+ - If the page text is very short (< 200 characters) and appears to be an error page, 404, or placeholder, do not flag any compliance violations. Score compliance as 10 and note "insufficient content".
805
+ - Context matters: words that match policy keywords but appear in news reporting, educational content, or informational discussion are NOT violations.
806
+
807
+ Page: ${page.url}
808
+
809
+ Content:
810
+ ${content}
811
+
812
+ Reply in ${langName} with JSON:
813
+ {
814
+ "compliance": <0-10>,
815
+ "verdict": "compliant|borderline|violation",
816
+ "assessment": "Brief explanation of your compliance determination"
817
+ }`;
818
+ try {
819
+ const text = await callAI(prompt, 1024);
820
+ const r = extractJson(text);
821
+ const newScore = clampScore(r.compliance);
822
+ const finalScore = Math.max(page.firstComplianceScore, newScore);
823
+ result.set(page.url, {
824
+ complianceScore: finalScore,
825
+ assessment: r.assessment ?? ""
826
+ });
827
+ } catch {
828
+ result.set(page.url, {
829
+ complianceScore: page.firstComplianceScore,
830
+ assessment: "Re-check failed, keeping original score"
831
+ });
832
+ }
833
+ }
834
+ return result;
835
+ }
605
836
  async function analyzeOverall(pageAnalyses, langName, date) {
606
837
  const summaries = pageAnalyses.map(
607
838
  (p, i) => `Page ${i + 1} (${p.url}): [${p.status}] value=${p.valueScore} originality=${p.originalityScore} relevance=${p.relevanceScore} compliance=${p.complianceScore} \u2014 ${p.assessment.slice(0, 150)}`
@@ -705,7 +936,7 @@ function extractJson2(text) {
705
936
  }
706
937
  throw new Error("No JSON found in response");
707
938
  }
708
- var VALID_TYPES = ["content", "tool", "game"];
939
+ var VALID_TYPES = ["content", "tool", "game", "video", "reference"];
709
940
  async function analyzeSiteTopic(homepage, lang = "en", apiKey) {
710
941
  const langName = lang === "zh" ? "\u4E2D\u6587" : "English";
711
942
  const content = homepage.text.slice(0, 2e3);
@@ -717,16 +948,18 @@ Homepage content (first 2000 chars):
717
948
  ${content}
718
949
 
719
950
  Classify this website into ONE of these types:
720
- - "content": informational site (news, blog, reference materials, educational content)
951
+ - "content": informational site (news, blog, educational articles, guides)
721
952
  - "tool": utility/tool site (calculator, converter, generator, online tool)
722
953
  - "game": online game site (playable games, game portal)
954
+ - "video": video site (video sharing, video blog, YouTube-style site with embedded videos)
955
+ - "reference": wiki/encyclopedia/reference site (structured knowledge base, searchable database, glossary, dictionary, encyclopedia-style content with interlinked articles, transcript archive)
723
956
  - "unsupported": e-commerce, SaaS product, social media, forum, portfolio, or anything not fitting above categories
724
957
 
725
958
  Reply language: ${langName}
726
959
 
727
960
  Reply in ${langName} with JSON:
728
961
  {
729
- "type": "content|tool|game|unsupported",
962
+ "type": "content|tool|game|video|reference|unsupported",
730
963
  "topic": "Main topic in 3-5 words (e.g. 'Excel translation reference')",
731
964
  "description": "One sentence describing what this site does",
732
965
  "confidence": "high|medium|low",
@@ -759,6 +992,10 @@ var GAME_NAV_KEYWORDS = /\b(games?|play\b|arcade|puzzle|action)\b/i;
759
992
  var GAME_NAV_KEYWORDS_ZH = /游戏|玩游戏/;
760
993
  var TOOL_NAV_KEYWORDS = /\b(calculator|converter|generator|tool|translat|calculat|checker|analyzer|formatter|validator|encoder|decoder)\b/i;
761
994
  var TOOL_NAV_KEYWORDS_ZH = /计算器|转换器|工具|翻译/;
995
+ var REFERENCE_NAV_KEYWORDS = /\b(wiki|encyclopedia|reference|glossary|docs|documentation|knowledge\s*base|archive|database|transcript)\b/i;
996
+ var REFERENCE_NAV_KEYWORDS_ZH = /百科|知识库|参考|词典|文档|数据库|档案/;
997
+ var VIDEO_NAV_KEYWORDS = /\b(video|videos|watch|channel|channels|stream|vlog|clip|playlist|shorts|tv)\b/i;
998
+ var VIDEO_NAV_KEYWORDS_ZH = /视频|频道|直播|短视频/;
762
999
  var GAME_IFRAME_PATTERNS = [
763
1000
  /game/i,
764
1001
  /play/i,
@@ -773,9 +1010,27 @@ var GAME_IFRAME_PATTERNS = [
773
1010
  /friv/i,
774
1011
  /itch\.io/i,
775
1012
  /htmlgames/i,
776
- /gameflare/i,
777
- /embed/i
1013
+ /gameflare/i
778
1014
  ];
1015
+ var VIDEO_IFRAME_PATTERNS = [
1016
+ /youtube\.com\/embed/i,
1017
+ /youtube-nocookie\.com/i,
1018
+ /youtu\.be/i,
1019
+ /player\.vimeo\.com/i,
1020
+ /player\.bilibili\.com/i,
1021
+ /dailymotion\.com\/embed/i,
1022
+ /embed\.twitch\.tv/i,
1023
+ /streamable\.com\/o/i,
1024
+ /wistia.*\.net\/medias/i,
1025
+ /vidyard\.com\/embed/i,
1026
+ /brightcove/i
1027
+ ];
1028
+ function isVideoIframe(src) {
1029
+ return VIDEO_IFRAME_PATTERNS.some((p) => p.test(src));
1030
+ }
1031
+ function isGameIframe(src) {
1032
+ return GAME_IFRAME_PATTERNS.some((p) => p.test(src));
1033
+ }
779
1034
  function detectSiteType(pagesSignals, navText, manualType) {
780
1035
  if (manualType) {
781
1036
  return { type: manualType, confidence: "high", signals: { iframeRatio: 0, canvasRatio: 0, articleRatio: 0, navGameKeywords: false } };
@@ -788,28 +1043,55 @@ function detectSiteType(pagesSignals, navText, manualType) {
788
1043
  let pagesWithCanvas = 0;
789
1044
  let pagesWithArticle = 0;
790
1045
  let pagesWithGameIframe = 0;
1046
+ let pagesWithVideoIframe = 0;
1047
+ let pagesWithVideoElement = 0;
791
1048
  let firstPageIframes = 0;
792
1049
  let firstPageCanvas = 0;
1050
+ let firstPageVideoIframes = 0;
793
1051
  let totalGameLinks = 0;
794
1052
  for (let i = 0; i < pagesSignals.length; i++) {
795
1053
  const sig = pagesSignals[i];
796
1054
  if (sig.iframeCount > 0) pagesWithIframe++;
797
1055
  if (sig.canvasCount > 0) pagesWithCanvas++;
798
1056
  if (sig.articleCount > 0) pagesWithArticle++;
1057
+ if (sig.videoElementCount > 0) pagesWithVideoElement++;
799
1058
  totalGameLinks += sig.gameLinks || 0;
800
1059
  if (i === 0) {
801
1060
  firstPageIframes = sig.iframeCount;
802
1061
  firstPageCanvas = sig.canvasCount;
803
1062
  }
804
- const hasGameIframe = sig.iframeSrcs.some((src) => GAME_IFRAME_PATTERNS.some((p) => p.test(src)));
1063
+ const hasGameIframe = sig.iframeSrcs.some((s) => isGameIframe(s));
1064
+ const hasVideoIframe = sig.iframeSrcs.some((s) => isVideoIframe(s));
805
1065
  if (hasGameIframe) pagesWithGameIframe++;
1066
+ if (hasVideoIframe) {
1067
+ pagesWithVideoIframe++;
1068
+ if (i === 0) firstPageVideoIframes = sig.iframeSrcs.filter((s) => isVideoIframe(s)).length;
1069
+ }
806
1070
  }
807
1071
  const avgGameLinks = totalGameLinks / total;
808
1072
  const iframeRatio = pagesWithIframe / total;
809
1073
  const canvasRatio = pagesWithCanvas / total;
810
1074
  const articleRatio = pagesWithArticle / total;
811
1075
  const gameIframeRatio = pagesWithGameIframe / total;
1076
+ const videoIframeRatio = pagesWithVideoIframe / total;
1077
+ const videoElementRatio = pagesWithVideoElement / total;
812
1078
  const navGameKeywords = GAME_NAV_KEYWORDS.test(navText) || GAME_NAV_KEYWORDS_ZH.test(navText);
1079
+ const navVideoKeywords = VIDEO_NAV_KEYWORDS.test(navText) || VIDEO_NAV_KEYWORDS_ZH.test(navText);
1080
+ let videoScore = 0;
1081
+ if (videoIframeRatio >= 0.3) videoScore += 5;
1082
+ else if (videoIframeRatio >= 0.1) videoScore += 3;
1083
+ if (videoElementRatio >= 0.3) videoScore += 5;
1084
+ else if (videoElementRatio >= 0.1) videoScore += 3;
1085
+ if (navVideoKeywords) videoScore += 3;
1086
+ if (firstPageVideoIframes >= 3) videoScore += 3;
1087
+ else if (firstPageVideoIframes >= 1) videoScore += 1;
1088
+ if (videoScore >= 3) {
1089
+ return {
1090
+ type: "video",
1091
+ confidence: videoScore >= 6 ? "high" : "medium",
1092
+ signals: { iframeRatio, canvasRatio, articleRatio, navGameKeywords }
1093
+ };
1094
+ }
813
1095
  let gameScore = 0;
814
1096
  if (gameIframeRatio >= 0.3) gameScore += 5;
815
1097
  else if (gameIframeRatio >= 0.1) gameScore += 3;
@@ -823,23 +1105,31 @@ function detectSiteType(pagesSignals, navText, manualType) {
823
1105
  else if (avgGameLinks >= 2) gameScore += 2;
824
1106
  else if (totalGameLinks >= 3) gameScore += 1;
825
1107
  if (articleRatio >= 0.7 && gameScore < 3) gameScore -= 2;
826
- const isGame = gameScore >= 3;
827
- let type;
828
- let confidence;
829
- if (isGame) {
830
- type = "game";
831
- confidence = gameScore >= 6 ? "high" : "medium";
832
- } else {
833
- const navToolKeywords = TOOL_NAV_KEYWORDS.test(navText) || TOOL_NAV_KEYWORDS_ZH.test(navText);
834
- if (navToolKeywords) {
835
- type = "tool";
836
- confidence = "medium";
837
- } else {
838
- type = "content";
839
- confidence = "high";
840
- }
1108
+ if (gameScore >= 3) {
1109
+ return {
1110
+ type: "game",
1111
+ confidence: gameScore >= 6 ? "high" : "medium",
1112
+ signals: { iframeRatio, canvasRatio, articleRatio, navGameKeywords }
1113
+ };
1114
+ }
1115
+ const navToolKeywords = TOOL_NAV_KEYWORDS.test(navText) || TOOL_NAV_KEYWORDS_ZH.test(navText);
1116
+ if (navToolKeywords) {
1117
+ return { type: "tool", confidence: "medium", signals: { iframeRatio, canvasRatio, articleRatio, navGameKeywords } };
1118
+ }
1119
+ const navReferenceKeywords = REFERENCE_NAV_KEYWORDS.test(navText) || REFERENCE_NAV_KEYWORDS_ZH.test(navText);
1120
+ let referenceScore = 0;
1121
+ if (articleRatio >= 0.7) referenceScore += 3;
1122
+ else if (articleRatio >= 0.5) referenceScore += 1;
1123
+ if (navReferenceKeywords) referenceScore += 3;
1124
+ if (iframeRatio < 0.1) referenceScore += 1;
1125
+ if (referenceScore >= 3) {
1126
+ return {
1127
+ type: "reference",
1128
+ confidence: referenceScore >= 6 ? "high" : "medium",
1129
+ signals: { iframeRatio, canvasRatio, articleRatio, navGameKeywords }
1130
+ };
841
1131
  }
842
- return { type, confidence, signals: { iframeRatio, canvasRatio, articleRatio, navGameKeywords } };
1132
+ return { type: "content", confidence: "high", signals: { iframeRatio, canvasRatio, articleRatio, navGameKeywords } };
843
1133
  }
844
1134
 
845
1135
  // src/scorer.ts
@@ -870,11 +1160,29 @@ function scorePage(pageType, contentChars, contentRatio, issues, siteType, aiSta
870
1160
  checks.push({ label: "Content depth", status: contentChars >= 300 ? "pass" : contentChars >= 100 ? "warn" : "fail", weight: 3 });
871
1161
  }
872
1162
  checks.push({ label: "Content ratio", status: contentRatio >= 30 ? "pass" : contentRatio >= 15 ? "warn" : "fail", weight: 2 });
1163
+ } else if (pageType === "video_detail") {
1164
+ if (siteType === "video") {
1165
+ checks.push({ label: "Video description", status: contentChars >= 50 ? "pass" : "warn", weight: 3 });
1166
+ checks.push({ label: "Content ratio", status: contentRatio >= 15 ? "pass" : contentRatio >= 5 ? "warn" : "fail", weight: 2 });
1167
+ } else {
1168
+ checks.push({ label: "Content depth", status: contentChars >= 300 ? "pass" : contentChars >= 100 ? "warn" : "fail", weight: 3 });
1169
+ checks.push({ label: "Content ratio", status: contentRatio >= 30 ? "pass" : contentRatio >= 15 ? "warn" : "fail", weight: 2 });
1170
+ }
1171
+ } else if (pageType === "reference_detail") {
1172
+ if (siteType === "reference") {
1173
+ checks.push({ label: "Entry completeness", status: contentChars >= 100 ? "pass" : contentChars >= 50 ? "warn" : "fail", weight: 3 });
1174
+ checks.push({ label: "Content ratio", status: contentRatio >= 20 ? "pass" : contentRatio >= 5 ? "warn" : "fail", weight: 2 });
1175
+ } else {
1176
+ checks.push({ label: "Content depth", status: contentChars >= 300 ? "pass" : contentChars >= 100 ? "warn" : "fail", weight: 3 });
1177
+ checks.push({ label: "Content ratio", status: contentRatio >= 30 ? "pass" : contentRatio >= 15 ? "warn" : "fail", weight: 2 });
1178
+ }
1179
+ } else if (pageType === "reference_listing") {
1180
+ checks.push({ label: "Listing content", status: contentChars >= 200 ? "pass" : contentChars >= 50 ? "warn" : "fail", weight: 2 });
1181
+ } else if (pageType === "listing") {
1182
+ checks.push({ label: "Content", status: contentChars >= 200 ? "pass" : contentChars >= 50 ? "warn" : "fail", weight: 2 });
873
1183
  } else if (pageType === "required") {
874
1184
  checks.push({ label: "Exists", status: contentChars > 0 ? "pass" : "fail", weight: 3 });
875
1185
  checks.push({ label: "Content depth", status: contentChars >= 300 ? "pass" : contentChars >= 100 ? "warn" : "fail", weight: 2 });
876
- } else if (pageType === "listing") {
877
- checks.push({ label: "Content", status: contentChars >= 200 ? "pass" : contentChars >= 50 ? "warn" : "fail", weight: 2 });
878
1186
  } else if (pageType === "utility") {
879
1187
  checks.push({ label: "Functional", status: contentChars > 0 ? "pass" : "warn", weight: 1 });
880
1188
  } else {
@@ -889,8 +1197,11 @@ var AI_PAGE_TYPE_WEIGHTS = {
889
1197
  homepage: 1.5,
890
1198
  content: 1,
891
1199
  game_detail: 1,
1200
+ video_detail: 1,
1201
+ reference_detail: 1,
892
1202
  unknown: 0.5,
893
1203
  listing: 0.1,
1204
+ reference_listing: 0.1,
894
1205
  required: 0.2,
895
1206
  utility: 0.1
896
1207
  };
@@ -1174,12 +1485,90 @@ function checkGameSite(pages, pagesSignals, lang) {
1174
1485
  }
1175
1486
  return items;
1176
1487
  }
1488
+ function checkVideoSite(pages, pagesSignals, lang) {
1489
+ const items = [];
1490
+ const subpages = pages.slice(1);
1491
+ const subpageSignals = pagesSignals.slice(1);
1492
+ if (subpages.length > 0) {
1493
+ let thinDesc = 0;
1494
+ const thinPages = [];
1495
+ for (let i = 0; i < subpages.length; i++) {
1496
+ const sig = subpageSignals[i];
1497
+ if (sig && sig.textLength < 50) {
1498
+ thinDesc++;
1499
+ try {
1500
+ thinPages.push(new URL(subpages[i].url).pathname);
1501
+ } catch {
1502
+ thinPages.push(subpages[i].url);
1503
+ }
1504
+ }
1505
+ }
1506
+ if (subpages.length > 0) {
1507
+ const ratio = thinDesc / subpages.length;
1508
+ items.push(
1509
+ ratio > 0.5 ? { name: t("item.content.video_desc", lang), status: "warn", message: t("content.video_desc.warn", lang, { thin: thinDesc, total: subpages.length }), detail: thinPages.slice(0, 5).join(", ") } : { name: t("item.content.video_desc", lang), status: "pass", message: t("content.video_desc.pass", lang, { total: subpages.length }) }
1510
+ );
1511
+ }
1512
+ }
1513
+ if (subpages.length >= 3) {
1514
+ const tpl = detectTemplatePages(subpages);
1515
+ items.push({
1516
+ name: t("item.content.video_variety", lang),
1517
+ status: tpl.isTemplate ? "warn" : "pass",
1518
+ message: t(tpl.isTemplate ? "content.video_variety.warn" : "content.video_variety.pass", lang, { pct: tpl.similarity })
1519
+ });
1520
+ }
1521
+ return items;
1522
+ }
1523
+ function checkReferenceSite(pages, pagesSignals, lang) {
1524
+ const items = [];
1525
+ const subpages = pages.slice(1);
1526
+ const subpageSignals = pagesSignals.slice(1);
1527
+ if (subpages.length > 0) {
1528
+ let thinEntries = 0;
1529
+ const thinPages = [];
1530
+ for (let i = 0; i < subpages.length; i++) {
1531
+ const sig = subpageSignals[i];
1532
+ if (sig && sig.textLength < 100) {
1533
+ thinEntries++;
1534
+ try {
1535
+ thinPages.push(new URL(subpages[i].url).pathname);
1536
+ } catch {
1537
+ thinPages.push(subpages[i].url);
1538
+ }
1539
+ }
1540
+ }
1541
+ if (subpages.length > 0) {
1542
+ const ratio = thinEntries / subpages.length;
1543
+ items.push(
1544
+ ratio > 0.5 ? { name: t("item.content.reference_entry", lang), status: "warn", message: t("content.reference_entry.warn", lang, { thin: thinEntries, total: subpages.length }), detail: thinPages.slice(0, 5).join(", ") } : { name: t("item.content.reference_entry", lang), status: "pass", message: t("content.reference_entry.pass", lang) }
1545
+ );
1546
+ }
1547
+ }
1548
+ if (subpages.length >= 3) {
1549
+ const tpl = detectTemplatePages(subpages);
1550
+ items.push({
1551
+ name: t("item.content.reference_variety", lang),
1552
+ status: tpl.similarity > 70 ? "warn" : "pass",
1553
+ message: t(tpl.similarity > 70 ? "content.reference_variety.warn" : "content.reference_variety.pass", lang, { pct: tpl.similarity })
1554
+ });
1555
+ }
1556
+ return items;
1557
+ }
1177
1558
  function checkContentQuality(pages, sitePageCount, lang, siteType = "content", pagesSignals) {
1178
1559
  const items = [];
1179
1560
  if (siteType === "game") {
1180
1561
  if (pagesSignals) {
1181
1562
  items.push(...checkGameSite(pages, pagesSignals, lang));
1182
1563
  }
1564
+ } else if (siteType === "video") {
1565
+ if (pagesSignals) {
1566
+ items.push(...checkVideoSite(pages, pagesSignals, lang));
1567
+ }
1568
+ } else if (siteType === "reference") {
1569
+ if (pagesSignals) {
1570
+ items.push(...checkReferenceSite(pages, pagesSignals, lang));
1571
+ }
1183
1572
  } else {
1184
1573
  items.push(...checkContentSite(pages, lang));
1185
1574
  }
@@ -1322,20 +1711,31 @@ function checkPolicyCompliance(pages, lang) {
1322
1711
  for (const page of pages) {
1323
1712
  for (const p of BLACKLIST) {
1324
1713
  const m = page.text.match(p);
1325
- if (m) violations.push({ url: page.url, match: m[0] });
1714
+ if (m) {
1715
+ const hasSubstance = page.text.replace(/\s+/g, "").length > 200;
1716
+ violations.push({ url: page.url, match: m[0], hasSubstance });
1717
+ }
1326
1718
  }
1327
1719
  }
1328
- items.push(
1329
- violations.length > 0 ? { name: t("item.policy.keywords", lang), status: "fail", message: t("policy.keywords.fail", lang, { count: violations.length }), detail: violations.map((v) => `${v.url}: "${v.match}"`).join("; ") } : { name: t("item.policy.keywords", lang), status: "pass", message: t("policy.keywords.pass", lang) }
1330
- );
1720
+ const allHaveSubstance = violations.length > 0 && violations.every((v) => v.hasSubstance);
1721
+ const status = violations.length === 0 ? "pass" : allHaveSubstance ? "warn" : "fail";
1722
+ items.push({
1723
+ name: t("item.policy.keywords", lang),
1724
+ status,
1725
+ message: violations.length > 0 ? t("policy.keywords.fail", lang, { count: violations.length }) : t("policy.keywords.pass", lang),
1726
+ detail: violations.length > 0 ? violations.map((v) => `${new URL(v.url).pathname}: "${v.match}"`).join("; ") : void 0
1727
+ });
1331
1728
  return { name: t("cat.policy", lang), items };
1332
1729
  }
1333
1730
 
1334
1731
  // src/classifier.ts
1335
- var REQUIRED_PATTERNS = [/\/about/i, /\/privacy/i, /\/contact/i, /\/terms/i, /\/legal/i];
1336
- var CONTENT_PREFIXES = ["/blog/", "/news/", "/guides/", "/articles/", "/posts/", "/tutorials/", "/wiki/"];
1732
+ var REQUIRED_PATTERNS = [/\/about/i, /\/privacy/i, /\/contact/i, /\/terms/i, /\/legal/i, /\/editorial-policy/i, /\/imprint/i];
1733
+ var CONTENT_PREFIXES = ["/blog/", "/news/", "/guides/", "/articles/", "/posts/", "/tutorials/"];
1337
1734
  var GAME_PREFIXES = ["/games/", "/game/", "/play/", "/online-games/"];
1338
- var LISTING_PATHS = ["/blog", "/news", "/guides", "/articles", "/games", "/play", "/categories", "/tags", "/archive"];
1735
+ var VIDEO_PREFIXES = ["/videos/", "/video/", "/watch/", "/v/", "/shorts/", "/clip/", "/stream/"];
1736
+ var REFERENCE_PREFIXES = ["/wiki/", "/reference/", "/docs/", "/encyclopedia/", "/glossary/", "/knowledge/", "/archive/", "/database/", "/transcript/"];
1737
+ var REFERENCE_LISTING_PATHS = ["/wiki", "/reference", "/docs", "/encyclopedia", "/glossary", "/knowledge", "/archive", "/database", "/transcript"];
1738
+ var LISTING_PATHS = ["/blog", "/news", "/guides", "/articles", "/games", "/play", "/videos", "/watch", "/channels", "/categories", "/tags", "/archive"];
1339
1739
  var UTILITY_PATTERNS = [/\/download/i, /\/search/i, /\/login/i, /\/signup/i, /\/register/i, /\/sitemap/i, /\/404/i];
1340
1740
  function classifyPage(url) {
1341
1741
  let pathname;
@@ -1361,6 +1761,19 @@ function classifyPage(url) {
1361
1761
  if (suffix.length > 1) return "game_detail";
1362
1762
  }
1363
1763
  }
1764
+ for (const prefix of VIDEO_PREFIXES) {
1765
+ if (normalizedPath.startsWith(prefix.replace(/\/$/, "/"))) {
1766
+ const suffix = normalizedPath.slice(prefix.replace(/\/$/, "").length);
1767
+ if (suffix.length > 1) return "video_detail";
1768
+ }
1769
+ }
1770
+ for (const prefix of REFERENCE_PREFIXES) {
1771
+ if (normalizedPath.startsWith(prefix.replace(/\/$/, "/"))) {
1772
+ const suffix = normalizedPath.slice(prefix.replace(/\/$/, "").length);
1773
+ if (suffix.length > 1) return "reference_detail";
1774
+ }
1775
+ }
1776
+ if (REFERENCE_LISTING_PATHS.some((p) => normalizedPath === p || normalizedPath === p.replace(/\/$/, ""))) return "reference_listing";
1364
1777
  if (LISTING_PATHS.some((p) => normalizedPath === p || normalizedPath === p.replace(/\/$/, ""))) return "listing";
1365
1778
  const langPrefix = normalizedPath.match(/^\/[a-z]{2}(\/|$)/);
1366
1779
  if (langPrefix) {
@@ -1380,6 +1793,20 @@ function classifyPage(url) {
1380
1793
  return "listing";
1381
1794
  }
1382
1795
  }
1796
+ for (const prefix of VIDEO_PREFIXES) {
1797
+ if (rest.startsWith(prefix.replace(/\/$/, "/"))) {
1798
+ const suffix = rest.slice(prefix.replace(/\/$/, "").length);
1799
+ if (suffix.length > 1) return "video_detail";
1800
+ return "listing";
1801
+ }
1802
+ }
1803
+ for (const prefix of REFERENCE_PREFIXES) {
1804
+ if (rest.startsWith(prefix.replace(/\/$/, "/"))) {
1805
+ const suffix = rest.slice(prefix.replace(/\/$/, "").length);
1806
+ if (suffix.length > 1) return "reference_detail";
1807
+ return "reference_listing";
1808
+ }
1809
+ }
1383
1810
  if (REQUIRED_PATTERNS.some((p) => p.test(rest))) return "required";
1384
1811
  }
1385
1812
  return "unknown";
@@ -1418,14 +1845,22 @@ function buildPageDetails(pages, aiAnalyses, siteType) {
1418
1845
  contentStatus = contentStatus === "fail" ? "fail" : "warn";
1419
1846
  }
1420
1847
  }
1421
- const pageType = classifyPage(page.url);
1422
1848
  const ai = aiMap.get(page.url);
1423
1849
  const aiStatus = ai?.status;
1424
1850
  const relevance = ai?.relevance;
1851
+ const pageType = ai?.inferredPageType ?? classifyPage(page.url);
1425
1852
  const { score } = scorePage(pageType, contentChars, contentRatio, issues, siteType, aiStatus);
1426
1853
  const detail = { url: page.url, title: page.title, pageType, totalChars, contentChars, contentRatio, contentStatus, issues, score };
1427
1854
  if (relevance) detail.relevance = relevance;
1428
- if (ai) detail.ai = { status: ai.status, assessment: ai.assessment, suggestions: ai.suggestions };
1855
+ if (ai) detail.ai = {
1856
+ status: ai.status,
1857
+ valueScore: ai.valueScore,
1858
+ originalityScore: ai.originalityScore,
1859
+ relevanceScore: ai.relevanceScore,
1860
+ complianceScore: ai.complianceScore,
1861
+ assessment: ai.assessment,
1862
+ suggestions: ai.suggestions
1863
+ };
1429
1864
  return detail;
1430
1865
  });
1431
1866
  }
@@ -1510,7 +1945,11 @@ async function check(options) {
1510
1945
  const allSignals = [homeData.signals];
1511
1946
  const internalLinks = homeData.links.filter((l) => {
1512
1947
  try {
1513
- return new URL(l).origin === origin && isContentUrl(l);
1948
+ const u = new URL(l);
1949
+ if (u.origin !== origin) return false;
1950
+ if (!isContentUrl(l)) return false;
1951
+ if (u.pathname === "/" && u.search.length > 0) return false;
1952
+ return true;
1514
1953
  } catch {
1515
1954
  return false;
1516
1955
  }
@@ -1525,18 +1964,24 @@ async function check(options) {
1525
1964
  const allInternal = [.../* @__PURE__ */ new Set([...internalLinks, ...sitemapInternal])];
1526
1965
  const uniqueLinks = allInternal.slice(0, phase1Limit);
1527
1966
  const deadLinks = [];
1528
- const crawledUrls = /* @__PURE__ */ new Set([url.replace(/\/+$/, "")]);
1967
+ const crawledUrls = /* @__PURE__ */ new Set([homeData.url.replace(/\/+$/, "")]);
1529
1968
  async function crawlPage(link) {
1530
- const norm = link.replace(/\/+$/, "");
1969
+ const norm = link.replace(/\/+$/, "").split("#")[0];
1531
1970
  if (crawledUrls.has(norm)) return;
1532
1971
  crawledUrls.add(norm);
1533
1972
  try {
1534
1973
  const pg = await browser.newPage();
1535
1974
  const data = await fetchPage(pg, link, timeout);
1975
+ const postNorm = data.url.replace(/\/+$/, "").split("#")[0];
1976
+ if (crawledUrls.has(postNorm) && postNorm !== norm) {
1977
+ await pg.close();
1978
+ return;
1979
+ }
1980
+ crawledUrls.add(postNorm);
1536
1981
  if (data.status >= 400) {
1537
1982
  deadLinks.push(`${link} (${data.status})`);
1538
1983
  } else {
1539
- pages.push({ url: link, text: data.text, title: data.title, links: data.links });
1984
+ pages.push({ url: data.url, text: data.text, title: data.title, links: data.links });
1540
1985
  allSignals.push(data.signals);
1541
1986
  }
1542
1987
  await pg.close();
@@ -1547,14 +1992,14 @@ async function check(options) {
1547
1992
  progress(`Phase 1: Crawling ${uniqueLinks.length} pages...`);
1548
1993
  for (let i = 0; i < uniqueLinks.length; i++) {
1549
1994
  const link = uniqueLinks[i];
1550
- progress(`Phase 1: [${i + 1}/${uniqueLinks.length}] ${new URL(link).pathname}`);
1995
+ progress(`Phase 1: [${i + 1}/${uniqueLinks.length}] ${new URL(link).pathname}${new URL(link).search}`);
1551
1996
  await crawlPage(link);
1552
1997
  }
1553
1998
  const CHILDREN_PER_LISTING = 10;
1554
1999
  const MAX_DISCOVERY_DEPTH = 3;
1555
2000
  const discoveredContent = /* @__PURE__ */ new Set();
1556
2001
  const discoveryQueue = pages.map((p) => ({ url: p.url, links: p.links, depth: 0 }));
1557
- const seenInDiscovery = new Set([...crawledUrls].map((u) => u.replace(/\/+$/, "")));
2002
+ const seenInDiscovery = new Set([...crawledUrls].map((u) => u.replace(/\/+$/, "").split("#")[0]));
1558
2003
  while (discoveryQueue.length > 0) {
1559
2004
  const current = discoveryQueue.shift();
1560
2005
  if (current.depth > MAX_DISCOVERY_DEPTH) continue;
@@ -1588,7 +2033,7 @@ async function check(options) {
1588
2033
  if (toCrawl.length > 0) progress(`Phase 2: Crawling ${toCrawl.length} content pages (from ${discoveredContent.size} discovered)...`);
1589
2034
  for (let i = 0; i < toCrawl.length; i++) {
1590
2035
  const link = toCrawl[i];
1591
- progress(`Phase 2: [${i + 1}/${toCrawl.length}] ${new URL(link).pathname}`);
2036
+ progress(`Phase 2: [${i + 1}/${toCrawl.length}] ${new URL(link).pathname}${new URL(link).search}`);
1592
2037
  await crawlPage(link);
1593
2038
  const crawledPage = pages[pages.length - 1];
1594
2039
  if (crawledPage && crawledPage.url === link) {
@@ -1666,11 +2111,55 @@ async function check(options) {
1666
2111
  aiItems.push({ name: t("item.ai.suggestions", lang), status: "warn", message: t("ai.suggestion_count", lang, { count: aiResult.suggestions.length }), detail: aiResult.suggestions.join("; ") });
1667
2112
  }
1668
2113
  allCategories.push({ name: t("group.ai_value", lang), items: aiItems, group: "soft" });
1669
- const seriousViolations = pageAnalyses.filter((a) => (a.complianceScore ?? 5) <= 2);
1670
- const suspiciousPages = pageAnalyses.filter((a) => {
2114
+ let suspiciousPages = pageAnalyses.filter((a) => {
1671
2115
  const c = a.complianceScore ?? 5;
1672
2116
  return c > 2 && c <= 5;
1673
2117
  });
2118
+ const shortTextPages = pageAnalyses.filter((a) => {
2119
+ const c = a.complianceScore ?? 5;
2120
+ const text = uniquePages.find((up) => up.url === a.url)?.text ?? "";
2121
+ return c <= 2 && text.replace(/\s+/g, "").length < 200;
2122
+ });
2123
+ const recheckUrls = new Set(suspiciousPages.map((p) => p.url));
2124
+ for (const p of shortTextPages) {
2125
+ if (!recheckUrls.has(p.url)) {
2126
+ suspiciousPages.push(p);
2127
+ recheckUrls.add(p.url);
2128
+ }
2129
+ }
2130
+ if (suspiciousPages.length > 0) {
2131
+ const apiKeyResolved2 = apiKey || process.env.AI_API_KEY;
2132
+ if (apiKeyResolved2) {
2133
+ const recheckResults = await recheckCompliance(
2134
+ suspiciousPages.map((p) => ({
2135
+ url: p.url,
2136
+ text: uniquePages.find((up) => up.url === p.url)?.text ?? "",
2137
+ firstComplianceScore: p.complianceScore ?? 5
2138
+ })),
2139
+ lang,
2140
+ progress
2141
+ );
2142
+ for (const analysis of pageAnalyses) {
2143
+ const recheck = recheckResults.get(analysis.url);
2144
+ if (recheck) {
2145
+ analysis.complianceScore = recheck.complianceScore;
2146
+ }
2147
+ }
2148
+ for (const analysis of pageAnalyses) {
2149
+ const v = analysis.valueScore ?? 5;
2150
+ const o = analysis.originalityScore ?? 5;
2151
+ const r = analysis.relevanceScore ?? 5;
2152
+ const c = analysis.complianceScore ?? 5;
2153
+ const geoMean = Math.pow(v * o * r * c, 0.25);
2154
+ analysis.status = geoMean >= 7 ? "pass" : geoMean >= 4 ? "warn" : "fail";
2155
+ }
2156
+ suspiciousPages = pageAnalyses.filter((a) => {
2157
+ const c = a.complianceScore ?? 5;
2158
+ return c > 2 && c <= 5;
2159
+ });
2160
+ }
2161
+ }
2162
+ const seriousViolations = pageAnalyses.filter((a) => (a.complianceScore ?? 5) <= 2);
1674
2163
  const complianceItems = [];
1675
2164
  if (seriousViolations.length > 0) {
1676
2165
  complianceItems.push({
@@ -1693,6 +2182,16 @@ async function check(options) {
1693
2182
  });
1694
2183
  }
1695
2184
  allCategories.push({ name: t("group.policy_compliance", lang), items: complianceItems, group: "hard" });
2185
+ const avgCompliance = pageAnalyses.length > 0 ? pageAnalyses.reduce((s, a) => s + (a.complianceScore ?? 5), 0) / pageAnalyses.length : 5;
2186
+ if (avgCompliance >= 7) {
2187
+ const policyCat2 = allCategories.find((c) => c.name === t("cat.policy", lang));
2188
+ if (policyCat2) {
2189
+ const keywordItem = policyCat2.items.find((i) => i.name === t("item.policy.keywords", lang));
2190
+ if (keywordItem && keywordItem.status === "fail") {
2191
+ keywordItem.status = "warn";
2192
+ }
2193
+ }
2194
+ }
1696
2195
  } catch (err) {
1697
2196
  allCategories.push({ name: t("group.ai_value", lang), items: [{ name: "AI", status: "skip", message: t("ai.fail", lang, { error: err instanceof Error ? err.message : String(err) }) }], group: "soft" });
1698
2197
  }
package/dist/cli.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  getSupportedLangs,
11
11
  isValidLang,
12
12
  t
13
- } from "./chunk-PGQWYP7I.js";
13
+ } from "./chunk-XKNR4LB4.js";
14
14
 
15
15
  // src/cli.ts
16
16
  import "dotenv/config";
@@ -63,12 +63,12 @@ function renderTerminalReport(report) {
63
63
  chalk.gray(` Site type: ${typeLabel}${confidenceLabel}`)
64
64
  ];
65
65
  if (report.siteTopic) {
66
- lines.push(chalk.gray(` Topic: ${report.siteTopic.topic} \u2014 ${report.siteTopic.description}`));
66
+ lines.push(chalk.gray(` ${t("reporter.topic", lang)}: ${report.siteTopic.topic} \u2014 ${report.siteTopic.description}`));
67
67
  }
68
68
  if (report.samplingInfo) {
69
69
  const s = report.samplingInfo;
70
70
  const confColor = s.confidence === "high" ? chalk.green : s.confidence === "medium" ? chalk.yellow : chalk.red;
71
- lines.push(chalk.gray(` Pages: ${s.totalDiscovered} total, ${s.recentCount} recent (6mo), ${s.sampledCount} sampled (${s.samplePct}%) ${confColor(s.confidence + " confidence")}`));
71
+ lines.push(chalk.gray(` ${t("reporter.pages_label", lang)}: ${s.totalDiscovered} total, ${s.recentCount} recent (6mo), ${s.sampledCount} sampled (${s.samplePct}%) ${confColor(t("reporter.confidence", lang, { confidence: s.confidence }))}`));
72
72
  }
73
73
  if (report.siteType === "unsupported") {
74
74
  lines.push("");
@@ -110,15 +110,16 @@ function renderTerminalReport(report) {
110
110
  lines.push(chalk.gray(` \u2502`));
111
111
  const hardContrib = Math.round(report.hardStatus === "ready" ? 100 * 0.4 : report.hardCategories.flatMap((c) => c.items).filter((i) => i.status === "pass").length / Math.max(1, report.hardCategories.flatMap((c) => c.items).length) * 100 * 0.4);
112
112
  const softContrib = Math.round(report.softScore * 0.6);
113
- lines.push(chalk.gray(` \u2502 Hard ${Math.round(hardContrib)}% \xD7 0.4 + Soft ${report.softScore}% \xD7 0.6 - Penalty ${report.warningPenalty} = ${report.compositeScore}`));
113
+ lines.push(chalk.gray(` \u2502 ${t("reporter.formula_label", lang, { hardPct: Math.round(hardContrib), softPct: report.softScore, penalty: report.warningPenalty, total: report.compositeScore })}`));
114
114
  if (report.siteAiScore > 0) {
115
- lines.push(chalk.gray(` \u2502 AI Value Score: ${report.siteAiScore}/100 (geometric mean \xD7 page-type weights)`));
115
+ lines.push(chalk.gray(` \u2502 ${t("reporter.ai_value_label", lang)}: ${report.siteAiScore}/100 (${t("reporter.ai_value_note", lang)})`));
116
116
  }
117
117
  if (report.aiDimensionAverages) {
118
118
  const d = report.aiDimensionAverages;
119
119
  const dimColor = (v) => v >= 8 ? chalk.green : v >= 5 ? chalk.yellow : chalk.red;
120
+ const dimLabel = (key, v) => `${t(`reporter.dim_${key}`, lang)} ${v}`;
120
121
  lines.push(
121
- chalk.gray(` \u2502 AI Dimensions: `) + `${dimColor(d.value)("Value " + d.value)} ${dimColor(d.originality)("Originality " + d.originality)} ${dimColor(d.relevance)("Relevance " + d.relevance)} ${dimColor(d.compliance)("Compliance " + d.compliance)} ` + chalk.gray("(avg /10)")
122
+ chalk.gray(` \u2502 ${t("reporter.ai_dimensions", lang)}: `) + `${dimColor(d.value)(dimLabel("value", d.value))} ${dimColor(d.originality)(dimLabel("originality", d.originality))} ${dimColor(d.relevance)(dimLabel("relevance", d.relevance))} ${dimColor(d.compliance)(dimLabel("compliance", d.compliance))} ` + chalk.gray(`(${t("reporter.avg_per_10", lang)})`)
122
123
  );
123
124
  }
124
125
  lines.push(chalk.gray(` \u2514\u2500`));
@@ -172,6 +173,9 @@ var PAGE_TYPE_ICONS = {
172
173
  homepage: chalk.cyan("*"),
173
174
  content: chalk.green("A"),
174
175
  game_detail: chalk.blue("G"),
176
+ video_detail: chalk.cyan("V"),
177
+ reference_detail: chalk.magenta("R"),
178
+ reference_listing: chalk.magenta("r"),
175
179
  required: chalk.yellow("!"),
176
180
  listing: chalk.gray("L"),
177
181
  utility: chalk.gray("#"),
@@ -215,28 +219,28 @@ function renderMarkdownReport(report) {
215
219
  const lang = report.lang;
216
220
  const typeKey = `detector.type.${report.siteType}`;
217
221
  const typeLabel = t(typeKey, lang);
218
- lines.push(`# AdSense \u5BA1\u6838\u62A5\u544A`);
222
+ lines.push(`# ${t("md.report_title", lang)}`);
219
223
  lines.push("");
220
- lines.push(`| \u9879\u76EE | \u503C |`);
224
+ lines.push(`| ${t("md.table.project", lang)} | ${t("md.table.value", lang)} |`);
221
225
  lines.push(`|------|-----|`);
222
- lines.push(`| URL | ${report.url} |`);
223
- lines.push(`| \u65F6\u95F4 | ${report.timestamp} |`);
224
- lines.push(`| \u7AD9\u70B9\u7C7B\u578B | ${typeLabel} (${report.siteTypeConfidence}) |`);
226
+ lines.push(`| ${t("md.table.url", lang)} | ${report.url} |`);
227
+ lines.push(`| ${t("md.table.time", lang)} | ${report.timestamp} |`);
228
+ lines.push(`| ${t("md.table.site_type", lang)} | ${typeLabel} (${report.siteTypeConfidence}) |`);
225
229
  if (report.siteTopic) {
226
- lines.push(`| \u4E3B\u9898 | ${report.siteTopic.topic} |`);
227
- lines.push(`| \u63CF\u8FF0 | ${report.siteTopic.description} |`);
230
+ lines.push(`| ${t("md.table.topic", lang)} | ${report.siteTopic.topic} |`);
231
+ lines.push(`| ${t("md.table.description", lang)} | ${report.siteTopic.description} |`);
228
232
  }
229
233
  if (report.samplingInfo) {
230
234
  const s = report.samplingInfo;
231
- lines.push(`| \u62BD\u6837 | ${s.totalDiscovered} total, ${s.recentCount} recent (6mo), ${s.sampledCount} sampled (${s.samplePct}%, ${s.confidence}) |`);
235
+ lines.push(`| ${t("md.table.sampling", lang)} | ${s.totalDiscovered} ${t("md.table.total", lang)}, ${s.recentCount} ${t("md.table.recent", lang)}, ${s.sampledCount} ${t("md.table.sampled", lang)} (${s.samplePct}%, ${s.confidence} ${t("md.table.confidence", lang)}) |`);
232
236
  }
233
237
  lines.push("");
234
- lines.push(`## \u7EFC\u5408\u8BC4\u5206: ${report.compositeScore}/100`);
238
+ lines.push(`## ${t("md.composite_score_title", lang)}: ${report.compositeScore}/100`);
235
239
  lines.push("");
236
240
  const hardFailCount = report.hardCategories.flatMap((c) => c.items).filter((i) => i.status === "fail").length;
237
241
  const hardWarnCount = report.hardCategories.flatMap((c) => c.items).filter((i) => i.status === "warn").length;
238
242
  const hardLabel = report.hardStatus === "ready" ? "\u2705 PASS" : report.hardStatus === "warn" ? "\u26A0\uFE0F WARN" : "\u274C FAIL";
239
- lines.push(`### \u786C\u6027\u8981\u6C42 ${hardLabel}`);
243
+ lines.push(`### ${t("md.hard_requirements", lang)} ${hardLabel}`);
240
244
  lines.push("");
241
245
  for (const cat of report.hardCategories) {
242
246
  for (const item of cat.items) {
@@ -245,7 +249,7 @@ function renderMarkdownReport(report) {
245
249
  }
246
250
  }
247
251
  lines.push("");
248
- lines.push(`### \u67D4\u6027\u8BC4\u5206 ${report.softScore}/100`);
252
+ lines.push(`### ${t("md.soft_scoring", lang)} ${report.softScore}/100`);
249
253
  lines.push("");
250
254
  for (const cat of report.softCategories) {
251
255
  const isAiCat = cat.name.includes("AI") || cat.name.includes("ai");
@@ -259,71 +263,89 @@ function renderMarkdownReport(report) {
259
263
  lines.push("");
260
264
  if (report.aiDimensionAverages) {
261
265
  const d = report.aiDimensionAverages;
262
- lines.push(`### AI \u4EF7\u503C\u5206\u6790`);
266
+ lines.push(`### ${t("md.ai_value_title", lang)}`);
263
267
  lines.push("");
264
- lines.push(`| \u7EF4\u5EA6 | \u5747\u5206 |`);
268
+ lines.push(`| ${t("md.table.dimension", lang)} | ${t("md.table.avg_score", lang)} |`);
265
269
  lines.push(`|------|------|`);
266
- lines.push(`| Value\uFF08\u4EF7\u503C\uFF09 | ${d.value}/10 |`);
267
- lines.push(`| Originality\uFF08\u539F\u521B\uFF09 | ${d.originality}/10 |`);
268
- lines.push(`| Relevance\uFF08\u76F8\u5173\uFF09 | ${d.relevance}/10 |`);
269
- lines.push(`| Compliance\uFF08\u5408\u89C4\uFF09 | ${d.compliance}/10 |`);
270
+ lines.push(`| ${t("md.dim_value", lang)} | ${d.value}/10 |`);
271
+ lines.push(`| ${t("md.dim_originality", lang)} | ${d.originality}/10 |`);
272
+ lines.push(`| ${t("md.dim_relevance", lang)} | ${d.relevance}/10 |`);
273
+ lines.push(`| ${t("md.dim_compliance", lang)} | ${d.compliance}/10 |`);
270
274
  lines.push("");
271
- lines.push(`**\u7AD9\u70B9 AI \u8BC4\u5206**: ${report.siteAiScore}/100\uFF08\u51E0\u4F55\u5747\u503C \xD7 \u9875\u9762\u7C7B\u578B\u52A0\u6743\uFF09`);
275
+ lines.push(`**${t("md.site_ai_score", lang)}**: ${report.siteAiScore}/100\uFF08${t("md.geometric_weighted", lang)}\uFF09`);
272
276
  lines.push("");
273
277
  }
274
278
  const hardContrib = Math.round(report.hardStatus === "ready" ? 100 * 0.4 : report.hardCategories.flatMap((c) => c.items).filter((i) => i.status === "pass").length / Math.max(1, report.hardCategories.flatMap((c) => c.items).length) * 100 * 0.4);
275
279
  lines.push(`> Hard ${Math.round(hardContrib)}% \xD7 0.4 + Soft ${report.softScore}% \xD7 0.6 - Penalty ${report.warningPenalty} = ${report.compositeScore}`);
276
280
  lines.push("");
277
281
  if (report.pages.length > 0) {
278
- lines.push(`### \u9010\u9875\u8BE6\u60C5 (${report.pages.length} pages)`);
282
+ lines.push(`### ${t("md.page_details", lang)} (${t("md.pages_count", lang, { count: report.pages.length })})`);
279
283
  lines.push("");
280
284
  const problems = report.pages.filter((p) => p.contentStatus !== "pass" || p.issues.length > 0 || p.ai && p.ai.status !== "pass");
281
285
  const ok = report.pages.filter((p) => p.contentStatus === "pass" && p.issues.length === 0 && (!p.ai || p.ai.status === "pass"));
282
- lines.push(`| \u72B6\u6001 | \u7C7B\u578B | \u8DEF\u5F84 | \u8BC4\u5206 | \u6B63\u6587\u6BD4 | \u6807\u9898 |`);
283
- lines.push(`|------|------|------|------|--------|------|`);
286
+ lines.push(`| ${t("md.table.status", lang)} | ${t("md.table.type", lang)} | ${t("md.table.path", lang)} | ${t("md.table.score", lang)} | ${t("md.table.content_ratio", lang)} | V | O | R | C | ${t("md.table.ai_composite", lang)} | ${t("md.table.title", lang)} |`);
287
+ lines.push(`|------|------|------|------|--------|---|---|---|---|--------|------|`);
284
288
  for (const p of [...problems, ...ok]) {
285
289
  const path = (() => {
286
290
  try {
287
- return new URL(p.url).pathname;
291
+ const u = new URL(p.url);
292
+ return u.pathname + u.search;
288
293
  } catch {
289
294
  return p.url;
290
295
  }
291
296
  })();
292
297
  const status = MD_ICONS[p.contentStatus];
293
- const scoreColor = p.score >= 80 ? "" : p.score >= 50 ? "" : "";
294
- lines.push(`| ${status} | ${p.pageType} | \`${path}\` | ${p.score}/100 | ${p.contentRatio}% | ${p.title} |`);
298
+ const ai = p.ai;
299
+ const v = ai?.valueScore != null ? ai.valueScore : "-";
300
+ const o = ai?.originalityScore != null ? ai.originalityScore : "-";
301
+ const r = ai?.relevanceScore != null ? ai.relevanceScore : "-";
302
+ const c = ai?.complianceScore != null ? ai.complianceScore : "-";
303
+ const aiComposite = ai?.valueScore != null && ai?.originalityScore != null && ai?.relevanceScore != null && ai?.complianceScore != null ? Math.round(Math.pow(ai.valueScore * ai.originalityScore * ai.relevanceScore * ai.complianceScore, 0.25) * 10) : "-";
304
+ lines.push(`| ${status} | ${p.pageType} | [\`${path}\`](${p.url}) | ${p.score}/100 | ${p.contentRatio}% | ${v} | ${o} | ${r} | ${c} | ${aiComposite} | ${p.title} |`);
295
305
  }
296
306
  lines.push("");
297
307
  const detailPages = problems.filter((p) => p.issues.length > 0 || p.ai && p.ai.status !== "pass");
298
308
  if (detailPages.length > 0) {
299
- lines.push(`#### \u95EE\u9898\u9875\u9762\u8BE6\u60C5`);
309
+ lines.push(`#### ${t("md.problem_pages", lang)}`);
300
310
  lines.push("");
301
311
  for (const p of detailPages) {
302
312
  const path = (() => {
303
313
  try {
304
- return new URL(p.url).pathname;
314
+ const u = new URL(p.url);
315
+ return u.pathname + u.search;
305
316
  } catch {
306
317
  return p.url;
307
318
  }
308
319
  })();
309
- lines.push(`**${path}** (${p.score}/100)`);
320
+ lines.push(`**[${path}](${p.url})** (${t("reporter.mechanical_label", lang)}: ${p.score}/100)`);
321
+ lines.push("");
310
322
  for (const issue of p.issues) lines.push(`- \u26A0\uFE0F ${issue}`);
311
323
  if (p.ai) {
312
- lines.push(`- ${MD_ICONS[p.ai.status]} AI: ${p.ai.assessment}`);
313
- for (const s of p.ai.suggestions.slice(0, 3)) lines.push(` - -> ${s}`);
324
+ const ai = p.ai;
325
+ lines.push(`- ${t("md.ai_status", lang)}: ${MD_ICONS[ai.status]} ${ai.status}`);
326
+ if (ai.valueScore != null) {
327
+ lines.push(`- ${t("md.four_dimensions", lang)}: **${t("md.dim_value", lang)} ${ai.valueScore}** | **${t("md.dim_originality", lang)} ${ai.originalityScore}** | **${t("md.dim_relevance", lang)} ${ai.relevanceScore}** | **${t("md.dim_compliance", lang)} ${ai.complianceScore}**`);
328
+ const geoMean = Math.round(Math.pow((ai.valueScore ?? 5) * (ai.originalityScore ?? 5) * (ai.relevanceScore ?? 5) * (ai.complianceScore ?? 5), 0.25) * 10);
329
+ lines.push(`- ${t("md.ai_composite_score", lang)}: ${geoMean}/100\uFF08${t("md.geometric_mean", lang)}\uFF09`);
330
+ }
331
+ lines.push(`- ${t("md.assessment", lang)}: ${ai.assessment}`);
332
+ if (ai.suggestions.length > 0) {
333
+ lines.push(`- ${t("md.suggestions", lang)}:`);
334
+ for (const s of ai.suggestions.slice(0, 3)) lines.push(` - ${s}`);
335
+ }
314
336
  }
315
337
  lines.push("");
316
338
  }
317
339
  }
318
340
  }
319
341
  if (report.hardStatus === "fail") {
320
- lines.push(`**\u274C NOT READY** \u2014 ${hardFailCount} \u9879\u5931\u8D25\u9700\u8981\u4FEE\u590D`);
342
+ lines.push(t("md.summary.not_ready", lang, { count: hardFailCount }));
321
343
  } else if (report.hardStatus === "warn") {
322
- lines.push(`**\u26A0\uFE0F NEEDS FIXES** \u2014 ${hardWarnCount} \u9879\u8B66\u544A\u5F85\u4FEE\u590D`);
344
+ lines.push(t("md.summary.needs_fixes", lang, { count: hardWarnCount }));
323
345
  } else if (report.warned > 0) {
324
- lines.push(`**\u26A0\uFE0F MOSTLY READY** \u2014 \u4FEE\u590D ${report.warned} \u9879\u8B66\u544A\u540E\u53EF\u63D0\u4EA4`);
346
+ lines.push(t("md.summary.mostly_ready", lang, { count: report.warned }));
325
347
  } else {
326
- lines.push(`**\u2705 READY** \u2014 \u53EF\u4EE5\u63D0\u4EA4 AdSense \u5BA1\u6838`);
348
+ lines.push(t("md.summary.ready", lang));
327
349
  }
328
350
  lines.push("");
329
351
  return lines.join("\n");
@@ -343,7 +365,7 @@ function getDomain(url) {
343
365
  }
344
366
  }
345
367
  var program = new Command();
346
- program.name("adsense-check").description("Check if a website meets Google AdSense review requirements").version("1.0.0").argument("<url>", "Website URL to check").option("-j, --json", "Output JSON to stdout").option("-n, --max-crawl <number>", "Total page crawl limit (Phase 1 + 2)", "50").option("-m, --page-limit <number>", "Max structural pages to crawl (Phase 1)", "50").option("-c, --content-limit <number>", "Max content pages to crawl (Phase 2)", "20").option("--sample-min <number>", "Min content pages to sample", "20").option("--sample-ratio <ratio>", "Content page sampling ratio (0-1)", "0.2").option("--ai", "Enable AI content quality analysis", false).option("-t, --timeout <ms>", "Page load timeout", "30000").option("--api-key <key>", "AI API key").option("-o, --output <dir>", "Report output directory", "tmp").option("--no-save", "Skip auto-saving report").option("-l, --lang <lang>", `Output language (${getSupportedLangs().join("|")})`, "en").option("--type <type>", "Force site type (content|tool|game), skip auto-detection").option("--detect-only", "Only detect site type/topic, skip full check").option("--page <url>", "Analyze a single page value (AI four-dimension scoring)").action(async (url, opts) => {
368
+ program.name("adsense-check").description("Check if a website meets Google AdSense review requirements").version("1.0.0").argument("<url>", "Website URL to check").option("-j, --json", "Output JSON to stdout").option("-n, --max-crawl <number>", "Total page crawl limit (Phase 1 + 2)", "50").option("-m, --page-limit <number>", "Max structural pages to crawl (Phase 1)", "50").option("-c, --content-limit <number>", "Max content pages to crawl (Phase 2)", "20").option("--sample-min <number>", "Min content pages to sample", "20").option("--sample-ratio <ratio>", "Content page sampling ratio (0-1)", "0.2").option("--ai", "Enable AI content quality analysis", false).option("-t, --timeout <ms>", "Page load timeout", "30000").option("--api-key <key>", "AI API key").option("-o, --output <dir>", "Report output directory", "tmp").option("--no-save", "Skip auto-saving report").option("-l, --lang <lang>", `Output language (${getSupportedLangs().join("|")})`, "en").option("--type <type>", "Force site type (content|tool|game|video|reference), skip auto-detection").option("--detect-only", "Only detect site type/topic, skip full check").option("--page <url>", "Analyze a single page value (AI four-dimension scoring)").action(async (url, opts) => {
347
369
  try {
348
370
  new URL(url);
349
371
  } catch {
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  type CheckStatus = 'pass' | 'warn' | 'fail' | 'skip';
2
2
  type Lang = string;
3
- type SiteType = 'content' | 'tool' | 'game' | 'unsupported';
4
- type PageType = 'homepage' | 'content' | 'game_detail' | 'required' | 'listing' | 'utility' | 'unknown';
3
+ type SiteType = 'content' | 'tool' | 'game' | 'video' | 'reference' | 'unsupported';
4
+ type PageType = 'homepage' | 'content' | 'game_detail' | 'video_detail' | 'reference_detail' | 'reference_listing' | 'required' | 'listing' | 'utility' | 'unknown';
5
5
  interface CheckItem {
6
6
  name: string;
7
7
  status: CheckStatus;
@@ -27,6 +27,10 @@ interface PageDetail {
27
27
  relevance?: 'relevant' | 'tangential' | 'off-topic';
28
28
  ai?: {
29
29
  status: CheckStatus;
30
+ valueScore?: number;
31
+ originalityScore?: number;
32
+ relevanceScore?: number;
33
+ complianceScore?: number;
30
34
  assessment: string;
31
35
  suggestions: string[];
32
36
  };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  check
3
- } from "./chunk-PGQWYP7I.js";
3
+ } from "./chunk-XKNR4LB4.js";
4
4
  export {
5
5
  check
6
6
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudcreate/adsense-check",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Check if a website meets Google AdSense review requirements",
5
5
  "homepage": "https://cloudcreate.ai",
6
6
  "repository": {