geo-ai-search-optimization 1.3.13 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -63,6 +63,24 @@ geo-ai-search-optimization agent-orchestrator ./reports/agent-playbook-pack.json
63
63
  - 做完之后下一条是什么
64
64
  - 可直接复制给 agent 的 orchestrator prompt
65
65
 
66
+ ## Page Audit 命令
67
+
68
+ 如果你不想一次看整站,而是想先判断“这个单页为什么不容易被生成式搜索理解与引用”,可以直接用 `page-audit`:
69
+
70
+ ```bash
71
+ geo-ai-search-optimization page-audit https://example.com/blog/geo-guide
72
+ geo-ai-search-optimization page-audit ./content/posts/geo-guide.mdx
73
+ geo-ai-search-optimization page-audit ./content/posts/geo-guide.mdx --json --out ./reports/page-audit.json
74
+ ```
75
+
76
+ 它会输出:
77
+
78
+ - 单页 GEO 分数
79
+ - 页面级问题区域
80
+ - 推荐新增模块
81
+ - rewrite brief
82
+ - 可直接交给 agent 的 page-level prompt
83
+
66
84
  ## Agent Resume 命令
67
85
 
68
86
  如果 GEO 工作已经做过一轮或多轮,你不想让下一个 agent 从头重新判断,而是想让它从最近一个可靠恢复点继续,可以直接用 `agent-resume`:
@@ -931,6 +949,20 @@ geo-ai-search-optimization help
931
949
  - `agent-runbook / agent-executor / agent-batch-executor / agent-progress-tracker / agent-status-board / agent-checkpoint / agent-decision-log / agent-retrospective / agent-playbook-pack` 已统一进入 registry
932
950
  - CLI 距离“所有命令族都使用显式 registry metadata”又更近一步
933
951
 
952
+ ## New in 1.3.14
953
+
954
+ - 把 registry metadata 推广到 `planning-delivery` 命令族
955
+ - `src/cli-planning-delivery-commands.js` 现在由同一份 registry 同时生成 help lines 和 handlers
956
+ - `agent-handoff / apply-plan / completion-report / handoff-bundle / share-pack / export-pack / html-pack / publish-pack / exec-summary / fix-plan / owner-board / meeting-pack / pm-brief / roadmap / report` 已统一进入 registry
957
+ - CLI 更接近所有命令族都使用显式 registry metadata 的结构
958
+
959
+ ## New in 1.4.0
960
+
961
+ - 新增 `page-audit`
962
+ - 支持对单个 URL 或本地单页文件做 GEO 页面级审计
963
+ - 输出页面分数、问题区域、推荐新增模块、rewrite brief 与 agent prompt
964
+ - 产品能力从“站点级诊断”扩展到“页面级修复入口”
965
+
934
966
  ## New in 1.2.20
935
967
 
936
968
  - 新增 `agent-continue`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "geo-ai-search-optimization",
3
- "version": "1.3.13",
3
+ "version": "1.4.0",
4
4
  "description": "Install and run a Generative Engine Optimization (GEO)-first, SEO-supported Codex skill for website optimization.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,6 +18,7 @@ import { generateReport, writeReportOutput } from "./report.js";
18
18
  import { createRoadmap, renderRoadmapMarkdown, writeRoadmapOutput } from "./roadmap.js";
19
19
  import { createSharePack, renderSharePackMarkdown, writeSharePackOutput } from "./share-pack.js";
