Skip to content

Conversation

@AlbertLighter
Copy link
Contributor

@AlbertLighter AlbertLighter commented Jan 14, 2026

  • 引入 SessionManager 来管理和维护一个模拟会话的状态,包括页面加载时间戳和各种计数器。
  • 重构签名方法以接受可选的 SessionManager 实例,从而生成更逼真的、包含单调递增计数器的签名。
  • 添加了相应的单元测试来验证新会话管理器的功能和向后兼容性。
    功能建议:风控参数模拟 #86

由 Sourcery 提供的总结

引入可感知会话的请求签名机制,在保留现有无状态行为的同时,生成更加真实、有状态的签名。

新功能:

  • 添加 SessionManagerSignState 抽象,用于维护每个会话的签名状态,例如时间戳和计数器。
  • 在签名和 Header 签名 API 中为 GET 和 POST 请求统一暴露可选的会话支持。

增强:

  • 扩展 CryptoProcessor 载荷构造逻辑,以接受可选的签名状态,从而生成基于会话的确定性载荷字段。

测试:

  • 添加覆盖会话驱动签名、无状态回退行为,以及会话状态在多次请求中演进的全面测试。
  • 添加一项集成风格测试,确保在提供 SessionManager 时客户端签名工作正常。
Original summary in English

Summary by Sourcery

Introduce session-aware request signing to generate more realistic, stateful signatures while preserving existing stateless behavior.

New Features:

  • Add SessionManager and SignState abstractions to maintain per-session signing state such as timestamps and counters.
  • Expose optional session support across signature and header signing APIs for both GET and POST requests.

Enhancements:

  • Extend CryptoProcessor payload construction to accept an optional signing state for deterministic, session-based payload fields.

Tests:

  • Add comprehensive tests covering session-driven signing, stateless fallback behavior, and evolution of session state across multiple requests.
  • Add an integration-style test ensuring client signing works correctly when a SessionManager is supplied.
Original summary in English

由 Sourcery 提供的总结

引入可感知会话的请求签名机制,在保留现有无状态行为的同时,生成更加真实、有状态的签名。

新功能:

  • 添加 SessionManagerSignState 抽象,用于维护每个会话的签名状态,例如时间戳和计数器。
  • 在签名和 Header 签名 API 中为 GET 和 POST 请求统一暴露可选的会话支持。

增强:

  • 扩展 CryptoProcessor 载荷构造逻辑,以接受可选的签名状态,从而生成基于会话的确定性载荷字段。

测试:

  • 添加覆盖会话驱动签名、无状态回退行为,以及会话状态在多次请求中演进的全面测试。
  • 添加一项集成风格测试,确保在提供 SessionManager 时客户端签名工作正常。
Original summary in English

Summary by Sourcery

Introduce session-aware request signing to generate more realistic, stateful signatures while preserving existing stateless behavior.

New Features:

  • Add SessionManager and SignState abstractions to maintain per-session signing state such as timestamps and counters.
  • Expose optional session support across signature and header signing APIs for both GET and POST requests.

Enhancements:

  • Extend CryptoProcessor payload construction to accept an optional signing state for deterministic, session-based payload fields.

Tests:

  • Add comprehensive tests covering session-driven signing, stateless fallback behavior, and evolution of session state across multiple requests.
  • Add an integration-style test ensuring client signing works correctly when a SessionManager is supplied.

- 引入 SessionManager 来管理和维护一个模拟会话的状态,包括页面加载时间戳和各种计数器。
- 重构签名方法以接受可选的 SessionManager 实例,从而生成更逼真的、包含单调递增计数器的签名。
- 添加了相应的单元测试来验证新会话管理器的功能和向后兼容性。
@sourcery-ai
Copy link

sourcery-ai bot commented Jan 14, 2026

审查者指南

通过引入 SessionManager / SignState 抽象,并在客户端签名 API 中贯穿一个可选的 session 参数直到加密负载构建器,实现了“会话感知”的签名流程。这样一来,签名既可以像之前一样以无状态方式生成,也可以基于每个会话的确定性状态生成,并添加了测试覆盖这两种行为。

带可选 SessionManager 的会话感知签名流程时序图

sequenceDiagram
    actor UserCode
    participant Xhshow
    participant SessionManager
    participant CryptoProcessor
    participant BitOps
    participant B64Encoder

    UserCode->>Xhshow: sign_headers(method, uri, cookies, xsec_appid, params/payload, timestamp, session)
    Xhshow->>Xhshow: _parse_cookies(cookies)
    Xhshow->>Xhshow: extract a1 from cookie_dict

    alt session provided
        Xhshow->>SessionManager: get_current_state(uri)
        SessionManager-->>Xhshow: SignState(page_load_timestamp, sequence_value, window_props_length, uri_length)
        Xhshow->>CryptoProcessor: build_payload_array(d_value, a1_value, xsec_appid, content_string, timestamp, sign_state)
    else no session
        Xhshow->>CryptoProcessor: build_payload_array(d_value, a1_value, xsec_appid, content_string, timestamp, sign_state=None)
    end

    CryptoProcessor->>CryptoProcessor: env_fingerprint_a(timestamp)
    alt sign_state provided
        CryptoProcessor->>CryptoProcessor: env_fingerprint_b(sign_state.page_load_timestamp)
        CryptoProcessor->>CryptoProcessor: use sign_state.sequence_value
        CryptoProcessor->>CryptoProcessor: use sign_state.window_props_length
        CryptoProcessor->>CryptoProcessor: use sign_state.uri_length
    else no sign_state
        CryptoProcessor->>CryptoProcessor: generate_random_byte_in_range for time_offset
        CryptoProcessor->>CryptoProcessor: env_fingerprint_b(timestamp - time_offset)
        CryptoProcessor->>CryptoProcessor: generate_random_byte_in_range for sequence_value
        CryptoProcessor->>CryptoProcessor: generate_random_byte_in_range for window_props_length
        CryptoProcessor->>CryptoProcessor: len(string_param) for uri_length
    end

    CryptoProcessor-->>Xhshow: payload_array
    Xhshow->>BitOps: xor_transform_array(payload_array)
    BitOps-->>Xhshow: xor_result
    Xhshow->>B64Encoder: encode_x3(xor_result[0..123])
    B64Encoder-->>Xhshow: x3_signature
    Xhshow->>Xhshow: build signature_data using SIGNATURE_DATA_TEMPLATE and x3_signature
    Xhshow->>B64Encoder: encode(json.dumps(signature_data))
    B64Encoder-->>Xhshow: encoded_signature
    Xhshow-->>UserCode: headers with x_s, x_s_common, x_t, trace ids
Loading

SessionManager、SignState、Xhshow 和 CryptoProcessor 的类图

classDiagram
    class SignState {
        <<NamedTuple>>
        +int page_load_timestamp
        +int sequence_value
        +int window_props_length
        +int uri_length
    }

    class SessionManager {
        -int page_load_timestamp
        -int sequence_value
        -int window_props_length
        +SessionManager()
        +update_state() void
        +get_current_state(uri str) SignState
    }

    class CryptoProcessor {
        +config
        +bit_ops
        +b64encoder
        +random_gen
        +build_payload_array(d_value str, a1_value str, app_identifier str, string_param str, timestamp float, sign_state SignState) list~int~
        +env_fingerprint_a(timestamp_ms int) list~int~
        +env_fingerprint_b(timestamp_ms int) list~int~
        +_int_to_le_bytes(value int, length int) list~int~
    }

    class Xhshow {
        +crypto_processor CryptoProcessor
        +sign_xs(method str, uri str, a1_value str, xsec_appid str, payload dict~str,Any~, timestamp float, session SessionManager) str
        +sign_xs_get(uri str, a1_value str, xsec_appid str, params dict~str,Any~, timestamp float, session SessionManager) str
        +sign_xs_post(uri str, a1_value str, xsec_appid str, payload dict~str,Any~, timestamp float, session SessionManager) str
        +sign_headers(method str, uri str, cookies dict~str,str~, xsec_appid str, params dict~str,Any~, payload dict~str,Any~, timestamp float, session SessionManager) dict~str,str~
        +sign_headers_get(uri str, cookies dict~str,str~, xsec_appid str, params dict~str,Any~, timestamp float, session SessionManager) dict~str,str~
        +sign_headers_post(uri str, cookies dict~str,str~, xsec_appid str, payload dict~str,Any~, timestamp float, session SessionManager) dict~str,str~
        +_build_content_string(method str, uri str, payload dict~str,Any~) str
        +_generate_d_value(content_string str) str
        +_parse_cookies(cookies dict~str,str~) dict~str,str~
    }

    class RequestSignatureValidator {
        +__call__(...) bool
        +validate_request_signature_params(...)
        +validate_get_signature_params(...)
        +validate_post_signature_params(...)
    }

    class validate_xs_common_params {
        <<decorator>>
    }

    class validate_get_signature_params {
        <<decorator>>
    }

    class validate_post_signature_params {
        <<decorator>>
    }

    SessionManager --> SignState : creates
    Xhshow --> CryptoProcessor : uses
    Xhshow --> SessionManager : optional session
    Xhshow --> SignState : passes to build_payload_array
    CryptoProcessor ..> SignState : TYPE_CHECKING reference
    validate_get_signature_params ..> RequestSignatureValidator : uses
    validate_post_signature_params ..> RequestSignatureValidator : uses
    validate_xs_common_params ..> RequestSignatureValidator : uses
