@spaceflow/review 0.29.1

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 (41) hide show
  1. package/CHANGELOG.md +533 -0
  2. package/README.md +124 -0
  3. package/dist/551.js +9 -0
  4. package/dist/index.js +5704 -0
  5. package/package.json +50 -0
  6. package/src/README.md +364 -0
  7. package/src/__mocks__/@anthropic-ai/claude-agent-sdk.js +3 -0
  8. package/src/__mocks__/json-stringify-pretty-compact.ts +4 -0
  9. package/src/deletion-impact.service.spec.ts +974 -0
  10. package/src/deletion-impact.service.ts +879 -0
  11. package/src/dto/mcp.dto.ts +42 -0
  12. package/src/index.ts +32 -0
  13. package/src/issue-verify.service.spec.ts +460 -0
  14. package/src/issue-verify.service.ts +309 -0
  15. package/src/locales/en/review.json +31 -0
  16. package/src/locales/index.ts +11 -0
  17. package/src/locales/zh-cn/review.json +31 -0
  18. package/src/parse-title-options.spec.ts +251 -0
  19. package/src/parse-title-options.ts +185 -0
  20. package/src/review-report/formatters/deletion-impact.formatter.ts +144 -0
  21. package/src/review-report/formatters/index.ts +4 -0
  22. package/src/review-report/formatters/json.formatter.ts +8 -0
  23. package/src/review-report/formatters/markdown.formatter.ts +291 -0
  24. package/src/review-report/formatters/terminal.formatter.ts +130 -0
  25. package/src/review-report/index.ts +4 -0
  26. package/src/review-report/review-report.module.ts +8 -0
  27. package/src/review-report/review-report.service.ts +58 -0
  28. package/src/review-report/types.ts +26 -0
  29. package/src/review-spec/index.ts +3 -0
  30. package/src/review-spec/review-spec.module.ts +10 -0
  31. package/src/review-spec/review-spec.service.spec.ts +1543 -0
  32. package/src/review-spec/review-spec.service.ts +902 -0
  33. package/src/review-spec/types.ts +143 -0
  34. package/src/review.command.ts +244 -0
  35. package/src/review.config.ts +58 -0
  36. package/src/review.mcp.ts +184 -0
  37. package/src/review.module.ts +52 -0
  38. package/src/review.service.spec.ts +3007 -0
  39. package/src/review.service.ts +2603 -0
  40. package/tsconfig.json +8 -0
  41. package/vitest.config.ts +34 -0
