Skip to content

Conversation

@illusiona
Copy link
Contributor

@illusiona illusiona commented Dec 9, 2025

Bug fix
Encode b1 from raw bytes (bytearray) instead of string/text to avoid byte corruption during encoding.

This restores parity with browser/VM output: the base64 result must start with I38r….

Prevents downstream requests from being rejected due to an invalid b1 value.

83436c665f13879d35d2120ad4755762 image

Sourcery 总结

基于完整的 cookies 和类浏览器指纹添加 x-s-common 签名流程,并将其与现有的 x-s 支持一起接入到 header 签名中。

新功能:

  • 通过新的 XsCommonSigner 以及客户端方法 sign_xs_common 和其别名 sign_xsc 暴露 x-s-common 签名能力,支持传入完整的 cookie 字典或 cookie 字符串。
  • 引入浏览器指纹生成器及相关静态数据,用于合成真实感指纹并推导 x-s-common 中使用的 b1 值。
  • 添加一个 CRC32 辅助工具,实现与 JavaScript 兼容的变体,以满足新签名方案的需求。

错误修复:

  • 修正自定义 Base64 编码逻辑,使其在原始字节上而非 UTF-8 字符串上运行,避免 b1 值损坏,并恢复与浏览器/VM 输出的一致性。

增强改进:

  • 扩展 Base64Encoder,加入缓存的转换表并支持更广泛的输入类型,在自定义字母表和 x3 编解码场景中提升性能与灵活性。
  • 扩展 CryptoConfig,加入更多加密常量,包括与 b1 相关的密钥、x-s-common 模板以及更新后的 X3 前缀。
  • 泛化客户端 header 签名 API 以接受完整 cookies(字典或字符串),在内部解析后同时推导 x-sx-s-common headers,并对 a1 等必需字段进行校验。
  • 引入 cookie 校验与解析辅助工具,用于在不同签名路径中统一规范化 cookie 输入。

构建:

  • 在项目配置中声明对 curl-cffipycryptodome 的新运行时依赖。

文档:

  • 更新 README 中的使用示例和参数说明,描述基于 cookie 的 header 签名、x-s-common 生成,以及 sign_headers*sign_xs*sign_xsc API 的职责分离。

测试:

  • 拓展加密和 header 签名测试,覆盖基于 cookie 的流程和 x-s-common 输出;新增针对 cookie 解析边界情况的测试,并为新的 CRC32 辅助工具添加单元测试。
Original summary in English

Summary by Sourcery

Add x-s-common signing pipeline based on full cookies and browser-like fingerprints, and wire it into header signing alongside existing x-s support.

New Features:

  • Expose x-s-common signing via new XsCommonSigner plus client methods sign_xs_common and its alias sign_xsc, accepting full cookie dictionaries or cookie strings.
  • Introduce a browser fingerprint generator and associated static data to synthesize realistic fingerprints and derive the b1 value used in x-s-common.
  • Add a CRC32 helper implementing the JavaScript-compatible variant required by the new signing scheme.

Bug Fixes:

  • Correct custom Base64 encoding to operate on raw bytes rather than UTF-8 strings, preventing corruption of b1 values and restoring parity with browser/VM output.

Enhancements:

  • Extend Base64Encoder with cached translation tables and broader input-type handling, improving performance and flexibility for custom alphabets and x3 encoding/decoding.
  • Expand CryptoConfig with additional cryptographic constants, including b1-related keys, x-s-common templates, and updated X3 prefix.
  • Generalize client header-signing APIs to accept complete cookies (dict or string), internally parse them, and derive both x-s and x-s-common headers with validation for required fields like a1.
  • Introduce cookie validation and parsing helpers to normalize cookie inputs across signing paths.

Build:

  • Declare new runtime dependencies on curl-cffi and pycryptodome in the project configuration.

Documentation:

  • Update README usage examples and parameter documentation to describe cookie-based header signing, x-s-common generation, and the separation of sign_headers*, sign_xs*, and sign_xsc APIs.

Tests:

  • Broaden crypto and header-signing tests to cover cookie-based flows and x-s-common output, add targeted cookie parsing edge-case coverage, and introduce unit tests for the new CRC32 helper.

新功能:

  • 添加 x-s-common 签名生成流水线,包括 CRC32 辅助函数、指纹生成器,以及根据 cookies 和公共 User-Agent 构建 x-s-common 负载的 XsCommonSigner
  • 暴露新的客户端 API sign_xs_common 及其别名 sign_xsc,用于从 cookie 字典或 cookie 字符串生成 x-s-common 值,并将其接入 sign_headers*,后者现在接受完整 cookies 而不仅仅是 a1
  • 引入浏览器指纹数据和辅助工具,用于合成逼真的客户端指纹以用于签名生成。

错误修复:

  • 修复自定义 Base64 编码,使其在原始字节而非 UTF-8 字符串上运行,以避免数据损坏并匹配浏览器/VM 的输出。

增强:

  • 扩展 Base64Encoder,加入缓存翻译表以及更广泛的输入类型支持,在自定义字母表场景下提升性能和灵活性。
  • 优化 CryptoConfig,补充更多与密码学和签名相关的常量,包括更新的 X3 前缀以及 b1/x-s-common 配置。
  • 添加 cookie 解析与校验辅助工具,使请求头签名和 x-s-common 生成可以同时消费字典或字符串格式的 cookies。

构建:

  • 在项目配置中声明对 curl-cffipycryptodome 的新运行时依赖。

文档:

  • 更新 README 中的使用示例和参数文档,以反映基于 cookie 的请求头签名、x-s-common 支持,以及 sign_headers*sign_xs* API 之间的职责划分。

测试:

  • 扩展加密与请求头签名相关测试,覆盖基于 cookie 的流程和 x-s-common 输出,并新增针对 CRC32 行为和 cookie 解析边界情况的专门测试。
Original summary in English

Sourcery 总结

基于完整的 cookies 和类浏览器指纹添加 x-s-common 签名流程,并将其与现有的 x-s 支持一起接入到 header 签名中。

新功能:

  • 通过新的 XsCommonSigner 以及客户端方法 sign_xs_common 和其别名 sign_xsc 暴露 x-s-common 签名能力,支持传入完整的 cookie 字典或 cookie 字符串。
  • 引入浏览器指纹生成器及相关静态数据,用于合成真实感指纹并推导 x-s-common 中使用的 b1 值。
  • 添加一个 CRC32 辅助工具,实现与 JavaScript 兼容的变体,以满足新签名方案的需求。

错误修复:

  • 修正自定义 Base64 编码逻辑,使其在原始字节上而非 UTF-8 字符串上运行,避免 b1 值损坏,并恢复与浏览器/VM 输出的一致性。

增强改进:

  • 扩展 Base64Encoder,加入缓存的转换表并支持更广泛的输入类型,在自定义字母表和 x3 编解码场景中提升性能与灵活性。
  • 扩展 CryptoConfig,加入更多加密常量,包括与 b1 相关的密钥、x-s-common 模板以及更新后的 X3 前缀。
  • 泛化客户端 header 签名 API 以接受完整 cookies(字典或字符串),在内部解析后同时推导 x-sx-s-common headers,并对 a1 等必需字段进行校验。
  • 引入 cookie 校验与解析辅助工具,用于在不同签名路径中统一规范化 cookie 输入。

构建:

  • 在项目配置中声明对 curl-cffipycryptodome 的新运行时依赖。

文档:

  • 更新 README 中的使用示例和参数说明,描述基于 cookie 的 header 签名、x-s-common 生成,以及 sign_headers*sign_xs*sign_xsc API 的职责分离。

测试:

  • 拓展加密和 header 签名测试,覆盖基于 cookie 的流程和 x-s-common 输出;新增针对 cookie 解析边界情况的测试,并为新的 CRC32 辅助工具添加单元测试。
Original summary in English

Summary by Sourcery

Add x-s-common signing pipeline based on full cookies and browser-like fingerprints, and wire it into header signing alongside existing x-s support.

New Features:

  • Expose x-s-common signing via new XsCommonSigner plus client methods sign_xs_common and its alias sign_xsc, accepting full cookie dictionaries or cookie strings.
  • Introduce a browser fingerprint generator and associated static data to synthesize realistic fingerprints and derive the b1 value used in x-s-common.
  • Add a CRC32 helper implementing the JavaScript-compatible variant required by the new signing scheme.

Bug Fixes:

  • Correct custom Base64 encoding to operate on raw bytes rather than UTF-8 strings, preventing corruption of b1 values and restoring parity with browser/VM output.

Enhancements:

  • Extend Base64Encoder with cached translation tables and broader input-type handling, improving performance and flexibility for custom alphabets and x3 encoding/decoding.
  • Expand CryptoConfig with additional cryptographic constants, including b1-related keys, x-s-common templates, and updated X3 prefix.
  • Generalize client header-signing APIs to accept complete cookies (dict or string), internally parse them, and derive both x-s and x-s-common headers with validation for required fields like a1.
  • Introduce cookie validation and parsing helpers to normalize cookie inputs across signing paths.

Build:

  • Declare new runtime dependencies on curl-cffi and pycryptodome in the project configuration.

Documentation:

  • Update README usage examples and parameter documentation to describe cookie-based header signing, x-s-common generation, and the separation of sign_headers*, sign_xs*, and sign_xsc APIs.

Tests:

  • Broaden crypto and header-signing tests to cover cookie-based flows and x-s-common output, add targeted cookie parsing edge-case coverage, and introduce unit tests for the new CRC32 helper.

Cloxl and others added 19 commits December 5, 2025 10:37
…ence methods

- fix: distinguish params/payload parameters in sign_headers method
  - params: for GET requests only
  - payload: for POST requests only
- feat: add sign_headers_get convenience method for GET requests
- feat: add sign_headers_post convenience method for POST requests
- docs: update docstrings with clear parameter usage examples
- fix: use params for GET and payload for POST in sign_headers tests
- test: add test_sign_headers_get for GET convenience method
- test: add test_sign_headers_post for POST convenience method
- test: verify timestamp parameter support in convenience methods
- feat: promote sign_headers_get/post as recommended approach
- feat: move build_url and build_json_body to recommended section
- refactor: move sign_headers unified method to traditional methods section
- refactor: collapse traditional single-field generation methods into details
- improve: clearer documentation structure for better user experience
- validate GET requests only use params, not payload
- validate POST requests only use payload, not params
- raise ValueError for unsupported HTTP methods (only GET/POST allowed)
- add comprehensive test coverage for parameter validation
- improve error messages for better developer experience
…ad of the official standard base64 method.

Add some initialization parameters to the configuration file to generate fingerprints.

Perhaps using customer_encoder to replace the encoder file name is better than directly using the encoder name, since encoders are a native Python method.
…ad of the official standard base64 method.

Add some initialization parameters to the configuration file to generate fingerprints.

ADD CRC32_encrypt for gen xs-common

Perhaps using customer_encoder to replace the encoder file name is better than directly using the encoder name, since encoders are a native Python method.

Use case:
cookie = "you cookie dict or string"
xs_common = client.sign_xsc(cookie)
…byte corruption during encoding.

This restores parity with browser/VM output: the base64 result must start with I38r….

Prevents downstream requests from being rejected due to an invalid b1 value.
@sourcery-ai
Copy link

sourcery-ai bot commented Dec 9, 2025

Reviewer's Guide

基于合成的浏览器指纹新增 x-s-common 签名流水线,重构 header 签名 API 以接受完整 cookies(而非仅 a1),引入 CRC32 和指纹生成功能工具以及相关配置常量,并修复/定制 Base64 编码逻辑,使其在原始字节和缓存转换表上工作,以贴近浏览器行为并提升性能。

带 x-s-common 生成流程的 sign_headers_get 时序图

