@raolin2025/claude-code-node 2.0.0 → 2.2.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.
@@ -43,6 +43,8 @@ export class QueryEngineConfig {
43
43
  this.costTracker = options.costTracker || null
44
44
  this.tokenBudget = options.tokenBudget || null
45
45
  this.initialMessages = options.initialMessages || []
46
+ this.onConfirmTool = options.onConfirmTool || null // ask 模式确认回调
47
+ this.readline = options.readline || null // 用于 AskUserQuestion 工具
46
48
  }
47
49
  }
48
50
 
@@ -196,39 +198,61 @@ export class QueryEngine {
196
198
 
197
199
 
198
200
  /**
199
- * 执行工具调用
201
+ * 执行工具调用 — 两阶段策略
202
+ * 阶段1(串行):安全检查 + ask 模式确认(需要用户交互,必须串行)
203
+ * 阶段2(并行):批准后的工具并行执行,互不依赖的工具同时跑
200
204
  */
201
205
  async _executeToolCalls(toolCalls) {
202
- const results = []
206
+ // 阶段1:串行安全检查
207
+ const approved = []
203
208
  for (const tc of toolCalls) {
204
- // 安全检查
205
209
  const permResult = await this.permissionChecker.check(tc.name, tc.input)
206
210
  if (!permResult.allowed) {
207
- results.push(new ToolResult(tc.id, `工具调用被安全策略拒绝: ${tc.name} — ${permResult.reason || ""}`, true))
208
- results[results.length - 1].toolName = tc.name
209
- continue
211
+ if (permResult.requiresConfirmation && this.config.onConfirmTool) {
212
+ const confirmed = await this.config.onConfirmTool(tc.name, tc.input)
213
+ if (!confirmed) {
214
+ approved.push({ tc, error: '用户未确认' })
215
+ continue
216
+ }
217
+ } else {
218
+ approved.push({ tc, error: `安全策略拒绝: ${permResult.reason || ""}` })
219
+ continue
220
+ }
210
221
  }
211
222
 
212
- // 查找工具
213
223
  const tool = this.config.tools.find(t => t.name === tc.name)
214
224
  if (!tool) {
215
- results.push(new ToolResult(tc.id, `未找到工具: ${tc.name}`, true))
216
- results[results.length - 1].toolName = tc.name
225
+ approved.push({ tc, error: `未找到工具: ${tc.name}` })
217
226
  continue
218
227
  }
219
228
 
229
+ approved.push({ tc, tool })
230
+ }
231
+
232
+ // 阶段2:并行执行已批准的工具
233
+ const execPromises = approved.map(async (item) => {
234
+ if (item.error) {
235
+ const r = new ToolResult(item.tc.id, item.error, true)
236
+ r.toolName = item.tc.name
237
+ return r
238
+ }
239
+ const { tc, tool } = item
240
+ tc.status = 'running'
220
241
  try {
221
- tc.status = 'running'
222
- const content = await tool.handler(tc.input, { cwd: this.config.cwd, engine: this })
242
+ const content = await tool.handler(tc.input, { cwd: this.config.cwd, engine: this, readline: this.config.readline })
223
243
  tc.status = 'done'
224
- results.push(new ToolResult(tc.id, typeof content === 'string' ? content : JSON.stringify(content), false))
225
- results[results.length - 1].toolName = tc.name
244
+ const r = new ToolResult(tc.id, typeof content === 'string' ? content : JSON.stringify(content), false)
245
+ r.toolName = tc.name
246
+ return r
226
247
  } catch (err) {
227
248
  tc.status = 'error'
228
- results.push(new ToolResult(tc.id, `工具执行错误: ${err.message}`, true))
229
- results[results.length - 1].toolName = tc.name
249
+ const r = new ToolResult(tc.id, `工具执行错误: ${err.message}`, true)
250
+ r.toolName = tc.name
251
+ return r
230
252
  }
231
- }
253
+ })
254
+
255
+ const results = await Promise.all(execPromises)
232
256
  return results
233
257
  }
234
258
 