Loading

文件级变更

变更 详情 文件
从客户端模块导出会话抽象,供外部使用。
  • 将 SessionManager 和 SignState 添加到客户端模块的导出中,使调用方可以管理每个会话的签名状态。
  • 在 sign_xs、sign_xs_get、sign_xs_post、sign_headers、sign_headers_get 和 sign_headers_post 上新增 session 参数,以支持有状态的签名流程。
src/xhshow/client.py
重构签名实现,以在加密负载中可选地使用每会话状态。
  • 修改 Xhshow.sign_xs:在提供 SessionManager 时从中获取 SignState,并在调用 crypto_processor.build_payload_array 时传入该状态。
  • 直接通过 xor_transform_array 和 encode_x3 从 payload 数组计算 x3,而不是使用之前的 _build_signature 辅助函数。
  • 更新 RequestSignatureValidator 的装饰器,使其接受并转发可选的 session 参数。
  • 扩展 CryptoProcessor.build_payload_array 以接受可选的 sign_state;在其存在时,使用其中的页面加载时间戳、序列计数器、窗口属性长度和 URI 长度,而不是生成随机值。
src/xhshow/client.py
src/xhshow/core/crypto.py
src/xhshow/utils/validators.py
实现 SessionManager 和 SignState,用以建模随会话演变的签名状态。
  • 新增 session 模块,将 SignState 定义为 NamedTuple,包含页面加载时间戳、序列值、窗口属性长度和 URI 长度。
  • 实现 SessionManager,使其初始化伪随机起始值,通过 update_state 演进这些值,并通过 get_current_state 为每个 URI 提供当前状态。
src/xhshow/session.py
添加测试以验证会话感知签名行为及向后兼容性。
  • 添加单元测试,mock CryptoProcessor,以断言在提供 session 时会将 sign_state 传入 build_payload_array,而在不提供时不会传入;GET 和 POST 签名辅助函数都要覆盖。
  • 添加测试以验证 SessionManager 的状态演进语义以及跨调用的 URI 长度处理。
  • 添加类似集成测试,确保在共享 SessionManager 的情况下重复调用 sign_xs_get 时能生成签名并走到会话路径。
tests/test_session.py
tests/test_url_utils.py

提示与命令

与 Sourcery 交互

  • 触发新审查: 在 pull request 中评论 @sourcery-ai review
  • 继续讨论: 直接回复 Sourcery 的审查评论。
  • 从审查评论生成 GitHub issue: 回复 Sourcery 的审查评论,请其基于该评论创建一个 issue。你也可以在审查评论下回复 @sourcery-ai issue 来从该评论创建 issue。
  • 生成 pull request 标题: 在 pull request 标题的任意位置写上 @sourcery-ai,即可随时生成标题。你也可以在 pull request 中评论 @sourcery-ai title 来(重新)生成标题。
  • 生成 pull request 摘要: 在 pull request 正文任意位置写上 @sourcery-ai summary,即可在该位置生成 PR 摘要。你也可以评论 @sourcery-ai summary 来在任意时间(重新)生成摘要。
  • 生成审查者指南: 在 pull request 中评论 @sourcery-ai guide,即可在任意时间(重新)生成审查者指南。
  • 解决所有 Sourcery 评论: 在 pull request 中评论 @sourcery-ai resolve,即可一次性解决所有 Sourcery 评论。如果你已经处理完所有评论且不想再看到它们,这会很有用。
  • 忽略所有 Sourcery 审查: 在 pull request 中评论 @sourcery-ai dismiss,即可忽略所有现有的 Sourcery 审查。尤其适合在想以一次全新审查重新开始时使用——别忘了评论 @sourcery-ai review 来触发新的审查!

自定义你的体验

访问你的 控制面板 以:

  • 启用或禁用审查特性,例如 Sourcery 生成的 pull request 摘要、审查者指南等。
  • 更改审查语言。
  • 添加、移除或编辑自定义审查指令。
  • 调整其他审查设置。

获得帮助

Original review guide in English

Reviewer's Guide

Introduces a session-aware signing flow by adding a SessionManager/SignState abstraction and threading an optional session parameter through the client signing APIs into the crypto payload builder so that signatures can be generated either in the original stateless way or using deterministic per-session state, with tests added to cover both behaviors.

Sequence diagram for session-aware signing flow with optional SessionManager

sequenceDiagram
    actor UserCode
    participant Xhshow
    participant SessionManager
    participant CryptoProcessor
    participant BitOps
    participant B64Encoder

    UserCode->>Xhshow: sign_headers(method, uri, cookies, xsec_appid, params/payload, timestamp, session)
    Xhshow->>Xhshow: _parse_cookies(cookies)
    Xhshow->>Xhshow: extract a1 from cookie_dict

    alt session provided
        Xhshow->>SessionManager: get_current_state(uri)
        SessionManager-->>Xhshow: SignState(page_load_timestamp, sequence_value, window_props_length, uri_length)
        Xhshow->>CryptoProcessor: build_payload_array(d_value, a1_value, xsec_appid, content_string, timestamp, sign_state)
    else no session
        Xhshow->>CryptoProcessor: build_payload_array(d_value, a1_value, xsec_appid, content_string, timestamp, sign_state=None)
    end

    CryptoProcessor->>CryptoProcessor: env_fingerprint_a(timestamp)
    alt sign_state provided
        CryptoProcessor->>CryptoProcessor: env_fingerprint_b(sign_state.page_load_timestamp)
        CryptoProcessor->>CryptoProcessor: use sign_state.sequence_value
        CryptoProcessor->>CryptoProcessor: use sign_state.window_props_length
        CryptoProcessor->>CryptoProcessor: use sign_state.uri_length
    else no sign_state
        CryptoProcessor->>CryptoProcessor: generate_random_byte_in_range for time_offset
        CryptoProcessor->>CryptoProcessor: env_fingerprint_b(timestamp - time_offset)
        CryptoProcessor->>CryptoProcessor: generate_random_byte_in_range for sequence_value
        CryptoProcessor->>CryptoProcessor: generate_random_byte_in_range for window_props_length
        CryptoProcessor->>CryptoProcessor: len(string_param) for uri_length
    end

    CryptoProcessor-->>Xhshow: payload_array
    Xhshow->>BitOps: xor_transform_array(payload_array)
    BitOps-->>Xhshow: xor_result
    Xhshow->>B64Encoder: encode_x3(xor_result[0..123])
    B64Encoder-->>Xhshow: x3_signature
    Xhshow->>Xhshow: build signature_data using SIGNATURE_DATA_TEMPLATE and x3_signature
    Xhshow->>B64Encoder: encode(json.dumps(signature_data))
    B64Encoder-->>Xhshow: encoded_signature
    Xhshow-->>UserCode: headers with x_s, x_s_common, x_t, trace ids
Loading

Class diagram for SessionManager, SignState, Xhshow, and CryptoProcessor

