step half finish

This commit is contained in:
gongwenxin
2025-06-05 15:17:51 +08:00
parent e23f2856d6
commit 7333cc8a2a
58 changed files with 11351 additions and 4788 deletions
+96
View File
@@ -0,0 +1,96 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API 测试工具</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<h1>API 合规性测试</h1>
<div class="configuration-section">
<h2>测试配置</h2>
<form id="test-config-form">
<div class="form-group">
<label for="base_url">API 基础URL (必填):</label>
<input type="text" id="base_url" name="base_url" required placeholder="例如:http://localhost:8080/api/v1">
</div>
<fieldset>
<legend>API 定义源 (选择一个)</legend>
<div class="form-group">
<label for="yapi_file_path">YAPI 文件路径:</label>
<input type="text" id="yapi_file_path" name="yapi_file_path" placeholder="例如:./assets/doc/yapi_spec.json">
<button type="button" class="action-button" onclick="fetchYapiCategories()">加载分类</button>
<div id="yapi-categories-container" class="categories-tags-container"></div>
</div>
<div class="form-group">
<label for="swagger_file_path">Swagger/OpenAPI 文件路径:</label>
<input type="text" id="swagger_file_path" name="swagger_file_path" placeholder="例如:./assets/doc/swagger_spec.json">
<button type="button" class="action-button" onclick="fetchSwaggerTags()">加载标签</button>
<div id="swagger-tags-container" class="categories-tags-container"></div>
</div>
</fieldset>
<div class="form-group">
<label for="custom_test_cases_dir">自定义测试用例目录:</label>
<input type="text" id="custom_test_cases_dir" name="custom_test_cases_dir" placeholder="例如:./custom_testcases">
</div>
<div class="form-group">
<label for="scenarios_dir">自定义场景目录:</label>
<input type="text" id="scenarios_dir" name="scenarios_dir" placeholder="例如:./custom_scenarios">
</div>
<div class="form-group">
<label for="output_dir">报告输出目录:</label>
<input type="text" id="output_dir" name="output_dir" placeholder="例如:./test_reports">
</div>
<fieldset>
<legend>LLM 配置 (可选)</legend>
<div class="form-group">
<label for="llm_api_key">LLM API Key:</label>
<input type="password" id="llm_api_key" name="llm_api_key" placeholder="留空则尝试读取环境变量">
</div>
<div class="form-group">
<label for="llm_base_url">LLM Base URL:</label>
<input type="text" id="llm_base_url" name="llm_base_url" placeholder="例如:https://dashscope.aliyuncs.com/compatible-mode/v1">
</div>
<div class="form-group">
<label for="llm_model_name">LLM 模型名称:</label>
<input type="text" id="llm_model_name" name="llm_model_name" placeholder="例如:qwen-plus">
</div>
<div class="form-group checkbox-group">
<input type="checkbox" id="use_llm_for_request_body" name="use_llm_for_request_body">
<label for="use_llm_for_request_body">使用LLM生成请求体</label>
</div>
<div class="form-group checkbox-group">
<input type="checkbox" id="use_llm_for_path_params" name="use_llm_for_path_params">
<label for="use_llm_for_path_params">使用LLM生成路径参数</label>
</div>
<div class="form-group checkbox-group">
<input type="checkbox" id="use_llm_for_query_params" name="use_llm_for_query_params">
<label for="use_llm_for_query_params">使用LLM生成查询参数</label>
</div>
<div class="form-group checkbox-group">
<input type="checkbox" id="use_llm_for_headers" name="use_llm_for_headers">
<label for="use_llm_for_headers">使用LLM生成头部参数</label>
</div>
</fieldset>
<button type="submit" class="submit-button">运行测试</button>
</form>
</div>
<div class="results-section">
<h2>测试状态与结果</h2>
<div id="status-area">等待配置并运行测试...</div>
<pre id="results-output"></pre>
<div id="report-link-area"></div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
+140
View File
@@ -0,0 +1,140 @@
document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('test-config-form');
const statusArea = document.getElementById('status-area');
const resultsOutput = document.getElementById('results-output');
const reportLinkArea = document.getElementById('report-link-area');
form.addEventListener('submit', async (event) => {
event.preventDefault();
statusArea.textContent = '正在运行测试,请稍候...';
resultsOutput.textContent = '';
reportLinkArea.innerHTML = '';
const formData = new FormData(form);
const config = {};
formData.forEach((value, key) => {
// 处理复选框
if (key.startsWith('use_llm_for_')) {
config[key] = form.elements[key].checked;
} else if (value.trim() !== '') { // 只添加非空值
config[key] = value.trim();
}
});
// 如果复选框未被选中,FormData 不会包含它们,所以要确保它们是 false
['use_llm_for_request_body', 'use_llm_for_path_params', 'use_llm_for_query_params', 'use_llm_for_headers'].forEach(key => {
if (!(key in config)) {
config[key] = false;
}
});
// 从 YAPI 分类和 Swagger 标签中获取选中的项
const selectedYapiCategories = Array.from(document.querySelectorAll('#yapi-categories-container input[type="checkbox"]:checked'))
.map(cb => cb.value);
if (selectedYapiCategories.length > 0) {
config['categories'] = selectedYapiCategories.join(',');
}
const selectedSwaggerTags = Array.from(document.querySelectorAll('#swagger-tags-container input[type="checkbox"]:checked'))
.map(cb => cb.value);
if (selectedSwaggerTags.length > 0) {
config['tags'] = selectedSwaggerTags.join(',');
}
try {
const response = await fetch('/run-tests', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(config)
});
const result = await response.json();
if (response.ok) {
statusArea.textContent = `测试完成: ${result.message || '成功'}`;
resultsOutput.textContent = JSON.stringify(result.summary, null, 2);
if (result.report_file) {
reportLinkArea.innerHTML = `<p>测试报告已保存到: <strong>${result.report_file}</strong></p>`;
}
} else {
statusArea.textContent = `测试失败: ${result.error || '未知错误'}`;
resultsOutput.textContent = JSON.stringify(result, null, 2);
}
} catch (error) {
statusArea.textContent = '运行测试时发生网络错误或服务器内部错误。';
resultsOutput.textContent = error.toString();
console.error('运行测试出错:', error);
}
});
});
async function fetchYapiCategories() {
const yapiFilePath = document.getElementById('yapi_file_path').value;
const container = document.getElementById('yapi-categories-container');
container.innerHTML = '正在加载分类...';
if (!yapiFilePath) {
container.innerHTML = '<p style="color: red;">请输入YAPI文件路径。</p>';
return;
}
try {
const response = await fetch('/list-yapi-categories', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ yapi_file_path: yapiFilePath })
});
const categories = await response.json();
if (response.ok) {
renderCheckboxes(container, categories, 'yapi_category');
} else {
container.innerHTML = `<p style="color: red;">加载YAPI分类失败: ${categories.error || '未知错误'}</p>`;
}
} catch (error) {
container.innerHTML = `<p style="color: red;">请求YAPI分类时出错: ${error}</p>`;
}
}
async function fetchSwaggerTags() {
const swaggerFilePath = document.getElementById('swagger_file_path').value;
const container = document.getElementById('swagger-tags-container');
container.innerHTML = '正在加载标签...';
if (!swaggerFilePath) {
container.innerHTML = '<p style="color: red;">请输入Swagger文件路径。</p>';
return;
}
try {
const response = await fetch('/list-swagger-tags', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ swagger_file_path: swaggerFilePath })
});
const tags = await response.json();
if (response.ok) {
renderCheckboxes(container, tags, 'swagger_tag');
} else {
container.innerHTML = `<p style="color: red;">加载Swagger标签失败: ${tags.error || '未知错误'}</p>`;
}
} catch (error) {
container.innerHTML = `<p style="color: red;">请求Swagger标签时出错: ${error}</p>`;
}
}
function renderCheckboxes(container, items, groupName) {
if (!items || items.length === 0) {
container.innerHTML = '<p>未找到任何项。</p>';
return;
}
let html = items.map((item, index) => {
const id = `${groupName}_${index}`;
return `<div>
<input type="checkbox" id="${id}" name="${groupName}[]" value="${item.name}">
<label for="${id}">${item.name} ${item.description ? '(' + item.description + ')' : ''}</label>
</div>`;
}).join('');
container.innerHTML = html;
}
+161
View File
@@ -0,0 +1,161 @@
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
line-height: 1.6;
margin: 0;
padding: 20px;
background-color: #f4f7f6;
color: #333;
}
.container {
max-width: 900px;
margin: 0 auto;
background-color: #fff;
padding: 25px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
color: #2c3e50;
text-align: center;
margin-bottom: 25px;
}
h2 {
color: #34495e;
border-bottom: 2px solid #ecf0f1;
padding-bottom: 10px;
margin-top: 30px;
margin-bottom: 20px;
}
.form-group {
margin-bottom: 18px;
}
.form-group label {
display: block;
margin-bottom: 6px;
font-weight: bold;
color: #555;
}
.form-group input[type="text"],
.form-group input[type="password"],
.form-group input[type="url"] {
width: calc(100% - 22px);
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
}
.form-group input[type="text"]:focus,
.form-group input[type="password"]:focus,
.form-group input[type="url"]:focus {
border-color: #3498db;
outline: none;
}
.checkbox-group label {
font-weight: normal;
display: inline-block;
margin-left: 5px;
}
.checkbox-group input[type="checkbox"] {
margin-right: 5px;
vertical-align: middle;
}
fieldset {
border: 1px solid #ddd;
padding: 15px;
border-radius: 4px;
margin-bottom: 20px;
}
legend {
padding: 0 10px;
font-weight: bold;
color: #3498db;
}
.action-button {
background-color: #3498db;
color: white;
padding: 8px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.9em;
margin-left: 10px;
}
.action-button:hover {
background-color: #2980b9;
}
.submit-button {
background-color: #2ecc71;
color: white;
padding: 12px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1.1em;
display: block;
width: 100%;
margin-top: 20px;
}
.submit-button:hover {
background-color: #27ae60;
}
.results-section {
margin-top: 30px;
}
#status-area {
font-weight: bold;
margin-bottom: 15px;
padding: 10px;
border-radius: 4px;
background-color: #ecf0f1;
border: 1px solid #bdc3c7;
}
#results-output {
background-color: #2c3e50;
color: #ecf0f1;
padding: 15px;
border-radius: 4px;
white-space: pre-wrap; /* Allows wrapping and preserves whitespace */
word-wrap: break-word; /* Breaks long words to prevent overflow */
max-height: 500px;
overflow-y: auto;
font-family: "Courier New", Courier, monospace;
}
#report-link-area p {
margin-top: 10px;
font-weight: bold;
}
.categories-tags-container {
margin-top: 10px;
padding: 10px;
background-color: #f9f9f9;
border: 1px solid #eee;
border-radius: 4px;
max-height: 150px;
overflow-y: auto;
}
.categories-tags-container div {
margin-bottom: 5px;
}
.categories-tags-container label {
font-weight: normal;
}