@cloudcreate/adsense-check 1.1.0 → 1.2.1
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 +24 -2
- package/dist/{chunk-GW4SHZYX.js → chunk-PGQWYP7I.js} +387 -331
- package/dist/cli.js +113 -6
- package/dist/index.d.ts +7 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -83,8 +83,29 @@ Checks are divided into **Hard Requirements** (pass/fail) and **Soft Scoring** (
|
|
|
83
83
|
Composite = Hard Pass Rate × 0.4 + Soft Score × 0.6 - Warning Penalty
|
|
84
84
|
```
|
|
85
85
|
|
|
86
|
-
- **Hard**: Site scale, required pages, structure, performance baseline, policy compliance
|
|
87
|
-
- **Soft**:
|
|
86
|
+
- **Hard**: Site scale, required pages, structure, performance baseline, policy compliance (including AI compliance)
|
|
87
|
+
- **Soft**: AI value analysis (45%), content quality (35%), user experience (10%), page quality (10%)
|
|
88
|
+
|
|
89
|
+
### AI Value Scoring
|
|
90
|
+
|
|
91
|
+
Each page is scored on four dimensions (0-10) by AI:
|
|
92
|
+
|
|
93
|
+
| Dimension | Description |
|
|
94
|
+
|-----------|-------------|
|
|
95
|
+
| **Value** | Does the page provide real, substantive information? |
|
|
96
|
+
| **Originality** | Is the content original (not scraped/AI-generated/copied)? |
|
|
97
|
+
| **Relevance** | How relevant is the page to the site's topic? |
|
|
98
|
+
| **Compliance** | Does the content comply with AdSense policies? |
|
|
99
|
+
|
|
100
|
+
Page score = geometric mean of all four dimensions. This means any weak dimension drags down the overall score significantly.
|
|
101
|
+
|
|
102
|
+
Site score = page-type weighted average across all analyzed pages (homepage and content pages have highest weight).
|
|
103
|
+
|
|
104
|
+
### Single-Page Analysis
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
adsense-check <site> --page <url> --ai
|
|
108
|
+
```
|
|
88
109
|
|
|
89
110
|
## Options
|
|
90
111
|
|
|
@@ -97,6 +118,7 @@ Composite = Hard Pass Rate × 0.4 + Soft Score × 0.6 - Warning Penalty
|
|
|
97
118
|
--sample-min <n> Min content pages to sample (default: 20)
|
|
98
119
|
--sample-ratio <ratio> Content page sampling ratio 0-1 (default: 0.2)
|
|
99
120
|
--ai Enable AI content quality analysis
|
|
121
|
+
--page <url> Analyze single page value (four-dimension scoring)
|
|
100
122
|
-t, --timeout <ms> Page load timeout (default: 30000)
|
|
101
123
|
--api-key <key> AI API key
|
|
102
124
|
-o, --output <dir> Report output dir (default: tmp)
|
|
@@ -195,6 +195,9 @@ var en = {
|
|
|
195
195
|
"item.ai.originality": "Originality",
|
|
196
196
|
"item.ai.compliance": "Compliance",
|
|
197
197
|
"item.ai.suggestions": "AI Suggestions",
|
|
198
|
+
"item.ai.compliance_serious": "Serious Violations",
|
|
199
|
+
"item.ai.compliance_suspicious": "Suspicious Content",
|
|
200
|
+
"item.ai.compliance_ok": "AI Compliance",
|
|
198
201
|
// Required page names
|
|
199
202
|
"page.about": "About",
|
|
200
203
|
"page.privacy": "Privacy Policy",
|
|
@@ -268,6 +271,9 @@ var en = {
|
|
|
268
271
|
"ai.fail": "AI analysis failed: {error}",
|
|
269
272
|
"ai.suggestion_count": "{count} suggestion(s)",
|
|
270
273
|
"ai.suggest_enable": "Tip: use --ai flag to enable AI content quality analysis for deeper insights",
|
|
274
|
+
"ai.compliance_serious": "{count} page(s) with serious policy violations detected",
|
|
275
|
+
"ai.compliance_suspicious": "{count}/{total} pages with potentially non-compliant content",
|
|
276
|
+
"ai.compliance_ok": "No compliance issues detected by AI",
|
|
271
277
|
// Reporter
|
|
272
278
|
"report.title": "AdSense Checklist Report",
|
|
273
279
|
"report.composite_score": "Composite Score",
|
|
@@ -295,10 +301,11 @@ var en = {
|
|
|
295
301
|
"group.policy": "Policy Compliance",
|
|
296
302
|
"group.site_scale": "Site Scale",
|
|
297
303
|
"group.content_quality": "Content Quality",
|
|
304
|
+
"group.ai_value": "AI Value Analysis",
|
|
298
305
|
"group.ai_analysis": "AI Content Analysis",
|
|
299
306
|
"group.page_quality": "Page Quality",
|
|
300
307
|
"group.user_experience": "User Experience",
|
|
301
|
-
"group.
|
|
308
|
+
"group.policy_compliance": "Policy Compliance",
|
|
302
309
|
// Topic & relevance
|
|
303
310
|
"item.relevance.topic": "Topic Relevance",
|
|
304
311
|
"detector.type.tool": "Tool Site",
|
|
@@ -342,6 +349,9 @@ var zh = {
|
|
|
342
349
|
"item.ai.originality": "\u539F\u521B\u6027\u8BC4\u4F30",
|
|
343
350
|
"item.ai.compliance": "\u5408\u89C4\u6027\u8BC4\u4F30",
|
|
344
351
|
"item.ai.suggestions": "AI \u5EFA\u8BAE",
|
|
352
|
+
"item.ai.compliance_serious": "\u4E25\u91CD\u8FDD\u89C4",
|
|
353
|
+
"item.ai.compliance_suspicious": "\u53EF\u7591\u5185\u5BB9",
|
|
354
|
+
"item.ai.compliance_ok": "AI \u5408\u89C4",
|
|
345
355
|
// 必要页面名称
|
|
346
356
|
"page.about": "About",
|
|
347
357
|
"page.privacy": "\u9690\u79C1\u653F\u7B56",
|
|
@@ -415,6 +425,9 @@ var zh = {
|
|
|
415
425
|
"ai.fail": "AI \u5206\u6790\u5931\u8D25: {error}",
|
|
416
426
|
"ai.suggestion_count": "{count} \u6761\u6539\u8FDB\u5EFA\u8BAE",
|
|
417
427
|
"ai.suggest_enable": "\u63D0\u793A: \u4F7F\u7528 --ai \u53C2\u6570\u542F\u7528 AI \u5185\u5BB9\u8D28\u91CF\u5206\u6790\uFF0C\u83B7\u53D6\u66F4\u6DF1\u5165\u7684\u5BA1\u67E5\u5EFA\u8BAE",
|
|
428
|
+
"ai.compliance_serious": "\u68C0\u6D4B\u5230 {count} \u4E2A\u9875\u9762\u5B58\u5728\u4E25\u91CD\u8FDD\u89C4\u5185\u5BB9",
|
|
429
|
+
"ai.compliance_suspicious": "{count}/{total} \u4E2A\u9875\u9762\u5B58\u5728\u53EF\u7591\u4E0D\u5408\u89C4\u5185\u5BB9",
|
|
430
|
+
"ai.compliance_ok": "AI \u672A\u68C0\u6D4B\u5230\u5408\u89C4\u95EE\u9898",
|
|
418
431
|
// 报告
|
|
419
432
|
"report.title": "AdSense \u5BA1\u6838\u68C0\u67E5\u62A5\u544A",
|
|
420
433
|
"report.composite_score": "\u7EFC\u5408\u8BC4\u5206",
|
|
@@ -442,10 +455,11 @@ var zh = {
|
|
|
442
455
|
"group.policy": "\u653F\u7B56\u5408\u89C4",
|
|
443
456
|
"group.site_scale": "\u7AD9\u70B9\u89C4\u6A21",
|
|
444
457
|
"group.content_quality": "\u5185\u5BB9\u8D28\u91CF",
|
|
458
|
+
"group.ai_value": "AI \u4EF7\u503C\u5206\u6790",
|
|
445
459
|
"group.ai_analysis": "AI \u5185\u5BB9\u5206\u6790",
|
|
446
460
|
"group.page_quality": "\u9875\u9762\u8D28\u91CF",
|
|
447
461
|
"group.user_experience": "\u7528\u6237\u4F53\u9A8C",
|
|
448
|
-
"group.
|
|
462
|
+
"group.policy_compliance": "\u653F\u7B56\u5408\u89C4",
|
|
449
463
|
// 主题和相关性
|
|
450
464
|
"item.relevance.topic": "\u4E3B\u9898\u76F8\u5173\u6027",
|
|
451
465
|
"detector.type.tool": "\u5DE5\u5177\u7AD9",
|
|
@@ -472,20 +486,23 @@ function t(key, lang, vars) {
|
|
|
472
486
|
return msg;
|
|
473
487
|
}
|
|
474
488
|
|
|
475
|
-
// src/ai/
|
|
489
|
+
// src/ai/analyzer.ts
|
|
476
490
|
function getApiEndpoint() {
|
|
477
491
|
const base = process.env.AI_API_BASE || "https://api.deepseek.com";
|
|
478
492
|
return `${base.replace(/\/$/, "")}/chat/completions`;
|
|
479
493
|
}
|
|
494
|
+
function getApiKey() {
|
|
495
|
+
return process.env.AI_API_KEY;
|
|
496
|
+
}
|
|
480
497
|
function getModel() {
|
|
481
498
|
return process.env.AI_MODEL || "deepseek-chat";
|
|
482
499
|
}
|
|
483
|
-
async function callAI(prompt,
|
|
500
|
+
async function callAI(prompt, maxTokens = 4096) {
|
|
484
501
|
const response = await fetch(getApiEndpoint(), {
|
|
485
502
|
method: "POST",
|
|
486
503
|
headers: {
|
|
487
504
|
"Content-Type": "application/json",
|
|
488
|
-
Authorization: `Bearer ${
|
|
505
|
+
Authorization: `Bearer ${getApiKey()}`
|
|
489
506
|
},
|
|
490
507
|
body: JSON.stringify({
|
|
491
508
|
model: getModel(),
|
|
@@ -506,6 +523,188 @@ function extractJson(text) {
|
|
|
506
523
|
}
|
|
507
524
|
throw new Error("No JSON found in response");
|
|
508
525
|
}
|
|
526
|
+
var AI_LANG_NAMES = {
|
|
527
|
+
en: "English",
|
|
528
|
+
zh: "\u4E2D\u6587"
|
|
529
|
+
};
|
|
530
|
+
function getAiLangName(lang) {
|
|
531
|
+
return AI_LANG_NAMES[lang] ?? lang;
|
|
532
|
+
}
|
|
533
|
+
var PAGE_CHARS = 5e3;
|
|
534
|
+
var CONCURRENCY = 3;
|
|
535
|
+
async function analyzePage(page, langName, date, siteTopic) {
|
|
536
|
+
const content = page.text.slice(0, PAGE_CHARS);
|
|
537
|
+
const topicContext = siteTopic ? `
|
|
538
|
+
Site topic: ${siteTopic.topic}
|
|
539
|
+
Site type: ${siteTopic.type}
|
|
540
|
+
Site description: ${siteTopic.description}` : "";
|
|
541
|
+
const prompt = `You are a Google AdSense review expert. Analyze this page and score it on four dimensions.
|
|
542
|
+
Current date: ${date}
|
|
543
|
+
Reply language: ${langName}
|
|
544
|
+
${topicContext}
|
|
545
|
+
|
|
546
|
+
Score each dimension from 0 to 10:
|
|
547
|
+
1. value (0-10): Does the page provide real, substantive information? 10 = highly valuable, 0 = completely empty/useless.
|
|
548
|
+
Consider: depth of information, usefulness to readers, whether it helps solve a problem or answers a question.
|
|
549
|
+
2. originality (0-10): Is the content original and not scraped/AI-generated/copied? 10 = fully original, 0 = clearly scraped or auto-generated.
|
|
550
|
+
Consider: unique perspective, personal experience, not just rephrasing others' content.
|
|
551
|
+
3. relevance (0-10): How relevant is this page to the site's topic? 10 = directly on-topic, 0 = completely off-topic.
|
|
552
|
+
Also set "relevanceLabel": "relevant" | "tangential" | "off-topic".
|
|
553
|
+
4. compliance (0-10): Does the content comply with Google AdSense policies? 10 = fully compliant, 0 = serious violations.
|
|
554
|
+
Flag: adult content, gambling, drugs, violence, copyright infringement, deceptive content.
|
|
555
|
+
|
|
556
|
+
Page: ${page.url}
|
|
557
|
+
|
|
558
|
+
Content:
|
|
559
|
+
${content}
|
|
560
|
+
|
|
561
|
+
Reply in ${langName} with JSON:
|
|
562
|
+
{
|
|
563
|
+
"value": <0-10>,
|
|
564
|
+
"originality": <0-10>,
|
|
565
|
+
"relevance": <0-10>,
|
|
566
|
+
"relevanceLabel": "relevant|tangential|off-topic",
|
|
567
|
+
"compliance": <0-10>,
|
|
568
|
+
"assessment": "Brief assessment covering the key findings across all dimensions",
|
|
569
|
+
"suggestions": ["Specific actionable suggestion to improve this page"]
|
|
570
|
+
}`;
|
|
571
|
+
try {
|
|
572
|
+
const text = await callAI(prompt, 2048);
|
|
573
|
+
const result = extractJson(text);
|
|
574
|
+
const valueScore = clampScore(result.value);
|
|
575
|
+
const originalityScore = clampScore(result.originality);
|
|
576
|
+
const relevanceScore = clampScore(result.relevance);
|
|
577
|
+
const complianceScore = clampScore(result.compliance);
|
|
578
|
+
const geoMean = Math.pow(valueScore * originalityScore * relevanceScore * complianceScore, 0.25);
|
|
579
|
+
const status = geoMean >= 7 ? "pass" : geoMean >= 4 ? "warn" : "fail";
|
|
580
|
+
return {
|
|
581
|
+
url: page.url,
|
|
582
|
+
status,
|
|
583
|
+
relevance: result.relevanceLabel ?? (relevanceScore >= 7 ? "relevant" : relevanceScore >= 4 ? "tangential" : "off-topic"),
|
|
584
|
+
valueScore,
|
|
585
|
+
originalityScore,
|
|
586
|
+
relevanceScore,
|
|
587
|
+
complianceScore,
|
|
588
|
+
assessment: result.assessment ?? "",
|
|
589
|
+
suggestions: result.suggestions ?? []
|
|
590
|
+
};
|
|
591
|
+
} catch (err) {
|
|
592
|
+
return {
|
|
593
|
+
url: page.url,
|
|
594
|
+
status: "warn",
|
|
595
|
+
assessment: `Analysis failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
596
|
+
suggestions: []
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
function clampScore(v) {
|
|
601
|
+
const n = Number(v);
|
|
602
|
+
if (isNaN(n)) return 5;
|
|
603
|
+
return Math.max(0, Math.min(10, Math.round(n)));
|
|
604
|
+
}
|
|
605
|
+
async function analyzeOverall(pageAnalyses, langName, date) {
|
|
606
|
+
const summaries = pageAnalyses.map(
|
|
607
|
+
(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)}`
|
|
608
|
+
).join("\n");
|
|
609
|
+
const prompt = `You are a Google AdSense review expert. Based on per-page dimension scores below, give site-wide improvement suggestions.
|
|
610
|
+
Current date: ${date}
|
|
611
|
+
Reply language: ${langName}
|
|
612
|
+
|
|
613
|
+
Per-page results:
|
|
614
|
+
${summaries}
|
|
615
|
+
|
|
616
|
+
Based on these results, provide improvement suggestions in ${langName} with JSON:
|
|
617
|
+
{
|
|
618
|
+
"suggestions": ["Top 3 priority site-wide improvement suggestions, most impactful first"]
|
|
619
|
+
}`;
|
|
620
|
+
try {
|
|
621
|
+
const text = await callAI(prompt, 2048);
|
|
622
|
+
const result = extractJson(text);
|
|
623
|
+
return {
|
|
624
|
+
suggestions: result.suggestions ?? []
|
|
625
|
+
};
|
|
626
|
+
} catch {
|
|
627
|
+
return {
|
|
628
|
+
suggestions: []
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
async function analyzeWithAI(pages, lang = "en", apiKey, onProgress, siteTopic) {
|
|
633
|
+
const key = apiKey || getApiKey();
|
|
634
|
+
const empty = {
|
|
635
|
+
suggestions: [],
|
|
636
|
+
pageAnalyses: []
|
|
637
|
+
};
|
|
638
|
+
if (!key) return empty;
|
|
639
|
+
const langName = getAiLangName(lang);
|
|
640
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
641
|
+
try {
|
|
642
|
+
const pageAnalyses = [];
|
|
643
|
+
const progress = onProgress ?? (() => {
|
|
644
|
+
});
|
|
645
|
+
for (let i = 0; i < pages.length; i += CONCURRENCY) {
|
|
646
|
+
const batch = pages.slice(i, i + CONCURRENCY);
|
|
647
|
+
const batchNum = Math.floor(i / CONCURRENCY) + 1;
|
|
648
|
+
const totalBatches = Math.ceil(pages.length / CONCURRENCY);
|
|
649
|
+
progress(`AI: batch ${batchNum}/${totalBatches} (${batch.map((p) => {
|
|
650
|
+
try {
|
|
651
|
+
return new URL(p.url).pathname;
|
|
652
|
+
} catch {
|
|
653
|
+
return p.url;
|
|
654
|
+
}
|
|
655
|
+
}).join(", ")})`);
|
|
656
|
+
const results = await Promise.all(
|
|
657
|
+
batch.map((p) => analyzePage(p, langName, date, siteTopic))
|
|
658
|
+
);
|
|
659
|
+
pageAnalyses.push(...results);
|
|
660
|
+
}
|
|
661
|
+
progress("AI: generating overall assessment...");
|
|
662
|
+
const overall = await analyzeOverall(pageAnalyses, langName, date);
|
|
663
|
+
return {
|
|
664
|
+
...overall,
|
|
665
|
+
pageAnalyses
|
|
666
|
+
};
|
|
667
|
+
} catch (err) {
|
|
668
|
+
return {
|
|
669
|
+
...empty
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// src/ai/topic.ts
|
|
675
|
+
function getApiEndpoint2() {
|
|
676
|
+
const base = process.env.AI_API_BASE || "https://api.deepseek.com";
|
|
677
|
+
return `${base.replace(/\/$/, "")}/chat/completions`;
|
|
678
|
+
}
|
|
679
|
+
function getModel2() {
|
|
680
|
+
return process.env.AI_MODEL || "deepseek-chat";
|
|
681
|
+
}
|
|
682
|
+
async function callAI2(prompt, apiKey, maxTokens = 1024) {
|
|
683
|
+
const response = await fetch(getApiEndpoint2(), {
|
|
684
|
+
method: "POST",
|
|
685
|
+
headers: {
|
|
686
|
+
"Content-Type": "application/json",
|
|
687
|
+
Authorization: `Bearer ${apiKey}`
|
|
688
|
+
},
|
|
689
|
+
body: JSON.stringify({
|
|
690
|
+
model: getModel2(),
|
|
691
|
+
max_tokens: maxTokens,
|
|
692
|
+
messages: [{ role: "user", content: prompt }]
|
|
693
|
+
})
|
|
694
|
+
});
|
|
695
|
+
if (!response.ok) {
|
|
696
|
+
throw new Error(`AI API error: ${response.status} ${response.statusText}`);
|
|
697
|
+
}
|
|
698
|
+
const data = await response.json();
|
|
699
|
+
return data.choices?.[0]?.message?.content ?? "";
|
|
700
|
+
}
|
|
701
|
+
function extractJson2(text) {
|
|
702
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
703
|
+
if (jsonMatch) {
|
|
704
|
+
return JSON.parse(jsonMatch[0]);
|
|
705
|
+
}
|
|
706
|
+
throw new Error("No JSON found in response");
|
|
707
|
+
}
|
|
509
708
|
var VALID_TYPES = ["content", "tool", "game"];
|
|
510
709
|
async function analyzeSiteTopic(homepage, lang = "en", apiKey) {
|
|
511
710
|
const langName = lang === "zh" ? "\u4E2D\u6587" : "English";
|
|
@@ -534,8 +733,8 @@ Reply in ${langName} with JSON:
|
|
|
534
733
|
"reasoning": "Brief explanation of why this type was chosen"
|
|
535
734
|
}`;
|
|
536
735
|
try {
|
|
537
|
-
const text = await
|
|
538
|
-
const result =
|
|
736
|
+
const text = await callAI2(prompt, apiKey);
|
|
737
|
+
const result = extractJson2(text);
|
|
539
738
|
const type = VALID_TYPES.includes(result.type) ? result.type : "unsupported";
|
|
540
739
|
return {
|
|
541
740
|
type,
|
|
@@ -643,6 +842,145 @@ function detectSiteType(pagesSignals, navText, manualType) {
|
|
|
643
842
|
return { type, confidence, signals: { iframeRatio, canvasRatio, articleRatio, navGameKeywords } };
|
|
644
843
|
}
|
|
645
844
|
|
|
845
|
+
// src/scorer.ts
|
|
846
|
+
function scoreFromChecks(checks) {
|
|
847
|
+
if (checks.length === 0) return 100;
|
|
848
|
+
let totalWeight = 0;
|
|
849
|
+
let earnedWeight = 0;
|
|
850
|
+
for (const c of checks) {
|
|
851
|
+
totalWeight += c.weight;
|
|
852
|
+
if (c.status === "pass") earnedWeight += c.weight;
|
|
853
|
+
else if (c.status === "warn") earnedWeight += c.weight * 0.4;
|
|
854
|
+
}
|
|
855
|
+
return totalWeight > 0 ? Math.round(earnedWeight / totalWeight * 100) : 100;
|
|
856
|
+
}
|
|
857
|
+
function scorePage(pageType, contentChars, contentRatio, issues, siteType, aiStatus) {
|
|
858
|
+
const checks = [];
|
|
859
|
+
if (pageType === "homepage") {
|
|
860
|
+
checks.push({ label: "Content depth", status: contentChars >= 500 ? "pass" : contentChars >= 200 ? "warn" : "fail", weight: 3 });
|
|
861
|
+
checks.push({ label: "Content ratio", status: contentRatio >= 40 ? "pass" : contentRatio >= 20 ? "warn" : "fail", weight: 2 });
|
|
862
|
+
} else if (pageType === "content") {
|
|
863
|
+
checks.push({ label: "Content depth", status: contentChars >= 500 ? "pass" : contentChars >= 300 ? "warn" : "fail", weight: 4 });
|
|
864
|
+
checks.push({ label: "Content ratio", status: contentRatio >= 40 ? "pass" : contentRatio >= 20 ? "warn" : "fail", weight: 2 });
|
|
865
|
+
if (issues.length > 0) checks.push({ label: "Issues", status: "warn", weight: 1 });
|
|
866
|
+
} else if (pageType === "game_detail") {
|
|
867
|
+
if (siteType === "game") {
|
|
868
|
+
checks.push({ label: "Game description", status: contentChars >= 100 ? "pass" : "warn", weight: 3 });
|
|
869
|
+
} else {
|
|
870
|
+
checks.push({ label: "Content depth", status: contentChars >= 300 ? "pass" : contentChars >= 100 ? "warn" : "fail", weight: 3 });
|
|
871
|
+
}
|
|
872
|
+
checks.push({ label: "Content ratio", status: contentRatio >= 30 ? "pass" : contentRatio >= 15 ? "warn" : "fail", weight: 2 });
|
|
873
|
+
} else if (pageType === "required") {
|
|
874
|
+
checks.push({ label: "Exists", status: contentChars > 0 ? "pass" : "fail", weight: 3 });
|
|
875
|
+
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
|
+
} else if (pageType === "utility") {
|
|
879
|
+
checks.push({ label: "Functional", status: contentChars > 0 ? "pass" : "warn", weight: 1 });
|
|
880
|
+
} else {
|
|
881
|
+
checks.push({ label: "Content", status: contentChars >= 300 ? "pass" : contentChars >= 100 ? "warn" : "fail", weight: 2 });
|
|
882
|
+
}
|
|
883
|
+
let score = scoreFromChecks(checks);
|
|
884
|
+
if (aiStatus === "fail") score = 0;
|
|
885
|
+
else if (aiStatus === "warn") score = Math.min(score, 70);
|
|
886
|
+
return { score, checks };
|
|
887
|
+
}
|
|
888
|
+
var AI_PAGE_TYPE_WEIGHTS = {
|
|
889
|
+
homepage: 1.5,
|
|
890
|
+
content: 1,
|
|
891
|
+
game_detail: 1,
|
|
892
|
+
unknown: 0.5,
|
|
893
|
+
listing: 0.1,
|
|
894
|
+
required: 0.2,
|
|
895
|
+
utility: 0.1
|
|
896
|
+
};
|
|
897
|
+
function computePageAiScore(analysis) {
|
|
898
|
+
const v = analysis.valueScore ?? 5;
|
|
899
|
+
const o = analysis.originalityScore ?? 5;
|
|
900
|
+
const r = analysis.relevanceScore ?? 5;
|
|
901
|
+
const c = analysis.complianceScore ?? 5;
|
|
902
|
+
const geoMean = Math.pow(v * o * r * c, 0.25);
|
|
903
|
+
return Math.round(geoMean * 10);
|
|
904
|
+
}
|
|
905
|
+
function computeSiteAiScore(pageAiScores) {
|
|
906
|
+
if (pageAiScores.length === 0) return 0;
|
|
907
|
+
let weightedSum = 0;
|
|
908
|
+
let totalWeight = 0;
|
|
909
|
+
for (const p of pageAiScores) {
|
|
910
|
+
const w = AI_PAGE_TYPE_WEIGHTS[p.pageType] ?? 0.5;
|
|
911
|
+
weightedSum += p.score * w;
|
|
912
|
+
totalWeight += w;
|
|
913
|
+
}
|
|
914
|
+
return totalWeight > 0 ? Math.round(weightedSum / totalWeight) : 0;
|
|
915
|
+
}
|
|
916
|
+
function statusToScore(status) {
|
|
917
|
+
if (status === "pass") return 100;
|
|
918
|
+
if (status === "warn") return 40;
|
|
919
|
+
if (status === "skip") return 0;
|
|
920
|
+
return 0;
|
|
921
|
+
}
|
|
922
|
+
function scoreCategory(category) {
|
|
923
|
+
if (category.items.length === 0) return { name: category.name, score: 0, maxScore: 0 };
|
|
924
|
+
const total = category.items.length * 100;
|
|
925
|
+
const earned = category.items.reduce((sum, item) => sum + statusToScore(item.status), 0);
|
|
926
|
+
return { name: category.name, score: earned, maxScore: total };
|
|
927
|
+
}
|
|
928
|
+
function categoryPassRate(category) {
|
|
929
|
+
if (category.items.length === 0) return 100;
|
|
930
|
+
const cs = scoreCategory(category);
|
|
931
|
+
return cs.maxScore > 0 ? Math.round(cs.score / cs.maxScore * 100) : 100;
|
|
932
|
+
}
|
|
933
|
+
var SOFT_CAT_WEIGHTS = {
|
|
934
|
+
aiValue: 0.45,
|
|
935
|
+
contentQuality: 0.35,
|
|
936
|
+
userExperience: 0.1,
|
|
937
|
+
pageQuality: 0.1
|
|
938
|
+
};
|
|
939
|
+
var HARD_COMPOSITE_WEIGHT = 0.4;
|
|
940
|
+
var SOFT_COMPOSITE_WEIGHT = 0.6;
|
|
941
|
+
function computeCompositeScore(pageScores, hardCategories, softCategories, aiAnalyses) {
|
|
942
|
+
const hardItems = hardCategories.flatMap((c) => c.items);
|
|
943
|
+
const hardPass = hardItems.filter((i) => i.status === "pass").length;
|
|
944
|
+
const hardFail = hardItems.filter((i) => i.status === "fail").length;
|
|
945
|
+
const hardWarn = hardItems.filter((i) => i.status === "warn").length;
|
|
946
|
+
const hardTotal = hardItems.length;
|
|
947
|
+
const hardPassRate = hardTotal > 0 ? hardPass / hardTotal * 100 : 100;
|
|
948
|
+
let hardStatus = "ready";
|
|
949
|
+
if (hardFail > 0) hardStatus = "fail";
|
|
950
|
+
else if (hardWarn > 0) hardStatus = "warn";
|
|
951
|
+
let siteAiScore = 0;
|
|
952
|
+
if (aiAnalyses && aiAnalyses.length > 0) {
|
|
953
|
+
siteAiScore = computeSiteAiScore(
|
|
954
|
+
aiAnalyses.map((a, i) => ({
|
|
955
|
+
pageType: pageScores[i]?.pageType ?? "unknown",
|
|
956
|
+
score: computePageAiScore(a)
|
|
957
|
+
}))
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
const aiCat = softCategories.find((c) => c.name.includes("AI") || c.name.includes("ai"));
|
|
961
|
+
const contentCat = softCategories.find((c) => c.name.includes("\u5185\u5BB9\u8D28\u91CF") || c.name.includes("Content"));
|
|
962
|
+
const uxCat = softCategories.find((c) => c.name.includes("\u4F53\u9A8C") || c.name.includes("UX") || c.name.includes("User"));
|
|
963
|
+
const aiValue = siteAiScore > 0 ? siteAiScore : aiCat ? categoryPassRate(aiCat) : 100;
|
|
964
|
+
const contentQuality = contentCat ? categoryPassRate(contentCat) : 100;
|
|
965
|
+
const userExperience = uxCat ? categoryPassRate(uxCat) : 100;
|
|
966
|
+
const pageQuality = pageScores.length > 0 ? Math.round(pageScores.reduce((s, p) => s + p.score, 0) / pageScores.length) : 100;
|
|
967
|
+
const softScore = Math.round(
|
|
968
|
+
aiValue * SOFT_CAT_WEIGHTS.aiValue + contentQuality * SOFT_CAT_WEIGHTS.contentQuality + userExperience * SOFT_CAT_WEIGHTS.userExperience + pageQuality * SOFT_CAT_WEIGHTS.pageQuality
|
|
969
|
+
);
|
|
970
|
+
const allItems = [...hardItems, ...softCategories.flatMap((c) => c.items)];
|
|
971
|
+
const totalWarn = allItems.filter((i) => i.status === "warn").length;
|
|
972
|
+
const totalAll = allItems.length;
|
|
973
|
+
const warningRatio = totalAll > 0 ? totalWarn / totalAll : 0;
|
|
974
|
+
const warningPenalty = warningRatio > 0.15 ? Math.round((warningRatio - 0.15) * 100) : 0;
|
|
975
|
+
const base = hardPassRate * HARD_COMPOSITE_WEIGHT + softScore * SOFT_COMPOSITE_WEIGHT;
|
|
976
|
+
const compositeScore = Math.min(100, Math.max(0, Math.round(base - warningPenalty)));
|
|
977
|
+
const categoryScores = [];
|
|
978
|
+
for (const cat of [...hardCategories, ...softCategories]) {
|
|
979
|
+
categoryScores.push(scoreCategory(cat));
|
|
980
|
+
}
|
|
981
|
+
return { compositeScore, categoryScores, hardStatus, softScore, warningRatio, warningPenalty, siteAiScore };
|
|
982
|
+
}
|
|
983
|
+
|
|
646
984
|
// src/checks/content.ts
|
|
647
985
|
function extractMainContent(text, allPageTexts) {
|
|
648
986
|
const paragraphs = text.split(/\n\s*\n/).map((p) => p.trim()).filter(Boolean);
|
|
@@ -993,185 +1331,6 @@ function checkPolicyCompliance(pages, lang) {
|
|
|
993
1331
|
return { name: t("cat.policy", lang), items };
|
|
994
1332
|
}
|
|
995
1333
|
|
|
996
|
-
// src/ai/analyzer.ts
|
|
997
|
-
function getApiEndpoint2() {
|
|
998
|
-
const base = process.env.AI_API_BASE || "https://api.deepseek.com";
|
|
999
|
-
return `${base.replace(/\/$/, "")}/chat/completions`;
|
|
1000
|
-
}
|
|
1001
|
-
function getApiKey() {
|
|
1002
|
-
return process.env.AI_API_KEY;
|
|
1003
|
-
}
|
|
1004
|
-
function getModel2() {
|
|
1005
|
-
return process.env.AI_MODEL || "deepseek-chat";
|
|
1006
|
-
}
|
|
1007
|
-
async function callAI2(prompt, maxTokens = 4096) {
|
|
1008
|
-
const response = await fetch(getApiEndpoint2(), {
|
|
1009
|
-
method: "POST",
|
|
1010
|
-
headers: {
|
|
1011
|
-
"Content-Type": "application/json",
|
|
1012
|
-
Authorization: `Bearer ${getApiKey()}`
|
|
1013
|
-
},
|
|
1014
|
-
body: JSON.stringify({
|
|
1015
|
-
model: getModel2(),
|
|
1016
|
-
max_tokens: maxTokens,
|
|
1017
|
-
messages: [{ role: "user", content: prompt }]
|
|
1018
|
-
})
|
|
1019
|
-
});
|
|
1020
|
-
if (!response.ok) {
|
|
1021
|
-
throw new Error(`AI API error: ${response.status} ${response.statusText}`);
|
|
1022
|
-
}
|
|
1023
|
-
const data = await response.json();
|
|
1024
|
-
return data.choices?.[0]?.message?.content ?? "";
|
|
1025
|
-
}
|
|
1026
|
-
function extractJson2(text) {
|
|
1027
|
-
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
1028
|
-
if (jsonMatch) {
|
|
1029
|
-
return JSON.parse(jsonMatch[0]);
|
|
1030
|
-
}
|
|
1031
|
-
throw new Error("No JSON found in response");
|
|
1032
|
-
}
|
|
1033
|
-
var AI_LANG_NAMES = {
|
|
1034
|
-
en: "English",
|
|
1035
|
-
zh: "\u4E2D\u6587"
|
|
1036
|
-
};
|
|
1037
|
-
function getAiLangName(lang) {
|
|
1038
|
-
return AI_LANG_NAMES[lang] ?? lang;
|
|
1039
|
-
}
|
|
1040
|
-
var PAGE_CHARS = 5e3;
|
|
1041
|
-
var CONCURRENCY = 3;
|
|
1042
|
-
async function analyzePage(page, langName, date, siteTopic) {
|
|
1043
|
-
const content = page.text.slice(0, PAGE_CHARS);
|
|
1044
|
-
const topicContext = siteTopic ? `
|
|
1045
|
-
Site topic: ${siteTopic.topic}
|
|
1046
|
-
Site type: ${siteTopic.type}
|
|
1047
|
-
Site description: ${siteTopic.description}
|
|
1048
|
-
|
|
1049
|
-
Evaluate whether this page is relevant to the site's topic and provides value to users interested in "${siteTopic.topic}".` : "";
|
|
1050
|
-
const prompt = `You are a Google AdSense review expert. Analyze this page for "low value content" issues.
|
|
1051
|
-
Current date: ${date}
|
|
1052
|
-
Reply language: ${langName}
|
|
1053
|
-
${topicContext}
|
|
1054
|
-
|
|
1055
|
-
Low value content signs:
|
|
1056
|
-
- Thin content lacking substantial information
|
|
1057
|
-
- Machine-generated or scraped content
|
|
1058
|
-
- No unique value for users
|
|
1059
|
-
- Padded/repetitive content to fill space
|
|
1060
|
-
- Template-like structure with minimal real content
|
|
1061
|
-
|
|
1062
|
-
Page: ${page.url}
|
|
1063
|
-
|
|
1064
|
-
Content:
|
|
1065
|
-
${content}
|
|
1066
|
-
|
|
1067
|
-
Reply in ${langName} with JSON:
|
|
1068
|
-
{
|
|
1069
|
-
"status": "pass|warn|fail",
|
|
1070
|
-
"relevance": "relevant|tangential|off-topic",
|
|
1071
|
-
"assessment": "Detailed assessment: content depth, originality, user value, specific issues found",
|
|
1072
|
-
"suggestions": ["Specific actionable suggestion to improve this page"]
|
|
1073
|
-
}`;
|
|
1074
|
-
try {
|
|
1075
|
-
const text = await callAI2(prompt, 2048);
|
|
1076
|
-
const result = extractJson2(text);
|
|
1077
|
-
return {
|
|
1078
|
-
url: page.url,
|
|
1079
|
-
status: result.status ?? "warn",
|
|
1080
|
-
relevance: result.relevance,
|
|
1081
|
-
assessment: result.assessment ?? "",
|
|
1082
|
-
suggestions: result.suggestions ?? []
|
|
1083
|
-
};
|
|
1084
|
-
} catch (err) {
|
|
1085
|
-
return {
|
|
1086
|
-
url: page.url,
|
|
1087
|
-
status: "warn",
|
|
1088
|
-
assessment: `Analysis failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1089
|
-
suggestions: []
|
|
1090
|
-
};
|
|
1091
|
-
}
|
|
1092
|
-
}
|
|
1093
|
-
async function analyzeOverall(pageAnalyses, langName, date) {
|
|
1094
|
-
const summaries = pageAnalyses.map(
|
|
1095
|
-
(p, i) => `Page ${i + 1} (${p.url}): [${p.status}] ${p.assessment.slice(0, 200)}`
|
|
1096
|
-
).join("\n");
|
|
1097
|
-
const prompt = `You are a Google AdSense review expert. Based on per-page analyses below, give an overall site assessment.
|
|
1098
|
-
Current date: ${date}
|
|
1099
|
-
Reply language: ${langName}
|
|
1100
|
-
|
|
1101
|
-
Per-page results:
|
|
1102
|
-
${summaries}
|
|
1103
|
-
|
|
1104
|
-
Based on these results, provide an overall assessment in ${langName} with JSON:
|
|
1105
|
-
{
|
|
1106
|
-
"contentQuality": { "status": "pass|warn|fail", "detail": "Overall content value assessment considering all pages" },
|
|
1107
|
-
"originality": { "status": "pass|warn|fail", "detail": "Overall originality assessment across the site" },
|
|
1108
|
-
"compliance": { "status": "pass|warn|fail", "detail": "Overall AdSense policy compliance" },
|
|
1109
|
-
"suggestions": ["Top priority site-wide improvement suggestion"]
|
|
1110
|
-
}`;
|
|
1111
|
-
try {
|
|
1112
|
-
const text = await callAI2(prompt, 2048);
|
|
1113
|
-
const result = extractJson2(text);
|
|
1114
|
-
return {
|
|
1115
|
-
contentQuality: result.contentQuality ?? { status: "warn", detail: "Parse error" },
|
|
1116
|
-
originality: result.originality ?? { status: "warn", detail: "Parse error" },
|
|
1117
|
-
compliance: result.compliance ?? { status: "warn", detail: "Parse error" },
|
|
1118
|
-
suggestions: result.suggestions ?? []
|
|
1119
|
-
};
|
|
1120
|
-
} catch {
|
|
1121
|
-
return {
|
|
1122
|
-
contentQuality: { status: "warn", detail: "Overall analysis failed" },
|
|
1123
|
-
originality: { status: "warn", detail: "N/A" },
|
|
1124
|
-
compliance: { status: "warn", detail: "N/A" },
|
|
1125
|
-
suggestions: []
|
|
1126
|
-
};
|
|
1127
|
-
}
|
|
1128
|
-
}
|
|
1129
|
-
async function analyzeWithAI(pages, lang = "en", apiKey, onProgress, siteTopic) {
|
|
1130
|
-
const key = apiKey || getApiKey();
|
|
1131
|
-
const empty = {
|
|
1132
|
-
contentQuality: { status: "skip", detail: t("ai.skip", lang) },
|
|
1133
|
-
originality: { status: "skip", detail: "N/A" },
|
|
1134
|
-
compliance: { status: "skip", detail: "N/A" },
|
|
1135
|
-
suggestions: [],
|
|
1136
|
-
pageAnalyses: []
|
|
1137
|
-
};
|
|
1138
|
-
if (!key) return empty;
|
|
1139
|
-
const langName = getAiLangName(lang);
|
|
1140
|
-
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1141
|
-
try {
|
|
1142
|
-
const pageAnalyses = [];
|
|
1143
|
-
const progress = onProgress ?? (() => {
|
|
1144
|
-
});
|
|
1145
|
-
for (let i = 0; i < pages.length; i += CONCURRENCY) {
|
|
1146
|
-
const batch = pages.slice(i, i + CONCURRENCY);
|
|
1147
|
-
const batchNum = Math.floor(i / CONCURRENCY) + 1;
|
|
1148
|
-
const totalBatches = Math.ceil(pages.length / CONCURRENCY);
|
|
1149
|
-
progress(`AI: batch ${batchNum}/${totalBatches} (${batch.map((p) => {
|
|
1150
|
-
try {
|
|
1151
|
-
return new URL(p.url).pathname;
|
|
1152
|
-
} catch {
|
|
1153
|
-
return p.url;
|
|
1154
|
-
}
|
|
1155
|
-
}).join(", ")})`);
|
|
1156
|
-
const results = await Promise.all(
|
|
1157
|
-
batch.map((p) => analyzePage(p, langName, date, siteTopic))
|
|
1158
|
-
);
|
|
1159
|
-
pageAnalyses.push(...results);
|
|
1160
|
-
}
|
|
1161
|
-
progress("AI: generating overall assessment...");
|
|
1162
|
-
const overall = await analyzeOverall(pageAnalyses, langName, date);
|
|
1163
|
-
return {
|
|
1164
|
-
...overall,
|
|
1165
|
-
pageAnalyses
|
|
1166
|
-
};
|
|
1167
|
-
} catch (err) {
|
|
1168
|
-
return {
|
|
1169
|
-
...empty,
|
|
1170
|
-
contentQuality: { status: "warn", detail: t("ai.fail", lang, { error: err instanceof Error ? err.message : String(err) }) }
|
|
1171
|
-
};
|
|
1172
|
-
}
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
1334
|
// src/classifier.ts
|
|
1176
1335
|
var REQUIRED_PATTERNS = [/\/about/i, /\/privacy/i, /\/contact/i, /\/terms/i, /\/legal/i];
|
|
1177
1336
|
var CONTENT_PREFIXES = ["/blog/", "/news/", "/guides/", "/articles/", "/posts/", "/tutorials/", "/wiki/"];
|
|
@@ -1226,127 +1385,6 @@ function classifyPage(url) {
|
|
|
1226
1385
|
return "unknown";
|
|
1227
1386
|
}
|
|
1228
1387
|
|
|
1229
|
-
// src/scorer.ts
|
|
1230
|
-
function scoreFromChecks(checks) {
|
|
1231
|
-
if (checks.length === 0) return 100;
|
|
1232
|
-
let totalWeight = 0;
|
|
1233
|
-
let earnedWeight = 0;
|
|
1234
|
-
for (const c of checks) {
|
|
1235
|
-
totalWeight += c.weight;
|
|
1236
|
-
if (c.status === "pass") earnedWeight += c.weight;
|
|
1237
|
-
else if (c.status === "warn") earnedWeight += c.weight * 0.4;
|
|
1238
|
-
}
|
|
1239
|
-
return totalWeight > 0 ? Math.round(earnedWeight / totalWeight * 100) : 100;
|
|
1240
|
-
}
|
|
1241
|
-
function scorePage(pageType, contentChars, contentRatio, issues, siteType, aiStatus) {
|
|
1242
|
-
const checks = [];
|
|
1243
|
-
if (pageType === "homepage") {
|
|
1244
|
-
checks.push({ label: "Content depth", status: contentChars >= 500 ? "pass" : contentChars >= 200 ? "warn" : "fail", weight: 3 });
|
|
1245
|
-
checks.push({ label: "Content ratio", status: contentRatio >= 40 ? "pass" : contentRatio >= 20 ? "warn" : "fail", weight: 2 });
|
|
1246
|
-
} else if (pageType === "content") {
|
|
1247
|
-
checks.push({ label: "Content depth", status: contentChars >= 500 ? "pass" : contentChars >= 300 ? "warn" : "fail", weight: 4 });
|
|
1248
|
-
checks.push({ label: "Content ratio", status: contentRatio >= 40 ? "pass" : contentRatio >= 20 ? "warn" : "fail", weight: 2 });
|
|
1249
|
-
if (issues.length > 0) checks.push({ label: "Issues", status: "warn", weight: 1 });
|
|
1250
|
-
} else if (pageType === "game_detail") {
|
|
1251
|
-
if (siteType === "game") {
|
|
1252
|
-
checks.push({ label: "Game description", status: contentChars >= 100 ? "pass" : "warn", weight: 3 });
|
|
1253
|
-
} else {
|
|
1254
|
-
checks.push({ label: "Content depth", status: contentChars >= 300 ? "pass" : contentChars >= 100 ? "warn" : "fail", weight: 3 });
|
|
1255
|
-
}
|
|
1256
|
-
checks.push({ label: "Content ratio", status: contentRatio >= 30 ? "pass" : contentRatio >= 15 ? "warn" : "fail", weight: 2 });
|
|
1257
|
-
} else if (pageType === "required") {
|
|
1258
|
-
checks.push({ label: "Exists", status: contentChars > 0 ? "pass" : "fail", weight: 3 });
|
|
1259
|
-
checks.push({ label: "Content depth", status: contentChars >= 300 ? "pass" : contentChars >= 100 ? "warn" : "fail", weight: 2 });
|
|
1260
|
-
} else if (pageType === "listing") {
|
|
1261
|
-
checks.push({ label: "Content", status: contentChars >= 200 ? "pass" : contentChars >= 50 ? "warn" : "fail", weight: 2 });
|
|
1262
|
-
} else if (pageType === "utility") {
|
|
1263
|
-
checks.push({ label: "Functional", status: contentChars > 0 ? "pass" : "warn", weight: 1 });
|
|
1264
|
-
} else {
|
|
1265
|
-
checks.push({ label: "Content", status: contentChars >= 300 ? "pass" : contentChars >= 100 ? "warn" : "fail", weight: 2 });
|
|
1266
|
-
}
|
|
1267
|
-
let score = scoreFromChecks(checks);
|
|
1268
|
-
if (aiStatus === "fail") score = 0;
|
|
1269
|
-
else if (aiStatus === "warn") score = Math.min(score, 70);
|
|
1270
|
-
return { score, checks };
|
|
1271
|
-
}
|
|
1272
|
-
function statusToScore(status) {
|
|
1273
|
-
if (status === "pass") return 100;
|
|
1274
|
-
if (status === "warn") return 40;
|
|
1275
|
-
if (status === "skip") return 0;
|
|
1276
|
-
return 0;
|
|
1277
|
-
}
|
|
1278
|
-
function scoreCategory(category) {
|
|
1279
|
-
if (category.items.length === 0) return { name: category.name, score: 0, maxScore: 0 };
|
|
1280
|
-
const total = category.items.length * 100;
|
|
1281
|
-
const earned = category.items.reduce((sum, item) => sum + statusToScore(item.status), 0);
|
|
1282
|
-
return { name: category.name, score: earned, maxScore: total };
|
|
1283
|
-
}
|
|
1284
|
-
var SOFT_WEIGHTS = {
|
|
1285
|
-
pageQuality: 0.25,
|
|
1286
|
-
// per-page scores (AI-adjusted)
|
|
1287
|
-
aiAnalysis: 0.35,
|
|
1288
|
-
// AI content analysis
|
|
1289
|
-
contentQuality: 0.2,
|
|
1290
|
-
// mechanical content checks
|
|
1291
|
-
userExperience: 0.2
|
|
1292
|
-
// font, popup checks
|
|
1293
|
-
};
|
|
1294
|
-
var HARD_COMPOSITE_WEIGHT = 0.4;
|
|
1295
|
-
var SOFT_COMPOSITE_WEIGHT = 0.6;
|
|
1296
|
-
function computeCompositeScore(pageScores, hardCategories, softCategories) {
|
|
1297
|
-
const hardItems = hardCategories.flatMap((c) => c.items);
|
|
1298
|
-
const hardPass = hardItems.filter((i) => i.status === "pass").length;
|
|
1299
|
-
const hardFail = hardItems.filter((i) => i.status === "fail").length;
|
|
1300
|
-
const hardWarn = hardItems.filter((i) => i.status === "warn").length;
|
|
1301
|
-
const hardTotal = hardItems.length;
|
|
1302
|
-
const hardPassRate = hardTotal > 0 ? hardPass / hardTotal * 100 : 100;
|
|
1303
|
-
let hardStatus = "ready";
|
|
1304
|
-
if (hardFail > 0) hardStatus = "fail";
|
|
1305
|
-
else if (hardWarn > 0) hardStatus = "warn";
|
|
1306
|
-
const allPages = pageScores.length;
|
|
1307
|
-
const ai = softCategories.find((c) => c.name.includes("AI") || c.name.includes("ai"));
|
|
1308
|
-
let pageQuality;
|
|
1309
|
-
if (allPages > 0) {
|
|
1310
|
-
const avgPageScore = pageScores.reduce((s, p) => s + p.score, 0) / allPages;
|
|
1311
|
-
pageQuality = avgPageScore;
|
|
1312
|
-
} else {
|
|
1313
|
-
pageQuality = 100;
|
|
1314
|
-
}
|
|
1315
|
-
let aiScore = 100;
|
|
1316
|
-
if (ai && ai.items.length > 0) {
|
|
1317
|
-
aiScore = ai.items.reduce((sum, item) => sum + statusToScore(item.status), 0) / ai.items.length;
|
|
1318
|
-
}
|
|
1319
|
-
const contentCats = softCategories.filter((c) => c !== ai);
|
|
1320
|
-
let contentScore = 100;
|
|
1321
|
-
if (contentCats.length > 0) {
|
|
1322
|
-
let totalEarned = 0;
|
|
1323
|
-
let totalItems = 0;
|
|
1324
|
-
for (const cat of contentCats) {
|
|
1325
|
-
for (const item of cat.items) {
|
|
1326
|
-
totalEarned += statusToScore(item.status);
|
|
1327
|
-
totalItems++;
|
|
1328
|
-
}
|
|
1329
|
-
}
|
|
1330
|
-
contentScore = totalItems > 0 ? totalEarned / totalItems : 100;
|
|
1331
|
-
}
|
|
1332
|
-
let uxScore = 100;
|
|
1333
|
-
const softScore = Math.round(
|
|
1334
|
-
pageQuality * SOFT_WEIGHTS.pageQuality + aiScore * SOFT_WEIGHTS.aiAnalysis + contentScore * SOFT_WEIGHTS.contentQuality + uxScore * SOFT_WEIGHTS.userExperience
|
|
1335
|
-
);
|
|
1336
|
-
const allItems = [...hardItems, ...softCategories.flatMap((c) => c.items)];
|
|
1337
|
-
const totalWarn = allItems.filter((i) => i.status === "warn").length;
|
|
1338
|
-
const totalAll = allItems.length;
|
|
1339
|
-
const warningRatio = totalAll > 0 ? totalWarn / totalAll : 0;
|
|
1340
|
-
const warningPenalty = warningRatio > 0.15 ? Math.round((warningRatio - 0.15) * 100) : 0;
|
|
1341
|
-
const base = hardPassRate * HARD_COMPOSITE_WEIGHT + softScore * SOFT_COMPOSITE_WEIGHT;
|
|
1342
|
-
const compositeScore = Math.min(100, Math.max(0, Math.round(base - warningPenalty)));
|
|
1343
|
-
const categoryScores = [];
|
|
1344
|
-
for (const cat of [...hardCategories, ...softCategories]) {
|
|
1345
|
-
categoryScores.push(scoreCategory(cat));
|
|
1346
|
-
}
|
|
1347
|
-
return { compositeScore, categoryScores, hardStatus, softScore, warningRatio, warningPenalty };
|
|
1348
|
-
}
|
|
1349
|
-
|
|
1350
1388
|
// src/checker.ts
|
|
1351
1389
|
function extractMainContent2(text, allPageTexts) {
|
|
1352
1390
|
const paragraphs = text.split(/\n\s*\n/).map((p) => p.trim()).filter(Boolean);
|
|
@@ -1623,40 +1661,54 @@ async function check(options) {
|
|
|
1623
1661
|
progress(`AI analysis: ${uniquePages.length} pages...`);
|
|
1624
1662
|
const aiResult = await analyzeWithAI(uniquePages, lang, apiKey, progress, siteTopic);
|
|
1625
1663
|
pageAnalyses = aiResult.pageAnalyses;
|
|
1626
|
-
const aiItems = [
|
|
1627
|
-
{ name: t("item.ai.quality", lang), status: aiResult.contentQuality.status, message: aiResult.contentQuality.detail.slice(0, 200) },
|
|
1628
|
-
{ name: t("item.ai.originality", lang), status: aiResult.originality.status, message: aiResult.originality.detail.slice(0, 200) },
|
|
1629
|
-
{ name: t("item.ai.compliance", lang), status: aiResult.compliance.status, message: aiResult.compliance.detail.slice(0, 200) }
|
|
1630
|
-
];
|
|
1664
|
+
const aiItems = [];
|
|
1631
1665
|
if (aiResult.suggestions.length > 0) {
|
|
1632
1666
|
aiItems.push({ name: t("item.ai.suggestions", lang), status: "warn", message: t("ai.suggestion_count", lang, { count: aiResult.suggestions.length }), detail: aiResult.suggestions.join("; ") });
|
|
1633
1667
|
}
|
|
1634
|
-
allCategories.push({ name: t("group.
|
|
1668
|
+
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) => {
|
|
1671
|
+
const c = a.complianceScore ?? 5;
|
|
1672
|
+
return c > 2 && c <= 5;
|
|
1673
|
+
});
|
|
1674
|
+
const complianceItems = [];
|
|
1675
|
+
if (seriousViolations.length > 0) {
|
|
1676
|
+
complianceItems.push({
|
|
1677
|
+
name: t("item.ai.compliance_serious", lang),
|
|
1678
|
+
status: "fail",
|
|
1679
|
+
message: t("ai.compliance_serious", lang, { count: seriousViolations.length }),
|
|
1680
|
+
detail: seriousViolations.map((a) => new URL(a.url).pathname).join("; ")
|
|
1681
|
+
});
|
|
1682
|
+
} else if (suspiciousPages.length > pageAnalyses.length * 0.2) {
|
|
1683
|
+
complianceItems.push({
|
|
1684
|
+
name: t("item.ai.compliance_suspicious", lang),
|
|
1685
|
+
status: "warn",
|
|
1686
|
+
message: t("ai.compliance_suspicious", lang, { count: suspiciousPages.length, total: pageAnalyses.length })
|
|
1687
|
+
});
|
|
1688
|
+
} else {
|
|
1689
|
+
complianceItems.push({
|
|
1690
|
+
name: t("item.ai.compliance_ok", lang),
|
|
1691
|
+
status: "pass",
|
|
1692
|
+
message: t("ai.compliance_ok", lang)
|
|
1693
|
+
});
|
|
1694
|
+
}
|
|
1695
|
+
allCategories.push({ name: t("group.policy_compliance", lang), items: complianceItems, group: "hard" });
|
|
1635
1696
|
} catch (err) {
|
|
1636
|
-
allCategories.push({ name: t("group.
|
|
1697
|
+
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" });
|
|
1637
1698
|
}
|
|
1638
1699
|
}
|
|
1639
1700
|
const pageDetails = buildPageDetails(uniquePages, pageAnalyses, siteType);
|
|
1640
|
-
if (pageAnalyses.length > 0) {
|
|
1641
|
-
const withRelevance = pageAnalyses.filter((a) => a.relevance);
|
|
1642
|
-
if (withRelevance.length > 0) {
|
|
1643
|
-
const offTopic = withRelevance.filter((a) => a.relevance === "off-topic").length;
|
|
1644
|
-
const tangential = withRelevance.filter((a) => a.relevance === "tangential").length;
|
|
1645
|
-
const offTopicRatio = offTopic / withRelevance.length;
|
|
1646
|
-
const relevanceStatus = offTopicRatio > 0.3 ? "fail" : offTopicRatio > 0.1 ? "warn" : "pass";
|
|
1647
|
-
const msg = offTopic > 0 || tangential > 0 ? `${offTopic} off-topic, ${tangential} tangential out of ${withRelevance.length} pages` : `All ${withRelevance.length} pages relevant to site topic`;
|
|
1648
|
-
allCategories.push({
|
|
1649
|
-
name: t("group.content_relevance", lang),
|
|
1650
|
-
items: [{ name: t("item.relevance.topic", lang), status: relevanceStatus, message: msg }],
|
|
1651
|
-
group: "soft"
|
|
1652
|
-
});
|
|
1653
|
-
}
|
|
1654
|
-
}
|
|
1655
1701
|
const hardCategories = allCategories.filter((c) => c.group === "hard");
|
|
1656
1702
|
const softCategories = allCategories.filter((c) => c.group === "soft");
|
|
1657
1703
|
const allItems = allCategories.flatMap((c) => c.items);
|
|
1658
1704
|
const pageScoresForComposite = pageDetails.map((p) => ({ pageType: p.pageType, score: p.score }));
|
|
1659
|
-
const { compositeScore, categoryScores, hardStatus, softScore, warningRatio, warningPenalty } = computeCompositeScore(pageScoresForComposite, hardCategories, softCategories);
|
|
1705
|
+
const { compositeScore, categoryScores, hardStatus, softScore, warningRatio, warningPenalty, siteAiScore } = computeCompositeScore(pageScoresForComposite, hardCategories, softCategories, pageAnalyses);
|
|
1706
|
+
const aiDimensionAverages = pageAnalyses.length > 0 ? {
|
|
1707
|
+
value: Math.round(pageAnalyses.reduce((s, a) => s + (a.valueScore ?? 5), 0) / pageAnalyses.length * 10) / 10,
|
|
1708
|
+
originality: Math.round(pageAnalyses.reduce((s, a) => s + (a.originalityScore ?? 5), 0) / pageAnalyses.length * 10) / 10,
|
|
1709
|
+
relevance: Math.round(pageAnalyses.reduce((s, a) => s + (a.relevanceScore ?? 5), 0) / pageAnalyses.length * 10) / 10,
|
|
1710
|
+
compliance: Math.round(pageAnalyses.reduce((s, a) => s + (a.complianceScore ?? 5), 0) / pageAnalyses.length * 10) / 10
|
|
1711
|
+
} : void 0;
|
|
1660
1712
|
return {
|
|
1661
1713
|
url,
|
|
1662
1714
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -1680,7 +1732,9 @@ async function check(options) {
|
|
|
1680
1732
|
hardStatus,
|
|
1681
1733
|
softScore,
|
|
1682
1734
|
warningRatio,
|
|
1683
|
-
warningPenalty
|
|
1735
|
+
warningPenalty,
|
|
1736
|
+
siteAiScore,
|
|
1737
|
+
aiDimensionAverages
|
|
1684
1738
|
};
|
|
1685
1739
|
} finally {
|
|
1686
1740
|
await browser.close();
|
|
@@ -1693,7 +1747,9 @@ export {
|
|
|
1693
1747
|
getSupportedLangs,
|
|
1694
1748
|
isValidLang,
|
|
1695
1749
|
t,
|
|
1750
|
+
analyzeWithAI,
|
|
1696
1751
|
analyzeSiteTopic,
|
|
1697
1752
|
detectSiteType,
|
|
1753
|
+
computePageAiScore,
|
|
1698
1754
|
check
|
|
1699
1755
|
};
|
package/dist/cli.js
CHANGED
|
@@ -2,13 +2,15 @@
|
|
|
2
2
|
import {
|
|
3
3
|
BrowserManager,
|
|
4
4
|
analyzeSiteTopic,
|
|
5
|
+
analyzeWithAI,
|
|
5
6
|
check,
|
|
7
|
+
computePageAiScore,
|
|
6
8
|
detectSiteType,
|
|
7
9
|
fetchPage,
|
|
8
10
|
getSupportedLangs,
|
|
9
11
|
isValidLang,
|
|
10
12
|
t
|
|
11
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-PGQWYP7I.js";
|
|
12
14
|
|
|
13
15
|
// src/cli.ts
|
|
14
16
|
import "dotenv/config";
|
|
@@ -95,7 +97,8 @@ function renderTerminalReport(report) {
|
|
|
95
97
|
lines.push("");
|
|
96
98
|
lines.push(chalk.bold(` \u250C\u2500 ${t("report.soft_scoring", lang)} `) + chalk.gray("\u2500".repeat(Math.max(0, 40 - t("report.soft_scoring", lang).length))) + ` ${scoreColor(report.softScore + "/100")}`);
|
|
97
99
|
for (const cat of report.softCategories) {
|
|
98
|
-
const
|
|
100
|
+
const isAiCat = cat.name.includes("AI") || cat.name.includes("ai");
|
|
101
|
+
const score = isAiCat && report.siteAiScore > 0 ? report.siteAiScore : categoryScore(cat);
|
|
99
102
|
const bar = renderBar(score, 100);
|
|
100
103
|
const pct = `${score}%`;
|
|
101
104
|
lines.push(` \u2502 ${bar} ${pct.padStart(4)} ${cat.name}`);
|
|
@@ -108,12 +111,23 @@ function renderTerminalReport(report) {
|
|
|
108
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);
|
|
109
112
|
const softContrib = Math.round(report.softScore * 0.6);
|
|
110
113
|
lines.push(chalk.gray(` \u2502 Hard ${Math.round(hardContrib)}% \xD7 0.4 + Soft ${report.softScore}% \xD7 0.6 - Penalty ${report.warningPenalty} = ${report.compositeScore}`));
|
|
114
|
+
if (report.siteAiScore > 0) {
|
|
115
|
+
lines.push(chalk.gray(` \u2502 AI Value Score: ${report.siteAiScore}/100 (geometric mean \xD7 page-type weights)`));
|
|
116
|
+
}
|
|
117
|
+
if (report.aiDimensionAverages) {
|
|
118
|
+
const d = report.aiDimensionAverages;
|
|
119
|
+
const dimColor = (v) => v >= 8 ? chalk.green : v >= 5 ? chalk.yellow : chalk.red;
|
|
120
|
+
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
|
+
);
|
|
123
|
+
}
|
|
111
124
|
lines.push(chalk.gray(` \u2514\u2500`));
|
|
112
125
|
lines.push("");
|
|
113
126
|
if (report.categoryScores.length > 0) {
|
|
114
127
|
for (const cs of report.categoryScores) {
|
|
115
|
-
const
|
|
116
|
-
const pct = cs.maxScore > 0 ? Math.round(cs.score / cs.maxScore * 100) : 0;
|
|
128
|
+
const isAiCat = cs.name.includes("AI") || cs.name.includes("ai");
|
|
129
|
+
const pct = isAiCat && report.siteAiScore > 0 ? report.siteAiScore : cs.maxScore > 0 ? Math.round(cs.score / cs.maxScore * 100) : 0;
|
|
130
|
+
const bar = renderBar(pct, 100);
|
|
117
131
|
lines.push(` ${bar} ${pct}% ${cs.name}`);
|
|
118
132
|
}
|
|
119
133
|
lines.push("");
|
|
@@ -146,7 +160,7 @@ function renderTerminalReport(report) {
|
|
|
146
160
|
} else {
|
|
147
161
|
lines.push(chalk.green.bold(` ${t("report.ready", lang)}`));
|
|
148
162
|
}
|
|
149
|
-
const hasAi = report.categories.some((c) => c.group === "soft" && c.name.includes("AI"));
|
|
163
|
+
const hasAi = report.categories.some((c) => c.group === "soft" && (c.name.includes("AI") || c.name.includes("ai")));
|
|
150
164
|
if (!hasAi) {
|
|
151
165
|
lines.push("");
|
|
152
166
|
lines.push(chalk.cyan(` \u{1F4A1} ${t("ai.suggest_enable", lang)}`));
|
|
@@ -205,7 +219,7 @@ function getDomain(url) {
|
|
|
205
219
|
}
|
|
206
220
|
}
|
|
207
221
|
var program = new Command();
|
|
208
|
-
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").action(async (url, opts) => {
|
|
222
|
+
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) => {
|
|
209
223
|
try {
|
|
210
224
|
new URL(url);
|
|
211
225
|
} catch {
|
|
@@ -259,6 +273,99 @@ program.name("adsense-check").description("Check if a website meets Google AdSen
|
|
|
259
273
|
process.exit(2);
|
|
260
274
|
}
|
|
261
275
|
}
|
|
276
|
+
if (opts.page) {
|
|
277
|
+
const pageUrl = opts.page.startsWith("http") ? opts.page : "https://" + opts.page;
|
|
278
|
+
const apiKey = opts.apiKey || process.env.AI_API_KEY;
|
|
279
|
+
if (!apiKey) {
|
|
280
|
+
console.error(chalk2.red("Error: --page requires AI (--ai --api-key or AI_API_KEY env)"));
|
|
281
|
+
process.exit(1);
|
|
282
|
+
}
|
|
283
|
+
const browser = new BrowserManager();
|
|
284
|
+
try {
|
|
285
|
+
process.stderr.write(chalk2.cyan(`\u25CF Analyzing page value: ${pageUrl}
|
|
286
|
+
`));
|
|
287
|
+
const pg = await browser.newPage();
|
|
288
|
+
const data = await fetchPage(pg, pageUrl, parseInt(opts.timeout, 10));
|
|
289
|
+
await pg.close();
|
|
290
|
+
process.stderr.write(chalk2.gray(" Detecting topic...\n"));
|
|
291
|
+
let siteTopic;
|
|
292
|
+
try {
|
|
293
|
+
siteTopic = await analyzeSiteTopic(
|
|
294
|
+
{ title: data.title, text: data.text, navText: data.navText + " " + data.footerText },
|
|
295
|
+
lang,
|
|
296
|
+
apiKey
|
|
297
|
+
);
|
|
298
|
+
process.stderr.write(chalk2.gray(` Topic: ${siteTopic.topic} (${siteTopic.type})
|
|
299
|
+
`));
|
|
300
|
+
} catch {
|
|
301
|
+
}
|
|
302
|
+
process.stderr.write(chalk2.gray(" AI: analyzing page value...\n"));
|
|
303
|
+
const result = await analyzeWithAI(
|
|
304
|
+
[{ url: pageUrl, text: data.text }],
|
|
305
|
+
lang,
|
|
306
|
+
apiKey,
|
|
307
|
+
void 0,
|
|
308
|
+
siteTopic
|
|
309
|
+
);
|
|
310
|
+
const analysis = result.pageAnalyses[0];
|
|
311
|
+
if (!analysis) {
|
|
312
|
+
console.error(chalk2.red(" AI analysis returned no results"));
|
|
313
|
+
process.exit(2);
|
|
314
|
+
}
|
|
315
|
+
const score = computePageAiScore(analysis);
|
|
316
|
+
const v = analysis.valueScore ?? 5;
|
|
317
|
+
const o = analysis.originalityScore ?? 5;
|
|
318
|
+
const r = analysis.relevanceScore ?? 5;
|
|
319
|
+
const c = analysis.complianceScore ?? 5;
|
|
320
|
+
const dimColor = (s) => s >= 8 ? chalk2.green : s >= 5 ? chalk2.yellow : chalk2.red;
|
|
321
|
+
const scoreColor = score >= 70 ? chalk2.green : score >= 40 ? chalk2.yellow : chalk2.red;
|
|
322
|
+
const lines = [
|
|
323
|
+
"",
|
|
324
|
+
chalk2.bold.cyan(" Page Value Analysis"),
|
|
325
|
+
chalk2.gray(` URL: ${pageUrl}`),
|
|
326
|
+
chalk2.gray(` Title: ${data.title}`)
|
|
327
|
+
];
|
|
328
|
+
if (siteTopic) {
|
|
329
|
+
lines.push(chalk2.gray(` Site topic: ${siteTopic.topic} (${siteTopic.type})`));
|
|
330
|
+
}
|
|
331
|
+
lines.push("");
|
|
332
|
+
lines.push(` ${dimColor(v)("Value")} ${v}/10`);
|
|
333
|
+
lines.push(` ${dimColor(o)("Originality")} ${o}/10`);
|
|
334
|
+
lines.push(` ${dimColor(r)("Relevance")} ${r}/10`);
|
|
335
|
+
lines.push(` ${dimColor(c)("Compliance")} ${c}/10`);
|
|
336
|
+
lines.push("");
|
|
337
|
+
lines.push(chalk2.bold(` Overall: ${scoreColor(score + "/100")}`) + chalk2.gray(" (geometric mean)"));
|
|
338
|
+
lines.push("");
|
|
339
|
+
if (analysis.assessment) {
|
|
340
|
+
lines.push(chalk2.gray(` ${analysis.assessment}`));
|
|
341
|
+
lines.push("");
|
|
342
|
+
}
|
|
343
|
+
for (const s of analysis.suggestions) {
|
|
344
|
+
lines.push(chalk2.yellow(` -> ${s}`));
|
|
345
|
+
}
|
|
346
|
+
lines.push("");
|
|
347
|
+
if (opts.json) {
|
|
348
|
+
console.log(JSON.stringify({
|
|
349
|
+
url: pageUrl,
|
|
350
|
+
title: data.title,
|
|
351
|
+
topic: siteTopic,
|
|
352
|
+
scores: { value: v, originality: o, relevance: r, compliance: c },
|
|
353
|
+
overall: score,
|
|
354
|
+
relevanceLabel: analysis.relevance,
|
|
355
|
+
assessment: analysis.assessment,
|
|
356
|
+
suggestions: analysis.suggestions
|
|
357
|
+
}, null, 2));
|
|
358
|
+
} else {
|
|
359
|
+
console.log(lines.join("\n"));
|
|
360
|
+
}
|
|
361
|
+
await browser.close();
|
|
362
|
+
process.exit(c <= 2 ? 1 : 0);
|
|
363
|
+
} catch (err) {
|
|
364
|
+
await browser.close();
|
|
365
|
+
console.error(chalk2.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
|
|
366
|
+
process.exit(2);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
262
369
|
process.stderr.write(chalk2.cyan(`\u25CF Checking ${url}...
|
|
263
370
|
`));
|
|
264
371
|
try {
|
package/dist/index.d.ts
CHANGED
|
@@ -73,6 +73,13 @@ interface CheckReport {
|
|
|
73
73
|
softScore: number;
|
|
74
74
|
warningRatio: number;
|
|
75
75
|
warningPenalty: number;
|
|
76
|
+
siteAiScore: number;
|
|
77
|
+
aiDimensionAverages?: {
|
|
78
|
+
value: number;
|
|
79
|
+
originality: number;
|
|
80
|
+
relevance: number;
|
|
81
|
+
compliance: number;
|
|
82
|
+
};
|
|
76
83
|
}
|
|
77
84
|
interface CheckOptions {
|
|
78
85
|
url: string;
|
package/dist/index.js
CHANGED