mcp查看与skill目录探测防卡死:只读链路_guard限时(本机8s/WSL20s)超时回菜单+WSL唤醒每5秒打点+dirs先展示两端目录存在性/数量/名字摘要并分端探测本机与WSL的API,单元与菜单e2e全绿
This commit is contained in:
@@ -70,7 +70,7 @@ python ocskill.py push .\my-skill --force
|
||||
# 反向:把 WSL 里的 skill 拷回本机
|
||||
python ocskill.py pull java-expert --out .\skills-backup
|
||||
|
||||
# 看两端目录与 API 连接状态
|
||||
# 看两端目录情况(路径 + skill 数量 + 名字摘要)与本机/WSL 分开的 API 连接状态
|
||||
python ocskill.py dirs
|
||||
```
|
||||
|
||||
@@ -162,12 +162,17 @@ python mcpctl.py off --no-reload # 只对调文件,不调用重载
|
||||
| 参数顺序被 argparse 拒(`mcpctl status --target wsl` 报 unrecognized) | `_hoist_shared()` 在解析前把共享 flag(`--target/--distro/--port/--timeout/--password/--no-reload/--ignore-mode`)提到最前,**子命令前后都能写** |
|
||||
| WSL 没跑时读配置卡死/假错 | `wsl_gate` 统一门控:**读写都先唤醒**——「先给机器启动起来,再去读配置」:status / mode 查看 / backup / `list` / `dirs` 等读操作,VM 没跑同样打印「正在唤醒」拉起 VM(超时 45s)再读,绝不秒回假错;唤醒失败给清晰指引 |
|
||||
| `wsl.exe` 超时后进程永久卡死(必须 ctrl+C) | 所有 `wsl.exe` 调用改走 `_wsl_safe_run`:`stdin=DEVNULL` + stdout/stderr 落**临时文件**(零管道)+ 先查 `-l -q --running` 绝不无提示冷启动。根因是管道句柄被 wslrelay 等后代继承,client 被 kill 后 `communicate()` 等 EOF 永不返回 |
|
||||
| UNC 半死时 `mcp 查看` / `skill dirs` 无限挂起(`Path.exists()`/`read_text` 阻塞在系统调用层,Ctrl+C 都打不断) | 只读链路统一走 `_guard()`:fn 放进 daemon 工作线程,主线程 `queue.get(timeout)` 限时等(本机 8s / WSL UNC 20s)。超时打印「读取超时(>Ns)」并返回菜单,绝不再挂死;主线程是 Python 层等待,Ctrl+C 随时可打断。覆盖 status / mode 查看 / backup / on·off 初始读 / dirs 两端目录探查 |
|
||||
| 唤醒 WSL 的十几秒里屏幕无输出,看着像卡死 | 唤醒统一包 `_run_with_beats()`:每 5 秒打一个 `.`,配合「正在唤醒(可能需要十几秒,期间每 5 秒打一个点)」提示,等待过程可见 |
|
||||
| 菜单被子命令 `raise SystemExit` 杀死 | `oc.py` 的 `run_safe()` 护栏:`KeyboardInterrupt` → 已取消回菜单;`SystemExit` → 报告信息回菜单;异常 → 打印错误回菜单,菜单本体永不退出 |
|
||||
|
||||
`list` / `dirs` 连接 OC 前会先打印「正在唤醒以便读配置…」(VM 没跑时)和
|
||||
「正在探测 OpenCode API 连接…」进度行(flush),
|
||||
`list` / `dirs` 连接 OC 前会先打印「正在唤醒以便读配置…」(VM 没跑时,每 5 秒
|
||||
打一个点)和「正在探测 OpenCode API 连接…」进度行(flush),
|
||||
几秒无响应时一眼看出是在唤醒/探测而不是死了;`list` 唤醒失败**绝不**回落到
|
||||
UNC 目录扫描(那会触发冷启动卡死),只走 API 或明确报错。
|
||||
`dirs` 的两端目录信息**永远先于** API 探测打印(路径 + 存在性 + skill 数量 +
|
||||
名字摘要),API 分端报「本机 OC / WSL OC」各自的连接结果,后面探测失败也不会
|
||||
吞掉前面的目录展示。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -23,8 +23,10 @@ import base64
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import queue
|
||||
import re
|
||||
import sys
|
||||
import threading
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
@@ -127,6 +129,61 @@ def _wsl_running(distro: str) -> bool:
|
||||
return distro in text
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 读操作防卡死
|
||||
# WSL UNC 路径(\\wsl.localhost\...)在 VM 半死 / 9p 无响应时,Path.exists() /
|
||||
# read_text 会在系统调用层无限挂起,且 Ctrl+C 打不断(Python 信号要等阻塞的
|
||||
# 系统调用返回才处理)—— 表现为「CLI 进入 mcp 查看直接卡死,只能强杀进程」。
|
||||
# 只读链路统一放进 daemon 工作线程限时等待:超时打印原因并返回菜单;
|
||||
# 主线程等的是 queue.get(timeout)(Python 层等待),Ctrl+C 随时能打断。
|
||||
def _guard(fn, timeout: float):
|
||||
"""在工作线程里跑 fn,主线程最多等 timeout 秒。
|
||||
返回 (True, 结果) 或 (False, 错误描述);错误描述为 'TIMEOUT' 表示超时。"""
|
||||
q: "queue.Queue" = queue.Queue(maxsize=1)
|
||||
|
||||
def _run() -> None:
|
||||
try:
|
||||
q.put((True, fn()))
|
||||
except Exception as exc: # noqa: BLE001
|
||||
q.put((False, f"{type(exc).__name__}: {exc}"))
|
||||
|
||||
threading.Thread(target=_run, daemon=True).start()
|
||||
try:
|
||||
return q.get(timeout=timeout)
|
||||
except queue.Empty:
|
||||
return False, "TIMEOUT"
|
||||
|
||||
|
||||
def _read_timeout(target: str) -> float:
|
||||
"""只读链路超时:本机文件秒回给 8s;WSL UNC 冷路径首访给 20s。"""
|
||||
return 20.0 if target == "wsl" else 8.0
|
||||
|
||||
|
||||
def _print_read_fail(what, res, tmo: float) -> int:
|
||||
"""_guard 读失败的统一出口(超时 / 异常都回菜单,不挂死)。"""
|
||||
if res == "TIMEOUT":
|
||||
print(f"读取超时(>{tmo:g}s):{what}")
|
||||
print(" WSL UNC 路径无响应时不再无限挂起;请检查 wsl.exe 状态后重试。")
|
||||
else:
|
||||
print(f"读取失败:{res}")
|
||||
return 2
|
||||
|
||||
|
||||
def _run_with_beats(fn, interval: float = 5.0):
|
||||
"""长操作期间每 interval 秒打一个进度点,避免用户把「等待」当成「卡死」。"""
|
||||
stop = threading.Event()
|
||||
|
||||
def _beat() -> None:
|
||||
while not stop.wait(interval):
|
||||
print(" .", end="", flush=True)
|
||||
|
||||
threading.Thread(target=_beat, daemon=True).start()
|
||||
try:
|
||||
return fn()
|
||||
finally:
|
||||
stop.set()
|
||||
print(flush=True)
|
||||
|
||||
|
||||
def fs_view(p: str, target: str, distro: str) -> str:
|
||||
"""mcpcfg 内部路径 -> 当前系统可访问的视图。
|
||||
Windows 操作 WSL 目标时,/root/... 必须换成 \\\\wsl.localhost\\<distro>\\...,
|
||||
@@ -148,8 +205,11 @@ def wsl_gate(args, need_write: bool = False) -> str | None:
|
||||
if _wsl_running(args.distro):
|
||||
return None
|
||||
verb = "读配置" if not need_write else "写配置"
|
||||
print(f"WSL({args.distro})未运行,正在唤醒以便{verb}(可能需要十几秒)...", flush=True)
|
||||
_wsl_safe_run(["wsl.exe", "-d", args.distro, "--", "true"], timeout=45)
|
||||
print(f"WSL({args.distro})未运行,正在唤醒以便{verb}"
|
||||
f"(可能需要十几秒,期间每 5 秒打一个点)...", flush=True)
|
||||
_run_with_beats(
|
||||
lambda: _wsl_safe_run(["wsl.exe", "-d", args.distro, "--", "true"], timeout=45)
|
||||
)
|
||||
if _wsl_running(args.distro):
|
||||
print("WSL 已就绪。", flush=True)
|
||||
return None
|
||||
@@ -585,8 +645,10 @@ def _stamp() -> str:
|
||||
return datetime.datetime.now().strftime("%Y%m%d%H%M%S")
|
||||
|
||||
|
||||
def print_state(cfg: dict) -> bool:
|
||||
has = active_has_mcp(cfg.get("active"))
|
||||
def print_state(cfg: dict, has: bool | None = None) -> bool:
|
||||
# has 由调用方经 _guard 预读(UNC 半死时活配置读取会挂,不能在这里裸读)
|
||||
if has is None:
|
||||
has = active_has_mcp(cfg.get("active"))
|
||||
print(f"模式:{MODE_LABEL.get(cfg['mode'], cfg['mode'])}(mcpcfg.txt「模式」键)")
|
||||
print(f"MCP :{'已开启(配置含 mcp.servers)' if has else '已关闭(安静,无 mcp 配置)'}")
|
||||
# 展示用原文(用户在 mcpcfg.txt 里看到的路径),内部访问已走视图翻译
|
||||
@@ -604,8 +666,15 @@ def cmd_status(args) -> int:
|
||||
return 2
|
||||
path = resolve_cfg_file(args.target, args.distro)
|
||||
print(f"配置文件:{path}")
|
||||
cfg = read_cfg(path, args.target, args.distro)
|
||||
print_state(cfg)
|
||||
tmo = _read_timeout(args.target)
|
||||
# 读链全线走 _guard:mcpcfg / 活配置任意一处 UNC 无响应都限时返回,不卡死
|
||||
ok, cfg = _guard(lambda: read_cfg(path, args.target, args.distro), tmo)
|
||||
if not ok:
|
||||
return _print_read_fail(path, cfg, tmo)
|
||||
ok, has = _guard(lambda: active_has_mcp(cfg.get("active")), tmo)
|
||||
if not ok:
|
||||
return _print_read_fail(cfg.get("active") or "活配置", has, tmo)
|
||||
print_state(cfg, has)
|
||||
return 0 if not cfg.get("error") else 2
|
||||
|
||||
|
||||
@@ -617,7 +686,10 @@ def _switch(args, want_on: bool) -> int:
|
||||
return 2
|
||||
path = resolve_cfg_file(args.target, args.distro)
|
||||
print(f"配置文件:{path}")
|
||||
cfg = read_cfg(path, args.target, args.distro)
|
||||
tmo = _read_timeout(args.target)
|
||||
ok, cfg = _guard(lambda: read_cfg(path, args.target, args.distro), tmo)
|
||||
if not ok:
|
||||
return _print_read_fail(path, cfg, tmo)
|
||||
if cfg.get("error"):
|
||||
print(f"失败:{cfg['error']}")
|
||||
return 2
|
||||
@@ -628,10 +700,12 @@ def _switch(args, want_on: bool) -> int:
|
||||
print("如需使用,请把模式改成「自动」或「手动」。")
|
||||
return 3
|
||||
|
||||
has = active_has_mcp(cfg["active"])
|
||||
ok, has = _guard(lambda: active_has_mcp(cfg["active"]), tmo)
|
||||
if not ok:
|
||||
return _print_read_fail(cfg.get("active") or "活配置", has, tmo)
|
||||
if has == want_on:
|
||||
print(f"无需切换:MCP 已是{'开启' if want_on else '关闭(安静)'}状态。")
|
||||
print_state(cfg)
|
||||
print_state(cfg, has)
|
||||
return 0
|
||||
|
||||
swap_files(cfg["on_path"], cfg["quiet_path"])
|
||||
@@ -693,12 +767,18 @@ def cmd_mode(args) -> int:
|
||||
print(err)
|
||||
return 2
|
||||
path = resolve_cfg_file(args.target, args.distro)
|
||||
if not path.exists():
|
||||
tmo = _read_timeout(args.target)
|
||||
ok, ex = _guard(lambda: path.exists(), tmo)
|
||||
if not ok:
|
||||
return _print_read_fail(path, ex, tmo)
|
||||
if not ex:
|
||||
print(f"配置文件不存在:{path}")
|
||||
return 2
|
||||
|
||||
if not args.value:
|
||||
cfg = read_cfg(path, args.target, args.distro)
|
||||
ok, cfg = _guard(lambda: read_cfg(path, args.target, args.distro), tmo)
|
||||
if not ok:
|
||||
return _print_read_fail(path, cfg, tmo)
|
||||
cur = MODE_LABEL.get(cfg.get("mode", "auto"), cfg.get("mode", "auto"))
|
||||
print(f"当前模式:{cur}")
|
||||
print("可选值:自动 / 手动 / 关闭(也支持 auto / manual / off)")
|
||||
@@ -720,11 +800,15 @@ def cmd_backup(args) -> int:
|
||||
print(err)
|
||||
return 2
|
||||
path = resolve_cfg_file(args.target, args.distro)
|
||||
if not path.exists():
|
||||
tmo = _read_timeout(args.target)
|
||||
ok, data = _guard(lambda: path.read_bytes() if path.exists() else None, tmo)
|
||||
if not ok:
|
||||
return _print_read_fail(path, data, tmo)
|
||||
if data is None:
|
||||
print(f"配置文件不存在:{path}")
|
||||
return 2
|
||||
bak = path.with_name(path.name + f".bak-{_stamp()}")
|
||||
bak.write_bytes(path.read_bytes())
|
||||
bak.write_bytes(data)
|
||||
print(f"已备份:{bak}")
|
||||
return 0
|
||||
|
||||
|
||||
@@ -519,7 +519,7 @@ def menu_skill(shared: dict) -> None:
|
||||
"1 -confirm 跳过;也可贴路径/zip;再选推送到哪)")
|
||||
print(" 3. 拉取技能到本机 OC(选来源即出编号列表:名字直拉;1 / 1. 二次确认,"
|
||||
"1 -confirm 跳过确认)")
|
||||
print(" 4. 查看技能目录与连接状态")
|
||||
print(" 4. 查看两端 skill 目录情况(路径 + skill 数量 + 本机/WSL API 分开探测)")
|
||||
print(" 5. 删除技能(选端即出编号列表:1 -confirm 直删;1 / 1. 打印名字+简介"
|
||||
"二次确认,confirm 删 / cancel 不动;先挑 .trash/ 备份,可恢复)")
|
||||
print(" 提示:任意位置输入 /home 或「返回首页」直接回顶层菜单")
|
||||
|
||||
+127
-27
@@ -26,11 +26,13 @@ import base64
|
||||
import binascii
|
||||
import json
|
||||
import os
|
||||
import queue
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
import zipfile
|
||||
@@ -133,12 +135,55 @@ def _wsl_running(distro: str) -> bool:
|
||||
return distro in text
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 读操作防卡死
|
||||
# WSL UNC 路径(\\wsl.localhost\...)在 VM 半死 / 9p 无响应时,Path.exists() /
|
||||
# iterdir 会在系统调用层无限挂起,且 Ctrl+C 打不断(Python 信号要等阻塞的
|
||||
# 系统调用返回才处理)。只读探查统一放进 daemon 工作线程限时等待:
|
||||
# 超时打印原因并回菜单;主线程等 queue.get(timeout)(Python 层),Ctrl+C 可打断。
|
||||
# (与 mcpctl._guard 逐字一致,保持三端同源。)
|
||||
def _guard(fn, timeout: float):
|
||||
"""在工作线程里跑 fn,主线程最多等 timeout 秒。
|
||||
返回 (True, 结果) 或 (False, 错误描述);错误描述为 'TIMEOUT' 表示超时。"""
|
||||
q: "queue.Queue" = queue.Queue(maxsize=1)
|
||||
|
||||
def _run() -> None:
|
||||
try:
|
||||
q.put((True, fn()))
|
||||
except Exception as exc: # noqa: BLE001
|
||||
q.put((False, f"{type(exc).__name__}: {exc}"))
|
||||
|
||||
threading.Thread(target=_run, daemon=True).start()
|
||||
try:
|
||||
return q.get(timeout=timeout)
|
||||
except queue.Empty:
|
||||
return False, "TIMEOUT"
|
||||
|
||||
|
||||
def _run_with_beats(fn, interval: float = 5.0):
|
||||
"""长操作期间每 interval 秒打一个进度点,避免用户把「等待」当成「卡死」。"""
|
||||
stop = threading.Event()
|
||||
|
||||
def _beat() -> None:
|
||||
while not stop.wait(interval):
|
||||
print(" .", end="", flush=True)
|
||||
|
||||
threading.Thread(target=_beat, daemon=True).start()
|
||||
try:
|
||||
return fn()
|
||||
finally:
|
||||
stop.set()
|
||||
print(flush=True)
|
||||
|
||||
|
||||
def _wsl_ensure_running(distro: str, purpose: str) -> str | None:
|
||||
"""UNC 写之前确保 VM 在跑;返回 None=可继续,字符串=错误信息。"""
|
||||
if os.name != "nt" or _wsl_running(distro):
|
||||
return None
|
||||
print(f"WSL({distro})未运行({purpose}需要访问它),正在唤醒(可能需要十几秒)...", flush=True)
|
||||
_wsl_safe_run(["wsl.exe", "-d", distro, "--", "true"], timeout=45)
|
||||
print(f"WSL({distro})未运行({purpose}需要访问它),正在唤醒"
|
||||
f"(可能需要十几秒,期间每 5 秒打一个点)...", flush=True)
|
||||
_run_with_beats(
|
||||
lambda: _wsl_safe_run(["wsl.exe", "-d", distro, "--", "true"], timeout=45)
|
||||
)
|
||||
if _wsl_running(distro):
|
||||
print("WSL 已就绪。", flush=True)
|
||||
return None
|
||||
@@ -231,22 +276,32 @@ def _discover_oc_ports() -> list[int]:
|
||||
return ports
|
||||
|
||||
|
||||
def _candidate_urls(explicit: str | None, distro: str, port: int) -> list[str]:
|
||||
def _candidate_urls(explicit: str | None, distro: str, port: int,
|
||||
side: str = "all") -> list[str]:
|
||||
"""side 决定探谁:local=只探本机(localhost + 端口发现);
|
||||
wsl=只探 WSL(WSL IP 优先,localhost/127 作镜像网络兜底);
|
||||
all=两者并集(保持旧行为)。显式 --url 时 side 被忽略。"""
|
||||
if explicit:
|
||||
return [explicit.rstrip("/")]
|
||||
urls = [
|
||||
f"http://localhost:{port}",
|
||||
f"http://127.0.0.1:{port}",
|
||||
]
|
||||
# 端口发现:默认端口没监听时,补上 opencode.exe 实际 LISTENING 的端口
|
||||
for p in _discover_oc_ports():
|
||||
urls.append(f"http://localhost:{p}")
|
||||
urls.append(f"http://127.0.0.1:{p}")
|
||||
# Windows 下再试 WSL 的真实 IP(localhost 转发失效时兜底)
|
||||
if os.name == "nt":
|
||||
ip = _wsl_ip(distro)
|
||||
if ip:
|
||||
urls.append(f"http://{ip}:{port}")
|
||||
urls: list[str] = []
|
||||
if side in ("all", "local"):
|
||||
urls += [
|
||||
f"http://localhost:{port}",
|
||||
f"http://127.0.0.1:{port}",
|
||||
]
|
||||
# 端口发现:默认端口没监听时,补上 opencode.exe 实际 LISTENING 的端口
|
||||
for p in _discover_oc_ports():
|
||||
urls.append(f"http://localhost:{p}")
|
||||
urls.append(f"http://127.0.0.1:{p}")
|
||||
if side in ("all", "wsl"):
|
||||
# Windows 下再试 WSL 的真实 IP(localhost 转发失效时兜底)
|
||||
if os.name == "nt":
|
||||
ip = _wsl_ip(distro)
|
||||
if ip:
|
||||
urls.append(f"http://{ip}:{port}")
|
||||
if side == "wsl":
|
||||
# WSL 侧补 localhost/127:镜像网络模式下 IP 不通时靠转发探中
|
||||
urls += [f"http://localhost:{port}", f"http://127.0.0.1:{port}"]
|
||||
seen, out = set(), []
|
||||
for u in urls:
|
||||
if u not in seen:
|
||||
@@ -301,14 +356,14 @@ def _try_fetch(url: str, password: str, timeout: float) -> tuple[int, bytes]:
|
||||
return 0, b""
|
||||
|
||||
|
||||
def connect_oc(args) -> tuple[str | None, object]:
|
||||
def connect_oc(args, side: str = "all") -> tuple[str | None, object]:
|
||||
"""
|
||||
连接 OC,返回 (base_url, payload)。
|
||||
依次尝试 /api/skill 与 /skill 两个已知端点;全失败则 payload=None。
|
||||
连接 OC,返回 (base_url, payload);side 见 _candidate_urls。
|
||||
依次尝试 /api/skill 与 /skill 两个已知端点;全失败则 payload=None(错误串)。
|
||||
"""
|
||||
password = args.password or _read_server_password(Path(args.remote_config_dir), args.distro)
|
||||
last_err = ""
|
||||
for base in _candidate_urls(args.url, args.distro, args.port):
|
||||
for base in _candidate_urls(args.url, args.distro, args.port, side):
|
||||
for path in ("/api/skill", "/skill"):
|
||||
code, body = _try_fetch(base + path, password, args.timeout)
|
||||
if code == 200:
|
||||
@@ -1147,16 +1202,61 @@ def cmd_delete(args) -> int:
|
||||
print(f"已删除{side} skill「{name}」(备份在 {backup},需要时可移回原位)")
|
||||
return 0
|
||||
|
||||
def _dirs_line(tag: str, root: str, timeout: float) -> None:
|
||||
"""打印一端 skill 目录情况:路径 + 存在性 + skill 数量(只读,限时防卡死)。
|
||||
目录信息永远先于 API 探测打印 —— 后面探测失败也不会吞掉目录展示。"""
|
||||
print(f" {tag}:{root}")
|
||||
ok, res = _guard(lambda: (Path(root).is_dir(), list_skill_names(root)), timeout)
|
||||
if not ok:
|
||||
if res == "TIMEOUT":
|
||||
print(f" 状态:读取超时(>{timeout:g}s,WSL UNC 无响应;不再无限挂起)")
|
||||
else:
|
||||
print(f" 状态:读取失败({res})")
|
||||
return
|
||||
isdir, names = res
|
||||
if isdir:
|
||||
print(f" 状态:存在,{len(names)} 个 skill")
|
||||
if names:
|
||||
shown = "、".join(names[:8]) + ("…" if len(names) > 8 else "")
|
||||
print(f" skill:{shown}")
|
||||
else:
|
||||
print(" 状态:目录不存在(首次推送/拉取时会自动创建)")
|
||||
|
||||
|
||||
def cmd_dirs(args) -> int:
|
||||
print(f"WSL skill 目录(push 目标):{wsl_skill_root(args.distro)}")
|
||||
print(f"本机 skill 目录(pull 默认落到当前目录):{WIN_LOCAL_ROOT}")
|
||||
# 读操作也先唤醒 VM(“先给机器启动起来,再去读配置”)
|
||||
"""展示两端 skill 目录情况 + 本机/WSL 分开的 API 连接状态。
|
||||
|
||||
2026-09-22 起按用户反馈增强:原版只打两行路径 + 一个混在一起的
|
||||
「OpenCode API」(本机和 WSL 的候选 URL 混探,连上本机也分不清是谁),
|
||||
看不到「目录是否存在 / 有多少 skill」。现在:
|
||||
1) 两端目录先打:路径 + 存在性 + skill 数量 + 名字摘要(限时防卡死);
|
||||
2) API 分端探测:本机(localhost + 端口发现)/ WSL(WSL IP 优先)各自报。
|
||||
"""
|
||||
local_root = WIN_LOCAL_ROOT
|
||||
wsl_root = wsl_skill_root(args.distro)
|
||||
|
||||
print("两端 skill 目录情况:")
|
||||
_dirs_line("本机 OC(pull 默认落点)", local_root, 8.0)
|
||||
gate_err = _wsl_ensure_running(args.distro, "dirs")
|
||||
if gate_err:
|
||||
print(gate_err)
|
||||
print("正在探测 OpenCode API 连接(约几秒;WSL 未运行会自动跳过 IP 探测)...", flush=True)
|
||||
base, _ = connect_oc(args)
|
||||
print(f"OpenCode API:{base or '未连接'}")
|
||||
print(f" WSL OC(push 缺省目标):{wsl_root}")
|
||||
print(f" 状态:无法访问({gate_err})")
|
||||
else:
|
||||
_dirs_line("WSL OC(push 缺省目标)", wsl_root, 20.0)
|
||||
|
||||
print("正在探测 OpenCode API 连接(本机 / WSL 分开探测)...", flush=True)
|
||||
base, err = connect_oc(args, side="local")
|
||||
print(f" 本机 OC API:已连接 {base}" if base
|
||||
else f" 本机 OC API:未连接({err or '未找到运行中的 OC'})")
|
||||
|
||||
# WSL 是否可探直接复用上面的门控结果 —— 再查一次 _wsl_running 会因
|
||||
# wsl.exe 列表偶发超时(刚唤醒/系统繁忙时)而误报「未运行」
|
||||
if gate_err:
|
||||
print(f" WSL OC API:未探测({gate_err})")
|
||||
else:
|
||||
base, err = connect_oc(args, side="wsl")
|
||||
print(f" WSL OC API:已连接 {base}" if base
|
||||
else f" WSL OC API:未连接({err or '未找到运行中的 OC'})")
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user