feat: 更新后端平台模块、Compute引擎、前端组件及构建产物

- 更新 backend 平台 API、platform_store、compute_gateway sync
- 更新 compute agent/engine/adapter 及 API
- 更新 Docker 部署配置(app/compute)
- 新增 frontend/src/utils/ 工具模块
- 新增 scripts/ops_diagnostics.py 运维诊断脚本
- 新增 docs/2026-07-23-development-summary.md 开发总结
- 重构 frontend/dist 构建产物(新 hash)
- 更新前端多个视图组件及 API 模块

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
wuyongtao
2026-07-23 19:32:42 +08:00
parent f04dc479bb
commit b28cfbc6fa
193 changed files with 2647 additions and 424 deletions

View File

@@ -32,16 +32,26 @@ def _task_for_compute_job(job_id: str) -> dict[str, Any] | None:
return next((task for task in get_platform_store().tasks() if task.get("compute_job_id") == job_id), None)
def _node_for_compute_job_record(job_id: str) -> dict[str, Any] | None:
store = get_platform_store()
try:
record = store.compute_job(job_id)
except KeyError:
return None
return next((node for node in store.compute_nodes() if node["id"] == record.get("node_id")), None)
async def _submit_fine_tune_task(store: Any, payload: dict[str, Any]) -> dict[str, Any]:
task_id = str(payload.get("task_id") or payload.get("id") or "")
if task_id and get_settings().compute_mode != "simulator":
try:
preflight = await _fine_tune_preflight(store, task_id, payload, validate=True)
preflight = await _fine_tune_preflight(store, task_id, payload, validate=True, sync_resources=True)
except Exception as exc: # noqa: BLE001 - task has not entered running state yet
raise RuntimeError(f"preflight failed: {exc}") from exc
if not preflight["valid"]:
errors = "; ".join(preflight.get("errors") or ["preflight failed"])
raise RuntimeError(f"preflight failed: {errors}")
payload = {**payload, "compute_node_id": preflight["node"]["id"]}
task = store.start_task(payload)
if get_settings().compute_mode == "simulator":
return task
@@ -55,8 +65,20 @@ async def _fine_tune_preflight(
task_id: str,
payload: dict[str, Any] | None = None,
validate: bool = True,
sync_resources: bool = False,
) -> dict[str, Any]:
node, job_payload = store.prepare_compute_job_payload(task_id, payload or {})
sync_results: list[dict[str, Any]] = []
sync_errors: list[str] = []
if sync_resources and get_settings().compute_mode != "simulator":
try:
sync_results = await _sync_training_dataset_to_compute_node(
store,
node,
str(job_payload.get("train_dataset_id") or ""),
)
except Exception as exc: # noqa: BLE001 - return as preflight error for page visibility
sync_errors.append(str(exc))
if get_settings().compute_mode == "simulator":
preview = {
"valid": True,
@@ -73,6 +95,7 @@ async def _fine_tune_preflight(
client = ComputeNodeClient(node["api_base_url"])
preview = await (client.validate_job(job_payload) if validate else client.preview_job(job_payload))
errors = list(preview.get("errors") or [])
errors.extend(sync_errors)
warnings = list(preview.get("warnings") or [])
if not node.get("enabled"):
errors.append(f"compute node disabled: {node.get('code')}")
@@ -92,6 +115,7 @@ async def _fine_tune_preflight(
},
"job_payload": job_payload,
"preview": preview,
"sync_results": sync_results,
}
@@ -197,6 +221,21 @@ async def delete_trained_model(model_id: str, type: str = Query(default="merged"
return ok({"deleted": model_id, "type": type})
@router.get("/model-manage/trained-models/{model_id}/artifacts")
async def trained_model_artifacts(model_id: str) -> dict[str, Any]:
return ok(get_platform_store().model_artifacts(model_id))
@router.get("/model-manage/trained-models/{model_id}/lineage")
async def trained_model_lineage(model_id: str) -> dict[str, Any]:
return ok(get_platform_store().model_lineage(model_id))
@router.get("/model-manage/export-jobs")
async def model_export_jobs(trained_model_id: str | None = Query(default=None)) -> dict[str, Any]:
return ok(get_platform_store().model_export_jobs(trained_model_id))
@router.get("/model-manage/name/{name}")
async def model_by_name(name: str) -> dict[str, Any]:
try:
@@ -254,7 +293,51 @@ async def delete_model(model_id: str) -> dict[str, Any]:
@router.post("/model-manage/merge")
async def merge_model(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
return ok({"job_id": f"merge_{uuid.uuid4().hex[:12]}", "status": "queued", **payload})
store = get_platform_store()
trained_model_id = str(payload.get("trained_model_id") or payload.get("model_id") or payload.get("model_name") or "")
trained_model = next(
(
item
for item in store.trained_models()
if trained_model_id and (item["id"] == trained_model_id or item["name"] == trained_model_id)
),
None,
)
base_model_path = payload.get("base_model_path") or (trained_model and trained_model.get("base_model_path"))
adapter_path = payload.get("adapter_path") or payload.get("adapter_name_or_path") or (trained_model and trained_model.get("merged_path"))
if not base_model_path:
raise fail(400, "base_model_path is required")
if not adapter_path:
raise fail(400, "adapter_path is required")
node = store.schedule_node({**payload, "gpus": payload.get("gpus") or []})
health = node.get("health_detail") or {}
output_root = str(health.get("output_root") or f"{node['data_root'].rstrip('/')}/outputs")
output_name = str(payload.get("output_model_name") or payload.get("merged_model_name") or f"{trained_model_id or 'model'}-merged")
output_dir = str(payload.get("output_dir") or f"{output_root.rstrip('/')}/{output_name}")
job_payload = {
**payload,
"id": str(payload.get("job_id") or f"merge_{uuid.uuid4().hex[:12]}"),
"name": output_name,
"engine": "merge",
"base_model": base_model_path,
"model_name_or_path": base_model_path,
"adapter_name_or_path": adapter_path,
"output_dir": output_dir,
"template": payload.get("template", "qwen"),
"train_method": payload.get("train_method", "lora"),
"gpus": payload.get("gpus") or [],
"trained_model_id": trained_model["id"] if trained_model else trained_model_id,
"model_name": trained_model["name"] if trained_model else payload.get("model_name"),
}
if get_settings().compute_mode == "simulator":
job = {"id": job_payload["id"], "status": "queued", "progress": 10, "command": [], "output_dir": output_dir}
else:
client = ComputeNodeClient(node["api_base_url"])
preview = await client.validate_job(job_payload)
if not preview.get("valid", False):
raise fail(409, "; ".join(preview.get("errors") or ["merge preflight failed"]))
job = await client.create_job(job_payload)
return ok(store.record_model_merge_job(node, job_payload, job, trained_model["id"] if trained_model else trained_model_id))
@router.get("/dataset-manage/preview/{file_id}")
@@ -364,6 +447,47 @@ async def _sync_dataset_file_to_compute_nodes(
return results
async def _sync_training_dataset_to_compute_node(
store: Any,
node: dict[str, Any],
dataset_id: str,
) -> list[dict[str, Any]]:
if not dataset_id:
raise RuntimeError("train_dataset_id is required")
files = store.training_dataset_files(dataset_id)
if not files:
raise RuntimeError(f"dataset has no uploaded file: {dataset_id}")
client = ComputeNodeClient(node["api_base_url"])
results: list[dict[str, Any]] = []
for item in files:
target_name = Path(str(item.get("name") or f"{item['id']}.jsonl")).name
result = await client.upload_file(
target_name,
str(item.get("content") or "").encode("utf-8"),
f"datasets/{dataset_id}/{target_name}",
resource_type="dataset",
resource_id=dataset_id,
)
store.upsert_resource_replica(
node["id"],
"dataset",
dataset_id,
str(result.get("local_path") or ""),
)
results.append(
{
"node_id": node["id"],
"node_code": node.get("code"),
"file_id": item.get("id"),
"name": target_name,
"local_path": result.get("local_path"),
"byte_size": result.get("byte_size"),
"checksum_sha256": result.get("checksum_sha256"),
}
)
return results
@router.post("/dataset-manage/upload/{dataset_id}")
async def upload_dataset_files(
dataset_id: str,
@@ -543,6 +667,10 @@ async def fine_tune_logs(
if node:
try:
logs = await ComputeNodeClient(node["api_base_url"]).job_logs(task["compute_job_id"], tail_lines, offset, limit)
try:
store.record_training_log_metrics(task_id, str(logs.get("content") or ""))
except Exception:
pass
if task.get("status") in {"queued", "running", "failed", "stopped", "completed"}:
try:
job = await ComputeNodeClient(node["api_base_url"]).get_job(task["compute_job_id"])
@@ -613,18 +741,36 @@ async def delete_fine_tune(task_id: str) -> dict[str, Any]:
@router.get("/fine-tune/{task_id}/overview")
async def fine_tune_overview(task_id: str) -> dict[str, Any]:
task = get_platform_store().task(task_id)
return ok({"task": task, "progress": get_platform_store().progress(task_id)})
store = get_platform_store()
task = store.task(task_id)
return ok(
{
"task": task,
"progress": store.progress(task_id),
"metrics": store.task_metrics(task_id),
"checkpoints": store.task_checkpoints(task_id),
}
)
@router.get("/fine-tune/{task_id}/checkpoints")
async def fine_tune_checkpoints(task_id: str) -> dict[str, Any]:
task = get_platform_store().task(task_id)
checkpoints = []
for step in [50, 100, 150]:
if task.get("progress", 0) >= min(100, step // 2):
checkpoints.append({"step": step, "path": f"/data/yg-ft/outputs/{task['name']}/checkpoint-{step}"})
return ok(checkpoints)
store = get_platform_store()
try:
store.task(task_id)
except KeyError:
raise fail(404, "fine tune task not found")
return ok(store.task_checkpoints(task_id))
@router.get("/fine-tune/{task_id}/metrics")
async def fine_tune_metrics(task_id: str) -> dict[str, Any]:
store = get_platform_store()
try:
store.task(task_id)
except KeyError:
raise fail(404, "fine tune task not found")
return ok(store.task_metrics(task_id))
@router.get("/model-eval")
@@ -909,6 +1055,142 @@ async def compute_node_replicas(node_id: str) -> dict[str, Any]:
return ok(get_platform_store().replicas(node_id))
@router.get("/compute/nodes/{node_id}/replicas/drift")
async def compute_node_replica_drift(node_id: str) -> dict[str, Any]:
store = get_platform_store()
node = next((item for item in store.compute_nodes() if item["id"] == node_id), None)
if not node:
raise fail(404, "compute node not found")
replicas = store.replicas(node_id)
if not replicas:
return ok({"node_id": node_id, "items": [], "drifted": 0})
paths = [
{
"name": replica["id"],
"path": replica["local_path"],
"type": "any",
"required": True,
}
for replica in replicas
]
try:
result = await ComputeNodeClient(node["api_base_url"]).check_paths(paths)
except Exception as exc: # noqa: BLE001
raise fail(502, f"replica drift check failed: {exc}")
check_map = {str(item.get("name")): item for item in result.get("items") or []}
items = []
for replica in replicas:
check = check_map.get(replica["id"], {})
updated = store.update_resource_replica_check(
replica["id"],
bool(check.get("ok")),
int(check.get("byte_size") or replica.get("byte_size") or 0),
"" if check.get("ok") else f"path not available: {replica['local_path']}",
)
items.append({**updated, "check": check})
return ok({"node_id": node_id, "items": items, "drifted": len([item for item in items if item.get("sync_status") == "drifted"])})
@router.post("/compute/nodes/{node_id}/replicas/repair")
async def compute_node_replica_repair(node_id: str, payload: dict[str, Any] | None = Body(default=None)) -> dict[str, Any]:
store = get_platform_store()
node = next((item for item in store.compute_nodes() if item["id"] == node_id), None)
if not node:
raise fail(404, "compute node not found")
payload = payload or {}
replica_ids = payload.get("replica_ids") or [
item["id"] for item in store.replicas(node_id) if item.get("sync_status") in {"drifted", "failed", "repair_pending"}
]
updated = store.mark_resource_replica_repair_pending([str(item) for item in replica_ids])
sync_id = store.create_sync_job(
node_id,
{
"resources": [
{
"resource_type": item.get("resource_type"),
"resource_id": item.get("resource_id"),
"replica_id": item.get("id"),
"target_path": item.get("local_path"),
}
for item in updated
]
},
)
client = ComputeNodeClient(node["api_base_url"])
repaired = []
failures = []
for replica in updated:
replica_id = str(replica["id"])
resource_type = str(replica.get("resource_type") or "")
resource_id = str(replica.get("resource_id") or "")
try:
if resource_type == "dataset":
files = store.training_dataset_files(resource_id)
if not files:
raise RuntimeError(f"dataset has no uploaded file: {resource_id}")
total_size = 0
checksum = ""
local_path = str(replica.get("local_path") or "")
for item in files:
filename = Path(str(item.get("name") or f"{item['id']}.jsonl")).name
result = await client.upload_file(
filename,
str(item.get("content") or "").encode("utf-8"),
f"datasets/{resource_id}/{filename}",
resource_type="dataset",
resource_id=resource_id,
)
total_size += int(result.get("byte_size") or 0)
checksum = str(result.get("checksum_sha256") or checksum)
local_path = str(result.get("local_path") or local_path)
repaired.append(store.update_resource_replica_sync_result(replica_id, True, local_path, total_size, checksum))
continue
source_path = ""
target_relative_path = ""
if resource_type == "model":
model = store.model(resource_id)
source_path = str(model.get("path") or "")
target_relative_path = f"models/{Path(source_path).name}" if source_path else ""
elif resource_type in {"trained_model", "model_artifact"}:
if resource_type == "trained_model":
artifacts = store.model_artifacts(resource_id)
artifact = next((item for item in artifacts if item.get("path")), None)
else:
artifact = store.model_artifact(resource_id)
source_path = str((artifact or {}).get("path") or replica.get("local_path") or "")
target_relative_path = f"outputs/{Path(source_path).name}" if source_path else ""
else:
source_path = str(payload.get("source_path") or replica.get("source_path") or replica.get("local_path") or "")
target_relative_path = str(payload.get("target_relative_path") or "")
if not source_path:
raise RuntimeError(f"authoritative source path not found for {resource_type}:{resource_id}")
result = await client.import_local_file(
{
"source_path": source_path,
"target_relative_path": target_relative_path,
"resource_type": resource_type,
"resource_id": resource_id,
}
)
repaired.append(
store.update_resource_replica_sync_result(
replica_id,
True,
str(result.get("local_path") or replica.get("local_path") or ""),
int(result.get("byte_size") or 0),
str(result.get("checksum_sha256") or ""),
)
)
except Exception as exc: # noqa: BLE001 - collect all replica repair failures
error = str(exc)
failures.append({"replica_id": replica_id, "resource_type": resource_type, "resource_id": resource_id, "error": error})
repaired.append(store.update_resource_replica_sync_result(replica_id, False, None, int(replica.get("byte_size") or 0), "", error))
store.update_sync_job(sync_id, "failed" if failures else "completed", 100 if not failures else 99, completed=True)
return ok({"sync": store.sync_job(sync_id), "replicas": repaired, "failed": failures})
@router.get("/compute/nodes/{node_id}/engines")
async def compute_node_engines(node_id: str) -> dict[str, Any]:
node = next((item for item in get_platform_store().compute_nodes() if item["id"] == node_id), None)
@@ -955,7 +1237,11 @@ async def compute_queue() -> dict[str, Any]:
async def compute_job_detail(job_id: str) -> dict[str, Any]:
task = _task_for_compute_job(job_id)
if not task:
raise fail(404, "compute job not found")
node = _node_for_compute_job_record(job_id)
if not node:
raise fail(404, "compute job not found")
job = await ComputeNodeClient(node["api_base_url"]).get_job(job_id)
return ok(get_platform_store().sync_model_merge_job(job_id, job))
node = _node_for_task(task)
if not node:
raise fail(404, "compute node not found")
@@ -966,7 +1252,11 @@ async def compute_job_detail(job_id: str) -> dict[str, Any]:
async def compute_job_stop(job_id: str) -> dict[str, Any]:
task = _task_for_compute_job(job_id)
if not task:
raise fail(404, "compute job not found")
node = _node_for_compute_job_record(job_id)
if not node:
raise fail(404, "compute job not found")
job = await ComputeNodeClient(node["api_base_url"]).stop_job(job_id)
return ok(get_platform_store().sync_model_merge_job(job_id, job))
node = _node_for_task(task)
if not node:
raise fail(404, "compute node not found")
@@ -984,7 +1274,10 @@ async def compute_job_logs(
) -> dict[str, Any]:
task = _task_for_compute_job(job_id)
if not task:
raise fail(404, "compute job not found")
node = _node_for_compute_job_record(job_id)
if not node:
raise fail(404, "compute job not found")
return ok(await ComputeNodeClient(node["api_base_url"]).job_logs(job_id, tail_lines, offset, limit))
node = _node_for_task(task)
if not node:
raise fail(404, "compute node not found")