查看历史记录-history-viewer
This commit is contained in:
parent
ff71f2aed6
commit
3d8b83e992
@ -23,3 +23,5 @@ make run_stages
|
||||
### 开发
|
||||
|
||||
cursor-memory-bank在memory-bank目录下,对话前先传给cursor或其他ai
|
||||
|
||||
|
||||
|
||||
BIN
assets/.DS_Store
vendored
BIN
assets/.DS_Store
vendored
Binary file not shown.
478
assets/doc/门户详情.txt
Normal file
478
assets/doc/门户详情.txt
Normal file
@ -0,0 +1,478 @@
|
||||
{
|
||||
"id": "685518af31d38b669955a139",
|
||||
"userId": "gyrj_portal",
|
||||
"userName": "工业软件门户",
|
||||
"loginButton": "<span style='margin-left:10px;margin-top:5px'><i class='icon-user size-large text-info' ></i><a href='/openapi/admin/market' style='padding:5px' title='进入个人中心'>工业软件门户,您好!</a></span>",
|
||||
"appDocs": [
|
||||
{
|
||||
"_id": {
|
||||
"timestamp": 1745727813,
|
||||
"counter": 4108062,
|
||||
"randomValue1": 16518548,
|
||||
"randomValue2": 19111
|
||||
},
|
||||
"appName": "接口集成应用",
|
||||
"appId": "lcap"
|
||||
},
|
||||
{
|
||||
"_id": {
|
||||
"timestamp": 1747877242,
|
||||
"counter": 4731629,
|
||||
"randomValue1": 2166043,
|
||||
"randomValue2": 18860
|
||||
},
|
||||
"appName": "平台开发应用",
|
||||
"appId": "xtkf_app"
|
||||
},
|
||||
{
|
||||
"_id": {
|
||||
"timestamp": 1748419576,
|
||||
"counter": 6179296,
|
||||
"randomValue1": 243350,
|
||||
"randomValue2": 621
|
||||
},
|
||||
"appName": "协同开发应用",
|
||||
"appId": "xtkf_application"
|
||||
},
|
||||
{
|
||||
"_id": {
|
||||
"timestamp": 1749102403,
|
||||
"counter": 16528014,
|
||||
"randomValue1": 6571710,
|
||||
"randomValue2": 23361
|
||||
},
|
||||
"appName": "演示应用001",
|
||||
"appId": "gyrj_001"
|
||||
},
|
||||
{
|
||||
"_id": {
|
||||
"timestamp": 1749106842,
|
||||
"counter": 16538052,
|
||||
"randomValue1": 6571710,
|
||||
"randomValue2": 23361
|
||||
},
|
||||
"appName": "算法中试平台",
|
||||
"appId": "Algopilot"
|
||||
},
|
||||
{
|
||||
"_id": {
|
||||
"timestamp": 1750124658,
|
||||
"counter": 5109861,
|
||||
"randomValue1": 3265419,
|
||||
"randomValue2": 26265
|
||||
},
|
||||
"appName": "fadssd",
|
||||
"appId": "asdfads"
|
||||
}
|
||||
],
|
||||
"doMain": "https://lcapdev.cnpc.com.cn:8082/gateway",
|
||||
"apiId": "6855143668dd72277d795036",
|
||||
"callApplyFlag": false,
|
||||
"changeLogDocs": [
|
||||
{
|
||||
"remark": "调用",
|
||||
"createTime": "2025-06-24 11:20:25",
|
||||
"userId": "13693104300",
|
||||
"userName": "许茂",
|
||||
"version": "1.0",
|
||||
"logType": "发布"
|
||||
},
|
||||
{
|
||||
"createTime": "2025-06-20 16:15:43",
|
||||
"userId": "13693104300",
|
||||
"userName": "许茂",
|
||||
"version": "1.0",
|
||||
"logType": "发布"
|
||||
}
|
||||
],
|
||||
"serviceApproverUserIds": "13693104300",
|
||||
"openBlackAndWhite": "0",
|
||||
"permissions": "",
|
||||
"configName": "嘴流计算",
|
||||
"configId": "GYRJ_TENANTALGOPILOT_API_250620013",
|
||||
"mapUrl": "https://lcapdev.cnpc.com.cn:8082/gateway/gyrj_tenant/gyrj_tenantalgopilot/v1/api/algopilot/property/chockQl",
|
||||
"beanId": "ServiceRegisterRest",
|
||||
"modelId": "",
|
||||
"beanMethodName": "redirectUrl",
|
||||
"methodType": "POST",
|
||||
"produces": "application/json;charset=utf-8",
|
||||
"consumes": "*",
|
||||
"extAttribute": "",
|
||||
"state": "1",
|
||||
"effectiveUser": "",
|
||||
"mockResponseConfigId": "",
|
||||
"regServiceUrl": "",
|
||||
"transaction": false,
|
||||
"joinRestId": "",
|
||||
"configType": "REG",
|
||||
"registerFlag": 1,
|
||||
"requestBodyFlag": true,
|
||||
"bodyEscapeFlag": false,
|
||||
"viewId": "",
|
||||
"logType": 2,
|
||||
"syncFlag": true,
|
||||
"scsPlugConfig": "[]",
|
||||
"regServerId": "",
|
||||
"bindingGatewayType": "",
|
||||
"responseSample": "",
|
||||
"failResponseSample": "",
|
||||
"syncAnnotation": true,
|
||||
"remark": "",
|
||||
"tags": "",
|
||||
"categoryId": "gyrj_tenantGYRJ_TENANTALGOPILOT_CATEGORY_250620002",
|
||||
"businessClassIds": "test_ddd",
|
||||
"version": "1.0",
|
||||
"parentId": "",
|
||||
"deprecated": false,
|
||||
"requestBodyRequired": false,
|
||||
"requestBodyDataType": "",
|
||||
"requestBodySchemaRef": "",
|
||||
"requestBodySampleStr": "\r\n { \"Pt\": 1.2,\r\n \"Ph\": 1,\r\n \"Dchock\": 8.467,\r\n \"Rowl\": 906,\r\n \"fw\": 0.5,\r\n \"GLR\": 100,\r\n \"k\": 1.299\r\n}",
|
||||
"sqlConfigId": "",
|
||||
"filters": "",
|
||||
"retryNum": 0,
|
||||
"retrySleep": 0,
|
||||
"loadBalanceId": "WeightRandomServer",
|
||||
"authType": 2,
|
||||
"approveAuthType": 3,
|
||||
"serializeNull": false,
|
||||
"visibleUserIds": "",
|
||||
"ownerUserId": "",
|
||||
"connecterId": "",
|
||||
"requestCompatibleFlag": 1,
|
||||
"paramsDocs": [
|
||||
{
|
||||
"id": "f9ff5b91868d41ccb755c1f2a6e7779b",
|
||||
"fieldName": "",
|
||||
"fieldId": "Pt",
|
||||
"fieldType": "Number",
|
||||
"hidden": false,
|
||||
"breakFlag": false,
|
||||
"required": false,
|
||||
"sampleValue": "1.2",
|
||||
"enCode": 0,
|
||||
"order": 0,
|
||||
"in": "body",
|
||||
"errorCode": "",
|
||||
"maxLength": 0,
|
||||
"minLength": 0,
|
||||
"deprecated": false,
|
||||
"sortNum": 1
|
||||
},
|
||||
{
|
||||
"id": "841c158c430a46748e391f078caf2c62",
|
||||
"fieldName": "",
|
||||
"fieldId": "Ph",
|
||||
"fieldType": "Number",
|
||||
"hidden": false,
|
||||
"breakFlag": false,
|
||||
"required": false,
|
||||
"sampleValue": "1",
|
||||
"enCode": 0,
|
||||
"order": 0,
|
||||
"in": "body",
|
||||
"errorCode": "",
|
||||
"maxLength": 0,
|
||||
"minLength": 0,
|
||||
"deprecated": false,
|
||||
"sortNum": 1
|
||||
},
|
||||
{
|
||||
"id": "25decf3885734b9ea3db3ba654518f2b",
|
||||
"fieldName": "",
|
||||
"fieldId": "Dchock",
|
||||
"fieldType": "Number",
|
||||
"hidden": false,
|
||||
"breakFlag": false,
|
||||
"required": false,
|
||||
"sampleValue": "8.467",
|
||||
"enCode": 0,
|
||||
"order": 0,
|
||||
"in": "body",
|
||||
"errorCode": "",
|
||||
"maxLength": 0,
|
||||
"minLength": 0,
|
||||
"deprecated": false,
|
||||
"sortNum": 1
|
||||
},
|
||||
{
|
||||
"id": "24f9497dc90249ff82abe6362a638042",
|
||||
"fieldName": "",
|
||||
"fieldId": "Rowl",
|
||||
"fieldType": "Number",
|
||||
"hidden": false,
|
||||
"breakFlag": false,
|
||||
"required": false,
|
||||
"sampleValue": "906",
|
||||
"enCode": 0,
|
||||
"order": 0,
|
||||
"in": "body",
|
||||
"errorCode": "",
|
||||
"maxLength": 0,
|
||||
"minLength": 0,
|
||||
"deprecated": false,
|
||||
"sortNum": 1
|
||||
},
|
||||
{
|
||||
"id": "68940892b97243f7bb686b4ba66449b1",
|
||||
"fieldName": "",
|
||||
"fieldId": "fw",
|
||||
"fieldType": "Number",
|
||||
"hidden": false,
|
||||
"breakFlag": false,
|
||||
"required": false,
|
||||
"sampleValue": "0.5",
|
||||
"enCode": 0,
|
||||
"order": 0,
|
||||
"in": "body",
|
||||
"errorCode": "",
|
||||
"maxLength": 0,
|
||||
"minLength": 0,
|
||||
"deprecated": false,
|
||||
"sortNum": 1
|
||||
},
|
||||
{
|
||||
"id": "3a6b09b7e4bd404e84585875f7de8134",
|
||||
"fieldName": "",
|
||||
"fieldId": "GLR",
|
||||
"fieldType": "Number",
|
||||
"hidden": false,
|
||||
"breakFlag": false,
|
||||
"required": false,
|
||||
"sampleValue": "100",
|
||||
"enCode": 0,
|
||||
"order": 0,
|
||||
"in": "body",
|
||||
"errorCode": "",
|
||||
"maxLength": 0,
|
||||
"minLength": 0,
|
||||
"deprecated": false,
|
||||
"sortNum": 1
|
||||
},
|
||||
{
|
||||
"id": "60419eb45bc641c68a14424944c7f1a8",
|
||||
"fieldName": "",
|
||||
"fieldId": "k",
|
||||
"fieldType": "Number",
|
||||
"hidden": false,
|
||||
"breakFlag": false,
|
||||
"required": false,
|
||||
"sampleValue": "1.299",
|
||||
"enCode": 0,
|
||||
"order": 0,
|
||||
"in": "body",
|
||||
"errorCode": "",
|
||||
"maxLength": 0,
|
||||
"minLength": 0,
|
||||
"deprecated": false,
|
||||
"sortNum": 1
|
||||
}
|
||||
],
|
||||
"responseDocs": [],
|
||||
"outputParamsDocs": [],
|
||||
"hotFlag": 0,
|
||||
"recommendFlag": 0,
|
||||
"clickNum": 7,
|
||||
"iconUrl": "",
|
||||
"price": "会员免费",
|
||||
"score": 80,
|
||||
"describeBody": "<p></p>",
|
||||
"publishStatus": "1",
|
||||
"backendMethod": "*",
|
||||
"backendUrl": "${$config.gyrj_tenantgyrj_tenantAlgopilot.server}/api/algopilot/property/chockQl",
|
||||
"backendServerId": "",
|
||||
"backendHaderTransparent": 0,
|
||||
"backendHeaderParams": "[]",
|
||||
"backendConnectTimeout": 30000,
|
||||
"backendParamType": 1,
|
||||
"backendResponseBodyStreamFlag": false,
|
||||
"backendResponseBodyChunkedStreamFlag": false,
|
||||
"backendRequestBodyTemplate": "",
|
||||
"webServiceRequestXmlFlag": true,
|
||||
"webServiceResponseXmlFlag": true,
|
||||
"webServiceRequestBody": "",
|
||||
"webServiceXmlSubNode": "",
|
||||
"webServiceRequestXmlFillType": 1,
|
||||
"webServiceWsdlUrl": "",
|
||||
"webServiceWsdlDocument": "",
|
||||
"webServiceRequestBodyFlag": true,
|
||||
"xmlTemplateId": "",
|
||||
"webServiceResponseBody": "",
|
||||
"webServiceResponseXmlFillType": 1,
|
||||
"dubboApplicationName": "",
|
||||
"dubboConnectionType": "",
|
||||
"dubboZookeeper": "",
|
||||
"dubboInterface": "",
|
||||
"dubboMethodName": "",
|
||||
"dubboParamsType": "",
|
||||
"dubboVersion": "",
|
||||
"processId": "",
|
||||
"procedureId": "",
|
||||
"bindServicesConfigId": "",
|
||||
"businessDataTagConfigs": "",
|
||||
"changeMethod": "",
|
||||
"slaLevel": 1,
|
||||
"isSystem": 0,
|
||||
"source": "GATEWAY",
|
||||
"responseCookie": false,
|
||||
"businessCodePath": "",
|
||||
"businessMessagePath": "",
|
||||
"configScore": 0,
|
||||
"allowRetry": 0,
|
||||
"assessConfigFlag": "0",
|
||||
"gRpcServer": "",
|
||||
"gRpcPackage": "",
|
||||
"gRpcService": "",
|
||||
"gRpcMethod": "",
|
||||
"ferry": false,
|
||||
"fileForwardPath": "",
|
||||
"fileForwardSelectPathType": "",
|
||||
"fileForwardSelectPath": "",
|
||||
"connectionPool": 0,
|
||||
"publishPortalStatus": "1",
|
||||
"sourceModuleType": "",
|
||||
"appId": "Algopilot",
|
||||
"createTime": "2025-06-20 16:13:01",
|
||||
"creator": "13693104300",
|
||||
"creatorName": "许茂",
|
||||
"editTime": "2025-06-24 13:36:15",
|
||||
"editor": "13693104300",
|
||||
"editorName": "许茂",
|
||||
"tenantCode": "gyrj_tenant",
|
||||
"paramsDocList": [
|
||||
{
|
||||
"id": "f9ff5b91868d41ccb755c1f2a6e7779b",
|
||||
"fieldName": "",
|
||||
"fieldId": "Pt",
|
||||
"fieldType": "Number",
|
||||
"hidden": false,
|
||||
"breakFlag": false,
|
||||
"required": false,
|
||||
"sampleValue": "1.2",
|
||||
"enCode": 0,
|
||||
"order": 0,
|
||||
"in": "body",
|
||||
"errorCode": "",
|
||||
"maxLength": 0,
|
||||
"minLength": 0,
|
||||
"deprecated": false,
|
||||
"sortNum": 1
|
||||
},
|
||||
{
|
||||
"id": "841c158c430a46748e391f078caf2c62",
|
||||
"fieldName": "",
|
||||
"fieldId": "Ph",
|
||||
"fieldType": "Number",
|
||||
"hidden": false,
|
||||
"breakFlag": false,
|
||||
"required": false,
|
||||
"sampleValue": "1",
|
||||
"enCode": 0,
|
||||
"order": 0,
|
||||
"in": "body",
|
||||
"errorCode": "",
|
||||
"maxLength": 0,
|
||||
"minLength": 0,
|
||||
"deprecated": false,
|
||||
"sortNum": 1
|
||||
},
|
||||
{
|
||||
"id": "25decf3885734b9ea3db3ba654518f2b",
|
||||
"fieldName": "",
|
||||
"fieldId": "Dchock",
|
||||
"fieldType": "Number",
|
||||
"hidden": false,
|
||||
"breakFlag": false,
|
||||
"required": false,
|
||||
"sampleValue": "8.467",
|
||||
"enCode": 0,
|
||||
"order": 0,
|
||||
"in": "body",
|
||||
"errorCode": "",
|
||||
"maxLength": 0,
|
||||
"minLength": 0,
|
||||
"deprecated": false,
|
||||
"sortNum": 1
|
||||
},
|
||||
{
|
||||
"id": "24f9497dc90249ff82abe6362a638042",
|
||||
"fieldName": "",
|
||||
"fieldId": "Rowl",
|
||||
"fieldType": "Number",
|
||||
"hidden": false,
|
||||
"breakFlag": false,
|
||||
"required": false,
|
||||
"sampleValue": "906",
|
||||
"enCode": 0,
|
||||
"order": 0,
|
||||
"in": "body",
|
||||
"errorCode": "",
|
||||
"maxLength": 0,
|
||||
"minLength": 0,
|
||||
"deprecated": false,
|
||||
"sortNum": 1
|
||||
},
|
||||
{
|
||||
"id": "68940892b97243f7bb686b4ba66449b1",
|
||||
"fieldName": "",
|
||||
"fieldId": "fw",
|
||||
"fieldType": "Number",
|
||||
"hidden": false,
|
||||
"breakFlag": false,
|
||||
"required": false,
|
||||
"sampleValue": "0.5",
|
||||
"enCode": 0,
|
||||
"order": 0,
|
||||
"in": "body",
|
||||
"errorCode": "",
|
||||
"maxLength": 0,
|
||||
"minLength": 0,
|
||||
"deprecated": false,
|
||||
"sortNum": 1
|
||||
},
|
||||
{
|
||||
"id": "3a6b09b7e4bd404e84585875f7de8134",
|
||||
"fieldName": "",
|
||||
"fieldId": "GLR",
|
||||
"fieldType": "Number",
|
||||
"hidden": false,
|
||||
"breakFlag": false,
|
||||
"required": false,
|
||||
"sampleValue": "100",
|
||||
"enCode": 0,
|
||||
"order": 0,
|
||||
"in": "body",
|
||||
"errorCode": "",
|
||||
"maxLength": 0,
|
||||
"minLength": 0,
|
||||
"deprecated": false,
|
||||
"sortNum": 1
|
||||
},
|
||||
{
|
||||
"id": "60419eb45bc641c68a14424944c7f1a8",
|
||||
"fieldName": "",
|
||||
"fieldId": "k",
|
||||
"fieldType": "Number",
|
||||
"hidden": false,
|
||||
"breakFlag": false,
|
||||
"required": false,
|
||||
"sampleValue": "1.299",
|
||||
"enCode": 0,
|
||||
"order": 0,
|
||||
"in": "body",
|
||||
"errorCode": "",
|
||||
"maxLength": 0,
|
||||
"minLength": 0,
|
||||
"deprecated": false,
|
||||
"sortNum": 1
|
||||
}
|
||||
],
|
||||
"subscribeApplyFlag": true,
|
||||
"isBlackAndWhite": "0",
|
||||
"publishTime": "2025-06-20 16:13:01",
|
||||
"url": "/gyrj_tenant/gyrj_tenantalgopilot/v1/api/algopilot/property/chockQl",
|
||||
"accessRouting": "https://lcapdev.cnpc.com.cn:8082/gateway",
|
||||
"commentDocs": [],
|
||||
"header": "{\"Content-Type\":\"*\"}",
|
||||
"appName": "算法中试平台"
|
||||
}
|
||||
Binary file not shown.
@ -1,5 +1,3 @@
|
||||
[
|
||||
|
||||
"API应该使用正确的HTTP方法:GET用于检索,POST用于创建,PUT用于更新,DELETE用于删除"
|
||||
|
||||
]
|
||||
]
|
||||
271
history_viewer.py
Normal file
271
history_viewer.py
Normal file
@ -0,0 +1,271 @@
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import logging
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from flask import Flask, request, jsonify, send_from_directory, session, redirect, url_for, render_template, g, flash, get_flashed_messages, abort
|
||||
from flask_cors import CORS
|
||||
from functools import wraps
|
||||
import markdown
|
||||
|
||||
app = Flask(__name__, static_folder='static', template_folder='templates')
|
||||
CORS(app)
|
||||
|
||||
# --- 基本配置 ---
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
APP_ROOT = os.path.dirname(os.path.abspath(__file__))
|
||||
DATABASE = os.path.join(APP_ROOT, 'users.db')
|
||||
REPORTS_DIR = os.path.join(APP_ROOT, 'test_reports')
|
||||
|
||||
app.config['SECRET_KEY'] = os.urandom(24)
|
||||
app.config['DATABASE'] = DATABASE
|
||||
app.config['REPORTS_DIR'] = REPORTS_DIR
|
||||
|
||||
os.makedirs(app.config['REPORTS_DIR'], exist_ok=True)
|
||||
|
||||
# --- 数据库 Schema 和辅助函数 (与 flask_app.py 相同) ---
|
||||
DB_SCHEMA = '''
|
||||
DROP TABLE IF EXISTS user;
|
||||
CREATE TABLE user (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password_hash TEXT NOT NULL
|
||||
);
|
||||
'''
|
||||
|
||||
def get_db():
|
||||
db = getattr(g, '_database', None)
|
||||
if db is None:
|
||||
db = g._database = sqlite3.connect(app.config['DATABASE'])
|
||||
db.row_factory = sqlite3.Row
|
||||
return db
|
||||
|
||||
@app.teardown_appcontext
|
||||
def close_connection(exception):
|
||||
db = getattr(g, '_database', None)
|
||||
if db is not None:
|
||||
db.close()
|
||||
|
||||
def init_db(force_create=False):
|
||||
if force_create or not os.path.exists(app.config['DATABASE']):
|
||||
with app.app_context():
|
||||
db = get_db()
|
||||
db.cursor().executescript(DB_SCHEMA)
|
||||
db.commit()
|
||||
logger.info("数据库已初始化!")
|
||||
create_default_user()
|
||||
else:
|
||||
logger.info("数据库已存在。")
|
||||
|
||||
def create_default_user(username="admin", password="admin123"):
|
||||
with app.app_context():
|
||||
db = get_db()
|
||||
user = db.execute('SELECT * FROM user WHERE username = ?', (username,)).fetchone()
|
||||
if user is None:
|
||||
db.execute("INSERT INTO user (username, password_hash) VALUES (?, ?)", (username, generate_password_hash(password)))
|
||||
db.commit()
|
||||
logger.info(f"已创建默认用户: {username}")
|
||||
else:
|
||||
logger.info(f"默认用户 {username} 已存在。")
|
||||
|
||||
@app.cli.command('init-db')
|
||||
def init_db_command():
|
||||
init_db(force_create=True)
|
||||
print("已初始化数据库。")
|
||||
|
||||
# --- 用户认证 (与 flask_app.py 相同) ---
|
||||
@app.route('/login', methods=('GET', 'POST'))
|
||||
def login():
|
||||
if g.user:
|
||||
return redirect(url_for('list_history'))
|
||||
if request.method == 'POST':
|
||||
username = request.form['username']
|
||||
password = request.form['password']
|
||||
db = get_db()
|
||||
error = None
|
||||
user = db.execute('SELECT * FROM user WHERE username = ?', (username,)).fetchone()
|
||||
if user is None:
|
||||
error = '用户名不存在。'
|
||||
elif not check_password_hash(user['password_hash'], password):
|
||||
error = '密码错误。'
|
||||
if error is None:
|
||||
session.clear()
|
||||
session['user_id'] = user['id']
|
||||
session['username'] = user['username']
|
||||
return redirect(url_for('list_history'))
|
||||
flash(error)
|
||||
return render_template('login.html')
|
||||
|
||||
@app.route('/logout')
|
||||
def logout():
|
||||
session.clear()
|
||||
flash('您已成功登出。')
|
||||
return redirect(url_for('login'))
|
||||
|
||||
def login_required(view):
|
||||
@wraps(view)
|
||||
def wrapped_view(**kwargs):
|
||||
if g.user is None:
|
||||
return redirect(url_for('login'))
|
||||
return view(**kwargs)
|
||||
return wrapped_view
|
||||
|
||||
@app.before_request
|
||||
def load_logged_in_user():
|
||||
user_id = session.get('user_id')
|
||||
if user_id is None:
|
||||
g.user = None
|
||||
else:
|
||||
g.user = get_db().execute('SELECT * FROM user WHERE id = ?', (user_id,)).fetchone()
|
||||
|
||||
# --- LLM配置视图 ---
|
||||
CRITERIA_FILE_PATH = os.path.join(APP_ROOT, 'custom_testcases', 'llm', 'compliance_criteria.json')
|
||||
|
||||
@app.route('/llm-config', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def llm_config():
|
||||
criteria_for_template = []
|
||||
file_exists = os.path.exists(CRITERIA_FILE_PATH)
|
||||
|
||||
if request.method == 'POST':
|
||||
# 从表单获取所有名为'criteria'的输入项,作为一个列表
|
||||
criteria_list = request.form.getlist('criteria')
|
||||
# 过滤掉用户可能提交的空规则
|
||||
criteria_list = [item.strip() for item in criteria_list if item.strip()]
|
||||
|
||||
try:
|
||||
# 将规则列表格式化为美观的JSON并保存
|
||||
pretty_content = json.dumps(criteria_list, indent=2, ensure_ascii=False)
|
||||
with open(CRITERIA_FILE_PATH, 'w', encoding='utf-8') as f:
|
||||
f.write(pretty_content)
|
||||
flash('LLM合规性标准已成功保存!', 'success')
|
||||
except Exception as e:
|
||||
flash(f'保存文件时发生未知错误: {e}', 'error')
|
||||
|
||||
# 无论是GET还是POST请求后,都重新从文件中读取最新的规则列表用于显示
|
||||
if file_exists:
|
||||
try:
|
||||
with open(CRITERIA_FILE_PATH, 'r', encoding='utf-8') as f:
|
||||
criteria_for_template = json.load(f)
|
||||
# 确保文件内容确实是一个列表
|
||||
if not isinstance(criteria_for_template, list):
|
||||
flash('配置文件格式错误:内容应为JSON数组。已重置为空列表。', 'error')
|
||||
criteria_for_template = []
|
||||
except Exception as e:
|
||||
flash(f'读取配置文件时出错: {e}', 'error')
|
||||
criteria_for_template = []
|
||||
|
||||
# 准备一个用于页面展示的示例API信息
|
||||
example_api_info = {
|
||||
"path_template": "/api/dms/instance/v1/message/push/myschema/1.0",
|
||||
"method": "POST",
|
||||
"title": "数据推送接口",
|
||||
"description": "用于向系统推送标准格式的数据。",
|
||||
"schema_request_body": {"...": "... (此处为请求体Schema定义)"},
|
||||
"instance_url": "http://example.com/api/dms/instance/v1/message/push/myschema/1.0",
|
||||
"instance_request_headers": {"X-Tenant-ID": "tenant-001", "...": "..."},
|
||||
"instance_request_body": {"id": "123", "data": "example"},
|
||||
"instance_response_status": 200,
|
||||
"instance_response_body": {"code": 0, "message": "success", "data": True}
|
||||
}
|
||||
|
||||
return render_template('llm_config.html', criteria=criteria_for_template, file_exists=file_exists, example_api_info=json.dumps(example_api_info, indent=2, ensure_ascii=False))
|
||||
|
||||
# --- 文件下载路由 ---
|
||||
@app.route('/download/<path:run_id>/<path:filename>')
|
||||
@login_required
|
||||
def download_report(run_id, filename):
|
||||
"""安全地提供指定运行记录中的报告文件下载。"""
|
||||
# 清理输入,防止目录遍历攻击
|
||||
run_id_safe = Path(run_id).name
|
||||
filename_safe = Path(filename).name
|
||||
|
||||
reports_dir = Path(app.config['REPORTS_DIR']).resolve()
|
||||
run_dir = (reports_dir / run_id_safe).resolve()
|
||||
|
||||
# 安全检查:确保请求的目录是REPORTS_DIR的子目录
|
||||
if not run_dir.is_dir() or run_dir.parent != reports_dir:
|
||||
abort(404, "找不到指定的测试记录或权限不足。")
|
||||
|
||||
return send_from_directory(run_dir, filename_safe, as_attachment=True)
|
||||
|
||||
# --- 历史记录视图 ---
|
||||
@app.route('/')
|
||||
@login_required
|
||||
def list_history():
|
||||
history = []
|
||||
reports_path = Path(app.config['REPORTS_DIR'])
|
||||
if not reports_path.is_dir():
|
||||
flash('报告目录不存在。')
|
||||
return render_template('history.html', history=[])
|
||||
|
||||
# 获取所有子目录(即测试运行记录)
|
||||
run_dirs = [d for d in reports_path.iterdir() if d.is_dir()]
|
||||
# 按名称(时间戳)降序排序
|
||||
run_dirs.sort(key=lambda x: x.name, reverse=True)
|
||||
|
||||
for run_dir in run_dirs:
|
||||
summary_path = run_dir / 'summary.json'
|
||||
details_path = run_dir / 'api_call_details.md'
|
||||
run_info = {'id': run_dir.name, 'summary': None, 'has_details': details_path.exists()}
|
||||
|
||||
if summary_path.exists():
|
||||
try:
|
||||
with open(summary_path, 'r', encoding='utf-8') as f:
|
||||
summary_data = json.load(f)
|
||||
run_info['summary'] = summary_data
|
||||
except (json.JSONDecodeError, IOError) as e:
|
||||
logger.error(f"无法读取或解析摘要文件 {summary_path}: {e}")
|
||||
run_info['summary'] = {'error': '无法加载摘要'}
|
||||
|
||||
history.append(run_info)
|
||||
|
||||
return render_template('history.html', history=history)
|
||||
|
||||
@app.route('/details/<run_id>')
|
||||
@login_required
|
||||
def show_details(run_id):
|
||||
run_id = Path(run_id).name # Sanitize input
|
||||
run_dir = Path(app.config['REPORTS_DIR']) / run_id
|
||||
|
||||
if not run_dir.is_dir():
|
||||
return "找不到指定的测试记录。", 404
|
||||
|
||||
summary_path = run_dir / 'summary.json'
|
||||
details_path = run_dir / 'api_call_details.md'
|
||||
|
||||
summary_content = "{}"
|
||||
details_content = "### 未找到API调用详情报告"
|
||||
|
||||
if summary_path.exists():
|
||||
try:
|
||||
with open(summary_path, 'r', encoding='utf-8') as f:
|
||||
summary_data = json.load(f)
|
||||
summary_content = json.dumps(summary_data, indent=2, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
summary_content = f"加载摘要文件出错: {e}"
|
||||
|
||||
if details_path.exists():
|
||||
try:
|
||||
with open(details_path, 'r', encoding='utf-8') as f:
|
||||
# 将Markdown转换为HTML
|
||||
details_content = markdown.markdown(f.read(), extensions=['fenced_code', 'tables', 'def_list', 'attr_list'])
|
||||
except Exception as e:
|
||||
details_content = f"加载详情文件出错: {e}"
|
||||
|
||||
return render_template('history_detail.html', run_id=run_id, summary_content=summary_content, details_content=details_content)
|
||||
|
||||
|
||||
# --- 根路径重定向 ---
|
||||
@app.route('/index')
|
||||
def index_redirect():
|
||||
return redirect(url_for('list_history'))
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 首次运行时确保数据库和用户存在
|
||||
init_db()
|
||||
app.run(debug=True, host='0.0.0.0', port=5051)
|
||||
12332
log_stage.txt
12332
log_stage.txt
File diff suppressed because one or more lines are too long
@ -17,4 +17,5 @@ prance[osv]>=23.0.0,<24.0.0
|
||||
# pytest-cov>=4.0.0,<5.0.0
|
||||
# httpx>=0.20.0,<0.28.0 # for testing API calls
|
||||
|
||||
Flask-Cors>=3.0
|
||||
Flask-Cors>=3.0
|
||||
markdown
|
||||
@ -13,6 +13,7 @@ import sys
|
||||
import json
|
||||
import logging
|
||||
import argparse
|
||||
import datetime
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
@ -264,13 +265,13 @@ def save_api_call_details_to_file(api_call_details: List[APICallDetail], output_
|
||||
markdown_content.append("- **Headers:**")
|
||||
markdown_content.append("```json")
|
||||
markdown_content.append(json.dumps(detail.request_headers, indent=2, ensure_ascii=False))
|
||||
markdown_content.append(" ```")
|
||||
markdown_content.append("```")
|
||||
|
||||
if detail.request_params:
|
||||
markdown_content.append("- **Query Parameters:**")
|
||||
markdown_content.append("```json")
|
||||
markdown_content.append(json.dumps(detail.request_params, indent=2, ensure_ascii=False))
|
||||
markdown_content.append(" ```")
|
||||
markdown_content.append("```")
|
||||
|
||||
if detail.request_body is not None:
|
||||
markdown_content.append("- **Body:**")
|
||||
@ -293,7 +294,7 @@ def save_api_call_details_to_file(api_call_details: List[APICallDetail], output_
|
||||
|
||||
markdown_content.append(f"```{body_lang}")
|
||||
markdown_content.append(formatted_body)
|
||||
markdown_content.append(" ```")
|
||||
markdown_content.append("```")
|
||||
|
||||
markdown_content.append("### Response Details")
|
||||
markdown_content.append(f"- **Status Code:** `{detail.response_status_code}`")
|
||||
@ -302,7 +303,7 @@ def save_api_call_details_to_file(api_call_details: List[APICallDetail], output_
|
||||
markdown_content.append("- **Headers:**")
|
||||
markdown_content.append("```json")
|
||||
markdown_content.append(json.dumps(detail.response_headers, indent=2, ensure_ascii=False))
|
||||
markdown_content.append(" ```")
|
||||
markdown_content.append("```")
|
||||
|
||||
if detail.response_body is not None:
|
||||
markdown_content.append("- **Body:**")
|
||||
@ -329,7 +330,7 @@ def save_api_call_details_to_file(api_call_details: List[APICallDetail], output_
|
||||
|
||||
markdown_content.append(f"```{resp_body_lang}")
|
||||
markdown_content.append(formatted_resp_body)
|
||||
markdown_content.append(" ```")
|
||||
markdown_content.append("```")
|
||||
markdown_content.append("") # Add a blank line for spacing before next --- or EOF
|
||||
markdown_content.append("---") # Separator
|
||||
|
||||
@ -376,27 +377,25 @@ def main():
|
||||
categories = args.categories.split(',') if args.categories else None
|
||||
tags = args.tags.split(',') if args.tags else None
|
||||
|
||||
DEFAULT_OUTPUT_DIR = Path("./test_reports")
|
||||
output_directory: Path
|
||||
main_report_file_path: Path
|
||||
|
||||
# 确定基础输出目录
|
||||
base_output_dir = Path("./test_reports")
|
||||
if args.output:
|
||||
output_arg_path = Path(args.output)
|
||||
if output_arg_path.suffix and output_arg_path.name: # Check if it looks like a file
|
||||
output_directory = output_arg_path.parent
|
||||
main_report_file_path = output_arg_path
|
||||
else:
|
||||
output_directory = output_arg_path
|
||||
main_report_file_path = output_directory / f"summary.{args.format}"
|
||||
else:
|
||||
output_directory = DEFAULT_OUTPUT_DIR
|
||||
main_report_file_path = output_directory / f"summary.{args.format}"
|
||||
# 如果提供了 --output,将其作为所有时间戳报告的基础目录。
|
||||
# 这简化了逻辑:--output 始终是一个目录。
|
||||
base_output_dir = Path(args.output)
|
||||
|
||||
# 为本次测试运行创建一个唯一的、带时间戳的子目录
|
||||
timestamp = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
||||
output_directory = base_output_dir / timestamp
|
||||
|
||||
# 主摘要报告的文件路径现在位于新目录内
|
||||
main_report_file_path = output_directory / f"summary.{args.format}"
|
||||
|
||||
try:
|
||||
output_directory.mkdir(parents=True, exist_ok=True)
|
||||
logger.info(f"主输出目录设置为: {output_directory.resolve()}")
|
||||
logger.info(f"测试报告将保存到: {output_directory.resolve()}")
|
||||
except OSError as e:
|
||||
logger.error(f"创建主输出目录失败 {output_directory}: {e}")
|
||||
logger.error(f"创建输出目录失败 {output_directory}: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
orchestrator = APITestOrchestrator(
|
||||
@ -468,36 +467,12 @@ def main():
|
||||
# 保存主测试摘要 (现在可能包含测试阶段结果)
|
||||
save_results(test_summary, str(main_report_file_path), args.format)
|
||||
|
||||
api_calls_output_path_str: Optional[str] = None
|
||||
# 默认文件名现在是 .md
|
||||
api_calls_filename: str = "api_call_details.md"
|
||||
|
||||
if args.api_calls_output:
|
||||
api_calls_output_file = Path(args.api_calls_output)
|
||||
# 确保后缀是 .md,如果用户提供了其他后缀或没有后缀
|
||||
if api_calls_output_file.suffix.lower() not in ['.md', '.markdown']:
|
||||
api_calls_output_file = api_calls_output_file.with_suffix('.md')
|
||||
logger.info(f"API调用详情输出文件名已调整为 Markdown 格式: {api_calls_output_file.name}")
|
||||
|
||||
api_calls_output_path_str = str(api_calls_output_file.parent)
|
||||
api_calls_filename = api_calls_output_file.name
|
||||
logger.info(f"API调用详情将以 Markdown 格式保存到: {api_calls_output_file}")
|
||||
elif args.output:
|
||||
output_arg_path = Path(args.output)
|
||||
if output_arg_path.is_dir():
|
||||
api_calls_output_path_str = str(output_arg_path)
|
||||
else:
|
||||
api_calls_output_path_str = str(output_arg_path.parent)
|
||||
logger.info(f"API调用详情将以 Markdown 格式保存到目录 '{api_calls_output_path_str}' (使用默认文件名 '{api_calls_filename}')")
|
||||
else:
|
||||
api_calls_output_path_str = "."
|
||||
logger.info(f"API调用详情将以 Markdown 格式保存到当前目录 '.' (使用默认文件名 '{api_calls_filename}')")
|
||||
|
||||
# 保存API调用详情
|
||||
if orchestrator and api_calls_output_path_str:
|
||||
# API调用详情报告也应保存在同一时间戳目录中
|
||||
api_calls_filename = "api_call_details.md"
|
||||
if orchestrator:
|
||||
save_api_call_details_to_file(
|
||||
orchestrator.get_api_call_details(),
|
||||
api_calls_output_path_str,
|
||||
str(output_directory),
|
||||
filename=api_calls_filename
|
||||
)
|
||||
|
||||
|
||||
215
static/history_style.css
Normal file
215
static/history_style.css
Normal file
@ -0,0 +1,215 @@
|
||||
/* history_style.css */
|
||||
|
||||
/* Header styles */
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header-controls {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
font-size: 0.9em;
|
||||
color: #555;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.user-info a {
|
||||
text-decoration: none;
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.user-info a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.download-btn {
|
||||
background-color: #6c757d;
|
||||
color: white !important;
|
||||
margin-left: 5px;
|
||||
}
|
||||
.download-btn:hover {
|
||||
background-color: #5a6268;
|
||||
}
|
||||
|
||||
/* 悬浮导航按钮 */
|
||||
.floating-nav {
|
||||
position: fixed;
|
||||
bottom: 30px;
|
||||
right: 30px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px; /* 按钮之间的间距 */
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.floating-nav .nav-btn {
|
||||
background-color: #007bff;
|
||||
color: white !important;
|
||||
padding: 10px 15px;
|
||||
border-radius: 5px;
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
||||
transition: background-color 0.2s, transform 0.2s;
|
||||
}
|
||||
|
||||
.floating-nav .nav-btn:hover {
|
||||
background-color: #0056b3;
|
||||
transform: scale(1.05); /* 轻微放大效果 */
|
||||
}
|
||||
|
||||
/* Table styles */
|
||||
.history-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.history-table th, .history-table td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 12px 15px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.history-table th {
|
||||
background-color: #f8f9fa;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.history-table tbody tr:nth-child(even) {
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
|
||||
.history-table tbody tr:hover {
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
|
||||
/* 根据最终状态为行添加柔和的背景色 */
|
||||
.status-row-passed:nth-child(even) {
|
||||
background-color: #e6f7ec;
|
||||
}
|
||||
.status-row-passed:nth-child(odd) {
|
||||
background-color: #f2fbf5;
|
||||
}
|
||||
.status-row-failed:nth-child(even) {
|
||||
background-color: #fbeae5;
|
||||
}
|
||||
.status-row-failed:nth-child(odd) {
|
||||
background-color: #fef4f1;
|
||||
}
|
||||
|
||||
.error-row {
|
||||
color: #dc3545;
|
||||
font-style: italic;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
/* Status badge styles */
|
||||
.status-badge {
|
||||
padding: 5px 10px;
|
||||
border-radius: 12px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 0.85em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.status-passed {
|
||||
background-color: #28a745; /* Green */
|
||||
}
|
||||
|
||||
.status-failed {
|
||||
background-color: #dc3545; /* Red */
|
||||
}
|
||||
|
||||
/* Button styles */
|
||||
.button {
|
||||
display: inline-block;
|
||||
padding: 8px 12px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
/* Detail page styles */
|
||||
.details-container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.json-output {
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
}
|
||||
|
||||
/* Markdown body styles */
|
||||
.markdown-body {
|
||||
margin-top: 20px;
|
||||
padding: 20px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
background-color: #fff;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.markdown-body h1, .markdown-body h2, .markdown-body h3 {
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 0.3em;
|
||||
margin-top: 24px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.markdown-body table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.markdown-body th, .markdown-body td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.markdown-body th {
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
|
||||
.markdown-body pre {
|
||||
background-color: #f6f8fa;
|
||||
padding: 16px;
|
||||
border-radius: 3px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.markdown-body code {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
background-color: rgba(27,31,35,0.05);
|
||||
padding: 0.2em 0.4em;
|
||||
margin: 0;
|
||||
font-size: 85%;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.markdown-body pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-size: 100%;
|
||||
}
|
||||
93
templates/history.html
Normal file
93
templates/history.html
Normal file
@ -0,0 +1,93 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>测试历史记录</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='history_style.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>测试历史记录</h1>
|
||||
<div class="user-info">
|
||||
<a href="{{ url_for('llm_config') }}">LLM 标准配置</a> |
|
||||
<span>欢迎, {{ g.user['username'] }}</span> |
|
||||
<a href="{{ url_for('logout') }}">登出</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<ul class=flashes>
|
||||
{% for message in messages %}
|
||||
<li>{{ message }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<table class="history-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>测试时间</th>
|
||||
<th>状态</th>
|
||||
<th>总计</th>
|
||||
<th>通过</th>
|
||||
<th>失败</th>
|
||||
<th>错误</th>
|
||||
<th>跳过</th>
|
||||
<th>耗时 (秒)</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% if history %}
|
||||
{% for run in history %}
|
||||
{% set summary = run.summary %}
|
||||
{% if summary and 'error' not in summary and 'overall_summary' in summary %}
|
||||
{% set overall = summary.overall_summary %}
|
||||
|
||||
{# 根据成功率计算渐变色 (HSL: 0=红, 120=绿) #}
|
||||
{% set success_rate_str = overall.test_case_success_rate | default('0%') %}
|
||||
{% set success_rate = success_rate_str.replace('%', '')|float %}
|
||||
{% set hue = success_rate * 1.2 %}
|
||||
|
||||
{% set status_text = 'PASSED' if overall.test_cases_failed == 0 and overall.test_cases_error == 0 and overall.stages_failed == 0 and overall.stages_error == 0 else 'FAILED' %}
|
||||
|
||||
{# 如果通过,行背景为淡绿色,否则为淡红色 #}
|
||||
<tr class="status-row-{{ 'passed' if status_text == 'PASSED' else 'failed' }}">
|
||||
<td>{{ run.id.replace('_', ' ') }}</td>
|
||||
<td>
|
||||
{# 状态标签使用动态计算的HSL颜色 #}
|
||||
<span class="status-badge" style="background-color: hsl({{ hue }}, 85%, 45%);">
|
||||
{{ "%.1f"|format(success_rate) }}%
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ overall.total_test_cases_executed }}</td>
|
||||
<td>{{ overall.test_cases_passed }}</td>
|
||||
<td>{{ overall.test_cases_failed }}</td>
|
||||
<td>{{ overall.test_cases_error }}</td>
|
||||
<td>{{ overall.test_cases_skipped_in_endpoint }}</td>
|
||||
<td>{{ "%.2f"|format(summary.duration_seconds|float) }}</td>
|
||||
<td>
|
||||
<a href="{{ url_for('show_details', run_id=run.id) }}" class="button">查看详情</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td>{{ run.id.replace('_', ' ') }}</td>
|
||||
<td colspan="8" class="error-row">无法加载此运行的摘要信息 (未运行结束或报告格式不正确)。</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="9" style="text-align: center;">没有找到任何测试记录。</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
43
templates/history_detail.html
Normal file
43
templates/history_detail.html
Normal file
@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>测试详情 - {{ run_id }}</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='history_style.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>测试详情: {{ run_id }}</h1>
|
||||
<div class="header-controls">
|
||||
<div class="user-info">
|
||||
<a href="{{ url_for('list_history') }}">返回列表</a> |
|
||||
<a href="{{ url_for('llm_config') }}">LLM 标准配置</a> |
|
||||
<span>欢迎, {{ g.user['username'] }}</span> |
|
||||
<a href="{{ url_for('logout') }}">登出</a>
|
||||
</div>
|
||||
<div class="download-actions">
|
||||
<a href="{{ url_for('download_report', run_id=run_id, filename='summary.json') }}" class="button download-btn">下载JSON报告</a>
|
||||
<a href="{{ url_for('download_report', run_id=run_id, filename='api_call_details.md') }}" class="button download-btn">下载MD报告</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="details-container">
|
||||
<h2 id="summary-section">测试摘要 (JSON)</h2>
|
||||
<pre class="json-output">{{ summary_content }}</pre>
|
||||
|
||||
<h2 id="details-section">API 调用详情</h2>
|
||||
<div class="markdown-body">
|
||||
{{ details_content|safe }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="floating-nav">
|
||||
<a href="#summary-section" class="button nav-btn" title="跳转到摘要">测试摘要</a>
|
||||
<a href="#details-section" class="button nav-btn" title="跳转到详情">API调用详情</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
117
templates/llm_config.html
Normal file
117
templates/llm_config.html
Normal file
@ -0,0 +1,117 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>LLM 合规性标准配置</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='history_style.css') }}">
|
||||
<style>
|
||||
.editor-section { margin-bottom: 20px; }
|
||||
.info-box { background-color: #f8f9fa; border: 1px solid #dee2e6; padding: 15px; border-radius: 5px; margin-top: 10px; }
|
||||
.info-box h3 { margin-top: 0; }
|
||||
.criteria-list-container { margin-bottom: 15px; }
|
||||
.criteria-item { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
|
||||
.criteria-item input[type="text"] { flex-grow: 1; padding: 8px; border: 1px solid #ccc; border-radius: 4px; }
|
||||
.delete-btn { background-color: #dc3545; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; transition: background-color 0.2s; }
|
||||
.delete-btn:hover { background-color: #c82333; }
|
||||
.add-btn { background-color: #28a745; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>LLM 合规性标准配置</h1>
|
||||
<div class="user-info">
|
||||
<a href="{{ url_for('list_history') }}">返回列表</a> |
|
||||
<span>欢迎, {{ g.user['username'] }}</span> |
|
||||
<a href="{{ url_for('logout') }}">登出</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="flash-{{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<div class="config-container">
|
||||
<div class="editor-section">
|
||||
<h2>编辑合规性标准</h2>
|
||||
<p>在这里,您可以直接增删改用于 <code>TC-LLM-COMPLIANCE-001</code> 测试用例的合规性标准规则。</p>
|
||||
|
||||
{% if file_exists %}
|
||||
<form method="post">
|
||||
<div id="criteria-list-container">
|
||||
{% for rule in criteria %}
|
||||
<div class="criteria-item">
|
||||
<input type="text" name="criteria" value="{{ rule }}" class="form-control">
|
||||
<button type="button" class="delete-btn">删除</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<button type="button" id="add-rule-btn" class="button add-btn">添加规则</button>
|
||||
<hr style="margin: 20px 0;">
|
||||
<input type="submit" value="保存所有标准" class="button">
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="flash-error">
|
||||
错误:无法找到配置文件 <code>custom_testcases/llm/compliance_criteria.json</code>。请确保该文件存在于正确的位置。
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="info-section">
|
||||
<div class="info-box">
|
||||
<h3>工作原理说明</h3>
|
||||
<p>
|
||||
此测试用例 (<code>TC-LLM-COMPLIANCE-001</code>) 利用大语言模型(LLM)自动评估API是否符合您在左侧定义的合规标准。
|
||||
</p>
|
||||
<p>
|
||||
在测试运行时,系统会收集API的**所有可用信息**(包括URL、请求方法、描述、参数定义、请求体Schema、示例请求和实际响应等),连同您定义的合规标准列表,一同发送给大模型。
|
||||
</p>
|
||||
<p>
|
||||
<strong>重要提示:</strong>大模型仅能基于其接收到的信息进行判断。因此,它非常适合检查命名规范、结构约定、数据格式等问题,但无法验证需要外部知识或业务上下文的规则(例如"此接口必须与XX系统同步")。
|
||||
</p>
|
||||
</div>
|
||||
<div class="info-box">
|
||||
<h3>发送给LLM的API信息示例</h3>
|
||||
<p>为了做出判断,系统会向LLM提供类似如下结构的API信息:</p>
|
||||
<pre class="json-output">{{ example_api_info }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template id="criteria-item-template">
|
||||
<div class="criteria-item">
|
||||
<input type="text" name="criteria" value="" class="form-control" placeholder="输入新的合规标准...">
|
||||
<button type="button" class="delete-btn">删除</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const listContainer = document.getElementById('criteria-list-container');
|
||||
const addBtn = document.getElementById('add-rule-btn');
|
||||
const template = document.getElementById('criteria-item-template');
|
||||
|
||||
if (addBtn) {
|
||||
addBtn.addEventListener('click', function () {
|
||||
const clone = template.content.cloneNode(true);
|
||||
listContainer.appendChild(clone);
|
||||
});
|
||||
}
|
||||
|
||||
if (listContainer) {
|
||||
listContainer.addEventListener('click', function (e) {
|
||||
if (e.target && e.target.classList.contains('delete-btn')) {
|
||||
e.target.closest('.criteria-item').remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
33
templates/login.html
Normal file
33
templates/login.html
Normal file
@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>登录 - DDMS合规性测试</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h2>请登录</h2>
|
||||
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div class="flashes">
|
||||
{% for message in messages %}
|
||||
<p class="flash-error">{{ message }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="post" class="form-group">
|
||||
<label for="username">用户名:</label>
|
||||
<input type="text" id="username" name="username" required>
|
||||
|
||||
<label for="password">密码:</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
|
||||
<input type="submit" value="登录">
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
9168
test_reports/2025-06-26_01-12-39/api_call_details.md
Normal file
9168
test_reports/2025-06-26_01-12-39/api_call_details.md
Normal file
File diff suppressed because it is too large
Load Diff
2378
test_reports/2025-06-26_01-12-39/summary.json
Normal file
2378
test_reports/2025-06-26_01-12-39/summary.json
Normal file
File diff suppressed because it is too large
Load Diff
9138
test_reports/2025-06-26_01-39-27/api_call_details.md
Normal file
9138
test_reports/2025-06-26_01-39-27/api_call_details.md
Normal file
File diff suppressed because it is too large
Load Diff
2385
test_reports/2025-06-26_01-39-27/summary.json
Normal file
2385
test_reports/2025-06-26_01-39-27/summary.json
Normal file
File diff suppressed because it is too large
Load Diff
9201
test_reports/2025-06-26_01-42-53/api_call_details.md
Normal file
9201
test_reports/2025-06-26_01-42-53/api_call_details.md
Normal file
File diff suppressed because it is too large
Load Diff
2385
test_reports/2025-06-26_01-42-53/summary.json
Normal file
2385
test_reports/2025-06-26_01-42-53/summary.json
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user