feat(v1.8.1): Windows 兼容性修复 + SKILL.md 铁律区块

Windows 兼容性修复(基于真实使用痛点):

- force_utf8_stdout(): 强制 stdout/stderr 为 UTF-8,修复 Windows GBK 崩溃(print('\\xa0') 不再炸)

- resolve_instances/load_config 新增 %APPDATA%/searxng-cli/ 路径,覆盖 Windows 配置约定

- fetch.py 失败诊断增强:输出 status_code=/cause=/url= 字段,AI Agent 可程序化区分 404/403/DNS 失败

SKILL.md 铁律区块(5 条,置顶):

- stdout=数据/stderr=日志 永不混淆

- 禁用 2>/dev/null(丢弃 stderr = 失败时零诊断)

- 排错去 --quiet 加 --verbose

- 配置查找覆盖 WSL + Windows 双路径

- 实例 URL 必填,公共实例发现已移除

测试: 352 -> 362(新增 10 个:force_utf8_stdout 幂等性/GBK 替换/非 ASCII 打印/APPDATA 路径发现/txt 回退/空 APPDATA)
This commit is contained in:
2026-08-01 20:50:42 +08:00
parent 657af0a221
commit f7cdc81c7f
8 changed files with 325 additions and 12 deletions
+35 -2
View File
@@ -34,6 +34,7 @@ from common import (
build_auth_headers,
classify_error,
emit_progress,
force_utf8_stdout,
resolve_auth_basic,
resolve_auth_bearer,
set_progress_enabled,
@@ -329,13 +330,39 @@ def _read_instance_file(path: Path) -> list:
return []
def _windows_appdata_config_dir() -> Path:
"""Return the Windows APPDATA config directory, or a sentinel Path if unset.
On Windows, the conventional per-user app config directory is
``%APPDATA%`` (typically ``C:\\Users\\<user>\\AppData\\Roaming``).
On POSIX, this env var is unset and we return a sentinel
``Path("/__no_appdata__")`` which never exists on disk, so the caller
can unconditionally append it to the candidate list without polluting
Linux/macOS lookups.
Note: ``Path("")`` resolves to ``.`` (current directory) on Windows,
which DOES exist — so we must use an absolute sentinel path instead.
"""
appdata = os.environ.get("APPDATA", "")
if appdata:
return Path(appdata) / "searxng-cli"
# Sentinel: absolute path that never exists. Using "/" + unlikely name
# keeps it false on both POSIX and Windows (where "/" is the drive root).
return Path("/__no_appdata__")
def resolve_instances(cli_arg: str = None) -> list:
"""Resolve instance URLs from (in priority order):
1. ``--instance`` CLI flag (comma-separated list)
2. ``SEARXNG_INSTANCE`` environment variable (comma-separated list)
3. config file: ``./searxng.toml`` → ``~/.config/searxng-cli/searxng.toml``
→ ``./instances.txt`` → ``~/.config/searxng-cli/instances.txt``
3. config file search order:
a. ``./searxng.toml``
b. ``~/.config/searxng-cli/searxng.toml``
c. ``%APPDATA%/searxng-cli/searxng.toml`` (Windows only)
d. ``./instances.txt``
e. ``~/.config/searxng-cli/instances.txt``
f. ``%APPDATA%/searxng-cli/instances.txt`` (Windows only)
Returns an empty list if no instance can be resolved.
"""
@@ -346,11 +373,14 @@ def resolve_instances(cli_arg: str = None) -> list:
if env:
return parse_instances(env)
win_dir = _windows_appdata_config_dir()
candidates = [
Path.cwd() / "searxng.toml",
Path.home() / ".config" / "searxng-cli" / "searxng.toml",
win_dir / "searxng.toml",
Path.cwd() / "instances.txt",
Path.home() / ".config" / "searxng-cli" / "instances.txt",
win_dir / "instances.txt",
]
for p in candidates:
if p.exists():
@@ -395,9 +425,11 @@ def load_config(config_path: str = None) -> dict:
except Exception as e:
logger.warning(f"Warning: cannot read config '{p}': {e}")
return {}
win_dir = _windows_appdata_config_dir()
candidates = [
Path.cwd() / "searxng.toml",
Path.home() / ".config" / "searxng-cli" / "searxng.toml",
win_dir / "searxng.toml",
]
for p in candidates:
if p.exists():
@@ -1379,6 +1411,7 @@ def main():
pre_args, _ = pre.parse_known_args()
setup_logging(verbose=pre_args.verbose, quiet=pre_args.quiet)
force_utf8_stdout() # Windows: prevent GBK crash on non-ASCII chars
# Load config defaults from --config file, else ./searxng.toml or
# ~/.config/searxng-cli/searxng.toml. Every CLI flag below can be