feat(v1.7.0): AI 友好度增强 + 测试补全 (155→309)
核心新增(面向 AI Agent 程序化使用): - 结构化错误码体系:E_CONFIG/E_AUTH/E_NETWORK/E_RATE_LIMIT/E_PARSE/E_EMPTY/E_INPUT/E_INTERNAL classify_error() 自动分类异常,JSON 错误输出含 error_code 字段 - JSON Lines 流式输出 (--stream):每条结果独立一行,AI 可增量处理 - 进度事件 (--progress):JSON Lines 事件流到 stderr(start/cache_hit/fetch_ok/done 等) 测试补全(+154 例,覆盖全部高风险盲区): - HTML 回退搜索路径 (19) - --fetch 自动抓取 (21) - --verify 健康检查 (15) - 输出格式化 (15) - 实例解析链 (20) - 并行多实例搜索 (10) - CLI 入口与端到端 (17) - 错误码分类 (27) - 流式输出与进度事件 (10) 源码改进: - search.py: h3 内 a 标签 href 作为 url fallback,提升 SearXNG 主题兼容性 - common.py: 新增 classify_error/emit_progress/set_progress_enabled 文档同步:SKILL.md 新增 AI Agent Integration Guide 章节,README.md 更新参数与错误码表
This commit is contained in:
@@ -259,3 +259,136 @@ def is_retryable_error(exc: BaseException) -> bool:
|
||||
# requests connection/timeout error without a response -> transient
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
# ----- Structured error classification -----
|
||||
#
|
||||
# 错误码体系:让 AI Agent 程序化地判断错误类型并采取恢复策略。
|
||||
# 所有错误码以 E_ 前缀,在 --format json 模式下随 error_code 字段输出。
|
||||
#
|
||||
# AI 可根据 error_code 决策:
|
||||
# E_CONFIG → 检查实例配置/环境变量,提示用户设置
|
||||
# E_AUTH → 检查 token/凭证,提示用户重新认证
|
||||
# E_NETWORK → 重试或切换实例/代理
|
||||
# E_RATE_LIMIT → 等待后重试,降低请求频率
|
||||
# E_PARSE → 检查实例是否支持 JSON,尝试 HTML 回退
|
||||
# E_EMPTY → 调整查询词或时间范围
|
||||
# E_INPUT → 修正参数/文件路径
|
||||
# E_INTERNAL → 报告 bug,附带完整错误信息
|
||||
|
||||
# 错误码常量(供 search.py / fetch.py 引用)
|
||||
E_CONFIG = "E_CONFIG"
|
||||
E_AUTH = "E_AUTH"
|
||||
E_NETWORK = "E_NETWORK"
|
||||
E_RATE_LIMIT = "E_RATE_LIMIT"
|
||||
E_PARSE = "E_PARSE"
|
||||
E_EMPTY = "E_EMPTY"
|
||||
E_INPUT = "E_INPUT"
|
||||
E_INTERNAL = "E_INTERNAL"
|
||||
|
||||
|
||||
def classify_error(exc: BaseException) -> str:
|
||||
"""将异常分类为结构化错误码,供 AI Agent 程序化处理。
|
||||
|
||||
分类逻辑(按优先级):
|
||||
1. 429 → E_RATE_LIMIT
|
||||
2. 401/403 → E_AUTH
|
||||
3. 4xx(非上述)→ E_INPUT(请求参数问题)
|
||||
4. 5xx / URLError / OSError / TimeoutError → E_NETWORK
|
||||
5. json.JSONDecodeError / ValueError → E_PARSE
|
||||
6. FileNotFoundError → E_INPUT
|
||||
7. RuntimeError → 尝试从消息中提取线索,否则 E_INTERNAL
|
||||
8. 其他 → E_INTERNAL
|
||||
"""
|
||||
import json as _json
|
||||
|
||||
# HTTP 错误(urllib 和 requests 都有 .code 或 .status_code)
|
||||
status = None
|
||||
if isinstance(exc, urllib.error.HTTPError):
|
||||
status = exc.code
|
||||
else:
|
||||
resp = getattr(exc, "response", None)
|
||||
status = getattr(resp, "status_code", None)
|
||||
|
||||
if status is not None:
|
||||
if status == 429:
|
||||
return E_RATE_LIMIT
|
||||
if status in (401, 403):
|
||||
return E_AUTH
|
||||
if 400 <= status < 500:
|
||||
return E_INPUT
|
||||
if 500 <= status < 600:
|
||||
return E_NETWORK
|
||||
|
||||
# 文件/输入错误(FileNotFoundError 是 OSError 子类,必须先于 OSError 检查)
|
||||
if isinstance(exc, FileNotFoundError):
|
||||
return E_INPUT
|
||||
|
||||
# 连接级错误
|
||||
if isinstance(exc, (urllib.error.URLError, OSError, TimeoutError)):
|
||||
return E_NETWORK
|
||||
if isinstance(exc, ConnectionError):
|
||||
return E_NETWORK
|
||||
|
||||
# 解析错误
|
||||
if isinstance(exc, (ValueError, _json.JSONDecodeError)):
|
||||
return E_PARSE
|
||||
|
||||
# RuntimeError:从消息中推断(search_multi 的 "All N instances failed" 等)
|
||||
msg = str(exc).lower()
|
||||
if isinstance(exc, RuntimeError):
|
||||
if "auth" in msg or "403" in msg or "401" in msg:
|
||||
return E_AUTH
|
||||
if "rate" in msg or "429" in msg:
|
||||
return E_RATE_LIMIT
|
||||
if "all" in msg and "instance" in msg and "fail" in msg:
|
||||
return E_NETWORK
|
||||
if "parse" in msg or "json" in msg or "html" in msg:
|
||||
return E_PARSE
|
||||
if "not found" in msg or "no " in msg and "instance" in msg:
|
||||
return E_CONFIG
|
||||
return E_INTERNAL
|
||||
|
||||
return E_INTERNAL
|
||||
|
||||
|
||||
# ----- Progress event emitter (for --progress flag) -----
|
||||
#
|
||||
# 当 --progress 启用时,search.py 会调用 emit_progress() 发射结构化事件到
|
||||
# stderr(JSON Lines 格式)。AI Agent 可解析这些事件来跟踪执行进度。
|
||||
#
|
||||
# 事件类型:
|
||||
# {"event": "start", "query": "...", "instances": N}
|
||||
# {"event": "instance_try", "url": "...", "attempt": 1}
|
||||
# {"event": "instance_ok", "url": "...", "latency": 0.5, "results": 10}
|
||||
# {"event": "instance_fail", "url": "...", "error": "...", "error_code": "E_*"}
|
||||
# {"event": "cache_hit", "query": "...", "ttl": 30}
|
||||
# {"event": "cache_store", "query": "...", "ttl": 30}
|
||||
# {"event": "fetch_start", "count": 3}
|
||||
# {"event": "fetch_ok", "url": "...", "chars": 1234}
|
||||
# {"event": "fetch_fail", "url": "...", "error": "..."}
|
||||
# {"event": "done", "results": N, "query": "..."}
|
||||
# {"event": "error", "error": "...", "error_code": "E_*", "query": "..."}
|
||||
|
||||
_progress_enabled = False
|
||||
|
||||
|
||||
def set_progress_enabled(enabled: bool) -> None:
|
||||
"""全局开关:是否向 stderr 输出 JSON Lines 格式的进度事件。"""
|
||||
global _progress_enabled
|
||||
_progress_enabled = enabled
|
||||
|
||||
|
||||
def emit_progress(event: str, **kwargs) -> None:
|
||||
"""向 stderr 输出一行 JSON 格式的进度事件。
|
||||
|
||||
仅当 --progress 启用时才输出。``event`` 是事件类型字符串,
|
||||
``kwargs`` 是事件的附加字段。输出格式为 JSON Lines(每行一个 JSON 对象)。
|
||||
"""
|
||||
if not _progress_enabled:
|
||||
return
|
||||
import json as _json
|
||||
payload = {"event": event}
|
||||
payload.update(kwargs)
|
||||
print(_json.dumps(payload, ensure_ascii=False), file=sys.stderr, flush=True)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user