sequenceDiagram
    actor Developer
    participant Xhshow
    participant RequestSignatureValidator as Validator
    participant XsCommonSigner as CommonSigner
    participant FingerprintGenerator as FpGen
    participant CRC32
    participant Base64Encoder as Encoder

    Developer->>Xhshow: sign_headers_get(uri, cookies, xsec_appid, params, timestamp?)
    activate Xhshow
    Xhshow->>Xhshow: sign_headers("GET", uri, cookies, xsec_appid, params, None, timestamp)

    Xhshow->>Xhshow: _parse_cookies(cookies)
    note right of Xhshow: Accept dict or cookie string
    Xhshow-->>Xhshow: cookie_dict

    Xhshow->>Xhshow: a1_value = cookie_dict["a1"]
    Xhshow->>XsCommonSigner: sign(cookie_dict)
    activate CommonSigner

    CommonSigner->>FpGen: generate(cookies=cookie_dict, user_agent=PUBLIC_USERAGENT)
    activate FpGen
    FpGen-->>CommonSigner: fingerprint dict

    CommonSigner->>FpGen: generate_b1(fingerprint)
    FpGen->>Encoder: encode(bytearray_from_encrypted_fp)
    activate Encoder
    Encoder-->>FpGen: b1 (custom base64)
    deactivate Encoder
    FpGen-->>CommonSigner: b1
    deactivate FpGen

    CommonSigner->>CRC32: crc32_js_int(b1)
    activate CRC32
    CRC32-->>CommonSigner: x9_crc32
    deactivate CRC32

    CommonSigner->>Encoder: encode(json.dumps(sign_struct))
    activate Encoder
    Encoder-->>CommonSigner: xs_common
    deactivate Encoder

    CommonSigner-->>Xhshow: xs_common
    deactivate CommonSigner

    Xhshow->>Xhshow: x_s = sign_xs("GET", uri, a1_value, xsec_appid, request_data, timestamp)
    Xhshow->>Xhshow: x_t = get_x_t(timestamp)
    Xhshow->>Xhshow: x_b3_traceid = get_b3_trace_id()
    Xhshow->>Xhshow: x_xray_traceid = get_xray_trace_id()

    Xhshow-->>Developer: headers {"x-s-common", "x-t", "x-b3-traceid", "x-xray-traceid"}
    deactivate Xhshow
Loading

签名、指纹与编码组件的更新类图

classDiagram
    class Xhshow {
        -config CryptoConfig
        +sign_xs_common(cookie_dict: dict~str,Any~ | str) str
        +sign_xsc(cookie_dict: dict~str,Any~ | str) str
        +sign_headers(method: str, uri: str, cookies: dict~str,Any~ | str, xsec_appid: str, params: dict~str,Any~?, payload: dict~str,Any~?, timestamp: float?) dict~str,str~
        +sign_headers_get(uri: str, cookies: dict~str,Any~ | str, xsec_appid: str, params: dict~str,Any~?, timestamp: float?) dict~str,str~
        +sign_headers_post(uri: str, cookies: dict~str,Any~ | str, xsec_appid: str, payload: dict~str,Any~?, timestamp: float?) dict~str,str~
        +sign_xs(method: str, uri: str, a1_value: str, xsec_appid: str, payload: dict~str,Any~?, timestamp: float?) str
        +get_x_t(timestamp: float?) int
        +get_b3_trace_id() str
        +get_xray_trace_id(timestamp: int) str
        -_parse_cookies(cookies: dict~str,Any~ | str) dict~str,Any~
    }

    class XsCommonSigner {
        -config CryptoConfig
        -_fp_generator FingerprintGenerator
        -_encoder Base64Encoder
        +XsCommonSigner(config: CryptoConfig?)
        +sign(cookie_dict: dict~str,Any~) str
    }

    class FingerprintGenerator {
        -config CryptoConfig
        -_b1_key bytes
        -_encoder Base64Encoder
        +FingerprintGenerator(config: CryptoConfig)
        +generate_b1(fp: dict~str,Any~) str
        +generate(cookies: dict~str,Any~, user_agent: str) dict~str,Any~
        +update(fp: dict~str,Any~, cookies: dict~str,Any~, url: str) void
    }

    class CRC32 {
        <<static>> MASK32 int
        <<static>> POLY int
        <<static>> _TABLE list~int~?
        <<static>> _ensure_table() void
        <<static>> _crc32_core(data: DataLike, string_mode: str) int
        <<static>> _to_signed32(u: int) int
        <<static>> crc32_js_int(data: DataLike, string_mode: str, signed: bool) int
    }

    class Base64Encoder {
        -config CryptoConfig
        -_custom_encode_table dict
        -_custom_decode_table dict
        -_x3_encode_table dict
        -_x3_decode_table dict
        +Base64Encoder(config: CryptoConfig)
        +encode(data_to_encode: bytes | str | Iterable~int~) str
        +decode(encoded_string: str) str
        +decode_x3(encoded_string: str) bytes
        +encode_x3(input_bytes: bytes | bytearray) str
    }

    class CryptoConfig {
        +DES_KEY str
        +GID_URL str
        +DATA_PALTFORM str
        +DATA_SVN str
        +DATA_SDK_VERSION str
        +DATA_webBuild str
        +MAX_32BIT int
        +MAX_SIGNED_32BIT int
        +STANDARD_BASE64_ALPHABET str
        +CUSTOM_BASE64_ALPHABET str
        +X3_BASE64_ALPHABET str
        +X3_PREFIX str
        +XYS_PREFIX str
        +XRAY_TRACE_ID_PREFIX str
        +XRAY_TRACE_ID_PART1_LENGTH int
        +XRAY_TRACE_ID_PART2_LENGTH int
        +B3_TRACE_ID_LENGTH int
        +B1_SECRET_KEY str
        +SIGNATURE_XSCOMMON_TEMPLATE dict~str,Any~
        +PUBLIC_USERAGENT str
        +with_overrides(**kwargs: Any) CryptoConfig
    }

    class RequestSignatureValidator {
        +validate_payload(payload: Any) dict~str,Any~?
        +validate_cookie(cookie: Any) dict~str,Any~ | str
    }

    class validate_xs_common_params {
        <<function>>
    }

    Xhshow --> CryptoConfig : uses
    Xhshow --> XsCommonSigner : creates
    Xhshow --> RequestSignatureValidator : via validate_xs_common_params
    XsCommonSigner --> FingerprintGenerator : composes
    XsCommonSigner --> Base64Encoder : composes
    XsCommonSigner --> CryptoConfig : uses
    XsCommonSigner --> CRC32 : calls
    FingerprintGenerator --> CryptoConfig : uses
    FingerprintGenerator --> Base64Encoder : uses
    Base64Encoder --> CryptoConfig : uses
    CRC32 --> CryptoConfig : uses POLY constant
    validate_xs_common_params --> RequestSignatureValidator : uses
Loading

File-Level Changes

Change Details Files
Extend client signing APIs to generate x-s-common from full cookies and to expose dedicated x-s-common helpers.
  • 在 Xhshow 上新增 sign_xs_common 和 sign_xsc 方法,并通过新的 validate_xs_common_params 装饰器进行参数校验。
  • 引入私有的 _parse_cookies 辅助函数,使用 http.cookies.SimpleCookie 统一规范化 dict 或 cookie 字符串输入。
  • 重构 sign_headers/sign_headers_get/sign_headers_post,使其接收 cookies 而非 a1_value,仅解析一次 cookies,强制要求存在 a1,并在返回的 headers 中同时输出 x-s-common 和 x-s。
  • 更新 README 示例和参数文档,展示基于 cookie 的用法、x-s-common 生成方式,并澄清 sign_headers* 与 sign_xs* 与 sign_xsc API 各自的职责。
src/xhshow/client.py
src/xhshow/utils/validators.py
README.md
Implement x-s-common signing based on browser-like fingerprint synthesis and CRC32.
  • 为 b1/x-s-common 增加 CryptoConfig 字段(B1_SECRET_KEY、SIGNATURE_XSCOMMON_TEMPLATE、PUBLIC_USERAGENT 以及其他加密相关常量)。
  • 新增 CRC32 辅助类,实现兼容 JS 的 CRC32 变体,并支持灵活的输入类型。
  • 新增 FingerprintGenerator 和 fingerprint_helpers 工具以及 fingerprint_data 常量,用于合成逼真的浏览器指纹,并计算 b1(对指纹子集进行 RC4 加密、URL 编码、自定义 Base64 编码)。
  • 实现 XsCommonSigner:基于配置模板、cookies、b1 和 CRC32(b1) 构建 x-s-common 结构,然后通过 Base64Encoder 对 JSON 进行编码。
  • 通过新的包级 init 模块对数据和生成器进行导出。
src/xhshow/config/config.py
src/xhshow/core/crc32_encrypt.py
src/xhshow/generators/fingerprint.py
src/xhshow/generators/fingerprint_helpers.py
src/xhshow/core/common_sign.py
src/xhshow/data/fingerprint_data.py
src/xhshow/data/__init__.py
src/xhshow/generators/__init__.py
Fix and enhance custom Base64 encoding/decoding to work on raw bytes, support multiple alphabets, and cache translation tables.
  • 修改 Base64Encoder.encode,使其接受 bytes/str/iterable[int],对 bytes 直接编码而不是重新编码字符串,并继续使用项目的自定义字母表。
  • 在 Base64Encoder.init 中预计算并复用自定义和 X3 字母表的转换表,避免每次调用都创建 maketrans。
  • 重构 decode/decode_x3/encode_x3,改为使用缓存的转换表,以提升性能和代码清晰度。
src/xhshow/utils/encoder.py
Add tests for cookie parsing, x-s-common signing, CRC32, and updated header-signing behavior.
  • 扩展现有的加密/头部签名测试,改为使用 cookies 而非 a1_value,断言 x-s-common 是否存在,并覆盖参数校验路径。
  • 新增专门的 CRC32 测试,覆盖基础数值、有符号与无符号输出、不同 string_mode 以及非字符串输入。
  • 新增 cookie_parsing 测试模块,覆盖 _parse_cookies 行为(dict vs 字符串及边界情况),以及在 dict 和字符串 cookies 下的 sign_xs_common/sign_xsc/sign_headers* 端到端流程。
tests/test_crypto.py
tests/test_cookie_parsing.py
Update build configuration to include new runtime dependencies required by the crypto/fingerprint pipeline.
  • 将 curl-cffi 和 pycryptodome 声明为项目核心依赖。
  • 重新生成 uv.lock,以反映更新后的依赖关系。
pyproject.toml
uv.lock

Possibly linked issues

  • #: PR 引入自定义 Base64、x-s-common 签名和 b1 生成,正是新版小红书 xs 算法实现

Tips and commands

Interacting with Sourcery

  • 触发新一轮 Review: 在 Pull Request 中评论 @sourcery-ai review
  • 继续讨论: 直接回复 Sourcery 的 Review 评论即可继续对话。
  • 从 Review 评论生成 GitHub Issue: 在某条 Review 评论下回复,要求 Sourcery 从该评论生成 issue。也可以直接回复 @sourcery-ai issue 来生成对应 issue。
  • 生成 Pull Request 标题: 在 PR 标题任意位置写上 @sourcery-ai,即可随时生成标题。你也可以在 PR 中评论 @sourcery-ai title 来(重新)生成标题。
  • 生成 Pull Request 摘要: 在 PR Body 的任意位置写上 @sourcery-ai summary,即可在该位置生成摘要。也可以在 PR 中评论 @sourcery-ai summary 来(重新)生成摘要。
  • 生成 Reviewer's Guide: 在 Pull Request 中评论 @sourcery-ai guide,即可(重新)生成本审查指南。
  • 一次性解决所有 Sourcery 评论: 在 Pull Request 中评论 @sourcery-ai resolve,即可将所有已有的 Sourcery 评论标记为已解决。适合在你已经处理完所有反馈且不希望再看到旧评论时使用。
  • 批量忽略所有 Sourcery Review: 在 Pull Request 中评论 @sourcery-ai dismiss,即可忽略所有现有的 Sourcery Review。尤其适用于你希望从头开始一轮新的 Review —— 记得再评论 @sourcery-ai review 来触发新一轮 Review!

Customizing Your Experience

打开你的 dashboard 以:

  • 启用或关闭诸如 Sourcery 自动生成 PR 摘要、Reviewer's Guide 等 Review 功能。
  • 修改 Review 语言。
  • 添加、删除或编辑自定义 Review 说明。
  • 调整其它 Review 设置。

Getting Help

Original review guide in English

Reviewer's Guide

Adds an x-s-common signing pipeline based on synthesized browser fingerprints, refactors header-signing APIs to accept full cookies instead of only a1, introduces CRC32 and fingerprint-generation utilities plus config constants, and fixes/customizes Base64 encoding to operate on raw bytes and cached translation tables for browser parity and performance.

Sequence diagram for sign_headers_get with x-s-common generation