@@ -0,0 +1,185 @@
1
+ import type { LLMMode, VerboseLevel } from "@spaceflow/core";
2
+ import type { AnalyzeDeletionsMode } from "./review.config";
3
+ import { normalizeVerbose } from "@spaceflow/core";
4
+
5
+ /**
6
+ * 从 PR 标题中解析的命令参数
7
+ */
8
+ export interface TitleOptions {
9
+ llmMode?: LLMMode;
10
+ verbose?: VerboseLevel;
11
+ dryRun?: boolean;
12
+ includes?: string[];
13
+ verifyFixes?: boolean;
14
+ analyzeDeletions?: AnalyzeDeletionsMode;
15
+ deletionOnly?: boolean;
16
+ deletionAnalysisMode?: LLMMode;
17
+ }
18
+
19
+ /**
20
+ * 从 PR 标题中解析命令参数
21
+ *
22
+ * 支持的格式:标题末尾 [/review -l openai -v 2]
23
+ *
24
+ * 支持的参数:
25
+ * - `-l, --llm-mode <mode>`: LLM 模式 (claude-code, openai, gemini)
26
+ * - `-v, --verbose [level]`: 详细输出级别 (1 或 2)
27
+ * - `-d, --dry-run`: 仅打印不执行
28
+ * - `-i, --includes <pattern>`: 文件过滤模式
29
+ * - `--verify-fixes`: 验证历史问题
30
+ * - `--no-verify-fixes`: 禁用历史问题验证
31
+ * - `--analyze-deletions`: 分析删除代码
32
+ * - `--deletion-only`: 仅执行删除代码分析
33
+ * - `--deletion-analysis-mode <mode>`: 删除分析模式
34
+ *
35
+ * @param title PR 标题
36
+ * @returns 解析出的命令参数,如果没有找到命令则返回空对象
37
+ */
38
+ export function parseTitleOptions(title: string): TitleOptions {
39
+ const options: TitleOptions = {};
40
+
41
+ // 匹配 [/review ...] 或 [/ai-review ...] (保持向后兼容) 格式
42
+ const match = title.match(/\[\/(review|ai-review)\s+([^\]]+)\]/i);
43
+ if (!match) {
44
+ return options;
45
+ }
46
+
47
+ const argsString = match[2].trim();
48
+ const args = parseArgs(argsString);
49
+
50
+ for (let i = 0; i < args.length; i++) {
51
+ const arg = args[i];
52
+
53
+ switch (arg) {
54
+ case "-l":
55
+ case "--llm-mode": {
56
+ const value = args[++i];
57
+ if (value && isValidLLMMode(value)) {
58
+ options.llmMode = value;
59
+ }
60
+ break;
61
+ }
62
+
63
+ case "-v":
64
+ case "--verbose": {
65
+ const nextArg = args[i + 1];
66
+ if (nextArg && !nextArg.startsWith("-")) {
67
+ const level = parseInt(nextArg, 10);
68
+ if (level === 1 || level === 2) {
69
+ options.verbose = level;
70
+ i++;
71
+ } else {
72
+ options.verbose = normalizeVerbose(1);
73
+ }
74
+ } else {
75
+ options.verbose = normalizeVerbose(1);
76
+ }
77
+ break;
78
+ }
79
+
80
+ case "-d":
81
+ case "--dry-run":
82
+ options.dryRun = true;
83
+ break;
84
+
85
+ case "-i":
86
+ case "--includes": {
87
+ const value = args[++i];
88
+ if (value && !value.startsWith("-")) {
89
+ if (!options.includes) {
90
+ options.includes = [];
91
+ }
92
+ options.includes.push(value);
93
+ }
94
+ break;
95
+ }
96
+
97
+ case "--verify-fixes":
98
+ options.verifyFixes = true;
99
+ break;
100
+
101
+ case "--no-verify-fixes":
102
+ options.verifyFixes = false;
103
+ break;
104
+
105
+ case "--analyze-deletions": {
106
+ const nextArg = args[i + 1];
107
+ if (nextArg && !nextArg.startsWith("-") && isValidAnalyzeDeletionsMode(nextArg)) {
108
+ options.analyzeDeletions = parseAnalyzeDeletionsValue(nextArg);
109
+ i++;
110
+ } else {
111
+ options.analyzeDeletions = true;
112
+ }
113
+ break;
114
+ }
115
+
116
+ case "--deletion-only":
117
+ options.deletionOnly = true;
118
+ break;
119
+
120
+ case "--deletion-analysis-mode": {
121
+ const value = args[++i];
122
+ if (value && isValidDeletionAnalysisMode(value)) {
123
+ options.deletionAnalysisMode = value;
124
+ }
125
+ break;
126
+ }
127
+ }
128
+ }
129
+
130
+ return options;
131
+ }
132
+
133
+ /**
134
+ * 解析参数字符串为数组
135
+ * 支持引号包裹的参数值
136
+ */
137
+ function parseArgs(argsString: string): string[] {
138
+ const args: string[] = [];
139
+ let current = "";
140
+ let inQuote = false;
141
+ let quoteChar = "";
142
+
143
+ for (let i = 0; i < argsString.length; i++) {
144
+ const char = argsString[i];
145
+
146
+ if ((char === '"' || char === "'") && !inQuote) {
147
+ inQuote = true;
148
+ quoteChar = char;
149
+ } else if (char === quoteChar && inQuote) {
150
+ inQuote = false;
151
+ quoteChar = "";
152
+ } else if (char === " " && !inQuote) {
153
+ if (current) {
154
+ args.push(current);
155
+ current = "";
156
+ }
157
+ } else {
158
+ current += char;
159
+ }
160
+ }
161
+
162
+ if (current) {
163
+ args.push(current);
164
+ }
165
+
166
+ return args;
167
+ }
168
+
169
+ function isValidLLMMode(value: string): value is LLMMode {
170
+ return ["claude-code", "openai", "gemini"].includes(value);
171
+ }
172
+
173
+ function isValidDeletionAnalysisMode(value: string): value is LLMMode {
174
+ return ["openai", "claude-code"].includes(value);
175
+ }
176
+
177
+ function isValidAnalyzeDeletionsMode(value: string): boolean {
178
+ return ["true", "false", "ci", "pr", "terminal"].includes(value);
179
+ }
180
+
181
+ function parseAnalyzeDeletionsValue(value: string): AnalyzeDeletionsMode {
182
+ if (value === "true") return true;
183
+ if (value === "false") return false;
184
+ return value as "ci" | "pr" | "terminal";
185
+ }
@@ -0,0 +1,144 @@
1
+ import type { DeletionImpactResult, DeletionImpact } from "../../review-spec/types";
2
+
3
+ const RISK_EMOJI: Record<string, string> = {
4
+ high: "🔴",
5
+ medium: "🟡",
6
+ low: "🟢",
7
+ none: "⚪",
8
+ };
9
+
10
+ const RISK_LABEL: Record<string, string> = {
11
+ high: "高风险",
12
+ medium: "中风险",
13
+ low: "低风险",
14
+ none: "无风险",
15
+ };
16
+
17
+ export interface DeletionImpactReportOptions {
18
+ includeJsonData?: boolean;
19
+ }
20
+
21
+ const DELETION_IMPACT_DATA_START = "<!-- spaceflow-deletion-impact-data-start -->";
22
+ const DELETION_IMPACT_DATA_END = "<!-- spaceflow-deletion-impact-data-end -->";
23
+
24
+ export class DeletionImpactFormatter {
25
+ format(result: DeletionImpactResult, options: DeletionImpactReportOptions = {}): string {
26
+ const { includeJsonData = true } = options;
27
+ const lines: string[] = [];
28
+
29
+ lines.push("## 🗑️ 删除代码影响分析\n");
30
+
31
+ // 防御性检查:确保 impacts 是数组
32
+ const impacts = result.impacts && Array.isArray(result.impacts) ? result.impacts : [];
33
+
34
+ if (impacts.length === 0) {
35
+ lines.push("✅ **未发现有风险的代码删除**\n");
36
+ lines.push(result.summary);
37
+ return lines.join("\n");
38
+ }
39
+
40
+ // 统计风险等级
41
+ const highRisk = impacts.filter((i) => i.riskLevel === "high");
42
+ const mediumRisk = impacts.filter((i) => i.riskLevel === "medium");
43
+ const lowRisk = impacts.filter((i) => i.riskLevel === "low");
44
+
45
+ lines.push("### 📊 风险概览\n");
46
+ lines.push(`| 风险等级 | 数量 |`);
47
+ lines.push(`|----------|------|`);
48
+ if (highRisk.length > 0) {
49
+ lines.push(`| ${RISK_EMOJI.high} 高风险 | ${highRisk.length} |`);
50
+ }
51
+ if (mediumRisk.length > 0) {
52
+ lines.push(`| ${RISK_EMOJI.medium} 中风险 | ${mediumRisk.length} |`);
53
+ }
54
+ if (lowRisk.length > 0) {
55
+ lines.push(`| ${RISK_EMOJI.low} 低风险 | ${lowRisk.length} |`);
56
+ }
57
+ lines.push("");
58
+
59
+ // 详情折叠
60
+ lines.push("<details>");
61
+ lines.push("<summary>📋 点击查看详情</summary>\n");
62
+
63
+ // 高风险项详情
64
+ if (highRisk.length > 0) {
65
+ lines.push("### 🔴 高风险删除\n");
66
+ lines.push(this.formatImpactList(highRisk));
67
+ }
68
+
69
+ // 中风险项详情
70
+ if (mediumRisk.length > 0) {
71
+ lines.push("### 🟡 中风险删除\n");
72
+ lines.push(this.formatImpactList(mediumRisk));
73
+ }
74
+
75
+ // 低风险项
76
+ if (lowRisk.length > 0) {
77
+ lines.push("### 🟢 低风险删除\n");
78
+ lines.push(this.formatImpactList(lowRisk));
79
+ }
80
+
81
+ // 总结
82
+ lines.push("\n### 📝 总结\n");
83
+ lines.push(result.summary);
84
+
85
+ lines.push("\n</details>");
86
+
87
+ return lines.join("\n");
88
+ }
89
+
90
+ private formatImpactList(impacts: DeletionImpact[]): string {
91
+ const lines: string[] = [];
92
+
93
+ for (const impact of impacts) {
94
+ const emoji = RISK_EMOJI[impact.riskLevel] || RISK_EMOJI.none;
95
+ const label = RISK_LABEL[impact.riskLevel] || "未知";
96
+ const codePreview =
97
+ impact.deletedCode.length > 50
98
+ ? impact.deletedCode.slice(0, 50) + "..."
99
+ : impact.deletedCode;
100
+
101
+ lines.push(`#### ${emoji} \`${impact.file}\`\n`);
102
+ lines.push(`- **风险等级**: ${label}`);
103
+ lines.push(`- **删除代码**: \`${codePreview.replace(/\n/g, " ")}\``);
104
+
105
+ if (impact.affectedFiles.length > 0) {
106
+ lines.push(`- **受影响文件**:`);
107
+ for (const file of impact.affectedFiles.slice(0, 5)) {
108
+ lines.push(` - \`${file}\``);
109
+ }
110
+ if (impact.affectedFiles.length > 5) {
111
+ lines.push(` - ... 还有 ${impact.affectedFiles.length - 5} 个文件`);
112
+ }
113
+ }
114
+
115
+ lines.push(`- **影响分析**: ${impact.reason}`);
116
+
117
+ if (impact.suggestion) {
118
+ lines.push(`- **建议**: ${impact.suggestion}`);
119
+ }
120
+
121
+ lines.push("");
122
+ }
123
+
124
+ return lines.join("\n");
125
+ }
126
+
127
+ parse(content: string): DeletionImpactResult | null {
128
+ const startIndex = content.indexOf(DELETION_IMPACT_DATA_START);
129
+ const endIndex = content.indexOf(DELETION_IMPACT_DATA_END);
130
+
131
+ if (startIndex === -1 || endIndex === -1 || startIndex >= endIndex) {
132
+ return null;
133
+ }
134
+
135
+ const jsonStart = startIndex + DELETION_IMPACT_DATA_START.length;
136
+ const jsonContent = content.slice(jsonStart, endIndex).trim();
137
+
138
+ try {
139
+ return JSON.parse(jsonContent) as DeletionImpactResult;
140
+ } catch {
141
+ return null;
142
+ }
143
+ }
144
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./markdown.formatter";
2
+ export * from "./json.formatter";
3
+ export * from "./terminal.formatter";
4
+ export * from "./deletion-impact.formatter";
@@ -0,0 +1,8 @@
1
+ import { ReviewResult } from "../../review-spec/types";
2
+ import { ReportOptions, ReviewReportFormatter } from "../types";
3
+
4
+ export class JsonFormatter implements ReviewReportFormatter {
5
+ format(result: ReviewResult, _options: ReportOptions = {}): string {
6
+ return JSON.stringify(result, null, 2);
7
+ }
8
+ }
@@ -0,0 +1,291 @@
1
+ import { extname } from "path";
2
+ import {
3
+ FileSummary,
4
+ ReviewIssue,
5
+ ReviewResult,
6
+ ReviewStats,
7
+ SEVERITY_EMOJI,
8
+ } from "../../review-spec/types";
9
+ import { ParsedReport, ReportOptions, ReviewReportFormatter, ReviewReportParser } from "../types";
10
+ import { DeletionImpactFormatter } from "./deletion-impact.formatter";
11
+
12
+ const REVIEW_DATA_START = "<!-- spaceflow-review-data-start -->";
13
+ const REVIEW_DATA_END = "<!-- spaceflow-review-data-end -->";
14
+
15
+ function formatDateToUTC8(dateStr: string): string {
16
+ const date = new Date(dateStr);
17
+ if (isNaN(date.getTime())) return dateStr;
18
+ return date.toLocaleString("zh-CN", {
19
+ timeZone: "Asia/Shanghai",
20
+ year: "numeric",
21
+ month: "2-digit",
22
+ day: "2-digit",
23
+ hour: "2-digit",
24
+ minute: "2-digit",
25
+ second: "2-digit",
26
+ hour12: false,
27
+ });
28
+ }
29
+
30
+ export class MarkdownFormatter implements ReviewReportFormatter, ReviewReportParser {
31
+ static clearReviewData(content: string, replaceData: string): string {
32
+ return content
33
+ .replace(new RegExp(`${REVIEW_DATA_START}[\\s\\S]*?${REVIEW_DATA_END}`, "g"), replaceData)
34
+ .trim();
35
+ }
36
+ private readonly deletionImpactFormatter = new DeletionImpactFormatter();
37
+
38
+ private formatIssueList(issues: ReviewIssue[]): string {
39
+ const lines: string[] = [];
40
+ for (const issue of issues) {
41
+ const severityEmoji = SEVERITY_EMOJI[issue.severity] || SEVERITY_EMOJI.error;
42
+ lines.push(`### ${issue.fixed ? "🟢" : severityEmoji} ${issue.file}:${issue.line}\n`);
43
+ lines.push(`- **问题**: ${issue.reason}`);
44
+ lines.push(`- **规则**: \`${issue.ruleId}\` (来自 \`${issue.specFile}\`)`);
45
+ if (issue.commit) {
46
+ lines.push(`- **Commit**: ${issue.commit}`);
47
+ }
48
+ lines.push(`- **开发人员**: ${issue.author ? "@" + issue.author.login : "未知"}`);
49
+ if (issue.date) {
50
+ lines.push(`- **发现时间**: ${formatDateToUTC8(issue.date)}`);
51
+ }
52
+ if (issue.fixed) {
53
+ lines.push(`- **修复时间**: ${formatDateToUTC8(issue.fixed)}`);
54
+ }
55
+ if (issue.suggestion) {
56
+ const ext = extname(issue.file).slice(1) || "";
57
+ const cleanSuggestion = issue.suggestion.replace(/```/g, "//").trim();
58
+ const lineCount = cleanSuggestion.split("\n").length;
59
+ lines.push("- **建议**:");
60
+ if (lineCount < 4) {
61
+ lines.push(`\`\`\`${ext}`);
62
+ lines.push(cleanSuggestion);
63
+ lines.push("```");
64
+ } else {
65
+ lines.push("<details>");
66
+ lines.push("<summary>💡 查看建议</summary>\n");
67
+ lines.push(`\`\`\`${ext}`);
68
+ lines.push(cleanSuggestion);
69
+ lines.push("```");
70
+ lines.push("\n</details>");
71
+ }
72
+ }
73
+ // 渲染消息(reactions 统计 + replies 详情,合并到一个折叠块)
74
+ const hasReactions = issue.reactions && issue.reactions.length > 0;
75
+ const hasReplies = issue.replies && issue.replies.length > 0;
76
+ if (hasReactions || hasReplies) {
77
+ const reactionCount = hasReactions
78
+ ? issue.reactions!.reduce((sum, r) => sum + r.users.length, 0)
79
+ : 0;
80
+ const replyCount = hasReplies ? issue.replies!.length : 0;
81
+ const totalCount = reactionCount + replyCount;
82
+ lines.push("<details>");
83
+ lines.push(`<summary>💬 消息 (${totalCount} 条)</summary>\n`);
84
+ // 先显示 reactions 统计(格式:👎(1) 👍(2))
85
+ if (hasReactions) {
86
+ const reactionStats = issue.reactions!.map((r) => {
87
+ const emoji = this.getReactionEmoji(r.content);
88
+ return `${emoji}(${r.users.length})`;
89
+ });
90
+ lines.push(`> ${reactionStats.join(" ")}`);
91
+ }
92
+ // 再显示 replies 详情
93
+ if (hasReplies) {
94
+ for (const reply of issue.replies!) {
95
+ const time = reply.createdAt ? formatDateToUTC8(reply.createdAt) : "";
96
+ lines.push(`> @${reply.user.login} ${time ? `(${time})` : ""}:`);
97
+ lines.push(`> ${reply.body.split("\n").join("\n> ")}`);
98
+ lines.push("");
99
+ }
100
+ }
101
+ lines.push("</details>");
102
+ }
103
+ lines.push("");
104
+ }
105
+ return lines.join("\n");
106
+ }
107
+
108
+ /** 将 reaction content 转换为 emoji */
109
+ private getReactionEmoji(content: string): string {
110
+ const emojiMap: Record<string, string> = {
111
+ "+1": "👍",
112
+ "-1": "👎",
113
+ laugh: "😄",
114
+ hooray: "🎉",
115
+ confused: "😕",
116
+ heart: "❤️",
117
+ rocket: "🚀",
118
+ eyes: "👀",
119
+ };
120
+ return emojiMap[content] || content;
121
+ }
122
+
123
+ private formatFileSummaries(summaries: FileSummary[], issues: ReviewIssue[]): string {
124
+ if (summaries.length === 0) {
125
+ return "没有需要审查的文件";
126
+ }
127
+
128
+ const issuesByFile = new Map<string, { resolved: number; unresolved: number }>();
129
+ for (const issue of issues) {
130
+ if (issue.valid === "false") continue;
131
+ const stats = issuesByFile.get(issue.file) || { resolved: 0, unresolved: 0 };
132
+ if (issue.fixed) {
133
+ stats.resolved++;
134
+ } else {
135
+ stats.unresolved++;
136
+ }
137
+ issuesByFile.set(issue.file, stats);
138
+ }
139
+
140
+ const lines: string[] = [];
141
+ lines.push("| 文件 | 🟢 | 🔴 | 总结 |");
142
+ lines.push("|------|----|----|------|");
143
+
144
+ for (const fileSummary of summaries) {
145
+ const stats = issuesByFile.get(fileSummary.file) || { resolved: 0, unresolved: 0 };
146
+ const summaryText = fileSummary.summary
147
+ .split("\n")
148
+ .filter((line) => line.trim())
149
+ .join("<br>");
150
+ lines.push(
151
+ `| \`${fileSummary.file}\` | ${stats.resolved} | ${stats.unresolved} | ${summaryText} |`,
152
+ );
153
+ }
154
+
155
+ return lines.join("\n");
156
+ }
157
+
158
+ parse(content: string): ParsedReport | null {
159
+ const startIndex = content.indexOf(REVIEW_DATA_START);
160
+ const endIndex = content.indexOf(REVIEW_DATA_END);
161
+
162
+ if (startIndex === -1 || endIndex === -1 || startIndex >= endIndex) {
163
+ return null;
164
+ }
165
+
166
+ const dataStart = startIndex + REVIEW_DATA_START.length;
167
+ const encodedContent = content.slice(dataStart, endIndex).trim();
168
+
169
+ try {
170
+ // 尝试 Base64 解码,如果失败则尝试直接解析 JSON(兼容旧格式)
171
+ let jsonContent: string;
172
+ try {
173
+ jsonContent = Buffer.from(encodedContent, "base64").toString("utf-8");
174
+ // 验证解码后是否为有效 JSON
175
+ JSON.parse(jsonContent);
176
+ } catch {
177
+ // Base64 解码失败或解码后不是有效 JSON,尝试直接解析(旧格式)
178
+ jsonContent = encodedContent;
179
+ }
180
+ const parsed = JSON.parse(jsonContent);
181
+ const hasReanalysisRequest = content.includes("- [x]");
182
+
183
+ // 新格式:完整的 ReviewResult
184
+ return {
185
+ result: parsed as ReviewResult,
186
+ hasReanalysisRequest,
187
+ };
188
+ } catch {
189
+ return null;
190
+ }
191
+ }
192
+
193
+ format(result: ReviewResult, options: ReportOptions = {}): string {
194
+ const {
195
+ prNumber,
196
+ includeReanalysisCheckbox = true,
197
+ includeJsonData = true,
198
+ reviewCommentMarker,
199
+ } = options;
200
+
201
+ const lines: string[] = [];
202
+
203
+ if (reviewCommentMarker) {
204
+ lines.push(reviewCommentMarker);
205
+ }
206
+
207
+ const validIssues = result.issues.filter((issue) => issue.valid !== "false");
208
+ const invalidIssues = result.issues.filter((issue) => issue.valid === "false");
209
+
210
+ lines.push("# 🤖 AI 代码审查报告\n");
211
+
212
+ // 输出统计信息(如果有)
213
+ if (result.stats) {
214
+ lines.push(this.formatStats(result.stats));
215
+ lines.push("");
216
+ }
217
+
218
+ // 输出 PR 功能描述
219
+ if (result.description) {
220
+ lines.push("## 📋 功能概述\n");
221
+ // 将 description 中的标题级别降低(h1->h3, h2->h4, h3->h5 等)
222
+ const adjustedDescription = result.description.replace(/^(#{1,4})\s/gm, "$1## ");
223
+ lines.push(adjustedDescription);
224
+ lines.push("");
225
+ }
226
+
227
+ lines.push("## 📝 新增代码分析\n");
228
+ lines.push("### 📊 审查概览\n");
229
+ lines.push(this.formatFileSummaries(result.summary, validIssues));
230
+ lines.push("<details>");
231
+ lines.push("<summary>📋 点击查看详情</summary>\n");
232
+ lines.push("### 🐛 详细问题\n");
233
+ if (validIssues.length === 0 && invalidIssues.length === 0) {
234
+ lines.push("✅ **未发现问题**\n");
235
+ } else {
236
+ if (validIssues.length === 0) {
237
+ lines.push("✅ **未发现有效问题**\n");
238
+ } else {
239
+ lines.push(`⚠️ **发现 ${validIssues.length} 个问题**\n`);
240
+ lines.push(this.formatIssueList(validIssues));
241
+ }
242
+
243
+ if (invalidIssues.length > 0) {
244
+ lines.push("\n<details>");
245
+ lines.push(`<summary>🚫 无效问题 (${invalidIssues.length} 个)</summary>\n`);
246
+ lines.push(this.formatIssueList(invalidIssues));
247
+ lines.push("\n</details>");
248
+ }
249
+ }
250
+ lines.push("\n</details>");
251
+
252
+ // if (includeReanalysisCheckbox) {
253
+ // lines.push("\n---");
254
+ // lines.push("<details>");
255
+ // lines.push("<summary>🔄 重新分析</summary>\n");
256
+ // if (prNumber) {
257
+ // lines.push("- [ ] 勾选此复选框后保存评论,将触发重新分析");
258
+ // }
259
+ // lines.push("\n</details>");
260
+ // }
261
+
262
+ // 输出删除代码影响分析报告
263
+ if (result.deletionImpact) {
264
+ lines.push("\n---\n");
265
+ lines.push(this.deletionImpactFormatter.format(result.deletionImpact, { includeJsonData }));
266
+ }
267
+
268
+ if (includeJsonData) {
269
+ lines.push("");
270
+ lines.push("<details>");
271
+ lines.push("<summary>📊 审查数据 (JSON)</summary>\n");
272
+ lines.push(REVIEW_DATA_START);
273
+ lines.push(Buffer.from(JSON.stringify(result)).toString("base64"));
274
+ lines.push(REVIEW_DATA_END);
275
+ lines.push("\n</details>");
276
+ }
277
+
278
+ return lines.join("\n");
279
+ }
280
+
281
+ formatStats(stats: ReviewStats, prNumber?: number): string {
282
+ const title = prNumber ? `PR #${prNumber} Review 状态统计` : "Review 状态统计";
283
+ const lines = [`## 📊 ${title}\n`, `| 指标 | 数量 |`, `|------|------|`];
284
+ lines.push(`| 总问题数 | ${stats.total} |`);
285
+ lines.push(`| ✅ 已修复 | ${stats.fixed} |`);
286
+ lines.push(`| ❌ 无效 | ${stats.invalid} |`);
287
+ lines.push(`| ⚠️ 待处理 | ${stats.pending} |`);
288
+ lines.push(`| 修复率 | ${stats.fixRate}% |`);
289
+ return lines.join("\n");
290
+ }
291
+ }