hong-review-cli 1.0.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.
- package/README.md +74 -0
- package/bin/hong-review.js +89 -0
- package/index.js +166 -0
- package/package.json +30 -0
- package/src/commands/actions.js +122 -0
- package/src/commands/config.js +49 -0
- package/src/commands/list.js +65 -0
- package/src/commands/login.js +69 -0
- package/src/commands/mr.js +270 -0
- package/src/core/agent.js +141 -0
- package/src/core/ai.js +108 -0
- package/src/core/cache.js +221 -0
- package/src/core/gitlab.js +164 -0
- package/src/utils/hooks.js +60 -0
- package/src/utils/logger.js +52 -0
- package/src/utils/storage.js +67 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
const storage = require('../utils/storage');
|
|
2
|
+
|
|
3
|
+
class RequestCache {
|
|
4
|
+
constructor(defaultTTL = 5 * 60 * 1000) {
|
|
5
|
+
this.cache = new Map();
|
|
6
|
+
this.defaultTTL = defaultTTL;
|
|
7
|
+
this.maxEntries = 100;
|
|
8
|
+
this.cleanupIntervalTime = 5 * 60 * 1000;
|
|
9
|
+
this.startAutoCleanup();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
generateKey(url, params) {
|
|
13
|
+
const paramsStr = params ? JSON.stringify(params) : '';
|
|
14
|
+
return `${url}:${paramsStr}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
get(key) {
|
|
18
|
+
const entry = this.cache.get(key);
|
|
19
|
+
if (!entry) return null;
|
|
20
|
+
|
|
21
|
+
if (Date.now() - entry.timestamp > entry.ttl) {
|
|
22
|
+
this.cache.delete(key);
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return entry.data;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
set(key, data, ttl) {
|
|
30
|
+
if (this.cache.size >= this.maxEntries && !this.cache.has(key)) {
|
|
31
|
+
this.evictOldest(Math.floor(this.maxEntries * 0.2));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
this.cache.set(key, {
|
|
35
|
+
data,
|
|
36
|
+
timestamp: Date.now(),
|
|
37
|
+
ttl: ttl ?? this.defaultTTL
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
delete(key) {
|
|
42
|
+
return this.cache.delete(key);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
clear() {
|
|
46
|
+
this.cache.clear();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
cleanup() {
|
|
50
|
+
const now = Date.now();
|
|
51
|
+
let cleanedCount = 0;
|
|
52
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
53
|
+
if (now - entry.timestamp > entry.ttl) {
|
|
54
|
+
this.cache.delete(key);
|
|
55
|
+
cleanedCount++;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return cleanedCount;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
evictOldest(count) {
|
|
62
|
+
const entries = Array.from(this.cache.entries());
|
|
63
|
+
entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
|
|
64
|
+
|
|
65
|
+
for (let i = 0; i < Math.min(count, entries.length); i++) {
|
|
66
|
+
this.cache.delete(entries[i][0]);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
getStats() {
|
|
71
|
+
const now = Date.now();
|
|
72
|
+
let expired = 0;
|
|
73
|
+
for (const entry of this.cache.values()) {
|
|
74
|
+
if (now - entry.timestamp > entry.ttl) {
|
|
75
|
+
expired++;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
size: this.cache.size,
|
|
80
|
+
expired
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
startAutoCleanup() {
|
|
85
|
+
this.cleanupInterval = setInterval(() => {
|
|
86
|
+
const cleaned = this.cleanup();
|
|
87
|
+
}, this.cleanupIntervalTime);
|
|
88
|
+
// 不阻断 Node 进程退出
|
|
89
|
+
if (this.cleanupInterval.unref) {
|
|
90
|
+
this.cleanupInterval.unref();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
stopAutoCleanup() {
|
|
95
|
+
if (this.cleanupInterval) {
|
|
96
|
+
clearInterval(this.cleanupInterval);
|
|
97
|
+
this.cleanupInterval = null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
setMaxEntries(max) {
|
|
102
|
+
this.maxEntries = Math.max(10, max);
|
|
103
|
+
while (this.cache.size > this.maxEntries) {
|
|
104
|
+
this.evictOldest(1);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async wrap(key, fetcher, ttl) {
|
|
109
|
+
const cached = this.get(key);
|
|
110
|
+
if (cached !== null) {
|
|
111
|
+
return cached;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const data = await fetcher();
|
|
115
|
+
this.set(key, data, ttl);
|
|
116
|
+
return data;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
destroy() {
|
|
120
|
+
this.stopAutoCleanup();
|
|
121
|
+
this.clear();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
class ReviewStorage {
|
|
126
|
+
constructor() {
|
|
127
|
+
this.STORAGE_KEY = 'reviewHistory';
|
|
128
|
+
this.MAX_HISTORY = 50;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
saveReview(data) {
|
|
132
|
+
try {
|
|
133
|
+
const history = this.getAllHistory();
|
|
134
|
+
|
|
135
|
+
const newRecord = {
|
|
136
|
+
...data,
|
|
137
|
+
id: `${data.projectId}-${data.mrIid}-${Date.now()}`,
|
|
138
|
+
reviewTime: Date.now()
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
history.unshift(newRecord);
|
|
142
|
+
|
|
143
|
+
if (history.length > this.MAX_HISTORY) {
|
|
144
|
+
history.length = this.MAX_HISTORY;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
storage.set(this.STORAGE_KEY, history);
|
|
148
|
+
return newRecord.id;
|
|
149
|
+
} catch (e) {
|
|
150
|
+
console.error('Failed to save review:', e);
|
|
151
|
+
return '';
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
getAllHistory() {
|
|
156
|
+
try {
|
|
157
|
+
return storage.get(this.STORAGE_KEY) || [];
|
|
158
|
+
} catch (e) {
|
|
159
|
+
return [];
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
getMRHistory(projectId, mrIid) {
|
|
164
|
+
const history = this.getAllHistory();
|
|
165
|
+
return history.filter(h => h.projectId === projectId && h.mrIid === mrIid);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
getReviewById(id) {
|
|
169
|
+
const history = this.getAllHistory();
|
|
170
|
+
return history.find(h => h.id === id) || null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
deleteReview(id) {
|
|
174
|
+
try {
|
|
175
|
+
const history = this.getAllHistory();
|
|
176
|
+
const filtered = history.filter(h => h.id !== id);
|
|
177
|
+
storage.set(this.STORAGE_KEY, filtered);
|
|
178
|
+
return true;
|
|
179
|
+
} catch (e) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
clearAll() {
|
|
185
|
+
try {
|
|
186
|
+
storage.set(this.STORAGE_KEY, []);
|
|
187
|
+
return true;
|
|
188
|
+
} catch (e) {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
clearMRHistory(projectId, mrIid) {
|
|
194
|
+
try {
|
|
195
|
+
const history = this.getAllHistory();
|
|
196
|
+
const filtered = history.filter(h => !(h.projectId === projectId && h.mrIid === mrIid));
|
|
197
|
+
storage.set(this.STORAGE_KEY, filtered);
|
|
198
|
+
return history.length - filtered.length;
|
|
199
|
+
} catch (e) {
|
|
200
|
+
return 0;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
cleanupExpired() {
|
|
205
|
+
try {
|
|
206
|
+
const history = this.getAllHistory();
|
|
207
|
+
const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
|
|
208
|
+
const filtered = history.filter(h => h.reviewTime > sevenDaysAgo);
|
|
209
|
+
storage.set(this.STORAGE_KEY, filtered);
|
|
210
|
+
return history.length - filtered.length;
|
|
211
|
+
} catch (e) {
|
|
212
|
+
return 0;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
module.exports = {
|
|
218
|
+
requestCache: new RequestCache(),
|
|
219
|
+
reviewStorage: new ReviewStorage(),
|
|
220
|
+
ReviewStorage
|
|
221
|
+
};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
|
|
3
|
+
class GitLabClient {
|
|
4
|
+
constructor(config) {
|
|
5
|
+
this.host = config.host || 'https://gitlab.com';
|
|
6
|
+
this.token = config.token;
|
|
7
|
+
this.client = axios.create({
|
|
8
|
+
baseURL: this.host,
|
|
9
|
+
headers: {
|
|
10
|
+
'PRIVATE-TOKEN': this.token
|
|
11
|
+
},
|
|
12
|
+
timeout: 30000
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async request(url, options = {}) {
|
|
17
|
+
try {
|
|
18
|
+
const response = await this.client.request({
|
|
19
|
+
url,
|
|
20
|
+
method: options.method || 'GET',
|
|
21
|
+
data: options.data,
|
|
22
|
+
params: options.params
|
|
23
|
+
});
|
|
24
|
+
return response.data;
|
|
25
|
+
} catch (error) {
|
|
26
|
+
let detailMessage = '';
|
|
27
|
+
const status = error.response ? error.response.status : 0;
|
|
28
|
+
const errorData = error.response ? error.response.data : null;
|
|
29
|
+
|
|
30
|
+
if (errorData) {
|
|
31
|
+
if (errorData.message) detailMessage = ` - ${errorData.message}`;
|
|
32
|
+
else if (errorData.error) detailMessage = ` - ${errorData.error}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
switch (status) {
|
|
36
|
+
case 401: throw new Error('GitLab Token 无效或已过期');
|
|
37
|
+
case 403: throw new Error(`没有权限执行此操作${detailMessage}`);
|
|
38
|
+
case 404: throw new Error('请求的资源不存在');
|
|
39
|
+
default: throw new Error(`GitLab API Error: ${error.message}${detailMessage}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async getMyReviewMergeRequests() {
|
|
45
|
+
// 合并待办和指派
|
|
46
|
+
const [todos, assignedMrs] = await Promise.all([
|
|
47
|
+
this.request('/api/v4/todos?state=pending'),
|
|
48
|
+
this.request('/api/v4/merge_requests?scope=assigned_to_me&state=opened')
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
const uniqueMrs = new Map();
|
|
52
|
+
|
|
53
|
+
const mrTodos = todos.filter(t => t.target_type === 'MergeRequest' && t.target && t.target.state === 'opened');
|
|
54
|
+
mrTodos.forEach(todo => {
|
|
55
|
+
const mr = todo.target;
|
|
56
|
+
if (!uniqueMrs.has(mr.id)) {
|
|
57
|
+
uniqueMrs.set(mr.id, { ...mr, source: 'todo' });
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
assignedMrs.forEach(mr => {
|
|
62
|
+
if (!uniqueMrs.has(mr.id)) {
|
|
63
|
+
uniqueMrs.set(mr.id, { ...mr, source: 'assigned' });
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return Array.from(uniqueMrs.values()).map(mr => ({
|
|
68
|
+
id: mr.id,
|
|
69
|
+
iid: mr.iid,
|
|
70
|
+
project_id: mr.project_id,
|
|
71
|
+
title: mr.title,
|
|
72
|
+
description: mr.description || '',
|
|
73
|
+
source_branch: mr.source_branch,
|
|
74
|
+
target_branch: mr.target_branch,
|
|
75
|
+
author: {
|
|
76
|
+
name: mr.author?.name || 'Unknown',
|
|
77
|
+
avatar_url: mr.author?.avatar_url || ''
|
|
78
|
+
},
|
|
79
|
+
web_url: mr.web_url,
|
|
80
|
+
created_at: mr.created_at
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async getMergeRequestChanges(projectId, mrIid) {
|
|
85
|
+
const result = await this.request(`/api/v4/projects/${encodeURIComponent(String(projectId))}/merge_requests/${mrIid}/changes`);
|
|
86
|
+
if (!result.changes) return [];
|
|
87
|
+
return result.changes.map(c => ({
|
|
88
|
+
old_path: c.old_path,
|
|
89
|
+
new_path: c.new_path,
|
|
90
|
+
new_file: c.new_file,
|
|
91
|
+
renamed_file: c.renamed_file,
|
|
92
|
+
deleted_file: c.deleted_file,
|
|
93
|
+
diff: c.diff
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async getMergeRequestDetail(projectId, mrIid) {
|
|
98
|
+
return await this.request(`/api/v4/projects/${encodeURIComponent(String(projectId))}/merge_requests/${mrIid}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async getFileContent(projectId, filePath, ref) {
|
|
102
|
+
try {
|
|
103
|
+
const response = await this.client.request({
|
|
104
|
+
url: `/api/v4/projects/${encodeURIComponent(String(projectId))}/repository/files/${encodeURIComponent(filePath)}/raw`,
|
|
105
|
+
method: 'GET',
|
|
106
|
+
params: { ref },
|
|
107
|
+
responseType: 'text'
|
|
108
|
+
});
|
|
109
|
+
return response.data;
|
|
110
|
+
} catch (err) {
|
|
111
|
+
console.error(`Failed to get file ${filePath}`);
|
|
112
|
+
throw err;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async canMergeMR(projectId, mrIid) {
|
|
117
|
+
try {
|
|
118
|
+
const mr = await this.getMergeRequestDetail(projectId, mrIid);
|
|
119
|
+
if (mr.user_can_merge === false) return { canMerge: false, reason: '没有合并权限' };
|
|
120
|
+
if (mr.merge_status !== 'can_be_merged' && mr.merge_status !== 'unchecked') return { canMerge: false, reason: '状态不允许合并' };
|
|
121
|
+
if (mr.has_conflicts) return { canMerge: false, reason: '存在冲突' };
|
|
122
|
+
return { canMerge: true };
|
|
123
|
+
} catch (e) {
|
|
124
|
+
return { canMerge: false, reason: e.message };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async merge(projectId, mrIid) {
|
|
129
|
+
const check = await this.canMergeMR(projectId, mrIid);
|
|
130
|
+
if (!check.canMerge) throw new Error(check.reason);
|
|
131
|
+
return await this.request(`/api/v4/projects/${encodeURIComponent(String(projectId))}/merge_requests/${mrIid}/merge`, { method: 'PUT' });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async getMergeRequestNotes(projectId, mrIid) {
|
|
135
|
+
return await this.request(`/api/v4/projects/${encodeURIComponent(String(projectId))}/merge_requests/${mrIid}/notes?sort=asc&per_page=100`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async addMergeRequestNote(projectId, mrIid, params) {
|
|
139
|
+
return await this.request(`/api/v4/projects/${encodeURIComponent(String(projectId))}/merge_requests/${mrIid}/notes`, {
|
|
140
|
+
method: 'POST',
|
|
141
|
+
data: params
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
static getFileOverview(changes) {
|
|
146
|
+
return changes.map(change => {
|
|
147
|
+
let status = 'modified';
|
|
148
|
+
if (change.new_file) status = 'added';
|
|
149
|
+
else if (change.deleted_file) status = 'deleted';
|
|
150
|
+
else if (change.renamed_file) status = 'renamed';
|
|
151
|
+
|
|
152
|
+
const diffLines = (change.diff || '').split('\n');
|
|
153
|
+
let additions = 0, deletions = 0;
|
|
154
|
+
for (const line of diffLines) {
|
|
155
|
+
if (line.startsWith('+') && !line.startsWith('+++')) additions++;
|
|
156
|
+
else if (line.startsWith('-') && !line.startsWith('---')) deletions++;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return { path: change.new_path, status, additions, deletions };
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
module.exports = GitLabClient;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const { exec } = require('child_process');
|
|
2
|
+
const axios = require('axios');
|
|
3
|
+
const storage = require('./storage');
|
|
4
|
+
const logger = require('./logger');
|
|
5
|
+
|
|
6
|
+
class Hooks {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.hooksConfig = storage.get('hooks') || {};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 执行指定生命周期的 Hook
|
|
13
|
+
*/
|
|
14
|
+
async emit(eventName, payload = {}) {
|
|
15
|
+
const hookAction = this.hooksConfig[eventName];
|
|
16
|
+
if (!hookAction) return;
|
|
17
|
+
|
|
18
|
+
logger.info(`触发 Hook: ${eventName}...`);
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
if (hookAction.startsWith('http')) {
|
|
22
|
+
// 如果是 URL,执行 Webhook 发送
|
|
23
|
+
await this.executeWebhook(hookAction, payload);
|
|
24
|
+
} else {
|
|
25
|
+
// 否则认为是本地的 shell command
|
|
26
|
+
await this.executeShell(hookAction, payload);
|
|
27
|
+
}
|
|
28
|
+
logger.success(`Hook [${eventName}] 执行成功`);
|
|
29
|
+
} catch (err) {
|
|
30
|
+
logger.error(`Hook [${eventName}] 执行失败: ${err.message}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
executeWebhook(url, payload) {
|
|
35
|
+
return axios.post(url, payload, { timeout: 5000 });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
executeShell(command, payload) {
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
// 将 payload 转换为环境变量传入 shell
|
|
41
|
+
const env = { ...process.env };
|
|
42
|
+
|
|
43
|
+
// 将 JSON payload 添加到全局环境变量中,以便 bash 脚本获取
|
|
44
|
+
env['HONG_REVIEW_PAYLOAD'] = JSON.stringify(payload);
|
|
45
|
+
// 也可以把特定的字段展开
|
|
46
|
+
if (payload.mrId) env['MR_ID'] = payload.mrId;
|
|
47
|
+
if (payload.status) env['REVIEW_STATUS'] = payload.status;
|
|
48
|
+
|
|
49
|
+
exec(command, { env }, (error, stdout, stderr) => {
|
|
50
|
+
if (error) {
|
|
51
|
+
reject(error);
|
|
52
|
+
} else {
|
|
53
|
+
resolve(stdout);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = new Hooks();
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const ora = require('ora');
|
|
3
|
+
|
|
4
|
+
const spinners = new Map();
|
|
5
|
+
|
|
6
|
+
class Logger {
|
|
7
|
+
info(msg) {
|
|
8
|
+
console.log(chalk.blue('ℹ ' + msg));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
success(msg) {
|
|
12
|
+
console.log(chalk.green('✔ ' + msg));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
warn(msg) {
|
|
16
|
+
console.log(chalk.yellow('⚠ ' + msg));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
error(msg) {
|
|
20
|
+
console.log(chalk.red('✖ ' + msg));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
startSpinner(id, text) {
|
|
24
|
+
if (spinners.has(id)) {
|
|
25
|
+
spinners.get(id).stop();
|
|
26
|
+
}
|
|
27
|
+
const spinner = ora(text).start();
|
|
28
|
+
spinners.set(id, spinner);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
stopSpinner(id, success = true, text = '') {
|
|
32
|
+
const spinner = spinners.get(id);
|
|
33
|
+
if (!spinner) return;
|
|
34
|
+
|
|
35
|
+
if (success) {
|
|
36
|
+
spinner.succeed(text || spinner.text);
|
|
37
|
+
} else {
|
|
38
|
+
spinner.fail(text || spinner.text);
|
|
39
|
+
}
|
|
40
|
+
spinners.delete(id);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
logRaw(msg) {
|
|
44
|
+
console.log(msg);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
divider() {
|
|
48
|
+
console.log(chalk.gray('-'.repeat(50)));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = new Logger();
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
const CONFIG_FILE = path.join(os.homedir(), '.hong-review-config.json');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 缺省配置
|
|
9
|
+
*/
|
|
10
|
+
const DEFAULT_CONFIG = {
|
|
11
|
+
gitlabUrl: 'https://gitlab.com',
|
|
12
|
+
gitlabToken: '',
|
|
13
|
+
aiKey: '',
|
|
14
|
+
aiBaseUrl: 'https://api.openai.com/v1',
|
|
15
|
+
aiModel: 'gpt-4o',
|
|
16
|
+
hooks: {
|
|
17
|
+
onReviewStart: '',
|
|
18
|
+
onReviewSuccess: '',
|
|
19
|
+
onReviewFailed: '',
|
|
20
|
+
onMerged: ''
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
class Storage {
|
|
25
|
+
constructor() {
|
|
26
|
+
this.config = this.loadConfig();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
loadConfig() {
|
|
30
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
31
|
+
return { ...DEFAULT_CONFIG };
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const data = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
|
35
|
+
return { ...DEFAULT_CONFIG, ...JSON.parse(data) };
|
|
36
|
+
} catch (e) {
|
|
37
|
+
console.warn('读取配置文件失败,使用默认配置', e.message);
|
|
38
|
+
return { ...DEFAULT_CONFIG };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
saveConfig(newConfig) {
|
|
43
|
+
this.config = { ...this.config, ...newConfig };
|
|
44
|
+
try {
|
|
45
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(this.config, null, 2), 'utf-8');
|
|
46
|
+
return true;
|
|
47
|
+
} catch (e) {
|
|
48
|
+
console.error('保存配置文件失败', e.message);
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
get(key) {
|
|
54
|
+
return this.config[key];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
set(key, value) {
|
|
58
|
+
this.config[key] = value;
|
|
59
|
+
return this.saveConfig(this.config);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
getAll() {
|
|
63
|
+
return this.config;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = new Storage();
|