20
20
  import {
21
+ buildCommandRegistry,
21
22
  buildFormatOptions,
22
23
  buildTaskFormatOutputDirOptions,
23
24
  buildTaskOutputDirOptions,
@@ -29,24 +30,6 @@ import {
29
30
  parsePositiveInteger
30
31
  } from "./cli-shared.js";
31
32
 
32
- export const PLANNING_DELIVERY_HELP_LINES = [
33
- " geo-ai-search-optimization agent-handoff <input> [--format <markdown|json>] [--out <file>]",
34
- " geo-ai-search-optimization apply-plan <input> [--task <id>] [--format <markdown|json>] [--out <file>]",
35
- " geo-ai-search-optimization completion-report <input> [--format <markdown|json>] [--out <file>]",
36
- " geo-ai-search-optimization handoff-bundle <input> [--task <id>] [--format <markdown|json>] [--out <file>]",
37
- " geo-ai-search-optimization share-pack <input> [--task <id>] [--format <markdown|json>] [--out <file>]",
38
- " geo-ai-search-optimization export-pack <input> [--task <id>] [--format <markdown|json>] [--out-dir <dir>]",
39
- " geo-ai-search-optimization html-pack <input> [--task <id>] [--out-dir <dir>]",
40
- " geo-ai-search-optimization publish-pack <input> [--task <id>] [--out-dir <dir>]",
41
- " geo-ai-search-optimization exec-summary <input> [--format <markdown|json>] [--out <file>]",
42
- " geo-ai-search-optimization fix-plan <input> [--format <markdown|json>] [--out <file>]",
43
- " geo-ai-search-optimization owner-board <input> [--format <markdown|json>] [--out <file>]",
44
- " geo-ai-search-optimization meeting-pack <input> [--format <markdown|json>] [--out <file>]",
45
- " geo-ai-search-optimization pm-brief <input> [--format <markdown|json>] [--out <file>]",
46
- " geo-ai-search-optimization roadmap <input> [--format <markdown|json>] [--out <file>]",
47
- " geo-ai-search-optimization report <input> [--mode <auto|audit|onboarding|scan>] [--format <markdown|html|json>] [--out <file>]"
48
- ];
49
-
50
33
  const handleAgentHandoff = createArtifactCommandHandler({
51
34
  commandName: "agent-handoff",
52
35
  commandLabel: "agent handoff",
@@ -184,20 +167,83 @@ const handleReport = createContentCommandHandler({
184
167
  writeOutput: writeReportOutput
185
168
  });
186
169
 
187
- export const PLANNING_DELIVERY_COMMAND_HANDLERS = {
188
- "agent-handoff": handleAgentHandoff,
189
- "apply-plan": handleApplyPlan,
190
- "completion-report": handleCompletionReport,
191
- "handoff-bundle": handleHandoffBundle,
192
- "share-pack": handleSharePack,
193
- "export-pack": handleExportPack,
194
- "html-pack": handleHtmlPack,
195
- "publish-pack": handlePublishPack,
196
- "exec-summary": handleExecSummary,
197
- "fix-plan": handleFixPlan,
198
- "owner-board": handleOwnerBoard,
199
- "meeting-pack": handleMeetingPack,
200
- "pm-brief": handlePmBrief,
201
- roadmap: handleRoadmap,
202
- report: handleReport
203
- };
170
+ const PLANNING_DELIVERY_COMMAND_REGISTRY = buildCommandRegistry([
171
+ {
172
+ name: "agent-handoff",
173
+ help: " geo-ai-search-optimization agent-handoff <input> [--format <markdown|json>] [--out <file>]",
174
+ handler: handleAgentHandoff
175
+ },
176
+ {
177
+ name: "apply-plan",
178
+ help: " geo-ai-search-optimization apply-plan <input> [--task <id>] [--format <markdown|json>] [--out <file>]",
179
+ handler: handleApplyPlan
180
+ },
181
+ {
182
+ name: "completion-report",
183
+ help: " geo-ai-search-optimization completion-report <input> [--format <markdown|json>] [--out <file>]",
184
+ handler: handleCompletionReport
185
+ },
186
+ {
187
+ name: "handoff-bundle",
188
+ help: " geo-ai-search-optimization handoff-bundle <input> [--task <id>] [--format <markdown|json>] [--out <file>]",
189
+ handler: handleHandoffBundle
190
+ },
191
+ {
192
+ name: "share-pack",
193
+ help: " geo-ai-search-optimization share-pack <input> [--task <id>] [--format <markdown|json>] [--out <file>]",
194
+ handler: handleSharePack
195
+ },
196
+ {
197
+ name: "export-pack",
198
+ help: " geo-ai-search-optimization export-pack <input> [--task <id>] [--format <markdown|json>] [--out-dir <dir>]",
199
+ handler: handleExportPack
200
+ },
201
+ {
202
+ name: "html-pack",
203
+ help: " geo-ai-search-optimization html-pack <input> [--task <id>] [--out-dir <dir>]",
204
+ handler: handleHtmlPack
205
+ },
206
+ {
207
+ name: "publish-pack",
208
+ help: " geo-ai-search-optimization publish-pack <input> [--task <id>] [--out-dir <dir>]",
209
+ handler: handlePublishPack
210
+ },
211
+ {
212
+ name: "exec-summary",
213
+ help: " geo-ai-search-optimization exec-summary <input> [--format <markdown|json>] [--out <file>]",
214
+ handler: handleExecSummary
215
+ },
216
+ {
217
+ name: "fix-plan",
218
+ help: " geo-ai-search-optimization fix-plan <input> [--format <markdown|json>] [--out <file>]",
219
+ handler: handleFixPlan
220
+ },
221
+ {
222
+ name: "owner-board",
223
+ help: " geo-ai-search-optimization owner-board <input> [--format <markdown|json>] [--out <file>]",
224
+ handler: handleOwnerBoard
225
+ },
226
+ {
227
+ name: "meeting-pack",
228
+ help: " geo-ai-search-optimization meeting-pack <input> [--format <markdown|json>] [--out <file>]",
229
+ handler: handleMeetingPack
230
+ },
231
+ {
232
+ name: "pm-brief",
233
+ help: " geo-ai-search-optimization pm-brief <input> [--format <markdown|json>] [--out <file>]",
234
+ handler: handlePmBrief
235
+ },
236
+ {
237
+ name: "roadmap",
238
+ help: " geo-ai-search-optimization roadmap <input> [--format <markdown|json>] [--out <file>]",
239
+ handler: handleRoadmap
240
+ },
241
+ {
242
+ name: "report",
243
+ help: " geo-ai-search-optimization report <input> [--mode <auto|audit|onboarding|scan>] [--format <markdown|html|json>] [--out <file>]",
244
+ handler: handleReport
245
+ }
246
+ ]);
247
+
248
+ export const PLANNING_DELIVERY_HELP_LINES = PLANNING_DELIVERY_COMMAND_REGISTRY.helpLines;
249
+ export const PLANNING_DELIVERY_COMMAND_HANDLERS = PLANNING_DELIVERY_COMMAND_REGISTRY.handlers;
@@ -13,6 +13,7 @@ import {
13
13
  writeInteractiveOnboardingOutput
14
14
  } from "./interactive-onboarding.js";
15
15
  import { createLlmsTxt } from "./llms-txt.js";
16
+ import { auditPage, renderPageAuditMarkdown, writePageAuditOutput } from "./page-audit.js";
16
17
  import { createQuickStartPlan, renderQuickStartMarkdown, writeQuickStartOutput } from "./quick-start.js";
17
18
  import { renderScanMarkdown, scanProject, writeScanOutput } from "./scan.js";
18
19
  import { createSchemaTemplate } from "./schema.js";
@@ -23,6 +24,7 @@ export const SITE_OPS_HELP_LINES = [
23
24
  " geo-ai-search-optimization quick-start [--json] [--out <file>]",
24
25
  " geo-ai-search-optimization onboard [--url <website-url>] [--goal <goal>] [--existing-assets <list>] [--json] [--out <file>]",
25
26
  " geo-ai-search-optimization onboard-url <website-url> [--json] [--out <file>]",
27
+ " geo-ai-search-optimization page-audit <url-or-file> [--json] [--out <file>]",
26
28
  " geo-ai-search-optimization init-llms [target-dir] [--site-name <name>] [--site-url <url>] [--overwrite] [--json]",
27
29
  " geo-ai-search-optimization init-schema <type> [target-dir] [--site-url <url>] [--overwrite] [--json]",
28
30
  " geo-ai-search-optimization audit <project-path> [--json] [--out <file>] [--max-file-size <bytes>] [--max-examples <count>]",
@@ -75,6 +77,21 @@ const handleOnboardUrl = createStructuredOutputCommandHandler({
75
77
  getOutputJson: (args) => hasFlag(args, "--json")
76
78
  });
77
79
 
80
+ const handlePageAudit = createStructuredOutputCommandHandler({
81
+ commandLabel: "单页审计",
82
+ execute: async (args) => {
83
+ const input = args.find((value) => !value.startsWith("-"));
84
+ if (!input) {
85
+ throw new Error("page-audit requires a URL or file path");
86
+ }
87
+
88
+ return auditPage(input, {});
89
+ },
90
+ renderMarkdown: (report) => `${renderPageAuditMarkdown(report)}\n`,
91
+ writeOutput: writePageAuditOutput,
92
+ getOutputJson: (args) => hasFlag(args, "--json")
93
+ });
94
+
78
95
  const handleInitLlms = createActionCommandHandler({
79
96
  execute: async (args) =>
80
97
  createLlmsTxt({
@@ -153,6 +170,7 @@ export const SITE_OPS_COMMAND_HANDLERS = {
153
170
  "quick-start": handleQuickStart,
154
171
  onboard: handleInteractiveOnboard,
155
172
  "onboard-url": handleOnboardUrl,
173
+ "page-audit": handlePageAudit,
156
174
  "init-llms": handleInitLlms,
157
175
  "init-schema": handleInitSchema,
158
176
  audit: handleAudit,
package/src/index.js CHANGED
@@ -34,6 +34,7 @@ export { installSkill } from "./install-skill.js";
34
34
  export { runCli } from "./cli.js";
35
35
  export { runDoctor, renderDoctorMarkdown } from "./doctor.js";
36
36
  export { createLlmsTxt } from "./llms-txt.js";
37
+ export { auditPage, renderPageAuditMarkdown, writePageAuditOutput } from "./page-audit.js";
37
38
  export { createExecSummary, renderExecSummaryMarkdown, writeExecSummaryOutput } from "./exec-summary.js";
38
39
  export { createExportPack, renderExportPackMarkdown, writeExportPack } from "./export-pack.js";
39
40
  export { createMeetingPack, renderMeetingPackMarkdown, writeMeetingPackOutput } from "./meeting-pack.js";
@@ -0,0 +1,464 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { SIGNAL_PATTERNS, writeScanOutput } from "./scan.js";
4
+
5
+ function isUrlInput(input) {
6
+ return /^https?:\/\//i.test(input);
7
+ }
8
+
9
+ function normalizeUrl(inputUrl) {
10
+ const normalizedInput = /^https?:\/\//i.test(inputUrl) ? inputUrl : `https://${inputUrl}`;
11
+ return new URL(normalizedInput);
12
+ }
13
+
14
+ async function fetchText(url) {
15
+ const response = await fetch(url, {
16
+ redirect: "follow",
17
+ headers: {
18
+ "user-agent": "geo-ai-search-optimization/1.4.0"
19
+ }
20
+ });
21
+
22
+ const text = await response.text();
23
+ return {
24
+ ok: response.ok,
25
+ status: response.status,
26
+ url: response.url,
27
+ text
28
+ };
29
+ }
30
+
31
+ async function readLocalFile(inputPath) {
32
+ const resolvedPath = path.resolve(inputPath);
33
+ const text = await fs.readFile(resolvedPath, "utf8");
34
+ return {
35
+ path: resolvedPath,
36
+ text
37
+ };
38
+ }
39
+
40
+ function extractTitle(text) {
41
+ const htmlMatch = text.match(/<title[^>]*>(.*?)<\/title>/i);
42
+ if (htmlMatch) {
43
+ return htmlMatch[1].trim();
44
+ }
45
+
46
+ const markdownMatch = text.match(/^#\s+(.+)$/m);
47
+ return markdownMatch ? markdownMatch[1].trim() : null;
48
+ }
49
+
50
+ function extractMetaDescription(text) {
51
+ const htmlMatch = text.match(/<meta[^>]+name=["']description["'][^>]+content=["']([^"']+)["']/i);
52
+ if (htmlMatch) {
53
+ return htmlMatch[1].trim();
54
+ }
55
+
56
+ const markdownMatch = text.match(/^description:\s*["']?(.+?)["']?$/im);
57
+ return markdownMatch ? markdownMatch[1].trim() : null;
58
+ }
59
+
60
+ function extractCanonical(text) {
61
+ const htmlMatch = text.match(/<link[^>]+rel=["']canonical["'][^>]+href=["']([^"']+)["']/i);
62
+ if (htmlMatch) {
63
+ return htmlMatch[1].trim();
64
+ }
65
+
66
+ const frontmatterMatch = text.match(/^canonical:\s*["']?(.+?)["']?$/im);
67
+ return frontmatterMatch ? frontmatterMatch[1].trim() : null;
68
+ }
69
+
70
+ function extractHeadings(text) {
71
+ const headings = [];
72
+ const markdownMatches = text.matchAll(/^(#{1,6})\s+(.+)$/gm);
73
+ for (const match of markdownMatches) {
74
+ headings.push({
75
+ level: match[1].length,
76
+ text: match[2].trim()
77
+ });
78
+ }
79
+
80
+ const htmlMatches = text.matchAll(/<h([1-6])[^>]*>(.*?)<\/h\1>/gim);
81
+ for (const match of htmlMatches) {
82
+ headings.push({
83
+ level: Number.parseInt(match[1], 10),
84
+ text: match[2].replace(/<[^>]+>/g, "").trim()
85
+ });
86
+ }
87
+
88
+ return headings;
89
+ }
90
+
91
+ function extractFirstParagraph(text) {
92
+ const stripped = text
93
+ .replace(/<script[\s\S]*?<\/script>/gi, " ")
94
+ .replace(/<style[\s\S]*?<\/style>/gi, " ")
95
+ .replace(/<[^>]+>/g, " ")
96
+ .replace(/\r/g, "");
97
+
98
+ const paragraphs = stripped
99
+ .split(/\n\s*\n/)
100
+ .map((entry) => entry.replace(/\s+/g, " ").trim())
101
+ .filter(Boolean);
102
+
103
+ return paragraphs[0] || null;
104
+ }
105
+
106
+ function buildSignalSummary(reference, text) {
107
+ return Object.fromEntries(
108
+ Object.keys(SIGNAL_PATTERNS).map((signal) => {
109
+ const matched = SIGNAL_PATTERNS[signal].test(text);
110
+ return [
111
+ signal,
112
+ {
113
+ count: matched ? 1 : 0,
114
+ examples: matched ? [reference] : []
115
+ }
116
+ ];
117
+ })
118
+ );
119
+ }
120
+
121
+ const PAGE_SCORE_RULES = [
122
+ {
123
+ key: "title",
124
+ label: "页面有清晰 title / 主标题",
125
+ maxPoints: 12,
126
+ passed: (page) => Boolean(page.metadata.title)
127
+ },
128
+ {
129
+ key: "metaDescription",
130
+ label: "页面有摘要描述",
131
+ maxPoints: 8,
132
+ passed: (page) => Boolean(page.metadata.metaDescription)
133
+ },
134
+ {
135
+ key: "canonical",
136
+ label: "页面有 canonical",
137
+ maxPoints: 8,
138
+ passed: (page) => Boolean(page.metadata.canonical)
139
+ },
140
+ {
141
+ key: "jsonLd",
142
+ label: "页面有结构化数据",
143
+ maxPoints: 12,
144
+ passed: (page) => page.signals.json_ld.count > 0
145
+ },
146
+ {
147
+ key: "author",
148
+ label: "页面有作者 / 时间 / 审校信号",
149
+ maxPoints: 10,
150
+ passed: (page) => page.signals.author_markers.count > 0
151
+ },
152
+ {
153
+ key: "citations",
154
+ label: "页面有来源链接",
155
+ maxPoints: 10,
156
+ passed: (page) => page.signals.citations.count > 0
157
+ },
158
+ {
159
+ key: "qaHeadings",
160
+ label: "页面结构适合被提取答案",
161
+ maxPoints: 12,
162
+ passed: (page) => page.signals.qa_headings.count > 0 || page.headingStats.questionHeadingCount > 0
163
+ },
164
+ {
165
+ key: "comparison",
166
+ label: "页面覆盖比较 / 决策信息",
167
+ maxPoints: 8,
168
+ passed: (page) => page.signals.comparison_intent.count > 0
169
+ },
170
+ {
171
+ key: "evidence",
172
+ label: "页面有第一方证据或方法论",
173
+ maxPoints: 10,
174
+ passed: (page) => page.signals.original_research.count > 0
175
+ },
176
+ {
177
+ key: "directAnswer",
178
+ label: "页面开头可快速提取直接答案",
179
+ maxPoints: 10,
180
+ passed: (page) => Boolean(page.directAnswerCandidate && page.directAnswerCandidate.length >= 50)
181
+ }
182
+ ];
183
+
184
+ function evaluatePageScore(page) {
185
+ const checks = PAGE_SCORE_RULES.map((rule) => {
186
+ const passed = rule.passed(page);
187
+ return {
188
+ key: rule.key,
189
+ label: rule.label,
190
+ passed,
191
+ maxPoints: rule.maxPoints,
192
+ pointsAwarded: passed ? rule.maxPoints : 0
193
+ };
194
+ });
195
+
196
+ const score = checks.reduce((sum, check) => sum + check.pointsAwarded, 0);
197
+ return {
198
+ score,
199
+ maxScore: 100,
200
+ checks
201
+ };
202
+ }
203
+
204
+ function getScoreLabel(score) {
205
+ if (score >= 80) {
206
+ return "稳固";
207
+ }
208
+ if (score >= 60) {
209
+ return "可优化";
210
+ }
211
+ if (score >= 40) {
212
+ return "偏弱";
213
+ }
214
+ return "需要重做";
215
+ }
216
+
217
+ function buildProblemAreas(page) {
218
+ const areas = [];
219
+
220
+ if (!page.metadata.title || !page.metadata.metaDescription || !page.metadata.canonical) {
221
+ areas.push({
222
+ area: "页面元信息",
223
+ severity: !page.metadata.title ? "高" : "中",
224
+ issues: [
225
+ !page.metadata.title ? "缺少 title / 主标题" : null,
226
+ !page.metadata.metaDescription ? "缺少摘要描述" : null,
227
+ !page.metadata.canonical ? "缺少 canonical" : null
228
+ ].filter(Boolean),
229
+ whyItMatters: "模型和搜索系统需要先理解这页是什么、应该归到哪个 canonical 实体上。"
230
+ });
231
+ }
232
+
233
+ if (page.signals.json_ld.count === 0 || page.signals.author_markers.count === 0 || page.signals.citations.count === 0) {
234
+ areas.push({
235
+ area: "可信度与引用",
236
+ severity: page.signals.citations.count === 0 ? "高" : "中",
237
+ issues: [
238
+ page.signals.json_ld.count === 0 ? "缺少结构化数据" : null,
239
+ page.signals.author_markers.count === 0 ? "缺少作者 / 更新时间 / 审校信号" : null,
240
+ page.signals.citations.count === 0 ? "缺少来源链接" : null
241
+ ].filter(Boolean),
242
+ whyItMatters: "如果页面没有可信来源、作者和结构化实体,AI 搜索更难放心引用。"
243
+ });
244
+ }
245
+
246
+ if (page.headingStats.questionHeadingCount === 0 && page.signals.qa_headings.count === 0) {
247
+ areas.push({
248
+ area: "答案可提取性",
249
+ severity: "高",
250
+ issues: ["缺少问答式或决策式标题", "页面结构不够 answer-first"],
251
+ whyItMatters: "模型需要在很短上下文里抓到直接答案、适用对象和限制条件。"
252
+ });
253
+ }
254
+
255
+ if (page.signals.original_research.count === 0 && page.signals.comparison_intent.count === 0) {
256
+ areas.push({
257
+ area: "差异化内容",
258
+ severity: "中",
259
+ issues: ["缺少方法论 / 第一方证据", "缺少比较与决策内容"],
260
+ whyItMatters: "如果页面没有独特证据和决策信息,就很难在生成式结果里占据引用位置。"
261
+ });
262
+ }
263
+
264
+ return areas;
265
+ }
266
+
267
+ function buildRecommendedBlocks(page) {
268
+ const blocks = [];
269
+ if (!page.directAnswerCandidate || page.directAnswerCandidate.length < 50) {
270
+ blocks.push("在首屏补一个直接答案摘要块,先回答“这是什么 / 适合谁 / 主要限制是什么”。");
271
+ }
272
+ if (page.headingStats.questionHeadingCount === 0) {
273
+ blocks.push("补 2 到 4 个问答式 H2,例如“适合谁”“什么时候不适合”“和替代方案差别是什么”。");
274
+ }
275
+ if (page.signals.citations.count === 0) {
276
+ blocks.push("增加来源链接或证据区块,给关键事实、数据和判断补出处。");
277
+ }
278
+ if (page.signals.original_research.count === 0) {
279
+ blocks.push("补第一方证据区块,例如方法论、流程、案例、截图、基准或内部数据。");
280
+ }
281
+ if (page.signals.comparison_intent.count === 0) {
282
+ blocks.push("增加 comparison / alternatives / trade-offs 模块,帮助模型提取决策信息。");
283
+ }
284
+ if (page.signals.json_ld.count === 0) {
285
+ blocks.push("补与页面可见内容一致的 JSON-LD。");
286
+ }
287
+ return blocks;
288
+ }
289
+
290
+ function buildRewriteBrief(page, problems, recommendedBlocks) {
291
+ return {
292
+ objective: "把当前页面改成更容易被生成式搜索引擎理解、提取、引用的 answer-first 页面。",
293
+ audience: "负责该页面的 PM、内容负责人、SEO 或 agent。",
294
+ keep: [
295
+ page.metadata.title ? `保留核心主题:${page.metadata.title}` : "保留当前页面的核心主题,不要改成另一个意图。",
296
+ "保留事实准确性,不用为了 SEO 或 GEO 添加页面没有证据支撑的说法。"
297
+ ],
298
+ change: problems.flatMap((problem) => problem.issues).slice(0, 6),
299
+ addSections: recommendedBlocks,
300
+ openingPattern: "用 2 到 4 句先给直接答案,再说明适用对象、关键限制与下一步。",
301
+ evidencePattern: "给结论补来源链接、案例、方法论、数据或截图说明。",
302
+ schemaHint:
303
+ page.signals.json_ld.count > 0
304
+ ? "保留现有 schema,但确认与页面可见内容一致。"
305
+ : "优先考虑 Article / FAQPage / Product / Organization 中最贴近页面真实内容的 schema。"
306
+ };
307
+ }
308
+
309
+ function buildAgentPrompt(page, rewriteBrief) {
310
+ return [
311
+ "请先把这页当成单页 GEO 修复任务处理。",
312
+ `当前页面:${page.reference}`,
313
+ `当前分数:${page.score.score}/${page.score.maxScore}(${getScoreLabel(page.score.score)})`,
314
+ `优先改动:${rewriteBrief.change.slice(0, 4).join(";") || "补直接答案、证据与结构化信息"}`,
315
+ "先重写页面 opening,再补问答式标题、来源链接、证据区块和最合适的 schema。",
316
+ "如果是代码仓库,请优先找模板、metadata helper、schema 生成点,而不是只改单一页面文案。",
317
+ "完成后重新跑 page-audit,确认分数和问题区域是否改善。"
318
+ ].join(" ");
319
+ }
320
+
321
+ function buildSummary(page, problems) {
322
+ if (page.score.score >= 80) {
323
+ return "这页已经具备较好的 GEO 基础,下一步重点是增强差异化证据与可引用素材。";
324
+ }
325
+
326
+ if (problems.length === 0) {
327
+ return "这页没有明显结构性缺口,可以继续加强引用资产和第一方证据。";
328
+ }
329
+
330
+ return `这页当前最需要优先处理的是${problems[0].area},否则模型很难稳定理解并引用页面内容。`;
331
+ }
332
+
333
+ export async function auditPage(input, options = {}) {
334
+ const sourceType = isUrlInput(input) ? "url" : "file";
335
+ let source;
336
+
337
+ if (sourceType === "url") {
338
+ const url = normalizeUrl(input);
339
+ const result = await fetchText(url);
340
+ if (!result.ok) {
341
+ throw new Error(`抓取失败:${url.toString()}(状态码 ${result.status})`);
342
+ }
343
+ source = {
344
+ reference: result.url,
345
+ content: result.text,
346
+ fetchStatus: result.status
347
+ };
348
+ } else {
349
+ const result = await readLocalFile(input);
350
+ source = {
351
+ reference: result.path,
352
+ content: result.text,
353
+ fetchStatus: null
354
+ };
355
+ }
356
+
357
+ const headings = extractHeadings(source.content);
358
+ const page = {
359
+ kind: "geo-page-audit",
360
+ input,
361
+ sourceType,
362
+ reference: source.reference,
363
+ fetchStatus: source.fetchStatus,
364
+ metadata: {
365
+ title: extractTitle(source.content),
366
+ metaDescription: extractMetaDescription(source.content),
367
+ canonical: extractCanonical(source.content)
368
+ },
369
+ directAnswerCandidate: extractFirstParagraph(source.content),
370
+ headingStats: {
371
+ totalHeadingCount: headings.length,
372
+ questionHeadingCount: headings.filter((heading) => heading.text.includes("?") || heading.text.includes("?")).length
373
+ },
374
+ headings: headings.slice(0, options.maxHeadings || 12),
375
+ signals: buildSignalSummary(source.reference, source.content)
376
+ };
377
+
378
+ page.score = evaluatePageScore(page);
379
+ page.scoreLabel = getScoreLabel(page.score.score);
380
+ page.problemAreas = buildProblemAreas(page);
381
+ page.recommendedBlocks = buildRecommendedBlocks(page);
382
+ page.rewriteBrief = buildRewriteBrief(page, page.problemAreas, page.recommendedBlocks);
383
+ page.summary = buildSummary(page, page.problemAreas);
384
+ page.agentPrompt = buildAgentPrompt(page, page.rewriteBrief);
385
+
386
+ return page;
387
+ }
388
+
389
+ export function renderPageAuditMarkdown(report) {
390
+ const lines = [
391
+ "# GEO 单页审计",
392
+ "",
393
+ `- 输入:\`${report.input}\``,
394
+ `- 来源类型:\`${report.sourceType}\``,
395
+ `- 分析对象:\`${report.reference}\``,
396
+ `- GEO 单页分数:\`${report.score.score}/${report.score.maxScore}\`(${report.scoreLabel})`,
397
+ `- 总结:${report.summary}`,
398
+ ""
399
+ ];
400
+
401
+ if (report.fetchStatus !== null) {
402
+ lines.push(`- 抓取状态:\`${report.fetchStatus}\``);
403
+ lines.push("");
404
+ }
405
+
406
+ lines.push("## 页面快照", "");
407
+ lines.push(`- title:\`${report.metadata.title ?? "缺失"}\``);
408
+ lines.push(`- meta description:\`${report.metadata.metaDescription ?? "缺失"}\``);
409
+ lines.push(`- canonical:\`${report.metadata.canonical ?? "缺失"}\``);
410
+ lines.push(`- 首段直接答案候选:${report.directAnswerCandidate ? report.directAnswerCandidate.slice(0, 180) : "缺失"}`);
411
+
412
+ lines.push("", "## 问题区域", "");
413
+ if (report.problemAreas.length === 0) {
414
+ lines.push("- 当前没有明显的页面级高风险区域。");
415
+ } else {
416
+ for (const area of report.problemAreas) {
417
+ lines.push(`- ${area.area}|${area.severity}|${area.issues.join("、")}`);
418
+ lines.push(` ${area.whyItMatters}`);
419
+ }
420
+ }
421
+
422
+ lines.push("", "## 推荐新增模块", "");
423
+ for (const item of report.recommendedBlocks) {
424
+ lines.push(`- ${item}`);
425
+ }
426
+
427
+ lines.push("", "## Rewrite Brief", "");
428
+ lines.push(`- 目标:${report.rewriteBrief.objective}`);
429
+ lines.push(`- Opening pattern:${report.rewriteBrief.openingPattern}`);
430
+ lines.push(`- Evidence pattern:${report.rewriteBrief.evidencePattern}`);
431
+ lines.push(`- Schema hint:${report.rewriteBrief.schemaHint}`);
432
+
433
+ lines.push("", "## 建议保留", "");
434
+ for (const item of report.rewriteBrief.keep) {
435
+ lines.push(`- ${item}`);
436
+ }
437
+
438
+ lines.push("", "## 建议调整", "");
439
+ for (const item of report.rewriteBrief.change) {
440
+ lines.push(`- ${item}`);
441
+ }
442
+
443
+ lines.push("", "## 建议新增段落", "");
444
+ for (const item of report.rewriteBrief.addSections) {
445
+ lines.push(`- ${item}`);
446
+ }
447
+
448
+ lines.push("", "## 页面标题快照", "");
449
+ for (const heading of report.headings) {
450
+ lines.push(`- H${heading.level}:${heading.text}`);
451
+ }
452
+
453
+ lines.push("", "## 评分明细", "");
454
+ for (const check of report.score.checks) {
455
+ lines.push(`- ${check.label}:\`${check.passed}\`(${check.pointsAwarded}/${check.maxPoints})`);
456
+ }
457
+
458
+ lines.push("", "## Agent Prompt", "", report.agentPrompt, "");
459
+ return lines.join("\n");
460
+ }
461
+
462
+ export async function writePageAuditOutput(outputPath, content) {
463
+ return writeScanOutput(outputPath, content);
464
+ }