geo-ai-search-optimization 1.4.0 → 1.4.2

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
@@ -81,6 +81,42 @@ geo-ai-search-optimization page-audit ./content/posts/geo-guide.mdx --json --out
81
81
  - rewrite brief
82
82
  - 可直接交给 agent 的 page-level prompt
83
83
 
84
+ ## Rewrite Pack 命令
85
+
86
+ 如果你已经知道某个页面有问题,想直接拿到更接近“改写方案”的产物,可以用 `rewrite-pack`:
87
+
88
+ ```bash
89
+ geo-ai-search-optimization rewrite-pack https://example.com/blog/geo-guide
90
+ geo-ai-search-optimization rewrite-pack ./content/posts/geo-guide.mdx
91
+ geo-ai-search-optimization rewrite-pack ./content/posts/geo-guide.mdx --json --out ./reports/rewrite-pack.json
92
+ ```
93
+
94
+ 它会输出:
95
+
96
+ - metadata drafts
97
+ - 建议章节结构
98
+ - FAQ questions
99
+ - schema recommendations
100
+ - execution checklist
101
+ - 可直接交给 agent 的 rewrite prompt
102
+
103
+ ## Repo Patch Plan 命令
104
+
105
+ 如果你希望从“页面建议”继续推进到“仓库里先改哪些文件和模板”,可以直接用 `repo-patch-plan`:
106
+
107
+ ```bash
108
+ geo-ai-search-optimization repo-patch-plan ./your-site
109
+ geo-ai-search-optimization repo-patch-plan ./your-site --json --out ./reports/repo-patch-plan.json
110
+ ```
111
+
112
+ 它会输出:
113
+
114
+ - 候选文件区域
115
+ - patch packets
116
+ - likely files
117
+ - execution notes
118
+ - 可直接交给 agent 的 repo-level prompt
119
+
84
120
  ## Agent Resume 命令
85
121
 
86
122
  如果 GEO 工作已经做过一轮或多轮,你不想让下一个 agent 从头重新判断,而是想让它从最近一个可靠恢复点继续,可以直接用 `agent-resume`:
@@ -963,6 +999,18 @@ geo-ai-search-optimization help
963
999
  - 输出页面分数、问题区域、推荐新增模块、rewrite brief 与 agent prompt
964
1000
  - 产品能力从“站点级诊断”扩展到“页面级修复入口”
965
1001
 
1002
+ ## New in 1.4.1
1003
+
1004
+ - 新增 `rewrite-pack`
1005
+ - 基于 `page-audit` 继续产出 metadata drafts、章节结构、FAQ questions、schema recommendations 与 execution checklist
1006
+ - 让产品从“页面级诊断”继续推进到“页面级改写方案”
1007
+
1008
+ ## New in 1.4.2
1009
+
1010
+ - 新增 `repo-patch-plan`
1011
+ - 把 GEO 问题继续收敛到仓库级 likely files、模板入口、metadata / schema / navigation 区域
1012
+ - 让 agent 更容易从“页面建议”切到“仓库改动计划”
1013
+
966
1014
  ## New in 1.2.20
967
1015
 
