tbtools-cli 1.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. tbtools_cli/__init__.py +18 -0
  2. tbtools_cli/auto_commands.py +1149 -0
  3. tbtools_cli/cli.py +528 -0
  4. tbtools_cli/cli_load.py +352 -0
  5. tbtools_cli/cli_rpc.py +303 -0
  6. tbtools_cli/cli_tools_registry.py +93 -0
  7. tbtools_cli/cli_top.py +1160 -0
  8. tbtools_cli/command_metadata.json +4606 -0
  9. tbtools_cli/command_spec.py +382 -0
  10. tbtools_cli/config.example.toml +19 -0
  11. tbtools_cli/config.py +43 -0
  12. tbtools_cli/core.py +508 -0
  13. tbtools_cli/errors.py +38 -0
  14. tbtools_cli/presets.py +100 -0
  15. tbtools_cli/scenarios.py +92 -0
  16. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/AdmixtureCli.java +91 -0
  17. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/AmazingMetaCli.java +78 -0
  18. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/AssemblyRecommandCli.java +47 -0
  19. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/BamIndexCli.java +31 -0
  20. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/BamSortCli.java +43 -0
  21. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/BamStateCli.java +49 -0
  22. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/BarPlotterCli.java +18 -0
  23. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/BarplotCli.java +168 -0
  24. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/BatchVizMotifsCli.java +47 -0
  25. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/BlastXmlConvertCli.java +45 -0
  26. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/BlatExecutorCli.java +66 -0
  27. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/CalcRepeatCli.java +62 -0
  28. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/CalculateSimilarityCli.java +34 -0
  29. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/CddMotifCli.java +46 -0
  30. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/CircleGeneViewerCli.java +81 -0
  31. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/CircosCli.java +76 -0
  32. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/ColorSchemeCli.java +29 -0
  33. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/CtgGroupCli.java +29 -0
  34. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/CubeHeatmapCli.java +70 -0
  35. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/DeHistCli.java +46 -0
  36. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/DegramdomCli.java +33 -0
  37. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/DiffExpCli.java +44 -0
  38. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/DistanceCli.java +53 -0
  39. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/DualSynCli.java +147 -0
  40. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/EggnogCli.java +39 -0
  41. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/ExprCorrCli.java +27 -0
  42. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/FastaMergerCli.java +36 -0
  43. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/FastaTableConvertCli.java +38 -0
  44. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/FileCleanerCli.java +31 -0
  45. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/FileSplitCli.java +15 -0
  46. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/FindBlockDualCli.java +71 -0
  47. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/FindBlockMultipleCli.java +67 -0
  48. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/FindPathCli.java +15 -0
  49. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/GSEAWrapperCli.java +33 -0
  50. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/GenBank2FastaCli.java +32 -0
  51. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/GeneDensityCli.java +38 -0
  52. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/GeneLocGffCli.java +140 -0
  53. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/GeneStructureCli.java +90 -0
  54. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/GenericCli.java +177 -0
  55. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/GetENALinksCli.java +36 -0
  56. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/GetSubNewickTreeCli.java +50 -0
  57. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/GoEnrichCli.java +42 -0
  58. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/GoLevelCli.java +59 -0
  59. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/GroupedBarCli.java +95 -0
  60. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/GsaDiagCli.java +45 -0
  61. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/GxfFilterCli.java +39 -0
  62. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/GxfSortCli.java +23 -0
  63. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/HclustCli.java +43 -0
  64. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/HeatmapCli.java +89 -0
  65. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/HmmerSuiteCli.java +47 -0
  66. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/JgblocksCli.java +65 -0
  67. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/KeggEnrichCli.java +31 -0
  68. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/LayoutHeatmapCli.java +90 -0
  69. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MCScanXCli.java +80 -0
  70. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MCScanXFastCli.java +41 -0
  71. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MSACli.java +46 -0
  72. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MakeMotifCli.java +39 -0
  73. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MarkerDesignCli.java +62 -0
  74. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MarkerToolsCli.java +57 -0
  75. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/Mast2TabCli.java +27 -0
  76. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MastCli.java +54 -0
  77. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MastRunCli.java +39 -0
  78. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/Meme2TabCli.java +26 -0
  79. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MemeCli.java +74 -0
  80. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MemeRunCli.java +40 -0
  81. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MgGxfCli.java +35 -0
  82. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MicroSynCli.java +118 -0
  83. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MirIdentifyCli.java +55 -0
  84. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MotifCli.java +75 -0
  85. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MotifPatternCli.java +67 -0
  86. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MotifShiftCli.java +30 -0
  87. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MountainPlotCli.java +44 -0
  88. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MultiSuperHeatCli.java +64 -0
  89. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/NeedlemanWunschCli.java +76 -0
  90. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/PafGC.java +15 -0
  91. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/PafVizCli.java +67 -0
  92. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/PeakDistCli.java +58 -0
  93. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/Pep2CodonCli.java +26 -0
  94. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/PfamMotifCli.java +43 -0
  95. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/PhyloTreeCli.java +92 -0
  96. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/PileUpCli.java +55 -0
  97. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/PlantCAREResultClassifyCli.java +36 -0
  98. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/PubmedSearchCli.java +25 -0
  99. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/QpcrCli.java +69 -0
  100. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/QpcrDdctCli.java +28 -0
  101. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/QpcrProcCli.java +28 -0
  102. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/QuickGenomeDotCli.java +66 -0
  103. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/QuickProteinAnnoCli.java +34 -0
  104. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/QuickRunIQtreeCli.java +69 -0
  105. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/QuickTrimALCli.java +67 -0
  106. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/RNAplotCli.java +129 -0
  107. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/RegionBedToGFF3Cli.java +43 -0
  108. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/RegionDepthCli.java +29 -0
  109. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/SamBamCovCli.java +33 -0
  110. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/SeqConverterCli.java +17 -0
  111. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/SeqLenTrackCli.java +41 -0
  112. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/SeveralSpeciesCli.java +132 -0
  113. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/SimpleEnricherCli.java +44 -0
  114. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/SimpleHmmscanCli.java +32 -0
  115. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/SixFrameTranlaterCli.java +35 -0
  116. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/StructAnnoCompareCli.java +66 -0
  117. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/SubmitSMARTCli.java +30 -0
  118. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/SuperCircosCli.java +330 -0
  119. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/TableColManipCli.java +56 -0
  120. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/TableCollapseCli.java +33 -0
  121. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/TargetScoreCli.java +47 -0
  122. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/TauCalcCli.java +28 -0
  123. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/TaxonomyBatchCli.java +72 -0
  124. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/TreeCli.java +75 -0
  125. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/TreeRootingCli.java +30 -0
  126. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/TrimMSACli.java +33 -0
  127. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/UnrootedTreeCli.java +44 -0
  128. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/UpSetCli.java +90 -0
  129. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/Venn5Cli.java +60 -0
  130. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/Venn6Cli.java +65 -0
  131. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/ViolinCli.java +66 -0
  132. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/VisualizeCli.java +57 -0
  133. tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/VizGFACli.java +39 -0
  134. tbtools_cli-1.2.0.dist-info/METADATA +504 -0
  135. tbtools_cli-1.2.0.dist-info/RECORD +139 -0
  136. tbtools_cli-1.2.0.dist-info/WHEEL +5 -0
  137. tbtools_cli-1.2.0.dist-info/entry_points.txt +2 -0
  138. tbtools_cli-1.2.0.dist-info/licenses/LICENSE +21 -0
  139. tbtools_cli-1.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,352 @@
