#!/usr/bin/env python3 """冒烟专用 stub 引擎(不访问外网;Contract §5-A6.4)。 单进程模拟三引擎形状(T1 契约:形状 = bench 实测样本,数值固定): - searxng:GET /search?q=...&format=json → bench/searxng-cn/samples/t1-1.excerpt.json 形状(2 条结果) - trafilatura:POST /v1/read → bench/trafilatura-http/app.py 响应形状(固定 markdown) - lightpanda/shell 冒烟为降级路径:/json/version 不实现(health 不可达 → 适配器降级), 断言搜索/精读走 searxng/trafilatura stub 仍 200。 仅监听容器内 :80(policy SSRF 契约仅允许 80/443,stub 对齐生产 URL 形状);无外网依赖。 """ import json import re from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from urllib.parse import urlparse, parse_qs LISTEN = ("0.0.0.0", 80) # 固定 2 条结果(形状对齐 bench/searxng-cn/samples/t1-1.excerpt.json:title/url/content/engine)。 SEARX_RESULTS = [ { "title": "stub-海关总署 税务总局关于跨境电子商务出口退运商品税收优惠政策", "url": "https://hainan.chinatax.gov.cn/xxgk_6_1/06163393.html", "content": "一、对自2026年1月1日至2027年12月31日期间在跨境电子商务海关监管代码(1210、9610、9710、9810)项下申报出口,因滞销、退货原因,自出口之日起6个月内原状退运进境的商品(不含食品),免征进口关税和进口环节增值税、消费税。", "engine": "baidu", }, { "title": "stub-雨果跨境-跨境电商品牌出海产业互联网平台", "url": "https://m.cifnews.com/", "content": "雨果跨境以雨果网作为流量依托,致力于为跨境电商从业者提供全球产业出海,链接全球流通,实现全球品牌的产业互联网平台。", "engine": "bing", }, ] # 固定 markdown(形状对齐 bench/trafilatura-http/app.py _extract 返回)。 STUB_MARKDOWN = "# stub 政策公告\n\n为支持跨境电子商务新业态发展,现将出口退运商品税收优惠政策公告如下:\n\n一、对自2026年1月1日至2027年12月31日期间在跨境电子商务海关监管代码项下申报出口的商品,免征进口关税和进口环节增值税、消费税。\n" STUB_TITLE = "财政部 海关总署 税务总局关于跨境电子商务出口退运商品税收优惠政策的公告(stub)" class Handler(BaseHTTPRequestHandler): server_version = "smoke-stub-engines/1.0" def _json(self, code, obj): raw = json.dumps(obj, ensure_ascii=False).encode("utf-8") self.send_response(code) self.send_header("Content-Type", "application/json; charset=utf-8") self.send_header("Content-Length", str(len(raw))) self.end_headers() self.wfile.write(raw) def do_GET(self): # noqa: N802 path = self.path.split("?", 1)[0] if path in ("/healthz", "/health"): # 同一进程同时顶 searxng(/healthz)与 trafilatura(/health)探活形状。 self._json(200, {"ok": True, "service": "smoke-stub-engines"}) return if path == "/search": qs = parse_qs(urlparse(self.path).query) q = (qs.get("q") or [""])[0] self._json(200, { "query": q, "number_of_results": None, "unresponsive_engines": [], "results": SEARX_RESULTS, }) return self._json(404, {"ok": False, "error": "not_found"}) def do_POST(self): # noqa: N802 path = self.path.split("?", 1)[0] length = int(self.headers.get("Content-Length") or 0) try: payload = json.loads(self.rfile.read(length) or b"{}") except json.JSONDecodeError: self._json(400, {"ok": False, "error": "bad_json"}) return if path == "/v1/read": # trafilatura 契约(app.py) max_chars = int(payload.get("max_chars") or 20000) md = STUB_MARKDOWN truncated = False if max_chars and len(md) > max_chars: md = md[:max_chars] truncated = True self._json(200, { "ok": True, "title": STUB_TITLE, "markdown": md, "char_count": len(md), "truncated": truncated, "url": payload.get("url") or "", }) return self._json(404, {"ok": False, "error": "not_found"}) def log_message(self, fmt, *args): # 静音(smoke 日志走 smoke-local.sh tee) pass def main(): httpd = ThreadingHTTPServer(LISTEN, Handler) print(f"stub-engines listening {LISTEN}", flush=True) httpd.serve_forever() if __name__ == "__main__": main()