sequenceDiagram
    actor Developer
    participant Xhshow
    participant RequestSignatureValidator as Validator
    participant XsCommonSigner as CommonSigner
    participant FingerprintGenerator as FpGen
    participant CRC32
    participant Base64Encoder as Encoder

    Developer->>Xhshow: sign_headers_get(uri, cookies, xsec_appid, params, timestamp?)
    activate Xhshow
    Xhshow->>Xhshow: sign_headers("GET", uri, cookies, xsec_appid, params, None, timestamp)

    Xhshow->>Xhshow: _parse_cookies(cookies)
    note right of Xhshow: Accept dict or cookie string
    Xhshow-->>Xhshow: cookie_dict

    Xhshow->>Xhshow: a1_value = cookie_dict["a1"]
    Xhshow->>XsCommonSigner: sign(cookie_dict)
    activate CommonSigner

    CommonSigner->>FpGen: generate(cookies=cookie_dict, user_agent=PUBLIC_USERAGENT)
    activate FpGen
    FpGen-->>CommonSigner: fingerprint dict

    CommonSigner->>FpGen: generate_b1(fingerprint)
    FpGen->>Encoder: encode(bytearray_from_encrypted_fp)
    activate Encoder
    Encoder-->>FpGen: b1 (custom base64)
    deactivate Encoder
    FpGen-->>CommonSigner: b1
    deactivate FpGen

    CommonSigner->>CRC32: crc32_js_int(b1)
    activate CRC32
    CRC32-->>CommonSigner: x9_crc32
    deactivate CRC32

    CommonSigner->>Encoder: encode(json.dumps(sign_struct))
    activate Encoder
    Encoder-->>CommonSigner: xs_common
    deactivate Encoder

    CommonSigner-->>Xhshow: xs_common
    deactivate CommonSigner

    Xhshow->>Xhshow: x_s = sign_xs("GET", uri, a1_value, xsec_appid, request_data, timestamp)
    Xhshow->>Xhshow: x_t = get_x_t(timestamp)
    Xhshow->>Xhshow: x_b3_traceid = get_b3_trace_id()
    Xhshow->>Xhshow: x_xray_traceid = get_xray_trace_id()

    Xhshow-->>Developer: headers {"x-s-common", "x-t", "x-b3-traceid", "x-xray-traceid"}
    deactivate Xhshow
Loading

Updated class diagram for signing, fingerprint, and encoding components

classDiagram
    class Xhshow {
        -config CryptoConfig
        +sign_xs_common(cookie_dict: dict~str,Any~ | str) str
        +sign_xsc(cookie_dict: dict~str,Any~ | str) str
        +sign_headers(method: str, uri: str, cookies: dict~str,Any~ | str, xsec_appid: str, params: dict~str,Any~?, payload: dict~str,Any~?, timestamp: float?) dict~str,str~
        +sign_headers_get(uri: str, cookies: dict~str,Any~ | str, xsec_appid: str, params: dict~str,Any~?, timestamp: float?) dict~str,str~
        +sign_headers_post(uri: str, cookies: dict~str,Any~ | str, xsec_appid: str, payload: dict~str,Any~?, timestamp: float?) dict~str,str~
        +sign_xs(method: str, uri: str, a1_value: str, xsec_appid: str, payload: dict~str,Any~?, timestamp: float?) str
        +get_x_t(timestamp: float?) int
        +get_b3_trace_id() str
        +get_xray_trace_id(timestamp: int) str
        -_parse_cookies(cookies: dict~str,Any~ | str) dict~str,Any~
    }

    class XsCommonSigner {
        -config CryptoConfig
        -_fp_generator FingerprintGenerator
        -_encoder Base64Encoder
        +XsCommonSigner(config: CryptoConfig?)
        +sign(cookie_dict: dict~str,Any~) str
    }

    class FingerprintGenerator {
        -config CryptoConfig
        -_b1_key bytes
        -_encoder Base64Encoder
        +FingerprintGenerator(config: CryptoConfig)
        +generate_b1(fp: dict~str,Any~) str
        +generate(cookies: dict~str,Any~, user_agent: str) dict~str,Any~
        +update(fp: dict~str,Any~, cookies: dict~str,Any~, url: str) void
    }

    class CRC32 {
        <<static>> MASK32 int
        <<static>> POLY int
        <<static>> _TABLE list~int~?
        <<static>> _ensure_table() void
        <<static>> _crc32_core(data: DataLike, string_mode: str) int
        <<static>> _to_signed32(u: int) int
        <<static>> crc32_js_int(data: DataLike, string_mode: str, signed: bool) int
    }

    class Base64Encoder {
        -config CryptoConfig
        -_custom_encode_table dict
        -_custom_decode_table dict
        -_x3_encode_table dict
        -_x3_decode_table dict
        +Base64Encoder(config: CryptoConfig)
        +encode(data_to_encode: bytes | str | Iterable~int~) str
        +decode(encoded_string: str) str
        +decode_x3(encoded_string: str) bytes
        +encode_x3(input_bytes: bytes | bytearray) str
    }

    class CryptoConfig {
        +DES_KEY str
        +GID_URL str
        +DATA_PALTFORM str
        +DATA_SVN str
        +DATA_SDK_VERSION str
        +DATA_webBuild str
        +MAX_32BIT int
        +MAX_SIGNED_32BIT int
        +STANDARD_BASE64_ALPHABET str
        +CUSTOM_BASE64_ALPHABET str
        +X3_BASE64_ALPHABET str
        +X3_PREFIX str
        +XYS_PREFIX str
        +XRAY_TRACE_ID_PREFIX str
        +XRAY_TRACE_ID_PART1_LENGTH int
        +XRAY_TRACE_ID_PART2_LENGTH int
        +B3_TRACE_ID_LENGTH int
        +B1_SECRET_KEY str
        +SIGNATURE_XSCOMMON_TEMPLATE dict~str,Any~
        +PUBLIC_USERAGENT str
        +with_overrides(**kwargs: Any) CryptoConfig
    }

    class RequestSignatureValidator {
        +validate_payload(payload: Any) dict~str,Any~?
        +validate_cookie(cookie: Any) dict~str,Any~ | str
    }

    class validate_xs_common_params {
        <<function>>
    }

    Xhshow --> CryptoConfig : uses
    Xhshow --> XsCommonSigner : creates
    Xhshow --> RequestSignatureValidator : via validate_xs_common_params
    XsCommonSigner --> FingerprintGenerator : composes
    XsCommonSigner --> Base64Encoder : composes
    XsCommonSigner --> CryptoConfig : uses
    XsCommonSigner --> CRC32 : calls
    FingerprintGenerator --> CryptoConfig : uses
    FingerprintGenerator --> Base64Encoder : uses
    Base64Encoder --> CryptoConfig : uses
    CRC32 --> CryptoConfig : uses POLY constant
    validate_xs_common_params --> RequestSignatureValidator : uses
Loading

File-Level Changes

Change Details Files
Extend client signing APIs to generate x-s-common from full cookies and to expose dedicated x-s-common helpers.
  • Add sign_xs_common and sign_xsc methods on Xhshow, with validation via a new validate_xs_common_params decorator.
  • Introduce a private _parse_cookies helper that normalizes dict or cookie-string inputs using http.cookies.SimpleCookie.
  • Refactor sign_headers/sign_headers_get/sign_headers_post to take cookies instead of a1_value, parse cookies once, enforce presence of a1, and emit x-s-common alongside x-s in returned headers.
  • Update README examples and parameter docs to show cookie-based usage, x-s-common generation, and clarify responsibilities of sign_headers* vs sign_xs* vs sign_xsc APIs.
src/xhshow/client.py
src/xhshow/utils/validators.py
README.md
Implement x-s-common signing based on browser-like fingerprint synthesis and CRC32.
  • Add CryptoConfig fields for b1/x-s-common (B1_SECRET_KEY, SIGNATURE_XSCOMMON_TEMPLATE, PUBLIC_USERAGENT and other crypto constants).
  • Introduce CRC32 helper class implementing a JS-compatible CRC32 variant with flexible input handling.
  • Add FingerprintGenerator and fingerprint_helpers utilities plus fingerprint_data constants to synthesize realistic browser fingerprints and compute b1 (RC4-encrypted, URL-encoded, custom-Base64-encoded subset of the fingerprint).
  • Implement XsCommonSigner that builds the x-s-common structure from config template, cookies, b1 and CRC32(b1), then encodes the JSON via Base64Encoder.
  • Expose data and generators via new package init modules.
src/xhshow/config/config.py
src/xhshow/core/crc32_encrypt.py
src/xhshow/generators/fingerprint.py
src/xhshow/generators/fingerprint_helpers.py
src/xhshow/core/common_sign.py
src/xhshow/data/fingerprint_data.py
src/xhshow/data/__init__.py
src/xhshow/generators/__init__.py
Fix and enhance custom Base64 encoding/decoding to work on raw bytes, support multiple alphabets, and cache translation tables.
  • Change Base64Encoder.encode to accept bytes/str/iterable[int], encode bytes directly instead of re-encoding strings, and keep using the project’s custom alphabet.
  • Precompute and reuse translation tables for custom and X3 alphabets in Base64Encoder.init to avoid per-call maketrans creation.
  • Refactor decode/decode_x3/encode_x3 to use the cached translation tables for improved performance and clarity.
src/xhshow/utils/encoder.py
Add tests for cookie parsing, x-s-common signing, CRC32, and updated header-signing behavior.
  • Extend existing crypto/header-signing tests to use cookies instead of a1_value, assert presence of x-s-common, and cover validation paths.
  • Add dedicated CRC32 tests covering basic values, signed vs unsigned output, different string modes, and non-string inputs.
  • Add a new cookie_parsing test module covering _parse_cookies behavior (dict vs string, edge cases) and end-to-end sign_xs_common/sign_xsc/sign_headers* flows with both dict and string cookies.
tests/test_crypto.py
tests/test_cookie_parsing.py
Update build configuration to include new runtime dependencies required by the crypto/fingerprint pipeline.
  • Declare curl-cffi and pycryptodome as core project dependencies.
  • Regenerate uv.lock to capture the updated dependency graph.
pyproject.toml
uv.lock

Possibly linked issues

  • #: PR 引入自定义 Base64、x-s-common 签名和 b1 生成,正是新版小红书 xs 算法实现

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there - I've reviewed your changes and found some issues that need to be addressed.

  • Base64Encoder.encode 现在声称支持 bytes 和 Iterable[int],但它仍然在任何非 bytearray 输入上无条件调用 .encode('utf-8'),这会导致对 bytes/iterable 输入报错,并且对 bytes 和 bytearray 的处理不一致;建议显式规范化输入类型(例如分别处理 str、bytes/bytearray/memoryview 和 Iterable[int])。
  • CRC32._crc32_core 使用了 isinstance(data, bytes | bytearray | memoryview),但在运行时 isinstance 不支持 PEP 604 形式的 union;请改为使用元组(例如 (bytes, bytearray, memoryview))以避免 TypeError。
  • RequestSignatureValidator.validate_cookie 里的错误信息似乎是从其它地方直接复制过来的,仍然提到 “payload” 和 “payload keys”,这在 cookie 校验场景中具有误导性;请将这些消息更新为引用 “cookie”,以提供更清晰的诊断信息。
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Base64Encoder.encode now advertises support for bytes and Iterable[int], but it still unconditionally calls .encode('utf-8') on any non-bytearray input, which will fail for bytes/iterables and also treats bytes differently from bytearray; consider normalizing input types explicitly (e.g., separate branches for str, bytes/bytearray/memoryview, and Iterable[int]).
- CRC32._crc32_core uses `isinstance(data, bytes | bytearray | memoryview)`, but `isinstance` does not accept PEP 604 unions at runtime; replace this with a tuple (e.g., `(bytes, bytearray, memoryview)`) to avoid a TypeError.
- RequestSignatureValidator.validate_cookie appears to have copy-pasted error messages mentioning 'payload' and 'payload keys', which is misleading for cookie validation; update these messages to refer to 'cookie' instead for clearer diagnostics.

## Individual Comments

### Comment 1
<location> `src/xhshow/utils/encoder.py:33-36` </location>
<code_context>
+        )

