@dsh-bio/dsh-bio-gem 0.1.1
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.
- package/LICENSE +21 -0
- package/README.md +194 -0
- package/cordis.patch.yml +7 -0
- package/docs/ARCHITECTURE.md +116 -0
- package/docs/DECISIONS-2026-08-29.md +37 -0
- package/docs/DECISIONS-/351/230/266/346/256/265A.md +67 -0
- package/docs/DECISIONS-/351/230/266/346/256/265E.md +56 -0
- package/index.js +5 -0
- package/package.json +50 -0
- package/python/annotate.py +208 -0
- package/python/benchmark.py +591 -0
- package/python/biomass_tools.py +329 -0
- package/python/budget.py +53 -0
- package/python/build.py +343 -0
- package/python/build_whitelist.py +127 -0
- package/python/double_knockout.py +201 -0
- package/python/enrichment.py +182 -0
- package/python/essential_scan.py +195 -0
- package/python/fluxscan.py +302 -0
- package/python/gapfill.py +176 -0
- package/python/gapfind.py +397 -0
- package/python/gapseq_wsl.py +251 -0
- package/python/gem_ops.py +520 -0
- package/python/l3_fix.py +641 -0
- package/python/ledger.py +581 -0
- package/python/model_card.py +248 -0
- package/python/phenotype_fix.py +115 -0
- package/python/roundtrip_check.py +45 -0
- package/python/secretion.py +179 -0
- package/python/sensitivity.py +484 -0
- package/python/silentio.py +28 -0
- package/python/targets.py +151 -0
- package/python/validate.py +393 -0
- package/skills/gem-expert.md +88 -0
- package/src/index.js +19 -0
- package/src/jobs.js +152 -0
- package/src/python.js +64 -0
- package/src/skills.js +29 -0
- package/src/tools.js +545 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# gapseq_wsl.py — dsh-bio-gem M2:gapseq 引擎的 WSL 桥(Windows → WSL2)
|
|
2
|
+
# 实测背景(农杆菌 C58 项目 2026-08):gapseq 2.1.0 部署于 WSL2 Ubuntu-22.04
|
|
3
|
+
# conda env "gapseq"(TUNA 渠道),序列库 F:\Datasets\gapseq\db\Bacteria(v1.5)。
|
|
4
|
+
# 关键实测(2026-08-29):
|
|
5
|
+
# - 新版 wsl.exe(Win11)输出 UTF-8;旧版 UTF-16LE —— decode 双兼容(UTF-8 strict 优先)
|
|
6
|
+
# - wsl.exe 命令行传含空格路径 + 引号必坏 -> 一切路径进脚本文件,脚本用 base64 传输
|
|
7
|
+
# - doall 是 bash 脚本:输入为位置参数(gapseq doall FILE.fna),-m/-f 组合实测触发 usage
|
|
8
|
+
# - wsl.exe 会话 teardown 会杀后台作业 -> nohup + setsid + sleep 3 保活
|
|
9
|
+
# 架构(用户 2026-08-29 指示):原子步骤思想 —— launch/status/fetch 拆为独立函数,
|
|
10
|
+
# 由 dsh agent 编排(gem_gapseq 工具:setup/launch/status/fetch),利用 agent 自愈推进长任务。
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
import time
|
|
16
|
+
|
|
17
|
+
# Windows 下 stdout/stderr 默认 GBK——统一 UTF-8(独立脚本也要)
|
|
18
|
+
for _s in ("stdout", "stderr"):
|
|
19
|
+
try:
|
|
20
|
+
getattr(sys, _s).reconfigure(encoding="utf-8")
|
|
21
|
+
except Exception:
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
WSL_DISTRO = os.environ.get("GEM_GAPSEQ_DISTRO", "Ubuntu-22.04")
|
|
25
|
+
CONDA_SH = "/opt/miniforge3/etc/profile.d/conda.sh"
|
|
26
|
+
GAPSEQ_ENV = "gapseq"
|
|
27
|
+
WSL_WORK = "/opt/gem-gapseq-work"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def wsl_run(bash_cmd, timeout=300):
|
|
31
|
+
"""在 WSL root 下执行 bash 命令,返回 (rc, stdout_utf8, stderr_utf8)。"""
|
|
32
|
+
try:
|
|
33
|
+
r = subprocess.run(
|
|
34
|
+
["wsl.exe", "-d", WSL_DISTRO, "-u", "root", "--", "bash", "-lc", bash_cmd],
|
|
35
|
+
capture_output=True, timeout=timeout)
|
|
36
|
+
except FileNotFoundError:
|
|
37
|
+
return (-1, "", "wsl.exe not found")
|
|
38
|
+
except subprocess.TimeoutExpired:
|
|
39
|
+
return (-2, "", f"timeout {timeout}s")
|
|
40
|
+
return (r.returncode, _decode(r.stdout), _decode(r.stderr))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _decode(b):
|
|
44
|
+
"""wsl.exe 输出解码:新版(Win11)UTF-8;旧版 UTF-16LE(含 BOM/00 填充)。"""
|
|
45
|
+
if not b:
|
|
46
|
+
return ""
|
|
47
|
+
try:
|
|
48
|
+
return b.decode("utf-8", errors="strict").strip()
|
|
49
|
+
except UnicodeDecodeError:
|
|
50
|
+
pass
|
|
51
|
+
try:
|
|
52
|
+
return b.decode("utf-16-le", errors="ignore").replace("\x00", "").strip()
|
|
53
|
+
except Exception:
|
|
54
|
+
return b.decode("utf-8", errors="ignore").strip()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def probe():
|
|
58
|
+
"""能力探测:wsl / 发行版 / gapseq 环境 / 序列库注册(防假已装 UniProt 灾难)。
|
|
59
|
+
返回 dict(level: OK / DEGRADED / MISSING + checks)。"""
|
|
60
|
+
res = {"capable": False, "level": "MISSING", "checks": {}}
|
|
61
|
+
rc, out, err = wsl_run("uname -a", 30)
|
|
62
|
+
res["checks"]["wsl"] = rc == 0
|
|
63
|
+
if rc != 0:
|
|
64
|
+
res["detail"] = f"wsl 不可用: {out[:200] or err[:200]}"
|
|
65
|
+
return res
|
|
66
|
+
rc, out, err = wsl_run("grep -iE '^ID=' /etc/os-release 2>/dev/null", 30)
|
|
67
|
+
res["checks"]["distro"] = rc == 0 and "ubuntu" in out.lower()
|
|
68
|
+
if not res["checks"]["distro"]:
|
|
69
|
+
res["detail"] = f"发行版非预期(期望 Ubuntu): {out[:200] or err[:200]}"
|
|
70
|
+
res["level"] = "DEGRADED"
|
|
71
|
+
return res
|
|
72
|
+
rc, out, err = wsl_run(
|
|
73
|
+
f"source {CONDA_SH} && conda activate {GAPSEQ_ENV} && gapseq -v 2>&1", 120)
|
|
74
|
+
res["checks"]["gapseq"] = rc == 0 and "gapseq" in out.lower()
|
|
75
|
+
if not res["checks"]["gapseq"]:
|
|
76
|
+
res["detail"] = f"gapseq 环境不可用: {out[:300] or err[:300]}"
|
|
77
|
+
res["level"] = "DEGRADED"
|
|
78
|
+
return res
|
|
79
|
+
import re
|
|
80
|
+
mm = re.search(r"gapseq version:\s*([\d.]+)", out)
|
|
81
|
+
res["gapseq_version"] = mm.group(1) if mm else out[:80]
|
|
82
|
+
seqdb = "/mnt/f/Datasets/gapseq/db"
|
|
83
|
+
rc, out, err = wsl_run(
|
|
84
|
+
f"source {CONDA_SH} && conda activate {GAPSEQ_ENV} && "
|
|
85
|
+
f"gapseq update-sequences -t Bacteria -D {seqdb} -q -c 2>&1", 300)
|
|
86
|
+
res["checks"]["seqdb"] = "up-to-date" in out.lower()
|
|
87
|
+
if not res["checks"]["seqdb"]:
|
|
88
|
+
res["detail"] = f"序列库未注册(可能触发在线下载灾难): {out[:400]}"
|
|
89
|
+
res["level"] = "DEGRADED"
|
|
90
|
+
return res
|
|
91
|
+
res["capable"] = True
|
|
92
|
+
res["level"] = "OK"
|
|
93
|
+
return res
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
# 原子步骤(dsh agent 编排:setup -> launch -> status* -> fetch)
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
def launch_gapseq(input_fna, name="model", work_win=None):
|
|
100
|
+
"""拷贝输入 + 构造 doall 脚本(base64 防引号)+ nohup/setsid 后台启动(立即返回)。
|
|
101
|
+
单实例保护:已有 running 任务时拒绝重复 launch(返回 launched:false + reason)。
|
|
102
|
+
返回 {launched, work_win, wsl_work, name, reason?}。"""
|
|
103
|
+
import base64
|
|
104
|
+
# 协议层单实例保护:正在运行则拒绝
|
|
105
|
+
st = status_gapseq()
|
|
106
|
+
if st["state"] == "running":
|
|
107
|
+
return {"launched": False, "reason": "已有 doall 在运行(state=running),先 action=status 等待完成,不要重复 launch",
|
|
108
|
+
"work_win": work_win, "wsl_work": WSL_WORK, "name": name}
|
|
109
|
+
work_dir = work_win or os.path.join(os.path.expanduser("~"), ".dsh", "dsh-bio-gem", "models")
|
|
110
|
+
os.makedirs(work_dir, exist_ok=True)
|
|
111
|
+
if not os.path.exists(input_fna):
|
|
112
|
+
raise ValueError(f"输入文件不存在: {input_fna}")
|
|
113
|
+
win_drive = input_fna.split(":")[0].lower()
|
|
114
|
+
src = f"/mnt/{win_drive}{input_fna.split(':', 1)[1].replace(chr(92), '/')}"
|
|
115
|
+
|
|
116
|
+
script = (
|
|
117
|
+
"#!/bin/bash\n"
|
|
118
|
+
# 残留 blastp 清理(blastp 不会出现在本脚本命令行里,pkill 无自伤风险;
|
|
119
|
+
# doall 相关进程不在此清——靠协议层 running 保护防重复启动)
|
|
120
|
+
"pkill -9 -f blastp 2>/dev/null; sleep 1\n"
|
|
121
|
+
f"mkdir -p {WSL_WORK} && rm -f {WSL_WORK}/*\n"
|
|
122
|
+
f"cp '{src}' {WSL_WORK}/{name}.fna || {{ echo 'COPY_FAIL' > {WSL_WORK}/doall.rc; exit 1; }}\n"
|
|
123
|
+
f"cd {WSL_WORK}\n"
|
|
124
|
+
f"source {CONDA_SH}\n"
|
|
125
|
+
f"conda activate {GAPSEQ_ENV}\n"
|
|
126
|
+
# doall 只吃位置参数(实测 -m/-f/-D 任何 option 组合都会触发 usage——doall.sh getopts 有坑);
|
|
127
|
+
# 默认介质 auto + 默认序列库目录(envs/gapseq/share/gapseq/dat/seq/Bacteria 已完整部署 1.1GB)
|
|
128
|
+
f"gapseq doall {name}.fna > doall.log 2>&1\n"
|
|
129
|
+
"echo $? > doall.rc\n"
|
|
130
|
+
)
|
|
131
|
+
b64 = base64.b64encode(script.encode("utf-8")).decode("ascii")
|
|
132
|
+
rc, out, err = wsl_run(
|
|
133
|
+
f"mkdir -p {WSL_WORK} && echo {b64} | base64 -d > {WSL_WORK}/run_doall.sh && "
|
|
134
|
+
f"chmod +x {WSL_WORK}/run_doall.sh && ls -la {WSL_WORK}/run_doall.sh", 60)
|
|
135
|
+
if "run_doall.sh" not in out:
|
|
136
|
+
raise RuntimeError(f"doall 脚本未落盘: {out[:300] or err[:300]}")
|
|
137
|
+
rc, out, err = wsl_run(
|
|
138
|
+
f"cd / && nohup setsid {WSL_WORK}/run_doall.sh >/dev/null 2>&1 < /dev/null & "
|
|
139
|
+
f"sleep 3 && echo LAUNCHED", 60)
|
|
140
|
+
return {"launched": "LAUNCHED" in out, "work_win": work_dir,
|
|
141
|
+
"wsl_work": WSL_WORK, "name": name, "launch_out": out[:120]}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def status_gapseq():
|
|
145
|
+
"""查 doall 状态:先看进程(blastp/gapseq_find 在跑 = running),再看哨兵 doall.rc。
|
|
146
|
+
返回 {state: idle|running|done|failed|unknown, rc, log_tail, hint}。"""
|
|
147
|
+
log_tail = _tail_wsl(WSL_WORK, 40)
|
|
148
|
+
# 1) 真实进程存在性
|
|
149
|
+
_r, proc_out, _e = wsl_run(
|
|
150
|
+
"ps aux | grep -E 'blastp|gapseq_find' | grep -v grep | wc -l", 30)
|
|
151
|
+
try:
|
|
152
|
+
proc_n = int((proc_out.strip() or "0").split()[0])
|
|
153
|
+
except (ValueError, IndexError):
|
|
154
|
+
proc_n = -1
|
|
155
|
+
# 2) 哨兵
|
|
156
|
+
_r, out, _e = wsl_run(
|
|
157
|
+
f"test -f {WSL_WORK}/doall.rc && cat {WSL_WORK}/doall.rc || echo NONE", 30)
|
|
158
|
+
has_sentinel = "NONE" not in out
|
|
159
|
+
if proc_n > 0 and not has_sentinel:
|
|
160
|
+
return {"state": "running", "rc": None, "log_tail": log_tail,
|
|
161
|
+
"note": "doall 运行中(30-60min),2-5 分钟后再查"}
|
|
162
|
+
if proc_n == 0 and not has_sentinel:
|
|
163
|
+
return {"state": "idle", "rc": None, "log_tail": log_tail,
|
|
164
|
+
"note": "无运行任务,可 action=launch 启动"}
|
|
165
|
+
try:
|
|
166
|
+
rc_val = int(out.strip().split()[-1])
|
|
167
|
+
except (ValueError, IndexError):
|
|
168
|
+
rc_val = None
|
|
169
|
+
if rc_val == 0:
|
|
170
|
+
return {"state": "done", "rc": 0, "log_tail": log_tail}
|
|
171
|
+
if rc_val is not None:
|
|
172
|
+
return {"state": "failed", "rc": rc_val, "log_tail": log_tail,
|
|
173
|
+
"hint": "COPY_FAIL=输入拷贝失败(检查 /mnt/f 路径可读);其它 rc 见日志尾部"}
|
|
174
|
+
return {"state": "unknown", "rc": None, "log_tail": log_tail}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _win_to_mnt(path):
|
|
178
|
+
"""Windows 路径 -> WSL /mnt 路径(cp 目标必须 /mnt/<盘>/...)。"""
|
|
179
|
+
p = path.replace("\\", "/")
|
|
180
|
+
if len(p) >= 2 and p[1] == ":":
|
|
181
|
+
return "/mnt/" + p[0].lower() + "/" + p[3:].lstrip("/")
|
|
182
|
+
return p
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def fetch_gapseq(work_win, name="model", log_tail=None):
|
|
186
|
+
"""doall 完成后把 WSL 产物拷回 Windows。返回 {model, files, wsl_out_ls, log_local}。"""
|
|
187
|
+
work_dir = work_win or os.path.join(os.path.expanduser("~"), ".dsh", "dsh-bio-gem", "models")
|
|
188
|
+
os.makedirs(work_dir, exist_ok=True)
|
|
189
|
+
rc2, out2, err2 = wsl_run(f"ls {WSL_WORK}/ {WSL_WORK}/*.xml 2>/dev/null", 60)
|
|
190
|
+
wsl_ls = out2[:400]
|
|
191
|
+
win_mnt = _win_to_mnt(work_dir) # WSL 侧可写的 /mnt 目标
|
|
192
|
+
wsl_run(
|
|
193
|
+
f"cp {WSL_WORK}/*.xml {WSL_WORK}/*.faa.gz {WSL_WORK}/*.tbl {WSL_WORK}/*.csv {win_mnt}/ 2>/dev/null; "
|
|
194
|
+
f"cp {WSL_WORK}/doall.log {win_mnt}/gapseq_doall.log 2>/dev/null", 180)
|
|
195
|
+
files = [f for f in os.listdir(work_dir)
|
|
196
|
+
if f.endswith((".xml", ".faa.gz", ".tbl", ".csv")) or f == "gapseq_doall.log"]
|
|
197
|
+
xmls = [f for f in files if f.endswith(".xml")]
|
|
198
|
+
if not xmls:
|
|
199
|
+
raise RuntimeError(f"gapseq 未产出 xml(WSL 目录: {wsl_ls};日志尾部: {log_tail or ''})")
|
|
200
|
+
model = os.path.join(work_dir, xmls[0])
|
|
201
|
+
return {"model": model, "files": files, "wsl_out_ls": wsl_ls,
|
|
202
|
+
"log_local": os.path.join(work_dir, "gapseq_doall.log")}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def run_gapseq(input_fna, work_win, name="model", progress=None):
|
|
206
|
+
"""组合封装(build.py 兼容):launch -> 轮询 -> fetch。
|
|
207
|
+
新架构推荐 agent 直接用原子步骤(gem_gapseq launch/status/fetch)编排。"""
|
|
208
|
+
launch_gapseq(input_fna, name=name, work_win=work_win)
|
|
209
|
+
st = time.time()
|
|
210
|
+
while True:
|
|
211
|
+
stt = status_gapseq()
|
|
212
|
+
if stt["state"] != "running":
|
|
213
|
+
break
|
|
214
|
+
if progress:
|
|
215
|
+
progress({"event": "gapseq_progress", "elapsed_s": int(time.time() - st),
|
|
216
|
+
"log_tail": stt.get("log_tail", "")})
|
|
217
|
+
time.sleep(120)
|
|
218
|
+
if stt["state"] == "failed":
|
|
219
|
+
raise RuntimeError(f"gapseq doall rc={stt.get('rc')}; 日志尾部: {stt.get('log_tail', '')}")
|
|
220
|
+
if stt["state"] != "done":
|
|
221
|
+
raise RuntimeError(f"gapseq doall 异常状态: {stt}")
|
|
222
|
+
fet = fetch_gapseq(work_win, name=name, log_tail=stt.get("log_tail"))
|
|
223
|
+
return fet["model"], _tail(work_win, "gapseq_doall.log")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _tail_wsl(wsl_work, n=40):
|
|
227
|
+
"""WSL 侧 doall.log 尾部(进度事件携带)。"""
|
|
228
|
+
_r, _o, _e = wsl_run(f"tail -{n} {wsl_work}/doall.log 2>/dev/null", 30)
|
|
229
|
+
return (_o or "").strip()
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _tail(work_dir, logname, n=60):
|
|
233
|
+
p = os.path.join(work_dir, logname)
|
|
234
|
+
if not os.path.exists(p):
|
|
235
|
+
return ""
|
|
236
|
+
with open(p, encoding="utf-8", errors="ignore") as f:
|
|
237
|
+
lines = f.readlines()
|
|
238
|
+
return "".join(lines[-n:])
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
if __name__ == "__main__":
|
|
242
|
+
import sys as _sys
|
|
243
|
+
cmd = _sys.argv[1] if len(_sys.argv) > 1 else "probe"
|
|
244
|
+
if cmd == "probe":
|
|
245
|
+
print(json.dumps(probe(), ensure_ascii=False, indent=2))
|
|
246
|
+
elif cmd == "version":
|
|
247
|
+
rc, out, err = wsl_run(
|
|
248
|
+
f"source {CONDA_SH} && conda activate {GAPSEQ_ENV} && gapseq -v 2>&1", 120)
|
|
249
|
+
print(out[:300] or err[:300])
|
|
250
|
+
else:
|
|
251
|
+
print("unknown cmd")
|