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.
- package/action.yml +130 -0
- package/examples/github-action.yml +27 -0
- package/package.json +17 -3
- package/src/audit-diff.js +184 -0
- package/src/auto-fix.js +349 -0
- package/src/batch-full-page-audit.js +151 -0
- package/src/batch-page-audit.js +140 -0
- package/src/benchmark.js +126 -0
- package/src/ci.js +81 -0
- package/src/citability.js +311 -0
- package/src/citation-check.js +157 -0
- package/src/citation-monitor.js +86 -0
- package/src/cli-site-ops-commands.js +638 -4
- package/src/compare.js +175 -0
- package/src/config.js +105 -0
- package/src/content-gap.js +170 -0
- package/src/crawlers.js +286 -0
- package/src/diagnose.js +221 -0
- package/src/eeat.js +251 -0
- package/src/freshness.js +281 -0
- package/src/full-audit.js +269 -0
- package/src/full-page-audit.js +273 -0
- package/src/heading-structure.js +287 -0
- package/src/index.d.ts +492 -0
- package/src/index.js +35 -0
- package/src/internal-links.js +298 -0
- package/src/page-audit.js +1 -1
- package/src/page-snapshot.js +198 -0
- package/src/pdf-report.js +205 -0
- package/src/platform-ready.js +238 -0
- package/src/plugins.js +126 -0
- package/src/pm-brief.js +8 -0
- package/src/pre-commit.js +62 -0
- package/src/readability.js +252 -0
- package/src/report.js +37 -12
- package/src/security.js +249 -0
- package/src/sitemap.js +323 -0
- package/src/snapshot.js +51 -0
- package/src/social-meta.js +293 -0
- package/src/topics.js +275 -0
- package/src/trend.js +102 -0
- package/src/url-onboarding.js +1 -1
- package/src/validate-llms.js +307 -0
- package/src/validate-schema.js +306 -0
- 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
|
-
|
|
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
|
};
|