geo-ai-search-optimization 1.4.2 → 2.2.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.
Files changed (45) hide show
  1. package/action.yml +130 -0
  2. package/examples/github-action.yml +27 -0
  3. package/package.json +17 -3
  4. package/src/audit-diff.js +184 -0
  5. package/src/auto-fix.js +349 -0
  6. package/src/batch-full-page-audit.js +151 -0
  7. package/src/batch-page-audit.js +140 -0
  8. package/src/benchmark.js +126 -0
  9. package/src/ci.js +81 -0
  10. package/src/citability.js +311 -0
  11. package/src/citation-check.js +157 -0
  12. package/src/citation-monitor.js +86 -0
  13. package/src/cli-site-ops-commands.js +638 -4
  14. package/src/compare.js +175 -0
  15. package/src/config.js +105 -0
  16. package/src/content-gap.js +170 -0
  17. package/src/crawlers.js +286 -0
  18. package/src/diagnose.js +221 -0
  19. package/src/eeat.js +251 -0
  20. package/src/freshness.js +281 -0
  21. package/src/full-audit.js +269 -0
  22. package/src/full-page-audit.js +273 -0
  23. package/src/heading-structure.js +287 -0
  24. package/src/index.d.ts +492 -0
  25. package/src/index.js +35 -0
  26. package/src/internal-links.js +298 -0
  27. package/src/page-audit.js +1 -1
  28. package/src/page-snapshot.js +198 -0
  29. package/src/pdf-report.js +205 -0
  30. package/src/platform-ready.js +238 -0
  31. package/src/plugins.js +126 -0
  32. package/src/pm-brief.js +8 -0
  33. package/src/pre-commit.js +62 -0
  34. package/src/readability.js +252 -0
  35. package/src/report.js +37 -12
  36. package/src/security.js +249 -0
  37. package/src/sitemap.js +323 -0
  38. package/src/snapshot.js +51 -0
  39. package/src/social-meta.js +293 -0
  40. package/src/topics.js +275 -0
  41. package/src/trend.js +102 -0
  42. package/src/url-onboarding.js +1 -1
  43. package/src/validate-llms.js +307 -0
  44. package/src/validate-schema.js +306 -0
  45. package/src/watch.js +49 -0
@@ -1,3 +1,14 @@
1
+ import { createAuditDiffFromFiles, renderAuditDiffMarkdown, writeAuditDiffOutput } from "./audit-diff.js";
2
+ import { batchAuditPages, renderBatchPageAuditMarkdown, writeBatchPageAuditOutput } from "./batch-page-audit.js";
3
+ import { saveSnapshot } from "./snapshot.js";
4
+ import { runBenchmark, renderBenchmarkMarkdown, writeBenchmarkOutput } from "./benchmark.js";
5
+ import { checkCitation, renderCitationCheckMarkdown, writeCitationCheckOutput } from "./citation-check.js";
6
+ import { monitorCitations, renderCitationMonitorMarkdown, writeCitationMonitorOutput } from "./citation-monitor.js";
7
+ import { runCiCheck, renderCiCheckMarkdown, writeCiCheckOutput } from "./ci.js";
8
+ import { analyzeContentGap, renderContentGapMarkdown, writeContentGapOutput } from "./content-gap.js";
9
+ import { initHook, renderInitHookMarkdown, writeInitHookOutput } from "./pre-commit.js";
10
+ import { startWatch } from "./watch.js";
11
+ import { buildTrend, renderTrendMarkdown, writeTrendOutput } from "./trend.js";
1
12
  import { auditProject, renderAuditMarkdown, writeAuditOutput } from "./audit.js";
