mobox-cli 0.1.0__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.
- mobox/__init__.py +1 -0
- mobox/auth.py +254 -0
- mobox/client.py +98 -0
- mobox/commands/__init__.py +0 -0
- mobox/commands/deploy.py +149 -0
- mobox/commands/local.py +82 -0
- mobox/commands/manage.py +188 -0
- mobox/commands/ops.py +125 -0
- mobox/config.py +82 -0
- mobox/local/__init__.py +0 -0
- mobox/local/analyze_volumes.py +483 -0
- mobox/local/config_utils.py +632 -0
- mobox/local/detect_framework.py +1406 -0
- mobox/local/detect_project.py +388 -0
- mobox/local/pack_project.py +543 -0
- mobox/main.py +109 -0
- mobox/utils.py +52 -0
- mobox_cli-0.1.0.dist-info/METADATA +9 -0
- mobox_cli-0.1.0.dist-info/RECORD +21 -0
- mobox_cli-0.1.0.dist-info/WHEEL +4 -0
- mobox_cli-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,1406 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Framework Detector - 框架检测器
|
|
4
|
+
|
|
5
|
+
自动识别项目框架类型、构建命令、启动命令和 Node.js 版本
|
|
6
|
+
支持 MCP 特性检测和 Hybrid 混合项目识别
|
|
7
|
+
|
|
8
|
+
版本: v1.1.0
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Dict, List, Optional, Tuple
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ============= MCP 依赖配置 =============
|
|
20
|
+
MCP_DEPENDENCIES = {
|
|
21
|
+
'python': ['mcp', 'fastmcp'],
|
|
22
|
+
'nodejs': ['@modelcontextprotocol/sdk'],
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# MCP Transport 模式匹配
|
|
26
|
+
MCP_TRANSPORT_PATTERNS = {
|
|
27
|
+
'python': {
|
|
28
|
+
'sse': [
|
|
29
|
+
r'transport\s*=\s*["\']sse["\']',
|
|
30
|
+
r'transport\s*=\s*["\']http["\']',
|
|
31
|
+
r'stateless_http\s*=\s*True',
|
|
32
|
+
],
|
|
33
|
+
'stdio': [r'transport\s*=\s*["\']stdio["\']'],
|
|
34
|
+
},
|
|
35
|
+
'nodejs': {
|
|
36
|
+
'sse': [r'SSEServerTransport', r'StreamableHTTPServerTransport'],
|
|
37
|
+
'stdio': [r'StdioServerTransport'],
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Hybrid 检测时忽略的目录
|
|
42
|
+
HYBRID_IGNORE_DIRS = [
|
|
43
|
+
'node_modules', 'venv', '.venv', 'env', '.env',
|
|
44
|
+
'__pycache__', '.git', 'dist', 'build', '.next',
|
|
45
|
+
'.nuxt', 'coverage', '.pytest_cache', '.mypy_cache',
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ============= Python 支持配置 =============
|
|
50
|
+
# 支持的 Python 版本
|
|
51
|
+
SUPPORTED_PYTHON_VERSIONS = ['3.9', '3.10', '3.11', '3.12', '3.13']
|
|
52
|
+
DEFAULT_PYTHON_VERSION = '3.11'
|
|
53
|
+
|
|
54
|
+
# 需要系统依赖的库黑名单
|
|
55
|
+
SYSTEM_DEPENDENCY_LIBRARIES = {
|
|
56
|
+
'opencv-python': '需要 libGL, libgthread 等系统库 (建议用 opencv-python-headless)',
|
|
57
|
+
'opencv-contrib-python': '需要 libGL, libgthread 等系统库',
|
|
58
|
+
'mysqlclient': '需要 libmysqlclient-dev (建议用 PyMySQL)',
|
|
59
|
+
'psycopg2': '需要 libpq-dev (建议用 psycopg2-binary)',
|
|
60
|
+
'lxml': '需要 libxml2-dev, libxslt1-dev',
|
|
61
|
+
'pyodbc': '需要 unixodbc-dev',
|
|
62
|
+
'pycurl': '需要 libcurl4-openssl-dev',
|
|
63
|
+
'python-ldap': '需要 libldap2-dev, libsasl2-dev',
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ============= 框架检测规则 =============
|
|
68
|
+
# 框架检测规则(按优先级排序)
|
|
69
|
+
DETECTION_RULES = {
|
|
70
|
+
'nextjs': {
|
|
71
|
+
'dependencies': ['next'],
|
|
72
|
+
'scripts_patterns': ['next build', 'next start'],
|
|
73
|
+
'files': ['next.config.js', 'next.config.mjs', 'next.config.ts'],
|
|
74
|
+
'dirs': ['pages', 'app'],
|
|
75
|
+
'default_port': 3000,
|
|
76
|
+
'build_command': 'npm run build',
|
|
77
|
+
'start_command': 'npm start',
|
|
78
|
+
},
|
|
79
|
+
'nuxt': {
|
|
80
|
+
'dependencies': ['nuxt'],
|
|
81
|
+
'scripts_patterns': ['nuxt build', 'nuxt dev'],
|
|
82
|
+
'files': ['nuxt.config.js', 'nuxt.config.ts'],
|
|
83
|
+
'dirs': [],
|
|
84
|
+
'default_port': 3000,
|
|
85
|
+
'build_command': 'npm run build',
|
|
86
|
+
'start_command': 'npm start',
|
|
87
|
+
},
|
|
88
|
+
'vite-react': {
|
|
89
|
+
'dependencies': ['vite', 'react'],
|
|
90
|
+
'scripts_patterns': ['vite build'],
|
|
91
|
+
'files': ['vite.config.js', 'vite.config.ts'],
|
|
92
|
+
'dirs': [],
|
|
93
|
+
'default_port': 4173,
|
|
94
|
+
'build_command': 'npm run build',
|
|
95
|
+
'start_command': 'npm run preview',
|
|
96
|
+
},
|
|
97
|
+
'vite-vue': {
|
|
98
|
+
'dependencies': ['vite', 'vue'],
|
|
99
|
+
'scripts_patterns': ['vite build'],
|
|
100
|
+
'files': ['vite.config.js', 'vite.config.ts'],
|
|
101
|
+
'dirs': [],
|
|
102
|
+
'default_port': 4173,
|
|
103
|
+
'build_command': 'npm run build',
|
|
104
|
+
'start_command': 'npm run preview',
|
|
105
|
+
},
|
|
106
|
+
'remix': {
|
|
107
|
+
'dependencies': ['@remix-run/node', '@remix-run/react'],
|
|
108
|
+
'scripts_patterns': ['remix build'],
|
|
109
|
+
'files': ['remix.config.js'],
|
|
110
|
+
'dirs': ['app'],
|
|
111
|
+
'default_port': 3000,
|
|
112
|
+
'build_command': 'npm run build',
|
|
113
|
+
'start_command': 'npm start',
|
|
114
|
+
},
|
|
115
|
+
'astro': {
|
|
116
|
+
'dependencies': ['astro'],
|
|
117
|
+
'scripts_patterns': ['astro build'],
|
|
118
|
+
'files': ['astro.config.mjs', 'astro.config.js'],
|
|
119
|
+
'dirs': [],
|
|
120
|
+
'default_port': 3000,
|
|
121
|
+
'build_command': 'npm run build',
|
|
122
|
+
'start_command': 'npm run preview',
|
|
123
|
+
},
|
|
124
|
+
'sveltekit': {
|
|
125
|
+
'dependencies': ['@sveltejs/kit'],
|
|
126
|
+
'scripts_patterns': ['vite build'],
|
|
127
|
+
'files': ['svelte.config.js'],
|
|
128
|
+
'dirs': [],
|
|
129
|
+
'default_port': 5173,
|
|
130
|
+
'build_command': 'npm run build',
|
|
131
|
+
'start_command': 'npm run preview',
|
|
132
|
+
},
|
|
133
|
+
'express': {
|
|
134
|
+
'dependencies': ['express'],
|
|
135
|
+
'scripts_patterns': ['node server', 'node index', 'node app'],
|
|
136
|
+
'files': ['server.js', 'app.js', 'index.js'],
|
|
137
|
+
'dirs': [],
|
|
138
|
+
'default_port': 3000,
|
|
139
|
+
'build_command': 'npm run build',
|
|
140
|
+
'start_command': 'npm start',
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
# ============= Python 框架 =============
|
|
144
|
+
'flask': {
|
|
145
|
+
'dependencies': ['flask'],
|
|
146
|
+
'files': ['app.py', 'application.py', 'wsgi.py'],
|
|
147
|
+
'default_port': 5000,
|
|
148
|
+
'build_command': None,
|
|
149
|
+
'language': 'python',
|
|
150
|
+
},
|
|
151
|
+
'fastapi': {
|
|
152
|
+
'dependencies': ['fastapi'],
|
|
153
|
+
'files': ['main.py', 'app.py', 'api.py'],
|
|
154
|
+
'default_port': 8000,
|
|
155
|
+
'build_command': None,
|
|
156
|
+
'language': 'python',
|
|
157
|
+
},
|
|
158
|
+
'streamlit': {
|
|
159
|
+
'dependencies': ['streamlit'],
|
|
160
|
+
'files': ['app.py', 'streamlit_app.py'],
|
|
161
|
+
'default_port': 8501,
|
|
162
|
+
'build_command': None,
|
|
163
|
+
'language': 'python',
|
|
164
|
+
},
|
|
165
|
+
'mcp': {
|
|
166
|
+
'dependencies': ['mcp', 'fastmcp'],
|
|
167
|
+
'files': ['server.py', 'mcp_server.py', 'main.py'],
|
|
168
|
+
'default_port': 9192,
|
|
169
|
+
'build_command': None,
|
|
170
|
+
'language': 'python',
|
|
171
|
+
},
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class FrameworkDetector:
|
|
176
|
+
def __init__(self, project_path: str):
|
|
177
|
+
self.project_path = Path(project_path).resolve()
|
|
178
|
+
self.package_json_path = self.project_path / 'package.json'
|
|
179
|
+
self.package_json = None
|
|
180
|
+
|
|
181
|
+
def detect(self) -> Dict:
|
|
182
|
+
"""执行框架检测(支持 hybrid 模式)"""
|
|
183
|
+
|
|
184
|
+
# 0. 首先检测是否为 hybrid 项目
|
|
185
|
+
hybrid_result = self._detect_hybrid()
|
|
186
|
+
if hybrid_result:
|
|
187
|
+
return hybrid_result
|
|
188
|
+
|
|
189
|
+
# 1. 优先检查 requirements.txt (Python 项目)
|
|
190
|
+
req_file = self.project_path / 'requirements.txt'
|
|
191
|
+
if req_file.exists():
|
|
192
|
+
return self._detect_python_framework()
|
|
193
|
+
|
|
194
|
+
# 2. 检查 package.json (Node.js 项目)
|
|
195
|
+
if not self.package_json_path.exists():
|
|
196
|
+
return self._error_result(
|
|
197
|
+
"No package.json or requirements.txt found. "
|
|
198
|
+
"This doesn't appear to be a Node.js or Python project."
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# 3. 执行 Node.js 检测 (保持原有逻辑)
|
|
202
|
+
try:
|
|
203
|
+
with open(self.package_json_path, 'r', encoding='utf-8') as f:
|
|
204
|
+
self.package_json = json.load(f)
|
|
205
|
+
except Exception as e:
|
|
206
|
+
return self._error_result(f"Failed to read package.json: {e}")
|
|
207
|
+
|
|
208
|
+
# 按优先级检测 Node.js 框架
|
|
209
|
+
# 1. 分析 dependencies
|
|
210
|
+
result = self._detect_by_dependencies()
|
|
211
|
+
if result:
|
|
212
|
+
return self._success_result(result)
|
|
213
|
+
|
|
214
|
+
# 2. 分析 scripts
|
|
215
|
+
result = self._detect_by_scripts()
|
|
216
|
+
if result:
|
|
217
|
+
return self._success_result(result)
|
|
218
|
+
|
|
219
|
+
# 3. 分析项目结构
|
|
220
|
+
result = self._detect_by_structure()
|
|
221
|
+
if result:
|
|
222
|
+
return self._success_result(result)
|
|
223
|
+
|
|
224
|
+
# 无法识别
|
|
225
|
+
return self._unknown_result()
|
|
226
|
+
|
|
227
|
+
# ============= Hybrid 检测方法 =============
|
|
228
|
+
|
|
229
|
+
def _detect_hybrid(self) -> Optional[Dict]:
|
|
230
|
+
"""检测是否为 hybrid 混合项目"""
|
|
231
|
+
root_languages = self._detect_languages_in_dir(self.project_path)
|
|
232
|
+
|
|
233
|
+
# 扫描一级子目录
|
|
234
|
+
sub_components = []
|
|
235
|
+
try:
|
|
236
|
+
for item in self.project_path.iterdir():
|
|
237
|
+
if not item.is_dir():
|
|
238
|
+
continue
|
|
239
|
+
if item.name in HYBRID_IGNORE_DIRS:
|
|
240
|
+
continue
|
|
241
|
+
if item.name.startswith('.'):
|
|
242
|
+
continue
|
|
243
|
+
|
|
244
|
+
sub_languages = self._detect_languages_in_dir(item)
|
|
245
|
+
if sub_languages:
|
|
246
|
+
sub_components.append({
|
|
247
|
+
'path': f'./{item.name}',
|
|
248
|
+
'languages': sub_languages,
|
|
249
|
+
})
|
|
250
|
+
except PermissionError:
|
|
251
|
+
pass
|
|
252
|
+
|
|
253
|
+
# 判断是否为 hybrid
|
|
254
|
+
all_languages = set(root_languages)
|
|
255
|
+
for comp in sub_components:
|
|
256
|
+
all_languages.update(comp['languages'])
|
|
257
|
+
|
|
258
|
+
if len(all_languages) < 2:
|
|
259
|
+
return None # 不是 hybrid 项目
|
|
260
|
+
|
|
261
|
+
# 构建 hybrid 结果
|
|
262
|
+
return self._build_hybrid_result(root_languages, sub_components)
|
|
263
|
+
|
|
264
|
+
def _detect_languages_in_dir(self, dir_path: Path) -> List[str]:
|
|
265
|
+
"""检测目录中的语言类型"""
|
|
266
|
+
languages = []
|
|
267
|
+
|
|
268
|
+
if (dir_path / 'requirements.txt').exists():
|
|
269
|
+
languages.append('python')
|
|
270
|
+
if (dir_path / 'package.json').exists():
|
|
271
|
+
languages.append('nodejs')
|
|
272
|
+
|
|
273
|
+
return languages
|
|
274
|
+
|
|
275
|
+
def _build_hybrid_result(self, root_languages: List[str], sub_components: List[Dict]) -> Dict:
|
|
276
|
+
"""构建 hybrid 检测结果"""
|
|
277
|
+
components = []
|
|
278
|
+
all_requires = set()
|
|
279
|
+
features = []
|
|
280
|
+
|
|
281
|
+
# 处理根目录组件
|
|
282
|
+
if root_languages:
|
|
283
|
+
for lang in root_languages:
|
|
284
|
+
comp_result = self._detect_component('.', lang)
|
|
285
|
+
if comp_result:
|
|
286
|
+
comp_result['primary'] = True # 根目录组件默认为 primary
|
|
287
|
+
components.append(comp_result)
|
|
288
|
+
all_requires.add(lang)
|
|
289
|
+
|
|
290
|
+
# 检查 MCP 特性
|
|
291
|
+
if comp_result.get('mcp'):
|
|
292
|
+
if 'mcp' not in features:
|
|
293
|
+
features.append('mcp')
|
|
294
|
+
|
|
295
|
+
# 处理子目录组件
|
|
296
|
+
for sub in sub_components:
|
|
297
|
+
for lang in sub['languages']:
|
|
298
|
+
comp_result = self._detect_component(sub['path'], lang)
|
|
299
|
+
if comp_result:
|
|
300
|
+
if not components: # 如果根目录没有组件,第一个子目录组件为 primary
|
|
301
|
+
comp_result['primary'] = True
|
|
302
|
+
components.append(comp_result)
|
|
303
|
+
all_requires.add(lang)
|
|
304
|
+
|
|
305
|
+
# 检查 MCP 特性
|
|
306
|
+
if comp_result.get('mcp'):
|
|
307
|
+
if 'mcp' not in features:
|
|
308
|
+
features.append('mcp')
|
|
309
|
+
|
|
310
|
+
# 检测启动脚本
|
|
311
|
+
start_command = self._detect_start_script()
|
|
312
|
+
|
|
313
|
+
result = {
|
|
314
|
+
'success': True,
|
|
315
|
+
'framework': 'hybrid',
|
|
316
|
+
'components': components,
|
|
317
|
+
'runtime': {
|
|
318
|
+
'command': start_command,
|
|
319
|
+
'requires': sorted(list(all_requires)),
|
|
320
|
+
},
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if features:
|
|
324
|
+
result['features'] = features
|
|
325
|
+
|
|
326
|
+
return result
|
|
327
|
+
|
|
328
|
+
def _detect_component(self, path: str, language: str) -> Optional[Dict]:
|
|
329
|
+
"""检测单个组件的框架信息"""
|
|
330
|
+
if path == '.':
|
|
331
|
+
comp_path = self.project_path
|
|
332
|
+
else:
|
|
333
|
+
comp_path = self.project_path / path.lstrip('./')
|
|
334
|
+
|
|
335
|
+
if language == 'python':
|
|
336
|
+
return self._detect_python_component(comp_path, path)
|
|
337
|
+
elif language == 'nodejs':
|
|
338
|
+
return self._detect_nodejs_component(comp_path, path)
|
|
339
|
+
|
|
340
|
+
return None
|
|
341
|
+
|
|
342
|
+
def _detect_python_component(self, comp_path: Path, rel_path: str) -> Optional[Dict]:
|
|
343
|
+
"""检测 Python 组件"""
|
|
344
|
+
req_file = comp_path / 'requirements.txt'
|
|
345
|
+
if not req_file.exists():
|
|
346
|
+
return None
|
|
347
|
+
|
|
348
|
+
try:
|
|
349
|
+
req_content = req_file.read_text(encoding='utf-8').lower()
|
|
350
|
+
except Exception:
|
|
351
|
+
return None
|
|
352
|
+
|
|
353
|
+
# 检测框架
|
|
354
|
+
detected_framework = None
|
|
355
|
+
for framework, rules in DETECTION_RULES.items():
|
|
356
|
+
if rules.get('language') != 'python':
|
|
357
|
+
continue
|
|
358
|
+
|
|
359
|
+
for dep in rules['dependencies']:
|
|
360
|
+
if dep.lower() in req_content:
|
|
361
|
+
# 检查入口文件
|
|
362
|
+
for file in rules.get('files', []):
|
|
363
|
+
if (comp_path / file).exists():
|
|
364
|
+
detected_framework = framework
|
|
365
|
+
break
|
|
366
|
+
if detected_framework:
|
|
367
|
+
break
|
|
368
|
+
if detected_framework:
|
|
369
|
+
break
|
|
370
|
+
|
|
371
|
+
if not detected_framework:
|
|
372
|
+
detected_framework = 'python' # 通用 Python
|
|
373
|
+
|
|
374
|
+
port = DETECTION_RULES.get(detected_framework, {}).get('default_port', 8000)
|
|
375
|
+
|
|
376
|
+
result = {
|
|
377
|
+
'path': rel_path,
|
|
378
|
+
'language': 'python',
|
|
379
|
+
'framework': detected_framework,
|
|
380
|
+
'port': port,
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
# 检测 MCP 特性
|
|
384
|
+
mcp_info = self._detect_mcp_feature(comp_path, 'python', req_content)
|
|
385
|
+
if mcp_info:
|
|
386
|
+
result['mcp'] = mcp_info
|
|
387
|
+
if mcp_info.get('port'):
|
|
388
|
+
result['port'] = mcp_info['port']
|
|
389
|
+
|
|
390
|
+
return result
|
|
391
|
+
|
|
392
|
+
def _detect_nodejs_component(self, comp_path: Path, rel_path: str) -> Optional[Dict]:
|
|
393
|
+
"""检测 Node.js 组件"""
|
|
394
|
+
pkg_file = comp_path / 'package.json'
|
|
395
|
+
if not pkg_file.exists():
|
|
396
|
+
return None
|
|
397
|
+
|
|
398
|
+
try:
|
|
399
|
+
with open(pkg_file, 'r', encoding='utf-8') as f:
|
|
400
|
+
pkg_json = json.load(f)
|
|
401
|
+
except Exception:
|
|
402
|
+
return None
|
|
403
|
+
|
|
404
|
+
dependencies = {
|
|
405
|
+
**pkg_json.get('dependencies', {}),
|
|
406
|
+
**pkg_json.get('devDependencies', {}),
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
# 检测框架
|
|
410
|
+
detected_framework = None
|
|
411
|
+
for framework, rules in DETECTION_RULES.items():
|
|
412
|
+
if rules.get('language') == 'python':
|
|
413
|
+
continue
|
|
414
|
+
if 'dependencies' not in rules:
|
|
415
|
+
continue
|
|
416
|
+
|
|
417
|
+
if all(dep in dependencies for dep in rules['dependencies']):
|
|
418
|
+
detected_framework = framework
|
|
419
|
+
break
|
|
420
|
+
|
|
421
|
+
if not detected_framework:
|
|
422
|
+
detected_framework = 'nodejs' # 通用 Node.js
|
|
423
|
+
|
|
424
|
+
port = DETECTION_RULES.get(detected_framework, {}).get('default_port', 3000)
|
|
425
|
+
|
|
426
|
+
result = {
|
|
427
|
+
'path': rel_path,
|
|
428
|
+
'language': 'nodejs',
|
|
429
|
+
'framework': detected_framework,
|
|
430
|
+
'port': port,
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
# 检测 MCP 特性
|
|
434
|
+
mcp_info = self._detect_mcp_feature(comp_path, 'nodejs', json.dumps(dependencies))
|
|
435
|
+
if mcp_info:
|
|
436
|
+
result['mcp'] = mcp_info
|
|
437
|
+
if mcp_info.get('port'):
|
|
438
|
+
result['port'] = mcp_info['port']
|
|
439
|
+
|
|
440
|
+
return result
|
|
441
|
+
|
|
442
|
+
def _detect_mcp_feature(self, comp_path: Path, language: str, deps_content: str) -> Optional[Dict]:
|
|
443
|
+
"""检测 MCP 特性"""
|
|
444
|
+
# 检查是否有 MCP 依赖
|
|
445
|
+
has_mcp = False
|
|
446
|
+
for dep in MCP_DEPENDENCIES.get(language, []):
|
|
447
|
+
if dep.lower() in deps_content.lower():
|
|
448
|
+
has_mcp = True
|
|
449
|
+
break
|
|
450
|
+
|
|
451
|
+
if not has_mcp:
|
|
452
|
+
return None
|
|
453
|
+
|
|
454
|
+
# 检测 transport 模式
|
|
455
|
+
transport = self._detect_mcp_transport(comp_path, language)
|
|
456
|
+
port = self._detect_mcp_port(comp_path, language)
|
|
457
|
+
|
|
458
|
+
mcp_info = {
|
|
459
|
+
'transport': transport,
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if transport == 'sse' and port:
|
|
463
|
+
mcp_info['port'] = port
|
|
464
|
+
elif transport == 'stdio':
|
|
465
|
+
mcp_info['warning'] = 'stdio 模式不支持远程容器部署,需改为 SSE 模式'
|
|
466
|
+
|
|
467
|
+
return mcp_info
|
|
468
|
+
|
|
469
|
+
def _detect_mcp_transport(self, comp_path: Path, language: str) -> str:
|
|
470
|
+
"""检测 MCP transport 模式"""
|
|
471
|
+
# 查找入口文件
|
|
472
|
+
entry_files = ['server.py', 'mcp_server.py', 'main.py'] if language == 'python' else ['server.js', 'server.ts', 'index.js', 'index.ts']
|
|
473
|
+
|
|
474
|
+
for entry_file in entry_files:
|
|
475
|
+
file_path = comp_path / entry_file
|
|
476
|
+
if not file_path.exists():
|
|
477
|
+
continue
|
|
478
|
+
|
|
479
|
+
try:
|
|
480
|
+
content = file_path.read_text(encoding='utf-8')
|
|
481
|
+
|
|
482
|
+
# 检查 SSE 模式
|
|
483
|
+
for pattern in MCP_TRANSPORT_PATTERNS.get(language, {}).get('sse', []):
|
|
484
|
+
if re.search(pattern, content):
|
|
485
|
+
return 'sse'
|
|
486
|
+
|
|
487
|
+
# 检查 stdio 模式
|
|
488
|
+
for pattern in MCP_TRANSPORT_PATTERNS.get(language, {}).get('stdio', []):
|
|
489
|
+
if re.search(pattern, content):
|
|
490
|
+
return 'stdio'
|
|
491
|
+
|
|
492
|
+
except Exception:
|
|
493
|
+
continue
|
|
494
|
+
|
|
495
|
+
# 默认假设 stdio(保守估计)
|
|
496
|
+
return 'stdio'
|
|
497
|
+
|
|
498
|
+
def _detect_mcp_port(self, comp_path: Path, language: str) -> Optional[int]:
|
|
499
|
+
"""检测 MCP 端口"""
|
|
500
|
+
entry_files = ['server.py', 'mcp_server.py', 'main.py'] if language == 'python' else ['server.js', 'server.ts', 'index.js', 'index.ts']
|
|
501
|
+
|
|
502
|
+
for entry_file in entry_files:
|
|
503
|
+
file_path = comp_path / entry_file
|
|
504
|
+
if not file_path.exists():
|
|
505
|
+
continue
|
|
506
|
+
|
|
507
|
+
try:
|
|
508
|
+
content = file_path.read_text(encoding='utf-8')
|
|
509
|
+
|
|
510
|
+
# 匹配端口号
|
|
511
|
+
patterns = [
|
|
512
|
+
r'port\s*=\s*(\d+)',
|
|
513
|
+
r'port:\s*(\d+)',
|
|
514
|
+
r'PORT\s*=\s*(\d+)',
|
|
515
|
+
]
|
|
516
|
+
|
|
517
|
+
for pattern in patterns:
|
|
518
|
+
match = re.search(pattern, content)
|
|
519
|
+
if match:
|
|
520
|
+
return int(match.group(1))
|
|
521
|
+
|
|
522
|
+
except Exception:
|
|
523
|
+
continue
|
|
524
|
+
|
|
525
|
+
return DETECTION_RULES.get('mcp', {}).get('default_port', 9192)
|
|
526
|
+
|
|
527
|
+
def _detect_start_script(self) -> Optional[str]:
|
|
528
|
+
"""检测统一启动脚本"""
|
|
529
|
+
start_scripts = ['start.sh', 'run.sh', 'docker-entrypoint.sh']
|
|
530
|
+
|
|
531
|
+
for script in start_scripts:
|
|
532
|
+
if (self.project_path / script).exists():
|
|
533
|
+
return f'./{script}'
|
|
534
|
+
|
|
535
|
+
return None
|
|
536
|
+
|
|
537
|
+
# ============= 原有检测方法 =============
|
|
538
|
+
|
|
539
|
+
def _detect_by_dependencies(self) -> Optional[Dict]:
|
|
540
|
+
"""通过 dependencies 检测框架"""
|
|
541
|
+
dependencies = {
|
|
542
|
+
**self.package_json.get('dependencies', {}),
|
|
543
|
+
**self.package_json.get('devDependencies', {})
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
for framework, rules in DETECTION_RULES.items():
|
|
547
|
+
required_deps = rules['dependencies']
|
|
548
|
+
|
|
549
|
+
# 检查是否所有必需依赖都存在
|
|
550
|
+
if all(dep in dependencies for dep in required_deps):
|
|
551
|
+
return {
|
|
552
|
+
'framework': framework,
|
|
553
|
+
'confidence': 100,
|
|
554
|
+
'detection_method': 'dependencies'
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return None
|
|
558
|
+
|
|
559
|
+
def _detect_by_scripts(self) -> Optional[Dict]:
|
|
560
|
+
"""通过 scripts 检测框架"""
|
|
561
|
+
scripts = self.package_json.get('scripts', {})
|
|
562
|
+
|
|
563
|
+
for framework, rules in DETECTION_RULES.items():
|
|
564
|
+
patterns = rules['scripts_patterns']
|
|
565
|
+
|
|
566
|
+
# 检查是否有匹配的脚本模式
|
|
567
|
+
for script_value in scripts.values():
|
|
568
|
+
if any(pattern in script_value for pattern in patterns):
|
|
569
|
+
return {
|
|
570
|
+
'framework': framework,
|
|
571
|
+
'confidence': 90,
|
|
572
|
+
'detection_method': 'scripts'
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return None
|
|
576
|
+
|
|
577
|
+
def _detect_by_structure(self) -> Optional[Dict]:
|
|
578
|
+
"""通过项目结构检测框架"""
|
|
579
|
+
for framework, rules in DETECTION_RULES.items():
|
|
580
|
+
# 检查配置文件
|
|
581
|
+
for file_name in rules['files']:
|
|
582
|
+
if (self.project_path / file_name).exists():
|
|
583
|
+
return {
|
|
584
|
+
'framework': framework,
|
|
585
|
+
'confidence': 80,
|
|
586
|
+
'detection_method': 'structure'
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
# 检查目录结构
|
|
590
|
+
for dir_name in rules['dirs']:
|
|
591
|
+
if (self.project_path / dir_name).is_dir():
|
|
592
|
+
return {
|
|
593
|
+
'framework': framework,
|
|
594
|
+
'confidence': 75,
|
|
595
|
+
'detection_method': 'structure'
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return None
|
|
599
|
+
|
|
600
|
+
def _get_node_version(self) -> str:
|
|
601
|
+
"""获取推荐的 Node.js 版本"""
|
|
602
|
+
engines = self.package_json.get('engines', {})
|
|
603
|
+
node_version = engines.get('node', '')
|
|
604
|
+
|
|
605
|
+
# 解析版本号(如 ">=18.0.0" -> "18")
|
|
606
|
+
if '>=' in node_version:
|
|
607
|
+
version = node_version.split('>=')[1].split('.')[0].strip()
|
|
608
|
+
return version
|
|
609
|
+
elif '^' in node_version or '~' in node_version:
|
|
610
|
+
version = node_version[1:].split('.')[0]
|
|
611
|
+
return version
|
|
612
|
+
|
|
613
|
+
# 默认返回 20
|
|
614
|
+
return '20'
|
|
615
|
+
|
|
616
|
+
def _get_build_command(self, framework: str) -> str:
|
|
617
|
+
"""获取构建命令"""
|
|
618
|
+
scripts = self.package_json.get('scripts', {})
|
|
619
|
+
|
|
620
|
+
# 优先从 package.json 的 scripts 中获取
|
|
621
|
+
if 'build' in scripts:
|
|
622
|
+
return 'npm run build'
|
|
623
|
+
|
|
624
|
+
# 使用默认命令
|
|
625
|
+
return DETECTION_RULES[framework]['build_command']
|
|
626
|
+
|
|
627
|
+
def _get_start_command(self, framework: str) -> str:
|
|
628
|
+
"""获取启动命令"""
|
|
629
|
+
scripts = self.package_json.get('scripts', {})
|
|
630
|
+
|
|
631
|
+
# 优先从 package.json 的 scripts 中获取
|
|
632
|
+
if 'start' in scripts:
|
|
633
|
+
return 'npm start'
|
|
634
|
+
elif 'preview' in scripts and framework.startswith('vite'):
|
|
635
|
+
return 'npm run preview'
|
|
636
|
+
|
|
637
|
+
# 使用默认命令
|
|
638
|
+
return DETECTION_RULES[framework]['start_command']
|
|
639
|
+
|
|
640
|
+
def _get_package_manager(self) -> str:
|
|
641
|
+
"""检测包管理器"""
|
|
642
|
+
if (self.project_path / 'pnpm-lock.yaml').exists():
|
|
643
|
+
return 'pnpm'
|
|
644
|
+
elif (self.project_path / 'yarn.lock').exists():
|
|
645
|
+
return 'yarn'
|
|
646
|
+
else:
|
|
647
|
+
return 'npm'
|
|
648
|
+
|
|
649
|
+
def _parse_port_from_scripts(self) -> Optional[int]:
|
|
650
|
+
"""从 package.json scripts 中解析端口"""
|
|
651
|
+
scripts = self.package_json.get('scripts', {})
|
|
652
|
+
|
|
653
|
+
# 检查常见的启动脚本
|
|
654
|
+
for script_name in ['start', 'preview', 'serve', 'prod']:
|
|
655
|
+
if script_name in scripts:
|
|
656
|
+
script_value = scripts[script_name]
|
|
657
|
+
|
|
658
|
+
# 匹配 -p 或 --port 参数
|
|
659
|
+
# 例如: "next start -p 3001" 或 "vite preview --port 4173"
|
|
660
|
+
patterns = [
|
|
661
|
+
r'-p\s+(\d+)', # -p 3000
|
|
662
|
+
r'--port[=\s]+(\d+)', # --port=3000 或 --port 3000
|
|
663
|
+
r'PORT=(\d+)', # PORT=3000
|
|
664
|
+
]
|
|
665
|
+
|
|
666
|
+
for pattern in patterns:
|
|
667
|
+
match = re.search(pattern, script_value)
|
|
668
|
+
if match:
|
|
669
|
+
return int(match.group(1))
|
|
670
|
+
|
|
671
|
+
return None
|
|
672
|
+
|
|
673
|
+
def _parse_env_file(self, env_file: Path) -> Optional[int]:
|
|
674
|
+
"""解析 .env 文件中的端口配置"""
|
|
675
|
+
if not env_file.exists():
|
|
676
|
+
return None
|
|
677
|
+
|
|
678
|
+
try:
|
|
679
|
+
with open(env_file, 'r', encoding='utf-8') as f:
|
|
680
|
+
for line in f:
|
|
681
|
+
line = line.strip()
|
|
682
|
+
# 跳过注释和空行
|
|
683
|
+
if not line or line.startswith('#'):
|
|
684
|
+
continue
|
|
685
|
+
|
|
686
|
+
# 匹配 PORT=3000 或 NEXT_PUBLIC_PORT=3000 等
|
|
687
|
+
match = re.match(r'^(?:NEXT_PUBLIC_)?PORT\s*=\s*(\d+)', line)
|
|
688
|
+
if match:
|
|
689
|
+
return int(match.group(1))
|
|
690
|
+
except Exception:
|
|
691
|
+
pass
|
|
692
|
+
|
|
693
|
+
return None
|
|
694
|
+
|
|
695
|
+
def _detect_port(self, framework: str) -> int:
|
|
696
|
+
"""检测项目实际端口(按优先级)"""
|
|
697
|
+
|
|
698
|
+
# 1. 优先从 package.json scripts 中检测
|
|
699
|
+
port = self._parse_port_from_scripts()
|
|
700
|
+
if port:
|
|
701
|
+
return port
|
|
702
|
+
|
|
703
|
+
# 2. 检查环境变量文件(按优先级)
|
|
704
|
+
env_files = [
|
|
705
|
+
'.env.production.local',
|
|
706
|
+
'.env.production',
|
|
707
|
+
'.env.local',
|
|
708
|
+
'.env',
|
|
709
|
+
]
|
|
710
|
+
|
|
711
|
+
for env_file_name in env_files:
|
|
712
|
+
env_file = self.project_path / env_file_name
|
|
713
|
+
port = self._parse_env_file(env_file)
|
|
714
|
+
if port:
|
|
715
|
+
return port
|
|
716
|
+
|
|
717
|
+
# 3. 尝试解析框架配置文件(Next.js 为例)
|
|
718
|
+
if framework == 'nextjs':
|
|
719
|
+
# 检查 next.config.js/mjs/ts 中的端口配置
|
|
720
|
+
# 注意:这里简化处理,因为完整解析 JS 配置文件比较复杂
|
|
721
|
+
# 实际项目中 Next.js 端口通常在 package.json scripts 或 .env 中配置
|
|
722
|
+
pass
|
|
723
|
+
|
|
724
|
+
# 4. 使用框架默认端口
|
|
725
|
+
return DETECTION_RULES[framework]['default_port']
|
|
726
|
+
|
|
727
|
+
def _success_result(self, detection: Dict) -> Dict:
|
|
728
|
+
"""构建成功结果"""
|
|
729
|
+
framework = detection['framework']
|
|
730
|
+
language = detection.get('language', 'nodejs')
|
|
731
|
+
|
|
732
|
+
if language == 'python':
|
|
733
|
+
# Python 项目结果 (已在 _detect_python_framework 中构建)
|
|
734
|
+
return detection
|
|
735
|
+
|
|
736
|
+
# Node.js 项目结果 (保持原有逻辑)
|
|
737
|
+
result = {
|
|
738
|
+
'success': True,
|
|
739
|
+
'framework': framework,
|
|
740
|
+
'confidence': detection['confidence'],
|
|
741
|
+
'detection_method': detection['detection_method'],
|
|
742
|
+
'node_version': self._get_node_version(),
|
|
743
|
+
'build_command': self._get_build_command(framework),
|
|
744
|
+
'start_command': self._get_start_command(framework),
|
|
745
|
+
'package_manager': self._get_package_manager(),
|
|
746
|
+
'port': self._detect_port(framework),
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
# Next.js 专属: 检测 Standalone 模式
|
|
750
|
+
if framework == 'nextjs':
|
|
751
|
+
standalone_info = self._detect_standalone()
|
|
752
|
+
if standalone_info:
|
|
753
|
+
result['standalone'] = standalone_info
|
|
754
|
+
|
|
755
|
+
return result
|
|
756
|
+
|
|
757
|
+
def _unknown_result(self) -> Dict:
|
|
758
|
+
"""无法识别的结果"""
|
|
759
|
+
# 尝试从 scripts 或 .env 检测端口,如果检测不到使用默认 3000
|
|
760
|
+
port = self._parse_port_from_scripts()
|
|
761
|
+
if not port:
|
|
762
|
+
for env_file_name in ['.env.production.local', '.env.production', '.env.local', '.env']:
|
|
763
|
+
env_file = self.project_path / env_file_name
|
|
764
|
+
port = self._parse_env_file(env_file)
|
|
765
|
+
if port:
|
|
766
|
+
break
|
|
767
|
+
if not port:
|
|
768
|
+
port = 3000
|
|
769
|
+
|
|
770
|
+
return {
|
|
771
|
+
'success': True,
|
|
772
|
+
'framework': 'unknown',
|
|
773
|
+
'confidence': 0,
|
|
774
|
+
'detection_method': 'none',
|
|
775
|
+
'node_version': self._get_node_version(),
|
|
776
|
+
'build_command': self._get_build_command('express'), # 使用通用命令
|
|
777
|
+
'start_command': self._get_start_command('express'),
|
|
778
|
+
'package_manager': self._get_package_manager(),
|
|
779
|
+
'port': port,
|
|
780
|
+
'warning': 'Unable to detect framework type. Please provide build and start commands manually.'
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
def _error_result(self, message: str) -> Dict:
|
|
784
|
+
"""构建错误结果"""
|
|
785
|
+
return {
|
|
786
|
+
'success': False,
|
|
787
|
+
'error': message
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
# ============= Python 检测方法 =============
|
|
791
|
+
|
|
792
|
+
def _detect_python_framework(self) -> Dict:
|
|
793
|
+
"""检测 Python 框架"""
|
|
794
|
+
req_file = self.project_path / 'requirements.txt'
|
|
795
|
+
|
|
796
|
+
try:
|
|
797
|
+
req_content = req_file.read_text(encoding='utf-8')
|
|
798
|
+
except Exception as e:
|
|
799
|
+
return self._error_result(f"Failed to read requirements.txt: {e}")
|
|
800
|
+
|
|
801
|
+
# 1. 检查问题依赖
|
|
802
|
+
dep_warnings = self._check_problematic_dependencies(req_content)
|
|
803
|
+
|
|
804
|
+
# 2. 检测 Python 版本
|
|
805
|
+
python_version = self._detect_python_version()
|
|
806
|
+
|
|
807
|
+
# 3. 匹配框架(优先检测 MCP)
|
|
808
|
+
for framework, rules in DETECTION_RULES.items():
|
|
809
|
+
if rules.get('language') != 'python':
|
|
810
|
+
continue
|
|
811
|
+
|
|
812
|
+
# 检查依赖
|
|
813
|
+
for dep in rules['dependencies']:
|
|
814
|
+
if dep.lower() in req_content.lower():
|
|
815
|
+
# 查找入口文件
|
|
816
|
+
entry_file = None
|
|
817
|
+
for file in rules['files']:
|
|
818
|
+
if (self.project_path / file).exists():
|
|
819
|
+
entry_file = file
|
|
820
|
+
break
|
|
821
|
+
|
|
822
|
+
if not entry_file:
|
|
823
|
+
continue
|
|
824
|
+
|
|
825
|
+
# 检测端口(优先级:.env → config.py → 代码 → 默认值)
|
|
826
|
+
detected_port = self._detect_python_port(framework, entry_file)
|
|
827
|
+
|
|
828
|
+
# 检测启动命令(传入端口参数)
|
|
829
|
+
start_cmd_result = self._detect_start_command(framework, entry_file, detected_port)
|
|
830
|
+
|
|
831
|
+
# 构建环境变量 (orchestrator DeployParams 格式)
|
|
832
|
+
environment = {
|
|
833
|
+
'PYTHON_VERSION': python_version # ← Python 版本标记
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
# 合并启动命令中的环境变量
|
|
837
|
+
if start_cmd_result.get('env'):
|
|
838
|
+
environment.update(start_cmd_result['env'])
|
|
839
|
+
|
|
840
|
+
result = {
|
|
841
|
+
'success': True,
|
|
842
|
+
'framework': framework,
|
|
843
|
+
'language': 'python',
|
|
844
|
+
'python_version': python_version,
|
|
845
|
+
'entry_file': entry_file,
|
|
846
|
+
'confidence': 100,
|
|
847
|
+
'detection_method': 'dependencies',
|
|
848
|
+
'build_command': None,
|
|
849
|
+
'start_command': start_cmd_result.get('command'),
|
|
850
|
+
'port': detected_port,
|
|
851
|
+
|
|
852
|
+
# ========== orchestrator DeployParams 字段 ==========
|
|
853
|
+
'environment': environment, # ← 改名: env → environment
|
|
854
|
+
'health_check_type': 'tcp', # ← Python 使用 TCP 健康检查
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
# 检测 MCP 特性
|
|
858
|
+
mcp_info = self._detect_mcp_feature(self.project_path, 'python', req_content)
|
|
859
|
+
if mcp_info:
|
|
860
|
+
result['features'] = ['mcp']
|
|
861
|
+
result['mcp'] = mcp_info
|
|
862
|
+
# 如果是 MCP 项目且检测到端口,使用 MCP 端口
|
|
863
|
+
if mcp_info.get('port'):
|
|
864
|
+
result['port'] = mcp_info['port']
|
|
865
|
+
|
|
866
|
+
# 添加依赖警告
|
|
867
|
+
if dep_warnings:
|
|
868
|
+
result['warnings'] = dep_warnings
|
|
869
|
+
|
|
870
|
+
# 添加启动命令提示
|
|
871
|
+
if start_cmd_result.get('hint'):
|
|
872
|
+
result['start_command_hint'] = start_cmd_result['hint']
|
|
873
|
+
|
|
874
|
+
# 添加启动命令警告(例如:没有 gunicorn 的提示)
|
|
875
|
+
if start_cmd_result.get('warning'):
|
|
876
|
+
if 'warnings' not in result:
|
|
877
|
+
result['warnings'] = []
|
|
878
|
+
result['warnings'].append(start_cmd_result['warning'])
|
|
879
|
+
|
|
880
|
+
return result
|
|
881
|
+
|
|
882
|
+
# 没有识别到支持的框架
|
|
883
|
+
return self._error_result(
|
|
884
|
+
"Python project detected but framework not recognized. "
|
|
885
|
+
"Only Flask, FastAPI, Streamlit, and MCP are supported."
|
|
886
|
+
)
|
|
887
|
+
|
|
888
|
+
def _detect_python_version(self) -> str:
|
|
889
|
+
"""检测 Python 版本 (按优先级)"""
|
|
890
|
+
# 1. runtime.txt (Heroku 规范)
|
|
891
|
+
version = self._detect_from_runtime_txt()
|
|
892
|
+
if version:
|
|
893
|
+
return self._validate_python_version(version)
|
|
894
|
+
|
|
895
|
+
# 2. .python-version (pyenv)
|
|
896
|
+
version = self._detect_from_python_version()
|
|
897
|
+
if version:
|
|
898
|
+
return self._validate_python_version(version)
|
|
899
|
+
|
|
900
|
+
# 3. pyproject.toml
|
|
901
|
+
version = self._detect_from_pyproject_toml()
|
|
902
|
+
if version:
|
|
903
|
+
return self._validate_python_version(version)
|
|
904
|
+
|
|
905
|
+
# 4. 默认版本
|
|
906
|
+
return DEFAULT_PYTHON_VERSION
|
|
907
|
+
|
|
908
|
+
def _detect_from_runtime_txt(self) -> Optional[str]:
|
|
909
|
+
"""从 runtime.txt 检测 (Heroku 规范)"""
|
|
910
|
+
runtime_file = self.project_path / 'runtime.txt'
|
|
911
|
+
if not runtime_file.exists():
|
|
912
|
+
return None
|
|
913
|
+
|
|
914
|
+
try:
|
|
915
|
+
content = runtime_file.read_text().strip()
|
|
916
|
+
# 匹配 "python-3.11" 或 "python-3.11.5"
|
|
917
|
+
match = re.match(r'python-(\d+\.\d+)(?:\.\d+)?', content)
|
|
918
|
+
if match:
|
|
919
|
+
return match.group(1)
|
|
920
|
+
except Exception:
|
|
921
|
+
pass
|
|
922
|
+
|
|
923
|
+
return None
|
|
924
|
+
|
|
925
|
+
def _detect_from_python_version(self) -> Optional[str]:
|
|
926
|
+
"""从 .python-version 检测 (pyenv)"""
|
|
927
|
+
pyenv_file = self.project_path / '.python-version'
|
|
928
|
+
if not pyenv_file.exists():
|
|
929
|
+
return None
|
|
930
|
+
|
|
931
|
+
try:
|
|
932
|
+
content = pyenv_file.read_text().strip()
|
|
933
|
+
# 匹配 "3.11" 或 "3.11.5"
|
|
934
|
+
match = re.match(r'(\d+\.\d+)(?:\.\d+)?', content)
|
|
935
|
+
if match:
|
|
936
|
+
return match.group(1)
|
|
937
|
+
except Exception:
|
|
938
|
+
pass
|
|
939
|
+
|
|
940
|
+
return None
|
|
941
|
+
|
|
942
|
+
def _detect_from_pyproject_toml(self) -> Optional[str]:
|
|
943
|
+
"""从 pyproject.toml 检测"""
|
|
944
|
+
pyproject_file = self.project_path / 'pyproject.toml'
|
|
945
|
+
if not pyproject_file.exists():
|
|
946
|
+
return None
|
|
947
|
+
|
|
948
|
+
try:
|
|
949
|
+
# 尝试导入 tomllib (Python 3.11+) 或 tomli
|
|
950
|
+
try:
|
|
951
|
+
import tomllib
|
|
952
|
+
except ImportError:
|
|
953
|
+
try:
|
|
954
|
+
import tomli as tomllib
|
|
955
|
+
except ImportError:
|
|
956
|
+
return None # 无法解析 TOML
|
|
957
|
+
|
|
958
|
+
content = pyproject_file.read_text()
|
|
959
|
+
data = tomllib.loads(content)
|
|
960
|
+
|
|
961
|
+
# 检查 [project] requires-python
|
|
962
|
+
if 'project' in data:
|
|
963
|
+
req = data['project'].get('requires-python', '')
|
|
964
|
+
match = re.search(r'(\d+\.\d+)', req)
|
|
965
|
+
if match:
|
|
966
|
+
return match.group(1)
|
|
967
|
+
|
|
968
|
+
# 检查 [tool.poetry] python 依赖
|
|
969
|
+
if 'tool' in data and 'poetry' in data['tool']:
|
|
970
|
+
deps = data['tool']['poetry'].get('dependencies', {})
|
|
971
|
+
python_dep = deps.get('python', '')
|
|
972
|
+
match = re.search(r'(\d+\.\d+)', str(python_dep))
|
|
973
|
+
if match:
|
|
974
|
+
return match.group(1)
|
|
975
|
+
|
|
976
|
+
except Exception:
|
|
977
|
+
pass
|
|
978
|
+
|
|
979
|
+
return None
|
|
980
|
+
|
|
981
|
+
def _validate_python_version(self, version: str) -> str:
|
|
982
|
+
"""验证 Python 版本是否支持"""
|
|
983
|
+
if version in SUPPORTED_PYTHON_VERSIONS:
|
|
984
|
+
return version
|
|
985
|
+
|
|
986
|
+
# 版本不支持,输出警告并使用默认版本
|
|
987
|
+
print(f"⚠️ Detected Python {version}, but only {SUPPORTED_PYTHON_VERSIONS} are supported.", file=sys.stderr)
|
|
988
|
+
print(f" Using default version {DEFAULT_PYTHON_VERSION}", file=sys.stderr)
|
|
989
|
+
return DEFAULT_PYTHON_VERSION
|
|
990
|
+
|
|
991
|
+
def _check_problematic_dependencies(self, req_content: str) -> List[str]:
|
|
992
|
+
"""检查需要系统依赖的库"""
|
|
993
|
+
warnings = []
|
|
994
|
+
|
|
995
|
+
for line in req_content.lower().split('\n'):
|
|
996
|
+
line = line.strip()
|
|
997
|
+
if not line or line.startswith('#'):
|
|
998
|
+
continue
|
|
999
|
+
|
|
1000
|
+
# 提取包名 (处理 package==1.0.0, package>=1.0 等格式)
|
|
1001
|
+
package = re.split(r'[=<>!]', line)[0].strip()
|
|
1002
|
+
|
|
1003
|
+
if package in SYSTEM_DEPENDENCY_LIBRARIES:
|
|
1004
|
+
reason = SYSTEM_DEPENDENCY_LIBRARIES[package]
|
|
1005
|
+
warnings.append(f"⚠️ {package}: {reason}")
|
|
1006
|
+
|
|
1007
|
+
return warnings
|
|
1008
|
+
|
|
1009
|
+
def _detect_start_command(self, framework: str, entry_file: str, port: int) -> Dict:
|
|
1010
|
+
"""检测启动命令"""
|
|
1011
|
+
if framework == 'flask':
|
|
1012
|
+
return self._detect_flask_start_command(entry_file, port)
|
|
1013
|
+
elif framework == 'fastapi':
|
|
1014
|
+
return self._detect_fastapi_start_command(entry_file, port)
|
|
1015
|
+
elif framework == 'streamlit':
|
|
1016
|
+
return self._detect_streamlit_start_command(entry_file, port)
|
|
1017
|
+
elif framework == 'mcp':
|
|
1018
|
+
return self._detect_mcp_start_command(entry_file, port)
|
|
1019
|
+
|
|
1020
|
+
return {'command': None, 'hint': 'Unknown framework'}
|
|
1021
|
+
|
|
1022
|
+
def _detect_flask_start_command(self, entry_file: str, port: int) -> Dict:
|
|
1023
|
+
"""检测 Flask 启动命令"""
|
|
1024
|
+
file_path = self.project_path / entry_file
|
|
1025
|
+
req_file = self.project_path / 'requirements.txt'
|
|
1026
|
+
|
|
1027
|
+
# 检查是否安装了 gunicorn
|
|
1028
|
+
has_gunicorn = False
|
|
1029
|
+
if req_file.exists():
|
|
1030
|
+
try:
|
|
1031
|
+
req_content = req_file.read_text(encoding='utf-8').lower()
|
|
1032
|
+
has_gunicorn = 'gunicorn' in req_content
|
|
1033
|
+
except Exception:
|
|
1034
|
+
pass
|
|
1035
|
+
|
|
1036
|
+
try:
|
|
1037
|
+
content = file_path.read_text(encoding='utf-8')
|
|
1038
|
+
except Exception:
|
|
1039
|
+
return {
|
|
1040
|
+
'command': None,
|
|
1041
|
+
'hint': f"Unable to read {entry_file}. Please manually specify the start command."
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
# 查找 Flask app 实例名称
|
|
1045
|
+
match = re.search(r'(\w+)\s*=\s*Flask\s*\(', content)
|
|
1046
|
+
app_var = match.group(1) if match else 'app'
|
|
1047
|
+
module = entry_file.replace('.py', '')
|
|
1048
|
+
|
|
1049
|
+
# 方式 1: if __name__ == '__main__': app.run()
|
|
1050
|
+
# 这种方式通常在开发时使用,保持原样(代码内部控制端口)
|
|
1051
|
+
if "if __name__ == '__main__':" in content and 'app.run()' in content:
|
|
1052
|
+
return {'command': f"python {entry_file}"}
|
|
1053
|
+
|
|
1054
|
+
# 方式 2: 优先使用 gunicorn (生产环境)
|
|
1055
|
+
if has_gunicorn and match:
|
|
1056
|
+
return {
|
|
1057
|
+
'command': f'gunicorn -b 0.0.0.0:{port} {module}:{app_var}',
|
|
1058
|
+
'env': {}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
# 方式 3: 降级到 flask run (开发环境)
|
|
1062
|
+
if match:
|
|
1063
|
+
result = {
|
|
1064
|
+
'command': f'flask run --host=0.0.0.0 --port={port}',
|
|
1065
|
+
'env': {'FLASK_APP': entry_file}
|
|
1066
|
+
}
|
|
1067
|
+
# 如果没有 gunicorn,给出警告
|
|
1068
|
+
if not has_gunicorn:
|
|
1069
|
+
result['warning'] = '⚠️ Using Flask development server. For production, add "gunicorn" to requirements.txt'
|
|
1070
|
+
return result
|
|
1071
|
+
|
|
1072
|
+
# 无法检测
|
|
1073
|
+
return {
|
|
1074
|
+
'command': None,
|
|
1075
|
+
'hint': (
|
|
1076
|
+
f"Unable to auto-detect Flask startup command. Common methods:\n"
|
|
1077
|
+
f" - python {entry_file} (if you have 'if __name__ == \"__main__\"')\n"
|
|
1078
|
+
f" - flask run --host=0.0.0.0 --port={port} (requires setting FLASK_APP={entry_file})\n"
|
|
1079
|
+
f" - gunicorn -b 0.0.0.0:{port} {module}:app (production environment)"
|
|
1080
|
+
)
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
def _detect_fastapi_start_command(self, entry_file: str, port: int) -> Dict:
|
|
1084
|
+
"""检测 FastAPI 启动命令"""
|
|
1085
|
+
file_path = self.project_path / entry_file
|
|
1086
|
+
|
|
1087
|
+
try:
|
|
1088
|
+
content = file_path.read_text(encoding='utf-8')
|
|
1089
|
+
except Exception:
|
|
1090
|
+
return {
|
|
1091
|
+
'command': None,
|
|
1092
|
+
'hint': f"Unable to read {entry_file}. Please manually specify the start command."
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
# 查找 app = FastAPI() 的变量名
|
|
1096
|
+
match = re.search(r'(\w+)\s*=\s*FastAPI\s*\(', content)
|
|
1097
|
+
if match:
|
|
1098
|
+
app_var = match.group(1)
|
|
1099
|
+
module = entry_file.replace('.py', '')
|
|
1100
|
+
return {
|
|
1101
|
+
'command': f"uvicorn {module}:{app_var} --host 0.0.0.0 --port {port}"
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
# 无法检测
|
|
1105
|
+
module = entry_file.replace('.py', '')
|
|
1106
|
+
return {
|
|
1107
|
+
'command': None,
|
|
1108
|
+
'hint': (
|
|
1109
|
+
f"Unable to auto-detect FastAPI application instance name. Please provide start command:\n"
|
|
1110
|
+
f" Example: uvicorn {module}:app --host 0.0.0.0 --port {port}"
|
|
1111
|
+
)
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
def _detect_streamlit_start_command(self, entry_file: str, port: int) -> Dict:
|
|
1115
|
+
"""检测 Streamlit 启动命令"""
|
|
1116
|
+
# Streamlit 格式固定,使用检测到的端口
|
|
1117
|
+
return {
|
|
1118
|
+
'command': f"streamlit run {entry_file} --server.port {port} --server.address 0.0.0.0"
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
def _detect_mcp_start_command(self, entry_file: str, port: int) -> Dict:
|
|
1122
|
+
"""检测 MCP Server 启动命令"""
|
|
1123
|
+
# MCP Server 直接运行 Python 脚本
|
|
1124
|
+
return {
|
|
1125
|
+
'command': f"python {entry_file}"
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
# ============= Python 端口检测方法 =============
|
|
1129
|
+
|
|
1130
|
+
def _detect_python_port(self, framework: str, entry_file: str) -> int:
|
|
1131
|
+
"""
|
|
1132
|
+
检测 Python 项目端口(按优先级)
|
|
1133
|
+
|
|
1134
|
+
优先级:
|
|
1135
|
+
1. .env 文件中的框架特定变量 (FLASK_PORT=3004)
|
|
1136
|
+
2. .env 文件中的通用变量 (PORT=3004)
|
|
1137
|
+
3. config.py 中的默认值 (FLASK_PORT = int(os.getenv('FLASK_PORT', 6101)))
|
|
1138
|
+
4. app.py 中的硬编码值 (app.run(port=8080))
|
|
1139
|
+
5. 框架默认值 (5000)
|
|
1140
|
+
"""
|
|
1141
|
+
|
|
1142
|
+
# 1. 从 .env 文件检测
|
|
1143
|
+
env_files = ['.env.production', '.env.local', '.env']
|
|
1144
|
+
for env_file_name in env_files:
|
|
1145
|
+
env_file = self.project_path / env_file_name
|
|
1146
|
+
port = self._parse_python_env_port(env_file, framework)
|
|
1147
|
+
if port:
|
|
1148
|
+
return port
|
|
1149
|
+
|
|
1150
|
+
# 2. 从 config.py 检测默认值
|
|
1151
|
+
port = self._parse_config_py_port(framework)
|
|
1152
|
+
if port:
|
|
1153
|
+
return port
|
|
1154
|
+
|
|
1155
|
+
# 3. 从入口文件检测硬编码值
|
|
1156
|
+
port = self._parse_port_from_python_code(entry_file, framework)
|
|
1157
|
+
if port:
|
|
1158
|
+
return port
|
|
1159
|
+
|
|
1160
|
+
# 4. 使用框架默认端口
|
|
1161
|
+
return DETECTION_RULES[framework]['default_port']
|
|
1162
|
+
|
|
1163
|
+
def _parse_python_env_port(self, env_file: Path, framework: str) -> Optional[int]:
|
|
1164
|
+
"""解析 .env 文件中的 Python 端口"""
|
|
1165
|
+
if not env_file.exists():
|
|
1166
|
+
return None
|
|
1167
|
+
|
|
1168
|
+
try:
|
|
1169
|
+
with open(env_file, 'r', encoding='utf-8') as f:
|
|
1170
|
+
for line in f:
|
|
1171
|
+
line = line.strip()
|
|
1172
|
+
if not line or line.startswith('#'):
|
|
1173
|
+
continue
|
|
1174
|
+
|
|
1175
|
+
# 优先匹配框架特定变量
|
|
1176
|
+
framework_patterns = {
|
|
1177
|
+
'flask': r'^FLASK_PORT\s*=\s*(\d+)',
|
|
1178
|
+
'fastapi': r'^(?:FASTAPI_|UVICORN_)?PORT\s*=\s*(\d+)',
|
|
1179
|
+
'streamlit': r'^STREAMLIT_(?:SERVER_)?PORT\s*=\s*(\d+)',
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
pattern = framework_patterns.get(framework)
|
|
1183
|
+
if pattern:
|
|
1184
|
+
match = re.match(pattern, line)
|
|
1185
|
+
if match:
|
|
1186
|
+
return int(match.group(1))
|
|
1187
|
+
|
|
1188
|
+
# 回退:匹配通用 PORT 变量
|
|
1189
|
+
match = re.match(r'^PORT\s*=\s*(\d+)', line)
|
|
1190
|
+
if match:
|
|
1191
|
+
return int(match.group(1))
|
|
1192
|
+
except Exception:
|
|
1193
|
+
pass
|
|
1194
|
+
|
|
1195
|
+
return None
|
|
1196
|
+
|
|
1197
|
+
def _parse_config_py_port(self, framework: str) -> Optional[int]:
|
|
1198
|
+
"""解析 config.py 中的端口默认值"""
|
|
1199
|
+
config_file = self.project_path / 'config.py'
|
|
1200
|
+
if not config_file.exists():
|
|
1201
|
+
return None
|
|
1202
|
+
|
|
1203
|
+
try:
|
|
1204
|
+
content = config_file.read_text(encoding='utf-8')
|
|
1205
|
+
|
|
1206
|
+
# 匹配 os.getenv() 模式: FLASK_PORT = int(os.getenv('FLASK_PORT', 6101))
|
|
1207
|
+
getenv_patterns = {
|
|
1208
|
+
'flask': r'FLASK_PORT\s*=\s*int\s*\(\s*os\.getenv\s*\([^,]+,\s*(\d+)\s*\)\s*\)',
|
|
1209
|
+
'fastapi': r'(?:FASTAPI_|UVICORN_)?PORT\s*=\s*int\s*\(\s*os\.getenv\s*\([^,]+,\s*(\d+)\s*\)\s*\)',
|
|
1210
|
+
'streamlit': r'STREAMLIT_PORT\s*=\s*int\s*\(\s*os\.getenv\s*\([^,]+,\s*(\d+)\s*\)\s*\)',
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
pattern = getenv_patterns.get(framework)
|
|
1214
|
+
if pattern:
|
|
1215
|
+
match = re.search(pattern, content)
|
|
1216
|
+
if match:
|
|
1217
|
+
return int(match.group(1))
|
|
1218
|
+
|
|
1219
|
+
# 匹配直接赋值模式: FLASK_PORT = 6101
|
|
1220
|
+
direct_patterns = {
|
|
1221
|
+
'flask': r'FLASK_PORT\s*=\s*(\d+)',
|
|
1222
|
+
'fastapi': r'(?:FASTAPI_|UVICORN_)?PORT\s*=\s*(\d+)',
|
|
1223
|
+
'streamlit': r'STREAMLIT_PORT\s*=\s*(\d+)',
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
pattern = direct_patterns.get(framework)
|
|
1227
|
+
if pattern:
|
|
1228
|
+
match = re.search(pattern, content)
|
|
1229
|
+
if match:
|
|
1230
|
+
return int(match.group(1))
|
|
1231
|
+
|
|
1232
|
+
except Exception:
|
|
1233
|
+
pass
|
|
1234
|
+
|
|
1235
|
+
return None
|
|
1236
|
+
|
|
1237
|
+
def _parse_port_from_python_code(self, entry_file: str, framework: str) -> Optional[int]:
|
|
1238
|
+
"""从 Python 代码中解析端口配置"""
|
|
1239
|
+
file_path = self.project_path / entry_file
|
|
1240
|
+
|
|
1241
|
+
try:
|
|
1242
|
+
content = file_path.read_text(encoding='utf-8')
|
|
1243
|
+
|
|
1244
|
+
if framework == 'flask':
|
|
1245
|
+
# 匹配 app.run(port=5000) 或 app.run(host='0.0.0.0', port=5000)
|
|
1246
|
+
match = re.search(r'app\.run\s*\([^)]*port\s*=\s*(\d+)', content)
|
|
1247
|
+
if match:
|
|
1248
|
+
return int(match.group(1))
|
|
1249
|
+
|
|
1250
|
+
elif framework == 'fastapi':
|
|
1251
|
+
# 匹配 uvicorn.run("main:app", port=8000)
|
|
1252
|
+
match = re.search(r'uvicorn\.run\s*\([^)]*port\s*=\s*(\d+)', content)
|
|
1253
|
+
if match:
|
|
1254
|
+
return int(match.group(1))
|
|
1255
|
+
|
|
1256
|
+
elif framework == 'streamlit':
|
|
1257
|
+
# Streamlit 端口通常在 .streamlit/config.toml 中
|
|
1258
|
+
config_file = self.project_path / '.streamlit' / 'config.toml'
|
|
1259
|
+
if config_file.exists():
|
|
1260
|
+
config_content = config_file.read_text()
|
|
1261
|
+
match = re.search(r'port\s*=\s*(\d+)', config_content)
|
|
1262
|
+
if match:
|
|
1263
|
+
return int(match.group(1))
|
|
1264
|
+
|
|
1265
|
+
except Exception:
|
|
1266
|
+
pass
|
|
1267
|
+
|
|
1268
|
+
return None
|
|
1269
|
+
|
|
1270
|
+
# ============= Next.js Standalone 检测方法 =============
|
|
1271
|
+
|
|
1272
|
+
def _detect_standalone(self) -> Optional[Dict]:
|
|
1273
|
+
"""
|
|
1274
|
+
检测 Next.js Standalone 模式配置
|
|
1275
|
+
|
|
1276
|
+
返回:
|
|
1277
|
+
{
|
|
1278
|
+
'enabled': bool, # 是否已启用 standalone
|
|
1279
|
+
'compatible': bool, # Next.js 版本是否兼容 (>=12.0)
|
|
1280
|
+
'configFile': str, # 配置文件路径
|
|
1281
|
+
'hasOutputConfig': bool # 是否已有 output 配置(任何值)
|
|
1282
|
+
}
|
|
1283
|
+
"""
|
|
1284
|
+
# 1. 查找 next.config 文件
|
|
1285
|
+
config_files = ['next.config.js', 'next.config.mjs', 'next.config.ts']
|
|
1286
|
+
config_file = None
|
|
1287
|
+
config_content = None
|
|
1288
|
+
|
|
1289
|
+
for filename in config_files:
|
|
1290
|
+
file_path = self.project_path / filename
|
|
1291
|
+
if file_path.exists():
|
|
1292
|
+
config_file = filename
|
|
1293
|
+
try:
|
|
1294
|
+
config_content = file_path.read_text(encoding='utf-8')
|
|
1295
|
+
except Exception:
|
|
1296
|
+
pass
|
|
1297
|
+
break
|
|
1298
|
+
|
|
1299
|
+
# 2. 检测 Next.js 版本兼容性 (>=12.0)
|
|
1300
|
+
compatible = self._check_nextjs_version_compatible()
|
|
1301
|
+
|
|
1302
|
+
# 3. 检测是否有自定义 server(不适用 Standalone)
|
|
1303
|
+
has_custom_server = (
|
|
1304
|
+
(self.project_path / 'server.js').exists() or
|
|
1305
|
+
(self.project_path / 'server.ts').exists()
|
|
1306
|
+
)
|
|
1307
|
+
|
|
1308
|
+
# 4. 解析配置文件
|
|
1309
|
+
enabled = False
|
|
1310
|
+
has_output_config = False
|
|
1311
|
+
|
|
1312
|
+
if config_content:
|
|
1313
|
+
# 检测 output: 'standalone'
|
|
1314
|
+
if re.search(r"output\s*:\s*['\"]standalone['\"]", config_content):
|
|
1315
|
+
enabled = True
|
|
1316
|
+
has_output_config = True
|
|
1317
|
+
# 检测其他 output 配置 (如 'export')
|
|
1318
|
+
elif re.search(r"output\s*:\s*['\"](\w+)['\"]", config_content):
|
|
1319
|
+
has_output_config = True
|
|
1320
|
+
|
|
1321
|
+
# 5. 构建返回结果
|
|
1322
|
+
result = {
|
|
1323
|
+
'enabled': enabled,
|
|
1324
|
+
'compatible': compatible,
|
|
1325
|
+
'configFile': config_file,
|
|
1326
|
+
'hasOutputConfig': has_output_config,
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
# 6. 判断是否应该建议启用
|
|
1330
|
+
# 条件: 未启用 + 版本兼容 + 无其他 output 配置 + 无自定义 server
|
|
1331
|
+
should_suggest = (
|
|
1332
|
+
not enabled and
|
|
1333
|
+
compatible and
|
|
1334
|
+
not has_output_config and
|
|
1335
|
+
not has_custom_server
|
|
1336
|
+
)
|
|
1337
|
+
result['shouldSuggest'] = should_suggest
|
|
1338
|
+
|
|
1339
|
+
if has_custom_server:
|
|
1340
|
+
result['skipReason'] = 'custom_server'
|
|
1341
|
+
elif not compatible:
|
|
1342
|
+
result['skipReason'] = 'version_incompatible'
|
|
1343
|
+
elif has_output_config and not enabled:
|
|
1344
|
+
result['skipReason'] = 'other_output_mode'
|
|
1345
|
+
|
|
1346
|
+
return result
|
|
1347
|
+
|
|
1348
|
+
def _check_nextjs_version_compatible(self) -> bool:
|
|
1349
|
+
"""检查 Next.js 版本是否支持 Standalone (>=12.0)"""
|
|
1350
|
+
if not self.package_json:
|
|
1351
|
+
return False
|
|
1352
|
+
|
|
1353
|
+
dependencies = {
|
|
1354
|
+
**self.package_json.get('dependencies', {}),
|
|
1355
|
+
**self.package_json.get('devDependencies', {})
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
next_version = dependencies.get('next', '')
|
|
1359
|
+
|
|
1360
|
+
# 处理各种版本格式
|
|
1361
|
+
# ^14.0.0, ~13.5.0, >=12.0.0, 14.0.0, latest, canary
|
|
1362
|
+
if not next_version or next_version in ['latest', 'canary', 'next']:
|
|
1363
|
+
# 无法确定版本,保守返回 False
|
|
1364
|
+
return False
|
|
1365
|
+
|
|
1366
|
+
# 提取主版本号
|
|
1367
|
+
match = re.search(r'(\d+)', next_version)
|
|
1368
|
+
if match:
|
|
1369
|
+
major_version = int(match.group(1))
|
|
1370
|
+
return major_version >= 12
|
|
1371
|
+
|
|
1372
|
+
return False
|
|
1373
|
+
|
|
1374
|
+
|
|
1375
|
+
def main():
|
|
1376
|
+
if len(sys.argv) < 2:
|
|
1377
|
+
print(json.dumps({
|
|
1378
|
+
'success': False,
|
|
1379
|
+
'error': 'Usage: detect_framework.py <project_path>'
|
|
1380
|
+
}, indent=2))
|
|
1381
|
+
sys.exit(1)
|
|
1382
|
+
|
|
1383
|
+
project_path = sys.argv[1]
|
|
1384
|
+
|
|
1385
|
+
# 检查路径是否存在
|
|
1386
|
+
if not os.path.exists(project_path):
|
|
1387
|
+
print(json.dumps({
|
|
1388
|
+
'success': False,
|
|
1389
|
+
'error': f'Project path does not exist: {project_path}'
|
|
1390
|
+
}, indent=2))
|
|
1391
|
+
sys.exit(1)
|
|
1392
|
+
|
|
1393
|
+
# 执行检测
|
|
1394
|
+
detector = FrameworkDetector(project_path)
|
|
1395
|
+
result = detector.detect()
|
|
1396
|
+
|
|
1397
|
+
# 输出 JSON 结果
|
|
1398
|
+
print(json.dumps(result, indent=2))
|
|
1399
|
+
|
|
1400
|
+
# 如果检测失败,返回非零退出码
|
|
1401
|
+
if not result.get('success', False):
|
|
1402
|
+
sys.exit(1)
|
|
1403
|
+
|
|
1404
|
+
|
|
1405
|
+
if __name__ == '__main__':
|
|
1406
|
+
main()
|