968
1016
  - 新增 `agent-continue`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "geo-ai-search-optimization",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
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": {
@@ -15,6 +15,8 @@ import {
15
15
  import { createLlmsTxt } from "./llms-txt.js";
16
16
  import { auditPage, renderPageAuditMarkdown, writePageAuditOutput } from "./page-audit.js";
17
17
  import { createQuickStartPlan, renderQuickStartMarkdown, writeQuickStartOutput } from "./quick-start.js";
18
+ import { createRepoPatchPlan, renderRepoPatchPlanMarkdown, writeRepoPatchPlanOutput } from "./repo-patch-plan.js";
19
+ import { createRewritePack, renderRewritePackMarkdown, writeRewritePackOutput } from "./rewrite-pack.js";
18
20
  import { renderScanMarkdown, scanProject, writeScanOutput } from "./scan.js";
19
21
  import { createSchemaTemplate } from "./schema.js";
20
22
  import { analyzeWebsiteUrl, renderWebsiteOnboardingMarkdown, writeWebsiteOnboardingOutput } from "./url-onboarding.js";
@@ -25,6 +27,8 @@ export const SITE_OPS_HELP_LINES = [
25
27
  " geo-ai-search-optimization onboard [--url <website-url>] [--goal <goal>] [--existing-assets <list>] [--json] [--out <file>]",
26
28
  " geo-ai-search-optimization onboard-url <website-url> [--json] [--out <file>]",
27
29
  " geo-ai-search-optimization page-audit <url-or-file> [--json] [--out <file>]",
30
+ " geo-ai-search-optimization rewrite-pack <url-or-file> [--json] [--out <file>]",
31
+ " geo-ai-search-optimization repo-patch-plan <project-path> [--json] [--out <file>]",
28
32
  " geo-ai-search-optimization init-llms [target-dir] [--site-name <name>] [--site-url <url>] [--overwrite] [--json]",
29
33
  " geo-ai-search-optimization init-schema <type> [target-dir] [--site-url <url>] [--overwrite] [--json]",
30
34
  " geo-ai-search-optimization audit <project-path> [--json] [--out <file>] [--max-file-size <bytes>] [--max-examples <count>]",
@@ -92,6 +96,43 @@ const handlePageAudit = createStructuredOutputCommandHandler({
92
96
  getOutputJson: (args) => hasFlag(args, "--json")
93
97
  });
94
98
 
99
+ const handleRewritePack = createStructuredOutputCommandHandler({
100
+ commandLabel: "rewrite pack",
101
+ execute: async (args) => {
102
+ const input = args.find((value) => !value.startsWith("-"));
103
+ if (!input) {
104
+ throw new Error("rewrite-pack requires a URL or file path");
105
+ }
106
+
107
+ return createRewritePack(input, {});
108
+ },
109
+ renderMarkdown: (report) => `${renderRewritePackMarkdown(report)}\n`,
110
+ writeOutput: writeRewritePackOutput,
111
+ getOutputJson: (args) => hasFlag(args, "--json")
112
+ });
113
+
114
+ const handleRepoPatchPlan = createStructuredOutputCommandHandler({
115
+ commandLabel: "repo patch plan",
116
+ execute: async (args) => {
117
+ const input = args.find((value) => !value.startsWith("-"));
118
+ if (!input) {
119
+ throw new Error("repo-patch-plan requires a project path");
120
+ }
121
+
122
+ return createRepoPatchPlan(input, {
123
+ maxFileSize: getFlagValue(args, "--max-file-size")
124
+ ? parsePositiveInteger(getFlagValue(args, "--max-file-size"), "--max-file-size")
125
+ : undefined,
126
+ maxExamples: getFlagValue(args, "--max-examples")
127
+ ? parsePositiveInteger(getFlagValue(args, "--max-examples"), "--max-examples")
128
+ : undefined
129
+ });
130
+ },
131
+ renderMarkdown: (report) => `${renderRepoPatchPlanMarkdown(report)}\n`,
132
+ writeOutput: writeRepoPatchPlanOutput,
133
+ getOutputJson: (args) => hasFlag(args, "--json")
134
+ });
135
+
95
136
  const handleInitLlms = createActionCommandHandler({
96
137
  execute: async (args) =>
97
138
  createLlmsTxt({
@@ -171,6 +212,8 @@ export const SITE_OPS_COMMAND_HANDLERS = {
171
212
  onboard: handleInteractiveOnboard,
172
213
  "onboard-url": handleOnboardUrl,
173
214
  "page-audit": handlePageAudit,
215
+ "rewrite-pack": handleRewritePack,
216
+ "repo-patch-plan": handleRepoPatchPlan,
174
217
  "init-llms": handleInitLlms,
175
218
  "init-schema": handleInitSchema,
176
219
  audit: handleAudit,
package/src/index.js CHANGED
@@ -42,7 +42,9 @@ export { createOwnerBoard, renderOwnerBoardMarkdown, writeOwnerBoardOutput } fro
42
42
  export { createPmBrief, renderPmBriefMarkdown, writePmBriefOutput } from "./pm-brief.js";
43
43
  export { renderPublishPackMarkdown, writePublishPack } from "./publish-pack.js";
44
44
  export { createQuickStartPlan, renderQuickStartMarkdown, writeQuickStartOutput } from "./quick-start.js";
45
+ export { createRepoPatchPlan, renderRepoPatchPlanMarkdown, writeRepoPatchPlanOutput } from "./repo-patch-plan.js";
45
46
  export { generateReport, writeReportOutput } from "./report.js";
47
+ export { createRewritePack, renderRewritePackMarkdown, writeRewritePackOutput } from "./rewrite-pack.js";
46
48
  export { createRoadmap, renderRoadmapMarkdown, writeRoadmapOutput } from "./roadmap.js";
47
49
  export { createSchemaTemplate } from "./schema.js";
48
50
  export { scanProject, renderScanMarkdown, writeScanOutput } from "./scan.js";
@@ -0,0 +1,257 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { auditProject } from "./audit.js";
4
+ import { writeScanOutput } from "./scan.js";
5
+
6
+ const CANDIDATE_PATTERNS = [
7
+ {
8
+ area: "metadata",
9
+ label: "metadata / SEO helper",
10
+ test: (filePath) =>
11
+ /(seo|meta|metadata|head|layout|rootlayout|app\.tsx|app\.jsx|document|helmet)/i.test(filePath)
12
+ },
13
+ {
14
+ area: "schema",
15
+ label: "schema / structured data",
16
+ test: (filePath) => /(schema|json-ld|structured-data)/i.test(filePath)
17
+ },
18
+ {
19
+ area: "content-template",
20
+ label: "content template",
21
+ test: (filePath) => /(post|article|blog|page|content|docs|template|route|slug)/i.test(filePath)
22
+ },
23
+ {
24
+ area: "navigation",
25
+ label: "navigation / sitemap / robots",
26
+ test: (filePath) => /(robots|sitemap|llms|routes|navigation|nav)/i.test(filePath)
27
+ }
28
+ ];
29
+
30
+ const TEXT_EXTENSIONS = new Set([
31
+ ".astro",
32
+ ".html",
33
+ ".js",
34
+ ".jsx",
35
+ ".md",
36
+ ".mdx",
37
+ ".mjs",
38
+ ".svelte",
39
+ ".ts",
40
+ ".tsx",
41
+ ".vue"
42
+ ]);
43
+
44
+ const IGNORE_DIRS = new Set([
45
+ ".git",
46
+ ".next",
47
+ ".nuxt",
48
+ ".output",
49
+ ".svelte-kit",
50
+ "build",
51
+ "coverage",
52
+ "dist",
53
+ "node_modules",
54
+ "out",
55
+ "tmp"
56
+ ]);
57
+
58
+ async function collectRepoFiles(root) {
59
+ const files = [];
60
+ const queue = [root];
61
+
62
+ while (queue.length > 0) {
63
+ const current = queue.shift();
64
+ const entries = await fs.readdir(current, { withFileTypes: true });
65
+
66
+ for (const entry of entries) {
67
+ const entryPath = path.join(current, entry.name);
68
+ if (entry.isDirectory()) {
69
+ if (!IGNORE_DIRS.has(entry.name)) {
70
+ queue.push(entryPath);
71
+ }
72
+ continue;
73
+ }
74
+
75
+ const extension = path.extname(entry.name).toLowerCase();
76
+ if (TEXT_EXTENSIONS.has(extension) || entry.name === "robots.txt" || entry.name === "llms.txt") {
77
+ files.push(entryPath);
78
+ }
79
+ }
80
+ }
81
+
82
+ return files;
83
+ }
84
+
85
+ function rankCandidateFiles(root, files) {
86
+ return CANDIDATE_PATTERNS.map((pattern) => {
87
+ const matches = files
88
+ .filter((filePath) => pattern.test(path.relative(root, filePath)))
89
+ .slice(0, 6)
90
+ .map((filePath) => path.relative(root, filePath));
91
+
92
+ return {
93
+ area: pattern.area,
94
+ label: pattern.label,
95
+ files: matches
96
+ };
97
+ }).filter((entry) => entry.files.length > 0);
98
+ }
99
+
100
+ function buildPatchPackets(auditReport, candidates) {
101
+ const packets = [];
102
+
103
+ if (auditReport.groups.blockers.length > 0) {
104
+ packets.push({
105
+ id: "repo-fix-01",
106
+ priority: "P0",
107
+ owner: "工程 / SEO",
108
+ title: "先修基础抓取与归一入口",
109
+ targetArea: "抓取与索引",
110
+ likelyFiles: candidates.find((item) => item.area === "navigation")?.files || [],
111
+ actions: auditReport.groups.blockers.slice(0, 4),
112
+ doneWhen: "robots.txt、sitemap、canonical 机制可在仓库中找到明确入口并已接通。"
113
+ });
114
+ }
115
+
116
+ if (auditReport.groups.highImpactFixes.length > 0) {
117
+ packets.push({
118
+ id: "repo-fix-02",
119
+ priority: "P1",
120
+ owner: "工程 / 内容 / SEO",
121
+ title: "补齐页面模板中的 GEO 核心信号",
122
+ targetArea: "模板与页面结构",
123
+ likelyFiles: [
124
+ ...(candidates.find((item) => item.area === "metadata")?.files || []),
125
+ ...(candidates.find((item) => item.area === "content-template")?.files || [])
126
+ ].slice(0, 8),
127
+ actions: auditReport.groups.highImpactFixes.slice(0, 5),
128
+ doneWhen: "metadata、answer-first opening、作者/时间、来源链接和关键模板入口已能统一治理。"
129
+ });
130
+ }
131
+
132
+ if (auditReport.groups.supportFixes.length > 0) {
133
+ packets.push({
134
+ id: "repo-fix-03",
135
+ priority: "P2",
136
+ owner: "工程 / SEO",
137
+ title: "补模板级辅助信号",
138
+ targetArea: "模板治理",
139
+ likelyFiles: [
140
+ ...(candidates.find((item) => item.area === "schema")?.files || []),
141
+ ...(candidates.find((item) => item.area === "metadata")?.files || [])
142
+ ].slice(0, 8),
143
+ actions: auditReport.groups.supportFixes.slice(0, 4),
144
+ doneWhen: "模板层的 metadata、breadcrumb、schema 和 comparison 支持已有明确实现入口。"
145
+ });
146
+ }
147
+
148
+ return packets;
149
+ }
150
+
151
+ function buildExecutionNotes(auditReport, candidates) {
152
+ const notes = [];
153
+
154
+ if (candidates.find((item) => item.area === "metadata")) {
155
+ notes.push("优先检查 metadata / head / layout / SEO helper,不要逐页手改。");
156
+ }
157
+ if (candidates.find((item) => item.area === "schema")) {
158
+ notes.push("如果仓库已有 schema 入口,先复用现有 schema builder,再补页面类型逻辑。");
159
+ }
160
+ if (candidates.find((item) => item.area === "content-template")) {
161
+ notes.push("优先改文章页、内容页或动态路由模板,再考虑单页补丁。");
162
+ }
163
+ if (auditReport.groups.highImpactFixes.length > 0) {
164
+ notes.push("先处理高影响 GEO 修复项,再进入支持性优化,避免工程分散。");
165
+ }
166
+
167
+ return notes;
168
+ }
169
+
170
+ export async function createRepoPatchPlan(rootInput, options = {}) {
171
+ const root = path.resolve(rootInput);
172
+ const stat = await fs.stat(root).catch(() => null);
173
+ if (!stat || !stat.isDirectory()) {
174
+ throw new Error("repo-patch-plan 需要一个本地项目目录");
175
+ }
176
+
177
+ const [auditReport, repoFiles] = await Promise.all([
178
+ auditProject(root, options),
179
+ collectRepoFiles(root)
180
+ ]);
181
+
182
+ const candidates = rankCandidateFiles(root, repoFiles);
183
+ const patchPackets = buildPatchPackets(auditReport, candidates);
184
+
185
+ return {
186
+ kind: "geo-repo-patch-plan",
187
+ root,
188
+ summary: "这份 repo patch plan 会把 GEO 问题尽量落到仓库级模板、metadata、schema 和路由入口,方便 agent 直接开始改代码。",
189
+ score: {
190
+ value: auditReport.score,
191
+ maxScore: auditReport.maxScore,
192
+ label: auditReport.scoreLabel
193
+ },
194
+ candidateFiles: candidates,
195
+ patchPackets,
196
+ executionNotes: buildExecutionNotes(auditReport, candidates),
197
+ auditReport,
198
+ agentPrompt: [
199
+ "请把这次 GEO 修复当成仓库级任务处理。",
200
+ `项目目录:${root}`,
201
+ `当前 GEO 分数:${auditReport.score}/${auditReport.maxScore}(${auditReport.scoreLabel})`,
202
+ `优先包:${patchPackets[0]?.id || "repo-fix-01"}`,
203
+ "先从 likelyFiles 开始确认 metadata、schema、content template、navigation 入口。",
204
+ "优先复用模板和 helper,不要只改单个页面。",
205
+ "完成一包后重新跑 audit 或 page-audit,验证问题是否实际减少。"
206
+ ].join(" ")
207
+ };
208
+ }
209
+
210
+ export function renderRepoPatchPlanMarkdown(plan) {
211
+ const lines = [
212
+ "# GEO Repo Patch Plan",
213
+ "",
214
+ `- 项目目录:\`${plan.root}\``,
215
+ `- 当前分数:\`${plan.score.value}/${plan.score.maxScore}\`(${plan.score.label})`,
216
+ `- 总结:${plan.summary}`,
217
+ "",
218
+ "## 候选文件区域",
219
+ ""
220
+ ];
221
+
222
+ for (const candidate of plan.candidateFiles) {
223
+ lines.push(`- ${candidate.label}`);
224
+ for (const file of candidate.files) {
225
+ lines.push(` - ${file}`);
226
+ }
227
+ }
228
+
229
+ lines.push("", "## Patch Packets", "");
230
+ for (const packet of plan.patchPackets) {
231
+ lines.push(`- ${packet.id}|${packet.priority}|${packet.owner}|${packet.title}`);
232
+ lines.push(` Target:${packet.targetArea}`);
233
+ lines.push(` Done when:${packet.doneWhen}`);
234
+ if (packet.likelyFiles.length > 0) {
235
+ lines.push(" Likely files:");
236
+ for (const file of packet.likelyFiles) {
237
+ lines.push(` - ${file}`);
238
+ }
239
+ }
240
+ lines.push(" Actions:");
241
+ for (const action of packet.actions) {
242
+ lines.push(` - ${action}`);
243
+ }
244
+ }
245
+
246
+ lines.push("", "## Execution Notes", "");
247
+ for (const note of plan.executionNotes) {
248
+ lines.push(`- ${note}`);
249
+ }
250
+
251
+ lines.push("", "## Agent Prompt", "", plan.agentPrompt, "");
252
+ return lines.join("\n");
253
+ }
254
+
255
+ export async function writeRepoPatchPlanOutput(outputPath, content) {
256
+ return writeScanOutput(outputPath, content);
257
+ }
@@ -0,0 +1,198 @@
1
+ import { auditPage, writePageAuditOutput } from "./page-audit.js";
2
+
3
+ function buildOpeningDraft(report) {
4
+ const title = report.metadata.title || "这个页面";
5
+ return [
6
+ `${title} 是一页需要先用直接答案说明核心价值的内容。`,
7
+ "这页最适合先讲清楚它是什么、适合谁、什么时候不适合,以及用户下一步应该看什么。",
8
+ "如果没有这些信息,生成式搜索系统很难稳定提取出可引用的答案。"
9
+ ].join(" ");
10
+ }
11
+
12
+ function buildSuggestedHeadings(report) {
13
+ const headings = [
14
+ "这是什么",
15
+ "适合谁",
16
+ "什么时候不适合",
17
+ "和其他方案有什么差别",
18
+ "关键事实与证据",
19
+ "下一步该怎么做"
20
+ ];
21
+
22
+ if (report.signals.comparison_intent.count > 0) {
23
+ headings.splice(3, 1, "和替代方案相比有什么差别");
24
+ }
25
+
26
+ return headings.slice(0, 6);
27
+ }
28
+
29
+ function buildFaqQuestions(report) {
30
+ return [
31
+ `${report.metadata.title || "这页的主题"} 适合哪些人?`,
32
+ "什么时候不应该选择这个方案?",
33
+ "有哪些限制条件或前提?",
34
+ "有哪些证据可以支持这页的主张?"
35
+ ];
36
+ }
37
+
38
+ function buildSchemaRecommendations(report) {
39
+ const recommendations = [];
40
+
41
+ if (report.signals.qa_headings.count > 0 || report.headingStats.questionHeadingCount > 0) {
42
+ recommendations.push("FAQPage");
43
+ }
44
+
45
+ if (report.signals.original_research.count > 0) {
46
+ recommendations.push("Article");
47
+ }
48
+
49
+ if (report.signals.comparison_intent.count > 0) {
50
+ recommendations.push("Article");
51
+ }
52
+
53
+ if (recommendations.length === 0) {
54
+ recommendations.push("Article");
55
+ }
56
+
57
+ return Array.from(new Set(recommendations));
58
+ }
59
+
60
+ function buildMetadataDrafts(report) {
61
+ const baseTitle = report.metadata.title || "页面主题";
62
+ return {
63
+ title: `${baseTitle}|适合谁、限制与关键信息`,
64
+ metaDescription: `${baseTitle} 的直接答案、适用对象、限制条件、关键证据与下一步建议。`,
65
+ canonical: report.metadata.canonical || "请设置到该页面的唯一正式 URL"
66
+ };
67
+ }
68
+
69
+ function buildContentSections(report) {
70
+ return [
71
+ {
72
+ name: "Opening summary",
73
+ goal: "用 2 到 4 句先给直接答案。",
74
+ draft: buildOpeningDraft(report)
75
+ },
76
+ {
77
+ name: "Decision block",
78
+ goal: "明确适合谁、什么时候不适合、主要 trade-off。",
79
+ bullets: [
80
+ "适合谁",
81
+ "主要限制",
82
+ "决策条件",
83
+ "如果不适合,应该去看哪一页"
84
+ ]
85
+ },
86
+ {
87
+ name: "Evidence block",
88
+ goal: "把结论和主张接到来源、案例、方法论或数据。",
89
+ bullets: [
90
+ "来源链接",
91
+ "方法论说明",
92
+ "案例或截图",
93
+ "更新时间"
94
+ ]
95
+ }
96
+ ];
97
+ }
98
+
99
+ function buildExecutionChecklist(report) {
100
+ return [
101
+ "先重写 opening summary,让第一屏可以直接回答用户问题。",
102
+ "补上问答式 H2 和 decision block。",
103
+ "补来源链接、作者/更新时间和 evidence block。",
104
+ "补最贴近页面真实内容的 schema。",
105
+ "完成后重新跑 page-audit,对比分数和问题区域。"
106
+ ];
107
+ }
108
+
109
+ export async function createRewritePack(input, options = {}) {
110
+ const pageAudit = await auditPage(input, options);
111
+
112
+ return {
113
+ kind: "geo-rewrite-pack",
114
+ input,
115
+ sourceType: pageAudit.sourceType,
116
+ reference: pageAudit.reference,
117
+ summary: `这份 rewrite pack 基于单页审计生成,优先帮助你把页面改成更适合生成式搜索理解与引用的 answer-first 结构。`,
118
+ currentScore: {
119
+ score: pageAudit.score.score,
120
+ maxScore: pageAudit.score.maxScore,
121
+ label: pageAudit.scoreLabel
122
+ },
123
+ pageAudit,
124
+ metadataDrafts: buildMetadataDrafts(pageAudit),
125
+ suggestedHeadings: buildSuggestedHeadings(pageAudit),
126
+ faqQuestions: buildFaqQuestions(pageAudit),
127
+ schemaRecommendations: buildSchemaRecommendations(pageAudit),
128
+ contentSections: buildContentSections(pageAudit),
129
+ executionChecklist: buildExecutionChecklist(pageAudit),
130
+ agentPrompt: [
131
+ "请根据这份 rewrite pack 改写页面。",
132
+ `页面:${pageAudit.reference}`,
133
+ `当前分数:${pageAudit.score.score}/${pageAudit.score.maxScore}`,
134
+ `优先处理:${pageAudit.problemAreas.map((item) => item.area).join("、") || "opening summary 与 evidence block"}`,
135
+ "先改 metadata drafts 和 opening summary,再补 headings、FAQ、evidence block、schema。",
136
+ "改完后重新跑 page-audit,确认页面级 GEO 分数有提升。"
137
+ ].join(" ")
138
+ };
139
+ }
140
+
141
+ export function renderRewritePackMarkdown(pack) {
142
+ const lines = [
143
+ "# GEO Rewrite Pack",
144
+ "",
145
+ `- 输入:\`${pack.input}\``,
146
+ `- 分析对象:\`${pack.reference}\``,
147
+ `- 当前分数:\`${pack.currentScore.score}/${pack.currentScore.maxScore}\`(${pack.currentScore.label})`,
148
+ `- 总结:${pack.summary}`,
149
+ "",
150
+ "## Metadata Drafts",
151
+ "",
152
+ `- title:${pack.metadataDrafts.title}`,
153
+ `- meta description:${pack.metadataDrafts.metaDescription}`,
154
+ `- canonical:${pack.metadataDrafts.canonical}`,
155
+ "",
156
+ "## Suggested Headings",
157
+ ""
158
+ ];
159
+
160
+ for (const heading of pack.suggestedHeadings) {
161
+ lines.push(`- ${heading}`);
162
+ }
163
+
164
+ lines.push("", "## FAQ Questions", "");
165
+ for (const question of pack.faqQuestions) {
166
+ lines.push(`- ${question}`);
167
+ }
168
+
169
+ lines.push("", "## Schema Recommendations", "");
170
+ for (const schema of pack.schemaRecommendations) {
171
+ lines.push(`- ${schema}`);
172
+ }
173
+
174
+ lines.push("", "## Content Sections", "");
175
+ for (const section of pack.contentSections) {
176
+ lines.push(`- ${section.name}|${section.goal}`);
177
+ if (section.draft) {
178
+ lines.push(` Draft:${section.draft}`);
179
+ }
180
+ if (section.bullets) {
181
+ for (const bullet of section.bullets) {
182
+ lines.push(` - ${bullet}`);
183
+ }
184
+ }
185
+ }
186
+
187
+ lines.push("", "## Execution Checklist", "");
188
+ for (const item of pack.executionChecklist) {
189
+ lines.push(`- ${item}`);
190
+ }
191
+
192
+ lines.push("", "## Agent Prompt", "", pack.agentPrompt, "");
193
+ return lines.join("\n");
194
+ }
195
+
196
+ export async function writeRewritePackOutput(outputPath, content) {
197
+ return writePageAuditOutput(outputPath, content);
198
+ }