-    def encode(self, data_to_encode: str) -> str:
+    def encode(self, data_to_encode: bytes | str | Iterable[int]) -> str:
         """
         Encode a string using custom Base64 alphabet
</code_context>

<issue_to_address>
**issue (bug_risk):** 新的 `encode` 函数签名与实现不一致,会在处理 `bytes` 和可迭代输入时出错。

新的类型标注允许 `bytes``Iterable[int]`,但在非 `bytearray` 分支中实现仍然假定参数为 `str``data_bytes = data_to_encode` 之后再调用 `data_to_encode.encode("utf-8")`,对于 `bytes` 和通用可迭代对象都会失败。

要么让实现与注解保持一致(例如先把所有输入统一规范化为 `bytes`,再进行编码/转换),要么如果不需要更宽的支持,就把类型标注收窄回 `str`。
</issue_to_address>

### Comment 2
<location> `src/xhshow/core/crc32_encrypt.py:66` </location>
<code_context>
+
+        c = cls.MASK32
+
+        if isinstance(data, bytes | bytearray | memoryview):
+            it = bytes(data)
+        elif isinstance(data, str):
</code_context>

<issue_to_address>
**issue (bug_risk):** 使用 `|` union 的 `isinstance` 检查会在运行时抛异常,应该改为使用类型元组。

在 CPython 中,`isinstance(obj, T1 | T2)` 会抛出 `TypeError: isinstance() argument 2 cannot be a union`。为了在不引入运行时错误的前提下保持相同行为,应修改为:

```python
if isinstance(data, (bytes, bytearray, memoryview)):
    it = bytes(data)
```
</issue_to_address>

### Comment 3
<location> `src/xhshow/utils/validators.py:80-90` </location>
<code_context>
         return payload

+    @staticmethod
+    def validate_cookie(cookie: Any) -> dict[str, Any] | str:
+        """Validate cookie parameter"""
+        if cookie is not None and not (isinstance(cookie, dict) or isinstance(cookie, str)):
+            raise TypeError(f"payload must be dict or None, got {type(cookie).__name__}")
+        # detect cookie dict validation
+        if cookie is not None and isinstance(cookie, dict):
+            for key in cookie.keys():
+                if not isinstance(key, str):
+                    raise TypeError(f"payload keys must be str, got {type(key).__name__} for key '{key}'")
+        return cookie
+
</code_context>

<issue_to_address>
**suggestion (bug_risk):** `validate_cookie` 中的错误信息引用了 `payload` 而不是 `cookie`,并且可接受类型与装饰器的用法不匹配。

当前抛出的异常消息提到 `payload`,并声称只接受 `dict or None`,这对一个 cookie 校验函数来说具有误导性,也会让调试变得困难。

此外还存在类型不匹配:`validate_xs_common_params` 可以传入 `None`(通过默认值),但 `validate_cookie` 的返回类型注解不包含 `None`。建议在函数签名中允许 `None`,并对错误信息和类型进行对齐,例如:

```python
def validate_cookie(cookie: Any) -> dict[str, Any] | str | None:
    if cookie is not None and not isinstance(cookie, (dict, str)):
        raise TypeError(f"cookie must be dict, str, or None, got {type(cookie).__name__}")
    if isinstance(cookie, dict):
        for key in cookie.keys():
            if not isinstance(key, str):
                raise TypeError(
                    f"cookie keys must be str, got {type(key).__name__} for key '{key}'"
                )
    return cookie
```

```suggestion
    @staticmethod
    def validate_cookie(cookie: Any) -> dict[str, Any] | str | None:
        """Validate cookie parameter"""
        if cookie is not None and not isinstance(cookie, (dict, str)):
            raise TypeError(
                f"cookie must be dict, str, or None, got {type(cookie).__name__}"
            )
        if isinstance(cookie, dict):
            for key in cookie.keys():
                if not isinstance(key, str):
                    raise TypeError(
                        f"cookie keys must be str, got {type(key).__name__} for key '{key}'"
                    )
        return cookie
```
</issue_to_address>

### Comment 4
<location> `src/xhshow/utils/validators.py:213-222` </location>
<code_context>
     return wrapper  # type: ignore
+
+
+def validate_xs_common_params(func: F) -> F:  # type: ignore[misc]  # noqa: UP047
+    """
+    Parameter validation decorator for the `sign_xsc` method.
+
+    This wrapper normalizes and validates the arguments before delegating to
+    the underlying signing implementation.
+
+    Args:
+        func: Method to be decorated.
+
+    Returns:
+        Wrapped method with validated parameters.
+    """
+
+    @wraps(func)
+    def wrapper(
+        self,
+        cookie_dict: dict[str, Any] | None = None,
+    ) -> str:
+        validator = RequestSignatureValidator()
</code_context>

<issue_to_address>
**issue (bug_risk):** `validate_xs_common_params` 更改了 `sign_xsc` 的函数签名并允许传入 `None`,这会在下游实现中导致问题。

当前包装器将 `sign_xsc` 的契约从 `dict[str, Any] | str` 改成了 `dict[str, Any] | None`,并丢失了 `str` 这种调用形式。

这会引发两个问题:
1. 传入字符串 cookie 的调用方不再受支持(或者类型标注变得不正确),因为按当前写法 `validate_cookie` 并不接受 `str | None`2.`cookie_dict``None` 时,该值会一路传递到 `sign_xsc``_parse_cookies``XsCommonSigner.sign`,而最后这个方法期望得到 `dict` 并执行 `cookie_dict["a1"]`,从而导致运行时抛出 `TypeError`,而不是给出一致的校验错误。

为了解决这个问题,应保持包装器签名与 `sign_xsc` 一致,并确保 `validate_cookie` 支持 `str` 且对于必填参数拒绝 `None````python
def wrapper(self, cookie_dict: dict[str, Any] | str) -> str:
    validator = RequestSignatureValidator()
    validated_cookie = validator.validate_cookie(cookie_dict)
    if validated_cookie is None:
        raise ValueError("cookie_dict is required")
    return func(self, validated_cookie)
```
</issue_to_address>

### Comment 5
<location> `tests/test_crypto.py:529` </location>
<code_context>
+        )
+
+        assert isinstance(headers, dict)
+        assert "x-s" in headers
+        assert "x-s-common" in headers
+        assert "x-t" in headers
</code_context>

<issue_to_address>
**issue (testing):** 测试期望 `sign_headers` 输出中包含 `x-s`,但实现当前只返回 `x-s-common``Xhshow.sign_headers` 的实现现在省略了 `"x-s"`,只返回 `"x-s-common"``"x-t"``"x-b3-traceid"``"x-xray-traceid"`,但这一处以及其它测试仍然断言存在 `"x-s"` 及其前缀。请先确定 `sign_headers*` 的对外契约究竟应该是什么,然后更新实现(重新暴露 `"x-s"`)或更新测试(不再断言它),以保证两者保持一致。
</issue_to_address>

### Comment 6
<location> `tests/test_crypto.py:584` </location>
<code_context>
+        )
+
+        assert isinstance(headers, dict)
+        assert all(k in headers for k in ["x-s", "x-s-common", "x-t", "x-b3-traceid", "x-xray-traceid"])
+        assert headers["x-s"].startswith("XYS_")
+        assert len(headers["x-s-common"]) > 0
</code_context>

<issue_to_address>
**issue (testing):** `sign_headers_get`/`sign_headers_post` 的测试仍然断言 `x-s` 的存在和格式,但该字段已从 headers 字典中移除。

这些测试仍然期望 `"x-s"` 存在并以 `"XYS_"` 开头,但 `client.sign_headers` 已不再返回这个 header。一旦确认期望的 header 集合,请只针对属于公开 API 的键进行断言。如果不再返回 `x-s`,请移除这里对 `XYS_` 前缀的断言,或者把它移动到直接针对底层 `sign_xs*` 帮助函数的测试中。
</issue_to_address>

### Comment 7
<location> `tests/test_cookie_parsing.py:214` </location>
<code_context>
+        )
+
+        assert isinstance(headers, dict)
+        assert "x-s" in headers
+        assert "x-s-common" in headers
+        assert "x-t" in headers
</code_context>

<issue_to_address>
**issue (testing):** 新增的 cookie/header 测试对 header 集合中的 `x-s` 进行了硬编码,这与当前实现不符。

这些测试目前断言 `sign_headers*` 的结果中存在 `"x-s"`,并在某些情况下断言它以 `"XYS_"` 开头,但本 PR 已经将 `sign_headers` 修改为仅返回 `x-s-common`(以及 trace ID),不再返回 `x-s`。请根据预期 API 进行对齐:

- 如果 `sign_headers*` 应该同时返回 `x-s``x-s-common`,请在实现中恢复 `x-s`- 如果只有 `x-s-common` 是对外暴露的字段,请删除这里对 `x-s` 的断言,并把任何关于 `x-s` 格式的检查迁移到更底层的签名函数测试中。
</issue_to_address>

### Comment 8
<location> `tests/test_cookie_parsing.py:135-96` </location>
<code_context>
+        with pytest.raises(KeyError, match="a1"):
+            self.client.sign_xs_common(cookies)
+
+    def test_sign_xsc_alias(self):
+        """测试 sign_xsc 别名方法"""
+        cookies = {
+            "a1": "test_a1_value",
+            "web_session": "test_session",
+        }
+
+        result = self.client.sign_xsc(cookies)
+
+        assert isinstance(result, str)
+        assert len(result) > 0
+
+
</code_context>

<issue_to_address>
**suggestion (testing):**`sign_xsc` / `validate_xs_common_params` 增加负向测试,覆盖类型和值校验逻辑。

由于 `sign_xsc` 使用了 `@validate_xs_common_params` 装饰器(内部会调用 `RequestSignatureValidator.validate_cookie`),当前测试只覆盖了传入合法字典的情况。请增加以下负向测试:

- 传入不受支持的类型(例如 list/int 作为 `cookie_dict`),并断言抛出带有预期消息的 `TypeError`- 使用包含非字符串键的 cookie 字典,以覆盖键类型校验路径。
- (可选)传入 `None`(取决于预期契约),并断言预期的行为。

这将帮助验证校验器的错误处理逻辑,并固定 `sign_xsc` 的参数契约。

建议实现:

```python
+    def test_sign_xsc_alias(self):
+        """测试 sign_xsc 别名方法"""
+        cookies = {
+            "a1": "test_a1_value",
+            "web_session": "test_session",
+        }
+
+        result = self.client.sign_xsc(cookies)
+
+        assert isinstance(result, str)
+        assert len(result) > 0
+
+    def test_sign_xsc_invalid_type(self):
+        """测试 sign_xsc 传入非 dict 类型参数时抛出异常"""
+        # list 作为参数
+        with pytest.raises(TypeError, match="cookie_dict"):
+            self.client.sign_xsc(["not", "a", "dict"])  # type: ignore[arg-type]
+
+        # int 作为参数
+        with pytest.raises(TypeError, match="cookie_dict"):
+            self.client.sign_xsc(123)  # type: ignore[arg-type]
+
+    def test_sign_xsc_non_string_keys(self):
+        """测试 sign_xsc 传入非字符串 key 的 cookie 时抛出异常"""
+        cookies = {
+            "a1": "test_a1_value",
+            "web_session": "test_session",
+            123: "value_with_int_key",  # 非字符串 key
+        }
+
+        with pytest.raises(TypeError, match="key"):
+            self.client.sign_xsc(cookies)  # type: ignore[dict-item]
+
+    def test_sign_xsc_none_cookie_dict(self):
+        """测试 sign_xsc 传入 None 时的参数校验行为"""
+        with pytest.raises(TypeError, match="cookie_dict"):
+            self.client.sign_xsc(None)  # type: ignore[arg-type]
+
+

```

这些测试假设 `@validate_xs_common_params` / `RequestSignatureValidator.validate_cookie`1.`cookie_dict` 不是 `dict`(list/int/None 情况)时抛出 `TypeError`,并且错误信息包含 `cookie_dict`2. 在存在非字符串键时抛出 `TypeError`,并且错误信息包含 `key`。

如果实际的校验器使用了不同的异常类型或消息,请相应地调整:
- `pytest.raises(...)` 中的异常类型;
- `match` 模式,以与真实错误消息保持一致。

如果测试类中已经提供了 `self.client.sign_xsc`,则不需要额外的代码变更。
</issue_to_address>

### Comment 9
<location> `tests/test_cookie_parsing.py:148-157` </location>
<code_context>
+class TestXsCommonSigner:
</code_context>

<issue_to_address>
**suggestion (testing):** 强化 `XsCommonSigner` 的测试,用固定向量或结构性断言覆盖 b1/Base64 回归问题。

当前测试只验证输出非空以及缺少 `a1` 时是否抛出 `KeyError`,并没有真正验证修复后的基于字节的 b1 Base64 行为。

为了更好地覆盖此次回归修复,建议增加一个确定性的测试:
- 使用固定的 cookie 字典和 `CryptoConfig`(具有确定性的 fingerprint),
- 通过 `FingerprintGenerator.generate_b1()``XsCommonSigner` 生成 `b1`- 断言 `b1`(或 `x-s-common`)与参考浏览器/VM 输出的已知正确值或前缀匹配(例如以 `"I38r"` 开头)。

建议实现:

```python
class TestXsCommonSigner:
    """测试 XsCommonSigner 类"""

    def setup_method(self):
        self.signer = XsCommonSigner()

    def test_sign_with_dict_only(self):
        """测试只接受字典参数"""
        cookies = {
            "a1": "test_a1_value",
            "web_session": "test_session",
        }

        # 基本行为:返回非空字符串
        result = self.signer.sign(cookies)
        assert isinstance(result, str)
        assert result

    def test_b1_base64_regression_vector(self):
        """使用固定向量验证 b1/Base64 行为,防止回归"""
        cookies = {
            "a1": "fixed_a1_value",
            "web_session": "fixed_session_value",
        }

        # 使用固定的 CryptoConfig 以获得确定性的指纹
        crypto_config = CryptoConfig(
            fingerprint="fixed_fingerprint_for_test",
        )

        fp_gen = FingerprintGenerator(crypto_config=crypto_config)

        # 直接生成 b1,确保是稳定的 Base64 字节编码
        b1 = fp_gen.generate_b1(cookies)
        assert isinstance(b1, str)
        assert b1  # 非空

        # 结构性断言:b1 必须以已知前缀开头(来自参考浏览器/VM 输出)
        # 注意:前缀 "I38r" 需要根据实际参考输出进行同步
        assert b1.startswith("I38r")

        # 通过 XsCommonSigner 生成 x-s-common 头部,应与 b1 行为一致
        x_s_common = self.signer.sign(cookies=cookies, crypto_config=crypto_config)
        assert isinstance(x_s_common, str)
        assert x_s_common
        assert x_s_common.startswith("I38r")

```

1.`tests/test_cookie_parsing.py` 顶部增加正确的导入(根据实际模块路径调整):
   ```python
   from your_module_path import CryptoConfig, FingerprintGenerator
   ```
   或者如果它们在不同模块中,请分别导入。

2. 确认 `XsCommonSigner` 的 API:
   - 如果当前签名方法不是 `sign`,而是例如 `sign_xsc` 或其他名字,请将
     ```python
     result = self.signer.sign(cookies)
     x_s_common = self.signer.sign(cookies=cookies, crypto_config=crypto_config)
     ```
     替换为实际的方法名和参数签名。

3. 如果 `CryptoConfig` 构造函数需要额外参数(例如密钥、盐值等),为测试构造一个完全确定性的配置(固定密钥/盐),并在 `test_b1_base64_regression_vector` 中补全这些参数。

4. `FingerprintGenerator` 的初始化和 `generate_b1` 调用可能需要不同的参数名或额外上下文:
   -```python
     fp_gen = FingerprintGenerator(crypto_config=crypto_config)
     b1 = fp_gen.generate_b1(cookies)
     ```
     调整为当前项目中实际的初始化方式与方法签名。

5. `"I38r"` 仅是示例前缀,请从参考浏览器/VM 输出中获取真实的 `b1` 值或前缀,并更新断言:
   ```python
   assert b1.startswith("<真实前缀>")
   assert x_s_common.startswith("<真实前缀>")
   ```

6. 如项目中已有 pytest fixture 提供 `CryptoConfig``FingerprintGenerator`(例如 `crypto_config``fingerprint_generator`),可以将测试改写为使用这些 fixture,而不是在测试中直接构造实例,以保持与现有测试风格一致。
</issue_to_address>

### Comment 10
<location> `README.md:193` </location>
<code_context>
+cookie_string = "a1=your_a1_value; web_session=your_web_session; webId=your_web_id"
+xs_common = client.sign_xsc(cookie_dict=cookie_string)
+
+# 使用在请求中
+headers = {
+    "x-s-common": xs_common,
</code_context>

<issue_to_address>
**suggestion (typo):** “使用在请求中”建议调整为更自然的表达,如“在请求中使用”。

这句注释略显别扭,建议改为“# 在请求中使用”,更符合中文表达习惯。

```suggestion
# 在请求中使用
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
Original comment in English

Hey there - I've reviewed your changes and found some issues that need to be addressed.

  • Base64Encoder.encode now advertises support for bytes and Iterable[int], but it still unconditionally calls .encode('utf-8') on any non-bytearray input, which will fail for bytes/iterables and also treats bytes differently from bytearray; consider normalizing input types explicitly (e.g., separate branches for str, bytes/bytearray/memoryview, and Iterable[int]).
  • CRC32._crc32_core uses isinstance(data, bytes | bytearray | memoryview), but isinstance does not accept PEP 604 unions at runtime; replace this with a tuple (e.g., (bytes, bytearray, memoryview)) to avoid a TypeError.
  • RequestSignatureValidator.validate_cookie appears to have copy-pasted error messages mentioning 'payload' and 'payload keys', which is misleading for cookie validation; update these messages to refer to 'cookie' instead for clearer diagnostics.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Base64Encoder.encode now advertises support for bytes and Iterable[int], but it still unconditionally calls .encode('utf-8') on any non-bytearray input, which will fail for bytes/iterables and also treats bytes differently from bytearray; consider normalizing input types explicitly (e.g., separate branches for str, bytes/bytearray/memoryview, and Iterable[int]).
- CRC32._crc32_core uses `isinstance(data, bytes | bytearray | memoryview)`, but `isinstance` does not accept PEP 604 unions at runtime; replace this with a tuple (e.g., `(bytes, bytearray, memoryview)`) to avoid a TypeError.
- RequestSignatureValidator.validate_cookie appears to have copy-pasted error messages mentioning 'payload' and 'payload keys', which is misleading for cookie validation; update these messages to refer to 'cookie' instead for clearer diagnostics.

## Individual Comments

### Comment 1
<location> `src/xhshow/utils/encoder.py:33-36` </location>
<code_context>
+        )

-    def encode(self, data_to_encode: str) -> str:
+    def encode(self, data_to_encode: bytes | str | Iterable[int]) -> str:
         """
         Encode a string using custom Base64 alphabet