classDiagram
    class SignState {
        <<NamedTuple>>
        +int page_load_timestamp
        +int sequence_value
        +int window_props_length
        +int uri_length
    }

    class SessionManager {
        -int page_load_timestamp
        -int sequence_value
        -int window_props_length
        +SessionManager()
        +update_state() void
        +get_current_state(uri str) SignState
    }

    class CryptoProcessor {
        +config
        +bit_ops
        +b64encoder
        +random_gen
        +build_payload_array(d_value str, a1_value str, app_identifier str, string_param str, timestamp float, sign_state SignState) list~int~
        +env_fingerprint_a(timestamp_ms int) list~int~
        +env_fingerprint_b(timestamp_ms int) list~int~
        +_int_to_le_bytes(value int, length int) list~int~
    }

    class Xhshow {
        +crypto_processor CryptoProcessor
        +sign_xs(method str, uri str, a1_value str, xsec_appid str, payload dict~str,Any~, timestamp float, session SessionManager) str
        +sign_xs_get(uri str, a1_value str, xsec_appid str, params dict~str,Any~, timestamp float, session SessionManager) str
        +sign_xs_post(uri str, a1_value str, xsec_appid str, payload dict~str,Any~, timestamp float, session SessionManager) str
        +sign_headers(method str, uri str, cookies dict~str,str~, xsec_appid str, params dict~str,Any~, payload dict~str,Any~, timestamp float, session SessionManager) dict~str,str~
        +sign_headers_get(uri str, cookies dict~str,str~, xsec_appid str, params dict~str,Any~, timestamp float, session SessionManager) dict~str,str~
        +sign_headers_post(uri str, cookies dict~str,str~, xsec_appid str, payload dict~str,Any~, timestamp float, session SessionManager) dict~str,str~
        +_build_content_string(method str, uri str, payload dict~str,Any~) str
        +_generate_d_value(content_string str) str
        +_parse_cookies(cookies dict~str,str~) dict~str,str~
    }

    class RequestSignatureValidator {
        +__call__(...) bool
        +validate_request_signature_params(...)
        +validate_get_signature_params(...)
        +validate_post_signature_params(...)
    }

    class validate_xs_common_params {
        <<decorator>>
    }

    class validate_get_signature_params {
        <<decorator>>
    }

    class validate_post_signature_params {
        <<decorator>>
    }

    SessionManager --> SignState : creates
    Xhshow --> CryptoProcessor : uses
    Xhshow --> SessionManager : optional session
    Xhshow --> SignState : passes to build_payload_array
    CryptoProcessor ..> SignState : TYPE_CHECKING reference
    validate_get_signature_params ..> RequestSignatureValidator : uses
    validate_post_signature_params ..> RequestSignatureValidator : uses
    validate_xs_common_params ..> RequestSignatureValidator : uses
Loading

File-Level Changes

Change Details Files
Expose session abstractions from the client module for external use.
  • Add SessionManager and SignState to the client module exports so callers can manage per-session signing state.
  • Introduce a new session parameter on sign_xs, sign_xs_get, sign_xs_post, sign_headers, sign_headers_get, and sign_headers_post to support stateful signing flows.
src/xhshow/client.py
Refactor signing implementation to optionally consume per-session state in the crypto payload.
  • Change Xhshow.sign_xs to obtain a SignState from SessionManager when provided and call crypto_processor.build_payload_array with that state.
  • Compute x3 directly from the payload array via xor_transform_array and encode_x3 instead of using the previous _build_signature helper.
  • Update RequestSignatureValidator decorators to accept and forward the optional session parameter.
  • Extend CryptoProcessor.build_payload_array to accept an optional sign_state and, when present, use its page load timestamp, sequence counter, window props length, and URI length instead of generating random values.
src/xhshow/client.py
src/xhshow/core/crypto.py
src/xhshow/utils/validators.py
Implement SessionManager and SignState to model evolving per-session signing state.
  • Add a new session module defining SignState as a NamedTuple carrying page load timestamp, sequence value, window props length, and URI length.
  • Implement SessionManager to initialize pseudo-random starting values, evolve them via update_state, and expose them for each URI through get_current_state.
src/xhshow/session.py
Add tests to validate session-aware signing behavior and backward compatibility.
  • Add unit tests that mock CryptoProcessor to assert sign_state is passed into build_payload_array when a session is supplied and omitted otherwise for both GET and POST signing helpers.
  • Add tests to verify SessionManager state evolution semantics and URI length handling across calls.
  • Add an integration-like test ensuring repeated sign_xs_get calls with a shared SessionManager generate signatures and exercise the session path.
tests/test_session.py
tests/test_url_utils.py

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,我发现了 8 个问题,并且给出了一些总体反馈:

  • src/xhshow/client.py 中,__all__ 包含 "SignState",但该符号在此模块中既没有被导入也没有被定义,因此 from xhshow.client import SignState 会失败;要么从 .session 中导入它,要么从 __all__ 中移除。
  • SessionManager 的 API 有点令人困惑:文档里说调用方应该在签名前调用 update_state(),但 get_current_state() 内部也会调用 update_state(),如果两者都被使用就会导致状态被更新两次;可以考虑只在一个地方显式推进状态(例如仅在 update_state() 中,而不是在 get_current_state() 内部),或者调整 docstring 来澄清预期用法。
  • tests/test_url_utils.py 中的 test_client_sign_with_session 里,print(signature) 会在测试运行期间产生噪声输出,并且没有用于断言;可以安全地移除。
面向 AI Agent 的提示
请根据本次代码评审中的评论进行修改:

## 总体评论
-`src/xhshow/client.py` 中,`__all__` 包含 `"SignState"`,但该符号在此模块中既没有被导入也没有被定义,因此 `from xhshow.client import SignState` 会失败;要么从 `.session` 中导入它,要么从 `__all__` 中移除。
- `SessionManager` 的 API 有点令人困惑:文档里说调用方应该在签名前调用 `update_state()`,但 `get_current_state()` 内部也会调用 `update_state()`,如果两者都被使用就会导致状态被更新两次;可以考虑只在一个地方显式推进状态(例如仅在 `update_state()` 中,而不是在 `get_current_state()` 内部),或者调整 docstring 来澄清预期用法。
-`tests/test_url_utils.py` 中的 `test_client_sign_with_session` 里,`print(signature)` 会在测试运行期间产生噪声输出,并且没有用于断言;可以安全地移除。

## 逐条评论

### Comment 1
<location> `src/xhshow/client.py:23` </location>
<code_context>
+if TYPE_CHECKING:
+    from .session import SessionManager
+
+__all__ = ["Xhshow", "SessionManager", "SignState"]


</code_context>

<issue_to_address>
**issue (bug_risk):** SessionManager 和 SignState 被列在 __all__ 中,但在这个模块里实际上从未绑定。

由于这些名字只在 `TYPE_CHECKING` 分支下被导入,它们在运行时并不存在于 `xhshow.client` 上,所以尽管它们在 `__all__` 中,`from xhshow.client import SessionManager` 仍然会失败。请在运行时导入并重新导出它们,或者把它们从 `__all__` 中移除,以保证公共 API 与实际可用的内容保持一致。
</issue_to_address>

