@raolin2025/claude-code-node 2.1.0 → 2.2.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.
@@ -0,0 +1,360 @@
1
+ /**
2
+ * GitHub REST API 封装(零外部依赖)
3
+ * 使用原生 fetch (Node.js ≥ 18)
4
+ */
5
+
6
+ export class GitHubAPI {
7
+ /**
8
+ * @param {Object} config
9
+ * @param {string} config.token - GitHub Personal Access Token
10
+ * @param {string} config.owner - Repository owner
11
+ * @param {string} config.repo - Repository name
12
+ * @param {string} [config.baseUrl] - API base URL (for enterprise)
13
+ */
14
+ constructor(config) {
15
+ this.token = config.token || process.env.GITHUB_TOKEN
16
+ this.owner = config.owner
17
+ this.repo = config.repo
18
+ this.baseUrl = config.baseUrl || 'https://api.github.com'
19
+ this.apiVersion = '2022-11-28' // GitHub API version
20
+
21
+ if (!this.token) {
22
+ throw new Error('GitHub token required (set GITHUB_TOKEN or pass token param)')
23
+ }
24
+ if (!this.owner || !this.repo) {
25
+ throw new Error('GitHub repository owner and repo required')
26
+ }
27
+ }
28
+
29
+ /**
30
+ * 发送请求到 GitHub API
31
+ */
32
+ async request(endpoint, options = {}) {
33
+ const url = `${this.baseUrl}${endpoint}`
34
+ const headers = {
35
+ 'Authorization': `Bearer ${this.token}`,
36
+ 'Accept': 'application/vnd.github+json',
37
+ 'X-GitHub-Api-Version': this.apiVersion,
38
+ ...options.headers
39
+ }
40
+
41
+ // 速率限制检查
42
+ const shouldRetry = this._shouldRetryAfter(options)
43
+ if (shouldRetry.retry) {
44
+ await this._delay(shouldRetry.after)
45
+ }
46
+
47
+ const response = await fetch(url, {
48
+ ...options,
49
+ headers
50
+ })
51
+
52
+ // 更新速率限制信息
53
+ this._updateRateLimitInfo(response)
54
+
55
+ // 处理错误
56
+ if (!response.ok) {
57
+ const error = await this._parseError(response)
58
+ if (response.status === 403 && this._isRateLimited(response)) {
59
+ throw new GitHubRateLimitError(error.message, response)
60
+ }
61
+ throw new GitHubError(error.message, response.status, error.documentation_url)
62
+ }
63
+
64
+ // 204 No Content
65
+ if (response.status === 204) {
66
+ return null
67
+ }
68
+
69
+ return await response.json()
70
+ }
71
+
72
+ /**
73
+ * GET 请求
74
+ */
75
+ async get(endpoint, params = {}) {
76
+ const fullUrl = `${this.baseUrl}${endpoint}`
77
+ const queryParts = []
78
+ Object.keys(params).forEach(key => {
79
+ if (params[key] !== undefined && params[key] !== null) {
80
+ queryParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(params[key]))}`)
81
+ }
82
+ })
83
+ const queryString = queryParts.length > 0 ? `?${queryParts.join('&')}` : ''
84
+
85
+ return this.request(`${endpoint}${queryString}`, { method: 'GET' })
86
+ }
87
+
88
+ /**
89
+ * POST 请求
90
+ */
91
+ async post(endpoint, body) {
92
+ return this.request(endpoint, {
93
+ method: 'POST',
94
+ body: JSON.stringify(body),
95
+ headers: { 'Content-Type': 'application/json' }
96
+ })
97
+ }
98
+
99
+ /**
100
+ * PUT 请求
101
+ */
102
+ async put(endpoint, body) {
103
+ return this.request(endpoint, {
104
+ method: 'PUT',
105
+ body: JSON.stringify(body),
106
+ headers: { 'Content-Type': 'application/json' }
107
+ })
108
+ }
109
+
110
+ /**
111
+ * PATCH 请求
112
+ */
113
+ async patch(endpoint, body) {
114
+ return this.request(endpoint, {
115
+ method: 'PATCH',
116
+ body: JSON.stringify(body),
117
+ headers: { 'Content-Type': 'application/json' }
118
+ })
119
+ }
120
+
121
+ /**
122
+ * DELETE 请求
123
+ */
124
+ async delete(endpoint) {
125
+ return this.request(endpoint, { method: 'DELETE' })
126
+ }
127
+
128
+ // ============ Pull Requests API ============
129
+
130
+ /**
131
+ * 列出 PRs
132
+ * GET /repos/{owner}/{repo}/pulls
133
+ */
134
+ async listPRs(params = {}) {
135
+ // 默认只获取 open 状态的 PR
136
+ const defaultParams = { state: 'open', per_page: 100 }
137
+ const query = { ...defaultParams, ...params }
138
+ return this.get(`/repos/${this.owner}/${this.repo}/pulls`, query)
139
+ }
140
+
141
+ /**
142
+ * 获取单个 PR
143
+ * GET /repos/{owner}/{repo}/pulls/{pr_number}
144
+ */
145
+ async getPR(prNumber) {
146
+ return this.get(`/repos/${this.owner}/${this.repo}/pulls/${prNumber}`)
147
+ }
148
+
149
+ /**
150
+ * 获取 PR 的 diff
151
+ * GET /repos/{owner}/{repo}/pulls/{pr_number}/files
152
+ */
153
+ async getPRFiles(prNumber) {
154
+ return this.get(`/repos/${this.owner}/${this.repo}/pulls/${prNumber}/files`)
155
+ }
156
+
157
+ /**
158
+ * 获取 PR 提交的 diff (未压缩)
159
+ */
160
+ async getPRDiff(prNumber) {
161
+ const endpoint = `/repos/${this.owner}/${this.repo}/pulls/${prNumber}`
162
+ // 使用统一的 request 方法,但走 diff 类型的 Accept
163
+ // 单独 fetch diff,因为需要 text() 而非 json()
164
+ const diffUrl = `${this.baseUrl}/repos/${this.owner}/${this.repo}/pulls/${prNumber}.diff`
165
+ const diffResponse = await fetch(diffUrl, {
166
+ method: 'GET',
167
+ headers: {
168
+ 'Authorization': `Bearer ${this.token}`,
169
+ 'Accept': 'application/vnd.github.v3.diff'
170
+ }
171
+ })
172
+
173
+ if (!diffResponse.ok) {
174
+ const errMsg = `Failed to fetch PR diff: ${diffResponse.status} ${diffResponse.statusText}`
175
+ throw new GitHubError(errMsg, diffResponse.status)
176
+ }
177
+
178
+ return await diffResponse.text()
179
+ }
180
+
181
+ /**
182
+ * 创建 PR 评论
183
+ * POST /repos/{owner}/{repo}/pulls/{pr_number}/reviews
184
+ */
185
+ async createReview(prNumber, body, event = 'COMMENT', comments = []) {
186
+ return this.post(`/repos/${this.owner}/${this.repo}/pulls/${prNumber}/reviews`, {
187
+ body,
188
+ event, // 'COMMENT' | 'APPROVE' | 'REQUEST_CHANGES'
189
+ comments // [{ path, position, body }]
190
+ })
191
+ }
192
+
193
+ /**
194
+ * 在 PR diff 的特定行添加评论
195
+ * POST /repos/{owner}/{repo}/pulls/{pr_number}/comments
196
+ */
197
+ async createComment(prNumber, body, { path, position, commitId }) {
198
+ return this.post(`/repos/${this.owner}/${this.repo}/pulls/${prNumber}/comments`, {
199
+ body,
200
+ path,
201
+ position,
202
+ commit_id: commitId
203
+ })
204
+ }
205
+
206
+ /**
207
+ * 更新 PR 评论(回复)
208
+ * PATCH /repos/{owner}/{repo}/pulls/comments/{comment_id}
209
+ */
210
+ async updateComment(commentId, body) {
211
+ return this.patch(`/repos/${this.owner}/${this.repo}/pulls/comments/${commentId}`, { body })
212
+ }
213
+
214
+ /**
215
+ * 删除 PR 评论
216
+ * DELETE /repos/{owner}/{repo}/pulls/comments/{comment_id}
217
+ */
218
+ async deleteComment(commentId) {
219
+ return this.delete(`/repos/${this.owner}/${this.repo}/pulls/comments/${commentId}`)
220
+ }
221
+
222
+ /**
223
+ * 获取 PR 的评论线程
224
+ * GET /repos/{owner}/{repo}/pulls/{pr_number}/comments
225
+ */
226
+ async listPRComments(prNumber) {
227
+ return this.get(`/repos/${this.owner}/${this.repo}/pulls/${prNumber}/comments`)
228
+ }
229
+
230
+ /**
231
+ * Approve PR
232
+ */
233
+ async approvePR(prNumber, body) {
234
+ return this.createReview(prNumber, body || 'Approved', 'APPROVE')
235
+ }
236
+
237
+ /**
238
+ * Request changes on PR
239
+ */
240
+ async requestChanges(prNumber, body) {
241
+ return this.createReview(prNumber, body || 'Changes requested', 'REQUEST_CHANGES')
242
+ }
243
+
244
+ /**
245
+ * 合并 PR
246
+ * POST /repos/{owner}/{repo}/pulls/{pr_number}/merge
247
+ */
248
+ async mergePR(prNumber, options = {}) {
249
+ const { commitTitle, commitMessage, mergeMethod = 'merge' } = options
250
+ return this.post(`/repos/${this.owner}/${this.repo}/pulls/${prNumber}/merge`, {
251
+ commit_title: commitTitle,
252
+ commit_message: commitMessage,
253
+ merge_method: mergeMethod // 'merge' | 'squash' | 'rebase'
254
+ })
255
+ }
256
+
257
+ /**
258
+ * 检查 PR 是否可合并
259
+ * GET /repos/{owner}/{repo}/pulls/{pr_number}/mergeable
260
+ */
261
+ async isMergeable(prNumber) {
262
+ const pr = await this.getPR(prNumber)
263
+ // GitHub 返回 null 表示还在计算中,不视为不可合并
264
+ return pr.mergeable !== false
265
+ }
266
+
267
+ /**
268
+ * 获取 PR 的状态检查(CI checks)
269
+ * GET /repos/{owner}/{repo}/commits/{commit_sha}/status
270
+ */
271
+ /**
272
+ * 获取 PR 的合并状态(combine status)
273
+ * GET /repos/{owner}/{repo}/commits/{commit_sha}/status
274
+ */
275
+ async getCombinedStatus(commitSha) {
276
+ if (!commitSha) {
277
+ throw new Error('commitSha is required for getCombinedStatus')
278
+ }
279
+ return this.get(`/repos/${this.owner}/${this.repo}/commits/${commitSha}/status`)
280
+ }
281
+
282
+ /**
283
+ * 列出 PR 的审查评论(reviews)
284
+ * GET /repos/{owner}/{repo}/pulls/{pr_number}/reviews
285
+ */
286
+ async listReviews(prNumber) {
287
+ return this.get(`/repos/${this.owner}/${this.repo}/pulls/${prNumber}/reviews`)
288
+ }
289
+
290
+ // ============ 辅助方法 ============
291
+
292
+ _updateRateLimitInfo(response) {
293
+ this.rateLimitRemaining = parseInt(response.headers.get('X-RateLimit-Remaining'), 10)
294
+ this.rateLimitReset = parseInt(response.headers.get('X-RateLimit-Reset'), 10) // Unix timestamp
295
+ this.rateLimitLimit = parseInt(response.headers.get('X-RateLimit-Limit'), 10)
296
+ }
297
+
298
+ _isRateLimited(response) {
299
+ return response.status === 403 && response.headers.get('X-RateLimit-Remaining') === '0'
300
+ }
301
+
302
+ _shouldRetryAfter(options) {
303
+ if (options._retryCount && options._retryCount > 3) {
304
+ return { retry: false }
305
+ }
306
+ if (this.rateLimitRemaining === 0 && this.rateLimitReset) {
307
+ const now = Math.floor(Date.now() / 1000)
308
+ const waitSeconds = Math.max(this.rateLimitReset - now + 1, 1)
309
+ return { retry: true, after: waitSeconds * 1000 }
310
+ }
311
+ return { retry: false }
312
+ }
313
+
314
+ _delay(ms) {
315
+ return new Promise(resolve => setTimeout(resolve, ms))
316
+ }
317
+
318
+ async _parseError(response) {
319
+ let message = `${response.status} ${response.statusText}`
320
+ let documentationUrl = null
321
+ try {
322
+ const data = await response.json()
323
+ message = data.message || message
324
+ documentationUrl = data.documentation_url || null
325
+ } catch {
326
+ // ignore parse error, keep fallback message
327
+ }
328
+ return { message, documentation_url: documentationUrl }
329
+ }
330
+ }
331
+
332
+ /**
333
+ * GitHub API 错误
334
+ */
335
+ export class GitHubError extends Error {
336
+ constructor(message, status, documentationUrl = null) {
337
+ super(message)
338
+ this.name = 'GitHubError'
339
+ this.status = status
340
+ this.documentationUrl = documentationUrl
341
+ }
342
+ }
343
+
344
+ /**
345
+ * 速率限制错误
346
+ */
347
+ export class GitHubRateLimitError extends GitHubError {
348
+ constructor(message, response) {
349
+ super(message, response.status)
350
+ this.name = 'GitHubRateLimitError'
351
+ this.resetAt = parseInt(response.headers.get('X-RateLimit-Reset'), 10)
352
+ }
353
+ }
354
+
355
+ /**
356
+ * 创建 GitHub API 实例的工厂函数
357
+ */
358
+ export function createGitHubAPI(options) {
359
+ return new GitHubAPI(options)
360
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Git 模块统一导出
3
+ */
4
+
5
+ export { GitHubAPI, createGitHubAPI, GitHubError, GitHubRateLimitError } from './github-api.js'
6
+ export { PRReviewer } from './pr-reviewer.js'
7
+ export { PRMergePolicy, DEFAULT_MERGE_POLICY, findEligiblePRs, isPRMergeable } from './pr-merge-policy.js'
8
+
9
+ // Diff 解析工具
10
+ export {
11
+ parseDiff,
12
+ splitDiffByFile,
13
+ getPositionInDiff,
14
+ buildCommentPayload,
15
+ getHunksForFileRaw,
16
+ fileExistsInDiff,
17
+ countFileChanges,
18
+ mapLineToPosition,
19
+ findAddedLines,
20
+ debugPrintHunks
21
+ } from './utils/diff-parser.js'
22
+
23
+ // LLM 助手
24
+ export { callLLM, buildPRReviewPrompt } from './llm-assistant.js'
25
+
26
+ /**
27
+ * 快速创建 GitTool
28
+ */
29
+ import { gitTool } from '../tools/git-tool.js'
30
+
31
+ export function createGitTool(config = {}) {
32
+ // 这里返回的是一个配置对象,实际 ToolDef 已注册
33
+ return {
34
+ config,
35
+ tool: gitTool
36
+ }
37
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * LLM 助手 - 调用 LLM API 进行智能分析
3
+ * 复用 cc-node 的 LLM 调用模式
4
+ */
5
+
6
+ /**
7
+ * 调用 LLM 分析 PR
8
+ * @param {string} systemPrompt
9
+ * @param {string} userPrompt
10
+ * @param {Object} apiConfig - { apiKey, apiBase, model }
11
+ */
12
+ export async function callLLM(systemPrompt, userPrompt, apiConfig = {}) {
13
+ const {
14
+ apiKey = process.env.LLM_API_KEY || process.env.DEEPSEEK_API_KEY,
15
+ apiBase = process.env.LLM_API_BASE || 'https://api.deepseek.com/v1',
16
+ model = process.env.LLM_MODEL || 'deepseek-chat'
17
+ } = apiConfig
18
+
19
+ if (!apiKey) {
20
+ throw new Error('LLM API key required (set LLM_API_KEY or DEEPSEEK_API_KEY)')
21
+ }
22
+
23
+ const response = await fetch(`${apiBase}/chat/completions`, {
24
+ method: 'POST',
25
+ headers: {
26
+ 'Content-Type': 'application/json',
27
+ 'Authorization': `Bearer ${apiKey}`
28
+ },
29
+ body: JSON.stringify({
30
+ model,
31
+ messages: [
32
+ { role: 'system', content: systemPrompt },
33
+ { role: 'user', content: userPrompt }
34
+ ],
35
+ temperature: 0.3,
36
+ max_tokens: 2000
37
+ })
38
+ })
39
+
40
+ if (!response.ok) {
41
+ const error = await response.json().catch(() => ({}))
42
+ throw new Error(`LLM API error: ${response.status} ${error.message || response.statusText}`)
43
+ }
44
+
45
+ const data = await response.json()
46
+ return data.choices[0].message.content
47
+ }
48
+
49
+ /**
50
+ * PR 智能审查 prompt
51
+ * @param {Object} pr - PR 信息
52
+ * @param {string} diff - diff 内容
53
+ * @param {Array} findings - 已发现的规则检查问题
54
+ */
55
+ export function buildPRReviewPrompt(pr, diff, findings) {
56
+ const findingsText = findings.map(f => `- ${f.file}:${f.line} [${f.severity}] ${f.message}`).join('\n')
57
+
58
+ return `
59
+ You are an experienced code reviewer. Analyze the following PR and provide a concise review summary.
60
+
61
+ ## PR Information
62
+ - Title: ${pr.title}
63
+ - Author: ${pr.user?.login}
64
+ - Files changed: ${pr.changed_files}
65
+ - Additions: ${pr.additions}, Deletions: ${pr.deletions}
66
+
67
+ ## Automated Findings
68
+ ${findingsText || 'No issues found by automated checks.'}
69
+
70
+ ## Diff
71
+ ${diff.substring(0, 5000)}... (truncated)
72
+
73
+ ## Instructions
74
+ Provide a review summary with:
75
+ 1. Overall assessment (POSITIVE / NEEDS_WORK / REJECT)
76
+ 2. Key strengths (if any)
77
+ 3. Critical issues that must be fixed
78
+ 4. Suggestions for improvement
79
+ 5. Merging recommendation (YES / NO)
80
+
81
+ Keep it concise (3-5 bullet points).
82
+ `
83
+ }