2
13
  import {
3
14
  createActionCommandHandler,
@@ -20,6 +31,29 @@ import { createRewritePack, renderRewritePackMarkdown, writeRewritePackOutput }
20
31
  import { renderScanMarkdown, scanProject, writeScanOutput } from "./scan.js";
21
32
  import { createSchemaTemplate } from "./schema.js";
22
33
  import { analyzeWebsiteUrl, renderWebsiteOnboardingMarkdown, writeWebsiteOnboardingOutput } from "./url-onboarding.js";
34
+ import { analyzeCrawlers, renderCrawlersMarkdown, writeCrawlersOutput } from "./crawlers.js";
35
+ import { analyzeCitability, renderCitabilityMarkdown, writeCitabilityOutput } from "./citability.js";
36
+ import { validateLlmsTxt, renderValidateLlmsMarkdown, writeValidateLlmsOutput } from "./validate-llms.js";
37
+ import { validateSchema, renderValidateSchemaMarkdown, writeValidateSchemaOutput } from "./validate-schema.js";
38
+ import { analyzeEeat, renderEeatMarkdown, writeEeatOutput } from "./eeat.js";
39
+ import { analyzePlatformReadiness, renderPlatformReadyMarkdown, writePlatformReadyOutput } from "./platform-ready.js";
40
+ import { analyzeReadability, renderReadabilityMarkdown, writeReadabilityOutput } from "./readability.js";
41
+ import { analyzeHeadingStructure, renderHeadingStructureMarkdown, writeHeadingStructureOutput } from "./heading-structure.js";
42
+ import { analyzeInternalLinks, renderInternalLinksMarkdown, writeInternalLinksOutput } from "./internal-links.js";
43
+ import { analyzeSocialMeta, renderSocialMetaMarkdown, writeSocialMetaOutput } from "./social-meta.js";
44
+ import { initConfig } from "./config.js";
45
+ import { writePdfReport } from "./pdf-report.js";
46
+ import { fullPageAudit, renderFullPageAuditMarkdown, writeFullPageAuditOutput } from "./full-page-audit.js";
47
+ import { fullAudit, renderFullAuditMarkdown, writeFullAuditOutput } from "./full-audit.js";
48
+ import { analyzeSitemap, renderSitemapMarkdown, writeSitemapOutput } from "./sitemap.js";
49
+ import { analyzeSecurity, renderSecurityMarkdown, writeSecurityOutput } from "./security.js";
50
+ import { analyzeFreshness, renderFreshnessMarkdown, writeFreshnessOutput } from "./freshness.js";
51
+ import { analyzeTopics, renderTopicsMarkdown, writeTopicsOutput } from "./topics.js";
52
+ import { batchFullPageAudit, renderBatchFullPageAuditMarkdown, writeBatchFullPageAuditOutput } from "./batch-full-page-audit.js";
53
+ import { generateAutoFix, renderAutoFixMarkdown, writeAutoFixOutput } from "./auto-fix.js";
54
+ import { savePageSnapshot, buildPageTrend, renderPageTrendMarkdown, writePageTrendOutput } from "./page-snapshot.js";
55
+ import { diagnose, renderDiagnoseMarkdown, writeDiagnoseOutput } from "./diagnose.js";
56
+ import { comparePages, renderCompareMarkdown, writeCompareOutput } from "./compare.js";
23
57
 
24
58
  export const SITE_OPS_HELP_LINES = [
25
59
  " geo-ai-search-optimization doctor [--json]",
@@ -31,8 +65,41 @@ export const SITE_OPS_HELP_LINES = [
31
65
  " geo-ai-search-optimization repo-patch-plan <project-path> [--json] [--out <file>]",
32
66
  " geo-ai-search-optimization init-llms [target-dir] [--site-name <name>] [--site-url <url>] [--overwrite] [--json]",
33
67
  " geo-ai-search-optimization init-schema <type> [target-dir] [--site-url <url>] [--overwrite] [--json]",
34
- " geo-ai-search-optimization audit <project-path> [--json] [--out <file>] [--max-file-size <bytes>] [--max-examples <count>]",
35
- " geo-ai-search-optimization scan <project-path> [--json] [--out <file>] [--max-file-size <bytes>] [--max-examples <count>]"
68
+ " geo-ai-search-optimization audit <project-path> [--json] [--out <file>] [--save] [--data-dir <dir>] [--max-file-size <bytes>] [--max-examples <count>]",
69
+ " geo-ai-search-optimization scan <project-path> [--json] [--out <file>] [--max-file-size <bytes>] [--max-examples <count>]",
70
+ " geo-ai-search-optimization audit-diff <baseline.json> <current.json> [--json] [--out <file>]",
71
+ " geo-ai-search-optimization batch-page-audit <url1> [url2...] [--urls-file <file>] [--concurrency <n>] [--json] [--out <file>]",
72
+ " geo-ai-search-optimization trend [--data-dir <dir>] [--last <n>] [--json] [--out <file>]",
73
+ " geo-ai-search-optimization benchmark <your-url> --competitors <url1,url2,...> [--json] [--out <file>]",
74
+ " geo-ai-search-optimization content-gap <your-url> --competitors <url1,url2,...> [--json] [--out <file>]",
75
+ " geo-ai-search-optimization ci <project-path> [--min-score <n>] [--baseline <file>] [--fail-on-regression] [--json]",
76
+ " geo-ai-search-optimization watch <project-path> [--debounce <ms>]",
77
+ " geo-ai-search-optimization init-hook [--min-score <n>] [target-dir]",
78
+ " geo-ai-search-optimization citation-check <site-url> --queries <q1,q2,...> [--queries-file <file>] [--engine <perplexity|google|all>] [--json] [--out <file>]",
79
+ " geo-ai-search-optimization citation-monitor <site-url> --queries-file <file> [--save] [--json] [--out <file>]",
80
+ " geo-ai-search-optimization crawlers <url-or-file> [--strategy <open|selective|block-all>] [--json] [--out <file>]",
81
+ " geo-ai-search-optimization citability <url-or-file> [--json] [--out <file>]",
82
+ " geo-ai-search-optimization validate-llms <url-or-file> [--json] [--out <file>]",
83
+ " geo-ai-search-optimization validate-schema <url-or-file> [--json] [--out <file>]",
84
+ " geo-ai-search-optimization eeat <url-or-file> [--json] [--out <file>]",
85
+ " geo-ai-search-optimization platform-ready <url-or-file> [--json] [--out <file>]",
86
+ " geo-ai-search-optimization readability <url-or-file> [--json] [--out <file>]",
87
+ " geo-ai-search-optimization heading-structure <url-or-file> [--json] [--out <file>]",
88
+ " geo-ai-search-optimization internal-links <url-or-file> [--base-url <url>] [--json] [--out <file>]",
89
+ " geo-ai-search-optimization social-meta <url-or-file> [--json] [--out <file>]",
90
+ " geo-ai-search-optimization init-config [target-dir] [--site-name <name>] [--site-url <url>] [--overwrite] [--json]",
91
+ " geo-ai-search-optimization pdf-report <audit.json> [--out <file>] [--title <title>]",
92
+ " geo-ai-search-optimization full-page-audit <url-or-file> [--base-url <url>] [--save] [--data-dir <dir>] [--json] [--out <file>]",
93
+ " geo-ai-search-optimization full-audit <project-path> [--sample-urls <url1,url2,...>] [--max-file-size <bytes>] [--save] [--data-dir <dir>] [--json] [--out <file>]",
94
+ " geo-ai-search-optimization sitemap <url-or-file> [--json] [--out <file>]",
95
+ " geo-ai-search-optimization security <url-or-file> [--json] [--out <file>]",
96
+ " geo-ai-search-optimization freshness <url-or-file> [--json] [--out <file>]",
97
+ " geo-ai-search-optimization topics <url-or-file> [--json] [--out <file>]",
98
+ " geo-ai-search-optimization batch-full-page-audit <url1> [url2...] [--urls-file <file>] [--concurrency <n>] [--json] [--out <file>]",
99
+ " geo-ai-search-optimization auto-fix <url-or-file> [--json] [--out <file>]",
100
+ " geo-ai-search-optimization page-trend <url-or-file> [--data-dir <dir>] [--last <n>] [--json] [--out <file>]",
101
+ " geo-ai-search-optimization diagnose <url-or-dir-or-file> [--json] [--out <file>]",
102
+ " geo-ai-search-optimization compare <url-or-file-A> <url-or-file-B> [--json] [--out <file>]"
36
103
  ];
37
104
 
38
105
  const passthroughWriteOutput = async (outputPath) => outputPath;
@@ -171,7 +238,7 @@ const handleAudit = createStructuredOutputCommandHandler({
171
238
  throw new Error("audit requires a project path");
172
239
  }
173
240
 
174
- return auditProject(projectPath, {
241
+ const result = await auditProject(projectPath, {
175
242
  maxFileSize: getFlagValue(args, "--max-file-size")
176
243
  ? parsePositiveInteger(getFlagValue(args, "--max-file-size"), "--max-file-size")
177
244
  : undefined,
@@ -179,6 +246,14 @@ const handleAudit = createStructuredOutputCommandHandler({
179
246
  ? parsePositiveInteger(getFlagValue(args, "--max-examples"), "--max-examples")
180
247
  : undefined
181
248
  });
249
+
250
+ if (hasFlag(args, "--save")) {
251
+ const dataDir = getFlagValue(args, "--data-dir") || undefined;
252
+ const saved = await saveSnapshot(result, { dataDir });
253
+ process.stderr.write(`快照已保存:${saved.path}\n`);
254
+ }
255
+
256
+ return result;
182
257
  },
183
258
  renderMarkdown: (report) => `${renderAuditMarkdown(report)}\n`,
184
259
  writeOutput: writeAuditOutput,
@@ -206,6 +281,532 @@ const handleScan = createStructuredOutputCommandHandler({
206
281
  getOutputJson: (args) => hasFlag(args, "--json")
207
282
  });
208
283
 
284
+ const handleBatchPageAudit = createStructuredOutputCommandHandler({
285
+ commandLabel: "批量页面审计",
286
+ execute: async (args) => {
287
+ const nonFlags = args.filter((value) => !value.startsWith("-"));
288
+ const urlsFile = getFlagValue(args, "--urls-file");
289
+ let urls = [...nonFlags];
290
+
291
+ if (urlsFile) {
292
+ const { readFile } = await import("node:fs/promises");
293
+ const fileContent = await readFile(urlsFile, "utf8");
294
+ const fileUrls = fileContent.split("\n").map((line) => line.trim()).filter(Boolean);
295
+ urls = [...urls, ...fileUrls];
296
+ }
297
+
298
+ if (urls.length === 0) {
299
+ throw new Error("batch-page-audit requires at least one URL or file path");
300
+ }
301
+
302
+ const concurrencyValue = getFlagValue(args, "--concurrency");
303
+ return batchAuditPages(urls, {
304
+ concurrency: concurrencyValue ? parsePositiveInteger(concurrencyValue, "--concurrency") : 3
305
+ });
306
+ },
307
+ renderMarkdown: renderBatchPageAuditMarkdown,
308
+ writeOutput: writeBatchPageAuditOutput,
309
+ getOutputJson: (args) => hasFlag(args, "--json")
310
+ });
311
+
312
+ const handleAuditDiff = createStructuredOutputCommandHandler({
313
+ commandLabel: "审计对比",
314
+ execute: async (args) => {
315
+ const nonFlags = args.filter((value) => !value.startsWith("-"));
316
+ if (nonFlags.length < 2) {
317
+ throw new Error("audit-diff requires two audit JSON files: <baseline.json> <current.json>");
318
+ }
319
+ return createAuditDiffFromFiles(nonFlags[0], nonFlags[1]);
320
+ },
321
+ renderMarkdown: renderAuditDiffMarkdown,
322
+ writeOutput: writeAuditDiffOutput,
323
+ getOutputJson: (args) => hasFlag(args, "--json")
324
+ });
325
+
326
+ const handleCi = createStructuredOutputCommandHandler({
327
+ commandLabel: "CI 检查",
328
+ execute: async (args) => {
329
+ const projectPath = args.find((value) => !value.startsWith("-"));
330
+ if (!projectPath) {
331
+ throw new Error("ci requires a project path");
332
+ }
333
+ const minScoreValue = getFlagValue(args, "--min-score");
334
+ const result = await runCiCheck(projectPath, {
335
+ minScore: minScoreValue ? parsePositiveInteger(minScoreValue, "--min-score") : 40,
336
+ baseline: getFlagValue(args, "--baseline") || undefined,
337
+ failOnRegression: hasFlag(args, "--fail-on-regression")
338
+ });
339
+ if (result.exitCode !== 0) {
340
+ process.exitCode = result.exitCode;
341
+ }
342
+ return result;
343
+ },
344
+ renderMarkdown: renderCiCheckMarkdown,
345
+ writeOutput: writeCiCheckOutput,
346
+ getOutputJson: (args) => hasFlag(args, "--json")
347
+ });
348
+
349
+ const handleBenchmark = createStructuredOutputCommandHandler({
350
+ commandLabel: "竞品基准",
351
+ execute: async (args) => {
352
+ const ownUrl = args.find((value) => !value.startsWith("-"));
353
+ if (!ownUrl) {
354
+ throw new Error("benchmark requires your URL as the first argument");
355
+ }
356
+ const competitorsRaw = getFlagValue(args, "--competitors");
357
+ if (!competitorsRaw) {
358
+ throw new Error("benchmark requires --competitors <url1,url2,...>");
359
+ }
360
+ const competitorUrls = competitorsRaw.split(",").map((u) => u.trim()).filter(Boolean);
361
+ return runBenchmark(ownUrl, competitorUrls);
362
+ },
363
+ renderMarkdown: renderBenchmarkMarkdown,
364
+ writeOutput: writeBenchmarkOutput,
365
+ getOutputJson: (args) => hasFlag(args, "--json")
366
+ });
367
+
368
+ const handleContentGap = createStructuredOutputCommandHandler({
369
+ commandLabel: "内容缺口",
370
+ execute: async (args) => {
371
+ const firstArg = args.find((value) => !value.startsWith("-"));
372
+ if (!firstArg) {
373
+ throw new Error("content-gap requires a URL or benchmark JSON file");
374
+ }
375
+
376
+ // Check if it's a benchmark JSON file
377
+ if (firstArg.endsWith(".json")) {
378
+ return analyzeContentGap(null, [], { benchmarkJson: firstArg });
379
+ }
380
+
381
+ const competitorsRaw = getFlagValue(args, "--competitors");
382
+ if (!competitorsRaw) {
383
+ throw new Error("content-gap requires --competitors <url1,url2,...> or a benchmark JSON file");
384
+ }
385
+ const competitorUrls = competitorsRaw.split(",").map((u) => u.trim()).filter(Boolean);
386
+ return analyzeContentGap(firstArg, competitorUrls);
387
+ },
388
+ renderMarkdown: renderContentGapMarkdown,
389
+ writeOutput: writeContentGapOutput,
390
+ getOutputJson: (args) => hasFlag(args, "--json")
391
+ });
392
+
393
+ const handleTrend = createStructuredOutputCommandHandler({
394
+ commandLabel: "趋势",
395
+ execute: async (args) => {
396
+ const lastValue = getFlagValue(args, "--last");
397
+ return buildTrend({
398
+ dataDir: getFlagValue(args, "--data-dir") || undefined,
399
+ last: lastValue ? parsePositiveInteger(lastValue, "--last") : 0
400
+ });
401
+ },
402
+ renderMarkdown: renderTrendMarkdown,
403
+ writeOutput: writeTrendOutput,
404
+ getOutputJson: (args) => hasFlag(args, "--json")
405
+ });
406
+
407
+ async function parseQueries(args) {
408
+ const queriesRaw = getFlagValue(args, "--queries");
409
+ const queriesFile = getFlagValue(args, "--queries-file");
410
+ let queries = [];
411
+
412
+ if (queriesRaw) {
413
+ queries = queriesRaw.split(",").map((q) => q.trim()).filter(Boolean);
414
+ }
415
+
416
+ if (queriesFile) {
417
+ const { readFile } = await import("node:fs/promises");
418
+ const fileContent = await readFile(queriesFile, "utf8");
419
+ const fileQueries = fileContent.split("\n").map((line) => line.trim()).filter(Boolean);
420
+ queries = [...queries, ...fileQueries];
421
+ }
422
+
423
+ return queries;
424
+ }
425
+
426
+ function parseEngines(args) {
427
+ const engineRaw = getFlagValue(args, "--engine");
428
+ if (!engineRaw || engineRaw === "all") return ["perplexity", "google"];
429
+ return [engineRaw];
430
+ }
431
+
432
+ const handleCitationCheck = createStructuredOutputCommandHandler({
433
+ commandLabel: "引用检查",
434
+ execute: async (args) => {
435
+ const siteUrl = args.find((value) => !value.startsWith("-"));
436
+ if (!siteUrl) throw new Error("citation-check requires a site URL");
437
+ const queries = await parseQueries(args);
438
+ if (queries.length === 0) throw new Error("citation-check requires --queries or --queries-file");
439
+ return checkCitation(siteUrl, queries, { engines: parseEngines(args) });
440
+ },
441
+ renderMarkdown: renderCitationCheckMarkdown,
442
+ writeOutput: writeCitationCheckOutput,
443
+ getOutputJson: (args) => hasFlag(args, "--json")
444
+ });
445
+
446
+ const handleCitationMonitor = createStructuredOutputCommandHandler({
447
+ commandLabel: "引用监控",
448
+ execute: async (args) => {
449
+ const siteUrl = args.find((value) => !value.startsWith("-"));
450
+ if (!siteUrl) throw new Error("citation-monitor requires a site URL");
451
+ const queries = await parseQueries(args);
452
+ if (queries.length === 0) throw new Error("citation-monitor requires --queries-file");
453
+ return monitorCitations(siteUrl, queries, {
454
+ engines: parseEngines(args),
455
+ save: hasFlag(args, "--save"),
456
+ dataDir: getFlagValue(args, "--data-dir") || undefined
457
+ });
458
+ },
459
+ renderMarkdown: renderCitationMonitorMarkdown,
460
+ writeOutput: writeCitationMonitorOutput,
461
+ getOutputJson: (args) => hasFlag(args, "--json")
462
+ });
463
+
464
+ const handleWatch = async (args) => {
465
+ const projectPath = args.find((value) => !value.startsWith("-"));
466
+ if (!projectPath) {
467
+ throw new Error("watch requires a project path");
468
+ }
469
+ const debounceValue = getFlagValue(args, "--debounce");
470
+ await startWatch(projectPath, {
471
+ debounce: debounceValue ? parsePositiveInteger(debounceValue, "--debounce") : 500
472
+ });
473
+ };
474
+
475
+ const handleInitHook = createStructuredOutputCommandHandler({
476
+ commandLabel: "init-hook",
477
+ execute: async (args) => {
478
+ const minScoreValue = getFlagValue(args, "--min-score");
479
+ const targetDir = args.find((value) => !value.startsWith("-")) || ".";
480
+ return initHook({
481
+ minScore: minScoreValue ? parsePositiveInteger(minScoreValue, "--min-score") : 40,
482
+ targetDir
483
+ });
484
+ },
485
+ renderMarkdown: renderInitHookMarkdown,
486
+ writeOutput: writeInitHookOutput,
487
+ getOutputJson: (args) => hasFlag(args, "--json")
488
+ });
489
+
490
+ const handleCrawlers = createStructuredOutputCommandHandler({
491
+ commandLabel: "AI crawler analysis",
492
+ execute: async (args) => {
493
+ const input = args.find((value) => !value.startsWith("-"));
494
+ if (!input) throw new Error("crawlers requires a URL or robots.txt file path");
495
+ return analyzeCrawlers(input, { strategy: getFlagValue(args, "--strategy") || "open" });
496
+ },
497
+ renderMarkdown: renderCrawlersMarkdown,
498
+ writeOutput: writeCrawlersOutput,
499
+ getOutputJson: (args) => hasFlag(args, "--json")
500
+ });
501
+
502
+ const handleCitability = createStructuredOutputCommandHandler({
503
+ commandLabel: "citability analysis",
504
+ execute: async (args) => {
505
+ const input = args.find((value) => !value.startsWith("-"));
506
+ if (!input) throw new Error("citability requires a URL or file path");
507
+ return analyzeCitability(input);
508
+ },
509
+ renderMarkdown: renderCitabilityMarkdown,
510
+ writeOutput: writeCitabilityOutput,
511
+ getOutputJson: (args) => hasFlag(args, "--json")
512
+ });
513
+
514
+ const handleValidateLlms = createStructuredOutputCommandHandler({
515
+ commandLabel: "llms.txt validation",
516
+ execute: async (args) => {
517
+ const input = args.find((value) => !value.startsWith("-"));
518
+ if (!input) throw new Error("validate-llms requires a URL or llms.txt file path");
519
+ return validateLlmsTxt(input);
520
+ },
521
+ renderMarkdown: renderValidateLlmsMarkdown,
522
+ writeOutput: writeValidateLlmsOutput,
523
+ getOutputJson: (args) => hasFlag(args, "--json")
524
+ });
525
+
526
+ const handleValidateSchema = createStructuredOutputCommandHandler({
527
+ commandLabel: "schema validation",
528
+ execute: async (args) => {
529
+ const input = args.find((value) => !value.startsWith("-"));
530
+ if (!input) throw new Error("validate-schema requires a URL or file path");
531
+ return validateSchema(input);
532
+ },
533
+ renderMarkdown: renderValidateSchemaMarkdown,
534
+ writeOutput: writeValidateSchemaOutput,
535
+ getOutputJson: (args) => hasFlag(args, "--json")
536
+ });
537
+
538
+ const handleEeat = createStructuredOutputCommandHandler({
539
+ commandLabel: "E-E-A-T analysis",
540
+ execute: async (args) => {
541
+ const input = args.find((value) => !value.startsWith("-"));
542
+ if (!input) throw new Error("eeat requires a URL or file path");
543
+ return analyzeEeat(input);
544
+ },
545
+ renderMarkdown: renderEeatMarkdown,
546
+ writeOutput: writeEeatOutput,
547
+ getOutputJson: (args) => hasFlag(args, "--json")
548
+ });
549
+
550
+ const handlePlatformReady = createStructuredOutputCommandHandler({
551
+ commandLabel: "platform readiness",
552
+ execute: async (args) => {
553
+ const input = args.find((value) => !value.startsWith("-"));
554
+ if (!input) throw new Error("platform-ready requires a URL or file path");
555
+ return analyzePlatformReadiness(input);
556
+ },
557
+ renderMarkdown: renderPlatformReadyMarkdown,
558
+ writeOutput: writePlatformReadyOutput,
559
+ getOutputJson: (args) => hasFlag(args, "--json")
560
+ });
561
+
562
+ const handleReadability = createStructuredOutputCommandHandler({
563
+ commandLabel: "readability analysis",
564
+ execute: async (args) => {
565
+ const input = args.find((value) => !value.startsWith("-"));
566
+ if (!input) throw new Error("readability requires a URL or file path");
567
+ return analyzeReadability(input);
568
+ },
569
+ renderMarkdown: renderReadabilityMarkdown,
570
+ writeOutput: writeReadabilityOutput,
571
+ getOutputJson: (args) => hasFlag(args, "--json")
572
+ });
573
+
574
+ const handleHeadingStructure = createStructuredOutputCommandHandler({
575
+ commandLabel: "heading structure",
576
+ execute: async (args) => {
577
+ const input = args.find((value) => !value.startsWith("-"));
578
+ if (!input) throw new Error("heading-structure requires a URL or file path");
579
+ return analyzeHeadingStructure(input);
580
+ },
581
+ renderMarkdown: renderHeadingStructureMarkdown,
582
+ writeOutput: writeHeadingStructureOutput,
583
+ getOutputJson: (args) => hasFlag(args, "--json")
584
+ });
585
+
586
+ const handleInternalLinks = createStructuredOutputCommandHandler({
587
+ commandLabel: "internal links",
588
+ execute: async (args) => {
589
+ const input = args.find((value) => !value.startsWith("-"));
590
+ if (!input) throw new Error("internal-links requires a URL or file path");
591
+ return analyzeInternalLinks(input, { baseUrl: getFlagValue(args, "--base-url") });
592
+ },
593
+ renderMarkdown: renderInternalLinksMarkdown,
594
+ writeOutput: writeInternalLinksOutput,
595
+ getOutputJson: (args) => hasFlag(args, "--json")
596
+ });
597
+
598
+ const handleSocialMeta = createStructuredOutputCommandHandler({
599
+ commandLabel: "social meta",
600
+ execute: async (args) => {
601
+ const input = args.find((value) => !value.startsWith("-"));
602
+ if (!input) throw new Error("social-meta requires a URL or file path");
603
+ return analyzeSocialMeta(input);
604
+ },
605
+ renderMarkdown: renderSocialMetaMarkdown,
606
+ writeOutput: writeSocialMetaOutput,
607
+ getOutputJson: (args) => hasFlag(args, "--json")
608
+ });
609
+
610
+ const handleInitConfig = createActionCommandHandler({
611
+ execute: async (args) =>
612
+ initConfig({
613
+ targetDir: args.find((value) => !value.startsWith("-")) || ".",
614
+ siteName: getFlagValue(args, "--site-name"),
615
+ siteUrl: getFlagValue(args, "--site-url"),
616
+ overwrite: hasFlag(args, "--overwrite")
617
+ }),
618
+ getOutputJson: (args) => hasFlag(args, "--json"),
619
+ renderText: (result) => `Created config at ${result.outputPath}\n`
620
+ });
621
+
622
+ const handlePdfReport = createActionCommandHandler({
623
+ execute: async (args) => {
624
+ const input = args.find((value) => !value.startsWith("-"));
625
+ if (!input) throw new Error("pdf-report requires an audit JSON file path");
626
+ return writePdfReport(input, getFlagValue(args, "--out"), {
627
+ title: getFlagValue(args, "--title")
628
+ });
629
+ },
630
+ getOutputJson: (args) => hasFlag(args, "--json"),
631
+ renderText: (result) => `Generated PDF-ready HTML report at ${result.outputPath}\n`
632
+ });
633
+
634
+ const handleSitemap = createStructuredOutputCommandHandler({
635
+ commandLabel: "sitemap analysis",
636
+ execute: async (args) => {
637
+ const input = args.find((value) => !value.startsWith("-"));
638
+ if (!input) throw new Error("sitemap requires a URL or sitemap file path");
639
+ return analyzeSitemap(input);
640
+ },
641
+ renderMarkdown: renderSitemapMarkdown,
642
+ writeOutput: writeSitemapOutput,
643
+ getOutputJson: (args) => hasFlag(args, "--json")
644
+ });
645
+
646
+ const handleSecurity = createStructuredOutputCommandHandler({
647
+ commandLabel: "security analysis",
648
+ execute: async (args) => {
649
+ const input = args.find((value) => !value.startsWith("-"));
650
+ if (!input) throw new Error("security requires a URL or file path");
651
+ return analyzeSecurity(input);
652
+ },
653
+ renderMarkdown: renderSecurityMarkdown,
654
+ writeOutput: writeSecurityOutput,
655
+ getOutputJson: (args) => hasFlag(args, "--json")
656
+ });
657
+
658
+ const handleFreshness = createStructuredOutputCommandHandler({
659
+ commandLabel: "freshness analysis",
660
+ execute: async (args) => {
661
+ const input = args.find((value) => !value.startsWith("-"));
662
+ if (!input) throw new Error("freshness requires a URL or file path");
663
+ return analyzeFreshness(input);
664
+ },
665
+ renderMarkdown: renderFreshnessMarkdown,
666
+ writeOutput: writeFreshnessOutput,
667
+ getOutputJson: (args) => hasFlag(args, "--json")
668
+ });
669
+
670
+ const handleTopics = createStructuredOutputCommandHandler({
671
+ commandLabel: "topic analysis",
672
+ execute: async (args) => {
673
+ const input = args.find((value) => !value.startsWith("-"));
674
+ if (!input) throw new Error("topics requires a URL or file path");
675
+ return analyzeTopics(input);
676
+ },
677
+ renderMarkdown: renderTopicsMarkdown,
678
+ writeOutput: writeTopicsOutput,
679
+ getOutputJson: (args) => hasFlag(args, "--json")
680
+ });
681
+
682
+ const handleBatchFullPageAudit = createStructuredOutputCommandHandler({
683
+ commandLabel: "batch full page audit",
684
+ execute: async (args) => {
685
+ const nonFlags = args.filter((value) => !value.startsWith("-"));
686
+ const urlsFile = getFlagValue(args, "--urls-file");
687
+ let urls = [...nonFlags];
688
+
689
+ if (urlsFile) {
690
+ const { readFile } = await import("node:fs/promises");
691
+ const fileContent = await readFile(urlsFile, "utf8");
692
+ const fileUrls = fileContent.split("\n").map((line) => line.trim()).filter(Boolean);
693
+ urls = [...urls, ...fileUrls];
694
+ }
695
+
696
+ if (urls.length === 0) throw new Error("batch-full-page-audit requires at least one URL");
697
+
698
+ const concurrencyValue = getFlagValue(args, "--concurrency");
699
+ return batchFullPageAudit(urls, {
700
+ concurrency: concurrencyValue ? parsePositiveInteger(concurrencyValue, "--concurrency") : 2
701
+ });
702
+ },
703
+ renderMarkdown: renderBatchFullPageAuditMarkdown,
704
+ writeOutput: writeBatchFullPageAuditOutput,
705
+ getOutputJson: (args) => hasFlag(args, "--json")
706
+ });
707
+
708
+ const handleDiagnose = createStructuredOutputCommandHandler({
709
+ commandLabel: "diagnose",
710
+ execute: async (args) => {
711
+ const input = args.find((value) => !value.startsWith("-"));
712
+ if (!input) throw new Error("diagnose requires a URL, file path, or directory");
713
+ return diagnose(input);
714
+ },
715
+ renderMarkdown: renderDiagnoseMarkdown,
716
+ writeOutput: writeDiagnoseOutput,
717
+ getOutputJson: (args) => hasFlag(args, "--json")
718
+ });
719
+
720
+ const handleCompare = createStructuredOutputCommandHandler({
721
+ commandLabel: "compare",
722
+ execute: async (args) => {
723
+ const nonFlags = args.filter((value) => !value.startsWith("-"));
724
+ if (nonFlags.length < 2) throw new Error("compare requires two URLs or file paths: <A> <B>");
725
+ return comparePages(nonFlags[0], nonFlags[1]);
726
+ },
727
+ renderMarkdown: renderCompareMarkdown,
728
+ writeOutput: writeCompareOutput,
729
+ getOutputJson: (args) => hasFlag(args, "--json")
730
+ });
731
+
732
+ const handleAutoFix = createStructuredOutputCommandHandler({
733
+ commandLabel: "auto-fix",
734
+ execute: async (args) => {
735
+ const input = args.find((value) => !value.startsWith("-"));
736
+ if (!input) throw new Error("auto-fix requires a URL or file path");
737
+ return generateAutoFix(input);
738
+ },
739
+ renderMarkdown: renderAutoFixMarkdown,
740
+ writeOutput: writeAutoFixOutput,
741
+ getOutputJson: (args) => hasFlag(args, "--json")
742
+ });
743
+
744
+ const handlePageTrend = createStructuredOutputCommandHandler({
745
+ commandLabel: "page trend",
746
+ execute: async (args) => {
747
+ const input = args.find((value) => !value.startsWith("-"));
748
+ if (!input) throw new Error("page-trend requires a URL or file path (same input used for full-page-audit)");
749
+ const lastValue = getFlagValue(args, "--last");
750
+ return buildPageTrend(input, {
751
+ dataDir: getFlagValue(args, "--data-dir") || undefined,
752
+ last: lastValue ? parsePositiveInteger(lastValue, "--last") : 0
753
+ });
754
+ },
755
+ renderMarkdown: renderPageTrendMarkdown,
756
+ writeOutput: writePageTrendOutput,
757
+ getOutputJson: (args) => hasFlag(args, "--json")
758
+ });
759
+
760
+ const handleFullPageAudit = createStructuredOutputCommandHandler({
761
+ commandLabel: "full page audit",
762
+ execute: async (args) => {
763
+ const input = args.find((value) => !value.startsWith("-"));
764
+ if (!input) throw new Error("full-page-audit requires a URL or file path");
765
+ const result = await fullPageAudit(input, { baseUrl: getFlagValue(args, "--base-url") });
766
+
767
+ if (hasFlag(args, "--save")) {
768
+ const saved = await savePageSnapshot(result, {
769
+ dataDir: getFlagValue(args, "--data-dir") || undefined
770
+ });
771
+ process.stderr.write(`快照已保存:${saved.path}\n`);
772
+ }
773
+
774
+ return result;
775
+ },
776
+ renderMarkdown: renderFullPageAuditMarkdown,
777
+ writeOutput: writeFullPageAuditOutput,
778
+ getOutputJson: (args) => hasFlag(args, "--json")
779
+ });
780
+
781
+ const handleFullAudit = createStructuredOutputCommandHandler({
782
+ commandLabel: "full audit",
783
+ execute: async (args) => {
784
+ const projectPath = args.find((value) => !value.startsWith("-"));
785
+ if (!projectPath) throw new Error("full-audit requires a project path");
786
+
787
+ const sampleUrlsRaw = getFlagValue(args, "--sample-urls");
788
+ const sampleUrls = sampleUrlsRaw ? sampleUrlsRaw.split(",").map((u) => u.trim()).filter(Boolean) : [];
789
+
790
+ const result = await fullAudit(projectPath, {
791
+ sampleUrls,
792
+ maxFileSize: getFlagValue(args, "--max-file-size")
793
+ ? parsePositiveInteger(getFlagValue(args, "--max-file-size"), "--max-file-size")
794
+ : undefined
795
+ });
796
+
797
+ if (hasFlag(args, "--save")) {
798
+ const dataDir = getFlagValue(args, "--data-dir") || undefined;
799
+ const saved = await saveSnapshot(result, { dataDir });
800
+ process.stderr.write(`快照已保存:${saved.path}\n`);
801
+ }
802
+
803
+ return result;
804
+ },
805
+ renderMarkdown: renderFullAuditMarkdown,
806
+ writeOutput: writeFullAuditOutput,
807
+ getOutputJson: (args) => hasFlag(args, "--json")
808
+ });
809
+
209
810
  export const SITE_OPS_COMMAND_HANDLERS = {
210
811
  doctor: handleDoctor,
211
812
  "quick-start": handleQuickStart,
@@ -217,5 +818,38 @@ export const SITE_OPS_COMMAND_HANDLERS = {
217
818
  "init-llms": handleInitLlms,
218
819
  "init-schema": handleInitSchema,
219
820
  audit: handleAudit,
220
- scan: handleScan
821
+ scan: handleScan,
822
+ "audit-diff": handleAuditDiff,
823
+ "batch-page-audit": handleBatchPageAudit,
824
+ trend: handleTrend,
825
+ benchmark: handleBenchmark,
826
+ "content-gap": handleContentGap,
827
+ ci: handleCi,
828
+ watch: handleWatch,
829
+ "init-hook": handleInitHook,
830
+ "citation-check": handleCitationCheck,
831
+ "citation-monitor": handleCitationMonitor,
832
+ crawlers: handleCrawlers,
833
+ citability: handleCitability,
834
+ "validate-llms": handleValidateLlms,
835
+ "validate-schema": handleValidateSchema,
836
+ eeat: handleEeat,
837
+ "platform-ready": handlePlatformReady,
838
+ readability: handleReadability,
839
+ "heading-structure": handleHeadingStructure,
840
+ "internal-links": handleInternalLinks,
841
+ "social-meta": handleSocialMeta,
842
+ "init-config": handleInitConfig,
843
+ "pdf-report": handlePdfReport,
844
+ "full-page-audit": handleFullPageAudit,
845
+ "full-audit": handleFullAudit,
846
+ sitemap: handleSitemap,
847
+ security: handleSecurity,
848
+ freshness: handleFreshness,
849
+ topics: handleTopics,
850
+ "batch-full-page-audit": handleBatchFullPageAudit,
851
+ "auto-fix": handleAutoFix,
852
+ "page-trend": handlePageTrend,
853
+ diagnose: handleDiagnose,
854
+ compare: handleCompare
221
855
  };