Merge branch 'master' into wip/qa7920-rag-stream
This commit is contained in:
+234
-117
@@ -1,6 +1,7 @@
|
||||
# web/pipeline_backend.py
|
||||
import csv
|
||||
import json
|
||||
import csv
|
||||
import http.client
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
@@ -48,6 +49,7 @@ from gfa_core.ppi_provenance import (
|
||||
record_ppi_preflight_decision,
|
||||
)
|
||||
from gfa_core.string_species_registry import (
|
||||
ARABIDOPSIS_TAXON_ID,
|
||||
StringSnapshotError,
|
||||
load_string_snapshot,
|
||||
resolve_ppi_string_species,
|
||||
@@ -1721,7 +1723,7 @@ def infer_string_species_from_ortholog_mapping(mapping_file: Path, fallback: str
|
||||
return fallback
|
||||
|
||||
|
||||
def infer_reference_species_from_ortholog_mapping(mapping_file: Path, fallback: str = "") -> str:
|
||||
def infer_reference_species_from_ortholog_mapping(mapping_file: Path, fallback: str = "") -> str:
|
||||
"""Infer the reference species label from the first data row of an ortholog mapping."""
|
||||
try:
|
||||
with mapping_file.open("r", encoding="utf-8", errors="ignore") as src:
|
||||
@@ -1735,10 +1737,34 @@ def infer_reference_species_from_ortholog_mapping(mapping_file: Path, fallback:
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
return fallback
|
||||
|
||||
|
||||
def _reference_species_from_manifest_data(data: dict[str, Any]) -> str:
|
||||
return fallback
|
||||
|
||||
|
||||
def ortholog_mapping_candidates(
|
||||
final_dir: Path,
|
||||
prefix: str,
|
||||
*,
|
||||
dependency_owner: str = "",
|
||||
scope_id: str = "",
|
||||
) -> list[Path]:
|
||||
from gfa_core.ortholog_materialize import ortholog_output_stem
|
||||
|
||||
output_stem = ortholog_output_stem(prefix, dependency_owner)
|
||||
candidates: list[Path] = []
|
||||
if scope_id:
|
||||
scoped_stem = f"{output_stem}.{sanitize_token(scope_id)}"
|
||||
candidates.extend([
|
||||
final_dir / f"{scoped_stem}.orthologs.txt",
|
||||
final_dir / f"{scoped_stem}.ortholog_pairs.txt",
|
||||
])
|
||||
candidates.extend([
|
||||
final_dir / f"{output_stem}.orthologs.txt",
|
||||
final_dir / f"{output_stem}.ortholog_pairs.txt",
|
||||
])
|
||||
return candidates
|
||||
|
||||
|
||||
def _reference_species_from_manifest_data(data: dict[str, Any]) -> str:
|
||||
for key in ("reference_species", "referenceSpecies"):
|
||||
value = data.get(key)
|
||||
if isinstance(value, list) and value:
|
||||
@@ -1905,11 +1931,10 @@ NCBI_REFERENCE_ANNOTATION_TYPES = {
|
||||
}
|
||||
|
||||
|
||||
def _reference_bundle_file_plan(requested_stages: Optional[list[str]], include_recommended: bool = False) -> list[str]:
|
||||
stages = _normalized_dependency_stages(requested_stages)
|
||||
files = ["pep"]
|
||||
if "kaks" in stages:
|
||||
files.append("cds")
|
||||
def _reference_bundle_file_plan(requested_stages: Optional[list[str]], include_recommended: bool = False) -> list[str]:
|
||||
# PPI owns reference preparation. Download PEP and CDS together so the
|
||||
# later Ka/Ks choice can reuse the exact same bundle without a second job.
|
||||
files = ["pep", "cds"]
|
||||
if include_recommended:
|
||||
for item in ("gff3", "genome"):
|
||||
if item not in files:
|
||||
@@ -2005,7 +2030,7 @@ def _download_url_to_file(
|
||||
raise RuntimeError("downloaded file is empty")
|
||||
temp_path.replace(output_path)
|
||||
return total
|
||||
except (urllib.error.URLError, TimeoutError, OSError, RuntimeError) as exc:
|
||||
except (http.client.IncompleteRead, urllib.error.URLError, TimeoutError, OSError, RuntimeError) as exc:
|
||||
last_error = exc
|
||||
if temp_path.exists():
|
||||
try:
|
||||
@@ -2119,6 +2144,8 @@ def _fasta_header_index(fasta_path: Path | str | None) -> dict[str, dict[str, st
|
||||
if not seq_id or seq_id in records:
|
||||
continue
|
||||
gene_match = re.search(r"\[gene=([^\]]+)\]", header)
|
||||
if not gene_match:
|
||||
gene_match = re.search(r"(?:^|\s)gene:([^\s]+)", header)
|
||||
ncbi_gene_match = re.search(r"\[db_xref=GeneID:(\d+)\]", header)
|
||||
protein_match = re.search(r"\[protein_id=([^\]]+)\]", header)
|
||||
records[seq_id] = {
|
||||
@@ -2220,7 +2247,7 @@ def download_reference_annotation_bundle(
|
||||
include_recommended=include_recommended,
|
||||
)
|
||||
assembly = {"accession": accession}
|
||||
emit({"phase": "select_assembly", "message": "正在解析推荐参考物种装配", "percentage": 5, "indeterminate": True})
|
||||
emit({"phase": "select_assembly", "message": "Resolving the recommended reference assembly", "percentage": 5, "indeterminate": True})
|
||||
if not accession:
|
||||
assembly = _select_ncbi_reference_assembly(taxon_id=taxon_id, species_name=species_name, timeout_sec=min(timeout_sec, 60))
|
||||
accession = assembly.get("accession", "")
|
||||
@@ -2231,24 +2258,24 @@ def download_reference_annotation_bundle(
|
||||
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
work_dir = ws / "reference_bundle_download" / f"{code}_{stamp}"
|
||||
zip_path = work_dir / f"{code}.ncbi_dataset.zip"
|
||||
emit({"phase": "download", "message": "正在下载 NCBI 参考注释包", "percentage": 10, "indeterminate": True})
|
||||
emit({"phase": "download", "message": "Downloading reference PEP and CDS from NCBI", "percentage": 10, "indeterminate": True})
|
||||
download_kwargs = {"timeout_sec": timeout_sec}
|
||||
if progress_callback:
|
||||
download_kwargs["progress_callback"] = lambda event: emit({"message": "正在下载 NCBI 参考注释包", **event})
|
||||
download_kwargs["progress_callback"] = lambda event: emit({"message": "Downloading reference PEP and CDS from NCBI", **event})
|
||||
bytes_downloaded = _download_url_to_file(download_url, zip_path, **download_kwargs)
|
||||
if not zipfile.is_zipfile(zip_path):
|
||||
raise RuntimeError(
|
||||
f"NCBI reference bundle download returned an invalid zip archive: {zip_path}"
|
||||
)
|
||||
species_dir = in_dir / code
|
||||
emit({"phase": "extract", "message": "正在解压参考 PEP/CDS", "percentage": 80, "indeterminate": True})
|
||||
emit({"phase": "extract", "message": "Extracting reference PEP and CDS", "percentage": 80, "indeterminate": True})
|
||||
extracted = _extract_ncbi_reference_bundle_zip(
|
||||
zip_path=zip_path,
|
||||
destination_dir=species_dir,
|
||||
species_code=code,
|
||||
requested_file_types=file_types,
|
||||
)
|
||||
emit({"phase": "validate_ids", "message": "正在校验 PEP/CDS ID 对齐", "percentage": 90, "indeterminate": True})
|
||||
emit({"phase": "validate_ids", "message": "Validating PEP/CDS identifier correspondence", "percentage": 90, "indeterminate": True})
|
||||
id_validation = _build_reference_bundle_id_map(
|
||||
species_code=code,
|
||||
pep_path=extracted.get("pep"),
|
||||
@@ -2288,7 +2315,7 @@ def download_reference_annotation_bundle(
|
||||
manifest["manifest_path"] = str(manifest_path)
|
||||
emit({
|
||||
"phase": "completed",
|
||||
"message": "参考物种注释包下载完成",
|
||||
"message": "PPI and Ka/Ks reference species data are ready",
|
||||
"percentage": 100,
|
||||
"indeterminate": False,
|
||||
"bytes_downloaded": bytes_downloaded,
|
||||
@@ -2311,6 +2338,7 @@ def _ppi_only_preflight_response(
|
||||
selected_string_species: str = "",
|
||||
reference_storage_key: str = "",
|
||||
reference_pep: str = "",
|
||||
reference_cds: str = "",
|
||||
mapping_file: Optional[Path] = None,
|
||||
mapping_reference_species: str = "",
|
||||
required_next_stage: str = "",
|
||||
@@ -2319,22 +2347,28 @@ def _ppi_only_preflight_response(
|
||||
) -> dict[str, Any]:
|
||||
candidates = list(species_resolution.get("candidates") or [])
|
||||
selection_mode = str(species_resolution.get("selection_mode") or "unresolved")
|
||||
required_files = ["pep", "cds"] if reference_storage_key else []
|
||||
bundle_files = {}
|
||||
if reference_pep:
|
||||
bundle_files["pep"] = reference_pep
|
||||
if reference_cds:
|
||||
bundle_files["cds"] = reference_cds
|
||||
reference_bundle_status = {
|
||||
"required_files": ["pep"],
|
||||
"required_files": required_files,
|
||||
"recommended_files": [],
|
||||
"files": {"pep": reference_pep} if reference_pep else {},
|
||||
"files": bundle_files,
|
||||
"missing_required": list(missing_required or []),
|
||||
"missing_recommended": [],
|
||||
"mode": (
|
||||
"complete"
|
||||
if reference_pep
|
||||
if not required_files or (reference_pep and reference_cds)
|
||||
else "not_selected"
|
||||
if status in {"confirmation_required", "unresolved", "conflict"}
|
||||
else "unavailable"
|
||||
if status == "resource_error"
|
||||
else "incomplete"
|
||||
),
|
||||
"kaks_ready": True,
|
||||
"kaks_ready": (not required_files) or bool(reference_pep and reference_cds),
|
||||
}
|
||||
diagnostics = {
|
||||
"issue_code": issue_code,
|
||||
@@ -2343,7 +2377,7 @@ def _ppi_only_preflight_response(
|
||||
"required_next_stage": required_next_stage,
|
||||
"recommendation": message,
|
||||
"dependency_owner_stages": ["ppi"],
|
||||
"required_bundle_files": ["pep"],
|
||||
"required_bundle_files": required_files,
|
||||
"reference_bundle_status": reference_bundle_status,
|
||||
"ortholog_dependency_role": "producer" if required_next_stage == "ortholog" else "not_requested",
|
||||
"reference_recommendations": candidates,
|
||||
@@ -2399,24 +2433,12 @@ def _effective_ppi_owner_scope_id(
|
||||
scope_data: dict[str, Any],
|
||||
reference_storage_key: str,
|
||||
) -> str:
|
||||
scope_mode = str(scope_data.get("scope_mode") or "target_plus_reference")
|
||||
comparative_species = [
|
||||
str(x) for x in scope_data.get("comparative_species", []) if str(x)
|
||||
]
|
||||
if scope_mode == "target_only":
|
||||
return str(scope_data.get("scope_id") or "") or build_scope_id(
|
||||
family,
|
||||
selected_targets,
|
||||
[str(x) for x in scope_data.get("reference_species", []) if str(x)],
|
||||
comparative_species,
|
||||
scope_mode,
|
||||
)
|
||||
return build_scope_id(
|
||||
family,
|
||||
selected_targets,
|
||||
[reference_storage_key],
|
||||
comparative_species,
|
||||
scope_mode,
|
||||
[],
|
||||
[],
|
||||
"target_only",
|
||||
)
|
||||
|
||||
|
||||
@@ -2616,6 +2638,16 @@ def _resolve_ppi_only_snapshot_preflight(
|
||||
),
|
||||
None,
|
||||
)
|
||||
explicit_model_fallback = bool(
|
||||
str(ppi_reference_selection_mode or "").strip().lower() == "model_fallback"
|
||||
and chosen_taxon_id == ARABIDOPSIS_TAXON_ID
|
||||
and chosen_taxon_id in snapshot.registry
|
||||
)
|
||||
if explicit_model_fallback:
|
||||
chosen = {
|
||||
"selected_string_taxon_id": chosen_taxon_id,
|
||||
"selected_string_name": snapshot.nodes[chosen_taxon_id].taxon_name,
|
||||
}
|
||||
if not (ppi_reference_user_confirmed and chosen):
|
||||
return _ppi_only_preflight_response(
|
||||
status="confirmation_required",
|
||||
@@ -2628,6 +2660,13 @@ def _resolve_ppi_only_snapshot_preflight(
|
||||
requested_ppi_species=requested_ppi_species,
|
||||
dependency_owner=dependency_owner,
|
||||
)
|
||||
if explicit_model_fallback:
|
||||
species_resolution = {
|
||||
**species_resolution,
|
||||
"selection_mode": "model_fallback",
|
||||
"candidate_source": "explicit_model_fallback",
|
||||
"issue_code": "",
|
||||
}
|
||||
|
||||
storage_key = str(ppi_reference_storage_key or "").strip() or f"string_taxon_{chosen_taxon_id}"
|
||||
selected_name = str(chosen.get("selected_string_name") or "")
|
||||
@@ -2638,16 +2677,16 @@ def _resolve_ppi_only_snapshot_preflight(
|
||||
ws, final_dir, in_dir, scope_data, storage_key, family, "pep", family_only=False
|
||||
)
|
||||
reference_pep = str(reference_pep_path) if reference_pep_path else ""
|
||||
mapping_candidates: list[Path] = []
|
||||
if scope_id:
|
||||
mapping_candidates.extend([
|
||||
final_dir / f"{prefix}.{sanitize_token(scope_id)}.orthologs.txt",
|
||||
final_dir / f"{prefix}.{sanitize_token(scope_id)}.ortholog_pairs.txt",
|
||||
])
|
||||
mapping_candidates.extend([
|
||||
final_dir / f"{prefix}.orthologs.txt",
|
||||
final_dir / f"{prefix}.ortholog_pairs.txt",
|
||||
])
|
||||
reference_cds_path = resolve_species_sequence_file(
|
||||
ws, final_dir, in_dir, scope_data, storage_key, family, "cds", family_only=False
|
||||
)
|
||||
reference_cds = str(reference_cds_path) if reference_cds_path else ""
|
||||
mapping_candidates = ortholog_mapping_candidates(
|
||||
final_dir,
|
||||
prefix,
|
||||
dependency_owner="ppi",
|
||||
scope_id=scope_id,
|
||||
)
|
||||
mapping_file = None if ignore_existing_ortholog else next(
|
||||
(path for path in mapping_candidates if path.exists() and path.stat().st_size > 0),
|
||||
None,
|
||||
@@ -2696,18 +2735,24 @@ def _resolve_ppi_only_snapshot_preflight(
|
||||
"selected_string_species": species_to_string_code(selected_name, ""),
|
||||
"reference_storage_key": storage_key,
|
||||
"reference_pep": reference_pep,
|
||||
"reference_cds": reference_cds,
|
||||
"mapping_file": mapping_file,
|
||||
"mapping_reference_species": mapping_reference_species,
|
||||
"dependency_owner": dependency_owner,
|
||||
}
|
||||
if not reference_pep:
|
||||
missing_reference_files = [
|
||||
file_type
|
||||
for file_type, file_path in (("pep", reference_pep), ("cds", reference_cds))
|
||||
if not file_path
|
||||
]
|
||||
if missing_reference_files:
|
||||
return _ppi_only_preflight_response(
|
||||
status="needs_reference_data",
|
||||
issue_code="ppi_reference_data_missing",
|
||||
message="The confirmed STRING reference requires a protein FASTA (PEP) before Ortholog.",
|
||||
message="The confirmed STRING reference requires matching PEP and CDS before Ortholog.",
|
||||
decision="needs_reference_bundle",
|
||||
required_next_stage="",
|
||||
missing_required=["pep"],
|
||||
missing_required=missing_reference_files,
|
||||
**common,
|
||||
)
|
||||
if mapping_file and not mapping_matches:
|
||||
@@ -2775,9 +2820,7 @@ def resolve_ppi_species_resolution(
|
||||
dependency_stages = _normalized_dependency_stages(requested_stages)
|
||||
needs_ppi = "ppi" in dependency_stages
|
||||
needs_kaks = "kaks" in dependency_stages
|
||||
required_bundle_files = ["pep"]
|
||||
if needs_kaks and "cds" not in required_bundle_files:
|
||||
required_bundle_files.append("cds")
|
||||
required_bundle_files = ["pep", "cds"]
|
||||
recommended_bundle_files = ["gff3", "genome"] if needs_kaks else []
|
||||
selected_targets = canonicalize_species_codes_for_job(in_dir, [str(x).strip() for x in (target_species or []) if str(x).strip()])
|
||||
references = canonicalize_species_codes_for_job(in_dir, [str(x).strip() for x in (reference_species or []) if str(x).strip()])
|
||||
@@ -2818,6 +2861,22 @@ def resolve_ppi_species_resolution(
|
||||
)
|
||||
|
||||
if needs_ppi and not needs_kaks:
|
||||
scope_id = _effective_ppi_owner_scope_id(
|
||||
family=family,
|
||||
selected_targets=selected_targets,
|
||||
scope_data=scope_data,
|
||||
reference_storage_key=str(ppi_reference_storage_key or ""),
|
||||
)
|
||||
scope_data = {
|
||||
**scope_data,
|
||||
"family": family,
|
||||
"scope_id": scope_id,
|
||||
"scope_mode": "target_only",
|
||||
"target_species": list(selected_targets),
|
||||
"reference_species": [],
|
||||
"comparative_species": [],
|
||||
"selected_species": list(selected_targets),
|
||||
}
|
||||
result = _resolve_ppi_only_snapshot_preflight(
|
||||
job=job,
|
||||
prefix=prefix,
|
||||
@@ -2886,17 +2945,15 @@ def resolve_ppi_species_resolution(
|
||||
and target_string_code
|
||||
and selected_string_species == target_string_code
|
||||
)
|
||||
mapping_candidates: list[Path] = []
|
||||
if not use_target_string_directly or needs_kaks:
|
||||
if scope_id:
|
||||
mapping_candidates.extend([
|
||||
final_dir / f"{prefix}.{sanitize_token(scope_id)}.orthologs.txt",
|
||||
final_dir / f"{prefix}.{sanitize_token(scope_id)}.ortholog_pairs.txt",
|
||||
])
|
||||
mapping_candidates.extend([
|
||||
final_dir / f"{prefix}.orthologs.txt",
|
||||
final_dir / f"{prefix}.ortholog_pairs.txt",
|
||||
])
|
||||
mapping_candidates: list[Path] = []
|
||||
if not use_target_string_directly or needs_kaks:
|
||||
mapping_owner = str(dependency_owner or "").strip().lower()
|
||||
mapping_candidates = ortholog_mapping_candidates(
|
||||
final_dir,
|
||||
prefix,
|
||||
dependency_owner=mapping_owner if mapping_owner in {"ppi", "kaks"} else "",
|
||||
scope_id=scope_id,
|
||||
)
|
||||
mapping_file = None if ignore_existing_ortholog else next((p for p in mapping_candidates if p.exists() and p.stat().st_size > 0), None)
|
||||
|
||||
reference_data_source = ""
|
||||
@@ -2917,7 +2974,7 @@ def resolve_ppi_species_resolution(
|
||||
reference_data_source = str(ref_pep) if ref_pep else ""
|
||||
if ref_pep:
|
||||
reference_bundle_files["pep"] = str(ref_pep)
|
||||
if needs_kaks:
|
||||
if "cds" in required_bundle_files:
|
||||
ref_cds = resolve_species_sequence_file(
|
||||
ws,
|
||||
final_dir,
|
||||
@@ -6026,6 +6083,7 @@ def run_stage(
|
||||
ppi_reference_selection_mode: str = "",
|
||||
ppi_reference_user_confirmed: bool = False,
|
||||
dependency_owner: str = "",
|
||||
ortholog_mapping_owner: str = "",
|
||||
ppi_score: int = 200,
|
||||
# Genome Prepare 参数
|
||||
genome_prepare_type: str = "auto",
|
||||
@@ -12514,20 +12572,42 @@ def run_stage(
|
||||
if not references:
|
||||
return "❌ Ortholog scope 中没有 reference_species,无法确定 subject/reference 物种。", manifest
|
||||
reference_species = references[0]
|
||||
subject_fasta = first_existing_scope_file(ortholog_scope_data, reference_species, ["family_pep", "pep"])
|
||||
if not subject_fasta:
|
||||
subject_fasta = resolve_species_family_pep(ws, final_dir, in_dir, reference_species, family)
|
||||
if not subject_fasta:
|
||||
subject_fasta = resolve_species_sequence_file(
|
||||
ws,
|
||||
final_dir,
|
||||
if ppi_owned_producer:
|
||||
subject_fasta = resolve_species_sequence_file(
|
||||
ws,
|
||||
final_dir,
|
||||
in_dir,
|
||||
ortholog_scope_data,
|
||||
reference_species,
|
||||
family,
|
||||
"pep",
|
||||
family_only=False,
|
||||
)
|
||||
"pep",
|
||||
family_only=False,
|
||||
)
|
||||
else:
|
||||
subject_fasta = first_existing_scope_file(
|
||||
ortholog_scope_data,
|
||||
reference_species,
|
||||
["family_pep", "pep"],
|
||||
)
|
||||
if not subject_fasta:
|
||||
subject_fasta = resolve_species_family_pep(
|
||||
ws,
|
||||
final_dir,
|
||||
in_dir,
|
||||
reference_species,
|
||||
family,
|
||||
)
|
||||
if not subject_fasta:
|
||||
subject_fasta = resolve_species_sequence_file(
|
||||
ws,
|
||||
final_dir,
|
||||
in_dir,
|
||||
ortholog_scope_data,
|
||||
reference_species,
|
||||
family,
|
||||
"pep",
|
||||
family_only=False,
|
||||
)
|
||||
if not subject_fasta:
|
||||
return (
|
||||
f"❌ Ortholog stage requires reference protein FASTA for {reference_species}. "
|
||||
@@ -12574,11 +12654,14 @@ def run_stage(
|
||||
ortholog_pairs_has_data,
|
||||
write_ortholog_run_sidecar,
|
||||
)
|
||||
ortholog_owner = str(dependency_owner or "").strip().lower()
|
||||
if ortholog_owner not in {"", "ppi", "kaks"}:
|
||||
return f"❌ Unsupported Ortholog dependency owner: {dependency_owner}", manifest
|
||||
|
||||
scope_fields: dict[str, Any] = {}
|
||||
if ortholog_scope_data:
|
||||
source_scope_mode = str(ortholog_scope_data.get("scope_mode") or "")
|
||||
preserve_target_only_scope = ppi_owned_producer and source_scope_mode == "target_only"
|
||||
preserve_target_only_scope = ppi_owned_producer
|
||||
effective_scope_id = str(ortholog_scope_data.get("scope_id") or "")
|
||||
if ppi_owned_producer:
|
||||
effective_scope_id = _effective_ppi_owner_scope_id(
|
||||
@@ -12588,19 +12671,19 @@ def run_stage(
|
||||
reference_storage_key=reference_species,
|
||||
)
|
||||
scope_fields = {
|
||||
"scope_file": str(ortholog_scope_path),
|
||||
"scope_file": str(ortholog_scope_path) if source_scope_mode == "target_only" else "",
|
||||
"scope_id": effective_scope_id,
|
||||
"scope_mode": ortholog_scope_data.get("scope_mode", ""),
|
||||
"scope_mode": "target_only" if ppi_owned_producer else ortholog_scope_data.get("scope_mode", ""),
|
||||
"target_species": [record[0] for record in run_specs],
|
||||
"reference_species": (
|
||||
ortholog_scope_data.get("reference_species", [])
|
||||
if preserve_target_only_scope
|
||||
[]
|
||||
if ppi_owned_producer
|
||||
else [reference_species] if reference_species else []
|
||||
),
|
||||
"comparative_species": ortholog_scope_data.get("comparative_species", []),
|
||||
"comparative_species": [] if ppi_owned_producer else ortholog_scope_data.get("comparative_species", []),
|
||||
"selected_species": (
|
||||
ortholog_scope_data.get("selected_species", [record[0] for record in run_specs])
|
||||
if preserve_target_only_scope
|
||||
[record[0] for record in run_specs]
|
||||
if ppi_owned_producer
|
||||
else [record[0] for record in run_specs] + ([reference_species] if reference_species else [])
|
||||
),
|
||||
"family": family,
|
||||
@@ -12679,9 +12762,10 @@ def run_stage(
|
||||
run_identities.append(completed_identity)
|
||||
|
||||
try:
|
||||
pair_count, ortholog_manifest = materialize_ortholog_final_outputs(
|
||||
final_dir=final_dir,
|
||||
prefix=prefix,
|
||||
pair_count, ortholog_manifest = materialize_ortholog_final_outputs(
|
||||
final_dir=final_dir,
|
||||
prefix=prefix,
|
||||
dependency_owner=ortholog_owner,
|
||||
run_records=run_records,
|
||||
job_id=job.job_id,
|
||||
workspace=ws,
|
||||
@@ -12855,18 +12939,15 @@ def run_stage(
|
||||
and target_string_taxon_id
|
||||
and selected_execution_taxon_id == target_string_taxon_id
|
||||
)
|
||||
mapping_candidates = []
|
||||
if not use_target_string_directly:
|
||||
if scope_id:
|
||||
mapping_candidates.extend([
|
||||
final_dir / f"{prefix}.{sanitize_token(scope_id)}.orthologs.txt",
|
||||
final_dir / f"{prefix}.{sanitize_token(scope_id)}.ortholog_pairs.txt",
|
||||
])
|
||||
mapping_candidates.extend([
|
||||
final_dir / f"{prefix}.orthologs.txt",
|
||||
final_dir / f"{prefix}.ortholog_pairs.txt",
|
||||
])
|
||||
mapping_file = next((p for p in mapping_candidates if p.exists() and p.stat().st_size > 0), None)
|
||||
mapping_candidates = []
|
||||
if not use_target_string_directly:
|
||||
mapping_candidates = ortholog_mapping_candidates(
|
||||
final_dir,
|
||||
prefix,
|
||||
dependency_owner="ppi" if ppi_owner_intent else "",
|
||||
scope_id=scope_id,
|
||||
)
|
||||
mapping_file = next((p for p in mapping_candidates if p.exists() and p.stat().st_size > 0), None)
|
||||
if mapping_file:
|
||||
gene_id_file = ws / "ppi" / "analysis" / f"{prefix}.{sanitize_token(scope_id) if scope_id else 'scope'}.ppi_source_ids.txt"
|
||||
mapped_source_count = build_ppi_gene_ids_from_ortholog_mapping(mapping_file, gene_id_file, ppi_selected_targets)
|
||||
@@ -13051,7 +13132,7 @@ def run_stage(
|
||||
if mapping_file:
|
||||
effective_normalization_label = (
|
||||
ppi_reference_storage_key
|
||||
if ppi_owner_auto_fallback and ppi_reference_storage_key
|
||||
if ppi_reference_storage_key
|
||||
else ppi_reference_species or ppi_species
|
||||
)
|
||||
elif len(ppi_selected_targets) == 1:
|
||||
@@ -13118,6 +13199,41 @@ def run_stage(
|
||||
if not ppi_storage_key:
|
||||
ppi_storage_key = str(ppi_species)
|
||||
id_map_file = Path(job.input_dir) / ppi_storage_key / f"{ppi_storage_key}.pep_cds_idmap.tsv"
|
||||
if mapping_file and not id_map_file.is_file():
|
||||
reference_family = str(
|
||||
(ppi_scope_data or {}).get("family")
|
||||
or (prefix.split(".", 1)[1] if "." in prefix else prefix)
|
||||
)
|
||||
reference_pep = resolve_species_sequence_file(
|
||||
ws,
|
||||
final_dir,
|
||||
in_dir,
|
||||
ppi_scope_data or {},
|
||||
ppi_storage_key,
|
||||
reference_family,
|
||||
"pep",
|
||||
family_only=False,
|
||||
)
|
||||
reference_cds = resolve_species_sequence_file(
|
||||
ws,
|
||||
final_dir,
|
||||
in_dir,
|
||||
ppi_scope_data or {},
|
||||
ppi_storage_key,
|
||||
reference_family,
|
||||
"cds",
|
||||
family_only=False,
|
||||
)
|
||||
if reference_pep and reference_cds:
|
||||
try:
|
||||
_build_reference_bundle_id_map(
|
||||
species_code=ppi_storage_key,
|
||||
pep_path=reference_pep,
|
||||
cds_path=reference_cds,
|
||||
output_path=id_map_file,
|
||||
)
|
||||
except (OSError, UnicodeError, csv.Error) as exc:
|
||||
print(f"[PPI] Could not build species-matched ID map: {exc}")
|
||||
if mapping_file and not id_map_file.is_file():
|
||||
return fail_ppi_execution(
|
||||
f"❌ PPI projected analysis requires species-matched ID map: {id_map_file}"
|
||||
@@ -13399,22 +13515,23 @@ def run_stage(
|
||||
f"Target.{target_species_name}.{family}",
|
||||
])
|
||||
candidate_prefixes = list(dict.fromkeys(str(x) for x in candidate_prefixes if str(x)))
|
||||
mapping_candidates = []
|
||||
if scope_id:
|
||||
for candidate_prefix in candidate_prefixes:
|
||||
mapping_candidates.extend([
|
||||
final_dir / f"{candidate_prefix}.{sanitize_token(scope_id)}.orthologs.txt",
|
||||
final_dir / f"{candidate_prefix}.{sanitize_token(scope_id)}.ortholog_pairs.txt",
|
||||
])
|
||||
for candidate_prefix in candidate_prefixes:
|
||||
mapping_candidates.extend([
|
||||
final_dir / f"{candidate_prefix}.orthologs.txt",
|
||||
final_dir / f"{candidate_prefix}.ortholog_pairs.txt",
|
||||
])
|
||||
mapping_candidates.append(in_dir / "ortholog_pairs.txt")
|
||||
scoped_pairs_source = next((p for p in mapping_candidates if p.exists() and p.stat().st_size > 0), None)
|
||||
if not scoped_pairs_source:
|
||||
return "❌ KaKs scope 模式需要 Ortholog 映射文件。请先运行 Ortholog 阶段。", manifest
|
||||
selected_mapping_owner = str(ortholog_mapping_owner or "").strip().lower()
|
||||
if selected_mapping_owner not in {"", "ppi", "kaks"}:
|
||||
return f"❌ KaKs Ortholog 映射所有者无效: {ortholog_mapping_owner}", manifest
|
||||
mapping_candidates = []
|
||||
for candidate_prefix in candidate_prefixes:
|
||||
mapping_candidates.extend(ortholog_mapping_candidates(
|
||||
final_dir,
|
||||
candidate_prefix,
|
||||
dependency_owner=selected_mapping_owner,
|
||||
scope_id=scope_id,
|
||||
))
|
||||
if not selected_mapping_owner:
|
||||
mapping_candidates.append(in_dir / "ortholog_pairs.txt")
|
||||
scoped_pairs_source = next((p for p in mapping_candidates if p.exists() and p.stat().st_size > 0), None)
|
||||
if not scoped_pairs_source:
|
||||
owner_hint = f"({selected_mapping_owner} 专属)" if selected_mapping_owner else ""
|
||||
return f"❌ KaKs scope 模式需要{owner_hint} Ortholog 映射文件。", manifest
|
||||
|
||||
ref_cds = resolve_species_sequence_file(ws, final_dir, in_dir, kaks_scope_data, ref_species, family, "cds")
|
||||
ref_pep = resolve_species_sequence_file(ws, final_dir, in_dir, kaks_scope_data, ref_species, family, "pep", family_only=False)
|
||||
|
||||
+907
-309
File diff suppressed because it is too large
Load Diff
+1026
-350
File diff suppressed because it is too large
Load Diff
@@ -124,6 +124,17 @@
|
||||
};
|
||||
}
|
||||
|
||||
async function extractEnvDeclaration(files) {
|
||||
for (const file of files) {
|
||||
const path = normalizedRelativePath(file).toLowerCase();
|
||||
if (!path.endsWith('.sh')) continue;
|
||||
const content = await file.text();
|
||||
const m = content.match(/^\s*#\s*lightweight-env\s*:\s*([A-Za-z0-9_.-]+)/m);
|
||||
if (m) return m[1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function encodeBase64(file) {
|
||||
const buffer = await file.arrayBuffer();
|
||||
const bytes = new Uint8Array(buffer);
|
||||
@@ -146,11 +157,12 @@
|
||||
payload.files.push(await encodeBase64(file));
|
||||
payload.relative_paths.push(normalizedRelativePath(file));
|
||||
}
|
||||
const response = await fetch('/api/job/import-folder', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const response = await hooks.uploadExtensionWithProgress(
|
||||
'/api/job/import-folder',
|
||||
JSON.stringify(payload),
|
||||
'正在镜像导入 Job',
|
||||
{ 'Content-Type': 'application/json' },
|
||||
);
|
||||
const data = await response.json();
|
||||
if (!response.ok || !data.success) {
|
||||
throw new Error(data.detail || data.error || data.message || 'Job 导入失败');
|
||||
@@ -160,6 +172,8 @@
|
||||
|
||||
async function resolveRuntimeEnv(files) {
|
||||
const dependencies = await extractDependencies(files);
|
||||
const envDeclaration = await extractEnvDeclaration(files);
|
||||
if (envDeclaration) dependencies.env_declaration = envDeclaration;
|
||||
console.log('[lightWeightExtensionTest] 提取的依赖:', JSON.stringify(dependencies), '| 扫描文件数:', files.length);
|
||||
try {
|
||||
const response = await fetch('/api/extension-stages/envs/resolve', {
|
||||
@@ -268,12 +282,23 @@
|
||||
function mountApi() {
|
||||
window.LightWeightExtensionTest = {
|
||||
async handleDrop(hooks, event) {
|
||||
const progress = hooks.lightweightUploadProgress;
|
||||
try {
|
||||
if (progress) {
|
||||
progress.active = true;
|
||||
progress.percent = 0;
|
||||
progress.loaded = 0;
|
||||
progress.total = 0;
|
||||
progress.speed = 0;
|
||||
progress.label = '本地文件读取中';
|
||||
}
|
||||
hooks.lightweightExtensionTestStatus.value = '本地文件读取中...';
|
||||
const files = await readDroppedFiles(event?.dataTransfer);
|
||||
await processFolderFiles(hooks, files);
|
||||
} catch (error) {
|
||||
const message = `失败:${error.message || '目录处理失败'}`;
|
||||
hooks.lightweightExtensionTestStatus.value = message;
|
||||
if (progress) progress.active = false;
|
||||
notifyError(message);
|
||||
}
|
||||
},
|
||||
|
||||
+236
-46
@@ -45,6 +45,7 @@ PROJECT_ROOT = Path(__file__).parent.parent.parent.parent
|
||||
DEFAULT_RUNTIME_ENV = os.environ.get("BBAV5_DEFAULT_RUNTIME_ENV", "test4py311")
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
from gfa_core.project_paths import get_conda_env_bin, get_conda_env_rscript, get_configured_path, get_wgs_bcftools, get_wgs_java, get_wgs_samtools
|
||||
from gfa_core.mas import confirmation_snapshot as _confirmation_snapshot
|
||||
|
||||
ASAW_BIN = get_conda_env_bin("ASAW")
|
||||
ASAW_RSCRIPT = get_conda_env_rscript("ASAW")
|
||||
@@ -705,9 +706,10 @@ def _confirmed_config_species_scope(job_id: str) -> Dict[str, list[str]]:
|
||||
if not job_id:
|
||||
return {}
|
||||
try:
|
||||
from gfa_core.mas.confirmation_snapshot import build_confirmation_snapshot
|
||||
|
||||
confirmation = build_confirmation_snapshot(PROJECT_ROOT, job_id)
|
||||
confirmation = _confirmation_snapshot.build_confirmation_snapshot(
|
||||
PROJECT_ROOT,
|
||||
job_id,
|
||||
)
|
||||
except (OSError, ValueError):
|
||||
return {}
|
||||
if not confirmation.get("config_confirmed"):
|
||||
@@ -880,7 +882,8 @@ def _species_role_conflict_message(conflicts: list[Dict[str, str]]) -> str:
|
||||
return (
|
||||
"检测到本轮请求与项目物种角色不一致:"
|
||||
+ detail_text
|
||||
+ "。系统已阻止执行,请按 config.xlsx 中已确认的角色重新描述分析范围。"
|
||||
+ "。系统已阻止执行,请按 config.xlsx 中已确认的角色重新描述分析范围;"
|
||||
"如需修改项目物种角色,请发送“打开 config.xlsx”。"
|
||||
)
|
||||
|
||||
|
||||
@@ -1090,6 +1093,8 @@ def _hydrate_goal_terms_from_current_job(
|
||||
return hydrated
|
||||
|
||||
config_scope = _confirmed_config_species_scope(job_id)
|
||||
if not config_scope and (INPUT_ROOT / job_id / "config.xlsx").is_file():
|
||||
config_scope = _configured_species_scope(job_id)
|
||||
for config_key, term_key in (
|
||||
("targetSpecies", "target_species"),
|
||||
("referenceSpecies", "reference_species"),
|
||||
@@ -1483,18 +1488,62 @@ def _canonicalize_species_codes_for_job_strict(job_id: str, species_codes: list[
|
||||
|
||||
|
||||
def _reference_bundle_species_available(job_id: str, species_code: str, require_cds: bool = False) -> tuple[bool, str]:
|
||||
"""Return whether a downloaded job-local reference bundle can satisfy PPI/KaKs."""
|
||||
"""Return whether job-local or config-resolved reference data can satisfy PPI/KaKs."""
|
||||
code = str(species_code or "").strip()
|
||||
if not job_id or not code:
|
||||
return False, "missing job_id/species_code"
|
||||
species_dir = INPUT_ROOT / job_id / code
|
||||
input_dir = INPUT_ROOT / job_id
|
||||
species_dir = input_dir / code
|
||||
pep_path = species_dir / f"{code}.pep.fasta"
|
||||
cds_path = species_dir / f"{code}.cds.fasta"
|
||||
if not pep_path.exists() or pep_path.stat().st_size <= 0:
|
||||
local_pep_ready = pep_path.exists() and pep_path.stat().st_size > 0
|
||||
local_cds_ready = cds_path.exists() and cds_path.stat().st_size > 0
|
||||
if local_pep_ready and (not require_cds or local_cds_ready):
|
||||
return True, str(species_dir)
|
||||
|
||||
try:
|
||||
scope = build_gene_family_scope(
|
||||
input_dir=input_dir,
|
||||
workspace=WORKSPACE_ROOT / job_id,
|
||||
prefix="",
|
||||
target_species=[],
|
||||
reference_species=[code],
|
||||
comparative_species=[],
|
||||
scope_mode="target_plus_reference",
|
||||
)
|
||||
species_files = scope.files.get(code, {})
|
||||
|
||||
def has_sequence(file_type: str) -> bool:
|
||||
return any(
|
||||
record.get("path")
|
||||
and Path(str(record["path"])).exists()
|
||||
and Path(str(record["path"])).stat().st_size > 0
|
||||
for record in species_files.get(file_type, [])
|
||||
)
|
||||
|
||||
if has_sequence("pep") and (not require_cds or has_sequence("cds")):
|
||||
return True, str(input_dir)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not local_pep_ready:
|
||||
return False, f"missing PEP: {pep_path}"
|
||||
if require_cds and (not cds_path.exists() or cds_path.stat().st_size <= 0):
|
||||
return False, f"missing CDS: {cds_path}"
|
||||
return True, str(species_dir)
|
||||
return False, f"missing CDS: {cds_path}"
|
||||
|
||||
|
||||
def _technical_reference_bundle_lookup_code(req: Any, species_code: str) -> str:
|
||||
"""Resolve the local bundle identity without changing the biological species code."""
|
||||
code = str(species_code or "").strip()
|
||||
storage_key = str(getattr(req, "ppiReferenceStorageKey", "") or "").strip()
|
||||
if not storage_key or not bool(getattr(req, "ppiReferenceUserConfirmed", False)):
|
||||
return code
|
||||
stage = str(getattr(req, "stage", "") or "").strip().lower()
|
||||
if stage == "ppi":
|
||||
return storage_key
|
||||
mapping_owner = str(getattr(req, "orthologMappingOwner", "") or "").strip().lower()
|
||||
if stage == "kaks" and mapping_owner == "ppi":
|
||||
return storage_key
|
||||
return code
|
||||
|
||||
|
||||
def _normalize_species_alias_for_job(value: str) -> str:
|
||||
@@ -1686,6 +1735,23 @@ def _looks_like_gene_family_workflow_query(message: str) -> bool:
|
||||
return any(token in text for token in ["基因家族分析", "家族分析", "完整分析", "全流程", "全部阶段", "完整流程"])
|
||||
|
||||
|
||||
def _full_gene_family_workflow_query_text(message: str, history: list[Any]) -> str:
|
||||
"""Recover the user's workflow request across the MainAgent confirmation turn."""
|
||||
current = str(message or "").strip()
|
||||
if _looks_like_gene_family_workflow_query(current):
|
||||
return current
|
||||
for item in reversed(history or []):
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
role = str(item.get("role") or item.get("sender") or "").strip().lower()
|
||||
if role not in {"user", "human"}:
|
||||
continue
|
||||
text = str(item.get("content") or item.get("message") or item.get("text") or "").strip()
|
||||
if _looks_like_gene_family_workflow_query(text):
|
||||
return text
|
||||
return ""
|
||||
|
||||
|
||||
def _looks_like_intraspecies_synteny_query(message: str) -> bool:
|
||||
text = str(message or "").lower()
|
||||
return (
|
||||
@@ -1989,7 +2055,7 @@ def _downstream_gene_family_stages_for_context(message: str) -> list[str]:
|
||||
stages = [
|
||||
"phylogeny", "motif", "domain", "structure", "combo",
|
||||
"promoter", "tfbs", "synteny", "interspecies_synteny", "karyotype", "pep",
|
||||
"ortholog", "kaks", "ppi",
|
||||
"ppi", "kaks",
|
||||
]
|
||||
if any(token in text for token in ["表达", "expression", "rna"]):
|
||||
stages.append("expression")
|
||||
@@ -1999,7 +2065,6 @@ def _downstream_gene_family_stages_for_context(message: str) -> list[str]:
|
||||
if any(token in text for token in ["种间", "比较物种", "interspecies"]):
|
||||
stages.append("interspecies_synteny")
|
||||
if any(token in text for token in ["kaks", "ka/ks", "选择压力"]):
|
||||
stages.append("ortholog")
|
||||
stages.append("kaks")
|
||||
if any(token in text for token in ["ppi", "互作"]):
|
||||
stages.append("ppi")
|
||||
@@ -2070,6 +2135,9 @@ def _resolve_contextual_gene_family_request(
|
||||
confirmed = _confirmed_subfamily_species(job_id, family)
|
||||
confirmed_species = [item["species"] for item in confirmed if item.get("source") == "confirmed"]
|
||||
mentioned = _extract_species_codes_from_text_for_job(job_id, text)
|
||||
has_explicit_scope = bool(
|
||||
mentioned or structured_targets or structured_refs or structured_comps
|
||||
)
|
||||
prefix_species = ""
|
||||
if request_prefix and "." in request_prefix:
|
||||
prefix_species = request_prefix.split(".", 1)[0].strip()
|
||||
@@ -2462,7 +2530,7 @@ def _stage_detail_defaults(stage_id: str) -> Dict[str, Any]:
|
||||
"interspecies_synteny": ["identify"],
|
||||
"karyotype": ["identify", "synteny"],
|
||||
"ortholog": ["identify"],
|
||||
"kaks": ["identify", "ortholog"],
|
||||
"kaks": ["identify"],
|
||||
"ppi": ["identify"],
|
||||
"expression": ["identify"],
|
||||
}
|
||||
@@ -2489,18 +2557,16 @@ FULL_GENE_FAMILY_WORKFLOW_STAGES = [
|
||||
"interspecies_synteny",
|
||||
"karyotype",
|
||||
"pep",
|
||||
"ortholog",
|
||||
"kaks",
|
||||
"ppi",
|
||||
"kaks",
|
||||
]
|
||||
|
||||
FORMAL_GENE_FAMILY_CORE_STAGES = {"phylogeny", "motif", "domain", "structure", "combo"}
|
||||
FULL_GENE_FAMILY_TARGET_ONLY_STAGES = {"promoter", "tfbs", "synteny", "karyotype", "pep"}
|
||||
FULL_GENE_FAMILY_TARGET_ONLY_STAGES = {"promoter", "tfbs", "synteny", "karyotype", "pep", "ppi"}
|
||||
FULL_GENE_FAMILY_REFERENCE_STAGES = FORMAL_GENE_FAMILY_CORE_STAGES | {
|
||||
"interspecies_synteny",
|
||||
"ortholog",
|
||||
"kaks",
|
||||
"ppi",
|
||||
}
|
||||
SCOPE_IDENTITY_PARAM_KEYS = (
|
||||
"scopeFile",
|
||||
@@ -2628,6 +2694,8 @@ def _normalize_full_gene_family_workflow_plan(
|
||||
if sid not in ordered:
|
||||
ordered.append(sid)
|
||||
for sid in stage_ids:
|
||||
if sid == "ortholog":
|
||||
continue
|
||||
if sid and sid not in ordered:
|
||||
ordered.append(sid)
|
||||
|
||||
@@ -2656,6 +2724,8 @@ def _normalize_full_gene_family_workflow_plan(
|
||||
detail["dependencies"] = ["phylogeny", "motif", "domain", "structure"]
|
||||
detail_params["comboPanels"] = ["motif", "domain", "structure"]
|
||||
detail_params["comboPanelSource"] = "regular_gene_family"
|
||||
if sid in {"ppi", "kaks"}:
|
||||
detail["dependencies"] = ["identify"]
|
||||
if sid in GENE_FAMILY_SCOPE_STAGES:
|
||||
detail["params"] = enrich_stage_params_with_context(sid, plan_prefix, detail_params)
|
||||
else:
|
||||
@@ -4989,12 +5059,6 @@ orchestrator = None
|
||||
langgraph_orchestrator = None
|
||||
if MAS_AVAILABLE:
|
||||
try:
|
||||
# 清除Python模块缓存,确保配置重新加载
|
||||
import sys
|
||||
modules_to_remove = [k for k in sys.modules.keys() if 'gfa_core.mas' in k]
|
||||
for m in modules_to_remove:
|
||||
del sys.modules[m]
|
||||
|
||||
# 加载MAS配置
|
||||
from gfa_core.mas.config import config as mas_config
|
||||
llm_config = mas_config.load_llm_config()
|
||||
@@ -5186,6 +5250,7 @@ def _prepare_execute_technical_reference_scope(
|
||||
stage == "ortholog"
|
||||
and str(getattr(req, "dependencyOwner", "") or "").strip().lower() == "ppi"
|
||||
)
|
||||
ppi_consumer_intent = stage == "ppi"
|
||||
if ppi_owner_intent:
|
||||
missing_fields = []
|
||||
if not bool(getattr(req, "ppiReferenceUserConfirmed", False)):
|
||||
@@ -5206,6 +5271,8 @@ def _prepare_execute_technical_reference_scope(
|
||||
)
|
||||
ppi_owned_ortholog = ppi_owner_intent
|
||||
if ppi_owned_ortholog:
|
||||
req.analysisRoute = "single_species"
|
||||
req.memberSetId = ""
|
||||
technical_reference_species = [
|
||||
str(getattr(req, "ppiReferenceStorageKey", "") or "").strip()
|
||||
]
|
||||
@@ -5264,12 +5331,22 @@ def _prepare_execute_technical_reference_scope(
|
||||
scope_params["scope_id"] = original_scope_id
|
||||
scope_params["refSpeciesList"] = list(identity.reference_species)
|
||||
scope_params["_validatedPpiOwnerScopeId"] = original_scope_id
|
||||
req.memberSetId = f"{original_scope_id}:single_species:all_members"
|
||||
else:
|
||||
scope_params["scopeMode"] = "target_only"
|
||||
scope_params["referenceSpecies"] = []
|
||||
scope_params["refSpeciesList"] = []
|
||||
scope_params["comparativeSpecies"] = []
|
||||
scope_params["scopeFile"] = ""
|
||||
elif ppi_consumer_intent:
|
||||
req.analysisRoute = "single_species"
|
||||
req.memberSetId = ""
|
||||
technical_reference_species = []
|
||||
scope_params["scopeMode"] = "target_only"
|
||||
scope_params["referenceSpecies"] = []
|
||||
scope_params["refSpeciesList"] = []
|
||||
scope_params["comparativeSpecies"] = []
|
||||
scope_params["scopeFile"] = ""
|
||||
else:
|
||||
technical_reference_species = []
|
||||
if stage in {"ortholog", "ppi", "kaks"}:
|
||||
@@ -6911,6 +6988,7 @@ class ExecuteRequest(BaseModel):
|
||||
ppiReferenceSelectionMode: str = ""
|
||||
ppiReferenceUserConfirmed: bool = False
|
||||
dependencyOwner: str = ""
|
||||
orthologMappingOwner: str = ""
|
||||
identifyBlastReferenceSpecies: List[str] = []
|
||||
primaryReferenceSpecies: List[str] = []
|
||||
refSpeciesList: List[str] = []
|
||||
@@ -7823,6 +7901,7 @@ def run_stage_sync(task_id: str, stage: str, job_id: str, params: dict):
|
||||
ppi_reference_selection_mode=params.get("ppiReferenceSelectionMode") or params.get("ppi_reference_selection_mode") or "",
|
||||
ppi_reference_user_confirmed=_bool_param("ppiReferenceUserConfirmed", "ppi_reference_user_confirmed", default=False),
|
||||
dependency_owner=params.get("dependencyOwner") or params.get("dependency_owner") or "",
|
||||
ortholog_mapping_owner=params.get("orthologMappingOwner") or params.get("ortholog_mapping_owner") or "",
|
||||
ppi_score=params.get("ppiScore", 200),
|
||||
# KaKs参数
|
||||
kaks_neutral_rate=params.get("kaksNeutralRate", 7.08e-9),
|
||||
@@ -8924,6 +9003,7 @@ class ExtensionStageResolveIntentRequest(BaseModel):
|
||||
class ExtensionStageEnvResolveRequest(BaseModel):
|
||||
python_imports: List[str] = Field(default_factory=list)
|
||||
r_packages: List[str] = Field(default_factory=list)
|
||||
env_declaration: Optional[str] = None
|
||||
|
||||
|
||||
|
||||
@@ -9020,6 +9100,24 @@ async def _r_packages_for_env(env_name: str) -> set[str]:
|
||||
|
||||
@app.post("/api/extension-stages/envs/resolve")
|
||||
async def resolve_extension_stage_env(req: ExtensionStageEnvResolveRequest):
|
||||
# 工具显式声明的环境优先(run.sh 里 # lightweight-env: <env>)
|
||||
if req.env_declaration:
|
||||
env = req.env_declaration.strip()
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
"conda", "run", "-n", env, "python", "--version",
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
await proc.communicate()
|
||||
if proc.returncode == 0:
|
||||
return {
|
||||
"success": True,
|
||||
"runtime_env": env,
|
||||
"reason": "explicit_env_declaration",
|
||||
"dependency": env,
|
||||
"fallback": False,
|
||||
}
|
||||
|
||||
python_imports = _normalized_dependency_names(req.python_imports)
|
||||
r_packages = _normalized_dependency_names(req.r_packages)
|
||||
if not python_imports and not r_packages:
|
||||
@@ -11654,7 +11752,12 @@ async def execute_stage(req: ExecuteRequest):
|
||||
for raw_value in raw_values:
|
||||
raw_code = str(raw_value or "").strip()
|
||||
if raw_code and raw_code.lower() in technical_ref_lookup:
|
||||
available, reason = _reference_bundle_species_available(job_id, raw_code, require_cds=require_cds)
|
||||
bundle_lookup_code = _technical_reference_bundle_lookup_code(req, raw_code)
|
||||
available, reason = _reference_bundle_species_available(
|
||||
job_id,
|
||||
bundle_lookup_code,
|
||||
require_cds=require_cds,
|
||||
)
|
||||
if not available:
|
||||
raise ValueError(
|
||||
f"PPI/KaKs 技术参考物种 {raw_code} 已选择但参考包不完整:{reason};"
|
||||
@@ -12354,6 +12457,7 @@ async def execute_stage(req: ExecuteRequest):
|
||||
"ppiScore": getattr(req, 'ppiScore', 200),
|
||||
# KaKs参数
|
||||
"kaksNeutralRate": getattr(req, 'kaksNeutralRate', 7.08e-9),
|
||||
"orthologMappingOwner": getattr(req, 'orthologMappingOwner', ''),
|
||||
# Genome Prepare参数
|
||||
"genomePrepareType": getattr(req, 'genomePrepareType', 'auto'),
|
||||
# Interspecies species selection
|
||||
@@ -15681,7 +15785,7 @@ def _apply_reference_bundle_stale_watchdog(task: Dict[str, Any]) -> None:
|
||||
progress = task.get("progress") if isinstance(task.get("progress"), dict) else {}
|
||||
progress = {
|
||||
"phase": progress.get("phase") or status or "running",
|
||||
"message": progress.get("message") or task.get("message") or "参考物种注释包仍在后台下载",
|
||||
"message": progress.get("message") or task.get("message") or "PPI and Ka/Ks reference species data are still downloading",
|
||||
"percentage": progress.get("percentage"),
|
||||
"indeterminate": progress.get("indeterminate", True),
|
||||
"pending_seconds": task["pending_seconds"],
|
||||
@@ -15699,13 +15803,13 @@ def _apply_reference_bundle_stale_watchdog(task: Dict[str, Any]) -> None:
|
||||
def run_reference_bundle_download_task(task_id: str, payload: Dict[str, Any]) -> None:
|
||||
tasks[task_id].update({
|
||||
"status": "running",
|
||||
"message": "正在后台下载参考物种注释包",
|
||||
"message": "Downloading PPI and Ka/Ks reference species PEP and CDS",
|
||||
"start_time": datetime.now().isoformat(),
|
||||
"timeout_sec": int(payload.get("timeout_sec") or 300),
|
||||
"payload": dict(payload),
|
||||
"progress": {
|
||||
"phase": "queued",
|
||||
"message": "参考物种注释包下载任务已启动",
|
||||
"message": "PPI and Ka/Ks reference species data download started",
|
||||
"percentage": 0,
|
||||
"indeterminate": True,
|
||||
},
|
||||
@@ -15736,7 +15840,7 @@ def run_reference_bundle_download_task(task_id: str, payload: Dict[str, Any]) ->
|
||||
progress.setdefault("indeterminate", percentage is None)
|
||||
tasks[task_id].update({
|
||||
"status": "running",
|
||||
"message": progress.get("message") or "正在后台下载参考物种注释包",
|
||||
"message": progress.get("message") or "Downloading PPI and Ka/Ks reference species PEP and CDS",
|
||||
"progress": progress,
|
||||
})
|
||||
_persist_reference_bundle_task(tasks[task_id])
|
||||
@@ -15755,11 +15859,11 @@ def run_reference_bundle_download_task(task_id: str, payload: Dict[str, Any]) ->
|
||||
success = result.get("status") == "success"
|
||||
tasks[task_id].update({
|
||||
"status": "success" if success else "failed",
|
||||
"message": "参考物种注释包下载完成" if success else "参考物种注释包下载未满足必需文件",
|
||||
"message": "PPI and Ka/Ks reference species data are ready" if success else "PPI and Ka/Ks reference species data are incomplete",
|
||||
"end_time": datetime.now().isoformat(),
|
||||
"progress": {
|
||||
"phase": "completed" if success else "failed",
|
||||
"message": "参考物种注释包下载完成" if success else "参考物种注释包下载未满足必需文件",
|
||||
"message": "PPI and Ka/Ks reference species data are ready" if success else "PPI and Ka/Ks reference species data are incomplete",
|
||||
"percentage": 100 if success else None,
|
||||
"indeterminate": False if success else True,
|
||||
},
|
||||
@@ -15802,7 +15906,7 @@ async def download_reference_bundle_api(req: ReferenceBundleDownloadRequest):
|
||||
"stage": "reference_bundle_download",
|
||||
"job_id": req.job_id,
|
||||
"status": "pending",
|
||||
"message": "参考物种注释包下载任务已提交",
|
||||
"message": "PPI and Ka/Ks reference species data download submitted",
|
||||
"created_at": datetime.now().isoformat(),
|
||||
"species_code": req.speciesCode,
|
||||
"species_name": req.speciesName,
|
||||
@@ -17349,11 +17453,6 @@ async def _run_agent_chat(
|
||||
executes analysis stages here.
|
||||
"""
|
||||
session_id = request.session_id or f"session_{int(time.time())}"
|
||||
# Inject language instruction for non-Chinese languages
|
||||
if request.language and request.language != DEFAULT_AGENT_LANG:
|
||||
lang_map = {"en": "Please respond in English."}
|
||||
lang_instruction = lang_map.get(request.language, f"Please respond in {request.language}.")
|
||||
request.message = f"{lang_instruction}\n\n{request.message}"
|
||||
completed_stages: list[str] = []
|
||||
if request.job_id:
|
||||
final_dir = PROJECT_ROOT / "workspace" / request.job_id / "final"
|
||||
@@ -17850,6 +17949,11 @@ async def _run_agent_chat(
|
||||
if isinstance(outcome.get("proposal"), dict)
|
||||
else None
|
||||
),
|
||||
recommendation=(
|
||||
outcome.get("recommendation")
|
||||
if isinstance(outcome.get("recommendation"), dict)
|
||||
else None
|
||||
),
|
||||
)
|
||||
|
||||
return {
|
||||
@@ -17871,8 +17975,17 @@ async def _run_agent_chat(
|
||||
"model_used": request.model,
|
||||
"session_id": session_id,
|
||||
"input_paths": list(outcome.get("input_paths") or []),
|
||||
"recommendation": (
|
||||
outcome.get("recommendation")
|
||||
if isinstance(outcome.get("recommendation"), dict)
|
||||
else None
|
||||
),
|
||||
"handoff_request": (
|
||||
str((outcome.get("proposal") or {}).get("request") or "").strip()
|
||||
str(
|
||||
(outcome.get("proposal") or {}).get("original_request")
|
||||
or (outcome.get("proposal") or {}).get("request")
|
||||
or ""
|
||||
).strip()
|
||||
if isinstance(outcome.get("proposal"), dict)
|
||||
else request.message
|
||||
) or request.message,
|
||||
@@ -18752,6 +18865,41 @@ class AgentPlanRequest(BaseModel):
|
||||
language: str = DEFAULT_AGENT_LANG # "zh" or "en"
|
||||
|
||||
|
||||
def _authorized_workflow_action_stages(intent: Dict[str, Any]) -> List[str]:
|
||||
candidate = intent or {}
|
||||
params = candidate.get("params") or {}
|
||||
action_id = str(
|
||||
candidate.get("workflow_action_id") or params.get("workflowActionId") or ""
|
||||
).strip()
|
||||
authorized = list(dict.fromkeys(_param_list(params.get("authorizedStages"))))
|
||||
declared = set(_param_list(candidate.get("stages")))
|
||||
if not action_id or not authorized or not set(authorized).issubset(declared):
|
||||
return []
|
||||
try:
|
||||
from gfa_core.mas.workflow_catalog import load_canonical_workflow_contract
|
||||
|
||||
contract = load_canonical_workflow_contract("gene_family")
|
||||
transition = next(item for item in contract.transitions if item.id == action_id)
|
||||
actions = {item.id: item for item in contract.actions}
|
||||
evidence_stages = {
|
||||
item.id: item.stage for item in contract.evidence_requirements
|
||||
}
|
||||
contract_stages = [
|
||||
evidence_stages[requirement_id]
|
||||
for requirement_id in transition.evidence_requirement_ids
|
||||
]
|
||||
contract_stages.append(actions[transition.action_id].target_stage)
|
||||
except (ImportError, StopIteration, KeyError):
|
||||
return []
|
||||
if set(authorized) != set(contract_stages):
|
||||
return []
|
||||
return [
|
||||
stage
|
||||
for stage in SUBFAMILY_PREDICTION_PLAN_STAGES
|
||||
if stage in set(contract_stages)
|
||||
]
|
||||
|
||||
|
||||
SUBFAMILY_PREDICTION_PLAN_STAGES = [
|
||||
"identify",
|
||||
"phylogeny",
|
||||
@@ -19035,9 +19183,13 @@ def _build_contextual_gene_family_plan_response(request: Any, session_id: str, c
|
||||
detail["params"] = enrich_stage_params_with_context(stage_id, plan_prefix, detail_params)
|
||||
stages_detail.append(detail)
|
||||
|
||||
if _looks_like_gene_family_workflow_query(getattr(request, "message", "") or "") and not subfamily_prerequisite_mode:
|
||||
full_workflow_query = _full_gene_family_workflow_query_text(
|
||||
getattr(request, "message", "") or "",
|
||||
getattr(request, "history", []) or [],
|
||||
)
|
||||
if full_workflow_query and not subfamily_prerequisite_mode:
|
||||
stage_ids, stages_detail = _normalize_full_gene_family_workflow_plan(
|
||||
message=getattr(request, "message", "") or "",
|
||||
message=full_workflow_query,
|
||||
stage_ids=stage_ids,
|
||||
stages_detail=stages_detail,
|
||||
plan_params=plan_params,
|
||||
@@ -20839,6 +20991,7 @@ async def agent_plan(request: AgentPlanRequest):
|
||||
request.intent = dict(request.intent or {})
|
||||
request.intent["params"] = dict(request.intent.get("params") or {})
|
||||
_apply_reconciled_species_to_intent(request.intent, plan_role_resolution)
|
||||
authorized_workflow_stages = _authorized_workflow_action_stages(request.intent)
|
||||
query_lower_for_context = (request.message or "").lower()
|
||||
result_report_query = (
|
||||
"结果报告" in query_lower_for_context
|
||||
@@ -20908,7 +21061,7 @@ async def agent_plan(request: AgentPlanRequest):
|
||||
and not _looks_like_gene_family_workflow_query(query_lower_for_context)
|
||||
and not _requires_confirmed_subfamily_query(query_lower_for_context)
|
||||
)
|
||||
if subfamily_prediction_query_for_context:
|
||||
if subfamily_prediction_query_for_context or authorized_workflow_stages:
|
||||
requested_prefix_for_subfamily = request.prefix or ""
|
||||
prefix_species, prefix_family = _prefix_species_family(requested_prefix_for_subfamily)
|
||||
family_for_subfamily = _resolve_authoritative_gene_family(
|
||||
@@ -20971,7 +21124,27 @@ async def agent_plan(request: AgentPlanRequest):
|
||||
}
|
||||
if family_for_subfamily and targets_for_subfamily:
|
||||
prefix_for_subfamily = f"{targets_for_subfamily[0]}.{family_for_subfamily}" if len(targets_for_subfamily) == 1 else f"Target.{family_for_subfamily}"
|
||||
deterministic_stages = ["identify", "phylogeny", "motif", "domain", "structure", "combo", "reference_classification"]
|
||||
deterministic_stages = authorized_workflow_stages or [
|
||||
"identify",
|
||||
"phylogeny",
|
||||
"motif",
|
||||
"domain",
|
||||
"structure",
|
||||
"combo",
|
||||
"reference_classification",
|
||||
]
|
||||
authorized_action_id = str(
|
||||
(request.intent or {}).get("workflow_action_id")
|
||||
or ((request.intent or {}).get("params") or {}).get(
|
||||
"workflowActionId"
|
||||
)
|
||||
or ""
|
||||
).strip()
|
||||
plan_description = (
|
||||
f"执行已确认的工作流业务动作 {authorized_action_id}"
|
||||
if authorized_workflow_stages
|
||||
else "执行 target/comparative + reference 亚家族分类"
|
||||
)
|
||||
classification_species_for_subfamily = list(targets_for_subfamily)
|
||||
classification_species_for_subfamily, role_targets_for_subfamily, role_comps_for_subfamily = _split_subfamily_selected_species_by_config_roles(
|
||||
request.job_id,
|
||||
@@ -20993,6 +21166,12 @@ async def agent_plan(request: AgentPlanRequest):
|
||||
"confirmPrefixBySpecies": confirm_prefix_by_species,
|
||||
"subfamilyState": subfamily_state_for_plan,
|
||||
}
|
||||
if authorized_workflow_stages:
|
||||
request_action_params = (request.intent or {}).get("params") or {}
|
||||
plan_params["workflowActionId"] = authorized_action_id
|
||||
plan_params["authorizedStages"] = _param_list(
|
||||
request_action_params.get("authorizedStages")
|
||||
)
|
||||
if identify_selection_for_subfamily:
|
||||
plan_params["identifySpeciesSelection"] = _identify_selection_with_default(
|
||||
identify_selection_for_subfamily,
|
||||
@@ -21062,7 +21241,7 @@ async def agent_plan(request: AgentPlanRequest):
|
||||
"comparative_species": role_comps_for_subfamily,
|
||||
"scopeMode": plan_params["scopeMode"],
|
||||
"prefix": prefix_for_subfamily,
|
||||
"description": "执行 target/comparative + reference 亚家族分类",
|
||||
"description": plan_description,
|
||||
"estimated_time": f"{estimated_minutes}分钟",
|
||||
"estimated_total_time": f"{estimated_minutes}分钟",
|
||||
"execution_strategy": "parallel",
|
||||
@@ -21084,10 +21263,15 @@ async def agent_plan(request: AgentPlanRequest):
|
||||
"comparative_species": role_comps_for_subfamily,
|
||||
"scopeMode": plan_params["scopeMode"],
|
||||
"stages": deterministic_stages,
|
||||
"description": "执行目标物种+参考物种亚家族分类",
|
||||
"description": plan_description,
|
||||
"confidence": 0.98,
|
||||
"prefix": prefix_for_subfamily,
|
||||
"params": plan_params,
|
||||
**(
|
||||
{"workflow_action_id": authorized_action_id}
|
||||
if authorized_action_id
|
||||
else {}
|
||||
),
|
||||
},
|
||||
"completed_stages": completed_stages,
|
||||
"session_id": session_id,
|
||||
@@ -22810,9 +22994,13 @@ async def agent_plan(request: AgentPlanRequest):
|
||||
intent["gene_family"] = combo_family
|
||||
|
||||
full_workflow_normalized = False
|
||||
if _looks_like_gene_family_workflow_query(request.message or "") and not subfamily_prediction_query:
|
||||
full_workflow_query = _full_gene_family_workflow_query_text(
|
||||
request.message or "",
|
||||
request.history or [],
|
||||
)
|
||||
if full_workflow_query and not subfamily_prediction_query:
|
||||
stage_ids, stages_detail = _normalize_full_gene_family_workflow_plan(
|
||||
message=request.message or "",
|
||||
message=full_workflow_query,
|
||||
stage_ids=stage_ids,
|
||||
stages_detail=stages_detail,
|
||||
plan_params=plan_params,
|
||||
@@ -22844,7 +23032,7 @@ async def agent_plan(request: AgentPlanRequest):
|
||||
|
||||
if full_workflow_normalized:
|
||||
stage_ids, stages_detail = _normalize_full_gene_family_workflow_plan(
|
||||
message=request.message or "",
|
||||
message=full_workflow_query,
|
||||
stage_ids=stage_ids,
|
||||
stages_detail=stages_detail,
|
||||
plan_params=plan_params,
|
||||
@@ -23079,6 +23267,7 @@ class AgentRunRequest(BaseModel):
|
||||
job_id: str = ""
|
||||
intent: dict = {} # 前端可传入已有的intent
|
||||
stages: list = [] # 要执行的阶段列表
|
||||
language: str = DEFAULT_AGENT_LANG # "zh" or "en"
|
||||
|
||||
|
||||
@app.post("/api/agent/run")
|
||||
@@ -23112,6 +23301,7 @@ async def agent_run(request: AgentRunRequest):
|
||||
)
|
||||
|
||||
# 添加额外的运行时字段
|
||||
initial_state["language"] = request.language
|
||||
initial_state["status"] = "running"
|
||||
initial_state["intent_result"] = request.intent or {} # 传入已有的 intent
|
||||
initial_state["completed_stages"] = []
|
||||
|
||||
@@ -712,5 +712,179 @@ window.__I18N_EN__ = {
|
||||
" 已暂停依赖 confirmed_subfamily.tsv": " has paused stages depending on confirmed_subfamily.tsv",
|
||||
";": ";",
|
||||
" 个分析阶段": " analysis stages",
|
||||
" 个阶段": " stages"
|
||||
" 个阶段": " stages",
|
||||
|
||||
// ===== 阶段 7:ElMessage/ElMessageBox i18n keys =====
|
||||
// 注意:key 为中文原文(zh 模式直接返回 key),value 为 English
|
||||
// tp() 类 key 需要同时注册变量 placeholder 版本和 static 文案
|
||||
|
||||
// ElMessageBox
|
||||
"删除会话": "Delete Session",
|
||||
"取消 PPI": "Cancel PPI",
|
||||
"自动注释并继续": "Auto-annotate & Continue",
|
||||
"我先去上传背景文件": "Upload Background Files First",
|
||||
"确认设计并写入": "Confirm Design & Write",
|
||||
"返回修改": "Return to Modify",
|
||||
"确认第一候选": "Confirm First Candidate",
|
||||
"使用 bcftools 重试": "Retry with bcftools",
|
||||
"GATK 执行失败": "GATK Execution Failed",
|
||||
"确认 DE 分析设计": "Confirm DE Analysis Design",
|
||||
"确认 PPI 的 STRING 近源参考物种": "Confirm STRING Close Reference Species for PPI",
|
||||
|
||||
// autoAnnotation 标题
|
||||
"GO/KEGG 富集背景缺失": "GO/KEGG Enrichment Background Missing",
|
||||
"KEGG 富集背景缺失": "KEGG Enrichment Background Missing",
|
||||
"GO 富集背景缺失": "GO Enrichment Background Missing",
|
||||
"RNA-seq 富集背景缺失": "RNA-seq Enrichment Background Missing",
|
||||
|
||||
// 会话管理
|
||||
"示例下载功能当前不可用": "Example download is currently unavailable",
|
||||
"已Clear当前会话": "Current session cleared",
|
||||
|
||||
// autoFill
|
||||
"请至少填写一个要预览修改的字段": "Please fill at least one field to preview modifications",
|
||||
"没有匹配的草稿行": "No matching draft rows",
|
||||
"请先预览要应用的修改": "Please preview modifications before applying",
|
||||
"请先生成或加载草稿": "Please generate or load a draft first",
|
||||
"生成草稿失败": "Failed to generate draft",
|
||||
"确认失败": "Confirmation failed",
|
||||
"已写入正式 config.xlsx": "Formal config.xlsx written",
|
||||
"已写入正式 samples.yaml": "Formal samples.yaml written",
|
||||
"请输入文件路径": "Please enter file path",
|
||||
"加载失败": "Load failed",
|
||||
|
||||
// RAG 解读
|
||||
"请求解读服务失败": "Interpretation service request failed",
|
||||
"RAG解读已刷新": "RAG interpretation refreshed",
|
||||
"RAG解读刷新失败": "RAG interpretation refresh failed",
|
||||
"下载报告失败": "Failed to download report",
|
||||
"JSON数据已加载到控制台": "JSON data loaded to console",
|
||||
"解读内容已复制": "Interpretation content copied",
|
||||
"复制失败": "Copy failed",
|
||||
|
||||
// 知识图谱
|
||||
"知识图谱加载完成": "Knowledge graph loaded",
|
||||
"未找到匹配的节点": "No matching node found",
|
||||
"视图已重置": "View reset",
|
||||
"请先通过问答加载子图,或点击「重新加载」": "Please load subgraph via Q&A first, or click Reload",
|
||||
|
||||
// PPI/STRING
|
||||
"当前没有可用 job_id": "No job_id currently available",
|
||||
"该 ID 已存在": "This ID already exists",
|
||||
"至少保留一个 gene ID": "Keep at least one gene ID",
|
||||
|
||||
// Working Directory
|
||||
"切换失败": "Switch failed",
|
||||
|
||||
// 图片/可视化
|
||||
"请先选择一张图片": "Please select an image first",
|
||||
"无法确定启动子输出前缀。": "Cannot determine promoter output prefix.",
|
||||
"请至少选择一个启动子元件或类别。": "Please select at least one promoter element or category.",
|
||||
"请至少选择一种图类型。": "Please select at least one plot type.",
|
||||
"无法确定进化树输出标签,请先选择树图或填写输出标签。": "Cannot determine phylogeny tree output label. Please select a tree image or fill in the output label first.",
|
||||
"进化树图重绘完成,已切换到新图": "Phylogeny tree replot complete; switched to new image",
|
||||
"无法确定种内共线性输出标签,请先选择 Circos 图。": "Cannot determine intraspecies synteny output label. Please select a Circos image first.",
|
||||
"无法确定 combo 输出标签,请先选择 combo 图。": "Cannot determine combo output label. Please select a combo image first.",
|
||||
"Combo 修订至少选择两个类型。": "Combo revision requires at least two panel types.",
|
||||
"Combo 图修订完成,已切换到新图": "Combo plot revision complete; switched to new image",
|
||||
"无法确定MSA输出标签,请先选择对应图片或填写输出标签。": "Cannot determine MSA output label. Please select corresponding image or fill in output label.",
|
||||
"请填写MSA alignment 起始列和终止列,或选择其他模式。": "Please fill in MSA alignment start/end columns or choose another mode.",
|
||||
"请填写参考基因及其起止残基编号。": "Please fill in reference gene and its start/end residue numbers.",
|
||||
|
||||
// GWAS/诊断
|
||||
"请填写 GWAS 物种简称(用于结果前缀)": "Please fill in GWAS species abbreviation (for result prefix)",
|
||||
"请先选择有效 VCF": "Please select a valid VCF first",
|
||||
"请先SelectAnalysis Stage": "Please select an analysis stage first",
|
||||
"诊断未发现明确问题,请查看阶段错误日志。": "Diagnosis found no clear issues. Please check stage error logs.",
|
||||
"诊断仅提示信息,无需确认修复。": "Diagnosis is informational only; no fix confirmation needed.",
|
||||
"✅ 已应用修复方案,继续执行...": "✅ Fix applied; continuing execution...",
|
||||
"❌ 应用修复失败": "❌ Fix application failed",
|
||||
"❌ 网络错误,请重试": "❌ Network error; please retry",
|
||||
"⏸ 已跳过修复;该问题会阻断下游,后续阶段已暂停。": "⏸ Fix skipped; issue blocks downstream; subsequent stages paused.",
|
||||
"已关闭诊断提示。": "Diagnosis prompt closed.",
|
||||
|
||||
// 任务/消息
|
||||
"当前消息未成功写入会话历史,刷新后可能丢失": "Message not persisted to session history; may be lost on refresh",
|
||||
"路径已复制": "Path copied",
|
||||
|
||||
// 拓展分析
|
||||
"当前筛选没有成员,未重建 final_members。": "No members matching current filter; final_members not rebuilt.",
|
||||
"当前选择为按物种分别分析,但后端没有返回完整的分物种亚家族结果;将尝试打开可用的联合结果。": "Per-species analysis selected but backend returned incomplete results; attempting to open available joint results.",
|
||||
"亚家族预测已完成,但未能自动打开确认表;请发送\"调出亚家族确认表\"手动打开。": "Subfamily prediction completed but confirmation table could not open automatically; please send \"show subfamily review table\" manually.",
|
||||
"没有可确认的亚家族标签。": "No subfamily labels to confirm.",
|
||||
"已请求停止当前拓展分析;状态仍会继续轮询直到进程退出。": "Stop requested for current extension; polling will continue until process exits.",
|
||||
"No running task to stop": "No running task to stop",
|
||||
"Task(s) stopped (backend + Celery when available)": "Task(s) stopped (backend + Celery when available)",
|
||||
"File uploaded successfully": "File uploaded successfully",
|
||||
"未检测到可用文件": "No usable files detected",
|
||||
"请先生成拓展分析阶段。": "Please generate the extension stage first.",
|
||||
"发布前验收已通过": "Pre-publish validation passed",
|
||||
"请填写名称,并上传原始代码。": "Please provide name and upload source code.",
|
||||
"拓展分析阶段已生成并发布": "Extension stage generated and published",
|
||||
"确认已保存": "Confirmation saved",
|
||||
"验证成功": "Validation successful",
|
||||
"验证未通过": "Validation not passed",
|
||||
"拓展分析阶段已发布": "Extension stage published",
|
||||
"拓展分析阶段执行完成": "Extension stage execution complete",
|
||||
"拓展分析阶段执行未成功,请查看结果日志": "Extension stage execution incomplete; please check result logs",
|
||||
"该阶段已在正式列表中": "This stage is already in the formal list",
|
||||
"已加入正式阶段列表,并默认纳入本次执行": "Added to formal stage list and included in this execution",
|
||||
|
||||
// 方法学
|
||||
"请先选择 PDF 或 MinerU content_list JSON 文献文件": "Please select a PDF or MinerU content_list JSON literature file first",
|
||||
"方法学提取启动失败": "Methodology extraction failed to start",
|
||||
"方法学 sandbox 示例读取失败": "Failed to read methodology sandbox examples",
|
||||
"请先选择或输入 methodology_dir": "Please select or enter methodology_dir first",
|
||||
"方法学 sandbox 已构建": "Methodology sandbox built",
|
||||
"请先构建临时库": "Please build the sandbox first",
|
||||
"请输入方法学问题": "Please enter methodology question",
|
||||
"方法学 sandbox 查询失败": "Methodology sandbox query failed",
|
||||
"方法学方案已保存,可先准备数据,稍后再按该方案执行": "Methodology plan saved; you may prepare data first and execute later",
|
||||
"方法学方案保存失败": "Failed to save methodology plan",
|
||||
"当前已有批量任务在执行,请等待完成后再提交方法学流程": "A batch task is currently running; please wait for completion before submitting methodology workflow",
|
||||
"请至少选择一个可执行的 bbav5 阶段": "Please select at least one executable bbav5 stage",
|
||||
"亚家族参考物种未写入当前 job 配置,已生成方法学本地计划,请在批量确认页核对参考物种": "Subfamily reference species not written to current job config; local methodology plan generated. Please verify reference species on batch confirmation page.",
|
||||
"方法学执行规划失败": "Methodology execution planning failed",
|
||||
|
||||
// Other
|
||||
"请Input内容": "Please enter content",
|
||||
"请先选择或创建 Working Directory": "Please select or create a Working Directory first",
|
||||
"规划请求失败": "Plan request failed",
|
||||
"重新绘图功能开发中...": "Replot feature is under development...",
|
||||
|
||||
// ===== tp() keys — 使用完整中文模板作为 key =====
|
||||
"确定删除会话「{title}」吗?该操作会从列表中移除,但保留后台记录。": "Delete session \"{title}\"? This removes it from the list but keeps backend records.",
|
||||
"切换到: {title}": "Switched to: {title}",
|
||||
"预览 {rows} 行:{fields}。确认无误后点击 Apply。": "Preview {rows} rows: {fields}. Click Apply once confirmed.",
|
||||
"已应用 {rows} 行修改。": "Applied {rows} row modifications.",
|
||||
"已取消预览,表格已恢复到预览前。": "Preview cancelled, table restored.",
|
||||
"链特异性为必填:还有 {count} 个样本未填写({examples})": "Library strandness required: {count} sample(s) unfilled ({examples})",
|
||||
"设计提示:{error};将保留原流程的兼容处理。": "Design tip: {error}; keeping original compatible handling.",
|
||||
"已加载 {count} files": "Loaded {count} files",
|
||||
"加载图谱失败: {message}": "Graph load failed: {message}",
|
||||
"找到节点: {name}": "Node found: {name}",
|
||||
"读取表格失败: {reason}": "Table read failed: {reason}",
|
||||
"PPI Reset 已完成: {reset_id}": "PPI Reset complete: {reset_id}",
|
||||
"STRING 链接文件内容:{preview}": "STRING link file content: {preview}",
|
||||
"读取文件失败: {message}": "File read failed: {message}",
|
||||
"已创建新Working Directory: {job_id}": "New Working Directory created: {job_id}",
|
||||
"创建失败: {message}": "Creation failed: {message}",
|
||||
"获取Working Directory列表失败: {message}": "Failed to get Working Directory list: {message}",
|
||||
"已切换到Working Directory: {job_id}": "Switched to Working Directory: {job_id}",
|
||||
"切换失败: {message}": "Switch failed: {message}",
|
||||
"进化树图重绘失败:{message}": "Phylogeny tree replot failed: {message}",
|
||||
"Combo 图修订失败:{message}": "Combo plot revision failed: {message}",
|
||||
"任务: {message}\n状态: {status}\n进度: {progress}%": "Task: {message}\nStatus: {status}\nProgress: {progress}%",
|
||||
"停止拓展分析失败:{message}": "Failed to stop extension: {message}",
|
||||
"已选择 {count} 个原始代码文件": "{count} code file(s) selected",
|
||||
"已选择 {count} 个测试数据文件": "{count} test data file(s) selected",
|
||||
"已拖入 {count} 个文件,已自动填充拓展包": "{count} file(s) dropped; extension package auto-filled",
|
||||
"❌ {message}": "❌ {message}",
|
||||
"Upload complete. Latest Excel is now used as config.xlsx.{warningText}": "Upload complete. Latest Excel is now used as config.xlsx.{warningText}",
|
||||
"{title},需要自动注释": "{title}, Auto-annotation Required",
|
||||
|
||||
// String close species (tp with multi-line)
|
||||
"目标物种不在 STRING 固定快照中。请确认是否采用排序第一的近源物种:": "Target species not in STRING fixed snapshot. Confirm using the top-ranked close species:",
|
||||
"确认后才会下载参考 PEP、生成 Ortholog,并执行 PPI。": "Reference PEP will be downloaded, Ortholog generated, and PPI executed only after confirmation.",
|
||||
"是否切换为\"自动注释 + 继续富集\"?": "Switch to \"Auto-annotation + Continue Enrichment\"?"
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user