</code_context>

<issue_to_address>
**issue (bug_risk):** The new `encode` signature and implementation are inconsistent and will break for `bytes` and iterable inputs.

The new type hint allows `bytes` and `Iterable[int]`, but the implementation still assumes a `str` in the non-`bytearray` branch. `data_bytes = data_to_encode` followed by `data_to_encode.encode("utf-8")` will fail for `bytes` and generic iterables.

Either align the implementation with the annotation (e.g. normalize all inputs to `bytes` first, then encode/translate) or narrow the annotation back to `str` if wider support isn’t required.
</issue_to_address>

### Comment 2
<location> `src/xhshow/core/crc32_encrypt.py:66` </location>
<code_context>
+
+        c = cls.MASK32
+
+        if isinstance(data, bytes | bytearray | memoryview):
+            it = bytes(data)
+        elif isinstance(data, str):
</code_context>

<issue_to_address>
**issue (bug_risk):** The `isinstance` check using a `|` union will raise at runtime; it should use a tuple of types instead.

In CPython, `isinstance(obj, T1 | T2)` raises `TypeError: isinstance() argument 2 cannot be a union`. To keep the same behavior without a runtime error, this should be:

```python
if isinstance(data, (bytes, bytearray, memoryview)):
    it = bytes(data)
```
</issue_to_address>

### Comment 3
<location> `src/xhshow/utils/validators.py:80-90` </location>
<code_context>
         return payload

+    @staticmethod
+    def validate_cookie(cookie: Any) -> dict[str, Any] | str:
+        """Validate cookie parameter"""
+        if cookie is not None and not (isinstance(cookie, dict) or isinstance(cookie, str)):
+            raise TypeError(f"payload must be dict or None, got {type(cookie).__name__}")
+        # detect cookie dict validation
+        if cookie is not None and isinstance(cookie, dict):
+            for key in cookie.keys():
+                if not isinstance(key, str):
+                    raise TypeError(f"payload keys must be str, got {type(key).__name__} for key '{key}'")
+        return cookie
+
</code_context>

<issue_to_address>
**suggestion (bug_risk):** The error messages in `validate_cookie` reference `payload` instead of `cookie` and the accepted types don’t match the decorator usage.

The exceptions currently mention `payload` and refer to `dict or None`, which is misleading for a cookie validator and will confuse debugging.

There’s also a type mismatch: `validate_xs_common_params` can pass `None` (via the default), but `validate_cookie`’s return annotation doesn’t include `None`. Consider allowing `None` in the signature and aligning the error messages and types, e.g.:

```python
def validate_cookie(cookie: Any) -> dict[str, Any] | str | None:
    if cookie is not None and not isinstance(cookie, (dict, str)):
        raise TypeError(f"cookie must be dict, str, or None, got {type(cookie).__name__}")
    if isinstance(cookie, dict):
        for key in cookie.keys():
            if not isinstance(key, str):
                raise TypeError(
                    f"cookie keys must be str, got {type(key).__name__} for key '{key}'"
                )
    return cookie
```

```suggestion
    @staticmethod
    def validate_cookie(cookie: Any) -> dict[str, Any] | str | None:
        """Validate cookie parameter"""
        if cookie is not None and not isinstance(cookie, (dict, str)):
            raise TypeError(
                f"cookie must be dict, str, or None, got {type(cookie).__name__}"
            )
        if isinstance(cookie, dict):
            for key in cookie.keys():
                if not isinstance(key, str):
                    raise TypeError(
                        f"cookie keys must be str, got {type(key).__name__} for key '{key}'"
                    )
        return cookie
```
</issue_to_address>

### Comment 4
<location> `src/xhshow/utils/validators.py:213-222` </location>
<code_context>
     return wrapper  # type: ignore
+
+
+def validate_xs_common_params(func: F) -> F:  # type: ignore[misc]  # noqa: UP047
+    """
+    Parameter validation decorator for the `sign_xsc` method.
+
+    This wrapper normalizes and validates the arguments before delegating to
+    the underlying signing implementation.
+
+    Args:
+        func: Method to be decorated.
+
+    Returns:
+        Wrapped method with validated parameters.
+    """
+
+    @wraps(func)
+    def wrapper(
+        self,
+        cookie_dict: dict[str, Any] | None = None,
+    ) -> str:
+        validator = RequestSignatureValidator()
</code_context>

<issue_to_address>
**issue (bug_risk):** `validate_xs_common_params` changes the `sign_xsc` signature and allows `None`, which will break in the downstream implementation.

The wrapper currently changes the contract of `sign_xsc` from `dict[str, Any] | str` to `dict[str, Any] | None` and drops the `str` case.

This causes two problems:
1. Callers passing a cookie string are no longer supported (or the types are wrong), since `validate_cookie` doesn’t accept `str | None` as written.
2. When `cookie_dict` is `None`, `None` propagates into `sign_xsc``_parse_cookies``XsCommonSigner.sign`, which expects a `dict` and does `cookie_dict["a1"]`, leading to a runtime `TypeError` instead of consistent validation errors.

To fix this, keep the wrapper signature aligned with `sign_xsc` and ensure `validate_cookie` supports `str` and rejects `None` for required parameters:

```python
def wrapper(self, cookie_dict: dict[str, Any] | str) -> str:
    validator = RequestSignatureValidator()
    validated_cookie = validator.validate_cookie(cookie_dict)
    if validated_cookie is None:
        raise ValueError("cookie_dict is required")
    return func(self, validated_cookie)
```
</issue_to_address>

### Comment 5
<location> `tests/test_crypto.py:529` </location>
<code_context>
+        )
+
+        assert isinstance(headers, dict)
+        assert "x-s" in headers
+        assert "x-s-common" in headers
+        assert "x-t" in headers
</code_context>

