gateway_billing_block.go 3.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. package service
  2. import (
  3. "crypto/sha256"
  4. "encoding/hex"
  5. "fmt"
  6. "github.com/tidwall/gjson"
  7. )
  8. // fingerprintSalt 是计算 cc_version 后缀指纹的盐值。
  9. //
  10. // 来源:与 Parrot src/transform/cc_mimicry.py 的 FINGERPRINT_SALT 完全一致;
  11. // 这是真实 Claude Code CLI 抓包推导出的常量,改动会导致 fp 与 CLI 不一致,
  12. // 进一步触发 Anthropic 的第三方检测。
  13. const fingerprintSalt = "59cf53e54c78"
  14. // computeClaudeCodeFingerprint 复刻真实 Claude Code CLI 的 cc_version 指纹算法:
  15. //
  16. // 1. 取 messages 中第一条 role=user 的纯文本(首块 text)
  17. // 2. 取该文本的第 4、7、20 字符(不足以 '0' 补齐)
  18. // 3. SHA256(SALT + chars + cc_version) 取 hex 前 3 字符
  19. //
  20. // 算法来自 Parrot src/transform/cc_mimicry.py:compute_fingerprint,与官方 CLI 字节对齐。
  21. // 任何偏差都会导致 cc_version=X.Y.Z.{fp} 在上游侧与真实 CLI 不一致。
  22. func computeClaudeCodeFingerprint(body []byte, version string) string {
  23. firstText := extractFirstUserText(body)
  24. indices := []int{4, 7, 20}
  25. chars := make([]byte, 0, 3)
  26. for _, i := range indices {
  27. if i < len(firstText) {
  28. chars = append(chars, firstText[i])
  29. } else {
  30. chars = append(chars, '0')
  31. }
  32. }
  33. sum := sha256.Sum256([]byte(fingerprintSalt + string(chars) + version))
  34. return hex.EncodeToString(sum[:])[:3]
  35. }
  36. // extractFirstUserText 提取 messages 中第一条 user 消息的首段 text 内容。
  37. // 兼容 string 和 []block 两种 content 格式。
  38. func extractFirstUserText(body []byte) string {
  39. messages := gjson.GetBytes(body, "messages")
  40. if !messages.IsArray() {
  41. return ""
  42. }
  43. first := ""
  44. messages.ForEach(func(_, msg gjson.Result) bool {
  45. if msg.Get("role").String() != "user" {
  46. return true
  47. }
  48. content := msg.Get("content")
  49. if content.Type == gjson.String {
  50. first = content.String()
  51. return false
  52. }
  53. if content.IsArray() {
  54. content.ForEach(func(_, block gjson.Result) bool {
  55. if block.Get("type").String() == "text" {
  56. first = block.Get("text").String()
  57. return false
  58. }
  59. return true
  60. })
  61. return false
  62. }
  63. return false
  64. })
  65. return first
  66. }
  67. // buildBillingAttributionText 构造 system 数组的 billing attribution 文本。
  68. //
  69. // 形态对齐真实 Claude Code CLI:
  70. //
  71. // x-anthropic-billing-header: cc_version=2.1.161.{fp}; cc_entrypoint=cli;
  72. //
  73. // 注意:新版 Claude Code CLI 已不再发送 cch=... 签名字段(见 issue #3358)。我们
  74. // 随之去掉了 cch 段——继续注入它反而会让伪装请求偏离真实 CLI 流量。cc_version +
  75. // cc_entrypoint=cli 仍保留:它们是客户端识别(claude_code_validator)与 Anthropic
  76. // 第一方判定都依赖的稳定信号。
  77. //
  78. // 此 block 不带 cache_control(与真实 CLI 一致;cache breakpoint 由后续的
  79. // Claude Code prompt block 承担)。
  80. func buildBillingAttributionText(body []byte, cliVersion string) (string, error) {
  81. if cliVersion == "" {
  82. return "", fmt.Errorf("cliVersion required")
  83. }
  84. fp := computeClaudeCodeFingerprint(body, cliVersion)
  85. return fmt.Sprintf(
  86. "x-anthropic-billing-header: cc_version=%s.%s; cc_entrypoint=cli;",
  87. cliVersion, fp,
  88. ), nil
  89. }