devops-analyzer 0.1.0__tar.gz
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.
- devops_analyzer-0.1.0/LICENSE +21 -0
- devops_analyzer-0.1.0/MANIFEST.in +2 -0
- devops_analyzer-0.1.0/PKG-INFO +95 -0
- devops_analyzer-0.1.0/README.md +74 -0
- devops_analyzer-0.1.0/devops/__init__.py +3 -0
- devops_analyzer-0.1.0/devops/__main__.py +3 -0
- devops_analyzer-0.1.0/devops/analysis.py +234 -0
- devops_analyzer-0.1.0/devops/cli.py +190 -0
- devops_analyzer-0.1.0/devops/config.py +159 -0
- devops_analyzer-0.1.0/devops/config_cmd.py +88 -0
- devops_analyzer-0.1.0/devops/extract.py +215 -0
- devops_analyzer-0.1.0/devops/models/__init__.py +50 -0
- devops_analyzer-0.1.0/devops/models/base.py +16 -0
- devops_analyzer-0.1.0/devops/models/openai.py +30 -0
- devops_analyzer-0.1.0/devops/models/zhipuai.py +28 -0
- devops_analyzer-0.1.0/devops/scan/__init__.py +3 -0
- devops_analyzer-0.1.0/devops/scan/scan.cpp +121 -0
- devops_analyzer-0.1.0/devops/scan/scan.h +7 -0
- devops_analyzer-0.1.0/devops/scan/scan_bindings.cpp +15 -0
- devops_analyzer-0.1.0/devops_analyzer.egg-info/PKG-INFO +95 -0
- devops_analyzer-0.1.0/devops_analyzer.egg-info/SOURCES.txt +28 -0
- devops_analyzer-0.1.0/devops_analyzer.egg-info/dependency_links.txt +1 -0
- devops_analyzer-0.1.0/devops_analyzer.egg-info/entry_points.txt +2 -0
- devops_analyzer-0.1.0/devops_analyzer.egg-info/not-zip-safe +1 -0
- devops_analyzer-0.1.0/devops_analyzer.egg-info/requires.txt +12 -0
- devops_analyzer-0.1.0/devops_analyzer.egg-info/top_level.txt +1 -0
- devops_analyzer-0.1.0/pyproject.toml +36 -0
- devops_analyzer-0.1.0/requirements.txt +5 -0
- devops_analyzer-0.1.0/setup.cfg +4 -0
- devops_analyzer-0.1.0/setup.py +49 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ZHUHK
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: devops-analyzer
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: 扫描源码、提取代码块、大模型分析并生成 README.md
|
|
5
|
+
Author: ZHUHK
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/example/devops-analyzer
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: tree-sitter<0.22,>=0.21.0
|
|
12
|
+
Requires-Dist: tree-sitter-languages>=1.10.0
|
|
13
|
+
Requires-Dist: openai>=1.40.0
|
|
14
|
+
Provides-Extra: zhipuai
|
|
15
|
+
Requires-Dist: zhipuai>=2.0.0; extra == "zhipuai"
|
|
16
|
+
Provides-Extra: all
|
|
17
|
+
Requires-Dist: zhipuai>=2.0.0; extra == "all"
|
|
18
|
+
Provides-Extra: build
|
|
19
|
+
Requires-Dist: build>=1.2.0; extra == "build"
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# Devops Analyzer
|
|
23
|
+
|
|
24
|
+
扫描项目源码 → Tree-sitter 提取代码块 → 大模型分析 → 生成 `README.md`。
|
|
25
|
+
|
|
26
|
+
## 安装
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
cd /path/to/Devops
|
|
30
|
+
conda activate devops
|
|
31
|
+
pip install -e .
|
|
32
|
+
|
|
33
|
+
# 使用智谱 AI 时额外安装
|
|
34
|
+
pip install zhipuai
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## 首次使用:配置模型 API
|
|
38
|
+
|
|
39
|
+
**必须先配置你自己的 API Key**,再分析项目:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
devops config
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
按提示选择提供商、输入 API Key 和模型名称。配置保存在 `~/.devops/config.json`(仅当前用户可读)。
|
|
46
|
+
|
|
47
|
+
查看当前配置(Key 脱敏显示):
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
devops config --show
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## 支持的模型提供商
|
|
54
|
+
|
|
55
|
+
| 提供商 | `provider` 值 | 默认模型 | 说明 |
|
|
56
|
+
|--------|---------------|----------|------|
|
|
57
|
+
| OpenAI | `openai` | `gpt-4o-mini` | 官方或自定义 Base URL |
|
|
58
|
+
| 智谱 AI | `zhipuai` | `glm-4-flash` | 需 `pip install zhipuai` |
|
|
59
|
+
| DeepSeek | `deepseek` | `deepseek-chat` | OpenAI 兼容协议 |
|
|
60
|
+
| Moonshot | `moonshot` | `moonshot-v1-8k` | OpenAI 兼容协议 |
|
|
61
|
+
|
|
62
|
+
也可用环境变量覆盖(优先级低于配置文件):
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
export DEVOPS_PROVIDER=openai
|
|
66
|
+
export DEVOPS_API_KEY=sk-...
|
|
67
|
+
export DEVOPS_MODEL=gpt-4o-mini
|
|
68
|
+
export DEVOPS_BASE_URL=https://api.openai.com/v1 # 可选
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## 分析项目
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
devops /path/to/your/project
|
|
75
|
+
|
|
76
|
+
# 或显式子命令
|
|
77
|
+
devops analyze /path/to/your/project
|
|
78
|
+
|
|
79
|
+
# 指定输出文件
|
|
80
|
+
devops /path/to/project -o README.md
|
|
81
|
+
|
|
82
|
+
# 临时覆盖模型(不改配置文件)
|
|
83
|
+
devops /path/to/project --provider deepseek --model deepseek-chat
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## 命令一览
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
devops config # 交互式配置 API
|
|
90
|
+
devops config --show # 查看配置
|
|
91
|
+
devops <项目路径> # 分析并生成 README.md
|
|
92
|
+
devops analyze <项目路径>
|
|
93
|
+
devops --help
|
|
94
|
+
devops --version
|
|
95
|
+
```
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Devops Analyzer
|
|
2
|
+
|
|
3
|
+
扫描项目源码 → Tree-sitter 提取代码块 → 大模型分析 → 生成 `README.md`。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
cd /path/to/Devops
|
|
9
|
+
conda activate devops
|
|
10
|
+
pip install -e .
|
|
11
|
+
|
|
12
|
+
# 使用智谱 AI 时额外安装
|
|
13
|
+
pip install zhipuai
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## 首次使用:配置模型 API
|
|
17
|
+
|
|
18
|
+
**必须先配置你自己的 API Key**,再分析项目:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
devops config
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
按提示选择提供商、输入 API Key 和模型名称。配置保存在 `~/.devops/config.json`(仅当前用户可读)。
|
|
25
|
+
|
|
26
|
+
查看当前配置(Key 脱敏显示):
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
devops config --show
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## 支持的模型提供商
|
|
33
|
+
|
|
34
|
+
| 提供商 | `provider` 值 | 默认模型 | 说明 |
|
|
35
|
+
|--------|---------------|----------|------|
|
|
36
|
+
| OpenAI | `openai` | `gpt-4o-mini` | 官方或自定义 Base URL |
|
|
37
|
+
| 智谱 AI | `zhipuai` | `glm-4-flash` | 需 `pip install zhipuai` |
|
|
38
|
+
| DeepSeek | `deepseek` | `deepseek-chat` | OpenAI 兼容协议 |
|
|
39
|
+
| Moonshot | `moonshot` | `moonshot-v1-8k` | OpenAI 兼容协议 |
|
|
40
|
+
|
|
41
|
+
也可用环境变量覆盖(优先级低于配置文件):
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
export DEVOPS_PROVIDER=openai
|
|
45
|
+
export DEVOPS_API_KEY=sk-...
|
|
46
|
+
export DEVOPS_MODEL=gpt-4o-mini
|
|
47
|
+
export DEVOPS_BASE_URL=https://api.openai.com/v1 # 可选
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## 分析项目
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
devops /path/to/your/project
|
|
54
|
+
|
|
55
|
+
# 或显式子命令
|
|
56
|
+
devops analyze /path/to/your/project
|
|
57
|
+
|
|
58
|
+
# 指定输出文件
|
|
59
|
+
devops /path/to/project -o README.md
|
|
60
|
+
|
|
61
|
+
# 临时覆盖模型(不改配置文件)
|
|
62
|
+
devops /path/to/project --provider deepseek --model deepseek-chat
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## 命令一览
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
devops config # 交互式配置 API
|
|
69
|
+
devops config --show # 查看配置
|
|
70
|
+
devops <项目路径> # 分析并生成 README.md
|
|
71
|
+
devops analyze <项目路径>
|
|
72
|
+
devops --help
|
|
73
|
+
devops --version
|
|
74
|
+
```
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""调用大模型 API 分析 extract 提取的代码块。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from devops.extract import CodeBlock, extract_blocks_from_paths
|
|
11
|
+
from devops.models import BaseChatModel, get_chat_model
|
|
12
|
+
|
|
13
|
+
MAX_CODE_LINES = 80
|
|
14
|
+
MAX_CHARS_PER_REQUEST = 24_000
|
|
15
|
+
|
|
16
|
+
SYSTEM_PROMPT = """你是一名资深代码审查专家。
|
|
17
|
+
请根据给出的代码块,输出简洁、结构化的中文分析,包含:
|
|
18
|
+
1. 文件/模块职责概述
|
|
19
|
+
2. 主要函数/类的作用
|
|
20
|
+
3. 潜在问题或改进建议(如有)
|
|
21
|
+
4. 关键依赖或调用关系(如能看出)
|
|
22
|
+
|
|
23
|
+
直接输出分析正文,不要重复粘贴完整源码。"""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _truncate_code(code: str, max_lines: int = MAX_CODE_LINES) -> str:
|
|
27
|
+
lines = code.splitlines()
|
|
28
|
+
if len(lines) <= max_lines:
|
|
29
|
+
return code
|
|
30
|
+
head = "\n".join(lines[:max_lines])
|
|
31
|
+
return f"{head}\n# ... (省略 {len(lines) - max_lines} 行)"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _format_blocks_for_prompt(blocks: list[CodeBlock]) -> str:
|
|
35
|
+
parts: list[str] = []
|
|
36
|
+
for i, block in enumerate(blocks, 1):
|
|
37
|
+
title = block.name or "(anonymous)"
|
|
38
|
+
parts.append(
|
|
39
|
+
f"### 块 {i}: {block.block_type} `{title}` "
|
|
40
|
+
f"(L{block.start_line}-L{block.end_line})\n"
|
|
41
|
+
f"```{block.language}\n{_truncate_code(block.code)}\n```"
|
|
42
|
+
)
|
|
43
|
+
return "\n\n".join(parts)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _chat(model: BaseChatModel, prompt: str) -> str:
|
|
47
|
+
return model.chat(SYSTEM_PROMPT, prompt, temperature=0.2)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def analyze_blocks(
|
|
51
|
+
blocks: list[CodeBlock],
|
|
52
|
+
*,
|
|
53
|
+
provider: str | None = None,
|
|
54
|
+
api_key: str | None = None,
|
|
55
|
+
model_name: str | None = None,
|
|
56
|
+
base_url: str | None = None,
|
|
57
|
+
) -> dict[str, Any]:
|
|
58
|
+
"""按文件分批调用大模型,并生成总览。"""
|
|
59
|
+
chat_model = get_chat_model(
|
|
60
|
+
provider=provider,
|
|
61
|
+
api_key=api_key,
|
|
62
|
+
model=model_name,
|
|
63
|
+
base_url=base_url,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
if not blocks:
|
|
67
|
+
return {
|
|
68
|
+
"summary": "未提取到任何代码块,请确认目录内有支持的源码文件。",
|
|
69
|
+
"files": [],
|
|
70
|
+
"blocks_count": 0,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
by_file: dict[str, list[CodeBlock]] = defaultdict(list)
|
|
74
|
+
for block in blocks:
|
|
75
|
+
by_file[block.file_path].append(block)
|
|
76
|
+
|
|
77
|
+
per_file: list[dict[str, Any]] = []
|
|
78
|
+
file_summaries: list[str] = []
|
|
79
|
+
|
|
80
|
+
for file_path, file_blocks in sorted(by_file.items()):
|
|
81
|
+
body = _format_blocks_for_prompt(file_blocks)
|
|
82
|
+
if len(body) > MAX_CHARS_PER_REQUEST:
|
|
83
|
+
body = body[:MAX_CHARS_PER_REQUEST] + "\n\n...(内容过长已截断)"
|
|
84
|
+
|
|
85
|
+
prompt = (
|
|
86
|
+
f"文件路径: {file_path}\n"
|
|
87
|
+
f"语言: {file_blocks[0].language}\n"
|
|
88
|
+
f"代码块数量: {len(file_blocks)}\n\n"
|
|
89
|
+
f"{body}"
|
|
90
|
+
)
|
|
91
|
+
analysis = _chat(chat_model, prompt)
|
|
92
|
+
per_file.append(
|
|
93
|
+
{
|
|
94
|
+
"file_path": file_path,
|
|
95
|
+
"blocks_count": len(file_blocks),
|
|
96
|
+
"analysis": analysis,
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
file_summaries.append(f"【{file_path}】\n{analysis}")
|
|
100
|
+
|
|
101
|
+
overview_prompt = (
|
|
102
|
+
"以下是多个源文件的逐项分析,请生成一份项目级总览(中文):\n"
|
|
103
|
+
"1. 项目整体结构\n"
|
|
104
|
+
"2. 各文件之间的关系\n"
|
|
105
|
+
"3. 共性问题或架构建议\n\n"
|
|
106
|
+
+ "\n\n---\n\n".join(file_summaries)
|
|
107
|
+
)
|
|
108
|
+
if len(overview_prompt) > MAX_CHARS_PER_REQUEST:
|
|
109
|
+
overview_prompt = overview_prompt[:MAX_CHARS_PER_REQUEST] + "\n...(已截断)"
|
|
110
|
+
|
|
111
|
+
summary = (
|
|
112
|
+
_chat(chat_model, overview_prompt)
|
|
113
|
+
if len(per_file) > 1
|
|
114
|
+
else per_file[0]["analysis"]
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
"summary": summary,
|
|
119
|
+
"files": per_file,
|
|
120
|
+
"blocks_count": len(blocks),
|
|
121
|
+
"files_count": len(per_file),
|
|
122
|
+
"model_provider": chat_model.config.provider,
|
|
123
|
+
"model_name": chat_model.config.model,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def analyze_files(
|
|
128
|
+
paths: list[str],
|
|
129
|
+
*,
|
|
130
|
+
provider: str | None = None,
|
|
131
|
+
api_key: str | None = None,
|
|
132
|
+
model_name: str | None = None,
|
|
133
|
+
base_url: str | None = None,
|
|
134
|
+
) -> dict[str, Any]:
|
|
135
|
+
"""从文件路径列表提取代码块并分析。"""
|
|
136
|
+
blocks = extract_blocks_from_paths(paths)
|
|
137
|
+
return analyze_blocks(
|
|
138
|
+
blocks,
|
|
139
|
+
provider=provider,
|
|
140
|
+
api_key=api_key,
|
|
141
|
+
model_name=model_name,
|
|
142
|
+
base_url=base_url,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def analyze_folder(
|
|
147
|
+
folder: str,
|
|
148
|
+
*,
|
|
149
|
+
provider: str | None = None,
|
|
150
|
+
api_key: str | None = None,
|
|
151
|
+
model_name: str | None = None,
|
|
152
|
+
base_url: str | None = None,
|
|
153
|
+
) -> dict[str, Any]:
|
|
154
|
+
"""扫描目录、提取代码块并分析。"""
|
|
155
|
+
from devops.scan import scan_code_files
|
|
156
|
+
|
|
157
|
+
folder_path = Path(folder).resolve()
|
|
158
|
+
paths = scan_code_files(str(folder_path))
|
|
159
|
+
result = analyze_files(
|
|
160
|
+
paths,
|
|
161
|
+
provider=provider,
|
|
162
|
+
api_key=api_key,
|
|
163
|
+
model_name=model_name,
|
|
164
|
+
base_url=base_url,
|
|
165
|
+
)
|
|
166
|
+
result["folder"] = str(folder_path)
|
|
167
|
+
result["project_name"] = folder_path.name
|
|
168
|
+
result["scanned_files_count"] = len(paths)
|
|
169
|
+
result["generated_at"] = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
|
|
170
|
+
return result
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def render_readme(result: dict[str, Any]) -> str:
|
|
174
|
+
"""将分析结果渲染为 README Markdown 正文。"""
|
|
175
|
+
project = result.get("project_name") or Path(result.get("folder", ".")).name
|
|
176
|
+
folder = result.get("folder", "")
|
|
177
|
+
generated_at = result.get("generated_at", "")
|
|
178
|
+
scanned = result.get("scanned_files_count", 0)
|
|
179
|
+
blocks = result.get("blocks_count", 0)
|
|
180
|
+
files_count = result.get("files_count", 0)
|
|
181
|
+
summary = result.get("summary", "").strip()
|
|
182
|
+
per_file: list[dict[str, Any]] = result.get("files", [])
|
|
183
|
+
|
|
184
|
+
lines: list[str] = [
|
|
185
|
+
f"# {project}",
|
|
186
|
+
"",
|
|
187
|
+
"> 本文档由代码分析工具自动生成。",
|
|
188
|
+
"",
|
|
189
|
+
"## 概览",
|
|
190
|
+
"",
|
|
191
|
+
summary or "_暂无分析内容。_",
|
|
192
|
+
"",
|
|
193
|
+
"## 统计",
|
|
194
|
+
"",
|
|
195
|
+
"| 指标 | 数值 |",
|
|
196
|
+
"| --- | --- |",
|
|
197
|
+
f"| 项目路径 | `{folder}` |",
|
|
198
|
+
f"| 扫描文件数 | {scanned} |",
|
|
199
|
+
f"| 提取代码块数 | {blocks} |",
|
|
200
|
+
f"| 已分析文件数 | {files_count} |",
|
|
201
|
+
f"| 生成时间 | {generated_at} |",
|
|
202
|
+
f"| 分析模型 | {result.get('model_provider', '')} / {result.get('model_name', '')} |",
|
|
203
|
+
"",
|
|
204
|
+
]
|
|
205
|
+
|
|
206
|
+
if per_file:
|
|
207
|
+
lines.extend(["## 文件分析", ""])
|
|
208
|
+
for item in per_file:
|
|
209
|
+
rel = item.get("file_path", "")
|
|
210
|
+
if folder and rel.startswith(folder):
|
|
211
|
+
rel = rel[len(folder) :].lstrip("/\\")
|
|
212
|
+
count = item.get("blocks_count", 0)
|
|
213
|
+
analysis = (item.get("analysis") or "").strip()
|
|
214
|
+
lines.append(f"### `{rel}`")
|
|
215
|
+
lines.append("")
|
|
216
|
+
lines.append(f"- **代码块数量**: {count}")
|
|
217
|
+
lines.append("")
|
|
218
|
+
lines.append(analysis or "_无分析结果。_")
|
|
219
|
+
lines.append("")
|
|
220
|
+
|
|
221
|
+
lines.append("---")
|
|
222
|
+
lines.append("")
|
|
223
|
+
lines.append("*Generated by Devops code analyzer*")
|
|
224
|
+
lines.append("")
|
|
225
|
+
return "\n".join(lines)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def write_project_readme(folder: str, result: dict[str, Any], filename: str = "README.md") -> Path:
|
|
229
|
+
"""将 Markdown 写入目标项目目录下的 README.md。"""
|
|
230
|
+
folder_path = Path(folder).resolve()
|
|
231
|
+
output_path = folder_path / filename
|
|
232
|
+
markdown = render_readme(result)
|
|
233
|
+
output_path.write_text(markdown, encoding="utf-8")
|
|
234
|
+
return output_path
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""终端入口:devops <项目路径> | devops config"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from devops import __version__
|
|
10
|
+
from devops.analysis import analyze_folder, write_project_readme
|
|
11
|
+
from devops.config import config_exists
|
|
12
|
+
from devops.config_cmd import run_config
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
16
|
+
parser = argparse.ArgumentParser(
|
|
17
|
+
prog="devops",
|
|
18
|
+
description="扫描项目源码,提取代码块,调用大模型分析并生成 README.md",
|
|
19
|
+
)
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
"-V",
|
|
22
|
+
"--version",
|
|
23
|
+
action="version",
|
|
24
|
+
version=f"%(prog)s {__version__}",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
28
|
+
|
|
29
|
+
config_parser = subparsers.add_parser(
|
|
30
|
+
"config",
|
|
31
|
+
help="配置模型 API(首次使用前必须执行)",
|
|
32
|
+
)
|
|
33
|
+
config_parser.add_argument(
|
|
34
|
+
"-s",
|
|
35
|
+
"--show",
|
|
36
|
+
action="store_true",
|
|
37
|
+
help="查看当前配置(API Key 脱敏)",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
analyze_parser = subparsers.add_parser(
|
|
41
|
+
"analyze",
|
|
42
|
+
help="分析项目并生成 README",
|
|
43
|
+
)
|
|
44
|
+
analyze_parser.add_argument(
|
|
45
|
+
"project",
|
|
46
|
+
nargs="?",
|
|
47
|
+
help="要分析的项目目录路径",
|
|
48
|
+
)
|
|
49
|
+
analyze_parser.add_argument(
|
|
50
|
+
"-o",
|
|
51
|
+
"--output",
|
|
52
|
+
default="README.md",
|
|
53
|
+
help="输出 Markdown 文件名(默认: README.md)",
|
|
54
|
+
)
|
|
55
|
+
analyze_parser.add_argument(
|
|
56
|
+
"--provider",
|
|
57
|
+
choices=["openai", "zhipuai", "deepseek", "moonshot"],
|
|
58
|
+
help="覆盖配置文件中的模型提供商",
|
|
59
|
+
)
|
|
60
|
+
analyze_parser.add_argument(
|
|
61
|
+
"--model",
|
|
62
|
+
dest="model_name",
|
|
63
|
+
help="覆盖配置文件中的模型名称",
|
|
64
|
+
)
|
|
65
|
+
analyze_parser.add_argument(
|
|
66
|
+
"--api-key",
|
|
67
|
+
help="临时指定 API Key(不推荐,会留在 shell 历史)",
|
|
68
|
+
)
|
|
69
|
+
analyze_parser.add_argument(
|
|
70
|
+
"--base-url",
|
|
71
|
+
help="临时指定 API Base URL(OpenAI 兼容接口)",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# 兼容: devops /path/to/project(无子命令)
|
|
75
|
+
parser.add_argument(
|
|
76
|
+
"project_legacy",
|
|
77
|
+
nargs="?",
|
|
78
|
+
help=argparse.SUPPRESS,
|
|
79
|
+
)
|
|
80
|
+
parser.add_argument(
|
|
81
|
+
"-o",
|
|
82
|
+
"--output",
|
|
83
|
+
default="README.md",
|
|
84
|
+
help=argparse.SUPPRESS,
|
|
85
|
+
)
|
|
86
|
+
parser.add_argument(
|
|
87
|
+
"--provider",
|
|
88
|
+
choices=["openai", "zhipuai", "deepseek", "moonshot"],
|
|
89
|
+
help=argparse.SUPPRESS,
|
|
90
|
+
)
|
|
91
|
+
parser.add_argument(
|
|
92
|
+
"--model",
|
|
93
|
+
dest="model_name",
|
|
94
|
+
help=argparse.SUPPRESS,
|
|
95
|
+
)
|
|
96
|
+
parser.add_argument(
|
|
97
|
+
"--api-key",
|
|
98
|
+
help=argparse.SUPPRESS,
|
|
99
|
+
)
|
|
100
|
+
parser.add_argument(
|
|
101
|
+
"--base-url",
|
|
102
|
+
help=argparse.SUPPRESS,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
return parser
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _run_analyze(args: argparse.Namespace) -> int:
|
|
109
|
+
folder = getattr(args, "project", None) or getattr(args, "project_legacy", None)
|
|
110
|
+
if not folder:
|
|
111
|
+
folder = input("请输入项目文件夹路径: ").strip()
|
|
112
|
+
if not folder:
|
|
113
|
+
print("错误: 未提供项目路径", file=sys.stderr)
|
|
114
|
+
return 1
|
|
115
|
+
|
|
116
|
+
if not config_exists() and not args.api_key:
|
|
117
|
+
print(
|
|
118
|
+
"错误: 尚未配置模型 API。请先运行:\n\n"
|
|
119
|
+
" devops config\n",
|
|
120
|
+
file=sys.stderr,
|
|
121
|
+
)
|
|
122
|
+
return 1
|
|
123
|
+
|
|
124
|
+
folder_path = Path(folder).expanduser().resolve()
|
|
125
|
+
if not folder_path.is_dir():
|
|
126
|
+
print(f"错误: 不是有效目录: {folder_path}", file=sys.stderr)
|
|
127
|
+
return 1
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
from devops.config import load_config
|
|
131
|
+
|
|
132
|
+
cfg = load_config(
|
|
133
|
+
provider=args.provider,
|
|
134
|
+
api_key=args.api_key,
|
|
135
|
+
model=args.model_name,
|
|
136
|
+
base_url=args.base_url,
|
|
137
|
+
)
|
|
138
|
+
print(
|
|
139
|
+
f"正在分析: {folder_path} "
|
|
140
|
+
f"[{cfg.provider} / {cfg.model}]"
|
|
141
|
+
)
|
|
142
|
+
result = analyze_folder(
|
|
143
|
+
str(folder_path),
|
|
144
|
+
provider=args.provider,
|
|
145
|
+
api_key=args.api_key,
|
|
146
|
+
model_name=args.model_name,
|
|
147
|
+
base_url=args.base_url,
|
|
148
|
+
)
|
|
149
|
+
output = write_project_readme(
|
|
150
|
+
str(folder_path), result, filename=args.output
|
|
151
|
+
)
|
|
152
|
+
print(f"已生成: {output}")
|
|
153
|
+
return 0
|
|
154
|
+
except KeyboardInterrupt:
|
|
155
|
+
print("\n已取消", file=sys.stderr)
|
|
156
|
+
return 130
|
|
157
|
+
except Exception as exc:
|
|
158
|
+
print(f"错误: {exc}", file=sys.stderr)
|
|
159
|
+
return 1
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def main(argv: list[str] | None = None) -> int:
|
|
163
|
+
argv = list(argv) if argv is not None else sys.argv[1:]
|
|
164
|
+
|
|
165
|
+
# devops /path/to/project -> 自动当作 analyze
|
|
166
|
+
if argv and not argv[0].startswith("-") and argv[0] not in ("config", "analyze"):
|
|
167
|
+
argv = ["analyze", *argv]
|
|
168
|
+
|
|
169
|
+
parser = build_parser()
|
|
170
|
+
args = parser.parse_args(argv)
|
|
171
|
+
|
|
172
|
+
if args.command == "config":
|
|
173
|
+
extra = ["--show"] if args.show else []
|
|
174
|
+
return run_config(extra)
|
|
175
|
+
|
|
176
|
+
if args.command == "analyze" or args.project_legacy is not None:
|
|
177
|
+
return _run_analyze(args)
|
|
178
|
+
|
|
179
|
+
parser.print_help()
|
|
180
|
+
print(
|
|
181
|
+
"\n快速开始:\n"
|
|
182
|
+
" 1. devops config # 首次配置 API\n"
|
|
183
|
+
" 2. devops /path/to/project\n",
|
|
184
|
+
file=sys.stderr,
|
|
185
|
+
)
|
|
186
|
+
return 0
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
if __name__ == "__main__":
|
|
190
|
+
raise SystemExit(main())
|