1
+ """cli_load.py — 动态命令注册(架构重构批次 B:从 cli.py 拆分)
2
+
3
+ CATEGORY_MAP/GROUPS 分组映射 + _load_auto_commands/_load_dynamic_commands +
4
+ _make_passthrough(metadata 驱动)+ ToolGroup 兜底注册。
5
+ 由 cli.py 在 cli 定义后调用 build_and_load(cli) 完成装配。
6
+ """
7
+ import difflib
8
+ import os
9
+ import re
10
+ import shutil
11
+ import subprocess
12
+ import sys
13
+
14
+ import click
15
+
16
+ from tbtools_cli import auto_commands as _ac
17
+ from tbtools_cli.core import ROOT, _, check_input_format, get_pitfall_hint, validate_file
18
+ from tbtools_cli.presets import PRESETS, apply_preset
19
+
20
+
21
+ # ---- 动态加载剩余命令 ----
22
+ # ---- 命令分类映射 ----
23
+ CATEGORY_MAP = {
24
+ "sixframe": "seq", # GUI 逆向:六框翻译
25
+ "longestorf": "seq", # GUI 逆向:最长 ORF 预测
26
+ "protparam": "seq", # GUI 逆向:蛋白理化性质
27
+ "genomefilter": "seq", # GUI 逆向:序列长度过滤
28
+ "seqpattern": "seq", # GUI 逆向:序列模式定位
29
+ "bed2gff3": "gxf", # GUI 逆向:BED→GFF3
30
+ "careclassify": "seq", # GUI 逆向:PlantCARE 元件分类
31
+ "protsim": "seq", # GUI 逆向:蛋白相似度矩阵
32
+ "xml2blasttab": "blast", "xml2pairwise": "blast", # GUI 逆向:BLAST XML 转换
33
+ "fa2tab": "seq", "tab2fa": "seq", # GUI 逆向:FASTA↔表
34
+ "muscle": "seq", # GUI 逆向:MUSCLE 比对(系统二进制)
35
+ "trimal": "seq", # GUI 逆向:trimAl 修剪
36
+ "gblocks": "seq", # GUI 逆向:Gblocks 修剪
37
+ "bestid": "blast", # GUI 逆向:最优 ID 转换
38
+ "fasplit": "seq", "famerge": "seq", # GUI 逆向:FASTA 拆/合
39
+ "clearchar": "table", # GUI 逆向:非法字符清理
40
+ "getseqdb": "blast", # GUI 逆向:BLAST 库提序列
41
+ "gb2fa": "seq", # GUI 逆向:GenBank→FASTA
42
+ "findhomolog": "blast", # GUI 逆向:最优同源
43
+ "taxparse": "table", # GUI 逆向:物种分类解析
44
+ "srr2ena": "table", # GUI 逆向:SRR→ENA 链接
45
+ "sraxml2tab": "table", # GUI 逆向:SRA XML→表
46
+ "sranum2info": "table", # GUI 逆向:SRR 信息表
47
+ "blat": "blast", # GUI 逆向:BLAT 比对
48
+ "seqrecommend": "engine", # GUI 逆向:测序量推荐
49
+ "seqfetch": "seq", # GUI 逆向:NCBI 序列下载
50
+ "pubmed": "table", # GUI 逆向:PubMed 检索
51
+ # 序列/结构/域
52
+ "genestructure": "seq", "motif": "seq", "msa": "seq", "seqlentrack": "seq",
53
+ "amazingmeta": "seq", "cddmotif": "seq", "pfammotif": "seq", "memerun": "seq",
54
+ "mastrun": "seq", "mastExtract": "seq", "mast2tab": "seq", "pep2codon": "seq",
55
+ "simplehmmscan": "seq", "gel": "seq", "gfa": "seq", "gfa2fa": "seq",
56
+ # 表达/统计
57
+ "pca": "expr", "hclust": "expr", "qpcr": "expr", "qpcrExp": "expr",
58
+ "groupedbar": "expr", "dehist": "expr", "barplot": "expr", "barplotter": "expr",
59
+ "layoutheatmap": "expr", "cubeheatmap": "expr", "violin": "expr",
60
+ "colorscheme": "expr", "distance": "expr", "mountain": "expr",
61
+ "tauIndex": "expr", "exprCorr": "expr", "groupCol": "expr",
62
+ "kaks": "tree", # GUI 逆向:成对 Ka/Ks
63
+ "subtree": "tree", # GUI 逆向:子树提取
64
+ "iqtree": "tree", # GUI 逆向:IQ-TREE 建树
65
+ # 树/进化
66
+ "phylotree": "tree", "unrooted": "tree", "treeRooting": "tree",
67
+ "onesteptree": "tree", "degramdom": "tree", "findpath": "tree",
68
+ "nwAlign": "tree",
69
+ # 共线性/基因组
70
+ "circos": "syn", "supercircos": "syn", "circlegene": "syn", "dotplot": "syn",
71
+ "microsyn": "syn", "msy": "syn", "multisyn": "syn", "dualsyn": "syn",
72
+ "pafviz": "syn", "pafcomp": "syn", "pafref": "syn",
73
+ "mcscanx": "syn", "collinearRegion": "syn",
74
+ "findblockdual": "syn", "findblockmultiple": "syn", "visualizeblock": "syn",
75
+ "conflictpaf": "syn", "partitionconflict": "syn",
76
+ "microgenome": "syn",
77
+ # 集合/ChIP
78
+ "venn2": "sets", "venn3": "sets", "venn4": "sets",
79
+ "venn5": "sets", "venn6": "sets", "upset": "sets",
80
+ "peaktss": "chipseq", "peakdist": "chipseq", "peakanno": "chipseq",
81
+ "pileup": "chipseq",
82
+ # 组装/注释
83
+ "ctgGroup": "asm", "homoPhase": "asm", "sepChr": "asm",
84
+ "bamMerge": "asm", "bamindex": "asm", "bamsort": "asm", "bamstate": "asm",
85
+ "hicEnzyme": "asm", "virusRecomb": "asm", "preparespecies": "asm",
86
+ "gxfRename": "gxf", "gxfStat": "gxf", "gxfAppend": "gxf", "gxfGenepos": "gxf",
87
+ "gxfSplit": "gxf", "gxfIdAppender": "gxf",
88
+ "gxfRegion": "gxf", "gxfFix": "gxf", "gxfOverlap": "gxf", "gxfRepIDs": "gxf",
89
+ "gxfRepGXF": "gxf", "gxfMatch": "gxf", "gxfRecall": "gxf",
90
+ "regionAnno": "gxf", "annocompare": "gxf", "genedensity": "gxf",
91
+ "genelocation": "gxf", "genelocgff": "gxf", "gxfSort": "gxf",
92
+ # miRNA
93
+ "mirnatarget": "mirna", "mirnaTarget2": "mirna", "mirnaIdentify": "mirna",
94
+ # GO/表格
95
+ "levelGo": "table", "goParse": "table", "batchReplace": "table",
96
+ "goAnno": "table", # GUI 逆向:GO 注释管道
97
+ "goEnrich": "table", "keggEnrich": "table",
98
+ "tableCollapse": "table", "tableColSelect": "table", "tableAppend": "table",
99
+ "tableMelt": "table", "tableColSel": "table", "tableCast": "table",
100
+ "tableUniq": "table", "tableTranspose": "table", "tableSplit": "table",
101
+ "tableMerge": "table",
102
+ # BLAST/比对
103
+ "recipBlast": "blast", "filterCScore": "blast", "quickFamily": "blast",
104
+ "twoSeqBlast": "blast",
105
+ # FASTQ
106
+ "fqTrim": "fastq", "fqfaConv": "fastq", "fastaSubseq": "fastq",
107
+ "fastaExtract": "fastq",
108
+ # HMM
109
+ "hmmExtract": "hmm", "hmmsearch": "hmm",
110
+ "gxfAttr": "gxf", "gdensity": "gxf",
111
+ "notung": "tree",
112
+ "newickRename": "tree",
113
+ "hmmerSearch": "hmm",
114
+ "memeViz": "seq",
115
+ "gsea": "table", "gbar": "expr", "golevel": "table", "sricher": "table",
116
+ "tfbsShift": "seq",
117
+ "kallisto": "expr",
118
+ "mcscanxd": "syn",
119
+ "qdot": "syn",
120
+ "quickAnno": "blast",
121
+ "smart": "seq",
122
+ "fimo": "seq", "meme": "seq", "mast": "seq", "meme2tab": "seq", "makemotif": "seq", "mpattern": "seq",
123
+ # GWAS
124
+ "mimicVqsr": "gwas", "vcfAddID": "gwas",
125
+ # 通用
126
+ "generic": "engine", "efpHeat": "expr", "multiEfp": "expr",
127
+ "plotrna": "seq", "rnaplot": "seq",
128
+ }
129
+
130
+ # 分组定义
131
+ GROUPS = {
132
+ "seq": "序列/结构/域",
133
+ "expr": "表达/统计",
134
+ "tree": "树/进化",
135
+ "syn": "共线性/基因组",
136
+ "sets": "集合/韦恩",
137
+ "chipseq": "ChIP-seq",
138
+ "asm": "组装/注释",
139
+ "gxf": "GXF/表格",
140
+ "mirna": "miRNA",
141
+ "table": "GO/表格",
142
+ "blast": "BLAST/比对",
143
+ "fastq": "FASTQ/FASTA",
144
+ "hmm": "HMM",
145
+ "gwas": "GWAS",
146
+ "engine": "通用",
147
+ }
148
+
149
+ # 全局分组表(build_and_load → _ensure_groups(cli) 填充;cli.py/list 等 import 使用)
150
+ _groups: dict[str, click.Group] = {}
151
+ def _load_auto_commands():
152
+ """从 auto_commands.py 加载所有命令到对应分组(弥补 tbplot.sh 遗漏的命令)"""
153
+ skip = {"seqlogo", "msa", "motif", "genestructure", # seq manual
154
+ "volcano", "heatmap2", "pca", "hclust", "dehist", # expr manual
155
+ "tree", "unrooted", "treeRooting", "onesteptree", # tree manual
156
+ "version", "doctor", "examples", "list", "presets", # top commands
157
+ "seq", "expr", "tool", # group names
158
+ "chipseq", "sets", "syn", "asm", "gxf", "mirna", "table",
159
+ "blast", "fastq", "hmm", "gwas", "engine"}
160
+ for name in sorted(dir(_ac)):
161
+ if name.startswith('_') and name.endswith('_impl') and not name.startswith('__'):
162
+ cmd = name[1:-5]
163
+ if cmd in skip:
164
+ continue
165
+ cat = CATEGORY_MAP.get(cmd, "engine")
166
+ target = _groups.get(cat)
167
+ if target and cmd not in target.commands:
168
+ _make_passthrough(cmd, target)
169
+
170
+ def _load_dynamic_commands():
171
+ """从 tbplot.sh 动态生成 click 命令,按分类注册到 group"""
172
+ tbplot_sh = os.path.join(ROOT, "bin", "tbplot.sh")
173
+ if not os.path.isfile(tbplot_sh):
174
+ return
175
+ with open(tbplot_sh) as f:
176
+ content = f.read()
177
+ cmds = set(re.findall(r'^ ([a-zA-Z][a-zA-Z0-9]+)\)$', content, re.MULTILINE))
178
+ # 已迁移命令的原始名(不动态转发)——用 tbplot.sh 里的原始命令名
179
+ # 只排除真正有 @xxx.command 手动注册的命令 + group 名
180
+ registered = {"seqlogo", "msa", "motif", "genestructure", # seq
181
+ "volcano", "heatmap2", "pca", "hclust", "dehist", # expr
182
+ "tree", "unrooted", "treeRooting", "onesteptree", # tree
183
+ "version", "doctor", "examples", "seq", "expr", "tool",
184
+ "chipseq", "sets", "syn", "asm", "gxf", "mirna", "table",
185
+ "blast", "fastq", "hmm", "gwas", "engine"}
186
+ for cmd_name in sorted(cmds - registered):
187
+ cat = CATEGORY_MAP.get(cmd_name, "engine")
188
+ _make_passthrough(cmd_name, _groups[cat])
189
+
190
+ # 给所有分组加未知子命令纠错(ToolGroup/rpc 已有自己的 resolve_command)
191
+ def _smart_resolve(self, ctx, args):
192
+ try:
193
+ return click.Group.resolve_command(self, ctx, args)
194
+ except click.UsageError:
195
+ if not args:
196
+ raise
197
+ name = args[0]
198
+ cmds = list(self.commands.keys())
199
+ # 前缀优先(venn2 案例:get_close_matches n=3 按相似度排序会挤掉正确建议)
200
+ prefix_hits = [c for c in cmds if c.startswith(name)]
201
+ close = prefix_hits[:5] or difflib.get_close_matches(name, cmds, n=3, cutoff=0.6)
202
+ if close:
203
+ click.echo(_("❌ '{n}' 不是 '{g}' 分组内的命令", "❌ '{n}' is not a command in group '{g}'").format(n=name, g=self.name), err=True)
204
+ click.echo(f" 你是不是想用: {' / '.join(close)}?", err=True)
205
+ else:
206
+ click.echo(_("❌ '{n}' 不是 '{g}' 分组内的命令", "❌ '{n}' is not a command in group '{g}'").format(n=name, g=self.name), err=True)
207
+ click.echo(f" 查看: tbtools {self.name} --help", err=True)
208
+ ctx.exit(2)
209
+ for gname, g in _groups.items():
210
+ if gname in ("tool", "rpc"):
211
+ continue
212
+ if not hasattr(g, "resolve_command") or g.__class__.__name__ == "Group":
213
+ g.resolve_command = _smart_resolve.__get__(g, type(g))
214
+
215
+ _META_JSON = None
216
+ def _load_meta_json():
217
+ """懒加载 command_metadata.json(N33: 143 命令完整 help,含 150 可选位)"""
218
+ global _META_JSON
219
+ if _META_JSON is None:
220
+ try:
221
+ import json as _json
222
+ _p = os.path.join(ROOT, "tbtools_cli", "command_metadata.json")
223
+ _META_JSON = _json.load(open(_p, encoding="utf-8")) if os.path.isfile(_p) else {}
224
+ except Exception:
225
+ _META_JSON = {}
226
+ return _META_JSON
227
+
228
+ def _parse_auto_metadata(name):
229
+ """从 auto_commands.py 解析命令元数据(docstring + 坑位)"""
230
+ impl = _ac._IMPL_REGISTRY.get(name) or getattr(_ac, f'_{name}_impl', None) # 显式注册表优先(第四轮评审)
231
+ doc = (impl.__doc__ or "").strip() if impl else ""
232
+ # N33: 优先 command_metadata.json 完整 help(含可选位;docstring 常被表驱动截断)
233
+ _meta = _load_meta_json().get(name)
234
+ if _meta and _meta.get("help"):
235
+ usage = _meta["help"]
236
+ elif ':' in doc:
237
+ usage = doc.split(':', 1)[1].strip()
238
+ else:
239
+ usage = f"{name} [参数...]"
240
+ pitfall = get_pitfall_hint(name)
241
+ return {'impl': impl, 'usage': usage, 'pitfall': pitfall}
242
+
243
+ def _make_passthrough(name, group=None):
244
+ """生成元数据驱动的 click 命令(help + 校验 + 预设 + 直调 Java)"""
245
+ meta = _parse_auto_metadata(name)
246
+ impl = meta['impl']
247
+ usage = meta['usage']
248
+ pitfall = meta['pitfall']
249
+
250
+ help_text = usage
251
+ if pitfall:
252
+ help_text += f"\n\n⚠️ {pitfall}"
253
+
254
+ _target = group # 调用方总传 group
255
+
256
+ def _cmd_impl(ctx, verbose, quiet, fmt, preset, height, width, threads):
257
+ args = list(ctx.args)
258
+
259
+ # 输入校验:第一个非选项参数通常是输入文件(mcscanxd/kallisto 首参为工作目录/自定义路径,跳过校验)
260
+ if args and not args[0].startswith('-') and name not in ("mcscanxd", "kallisto", "famerge", "getseqdb", "seqrecommend", "pubmed", "tableMerge", "generic", "preparespecies", "marker", "markertools", "venn5", "venn6"):
261
+ ok, msg = validate_file(args[0], f"{name} 输入文件")
262
+ if not ok:
263
+ print(msg, file=sys.stderr)
264
+ sys.exit(2)
265
+ # C2: 早期格式不匹配警告(不阻断,仅提示)
266
+ warn = check_input_format(name, args[0])
267
+ if warn:
268
+ print(f"⚠️ 格式提醒: {warn}", file=sys.stderr)
269
+ print(" (继续执行;如确认无误可忽略)", file=sys.stderr)
270
+ if preset:
271
+ p = apply_preset(preset, width=width, height=height)
272
+ if not p:
273
+ print(f"❌ 未知预设: {preset}", file=sys.stderr)
274
+ print(f" 可用: {', '.join(PRESETS.keys())}", file=sys.stderr)
275
+ sys.exit(1)
276
+ if 'width' in p and not width:
277
+ width = p['width']
278
+ if 'height' in p and not height:
279
+ height = p['height']
280
+
281
+ # 格式覆盖:替换输出文件扩展名
282
+ if fmt:
283
+ for i in range(len(args) - 1, -1, -1):
284
+ if args[i].endswith(('.svg', '.png', '.pdf')):
285
+ base = os.path.splitext(args[i])[0]
286
+ args[i] = f"{base}.{fmt}"
287
+ break
288
+
289
+ # 追加 width/height 到参数末尾(大多数命令接受 [w] [h] 位置参数)
290
+ if width:
291
+ args.append(str(width))
292
+ if height:
293
+ args.append(str(height))
294
+
295
+ # 调用 impl 或回退到 bash tbplot.sh
296
+ if impl:
297
+ ec = impl(args, verbose=verbose, quiet=quiet)
298
+ else:
299
+ # 兜底走 bash tbplot.sh(Windows 需 Git Bash;无 bash 时报清晰错误)
300
+ bash_bin = shutil.which("bash")
301
+ if bash_bin:
302
+ bash_args = [bash_bin, os.path.join(ROOT, "bin", "tbplot.sh"), name] + list(ctx.args)
303
+ ec = subprocess.run(bash_args).returncode
304
+ else:
305
+ print(f"❌ {name} 需要 bash 兜底(tbplot.sh),但未找到 bash(Windows 请装 Git Bash)", file=sys.stderr)
306
+ ec = 1
307
+ sys.exit(ec)
308
+
309
+ # 先设置 docstring,再装饰
310
+ _cmd_impl.__doc__ = help_text
311
+ _cmd_impl = click.pass_context(_cmd_impl)
312
+ for opt_args, opt_kwargs in [
313
+ (("--verbose", "-V"), {"is_flag": True, "default": False, "help": "显示完整堆栈"}),
314
+ (("--quiet", "-q"), {"is_flag": True, "default": False, "help": "静默模式"}),
315
+ (("--format", "-f", "fmt"), {"default": None, "help": "输出格式: svg|png|pdf"}),
316
+ (("--preset", "-p"), {"default": None, "help": "出版预设: nature|cell|plant_journal|wide|poster"}),
317
+ (("--height", "-H"), {"type": int, "default": None, "help": "画布高度"}),
318
+ (("--width", "-W"), {"type": int, "default": None, "help": "画布宽度"}),
319
+ (("--threads", "-t"), {"type": int, "default": None, "help": "线程数"}),
320
+ ]:
321
+ _cmd_impl = click.option(*opt_args, **opt_kwargs)(_cmd_impl) # type: ignore[arg-type]
322
+
323
+ # 提取 click.option 装饰器注册的参数(__click_params__)
324
+ params = getattr(_cmd_impl, '__click_params__', [])
325
+ params = params[::-1] # click 处理顺序是反的
326
+
327
+ cmd = click.Command(name=name, callback=_cmd_impl, params=params,
328
+ context_settings={"ignore_unknown_options": True, "allow_extra_args": True},
329
+ help=help_text)
330
+ _target.add_command(cmd)
331
+
332
+
333
+ def build_and_load(cli):
334
+ """组装: 建 groups → 动态注册 auto_commands + tbplot.sh 遗留 → 返回 groups dict"""
335
+ _ensure_groups(cli)
336
+ _load_auto_commands()
337
+ _load_dynamic_commands()
338
+ return _groups
339
+
340
+
341
+ def _ensure_groups(cli):
342
+ """按 GROUPS 定义补齐分组(复用装饰器已创建的;原地填充保持 _groups 引用不变)"""
343
+ global _groups
344
+ _groups.clear()
345
+ for key, desc in GROUPS.items():
346
+ if key in cli.commands:
347
+ _groups[key] = cli.commands[key]
348
+ else:
349
+ g = click.Group(name=key, help=desc)
350
+ cli.add_command(g, name=key)
351
+ _groups[key] = g
352
+ # 子命令纠错绑定在 _load_dynamic_commands 尾部完成(_smart_resolve)
tbtools_cli/cli_rpc.py ADDED
@@ -0,0 +1,303 @@
1
+ """cli_rpc.py — RPC 服务器管理(架构重构批次 B:从 cli.py 拆分)
2
+
3
+ rpc 分组 + N34/N35 自愈基础设施(pid 文件/健康探针/自动拉起)+ N38 错误兜底。
4
+ 由 cli.py 以 `cli_rpc.build_rpc_group()` 注册。
5
+ """
6
+ import json
7
+ import os
8
+ import subprocess
9
+ import sys
10
+ import time as _t
11
+ import urllib.request
12
+
13
+ import click
14
+
15
+ from tbtools_cli.core import JAR
16
+ from tbtools_cli.core import _ as _tr
17
+
18
+ @click.group('rpc')
19
+ def rpc_group():
20
+ """RPC 服务器管理(188 方法)"""
21
+
22
+ # ---------- RPC 自愈基础设施(N34/N35 批次 2)----------
23
+ # 症状:引擎进程内存/空闲期自发死亡;代理层把死亡伪装成 502;CLI 无感知、无自愈。
24
+ # 方案:pid 文件 + 健康探针(system.listMethods)+ call/methods 前 ensure 自动拉起。
25
+ # 注意:urllib 必须绕过代理(N41:HTTP_PROXY 注入会让 127.0.0.1 请求走代理转发失败)。
26
+
27
+ def _rpc_state_dir():
28
+ d = os.environ.get(
29
+ "TBTOOLS_RPC_DIR",
30
+ os.path.join(os.path.expanduser("~"), ".config", "tbtools-cli"),
31
+ )
32
+ os.makedirs(d, exist_ok=True)
33
+ return d
34
+
35
+ def _rpc_pid_file(port):
36
+ return os.path.join(_rpc_state_dir(), f"rpc-{port}.pid")
37
+
38
+ def _rpc_log_file(port):
39
+ return os.path.join(_rpc_state_dir(), f"rpc-{port}.log")
40
+
41
+ def _rpc_ping(port, timeout=5):
42
+ """健康探针:POST system.listMethods。绕过代理。返回 True/False"""
43
+ try:
44
+ opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
45
+ req = urllib.request.Request(
46
+ f"http://127.0.0.1:{port}/rpc",
47
+ data=json.dumps({"jsonrpc": "2.0", "method": "system.listMethods",
48
+ "params": {}, "id": 1}).encode(),
49
+ headers={"Content-Type": "application/json"})
50
+ resp = opener.open(req, timeout=timeout)
51
+ result = json.loads(resp.read())
52
+ return "result" in result
53
+ except Exception:
54
+ return False
55
+
56
+ def _rpc_read_pid(port):
57
+ """读 pid 文件;进程不存在或与 RPC 无关则清理并返回 None"""
58
+ pid_file = _rpc_pid_file(port)
59
+ try:
60
+ with open(pid_file) as f:
61
+ pid = int(f.read().strip())
62
+ except Exception:
63
+ return None
64
+ try:
65
+ os.kill(pid, 0)
66
+ except (ProcessLookupError, PermissionError, OverflowError, ValueError):
67
+ _rpc_remove_pid(port)
68
+ return None
69
+ # Linux 下确认 cmdline 是 RPC server(防 pid 复用误杀)
70
+ cmdline = f"/proc/{pid}/cmdline"
71
+ if os.path.isfile(cmdline):
72
+ try:
73
+ with open(cmdline, "rb") as f:
74
+ if b"biocjava.rpc.RpcServer" not in f.read():
75
+ _rpc_remove_pid(port)
76
+ return None
77
+ except Exception:
78
+ pass
79
+ return pid
80
+
81
+ def _rpc_write_pid(port, pid):
82
+ with open(_rpc_pid_file(port), "w") as f:
83
+ f.write(str(pid))
84
+
85
+ def _rpc_remove_pid(port):
86
+ try:
87
+ os.remove(_rpc_pid_file(port))
88
+ except OSError:
89
+ pass
90
+
91
+ def _rpc_launch(port, mem):
92
+ """拉起 RPC 服务器(detached,OOM 崩溃转储,日志落盘)。返回 Popen"""
93
+ if not JAR or not os.path.isfile(JAR):
94
+ raise FileNotFoundError(
95
+ "TBtools jar 未找到。请设置 TBTOOLS_JAR 环境变量或放入常见位置")
96
+ log_path = _rpc_log_file(port)
97
+ log_fh = open(log_path, "ab", buffering=0)
98
+ args = [
99
+ "java", f"-Xmx{mem}",
100
+ # N35: OOM 时宁可崩溃(可自愈拉起)也不要僵尸悬挂;同时留堆转储供排查
101
+ "-XX:+CrashOnOutOfMemoryError",
102
+ "-XX:+HeapDumpOnOutOfMemoryError",
103
+ f"-XX:HeapDumpPath={_rpc_state_dir()}",
104
+ "-cp", JAR, "biocjava.rpc.RpcServer",
105
+ ]
106
+ proc = subprocess.Popen(
107
+ args, stdout=log_fh, stderr=log_fh, start_new_session=True)
108
+ _rpc_write_pid(port, proc.pid)
109
+ return proc
110
+
111
+ def _ensure_rpc(port, mem="4g", wait_s=30, quiet=False):
112
+ """ensure 逻辑(同交付包 run_p*.py 的 ensure_srv):
113
+ 健康 → True;不健康/死亡 → 清 stale pid → 拉起 → 轮询健康。"""
114
+ if _rpc_ping(port):
115
+ return True
116
+ old_pid = _rpc_read_pid(port)
117
+ if old_pid:
118
+ if not quiet:
119
+ click.echo(f"⚠️ 检测到旧 RPC 进程 (PID {old_pid}) 无响应,终止后拉起新实例...", err=True)
120
+ try:
121
+ os.kill(old_pid, 15)
122
+ except OSError:
123
+ pass
124
+ _rpc_remove_pid(port)
125
+ _t.sleep(1)
126
+ elif not quiet:
127
+ click.echo(_tr("⚠️ RPC 服务器不可达(端口 {p}),自动拉起...", "⚠️ RPC server unreachable (port {p}) — auto-restarting...").format(p=port), err=True)
128
+ try:
129
+ _rpc_launch(port, mem)
130
+ except FileNotFoundError as e:
131
+ click.echo(f"❌ {e}", err=True)
132
+ return False
133
+ for _ in range(wait_s):
134
+ _t.sleep(1)
135
+ if _rpc_ping(port):
136
+ return True
137
+ click.echo(_tr("❌ RPC 服务器 {s}s 内未就绪,日志: {log}", "❌ RPC server not ready within {s}s, log: {log}").format(s=wait_s, log=_rpc_log_file(port)), err=True)
138
+ return False
139
+
140
+ @rpc_group.command('start')
141
+ @click.option('--port', '-p', type=int, default=8765, help='RPC 端口')
142
+ @click.option('--mem', '-m', default='4g', help='Java 堆内存')
143
+ @click.option('--force', '-f', is_flag=True, help='强制重启(杀掉已有实例)')
144
+ def rpc_start(port, mem, force):
145
+ """启动 RPC 服务器(pid 文件 + 健康检查;已在跑则幂等返回)"""
146
+ if _rpc_ping(port):
147
+ if not force:
148
+ pid = _rpc_read_pid(port)
149
+ click.echo(_tr("✅ RPC 服务器已在运行(端口 {p}, PID {pid})", "✅ RPC server already running (port {p}, PID {pid})").format(p=port, pid=pid or '?'))
150
+ return
151
+ old = _rpc_read_pid(port)
152
+ click.echo(f"🔄 --force:终止旧实例 (PID {old or '?'})...")
153
+ if old:
154
+ try:
155
+ os.kill(old, 15)
156
+ except OSError:
157
+ pass
158
+ _rpc_remove_pid(port)
159
+ _t.sleep(1)
160
+ else:
161
+ # 清 stale(引擎自发死亡残留)
162
+ old = _rpc_read_pid(port)
163
+ if old:
164
+ click.echo(f"⚠️ 旧 RPC 进程 (PID {old}) 无响应,终止...")
165
+ try:
166
+ os.kill(old, 15)
167
+ except OSError:
168
+ pass
169
+ _rpc_remove_pid(port)
170
+ _t.sleep(1)
171
+ click.echo(_tr("🚀 启动 RPC 服务器(端口 {p},堆 {m})...", "🚀 Starting RPC server (port {p}, heap {m})...").format(p=port, m=mem))
172
+ try:
173
+ proc = _rpc_launch(port, mem)
174
+ except FileNotFoundError as e:
175
+ click.echo(f"❌ {e}", err=True)
176
+ sys.exit(1)
177
+ for _ in range(30):
178
+ _t.sleep(1)
179
+ if _rpc_ping(port):
180
+ click.echo(f"✅ RPC 服务器就绪 (PID {proc.pid})")
181
+ click.echo(f" pid 文件: {_rpc_pid_file(port)}")
182
+ click.echo(f" 日志: {_rpc_log_file(port)}")
183
+ click.echo(f" 测试: curl -X POST http://127.0.0.1:{port}/rpc -H 'Content-Type: application/json' -d '{{\"method\":\"system.listMethods\",\"params\":{{}},\"id\":1}}'")
184
+ return
185
+ click.echo(f"❌ 启动超时(30s),日志: {_rpc_log_file(port)}", err=True)
186
+ sys.exit(1)
187
+
188
+
189
+ @rpc_group.command('logs')
190
+ @click.option('--tail', 'n', type=int, default=50, help='显示末尾 N 行(0=全部)')
191
+ @click.option('--port', default=8765, help='RPC 端口')
192
+ def logs(n, port):
193
+ """查看 RPC 服务器日志(尾部 N 行;0=全部)"""
194
+ import os as _os
195
+ log = _rpc_log_file(port)
196
+ if not _os.path.isfile(log):
197
+ click.echo(f"❌ 无日志文件(服务器未启动过?): {log}", err=True)
198
+ sys.exit(1)
199
+ if n <= 0:
200
+ click.echo(open(log, encoding="utf-8", errors="replace").read())
201
+ else:
202
+ lines = open(log, encoding="utf-8", errors="replace").read().splitlines()
203
+ click.echo("\n".join(lines[-n:]))
204
+
205
+ @rpc_group.command('stop')
206
+ @click.option('--port', '-p', type=int, default=8765, help='RPC 端口')
207
+ def rpc_stop(port):
208
+ """停止 RPC 服务器"""
209
+ pid = _rpc_read_pid(port)
210
+ if not pid:
211
+ click.echo(f"RPC 服务器未在运行(端口 {port})")
212
+ _rpc_remove_pid(port)
213
+ return
214
+ try:
215
+ os.kill(pid, 15)
216
+ click.echo(_tr("✅ 已发送 SIGTERM (PID {p})", "✅ SIGTERM sent (PID {p})").format(p=pid))
217
+ except OSError as e:
218
+ click.echo(f"❌ 终止失败: {e}", err=True)
219
+ sys.exit(1)
220
+ _rpc_remove_pid(port)
221
+
222
+ @rpc_group.command('status')
223
+ @click.option('--port', '-p', type=int, default=8765, help='RPC 端口')
224
+ def rpc_status(port):
225
+ """查看 RPC 服务器状态"""
226
+ pid = _rpc_read_pid(port)
227
+ healthy = _rpc_ping(port)
228
+ if healthy:
229
+ click.echo(f"✅ 运行中(端口 {port}, PID {pid or '?'})")
230
+ elif pid:
231
+ click.echo(f"⚠️ 进程存在 (PID {pid}) 但健康检查失败(可能正在启动或假死)")
232
+ sys.exit(2)
233
+ else:
234
+ click.echo(f"❌ 未运行(端口 {port})。启动: tbtools rpc start")
235
+ sys.exit(1)
236
+
237
+ @rpc_group.command('methods')
238
+ @click.option('--port', '-p', type=int, default=8765, help='RPC 端口')
239
+ @click.option('--mem', '-m', default='4g', help='--autostart 时的 Java 堆内存')
240
+ @click.option('--autostart', is_flag=True, help='服务不可达时自动拉起(默认不启动进程——发现操作应 side-effect free)')
241
+ def rpc_methods(port, mem, autostart):
242
+ """列出全部 188 RPC 方法(默认静态发现, 不启动服务器; --autostart 时自动拉起)"""
243
+ if autostart and not _ensure_rpc(port, mem):
244
+ click.echo(" 手动启动: tbtools rpc start", err=True)
245
+ sys.exit(1)
246
+ try:
247
+ opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
248
+ req = urllib.request.Request(
249
+ f"http://127.0.0.1:{port}/rpc",
250
+ data=json.dumps({"jsonrpc": "2.0", "method": "system.listMethods", "params": {}, "id": 1}).encode(),
251
+ headers={"Content-Type": "application/json"})
252
+ resp = opener.open(req, timeout=15)
253
+ result = json.loads(resp.read())
254
+ res = result.get('result', [])
255
+ methods = res.get('methods', res) if isinstance(res, dict) else res
256
+ click.echo(f"RPC 方法({len(methods)} 个):")
257
+ for m in methods:
258
+ click.echo(f" {m}")
259
+ except Exception as e:
260
+ click.echo(f"❌ RPC 调用失败: {e}", err=True)
261
+ click.echo(" 引擎可能已死,尝试: tbtools rpc start --force", err=True)
262
+ sys.exit(1)
263
+
264
+ @rpc_group.command('call')
265
+ @click.argument('method')
266
+ @click.argument('params', required=False)
267
+ @click.option('--port', '-p', type=int, default=8765, help='RPC 端口')
268
+ @click.option('--mem', '-m', default='4g', help='自动拉起时的 Java 堆内存')
269
+ @click.option('--timeout', '-t', type=int, default=300, help='调用超时(秒)')
270
+ @click.option('--no-autostart', is_flag=True, help='禁用在不可达时自动拉起')
271
+ def rpc_call(method, params, port, mem, timeout, no_autostart):
272
+ """调用 RPC 方法(服务不可达时自动拉起)"""
273
+ params_obj = json.loads(params) if params else {}
274
+ if not no_autostart and not _ensure_rpc(port, mem):
275
+ click.echo(" 手动启动: tbtools rpc start", err=True)
276
+ sys.exit(1)
277
+ try:
278
+ opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
279
+ req = urllib.request.Request(
280
+ f"http://127.0.0.1:{port}/rpc",
281
+ data=json.dumps({"jsonrpc": "2.0", "method": method, "params": params_obj, "id": 1}).encode(),
282
+ headers={"Content-Type": "application/json"})
283
+ resp = opener.open(req, timeout=timeout)
284
+ result = json.loads(resp.read())
285
+ if result.get('error'):
286
+ # N38: 引擎错误 message 空/占位符时补友好提示(GffReconstructorBatch 等家族)
287
+ err = result['error']
288
+ msg = str(err.get('data', {}).get('message') if isinstance(err.get('data'), dict) else err.get('data') or err.get('message') or '')
289
+ if not msg.strip() or msg.strip() == '===== See Following Info =====' or msg.startswith('Something Error'):
290
+ msg = '引擎内部错误且未提供消息(N38 家族,GffReconstructorBatch/BestIdConverter/ReciprocalBlast 已知)'
291
+ click.echo(f"❌ RPC 错误 [{err.get('code')}]: {msg}(原始: {json.dumps(err, ensure_ascii=False)[:200]})", err=True)
292
+ else:
293
+ click.echo(f"❌ RPC 错误 [{err.get('code')}]: {msg}", err=True)
294
+ sys.exit(1)
295
+ click.echo(json.dumps(result.get('result', ''), indent=2, ensure_ascii=False))
296
+ except Exception as e:
297
+ click.echo(f"❌ RPC 调用失败: {e}", err=True)
298
+ sys.exit(1)
299
+
300
+
301
+ def build_rpc_group():
302
+ """构建 rpc 分组(由主 CLI 注册)"""
303
+ return rpc_group