@@ -289,6 +313,12 @@ export class QueryEngine {
289
313
 
290
314
  // 带重试的 fetch
291
315
  const maxRetries = 3
316
+ // Jitter 退避 — 指数退避 + 随机 ±50%,防止惊群效应
317
+ const retryDelay = (baseMs, attempt) => {
318
+ const ms = baseMs * Math.pow(2, attempt - 1)
319
+ const jitter = ms * (0.5 + Math.random() * 0.5) // 50%-100% of base
320
+ return Math.round(jitter)
321
+ }
292
322
  let lastError = null
293
323
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
294
324
  try {
@@ -310,7 +340,7 @@ export class QueryEngine {
310
340
  const errText = await response.text()
311
341
  // 429/503 可重试
312
342
  if ((response.status === 429 || response.status === 503) && attempt < maxRetries) {
313
- const waitMs = response.status === 429 ? 2000 * attempt : 1000
343
+ const waitMs = retryDelay(response.status === 429 ? 2000 : 1000, attempt)
314
344
  if (this.config.verbose) {
315
345
  console.error(`[retry] API ${response.status}, waiting ${waitMs}ms (attempt ${attempt}/${maxRetries})`)
316
346
  }
@@ -322,11 +352,17 @@ export class QueryEngine {
322
352
 
323
353
  // 流式或非流式处理
324
354
  if (useStream && response.body) {
325
- return await this._handleStreamResponse(response)
355
+ const result = await this._handleStreamResponse(response)
356
+ if (result.usage && this.costTracker) {
357
+ this.costTracker.recordUsage(result.usage)
358
+ }
359
+ if (this.tokenBudget && result.usage) {
360
+ this.tokenBudget.recordUsage(result.usage)
361
+ }
362
+ return result
326
363
  } else {
327
364
  const data = await response.json()
328
365
  const result = parseNonStreamResponse(data)
329
- // M4: 记录非流式响应费用
330
366
  if (result.usage && this.costTracker) {
331
367
  this.costTracker.recordUsage(result.usage)
332
368
  }
@@ -339,7 +375,7 @@ export class QueryEngine {
339
375
  lastError = err
340
376
  // 网络错误重试
341
377
  if (err.name !== 'AbortError' && attempt < maxRetries && !err.message.startsWith('API 错误')) {
342
- const waitMs = 1000 * attempt
378
+ const waitMs = retryDelay(1000, attempt)
343
379
  if (this.config.verbose) {
344
380
  console.error(`[retry] Network error: ${err.message}, waiting ${waitMs}ms (attempt ${attempt}/${maxRetries})`)
345
381
  }
@@ -394,42 +430,9 @@ export class QueryEngine {
394
430
  return result
395
431
  }
396
432
 
397
- /**
398
- * 解析 OpenAI 兼容响应
399
- */
400
- _parseResponse(data) {
401
- const result = { content: '', toolCalls: [] }
402
- const choice = data.choices?.[0]
403
- if (!choice) return result
404
-
405
- const message = choice.message
406
- if (message.content) {
407
- result.content = message.content
408
- }
409
-
410
- for (const tc of (message.tool_calls || [])) {
411
- let input = {}
412
- try {
413
- input = JSON.parse(tc.function.arguments || '{}')
414
- } catch {
415
- input = { _raw: tc.function.arguments }
416
- }
417
- result.toolCalls.push(new ToolCall(tc.id, tc.function.name, input))
418
- }
419
-
420
- // M4: 记录 API 调用费用
421
- if (result.usage && this.costTracker) {
422
- this.costTracker.recordUsage(result.usage)
423
- }
424
- if (this.tokenBudget && result.usage) {
425
- this.tokenBudget.recordUsage(result.usage)
426
- }
427
-
428
- return result
429
- }
430
-
431
433
  /** 格式化内容 */
432
434
  _formatContent(content) {
435
+ if (content == null) return ''
433
436
  if (typeof content === 'string') return content
434
437
  if (typeof content === 'object') return JSON.stringify(content)
435
438
  return String(content)
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { readFile, writeFile, mkdir, readdir, rm, chmod } from 'fs/promises'
6
6
  import { resolve, join } from 'path'
7
+ import { randomBytes } from 'crypto'
7
8
 
8
9
  const DEFAULT_SESSIONS_DIR = '.claude-code/sessions'
9
10
 
@@ -21,7 +22,8 @@ export class SessionManager {
21
22
  /** 创建新会话 */
22
23
  async create(title = '') {
23
24
  await this.ensureDir()
24
- const id = `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
25
+ // M4 fix: 使用 crypto.randomBytes 生成不可预测的会话 ID
26
+ const id = `session-${Date.now()}-${randomBytes(8).toString('hex')}`
25
27
  const session = {
26
28
  id,
27
29
  title: title || `Session ${new Date().toISOString().slice(0, 19)}`,
@@ -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
+ }