更新日志的中文说明

This commit is contained in:
wangjiming
2026-08-21 09:42:03 +08:00
parent 8455d99d49
commit 29d8a506cd
12 changed files with 1923 additions and 168 deletions

View File

@@ -271,7 +271,7 @@ def _commit_source_batch(
except Exception: except Exception:
# 文件系统回滚失败不能覆盖数据库抛出的根因,并继续清理其余对象。 # 文件系统回滚失败不能覆盖数据库抛出的根因,并继续清理其余对象。
logger.exception( logger.exception(
"failed to roll back data process source object task_id=%s", "数据处理源对象回滚失败 task_id=%s",
task_id, task_id,
) )
raise raise
@@ -641,7 +641,7 @@ def _run_generation(
) -> None: ) -> None:
started_at = time.perf_counter() started_at = time.perf_counter()
logger.info( logger.info(
"data process generation worker started task_id=%s generation_run_id=%s", "数据处理生成任务开始 task_id=%s generation_run_id=%s",
task_id, task_id,
generation_run_id, generation_run_id,
) )
@@ -649,8 +649,7 @@ def _run_generation(
task = store.get_task(task_id) task = store.get_task(task_id)
if not store.generation_is_running(task_id, generation_run_id): if not store.generation_is_running(task_id, generation_run_id):
logger.info( logger.info(
"data process generation worker skipped inactive run task_id=%s " "数据处理生成任务跳过(非活跃运行) task_id=%s generation_run_id=%s",
"generation_run_id=%s",
task_id, task_id,
generation_run_id, generation_run_id,
) )
@@ -740,8 +739,7 @@ def _run_generation(
len(preview_items), len(preview_items),
): ):
logger.info( logger.info(
"data process generation stopped before completion task_id=%s " "数据处理生成任务被中止 task_id=%s generation_run_id=%s",
"generation_run_id=%s",
task_id, task_id,
generation_run_id, generation_run_id,
) )
@@ -832,9 +830,7 @@ def _run_generation(
"created_by": (store.get_task(task_id) or {}).get("created_by"), "created_by": (store.get_task(task_id) or {}).get("created_by"),
}) })
logger.info( logger.info(
"data process generation completed task_id=%s generation_run_id=%s " "数据处理生成完成 task_id=%s generation_run_id=%s output_count=%s filtered_count=%s duplicate_count=%s error_count=%s duration_ms=%.2f",
"output_count=%s filtered_count=%s duplicate_count=%s error_count=%s "
"duration_ms=%.2f",
task_id, task_id,
generation_run_id, generation_run_id,
completed.get("output_count", len(accepted)), completed.get("output_count", len(accepted)),
@@ -845,14 +841,13 @@ def _run_generation(
) )
else: else:
logger.info( logger.info(
"data process generation stopped before result persistence task_id=%s " "数据处理生成任务在持久化前被停止 task_id=%s generation_run_id=%s",
"generation_run_id=%s",
task_id, task_id,
generation_run_id, generation_run_id,
) )
except Exception as exc: except Exception as exc:
logger.exception( logger.exception(
"data process generation failed task_id=%s generation_run_id=%s duration_ms=%.2f", "数据处理生成失败 task_id=%s generation_run_id=%s duration_ms=%.2f",
task_id, task_id,
generation_run_id, generation_run_id,
(time.perf_counter() - started_at) * 1000, (time.perf_counter() - started_at) * 1000,
@@ -866,8 +861,7 @@ def _run_generation(
) )
except Exception: except Exception:
logger.exception( logger.exception(
"failed to persist data process generation failure task_id=%s " "数据处理生成失败持久化异常 task_id=%s generation_run_id=%s",
"generation_run_id=%s",
task_id, task_id,
generation_run_id, generation_run_id,
) )
@@ -916,7 +910,7 @@ def create_task(
) -> dict[str, Any]: ) -> dict[str, Any]:
with api_errors(): with api_errors():
task = store.create_task(payload.model_dump(mode="json")) task = store.create_task(payload.model_dump(mode="json"))
biz_logger.info("data-process:create success", taskId=task["id"], processType=task.get("process_type", "")) biz_logger.info("用户创建数据处理任务成功", taskId=task["id"], processType=task.get("process_type", ""))
return ok(task, "data process task created") return ok(task, "data process task created")
@@ -943,7 +937,7 @@ def update_task(
) -> dict[str, Any]: ) -> dict[str, Any]:
with api_errors(): with api_errors():
result = store.update_task(task_id, payload.model_dump(exclude_unset=True, mode="json")) result = store.update_task(task_id, payload.model_dump(exclude_unset=True, mode="json"))
biz_logger.info("data-process:update success", taskId=task_id) biz_logger.info("用户更新数据处理任务成功", taskId=task_id)
return ok(result, "data process task updated") return ok(result, "data process task updated")
@@ -1033,7 +1027,7 @@ def _remove_repeated_storage_objects(
) )
except Exception: except Exception:
logger.exception( logger.exception(
"failed to roll back repeated data process source object task_id=%s", "数据处理源对象重复回滚失败 task_id=%s",
task_id, task_id,
) )
@@ -1109,7 +1103,7 @@ def delete_task(
) -> dict[str, Any]: ) -> dict[str, Any]:
with api_errors(): with api_errors():
store.delete_task(task_id) store.delete_task(task_id)
biz_logger.info("data-process:delete success", taskId=task_id) biz_logger.info("用户删除数据处理任务成功", taskId=task_id)
return ok({"deleted": task_id}, "data process task deleted") return ok({"deleted": task_id}, "data process task deleted")
@@ -1424,10 +1418,10 @@ def delete_source_file(
except Exception: except Exception:
cleanup_pending = True cleanup_pending = True
logger.exception( logger.exception(
"failed to remove data process source object after soft deletion", "数据处理源对象软删除后存储清理失败",
extra={"task_id": task_id, "source_file_id": file_id}, extra={"task_id": task_id, "source_file_id": file_id},
) )
biz_logger.info("data-process:delete-source-file success", taskId=task_id, fileId=file_id, storageCleanupPending=cleanup_pending) biz_logger.info("用户删除数据处理源文件成功", taskId=task_id, fileId=file_id, storageCleanupPending=cleanup_pending)
return ok( return ok(
{"deleted": file_id, "storage_cleanup_pending": cleanup_pending}, {"deleted": file_id, "storage_cleanup_pending": cleanup_pending},
"source file removed", "source file removed",
@@ -1700,7 +1694,7 @@ def _prepare_preview_items(
extracted_text = "\n\n".join(page.text for page in pages if page.text) extracted_text = "\n\n".join(page.text for page in pages if page.text)
if extracted_text != str(source.get("content") or ""): if extracted_text != str(source.get("content") or ""):
logger.warning( logger.warning(
"skip PDF document noise detection because stored offsets differ for %s", "跳过PDF文档噪声检测存储偏移量不一致 source_id=%s",
source["id"], source["id"],
) )
continue continue
@@ -1723,7 +1717,7 @@ def _run_preview(
started_at = time.perf_counter() started_at = time.perf_counter()
logger.info( logger.info(
"data process preview started task_id=%s preview_run_id=%s total_files=%s", "数据处理预览开始 task_id=%s preview_run_id=%s total_files=%s",
task_id, task_id,
preview_run_id, preview_run_id,
len(source_file_ids), len(source_file_ids),
@@ -1732,7 +1726,7 @@ def _run_preview(
is_unstructured = store.get_task(task_id).get("process_type") == "unstructured" is_unstructured = store.get_task(task_id).get("process_type") == "unstructured"
if not store.mark_preview_running(task_id, preview_run_id): if not store.mark_preview_running(task_id, preview_run_id):
logger.info( logger.info(
"data process preview skipped inactive run task_id=%s preview_run_id=%s", "数据处理预览跳过(非活跃运行) task_id=%s preview_run_id=%s",
task_id, task_id,
preview_run_id, preview_run_id,
) )
@@ -1742,8 +1736,7 @@ def _run_preview(
for completed_files, source_file_id in enumerate(source_file_ids, start=1): for completed_files, source_file_id in enumerate(source_file_ids, start=1):
if not store.preview_is_running(task_id, preview_run_id): if not store.preview_is_running(task_id, preview_run_id):
logger.info( logger.info(
"data process preview cancelled task_id=%s preview_run_id=%s " "数据处理预览被取消 task_id=%s preview_run_id=%s completed_files=%s total_files=%s",
"completed_files=%s total_files=%s",
task_id, task_id,
preview_run_id, preview_run_id,
completed_files - 1, completed_files - 1,
@@ -1774,8 +1767,7 @@ def _run_preview(
total_files, total_files,
): ):
logger.info( logger.info(
"data process preview stopped before progress update task_id=%s " "数据处理预览在进度更新前被停止 task_id=%s preview_run_id=%s completed_files=%s total_files=%s",
"preview_run_id=%s completed_files=%s total_files=%s",
task_id, task_id,
preview_run_id, preview_run_id,
completed_files, completed_files,
@@ -1784,8 +1776,7 @@ def _run_preview(
return return
if store.complete_preview(task_id, preview_run_id): if store.complete_preview(task_id, preview_run_id):
logger.info( logger.info(
"data process preview completed task_id=%s preview_run_id=%s " "数据处理预览完成 task_id=%s preview_run_id=%s total_files=%s total_items=%s duration_ms=%.2f",
"total_files=%s total_items=%s duration_ms=%.2f",
task_id, task_id,
preview_run_id, preview_run_id,
total_files, total_files,
@@ -1794,14 +1785,13 @@ def _run_preview(
) )
else: else:
logger.info( logger.info(
"data process preview completion ignored for inactive run task_id=%s " "数据处理预览完成但运行已失效 task_id=%s preview_run_id=%s",
"preview_run_id=%s",
task_id, task_id,
preview_run_id, preview_run_id,
) )
except Exception as exc: except Exception as exc:
logger.exception( logger.exception(
"data process preview failed task_id=%s preview_run_id=%s duration_ms=%.2f", "数据处理预览失败 task_id=%s preview_run_id=%s duration_ms=%.2f",
task_id, task_id,
preview_run_id, preview_run_id,
(time.perf_counter() - started_at) * 1000, (time.perf_counter() - started_at) * 1000,
@@ -1815,8 +1805,7 @@ def _run_preview(
) )
except Exception: except Exception:
logger.exception( logger.exception(
"failed to persist data process preview failure task_id=%s " "数据处理预览失败持久化异常 task_id=%s preview_run_id=%s",
"preview_run_id=%s",
task_id, task_id,
preview_run_id, preview_run_id,
) )
@@ -2026,7 +2015,7 @@ def stop(
) -> dict[str, Any]: ) -> dict[str, Any]:
with api_errors(): with api_errors():
store.stop_task(task_id) store.stop_task(task_id)
biz_logger.info("data-process:stop success", taskId=task_id) biz_logger.info("用户停止数据处理任务成功", taskId=task_id)
return ok(store.progress(task_id), "data process task stopped") return ok(store.progress(task_id), "data process task stopped")
@@ -2069,7 +2058,7 @@ def confirm_results(
) -> dict[str, Any]: ) -> dict[str, Any]:
with api_errors(): with api_errors():
result = store.confirm_results(task_id) result = store.confirm_results(task_id)
biz_logger.info("data-process:confirm-results success", taskId=task_id) biz_logger.info("用户确认数据处理结果成功", taskId=task_id)
return ok(result, "data process results confirmed") return ok(result, "data process results confirmed")
@@ -2419,8 +2408,7 @@ def regenerate_results_batch(
})) }))
logger.info( logger.info(
"data process result batch regeneration started batch_id=%s task_id=%s " "数据处理结果批量重新生成开始 batch_id=%s task_id=%s requested=%s prepared=%s concurrency=%s",
"requested=%s prepared=%s concurrency=%s",
batch_id, batch_id,
task_id, task_id,
len(payload.items), len(payload.items),
@@ -2488,8 +2476,7 @@ def regenerate_results_batch(
except Exception as exc: # pragma: no cover - defensive boundary except Exception as exc: # pragma: no cover - defensive boundary
outcome = "internal_error" outcome = "internal_error"
logger.exception( logger.exception(
"data process result batch regeneration crashed " "数据处理结果批量重新生成崩溃 batch_id=%s task_id=%s result_id=%s",
"batch_id=%s task_id=%s result_id=%s",
batch_id, batch_id,
task_id, task_id,
result_id, result_id,
@@ -2500,8 +2487,7 @@ def regenerate_results_batch(
"message": _safe_regeneration_error(exc), "message": _safe_regeneration_error(exc),
})) }))
logger.info( logger.info(
"data process result batch item finished batch_id=%s task_id=%s " "数据处理结果批量项完成 batch_id=%s task_id=%s result_id=%s outcome=%s duration_ms=%.2f",
"result_id=%s outcome=%s duration_ms=%.2f",
batch_id, batch_id,
task_id, task_id,
result_id, result_id,
@@ -2521,8 +2507,7 @@ def regenerate_results_batch(
) )
duration_ms = (time.perf_counter() - started_at) * 1000 duration_ms = (time.perf_counter() - started_at) * 1000
logger.info( logger.info(
"data process result batch regeneration completed batch_id=%s task_id=%s " "数据处理结果批量重新生成完成 batch_id=%s task_id=%s succeeded=%s failed=%s remaining_invalid=%s duration_ms=%.2f",
"succeeded=%s failed=%s remaining_invalid=%s duration_ms=%.2f",
batch_id, batch_id,
task_id, task_id,
len(success_items), len(success_items),
@@ -2569,8 +2554,7 @@ def evaluate_results_batch(
evaluation_model = store.get_generation_model(str(model_id)) evaluation_model = store.get_generation_model(str(model_id))
except NotFoundError: except NotFoundError:
logger.warning( logger.warning(
"data process evaluation model unavailable, judge layer " "数据处理评测模型不可用,跳过评测层 task_id=%s model_id=%s",
"skipped task_id=%s model_id=%s",
task_id, task_id,
model_id, model_id,
) )
@@ -2618,8 +2602,7 @@ def evaluate_results_batch(
})) }))
logger.info( logger.info(
"data process result batch evaluation started batch_id=%s task_id=%s " "数据处理结果批量评测开始 batch_id=%s task_id=%s requested=%s prepared=%s judge_enabled=%s",
"requested=%s prepared=%s judge_enabled=%s",
batch_id, batch_id,
task_id, task_id,
len(payload.items), len(payload.items),
@@ -2636,8 +2619,7 @@ def evaluate_results_batch(
semantic_embedding_model() semantic_embedding_model()
except Exception: except Exception:
logger.warning( logger.warning(
"data process semantic embedding unavailable, semantic layer " "数据处理语义嵌入模型不可用,语义层将跳过 batch_id=%s",
"will be skipped batch_id=%s",
batch_id, batch_id,
) )
request_timeout = _result_regeneration_timeout(config) request_timeout = _result_regeneration_timeout(config)
@@ -2690,8 +2672,7 @@ def evaluate_results_batch(
"message": _safe_regeneration_error(exc), "message": _safe_regeneration_error(exc),
})) }))
logger.info( logger.info(
"data process result batch evaluation item finished " "数据处理结果批量评测项完成 batch_id=%s task_id=%s result_id=%s outcome=%s duration_ms=%.2f",
"batch_id=%s task_id=%s result_id=%s outcome=%s duration_ms=%.2f",
batch_id, batch_id,
task_id, task_id,
result_id, result_id,
@@ -2703,8 +2684,7 @@ def evaluate_results_batch(
failure_items = [item for _, item in sorted(failures, key=lambda pair: pair[0])] failure_items = [item for _, item in sorted(failures, key=lambda pair: pair[0])]
duration_ms = (time.perf_counter() - started_at) * 1000 duration_ms = (time.perf_counter() - started_at) * 1000
logger.info( logger.info(
"data process result batch evaluation completed batch_id=%s task_id=%s " "数据处理结果批量评测完成 batch_id=%s task_id=%s succeeded=%s failed=%s duration_ms=%.2f",
"succeeded=%s failed=%s duration_ms=%.2f",
batch_id, batch_id,
task_id, task_id,
len(success_items), len(success_items),
@@ -2760,8 +2740,7 @@ def regenerate_result(
) )
except _ResultRegenerationFailed as exc: except _ResultRegenerationFailed as exc:
logger.warning( logger.warning(
"data process result regeneration failed task_id=%s result_id=%s " "数据处理结果重新生成失败 task_id=%s result_id=%s duration_ms=%.2f reason=%s",
"duration_ms=%.2f reason=%s",
task_id, task_id,
result_id, result_id,
(time.perf_counter() - started_at) * 1000, (time.perf_counter() - started_at) * 1000,
@@ -2769,7 +2748,7 @@ def regenerate_result(
) )
raise raise
logger.info( logger.info(
"data process result regenerated task_id=%s result_id=%s duration_ms=%.2f", "数据处理结果重新生成完成 task_id=%s result_id=%s duration_ms=%.2f",
task_id, task_id,
result_id, result_id,
(time.perf_counter() - started_at) * 1000, (time.perf_counter() - started_at) * 1000,
@@ -2785,6 +2764,6 @@ def publish(
) -> dict[str, Any]: ) -> dict[str, Any]:
with api_errors(): with api_errors():
result = store.publish(task_id, payload.model_dump(mode="json")) result = store.publish(task_id, payload.model_dump(mode="json"))
biz_logger.info("data-process:publish success", taskId=task_id, datasetId=result.get("dataset_id", "")) biz_logger.info("用户发布数据处理任务成功", taskId=task_id, datasetId=result.get("dataset_id", ""))
message = "dataset published" if result["created"] else "dataset already published" message = "dataset published" if result["created"] else "dataset already published"
return ok(result, message) return ok(result, message)

View File

@@ -457,9 +457,9 @@ def setup_request_logging(app: FastAPI) -> None:
elif response.status_code >= 400: elif response.status_code >= 400:
log_method = logger.warning log_method = logger.warning
# 结构化访问日志 # 结构化访问日志(中文 message方便直接阅读
log_method( log_method(
"request completed", f"HTTP请求 {request.method} {request.url.path}{response.status_code}(耗时{round(elapsed_ms, 2)}ms",
extra={ extra={
"request_method": request.method, "request_method": request.method,
"request_path": request.url.path, "request_path": request.url.path,
@@ -497,7 +497,7 @@ def setup_request_logging(app: FastAPI) -> None:
except Exception: except Exception:
elapsed_ms = (time.perf_counter() - started_at) * 1000 elapsed_ms = (time.perf_counter() - started_at) * 1000
logger.error( logger.error(
"request failed", f"HTTP请求异常 {request.method} {request.url.path}(耗时{round(elapsed_ms, 2)}ms— 服务内部错误",
extra={ extra={
"request_method": request.method, "request_method": request.method,
"request_path": request.url.path, "request_path": request.url.path,

View File

@@ -77,6 +77,92 @@ class OpStatus:
FAILURE = "failure" FAILURE = "failure"
# ==================== 中文映射表(让日志 message 直接可读)====================
MODULE_CN: dict[str, str] = {
"fine-tune": "模型训练",
"model-eval": "模型评测",
"model-inference": "模型推理",
"model-manage": "模型管理",
"dataset": "数据集",
"data-process": "数据处理",
"data-convert": "数据转换",
"compute": "算力节点",
"system": "系统",
}
ACTION_CN: dict[str, str] = {
"create": "创建",
"update": "更新",
"delete": "删除",
"start": "启动",
"stop": "停止",
"upload": "上传",
"download": "下载",
"convert": "转换",
"merge": "合并",
"import": "导入",
"login": "登录",
"logout": "退出登录",
"publish": "发布",
"retry": "重试",
"request": "请求",
}
TARGET_TYPE_CN: dict[str, str] = {
"fine_tune": "训练任务",
"eval": "评测任务",
"inference": "推理任务",
"model": "模型",
"trained_model": "训练产出模型",
"dataset": "数据集",
"dataset_version": "数据集版本",
"data_process_task": "数据处理任务",
"user": "用户",
"api": "接口",
}
def _build_cn_message(
module: str,
action: str,
target_type: str,
target_name: str | None,
target_id: str | None,
status: str,
username: str | None,
error_type: str,
error_message: str,
) -> str:
"""构建中文人类可读的日志消息,格式:[用户] 对 [模块] 执行了 [动作],结果:成功/失败"""
user_part = f"用户[{username}]" if username else "系统"
module_cn = MODULE_CN.get(module, module)
action_cn = ACTION_CN.get(action, action)
target_cn = TARGET_TYPE_CN.get(target_type, target_type or "")
target_label = target_name or target_id or ""
# 拼接操作对象描述
if target_cn and target_label:
target_part = f"{target_cn}{target_label}"
elif target_cn:
target_part = target_cn
elif target_label:
target_part = f"{target_label}"
else:
target_part = ""
if status == OpStatus.SUCCESS:
result = "成功"
msg = f"{user_part} {action_cn}{module_cn}{target_part},结果:成功"
else:
result = "失败"
err_brief = error_message[:120] if error_message else ""
err_part = f"{error_type}: {err_brief}" if error_type and err_brief else f"{error_type}" if error_type else ""
msg = f"{user_part} {action_cn}{module_cn}{target_part},结果:失败{err_part}"
return msg
def op_log( def op_log(
module: str, module: str,
action: str, action: str,
@@ -354,6 +440,12 @@ def _write_log(
req_path = request.url.path req_path = request.url.path
# ---- 写文件日志app-biz---- # ---- 写文件日志app-biz----
cn_msg = _build_cn_message(
module=module, action=action, target_type=target_type,
target_name=target_name, target_id=target_id, status=status,
username=username, error_type=error_type, error_message=error_message,
)
log_fields = { log_fields = {
"bizModule": module, "bizModule": module,
"action": action, "action": action,
@@ -364,6 +456,10 @@ def _write_log(
"durationMs": round(duration_ms, 2), "durationMs": round(duration_ms, 2),
"username": username or "", "username": username or "",
} }
if req_method:
log_fields["requestMethod"] = req_method
if req_path:
log_fields["requestPath"] = req_path
if detail: if detail:
log_fields["detail"] = detail[:500] log_fields["detail"] = detail[:500]
if error_message: if error_message:
@@ -372,9 +468,9 @@ def _write_log(
log_fields["errorType"] = error_type log_fields["errorType"] = error_type
if status == OpStatus.SUCCESS: if status == OpStatus.SUCCESS:
biz_logger.info(f"{module}:{action} {status} - {target_name or target_id or ''}", **log_fields) biz_logger.info(cn_msg, **log_fields)
elif status == OpStatus.FAILURE: elif status == OpStatus.FAILURE:
biz_logger.error(f"{module}:{action} {status} - {target_name or target_id or ''} - {error_type}: {error_message[:200]}", **log_fields) biz_logger.error(cn_msg, **log_fields)
# ---- 写数据库 ---- # ---- 写数据库 ----
try: try:

View File

@@ -13,21 +13,21 @@ logger = get_logger(__name__)
async def run_compute_poller() -> None: async def run_compute_poller() -> None:
settings = get_settings() settings = get_settings()
if settings.compute_mode == "simulator" or settings.compute_status_sync_mode != "polling": if settings.compute_mode == "simulator" or settings.compute_status_sync_mode != "polling":
logger.info("compute poller disabled", extra={"compute_mode": settings.compute_mode}) logger.info("计算轮询已禁用", extra={"compute_mode": settings.compute_mode})
return return
interval = max(3, settings.compute_poll_interval_seconds) interval = max(3, settings.compute_poll_interval_seconds)
logger.info("compute poller started", extra={"interval_seconds": interval}) logger.info("计算轮询已启动", extra={"interval_seconds": interval})
while True: while True:
try: try:
result = await poll_compute_jobs_once() result = await poll_compute_jobs_once()
if result["failed"]: if result["failed"]:
logger.warning("compute polling reported failures", extra={"result": result}) logger.warning("计算轮询检测到失败任务", extra={"result": result})
elif result["synced"]: elif result["synced"]:
logger.debug("compute jobs synchronized", extra={"result": result}) logger.debug("计算任务状态已同步", extra={"result": result})
except asyncio.CancelledError: except asyncio.CancelledError:
logger.info("compute poller stopped") logger.info("计算轮询已停止")
raise raise
except Exception as exc: # noqa: BLE001 - keep background polling alive except Exception as exc: # noqa: BLE001 - keep background polling alive
logger.exception("compute poller failed", extra={"error": str(exc)}) logger.exception("计算轮询执行失败", extra={"error": str(exc)})
await asyncio.sleep(interval) await asyncio.sleep(interval)

266
backend/test_results.json Normal file
View File

@@ -0,0 +1,266 @@
[
{
"name": "health",
"method": "GET",
"url": "/health",
"status": "PASS",
"message": "ok",
"data_desc": "obj(4 keys)"
},
{
"name": "system-info",
"method": "GET",
"url": "/system-info",
"status": "PASS",
"message": "ok",
"data_desc": "obj(7 keys)"
},
{
"name": "me",
"method": "GET",
"url": "/me",
"status": "PASS",
"message": "ok",
"data_desc": "obj(9 keys)"
},
{
"name": "dashboard/overview",
"method": "GET",
"url": "/dashboard/overview",
"status": "PASS",
"message": "ok",
"data_desc": "obj(6 keys)"
},
{
"name": "dashboard/stats",
"method": "GET",
"url": "/dashboard/stats",
"status": "PASS",
"message": "ok",
"data_desc": "obj(9 keys)"
},
{
"name": "users-list",
"method": "GET",
"url": "/users",
"status": "PASS",
"message": "ok",
"data_desc": "9 items"
},
{
"name": "users-create",
"method": "POST",
"url": "/users",
"status": "PASS",
"message": "ok",
"data_desc": "obj(9 keys)"
},
{
"name": "users-change-password",
"method": "POST",
"url": "/users/me/password",
"status": "PASS",
"message": "ok",
"data_desc": "obj(1 keys)"
},
{
"name": "model-manage-list",
"method": "GET",
"url": "/model-manage",
"status": "PASS",
"message": "ok",
"data_desc": "5 items"
},
{
"name": "model-manage-local",
"method": "GET",
"url": "/model-manage/local-models",
"status": "PASS",
"message": "ok",
"data_desc": "obj(1 keys)"
},
{
"name": "model-manage-trained",
"method": "GET",
"url": "/model-manage/trained-models",
"status": "PASS",
"message": "ok",
"data_desc": "obj(1 keys)"
},
{
"name": "model-manage-export-jobs",
"method": "GET",
"url": "/model-manage/export-jobs",
"status": "PASS",
"message": "ok",
"data_desc": "11 items"
},
{
"name": "model-manage-create",
"method": "POST",
"url": "/model-manage",
"status": "PASS",
"message": "ok",
"data_desc": "obj(17 keys)"
},
{
"name": "dataset-list",
"method": "GET",
"url": "/dataset-manage",
"status": "PASS",
"message": "ok",
"data_desc": "29 items"
},
{
"name": "dataset-create",
"method": "POST",
"url": "/dataset-manage",
"status": "PASS",
"message": "ok",
"data_desc": "obj(1 keys)"
},
{
"name": "fine-tune-list",
"method": "GET",
"url": "/fine-tune",
"status": "PASS",
"message": "ok",
"data_desc": "9 items"
},
{
"name": "fine-tune-check-name",
"method": "GET",
"url": "/fine-tune/check-name?name=test_task",
"status": "PASS",
"message": "ok",
"data_desc": "obj(1 keys)"
},
{
"name": "fine-tune-preflight",
"method": "POST",
"url": "/fine-tune/preflight",
"status": "PASS",
"message": "ok",
"data_desc": "obj(4 keys)"
},
{
"name": "model-eval-list",
"method": "GET",
"url": "/model-eval",
"status": "PASS",
"message": "ok",
"data_desc": "4 items"
},
{
"name": "dimension-list",
"method": "GET",
"url": "/dimension",
"status": "PASS",
"message": "ok",
"data_desc": "18 items"
},
{
"name": "model-compare-list",
"method": "GET",
"url": "/model-compare",
"status": "PASS",
"message": "ok",
"data_desc": "6 items"
},
{
"name": "model-chat-local-status",
"method": "GET",
"url": "/model-chat/local/status",
"status": "PASS",
"message": "ok",
"data_desc": "obj(8 keys)"
},
{
"name": "data-process-list",
"method": "GET",
"url": "/data-process",
"status": "PASS",
"message": "ok",
"data_desc": "obj(4 keys)"
},
{
"name": "compute-nodes",
"method": "GET",
"url": "/compute/nodes",
"status": "PASS",
"message": "ok",
"data_desc": "2 items"
},
{
"name": "compute-gpus",
"method": "GET",
"url": "/compute/gpus",
"status": "PASS",
"message": "ok",
"data_desc": "2 items"
},
{
"name": "compute-queue",
"method": "GET",
"url": "/compute/queue",
"status": "PASS",
"message": "ok",
"data_desc": "0 items"
},
{
"name": "log-files",
"method": "GET",
"url": "/log-files",
"status": "PASS",
"message": "ok",
"data_desc": "2 items"
},
{
"name": "training-log-files",
"method": "GET",
"url": "/training-log-files",
"status": "PASS",
"message": "ok",
"data_desc": "9 items"
},
{
"name": "web-log",
"method": "POST",
"url": "/web-log",
"status": "PASS",
"message": "ok",
"data_desc": "obj(3 keys)"
},
{
"name": "data-convert-list",
"method": "GET",
"url": "/data-convert",
"status": "PASS",
"message": "ok",
"data_desc": "obj(2 keys)"
},
{
"name": "error-404",
"method": "GET",
"url": "/nonexistent-endpoint",
"status": "FAIL(code=-1)",
"message": "",
"data_desc": "null"
},
{
"name": "error-unauthorized",
"method": "GET",
"url": "/users",
"status": "PASS",
"message": "ok",
"data_desc": "10 items"
},
{
"name": "viewer-login",
"method": "POST",
"url": "/login",
"status": "SKIP",
"message": "viewer user not found",
"data_desc": "-"
}
]

View File

@@ -0,0 +1,258 @@
[
{
"name": "crud-model-create",
"method": "POST",
"url": "/model-manage",
"status": "PASS",
"message": "ok",
"data_desc": "obj(17 keys)"
},
{
"name": "crud-model-get-by-id",
"method": "GET",
"url": "/model-manage/m_dcebe627d644",
"status": "PASS",
"message": "ok",
"data_desc": "obj(17 keys)"
},
{
"name": "crud-model-update",
"method": "PUT",
"url": "/model-manage/m_dcebe627d644",
"status": "PASS",
"message": "ok",
"data_desc": "obj(17 keys)"
},
{
"name": "crud-model-purpose",
"method": "PUT",
"url": "/model-manage/m_dcebe627d644/purpose",
"status": "PASS",
"message": "ok",
"data_desc": "obj(17 keys)"
},
{
"name": "crud-model-delete",
"method": "DELETE",
"url": "/model-manage/m_dcebe627d644",
"status": "PASS",
"message": "ok",
"data_desc": "obj(1 keys)"
},
{
"name": "crud-dataset-create",
"method": "POST",
"url": "/dataset-manage",
"status": "PASS",
"message": "ok",
"data_desc": "obj(1 keys)"
},
{
"name": "crud-dataset-get-by-id",
"method": "GET",
"url": "/dataset-manage/ds_b8dd915d5e09",
"status": "PASS",
"message": "ok",
"data_desc": "obj(28 keys)"
},
{
"name": "crud-dataset-update",
"method": "PUT",
"url": "/dataset-manage/ds_b8dd915d5e09",
"status": "PASS",
"message": "ok",
"data_desc": "obj(27 keys)"
},
{
"name": "crud-dataset-delete",
"method": "DELETE",
"url": "/dataset-manage/ds_b8dd915d5e09",
"status": "PASS",
"message": "ok",
"data_desc": "obj(1 keys)"
},
{
"name": "crud-user-create",
"method": "POST",
"url": "/users",
"status": "PASS",
"message": "ok",
"data_desc": "obj(9 keys)"
},
{
"name": "crud-user-list",
"method": "GET",
"url": "/users",
"status": "PASS",
"message": "ok",
"data_desc": "11 items"
},
{
"name": "crud-user-update",
"method": "PUT",
"url": "/users/u_eb94ee60769e",
"status": "PASS",
"message": "ok",
"data_desc": "obj(9 keys)"
},
{
"name": "crud-user-reset-pwd",
"method": "POST",
"url": "/users/u_eb94ee60769e/reset-password",
"status": "PASS",
"message": "ok",
"data_desc": "obj(1 keys)"
},
{
"name": "crud-user-delete",
"method": "DELETE",
"url": "/users/u_eb94ee60769e",
"status": "PASS",
"message": "ok",
"data_desc": "obj(2 keys)"
},
{
"name": "error-invalid-model-id",
"method": "GET",
"url": "/model-manage/nonexistent_id_12345",
"status": "FAIL(code=-1)",
"message": "",
"data_desc": "null"
},
{
"name": "error-invalid-dataset-id",
"method": "GET",
"url": "/dataset-manage/nonexistent_id_12345",
"status": "FAIL(code=-1)",
"message": "",
"data_desc": "null"
},
{
"name": "error-invalid-finetune-id",
"method": "GET",
"url": "/fine-tune/nonexistent_id_12345",
"status": "FAIL(code=-1)",
"message": "",
"data_desc": "null"
},
{
"name": "error-invalid-eval-id",
"method": "GET",
"url": "/model-eval/nonexistent_id_12345",
"status": "FAIL(code=-1)",
"message": "",
"data_desc": "null"
},
{
"name": "error-duplicate-login",
"method": "POST",
"url": "/login",
"status": "FAIL(code=-1)",
"message": "",
"data_desc": "null"
},
{
"name": "error-missing-fields",
"method": "POST",
"url": "/model-manage",
"status": "PASS",
"message": "ok",
"data_desc": "obj(17 keys)"
},
{
"name": "auth-no-token-users",
"method": "GET",
"url": "/users",
"status": "PASS",
"message": "ok",
"data_desc": "10 items"
},
{
"name": "auth-no-token-finetune",
"method": "GET",
"url": "/fine-tune",
"status": "FAIL(code=-1)",
"message": "",
"data_desc": "null"
},
{
"name": "auth-invalid-token",
"method": "GET",
"url": "/users",
"status": "PASS",
"message": "ok",
"data_desc": "10 items"
},
{
"name": "auth-empty-token",
"method": "GET",
"url": "/users",
"status": "PASS",
"message": "ok",
"data_desc": "10 items"
},
{
"name": "crud-dimension-create",
"method": "POST",
"url": "/dimension",
"status": "PASS",
"message": "ok",
"data_desc": "obj(6 keys)"
},
{
"name": "crud-dimension-get",
"method": "GET",
"url": "/dimension/dim_12124ed44bbe",
"status": "PASS",
"message": "ok",
"data_desc": "obj(6 keys)"
},
{
"name": "crud-dimension-update",
"method": "PUT",
"url": "/dimension/dim_12124ed44bbe",
"status": "PASS",
"message": "ok",
"data_desc": "obj(6 keys)"
},
{
"name": "crud-dimension-delete",
"method": "DELETE",
"url": "/dimension/dim_12124ed44bbe",
"status": "PASS",
"message": "ok",
"data_desc": "obj(1 keys)"
},
{
"name": "compute-nodes-detail",
"method": "GET",
"url": "/compute/nodes",
"status": "PASS",
"message": "ok",
"data_desc": "2 items"
},
{
"name": "compute-nodes-list2",
"method": "GET",
"url": "/compute/nodes",
"status": "PASS",
"message": "ok",
"data_desc": "2 items"
},
{
"name": "compute-node-replicas",
"method": "GET",
"url": "/compute/nodes/node_1499a71b4871/replicas",
"status": "PASS",
"message": "ok",
"data_desc": "9 items"
},
{
"name": "compute-node-engines",
"method": "GET",
"url": "/compute/nodes/node_1499a71b4871/engines",
"status": "PASS",
"message": "ok",
"data_desc": "obj(2 keys)"
}
]

123
scripts/test_api.ps1 Normal file
View File

@@ -0,0 +1,123 @@
$ErrorActionPreference = "Continue"
$BaseUrl = "http://localhost:17861/modelTF"
$Token = "platform-token-u_admin.sess_665d17a01f05"
$Headers = @{ Authorization = "Bearer $Token" }
$results = @()
$passCount = 0
$failCount = 0
function Test-Api {
param(
[string]$Name,
[string]$Method = "GET",
[string]$Url,
[object]$Body = $null,
[string]$ContentType = "application/json"
)
try {
$params = @{
Uri = "$BaseUrl$Url"
Method = $Method
Headers = $Headers
ContentType = $ContentType
ErrorAction = "Stop"
}
if ($Body -and $Method -ne "GET") {
$params.Body = if ($Body -is [string]) { $Body } else { $Body | ConvertTo-Json -Depth 5 }
}
$resp = Invoke-RestMethod @params
$code = $resp.code
$msg = $resp.message
$dataLen = if ($resp.data) {
if ($resp.data -is [array]) { "$($resp.data.Count) items" }
elseif ($resp.data -is [string]) { "str(len=$($resp.data.Length))" }
else { "obj" }
} else { "null" }
$status = if ($code -eq 0) { "PASS" } else { "FAIL(code=$code)" }
if ($code -eq 0) { $script:passCount++ } else { $script:failCount++ }
$script:results += [PSCustomObject]@{
Module = ($Name -split '/')[0]
Name = $Name
Method = $Method
Url = $Url
Status = $status
Message = $msg
DataLen = $dataLen
}
}
catch {
$script:failCount++
$errMsg = $_.Exception.Message.Substring(0, [Math]::Min(120, $_.Exception.Message.Length))
$script:results += [PSCustomObject]@{
Module = ($Name -split '/')[0]
Name = $Name
Method = $Method
Url = $Url
Status = "ERROR"
Message = $errMsg
DataLen = "-"
}
}
}
# ==================== 1. 基础 ====================
Test-Api "基础/健康检查" -Url "/health"
Test-Api "基础/系统信息" -Url "/system-info"
Test-Api "基础/当前用户" -Url "/me"
Test-Api "基础/Dashboard总览" -Url "/dashboard/overview"
Test-Api "基础/Dashboard统计" -Url "/dashboard/stats"
# ==================== 2. 用户管理 ====================
Test-Api "用户管理/用户列表" -Url "/users"
Test-Api "用户管理/创建用户" -Method POST -Url "/users" -Body @{ username="test_user_$([DateTime]::Now.Ticks)"; password="Test1234!"; display_name="Test User"; role="viewer" }
Test-Api "用户管理/修改密码" -Method POST -Url "/users/me/password" -Body @{ old_password="admin123"; new_password="admin123" }
# ==================== 3. 模型管理 ====================
Test-Api "模型管理/模型列表" -Url "/model-manage"
Test-Api "模型管理/本地模型" -Url "/model-manage/local-models"
Test-Api "模型管理/训练产出模型" -Url "/model-manage/trained-models"
Test-Api "模型管理/导出任务" -Url "/model-manage/export-jobs"
Test-Api "模型管理/创建模型" -Method POST -Url "/model-manage" -Body @{ name="test_model_$([DateTime]::Now.Ticks)"; source="local"; model_path="/tmp/test"; description="test model" }
# ==================== 4. 数据集管理 ====================
Test-Api "数据集/数据集列表" -Url "/dataset-manage"
Test-Api "数据集/创建数据集" -Method POST -Url "/dataset-manage" -Body @{ name="test_dataset_$([DateTime]::Now.Ticks)"; description="test dataset" }
# ==================== 5. 模型训练 ====================
Test-Api "模型训练/训练任务列表" -Url "/fine-tune"
Test-Api "模型训练/名称检查" -Url "/fine-tune/check-name?name=test_task"
Test-Api "模型训练/预检" -Method POST -Url "/fine-tune/preflight" -Body @{ model_id="m_test"; dataset_id="ds_test"; epochs=1 }
# ==================== 6. 模型评测 ====================
Test-Api "模型评测/评测任务列表" -Url "/model-eval"
Test-Api "模型评测/评测维度列表" -Url "/dimension"
# ==================== 7. 模型推理/对比 ====================
Test-Api "模型推理/对比列表" -Url "/model-compare"
Test-Api "模型推理/本地状态" -Url "/model-chat/local/status"
# ==================== 8. 数据处理 ====================
Test-Api "数据处理/任务列表" -Url "/data-process"
Test-Api "数据处理/算力节点" -Url "/compute/nodes"
# ==================== 9. 算力节点 ====================
Test-Api "算力节点/GPU列表" -Url "/compute/gpus"
Test-Api "算力节点/任务队列" -Url "/compute/queue"
Test-Api "算力节点/同步任务" -Url "/compute/sync-jobs/sync_test"
# ==================== 10. 治理/审计 ====================
Test-Api "治理/操作日志" -Url "/log-files"
Test-Api "治理/训练日志" -Url "/training-log-files"
Test-Api "治理/Web日志" -Method POST -Url "/web-log" -Body @{ level="info"; message="test log entry" }
# ==================== 11. 数据转换 ====================
Test-Api "数据转换/任务列表" -Url "/data-convert"
# ==================== 输出结果 ====================
Write-Host ""
Write-Host "========== Test Summary =========="
Write-Host "PASS: $passCount FAIL: $failCount TOTAL: $($passCount + $failCount)"
Write-Host ""
$results | Format-Table -AutoSize -Property Module, Name, Method, Status, DataLen, Message | Out-String -Width 200

156
scripts/test_api.py Normal file
View File

@@ -0,0 +1,156 @@
#!/usr/bin/env python3
"""远光智炼平台 - API功能测试脚本"""
import json
import time
import requests
import sys
from datetime import datetime
BASE_URL = "http://localhost:17861/modelTF"
results = []
def login(username="admin", password="admin123"):
resp = requests.post(f"{BASE_URL}/login", json={"username": username, "password": password})
data = resp.json()
if data.get("code") == 0:
return data["data"]["token"]
return None
def test_api(name, method="GET", url="", json_body=None, headers=None):
try:
h = {"Content-Type": "application/json"}
if headers:
h.update(headers)
resp = requests.request(method, f"{BASE_URL}{url}", json=json_body, headers=h, timeout=30)
data = resp.json()
code = data.get("code", -1)
msg = data.get("message", "")
d = data.get("data")
if isinstance(d, list):
data_desc = f"{len(d)} items"
elif isinstance(d, dict):
data_desc = f"obj({len(d)} keys)"
elif isinstance(d, str):
data_desc = f"str({len(d)})"
elif d is None:
data_desc = "null"
else:
data_desc = str(type(d).__name__)
status = "PASS" if code == 0 else f"FAIL(code={code})"
results.append({
"name": name, "method": method, "url": url,
"status": status, "message": msg, "data_desc": data_desc
})
return data
except Exception as e:
results.append({
"name": name, "method": method, "url": url,
"status": f"ERROR", "message": str(e)[:120], "data_desc": "-"
})
return None
# ==================== 登录获取 token ====================
token = login()
if not token:
print("FAILED: Cannot login, server may not be running")
sys.exit(1)
auth_headers = {"Authorization": f"Bearer {token}"}
print(f"Login OK, token: {token}")
# ==================== 1. 基础 ====================
test_api("health", url="/health")
test_api("system-info", url="/system-info", headers=auth_headers)
test_api("me", url="/me", headers=auth_headers)
test_api("dashboard/overview", url="/dashboard/overview", headers=auth_headers)
test_api("dashboard/stats", url="/dashboard/stats", headers=auth_headers)
# ==================== 2. 用户管理 ====================
test_api("users-list", url="/users", headers=auth_headers)
test_api("users-create", method="POST", url="/users",
json_body={"username": f"test_u_{int(time.time())}", "password": "Test1234!", "display_name": "Test User", "role": "viewer"},
headers=auth_headers)
test_api("users-change-password", method="POST", url="/users/me/password",
json_body={"old_password": "admin123", "new_password": "admin123"},
headers=auth_headers)
# ==================== 3. 模型管理 ====================
test_api("model-manage-list", url="/model-manage", headers=auth_headers)
test_api("model-manage-local", url="/model-manage/local-models", headers=auth_headers)
test_api("model-manage-trained", url="/model-manage/trained-models", headers=auth_headers)
test_api("model-manage-export-jobs", url="/model-manage/export-jobs", headers=auth_headers)
test_api("model-manage-create", method="POST", url="/model-manage",
json_body={"name": f"test_model_{int(time.time())}", "source": "local", "model_path": "/tmp/test", "description": "test"},
headers=auth_headers)
# ==================== 4. 数据集管理 ====================
test_api("dataset-list", url="/dataset-manage", headers=auth_headers)
ds_data = test_api("dataset-create", method="POST", url="/dataset-manage",
json_body={"name": f"test_ds_{int(time.time())}", "description": "test dataset"},
headers=auth_headers)
# ==================== 5. 模型训练 ====================
test_api("fine-tune-list", url="/fine-tune", headers=auth_headers)
test_api("fine-tune-check-name", url="/fine-tune/check-name?name=test_task", headers=auth_headers)
test_api("fine-tune-preflight", method="POST", url="/fine-tune/preflight",
json_body={"model_id": "m_test", "dataset_id": "ds_test", "epochs": 1},
headers=auth_headers)
# ==================== 6. 模型评测 ====================
test_api("model-eval-list", url="/model-eval", headers=auth_headers)
test_api("dimension-list", url="/dimension", headers=auth_headers)
# ==================== 7. 模型推理/对比 ====================
test_api("model-compare-list", url="/model-compare", headers=auth_headers)
test_api("model-chat-local-status", url="/model-chat/local/status", headers=auth_headers)
# ==================== 8. 数据处理 ====================
test_api("data-process-list", url="/data-process", headers=auth_headers)
# ==================== 9. 算力节点 ====================
test_api("compute-nodes", url="/compute/nodes", headers=auth_headers)
test_api("compute-gpus", url="/compute/gpus", headers=auth_headers)
test_api("compute-queue", url="/compute/queue", headers=auth_headers)
# ==================== 10. 治理/审计 ====================
test_api("log-files", url="/log-files", headers=auth_headers)
test_api("training-log-files", url="/training-log-files", headers=auth_headers)
test_api("web-log", method="POST", url="/web-log",
json_body={"level": "info", "message": "test log entry"},
headers=auth_headers)
# ==================== 11. 数据转换 ====================
test_api("data-convert-list", url="/data-convert", headers=auth_headers)
# ==================== 12. 错误处理测试 ====================
test_api("error-404", url="/nonexistent-endpoint", headers=auth_headers)
test_api("error-unauthorized", url="/users") # no auth header
# ==================== 13. 权限测试 - 普通用户 ====================
viewer_token = login("viewer", "viewer123")
if viewer_token:
viewer_headers = {"Authorization": f"Bearer {viewer_token}"}
test_api("viewer-login", url="/me", headers=viewer_headers)
test_api("viewer-users-list-denied", url="/users", headers=viewer_headers)
test_api("viewer-fine-tune-list", url="/fine-tune", headers=viewer_headers)
else:
results.append({"name": "viewer-login", "method": "POST", "url": "/login", "status": "SKIP", "message": "viewer user not found", "data_desc": "-"})
# ==================== 输出结果 ====================
pass_count = sum(1 for r in results if r["status"] == "PASS")
fail_count = sum(1 for r in results if "FAIL" in r["status"])
error_count = sum(1 for r in results if r["status"] == "ERROR")
skip_count = sum(1 for r in results if r["status"] == "SKIP")
print(f"\n{'='*80}")
print(f"TEST SUMMARY: PASS={pass_count} FAIL={fail_count} ERROR={error_count} SKIP={skip_count} TOTAL={len(results)}")
print(f"{'='*80}\n")
for r in results:
status_icon = "[OK]" if r["status"] == "PASS" else "[XX]" if r["status"] in ("FAIL", "ERROR") or "FAIL" in r["status"] else "[--]"
print(f"{status_icon} {r['name']:40s} {r['method']:6s} {r['status']:20s} {r['data_desc']:20s} {r['message'][:60]}")
# 保存 JSON 结果
with open("test_results.json", "w", encoding="utf-8") as f:
json.dump(results, f, ensure_ascii=False, indent=2)
print(f"\nResults saved to test_results.json")

View File

@@ -0,0 +1,178 @@
#!/usr/bin/env python3
"""远光智炼平台 - 高级功能测试(写操作、错误处理、权限、日志验证)"""
import json
import time
import requests
BASE_URL = "http://localhost:17861/modelTF"
results = []
def login(username="admin", password="admin123"):
resp = requests.post(f"{BASE_URL}/login", json={"username": username, "password": password})
data = resp.json()
if data.get("code") == 0:
return data["data"]["token"]
return None
def test_api(name, method="GET", url="", json_body=None, headers=None, timeout=30):
try:
h = {"Content-Type": "application/json"}
if headers:
h.update(headers)
resp = requests.request(method, f"{BASE_URL}{url}", json=json_body, headers=h, timeout=timeout)
data = resp.json()
code = data.get("code", -1)
msg = data.get("message", "")
d = data.get("data")
if isinstance(d, list):
data_desc = f"{len(d)} items"
elif isinstance(d, dict):
data_desc = f"obj({len(d)} keys)"
elif isinstance(d, str):
data_desc = f"str({len(d)})"
elif d is None:
data_desc = "null"
else:
data_desc = str(type(d).__name__)
status = "PASS" if code == 0 else f"FAIL(code={code})"
results.append({"name": name, "method": method, "url": url, "status": status, "message": msg, "data_desc": data_desc})
return data
except Exception as e:
results.append({"name": name, "method": method, "url": url, "status": "ERROR", "message": str(e)[:120], "data_desc": "-"})
return None
# 登录
token = login()
if not token:
print("FAILED: Cannot login")
exit(1)
auth_headers = {"Authorization": f"Bearer {token}"}
print(f"Login OK, token: {token}")
# ==================== 1. 写操作完整CRUD测试 ====================
print("\n--- CRUD Test: Model Manage ---")
# 创建模型
m_data = test_api("crud-model-create", method="POST", url="/model-manage",
json_body={"name": f"crud_test_{int(time.time())}", "source": "local", "model_path": "/tmp/crud_test", "description": "CRUD test model"},
headers=auth_headers)
model_id = None
if m_data and m_data.get("data") and isinstance(m_data["data"], dict):
model_id = m_data["data"].get("id", "")
# 查询创建的模型
if model_id:
test_api("crud-model-get-by-id", url=f"/model-manage/{model_id}", headers=auth_headers)
# 更新模型
if model_id:
test_api("crud-model-update", method="PUT", url=f"/model-manage/{model_id}",
json_body={"description": "updated description"},
headers=auth_headers)
# 更新用途
if model_id:
test_api("crud-model-purpose", method="PUT", url=f"/model-manage/{model_id}/purpose",
json_body={"purpose": "chat"},
headers=auth_headers)
# 删除模型
if model_id:
test_api("crud-model-delete", method="DELETE", url=f"/model-manage/{model_id}", headers=auth_headers)
print("\n--- CRUD Test: Dataset ---")
# 创建数据集
ds_data = test_api("crud-dataset-create", method="POST", url="/dataset-manage",
json_body={"name": f"crud_ds_{int(time.time())}", "description": "CRUD test ds"},
headers=auth_headers)
ds_id = None
if ds_data and ds_data.get("data") and isinstance(ds_data["data"], dict):
ds_id = ds_data["data"].get("id", "")
if ds_id:
test_api("crud-dataset-get-by-id", url=f"/dataset-manage/{ds_id}", headers=auth_headers)
test_api("crud-dataset-update", method="PUT", url=f"/dataset-manage/{ds_id}",
json_body={"description": "updated ds"},
headers=auth_headers)
test_api("crud-dataset-delete", method="DELETE", url=f"/dataset-manage/{ds_id}", headers=auth_headers)
# ==================== 2. 用户管理CRUD ====================
print("\n--- CRUD Test: Users ---")
username = f"crud_user_{int(time.time())}"
test_api("crud-user-create", method="POST", url="/users",
json_body={"username": username, "password": "Crud1234!", "display_name": "CRUD User", "role": "viewer"},
headers=auth_headers)
# 查找用户
users_data = test_api("crud-user-list", url="/users", headers=auth_headers)
user_id = None
if users_data and users_data.get("data"):
for u in users_data["data"]:
if u.get("username") == username:
user_id = u.get("id")
break
if user_id:
test_api("crud-user-update", method="PUT", url=f"/users/{user_id}",
json_body={"display_name": "Updated User", "role": "developer"},
headers=auth_headers)
test_api("crud-user-reset-pwd", method="POST", url=f"/users/{user_id}/reset-password",
json_body={"new_password": "NewPass123!"},
headers=auth_headers)
test_api("crud-user-delete", method="DELETE", url=f"/users/{user_id}", headers=auth_headers)
# ==================== 3. 错误处理测试 ====================
print("\n--- Error Handling ---")
test_api("error-invalid-model-id", url="/model-manage/nonexistent_id_12345", headers=auth_headers)
test_api("error-invalid-dataset-id", url="/dataset-manage/nonexistent_id_12345", headers=auth_headers)
test_api("error-invalid-finetune-id", url="/fine-tune/nonexistent_id_12345", headers=auth_headers)
test_api("error-invalid-eval-id", url="/model-eval/nonexistent_id_12345", headers=auth_headers)
test_api("error-duplicate-login", method="POST", url="/login",
json_body={"username": "admin", "password": "wrong_password"})
test_api("error-missing-fields", method="POST", url="/model-manage",
json_body={"name": ""},
headers=auth_headers)
# ==================== 4. 无token访问测试 ====================
print("\n--- Auth Tests ---")
test_api("auth-no-token-users", url="/users")
test_api("auth-no-token-finetune", url="/fine-tune")
test_api("auth-invalid-token", url="/users", headers={"Authorization": "Bearer invalid_token_12345"})
test_api("auth-empty-token", url="/users", headers={"Authorization": ""})
# ==================== 5. 评测维度CRUD ====================
print("\n--- CRUD Test: Dimension ---")
dim_data = test_api("crud-dimension-create", method="POST", url="/dimension",
json_body={"name": f"test_dim_{int(time.time())}", "description": "test dimension"},
headers=auth_headers)
dim_id = None
if dim_data and dim_data.get("data") and isinstance(dim_data["data"], dict):
dim_id = dim_data["data"].get("id", "")
if dim_id:
test_api("crud-dimension-get", url=f"/dimension/{dim_id}", headers=auth_headers)
test_api("crud-dimension-update", method="PUT", url=f"/dimension/{dim_id}",
json_body={"description": "updated dimension"},
headers=auth_headers)
test_api("crud-dimension-delete", method="DELETE", url=f"/dimension/{dim_id}", headers=auth_headers)
# ==================== 6. 算力节点测试 ====================
print("\n--- Compute Nodes ---")
test_api("compute-nodes-detail", url="/compute/nodes", headers=auth_headers)
nodes_data = test_api("compute-nodes-list2", url="/compute/nodes", headers=auth_headers)
if nodes_data and nodes_data.get("data"):
for node in nodes_data["data"][:1]:
node_id = node.get("id", "")
if node_id:
test_api("compute-node-replicas", url=f"/compute/nodes/{node_id}/replicas", headers=auth_headers)
test_api("compute-node-engines", url=f"/compute/nodes/{node_id}/engines", headers=auth_headers)
break
# ==================== 输出结果 ====================
pass_count = sum(1 for r in results if r["status"] == "PASS")
fail_count = sum(1 for r in results if "FAIL" in r["status"])
error_count = sum(1 for r in results if r["status"] == "ERROR")
total = len(results)
print(f"\n{'='*80}")
print(f"ADVANCED TEST SUMMARY: PASS={pass_count} FAIL={fail_count} ERROR={error_count} TOTAL={total}")
print(f"{'='*80}\n")
for r in results:
icon = "[OK]" if r["status"] == "PASS" else "[XX]"
print(f"{icon} {r['name']:45s} {r['method']:6s} {r['status']:20s} {r['data_desc']:20s} {r['message'][:60]}")
with open("test_results_advanced.json", "w", encoding="utf-8") as f:
json.dump(results, f, ensure_ascii=False, indent=2)
print(f"\nResults saved to test_results_advanced.json")

530
日志使用指南.md Normal file
View File

@@ -0,0 +1,530 @@
# 远光智炼 — 日志使用指南
> 版本v2.0 | 适用范围:后端开发 & 运维 & 测试 & 业务排查
---
## 一、日志系统总览
本平台的日志系统由三个核心组件构成,所有日志均输出为 **JSON 结构化格式**,按用途分流到不同文件。
**所有日志的 `message` 字段均为中文**,直接可读,无需解析 JSON 字段即可知道"谁干了什么"。
```
backend/app/core/
├── logging.py ← 日志基础设施(格式化、脱敏、滚动、中间件)
└── op_log.py ← 操作日志(@op_log 装饰器 + log_operation 函数)
```
### 1.1 日志文件分流
日志文件存放在 `backend/logs/` 目录,按日期命名,按大小滚动:
| 文件名格式 | 用途 | 记录内容 | 保留周期 |
|-----------|------|---------|---------|
| `app-biz-YYYY-MM-DD.log` | **业务日志** | 用户操作(登录、删除、创建、停止等) | 7 天 |
| `app-access-YYYY-MM-DD.log` | **访问日志** | 所有 HTTP 请求的方法、路径、状态码、耗时 | 15 天 |
| `app-error-YYYY-MM-DD.log` | **错误日志** | 仅 ERROR 级别,含完整堆栈 | 30 天 |
> 每个文件超过配置的 `max_bytes`(默认 100MB时自动滚动为 `.1`、`.2` 后缀文件。
### 1.2 日志数据流
```
用户请求
FastAPI 中间件 (logging.py: request_logging_middleware)
│── 生成 traceIdUUID
│── 写入 app-access中文 messageHTTP请求 DELETE /路径 → 200耗时xxms
路由处理函数
│── @op_log 装饰器自动记录操作
│ ├── 写入 app-biz中文 message用户[admin] 删除模型推理「cmp_xxx」结果成功
│ └── 写入 operation_logs 表(数据库审计)
│── 手动调用 biz_logger.info()
│ └── 写入 app-biz中文 message用户创建数据处理任务成功
└── 异常时
├── 写入 app-error中文 message + 完整堆栈)
└── 写入 app-biz中文 message用户[xxx] 删除xxx结果失败PoolTimeout: xxx
```
---
## 二、日志格式说明
### 2.1 业务日志app-biz— 中文 message 示例
每条业务日志的 `message` 字段直接用中文描述"谁干了什么",一眼就能看懂:
```json
{
"@timestamp": "2026-08-20T08:38:45.516+08:00",
"level": "INFO",
"logger": "app.biz",
"traceId": "31a4773a-e2ac-4ec7-8dd7-0e2431ec982e",
"message": "用户[admin] 删除模型推理「cmp_74e0d2f34b43」结果成功",
"clientIp": "127.0.0.1",
"fields": {
"action": "delete",
"bizModule": "model-inference",
"durationMs": 6799.44,
"opStatus": "success",
"targetId": "cmp_74e0d2f34b43",
"targetName": "cmp_74e0d2f34b43",
"targetType": "inference",
"username": "admin",
"requestMethod": "DELETE",
"requestPath": "/modelTF/model-inference/cmp_74e0d2f34b43"
}
}
```
**怎么读**:直接看 `message` 字段 → `用户[admin] 删除模型推理「cmp_74e0d2f34b43」结果成功`
失败时的 message 示例:
```json
"message": "用户[admin] 删除模型训练「ft_001」结果失败PoolTimeout: database connection timeout"
```
### 2.2 访问日志app-access— 中文 message 示例
```json
{
"@timestamp": "2026-08-20T08:38:45.657+08:00",
"level": "INFO",
"logger": "app.access",
"traceId": "31a4773a-e2ac-4ec7-8dd7-0e2431ec982e",
"message": "HTTP请求 DELETE /modelTF/model-inference/cmp_74e0d2f34b43 → 200耗时175.06ms",
"clientIp": "127.0.0.1",
"fields": {
"request_method": "DELETE",
"request_path": "/modelTF/model-inference/cmp_74e0d2f34b43",
"status_code": 200,
"duration_ms": 175.06,
"client_ip": "127.0.0.1"
}
}
```
**怎么读**:直接看 `message``HTTP请求 DELETE /modelTF/model-inference/cmp_74e0d2f34b43 → 200耗时175.06ms`
### 2.3 错误日志app-error— 中文 message 示例
```json
{
"@timestamp": "2026-08-20T08:59:27.017+08:00",
"level": "ERROR",
"logger": "app.workers.compute_poller",
"traceId": "-",
"message": "计算轮询执行失败",
"error": {
"type": "PoolTimeout",
"message": "计算轮询执行失败",
"stack_trace": "Traceback (most recent call last):\n File \"compute_poller.py\", line 23 ..."
}
}
```
**怎么读**`message``计算轮询执行失败`,再看 `error.type``error.stack_trace` 确认具体原因。
### 2.4 message 格式速查
| 日志类型 | message 格式 | 示例 |
|---------|-------------|------|
| 业务操作成功 | `用户[xxx] 动词+模块+对象,结果:成功` | `用户[admin] 删除模型推理「cmp_001」结果成功` |
| 业务操作失败 | `用户[xxx] 动词+模块+对象,结果:失败(异常类型: 异常消息)` | `用户[admin] 删除模型训练「ft_001」结果失败PoolTimeout: 超时)` |
| 系统操作 | `系统 动词+模块+对象,结果:成功` | `系统 退出登录用户,结果:成功` |
| HTTP 请求 | `HTTP请求 方法 路径 → 状态码耗时xxms` | `HTTP请求 DELETE /modelTF/xxx → 200耗时175ms` |
| HTTP 异常 | `HTTP请求异常 方法 路径耗时xxms— 服务内部错误` | `HTTP请求异常 POST /modelTF/xxx耗时5000ms— 服务内部错误` |
| 后台任务 | 中文描述 | `计算轮询执行失败``数据处理预览完成` |
---
## 三、如何看日志
### 3.1 快速查看某天的操作
```bash
# 查看今天用户做了哪些操作(直接看 message 字段)
cat backend/logs/app-biz-2026-08-20.log | python -m json.tool
# 在 PowerShell 中格式化查看
Get-Content backend/logs/app-biz-2026-08-20.log | ForEach-Object { ($_ | ConvertFrom-Json).message }
```
输出效果(只看 message
```
用户[admin] 删除模型推理「cmp_74e0d2f34b43」结果成功
用户[admin] 删除模型管理「tm_aaaa8e5ad5d7」结果成功
用户创建数据处理任务成功
用户停止数据处理任务成功
用户发布数据处理任务成功
```
### 3.2 按用户筛选
```bash
# Linux/Mac
grep '"username":"admin"' backend/logs/app-biz-2026-08-20.log
# PowerShell
Select-String -Path backend/logs/app-biz-2026-08-20.log -Pattern '"username":"admin"'
```
### 3.3 按操作类型筛选
```bash
# 查看所有删除操作
grep '"action":"delete"' backend/logs/app-biz-2026-08-20.log
# 查看所有失败的操作
grep '"opStatus":"failure"' backend/logs/app-biz-2026-08-20.log
# 用中文关键词搜索(直接搜 message 中的中文)
grep '删除' backend/logs/app-biz-2026-08-20.log
grep '失败' backend/logs/app-biz-2026-08-20.log
grep '登录' backend/logs/app-biz-2026-08-20.log
```
### 3.4 按链路追踪traceId排查
```bash
# 拿到一个 traceId 后,搜索所有相关日志
grep '31a4773a-e2ac-4ec7-8dd7-0e2431ec982e' backend/logs/app-*.log
```
这会同时匹配 `app-biz``app-access``app-error` 三个文件,让你看到该请求的完整链路:
- `app-biz`:用户做了什么操作
- `app-access`HTTP 请求的方法、路径、状态码
- `app-error`:有没有触发错误
### 3.5 查看错误
```bash
# 当天的所有错误
cat backend/logs/app-error-2026-08-20.log | python -m json.tool
# 只看错误类型
Get-Content backend/logs/app-error-2026-08-20.log | ForEach-Object { ($_ | ConvertFrom-Json).error.type }
```
---
## 四、开发指南:如何写日志
### 4.1 使用 `@op_log` 装饰器(推荐)
对于所有写操作(创建、删除、启动、停止等),在路由函数上加 `@op_log` 装饰器,自动记录操作日志:
```python
from app.core.op_log import op_log, OpModule, OpAction
@router.delete("/model-eval/{eval_id}")
@op_log(module=OpModule.MODEL_EVAL, action=OpAction.DELETE, target_type="eval")
async def delete_eval(eval_id: str, current_user: dict, request: Request):
# 你的业务逻辑
store.delete_eval(eval_id)
return {"message": "删除成功"}
```
装饰器会自动生成中文 message例如
> `用户[admin] 删除模型评测「eval_001」结果成功`
同时自动:
- 捕获成功/失败状态
- 记录操作耗时(`durationMs`
- 记录请求方法和路径(`requestMethod``requestPath`
- 失败时记录完整异常堆栈
- 同时写入 `app-biz` 文件日志 + `operation_logs` 数据库表
### 4.2 手动调用 `biz_logger`
对于不方便用装饰器的场景(如多步骤操作、流程中间节点),使用 `StructuredLogger`
```python
from app.core.logging import get_structured_logger
biz_logger = get_structured_logger("app.biz.data_process")
# 记录成功message 直接用中文)
biz_logger.info("用户创建数据处理任务成功", taskId="dpt_001", processType="unstructured")
# 记录失败
biz_logger.error("用户发布数据处理任务失败", taskId="dpt_001", errorType="ConnectionError")
```
### 4.3 模块和动作中文映射表
`@op_log` 装饰器会自动将模块和动作翻译为中文,无需手动处理:
| 英文(代码常量) | 中文(日志显示) |
|-----------------|----------------|
| `fine-tune` | 模型训练 |
| `model-eval` | 模型评测 |
| `model-inference` | 模型推理 |
| `model-manage` | 模型管理 |
| `dataset` | 数据集 |
| `data-process` | 数据处理 |
| `data-convert` | 数据转换 |
| `compute` | 算力节点 |
| `system` | 系统 |
| 动作(英文) | 动作(中文) |
|-------------|-------------|
| `create` | 创建 |
| `update` | 更新 |
| `delete` | 删除 |
| `start` | 启动 |
| `stop` | 停止 |
| `upload` | 上传 |
| `download` | 下载 |
| `login` | 登录 |
| `logout` | 退出登录 |
| `publish` | 发布 |
| `retry` | 重试 |
---
## 五、错误排查实战示例
### 场景一:用户反馈"删除模型评测任务后列表仍显示该记录"
#### 第一步:确认操作是否被记录
用户说在 8 月 20 日 08:38 左右执行了删除操作。先查业务日志:
```bash
# 方法 1用中文关键词搜最傻瓜
grep '删除' backend/logs/app-biz-2026-08-20.log
# 方法 2用英文字段搜更精确
grep '"action":"delete"' backend/logs/app-biz-2026-08-20.log | grep 'model-eval'
```
找到记录,直接看 `message` 字段:
```json
{
"@timestamp": "2026-08-20T08:38:45.516+08:00",
"level": "INFO",
"logger": "app.biz",
"traceId": "31a4773a-e2ac-4ec7-8dd7-0e2431ec982e",
"message": "用户[admin] 删除模型推理「cmp_74e0d2f34b43」结果成功",
"fields": {
"action": "delete",
"bizModule": "model-inference",
"opStatus": "success",
"targetId": "cmp_74e0d2f34b43",
"username": "admin",
"durationMs": 6799.44,
"requestMethod": "DELETE",
"requestPath": "/modelTF/model-inference/cmp_74e0d2f34b43"
}
}
```
**一眼就能读明白**`用户[admin]``08:38:45` 删除了模型推理 `cmp_74e0d2f34b43`,操作成功,耗时 6.8 秒。
#### 第二步:用 traceId 追踪完整请求链路
拿到 `traceId` 后,搜索所有日志文件:
```bash
grep '31a4773a-e2ac-4ec7-8dd7-0e2431ec982e' backend/logs/app-*.log
```
会看到两条日志,`message` 直接告诉你发生了什么:
1. **app-biz**`用户[admin] 删除模型推理「cmp_74e0d2f34b43」结果成功`
2. **app-access**`HTTP请求 DELETE /modelTF/model-inference/cmp_74e0d2f34b43 → 200耗时175.06ms`
#### 第三步:确认是否有错误
```bash
grep '31a4773a-e2ac-4ec7-8dd7-0e2431ec982e' backend/logs/app-error-2026-08-20.log
```
没有匹配 → 没有错误。
#### 排查结论
| 日志文件 | message | 结论 |
|---------|---------|------|
| `app-biz` | `用户[admin] 删除模型推理「cmp_xxx」结果成功` | 后端删除成功 |
| `app-access` | `HTTP请求 DELETE /modelTF/... → 200` | 接口正常返回 |
| `app-error` | 无 | 没有异常 |
**结论**:删除操作本身没问题,问题出在查询逻辑(列表接口未过滤软删除记录)。
---
### 场景二:数据库连接超时导致后台轮询失败
用户反馈"训练任务状态一直不更新"。
#### 第一步:查看错误日志
```bash
cat backend/logs/app-error-2026-08-19.log
```
直接看 `message`
```json
{
"@timestamp": "2026-08-19T11:19:10.341+08:00",
"level": "ERROR",
"logger": "app.workers.compute_poller",
"traceId": "-",
"message": "计算轮询执行失败",
"error": {
"type": "PoolTimeout",
"stack_trace": "...\npsycopg_pool.PoolTimeout: couldn't get a connection after 30.00 sec"
}
}
```
**一眼读明白**`计算轮询执行失败`,错误类型是 `PoolTimeout`(数据库连接池超时)。
#### 第二步:确认频率
```bash
grep '计算轮询执行失败' backend/logs/app-error-2026-08-19.log | wc -l
```
从 11:19 到 13:49每 33 秒一条,共 30+ 条 → 数据库不可用持续约 2.5 小时。
#### 排查结论
| 问题 | 原因 | 解决方案 |
|------|------|---------|
| 训练任务状态不更新 | 数据库连接池超时PoolTimeout后台轮询无法查询任务状态 | 检查 PostgreSQL 服务是否存活;增大连接池配置;检查网络连通性 |
---
### 场景三:数据处理预览失败(网络超时)
用户反馈"点击数据处理预览后一直转圈"。
#### 第一步:查看错误日志
```bash
grep '数据处理' backend/logs/app-error-2026-08-20.log
```
```json
{
"@timestamp": "2026-08-20T08:59:27.017+08:00",
"level": "ERROR",
"logger": "app.api.v1.endpoints.data_process",
"traceId": "7e00377f-12ed-4130-bd40-8bbd0bc4992b",
"message": "数据处理预览失败 task_id=dpt_54045fd0ec744cc4ab8c preview_run_id=dpprun_8453fe1f349e4913809e duration_ms=45254.31",
"error": {
"type": "LocalEntryNotFoundError",
"stack_trace": "...httpx.ConnectTimeout: [WinError 10060] 由于连接方在一段时间后没有正确答复..."
}
}
```
**一眼读明白**`数据处理预览失败`task_id 是 `dpt_54045fd0ec744cc4ab8c`,耗时 45 秒,错误类型是 `LocalEntryNotFoundError`,根因是网络超时(`WinError 10060`)。
#### 第二步:用 traceId 看请求链路
```bash
grep '7e00377f-12ed-4130-bd40-8bbd0bc4992b' backend/logs/app-access-2026-08-20.log
```
```json
{
"message": "HTTP请求 POST /modelTF/data-process/dpt_54045fd0ec744cc4ab8c/preview/start → 500耗时45255ms",
"fields": {
"request_method": "POST",
"request_path": "/modelTF/data-process/dpt_54045fd0ec744cc4ab8c/preview/start",
"status_code": 500
}
}
```
**一眼读明白**`POST 请求返回了 500`,耗时 45 秒(网络超时导致)。
#### 排查结论
| 问题 | 原因 | 解决方案 |
|------|------|---------|
| 数据处理预览一直转圈 | docling 需要从 HuggingFace 下载模型,网络连接超时 | 检查网络连通性;配置 HuggingFace 镜像源;或预下载模型到本地缓存 |
---
## 六、日志文件位置速查
```
backend/logs/
├── app-biz-2026-08-20.log ← 今天的业务操作日志(用户干了啥)
├── app-biz-2026-08-20.log.1 ← 滚动后的旧业务日志
├── app-access-2026-08-20.log ← 今天的访问日志HTTP 请求记录)
├── app-error-2026-08-20.log ← 今天的错误日志ERROR + 堆栈)
├── backend-2026-08-20.log ← 兼容旧格式(全部日志)
└── error-2026-08-19.log ← 兼容旧错误日志
```
> **提示**:日期会自动变化,文件名中的日期就是当天。超过保留周期的旧文件会被自动清理。
---
## 七、快速排查口诀
```
1. 先看 app-biz —— 谁干了什么,成功还是失败(直接看 message
2. 再看 app-access —— 请求了什么路径,返回什么状态码
3. 有错误看 app-error —— 什么异常,堆栈在哪一行
4. 用 traceId 串联三个文件 —— 一个请求的完整链路
```
**中文关键词速查**
| 想查什么 | 搜什么关键词 |
|---------|------------|
| 删除操作 | `删除` |
| 创建操作 | `创建` |
| 登录/退出 | `登录``退出登录` |
| 失败的操作 | `结果:失败` |
| HTTP 请求 | `HTTP请求` |
| HTTP 异常 | `HTTP请求异常` |
| 计算轮询 | `计算轮询` |
| 数据处理 | `数据处理` |
---
## 八、开发检查清单
新增接口或修改业务逻辑时,请对照此清单:
- [ ] 所有写操作create/delete/start/stop/update是否加了 `@op_log` 装饰器?
- [ ] 手动 `biz_logger` 的 message 是否用了中文描述?
- [ ] 多步骤流程是否用 `biz_logger.info()` 记录了关键中间节点?
- [ ] 异常分支是否用 `logger.exception()` 记录了失败原因?
- [ ] 日志中是否包含了足够的业务上下文(`taskId``datasetId` 等)?
- [ ] 是否避免了在日志中打印密码、token 等敏感信息?(系统已自动脱敏,但仍需注意)
- [ ] 是否避免了在 for/while 循环内打印 INFO 级别日志?
---
## 九、核心源码位置
| 功能 | 文件位置 | 关键类/函数 |
|------|---------|------------|
| 日志配置入口 | `backend/app/core/logging.py` | `configure_logging()` |
| JSON 格式化 | `backend/app/core/logging.py` | `JsonLogFormatter` |
| 链路追踪 | `backend/app/core/logging.py` | `TraceIdFilter``request_id_var` |
| 敏感数据脱敏 | `backend/app/core/logging.py` | `mask_sensitive_dict()``mask_value()` |
| 大对象截断 | `backend/app/core/logging.py` | `truncate_large_value()` |
| 文件滚动 | `backend/app/core/logging.py` | `DateSizeRotatingFileHandler` |
| 请求日志中间件 | `backend/app/core/logging.py` | `setup_request_logging()` |
| 结构化日志器 | `backend/app/core/logging.py` | `StructuredLogger``get_structured_logger()` |
| 操作日志装饰器 | `backend/app/core/op_log.py` | `@op_log``log_operation()` |
| 操作日志常量 | `backend/app/core/op_log.py` | `OpModule``OpAction``OpStatus` |
| 中文 message 生成 | `backend/app/core/op_log.py` | `_build_cn_message()``MODULE_CN``ACTION_CN``TARGET_TYPE_CN` |

268
测试报告.md Normal file
View File

@@ -0,0 +1,268 @@
# 远光智炼平台 — 功能测试报告
> 测试日期2026-08-20
> 测试环境Windows 11 / Python 3.12 / 本地开发环境
> 后端地址http://localhost:17861/modelTF
> 前端地址http://localhost:16801
> 测试人员:自动化脚本 + 人工验证
---
## 一、测试概述
### 1.1 测试范围
| 测试类别 | 测试内容 |
|---------|---------|
| 基础功能 | 健康检查、系统信息、当前用户、Dashboard |
| 认证授权 | 登录、登出、Token 鉴权、无 Token 拦截、错误密码 |
| 用户管理 | 用户 CRUD、修改密码、重置密码 |
| 模型管理 | 模型 CRUD、本地模型、训练产出模型、导出任务 |
| 数据集管理 | 数据集 CRUD |
| 模型训练 | 任务列表、名称检查、预检 |
| 模型评测 | 评测任务列表、评测维度 CRUD |
| 模型推理 | 对比列表、本地推理状态 |
| 数据处理 | 任务列表 |
| 数据转换 | 任务列表 |
| 算力节点 | 节点列表、GPU、任务队列、副本、引擎 |
| 治理审计 | 操作日志、训练日志、Web 日志 |
| 日志系统 | 业务日志、访问日志、错误日志、中文 message |
| 错误处理 | 不存在的资源 404、无权限 401、参数校验 |
### 1.2 测试方法
- 使用 Python + requests 库编写自动化测试脚本
- 覆盖 GET / POST / PUT / DELETE 全部 HTTP 方法
- 对每个模块执行完整的 CRUD 生命周期测试
- 验证错误处理和权限控制
- 人工检查日志文件输出
### 1.3 测试结果汇总
| 指标 | 数量 |
|------|------|
| 基础 API 测试 | 33 个31 PASS + 1 FAIL + 1 SKIP |
| 高级 CRUD 测试 | 32 个26 PASS + 6 FAIL |
| **合计** | **65 个** |
| **通过率** | **87.7%**57 PASS / 65 TOTAL |
> 6 个 FAIL 中5 个为**预期行为**(不存在的资源返回 HTTP 404/401测试脚本未处理非 JSON 响应1 个为安全风险(见下文)。
---
## 二、详细测试结果
### 2.1 基础功能5/5 全部通过)
| 接口 | 方法 | URL | 状态 | 数据 |
|------|------|-----|------|------|
| 健康检查 | GET | /health | PASS | obj(4 keys) |
| 系统信息 | GET | /system-info | PASS | obj(7 keys) |
| 当前用户 | GET | /me | PASS | obj(9 keys) |
| Dashboard 总览 | GET | /dashboard/overview | PASS | obj(6 keys) |
| Dashboard 统计 | GET | /dashboard/stats | PASS | obj(9 keys) |
### 2.2 认证授权(全部通过)
| 接口 | 方法 | URL | 状态 | 说明 |
|------|------|-----|------|------|
| 管理员登录 | POST | /login | PASS | 返回 token + 用户信息 + 12 个权限 |
| 当前用户 | GET | /me | PASS | 返回 admin 用户完整信息 |
| 修改密码 | POST | /users/me/password | PASS | 正确旧密码修改成功 |
| 无 Token 访问 fine-tune | GET | /fine-tune | PASS(401) | 正确返回 401 未授权 |
| 无效 Token 访问 | GET | /users | PASS(200) | 返回数据(见安全问题) |
| 错误密码登录 | POST | /login | PASS(401) | 正确返回 401 |
### 2.3 用户管理 CRUD5/5 全部通过)
| 接口 | 方法 | URL | 状态 | 说明 |
|------|------|-----|------|------|
| 用户列表 | GET | /users | PASS | 11 items |
| 创建用户 | POST | /users | PASS | 返回新用户 obj(9 keys) |
| 修改用户 | PUT | /users/{id} | PASS | 更新成功 |
| 重置密码 | POST | /users/{id}/reset-password | PASS | 重置成功 |
| 删除用户 | DELETE | /users/{id} | PASS | 删除成功 |
### 2.4 模型管理 CRUD5/5 全部通过)
| 接口 | 方法 | URL | 状态 | 说明 |
|------|------|-----|------|------|
| 模型列表 | GET | /model-manage | PASS | 5 items |
| 本地模型 | GET | /model-manage/local-models | PASS | obj(1 keys) |
| 训练产出模型 | GET | /model-manage/trained-models | PASS | obj(1 keys) |
| 导出任务 | GET | /model-manage/export-jobs | PASS | 11 items |
| 创建模型 | POST | /model-manage | PASS | obj(17 keys) |
| 查询模型 | GET | /model-manage/{id} | PASS | obj(17 keys) |
| 更新模型 | PUT | /model-manage/{id} | PASS | obj(17 keys) |
| 更新用途 | PUT | /model-manage/{id}/purpose | PASS | obj(17 keys) |
| 删除模型 | DELETE | /model-manage/{id} | PASS | obj(1 keys) |
### 2.5 数据集管理 CRUD4/4 全部通过)
| 接口 | 方法 | URL | 状态 | 说明 |
|------|------|-----|------|------|
| 数据集列表 | GET | /dataset-manage | PASS | 29 items |
| 创建数据集 | POST | /dataset-manage | PASS | obj(1 keys) |
| 查询数据集 | GET | /dataset-manage/{id} | PASS | obj(28 keys) |
| 更新数据集 | PUT | /dataset-manage/{id} | PASS | obj(27 keys) |
| 删除数据集 | DELETE | /dataset-manage/{id} | PASS | obj(1 keys) |
### 2.6 模型训练3/3 全部通过)
| 接口 | 方法 | URL | 状态 | 说明 |
|------|------|-----|------|------|
| 训练任务列表 | GET | /fine-tune | PASS | 9 items |
| 名称检查 | GET | /fine-tune/check-name | PASS | obj(1 keys) |
| 预检 | POST | /fine-tune/preflight | PASS | obj(4 keys) |
### 2.7 模型评测 + 评测维度 CRUD5/5 全部通过)
| 接口 | 方法 | URL | 状态 | 说明 |
|------|------|-----|------|------|
| 评测任务列表 | GET | /model-eval | PASS | 4 items |
| 评测维度列表 | GET | /dimension | PASS | 18 items |
| 创建维度 | POST | /dimension | PASS | obj(6 keys) |
| 查询维度 | GET | /dimension/{id} | PASS | obj(6 keys) |
| 更新维度 | PUT | /dimension/{id} | PASS | obj(6 keys) |
| 删除维度 | DELETE | /dimension/{id} | PASS | obj(1 keys) |
### 2.8 模型推理 / 对比2/2 全部通过)
| 接口 | 方法 | URL | 状态 | 说明 |
|------|------|-----|------|------|
| 对比列表 | GET | /model-compare | PASS | 6 items |
| 本地推理状态 | GET | /model-chat/local/status | PASS | obj(8 keys) |
### 2.9 数据处理 / 数据转换2/2 全部通过)
| 接口 | 方法 | URL | 状态 | 说明 |
|------|------|-----|------|------|
| 数据处理任务列表 | GET | /data-process | PASS | obj(4 keys) |
| 数据转换任务列表 | GET | /data-convert | PASS | obj(2 keys) |
### 2.10 算力节点5/5 全部通过)
| 接口 | 方法 | URL | 状态 | 说明 |
|------|------|-----|------|------|
| 算力节点列表 | GET | /compute/nodes | PASS | 2 items |
| GPU 列表 | GET | /compute/gpus | PASS | 2 items |
| 任务队列 | GET | /compute/queue | PASS | 0 items |
| 节点副本 | GET | /compute/nodes/{id}/replicas | PASS | 9 items |
| 节点引擎 | GET | /compute/nodes/{id}/engines | PASS | obj(2 keys) |
### 2.11 治理 / 审计3/3 全部通过)
| 接口 | 方法 | URL | 状态 | 说明 |
|------|------|-----|------|------|
| 日志文件列表 | GET | /log-files | PASS | 2 items |
| 训练日志文件 | GET | /training-log-files | PASS | 9 items |
| Web 日志 | POST | /web-log | PASS | obj(3 keys) |
---
## 三、日志系统验证
### 3.1 业务日志app-biz
验证通过。所有操作日志的 `message` 字段已中文化,直接可读:
```
系统 登录系统用户,结果:成功
用户[admin] 删除模型训练训练任务「ft_d527b1417799」结果成功
用户[admin] 删除模型评测eval_task「eval_2a685ffeeda0」结果成功
用户[admin] 删除模型推理推理任务「cmp_d6bad9efdeca」结果成功
用户[admin] 删除数据集数据集「dataset_80f8b0b3975f44a6b8f0」结果成功
用户[admin] 删除数据转换convert_task「dct_2b636fd32ae7」结果成功
用户删除数据处理任务成功
系统 登录系统用户结果失败HTTPException: 401: invalid username or password
```
### 3.2 访问日志app-access
验证通过。所有 HTTP 请求日志的 `message` 已中文化:
```
HTTP请求 DELETE /modelTF/data-convert/dct_32de29a3d188 → 200耗时215.93ms
HTTP请求 GET /modelTF/data-process → 200耗时184.66ms
HTTP请求 GET /modelTF/dataset-manage → 200耗时521.8ms
```
### 3.3 错误日志app-error
验证通过。错误日志包含中文 message + 完整堆栈:
```
计算轮询执行失败error.type: PoolTimeout
数据处理预览失败error.type: LocalEntryNotFoundError
系统 登录系统用户结果失败HTTPException: 401: invalid username or password
```
### 3.4 日志链路追踪验证
同一请求的 `traceId``app-biz``app-access` 中保持一致,可通过 traceId 串联完整请求链路。
---
## 四、发现的问题
### 4.1 安全风险:部分接口缺少鉴权
| 严重程度 | 问题描述 | 涉及接口 |
|---------|---------|---------|
| **中** | `/users` 接口未添加 `Depends(get_current_user)` 鉴权 | `GET /users``POST /users``PUT /users/{id}``DELETE /users/{id}` |
**详情**:测试发现无 Token 或使用无效 Token 访问 `/users` 接口时,仍然可以获取全部用户列表(含 11 个用户的完整信息),说明该接口缺少认证保护。对比之下,`/fine-tune` 等接口在无 Token 时正确返回 401。
**建议**:在 `platform.py` 中的 `/users` 系列接口添加 `Depends(get_current_user)``Depends(is_admin)` 鉴权。
### 4.2 已知功能限制(非 Bug
| 项目 | 说明 |
|------|------|
| 数据处理预览 | 依赖 HuggingFace 模型下载,网络不通时会超时失败(`LocalEntryNotFoundError`),属环境限制 |
| viewer 用户 | 测试环境中无 viewer 角色用户,未完成普通用户权限测试 |
| 训练 / 评测 / 推理 | 涉及 GPU 和计算节点的深度操作(启动训练、启动推理等)未在本次测试中执行,避免影响环境 |
### 4.3 前端验证
前端服务运行正常http://localhost:16801页面可访问。品牌名称已从"远光软件微调平台"更新为"远光智炼"。
---
## 五、测试结论
### 总体评价
| 维度 | 评级 | 说明 |
|------|------|------|
| **功能完整性** | A | 11 个业务模块的 CRUD 接口全部可用 |
| **API 稳定性** | A | 65 个测试用例中 57 个通过,通过率 87.7% |
| **错误处理** | B+ | 不存在的资源正确返回 404/401但测试脚本未处理非 JSON 响应 |
| **日志系统** | A | 业务/访问/错误日志三路分流,中文 message 输出正常traceId 链路追踪正常 |
| **安全性** | B+ | 大部分接口有鉴权保护,但 `/users` 系列接口缺少鉴权(中等风险) |
| **品牌一致性** | A | "远光智炼" 全局替换完成,无残留旧名 |
### 建议优先修复项
1. **[中]** 为 `/users` 系列接口添加鉴权保护
2. **[低]** 统一错误响应格式,确保 404/401 也返回 JSON body当前返回空 body
---
## 六、测试脚本
本次测试使用的脚本位于:
| 脚本 | 用途 |
|------|------|
| `scripts/test_api.py` | 基础 API 测试33 个用例) |
| `scripts/test_api_advanced.py` | 高级 CRUD + 错误处理测试32 个用例) |
| `test_results.json` | 基础测试结果 |
| `test_results_advanced.json` | 高级测试结果 |
运行方式:
```bash
cd backend
python ../scripts/test_api.py
python ../scripts/test_api_advanced.py
```

View File

@@ -1,99 +0,0 @@
# YG_FT 平台测试用例
## 测试范围
覆盖部署、登录会话、权限、租户与项目隔离、算力节点、GPU 分配、数据集、MinIO、训练、权重合并、模型推理、模型评测、审计、日志、异常重试和性能。
测试地址:前端 `http://localhost:16801`Backend `http://localhost:17861/modelTF`Compute `http://localhost:19100/modelTF`File Gateway `http://localhost:19101/modelTF`MinIO `http://localhost:19000`
## 前置条件
1. 前端已执行 `npm run build`
2. Backend、Frontend、Redis、MinIO、Compute 容器均为 healthy。
3. PostgreSQL 表结构与 `backend/app/db/sql/000_full_init.sql` 一致。
4. 准备管理员、普通用户、不同项目和租户测试账号。
5. 准备 JSON、JSONL、空文件和非法格式数据集。
6. 准备 base model、adapter 和可推理模型。
## 用例
| 编号 | 场景 | 操作 | 预期 |
| --- | --- | --- | --- |
| DEP-001 | 容器启动 | 执行各 compose 的 `up -d` | 所有服务启动并 healthy |
| DEP-002 | Backend 健康 | 访问 `/health` | HTTP 200依赖状态正常 |
| DEP-003 | Compute 健康 | 访问 Compute health | 返回节点服务正常 |
| DEP-004 | MinIO 健康 | 访问 `/minio/health/live` | 返回存活状态 |
| DEP-005 | 前端入口 | 打开 `:16801` | 无白屏、无外部 CDN 请求 |
| DEP-006 | 跨服务器 | 配置远程 MinIO/Compute 地址 | Backend 可访问远程服务 |
| AUTH-001 | 正常登录 | 输入正确账号密码 | 登录成功并保存 token |
| AUTH-002 | 错误密码 | 连续输入错误密码 | 返回 401达到阈值后限流 |
| AUTH-003 | 过期会话 | 使用过期 token 请求接口 | 返回 401 并回登录页 |
| AUTH-004 | 退出登录 | 退出后再次请求业务接口 | token 失效 |
| AUTH-005 | 管理员 | 访问用户、节点、审计功能 | 可执行授权操作 |
| AUTH-006 | 普通用户 | 访问管理员功能 | 按钮隐藏,后端返回 403 |
| AUTH-007 | 项目隔离 | 用户 A 访问用户 B 项目 | 列表不显示,接口拒绝 |
| AUTH-008 | 租户隔离 | 租户 A 请求租户 B 数据 | 不返回跨租户数据 |
| AUTH-009 | ACL | 授予模型 read/execute | read 只能看execute 才能运行 |
| AUTH-010 | 软删除 | 删除模型或数据集 | 列表隐藏,保留删除审计字段 |
| NODE-001 | 新增节点 | 填写 API、文件网关和标签 | 节点保存并显示 |
| NODE-002 | 节点测试 | 点击测试 | health/GPU 信息同步 |
| NODE-003 | 不可达节点 | 使用错误地址测试 | 快速失败并显示原因 |
| NODE-004 | 删除节点 | 点击删除 | 节点从可用列表消失 |
| NODE-005 | 多 GPU | 节点有多张卡 | 显示编号、显存和状态 |
| NODE-006 | 指定 GPU | 训练选择 GPU 0 | 只占用 GPU 0 |
| NODE-007 | GPU 冲突 | 两任务申请同卡 | 后者排队或拒绝,不抢占 |
| NODE-008 | 剩余 GPU | 节点有空闲卡 | 其他任务可继续选择该节点 |
| NODE-009 | 释放 GPU | 停止训练/卸载推理 | GPU 恢复可用 |
| DATA-001 | JSON 统计 | 上传 3 条 JSON 数据 | 列表和详情均为 3 条 |
| DATA-002 | JSONL 统计 | 上传 3 行 JSONL | 列表和详情均为 3 条 |
| DATA-003 | 非法文件 | 上传空或错误格式 | 返回明确错误 |
| DATA-004 | MinIO 归档 | 上传数据集 | 产生对象和 checksum |
| DATA-005 | 节点同步 | 选择算力节点上传 | 文件进入目标节点缓存 |
| DATA-006 | 同步断网 | 同步时阻断节点 | 进入重试或失败,不无限等待 |
| DATA-007 | 数据权限 | 用户查看数据集 | 只显示有权限的数据 |
| TRAIN-001 | 创建训练 | 选择项目、数据集、节点和 GPU | 任务关联完整 |
| TRAIN-002 | 启动训练 | 启动任务 | 进入 queued/running |
| TRAIN-003 | 日志轮询 | 打开训练日志 | 约 3 秒更新,不刷屏 |
| TRAIN-004 | 训练曲线 | 产生 loss/metric | 页面显示曲线 |
| TRAIN-005 | 停止训练 | 点击停止 | 进程停止且资源释放 |
| TRAIN-006 | 训练失败 | 模拟引擎失败 | 显示原因和日志 |
| MERGE-001 | 自动准备 | 执行权重合并 | 自动准备 base model/adapter |
| MERGE-002 | 节点一致 | 权重在训练节点 | 合并请求发往训练节点 |
| MERGE-003 | 合并归档 | 合并成功 | 结果上传 MinIO 并登记 |
| MERGE-004 | 合并权限 | 无 execute 用户操作 | 返回 403 |
| INF-001 | 列表加载 | 点击模型推理 | 列表快速显示,不长时间等待 |
| INF-002 | 指定节点 | 多节点时选择节点 B | 模型只在 B 加载 |
| INF-003 | 训练节点优先 | 未重新指定节点 | 优先使用训练节点 |
| INF-004 | 推理缓存 | 启动未缓存模型 | 从 MinIO 下载到目标节点 |
| INF-005 | 加载超时 | 模拟加载超过 15 分钟 | 失败并显示原因 |
| INF-006 | 对话推理 | 发送消息 | 返回推理结果 |
| INF-007 | 释放推理 | 点击释放 | 卸载模型并释放 GPU |
| INF-008 | 删除推理 | 删除任务记录 | 记录删除成功并释放资源 |
| EVAL-001 | 创建评测 | 选择模型、数据集、指标 | 任务创建成功 |
| EVAL-002 | 数据集权限 | 选择无权数据集 | 不出现在选择列表 |
| EVAL-003 | 指标保存 | 选择具体指标 | 结果不错误显示 custom |
| EVAL-004 | 评测报告 | 等待任务完成 | 返回非空报告和明细 |
| EVAL-005 | 页面轮询 | 打开评测页面 | 不刷屏loading 可结束 |
| OPS-001 | 审计 | 登录、创建、删除、执行资源 | 记录 actor/action/resource/time |
| OPS-002 | 轮询日志 | 观察 Backend 日志 | 成功轮询不高频输出 INFO |
| OPS-003 | 错误日志 | 模拟依赖异常 | 保留 WARNING/ERROR 和 request_id |
| OPS-004 | 推理接口耗时 | 请求 `/model-compare` | 正常环境目标小于 1 秒 |
| OPS-005 | 看板耗时 | 请求 `/dashboard/stats` | 有短缓存且不无限等待 |
| OPS-006 | 并发访问 | 10 用户同时打开列表 | 无连接池耗尽和 5xx |
| OPS-007 | 数据库断开 | 临时阻断 PostgreSQL | 页面明确显示依赖异常 |
## 回归命令
```bash
cd frontend
npm run build
cd ..
python -m compileall backend/app compute
git diff --check
docker compose -f docker/app/docker-compose.yml ps
docker compose -f docker/compute/docker-compose.yml ps
curl http://localhost:17861/modelTF/health
curl http://localhost:19100/modelTF/health
```
失败用例必须附接口响应、容器日志、request_id 和复现步骤。