@wuyanbin/ai-code-review-cli-remote 1.0.11 → 1.0.12
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/dist/core/context.d.ts +6 -0
- package/dist/core/context.d.ts.map +1 -0
- package/dist/core/context.js +154 -0
- package/dist/core/context.js.map +1 -0
- package/dist/core/diff.d.ts +14 -0
- package/dist/core/diff.d.ts.map +1 -0
- package/dist/core/diff.js +179 -0
- package/dist/core/diff.js.map +1 -0
- package/dist/core/model.d.ts +6 -0
- package/dist/core/model.d.ts.map +1 -0
- package/dist/core/model.js +120 -0
- package/dist/core/model.js.map +1 -0
- package/dist/core/output.d.ts +4 -0
- package/dist/core/output.d.ts.map +1 -0
- package/dist/core/output.js +31 -0
- package/dist/core/output.js.map +1 -0
- package/dist/core/prompt.d.ts +17 -0
- package/dist/core/prompt.d.ts.map +1 -0
- package/dist/core/prompt.js +92 -0
- package/dist/core/prompt.js.map +1 -0
- package/dist/core/reviewer.d.ts +6 -0
- package/dist/core/reviewer.d.ts.map +1 -0
- package/dist/core/reviewer.js +123 -0
- package/dist/core/reviewer.js.map +1 -0
- package/dist/core/summarizer.d.ts +3 -0
- package/dist/core/summarizer.d.ts.map +1 -0
- package/dist/core/summarizer.js +48 -0
- package/dist/core/summarizer.js.map +1 -0
- package/dist/gitlab/api.d.ts +13 -0
- package/dist/gitlab/api.d.ts.map +1 -0
- package/dist/gitlab/api.js +317 -0
- package/dist/gitlab/api.js.map +1 -0
- package/dist/gitlab.d.ts.map +1 -1
- package/dist/gitlab.js +25 -21
- package/dist/gitlab.js.map +1 -1
- package/dist/http.d.ts +1 -0
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +13 -0
- package/dist/http.js.map +1 -1
- package/dist/platform/config.d.ts +10 -0
- package/dist/platform/config.d.ts.map +1 -0
- package/dist/platform/config.js +156 -0
- package/dist/platform/config.js.map +1 -0
- package/dist/platform/report.d.ts +9 -0
- package/dist/platform/report.d.ts.map +1 -0
- package/dist/platform/report.js +32 -0
- package/dist/platform/report.js.map +1 -0
- package/dist/shared/http.d.ts +3 -0
- package/dist/shared/http.d.ts.map +1 -0
- package/dist/shared/http.js +18 -0
- package/dist/shared/http.js.map +1 -0
- package/dist/shared/identity.d.ts +18 -0
- package/dist/shared/identity.d.ts.map +1 -0
- package/dist/shared/identity.js +51 -0
- package/dist/shared/identity.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SummaryPromptBuilder = exports.PromptBuilder = void 0;
|
|
4
|
+
const diff_1 = require("./diff");
|
|
5
|
+
const COMMON_RULES = [
|
|
6
|
+
"重点关注潜在 bug、性能问题、可读性与安全性(如 XSS、注入、敏感信息泄露)",
|
|
7
|
+
"代码要有适当的错误处理",
|
|
8
|
+
"避免代码重复,注意抽离复用逻辑",
|
|
9
|
+
"函数和变量命名要语义化,见名知意",
|
|
10
|
+
"复杂逻辑要添加注释说明",
|
|
11
|
+
];
|
|
12
|
+
class PromptBuilder {
|
|
13
|
+
constructor() {
|
|
14
|
+
this.diff = "";
|
|
15
|
+
this.rules = [];
|
|
16
|
+
this.context = "";
|
|
17
|
+
}
|
|
18
|
+
setDiff(diff) {
|
|
19
|
+
this.diff = diff;
|
|
20
|
+
return this;
|
|
21
|
+
}
|
|
22
|
+
setRules(custom) {
|
|
23
|
+
this.rules = [...COMMON_RULES, ...custom];
|
|
24
|
+
return this;
|
|
25
|
+
}
|
|
26
|
+
setContext(context) {
|
|
27
|
+
this.context = context;
|
|
28
|
+
return this;
|
|
29
|
+
}
|
|
30
|
+
buildSystem() {
|
|
31
|
+
return "你是一个资深的代码审查助手,需要根据给定的 Git diff 和项目规范,对改动的代码进行严格的 Code Review。";
|
|
32
|
+
}
|
|
33
|
+
buildUser() {
|
|
34
|
+
return [
|
|
35
|
+
"请你只针对 diff 中【新增或修改】的代码行给出审查意见。",
|
|
36
|
+
"输出格式要求:必须返回严格的 JSON,形如:",
|
|
37
|
+
'{"comments":[{"file":"src/xxx.ts","line":123,"code":"该行完整代码","severity":"warning","message":"问题描述","suggestion":"修复建议"}]}',
|
|
38
|
+
"注意:severity 只能是 info / warning / error;file 必须使用 diff 中出现的路径;line 必须是 new file 侧真实行号;只能评论新增行。",
|
|
39
|
+
"",
|
|
40
|
+
"可评论行索引:",
|
|
41
|
+
(0, diff_1.buildAddedLinesIndexText)(this.diff),
|
|
42
|
+
"",
|
|
43
|
+
"审查上下文(MR 元信息、项目规则、变更附近源码):",
|
|
44
|
+
this.context,
|
|
45
|
+
"",
|
|
46
|
+
"项目规范:",
|
|
47
|
+
this.rules.join("\n"),
|
|
48
|
+
"",
|
|
49
|
+
"Git diff:",
|
|
50
|
+
this.diff,
|
|
51
|
+
].join("\n");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
exports.PromptBuilder = PromptBuilder;
|
|
55
|
+
class SummaryPromptBuilder {
|
|
56
|
+
constructor() {
|
|
57
|
+
this.diff = "";
|
|
58
|
+
}
|
|
59
|
+
setDiff(diff) {
|
|
60
|
+
this.diff = diff;
|
|
61
|
+
return this;
|
|
62
|
+
}
|
|
63
|
+
buildSystem() {
|
|
64
|
+
return [
|
|
65
|
+
"你是一位资深开发工程师,擅长分析代码改动并生成清晰的技术摘要。",
|
|
66
|
+
"根据给定 Git diff 生成中文 Markdown 汇总。",
|
|
67
|
+
"必须直接输出纯 Markdown 文本,不要返回 JSON,不要用代码块包裹。",
|
|
68
|
+
].join("\n");
|
|
69
|
+
}
|
|
70
|
+
buildUser() {
|
|
71
|
+
return [
|
|
72
|
+
"请根据以下 Git diff 生成一份代码改动汇总报告。",
|
|
73
|
+
"",
|
|
74
|
+
"## 概述",
|
|
75
|
+
"用 1-2 句话说明整体目的和范围。",
|
|
76
|
+
"",
|
|
77
|
+
"## 主要变更",
|
|
78
|
+
"按新功能、Bug 修复、重构优化、依赖更新、配置调整分类列出。",
|
|
79
|
+
"",
|
|
80
|
+
"## 受影响文件",
|
|
81
|
+
"用表格列出文件路径、变更类型、说明。",
|
|
82
|
+
"",
|
|
83
|
+
"## 注意事项",
|
|
84
|
+
"列出需要关注的风险;没有则写\u201C无\u201D。",
|
|
85
|
+
"",
|
|
86
|
+
"Git diff:",
|
|
87
|
+
this.diff,
|
|
88
|
+
].join("\n");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
exports.SummaryPromptBuilder = SummaryPromptBuilder;
|
|
92
|
+
//# sourceMappingURL=prompt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompt.js","sourceRoot":"","sources":["../../src/core/prompt.ts"],"names":[],"mappings":";;;AAAA,iCAAkD;AAElD,MAAM,YAAY,GAAG;IACnB,0CAA0C;IAC1C,aAAa;IACb,iBAAiB;IACjB,kBAAkB;IAClB,aAAa;CACd,CAAC;AAEF,MAAa,aAAa;IAA1B;QACU,SAAI,GAAG,EAAE,CAAC;QACV,UAAK,GAAa,EAAE,CAAC;QACrB,YAAO,GAAG,EAAE,CAAC;IAyCvB,CAAC;IAvCC,OAAO,CAAC,IAAY;QAClB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,QAAQ,CAAC,MAAgB;QACvB,IAAI,CAAC,KAAK,GAAG,CAAC,GAAG,YAAY,EAAE,GAAG,MAAM,CAAC,CAAC;QAC1C,OAAO,IAAI,CAAC;IACd,CAAC;IAED,UAAU,CAAC,OAAe;QACxB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,WAAW;QACT,OAAO,+DAA+D,CAAC;IACzE,CAAC;IAED,SAAS;QACP,OAAO;YACL,gCAAgC;YAChC,yBAAyB;YACzB,2HAA2H;YAC3H,+FAA+F;YAC/F,EAAE;YACF,SAAS;YACT,IAAA,+BAAwB,EAAC,IAAI,CAAC,IAAI,CAAC;YACnC,EAAE;YACF,4BAA4B;YAC5B,IAAI,CAAC,OAAO;YACZ,EAAE;YACF,OAAO;YACP,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC;YACrB,EAAE;YACF,WAAW;YACX,IAAI,CAAC,IAAI;SACV,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACf,CAAC;CACF;AA5CD,sCA4CC;AAED,MAAa,oBAAoB;IAAjC;QACU,SAAI,GAAG,EAAE,CAAC;IAmCpB,CAAC;IAjCC,OAAO,CAAC,IAAY;QAClB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,WAAW;QACT,OAAO;YACL,iCAAiC;YACjC,iCAAiC;YACjC,yCAAyC;SAC1C,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACf,CAAC;IAED,SAAS;QACP,OAAO;YACL,8BAA8B;YAC9B,EAAE;YACF,OAAO;YACP,oBAAoB;YACpB,EAAE;YACF,SAAS;YACT,iCAAiC;YACjC,EAAE;YACF,UAAU;YACV,oBAAoB;YACpB,EAAE;YACF,SAAS;YACT,8BAA8B;YAC9B,EAAE;YACF,WAAW;YACX,IAAI,CAAC,IAAI;SACV,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACf,CAAC;CACF;AApCD,oDAoCC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reviewer.d.ts","sourceRoot":"","sources":["../../src/core/reviewer.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAa,UAAU,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAyFxE,wBAAsB,UAAU,CAC9B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,gBAAgB,EACxB,OAAO,GAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAA;CAAO,GACtD,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CA2E5B"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.reviewCode = reviewCode;
|
|
4
|
+
const jsonrepair_1 = require("jsonrepair");
|
|
5
|
+
const zod_1 = require("zod");
|
|
6
|
+
const context_1 = require("./context");
|
|
7
|
+
const diff_1 = require("./diff");
|
|
8
|
+
const identity_1 = require("../shared/identity");
|
|
9
|
+
const model_1 = require("./model");
|
|
10
|
+
const prompt_1 = require("./prompt");
|
|
11
|
+
const SEVERITY_ORDER = { info: 0, warning: 1, error: 2 };
|
|
12
|
+
const SHARD_CONCURRENCY = 3;
|
|
13
|
+
const aiCommentSchema = zod_1.z.object({
|
|
14
|
+
file: zod_1.z.string().trim().min(1),
|
|
15
|
+
line: zod_1.z.number().int().positive(),
|
|
16
|
+
code: zod_1.z.string().optional(),
|
|
17
|
+
severity: zod_1.z.enum(["info", "warning", "error"]),
|
|
18
|
+
message: zod_1.z.string().trim().min(1),
|
|
19
|
+
suggestion: zod_1.z.string().optional(),
|
|
20
|
+
});
|
|
21
|
+
const aiResponseSchema = zod_1.z.object({
|
|
22
|
+
comments: zod_1.z.array(zod_1.z.unknown()).default([]),
|
|
23
|
+
});
|
|
24
|
+
function stripFences(text) {
|
|
25
|
+
return (text ?? "")
|
|
26
|
+
.replace(/^```(?:json)?\s*/i, "")
|
|
27
|
+
.replace(/```$/i, "")
|
|
28
|
+
.trim();
|
|
29
|
+
}
|
|
30
|
+
function extractJsonObject(content) {
|
|
31
|
+
const text = stripFences(content);
|
|
32
|
+
const start = text.indexOf("{");
|
|
33
|
+
const end = text.lastIndexOf("}");
|
|
34
|
+
if (start >= 0 && end > start)
|
|
35
|
+
return text.slice(start, end + 1);
|
|
36
|
+
return text;
|
|
37
|
+
}
|
|
38
|
+
function parseAIResponse(content) {
|
|
39
|
+
const jsonText = extractJsonObject(content);
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse((0, jsonrepair_1.jsonrepair)(jsonText));
|
|
42
|
+
const response = aiResponseSchema.safeParse(parsed);
|
|
43
|
+
if (!response.success)
|
|
44
|
+
return { comments: [] };
|
|
45
|
+
const comments = [];
|
|
46
|
+
for (const rawComment of response.data.comments) {
|
|
47
|
+
const comment = aiCommentSchema.safeParse(rawComment);
|
|
48
|
+
if (comment.success)
|
|
49
|
+
comments.push(comment.data);
|
|
50
|
+
}
|
|
51
|
+
return { comments };
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
console.error("[reviewer] AI 返回 JSON 解析失败");
|
|
55
|
+
console.error(error);
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function runWithConcurrency(tasks, concurrency) {
|
|
60
|
+
const results = new Array(tasks.length);
|
|
61
|
+
let next = 0;
|
|
62
|
+
async function worker() {
|
|
63
|
+
while (next < tasks.length) {
|
|
64
|
+
const index = next++;
|
|
65
|
+
results[index] = await tasks[index]();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, tasks.length) }, () => worker()));
|
|
69
|
+
return results;
|
|
70
|
+
}
|
|
71
|
+
async function reviewCode(diff, config, options = {}) {
|
|
72
|
+
const effectiveDiff = (0, diff_1.filterDiffByIgnore)(diff, config.review.ignore);
|
|
73
|
+
if (!effectiveDiff.trim())
|
|
74
|
+
return { comments: [] };
|
|
75
|
+
if (!config.ai.apiKey || !config.ai.endpoint)
|
|
76
|
+
return null;
|
|
77
|
+
const adapter = (0, model_1.createModelAdapter)(config.ai.endpoint, config.ai.apiKey, config.ai.apiFormat);
|
|
78
|
+
const shards = (0, diff_1.splitDiffByMaxBytes)(effectiveDiff, config.review.shardMaxBytes);
|
|
79
|
+
const minSeverity = SEVERITY_ORDER[config.review.severity];
|
|
80
|
+
console.error(`[reviewer] diff shards=${shards.length} shardMaxBytes=${config.review.shardMaxBytes} concurrency=${SHARD_CONCURRENCY}`);
|
|
81
|
+
const tasks = shards.map((shard, index) => async () => {
|
|
82
|
+
const context = await (0, context_1.collectReviewContext)(shard, options);
|
|
83
|
+
const prompt = new prompt_1.PromptBuilder()
|
|
84
|
+
.setDiff(shard)
|
|
85
|
+
.setRules(config.rules.custom)
|
|
86
|
+
.setContext(context);
|
|
87
|
+
const raw = await adapter.call(config.ai.model, prompt.buildSystem(), prompt.buildUser());
|
|
88
|
+
if (!raw) {
|
|
89
|
+
console.error(`[reviewer] shard ${index + 1}/${shards.length} returned empty`);
|
|
90
|
+
return { index, comments: [], success: false };
|
|
91
|
+
}
|
|
92
|
+
const parsed = parseAIResponse(raw);
|
|
93
|
+
if (!parsed) {
|
|
94
|
+
console.error(`[reviewer] shard ${index + 1}/${shards.length} parse failed`);
|
|
95
|
+
return { index, comments: [], success: false };
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
index,
|
|
99
|
+
comments: (0, diff_1.alignCommentsToDiffAddedLines)(shard, parsed.comments).filter((c) => SEVERITY_ORDER[c.severity] >= minSeverity),
|
|
100
|
+
success: true,
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
const results = await runWithConcurrency(tasks, SHARD_CONCURRENCY);
|
|
104
|
+
const successCount = results.filter((r) => r.success).length;
|
|
105
|
+
if (successCount === 0)
|
|
106
|
+
return null;
|
|
107
|
+
const allComments = results.flatMap((r) => r.comments);
|
|
108
|
+
const seen = new Set();
|
|
109
|
+
let deduped = allComments
|
|
110
|
+
.filter((comment) => {
|
|
111
|
+
const fingerprint = (0, identity_1.buildContentFingerprint)(comment);
|
|
112
|
+
if (seen.has(fingerprint))
|
|
113
|
+
return false;
|
|
114
|
+
seen.add(fingerprint);
|
|
115
|
+
return true;
|
|
116
|
+
})
|
|
117
|
+
.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
|
|
118
|
+
if (deduped.length > config.review.maxComments) {
|
|
119
|
+
deduped = deduped.slice(0, config.review.maxComments);
|
|
120
|
+
}
|
|
121
|
+
return { comments: deduped };
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=reviewer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reviewer.js","sourceRoot":"","sources":["../../src/core/reviewer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"summarizer.d.ts","sourceRoot":"","sources":["../../src/core/summarizer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AA8BjD,wBAAsB,eAAe,CACnC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,gBAAgB,GACvB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAiBxB"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateSummary = generateSummary;
|
|
4
|
+
const diff_1 = require("./diff");
|
|
5
|
+
const model_1 = require("./model");
|
|
6
|
+
const prompt_1 = require("./prompt");
|
|
7
|
+
function cleanMarkdownResponse(content) {
|
|
8
|
+
let text = content.trim();
|
|
9
|
+
if (text.startsWith("```markdown"))
|
|
10
|
+
text = text.slice("```markdown".length);
|
|
11
|
+
else if (text.startsWith("```md"))
|
|
12
|
+
text = text.slice("```md".length);
|
|
13
|
+
else if (text.startsWith("```json"))
|
|
14
|
+
text = text.slice("```json".length);
|
|
15
|
+
else if (text.startsWith("```"))
|
|
16
|
+
text = text.slice(3);
|
|
17
|
+
if (text.endsWith("```"))
|
|
18
|
+
text = text.slice(0, -3);
|
|
19
|
+
text = text.trim();
|
|
20
|
+
if (text.startsWith("{") || text.startsWith('"')) {
|
|
21
|
+
try {
|
|
22
|
+
const parsed = JSON.parse(text);
|
|
23
|
+
if (typeof parsed === "string")
|
|
24
|
+
return parsed.trim();
|
|
25
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
26
|
+
const first = Object.values(parsed).find((value) => typeof value === "string");
|
|
27
|
+
if (first)
|
|
28
|
+
return first.trim();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// keep text as-is
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return text.replace(/\\n/g, "\n").trim();
|
|
36
|
+
}
|
|
37
|
+
async function generateSummary(diff, config) {
|
|
38
|
+
const effectiveDiff = (0, diff_1.filterDiffByIgnore)(diff, config.review.ignore);
|
|
39
|
+
if (!effectiveDiff.trim())
|
|
40
|
+
return null;
|
|
41
|
+
if (!config.ai.apiKey || !config.ai.endpoint)
|
|
42
|
+
return null;
|
|
43
|
+
const adapter = (0, model_1.createModelAdapter)(config.ai.endpoint, config.ai.apiKey, config.ai.apiFormat);
|
|
44
|
+
const prompt = new prompt_1.SummaryPromptBuilder().setDiff(effectiveDiff);
|
|
45
|
+
const raw = await adapter.call(config.ai.model, prompt.buildSystem(), prompt.buildUser());
|
|
46
|
+
return raw ? cleanMarkdownResponse(raw) : null;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=summarizer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"summarizer.js","sourceRoot":"","sources":["../../src/core/summarizer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AIComment } from "../types";
|
|
2
|
+
export declare function postGitlabDiscussions(options: {
|
|
3
|
+
comments: AIComment[];
|
|
4
|
+
diff?: string;
|
|
5
|
+
token?: string;
|
|
6
|
+
env?: NodeJS.ProcessEnv;
|
|
7
|
+
}): Promise<boolean>;
|
|
8
|
+
export declare function postGitlabSummary(options: {
|
|
9
|
+
markdown: string;
|
|
10
|
+
token?: string;
|
|
11
|
+
env?: NodeJS.ProcessEnv;
|
|
12
|
+
}): Promise<boolean>;
|
|
13
|
+
//# sourceMappingURL=api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../src/gitlab/api.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AA2O1C,wBAAsB,qBAAqB,CAAC,OAAO,EAAE;IACnD,QAAQ,EAAE,SAAS,EAAE,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACzB,GAAG,OAAO,CAAC,OAAO,CAAC,CAkHnB;AAED,wBAAsB,iBAAiB,CAAC,OAAO,EAAE;IAC/C,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACzB,GAAG,OAAO,CAAC,OAAO,CAAC,CAgDnB"}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.postGitlabDiscussions = postGitlabDiscussions;
|
|
4
|
+
exports.postGitlabSummary = postGitlabSummary;
|
|
5
|
+
const constants_1 = require("../constants");
|
|
6
|
+
const diff_1 = require("../core/diff");
|
|
7
|
+
const http_1 = require("../shared/http");
|
|
8
|
+
const identity_1 = require("../shared/identity");
|
|
9
|
+
/**
|
|
10
|
+
* Facade: encapsulates all low-level GitLab REST API interactions.
|
|
11
|
+
* Consumers use high-level methods without knowing pagination, URL construction, or auth headers.
|
|
12
|
+
*/
|
|
13
|
+
class GitlabApiFacade {
|
|
14
|
+
constructor(token, env) {
|
|
15
|
+
this.token = token;
|
|
16
|
+
this.env = env;
|
|
17
|
+
}
|
|
18
|
+
buildApiUrl(suffix) {
|
|
19
|
+
if (!this.env.CI_API_V4_URL || !this.env.CI_PROJECT_ID || !this.env.CI_MERGE_REQUEST_IID) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const apiBase = this.env.CI_API_V4_URL.replace(/\/+$/, "");
|
|
23
|
+
const projectId = encodeURIComponent(this.env.CI_PROJECT_ID);
|
|
24
|
+
const mrIid = encodeURIComponent(this.env.CI_MERGE_REQUEST_IID);
|
|
25
|
+
return `${apiBase}/projects/${projectId}/merge_requests/${mrIid}${suffix}`;
|
|
26
|
+
}
|
|
27
|
+
fetch(url, init = {}) {
|
|
28
|
+
return (0, http_1.fetchWithTimeout)(url, init, constants_1.API_TIMEOUT_MS);
|
|
29
|
+
}
|
|
30
|
+
authHeaders() {
|
|
31
|
+
return { Authorization: `Bearer ${this.token}` };
|
|
32
|
+
}
|
|
33
|
+
get discussionUrl() {
|
|
34
|
+
return this.buildApiUrl("/discussions");
|
|
35
|
+
}
|
|
36
|
+
get noteUrl() {
|
|
37
|
+
return this.buildApiUrl("/notes");
|
|
38
|
+
}
|
|
39
|
+
async fetchCurrentUser() {
|
|
40
|
+
const apiBase = this.env.CI_API_V4_URL?.replace(/\/+$/, "");
|
|
41
|
+
if (!apiBase)
|
|
42
|
+
return null;
|
|
43
|
+
try {
|
|
44
|
+
const resp = await this.fetch(`${apiBase}/user`, {
|
|
45
|
+
headers: this.authHeaders(),
|
|
46
|
+
});
|
|
47
|
+
if (!resp.ok) {
|
|
48
|
+
console.error(`[gitlab] fetch current user failed status=${resp.status}`);
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
return await (0, http_1.parseJsonSafe)(resp);
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
console.error(`[gitlab] fetch current user error: ${err instanceof Error ? err.message : err}`);
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async fetchMergeRequest() {
|
|
59
|
+
const url = this.buildApiUrl("");
|
|
60
|
+
if (!url)
|
|
61
|
+
return null;
|
|
62
|
+
try {
|
|
63
|
+
const resp = await this.fetch(url, {
|
|
64
|
+
headers: this.authHeaders(),
|
|
65
|
+
});
|
|
66
|
+
if (!resp.ok) {
|
|
67
|
+
console.error(`[gitlab] merge request fetch failed status=${resp.status}`);
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
return await (0, http_1.parseJsonSafe)(resp);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
console.error(`[gitlab] merge request fetch error: ${err instanceof Error ? err.message : err}`);
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async fetchAllPages(url) {
|
|
78
|
+
const results = [];
|
|
79
|
+
let page = 1;
|
|
80
|
+
while (page <= constants_1.MAX_PAGINATION_PAGES) {
|
|
81
|
+
const sep = url.includes("?") ? "&" : "?";
|
|
82
|
+
let resp;
|
|
83
|
+
try {
|
|
84
|
+
resp = await this.fetch(`${url}${sep}per_page=100&page=${page}`, { headers: this.authHeaders() });
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
console.error(`[gitlab] pagination request failed page=${page}`);
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
if (!resp.ok)
|
|
91
|
+
break;
|
|
92
|
+
const items = await (0, http_1.parseJsonSafe)(resp);
|
|
93
|
+
if (!items?.length)
|
|
94
|
+
break;
|
|
95
|
+
results.push(...items);
|
|
96
|
+
if (items.length < 100)
|
|
97
|
+
break;
|
|
98
|
+
page += 1;
|
|
99
|
+
}
|
|
100
|
+
return results;
|
|
101
|
+
}
|
|
102
|
+
async postDiscussion(body) {
|
|
103
|
+
const url = this.discussionUrl;
|
|
104
|
+
if (!url)
|
|
105
|
+
return null;
|
|
106
|
+
return this.fetch(url, {
|
|
107
|
+
method: "POST",
|
|
108
|
+
headers: { "Content-Type": "application/json", ...this.authHeaders() },
|
|
109
|
+
body: JSON.stringify(body),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
async postNote(body) {
|
|
113
|
+
const url = this.noteUrl;
|
|
114
|
+
if (!url)
|
|
115
|
+
return null;
|
|
116
|
+
return this.fetch(url, {
|
|
117
|
+
method: "POST",
|
|
118
|
+
headers: { "Content-Type": "application/json", ...this.authHeaders() },
|
|
119
|
+
body: JSON.stringify(body),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
async updateNote(discussionId, noteId, body) {
|
|
123
|
+
const baseUrl = this.discussionUrl;
|
|
124
|
+
if (!baseUrl)
|
|
125
|
+
return false;
|
|
126
|
+
const resp = await this.fetch(`${baseUrl}/${discussionId}/notes/${noteId}`, {
|
|
127
|
+
method: "PUT",
|
|
128
|
+
headers: { "Content-Type": "application/json", ...this.authHeaders() },
|
|
129
|
+
body: JSON.stringify({ body }),
|
|
130
|
+
});
|
|
131
|
+
return resp.ok;
|
|
132
|
+
}
|
|
133
|
+
async deleteNote(discussionId, noteId) {
|
|
134
|
+
const baseUrl = this.discussionUrl;
|
|
135
|
+
if (!baseUrl)
|
|
136
|
+
return false;
|
|
137
|
+
const resp = await this.fetch(`${baseUrl}/${discussionId}/notes/${noteId}`, {
|
|
138
|
+
method: "DELETE",
|
|
139
|
+
headers: this.authHeaders(),
|
|
140
|
+
});
|
|
141
|
+
return resp.ok || resp.status === 204;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function buildCommentBody(comment) {
|
|
145
|
+
const message = comment.message || constants_1.DEFAULT_COMMENT_MESSAGE;
|
|
146
|
+
const body = comment.suggestion
|
|
147
|
+
? `${message}\n\n建议:${comment.suggestion}`
|
|
148
|
+
: message;
|
|
149
|
+
return `${body}\n\n${(0, identity_1.buildHiddenMarker)(identity_1.COMMENT_MARKER_PREFIX, (0, identity_1.buildCommentFingerprint)(comment))}`;
|
|
150
|
+
}
|
|
151
|
+
function collectBotNotesByMarker(discussions, userId, markerKind) {
|
|
152
|
+
const notes = new Map();
|
|
153
|
+
for (const discussion of discussions) {
|
|
154
|
+
for (const note of discussion.notes) {
|
|
155
|
+
if (note.author.id !== userId || note.system || !note.body)
|
|
156
|
+
continue;
|
|
157
|
+
const fingerprint = (0, identity_1.extractHiddenMarker)(note.body, markerKind);
|
|
158
|
+
if (!fingerprint)
|
|
159
|
+
continue;
|
|
160
|
+
const current = notes.get(fingerprint) ?? [];
|
|
161
|
+
current.push({
|
|
162
|
+
discussionId: discussion.id,
|
|
163
|
+
noteId: note.id,
|
|
164
|
+
body: note.body,
|
|
165
|
+
});
|
|
166
|
+
notes.set(fingerprint, current);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return notes;
|
|
170
|
+
}
|
|
171
|
+
function buildValidLineSet(diff) {
|
|
172
|
+
const set = new Set();
|
|
173
|
+
for (const [file, lines] of (0, diff_1.collectAddedLines)(diff)) {
|
|
174
|
+
for (const { line } of lines) {
|
|
175
|
+
set.add(`${file}:${line}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return set;
|
|
179
|
+
}
|
|
180
|
+
function isLineValid(validLines, file, line) {
|
|
181
|
+
if (!validLines)
|
|
182
|
+
return true;
|
|
183
|
+
return validLines.has(`${(0, diff_1.normalizePath)(file)}:${line}`);
|
|
184
|
+
}
|
|
185
|
+
async function postGitlabDiscussions(options) {
|
|
186
|
+
const env = options.env ?? process.env;
|
|
187
|
+
if (!options.token) {
|
|
188
|
+
console.error("[gitlab] MR discussions skipped: missing GitLab token");
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
const facade = new GitlabApiFacade(options.token, env);
|
|
192
|
+
const discussionUrl = facade.discussionUrl;
|
|
193
|
+
if (!discussionUrl) {
|
|
194
|
+
console.error("[gitlab] MR discussions skipped: missing GitLab CI context");
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
const validLines = options.diff ? buildValidLineSet(options.diff) : null;
|
|
198
|
+
const user = await facade.fetchCurrentUser();
|
|
199
|
+
const discussions = user
|
|
200
|
+
? await facade.fetchAllPages(discussionUrl)
|
|
201
|
+
: [];
|
|
202
|
+
const existing = user
|
|
203
|
+
? collectBotNotesByMarker(discussions, user.id, identity_1.COMMENT_MARKER_PREFIX)
|
|
204
|
+
: new Map();
|
|
205
|
+
const activeFingerprints = new Set();
|
|
206
|
+
const mr = options.comments.length
|
|
207
|
+
? await facade.fetchMergeRequest()
|
|
208
|
+
: null;
|
|
209
|
+
const refs = mr?.diff_refs;
|
|
210
|
+
if (options.comments.length && (!refs?.base_sha || !refs.start_sha || !refs.head_sha)) {
|
|
211
|
+
console.error("[gitlab] MR discussions skipped: missing diff refs");
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
let posted = 0;
|
|
215
|
+
let updated = 0;
|
|
216
|
+
let skippedInvalid = 0;
|
|
217
|
+
for (const comment of options.comments) {
|
|
218
|
+
if (!comment.file || !comment.line)
|
|
219
|
+
continue;
|
|
220
|
+
if (!isLineValid(validLines, comment.file, comment.line)) {
|
|
221
|
+
skippedInvalid += 1;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
const fingerprint = (0, identity_1.buildCommentFingerprint)(comment);
|
|
225
|
+
const body = buildCommentBody(comment);
|
|
226
|
+
activeFingerprints.add(fingerprint);
|
|
227
|
+
const old = existing.get(fingerprint)?.[0];
|
|
228
|
+
try {
|
|
229
|
+
if (old) {
|
|
230
|
+
const ok = await facade.updateNote(old.discussionId, old.noteId, body);
|
|
231
|
+
if (ok) {
|
|
232
|
+
updated += 1;
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
console.error(`[gitlab] MR discussion update failed file=${comment.file} line=${comment.line}`);
|
|
236
|
+
}
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const resp = await facade.postDiscussion({
|
|
240
|
+
body,
|
|
241
|
+
position: {
|
|
242
|
+
base_sha: refs.base_sha,
|
|
243
|
+
start_sha: refs.start_sha,
|
|
244
|
+
head_sha: refs.head_sha,
|
|
245
|
+
position_type: "text",
|
|
246
|
+
new_path: comment.file,
|
|
247
|
+
new_line: Number(comment.line),
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
if (!resp || !resp.ok) {
|
|
251
|
+
console.error(`[gitlab] MR discussion failed file=${comment.file} line=${comment.line} status=${resp?.status}`);
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
posted += 1;
|
|
255
|
+
}
|
|
256
|
+
catch (err) {
|
|
257
|
+
console.error(`[gitlab] MR discussion error file=${comment.file} line=${comment.line}: ${err instanceof Error ? err.message : err}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
let deleted = 0;
|
|
261
|
+
for (const [fingerprint, oldNotes] of existing) {
|
|
262
|
+
const staleNotes = activeFingerprints.has(fingerprint)
|
|
263
|
+
? oldNotes.slice(1)
|
|
264
|
+
: oldNotes;
|
|
265
|
+
for (const old of staleNotes) {
|
|
266
|
+
try {
|
|
267
|
+
if (await facade.deleteNote(old.discussionId, old.noteId)) {
|
|
268
|
+
deleted += 1;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
catch (err) {
|
|
272
|
+
console.error(`[gitlab] stale discussion delete error discussion=${old.discussionId} note=${old.noteId}: ${err instanceof Error ? err.message : err}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
console.error(`[gitlab] MR discussions lifecycle posted=${posted} updated=${updated} deleted=${deleted} skippedInvalid=${skippedInvalid}`);
|
|
277
|
+
return posted + updated + deleted > 0;
|
|
278
|
+
}
|
|
279
|
+
async function postGitlabSummary(options) {
|
|
280
|
+
const env = options.env ?? process.env;
|
|
281
|
+
const markdown = options.markdown.trim();
|
|
282
|
+
if (!markdown) {
|
|
283
|
+
console.error("[gitlab] summary skipped: empty markdown");
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
if (!options.token) {
|
|
287
|
+
console.error("[gitlab] summary skipped: missing GitLab token");
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
const facade = new GitlabApiFacade(options.token, env);
|
|
291
|
+
const discussionUrl = facade.discussionUrl;
|
|
292
|
+
const noteUrl = facade.noteUrl;
|
|
293
|
+
if (!discussionUrl || !noteUrl) {
|
|
294
|
+
console.error("[gitlab] summary skipped: missing GitLab CI context");
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
const user = await facade.fetchCurrentUser();
|
|
298
|
+
const summaryFingerprint = (0, identity_1.stableHash)(identity_1.SUMMARY_MARKER);
|
|
299
|
+
const body = `${markdown}\n\n${(0, identity_1.buildHiddenMarker)(identity_1.SUMMARY_MARKER, summaryFingerprint)}`;
|
|
300
|
+
if (user) {
|
|
301
|
+
const discussions = await facade.fetchAllPages(discussionUrl);
|
|
302
|
+
const existing = collectBotNotesByMarker(discussions, user.id, identity_1.SUMMARY_MARKER).get(summaryFingerprint)?.[0];
|
|
303
|
+
if (existing) {
|
|
304
|
+
const ok = await facade.updateNote(existing.discussionId, existing.noteId, body);
|
|
305
|
+
console.error(ok ? "[gitlab] summary note updated" : "[gitlab] summary note update failed");
|
|
306
|
+
return ok;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
const resp = await facade.postNote({ body });
|
|
310
|
+
if (!resp || !resp.ok) {
|
|
311
|
+
console.error(`[gitlab] summary note post failed status=${resp?.status}`);
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
console.error("[gitlab] summary note posted");
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
//# sourceMappingURL=api.js.map
|