<issue_to_address>
**issue (testing):** Tests expect `x-s` in `sign_headers` output but implementation currently only returns `x-s-common`

The implementation of `Xhshow.sign_headers` now omits `"x-s"` and only returns `"x-s-common"`, `"x-t"`, `"x-b3-traceid"`, and `"x-xray-traceid"`, but this and other tests still assert `"x-s"` and its prefix. Please decide what the contract of `sign_headers*` should be and update either the implementation (to re‑expose `"x-s"`) or the tests (to stop asserting it) so they are consistent.
</issue_to_address>

### Comment 6
<location> `tests/test_crypto.py:584` </location>
<code_context>
+        )
+
+        assert isinstance(headers, dict)
+        assert all(k in headers for k in ["x-s", "x-s-common", "x-t", "x-b3-traceid", "x-xray-traceid"])
+        assert headers["x-s"].startswith("XYS_")
+        assert len(headers["x-s-common"]) > 0
</code_context>

<issue_to_address>
**issue (testing):** `sign_headers_get`/`sign_headers_post` tests still assert presence and format of `x-s` despite it being removed from the headers dict

These tests still expect `"x-s"` to be present and to start with `"XYS_"`, but `client.sign_headers` no longer returns that header. Once the expected header set is confirmed, please update them to only assert keys that are part of the public API. If `x-s` is no longer returned, remove the `XYS_` prefix assertion here or move it into a test that directly targets the lower-level `sign_xs*` helpers.
</issue_to_address>

### Comment 7
<location> `tests/test_cookie_parsing.py:214` </location>
<code_context>
+        )
+
+        assert isinstance(headers, dict)
+        assert "x-s" in headers
+        assert "x-s-common" in headers
+        assert "x-t" in headers
</code_context>

<issue_to_address>
**issue (testing):** New cookie/header tests hard-code `x-s` in the header set, which conflicts with the current implementation

These tests currently assert that `"x-s"` is present in `sign_headers*` results and sometimes that it starts with `"XYS_"`, but this PR changes `sign_headers` to return `x-s-common` (and trace IDs) without `x-s`. Please align the expectations with the intended API:

- If `sign_headers*` should return both `x-s` and `x-s-common`, restore `x-s` in the implementation.
- If only `x-s-common` is public, drop the `x-s` assertions here and move any `x-s` format checks into lower-level signing tests.
</issue_to_address>

### Comment 8
<location> `tests/test_cookie_parsing.py:135-96` </location>
<code_context>
+        with pytest.raises(KeyError, match="a1"):
+            self.client.sign_xs_common(cookies)
+
+    def test_sign_xsc_alias(self):
+        """测试 sign_xsc 别名方法"""
+        cookies = {
+            "a1": "test_a1_value",
+            "web_session": "test_session",
+        }
+
+        result = self.client.sign_xsc(cookies)
+
+        assert isinstance(result, str)
+        assert len(result) > 0
+
+
</code_context>

<issue_to_address>
**suggestion (testing):** Add negative tests for `sign_xsc` / `validate_xs_common_params` to cover type and value validation

Since `sign_xsc` is decorated with `@validate_xs_common_params` (which calls `RequestSignatureValidator.validate_cookie`), the current test only covers the valid dict case. Please add negative tests that:

- Pass an unsupported type (e.g. list/int) as `cookie_dict` and assert a `TypeError` with the expected message.
- Use a cookie dict with non-string keys to cover the key-type validation path.
- Optionally, pass `None` (depending on the intended contract) and assert the expected behaviour.

This will exercise the validator’s error handling and lock in the parameter contract for `sign_xsc`.

Suggested implementation:

```python
+    def test_sign_xsc_alias(self):
+        """测试 sign_xsc 别名方法"""
+        cookies = {
+            "a1": "test_a1_value",
+            "web_session": "test_session",
+        }
+
+        result = self.client.sign_xsc(cookies)
+
+        assert isinstance(result, str)
+        assert len(result) > 0
+
+    def test_sign_xsc_invalid_type(self):
+        """测试 sign_xsc 传入非 dict 类型参数时抛出异常"""
+        # list 作为参数
+        with pytest.raises(TypeError, match="cookie_dict"):
+            self.client.sign_xsc(["not", "a", "dict"])  # type: ignore[arg-type]
+
+        # int 作为参数
+        with pytest.raises(TypeError, match="cookie_dict"):
+            self.client.sign_xsc(123)  # type: ignore[arg-type]
+
+    def test_sign_xsc_non_string_keys(self):
+        """测试 sign_xsc 传入非字符串 key 的 cookie 时抛出异常"""
+        cookies = {
+            "a1": "test_a1_value",
+            "web_session": "test_session",
+            123: "value_with_int_key",  # 非字符串 key
+        }
+
+        with pytest.raises(TypeError, match="key"):
+            self.client.sign_xsc(cookies)  # type: ignore[dict-item]
+
+    def test_sign_xsc_none_cookie_dict(self):
+        """测试 sign_xsc 传入 None 时的参数校验行为"""
+        with pytest.raises(TypeError, match="cookie_dict"):
+            self.client.sign_xsc(None)  # type: ignore[arg-type]
+
+

```

These tests assume that `@validate_xs_common_params` / `RequestSignatureValidator.validate_cookie`:
1. Raise `TypeError` when `cookie_dict` is not a `dict` (list/int/None case) and that the error message contains `cookie_dict`.
2. Raise `TypeError` for non-string keys with an error message containing `key`.

If the actual validator uses different exception types or messages, adjust:
- The exception class in `pytest.raises(...)`.
- The `match` patterns to align with the real error messages.

No other code changes should be required if `self.client.sign_xsc` is already available in the test class.
</issue_to_address>

### Comment 9
<location> `tests/test_cookie_parsing.py:148-157` </location>
<code_context>
+class TestXsCommonSigner:
</code_context>

<issue_to_address>
**suggestion (testing):** Strengthen `XsCommonSigner` tests with a fixed-vector or structural assertion that covers the b1/Base64 regression

Current tests only verify that the output is non-empty and that missing `a1` raises `KeyError`, which doesn’t validate the corrected byte-based Base64 behaviour for `b1`.

To better cover this regression, please add a deterministic test that:
- Uses a fixed cookie dict and `CryptoConfig` (with deterministic fingerprint),
- Produces `b1` via `FingerprintGenerator.generate_b1()` or `XsCommonSigner`, and
- Asserts that `b1` (or `x-s-common`) matches a known-good value or prefix from reference browser/VM output (e.g. starts with `"I38r"`).

Suggested implementation:

```python
class TestXsCommonSigner:
    """测试 XsCommonSigner 类"""

    def setup_method(self):
        self.signer = XsCommonSigner()

    def test_sign_with_dict_only(self):
        """测试只接受字典参数"""
        cookies = {
            "a1": "test_a1_value",
            "web_session": "test_session",
        }

        # 基本行为:返回非空字符串
        result = self.signer.sign(cookies)
        assert isinstance(result, str)
        assert result

    def test_b1_base64_regression_vector(self):
        """使用固定向量验证 b1/Base64 行为,防止回归"""
        cookies = {
            "a1": "fixed_a1_value",
            "web_session": "fixed_session_value",
        }

        # 使用固定的 CryptoConfig 以获得确定性的指纹
        crypto_config = CryptoConfig(
            fingerprint="fixed_fingerprint_for_test",
        )

        fp_gen = FingerprintGenerator(crypto_config=crypto_config)

        # 直接生成 b1,确保是稳定的 Base64 字节编码
        b1 = fp_gen.generate_b1(cookies)
        assert isinstance(b1, str)
        assert b1  # 非空

        # 结构性断言:b1 必须以已知前缀开头(来自参考浏览器/VM 输出)
        # 注意:前缀 "I38r" 需要根据实际参考输出进行同步
        assert b1.startswith("I38r")

        # 通过 XsCommonSigner 生成 x-s-common 头部,应与 b1 行为一致
        x_s_common = self.signer.sign(cookies=cookies, crypto_config=crypto_config)
        assert isinstance(x_s_common, str)
        assert x_s_common
        assert x_s_common.startswith("I38r")

```

1.`tests/test_cookie_parsing.py` 顶部增加正确的导入(根据实际模块路径调整):
   ```python
   from your_module_path import CryptoConfig, FingerprintGenerator
   ```
   或者如果它们在不同模块中,请分别导入。

2. 确认 `XsCommonSigner` 的 API:
   - 如果当前签名方法不是 `sign`,而是例如 `sign_xsc` 或其他名字,请将
     ```python
     result = self.signer.sign(cookies)
     x_s_common = self.signer.sign(cookies=cookies, crypto_config=crypto_config)
     ```
     替换为实际的方法名和参数签名。

3. 如果 `CryptoConfig` 构造函数需要额外参数(例如密钥、盐值等),为测试构造一个完全确定性的配置(固定密钥/盐),并在 `test_b1_base64_regression_vector` 中补全这些参数。

4. `FingerprintGenerator` 的初始化和 `generate_b1` 调用可能需要不同的参数名或额外上下文:
   -```python
     fp_gen = FingerprintGenerator(crypto_config=crypto_config)
     b1 = fp_gen.generate_b1(cookies)
     ```
     调整为当前项目中实际的初始化方式与方法签名。

5. `"I38r"` 仅是示例前缀,请从参考浏览器/VM 输出中获取真实的 `b1` 值或前缀,并更新断言:
   ```python
   assert b1.startswith("<真实前缀>")
   assert x_s_common.startswith("<真实前缀>")
   ```

6. 如项目中已有 pytest fixture 提供 `CryptoConfig``FingerprintGenerator`(例如 `crypto_config``fingerprint_generator`),可以将测试改写为使用这些 fixture,而不是在测试中直接构造实例,以保持与现有测试风格一致。
</issue_to_address>

