Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

## Table of Contents

- [What's new (2026-06-22) — HTTP Content Negotiation & Decompression](#whats-new-2026-06-22--http-content-negotiation--decompression)
- [What's new (2026-06-22) — multipart/form-data Build & Parse](#whats-new-2026-06-22--multipartform-data-build--parse)
- [What's new (2026-06-22) — Secret Redaction for Config & Logs](#whats-new-2026-06-22--secret-redaction-for-config--logs)
- [What's new (2026-06-22) — RFC 8288 Link Header & Pagination](#whats-new-2026-06-22--rfc-8288-link-header--pagination)
Expand Down Expand Up @@ -142,6 +143,12 @@

---

## What's new (2026-06-22) — HTTP Content Negotiation & Decompression

Build `Accept` headers and decode gzip/deflate. Full reference: [`docs/source/Eng/doc/new_features/v90_features_doc.rst`](docs/source/Eng/doc/new_features/v90_features_doc.rst).

- **`build_accept` / `build_accept_encoding` / `parse_quality_values` / `decode_body` / `negotiated_call`** (`AC_decode_body`, `AC_parse_quality_values`): `urllib`/`http_request` never set `Accept-Encoding` nor decoded `Content-Encoding`, so compressed bodies arrived raw. This adds `Accept`/`Accept-Encoding` builders, a q-value parser (sorted by quality), and gzip/deflate (incl. raw deflate) decoding. Brotli excluded (not stdlib). Pure-stdlib, deterministic.

## What's new (2026-06-22) — multipart/form-data Build & Parse

Build file-upload bodies. Full reference: [`docs/source/Eng/doc/new_features/v89_features_doc.rst`](docs/source/Eng/doc/new_features/v89_features_doc.rst).
Expand Down
7 changes: 7 additions & 0 deletions README/README_zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

## 目录

- [本次更新 (2026-06-22) — HTTP 内容协商与解压](#本次更新-2026-06-22--http-内容协商与解压)
- [本次更新 (2026-06-22) — multipart/form-data 构建与解析](#本次更新-2026-06-22--multipartform-data-构建与解析)
- [本次更新 (2026-06-22) — 配置与日志的机密脱敏](#本次更新-2026-06-22--配置与日志的机密脱敏)
- [本次更新 (2026-06-22) — RFC 8288 Link 标头与分页](#本次更新-2026-06-22--rfc-8288-link-标头与分页)
Expand Down Expand Up @@ -141,6 +142,12 @@

---

## 本次更新 (2026-06-22) — HTTP 内容协商与解压

构建 `Accept` 标头并解码 gzip/deflate。完整参考:[`docs/source/Zh/doc/new_features/v90_features_doc.rst`](../docs/source/Zh/doc/new_features/v90_features_doc.rst)。

- **`build_accept` / `build_accept_encoding` / `parse_quality_values` / `decode_body` / `negotiated_call`**(`AC_decode_body`、`AC_parse_quality_values`):`urllib`/`http_request` 从不设置 `Accept-Encoding` 也不解码 `Content-Encoding`,压缩内文以原始形式抵达。本功能加入 `Accept`/`Accept-Encoding` 构建器、q-value 解析器(按品质排序),以及 gzip/deflate(含 raw deflate)解码。排除 Brotli(非标准库)。纯标准库、确定。

## 本次更新 (2026-06-22) — multipart/form-data 构建与解析

构建文件上传内文。完整参考:[`docs/source/Zh/doc/new_features/v89_features_doc.rst`](../docs/source/Zh/doc/new_features/v89_features_doc.rst)。
Expand Down
7 changes: 7 additions & 0 deletions README/README_zh-TW.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

## 目錄

- [本次更新 (2026-06-22) — HTTP 內容協商與解壓縮](#本次更新-2026-06-22--http-內容協商與解壓縮)
- [本次更新 (2026-06-22) — multipart/form-data 建立與解析](#本次更新-2026-06-22--multipartform-data-建立與解析)
- [本次更新 (2026-06-22) — 設定與日誌的機密遮蔽](#本次更新-2026-06-22--設定與日誌的機密遮蔽)
- [本次更新 (2026-06-22) — RFC 8288 Link 標頭與分頁](#本次更新-2026-06-22--rfc-8288-link-標頭與分頁)
Expand Down Expand Up @@ -141,6 +142,12 @@

---

## 本次更新 (2026-06-22) — HTTP 內容協商與解壓縮

建立 `Accept` 標頭並解碼 gzip/deflate。完整參考:[`docs/source/Zh/doc/new_features/v90_features_doc.rst`](../docs/source/Zh/doc/new_features/v90_features_doc.rst)。

- **`build_accept` / `build_accept_encoding` / `parse_quality_values` / `decode_body` / `negotiated_call`**(`AC_decode_body`、`AC_parse_quality_values`):`urllib`/`http_request` 從不設定 `Accept-Encoding` 也不解碼 `Content-Encoding`,壓縮內文以原始形式抵達。本功能加入 `Accept`/`Accept-Encoding` 建構器、q-value 解析器(依品質排序),以及 gzip/deflate(含 raw deflate)解碼。排除 Brotli(非標準函式庫)。純標準函式庫、具決定性。

## 本次更新 (2026-06-22) — multipart/form-data 建立與解析

建立檔案上傳內文。完整參考:[`docs/source/Zh/doc/new_features/v89_features_doc.rst`](../docs/source/Zh/doc/new_features/v89_features_doc.rst)。
Expand Down
49 changes: 49 additions & 0 deletions docs/source/Eng/doc/new_features/v90_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
HTTP Content Negotiation & Decompression
========================================

``urllib`` / ``http_request`` never sets ``Accept-Encoding`` and never decodes a
``Content-Encoding`` response, so a body from a server that compresses arrives
raw; there was also no quality-value parser. This adds ``Accept`` /
``Accept-Encoding`` builders, a q-value parser, and gzip / deflate decoding.

Pure standard library (``gzip`` / ``zlib``); imports no ``PySide6``. Brotli is
deliberately excluded (not stdlib). Every function is pure, so it is fully
deterministic in CI.

Headless API
------------

.. code-block:: python

from je_auto_control import (
build_accept, build_accept_encoding, negotiated_call,
parse_quality_values, decode_body, build_call,
)

call = negotiated_call(
build_call(url),
accept=build_accept([("application/json", 1.0), ("text/html", 0.8)]),
accept_encoding=build_accept_encoding(),
)
# ... perform the call, then:
body = decode_body(response["headers"], raw_bytes)

ranked = parse_quality_values("text/html;q=0.8, application/json")
# [("application/json", 1.0), ("text/html", 0.8)]

``build_accept`` turns media types or ``(type, q)`` pairs into an ``Accept``
header; ``build_accept_encoding`` defaults to ``gzip, deflate``.
``parse_quality_values`` parses an ``Accept`` / ``Accept-Encoding`` header into
``(token, q)`` pairs sorted by quality (ties keep their order). ``decode_body``
decompresses a response body according to its ``Content-Encoding`` (``gzip`` /
``deflate``, including raw deflate, plus ``identity``), raising ``ValueError``
for anything unsupported. ``negotiated_call`` adds the negotiation headers to a
``build_call`` dict.

Executor commands
-----------------

``AC_decode_body`` decodes a base64 ``body_base64`` per its ``headers`` and
returns ``{body_base64, text}``; ``AC_parse_quality_values`` returns
``{values}``. Both are exposed as MCP tools (``ac_decode_body`` /
``ac_parse_quality_values``) and as Script Builder commands under **Data**.
1 change: 1 addition & 0 deletions docs/source/Eng/eng_index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ Comprehensive guides for all AutoControl features.
doc/new_features/v87_features_doc
doc/new_features/v88_features_doc
doc/new_features/v89_features_doc
doc/new_features/v90_features_doc
doc/ocr_backends/ocr_backends_doc
doc/observability/observability_doc
doc/operations_layer/operations_layer_doc
Expand Down
43 changes: 43 additions & 0 deletions docs/source/Zh/doc/new_features/v90_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
HTTP 內容協商與解壓縮
===================

``urllib`` / ``http_request`` 從不設定 ``Accept-Encoding``,也從不解碼 ``Content-Encoding`` 回應,因此
會壓縮的伺服器回傳的內文是原始的;也沒有 quality-value 解析器。本功能加入 ``Accept`` / ``Accept-Encoding``
建構器、q-value 解析器,以及 gzip / deflate 解碼。

純標準函式庫(``gzip`` / ``zlib``);不匯入 ``PySide6``。刻意排除 Brotli(非標準函式庫)。每個函式皆為
純函式,因此在 CI 中完全具決定性。

無頭 API
--------

.. code-block:: python

from je_auto_control import (
build_accept, build_accept_encoding, negotiated_call,
parse_quality_values, decode_body, build_call,
)

call = negotiated_call(
build_call(url),
accept=build_accept([("application/json", 1.0), ("text/html", 0.8)]),
accept_encoding=build_accept_encoding(),
)
# ... 執行呼叫,然後:
body = decode_body(response["headers"], raw_bytes)

ranked = parse_quality_values("text/html;q=0.8, application/json")
# [("application/json", 1.0), ("text/html", 0.8)]

``build_accept`` 把媒體型別或 ``(type, q)`` 配對轉成 ``Accept`` 標頭;``build_accept_encoding`` 預設
``gzip, deflate``。``parse_quality_values`` 把 ``Accept`` / ``Accept-Encoding`` 標頭解析成依品質排序的
``(token, q)`` 配對(同分維持順序)。``decode_body`` 依 ``Content-Encoding``(``gzip`` / ``deflate``,含
raw deflate,以及 ``identity``)解壓回應內文,不支援者拋出 ``ValueError``。``negotiated_call`` 把協商標頭
加到 ``build_call`` dict。

執行器命令
----------

``AC_decode_body`` 依 ``headers`` 解碼 base64 的 ``body_base64`` 回傳 ``{body_base64, text}``;
``AC_parse_quality_values`` 回傳 ``{values}``。兩者皆以 MCP 工具(``ac_decode_body`` /
``ac_parse_quality_values``)以及 Script Builder 中 **Data** 分類下的命令提供。
1 change: 1 addition & 0 deletions docs/source/Zh/zh_index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ AutoControl 所有功能的完整使用指南。
doc/new_features/v87_features_doc
doc/new_features/v88_features_doc
doc/new_features/v89_features_doc
doc/new_features/v90_features_doc
doc/ocr_backends/ocr_backends_doc
doc/observability/observability_doc
doc/operations_layer/operations_layer_doc
Expand Down
7 changes: 7 additions & 0 deletions je_auto_control/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,11 @@
from je_auto_control.utils.multipart import (
MultipartFile, build_multipart, new_boundary, parse_multipart,
)
# HTTP content negotiation + gzip/deflate response decoding
from je_auto_control.utils.http_content import (
build_accept, build_accept_encoding, decode_body, negotiated_call,
parse_quality_values,
)
# W3C Trace Context propagation (traceparent / tracestate)
from je_auto_control.utils.trace_context import (
SpanContext, TraceContextError, child_context, extract_context,
Expand Down Expand Up @@ -948,6 +953,8 @@ def start_autocontrol_gui(*args, **kwargs):
"SSEEvent", "SSEParser", "parse_event_stream",
"Link", "links_by_rel", "next_url", "paginate", "parse_link_header",
"MultipartFile", "build_multipart", "new_boundary", "parse_multipart",
"build_accept", "build_accept_encoding", "decode_body", "negotiated_call",
"parse_quality_values",
"SpanContext", "TraceContextError", "child_context", "extract_context",
"format_traceparent", "format_tracestate", "inject_context",
"new_root_context", "new_span_id", "new_trace_id", "parse_traceparent",
Expand Down
18 changes: 18 additions & 0 deletions je_auto_control/gui/script_builder/command_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -1766,6 +1766,24 @@ def _add_resilience_specs(specs: List[CommandSpec]) -> None:
),
description="Parse a base64 multipart body into fields and files.",
))
specs.append(CommandSpec(
"AC_decode_body", "Data", "HTTP Content: Decode Body",
fields=(
FieldSpec("headers", FieldType.STRING,
placeholder='{"Content-Encoding": "gzip"}'),
FieldSpec("body_base64", FieldType.STRING,
placeholder="<base64 compressed body>"),
),
description="Decode a gzip/deflate response body by Content-Encoding.",
))
specs.append(CommandSpec(
"AC_parse_quality_values", "Data", "HTTP Content: Quality Values",
fields=(
FieldSpec("header", FieldType.STRING,
placeholder="text/html;q=0.8, application/json"),
),
description="Parse an Accept/Accept-Encoding header by q-value.",
))
specs.append(CommandSpec(
"AC_resolve_config", "Data", "Layered Config: Resolve",
fields=(
Expand Down
24 changes: 24 additions & 0 deletions je_auto_control/utils/executor/action_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3030,6 +3030,28 @@ def _redact_secret_text(text: str, mask: str = "***") -> Dict[str, Any]:
return {"text": redact_secret_text(text, mask=mask)}


def _decode_body(headers: Any, body_base64: str) -> Dict[str, Any]:
"""Adapter: decode a Content-Encoding (gzip/deflate) base64 body."""
import base64
import json
from je_auto_control.utils.http_content import decode_body
if isinstance(headers, str):
headers = json.loads(headers)
decoded = decode_body(headers, base64.b64decode(body_base64))
try:
text: Any = decoded.decode("utf-8")
except UnicodeDecodeError:
text = None
return {"body_base64": base64.b64encode(decoded).decode("ascii"),
"text": text}


def _parse_quality_values(header: str) -> Dict[str, Any]:
"""Adapter: parse a quality-value header into {values}."""
from je_auto_control.utils.http_content import parse_quality_values
return {"values": [list(item) for item in parse_quality_values(header)]}


def _build_multipart(fields: Any = None, files: Any = None,
boundary: Any = None) -> Dict[str, Any]:
"""Adapter: build a multipart/form-data body (base64-encoded)."""
Expand Down Expand Up @@ -4330,6 +4352,8 @@ def __init__(self):
"AC_next_url": _next_url,
"AC_build_multipart": _build_multipart,
"AC_parse_multipart": _parse_multipart,
"AC_decode_body": _decode_body,
"AC_parse_quality_values": _parse_quality_values,
"AC_profile_rows": _profile_rows,
"AC_infer_schema": _infer_schema,
"AC_parse_problem": _parse_problem,
Expand Down
10 changes: 10 additions & 0 deletions je_auto_control/utils/http_content/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""HTTP content negotiation and response decompression for AutoControl."""
from je_auto_control.utils.http_content.http_content import (
build_accept, build_accept_encoding, decode_body, negotiated_call,
parse_quality_values,
)

__all__ = [
"build_accept", "build_accept_encoding", "decode_body", "negotiated_call",
"parse_quality_values",
]
93 changes: 93 additions & 0 deletions je_auto_control/utils/http_content/http_content.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"""HTTP content negotiation and transparent response decompression.

``urllib`` / ``http_request`` never sets ``Accept-Encoding`` and never decodes a
``Content-Encoding`` response, so a body from a server that compresses arrives
raw; there was also no quality-value parser. This adds ``Accept`` /
``Accept-Encoding`` builders, a q-value parser, and gzip / deflate decoding.

Pure standard library (``gzip`` / ``zlib``); imports no ``PySide6``. Brotli is
deliberately excluded (not stdlib). Every function is pure, so it is fully
deterministic in CI.
"""
import gzip
import zlib
from typing import Any, Dict, List, Mapping, Optional, Sequence, Tuple, Union

AcceptEntry = Union[str, Tuple[str, float]]
_DEFAULT_ENCODINGS = ("gzip", "deflate")


def build_accept(types: Sequence[AcceptEntry]) -> str:
"""Build an ``Accept`` header from media types or ``(type, q)`` pairs."""
parts: List[str] = []
for entry in types:
if isinstance(entry, (tuple, list)):
media, quality = entry[0], float(entry[1])
parts.append(media if quality >= 1.0 else f"{media};q={quality}")
else:
parts.append(entry)
return ", ".join(parts)


def build_accept_encoding(encodings: Optional[Sequence[str]] = None) -> str:
"""Build an ``Accept-Encoding`` header (default ``gzip, deflate``)."""
return ", ".join(encodings if encodings is not None else _DEFAULT_ENCODINGS)


def _quality_of(params: str) -> float:
for param in params.split(";"):
key, sep, value = param.partition("=")
if sep and key.strip() == "q":
try:
return float(value.strip())
except ValueError:
return 0.0
return 1.0


def parse_quality_values(header: Optional[str]) -> List[Tuple[str, float]]:
"""Parse a quality-value header into ``(token, q)`` sorted by ``q`` desc."""
items: List[Tuple[str, float]] = []
for part in (header or "").split(","):
cleaned = part.strip()
if not cleaned:
continue
token, _, params = cleaned.partition(";")
items.append((token.strip(), _quality_of(params)))
items.sort(key=lambda entry: -entry[1]) # stable: ties keep order
return items


def _content_encoding(headers: Optional[Mapping[str, Any]]) -> str:
for key, value in (headers or {}).items():
if str(key).lower() == "content-encoding":
return str(value).strip().lower()
return ""


def decode_body(headers: Optional[Mapping[str, Any]], raw: bytes) -> bytes:
"""Decode ``raw`` per the response ``Content-Encoding`` header."""
encoding = _content_encoding(headers)
if encoding in ("", "identity"):
return raw
if encoding == "gzip":
return gzip.decompress(raw)
if encoding == "deflate":
try:
return zlib.decompress(raw)
except zlib.error:
return zlib.decompress(raw, -zlib.MAX_WBITS) # raw deflate stream
raise ValueError(f"unsupported content-encoding: {encoding!r}")


def negotiated_call(call: Mapping[str, Any], *, accept: Optional[str] = None,
accept_encoding: str = "gzip, deflate") -> Dict[str, Any]:
"""Return a copy of a ``build_call`` dict with negotiation headers set."""
headers = dict(call.get("headers") or {})
if accept:
headers["Accept"] = accept
if accept_encoding:
headers["Accept-Encoding"] = accept_encoding
out = dict(call)
out["headers"] = headers
return out
Loading
Loading