### Comment 2
<location> `src/xhshow/client.py:148` </location>
<code_context>
-        signature_data["x3"] = self.crypto_processor.config.X3_PREFIX + self._build_signature(
-            d_value, a1_value, xsec_appid, content_string, timestamp
+
+        sign_state = session.get_current_state(content_string) if session else None
+
+        payload_array = self.crypto_processor.build_payload_array(
</code_context>

<issue_to_address>
**issue (bug_risk):** 对 get_current_state 使用 content_string 而不是裸 URI,可能会破坏预期的 URI 长度建模。

`SessionManager.get_current_state` 期望的是原始 URI,并使用 `len(uri)` 作为 `SignState.uri_length`。传入 `content_string`(它把 method/URI/payload 组合在一起)意味着 `uri_length` 将不再匹配真实 URI。为了让 URI 长度建模保持准确并与 docstring 一致,这里应该传入真实的 `uri`,而 `_build_content_string` 可以继续单独使用组合后的值。
</issue_to_address>

### Comment 3
<location> `src/xhshow/session.py:35-38` </location>
<code_context>
+
+        This method should be called before each signing operation.
+        """
+        # Simulate realistic counter increments
+        # self.sequence_value += random.randint(0, 1)
+        self.window_props_length += random.randint(1, 10)
+        self.uri_length += random.randint(0, 2)
</code_context>

<issue_to_address>
**suggestion (bug_risk):** sequence_value 从未被更新,这削弱了将其作为会话状态演化部分进行追踪的意义。

`SignState` 和 docstring 表明 `sequence_value` 应该在每个请求中发生变化,但 `update_state` 只更新了 `window_props_length``uri_length`。由于递增操作被注释掉,同一会话中的所有签名操作都会使用相同的 `sequence_value`。如果这不是有意为之,请重新启用递增(如有需要可添加适当的边界)以便状态在每次调用之间真正演化。

```suggestion
        # Simulate realistic counter increments
        self.sequence_value += random.randint(0, 1)
        self.window_props_length += random.randint(1, 10)
        self.uri_length += random.randint(0, 2)
```
</issue_to_address>

### Comment 4
<location> `src/xhshow/session.py:27` </location>
<code_context>
+        self.page_load_timestamp: int = int(time.time() * 1000)
+        self.sequence_value: int = random.randint(15, 17)
+        self.window_props_length: int = random.randint(1000, 2000)
+        self.uri_length: int = random.randint(200, 400)
+
+    def update_state(self):
</code_context>

<issue_to_address>
**suggestion:** 存储的 uri_length 计数实际上没有被有效使用,因为 get_current_state 总是用 len(uri)。

`uri_length` 在初始化时被设置,并在 `update_state` 中更新,但 `get_current_state` 总是设置 `uri_length=len(uri)` 而不是使用存储的值,使得该字段实际上是多余的。请要么移除 `uri_length` 及其更新逻辑,要么修改 `get_current_state``self.uri_length` 读取(可以选择与 `len(uri)` 一起使用),如果它本来就应该代表演化中的 URI 状态。

建议实现:

```python
        self.page_load_timestamp: int = int(time.time() * 1000)
        self.sequence_value: int = random.randint(15, 17)
        self.window_props_length: int = random.randint(1000, 2000)

```

```python
        # Simulate realistic counter increments
        # self.sequence_value += random.randint(0, 1)
        self.window_props_length += random.randint(1, 10)

````src/xhshow/session.py` 的其余部分(以及所有调用方)中搜索对 `uri_length` 的剩余引用(例如作为属性、在 `get_current_state` 中或在类型注解中)并将其移除。之后,确保 `get_current_state`(或等价方法)只使用 `len(uri)` 或其他可用字段,而不会再依赖 `self.uri_length` 的存在。
</issue_to_address>

### Comment 5
<location> `src/xhshow/client.py:120-121` </location>
<code_context>
             timestamp: Unix timestamp in seconds (defaults to current time)
+            session: Optional session manager for stateful signing.

         Returns:
             str: Complete signature string
</code_context>

<issue_to_address>
**nitpick (typo):** sign_headers_post 的 docstring 在 x-b3-traceid 命名上与其他方法不一致。

请更新 docstring,使其使用 `x-b3-traceid`(如果这仍然是实际的 header 名称),这样该 header 名称在所有辅助方法中保持一致,从而让 API 文档更清晰。

```suggestion
        """
        Generate request signature (supports GET and POST) for use in the
        `x-b3-traceid` header.
                - GET request: params value
                - POST request: payload value
            timestamp: Unix timestamp in seconds (defaults to current time)
            session: Optional session manager for stateful signing.
```
</issue_to_address>

### Comment 6
<location> `tests/test_url_utils.py:154-163` </location>
<code_context>
                 params="invalid",  # type: ignore
             )
+
+    def test_client_sign_with_session(self):
+        client = Xhshow()
+        session = SessionManager()
+        for i in range(10):
+            signature = client.sign_xs_get(
+                uri="/api/sns/web/v1/user_posted",
+                a1_value="test_a1_value",
+                params={"num": "30"},
+                session=session,
+            )
+            session.update_state()
+            print(signature)
+
+        assert signature.startswith("XYS_")
</code_context>

<issue_to_address>
**suggestion (testing):** 加强基于 Session 的签名测试并避免副作用。

这个测试只断言签名以 `"XYS_"` 开头,而这一点已经由实现保证了,并不能验证基于 Session 的签名与无状态情况之间的差异。它同时在循环中调用 `session.update_state()`(尽管 `SessionManager.get_current_state` 已经会推进状态),并打印签名。

建议:
- 去掉循环中的显式 `session.update_state()` 调用(或者清楚地说明为何需要两套机制同时存在)。
- 移除 `print(signature)`,以避免测试中产生噪声副作用。
- 加强断言,例如收集所有签名并断言它们并非全部相同,或者检查某些依赖状态的属性。也可以对比有无 `SessionManager` 时的行为,以确认新流程被真正覆盖。
</issue_to_address>

### Comment 7
<location> `tests/test_session.py:48-57` </location>
<code_context>
+    cookies = {"a1": "test_a1", "web_session": "test_session"}
+    content_string = uri  # Simplified for this test
+
+    # Perform signing
+    client.sign_headers_get(uri=uri, cookies=cookies, session=session)
+
+    # Get the expected state from the session
+    expected_state = session.get_current_state(content_string)
+
+    # Assert that build_payload_array was called
+    mock_crypto_processor.build_payload_array.assert_called_once()
+
+    # Get the actual arguments passed to the mock
+    _, kwargs = mock_crypto_processor.build_payload_array.call_args
+    actual_state = kwargs.get("sign_state")
+
+    # Verify the state matches
+    assert actual_state is not None
+    assert actual_state.page_load_timestamp == expected_state.page_load_timestamp
+    assert actual_state.sequence_value == expected_state.sequence_value
+    assert actual_state.window_props_length == expected_state.window_props_length
+    assert actual_state.uri_length == len(content_string)
+
+
</code_context>

<issue_to_address>
**issue (testing):** 为了避免测试不稳定,不要在计算 expected_state 时再次调用 `get_current_state``client.sign_headers_get(..., session=session)` 已经调用过一次 `session.get_current_state(content_string)`,并通过 `update_state` 改变了状态。为了得到 `expected_state` 再调用一次 `session.get_current_state(content_string)` 会第二次推进状态,因此 `expected_state` 可能与实际传给 `build_payload_array` 的值不一致,一旦 `update_state` 的逻辑发生演化,就可能导致测试不稳定。

与其重新计算 `expected_state`,不如直接对 `actual_state` 进行断言,例如:
- `assert isinstance(actual_state, SignState)`
- `assert actual_state.page_load_timestamp == session.page_load_timestamp`
- `assert actual_state.uri_length == len(content_string)`
- 还可以可选地检查单调属性,例如 `window_props_length` / `uri_length` 相对于初始值是否 `>=`。

这样可以在不依赖额外有副作用调用的前提下,验证状态传递是否正确。
</issue_to_address>

### Comment 8
<location> `tests/test_session.py:69-78` </location>
<code_context>
+    assert actual_state.uri_length == len(content_string)
+
+
+def test_signing_without_session(mock_crypto_processor):
+    """
+    Verify that signing falls back to the old method when no session is provided.
+    """
+    client = Xhshow()
+    uri = "/api/sns/web/v1/user/posted"
+    cookies = {"a1": "test_a1", "web_session": "test_session"}
+
+    # Perform signing without a session
+    client.sign_headers_get(uri=uri, cookies=cookies)
+
+    # Assert that build_payload_array was called
+    mock_crypto_processor.build_payload_array.assert_called_once()
+
+    # Get the actual arguments passed to the mock
+    _, kwargs = mock_crypto_processor.build_payload_array.call_args
+    actual_state = kwargs.get("sign_state")
+
+    # Verify that no state was passed (it should be None)
+    assert actual_state is None
</code_context>

<issue_to_address>
**suggestion (testing):** 建议为使用 `SessionManager` 的 POST 变体增加测试。

当前测试只覆盖了带 / 不带 `SessionManager``sign_headers_get`,但这个 PR 也为 `sign_headers_post` / `sign_xs_post` 添加了 `session` 支持。请增加对应的 POST 测试:

- `test_signing_with_session_post`:在调用 `sign_headers_post(..., session=session)` 时,断言 `sign_state` 被传递给 `build_payload_array`- `test_signing_without_session_post`:在调用 `sign_headers_post(..., session=None)` 时,断言 `sign_state``None`。

这将同时验证 POST 请求中新引入的 Session 行为。

建议实现:

```python
    # Get the actual arguments passed to the mock
    _, kwargs = mock_crypto_processor.build_payload_array.call_args
    actual_state = kwargs.get("sign_state")

    # Verify that no state was passed (it should be None)
    assert actual_state is None


def test_signing_with_session_post(mock_crypto_processor, mock_session_manager):
    """
    Verify that signing uses the session state when provided for POST requests.
    """
    client = Xhshow()
    uri = "/api/sns/web/v1/user/posted"
    cookies = {"a1": "test_a1", "web_session": "test_session"}
    data = {"foo": "bar"}

    # Obtain a session (using the same pattern as the GET tests)
    session = mock_session_manager.get_or_create_session.return_value

    # Perform signing with a session for POST
    client.sign_headers_post(uri=uri, data=data, cookies=cookies, session=session)

    # Assert that build_payload_array was called
    mock_crypto_processor.build_payload_array.assert_called_once()

    # Get the actual arguments passed to the mock
    _, kwargs = mock_crypto_processor.build_payload_array.call_args
    actual_state = kwargs.get("sign_state")

    # Verify that session state was passed
    assert actual_state is not None


def test_signing_without_session_post(mock_crypto_processor):
    """
    Verify that signing falls back to the old method when no session is provided for POST requests.
    """
    client = Xhshow()
    uri = "/api/sns/web/v1/user/posted"
    cookies = {"a1": "test_a1", "web_session": "test_session"}
    data = {"foo": "bar"}

    # Perform signing without a session for POST
    client.sign_headers_post(uri=uri, data=data, cookies=cookies, session=None)

    # Assert that build_payload_array was called
    mock_crypto_processor.build_payload_array.assert_called_once()

    # Get the actual arguments passed to the mock
    _, kwargs = mock_crypto_processor.build_payload_array.call_args
    actual_state = kwargs.get("sign_state")

    # Verify that no state was passed (it should be None)
    assert actual_state is None


```

如果现有的 GET 测试使用了不同的 fixture 名称或获取 Session 的方式(例如 `session = mock_session_manager.return_value` 或单独的 `session` fixture),请相应地调整 `test_signing_with_session_post` 的参数和 Session 初始化方式,以与文件中现有约定保持一致,使这些测试能够无缝集成。
</issue_to_address>

Sourcery 对开源项目免费使用——如果你觉得这些评审有帮助,可以考虑分享给更多人 ✨
帮我变得更有用!请对每条评论点 👍 或 👎,我会根据反馈改进后续的评审质量。
Original comment in English

Hey - I've found 8 issues, and left some high level feedback:

  • In src/xhshow/client.py, __all__ includes "SignState" but the symbol is never imported or defined in this module, so from xhshow.client import SignState will fail; either import it from .session or remove it from __all__.
  • The SessionManager API is a bit confusing: update_state() is documented as something the caller should invoke before signing, but get_current_state() also calls update_state() internally, which can lead to double updates if both are used; consider making state advancement explicit in one place (e.g., only in update_state() and not inside get_current_state()) or adjusting the docstrings to clarify the intended usage.
  • In test_client_sign_with_session (in tests/test_url_utils.py), the print(signature) call introduces noisy output during test runs and isn’t used in assertions; it can be safely removed.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `src/xhshow/client.py`, `__all__` includes `"SignState"` but the symbol is never imported or defined in this module, so `from xhshow.client import SignState` will fail; either import it from `.session` or remove it from `__all__`.
- The `SessionManager` API is a bit confusing: `update_state()` is documented as something the caller should invoke before signing, but `get_current_state()` also calls `update_state()` internally, which can lead to double updates if both are used; consider making state advancement explicit in one place (e.g., only in `update_state()` and not inside `get_current_state()`) or adjusting the docstrings to clarify the intended usage.
- In `test_client_sign_with_session` (in `tests/test_url_utils.py`), the `print(signature)` call introduces noisy output during test runs and isn’t used in assertions; it can be safely removed.

## Individual Comments

### Comment 1
<location> `src/xhshow/client.py:23` </location>
<code_context>
+if TYPE_CHECKING:
+    from .session import SessionManager
+
+__all__ = ["Xhshow", "SessionManager", "SignState"]


</code_context>

<issue_to_address>
**issue (bug_risk):** SessionManager and SignState are listed in __all__ but never actually bound in this module.

Because these names are only imported under `TYPE_CHECKING`, they don’t exist at runtime on `xhshow.client`, so `from xhshow.client import SessionManager` will fail despite being in `__all__`. Either import and re-export them at runtime, or remove them from `__all__` to keep the public API consistent with what’s actually available.
</issue_to_address>

### Comment 2
<location> `src/xhshow/client.py:148` </location>
<code_context>
-        signature_data["x3"] = self.crypto_processor.config.X3_PREFIX + self._build_signature(
-            d_value, a1_value, xsec_appid, content_string, timestamp
+
+        sign_state = session.get_current_state(content_string) if session else None
+
+        payload_array = self.crypto_processor.build_payload_array(
</code_context>

<issue_to_address>
**issue (bug_risk):** Using content_string instead of the bare URI for get_current_state likely breaks the intended URI length modeling.

`SessionManager.get_current_state` expects the raw URI and uses `len(uri)` for `SignState.uri_length`. Passing `content_string` (which combines method/URI/payload) means `uri_length` no longer matches the actual URI. To keep the URI length modeling accurate and consistent with the docstring, this should receive the real `uri`, while `_build_content_string` can continue to use the composed value separately.
</issue_to_address>

### Comment 3
<location> `src/xhshow/session.py:35-38` </location>
<code_context>
+
+        This method should be called before each signing operation.
+        """
+        # Simulate realistic counter increments
+        # self.sequence_value += random.randint(0, 1)
+        self.window_props_length += random.randint(1, 10)
+        self.uri_length += random.randint(0, 2)
</code_context>

<issue_to_address>
**suggestion (bug_risk):** sequence_value is never updated, which undermines the purpose of tracking it as evolving session state.

`SignState` and the docstring imply `sequence_value` should change per request, but `update_state` only updates `window_props_length` and `uri_length`. With the increment commented out, all sign operations in a session use the same `sequence_value`. If this isn’t intentional, re-enable the increment (with appropriate bounds if needed) so the state actually evolves between calls.

```suggestion
        # Simulate realistic counter increments
        self.sequence_value += random.randint(0, 1)
        self.window_props_length += random.randint(1, 10)
        self.uri_length += random.randint(0, 2)
```
</issue_to_address>

### Comment 4
<location> `src/xhshow/session.py:27` </location>
<code_context>
+        self.page_load_timestamp: int = int(time.time() * 1000)
+        self.sequence_value: int = random.randint(15, 17)
+        self.window_props_length: int = random.randint(1000, 2000)
+        self.uri_length: int = random.randint(200, 400)
+
+    def update_state(self):
</code_context>

<issue_to_address>
**suggestion:** The stored uri_length counter is effectively unused because get_current_state always uses len(uri).

`uri_length` is initialized and updated in `update_state`, but `get_current_state` always sets `uri_length=len(uri)` instead of using the stored value, making the field effectively redundant. Please either remove `uri_length` and its updates, or change `get_current_state` to read from `self.uri_length` (optionally alongside `len(uri)`) if it’s meant to represent evolving URI state.

Suggested implementation:

```python
        self.page_load_timestamp: int = int(time.time() * 1000)
        self.sequence_value: int = random.randint(15, 17)
        self.window_props_length: int = random.randint(1000, 2000)

```

```python
        # Simulate realistic counter increments
        # self.sequence_value += random.randint(0, 1)
        self.window_props_length += random.randint(1, 10)

```

Search the rest of `src/xhshow/session.py` (and any callers) for any remaining references to `uri_length` (e.g., as an attribute, in `get_current_state`, or in type hints) and remove them. After that, ensure that `get_current_state` (or any equivalent method) only uses `len(uri)` or other available fields, without expecting `self.uri_length` to exist.
</issue_to_address>

### Comment 5
<location> `src/xhshow/client.py:120-121` </location>
<code_context>
             timestamp: Unix timestamp in seconds (defaults to current time)
+            session: Optional session manager for stateful signing.

         Returns:
             str: Complete signature string
</code_context>

<issue_to_address>
**nitpick (typo):** Docstring for sign_headers_post is inconsistent with other methods regarding x-b3-traceid naming.

Please update the docstring to use `x-b3-traceid` (if that’s still the actual header name) so the header name is consistent across all helper methods and the API docs remain clear.

```suggestion
        """
        Generate request signature (supports GET and POST) for use in the
        `x-b3-traceid` header.
                - GET request: params value
                - POST request: payload value
            timestamp: Unix timestamp in seconds (defaults to current time)
            session: Optional session manager for stateful signing.
```
</issue_to_address>

### Comment 6
<location> `tests/test_url_utils.py:154-163` </location>
<code_context>
                 params="invalid",  # type: ignore
             )
+
+    def test_client_sign_with_session(self):
+        client = Xhshow()
+        session = SessionManager()
+        for i in range(10):
+            signature = client.sign_xs_get(
+                uri="/api/sns/web/v1/user_posted",
+                a1_value="test_a1_value",
+                params={"num": "30"},
+                session=session,
+            )
+            session.update_state()
+            print(signature)
+
+        assert signature.startswith("XYS_")
</code_context>

<issue_to_address>
**suggestion (testing):** Strengthen the session-based signing test and avoid side effects

This test only asserts that the signature starts with `"XYS_"`, which is already guaranteed by the implementation and doesn’t verify that session-based signing differs from the stateless case. It also both calls `session.update_state()` (even though `SessionManager.get_current_state` already advances state) and prints the signature.

I’d recommend:
- Dropping the explicit `session.update_state()` in the loop (or clearly documenting why both mechanisms are needed).
- Removing `print(signature)` to avoid noisy side effects in tests.
- Strengthening assertions, e.g. collecting all signatures and asserting they’re not all identical, or otherwise checking a state-dependent property. Optionally, compare behavior with and without `SessionManager` to confirm the new flow is exercised.
</issue_to_address>

### Comment 7
<location> `tests/test_session.py:48-57` </location>
<code_context>
+    cookies = {"a1": "test_a1", "web_session": "test_session"}
+    content_string = uri  # Simplified for this test
+
+    # Perform signing
+    client.sign_headers_get(uri=uri, cookies=cookies, session=session)
+
+    # Get the expected state from the session
+    expected_state = session.get_current_state(content_string)
+
+    # Assert that build_payload_array was called
+    mock_crypto_processor.build_payload_array.assert_called_once()
+
+    # Get the actual arguments passed to the mock
+    _, kwargs = mock_crypto_processor.build_payload_array.call_args
+    actual_state = kwargs.get("sign_state")
+
+    # Verify the state matches
+    assert actual_state is not None
+    assert actual_state.page_load_timestamp == expected_state.page_load_timestamp
+    assert actual_state.sequence_value == expected_state.sequence_value
+    assert actual_state.window_props_length == expected_state.window_props_length
+    assert actual_state.uri_length == len(content_string)
+
+
</code_context>

<issue_to_address>
**issue (testing):** Avoid flakiness by not calling `get_current_state` a second time when computing `expected_state`

`client.sign_headers_get(..., session=session)` already calls `session.get_current_state(content_string)`, which mutates state via `update_state`. Calling `session.get_current_state(content_string)` again for `expected_state` advances the state a second time, so `expected_state` may not match what was actually passed to `build_payload_array`, making the test flaky if `update_state` evolves.

Instead of recomputing `expected_state`, assert directly on `actual_state`, e.g.:
- `assert isinstance(actual_state, SignState)`
- `assert actual_state.page_load_timestamp == session.page_load_timestamp`
- `assert actual_state.uri_length == len(content_string)`
- Optionally check monotonic properties like `window_props_length` / `uri_length` being `>=` initial values.

That way the test verifies the state wiring without relying on an extra mutating call.
</issue_to_address>

### Comment 8
<location> `tests/test_session.py:69-78` </location>
<code_context>
+    assert actual_state.uri_length == len(content_string)
+
+
+def test_signing_without_session(mock_crypto_processor):
+    """
+    Verify that signing falls back to the old method when no session is provided.
+    """
+    client = Xhshow()
+    uri = "/api/sns/web/v1/user/posted"
+    cookies = {"a1": "test_a1", "web_session": "test_session"}
+
+    # Perform signing without a session
+    client.sign_headers_get(uri=uri, cookies=cookies)
+
+    # Assert that build_payload_array was called
+    mock_crypto_processor.build_payload_array.assert_called_once()
+
+    # Get the actual arguments passed to the mock
+    _, kwargs = mock_crypto_processor.build_payload_array.call_args
+    actual_state = kwargs.get("sign_state")
+
+    # Verify that no state was passed (it should be None)
+    assert actual_state is None
</code_context>

<issue_to_address>
**suggestion (testing):** Consider adding tests for POST variants using `SessionManager`

The current tests only cover `sign_headers_get` with/without `SessionManager`, but this PR also adds `session` support to `sign_headers_post` / `sign_xs_post`. Please add analogous POST tests:

- `test_signing_with_session_post`: assert `sign_state` is passed to `build_payload_array` when calling `sign_headers_post(..., session=session)`.
- `test_signing_without_session_post`: assert `sign_state` is `None` when calling `sign_headers_post(..., session=None)`.

This will validate the new session behavior for POST requests as well.

Suggested implementation:

```python
    # Get the actual arguments passed to the mock
    _, kwargs = mock_crypto_processor.build_payload_array.call_args
    actual_state = kwargs.get("sign_state")

    # Verify that no state was passed (it should be None)
    assert actual_state is None


def test_signing_with_session_post(mock_crypto_processor, mock_session_manager):
    """
    Verify that signing uses the session state when provided for POST requests.
    """
    client = Xhshow()
    uri = "/api/sns/web/v1/user/posted"
    cookies = {"a1": "test_a1", "web_session": "test_session"}
    data = {"foo": "bar"}

    # Obtain a session (using the same pattern as the GET tests)
    session = mock_session_manager.get_or_create_session.return_value

    # Perform signing with a session for POST
    client.sign_headers_post(uri=uri, data=data, cookies=cookies, session=session)

    # Assert that build_payload_array was called
    mock_crypto_processor.build_payload_array.assert_called_once()

    # Get the actual arguments passed to the mock
    _, kwargs = mock_crypto_processor.build_payload_array.call_args
    actual_state = kwargs.get("sign_state")

    # Verify that session state was passed
    assert actual_state is not None


def test_signing_without_session_post(mock_crypto_processor):
    """
    Verify that signing falls back to the old method when no session is provided for POST requests.
    """
    client = Xhshow()
    uri = "/api/sns/web/v1/user/posted"
    cookies = {"a1": "test_a1", "web_session": "test_session"}
    data = {"foo": "bar"}

    # Perform signing without a session for POST
    client.sign_headers_post(uri=uri, data=data, cookies=cookies, session=None)

    # Assert that build_payload_array was called
    mock_crypto_processor.build_payload_array.assert_called_once()

    # Get the actual arguments passed to the mock
    _, kwargs = mock_crypto_processor.build_payload_array.call_args
    actual_state = kwargs.get("sign_state")

    # Verify that no state was passed (it should be None)
    assert actual_state is None


```

If the existing GET tests use a different fixture name or pattern for obtaining a session (e.g. `session = mock_session_manager.return_value` or a custom `session` fixture), adjust the `test_signing_with_session_post` signature and session initialization to match that existing convention so the tests integrate cleanly with the rest of the file.
</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.

@Cloxl
Copy link
Owner

Cloxl commented Jan 14, 2026

感谢你的贡献! 我会在空闲时间处理这个pr

@Cloxl
Copy link
Owner

Cloxl commented Jan 14, 2026

我对这个 PR 做了些修改:

主要改动:

  1. 用 ruff 统一了代码格式,修了几个小问题(未使用的变量之类的)
  2. 按 Sourcery-AI 的建议做了些优化:
    • 删了没用到的 uri_length 字段
    • 修复了导入问题
    • 启用了 sequence_value 递增逻辑
    • 补了 6 个测试用例

需要讨论的问题:

我改了 client.py:145 这行,让 uri_length 用真实 URI 长度,但发现一个问题:

  • 有 Session 时:uri_length = URI 长度(比如 /api/user = 10)
  • 无 Session 时:uri_length = content_string 长度(比如 /api/user?page=1 = 28)

这个不一致不知道是不是故意的?如果不是,我可以改成统一的逻辑。担心改签名算法会影响现有功能。

如果有问题我可以先回退这部分修改。
麻烦 review 下

@AlbertLighter

@Cloxl
Copy link
Owner

Cloxl commented Jan 14, 2026

@sourcery-ai review

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 - 我发现了 3 个问题,并给出了一些总体性的反馈:

  • client 模块中的 __all__ = ["Xhshow", "SessionManager", "SignState"] 有些误导,因为 SessionManager/SignState 并没有在 client.py 中定义或重新导出;你可以选择在 client.py 中导入并重新导出它们,或者把它们从 __all__ 中移除,仅在 xhshow.session 中保留。
  • SessionManager.get_current_state() 已经会调用 update_state(),但 update_state() 的文档字符串却建议在每次签名前都调用它;这很容易导致状态被重复递增(就像你测试中那样)——可以考虑把 update_state() 设为私有方法,或者更清晰地说明预期用法,避免调用方在调用 get_current_state() 之前再手动推进状态。
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The client module’s `__all__ = ["Xhshow", "SessionManager", "SignState"]` is misleading as `SessionManager`/`SignState` are not defined or re-exported in `client.py`; either import and re-export them there or drop them from `__all__` and keep them only in `xhshow.session`.
- `SessionManager.get_current_state()` already calls `update_state()`, but the docstring of `update_state()` suggests it should be called before each signing; this can easily lead to double increments (as in your tests)—consider making `update_state()` private or clarifying the expected usage so callers don’t manually advance state before calling `get_current_state()`.

## Individual Comments

### Comment 1
<location> `src/xhshow/session.py:26-35` </location>
<code_context>
+    def __init__(self):
+        self.page_load_timestamp: int = int(time.time() * 1000)
+        self.sequence_value: int = random.randint(15, 17)
+        self.window_props_length: int = random.randint(1000, 2000)
+
+    def update_state(self):
+        """
+        Updates the session state to simulate user activity between requests.
+
+        This method should be called before each signing operation.
+        """
+        self.sequence_value += random.randint(0, 1)
+        self.window_props_length += random.randint(1, 10)
+
+    def get_current_state(self, uri: str) -> SignState:
</code_context>

<issue_to_address>
**suggestion (bug_risk):** `window_props_length` 在长生命周期的会话中会无限增长,可能会严重偏离真实的范围。

由于 `window_props_length` 初始值在 `[1000, 2000]` 之间,并且每次调用 `update_state` 时都会加上 `random.randint(1, 10)`,在长时间或高并发的会话中,这个值会无界增长。如果这个字段是为了模拟真实客户端窗口相关的指标,可以考虑强制一个最大值(例如使用一个可配置的 `MAX_WINDOW_PROPS_LENGTH`),或者定期重新随机到一个合理范围,从而避免该值漂移到明显不真实的区间。

建议实现方式:

```python
    """
    Manages the state for a simulated user session to generate more realistic signatures.

    This class maintains counters that should persist and evolve across multiple requests
    within the same logical session.
    """

    WINDOW_PROPS_MIN_LENGTH: int = 1000
    WINDOW_PROPS_MAX_LENGTH: int = 2000

    def __init__(self):
        self.page_load_timestamp: int = int(time.time() * 1000)
        self.sequence_value: int = random.randint(15, 17)
        self.window_props_length: int = random.randint(
            self.WINDOW_PROPS_MIN_LENGTH,
            self.WINDOW_PROPS_MAX_LENGTH,
        )

```

```python
    def update_state(self):
        """
        Updates the session state to simulate user activity between requests.

        This method should be called before each signing operation.
        """
        self.sequence_value += random.randint(0, 1)
        self.window_props_length += random.randint(1, 10)

        # Prevent window_props_length from drifting unbounded by re-randomizing
        # into a realistic range once it exceeds the configured maximum.
        if self.window_props_length > self.WINDOW_PROPS_MAX_LENGTH:
            self.window_props_length = random.randint(
                self.WINDOW_PROPS_MIN_LENGTH,
                self.WINDOW_PROPS_MAX_LENGTH,
            )

```
</issue_to_address>

### Comment 2
<location> `tests/test_url_utils.py:154-163` </location>
<code_context>
                 params="invalid",  # type: ignore
             )
+
+    def test_client_sign_with_session(self):
+        client = Xhshow()
+        session = SessionManager()
+        for _ in range(10):
+            signature = client.sign_xs_get(
+                uri="/api/sns/web/v1/user_posted",
+                a1_value="test_a1_value",
+                params={"num": "30"},
+                session=session,
+            )
+            session.update_state()
+
+        assert signature.startswith("XYS_")
</code_context>

<issue_to_address>
**suggestion (testing):** 加强 `test_client_sign_with_session`,以更好地测试与会话相关的行为。

当前这个测试在每次迭代中会对会话状态进行两次推进(`sign_xs_get``get_current_state``update_state`,再加上显式的 `session.update_state()`),如果你只是想验证“带会话的签名可以工作”,这种做法会让测试意图变得不清晰。同时,它只检查最终一次签名的前缀,因此并没有真正验证与会话相关的行为。

可以考虑:
- 移除循环和额外的 `session.update_state()`,只保留一个简单的冒烟测试;或者
- 把它做成一个更强的集成测试:收集多次签名并断言在相同会话下多次调用的签名会发生变化,和/或像 `tests/test_session.py` 那样 mock `CryptoProcessor`,以断言在提供 `session` 时确实使用了 `sign_state`。
</issue_to_address>

### Comment 3
<location> `src/xhshow/client.py:147` </location>
<code_context>
+
+        sign_state = session.get_current_state(uri) if session else None
+
+        payload_array = self.crypto_processor.build_payload_array(
+            d_value, a1_value, xsec_appid, content_string, timestamp, sign_state=sign_state
         )
</code_context>

<issue_to_address>
**issue (complexity):** 建议把构建底层 x3 签名的逻辑移回 `CryptoProcessor` 中的一个高层封装方法里,让 `Xhshow.sign_xs` 只负责组织请求/会话相关的数据。

你可以保留现在的会话感知行为,同时通过重新引入 `CryptoProcessor` 中更高层的抽象来避免在 `Xhshow.sign_xs` 中暴露底层加密细节。

### 1. 在 `CryptoProcessor` 中添加一个高层的 x3 构建方法`core/crypto.py`(或者 `CryptoProcessor` 所在的位置)中新增一个方法,封装 payload 数组 + xor + base64 的流水线,并接受可选的会话状态:

```python
class CryptoProcessor:
    # existing code...

    def build_x3_signature(
        self,
        d_value: str,
        a1_value: str,
        xsec_appid: str,
        content_string: str,
        timestamp: float | None = None,
        sign_state: SignState | None = None,
    ) -> str:
        payload_array = self.build_payload_array(
            d_value,
            a1_value,
            xsec_appid,
            content_string,
            timestamp,
            sign_state=sign_state,
        )
        xor_result = self.bit_ops.xor_transform_array(payload_array)
        return self.b64encoder.encode_x3(xor_result[:124])
```

这样既能保留新的会话感知逻辑,又能把底层细节从 `client.py` 中隐藏起来。

### 2. 将 `Xhshow.sign_xs` 简化回高层的编排逻辑

然后在 `Xhshow.sign_xs` 中,改为委托给这个新 helper,而不是在其中直接编排 `build_payload_array`/`xor`/`encode_x3````python
@validate_signature_params
def sign_xs(
    self,
    method: Literal["GET", "POST"],
    uri: str,
    a1_value: str,
    xsec_appid: str = "xhs-pc-web",
    payload: dict[str, Any] | None = None,
    timestamp: float | None = None,
    session: SessionManager | None = None,
) -> str:
    uri = extract_uri(uri)
    content_string = self._build_content_string(method, uri, payload)
    d_value = self._generate_d_value(content_string)

    sign_state = session.get_current_state(uri) if session else None
    x3_signature = self.crypto_processor.build_x3_signature(
        d_value,
        a1_value,
        xsec_appid,
        content_string,
        timestamp,
        sign_state=sign_state,
    )

    signature_data = self.crypto_processor.config.SIGNATURE_DATA_TEMPLATE.copy()
    signature_data["x3"] = self.crypto_processor.config.X3_PREFIX + x3_signature

    return self.crypto_processor.config.XYS_PREFIX + self.crypto_processor.b64encoder.encode(
        json.dumps(signature_data, separators=(",", ":"), ensure_ascii=False)
    )
```

这可以恢复之前的分层结构:

- `Xhshow` 负责请求层面的逻辑(URI、method、payload、session 查找)。
- `CryptoProcessor` 负责加密流水线,包括新的会话感知行为。

功能保持不变(包括 `sign_xs_get``sign_xs_post``sign_headers*` 中的会话传递),但复杂度被重新聚合回加密层中。
</issue_to_address>

Sourcery 对开源项目是免费的——如果你喜欢我们的代码审查,欢迎分享 ✨
帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据你的反馈改进后续的审查。
Original comment in English

Hey - I've found 3 issues, and left some high level feedback:

  • The client module’s __all__ = ["Xhshow", "SessionManager", "SignState"] is misleading as SessionManager/SignState are not defined or re-exported in client.py; either import and re-export them there or drop them from __all__ and keep them only in xhshow.session.
  • SessionManager.get_current_state() already calls update_state(), but the docstring of update_state() suggests it should be called before each signing; this can easily lead to double increments (as in your tests)—consider making update_state() private or clarifying the expected usage so callers don’t manually advance state before calling get_current_state().
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The client module’s `__all__ = ["Xhshow", "SessionManager", "SignState"]` is misleading as `SessionManager`/`SignState` are not defined or re-exported in `client.py`; either import and re-export them there or drop them from `__all__` and keep them only in `xhshow.session`.
- `SessionManager.get_current_state()` already calls `update_state()`, but the docstring of `update_state()` suggests it should be called before each signing; this can easily lead to double increments (as in your tests)—consider making `update_state()` private or clarifying the expected usage so callers don’t manually advance state before calling `get_current_state()`.

## Individual Comments

### Comment 1
<location> `src/xhshow/session.py:26-35` </location>
<code_context>
+    def __init__(self):
+        self.page_load_timestamp: int = int(time.time() * 1000)
+        self.sequence_value: int = random.randint(15, 17)
+        self.window_props_length: int = random.randint(1000, 2000)
+
+    def update_state(self):
+        """
+        Updates the session state to simulate user activity between requests.
+
+        This method should be called before each signing operation.
+        """
+        self.sequence_value += random.randint(0, 1)
+        self.window_props_length += random.randint(1, 10)
+
+    def get_current_state(self, uri: str) -> SignState:
</code_context>

<issue_to_address>
**suggestion (bug_risk):** `window_props_length` grows unbounded over a long-lived session, which may drift far from realistic ranges.

Since `window_props_length` starts in `[1000, 2000]` and is incremented by `random.randint(1, 10)` on every `update_state` call, it will grow without bound in long-lived or high-traffic sessions. If this field is meant to mimic real client window metrics, consider enforcing a maximum (e.g., using a configurable `MAX_WINDOW_PROPS_LENGTH`) or periodically re-randomizing it into a realistic range so the value doesn’t drift into clearly synthetic territory.

Suggested implementation:

```python
    """
    Manages the state for a simulated user session to generate more realistic signatures.

    This class maintains counters that should persist and evolve across multiple requests
    within the same logical session.
    """

    WINDOW_PROPS_MIN_LENGTH: int = 1000
    WINDOW_PROPS_MAX_LENGTH: int = 2000

    def __init__(self):
        self.page_load_timestamp: int = int(time.time() * 1000)
        self.sequence_value: int = random.randint(15, 17)
        self.window_props_length: int = random.randint(
            self.WINDOW_PROPS_MIN_LENGTH,
            self.WINDOW_PROPS_MAX_LENGTH,
        )

```

```python
    def update_state(self):
        """
        Updates the session state to simulate user activity between requests.

        This method should be called before each signing operation.
        """
        self.sequence_value += random.randint(0, 1)
        self.window_props_length += random.randint(1, 10)

        # Prevent window_props_length from drifting unbounded by re-randomizing
        # into a realistic range once it exceeds the configured maximum.
        if self.window_props_length > self.WINDOW_PROPS_MAX_LENGTH:
            self.window_props_length = random.randint(
                self.WINDOW_PROPS_MIN_LENGTH,
                self.WINDOW_PROPS_MAX_LENGTH,
            )

```
</issue_to_address>

### Comment 2
<location> `tests/test_url_utils.py:154-163` </location>
<code_context>
                 params="invalid",  # type: ignore
             )
+
+    def test_client_sign_with_session(self):
+        client = Xhshow()
+        session = SessionManager()
+        for _ in range(10):
+            signature = client.sign_xs_get(
+                uri="/api/sns/web/v1/user_posted",
+                a1_value="test_a1_value",
+                params={"num": "30"},
+                session=session,
+            )
+            session.update_state()
+
+        assert signature.startswith("XYS_")
</code_context>

<issue_to_address>
**suggestion (testing):** Strengthen `test_client_sign_with_session` to better exercise the session-aware behavior.

This test advances the session state twice per iteration (`sign_xs_get``get_current_state``update_state`, plus the explicit `session.update_state()`), which obscures the intent if you only want to verify “signing works with a session.” It also only checks the prefix of the final signature, so it doesn’t actually validate session-aware behavior.

Consider either:
- Removing the loop and extra `session.update_state()` for a simple smoke test, or
- Making it a stronger integration check by capturing multiple signatures and asserting they change across calls with the same session and/or mocking `CryptoProcessor` (like in `tests/test_session.py`) to assert that `sign_state` is used when `session` is provided.
</issue_to_address>

### Comment 3
<location> `src/xhshow/client.py:147` </location>
<code_context>
+
+        sign_state = session.get_current_state(uri) if session else None
+
+        payload_array = self.crypto_processor.build_payload_array(
+            d_value, a1_value, xsec_appid, content_string, timestamp, sign_state=sign_state
         )
</code_context>

<issue_to_address>
**issue (complexity):** Consider moving the low-level x3 signature construction back into a high-level helper on CryptoProcessor so Xhshow.sign_xs only orchestrates request/session data.

You can keep the new session-aware behavior but still avoid leaking low‑level crypto details into `Xhshow.sign_xs` by re‑introducing a higher‑level abstraction in `CryptoProcessor`.

### 1. Add a high-level x3 builder to `CryptoProcessor`

In `core/crypto.py` (or wherever `CryptoProcessor` lives), add a method that encapsulates the payload array + xor + base64 pipeline and accepts the optional session state:

```python
class CryptoProcessor:
    # existing code...

    def build_x3_signature(
        self,
        d_value: str,
        a1_value: str,
        xsec_appid: str,
        content_string: str,
        timestamp: float | None = None,
        sign_state: SignState | None = None,
    ) -> str:
        payload_array = self.build_payload_array(
            d_value,
            a1_value,
            xsec_appid,
            content_string,
            timestamp,
            sign_state=sign_state,
        )
        xor_result = self.bit_ops.xor_transform_array(payload_array)
        return self.b64encoder.encode_x3(xor_result[:124])
```

This keeps all the new session-aware logic intact, but hides the low‑level details from `client.py`.

### 2. Simplify `Xhshow.sign_xs` back to high-level orchestration

Then, in `Xhshow.sign_xs`, delegate to the new helper instead of orchestrating `build_payload_array`/`xor`/`encode_x3` directly:

```python
@validate_signature_params
def sign_xs(
    self,
    method: Literal["GET", "POST"],
    uri: str,
    a1_value: str,
    xsec_appid: str = "xhs-pc-web",
    payload: dict[str, Any] | None = None,
    timestamp: float | None = None,
    session: SessionManager | None = None,
) -> str:
    uri = extract_uri(uri)
    content_string = self._build_content_string(method, uri, payload)
    d_value = self._generate_d_value(content_string)

    sign_state = session.get_current_state(uri) if session else None
    x3_signature = self.crypto_processor.build_x3_signature(
        d_value,
        a1_value,
        xsec_appid,
        content_string,
        timestamp,
        sign_state=sign_state,
    )

    signature_data = self.crypto_processor.config.SIGNATURE_DATA_TEMPLATE.copy()
    signature_data["x3"] = self.crypto_processor.config.X3_PREFIX + x3_signature

    return self.crypto_processor.config.XYS_PREFIX + self.crypto_processor.b64encoder.encode(
        json.dumps(signature_data, separators=(",", ":"), ensure_ascii=False)
    )
```

This restores the previous layering:

- `Xhshow` handles request-level concerns (URI, method, payload, session lookup).
- `CryptoProcessor` owns the crypto pipeline, including the new session-aware behavior.

Functionality (including session plumbing in `sign_xs_get`, `sign_xs_post`, and `sign_headers*`) remains unchanged, but the complexity is localized back into the crypto layer.
</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.

@Cloxl Cloxl merged commit 8b24b0a into Cloxl:master Jan 15, 2026
4 checks passed
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