### Comment 10
<location> `README.md:193` </location>
<code_context>
+cookie_string = "a1=your_a1_value; web_session=your_web_session; webId=your_web_id"
+xs_common = client.sign_xsc(cookie_dict=cookie_string)
+
+# 使用在请求中
+headers = {
+    "x-s-common": xs_common,
</code_context>

<issue_to_address>
**suggestion (typo):** “使用在请求中”建议调整为更自然的表达,如“在请求中使用”。

这句注释略显别扭,建议改为“# 在请求中使用”,更符合中文表达习惯。

```suggestion
# 在请求中使用
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +33 to 36
def encode(self, data_to_encode: bytes | str | Iterable[int]) -> str:
"""
Encode a string using custom Base64 alphabet
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): 新的 encode 函数签名与实现不一致,会在处理 bytes 和可迭代输入时出错。

新的类型标注允许 bytesIterable[int],但在非 bytearray 分支中实现仍然假定参数为 strdata_bytes = data_to_encode 之后再调用 data_to_encode.encode("utf-8"),对于 bytes 和通用可迭代对象都会失败。

要么让实现与注解保持一致(例如先把所有输入统一规范化为 bytes,再进行编码/转换),要么如果不需要更宽的支持,就把类型标注收窄回 str

Original comment in English

issue (bug_risk): The new encode signature and implementation are inconsistent and will break for bytes and iterable inputs.

The new type hint allows bytes and Iterable[int], but the implementation still assumes a str in the non-bytearray branch. data_bytes = data_to_encode followed by data_to_encode.encode("utf-8") will fail for bytes and generic iterables.

Either align the implementation with the annotation (e.g. normalize all inputs to bytes first, then encode/translate) or narrow the annotation back to str if wider support isn’t required.


c = cls.MASK32

if isinstance(data, bytes | bytearray | memoryview):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): 使用 | union 的 isinstance 检查会在运行时抛异常,应该改为使用类型元组。

在 CPython 中,isinstance(obj, T1 | T2) 会抛出 TypeError: isinstance() argument 2 cannot be a union。为了在不引入运行时错误的前提下保持相同行为,应修改为:

if isinstance(data, (bytes, bytearray, memoryview)):
    it = bytes(data)
Original comment in English

issue (bug_risk): The isinstance check using a | union will raise at runtime; it should use a tuple of types instead.

In CPython, isinstance(obj, T1 | T2) raises TypeError: isinstance() argument 2 cannot be a union. To keep the same behavior without a runtime error, this should be:

if isinstance(data, (bytes, bytearray, memoryview)):
    it = bytes(data)

Comment on lines +80 to +90
@staticmethod
def validate_cookie(cookie: Any) -> dict[str, Any] | str:
"""Validate cookie parameter"""
if cookie is not None and not (isinstance(cookie, dict) or isinstance(cookie, str)):
raise TypeError(f"payload must be dict or None, got {type(cookie).__name__}")
# detect cookie dict validation
if cookie is not None and isinstance(cookie, dict):
for key in cookie.keys():
if not isinstance(key, str):
raise TypeError(f"payload keys must be str, got {type(key).__name__} for key '{key}'")
return cookie
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): validate_cookie 中的错误信息引用了 payload 而不是 cookie,并且可接受类型与装饰器的用法不匹配。

当前抛出的异常消息提到 payload,并声称只接受 dict or None,这对一个 cookie 校验函数来说具有误导性,也会让调试变得困难。

此外还存在类型不匹配:validate_xs_common_params 可以传入 None(通过默认值),但 validate_cookie 的返回类型注解不包含 None。建议在函数签名中允许 None,并对错误信息和类型进行对齐,例如:

def validate_cookie(cookie: Any) -> dict[str, Any] | str | None:
    if cookie is not None and not isinstance(cookie, (dict, str)):
        raise TypeError(f"cookie must be dict, str, or None, got {type(cookie).__name__}")
    if isinstance(cookie, dict):
        for key in cookie.keys():
            if not isinstance(key, str):
                raise TypeError(
                    f"cookie keys must be str, got {type(key).__name__} for key '{key}'"
                )
    return cookie
Suggested change
@staticmethod
def validate_cookie(cookie: Any) -> dict[str, Any] | str:
"""Validate cookie parameter"""
if cookie is not None and not (isinstance(cookie, dict) or isinstance(cookie, str)):
raise TypeError(f"payload must be dict or None, got {type(cookie).__name__}")
# detect cookie dict validation
if cookie is not None and isinstance(cookie, dict):
for key in cookie.keys():
if not isinstance(key, str):
raise TypeError(f"payload keys must be str, got {type(key).__name__} for key '{key}'")
return cookie
@staticmethod
def validate_cookie(cookie: Any) -> dict[str, Any] | str | None:
"""Validate cookie parameter"""
if cookie is not None and not isinstance(cookie, (dict, str)):
raise TypeError(
f"cookie must be dict, str, or None, got {type(cookie).__name__}"
)
if isinstance(cookie, dict):
for key in cookie.keys():
if not isinstance(key, str):
raise TypeError(
f"cookie keys must be str, got {type(key).__name__} for key '{key}'"
)
return cookie
Original comment in English

suggestion (bug_risk): The error messages in validate_cookie reference payload instead of cookie and the accepted types don’t match the decorator usage.

The exceptions currently mention payload and refer to dict or None, which is misleading for a cookie validator and will confuse debugging.

There’s also a type mismatch: validate_xs_common_params can pass None (via the default), but validate_cookie’s return annotation doesn’t include None. Consider allowing None in the signature and aligning the error messages and types, e.g.:

def validate_cookie(cookie: Any) -> dict[str, Any] | str | None:
    if cookie is not None and not isinstance(cookie, (dict, str)):
        raise TypeError(f"cookie must be dict, str, or None, got {type(cookie).__name__}")
    if isinstance(cookie, dict):
        for key in cookie.keys():
            if not isinstance(key, str):
                raise TypeError(
                    f"cookie keys must be str, got {type(key).__name__} for key '{key}'"
                )
    return cookie
Suggested change
@staticmethod
def validate_cookie(cookie: Any) -> dict[str, Any] | str:
"""Validate cookie parameter"""
if cookie is not None and not (isinstance(cookie, dict) or isinstance(cookie, str)):
raise TypeError(f"payload must be dict or None, got {type(cookie).__name__}")
# detect cookie dict validation
if cookie is not None and isinstance(cookie, dict):
for key in cookie.keys():
if not isinstance(key, str):
raise TypeError(f"payload keys must be str, got {type(key).__name__} for key '{key}'")
return cookie
@staticmethod
def validate_cookie(cookie: Any) -> dict[str, Any] | str | None:
"""Validate cookie parameter"""
if cookie is not None and not isinstance(cookie, (dict, str)):
raise TypeError(
f"cookie must be dict, str, or None, got {type(cookie).__name__}"
)
if isinstance(cookie, dict):
for key in cookie.keys():
if not isinstance(key, str):
raise TypeError(
f"cookie keys must be str, got {type(key).__name__} for key '{key}'"
)
return cookie

Comment on lines +213 to +222
def validate_xs_common_params(func: F) -> F: # type: ignore[misc] # noqa: UP047
"""
Parameter validation decorator for the `sign_xsc` method.
This wrapper normalizes and validates the arguments before delegating to
the underlying signing implementation.
Args:
func: Method to be decorated.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): validate_xs_common_params 更改了 sign_xsc 的函数签名并允许传入 None,这会在下游实现中导致问题。

当前包装器将 sign_xsc 的契约从 dict[str, Any] | str 改成了 dict[str, Any] | None,并丢失了 str 这种调用形式。

这会引发两个问题:

  1. 传入字符串 cookie 的调用方不再受支持(或者类型标注变得不正确),因为按当前写法 validate_cookie 并不接受 str | None
  2. cookie_dictNone 时,该值会一路传递到 sign_xsc_parse_cookiesXsCommonSigner.sign,而最后这个方法期望得到 dict 并执行 cookie_dict["a1"],从而导致运行时抛出 TypeError,而不是给出一致的校验错误。

为了解决这个问题,应保持包装器签名与 sign_xsc 一致,并确保 validate_cookie 支持 str 且对于必填参数拒绝 None

def wrapper(self, cookie_dict: dict[str, Any] | str) -> str:
    validator = RequestSignatureValidator()
    validated_cookie = validator.validate_cookie(cookie_dict)
    if validated_cookie is None:
        raise ValueError("cookie_dict is required")
    return func(self, validated_cookie)
Original comment in English

issue (bug_risk): validate_xs_common_params changes the sign_xsc signature and allows None, which will break in the downstream implementation.

The wrapper currently changes the contract of sign_xsc from dict[str, Any] | str to dict[str, Any] | None and drops the str case.

This causes two problems:

  1. Callers passing a cookie string are no longer supported (or the types are wrong), since validate_cookie doesn’t accept str | None as written.
  2. When cookie_dict is None, None propagates into sign_xsc_parse_cookiesXsCommonSigner.sign, which expects a dict and does cookie_dict["a1"], leading to a runtime TypeError instead of consistent validation errors.

To fix this, keep the wrapper signature aligned with sign_xsc and ensure validate_cookie supports str and rejects None for required parameters:

def wrapper(self, cookie_dict: dict[str, Any] | str) -> str:
    validator = RequestSignatureValidator()
    validated_cookie = validator.validate_cookie(cookie_dict)
    if validated_cookie is None:
        raise ValueError("cookie_dict is required")
    return func(self, validated_cookie)

)

assert isinstance(headers, dict)
assert "x-s" in headers
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (testing): 测试期望 sign_headers 输出中包含 x-s,但实现当前只返回 x-s-common

Xhshow.sign_headers 的实现现在省略了 "x-s",只返回 "x-s-common""x-t""x-b3-traceid""x-xray-traceid",但这一处以及其它测试仍然断言存在 "x-s" 及其前缀。请先确定 sign_headers* 的对外契约究竟应该是什么,然后更新实现(重新暴露 "x-s")或更新测试(不再断言它),以保证两者保持一致。

Original comment in English

issue (testing): Tests expect x-s in sign_headers output but implementation currently only returns x-s-common

The implementation of Xhshow.sign_headers now omits "x-s" and only returns "x-s-common", "x-t", "x-b3-traceid", and "x-xray-traceid", but this and other tests still assert "x-s" and its prefix. Please decide what the contract of sign_headers* should be and update either the implementation (to re‑expose "x-s") or the tests (to stop asserting it) so they are consistent.


assert isinstance(headers, dict)
assert all(k in headers for k in ["x-s", "x-t", "x-b3-traceid", "x-xray-traceid"])
assert all(k in headers for k in ["x-s", "x-s-common", "x-t", "x-b3-traceid", "x-xray-traceid"])
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (testing): sign_headers_get/sign_headers_post 的测试仍然断言 x-s 的存在和格式,但该字段已从 headers 字典中移除。

这些测试仍然期望 "x-s" 存在并以 "XYS_" 开头,但 client.sign_headers 已不再返回这个 header。一旦确认期望的 header 集合,请只针对属于公开 API 的键进行断言。如果不再返回 x-s,请移除这里对 XYS_ 前缀的断言,或者把它移动到直接针对底层 sign_xs* 帮助函数的测试中。

Original comment in English

issue (testing): sign_headers_get/sign_headers_post tests still assert presence and format of x-s despite it being removed from the headers dict

These tests still expect "x-s" to be present and to start with "XYS_", but client.sign_headers no longer returns that header. Once the expected header set is confirmed, please update them to only assert keys that are part of the public API. If x-s is no longer returned, remove the XYS_ prefix assertion here or move it into a test that directly targets the lower-level sign_xs* helpers.

)

assert isinstance(headers, dict)
assert "x-s" in headers
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (testing): 新增的 cookie/header 测试对 header 集合中的 x-s 进行了硬编码,这与当前实现不符。

这些测试目前断言 sign_headers* 的结果中存在 "x-s",并在某些情况下断言它以 "XYS_" 开头,但本 PR 已经将 sign_headers 修改为仅返回 x-s-common(以及 trace ID),不再返回 x-s。请根据预期 API 进行对齐:

  • 如果 sign_headers* 应该同时返回 x-sx-s-common,请在实现中恢复 x-s
  • 如果只有 x-s-common 是对外暴露的字段,请删除这里对 x-s 的断言,并把任何关于 x-s 格式的检查迁移到更底层的签名函数测试中。
Original comment in English

issue (testing): New cookie/header tests hard-code x-s in the header set, which conflicts with the current implementation

These tests currently assert that "x-s" is present in sign_headers* results and sometimes that it starts with "XYS_", but this PR changes sign_headers to return x-s-common (and trace IDs) without x-s. Please align the expectations with the intended API:

  • If sign_headers* should return both x-s and x-s-common, restore x-s in the implementation.
  • If only x-s-common is public, drop the x-s assertions here and move any x-s format checks into lower-level signing tests.

result = self.client.sign_xs_common(cookies)

assert isinstance(result, str)
assert len(result) > 0
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing):sign_xsc / validate_xs_common_params 增加负向测试,覆盖类型和值校验逻辑。

由于 sign_xsc 使用了 @validate_xs_common_params 装饰器(内部会调用 RequestSignatureValidator.validate_cookie),当前测试只覆盖了传入合法字典的情况。请增加以下负向测试:

  • 传入不受支持的类型(例如 list/int 作为 cookie_dict),并断言抛出带有预期消息的 TypeError
  • 使用包含非字符串键的 cookie 字典,以覆盖键类型校验路径。
  • (可选)传入 None(取决于预期契约),并断言预期的行为。

这将帮助验证校验器的错误处理逻辑,并固定 sign_xsc 的参数契约。

建议实现:

+    def test_sign_xsc_alias(self):
+        """测试 sign_xsc 别名方法"""
+        cookies = {
+            "a1": "test_a1_value",
+            "web_session": "test_session",
+        }
+
+        result = self.client.sign_xsc(cookies)
+
+        assert isinstance(result, str)
+        assert len(result) > 0
+
+    def test_sign_xsc_invalid_type(self):
+        """测试 sign_xsc 传入非 dict 类型参数时抛出异常"""
+        # list 作为参数
+        with pytest.raises(TypeError, match="cookie_dict"):
+            self.client.sign_xsc(["not", "a", "dict"])  # type: ignore[arg-type]
+
+        # int 作为参数
+        with pytest.raises(TypeError, match="cookie_dict"):
+            self.client.sign_xsc(123)  # type: ignore[arg-type]
+
+    def test_sign_xsc_non_string_keys(self):
+        """测试 sign_xsc 传入非字符串 key 的 cookie 时抛出异常"""
+        cookies = {
+            "a1": "test_a1_value",
+            "web_session": "test_session",
+            123: "value_with_int_key",  # 非字符串 key
+        }
+
+        with pytest.raises(TypeError, match="key"):
+            self.client.sign_xsc(cookies)  # type: ignore[dict-item]
+
+    def test_sign_xsc_none_cookie_dict(self):
+        """测试 sign_xsc 传入 None 时的参数校验行为"""
+        with pytest.raises(TypeError, match="cookie_dict"):
+            self.client.sign_xsc(None)  # type: ignore[arg-type]
+
+

这些测试假设 @validate_xs_common_params / RequestSignatureValidator.validate_cookie

  1. cookie_dict 不是 dict(list/int/None 情况)时抛出 TypeError,并且错误信息包含 cookie_dict
  2. 在存在非字符串键时抛出 TypeError,并且错误信息包含 key

如果实际的校验器使用了不同的异常类型或消息,请相应地调整:

  • pytest.raises(...) 中的异常类型;
  • match 模式,以与真实错误消息保持一致。

如果测试类中已经提供了 self.client.sign_xsc,则不需要额外的代码变更。

Original comment in English

suggestion (testing): Add negative tests for sign_xsc / validate_xs_common_params to cover type and value validation

Since sign_xsc is decorated with @validate_xs_common_params (which calls RequestSignatureValidator.validate_cookie), the current test only covers the valid dict case. Please add negative tests that:

  • Pass an unsupported type (e.g. list/int) as cookie_dict and assert a TypeError with the expected message.
  • Use a cookie dict with non-string keys to cover the key-type validation path.
  • Optionally, pass None (depending on the intended contract) and assert the expected behaviour.

This will exercise the validator’s error handling and lock in the parameter contract for sign_xsc.

Suggested implementation:

+    def test_sign_xsc_alias(self):
+        """测试 sign_xsc 别名方法"""
+        cookies = {
+            "a1": "test_a1_value",
+            "web_session": "test_session",
+        }
+
+        result = self.client.sign_xsc(cookies)
+
+        assert isinstance(result, str)
+        assert len(result) > 0
+
+    def test_sign_xsc_invalid_type(self):
+        """测试 sign_xsc 传入非 dict 类型参数时抛出异常"""
+        # list 作为参数
+        with pytest.raises(TypeError, match="cookie_dict"):
+            self.client.sign_xsc(["not", "a", "dict"])  # type: ignore[arg-type]
+
+        # int 作为参数
+        with pytest.raises(TypeError, match="cookie_dict"):
+            self.client.sign_xsc(123)  # type: ignore[arg-type]
+
+    def test_sign_xsc_non_string_keys(self):
+        """测试 sign_xsc 传入非字符串 key 的 cookie 时抛出异常"""
+        cookies = {
+            "a1": "test_a1_value",
+            "web_session": "test_session",
+            123: "value_with_int_key",  # 非字符串 key
+        }
+
+        with pytest.raises(TypeError, match="key"):
+            self.client.sign_xsc(cookies)  # type: ignore[dict-item]
+
+    def test_sign_xsc_none_cookie_dict(self):
+        """测试 sign_xsc 传入 None 时的参数校验行为"""
+        with pytest.raises(TypeError, match="cookie_dict"):
+            self.client.sign_xsc(None)  # type: ignore[arg-type]
+
+

These tests assume that @validate_xs_common_params / RequestSignatureValidator.validate_cookie:

  1. Raise TypeError when cookie_dict is not a dict (list/int/None case) and that the error message contains cookie_dict.
  2. Raise TypeError for non-string keys with an error message containing key.

If the actual validator uses different exception types or messages, adjust:

  • The exception class in pytest.raises(...).
  • The match patterns to align with the real error messages.

No other code changes should be required if self.client.sign_xsc is already available in the test class.

Comment on lines +148 to +157
class TestXsCommonSigner:
"""测试 XsCommonSigner 类"""

def setup_method(self):
self.signer = XsCommonSigner()

def test_sign_with_dict_only(self):
"""测试只接受字典参数"""
cookies = {
"a1": "test_a1_value",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): 强化 XsCommonSigner 的测试,用固定向量或结构性断言覆盖 b1/Base64 回归问题。

当前测试只验证输出非空以及缺少 a1 时是否抛出 KeyError,并没有真正验证修复后的基于字节的 b1 Base64 行为。

为了更好地覆盖此次回归修复,建议增加一个确定性的测试:

  • 使用固定的 cookie 字典和 CryptoConfig(具有确定性的 fingerprint),
  • 通过 FingerprintGenerator.generate_b1()XsCommonSigner 生成 b1
  • 断言 b1(或 x-s-common)与参考浏览器/VM 输出的已知正确值或前缀匹配(例如以 "I38r" 开头)。

建议实现:

class TestXsCommonSigner:
    """测试 XsCommonSigner 类"""

    def setup_method(self):
        self.signer = XsCommonSigner()

    def test_sign_with_dict_only(self):
        """测试只接受字典参数"""
        cookies = {
            "a1": "test_a1_value",
            "web_session": "test_session",
        }

        # 基本行为:返回非空字符串
        result = self.signer.sign(cookies)
        assert isinstance(result, str)
        assert result

    def test_b1_base64_regression_vector(self):
        """使用固定向量验证 b1/Base64 行为,防止回归"""
        cookies = {
            "a1": "fixed_a1_value",
            "web_session": "fixed_session_value",
        }

        # 使用固定的 CryptoConfig 以获得确定性的指纹
        crypto_config = CryptoConfig(
            fingerprint="fixed_fingerprint_for_test",
        )

        fp_gen = FingerprintGenerator(crypto_config=crypto_config)

        # 直接生成 b1,确保是稳定的 Base64 字节编码
        b1 = fp_gen.generate_b1(cookies)
        assert isinstance(b1, str)
        assert b1  # 非空

        # 结构性断言:b1 必须以已知前缀开头(来自参考浏览器/VM 输出)
        # 注意:前缀 "I38r" 需要根据实际参考输出进行同步
        assert b1.startswith("I38r")

        # 通过 XsCommonSigner 生成 x-s-common 头部,应与 b1 行为一致
        x_s_common = self.signer.sign(cookies=cookies, crypto_config=crypto_config)
        assert isinstance(x_s_common, str)
        assert x_s_common
        assert x_s_common.startswith("I38r")
  1. tests/test_cookie_parsing.py 顶部增加正确的导入(根据实际模块路径调整):

    from your_module_path import CryptoConfig, FingerprintGenerator

    或者如果它们在不同模块中,请分别导入。

  2. 确认 XsCommonSigner 的 API:

    • 如果当前签名方法不是 sign,而是例如 sign_xsc 或其他名字,请将
      result = self.signer.sign(cookies)
      x_s_common = self.signer.sign(cookies=cookies, crypto_config=crypto_config)
      替换为实际的方法名和参数签名。
  3. 如果 CryptoConfig 构造函数需要额外参数(例如密钥、盐值等),为测试构造一个完全确定性的配置(固定密钥/盐),并在 test_b1_base64_regression_vector 中补全这些参数。

  4. FingerprintGenerator 的初始化和 generate_b1 调用可能需要不同的参数名或额外上下文:

    • fp_gen = FingerprintGenerator(crypto_config=crypto_config)
      b1 = fp_gen.generate_b1(cookies)
      调整为当前项目中实际的初始化方式与方法签名。
  5. "I38r" 仅是示例前缀,请从参考浏览器/VM 输出中获取真实的 b1 值或前缀,并更新断言:

    assert b1.startswith("<真实前缀>")
    assert x_s_common.startswith("<真实前缀>")
  6. 如项目中已有 pytest fixture 提供 CryptoConfigFingerprintGenerator(例如 crypto_configfingerprint_generator),可以将测试改写为使用这些 fixture,而不是在测试中直接构造实例,以保持与现有测试风格一致。

Original comment in English

suggestion (testing): Strengthen XsCommonSigner tests with a fixed-vector or structural assertion that covers the b1/Base64 regression

Current tests only verify that the output is non-empty and that missing a1 raises KeyError, which doesn’t validate the corrected byte-based Base64 behaviour for b1.

To better cover this regression, please add a deterministic test that:

  • Uses a fixed cookie dict and CryptoConfig (with deterministic fingerprint),
  • Produces b1 via FingerprintGenerator.generate_b1() or XsCommonSigner, and
  • Asserts that b1 (or x-s-common) matches a known-good value or prefix from reference browser/VM output (e.g. starts with "I38r").

Suggested implementation:

class TestXsCommonSigner:
    """测试 XsCommonSigner 类"""

    def setup_method(self):
        self.signer = XsCommonSigner()

    def test_sign_with_dict_only(self):
        """测试只接受字典参数"""
        cookies = {
            "a1": "test_a1_value",
            "web_session": "test_session",
        }

        # 基本行为:返回非空字符串
        result = self.signer.sign(cookies)
        assert isinstance(result, str)
        assert result

    def test_b1_base64_regression_vector(self):
        """使用固定向量验证 b1/Base64 行为,防止回归"""
        cookies = {
            "a1": "fixed_a1_value",
            "web_session": "fixed_session_value",
        }

        # 使用固定的 CryptoConfig 以获得确定性的指纹
        crypto_config = CryptoConfig(
            fingerprint="fixed_fingerprint_for_test",
        )

        fp_gen = FingerprintGenerator(crypto_config=crypto_config)

        # 直接生成 b1,确保是稳定的 Base64 字节编码
        b1 = fp_gen.generate_b1(cookies)
        assert isinstance(b1, str)
        assert b1  # 非空

        # 结构性断言:b1 必须以已知前缀开头(来自参考浏览器/VM 输出)
        # 注意:前缀 "I38r" 需要根据实际参考输出进行同步
        assert b1.startswith("I38r")

        # 通过 XsCommonSigner 生成 x-s-common 头部,应与 b1 行为一致
        x_s_common = self.signer.sign(cookies=cookies, crypto_config=crypto_config)
        assert isinstance(x_s_common, str)
        assert x_s_common
        assert x_s_common.startswith("I38r")
  1. tests/test_cookie_parsing.py 顶部增加正确的导入(根据实际模块路径调整):

    from your_module_path import CryptoConfig, FingerprintGenerator

    或者如果它们在不同模块中,请分别导入。

  2. 确认 XsCommonSigner 的 API:

    • 如果当前签名方法不是 sign,而是例如 sign_xsc 或其他名字,请将
      result = self.signer.sign(cookies)
      x_s_common = self.signer.sign(cookies=cookies, crypto_config=crypto_config)
      替换为实际的方法名和参数签名。
  3. 如果 CryptoConfig 构造函数需要额外参数(例如密钥、盐值等),为测试构造一个完全确定性的配置(固定密钥/盐),并在 test_b1_base64_regression_vector 中补全这些参数。

  4. FingerprintGenerator 的初始化和 generate_b1 调用可能需要不同的参数名或额外上下文:

    • fp_gen = FingerprintGenerator(crypto_config=crypto_config)
      b1 = fp_gen.generate_b1(cookies)
      调整为当前项目中实际的初始化方式与方法签名。
  5. "I38r" 仅是示例前缀,请从参考浏览器/VM 输出中获取真实的 b1 值或前缀,并更新断言:

    assert b1.startswith("<真实前缀>")
    assert x_s_common.startswith("<真实前缀>")
  6. 如项目中已有 pytest fixture 提供 CryptoConfigFingerprintGenerator(例如 crypto_configfingerprint_generator),可以将测试改写为使用这些 fixture,而不是在测试中直接构造实例,以保持与现有测试风格一致。

cookie_string = "a1=your_a1_value; web_session=your_web_session; webId=your_web_id"
xs_common = client.sign_xsc(cookie_dict=cookie_string)

# 使用在请求中
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (typo): “使用在请求中”建议调整为更自然的表达,如“在请求中使用”。

这句注释略显别扭,建议改为“# 在请求中使用”,更符合中文表达习惯。

Suggested change
# 使用在请求中
# 在请求中使用
Original comment in English

suggestion (typo): “使用在请求中”建议调整为更自然的表达,如“在请求中使用”。

这句注释略显别扭,建议改为“# 在请求中使用”,更符合中文表达习惯。

Suggested change
# 使用在请求中
# 在请求中使用

@Cloxl Cloxl changed the base branch from fix/issue-70 to master December 12, 2025 04:39
@Cloxl Cloxl closed this Dec 12, 2025
@Cloxl
Copy link
Owner

Cloxl commented Dec 12, 2025

#75

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants