@cloudcreate/adsense-check 1.0.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 ADDED
@@ -0,0 +1,77 @@
1
+ # adsense-check
2
+
3
+ Check if a website meets Google AdSense review requirements.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g adsense-check
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ # Basic check
15
+ adsense-check https://example.com
16
+
17
+ # JSON output (for programmatic use)
18
+ adsense-check https://example.com --json
19
+
20
+ # Crawl more internal pages (default: 5)
21
+ adsense-check https://example.com --depth 10
22
+
23
+ # Skip AI content analysis
24
+ adsense-check https://example.com --skip-ai
25
+
26
+ # Custom timeout (ms)
27
+ adsense-check https://example.com --timeout 60000
28
+ ```
29
+
30
+ ## What it checks
31
+
32
+ | Category | Checks |
33
+ |----------|--------|
34
+ | **Content Quality** | Page word count, content duplication |
35
+ | **Required Pages** | About, Privacy Policy, Contact, Terms of Service |
36
+ | **Site Structure** | H1 tags, robots.txt, sitemap.xml, internal links, dead links |
37
+ | **Performance** | Load time, mobile viewport, responsive layout, font size, popups |
38
+ | **Policy Compliance** | Blacklisted keywords (porn, gambling, piracy, etc.) |
39
+ | **AI Analysis** | Content originality, quality, compliance (requires `AI_API_KEY`) |
40
+
41
+ ## Options
42
+
43
+ ```
44
+ -v, --version Show version
45
+ -j, --json Output as JSON
46
+ -d, --depth <n> Number of internal pages to crawl (default: 5)
47
+ -s, --skip-ai Skip AI content analysis
48
+ -t, --timeout <ms> Page load timeout (default: 30000)
49
+ --api-key <key> Anthropic API key (or set ANTHROPIC_API_KEY env var)
50
+ ```
51
+
52
+ ## AI Analysis
53
+
54
+ For deeper content quality assessment, configure AI API in `.env`:
55
+
56
+ ```bash
57
+ cp .env.example .env
58
+ # Edit .env and set AI_API_KEY, AI_API_BASE, AI_MODEL
59
+ ```
60
+
61
+ Compatible with any OpenAI-format API: DeepSeek, OpenAI, Moonshot, local LLM, etc.
62
+
63
+ Or pass the key directly:
64
+
65
+ ```bash
66
+ adsense-check https://example.com --api-key sk-xxx...
67
+ ```
68
+
69
+ ## Exit codes
70
+
71
+ - `0` — No failures (ready or mostly ready)
72
+ - `1` — Has failures (not ready)
73
+ - `2` — Error (invalid URL, network failure, etc.)
74
+
75
+ ## License
76
+
77
+ MIT
@@ -0,0 +1,1015 @@
1
+ // src/browser.ts
2
+ import { chromium } from "playwright";
3
+ var BrowserManager = class {
4
+ browser = null;
5
+ async launch() {
6
+ if (!this.browser) {
7
+ this.browser = await chromium.launch({ headless: true });
8
+ }
9
+ return this.browser;
10
+ }
11
+ async newPage() {
12
+ const browser = await this.launch();
13
+ const context = await browser.newContext({
14
+ userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
15
+ viewport: { width: 1280, height: 720 }
16
+ });
17
+ return context.newPage();
18
+ }
19
+ async newMobilePage() {
20
+ const browser = await this.launch();
21
+ const context = await browser.newContext({
22
+ userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1",
23
+ viewport: { width: 390, height: 844 },
24
+ isMobile: true,
25
+ hasTouch: true
26
+ });
27
+ return context.newPage();
28
+ }
29
+ async close() {
30
+ if (this.browser) {
31
+ await this.browser.close();
32
+ this.browser = null;
33
+ }
34
+ }
35
+ };
36
+ async function fetchPage(page, url, timeout = 3e4) {
37
+ const response = await page.goto(url, { waitUntil: "domcontentloaded", timeout });
38
+ const status = response?.status() ?? 0;
39
+ const content = await page.content();
40
+ const text = await page.evaluate(() => document.body?.innerText ?? "");
41
+ const links = await page.evaluate(
42
+ () => Array.from(document.querySelectorAll("a[href]")).map((a) => a.href).filter((href) => href.startsWith("http"))
43
+ );
44
+ const linkDetails = await page.evaluate(
45
+ () => Array.from(document.querySelectorAll("a[href]")).filter((a) => a.href.startsWith("http")).map((a) => ({
46
+ href: a.href,
47
+ text: a.innerText.trim()
48
+ }))
49
+ );
50
+ const navText = await page.evaluate(() => {
51
+ const nav = document.querySelector("nav");
52
+ return nav?.innerText ?? "";
53
+ });
54
+ const footerText = await page.evaluate(() => {
55
+ const footer = document.querySelector("footer");
56
+ return footer?.innerText ?? "";
57
+ });
58
+ const title = await page.title();
59
+ return { status, content, text, links, linkDetails, navText, footerText, title, url };
60
+ }
61
+ async function checkRobotsTxt(origin) {
62
+ try {
63
+ const resp = await fetch(`${origin}/robots.txt`);
64
+ return resp.ok;
65
+ } catch {
66
+ return false;
67
+ }
68
+ }
69
+ async function checkSitemap(origin) {
70
+ try {
71
+ const resp = await fetch(`${origin}/sitemap.xml`);
72
+ return resp.ok;
73
+ } catch {
74
+ return false;
75
+ }
76
+ }
77
+ async function fetchSitemapUrls(origin) {
78
+ try {
79
+ const resp = await fetch(`${origin}/sitemap.xml`);
80
+ if (!resp.ok) return [];
81
+ const text = await resp.text();
82
+ const matches = text.match(/<loc>(.*?)<\/loc>/g);
83
+ if (!matches) return [];
84
+ return matches.map((m) => m.replace(/<\/?loc>/g, "")).filter((u) => u.startsWith("http"));
85
+ } catch {
86
+ return [];
87
+ }
88
+ }
89
+
90
+ // src/checks/content.ts
91
+ function extractMainContent(text, allPageTexts) {
92
+ const paragraphs = text.split(/\n\s*\n/).map((p) => p.trim()).filter(Boolean);
93
+ if (allPageTexts.length <= 1) return paragraphs.join("\n\n");
94
+ const otherTexts = allPageTexts.filter((t) => t !== text);
95
+ const threshold = Math.ceil(otherTexts.length * 0.6);
96
+ const contentParagraphs = paragraphs.filter((para) => {
97
+ if (para.length < 20) return true;
98
+ const normalized = para.replace(/\s+/g, " ").slice(0, 100);
99
+ const appearanceCount = otherTexts.filter(
100
+ (other) => other.replace(/\s+/g, " ").includes(normalized)
101
+ ).length;
102
+ return appearanceCount < threshold;
103
+ });
104
+ return contentParagraphs.join("\n\n");
105
+ }
106
+ function contentRatio(pageText, mainContent) {
107
+ const total = pageText.replace(/\s+/g, "").length;
108
+ if (total === 0) return 0;
109
+ return mainContent.replace(/\s+/g, "").length / total;
110
+ }
111
+ function detectFillerPatterns(text) {
112
+ const fillers = [
113
+ /(?:总之|综上所述|总的来说|简单来说|众所周知|毫无疑问|显而易见|毋庸置疑)/g,
114
+ /(?:in conclusion|as we all know|it goes without saying|needless to say|obviously)/gi,
115
+ /(.{10,30})\1{3,}/g,
116
+ // repeated phrases (e.g. "this is great this is great this is great")
117
+ /(?:点击这里|了解更多|查看更多|click here|read more|learn more|check out){2,}/gi
118
+ ];
119
+ const examples = [];
120
+ let count = 0;
121
+ for (const pattern of fillers) {
122
+ const matches = text.match(pattern);
123
+ if (matches) {
124
+ count += matches.length;
125
+ examples.push(...matches.slice(0, 2));
126
+ }
127
+ }
128
+ return { count, examples: examples.slice(0, 5) };
129
+ }
130
+ function detectTemplatePages(pages) {
131
+ if (pages.length < 3) return { isTemplate: false, similarity: 0, details: "" };
132
+ const structures = pages.map((p) => {
133
+ return p.text.replace(/[a-zA-Z一-鿿]+/g, "W").replace(/\d+/g, "N").replace(/\s+/g, " ").slice(0, 1e3);
134
+ });
135
+ let totalSimilarity = 0;
136
+ let pairs = 0;
137
+ for (let i = 0; i < structures.length; i++) {
138
+ for (let j = i + 1; j < structures.length; j++) {
139
+ const a = structures[i];
140
+ const b = structures[j];
141
+ const longer = a.length > b.length ? a : b;
142
+ const shorter = a.length > b.length ? b : a;
143
+ let common = 0;
144
+ for (let k = 0; k < shorter.length; k++) {
145
+ if (shorter[k] === longer[k]) common++;
146
+ }
147
+ totalSimilarity += common / longer.length;
148
+ pairs++;
149
+ }
150
+ }
151
+ const avgSimilarity = pairs > 0 ? totalSimilarity / pairs : 0;
152
+ return {
153
+ isTemplate: avgSimilarity > 0.6,
154
+ similarity: Math.round(avgSimilarity * 100),
155
+ details: avgSimilarity > 0.6 ? `\u9875\u9762\u7ED3\u6784\u76F8\u4F3C\u5EA6 ${Math.round(avgSimilarity * 100)}%\uFF0C\u7591\u4F3C\u6A21\u677F\u6279\u91CF\u751F\u6210` : ""
156
+ };
157
+ }
158
+ function checkFreshness(pages) {
159
+ const datePatterns = [
160
+ /(\d{4})[年/\-.](\d{1,2})[月/\-.](\d{1,2})/g,
161
+ /(January|February|March|April|May|June|July|August|September|October|November|December)\s+(\d{1,2}),?\s+(\d{4})/gi,
162
+ /(\d{1,2})\s+(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\w*\s+(\d{4})/gi
163
+ ];
164
+ const now = /* @__PURE__ */ new Date();
165
+ const sixMonthsAgo = new Date(now.getTime() - 180 * 24 * 60 * 60 * 1e3);
166
+ let latestDate = "";
167
+ let latestDateObj = /* @__PURE__ */ new Date(0);
168
+ const stalePages = [];
169
+ let hasAnyDate = false;
170
+ for (const page of pages) {
171
+ let pageHasRecentDate = false;
172
+ for (const pattern of datePatterns) {
173
+ const matches = [...page.text.matchAll(pattern)];
174
+ for (const match of matches) {
175
+ hasAnyDate = true;
176
+ try {
177
+ let dateStr;
178
+ if (pattern.source.includes("January|February")) {
179
+ dateStr = `${match[1]} ${match[2]} ${match[3]}`;
180
+ } else if (pattern.source.includes("Jan|Feb")) {
181
+ dateStr = `${match[1]} ${match[2]} ${match[3]}`;
182
+ } else {
183
+ dateStr = `${match[1]}-${match[2].padStart(2, "0")}-${match[3].padStart(2, "0")}`;
184
+ }
185
+ const d = new Date(dateStr);
186
+ if (!isNaN(d.getTime()) && d > /* @__PURE__ */ new Date("2020-01-01") && d <= now) {
187
+ if (d > latestDateObj) {
188
+ latestDateObj = d;
189
+ latestDate = dateStr;
190
+ }
191
+ if (d >= sixMonthsAgo) pageHasRecentDate = true;
192
+ }
193
+ } catch {
194
+ }
195
+ }
196
+ }
197
+ if (!pageHasRecentDate && page.text.length > 200) {
198
+ stalePages.push(page.url);
199
+ }
200
+ }
201
+ return {
202
+ hasRecentContent: hasAnyDate && latestDateObj >= sixMonthsAgo,
203
+ latestDate: latestDate || "\u672A\u68C0\u6D4B\u5230\u65E5\u671F",
204
+ stalePages
205
+ };
206
+ }
207
+ function checkContentQuality(pages, sitePageCount) {
208
+ const items = [];
209
+ const allTexts = pages.map((p) => p.text);
210
+ const lowRatioPages = [];
211
+ for (const page of pages) {
212
+ const mainContent = extractMainContent(page.text, allTexts);
213
+ const ratio = contentRatio(page.text, mainContent);
214
+ const contentChars = mainContent.replace(/\s+/g, "").length;
215
+ if (ratio < 0.3 && page.text.replace(/\s+/g, "").length > 200) {
216
+ lowRatioPages.push({ url: page.url, ratio: Math.round(ratio * 100), contentChars });
217
+ }
218
+ }
219
+ if (lowRatioPages.length > 0) {
220
+ const details = lowRatioPages.map((p) => `${new URL(p.url).pathname}: \u6B63\u6587\u5360\u6BD4 ${p.ratio}% (${p.contentChars} \u5B57)`).join("; ");
221
+ items.push({
222
+ name: "\u6709\u6548\u5185\u5BB9\u6BD4\u7387",
223
+ status: "fail",
224
+ message: `${lowRatioPages.length} \u4E2A\u9875\u9762\u6B63\u6587\u5360\u6BD4\u8FC7\u4F4E\uFF08<30%\uFF09\uFF0C\u5927\u91CF\u5185\u5BB9\u4E3A\u5BFC\u822A/\u9875\u811A\u7B49\u6A21\u677F\u5143\u7D20`,
225
+ detail: details
226
+ });
227
+ } else {
228
+ items.push({
229
+ name: "\u6709\u6548\u5185\u5BB9\u6BD4\u7387",
230
+ status: "pass",
231
+ message: "\u5404\u9875\u9762\u6B63\u6587\u5360\u6BD4\u6B63\u5E38\uFF0C\u6A21\u677F\u5143\u7D20\u5360\u6BD4\u5408\u7406"
232
+ });
233
+ }
234
+ let thinPages = 0;
235
+ for (const page of pages) {
236
+ const mainContent = extractMainContent(page.text, allTexts);
237
+ const contentChars = mainContent.replace(/\s+/g, "").length;
238
+ if (pages.indexOf(page) === 0) {
239
+ if (contentChars >= 500) {
240
+ items.push({
241
+ name: "\u9996\u9875\u5B9E\u8D28\u5185\u5BB9",
242
+ status: "pass",
243
+ message: `\u9996\u9875\u6B63\u6587\u5185\u5BB9\u5145\u8DB3 (${contentChars.toLocaleString()} \u5B57)`
244
+ });
245
+ } else {
246
+ items.push({
247
+ name: "\u9996\u9875\u5B9E\u8D28\u5185\u5BB9",
248
+ status: "fail",
249
+ message: `\u9996\u9875\u6B63\u6587\u5185\u5BB9\u4E0D\u8DB3 (${contentChars} \u5B57\uFF0C\u5EFA\u8BAE 500+ \u5B57)`
250
+ });
251
+ }
252
+ } else {
253
+ if (contentChars < 300) thinPages++;
254
+ }
255
+ }
256
+ if (pages.length > 1) {
257
+ if (thinPages > 0) {
258
+ items.push({
259
+ name: "\u5185\u9875\u5185\u5BB9\u6DF1\u5EA6",
260
+ status: thinPages > pages.length * 0.5 ? "fail" : "warn",
261
+ message: `${thinPages}/${pages.length - 1} \u4E2A\u5185\u9875\u6B63\u6587\u5185\u5BB9\u4E0D\u8DB3 (<300 \u5B57)`
262
+ });
263
+ } else {
264
+ items.push({
265
+ name: "\u5185\u9875\u5185\u5BB9\u6DF1\u5EA6",
266
+ status: "pass",
267
+ message: "\u6240\u6709\u5185\u9875\u6B63\u6587\u5185\u5BB9\u5145\u8DB3"
268
+ });
269
+ }
270
+ }
271
+ const templateResult = detectTemplatePages(pages);
272
+ if (pages.length >= 3) {
273
+ items.push({
274
+ name: "\u6A21\u677F\u5316\u68C0\u6D4B",
275
+ status: templateResult.isTemplate ? "fail" : "pass",
276
+ message: templateResult.isTemplate ? templateResult.details : `\u9875\u9762\u7ED3\u6784\u591A\u6837\u6027\u6B63\u5E38 (\u76F8\u4F3C\u5EA6 ${templateResult.similarity}%)`
277
+ });
278
+ }
279
+ let totalFiller = 0;
280
+ const fillerExamples = [];
281
+ for (const page of pages) {
282
+ const filler = detectFillerPatterns(page.text);
283
+ totalFiller += filler.count;
284
+ fillerExamples.push(...filler.examples);
285
+ }
286
+ if (totalFiller > pages.length * 3) {
287
+ items.push({
288
+ name: "\u51D1\u5B57\u6570\u68C0\u6D4B",
289
+ status: "warn",
290
+ message: `\u68C0\u6D4B\u5230 ${totalFiller} \u5904\u7591\u4F3C\u51D1\u5B57\u6570\u7684\u586B\u5145\u5185\u5BB9`,
291
+ detail: fillerExamples.slice(0, 3).join("; ")
292
+ });
293
+ } else {
294
+ items.push({
295
+ name: "\u51D1\u5B57\u6570\u68C0\u6D4B",
296
+ status: "pass",
297
+ message: "\u672A\u68C0\u6D4B\u5230\u660E\u663E\u7684\u586B\u5145/\u51D1\u5B57\u6570\u5185\u5BB9"
298
+ });
299
+ }
300
+ if (pages.length > 1) {
301
+ const chunkSize = 200;
302
+ let duplicatedChunks = 0;
303
+ const allChunks = /* @__PURE__ */ new Set();
304
+ for (const page of pages) {
305
+ const text = page.text.replace(/\s+/g, " ");
306
+ for (let i = 0; i < text.length - chunkSize; i += chunkSize) {
307
+ const chunk = text.slice(i, i + chunkSize);
308
+ if (allChunks.has(chunk)) {
309
+ duplicatedChunks++;
310
+ } else {
311
+ allChunks.add(chunk);
312
+ }
313
+ }
314
+ }
315
+ const totalChunks = pages.reduce((sum, p) => sum + Math.max(1, Math.floor(p.text.replace(/\s+/g, " ").length / chunkSize)), 0);
316
+ const dupRatio = totalChunks > 0 ? duplicatedChunks / totalChunks : 0;
317
+ if (dupRatio > 0.3) {
318
+ items.push({
319
+ name: "\u8DE8\u9875\u5185\u5BB9\u91CD\u590D",
320
+ status: "warn",
321
+ message: `${Math.round(dupRatio * 100)}% \u7684\u5185\u5BB9\u7247\u6BB5\u5728\u591A\u4E2A\u9875\u9762\u91CD\u590D\u51FA\u73B0`
322
+ });
323
+ } else {
324
+ items.push({
325
+ name: "\u8DE8\u9875\u5185\u5BB9\u91CD\u590D",
326
+ status: "pass",
327
+ message: `\u5404\u9875\u9762\u5185\u5BB9\u72EC\u7ACB\u6027\u826F\u597D (\u91CD\u590D\u7387 ${Math.round(dupRatio * 100)}%)`
328
+ });
329
+ }
330
+ }
331
+ const freshness = checkFreshness(pages);
332
+ if (freshness.hasRecentContent) {
333
+ items.push({
334
+ name: "\u5185\u5BB9\u65B0\u9C9C\u5EA6",
335
+ status: "pass",
336
+ message: `\u6700\u8FD1\u6709\u66F4\u65B0\u5185\u5BB9 (\u6700\u65B0: ${freshness.latestDate})`
337
+ });
338
+ } else if (freshness.latestDate !== "\u672A\u68C0\u6D4B\u5230\u65E5\u671F") {
339
+ items.push({
340
+ name: "\u5185\u5BB9\u65B0\u9C9C\u5EA6",
341
+ status: "warn",
342
+ message: `\u6700\u8FD1\u66F4\u65B0: ${freshness.latestDate}\uFF0C\u8D85\u8FC7 6 \u4E2A\u6708\u672A\u66F4\u65B0`,
343
+ detail: freshness.stalePages.length > 0 ? `\u65E0\u8FD1\u671F\u65E5\u671F\u7684\u9875\u9762: ${freshness.stalePages.map((u) => new URL(u).pathname).join(", ")}` : ""
344
+ });
345
+ } else {
346
+ items.push({
347
+ name: "\u5185\u5BB9\u65B0\u9C9C\u5EA6",
348
+ status: "warn",
349
+ message: "\u9875\u9762\u4E2D\u672A\u68C0\u6D4B\u5230\u65E5\u671F\u4FE1\u606F\uFF0C\u65E0\u6CD5\u5224\u65AD\u5185\u5BB9\u65F6\u6548\u6027"
350
+ });
351
+ }
352
+ if (sitePageCount !== void 0) {
353
+ if (sitePageCount < 10) {
354
+ items.push({
355
+ name: "\u7AD9\u70B9\u89C4\u6A21",
356
+ status: "warn",
357
+ message: `\u7AD9\u70B9\u4EC5 ${sitePageCount} \u4E2A\u9875\u9762\uFF08\u5EFA\u8BAE\u81F3\u5C11 10+ \u4E2A\u6709\u4EF7\u503C\u7684\u5185\u5BB9\u9875\uFF09`
358
+ });
359
+ } else if (sitePageCount < 30) {
360
+ items.push({
361
+ name: "\u7AD9\u70B9\u89C4\u6A21",
362
+ status: "pass",
363
+ message: `\u7AD9\u70B9\u6709 ${sitePageCount} \u4E2A\u9875\u9762`
364
+ });
365
+ } else {
366
+ items.push({
367
+ name: "\u7AD9\u70B9\u89C4\u6A21",
368
+ status: "pass",
369
+ message: `\u7AD9\u70B9\u89C4\u6A21\u826F\u597D (${sitePageCount} \u4E2A\u9875\u9762)`
370
+ });
371
+ }
372
+ }
373
+ return { name: "Content Quality", items };
374
+ }
375
+
376
+ // src/checks/pages.ts
377
+ var REQUIRED_PAGES = [
378
+ {
379
+ name: "About",
380
+ required: true,
381
+ urlPatterns: [/\/about/i, /about[- _]?(us)?/i, /关于我们/, /公司介绍/, /关于/],
382
+ textPatterns: [/about/i, /关于我们/, /公司介绍/, /关于/],
383
+ titlePatterns: [/about/i, /关于/],
384
+ contentPatterns: [/about/i, /关于我们/, /公司简介/, /团队/, /our (story|team|mission)/i]
385
+ },
386
+ {
387
+ name: "Privacy Policy",
388
+ required: true,
389
+ urlPatterns: [/\/privacy/i, /privacy[- _]?policy/i, /隐私政策/, /隐私声明/, /隐私条款/],
390
+ textPatterns: [/privacy/i, /隐私/, /cookie policy/i],
391
+ titlePatterns: [/privacy/i, /隐私/],
392
+ contentPatterns: [/privacy/i, /隐私/, /personal data/i, /个人信息/, /data (collection|use|protect)/i]
393
+ },
394
+ {
395
+ name: "Contact",
396
+ required: true,
397
+ urlPatterns: [/\/contact/i, /contact/i, /联系我们/, /联系方式/, /联系/],
398
+ textPatterns: [/contact/i, /联系我们/, /联系方式/, /联系/, /support/i, /help/i],
399
+ titlePatterns: [/contact/i, /联系/, /support/],
400
+ contentPatterns: [/contact/i, /联系/, /email/i, /邮箱/, /电话/, /address/i]
401
+ },
402
+ {
403
+ name: "Terms of Service",
404
+ required: false,
405
+ urlPatterns: [/\/terms/i, /terms[- _]?(of[- _]?)?service/i, /terms[- _]?and[- _]?conditions/i, /服务条款/, /使用条款/],
406
+ textPatterns: [/terms/i, /服务条款/, /使用条款/, /legal/i],
407
+ titlePatterns: [/terms/i, /条款/, /service agreement/i],
408
+ contentPatterns: [/terms/i, /条款/, /agreement/i, /条款/, /governing law/i, /适用法律/]
409
+ }
410
+ ];
411
+ async function checkRequiredPages(input) {
412
+ const items = [];
413
+ const { allLinks, navText, footerText, sitemapUrls } = input;
414
+ const linkTexts = allLinks.map((l) => l.text).join("\n");
415
+ const linkHrefs = allLinks.map((l) => l.href).join("\n");
416
+ const allText = [linkTexts, linkHrefs, navText, footerText, sitemapUrls.join("\n")].join("\n");
417
+ for (const page of REQUIRED_PAGES) {
418
+ let found = false;
419
+ let foundUrl = "";
420
+ for (const pattern of page.urlPatterns) {
421
+ const linkMatch = allLinks.find((l) => pattern.test(l.href));
422
+ if (linkMatch) {
423
+ found = true;
424
+ foundUrl = linkMatch.href;
425
+ break;
426
+ }
427
+ const sitemapMatch = sitemapUrls.find((u) => pattern.test(u));
428
+ if (sitemapMatch) {
429
+ found = true;
430
+ foundUrl = sitemapMatch;
431
+ break;
432
+ }
433
+ }
434
+ if (!found) {
435
+ for (const pattern of page.textPatterns) {
436
+ const linkMatch = allLinks.find((l) => pattern.test(l.text));
437
+ if (linkMatch) {
438
+ found = true;
439
+ foundUrl = linkMatch.href;
440
+ break;
441
+ }
442
+ }
443
+ }
444
+ if (!found) {
445
+ for (const pattern of page.titlePatterns) {
446
+ const sitemapMatch = sitemapUrls.find((u) => {
447
+ const path = new URL(u).pathname;
448
+ return pattern.test(path);
449
+ });
450
+ if (sitemapMatch) {
451
+ found = true;
452
+ foundUrl = sitemapMatch;
453
+ break;
454
+ }
455
+ }
456
+ }
457
+ if (found) {
458
+ items.push({
459
+ name: `${page.name} \u9875\u9762`,
460
+ status: "pass",
461
+ message: `\u627E\u5230 ${page.name} \u9875\u9762${foundUrl ? ` (${new URL(foundUrl).pathname})` : ""}`
462
+ });
463
+ } else if (page.required) {
464
+ items.push({
465
+ name: `${page.name} \u9875\u9762`,
466
+ status: "fail",
467
+ message: `\u672A\u627E\u5230 ${page.name} \u9875\u9762\uFF08\u5FC5\u9700\uFF09`
468
+ });
469
+ } else {
470
+ items.push({
471
+ name: `${page.name} \u9875\u9762`,
472
+ status: "warn",
473
+ message: `\u672A\u627E\u5230 ${page.name} \u9875\u9762\uFF08\u5EFA\u8BAE\u6DFB\u52A0\uFF09`
474
+ });
475
+ }
476
+ }
477
+ return { name: "Required Pages", items };
478
+ }
479
+
480
+ // src/checks/structure.ts
481
+ async function checkSiteStructure(origin, links, h1Count, deadLinks = []) {
482
+ const items = [];
483
+ if (h1Count === 1) {
484
+ items.push({
485
+ name: "H1 \u6807\u7B7E",
486
+ status: "pass",
487
+ message: "\u9875\u9762\u6709\u4E14\u4EC5\u6709\u4E00\u4E2A H1 \u6807\u7B7E"
488
+ });
489
+ } else if (h1Count === 0) {
490
+ items.push({
491
+ name: "H1 \u6807\u7B7E",
492
+ status: "warn",
493
+ message: "\u9875\u9762\u7F3A\u5C11 H1 \u6807\u7B7E"
494
+ });
495
+ } else {
496
+ items.push({
497
+ name: "H1 \u6807\u7B7E",
498
+ status: "warn",
499
+ message: `\u9875\u9762\u6709 ${h1Count} \u4E2A H1 \u6807\u7B7E\uFF08\u5EFA\u8BAE\u4FDD\u7559 1 \u4E2A\uFF09`
500
+ });
501
+ }
502
+ const hasRobots = await checkRobotsTxt(origin);
503
+ items.push({
504
+ name: "robots.txt",
505
+ status: hasRobots ? "pass" : "warn",
506
+ message: hasRobots ? "robots.txt \u5B58\u5728" : "\u672A\u627E\u5230 robots.txt\uFF08\u5EFA\u8BAE\u6DFB\u52A0\uFF09"
507
+ });
508
+ const hasSitemap = await checkSitemap(origin);
509
+ items.push({
510
+ name: "sitemap.xml",
511
+ status: hasSitemap ? "pass" : "warn",
512
+ message: hasSitemap ? "sitemap.xml \u5B58\u5728" : "\u672A\u627E\u5230 sitemap.xml\uFF08\u5EFA\u8BAE\u6DFB\u52A0\uFF09"
513
+ });
514
+ const internalLinks = links.filter((l) => {
515
+ try {
516
+ return new URL(l).origin === origin;
517
+ } catch {
518
+ return false;
519
+ }
520
+ });
521
+ if (internalLinks.length >= 5) {
522
+ items.push({
523
+ name: "\u5185\u90E8\u94FE\u63A5",
524
+ status: "pass",
525
+ message: `\u9996\u9875\u6709 ${internalLinks.length} \u4E2A\u5185\u90E8\u94FE\u63A5`
526
+ });
527
+ } else {
528
+ items.push({
529
+ name: "\u5185\u90E8\u94FE\u63A5",
530
+ status: "warn",
531
+ message: `\u9996\u9875\u4EC5 ${internalLinks.length} \u4E2A\u5185\u90E8\u94FE\u63A5\uFF08\u5EFA\u8BAE\u589E\u52A0\u5BFC\u822A\u94FE\u63A5\uFF09`
532
+ });
533
+ }
534
+ if (deadLinks.length > 0) {
535
+ items.push({
536
+ name: "\u6B7B\u94FE\u68C0\u6D4B",
537
+ status: "fail",
538
+ message: `\u68C0\u6D4B\u5230 ${deadLinks.length} \u4E2A\u6B7B\u94FE`,
539
+ detail: deadLinks.join(", ")
540
+ });
541
+ } else {
542
+ items.push({
543
+ name: "\u6B7B\u94FE\u68C0\u6D4B",
544
+ status: "pass",
545
+ message: "\u672A\u68C0\u6D4B\u5230\u6B7B\u94FE"
546
+ });
547
+ }
548
+ return { name: "Site Structure", items };
549
+ }
550
+
551
+ // src/checks/performance.ts
552
+ async function checkPerformance(page, url, browser) {
553
+ const items = [];
554
+ const startTime = Date.now();
555
+ try {
556
+ await page.goto(url, { waitUntil: "domcontentloaded", timeout: 3e4 });
557
+ const loadTime = Date.now() - startTime;
558
+ if (loadTime < 3e3) {
559
+ items.push({
560
+ name: "\u9875\u9762\u52A0\u8F7D\u901F\u5EA6",
561
+ status: "pass",
562
+ message: `\u52A0\u8F7D\u65F6\u95F4 ${(loadTime / 1e3).toFixed(1)}s`
563
+ });
564
+ } else if (loadTime < 6e3) {
565
+ items.push({
566
+ name: "\u9875\u9762\u52A0\u8F7D\u901F\u5EA6",
567
+ status: "warn",
568
+ message: `\u52A0\u8F7D\u65F6\u95F4 ${(loadTime / 1e3).toFixed(1)}s\uFF08\u5EFA\u8BAE\u4F18\u5316\u5230 3s \u4EE5\u5185\uFF09`
569
+ });
570
+ } else {
571
+ items.push({
572
+ name: "\u9875\u9762\u52A0\u8F7D\u901F\u5EA6",
573
+ status: "fail",
574
+ message: `\u52A0\u8F7D\u65F6\u95F4 ${(loadTime / 1e3).toFixed(1)}s\uFF08\u8FC7\u6162\uFF0C\u4E25\u91CD\u5F71\u54CD\u7528\u6237\u4F53\u9A8C\uFF09`
575
+ });
576
+ }
577
+ } catch {
578
+ items.push({
579
+ name: "\u9875\u9762\u52A0\u8F7D\u901F\u5EA6",
580
+ status: "fail",
581
+ message: "\u9875\u9762\u52A0\u8F7D\u8D85\u65F6\uFF0830s\uFF09"
582
+ });
583
+ }
584
+ const hasViewport = await page.evaluate(() => {
585
+ const meta = document.querySelector('meta[name="viewport"]');
586
+ return !!meta;
587
+ });
588
+ items.push({
589
+ name: "viewport \u6807\u7B7E",
590
+ status: hasViewport ? "pass" : "warn",
591
+ message: hasViewport ? "\u5B58\u5728 viewport meta \u6807\u7B7E" : "\u7F3A\u5C11 viewport meta \u6807\u7B7E"
592
+ });
593
+ try {
594
+ const mobileContext = await browser.newContext({
595
+ userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1",
596
+ viewport: { width: 390, height: 844 },
597
+ isMobile: true,
598
+ hasTouch: true
599
+ });
600
+ const mobilePage = await mobileContext.newPage();
601
+ await mobilePage.goto(url, { waitUntil: "domcontentloaded", timeout: 3e4 });
602
+ const mobileCheck = await mobilePage.evaluate(() => {
603
+ const body = document.body;
604
+ const hasHorizontalScroll = body.scrollWidth > window.innerWidth;
605
+ const textTooSmall = Array.from(document.querySelectorAll("p, span, a, li")).some((el) => {
606
+ const fontSize = parseFloat(window.getComputedStyle(el).fontSize);
607
+ return fontSize > 0 && fontSize < 12;
608
+ });
609
+ return { hasHorizontalScroll, textTooSmall };
610
+ });
611
+ if (mobileCheck.hasHorizontalScroll) {
612
+ items.push({
613
+ name: "\u79FB\u52A8\u7AEF\u6A2A\u5411\u6EA2\u51FA",
614
+ status: "warn",
615
+ message: "\u79FB\u52A8\u7AEF\u9875\u9762\u5B58\u5728\u6A2A\u5411\u6EDA\u52A8\uFF08body \u5BBD\u5EA6\u8D85\u51FA\u89C6\u53E3\uFF09"
616
+ });
617
+ } else {
618
+ items.push({
619
+ name: "\u79FB\u52A8\u7AEF\u6A2A\u5411\u6EA2\u51FA",
620
+ status: "pass",
621
+ message: "\u79FB\u52A8\u7AEF\u9875\u9762\u65E0\u6A2A\u5411\u6EA2\u51FA"
622
+ });
623
+ }
624
+ if (mobileCheck.textTooSmall) {
625
+ items.push({
626
+ name: "\u79FB\u52A8\u7AEF\u5B57\u4F53\u5927\u5C0F",
627
+ status: "warn",
628
+ message: "\u90E8\u5206\u6587\u5B57\u5B57\u53F7\u5C0F\u4E8E 12px\uFF0C\u79FB\u52A8\u7AEF\u9605\u8BFB\u56F0\u96BE"
629
+ });
630
+ } else {
631
+ items.push({
632
+ name: "\u79FB\u52A8\u7AEF\u5B57\u4F53\u5927\u5C0F",
633
+ status: "pass",
634
+ message: "\u79FB\u52A8\u7AEF\u5B57\u53F7\u9002\u4E2D"
635
+ });
636
+ }
637
+ await mobileContext.close();
638
+ } catch {
639
+ items.push({
640
+ name: "\u79FB\u52A8\u7AEF\u6D4B\u8BD5",
641
+ status: "skip",
642
+ message: "\u79FB\u52A8\u7AEF\u6D4B\u8BD5\u5931\u8D25\uFF08\u9875\u9762\u52A0\u8F7D\u5F02\u5E38\uFF09"
643
+ });
644
+ }
645
+ const hasOverlay = await page.evaluate(() => {
646
+ const overlays = document.querySelectorAll(
647
+ '[class*="modal"], [class*="popup"], [class*="overlay"], [id*="modal"], [id*="popup"]'
648
+ );
649
+ const visible = Array.from(overlays).filter((el) => {
650
+ const style = window.getComputedStyle(el);
651
+ return style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0";
652
+ });
653
+ return visible.length;
654
+ });
655
+ if (hasOverlay > 0) {
656
+ items.push({
657
+ name: "\u5F39\u7A97\u68C0\u6D4B",
658
+ status: "warn",
659
+ message: `\u68C0\u6D4B\u5230 ${hasOverlay} \u4E2A\u53EF\u80FD\u7684\u5F39\u7A97/\u906E\u7F69\u5C42\uFF08\u8FC7\u591A\u5F39\u7A97\u4F1A\u5F71\u54CD\u5BA1\u6838\uFF09`
660
+ });
661
+ } else {
662
+ items.push({
663
+ name: "\u5F39\u7A97\u68C0\u6D4B",
664
+ status: "pass",
665
+ message: "\u672A\u68C0\u6D4B\u5230\u660E\u663E\u7684\u5F39\u7A97/\u906E\u7F69\u5C42"
666
+ });
667
+ }
668
+ return { name: "Performance", items };
669
+ }
670
+
671
+ // src/checks/policy.ts
672
+ var BLACKLIST_PATTERNS = [
673
+ /\b(porn|xxx|nude|naked|sex\s*tube)\b/i,
674
+ /\b(gamble|casino|betting|lottery)\b/i,
675
+ /\b(hack|crack|pirate|torrent|warez)\b/i,
676
+ /\b(drug|marijuana|cocaine|heroin)\b/i,
677
+ /色情|赌博|毒品|暴力|盗版/
678
+ ];
679
+ function checkPolicyCompliance(pages) {
680
+ const items = [];
681
+ const violations = [];
682
+ for (const page of pages) {
683
+ for (const pattern of BLACKLIST_PATTERNS) {
684
+ const match = page.text.match(pattern);
685
+ if (match) {
686
+ violations.push({ url: page.url, match: match[0] });
687
+ }
688
+ }
689
+ }
690
+ if (violations.length > 0) {
691
+ const details = violations.map((v) => `${v.url}: "${v.match}"`).join("; ");
692
+ items.push({
693
+ name: "\u8FDD\u89C4\u5173\u952E\u8BCD",
694
+ status: "fail",
695
+ message: `\u68C0\u6D4B\u5230 ${violations.length} \u4E2A\u53EF\u7591\u5173\u952E\u8BCD`,
696
+ detail: details
697
+ });
698
+ } else {
699
+ items.push({
700
+ name: "\u8FDD\u89C4\u5173\u952E\u8BCD",
701
+ status: "pass",
702
+ message: "\u672A\u68C0\u6D4B\u5230\u660E\u663E\u7684\u8FDD\u89C4\u5173\u952E\u8BCD"
703
+ });
704
+ }
705
+ const hasAdKeywords = pages.some(
706
+ (p) => /ad[-_]?slot|google[-_]?ad|adsbygoogle|广告位/i.test(p.text)
707
+ );
708
+ if (hasAdKeywords) {
709
+ items.push({
710
+ name: "\u5E7F\u544A\u4EE3\u7801",
711
+ status: "warn",
712
+ message: "\u9875\u9762\u5DF2\u5B58\u5728\u5E7F\u544A\u4EE3\u7801\u5360\u4F4D\uFF0C\u786E\u8BA4\u4E0D\u5F71\u54CD\u5BA1\u6838"
713
+ });
714
+ }
715
+ return { name: "Policy Compliance", items };
716
+ }
717
+
718
+ // src/ai/analyzer.ts
719
+ function getApiEndpoint() {
720
+ const base = process.env.AI_API_BASE || "https://api.deepseek.com";
721
+ return `${base.replace(/\/$/, "")}/chat/completions`;
722
+ }
723
+ function getApiKey() {
724
+ return process.env.AI_API_KEY;
725
+ }
726
+ function getModel() {
727
+ return process.env.AI_MODEL || "deepseek-chat";
728
+ }
729
+ async function callAI(prompt, maxTokens = 4096) {
730
+ const response = await fetch(getApiEndpoint(), {
731
+ method: "POST",
732
+ headers: {
733
+ "Content-Type": "application/json",
734
+ Authorization: `Bearer ${getApiKey()}`
735
+ },
736
+ body: JSON.stringify({
737
+ model: getModel(),
738
+ max_tokens: maxTokens,
739
+ messages: [{ role: "user", content: prompt }]
740
+ })
741
+ });
742
+ if (!response.ok) {
743
+ throw new Error(`AI API error: ${response.status} ${response.statusText}`);
744
+ }
745
+ const data = await response.json();
746
+ return data.choices?.[0]?.message?.content ?? "";
747
+ }
748
+ function extractJson(text) {
749
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
750
+ if (jsonMatch) {
751
+ return JSON.parse(jsonMatch[0]);
752
+ }
753
+ throw new Error("No JSON found in response");
754
+ }
755
+ async function analyzeWithAI(pages, apiKey) {
756
+ const key = apiKey || getApiKey();
757
+ const empty = {
758
+ contentQuality: { status: "skip", detail: "\u672A\u914D\u7F6E AI_API_KEY\uFF0C\u8DF3\u8FC7 AI \u5206\u6790" },
759
+ originality: { status: "skip", detail: "N/A" },
760
+ compliance: { status: "skip", detail: "N/A" },
761
+ suggestions: [],
762
+ pageAnalyses: []
763
+ };
764
+ if (!key) return empty;
765
+ const sampled = pages.slice(0, 8);
766
+ const pageContents = sampled.map((p, i) => `=== \u9875\u9762 ${i + 1}: ${p.url} ===
767
+ ${p.text.slice(0, 1500)}`).join("\n\n");
768
+ const prompt = `\u4F60\u662F\u4E00\u4E2A Google AdSense \u5BA1\u6838\u4E13\u5BB6\uFF0C\u4E13\u95E8\u5224\u65AD\u7F51\u7AD9\u662F\u5426\u5B58\u5728 "low value content" \u95EE\u9898\u3002
769
+ \u5F53\u524D\u65E5\u671F\uFF1A${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}
770
+
771
+ AdSense \u6700\u5E38\u89C1\u7684\u62D2\u7EDD\u7406\u7531\u662F "low value content"\uFF08\u4F4E\u4EF7\u503C\u5185\u5BB9\uFF09\uFF0C\u8868\u73B0\u5305\u62EC\uFF1A
772
+ - \u9875\u9762\u5185\u5BB9\u592A\u8584\uFF0C\u7F3A\u4E4F\u5B9E\u8D28\u6027\u4FE1\u606F
773
+ - \u5185\u5BB9\u50CF\u662F\u673A\u5668\u6279\u91CF\u751F\u6210\u6216\u4ECE\u5176\u4ED6\u7F51\u7AD9\u91C7\u96C6\u7684
774
+ - \u7F51\u7AD9\u6CA1\u6709\u4E3A\u7528\u6237\u63D0\u4F9B\u72EC\u7279\u7684\u4EF7\u503C
775
+ - \u5185\u5BB9\u7A7A\u6D1E\uFF0C\u5927\u91CF\u51D1\u5B57\u6570\u3001\u91CD\u590D\u8868\u8FF0
776
+ - \u591A\u4E2A\u9875\u9762\u5185\u5BB9\u9AD8\u5EA6\u96F7\u540C\uFF0C\u53EA\u662F\u6362\u4E86\u5173\u952E\u8BCD
777
+
778
+ \u8BF7\u5206\u6790\u4EE5\u4E0B ${sampled.length} \u4E2A\u9875\u9762\uFF0C\u8FD4\u56DE JSON\uFF1A
779
+
780
+ {
781
+ "overall": {
782
+ "contentQuality": { "status": "pass|warn|fail", "detail": "\u6574\u4F53\u5185\u5BB9\u4EF7\u503C\u8BC4\u4F30..." },
783
+ "originality": { "status": "pass|warn|fail", "detail": "\u6574\u4F53\u539F\u521B\u6027\u8BC4\u4F30..." },
784
+ "compliance": { "status": "pass|warn|fail", "detail": "\u6574\u4F53\u5408\u89C4\u6027\u8BC4\u4F30..." },
785
+ "suggestions": ["\u6539\u8FDB\u5EFA\u8BAE1", "\u6539\u8FDB\u5EFA\u8BAE2"]
786
+ },
787
+ "pages": [
788
+ {
789
+ "url": "\u9875\u9762URL",
790
+ "status": "pass|warn|fail",
791
+ "assessment": "\u8BE5\u9875\u9762\u7684\u5177\u4F53\u8BC4\u4F30\uFF0C\u8BF4\u660E\u5185\u5BB9\u4EF7\u503C\u3001\u95EE\u9898\u6240\u5728",
792
+ "suggestions": ["\u9488\u5BF9\u8BE5\u9875\u9762\u7684\u5177\u4F53\u6539\u8FDB\u5EFA\u8BAE"]
793
+ }
794
+ ]
795
+ }
796
+
797
+ \u9875\u9762\u5185\u5BB9\uFF1A
798
+
799
+ ${pageContents}`;
800
+ try {
801
+ const text = await callAI(prompt, 4096);
802
+ const result = extractJson(text);
803
+ return {
804
+ contentQuality: result.overall?.contentQuality ?? { status: "warn", detail: "\u89E3\u6790\u5F02\u5E38" },
805
+ originality: result.overall?.originality ?? { status: "warn", detail: "\u89E3\u6790\u5F02\u5E38" },
806
+ compliance: result.overall?.compliance ?? { status: "warn", detail: "\u89E3\u6790\u5F02\u5E38" },
807
+ suggestions: result.overall?.suggestions ?? [],
808
+ pageAnalyses: (result.pages ?? []).map((p) => ({
809
+ url: p.url,
810
+ status: p.status ?? "warn",
811
+ assessment: p.assessment ?? "",
812
+ suggestions: p.suggestions ?? []
813
+ }))
814
+ };
815
+ } catch (err) {
816
+ return {
817
+ ...empty,
818
+ contentQuality: { status: "warn", detail: `AI \u5206\u6790\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}` }
819
+ };
820
+ }
821
+ }
822
+
823
+ // src/checker.ts
824
+ function extractMainContent2(text, allPageTexts) {
825
+ const paragraphs = text.split(/\n\s*\n/).map((p) => p.trim()).filter(Boolean);
826
+ if (allPageTexts.length <= 1) return paragraphs.join("\n\n");
827
+ const otherTexts = allPageTexts.filter((t) => t !== text);
828
+ const threshold = Math.ceil(otherTexts.length * 0.6);
829
+ return paragraphs.filter((para) => {
830
+ if (para.length < 20) return true;
831
+ const normalized = para.replace(/\s+/g, " ").slice(0, 100);
832
+ const count = otherTexts.filter((o) => o.replace(/\s+/g, " ").includes(normalized)).length;
833
+ return count < threshold;
834
+ }).join("\n\n");
835
+ }
836
+ function buildPageDetails(pages, aiAnalyses) {
837
+ const allTexts = pages.map((p) => p.text);
838
+ const aiMap = new Map(aiAnalyses.map((a) => [a.url, a]));
839
+ return pages.map((page) => {
840
+ const totalChars = page.text.replace(/\s+/g, "").length;
841
+ const mainContent = extractMainContent2(page.text, allTexts);
842
+ const contentChars = mainContent.replace(/\s+/g, "").length;
843
+ const contentRatio2 = totalChars > 0 ? Math.round(contentChars / totalChars * 100) : 0;
844
+ const issues = [];
845
+ let contentStatus = "pass";
846
+ if (contentRatio2 < 30 && totalChars > 200) {
847
+ issues.push(`\u6B63\u6587\u5360\u6BD4\u4EC5 ${contentRatio2}%\uFF0C\u5927\u91CF\u6A21\u677F\u5143\u7D20`);
848
+ contentStatus = "fail";
849
+ }
850
+ if (contentChars < 300) {
851
+ issues.push(`\u6B63\u6587\u5185\u5BB9\u4E0D\u8DB3 (${contentChars} \u5B57)`);
852
+ contentStatus = contentStatus === "fail" ? "fail" : "warn";
853
+ }
854
+ const ai = aiMap.get(page.url);
855
+ const detail = {
856
+ url: page.url,
857
+ title: page.title,
858
+ totalChars,
859
+ contentChars,
860
+ contentRatio: contentRatio2,
861
+ contentStatus,
862
+ issues
863
+ };
864
+ if (ai) {
865
+ detail.ai = {
866
+ status: ai.status,
867
+ assessment: ai.assessment,
868
+ suggestions: ai.suggestions
869
+ };
870
+ }
871
+ return detail;
872
+ });
873
+ }
874
+ async function check(options) {
875
+ const { url, depth = 10, skipAi = false, timeout = 3e4, apiKey } = options;
876
+ const origin = new URL(url).origin;
877
+ const browser = new BrowserManager();
878
+ try {
879
+ const homepage = await browser.newPage();
880
+ const homeData = await fetchPage(homepage, url, timeout);
881
+ const h1Count = await homepage.evaluate(
882
+ () => document.querySelectorAll("h1").length
883
+ );
884
+ const sitemapUrls = await fetchSitemapUrls(origin);
885
+ const pages = [
886
+ { url: homeData.url, text: homeData.text, title: homeData.title }
887
+ ];
888
+ const internalLinks = homeData.links.filter((l) => {
889
+ try {
890
+ return new URL(l).origin === origin;
891
+ } catch {
892
+ return false;
893
+ }
894
+ });
895
+ const sitemapInternal = sitemapUrls.filter((u) => {
896
+ try {
897
+ return new URL(u).origin === origin;
898
+ } catch {
899
+ return false;
900
+ }
901
+ });
902
+ const allInternal = [.../* @__PURE__ */ new Set([...internalLinks, ...sitemapInternal])];
903
+ const uniqueLinks = allInternal.slice(0, depth);
904
+ const deadLinks = [];
905
+ for (const link of uniqueLinks) {
906
+ if (link === url) continue;
907
+ try {
908
+ const page = await browser.newPage();
909
+ const resp = await page.goto(link, { waitUntil: "domcontentloaded", timeout });
910
+ const status = resp?.status() ?? 0;
911
+ if (status >= 400) {
912
+ deadLinks.push(`${link} (${status})`);
913
+ } else {
914
+ const data = await fetchPage(page, link, timeout);
915
+ pages.push({ url: link, text: data.text, title: data.title });
916
+ }
917
+ await page.close();
918
+ } catch {
919
+ deadLinks.push(`${link} (timeout/error)`);
920
+ }
921
+ }
922
+ const seen = /* @__PURE__ */ new Set();
923
+ const uniquePages = pages.filter((p) => {
924
+ const normalized = p.url.replace(/\/+$/, "").split("#")[0];
925
+ if (seen.has(normalized)) return false;
926
+ seen.add(normalized);
927
+ return true;
928
+ });
929
+ const categories = [];
930
+ categories.push(checkContentQuality(uniquePages, allInternal.length));
931
+ categories.push(await checkRequiredPages({
932
+ allLinks: homeData.linkDetails,
933
+ navText: homeData.navText,
934
+ footerText: homeData.footerText,
935
+ sitemapUrls
936
+ }));
937
+ categories.push(await checkSiteStructure(origin, homeData.links, h1Count, deadLinks));
938
+ const playBrowser = await browser.launch();
939
+ const perfPage = await browser.newPage();
940
+ categories.push(await checkPerformance(perfPage, url, playBrowser));
941
+ await perfPage.close();
942
+ categories.push(checkPolicyCompliance(uniquePages));
943
+ let pageAnalyses = [];
944
+ if (!skipAi) {
945
+ try {
946
+ const aiResult = await analyzeWithAI(uniquePages, apiKey);
947
+ pageAnalyses = aiResult.pageAnalyses;
948
+ const aiCategory = {
949
+ name: "AI Content Analysis",
950
+ items: [
951
+ {
952
+ name: "\u5185\u5BB9\u8D28\u91CF\u8BC4\u4F30",
953
+ status: aiResult.contentQuality.status,
954
+ message: aiResult.contentQuality.detail.slice(0, 200)
955
+ },
956
+ {
957
+ name: "\u539F\u521B\u6027\u8BC4\u4F30",
958
+ status: aiResult.originality.status,
959
+ message: aiResult.originality.detail.slice(0, 200)
960
+ },
961
+ {
962
+ name: "\u5408\u89C4\u6027\u8BC4\u4F30",
963
+ status: aiResult.compliance.status,
964
+ message: aiResult.compliance.detail.slice(0, 200)
965
+ }
966
+ ]
967
+ };
968
+ if (aiResult.suggestions.length > 0) {
969
+ aiCategory.items.push({
970
+ name: "AI \u5EFA\u8BAE",
971
+ status: "warn",
972
+ message: `${aiResult.suggestions.length} \u6761\u6539\u8FDB\u5EFA\u8BAE`,
973
+ detail: aiResult.suggestions.join("; ")
974
+ });
975
+ }
976
+ categories.push(aiCategory);
977
+ } catch (err) {
978
+ categories.push({
979
+ name: "AI Content Analysis",
980
+ items: [
981
+ {
982
+ name: "AI \u5206\u6790",
983
+ status: "skip",
984
+ message: `AI \u5206\u6790\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`
985
+ }
986
+ ]
987
+ });
988
+ }
989
+ }
990
+ const pageDetails = buildPageDetails(uniquePages, pageAnalyses);
991
+ const allItems = categories.flatMap((c) => c.items);
992
+ const passed = allItems.filter((i) => i.status === "pass").length;
993
+ const warned = allItems.filter((i) => i.status === "warn").length;
994
+ const failed = allItems.filter((i) => i.status === "fail").length;
995
+ const skipped = allItems.filter((i) => i.status === "skip").length;
996
+ return {
997
+ url,
998
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
999
+ categories,
1000
+ score: passed,
1001
+ totalChecks: allItems.length,
1002
+ passed,
1003
+ warned,
1004
+ failed,
1005
+ skipped,
1006
+ pages: pageDetails
1007
+ };
1008
+ } finally {
1009
+ await browser.close();
1010
+ }
1011
+ }
1012
+
1013
+ export {
1014
+ check
1015
+ };
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.js ADDED
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ check
4
+ } from "./chunk-V2YZ36NU.js";
5
+
6
+ // src/cli.ts
7
+ import "dotenv/config";
8
+ import { Command } from "commander";
9
+ import chalk2 from "chalk";
10
+ import { mkdirSync, writeFileSync } from "fs";
11
+ import { join, dirname } from "path";
12
+ import { fileURLToPath } from "url";
13
+
14
+ // src/reporter.ts
15
+ import chalk from "chalk";
16
+ import figures from "figures";
17
+ var STATUS_ICONS = {
18
+ pass: chalk.green(figures.tick),
19
+ warn: chalk.yellow(figures.warning),
20
+ fail: chalk.red(figures.cross),
21
+ skip: chalk.gray("-")
22
+ };
23
+ var STATUS_LABELS = {
24
+ pass: chalk.green("PASS"),
25
+ warn: chalk.yellow("WARN"),
26
+ fail: chalk.red("FAIL"),
27
+ skip: chalk.gray("SKIP")
28
+ };
29
+ function getStatusSummary(report) {
30
+ if (report.failed > 0) {
31
+ return chalk.red.bold(`NOT READY \u2014 ${report.failed} \u9879\u5931\u8D25\u9700\u8981\u4FEE\u590D`);
32
+ }
33
+ if (report.warned > 0) {
34
+ return chalk.yellow.bold(`MOSTLY READY \u2014 \u4FEE\u590D ${report.warned} \u9879\u8B66\u544A\u540E\u53EF\u63D0\u4EA4\u5BA1\u6838`);
35
+ }
36
+ return chalk.green.bold("READY \u2014 \u53EF\u4EE5\u63D0\u4EA4 AdSense \u5BA1\u6838");
37
+ }
38
+ function renderTerminalReport(report) {
39
+ const lines = [];
40
+ lines.push("");
41
+ lines.push(chalk.bold.cyan(" AdSense Checklist Report"));
42
+ lines.push(chalk.gray(` Website: ${report.url}`));
43
+ lines.push(chalk.gray(` Checked: ${report.timestamp}`));
44
+ lines.push("");
45
+ for (const category of report.categories) {
46
+ lines.push(chalk.bold(` ${category.name}`));
47
+ for (const item of category.items) {
48
+ const icon = STATUS_ICONS[item.status];
49
+ const label = STATUS_LABELS[item.status];
50
+ lines.push(` ${icon} [${label}] ${item.message}`);
51
+ if (item.detail) {
52
+ lines.push(chalk.gray(` ${item.detail}`));
53
+ }
54
+ }
55
+ lines.push("");
56
+ }
57
+ if (report.pages.length > 0) {
58
+ lines.push(chalk.bold(" Page Details"));
59
+ lines.push(chalk.gray(` (${report.pages.length} pages analyzed)`));
60
+ lines.push("");
61
+ const problemPages = report.pages.filter(
62
+ (p) => p.contentStatus !== "pass" || p.issues.length > 0 || p.ai && p.ai.status !== "pass"
63
+ );
64
+ const okPages = report.pages.filter(
65
+ (p) => p.contentStatus === "pass" && p.issues.length === 0 && (!p.ai || p.ai.status === "pass")
66
+ );
67
+ for (const page of problemPages) {
68
+ renderPageDetail(lines, page, true);
69
+ }
70
+ if (okPages.length > 0) {
71
+ lines.push(chalk.gray(` + ${okPages.length} \u4E2A\u9875\u9762\u65E0\u95EE\u9898`));
72
+ lines.push("");
73
+ }
74
+ }
75
+ lines.push(chalk.bold(" Score: ") + `${report.score}/${report.totalChecks}`);
76
+ lines.push(` Status: ${getStatusSummary(report)}`);
77
+ lines.push("");
78
+ return lines.join("\n");
79
+ }
80
+ function renderPageDetail(lines, page, verbose) {
81
+ const path = safePath(page.url);
82
+ const statusIcon = STATUS_ICONS[page.contentStatus];
83
+ const ratioColor = page.contentRatio >= 50 ? chalk.green : page.contentRatio >= 30 ? chalk.yellow : chalk.red;
84
+ lines.push(` ${statusIcon} ${chalk.bold(path)}`);
85
+ lines.push(chalk.gray(` ${page.title}`));
86
+ lines.push(` \u6B63\u6587 ${ratioColor(page.contentRatio + "%")} (${page.contentChars}/${page.totalChars} \u5B57)`);
87
+ for (const issue of page.issues) {
88
+ lines.push(chalk.yellow(` ! ${issue}`));
89
+ }
90
+ if (page.ai) {
91
+ const aiIcon = STATUS_ICONS[page.ai.status];
92
+ lines.push(` ${aiIcon} AI: ${truncate(page.ai.assessment, 80)}`);
93
+ for (const s of page.ai.suggestions.slice(0, 2)) {
94
+ lines.push(chalk.gray(` \u2192 ${truncate(s, 70)}`));
95
+ }
96
+ }
97
+ lines.push("");
98
+ }
99
+ function safePath(url) {
100
+ try {
101
+ return new URL(url).pathname;
102
+ } catch {
103
+ return url;
104
+ }
105
+ }
106
+ function truncate(s, max) {
107
+ return s.length > max ? s.slice(0, max - 1) + "..." : s;
108
+ }
109
+ function renderJsonReport(report) {
110
+ return JSON.stringify(report, null, 2);
111
+ }
112
+
113
+ // src/cli.ts
114
+ var __dirname = dirname(fileURLToPath(import.meta.url));
115
+ function formatTimestamp() {
116
+ const d = /* @__PURE__ */ new Date();
117
+ const pad = (n) => String(n).padStart(2, "0");
118
+ return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
119
+ }
120
+ function getDomain(url) {
121
+ try {
122
+ return new URL(url).hostname;
123
+ } catch {
124
+ return "unknown";
125
+ }
126
+ }
127
+ var program = new Command();
128
+ 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 as JSON to stdout").option("-d, --depth <number>", "Number of internal pages to crawl", "10").option("-s, --skip-ai", "Skip AI content analysis", false).option("-t, --timeout <ms>", "Page load timeout in milliseconds", "30000").option("--api-key <key>", "AI API key (or set AI_API_KEY in .env)").option("-o, --output <dir>", "Report output directory", "tmp").option("--no-save", "Skip auto-saving report files").action(async (url, opts) => {
129
+ try {
130
+ new URL(url);
131
+ } catch {
132
+ console.error(chalk2.red(`Error: Invalid URL "${url}"`));
133
+ process.exit(1);
134
+ }
135
+ if (!url.startsWith("http")) {
136
+ url = "https://" + url;
137
+ }
138
+ const spinner = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
139
+ let frame = 0;
140
+ const interval = setInterval(() => {
141
+ process.stderr.write(`\r${chalk2.cyan(spinner[frame++ % spinner.length])} Checking ${url}...`);
142
+ }, 80);
143
+ try {
144
+ const report = await check({
145
+ url,
146
+ depth: parseInt(opts.depth, 10),
147
+ skipAi: opts.skipAi,
148
+ timeout: parseInt(opts.timeout, 10),
149
+ apiKey: opts.apiKey
150
+ });
151
+ clearInterval(interval);
152
+ process.stderr.write("\r" + " ".repeat(60) + "\r");
153
+ if (opts.json) {
154
+ console.log(renderJsonReport(report));
155
+ } else {
156
+ console.log(renderTerminalReport(report));
157
+ }
158
+ if (opts.save !== false) {
159
+ const ts = formatTimestamp();
160
+ const domain = getDomain(url);
161
+ const outDir = join(process.cwd(), opts.output);
162
+ try {
163
+ mkdirSync(outDir, { recursive: true });
164
+ const jsonPath = join(outDir, `${domain}-${ts}.json`);
165
+ writeFileSync(jsonPath, renderJsonReport(report), "utf-8");
166
+ console.log(chalk2.gray(` Report saved: ${jsonPath}`));
167
+ } catch (saveErr) {
168
+ console.error(chalk2.yellow(` Warning: Failed to save report: ${saveErr instanceof Error ? saveErr.message : String(saveErr)}`));
169
+ }
170
+ }
171
+ process.exit(report.failed > 0 ? 1 : 0);
172
+ } catch (err) {
173
+ clearInterval(interval);
174
+ process.stderr.write("\r" + " ".repeat(60) + "\r");
175
+ console.error(chalk2.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
176
+ process.exit(2);
177
+ }
178
+ });
179
+ program.parse();
@@ -0,0 +1,48 @@
1
+ type CheckStatus = 'pass' | 'warn' | 'fail' | 'skip';
2
+ interface CheckItem {
3
+ name: string;
4
+ status: CheckStatus;
5
+ message: string;
6
+ detail?: string;
7
+ }
8
+ interface CheckCategory {
9
+ name: string;
10
+ items: CheckItem[];
11
+ }
12
+ interface PageDetail {
13
+ url: string;
14
+ title: string;
15
+ totalChars: number;
16
+ contentChars: number;
17
+ contentRatio: number;
18
+ contentStatus: CheckStatus;
19
+ issues: string[];
20
+ ai?: {
21
+ status: CheckStatus;
22
+ assessment: string;
23
+ suggestions: string[];
24
+ };
25
+ }
26
+ interface CheckReport {
27
+ url: string;
28
+ timestamp: string;
29
+ categories: CheckCategory[];
30
+ score: number;
31
+ totalChecks: number;
32
+ passed: number;
33
+ warned: number;
34
+ failed: number;
35
+ skipped: number;
36
+ pages: PageDetail[];
37
+ }
38
+ interface CheckOptions {
39
+ url: string;
40
+ depth?: number;
41
+ skipAi?: boolean;
42
+ timeout?: number;
43
+ apiKey?: string;
44
+ }
45
+
46
+ declare function check(options: CheckOptions): Promise<CheckReport>;
47
+
48
+ export { type CheckCategory, type CheckItem, type CheckOptions, type CheckReport, check };
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ import {
2
+ check
3
+ } from "./chunk-V2YZ36NU.js";
4
+ export {
5
+ check
6
+ };
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@cloudcreate/adsense-check",
3
+ "version": "1.0.0",
4
+ "description": "Check if a website meets Google AdSense review requirements",
5
+ "homepage": "https://cloudcreate.ai",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/cloudcreate-ai/adsense-checklist.git"
9
+ },
10
+ "bugs": {
11
+ "url": "https://github.com/cloudcreate-ai/adsense-checklist/issues"
12
+ },
13
+ "type": "module",
14
+ "bin": {
15
+ "adsense-check": "./dist/cli.js"
16
+ },
17
+ "main": "./dist/index.js",
18
+ "types": "./dist/index.d.ts",
19
+ "exports": {
20
+ ".": {
21
+ "import": "./dist/index.js",
22
+ "types": "./dist/index.d.ts"
23
+ }
24
+ },
25
+ "files": [
26
+ "dist"
27
+ ],
28
+ "scripts": {
29
+ "build": "tsup",
30
+ "dev": "tsx src/cli.ts",
31
+ "typecheck": "tsc --noEmit",
32
+ "prepublishOnly": "npm run build"
33
+ },
34
+ "keywords": [
35
+ "adsense",
36
+ "google",
37
+ "website",
38
+ "checker",
39
+ "seo",
40
+ "audit"
41
+ ],
42
+ "license": "MIT",
43
+ "dependencies": {
44
+ "chalk": "^5.4.1",
45
+ "commander": "^13.1.0",
46
+ "dotenv": "^16.5.0",
47
+ "figures": "^6.1.0",
48
+ "playwright": "^1.52.0"
49
+ },
50
+ "devDependencies": {
51
+ "@types/node": "^22.15.0",
52
+ "tsup": "^8.4.0",
53
+ "tsx": "^4.19.0",
54
+ "typescript": "^5.8.0"
55
+ }
56
+ }