mm-qa-mcp 3.0.2__py3-none-any.whl → 3.0.4__py3-none-any.whl
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.
- minimax_qa_mcp/conf/conf.ini +12 -0
- minimax_qa_mcp/server.py +175 -3
- minimax_qa_mcp/src/auto_case/__init__.py +0 -0
- minimax_qa_mcp/src/auto_case/case_write.py +182 -0
- minimax_qa_mcp/src/auto_case/pdf_jiexi.py +28 -0
- minimax_qa_mcp/src/gitlab_branch_analyzer/__init__.py +11 -0
- minimax_qa_mcp/src/gitlab_branch_analyzer/gitlab_branch_service.py +1150 -0
- {mm_qa_mcp-3.0.2.dist-info → mm_qa_mcp-3.0.4.dist-info}/METADATA +2 -1
- {mm_qa_mcp-3.0.2.dist-info → mm_qa_mcp-3.0.4.dist-info}/RECORD +12 -7
- {mm_qa_mcp-3.0.2.dist-info → mm_qa_mcp-3.0.4.dist-info}/WHEEL +1 -1
- {mm_qa_mcp-3.0.2.dist-info → mm_qa_mcp-3.0.4.dist-info}/entry_points.txt +0 -0
- {mm_qa_mcp-3.0.2.dist-info → mm_qa_mcp-3.0.4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1150 @@
|
|
|
1
|
+
"""
|
|
2
|
+
coding:utf-8
|
|
3
|
+
@Software: PyCharm
|
|
4
|
+
@Time: 2026/2/4
|
|
5
|
+
@Author: xingyun
|
|
6
|
+
@Desc: GitLab分支查询服务 - 根据产品线获取最近创建的分支
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import requests
|
|
11
|
+
from datetime import datetime, timedelta, timezone
|
|
12
|
+
from typing import List, Dict, Optional, Union
|
|
13
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
14
|
+
|
|
15
|
+
from minimax_qa_mcp.utils.logger import logger
|
|
16
|
+
from minimax_qa_mcp.utils.utils import Utils
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class GitlabBranchService:
|
|
20
|
+
"""
|
|
21
|
+
GitLab分支查询服务
|
|
22
|
+
功能:
|
|
23
|
+
1. 从Apollo获取产品线对应的GitLab Group ID
|
|
24
|
+
2. 自动获取该Group下的所有项目
|
|
25
|
+
3. 查询这些项目最近N天内新创建的分支
|
|
26
|
+
4. 返回仓库地址+分支名称
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, product_line: str):
|
|
30
|
+
"""
|
|
31
|
+
初始化GitLab分支查询服务
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
product_line: 产品线名称,如 xingye, talkie, hailuo
|
|
35
|
+
"""
|
|
36
|
+
self.product_line = product_line
|
|
37
|
+
|
|
38
|
+
# 从配置文件读取GitLab相关配置
|
|
39
|
+
try:
|
|
40
|
+
self.gitlab_url = Utils.get_conf('git_info', 'gitlab_url')
|
|
41
|
+
except KeyError:
|
|
42
|
+
self.gitlab_url = "https://gitlab.xaminim.com"
|
|
43
|
+
logger.warning(f"未找到gitlab_url配置,使用默认值: {self.gitlab_url}")
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
self.access_token = Utils.get_conf('git_info', 'access_token')
|
|
47
|
+
except KeyError:
|
|
48
|
+
logger.error("未找到GitLab access_token配置,请在conf.ini中配置[git_info] access_token")
|
|
49
|
+
raise ValueError("缺少GitLab access_token配置")
|
|
50
|
+
|
|
51
|
+
self.headers = {'PRIVATE-TOKEN': self.access_token}
|
|
52
|
+
|
|
53
|
+
# Apollo配置
|
|
54
|
+
try:
|
|
55
|
+
self.apollo_base_url = Utils.get_conf('apollo_info', 'apollo_base_url')
|
|
56
|
+
except KeyError:
|
|
57
|
+
self.apollo_base_url = "http://swing-babel-ali-prod.xaminim.com/swing/api/get_apollo_value_by_key"
|
|
58
|
+
logger.info(f"使用默认Apollo URL: {self.apollo_base_url}")
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
self.apollo_key = Utils.get_conf('apollo_info', 'product_line_gitlab_key')
|
|
62
|
+
except KeyError:
|
|
63
|
+
self.apollo_key = "product_line_gitlab_projects"
|
|
64
|
+
logger.info(f"使用默认Apollo key: {self.apollo_key}")
|
|
65
|
+
|
|
66
|
+
# 并发配置
|
|
67
|
+
self.max_workers = 5
|
|
68
|
+
|
|
69
|
+
logger.info(f"初始化GitlabBranchService: product_line={product_line}, gitlab_url={self.gitlab_url}")
|
|
70
|
+
|
|
71
|
+
def get_projects_from_group(self, group_id: int) -> List[Dict]:
|
|
72
|
+
"""
|
|
73
|
+
从GitLab Group获取所有项目
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
group_id: GitLab Group ID
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
list: 项目列表,每项包含 project_id, project_name, project_path
|
|
80
|
+
"""
|
|
81
|
+
logger.info(f"从GitLab Group {group_id} 获取所有项目")
|
|
82
|
+
|
|
83
|
+
projects = []
|
|
84
|
+
page = 1
|
|
85
|
+
per_page = 100
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
while True:
|
|
89
|
+
# GitLab API: 获取Group下的所有项目
|
|
90
|
+
group_projects_url = f"{self.gitlab_url}/api/v4/groups/{group_id}/projects"
|
|
91
|
+
params = {
|
|
92
|
+
'per_page': per_page,
|
|
93
|
+
'page': page,
|
|
94
|
+
'include_subgroups': True, # 包含子Group的项目
|
|
95
|
+
'archived': False # 排除已归档的项目
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
response = requests.get(group_projects_url, headers=self.headers, params=params, timeout=30)
|
|
99
|
+
|
|
100
|
+
if response.status_code != 200:
|
|
101
|
+
logger.error(f"获取Group项目列表失败: {response.status_code} - {response.text}")
|
|
102
|
+
break
|
|
103
|
+
|
|
104
|
+
page_projects = response.json()
|
|
105
|
+
|
|
106
|
+
if not page_projects:
|
|
107
|
+
break
|
|
108
|
+
|
|
109
|
+
for proj in page_projects:
|
|
110
|
+
projects.append({
|
|
111
|
+
'project_id': proj.get('id'),
|
|
112
|
+
'project_name': proj.get('name', ''),
|
|
113
|
+
'project_path': proj.get('path_with_namespace', ''),
|
|
114
|
+
'web_url': proj.get('web_url', ''),
|
|
115
|
+
'default_branch': proj.get('default_branch', 'main')
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
# 检查是否还有下一页
|
|
119
|
+
if len(page_projects) < per_page:
|
|
120
|
+
break
|
|
121
|
+
|
|
122
|
+
page += 1
|
|
123
|
+
|
|
124
|
+
logger.info(f"Group {group_id} 共找到 {len(projects)} 个项目")
|
|
125
|
+
return projects
|
|
126
|
+
|
|
127
|
+
except requests.RequestException as e:
|
|
128
|
+
logger.error(f"请求GitLab Group项目列表失败: {str(e)}")
|
|
129
|
+
return []
|
|
130
|
+
except Exception as e:
|
|
131
|
+
logger.error(f"处理Group项目数据失败: {str(e)}")
|
|
132
|
+
return []
|
|
133
|
+
|
|
134
|
+
def get_group_config_from_apollo(self) -> Union[List[int], None]:
|
|
135
|
+
"""
|
|
136
|
+
从Apollo获取产品线对应的GitLab Group ID列表
|
|
137
|
+
|
|
138
|
+
Apollo配置格式示例:
|
|
139
|
+
{
|
|
140
|
+
"qa_group": [{"group_id": 1117, "group_name": "qa"}],
|
|
141
|
+
"xingye": [{"group_id": 123, "group_name": "xingye-server"}],
|
|
142
|
+
"talkie": [{"group_id": 456, "group_name": "talkie"}, {"group_id": 789, "group_name": "talkie-common"}]
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
list: Group ID列表,如果未找到返回None
|
|
147
|
+
"""
|
|
148
|
+
logger.info(f"从Apollo获取产品线 {self.product_line} 的GitLab Group配置")
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
# 构建Apollo请求URL
|
|
152
|
+
apollo_url = f"{self.apollo_base_url}?key={self.apollo_key}"
|
|
153
|
+
logger.info(f"请求Apollo URL: {apollo_url}")
|
|
154
|
+
|
|
155
|
+
response = requests.get(apollo_url, timeout=10)
|
|
156
|
+
|
|
157
|
+
if response.status_code != 200:
|
|
158
|
+
logger.error(f"Apollo请求失败: {response.status_code} - {response.text}")
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
# 解析响应
|
|
162
|
+
result = response.json()
|
|
163
|
+
logger.info(f"Apollo响应: {result}")
|
|
164
|
+
|
|
165
|
+
# Apollo返回的数据格式: {"value": "...", "base_resp": {...}}
|
|
166
|
+
if isinstance(result, dict):
|
|
167
|
+
if 'value' in result:
|
|
168
|
+
config_data = result['value']
|
|
169
|
+
elif 'data' in result:
|
|
170
|
+
config_data = result['data']
|
|
171
|
+
else:
|
|
172
|
+
config_data = result
|
|
173
|
+
else:
|
|
174
|
+
config_data = result
|
|
175
|
+
|
|
176
|
+
# 如果config_data是字符串,尝试解析为JSON
|
|
177
|
+
if isinstance(config_data, str):
|
|
178
|
+
try:
|
|
179
|
+
# 移除可能存在的尾随逗号(非标准JSON)
|
|
180
|
+
import re
|
|
181
|
+
cleaned_data = re.sub(r',\s*}', '}', config_data)
|
|
182
|
+
cleaned_data = re.sub(r',\s*\]', ']', cleaned_data)
|
|
183
|
+
config_data = json.loads(cleaned_data)
|
|
184
|
+
except json.JSONDecodeError as e:
|
|
185
|
+
logger.error(f"Apollo返回的数据不是有效的JSON: {config_data}, 错误: {e}")
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
# 获取对应产品线的Group配置
|
|
189
|
+
if self.product_line in config_data:
|
|
190
|
+
group_config = config_data[self.product_line]
|
|
191
|
+
group_ids = []
|
|
192
|
+
|
|
193
|
+
# 格式: [{"group_id": 1117, "group_name": "qa"}, ...]
|
|
194
|
+
if isinstance(group_config, list):
|
|
195
|
+
for item in group_config:
|
|
196
|
+
if isinstance(item, dict) and 'group_id' in item:
|
|
197
|
+
group_ids.append(item['group_id'])
|
|
198
|
+
elif isinstance(item, int):
|
|
199
|
+
# 兼容直接配置数字的情况
|
|
200
|
+
group_ids.append(item)
|
|
201
|
+
|
|
202
|
+
if group_ids:
|
|
203
|
+
logger.info(f"产品线 {self.product_line} 配置了 {len(group_ids)} 个Group: {group_ids}")
|
|
204
|
+
return group_ids
|
|
205
|
+
else:
|
|
206
|
+
logger.warning(f"产品线 {self.product_line} 的Group配置为空")
|
|
207
|
+
return None
|
|
208
|
+
elif isinstance(group_config, int):
|
|
209
|
+
# 兼容直接配置单个group_id的情况
|
|
210
|
+
return [group_config]
|
|
211
|
+
else:
|
|
212
|
+
logger.warning(f"产品线 {self.product_line} 的Group配置格式不正确: {group_config}")
|
|
213
|
+
return None
|
|
214
|
+
else:
|
|
215
|
+
logger.warning(f"未找到产品线 {self.product_line} 的配置,可用的产品线: {list(config_data.keys())}")
|
|
216
|
+
return None
|
|
217
|
+
|
|
218
|
+
except requests.RequestException as e:
|
|
219
|
+
logger.error(f"请求Apollo失败: {str(e)}")
|
|
220
|
+
return None
|
|
221
|
+
except Exception as e:
|
|
222
|
+
logger.error(f"解析Apollo响应失败: {str(e)}")
|
|
223
|
+
return None
|
|
224
|
+
|
|
225
|
+
def get_all_projects(self) -> List[Dict]:
|
|
226
|
+
"""
|
|
227
|
+
获取产品线对应的所有GitLab项目
|
|
228
|
+
|
|
229
|
+
通过Apollo获取Group ID列表,然后自动获取这些Group下的所有项目
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
list: 项目列表,每项包含 project_id, project_name, project_path
|
|
233
|
+
"""
|
|
234
|
+
logger.info(f"获取产品线 {self.product_line} 的所有GitLab项目")
|
|
235
|
+
|
|
236
|
+
# 从Apollo获取Group ID列表
|
|
237
|
+
group_ids = self.get_group_config_from_apollo()
|
|
238
|
+
|
|
239
|
+
if not group_ids:
|
|
240
|
+
logger.warning(f"产品线 {self.product_line} 未配置任何GitLab Group")
|
|
241
|
+
return []
|
|
242
|
+
|
|
243
|
+
all_projects = []
|
|
244
|
+
|
|
245
|
+
# 遍历所有Group,获取其下的项目
|
|
246
|
+
for group_id in group_ids:
|
|
247
|
+
projects = self.get_projects_from_group(group_id)
|
|
248
|
+
all_projects.extend(projects)
|
|
249
|
+
|
|
250
|
+
# 去重(按project_id)
|
|
251
|
+
seen_ids = set()
|
|
252
|
+
unique_projects = []
|
|
253
|
+
for proj in all_projects:
|
|
254
|
+
if proj['project_id'] not in seen_ids:
|
|
255
|
+
seen_ids.add(proj['project_id'])
|
|
256
|
+
unique_projects.append(proj)
|
|
257
|
+
|
|
258
|
+
logger.info(f"产品线 {self.product_line} 共找到 {len(unique_projects)} 个唯一项目")
|
|
259
|
+
return unique_projects
|
|
260
|
+
|
|
261
|
+
def get_recent_branches(self, project_id: int, project_name: str,
|
|
262
|
+
project_path: str, days: int = 3) -> List[Dict]:
|
|
263
|
+
"""
|
|
264
|
+
获取项目最近N天内新创建的分支
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
project_id: GitLab项目ID
|
|
268
|
+
project_name: 项目名称
|
|
269
|
+
project_path: 项目路径
|
|
270
|
+
days: 查询最近多少天的分支,默认3天
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
list: 分支列表,每项包含 repo_url, project_name, branch_name, created_at, author
|
|
274
|
+
"""
|
|
275
|
+
logger.info(f"获取项目 {project_name} (ID: {project_id}) 最近 {days} 天的分支")
|
|
276
|
+
|
|
277
|
+
recent_branches = []
|
|
278
|
+
|
|
279
|
+
try:
|
|
280
|
+
# 计算时间阈值
|
|
281
|
+
now = datetime.now(timezone(timedelta(hours=8)))
|
|
282
|
+
threshold = now - timedelta(days=days)
|
|
283
|
+
|
|
284
|
+
# 获取分支列表
|
|
285
|
+
branches_url = f"{self.gitlab_url}/api/v4/projects/{project_id}/repository/branches"
|
|
286
|
+
params = {
|
|
287
|
+
'per_page': 100 # 获取更多分支
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
response = requests.get(branches_url, headers=self.headers, params=params, timeout=30)
|
|
291
|
+
|
|
292
|
+
if response.status_code != 200:
|
|
293
|
+
logger.error(f"获取分支列表失败: {response.status_code} - {response.text}")
|
|
294
|
+
return []
|
|
295
|
+
|
|
296
|
+
branches = response.json()
|
|
297
|
+
logger.info(f"项目 {project_name} 共有 {len(branches)} 个分支")
|
|
298
|
+
|
|
299
|
+
# 筛选最近N天创建的分支
|
|
300
|
+
for branch in branches:
|
|
301
|
+
branch_name = branch.get('name', '')
|
|
302
|
+
commit = branch.get('commit', {})
|
|
303
|
+
committed_date_str = commit.get('committed_date', '')
|
|
304
|
+
author_name = commit.get('author_name', 'unknown')
|
|
305
|
+
|
|
306
|
+
if not committed_date_str:
|
|
307
|
+
continue
|
|
308
|
+
|
|
309
|
+
# 解析提交时间
|
|
310
|
+
try:
|
|
311
|
+
# GitLab返回的时间格式: 2026-02-03T10:30:00.000+08:00
|
|
312
|
+
committed_date = datetime.fromisoformat(committed_date_str.replace('Z', '+00:00'))
|
|
313
|
+
|
|
314
|
+
# 检查是否在时间范围内
|
|
315
|
+
if committed_date >= threshold:
|
|
316
|
+
# 排除主分支
|
|
317
|
+
if branch_name not in ['main', 'master', 'develop', 'release']:
|
|
318
|
+
repo_url = f"{self.gitlab_url}/{project_path}"
|
|
319
|
+
recent_branches.append({
|
|
320
|
+
'repo_url': repo_url,
|
|
321
|
+
'project_name': project_name,
|
|
322
|
+
'branch_name': branch_name,
|
|
323
|
+
'created_at': committed_date_str,
|
|
324
|
+
'author': author_name
|
|
325
|
+
})
|
|
326
|
+
logger.info(f"找到新分支: {branch_name} (创建于 {committed_date_str})")
|
|
327
|
+
except (ValueError, TypeError) as e:
|
|
328
|
+
logger.warning(f"解析分支 {branch_name} 的时间失败: {e}")
|
|
329
|
+
continue
|
|
330
|
+
|
|
331
|
+
logger.info(f"项目 {project_name} 最近 {days} 天有 {len(recent_branches)} 个新分支")
|
|
332
|
+
return recent_branches
|
|
333
|
+
|
|
334
|
+
except requests.RequestException as e:
|
|
335
|
+
logger.error(f"请求GitLab分支列表失败: {str(e)}")
|
|
336
|
+
return []
|
|
337
|
+
except Exception as e:
|
|
338
|
+
logger.error(f"处理分支数据失败: {str(e)}")
|
|
339
|
+
return []
|
|
340
|
+
|
|
341
|
+
def analyze_all_projects(self, days: int = 3) -> List[Dict]:
|
|
342
|
+
"""
|
|
343
|
+
分析所有项目的新分支
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
days: 查询最近多少天的分支,默认3天
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
list: 所有新分支的列表
|
|
350
|
+
"""
|
|
351
|
+
logger.info(f"开始分析产品线 {self.product_line} 所有项目最近 {days} 天的新分支")
|
|
352
|
+
|
|
353
|
+
# 从Apollo获取Group配置,然后自动获取Group下所有项目
|
|
354
|
+
projects = self.get_all_projects()
|
|
355
|
+
|
|
356
|
+
if not projects:
|
|
357
|
+
logger.warning(f"产品线 {self.product_line} 没有找到任何GitLab项目")
|
|
358
|
+
return []
|
|
359
|
+
|
|
360
|
+
all_branches = []
|
|
361
|
+
|
|
362
|
+
# 使用线程池并发查询各项目的分支
|
|
363
|
+
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
|
|
364
|
+
future_to_project = {}
|
|
365
|
+
|
|
366
|
+
for project in projects:
|
|
367
|
+
project_id = project.get('project_id')
|
|
368
|
+
project_name = project.get('project_name', '')
|
|
369
|
+
project_path = project.get('project_path', '')
|
|
370
|
+
|
|
371
|
+
if not project_id:
|
|
372
|
+
logger.warning(f"项目配置缺少project_id: {project}")
|
|
373
|
+
continue
|
|
374
|
+
|
|
375
|
+
future = executor.submit(
|
|
376
|
+
self.get_recent_branches,
|
|
377
|
+
project_id,
|
|
378
|
+
project_name,
|
|
379
|
+
project_path,
|
|
380
|
+
days
|
|
381
|
+
)
|
|
382
|
+
future_to_project[future] = project_name
|
|
383
|
+
|
|
384
|
+
# 收集结果
|
|
385
|
+
for future in as_completed(future_to_project):
|
|
386
|
+
project_name = future_to_project[future]
|
|
387
|
+
try:
|
|
388
|
+
branches = future.result()
|
|
389
|
+
all_branches.extend(branches)
|
|
390
|
+
except Exception as e:
|
|
391
|
+
logger.error(f"获取项目 {project_name} 分支失败: {str(e)}")
|
|
392
|
+
|
|
393
|
+
# 按创建时间排序(最新的在前)
|
|
394
|
+
all_branches.sort(key=lambda x: x.get('created_at', ''), reverse=True)
|
|
395
|
+
|
|
396
|
+
logger.info(f"产品线 {self.product_line} 共找到 {len(all_branches)} 个新分支")
|
|
397
|
+
return all_branches
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def get_recent_branches_by_product_line(product_line: str, days: int = 3) -> List[Dict]:
|
|
401
|
+
"""
|
|
402
|
+
获取产品线最近N天新创建的分支列表(供外部调用的便捷函数)
|
|
403
|
+
|
|
404
|
+
Args:
|
|
405
|
+
product_line: 产品线名称,如 xingye, talkie, hailuo
|
|
406
|
+
days: 查询最近多少天的分支,默认3天
|
|
407
|
+
|
|
408
|
+
Returns:
|
|
409
|
+
列表,包含 [{"repo_url": "xxx", "branch_name": "xxx", "created_at": "xxx", "author": "xxx"}, ...]
|
|
410
|
+
"""
|
|
411
|
+
service = GitlabBranchService(product_line)
|
|
412
|
+
return service.analyze_all_projects(days)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
class GitlabRepoTypeDetector:
|
|
416
|
+
"""
|
|
417
|
+
GitLab仓库类型检测器
|
|
418
|
+
通过分析仓库的文件结构判断是 Server 还是 FE 项目
|
|
419
|
+
"""
|
|
420
|
+
|
|
421
|
+
# Server 项目特征文件和目录
|
|
422
|
+
SERVER_INDICATORS = {
|
|
423
|
+
'go': {
|
|
424
|
+
'files': ['go.mod', 'go.sum'],
|
|
425
|
+
'dirs': ['cmd/', 'pkg/', 'internal/', 'api/'],
|
|
426
|
+
'weight': 1.0
|
|
427
|
+
},
|
|
428
|
+
'java': {
|
|
429
|
+
'files': ['pom.xml', 'build.gradle', 'build.gradle.kts'],
|
|
430
|
+
'dirs': ['src/main/java/', 'src/main/resources/'],
|
|
431
|
+
'weight': 1.0
|
|
432
|
+
},
|
|
433
|
+
'python': {
|
|
434
|
+
'files': ['requirements.txt', 'pyproject.toml', 'setup.py', 'Pipfile'],
|
|
435
|
+
'dirs': ['src/', 'app/', 'api/'],
|
|
436
|
+
'weight': 0.8 # Python 也可能是脚本项目
|
|
437
|
+
},
|
|
438
|
+
'rust': {
|
|
439
|
+
'files': ['Cargo.toml', 'Cargo.lock'],
|
|
440
|
+
'dirs': ['src/'],
|
|
441
|
+
'weight': 1.0
|
|
442
|
+
},
|
|
443
|
+
'csharp': {
|
|
444
|
+
'files': ['*.csproj', '*.sln'],
|
|
445
|
+
'dirs': ['Controllers/', 'Services/'],
|
|
446
|
+
'weight': 1.0
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
# FE 项目特征文件和目录
|
|
451
|
+
FE_INDICATORS = {
|
|
452
|
+
'react': {
|
|
453
|
+
'files': ['package.json'],
|
|
454
|
+
'dirs': ['src/components/', 'src/pages/', 'public/'],
|
|
455
|
+
'package_deps': ['react', 'react-dom'],
|
|
456
|
+
'weight': 1.0
|
|
457
|
+
},
|
|
458
|
+
'vue': {
|
|
459
|
+
'files': ['package.json', 'vue.config.js', 'vite.config.ts'],
|
|
460
|
+
'dirs': ['src/components/', 'src/views/', 'public/'],
|
|
461
|
+
'package_deps': ['vue'],
|
|
462
|
+
'weight': 1.0
|
|
463
|
+
},
|
|
464
|
+
'angular': {
|
|
465
|
+
'files': ['package.json', 'angular.json'],
|
|
466
|
+
'dirs': ['src/app/', 'src/assets/'],
|
|
467
|
+
'package_deps': ['@angular/core'],
|
|
468
|
+
'weight': 1.0
|
|
469
|
+
},
|
|
470
|
+
'nextjs': {
|
|
471
|
+
'files': ['package.json', 'next.config.js', 'next.config.mjs'],
|
|
472
|
+
'dirs': ['pages/', 'app/', 'components/'],
|
|
473
|
+
'package_deps': ['next'],
|
|
474
|
+
'weight': 1.0
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
def __init__(self):
|
|
479
|
+
"""初始化检测器"""
|
|
480
|
+
try:
|
|
481
|
+
self.gitlab_url = Utils.get_conf('git_info', 'gitlab_url')
|
|
482
|
+
except KeyError:
|
|
483
|
+
self.gitlab_url = "https://gitlab.xaminim.com"
|
|
484
|
+
|
|
485
|
+
try:
|
|
486
|
+
self.access_token = Utils.get_conf('git_info', 'access_token')
|
|
487
|
+
except KeyError:
|
|
488
|
+
raise ValueError("缺少GitLab access_token配置")
|
|
489
|
+
|
|
490
|
+
self.headers = {'PRIVATE-TOKEN': self.access_token}
|
|
491
|
+
|
|
492
|
+
def get_repo_tree(self, project_id: int, branch: str = None, path: str = '', recursive: bool = False) -> List[Dict]:
|
|
493
|
+
"""
|
|
494
|
+
获取仓库文件树
|
|
495
|
+
|
|
496
|
+
Args:
|
|
497
|
+
project_id: GitLab 项目 ID
|
|
498
|
+
branch: 分支名,默认使用默认分支
|
|
499
|
+
path: 子路径
|
|
500
|
+
recursive: 是否递归获取
|
|
501
|
+
|
|
502
|
+
Returns:
|
|
503
|
+
文件树列表
|
|
504
|
+
"""
|
|
505
|
+
url = f"{self.gitlab_url}/api/v4/projects/{project_id}/repository/tree"
|
|
506
|
+
params = {
|
|
507
|
+
'per_page': 100,
|
|
508
|
+
'recursive': recursive
|
|
509
|
+
}
|
|
510
|
+
if branch:
|
|
511
|
+
params['ref'] = branch
|
|
512
|
+
if path:
|
|
513
|
+
params['path'] = path
|
|
514
|
+
|
|
515
|
+
try:
|
|
516
|
+
response = requests.get(url, headers=self.headers, params=params, timeout=30)
|
|
517
|
+
if response.status_code == 200:
|
|
518
|
+
return response.json()
|
|
519
|
+
else:
|
|
520
|
+
logger.warning(f"获取仓库文件树失败: {response.status_code}")
|
|
521
|
+
return []
|
|
522
|
+
except Exception as e:
|
|
523
|
+
logger.error(f"请求GitLab文件树失败: {e}")
|
|
524
|
+
return []
|
|
525
|
+
|
|
526
|
+
def get_file_content(self, project_id: int, file_path: str, branch: str = None) -> Optional[str]:
|
|
527
|
+
"""
|
|
528
|
+
获取文件内容
|
|
529
|
+
|
|
530
|
+
Args:
|
|
531
|
+
project_id: GitLab 项目 ID
|
|
532
|
+
file_path: 文件路径
|
|
533
|
+
branch: 分支名
|
|
534
|
+
|
|
535
|
+
Returns:
|
|
536
|
+
文件内容字符串
|
|
537
|
+
"""
|
|
538
|
+
import urllib.parse
|
|
539
|
+
encoded_path = urllib.parse.quote(file_path, safe='')
|
|
540
|
+
url = f"{self.gitlab_url}/api/v4/projects/{project_id}/repository/files/{encoded_path}/raw"
|
|
541
|
+
params = {}
|
|
542
|
+
if branch:
|
|
543
|
+
params['ref'] = branch
|
|
544
|
+
|
|
545
|
+
try:
|
|
546
|
+
response = requests.get(url, headers=self.headers, params=params, timeout=30)
|
|
547
|
+
if response.status_code == 200:
|
|
548
|
+
return response.text
|
|
549
|
+
else:
|
|
550
|
+
return None
|
|
551
|
+
except Exception as e:
|
|
552
|
+
logger.error(f"获取文件内容失败: {e}")
|
|
553
|
+
return None
|
|
554
|
+
|
|
555
|
+
def detect_repo_type(self, project_id: int, branch: str = None) -> Dict:
|
|
556
|
+
"""
|
|
557
|
+
检测仓库类型
|
|
558
|
+
|
|
559
|
+
Args:
|
|
560
|
+
project_id: GitLab 项目 ID
|
|
561
|
+
branch: 分支名,默认使用默认分支
|
|
562
|
+
|
|
563
|
+
Returns:
|
|
564
|
+
{
|
|
565
|
+
"type": "server" | "fe" | "mixed" | "unknown",
|
|
566
|
+
"language": "go" | "java" | "python" | "react" | "vue" | ...,
|
|
567
|
+
"framework": 框架名称,
|
|
568
|
+
"confidence": 0.0-1.0,
|
|
569
|
+
"indicators": ["go.mod", "cmd/", ...]
|
|
570
|
+
}
|
|
571
|
+
"""
|
|
572
|
+
logger.info(f"检测项目 {project_id} 的仓库类型")
|
|
573
|
+
|
|
574
|
+
# 获取根目录文件列表
|
|
575
|
+
tree = self.get_repo_tree(project_id, branch)
|
|
576
|
+
if not tree:
|
|
577
|
+
return {
|
|
578
|
+
"type": "unknown",
|
|
579
|
+
"language": None,
|
|
580
|
+
"framework": None,
|
|
581
|
+
"confidence": 0.0,
|
|
582
|
+
"indicators": [],
|
|
583
|
+
"error": "无法获取仓库文件树"
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
# 提取文件名和目录名
|
|
587
|
+
files = set()
|
|
588
|
+
dirs = set()
|
|
589
|
+
for item in tree:
|
|
590
|
+
name = item.get('name', '')
|
|
591
|
+
item_type = item.get('type', '')
|
|
592
|
+
if item_type == 'blob':
|
|
593
|
+
files.add(name)
|
|
594
|
+
elif item_type == 'tree':
|
|
595
|
+
dirs.add(name + '/')
|
|
596
|
+
|
|
597
|
+
logger.info(f"根目录文件: {files}")
|
|
598
|
+
logger.info(f"根目录目录: {dirs}")
|
|
599
|
+
|
|
600
|
+
# 检测 Server 特征
|
|
601
|
+
server_score = 0.0
|
|
602
|
+
server_lang = None
|
|
603
|
+
server_indicators = []
|
|
604
|
+
|
|
605
|
+
for lang, indicators in self.SERVER_INDICATORS.items():
|
|
606
|
+
lang_score = 0
|
|
607
|
+
lang_indicators = []
|
|
608
|
+
|
|
609
|
+
# 检查特征文件
|
|
610
|
+
for f in indicators.get('files', []):
|
|
611
|
+
if f in files:
|
|
612
|
+
lang_score += 1
|
|
613
|
+
lang_indicators.append(f)
|
|
614
|
+
|
|
615
|
+
# 检查特征目录
|
|
616
|
+
for d in indicators.get('dirs', []):
|
|
617
|
+
dir_name = d.rstrip('/')
|
|
618
|
+
if dir_name + '/' in dirs:
|
|
619
|
+
lang_score += 0.5
|
|
620
|
+
lang_indicators.append(d)
|
|
621
|
+
|
|
622
|
+
if lang_score > server_score:
|
|
623
|
+
server_score = lang_score * indicators.get('weight', 1.0)
|
|
624
|
+
server_lang = lang
|
|
625
|
+
server_indicators = lang_indicators
|
|
626
|
+
|
|
627
|
+
# 检测 FE 特征
|
|
628
|
+
fe_score = 0.0
|
|
629
|
+
fe_framework = None
|
|
630
|
+
fe_indicators = []
|
|
631
|
+
|
|
632
|
+
# 先检查是否有 package.json
|
|
633
|
+
has_package_json = 'package.json' in files
|
|
634
|
+
|
|
635
|
+
if has_package_json:
|
|
636
|
+
# 读取 package.json 分析依赖
|
|
637
|
+
package_content = self.get_file_content(project_id, 'package.json', branch)
|
|
638
|
+
deps = set()
|
|
639
|
+
if package_content:
|
|
640
|
+
try:
|
|
641
|
+
package_json = json.loads(package_content)
|
|
642
|
+
deps.update(package_json.get('dependencies', {}).keys())
|
|
643
|
+
deps.update(package_json.get('devDependencies', {}).keys())
|
|
644
|
+
except json.JSONDecodeError:
|
|
645
|
+
pass
|
|
646
|
+
|
|
647
|
+
for framework, indicators in self.FE_INDICATORS.items():
|
|
648
|
+
framework_score = 0
|
|
649
|
+
framework_indicators = []
|
|
650
|
+
|
|
651
|
+
# 检查 package.json 依赖
|
|
652
|
+
for dep in indicators.get('package_deps', []):
|
|
653
|
+
if dep in deps:
|
|
654
|
+
framework_score += 2
|
|
655
|
+
framework_indicators.append(f"dep:{dep}")
|
|
656
|
+
|
|
657
|
+
# 检查特征文件
|
|
658
|
+
for f in indicators.get('files', []):
|
|
659
|
+
if f in files and f != 'package.json':
|
|
660
|
+
framework_score += 0.5
|
|
661
|
+
framework_indicators.append(f)
|
|
662
|
+
|
|
663
|
+
# 检查特征目录
|
|
664
|
+
for d in indicators.get('dirs', []):
|
|
665
|
+
dir_name = d.rstrip('/')
|
|
666
|
+
if dir_name + '/' in dirs:
|
|
667
|
+
framework_score += 0.5
|
|
668
|
+
framework_indicators.append(d)
|
|
669
|
+
|
|
670
|
+
if framework_score > fe_score:
|
|
671
|
+
fe_score = framework_score * indicators.get('weight', 1.0)
|
|
672
|
+
fe_framework = framework
|
|
673
|
+
fe_indicators = framework_indicators
|
|
674
|
+
|
|
675
|
+
# 判断类型
|
|
676
|
+
logger.info(f"Server 得分: {server_score}, FE 得分: {fe_score}")
|
|
677
|
+
|
|
678
|
+
if server_score > 0 and fe_score > 0:
|
|
679
|
+
# 混合项目
|
|
680
|
+
if server_score > fe_score * 1.5:
|
|
681
|
+
repo_type = "server"
|
|
682
|
+
language = server_lang
|
|
683
|
+
framework = None
|
|
684
|
+
confidence = min(server_score / 3, 1.0)
|
|
685
|
+
indicators = server_indicators
|
|
686
|
+
elif fe_score > server_score * 1.5:
|
|
687
|
+
repo_type = "fe"
|
|
688
|
+
language = fe_framework
|
|
689
|
+
framework = fe_framework
|
|
690
|
+
confidence = min(fe_score / 3, 1.0)
|
|
691
|
+
indicators = fe_indicators
|
|
692
|
+
else:
|
|
693
|
+
repo_type = "mixed"
|
|
694
|
+
language = f"{server_lang}+{fe_framework}"
|
|
695
|
+
framework = fe_framework
|
|
696
|
+
confidence = min((server_score + fe_score) / 5, 1.0)
|
|
697
|
+
indicators = server_indicators + fe_indicators
|
|
698
|
+
elif server_score > 0:
|
|
699
|
+
repo_type = "server"
|
|
700
|
+
language = server_lang
|
|
701
|
+
framework = None
|
|
702
|
+
confidence = min(server_score / 2, 1.0)
|
|
703
|
+
indicators = server_indicators
|
|
704
|
+
elif fe_score > 0:
|
|
705
|
+
repo_type = "fe"
|
|
706
|
+
language = fe_framework
|
|
707
|
+
framework = fe_framework
|
|
708
|
+
confidence = min(fe_score / 2, 1.0)
|
|
709
|
+
indicators = fe_indicators
|
|
710
|
+
else:
|
|
711
|
+
repo_type = "unknown"
|
|
712
|
+
language = None
|
|
713
|
+
framework = None
|
|
714
|
+
confidence = 0.0
|
|
715
|
+
indicators = []
|
|
716
|
+
|
|
717
|
+
result = {
|
|
718
|
+
"type": repo_type,
|
|
719
|
+
"language": language,
|
|
720
|
+
"framework": framework,
|
|
721
|
+
"confidence": round(confidence, 2),
|
|
722
|
+
"indicators": indicators
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
logger.info(f"检测结果: {result}")
|
|
726
|
+
return result
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
def detect_repo_type(project_id: int, branch: str = None) -> Dict:
|
|
730
|
+
"""
|
|
731
|
+
检测仓库类型(供外部调用的便捷函数)
|
|
732
|
+
|
|
733
|
+
Args:
|
|
734
|
+
project_id: GitLab 项目 ID
|
|
735
|
+
branch: 分支名,默认使用默认分支
|
|
736
|
+
|
|
737
|
+
Returns:
|
|
738
|
+
{
|
|
739
|
+
"type": "server" | "fe" | "mixed" | "unknown",
|
|
740
|
+
"language": 语言/框架名称,
|
|
741
|
+
"framework": 框架名称(FE 项目),
|
|
742
|
+
"confidence": 置信度 0.0-1.0,
|
|
743
|
+
"indicators": 检测到的特征列表
|
|
744
|
+
}
|
|
745
|
+
"""
|
|
746
|
+
detector = GitlabRepoTypeDetector()
|
|
747
|
+
return detector.detect_repo_type(project_id, branch)
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
class GitlabBedrockResolver:
|
|
751
|
+
"""
|
|
752
|
+
GitLab 仓库与 Bedrock 服务关联解析器
|
|
753
|
+
|
|
754
|
+
通过解析仓库的 CI 配置文件来获取 Bedrock 部署信息
|
|
755
|
+
"""
|
|
756
|
+
|
|
757
|
+
def __init__(self):
|
|
758
|
+
"""初始化解析器"""
|
|
759
|
+
try:
|
|
760
|
+
self.gitlab_url = Utils.get_conf('git_info', 'gitlab_url')
|
|
761
|
+
except KeyError:
|
|
762
|
+
self.gitlab_url = "https://gitlab.xaminim.com"
|
|
763
|
+
|
|
764
|
+
try:
|
|
765
|
+
self.access_token = Utils.get_conf('git_info', 'access_token')
|
|
766
|
+
except KeyError:
|
|
767
|
+
raise ValueError("缺少GitLab access_token配置")
|
|
768
|
+
|
|
769
|
+
self.headers = {'PRIVATE-TOKEN': self.access_token}
|
|
770
|
+
|
|
771
|
+
def get_file_content(self, project_id: int, file_path: str, branch: str = None) -> Optional[str]:
|
|
772
|
+
"""
|
|
773
|
+
获取文件内容
|
|
774
|
+
|
|
775
|
+
Args:
|
|
776
|
+
project_id: GitLab 项目 ID
|
|
777
|
+
file_path: 文件路径
|
|
778
|
+
branch: 分支名
|
|
779
|
+
|
|
780
|
+
Returns:
|
|
781
|
+
文件内容字符串
|
|
782
|
+
"""
|
|
783
|
+
import urllib.parse
|
|
784
|
+
encoded_path = urllib.parse.quote(file_path, safe='')
|
|
785
|
+
url = f"{self.gitlab_url}/api/v4/projects/{project_id}/repository/files/{encoded_path}/raw"
|
|
786
|
+
params = {}
|
|
787
|
+
if branch:
|
|
788
|
+
params['ref'] = branch
|
|
789
|
+
|
|
790
|
+
try:
|
|
791
|
+
response = requests.get(url, headers=self.headers, params=params, timeout=30)
|
|
792
|
+
if response.status_code == 200:
|
|
793
|
+
return response.text
|
|
794
|
+
else:
|
|
795
|
+
logger.debug(f"获取文件 {file_path} 失败: {response.status_code}")
|
|
796
|
+
return None
|
|
797
|
+
except Exception as e:
|
|
798
|
+
logger.error(f"获取文件内容失败: {e}")
|
|
799
|
+
return None
|
|
800
|
+
|
|
801
|
+
def get_project_info(self, project_id: int) -> Optional[Dict]:
|
|
802
|
+
"""
|
|
803
|
+
获取项目基本信息
|
|
804
|
+
|
|
805
|
+
Args:
|
|
806
|
+
project_id: GitLab 项目 ID
|
|
807
|
+
|
|
808
|
+
Returns:
|
|
809
|
+
项目信息字典
|
|
810
|
+
"""
|
|
811
|
+
url = f"{self.gitlab_url}/api/v4/projects/{project_id}"
|
|
812
|
+
|
|
813
|
+
try:
|
|
814
|
+
response = requests.get(url, headers=self.headers, timeout=30)
|
|
815
|
+
if response.status_code == 200:
|
|
816
|
+
return response.json()
|
|
817
|
+
else:
|
|
818
|
+
logger.warning(f"获取项目信息失败: {response.status_code}")
|
|
819
|
+
return None
|
|
820
|
+
except Exception as e:
|
|
821
|
+
logger.error(f"请求项目信息失败: {e}")
|
|
822
|
+
return None
|
|
823
|
+
|
|
824
|
+
def parse_gitlab_ci(self, ci_content: str) -> Dict:
|
|
825
|
+
"""
|
|
826
|
+
解析 .gitlab-ci.yml 内容,提取 Bedrock 相关配置
|
|
827
|
+
|
|
828
|
+
Args:
|
|
829
|
+
ci_content: .gitlab-ci.yml 文件内容
|
|
830
|
+
|
|
831
|
+
Returns:
|
|
832
|
+
Bedrock 配置信息
|
|
833
|
+
"""
|
|
834
|
+
import re
|
|
835
|
+
import yaml
|
|
836
|
+
|
|
837
|
+
result = {
|
|
838
|
+
'psm': None,
|
|
839
|
+
'bedrock_project': None,
|
|
840
|
+
'bedrock_app': None,
|
|
841
|
+
'image_repo': None,
|
|
842
|
+
'deploy_stages': [],
|
|
843
|
+
'raw_variables': {}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
try:
|
|
847
|
+
# 尝试解析 YAML
|
|
848
|
+
ci_config = yaml.safe_load(ci_content)
|
|
849
|
+
|
|
850
|
+
if not ci_config:
|
|
851
|
+
return result
|
|
852
|
+
|
|
853
|
+
# 1. 检查全局 variables
|
|
854
|
+
global_vars = ci_config.get('variables', {})
|
|
855
|
+
if global_vars:
|
|
856
|
+
result['raw_variables'].update(global_vars)
|
|
857
|
+
|
|
858
|
+
# 提取常见的 Bedrock 相关变量
|
|
859
|
+
for key, value in global_vars.items():
|
|
860
|
+
key_upper = key.upper()
|
|
861
|
+
if key_upper == 'PSM' or key_upper.endswith('_PSM'):
|
|
862
|
+
result['psm'] = value
|
|
863
|
+
elif key_upper == 'BEDROCK_PROJECT' or key_upper == 'PROJECT':
|
|
864
|
+
result['bedrock_project'] = value
|
|
865
|
+
elif key_upper == 'BEDROCK_APP' or key_upper == 'APP_NAME':
|
|
866
|
+
result['bedrock_app'] = value
|
|
867
|
+
elif 'IMAGE' in key_upper or 'REGISTRY' in key_upper:
|
|
868
|
+
if isinstance(value, str) and 'harbor' in value.lower():
|
|
869
|
+
result['image_repo'] = value
|
|
870
|
+
|
|
871
|
+
# 2. 检查各个 job 中的 variables
|
|
872
|
+
for job_name, job_config in ci_config.items():
|
|
873
|
+
if isinstance(job_config, dict):
|
|
874
|
+
job_vars = job_config.get('variables', {})
|
|
875
|
+
if job_vars:
|
|
876
|
+
for key, value in job_vars.items():
|
|
877
|
+
key_upper = key.upper()
|
|
878
|
+
if key_upper == 'PSM' and not result['psm']:
|
|
879
|
+
result['psm'] = value
|
|
880
|
+
elif key_upper == 'BEDROCK_PROJECT' and not result['bedrock_project']:
|
|
881
|
+
result['bedrock_project'] = value
|
|
882
|
+
elif key_upper == 'BEDROCK_APP' and not result['bedrock_app']:
|
|
883
|
+
result['bedrock_app'] = value
|
|
884
|
+
|
|
885
|
+
# 检查是否有 deploy 相关的 stage
|
|
886
|
+
stage = job_config.get('stage', '')
|
|
887
|
+
script = job_config.get('script', [])
|
|
888
|
+
if isinstance(stage, str) and 'deploy' in stage.lower():
|
|
889
|
+
result['deploy_stages'].append({
|
|
890
|
+
'job_name': job_name,
|
|
891
|
+
'stage': stage
|
|
892
|
+
})
|
|
893
|
+
# 检查 script 中是否有 bedrock 相关命令
|
|
894
|
+
if isinstance(script, list):
|
|
895
|
+
for cmd in script:
|
|
896
|
+
if isinstance(cmd, str) and 'bedrock' in cmd.lower():
|
|
897
|
+
if job_name not in [s.get('job_name') for s in result['deploy_stages']]:
|
|
898
|
+
result['deploy_stages'].append({
|
|
899
|
+
'job_name': job_name,
|
|
900
|
+
'stage': stage or 'unknown'
|
|
901
|
+
})
|
|
902
|
+
break
|
|
903
|
+
|
|
904
|
+
except yaml.YAMLError as e:
|
|
905
|
+
logger.warning(f"YAML 解析失败,尝试正则提取: {e}")
|
|
906
|
+
# 降级:使用正则表达式提取
|
|
907
|
+
|
|
908
|
+
# 提取 PSM
|
|
909
|
+
psm_match = re.search(r'PSM[:\s]*["\']?([a-zA-Z0-9_.]+)["\']?', ci_content, re.IGNORECASE)
|
|
910
|
+
if psm_match:
|
|
911
|
+
result['psm'] = psm_match.group(1)
|
|
912
|
+
|
|
913
|
+
# 提取 BEDROCK_PROJECT
|
|
914
|
+
project_match = re.search(r'BEDROCK_PROJECT[:\s]*["\']?([a-zA-Z0-9_-]+)["\']?', ci_content, re.IGNORECASE)
|
|
915
|
+
if project_match:
|
|
916
|
+
result['bedrock_project'] = project_match.group(1)
|
|
917
|
+
|
|
918
|
+
# 提取镜像仓库
|
|
919
|
+
image_match = re.search(r'(txharbor\.xaminim\.com/[a-zA-Z0-9_/-]+)', ci_content)
|
|
920
|
+
if image_match:
|
|
921
|
+
result['image_repo'] = image_match.group(1)
|
|
922
|
+
|
|
923
|
+
return result
|
|
924
|
+
|
|
925
|
+
def parse_makefile(self, makefile_content: str) -> Dict:
|
|
926
|
+
"""
|
|
927
|
+
解析 Makefile 内容,提取 Bedrock 相关配置
|
|
928
|
+
|
|
929
|
+
Args:
|
|
930
|
+
makefile_content: Makefile 文件内容
|
|
931
|
+
|
|
932
|
+
Returns:
|
|
933
|
+
Bedrock 配置信息
|
|
934
|
+
"""
|
|
935
|
+
import re
|
|
936
|
+
|
|
937
|
+
result = {
|
|
938
|
+
'psm': None,
|
|
939
|
+
'bedrock_app': None,
|
|
940
|
+
'image_repo': None
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
# 提取 PSM
|
|
944
|
+
psm_match = re.search(r'PSM\s*[:?]?=\s*([a-zA-Z0-9_.]+)', makefile_content)
|
|
945
|
+
if psm_match:
|
|
946
|
+
result['psm'] = psm_match.group(1)
|
|
947
|
+
|
|
948
|
+
# 提取 APP_NAME
|
|
949
|
+
app_match = re.search(r'APP_NAME\s*[:?]?=\s*([a-zA-Z0-9_-]+)', makefile_content)
|
|
950
|
+
if app_match:
|
|
951
|
+
result['bedrock_app'] = app_match.group(1)
|
|
952
|
+
|
|
953
|
+
# 提取镜像仓库
|
|
954
|
+
image_match = re.search(r'(txharbor\.xaminim\.com/[a-zA-Z0-9_/-]+)', makefile_content)
|
|
955
|
+
if image_match:
|
|
956
|
+
result['image_repo'] = image_match.group(1)
|
|
957
|
+
|
|
958
|
+
return result
|
|
959
|
+
|
|
960
|
+
def infer_from_project_name(self, project_name: str, project_path: str) -> Dict:
|
|
961
|
+
"""
|
|
962
|
+
根据项目名称推断 Bedrock 配置
|
|
963
|
+
|
|
964
|
+
Args:
|
|
965
|
+
project_name: 项目名称
|
|
966
|
+
project_path: 项目路径 (如 qa/apicase_generate_tool)
|
|
967
|
+
|
|
968
|
+
Returns:
|
|
969
|
+
推断的 Bedrock 配置
|
|
970
|
+
"""
|
|
971
|
+
import re
|
|
972
|
+
|
|
973
|
+
result = {
|
|
974
|
+
'psm_guess': None,
|
|
975
|
+
'app_guess': None,
|
|
976
|
+
'confidence': 'low'
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
# 从路径中提取 group 和 project
|
|
980
|
+
path_parts = project_path.split('/')
|
|
981
|
+
if len(path_parts) >= 2:
|
|
982
|
+
group = path_parts[-2]
|
|
983
|
+
project = path_parts[-1]
|
|
984
|
+
|
|
985
|
+
# 转换项目名为 PSM 格式 (下划线转点号)
|
|
986
|
+
# 例如: qa/apicase_generate_tool -> qa.apicase.generate_tool
|
|
987
|
+
psm_project = project.replace('-', '_').lower()
|
|
988
|
+
result['psm_guess'] = f"{group.lower()}.{psm_project}"
|
|
989
|
+
|
|
990
|
+
# 应用名通常是项目名的 kebab-case
|
|
991
|
+
result['app_guess'] = project.replace('_', '-').lower()
|
|
992
|
+
|
|
993
|
+
result['confidence'] = 'medium'
|
|
994
|
+
|
|
995
|
+
return result
|
|
996
|
+
|
|
997
|
+
def get_bedrock_info(self, project_id: int, branch: str = None) -> Dict:
|
|
998
|
+
"""
|
|
999
|
+
获取仓库对应的 Bedrock 部署信息
|
|
1000
|
+
|
|
1001
|
+
Args:
|
|
1002
|
+
project_id: GitLab 项目 ID
|
|
1003
|
+
branch: 分支名,默认使用默认分支
|
|
1004
|
+
|
|
1005
|
+
Returns:
|
|
1006
|
+
{
|
|
1007
|
+
"found": True/False,
|
|
1008
|
+
"source": "gitlab-ci" | "makefile" | "inferred",
|
|
1009
|
+
"psm": "xxx.yyy.zzz",
|
|
1010
|
+
"bedrock_project": "project-name",
|
|
1011
|
+
"bedrock_app": "app-name",
|
|
1012
|
+
"image_repo": "txharbor.xaminim.com/...",
|
|
1013
|
+
"deploy_stages": [...],
|
|
1014
|
+
"confidence": "high" | "medium" | "low",
|
|
1015
|
+
"project_info": {...}
|
|
1016
|
+
}
|
|
1017
|
+
"""
|
|
1018
|
+
logger.info(f"获取项目 {project_id} 的 Bedrock 部署信息")
|
|
1019
|
+
|
|
1020
|
+
result = {
|
|
1021
|
+
'found': False,
|
|
1022
|
+
'source': None,
|
|
1023
|
+
'psm': None,
|
|
1024
|
+
'bedrock_project': None,
|
|
1025
|
+
'bedrock_app': None,
|
|
1026
|
+
'image_repo': None,
|
|
1027
|
+
'deploy_stages': [],
|
|
1028
|
+
'confidence': 'none',
|
|
1029
|
+
'project_info': None
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
# 1. 获取项目基本信息
|
|
1033
|
+
project_info = self.get_project_info(project_id)
|
|
1034
|
+
if project_info:
|
|
1035
|
+
result['project_info'] = {
|
|
1036
|
+
'name': project_info.get('name'),
|
|
1037
|
+
'path': project_info.get('path_with_namespace'),
|
|
1038
|
+
'web_url': project_info.get('web_url'),
|
|
1039
|
+
'default_branch': project_info.get('default_branch', 'main')
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
if not branch:
|
|
1043
|
+
branch = project_info.get('default_branch', 'main')
|
|
1044
|
+
|
|
1045
|
+
# 2. 尝试读取 .gitlab-ci.yml
|
|
1046
|
+
ci_content = self.get_file_content(project_id, '.gitlab-ci.yml', branch)
|
|
1047
|
+
if ci_content:
|
|
1048
|
+
ci_info = self.parse_gitlab_ci(ci_content)
|
|
1049
|
+
|
|
1050
|
+
if ci_info.get('psm') or ci_info.get('bedrock_project') or ci_info.get('bedrock_app'):
|
|
1051
|
+
result['found'] = True
|
|
1052
|
+
result['source'] = 'gitlab-ci'
|
|
1053
|
+
result['psm'] = ci_info.get('psm')
|
|
1054
|
+
result['bedrock_project'] = ci_info.get('bedrock_project')
|
|
1055
|
+
result['bedrock_app'] = ci_info.get('bedrock_app')
|
|
1056
|
+
result['image_repo'] = ci_info.get('image_repo')
|
|
1057
|
+
result['deploy_stages'] = ci_info.get('deploy_stages', [])
|
|
1058
|
+
result['confidence'] = 'high'
|
|
1059
|
+
|
|
1060
|
+
logger.info(f"从 .gitlab-ci.yml 解析到 Bedrock 配置: {result}")
|
|
1061
|
+
return result
|
|
1062
|
+
|
|
1063
|
+
# 3. 尝试读取 Makefile
|
|
1064
|
+
makefile_content = self.get_file_content(project_id, 'Makefile', branch)
|
|
1065
|
+
if makefile_content:
|
|
1066
|
+
makefile_info = self.parse_makefile(makefile_content)
|
|
1067
|
+
|
|
1068
|
+
if makefile_info.get('psm') or makefile_info.get('bedrock_app'):
|
|
1069
|
+
result['found'] = True
|
|
1070
|
+
result['source'] = 'makefile'
|
|
1071
|
+
result['psm'] = makefile_info.get('psm')
|
|
1072
|
+
result['bedrock_app'] = makefile_info.get('bedrock_app')
|
|
1073
|
+
result['image_repo'] = makefile_info.get('image_repo')
|
|
1074
|
+
result['confidence'] = 'medium'
|
|
1075
|
+
|
|
1076
|
+
logger.info(f"从 Makefile 解析到 Bedrock 配置: {result}")
|
|
1077
|
+
return result
|
|
1078
|
+
|
|
1079
|
+
# 4. 尝试读取 deploy.yaml 或 bedrock.yaml
|
|
1080
|
+
for config_file in ['deploy.yaml', 'bedrock.yaml', '.bedrock.yaml', 'deploy/bedrock.yaml']:
|
|
1081
|
+
config_content = self.get_file_content(project_id, config_file, branch)
|
|
1082
|
+
if config_content:
|
|
1083
|
+
try:
|
|
1084
|
+
import yaml
|
|
1085
|
+
config = yaml.safe_load(config_content)
|
|
1086
|
+
if config:
|
|
1087
|
+
result['found'] = True
|
|
1088
|
+
result['source'] = config_file
|
|
1089
|
+
result['psm'] = config.get('psm') or config.get('PSM')
|
|
1090
|
+
result['bedrock_project'] = config.get('project') or config.get('bedrock_project')
|
|
1091
|
+
result['bedrock_app'] = config.get('app') or config.get('app_name')
|
|
1092
|
+
result['confidence'] = 'high'
|
|
1093
|
+
|
|
1094
|
+
logger.info(f"从 {config_file} 解析到 Bedrock 配置: {result}")
|
|
1095
|
+
return result
|
|
1096
|
+
except Exception:
|
|
1097
|
+
continue
|
|
1098
|
+
|
|
1099
|
+
# 5. 降级:根据项目名推断
|
|
1100
|
+
if project_info:
|
|
1101
|
+
inferred = self.infer_from_project_name(
|
|
1102
|
+
project_info.get('name', ''),
|
|
1103
|
+
project_info.get('path_with_namespace', '')
|
|
1104
|
+
)
|
|
1105
|
+
|
|
1106
|
+
result['found'] = True
|
|
1107
|
+
result['source'] = 'inferred'
|
|
1108
|
+
result['psm'] = inferred.get('psm_guess')
|
|
1109
|
+
result['bedrock_app'] = inferred.get('app_guess')
|
|
1110
|
+
result['confidence'] = 'low'
|
|
1111
|
+
|
|
1112
|
+
logger.info(f"通过项目名推断 Bedrock 配置: {result}")
|
|
1113
|
+
|
|
1114
|
+
return result
|
|
1115
|
+
|
|
1116
|
+
|
|
1117
|
+
def get_bedrock_info_by_repo(project_id: int, branch: str = None) -> Dict:
|
|
1118
|
+
"""
|
|
1119
|
+
获取 Git 仓库对应的 Bedrock 部署信息(供外部调用的便捷函数)
|
|
1120
|
+
|
|
1121
|
+
通过解析仓库的 .gitlab-ci.yml、Makefile 等配置文件,
|
|
1122
|
+
提取 Bedrock 部署相关信息,包括 PSM、项目名、应用名等。
|
|
1123
|
+
|
|
1124
|
+
Args:
|
|
1125
|
+
project_id: GitLab 项目 ID
|
|
1126
|
+
branch: 分支名,默认使用默认分支
|
|
1127
|
+
|
|
1128
|
+
Returns:
|
|
1129
|
+
{
|
|
1130
|
+
"found": True/False,
|
|
1131
|
+
"source": "gitlab-ci" | "makefile" | "inferred",
|
|
1132
|
+
"psm": "xxx.yyy.zzz",
|
|
1133
|
+
"bedrock_project": "project-name",
|
|
1134
|
+
"bedrock_app": "app-name",
|
|
1135
|
+
"image_repo": "txharbor.xaminim.com/...",
|
|
1136
|
+
"deploy_stages": [...],
|
|
1137
|
+
"confidence": "high" | "medium" | "low",
|
|
1138
|
+
"project_info": {...}
|
|
1139
|
+
}
|
|
1140
|
+
"""
|
|
1141
|
+
resolver = GitlabBedrockResolver()
|
|
1142
|
+
return resolver.get_bedrock_info(project_id, branch)
|
|
1143
|
+
|
|
1144
|
+
|
|
1145
|
+
if __name__ == '__main__':
|
|
1146
|
+
# 测试代码
|
|
1147
|
+
result = get_recent_branches_by_product_line("qa_group", 3)
|
|
1148
|
+
print(f"找到 {len(result)} 个新分支:")
|
|
1149
|
+
for branch in result:
|
|
1150
|
+
print(f" - {branch['project_name']}/{branch['branch_name']} ({branch['created_at']})")
|